cinematic-web 0.1.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/LICENSE +21 -0
- package/assets/CLAUDE.md +150 -0
- package/assets/presets/3d-immersive/README.md +30 -0
- package/assets/presets/3d-immersive/preset.json +45 -0
- package/assets/presets/antigravity-lift/README.md +24 -0
- package/assets/presets/antigravity-lift/preset.json +38 -0
- package/assets/presets/brutalist-signal/README.md +24 -0
- package/assets/presets/brutalist-signal/preset.json +36 -0
- package/assets/presets/midnight-luxe/README.md +24 -0
- package/assets/presets/midnight-luxe/preset.json +36 -0
- package/assets/presets/organic-tech/README.md +24 -0
- package/assets/presets/organic-tech/preset.json +37 -0
- package/assets/presets/vapor-clinic/README.md +24 -0
- package/assets/presets/vapor-clinic/preset.json +36 -0
- package/assets/prompts/product-development/Guided-MVP-Concept.md +67 -0
- package/assets/prompts/product-development/Guided-MVP.md +65 -0
- package/assets/prompts/product-development/Guided-PRD-Creation.md +51 -0
- package/assets/prompts/product-development/Guided-Test-Plan.md +57 -0
- package/assets/prompts/product-development/Guided-UX-User-Flow.md +93 -0
- package/assets/prompts/product-development/README.md +21 -0
- package/assets/prompts/product-development/v0-design-prompt.md +107 -0
- package/assets/templates/base-react/index.html +18 -0
- package/assets/templates/base-react/package.json +26 -0
- package/assets/templates/base-react/postcss.config.js +6 -0
- package/assets/templates/base-react/src/App.jsx +33 -0
- package/assets/templates/base-react/src/index.css +90 -0
- package/assets/templates/base-react/src/main.jsx +10 -0
- package/assets/templates/base-react/src/sections/Features.jsx +238 -0
- package/assets/templates/base-react/src/sections/Footer.jsx +120 -0
- package/assets/templates/base-react/src/sections/Hero.jsx +96 -0
- package/assets/templates/base-react/src/sections/Navbar.jsx +119 -0
- package/assets/templates/base-react/src/sections/Philosophy.jsx +67 -0
- package/assets/templates/base-react/src/sections/Pricing.jsx +135 -0
- package/assets/templates/base-react/src/sections/Protocol.jsx +123 -0
- package/assets/templates/base-react/tailwind.config.js +26 -0
- package/assets/templates/base-react/vite.config.js +6 -0
- package/assets/templates/three-fiber/eslint.config.js +21 -0
- package/assets/templates/three-fiber/index.html +16 -0
- package/assets/templates/three-fiber/package.json +36 -0
- package/assets/templates/three-fiber/postcss.config.js +6 -0
- package/assets/templates/three-fiber/src/App.jsx +61 -0
- package/assets/templates/three-fiber/src/components/CameraRig.jsx +42 -0
- package/assets/templates/three-fiber/src/components/NetworkGraph.jsx +120 -0
- package/assets/templates/three-fiber/src/components/ParticleSystem.jsx +77 -0
- package/assets/templates/three-fiber/src/components/Scene.jsx +39 -0
- package/assets/templates/three-fiber/src/components/SocialProofBillboards.jsx +56 -0
- package/assets/templates/three-fiber/src/context/SceneContext.jsx +21 -0
- package/assets/templates/three-fiber/src/index.css +37 -0
- package/assets/templates/three-fiber/src/main.jsx +21 -0
- package/assets/templates/three-fiber/src/sections/CTA.jsx +41 -0
- package/assets/templates/three-fiber/src/sections/Hero.jsx +66 -0
- package/assets/templates/three-fiber/src/sections/HowItWorks.jsx +55 -0
- package/assets/templates/three-fiber/src/sections/Navbar.jsx +40 -0
- package/assets/templates/three-fiber/src/sections/SocialProof.jsx +50 -0
- package/assets/templates/three-fiber/src/sections/TheOldWay.jsx +28 -0
- package/assets/templates/three-fiber/src/sections/ValueProps.jsx +50 -0
- package/assets/templates/three-fiber/tailwind.config.js +21 -0
- package/assets/templates/three-fiber/vite.config.js +7 -0
- package/dist/cli.js +539 -0
- package/dist/cli.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { gsap } from 'gsap'
|
|
3
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
|
4
|
+
import { Check } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
const TIERS = [
|
|
7
|
+
{
|
|
8
|
+
name: 'Essential',
|
|
9
|
+
price: '$0',
|
|
10
|
+
period: 'forever',
|
|
11
|
+
description: 'Start your journey. No commitments.',
|
|
12
|
+
features: ['Core access', 'Basic analytics', 'Community support', '1 active project'],
|
|
13
|
+
cta: 'Get started',
|
|
14
|
+
highlighted: false,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Pro',
|
|
18
|
+
price: '$49',
|
|
19
|
+
period: 'per month',
|
|
20
|
+
description: 'For practitioners who demand precision.',
|
|
21
|
+
features: ['Everything in Essential', 'Advanced analytics', 'Priority support', 'Unlimited projects', 'API access'],
|
|
22
|
+
cta: '{{cta}}',
|
|
23
|
+
highlighted: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'Enterprise',
|
|
27
|
+
price: 'Custom',
|
|
28
|
+
period: 'contact us',
|
|
29
|
+
description: 'Tailored solutions for organizations.',
|
|
30
|
+
features: ['Everything in Pro', 'Dedicated support', 'SLA guarantee', 'Custom integrations', 'White-label option'],
|
|
31
|
+
cta: 'Talk to us',
|
|
32
|
+
highlighted: false,
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
export default function Pricing() {
|
|
37
|
+
const sectionRef = useRef(null)
|
|
38
|
+
const cardsRef = useRef([])
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const ctx = gsap.context(() => {
|
|
42
|
+
gsap.from(cardsRef.current, {
|
|
43
|
+
y: 50,
|
|
44
|
+
opacity: 0,
|
|
45
|
+
duration: 0.9,
|
|
46
|
+
ease: 'power3.out',
|
|
47
|
+
stagger: 0.15,
|
|
48
|
+
scrollTrigger: {
|
|
49
|
+
trigger: sectionRef.current,
|
|
50
|
+
start: 'top 75%',
|
|
51
|
+
toggleActions: 'play none none none',
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
}, sectionRef)
|
|
55
|
+
return () => ctx.revert()
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<section
|
|
60
|
+
ref={sectionRef}
|
|
61
|
+
id="pricing"
|
|
62
|
+
className="py-24 md:py-32 px-8 md:px-16 lg:px-24"
|
|
63
|
+
style={{ background: 'var(--color-background)' }}
|
|
64
|
+
>
|
|
65
|
+
<div className="max-w-6xl mx-auto">
|
|
66
|
+
<p className="font-data text-xs tracking-widest uppercase mb-4" style={{ color: 'var(--color-accent)' }}>
|
|
67
|
+
Membership
|
|
68
|
+
</p>
|
|
69
|
+
<h2 className="font-heading font-bold text-4xl md:text-5xl mb-16 tracking-tight" style={{ color: 'var(--color-dark)' }}>
|
|
70
|
+
Choose your tier.
|
|
71
|
+
</h2>
|
|
72
|
+
|
|
73
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 items-start">
|
|
74
|
+
{TIERS.map((tier, i) => (
|
|
75
|
+
<div
|
|
76
|
+
key={i}
|
|
77
|
+
ref={(el) => { cardsRef.current[i] = el }}
|
|
78
|
+
className={`rounded-3xl p-8 flex flex-col relative overflow-hidden ${
|
|
79
|
+
tier.highlighted ? 'md:-mt-4 md:-mb-4' : ''
|
|
80
|
+
}`}
|
|
81
|
+
style={{
|
|
82
|
+
background: tier.highlighted ? 'var(--color-primary)' : 'var(--color-dark)',
|
|
83
|
+
border: tier.highlighted ? '1px solid var(--color-accent)' : '1px solid rgba(255,255,255,0.06)',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{tier.highlighted && (
|
|
87
|
+
<div
|
|
88
|
+
className="absolute top-4 right-4 font-data text-xs tracking-widest uppercase px-3 py-1 rounded-full"
|
|
89
|
+
style={{ background: 'var(--color-accent)', color: 'white' }}
|
|
90
|
+
>
|
|
91
|
+
Popular
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
<p className="font-data text-xs tracking-widest uppercase mb-4" style={{ color: 'var(--color-accent)' }}>
|
|
96
|
+
{tier.name}
|
|
97
|
+
</p>
|
|
98
|
+
<div className="mb-2">
|
|
99
|
+
<span className="font-heading font-bold text-4xl text-white">{tier.price}</span>
|
|
100
|
+
<span className="font-data text-sm text-white/40 ml-2">/ {tier.period}</span>
|
|
101
|
+
</div>
|
|
102
|
+
<p className="font-heading text-sm text-white/50 mb-8 leading-relaxed">
|
|
103
|
+
{tier.description}
|
|
104
|
+
</p>
|
|
105
|
+
|
|
106
|
+
<ul className="space-y-3 mb-10 flex-1">
|
|
107
|
+
{tier.features.map((feat, fi) => (
|
|
108
|
+
<li key={fi} className="flex items-center gap-3">
|
|
109
|
+
<Check size={14} style={{ color: 'var(--color-accent)', flexShrink: 0 }} />
|
|
110
|
+
<span className="font-heading text-sm text-white/70">{feat}</span>
|
|
111
|
+
</li>
|
|
112
|
+
))}
|
|
113
|
+
</ul>
|
|
114
|
+
|
|
115
|
+
<a
|
|
116
|
+
href="#"
|
|
117
|
+
className="btn-magnetic relative flex items-center justify-center w-full py-3 rounded-2xl text-sm font-semibold font-heading overflow-hidden"
|
|
118
|
+
style={{
|
|
119
|
+
background: tier.highlighted ? 'var(--color-accent)' : 'rgba(255,255,255,0.06)',
|
|
120
|
+
color: 'white',
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<span
|
|
124
|
+
className="btn-fill"
|
|
125
|
+
style={{ background: tier.highlighted ? 'rgba(255,255,255,0.15)' : 'var(--color-accent)' }}
|
|
126
|
+
/>
|
|
127
|
+
<span className="relative z-10">{tier.cta}</span>
|
|
128
|
+
</a>
|
|
129
|
+
</div>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</section>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { gsap } from 'gsap'
|
|
3
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
|
4
|
+
|
|
5
|
+
const STEPS = [
|
|
6
|
+
{
|
|
7
|
+
number: '01',
|
|
8
|
+
title: '{{protocolStep0Title}}',
|
|
9
|
+
description: '{{protocolStep0Desc}}',
|
|
10
|
+
tag: 'Intake',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
number: '02',
|
|
14
|
+
title: '{{protocolStep1Title}}',
|
|
15
|
+
description: '{{protocolStep1Desc}}',
|
|
16
|
+
tag: 'Analysis',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
number: '03',
|
|
20
|
+
title: '{{protocolStep2Title}}',
|
|
21
|
+
description: '{{protocolStep2Desc}}',
|
|
22
|
+
tag: 'Delivery',
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export default function Protocol() {
|
|
27
|
+
const sectionRef = useRef(null)
|
|
28
|
+
const cardsRef = useRef([])
|
|
29
|
+
const stickyContainerRef = useRef(null)
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const ctx = gsap.context(() => {
|
|
33
|
+
const cards = cardsRef.current
|
|
34
|
+
|
|
35
|
+
cards.forEach((card, i) => {
|
|
36
|
+
if (i === 0) return // First card stays static on top
|
|
37
|
+
|
|
38
|
+
// Cards below: scale + blur + fade as scroll brings them down
|
|
39
|
+
gsap.to(cards.slice(0, i), {
|
|
40
|
+
scale: 1 - (i * 0.04),
|
|
41
|
+
filter: `blur(${i * 6}px)`,
|
|
42
|
+
opacity: 1 - (i * 0.25),
|
|
43
|
+
ease: 'none',
|
|
44
|
+
scrollTrigger: {
|
|
45
|
+
trigger: card,
|
|
46
|
+
start: 'top top',
|
|
47
|
+
end: 'bottom top',
|
|
48
|
+
scrub: true,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
}, sectionRef)
|
|
53
|
+
|
|
54
|
+
return () => ctx.revert()
|
|
55
|
+
}, [])
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<section
|
|
59
|
+
ref={sectionRef}
|
|
60
|
+
id="protocol"
|
|
61
|
+
className="py-24 md:py-32"
|
|
62
|
+
style={{ background: 'var(--color-background)' }}
|
|
63
|
+
>
|
|
64
|
+
<div className="px-8 md:px-16 lg:px-24 max-w-6xl mx-auto mb-16">
|
|
65
|
+
<p className="font-data text-xs tracking-widest uppercase mb-4" style={{ color: 'var(--color-accent)' }}>
|
|
66
|
+
The Protocol
|
|
67
|
+
</p>
|
|
68
|
+
<h2 className="font-heading font-bold text-4xl md:text-5xl tracking-tight" style={{ color: 'var(--color-dark)' }}>
|
|
69
|
+
How it works.
|
|
70
|
+
</h2>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Sticky stacking archive */}
|
|
74
|
+
<div ref={stickyContainerRef} className="relative">
|
|
75
|
+
{STEPS.map((step, i) => (
|
|
76
|
+
<div
|
|
77
|
+
key={i}
|
|
78
|
+
ref={(el) => { cardsRef.current[i] = el }}
|
|
79
|
+
className="sticky top-0 min-h-screen flex items-center justify-center px-8 md:px-16 lg:px-24"
|
|
80
|
+
style={{ paddingTop: `${i * 2}rem` }}
|
|
81
|
+
>
|
|
82
|
+
<div
|
|
83
|
+
className="w-full max-w-4xl rounded-3xl md:rounded-[3rem] p-10 md:p-16 relative overflow-hidden"
|
|
84
|
+
style={{
|
|
85
|
+
background: i % 2 === 0 ? 'var(--color-dark)' : 'var(--color-primary)',
|
|
86
|
+
minHeight: '60vh',
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{/* Step number */}
|
|
90
|
+
<span
|
|
91
|
+
className="absolute top-8 right-10 font-data text-6xl md:text-8xl font-bold select-none"
|
|
92
|
+
style={{ color: 'rgba(255,255,255,0.06)' }}
|
|
93
|
+
>
|
|
94
|
+
{step.number}
|
|
95
|
+
</span>
|
|
96
|
+
|
|
97
|
+
{/* Tag */}
|
|
98
|
+
<span
|
|
99
|
+
className="inline-block font-data text-xs tracking-widest uppercase px-3 py-1 rounded-full mb-8"
|
|
100
|
+
style={{
|
|
101
|
+
color: 'var(--color-accent)',
|
|
102
|
+
border: '1px solid var(--color-accent)',
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{step.tag}
|
|
106
|
+
</span>
|
|
107
|
+
|
|
108
|
+
{/* Content */}
|
|
109
|
+
<div className="max-w-xl">
|
|
110
|
+
<h3 className="font-heading font-bold text-3xl md:text-4xl text-white mb-6 tracking-tight">
|
|
111
|
+
{step.title}
|
|
112
|
+
</h3>
|
|
113
|
+
<p className="font-heading text-white/50 text-lg leading-relaxed">
|
|
114
|
+
{step.description}
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</section>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
|
4
|
+
theme: {
|
|
5
|
+
extend: {
|
|
6
|
+
colors: {
|
|
7
|
+
primary: 'var(--color-primary)',
|
|
8
|
+
accent: 'var(--color-accent)',
|
|
9
|
+
background: 'var(--color-background)',
|
|
10
|
+
dark: 'var(--color-dark)',
|
|
11
|
+
},
|
|
12
|
+
fontFamily: {
|
|
13
|
+
heading: 'var(--font-heading)',
|
|
14
|
+
drama: 'var(--font-drama)',
|
|
15
|
+
data: 'var(--font-data)',
|
|
16
|
+
},
|
|
17
|
+
borderRadius: {
|
|
18
|
+
'2xl': '1rem',
|
|
19
|
+
'3xl': '1.5rem',
|
|
20
|
+
'4xl': '2rem',
|
|
21
|
+
'5xl': '2.5rem',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
plugins: [],
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
6
|
+
|
|
7
|
+
export default defineConfig([
|
|
8
|
+
globalIgnores(['dist']),
|
|
9
|
+
{
|
|
10
|
+
files: ['**/*.{js,jsx}'],
|
|
11
|
+
extends: [
|
|
12
|
+
js.configs.recommended,
|
|
13
|
+
reactHooks.configs.flat.recommended,
|
|
14
|
+
reactRefresh.configs.vite,
|
|
15
|
+
],
|
|
16
|
+
languageOptions: {
|
|
17
|
+
globals: globals.browser,
|
|
18
|
+
parserOptions: { ecmaFeatures: { jsx: true } },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
])
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{brand}}</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="{{googleFonts}}" rel="stylesheet" />
|
|
10
|
+
<style>html,body{margin:0;padding:0;background:{{palette.background}};overflow-x:hidden;}</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{brand-slug}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@react-three/drei": "^10.7.7",
|
|
14
|
+
"@react-three/fiber": "^9.6.1",
|
|
15
|
+
"@react-three/postprocessing": "^3.0.4",
|
|
16
|
+
"@studio-freight/lenis": "^1.0.42",
|
|
17
|
+
"gsap": "^3.15.0",
|
|
18
|
+
"react": "^19.2.6",
|
|
19
|
+
"react-dom": "^19.2.6",
|
|
20
|
+
"three": "^0.184.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@eslint/js": "^10.0.1",
|
|
24
|
+
"@types/react": "^19.2.14",
|
|
25
|
+
"@types/react-dom": "^19.2.3",
|
|
26
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
27
|
+
"autoprefixer": "^10.5.0",
|
|
28
|
+
"eslint": "^10.3.0",
|
|
29
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
30
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
31
|
+
"globals": "^17.6.0",
|
|
32
|
+
"postcss": "^8.5.15",
|
|
33
|
+
"tailwindcss": "^3.4.17",
|
|
34
|
+
"vite": "^8.0.12"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { gsap } from 'gsap'
|
|
3
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
|
4
|
+
import { SceneProvider, useScene } from './context/SceneContext'
|
|
5
|
+
import Scene from './components/Scene'
|
|
6
|
+
import Navbar from './sections/Navbar'
|
|
7
|
+
import Hero from './sections/Hero'
|
|
8
|
+
import TheOldWay from './sections/TheOldWay'
|
|
9
|
+
import ValueProps from './sections/ValueProps'
|
|
10
|
+
import HowItWorks from './sections/HowItWorks'
|
|
11
|
+
import SocialProof from './sections/SocialProof'
|
|
12
|
+
import CTA from './sections/CTA'
|
|
13
|
+
|
|
14
|
+
// Scene section top offsets (6 scenes × 100vh = 600vh total)
|
|
15
|
+
const SCENE_TOPS = ['0vh', '100vh', '200vh', '300vh', '400vh', '500vh']
|
|
16
|
+
|
|
17
|
+
function AppInner() {
|
|
18
|
+
const containerRef = useRef(null)
|
|
19
|
+
const { setProgress } = useScene()
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const trigger = ScrollTrigger.create({
|
|
23
|
+
trigger: containerRef.current,
|
|
24
|
+
start: 'top top',
|
|
25
|
+
end: 'bottom bottom',
|
|
26
|
+
scrub: true,
|
|
27
|
+
onUpdate: (self) => setProgress(self.progress),
|
|
28
|
+
})
|
|
29
|
+
return () => trigger.kill()
|
|
30
|
+
}, [setProgress])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
{/* Fixed 3D canvas — background layer */}
|
|
35
|
+
<div className="fixed inset-0 z-0">
|
|
36
|
+
<Scene />
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{/* Navbar — above canvas */}
|
|
40
|
+
<Navbar />
|
|
41
|
+
|
|
42
|
+
{/* 600vh scroll container — sections positioned absolutely inside */}
|
|
43
|
+
<div ref={containerRef} className="relative" style={{ height: '600vh' }}>
|
|
44
|
+
<Hero style={{ top: SCENE_TOPS[0] }} />
|
|
45
|
+
<TheOldWay style={{ top: SCENE_TOPS[1] }} />
|
|
46
|
+
<ValueProps style={{ top: SCENE_TOPS[2] }} />
|
|
47
|
+
<HowItWorks style={{ top: SCENE_TOPS[3] }} />
|
|
48
|
+
<SocialProof style={{ top: SCENE_TOPS[4] }} />
|
|
49
|
+
<CTA style={{ top: SCENE_TOPS[5] }} />
|
|
50
|
+
</div>
|
|
51
|
+
</>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function App() {
|
|
56
|
+
return (
|
|
57
|
+
<SceneProvider>
|
|
58
|
+
<AppInner />
|
|
59
|
+
</SceneProvider>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import { useFrame, useThree } from '@react-three/fiber'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import { useScene } from '../context/SceneContext'
|
|
5
|
+
|
|
6
|
+
// 7 control points: one per scene transition + start/end
|
|
7
|
+
const CAMERA_PATH = new THREE.CatmullRomCurve3([
|
|
8
|
+
new THREE.Vector3(0, 0, 20), // Scene 1 start: far back
|
|
9
|
+
new THREE.Vector3(0, 2, 12), // Scene 1 end: network revealed
|
|
10
|
+
new THREE.Vector3(-3, 0, 10), // Scene 2: wide shot
|
|
11
|
+
new THREE.Vector3(0, -1, 7), // Scene 3: tight center
|
|
12
|
+
new THREE.Vector3(2, 1, 5), // Scene 4: inside network
|
|
13
|
+
new THREE.Vector3(-2, 3, 9), // Scene 5: nebula angle
|
|
14
|
+
new THREE.Vector3(0, 0, 18), // Scene 6: full pull-back
|
|
15
|
+
], false, 'catmullrom', 0.5)
|
|
16
|
+
|
|
17
|
+
const LOOK_TARGET = new THREE.Vector3()
|
|
18
|
+
const LOOK_OFFSET = 0.02
|
|
19
|
+
|
|
20
|
+
export default function CameraRig() {
|
|
21
|
+
const { camera } = useThree()
|
|
22
|
+
const { progress } = useScene()
|
|
23
|
+
const smoothProgress = useRef(0)
|
|
24
|
+
|
|
25
|
+
useFrame((_, delta) => {
|
|
26
|
+
// Lerp scroll progress for cinematic smoothness
|
|
27
|
+
smoothProgress.current = THREE.MathUtils.lerp(
|
|
28
|
+
smoothProgress.current,
|
|
29
|
+
progress,
|
|
30
|
+
Math.min(1, delta * 3)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const p = smoothProgress.current
|
|
34
|
+
const ahead = Math.min(1, p + LOOK_OFFSET)
|
|
35
|
+
|
|
36
|
+
CAMERA_PATH.getPoint(p, camera.position)
|
|
37
|
+
CAMERA_PATH.getPoint(ahead, LOOK_TARGET)
|
|
38
|
+
camera.lookAt(LOOK_TARGET)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useRef, useMemo, useEffect } from 'react'
|
|
2
|
+
import { useFrame } from '@react-three/fiber'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import { useScene } from '../context/SceneContext'
|
|
5
|
+
|
|
6
|
+
// NODE_COLORS use palette tokens — replaced at scaffold time
|
|
7
|
+
const NODE_COLORS = {
|
|
8
|
+
idle: new THREE.Color('{{palette.accent}}'),
|
|
9
|
+
dark: new THREE.Color(0x2A2A3A),
|
|
10
|
+
success: new THREE.Color(0x10B981),
|
|
11
|
+
glow: new THREE.Color('{{palette.accent}}'),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Fibonacci sphere — evenly distributes N points on sphere surface
|
|
15
|
+
function fibonacciSphere(count, radius) {
|
|
16
|
+
const positions = []
|
|
17
|
+
const phi = Math.PI * (Math.sqrt(5) - 1)
|
|
18
|
+
for (let i = 0; i < count; i++) {
|
|
19
|
+
const y = 1 - (i / (count - 1)) * 2
|
|
20
|
+
const r = Math.sqrt(1 - y * y)
|
|
21
|
+
const theta = phi * i
|
|
22
|
+
positions.push(new THREE.Vector3(
|
|
23
|
+
Math.cos(theta) * r * radius,
|
|
24
|
+
y * radius,
|
|
25
|
+
Math.sin(theta) * r * radius
|
|
26
|
+
))
|
|
27
|
+
}
|
|
28
|
+
return positions
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Build connection pairs (nodes within 2.5 units, capped at maxPairs)
|
|
32
|
+
function buildConnections(positions, maxPairs = 2000) {
|
|
33
|
+
const pairs = []
|
|
34
|
+
const subset = positions.slice(0, 200)
|
|
35
|
+
for (let i = 0; i < subset.length && pairs.length < maxPairs; i++) {
|
|
36
|
+
for (let j = i + 1; j < subset.length && pairs.length < maxPairs; j++) {
|
|
37
|
+
if (positions[i].distanceTo(positions[j]) < 2.5) {
|
|
38
|
+
pairs.push(i, j)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return pairs
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function NetworkGraph({ lowEnd = false }) {
|
|
46
|
+
const COUNT = lowEnd ? 200 : 1000
|
|
47
|
+
const meshRef = useRef()
|
|
48
|
+
const linesRef = useRef()
|
|
49
|
+
const { sceneIndex } = useScene()
|
|
50
|
+
const colorRef = useRef(new THREE.Color())
|
|
51
|
+
|
|
52
|
+
const { positions, connectionPairs } = useMemo(() => {
|
|
53
|
+
const pos = fibonacciSphere(COUNT, 8)
|
|
54
|
+
const pairs = buildConnections(pos)
|
|
55
|
+
return { positions: pos, connectionPairs: pairs }
|
|
56
|
+
}, [COUNT])
|
|
57
|
+
|
|
58
|
+
// Set initial instance matrices
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!meshRef.current) return
|
|
61
|
+
const dummy = new THREE.Object3D()
|
|
62
|
+
positions.forEach((pos, i) => {
|
|
63
|
+
dummy.position.copy(pos)
|
|
64
|
+
const isHub = i % 50 === 0
|
|
65
|
+
dummy.scale.setScalar(isHub ? 0.12 : 0.06)
|
|
66
|
+
dummy.updateMatrix()
|
|
67
|
+
meshRef.current.setMatrixAt(i, dummy.matrix)
|
|
68
|
+
})
|
|
69
|
+
meshRef.current.instanceMatrix.needsUpdate = true
|
|
70
|
+
}, [positions])
|
|
71
|
+
|
|
72
|
+
// Connection line geometry
|
|
73
|
+
const lineGeometry = useMemo(() => {
|
|
74
|
+
const geo = new THREE.BufferGeometry()
|
|
75
|
+
const verts = []
|
|
76
|
+
for (let k = 0; k < connectionPairs.length; k += 2) {
|
|
77
|
+
const a = positions[connectionPairs[k]]
|
|
78
|
+
const b = positions[connectionPairs[k + 1]]
|
|
79
|
+
if (a && b) verts.push(a.x, a.y, a.z, b.x, b.y, b.z)
|
|
80
|
+
}
|
|
81
|
+
geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3))
|
|
82
|
+
return geo
|
|
83
|
+
}, [positions, connectionPairs])
|
|
84
|
+
|
|
85
|
+
// Scene-driven color transitions each frame
|
|
86
|
+
useFrame(() => {
|
|
87
|
+
if (!meshRef.current) return
|
|
88
|
+
const c = colorRef.current
|
|
89
|
+
for (let i = 0; i < COUNT; i++) {
|
|
90
|
+
if (sceneIndex === 1) {
|
|
91
|
+
c.copy(NODE_COLORS.dark)
|
|
92
|
+
} else if (sceneIndex >= 2) {
|
|
93
|
+
const activated = i < (sceneIndex - 1) * (COUNT / 4)
|
|
94
|
+
c.copy(activated ? NODE_COLORS.success : NODE_COLORS.idle)
|
|
95
|
+
} else {
|
|
96
|
+
c.copy(NODE_COLORS.idle)
|
|
97
|
+
}
|
|
98
|
+
meshRef.current.setColorAt(i, c)
|
|
99
|
+
}
|
|
100
|
+
if (meshRef.current.instanceColor) {
|
|
101
|
+
meshRef.current.instanceColor.needsUpdate = true
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const lineOpacity = sceneIndex === 1 ? 0.05 : sceneIndex >= 2 ? 0.4 : 0.2
|
|
106
|
+
const lineColor = sceneIndex >= 2 ? '#22D3EE' : '{{palette.accent}}'
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<group>
|
|
110
|
+
<instancedMesh ref={meshRef} args={[null, null, COUNT]} frustumCulled={false}>
|
|
111
|
+
<sphereGeometry args={[1, 8, 8]} />
|
|
112
|
+
<meshBasicMaterial vertexColors />
|
|
113
|
+
</instancedMesh>
|
|
114
|
+
|
|
115
|
+
<lineSegments ref={linesRef} geometry={lineGeometry}>
|
|
116
|
+
<lineBasicMaterial color={lineColor} transparent opacity={lineOpacity} />
|
|
117
|
+
</lineSegments>
|
|
118
|
+
</group>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useRef, useMemo } from 'react'
|
|
2
|
+
import { useFrame } from '@react-three/fiber'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import { useScene } from '../context/SceneContext'
|
|
5
|
+
|
|
6
|
+
const PARTICLE_COUNT = 300
|
|
7
|
+
|
|
8
|
+
export default function ParticleSystem() {
|
|
9
|
+
const ref = useRef()
|
|
10
|
+
const { sceneIndex } = useScene()
|
|
11
|
+
|
|
12
|
+
const { positions, velocities } = useMemo(() => {
|
|
13
|
+
const pos = new Float32Array(PARTICLE_COUNT * 3)
|
|
14
|
+
const vel = []
|
|
15
|
+
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
|
16
|
+
const theta = Math.random() * Math.PI * 2
|
|
17
|
+
const phi = Math.acos(2 * Math.random() - 1)
|
|
18
|
+
const r = 6 + Math.random() * 4
|
|
19
|
+
pos[i * 3] = r * Math.sin(phi) * Math.cos(theta)
|
|
20
|
+
pos[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta)
|
|
21
|
+
pos[i * 3 + 2] = r * Math.cos(phi)
|
|
22
|
+
vel.push(new THREE.Vector3(
|
|
23
|
+
(Math.random() - 0.5) * 0.02,
|
|
24
|
+
-0.01 - Math.random() * 0.02, // drift downward (failure mode)
|
|
25
|
+
(Math.random() - 0.5) * 0.02
|
|
26
|
+
))
|
|
27
|
+
}
|
|
28
|
+
return { positions: pos, velocities: vel }
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
31
|
+
const geometry = useMemo(() => {
|
|
32
|
+
const geo = new THREE.BufferGeometry()
|
|
33
|
+
geo.setAttribute('position', new THREE.BufferAttribute(positions.slice(), 3))
|
|
34
|
+
return geo
|
|
35
|
+
}, [positions])
|
|
36
|
+
|
|
37
|
+
useFrame(() => {
|
|
38
|
+
if (!ref.current) return
|
|
39
|
+
const pos = ref.current.geometry.attributes.position
|
|
40
|
+
const isExplosion = sceneIndex === 5
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
|
43
|
+
const v = velocities[i]
|
|
44
|
+
if (isExplosion) {
|
|
45
|
+
pos.setXYZ(i,
|
|
46
|
+
pos.getX(i) + v.x * 4,
|
|
47
|
+
pos.getY(i) + v.y * 2,
|
|
48
|
+
pos.getZ(i) + v.z * 4,
|
|
49
|
+
)
|
|
50
|
+
if (Math.abs(pos.getX(i)) > 15) {
|
|
51
|
+
pos.setXYZ(i,
|
|
52
|
+
(Math.random() - 0.5) * 2,
|
|
53
|
+
(Math.random() - 0.5) * 2,
|
|
54
|
+
(Math.random() - 0.5) * 2,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
pos.setXYZ(i,
|
|
59
|
+
pos.getX(i) + v.x,
|
|
60
|
+
pos.getY(i) + v.y,
|
|
61
|
+
pos.getZ(i) + v.z,
|
|
62
|
+
)
|
|
63
|
+
if (pos.getY(i) < -9) pos.setY(i, 9)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
pos.needsUpdate = true
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const visible = sceneIndex === 1 || sceneIndex === 5
|
|
70
|
+
const color = sceneIndex === 1 ? '#EF4444' : '#A78BFA'
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<points ref={ref} geometry={geometry} visible={visible}>
|
|
74
|
+
<pointsMaterial color={color} size={0.08} transparent opacity={0.8} sizeAttenuation />
|
|
75
|
+
</points>
|
|
76
|
+
)
|
|
77
|
+
}
|