Compare commits

..

No commits in common. "80307f21f774fe4b0a235820ca04a56f54599f23" and "e7148ffae3a55023ce3510653b98eb996ae96757" have entirely different histories.

20 changed files with 111 additions and 425 deletions

View file

@ -1,110 +0,0 @@
@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,8 +2,6 @@
@import "../partials/clean/mixins"
@import "../partials/item_header"
@import "../application/outfit-viewer"
#container
width: 900px // A bit more generous to the preview area!
@ -80,10 +78,93 @@
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%
@ -98,8 +179,16 @@ 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-loading
#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:has(outfit-layer:state(error))
outfit-viewer

View file

@ -1,5 +0,0 @@
outfit-viewer
margin: 0 auto
dt
cursor: help

View file

@ -1,32 +0,0 @@
.pet-types
list-style-type: none
display: flex
flex-wrap: wrap
justify-content: center
gap: .5em
> li
width: 150px
max-width: calc(50% - .25em)
min-width: 150px
box-sizing: border-box
text-align: center
a
display: block
border-radius: 1em
padding: .5em
img
width: 100%
height: auto
aspect-ratio: 1 / 1
position: relative
z-index: -1
margin-bottom: -1em
.name
background: white
padding: .25em .5em
border-radius: .5em
margin: 0 auto

View file

@ -1,36 +0,0 @@
@import "../partials/clean/constants"
.pet-states
list-style-type: none
display: flex
flex-wrap: wrap
justify-content: center
gap: .5em
> li
width: 200px
max-width: calc(50% - .25em)
min-width: 150px
box-sizing: border-box
text-align: center
a
display: block
border-radius: 1em
padding: .5em
outfit-viewer
width: 100%
height: auto
aspect-ratio: 1 / 1
position: relative
z-index: -1
margin-bottom: -1em
.name
background: white
padding: .25em .5em
border-radius: .5em
.glitched
cursor: help

View file

@ -1,6 +0,0 @@
class PetStatesController < ApplicationController
def show
@pet_type = PetType.matching_name_param(params[:pet_type_name]).first!
@pet_state = @pet_type.pet_states.find(params[:id])
end
end

View file

@ -1,78 +1,10 @@
class PetTypesController < ApplicationController
def index
@species_names = Species.order(:name).map(&:human_name)
@color_names = Color.order(:name).map(&:human_name)
if params[:species].present?
@selected_species = Species.find_by!(name: params[:species])
@selected_species_name = @selected_species.human_name
end
if params[:color].present?
@selected_color = Color.find_by!(name: params[:color])
@selected_color_name = @selected_color.human_name
end
@pet_types = PetType.
includes(:color, :species).
order(created_at: :desc).
paginate(page: params[:page], per_page: 30)
if @selected_species
@pet_types = @pet_types.where(species_id: @selected_species)
end
if @selected_color
@pet_types = @pet_types.where(color_id: @selected_color)
end
end
def show
@pet_type = find_pet_type
@pet_type = PetType.
where(species_id: params[:species_id]).
where(color_id: params[:color_id]).
first
respond_to do |format|
format.html do
@pet_states = group_pet_states @pet_type.pet_states
end
format.json { render json: @pet_type }
end
end
protected
# The API-ish route uses IDs, but the human-facing route uses names.
def find_pet_type
if params[:species_id] && params[:color_id]
PetType.find_by!(
species_id: params[:species_id],
color_id: params[:color_id],
)
elsif params[:name]
color_name, _, species_name = params[:name].rpartition("-")
raise ActiveRecord::RecordNotFound if species_name.blank?
PetType.matching_name(color_name, species_name).first!
else
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!
#
# If no main poses are available, then we just make all the poses
# "canonical", and show the whole mish-mash!
MAIN_POSES = %w(HAPPY_FEM HAPPY_MASC SAD_FEM SAD_MASC SICK_FEM SICK_MASC)
def group_pet_states(pet_states)
pose_groups = pet_states.emotion_order.group_by(&:pose)
main_groups = pose_groups.select { |k| MAIN_POSES.include?(k) }.values
other_groups = pose_groups.reject { |k| MAIN_POSES.include?(k) }.values
if main_groups.empty?
return {canonical: other_groups.flatten(1).sort_by(&:pose), other: []}
end
canonical = main_groups.map(&:first).sort_by(&:pose)
main_others = main_groups.map { |l| l.drop(1) }.flatten(1)
other = (main_others + other_groups.flatten(1)).sort_by(&:pose)
{canonical:, other:}
render json: @pet_type
end
end

View file

@ -14,30 +14,19 @@ module ItemsHelper
}
Sizes = {
face: 1, # 50x50
face_3x: 6, # 150x150
thumb: 2, # 150x150
full: 4, # 300x300
large: 5, # 500x500
xlarge: 7, # 640x640
zoom: 3, # 80x80
autocrop: 9, # <varies>
}
SizeUpgrades = {
face: :face_3x,
thumb: :full,
full: :xlarge,
face: 1,
thumb: 2,
zoom: 3,
full: 4,
face_2x: 6,
}
end
def pet_type_image_url(pet_type, emotion: :happy, size: :face)
PetTypeImage::Template.expand(
hash: pet_type.basic_image_hash || pet_type.image_hash,
emotion: PetTypeImage::Emotions.fetch(emotion),
size: PetTypeImage::Sizes.fetch(size),
emotion: PetTypeImage::Emotions[emotion],
size: PetTypeImage::Sizes[size],
).to_s
end
@ -257,10 +246,8 @@ module ItemsHelper
def pet_type_image(pet_type, emotion, size, **options)
src = pet_type_image_url(pet_type, emotion:, size:)
size_2x = PetTypeImage::SizeUpgrades[size]
srcset = if size_2x
[[pet_type_image_url(pet_type, emotion:, size: size_2x), "2x"]]
srcset = if size == :face
[[pet_type_image_url(pet_type, emotion:, size: :face_2x), "2x"]]
end
image_tag(src, srcset:, **options)

