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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/assets/CLAUDE.md +150 -0
  3. package/assets/presets/3d-immersive/README.md +30 -0
  4. package/assets/presets/3d-immersive/preset.json +45 -0
  5. package/assets/presets/antigravity-lift/README.md +24 -0
  6. package/assets/presets/antigravity-lift/preset.json +38 -0
  7. package/assets/presets/brutalist-signal/README.md +24 -0
  8. package/assets/presets/brutalist-signal/preset.json +36 -0
  9. package/assets/presets/midnight-luxe/README.md +24 -0
  10. package/assets/presets/midnight-luxe/preset.json +36 -0
  11. package/assets/presets/organic-tech/README.md +24 -0
  12. package/assets/presets/organic-tech/preset.json +37 -0
  13. package/assets/presets/vapor-clinic/README.md +24 -0
  14. package/assets/presets/vapor-clinic/preset.json +36 -0
  15. package/assets/prompts/product-development/Guided-MVP-Concept.md +67 -0
  16. package/assets/prompts/product-development/Guided-MVP.md +65 -0
  17. package/assets/prompts/product-development/Guided-PRD-Creation.md +51 -0
  18. package/assets/prompts/product-development/Guided-Test-Plan.md +57 -0
  19. package/assets/prompts/product-development/Guided-UX-User-Flow.md +93 -0
  20. package/assets/prompts/product-development/README.md +21 -0
  21. package/assets/prompts/product-development/v0-design-prompt.md +107 -0
  22. package/assets/templates/base-react/index.html +18 -0
  23. package/assets/templates/base-react/package.json +26 -0
  24. package/assets/templates/base-react/postcss.config.js +6 -0
  25. package/assets/templates/base-react/src/App.jsx +33 -0
  26. package/assets/templates/base-react/src/index.css +90 -0
  27. package/assets/templates/base-react/src/main.jsx +10 -0
  28. package/assets/templates/base-react/src/sections/Features.jsx +238 -0
  29. package/assets/templates/base-react/src/sections/Footer.jsx +120 -0
  30. package/assets/templates/base-react/src/sections/Hero.jsx +96 -0
  31. package/assets/templates/base-react/src/sections/Navbar.jsx +119 -0
  32. package/assets/templates/base-react/src/sections/Philosophy.jsx +67 -0
  33. package/assets/templates/base-react/src/sections/Pricing.jsx +135 -0
  34. package/assets/templates/base-react/src/sections/Protocol.jsx +123 -0
  35. package/assets/templates/base-react/tailwind.config.js +26 -0
  36. package/assets/templates/base-react/vite.config.js +6 -0
  37. package/assets/templates/three-fiber/eslint.config.js +21 -0
  38. package/assets/templates/three-fiber/index.html +16 -0
  39. package/assets/templates/three-fiber/package.json +36 -0
  40. package/assets/templates/three-fiber/postcss.config.js +6 -0
  41. package/assets/templates/three-fiber/src/App.jsx +61 -0
  42. package/assets/templates/three-fiber/src/components/CameraRig.jsx +42 -0
  43. package/assets/templates/three-fiber/src/components/NetworkGraph.jsx +120 -0
  44. package/assets/templates/three-fiber/src/components/ParticleSystem.jsx +77 -0
  45. package/assets/templates/three-fiber/src/components/Scene.jsx +39 -0
  46. package/assets/templates/three-fiber/src/components/SocialProofBillboards.jsx +56 -0
  47. package/assets/templates/three-fiber/src/context/SceneContext.jsx +21 -0
  48. package/assets/templates/three-fiber/src/index.css +37 -0
  49. package/assets/templates/three-fiber/src/main.jsx +21 -0
  50. package/assets/templates/three-fiber/src/sections/CTA.jsx +41 -0
  51. package/assets/templates/three-fiber/src/sections/Hero.jsx +66 -0
  52. package/assets/templates/three-fiber/src/sections/HowItWorks.jsx +55 -0
  53. package/assets/templates/three-fiber/src/sections/Navbar.jsx +40 -0
  54. package/assets/templates/three-fiber/src/sections/SocialProof.jsx +50 -0
  55. package/assets/templates/three-fiber/src/sections/TheOldWay.jsx +28 -0
  56. package/assets/templates/three-fiber/src/sections/ValueProps.jsx +50 -0
  57. package/assets/templates/three-fiber/tailwind.config.js +21 -0
  58. package/assets/templates/three-fiber/vite.config.js +7 -0
  59. package/dist/cli.js +539 -0
  60. package/dist/cli.js.map +1 -0
  61. 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,6 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ })
@@ -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,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -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
+ }