@naeil/ui 0.1.0 → 0.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/CHANGELOG.md +18 -0
- package/DESIGN.md +186 -0
- package/LICENSE +21 -0
- package/README.md +85 -22
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/dist/chunk-JBTDRVZ7.js +46 -0
- package/dist/chunk-QKVN6Q6C.js +438 -0
- package/dist/components/index.d.ts +87 -0
- package/dist/components/index.js +832 -0
- package/dist/components/ui/index.d.ts +105 -0
- package/dist/components/ui/index.js +736 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +7 -0
- package/dist/source/components/footer.d.ts +15 -0
- package/dist/source/components/footer.js +172 -0
- package/dist/source/components/index.d.ts +11 -0
- package/dist/source/components/index.js +26 -0
- package/dist/source/components/locale-switcher.d.ts +17 -0
- package/dist/source/components/locale-switcher.js +66 -0
- package/dist/source/components/logo.d.ts +9 -0
- package/dist/source/components/logo.js +52 -0
- package/dist/source/components/nav.d.ts +39 -0
- package/dist/source/components/nav.js +412 -0
- package/dist/source/components/theme-provider.d.ts +6 -0
- package/dist/source/components/theme-provider.js +45 -0
- package/dist/source/components/theme-toggle-icon.d.ts +5 -0
- package/dist/source/components/theme-toggle-icon.js +62 -0
- package/dist/source/components/theme-toggle.d.ts +5 -0
- package/dist/source/components/theme-toggle.js +34 -0
- package/dist/source/components/typography.d.ts +12 -0
- package/dist/source/components/typography.js +49 -0
- package/dist/source/components/ui/avatar.d.ts +14 -0
- package/dist/source/components/ui/avatar.js +137 -0
- package/dist/source/components/ui/badge.d.ts +13 -0
- package/dist/source/components/ui/badge.js +76 -0
- package/dist/source/components/ui/button.d.ts +14 -0
- package/dist/source/components/ui/button.js +88 -0
- package/dist/source/components/ui/card.d.ts +13 -0
- package/dist/source/components/ui/card.js +135 -0
- package/dist/source/components/ui/checkbox.d.ts +7 -0
- package/dist/source/components/ui/checkbox.js +58 -0
- package/dist/source/components/ui/dialog.d.ts +20 -0
- package/dist/source/components/ui/dialog.js +182 -0
- package/dist/source/components/ui/dropdown-menu.d.ts +28 -0
- package/dist/source/components/ui/dropdown-menu.js +282 -0
- package/dist/source/components/ui/index.d.ts +19 -0
- package/dist/source/components/ui/index.js +131 -0
- package/dist/source/components/ui/input.d.ts +6 -0
- package/dist/source/components/ui/input.js +49 -0
- package/dist/source/components/ui/select.d.ts +16 -0
- package/dist/source/components/ui/select.js +197 -0
- package/dist/source/components/ui/sonner.d.ts +6 -0
- package/dist/source/components/ui/sonner.js +80 -0
- package/dist/source/components/ui/switch.d.ts +7 -0
- package/dist/source/components/ui/switch.js +54 -0
- package/dist/source/components/ui/tabs.d.ts +10 -0
- package/dist/source/components/ui/tabs.js +57 -0
- package/dist/source/components/ui/textarea.d.ts +6 -0
- package/dist/source/components/ui/textarea.js +46 -0
- package/dist/source/i18n/config.d.ts +6 -0
- package/dist/source/i18n/config.js +12 -0
- package/dist/source/i18n/routing.d.ts +371 -0
- package/dist/source/i18n/routing.js +17 -0
- package/dist/source/lib/design/modal-inert.d.ts +6 -0
- package/dist/source/lib/design/modal-inert.js +73 -0
- package/dist/source/lib/utils.d.ts +5 -0
- package/dist/source/lib/utils.js +8 -0
- package/dist/tokens.ts +217 -0
- package/docs/components/README.md +21 -0
- package/docs/components/avatar.md +47 -0
- package/docs/components/badge.md +29 -0
- package/docs/components/button.md +39 -0
- package/docs/components/card.md +32 -0
- package/docs/components/checkbox.md +35 -0
- package/docs/components/common.md +31 -0
- package/docs/components/composition.md +68 -0
- package/docs/components/dialog.md +41 -0
- package/docs/components/dropdown-menu.md +38 -0
- package/docs/components/input.md +32 -0
- package/docs/components/select.md +39 -0
- package/docs/components/sonner.md +37 -0
- package/docs/components/switch.md +33 -0
- package/docs/components/tabs.md +40 -0
- package/docs/components/textarea.md +35 -0
- package/docs/design/component-demand.md +30 -0
- package/docs/design/public-ui-final-verification.md +96 -0
- package/docs/design/public-ui-release-validation.md +86 -0
- package/docs/design/public-ui-stage3-verification.md +98 -0
- package/docs/design/public-ui-stage4-verification.md +129 -0
- package/docs/design/public-ui-support.md +40 -0
- package/docs/design/v2-migration.md +199 -0
- package/docs/package-release.md +89 -0
- package/licenses/Noto-Sans-JP-OFL.txt +93 -0
- package/licenses/Nunito-Sans-OFL.txt +93 -0
- package/licenses/Pretendard-OFL.txt +94 -0
- package/licenses/React-MIT.txt +21 -0
- package/licenses/Storybook-MIT.txt +21 -0
- package/licenses/popper-MIT.txt +25 -0
- package/licenses/react-remove-scroll-bar-MIT.txt +21 -0
- package/licenses/store2-MIT.txt +22 -0
- package/licenses/toggle-selection-MIT.txt +21 -0
- package/package.json +215 -26
- package/src/styles/components.css +276 -0
- package/src/styles/globals.css +18 -0
- package/src/styles/theme.css +293 -0
- package/messages/en.json +0 -221
- package/messages/ja.json +0 -221
- package/messages/ko.json +0 -221
- package/postcss.config.mjs +0 -7
- package/src/app/[locale]/(content)/baby-agent/page.tsx +0 -30
- package/src/app/[locale]/(content)/blog/page.tsx +0 -68
- package/src/app/[locale]/(content)/cc/page.tsx +0 -216
- package/src/app/[locale]/(content)/design/page.tsx +0 -703
- package/src/app/[locale]/(content)/layout.tsx +0 -7
- package/src/app/[locale]/(content)/pkm/page.tsx +0 -31
- package/src/app/[locale]/(content)/projects/page.tsx +0 -100
- package/src/app/[locale]/(content)/sa/page.tsx +0 -179
- package/src/app/[locale]/(landing)/page.tsx +0 -157
- package/src/app/[locale]/layout.tsx +0 -72
- package/src/app/[locale]/login/actions.ts +0 -10
- package/src/app/[locale]/login/oauth-buttons.tsx +0 -78
- package/src/app/[locale]/login/page.tsx +0 -85
- package/src/app/auth/callback/route.ts +0 -78
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +0 -32
- package/src/app/layout.tsx +0 -22
- package/src/app/sitemap.ts +0 -26
- package/src/components/accent-picker.tsx +0 -62
- package/src/components/auth-slot.tsx +0 -96
- package/src/components/footer-wrapper.tsx +0 -8
- package/src/components/footer.tsx +0 -193
- package/src/components/hero-scene.tsx +0 -948
- package/src/components/hero-section.tsx +0 -57
- package/src/components/locale-switcher.tsx +0 -74
- package/src/components/logo.tsx +0 -51
- package/src/components/nav-server-wrapper.tsx +0 -22
- package/src/components/nav-wrapper.tsx +0 -45
- package/src/components/nav.tsx +0 -550
- package/src/components/paraglider-cursor.tsx +0 -112
- package/src/components/project-layout.tsx +0 -194
- package/src/components/theme-provider.tsx +0 -10
- package/src/components/theme-toggle-icon.tsx +0 -62
- package/src/components/theme-toggle.tsx +0 -39
- package/src/components/typography.tsx +0 -27
- package/src/components/ui/avatar.tsx +0 -109
- package/src/components/ui/badge.tsx +0 -47
- package/src/components/ui/button.tsx +0 -65
- package/src/components/ui/card.tsx +0 -106
- package/src/components/ui/dialog.tsx +0 -158
- package/src/components/ui/dropdown-menu.tsx +0 -257
- package/src/components/ui/input.tsx +0 -21
- package/src/components/ui/sonner.tsx +0 -54
- package/src/components/workflow-diagram.tsx +0 -414
- package/src/i18n/config.ts +0 -8
- package/src/i18n/request.ts +0 -16
- package/src/i18n/routing.ts +0 -12
- package/src/lib/supabase/client.ts +0 -18
- package/src/lib/supabase/middleware.ts +0 -42
- package/src/lib/supabase/server.ts +0 -37
- package/src/lib/utils.ts +0 -6
- package/src/proxy.ts +0 -62
- package/src/tokens/.gitkeep +0 -0
- package/src/tokens/color.json +0 -56
- package/src/tokens/radius.json +0 -9
- package/src/tokens/semantic.json +0 -81
- package/src/tokens/shadow.json +0 -7
- package/src/tokens/spacing.json +0 -16
- package/src/tokens/typography.json +0 -34
- package/src/types/i18n.d.ts +0 -6
- package/src/utils/.gitkeep +0 -0
|
@@ -1,948 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useRef, useMemo, useCallback, useEffect, useSyncExternalStore } from "react";
|
|
4
|
-
import { useTheme } from "next-themes";
|
|
5
|
-
import { Canvas, useFrame, useLoader, useThree } from "@react-three/fiber";
|
|
6
|
-
import type { Mesh } from "three";
|
|
7
|
-
import * as THREE from "three";
|
|
8
|
-
|
|
9
|
-
// ─── Gradient Glow Sun (夕焼け) ───
|
|
10
|
-
|
|
11
|
-
const GRADIENT_VERTEX = /* glsl */ `
|
|
12
|
-
varying vec2 vUv;
|
|
13
|
-
varying float vWorldY;
|
|
14
|
-
void main() {
|
|
15
|
-
vUv = uv;
|
|
16
|
-
vec4 worldPos = modelMatrix * vec4(position, 1.0);
|
|
17
|
-
vWorldY = worldPos.y;
|
|
18
|
-
gl_Position = projectionMatrix * viewMatrix * worldPos;
|
|
19
|
-
}
|
|
20
|
-
`;
|
|
21
|
-
|
|
22
|
-
const GRADIENT_FRAGMENT = /* glsl */ `
|
|
23
|
-
uniform vec3 c1, c2, c3, c4, c5;
|
|
24
|
-
uniform float uTime;
|
|
25
|
-
uniform float uPeak;
|
|
26
|
-
uniform float uClipY;
|
|
27
|
-
varying vec2 vUv;
|
|
28
|
-
varying float vWorldY;
|
|
29
|
-
void main() {
|
|
30
|
-
float dist = length(vUv - 0.5) * 2.0;
|
|
31
|
-
float pulse = 1.0 + sin(uTime * 0.4) * 0.06;
|
|
32
|
-
float d = dist * pulse;
|
|
33
|
-
vec3 color = mix(c1, c2, smoothstep(0.0, 0.25, d));
|
|
34
|
-
color = mix(color, c3, smoothstep(0.15, 0.45, d));
|
|
35
|
-
color = mix(color, c4, smoothstep(0.35, 0.70, d));
|
|
36
|
-
color = mix(color, c5, smoothstep(0.55, 0.95, d));
|
|
37
|
-
float alpha = pow(max(1.0 - d * d, 0.0), 2.0) * uPeak;
|
|
38
|
-
alpha *= smoothstep(uClipY - 0.1, uClipY + 0.05, vWorldY);
|
|
39
|
-
gl_FragColor = vec4(color, alpha);
|
|
40
|
-
}
|
|
41
|
-
`;
|
|
42
|
-
|
|
43
|
-
// 夕焼け palette: red → orange → yellow → teal → cyan
|
|
44
|
-
const SUNSET_STOPS: [number, number, number, number, number] = [0xdc2626, 0xea580c, 0xeab308, 0x0d9488, 0x0e7490];
|
|
45
|
-
|
|
46
|
-
// 朝焼け (sunrise): rose → coral → gold → sky → cyan
|
|
47
|
-
const SUNRISE_STOPS: [number, number, number, number, number] = [0xfda4af, 0xfb923c, 0xfde047, 0x93c5fd, 0x67e8f9];
|
|
48
|
-
|
|
49
|
-
const SUNRISE_LAYERS = [
|
|
50
|
-
{ r: 2.5, peak: 0.50, y: -0.25, z: -0.2 },
|
|
51
|
-
{ r: 0.5, peak: 0.55, y: -0.38, z: -0.05 },
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
const emptySubscribe = () => () => {};
|
|
55
|
-
function useIsMounted() {
|
|
56
|
-
return useSyncExternalStore(emptySubscribe, () => true, () => false);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type Tier = "low" | "medium" | "high";
|
|
60
|
-
|
|
61
|
-
type SceneProfile = {
|
|
62
|
-
animate: boolean;
|
|
63
|
-
targetFps: number;
|
|
64
|
-
tier: Tier;
|
|
65
|
-
dpr: [number, number];
|
|
66
|
-
antialias: boolean;
|
|
67
|
-
enableClouds: boolean;
|
|
68
|
-
enableSeaLife: boolean;
|
|
69
|
-
seaLifeLite: boolean;
|
|
70
|
-
sunSegments: number;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
function useSceneProfile(): SceneProfile {
|
|
74
|
-
const [profile, setProfile] = useState<SceneProfile>({
|
|
75
|
-
animate: true,
|
|
76
|
-
targetFps: 45,
|
|
77
|
-
tier: "high",
|
|
78
|
-
dpr: [1, 2],
|
|
79
|
-
antialias: true,
|
|
80
|
-
enableClouds: true,
|
|
81
|
-
enableSeaLife: true,
|
|
82
|
-
seaLifeLite: false,
|
|
83
|
-
sunSegments: 64,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
const reduceMotionMedia = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
88
|
-
const mobileMedia = window.matchMedia("(max-width: 767px)");
|
|
89
|
-
|
|
90
|
-
const update = () => {
|
|
91
|
-
const reduceMotion = reduceMotionMedia.matches;
|
|
92
|
-
const isMobile = mobileMedia.matches;
|
|
93
|
-
const cores = navigator.hardwareConcurrency ?? 8;
|
|
94
|
-
const memory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory ?? 8;
|
|
95
|
-
const lowDevice = isMobile || cores <= 4 || memory <= 4;
|
|
96
|
-
const midDevice = !lowDevice && (cores <= 6 || memory <= 6);
|
|
97
|
-
|
|
98
|
-
const tier: Tier = lowDevice ? "low" : midDevice ? "medium" : "high";
|
|
99
|
-
|
|
100
|
-
setProfile({
|
|
101
|
-
animate: !reduceMotion && document.visibilityState === "visible",
|
|
102
|
-
targetFps: tier === "low" ? 24 : tier === "medium" ? 35 : 45,
|
|
103
|
-
tier,
|
|
104
|
-
dpr: tier === "low" ? [1, 1.25] : tier === "medium" ? [1, 1.5] : [1, 2],
|
|
105
|
-
antialias: tier !== "low",
|
|
106
|
-
enableClouds: tier !== "low",
|
|
107
|
-
enableSeaLife: true,
|
|
108
|
-
seaLifeLite: tier === "low",
|
|
109
|
-
sunSegments: tier === "low" ? 24 : tier === "medium" ? 40 : 64,
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
update();
|
|
114
|
-
window.addEventListener("resize", update, { passive: true });
|
|
115
|
-
document.addEventListener("visibilitychange", update);
|
|
116
|
-
reduceMotionMedia.addEventListener?.("change", update);
|
|
117
|
-
mobileMedia.addEventListener?.("change", update);
|
|
118
|
-
|
|
119
|
-
return () => {
|
|
120
|
-
window.removeEventListener("resize", update);
|
|
121
|
-
document.removeEventListener("visibilitychange", update);
|
|
122
|
-
reduceMotionMedia.removeEventListener?.("change", update);
|
|
123
|
-
mobileMedia.removeEventListener?.("change", update);
|
|
124
|
-
};
|
|
125
|
-
}, []);
|
|
126
|
-
|
|
127
|
-
return profile;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const GRADIENT_LAYERS = [
|
|
131
|
-
{ r: 2.5, peak: 0.60, y: -0.25, z: -0.2 },
|
|
132
|
-
{ r: 0.5, peak: 0.50, y: -0.38, z: -0.05 },
|
|
133
|
-
];
|
|
134
|
-
|
|
135
|
-
function GradientGlowSun({ isDark, segments = 64 }: { isDark: boolean; segments?: number }) {
|
|
136
|
-
const refs = useRef<(THREE.ShaderMaterial | null)[]>([]);
|
|
137
|
-
const stops = isDark ? SUNSET_STOPS : SUNRISE_STOPS;
|
|
138
|
-
const layers = isDark ? GRADIENT_LAYERS : SUNRISE_LAYERS;
|
|
139
|
-
const [s0, s1, s2, s3, s4] = stops;
|
|
140
|
-
|
|
141
|
-
const uniforms = useMemo(
|
|
142
|
-
() =>
|
|
143
|
-
layers.map((cfg) => ({
|
|
144
|
-
c1: { value: new THREE.Color(s0) },
|
|
145
|
-
c2: { value: new THREE.Color(s1) },
|
|
146
|
-
c3: { value: new THREE.Color(s2) },
|
|
147
|
-
c4: { value: new THREE.Color(s3) },
|
|
148
|
-
c5: { value: new THREE.Color(s4) },
|
|
149
|
-
uTime: { value: 0 },
|
|
150
|
-
uPeak: { value: cfg.peak },
|
|
151
|
-
uClipY: { value: -0.5 },
|
|
152
|
-
})),
|
|
153
|
-
[s0, s1, s2, s3, s4, isDark],
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
useFrame(({ clock }) => {
|
|
157
|
-
const t = clock.getElapsedTime();
|
|
158
|
-
refs.current.forEach((mat) => {
|
|
159
|
-
if (mat) mat.uniforms.uTime.value = t;
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
return (
|
|
164
|
-
<group position={[0, 0, -1.2]}>
|
|
165
|
-
{layers.map((cfg, i) => (
|
|
166
|
-
<mesh key={i} position={[0, cfg.y, cfg.z]}>
|
|
167
|
-
<circleGeometry args={[cfg.r, segments]} />
|
|
168
|
-
<shaderMaterial
|
|
169
|
-
ref={(el) => {
|
|
170
|
-
refs.current[i] = el;
|
|
171
|
-
}}
|
|
172
|
-
vertexShader={GRADIENT_VERTEX}
|
|
173
|
-
fragmentShader={GRADIENT_FRAGMENT}
|
|
174
|
-
uniforms={uniforms[i]}
|
|
175
|
-
transparent
|
|
176
|
-
side={THREE.DoubleSide}
|
|
177
|
-
depthWrite={false}
|
|
178
|
-
blending={isDark ? THREE.AdditiveBlending : THREE.NormalBlending}
|
|
179
|
-
/>
|
|
180
|
-
</mesh>
|
|
181
|
-
))}
|
|
182
|
-
</group>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ─── Clouds (Scatter) ───
|
|
187
|
-
|
|
188
|
-
const CLOUD_VERTEX = /* glsl */ `
|
|
189
|
-
varying vec2 vUv;
|
|
190
|
-
void main() {
|
|
191
|
-
vUv = uv;
|
|
192
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
193
|
-
}
|
|
194
|
-
`;
|
|
195
|
-
|
|
196
|
-
const CLOUD_FRAGMENT = /* glsl */ `
|
|
197
|
-
uniform float uTime;
|
|
198
|
-
uniform vec3 uCloudColor;
|
|
199
|
-
uniform float uMaxAlpha;
|
|
200
|
-
varying vec2 vUv;
|
|
201
|
-
|
|
202
|
-
float hash(vec2 p) {
|
|
203
|
-
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
|
204
|
-
}
|
|
205
|
-
float noise(vec2 p) {
|
|
206
|
-
vec2 i = floor(p);
|
|
207
|
-
vec2 f = fract(p);
|
|
208
|
-
f = f * f * (3.0 - 2.0 * f);
|
|
209
|
-
float a = hash(i);
|
|
210
|
-
float b = hash(i + vec2(1.0, 0.0));
|
|
211
|
-
float c = hash(i + vec2(0.0, 1.0));
|
|
212
|
-
float d = hash(i + vec2(1.0, 1.0));
|
|
213
|
-
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
214
|
-
}
|
|
215
|
-
float fbm(vec2 p) {
|
|
216
|
-
float v = 0.0;
|
|
217
|
-
float a = 0.5;
|
|
218
|
-
for (int i = 0; i < 4; i++) {
|
|
219
|
-
v += a * noise(p);
|
|
220
|
-
p *= 2.0;
|
|
221
|
-
a *= 0.5;
|
|
222
|
-
}
|
|
223
|
-
return v;
|
|
224
|
-
}
|
|
225
|
-
void main() {
|
|
226
|
-
vec2 uv = vUv;
|
|
227
|
-
uv.x += uTime * 0.015;
|
|
228
|
-
float n = fbm(uv * 12.0);
|
|
229
|
-
float cloud = smoothstep(0.35, 0.7, n);
|
|
230
|
-
float vFade = smoothstep(0.0, 0.3, vUv.y) * smoothstep(1.0, 0.7, vUv.y);
|
|
231
|
-
float hFade = smoothstep(0.0, 0.15, vUv.x) * smoothstep(1.0, 0.85, vUv.x);
|
|
232
|
-
float alpha = cloud * vFade * hFade * uMaxAlpha;
|
|
233
|
-
vec3 color = uCloudColor;
|
|
234
|
-
gl_FragColor = vec4(color, alpha);
|
|
235
|
-
}
|
|
236
|
-
`;
|
|
237
|
-
|
|
238
|
-
function Clouds({ isDark }: { isDark: boolean }) {
|
|
239
|
-
const ref = useRef<THREE.ShaderMaterial>(null);
|
|
240
|
-
const uniforms = useMemo(() => ({
|
|
241
|
-
uTime: { value: 0 },
|
|
242
|
-
uCloudColor: { value: isDark ? new THREE.Vector3(0.75, 0.55, 0.35) : new THREE.Vector3(0.75, 0.60, 0.65) },
|
|
243
|
-
uMaxAlpha: { value: isDark ? 0.08 : 0.16 },
|
|
244
|
-
}), [isDark]);
|
|
245
|
-
|
|
246
|
-
useFrame(({ clock }) => {
|
|
247
|
-
if (ref.current) ref.current.uniforms.uTime.value = clock.getElapsedTime();
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
return (
|
|
251
|
-
<mesh position={[0, 0.5, -1.0]}>
|
|
252
|
-
<planeGeometry args={[8, 2]} />
|
|
253
|
-
<shaderMaterial
|
|
254
|
-
ref={ref}
|
|
255
|
-
vertexShader={CLOUD_VERTEX}
|
|
256
|
-
fragmentShader={CLOUD_FRAGMENT}
|
|
257
|
-
uniforms={uniforms}
|
|
258
|
-
transparent
|
|
259
|
-
depthWrite={false}
|
|
260
|
-
side={THREE.DoubleSide}
|
|
261
|
-
/>
|
|
262
|
-
</mesh>
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ─── Water Surface ───
|
|
267
|
-
|
|
268
|
-
const WATER_VERTEX = /* glsl */ `
|
|
269
|
-
varying vec2 vUv;
|
|
270
|
-
void main() {
|
|
271
|
-
vUv = uv;
|
|
272
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
273
|
-
}
|
|
274
|
-
`;
|
|
275
|
-
|
|
276
|
-
const WATER_FRAGMENT = /* glsl */ `
|
|
277
|
-
uniform float uTime;
|
|
278
|
-
uniform vec3 uWarm;
|
|
279
|
-
uniform vec3 uHot;
|
|
280
|
-
uniform vec3 uCold;
|
|
281
|
-
varying vec2 vUv;
|
|
282
|
-
void main() {
|
|
283
|
-
float y = vUv.y;
|
|
284
|
-
float x = vUv.x;
|
|
285
|
-
float ripple1 = sin((y * 60.0) + uTime * 0.8 + x * 3.0) * 0.5 + 0.5;
|
|
286
|
-
float ripple2 = sin((y * 30.0) - uTime * 0.5 + 1.5) * 0.5 + 0.5;
|
|
287
|
-
float ripple3 = sin((y * 100.0) + uTime * 1.2 - x * 5.0) * 0.5 + 0.5;
|
|
288
|
-
float ripple = ripple1 * 0.4 + ripple2 * 0.35 + ripple3 * 0.25;
|
|
289
|
-
float shimmer = sin(x * 8.0 + uTime * 0.3) * sin(y * 12.0 - uTime * 0.4);
|
|
290
|
-
shimmer = max(shimmer, 0.0) * 0.4;
|
|
291
|
-
float fadeTop = pow(1.0 - y, 1.5);
|
|
292
|
-
float fadeBottom = smoothstep(0.0, 0.35, y);
|
|
293
|
-
float fade = fadeTop * fadeBottom;
|
|
294
|
-
vec3 warm = uWarm;
|
|
295
|
-
vec3 hot = uHot;
|
|
296
|
-
vec3 cold = uCold;
|
|
297
|
-
vec3 base = mix(warm, cold, y);
|
|
298
|
-
vec3 color = mix(base, hot, shimmer * (1.0 - y));
|
|
299
|
-
float alpha = (ripple + shimmer * 0.3) * fade * 1.0;
|
|
300
|
-
gl_FragColor = vec4(color, alpha);
|
|
301
|
-
}
|
|
302
|
-
`;
|
|
303
|
-
|
|
304
|
-
function WaterSurface({ isDark }: { isDark: boolean }) {
|
|
305
|
-
const ref = useRef<THREE.ShaderMaterial>(null);
|
|
306
|
-
const uniforms = useMemo(() => ({
|
|
307
|
-
uTime: { value: 0 },
|
|
308
|
-
uWarm: { value: isDark ? new THREE.Vector3(0.40, 0.14, 0.04) : new THREE.Vector3(0.85, 0.55, 0.35) },
|
|
309
|
-
uHot: { value: isDark ? new THREE.Vector3(0.55, 0.20, 0.06) : new THREE.Vector3(0.95, 0.60, 0.30) },
|
|
310
|
-
uCold: { value: isDark ? new THREE.Vector3(0.03, 0.04, 0.06) : new THREE.Vector3(0.45, 0.60, 0.80) },
|
|
311
|
-
}), [isDark]);
|
|
312
|
-
|
|
313
|
-
useFrame(({ clock }) => {
|
|
314
|
-
if (ref.current) ref.current.uniforms.uTime.value = clock.getElapsedTime();
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
return (
|
|
318
|
-
<mesh position={[0, -0.55, -0.5]} rotation={[-Math.PI / 2, 0, 0]}>
|
|
319
|
-
<planeGeometry args={[14, 4]} />
|
|
320
|
-
<shaderMaterial
|
|
321
|
-
ref={ref}
|
|
322
|
-
vertexShader={WATER_VERTEX}
|
|
323
|
-
fragmentShader={WATER_FRAGMENT}
|
|
324
|
-
uniforms={uniforms}
|
|
325
|
-
transparent
|
|
326
|
-
depthWrite={false}
|
|
327
|
-
side={THREE.DoubleSide}
|
|
328
|
-
/>
|
|
329
|
-
</mesh>
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// ─── Sea Creatures ───
|
|
334
|
-
|
|
335
|
-
interface Swimmer {
|
|
336
|
-
label: string;
|
|
337
|
-
color: string;
|
|
338
|
-
speed: number;
|
|
339
|
-
y: number;
|
|
340
|
-
z: number;
|
|
341
|
-
scale: number;
|
|
342
|
-
shape: THREE.Shape;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function makeFish(): THREE.Shape {
|
|
346
|
-
const s = new THREE.Shape();
|
|
347
|
-
// Tail fork
|
|
348
|
-
s.moveTo(-0.10, 0.07);
|
|
349
|
-
s.lineTo(-0.07, 0.05);
|
|
350
|
-
s.lineTo(-0.10, 0.03);
|
|
351
|
-
s.lineTo(-0.07, 0.035);
|
|
352
|
-
// Body bottom
|
|
353
|
-
s.lineTo(-0.04, 0.02);
|
|
354
|
-
s.lineTo(0, 0.01);
|
|
355
|
-
s.lineTo(0.04, 0.01);
|
|
356
|
-
s.lineTo(0.08, 0.015);
|
|
357
|
-
// Mouth
|
|
358
|
-
s.lineTo(0.12, 0.03);
|
|
359
|
-
s.lineTo(0.13, 0.04);
|
|
360
|
-
s.lineTo(0.14, 0.05);
|
|
361
|
-
// Head top
|
|
362
|
-
s.lineTo(0.12, 0.065);
|
|
363
|
-
s.lineTo(0.10, 0.075);
|
|
364
|
-
// Eye indent
|
|
365
|
-
s.lineTo(0.09, 0.07);
|
|
366
|
-
s.lineTo(0.08, 0.075);
|
|
367
|
-
// Dorsal line
|
|
368
|
-
s.lineTo(0.05, 0.085);
|
|
369
|
-
s.lineTo(0.02, 0.09);
|
|
370
|
-
// Dorsal fin
|
|
371
|
-
s.lineTo(-0.01, 0.11);
|
|
372
|
-
s.lineTo(-0.02, 0.10);
|
|
373
|
-
s.lineTo(-0.03, 0.085);
|
|
374
|
-
// Back to tail
|
|
375
|
-
s.lineTo(-0.05, 0.075);
|
|
376
|
-
s.lineTo(-0.07, 0.065);
|
|
377
|
-
s.closePath();
|
|
378
|
-
// Pectoral fin
|
|
379
|
-
s.moveTo(0.04, 0.03);
|
|
380
|
-
s.lineTo(0.02, -0.01);
|
|
381
|
-
s.lineTo(0.06, 0.02);
|
|
382
|
-
return s;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function makeJellyfish(): THREE.Shape {
|
|
386
|
-
const s = new THREE.Shape();
|
|
387
|
-
// Bell dome (angular)
|
|
388
|
-
s.moveTo(-0.07, 0.01);
|
|
389
|
-
s.lineTo(-0.08, 0.03);
|
|
390
|
-
s.lineTo(-0.075, 0.06);
|
|
391
|
-
s.lineTo(-0.06, 0.08);
|
|
392
|
-
s.lineTo(-0.04, 0.10);
|
|
393
|
-
s.lineTo(-0.02, 0.11);
|
|
394
|
-
s.lineTo(0, 0.115);
|
|
395
|
-
s.lineTo(0.02, 0.11);
|
|
396
|
-
s.lineTo(0.04, 0.10);
|
|
397
|
-
s.lineTo(0.06, 0.08);
|
|
398
|
-
s.lineTo(0.075, 0.06);
|
|
399
|
-
s.lineTo(0.08, 0.03);
|
|
400
|
-
s.lineTo(0.07, 0.01);
|
|
401
|
-
// Bell rim
|
|
402
|
-
s.lineTo(0.06, 0.005);
|
|
403
|
-
s.lineTo(0.04, 0.01);
|
|
404
|
-
s.lineTo(0.02, 0.005);
|
|
405
|
-
s.lineTo(0, 0.01);
|
|
406
|
-
s.lineTo(-0.02, 0.005);
|
|
407
|
-
s.lineTo(-0.04, 0.01);
|
|
408
|
-
s.lineTo(-0.06, 0.005);
|
|
409
|
-
s.closePath();
|
|
410
|
-
// Tentacles (separate paths)
|
|
411
|
-
s.moveTo(-0.05, 0.005);
|
|
412
|
-
s.lineTo(-0.055, -0.04);
|
|
413
|
-
s.lineTo(-0.045, -0.02);
|
|
414
|
-
s.lineTo(-0.04, -0.06);
|
|
415
|
-
s.lineTo(-0.035, -0.03);
|
|
416
|
-
s.moveTo(-0.01, 0.005);
|
|
417
|
-
s.lineTo(-0.015, -0.05);
|
|
418
|
-
s.lineTo(-0.005, -0.03);
|
|
419
|
-
s.lineTo(0, -0.07);
|
|
420
|
-
s.lineTo(0.005, -0.03);
|
|
421
|
-
s.lineTo(0.015, -0.05);
|
|
422
|
-
s.moveTo(0.04, 0.005);
|
|
423
|
-
s.lineTo(0.035, -0.03);
|
|
424
|
-
s.lineTo(0.04, -0.06);
|
|
425
|
-
s.lineTo(0.045, -0.02);
|
|
426
|
-
s.lineTo(0.055, -0.04);
|
|
427
|
-
return s;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function makeTurtle(): THREE.Shape {
|
|
431
|
-
const s = new THREE.Shape();
|
|
432
|
-
// Head
|
|
433
|
-
s.moveTo(0.14, 0.04);
|
|
434
|
-
s.lineTo(0.16, 0.045);
|
|
435
|
-
s.lineTo(0.17, 0.04);
|
|
436
|
-
s.lineTo(0.16, 0.035);
|
|
437
|
-
s.lineTo(0.14, 0.035);
|
|
438
|
-
// Neck
|
|
439
|
-
s.lineTo(0.11, 0.03);
|
|
440
|
-
// Front flipper top
|
|
441
|
-
s.lineTo(0.09, 0.015);
|
|
442
|
-
s.lineTo(0.07, -0.01);
|
|
443
|
-
s.lineTo(0.10, 0.005);
|
|
444
|
-
// Shell bottom front
|
|
445
|
-
s.lineTo(0.08, 0.02);
|
|
446
|
-
s.lineTo(0.04, 0.015);
|
|
447
|
-
s.lineTo(0, 0.015);
|
|
448
|
-
// Rear flipper
|
|
449
|
-
s.lineTo(-0.04, 0.01);
|
|
450
|
-
s.lineTo(-0.07, -0.005);
|
|
451
|
-
s.lineTo(-0.05, 0.015);
|
|
452
|
-
// Tail
|
|
453
|
-
s.lineTo(-0.08, 0.025);
|
|
454
|
-
s.lineTo(-0.10, 0.03);
|
|
455
|
-
s.lineTo(-0.08, 0.035);
|
|
456
|
-
// Shell top
|
|
457
|
-
s.lineTo(-0.06, 0.04);
|
|
458
|
-
s.lineTo(-0.04, 0.055);
|
|
459
|
-
s.lineTo(-0.02, 0.065);
|
|
460
|
-
s.lineTo(0, 0.07);
|
|
461
|
-
s.lineTo(0.02, 0.07);
|
|
462
|
-
s.lineTo(0.04, 0.065);
|
|
463
|
-
s.lineTo(0.06, 0.06);
|
|
464
|
-
s.lineTo(0.08, 0.055);
|
|
465
|
-
// Shell pattern lines
|
|
466
|
-
s.lineTo(0.10, 0.048);
|
|
467
|
-
// Neck top
|
|
468
|
-
s.lineTo(0.12, 0.045);
|
|
469
|
-
s.closePath();
|
|
470
|
-
return s;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function makeWhale(): THREE.Shape {
|
|
474
|
-
const s = new THREE.Shape();
|
|
475
|
-
// Tail flukes
|
|
476
|
-
s.moveTo(-0.22, 0.06);
|
|
477
|
-
s.lineTo(-0.18, 0.04);
|
|
478
|
-
s.lineTo(-0.22, 0.02);
|
|
479
|
-
s.lineTo(-0.19, 0.035);
|
|
480
|
-
// Tail narrow
|
|
481
|
-
s.lineTo(-0.16, 0.03);
|
|
482
|
-
s.lineTo(-0.13, 0.025);
|
|
483
|
-
// Belly
|
|
484
|
-
s.lineTo(-0.08, 0.015);
|
|
485
|
-
s.lineTo(-0.03, 0.01);
|
|
486
|
-
s.lineTo(0.02, 0.01);
|
|
487
|
-
s.lineTo(0.07, 0.015);
|
|
488
|
-
// Jaw line
|
|
489
|
-
s.lineTo(0.12, 0.025);
|
|
490
|
-
s.lineTo(0.16, 0.035);
|
|
491
|
-
// Mouth
|
|
492
|
-
s.lineTo(0.18, 0.04);
|
|
493
|
-
s.lineTo(0.17, 0.045);
|
|
494
|
-
// Upper jaw
|
|
495
|
-
s.lineTo(0.15, 0.05);
|
|
496
|
-
s.lineTo(0.12, 0.055);
|
|
497
|
-
// Eye area
|
|
498
|
-
s.lineTo(0.10, 0.06);
|
|
499
|
-
s.lineTo(0.09, 0.058);
|
|
500
|
-
s.lineTo(0.08, 0.06);
|
|
501
|
-
// Forehead
|
|
502
|
-
s.lineTo(0.06, 0.07);
|
|
503
|
-
s.lineTo(0.03, 0.08);
|
|
504
|
-
// Back
|
|
505
|
-
s.lineTo(0, 0.085);
|
|
506
|
-
s.lineTo(-0.04, 0.08);
|
|
507
|
-
s.lineTo(-0.08, 0.075);
|
|
508
|
-
// Dorsal fin
|
|
509
|
-
s.lineTo(-0.10, 0.075);
|
|
510
|
-
s.lineTo(-0.11, 0.095);
|
|
511
|
-
s.lineTo(-0.13, 0.075);
|
|
512
|
-
// Tail section
|
|
513
|
-
s.lineTo(-0.15, 0.065);
|
|
514
|
-
s.lineTo(-0.17, 0.055);
|
|
515
|
-
s.lineTo(-0.19, 0.045);
|
|
516
|
-
s.closePath();
|
|
517
|
-
// Pectoral fin
|
|
518
|
-
s.moveTo(0.04, 0.02);
|
|
519
|
-
s.lineTo(0.02, -0.01);
|
|
520
|
-
s.lineTo(0.06, 0.01);
|
|
521
|
-
return s;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Diver uses PNG sprite instead of Shape path
|
|
525
|
-
|
|
526
|
-
const SWIMMERS: Swimmer[] = [];
|
|
527
|
-
|
|
528
|
-
function SwimmingCreature({ swimmer }: { swimmer: Swimmer }) {
|
|
529
|
-
const ref = useRef<Mesh>(null);
|
|
530
|
-
const geo = useMemo(() => new THREE.ShapeGeometry(swimmer.shape), [swimmer.shape]);
|
|
531
|
-
|
|
532
|
-
useFrame(({ clock }) => {
|
|
533
|
-
if (!ref.current) return;
|
|
534
|
-
const t = clock.getElapsedTime();
|
|
535
|
-
// Swim left to right, loop across screen
|
|
536
|
-
const x = ((t * swimmer.speed + swimmer.z * 2) % 5) - 2.5;
|
|
537
|
-
// Gentle bob up and down
|
|
538
|
-
const bob = Math.sin(t * 1.5 + swimmer.z * 10) * 0.008;
|
|
539
|
-
ref.current.position.x = x;
|
|
540
|
-
ref.current.position.y = swimmer.y + bob;
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
return (
|
|
544
|
-
<mesh ref={ref} position={[0, swimmer.y, swimmer.z]} scale={swimmer.scale}>
|
|
545
|
-
<primitive object={geo} attach="geometry" />
|
|
546
|
-
<meshBasicMaterial color={swimmer.color} transparent opacity={0.35} side={THREE.DoubleSide} />
|
|
547
|
-
</mesh>
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Free-swimming sprite: Lissajous curves for organic 2D movement
|
|
552
|
-
function useSwim(cfg: { cx: number; cy: number; rx: number; ry: number; freqX: number; freqY: number; phaseX: number; phaseY: number }) {
|
|
553
|
-
const ref = useRef<Mesh>(null);
|
|
554
|
-
useFrame(({ clock }) => {
|
|
555
|
-
if (!ref.current) return;
|
|
556
|
-
const t = clock.getElapsedTime();
|
|
557
|
-
const x = cfg.cx + Math.sin(t * cfg.freqX + cfg.phaseX) * cfg.rx;
|
|
558
|
-
const y = cfg.cy + Math.sin(t * cfg.freqY + cfg.phaseY) * cfg.ry;
|
|
559
|
-
// Face movement direction
|
|
560
|
-
const dx = Math.cos(t * cfg.freqX + cfg.phaseX) * cfg.freqX * cfg.rx;
|
|
561
|
-
const dy = Math.cos(t * cfg.freqY + cfg.phaseY) * cfg.freqY * cfg.ry;
|
|
562
|
-
ref.current.position.x = x;
|
|
563
|
-
ref.current.position.y = y;
|
|
564
|
-
ref.current.rotation.z = Math.atan2(dy, dx) * 0.3;
|
|
565
|
-
});
|
|
566
|
-
return ref;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function JellyfishSprite({ isDark }: { isDark: boolean }) {
|
|
570
|
-
const texture = useLoader(THREE.TextureLoader, "/images/jellyfish.png");
|
|
571
|
-
const ref = useSwim({ cx: -0.8, cy: -0.62, rx: 0.6, ry: 0.12, freqX: 0.12, freqY: 0.25, phaseX: 0, phaseY: 1.5 });
|
|
572
|
-
return (
|
|
573
|
-
<mesh ref={ref} position={[0, -0.63, -0.4]} scale={0.16}>
|
|
574
|
-
<planeGeometry args={[0.85, 1]} />
|
|
575
|
-
<meshBasicMaterial map={texture} transparent opacity={isDark ? 0.35 : 0.65} side={THREE.DoubleSide} />
|
|
576
|
-
</mesh>
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function FishSprite({ isDark }: { isDark: boolean }) {
|
|
581
|
-
const texture = useLoader(THREE.TextureLoader, "/images/fish.png");
|
|
582
|
-
const ref = useSwim({ cx: 0.3, cy: -0.60, rx: 1.2, ry: 0.10, freqX: 0.3, freqY: 0.7, phaseX: 2.0, phaseY: 0 });
|
|
583
|
-
return (
|
|
584
|
-
<mesh ref={ref} position={[0, -0.58, -0.3]} scale={0.22}>
|
|
585
|
-
<planeGeometry args={[1, 0.65]} />
|
|
586
|
-
<meshBasicMaterial map={texture} transparent opacity={isDark ? 0.4 : 0.7} side={THREE.DoubleSide} />
|
|
587
|
-
</mesh>
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function WhaleSprite({ isDark }: { isDark: boolean }) {
|
|
592
|
-
const texture = useLoader(THREE.TextureLoader, "/images/whale.png");
|
|
593
|
-
const ref = useSwim({ cx: 0, cy: -0.65, rx: 1.0, ry: 0.15, freqX: 0.06, freqY: 0.1, phaseX: 3.0, phaseY: 0.5 });
|
|
594
|
-
return (
|
|
595
|
-
<mesh ref={ref} position={[0, -0.65, -0.5]} scale={0.5}>
|
|
596
|
-
<planeGeometry args={[1, 0.7]} />
|
|
597
|
-
<meshBasicMaterial map={texture} transparent opacity={isDark ? 0.35 : 0.65} side={THREE.DoubleSide} />
|
|
598
|
-
</mesh>
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function TurtleSprite({ isDark }: { isDark: boolean }) {
|
|
603
|
-
const texture = useLoader(THREE.TextureLoader, "/images/turtle.png");
|
|
604
|
-
const ref = useSwim({ cx: 0.5, cy: -0.60, rx: 0.8, ry: 0.12, freqX: 0.15, freqY: 0.35, phaseX: 1.0, phaseY: 3.0 });
|
|
605
|
-
return (
|
|
606
|
-
<mesh ref={ref} position={[0, -0.56, -0.2]} scale={0.20}>
|
|
607
|
-
<planeGeometry args={[1, 1]} />
|
|
608
|
-
<meshBasicMaterial map={texture} transparent opacity={isDark ? 0.4 : 0.7} side={THREE.DoubleSide} />
|
|
609
|
-
</mesh>
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function DiverSprite({ isDark }: { isDark: boolean }) {
|
|
614
|
-
const texture = useLoader(THREE.TextureLoader, "/images/diver.png");
|
|
615
|
-
const ref = useSwim({ cx: -0.3, cy: -0.60, rx: 1.0, ry: 0.13, freqX: 0.18, freqY: 0.22, phaseX: 4.0, phaseY: 1.0 });
|
|
616
|
-
return (
|
|
617
|
-
<mesh ref={ref} position={[0, -0.60, -0.15]} scale={0.22}>
|
|
618
|
-
<planeGeometry args={[1, 1]} />
|
|
619
|
-
<meshBasicMaterial map={texture} transparent opacity={isDark ? 0.35 : 0.65} side={THREE.DoubleSide} />
|
|
620
|
-
</mesh>
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function CoralSprite({ isDark }: { isDark: boolean }) {
|
|
625
|
-
const texture = useLoader(THREE.TextureLoader, "/images/coral.png");
|
|
626
|
-
const ref = useRef<Mesh>(null);
|
|
627
|
-
// Gentle sway — stationary coral, subtle rotation
|
|
628
|
-
useFrame(({ clock }) => {
|
|
629
|
-
if (!ref.current) return;
|
|
630
|
-
const t = clock.getElapsedTime();
|
|
631
|
-
ref.current.rotation.z = Math.sin(t * 0.3) * 0.02;
|
|
632
|
-
});
|
|
633
|
-
return (
|
|
634
|
-
<mesh ref={ref} position={[0.9, -0.78, -0.6]} scale={0.22}>
|
|
635
|
-
<planeGeometry args={[1, 1]} />
|
|
636
|
-
<meshBasicMaterial map={texture} transparent opacity={isDark ? 0.3 : 0.55} side={THREE.DoubleSide} />
|
|
637
|
-
</mesh>
|
|
638
|
-
);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function SeaLife({ isDark }: { isDark: boolean }) {
|
|
642
|
-
return (
|
|
643
|
-
<group>
|
|
644
|
-
{SWIMMERS.map((sw) => (
|
|
645
|
-
<SwimmingCreature key={sw.label} swimmer={sw} />
|
|
646
|
-
))}
|
|
647
|
-
<FishSprite isDark={isDark} />
|
|
648
|
-
<DiverSprite isDark={isDark} />
|
|
649
|
-
<JellyfishSprite isDark={isDark} />
|
|
650
|
-
<WhaleSprite isDark={isDark} />
|
|
651
|
-
<TurtleSprite isDark={isDark} />
|
|
652
|
-
<CoralSprite isDark={isDark} />
|
|
653
|
-
</group>
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// ─── BaseLine ───
|
|
658
|
-
|
|
659
|
-
function BaseLine() {
|
|
660
|
-
return (
|
|
661
|
-
<mesh position={[0, -0.51, -0.98]}>
|
|
662
|
-
<boxGeometry args={[16, 0.012, 0.01]} />
|
|
663
|
-
<meshBasicMaterial color="#050505" />
|
|
664
|
-
</mesh>
|
|
665
|
-
);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// ─── Mountain Silhouettes ───
|
|
669
|
-
|
|
670
|
-
function MountainSilhouette() {
|
|
671
|
-
const islandsGeo = useMemo(() => {
|
|
672
|
-
const shape = new THREE.Shape();
|
|
673
|
-
shape.moveTo(-2.5, 0);
|
|
674
|
-
// Small rock (far left)
|
|
675
|
-
shape.lineTo(-1.8, 0);
|
|
676
|
-
shape.lineTo(-1.7, 0.08);
|
|
677
|
-
shape.lineTo(-1.6, 0.15);
|
|
678
|
-
shape.lineTo(-1.5, 0.08);
|
|
679
|
-
shape.lineTo(-1.4, 0);
|
|
680
|
-
// Gap
|
|
681
|
-
shape.lineTo(-1.2, 0);
|
|
682
|
-
// Island with 3 peaks — tallest left, descending right
|
|
683
|
-
shape.lineTo(-1.1, 0.05);
|
|
684
|
-
shape.lineTo(-1.05, 0.20);
|
|
685
|
-
shape.lineTo(-1.0, 0.40);
|
|
686
|
-
shape.lineTo(-0.95, 0.45);
|
|
687
|
-
shape.lineTo(-0.88, 0.40);
|
|
688
|
-
shape.lineTo(-0.80, 0.28);
|
|
689
|
-
shape.lineTo(-0.70, 0.30);
|
|
690
|
-
shape.lineTo(-0.62, 0.22);
|
|
691
|
-
shape.lineTo(-0.52, 0.24);
|
|
692
|
-
shape.lineTo(-0.42, 0.15);
|
|
693
|
-
shape.lineTo(-0.30, 0.05);
|
|
694
|
-
shape.lineTo(-0.2, 0);
|
|
695
|
-
// Center gap — sun space
|
|
696
|
-
shape.lineTo(0.2, 0);
|
|
697
|
-
// Large rounded mountain (right)
|
|
698
|
-
shape.lineTo(0.3, 0.05);
|
|
699
|
-
shape.lineTo(0.5, 0.18);
|
|
700
|
-
shape.lineTo(0.7, 0.35);
|
|
701
|
-
shape.lineTo(0.9, 0.48);
|
|
702
|
-
shape.lineTo(1.1, 0.52);
|
|
703
|
-
shape.lineTo(1.3, 0.48);
|
|
704
|
-
shape.lineTo(1.5, 0.38);
|
|
705
|
-
shape.lineTo(1.7, 0.25);
|
|
706
|
-
shape.lineTo(1.9, 0.12);
|
|
707
|
-
shape.lineTo(2.1, 0.05);
|
|
708
|
-
shape.lineTo(2.5, 0);
|
|
709
|
-
// Close bottom
|
|
710
|
-
shape.lineTo(2.5, -0.02);
|
|
711
|
-
shape.lineTo(-2.5, -0.02);
|
|
712
|
-
shape.closePath();
|
|
713
|
-
return new THREE.ShapeGeometry(shape);
|
|
714
|
-
}, []);
|
|
715
|
-
|
|
716
|
-
return (
|
|
717
|
-
<group position={[0, -0.5, -1]}>
|
|
718
|
-
<mesh geometry={islandsGeo}>
|
|
719
|
-
<meshBasicMaterial color="#050505" side={THREE.DoubleSide} />
|
|
720
|
-
</mesh>
|
|
721
|
-
<mesh geometry={islandsGeo} position={[0, 0.003, 0.01]}>
|
|
722
|
-
<meshBasicMaterial color="#0a0a0a" side={THREE.DoubleSide} />
|
|
723
|
-
</mesh>
|
|
724
|
-
{/* Pavilion on right mountain */}
|
|
725
|
-
<group position={[0.95, 0.48, 0.02]} scale={0.8}>
|
|
726
|
-
<mesh position={[-0.05, 0.025, 0]}>
|
|
727
|
-
<boxGeometry args={[0.006, 0.05, 0.01]} />
|
|
728
|
-
<meshBasicMaterial color="#050505" />
|
|
729
|
-
</mesh>
|
|
730
|
-
<mesh position={[0.05, 0.025, 0]}>
|
|
731
|
-
<boxGeometry args={[0.006, 0.05, 0.01]} />
|
|
732
|
-
<meshBasicMaterial color="#050505" />
|
|
733
|
-
</mesh>
|
|
734
|
-
<mesh position={[0, 0.053, 0]}>
|
|
735
|
-
<boxGeometry args={[0.14, 0.006, 0.01]} />
|
|
736
|
-
<meshBasicMaterial color="#050505" />
|
|
737
|
-
</mesh>
|
|
738
|
-
<mesh position={[0, 0.058, 0]}>
|
|
739
|
-
<boxGeometry args={[0.16, 0.004, 0.01]} />
|
|
740
|
-
<meshBasicMaterial color="#050505" />
|
|
741
|
-
</mesh>
|
|
742
|
-
</group>
|
|
743
|
-
</group>
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// ─── Main Scene ───
|
|
748
|
-
|
|
749
|
-
function FrameDriver({ active, targetFps }: { active: boolean; targetFps: number }) {
|
|
750
|
-
const { invalidate } = useThree();
|
|
751
|
-
|
|
752
|
-
useEffect(() => {
|
|
753
|
-
invalidate();
|
|
754
|
-
if (!active) return;
|
|
755
|
-
|
|
756
|
-
let raf = 0;
|
|
757
|
-
let last = 0;
|
|
758
|
-
const interval = 1000 / Math.max(1, targetFps);
|
|
759
|
-
|
|
760
|
-
const tick = (now: number) => {
|
|
761
|
-
if (now - last >= interval) {
|
|
762
|
-
last = now;
|
|
763
|
-
invalidate();
|
|
764
|
-
}
|
|
765
|
-
raf = requestAnimationFrame(tick);
|
|
766
|
-
};
|
|
767
|
-
|
|
768
|
-
raf = requestAnimationFrame(tick);
|
|
769
|
-
return () => cancelAnimationFrame(raf);
|
|
770
|
-
}, [active, targetFps, invalidate]);
|
|
771
|
-
|
|
772
|
-
return null;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/** Handles WebGL context lost/restored — forces re-render on recovery */
|
|
776
|
-
function ContextGuard() {
|
|
777
|
-
const { gl, invalidate } = useThree();
|
|
778
|
-
|
|
779
|
-
useEffect(() => {
|
|
780
|
-
const canvas = gl.domElement;
|
|
781
|
-
const onLost = (e: Event) => {
|
|
782
|
-
e.preventDefault();
|
|
783
|
-
console.warn("[HeroScene] WebGL context lost — pausing");
|
|
784
|
-
};
|
|
785
|
-
const onRestored = () => {
|
|
786
|
-
console.info("[HeroScene] WebGL context restored — resuming");
|
|
787
|
-
invalidate();
|
|
788
|
-
};
|
|
789
|
-
canvas.addEventListener("webglcontextlost", onLost);
|
|
790
|
-
canvas.addEventListener("webglcontextrestored", onRestored);
|
|
791
|
-
return () => {
|
|
792
|
-
canvas.removeEventListener("webglcontextlost", onLost);
|
|
793
|
-
canvas.removeEventListener("webglcontextrestored", onRestored);
|
|
794
|
-
};
|
|
795
|
-
}, [gl, invalidate]);
|
|
796
|
-
|
|
797
|
-
return null;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/** Disposes scene geometry/materials on unmount (renderer lifecycle managed by R3F) */
|
|
801
|
-
function ResourceCleanup() {
|
|
802
|
-
const { scene } = useThree();
|
|
803
|
-
|
|
804
|
-
useEffect(() => {
|
|
805
|
-
return () => {
|
|
806
|
-
scene.traverse((obj) => {
|
|
807
|
-
if (obj instanceof THREE.Mesh) {
|
|
808
|
-
obj.geometry?.dispose();
|
|
809
|
-
const mat = obj.material;
|
|
810
|
-
if (Array.isArray(mat)) {
|
|
811
|
-
mat.forEach((m) => m.dispose());
|
|
812
|
-
} else if (mat) {
|
|
813
|
-
mat.dispose();
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
};
|
|
818
|
-
}, [scene]);
|
|
819
|
-
|
|
820
|
-
return null;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// Screenshot helper — captures the WebGL canvas
|
|
824
|
-
function ScreenshotHelper({ onCapture }: { onCapture: ((fn: () => void) => void) }) {
|
|
825
|
-
const { gl, scene, camera } = useThree();
|
|
826
|
-
const capture = useCallback(() => {
|
|
827
|
-
gl.render(scene, camera);
|
|
828
|
-
const dataUrl = gl.domElement.toDataURL("image/png");
|
|
829
|
-
const link = document.createElement("a");
|
|
830
|
-
link.download = "hero-scene.png";
|
|
831
|
-
link.href = dataUrl;
|
|
832
|
-
link.click();
|
|
833
|
-
}, [gl, scene, camera]);
|
|
834
|
-
|
|
835
|
-
// Register the capture function
|
|
836
|
-
useEffect(() => onCapture(capture), [onCapture, capture]);
|
|
837
|
-
return null;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const LAYERS = ["Glow", "Mountain", "BaseLine", "Water", "Clouds", "SeaLife"] as const;
|
|
841
|
-
|
|
842
|
-
/** Static CSS gradient fallback for reduced-motion / no-WebGL */
|
|
843
|
-
function StaticFallback({ isDark }: { isDark: boolean }) {
|
|
844
|
-
return (
|
|
845
|
-
<div
|
|
846
|
-
className="h-full w-full"
|
|
847
|
-
style={{
|
|
848
|
-
background: isDark
|
|
849
|
-
? "radial-gradient(ellipse 80% 60% at 50% 65%, rgba(220,38,38,0.25) 0%, rgba(234,88,12,0.15) 25%, rgba(14,116,144,0.08) 50%, transparent 80%)"
|
|
850
|
-
: "radial-gradient(ellipse 80% 60% at 50% 65%, rgba(253,164,175,0.3) 0%, rgba(251,146,60,0.18) 25%, rgba(147,197,253,0.1) 50%, transparent 80%)",
|
|
851
|
-
}}
|
|
852
|
-
/>
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
export function HeroScene({ active = true }: { active?: boolean }) {
|
|
857
|
-
const { resolvedTheme } = useTheme();
|
|
858
|
-
const mounted = useIsMounted();
|
|
859
|
-
const profile = useSceneProfile();
|
|
860
|
-
const isDark = !mounted || resolvedTheme !== "light";
|
|
861
|
-
const isDev = process.env.NODE_ENV === "development";
|
|
862
|
-
const shouldAnimate = active && profile.animate;
|
|
863
|
-
|
|
864
|
-
// reduced-motion: show static CSS fallback instead of WebGL
|
|
865
|
-
const [reducedMotion, setReducedMotion] = useState(false);
|
|
866
|
-
useEffect(() => {
|
|
867
|
-
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
868
|
-
setReducedMotion(mq.matches);
|
|
869
|
-
const onChange = () => setReducedMotion(mq.matches);
|
|
870
|
-
mq.addEventListener?.("change", onChange);
|
|
871
|
-
return () => mq.removeEventListener?.("change", onChange);
|
|
872
|
-
}, []);
|
|
873
|
-
|
|
874
|
-
const [layers, setLayers] = useState({
|
|
875
|
-
Glow: true,
|
|
876
|
-
Mountain: true,
|
|
877
|
-
BaseLine: false,
|
|
878
|
-
Water: true,
|
|
879
|
-
Clouds: true,
|
|
880
|
-
SeaLife: true,
|
|
881
|
-
});
|
|
882
|
-
const captureRef = useRef<(() => void) | null>(null);
|
|
883
|
-
const canvasStyle = useMemo(() => ({ width: "100%", height: "100%" }) as const, []);
|
|
884
|
-
const toggle = (key: string) => setLayers((prev) => ({ ...prev, [key]: !prev[key as keyof typeof prev] }));
|
|
885
|
-
const on = (key: string) => layers[key as keyof typeof layers];
|
|
886
|
-
|
|
887
|
-
if (reducedMotion) {
|
|
888
|
-
return (
|
|
889
|
-
<div className="relative h-[400px] w-full overflow-hidden md:h-[550px]">
|
|
890
|
-
<StaticFallback isDark={isDark} />
|
|
891
|
-
</div>
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
return (
|
|
896
|
-
<div className="relative h-[400px] w-full overflow-hidden md:h-[550px]">
|
|
897
|
-
<div className="pointer-events-none h-full w-full">
|
|
898
|
-
<Canvas
|
|
899
|
-
style={canvasStyle}
|
|
900
|
-
camera={{ position: [0, 0.25, 3.0], fov: 50 }}
|
|
901
|
-
frameloop="demand"
|
|
902
|
-
dpr={profile.dpr}
|
|
903
|
-
gl={{
|
|
904
|
-
antialias: profile.antialias,
|
|
905
|
-
alpha: true,
|
|
906
|
-
preserveDrawingBuffer: isDev,
|
|
907
|
-
powerPreference: profile.tier === "low" ? "low-power" : "default",
|
|
908
|
-
}}
|
|
909
|
-
>
|
|
910
|
-
<ContextGuard />
|
|
911
|
-
<ResourceCleanup />
|
|
912
|
-
<FrameDriver active={shouldAnimate} targetFps={profile.targetFps} />
|
|
913
|
-
{isDev && <ScreenshotHelper onCapture={(fn) => { captureRef.current = fn; }} />}
|
|
914
|
-
{on("Water") && <WaterSurface isDark={isDark} />}
|
|
915
|
-
{on("Glow") && <GradientGlowSun isDark={isDark} segments={profile.sunSegments} />}
|
|
916
|
-
{profile.enableClouds && on("Clouds") && <Clouds isDark={isDark} />}
|
|
917
|
-
{on("Mountain") && <MountainSilhouette />}
|
|
918
|
-
{on("BaseLine") && <BaseLine />}
|
|
919
|
-
{profile.enableSeaLife && on("SeaLife") && <SeaLife isDark={isDark} />}
|
|
920
|
-
</Canvas>
|
|
921
|
-
</div>
|
|
922
|
-
|
|
923
|
-
{/* Layer toggles — dev only */}
|
|
924
|
-
{isDev && <div className="absolute bottom-4 left-1/2 z-20 flex -translate-x-1/2 gap-1">
|
|
925
|
-
<button
|
|
926
|
-
onClick={() => captureRef.current?.()}
|
|
927
|
-
className="rounded-md bg-muted/50 px-2 py-0.5 font-mono text-[9px] text-muted-foreground transition-colors hover:bg-muted"
|
|
928
|
-
>
|
|
929
|
-
📷
|
|
930
|
-
</button>
|
|
931
|
-
<span className="w-px self-stretch bg-muted/30" />
|
|
932
|
-
{LAYERS.map((key) => (
|
|
933
|
-
<button
|
|
934
|
-
key={key}
|
|
935
|
-
onClick={() => toggle(key)}
|
|
936
|
-
className={`rounded-md px-2 py-0.5 font-mono text-[9px] transition-colors ${
|
|
937
|
-
on(key)
|
|
938
|
-
? "bg-foreground text-background"
|
|
939
|
-
: "bg-muted/50 text-muted-foreground hover:bg-muted"
|
|
940
|
-
}`}
|
|
941
|
-
>
|
|
942
|
-
{key}
|
|
943
|
-
</button>
|
|
944
|
-
))}
|
|
945
|
-
</div>}
|
|
946
|
-
</div>
|
|
947
|
-
);
|
|
948
|
-
}
|