Repository navigation
feat: ease Nino's motion like Blobatar (#23) - #49
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Stills from the built Storybook, sampled at one eighth speed so the 300ms morph can be read frame by frame: idle to happy and happy to hiding in light and dark, and the idle glance and start/end looks on an RTL page in dark. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
play() re-keyed the stage on every call, so a play() while hiding inserted fresh arms and hands whose first computed style already had --acting applied; a transition has no before-change style to start from, so the hands vanished and the side arms appeared at once instead of cross-fading. The stage is now re-keyed only when the DOM currently shows an action, read from a post-flush record of activeAction: a replay (the same action again, or a cut-off chain from an action-end handler) still restarts its keyframes at frame 0, while a play() from rest keeps every element, so hiding cross-fades both ways and the ambient loops keep their phase. From the same review: the reduced-motion twin test is keyed by property family as well as selector; every held keyframe stop is tested against the two-unit grid again (framesOf drops custom-property names before reading numbers, since --i9k-nino-dir carries a 9); a comment pass in I9kNino.vue names both off-grid exceptions once, the three eye layers, the jump's three lists and the transitioned reduced-motion twins; and the design spec's wording follows the re-key rule, records the blink-into-beat one-frame pop and corrects dizzy's rest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A same-action replay chained from play()'s promise (or from nextTick, or a timer in the same frame) ran after the post-flush watcher had cleared the rendered-action record but before the browser's next style pass, so the stage was not re-keyed and the finished animation stayed finished while the promise still resolved completed: true. The record now clears only in the second requestAnimationFrame callback after the action ends, once a style recalculation has seen the class gone: a play() with an action on screen, or gone for less than a frame, re-keys the stage, and a play() from rest still keeps every element. Unmount cancels a pending frame. Without requestAnimationFrame the record clears at once, as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Deploying 9k-design-system with
|
| Latest commit: |
5eb46eb
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://915fb642.9k-design-system.pages.dev |
| Branch Preview URL: | https://feat-23-smooth-motion-for-ni.9k-design-system.pages.dev |
ismail9k
commented
Oct 26, 2026
ismail9k
left a comment
Member
Author
There was a problem hiding this comment.
No actionable findings: 0 Critical, 0 Important, 0 Minor. Intent pass: the implementation satisfies issue #23's scope and constraints, including eased motion, motion-off handling, documentation, and committed light/dark/RTL visual evidence. Code pass completed using gpt-6-astra, including action replay scheduling and cleanup. Read-only source review; tests, builds, and browser checks were not rerun.
ismail9k
marked this pull request as ready for review
October 26, 2026 18:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
look, eased blinks, breathing in every mood, an idle glance that holds and then flicks, eased bob/tremor/hop, and eased one-shot actions. Nosteps()is left inI9kNino.vue. Resting poses stay on the two-unit grid; parts travel between cells only while moving.i9k-nino--beat-shut→--beat-open,NINO_BEAT = { swapAt: 120, duration: 300 }). Face parts squash shut, the new face swaps in behind them, and they open. Parts that mount at the swap (brows, hands, cheeks) start shut. While hiding, arms and hands cross-fade withopacityinstead ofvisibility.glancelayer insideeyes(data-nino-part="glance"). It carries the idle glance animation whileeyescarries thelooktransition. A transition cannot hand over to an animation created or cancelled in the same style change, so on one elementcenter → downwould snap.play()now re-keys the stage only when an action is on screen, or was on screen until less than a frame ago (doublerequestAnimationFrame, cancelled on the next action and on unmount, with an SSR guard). An action that starts from rest keeps the stage, so hiding cross-fades both ways. A replay chained from the resolved promise still restarts. This is a script-level change beyond the plan. Please read it closely.animatedis false and underprefers-reduced-motion: reduce, where every animated or transitioned selector is repeated withnone. No public API change. Durations inNINO_ACTION_DURATIONSare unchanged, so consumers see the sameplay()timing. AGENTS.md, the story (newMoodMorph), the showcase registry and the tests are updated.Closes #23
Follow-ups: 9k.school gets this only after it updates
@9klabs/design. Once it does, check the sign-in mascot by hand: focus and submit should glide and morph, and no consumer change is needed. Recordings of the motion (nino-mood-morph,nino-actions,nino-hiding-wave,nino-talking, as GIF and MP4) were captured locally and still need to be attached to this PR by hand. The committed stills are indocs/visual-evidence/i9k-nino-smooth-motion/(light, dark, and RTL dark).Verification
Run by the dispatcher in the branch's worktree at
5eb46eb:pnpm run check(test, format:check, lint, typecheck, build, build-storybook, build:showcase): exit 0. 77 test files and 1173 tests passed; the showcase wrote 78 machine-readable files for 74 pages.Reported by the build agent and not re-run by the dispatcher: browser checks in Chromium against the built Storybook. These covered the idle → happy → hiding morph in light and dark, RTL dark (glance direction flips), the ambient loops in every mood, a single 200 ms
looktransition, zero animations under reduced motion and on the Still story, talking, andplay()replays.Decisions
Made by the agents on the owner's behalf, with the cost if wrong:
crispEdgesstays. If wrong (every frame must be pixel-perfect), the work becomes frame-by-frame sprite animation.cubic-bezier(0.23, 1, 0.32, 1)to settle,ease-in-outsways, alineartremor. If wrong, nothing practical: its parametric poses can't drive Nino's path faces.I9kNinoMazehere andNinoMaze.vuein 9k.school keepsteps(2, end). If wrong, one line in each repo.eyesandglance(see Summary). If wrong (template must stay untouched), go back to one element and accept the snap.--beat-even/odd. Those classes are removed; the agents found no use of them in this repo, 9k.school or ismail9k.com. If wrong, they are internal class names only.opacityover 120 ms. If wrong, a brief translucent arm; revert tovisibility.play()re-keys only when an action is on screen or was less than a frame ago (see Summary). The final review found that hiding did not cross-fade; the first fix broke promise-chained replays, and a second fix round (beyond the build skill's usual one) settled it, with tests and a browser check. If wrong, a replay within one frame of an action ending fails to restart, or a frame callback is left pending after unmount. Both are pinned by tests.docs/visual-evidence/. If wrong, cosmetic; drop the three PNGs.I9kNinoSkytest'srequestAnimationFramespy would also see Nino's rest-frame scheduling if a future Sky test lets an action finish. If wrong, one comment line and one spec phrase.Spec:
docs/superpowers/specs/2026-10-26-nino-smooth-motion-design.md· Plan:docs/superpowers/plans/2026-10-26-nino-smooth-motion.md🤖 Generated with Claude Code