animot-presenter 0.5.9 → 0.5.11
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 +5 -3
- package/dist/cdn/animot-presenter.css +1 -1
- package/dist/cdn/animot-presenter.esm.js +2466 -2402
- package/dist/cdn/animot-presenter.min.js +9 -9
- package/dist/types.d.ts +1 -0
- package/dist/utils/camera.js +12 -3
- package/dist/utils/decorations.d.ts +8 -0
- package/dist/utils/decorations.js +170 -64
- package/package.json +1 -1
package/dist/types.d.ts
CHANGED
package/dist/utils/camera.js
CHANGED
|
@@ -59,8 +59,17 @@ export function parallaxOffset(camera, depth, worldWidth, worldHeight) {
|
|
|
59
59
|
const cy = camera.y + camera.height / 2;
|
|
60
60
|
const wx = worldWidth / 2;
|
|
61
61
|
const wy = worldHeight / 2;
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
62
|
+
// Parallax magnitude is `depth × camera-offset-from-world-center × factor`.
|
|
63
|
+
// The earlier 0.4 factor blew up on large worlds (6000×4000) because the
|
|
64
|
+
// camera-offset term scales with world size — depth=0.7 gave ±840px shifts,
|
|
65
|
+
// destroying composition.
|
|
66
|
+
//
|
|
67
|
+
// Fix: scale the factor by the CAMERA WIDTH ratio to a 1920-baseline so the
|
|
68
|
+
// effect feels the same regardless of world size. A 1920-wide camera gets
|
|
69
|
+
// the original feel; a 3200-wide camera (zoomed out) gets a smaller offset
|
|
70
|
+
// per unit-of-world-distance so the layout doesn't fly apart.
|
|
71
|
+
const baseFactor = depth * 0.15;
|
|
72
|
+
const widthScale = Math.min(1, 1920 / Math.max(1, camera.width));
|
|
73
|
+
const factor = baseFactor * widthScale;
|
|
65
74
|
return { x: -(cx - wx) * factor, y: -(cy - wy) * factor };
|
|
66
75
|
}
|
|
@@ -16,6 +16,14 @@ export interface DecorationsParams {
|
|
|
16
16
|
/** Slide loop duration; when present, each effect's cycle is rounded so an
|
|
17
17
|
* integer number fits in slide_duration — guarantees seamless GIF loop. */
|
|
18
18
|
slideDuration?: number;
|
|
19
|
+
/** Optional shape info — if present, shimmer and gradientShift overlays
|
|
20
|
+
* get clipped to this silhouette. Without it both effects render against
|
|
21
|
+
* the rectangular wrapper bounds (which looks wrong on circles/hexagons/
|
|
22
|
+
* stars). Pass `{ type: 'circle' }` etc. from the host for shape elements. */
|
|
23
|
+
shape?: {
|
|
24
|
+
type: 'rectangle' | 'circle' | 'ellipse' | 'triangle' | 'hexagon' | 'star';
|
|
25
|
+
borderRadius?: number;
|
|
26
|
+
};
|
|
19
27
|
/** Bumped by the host when ANY decoration prop changes. Without it the
|
|
20
28
|
* action would silently keep running with stale params. */
|
|
21
29
|
key?: unknown;
|
|
@@ -10,6 +10,55 @@
|
|
|
10
10
|
* • gradientShift — animates background-position on a multi-color gradient
|
|
11
11
|
* • rgbSplit — chromatic-aberration-style R/B channel offset (drop-shadow)
|
|
12
12
|
*/
|
|
13
|
+
/** Build a CSS clip-path that matches a shape silhouette. Returns null for
|
|
14
|
+
* rectangles with no border-radius (no clipping needed). */
|
|
15
|
+
function shapeClipPath(shape) {
|
|
16
|
+
switch (shape.type) {
|
|
17
|
+
case 'circle': return 'circle(50% at 50% 50%)';
|
|
18
|
+
case 'ellipse': return 'ellipse(50% 50% at 50% 50%)';
|
|
19
|
+
case 'triangle': return 'polygon(50% 0%, 0% 100%, 100% 100%)';
|
|
20
|
+
case 'hexagon': return 'polygon(50% 0%, 93.3% 25%, 93.3% 75%, 50% 100%, 6.7% 75%, 6.7% 25%)';
|
|
21
|
+
case 'star': return 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)';
|
|
22
|
+
case 'rectangle':
|
|
23
|
+
return shape.borderRadius && shape.borderRadius > 0
|
|
24
|
+
? `inset(0 round ${shape.borderRadius}px)`
|
|
25
|
+
: null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// SVG mask for the gradient border ring. We stroke the shape silhouette in a
|
|
29
|
+
// 100×100 viewBox with `preserveAspectRatio=none` so it stretches to the
|
|
30
|
+
// element's box, and `vector-effect=non-scaling-stroke` so the stroke stays a
|
|
31
|
+
// constant pixel width regardless of element size or aspect ratio. The stroke
|
|
32
|
+
// is centered on the path, so half of it falls outside the silhouette — the
|
|
33
|
+
// caller's clip-path trims that outer half away, leaving an inner ring of
|
|
34
|
+
// `borderWidth` px against the silhouette edge. We therefore set stroke-width
|
|
35
|
+
// to `borderWidth * 2`.
|
|
36
|
+
function silhouetteMaskUrl(shapeType, borderWidth) {
|
|
37
|
+
const sw = borderWidth * 2;
|
|
38
|
+
const common = `vector-effect="non-scaling-stroke" fill="none" stroke="#fff" stroke-width="${sw}"`;
|
|
39
|
+
let inner = '';
|
|
40
|
+
switch (shapeType) {
|
|
41
|
+
case 'circle':
|
|
42
|
+
inner = `<circle cx="50" cy="50" r="50" ${common}/>`;
|
|
43
|
+
break;
|
|
44
|
+
case 'ellipse':
|
|
45
|
+
inner = `<ellipse cx="50" cy="50" rx="50" ry="50" ${common}/>`;
|
|
46
|
+
break;
|
|
47
|
+
case 'triangle':
|
|
48
|
+
inner = `<polygon points="50,0 0,100 100,100" ${common}/>`;
|
|
49
|
+
break;
|
|
50
|
+
case 'hexagon':
|
|
51
|
+
inner = `<polygon points="50,0 93.3,25 93.3,75 50,100 6.7,75 6.7,25" ${common}/>`;
|
|
52
|
+
break;
|
|
53
|
+
case 'star':
|
|
54
|
+
inner = `<polygon points="50,0 61,35 98,35 68,57 79,91 50,70 21,91 32,57 2,35 39,35" ${common}/>`;
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
inner = `<rect x="0" y="0" width="100" height="100" ${common}/>`;
|
|
58
|
+
}
|
|
59
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none">${inner}</svg>`;
|
|
60
|
+
return `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`;
|
|
61
|
+
}
|
|
13
62
|
function effectiveCycle(requested, slideDuration) {
|
|
14
63
|
if (slideDuration && slideDuration > 0) {
|
|
15
64
|
return slideDuration / Math.max(1, Math.round(slideDuration / requested));
|
|
@@ -19,32 +68,34 @@ function effectiveCycle(requested, slideDuration) {
|
|
|
19
68
|
export function decorations(node, params) {
|
|
20
69
|
let raf = 0;
|
|
21
70
|
let shimmerEl = null;
|
|
71
|
+
// Dedicated child overlay for the animated gradient background. We CANNOT
|
|
72
|
+
// put the gradient on the wrapper itself if a shape clip is needed —
|
|
73
|
+
// `clip-path` on the wrapper would also clip the wrapper's `filter:
|
|
74
|
+
// drop-shadow` (the glow halo), so the glow can never extend past the
|
|
75
|
+
// silhouette. By putting the gradient on a clipped child, the wrapper
|
|
76
|
+
// stays unclipped and the glow halo renders correctly outside the shape.
|
|
77
|
+
let gradientEl = null;
|
|
22
78
|
let originalBoxShadow = '';
|
|
23
79
|
let originalFilter = '';
|
|
24
|
-
let originalBackgroundImage = '';
|
|
25
|
-
let originalBackgroundSize = '';
|
|
26
|
-
let originalBackgroundPosition = '';
|
|
27
80
|
let savedOriginal = false;
|
|
28
81
|
function saveOriginalStyles() {
|
|
29
82
|
if (savedOriginal)
|
|
30
83
|
return;
|
|
31
84
|
originalBoxShadow = node.style.boxShadow;
|
|
32
85
|
originalFilter = node.style.filter;
|
|
33
|
-
originalBackgroundImage = node.style.backgroundImage;
|
|
34
|
-
originalBackgroundSize = node.style.backgroundSize;
|
|
35
|
-
originalBackgroundPosition = node.style.backgroundPosition;
|
|
36
86
|
savedOriginal = true;
|
|
37
87
|
}
|
|
38
88
|
function restoreOriginalStyles() {
|
|
39
89
|
node.style.boxShadow = originalBoxShadow;
|
|
40
90
|
node.style.filter = originalFilter;
|
|
41
|
-
node.style.backgroundImage = originalBackgroundImage;
|
|
42
|
-
node.style.backgroundSize = originalBackgroundSize;
|
|
43
|
-
node.style.backgroundPosition = originalBackgroundPosition;
|
|
44
91
|
if (shimmerEl) {
|
|
45
92
|
shimmerEl.remove();
|
|
46
93
|
shimmerEl = null;
|
|
47
94
|
}
|
|
95
|
+
if (gradientEl) {
|
|
96
|
+
gradientEl.remove();
|
|
97
|
+
gradientEl = null;
|
|
98
|
+
}
|
|
48
99
|
}
|
|
49
100
|
function reset() {
|
|
50
101
|
if (raf)
|
|
@@ -61,12 +112,13 @@ export function decorations(node, params) {
|
|
|
61
112
|
if (!anyEnabled)
|
|
62
113
|
return;
|
|
63
114
|
saveOriginalStyles();
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
//
|
|
115
|
+
// Compute the shape clip-path once — applied to shimmer + gradient
|
|
116
|
+
// children individually (NOT the wrapper) so the wrapper's glow filter
|
|
117
|
+
// halo can render outside the silhouette without being clipped.
|
|
118
|
+
const shapeClip = params.shape ? shapeClipPath(params.shape) : null;
|
|
119
|
+
// Shimmer: a child overlay with the diagonal stripe gradient. Clipped
|
|
120
|
+
// to the shape silhouette individually so circle/hexagon/etc. shapes
|
|
121
|
+
// don't get rectangular shimmer.
|
|
70
122
|
if (cfg.shimmer?.enabled) {
|
|
71
123
|
if (getComputedStyle(node).position === 'static') {
|
|
72
124
|
node.style.position = 'relative';
|
|
@@ -84,49 +136,100 @@ export function decorations(node, params) {
|
|
|
84
136
|
borderRadius: 'inherit',
|
|
85
137
|
background: `linear-gradient(${angle}deg, transparent 0%, transparent ${50 - widthPct / 2}%, ${color} 50%, transparent ${50 + widthPct / 2}%, transparent 100%)`,
|
|
86
138
|
backgroundSize: '300% 300%',
|
|
87
|
-
// Default `repeat` would tile the gradient — when the bg slides past
|
|
88
|
-
// an edge, the next tile's stripe enters from the opposite side
|
|
89
|
-
// (the "two squares touching corners" artifact). One stripe per cycle.
|
|
90
139
|
backgroundRepeat: 'no-repeat',
|
|
91
140
|
backgroundPosition: '-100% -100%',
|
|
92
|
-
willChange: 'background-position'
|
|
141
|
+
willChange: 'background-position',
|
|
142
|
+
...(shapeClip ? { clipPath: shapeClip } : {})
|
|
93
143
|
});
|
|
94
144
|
node.appendChild(shimmerEl);
|
|
95
145
|
}
|
|
96
|
-
//
|
|
146
|
+
// Animated gradient: border-only ring along the element's silhouette.
|
|
147
|
+
// The user's fillColor stays visible inside; only the perimeter receives
|
|
148
|
+
// the animated gradient. Two strategies:
|
|
149
|
+
// • Polygon / curved shapes (circle, ellipse, triangle, hexagon, star):
|
|
150
|
+
// SVG mask of a stroked silhouette (vector-effect=non-scaling-stroke)
|
|
151
|
+
// combined with the shape clip-path that trims the outer half of the
|
|
152
|
+
// stroke — the inner half (= borderWidth px) forms the visible ring.
|
|
153
|
+
// • Rectangles / non-shape elements (text, code, image, icon):
|
|
154
|
+
// classic CSS gradient-border trick — transparent border + two-layer
|
|
155
|
+
// mask with `mask-composite: exclude` so only the border ring shows.
|
|
156
|
+
// Respects border-radius via `borderRadius: inherit`.
|
|
97
157
|
if (cfg.gradientShift?.enabled) {
|
|
158
|
+
if (getComputedStyle(node).position === 'static') {
|
|
159
|
+
node.style.position = 'relative';
|
|
160
|
+
}
|
|
98
161
|
const colors = cfg.gradientShift.colors ?? ['#7c3aed', '#06b6d4', '#ec4899', '#7c3aed'];
|
|
99
162
|
const angle = cfg.gradientShift.angle ?? 135;
|
|
100
163
|
const direction = cfg.gradientShift.direction ?? 'forward';
|
|
164
|
+
const borderWidth = cfg.gradientShift.borderWidth ?? 3;
|
|
165
|
+
gradientEl = document.createElement('div');
|
|
166
|
+
gradientEl.className = 'animot-gradient-shift';
|
|
167
|
+
Object.assign(gradientEl.style, {
|
|
168
|
+
position: 'absolute',
|
|
169
|
+
inset: '0',
|
|
170
|
+
pointerEvents: 'none',
|
|
171
|
+
zIndex: '0'
|
|
172
|
+
});
|
|
173
|
+
const useSilhouetteMask = params.shape && params.shape.type !== 'rectangle';
|
|
174
|
+
if (useSilhouetteMask) {
|
|
175
|
+
const svgMask = silhouetteMaskUrl(params.shape.type, borderWidth);
|
|
176
|
+
gradientEl.style.webkitMaskImage = svgMask;
|
|
177
|
+
gradientEl.style.maskImage = svgMask;
|
|
178
|
+
gradientEl.style.webkitMaskRepeat = 'no-repeat';
|
|
179
|
+
gradientEl.style.maskRepeat = 'no-repeat';
|
|
180
|
+
gradientEl.style.webkitMaskSize = '100% 100%';
|
|
181
|
+
gradientEl.style.maskSize = '100% 100%';
|
|
182
|
+
// Trim the outer half of the stroke so the visible ring sits
|
|
183
|
+
// flush with the silhouette edge instead of bleeding outside.
|
|
184
|
+
if (shapeClip)
|
|
185
|
+
gradientEl.style.clipPath = shapeClip;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
gradientEl.style.boxSizing = 'border-box';
|
|
189
|
+
gradientEl.style.border = `${borderWidth}px solid transparent`;
|
|
190
|
+
gradientEl.style.borderRadius = 'inherit';
|
|
191
|
+
const m = 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)';
|
|
192
|
+
gradientEl.style.webkitMask = m;
|
|
193
|
+
gradientEl.style.mask = m;
|
|
194
|
+
gradientEl.style.webkitMaskComposite = 'xor';
|
|
195
|
+
gradientEl.style.maskComposite = 'exclude';
|
|
196
|
+
}
|
|
101
197
|
if (direction === 'chase') {
|
|
102
|
-
// Conic gradient: each color is a slice of the pie. Repeat the first
|
|
103
|
-
// color at the end so the seam where it wraps doesn't show. The tick
|
|
104
|
-
// loop animates the `from` angle every frame.
|
|
105
198
|
const stops = [...colors, colors[0]].join(', ');
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
199
|
+
gradientEl.style.backgroundImage = `conic-gradient(from 0deg at 50% 50%, ${stops})`;
|
|
200
|
+
gradientEl.style.backgroundSize = '100% 100%';
|
|
201
|
+
gradientEl.style.backgroundPosition = '0 0';
|
|
109
202
|
}
|
|
110
203
|
else {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// sweep before the gradient repeats. 400% lets snake mode swing back
|
|
114
|
-
// and forth without ever showing a tile seam, even at 45° angles.
|
|
115
|
-
node.style.backgroundSize = '400% 400%';
|
|
204
|
+
gradientEl.style.backgroundImage = `linear-gradient(${angle}deg, ${colors.join(', ')})`;
|
|
205
|
+
gradientEl.style.backgroundSize = '400% 400%';
|
|
116
206
|
}
|
|
207
|
+
// Insert as the FIRST child so it sits behind the actual element
|
|
208
|
+
// content (text, code block, etc.) instead of covering it.
|
|
209
|
+
node.insertBefore(gradientEl, node.firstChild);
|
|
117
210
|
}
|
|
118
211
|
const start = performance.now();
|
|
119
212
|
function tick(now) {
|
|
213
|
+
// Re-alias cfg as non-null inside the tick closure — TS loses the
|
|
214
|
+
// `if (!cfg) return` narrowing across the function boundary,
|
|
215
|
+
// otherwise every cfg.* access flags as "possibly undefined".
|
|
216
|
+
const c = cfg;
|
|
120
217
|
const elapsed = now - start;
|
|
121
|
-
// GLOW — pulsing box-shadow
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
218
|
+
// GLOW — pulsing halo. Uses `filter: drop-shadow` (not box-shadow)
|
|
219
|
+
// so the halo follows the element's actual ALPHA MASK — SVG shapes,
|
|
220
|
+
// icons, and PNGs with transparency get a halo around the visible
|
|
221
|
+
// silhouette, not around their rectangular bounding box. Composed
|
|
222
|
+
// with rgbSplit below into a single filter string.
|
|
223
|
+
let glowFilter = '';
|
|
224
|
+
if (c.glow?.enabled) {
|
|
225
|
+
const cycle = effectiveCycle(c.glow.speedMs ?? 2400, params.slideDuration);
|
|
226
|
+
const phase = (elapsed % cycle) / cycle;
|
|
227
|
+
const wave = (Math.sin(phase * Math.PI * 2) + 1) / 2;
|
|
228
|
+
const intensity = c.glow.intensity ?? 0.6;
|
|
229
|
+
const blur = 6 + wave * 24 * intensity;
|
|
230
|
+
// Two stacked drop-shadows give a denser halo than a single one
|
|
231
|
+
// (drop-shadow has no `spread` parameter, so we layer them).
|
|
232
|
+
glowFilter = `drop-shadow(0 0 ${blur}px ${c.glow.color}) drop-shadow(0 0 ${blur * 0.4}px ${c.glow.color})`;
|
|
130
233
|
}
|
|
131
234
|
// SHIMMER — sweep the stretched gradient ALONG its own gradient
|
|
132
235
|
// direction (derived from `angle`) so the stripe glides perpendicular
|
|
@@ -137,10 +240,10 @@ export function decorations(node, params) {
|
|
|
137
240
|
// instead of metronomic. Per-cycle state (start time, duration,
|
|
138
241
|
// pause length) is stashed on the wrapper so we don't burn a closure
|
|
139
242
|
// rebuild every frame.
|
|
140
|
-
if (
|
|
141
|
-
const baseSpeed =
|
|
243
|
+
if (c.shimmer?.enabled && shimmerEl) {
|
|
244
|
+
const baseSpeed = c.shimmer.speedMs ?? 3000;
|
|
142
245
|
const baseCycle = effectiveCycle(baseSpeed, params.slideDuration);
|
|
143
|
-
const randomness = Math.max(0, Math.min(1,
|
|
246
|
+
const randomness = Math.max(0, Math.min(1, c.shimmer.randomness ?? 0));
|
|
144
247
|
const w = shimmerEl;
|
|
145
248
|
if (typeof w.__shimCycleStart !== 'number') {
|
|
146
249
|
w.__shimCycleStart = elapsed;
|
|
@@ -167,7 +270,7 @@ export function decorations(node, params) {
|
|
|
167
270
|
w.__shimCycleStart = elapsed;
|
|
168
271
|
local = 0;
|
|
169
272
|
}
|
|
170
|
-
const angle =
|
|
273
|
+
const angle = c.shimmer.angle ?? 110;
|
|
171
274
|
const rad = angle * Math.PI / 180;
|
|
172
275
|
const dirX = Math.sin(rad);
|
|
173
276
|
const dirY = -Math.cos(rad);
|
|
@@ -193,22 +296,21 @@ export function decorations(node, params) {
|
|
|
193
296
|
// next side" pinwheel)
|
|
194
297
|
// Sweep direction (for non-chase modes) is derived from `angle` so
|
|
195
298
|
// the gradient flows perpendicular to its own bands.
|
|
196
|
-
if (
|
|
197
|
-
const cycle = effectiveCycle(
|
|
198
|
-
const direction =
|
|
299
|
+
if (c.gradientShift?.enabled) {
|
|
300
|
+
const cycle = effectiveCycle(c.gradientShift.speedMs ?? 6000, params.slideDuration);
|
|
301
|
+
const direction = c.gradientShift.direction ?? 'forward';
|
|
302
|
+
// Write to the gradient child element instead of the wrapper —
|
|
303
|
+
// the wrapper carries the glow filter, which would be clipped
|
|
304
|
+
// if the wrapper itself had clip-path.
|
|
305
|
+
const target = gradientEl ?? node;
|
|
199
306
|
if (direction === 'chase') {
|
|
200
|
-
|
|
201
|
-
// angle. Cheap to render — modern browsers compose conic
|
|
202
|
-
// gradients on the GPU. Color list is closed (first color
|
|
203
|
-
// repeated at end) so the seam where 360° wraps to 0° is
|
|
204
|
-
// invisible.
|
|
205
|
-
const colors = cfg.gradientShift.colors ?? ['#7c3aed', '#06b6d4', '#ec4899', '#7c3aed'];
|
|
307
|
+
const colors = c.gradientShift.colors ?? ['#7c3aed', '#06b6d4', '#ec4899', '#7c3aed'];
|
|
206
308
|
const stops = [...colors, colors[0]].join(', ');
|
|
207
309
|
const rotateDeg = ((elapsed % cycle) / cycle) * 360;
|
|
208
|
-
|
|
310
|
+
target.style.backgroundImage = `conic-gradient(from ${rotateDeg}deg at 50% 50%, ${stops})`;
|
|
209
311
|
}
|
|
210
312
|
else {
|
|
211
|
-
const angle =
|
|
313
|
+
const angle = c.gradientShift.angle ?? 135;
|
|
212
314
|
const rad = angle * Math.PI / 180;
|
|
213
315
|
const dirX = Math.sin(rad);
|
|
214
316
|
const dirY = -Math.cos(rad);
|
|
@@ -222,27 +324,31 @@ export function decorations(node, params) {
|
|
|
222
324
|
else {
|
|
223
325
|
phase = (elapsed % cycle) / cycle;
|
|
224
326
|
}
|
|
225
|
-
// Sweep range = 200% of host (centered at 50%). With
|
|
226
|
-
// backgroundSize 400%, position values from -50% to 250% all
|
|
227
|
-
// keep the gradient fully covering the element across all angles.
|
|
228
327
|
const range = 200;
|
|
229
328
|
const offset = (phase - 0.5) * range;
|
|
230
329
|
const posX = 50 + offset * dirX;
|
|
231
330
|
const posY = 50 + offset * dirY;
|
|
232
|
-
|
|
331
|
+
target.style.backgroundPosition = `${posX}% ${posY}%`;
|
|
233
332
|
}
|
|
234
333
|
}
|
|
235
334
|
// RGB SPLIT — chromatic aberration via stacked drop-shadows.
|
|
236
|
-
|
|
237
|
-
|
|
335
|
+
let rgbSplitFilter = '';
|
|
336
|
+
if (c.rgbSplit?.enabled) {
|
|
337
|
+
const offset = c.rgbSplit.offset ?? 3;
|
|
238
338
|
let dx = offset;
|
|
239
|
-
if ((
|
|
240
|
-
const cycle = effectiveCycle(
|
|
339
|
+
if ((c.rgbSplit.speedMs ?? 0) > 0) {
|
|
340
|
+
const cycle = effectiveCycle(c.rgbSplit.speedMs, params.slideDuration);
|
|
241
341
|
const phase = (elapsed % cycle) / cycle;
|
|
242
342
|
dx = offset * Math.sin(phase * Math.PI * 2);
|
|
243
343
|
}
|
|
244
|
-
|
|
344
|
+
rgbSplitFilter = `drop-shadow(${dx}px 0 0 rgba(255, 0, 64, 0.7)) drop-shadow(${-dx}px 0 0 rgba(0, 200, 255, 0.7))`;
|
|
245
345
|
}
|
|
346
|
+
// Compose the user's original filter + glow + rgbSplit into a single
|
|
347
|
+
// `filter` value. Writing each effect to node.style.filter
|
|
348
|
+
// independently would clobber the others, since CSS filter is one
|
|
349
|
+
// string property — not a multi-value list.
|
|
350
|
+
const composed = [originalFilter, glowFilter, rgbSplitFilter].filter(Boolean).join(' ');
|
|
351
|
+
node.style.filter = composed;
|
|
246
352
|
raf = requestAnimationFrame(tick);
|
|
247
353
|
}
|
|
248
354
|
raf = requestAnimationFrame(tick);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "animot-presenter",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "Embed animated presentations anywhere. Works with vanilla JS, React, Vue, Angular, Svelte, and any frontend framework. Morphing animations, code highlighting, charts, particles, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./dist/index.js",
|