From 734b7fba1dff5e6a9ad4efd0363b3da5dc95f369 Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Thu, 26 Sep 2024 18:20:05 -0700 Subject: [PATCH] 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! --- .../application/outfit-viewer.sass | 110 ++++++++++++++++++ app/assets/stylesheets/items/show.sass | 97 +-------------- app/controllers/pet_types_controller.rb | 24 +++- app/helpers/outfits_helper.rb | 12 ++ app/helpers/pet_states_helper.rb | 22 ++++ .../_outfit_viewer.html.haml | 4 +- app/views/items/show.html.haml | 4 +- app/views/pet_states/_pet_state.html.haml | 6 + app/views/pet_types/show.html.haml | 25 ++-- 9 files changed, 198 insertions(+), 106 deletions(-) create mode 100644 app/assets/stylesheets/application/outfit-viewer.sass create mode 100644 app/helpers/pet_states_helper.rb rename app/views/{items => application}/_outfit_viewer.html.haml (92%) create mode 100644 app/views/pet_states/_pet_state.html.haml diff --git a/app/assets/stylesheets/application/outfit-viewer.sass b/app/assets/stylesheets/application/outfit-viewer.sass new file mode 100644 index 00000000..974ab9c1 --- /dev/null +++ b/app/assets/stylesheets/application/outfit-viewer.sass @@ -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 diff --git a/app/assets/stylesheets/items/show.sass b/app/assets/stylesheets/items/show.sass index 77ae18a8..5aa5fc60 100644 --- a/app/assets/stylesheets/items/show.sass +++ b/app/assets/stylesheets/items/show.sass @@ -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 diff --git a/app/controllers/pet_types_controller.rb b/app/controllers/pet_types_controller.rb index 5a468c2a..a8b4bdf3 100644 --- a/app/controllers/pet_types_controller.rb +++ b/app/controllers/pet_types_controller.rb @@ -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 diff --git a/app/helpers/outfits_helper.rb b/app/helpers/outfits_helper.rb index ab237ab8..86fc57c7 100644 --- a/app/helpers/outfits_helper.rb +++ b/app/helpers/outfits_helper.rb @@ -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 diff --git a/app/helpers/pet_states_helper.rb b/app/helpers/pet_states_helper.rb new file mode 100644 index 00000000..c06be602 --- /dev/null +++ b/app/helpers/pet_states_helper.rb @@ -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 diff --git a/app/views/items/_outfit_viewer.html.haml b/app/views/application/_outfit_viewer.html.haml similarity index 92% rename from app/views/items/_outfit_viewer.html.haml rename to app/views/application/_outfit_viewer.html.haml index e03c2c3b..c9793c5a 100644 --- a/app/views/items/_outfit_viewer.html.haml +++ b/app/views/application/_outfit_viewer.html.haml @@ -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} \ No newline at end of file + / No movie or image available for SWF asset: #{swf_asset.url} diff --git a/app/views/items/show.html.haml b/app/views/items/show.html.haml index 6af3cecb..771ab201 100644 --- a/app/views/items/show.html.haml +++ b/app/views/items/show.html.haml @@ -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" diff --git a/app/views/pet_states/_pet_state.html.haml b/app/views/pet_states/_pet_state.html.haml new file mode 100644 index 00000000..71af1c2e --- /dev/null +++ b/app/views/pet_states/_pet_state.html.haml @@ -0,0 +1,6 @@ +%li + = outfit_viewer pet_state: + = pose_name pet_state.pose + (#{pet_state.id}) + - if pet_state.glitched? + 👾 diff --git a/app/views/pet_types/show.html.haml b/app/views/pet_types/show.html.haml index 66bebc38..d04ef993 100644 --- a/app/views/pet_types/show.html.haml +++ b/app/views/pet_types/show.html.haml @@ -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