animot-presenter 0.1.0

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.
@@ -0,0 +1,6 @@
1
+ import { type Highlighter } from 'shiki';
2
+ export declare function getHighlighter(): Promise<Highlighter>;
3
+ export interface HighlightOptions {
4
+ showLineNumbers?: boolean;
5
+ }
6
+ export declare function highlightCode(code: string, language: string, theme?: string, options?: HighlightOptions): Promise<string>;
@@ -0,0 +1,45 @@
1
+ import { createHighlighter } from 'shiki';
2
+ let highlighterPromise = null;
3
+ const THEMES = ['github-dark', 'github-light', 'dracula', 'nord', 'one-dark-pro', 'vitesse-dark'];
4
+ const LANGUAGES = [
5
+ 'javascript', 'typescript', 'python', 'rust', 'go', 'java', 'cpp', 'c',
6
+ 'csharp', 'php', 'ruby', 'swift', 'kotlin', 'html', 'css', 'scss',
7
+ 'json', 'yaml', 'markdown', 'sql', 'bash', 'shell', 'dockerfile', 'svelte', 'vue', 'jsx', 'tsx'
8
+ ];
9
+ export async function getHighlighter() {
10
+ if (!highlighterPromise) {
11
+ highlighterPromise = createHighlighter({ themes: THEMES, langs: LANGUAGES });
12
+ }
13
+ return highlighterPromise;
14
+ }
15
+ export async function highlightCode(code, language, theme = 'github-dark', options = {}) {
16
+ try {
17
+ const highlighter = await getHighlighter();
18
+ const loadedLangs = highlighter.getLoadedLanguages();
19
+ if (!loadedLangs.includes(language)) {
20
+ language = 'javascript';
21
+ }
22
+ const html = highlighter.codeToHtml(code, { lang: language, theme });
23
+ if (options.showLineNumbers)
24
+ return addLineNumbers(html);
25
+ return html;
26
+ }
27
+ catch (e) {
28
+ console.error('Highlighting error:', e);
29
+ return `<pre><code>${escapeHtml(code)}</code></pre>`;
30
+ }
31
+ }
32
+ function addLineNumbers(html) {
33
+ const codeMatch = html.match(/<code[^>]*>([\s\S]*)<\/code>/);
34
+ if (!codeMatch)
35
+ return html;
36
+ const codeContent = codeMatch[1];
37
+ const codeLines = codeContent.split('\n');
38
+ const numberedLines = codeLines.map((line, i) => {
39
+ return `<span class="line-number">${i + 1}</span>${line}`;
40
+ }).join('\n');
41
+ return html.replace(codeMatch[1], numberedLines);
42
+ }
43
+ function escapeHtml(str) {
44
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
45
+ }
@@ -0,0 +1,3 @@
1
+ export { default as AnimotPresenter } from './AnimotPresenter.svelte';
2
+ export type { AnimotProject, AnimotPresenterProps, Slide, SlideCanvas, CanvasElement, CanvasBackground, TransitionConfig, TransitionType, ProjectSettings, BaseElement, CodeElement, TextElement, ArrowElement, ImageElement, ShapeElement, CounterElement, ChartElement, IconElement, ElementAnimationConfig, FloatingAnimationConfig, ParticlesConfig, ConfettiConfig } from './types';
3
+ export { AnimotPresenterElement } from './element';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { default as AnimotPresenter } from './AnimotPresenter.svelte';
2
+ // Re-export element registration for convenience
3
+ // Users who just want the web component can import 'animot-presenter/element'
4
+ export { AnimotPresenterElement } from './element';
@@ -0,0 +1,341 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import type { ChartElement } from '../types';
4
+
5
+ interface Props { element: ChartElement; slideId: string; }
6
+ let { element, slideId }: Props = $props();
7
+
8
+ let animationProgress = $state(1);
9
+ let animationFrame: number | null = null;
10
+
11
+ function animateChart() {
12
+ const startTime = performance.now();
13
+ const duration = element.animationDuration;
14
+
15
+ function tick() {
16
+ const elapsed = performance.now() - startTime;
17
+ const progress = Math.min(elapsed / duration, 1);
18
+ // Ease out cubic
19
+ const eased = 1 - Math.pow(1 - progress, 3);
20
+ animationProgress = eased;
21
+
22
+ if (progress < 1) {
23
+ animationFrame = requestAnimationFrame(tick);
24
+ }
25
+ }
26
+
27
+ // Cancel any existing animation
28
+ if (animationFrame) cancelAnimationFrame(animationFrame);
29
+
30
+ // Start animation after a short delay
31
+ setTimeout(() => {
32
+ animationProgress = 0;
33
+ animationFrame = requestAnimationFrame(tick);
34
+ }, 200);
35
+ }
36
+
37
+ // Trigger animation on mount
38
+ onMount(() => {
39
+ animateChart();
40
+ return () => {
41
+ if (animationFrame) cancelAnimationFrame(animationFrame);
42
+ };
43
+ });
44
+
45
+ // Also trigger when slideId changes
46
+ $effect(() => {
47
+ if (slideId) {
48
+ animateChart();
49
+ }
50
+ });
51
+
52
+ // Calculate max value for scaling
53
+ const maxValue = $derived(Math.max(...element.data.map(d => d.value), 1));
54
+
55
+ // Calculate line/area chart points
56
+ const linePoints = $derived.by(() => {
57
+ return element.data.map((d, i) => {
58
+ const x = 10 + (i / Math.max(element.data.length - 1, 1)) * 80;
59
+ const y = 90 - (d.value / maxValue) * 80 * animationProgress;
60
+ return `${x},${y}`;
61
+ }).join(' ');
62
+ });
63
+
64
+ // Get color for data point
65
+ function getColor(index: number, customColor?: string): string {
66
+ if (customColor) return customColor;
67
+ return element.colors[index % element.colors.length];
68
+ }
69
+
70
+ // Calculate pie/donut segments
71
+ const pieSegments = $derived.by(() => {
72
+ const total = element.data.reduce((sum, d) => sum + d.value, 0);
73
+ let currentAngle = -90; // Start from top
74
+
75
+ return element.data.map((d, i) => {
76
+ const percentage = d.value / total;
77
+ const angle = percentage * 360 * animationProgress;
78
+ const startAngle = currentAngle;
79
+ currentAngle += angle;
80
+
81
+ return {
82
+ ...d,
83
+ startAngle,
84
+ endAngle: currentAngle,
85
+ percentage,
86
+ color: getColor(i, d.color)
87
+ };
88
+ });
89
+ });
90
+
91
+ // SVG arc path for pie
92
+ function describeArc(cx: number, cy: number, r: number, startAngle: number, endAngle: number): string {
93
+ const start = polarToCartesian(cx, cy, r, endAngle);
94
+ const end = polarToCartesian(cx, cy, r, startAngle);
95
+ const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1;
96
+
97
+ return [
98
+ 'M', cx, cy,
99
+ 'L', start.x, start.y,
100
+ 'A', r, r, 0, largeArcFlag, 0, end.x, end.y,
101
+ 'Z'
102
+ ].join(' ');
103
+ }
104
+
105
+ // SVG arc path for donut
106
+ function describeDonutArc(cx: number, cy: number, outerR: number, innerR: number, startAngle: number, endAngle: number): string {
107
+ const outerStart = polarToCartesian(cx, cy, outerR, endAngle);
108
+ const outerEnd = polarToCartesian(cx, cy, outerR, startAngle);
109
+ const innerStart = polarToCartesian(cx, cy, innerR, endAngle);
110
+ const innerEnd = polarToCartesian(cx, cy, innerR, startAngle);
111
+ const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1;
112
+
113
+ return [
114
+ 'M', outerStart.x, outerStart.y,
115
+ 'A', outerR, outerR, 0, largeArcFlag, 0, outerEnd.x, outerEnd.y,
116
+ 'L', innerEnd.x, innerEnd.y,
117
+ 'A', innerR, innerR, 0, largeArcFlag, 1, innerStart.x, innerStart.y,
118
+ 'Z'
119
+ ].join(' ');
120
+ }
121
+
122
+ function polarToCartesian(cx: number, cy: number, r: number, angle: number) {
123
+ const rad = (angle * Math.PI) / 180;
124
+ return {
125
+ x: cx + r * Math.cos(rad),
126
+ y: cy + r * Math.sin(rad)
127
+ };
128
+ }
129
+ </script>
130
+
131
+ <div
132
+ class="chart"
133
+ style:background-color={element.backgroundColor}
134
+ style:padding="{element.padding}px"
135
+ style:border-radius="{element.borderRadius}px"
136
+ style:color={element.textColor}
137
+ style:font-size="{element.fontSize}px"
138
+ >
139
+ {#if element.title}
140
+ <div class="chart-title">{element.title}</div>
141
+ {/if}
142
+
143
+ <div class="chart-content">
144
+ {#if element.chartType === 'bar'}
145
+ <svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" class="bar-chart">
146
+ {#if element.showGrid}
147
+ {#each [0, 25, 50, 75, 100] as y}
148
+ <line x1="0" y1={y} x2="100" y2={y} stroke="currentColor" stroke-opacity="0.1" stroke-width="0.5" />
149
+ {/each}
150
+ {/if}
151
+ {#each element.data as point, i}
152
+ {@const barWidth = 80 / element.data.length}
153
+ {@const barX = 10 + i * (barWidth + 5)}
154
+ {@const barHeight = (point.value / maxValue) * 80 * animationProgress}
155
+ <rect
156
+ x={barX}
157
+ y={90 - barHeight}
158
+ width={barWidth}
159
+ height={barHeight}
160
+ fill={getColor(i, point.color)}
161
+ rx="1"
162
+ />
163
+ {#if element.showLabels}
164
+ <text
165
+ x={barX + barWidth / 2}
166
+ y="98"
167
+ text-anchor="middle"
168
+ font-size="4"
169
+ fill="currentColor"
170
+ >{point.label}</text>
171
+ {/if}
172
+ {#if element.showValues}
173
+ <text
174
+ x={barX + barWidth / 2}
175
+ y={88 - barHeight}
176
+ text-anchor="middle"
177
+ font-size="3.5"
178
+ fill="currentColor"
179
+ >{Math.round(point.value * animationProgress)}</text>
180
+ {/if}
181
+ {/each}
182
+ </svg>
183
+ {:else if element.chartType === 'line' || element.chartType === 'area'}
184
+ <svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" class="line-chart">
185
+ {#if element.showGrid}
186
+ {#each [0, 25, 50, 75, 100] as y}
187
+ <line x1="0" y1={y} x2="100" y2={y} stroke="currentColor" stroke-opacity="0.1" stroke-width="0.5" />
188
+ {/each}
189
+ {#each element.data as _, i}
190
+ {@const x = 10 + (i / (element.data.length - 1)) * 80}
191
+ <line x1={x} y1="10" x2={x} y2="90" stroke="currentColor" stroke-opacity="0.1" stroke-width="0.5" />
192
+ {/each}
193
+ {/if}
194
+
195
+ {#if element.chartType === 'area'}
196
+ <polygon
197
+ points="{linePoints} {10 + 80},90 10,90"
198
+ fill={element.colors[0]}
199
+ fill-opacity="0.3"
200
+ />
201
+ {/if}
202
+
203
+ <polyline
204
+ points={linePoints}
205
+ fill="none"
206
+ stroke={element.colors[0]}
207
+ stroke-width="2"
208
+ stroke-linecap="round"
209
+ stroke-linejoin="round"
210
+ />
211
+
212
+ {#each element.data as point, i}
213
+ {@const x = 10 + (i / Math.max(element.data.length - 1, 1)) * 80}
214
+ {@const y = 90 - (point.value / maxValue) * 80 * animationProgress}
215
+ <circle cx={x} cy={y} r="2" fill={getColor(i, point.color) || element.colors[0]} />
216
+ {#if element.showLabels}
217
+ <text x={x} y="98" text-anchor="middle" font-size="4" fill="currentColor">{point.label}</text>
218
+ {/if}
219
+ {#if element.showValues}
220
+ <text x={x} y={y - 4} text-anchor="middle" font-size="3.5" fill="currentColor">{Math.round(point.value * animationProgress)}</text>
221
+ {/if}
222
+ {/each}
223
+ </svg>
224
+ {:else if element.chartType === 'pie'}
225
+ <svg viewBox="0 0 100 100" class="pie-chart">
226
+ {#each pieSegments as segment, i}
227
+ {#if segment.endAngle - segment.startAngle > 0.1}
228
+ {@const midAngle = (segment.startAngle + segment.endAngle) / 2}
229
+ {@const valuePos = polarToCartesian(50, 50, 28, midAngle)}
230
+ {@const labelPos = polarToCartesian(50, 50, 48, midAngle)}
231
+ {@const textAnchor = midAngle > -90 && midAngle < 90 ? 'start' : 'end'}
232
+ <path
233
+ d={describeArc(50, 50, 40, segment.startAngle, segment.endAngle)}
234
+ fill={segment.color}
235
+ />
236
+ {#if element.showValues}
237
+ <text
238
+ x={valuePos.x}
239
+ y={valuePos.y}
240
+ text-anchor="middle"
241
+ dominant-baseline="middle"
242
+ font-size="4"
243
+ fill="white"
244
+ font-weight="600"
245
+ >{Math.round(segment.percentage * 100)}%</text>
246
+ {/if}
247
+ {#if element.showLabels}
248
+ <text
249
+ x={labelPos.x}
250
+ y={labelPos.y}
251
+ text-anchor={textAnchor}
252
+ dominant-baseline="middle"
253
+ font-size="3.5"
254
+ fill="currentColor"
255
+ >{element.data[i].label}</text>
256
+ {/if}
257
+ {/if}
258
+ {/each}
259
+ {#if element.showLegend}
260
+ {#each element.data as point, i}
261
+ <rect x="85" y={10 + i * 8} width="4" height="4" fill={getColor(i, point.color)} />
262
+ <text x="91" y={14 + i * 8} font-size="4" fill="currentColor">{point.label}</text>
263
+ {/each}
264
+ {/if}
265
+ </svg>
266
+ {:else if element.chartType === 'donut'}
267
+ <svg viewBox="0 0 100 100" class="donut-chart">
268
+ {#each pieSegments as segment, i}
269
+ {#if segment.endAngle - segment.startAngle > 0.1}
270
+ {@const midAngle = (segment.startAngle + segment.endAngle) / 2}
271
+ {@const valuePos = polarToCartesian(50, 50, 32.5, midAngle)}
272
+ {@const labelPos = polarToCartesian(50, 50, 48, midAngle)}
273
+ {@const textAnchor = midAngle > -90 && midAngle < 90 ? 'start' : 'end'}
274
+ <path
275
+ d={describeDonutArc(50, 50, 40, 25, segment.startAngle, segment.endAngle)}
276
+ fill={segment.color}
277
+ />
278
+ {#if element.showValues}
279
+ <text
280
+ x={valuePos.x}
281
+ y={valuePos.y}
282
+ text-anchor="middle"
283
+ dominant-baseline="middle"
284
+ font-size="3.5"
285
+ fill="white"
286
+ font-weight="600"
287
+ >{Math.round(segment.percentage * 100)}%</text>
288
+ {/if}
289
+ {#if element.showLabels}
290
+ <text
291
+ x={labelPos.x}
292
+ y={labelPos.y}
293
+ text-anchor={textAnchor}
294
+ dominant-baseline="middle"
295
+ font-size="3.5"
296
+ fill="currentColor"
297
+ >{element.data[i].label}</text>
298
+ {/if}
299
+ {/if}
300
+ {/each}
301
+ {#if element.showLegend}
302
+ {#each element.data as point, i}
303
+ <rect x="85" y={10 + i * 8} width="4" height="4" fill={getColor(i, point.color)} />
304
+ <text x="91" y={14 + i * 8} font-size="4" fill="currentColor">{point.label}</text>
305
+ {/each}
306
+ {/if}
307
+ </svg>
308
+ {/if}
309
+ </div>
310
+ </div>
311
+
312
+ <style>
313
+ .chart {
314
+ width: 100%;
315
+ height: 100%;
316
+ display: flex;
317
+ flex-direction: column;
318
+ box-sizing: border-box;
319
+ overflow: hidden;
320
+ }
321
+
322
+ .chart-title {
323
+ font-weight: 600;
324
+ text-align: center;
325
+ margin-bottom: 8px;
326
+ }
327
+
328
+ .chart-content {
329
+ flex: 1;
330
+ min-height: 0;
331
+ }
332
+
333
+ .chart-content svg {
334
+ width: 100%;
335
+ height: 100%;
336
+ }
337
+
338
+ .bar-chart, .line-chart {
339
+ overflow: visible;
340
+ }
341
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { ChartElement } from '../types';
2
+ interface Props {
3
+ element: ChartElement;
4
+ slideId: string;
5
+ }
6
+ declare const ChartRenderer: import("svelte").Component<Props, {}, "">;
7
+ type ChartRenderer = ReturnType<typeof ChartRenderer>;
8
+ export default ChartRenderer;
@@ -0,0 +1,64 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import type { CounterElement } from '../types';
4
+
5
+ interface Props { element: CounterElement; slideId: string; }
6
+ let { element, slideId }: Props = $props();
7
+
8
+ let displayValue = $state(0);
9
+ let animationFrame: number | null = null;
10
+
11
+ function animateCounter() {
12
+ const startTime = performance.now();
13
+ const startVal = element.startValue;
14
+ const endVal = element.endValue;
15
+ const dur = element.duration;
16
+
17
+ function tick() {
18
+ const elapsed = performance.now() - startTime;
19
+ const progress = Math.min(elapsed / dur, 1);
20
+ const eased = 1 - Math.pow(1 - progress, 3);
21
+ displayValue = startVal + (endVal - startVal) * eased;
22
+ if (progress < 1) animationFrame = requestAnimationFrame(tick);
23
+ }
24
+
25
+ if (animationFrame) cancelAnimationFrame(animationFrame);
26
+ setTimeout(() => { displayValue = startVal; animationFrame = requestAnimationFrame(tick); }, 200);
27
+ }
28
+
29
+ onMount(() => { animateCounter(); return () => { if (animationFrame) cancelAnimationFrame(animationFrame); }; });
30
+ $effect(() => { if (slideId) animateCounter(); });
31
+
32
+ function formatValue(val: number): string {
33
+ switch (element.counterType) {
34
+ case 'letter': return String.fromCharCode(65 + (Math.round(val) % 26));
35
+ case 'percentage': return val.toFixed(element.decimals) + '%';
36
+ case 'currency': return '$' + val.toLocaleString('en-US', { minimumFractionDigits: element.decimals, maximumFractionDigits: element.decimals });
37
+ default: return element.decimals === 0 ? Math.round(val).toLocaleString() : val.toFixed(element.decimals);
38
+ }
39
+ }
40
+
41
+ const formattedValue = $derived(element.prefix + formatValue(displayValue) + element.suffix);
42
+ </script>
43
+
44
+ <div
45
+ class="counter"
46
+ style:font-size="{element.fontSize}px"
47
+ style:font-weight={element.fontWeight}
48
+ style:font-family="'{element.fontFamily}', sans-serif"
49
+ style:color={element.color}
50
+ style:background-color={element.backgroundColor}
51
+ style:padding="{element.padding}px"
52
+ style:border-radius="{element.borderRadius}px"
53
+ style:text-align={element.textAlign}
54
+ >
55
+ {formattedValue}
56
+ </div>
57
+
58
+ <style>
59
+ .counter {
60
+ width: 100%; height: 100%; display: flex; align-items: center;
61
+ justify-content: center; box-sizing: border-box; overflow: hidden;
62
+ white-space: nowrap;
63
+ }
64
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { CounterElement } from '../types';
2
+ interface Props {
3
+ element: CounterElement;
4
+ slideId: string;
5
+ }
6
+ declare const CounterRenderer: import("svelte").Component<Props, {}, "">;
7
+ type CounterRenderer = ReturnType<typeof CounterRenderer>;
8
+ export default CounterRenderer;
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import type { IconElement } from '../types';
3
+ interface Props { element: IconElement; }
4
+ let { element }: Props = $props();
5
+
6
+ const svgMarkup = $derived(() => {
7
+ const fill = element.fillMode === 'fill' || element.fillMode === 'both' ? element.color : 'none';
8
+ const stroke = element.fillMode === 'stroke' || element.fillMode === 'both' ? element.color : 'none';
9
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="${fill}" stroke="${stroke}" stroke-width="${element.strokeWidth}" stroke-linecap="round" stroke-linejoin="round">${element.svgContent}</svg>`;
10
+ });
11
+ </script>
12
+
13
+ <div class="icon-element">{@html svgMarkup()}</div>
14
+
15
+ <style>
16
+ .icon-element { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
17
+ .icon-element :global(svg) { width: 100%; height: 100%; }
18
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { IconElement } from '../types';
2
+ interface Props {
3
+ element: IconElement;
4
+ }
5
+ declare const IconRenderer: import("svelte").Component<Props, {}, "">;
6
+ type IconRenderer = ReturnType<typeof IconRenderer>;
7
+ export default IconRenderer;
@@ -0,0 +1,48 @@
1
+ /* ───── Floating animations ───── */
2
+ @keyframes float-vertical {
3
+ 0%, 100% { translate: 0 0; }
4
+ 50% { translate: 0 calc(-1 * var(--float-amp, 10px)); }
5
+ }
6
+
7
+ @keyframes float-horizontal {
8
+ 0%, 100% { translate: 0 0; }
9
+ 50% { translate: var(--float-amp, 10px) 0; }
10
+ }
11
+
12
+ @keyframes float-both-0 {
13
+ 0% { translate: 0 0; }
14
+ 15% { translate: calc(0.8 * var(--float-amp, 10px)) calc(-0.6 * var(--float-amp, 10px)); }
15
+ 35% { translate: calc(-0.4 * var(--float-amp, 10px)) calc(-1 * var(--float-amp, 10px)); }
16
+ 55% { translate: calc(-0.9 * var(--float-amp, 10px)) calc(0.3 * var(--float-amp, 10px)); }
17
+ 75% { translate: calc(0.3 * var(--float-amp, 10px)) calc(0.7 * var(--float-amp, 10px)); }
18
+ 100% { translate: 0 0; }
19
+ }
20
+
21
+ @keyframes float-both-1 {
22
+ 0% { translate: 0 0; }
23
+ 20% { translate: calc(-0.7 * var(--float-amp, 10px)) calc(-0.8 * var(--float-amp, 10px)); }
24
+ 40% { translate: calc(0.5 * var(--float-amp, 10px)) calc(-0.3 * var(--float-amp, 10px)); }
25
+ 60% { translate: calc(0.9 * var(--float-amp, 10px)) calc(0.6 * var(--float-amp, 10px)); }
26
+ 80% { translate: calc(-0.4 * var(--float-amp, 10px)) calc(0.9 * var(--float-amp, 10px)); }
27
+ 100% { translate: 0 0; }
28
+ }
29
+
30
+ @keyframes float-both-2 {
31
+ 0% { translate: 0 0; }
32
+ 12% { translate: calc(0.6 * var(--float-amp, 10px)) calc(0.5 * var(--float-amp, 10px)); }
33
+ 30% { translate: calc(1 * var(--float-amp, 10px)) calc(-0.4 * var(--float-amp, 10px)); }
34
+ 50% { translate: calc(-0.3 * var(--float-amp, 10px)) calc(-0.9 * var(--float-amp, 10px)); }
35
+ 70% { translate: calc(-0.8 * var(--float-amp, 10px)) calc(0.2 * var(--float-amp, 10px)); }
36
+ 88% { translate: calc(0.2 * var(--float-amp, 10px)) calc(0.8 * var(--float-amp, 10px)); }
37
+ 100% { translate: 0 0; }
38
+ }
39
+
40
+ @keyframes float-both-3 {
41
+ 0% { translate: 0 0; }
42
+ 17% { translate: calc(-0.9 * var(--float-amp, 10px)) calc(0.4 * var(--float-amp, 10px)); }
43
+ 33% { translate: calc(-0.5 * var(--float-amp, 10px)) calc(-0.7 * var(--float-amp, 10px)); }
44
+ 50% { translate: calc(0.7 * var(--float-amp, 10px)) calc(-0.9 * var(--float-amp, 10px)); }
45
+ 67% { translate: calc(0.9 * var(--float-amp, 10px)) calc(0.5 * var(--float-amp, 10px)); }
46
+ 83% { translate: calc(-0.2 * var(--float-amp, 10px)) calc(0.8 * var(--float-amp, 10px)); }
47
+ 100% { translate: 0 0; }
48
+ }