angular-movement 0.0.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/README.md +64 -0
- package/fesm2022/angular-movement.mjs +2190 -0
- package/fesm2022/angular-movement.mjs.map +1 -0
- package/package.json +47 -0
- package/types/angular-movement.d.ts +455 -0
|
@@ -0,0 +1,2190 @@
|
|
|
1
|
+
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { InjectionToken, inject, PLATFORM_ID, Injectable, input, ElementRef, Directive, computed, effect, forwardRef, afterEveryRender, NgZone, signal, ViewContainerRef, TemplateRef, makeEnvironmentProviders } from '@angular/core';
|
|
4
|
+
|
|
5
|
+
const MOVEMENT_DEFAULTS = {
|
|
6
|
+
duration: 300,
|
|
7
|
+
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
|
8
|
+
delay: 0,
|
|
9
|
+
disabled: false,
|
|
10
|
+
};
|
|
11
|
+
const MOVEMENT_CONFIG = new InjectionToken('MOVEMENT_CONFIG', {
|
|
12
|
+
factory: () => ({ ...MOVEMENT_DEFAULTS }),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const DEFAULT_DURATION = 300;
|
|
16
|
+
const DEFAULT_EASING = 'ease';
|
|
17
|
+
const DEFAULT_DELAY = 0;
|
|
18
|
+
const DEFAULT_PERSPECTIVE = '1200px';
|
|
19
|
+
const DEFAULT_SPRING = {
|
|
20
|
+
stiffness: 500,
|
|
21
|
+
damping: 30,
|
|
22
|
+
};
|
|
23
|
+
const SPRING_BASE = {
|
|
24
|
+
stiffness: 100,
|
|
25
|
+
damping: 10,
|
|
26
|
+
mass: 1,
|
|
27
|
+
velocity: 0,
|
|
28
|
+
};
|
|
29
|
+
const SIMULATION_TICK_RATE = 1000 / 60; // 60fps
|
|
30
|
+
const SIMULATION_MAX_ITERATIONS = 600;
|
|
31
|
+
const SIMULATION_SETTLED_THRESHOLD = 0.001;
|
|
32
|
+
const SCROLL_MAP_DURATION = 1000;
|
|
33
|
+
const SCROLL_WAAPI_CAP = 0.999;
|
|
34
|
+
const SCROLL_LERP_FACTOR = 0.12;
|
|
35
|
+
const SCROLL_RAF_THRESHOLD = 0.001;
|
|
36
|
+
const SMOOTH_SCROLL_FRICTION = 0.92;
|
|
37
|
+
const SMOOTH_SCROLL_MIN_VELOCITY = 0.05;
|
|
38
|
+
const DEFAULT_STAGGER_TIME = 100;
|
|
39
|
+
const DEFAULT_TEXT_STAGGER = 30;
|
|
40
|
+
const DRAG_SNAP_DURATION = 300;
|
|
41
|
+
const DRAG_SNAP_EASING = 'ease';
|
|
42
|
+
|
|
43
|
+
function getValAt(arr, index) {
|
|
44
|
+
if (!arr || arr.length === 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
return arr[Math.min(index, arr.length - 1)];
|
|
47
|
+
}
|
|
48
|
+
function getInterpolated(arr, i1, i2, p) {
|
|
49
|
+
if (!arr || arr.length === 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
const v1 = arr[Math.min(i1, arr.length - 1)];
|
|
52
|
+
const v2 = arr[Math.min(i2, arr.length - 1)];
|
|
53
|
+
return v1 + (v2 - v1) * p;
|
|
54
|
+
}
|
|
55
|
+
function buildKeyframe(frames, getVal) {
|
|
56
|
+
const keyframe = {};
|
|
57
|
+
const opacity = getVal(frames.opacity);
|
|
58
|
+
if (opacity !== undefined) {
|
|
59
|
+
keyframe.opacity = opacity;
|
|
60
|
+
}
|
|
61
|
+
const x = getVal(frames.x);
|
|
62
|
+
const y = getVal(frames.y);
|
|
63
|
+
if (x !== undefined || y !== undefined) {
|
|
64
|
+
keyframe.translate = `${x ?? 0}px ${y ?? 0}px`;
|
|
65
|
+
}
|
|
66
|
+
const scale = getVal(frames.scale);
|
|
67
|
+
if (scale !== undefined) {
|
|
68
|
+
keyframe.scale = `${scale}`;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const scaleX = getVal(frames.scaleX);
|
|
72
|
+
const scaleY = getVal(frames.scaleY);
|
|
73
|
+
if (scaleX !== undefined || scaleY !== undefined) {
|
|
74
|
+
keyframe.scale = `${scaleX ?? 1} ${scaleY ?? 1}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const rotate = getVal(frames.rotate);
|
|
78
|
+
if (rotate !== undefined) {
|
|
79
|
+
keyframe.rotate = `${rotate}deg`;
|
|
80
|
+
}
|
|
81
|
+
const blur = getVal(frames.blur);
|
|
82
|
+
if (blur !== undefined) {
|
|
83
|
+
keyframe.filter = `blur(${blur}px)`;
|
|
84
|
+
}
|
|
85
|
+
const rotateX = getVal(frames.rotateX);
|
|
86
|
+
const rotateY = getVal(frames.rotateY);
|
|
87
|
+
if (rotateX !== undefined || rotateY !== undefined) {
|
|
88
|
+
keyframe.transform = `perspective(${DEFAULT_PERSPECTIVE}) rotateX(${rotateX ?? 0}deg) rotateY(${rotateY ?? 0}deg)`;
|
|
89
|
+
}
|
|
90
|
+
return keyframe;
|
|
91
|
+
}
|
|
92
|
+
function composeKeyframeAt(frames, index) {
|
|
93
|
+
return buildKeyframe(frames, (arr) => getValAt(arr, index));
|
|
94
|
+
}
|
|
95
|
+
function composeInterpolatedKeyframe(frames, i1, i2, p) {
|
|
96
|
+
return buildKeyframe(frames, (arr) => getInterpolated(arr, i1, i2, p));
|
|
97
|
+
}
|
|
98
|
+
function composeInitialStyle(frames) {
|
|
99
|
+
return buildKeyframe(frames, (arr) => (arr && arr.length > 0 ? arr[0] : undefined));
|
|
100
|
+
}
|
|
101
|
+
function composeFinalStyle(frames) {
|
|
102
|
+
return buildKeyframe(frames, (arr) => (arr && arr.length > 0 ? arr[arr.length - 1] : undefined));
|
|
103
|
+
}
|
|
104
|
+
function applyComposedStyle(el, style) {
|
|
105
|
+
if (style.opacity !== undefined)
|
|
106
|
+
el.style.opacity = `${style.opacity}`;
|
|
107
|
+
if (style.translate !== undefined)
|
|
108
|
+
el.style.translate = style.translate;
|
|
109
|
+
if (style.scale !== undefined)
|
|
110
|
+
el.style.scale = style.scale;
|
|
111
|
+
if (style.rotate !== undefined)
|
|
112
|
+
el.style.rotate = style.rotate;
|
|
113
|
+
if (style.transform !== undefined)
|
|
114
|
+
el.style.transform = style.transform;
|
|
115
|
+
if (style.filter !== undefined)
|
|
116
|
+
el.style.filter = style.filter;
|
|
117
|
+
}
|
|
118
|
+
function clearComposedStyle(el) {
|
|
119
|
+
el.style.opacity = '';
|
|
120
|
+
el.style.translate = '';
|
|
121
|
+
el.style.scale = '';
|
|
122
|
+
el.style.rotate = '';
|
|
123
|
+
el.style.transform = '';
|
|
124
|
+
el.style.filter = '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const DEFAULT_FADE_OPACITY = [0, 1];
|
|
128
|
+
const DEFAULT_LEAVE_OPACITY = [1, 0];
|
|
129
|
+
const MOVE_PRESETS = {
|
|
130
|
+
'fade-up': {
|
|
131
|
+
enter: { opacity: DEFAULT_FADE_OPACITY, y: [24, 0] },
|
|
132
|
+
leave: { opacity: DEFAULT_LEAVE_OPACITY, y: [0, -16] },
|
|
133
|
+
},
|
|
134
|
+
'fade-down': {
|
|
135
|
+
enter: { opacity: DEFAULT_FADE_OPACITY, y: [-24, 0] },
|
|
136
|
+
leave: { opacity: DEFAULT_LEAVE_OPACITY, y: [0, 16] },
|
|
137
|
+
},
|
|
138
|
+
'fade-left': {
|
|
139
|
+
enter: { opacity: DEFAULT_FADE_OPACITY, x: [24, 0] },
|
|
140
|
+
leave: { opacity: DEFAULT_LEAVE_OPACITY, x: [0, -16] },
|
|
141
|
+
},
|
|
142
|
+
'fade-right': {
|
|
143
|
+
enter: { opacity: DEFAULT_FADE_OPACITY, x: [-24, 0] },
|
|
144
|
+
leave: { opacity: DEFAULT_LEAVE_OPACITY, x: [0, 16] },
|
|
145
|
+
},
|
|
146
|
+
'slide-up': {
|
|
147
|
+
enter: { y: [60, 0], opacity: DEFAULT_FADE_OPACITY },
|
|
148
|
+
leave: { y: [0, -60], opacity: DEFAULT_LEAVE_OPACITY },
|
|
149
|
+
},
|
|
150
|
+
'slide-down': {
|
|
151
|
+
enter: { y: [-60, 0], opacity: DEFAULT_FADE_OPACITY },
|
|
152
|
+
leave: { y: [0, 60], opacity: DEFAULT_LEAVE_OPACITY },
|
|
153
|
+
},
|
|
154
|
+
'slide-left': {
|
|
155
|
+
enter: { x: [60, 0], opacity: DEFAULT_FADE_OPACITY },
|
|
156
|
+
leave: { x: [0, -60], opacity: DEFAULT_LEAVE_OPACITY },
|
|
157
|
+
},
|
|
158
|
+
'slide-right': {
|
|
159
|
+
enter: { x: [-60, 0], opacity: DEFAULT_FADE_OPACITY },
|
|
160
|
+
leave: { x: [0, 60], opacity: DEFAULT_LEAVE_OPACITY },
|
|
161
|
+
},
|
|
162
|
+
'zoom-in': {
|
|
163
|
+
enter: { opacity: DEFAULT_FADE_OPACITY, scale: [0.5, 1] },
|
|
164
|
+
leave: { opacity: DEFAULT_LEAVE_OPACITY, scale: [1, 0.5] },
|
|
165
|
+
},
|
|
166
|
+
'zoom-out': {
|
|
167
|
+
enter: { opacity: DEFAULT_FADE_OPACITY, scale: [1.3, 1] },
|
|
168
|
+
leave: { opacity: DEFAULT_LEAVE_OPACITY, scale: [1, 1.3] },
|
|
169
|
+
},
|
|
170
|
+
'flip-x': {
|
|
171
|
+
enter: { opacity: DEFAULT_FADE_OPACITY, rotateX: [-90, 0] },
|
|
172
|
+
leave: { opacity: DEFAULT_LEAVE_OPACITY, rotateX: [0, 90] },
|
|
173
|
+
},
|
|
174
|
+
'flip-y': {
|
|
175
|
+
enter: { opacity: DEFAULT_FADE_OPACITY, rotateY: [-90, 0] },
|
|
176
|
+
leave: { opacity: DEFAULT_LEAVE_OPACITY, rotateY: [0, 90] },
|
|
177
|
+
},
|
|
178
|
+
'bounce-in': {
|
|
179
|
+
enter: { opacity: DEFAULT_FADE_OPACITY, y: [30, 0], scale: [0.85, 1] },
|
|
180
|
+
leave: { opacity: DEFAULT_LEAVE_OPACITY, y: [0, -20], scale: [1, 0.9] },
|
|
181
|
+
},
|
|
182
|
+
'blur-in': {
|
|
183
|
+
enter: { opacity: DEFAULT_FADE_OPACITY, blur: [10, 0] },
|
|
184
|
+
leave: { opacity: DEFAULT_LEAVE_OPACITY, blur: [0, 10] },
|
|
185
|
+
},
|
|
186
|
+
spin: {
|
|
187
|
+
enter: { opacity: DEFAULT_FADE_OPACITY, rotate: [-360, 0] },
|
|
188
|
+
leave: { opacity: DEFAULT_LEAVE_OPACITY, rotate: [0, 360] },
|
|
189
|
+
},
|
|
190
|
+
pulse: {
|
|
191
|
+
enter: { scale: [1, 1.05, 1] },
|
|
192
|
+
leave: { scale: [1, 0.95, 1] },
|
|
193
|
+
},
|
|
194
|
+
none: {
|
|
195
|
+
enter: { opacity: [1, 1] },
|
|
196
|
+
leave: { opacity: [1, 1] },
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
function resolveMovementConfig(defaults, overrides, reducedMotion) {
|
|
201
|
+
if (reducedMotion && typeof ngDevMode !== 'undefined' && ngDevMode) {
|
|
202
|
+
console.warn('[Movement] Animations disabled: prefers-reduced-motion is active. ' +
|
|
203
|
+
'Disable "Reduce motion" in your OS accessibility settings to see animations.');
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
duration: Math.max(0, overrides.duration ?? defaults.duration),
|
|
207
|
+
easing: overrides.easing ?? defaults.easing,
|
|
208
|
+
delay: Math.max(0, overrides.delay ?? defaults.delay),
|
|
209
|
+
disabled: reducedMotion || (overrides.disabled ?? defaults.disabled),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function resolveMoveFrames(value, phase) {
|
|
213
|
+
if (typeof value === 'string') {
|
|
214
|
+
const preset = MOVE_PRESETS[value];
|
|
215
|
+
if (!preset) {
|
|
216
|
+
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
|
|
217
|
+
console.warn(`[Movement] Unknown preset: "${value}". Using "none" preset.`);
|
|
218
|
+
}
|
|
219
|
+
return MOVE_PRESETS['none'][phase];
|
|
220
|
+
}
|
|
221
|
+
return preset[phase];
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
function prefersReducedMotion(documentRef) {
|
|
226
|
+
const view = documentRef.defaultView;
|
|
227
|
+
if (!view || typeof view.matchMedia !== 'function') {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
return view.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Reverses all keyframe value arrays in a MoveKeyframes object.
|
|
234
|
+
* Used by hover and tap directives to animate back to the original state.
|
|
235
|
+
*/
|
|
236
|
+
function reverseFrames(frames) {
|
|
237
|
+
const reversed = {};
|
|
238
|
+
for (const key in frames) {
|
|
239
|
+
const k = key;
|
|
240
|
+
const arr = frames[k];
|
|
241
|
+
if (arr) {
|
|
242
|
+
reversed[k] = [...arr].reverse();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return reversed;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Applies the first keyframe values as inline styles to an element.
|
|
249
|
+
* Used by in-view and text directives to set the initial (hidden) state
|
|
250
|
+
* before the IntersectionObserver triggers the animation.
|
|
251
|
+
*/
|
|
252
|
+
function applyInitialStyles(el, frames) {
|
|
253
|
+
applyComposedStyle(el, composeInitialStyle(frames));
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Clears all inline styles set by `applyInitialStyles`.
|
|
257
|
+
* Called just before WAAPI animates so it can take full control.
|
|
258
|
+
*/
|
|
259
|
+
function clearInitialStyles(el) {
|
|
260
|
+
clearComposedStyle(el);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
class WaapiPlayer {
|
|
264
|
+
#animation = null;
|
|
265
|
+
#resolveFinished;
|
|
266
|
+
finished = new Promise((resolve) => {
|
|
267
|
+
this.#resolveFinished = resolve;
|
|
268
|
+
});
|
|
269
|
+
constructor(host, frames, config, onDone) {
|
|
270
|
+
if (typeof host.animate !== 'function') {
|
|
271
|
+
this.#resolveFinished();
|
|
272
|
+
onDone?.();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const keyframes = this.#toWAAPIKeyframes(frames);
|
|
276
|
+
this.#animation = host.animate(keyframes, {
|
|
277
|
+
duration: config.duration,
|
|
278
|
+
easing: config.easing,
|
|
279
|
+
delay: config.delay,
|
|
280
|
+
fill: 'both',
|
|
281
|
+
});
|
|
282
|
+
this.#animation.addEventListener('finish', () => {
|
|
283
|
+
this.#animation?.commitStyles?.();
|
|
284
|
+
this.#animation?.cancel();
|
|
285
|
+
this.#resolveFinished();
|
|
286
|
+
onDone?.();
|
|
287
|
+
}, { once: true });
|
|
288
|
+
}
|
|
289
|
+
play() {
|
|
290
|
+
this.#animation?.play();
|
|
291
|
+
}
|
|
292
|
+
pause() {
|
|
293
|
+
this.#animation?.pause();
|
|
294
|
+
}
|
|
295
|
+
cancel() {
|
|
296
|
+
if (this.#animation?.playState !== 'idle') {
|
|
297
|
+
this.#animation?.cancel();
|
|
298
|
+
}
|
|
299
|
+
this.#resolveFinished();
|
|
300
|
+
}
|
|
301
|
+
get currentTime() {
|
|
302
|
+
return this.#animation?.currentTime ?? 0;
|
|
303
|
+
}
|
|
304
|
+
set currentTime(time) {
|
|
305
|
+
if (this.#animation) {
|
|
306
|
+
this.#animation.currentTime = time;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
#toWAAPIKeyframes(frames) {
|
|
310
|
+
let maxLength = 0;
|
|
311
|
+
for (const key in frames) {
|
|
312
|
+
const arr = frames[key];
|
|
313
|
+
if (Array.isArray(arr)) {
|
|
314
|
+
maxLength = Math.max(maxLength, arr.length);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (maxLength === 0)
|
|
318
|
+
return [];
|
|
319
|
+
const keyframes = [];
|
|
320
|
+
for (let i = 0; i < maxLength; i++) {
|
|
321
|
+
keyframes.push(composeKeyframeAt(frames, i));
|
|
322
|
+
}
|
|
323
|
+
return keyframes;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
class SpringPlayer {
|
|
328
|
+
host;
|
|
329
|
+
frames;
|
|
330
|
+
delay;
|
|
331
|
+
onDone;
|
|
332
|
+
#resolveFinished;
|
|
333
|
+
finished = new Promise((resolve) => {
|
|
334
|
+
this.#resolveFinished = resolve;
|
|
335
|
+
});
|
|
336
|
+
#animation = null;
|
|
337
|
+
constructor(host, frames, userConfig, delay, onDone) {
|
|
338
|
+
this.host = host;
|
|
339
|
+
this.frames = frames;
|
|
340
|
+
this.delay = delay;
|
|
341
|
+
this.onDone = onDone;
|
|
342
|
+
if (typeof host.animate !== 'function') {
|
|
343
|
+
this.#resolveFinished();
|
|
344
|
+
onDone?.();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const config = {
|
|
348
|
+
stiffness: 100,
|
|
349
|
+
damping: 10,
|
|
350
|
+
mass: 1,
|
|
351
|
+
velocity: 0,
|
|
352
|
+
...userConfig,
|
|
353
|
+
};
|
|
354
|
+
const keyframes = this.#generateSpringKeyframes(frames, config);
|
|
355
|
+
if (keyframes.length === 0) {
|
|
356
|
+
this.#resolveFinished();
|
|
357
|
+
onDone?.();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Default duration of the calculated simulation is bound to the arrays output.
|
|
361
|
+
// We run it over that specific time frame, we know exactly the duration by counting ticks * tick duration.
|
|
362
|
+
// Let's assume tick rate is 16.66ms (60fps simulation)
|
|
363
|
+
const duration = keyframes.length * SIMULATION_TICK_RATE;
|
|
364
|
+
this.#animation = host.animate(keyframes, {
|
|
365
|
+
duration,
|
|
366
|
+
delay: this.delay,
|
|
367
|
+
fill: 'both',
|
|
368
|
+
easing: 'linear', // Spring physics already has the easing baked into the frames
|
|
369
|
+
});
|
|
370
|
+
this.#animation.addEventListener('finish', () => {
|
|
371
|
+
this.#animation?.commitStyles?.();
|
|
372
|
+
this.#animation?.cancel();
|
|
373
|
+
this.#resolveFinished();
|
|
374
|
+
onDone?.();
|
|
375
|
+
}, { once: true });
|
|
376
|
+
}
|
|
377
|
+
play() {
|
|
378
|
+
this.#animation?.play();
|
|
379
|
+
}
|
|
380
|
+
pause() {
|
|
381
|
+
this.#animation?.pause();
|
|
382
|
+
}
|
|
383
|
+
cancel() {
|
|
384
|
+
if (this.#animation?.playState !== 'idle') {
|
|
385
|
+
this.#animation?.cancel();
|
|
386
|
+
}
|
|
387
|
+
this.#resolveFinished();
|
|
388
|
+
}
|
|
389
|
+
get currentTime() {
|
|
390
|
+
return this.#animation?.currentTime ?? 0;
|
|
391
|
+
}
|
|
392
|
+
set currentTime(time) {
|
|
393
|
+
if (this.#animation) {
|
|
394
|
+
this.#animation.currentTime = time;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
#generateSpringKeyframes(frames, config) {
|
|
398
|
+
let maxSteps = 0;
|
|
399
|
+
for (const key in frames) {
|
|
400
|
+
const arr = frames[key];
|
|
401
|
+
if (Array.isArray(arr)) {
|
|
402
|
+
maxSteps = Math.max(maxSteps, arr.length);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (maxSteps <= 1)
|
|
406
|
+
return [];
|
|
407
|
+
const keyframes = [];
|
|
408
|
+
const dt = 1 / 60; // Simulate at 60fps (16.66ms per tick)
|
|
409
|
+
const stiffness = config.stiffness;
|
|
410
|
+
const damping = config.damping;
|
|
411
|
+
const mass = config.mass;
|
|
412
|
+
// We will simulate segments between keyframes based on physics
|
|
413
|
+
for (let step = 0; step < maxSteps - 1; step++) {
|
|
414
|
+
let progress = 0;
|
|
415
|
+
let velocity = step === 0 ? (config.velocity ?? 0) : 0;
|
|
416
|
+
let isSettled = false;
|
|
417
|
+
// Simulate the spring for this intermediate segment
|
|
418
|
+
// Prevent infinite loops safely by bounding iterations
|
|
419
|
+
let iterations = 0;
|
|
420
|
+
const maxIterations = SIMULATION_MAX_ITERATIONS; // max 10 seconds per step
|
|
421
|
+
while (!isSettled && iterations < maxIterations) {
|
|
422
|
+
// Evaluate frame
|
|
423
|
+
const p = Math.min(Math.max(progress, 0), 1);
|
|
424
|
+
keyframes.push(this.#composeFrame(frames, step, step + 1, p));
|
|
425
|
+
// Advance physics F = -k*x - c*v
|
|
426
|
+
const displacement = progress - 1;
|
|
427
|
+
const force = -stiffness * displacement - damping * velocity;
|
|
428
|
+
const acceleration = force / mass;
|
|
429
|
+
velocity += acceleration * dt;
|
|
430
|
+
progress += velocity * dt;
|
|
431
|
+
if (Math.abs(displacement) < SIMULATION_SETTLED_THRESHOLD &&
|
|
432
|
+
Math.abs(velocity) < SIMULATION_SETTLED_THRESHOLD) {
|
|
433
|
+
isSettled = true;
|
|
434
|
+
}
|
|
435
|
+
iterations++;
|
|
436
|
+
}
|
|
437
|
+
// Ensure the final state of the step is exactly 1
|
|
438
|
+
keyframes.push(this.#composeFrame(frames, step, step + 1, 1));
|
|
439
|
+
}
|
|
440
|
+
return keyframes;
|
|
441
|
+
}
|
|
442
|
+
#composeFrame(frames, i1, i2, p) {
|
|
443
|
+
return composeInterpolatedKeyframe(frames, i1, i2, p);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
class AnimationEngine {
|
|
448
|
+
#platformId = inject(PLATFORM_ID);
|
|
449
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
450
|
+
play(host, frames, options = {}) {
|
|
451
|
+
if (!isPlatformBrowser(this.#platformId)) {
|
|
452
|
+
options.onDone?.();
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
if (options.disabled) {
|
|
456
|
+
this.#applyFinalStyles(host, frames);
|
|
457
|
+
options.onDone?.();
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const config = options.config ?? this.#defaults;
|
|
461
|
+
const isSpring = options.spring || config.easing === 'spring';
|
|
462
|
+
if (isSpring) {
|
|
463
|
+
return new SpringPlayer(host, frames, options.spring ?? {}, options.delay ?? config.delay, options.onDone);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
return new WaapiPlayer(host, frames, {
|
|
467
|
+
duration: config.duration,
|
|
468
|
+
easing: config.easing,
|
|
469
|
+
delay: options.delay ?? config.delay,
|
|
470
|
+
disabled: false,
|
|
471
|
+
}, options.onDone);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
#applyFinalStyles(host, frames) {
|
|
475
|
+
applyComposedStyle(host, composeFinalStyle(frames));
|
|
476
|
+
}
|
|
477
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: AnimationEngine, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
478
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: AnimationEngine, providedIn: 'root' });
|
|
479
|
+
}
|
|
480
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: AnimationEngine, decorators: [{
|
|
481
|
+
type: Injectable,
|
|
482
|
+
args: [{ providedIn: 'root' }]
|
|
483
|
+
}] });
|
|
484
|
+
|
|
485
|
+
const MOVE_STAGGER_PARENT = new InjectionToken('MOVE_STAGGER_PARENT');
|
|
486
|
+
|
|
487
|
+
const MOVE_PRESENCE_PARENT = new InjectionToken('MOVE_PRESENCE_PARENT');
|
|
488
|
+
|
|
489
|
+
class MoveAnimateDirective {
|
|
490
|
+
move = input(undefined, ...(ngDevMode ? [{ debugName: "move" }] : /* istanbul ignore next */ []));
|
|
491
|
+
moveAnimate = input(undefined, ...(ngDevMode ? [{ debugName: "moveAnimate" }] : /* istanbul ignore next */ []));
|
|
492
|
+
moveAnimateLeave = input(undefined, ...(ngDevMode ? [{ debugName: "moveAnimateLeave" }] : /* istanbul ignore next */ []));
|
|
493
|
+
moveDuration = input(undefined, ...(ngDevMode ? [{ debugName: "moveDuration" }] : /* istanbul ignore next */ []));
|
|
494
|
+
moveEasing = input(undefined, ...(ngDevMode ? [{ debugName: "moveEasing" }] : /* istanbul ignore next */ []));
|
|
495
|
+
moveDelay = input(undefined, ...(ngDevMode ? [{ debugName: "moveDelay" }] : /* istanbul ignore next */ []));
|
|
496
|
+
moveDisabled = input(undefined, ...(ngDevMode ? [{ debugName: "moveDisabled" }] : /* istanbul ignore next */ []));
|
|
497
|
+
moveSpring = input(undefined, ...(ngDevMode ? [{ debugName: "moveSpring" }] : /* istanbul ignore next */ []));
|
|
498
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
499
|
+
#documentRef = inject(DOCUMENT);
|
|
500
|
+
#host = inject((ElementRef));
|
|
501
|
+
#engine = inject(AnimationEngine);
|
|
502
|
+
#stagger = inject(MOVE_STAGGER_PARENT, { optional: true });
|
|
503
|
+
#presence = inject(MOVE_PRESENCE_PARENT, { optional: true });
|
|
504
|
+
#config = this.#defaults;
|
|
505
|
+
#enterPlayer = null;
|
|
506
|
+
#leavePlayer = null;
|
|
507
|
+
ngOnInit() {
|
|
508
|
+
this.#stagger?.register(this.#host.nativeElement);
|
|
509
|
+
this.#presence?.register(this);
|
|
510
|
+
Promise.resolve().then(() => {
|
|
511
|
+
const staggerDelay = this.#stagger?.getDelay(this.#host.nativeElement) ?? 0;
|
|
512
|
+
this.#config = resolveMovementConfig(this.#defaults, {
|
|
513
|
+
duration: this.moveDuration(),
|
|
514
|
+
easing: this.moveEasing(),
|
|
515
|
+
delay: (this.moveDelay() ?? 0) + staggerDelay,
|
|
516
|
+
disabled: this.moveDisabled(),
|
|
517
|
+
}, prefersReducedMotion(this.#documentRef));
|
|
518
|
+
const enterInput = this.resolveEnterInput();
|
|
519
|
+
this.#enterPlayer = this.#engine.play(this.#host.nativeElement, resolveMoveFrames(enterInput, 'enter'), {
|
|
520
|
+
config: this.#config,
|
|
521
|
+
spring: this.moveSpring(),
|
|
522
|
+
disabled: this.#config.disabled,
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
ngOnDestroy() {
|
|
527
|
+
this.#stagger?.unregister(this.#host.nativeElement);
|
|
528
|
+
this.#presence?.unregister(this);
|
|
529
|
+
this.#enterPlayer?.cancel();
|
|
530
|
+
this.#leavePlayer?.cancel();
|
|
531
|
+
}
|
|
532
|
+
playLeave() {
|
|
533
|
+
if (this.#config.disabled) {
|
|
534
|
+
return Promise.resolve();
|
|
535
|
+
}
|
|
536
|
+
this.#enterPlayer?.cancel();
|
|
537
|
+
this.#leavePlayer = this.#engine.play(this.#host.nativeElement, resolveMoveFrames(this.resolveLeaveInput(), 'leave'), {
|
|
538
|
+
config: this.#config,
|
|
539
|
+
spring: this.moveSpring(),
|
|
540
|
+
disabled: false,
|
|
541
|
+
});
|
|
542
|
+
return this.#leavePlayer?.finished ?? Promise.resolve();
|
|
543
|
+
}
|
|
544
|
+
resolveEnterInput() {
|
|
545
|
+
return this.moveAnimate() ?? this.move() ?? 'none';
|
|
546
|
+
}
|
|
547
|
+
resolveLeaveInput() {
|
|
548
|
+
return this.moveAnimateLeave() ?? this.resolveEnterInput();
|
|
549
|
+
}
|
|
550
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveAnimateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
551
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveAnimateDirective, isStandalone: true, selector: "[move],[moveAnimate]", inputs: { move: { classPropertyName: "move", publicName: "move", isSignal: true, isRequired: false, transformFunction: null }, moveAnimate: { classPropertyName: "moveAnimate", publicName: "moveAnimate", isSignal: true, isRequired: false, transformFunction: null }, moveAnimateLeave: { classPropertyName: "moveAnimateLeave", publicName: "moveAnimateLeave", isSignal: true, isRequired: false, transformFunction: null }, moveDuration: { classPropertyName: "moveDuration", publicName: "moveDuration", isSignal: true, isRequired: false, transformFunction: null }, moveEasing: { classPropertyName: "moveEasing", publicName: "moveEasing", isSignal: true, isRequired: false, transformFunction: null }, moveDelay: { classPropertyName: "moveDelay", publicName: "moveDelay", isSignal: true, isRequired: false, transformFunction: null }, moveDisabled: { classPropertyName: "moveDisabled", publicName: "moveDisabled", isSignal: true, isRequired: false, transformFunction: null }, moveSpring: { classPropertyName: "moveSpring", publicName: "moveSpring", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
552
|
+
}
|
|
553
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveAnimateDirective, decorators: [{
|
|
554
|
+
type: Directive,
|
|
555
|
+
args: [{
|
|
556
|
+
selector: '[move],[moveAnimate]',
|
|
557
|
+
}]
|
|
558
|
+
}], propDecorators: { move: [{ type: i0.Input, args: [{ isSignal: true, alias: "move", required: false }] }], moveAnimate: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveAnimate", required: false }] }], moveAnimateLeave: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveAnimateLeave", required: false }] }], moveDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDuration", required: false }] }], moveEasing: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveEasing", required: false }] }], moveDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDelay", required: false }] }], moveDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDisabled", required: false }] }], moveSpring: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSpring", required: false }] }] } });
|
|
559
|
+
|
|
560
|
+
class MoveAnimationDirective {
|
|
561
|
+
moveAnimation = input.required(...(ngDevMode ? [{ debugName: "moveAnimation" }] : /* istanbul ignore next */ []));
|
|
562
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
563
|
+
#documentRef = inject(DOCUMENT);
|
|
564
|
+
#host = inject((ElementRef));
|
|
565
|
+
#engine = inject(AnimationEngine);
|
|
566
|
+
#stagger = inject(MOVE_STAGGER_PARENT, { optional: true });
|
|
567
|
+
#presence = inject(MOVE_PRESENCE_PARENT, { optional: true });
|
|
568
|
+
#config = this.#defaults;
|
|
569
|
+
#enterPlayer = null;
|
|
570
|
+
#leavePlayer = null;
|
|
571
|
+
ngOnInit() {
|
|
572
|
+
this.#stagger?.register(this.#host.nativeElement);
|
|
573
|
+
this.#presence?.register(this);
|
|
574
|
+
Promise.resolve().then(() => {
|
|
575
|
+
const cfg = this.moveAnimation();
|
|
576
|
+
const staggerDelay = this.#stagger?.getDelay(this.#host.nativeElement) ?? 0;
|
|
577
|
+
this.#config = resolveMovementConfig(this.#defaults, {
|
|
578
|
+
duration: cfg.duration,
|
|
579
|
+
easing: cfg.easing,
|
|
580
|
+
delay: (cfg.delay ?? 0) + staggerDelay,
|
|
581
|
+
disabled: undefined,
|
|
582
|
+
}, prefersReducedMotion(this.#documentRef));
|
|
583
|
+
if (!cfg.initial || !cfg.animate)
|
|
584
|
+
return;
|
|
585
|
+
const frames = statesToKeyframes(cfg.initial, cfg.animate);
|
|
586
|
+
if (Object.keys(frames).length === 0)
|
|
587
|
+
return;
|
|
588
|
+
this.#enterPlayer = this.#engine.play(this.#host.nativeElement, frames, {
|
|
589
|
+
config: this.#config,
|
|
590
|
+
spring: cfg.spring,
|
|
591
|
+
disabled: this.#config.disabled,
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
ngOnDestroy() {
|
|
596
|
+
this.#stagger?.unregister(this.#host.nativeElement);
|
|
597
|
+
this.#presence?.unregister(this);
|
|
598
|
+
this.#enterPlayer?.cancel();
|
|
599
|
+
this.#leavePlayer?.cancel();
|
|
600
|
+
}
|
|
601
|
+
playLeave() {
|
|
602
|
+
const cfg = this.moveAnimation();
|
|
603
|
+
if (this.#config.disabled || !cfg.exit || !cfg.animate) {
|
|
604
|
+
return Promise.resolve();
|
|
605
|
+
}
|
|
606
|
+
this.#enterPlayer?.cancel();
|
|
607
|
+
const frames = statesToKeyframes(cfg.animate, cfg.exit);
|
|
608
|
+
if (Object.keys(frames).length === 0)
|
|
609
|
+
return Promise.resolve();
|
|
610
|
+
this.#leavePlayer = this.#engine.play(this.#host.nativeElement, frames, {
|
|
611
|
+
config: this.#config,
|
|
612
|
+
spring: cfg.spring,
|
|
613
|
+
disabled: false,
|
|
614
|
+
});
|
|
615
|
+
return this.#leavePlayer?.finished ?? Promise.resolve();
|
|
616
|
+
}
|
|
617
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveAnimationDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
618
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveAnimationDirective, isStandalone: true, selector: "[moveAnimation]", inputs: { moveAnimation: { classPropertyName: "moveAnimation", publicName: "moveAnimation", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
619
|
+
}
|
|
620
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveAnimationDirective, decorators: [{
|
|
621
|
+
type: Directive,
|
|
622
|
+
args: [{
|
|
623
|
+
selector: '[moveAnimation]',
|
|
624
|
+
}]
|
|
625
|
+
}], propDecorators: { moveAnimation: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveAnimation", required: true }] }] } });
|
|
626
|
+
function statesToKeyframes(from, to) {
|
|
627
|
+
const result = {};
|
|
628
|
+
for (const key of Object.keys(from)) {
|
|
629
|
+
const f = from[key];
|
|
630
|
+
const t = to[key];
|
|
631
|
+
if (f !== undefined && t !== undefined) {
|
|
632
|
+
result[key] = [f, t];
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return result;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
class MoveEnterDirective {
|
|
639
|
+
moveEnter = input('none', ...(ngDevMode ? [{ debugName: "moveEnter" }] : /* istanbul ignore next */ []));
|
|
640
|
+
moveDuration = input(undefined, ...(ngDevMode ? [{ debugName: "moveDuration" }] : /* istanbul ignore next */ []));
|
|
641
|
+
moveEasing = input(undefined, ...(ngDevMode ? [{ debugName: "moveEasing" }] : /* istanbul ignore next */ []));
|
|
642
|
+
moveDelay = input(undefined, ...(ngDevMode ? [{ debugName: "moveDelay" }] : /* istanbul ignore next */ []));
|
|
643
|
+
moveDisabled = input(undefined, ...(ngDevMode ? [{ debugName: "moveDisabled" }] : /* istanbul ignore next */ []));
|
|
644
|
+
moveSpring = input(undefined, ...(ngDevMode ? [{ debugName: "moveSpring" }] : /* istanbul ignore next */ []));
|
|
645
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
646
|
+
#documentRef = inject(DOCUMENT);
|
|
647
|
+
#host = inject((ElementRef));
|
|
648
|
+
#engine = inject(AnimationEngine);
|
|
649
|
+
#stagger = inject(MOVE_STAGGER_PARENT, { optional: true });
|
|
650
|
+
#player = null;
|
|
651
|
+
ngOnInit() {
|
|
652
|
+
this.#stagger?.register(this.#host.nativeElement);
|
|
653
|
+
Promise.resolve().then(() => {
|
|
654
|
+
const staggerDelay = this.#stagger?.getDelay(this.#host.nativeElement) ?? 0;
|
|
655
|
+
const config = resolveMovementConfig(this.#defaults, {
|
|
656
|
+
duration: this.moveDuration(),
|
|
657
|
+
easing: this.moveEasing(),
|
|
658
|
+
delay: (this.moveDelay() ?? 0) + staggerDelay,
|
|
659
|
+
disabled: this.moveDisabled(),
|
|
660
|
+
}, prefersReducedMotion(this.#documentRef));
|
|
661
|
+
this.#player = this.#engine.play(this.#host.nativeElement, resolveMoveFrames(this.moveEnter(), 'enter'), {
|
|
662
|
+
config: config,
|
|
663
|
+
spring: this.moveSpring(),
|
|
664
|
+
disabled: config.disabled,
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
ngOnDestroy() {
|
|
669
|
+
this.#stagger?.unregister(this.#host.nativeElement);
|
|
670
|
+
this.#player?.cancel();
|
|
671
|
+
}
|
|
672
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveEnterDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
673
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveEnterDirective, isStandalone: true, selector: "[moveEnter]", inputs: { moveEnter: { classPropertyName: "moveEnter", publicName: "moveEnter", isSignal: true, isRequired: false, transformFunction: null }, moveDuration: { classPropertyName: "moveDuration", publicName: "moveDuration", isSignal: true, isRequired: false, transformFunction: null }, moveEasing: { classPropertyName: "moveEasing", publicName: "moveEasing", isSignal: true, isRequired: false, transformFunction: null }, moveDelay: { classPropertyName: "moveDelay", publicName: "moveDelay", isSignal: true, isRequired: false, transformFunction: null }, moveDisabled: { classPropertyName: "moveDisabled", publicName: "moveDisabled", isSignal: true, isRequired: false, transformFunction: null }, moveSpring: { classPropertyName: "moveSpring", publicName: "moveSpring", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
674
|
+
}
|
|
675
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveEnterDirective, decorators: [{
|
|
676
|
+
type: Directive,
|
|
677
|
+
args: [{
|
|
678
|
+
selector: '[moveEnter]',
|
|
679
|
+
}]
|
|
680
|
+
}], propDecorators: { moveEnter: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveEnter", required: false }] }], moveDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDuration", required: false }] }], moveEasing: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveEasing", required: false }] }], moveDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDelay", required: false }] }], moveDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDisabled", required: false }] }], moveSpring: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSpring", required: false }] }] } });
|
|
681
|
+
|
|
682
|
+
class MoveLeaveDirective {
|
|
683
|
+
moveLeave = input('none', ...(ngDevMode ? [{ debugName: "moveLeave" }] : /* istanbul ignore next */ []));
|
|
684
|
+
moveDuration = input(undefined, ...(ngDevMode ? [{ debugName: "moveDuration" }] : /* istanbul ignore next */ []));
|
|
685
|
+
moveEasing = input(undefined, ...(ngDevMode ? [{ debugName: "moveEasing" }] : /* istanbul ignore next */ []));
|
|
686
|
+
moveDelay = input(undefined, ...(ngDevMode ? [{ debugName: "moveDelay" }] : /* istanbul ignore next */ []));
|
|
687
|
+
moveDisabled = input(undefined, ...(ngDevMode ? [{ debugName: "moveDisabled" }] : /* istanbul ignore next */ []));
|
|
688
|
+
moveSpring = input(undefined, ...(ngDevMode ? [{ debugName: "moveSpring" }] : /* istanbul ignore next */ []));
|
|
689
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
690
|
+
#documentRef = inject(DOCUMENT);
|
|
691
|
+
#host = inject((ElementRef));
|
|
692
|
+
#engine = inject(AnimationEngine);
|
|
693
|
+
#presence = inject(MOVE_PRESENCE_PARENT, { optional: true });
|
|
694
|
+
#config = this.#defaults;
|
|
695
|
+
#player = null;
|
|
696
|
+
ngOnInit() {
|
|
697
|
+
this.#presence?.register(this);
|
|
698
|
+
this.#config = resolveMovementConfig(this.#defaults, {
|
|
699
|
+
duration: this.moveDuration(),
|
|
700
|
+
easing: this.moveEasing(),
|
|
701
|
+
delay: this.moveDelay(),
|
|
702
|
+
disabled: this.moveDisabled(),
|
|
703
|
+
}, prefersReducedMotion(this.#documentRef));
|
|
704
|
+
}
|
|
705
|
+
ngOnDestroy() {
|
|
706
|
+
this.#presence?.unregister(this);
|
|
707
|
+
this.#player?.cancel();
|
|
708
|
+
}
|
|
709
|
+
playLeave() {
|
|
710
|
+
if (this.#config.disabled) {
|
|
711
|
+
return Promise.resolve();
|
|
712
|
+
}
|
|
713
|
+
this.#player = this.#engine.play(this.#host.nativeElement, resolveMoveFrames(this.moveLeave(), 'leave'), {
|
|
714
|
+
config: this.#config,
|
|
715
|
+
spring: this.moveSpring(),
|
|
716
|
+
disabled: false,
|
|
717
|
+
});
|
|
718
|
+
return this.#player?.finished ?? Promise.resolve();
|
|
719
|
+
}
|
|
720
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveLeaveDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
721
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveLeaveDirective, isStandalone: true, selector: "[moveLeave]", inputs: { moveLeave: { classPropertyName: "moveLeave", publicName: "moveLeave", isSignal: true, isRequired: false, transformFunction: null }, moveDuration: { classPropertyName: "moveDuration", publicName: "moveDuration", isSignal: true, isRequired: false, transformFunction: null }, moveEasing: { classPropertyName: "moveEasing", publicName: "moveEasing", isSignal: true, isRequired: false, transformFunction: null }, moveDelay: { classPropertyName: "moveDelay", publicName: "moveDelay", isSignal: true, isRequired: false, transformFunction: null }, moveDisabled: { classPropertyName: "moveDisabled", publicName: "moveDisabled", isSignal: true, isRequired: false, transformFunction: null }, moveSpring: { classPropertyName: "moveSpring", publicName: "moveSpring", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
722
|
+
}
|
|
723
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveLeaveDirective, decorators: [{
|
|
724
|
+
type: Directive,
|
|
725
|
+
args: [{
|
|
726
|
+
selector: '[moveLeave]',
|
|
727
|
+
}]
|
|
728
|
+
}], propDecorators: { moveLeave: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveLeave", required: false }] }], moveDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDuration", required: false }] }], moveEasing: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveEasing", required: false }] }], moveDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDelay", required: false }] }], moveDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDisabled", required: false }] }], moveSpring: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSpring", required: false }] }] } });
|
|
729
|
+
|
|
730
|
+
class MoveHoverDirective {
|
|
731
|
+
moveWhileHover = input.required(...(ngDevMode ? [{ debugName: "moveWhileHover" }] : /* istanbul ignore next */ []));
|
|
732
|
+
moveDuration = input(undefined, ...(ngDevMode ? [{ debugName: "moveDuration" }] : /* istanbul ignore next */ []));
|
|
733
|
+
moveEasing = input(undefined, ...(ngDevMode ? [{ debugName: "moveEasing" }] : /* istanbul ignore next */ []));
|
|
734
|
+
moveDelay = input(undefined, ...(ngDevMode ? [{ debugName: "moveDelay" }] : /* istanbul ignore next */ []));
|
|
735
|
+
moveDisabled = input(undefined, ...(ngDevMode ? [{ debugName: "moveDisabled" }] : /* istanbul ignore next */ []));
|
|
736
|
+
moveSpring = input(undefined, ...(ngDevMode ? [{ debugName: "moveSpring" }] : /* istanbul ignore next */ []));
|
|
737
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
738
|
+
#documentRef = inject(DOCUMENT);
|
|
739
|
+
#host = inject((ElementRef));
|
|
740
|
+
#engine = inject(AnimationEngine);
|
|
741
|
+
#currentPlayer = null;
|
|
742
|
+
#isHovered = false;
|
|
743
|
+
onMouseEnter() {
|
|
744
|
+
if (this.#isHovered)
|
|
745
|
+
return;
|
|
746
|
+
this.#isHovered = true;
|
|
747
|
+
this.play(false);
|
|
748
|
+
}
|
|
749
|
+
onMouseLeave() {
|
|
750
|
+
if (!this.#isHovered)
|
|
751
|
+
return;
|
|
752
|
+
this.#isHovered = false;
|
|
753
|
+
this.play(true);
|
|
754
|
+
}
|
|
755
|
+
onTouchStart(event) {
|
|
756
|
+
event.preventDefault();
|
|
757
|
+
if (this.#isHovered)
|
|
758
|
+
return;
|
|
759
|
+
this.#isHovered = true;
|
|
760
|
+
this.play(false);
|
|
761
|
+
}
|
|
762
|
+
onTouchEnd() {
|
|
763
|
+
if (!this.#isHovered)
|
|
764
|
+
return;
|
|
765
|
+
this.#isHovered = false;
|
|
766
|
+
this.play(true);
|
|
767
|
+
}
|
|
768
|
+
play(reverse) {
|
|
769
|
+
this.#currentPlayer?.cancel();
|
|
770
|
+
const isReduced = prefersReducedMotion(this.#documentRef);
|
|
771
|
+
const config = resolveMovementConfig(this.#defaults, {
|
|
772
|
+
duration: this.moveDuration(),
|
|
773
|
+
easing: this.moveEasing(),
|
|
774
|
+
delay: this.moveDelay(),
|
|
775
|
+
disabled: this.moveDisabled(),
|
|
776
|
+
}, isReduced);
|
|
777
|
+
if (config.disabled)
|
|
778
|
+
return;
|
|
779
|
+
let frames = resolveMoveFrames(this.moveWhileHover(), 'enter');
|
|
780
|
+
if (reverse) {
|
|
781
|
+
frames = reverseFrames(frames);
|
|
782
|
+
}
|
|
783
|
+
this.#currentPlayer = this.#engine.play(this.#host.nativeElement, frames, {
|
|
784
|
+
config,
|
|
785
|
+
spring: this.moveSpring(),
|
|
786
|
+
disabled: false,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
ngOnDestroy() {
|
|
790
|
+
this.#currentPlayer?.cancel();
|
|
791
|
+
}
|
|
792
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveHoverDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
793
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveHoverDirective, isStandalone: true, selector: "[moveWhileHover]", inputs: { moveWhileHover: { classPropertyName: "moveWhileHover", publicName: "moveWhileHover", isSignal: true, isRequired: true, transformFunction: null }, moveDuration: { classPropertyName: "moveDuration", publicName: "moveDuration", isSignal: true, isRequired: false, transformFunction: null }, moveEasing: { classPropertyName: "moveEasing", publicName: "moveEasing", isSignal: true, isRequired: false, transformFunction: null }, moveDelay: { classPropertyName: "moveDelay", publicName: "moveDelay", isSignal: true, isRequired: false, transformFunction: null }, moveDisabled: { classPropertyName: "moveDisabled", publicName: "moveDisabled", isSignal: true, isRequired: false, transformFunction: null }, moveSpring: { classPropertyName: "moveSpring", publicName: "moveSpring", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()", "touchstart": "onTouchStart($event)", "touchend": "onTouchEnd()", "touchcancel": "onTouchEnd()" } }, ngImport: i0 });
|
|
794
|
+
}
|
|
795
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveHoverDirective, decorators: [{
|
|
796
|
+
type: Directive,
|
|
797
|
+
args: [{
|
|
798
|
+
selector: '[moveWhileHover]',
|
|
799
|
+
host: {
|
|
800
|
+
'(mouseenter)': 'onMouseEnter()',
|
|
801
|
+
'(mouseleave)': 'onMouseLeave()',
|
|
802
|
+
'(touchstart)': 'onTouchStart($event)',
|
|
803
|
+
'(touchend)': 'onTouchEnd()',
|
|
804
|
+
'(touchcancel)': 'onTouchEnd()',
|
|
805
|
+
},
|
|
806
|
+
}]
|
|
807
|
+
}], propDecorators: { moveWhileHover: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveWhileHover", required: true }] }], moveDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDuration", required: false }] }], moveEasing: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveEasing", required: false }] }], moveDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDelay", required: false }] }], moveDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDisabled", required: false }] }], moveSpring: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSpring", required: false }] }] } });
|
|
808
|
+
|
|
809
|
+
class MoveTapDirective {
|
|
810
|
+
moveWhileTap = input.required(...(ngDevMode ? [{ debugName: "moveWhileTap" }] : /* istanbul ignore next */ []));
|
|
811
|
+
moveDuration = input(undefined, ...(ngDevMode ? [{ debugName: "moveDuration" }] : /* istanbul ignore next */ []));
|
|
812
|
+
moveEasing = input(undefined, ...(ngDevMode ? [{ debugName: "moveEasing" }] : /* istanbul ignore next */ []));
|
|
813
|
+
moveDelay = input(undefined, ...(ngDevMode ? [{ debugName: "moveDelay" }] : /* istanbul ignore next */ []));
|
|
814
|
+
moveDisabled = input(undefined, ...(ngDevMode ? [{ debugName: "moveDisabled" }] : /* istanbul ignore next */ []));
|
|
815
|
+
moveSpring = input(undefined, ...(ngDevMode ? [{ debugName: "moveSpring" }] : /* istanbul ignore next */ []));
|
|
816
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
817
|
+
#documentRef = inject(DOCUMENT);
|
|
818
|
+
#host = inject((ElementRef));
|
|
819
|
+
#engine = inject(AnimationEngine);
|
|
820
|
+
#currentPlayer = null;
|
|
821
|
+
#isTapped = false;
|
|
822
|
+
onPointerDown() {
|
|
823
|
+
if (this.#isTapped)
|
|
824
|
+
return;
|
|
825
|
+
this.#isTapped = true;
|
|
826
|
+
this.play(false);
|
|
827
|
+
}
|
|
828
|
+
onPointerUp() {
|
|
829
|
+
if (!this.#isTapped)
|
|
830
|
+
return;
|
|
831
|
+
this.#isTapped = false;
|
|
832
|
+
this.play(true);
|
|
833
|
+
}
|
|
834
|
+
play(reverse) {
|
|
835
|
+
this.#currentPlayer?.cancel();
|
|
836
|
+
const isReduced = prefersReducedMotion(this.#documentRef);
|
|
837
|
+
const config = resolveMovementConfig(this.#defaults, {
|
|
838
|
+
duration: this.moveDuration(),
|
|
839
|
+
easing: this.moveEasing(),
|
|
840
|
+
delay: this.moveDelay(),
|
|
841
|
+
disabled: this.moveDisabled(),
|
|
842
|
+
}, isReduced);
|
|
843
|
+
if (config.disabled)
|
|
844
|
+
return;
|
|
845
|
+
let frames = resolveMoveFrames(this.moveWhileTap(), 'enter');
|
|
846
|
+
if (reverse) {
|
|
847
|
+
frames = reverseFrames(frames);
|
|
848
|
+
}
|
|
849
|
+
this.#currentPlayer = this.#engine.play(this.#host.nativeElement, frames, {
|
|
850
|
+
config,
|
|
851
|
+
spring: this.moveSpring(),
|
|
852
|
+
disabled: false,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
ngOnDestroy() {
|
|
856
|
+
this.#currentPlayer?.cancel();
|
|
857
|
+
}
|
|
858
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveTapDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
859
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveTapDirective, isStandalone: true, selector: "[moveWhileTap]", inputs: { moveWhileTap: { classPropertyName: "moveWhileTap", publicName: "moveWhileTap", isSignal: true, isRequired: true, transformFunction: null }, moveDuration: { classPropertyName: "moveDuration", publicName: "moveDuration", isSignal: true, isRequired: false, transformFunction: null }, moveEasing: { classPropertyName: "moveEasing", publicName: "moveEasing", isSignal: true, isRequired: false, transformFunction: null }, moveDelay: { classPropertyName: "moveDelay", publicName: "moveDelay", isSignal: true, isRequired: false, transformFunction: null }, moveDisabled: { classPropertyName: "moveDisabled", publicName: "moveDisabled", isSignal: true, isRequired: false, transformFunction: null }, moveSpring: { classPropertyName: "moveSpring", publicName: "moveSpring", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "pointerdown": "onPointerDown()", "pointerup": "onPointerUp()", "pointercancel": "onPointerUp()", "pointerleave": "onPointerUp()" } }, ngImport: i0 });
|
|
860
|
+
}
|
|
861
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveTapDirective, decorators: [{
|
|
862
|
+
type: Directive,
|
|
863
|
+
args: [{
|
|
864
|
+
selector: '[moveWhileTap]',
|
|
865
|
+
host: {
|
|
866
|
+
'(pointerdown)': 'onPointerDown()',
|
|
867
|
+
'(pointerup)': 'onPointerUp()',
|
|
868
|
+
'(pointercancel)': 'onPointerUp()',
|
|
869
|
+
'(pointerleave)': 'onPointerUp()',
|
|
870
|
+
},
|
|
871
|
+
}]
|
|
872
|
+
}], propDecorators: { moveWhileTap: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveWhileTap", required: true }] }], moveDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDuration", required: false }] }], moveEasing: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveEasing", required: false }] }], moveDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDelay", required: false }] }], moveDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDisabled", required: false }] }], moveSpring: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSpring", required: false }] }] } });
|
|
873
|
+
|
|
874
|
+
const MOVE_VARIANTS_PARENT = new InjectionToken('MOVE_VARIANTS_PARENT');
|
|
875
|
+
class MoveVariantsDirective {
|
|
876
|
+
moveVariants = input.required(...(ngDevMode ? [{ debugName: "moveVariants" }] : /* istanbul ignore next */ []));
|
|
877
|
+
moveAnimate = input(undefined, ...(ngDevMode ? [{ debugName: "moveAnimate" }] : /* istanbul ignore next */ []));
|
|
878
|
+
moveDuration = input(undefined, ...(ngDevMode ? [{ debugName: "moveDuration" }] : /* istanbul ignore next */ []));
|
|
879
|
+
moveEasing = input(undefined, ...(ngDevMode ? [{ debugName: "moveEasing" }] : /* istanbul ignore next */ []));
|
|
880
|
+
moveDelay = input(undefined, ...(ngDevMode ? [{ debugName: "moveDelay" }] : /* istanbul ignore next */ []));
|
|
881
|
+
moveDisabled = input(undefined, ...(ngDevMode ? [{ debugName: "moveDisabled" }] : /* istanbul ignore next */ []));
|
|
882
|
+
moveSpring = input(undefined, ...(ngDevMode ? [{ debugName: "moveSpring" }] : /* istanbul ignore next */ []));
|
|
883
|
+
#parent = inject(MOVE_VARIANTS_PARENT, { optional: true, skipSelf: true });
|
|
884
|
+
#engine = inject(AnimationEngine);
|
|
885
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
886
|
+
#documentRef = inject(DOCUMENT);
|
|
887
|
+
#host = inject((ElementRef));
|
|
888
|
+
#stagger = inject(MOVE_STAGGER_PARENT, { optional: true });
|
|
889
|
+
#currentPlayer = null;
|
|
890
|
+
#isReducedMotion = false;
|
|
891
|
+
activeVariant = computed(() => {
|
|
892
|
+
return this.moveAnimate() ?? this.#parent?.activeVariant();
|
|
893
|
+
}, ...(ngDevMode ? [{ debugName: "activeVariant" }] : /* istanbul ignore next */ []));
|
|
894
|
+
constructor() {
|
|
895
|
+
this.#isReducedMotion = prefersReducedMotion(this.#documentRef);
|
|
896
|
+
this.#stagger?.register(this.#host.nativeElement);
|
|
897
|
+
effect(() => {
|
|
898
|
+
const variantName = this.activeVariant();
|
|
899
|
+
if (!variantName)
|
|
900
|
+
return;
|
|
901
|
+
const variants = this.moveVariants();
|
|
902
|
+
if (!variants)
|
|
903
|
+
return;
|
|
904
|
+
const state = variants[variantName];
|
|
905
|
+
if (!state)
|
|
906
|
+
return;
|
|
907
|
+
this.#currentPlayer?.cancel();
|
|
908
|
+
const { spring, duration, easing, delay, ...keyframesMap } = state;
|
|
909
|
+
const keyframes = keyframesMap;
|
|
910
|
+
const staggerDelay = this.#stagger?.getDelay(this.#host.nativeElement) ?? 0;
|
|
911
|
+
const config = resolveMovementConfig(this.#defaults, {
|
|
912
|
+
duration: duration ?? this.moveDuration(),
|
|
913
|
+
easing: easing ?? this.moveEasing(),
|
|
914
|
+
delay: (delay ?? this.moveDelay() ?? 0) + staggerDelay,
|
|
915
|
+
disabled: this.moveDisabled(),
|
|
916
|
+
}, this.#isReducedMotion);
|
|
917
|
+
this.#currentPlayer = this.#engine.play(this.#host.nativeElement, keyframes, {
|
|
918
|
+
config,
|
|
919
|
+
spring: spring ?? this.moveSpring(),
|
|
920
|
+
disabled: config.disabled,
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
ngOnDestroy() {
|
|
925
|
+
this.#stagger?.unregister(this.#host.nativeElement);
|
|
926
|
+
this.#currentPlayer?.cancel();
|
|
927
|
+
}
|
|
928
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveVariantsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
929
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveVariantsDirective, isStandalone: true, selector: "[moveVariants]", inputs: { moveVariants: { classPropertyName: "moveVariants", publicName: "moveVariants", isSignal: true, isRequired: true, transformFunction: null }, moveAnimate: { classPropertyName: "moveAnimate", publicName: "moveAnimate", isSignal: true, isRequired: false, transformFunction: null }, moveDuration: { classPropertyName: "moveDuration", publicName: "moveDuration", isSignal: true, isRequired: false, transformFunction: null }, moveEasing: { classPropertyName: "moveEasing", publicName: "moveEasing", isSignal: true, isRequired: false, transformFunction: null }, moveDelay: { classPropertyName: "moveDelay", publicName: "moveDelay", isSignal: true, isRequired: false, transformFunction: null }, moveDisabled: { classPropertyName: "moveDisabled", publicName: "moveDisabled", isSignal: true, isRequired: false, transformFunction: null }, moveSpring: { classPropertyName: "moveSpring", publicName: "moveSpring", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
930
|
+
{
|
|
931
|
+
provide: MOVE_VARIANTS_PARENT,
|
|
932
|
+
useExisting: forwardRef(() => MoveVariantsDirective),
|
|
933
|
+
},
|
|
934
|
+
], ngImport: i0 });
|
|
935
|
+
}
|
|
936
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveVariantsDirective, decorators: [{
|
|
937
|
+
type: Directive,
|
|
938
|
+
args: [{
|
|
939
|
+
selector: '[moveVariants]',
|
|
940
|
+
providers: [
|
|
941
|
+
{
|
|
942
|
+
provide: MOVE_VARIANTS_PARENT,
|
|
943
|
+
useExisting: forwardRef(() => MoveVariantsDirective),
|
|
944
|
+
},
|
|
945
|
+
],
|
|
946
|
+
}]
|
|
947
|
+
}], ctorParameters: () => [], propDecorators: { moveVariants: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveVariants", required: true }] }], moveAnimate: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveAnimate", required: false }] }], moveDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDuration", required: false }] }], moveEasing: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveEasing", required: false }] }], moveDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDelay", required: false }] }], moveDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDisabled", required: false }] }], moveSpring: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSpring", required: false }] }] } });
|
|
948
|
+
|
|
949
|
+
class MoveStaggerDirective {
|
|
950
|
+
moveStagger = input.required(...(ngDevMode ? [{ debugName: "moveStagger" }] : /* istanbul ignore next */ []));
|
|
951
|
+
moveStaggerDirection = input('first', ...(ngDevMode ? [{ debugName: "moveStaggerDirection" }] : /* istanbul ignore next */ []));
|
|
952
|
+
#children = new Set();
|
|
953
|
+
register(el) {
|
|
954
|
+
this.#children.add(el);
|
|
955
|
+
}
|
|
956
|
+
unregister(el) {
|
|
957
|
+
this.#children.delete(el);
|
|
958
|
+
}
|
|
959
|
+
getDelay(el) {
|
|
960
|
+
if (!this.#children.has(el))
|
|
961
|
+
return 0;
|
|
962
|
+
const list = Array.from(this.#children).sort((a, b) => {
|
|
963
|
+
// Unreliable on detached elements, but they are in DOM when sorting
|
|
964
|
+
const pos = a.compareDocumentPosition(b);
|
|
965
|
+
return pos & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1;
|
|
966
|
+
});
|
|
967
|
+
const index = list.indexOf(el);
|
|
968
|
+
if (index === -1)
|
|
969
|
+
return 0;
|
|
970
|
+
const staggerConfig = this.moveStagger();
|
|
971
|
+
// If it's a spring config but used for stagger, default to 100ms or try to derive.
|
|
972
|
+
const staggerTime = typeof staggerConfig === 'number' ? staggerConfig : 100;
|
|
973
|
+
const direction = this.moveStaggerDirection();
|
|
974
|
+
const total = list.length;
|
|
975
|
+
let staggerIndex = index;
|
|
976
|
+
if (direction === 'last') {
|
|
977
|
+
staggerIndex = total - 1 - index;
|
|
978
|
+
}
|
|
979
|
+
else if (direction === 'center') {
|
|
980
|
+
const center = (total - 1) / 2;
|
|
981
|
+
staggerIndex = Math.abs(index - center);
|
|
982
|
+
}
|
|
983
|
+
return staggerIndex * staggerTime;
|
|
984
|
+
}
|
|
985
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveStaggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
986
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveStaggerDirective, isStandalone: true, selector: "[moveStagger]", inputs: { moveStagger: { classPropertyName: "moveStagger", publicName: "moveStagger", isSignal: true, isRequired: true, transformFunction: null }, moveStaggerDirection: { classPropertyName: "moveStaggerDirection", publicName: "moveStaggerDirection", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
987
|
+
{
|
|
988
|
+
provide: MOVE_STAGGER_PARENT,
|
|
989
|
+
useExisting: forwardRef(() => MoveStaggerDirective),
|
|
990
|
+
},
|
|
991
|
+
], ngImport: i0 });
|
|
992
|
+
}
|
|
993
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveStaggerDirective, decorators: [{
|
|
994
|
+
type: Directive,
|
|
995
|
+
args: [{
|
|
996
|
+
selector: '[moveStagger]',
|
|
997
|
+
providers: [
|
|
998
|
+
{
|
|
999
|
+
provide: MOVE_STAGGER_PARENT,
|
|
1000
|
+
useExisting: forwardRef(() => MoveStaggerDirective),
|
|
1001
|
+
},
|
|
1002
|
+
],
|
|
1003
|
+
}]
|
|
1004
|
+
}], propDecorators: { moveStagger: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveStagger", required: true }] }], moveStaggerDirection: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveStaggerDirection", required: false }] }] } });
|
|
1005
|
+
|
|
1006
|
+
class MoveLayoutDirective {
|
|
1007
|
+
moveLayout = input(true, ...(ngDevMode ? [{ debugName: "moveLayout" }] : /* istanbul ignore next */ []));
|
|
1008
|
+
moveLayoutId = input(undefined, ...(ngDevMode ? [{ debugName: "moveLayoutId" }] : /* istanbul ignore next */ []));
|
|
1009
|
+
moveDuration = input(undefined, ...(ngDevMode ? [{ debugName: "moveDuration" }] : /* istanbul ignore next */ []));
|
|
1010
|
+
moveEasing = input(undefined, ...(ngDevMode ? [{ debugName: "moveEasing" }] : /* istanbul ignore next */ []));
|
|
1011
|
+
moveDelay = input(undefined, ...(ngDevMode ? [{ debugName: "moveDelay" }] : /* istanbul ignore next */ []));
|
|
1012
|
+
moveDisabled = input(undefined, ...(ngDevMode ? [{ debugName: "moveDisabled" }] : /* istanbul ignore next */ []));
|
|
1013
|
+
moveSpring = input(undefined, ...(ngDevMode ? [{ debugName: "moveSpring" }] : /* istanbul ignore next */ []));
|
|
1014
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
1015
|
+
#documentRef = inject(DOCUMENT);
|
|
1016
|
+
#host = inject((ElementRef));
|
|
1017
|
+
#engine = inject(AnimationEngine);
|
|
1018
|
+
#snapshot = null;
|
|
1019
|
+
#currentPlayer = null;
|
|
1020
|
+
#isReducedMotion = false;
|
|
1021
|
+
#isAnimating = false;
|
|
1022
|
+
constructor() {
|
|
1023
|
+
this.#isReducedMotion = prefersReducedMotion(this.#documentRef);
|
|
1024
|
+
afterEveryRender({
|
|
1025
|
+
earlyRead: () => {
|
|
1026
|
+
if (this.moveLayout() === false ||
|
|
1027
|
+
this.moveDisabled() ||
|
|
1028
|
+
this.#isReducedMotion ||
|
|
1029
|
+
this.#isAnimating) {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
const currentRect = this.#host.nativeElement.getBoundingClientRect();
|
|
1033
|
+
if (this.#snapshot) {
|
|
1034
|
+
const dx = this.#snapshot.left - currentRect.left;
|
|
1035
|
+
const dy = this.#snapshot.top - currentRect.top;
|
|
1036
|
+
const dw = this.#snapshot.width / currentRect.width;
|
|
1037
|
+
const dh = this.#snapshot.height / currentRect.height;
|
|
1038
|
+
if (Math.abs(dx) > 0.5 ||
|
|
1039
|
+
Math.abs(dy) > 0.5 ||
|
|
1040
|
+
Math.abs(dw - 1) > 0.01 ||
|
|
1041
|
+
Math.abs(dh - 1) > 0.01) {
|
|
1042
|
+
return {
|
|
1043
|
+
dx,
|
|
1044
|
+
dy,
|
|
1045
|
+
dw,
|
|
1046
|
+
dh,
|
|
1047
|
+
targetRect: currentRect,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
this.#snapshot = currentRect;
|
|
1052
|
+
return null;
|
|
1053
|
+
},
|
|
1054
|
+
write: (flipData) => {
|
|
1055
|
+
if (flipData) {
|
|
1056
|
+
this.playFlip(flipData);
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
playFlip(flipData) {
|
|
1062
|
+
this.#isAnimating = true;
|
|
1063
|
+
// The host is currently visually at its NEW position (unpainted).
|
|
1064
|
+
// We apply the inverse transform to make it LOOK like it's at the OLD position.
|
|
1065
|
+
// We apply transform origin 0 0 so scaling works correctly from top-left.
|
|
1066
|
+
const transformOrigin = this.#host.nativeElement.style.transformOrigin;
|
|
1067
|
+
this.#host.nativeElement.style.transformOrigin = '0 0';
|
|
1068
|
+
const config = resolveMovementConfig(this.#defaults, {
|
|
1069
|
+
duration: this.moveDuration(),
|
|
1070
|
+
easing: this.moveEasing(),
|
|
1071
|
+
delay: this.moveDelay(),
|
|
1072
|
+
disabled: this.moveDisabled(),
|
|
1073
|
+
}, this.#isReducedMotion);
|
|
1074
|
+
this.#currentPlayer = this.#engine.play(this.#host.nativeElement, {
|
|
1075
|
+
x: [flipData.dx, 0],
|
|
1076
|
+
y: [flipData.dy, 0],
|
|
1077
|
+
scaleX: [flipData.dw, 1],
|
|
1078
|
+
scaleY: [flipData.dh, 1],
|
|
1079
|
+
}, {
|
|
1080
|
+
config,
|
|
1081
|
+
spring: this.moveSpring(),
|
|
1082
|
+
disabled: false,
|
|
1083
|
+
onDone: () => {
|
|
1084
|
+
this.#isAnimating = false;
|
|
1085
|
+
this.#host.nativeElement.style.transformOrigin = transformOrigin;
|
|
1086
|
+
// Take final snapshot so next check is fresh
|
|
1087
|
+
this.#snapshot = this.#host.nativeElement.getBoundingClientRect();
|
|
1088
|
+
},
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
ngOnDestroy() {
|
|
1092
|
+
this.#currentPlayer?.cancel();
|
|
1093
|
+
}
|
|
1094
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveLayoutDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1095
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveLayoutDirective, isStandalone: true, selector: "[moveLayout]", inputs: { moveLayout: { classPropertyName: "moveLayout", publicName: "moveLayout", isSignal: true, isRequired: false, transformFunction: null }, moveLayoutId: { classPropertyName: "moveLayoutId", publicName: "moveLayoutId", isSignal: true, isRequired: false, transformFunction: null }, moveDuration: { classPropertyName: "moveDuration", publicName: "moveDuration", isSignal: true, isRequired: false, transformFunction: null }, moveEasing: { classPropertyName: "moveEasing", publicName: "moveEasing", isSignal: true, isRequired: false, transformFunction: null }, moveDelay: { classPropertyName: "moveDelay", publicName: "moveDelay", isSignal: true, isRequired: false, transformFunction: null }, moveDisabled: { classPropertyName: "moveDisabled", publicName: "moveDisabled", isSignal: true, isRequired: false, transformFunction: null }, moveSpring: { classPropertyName: "moveSpring", publicName: "moveSpring", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1096
|
+
}
|
|
1097
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveLayoutDirective, decorators: [{
|
|
1098
|
+
type: Directive,
|
|
1099
|
+
args: [{
|
|
1100
|
+
selector: '[moveLayout]',
|
|
1101
|
+
}]
|
|
1102
|
+
}], ctorParameters: () => [], propDecorators: { moveLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveLayout", required: false }] }], moveLayoutId: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveLayoutId", required: false }] }], moveDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDuration", required: false }] }], moveEasing: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveEasing", required: false }] }], moveDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDelay", required: false }] }], moveDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDisabled", required: false }] }], moveSpring: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSpring", required: false }] }] } });
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* SmoothScrollService — Lenis-inspired smooth scroll for Angular.
|
|
1106
|
+
*
|
|
1107
|
+
* Desktop: intercepts wheel events → lerp interpolation → synthetic scrollTop.
|
|
1108
|
+
* Mobile: listens to native touch scroll → lerp interpolation → synthetic scrollTop.
|
|
1109
|
+
* Native scroll is prevented during touch to avoid the vibration conflict.
|
|
1110
|
+
*
|
|
1111
|
+
* The `scrollY` signal is updated on every RAF tick and can be consumed by
|
|
1112
|
+
* `MoveScrollDirective` (or any other consumer) to react to smooth scroll position
|
|
1113
|
+
* without relying on the native `scroll` event (which is NOT fired during smooth scroll).
|
|
1114
|
+
*
|
|
1115
|
+
* Usage (in app root or a layout service):
|
|
1116
|
+
*
|
|
1117
|
+
* constructor() {
|
|
1118
|
+
* inject(SmoothScrollService).init();
|
|
1119
|
+
* }
|
|
1120
|
+
*/
|
|
1121
|
+
class SmoothScrollService {
|
|
1122
|
+
#document = inject(DOCUMENT);
|
|
1123
|
+
#platformId = inject(PLATFORM_ID);
|
|
1124
|
+
#zone = inject(NgZone);
|
|
1125
|
+
/** Lerp factor — lower = smoother/slower, higher = snappier. Range 0–1. */
|
|
1126
|
+
#lerp = 0.1;
|
|
1127
|
+
#targetY = 0;
|
|
1128
|
+
#currentY = 0;
|
|
1129
|
+
#rafId = 0;
|
|
1130
|
+
#isRunning = false;
|
|
1131
|
+
#scrollElement = null;
|
|
1132
|
+
/**
|
|
1133
|
+
* Reactive scroll position (in pixels) updated on every RAF tick.
|
|
1134
|
+
* Use this signal instead of listening to `window.scroll` when smooth scroll is active,
|
|
1135
|
+
* since native scroll events are NOT fired during lerp-based scrolling.
|
|
1136
|
+
*/
|
|
1137
|
+
scrollY = signal(0, ...(ngDevMode ? [{ debugName: "scrollY" }] : /* istanbul ignore next */ []));
|
|
1138
|
+
// Touch tracking
|
|
1139
|
+
#touchStartY = 0;
|
|
1140
|
+
#lastTouchY = 0;
|
|
1141
|
+
#touchVelocity = 0;
|
|
1142
|
+
#isTouching = false;
|
|
1143
|
+
#lastTouchTimestamp = 0;
|
|
1144
|
+
// ─── Event handlers ────────────────────────────────────────────────────────
|
|
1145
|
+
#onWheel = (e) => {
|
|
1146
|
+
if (this.#isInsideScrollable(e.target))
|
|
1147
|
+
return;
|
|
1148
|
+
e.preventDefault();
|
|
1149
|
+
this.#targetY = this.#clamp(this.#targetY + e.deltaY);
|
|
1150
|
+
};
|
|
1151
|
+
#onTouchStart = (e) => {
|
|
1152
|
+
if (this.#isInsideScrollable(e.target))
|
|
1153
|
+
return;
|
|
1154
|
+
this.#isTouching = true;
|
|
1155
|
+
this.#touchVelocity = 0;
|
|
1156
|
+
this.#touchStartY = e.touches[0].clientY;
|
|
1157
|
+
this.#lastTouchY = this.#touchStartY;
|
|
1158
|
+
this.#lastTouchTimestamp = e.timeStamp;
|
|
1159
|
+
};
|
|
1160
|
+
#onTouchMove = (e) => {
|
|
1161
|
+
if (!this.#isTouching)
|
|
1162
|
+
return;
|
|
1163
|
+
if (this.#isInsideScrollable(e.target))
|
|
1164
|
+
return;
|
|
1165
|
+
// Prevent native scroll on the root element — we handle it ourselves
|
|
1166
|
+
e.preventDefault();
|
|
1167
|
+
const currentY = e.touches[0].clientY;
|
|
1168
|
+
const deltaTime = Math.max(e.timeStamp - this.#lastTouchTimestamp, 1);
|
|
1169
|
+
const deltaY = this.#lastTouchY - currentY; // inverted: swipe up → scroll down
|
|
1170
|
+
// Track velocity (px/ms) for momentum after lift
|
|
1171
|
+
this.#touchVelocity = deltaY / deltaTime;
|
|
1172
|
+
this.#targetY = this.#clamp(this.#targetY + deltaY);
|
|
1173
|
+
this.#lastTouchY = currentY;
|
|
1174
|
+
this.#lastTouchTimestamp = e.timeStamp;
|
|
1175
|
+
};
|
|
1176
|
+
#onTouchEnd = () => {
|
|
1177
|
+
this.#isTouching = false;
|
|
1178
|
+
// Apply momentum: fling the targetY by velocity × decay frames
|
|
1179
|
+
this.#applyMomentum();
|
|
1180
|
+
};
|
|
1181
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
1182
|
+
/** Whether the service is currently active (i.e. `init()` has been called). */
|
|
1183
|
+
get isActive() {
|
|
1184
|
+
return this.#isRunning;
|
|
1185
|
+
}
|
|
1186
|
+
init(options = {}) {
|
|
1187
|
+
if (!isPlatformBrowser(this.#platformId))
|
|
1188
|
+
return;
|
|
1189
|
+
if (this.#isRunning)
|
|
1190
|
+
return;
|
|
1191
|
+
this.#lerp = options.lerp ?? 0.1;
|
|
1192
|
+
this.#scrollElement = options.element ?? this.#document.documentElement;
|
|
1193
|
+
this.#currentY = this.#scrollElement.scrollTop;
|
|
1194
|
+
this.#targetY = this.#currentY;
|
|
1195
|
+
// Run entirely outside Angular zone to avoid change detection on every RAF
|
|
1196
|
+
this.#zone.runOutsideAngular(() => {
|
|
1197
|
+
const el = this.#scrollElement;
|
|
1198
|
+
el.addEventListener('wheel', this.#onWheel, { passive: false });
|
|
1199
|
+
// Touch events must also be non-passive to call preventDefault()
|
|
1200
|
+
el.addEventListener('touchstart', this.#onTouchStart, { passive: true });
|
|
1201
|
+
el.addEventListener('touchmove', this.#onTouchMove, { passive: false });
|
|
1202
|
+
el.addEventListener('touchend', this.#onTouchEnd, { passive: true });
|
|
1203
|
+
this.#isRunning = true;
|
|
1204
|
+
this.#tick();
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
destroy() {
|
|
1208
|
+
if (!isPlatformBrowser(this.#platformId))
|
|
1209
|
+
return;
|
|
1210
|
+
this.#isRunning = false;
|
|
1211
|
+
cancelAnimationFrame(this.#rafId);
|
|
1212
|
+
const el = this.#scrollElement;
|
|
1213
|
+
if (el) {
|
|
1214
|
+
el.removeEventListener('wheel', this.#onWheel);
|
|
1215
|
+
el.removeEventListener('touchstart', this.#onTouchStart);
|
|
1216
|
+
el.removeEventListener('touchmove', this.#onTouchMove);
|
|
1217
|
+
el.removeEventListener('touchend', this.#onTouchEnd);
|
|
1218
|
+
}
|
|
1219
|
+
this.#scrollElement = null;
|
|
1220
|
+
}
|
|
1221
|
+
/** Scroll programmatically to a Y position */
|
|
1222
|
+
scrollTo(y, instant = false) {
|
|
1223
|
+
this.#targetY = this.#clamp(y);
|
|
1224
|
+
if (instant) {
|
|
1225
|
+
this.#currentY = this.#targetY;
|
|
1226
|
+
this.#applyScroll(this.#currentY);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
ngOnDestroy() {
|
|
1230
|
+
this.destroy();
|
|
1231
|
+
}
|
|
1232
|
+
// ─── Internal ──────────────────────────────────────────────────────────────
|
|
1233
|
+
#tick() {
|
|
1234
|
+
if (!this.#isRunning)
|
|
1235
|
+
return;
|
|
1236
|
+
this.#currentY += (this.#targetY - this.#currentY) * this.#lerp;
|
|
1237
|
+
if (Math.abs(this.#targetY - this.#currentY) < 0.1) {
|
|
1238
|
+
this.#currentY = this.#targetY;
|
|
1239
|
+
}
|
|
1240
|
+
this.#applyScroll(this.#currentY);
|
|
1241
|
+
this.#rafId = requestAnimationFrame(() => this.#tick());
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* After finger lift, simulate Lenis-style momentum decay.
|
|
1245
|
+
* Velocity decays by a friction factor each frame until negligible.
|
|
1246
|
+
*/
|
|
1247
|
+
#applyMomentum() {
|
|
1248
|
+
const FRICTION = 0.92; // higher = more glide; lower = stops faster
|
|
1249
|
+
const MIN_VELOCITY = 0.05; // px/ms threshold to stop
|
|
1250
|
+
let v = this.#touchVelocity * 16; // convert px/ms → px/frame (~16ms)
|
|
1251
|
+
const step = () => {
|
|
1252
|
+
if (!this.#isRunning || Math.abs(v) < MIN_VELOCITY)
|
|
1253
|
+
return;
|
|
1254
|
+
v *= FRICTION;
|
|
1255
|
+
this.#targetY = this.#clamp(this.#targetY + v);
|
|
1256
|
+
requestAnimationFrame(step);
|
|
1257
|
+
};
|
|
1258
|
+
requestAnimationFrame(step);
|
|
1259
|
+
}
|
|
1260
|
+
#applyScroll(y) {
|
|
1261
|
+
if (!this.#scrollElement)
|
|
1262
|
+
return;
|
|
1263
|
+
this.#scrollElement.scrollTop = y;
|
|
1264
|
+
// Update the reactive signal so consumers (e.g. MoveScrollDirective) can react
|
|
1265
|
+
// without relying on native scroll events which don't fire during lerp-based scroll.
|
|
1266
|
+
this.scrollY.set(y);
|
|
1267
|
+
}
|
|
1268
|
+
#getMaxScroll() {
|
|
1269
|
+
if (!this.#scrollElement)
|
|
1270
|
+
return 0;
|
|
1271
|
+
return this.#scrollElement.scrollHeight - this.#scrollElement.clientHeight;
|
|
1272
|
+
}
|
|
1273
|
+
#clamp(y) {
|
|
1274
|
+
return Math.max(0, Math.min(y, this.#getMaxScroll()));
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Checks if the element is inside a scrollable container OTHER than the root scroll element.
|
|
1278
|
+
* Used to let native scroll handle inner overflow-y:auto containers (like Lenis does).
|
|
1279
|
+
*/
|
|
1280
|
+
#isInsideScrollable(el) {
|
|
1281
|
+
let current = el.parentElement;
|
|
1282
|
+
while (current && current !== this.#scrollElement) {
|
|
1283
|
+
const { overflowY } = getComputedStyle(current);
|
|
1284
|
+
if ((overflowY === 'auto' || overflowY === 'scroll') &&
|
|
1285
|
+
current.scrollHeight > current.clientHeight) {
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
current = current.parentElement;
|
|
1289
|
+
}
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: SmoothScrollService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1293
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: SmoothScrollService, providedIn: 'root' });
|
|
1294
|
+
}
|
|
1295
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: SmoothScrollService, decorators: [{
|
|
1296
|
+
type: Injectable,
|
|
1297
|
+
args: [{ providedIn: 'root' }]
|
|
1298
|
+
}] });
|
|
1299
|
+
|
|
1300
|
+
class MoveScrollDirective {
|
|
1301
|
+
moveScroll = input(undefined, ...(ngDevMode ? [{ debugName: "moveScroll" }] : /* istanbul ignore next */ []));
|
|
1302
|
+
moveScrollOffset = input(['0 1', '1 0'], ...(ngDevMode ? [{ debugName: "moveScrollOffset" }] : /* istanbul ignore next */ []));
|
|
1303
|
+
/** Optional CSS selector for a custom scrollable container. Defaults to window scroll. */
|
|
1304
|
+
moveScrollContainer = input(null, ...(ngDevMode ? [{ debugName: "moveScrollContainer" }] : /* istanbul ignore next */ []));
|
|
1305
|
+
progress = signal(0, ...(ngDevMode ? [{ debugName: "progress" }] : /* istanbul ignore next */ []));
|
|
1306
|
+
#documentRef = inject(DOCUMENT);
|
|
1307
|
+
#platformId = inject(PLATFORM_ID);
|
|
1308
|
+
#host = inject((ElementRef));
|
|
1309
|
+
#engine = inject(AnimationEngine);
|
|
1310
|
+
#smoothScroll = inject(SmoothScrollService, { optional: true });
|
|
1311
|
+
#player = null;
|
|
1312
|
+
#observer = null;
|
|
1313
|
+
#isVisible = false;
|
|
1314
|
+
#scrollListener = () => this.#updateProgress();
|
|
1315
|
+
#scrollTarget = null;
|
|
1316
|
+
#targetProgress = 0;
|
|
1317
|
+
#animProgress = 0;
|
|
1318
|
+
#rafId = null;
|
|
1319
|
+
constructor() {
|
|
1320
|
+
// Recreate the player whenever keyframes change (e.g. user changes effect type).
|
|
1321
|
+
// Uses duration=1000 + linear easing so that currentTime ∈ [0,1000] maps cleanly
|
|
1322
|
+
// to the [0,1] scroll progress without easing distortion or premature finish.
|
|
1323
|
+
effect(() => {
|
|
1324
|
+
if (!isPlatformBrowser(this.#platformId))
|
|
1325
|
+
return;
|
|
1326
|
+
const keyframes = this.moveScroll();
|
|
1327
|
+
if (this.#rafId !== null) {
|
|
1328
|
+
this.#documentRef.defaultView?.cancelAnimationFrame(this.#rafId);
|
|
1329
|
+
this.#rafId = null;
|
|
1330
|
+
}
|
|
1331
|
+
this.#player?.cancel();
|
|
1332
|
+
this.#player = null;
|
|
1333
|
+
this.#animProgress = 0;
|
|
1334
|
+
this.#targetProgress = 0;
|
|
1335
|
+
if (!keyframes)
|
|
1336
|
+
return;
|
|
1337
|
+
const view = this.#documentRef.defaultView;
|
|
1338
|
+
if (!view)
|
|
1339
|
+
return;
|
|
1340
|
+
this.#player = this.#engine.play(this.#host.nativeElement, keyframes, {
|
|
1341
|
+
config: { duration: 1000, easing: 'linear', delay: 0, disabled: false },
|
|
1342
|
+
});
|
|
1343
|
+
this.#player?.pause();
|
|
1344
|
+
// Sync the new player to the current scroll position immediately.
|
|
1345
|
+
this.#updateProgress();
|
|
1346
|
+
});
|
|
1347
|
+
// Smooth-scroll service support (fires via RAF signal instead of native scroll event).
|
|
1348
|
+
effect(() => {
|
|
1349
|
+
this.#smoothScroll?.scrollY();
|
|
1350
|
+
if (this.#smoothScroll?.isActive && this.#isVisible) {
|
|
1351
|
+
this.#updateProgress();
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
ngOnInit() {
|
|
1356
|
+
if (!isPlatformBrowser(this.#platformId))
|
|
1357
|
+
return;
|
|
1358
|
+
const view = this.#documentRef.defaultView;
|
|
1359
|
+
if (!view)
|
|
1360
|
+
return;
|
|
1361
|
+
// Resolve scroll target: custom container selector or window.
|
|
1362
|
+
const containerSelector = this.moveScrollContainer();
|
|
1363
|
+
const containerEl = containerSelector
|
|
1364
|
+
? this.#documentRef.querySelector(containerSelector)
|
|
1365
|
+
: null;
|
|
1366
|
+
this.#scrollTarget = containerEl ?? view;
|
|
1367
|
+
// Use the custom container as IntersectionObserver root when available so that
|
|
1368
|
+
// the observer fires based on container-viewport visibility, not page-viewport visibility.
|
|
1369
|
+
this.#observer = new IntersectionObserver((entries) => {
|
|
1370
|
+
const entry = entries[0];
|
|
1371
|
+
this.#isVisible = entry.isIntersecting;
|
|
1372
|
+
if (entry.isIntersecting) {
|
|
1373
|
+
if (!this.#smoothScroll?.isActive) {
|
|
1374
|
+
this.#scrollTarget.addEventListener('scroll', this.#scrollListener, { passive: true });
|
|
1375
|
+
}
|
|
1376
|
+
this.#updateProgress();
|
|
1377
|
+
}
|
|
1378
|
+
else {
|
|
1379
|
+
this.#scrollTarget?.removeEventListener('scroll', this.#scrollListener);
|
|
1380
|
+
}
|
|
1381
|
+
}, {
|
|
1382
|
+
root: containerEl ?? null,
|
|
1383
|
+
threshold: [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1],
|
|
1384
|
+
});
|
|
1385
|
+
this.#observer.observe(this.#host.nativeElement);
|
|
1386
|
+
}
|
|
1387
|
+
#updateProgress() {
|
|
1388
|
+
const view = this.#documentRef.defaultView;
|
|
1389
|
+
if (!view)
|
|
1390
|
+
return;
|
|
1391
|
+
const el = this.#host.nativeElement;
|
|
1392
|
+
const containerEl = this.#scrollTarget instanceof HTMLElement ? this.#scrollTarget : null;
|
|
1393
|
+
let p;
|
|
1394
|
+
if (containerEl) {
|
|
1395
|
+
// Container-relative scroll progress.
|
|
1396
|
+
const containerRect = containerEl.getBoundingClientRect();
|
|
1397
|
+
const elRect = el.getBoundingClientRect();
|
|
1398
|
+
// Element's fixed position within the scroll content (invariant to scrollTop changes).
|
|
1399
|
+
const elTop = elRect.top - containerRect.top + containerEl.scrollTop;
|
|
1400
|
+
const elHeight = el.offsetHeight;
|
|
1401
|
+
const containerHeight = containerEl.clientHeight;
|
|
1402
|
+
const offsets = this.moveScrollOffset();
|
|
1403
|
+
const startScroll = this.#calcContainerOffset(elTop, elHeight, containerHeight, offsets[0]);
|
|
1404
|
+
const endScroll = this.#calcContainerOffset(elTop, elHeight, containerHeight, offsets[1]);
|
|
1405
|
+
const range = endScroll - startScroll;
|
|
1406
|
+
if (range === 0)
|
|
1407
|
+
return;
|
|
1408
|
+
p = (containerEl.scrollTop - startScroll) / range;
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
// Viewport-relative scroll progress (window scroll).
|
|
1412
|
+
const rect = el.getBoundingClientRect();
|
|
1413
|
+
const windowHeight = view.innerHeight;
|
|
1414
|
+
const offsets = this.moveScrollOffset();
|
|
1415
|
+
const startY = this.#calcViewportOffset(rect, windowHeight, offsets[0]);
|
|
1416
|
+
const endY = this.#calcViewportOffset(rect, windowHeight, offsets[1]);
|
|
1417
|
+
const totalDistance = startY - endY;
|
|
1418
|
+
if (totalDistance === 0)
|
|
1419
|
+
return;
|
|
1420
|
+
p = startY / totalDistance;
|
|
1421
|
+
}
|
|
1422
|
+
// Cap at 0.999 to prevent the WAAPI player's `finish` event from firing,
|
|
1423
|
+
// which would cancel the animation and make it uncontrollable for further scroll.
|
|
1424
|
+
p = Math.max(0, Math.min(0.999, p));
|
|
1425
|
+
this.#targetProgress = p;
|
|
1426
|
+
this.#startRaf();
|
|
1427
|
+
}
|
|
1428
|
+
#startRaf() {
|
|
1429
|
+
if (this.#rafId !== null)
|
|
1430
|
+
return;
|
|
1431
|
+
const view = this.#documentRef.defaultView;
|
|
1432
|
+
if (!view)
|
|
1433
|
+
return;
|
|
1434
|
+
const tick = () => {
|
|
1435
|
+
const diff = this.#targetProgress - this.#animProgress;
|
|
1436
|
+
// Stop the loop when close enough (< 0.001 difference).
|
|
1437
|
+
if (Math.abs(diff) < 0.001) {
|
|
1438
|
+
this.#animProgress = this.#targetProgress;
|
|
1439
|
+
this.#rafId = null;
|
|
1440
|
+
this.#applyProgress(this.#animProgress);
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
this.#animProgress += diff * 0.12;
|
|
1444
|
+
this.#applyProgress(this.#animProgress);
|
|
1445
|
+
this.#rafId = view.requestAnimationFrame(tick);
|
|
1446
|
+
};
|
|
1447
|
+
this.#rafId = view.requestAnimationFrame(tick);
|
|
1448
|
+
}
|
|
1449
|
+
#applyProgress(p) {
|
|
1450
|
+
this.progress.set(p);
|
|
1451
|
+
if (this.#player) {
|
|
1452
|
+
this.#player.currentTime = p * 1000;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Scroll position at which the animation start/end offset is reached.
|
|
1457
|
+
* "elFraction viewFraction" → e.g. "0 1" = element-top meets container-bottom.
|
|
1458
|
+
*/
|
|
1459
|
+
#calcContainerOffset(elTop, elHeight, containerHeight, offsetStr) {
|
|
1460
|
+
const [elVal, viewVal] = offsetStr.split(' ').map(parseFloat);
|
|
1461
|
+
return elTop + elHeight * elVal - containerHeight * viewVal;
|
|
1462
|
+
}
|
|
1463
|
+
#calcViewportOffset(rect, windowHeight, offsetStr) {
|
|
1464
|
+
const [elVal, viewVal] = offsetStr.split(' ').map(parseFloat);
|
|
1465
|
+
return rect.top + rect.height * elVal - windowHeight * viewVal;
|
|
1466
|
+
}
|
|
1467
|
+
ngOnDestroy() {
|
|
1468
|
+
if (this.#rafId !== null) {
|
|
1469
|
+
this.#documentRef.defaultView?.cancelAnimationFrame(this.#rafId);
|
|
1470
|
+
this.#rafId = null;
|
|
1471
|
+
}
|
|
1472
|
+
this.#player?.cancel();
|
|
1473
|
+
this.#scrollTarget?.removeEventListener('scroll', this.#scrollListener);
|
|
1474
|
+
this.#observer?.disconnect();
|
|
1475
|
+
}
|
|
1476
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveScrollDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1477
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveScrollDirective, isStandalone: true, selector: "[moveScroll]", inputs: { moveScroll: { classPropertyName: "moveScroll", publicName: "moveScroll", isSignal: true, isRequired: false, transformFunction: null }, moveScrollOffset: { classPropertyName: "moveScrollOffset", publicName: "moveScrollOffset", isSignal: true, isRequired: false, transformFunction: null }, moveScrollContainer: { classPropertyName: "moveScrollContainer", publicName: "moveScrollContainer", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["moveScroll"], ngImport: i0 });
|
|
1478
|
+
}
|
|
1479
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveScrollDirective, decorators: [{
|
|
1480
|
+
type: Directive,
|
|
1481
|
+
args: [{
|
|
1482
|
+
selector: '[moveScroll]',
|
|
1483
|
+
exportAs: 'moveScroll',
|
|
1484
|
+
}]
|
|
1485
|
+
}], ctorParameters: () => [], propDecorators: { moveScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveScroll", required: false }] }], moveScrollOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveScrollOffset", required: false }] }], moveScrollContainer: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveScrollContainer", required: false }] }] } });
|
|
1486
|
+
|
|
1487
|
+
class MovePresenceDirective {
|
|
1488
|
+
movePresence = input(...(ngDevMode ? [undefined, { debugName: "movePresence" }] : /* istanbul ignore next */ []));
|
|
1489
|
+
#viewContainer = inject(ViewContainerRef);
|
|
1490
|
+
#template = inject(TemplateRef);
|
|
1491
|
+
#view = null;
|
|
1492
|
+
#isRemoving = false;
|
|
1493
|
+
#children = new Set();
|
|
1494
|
+
constructor() {
|
|
1495
|
+
effect(() => {
|
|
1496
|
+
const show = !!this.movePresence();
|
|
1497
|
+
if (show) {
|
|
1498
|
+
if (!this.#view) {
|
|
1499
|
+
this.#view = this.#viewContainer.createEmbeddedView(this.#template);
|
|
1500
|
+
this.#isRemoving = false;
|
|
1501
|
+
}
|
|
1502
|
+
else if (this.#isRemoving) {
|
|
1503
|
+
// If we were removing, cancel the removal
|
|
1504
|
+
this.#isRemoving = false;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
else if (!show && this.#view && !this.#isRemoving) {
|
|
1508
|
+
this.#isRemoving = true;
|
|
1509
|
+
this.removeView();
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
register(child) {
|
|
1514
|
+
this.#children.add(child);
|
|
1515
|
+
}
|
|
1516
|
+
unregister(child) {
|
|
1517
|
+
this.#children.delete(child);
|
|
1518
|
+
}
|
|
1519
|
+
async removeView() {
|
|
1520
|
+
const promises = [];
|
|
1521
|
+
// Trigger leave on all registered children
|
|
1522
|
+
for (const child of this.#children) {
|
|
1523
|
+
promises.push(child.playLeave());
|
|
1524
|
+
}
|
|
1525
|
+
try {
|
|
1526
|
+
if (promises.length > 0) {
|
|
1527
|
+
await Promise.all(promises);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
catch {
|
|
1531
|
+
// Ignore errors in animations
|
|
1532
|
+
}
|
|
1533
|
+
if (this.#isRemoving && this.#view) {
|
|
1534
|
+
this.#viewContainer.clear();
|
|
1535
|
+
this.#view = null;
|
|
1536
|
+
this.#isRemoving = false;
|
|
1537
|
+
this.#children.clear();
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MovePresenceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1541
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MovePresenceDirective, isStandalone: true, selector: "[movePresence]", inputs: { movePresence: { classPropertyName: "movePresence", publicName: "movePresence", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
1542
|
+
{
|
|
1543
|
+
provide: MOVE_PRESENCE_PARENT,
|
|
1544
|
+
useExisting: forwardRef(() => MovePresenceDirective),
|
|
1545
|
+
},
|
|
1546
|
+
], ngImport: i0 });
|
|
1547
|
+
}
|
|
1548
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MovePresenceDirective, decorators: [{
|
|
1549
|
+
type: Directive,
|
|
1550
|
+
args: [{
|
|
1551
|
+
selector: '[movePresence]',
|
|
1552
|
+
providers: [
|
|
1553
|
+
{
|
|
1554
|
+
provide: MOVE_PRESENCE_PARENT,
|
|
1555
|
+
useExisting: forwardRef(() => MovePresenceDirective),
|
|
1556
|
+
},
|
|
1557
|
+
],
|
|
1558
|
+
}]
|
|
1559
|
+
}], ctorParameters: () => [], propDecorators: { movePresence: [{ type: i0.Input, args: [{ isSignal: true, alias: "movePresence", required: false }] }] } });
|
|
1560
|
+
|
|
1561
|
+
class MoveDragDirective {
|
|
1562
|
+
moveDrag = input(true, ...(ngDevMode ? [{ debugName: "moveDrag" }] : /* istanbul ignore next */ []));
|
|
1563
|
+
moveDragConstraints = input(undefined, ...(ngDevMode ? [{ debugName: "moveDragConstraints" }] : /* istanbul ignore next */ []));
|
|
1564
|
+
moveDragElastic = input(0.5, ...(ngDevMode ? [{ debugName: "moveDragElastic" }] : /* istanbul ignore next */ []));
|
|
1565
|
+
moveSpring = input(undefined, ...(ngDevMode ? [{ debugName: "moveSpring" }] : /* istanbul ignore next */ []));
|
|
1566
|
+
#documentRef = inject(DOCUMENT);
|
|
1567
|
+
#host = inject((ElementRef));
|
|
1568
|
+
#engine = inject(AnimationEngine);
|
|
1569
|
+
#isDragging = false;
|
|
1570
|
+
#pointerId = null;
|
|
1571
|
+
#startX = 0;
|
|
1572
|
+
#startY = 0;
|
|
1573
|
+
#_x = 0;
|
|
1574
|
+
#_y = 0;
|
|
1575
|
+
#dragBounds = null;
|
|
1576
|
+
#player = null;
|
|
1577
|
+
onPointerDown(e) {
|
|
1578
|
+
if (this.moveDrag() === false || e.button !== 0)
|
|
1579
|
+
return;
|
|
1580
|
+
this.#isDragging = true;
|
|
1581
|
+
this.#pointerId = e.pointerId;
|
|
1582
|
+
this.#host.nativeElement.setPointerCapture(e.pointerId);
|
|
1583
|
+
this.#player?.cancel();
|
|
1584
|
+
// read bounds cleanly before next render
|
|
1585
|
+
this.#dragBounds = this.resolveBounds();
|
|
1586
|
+
this.#startX = e.clientX - this.#_x;
|
|
1587
|
+
this.#startY = e.clientY - this.#_y;
|
|
1588
|
+
// Prevent text selection while dragging
|
|
1589
|
+
this.#host.nativeElement.style.touchAction = 'none';
|
|
1590
|
+
this.#host.nativeElement.style.userSelect = 'none';
|
|
1591
|
+
}
|
|
1592
|
+
onPointerMove(e) {
|
|
1593
|
+
if (!this.#isDragging || e.pointerId !== this.#pointerId)
|
|
1594
|
+
return;
|
|
1595
|
+
this.#_x = e.clientX - this.#startX;
|
|
1596
|
+
this.#_y = e.clientY - this.#startY;
|
|
1597
|
+
this.applyTransform();
|
|
1598
|
+
}
|
|
1599
|
+
onPointerUp(e) {
|
|
1600
|
+
if (!this.#isDragging || e.pointerId !== this.#pointerId)
|
|
1601
|
+
return;
|
|
1602
|
+
this.#isDragging = false;
|
|
1603
|
+
this.#host.nativeElement.releasePointerCapture(e.pointerId);
|
|
1604
|
+
this.#pointerId = null;
|
|
1605
|
+
this.#host.nativeElement.style.touchAction = '';
|
|
1606
|
+
this.#host.nativeElement.style.userSelect = '';
|
|
1607
|
+
this.snapBackIfNeeded();
|
|
1608
|
+
}
|
|
1609
|
+
resolveBounds() {
|
|
1610
|
+
const constraints = this.moveDragConstraints();
|
|
1611
|
+
if (!constraints)
|
|
1612
|
+
return null;
|
|
1613
|
+
if (constraints instanceof HTMLElement) {
|
|
1614
|
+
const oldTranslate = this.#host.nativeElement.style.translate;
|
|
1615
|
+
this.#host.nativeElement.style.translate = 'none';
|
|
1616
|
+
const elRect = this.#host.nativeElement.getBoundingClientRect();
|
|
1617
|
+
const containerRect = constraints.getBoundingClientRect();
|
|
1618
|
+
this.#host.nativeElement.style.translate = oldTranslate;
|
|
1619
|
+
return {
|
|
1620
|
+
left: containerRect.left - elRect.left,
|
|
1621
|
+
right: containerRect.right - elRect.right,
|
|
1622
|
+
top: containerRect.top - elRect.top,
|
|
1623
|
+
bottom: containerRect.bottom - elRect.bottom,
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
return constraints;
|
|
1627
|
+
}
|
|
1628
|
+
applyTransform() {
|
|
1629
|
+
let x = this.#_x;
|
|
1630
|
+
let y = this.#_y;
|
|
1631
|
+
if (this.#dragBounds) {
|
|
1632
|
+
const elastic = this.moveDragElastic();
|
|
1633
|
+
if (this.#dragBounds.left !== undefined && x < this.#dragBounds.left) {
|
|
1634
|
+
x = this.#dragBounds.left - (this.#dragBounds.left - x) * elastic;
|
|
1635
|
+
}
|
|
1636
|
+
else if (this.#dragBounds.right !== undefined && x > this.#dragBounds.right) {
|
|
1637
|
+
x = this.#dragBounds.right + (x - this.#dragBounds.right) * elastic;
|
|
1638
|
+
}
|
|
1639
|
+
if (this.#dragBounds.top !== undefined && y < this.#dragBounds.top) {
|
|
1640
|
+
y = this.#dragBounds.top - (this.#dragBounds.top - y) * elastic;
|
|
1641
|
+
}
|
|
1642
|
+
else if (this.#dragBounds.bottom !== undefined && y > this.#dragBounds.bottom) {
|
|
1643
|
+
y = this.#dragBounds.bottom + (y - this.#dragBounds.bottom) * elastic;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
this.#host.nativeElement.style.translate = `${x}px ${y}px`;
|
|
1647
|
+
}
|
|
1648
|
+
snapBackIfNeeded() {
|
|
1649
|
+
if (!this.#dragBounds)
|
|
1650
|
+
return;
|
|
1651
|
+
// We base the snap on the current logical _x, _y, calculating the nearest valid position.
|
|
1652
|
+
let targetX = this.#_x;
|
|
1653
|
+
let targetY = this.#_y;
|
|
1654
|
+
if (this.#dragBounds.left !== undefined && targetX < this.#dragBounds.left)
|
|
1655
|
+
targetX = this.#dragBounds.left;
|
|
1656
|
+
if (this.#dragBounds.right !== undefined && targetX > this.#dragBounds.right)
|
|
1657
|
+
targetX = this.#dragBounds.right;
|
|
1658
|
+
if (this.#dragBounds.top !== undefined && targetY < this.#dragBounds.top)
|
|
1659
|
+
targetY = this.#dragBounds.top;
|
|
1660
|
+
if (this.#dragBounds.bottom !== undefined && targetY > this.#dragBounds.bottom)
|
|
1661
|
+
targetY = this.#dragBounds.bottom;
|
|
1662
|
+
if (targetX !== this.#_x || targetY !== this.#_y) {
|
|
1663
|
+
// Find the currently visible coordinates (which include elasticity)
|
|
1664
|
+
let currentVisX = this.#_x;
|
|
1665
|
+
let currentVisY = this.#_y;
|
|
1666
|
+
const elastic = this.moveDragElastic();
|
|
1667
|
+
if (this.#dragBounds.left !== undefined && this.#_x < this.#dragBounds.left) {
|
|
1668
|
+
currentVisX = this.#dragBounds.left - (this.#dragBounds.left - this.#_x) * elastic;
|
|
1669
|
+
}
|
|
1670
|
+
else if (this.#dragBounds.right !== undefined && this.#_x > this.#dragBounds.right) {
|
|
1671
|
+
currentVisX = this.#dragBounds.right + (this.#_x - this.#dragBounds.right) * elastic;
|
|
1672
|
+
}
|
|
1673
|
+
if (this.#dragBounds.top !== undefined && this.#_y < this.#dragBounds.top) {
|
|
1674
|
+
currentVisY = this.#dragBounds.top - (this.#dragBounds.top - this.#_y) * elastic;
|
|
1675
|
+
}
|
|
1676
|
+
else if (this.#dragBounds.bottom !== undefined && this.#_y > this.#dragBounds.bottom) {
|
|
1677
|
+
currentVisY = this.#dragBounds.bottom + (this.#_y - this.#dragBounds.bottom) * elastic;
|
|
1678
|
+
}
|
|
1679
|
+
this.#player = this.#engine.play(this.#host.nativeElement, {
|
|
1680
|
+
x: [currentVisX, targetX],
|
|
1681
|
+
y: [currentVisY, targetY],
|
|
1682
|
+
}, {
|
|
1683
|
+
config: { duration: 300, easing: 'ease', delay: 0, disabled: false },
|
|
1684
|
+
spring: this.moveSpring() ?? { stiffness: 500, damping: 30 },
|
|
1685
|
+
disabled: prefersReducedMotion(this.#documentRef),
|
|
1686
|
+
});
|
|
1687
|
+
this.#_x = targetX;
|
|
1688
|
+
this.#_y = targetY;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
ngOnDestroy() {
|
|
1692
|
+
this.#player?.cancel();
|
|
1693
|
+
}
|
|
1694
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveDragDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1695
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveDragDirective, isStandalone: true, selector: "[moveDrag]", inputs: { moveDrag: { classPropertyName: "moveDrag", publicName: "moveDrag", isSignal: true, isRequired: false, transformFunction: null }, moveDragConstraints: { classPropertyName: "moveDragConstraints", publicName: "moveDragConstraints", isSignal: true, isRequired: false, transformFunction: null }, moveDragElastic: { classPropertyName: "moveDragElastic", publicName: "moveDragElastic", isSignal: true, isRequired: false, transformFunction: null }, moveSpring: { classPropertyName: "moveSpring", publicName: "moveSpring", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "pointerdown": "onPointerDown($event)", "pointermove": "onPointerMove($event)", "pointerup": "onPointerUp($event)", "pointercancel": "onPointerUp($event)" } }, ngImport: i0 });
|
|
1696
|
+
}
|
|
1697
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveDragDirective, decorators: [{
|
|
1698
|
+
type: Directive,
|
|
1699
|
+
args: [{
|
|
1700
|
+
selector: '[moveDrag]',
|
|
1701
|
+
host: {
|
|
1702
|
+
'(pointerdown)': 'onPointerDown($event)',
|
|
1703
|
+
'(pointermove)': 'onPointerMove($event)',
|
|
1704
|
+
'(pointerup)': 'onPointerUp($event)',
|
|
1705
|
+
'(pointercancel)': 'onPointerUp($event)',
|
|
1706
|
+
},
|
|
1707
|
+
}]
|
|
1708
|
+
}], propDecorators: { moveDrag: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDrag", required: false }] }], moveDragConstraints: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDragConstraints", required: false }] }], moveDragElastic: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDragElastic", required: false }] }], moveSpring: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSpring", required: false }] }] } });
|
|
1709
|
+
|
|
1710
|
+
class MoveInViewDirective {
|
|
1711
|
+
moveInView = input('none', ...(ngDevMode ? [{ debugName: "moveInView" }] : /* istanbul ignore next */ []));
|
|
1712
|
+
moveInViewMargin = input('0px', ...(ngDevMode ? [{ debugName: "moveInViewMargin" }] : /* istanbul ignore next */ []));
|
|
1713
|
+
moveInViewOnce = input(true, ...(ngDevMode ? [{ debugName: "moveInViewOnce" }] : /* istanbul ignore next */ []));
|
|
1714
|
+
/** CSS selector for the IntersectionObserver root (for inner scrollable containers). */
|
|
1715
|
+
moveInViewRoot = input(null, ...(ngDevMode ? [{ debugName: "moveInViewRoot" }] : /* istanbul ignore next */ []));
|
|
1716
|
+
moveDuration = input(undefined, ...(ngDevMode ? [{ debugName: "moveDuration" }] : /* istanbul ignore next */ []));
|
|
1717
|
+
moveEasing = input(undefined, ...(ngDevMode ? [{ debugName: "moveEasing" }] : /* istanbul ignore next */ []));
|
|
1718
|
+
moveDelay = input(undefined, ...(ngDevMode ? [{ debugName: "moveDelay" }] : /* istanbul ignore next */ []));
|
|
1719
|
+
moveDisabled = input(undefined, ...(ngDevMode ? [{ debugName: "moveDisabled" }] : /* istanbul ignore next */ []));
|
|
1720
|
+
moveSpring = input(undefined, ...(ngDevMode ? [{ debugName: "moveSpring" }] : /* istanbul ignore next */ []));
|
|
1721
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
1722
|
+
#documentRef = inject(DOCUMENT);
|
|
1723
|
+
#platformId = inject(PLATFORM_ID);
|
|
1724
|
+
#host = inject((ElementRef));
|
|
1725
|
+
#engine = inject(AnimationEngine);
|
|
1726
|
+
#player = null;
|
|
1727
|
+
#observer = null;
|
|
1728
|
+
#frames = null;
|
|
1729
|
+
#isAnimated = false;
|
|
1730
|
+
ngOnInit() {
|
|
1731
|
+
if (!isPlatformBrowser(this.#platformId) || this.moveDisabled())
|
|
1732
|
+
return;
|
|
1733
|
+
const isReduced = prefersReducedMotion(this.#documentRef);
|
|
1734
|
+
if (isReduced)
|
|
1735
|
+
return;
|
|
1736
|
+
this.#frames = resolveMoveFrames(this.moveInView(), 'enter');
|
|
1737
|
+
// Apply initial (invisible) state directly to DOM - avoid creating a player
|
|
1738
|
+
applyInitialStyles(this.#host.nativeElement, this.#frames);
|
|
1739
|
+
const rootSelector = this.moveInViewRoot();
|
|
1740
|
+
const rootEl = rootSelector ? this.#documentRef.querySelector(rootSelector) : null;
|
|
1741
|
+
this.#observer = new IntersectionObserver((entries) => {
|
|
1742
|
+
const entry = entries[0];
|
|
1743
|
+
if (entry.isIntersecting && !this.#isAnimated) {
|
|
1744
|
+
this.#playAnimation();
|
|
1745
|
+
if (this.moveInViewOnce()) {
|
|
1746
|
+
this.#isAnimated = true;
|
|
1747
|
+
this.#observer?.disconnect();
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
else if (!entry.isIntersecting && !this.moveInViewOnce() && this.#isAnimated) {
|
|
1751
|
+
// Reset to initial state for re-animation
|
|
1752
|
+
this.#player?.cancel();
|
|
1753
|
+
this.#player = null;
|
|
1754
|
+
this.#isAnimated = false;
|
|
1755
|
+
if (this.#frames) {
|
|
1756
|
+
applyInitialStyles(this.#host.nativeElement, this.#frames);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}, {
|
|
1760
|
+
root: rootEl,
|
|
1761
|
+
rootMargin: this.moveInViewMargin(),
|
|
1762
|
+
threshold: 0,
|
|
1763
|
+
});
|
|
1764
|
+
this.#observer.observe(this.#host.nativeElement);
|
|
1765
|
+
}
|
|
1766
|
+
#playAnimation() {
|
|
1767
|
+
if (!this.#frames)
|
|
1768
|
+
return;
|
|
1769
|
+
const config = resolveMovementConfig(this.#defaults, {
|
|
1770
|
+
duration: this.moveDuration(),
|
|
1771
|
+
easing: this.moveEasing(),
|
|
1772
|
+
delay: this.moveDelay() ?? 0,
|
|
1773
|
+
disabled: false,
|
|
1774
|
+
}, false);
|
|
1775
|
+
// Clear inline styles before animating so WAAPI can take over cleanly
|
|
1776
|
+
clearInitialStyles(this.#host.nativeElement);
|
|
1777
|
+
this.#player = this.#engine.play(this.#host.nativeElement, this.#frames, {
|
|
1778
|
+
config,
|
|
1779
|
+
spring: this.moveSpring(),
|
|
1780
|
+
disabled: false,
|
|
1781
|
+
});
|
|
1782
|
+
this.#isAnimated = true;
|
|
1783
|
+
}
|
|
1784
|
+
ngOnDestroy() {
|
|
1785
|
+
this.#observer?.disconnect();
|
|
1786
|
+
this.#player?.cancel();
|
|
1787
|
+
}
|
|
1788
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveInViewDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1789
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveInViewDirective, isStandalone: true, selector: "[moveInView]", inputs: { moveInView: { classPropertyName: "moveInView", publicName: "moveInView", isSignal: true, isRequired: false, transformFunction: null }, moveInViewMargin: { classPropertyName: "moveInViewMargin", publicName: "moveInViewMargin", isSignal: true, isRequired: false, transformFunction: null }, moveInViewOnce: { classPropertyName: "moveInViewOnce", publicName: "moveInViewOnce", isSignal: true, isRequired: false, transformFunction: null }, moveInViewRoot: { classPropertyName: "moveInViewRoot", publicName: "moveInViewRoot", isSignal: true, isRequired: false, transformFunction: null }, moveDuration: { classPropertyName: "moveDuration", publicName: "moveDuration", isSignal: true, isRequired: false, transformFunction: null }, moveEasing: { classPropertyName: "moveEasing", publicName: "moveEasing", isSignal: true, isRequired: false, transformFunction: null }, moveDelay: { classPropertyName: "moveDelay", publicName: "moveDelay", isSignal: true, isRequired: false, transformFunction: null }, moveDisabled: { classPropertyName: "moveDisabled", publicName: "moveDisabled", isSignal: true, isRequired: false, transformFunction: null }, moveSpring: { classPropertyName: "moveSpring", publicName: "moveSpring", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1790
|
+
}
|
|
1791
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveInViewDirective, decorators: [{
|
|
1792
|
+
type: Directive,
|
|
1793
|
+
args: [{
|
|
1794
|
+
selector: '[moveInView]',
|
|
1795
|
+
}]
|
|
1796
|
+
}], propDecorators: { moveInView: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveInView", required: false }] }], moveInViewMargin: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveInViewMargin", required: false }] }], moveInViewOnce: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveInViewOnce", required: false }] }], moveInViewRoot: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveInViewRoot", required: false }] }], moveDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDuration", required: false }] }], moveEasing: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveEasing", required: false }] }], moveDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDelay", required: false }] }], moveDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDisabled", required: false }] }], moveSpring: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSpring", required: false }] }] } });
|
|
1797
|
+
|
|
1798
|
+
class MoveTextDirective {
|
|
1799
|
+
moveText = input('fade-up', ...(ngDevMode ? [{ debugName: "moveText" }] : /* istanbul ignore next */ []));
|
|
1800
|
+
moveTextSplit = input('chars', ...(ngDevMode ? [{ debugName: "moveTextSplit" }] : /* istanbul ignore next */ []));
|
|
1801
|
+
moveTextStagger = input(30, ...(ngDevMode ? [{ debugName: "moveTextStagger" }] : /* istanbul ignore next */ []));
|
|
1802
|
+
moveDuration = input(undefined, ...(ngDevMode ? [{ debugName: "moveDuration" }] : /* istanbul ignore next */ []));
|
|
1803
|
+
moveEasing = input(undefined, ...(ngDevMode ? [{ debugName: "moveEasing" }] : /* istanbul ignore next */ []));
|
|
1804
|
+
moveDelay = input(undefined, ...(ngDevMode ? [{ debugName: "moveDelay" }] : /* istanbul ignore next */ []));
|
|
1805
|
+
moveDisabled = input(undefined, ...(ngDevMode ? [{ debugName: "moveDisabled" }] : /* istanbul ignore next */ []));
|
|
1806
|
+
moveSpring = input(undefined, ...(ngDevMode ? [{ debugName: "moveSpring" }] : /* istanbul ignore next */ []));
|
|
1807
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
1808
|
+
#documentRef = inject(DOCUMENT);
|
|
1809
|
+
#platformId = inject(PLATFORM_ID);
|
|
1810
|
+
#host = inject((ElementRef));
|
|
1811
|
+
#engine = inject(AnimationEngine);
|
|
1812
|
+
#players = [];
|
|
1813
|
+
#spans = [];
|
|
1814
|
+
#observer = null;
|
|
1815
|
+
#frames = null;
|
|
1816
|
+
ngOnInit() {
|
|
1817
|
+
if (!isPlatformBrowser(this.#platformId) || this.moveDisabled())
|
|
1818
|
+
return;
|
|
1819
|
+
// Defer to after Angular has completed its first rendering pass and set
|
|
1820
|
+
// the interpolated text content on the host element (same pattern as moveEnter).
|
|
1821
|
+
Promise.resolve().then(() => {
|
|
1822
|
+
this.#splitText();
|
|
1823
|
+
const isReduced = prefersReducedMotion(this.#documentRef);
|
|
1824
|
+
if (isReduced)
|
|
1825
|
+
return;
|
|
1826
|
+
this.#frames = resolveMoveFrames(this.moveText(), 'enter');
|
|
1827
|
+
// Apply initial (invisible) state to each span directly — no player yet
|
|
1828
|
+
this.#spans.forEach((span) => {
|
|
1829
|
+
if (this.#frames) {
|
|
1830
|
+
applyInitialStyles(span, this.#frames);
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
// Create player and animate only when visible
|
|
1834
|
+
this.#observer = new IntersectionObserver((entries) => {
|
|
1835
|
+
if (entries[0].isIntersecting) {
|
|
1836
|
+
this.#playAll();
|
|
1837
|
+
this.#observer?.disconnect();
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
this.#observer.observe(this.#host.nativeElement);
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
#playAll() {
|
|
1844
|
+
if (!this.#frames)
|
|
1845
|
+
return;
|
|
1846
|
+
const baseDelay = this.moveDelay() ?? 0;
|
|
1847
|
+
const stagger = this.moveTextStagger();
|
|
1848
|
+
this.#spans.forEach((span, index) => {
|
|
1849
|
+
if (!this.#frames)
|
|
1850
|
+
return;
|
|
1851
|
+
const config = resolveMovementConfig(this.#defaults, {
|
|
1852
|
+
duration: this.moveDuration(),
|
|
1853
|
+
easing: this.moveEasing(),
|
|
1854
|
+
delay: baseDelay + index * stagger,
|
|
1855
|
+
disabled: false,
|
|
1856
|
+
}, false);
|
|
1857
|
+
// Clear inline styles so WAAPI can animate from the keyframe starting point
|
|
1858
|
+
clearInitialStyles(span);
|
|
1859
|
+
const player = this.#engine.play(span, this.#frames, {
|
|
1860
|
+
config,
|
|
1861
|
+
spring: this.moveSpring(),
|
|
1862
|
+
disabled: false,
|
|
1863
|
+
});
|
|
1864
|
+
if (player) {
|
|
1865
|
+
this.#players.push(player);
|
|
1866
|
+
}
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
#splitText() {
|
|
1870
|
+
const el = this.#host.nativeElement;
|
|
1871
|
+
const text = (el.textContent ?? '').trim();
|
|
1872
|
+
el.innerHTML = '';
|
|
1873
|
+
el.setAttribute('aria-label', text);
|
|
1874
|
+
const byChars = this.moveTextSplit() === 'chars';
|
|
1875
|
+
if (byChars) {
|
|
1876
|
+
// Split character by character, preserving spaces as text nodes
|
|
1877
|
+
[...text].forEach((char) => {
|
|
1878
|
+
if (char === ' ') {
|
|
1879
|
+
el.appendChild(this.#documentRef.createTextNode(' '));
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
const span = this.#documentRef.createElement('span');
|
|
1883
|
+
span.setAttribute('aria-hidden', 'true');
|
|
1884
|
+
span.style.display = 'inline-block';
|
|
1885
|
+
span.textContent = char;
|
|
1886
|
+
el.appendChild(span);
|
|
1887
|
+
this.#spans.push(span);
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
else {
|
|
1891
|
+
// Split word by word
|
|
1892
|
+
const words = text.split(/\s+/);
|
|
1893
|
+
words.forEach((word, index) => {
|
|
1894
|
+
const span = this.#documentRef.createElement('span');
|
|
1895
|
+
span.setAttribute('aria-hidden', 'true');
|
|
1896
|
+
span.style.display = 'inline-block';
|
|
1897
|
+
span.style.whiteSpace = 'pre';
|
|
1898
|
+
span.textContent = index < words.length - 1 ? word + ' ' : word;
|
|
1899
|
+
el.appendChild(span);
|
|
1900
|
+
this.#spans.push(span);
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
ngOnDestroy() {
|
|
1905
|
+
this.#observer?.disconnect();
|
|
1906
|
+
this.#players.forEach((p) => p.cancel());
|
|
1907
|
+
}
|
|
1908
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveTextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1909
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveTextDirective, isStandalone: true, selector: "[moveText]", inputs: { moveText: { classPropertyName: "moveText", publicName: "moveText", isSignal: true, isRequired: false, transformFunction: null }, moveTextSplit: { classPropertyName: "moveTextSplit", publicName: "moveTextSplit", isSignal: true, isRequired: false, transformFunction: null }, moveTextStagger: { classPropertyName: "moveTextStagger", publicName: "moveTextStagger", isSignal: true, isRequired: false, transformFunction: null }, moveDuration: { classPropertyName: "moveDuration", publicName: "moveDuration", isSignal: true, isRequired: false, transformFunction: null }, moveEasing: { classPropertyName: "moveEasing", publicName: "moveEasing", isSignal: true, isRequired: false, transformFunction: null }, moveDelay: { classPropertyName: "moveDelay", publicName: "moveDelay", isSignal: true, isRequired: false, transformFunction: null }, moveDisabled: { classPropertyName: "moveDisabled", publicName: "moveDisabled", isSignal: true, isRequired: false, transformFunction: null }, moveSpring: { classPropertyName: "moveSpring", publicName: "moveSpring", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1910
|
+
}
|
|
1911
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveTextDirective, decorators: [{
|
|
1912
|
+
type: Directive,
|
|
1913
|
+
args: [{
|
|
1914
|
+
selector: '[moveText]',
|
|
1915
|
+
}]
|
|
1916
|
+
}], propDecorators: { moveText: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveText", required: false }] }], moveTextSplit: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveTextSplit", required: false }] }], moveTextStagger: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveTextStagger", required: false }] }], moveDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDuration", required: false }] }], moveEasing: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveEasing", required: false }] }], moveDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDelay", required: false }] }], moveDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDisabled", required: false }] }], moveSpring: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSpring", required: false }] }] } });
|
|
1917
|
+
|
|
1918
|
+
/**
|
|
1919
|
+
* Directive that enables smooth scrolling on a specific scrollable container.
|
|
1920
|
+
*
|
|
1921
|
+
* Supports both mouse wheel (desktop) and touch scroll (mobile) with
|
|
1922
|
+
* momentum-based inertia after finger lift, similar to Lenis.
|
|
1923
|
+
*
|
|
1924
|
+
* ⚠️ SmoothScrollService is a root singleton. Do NOT use this directive
|
|
1925
|
+
* on multiple elements simultaneously — use it once on the page root or
|
|
1926
|
+
* inject the service directly where needed.
|
|
1927
|
+
*
|
|
1928
|
+
* Usage:
|
|
1929
|
+
* <div moveSmoothScroll [moveSmoothScrollLerp]="0.08" style="overflow-y: auto; height: 100vh">
|
|
1930
|
+
* ...content...
|
|
1931
|
+
* </div>
|
|
1932
|
+
*
|
|
1933
|
+
* Or use the service directly (e.g. on documentElement):
|
|
1934
|
+
* constructor() {
|
|
1935
|
+
* inject(SmoothScrollService).init({ lerp: 0.1 });
|
|
1936
|
+
* }
|
|
1937
|
+
*/
|
|
1938
|
+
class MoveSmoothScrollDirective {
|
|
1939
|
+
/** Lerp factor passed to SmoothScrollService. Lower = smoother. Range: 0–1. */
|
|
1940
|
+
moveSmoothScrollLerp = input(0.1, ...(ngDevMode ? [{ debugName: "moveSmoothScrollLerp" }] : /* istanbul ignore next */ []));
|
|
1941
|
+
#host = inject((ElementRef));
|
|
1942
|
+
#scroll = inject(SmoothScrollService);
|
|
1943
|
+
#platformId = inject(PLATFORM_ID);
|
|
1944
|
+
ngOnInit() {
|
|
1945
|
+
if (!isPlatformBrowser(this.#platformId))
|
|
1946
|
+
return;
|
|
1947
|
+
this.#scroll.init({
|
|
1948
|
+
lerp: this.moveSmoothScrollLerp(),
|
|
1949
|
+
element: this.#host.nativeElement,
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
ngOnDestroy() {
|
|
1953
|
+
this.#scroll.destroy();
|
|
1954
|
+
}
|
|
1955
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveSmoothScrollDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1956
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveSmoothScrollDirective, isStandalone: true, selector: "[moveSmoothScroll]", inputs: { moveSmoothScrollLerp: { classPropertyName: "moveSmoothScrollLerp", publicName: "moveSmoothScrollLerp", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1957
|
+
}
|
|
1958
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveSmoothScrollDirective, decorators: [{
|
|
1959
|
+
type: Directive,
|
|
1960
|
+
args: [{
|
|
1961
|
+
selector: '[moveSmoothScroll]',
|
|
1962
|
+
}]
|
|
1963
|
+
}], propDecorators: { moveSmoothScrollLerp: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSmoothScrollLerp", required: false }] }] } });
|
|
1964
|
+
|
|
1965
|
+
class MoveFocusDirective {
|
|
1966
|
+
moveWhileFocus = input.required(...(ngDevMode ? [{ debugName: "moveWhileFocus" }] : /* istanbul ignore next */ []));
|
|
1967
|
+
moveDuration = input(undefined, ...(ngDevMode ? [{ debugName: "moveDuration" }] : /* istanbul ignore next */ []));
|
|
1968
|
+
moveEasing = input(undefined, ...(ngDevMode ? [{ debugName: "moveEasing" }] : /* istanbul ignore next */ []));
|
|
1969
|
+
moveDelay = input(undefined, ...(ngDevMode ? [{ debugName: "moveDelay" }] : /* istanbul ignore next */ []));
|
|
1970
|
+
moveDisabled = input(undefined, ...(ngDevMode ? [{ debugName: "moveDisabled" }] : /* istanbul ignore next */ []));
|
|
1971
|
+
moveSpring = input(undefined, ...(ngDevMode ? [{ debugName: "moveSpring" }] : /* istanbul ignore next */ []));
|
|
1972
|
+
#defaults = inject(MOVEMENT_CONFIG);
|
|
1973
|
+
#documentRef = inject(DOCUMENT);
|
|
1974
|
+
#host = inject((ElementRef));
|
|
1975
|
+
#engine = inject(AnimationEngine);
|
|
1976
|
+
#currentPlayer = null;
|
|
1977
|
+
#isFocused = false;
|
|
1978
|
+
onFocus() {
|
|
1979
|
+
if (this.#isFocused)
|
|
1980
|
+
return;
|
|
1981
|
+
this.#isFocused = true;
|
|
1982
|
+
this.play(false);
|
|
1983
|
+
}
|
|
1984
|
+
onBlur() {
|
|
1985
|
+
if (!this.#isFocused)
|
|
1986
|
+
return;
|
|
1987
|
+
this.#isFocused = false;
|
|
1988
|
+
this.play(true);
|
|
1989
|
+
}
|
|
1990
|
+
play(reverse) {
|
|
1991
|
+
this.#currentPlayer?.cancel();
|
|
1992
|
+
const isReduced = prefersReducedMotion(this.#documentRef);
|
|
1993
|
+
const config = resolveMovementConfig(this.#defaults, {
|
|
1994
|
+
duration: this.moveDuration(),
|
|
1995
|
+
easing: this.moveEasing(),
|
|
1996
|
+
delay: this.moveDelay(),
|
|
1997
|
+
disabled: this.moveDisabled(),
|
|
1998
|
+
}, isReduced);
|
|
1999
|
+
if (config.disabled)
|
|
2000
|
+
return;
|
|
2001
|
+
let frames = resolveMoveFrames(this.moveWhileFocus(), 'enter');
|
|
2002
|
+
if (reverse) {
|
|
2003
|
+
frames = reverseFrames(frames);
|
|
2004
|
+
}
|
|
2005
|
+
this.#currentPlayer = this.#engine.play(this.#host.nativeElement, frames, {
|
|
2006
|
+
config,
|
|
2007
|
+
spring: this.moveSpring(),
|
|
2008
|
+
disabled: false,
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
ngOnDestroy() {
|
|
2012
|
+
this.#currentPlayer?.cancel();
|
|
2013
|
+
}
|
|
2014
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2015
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveFocusDirective, isStandalone: true, selector: "[moveWhileFocus]", inputs: { moveWhileFocus: { classPropertyName: "moveWhileFocus", publicName: "moveWhileFocus", isSignal: true, isRequired: true, transformFunction: null }, moveDuration: { classPropertyName: "moveDuration", publicName: "moveDuration", isSignal: true, isRequired: false, transformFunction: null }, moveEasing: { classPropertyName: "moveEasing", publicName: "moveEasing", isSignal: true, isRequired: false, transformFunction: null }, moveDelay: { classPropertyName: "moveDelay", publicName: "moveDelay", isSignal: true, isRequired: false, transformFunction: null }, moveDisabled: { classPropertyName: "moveDisabled", publicName: "moveDisabled", isSignal: true, isRequired: false, transformFunction: null }, moveSpring: { classPropertyName: "moveSpring", publicName: "moveSpring", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "focusin": "onFocus()", "focusout": "onBlur()" } }, ngImport: i0 });
|
|
2016
|
+
}
|
|
2017
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveFocusDirective, decorators: [{
|
|
2018
|
+
type: Directive,
|
|
2019
|
+
args: [{
|
|
2020
|
+
selector: '[moveWhileFocus]',
|
|
2021
|
+
host: {
|
|
2022
|
+
'(focusin)': 'onFocus()',
|
|
2023
|
+
'(focusout)': 'onBlur()',
|
|
2024
|
+
},
|
|
2025
|
+
}]
|
|
2026
|
+
}], propDecorators: { moveWhileFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveWhileFocus", required: true }] }], moveDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDuration", required: false }] }], moveEasing: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveEasing", required: false }] }], moveDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDelay", required: false }] }], moveDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveDisabled", required: false }] }], moveSpring: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveSpring", required: false }] }] } });
|
|
2027
|
+
|
|
2028
|
+
class MoveParallaxDirective {
|
|
2029
|
+
moveParallax = input(0.2, ...(ngDevMode ? [{ debugName: "moveParallax" }] : /* istanbul ignore next */ []));
|
|
2030
|
+
moveParallaxAxis = input('y', ...(ngDevMode ? [{ debugName: "moveParallaxAxis" }] : /* istanbul ignore next */ []));
|
|
2031
|
+
#documentRef = inject(DOCUMENT);
|
|
2032
|
+
#platformId = inject(PLATFORM_ID);
|
|
2033
|
+
#host = inject((ElementRef));
|
|
2034
|
+
#engine = inject(AnimationEngine);
|
|
2035
|
+
#smoothScroll = inject(SmoothScrollService, { optional: true });
|
|
2036
|
+
#player = null;
|
|
2037
|
+
#observer = null;
|
|
2038
|
+
#isVisible = false;
|
|
2039
|
+
#scrollListener = () => this.#updateProgress();
|
|
2040
|
+
#elHeight = 0;
|
|
2041
|
+
#windowHeight = 0;
|
|
2042
|
+
#totalDistance = 0;
|
|
2043
|
+
#initialAbsoluteTop = 0;
|
|
2044
|
+
constructor() {
|
|
2045
|
+
effect(() => {
|
|
2046
|
+
this.#smoothScroll?.scrollY();
|
|
2047
|
+
if (this.#smoothScroll?.isActive && this.#isVisible) {
|
|
2048
|
+
this.#updateProgress();
|
|
2049
|
+
}
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
ngOnInit() {
|
|
2053
|
+
if (!isPlatformBrowser(this.#platformId))
|
|
2054
|
+
return;
|
|
2055
|
+
const view = this.#documentRef.defaultView;
|
|
2056
|
+
if (!view)
|
|
2057
|
+
return;
|
|
2058
|
+
this.#initAnimation();
|
|
2059
|
+
this.#observer = new IntersectionObserver((entries) => {
|
|
2060
|
+
const entry = entries[0];
|
|
2061
|
+
this.#isVisible = entry.isIntersecting;
|
|
2062
|
+
if (entry.isIntersecting) {
|
|
2063
|
+
if (!this.#smoothScroll?.isActive) {
|
|
2064
|
+
view.addEventListener('scroll', this.#scrollListener, { passive: true });
|
|
2065
|
+
}
|
|
2066
|
+
this.#updateProgress();
|
|
2067
|
+
}
|
|
2068
|
+
else {
|
|
2069
|
+
view.removeEventListener('scroll', this.#scrollListener);
|
|
2070
|
+
}
|
|
2071
|
+
}, { root: null });
|
|
2072
|
+
this.#observer.observe(this.#host.nativeElement);
|
|
2073
|
+
// Re-initialize animation on resize to update dimensions
|
|
2074
|
+
view.addEventListener('resize', this.#resizeListener, { passive: true });
|
|
2075
|
+
}
|
|
2076
|
+
#resizeListener = () => {
|
|
2077
|
+
this.#initAnimation();
|
|
2078
|
+
};
|
|
2079
|
+
#initAnimation() {
|
|
2080
|
+
const view = this.#documentRef.defaultView;
|
|
2081
|
+
if (!view)
|
|
2082
|
+
return;
|
|
2083
|
+
this.#windowHeight = view.innerHeight;
|
|
2084
|
+
this.#elHeight = this.#host.nativeElement.offsetHeight;
|
|
2085
|
+
this.#totalDistance = this.#windowHeight + this.#elHeight;
|
|
2086
|
+
const scrollY = this.#smoothScroll?.isActive
|
|
2087
|
+
? this.#smoothScroll.scrollY()
|
|
2088
|
+
: view.scrollY || view.pageYOffset || 0;
|
|
2089
|
+
const rect = this.#host.nativeElement.getBoundingClientRect();
|
|
2090
|
+
this.#initialAbsoluteTop = scrollY + rect.top;
|
|
2091
|
+
const speed = this.moveParallax();
|
|
2092
|
+
const axis = this.moveParallaxAxis();
|
|
2093
|
+
// Total translation distance across the entire scroll intersection
|
|
2094
|
+
const translateDist = this.#totalDistance * speed;
|
|
2095
|
+
const frames = {};
|
|
2096
|
+
if (axis === 'x') {
|
|
2097
|
+
frames.x = [translateDist / 2, -translateDist / 2];
|
|
2098
|
+
}
|
|
2099
|
+
else {
|
|
2100
|
+
frames.y = [translateDist / 2, -translateDist / 2];
|
|
2101
|
+
}
|
|
2102
|
+
this.#player?.cancel();
|
|
2103
|
+
this.#player = this.#engine.play(this.#host.nativeElement, frames, {
|
|
2104
|
+
config: { duration: 1000, delay: 0, easing: 'linear', disabled: false },
|
|
2105
|
+
});
|
|
2106
|
+
this.#player?.pause();
|
|
2107
|
+
if (this.#player) {
|
|
2108
|
+
this.#player.currentTime = 0;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
#updateProgress() {
|
|
2112
|
+
const view = this.#documentRef.defaultView;
|
|
2113
|
+
if (!view || this.#totalDistance === 0)
|
|
2114
|
+
return;
|
|
2115
|
+
const scrollY = this.#smoothScroll?.isActive
|
|
2116
|
+
? this.#smoothScroll.scrollY()
|
|
2117
|
+
: view.scrollY || view.pageYOffset || 0;
|
|
2118
|
+
// Efficiently calculate the current visual top without triggering layout thrashing
|
|
2119
|
+
// or feedback loops caused by the active CSS transform translating the element.
|
|
2120
|
+
const currentVirtualTop = this.#initialAbsoluteTop - scrollY;
|
|
2121
|
+
// progress is 0 when element top hits window bottom (currentVirtualTop === windowHeight)
|
|
2122
|
+
// progress is 1 when element bottom hits window top (currentVirtualTop === -elHeight)
|
|
2123
|
+
let p = (this.#windowHeight - currentVirtualTop) / this.#totalDistance;
|
|
2124
|
+
p = Math.max(0, Math.min(1, p));
|
|
2125
|
+
if (this.#player) {
|
|
2126
|
+
this.#player.currentTime = p * 1000;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
ngOnDestroy() {
|
|
2130
|
+
this.#player?.cancel();
|
|
2131
|
+
const view = this.#documentRef.defaultView;
|
|
2132
|
+
if (view) {
|
|
2133
|
+
view.removeEventListener('scroll', this.#scrollListener);
|
|
2134
|
+
view.removeEventListener('resize', this.#resizeListener);
|
|
2135
|
+
}
|
|
2136
|
+
this.#observer?.disconnect();
|
|
2137
|
+
}
|
|
2138
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveParallaxDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2139
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.2", type: MoveParallaxDirective, isStandalone: true, selector: "[moveParallax]", inputs: { moveParallax: { classPropertyName: "moveParallax", publicName: "moveParallax", isSignal: true, isRequired: false, transformFunction: null }, moveParallaxAxis: { classPropertyName: "moveParallaxAxis", publicName: "moveParallaxAxis", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["moveParallax"], ngImport: i0 });
|
|
2140
|
+
}
|
|
2141
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: MoveParallaxDirective, decorators: [{
|
|
2142
|
+
type: Directive,
|
|
2143
|
+
args: [{
|
|
2144
|
+
selector: '[moveParallax]',
|
|
2145
|
+
exportAs: 'moveParallax',
|
|
2146
|
+
}]
|
|
2147
|
+
}], ctorParameters: () => [], propDecorators: { moveParallax: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveParallax", required: false }] }], moveParallaxAxis: [{ type: i0.Input, args: [{ isSignal: true, alias: "moveParallaxAxis", required: false }] }] } });
|
|
2148
|
+
|
|
2149
|
+
function provideMovement(config = {}) {
|
|
2150
|
+
return makeEnvironmentProviders([
|
|
2151
|
+
{
|
|
2152
|
+
provide: MOVEMENT_CONFIG,
|
|
2153
|
+
useValue: {
|
|
2154
|
+
...MOVEMENT_DEFAULTS,
|
|
2155
|
+
...config,
|
|
2156
|
+
},
|
|
2157
|
+
},
|
|
2158
|
+
]);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
const MOVEMENT_DIRECTIVES = [
|
|
2162
|
+
MoveEnterDirective,
|
|
2163
|
+
MoveLeaveDirective,
|
|
2164
|
+
MoveAnimateDirective,
|
|
2165
|
+
MoveHoverDirective,
|
|
2166
|
+
MoveTapDirective,
|
|
2167
|
+
MoveVariantsDirective,
|
|
2168
|
+
MoveStaggerDirective,
|
|
2169
|
+
MoveLayoutDirective,
|
|
2170
|
+
MoveScrollDirective,
|
|
2171
|
+
MovePresenceDirective,
|
|
2172
|
+
MoveDragDirective,
|
|
2173
|
+
MoveInViewDirective,
|
|
2174
|
+
MoveTextDirective,
|
|
2175
|
+
MoveSmoothScrollDirective,
|
|
2176
|
+
MoveFocusDirective,
|
|
2177
|
+
MoveParallaxDirective,
|
|
2178
|
+
MoveAnimationDirective,
|
|
2179
|
+
];
|
|
2180
|
+
|
|
2181
|
+
/*
|
|
2182
|
+
* Public API Surface of movement
|
|
2183
|
+
*/
|
|
2184
|
+
|
|
2185
|
+
/**
|
|
2186
|
+
* Generated bundle index. Do not edit.
|
|
2187
|
+
*/
|
|
2188
|
+
|
|
2189
|
+
export { AnimationEngine, MOVEMENT_CONFIG, MOVEMENT_DEFAULTS, MOVEMENT_DIRECTIVES, MOVE_PRESENCE_PARENT, MOVE_PRESETS, MOVE_STAGGER_PARENT, MOVE_VARIANTS_PARENT, MoveAnimateDirective, MoveAnimationDirective, MoveDragDirective, MoveEnterDirective, MoveFocusDirective, MoveHoverDirective, MoveInViewDirective, MoveLayoutDirective, MoveLeaveDirective, MoveParallaxDirective, MovePresenceDirective, MoveScrollDirective, MoveSmoothScrollDirective, MoveStaggerDirective, MoveTapDirective, MoveTextDirective, MoveVariantsDirective, SmoothScrollService, SpringPlayer, WaapiPlayer, provideMovement };
|
|
2190
|
+
//# sourceMappingURL=angular-movement.mjs.map
|