@afromero/kin3o 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.
Files changed (45) hide show
  1. package/README.md +204 -0
  2. package/dist/brand.d.ts +39 -0
  3. package/dist/brand.js +63 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +249 -0
  6. package/dist/packager.d.ts +16 -0
  7. package/dist/packager.js +40 -0
  8. package/dist/preview.d.ts +2 -0
  9. package/dist/preview.js +30 -0
  10. package/dist/prompts/examples-interactive.d.ts +339 -0
  11. package/dist/prompts/examples-interactive.js +139 -0
  12. package/dist/prompts/examples-mascot.d.ts +765 -0
  13. package/dist/prompts/examples-mascot.js +319 -0
  14. package/dist/prompts/examples.d.ts +238 -0
  15. package/dist/prompts/examples.js +168 -0
  16. package/dist/prompts/index.d.ts +17 -0
  17. package/dist/prompts/index.js +21 -0
  18. package/dist/prompts/system-interactive.d.ts +2 -0
  19. package/dist/prompts/system-interactive.js +93 -0
  20. package/dist/prompts/system.d.ts +3 -0
  21. package/dist/prompts/system.js +94 -0
  22. package/dist/prompts/tokens.d.ts +6 -0
  23. package/dist/prompts/tokens.js +28 -0
  24. package/dist/providers/anthropic.d.ts +2 -0
  25. package/dist/providers/anthropic.js +25 -0
  26. package/dist/providers/claude.d.ts +2 -0
  27. package/dist/providers/claude.js +47 -0
  28. package/dist/providers/codex.d.ts +2 -0
  29. package/dist/providers/codex.js +60 -0
  30. package/dist/providers/registry.d.ts +18 -0
  31. package/dist/providers/registry.js +62 -0
  32. package/dist/state-machine-validator.d.ts +6 -0
  33. package/dist/state-machine-validator.js +182 -0
  34. package/dist/utils.d.ts +20 -0
  35. package/dist/utils.js +89 -0
  36. package/dist/validator.d.ts +8 -0
  37. package/dist/validator.js +195 -0
  38. package/examples/interactive-button.lottie +0 -0
  39. package/examples/mascot.json +760 -0
  40. package/examples/mascot.lottie +0 -0
  41. package/examples/pulse.json +75 -0
  42. package/examples/waveform.json +179 -0
  43. package/package.json +54 -0
  44. package/preview/template-interactive.html +223 -0
  45. package/preview/template.html +133 -0
