Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,27 @@
.koala-btn {
position: relative;
width: clamp(76px, 9vw, 116px);
/*
* A FIXED box, not one sized by whichever pose is showing.
*
* The eight poses have eight different intrinsic ratios — 440 to 560 wide
* against 560 tall, plus koala-sleep which is landscape. With `height: auto`
* on the image, the button's height therefore changed on every single tap,
* and because this container is bottom-anchored the whole mascot resized
* under the cursor mid-animation. Pinning the ratio here means a pose swap
* changes the picture and nothing else.
*/
aspect-ratio: 500 / 560;
/*
* Both of these are load-bearing. This button is a flex item in a column,
* so its default `min-height: auto` lets it grow to the natural height of
* the image inside — which defeats the aspect-ratio above for exactly the
* poses that are more portrait than 500/560 (koala-read grew the box to
* 139px, koala-tree to 147px). Pinning min-height to 0 is what makes the
* ratio the final word.
*/
flex: none;
min-height: 0;
border: 0;
background: transparent;
cursor: pointer;
Expand All @@ -958,7 +979,12 @@
}
.koala-img {
width: 100%;
height: auto;
height: 100%;
/* Every pose is letterboxed inside the fixed box above rather than resizing
it, and anchored to the bottom so Koda's feet stay on the same line as the
picture changes shape. */
object-fit: contain;
object-position: center bottom;
display: block;
filter: drop-shadow(0 8px 14px rgba(21, 18, 12, 0.18));
animation: koala-pop 0.42s ease-out;
Expand Down
29 changes: 28 additions & 1 deletion src/components/KoalaMascot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,30 @@ function KoalaBase({ canOfferSignup }: { canOfferSignup: boolean }) {
/* eslint-enable react-hooks/set-state-in-effect */
}, []);

/**
* Decode every pose once, up front.
*
* The <img> below is keyed on `index`, and that is deliberate: a keyed
* remount is what replays the koala-pop animation on each tap. The cost is
* that every tap mounts a brand-new element with no bitmap attached, and
* `decoding="async"` explicitly permits the browser to paint that empty
* element before the image is ready. Since koala-pop starts at opacity 0,
* the two together produced a visible blank frame on every single tap.
*
* Decoding all eight poses on mount means the next pose's bitmap is already
* in memory when the remount happens, so the new element paints with content
* in the same frame React commits it.
*/
useEffect(() => {
for (const p of POSES) {
const img = new Image();
img.src = p.src;
// A pose that refuses to decode simply behaves as it did before; there
// is nothing useful to do about it here.
void img.decode?.().catch(() => {});
}
}, []);

// Wave hello shortly after arriving (deferred inside a timer).
useEffect(() => {
if (dismissed) return;
Expand Down Expand Up @@ -201,7 +225,10 @@ function KoalaBase({ canOfferSignup }: { canOfferSignup: boolean }) {
width={pose.w}
height={pose.h}
draggable={false}
decoding="async"
/* Not "async": this element is remounted on every tap, and async
decoding is permission to paint it before the bitmap arrives. The
preload effect above means there is nothing to wait for. */
decoding="sync"
/>
</button>

Expand Down
12 changes: 9 additions & 3 deletions src/components/learn/shell/LessonGate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,15 @@ export function LessonGate({ track, slug, prev, children }: LessonGateProps) {
if (status === "locked" && prev) {
return (
<div className="mt-10 rounded-learn-xl border-[0.5px] border-learn-line bg-learn-surface p-8 text-center md:p-10">
<div className="text-3xl" aria-hidden="true">
🔒
</div>
<svg
viewBox="0 0 24 24"
className="mx-auto h-8 w-8 text-learn-subtle"
aria-hidden="true"
fill="none"
>
<rect x="4" y="10" width="16" height="10" rx="2" stroke="currentColor" strokeWidth="1.6" />
<path d="M8 10V7a4 4 0 0 1 8 0v3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
</svg>
<h2 className="mt-3 text-lg text-learn-strong md:text-xl">
Finish the previous chapter first
</h2>
Expand Down
Loading
Loading