-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
152 lines (137 loc) · 5.85 KB
/
index.js
File metadata and controls
152 lines (137 loc) · 5.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import {animate} from "animejs";
const styleSheetType = {
["sectionId"]: {
["meshName"]: {
rotation: (mesh) => { return [0, 0, 0]; },
position: (mesh) => { return [0, 0, 0] },
scale:(mesh)=>{return [1,1,1]},
customStyle: (mesh, params) => {
//a plain object with proper keynames is expected
return params
}
}
}
}
/**
* Defines the structure of a scroll-triggered style sheet for Three.js meshes when an html element is in-viewport .
* Each section ID maps to mesh names, and each mesh name maps to animation functions.
* @typedef {Object} ScrollStyleSheet
* @property {Object.<string, MeshStyleConfig>} [sectionId] - HTML section ID (e.g., "three-scroll-step-1")
*/
/**
* Configuration object for a specific mesh's scroll-triggered animations.
* @typedef {Object} MeshStyleConfig
* @property {function(THREE.Mesh): [number, number, number]} [position] - Returns target [x, y, z] position.
* @property {function(THREE.Mesh): [number, number, number]} [rotation] - Returns target [x, y, z] rotation (in radians or degrees, depending on your setup).
* @property {function(THREE.Mesh): [number, number, number]} [scale] - Returns target [x, y, z] scale.
* @property {function(THREE.Mesh, any): Object} [customStyle] - Custom logic to apply (e.g., material changes). Returns optional params object.
*/
/**
* Creates scroll-triggered animations for Three.js meshes based on viewport intersection.
* When an HTML section (by ID) enters the viewport, matching meshes animate to defined styles.
*
* @param {THREE.Mesh[]} [meshes=[]] - Array of Three.js meshes. Each must have `userData.name` set to match stylesheet keys.
* @param {ScrollStyleSheet} [styleSheet={}] - Declarative style mapping: sectionId → meshName → style functions.
* @param {number} [threshold=0.5] - IntersectionObserver threshold (0 to 1). Animation triggers when this % of the section is visible.
* @returns {{
* observers: IntersectionObserver[],
* elements: Element[]
* }} Object containing active observers and observed DOM elements for cleanup or debugging.
*
* @example
* const boxes = Array.from({ length: 5 }, (_, i) => {
* const mesh = new THREE.Mesh(geometry, material);
* mesh.userData.name = "box1";
* scene.add(mesh);
* return mesh;
* });
*
* const styleSheet = {
* "three-scroll-step-1": {
* box1: {
* position: (m) => [1, -1, -5],
* rotation: (m) => [Math.PI/4, 0, 0],
* scale: (m) => [2, 2, 2],
* customStyle: (m) => {
* m.material.color.set("dodgerblue");
* }
* }
* }
* };
*
* const sheet = createScrollSheet(boxes, styleSheet, 0.5);
* // Later, to cleanup:
* // sheet.observers.forEach(o => o.disconnect());
*/
export const createScrollSheet = (meshes = [], styleSheet = {}, threshold = 0.5) => {
let keys = Object.keys(styleSheet);
let elements = keys.map((identifierId) => {
let ele = document.querySelector(`#${identifierId}`);
return ele;
})?.filter((ele) => ele);
/**
* Callback for IntersectionObserver — triggers animations when section enters viewport.
* @param {IntersectionObserverEntry[]} entries
*/
const observerCallback = (entries) => {
const entry = entries?.[0];
if (!entry?.isIntersecting) return;
const sectionId = entry.target.id;
const style = styleSheet[sectionId];
if (!style) {
console.warn("Could not find style for section:", sectionId, entry);
return;
}
const meshNamesInStyle = Object.keys(style);
const targetMeshes = meshes?.filter((mesh) =>
mesh?.userData?.name && meshNamesInStyle.includes(mesh.userData.name)
);
targetMeshes?.forEach((mesh) => {
const meshStyle = style[mesh.userData.name];
const expected = {
position: meshStyle?.position?.(mesh) || [mesh.position.x, mesh.position.y, mesh.position.z],
rotation: meshStyle?.rotation?.(mesh) || [mesh.rotation.x, mesh.rotation.y, mesh.rotation.z],
scale: meshStyle?.scale?.(mesh) || [mesh.scale.x, mesh.scale.y, mesh.scale.z],
customObj: meshStyle?.customStyle?.(mesh) || {}
};
// Animate position
const pos = { x: mesh.position.x, y: mesh.position.y, z: mesh.position.z };
animate(pos, {
x: expected.position[0],
y: expected.position[1],
z: expected.position[2],
duration: 1000,
easing: "linear",
onUpdate: () => mesh.position.set(pos.x, pos.y, pos.z)
});
// Animate rotation
const rot = { x: mesh.rotation.x, y: mesh.rotation.y, z: mesh.rotation.z };
animate(rot, {
x: expected.rotation[0],
y: expected.rotation[1],
z: expected.rotation[2],
duration: 1000,
easing: "linear",
onUpdate: () => mesh.rotation.set(rot.x, rot.y, rot.z)
});
// Animate scale — FIXED: was incorrectly using rotation.set
const scl = { x: mesh.scale.x, y: mesh.scale.y, z: mesh.scale.z };
if (expected?.scale) {
animate(scl, {
x: expected.scale[0],
y: expected.scale[1],
z: expected.scale[2],
duration: 1000,
easing: "linear",
onUpdate: () => mesh.scale.set(scl.x, scl.y, scl.z) // ✅ Fixed!
});
}
});
};
const observers = elements.map((ele) => {
const observer = new IntersectionObserver(observerCallback, { threshold });
observer.observe(ele);
return observer;
});
return { observers, elements };
};