Binary file
@@ -0,0 +1,75 @@
1
+ {
2
+ "v": "5.5.2",
3
+ "fr": 60,
4
+ "ip": 0,
5
+ "op": 120,
6
+ "w": 512,
7
+ "h": 512,
8
+ "ddd": 0,
9
+ "assets": [],
10
+ "layers": [
11
+ {
12
+ "ty": 4,
13
+ "ind": 0,
14
+ "nm": "Pulsing Circle",
15
+ "ip": 0,
16
+ "op": 120,
17
+ "st": 0,
18
+ "ddd": 0,
19
+ "ks": {
20
+ "a": { "a": 0, "k": [0, 0] },
21
+ "p": { "a": 0, "k": [256, 256] },
22
+ "s": {
23
+ "a": 1,
24
+ "k": [
25
+ {
26
+ "t": 0,
27
+ "s": [80, 80],
28
+ "o": { "x": [0.42], "y": [0] },
29
+ "i": { "x": [0.58], "y": [1] }
30
+ },
31
+ {
32
+ "t": 60,
33
+ "s": [120, 120],
34
+ "o": { "x": [0.42], "y": [0] },
35
+ "i": { "x": [0.58], "y": [1] }
36
+ },
37
+ { "t": 120, "s": [80, 80] }
38
+ ]
39
+ },
40
+ "r": { "a": 0, "k": 0 },
41
+ "o": { "a": 0, "k": 100 }
42
+ },
43
+ "shapes": [
44
+ {
45
+ "ty": "gr",
46
+ "nm": "Circle Group",
47
+ "it": [
48
+ {
49
+ "ty": "el",
50
+ "nm": "Ellipse",
51
+ "p": { "a": 0, "k": [0, 0] },
52
+ "s": { "a": 0, "k": [200, 200] }
53
+ },
54
+ {
55
+ "ty": "fl",
56
+ "nm": "Fill",
57
+ "c": { "a": 0, "k": [0.851, 0.467, 0.024, 1] },
58
+ "o": { "a": 0, "k": 100 },
59
+ "r": 1
60
+ },
61
+ {
62
+ "ty": "tr",
63
+ "p": { "a": 0, "k": [0, 0] },
64
+ "a": { "a": 0, "k": [0, 0] },
65
+ "s": { "a": 0, "k": [100, 100] },
66
+ "r": { "a": 0, "k": 0 },
67
+ "o": { "a": 0, "k": 100 }
68
+ }
69
+ ]
70
+ }
71
+ ],
72
+ "bm": 0
73
+ }
74
+ ]
75
+ }
@@ -0,0 +1,179 @@
1
+ {
2
+ "v": "5.5.2",
3
+ "fr": 60,
4
+ "ip": 0,
5
+ "op": 120,
6
+ "w": 512,
7
+ "h": 512,
8
+ "ddd": 0,
9
+ "assets": [],
10
+ "layers": [
11
+ {
12
+ "ty": 4,
13
+ "ind": 0,
14
+ "nm": "Bar 1",
15
+ "ip": 0,
16
+ "op": 120,
17
+ "st": 0,
18
+ "ddd": 0,
19
+ "ks": {
20
+ "a": { "a": 0, "k": [0, 0] },
21
+ "p": { "a": 0, "k": [206, 256] },
22
+ "s": { "a": 0, "k": [100, 100] },
23
+ "r": { "a": 0, "k": 0 },
24
+ "o": { "a": 0, "k": 100 }
25
+ },
26
+ "shapes": [
27
+ {
28
+ "ty": "gr",
29
+ "nm": "Bar Group 1",
30
+ "it": [
31
+ {
32
+ "ty": "rc",
33
+ "nm": "Rectangle",
34
+ "p": { "a": 0, "k": [0, 0] },
35
+ "s": {
36
+ "a": 1,
37
+ "k": [
38
+ { "t": 0, "s": [30, 40], "o": { "x": [0.42], "y": [0] }, "i": { "x": [0.58], "y": [1] } },
39
+ { "t": 30, "s": [30, 160], "o": { "x": [0.42], "y": [0] }, "i": { "x": [0.58], "y": [1] } },
40
+ { "t": 60, "s": [30, 40], "o": { "x": [0.42], "y": [0] }, "i": { "x": [0.58], "y": [1] } },
41
+ { "t": 90, "s": [30, 120], "o": { "x": [0.42], "y": [0] }, "i": { "x": [0.58], "y": [1] } },
42
+ { "t": 120, "s": [30, 40] }
43
+ ]
44
+ },
45
+ "r": { "a": 0, "k": 4 }
46
+ },
47
+ {
48
+ "ty": "fl",
49
+ "nm": "Fill",
50
+ "c": { "a": 0, "k": [0.851, 0.467, 0.024, 1] },
51
+ "o": { "a": 0, "k": 100 },
52
+ "r": 1
53
+ },
54
+ {
55
+ "ty": "tr",
56
+ "p": { "a": 0, "k": [0, 0] },
57
+ "a": { "a": 0, "k": [0, 0] },
58
+ "s": { "a": 0, "k": [100, 100] },
59
+ "r": { "a": 0, "k": 0 },
60
+ "o": { "a": 0, "k": 100 }
61
+ }
62
+ ]
63
+ }
64
+ ],
65
+ "bm": 0
66
+ },
67
+ {
68
+ "ty": 4,
69
+ "ind": 1,
70
+ "nm": "Bar 2",
71
+ "ip": 0,
72
+ "op": 120,
73
+ "st": 0,
74
+ "ddd": 0,
75
+ "ks": {
76
+ "a": { "a": 0, "k": [0, 0] },
77
+ "p": { "a": 0, "k": [256, 256] },
78
+ "s": { "a": 0, "k": [100, 100] },
79
+ "r": { "a": 0, "k": 0 },
80
+ "o": { "a": 0, "k": 100 }
81
+ },
82
+ "shapes": [
83
+ {
84
+ "ty": "gr",
85
+ "nm": "Bar Group 2",
86
+ "it": [
87
+ {
88
+ "ty": "rc",
89
+ "nm": "Rectangle",
90
+ "p": { "a": 0, "k": [0, 0] },
91
+ "s": {
92
+ "a": 1,
93
+ "k": [
94
+ { "t": 15, "s": [30, 40], "o": { "x": [0.42], "y": [0] }, "i": { "x": [0.58], "y": [1] } },
95
+ { "t": 45, "s": [30, 160], "o": { "x": [0.42], "y": [0] }, "i": { "x": [0.58], "y": [1] } },
96
+ { "t": 75, "s": [30, 40], "o": { "x": [0.42], "y": [0] }, "i": { "x": [0.58], "y": [1] } },
97
+ { "t": 105, "s": [30, 120], "o": { "x": [0.42], "y": [0] }, "i": { "x": [0.58], "y": [1] } },
98
+ { "t": 120, "s": [30, 40] }
99
+ ]
100
+ },
101
+ "r": { "a": 0, "k": 4 }
102
+ },
103
+ {
104
+ "ty": "fl",
105
+ "nm": "Fill",
106
+ "c": { "a": 0, "k": [0.851, 0.467, 0.024, 1] },
107
+ "o": { "a": 0, "k": 100 },
108
+ "r": 1
109
+ },
110
+ {
111
+ "ty": "tr",
112
+ "p": { "a": 0, "k": [0, 0] },
113
+ "a": { "a": 0, "k": [0, 0] },
114
+ "s": { "a": 0, "k": [100, 100] },
115
+ "r": { "a": 0, "k": 0 },
116
+ "o": { "a": 0, "k": 100 }
117
+ }
118
+ ]
119
+ }
120
+ ],
121
+ "bm": 0
122
+ },
123
+ {
124
+ "ty": 4,
125
+ "ind": 2,
126
+ "nm": "Bar 3",
127
+ "ip": 0,
128
+ "op": 120,
129
+ "st": 0,
130
+ "ddd": 0,
131
+ "ks": {
132
+ "a": { "a": 0, "k": [0, 0] },
133
+ "p": { "a": 0, "k": [306, 256] },
134
+ "s": { "a": 0, "k": [100, 100] },
135
+ "r": { "a": 0, "k": 0 },
136
+ "o": { "a": 0, "k": 100 }
137
+ },
138
+ "shapes": [
139
+ {
140
+ "ty": "gr",
141
+ "nm": "Bar Group 3",
142
+ "it": [
143
+ {
144
+ "ty": "rc",
145
+ "nm": "Rectangle",
146
+ "p": { "a": 0, "k": [0, 0] },
147
+ "s": {
148
+ "a": 1,
149
+ "k": [
150
+ { "t": 30, "s": [30, 40], "o": { "x": [0.42], "y": [0] }, "i": { "x": [0.58], "y": [1] } },
151
+ { "t": 60, "s": [30, 160], "o": { "x": [0.42], "y": [0] }, "i": { "x": [0.58], "y": [1] } },
152
+ { "t": 90, "s": [30, 40], "o": { "x": [0.42], "y": [0] }, "i": { "x": [0.58], "y": [1] } },
153
+ { "t": 120, "s": [30, 120] }
154
+ ]
155
+ },
156
+ "r": { "a": 0, "k": 4 }
157
+ },
158
+ {
159
+ "ty": "fl",
160
+ "nm": "Fill",
161
+ "c": { "a": 0, "k": [0.851, 0.467, 0.024, 1] },
162
+ "o": { "a": 0, "k": 100 },
163
+ "r": 1
164
+ },
165
+ {
166
+ "ty": "tr",
167
+ "p": { "a": 0, "k": [0, 0] },
168
+ "a": { "a": 0, "k": [0, 0] },
169
+ "s": { "a": 0, "k": [100, 100] },
170
+ "r": { "a": 0, "k": 0 },
171
+ "o": { "a": 0, "k": 100 }
172
+ }
173
+ ]
174
+ }
175
+ ],
176
+ "bm": 0
177
+ }
178
+ ]
179
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@afromero/kin3o",
3
+ "version": "0.1.0",
4
+ "description": "AI-powered Lottie animation generator — text to motion from your terminal",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Andres Romero <me@afromero.co>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/affromero/kin3o.git"
11
+ },
12
+ "keywords": [
13
+ "lottie",
14
+ "animation",
15
+ "ai",
16
+ "dotlottie",
17
+ "state-machine",
18
+ "cli",
19
+ "motion",
20
+ "claude",
21
+ "codex"
22
+ ],
23
+ "bin": {
24
+ "kin3o": "./dist/index.js"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "preview",
29
+ "examples"
30
+ ],
31
+ "scripts": {
32
+ "dev": "tsx src/index.ts",
33
+ "generate": "tsx src/index.ts generate",
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "node --import tsx/esm --test 'src/**/*.test.ts'",
37
+ "ci": "npm run typecheck && npm run test",
38
+ "prepublishOnly": "npm run ci && npm run build"
39
+ },
40
+ "dependencies": {
41
+ "@anthropic-ai/sdk": "^0.78.0",
42
+ "@dotlottie/dotlottie-js": "^1.6.3",
43
+ "commander": "^13.0.0",
44
+ "open": "^10.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.13.0",
48
+ "tsx": "^4.0.0",
49
+ "typescript": "^5.7.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=18"
53
+ }
54
+ }
@@ -0,0 +1,223 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>kin3o — Interactive Preview</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ background: #1a1a1a;
11
+ color: #e0e0e0;
12
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
13
+ display: flex;
14
+ flex-direction: column;
15
+ align-items: center;
16
+ min-height: 100vh;
17
+ padding: 2rem;
18
+ }
19
+ h1 {
20
+ font-size: 1.2rem;
21
+ font-weight: 500;
22
+ color: #888;
23
+ margin-bottom: 0.5rem;
24
+ letter-spacing: 0.05em;
25
+ }
26
+ .badge {
27
+ display: inline-block;
28
+ background: #D97706;
29
+ color: #fff;
30
+ font-size: 0.65rem;
31
+ font-weight: 600;
32
+ padding: 0.15rem 0.5rem;
33
+ border-radius: 999px;
34
+ margin-bottom: 1.5rem;
35
+ letter-spacing: 0.04em;
36
+ text-transform: uppercase;
37
+ }
38
+ #canvas-container {
39
+ width: 512px;
40
+ height: 512px;
41
+ max-width: 90vw;
42
+ max-height: 90vw;
43
+ background: #2a2a2a;
44
+ border-radius: 12px;
45
+ overflow: hidden;
46
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
47
+ position: relative;
48
+ }
49
+ canvas {
50
+ width: 100%;
51
+ height: 100%;
52
+ display: block;
53
+ }
54
+ .controls {
55
+ display: flex;
56
+ gap: 0.75rem;
57
+ margin-top: 1.5rem;
58
+ flex-wrap: wrap;
59
+ justify-content: center;
60
+ }
61
+ button {
62
+ background: #333;
63
+ color: #e0e0e0;
64
+ border: 1px solid #444;
65
+ padding: 0.5rem 1rem;
66
+ border-radius: 6px;
67
+ cursor: pointer;
68
+ font-size: 0.85rem;
69
+ transition: background 0.15s;
70
+ }
71
+ button:hover { background: #444; }
72
+ button.active { background: #D97706; color: #fff; border-color: #D97706; }
73
+ .state-display {
74
+ margin-top: 1rem;
75
+ display: flex;
76
+ gap: 0.75rem;
77
+ align-items: center;
78
+ flex-wrap: wrap;
79
+ justify-content: center;
80
+ }
81
+ .state-chip {
82
+ font-size: 0.75rem;
83
+ padding: 0.3rem 0.75rem;
84
+ border-radius: 999px;
85
+ background: #333;
86
+ border: 1px solid #444;
87
+ color: #aaa;
88
+ transition: all 0.2s;
89
+ }
90
+ .state-chip.current {
91
+ background: #D97706;
92
+ border-color: #D97706;
93
+ color: #fff;
94
+ font-weight: 600;
95
+ }
96
+ .hint {
97
+ margin-top: 1rem;
98
+ font-size: 0.8rem;
99
+ color: #666;
100
+ text-align: center;
101
+ line-height: 1.6;
102
+ }
103
+ .meta {
104
+ margin-top: 0.75rem;
105
+ font-size: 0.8rem;
106
+ color: #555;
107
+ text-align: center;
108
+ }
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <h1>KIN3O</h1>
113
+ <span class="badge">Interactive</span>
114
+ <div id="canvas-container">
115
+ <canvas id="dotlottie-canvas" width="512" height="512"></canvas>
116
+ </div>
117
+ <div class="controls">
118
+ <button id="btn-play" class="active" onclick="togglePlay()">Pause</button>
119
+ <button id="btn-05x" onclick="setSpeed(0.5)">0.5x</button>
120
+ <button id="btn-1x" class="active" onclick="setSpeed(1)">1x</button>
121
+ <button id="btn-2x" onclick="setSpeed(2)">2x</button>
122
+ </div>
123
+ <div class="state-display" id="state-display"></div>
124
+ <div class="hint" id="hint">Hover over or click the animation to trigger state transitions</div>
125
+ <div class="meta" id="meta"></div>
126
+
127
+ <script type="module">
128
+ import { DotLottie } from 'https://esm.sh/@lottiefiles/dotlottie-web@latest';
129
+
130
+ // Decode embedded .lottie data
131
+ const base64Data = '__DOTLOTTIE_DATA_BASE64__';
132
+ const binaryString = atob(base64Data);
133
+ const bytes = new Uint8Array(binaryString.length);
134
+ for (let i = 0; i < binaryString.length; i++) {
135
+ bytes[i] = binaryString.charCodeAt(i);
136
+ }
137
+
138
+ const canvas = document.getElementById('dotlottie-canvas');
139
+
140
+ const dotLottie = new DotLottie({
141
+ canvas,
142
+ data: bytes.buffer,
143
+ autoplay: true,
144
+ loop: true,
145
+ });
146
+
147
+ // Expose for button controls
148
+ window._dotLottie = dotLottie;
149
+ window._playing = true;
150
+ window._speed = 1;
151
+
152
+ // State display
153
+ const stateDisplay = document.getElementById('state-display');
154
+ const metaEl = document.getElementById('meta');
155
+
156
+ dotLottie.addEventListener('load', () => {
157
+ const manifest = dotLottie.manifest;
158
+ if (manifest && manifest.stateMachines && manifest.stateMachines.length > 0) {
159
+ // Start the state machine
160
+ const smId = manifest.stateMachines[0].id;
161
+ if (smId) {
162
+ dotLottie.loadStateMachine(smId);
163
+ dotLottie.startStateMachine();
164
+ }
165
+ }
166
+
167
+ // Show animation info
168
+ const animations = manifest?.animations || [];
169
+ metaEl.textContent = `${animations.length} animation${animations.length !== 1 ? 's' : ''} · 512×512px`;
170
+
171
+ // Build state chips from manifest
172
+ if (manifest?.stateMachines?.[0]) {
173
+ updateStateChips(manifest.stateMachines[0]);
174
+ }
175
+ });
176
+
177
+ function updateStateChips(sm) {
178
+ // We don't have direct access to the state machine descriptor from manifest
179
+ // but we can show animation names as proxy states
180
+ stateDisplay.innerHTML = '';
181
+ const animations = window._dotLottie.manifest?.animations || [];
182
+ for (const anim of animations) {
183
+ const chip = document.createElement('span');
184
+ chip.className = 'state-chip';
185
+ chip.textContent = anim.id || 'unknown';
186
+ chip.dataset.id = anim.id || '';
187
+ stateDisplay.appendChild(chip);
188
+ }
189
+
190
+ // Highlight first as current
191
+ if (stateDisplay.children.length > 0) {
192
+ stateDisplay.children[0].classList.add('current');
193
+ }
194
+ }
195
+
196
+ // Track active animation changes
197
+ dotLottie.addEventListener('render', () => {
198
+ const activeId = dotLottie.activeAnimationId;
199
+ if (activeId && stateDisplay.children.length > 0) {
200
+ for (const chip of stateDisplay.children) {
201
+ chip.classList.toggle('current', chip.dataset.id === activeId);
202
+ }
203
+ }
204
+ });
205
+
206
+ // Global controls
207
+ window.togglePlay = function() {
208
+ window._playing = !window._playing;
209
+ if (window._playing) { dotLottie.play(); } else { dotLottie.pause(); }
210
+ document.getElementById('btn-play').textContent = window._playing ? 'Pause' : 'Play';
211
+ document.getElementById('btn-play').classList.toggle('active', window._playing);
212
+ };
213
+
214
+ window.setSpeed = function(s) {
215
+ window._speed = s;
216
+ dotLottie.setSpeed(s);
217
+ document.getElementById('btn-05x').classList.toggle('active', s === 0.5);
218
+ document.getElementById('btn-1x').classList.toggle('active', s === 1);
219
+ document.getElementById('btn-2x').classList.toggle('active', s === 2);
220
+ };
221
+ </script>
222
+ </body>
223
+ </html>
@@ -0,0 +1,133 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>kin3o — Lottie Preview</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ background: #1a1a1a;
11
+ color: #e0e0e0;
12
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
13
+ display: flex;
14
+ flex-direction: column;
15
+ align-items: center;
16
+ min-height: 100vh;
17
+ padding: 2rem;
18
+ }
19
+ h1 {
20
+ font-size: 1.2rem;
21
+ font-weight: 500;
22
+ color: #888;
23
+ margin-bottom: 1.5rem;
24
+ letter-spacing: 0.05em;
25
+ }
26
+ #animation-container {
27
+ width: 512px;
28
+ height: 512px;
29
+ max-width: 90vw;
30
+ max-height: 90vw;
31
+ background: #2a2a2a;
32
+ border-radius: 12px;
33
+ overflow: hidden;
34
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
35
+ }
36
+ .controls {
37
+ display: flex;
38
+ gap: 0.75rem;
39
+ margin-top: 1.5rem;
40
+ flex-wrap: wrap;
41
+ justify-content: center;
42
+ }
43
+ button {
44
+ background: #333;
45
+ color: #e0e0e0;
46
+ border: 1px solid #444;
47
+ padding: 0.5rem 1rem;
48
+ border-radius: 6px;
49
+ cursor: pointer;
50
+ font-size: 0.85rem;
51
+ transition: background 0.15s;
52
+ }
53
+ button:hover { background: #444; }
54
+ button.active { background: #D97706; color: #fff; border-color: #D97706; }
55
+ .meta {
56
+ margin-top: 1rem;
57
+ font-size: 0.8rem;
58
+ color: #666;
59
+ text-align: center;
60
+ line-height: 1.6;
61
+ }
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <h1>KIN3O</h1>
66
+ <div id="animation-container"></div>
67
+ <div class="controls">
68
+ <button id="btn-play" class="active" onclick="togglePlay()">Pause</button>
69
+ <button id="btn-05x" onclick="setSpeed(0.5)">0.5x</button>
70
+ <button id="btn-1x" class="active" onclick="setSpeed(1)">1x</button>
71
+ <button id="btn-2x" onclick="setSpeed(2)">2x</button>
72
+ <button id="btn-loop" class="active" onclick="toggleLoop()">Loop</button>
73
+ <button onclick="copyJson()">Copy JSON</button>
74
+ <button onclick="downloadJson()">Download</button>
75
+ </div>
76
+ <div class="meta" id="meta"></div>
77
+
78
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie.min.js"></script>
79
+ <script>
80
+ const animData = __ANIMATION_DATA__;
81
+ const anim = lottie.loadAnimation({
82
+ container: document.getElementById('animation-container'),
83
+ renderer: 'svg',
84
+ loop: true,
85
+ autoplay: true,
86
+ animationData: animData,
87
+ });
88
+
89
+ const fps = animData.fr || 60;
90
+ const frames = (animData.op || 0) - (animData.ip || 0);
91
+ const duration = (frames / fps).toFixed(1);
92
+ document.getElementById('meta').innerHTML =
93
+ `${animData.w}×${animData.h}px · ${fps}fps · ${frames} frames · ${duration}s`;
94
+
95
+ let playing = true;
96
+ let looping = true;
97
+ let speed = 1;
98
+
99
+ function togglePlay() {
100
+ playing = !playing;
101
+ if (playing) { anim.play(); } else { anim.pause(); }
102
+ document.getElementById('btn-play').textContent = playing ? 'Pause' : 'Play';
103
+ document.getElementById('btn-play').classList.toggle('active', playing);
104
+ }
105
+
106
+ function setSpeed(s) {
107
+ speed = s;
108
+ anim.setSpeed(s);
109
+ document.getElementById('btn-05x').classList.toggle('active', s === 0.5);
110
+ document.getElementById('btn-1x').classList.toggle('active', s === 1);
111
+ document.getElementById('btn-2x').classList.toggle('active', s === 2);
112
+ }
113
+
114
+ function toggleLoop() {
115
+ looping = !looping;
116
+ anim.loop = looping;
117
+ document.getElementById('btn-loop').classList.toggle('active', looping);
118
+ }
119
+
120
+ function copyJson() {
121
+ navigator.clipboard.writeText(JSON.stringify(animData, null, 2));
122
+ }
123
+
124
+ function downloadJson() {
125
+ const blob = new Blob([JSON.stringify(animData, null, 2)], { type: 'application/json' });
126
+ const a = document.createElement('a');
127
+ a.href = URL.createObjectURL(blob);
128
+ a.download = 'animation.json';
129
+ a.click();
130
+ }
131
+ </script>
132
+ </body>
133
+ </html>