Skip to content

feat(motion): lean animation path for scroll jank and reduced motion (spike 659) - #581

Closed
ExcelDsigN-tech wants to merge 25 commits into
PHASE-STELLAR:mainfrom
ExcelDsigN-tech:feat/animation-scroll-jank-purge
Closed

ExcelDsigN-tech wants to merge 25 commits into
PHASE-STELLAR:mainfrom
ExcelDsigN-tech:feat/animation-scroll-jank-purge

Conversation

@ExcelDsigN-tech

@ExcelDsigN-tech ExcelDsigN-tech commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a lean motion path behind phase-motion (default off). Smooth scroll only runs where it's needed, the hero fades on a CSS scroll timeline, ScrollTriggers are batched, the H1 renders on the server and flips with WAAPI, infinite loops animate only transform/opacity, will-change lives only while something animates, and prefers-reduced-motion is honored by the ticker, skeletons, flap text and smooth scroll. It also fixes a gsap ticker leak on every route change (always on).

The flag is the named phase-motion. It was phase-159, then phase-164, and upstream took both numbers for other spikes, so it now has a name that cannot collide. The lab runs below set NEXT_PUBLIC_FEATURE_PHASE_159; only the name changed since.

Changes

  • app/layout.tsx: stamps data-phase-motion="lean" when the build flag is set. Every CSS override is scoped to that attribute.
  • components/smooth-scroll.tsx: Lenis only on / in lean mode, never under reduced motion; fixed teardown (the anonymous rAF callback was never removed).
  • components/hero-section.tsx: CSS scroll-timeline fade with the gsap fallback.
  • colophon, principles, signals, work sections, highlight-text: ScrollTrigger.batch reveals via lib/motion-reveal.ts, quickTo cursor, no scrub parallax in lean mode.
  • components/split-flap-text.tsx: server-rendered final glyphs, textContent scramble, WAAPI settle flip, audio only when unmuted and motion is allowed.
  • app/tactical-command.css, app/globals.css, components/system-ticker.tsx: compositor-only loops, will-change: auto unless interacting, reduced-motion freezes.
  • lib/motion-performance.ts: pure helpers.
  • scripts/proof-motion-659.ts (npm run proof:659): Lighthouse before/after runner.

Files ruled out

draw-text.tsx and scramble-text.tsx have no importers. app/explore/page.tsx is covered by route scoping and the scoped CSS. package.json only gains the proof script.

Flag and rollback

NEXT_PUBLIC_FEATURE_PHASE_MOTION, build-time, default off. Rollback: unset, rebuild and redeploy. The scoped CSS goes inert and the legacy paths run. The ticker-leak fix stays on.

Tests

  • lib/__tests__/motion-performance.test.ts (12), including a CSS guard that every lean rule is attribute-scoped and compositor-only.
  • tsc --noEmit: 0 errors. vitest run: green with flags off.
  • Headless Chromium with the flag on: the server HTML has the final H1, the ticker's will-change is auto, and reduced motion turns off Lenis, the hero animation and the ticker. /explore runs without Lenis. No page errors.

Live proof

Lab run on production builds (Lighthouse mobile, median of 5, two passes): on /, main-thread work is down 24–27% and Speed Index down 13–25%, with LCP and CLS flat. Reduced motion verified in headless Chromium: Lenis, the hero animation and the ticker all stop with the flag on, and the flag-off build keeps the legacy behavior. Full tables are in the spike doc under "Lab results".

Still pending: a real low-end Android trace (checklist step 4). I will add it as a comment when captured.

Follow-up

Delete the dead text components, isolate the ticker clock, decide the CSP for the ticker's price and time APIs, and add production RUM.

Depends on

#574 (chore/fix-main-baseline). Merge that first; until it merges, this diff also shows its commits.

Closes #522

