@needle-tools/engine 4.10.0-next.55c0bf9 → 4.10.0-next.870425c
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 +8 -3
- package/README.md +2 -1
- package/components.needle.json +1 -1
- package/dist/{needle-engine.bundle-Cf5H9Zy9.umd.cjs → needle-engine.bundle-0b6rexDr.umd.cjs} +154 -141
- package/dist/{needle-engine.bundle-CUo74dPe.js → needle-engine.bundle-B5GtGvbq.js} +8329 -8050
- package/dist/needle-engine.bundle-CicGQeCY.min.js +1652 -0
- package/dist/needle-engine.js +259 -257
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/vendor-CPuBPspY.umd.cjs +1121 -0
- package/dist/vendor-DPCU8cUF.min.js +1121 -0
- package/dist/vendor-MBoqSyFm.js +16240 -0
- package/lib/engine/codegen/register_types.js +2 -0
- package/lib/engine/codegen/register_types.js.map +1 -1
- package/lib/engine/engine_camera.d.ts +7 -1
- package/lib/engine/engine_camera.fit.d.ts +1 -1
- package/lib/engine/engine_camera.fit.js +3 -30
- package/lib/engine/engine_camera.fit.js.map +1 -1
- package/lib/engine/engine_camera.js +46 -6
- package/lib/engine/engine_camera.js.map +1 -1
- package/lib/engine/engine_context.d.ts +6 -0
- package/lib/engine/engine_context.js +48 -9
- package/lib/engine/engine_context.js.map +1 -1
- package/lib/engine/engine_gizmos.d.ts +11 -10
- package/lib/engine/engine_gizmos.js +24 -10
- package/lib/engine/engine_gizmos.js.map +1 -1
- package/lib/engine/engine_license.js +1 -1
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_lightdata.d.ts +3 -3
- package/lib/engine/engine_lightdata.js +10 -10
- package/lib/engine/engine_lightdata.js.map +1 -1
- package/lib/engine/engine_physics_rapier.js +4 -0
- package/lib/engine/engine_physics_rapier.js.map +1 -1
- package/lib/engine/engine_scenelighting.d.ts +1 -1
- package/lib/engine/engine_scenelighting.js +4 -5
- package/lib/engine/engine_scenelighting.js.map +1 -1
- package/lib/engine/engine_utils.d.ts +3 -1
- package/lib/engine/engine_utils.js +11 -0
- package/lib/engine/engine_utils.js.map +1 -1
- package/lib/engine/extensions/NEEDLE_lightmaps.js +1 -1
- package/lib/engine/extensions/NEEDLE_lightmaps.js.map +1 -1
- package/lib/engine/extensions/extension_utils.js +1 -1
- package/lib/engine/extensions/extension_utils.js.map +1 -1
- package/lib/engine/webcomponents/logo-element.d.ts +1 -1
- package/lib/engine/webcomponents/logo-element.js +29 -5
- package/lib/engine/webcomponents/logo-element.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.js +4 -3
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +22 -0
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.d.ts +0 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +3 -36
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/NeedleXRController.d.ts +3 -3
- package/lib/engine/xr/NeedleXRController.js +28 -0
- package/lib/engine/xr/NeedleXRController.js.map +1 -1
- package/lib/engine-components/Camera.d.ts +1 -1
- package/lib/engine-components/Camera.js +1 -1
- package/lib/engine-components/CameraUtils.js +2 -1
- package/lib/engine-components/CameraUtils.js.map +1 -1
- package/lib/engine-components/CharacterController.d.ts +2 -2
- package/lib/engine-components/CharacterController.js +2 -2
- package/lib/engine-components/OrbitControls.d.ts +5 -2
- package/lib/engine-components/OrbitControls.js +31 -7
- package/lib/engine-components/OrbitControls.js.map +1 -1
- package/lib/engine-components/Renderer.js +6 -1
- package/lib/engine-components/Renderer.js.map +1 -1
- package/lib/engine-components/Skybox.js +22 -4
- package/lib/engine-components/Skybox.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/debug/LogStats.d.ts +1 -0
- package/lib/engine-components/debug/LogStats.js +1 -0
- package/lib/engine-components/debug/LogStats.js.map +1 -1
- package/lib/engine-components/timeline/PlayableDirector.d.ts +7 -0
- package/lib/engine-components/timeline/PlayableDirector.js +8 -1
- package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
- package/lib/engine-components/timeline/TimelineModels.d.ts +45 -2
- package/lib/engine-components/timeline/TimelineModels.js +6 -0
- package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
- package/lib/engine-components/timeline/TimelineTracks.d.ts +2 -1
- package/lib/engine-components/timeline/TimelineTracks.js +30 -25
- package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
- package/lib/engine-components/utils/LookAt.js +5 -1
- package/lib/engine-components/utils/LookAt.js.map +1 -1
- package/lib/engine-components/web/Clickthrough.js +10 -2
- package/lib/engine-components/web/Clickthrough.js.map +1 -1
- package/lib/engine-components/web/ScrollFollow.d.ts +24 -0
- package/lib/engine-components/web/ScrollFollow.js +167 -41
- package/lib/engine-components/web/ScrollFollow.js.map +1 -1
- package/lib/engine-components/web/ViewBox.d.ts +46 -0
- package/lib/engine-components/web/ViewBox.js +270 -0
- package/lib/engine-components/web/ViewBox.js.map +1 -0
- package/lib/engine-components/web/index.d.ts +1 -0
- package/lib/engine-components/web/index.js +1 -0
- package/lib/engine-components/web/index.js.map +1 -1
- package/lib/engine-components/webxr/WebARSessionRoot.js +1 -0
- package/lib/engine-components/webxr/WebARSessionRoot.js.map +1 -1
- package/lib/engine-components-experimental/Presentation.d.ts +1 -0
- package/lib/engine-components-experimental/Presentation.js +1 -0
- package/lib/engine-components-experimental/Presentation.js.map +1 -1
- package/package.json +2 -1
- package/src/engine/codegen/register_types.ts +2 -0
- package/src/engine/engine_camera.fit.ts +2 -32
- package/src/engine/engine_camera.ts +61 -9
- package/src/engine/engine_context.ts +50 -10
- package/src/engine/engine_gizmos.ts +37 -23
- package/src/engine/engine_license.ts +1 -1
- package/src/engine/engine_lightdata.ts +11 -11
- package/src/engine/engine_physics_rapier.ts +3 -0
- package/src/engine/engine_scenelighting.ts +5 -6
- package/src/engine/engine_utils.ts +12 -0
- package/src/engine/extensions/NEEDLE_lightmaps.ts +1 -1
- package/src/engine/extensions/extension_utils.ts +1 -1
- package/src/engine/webcomponents/logo-element.ts +29 -4
- package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -3
- package/src/engine/webcomponents/needle-engine.loading.ts +32 -32
- package/src/engine/webcomponents/needle-engine.ts +33 -6
- package/src/engine/xr/NeedleXRController.ts +36 -4
- package/src/engine-components/Camera.ts +1 -1
- package/src/engine-components/CameraUtils.ts +1 -1
- package/src/engine-components/CharacterController.ts +2 -2
- package/src/engine-components/OrbitControls.ts +41 -2
- package/src/engine-components/Renderer.ts +6 -1
- package/src/engine-components/Skybox.ts +26 -7
- package/src/engine-components/codegen/components.ts +1 -0
- package/src/engine-components/debug/LogStats.ts +1 -0
- package/src/engine-components/timeline/PlayableDirector.ts +10 -1
- package/src/engine-components/timeline/TimelineModels.ts +45 -3
- package/src/engine-components/timeline/TimelineTracks.ts +30 -25
- package/src/engine-components/utils/LookAt.ts +5 -1
- package/src/engine-components/web/Clickthrough.ts +11 -2
- package/src/engine-components/web/ScrollFollow.ts +200 -48
- package/src/engine-components/web/ViewBox.ts +292 -0
- package/src/engine-components/web/index.ts +2 -1
- package/src/engine-components/webxr/WebARSessionRoot.ts +1 -0
- package/src/engine-components-experimental/Presentation.ts +1 -0
- package/dist/needle-engine.bundle-DlAVTipB.min.js +0 -1639
- package/dist/vendor-D0Yvltn9.umd.cjs +0 -1121
- package/dist/vendor-DU8tJyl_.js +0 -14366
- package/dist/vendor-JyrX4DVM.min.js +0 -1121
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { NEPointerEvent } from "../../engine/engine_input.js";
|
|
2
2
|
import { onStart } from "../../engine/engine_lifecycle_api.js";
|
|
3
|
+
import { addAttributeChangeCallback } from "../../engine/engine_utils.js";
|
|
3
4
|
import { Behaviour } from "../Component.js";
|
|
4
5
|
|
|
5
6
|
// Automatically add ClickThrough component if "clickthrough" attribute is present on the needle-engine element
|
|
6
7
|
onStart(ctx => {
|
|
7
8
|
const attribute = ctx.domElement.getAttribute("clickthrough");
|
|
8
|
-
if (attribute
|
|
9
|
-
ctx.scene.addComponent(ClickThrough);
|
|
9
|
+
if (clickthroughEnabled(attribute)) {
|
|
10
|
+
const comp = ctx.scene.addComponent(ClickThrough);
|
|
11
|
+
addAttributeChangeCallback(ctx.domElement, "clickthrough", () => {
|
|
12
|
+
const attribute = ctx.domElement.getAttribute("clickthrough");
|
|
13
|
+
comp.enabled = clickthroughEnabled(attribute);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function clickthroughEnabled(val: string | null) {
|
|
18
|
+
return val !== null && val !== "0" && val !== "false";
|
|
10
19
|
}
|
|
11
20
|
});
|
|
12
21
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
// For firefox ViewTimeline support
|
|
2
|
+
import "scroll-timeline-polyfill/dist/scroll-timeline.js";
|
|
3
|
+
|
|
1
4
|
import { Box3, Object3D } from "three";
|
|
2
|
-
import { element } from "three/src/nodes/TSL.js";
|
|
3
|
-
import { Context } from "../../engine/engine_context.js";
|
|
4
5
|
|
|
6
|
+
import { isDevEnvironment } from "../../engine/debug/debug.js";
|
|
5
7
|
import { Mathf } from "../../engine/engine_math.js";
|
|
6
8
|
import { serializable } from "../../engine/engine_serialization.js";
|
|
7
|
-
import { getBoundingBox } from "../../engine/engine_three_utils.js";
|
|
9
|
+
import { getBoundingBox, setVisibleInCustomShadowRendering } from "../../engine/engine_three_utils.js";
|
|
8
10
|
import { getParam } from "../../engine/engine_utils.js";
|
|
9
11
|
import { Animation } from "../Animation.js";
|
|
10
12
|
import { Animator } from "../Animator.js";
|
|
@@ -38,6 +40,7 @@ type ScrollFollowEvent = {
|
|
|
38
40
|
*
|
|
39
41
|
* @link Example at https://scrollytelling-2-z23hmxby7c6x-u30ld.needle.run/
|
|
40
42
|
* @link Template at https://github.com/needle-engine/scrollytelling-template
|
|
43
|
+
* @link [Scrollytelling Bike Demo](https://scrollytelling-bike-z23hmxb2gnu5a.needle.run/)
|
|
41
44
|
*
|
|
42
45
|
* ## How to use with an Animator
|
|
43
46
|
* 1. Create an Animator component and set up a float parameter named "scroll".
|
|
@@ -150,7 +153,8 @@ export class ScrollFollow extends Behaviour {
|
|
|
150
153
|
}
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
if (this._current_value !== this._appliedValue)
|
|
156
|
+
// if (this._current_value !== this._appliedValue)
|
|
157
|
+
{
|
|
154
158
|
this._appliedValue = this._current_value;
|
|
155
159
|
|
|
156
160
|
let defaultPrevented = false;
|
|
@@ -172,8 +176,8 @@ export class ScrollFollow extends Behaviour {
|
|
|
172
176
|
|
|
173
177
|
const value = this.invert ? 1 - this._current_value : this._current_value;
|
|
174
178
|
|
|
175
|
-
const height = this._rangeEndValue - this._rangeStartValue;
|
|
176
|
-
const pixelValue = this._rangeStartValue + value * height;
|
|
179
|
+
// const height = this._rangeEndValue - this._rangeStartValue;
|
|
180
|
+
// const pixelValue = this._rangeStartValue + value * height;
|
|
177
181
|
|
|
178
182
|
// apply scroll to target(s)
|
|
179
183
|
if (Array.isArray(this.target)) {
|
|
@@ -184,7 +188,7 @@ export class ScrollFollow extends Behaviour {
|
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
if (debug && this.context.time.frame % 30 === 0) {
|
|
187
|
-
console.debug(`[ScrollFollow] ${this._current_value.toFixed(5)} — ${(this._target_value * 100).toFixed(0)}
|
|
191
|
+
console.debug(`[ScrollFollow] ${this._current_value.toFixed(5)} — ${(this._target_value * 100).toFixed(0)}%, targets [${Array.isArray(this.target) ? this.target.length : 1}]`);
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
194
|
}
|
|
@@ -242,7 +246,8 @@ export class ScrollFollow extends Behaviour {
|
|
|
242
246
|
|
|
243
247
|
if (target instanceof PlayableDirector) {
|
|
244
248
|
this.handleTimelineTarget(target, value);
|
|
245
|
-
if (
|
|
249
|
+
if (target.isPlaying) target.pause();
|
|
250
|
+
target.evaluate();
|
|
246
251
|
}
|
|
247
252
|
else if (target instanceof Animator) {
|
|
248
253
|
target.setFloat("scroll", value);
|
|
@@ -292,18 +297,37 @@ export class ScrollFollow extends Behaviour {
|
|
|
292
297
|
let scrollRegionEnd = 0;
|
|
293
298
|
markersArray.length = 0;
|
|
294
299
|
|
|
295
|
-
|
|
300
|
+
// querySelectorResults.length = 0;
|
|
301
|
+
let markerIndex = 0;
|
|
302
|
+
|
|
303
|
+
// https://scroll-driven-animations.style/tools/view-timeline/ranges
|
|
304
|
+
for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean, timeline?: ViewTimeline }>("ScrollMarker")) {
|
|
305
|
+
|
|
306
|
+
const index = markerIndex++;
|
|
296
307
|
|
|
297
308
|
// Get marker elements from DOM
|
|
298
|
-
if (
|
|
309
|
+
if ((marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (marker.element && !marker.element?.parentNode))) {
|
|
299
310
|
marker.needsUpdate = false;
|
|
300
311
|
try {
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
// TODO: with this it's currently not possible to remap markers from HTML. For example if I have two sections and I want to now use the marker["center"] multiple times to stay at that marker for a longer time
|
|
313
|
+
marker.element = tryGetElementsForSelector(index, marker.name) as HTMLElement | null;
|
|
314
|
+
if (debug) console.debug(`ScrollMarker #${index} "${marker.name}" (${marker.time.toFixed(2)}) found`, marker.element);
|
|
315
|
+
if (!marker.element) {
|
|
316
|
+
marker.timeline = undefined;
|
|
317
|
+
if (debug || isDevEnvironment()) console.warn(`No HTML element found for ScrollMarker: ${marker.name} (index ${index})`);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
/** @ts-ignore */
|
|
322
|
+
marker.timeline = new ViewTimeline({
|
|
323
|
+
subject: marker.element,
|
|
324
|
+
axis: 'block', // https://drafts.csswg.org/scroll-animations/#scroll-notation
|
|
325
|
+
});
|
|
326
|
+
}
|
|
303
327
|
}
|
|
304
328
|
catch (error) {
|
|
305
329
|
marker.element = null;
|
|
306
|
-
console.error("ScrollMarker selector is not valid: " + marker.
|
|
330
|
+
console.error("ScrollMarker selector is not valid: " + marker.name + "\n", error);
|
|
307
331
|
}
|
|
308
332
|
}
|
|
309
333
|
|
|
@@ -330,41 +354,75 @@ export class ScrollFollow extends Behaviour {
|
|
|
330
354
|
|
|
331
355
|
weightsArray.length = 0;
|
|
332
356
|
let sum = 0;
|
|
357
|
+
const oneFrameTime = 1 / 60;
|
|
333
358
|
|
|
359
|
+
// We keep a separate count here in case there are some markers that could not be resolved so point to *invalid* elements - the timeline should fallback to 0-1 scroll behaviour then
|
|
334
360
|
let markerCount = 0;
|
|
335
|
-
for (
|
|
336
|
-
|
|
361
|
+
for (let i = 0; i < markersArray.length; i++) {
|
|
362
|
+
const marker = markersArray[i];
|
|
337
363
|
if (!marker.element) continue;
|
|
364
|
+
const nextMarker = markersArray[i + 1];
|
|
338
365
|
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
}
|
|
366
|
+
const nextTime = nextMarker
|
|
367
|
+
? (nextMarker.time - oneFrameTime)
|
|
368
|
+
: duration;
|
|
361
369
|
|
|
362
370
|
markerCount += 1;
|
|
363
371
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
372
|
+
const timeline = marker.timeline;
|
|
373
|
+
if (timeline) {
|
|
374
|
+
const time01 = calculateTimelinePositionNormalized(timeline);
|
|
375
|
+
// remap 0-1 to 0 - 1 - 0 (full weight at center)
|
|
376
|
+
const weight = 1 - Math.abs(time01 - 0.5) * 2;
|
|
377
|
+
const name = marker.name || `marker${i}`;
|
|
378
|
+
if (time01 > 0 && time01 <= 1) {
|
|
379
|
+
const lerpTime = marker.time + (nextTime - marker.time) * time01;
|
|
380
|
+
weightsArray.push({ name, time: lerpTime, weight: weight });
|
|
381
|
+
sum += weight;
|
|
382
|
+
}
|
|
383
|
+
// Before the first marker is reached
|
|
384
|
+
else if (i === 0 && time01 <= 0) {
|
|
385
|
+
weightsArray.push({ name, time: 0, weight: 1 });
|
|
386
|
+
sum += 1;
|
|
387
|
+
}
|
|
388
|
+
// After the last marker is reached
|
|
389
|
+
else if (i === markersArray.length - 1 && time01 >= 1) {
|
|
390
|
+
weightsArray.push({ name, time: duration, weight: 1 });
|
|
391
|
+
sum += 1;
|
|
392
|
+
}
|
|
367
393
|
}
|
|
394
|
+
continue;
|
|
395
|
+
// if(this.context.time.frame % 10 === 0) console.log(marker.element?.className, timeline, calculateTimelinePositionNormalized(timeline!));
|
|
396
|
+
|
|
397
|
+
// const top = marker.element.offsetTop - this._scrollContainerHeight;
|
|
398
|
+
// const height = marker.element.offsetHeight + this._scrollContainerHeight;
|
|
399
|
+
// const bottom = top + height;
|
|
400
|
+
// let overlap = 0;
|
|
401
|
+
|
|
402
|
+
// // 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
|
|
403
|
+
|
|
404
|
+
// if (bottom < currentTop) {
|
|
405
|
+
// // marker is above scroll region
|
|
406
|
+
// overlap = 0;
|
|
407
|
+
// }
|
|
408
|
+
// else if (top > currentBottom) {
|
|
409
|
+
// // marker is below scroll region
|
|
410
|
+
// overlap = 0;
|
|
411
|
+
// }
|
|
412
|
+
// else {
|
|
413
|
+
// // calculate overlap in pixels
|
|
414
|
+
// const overlapTop = Math.max(top, currentTop);
|
|
415
|
+
// const overlapBottom = Math.min(bottom, currentBottom);
|
|
416
|
+
// const height = Math.max(1, currentBottom - currentTop);
|
|
417
|
+
// overlap = Math.max(0, overlapBottom - overlapTop);
|
|
418
|
+
// }
|
|
419
|
+
|
|
420
|
+
// // if(this.context.time.frame % 20 === 0) console.log(overlap)
|
|
421
|
+
|
|
422
|
+
// if (overlap > 0) {
|
|
423
|
+
// weightsArray.push({ time: marker.time, weight: overlap });
|
|
424
|
+
// sum += overlap;
|
|
425
|
+
// }
|
|
368
426
|
}
|
|
369
427
|
|
|
370
428
|
if (weightsArray.length <= 0 && markerCount <= 0) {
|
|
@@ -372,25 +430,119 @@ export class ScrollFollow extends Behaviour {
|
|
|
372
430
|
}
|
|
373
431
|
else if (weightsArray.length > 0) {
|
|
374
432
|
// normalize and calculate weighted time
|
|
375
|
-
let time = weightsArray[0].time;
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
433
|
+
let time = weightsArray[0].time; // fallback to first time
|
|
434
|
+
if (weightsArray.length > 1) {
|
|
435
|
+
for (const entry of weightsArray) {
|
|
436
|
+
const weight = entry.weight / Math.max(0.00001, sum);
|
|
437
|
+
// console.log(weight.toFixed(2))
|
|
438
|
+
// lerp time based on weight
|
|
439
|
+
const diff = Math.abs(entry.time - time);
|
|
440
|
+
time += diff * weight;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (this.damping <= 0) {
|
|
444
|
+
director.time = time;
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
director.time = Mathf.lerp(director.time, time, this.context.time.deltaTime / this.damping);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (debug && this.context.time.frame % 30 === 0) {
|
|
451
|
+
console.log(`[ScrollFollow ] Timeline ${director.name}: ${time.toFixed(3)}`, weightsArray.map(w => `[${w.name} ${(w.weight * 100).toFixed(0)}%]`).join(", "));
|
|
381
452
|
}
|
|
382
|
-
director.time = time;
|
|
383
453
|
}
|
|
384
454
|
}
|
|
385
455
|
|
|
386
456
|
}
|
|
387
457
|
|
|
458
|
+
|
|
459
|
+
|
|
388
460
|
const weightsArray: OverlapInfo[] = [];
|
|
389
|
-
const markersArray:
|
|
461
|
+
const markersArray: Array<ScrollMarkerModel & {
|
|
462
|
+
element?: HTMLElement | null,
|
|
463
|
+
timeline?: ViewTimeline,
|
|
464
|
+
}> = [];
|
|
390
465
|
|
|
391
466
|
type OverlapInfo = {
|
|
467
|
+
name: string,
|
|
392
468
|
/** Marker time */
|
|
393
469
|
time: number,
|
|
394
470
|
/** Overlap in pixels */
|
|
395
471
|
weight: number,
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
// type SelectorCache = {
|
|
476
|
+
// /** The selector used to query the *elements */
|
|
477
|
+
// selector: string,
|
|
478
|
+
// elements: Element[] | null,
|
|
479
|
+
// usedElementCount: number,
|
|
480
|
+
// }
|
|
481
|
+
// const querySelectorResults: Array<SelectorCache> = [];
|
|
482
|
+
|
|
483
|
+
const needleScrollMarkerIndexCache = new Map<number, Element | null>();
|
|
484
|
+
const needleScrollMarkerNameCache = new Map<string, Element | null>();
|
|
485
|
+
let needsScrollMarkerRefresh = true;
|
|
486
|
+
|
|
487
|
+
function tryGetElementsForSelector(index: number, name: string, _cycle: number = 0): Element | null {
|
|
488
|
+
|
|
489
|
+
if (!needsScrollMarkerRefresh) {
|
|
490
|
+
if (name?.length) {
|
|
491
|
+
const element = needleScrollMarkerNameCache.get(name) || null;
|
|
492
|
+
if (element) return element;
|
|
493
|
+
// const isNumber = !isNaN(Number(name));
|
|
494
|
+
// if (!isNumber) {
|
|
495
|
+
// }
|
|
496
|
+
}
|
|
497
|
+
const element = needleScrollMarkerIndexCache.get(index) || null;
|
|
498
|
+
const value = element?.getAttribute("data-timeline-marker");
|
|
499
|
+
// if (value?.length) {
|
|
500
|
+
// if (cycle === 0) {
|
|
501
|
+
// // if the HTML marker we found by index does define a different marker name we try to find the correct HTML element by name
|
|
502
|
+
// return tryGetElementsForSelector(index, value, 1);
|
|
503
|
+
// }
|
|
504
|
+
// if (isDevEnvironment()) console.warn(`ScrollMarker name mismatch: expected "${name}", got "${value}"`);
|
|
505
|
+
// }
|
|
506
|
+
return element;
|
|
507
|
+
}
|
|
508
|
+
needsScrollMarkerRefresh = false;
|
|
509
|
+
needleScrollMarkerIndexCache.clear();
|
|
510
|
+
const markers = document.querySelectorAll(`[data-timeline-marker]`);
|
|
511
|
+
markers.forEach((m, i) => {
|
|
512
|
+
needleScrollMarkerIndexCache.set(i, m);
|
|
513
|
+
const name = m.getAttribute("data-timeline-marker");
|
|
514
|
+
if (name?.length) needleScrollMarkerNameCache.set(name, m);
|
|
515
|
+
});
|
|
516
|
+
needsScrollMarkerRefresh = false;
|
|
517
|
+
return tryGetElementsForSelector(index, name);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
// #region ScrollTimeline
|
|
522
|
+
|
|
523
|
+
function calculateTimelinePositionNormalized(timeline: ViewTimeline) {
|
|
524
|
+
if (!timeline.source) return 0;
|
|
525
|
+
const currentTime = timeline.currentTime;
|
|
526
|
+
const duration = timeline.duration;
|
|
527
|
+
let durationValue = 1;
|
|
528
|
+
if (duration.unit === "seconds") {
|
|
529
|
+
durationValue = duration.value;
|
|
530
|
+
}
|
|
531
|
+
else if (duration.unit === "percent") {
|
|
532
|
+
durationValue = duration.value;
|
|
533
|
+
}
|
|
534
|
+
const t01 = currentTime.unit === "seconds" ? (currentTime.value / durationValue) : (currentTime.value / 100);
|
|
535
|
+
return t01;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
declare global {
|
|
540
|
+
interface ViewTimeline {
|
|
541
|
+
axis: 'block' | 'inline';
|
|
542
|
+
currentTime: { unit: 'seconds' | 'percent', value: number };
|
|
543
|
+
duration: { unit: 'seconds' | 'percent', value: number };
|
|
544
|
+
source: Element | null;
|
|
545
|
+
startOffset: { unit: 'px', value: number };
|
|
546
|
+
endOffset: { unit: 'px', value: number };
|
|
547
|
+
}
|
|
396
548
|
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { Camera, Matrix4, PerspectiveCamera, Quaternion, Scene, Vector2, Vector3 } from "three";
|
|
2
|
+
|
|
3
|
+
import { isDevEnvironment } from "../../engine/debug/debug.js";
|
|
4
|
+
import { Gizmos } from "../../engine/engine_gizmos.js";
|
|
5
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
|
6
|
+
import { getTempVector } from "../../engine/engine_three_utils.js";
|
|
7
|
+
import { registerType } from "../../engine/engine_typestore.js";
|
|
8
|
+
import { getParam } from "../../engine/engine_utils.js";
|
|
9
|
+
import { RGBAColor } from "../../engine/js-extensions/RGBAColor.js";
|
|
10
|
+
import { Behaviour } from "../Component.js";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const debugParam = getParam("debugviewbox");
|
|
14
|
+
const disabledGizmoColor = new RGBAColor(.5, .5, .5, .5);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* This component can be used to automatically fit a certain box area into the camera view - no matter your screen size or aspect ratio.
|
|
18
|
+
*
|
|
19
|
+
* Add the ViewBox to an object into your scene
|
|
20
|
+
*
|
|
21
|
+
* @link [Example on needle.run](https://viewbox-demo-z23hmxbz2gkayo-z1nyzm6.needle.run/)
|
|
22
|
+
* @link [Scrollytelling Demo using animated Viewbox](https://scrollytelling-bike-z23hmxb2gnu5a.needle.run/)
|
|
23
|
+
* @link [Example on Stackblitz](https://stackblitz.com/edit/needle-engine-view-box-example)
|
|
24
|
+
*
|
|
25
|
+
* @example Add a Viewbox component to an object in your scene
|
|
26
|
+
* ```ts
|
|
27
|
+
const viewBox = new Object3D();
|
|
28
|
+
viewBox.scale.set(0, 0, 0);
|
|
29
|
+
viewBox.addComponent(ViewBox, { debug: true });
|
|
30
|
+
scene.add(viewBox);
|
|
31
|
+
* ```
|
|
32
|
+
|
|
33
|
+
* @category Camera
|
|
34
|
+
* @group Components
|
|
35
|
+
* @component
|
|
36
|
+
*/
|
|
37
|
+
@registerType
|
|
38
|
+
export class ViewBox extends Behaviour {
|
|
39
|
+
|
|
40
|
+
static readonly instances: ViewBox[] = [];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The reference field of view is used to calculate the box size. This should usually be the same as your camera's fov.
|
|
44
|
+
* @default -1 (meaning it will use the camera fov on the first frame)
|
|
45
|
+
*/
|
|
46
|
+
@serializable()
|
|
47
|
+
referenceFieldOfView: number = -1;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Enable debug logs and rendering for this component instance
|
|
51
|
+
*/
|
|
52
|
+
@serializable()
|
|
53
|
+
debug: boolean = false;
|
|
54
|
+
|
|
55
|
+
onEnable(): void {
|
|
56
|
+
if (debugParam || this.debug || isDevEnvironment()) console.debug("[ViewBox] Using camera fov:", this.referenceFieldOfView);
|
|
57
|
+
// register instance
|
|
58
|
+
ViewBox.instances.push(this);
|
|
59
|
+
|
|
60
|
+
this.removeUpdateCallback();
|
|
61
|
+
this.context.pre_render_callbacks.push(this.internalUpdate);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onDisable(): void {
|
|
65
|
+
if (debugParam || this.debug) console.debug("[ViewBox] Disabled");
|
|
66
|
+
// unregister instance
|
|
67
|
+
const idx = ViewBox.instances.indexOf(this);
|
|
68
|
+
if (idx !== -1) ViewBox.instances.splice(idx, 1);
|
|
69
|
+
this._projectedBoxElement?.remove();
|
|
70
|
+
this.removeUpdateCallback();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private removeUpdateCallback() {
|
|
74
|
+
// remove prerender callback
|
|
75
|
+
const cbIdx = this.context.pre_render_callbacks.indexOf(this.internalUpdate);
|
|
76
|
+
if (cbIdx !== -1) this.context.pre_render_callbacks.splice(cbIdx, 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private static readonly _tempProjectionMatrix: Matrix4 = new Matrix4();
|
|
80
|
+
private static readonly _tempProjectionMatrixInverse: Matrix4 = new Matrix4();
|
|
81
|
+
|
|
82
|
+
private internalUpdate = () => {
|
|
83
|
+
if (this.context.isInXR) return;
|
|
84
|
+
if (this.destroyed || !this.activeAndEnabled) return;
|
|
85
|
+
const isActive = ViewBox.instances[ViewBox.instances.length - 1] === this;
|
|
86
|
+
if (!isActive) {
|
|
87
|
+
if (debugParam || this.debug) {
|
|
88
|
+
Gizmos.DrawWireBox(this.gameObject.worldPosition, this.gameObject.worldScale, disabledGizmoColor);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (debugParam || this.debug) Gizmos.DrawWireBox(this.gameObject.worldPosition, this.gameObject.worldScale, 0xdddd00, 0, true, this.gameObject.worldQuaternion);
|
|
93
|
+
|
|
94
|
+
// calculate box size to fit the camera frustrum size at the current position (just scale)
|
|
95
|
+
const camera = this.context.mainCamera;
|
|
96
|
+
if (!camera) return;
|
|
97
|
+
if (!(camera instanceof PerspectiveCamera)) {
|
|
98
|
+
// TODO: support orthographic camera
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (this.referenceFieldOfView === undefined || this.referenceFieldOfView === -1) {
|
|
103
|
+
this.referenceFieldOfView = camera.fov;
|
|
104
|
+
console.debug("[ViewBox] No referenceFieldOfView set, using camera fov:", this.referenceFieldOfView);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.referenceFieldOfView === undefined || this.referenceFieldOfView <= 0) {
|
|
108
|
+
if (debugParam || this.debug) console.warn("[ViewBox] No valid referenceFieldOfView set, cannot adjust box size:", this.referenceFieldOfView);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const domWidth = this.context.domWidth;
|
|
113
|
+
const domHeight = this.context.domHeight;
|
|
114
|
+
|
|
115
|
+
let rectWidth = domWidth;
|
|
116
|
+
let rectHeight = domHeight;
|
|
117
|
+
let diffWidth = 1;
|
|
118
|
+
let diffHeight = 1;
|
|
119
|
+
// use focus rect if available
|
|
120
|
+
const focusRectSize = this.context.focusRectSize;
|
|
121
|
+
if (focusRectSize) {
|
|
122
|
+
rectWidth = focusRectSize.width;
|
|
123
|
+
rectHeight = focusRectSize.height;
|
|
124
|
+
diffWidth = domWidth / rectWidth;
|
|
125
|
+
diffHeight = domHeight / rectHeight;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
// Copy the projection matrix and restore values so we can reset it later
|
|
130
|
+
ViewBox._tempProjectionMatrix.copy(camera.projectionMatrix);
|
|
131
|
+
ViewBox._tempProjectionMatrixInverse.copy(camera.projectionMatrixInverse);
|
|
132
|
+
const view = camera.view;
|
|
133
|
+
const zoom = camera.zoom;
|
|
134
|
+
const aspect = camera.aspect;
|
|
135
|
+
const fov = camera.fov;
|
|
136
|
+
// Set values to default so we can calculate the box size correctly
|
|
137
|
+
camera.view = null;
|
|
138
|
+
camera.zoom = 1;
|
|
139
|
+
camera.fov = this.referenceFieldOfView;
|
|
140
|
+
camera.updateProjectionMatrix();
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
const boxPosition = this.gameObject.worldPosition;
|
|
144
|
+
const boxScale = this.gameObject.worldScale;
|
|
145
|
+
|
|
146
|
+
const cameraPosition = camera.worldPosition;
|
|
147
|
+
const distance = cameraPosition.distanceTo(boxPosition);
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
// #region camera fixes
|
|
151
|
+
// If the camera is inside the box, move it out
|
|
152
|
+
const boxSizeMax = Math.max(boxScale.x, boxScale.y, boxScale.z);
|
|
153
|
+
const direction = getTempVector(cameraPosition).sub(boxPosition);
|
|
154
|
+
if (distance < boxSizeMax) {
|
|
155
|
+
// move camera out of bounds
|
|
156
|
+
if (this.debug || debugParam) console.warn("[ViewBox] Moving camera out of bounds", distance, "<", boxSizeMax);
|
|
157
|
+
const positionDirection = getTempVector(direction);
|
|
158
|
+
positionDirection.y *= .00000001; // stay on horizontal plane mostly
|
|
159
|
+
positionDirection.normalize();
|
|
160
|
+
const lengthToMove = (boxSizeMax - distance);
|
|
161
|
+
const newPosition = cameraPosition.add(positionDirection.multiplyScalar(lengthToMove));
|
|
162
|
+
camera.worldPosition = newPosition.lerp(cameraPosition, 1 - this.context.time.deltaTime);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Ensure the camera looks at the ViewBox
|
|
166
|
+
// TOOD: smooth lookat over multiple frames if we have multiple viewboxes
|
|
167
|
+
// const dot = direction.normalize().dot(camera.worldForward);
|
|
168
|
+
// if (dot < .9) {
|
|
169
|
+
// console.log(dot);
|
|
170
|
+
// const targetRotation = direction;
|
|
171
|
+
// const rotation = getTempQuaternion();
|
|
172
|
+
// rotation.setFromUnitVectors(camera.worldForward.multiplyScalar(-1), targetRotation);
|
|
173
|
+
// camera.worldQuaternion = rotation;
|
|
174
|
+
// camera.updateMatrixWorld();
|
|
175
|
+
// }
|
|
176
|
+
const boxPositionInCameraSpace = getTempVector(boxPosition);
|
|
177
|
+
camera.worldToLocal(boxPositionInCameraSpace);
|
|
178
|
+
camera.lookAt(boxPosition);
|
|
179
|
+
camera.updateMatrixWorld();
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
// #region calculate fit
|
|
183
|
+
const vFOV = this.referenceFieldOfView * Math.PI / 180; // convert vertical fov to radians
|
|
184
|
+
const height = 2 * Math.tan(vFOV / 2) * distance; // visible height
|
|
185
|
+
const width = height * camera.aspect; // visible width
|
|
186
|
+
|
|
187
|
+
const projectedBox = this.projectBoxIntoCamera(camera, 1);
|
|
188
|
+
// return
|
|
189
|
+
const boxWidth = (projectedBox.maxX - projectedBox.minX);
|
|
190
|
+
const boxHeight = (projectedBox.maxY - projectedBox.minY);
|
|
191
|
+
|
|
192
|
+
const scale = this.fit(
|
|
193
|
+
boxWidth * camera.aspect,
|
|
194
|
+
boxHeight,
|
|
195
|
+
width / diffWidth,
|
|
196
|
+
height / diffHeight
|
|
197
|
+
);
|
|
198
|
+
// console.log({ scale, width, height, boxWidth: boxWidth * camera.aspect, boxHeight, diffWidth, diffHeight, aspect: camera.aspect, distance })
|
|
199
|
+
// this.context.focusRectSettings.zoom = 1.39;
|
|
200
|
+
// if (!this.context.focusRect) this.context.setCameraFocusRect(this.context.domElement);
|
|
201
|
+
// return
|
|
202
|
+
const vec = getTempVector(boxPosition);
|
|
203
|
+
vec.project(camera);
|
|
204
|
+
this.context.focusRectSettings.offsetX = vec.x;
|
|
205
|
+
this.context.focusRectSettings.offsetY = vec.y;
|
|
206
|
+
this.context.focusRectSettings.zoom = scale / (height * .5);
|
|
207
|
+
// if we don't have a focus rect yet, set it to the dom element
|
|
208
|
+
if (!this.context.focusRect) this.context.setCameraFocusRect(this.context.domElement);
|
|
209
|
+
|
|
210
|
+
// Reset values
|
|
211
|
+
camera.view = view;
|
|
212
|
+
camera.zoom = zoom;
|
|
213
|
+
camera.aspect = aspect;
|
|
214
|
+
camera.fov = fov;
|
|
215
|
+
camera.projectionMatrix.copy(ViewBox._tempProjectionMatrix);
|
|
216
|
+
camera.projectionMatrixInverse.copy(ViewBox._tempProjectionMatrixInverse);
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
// BACKLOG: some code for box scale of an object (different component)
|
|
220
|
+
// this.gameObject.worldScale = getTempVector(width, height, worldscale.z);
|
|
221
|
+
// this.gameObject.scale.multiplyScalar(.98)
|
|
222
|
+
// const minscale = Math.min(width, height);
|
|
223
|
+
// console.log(width, height);
|
|
224
|
+
// this.gameObject.worldScale = getTempVector(scale, scale, scale);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Cover fit
|
|
230
|
+
*/
|
|
231
|
+
private fit(width1: number, height1: number, width2: number, height2: number) {
|
|
232
|
+
const scaleX = width2 / width1;
|
|
233
|
+
const scaleY = height2 / height1;
|
|
234
|
+
return Math.min(scaleX, scaleY);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
private projectBoxIntoCamera(camera: Camera, _factor: number) {
|
|
240
|
+
const factor = .5 * _factor;
|
|
241
|
+
|
|
242
|
+
const corners = [
|
|
243
|
+
getTempVector(-factor, -factor, -factor),
|
|
244
|
+
getTempVector(factor, -factor, -factor),
|
|
245
|
+
getTempVector(-factor, factor, -factor),
|
|
246
|
+
getTempVector(factor, factor, -factor),
|
|
247
|
+
getTempVector(-factor, -factor, factor),
|
|
248
|
+
getTempVector(factor, -factor, factor),
|
|
249
|
+
getTempVector(-factor, factor, factor),
|
|
250
|
+
getTempVector(factor, factor, factor),
|
|
251
|
+
];
|
|
252
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
253
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
254
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
255
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
256
|
+
for (let i = 0; i < corners.length; i++) {
|
|
257
|
+
const c = corners[i];
|
|
258
|
+
c.applyMatrix4(this.gameObject.matrixWorld);
|
|
259
|
+
c.project(camera);
|
|
260
|
+
if (c.x < minX) minX = c.x;
|
|
261
|
+
if (c.x > maxX) maxX = c.x;
|
|
262
|
+
if (c.y < minY) minY = c.y;
|
|
263
|
+
if (c.y > maxY) maxY = c.y;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (debugParam) {
|
|
267
|
+
if (!this._projectedBoxElement) {
|
|
268
|
+
this._projectedBoxElement = document.createElement("div");
|
|
269
|
+
}
|
|
270
|
+
if (this._projectedBoxElement.parentElement !== this.context.domElement)
|
|
271
|
+
this.context.domElement.appendChild(this._projectedBoxElement);
|
|
272
|
+
this._projectedBoxElement.style.position = "fixed";
|
|
273
|
+
// dotted but with larger gaps
|
|
274
|
+
this._projectedBoxElement.style.outline = "2px dashed rgba(255,0,0,.5)";
|
|
275
|
+
this._projectedBoxElement.style.left = ((minX * .5 + .5) * this.context.domWidth) + "px";
|
|
276
|
+
this._projectedBoxElement.style.top = ((-maxY * .5 + .5) * this.context.domHeight) + "px";
|
|
277
|
+
this._projectedBoxElement.style.width = ((maxX - minX) * .5 * this.context.domWidth) + "px";
|
|
278
|
+
this._projectedBoxElement.style.height = ((maxY - minY) * .5 * this.context.domHeight) + "px";
|
|
279
|
+
this._projectedBoxElement.style.pointerEvents = "none";
|
|
280
|
+
this._projectedBoxElement.style.zIndex = "1000";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
return { minX, maxX, minY, maxY };
|
|
285
|
+
|
|
286
|
+
}
|
|
287
|
+
private _projectedBoxElement: HTMLElement | null = null;
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
}
|