@struq/skill 0.0.0-stage → 0.2.2
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/LICENSE +21 -0
- package/README.md +141 -2
- package/cli/struq.mjs +132 -0
- package/package.json +41 -4
- package/skills/struq/SKILL.md +237 -0
- package/skills/struq/cinematic/README.md +409 -0
- package/skills/struq/cinematic/checklists/01-preproduction.md +15 -0
- package/skills/struq/cinematic/checklists/02-implementation.md +17 -0
- package/skills/struq/cinematic/checklists/03-polish.md +17 -0
- package/skills/struq/cinematic/examples/code/nextjs/README.md +13 -0
- package/skills/struq/cinematic/examples/code/nextjs/app/cinematic-experience.tsx +13 -0
- package/skills/struq/cinematic/examples/code/nextjs/app/components/LenisRoot.tsx +42 -0
- package/skills/struq/cinematic/examples/code/nextjs/app/page.tsx +6 -0
- package/skills/struq/cinematic/examples/code/shared/CameraRig.tsx +65 -0
- package/skills/struq/cinematic/examples/code/shared/ExperienceCanvas.tsx +20 -0
- package/skills/struq/cinematic/examples/code/shared/ExperienceDirector.tsx +57 -0
- package/skills/struq/cinematic/examples/code/shared/ExperienceOverlay.tsx +37 -0
- package/skills/struq/cinematic/examples/code/shared/ExperiencePage.tsx +52 -0
- package/skills/struq/cinematic/examples/code/shared/GsapFrameLoop.tsx +32 -0
- package/skills/struq/cinematic/examples/code/shared/InteriorRoot.tsx +30 -0
- package/skills/struq/cinematic/examples/code/shared/README.md +20 -0
- package/skills/struq/cinematic/examples/code/shared/SubjectRig.tsx +58 -0
- package/skills/struq/cinematic/examples/code/shared/build-experience-timeline.ts +237 -0
- package/skills/struq/cinematic/examples/code/shared/direction.ts +52 -0
- package/skills/struq/cinematic/examples/code/shared/gsap-client.ts +32 -0
- package/skills/struq/cinematic/examples/code/shared/math.ts +12 -0
- package/skills/struq/cinematic/examples/code/shared/registry.ts +58 -0
- package/skills/struq/cinematic/examples/code/shared/runtime.ts +52 -0
- package/skills/struq/cinematic/examples/code/shared/styles.css +127 -0
- package/skills/struq/cinematic/examples/code/shared/useExperienceInput.ts +42 -0
- package/skills/struq/cinematic/examples/code/vite/README.md +14 -0
- package/skills/struq/cinematic/examples/code/vite/src/App.tsx +11 -0
- package/skills/struq/cinematic/examples/code/vite/src/LenisRoot.tsx +37 -0
- package/skills/struq/cinematic/examples/code/vite/src/main.tsx +9 -0
- package/skills/struq/cinematic/examples/concise/01-chapter-spec-example.md +42 -0
- package/skills/struq/cinematic/examples/concise/02-handoff-example.md +25 -0
- package/skills/struq/cinematic/examples/concise/03-audit-finding-example.md +13 -0
- package/skills/struq/cinematic/examples/verbose/01-full-experience-direction.md +687 -0
- package/skills/struq/cinematic/examples/verbose/02-full-audit-example.md +362 -0
- package/skills/struq/cinematic/references/01-core-mental-model.md +221 -0
- package/skills/struq/cinematic/references/02-vocabulary.md +384 -0
- package/skills/struq/cinematic/references/03-experience-architecture.md +302 -0
- package/skills/struq/cinematic/references/04-timeline-choreography.md +264 -0
- package/skills/struq/cinematic/references/05-handoff-contracts.md +241 -0
- package/skills/struq/cinematic/references/06-three-rig-architecture.md +286 -0
- package/skills/struq/cinematic/references/07-stack-integration.md +228 -0
- package/skills/struq/cinematic/references/08-gsap-plugin-patterns.md +218 -0
- package/skills/struq/cinematic/references/09-tuning-harness.md +202 -0
- package/skills/struq/cinematic/references/10-quality-systems.md +206 -0
- package/skills/struq/cinematic/references/11-agent-workflows.md +246 -0
- package/skills/struq/cinematic/references/12-audit-rubric.md +176 -0
- package/skills/struq/cinematic/references/13-official-docs.md +53 -0
- package/skills/struq/cinematic/templates/01-experience-brief.md +105 -0
- package/skills/struq/cinematic/templates/02-experience-contract.md +80 -0
- package/skills/struq/cinematic/templates/03-chapter-spec.md +103 -0
- package/skills/struq/cinematic/templates/04-handoff-contract.md +102 -0
- package/skills/struq/cinematic/templates/05-ownership-matrix.md +29 -0
- package/skills/struq/cinematic/templates/06-shot-presets.md +59 -0
- package/skills/struq/cinematic/templates/07-implementation-slice.md +66 -0
- package/skills/struq/cinematic/templates/08-tuning-register.md +36 -0
- package/skills/struq/cinematic/templates/09-acceptance-tests.md +57 -0
- package/skills/struq/references/anti-patterns.md +265 -0
- package/skills/struq/references/baseline-prompt.md +118 -0
- package/skills/struq/references/brand.md +143 -0
- package/skills/struq/references/brief-router.md +219 -0
- package/skills/struq/references/composition.md +191 -0
- package/skills/struq/references/depth-parallax.md +235 -0
- package/skills/struq/references/direction.md +188 -0
- package/skills/struq/references/feel-layer.md +107 -0
- package/skills/struq/references/gsap-api.md +293 -0
- package/skills/struq/references/iconography.md +255 -0
- package/skills/struq/references/imagery.md +144 -0
- package/skills/struq/references/loaders.md +287 -0
- package/skills/struq/references/micro-interactions.md +270 -0
- package/skills/struq/references/mobile-composition.md +70 -0
- package/skills/struq/references/nav-header.md +296 -0
- package/skills/struq/references/page-architecture.md +134 -0
- package/skills/struq/references/page-entrances.md +198 -0
- package/skills/struq/references/performance.md +235 -0
- package/skills/struq/references/pinning-scrub.md +341 -0
- package/skills/struq/references/pitfalls.md +290 -0
- package/skills/struq/references/route-transitions.md +281 -0
- package/skills/struq/references/scaling.md +151 -0
- package/skills/struq/references/scroll-reveals.md +280 -0
- package/skills/struq/references/seo.md +219 -0
- package/skills/struq/references/setup.md +397 -0
- package/skills/struq/references/showcase.md +87 -0
- package/skills/struq/references/stack-targets.md +78 -0
- package/skills/struq/references/struq-imagery.md +107 -0
- package/skills/struq/references/svg-motion.md +390 -0
- package/skills/struq/references/transitions.md +160 -0
- package/skills/struq/references/triage.md +71 -0
- package/skills/struq/references/typography.md +720 -0
- package/skills/struq/references/vibe-presets.md +287 -0
- package/skills/struq/references/webgl-r3f.md +274 -0
- package/skills/struq/references/webgl-scenes.md +593 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type ViewportClass = 'desktop' | 'tablet' | 'mobile';
|
|
2
|
+
export type PerformanceTier = 'high' | 'balanced' | 'safe';
|
|
3
|
+
|
|
4
|
+
export type ExperienceRuntime = {
|
|
5
|
+
globalProgress: number;
|
|
6
|
+
activeChapter: 'arrival' | 'material' | 'interior';
|
|
7
|
+
chapterProgress: number;
|
|
8
|
+
activeHandoff: string | null;
|
|
9
|
+
elapsedTime: number;
|
|
10
|
+
deltaTime: number;
|
|
11
|
+
scrollVelocity: number;
|
|
12
|
+
scrollDirection: -1 | 0 | 1;
|
|
13
|
+
pointer: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
speed: number;
|
|
17
|
+
};
|
|
18
|
+
channels: {
|
|
19
|
+
pointerCameraStrength: number;
|
|
20
|
+
narrativeDistortion: number;
|
|
21
|
+
velocityDistortion: number;
|
|
22
|
+
interiorReveal: number;
|
|
23
|
+
};
|
|
24
|
+
viewport: ViewportClass;
|
|
25
|
+
reducedMotion: boolean;
|
|
26
|
+
performanceTier: PerformanceTier;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const runtime: ExperienceRuntime = {
|
|
30
|
+
globalProgress: 0,
|
|
31
|
+
activeChapter: 'arrival',
|
|
32
|
+
chapterProgress: 0,
|
|
33
|
+
activeHandoff: null,
|
|
34
|
+
elapsedTime: 0,
|
|
35
|
+
deltaTime: 0,
|
|
36
|
+
scrollVelocity: 0,
|
|
37
|
+
scrollDirection: 0,
|
|
38
|
+
pointer: { x: 0, y: 0, speed: 0 },
|
|
39
|
+
channels: {
|
|
40
|
+
pointerCameraStrength: 0.035,
|
|
41
|
+
narrativeDistortion: 0,
|
|
42
|
+
velocityDistortion: 0,
|
|
43
|
+
interiorReveal: 0,
|
|
44
|
+
},
|
|
45
|
+
viewport: 'desktop',
|
|
46
|
+
reducedMotion: false,
|
|
47
|
+
performanceTier: 'balanced',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function updateViewportClass(width: number): void {
|
|
51
|
+
runtime.viewport = width < 768 ? 'mobile' : width < 1100 ? 'tablet' : 'desktop';
|
|
52
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
html,
|
|
6
|
+
body {
|
|
7
|
+
margin: 0;
|
|
8
|
+
background: #080808;
|
|
9
|
+
color: #f5f2ea;
|
|
10
|
+
font-family: system-ui, sans-serif;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
button,
|
|
14
|
+
a {
|
|
15
|
+
font: inherit;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.experience-root {
|
|
19
|
+
min-height: 100%;
|
|
20
|
+
background: #080808;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.experience-narrative {
|
|
24
|
+
position: relative;
|
|
25
|
+
min-height: 1000vh;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.experience-stage {
|
|
29
|
+
position: relative;
|
|
30
|
+
width: 100%;
|
|
31
|
+
height: 100svh;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.experience-stage canvas {
|
|
36
|
+
position: absolute;
|
|
37
|
+
inset: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.experience-scroll-space {
|
|
41
|
+
height: 900vh;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.experience-overlay {
|
|
45
|
+
position: absolute;
|
|
46
|
+
inset: 0;
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
display: grid;
|
|
49
|
+
align-items: center;
|
|
50
|
+
padding: clamp(1rem, 0.5rem + 3.5vw, 4rem);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.experience-copy {
|
|
54
|
+
width: min(34rem, 80vw);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Demo values. In a Struq build the h1 takes `font-display text-display` and the
|
|
58
|
+
pairing's tracking from typography.md §3.1; the clamp keeps a rem term for zoom. */
|
|
59
|
+
.experience-copy h1 {
|
|
60
|
+
margin: 0;
|
|
61
|
+
font-size: clamp(2.875rem, 1.2rem + 6.86vw, 8rem);
|
|
62
|
+
font-weight: 300;
|
|
63
|
+
line-height: 0.96;
|
|
64
|
+
letter-spacing: -0.03em;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.experience-bridge {
|
|
68
|
+
position: absolute;
|
|
69
|
+
inset: auto 5vw 8vh auto;
|
|
70
|
+
width: min(32rem, 45vw);
|
|
71
|
+
overflow: visible;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.local-ui-button {
|
|
75
|
+
position: absolute;
|
|
76
|
+
left: clamp(1rem, 0.5rem + 3.5vw, 4rem);
|
|
77
|
+
bottom: 4vh;
|
|
78
|
+
pointer-events: auto;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.experience-semantic-content {
|
|
82
|
+
position: absolute;
|
|
83
|
+
inset: 0;
|
|
84
|
+
pointer-events: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.experience-semantic-content section {
|
|
88
|
+
min-height: 300vh;
|
|
89
|
+
padding: 2rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.experience-semantic-content h2,
|
|
93
|
+
.experience-semantic-content p {
|
|
94
|
+
position: absolute;
|
|
95
|
+
width: 1px;
|
|
96
|
+
height: 1px;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
clip: rect(0 0 0 0);
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.experience-footer {
|
|
103
|
+
min-height: 70vh;
|
|
104
|
+
display: grid;
|
|
105
|
+
place-items: center;
|
|
106
|
+
padding: 2rem;
|
|
107
|
+
background: #ece9e0;
|
|
108
|
+
color: #111;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@media (max-width: 767px) {
|
|
112
|
+
.experience-copy {
|
|
113
|
+
align-self: start;
|
|
114
|
+
margin-top: 12vh;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.experience-bridge {
|
|
118
|
+
width: 70vw;
|
|
119
|
+
right: 15vw;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@media (prefers-reduced-motion: reduce) {
|
|
124
|
+
.local-ui-button {
|
|
125
|
+
transition: none;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { runtime, updateViewportClass } from './runtime';
|
|
3
|
+
|
|
4
|
+
export function useExperienceInput(): void {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
let previousX = window.innerWidth / 2;
|
|
7
|
+
let previousY = window.innerHeight / 2;
|
|
8
|
+
|
|
9
|
+
const onPointerMove = (event: PointerEvent) => {
|
|
10
|
+
const x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
11
|
+
const y = (event.clientY / window.innerHeight) * 2 - 1;
|
|
12
|
+
const dx = event.clientX - previousX;
|
|
13
|
+
const dy = event.clientY - previousY;
|
|
14
|
+
|
|
15
|
+
runtime.pointer.x = x;
|
|
16
|
+
runtime.pointer.y = y;
|
|
17
|
+
runtime.pointer.speed = Math.min(1, Math.hypot(dx, dy) / 80);
|
|
18
|
+
|
|
19
|
+
previousX = event.clientX;
|
|
20
|
+
previousY = event.clientY;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const onResize = () => updateViewportClass(window.innerWidth);
|
|
24
|
+
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
25
|
+
const onMotionChange = () => {
|
|
26
|
+
runtime.reducedMotion = reducedMotion.matches;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
onResize();
|
|
30
|
+
onMotionChange();
|
|
31
|
+
|
|
32
|
+
window.addEventListener('pointermove', onPointerMove, { passive: true });
|
|
33
|
+
window.addEventListener('resize', onResize);
|
|
34
|
+
reducedMotion.addEventListener('change', onMotionChange);
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
38
|
+
window.removeEventListener('resize', onResize);
|
|
39
|
+
reducedMotion.removeEventListener('change', onMotionChange);
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Vite Adapter
|
|
2
|
+
|
|
3
|
+
## Create and install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create vite@latest cinematic-experience -- --template react-ts
|
|
7
|
+
cd cinematic-experience
|
|
8
|
+
npm install
|
|
9
|
+
npm install gsap @gsap/react lenis three @react-three/fiber @react-three/drei motion
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Copy `cinematic/examples/code/shared` to `src/experience`, then use the adapter files in this folder.
|
|
13
|
+
|
|
14
|
+
Vite does not require a Next.js client boundary, but the same cleanup, target readiness, ownership, and responsive rules apply.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
import { ExperiencePage } from './experience/ExperiencePage';
|
|
3
|
+
import { LenisRoot } from './LenisRoot';
|
|
4
|
+
|
|
5
|
+
export default function App(): JSX.Element {
|
|
6
|
+
return (
|
|
7
|
+
<LenisRoot>
|
|
8
|
+
<ExperiencePage />
|
|
9
|
+
</LenisRoot>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ReactLenis, useLenis, type LenisRef } from 'lenis/react';
|
|
2
|
+
import 'lenis/dist/lenis.css';
|
|
3
|
+
import { useEffect, useRef, type JSX, type ReactNode } from 'react';
|
|
4
|
+
import { gsap, ScrollTrigger, registerGsap } from './experience/gsap-client';
|
|
5
|
+
|
|
6
|
+
registerGsap();
|
|
7
|
+
|
|
8
|
+
type LenisRootProps = {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function LenisRoot({ children }: LenisRootProps): JSX.Element {
|
|
13
|
+
const lenisRef = useRef<LenisRef>(null);
|
|
14
|
+
|
|
15
|
+
// ScrollTrigger sync without capturing the instance once.
|
|
16
|
+
useLenis(() => ScrollTrigger.update());
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
// Registered unconditionally, instance resolved per frame (references/setup.md §2).
|
|
20
|
+
const update = (time: number) => {
|
|
21
|
+
lenisRef.current?.lenis?.raf(time * 1000);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
gsap.ticker.add(update);
|
|
25
|
+
gsap.ticker.lagSmoothing(0);
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
gsap.ticker.remove(update);
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<ReactLenis root ref={lenisRef} options={{ lerp: 0.1, anchors: true, autoRaf: false }}>
|
|
34
|
+
{children}
|
|
35
|
+
</ReactLenis>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Concise Chapter Example: Product Disassembly
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Reveal three functional systems by spatially separating one hero object while preserving it as a single readable composition.
|
|
6
|
+
|
|
7
|
+
## State
|
|
8
|
+
|
|
9
|
+
**Incoming:** complete object in shot `heroThreeQuarter`; title visible; pointer yaw strength `0.045`.
|
|
10
|
+
|
|
11
|
+
**Outgoing:** three layers separated along the camera's depth axis; outer ring centered and facing camera as the bridge into the next chapter.
|
|
12
|
+
|
|
13
|
+
## Progress
|
|
14
|
+
|
|
15
|
+
| Range | Action |
|
|
16
|
+
|---|---|
|
|
17
|
+
| 0.00-0.12 | Hold hero pose; lower ambient noise and prepare rim light. |
|
|
18
|
+
| 0.12-0.30 | Dolly 8% closer; SplitText title exits by lines toward the left vanishing point. |
|
|
19
|
+
| 0.30-0.62 | Separate the three product layers with 60 ms overlap; reveal one feature label per layer. |
|
|
20
|
+
| 0.62-0.78 | Hold separation; allow restrained pointer depth response. |
|
|
21
|
+
| 0.78-0.90 | Rotate outer ring toward camera and move other layers behind it. |
|
|
22
|
+
| 0.90-1.00 | Reduce pointer influence; ring reaches viewport center and becomes the transition aperture. |
|
|
23
|
+
|
|
24
|
+
## Cues
|
|
25
|
+
|
|
26
|
+
- `chapter:enter` at 0.00
|
|
27
|
+
- `camera:commit` at 0.12
|
|
28
|
+
- `layers:readable` at 0.62
|
|
29
|
+
- `handoff:armed` at 0.78
|
|
30
|
+
- `aperture:aligned` at 0.94
|
|
31
|
+
- `chapter:resolved` at 1.00
|
|
32
|
+
|
|
33
|
+
## Ownership
|
|
34
|
+
|
|
35
|
+
- GSAP chapter timeline owns narrative camera and layer placement rigs.
|
|
36
|
+
- R3F `useFrame` owns layer idle float and pointer reaction rigs.
|
|
37
|
+
- SplitText timeline owns the title mask wrappers.
|
|
38
|
+
- Handoff timeline takes the outer-ring placement rig at `handoff:armed`.
|
|
39
|
+
|
|
40
|
+
## Mobile
|
|
41
|
+
|
|
42
|
+
Use a shallower layer separation and move labels below the subject. Replace lateral camera truck with a target shift. Increase ring scale by 18% to guarantee transition coverage.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Concise Handoff Example: Ring to Portal
|
|
2
|
+
|
|
3
|
+
**From:** `disassembly`
|
|
4
|
+
|
|
5
|
+
**To:** `interior`
|
|
6
|
+
|
|
7
|
+
**Window:** global progress `0.51 to 0.60`
|
|
8
|
+
|
|
9
|
+
**Bridge:** the product's outer ring.
|
|
10
|
+
|
|
11
|
+
**Continuity:** screen-space center, apparent scale, ring rotation, and camera target must match at transfer.
|
|
12
|
+
|
|
13
|
+
**Sequence:**
|
|
14
|
+
|
|
15
|
+
1. At handoff progress `0.00`, disable ring hover and reduce pointer camera yaw from `0.045` to `0.008`.
|
|
16
|
+
2. From `0.00 to 0.42`, align the ring normal to the camera and dolly toward it.
|
|
17
|
+
3. At `0.28`, start rendering the interior scene behind the ring mask.
|
|
18
|
+
4. At `0.68`, the ring covers at least `95%` of the central viewport safe area.
|
|
19
|
+
5. At `0.72`, disable outgoing layers and change environment ownership under full cover.
|
|
20
|
+
6. At `0.78`, transfer camera target ownership to the interior tunnel anchor.
|
|
21
|
+
7. From `0.78 to 1.00`, restore pointer influence to the interior value `0.025` and reveal depth fog.
|
|
22
|
+
|
|
23
|
+
**Reverse:** retain outgoing product layers until global progress exceeds `0.66`; all visibility is derived from progress, not one-time callbacks.
|
|
24
|
+
|
|
25
|
+
**Completion:** camera is inside the tunnel, no ring edge is visible, interior lighting is authoritative, and incoming chapter local progress equals `0`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Concise Audit Finding Example
|
|
2
|
+
|
|
3
|
+
## Finding: Camera drift during chapter transitions
|
|
4
|
+
|
|
5
|
+
**Symptom:** The camera never lands on the same composition twice. Fast scroll produces a different final framing than slow scroll.
|
|
6
|
+
|
|
7
|
+
**Cause:** The ScrollTrigger timeline writes directly to `camera.position`, while `useFrame` lerps the same position toward a pointer-based target. A route transition also animates the camera directly.
|
|
8
|
+
|
|
9
|
+
**Perceptual consequence:** Key poses feel soft and unintentional. Handoffs expose small jumps because the outgoing final pose is unstable.
|
|
10
|
+
|
|
11
|
+
**Correction:** Introduce `cameraNarrativeRig`, `cameraReactionRig`, and `cameraRouteRig`. Assign the chapter timeline to the narrative rig, pointer damping to the reaction rig, and route transitions to a temporary parent route rig. The physical camera only resolves look-at and projection.
|
|
12
|
+
|
|
13
|
+
**Validation:** Scrub the chapter forward and backward ten times with pointer at different positions. The narrative rig must end at identical values, and the final subject screen position must remain within one pixel before reactive modulation is applied.
|