utm-scroll-scene 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/dist/index.js ADDED
@@ -0,0 +1,944 @@
1
+ // components/ScrollScene.tsx
2
+ import { useEffect as useEffect3, useLayoutEffect, useRef as useRef2, useState as useState2 } from "react";
3
+ import gsap2 from "gsap";
4
+
5
+ // components/DroneScene.tsx
6
+ import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
7
+
8
+ // lib/three/setup.ts
9
+ import * as THREE2 from "three";
10
+ import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
11
+ import { RoundedBoxGeometry } from "three/addons/geometries/RoundedBoxGeometry.js";
12
+
13
+ // lib/three/path.ts
14
+ import * as THREE from "three";
15
+ var launch0 = new THREE.Vector3(1.92, 0.35, 1.48);
16
+ var launch1 = new THREE.Vector3(1.75, 0.42, 1.22);
17
+ var launch2 = new THREE.Vector3(1.42, 0.5, 1);
18
+ var launch3 = new THREE.Vector3(1, 0.58, 0.7);
19
+ var flight0 = new THREE.Vector3(1, 0.58, 0.7);
20
+ var flight1 = new THREE.Vector3(0.4, 1.5, -0.4);
21
+ var flight2 = new THREE.Vector3(0, 1.75, -1.8);
22
+ var flight3 = new THREE.Vector3(0.65, 0.5, 0.1);
23
+ var flight2_0 = new THREE.Vector3(0.65, 0.5, 0.1);
24
+ var flight2_1 = new THREE.Vector3(0.4, 1.5, -1.5);
25
+ var flight2_2 = new THREE.Vector3(0.1, 2.2, -4.5);
26
+ var flight2_3 = new THREE.Vector3(-0.05, 1.5, -3.5);
27
+ var flight2_4 = new THREE.Vector3(-0.08, 1, -3.12);
28
+ var exit0 = new THREE.Vector3(-0.08, 0.35, -3.12);
29
+ var exit1 = new THREE.Vector3(-0.08, 1.3, -4.5);
30
+ var exit2 = new THREE.Vector3(-0.08, 2.4, -6.5);
31
+ var launchPath = new THREE.CatmullRomCurve3(
32
+ [launch0, launch1, launch2, launch3],
33
+ false,
34
+ "catmullrom",
35
+ 0.5
36
+ );
37
+ var flightPath = new THREE.CatmullRomCurve3(
38
+ [flight0, flight1, flight2, flight3],
39
+ false,
40
+ "catmullrom",
41
+ 0.42
42
+ );
43
+ var flightPath2 = new THREE.CatmullRomCurve3(
44
+ [flight2_0, flight2_1, flight2_2, flight2_3, flight2_4],
45
+ false,
46
+ "catmullrom",
47
+ 0.42
48
+ );
49
+ var INTRO_DRONE_POS = new THREE.Vector3(1.4, 0.5, 0.5);
50
+ var INTRO_DRONE_SCALE = 1.8;
51
+ var PICKUP_KIOSK_POS = new THREE.Vector3(0.65, -0.15, 0.1);
52
+ var exitPath = new THREE.CatmullRomCurve3(
53
+ [exit0, exit1, exit2],
54
+ false,
55
+ "catmullrom",
56
+ 0.5
57
+ );
58
+ var KIOSK_CENTER = new THREE.Vector3(-0.08, -0.175, -3.12);
59
+ var KIOSK_TOP_Y = 0;
60
+ function clamp01(value) {
61
+ return Math.max(0, Math.min(1, value));
62
+ }
63
+
64
+ // lib/three/setup.ts
65
+ function createFallbackDrone() {
66
+ const group = new THREE2.Group();
67
+ const bodyGeo = new THREE2.BoxGeometry(0.12, 0.06, 0.16);
68
+ const bodyMat = new THREE2.MeshStandardMaterial({
69
+ color: "#d7d1c4",
70
+ metalness: 0.3,
71
+ roughness: 0.6
72
+ });
73
+ const body = new THREE2.Mesh(bodyGeo, bodyMat);
74
+ body.castShadow = true;
75
+ group.add(body);
76
+ const propGeo = new THREE2.CylinderGeometry(0.02, 0.02, 5e-3, 8);
77
+ const propMat = new THREE2.MeshStandardMaterial({ color: "#333" });
78
+ for (let i = 0; i < 4; i++) {
79
+ const prop = new THREE2.Mesh(propGeo, propMat);
80
+ const angle = i / 4 * Math.PI * 2;
81
+ prop.position.set(
82
+ Math.cos(angle) * 0.1,
83
+ 0.04,
84
+ Math.sin(angle) * 0.1
85
+ );
86
+ group.add(prop);
87
+ }
88
+ group.rotation.x = Math.PI / 2;
89
+ return group;
90
+ }
91
+ function createPackageMesh() {
92
+ const geo = new RoundedBoxGeometry(0.08, 0.05, 0.12, 2, 8e-3);
93
+ const mat = new THREE2.MeshStandardMaterial({
94
+ color: "#8B6F47",
95
+ metalness: 0,
96
+ roughness: 0.92,
97
+ envMapIntensity: 0.3
98
+ });
99
+ const mesh = new THREE2.Mesh(geo, mat);
100
+ mesh.castShadow = true;
101
+ return mesh;
102
+ }
103
+ function createKioskMesh() {
104
+ const geo = new THREE2.BoxGeometry(0.5, 0.35, 0.4);
105
+ const mat = new THREE2.MeshStandardMaterial({
106
+ color: "#e8e4dc",
107
+ metalness: 0.1,
108
+ roughness: 0.8
109
+ });
110
+ const mesh = new THREE2.Mesh(geo, mat);
111
+ mesh.castShadow = true;
112
+ mesh.receiveShadow = true;
113
+ return mesh;
114
+ }
115
+ function createDroneRig(canvas) {
116
+ const renderer = new THREE2.WebGLRenderer({
117
+ canvas,
118
+ antialias: true,
119
+ alpha: true
120
+ });
121
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
122
+ renderer.toneMapping = THREE2.ACESFilmicToneMapping;
123
+ renderer.toneMappingExposure = 1;
124
+ renderer.outputColorSpace = THREE2.SRGBColorSpace;
125
+ renderer.shadowMap.enabled = true;
126
+ renderer.shadowMap.type = THREE2.PCFSoftShadowMap;
127
+ const scene = new THREE2.Scene();
128
+ scene.background = null;
129
+ const camera = new THREE2.PerspectiveCamera(
130
+ 45,
131
+ canvas.clientWidth / canvas.clientHeight,
132
+ 0.1,
133
+ 100
134
+ );
135
+ camera.position.set(0, 0.5, 4);
136
+ camera.lookAt(0, 0.3, 0);
137
+ const ambient = new THREE2.AmbientLight(16777215, 0.7);
138
+ scene.add(ambient);
139
+ const dirLight = new THREE2.DirectionalLight(16777215, 0.9);
140
+ dirLight.position.set(2, 4, 3);
141
+ dirLight.castShadow = true;
142
+ dirLight.shadow.mapSize.set(1024, 1024);
143
+ dirLight.shadow.camera.near = 0.5;
144
+ dirLight.shadow.camera.far = 20;
145
+ dirLight.shadow.camera.left = -4;
146
+ dirLight.shadow.camera.right = 4;
147
+ dirLight.shadow.camera.top = 4;
148
+ dirLight.shadow.camera.bottom = -4;
149
+ scene.add(dirLight);
150
+ const dronePayload = new THREE2.Group();
151
+ const drone = createFallbackDrone();
152
+ dronePayload.add(drone);
153
+ const loader = new GLTFLoader();
154
+ loader.load(
155
+ "/3d-models/assets/drone/scene.gltf",
156
+ (gltf) => {
157
+ const root = gltf.scene;
158
+ root.traverse((child) => {
159
+ if (child instanceof THREE2.Mesh) {
160
+ child.castShadow = true;
161
+ }
162
+ });
163
+ root.scale.setScalar(0.15);
164
+ while (drone.children.length) drone.remove(drone.children[0]);
165
+ drone.add(root);
166
+ },
167
+ void 0,
168
+ () => {
169
+ }
170
+ );
171
+ const pkg = createPackageMesh();
172
+ pkg.position.set(PICKUP_KIOSK_POS.x, PICKUP_KIOSK_POS.y + 0.175 + PACKAGE_HEIGHT / 2, PICKUP_KIOSK_POS.z);
173
+ pkg.visible = false;
174
+ scene.add(pkg);
175
+ scene.add(dronePayload);
176
+ const pickupKiosk = new THREE2.Group();
177
+ pickupKiosk.position.copy(PICKUP_KIOSK_POS);
178
+ pickupKiosk.visible = false;
179
+ const pickupKioskFallback = createKioskMesh();
180
+ pickupKiosk.add(pickupKioskFallback);
181
+ scene.add(pickupKiosk);
182
+ const pickupKioskLoader = new GLTFLoader();
183
+ pickupKioskLoader.load(
184
+ "/3d-models/assets/smart_box/scene.gltf",
185
+ (gltf) => {
186
+ const root = gltf.scene;
187
+ root.traverse((child) => {
188
+ if (child instanceof THREE2.Mesh) {
189
+ child.castShadow = true;
190
+ child.receiveShadow = true;
191
+ }
192
+ });
193
+ root.scale.setScalar(0.2);
194
+ root.position.set(0, 0, 0);
195
+ pickupKiosk.remove(pickupKioskFallback);
196
+ pickupKioskFallback.geometry.dispose();
197
+ pickupKioskFallback.material.dispose();
198
+ pickupKiosk.add(root);
199
+ },
200
+ void 0,
201
+ () => {
202
+ }
203
+ );
204
+ const kiosk = new THREE2.Group();
205
+ kiosk.position.copy(KIOSK_CENTER);
206
+ kiosk.visible = false;
207
+ const kioskFallback = createKioskMesh();
208
+ kiosk.add(kioskFallback);
209
+ scene.add(kiosk);
210
+ const kioskLoader = new GLTFLoader();
211
+ kioskLoader.load(
212
+ "/3d-models/assets/smart_box/scene.gltf",
213
+ (gltf) => {
214
+ const root = gltf.scene;
215
+ root.traverse((child) => {
216
+ if (child instanceof THREE2.Mesh) {
217
+ child.castShadow = true;
218
+ child.receiveShadow = true;
219
+ }
220
+ });
221
+ root.scale.setScalar(0.2);
222
+ root.position.set(0, 0, 0);
223
+ kiosk.remove(kioskFallback);
224
+ kioskFallback.geometry.dispose();
225
+ kioskFallback.material.dispose();
226
+ kiosk.add(root);
227
+ },
228
+ void 0,
229
+ () => {
230
+ }
231
+ );
232
+ return {
233
+ renderer,
234
+ scene,
235
+ camera,
236
+ dronePayload,
237
+ drone,
238
+ package: pkg,
239
+ pickupKiosk,
240
+ kiosk
241
+ };
242
+ }
243
+ function resizeDroneRig(rig, width, height, dpr) {
244
+ rig.renderer.setPixelRatio(Math.min(dpr, 2));
245
+ rig.renderer.setSize(width, height);
246
+ rig.camera.aspect = width / height;
247
+ rig.camera.updateProjectionMatrix();
248
+ }
249
+ function lerp(a, b, t) {
250
+ return a + (b - a) * clamp01(t);
251
+ }
252
+ var HOVER_ALTITUDE = 1;
253
+ var PACKAGE_HEIGHT = 0.05;
254
+ var PACKAGE_LAND_Y = KIOSK_TOP_Y + PACKAGE_HEIGHT / 2;
255
+ var PACKAGE_OFFSET_BELOW_DRONE = 0.4;
256
+ var RELEASE_ALTITUDE = PACKAGE_LAND_Y + PACKAGE_OFFSET_BELOW_DRONE + 0.06;
257
+ function renderDroneState(rig, state) {
258
+ const { dronePayload, drone, package: pkg, pickupKiosk, kiosk, camera } = rig;
259
+ const introDroneT = clamp01(state.introDroneT);
260
+ const emergeT = clamp01(state.emergeT);
261
+ const flightT = clamp01(state.flightT);
262
+ const flight2T = clamp01(state.flight2T);
263
+ const pickupT = clamp01(state.pickupT);
264
+ const dropT = clamp01(state.drop);
265
+ const releaseT = clamp01(state.release);
266
+ const departT = clamp01(state.depart);
267
+ const hoverT = clamp01(state.hover);
268
+ const bank = state.bank;
269
+ const pickupKioskReveal = clamp01(state.pickupKioskReveal);
270
+ const kioskReveal = clamp01(state.kioskReveal);
271
+ let scale;
272
+ let pos;
273
+ if (introDroneT > 0) {
274
+ const launchStart = launchPath.getPoint(0);
275
+ pos = introDroneT >= 1 ? INTRO_DRONE_POS.clone() : INTRO_DRONE_POS.clone().lerp(launchStart, 1 - introDroneT);
276
+ scale = lerp(1, INTRO_DRONE_SCALE, introDroneT);
277
+ } else if (departT > 0) {
278
+ scale = lerp(1, 0, departT);
279
+ pos = exitPath.getPoint(departT);
280
+ } else if (emergeT < 1) {
281
+ scale = 1;
282
+ pos = launchPath.getPoint(emergeT);
283
+ } else {
284
+ scale = 1;
285
+ if (flightT < 1) {
286
+ pos = flightPath.getPoint(flightT);
287
+ } else if (pickupT < 1) {
288
+ const flightEnd = flightPath.getPoint(1);
289
+ const pickupHover = new THREE2.Vector3(0.65, 0.5, 0.1);
290
+ pos = flightEnd.clone().lerp(pickupHover, pickupT);
291
+ } else if (flight2T < 1) {
292
+ pos = flightPath2.getPoint(flight2T);
293
+ const flyOutEnd = 0.35;
294
+ const flyInStart = 0.65;
295
+ if (flight2T <= flyOutEnd) {
296
+ scale = lerp(1, 0.12, flight2T / flyOutEnd);
297
+ } else if (flight2T >= flyInStart) {
298
+ scale = lerp(0.12, 1, (flight2T - flyInStart) / (1 - flyInStart));
299
+ } else {
300
+ scale = 0.12;
301
+ }
302
+ } else if (dropT > 0) {
303
+ const descendY = lerp(HOVER_ALTITUDE, RELEASE_ALTITUDE, dropT);
304
+ pos = new THREE2.Vector3(-0.08, descendY, -3.12);
305
+ } else {
306
+ const flight2End = flightPath2.getPoint(1);
307
+ const hoverPos = new THREE2.Vector3(-0.08, HOVER_ALTITUDE, -3.12);
308
+ pos = flight2End.clone().lerp(hoverPos, hoverT);
309
+ }
310
+ }
311
+ dronePayload.position.copy(pos);
312
+ dronePayload.scale.setScalar(scale);
313
+ dronePayload.updateMatrixWorld(true);
314
+ const bankRad = bank * 0.45;
315
+ dronePayload.rotation.order = "YXZ";
316
+ dronePayload.rotation.x = Math.PI / 2;
317
+ dronePayload.rotation.y = 0;
318
+ dronePayload.rotation.z = departT > 0 ? -0.2 : -bankRad;
319
+ const depthT = state.flightDepth;
320
+ const camOffsetX = -1;
321
+ const camOffsetZ = 2;
322
+ const camX = pos.x + camOffsetX;
323
+ const camZ = pos.z + camOffsetZ;
324
+ const camY = lerp(0.5, pos.y * 0.3 + 0.4, depthT);
325
+ camera.position.set(camX, camY, camZ);
326
+ const lookZ = pos.z - 0.4;
327
+ const lookY = pos.y * 0.6 + 0.15;
328
+ camera.lookAt(pos.x + 0.3, lookY, lookZ);
329
+ const pickupKioskRiseY = lerp(PICKUP_KIOSK_POS.y - 0.5, PICKUP_KIOSK_POS.y, pickupKioskReveal);
330
+ const pickupKioskTopY = pickupKioskRiseY + 0.175;
331
+ const pickupPackageY = pickupKioskTopY + PACKAGE_HEIGHT / 2;
332
+ const deliveryKioskPos = new THREE2.Vector3(-0.08, PACKAGE_LAND_Y, -3.12);
333
+ pkg.visible = introDroneT <= 0 && pickupKioskReveal > 0;
334
+ if (pkg.visible) {
335
+ if (pickupT < 1) {
336
+ if (pkg.parent) {
337
+ pkg.parent.remove(pkg);
338
+ rig.scene.add(pkg);
339
+ }
340
+ const pickupPos = new THREE2.Vector3(0.65, pickupPackageY, 0.1);
341
+ const attachWorld = new THREE2.Vector3(0, 0.4, 0).applyMatrix4(drone.matrixWorld);
342
+ pkg.position.lerpVectors(pickupPos, attachWorld, pickupT);
343
+ pkg.rotation.set(0, 0, 0);
344
+ pkg.scale.setScalar(1);
345
+ } else if (releaseT >= 1) {
346
+ if (pkg.parent) {
347
+ pkg.parent.remove(pkg);
348
+ rig.scene.add(pkg);
349
+ }
350
+ pkg.position.copy(deliveryKioskPos);
351
+ pkg.rotation.set(0, 0, 0);
352
+ pkg.scale.setScalar(1);
353
+ } else if (releaseT > 0) {
354
+ if (pkg.parent) {
355
+ pkg.parent.remove(pkg);
356
+ rig.scene.add(pkg);
357
+ }
358
+ const attachedWorld = new THREE2.Vector3(0, 0.4, 0).applyMatrix4(drone.matrixWorld);
359
+ pkg.position.lerpVectors(attachedWorld, deliveryKioskPos, releaseT);
360
+ pkg.rotation.set(0, 0, 0);
361
+ pkg.scale.setScalar(lerp(scale, 1, releaseT));
362
+ } else {
363
+ if (pkg.parent !== rig.drone) {
364
+ rig.scene.remove(pkg);
365
+ rig.drone.add(pkg);
366
+ }
367
+ pkg.position.set(0, 0.4, 0);
368
+ pkg.rotation.set(0, 0, 0);
369
+ pkg.scale.setScalar(1);
370
+ }
371
+ }
372
+ pickupKiosk.visible = pickupKioskReveal > 0;
373
+ pickupKiosk.position.set(PICKUP_KIOSK_POS.x, pickupKioskRiseY, PICKUP_KIOSK_POS.z);
374
+ kiosk.visible = kioskReveal > 0;
375
+ kiosk.traverse((obj) => {
376
+ if (obj instanceof THREE2.Mesh && obj.material) {
377
+ const mat = Array.isArray(obj.material) ? obj.material[0] : obj.material;
378
+ if (mat instanceof THREE2.MeshStandardMaterial) {
379
+ mat.opacity = kioskReveal;
380
+ mat.transparent = kioskReveal < 1;
381
+ }
382
+ }
383
+ });
384
+ rig.renderer.render(rig.scene, rig.camera);
385
+ }
386
+ function disposeDroneRig(rig) {
387
+ rig.renderer.dispose();
388
+ rig.dronePayload.traverse((obj) => {
389
+ var _a, _b;
390
+ if (obj instanceof THREE2.Mesh) {
391
+ (_a = obj.geometry) == null ? void 0 : _a.dispose();
392
+ if (Array.isArray(obj.material)) {
393
+ obj.material.forEach((m) => m.dispose());
394
+ } else {
395
+ (_b = obj.material) == null ? void 0 : _b.dispose();
396
+ }
397
+ }
398
+ });
399
+ rig.package.geometry.dispose();
400
+ rig.package.material.dispose();
401
+ rig.pickupKiosk.traverse((obj) => {
402
+ var _a, _b;
403
+ if (obj instanceof THREE2.Mesh) {
404
+ (_a = obj.geometry) == null ? void 0 : _a.dispose();
405
+ if (Array.isArray(obj.material)) {
406
+ obj.material.forEach((m) => m.dispose());
407
+ } else {
408
+ (_b = obj.material) == null ? void 0 : _b.dispose();
409
+ }
410
+ }
411
+ });
412
+ rig.kiosk.traverse((obj) => {
413
+ var _a, _b;
414
+ if (obj instanceof THREE2.Mesh) {
415
+ (_a = obj.geometry) == null ? void 0 : _a.dispose();
416
+ if (Array.isArray(obj.material)) {
417
+ obj.material.forEach((m) => m.dispose());
418
+ } else {
419
+ (_b = obj.material) == null ? void 0 : _b.dispose();
420
+ }
421
+ }
422
+ });
423
+ }
424
+
425
+ // components/DroneScene.tsx
426
+ import { jsx } from "react/jsx-runtime";
427
+ var DroneScene = forwardRef(function DroneScene2({ className }, ref) {
428
+ const canvasRef = useRef(null);
429
+ const rigRef = useRef(null);
430
+ const stateRef = useRef(null);
431
+ useImperativeHandle(ref, () => ({
432
+ setState(nextState) {
433
+ stateRef.current = nextState;
434
+ },
435
+ resize(width, height, dpr) {
436
+ if (!rigRef.current) {
437
+ return;
438
+ }
439
+ resizeDroneRig(rigRef.current, width, height, dpr);
440
+ },
441
+ renderFrame() {
442
+ if (!rigRef.current || !stateRef.current) {
443
+ return;
444
+ }
445
+ renderDroneState(rigRef.current, stateRef.current);
446
+ }
447
+ }));
448
+ useEffect(() => {
449
+ if (!canvasRef.current) {
450
+ return;
451
+ }
452
+ const rig = createDroneRig(canvasRef.current);
453
+ rigRef.current = rig;
454
+ return () => {
455
+ if (!rigRef.current) {
456
+ return;
457
+ }
458
+ disposeDroneRig(rigRef.current);
459
+ rigRef.current = null;
460
+ };
461
+ }, []);
462
+ return /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className });
463
+ });
464
+
465
+ // components/NarrationRevolver.tsx
466
+ import { forwardRef as forwardRef2, useImperativeHandle as useImperativeHandle2 } from "react";
467
+ import { motion, useMotionValue, useSpring, useTransform } from "motion/react";
468
+ import { jsx as jsx2 } from "react/jsx-runtime";
469
+ var NARRATIONS = [
470
+ {
471
+ text: "K\u1EF7 nguy\xEAn m\u1EDBi c\u1EE7a v\u1EADn t\u1EA3i th\xF4ng minh l\u1EA5y UAV l\xE0m c\u1ED1t l\xF5i",
472
+ tag: "h1",
473
+ className: "zipline-narration-intro"
474
+ },
475
+ {
476
+ text: "\u0110\u1EB7t h\xE0ng ti\u1EC7n l\u1EE3i tr\xEAn \u1EE9ng d\u1EE5ng",
477
+ tag: "h2",
478
+ className: "zipline-narration-order"
479
+ },
480
+ {
481
+ text: "Giao h\xE0ng si\xEAu t\u1ED1c trong v\xE0i ph\xFAt",
482
+ tag: "h2",
483
+ className: "zipline-narration-order"
484
+ },
485
+ {
486
+ text: "Nh\u1EADn h\xE0ng trong v\xE0i gi\xE2y",
487
+ tag: "h2",
488
+ className: "zipline-narration-delivers"
489
+ },
490
+ {
491
+ text: "Kh\xF4ng c\u1EA7n ch\u1EDD. UAV giao ngay. K\u1EBFt n\u1ED1i tr\u0103m tri\u1EC7u kh\xE1ch h\xE0ng. Tham gia LAE Sandbox ngay",
492
+ tag: "h3",
493
+ className: "zipline-narration-cta"
494
+ }
495
+ ];
496
+ var NarrationRevolver = forwardRef2(function NarrationRevolver2(_, ref) {
497
+ const activeHeading = useMotionValue(0);
498
+ const springHeading = useSpring(activeHeading, {
499
+ stiffness: 100,
500
+ damping: 25,
501
+ restDelta: 1e-3
502
+ });
503
+ useImperativeHandle2(ref, () => ({
504
+ setActiveHeading(value) {
505
+ activeHeading.set(value);
506
+ }
507
+ }));
508
+ const stripY = useTransform(springHeading, (v) => `${-v * 100}%`);
509
+ return /* @__PURE__ */ jsx2("div", { className: "zipline-narration-viewport", children: /* @__PURE__ */ jsx2(motion.div, { className: "zipline-narration-strip", style: { y: stripY }, children: NARRATIONS.map((item, index) => {
510
+ const Tag = item.tag;
511
+ const slotRotateX = useTransform(
512
+ springHeading,
513
+ (v) => (v - index) * 18
514
+ );
515
+ return /* @__PURE__ */ jsx2(
516
+ motion.div,
517
+ {
518
+ className: `zipline-narration-slot ${item.className}`,
519
+ style: {
520
+ rotateX: slotRotateX,
521
+ transformOrigin: "left top -300px",
522
+ backfaceVisibility: "hidden"
523
+ },
524
+ children: /* @__PURE__ */ jsx2(Tag, { children: item.text })
525
+ },
526
+ index
527
+ );
528
+ }) }) });
529
+ });
530
+
531
+ // components/PhoneUI.tsx
532
+ import { forwardRef as forwardRef3 } from "react";
533
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
534
+ var PhoneUI = forwardRef3(
535
+ function PhoneUI2({ screenRef }, ref) {
536
+ return /* @__PURE__ */ jsxs(
537
+ "div",
538
+ {
539
+ ref,
540
+ className: "phone-container",
541
+ style: {
542
+ width: "280px",
543
+ perspective: "1000px",
544
+ position: "relative"
545
+ },
546
+ children: [
547
+ /* @__PURE__ */ jsxs(
548
+ "svg",
549
+ {
550
+ viewBox: "0 0 280 560",
551
+ fill: "none",
552
+ xmlns: "http://www.w3.org/2000/svg",
553
+ style: { width: "100%", height: "auto", display: "block" },
554
+ children: [
555
+ /* @__PURE__ */ jsxs("defs", { children: [
556
+ /* @__PURE__ */ jsx3("clipPath", { id: "screenClip", children: /* @__PURE__ */ jsx3("rect", { x: "12", y: "12", width: "256", height: "536", rx: "28" }) }),
557
+ /* @__PURE__ */ jsxs("linearGradient", { id: "phoneBg", x1: "0", y1: "0", x2: "0", y2: "1", children: [
558
+ /* @__PURE__ */ jsx3("stop", { offset: "0%", stopColor: "#f7f9fc" }),
559
+ /* @__PURE__ */ jsx3("stop", { offset: "100%", stopColor: "#edf1f7" })
560
+ ] }),
561
+ /* @__PURE__ */ jsx3("filter", { id: "phoneShadow", children: /* @__PURE__ */ jsx3("feDropShadow", { dx: "0", dy: "8", stdDeviation: "16", floodOpacity: "0.15" }) })
562
+ ] }),
563
+ /* @__PURE__ */ jsx3(
564
+ "rect",
565
+ {
566
+ x: "2",
567
+ y: "2",
568
+ width: "276",
569
+ height: "556",
570
+ rx: "36",
571
+ fill: "#1a1a1a",
572
+ filter: "url(#phoneShadow)"
573
+ }
574
+ ),
575
+ /* @__PURE__ */ jsx3(
576
+ "rect",
577
+ {
578
+ x: "6",
579
+ y: "6",
580
+ width: "268",
581
+ height: "548",
582
+ rx: "33",
583
+ fill: "#2a2a2a"
584
+ }
585
+ ),
586
+ /* @__PURE__ */ jsxs("g", { clipPath: "url(#screenClip)", children: [
587
+ /* @__PURE__ */ jsx3("rect", { x: "12", y: "12", width: "256", height: "536", fill: "url(#phoneBg)" }),
588
+ /* @__PURE__ */ jsx3("rect", { x: "92", y: "22", width: "96", height: "24", rx: "12", fill: "#1a1a1a" }),
589
+ /* @__PURE__ */ jsx3("circle", { cx: "140", cy: "34", r: "5", fill: "#333" }),
590
+ /* @__PURE__ */ jsx3(
591
+ "text",
592
+ {
593
+ x: "140",
594
+ y: "72",
595
+ textAnchor: "middle",
596
+ fill: "#1a6bff",
597
+ fontSize: "11",
598
+ fontWeight: "700",
599
+ fontFamily: "system-ui, sans-serif",
600
+ children: "LAE Sandbox"
601
+ }
602
+ ),
603
+ /* @__PURE__ */ jsx3("rect", { x: "32", y: "90", width: "216", height: "100", rx: "16", fill: "#ffffff" }),
604
+ /* @__PURE__ */ jsx3(
605
+ "rect",
606
+ {
607
+ x: "32",
608
+ y: "90",
609
+ width: "216",
610
+ height: "100",
611
+ rx: "16",
612
+ fill: "none",
613
+ stroke: "#e5e7eb",
614
+ strokeWidth: "1"
615
+ }
616
+ ),
617
+ /* @__PURE__ */ jsx3("circle", { cx: "60", cy: "120", r: "16", fill: "#eef4ff" }),
618
+ /* @__PURE__ */ jsx3(
619
+ "text",
620
+ {
621
+ x: "60",
622
+ y: "124",
623
+ textAnchor: "middle",
624
+ fill: "#1a6bff",
625
+ fontSize: "14",
626
+ children: "\u{1F4E6}"
627
+ }
628
+ ),
629
+ /* @__PURE__ */ jsx3(
630
+ "text",
631
+ {
632
+ x: "88",
633
+ y: "118",
634
+ fill: "#111",
635
+ fontSize: "12",
636
+ fontWeight: "600",
637
+ fontFamily: "system-ui, sans-serif",
638
+ children: "\u0110\u01A1n h\xE0ng h\u1ECFa t\u1ED1c"
639
+ }
640
+ ),
641
+ /* @__PURE__ */ jsx3(
642
+ "text",
643
+ {
644
+ x: "88",
645
+ y: "136",
646
+ fill: "#6b7280",
647
+ fontSize: "10",
648
+ fontFamily: "system-ui, sans-serif",
649
+ children: "\u0110\u1EBFn trong ~4 ph\xFAt"
650
+ }
651
+ ),
652
+ /* @__PURE__ */ jsx3("rect", { x: "88", y: "152", width: "140", height: "6", rx: "3", fill: "#e5e7eb" }),
653
+ /* @__PURE__ */ jsx3("rect", { x: "88", y: "152", width: "95", height: "6", rx: "3", fill: "#1a6bff" }),
654
+ /* @__PURE__ */ jsx3("rect", { x: "32", y: "210", width: "216", height: "180", rx: "16", fill: "#e8f0fe" }),
655
+ /* @__PURE__ */ jsx3("circle", { cx: "140", cy: "280", r: "28", fill: "#ffffff", stroke: "#1a6bff", strokeWidth: "2" }),
656
+ /* @__PURE__ */ jsx3(
657
+ "text",
658
+ {
659
+ x: "140",
660
+ y: "276",
661
+ textAnchor: "middle",
662
+ fill: "#1a6bff",
663
+ fontSize: "16",
664
+ fontWeight: "700",
665
+ fontFamily: "system-ui, sans-serif",
666
+ children: "4"
667
+ }
668
+ ),
669
+ /* @__PURE__ */ jsx3(
670
+ "text",
671
+ {
672
+ x: "140",
673
+ y: "292",
674
+ textAnchor: "middle",
675
+ fill: "#1a6bff",
676
+ fontSize: "8",
677
+ fontFamily: "system-ui, sans-serif",
678
+ children: "MIN"
679
+ }
680
+ ),
681
+ /* @__PURE__ */ jsx3("circle", { cx: "100", cy: "340", r: "4", fill: "#1a6bff" }),
682
+ /* @__PURE__ */ jsx3("circle", { cx: "140", cy: "340", r: "4", fill: "#1a6bff", opacity: "0.5" }),
683
+ /* @__PURE__ */ jsx3("circle", { cx: "180", cy: "340", r: "4", fill: "#d1d5db" }),
684
+ /* @__PURE__ */ jsx3(
685
+ "text",
686
+ {
687
+ x: "140",
688
+ y: "370",
689
+ textAnchor: "middle",
690
+ fill: "#9ca3af",
691
+ fontSize: "9",
692
+ fontFamily: "system-ui, sans-serif",
693
+ children: "Theo d\xF5i tr\u1EF1c ti\u1EBFp"
694
+ }
695
+ ),
696
+ /* @__PURE__ */ jsx3("rect", { x: "32", y: "420", width: "216", height: "44", rx: "22", fill: "#1a6bff" }),
697
+ /* @__PURE__ */ jsx3(
698
+ "text",
699
+ {
700
+ x: "140",
701
+ y: "447",
702
+ textAnchor: "middle",
703
+ fill: "#ffffff",
704
+ fontSize: "13",
705
+ fontWeight: "600",
706
+ fontFamily: "system-ui, sans-serif",
707
+ children: "Theo d\xF5i \u0111\u01A1n h\xE0ng"
708
+ }
709
+ )
710
+ ] })
711
+ ]
712
+ }
713
+ ),
714
+ /* @__PURE__ */ jsx3(
715
+ "div",
716
+ {
717
+ ref: screenRef,
718
+ style: {
719
+ position: "absolute",
720
+ top: "12px",
721
+ left: "12px",
722
+ right: "12px",
723
+ bottom: "12px",
724
+ pointerEvents: "none"
725
+ }
726
+ }
727
+ )
728
+ ]
729
+ }
730
+ );
731
+ }
732
+ );
733
+ var PhoneUI_default = PhoneUI;
734
+
735
+ // lib/animation/timeline.ts
736
+ import gsap from "gsap";
737
+ import { ScrollTrigger } from "gsap/ScrollTrigger";
738
+ gsap.registerPlugin(ScrollTrigger);
739
+ function createMasterTimeline(root, state, onUpdate) {
740
+ const timeline = gsap.timeline({
741
+ defaults: { ease: "none" },
742
+ onUpdate,
743
+ scrollTrigger: {
744
+ trigger: root,
745
+ start: "top top",
746
+ end: "+=8000",
747
+ pin: true,
748
+ scrub: true,
749
+ anticipatePin: 1,
750
+ invalidateOnRefresh: true
751
+ }
752
+ });
753
+ timeline.addLabel("intro", 0).set(state, {
754
+ introDroneT: 1,
755
+ phoneOpacity: 0,
756
+ phoneMaskOpen: 0,
757
+ phoneTilt: 0,
758
+ phoneScale: 1,
759
+ portalBloom: 0,
760
+ uiPulse: 0,
761
+ emergeT: 0,
762
+ flightT: 0,
763
+ flight2T: 0,
764
+ pickupT: 0,
765
+ flightDepth: 0,
766
+ bank: 0,
767
+ hover: 0,
768
+ drop: 0,
769
+ release: 0,
770
+ reel: 0,
771
+ depart: 0,
772
+ settle: 0,
773
+ cloudFarShift: 0,
774
+ cloudNearShift: 0,
775
+ groundRise: 0,
776
+ pickupKioskReveal: 0,
777
+ kioskReveal: 0,
778
+ activeHeading: 0
779
+ }).addLabel("order_intro", 0.5).to(state, { introDroneT: 0, duration: 0.55 }, "order_intro").to(state, { phoneOpacity: 1, duration: 0.5 }, "order_intro").to(state, { activeHeading: 1, duration: 0.55, ease: "power2.inOut" }, "order_intro").addLabel("order_confirmed", 1.15).to(state, { uiPulse: 1, duration: 0.28 }, "order_confirmed").to(state, { phoneMaskOpen: 0.3, duration: 0.45 }, "order_confirmed+=0.05").to(state, { phoneTilt: -6, phoneScale: 0.95, duration: 0.48 }, "order_confirmed+=0.08").addLabel("drone_launch", 1.7).to(state, { portalBloom: 1, duration: 0.32 }, "drone_launch").to(state, { emergeT: 1, duration: 0.75 }, "drone_launch+=0.04").to(state, { phoneMaskOpen: 1, duration: 0.7 }, "drone_launch+=0.08").to(state, { phoneOpacity: 0.35, duration: 0.55 }, "drone_launch+=0.1").to(state, { activeHeading: 2, duration: 0.5 }, "drone_launch+=0.22").addLabel("flight", 2.65).to(state, { flightT: 1, duration: 2 }, "flight").to(state, { flightDepth: 1, duration: 1.8 }, "flight+=0.08").to(state, { bank: 1.15, duration: 0.5 }, "flight").to(state, { bank: -0.88, duration: 0.5 }, "flight+=0.5").to(state, { bank: 0.25, duration: 0.6 }, "flight+=1.1").to(state, { cloudFarShift: 1, duration: 2 }, "flight").to(state, { cloudNearShift: 1, duration: 2 }, "flight+=0.08").addLabel("pickup", 4.7).to(state, { pickupKioskReveal: 1, duration: 0.4 }, "pickup").to(state, { pickupT: 1, duration: 0.22 }, "pickup+=0.45").addLabel("flight_to_delivery", 5.42).to(state, { flight2T: 1, duration: 2.2 }, "flight_to_delivery").addLabel("package_drop", 6.8).to(state, { kioskReveal: 1, duration: 0.52 }, "package_drop").to(state, { hover: 1, duration: 0.45 }, "package_drop").to(state, { activeHeading: 3, duration: 0.45, ease: "power2.inOut" }, "package_drop+=0.06").to(state, { drop: 1, duration: 1.25 }, "package_drop+=0.12").to(state, { groundRise: 1, duration: 1.25 }, "package_drop+=0.12").to(state, { settle: 1, duration: 0.3 }, "package_drop+=1.28").to(state, { release: 1, duration: 0.08 }, "package_drop+=1.3").to(state, { reel: 1, duration: 0.01 }, "package_drop+=1.3").to(state, { depart: 1, duration: 2 }, "package_drop+=1.58").addLabel("exit", 8.5).to(state, { activeHeading: 4, duration: 0.5, ease: "power2.inOut" }, "exit").to(state, { phoneOpacity: 0, portalBloom: 0, duration: 0.5 }, "exit").to(state, { exit: 1, duration: 0.65 }, "exit+=0.08");
780
+ return timeline;
781
+ }
782
+
783
+ // lib/animation/types.ts
784
+ function createInitialNarrativeState() {
785
+ return {
786
+ introDroneT: 1,
787
+ phoneOpacity: 0,
788
+ phoneMaskOpen: 0,
789
+ phoneTilt: 0,
790
+ phoneScale: 1,
791
+ portalBloom: 0,
792
+ uiPulse: 0,
793
+ emergeT: 0,
794
+ flightT: 0,
795
+ flight2T: 0,
796
+ pickupT: 0,
797
+ flightDepth: 0,
798
+ bank: 0,
799
+ hover: 0,
800
+ drop: 0,
801
+ release: 0,
802
+ reel: 0,
803
+ depart: 0,
804
+ settle: 0,
805
+ cloudFarShift: 0,
806
+ cloudNearShift: 0,
807
+ groundRise: 0,
808
+ pickupKioskReveal: 0,
809
+ kioskReveal: 0,
810
+ activeHeading: 0,
811
+ exit: 0
812
+ };
813
+ }
814
+
815
+ // lib/hooks/useReducedMotion.ts
816
+ import { useEffect as useEffect2, useState } from "react";
817
+ function useReducedMotion() {
818
+ const [prefersReduced, setPrefersReduced] = useState(false);
819
+ useEffect2(() => {
820
+ const media = window.matchMedia("(prefers-reduced-motion: reduce)");
821
+ setPrefersReduced(media.matches);
822
+ const handler = (e) => setPrefersReduced(e.matches);
823
+ media.addEventListener("change", handler);
824
+ return () => media.removeEventListener("change", handler);
825
+ }, []);
826
+ return prefersReduced;
827
+ }
828
+
829
+ // components/ScrollScene.tsx
830
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
831
+ function ScrollScene() {
832
+ const rootRef = useRef2(null);
833
+ const droneRef = useRef2(null);
834
+ const narrationRef = useRef2(null);
835
+ const reducedMotion = useReducedMotion();
836
+ const [mobileViewport, setMobileViewport] = useState2(false);
837
+ const stateRef = useRef2(createInitialNarrativeState());
838
+ useEffect3(() => {
839
+ const media = window.matchMedia("(max-width: 900px)");
840
+ const update = () => setMobileViewport(media.matches);
841
+ update();
842
+ media.addEventListener("change", update);
843
+ return () => media.removeEventListener("change", update);
844
+ }, []);
845
+ useLayoutEffect(() => {
846
+ if (!rootRef.current) {
847
+ return;
848
+ }
849
+ const root = rootRef.current;
850
+ const webglDisabled = reducedMotion || mobileViewport;
851
+ const state = stateRef.current;
852
+ const updateLayers = () => {
853
+ var _a, _b, _c;
854
+ root.style.setProperty("--phone-opacity", state.phoneOpacity.toFixed(4));
855
+ root.style.setProperty("--phone-mask-open", state.phoneMaskOpen.toFixed(4));
856
+ root.style.setProperty("--phone-tilt", `${state.phoneTilt.toFixed(3)}deg`);
857
+ root.style.setProperty("--phone-scale", state.phoneScale.toFixed(4));
858
+ root.style.setProperty("--portal-bloom", state.portalBloom.toFixed(4));
859
+ root.style.setProperty("--ui-pulse", state.uiPulse.toFixed(4));
860
+ root.style.setProperty("--emerge", state.emergeT.toFixed(4));
861
+ root.style.setProperty("--flight-progress", state.flightT.toFixed(4));
862
+ root.style.setProperty("--flight-depth", state.flightDepth.toFixed(4));
863
+ root.style.setProperty("--hover", state.hover.toFixed(4));
864
+ root.style.setProperty("--drop", state.drop.toFixed(4));
865
+ root.style.setProperty("--cloud-far", state.cloudFarShift.toFixed(4));
866
+ root.style.setProperty("--cloud-near", state.cloudNearShift.toFixed(4));
867
+ root.style.setProperty("--ground-rise", state.groundRise.toFixed(4));
868
+ root.style.setProperty("--kiosk-reveal", state.kioskReveal.toFixed(4));
869
+ root.style.setProperty("--active-heading", state.activeHeading.toFixed(4));
870
+ (_a = narrationRef.current) == null ? void 0 : _a.setActiveHeading(state.activeHeading);
871
+ root.style.setProperty("--exit", state.exit.toFixed(4));
872
+ if (!webglDisabled) {
873
+ const width = root.clientWidth;
874
+ const height = root.clientHeight;
875
+ (_b = droneRef.current) == null ? void 0 : _b.resize(width, height, window.devicePixelRatio || 1);
876
+ (_c = droneRef.current) == null ? void 0 : _c.setState(state);
877
+ }
878
+ };
879
+ const timeline = createMasterTimeline(root, state, updateLayers);
880
+ const renderFromTicker = () => {
881
+ var _a;
882
+ if (!webglDisabled) {
883
+ (_a = droneRef.current) == null ? void 0 : _a.renderFrame();
884
+ }
885
+ };
886
+ gsap2.ticker.add(renderFromTicker);
887
+ updateLayers();
888
+ const onResize = () => {
889
+ var _a;
890
+ updateLayers();
891
+ (_a = timeline.scrollTrigger) == null ? void 0 : _a.refresh();
892
+ };
893
+ window.addEventListener("resize", onResize);
894
+ return () => {
895
+ var _a;
896
+ window.removeEventListener("resize", onResize);
897
+ gsap2.ticker.remove(renderFromTicker);
898
+ (_a = timeline.scrollTrigger) == null ? void 0 : _a.kill();
899
+ timeline.kill();
900
+ };
901
+ }, [mobileViewport, reducedMotion]);
902
+ const showWebgl = !reducedMotion && !mobileViewport;
903
+ return /* @__PURE__ */ jsxs2("section", { ref: rootRef, className: "zipline-scroll-scene", children: [
904
+ /* @__PURE__ */ jsx4("div", { className: "zipline-dom-layer", children: /* @__PURE__ */ jsx4(NarrationRevolver, { ref: narrationRef }) }),
905
+ /* @__PURE__ */ jsxs2(
906
+ "svg",
907
+ {
908
+ className: "zipline-svg-layer",
909
+ viewBox: "0 0 1440 900",
910
+ preserveAspectRatio: "xMidYMid slice",
911
+ "aria-hidden": "true",
912
+ children: [
913
+ /* @__PURE__ */ jsx4("defs", { children: /* @__PURE__ */ jsxs2("linearGradient", { id: "zip-sky", x1: "0%", y1: "0%", x2: "0%", y2: "100%", children: [
914
+ /* @__PURE__ */ jsx4("stop", { offset: "0%", stopColor: "#a9d4ff" }),
915
+ /* @__PURE__ */ jsx4("stop", { offset: "52%", stopColor: "#d5ebff" }),
916
+ /* @__PURE__ */ jsx4("stop", { offset: "100%", stopColor: "#f5f9ff" })
917
+ ] }) }),
918
+ /* @__PURE__ */ jsx4("rect", { className: "zipline-sky", x: "0", y: "0", width: "1440", height: "900", fill: "url(#zip-sky)" }),
919
+ /* @__PURE__ */ jsxs2("g", { className: "zipline-cloud-far", children: [
920
+ /* @__PURE__ */ jsx4("ellipse", { cx: "1100", cy: "225", rx: "360", ry: "128", fill: "rgba(255,255,255,0.35)" }),
921
+ /* @__PURE__ */ jsx4("ellipse", { cx: "540", cy: "272", rx: "300", ry: "106", fill: "rgba(255,255,255,0.3)" }),
922
+ /* @__PURE__ */ jsx4("ellipse", { cx: "880", cy: "412", rx: "460", ry: "132", fill: "rgba(255,255,255,0.28)" })
923
+ ] }),
924
+ /* @__PURE__ */ jsxs2("g", { className: "zipline-cloud-near", children: [
925
+ /* @__PURE__ */ jsx4("ellipse", { cx: "360", cy: "610", rx: "350", ry: "110", fill: "rgba(255,255,255,0.45)" }),
926
+ /* @__PURE__ */ jsx4("ellipse", { cx: "920", cy: "620", rx: "320", ry: "92", fill: "rgba(255,255,255,0.4)" }),
927
+ /* @__PURE__ */ jsx4("ellipse", { cx: "1220", cy: "540", rx: "230", ry: "74", fill: "rgba(255,255,255,0.34)" })
928
+ ] }),
929
+ /* @__PURE__ */ jsx4("rect", { className: "zipline-ground", x: "0", y: "705", width: "1440", height: "240" })
930
+ ]
931
+ }
932
+ ),
933
+ /* @__PURE__ */ jsx4("div", { className: "zipline-phone-wrapper", children: /* @__PURE__ */ jsx4(PhoneUI_default, {}) }),
934
+ showWebgl ? /* @__PURE__ */ jsx4(DroneScene, { ref: droneRef, className: "zipline-webgl-layer" }) : null,
935
+ /* @__PURE__ */ jsxs2("div", { className: "zipline-mobile-fallback", "aria-hidden": showWebgl, children: [
936
+ /* @__PURE__ */ jsx4("div", { className: "zipline-fallback-drone" }),
937
+ /* @__PURE__ */ jsx4("div", { className: "zipline-fallback-package" })
938
+ ] })
939
+ ] });
940
+ }
941
+ export {
942
+ ScrollScene
943
+ };
944
+ //# sourceMappingURL=index.js.map