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