View file

@ -69,17 +69,5 @@ 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

@ -1,22 +0,0 @@
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

@ -110,7 +110,7 @@ class PetState < ApplicationRecord
def swf_asset_ids=(ids)
self['swf_asset_ids'] = ids
end
def handle_assets!
@parent_swf_asset_relationships_to_update.each do |rel|
rel.swf_asset.save!
@ -118,10 +118,6 @@ class PetState < ApplicationRecord
end
end
def to_param
"#{id}-#{pose.split('_').map(&:capitalize).join('-')}"
end
def self.from_pet_type_and_biology_info(pet_type, info)
swf_asset_ids = []
info.each do |zone_id, asset_info|

View file

@ -15,10 +15,6 @@ class PetType < ApplicationRecord
species = Species.find_by_name!(species_name)
where(color_id: color.id, species_id: species.id)
}
scope :matching_name_param, ->(name_param) {
color_name, _, species_name = name_param.rpartition("-")
matching_name(color_name, species_name)
}
scope :preferring_species, ->(species_id) {
joins(:species).order([Arel.sql("species_id = ? DESC"), species_id])
}
@ -112,10 +108,6 @@ class PetType < ApplicationRecord
Item.appearances_for(item, self, ...)
end
def to_param
"#{color.human_name}-#{species.human_name}"
end
def self.all_by_ids_or_children(ids, pet_states)
pet_states_by_pet_type_id = {}
pet_states.each do |pet_state|

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: "", loading: "lazy"
= image_tag swf_asset.image_url, alt: ""
- else
/ No movie or image available for SWF asset: #{swf_asset.url}
/ 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
= outfit_viewer @preview_outfit
= render partial: "outfit_viewer", locals: {outfit: @preview_outfit}
.error-indicator
💥 We couldn't load all of this outfit. Try again?
= link_to wardrobe_path(params: @preview_outfit.wardrobe_params),
@ -122,8 +122,6 @@
- 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

@ -1,6 +0,0 @@
%li
= link_to [pet_state.pet_type, pet_state] do
= outfit_viewer pet_state:
.name= pose_name pet_state.pose
- if pet_state.glitched?
%span.glitched{title: "Glitched"} 👾

View file

@ -1,36 +0,0 @@
- title "#{@pet_type.human_name}: #{pose_name @pet_state.pose} [\##{@pet_state.id}]"
- use_responsive_design
= outfit_viewer pet_state: @pet_state
%dl
%dt{title: "Pose usually affects just the eyes and mouth. Neopets " +
"genders these as Male/Female, but I don't like those " +
"terms for like… it's just eyelashes! Sheesh!"}
Pose
%dd
= pose_name @pet_state.pose
- if @pet_state.pose == "UNCONVERTED"
(Retired, replaced by #{link_to "Alt Styles", alt_styles_path})
%dt{title: "This is our own internal ID number, nothing to do with " +
"Neopets's official data."}
DTI ID
%dd= @pet_state.id
%dt{title: "When we notice a form looks wrong, we mark it Glitched, to " +
"tell our systems to prefer other forms for this pose instead."}
Glitched?
%dd
- if @pet_state.glitched?
👾 Yes, it's bad news bonko'd
- else
✅ Not marked as Glitched
- content_for :stylesheets do
= stylesheet_link_tag "application/hanger-spinner"
= stylesheet_link_tag "application/outfit-viewer"
= stylesheet_link_tag "pet_states/show"
- content_for :javascripts do
= javascript_include_tag "outfit-viewer", async: true

View file

@ -1,4 +0,0 @@
%li
= link_to pet_type do
= pet_type_image pet_type, :happy, :thumb
.name= pet_type.human_name

View file

@ -1,16 +0,0 @@
- title "Rainbow Pool"
- use_responsive_design
= form_with method: :get do |form|
= form.select :color, @color_names, selected: @selected_color&.human_name, include_blank: "Color…"
= form.select :species, @species_names, selected: @selected_species&.human_name, include_blank: "Species…"
= form.submit "Filter"
= will_paginate @pet_types
%ui.pet-types= render @pet_types
= will_paginate @pet_types
- content_for :stylesheets do
= stylesheet_link_tag "pet_types/index"

View file

@ -1,19 +0,0 @@
- title "#{@pet_type.human_name}"
- use_responsive_design
%ul.pet-states
= render @pet_states[:canonical]
- if @pet_states[:other].present?
%details
%summary Other
%ul.pet-states
= render @pet_states[:other]
- content_for :stylesheets do
= stylesheet_link_tag "application/hanger-spinner"
= stylesheet_link_tag "application/outfit-viewer"
= stylesheet_link_tag "pet_types/show"
- content_for :javascripts do
= javascript_include_tag "outfit-viewer", async: true

View file

@ -37,10 +37,6 @@ OpenneoImpressItems::Application.routes.draw do
end
resources :alt_styles, path: 'alt-styles', only: [:index]
resources :swf_assets, path: 'swf-assets', only: [:show]
resources :pet_types, path: 'rainbow-pool', param: "name",
only: [:index, :show] do
resources :pet_states, only: [:show], path: "forms"
end
# Loading and modeling pets!
post '/pets/load' => 'pets#load', :as => :load_pet