Inspired by the "Flying in an Airplane" bug (item 82287), where the official SVG (and I think SWF) were visually glitched and included both zones in the image, but the official PNG was correct.
This flag lets us use the PNG, like the official player does—but only for this item, while still keeping SVGs for everyone else!
I've decided that covering up the species faces with other species info is too weird! It feels like it's removing some ability to cross-reference.
A cool UI affordance would be to have this and the faces interact with each other, like you can hover to highlight the relevant species faces, or even vice-versa, to show the relevant zones for this species. But that's probably way overkill for this relatively niche feature.
Oops, I never actually saw the practically invisible text in light mode! Let's make it actually dark in light mode item pages, and still dark in all wardrobe pages!
Here, we offer a second syntax for `<OutfitPreview />`: a hook that offers the same UI as `preview`, but _also_ shares the `appearance` data.
This makes it easier to have UI that depends on the outfit appearance, without having to commit to all the `useOutfitAppearance` stuff in the parent. Same easy syntax! :3
I've refactored the item page to use this for compatibility testing, instead of using the Apollo cache (which was also cute and same perf impact, but more overhead!)
Oh right, I left in a hack to just always pick HAPPY_MASC or HAPPY_FEM, back when it was just the basic colors. Now that we're all the colors, we need to be able to handle fallbacks for missing or unlabeled poses, too!!
I think what's happening in Sentry error IMPRESS-2020-1F is that mobile devices are running out of memory, so `canvas.getContext("2d")` returns null.
Now, we have a UI affordance to let you know when this is probably what's happening!
Also, when researching this, I learned about a Safari bug where you need to manually garbage-collect your own canvas data. It's possible that Safari users have been having particular trouble with memory leaks over long sessions? I'm not sure, but it seems like a good idea to add this small garbage-collection code!
Ok I think I've finally narrowed this bug down! We had one more loading case: the items page needs time to figure out which species/color to default the fields to, and passes null into the component while this loads. Now, we wait for that!
We're occasionally getting errors on the homepage, of the new message I added: `Error loading valid poses for species=, color=108: byteOffset cannot be negative`.
So ok, now we know it's a species undefined bug, coming from `onChangeSpecies`! That suggests we're not finding by ID correctly?
So I'm adding some new logging to help me understand the sequence of actions leading up to this point, and the species data state when the error itself happens!
I'm getting some vague errors in Sentry about `canvas.getContext` returning null? Weird. (IMPRESS-2020-1F)
I'm not sure what that's about, so I don't want to stop sending it to Sentry. But I do want to make sure we handle this kind of error gracefully! (I'm thinking about how, while I don't think this one was, in the future this _could_ be caused by errors in Neopets movie clip JS, and I don't want our app to start messing up because of it!)
Here, we make sure to log the error to the console with more detail (the library URL), and show feedback to the user, and only log the error once per clip (so that animated ones don't like, send a bunch).
Oops, we were getting errors when people tried to change the species/color picker before the valid pose data was ready!
This was only happening on the homepage and item page, because on the wardrobe page we wait for the valids to load before showing the picker at all (`showPlaceholders` is false).
To fix this, we add the valid poses loading state to our existing `isLoading` state on the selects, which disables the element and adds a loading spinner cursor. This prevents interactions we're not ready for!
I'm not sure why, but people are seeing errors when reading from the /api/validPetPoses binary blob. I think it's the picker not handling loading states well?
In this change, we start by just giving it graceful handling, and improving the logging. I'll also try to fix the cause in the next change!
I switched from my `_NoAuthRequired` opname hack, to a more robust `context` argument, and it's opt-in!
This should make queries without user data faster by default. We'll need to remember to specify this in order to get user data, but it shouldn't be something we'd like, ship without remembering—the feature just won't work until we do!
When building the code to await auth before sending _any_ GraphQL queries, I didn't realize that auth might be kinda slow. So, I've added a hack to let me mark queries with no user-specific data to skip auth, and applied that to the main queries on the homepage.
I think this is a hint that we might want to change our strategy - e.g. to flip it to hackily mark that auth _is_ required, or to create wrappers or option-builder helpers for logged-in queries, etc.
I also notice that SSR would have resolved this particular case...
I took out virtualization for now too, I wanna see how this non-Chakra UI version, with fewer nodes and no tooltips etc, performs on large lists in production.
Huh, I'm not sure why SVGs ever didn't have `crossOrigin: "anonymous"`? The old commit isn't really super helpful for understanding that. Maybe I just didn't notice the problem in that case?
Well, whatever. Let's see if this breaks something else! (I'm also wondering if we should just like, _always_ ask for things with crossOrigin set?)
To help the load time for outfits feel shorter, we now reuse the outfit thumbnail from the Your Outfits page as a placeholder!
This doesn't add any overhead when the thumbnail data _isn't_ in your session cache, e.g. if you navigate to the outfit directly. But if we have the thumbnail on hand already, we just show it, easy peasy!
Oops, when switching to @emotion/react, it turns out they no longer support that cute hack I was doing to append suffixes to class names!
Here, I change strategy and let `CSSTransition` apply the plain `exit` and `exit-active` classes to its children, and apply Emotion styles to the child to check for _also_ having those classes.
Still a pretty limited early version, no saving _back_ to the server. But you can click from the Your Outfits page and see the outfit for real! :3 We have a WIPCallout explaining the basics.
This has been bothering me for a long time, but I couldn't really figure out what to do about it. But tweaking the site bg color a smidge has helped us really add texture to the cards I want to have pop out, like the outfit polaroids!
I kinda went all-in in a burst, but tbh I think it looks great :3
I haven't really touched the wardrobe page with it yet though, that'll probably need some tweaking... for now I'm overriding it to keep the old background!
Looks like there was some kind of runtime conflict when running @emotion/css and @emotion/react at the same time in this app? Some styles would just get clobbered, making things look all weird.
Here, I've removed our @emotion/css dependency, and use the `<ClassNames>` utility element from `@emotion/react` instead. I'm not thrilled about the solution, but it seems okay for now...
...one other thing I tried was passing a `css` prop to Chakra elements, which seemed to work, but to clobber the element's own Emotion-based styles. I assumed that the Babel macro wouldn't help us, and wouldn't convert css props to className props for non-HTML elements... but I suppose I'm not sure!
Anyway, I don't love this syntax... but I'm happy for the site to be working again. I wonder if we can find something better.
Oops, creating a new `SpeciesColorPicker` fn on each render meant that React treated it as a whole new dropdown each time. I've extracted it out into a stable component class, and just pass in the extra props now!
This bug caused changes to kick you out of focus for the dropdown, because it had unmounted and remounted.
We do animation detection during the preload now, but this wasn't always working correctly: some movies don't actually fully mount the children onto the stage until we start playing. This caused the play/pause button to be missing on the outfit page and the item page, but the animations would still play, depending on the user's saved play/pause state in localStorage.
I saw the short-near-the-front and it just frankly looked awkward? Not sure why I liked it before?
I think this medium at the end of the list is better aesthatically, though it's starting to get a bit messy with the different colors mixed around… but I think there's also a semantic argument that we're keeping the facts about the item together, and the _user-specific_ stuff separate at the end… (putting it at the front would be a good semantic argument too, but I think the NC/NP alignment is too important)
In a previous change, I moved the margin for item badges onto an ItemBadge element… but I didn't think through how that would break the spacing for the loading state of ItemPage. Now, the loading skeleton items _contained_ the badge margin, and so the spacing between badges was shiny skeleton-y.
Here, I replace ZoneBadgesList with a function that just returns the elements, and go back to using Chakra's Wrap component. That will apply the margin to direct children, and the zone badges are direct children now.
One option I'm thinking of in hindsight is an idea I had earlier: Chakra hacks the margin onto _React_ children, but could we use CSS direct child selector instead? A bit trickier to resolve the margin size to the theme's value, but plenty doable… something to consider!
"Beautiful Green Painting Background" wasn't loading! https://impress-2020.openneo.net/items/75594
```Error building movie clips Error: Expected JS movie library http://images.neopets.com/cp/items/data/000/000/491/491273_31368b3745/491273_2_HTML5%20Canvas.js to contain a constructor named _491273_2_HTML5%20Canvas, but it did not: ssMetadata,Bitmap3,Bitmap5,CachedTexturedBitmap_4183,CachedTexturedBitmap_4184,CachedTexturedBitmap_4185,CachedTexturedBitmap_4186,CachedTexturedBitmap_4187,Symbol20,Symbol8,Symbol4,Symbol7,Symbol2,Symbol1,Symbol9,Symbol2copy,Symbol2_1,_491273_2_HTML5Canvas,properties,Stage```
We already had code to strip out spaces, but not encoded spaces like %20. Now, we decode the URL first, so that space-stripping will work even if it was encoded.
Oops, the <Wrap> component is nice, but it uses React.Children to apply margin to its _direct_ children, and our badges are not always direct children! (See the new `ZoneBadgeList`.)
I poked my head into how `Wrap` works, and it's honestly pretty simple, so I've applied the same styles manually. Ta da!
Not sure why movie clip building is failing! But it happened outside our try-catch, so it left us in an infinite spinner state.
The repro item is the Spring Topiary Garden Background!
I guess something got more picky about the loading sequencing: the fade in animation was happening faster than the cached image could load. Now, we explicitly wait for the image to load (even though we know it's probably cached) before fading it in.
Oops, our movie layer promises don't have a .cancel() method, so calling it crashed our error handler. Now, when there's an error loading a layer and there are HTML5 layers visible, we'll correctly show the "Could not load preview. Try again?" message.
So I broke the Download button when we switched to impress-2020.openneo.net, and I forgot to update the Amazon S3 config.
But in addition to that, I'm making some code changes here, to make downloads faster: we now use exactly the same URL and crossOrigin configuration between the <img> tag on the page, and the image that the Download button requests, which ensures that it can use the cached copy instead of loading new stuff. (There were two main cases: 1. it always loaded the PNGs instead of the SVG, which doesn't matter for quality if we're rendering a 600x600 bitmap anyway, but is good caching, and 2. send `crossOrigin` on the <img> tag, which isn't necessary there, but is necessary for Download, and having them match means we can use the cached copy.)
Previously I tried to be clever and pre-optimize by putting all the layers onto one canvas… I think this probably helped by batching their paints, but it made fades less smooth by not taking advantage of native CSS transitions, and it made us dip into JS way more often than necessary.
Here, I take the simpler approach: just layers of <img> and <canvas> tags, with each animated layer on its own canvas, and letting the browser handle transitions and compositing, and separate `setInterval` timers to manage their framerates.
I have a suspicion that batching the paints could help performance more, but honestly, maybe that batching is already happening somehow, because things look pretty great on my big-screen stress test now; and so if it _is_ relevant, I want to wait and see after testing on low-power devices.
These changes are most relevant for playing around in the dev server, modeing against an empty database. But they'll also help in real-world modeling scenarios! e.g. modeling a new species/color combo is now a bit nicer, we don't show a blank entry in the color picker
Honestly kinda surprised this worked on the first go! I was worried something about the process would make the sorta like, instant-cache expectation not work.
Still thinking it might be considerate to like, keep a LRU cache of MovieClip options, so that we don't double-execute these scripts when adding stuff… we even re-execute the ones already applied lol 😅 and that adds lots of script tags to the body!
But yeah I'm not gonna push on it yet until I see evidence that it actually causes performance issues in practice
This is really very cute, but too many items it turns out are lod despite not actually being animated 🙃 it's helpful for looking for test cases tho, so I'm keeping it, but support only!
I also ended up really liking the icon-badge+tooltip design as a way to summarize lil things, so I'm trying Own/Want short badges in the same style.
reflecting further on the abstraction, I'm noticing that this isn't an Easel abstraction like I envisioned early, and that we're baking some Neopets stuff into it. And I think that's the right call, esp with the tricky MovieClip stuff coming up, where I think more barriers would hurt more than they help. So, a new name!
Did it revert? Or did I just never notice that it only worked on mount, not on new loading states?
Also, fixed a bug where we were injecting the script tag way too much, and triggering loading too much that way too!
still just for static stuff, but it's good to be working!
PosePicker got a bit broken, CSS scaling doesn't work quite right anymore, we might need to just up the internal resolution or something?
Fading the whole preview to a black overlay for the loading state was feeling aggressive, especially since loading delay wasn't working correctly!
In this change, I fix loading delay, and I add a nice subtle "corner" variant for outfit preview spinners :)
It looks nice, but also particularly means we can handle the loading for the preview separately, get that started faster and iterate better on it in dev!
Initially the spinner was only used in OutfitPreview, where the background was always pretty dark. Now that we use it in more general contexts, we need a light/dark distinction!
Also went and standardized out the `size` props
I think the Chakra upgrades made these overrides stop working? added !important so that they happen again!
The regression meant the homepage looked worse, always having the selects fade in :/
(the permissioning happens on the backend in the prev change! but yeah we send the auth token in the headers, so the backend knows who you are and whether to show you private data)
(also it is just owned items not in any list!)
okay so the PetAppearance restrictions are stored on the asset, because that's how they're defined on Neopets.com too
but I think that's a confusing API, so here I define `PetAppearance.restrictedZones`, which just maps over the layers and aggregates the zones server-side, same as we would have done on the client
I think that's much easier to understand than having layer contain a field, but having to know that item restrictions _don't_ work that way, you know?
Huh, maybe this is a Firefox bug or something? but the container wasn't applying partial opacity correctly to its children, it was only doing 0 or 1, I think maybe because the children weren't static? I refactor here to make the DOM structure a bit more natural, and fade ins work again 🤷♀️
I'm not sure when this regressed, but changing the outfit was clearing out the whole preview and showing an empty loading state, instead of the intended behavior of showing a loading spinner over the old preview. This affected both the home page and the wardrobe page.
Yeah, so, huh. Fixed! I hope I didn't goof anything else with these effect trigger changes though 😅
The reason I'm doing this now is not just that it's annoying, but as a pair with the color change fix from just now, I want those color changes feeling buttery smooth!
Previously, when changing a pet's color, we would refresh the items panel and send a new network request for the item appearances, even though they're all the same. This is because item appearance data is queried by species/color, for ease of specification.
But! Item appearances are //cached// by body ID. So, if this is a standard color, it's not hard to look in the cache for the standard color's body ID!
Now, most color changes are faster and don't flicker the item panel anymore. We do still refresh the panel and send the requests for color changes that _do_ matter though, like standard <-> mutant!
Still just read-only stuff, but now you can look at all the different poses we have for a species/color!
Soon I'll make the pose/glitched stuff editable :3
Some sizable refactors here to add the ability to specify appearance ID as well as pose… most of the app still doesn't use it, it's mostly just lil extra logic to make it win if it's available!
(The rationale for making it an override, rather than always tracking appearance ID, is that it gets really inconvenient in practice to //wait// on looking up the appearance ID in order to start loading various queries. Species/color/pose is a more intuitive key, and works better and faster when the canonical appearance is what you want!)
This is in support of a caching issue in a hack tool coming next! Without this, the change to ItemAppearance restricted zones would make other ItemAppearance fields go missing (bc our hack tool didn't also specify them), so the query would re-execute over the network to find the missing fields we overwrote with nothingness—which would undo the local hack change.
Dice reported this, thank you!
My mistake here was that `loadImage` _does_ reject when the image fails to load… but it ends up throwing `undefined`, since I forgot to pass the error along from `onerror` to `reject`!
So we would cancel stuff, but then store `undefined` as our error in state, which our component interprets as no-error.
I tested this by using Firefox DevTools request blocking!
On the homepage, I want to keep the ability to enter invalid species/color pairs, so that you can say "Alien, Aisha" instead of having to pick the Aisha first.
But on the wardrobe page, we were rejecting invalid state changes anyway, so I decided to remove invalid color options from the list. And I added an ability to still switch to any species, and potentially resetting to a basic color automatically to match.
In this change, we cache the zones table as part of the JS build process. This keeps the database as our source of truth, while aggressively caching the data at deploy time.
See the new README for some rationale!
I tested this by pulling up dev Honeycomb, and observing that we no longer run db queries to `zones` in the new traces for the wardrobe page. (It's a good thing we did it this way, because I noticed some code in the server that was still loading the zone anyway, and fixed it here!)
Hey wow this was not so hard, just set some global styles, removed some hardcoded colors, and walked through the remaining hardcoded colors to pick a dark mode variant :) neat!!
I noticed that item wear/unwear is slow on mobile, because we re-render the whole app tree, and my laptop handles that super fine, but my few-years-old fun takes ~300ms, which is very noticeable.
There's some hacks we could do to get faster feedback, but first I'm diving into the render tree to find the unnecessary renders and stop 'em! That should help build perf across the board, rather than in just one spot, and hopefully be less of a weird sore spot :)
Using the newly extracted OutfitPreview! I'm really happy with how this turned out :3
It also makes the pageload after clicking Start super smooth, no spinner! Thanks Apollo cache!!