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:
Emi Matchu 2024-09-26 18:20:05 -07:00
parent a1d6961249
commit 734b7fba1d
9 changed files with 198 additions and 106 deletions

View 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

View file

@ -2,6 +2,8 @@
@import "../partials/clean/mixins"
@import "../partials/item_header"
@import "../application/outfit-viewer"
#container
width: 900px // A bit more generous to the preview area!
@ -78,93 +80,10 @@
width: var(--natural-width)
outfit-viewer
display: block
position: relative
width: 300px
height: 300px
border: 1px solid $module-border-color
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
font-size: 85%
@ -179,16 +98,8 @@ outfit-viewer
//
// We only apply the delay here, not on the base styles, because fading
// *out* on load should be instant.
#item-preview[busy] outfit-viewer, outfit-viewer:has(outfit-layer:state(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
#item-preview[busy] outfit-viewer
+outfit-viewer-loading
#item-preview:has(outfit-layer:state(error))
outfit-viewer

View file

@ -3,7 +3,9 @@ class PetTypesController < ApplicationController
@pet_type = find_pet_type
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 }
end
end
@ -25,4 +27,24 @@ class PetTypesController < ApplicationController
raise "expected params: species_id and color_id, or name"
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

View file

@ -69,5 +69,17 @@ module OutfitsHelper
options = {:spellcheck => false, :id => nil}.merge(options)
text_field_tag 'name', nil, options
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

View 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

View file

@ -21,6 +21,6 @@
- if swf_asset.canvas_movie?
%iframe{src: swf_asset_path(swf_asset, playing: outfit_viewer_is_playing ? true : nil)}
- elsif swf_asset.image_url.present?
= image_tag swf_asset.image_url, alt: ""
= image_tag swf_asset.image_url, alt: "", loading: "lazy"
- else
/ No movie or image available for SWF asset: #{swf_asset.url}

View file

@ -16,7 +16,7 @@
= turbo_frame_tag "item-preview" do
.preview-area
= render partial: "outfit_viewer", locals: {outfit: @preview_outfit}
= outfit_viewer @preview_outfit
.error-indicator
💥 We couldn't load all of this outfit. Try again?
= link_to wardrobe_path(params: @preview_outfit.wardrobe_params),
@ -122,6 +122,8 @@
- content_for :stylesheets do
= 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 "items/show"

View file

@ -0,0 +1,6 @@
%li
= outfit_viewer pet_state:
= pose_name pet_state.pose
(#{pet_state.id})
- if pet_state.glitched?
👾

View file

@ -1,11 +1,18 @@
- title "#{@pet_type.human_name}"
- use_responsive_design
%dl
%dt Happy
%dd= pet_type_image @pet_type, :happy, :full
%dt Sad
%dd= pet_type_image @pet_type, :sad, :full
%dt Angry
%dd= pet_type_image @pet_type, :angry, :full
%dt Ill
%dd= pet_type_image @pet_type, :ill, :full
%ul
= render @pet_states[:canonical]
- if @pet_states[:other].present?
%details
%summary Other
%ul
= render @pet_states[:other]
- 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