chati-dev 4.2.1 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -53
- package/bin/chati.js +150 -5
- package/framework/agents/build/dev.md +509 -76
- package/framework/agents/deploy/devops.md +40 -25
- package/framework/agents/discover/brief.md +156 -22
- package/framework/agents/discover/brownfield-wu.md +24 -14
- package/framework/agents/discover/greenfield-wu.md +100 -14
- package/framework/agents/plan/architect-data-engineer.md +6 -6
- package/framework/agents/plan/architect-system.md +46 -12
- package/framework/agents/plan/architect.md +40 -20
- package/framework/agents/plan/detail.md +36 -24
- package/framework/agents/plan/phases.md +36 -26
- package/framework/agents/plan/tasks.md +114 -33
- package/framework/agents/plan/ux-brand-architect.md +240 -8
- package/framework/agents/plan/ux-component-engineer.md +28 -13
- package/framework/agents/plan/ux-researcher.md +7 -6
- package/framework/agents/plan/ux.md +55 -22
- package/framework/agents/quality/qa-implementation.md +143 -74
- package/framework/agents/quality/qa-planning.md +115 -42
- package/framework/agents/quality/qa-visual.md +439 -0
- package/framework/agents/shared/visualizer.md +128 -0
- package/framework/config.yaml +8 -7
- package/framework/constitution.md +127 -43
- package/framework/context/governance.md +12 -7
- package/framework/context/quality.md +6 -5
- package/framework/context/root.md +6 -6
- package/framework/data/entity-registry.yaml +377 -4
- package/framework/data/model-limits.json +19 -0
- package/framework/domains/agents/qa-visual.yaml +74 -0
- package/framework/domains/constitution.yaml +46 -2
- package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
- package/framework/hooks/advance-trigger.js +131 -0
- package/framework/hooks/brief-validator.js +83 -0
- package/framework/hooks/constitution-guard.js +91 -36
- package/framework/hooks/license-guard.js +129 -190
- package/framework/hooks/mode-governance.js +68 -16
- package/framework/hooks/model-governance.js +19 -8
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/post-dev.js +207 -0
- package/framework/hooks/prism-engine.js +288 -105
- package/framework/hooks/read-protection.js +37 -9
- package/framework/hooks/reasoning-escalator.js +371 -0
- package/framework/hooks/reference-trigger.js +117 -0
- package/framework/hooks/session-digest.js +94 -20
- package/framework/hooks/settings.json +32 -1
- package/framework/hooks/style-guard.js +52 -13
- package/framework/hooks/team-quality-gate.js +58 -25
- package/framework/hooks/undercover-guard.js +34 -13
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/context-engine.md +4 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/intelligence/hooks-performance.md +3 -3
- package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
- package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
- package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
- package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
- package/framework/orchestrator/chati-router.js +387 -0
- package/framework/orchestrator/chati-update.md +40 -40
- package/framework/orchestrator/chati.md +318 -115
- package/framework/scaffold/motion-premium/README.md +89 -0
- package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
- package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
- package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
- package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
- package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
- package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
- package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
- package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
- package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
- package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
- package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
- package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
- package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
- package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
- package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
- package/framework/scaffold/motion-premium-3d/README.md +80 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
- package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
- package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
- package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
- package/framework/schemas/session.schema.json +109 -21
- package/framework/scripts/reference-capture.js +430 -0
- package/framework/scripts/visual-qa.js +674 -0
- package/framework/tasks/orchestrator-handoff.md +1 -1
- package/framework/tasks/orchestrator-resume.md +1 -1
- package/framework/tasks/orchestrator-route.md +1 -1
- package/framework/tasks/orchestrator-status.md +3 -3
- package/framework/tasks/qa-planning-gate-define.md +1 -1
- package/framework/templates/brandbook-html-tmpl.md +1 -1
- package/framework/templates/brandbook-tmpl.yaml +1 -1
- package/framework/templates/component-spec-tmpl.yaml +1 -1
- package/framework/templates/design-token-tmpl.yaml +1 -1
- package/framework/templates/icon-system-tmpl.yaml +1 -1
- package/framework/templates/team-planning-tasks.yaml +6 -5
- package/framework/workflows/brownfield-discovery.yaml +2 -2
- package/framework/workflows/brownfield-fullstack.yaml +15 -11
- package/framework/workflows/brownfield-service.yaml +14 -10
- package/framework/workflows/brownfield-ui.yaml +15 -11
- package/framework/workflows/greenfield-fullstack.yaml +16 -13
- package/framework/workflows/quick-flow.yaml +3 -3
- package/framework/workflows/standard-flow.yaml +12 -9
- package/package.json +10 -5
- package/src/autonomy/autonomous-gate.js +1 -0
- package/src/autonomy/build-state.js +1 -2
- package/src/autonomy/progress-reporter.js +1 -1
- package/src/config/agent-customizer.js +11 -3
- package/src/config/claude-settings-generator.js +226 -0
- package/src/config/context-file-generator.js +41 -21
- package/src/config/framework-adapter.js +1 -0
- package/src/config/gemini-hooks-generator.js +76 -6
- package/src/config/mcp-configs.js +1 -0
- package/src/context/layers/l1-global.js +2 -1
- package/src/dashboard/data-reader.js +4 -3
- package/src/dashboard/layout.js +2 -1
- package/src/decision/analyzer.js +6 -30
- package/src/decision/engine.js +4 -28
- package/src/decision/registry-healer.js +3 -2
- package/src/decision/registry-updater.js +23 -14
- package/src/extensions/loader.js +2 -8
- package/src/gates/g1-planning-complete.js +2 -1
- package/src/gates/g2-qa-planning.js +2 -1
- package/src/gates/g3-implementation.js +2 -1
- package/src/gates/g4-qa-implementation.js +3 -2
- package/src/gates/g5-deploy-ready.js +2 -1
- package/src/health/engine.js +4 -3
- package/src/installer/core.js +557 -81
- package/src/installer/preflight.js +131 -0
- package/src/installer/provider-overlay.js +3 -3
- package/src/installer/scaffold-applier.js +358 -0
- package/src/installer/templates.js +52 -31
- package/src/installer/validator.js +17 -12
- package/src/intelligence/registry-manager.js +22 -29
- package/src/intelligence/timeline.js +11 -6
- package/src/license/commands.js +1 -1
- package/src/license/wait.js +102 -0
- package/src/memory/agent-memory.js +81 -0
- package/src/memory/dream.js +32 -1
- package/src/merger/replace-merger.js +28 -15
- package/src/orchestrator/agent-selector.js +2 -1
- package/src/orchestrator/cli.js +1908 -80
- package/src/orchestrator/doctor.js +270 -0
- package/src/orchestrator/handoff-engine.js +4 -3
- package/src/orchestrator/index.js +2 -0
- package/src/orchestrator/pipeline-manager.js +306 -15
- package/src/orchestrator/session-manager.js +331 -6
- package/src/tasks/handoff.js +3 -2
- package/src/telemetry/config.js +4 -3
- package/src/telemetry/schema.js +1 -0
- package/src/terminal/collector.js +3 -2
- package/src/terminal/index.js +1 -2
- package/src/terminal/isolation.js +52 -18
- package/src/terminal/prompt-builder.js +42 -25
- package/src/terminal/run-parallel.js +1 -1
- package/src/terminal/run-team.js +3 -3
- package/src/terminal/team-task-list.js +43 -4
- package/src/upgrade/backup.js +3 -2
- package/src/upgrade/checker.js +3 -2
- package/src/upgrade/migrator.js +65 -7
- package/src/upgrade/tracked-files-detector.js +86 -0
- package/src/upgrade/user-messages.js +94 -0
- package/src/utils/config-parser.js +2 -1
- package/src/utils/feature-flags.js +2 -1
- package/src/utils/flatten-entities.js +69 -0
- package/src/utils/framework-dir.js +16 -0
- package/src/utils/model-id.js +85 -0
- package/src/utils/provider-limits.js +84 -23
- package/src/utils/schema-validator.js +1 -1
- package/src/wizard/i18n.js +5 -4
- package/src/wizard/index.js +14 -0
- package/assets/logo - c/303/263pia.png +0 -0
- package/assets/logo.svg +0 -42
- package/assets/logo2.png +0 -0
- package/assets/social-preview.png +0 -0
- package/scripts/bundle-framework.js +0 -69
- package/scripts/changelog-generator.js +0 -222
- package/scripts/codebase-mapper.js +0 -728
- package/scripts/commit-message-generator.js +0 -167
- package/scripts/coverage-analyzer.js +0 -260
- package/scripts/dependency-analyzer.js +0 -280
- package/scripts/doctor/checks/agents.js +0 -77
- package/scripts/doctor/checks/constitution.js +0 -41
- package/scripts/doctor/checks/domain-alignment.js +0 -58
- package/scripts/doctor/checks/prism-layers.js +0 -84
- package/scripts/doctor/checks/registry.js +0 -55
- package/scripts/doctor/checks/schemas.js +0 -61
- package/scripts/doctor/fixes/reference-fix.js +0 -100
- package/scripts/doctor/fixes/registry-fix.js +0 -56
- package/scripts/doctor/index.js +0 -212
- package/scripts/framework-analyzer.js +0 -308
- package/scripts/generate-constitution-domain.js +0 -253
- package/scripts/generate-signing-key.js +0 -33
- package/scripts/health-check.js +0 -481
- package/scripts/ide-sync.js +0 -326
- package/scripts/performance-analyzer.js +0 -325
- package/scripts/plan-tracker.js +0 -278
- package/scripts/populate-entity-registry.js +0 -481
- package/scripts/pr-review.js +0 -317
- package/scripts/rollback-manager.js +0 -310
- package/scripts/semantic-lint.js +0 -328
- package/scripts/sign-manifest.js +0 -53
- package/scripts/stuck-detector.js +0 -343
- package/scripts/test-quality-assessment.js +0 -257
- package/scripts/validate-agents.js +0 -368
- package/scripts/validate-package.js +0 -367
- package/scripts/validate-tasks.js +0 -465
- package/src/autonomy/worktree-manager.js +0 -250
- package/src/intelligence/decision-engine.js +0 -256
- package/src/intelligence/document-sharder.js +0 -221
- package/src/intelligence/elicitation.js +0 -265
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/animations/useSmoothScroll.ts — scaffold motion-premium v1.0.0
|
|
3
|
+
*
|
|
4
|
+
* Initializes Lenis + GSAP integration exactly once per app lifecycle.
|
|
5
|
+
* Must be mounted in a top-level client component (phase 3: LenisProvider).
|
|
6
|
+
*
|
|
7
|
+
* Non-obvious choices:
|
|
8
|
+
*
|
|
9
|
+
* - `destroyed` flag closure + check inside `.then` — React 19 + strict mode
|
|
10
|
+
* double-mount problem. `initLenis()` is async (dynamic import of 'lenis').
|
|
11
|
+
* If the component mounts, unmounts, and remounts quickly (strict mode does
|
|
12
|
+
* this on first render in dev), the first .then callback may resolve AFTER
|
|
13
|
+
* the first unmount ran. Without the flag, the stale instance gets written
|
|
14
|
+
* to lenisRef.current and the cleanup function for the first mount destroys
|
|
15
|
+
* the NEW (second mount's) instance. Flag + immediate `lenis.destroy()` in
|
|
16
|
+
* the `if (destroyed)` branch guarantees at most one live Lenis, always.
|
|
17
|
+
*
|
|
18
|
+
* - `useRef<Awaited<ReturnType<typeof initLenis>> | null>` — typing the ref
|
|
19
|
+
* as the awaited return of the async function avoids duplicating the Lenis
|
|
20
|
+
* type import here. If `initLenis` is refactored later, the ref type
|
|
21
|
+
* follows automatically.
|
|
22
|
+
*
|
|
23
|
+
* - Returns the ref, not the instance — consumers (LenisProvider) can pass
|
|
24
|
+
* the ref through React context without re-rendering when the internal
|
|
25
|
+
* Lenis instance changes (refs don't trigger renders).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
'use client';
|
|
29
|
+
|
|
30
|
+
import { useEffect, useRef } from 'react';
|
|
31
|
+
import { initLenis } from './gsap';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Hook that initializes Lenis smooth scroll once per app lifecycle.
|
|
35
|
+
* Must be called in a top-level client component (e.g. LenisProvider in phase 3).
|
|
36
|
+
* Handles cleanup on unmount; race-safe under React strict mode.
|
|
37
|
+
*
|
|
38
|
+
* Returns a ref that holds the live Lenis instance, or null before init
|
|
39
|
+
* completes / after unmount.
|
|
40
|
+
*/
|
|
41
|
+
export function useSmoothScroll() {
|
|
42
|
+
const lenisRef = useRef<Awaited<ReturnType<typeof initLenis>> | null>(null);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let destroyed = false;
|
|
46
|
+
|
|
47
|
+
initLenis().then((lenis) => {
|
|
48
|
+
// Race guard: if the component already unmounted before Lenis finished
|
|
49
|
+
// loading (dynamic import), destroy the fresh instance immediately.
|
|
50
|
+
if (destroyed) {
|
|
51
|
+
lenis.destroy();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
lenisRef.current = lenis;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
destroyed = true;
|
|
59
|
+
if (lenisRef.current) {
|
|
60
|
+
lenisRef.current.destroy();
|
|
61
|
+
lenisRef.current = null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
return lenisRef;
|
|
67
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand tokens — single source of truth for project colors.
|
|
3
|
+
*
|
|
4
|
+
* Scaffold-applied file; user-owned (protected from upgrades via hash diff
|
|
5
|
+
* in .chati/manifest.json). Edit freely.
|
|
6
|
+
*
|
|
7
|
+
* To re-sync these values into app/globals.css (the CSS custom properties
|
|
8
|
+
* consumed by Tailwind v4), run:
|
|
9
|
+
*
|
|
10
|
+
* npx chati-dev scaffold motion-premium --sync-brand
|
|
11
|
+
*
|
|
12
|
+
* The scaffold-applier reads this file, resolves each placeholder in
|
|
13
|
+
* globals.css.template (e.g. {{PRIMARY_HEX}}), and rewrites only the brand
|
|
14
|
+
* --color-* var block. Your spacing/radius/shadow customizations are preserved.
|
|
15
|
+
*
|
|
16
|
+
* See scaffold/motion-premium/scaffold.yaml for the full placeholder map.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const brand = {
|
|
20
|
+
colors: {
|
|
21
|
+
/** Primary brand color (hex). Used for CTAs, focus rings, primary actions. */
|
|
22
|
+
primary: '{{PRIMARY_HEX}}',
|
|
23
|
+
/** Primary as "R, G, B" triple — required for rgba(var(--color-primary-rgb), alpha) constructs in shadows. */
|
|
24
|
+
primaryRgb: '{{PRIMARY_RGB}}',
|
|
25
|
+
/** Primary hover state (usually lighter/more saturated). */
|
|
26
|
+
primaryHover: '{{PRIMARY_HOVER_HEX}}',
|
|
27
|
+
/** Deepest background layer — page body default. */
|
|
28
|
+
bgDeep: '{{BG_DEEP_HEX}}',
|
|
29
|
+
/** Primary background level — main content surface. */
|
|
30
|
+
bgPrimary: '{{BG_PRIMARY_HEX}}',
|
|
31
|
+
/** Elevated background — cards, modals, panels. */
|
|
32
|
+
bgElevated: '{{BG_ELEVATED_HEX}}',
|
|
33
|
+
/** Primary text color — high-contrast copy. */
|
|
34
|
+
textPrimary: '{{TEXT_PRIMARY_HEX}}',
|
|
35
|
+
/** Secondary text — supporting copy, metadata. */
|
|
36
|
+
textSecondary: '{{TEXT_SECONDARY_HEX}}',
|
|
37
|
+
/** Inverse text — sits on primary-colored backgrounds (buttons etc). */
|
|
38
|
+
textInverse: '{{TEXT_INVERSE_HEX}}',
|
|
39
|
+
},
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
export type Brand = typeof brand;
|
|
43
|
+
export type BrandColor = keyof typeof brand.colors;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Scaffold Premium — motion-premium v1.0.0
|
|
2
|
+
#
|
|
3
|
+
# Declarative manifest for the motion-premium scaffold preset. Consumed by
|
|
4
|
+
# the scaffold-applier at apply time (Phase 4) to resolve placeholders and
|
|
5
|
+
# emit files into the target project.
|
|
6
|
+
#
|
|
7
|
+
# Contract: this schema is STABLE within v1.x. Additions only (new optional
|
|
8
|
+
# fields). Breaking changes bump the major version and provide migrations.
|
|
9
|
+
|
|
10
|
+
id: motion-premium
|
|
11
|
+
version: 1.0.0
|
|
12
|
+
description: "Premium animation infrastructure: GSAP + Lenis + cinematic providers. Opinionated for Next.js / React 18+ / Tailwind v4."
|
|
13
|
+
|
|
14
|
+
framework:
|
|
15
|
+
chati_dev: ">=4.3.0"
|
|
16
|
+
|
|
17
|
+
stack:
|
|
18
|
+
build_tools: ["nextjs", "react-vite"]
|
|
19
|
+
tailwind: ">=4.0.0"
|
|
20
|
+
typescript: ">=5.0.0"
|
|
21
|
+
node: ">=18.0.0"
|
|
22
|
+
react: ">=18.0.0"
|
|
23
|
+
# Runtime peer deps imported directly by templates. Bumping policy: every two
|
|
24
|
+
# minor bumps, re-validate templates. GSAP 3.12 is the stable line post-3.11;
|
|
25
|
+
# framer-motion 11 is the React 19 support line; lenis 1.1 introduced the
|
|
26
|
+
# `wrapper`/`content` refactor templates rely on.
|
|
27
|
+
gsap: "^3.12.5"
|
|
28
|
+
"framer-motion": "^11.0.0"
|
|
29
|
+
lenis: "^1.1.0"
|
|
30
|
+
|
|
31
|
+
# Placeholder resolution — keys reference brand source fields.
|
|
32
|
+
# Resolved once at scaffold apply time (Decision 3: install-time, one-shot).
|
|
33
|
+
# To re-sync CSS vars after editing brand.ts, run: scaffold motion-premium --sync-brand
|
|
34
|
+
placeholders:
|
|
35
|
+
PRIMARY_HEX:
|
|
36
|
+
source: brand.colors.primary
|
|
37
|
+
default: "#7C3AED"
|
|
38
|
+
type: hex
|
|
39
|
+
PRIMARY_RGB:
|
|
40
|
+
source: brand.colors.primaryRgb
|
|
41
|
+
default: "124, 58, 237"
|
|
42
|
+
type: rgb_triple
|
|
43
|
+
PRIMARY_HOVER_HEX:
|
|
44
|
+
source: brand.colors.primaryHover
|
|
45
|
+
default: "#6D28D9"
|
|
46
|
+
type: hex
|
|
47
|
+
BG_DEEP_HEX:
|
|
48
|
+
source: brand.colors.bgDeep
|
|
49
|
+
default: "#0A0A0F"
|
|
50
|
+
type: hex
|
|
51
|
+
BG_PRIMARY_HEX:
|
|
52
|
+
source: brand.colors.bgPrimary
|
|
53
|
+
default: "#111118"
|
|
54
|
+
type: hex
|
|
55
|
+
BG_ELEVATED_HEX:
|
|
56
|
+
source: brand.colors.bgElevated
|
|
57
|
+
default: "#1A1A24"
|
|
58
|
+
type: hex
|
|
59
|
+
TEXT_PRIMARY_HEX:
|
|
60
|
+
source: brand.colors.textPrimary
|
|
61
|
+
default: "#FAFAFA"
|
|
62
|
+
type: hex
|
|
63
|
+
TEXT_SECONDARY_HEX:
|
|
64
|
+
source: brand.colors.textSecondary
|
|
65
|
+
default: "#A1A1AA"
|
|
66
|
+
type: hex
|
|
67
|
+
TEXT_INVERSE_HEX:
|
|
68
|
+
source: brand.colors.textInverse
|
|
69
|
+
default: "#0A0A0F"
|
|
70
|
+
type: hex
|
|
71
|
+
|
|
72
|
+
# Brand source resolution priority (Decision 3). First match wins.
|
|
73
|
+
brand_sources:
|
|
74
|
+
- path: lib/brand.ts
|
|
75
|
+
type: typescript
|
|
76
|
+
description: "Existing brand tokens (brownfield or post-F3 scaffold install)"
|
|
77
|
+
- path: artifacts/4-UX/brandbook.md
|
|
78
|
+
type: markdown
|
|
79
|
+
description: "UX-authored brandbook (greenfield, post-UX phase)"
|
|
80
|
+
- path: artifacts/4-UX/brandbook.yaml
|
|
81
|
+
type: yaml
|
|
82
|
+
description: "Alternate brandbook format"
|
|
83
|
+
- type: interactive
|
|
84
|
+
prompt: true
|
|
85
|
+
description: "Fallback: ask user for each placeholder with default hints"
|
|
86
|
+
|
|
87
|
+
# Files shipped by this scaffold. Phase 1: foundation (2). Phase 2: animations (6). Phase 3: providers (5). Phase 4: ui contract (2). Total: 15.
|
|
88
|
+
files:
|
|
89
|
+
- source: app/globals.css.template
|
|
90
|
+
target: app/globals.css
|
|
91
|
+
phase: foundation
|
|
92
|
+
override: partial
|
|
93
|
+
notes: "Brand --color-* vars auto-synced from brand.ts; spacing/radius/shadow user-editable."
|
|
94
|
+
- source: lib/brand.ts.template
|
|
95
|
+
target: lib/brand.ts
|
|
96
|
+
phase: foundation
|
|
97
|
+
override: full
|
|
98
|
+
notes: "User-owned. Protected from upgrades via hash diff in manifest.json."
|
|
99
|
+
- source: lib/animations/gsap.ts.template
|
|
100
|
+
target: lib/animations/gsap.ts
|
|
101
|
+
phase: animations
|
|
102
|
+
override: none
|
|
103
|
+
notes: "Module-level plugin registration + Lenis/GSAP integration. Scaffold-owned — the 13 sauce patterns live here."
|
|
104
|
+
- source: lib/animations/tokens.ts.template
|
|
105
|
+
target: lib/animations/tokens.ts
|
|
106
|
+
phase: animations
|
|
107
|
+
override: partial
|
|
108
|
+
notes: "Adjust values, keep the exported shape (EASINGS/EASING_CURVES/DURATIONS/STAGGERS/SCROLL_TRIGGER_DEFAULTS)."
|
|
109
|
+
- source: lib/animations/useGsapContext.ts.template
|
|
110
|
+
target: lib/animations/useGsapContext.ts
|
|
111
|
+
phase: animations
|
|
112
|
+
override: none
|
|
113
|
+
notes: "Scoped context + reduced-motion safeguard + ctx.revert cleanup. Core pattern — do not modify."
|
|
114
|
+
- source: lib/animations/useSmoothScroll.ts.template
|
|
115
|
+
target: lib/animations/useSmoothScroll.ts
|
|
116
|
+
phase: animations
|
|
117
|
+
override: none
|
|
118
|
+
notes: "Race-guarded async Lenis init. Used by LenisProvider in phase 3."
|
|
119
|
+
- source: lib/animations/useScrollSnapStepper.ts.template
|
|
120
|
+
target: lib/animations/useScrollSnapStepper.ts
|
|
121
|
+
phase: animations
|
|
122
|
+
override: none
|
|
123
|
+
notes: "Wheel accumulator + touch + entry cooldown for panel-snap pins. Imported into components that use pinned sequences."
|
|
124
|
+
- source: lib/animations/refreshCoordinator.ts.template
|
|
125
|
+
target: lib/animations/refreshCoordinator.ts
|
|
126
|
+
phase: animations
|
|
127
|
+
override: none
|
|
128
|
+
notes: "Debounced ScrollTrigger.refresh() singleton. Coalesces mount-burst refreshes."
|
|
129
|
+
- source: components/animation/LenisProvider.tsx.template
|
|
130
|
+
target: components/animation/LenisProvider.tsx
|
|
131
|
+
phase: providers
|
|
132
|
+
override: none
|
|
133
|
+
notes: "Root client component for Lenis init + scrollRestoration manual enforcement."
|
|
134
|
+
- source: components/animation/BackgroundCrossfadeProvider.tsx.template
|
|
135
|
+
target: components/animation/BackgroundCrossfadeProvider.tsx
|
|
136
|
+
phase: providers
|
|
137
|
+
override: partial
|
|
138
|
+
notes: "User customizes color stops via props; ScrollTrigger logic is scaffold-owned."
|
|
139
|
+
- source: components/animation/PageTransitionWrapper.tsx.template
|
|
140
|
+
target: components/animation/PageTransitionWrapper.tsx
|
|
141
|
+
phase: providers
|
|
142
|
+
override: none
|
|
143
|
+
notes: "Framer Motion opacity fade on pathname change + post-exit scroll reset."
|
|
144
|
+
- source: components/animation/Preloader.tsx.template
|
|
145
|
+
target: components/animation/Preloader.tsx
|
|
146
|
+
phase: providers
|
|
147
|
+
override: partial
|
|
148
|
+
notes: "User supplies children (logo slot), videoSelector, durations; readiness gate + RAF are scaffold-owned."
|
|
149
|
+
- source: app/layout.tsx.template
|
|
150
|
+
target: app/layout.tsx
|
|
151
|
+
phase: providers
|
|
152
|
+
override: partial
|
|
153
|
+
notes: "Scaffold ships provider nesting + pre-hydration scroll/motion scripts. User fills metadata, fonts, chrome, analytics."
|
|
154
|
+
- source: components/ui/Container.tsx.template
|
|
155
|
+
target: components/ui/Container.tsx
|
|
156
|
+
phase: ui-contract
|
|
157
|
+
override: none
|
|
158
|
+
notes: "@contract: sole authority for max-width + horizontal padding. Never edit."
|
|
159
|
+
- source: components/ui/PageSection.tsx.template
|
|
160
|
+
target: components/ui/PageSection.tsx
|
|
161
|
+
phase: ui-contract
|
|
162
|
+
override: none
|
|
163
|
+
notes: "Vertical rhythm wrapper. noPadding / noContainer escape hatches for heroes and full-bleed layouts."
|
|
164
|
+
|
|
165
|
+
# Contract — principles this scaffold encodes.
|
|
166
|
+
# Violating any of these during dev breaks the premium guarantees.
|
|
167
|
+
principles:
|
|
168
|
+
- "One Container authority — components/ui/Container.tsx is the sole max-width + horizontal padding owner (Phase 4)"
|
|
169
|
+
- "Fluid type from day one — Utopia clamp scale across all text-* tokens, no retrofit"
|
|
170
|
+
- "xl breakpoint overridden to 75rem — eliminates the 1200–1279px dead zone (MacBook Air M4)"
|
|
171
|
+
- "Mobile-first stack; promote to desktop via matchMedia gates, not CSS-only"
|
|
172
|
+
- "Reduced motion first-class — @media (prefers-reduced-motion) + data-gsap-target safety net"
|
|
173
|
+
- "Single RAF loop — gsap.ticker drives Lenis (Phase 2)"
|
|
174
|
+
- "Scope isolation via gsap.context(fn, containerRef) (Phase 2)"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# motion-premium-3d
|
|
2
|
+
|
|
3
|
+
React Three Fiber + Three.js scroll-driven 3D layer for premium sites. Extends `motion-premium` — apply that one FIRST (the 2D base), then this one on top.
|
|
4
|
+
|
|
5
|
+
## When to apply
|
|
6
|
+
|
|
7
|
+
Apply when the project brief or Animation Inventory cites **explicit 3D intent**:
|
|
8
|
+
|
|
9
|
+
- Named 3D reference sites (Oryzo camera rig, Igloo Inc scenes, many Awwwards winners).
|
|
10
|
+
- Phrase-level signals: "scroll-synced 3D hero", "camera pull", "product rotation", "scroll-driven WebGL", "interactive scene".
|
|
11
|
+
- Animation Inventory rows whose "Reproducible via" column points at `scaffold/motion-premium-3d/*`.
|
|
12
|
+
|
|
13
|
+
The Fase 10 auto-gate fires for this preset when `scaffold_signals.motion_premium_3d.confidence >= 0.8` (higher threshold than 2D motion-premium's 0.7 per ADR-3D-07 — false positives here cost ~600KB of bundle pressed onto a project that did not need it).
|
|
14
|
+
|
|
15
|
+
## When NOT to apply
|
|
16
|
+
|
|
17
|
+
- 2D-only projects. Apply motion-premium alone.
|
|
18
|
+
- Mobile-first experiences where GPU budget is the primary constraint. The WebGL fallback (ADR-3D-05) handles low-end cases, but you pay the detection cost everywhere.
|
|
19
|
+
- Static marketing pages without scroll choreography.
|
|
20
|
+
- Projects on providers other than Claude Code where R3F's peer-dep matrix is hard to verify at install.
|
|
21
|
+
|
|
22
|
+
## What it ships
|
|
23
|
+
|
|
24
|
+
12 templates across 4 phases (webgl / canvas / scroll-scene / demo). Final inventory:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
lib/webgl/detect.ts.template
|
|
28
|
+
components/webgl/WebGLContext.tsx.template
|
|
29
|
+
components/webgl/SceneFallback.tsx.template
|
|
30
|
+
components/3d/CanvasProvider.tsx.template
|
|
31
|
+
components/3d/InvalidateOnScroll.tsx.template
|
|
32
|
+
components/3d/ScrollScene.tsx.template
|
|
33
|
+
components/3d/CameraRig.tsx.template
|
|
34
|
+
components/3d/ScrollCrossfade.tsx.template
|
|
35
|
+
components/3d/MeshCrossfade.tsx.template
|
|
36
|
+
app/(3d)/scroll-demo/page.tsx.template
|
|
37
|
+
app/(3d)/scroll-demo/ScrollDemoClient.tsx.template
|
|
38
|
+
app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
All 12 entries match `scaffold.yaml` `files[]` exactly. All target paths are DISJOINT from motion-premium. The two presets compose.
|
|
42
|
+
|
|
43
|
+
## Override classification
|
|
44
|
+
|
|
45
|
+
| File pattern | Override | Notes |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `lib/webgl/*` | none | Scaffold-owned — detection logic is invariant |
|
|
48
|
+
| `components/webgl/WebGLContext` | none | Context shape consumed by every 3D template |
|
|
49
|
+
| `components/webgl/SceneFallback` | partial | Extend with children (headline/CTA); keep role/aria contract |
|
|
50
|
+
| `components/3d/CanvasProvider` | none | Single-RAF contract + WebGL gate — do not modify |
|
|
51
|
+
| `components/3d/InvalidateOnScroll` | none | RAF bridge from gsap.ticker to R3F invalidate() |
|
|
52
|
+
| `components/3d/ScrollScene` | none | Pinned section + progress ref — scaffold-owned |
|
|
53
|
+
| `components/3d/CameraRig` | partial | User supplies keyframes[] via props; lerp internals fixed |
|
|
54
|
+
| `components/3d/ScrollCrossfade` | partial | Customise stops[] / at[] for uneven spacing |
|
|
55
|
+
| `components/3d/MeshCrossfade` | partial | Mesh geometry / materials are project-specific |
|
|
56
|
+
| `app/(3d)/scroll-demo/page` | partial | Route-group entry — customise or remove for production |
|
|
57
|
+
| `app/(3d)/scroll-demo/ScrollDemoClient` | partial | Client wrapper + overlay HTML — user extends |
|
|
58
|
+
| `app/(3d)/scroll-demo/ScrollDemoCanvas` | partial | Canvas subtree (CanvasProvider + InvalidateOnScroll + CameraRig + MeshCrossfade + Preload) |
|
|
59
|
+
|
|
60
|
+
## ADRs governing this preset
|
|
61
|
+
|
|
62
|
+
- **ADR-3D-01** — Separate preset, not extension
|
|
63
|
+
- **ADR-3D-02** — R3F + drei, not vanilla Three.js
|
|
64
|
+
- **ADR-3D-03** — Single RAF via `frameloop="demand"`
|
|
65
|
+
- **ADR-3D-04** — `next/dynamic({ ssr: false })` for all Canvas templates
|
|
66
|
+
- **ADR-3D-05** — WebGL detection + graceful degradation
|
|
67
|
+
- **ADR-3D-06** — Peer-deps pinned by minor (three `^0.160`, R3F `^8.15`)
|
|
68
|
+
- **ADR-3D-07** — New auto-gate signal with confidence threshold 0.8
|
|
69
|
+
- **ADR-3D-08** — `prefers-reduced-motion: reduce` MUST render the fallback
|
|
70
|
+
|
|
71
|
+
Full context in `.planning/motion-premium-3d-v1.md`.
|
|
72
|
+
|
|
73
|
+
## How to apply
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
npx chati-dev scaffold motion-premium # 2D base first
|
|
77
|
+
npx chati-dev scaffold motion-premium-3d # 3D layer on top
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or via the Fase 10 auto-gate after qa-planning, when signals align.
|
package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ScrollDemoCanvas — dynamically imported via next/dynamic({ ssr: false })
|
|
5
|
+
* from ScrollDemoClient. Owns the Canvas subtree.
|
|
6
|
+
*
|
|
7
|
+
* Bundles:
|
|
8
|
+
* @react-three/fiber — Canvas, useThree, useFrame
|
|
9
|
+
* @react-three/drei — Preload (utility)
|
|
10
|
+
* three — Vector3, geometries, materials
|
|
11
|
+
* Rest of the app does not import these; the dynamic boundary isolates
|
|
12
|
+
* the ~600KB 3D bundle from pages that do not need it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Preload } from '@react-three/drei';
|
|
16
|
+
import { CanvasProvider } from '@/components/3d/CanvasProvider';
|
|
17
|
+
import { InvalidateOnScroll } from '@/components/3d/InvalidateOnScroll';
|
|
18
|
+
import { CameraRig } from '@/components/3d/CameraRig';
|
|
19
|
+
import { MeshCrossfade } from '@/components/3d/MeshCrossfade';
|
|
20
|
+
import { SceneFallback } from '@/components/webgl/SceneFallback';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Keyframes for the demo camera path. Replace with your own keyframes
|
|
24
|
+
* when customising — CameraRig lerps position + lookAt + optional FOV.
|
|
25
|
+
*/
|
|
26
|
+
const DEMO_KEYFRAMES = [
|
|
27
|
+
{ at: 0, position: [0, 0, 8] as [number, number, number], lookAt: [0, 0, 0] as [number, number, number], fov: 50 },
|
|
28
|
+
{ at: 0.5, position: [4, 2, 6] as [number, number, number], lookAt: [0, 0, 0] as [number, number, number], fov: 55 },
|
|
29
|
+
{ at: 1, position: [0, 4, 4] as [number, number, number], lookAt: [0, 0, 0] as [number, number, number], fov: 60 },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export default function ScrollDemoCanvas() {
|
|
33
|
+
return (
|
|
34
|
+
<CanvasProvider fallback={<SceneFallback fullBleed accessibleLabel="3D scene unavailable" />}>
|
|
35
|
+
<InvalidateOnScroll />
|
|
36
|
+
|
|
37
|
+
<ambientLight intensity={0.4} />
|
|
38
|
+
<directionalLight position={[5, 5, 5]} intensity={0.8} />
|
|
39
|
+
|
|
40
|
+
<CameraRig keyframes={DEMO_KEYFRAMES} />
|
|
41
|
+
|
|
42
|
+
{/*
|
|
43
|
+
MeshCrossfade: single slot; only the active variant mounts.
|
|
44
|
+
Replace primitives with your own glTF meshes (useGLTF from drei).
|
|
45
|
+
*/}
|
|
46
|
+
<MeshCrossfade
|
|
47
|
+
variants={[
|
|
48
|
+
{
|
|
49
|
+
at: 0,
|
|
50
|
+
render: () => (
|
|
51
|
+
<mesh>
|
|
52
|
+
<boxGeometry args={[1.5, 1.5, 1.5]} />
|
|
53
|
+
<meshStandardMaterial color="#7C3AED" />
|
|
54
|
+
</mesh>
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
at: 0.5,
|
|
59
|
+
render: () => (
|
|
60
|
+
<mesh>
|
|
61
|
+
<sphereGeometry args={[1, 32, 32]} />
|
|
62
|
+
<meshStandardMaterial color="#A78BFA" />
|
|
63
|
+
</mesh>
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
at: 1,
|
|
68
|
+
render: () => (
|
|
69
|
+
<mesh>
|
|
70
|
+
<torusGeometry args={[1, 0.4, 16, 64]} />
|
|
71
|
+
<meshStandardMaterial color="#EDE9FE" />
|
|
72
|
+
</mesh>
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
]}
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<Preload all />
|
|
79
|
+
</CanvasProvider>
|
|
80
|
+
);
|
|
81
|
+
}
|
package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ScrollDemoClient — working example wiring together every P2-P4
|
|
5
|
+
* template. A single pinned scroll scene with:
|
|
6
|
+
* - WebGL gate (P2: WebGLContext + SceneFallback)
|
|
7
|
+
* - Canvas mount with single-RAF (P3: CanvasProvider +
|
|
8
|
+
* InvalidateOnScroll)
|
|
9
|
+
* - Scroll-driven camera (P4: CameraRig)
|
|
10
|
+
* - Mesh crossfade (P4: MeshCrossfade)
|
|
11
|
+
*
|
|
12
|
+
* Real-world scenes replace the box/sphere/torus primitives with
|
|
13
|
+
* glTF meshes loaded via drei's `useGLTF` (not imported here to keep
|
|
14
|
+
* the demo light; see motion-premium-3d README for asset pipeline
|
|
15
|
+
* guidance).
|
|
16
|
+
*
|
|
17
|
+
* Preload hint: drei's <Preload all /> tells R3F to compile all
|
|
18
|
+
* materials in the scene at mount time, eliminating the flash that
|
|
19
|
+
* otherwise happens when a new material appears mid-scroll.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useRef } from 'react';
|
|
23
|
+
import dynamic from 'next/dynamic';
|
|
24
|
+
import { Preload } from '@react-three/drei';
|
|
25
|
+
import { Container } from '@/components/ui/Container';
|
|
26
|
+
import { WebGLProvider } from '@/components/webgl/WebGLContext';
|
|
27
|
+
import { SceneFallback } from '@/components/webgl/SceneFallback';
|
|
28
|
+
import {
|
|
29
|
+
ScrollScene,
|
|
30
|
+
ScrollSceneContext,
|
|
31
|
+
type ScrollSceneRef,
|
|
32
|
+
} from '@/components/3d/ScrollScene';
|
|
33
|
+
|
|
34
|
+
// ADR-3D-04: R3F / Three.js dependencies must load client-only. Wrapping
|
|
35
|
+
// the scene in next/dynamic with ssr: false ensures Next never attempts
|
|
36
|
+
// to run Canvas code on the server, and the loading state paints the
|
|
37
|
+
// considered fallback instead of a blank canvas during hydration.
|
|
38
|
+
const ScrollDemoCanvas = dynamic(() => import('./ScrollDemoCanvas'), {
|
|
39
|
+
ssr: false,
|
|
40
|
+
loading: () => <SceneFallback fullBleed accessibleLabel="Loading scroll demo" />,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export default function ScrollDemoClient() {
|
|
44
|
+
const sceneRef = useRef<ScrollSceneRef>(null);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<WebGLProvider>
|
|
48
|
+
<ScrollScene ref={sceneRef} length={3} scrub={true}>
|
|
49
|
+
<Container className="relative z-10 flex h-full flex-col justify-end pb-24">
|
|
50
|
+
<p className="text-sm uppercase tracking-wider opacity-60">motion-premium-3d demo</p>
|
|
51
|
+
<h1 className="text-display-lg max-w-2xl">
|
|
52
|
+
Scroll to animate the scene.
|
|
53
|
+
</h1>
|
|
54
|
+
</Container>
|
|
55
|
+
|
|
56
|
+
<div className="absolute inset-0">
|
|
57
|
+
<ScrollSceneContext.Provider value={sceneRef.current?.progress ?? null}>
|
|
58
|
+
<ScrollDemoCanvas />
|
|
59
|
+
</ScrollSceneContext.Provider>
|
|
60
|
+
</div>
|
|
61
|
+
</ScrollScene>
|
|
62
|
+
</WebGLProvider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Sub-components defined below are referenced only via next/dynamic above.
|
|
68
|
+
// They are exported from the Client module so the dynamic import is cheap
|
|
69
|
+
// (single chunk). In production demos, prefer a separate file for clearer
|
|
70
|
+
// code-split boundaries.
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
export function ScrollDemoCanvasInline() {
|
|
74
|
+
return <Preload all />;
|
|
75
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /scroll-demo — demo route for motion-premium-3d.
|
|
3
|
+
*
|
|
4
|
+
* Server Component by design: metadata exports live here; the actual
|
|
5
|
+
* scroll-scene + Canvas tree live in the sibling Client Component
|
|
6
|
+
* (ScrollDemoClient). This separation keeps the route group
|
|
7
|
+
* "(3d)" URL-transparent while respecting the Next.js 16 rule that
|
|
8
|
+
* `next/dynamic({ ssr: false })` must be invoked from a Client boundary.
|
|
9
|
+
*
|
|
10
|
+
* Users typically remove this route after validating the scaffold on
|
|
11
|
+
* their own content. The `(3d)` route group ( -parens- ) means URLs
|
|
12
|
+
* stay at `/scroll-demo`; dropping the whole `app/(3d)/` folder is a
|
|
13
|
+
* one-step undo.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Metadata } from 'next';
|
|
17
|
+
import ScrollDemoClient from './ScrollDemoClient';
|
|
18
|
+
|
|
19
|
+
export const metadata: Metadata = {
|
|
20
|
+
title: 'Scroll Demo — motion-premium-3d',
|
|
21
|
+
description: 'Scaffold demo: pinned scroll scene, camera rig tied to scroll progress, scene crossfade. Remove when validated.',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default function ScrollDemoPage() {
|
|
25
|
+
return <ScrollDemoClient />;
|
|
26
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CameraRig — interpolates camera position, target, and optional FOV
|
|
5
|
+
* across keyframes driven by the ScrollSceneContext progress ref.
|
|
6
|
+
*
|
|
7
|
+
* Must mount inside <CanvasProvider> AND inside <ScrollScene>'s
|
|
8
|
+
* context. Reads progress on every frame via useFrame — zero React
|
|
9
|
+
* re-renders, no state library.
|
|
10
|
+
*
|
|
11
|
+
* Keyframe format: an array of { at: 0..1, position, lookAt, fov? }
|
|
12
|
+
* sorted by `at`. Between keyframes, linear interpolation (or custom
|
|
13
|
+
* easing via `ease` on the keyframe). Before the first/after the last,
|
|
14
|
+
* the camera clamps to that keyframe's pose.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* <CameraRig
|
|
18
|
+
* keyframes={[
|
|
19
|
+
* { at: 0, position: [0, 0, 8], lookAt: [0, 0, 0], fov: 50 },
|
|
20
|
+
* { at: 0.5, position: [4, 2, 6], lookAt: [0, 0, 0] },
|
|
21
|
+
* { at: 1, position: [0, 4, 3], lookAt: [0, 0, 0], fov: 60 },
|
|
22
|
+
* ]}
|
|
23
|
+
* />
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { useContext, useMemo } from 'react';
|
|
27
|
+
import { useThree, useFrame } from '@react-three/fiber';
|
|
28
|
+
import { Vector3, PerspectiveCamera as ThreePerspectiveCamera } from 'three';
|
|
29
|
+
import { ScrollSceneContext } from './ScrollScene';
|
|
30
|
+
|
|
31
|
+
export interface CameraKeyframe {
|
|
32
|
+
at: number; // 0..1
|
|
33
|
+
position: [number, number, number];
|
|
34
|
+
lookAt: [number, number, number];
|
|
35
|
+
fov?: number;
|
|
36
|
+
/** Custom easing on the segment ending at this keyframe (optional). */
|
|
37
|
+
ease?: (t: number) => number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CameraRigProps {
|
|
41
|
+
keyframes: CameraKeyframe[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_EASE = (t: number) => t; // linear; override per keyframe
|
|
45
|
+
|
|
46
|
+
export function CameraRig({ keyframes }: CameraRigProps) {
|
|
47
|
+
const progressRef = useContext(ScrollSceneContext);
|
|
48
|
+
const camera = useThree((s) => s.camera);
|
|
49
|
+
|
|
50
|
+
// Sorted copy + Vector3 presets — done once.
|
|
51
|
+
const kf = useMemo(() => {
|
|
52
|
+
const sorted = [...keyframes].sort((a, b) => a.at - b.at);
|
|
53
|
+
return sorted.map((k) => ({
|
|
54
|
+
at: k.at,
|
|
55
|
+
position: new Vector3(...k.position),
|
|
56
|
+
lookAt: new Vector3(...k.lookAt),
|
|
57
|
+
fov: k.fov,
|
|
58
|
+
ease: k.ease || DEFAULT_EASE,
|
|
59
|
+
}));
|
|
60
|
+
}, [keyframes]);
|
|
61
|
+
|
|
62
|
+
// Scratch vectors reused every frame (no per-frame allocation).
|
|
63
|
+
const posScratch = useMemo(() => new Vector3(), []);
|
|
64
|
+
const lookAtScratch = useMemo(() => new Vector3(), []);
|
|
65
|
+
|
|
66
|
+
useFrame(() => {
|
|
67
|
+
if (!progressRef || kf.length === 0) return;
|
|
68
|
+
const t = Math.max(0, Math.min(1, progressRef.current.progress));
|
|
69
|
+
|
|
70
|
+
// Find the bracketing keyframe pair.
|
|
71
|
+
let a = kf[0];
|
|
72
|
+
let b = kf[kf.length - 1];
|
|
73
|
+
for (let i = 0; i < kf.length - 1; i++) {
|
|
74
|
+
if (t >= kf[i].at && t <= kf[i + 1].at) {
|
|
75
|
+
a = kf[i];
|
|
76
|
+
b = kf[i + 1];
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const range = b.at - a.at;
|
|
82
|
+
const local = range === 0 ? 0 : (t - a.at) / range;
|
|
83
|
+
const eased = b.ease(local);
|
|
84
|
+
|
|
85
|
+
posScratch.lerpVectors(a.position, b.position, eased);
|
|
86
|
+
lookAtScratch.lerpVectors(a.lookAt, b.lookAt, eased);
|
|
87
|
+
|
|
88
|
+
camera.position.copy(posScratch);
|
|
89
|
+
camera.lookAt(lookAtScratch);
|
|
90
|
+
|
|
91
|
+
// FOV interpolation (only meaningful on perspective cameras).
|
|
92
|
+
if (a.fov != null && b.fov != null && (camera as ThreePerspectiveCamera).isPerspectiveCamera) {
|
|
93
|
+
const fov = a.fov + (b.fov - a.fov) * eased;
|
|
94
|
+
(camera as ThreePerspectiveCamera).fov = fov;
|
|
95
|
+
(camera as ThreePerspectiveCamera).updateProjectionMatrix();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|