Skip to content

fix(completion): cross-fade the M1 accent seep under Reduce Motion - #1100

Merged
wei18 merged 2 commits into
mainfrom
fix/1065-rm-accent-seep-crossfade
Sep 23, 2026
Merged

wei18 merged 2 commits into
mainfrom
fix/1065-rm-accent-seep-crossfade

Conversation

@wei18

@wei18 wei18 commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

What

design.md §6 row M1 says the completion accent seep is 0.6s ease-out by default and a crossfade 0.25s under Reduce Motion (hard constraint 2: "Reduce Motion 不是關掉,是換 fade"). On main the glow was gated on M10 being in its non-Reduce-Motion .rise form, so with Reduce Motion on it was dropped, while CompletionMotionPlan.accentSeep computed a .crossfade(0.25) that nothing rendered (#1028 B-5 finding). This aligns the code to the design doc; the doc is untouched.

  • New CompletionAccentGlow view (Packages/GameShellKit/Sources/GameShellUI/Completion/CompletionAccentGlow.swift) resolves which glow plays from the plan alone: M1 .seep / .crossfade whenever the ritual plays; M10's 0.6 s glow for a live loss (the MS-loss status.error wash, unchanged); nothing for review. .seep keeps today's .easeOut(0.6); .crossfade is opacity-only .easeInOut(0.25) — no gradient sweep, no positional motion.
  • CompletionOverlayScaffold mounts it unconditionally — the if case .rise = motionPlan.panelRise gate is gone. It also gains an internal completionReduceMotionOverride environment seam (same pattern as completionHeroSkipsReveal) because accessibilityReduceMotion is read-only in EnvironmentValues (typecheck-verified); nil by default = the live setting decides.
  • Shared GameShellUI, so one change covers both apps.

Tests (CompletionAccentGlowTests, 10 tests, rendered path not the dead value)

  • form(for:) / animation(for:) — the resolutions the view body renders through.
  • Offscreen AppKit render of the real scaffold under the Reduce-Motion seam, probing the bottom band (y 640–870 pt, the band the issue measured): Reduce Motion on must carry the same settled tint as the sweep path; review is the no-glow control.
  • Loss wash pinned rendered: paints with Reduce Motion off, absent with it on — exactly as main (M10's Reduce-Motion row is silent on its glow; kept, not decided here).
  • Seam default: EnvironmentValues().completionReduceMotionOverride == nil and effectiveReduceMotion(override: nil, system:) follows the system value.

Mutation 1 — restore the if case .rise gate → red:

reduceMotionStillPaintsTheGlowInTheScaffold() … Expectation failed: (noGlow - reduceMotionOn → 0.0) > 1.0
reduceMotionStillPaintsTheGlowInTheScaffold() … Expectation failed: (abs(reduceMotionOn - reduceMotionOff) → 5.459294109244695) < 0.5

Mutation 2 — render .crossfade with the sweep's animation → red:

crossfadeAnimatesOpacityOnlyNotTheSweep() … Expectation failed: (crossfade → BezierAnimation(duration: 0.6, …)) == (.easeInOut(duration: 0.25) → BezierAnimation(duration: 0.25, …))
crossfadeAnimatesOpacityOnlyNotTheSweep() … Expectation failed: (crossfade → BezierAnimation(duration: 0.6, …)) != (sweep → BezierAnimation(duration: 0.6, …))

Mutation 3 — seam default false → red:

reduceMotionSeamDefaultsToTheSystemSetting() … Expectation failed: (EnvironmentValues().completionReduceMotionOverride → false) == nil

Honest limit: the offscreen capture is one settled frame, so it proves the glow exists under Reduce Motion at the sweep path's settled tint; the crossfade-vs-seep timing is pinned on the animation function the body calls and shown frame-by-frame on the simulator below.

swift test GameShellKit: 134 tests / 20 suites green (bounded run, fresh .build).

Snapshots — nothing re-recorded

SudokuUITests CompletionOverlayScaffoldTests (5) + CompletionViewTests (3) and MinesweeperUITests MinesweeperCompletionSnapshotTests (7) fail strict .image on this branch and identically on origin/main 73f76b4 (same 8 + 7). All 15 head-rendered images are byte-identical (cmp) to the base-rendered images; the diffs against the committed baselines are 0.2–2.0 % of pixels, all in the card text / CTA label rows, bottom glow pixel-identical — the #1097 macOS 27 renderer drift. The committed baselines render with Reduce Motion off, so identical output also proves the seep path did not move. Tolerant MinesweeperBoardTerminalOverlaySnapshotTests (scaffold over a live loss board) passes. Left to #1097.

Simulator evidence (own sim, iPhone 17 Pro, iOS 26.4, Reduce Motion set through the Settings app + read back from defaults)

Mean B−R of the settled completion screen's bottom band (y 640–870 pt), same method as the issue table (build/1065/tools/band.py; re-running it on the #1028 PNGs reproduces −13.2 / −6.1 / −7.3 / +0.9):

app issue RM0 issue RM1 (bug) this branch RM0 this branch RM1
Sudoku −13.9 −6.0 (bare paper) −13.2 −13.2
Minesweeper −8.0 +1.1 (neutral) −7.3 −7.3

Per-frame band tint from the first changed frame (bandtimeline.txt): Sudoku RM0 reaches −13.3 at +566 ms (0.6 s seep under the 0.45 s rise); RM1 reaches −13.0 at +333 ms and settles by +467 ms (0.25 s crossfade, bounded by M10's 0.45 s fade-in-place). Minesweeper: RM0 settles at +566 ms, RM1 at +400 ms. Panel (shift.py, band y 240–720): RM0 rises dy +60→0 (Sudoku) / +21→0 (MS) over ~450 ms; RM1 dy = 0 on every frame after the first two near-blank frames — fade in place, no sweep.

Local evidence (gitignored build/, this worktree): build/1065/{RM0,RM1}/{sudoku,minesweeper}/ — completion.png, band.txt, completion.mov, completion-frames/ (33 ms), completion-sheet.png (frame strip), shift.txt, bandtimeline.txt, toggles.txt.

Open questions (not fixed here)

  • Reduce Transparency (issue side observation): with RT on the glow is nearly invisible even with RM off because it is painted behind the now-opaque glass (Sudoku B−R −2.6 in test(v3): B-5 — eight accessibility-toggle-combination matrix on board + G4 #1028's RM0-IC0-RT1 run). §4/§7 should say whether that is intended.
  • Loss glow under Reduce Motion: a live Minesweeper loss still gets no wash under Reduce Motion (pre-existing, now pinned by lossWashIsUnchangedByResolvingFromThePlan). M10's Reduce-Motion column is silent on its glow; if constraint 2 applies to it too, that is a one-line change in CompletionAccentGlow.form(for:) plus a design.md row.

Closes #1065
part of #1014

🤖 Generated with Claude Code

The accent glow behind the completion glass was gated on M10 being in its
non-Reduce-Motion `.rise` form, so with Reduce Motion on the glow was
dropped outright while `CompletionMotionPlan.accentSeep` computed a
`.crossfade(0.25)` that nothing rendered (#1028 B-5 finding). design.md
§6 M1 + hard constraint 2 say Reduce Motion swaps the seep for a 0.25 s
crossfade, not off.

- New `CompletionAccentGlow` view resolves which glow plays from the plan
  alone: M1 `.seep` / `.crossfade` when the ritual plays, M10 glow for a
  live loss, nothing for review. `.seep` keeps the 0.6 s ease-out;
  `.crossfade` is an opacity-only 0.25 s ease-in-out.
- `CompletionOverlayScaffold` mounts it unconditionally (no `panelRise`
  gate) and gains an internal `completionReduceMotionOverride`
  environment seam because `accessibilityReduceMotion` is read-only.
- `CompletionAccentGlowTests` pin the rendered path: form + animation
  resolution, and an offscreen AppKit render of the real scaffold whose
  bottom band must carry the tint under Reduce Motion.

Closes #1065
part of #1014

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Code review MINOR on #1100: `CompletionAccentGlow.form(for:)` /
`animation(for:)` and `CompletionOverlayScaffold.effectiveReduceMotion`
are pure value transforms nested in View types, so they were implicitly
`@MainActor` and every fresh compile of the nonisolated test body warned
"implicitly asynchronous". Marking them `nonisolated` removes the
warnings; a fresh `.build` of GameShellKit now compiles the test target
without them (134 tests / 20 suites green).

part of #1065

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@wei18

wei18 commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

PM merge record, head 5d6e9b5c.

Code review + acceptance: ACCEPT / MERGE, no BLOCKER or MAJOR. The reviewer reproduced everything first-hand:

  • Mutations, all red: restoring the if case .rise gate; animating .crossfade with the seep animation; defaulting the seam to false; resolving a live loss through M1 instead of M10 — the last one double-pinned by lossWashIsUnchangedByResolvingFromThePlan and liveSolveFailureUnderReduceMotionStaysUnlit.
  • Snapshots: 15/15 rendered byte-identical head vs base (SudokuKit 5 + 3, MinesweeperKit 7), so the failures are test(snapshots): strict baselines fail after the macOS 27 upgrade (2026-09-16), renderer drift with no code change #1097 drift and the Reduce-Motion-off seep path did not move. No __Snapshots__ file is touched by this PR.
  • The seam: completionReduceMotionOverride is internal, defaults to nil, and no production code writes it, so the user's Reduce Motion setting cannot be silently overridden.
  • Suites: GameShellKit 134 tests / 20 suites green on a fresh compile.

Two MINORs were fixed before merge: three pure value transforms are now nonisolated (CompletionAccentGlow.form(for:), animation(for:), CompletionOverlayScaffold.effectiveReduceMotion(override:system:)), which clears the implicit-@MainActor warnings this PR's tests emitted on every clean build; and the PR body's counts were corrected to 134 tests / 20 suites and 10 tests in the new suite. Verified from the ref: the follow-up is those three annotations and nothing else.

Behaviour evidence (simulator, both apps). The bottom-band tint under Reduce Motion now matches Reduce Motion off — Sudoku −13.2 vs −13.2, Minesweeper −7.3 vs −7.3, against −6.0 and +1.1 before the fix, where the glow was absent. Frame strips show the panel fading in place (dy = 0) with no sweep.

Deferred, recorded in the PR body: the Reduce Transparency question (the glow sits behind now-opaque glass). The 82 pre-existing implicit-@MainActor warnings in TabRootsAccessibilityTests.swift are untouched and out of scope.

@wei18
wei18 merged commit c23c052 into main Sep 23, 2026
7 checks passed
@wei18
wei18 deleted the fix/1065-rm-accent-seep-crossfade branch September 23, 2026 11:46
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.

fix(completion): M1 accent seep is dropped under Reduce Motion instead of cross-faded — computed RM form never wired (B-5 finding)

1 participant