@needle-tools/engine 4.9.3 → 4.10.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/components.needle.json +1 -1
- package/dist/{gltf-progressive-DhE1A6hX.min.js → gltf-progressive-CoZbSfPR.min.js} +1 -1
- package/dist/{gltf-progressive-egsMzRdv.js → gltf-progressive-DUR9TuAH.js} +3 -3
- package/dist/{gltf-progressive-DWiyqrwB.umd.cjs → gltf-progressive-Iy7aSAPk.umd.cjs} +1 -1
- package/dist/{needle-engine.bundle-C7LSzO5L.umd.cjs → needle-engine.bundle-6so_os_w.umd.cjs} +179 -145
- package/dist/{needle-engine.bundle-BAsxNKpA.js → needle-engine.bundle-Dj2DYdMY.js} +7699 -7235
- package/dist/needle-engine.bundle-Djy6H4lx.min.js +1650 -0
- package/dist/needle-engine.js +460 -456
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/{postprocessing-BZOSD1ln.min.js → postprocessing-BHMVuZQ1.min.js} +1 -1
- package/dist/{postprocessing-Bb5StX0o.umd.cjs → postprocessing-BsnRNRRS.umd.cjs} +1 -1
- package/dist/{postprocessing-BzFF7i-7.js → postprocessing-DQ2pynXW.js} +2 -2
- package/dist/{three-BK56xWDs.umd.cjs → three-B-jwTHao.umd.cjs} +11 -11
- package/dist/{three-CsHK73Zc.js → three-CJSAehtG.js} +1 -0
- package/dist/{three-examples-Bph291U2.min.js → three-examples-BivkhnvN.min.js} +1 -1
- package/dist/{three-examples-C9WfZu-X.umd.cjs → three-examples-Deqc1bNw.umd.cjs} +1 -1
- package/dist/{three-examples-BvMpKSun.js → three-examples-Doq0rvFU.js} +1 -1
- package/dist/{three-mesh-ui-CN6aRT7i.js → three-mesh-ui-CktOi6oI.js} +1 -1
- package/dist/{three-mesh-ui-DnxkZWNA.umd.cjs → three-mesh-ui-CsHwj9cJ.umd.cjs} +1 -1
- package/dist/{three-mesh-ui-n_qS2BM-.min.js → three-mesh-ui-DhYXcXZe.min.js} +1 -1
- package/dist/{three-TNFQHSFa.min.js → three-qw28ZtTy.min.js} +10 -10
- package/dist/{vendor-BtJpSuCj.umd.cjs → vendor-D0Yvltn9.umd.cjs} +1 -1
- package/dist/{vendor-k9i6CeGi.js → vendor-DU8tJyl_.js} +1 -1
- package/dist/{vendor-XJ9xiwrv.min.js → vendor-JyrX4DVM.min.js} +1 -1
- package/lib/engine/api.d.ts +1 -0
- package/lib/engine/api.js +1 -0
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/codegen/register_types.js +6 -0
- package/lib/engine/codegen/register_types.js.map +1 -1
- package/lib/engine/engine_animation.d.ts +21 -1
- package/lib/engine/engine_animation.js +32 -1
- package/lib/engine/engine_animation.js.map +1 -1
- package/lib/engine/engine_camera.d.ts +7 -1
- package/lib/engine/engine_camera.fit.d.ts +68 -0
- package/lib/engine/engine_camera.fit.js +166 -0
- package/lib/engine/engine_camera.fit.js.map +1 -0
- 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 +2 -2
- package/lib/engine/engine_gizmos.js +2 -2
- package/lib/engine/engine_physics.js +6 -3
- package/lib/engine/engine_physics.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.d.ts +1 -0
- package/lib/engine/webcomponents/needle-engine.js +6 -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 +62 -59
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine-components/AnimatorController.js +16 -0
- package/lib/engine-components/AnimatorController.js.map +1 -1
- package/lib/engine-components/CameraUtils.js +8 -9
- package/lib/engine-components/CameraUtils.js.map +1 -1
- package/lib/engine-components/OrbitControls.d.ts +7 -47
- package/lib/engine-components/OrbitControls.js +25 -149
- package/lib/engine-components/OrbitControls.js.map +1 -1
- package/lib/engine-components/Renderer.d.ts +2 -2
- package/lib/engine-components/Renderer.js +10 -5
- package/lib/engine-components/Renderer.js.map +1 -1
- package/lib/engine-components/api.d.ts +0 -1
- package/lib/engine-components/api.js.map +1 -1
- package/lib/engine-components/codegen/components.d.ts +3 -0
- package/lib/engine-components/codegen/components.js +3 -0
- package/lib/engine-components/codegen/components.js.map +1 -1
- package/lib/engine-components/timeline/PlayableDirector.d.ts +35 -6
- package/lib/engine-components/timeline/PlayableDirector.js +67 -26
- package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
- package/lib/engine-components/timeline/TimelineModels.d.ts +11 -0
- package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
- package/lib/engine-components/timeline/TimelineTracks.d.ts +7 -0
- package/lib/engine-components/timeline/TimelineTracks.js +23 -2
- 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.d.ts +3 -0
- package/lib/engine-components/web/Clickthrough.js +13 -2
- package/lib/engine-components/web/Clickthrough.js.map +1 -1
- package/lib/engine-components/web/CursorFollow.d.ts +3 -0
- package/lib/engine-components/web/CursorFollow.js +3 -0
- package/lib/engine-components/web/CursorFollow.js.map +1 -1
- package/lib/engine-components/web/HoverAnimation.d.ts +44 -0
- package/lib/engine-components/web/HoverAnimation.js +105 -0
- package/lib/engine-components/web/HoverAnimation.js.map +1 -0
- package/lib/engine-components/web/ScrollFollow.d.ts +40 -4
- package/lib/engine-components/web/ScrollFollow.js +256 -27
- package/lib/engine-components/web/ScrollFollow.js.map +1 -1
- package/lib/engine-components/web/ViewBox.d.ts +16 -0
- package/lib/engine-components/web/ViewBox.js +183 -0
- package/lib/engine-components/web/ViewBox.js.map +1 -0
- package/lib/engine-components/web/index.d.ts +2 -0
- package/lib/engine-components/web/index.js +2 -0
- package/lib/engine-components/web/index.js.map +1 -1
- package/package.json +1 -1
- package/plugins/vite/alias.js +5 -3
- package/plugins/vite/poster-client.js +22 -21
- package/src/engine/api.ts +2 -1
- package/src/engine/codegen/register_types.ts +6 -0
- package/src/engine/engine_animation.ts +69 -1
- package/src/engine/engine_camera.fit.ts +258 -0
- package/src/engine/engine_camera.ts +62 -8
- package/src/engine/engine_context.ts +50 -10
- package/src/engine/engine_gizmos.ts +2 -2
- package/src/engine/engine_physics.ts +6 -3
- 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 +95 -56
- package/src/engine/webcomponents/needle-engine.ts +6 -1
- package/src/engine-components/AnimatorController.ts +21 -2
- package/src/engine-components/CameraUtils.ts +8 -9
- package/src/engine-components/OrbitControls.ts +36 -206
- package/src/engine-components/Renderer.ts +10 -5
- package/src/engine-components/api.ts +0 -1
- package/src/engine-components/codegen/components.ts +3 -0
- package/src/engine-components/timeline/PlayableDirector.ts +88 -34
- package/src/engine-components/timeline/TimelineModels.ts +11 -0
- package/src/engine-components/timeline/TimelineTracks.ts +26 -2
- package/src/engine-components/utils/LookAt.ts +5 -1
- package/src/engine-components/web/Clickthrough.ts +14 -2
- package/src/engine-components/web/CursorFollow.ts +3 -0
- package/src/engine-components/web/HoverAnimation.ts +99 -0
- package/src/engine-components/web/ScrollFollow.ts +316 -25
- package/src/engine-components/web/ViewBox.ts +199 -0
- package/src/engine-components/web/index.ts +3 -1
- package/dist/needle-engine.bundle-ugr1bBtk.min.js +0 -1616
|
@@ -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
|
|
|
@@ -21,6 +30,9 @@ onStart(ctx => {
|
|
|
21
30
|
* - Alternatively, add the `clickthrough` attribute to the `<needle-engine>` HTML element (e.g. `<needle-engine clickthrough></needle-engine>`).
|
|
22
31
|
*
|
|
23
32
|
* @link Example https://stackblitz.com/~/github.com/needle-engine/sample-3d-over-html
|
|
33
|
+
* @category Web
|
|
34
|
+
* @group Components
|
|
35
|
+
* @component
|
|
24
36
|
*/
|
|
25
37
|
export class ClickThrough extends Behaviour {
|
|
26
38
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { AnimationClip } from "three";
|
|
2
|
+
|
|
3
|
+
import { ScaleClipType } from "../../engine/engine_animation.js";
|
|
4
|
+
import { AnimationUtils } from "../../engine/engine_animation.js";
|
|
5
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
|
6
|
+
import { registerType } from "../../engine/engine_typestore.js";
|
|
7
|
+
import { Animation } from "../Animation.js";
|
|
8
|
+
import { Behaviour } from "../Component.js";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @category Web
|
|
13
|
+
* @group Components
|
|
14
|
+
* @component
|
|
15
|
+
*/
|
|
16
|
+
@registerType
|
|
17
|
+
export class HoverAnimation extends Behaviour {
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default hover animation type if no custom clip is provided.
|
|
21
|
+
* **Node**: This is only used if no custom hover animation clip is provided.
|
|
22
|
+
* @default "linear"
|
|
23
|
+
*/
|
|
24
|
+
@serializable()
|
|
25
|
+
type: ScaleClipType = "linear";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Duration of the hover animation in seconds.
|
|
29
|
+
* **Node**: This is only used if no custom hover animation clip is provided.
|
|
30
|
+
* @default 0.1
|
|
31
|
+
*/
|
|
32
|
+
@serializable()
|
|
33
|
+
duration: number = 0.1;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Scale factor to apply when hovering.
|
|
37
|
+
* **Node**: This is only used if no custom hover animation clip is provided.
|
|
38
|
+
* @default 1.1
|
|
39
|
+
*/
|
|
40
|
+
@serializable()
|
|
41
|
+
scaleFactor: number = 1.1;
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Animation clip to play when hovering. If null, a default scale-up animation is used.
|
|
46
|
+
*/
|
|
47
|
+
@serializable(AnimationClip)
|
|
48
|
+
hovered: AnimationClip | null = null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Animation clip to play when not hovering. If null, an empty clip is used.
|
|
52
|
+
*/
|
|
53
|
+
@serializable(AnimationClip)
|
|
54
|
+
idle: AnimationClip | null = null;
|
|
55
|
+
|
|
56
|
+
private animation: Animation | null = null;
|
|
57
|
+
|
|
58
|
+
start() {
|
|
59
|
+
if (!this.idle) this.idle = AnimationUtils.emptyClip();
|
|
60
|
+
|
|
61
|
+
if (!this.hovered) {
|
|
62
|
+
this.hovered = AnimationUtils.createScaleClip({
|
|
63
|
+
type: "linear",
|
|
64
|
+
duration: this.duration || 0.1,
|
|
65
|
+
scale: this.gameObject.scale,
|
|
66
|
+
scaleFactor: this.scaleFactor || 1.1,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.animation ??= this.gameObject.addComponent(Animation);
|
|
71
|
+
this.animation.playAutomatically = false;
|
|
72
|
+
this.playIdle();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onEnable() {
|
|
76
|
+
if (this.animation) this.animation.enabled = true;
|
|
77
|
+
this.playIdle();
|
|
78
|
+
}
|
|
79
|
+
onDisable() {
|
|
80
|
+
if (this.animation) this.animation.enabled = false;
|
|
81
|
+
this.playIdle();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onPointerEnter() {
|
|
85
|
+
this.playHover();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onPointerExit() {
|
|
89
|
+
this.playIdle();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private playIdle() {
|
|
93
|
+
if (this.idle) this.animation?.play(this.idle, { exclusive: true, fadeDuration: .1, loop: true });
|
|
94
|
+
}
|
|
95
|
+
private playHover() {
|
|
96
|
+
if (this.hovered) this.animation?.play(this.hovered, { exclusive: true, fadeDuration: .1, loop: false, clampWhenFinished: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Box3, Object3D } from "three";
|
|
2
|
+
import { element } from "three/src/nodes/TSL.js";
|
|
2
3
|
|
|
4
|
+
import { Context } from "../../engine/engine_context.js";
|
|
3
5
|
import { Mathf } from "../../engine/engine_math.js";
|
|
4
6
|
import { serializable } from "../../engine/engine_serialization.js";
|
|
5
7
|
import { getBoundingBox } from "../../engine/engine_three_utils.js";
|
|
8
|
+
import { getParam } from "../../engine/engine_utils.js";
|
|
6
9
|
import { Animation } from "../Animation.js";
|
|
7
10
|
import { Animator } from "../Animator.js";
|
|
8
11
|
import { AudioSource } from "../AudioSource.js";
|
|
@@ -11,6 +14,9 @@ import { EventList } from "../EventList.js";
|
|
|
11
14
|
import { Light } from "../Light.js";
|
|
12
15
|
import { SplineWalker } from "../splines/SplineWalker.js";
|
|
13
16
|
import { PlayableDirector } from "../timeline/PlayableDirector.js";
|
|
17
|
+
import { ScrollMarkerModel } from "../timeline/TimelineModels.js";
|
|
18
|
+
|
|
19
|
+
const debug = getParam("debugscroll");
|
|
14
20
|
|
|
15
21
|
type ScrollFollowEvent = {
|
|
16
22
|
/** Event type */
|
|
@@ -44,6 +50,10 @@ type ScrollFollowEvent = {
|
|
|
44
50
|
* 2. Add a ScrollFollow component to the same GameObject or another GameObject in the scene.
|
|
45
51
|
* 3. Assign the PlayableDirector component to the ScrollFollow's target property.
|
|
46
52
|
* 4. The timeline will now scrub based on the scroll position of the page.
|
|
53
|
+
*
|
|
54
|
+
* @category Web
|
|
55
|
+
* @group Components
|
|
56
|
+
* @component
|
|
47
57
|
*/
|
|
48
58
|
export class ScrollFollow extends Behaviour {
|
|
49
59
|
|
|
@@ -77,7 +87,8 @@ export class ScrollFollow extends Behaviour {
|
|
|
77
87
|
@serializable()
|
|
78
88
|
invert: boolean = false;
|
|
79
89
|
|
|
80
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* **Experimental - might change in future updates**
|
|
81
92
|
* If set, the scroll position will be read from the specified element instead of the window.
|
|
82
93
|
* Use a CSS selector to specify the element, e.g. `#my-scrollable-div` or `.scroll-container`.
|
|
83
94
|
* @default null
|
|
@@ -98,17 +109,23 @@ export class ScrollFollow extends Behaviour {
|
|
|
98
109
|
* Current scroll value in "pages" (0 = top of page, 1 = bottom of page)
|
|
99
110
|
*/
|
|
100
111
|
get currentValue() {
|
|
101
|
-
return this.
|
|
112
|
+
return this._current_value;
|
|
102
113
|
}
|
|
103
114
|
|
|
104
|
-
private
|
|
105
|
-
private
|
|
106
|
-
private
|
|
115
|
+
private _current_value: number = 0;
|
|
116
|
+
private _target_value: number = 0;
|
|
117
|
+
private _appliedValue: number = -1;
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
private _scrollStart: number = 0;
|
|
121
|
+
private _scrollEnd: number = 0;
|
|
122
|
+
private _scrollValue: number = 0;
|
|
123
|
+
private _scrollContainerHeight: number = 0;
|
|
107
124
|
|
|
108
125
|
/** @internal */
|
|
109
126
|
onEnable() {
|
|
110
127
|
window.addEventListener("wheel", this.updateCurrentScrollValue, { passive: true });
|
|
111
|
-
this.
|
|
128
|
+
this._appliedValue = -1;
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
/** @internal */
|
|
@@ -121,23 +138,28 @@ export class ScrollFollow extends Behaviour {
|
|
|
121
138
|
|
|
122
139
|
this.updateCurrentScrollValue();
|
|
123
140
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
if (this._target_value >= 0) {
|
|
142
|
+
if (this.damping > 0) { // apply damping
|
|
143
|
+
this._current_value = Mathf.lerp(this._current_value, this._target_value, this.context.time.deltaTime / this.damping);
|
|
144
|
+
if (Math.abs(this._current_value - this._target_value) < 0.001) {
|
|
145
|
+
this._current_value = this._target_value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
this._current_value = this._target_value;
|
|
150
|
+
}
|
|
130
151
|
}
|
|
131
152
|
|
|
132
|
-
if (this.
|
|
133
|
-
|
|
153
|
+
// if (this._current_value !== this._appliedValue)
|
|
154
|
+
{
|
|
155
|
+
this._appliedValue = this._current_value;
|
|
134
156
|
|
|
135
157
|
let defaultPrevented = false;
|
|
136
158
|
if (this.changed.listenerCount > 0) {
|
|
137
159
|
// fire change event
|
|
138
160
|
const event: ScrollFollowEvent = {
|
|
139
161
|
type: "change",
|
|
140
|
-
value: this.
|
|
162
|
+
value: this._current_value,
|
|
141
163
|
component: this,
|
|
142
164
|
preventDefault: () => { event.defaultPrevented = true; },
|
|
143
165
|
defaultPrevented: false,
|
|
@@ -149,14 +171,21 @@ export class ScrollFollow extends Behaviour {
|
|
|
149
171
|
// if not prevented apply scroll
|
|
150
172
|
if (!defaultPrevented) {
|
|
151
173
|
|
|
152
|
-
const value = this.invert ? 1 - this.
|
|
174
|
+
const value = this.invert ? 1 - this._current_value : this._current_value;
|
|
175
|
+
|
|
176
|
+
const height = this._rangeEndValue - this._rangeStartValue;
|
|
177
|
+
const pixelValue = this._rangeStartValue + value * height;
|
|
153
178
|
|
|
154
179
|
// apply scroll to target(s)
|
|
155
180
|
if (Array.isArray(this.target)) {
|
|
156
|
-
this.target.forEach(t => t &&
|
|
181
|
+
this.target.forEach(t => t && this.applyScroll(t, value));
|
|
157
182
|
}
|
|
158
183
|
else if (this.target) {
|
|
159
|
-
|
|
184
|
+
this.applyScroll(this.target, value);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (debug && this.context.time.frame % 30 === 0) {
|
|
188
|
+
console.debug(`[ScrollFollow] ${this._current_value.toFixed(5)} — ${(this._target_value * 100).toFixed(0)}%`);
|
|
160
189
|
}
|
|
161
190
|
}
|
|
162
191
|
}
|
|
@@ -164,11 +193,16 @@ export class ScrollFollow extends Behaviour {
|
|
|
164
193
|
|
|
165
194
|
private _lastSelectorValue: string | null = null;
|
|
166
195
|
private _lastSelectorElement: Element | null = null;
|
|
196
|
+
/** Top y */
|
|
197
|
+
private _rangeStartValue: number = 0;
|
|
198
|
+
/** Bottom y */
|
|
199
|
+
private _rangeEndValue: number = 0;
|
|
167
200
|
|
|
168
201
|
private updateCurrentScrollValue = () => {
|
|
169
202
|
|
|
170
203
|
switch (this.mode) {
|
|
171
204
|
case "window":
|
|
205
|
+
|
|
172
206
|
if (this.htmlSelector?.length) {
|
|
173
207
|
if (this.htmlSelector !== this._lastSelectorValue) {
|
|
174
208
|
this._lastSelectorElement = document.querySelector(this.htmlSelector);
|
|
@@ -176,28 +210,41 @@ export class ScrollFollow extends Behaviour {
|
|
|
176
210
|
}
|
|
177
211
|
if (this._lastSelectorElement) {
|
|
178
212
|
const rect = this._lastSelectorElement.getBoundingClientRect();
|
|
179
|
-
|
|
180
|
-
|
|
213
|
+
|
|
214
|
+
this._scrollStart = rect.top + window.scrollY;
|
|
215
|
+
this._scrollEnd = rect.height - window.innerHeight;
|
|
216
|
+
this._scrollValue = -rect.top;
|
|
217
|
+
this._target_value = -rect.top / (rect.height - window.innerHeight);
|
|
218
|
+
this._rangeStartValue = rect.top + window.scrollY;
|
|
219
|
+
this._rangeEndValue = this._rangeStartValue + rect.height - window.innerHeight;
|
|
220
|
+
this._scrollContainerHeight = rect.height;
|
|
181
221
|
break;
|
|
182
222
|
}
|
|
183
223
|
}
|
|
184
224
|
else {
|
|
185
|
-
this.
|
|
225
|
+
this._scrollStart = 0;
|
|
226
|
+
this._scrollEnd = window.document.body.scrollHeight - window.innerHeight;
|
|
227
|
+
this._scrollValue = window.scrollY;
|
|
228
|
+
this._target_value = this._scrollValue / (this._scrollEnd || 1);
|
|
229
|
+
this._rangeStartValue = 0;
|
|
230
|
+
this._rangeEndValue = document.body.scrollHeight;
|
|
231
|
+
this._scrollContainerHeight = window.innerHeight;
|
|
186
232
|
}
|
|
187
|
-
if (isNaN(this.target_value) || !isFinite(this.target_value)) this.target_value = 0;
|
|
188
233
|
break;
|
|
189
234
|
}
|
|
190
235
|
|
|
236
|
+
if (isNaN(this._target_value) || !isFinite(this._target_value)) this._target_value = -1;
|
|
191
237
|
}
|
|
192
238
|
|
|
193
239
|
|
|
194
|
-
private
|
|
240
|
+
private applyScroll(target: object, value: number) {
|
|
195
241
|
|
|
196
242
|
if (!target) return;
|
|
197
243
|
|
|
198
244
|
if (target instanceof PlayableDirector) {
|
|
199
|
-
target
|
|
200
|
-
if (
|
|
245
|
+
this.handleTimelineTarget(target, value);
|
|
246
|
+
if (target.isPlaying) target.pause();
|
|
247
|
+
target.evaluate();
|
|
201
248
|
}
|
|
202
249
|
else if (target instanceof Animator) {
|
|
203
250
|
target.setFloat("scroll", value);
|
|
@@ -236,4 +283,248 @@ export class ScrollFollow extends Behaviour {
|
|
|
236
283
|
}
|
|
237
284
|
}
|
|
238
285
|
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
private handleTimelineTarget(director: PlayableDirector, value: number) {
|
|
289
|
+
|
|
290
|
+
const duration = director.duration;
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
let scrollRegionStart = Infinity;
|
|
294
|
+
let scrollRegionEnd = 0;
|
|
295
|
+
markersArray.length = 0;
|
|
296
|
+
|
|
297
|
+
// querySelectorResults.length = 0;
|
|
298
|
+
let markerIndex = 0;
|
|
299
|
+
|
|
300
|
+
// https://scroll-driven-animations.style/tools/view-timeline/ranges
|
|
301
|
+
for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean, timeline?: ViewTimeline }>("ScrollMarker")) {
|
|
302
|
+
|
|
303
|
+
const index = markerIndex++;
|
|
304
|
+
|
|
305
|
+
// Get marker elements from DOM
|
|
306
|
+
if ((marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (!marker.element?.parentNode))) {
|
|
307
|
+
marker.needsUpdate = false;
|
|
308
|
+
try {
|
|
309
|
+
marker.element = tryGetElementsForSelector(index, marker.name) as HTMLElement | null;
|
|
310
|
+
if (debug) console.debug("ScrollMarker found on page", marker.element, marker.name);
|
|
311
|
+
// if (!marker.element) {
|
|
312
|
+
// marker.timeline = undefined;
|
|
313
|
+
// continue;
|
|
314
|
+
// }
|
|
315
|
+
// else {
|
|
316
|
+
// /** @ts-ignore */
|
|
317
|
+
// marker.timeline = new ViewTimeline({
|
|
318
|
+
// subject: marker.element,
|
|
319
|
+
// axis: 'block', // https://drafts.csswg.org/scroll-animations/#scroll-notation
|
|
320
|
+
// });
|
|
321
|
+
// }
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
marker.element = null;
|
|
325
|
+
console.error("ScrollMarker selector is not valid: " + marker.name + "\n", error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// skip markers without element (e.g. if the selector didn't return any element)
|
|
330
|
+
if (!marker.element) continue;
|
|
331
|
+
|
|
332
|
+
markersArray.push(marker);
|
|
333
|
+
|
|
334
|
+
const top = marker.element.offsetTop;
|
|
335
|
+
const height = marker.element.offsetHeight;
|
|
336
|
+
const bottom = top + height;
|
|
337
|
+
if (top < scrollRegionStart) {
|
|
338
|
+
scrollRegionStart = top;
|
|
339
|
+
}
|
|
340
|
+
if (bottom > scrollRegionEnd) {
|
|
341
|
+
scrollRegionEnd = bottom;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
const currentTop = this._scrollValue;
|
|
348
|
+
const currentBottom = currentTop + this._scrollContainerHeight;
|
|
349
|
+
|
|
350
|
+
weightsArray.length = 0;
|
|
351
|
+
let sum = 0;
|
|
352
|
+
|
|
353
|
+
// 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
|
|
354
|
+
let markerCount = 0;
|
|
355
|
+
for (const marker of markersArray) {
|
|
356
|
+
|
|
357
|
+
if (!marker.element) continue;
|
|
358
|
+
|
|
359
|
+
markerCount += 1;
|
|
360
|
+
|
|
361
|
+
// const timeline = marker.timeline;
|
|
362
|
+
// if (timeline) {
|
|
363
|
+
// const time01 = calculateTimelinePositionNormalized(timeline);
|
|
364
|
+
// if (time01 > 0 && time01 <= 1) {
|
|
365
|
+
// const overlap = calculateTimelinePositionNormalized(timeline!);
|
|
366
|
+
// const weight = overlap;
|
|
367
|
+
// // console.log(marker.element.className, time01)
|
|
368
|
+
// weightsArray.push({ time: marker.time, weight: weight });
|
|
369
|
+
// sum += weight;
|
|
370
|
+
// }
|
|
371
|
+
// }
|
|
372
|
+
// continue;
|
|
373
|
+
// if(this.context.time.frame % 10 === 0) console.log(marker.element?.className, timeline, calculateTimelinePositionNormalized(timeline!));
|
|
374
|
+
|
|
375
|
+
const top = marker.element.offsetTop;
|
|
376
|
+
const height = marker.element.offsetHeight;
|
|
377
|
+
const bottom = top + height;
|
|
378
|
+
let overlap = 0;
|
|
379
|
+
|
|
380
|
+
// 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
|
|
381
|
+
|
|
382
|
+
if (bottom < currentTop) {
|
|
383
|
+
// marker is above scroll region
|
|
384
|
+
overlap = 0;
|
|
385
|
+
}
|
|
386
|
+
else if (top > currentBottom) {
|
|
387
|
+
// marker is below scroll region
|
|
388
|
+
overlap = 0;
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
// calculate overlap in pixels
|
|
392
|
+
const overlapTop = Math.max(top, currentTop);
|
|
393
|
+
const overlapBottom = Math.min(bottom, currentBottom);
|
|
394
|
+
overlap = Math.max(0, overlapBottom - overlapTop);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
markerCount += 1;
|
|
398
|
+
|
|
399
|
+
if (overlap > 0) {
|
|
400
|
+
weightsArray.push({ time: marker.time, weight: overlap });
|
|
401
|
+
sum += overlap;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (weightsArray.length <= 0 && markerCount <= 0) {
|
|
406
|
+
director.time = value * duration;
|
|
407
|
+
}
|
|
408
|
+
else if (weightsArray.length > 0) {
|
|
409
|
+
// normalize and calculate weighted time
|
|
410
|
+
let time = 0;
|
|
411
|
+
for (const entry of weightsArray) {
|
|
412
|
+
const weight = entry.weight / Math.max(0.00001, sum);
|
|
413
|
+
// console.log(weight.toFixed(2))
|
|
414
|
+
// lerp time based on weight
|
|
415
|
+
const diff = Math.abs(entry.time - time);
|
|
416
|
+
time += diff * weight;
|
|
417
|
+
}
|
|
418
|
+
// console.log(time.toFixed(2), [...weightsArray])
|
|
419
|
+
if (this.damping <= 0)
|
|
420
|
+
director.time = time;
|
|
421
|
+
else
|
|
422
|
+
director.time = Mathf.lerp(director.time, time, this.context.time.deltaTime / this.damping);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
const weightsArray: OverlapInfo[] = [];
|
|
431
|
+
const markersArray: (ScrollMarkerModel & { element?: HTMLElement | null, timeline?: ViewTimeline })[] = [];
|
|
432
|
+
|
|
433
|
+
type OverlapInfo = {
|
|
434
|
+
/** Marker time */
|
|
435
|
+
time: number,
|
|
436
|
+
/** Overlap in pixels */
|
|
437
|
+
weight: number,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
// type SelectorCache = {
|
|
442
|
+
// /** The selector used to query the *elements */
|
|
443
|
+
// selector: string,
|
|
444
|
+
// elements: Element[] | null,
|
|
445
|
+
// usedElementCount: number,
|
|
446
|
+
// }
|
|
447
|
+
// const querySelectorResults: Array<SelectorCache> = [];
|
|
448
|
+
|
|
449
|
+
const needleScrollMarkerCacheKey = "data-timeline-marker";
|
|
450
|
+
const needleScrollMarkerIndexCache = new Map<number, Element | null>();
|
|
451
|
+
const needleScrollMarkerNameCache = new Map<string, Element | null>();
|
|
452
|
+
let needsScrollMarkerRefresh = true;
|
|
453
|
+
|
|
454
|
+
function tryGetElementsForSelector(index: number, name: string): Element | null {
|
|
455
|
+
|
|
456
|
+
if (!needsScrollMarkerRefresh) {
|
|
457
|
+
let element = name?.length ? needleScrollMarkerNameCache.get(name) : null;
|
|
458
|
+
if (element) return element;
|
|
459
|
+
element = needleScrollMarkerIndexCache.get(index) || null;
|
|
460
|
+
return element;
|
|
461
|
+
}
|
|
462
|
+
needsScrollMarkerRefresh = false;
|
|
463
|
+
needleScrollMarkerIndexCache.clear();
|
|
464
|
+
const markers = document.querySelectorAll(`[data-timeline-marker]`);
|
|
465
|
+
markers.forEach((m, i) => {
|
|
466
|
+
needleScrollMarkerIndexCache.set(i, m);
|
|
467
|
+
const name = m.getAttribute("data-timeline-marker");
|
|
468
|
+
if (name?.length) needleScrollMarkerNameCache.set(name, m);
|
|
469
|
+
});
|
|
470
|
+
const element = needleScrollMarkerIndexCache.get(index) || null;
|
|
471
|
+
return element;
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
/* e.g.
|
|
475
|
+
<div class="section behind start" data-needle-scroll-marker>
|
|
476
|
+
*/
|
|
477
|
+
// console.log(index, element)
|
|
478
|
+
if (element) return element;
|
|
479
|
+
|
|
480
|
+
// for (const entry of querySelectorResults) {
|
|
481
|
+
// if (entry.selector === selector) {
|
|
482
|
+
// const index = entry.usedElementCount++;
|
|
483
|
+
// return entry.elements && index < entry.elements.length ? entry.elements[index] : null;
|
|
484
|
+
// }
|
|
485
|
+
// }
|
|
486
|
+
// const elements = document.querySelectorAll(selector);
|
|
487
|
+
// querySelectorResults.push({ selector, elements: Array.from(elements), usedElementCount: 1 });
|
|
488
|
+
// if (elements.length > 0) return elements[0];
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
// #region ScrollTimeline
|
|
494
|
+
|
|
495
|
+
function calculateTimelinePositionNormalized(timeline: ViewTimeline) {
|
|
496
|
+
if (!timeline.source) return 0;
|
|
497
|
+
const currentTime = timeline.currentTime;
|
|
498
|
+
const duration = timeline.duration;
|
|
499
|
+
let durationValue = 1;
|
|
500
|
+
if (duration.unit === "seconds") {
|
|
501
|
+
durationValue = duration.value;
|
|
502
|
+
}
|
|
503
|
+
else if (duration.unit === "percent") {
|
|
504
|
+
durationValue = duration.value;
|
|
505
|
+
}
|
|
506
|
+
const t01 = currentTime.unit === "seconds" ? (currentTime.value / durationValue) : (currentTime.value / 100);
|
|
507
|
+
return t01;
|
|
508
|
+
}
|
|
509
|
+
function calculateNormalizedOverlap(timeline: ViewTimeline) {
|
|
510
|
+
if (!timeline.source) return 0;
|
|
511
|
+
const start = timeline.startOffset;
|
|
512
|
+
const end = timeline.endOffset;
|
|
513
|
+
const total = start.value + end.value;
|
|
514
|
+
if (total <= 0) return 1;
|
|
515
|
+
const startNorm = start.value / total;
|
|
516
|
+
const endNorm = end.value / total;
|
|
517
|
+
return 1 - (startNorm + endNorm);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
declare global {
|
|
522
|
+
interface ViewTimeline {
|
|
523
|
+
axis: 'block' | 'inline';
|
|
524
|
+
currentTime: { unit: 'seconds' | 'percent', value: number };
|
|
525
|
+
duration: { unit: 'seconds' | 'percent', value: number };
|
|
526
|
+
source: Element | null;
|
|
527
|
+
startOffset: { unit: 'px', value: number };
|
|
528
|
+
endOffset: { unit: 'px', value: number };
|
|
529
|
+
}
|
|
239
530
|
}
|