PhaseFeatureFlag lost phase-146..153, phase-editions and phase-timed-drops,
and SecurityCounterName lost ~50 counters used by merged spikes. Restoring
them clears 96 of the 120 tsc errors on main. Also registers phase-159,
phase-161, phase-162 and phase-163 for spikes 659, 661, 662 and 663.
…chamber modules

0d14b9a ("fixed") re-encoded several files as cp1252-decoded UTF-8 and
prepended a BOM plus // @ts-nocheck. User-facing strings in the chamber
(e.g. "Transacción", "…") shipped corrupted, and a few glyphs (●, ”)
were lost outright. Files with no later commits are restored from
0d14b9a^; files edited since get only their mojibake comments repaired.

60001c5 (PHASE-STELLAR#33 refactor) moved ChamberLogStream, ChamberCatalogThumb,
PhaserLiqTokenLink and the chamber chrome classes out of fusion-chamber
into components/chamber/* but never committed those files, and left
five one-word "placeholder" files behind. The four modules are recreated
verbatim from the code it deleted; the unused placeholder imports and
files are removed.
- offers accept route: spike 674 collab-split fan-out (3e4b562), with the
  splits binding hoisted so the response can actually read it
- IpfsDisplayImg: `fallback` prop destructure (b89c4f2)
- wash-screening: washScreenReasonForEdition (3e4b562)
- viewer-signature: viewerSignatureShort (f9a08b6; removed by 7cc03e4
  while three components still import it)
- sqlite-db: exhibitions, price_suggestions, moderation_queue and
  quest_arcs tables (b89c4f2). Their absence failed 27 tests.
Real bugs the type errors were hiding:
- nft-index subscribe: validateWebhookUrl accepted https://127.0.0.1 and
  every 172.16/12, 100.64/10 and 0/8 literal (SSRF). Now a proper IPv4
  range check.
- lib/stellar.ts settle verification read meta v1/v3 only and called
  ContractEventV0.contractId(), which no longer exists, so every settle
  check fell through to SETTLE_EVENT_NOT_FOUND. It now reads meta v4
  (protocol 23) and v3 and takes the emitter from ContractEvent.
  Still fails closed on any mismatch.
- artist-attestation passed a string where an ScAddress is required and
  decoded an already-decoded retval, so attestation never read true.

Also: winback route called a nonexistent addNotification (now
createNotification with a winback_reactivation type), wallet-provider
declared `controller` twice, duplicate `ok` keys in ve-lock, and
Buffer/Uint8Array typing in the ipfs and avatar routes. wallet-provider
and stellar.ts are also restored from the 0d14b9a encoding damage.
- artist-profile-client imported describe/it from node:test, so vitest
  found no suite
- world-roles-rbac seeded worldRoles.json while the store reads
  world-roles.json (serverDataJsonPath("worldRoles"))
- avatar-provenance hashed a string through the bytes-only sha256Hex;
  encoding first gives the same digest
- forge-pipeline imported bun:test; the APIs it uses are identical in vitest
- viewer-authorization: Buffer typing for Keypair.sign
eslint.config.mjs imports eslint-config-next, which was never installed,
so npm run lint failed outright. dotenv is imported by scripts that the
root test suite loads (scripts/reset-phase.ts).
- ethnum 1.5.2 -> 1.5.3 in both lockfiles: 1.5.2 fails to compile on
  rustc 1.97 (E0512 transmute), which broke cargo test for both crates
- mock-token test called an associated fn as a method outside a contract
  context; it now goes through the generated MockTokenClient

phase-protocol still does not compile: see the baseline notes. Three
merged spikes each claimed PhaseError discriminants from 18 upward.
tsc is at 0 errors and vitest is green on this branch, so both can gate
PRs now. docs/investigations/main-baseline-2026-10-17.md records what was
fixed and what is blocked (phase-protocol discriminant collision).
…lient)

lib/stellar-wallet-kit.ts is loaded by the wallet provider on every route
and imported randomUUID from node:crypto. The client polyfill has no
randomUUID, so hydration threw and nothing interactive rendered. The nonce
path is browser-only, so it now uses globalThis.crypto.randomUUID().
…STELLAR#522)

The root layout stamps data-phase-motion="lean" on <html> when
NEXT_PUBLIC_FEATURE_PHASE_159 is set. Every CSS override is scoped to that
attribute and every JS branch reads the same value, so flag off runs the
legacy paths unchanged.

- Lenis only on "/" in lean mode, never under reduced motion
- fix ticker teardown: the anonymous rAF callback was never removed
- hero fade on a CSS scroll timeline, gsap scrub kept as fallback
- ScrollTrigger.batch reveals with will-change only while tweening
- server-rendered H1 with a textContent scramble and a WAAPI settle flip
- compositor-only loops, no permanent box-shadow/filter will-change
- reduced motion honored by ticker, skeletons, flap and smooth scroll
- clear lint errors in the touched files

Tests: lib/__tests__/motion-performance.test.ts, including a CSS guard
that lean rules are attribute-scoped and compositor-only.
…659)

- scripts/proof-motion-659.ts (npm run proof:659): median-of-N Lighthouse
  (mobile, simulated throttling) for / and /explore, refuses a build whose
  data-phase-motion attribute does not match --label, and prints an
  off/on comparison table
- spike doc rewritten against the real issue: browser-verified behavior
  table (normal vs reduced motion), Lighthouse/trace/reduced-motion
  checklist with placeholders, follow-up
- docs/spikes/PR_659.md
The ticker's inline <style> contains html[data-phase-motion="lean"] in
every build, so the mislabel guard rejected the flag-off build.
…ion builds

Two passes, median of 5 runs each, flag off vs on. On /, main-thread
work is down 24-27% and Speed Index down 13-25%, LCP flat. /explore LCP
is bimodal noise in both builds. A headless Chromium check confirms
reduced motion stops Lenis, the hero fade and the ticker only with the
flag on. The device trace is still pending.
SEP-53 wallets sign SHA256("Stellar Signed Message:\n" + message). Signals,
world roles, avatar ownership and gated-preview viewer auth verified the
raw prefixed bytes instead, so every real Freighter signature was rejected
with "Invalid signature: payload not signed by this wallet". The unit
tests passed only because they signed the same raw bytes.

- lib/viewer-signature.ts: sep53SignedBytes + verifySep53Signature, used
  by verifySignalSignature, verifyWorldRoleSignature and the avatar route
- lib/viewer-authorization.ts: viewerSignatureBytes is now the SHA-256
  digest the wallet's key actually signs
- tests sign like a wallet; the official SEP-53 spec vectors are a
  regression test; tests/viewer-authorization.test.ts used an address
  unrelated to its seed and could never pass
Resolve lib/feature-flags.ts: keep the restored phase-146..153, editions
and timed-drops members alongside upstream phase-158..161. The spike flags
move to their own branches.
Upstream PHASE-STELLAR#573 assigned phase-159 to the batched owner-key loader.
…animation-scroll-jank-purge

Keep both flag rows in the docs; feature-flags.ts takes the baseline and adds phase-164.
Resolves conflicts in the wallet provider, fusion chamber, security
counters, wallet kit, flags and copy. Registers phase-170..173, types the
new copy blocks, restores the legal data paths and QR scanner imports,
and drops the re-added @ts-nocheck and mojibake.
Upstream assigned phase-164 to age bands. A named flag cannot collide
with later numbering.
…k-purge

# Conflicts:
#	PROJECT_ARCHITECTURE.md
#	app/layout.tsx
#	lib/feature-flags.ts
@drips-wave

drips-wave Bot commented Oct 20, 2026

Copy link
Copy Markdown

@ExcelDsigN-tech Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

# Spike 659 — Animation performance with off-main-thread motion and scroll-jank purge

1 participant