@needle-tools/engine 4.10.2 → 4.10.3

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.
@@ -53,6 +53,12 @@ type ScrollFollowEvent = {
53
53
  * 2. Add a ScrollFollow component to the same GameObject or another GameObject in the scene.
54
54
  * 3. Assign the PlayableDirector component to the ScrollFollow's target property.
55
55
  * 4. The timeline will now scrub based on the scroll position of the page.
56
+ * 5. (Optional) Add ScrollMarker markers to your HTML to define specific points in the timeline that correspond to elements on the page. For example:
57
+ * ```html
58
+ * <div data-timeline-marker="0.0">Start of Timeline</div>
59
+ * <div data-timeline-marker="0.5">Middle of Timeline</div>
60
+ * <div data-timeline-marker="1.0">End of Timeline</div>
61
+ * ```
56
62
  *
57
63
  * @category Web
58
64
  * @group Components
@@ -291,67 +297,75 @@ export class ScrollFollow extends Behaviour {
291
297
  private handleTimelineTarget(director: PlayableDirector, value: number) {
292
298
 
293
299
  const duration = director.duration;
294
-
295
-
296
- let scrollRegionStart = Infinity;
297
- let scrollRegionEnd = 0;
298
- markersArray.length = 0;
299
-
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++;
307
-
308
- // Get marker elements from DOM
309
- if ((marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (marker.element && !marker.element?.parentNode))) {
310
- marker.needsUpdate = false;
311
- try {
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;
300
+ let markersArray = timelineMarkerArrays.get(director);
301
+
302
+ // Create markers array
303
+ if (!markersArray) {
304
+ markersArray = [];
305
+ timelineMarkerArrays.set(director, markersArray);
306
+
307
+ let markerIndex = 0;
308
+
309
+ for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean, timeline?: ViewTimeline }>("ScrollMarker")) {
310
+
311
+ const index = markerIndex++;
312
+
313
+ // Get marker elements from DOM
314
+ if ((marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (marker.element && !marker.element?.parentNode))) {
315
+ marker.needsUpdate = false;
316
+ try {
317
+ // 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
318
+ marker.element = tryGetElementsForSelector(index) as HTMLElement | null;
319
+ if (debug) console.debug(`ScrollMarker #${index} (${marker.time.toFixed(2)}) found`, marker.element);
320
+ if (!marker.element) {
321
+ if (debug || isDevEnvironment()) console.warn(`No HTML element found for ScrollMarker: ${marker.name} (index ${index})`);
322
+ continue;
323
+ }
319
324
  }
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
- });
325
+ catch (error) {
326
+ marker.element = null;
327
+ console.error("ScrollMarker selector is not valid: " + marker.name + "\n", error);
326
328
  }
327
329
  }
328
- catch (error) {
329
- marker.element = null;
330
- console.error("ScrollMarker selector is not valid: " + marker.name + "\n", error);
331
- }
332
- }
333
330
 
334
- // skip markers without element (e.g. if the selector didn't return any element)
335
- if (!marker.element) continue;
331
+ // skip markers without element (e.g. if the selector didn't return any element)
332
+ if (!marker.element) continue;
336
333
 
337
- markersArray.push(marker);
334
+ markersArray.push(marker);
335
+ }
338
336
 
339
- const top = marker.element.offsetTop;
340
- const height = marker.element.offsetHeight;
341
- const bottom = top + height;
342
- if (top < scrollRegionStart) {
343
- scrollRegionStart = top;
337
+ // If the timeline has no markers defined we can use timeline-marker elements in the DOM. These must define times then
338
+ if (markersArray.length <= 0) {
339
+ const markers = document.querySelectorAll(`[data-timeline-marker]`);
340
+ markers.forEach((element) => {
341
+ const value = element.getAttribute("data-timeline-marker");
342
+ const time = parseFloat(value || ("NaN"));
343
+ if (!isNaN(time)) {
344
+ markersArray!.push({
345
+ time,
346
+ element: element as HTMLElement,
347
+ });
348
+ }
349
+ else if (isDevEnvironment() || debug) {
350
+ console.warn("[ScrollFollow] data-timeline-marker attribute is not a valid number. Supported are numbers only (e.g. <div data-timeline-marker=\"0.5\">)");
351
+ }
352
+ });
344
353
  }
345
- if (bottom > scrollRegionEnd) {
346
- scrollRegionEnd = bottom;
354
+
355
+ // Init ViewTimeline for markers
356
+ for (const marker of markersArray) {
357
+ if (marker.element) {
358
+ // https://scroll-driven-animations.style/tools/view-timeline/ranges
359
+ /** @ts-ignore */
360
+ marker.timeline = new ViewTimeline({
361
+ subject: marker.element,
362
+ axis: 'block', // https://drafts.csswg.org/scroll-animations/#scroll-notation
363
+ });
364
+ }
347
365
  }
348
366
  }
349
367
 
350
368
 
351
-
352
- const currentTop = this._scrollValue;
353
- const currentBottom = currentTop + this._scrollContainerHeight;
354
-
355
369
  weightsArray.length = 0;
356
370
  let sum = 0;
357
371
  const oneFrameTime = 1 / 60;
@@ -374,7 +388,7 @@ export class ScrollFollow extends Behaviour {
374
388
  const time01 = calculateTimelinePositionNormalized(timeline);
375
389
  // remap 0-1 to 0 - 1 - 0 (full weight at center)
376
390
  const weight = 1 - Math.abs(time01 - 0.5) * 2;
377
- const name = marker.name || `marker${i}`;
391
+ const name = `marker${i}`;
378
392
  if (time01 > 0 && time01 <= 1) {
379
393
  const lerpTime = marker.time + (nextTime - marker.time) * time01;
380
394
  weightsArray.push({ name, time: lerpTime, weight: weight });
@@ -391,38 +405,6 @@ export class ScrollFollow extends Behaviour {
391
405
  sum += 1;
392
406
  }
393
407
  }
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
- // }
426
408
  }
427
409
 
428
410
  if (weightsArray.length <= 0 && markerCount <= 0) {
@@ -456,12 +438,14 @@ export class ScrollFollow extends Behaviour {
456
438
  }
457
439
 
458
440
 
441
+ const timelineMarkerArrays: WeakMap<PlayableDirector,
442
+ Array<{
443
+ time: number,
444
+ element?: HTMLElement | null | undefined,
445
+ timeline?: ViewTimeline,
446
+ }>
447
+ > = new WeakMap();
459
448
 
460
- const weightsArray: OverlapInfo[] = [];
461
- const markersArray: Array<ScrollMarkerModel & {
462
- element?: HTMLElement | null,
463
- timeline?: ViewTimeline,
464
- }> = [];
465
449
 
466
450
  type OverlapInfo = {
467
451
  name: string,
@@ -471,6 +455,8 @@ type OverlapInfo = {
471
455
  weight: number,
472
456
  }
473
457
 
458
+ const weightsArray: OverlapInfo[] = [];
459
+
474
460
 
475
461
  // type SelectorCache = {
476
462
  // /** The selector used to query the *elements */
@@ -480,41 +466,23 @@ type OverlapInfo = {
480
466
  // }
481
467
  // const querySelectorResults: Array<SelectorCache> = [];
482
468
 
483
- const needleScrollMarkerIndexCache = new Map<number, Element | null>();
484
- const needleScrollMarkerNameCache = new Map<string, Element | null>();
469
+ const needleScrollMarkerCache = new Array<Element>();
485
470
  let needsScrollMarkerRefresh = true;
486
471
 
487
- function tryGetElementsForSelector(index: number, name: string, _cycle: number = 0): Element | null {
472
+ function tryGetElementsForSelector(index: number): Element | null {
488
473
 
489
474
  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;
475
+ const element = needleScrollMarkerCache[index] || null;
476
+ if (element) return element;
507
477
  }
508
478
  needsScrollMarkerRefresh = false;
509
- needleScrollMarkerIndexCache.clear();
479
+ needleScrollMarkerCache.length = 0;
510
480
  const markers = document.querySelectorAll(`[data-timeline-marker]`);
511
481
  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);
482
+ needleScrollMarkerCache[i] = m;
515
483
  });
516
484
  needsScrollMarkerRefresh = false;
517
- return tryGetElementsForSelector(index, name);
485
+ return tryGetElementsForSelector(index);
518
486
  }
519
487
 
520
488