animot-presenter 0.6.2 → 0.6.4
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/dist/AnimotPresenter.svelte +428 -58
- package/dist/cdn/animot-presenter.css +1 -1
- package/dist/cdn/animot-presenter.esm.js +9856 -7624
- package/dist/cdn/animot-presenter.min.js +14 -10
- package/dist/styles/presenter.css +38 -0
- package/dist/types.d.ts +33 -2
- package/dist/utils/freehand.d.ts +26 -0
- package/dist/utils/freehand.js +70 -0
- package/dist/utils/path-morph.d.ts +89 -0
- package/dist/utils/path-morph.js +420 -0
- package/dist/utils/svg-path-edit.d.ts +37 -0
- package/dist/utils/svg-path-edit.js +279 -0
- package/package.json +3 -1
|
@@ -19,10 +19,13 @@
|
|
|
19
19
|
import { parseEmbedUrl } from './utils/video-embed';
|
|
20
20
|
import EmbedPlayer from './EmbedPlayer.svelte';
|
|
21
21
|
import { easeInOutCubic, getEasingFn, getBackgroundStyle, gradientShapeToCss, hashFraction, getFloatAnimName, getIdleAnimName, computeFloatAmp, computeFloatSpeed, entranceRuntimeKeyframe, exitRuntimeKeyframe, emphasisKeyframeName } from './engine/utils';
|
|
22
|
+
import { drawElementToPath } from './utils/freehand';
|
|
23
|
+
import { makePathInterpolator, makeMorphParts, fitPathToBox, resolveElementPath, resolveElementPathParts, shapeToPath, extractCombinedPath, extractFill, type PathInterpolator, type MorphPart } from './utils/path-morph';
|
|
22
24
|
import type {
|
|
23
25
|
AnimotProject, AnimotPresenterProps, CanvasElement, CodeElement, TextElement,
|
|
24
26
|
ArrowElement, ImageElement, VideoElement, ShapeElement, CounterElement, ChartElement, IconElement,
|
|
25
27
|
SvgElement, MotionPathElement, ProgressElement, ContainerElement, PathPoint,
|
|
28
|
+
DrawElement, StickyElement,
|
|
26
29
|
Slide, CodeAnimationMode, AnimatableProperty
|
|
27
30
|
} from './types';
|
|
28
31
|
import './styles/presenter.css';
|
|
@@ -75,6 +78,7 @@
|
|
|
75
78
|
strokeColor: ReturnType<typeof tween<string>> | null;
|
|
76
79
|
strokeWidth: TweenValue | null;
|
|
77
80
|
shapeMorph: TweenValue | null;
|
|
81
|
+
pathMorph: TweenValue | null;
|
|
78
82
|
motionPathProgress: TweenValue | null;
|
|
79
83
|
blur: TweenValue;
|
|
80
84
|
brightness: TweenValue;
|
|
@@ -84,6 +88,58 @@
|
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
interface ShapeMorphState { fromType: string; toType: string; }
|
|
91
|
+
interface PathMorphState { interp: PathInterpolator; fromColor: string; toColor: string; viewBox?: string; parts?: MorphPart[]; }
|
|
92
|
+
|
|
93
|
+
// Resolve any svg/shape element to a single morphable path `d`.
|
|
94
|
+
function resolveMorphPath(el: CanvasElement): string | null {
|
|
95
|
+
if (el.type === 'svg') return extractCombinedPath((el as SvgElement).svgContent);
|
|
96
|
+
if (el.type === 'shape') {
|
|
97
|
+
const s = el as ShapeElement;
|
|
98
|
+
return shapeToPath(s.shapeType, s.size.width, s.size.height, s.borderRadius);
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function resolveMorphColor(el: CanvasElement): string {
|
|
103
|
+
if (el.type === 'shape') return (el as ShapeElement).fillColor || '#888888';
|
|
104
|
+
const svg = el as SvgElement;
|
|
105
|
+
return svg.color || extractFill(svg.svgContent) || '#888888';
|
|
106
|
+
}
|
|
107
|
+
// Resting geometry for keyframe-path elements before animateKeyframes runs,
|
|
108
|
+
// so the element never flashes its base/last shape on slide enter.
|
|
109
|
+
function restingKfMorph(el: CanvasElement): PathMorphState | null {
|
|
110
|
+
const kfs = (el.keyframes ?? []).filter((k) => k.path);
|
|
111
|
+
if (kfs.length < 1) return null;
|
|
112
|
+
const sorted = [...kfs].sort((a, b) => a.time - b.time);
|
|
113
|
+
const d = sorted[0].path as string;
|
|
114
|
+
const c = resolveMorphColor(el);
|
|
115
|
+
return { interp: () => d, fromColor: c, toColor: c, viewBox: resolveElementPath(el as any)?.viewBox };
|
|
116
|
+
}
|
|
117
|
+
// viewBox to render a morph path in — the element's own coordinate space.
|
|
118
|
+
function morphViewBox(el: CanvasElement, w: number, h: number): string {
|
|
119
|
+
if (el.type === 'svg') {
|
|
120
|
+
const s = el as SvgElement;
|
|
121
|
+
if (s.viewBox) return s.viewBox;
|
|
122
|
+
const m = s.svgContent.match(/viewBox=["']([^"']+)["']/i);
|
|
123
|
+
if (m) return m[1];
|
|
124
|
+
}
|
|
125
|
+
return `0 0 ${Math.max(1, w)} ${Math.max(1, h)}`;
|
|
126
|
+
}
|
|
127
|
+
// Linear interpolate two hex colors (#rgb/#rrggbb). Falls back to `to`.
|
|
128
|
+
function lerpHex(from: string, to: string, t: number): string {
|
|
129
|
+
const parse = (c: string): [number, number, number] | null => {
|
|
130
|
+
let h = c.trim().replace('#', '');
|
|
131
|
+
if (h.length === 3) h = h.split('').map((x) => x + x).join('');
|
|
132
|
+
if (h.length !== 6) return null;
|
|
133
|
+
const n = parseInt(h, 16);
|
|
134
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
135
|
+
};
|
|
136
|
+
const a = parse(from), b = parse(to);
|
|
137
|
+
if (!a || !b) return t < 0.5 ? from : to;
|
|
138
|
+
const r = Math.round(a[0] + (b[0] - a[0]) * t);
|
|
139
|
+
const g = Math.round(a[1] + (b[1] - a[1]) * t);
|
|
140
|
+
const bl = Math.round(a[2] + (b[2] - a[2]) * t);
|
|
141
|
+
return `rgb(${r}, ${g}, ${bl})`;
|
|
142
|
+
}
|
|
87
143
|
|
|
88
144
|
// Race a promise against an AbortSignal so awaits unwind the instant a
|
|
89
145
|
// loop is cancelled — otherwise tween.to() / setTimeout promises keep
|
|
@@ -137,6 +193,7 @@
|
|
|
137
193
|
for (const id of keyframeTimeouts) clearTimeout(id);
|
|
138
194
|
keyframeTimeouts = [];
|
|
139
195
|
}
|
|
196
|
+
cancelKfPathRafs();
|
|
140
197
|
}
|
|
141
198
|
function setKeyframeOverride(elementId: string, prop: string, value: any) {
|
|
142
199
|
const cur = keyframeOverrides.get(elementId) ?? {};
|
|
@@ -151,10 +208,103 @@
|
|
|
151
208
|
function easingForTween(name: string | undefined): (t: number) => number {
|
|
152
209
|
return getEasingFn(name ?? 'ease-out');
|
|
153
210
|
}
|
|
154
|
-
|
|
211
|
+
|
|
212
|
+
// ── Cross-slide shape/path morph (on slide enter) ──────────────────────
|
|
213
|
+
let morphLoopAbort: AbortController | null = null;
|
|
214
|
+
// Armed at the top of animateToSlide, played only after currentSlideIndex
|
|
215
|
+
// flips so the full 0→1 morph runs while the target slide is on screen
|
|
216
|
+
// (playing before the target paints finishes the tween invisibly → snap).
|
|
217
|
+
let pendingMorphPlays: Array<{ id: string; duration: number; easing: (t: number) => number }> = [];
|
|
218
|
+
function cancelMorphLoops() {
|
|
219
|
+
if (morphLoopAbort) { morphLoopAbort.abort(); morphLoopAbort = null; }
|
|
220
|
+
pendingMorphPlays = [];
|
|
221
|
+
cancelMorphRaf();
|
|
222
|
+
if (crossMorphProg.size > 0) crossMorphProg = new Map();
|
|
223
|
+
}
|
|
224
|
+
function playCrossSlideMorphs() {
|
|
225
|
+
const plays = pendingMorphPlays;
|
|
226
|
+
pendingMorphPlays = [];
|
|
227
|
+
if (!plays.length || typeof requestAnimationFrame === 'undefined') return;
|
|
228
|
+
const maxDur = Math.max(...plays.map((p) => p.duration), 1);
|
|
229
|
+
cancelMorphRaf();
|
|
230
|
+
let startTs = 0;
|
|
231
|
+
const step = (ts: number) => {
|
|
232
|
+
if (!startTs) startTs = ts;
|
|
233
|
+
const elapsed = ts - startTs;
|
|
234
|
+
const next = new Map(crossMorphProg);
|
|
235
|
+
let done = true;
|
|
236
|
+
for (const p of plays) {
|
|
237
|
+
const lin = Math.min(1, p.duration > 0 ? elapsed / p.duration : 1);
|
|
238
|
+
next.set(p.id, p.easing(lin));
|
|
239
|
+
if (lin < 1) done = false;
|
|
240
|
+
}
|
|
241
|
+
crossMorphProg = next;
|
|
242
|
+
if (!done) {
|
|
243
|
+
morphRafId = requestAnimationFrame(step);
|
|
244
|
+
} else {
|
|
245
|
+
// Morph complete — drop both progress and morph state so the render
|
|
246
|
+
// hands off to the faithful static SVG (preserves layering/opacity).
|
|
247
|
+
morphRafId = null;
|
|
248
|
+
const cleared = new Map(crossMorphProg);
|
|
249
|
+
const pm = new Map(pathMorphStates);
|
|
250
|
+
for (const p of plays) { cleared.delete(p.id); pm.delete(p.id); }
|
|
251
|
+
crossMorphProg = cleared;
|
|
252
|
+
pathMorphStates = pm;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
morphRafId = requestAnimationFrame(step);
|
|
256
|
+
}
|
|
257
|
+
function animateCrossSlideMorph(fromSlide: Slide | undefined, toSlide: Slide) {
|
|
258
|
+
cancelMorphLoops();
|
|
259
|
+
if (pathMorphStates.size > 0) pathMorphStates = new Map();
|
|
260
|
+
if (!fromSlide || fromSlide === toSlide) return;
|
|
261
|
+
morphLoopAbort = new AbortController();
|
|
262
|
+
const signal = morphLoopAbort.signal;
|
|
263
|
+
for (const element of toSlide.canvas.elements) {
|
|
264
|
+
try {
|
|
265
|
+
if (element.type !== 'shape' && element.type !== 'svg' && element.type !== 'draw') continue;
|
|
266
|
+
// Keyframe path-morphs own pathMorph on their slide — skip to avoid
|
|
267
|
+
// two drivers fighting (snap).
|
|
268
|
+
if (element.keyframes?.some((k) => k.path)) continue;
|
|
269
|
+
const fromEl = fromSlide.canvas.elements.find((e) => e.id === element.id);
|
|
270
|
+
if (!fromEl || (fromEl.type !== 'shape' && fromEl.type !== 'svg' && fromEl.type !== 'draw')) continue;
|
|
271
|
+
const animated = animatedElements.get(element.id) as any;
|
|
272
|
+
if (!animated?.pathMorph) continue;
|
|
273
|
+
const from = resolveElementPathParts(fromEl as any);
|
|
274
|
+
const to = resolveElementPathParts(element as any);
|
|
275
|
+
if (!from || !to) continue;
|
|
276
|
+
// Skip identical geometry (compare RAW parts — fitPathToBox reserializes
|
|
277
|
+
// and would never string-match its own source). See app copy.
|
|
278
|
+
if (from.viewBox === to.viewBox && from.parts.length === to.parts.length && from.parts.every((p, i) => p.d === to.parts[i].d)) continue;
|
|
279
|
+
const tvb = to.viewBox.split(/[\s,]+/).map(Number);
|
|
280
|
+
const fromFitted = from.parts.map((p) => ({ d: fitPathToBox(p.d, from.viewBox, tvb[2] || 100, tvb[3] || 100), fill: p.fill }));
|
|
281
|
+
const duration = (element.animationConfig?.duration ?? 800);
|
|
282
|
+
const parts = makeMorphParts(fromFitted, to.parts);
|
|
283
|
+
pathMorphStates.set(element.id, {
|
|
284
|
+
interp: parts[0].interp,
|
|
285
|
+
fromColor: parts[0].fromColor,
|
|
286
|
+
toColor: parts[0].toColor,
|
|
287
|
+
viewBox: to.viewBox,
|
|
288
|
+
parts
|
|
289
|
+
});
|
|
290
|
+
pathMorphStates = new Map(pathMorphStates);
|
|
291
|
+
if (signal.aborted) return;
|
|
292
|
+
// Drive progress via the rAF loop + crossMorphProg $state (not the
|
|
293
|
+
// tween — its reactivity doesn't cross the bundle boundary). Seed 0.
|
|
294
|
+
crossMorphProg.set(element.id, 0);
|
|
295
|
+
pendingMorphPlays.push({ id: element.id, duration, easing: getEasingFn(element.animationConfig?.easing) });
|
|
296
|
+
} catch (e) {
|
|
297
|
+
console.warn('[morph] skipped element', element.id, e);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (crossMorphProg.size > 0) crossMorphProg = new Map(crossMorphProg);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function animateKeyframes(slide: Slide, fromSlide?: Slide) {
|
|
155
304
|
cancelKeyframeLoops();
|
|
156
305
|
const hasAnyKeyframes = slide.canvas.elements.some((el) => el.keyframes && el.keyframes.length > 0);
|
|
157
306
|
if (keyframeOverrides.size > 0) keyframeOverrides = new Map();
|
|
307
|
+
if (kfPathStates.size > 0) kfPathStates = new Map();
|
|
158
308
|
if (!hasAnyKeyframes) return;
|
|
159
309
|
keyframeLoopAbort = new AbortController();
|
|
160
310
|
const signal = keyframeLoopAbort.signal;
|
|
@@ -164,13 +314,19 @@
|
|
|
164
314
|
if (!animated) continue;
|
|
165
315
|
const sorted = [...element.keyframes].sort((a, b) => a.time - b.time);
|
|
166
316
|
const first = sorted[0];
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
317
|
+
// If we came from another slide where this element existed, morph the
|
|
318
|
+
// first keyframe's transform props IN from the previous pose (so a
|
|
319
|
+
// slide change / loop wrap blends like every other element). openDur=0
|
|
320
|
+
// (no source) keeps the snap-to-KF1 behavior on initial load.
|
|
321
|
+
const cameFromEl = fromSlide?.canvas.elements.find((e) => e.id === element.id);
|
|
322
|
+
const openDur = cameFromEl ? Math.max(50, first.time || 600) : 0;
|
|
323
|
+
const openEase = easingForTween('ease-in-out');
|
|
324
|
+
if (first.position) { animated.x?.to(first.position.x, { duration: openDur, easing: openEase }); animated.y?.to(first.position.y, { duration: openDur, easing: openEase }); }
|
|
325
|
+
if (first.size) { animated.width?.to(first.size.width, { duration: openDur, easing: openEase }); animated.height?.to(first.size.height, { duration: openDur, easing: openEase }); }
|
|
326
|
+
if (first.rotation !== undefined) animated.rotation?.to(first.rotation, { duration: openDur, easing: openEase });
|
|
171
327
|
if (first.opacity !== undefined) animated.opacity?.to(first.opacity, { duration: 0 });
|
|
172
|
-
if (first.skewX !== undefined) animated.skewX?.to(first.skewX, { duration:
|
|
173
|
-
if (first.skewY !== undefined) animated.skewY?.to(first.skewY, { duration:
|
|
328
|
+
if (first.skewX !== undefined) animated.skewX?.to(first.skewX, { duration: openDur, easing: openEase });
|
|
329
|
+
if (first.skewY !== undefined) animated.skewY?.to(first.skewY, { duration: openDur, easing: openEase });
|
|
174
330
|
if (first.tiltX !== undefined) animated.tiltX?.to(first.tiltX, { duration: 0 });
|
|
175
331
|
if (first.tiltY !== undefined) animated.tiltY?.to(first.tiltY, { duration: 0 });
|
|
176
332
|
if (first.borderRadius !== undefined) animated.borderRadius?.to(first.borderRadius, { duration: 0 });
|
|
@@ -185,7 +341,44 @@
|
|
|
185
341
|
if (first.grayscale !== undefined) animated.grayscale?.to(first.grayscale, { duration: 0 });
|
|
186
342
|
if (first.backgroundColor !== undefined) setKeyframeOverride(element.id, 'backgroundColor', first.backgroundColor);
|
|
187
343
|
if (first.color !== undefined) setKeyframeOverride(element.id, 'color', first.color);
|
|
344
|
+
// Path morph: snap to the first keyframe's path (static interp) so the
|
|
345
|
+
// element shows that geometry from the start.
|
|
346
|
+
const kfColor = resolveMorphColor(element);
|
|
347
|
+
const kfVB = resolveElementPath(element as any)?.viewBox;
|
|
348
|
+
if (first.path && animated.pathMorph) {
|
|
349
|
+
const fromEl = fromSlide?.canvas.elements.find((e) => e.id === element.id);
|
|
350
|
+
let openFrom: string | null = null;
|
|
351
|
+
if (fromEl && (fromEl.type === 'shape' || fromEl.type === 'svg' || fromEl.type === 'draw')) {
|
|
352
|
+
try {
|
|
353
|
+
const fp = resolveElementPath(fromEl as any);
|
|
354
|
+
if (fp && fp.d !== first.path) {
|
|
355
|
+
const tvb = (kfVB ?? '0 0 100 100').split(/[\s,]+/).map(Number);
|
|
356
|
+
openFrom = fitPathToBox(fp.d, fp.viewBox, tvb[2] || 100, tvb[3] || 100);
|
|
357
|
+
}
|
|
358
|
+
} catch { /* skip */ }
|
|
359
|
+
}
|
|
360
|
+
if (openFrom) {
|
|
361
|
+
const openSpan = Math.max(50, first.time || 600);
|
|
362
|
+
kfPathStates.set(element.id, { interp: makePathInterpolator(openFrom, first.path), fromColor: kfColor, toColor: kfColor, viewBox: kfVB });
|
|
363
|
+
kfPathStates = new Map(kfPathStates);
|
|
364
|
+
driveKfPathMorph(element.id, openSpan, getEasingFn('ease-in-out'));
|
|
365
|
+
} else {
|
|
366
|
+
const staticD = first.path;
|
|
367
|
+
kfPathStates.set(element.id, { interp: () => staticD, fromColor: kfColor, toColor: kfColor, viewBox: kfVB });
|
|
368
|
+
kfPathStates = new Map(kfPathStates);
|
|
369
|
+
cancelKfPathRaf(element.id);
|
|
370
|
+
kfPathProg.set(element.id, 1);
|
|
371
|
+
kfPathProg = new Map(kfPathProg);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
let kfPrevPath = first.path;
|
|
188
375
|
for (const kf of sorted.slice(1)) {
|
|
376
|
+
const kfSegFrom = kfPrevPath;
|
|
377
|
+
if (kf.path) kfPrevPath = kf.path;
|
|
378
|
+
// Fire at the PREVIOUS keyframe and animate over the gap so the
|
|
379
|
+
// element arrives AT this keyframe's time (not span ms later).
|
|
380
|
+
const kfIdx = sorted.indexOf(kf);
|
|
381
|
+
const segPrevTime = kfIdx <= 1 ? sorted[0].time : sorted[kfIdx - 1].time;
|
|
189
382
|
const tid = setTimeout(() => {
|
|
190
383
|
if (signal.aborted) return;
|
|
191
384
|
const easing = easingForTween(kf.easing);
|
|
@@ -212,7 +405,16 @@
|
|
|
212
405
|
if (kf.grayscale !== undefined) animated.grayscale?.to(kf.grayscale, { duration: span, easing });
|
|
213
406
|
if (kf.backgroundColor !== undefined) setKeyframeOverride(element.id, 'backgroundColor', kf.backgroundColor);
|
|
214
407
|
if (kf.color !== undefined) setKeyframeOverride(element.id, 'color', kf.color);
|
|
215
|
-
|
|
408
|
+
// Path morph between the previous path and this keyframe's path.
|
|
409
|
+
if (kf.path && kfSegFrom && kf.path !== kfSegFrom && animated.pathMorph) {
|
|
410
|
+
kfPathStates.set(element.id, {
|
|
411
|
+
interp: makePathInterpolator(kfSegFrom, kf.path),
|
|
412
|
+
fromColor: kfColor, toColor: kfColor, viewBox: kfVB
|
|
413
|
+
});
|
|
414
|
+
kfPathStates = new Map(kfPathStates);
|
|
415
|
+
driveKfPathMorph(element.id, span, easing);
|
|
416
|
+
}
|
|
417
|
+
}, Math.max(0, segPrevTime));
|
|
216
418
|
keyframeTimeouts.push(tid);
|
|
217
419
|
}
|
|
218
420
|
}
|
|
@@ -403,6 +605,62 @@
|
|
|
403
605
|
let textTypewriterState = $state<Map<string, {fullText: string, displayedChars: number, isAnimating: boolean}>>(new Map());
|
|
404
606
|
let typewriterIntervals = new Map<string, ReturnType<typeof setInterval>>();
|
|
405
607
|
let shapeMorphStates = $state<Map<string, ShapeMorphState>>(new Map());
|
|
608
|
+
let pathMorphStates = $state<Map<string, PathMorphState>>(new Map());
|
|
609
|
+
// Within-slide keyframe path morph (separate from cross-slide pathMorphStates
|
|
610
|
+
// so the render keeps interpolating even at progress=1, since the element's
|
|
611
|
+
// base svgContent is NOT the keyframe target).
|
|
612
|
+
let kfPathStates = $state<Map<string, PathMorphState>>(new Map());
|
|
613
|
+
// Cross-slide morph progress, driven by a manual rAF loop into this
|
|
614
|
+
// component-owned $state map. The @animotion Tween's reactive `.current`
|
|
615
|
+
// does NOT propagate re-renders across the packaged-bundle boundary (it
|
|
616
|
+
// does in the editor app), so reading the tween in the template never
|
|
617
|
+
// repaints → snap. A $state map we own DOES repaint (same as
|
|
618
|
+
// pathMorphStates), and the manual loop is the pattern motion paths use.
|
|
619
|
+
let crossMorphProg = $state<Map<string, number>>(new Map());
|
|
620
|
+
let kfPathProg = $state<Map<string, number>>(new Map());
|
|
621
|
+
let morphRafId: number | null = null;
|
|
622
|
+
function cancelMorphRaf() {
|
|
623
|
+
if (morphRafId !== null && typeof cancelAnimationFrame !== 'undefined') cancelAnimationFrame(morphRafId);
|
|
624
|
+
morphRafId = null;
|
|
625
|
+
}
|
|
626
|
+
let kfPathRafIds = new Map<string, number>();
|
|
627
|
+
function cancelKfPathRafs() {
|
|
628
|
+
if (typeof cancelAnimationFrame !== 'undefined') {
|
|
629
|
+
for (const id of kfPathRafIds.values()) cancelAnimationFrame(id);
|
|
630
|
+
}
|
|
631
|
+
kfPathRafIds.clear();
|
|
632
|
+
if (kfPathProg.size > 0) kfPathProg = new Map();
|
|
633
|
+
}
|
|
634
|
+
function cancelKfPathRaf(elementId: string) {
|
|
635
|
+
const rafId = kfPathRafIds.get(elementId);
|
|
636
|
+
if (rafId !== undefined && typeof cancelAnimationFrame !== 'undefined') cancelAnimationFrame(rafId);
|
|
637
|
+
kfPathRafIds.delete(elementId);
|
|
638
|
+
}
|
|
639
|
+
function driveKfPathMorph(elementId: string, duration: number, easing: (t: number) => number) {
|
|
640
|
+
if (typeof requestAnimationFrame === 'undefined') {
|
|
641
|
+
kfPathProg.set(elementId, 1);
|
|
642
|
+
kfPathProg = new Map(kfPathProg);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
cancelKfPathRaf(elementId);
|
|
646
|
+
kfPathProg.set(elementId, 0);
|
|
647
|
+
kfPathProg = new Map(kfPathProg);
|
|
648
|
+
let startTs = 0;
|
|
649
|
+
const span = Math.max(1, duration);
|
|
650
|
+
const step = (ts: number) => {
|
|
651
|
+
if (!startTs) startTs = ts;
|
|
652
|
+
const lin = Math.min(1, (ts - startTs) / span);
|
|
653
|
+
const next = new Map(kfPathProg);
|
|
654
|
+
next.set(elementId, easing(lin));
|
|
655
|
+
kfPathProg = next;
|
|
656
|
+
if (lin < 1) {
|
|
657
|
+
kfPathRafIds.set(elementId, requestAnimationFrame(step));
|
|
658
|
+
} else {
|
|
659
|
+
kfPathRafIds.delete(elementId);
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
kfPathRafIds.set(elementId, requestAnimationFrame(step));
|
|
663
|
+
}
|
|
406
664
|
let autoplayTimer: ReturnType<typeof setTimeout> | null = null;
|
|
407
665
|
let menuVisible = $state(true);
|
|
408
666
|
let mouseIdleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -713,6 +971,10 @@
|
|
|
713
971
|
strokeColor: shapeEl ? mkTween(seedStroke, { duration: 500 }) : null,
|
|
714
972
|
strokeWidth: shapeEl ? mkTween(seedStrokeW, { duration: 500 }) : null,
|
|
715
973
|
shapeMorph: shapeEl ? mkTween(1, { duration: 500 }) : null,
|
|
974
|
+
// Raw tween (NOT mkTween): wrapTween's `current === value` guard
|
|
975
|
+
// would skip the deferred play-to-1 (pathMorph inits to 1 and the
|
|
976
|
+
// arm's async to(0) hasn't applied yet) → cross-slide morph snaps.
|
|
977
|
+
pathMorph: (element.type === 'svg' || element.type === 'draw' || shapeEl) ? tween(1, { duration: 500 }) : null,
|
|
716
978
|
motionPathProgress: element.motionPathConfig ? mkTween(0, { duration: 500 }) : null,
|
|
717
979
|
blur: mkTween(seedBlur, { duration: 500 }),
|
|
718
980
|
brightness: mkTween(seedBright, { duration: 500 }),
|
|
@@ -784,6 +1046,7 @@
|
|
|
784
1046
|
clearAllTypewriterAnimations();
|
|
785
1047
|
cancelMotionPathLoops();
|
|
786
1048
|
cancelKeyframeLoops();
|
|
1049
|
+
cancelMorphLoops();
|
|
787
1050
|
const firstSlide = slides[0];
|
|
788
1051
|
if (!firstSlide) { isTransitioning = false; return; }
|
|
789
1052
|
|
|
@@ -845,6 +1108,7 @@
|
|
|
845
1108
|
previousChartContent = new Map();
|
|
846
1109
|
previousProgressContent = new Map();
|
|
847
1110
|
shapeMorphStates = new Map();
|
|
1111
|
+
pathMorphStates = new Map();
|
|
848
1112
|
elementContent = new Map(elementContent);
|
|
849
1113
|
currentSlideIndex = 0;
|
|
850
1114
|
isTransitioning = false;
|
|
@@ -877,9 +1141,14 @@
|
|
|
877
1141
|
isTransitioning = true;
|
|
878
1142
|
transitionDirection = targetIndex > currentSlideIndex ? 'forward' : 'backward';
|
|
879
1143
|
const targetSlide = slides[targetIndex];
|
|
1144
|
+
// Slide we're leaving — `currentSlideIndex` flips to target mid-function
|
|
1145
|
+
// and `currentSlide` derives from it, so morph source geometry must read
|
|
1146
|
+
// from this snapshot.
|
|
1147
|
+
const sourceSlide = slides[currentSlideIndex];
|
|
880
1148
|
clearAllTypewriterAnimations();
|
|
881
1149
|
cancelMotionPathLoops();
|
|
882
1150
|
cancelKeyframeLoops();
|
|
1151
|
+
cancelMorphLoops();
|
|
883
1152
|
// Manual arrow nav and the autoplay timer are both forms of "user
|
|
884
1153
|
// is moving forward in the deck" — both should swap narration to
|
|
885
1154
|
// the new slide. The pause button is what stops audio. Without
|
|
@@ -891,6 +1160,11 @@
|
|
|
891
1160
|
transitionDurationMs = duration;
|
|
892
1161
|
const hasSlideTransition = transition.type !== 'none';
|
|
893
1162
|
|
|
1163
|
+
// Cross-slide shape/path morph: set up BEFORE the flip + transition so
|
|
1164
|
+
// the first render shows the source geometry and morphs to target
|
|
1165
|
+
// concurrently (no target→source flash), and fires on the loop wrap.
|
|
1166
|
+
animateCrossSlideMorph(sourceSlide, targetSlide);
|
|
1167
|
+
|
|
894
1168
|
if (hasSlideTransition) {
|
|
895
1169
|
// Phase 2 sprint-1 polish — fire per-element exit presets BEFORE
|
|
896
1170
|
// the slide-level CSS transition starts, then WAIT for them to
|
|
@@ -987,6 +1261,9 @@
|
|
|
987
1261
|
}
|
|
988
1262
|
}
|
|
989
1263
|
codeHighlights = new Map(codeHighlights);
|
|
1264
|
+
// Do NOT clear pathMorphStates — animateCrossSlideMorph() armed it at
|
|
1265
|
+
// the top of this function and its tween is mid-flight; wiping it makes
|
|
1266
|
+
// the cross-slide morph vanish behind the slide transition.
|
|
990
1267
|
shapeMorphStates = new Map();
|
|
991
1268
|
codeMorphState = newCodeMorphState;
|
|
992
1269
|
previousCodeContent = newPreviousCodeContent;
|
|
@@ -995,6 +1272,7 @@
|
|
|
995
1272
|
elementContent = newElementContent;
|
|
996
1273
|
animatedElements = new Map(animatedElements);
|
|
997
1274
|
currentSlideIndex = targetIndex;
|
|
1275
|
+
playCrossSlideMorphs();
|
|
998
1276
|
for (const elementId of allElementIds) {
|
|
999
1277
|
const targetEl = getElementInSlide(targetSlide, elementId);
|
|
1000
1278
|
if (targetEl?.type === 'text') {
|
|
@@ -1009,7 +1287,7 @@
|
|
|
1009
1287
|
firePresenterEntrancePresets(targetIndex);
|
|
1010
1288
|
await new Promise(r => setTimeout(r, duration * 0.6));
|
|
1011
1289
|
transitionClass = '';
|
|
1012
|
-
animateKeyframes(targetSlide);
|
|
1290
|
+
animateKeyframes(targetSlide, sourceSlide);
|
|
1013
1291
|
animateMotionPaths(targetSlide);
|
|
1014
1292
|
isTransitioning = false;
|
|
1015
1293
|
onslidechange?.(targetIndex, slides.length);
|
|
@@ -1256,18 +1534,16 @@
|
|
|
1256
1534
|
if (animated.strokeColor) anims.push(animated.strokeColor.to(ts.strokeColor, { duration: elementDuration, easing }));
|
|
1257
1535
|
if (animated.strokeWidth) anims.push(animated.strokeWidth.to(ts.strokeWidth, { duration: elementDuration, easing }));
|
|
1258
1536
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
shapeMorphStates = new Map(shapeMorphStates);
|
|
1262
|
-
anims.push(animated.shapeMorph.to(0, { duration: 0 }));
|
|
1263
|
-
anims.push(animated.shapeMorph.to(1, { duration: elementDuration, easing }));
|
|
1264
|
-
}
|
|
1537
|
+
// Shape→shape geometry change is handled by the flubber path
|
|
1538
|
+
// morph below (true geometric morph, not a crossfade).
|
|
1265
1539
|
} else if (targetEl.type === 'shape' && !propertySequences?.length) {
|
|
1266
1540
|
const s = targetEl as ShapeElement;
|
|
1267
1541
|
if (animated.fillColor) anims.push(animated.fillColor.to(s.fillColor, { duration: elementDuration, easing }));
|
|
1268
1542
|
if (animated.strokeColor) anims.push(animated.strokeColor.to(s.strokeColor, { duration: elementDuration, easing }));
|
|
1269
1543
|
if (animated.strokeWidth) anims.push(animated.strokeWidth.to(s.strokeWidth, { duration: elementDuration, easing }));
|
|
1270
1544
|
}
|
|
1545
|
+
// Cross-slide shape/path morph is handled on slide ENTER by
|
|
1546
|
+
// animateCrossSlideMorph (transition-independent), not here.
|
|
1271
1547
|
if (!currentEl) {
|
|
1272
1548
|
// Snap ALL properties to target instantly — the tween may hold
|
|
1273
1549
|
// stale values from a previous slide where the element last appeared
|
|
@@ -1385,11 +1661,14 @@
|
|
|
1385
1661
|
}
|
|
1386
1662
|
}
|
|
1387
1663
|
codeHighlights = new Map(codeHighlights);
|
|
1664
|
+
// Do NOT clear pathMorphStates (same fix as the slide-transition branch) —
|
|
1665
|
+
// animateCrossSlideMorph() armed it at the top and its tween is mid-flight.
|
|
1388
1666
|
shapeMorphStates = new Map();
|
|
1389
1667
|
codeMorphState = newCodeMorphState;
|
|
1390
1668
|
previousCodeContent = newPreviousCodeContent;
|
|
1391
1669
|
elementContent = newElementContent;
|
|
1392
1670
|
currentSlideIndex = targetIndex;
|
|
1671
|
+
playCrossSlideMorphs();
|
|
1393
1672
|
isTransitioning = false;
|
|
1394
1673
|
// Ensure elements not on the new slide are fully hidden. Phase 2:
|
|
1395
1674
|
// skip elements with an active runtime exit registration — their
|
|
@@ -1403,7 +1682,7 @@
|
|
|
1403
1682
|
const hasActiveExit = !!reg?.get(elementId)?.exit;
|
|
1404
1683
|
if (!onSlide && animated && !hasActiveExit) { animated.opacity.to(0, { duration: 0 }); }
|
|
1405
1684
|
}
|
|
1406
|
-
animateKeyframes(slides[targetIndex]);
|
|
1685
|
+
animateKeyframes(slides[targetIndex], sourceSlide);
|
|
1407
1686
|
animateMotionPaths(slides[targetIndex]);
|
|
1408
1687
|
onslidechange?.(targetIndex, slides.length);
|
|
1409
1688
|
if (targetIndex === slides.length - 1 && !loop) oncomplete?.();
|
|
@@ -1418,7 +1697,7 @@
|
|
|
1418
1697
|
autoplayTimer = setTimeout(() => {
|
|
1419
1698
|
if (currentSlideIndex < slides.length - 1) animateToSlide(currentSlideIndex + 1);
|
|
1420
1699
|
else if (loop) {
|
|
1421
|
-
const loopMode = project?.settings?.loopMode ?? 'reset';
|
|
1700
|
+
const loopMode = project?.settings?.loopMode ?? ((project as any)?.mode === 'flow' ? 'reset' : 'transition');
|
|
1422
1701
|
if (loopMode === 'transition') animateToSlide(0);
|
|
1423
1702
|
else resetToFirstSlide();
|
|
1424
1703
|
}
|
|
@@ -1432,7 +1711,7 @@
|
|
|
1432
1711
|
if (currentSlideIndex < slides.length - 1) {
|
|
1433
1712
|
animateToSlide(currentSlideIndex + 1);
|
|
1434
1713
|
} else if (loop) {
|
|
1435
|
-
const loopMode = project?.settings?.loopMode ?? 'reset';
|
|
1714
|
+
const loopMode = project?.settings?.loopMode ?? ((project as any)?.mode === 'flow' ? 'reset' : 'transition');
|
|
1436
1715
|
if (loopMode === 'transition') {
|
|
1437
1716
|
animateToSlide(0);
|
|
1438
1717
|
} else {
|
|
@@ -1661,7 +1940,11 @@
|
|
|
1661
1940
|
aria-label="Animot Presentation"
|
|
1662
1941
|
>
|
|
1663
1942
|
{#if loading}
|
|
1664
|
-
|
|
1943
|
+
<!-- Skeleton placeholder — brand-aligned shimmer instead of a spinning
|
|
1944
|
+
circle. Single full-bleed surface with a purple aurora glow and
|
|
1945
|
+
a horizontal sweep keyframe. Reads as "content loading" without
|
|
1946
|
+
the ugly default web-component spinner. -->
|
|
1947
|
+
<div class="animot-loading"><div class="animot-skeleton"></div></div>
|
|
1665
1948
|
{:else if error}
|
|
1666
1949
|
<div class="animot-error">{error}</div>
|
|
1667
1950
|
{:else if project && currentSlide}
|
|
@@ -1898,21 +2181,27 @@
|
|
|
1898
2181
|
{@const animFill = animated.fillColor?.current ?? shapeEl.fillColor}
|
|
1899
2182
|
{@const animStroke = animated.strokeColor?.current ?? shapeEl.strokeColor}
|
|
1900
2183
|
{@const animStrokeWidth = animated.strokeWidth?.current ?? shapeEl.strokeWidth}
|
|
1901
|
-
{@const
|
|
1902
|
-
{@const
|
|
1903
|
-
{@const
|
|
1904
|
-
{@const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
{
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
{
|
|
1914
|
-
|
|
1915
|
-
|
|
2184
|
+
{@const shapeKpm = kfPathStates.get(element.id)}
|
|
2185
|
+
{@const shapePm = pathMorphStates.get(element.id)}
|
|
2186
|
+
{@const shapePmProg = crossMorphProg.get(elementId) ?? (animated.pathMorph?.current ?? 1)}
|
|
2187
|
+
{@const shapeKpmProg = kfPathProg.get(elementId) ?? (animated.pathMorph?.current ?? 1)}
|
|
2188
|
+
{@const shapeMorphProg = shapeKpm ? shapeKpmProg : shapePmProg}
|
|
2189
|
+
{@const shapeMorphActive = shapeKpm ?? ((shapePm && shapePmProg > 0 && shapePmProg < 1) ? shapePm : restingKfMorph(element))}
|
|
2190
|
+
{#if shapeMorphActive}
|
|
2191
|
+
<svg class="animot-shape-element" width="100%" height="100%" viewBox={shapeMorphActive.viewBox ?? `0 0 ${Math.max(1, animated.width.current)} ${Math.max(1, animated.height.current)}`} preserveAspectRatio="none">
|
|
2192
|
+
{#if shapeMorphActive.parts && shapeMorphActive.parts.length > 1}
|
|
2193
|
+
{#each shapeMorphActive.parts as part}
|
|
2194
|
+
<path d={part.interp(Math.max(0, Math.min(1, shapeMorphProg)))} fill={(element as any).morphColor ?? part.toColor} fill-rule="evenodd" />
|
|
2195
|
+
{/each}
|
|
2196
|
+
{:else}
|
|
2197
|
+
<path d={shapeMorphActive.interp(Math.max(0, Math.min(1, shapeMorphProg)))} fill={animFill} stroke={animStroke} stroke-width={animStrokeWidth} />
|
|
2198
|
+
{/if}
|
|
2199
|
+
</svg>
|
|
2200
|
+
{:else}
|
|
2201
|
+
<svg class="animot-shape-element" viewBox="0 0 {Math.max(0, animated.width.current)} {Math.max(0, animated.height.current)}" fill-opacity={shapeEl.fillOpacity ?? 1} stroke-opacity={shapeEl.strokeOpacity ?? 1} style:filter={shapeEl.boxShadow?.enabled ? `drop-shadow(${shapeEl.boxShadow.offsetX}px ${shapeEl.boxShadow.offsetY}px ${shapeEl.boxShadow.blur}px ${shapeEl.boxShadow.color})` : 'none'}>
|
|
2202
|
+
{@html renderShape(shapeEl.shapeType, Math.max(0, animated.width.current), Math.max(0, animated.height.current), animated.borderRadius.current, animFill, animStroke, animStrokeWidth, shapeEl.strokeStyle, shapeEl.strokeDashGap)}
|
|
2203
|
+
</svg>
|
|
2204
|
+
{/if}
|
|
1916
2205
|
{:else if element.type === 'counter'}
|
|
1917
2206
|
<CounterRenderer element={element as CounterElement} slideId={currentSlide?.id ?? ''} />
|
|
1918
2207
|
{:else if element.type === 'chart'}
|
|
@@ -1934,28 +2223,41 @@
|
|
|
1934
2223
|
<IconRenderer element={element as IconElement} />
|
|
1935
2224
|
{:else if element.type === 'svg'}
|
|
1936
2225
|
{@const svgEl = element as SvgElement}
|
|
1937
|
-
{@const
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2226
|
+
{@const kpm = kfPathStates.get(element.id)}
|
|
2227
|
+
{@const pm = pathMorphStates.get(element.id)}
|
|
2228
|
+
{@const pmProg = crossMorphProg.get(elementId) ?? (animated.pathMorph?.current ?? 1)}
|
|
2229
|
+
{@const kpmProg = kfPathProg.get(elementId) ?? (animated.pathMorph?.current ?? 1)}
|
|
2230
|
+
{@const morphProg = kpm ? kpmProg : pmProg}
|
|
2231
|
+
{@const svgMorph = kpm ?? ((pm && pmProg > 0 && pmProg < 1) ? pm : restingKfMorph(element))}
|
|
2232
|
+
{#if svgMorph}
|
|
2233
|
+
<svg class="animot-svg-element" width="100%" height="100%" viewBox={svgMorph.viewBox ?? morphViewBox(element, animated.width.current, animated.height.current)} preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
|
2234
|
+
{#each (svgMorph.parts ?? [{ interp: svgMorph.interp, fromColor: svgMorph.fromColor, toColor: svgMorph.toColor }]) as part}
|
|
2235
|
+
<path d={part.interp(Math.max(0, Math.min(1, morphProg)))} fill={(element as any).morphColor ?? part.toColor} fill-rule="evenodd" />
|
|
2236
|
+
{/each}
|
|
2237
|
+
</svg> {:else}
|
|
2238
|
+
{@const svgParsed = (() => { const m = svgEl.svgContent.trim().match(/^<svg([^>]*)>([\s\S]*)<\/svg>$/i); if (m) { const vb = m[1].match(/viewBox=["']([^"']+)["']/i); return { inner: m[2], viewBox: vb ? vb[1] : null }; } return { inner: svgEl.svgContent, viewBox: null }; })()}
|
|
2239
|
+
{@const svgAnimMode = svgEl.animation?.mode ?? 'none'}
|
|
2240
|
+
{@const svgAnimDur = svgEl.animation?.duration ?? 800}
|
|
2241
|
+
{@const svgAnimLoop = svgEl.animation?.loop ?? false}
|
|
2242
|
+
{@const svgAnimReverse = svgEl.animation?.direction === 'reverse'}
|
|
2243
|
+
<div
|
|
2244
|
+
class="animot-svg-element"
|
|
2245
|
+
use:traceSvgPaths={{
|
|
2246
|
+
enabled: svgAnimMode !== 'none',
|
|
2247
|
+
mode: svgAnimMode as 'none' | 'draw' | 'undraw' | 'draw-undraw' | 'flow',
|
|
2248
|
+
duration: svgAnimDur,
|
|
2249
|
+
loop: svgAnimLoop,
|
|
2250
|
+
reverse: svgAnimReverse,
|
|
2251
|
+
key: `${svgEl.id}-${svgAnimMode}-${svgAnimDur}-${currentSlideIndex}`
|
|
2252
|
+
}}
|
|
2253
|
+
>
|
|
2254
|
+
<svg width="100%" height="100%" viewBox={svgEl.viewBox ?? svgParsed.viewBox ?? `0 0 ${svgEl.size.width} ${svgEl.size.height}`} preserveAspectRatio={svgEl.preserveAspectRatio} xmlns="http://www.w3.org/2000/svg">
|
|
2255
|
+
<g style={svgEl.color ? `fill:${svgEl.color};stroke:${svgEl.color}` : ''}>
|
|
2256
|
+
{@html svgParsed.inner}
|
|
2257
|
+
</g>
|
|
2258
|
+
</svg>
|
|
2259
|
+
</div>
|
|
2260
|
+
{/if}
|
|
1959
2261
|
{:else if element.type === 'motionPath'}
|
|
1960
2262
|
{@const mpEl = element as MotionPathElement}
|
|
1961
2263
|
{#if mpEl.showInPresentation}
|
|
@@ -1963,6 +2265,31 @@
|
|
|
1963
2265
|
<path d={buildPresenterPathD(mpEl.points, mpEl.closed)} stroke={mpEl.pathColor} stroke-width={mpEl.pathWidth} fill="none" stroke-dasharray="8 4" />
|
|
1964
2266
|
</svg>
|
|
1965
2267
|
{/if}
|
|
2268
|
+
{:else if element.type === 'draw'}
|
|
2269
|
+
{@const drawEl = element as DrawElement}
|
|
2270
|
+
{@const dkpm = kfPathStates.get(element.id)}
|
|
2271
|
+
{@const dpm = pathMorphStates.get(element.id)}
|
|
2272
|
+
{@const pmProg = crossMorphProg.get(elementId) ?? (animated.pathMorph?.current ?? 1)}
|
|
2273
|
+
{@const kpmProg = kfPathProg.get(elementId) ?? (animated.pathMorph?.current ?? 1)}
|
|
2274
|
+
{@const morphProg = dkpm ? kpmProg : pmProg}
|
|
2275
|
+
{@const drawMorph = dkpm ?? ((dpm && pmProg > 0 && pmProg < 1) ? dpm : restingKfMorph(element))}
|
|
2276
|
+
{@const drawPath = drawElementToPath(drawEl)}
|
|
2277
|
+
<svg class="animot-draw-element" width="100%" height="100%" viewBox={drawMorph?.viewBox ?? drawPath.viewBox} preserveAspectRatio="none" style="display:block;overflow:visible;" xmlns="http://www.w3.org/2000/svg">
|
|
2278
|
+
{#if drawMorph}
|
|
2279
|
+
{#each (drawMorph.parts ?? [{ interp: drawMorph.interp, fromColor: drawMorph.fromColor, toColor: drawMorph.toColor }]) as part}
|
|
2280
|
+
<path d={part.interp(Math.max(0, Math.min(1, morphProg)))} fill={(element as any).morphColor ?? part.toColor} fill-rule="evenodd" />
|
|
2281
|
+
{/each}
|
|
2282
|
+
{:else}
|
|
2283
|
+
<path d={drawPath.d} fill={drawEl.color} />
|
|
2284
|
+
{/if}
|
|
2285
|
+
</svg>
|
|
2286
|
+
{:else if element.type === 'sticky'}
|
|
2287
|
+
{@const stickyEl = element as StickyElement}
|
|
2288
|
+
<div class="animot-sticky-element" style:background={stickyEl.bgColor} style:color={stickyEl.textColor} style:border-radius="{stickyEl.borderRadius}px" style:padding="{stickyEl.padding}px" style:box-shadow={stickyEl.shadow === false ? 'none' : '0 10px 24px rgba(0,0,0,0.30), 0 3px 6px rgba(0,0,0,0.20)'}>
|
|
2289
|
+
<div class="animot-sticky-sheen"></div>
|
|
2290
|
+
<div class="animot-sticky-fold"></div>
|
|
2291
|
+
<div class="animot-sticky-text" style:font-size="{stickyEl.fontSize}px" style:font-family="'{stickyEl.fontFamily}', sans-serif" style:font-weight={stickyEl.fontWeight} style:text-align={stickyEl.textAlign}>{stickyEl.text}</div>
|
|
2292
|
+
</div>
|
|
1966
2293
|
{/if}
|
|
1967
2294
|
</div>
|
|
1968
2295
|
{/if}
|
|
@@ -2282,8 +2609,51 @@
|
|
|
2282
2609
|
.animot-progress-fill { height: 100%; background: linear-gradient(135deg, #7c3aed, #ec4899); transition: width 0.6s ease; }
|
|
2283
2610
|
|
|
2284
2611
|
/* Loading / Error */
|
|
2285
|
-
.animot-loading {
|
|
2286
|
-
|
|
2612
|
+
.animot-loading {
|
|
2613
|
+
display: flex;
|
|
2614
|
+
align-items: center;
|
|
2615
|
+
justify-content: center;
|
|
2616
|
+
width: 100%;
|
|
2617
|
+
height: 100%;
|
|
2618
|
+
background:
|
|
2619
|
+
radial-gradient(circle at 70% 30%, rgba(168, 66, 255, 0.12), transparent 60%),
|
|
2620
|
+
radial-gradient(circle at 25% 75%, rgba(34, 199, 214, 0.08), transparent 65%),
|
|
2621
|
+
rgba(8, 10, 24, 0.8);
|
|
2622
|
+
}
|
|
2623
|
+
.animot-skeleton {
|
|
2624
|
+
position: relative;
|
|
2625
|
+
width: 100%;
|
|
2626
|
+
height: 100%;
|
|
2627
|
+
overflow: hidden;
|
|
2628
|
+
}
|
|
2629
|
+
.animot-skeleton::before,
|
|
2630
|
+
.animot-skeleton::after {
|
|
2631
|
+
content: '';
|
|
2632
|
+
position: absolute;
|
|
2633
|
+
inset: 0;
|
|
2634
|
+
}
|
|
2635
|
+
.animot-skeleton::before {
|
|
2636
|
+
background: linear-gradient(
|
|
2637
|
+
115deg,
|
|
2638
|
+
transparent 30%,
|
|
2639
|
+
rgba(168, 66, 255, 0.18) 50%,
|
|
2640
|
+
transparent 70%
|
|
2641
|
+
);
|
|
2642
|
+
background-size: 220% 100%;
|
|
2643
|
+
animation: animot-shimmer 1.6s linear infinite;
|
|
2644
|
+
}
|
|
2645
|
+
.animot-skeleton::after {
|
|
2646
|
+
background: radial-gradient(circle at 50% 50%, rgba(168, 66, 255, 0.08), transparent 60%);
|
|
2647
|
+
animation: animot-pulse 2s ease-in-out infinite;
|
|
2648
|
+
}
|
|
2649
|
+
@keyframes animot-shimmer {
|
|
2650
|
+
0% { background-position: 220% 0; }
|
|
2651
|
+
100% { background-position: -220% 0; }
|
|
2652
|
+
}
|
|
2653
|
+
@keyframes animot-pulse {
|
|
2654
|
+
0%, 100% { opacity: 0.6; }
|
|
2655
|
+
50% { opacity: 1; }
|
|
2656
|
+
}
|
|
2287
2657
|
@keyframes animot-spin { to { transform: rotate(360deg); } }
|
|
2288
2658
|
.animot-error { color: #ef4444; padding: 20px; text-align: center; font-family: system-ui, sans-serif; }
|
|
2289
2659
|
</style>
|