feat(onboarding): first-chart reveal becomes a star convergence, no buttons (F4)
TASK-birth-sky-followup-20260928 F4: - pure timeline / seeded scatter / interpolation in lib/birth-sky/converge.ts; drawBirthSky takes an optional frame (positions interpolated, alphas multiplied) so the settled frame is the exported cover call for call - lazy full-screen <dialog> overlay driven by requestAnimationFrame at viewport x DPR; tap / click / Esc end it; reduced motion only fades - data, serif and overlay chunk must all arrive inside the 3 s budget, otherwise skipped silently; page.tsx wiring unchanged - one sonner hint after it ends; dialog reveal variant (继续, is-reveal) removed - DESIGN §17, VOICE, CHANGELOG, checklist, PROGRESS with verification Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a9a917d50c
commit
731c3b364b
@@ -1,34 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { loadBirthSkyCover, releaseBirthSkyCover, type BirthSkyCover } from "@/lib/birth-sky/cover";
|
||||
import { startBirthSkyReveal } from "@/lib/birth-sky/reveal";
|
||||
import { loadBirthSkyRevealSky, startBirthSkyReveal, type BirthSkyRevealSky } from "@/lib/birth-sky/reveal";
|
||||
|
||||
/** Shown once on the home page after the convergence ends (VOICE.md). */
|
||||
export const BIRTH_SKY_REVEAL_HINT = "这片天空在星盘页可以保存。";
|
||||
|
||||
/**
|
||||
* The one-time 「那一刻的天空」 reveal after the reader's own first chart
|
||||
* (TASK-birth-sky-cover-20260928 T6). Owns its state so Home only wires it:
|
||||
* `begin` goes to the onboarding hook, `cover` / `close` to the dialog.
|
||||
* Saving one's own birth place happens once per account, so no "seen" flag.
|
||||
* The one-time 「那一刻的天空」 convergence after the reader's own first chart
|
||||
* (TASK-birth-sky-cover-20260928 T6 → TASK-birth-sky-followup-20260928 F4).
|
||||
* Owns its state so Home only wires it: `begin` goes to the onboarding hook,
|
||||
* `cover` / `close` to the overlay. `cover` is the sky plus the serif verdict,
|
||||
* ready before the first frame. Saving one's own birth place happens once per
|
||||
* account, so no "seen" flag.
|
||||
*/
|
||||
export function useBirthSkyReveal() {
|
||||
const [cover, setCover] = useState<BirthSkyCover | null>(null);
|
||||
const [cover, setCover] = useState<BirthSkyRevealSky | null>(null);
|
||||
const cancel = useRef<(() => void) | null>(null);
|
||||
const playing = useRef(false);
|
||||
|
||||
const begin = useCallback(() => {
|
||||
cancel.current?.();
|
||||
cancel.current = startBirthSkyReveal({
|
||||
load: (signal) => loadBirthSkyCover("self:reveal", "self", signal),
|
||||
onReady: (next) => setCover(next),
|
||||
onDiscard: releaseBirthSkyCover,
|
||||
cancel.current = startBirthSkyReveal<BirthSkyRevealSky>({
|
||||
// The overlay chunk loads inside the same budget, so the first frame never waits for code.
|
||||
load: async (signal) => {
|
||||
const [sky] = await Promise.all([
|
||||
loadBirthSkyRevealSky(signal),
|
||||
import("@/components/birth-sky/birth-sky-converge"),
|
||||
]);
|
||||
return sky;
|
||||
},
|
||||
onReady: (next) => {
|
||||
playing.current = true;
|
||||
setCover(next);
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setCover((current) => {
|
||||
releaseBirthSkyCover(current);
|
||||
return null;
|
||||
});
|
||||
if (!playing.current) return;
|
||||
playing.current = false;
|
||||
setCover(null);
|
||||
toast(BIRTH_SKY_REVEAL_HINT);
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => { cancel.current?.(); }, []);
|
||||
|
||||
Reference in New Issue
Block a user