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