WIP: Outfit viewers on pet type Rainbow Pool page
Now that we have such a convenient lil outfit viewer component we built for the item page preview, it's easy peasy to drop it in here too! And it's all nice and lightweight, since in this case it's basically just. image tags, with some supporting enhancements. Anyway, this page has no actual useful styles of its own yet. Gonna make it look nice and such!
This commit is contained in:
parent
a1d6961249
commit
734b7fba1d
9 changed files with 198 additions and 106 deletions
110
app/assets/stylesheets/application/outfit-viewer.sass
Normal file
110
app/assets/stylesheets/application/outfit-viewer.sass
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
@import "../partials/clean/constants"
|
||||||
|
|
||||||
|
// When loading, fade in the loading spinner after a brief delay. We only apply
|
||||||
|
// the delay here, not on the base styles, because fading *out* on load should
|
||||||
|
// be instant.
|
||||||
|
//
|
||||||
|
// This is implemented as a mixin, so that the item page can leverage the same
|
||||||
|
// loading state when loading a new preview altogether. Once CSS container
|
||||||
|
// style queries gain wider support, maybe use that instead.
|
||||||
|
=outfit-viewer-loading
|
||||||
|
cursor: wait
|
||||||
|
|
||||||
|
.loading-indicator
|
||||||
|
opacity: 1
|
||||||
|
transition-delay: 2s
|
||||||
|
|
||||||
|
// If the outfit *starts* in loading state, still delay the fade-in.
|
||||||
|
@starting-style
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
outfit-viewer
|
||||||
|
display: block
|
||||||
|
position: relative
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
// These are default widths, expected to often be overridden.
|
||||||
|
width: 300px
|
||||||
|
height: 300px
|
||||||
|
|
||||||
|
// There's no useful text in here, but double-clicking the play/pause
|
||||||
|
// button can cause a weird selection state. Disable text selection.
|
||||||
|
user-select: none
|
||||||
|
-webkit-user-select: none
|
||||||
|
|
||||||
|
outfit-layer
|
||||||
|
display: block
|
||||||
|
position: absolute
|
||||||
|
inset: 0
|
||||||
|
|
||||||
|
// We disable pointer-events most importantly for the iframes, which
|
||||||
|
// will ignore our `cursor: wait` and show a plain cursor for the
|
||||||
|
// inside of its own document. But also, the context menus for these
|
||||||
|
// elements are kinda actively misleading, too!
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
|
img, iframe
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
|
||||||
|
.loading-indicator
|
||||||
|
position: absolute
|
||||||
|
z-index: 1000
|
||||||
|
bottom: 0px
|
||||||
|
right: 4px
|
||||||
|
padding: 8px
|
||||||
|
background: radial-gradient(circle closest-side, white 45%, #ffffff00)
|
||||||
|
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
.play-pause-button
|
||||||
|
position: absolute
|
||||||
|
z-index: 1001
|
||||||
|
left: 8px
|
||||||
|
bottom: 8px
|
||||||
|
display: none
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
color: white
|
||||||
|
background: rgba(0, 0, 0, 0.64)
|
||||||
|
width: 2.5em
|
||||||
|
height: 2.5em
|
||||||
|
border-radius: 100%
|
||||||
|
border: 2px solid transparent
|
||||||
|
transition: all .25s
|
||||||
|
|
||||||
|
.playing-label, .paused-label
|
||||||
|
display: none
|
||||||
|
width: 1em
|
||||||
|
height: 1em
|
||||||
|
|
||||||
|
.play-pause-toggle
|
||||||
|
// Visually hidden
|
||||||
|
clip: rect(0 0 0 0)
|
||||||
|
clip-path: inset(50%)
|
||||||
|
height: 1px
|
||||||
|
overflow: hidden
|
||||||
|
position: absolute
|
||||||
|
white-space: nowrap
|
||||||
|
width: 1px
|
||||||
|
|
||||||
|
&:checked ~ .playing-label
|
||||||
|
display: block
|
||||||
|
|
||||||
|
&:not(:checked) ~ .paused-label
|
||||||
|
display: block
|
||||||
|
|
||||||
|
&:hover, &:has(.play-pause-toggle:focus)
|
||||||
|
border: 2px solid $module-border-color
|
||||||
|
background: $module-bg-color
|
||||||
|
color: $text-color
|
||||||
|
|
||||||
|
&:has(.play-pause-toggle:active)
|
||||||
|
transform: translateY(2px)
|
||||||
|
|
||||||
|
&:has(outfit-layer:state(has-animations))
|
||||||
|
.play-pause-button
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
&:has(outfit-layer:state(loading))
|
||||||
|
+outfit-viewer-loading
|
|
@ -2,6 +2,8 @@
|
||||||
@import "../partials/clean/mixins"
|
@import "../partials/clean/mixins"
|
||||||
@import "../partials/item_header"
|
@import "../partials/item_header"
|
||||||
|
|
||||||
|
@import "../application/outfit-viewer"
|
||||||
|
|
||||||
#container
|
#container
|
||||||
width: 900px // A bit more generous to the preview area!
|
width: 900px // A bit more generous to the preview area!
|
||||||
|
|
||||||
|
@ -78,93 +80,10 @@
|
||||||
width: var(--natural-width)
|
width: var(--natural-width)
|
||||||
|
|
||||||
outfit-viewer
|
outfit-viewer
|
||||||
display: block
|
|
||||||
position: relative
|
|
||||||
width: 300px
|
width: 300px
|
||||||
height: 300px
|
height: 300px
|
||||||
border: 1px solid $module-border-color
|
border: 1px solid $module-border-color
|
||||||
border-radius: 1em
|
border-radius: 1em
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
// There's no useful text in here, but double-clicking the play/pause
|
|
||||||
// button can cause a weird selection state. Disable text selection.
|
|
||||||
user-select: none
|
|
||||||
-webkit-user-select: none
|
|
||||||
|
|
||||||
outfit-layer
|
|
||||||
display: block
|
|
||||||
position: absolute
|
|
||||||
inset: 0
|
|
||||||
|
|
||||||
// We disable pointer-events most importantly for the iframes, which
|
|
||||||
// will ignore our `cursor: wait` and show a plain cursor for the
|
|
||||||
// inside of its own document. But also, the context menus for these
|
|
||||||
// elements are kinda actively misleading, too!
|
|
||||||
pointer-events: none
|
|
||||||
|
|
||||||
img, iframe
|
|
||||||
width: 100%
|
|
||||||
height: 100%
|
|
||||||
|
|
||||||
.loading-indicator
|
|
||||||
position: absolute
|
|
||||||
z-index: 1000
|
|
||||||
bottom: 0px
|
|
||||||
right: 4px
|
|
||||||
padding: 8px
|
|
||||||
background: radial-gradient(circle closest-side, white 45%, #ffffff00)
|
|
||||||
|
|
||||||
opacity: 0
|
|
||||||
transition: opacity .5s
|
|
||||||
|
|
||||||
.play-pause-button
|
|
||||||
position: absolute
|
|
||||||
z-index: 1001
|
|
||||||
left: 8px
|
|
||||||
bottom: 8px
|
|
||||||
display: none
|
|
||||||
align-items: center
|
|
||||||
justify-content: center
|
|
||||||
color: white
|
|
||||||
background: rgba(0, 0, 0, 0.64)
|
|
||||||
width: 2.5em
|
|
||||||
height: 2.5em
|
|
||||||
border-radius: 100%
|
|
||||||
border: 2px solid transparent
|
|
||||||
transition: all .25s
|
|
||||||
|
|
||||||
.playing-label, .paused-label
|
|
||||||
display: none
|
|
||||||
width: 1em
|
|
||||||
height: 1em
|
|
||||||
|
|
||||||
.play-pause-toggle
|
|
||||||
// Visually hidden
|
|
||||||
clip: rect(0 0 0 0)
|
|
||||||
clip-path: inset(50%)
|
|
||||||
height: 1px
|
|
||||||
overflow: hidden
|
|
||||||
position: absolute
|
|
||||||
white-space: nowrap
|
|
||||||
width: 1px
|
|
||||||
|
|
||||||
&:checked ~ .playing-label
|
|
||||||
display: block
|
|
||||||
|
|
||||||
&:not(:checked) ~ .paused-label
|
|
||||||
display: block
|
|
||||||
|
|
||||||
&:hover, &:has(.play-pause-toggle:focus)
|
|
||||||
border: 2px solid $module-border-color
|
|
||||||
background: $module-bg-color
|
|
||||||
color: $text-color
|
|
||||||
|
|
||||||
&:has(.play-pause-toggle:active)
|
|
||||||
transform: translateY(2px)
|
|
||||||
|
|
||||||
&:has(outfit-layer:state(has-animations))
|
|
||||||
.play-pause-button
|
|
||||||
display: flex
|
|
||||||
|
|
||||||
.error-indicator
|
.error-indicator
|
||||||
font-size: 85%
|
font-size: 85%
|
||||||
|
@ -179,16 +98,8 @@ outfit-viewer
|
||||||
//
|
//
|
||||||
// We only apply the delay here, not on the base styles, because fading
|
// We only apply the delay here, not on the base styles, because fading
|
||||||
// *out* on load should be instant.
|
// *out* on load should be instant.
|
||||||
#item-preview[busy] outfit-viewer, outfit-viewer:has(outfit-layer:state(loading))
|
#item-preview[busy] outfit-viewer
|
||||||
cursor: wait
|
+outfit-viewer-loading
|
||||||
|
|
||||||
.loading-indicator
|
|
||||||
opacity: 1
|
|
||||||
transition-delay: 2s
|
|
||||||
|
|
||||||
// If the outfit *starts* in loading state, still delay the fade-in.
|
|
||||||
@starting-style
|
|
||||||
opacity: 0
|
|
||||||
|
|
||||||
#item-preview:has(outfit-layer:state(error))
|
#item-preview:has(outfit-layer:state(error))
|
||||||
outfit-viewer
|
outfit-viewer
|
||||||
|
|
|
@ -3,7 +3,9 @@ class PetTypesController < ApplicationController
|
||||||
@pet_type = find_pet_type
|
@pet_type = find_pet_type
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { render }
|
format.html do
|
||||||
|
@pet_states = group_pet_states @pet_type.pet_states
|
||||||
|
end
|
||||||
format.json { render json: @pet_type }
|
format.json { render json: @pet_type }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -25,4 +27,24 @@ class PetTypesController < ApplicationController
|
||||||
raise "expected params: species_id and color_id, or name"
|
raise "expected params: species_id and color_id, or name"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The `canonical` pet states are the main ones we want to show: the most
|
||||||
|
# canonical state for each pose. The `other` pet states are, the others!
|
||||||
|
#
|
||||||
|
# We put *all* the UNKNOWN pet states into `other`, unless it is the only
|
||||||
|
# pose available, in which case one will be in `canonical`.
|
||||||
|
def group_pet_states(pet_states)
|
||||||
|
pose_groups = pet_states.emotion_order.group_by(&:pose)
|
||||||
|
unknowns = if pose_groups.keys != ["UNKNOWN"]
|
||||||
|
pose_groups.delete("UNKNOWN") { [] }
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
canonical = pose_groups.values.map(&:first).sort_by(&:pose)
|
||||||
|
posed_others = pose_groups.values.map { |l| l.drop(1) }.flatten(1)
|
||||||
|
other = (posed_others + unknowns).sort_by(&:pose)
|
||||||
|
|
||||||
|
{canonical:, other:}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -69,5 +69,17 @@ module OutfitsHelper
|
||||||
options = {:spellcheck => false, :id => nil}.merge(options)
|
options = {:spellcheck => false, :id => nil}.merge(options)
|
||||||
text_field_tag 'name', nil, options
|
text_field_tag 'name', nil, options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def outfit_viewer(outfit_or_options)
|
||||||
|
outfit = if outfit_or_options.is_a? Hash
|
||||||
|
Outfit.new(outfit_or_options)
|
||||||
|
elsif outfit_or_options.is_a? Outfit
|
||||||
|
outfit_or_options
|
||||||
|
else
|
||||||
|
raise TypeError, "must be an outfit or hash of options to create one"
|
||||||
|
end
|
||||||
|
|
||||||
|
render partial: "outfit_viewer", locals: {outfit:}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
22
app/helpers/pet_states_helper.rb
Normal file
22
app/helpers/pet_states_helper.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
module PetStatesHelper
|
||||||
|
def pose_name(pose)
|
||||||
|
case pose
|
||||||
|
when "HAPPY_FEM"
|
||||||
|
"Happy (Feminine)"
|
||||||
|
when "HAPPY_MASC"
|
||||||
|
"Happy (Masculine)"
|
||||||
|
when "SAD_FEM"
|
||||||
|
"Sad (Feminine)"
|
||||||
|
when "SAD_MASC"
|
||||||
|
"Sad (Masculine)"
|
||||||
|
when "SICK_FEM"
|
||||||
|
"Sick (Feminine)"
|
||||||
|
when "SICK_MASC"
|
||||||
|
"Sick (Masculine)"
|
||||||
|
when "UNCONVERTED"
|
||||||
|
"Unconverted"
|
||||||
|
else
|
||||||
|
"(Unknown)"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,6 +21,6 @@
|
||||||
- if swf_asset.canvas_movie?
|
- if swf_asset.canvas_movie?
|
||||||
%iframe{src: swf_asset_path(swf_asset, playing: outfit_viewer_is_playing ? true : nil)}
|
%iframe{src: swf_asset_path(swf_asset, playing: outfit_viewer_is_playing ? true : nil)}
|
||||||
- elsif swf_asset.image_url.present?
|
- elsif swf_asset.image_url.present?
|
||||||
= image_tag swf_asset.image_url, alt: ""
|
= image_tag swf_asset.image_url, alt: "", loading: "lazy"
|
||||||
- else
|
- else
|
||||||
/ No movie or image available for SWF asset: #{swf_asset.url}
|
/ No movie or image available for SWF asset: #{swf_asset.url}
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
= turbo_frame_tag "item-preview" do
|
= turbo_frame_tag "item-preview" do
|
||||||
.preview-area
|
.preview-area
|
||||||
= render partial: "outfit_viewer", locals: {outfit: @preview_outfit}
|
= outfit_viewer @preview_outfit
|
||||||
.error-indicator
|
.error-indicator
|
||||||
💥 We couldn't load all of this outfit. Try again?
|
💥 We couldn't load all of this outfit. Try again?
|
||||||
= link_to wardrobe_path(params: @preview_outfit.wardrobe_params),
|
= link_to wardrobe_path(params: @preview_outfit.wardrobe_params),
|
||||||
|
@ -122,6 +122,8 @@
|
||||||
|
|
||||||
- content_for :stylesheets do
|
- content_for :stylesheets do
|
||||||
= stylesheet_link_tag "application/hanger-spinner"
|
= stylesheet_link_tag "application/hanger-spinner"
|
||||||
|
-# This is imported into items/show directly, to gain access to its mixins.
|
||||||
|
-# = stylesheet_link_tag "application/outfit-viewer"
|
||||||
= page_stylesheet_link_tag "layouts/items"
|
= page_stylesheet_link_tag "layouts/items"
|
||||||
= page_stylesheet_link_tag "items/show"
|
= page_stylesheet_link_tag "items/show"
|
||||||
|
|
||||||
|
|
6
app/views/pet_states/_pet_state.html.haml
Normal file
6
app/views/pet_states/_pet_state.html.haml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
%li
|
||||||
|
= outfit_viewer pet_state:
|
||||||
|
= pose_name pet_state.pose
|
||||||
|
(#{pet_state.id})
|
||||||
|
- if pet_state.glitched?
|
||||||
|
👾
|
|
@ -1,11 +1,18 @@
|
||||||
- title "#{@pet_type.human_name}"
|
- title "#{@pet_type.human_name}"
|
||||||
|
- use_responsive_design
|
||||||
|
|
||||||
%dl
|
%ul
|
||||||
%dt Happy
|
= render @pet_states[:canonical]
|
||||||
%dd= pet_type_image @pet_type, :happy, :full
|
|
||||||
%dt Sad
|
- if @pet_states[:other].present?
|
||||||
%dd= pet_type_image @pet_type, :sad, :full
|
%details
|
||||||
%dt Angry
|
%summary Other
|
||||||
%dd= pet_type_image @pet_type, :angry, :full
|
%ul
|
||||||
%dt Ill
|
= render @pet_states[:other]
|
||||||
%dd= pet_type_image @pet_type, :ill, :full
|
|
||||||
|
- content_for :stylesheets do
|
||||||
|
= stylesheet_link_tag "application/hanger-spinner"
|
||||||
|
= stylesheet_link_tag "application/outfit-viewer"
|
||||||
|
|
||||||
|
- content_for :javascripts do
|
||||||
|
= javascript_include_tag "outfit-viewer", async: true
|
||||||
|
|
Loading…
Reference in a new issue