@needle-tools/engine 4.9.3-next.0facab6 → 4.10.0-beta
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 +9 -0
- package/dist/{needle-engine.bundle-DAo7BPxQ.umd.cjs → needle-engine.bundle-42AmEGfk.umd.cjs} +127 -126
- package/dist/{needle-engine.bundle-DP2gYtOQ.min.js → needle-engine.bundle-C6zhyLF5.min.js} +128 -127
- package/dist/{needle-engine.bundle-TvT7wv7z.js → needle-engine.bundle-Dj6faVbC.js} +4967 -4864
- package/dist/needle-engine.js +414 -413
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/codegen/register_types.js +2 -0
- package/lib/engine/codegen/register_types.js.map +1 -1
- package/lib/engine-components/Renderer.d.ts +2 -2
- package/lib/engine-components/Renderer.js +6 -4
- package/lib/engine-components/Renderer.js.map +1 -1
- package/lib/engine-components/codegen/components.d.ts +1 -0
- package/lib/engine-components/codegen/components.js +1 -0
- package/lib/engine-components/codegen/components.js.map +1 -1
- package/lib/engine-components/timeline/PlayableDirector.d.ts +28 -6
- package/lib/engine-components/timeline/PlayableDirector.js +60 -26
- package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
- package/lib/engine-components/timeline/TimelineModels.d.ts +3 -0
- package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
- package/lib/engine-components/timeline/TimelineTracks.d.ts +7 -0
- package/lib/engine-components/timeline/TimelineTracks.js +19 -0
- package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
- package/lib/engine-components/web/ScrollFollow.d.ts +14 -4
- package/lib/engine-components/web/ScrollFollow.js +139 -25
- package/lib/engine-components/web/ScrollFollow.js.map +1 -1
- package/package.json +2 -2
- package/src/engine/codegen/register_types.ts +2 -0
- package/src/engine-components/Renderer.ts +6 -4
- package/src/engine-components/codegen/components.ts +1 -0
- package/src/engine-components/timeline/PlayableDirector.ts +79 -34
- package/src/engine-components/timeline/TimelineModels.ts +3 -0
- package/src/engine-components/timeline/TimelineTracks.ts +22 -0
- package/src/engine-components/web/ScrollFollow.ts +177 -24
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Box3, Object3D } from "three";
|
|
2
|
+
import { element } from "three/src/nodes/TSL.js";
|
|
3
|
+
import { Context } from "../../engine/engine_context.js";
|
|
2
4
|
|
|
3
5
|
import { Mathf } from "../../engine/engine_math.js";
|
|
4
6
|
import { serializable } from "../../engine/engine_serialization.js";
|
|
5
7
|
import { getBoundingBox } from "../../engine/engine_three_utils.js";
|
|
8
|
+
import { getParam } from "../../engine/engine_utils.js";
|
|
6
9
|
import { Animation } from "../Animation.js";
|
|
7
10
|
import { Animator } from "../Animator.js";
|
|
8
11
|
import { AudioSource } from "../AudioSource.js";
|
|
@@ -11,6 +14,9 @@ import { EventList } from "../EventList.js";
|
|
|
11
14
|
import { Light } from "../Light.js";
|
|
12
15
|
import { SplineWalker } from "../splines/SplineWalker.js";
|
|
13
16
|
import { PlayableDirector } from "../timeline/PlayableDirector.js";
|
|
17
|
+
import { ScrollMarkerModel } from "../timeline/TimelineModels.js";
|
|
18
|
+
|
|
19
|
+
const debug = getParam("debugscroll");
|
|
14
20
|
|
|
15
21
|
type ScrollFollowEvent = {
|
|
16
22
|
/** Event type */
|
|
@@ -81,7 +87,8 @@ export class ScrollFollow extends Behaviour {
|
|
|
81
87
|
@serializable()
|
|
82
88
|
invert: boolean = false;
|
|
83
89
|
|
|
84
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* **Experimental - might change in future updates**
|
|
85
92
|
* If set, the scroll position will be read from the specified element instead of the window.
|
|
86
93
|
* Use a CSS selector to specify the element, e.g. `#my-scrollable-div` or `.scroll-container`.
|
|
87
94
|
* @default null
|
|
@@ -102,17 +109,23 @@ export class ScrollFollow extends Behaviour {
|
|
|
102
109
|
* Current scroll value in "pages" (0 = top of page, 1 = bottom of page)
|
|
103
110
|
*/
|
|
104
111
|
get currentValue() {
|
|
105
|
-
return this.
|
|
112
|
+
return this._current_value;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
|
-
private
|
|
109
|
-
private
|
|
110
|
-
private
|
|
115
|
+
private _current_value: number = 0;
|
|
116
|
+
private _target_value: number = 0;
|
|
117
|
+
private _appliedValue: number = -1;
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
private _scrollStart: number = 0;
|
|
121
|
+
private _scrollEnd: number = 0;
|
|
122
|
+
private _scrollValue: number = 0;
|
|
123
|
+
private _scrollContainerHeight: number = 0;
|
|
111
124
|
|
|
112
125
|
/** @internal */
|
|
113
126
|
onEnable() {
|
|
114
127
|
window.addEventListener("wheel", this.updateCurrentScrollValue, { passive: true });
|
|
115
|
-
this.
|
|
128
|
+
this._appliedValue = -1;
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
/** @internal */
|
|
@@ -125,23 +138,27 @@ export class ScrollFollow extends Behaviour {
|
|
|
125
138
|
|
|
126
139
|
this.updateCurrentScrollValue();
|
|
127
140
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
if (this._target_value >= 0) {
|
|
142
|
+
if (this.damping > 0) { // apply damping
|
|
143
|
+
this._current_value = Mathf.lerp(this._current_value, this._target_value, this.context.time.deltaTime / this.damping);
|
|
144
|
+
if (Math.abs(this._current_value - this._target_value) < 0.001) {
|
|
145
|
+
this._current_value = this._target_value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
this._current_value = this._target_value;
|
|
150
|
+
}
|
|
134
151
|
}
|
|
135
152
|
|
|
136
|
-
if (this.
|
|
137
|
-
this.
|
|
153
|
+
if (this._current_value !== this._appliedValue) {
|
|
154
|
+
this._appliedValue = this._current_value;
|
|
138
155
|
|
|
139
156
|
let defaultPrevented = false;
|
|
140
157
|
if (this.changed.listenerCount > 0) {
|
|
141
158
|
// fire change event
|
|
142
159
|
const event: ScrollFollowEvent = {
|
|
143
160
|
type: "change",
|
|
144
|
-
value: this.
|
|
161
|
+
value: this._current_value,
|
|
145
162
|
component: this,
|
|
146
163
|
preventDefault: () => { event.defaultPrevented = true; },
|
|
147
164
|
defaultPrevented: false,
|
|
@@ -153,14 +170,21 @@ export class ScrollFollow extends Behaviour {
|
|
|
153
170
|
// if not prevented apply scroll
|
|
154
171
|
if (!defaultPrevented) {
|
|
155
172
|
|
|
156
|
-
const value = this.invert ? 1 - this.
|
|
173
|
+
const value = this.invert ? 1 - this._current_value : this._current_value;
|
|
174
|
+
|
|
175
|
+
const height = this._rangeEndValue - this._rangeStartValue;
|
|
176
|
+
const pixelValue = this._rangeStartValue + value * height;
|
|
157
177
|
|
|
158
178
|
// apply scroll to target(s)
|
|
159
179
|
if (Array.isArray(this.target)) {
|
|
160
|
-
this.target.forEach(t => t &&
|
|
180
|
+
this.target.forEach(t => t && this.applyScroll(t, value));
|
|
161
181
|
}
|
|
162
182
|
else if (this.target) {
|
|
163
|
-
|
|
183
|
+
this.applyScroll(this.target, value);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (debug && this.context.time.frame % 30 === 0) {
|
|
187
|
+
console.debug(`[ScrollFollow] ${this._current_value.toFixed(5)} — ${(this._target_value * 100).toFixed(0)}%`);
|
|
164
188
|
}
|
|
165
189
|
}
|
|
166
190
|
}
|
|
@@ -168,11 +192,16 @@ export class ScrollFollow extends Behaviour {
|
|
|
168
192
|
|
|
169
193
|
private _lastSelectorValue: string | null = null;
|
|
170
194
|
private _lastSelectorElement: Element | null = null;
|
|
195
|
+
/** Top y */
|
|
196
|
+
private _rangeStartValue: number = 0;
|
|
197
|
+
/** Bottom y */
|
|
198
|
+
private _rangeEndValue: number = 0;
|
|
171
199
|
|
|
172
200
|
private updateCurrentScrollValue = () => {
|
|
173
201
|
|
|
174
202
|
switch (this.mode) {
|
|
175
203
|
case "window":
|
|
204
|
+
|
|
176
205
|
if (this.htmlSelector?.length) {
|
|
177
206
|
if (this.htmlSelector !== this._lastSelectorValue) {
|
|
178
207
|
this._lastSelectorElement = document.querySelector(this.htmlSelector);
|
|
@@ -180,27 +209,39 @@ export class ScrollFollow extends Behaviour {
|
|
|
180
209
|
}
|
|
181
210
|
if (this._lastSelectorElement) {
|
|
182
211
|
const rect = this._lastSelectorElement.getBoundingClientRect();
|
|
183
|
-
|
|
184
|
-
|
|
212
|
+
|
|
213
|
+
this._scrollStart = rect.top + window.scrollY;
|
|
214
|
+
this._scrollEnd = rect.height - window.innerHeight;
|
|
215
|
+
this._scrollValue = -rect.top;
|
|
216
|
+
this._target_value = -rect.top / (rect.height - window.innerHeight);
|
|
217
|
+
this._rangeStartValue = rect.top + window.scrollY;
|
|
218
|
+
this._rangeEndValue = this._rangeStartValue + rect.height - window.innerHeight;
|
|
219
|
+
this._scrollContainerHeight = rect.height;
|
|
185
220
|
break;
|
|
186
221
|
}
|
|
187
222
|
}
|
|
188
223
|
else {
|
|
189
|
-
this.
|
|
224
|
+
this._scrollStart = 0;
|
|
225
|
+
this._scrollEnd = window.document.body.scrollHeight - window.innerHeight;
|
|
226
|
+
this._scrollValue = window.scrollY;
|
|
227
|
+
this._target_value = this._scrollValue / (this._scrollEnd || 1);
|
|
228
|
+
this._rangeStartValue = 0;
|
|
229
|
+
this._rangeEndValue = document.body.scrollHeight;
|
|
230
|
+
this._scrollContainerHeight = window.innerHeight;
|
|
190
231
|
}
|
|
191
|
-
if (isNaN(this.target_value) || !isFinite(this.target_value)) this.target_value = 0;
|
|
192
232
|
break;
|
|
193
233
|
}
|
|
194
234
|
|
|
235
|
+
if (isNaN(this._target_value) || !isFinite(this._target_value)) this._target_value = -1;
|
|
195
236
|
}
|
|
196
237
|
|
|
197
238
|
|
|
198
|
-
private
|
|
239
|
+
private applyScroll(target: object, value: number) {
|
|
199
240
|
|
|
200
241
|
if (!target) return;
|
|
201
242
|
|
|
202
243
|
if (target instanceof PlayableDirector) {
|
|
203
|
-
target
|
|
244
|
+
this.handleTimelineTarget(target, value);
|
|
204
245
|
if (!target.isPlaying) target.evaluate();
|
|
205
246
|
}
|
|
206
247
|
else if (target instanceof Animator) {
|
|
@@ -240,4 +281,116 @@ export class ScrollFollow extends Behaviour {
|
|
|
240
281
|
}
|
|
241
282
|
}
|
|
242
283
|
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
private handleTimelineTarget(director: PlayableDirector, value: number) {
|
|
287
|
+
|
|
288
|
+
const duration = director.duration;
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
let scrollRegionStart = Infinity;
|
|
292
|
+
let scrollRegionEnd = 0;
|
|
293
|
+
markersArray.length = 0;
|
|
294
|
+
|
|
295
|
+
for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean }>("ScrollMarker")) {
|
|
296
|
+
|
|
297
|
+
// Get marker elements from DOM
|
|
298
|
+
if (marker.selector?.length && (marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (!marker.element?.parentNode))) {
|
|
299
|
+
marker.needsUpdate = false;
|
|
300
|
+
try {
|
|
301
|
+
marker.element = document.querySelector<HTMLElement>(marker.selector) || null;
|
|
302
|
+
if (debug) console.debug("ScrollMarker found on page", marker.element, marker.selector);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
marker.element = null;
|
|
306
|
+
console.error("ScrollMarker selector is not valid: " + marker.selector + "\n", error);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// skip markers without element (e.g. if the selector didn't return any element)
|
|
311
|
+
if (!marker.element) continue;
|
|
312
|
+
|
|
313
|
+
markersArray.push(marker);
|
|
314
|
+
|
|
315
|
+
const top = marker.element.offsetTop;
|
|
316
|
+
const height = marker.element.offsetHeight;
|
|
317
|
+
const bottom = top + height;
|
|
318
|
+
if (top < scrollRegionStart) {
|
|
319
|
+
scrollRegionStart = top;
|
|
320
|
+
}
|
|
321
|
+
if (bottom > scrollRegionEnd) {
|
|
322
|
+
scrollRegionEnd = bottom;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
const currentTop = this._scrollValue;
|
|
329
|
+
const currentBottom = currentTop + this._scrollContainerHeight;
|
|
330
|
+
|
|
331
|
+
weightsArray.length = 0;
|
|
332
|
+
let sum = 0;
|
|
333
|
+
|
|
334
|
+
let markerCount = 0;
|
|
335
|
+
for (const marker of markersArray) {
|
|
336
|
+
|
|
337
|
+
if (!marker.element) continue;
|
|
338
|
+
|
|
339
|
+
const top = marker.element.offsetTop;
|
|
340
|
+
const height = marker.element.offsetHeight;
|
|
341
|
+
const bottom = top + height;
|
|
342
|
+
let overlap = 0;
|
|
343
|
+
|
|
344
|
+
// TODO: if we have two marker sections where no HTML overlaps (vor example because some large section is between them) we probably want to still virtually interpolate between them slowly in that region
|
|
345
|
+
|
|
346
|
+
if (bottom < currentTop) {
|
|
347
|
+
// marker is above scroll region
|
|
348
|
+
overlap = 0;
|
|
349
|
+
}
|
|
350
|
+
else if (top > currentBottom) {
|
|
351
|
+
// marker is below scroll region
|
|
352
|
+
overlap = 0;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// calculate overlap in pixels
|
|
356
|
+
const overlapTop = Math.max(top, currentTop);
|
|
357
|
+
const overlapBottom = Math.min(bottom, currentBottom);
|
|
358
|
+
overlap = Math.max(0, overlapBottom - overlapTop);
|
|
359
|
+
// console.log(marker.element.className, overlap)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
markerCount += 1;
|
|
363
|
+
|
|
364
|
+
if (overlap > 0) {
|
|
365
|
+
weightsArray.push({ time: marker.time, weight: overlap });
|
|
366
|
+
sum += overlap;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (weightsArray.length <= 0 && markerCount <= 0) {
|
|
371
|
+
director.time = value * duration;
|
|
372
|
+
}
|
|
373
|
+
else if (weightsArray.length > 0) {
|
|
374
|
+
// normalize and calculate weighted time
|
|
375
|
+
let time = weightsArray[0].time;
|
|
376
|
+
for (const o of weightsArray) {
|
|
377
|
+
const weight = o.weight / Math.max(0.00001, sum);
|
|
378
|
+
// lerp time based on weight
|
|
379
|
+
const diff = Math.abs(o.time - time);
|
|
380
|
+
time += diff * weight;
|
|
381
|
+
}
|
|
382
|
+
director.time = time;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const weightsArray: OverlapInfo[] = [];
|
|
389
|
+
const markersArray: (ScrollMarkerModel & { element?: HTMLElement | null })[] = [];
|
|
390
|
+
|
|
391
|
+
type OverlapInfo = {
|
|
392
|
+
/** Marker time */
|
|
393
|
+
time: number,
|
|
394
|
+
/** Overlap in pixels */
|
|
395
|
+
weight: number,
|
|
243
396
|
}
|