@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.
Files changed (142) hide show
  1. package/CHANGELOG.md +8 -3
  2. package/README.md +2 -1
  3. package/components.needle.json +1 -1
  4. package/dist/{needle-engine.bundle-Cf5H9Zy9.umd.cjs → needle-engine.bundle-0b6rexDr.umd.cjs} +154 -141
  5. package/dist/{needle-engine.bundle-CUo74dPe.js → needle-engine.bundle-B5GtGvbq.js} +8329 -8050
  6. package/dist/needle-engine.bundle-CicGQeCY.min.js +1652 -0
  7. package/dist/needle-engine.js +259 -257
  8. package/dist/needle-engine.min.js +1 -1
  9. package/dist/needle-engine.umd.cjs +1 -1
  10. package/dist/vendor-CPuBPspY.umd.cjs +1121 -0
  11. package/dist/vendor-DPCU8cUF.min.js +1121 -0
  12. package/dist/vendor-MBoqSyFm.js +16240 -0
  13. package/lib/engine/codegen/register_types.js +2 -0
  14. package/lib/engine/codegen/register_types.js.map +1 -1
  15. package/lib/engine/engine_camera.d.ts +7 -1
  16. package/lib/engine/engine_camera.fit.d.ts +1 -1
  17. package/lib/engine/engine_camera.fit.js +3 -30
  18. package/lib/engine/engine_camera.fit.js.map +1 -1
  19. package/lib/engine/engine_camera.js +46 -6
  20. package/lib/engine/engine_camera.js.map +1 -1
  21. package/lib/engine/engine_context.d.ts +6 -0
  22. package/lib/engine/engine_context.js +48 -9
  23. package/lib/engine/engine_context.js.map +1 -1
  24. package/lib/engine/engine_gizmos.d.ts +11 -10
  25. package/lib/engine/engine_gizmos.js +24 -10
  26. package/lib/engine/engine_gizmos.js.map +1 -1
  27. package/lib/engine/engine_license.js +1 -1
  28. package/lib/engine/engine_license.js.map +1 -1
  29. package/lib/engine/engine_lightdata.d.ts +3 -3
  30. package/lib/engine/engine_lightdata.js +10 -10
  31. package/lib/engine/engine_lightdata.js.map +1 -1
  32. package/lib/engine/engine_physics_rapier.js +4 -0
  33. package/lib/engine/engine_physics_rapier.js.map +1 -1
  34. package/lib/engine/engine_scenelighting.d.ts +1 -1
  35. package/lib/engine/engine_scenelighting.js +4 -5
  36. package/lib/engine/engine_scenelighting.js.map +1 -1
  37. package/lib/engine/engine_utils.d.ts +3 -1
  38. package/lib/engine/engine_utils.js +11 -0
  39. package/lib/engine/engine_utils.js.map +1 -1
  40. package/lib/engine/extensions/NEEDLE_lightmaps.js +1 -1
  41. package/lib/engine/extensions/NEEDLE_lightmaps.js.map +1 -1
  42. package/lib/engine/extensions/extension_utils.js +1 -1
  43. package/lib/engine/extensions/extension_utils.js.map +1 -1
  44. package/lib/engine/webcomponents/logo-element.d.ts +1 -1
  45. package/lib/engine/webcomponents/logo-element.js +29 -5
  46. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  47. package/lib/engine/webcomponents/needle menu/needle-menu.js +4 -3
  48. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  49. package/lib/engine/webcomponents/needle-engine.js +22 -0
  50. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  51. package/lib/engine/webcomponents/needle-engine.loading.d.ts +0 -1
  52. package/lib/engine/webcomponents/needle-engine.loading.js +3 -36
  53. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  54. package/lib/engine/xr/NeedleXRController.d.ts +3 -3
  55. package/lib/engine/xr/NeedleXRController.js +28 -0
  56. package/lib/engine/xr/NeedleXRController.js.map +1 -1
  57. package/lib/engine-components/Camera.d.ts +1 -1
  58. package/lib/engine-components/Camera.js +1 -1
  59. package/lib/engine-components/CameraUtils.js +2 -1
  60. package/lib/engine-components/CameraUtils.js.map +1 -1
  61. package/lib/engine-components/CharacterController.d.ts +2 -2
  62. package/lib/engine-components/CharacterController.js +2 -2
  63. package/lib/engine-components/OrbitControls.d.ts +5 -2
  64. package/lib/engine-components/OrbitControls.js +31 -7
  65. package/lib/engine-components/OrbitControls.js.map +1 -1
  66. package/lib/engine-components/Renderer.js +6 -1
  67. package/lib/engine-components/Renderer.js.map +1 -1
  68. package/lib/engine-components/Skybox.js +22 -4
  69. package/lib/engine-components/Skybox.js.map +1 -1
  70. package/lib/engine-components/codegen/components.d.ts +1 -0
  71. package/lib/engine-components/codegen/components.js +1 -0
  72. package/lib/engine-components/codegen/components.js.map +1 -1
  73. package/lib/engine-components/debug/LogStats.d.ts +1 -0
  74. package/lib/engine-components/debug/LogStats.js +1 -0
  75. package/lib/engine-components/debug/LogStats.js.map +1 -1
  76. package/lib/engine-components/timeline/PlayableDirector.d.ts +7 -0
  77. package/lib/engine-components/timeline/PlayableDirector.js +8 -1
  78. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  79. package/lib/engine-components/timeline/TimelineModels.d.ts +45 -2
  80. package/lib/engine-components/timeline/TimelineModels.js +6 -0
  81. package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
  82. package/lib/engine-components/timeline/TimelineTracks.d.ts +2 -1
  83. package/lib/engine-components/timeline/TimelineTracks.js +30 -25
  84. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  85. package/lib/engine-components/utils/LookAt.js +5 -1
  86. package/lib/engine-components/utils/LookAt.js.map +1 -1
  87. package/lib/engine-components/web/Clickthrough.js +10 -2
  88. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  89. package/lib/engine-components/web/ScrollFollow.d.ts +24 -0
  90. package/lib/engine-components/web/ScrollFollow.js +167 -41
  91. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  92. package/lib/engine-components/web/ViewBox.d.ts +46 -0
  93. package/lib/engine-components/web/ViewBox.js +270 -0
  94. package/lib/engine-components/web/ViewBox.js.map +1 -0
  95. package/lib/engine-components/web/index.d.ts +1 -0
  96. package/lib/engine-components/web/index.js +1 -0
  97. package/lib/engine-components/web/index.js.map +1 -1
  98. package/lib/engine-components/webxr/WebARSessionRoot.js +1 -0
  99. package/lib/engine-components/webxr/WebARSessionRoot.js.map +1 -1
  100. package/lib/engine-components-experimental/Presentation.d.ts +1 -0
  101. package/lib/engine-components-experimental/Presentation.js +1 -0
  102. package/lib/engine-components-experimental/Presentation.js.map +1 -1
  103. package/package.json +2 -1
  104. package/src/engine/codegen/register_types.ts +2 -0
  105. package/src/engine/engine_camera.fit.ts +2 -32
  106. package/src/engine/engine_camera.ts +61 -9
  107. package/src/engine/engine_context.ts +50 -10
  108. package/src/engine/engine_gizmos.ts +37 -23
  109. package/src/engine/engine_license.ts +1 -1
  110. package/src/engine/engine_lightdata.ts +11 -11
  111. package/src/engine/engine_physics_rapier.ts +3 -0
  112. package/src/engine/engine_scenelighting.ts +5 -6
  113. package/src/engine/engine_utils.ts +12 -0
  114. package/src/engine/extensions/NEEDLE_lightmaps.ts +1 -1
  115. package/src/engine/extensions/extension_utils.ts +1 -1
  116. package/src/engine/webcomponents/logo-element.ts +29 -4
  117. package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -3
  118. package/src/engine/webcomponents/needle-engine.loading.ts +32 -32
  119. package/src/engine/webcomponents/needle-engine.ts +33 -6
  120. package/src/engine/xr/NeedleXRController.ts +36 -4
  121. package/src/engine-components/Camera.ts +1 -1
  122. package/src/engine-components/CameraUtils.ts +1 -1
  123. package/src/engine-components/CharacterController.ts +2 -2
  124. package/src/engine-components/OrbitControls.ts +41 -2
  125. package/src/engine-components/Renderer.ts +6 -1
  126. package/src/engine-components/Skybox.ts +26 -7
  127. package/src/engine-components/codegen/components.ts +1 -0
  128. package/src/engine-components/debug/LogStats.ts +1 -0
  129. package/src/engine-components/timeline/PlayableDirector.ts +10 -1
  130. package/src/engine-components/timeline/TimelineModels.ts +45 -3
  131. package/src/engine-components/timeline/TimelineTracks.ts +30 -25
  132. package/src/engine-components/utils/LookAt.ts +5 -1
  133. package/src/engine-components/web/Clickthrough.ts +11 -2
  134. package/src/engine-components/web/ScrollFollow.ts +200 -48
  135. package/src/engine-components/web/ViewBox.ts +292 -0
  136. package/src/engine-components/web/index.ts +2 -1
  137. package/src/engine-components/webxr/WebARSessionRoot.ts +1 -0
  138. package/src/engine-components-experimental/Presentation.ts +1 -0
  139. package/dist/needle-engine.bundle-DlAVTipB.min.js +0 -1639
  140. package/dist/vendor-D0Yvltn9.umd.cjs +0 -1121
  141. package/dist/vendor-DU8tJyl_.js +0 -14366
  142. 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 !== null && attribute !== "0" && attribute !== "false") {
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 (!target.isPlaying) target.evaluate();
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
- for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean }>("ScrollMarker")) {
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 (marker.selector?.length && (marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (!marker.element?.parentNode))) {
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
- marker.element = document.querySelector<HTMLElement>(marker.selector) || null;
302
- if (debug) console.debug("ScrollMarker found on page", marker.element, marker.selector);
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.selector + "\n", error);
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 (const marker of markersArray) {
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 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
- }
366
+ const nextTime = nextMarker
367
+ ? (nextMarker.time - oneFrameTime)
368
+ : duration;
361
369
 
362
370
  markerCount += 1;
363
371
 
364
- if (overlap > 0) {
365
- weightsArray.push({ time: marker.time, weight: overlap });
366
- sum += overlap;
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
- 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;
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: (ScrollMarkerModel & { element?: HTMLElement | null })[] = [];
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
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./Clickthrough.js";
2
2
  export * from "./CursorFollow.js";
3
3
  export * from "./HoverAnimation.js";
4
- export * from "./ScrollFollow.js";
4
+ export * from "./ScrollFollow.js";
5
+ export * from "./ViewBox.js";
@@ -465,6 +465,7 @@ export class WebARSessionRoot extends Behaviour {
465
465
  return;
466
466
  }
467
467
  else {
468
+ // @ts-ignore
468
469
  const anchor = await hit.createAnchor(session.viewerPose!.transform);
469
470
  // make sure the session is still active
470
471
  if (session.running && anchor) {
@@ -1,6 +1,7 @@
1
1
  import type { KeyCode } from "../engine/engine_input.js";
2
2
  import { Behaviour } from "../engine-components/Component.js";
3
3
 
4
+ /** @internal */
4
5
  export class PresentationMode extends Behaviour {
5
6
 
6
7
  toggleKey: KeyCode = "KeyP";