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.
- package/LICENSE +12 -0
- package/README.md +582 -0
- package/dist/AnimotPresenter.svelte +979 -0
- package/dist/AnimotPresenter.svelte.d.ts +14 -0
- package/dist/cdn/animot-presenter.css +1 -0
- package/dist/cdn/animot-presenter.esm.js +12427 -0
- package/dist/cdn/animot-presenter.min.js +16 -0
- package/dist/effects/ConfettiEffect.svelte +83 -0
- package/dist/effects/ConfettiEffect.svelte.d.ts +9 -0
- package/dist/effects/ParticlesBackground.svelte +114 -0
- package/dist/effects/ParticlesBackground.svelte.d.ts +9 -0
- package/dist/element.d.ts +24 -0
- package/dist/element.js +128 -0
- package/dist/engine/utils.d.ts +22 -0
- package/dist/engine/utils.js +72 -0
- package/dist/highlight/CodeMorph.svelte +136 -0
- package/dist/highlight/CodeMorph.svelte.d.ts +16 -0
- package/dist/highlight/highlighter-web.d.ts +10 -0
- package/dist/highlight/highlighter-web.js +54 -0
- package/dist/highlight/highlighter.d.ts +6 -0
- package/dist/highlight/highlighter.js +45 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/renderers/ChartRenderer.svelte +341 -0
- package/dist/renderers/ChartRenderer.svelte.d.ts +8 -0
- package/dist/renderers/CounterRenderer.svelte +64 -0
- package/dist/renderers/CounterRenderer.svelte.d.ts +8 -0
- package/dist/renderers/IconRenderer.svelte +18 -0
- package/dist/renderers/IconRenderer.svelte.d.ts +7 -0
- package/dist/styles/presenter.css +48 -0
- package/dist/types.d.ts +319 -0
- package/dist/types.js +1 -0
- package/package.json +83 -0
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
45
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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,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
|
+
}
|