@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
@@ -0,0 +1,62 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { generateWithClaude } from './claude.js';
6
+ import { generateWithCodex } from './codex.js';
7
+ const home = homedir();
8
+ function hasBinary(name) {
9
+ try {
10
+ execSync(`which ${name}`, { stdio: 'ignore' });
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ export const PROVIDERS = {
18
+ 'claude-code': {
19
+ displayName: 'Claude Code (Max/Pro)',
20
+ models: ['sonnet', 'opus', 'haiku'],
21
+ defaultModel: 'sonnet',
22
+ isAvailable: async () => {
23
+ if (!hasBinary('claude'))
24
+ return false;
25
+ return existsSync(join(home, '.claude.json'))
26
+ || existsSync(join(home, '.claude', 'credentials.json'));
27
+ },
28
+ generate: generateWithClaude,
29
+ },
30
+ codex: {
31
+ displayName: 'OpenAI Codex (CLI)',
32
+ models: ['gpt-5.4', 'gpt-5.3-codex', 'gpt-5.3-codex-spark', 'codex-mini-latest'],
33
+ defaultModel: 'codex-mini-latest',
34
+ isAvailable: async () => {
35
+ if (!hasBinary('codex'))
36
+ return false;
37
+ return existsSync(join(home, '.codex', 'auth.json'));
38
+ },
39
+ generate: generateWithCodex,
40
+ },
41
+ };
42
+ /** Detect which providers are available (binary + auth) */
43
+ export async function detectAvailableProviders() {
44
+ const available = [];
45
+ for (const [key, config] of Object.entries(PROVIDERS)) {
46
+ if (await config.isAvailable()) {
47
+ available.push(key);
48
+ }
49
+ }
50
+ return available;
51
+ }
52
+ /** Get default provider (first available in priority order) */
53
+ export async function getDefaultProvider() {
54
+ const priority = ['claude-code', 'codex'];
55
+ for (const key of priority) {
56
+ const config = PROVIDERS[key];
57
+ if (config && await config.isAvailable()) {
58
+ return key;
59
+ }
60
+ }
61
+ return null;
62
+ }
@@ -0,0 +1,6 @@
1
+ export interface StateMachineValidationResult {
2
+ valid: boolean;
3
+ errors: string[];
4
+ warnings: string[];
5
+ }
6
+ export declare function validateStateMachine(sm: unknown, animationIds: string[]): StateMachineValidationResult;
@@ -0,0 +1,182 @@
1
+ function isObject(v) {
2
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
3
+ }
4
+ export function validateStateMachine(sm, animationIds) {
5
+ const errors = [];
6
+ const warnings = [];
7
+ if (!isObject(sm)) {
8
+ return { valid: false, errors: ['State machine is not an object'], warnings };
9
+ }
10
+ // Validate initial
11
+ if (typeof sm['initial'] !== 'string') {
12
+ errors.push('Missing or invalid "initial" (must be string)');
13
+ }
14
+ // Validate states
15
+ if (!Array.isArray(sm['states'])) {
16
+ errors.push('Missing or invalid "states" (must be array)');
17
+ return { valid: errors.length === 0, errors, warnings };
18
+ }
19
+ const states = sm['states'];
20
+ const stateNames = new Set();
21
+ for (let i = 0; i < states.length; i++) {
22
+ const state = states[i];
23
+ if (!isObject(state)) {
24
+ errors.push(`states[${i}] is not an object`);
25
+ continue;
26
+ }
27
+ if (typeof state['name'] !== 'string') {
28
+ errors.push(`states[${i}] missing "name" (must be string)`);
29
+ continue;
30
+ }
31
+ const name = state['name'];
32
+ stateNames.add(name);
33
+ const type = state['type'];
34
+ if (type !== 'PlaybackState' && type !== 'GlobalState') {
35
+ errors.push(`states[${i}] ("${name}") has invalid "type": "${String(type)}" (must be "PlaybackState" or "GlobalState")`);
36
+ }
37
+ // PlaybackState must reference a valid animation
38
+ if (type === 'PlaybackState' && typeof state['animation'] === 'string') {
39
+ if (!animationIds.includes(state['animation'])) {
40
+ errors.push(`states[${i}] ("${name}") references unknown animation "${state['animation']}"`);
41
+ }
42
+ }
43
+ }
44
+ // Validate initial references existing state
45
+ if (typeof sm['initial'] === 'string' && !stateNames.has(sm['initial'])) {
46
+ errors.push(`"initial" references non-existent state "${sm['initial']}"`);
47
+ }
48
+ // Collect declared inputs
49
+ const declaredInputs = new Set();
50
+ if (Array.isArray(sm['inputs'])) {
51
+ for (const input of sm['inputs']) {
52
+ if (isObject(input) && typeof input['name'] === 'string') {
53
+ declaredInputs.add(input['name']);
54
+ }
55
+ }
56
+ }
57
+ // Validate transitions on states + collect referenced inputs
58
+ const referencedInputs = new Set();
59
+ const transitionTargets = new Set();
60
+ const statesWithTransitions = new Set();
61
+ for (let i = 0; i < states.length; i++) {
62
+ const state = states[i];
63
+ if (!isObject(state))
64
+ continue;
65
+ const name = state['name'] ?? `states[${i}]`;
66
+ const transitions = state['transitions'];
67
+ if (Array.isArray(transitions)) {
68
+ if (transitions.length > 0)
69
+ statesWithTransitions.add(name);
70
+ for (let j = 0; j < transitions.length; j++) {
71
+ const t = transitions[j];
72
+ if (!isObject(t))
73
+ continue;
74
+ if (typeof t['toState'] === 'string') {
75
+ const target = t['toState'];
76
+ transitionTargets.add(target);
77
+ if (!stateNames.has(target)) {
78
+ errors.push(`states[${i}] ("${name}") transition[${j}] references non-existent toState "${target}"`);
79
+ }
80
+ }
81
+ // Check guards
82
+ const guards = t['guards'];
83
+ if (Array.isArray(guards)) {
84
+ for (const guard of guards) {
85
+ if (isObject(guard) && typeof guard['inputName'] === 'string') {
86
+ const inputName = guard['inputName'];
87
+ referencedInputs.add(inputName);
88
+ if (!declaredInputs.has(inputName)) {
89
+ errors.push(`states[${i}] ("${name}") guard references undeclared input "${inputName}"`);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ // Validate interactions
98
+ if (Array.isArray(sm['interactions'])) {
99
+ for (let i = 0; i < sm['interactions'].length; i++) {
100
+ const interaction = sm['interactions'][i];
101
+ if (!isObject(interaction))
102
+ continue;
103
+ // Check action inputName references
104
+ const actions = interaction['actions'];
105
+ if (Array.isArray(actions)) {
106
+ for (const action of actions) {
107
+ if (isObject(action) && typeof action['inputName'] === 'string') {
108
+ const inputName = action['inputName'];
109
+ referencedInputs.add(inputName);
110
+ if (!declaredInputs.has(inputName)) {
111
+ errors.push(`interactions[${i}] action references undeclared input "${inputName}"`);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ // OnComplete/OnLoopComplete stateName references
117
+ const type = interaction['type'];
118
+ if ((type === 'OnComplete' || type === 'OnLoopComplete') && typeof interaction['stateName'] === 'string') {
119
+ const stateName = interaction['stateName'];
120
+ if (!stateNames.has(stateName)) {
121
+ errors.push(`interactions[${i}] ("${String(type)}") references non-existent stateName "${stateName}"`);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ // Warnings: unreachable states (BFS from initial)
127
+ if (typeof sm['initial'] === 'string' && stateNames.has(sm['initial'])) {
128
+ const reachable = new Set();
129
+ const queue = [sm['initial']];
130
+ while (queue.length > 0) {
131
+ const current = queue.shift();
132
+ if (reachable.has(current))
133
+ continue;
134
+ reachable.add(current);
135
+ // Find transitions from this state
136
+ const stateObj = states.find(s => isObject(s) && s['name'] === current);
137
+ if (stateObj && Array.isArray(stateObj['transitions'])) {
138
+ for (const t of stateObj['transitions']) {
139
+ if (isObject(t) && typeof t['toState'] === 'string') {
140
+ queue.push(t['toState']);
141
+ }
142
+ }
143
+ }
144
+ }
145
+ for (const name of stateNames) {
146
+ if (!reachable.has(name)) {
147
+ warnings.push(`State "${name}" is unreachable from initial state`);
148
+ }
149
+ }
150
+ }
151
+ // Warnings: dead-end states (non-final states with zero transitions)
152
+ for (const state of states) {
153
+ if (!isObject(state))
154
+ continue;
155
+ const name = state['name'];
156
+ if (!name)
157
+ continue;
158
+ const transitions = state['transitions'];
159
+ const hasTransitions = Array.isArray(transitions) && transitions.length > 0;
160
+ // A state is a dead end if it has no transitions and other states transition TO it
161
+ // (i.e., it's not the only state)
162
+ if (!hasTransitions && stateNames.size > 1 && transitionTargets.has(name)) {
163
+ // This is a valid terminal state, not necessarily a warning
164
+ // Only warn if it's NOT the only state reached as a target and it's not a natural end
165
+ }
166
+ // Simpler: warn about non-final states with no outgoing transitions
167
+ // (states that are not leaves in the graph)
168
+ if (!hasTransitions && stateNames.size > 1) {
169
+ // Check if this is the initial state with no transitions — that's a real dead end
170
+ if (sm['initial'] === name) {
171
+ warnings.push(`Initial state "${name}" has no transitions (dead end)`);
172
+ }
173
+ }
174
+ }
175
+ // Warnings: declared inputs never referenced
176
+ for (const inputName of declaredInputs) {
177
+ if (!referencedInputs.has(inputName)) {
178
+ warnings.push(`Input "${inputName}" is declared but never referenced in guards or interactions`);
179
+ }
180
+ }
181
+ return { valid: errors.length === 0, errors, warnings };
182
+ }
@@ -0,0 +1,20 @@
1
+ /** Strip benign CLI warnings (e.g. PATH update failures) from stderr */
2
+ export declare function filterCliStderr(stderr: string): string;
3
+ /** Extract a JSON object from LLM output (strips markdown fences, surrounding text) */
4
+ export declare function extractJson(raw: string): string;
5
+ export interface InteractiveEnvelope {
6
+ animations: Record<string, object>;
7
+ stateMachine: {
8
+ initial: string;
9
+ states: unknown[];
10
+ [key: string]: unknown;
11
+ };
12
+ }
13
+ /** Extract an interactive envelope JSON (animations + stateMachine) from LLM output */
14
+ export declare function extractInteractiveJson(raw: string): InteractiveEnvelope;
15
+ /** Convert hex color string to Lottie [r, g, b, 1] array (0-1 floats) */
16
+ export declare function hexToLottieColor(hex: string): [number, number, number, number];
17
+ /** Convert a prompt string to a filename-safe slug */
18
+ export declare function slugify(text: string, maxLength?: number): string;
19
+ /** Ensure the output directory exists */
20
+ export declare function ensureOutputDir(dir?: string): string;
package/dist/utils.js ADDED
@@ -0,0 +1,89 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /** Strip benign CLI warnings (e.g. PATH update failures) from stderr */
4
+ export function filterCliStderr(stderr) {
5
+ return stderr
6
+ .split('\n')
7
+ .filter(line => !line.includes('could not update PATH'))
8
+ .join('\n')
9
+ .trim();
10
+ }
11
+ /** Extract a JSON object from LLM output (strips markdown fences, surrounding text) */
12
+ export function extractJson(raw) {
13
+ // Strip markdown code fences
14
+ let cleaned = raw.replace(/```(?:json)?\s*\n?/g, '').replace(/```\s*$/g, '');
15
+ // Try to extract a JSON object
16
+ const match = cleaned.match(/\{[\s\S]*\}/);
17
+ if (!match) {
18
+ throw new Error('No JSON object found in output');
19
+ }
20
+ const jsonStr = match[0];
21
+ // Validate it parses
22
+ JSON.parse(jsonStr);
23
+ return jsonStr;
24
+ }
25
+ /** Extract an interactive envelope JSON (animations + stateMachine) from LLM output */
26
+ export function extractInteractiveJson(raw) {
27
+ const jsonStr = extractJson(raw);
28
+ const parsed = JSON.parse(jsonStr);
29
+ if (typeof parsed['animations'] !== 'object' || parsed['animations'] === null || Array.isArray(parsed['animations'])) {
30
+ throw new Error('Interactive envelope missing "animations" object');
31
+ }
32
+ const animations = parsed['animations'];
33
+ const animKeys = Object.keys(animations);
34
+ if (animKeys.length === 0) {
35
+ throw new Error('Interactive envelope "animations" is empty');
36
+ }
37
+ for (const key of animKeys) {
38
+ if (typeof animations[key] !== 'object' || animations[key] === null) {
39
+ throw new Error(`Animation "${key}" is not a valid object`);
40
+ }
41
+ }
42
+ if (typeof parsed['stateMachine'] !== 'object' || parsed['stateMachine'] === null || Array.isArray(parsed['stateMachine'])) {
43
+ throw new Error('Interactive envelope missing "stateMachine" object');
44
+ }
45
+ const sm = parsed['stateMachine'];
46
+ if (typeof sm['initial'] !== 'string') {
47
+ throw new Error('State machine missing "initial" string');
48
+ }
49
+ if (!Array.isArray(sm['states'])) {
50
+ throw new Error('State machine missing "states" array');
51
+ }
52
+ return {
53
+ animations: animations,
54
+ stateMachine: sm,
55
+ };
56
+ }
57
+ /** Convert hex color string to Lottie [r, g, b, 1] array (0-1 floats) */
58
+ export function hexToLottieColor(hex) {
59
+ let h = hex.replace(/^#/, '');
60
+ // Expand 3-char hex
61
+ if (h.length === 3) {
62
+ h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
63
+ }
64
+ const r = parseInt(h.slice(0, 2), 16) / 255;
65
+ const g = parseInt(h.slice(2, 4), 16) / 255;
66
+ const b = parseInt(h.slice(4, 6), 16) / 255;
67
+ return [
68
+ Math.round(r * 1000) / 1000,
69
+ Math.round(g * 1000) / 1000,
70
+ Math.round(b * 1000) / 1000,
71
+ 1,
72
+ ];
73
+ }
74
+ /** Convert a prompt string to a filename-safe slug */
75
+ export function slugify(text, maxLength = 40) {
76
+ return text
77
+ .toLowerCase()
78
+ .replace(/[^a-z0-9\s-]/g, '')
79
+ .replace(/\s+/g, '-')
80
+ .replace(/-+/g, '-')
81
+ .replace(/^-|-$/g, '')
82
+ .slice(0, maxLength);
83
+ }
84
+ /** Ensure the output directory exists */
85
+ export function ensureOutputDir(dir = 'output') {
86
+ const outputPath = join(process.cwd(), dir);
87
+ mkdirSync(outputPath, { recursive: true });
88
+ return outputPath;
89
+ }
@@ -0,0 +1,8 @@
1
+ export interface ValidationResult {
2
+ valid: boolean;
3
+ errors: string[];
4
+ warnings: string[];
5
+ }
6
+ export declare function validateLottie(json: unknown): ValidationResult;
7
+ /** Auto-fix common AI generation mistakes (returns a deep clone) */
8
+ export declare function autoFix(json: object): object;
@@ -0,0 +1,195 @@
1
+ function isObject(v) {
2
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
3
+ }
4
+ function checkAnimatedProperty(prop, path, errors) {
5
+ if (!isObject(prop))
6
+ return;
7
+ const a = prop['a'];
8
+ const k = prop['k'];
9
+ if (a === 1 && Array.isArray(k)) {
10
+ for (let i = 0; i < k.length; i++) {
11
+ const kf = k[i];
12
+ if (!isObject(kf)) {
13
+ errors.push(`${path}.k[${i}] is not an object`);
14
+ continue;
15
+ }
16
+ if (typeof kf['t'] !== 'number') {
17
+ errors.push(`${path}.k[${i}] missing "t" (frame number)`);
18
+ }
19
+ if (!Array.isArray(kf['s'])) {
20
+ errors.push(`${path}.k[${i}] missing "s" (start values array)`);
21
+ }
22
+ }
23
+ }
24
+ }
25
+ function checkGroup(shape, path, errors) {
26
+ const it = shape['it'];
27
+ if (!Array.isArray(it) || it.length === 0) {
28
+ errors.push(`${path} group has no "it" array`);
29
+ return;
30
+ }
31
+ const last = it[it.length - 1];
32
+ if (!isObject(last) || last['ty'] !== 'tr') {
33
+ errors.push(`${path} group "it" array must end with a "tr" (transform) item`);
34
+ }
35
+ }
36
+ function checkColors(obj, path, warnings) {
37
+ if (!isObject(obj))
38
+ return;
39
+ // Check if this looks like a color property
40
+ if (obj['ty'] === 'fl' || obj['ty'] === 'st') {
41
+ const c = obj['c'];
42
+ if (isObject(c)) {
43
+ const k = c['k'];
44
+ if (Array.isArray(k) && k.length >= 3) {
45
+ const hasHighValues = k.slice(0, 3).some((v) => typeof v === 'number' && v > 1);
46
+ if (hasHighValues) {
47
+ warnings.push(`${path}.c has values > 1 (likely 0-255 instead of 0-1)`);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ for (const [key, val] of Object.entries(obj)) {
53
+ if (Array.isArray(val)) {
54
+ val.forEach((item, i) => checkColors(item, `${path}.${key}[${i}]`, warnings));
55
+ }
56
+ else if (isObject(val)) {
57
+ checkColors(val, `${path}.${key}`, warnings);
58
+ }
59
+ }
60
+ }
61
+ export function validateLottie(json) {
62
+ const errors = [];
63
+ const warnings = [];
64
+ if (!isObject(json)) {
65
+ return { valid: false, errors: ['Input is not an object'], warnings };
66
+ }
67
+ const lottie = json;
68
+ // Required top-level fields
69
+ for (const field of ['w', 'h', 'fr', 'ip', 'op']) {
70
+ if (typeof lottie[field] !== 'number') {
71
+ errors.push(`Missing or invalid required field "${field}" (must be number)`);
72
+ }
73
+ }
74
+ if (!Array.isArray(lottie['layers'])) {
75
+ errors.push('Missing or invalid "layers" (must be array)');
76
+ return { valid: errors.length === 0, errors, warnings };
77
+ }
78
+ // Warnings for optional fields
79
+ if (typeof lottie['v'] !== 'string') {
80
+ warnings.push('Missing "v" version string');
81
+ }
82
+ if (!Array.isArray(lottie['assets'])) {
83
+ warnings.push('Missing "assets" array at top level');
84
+ }
85
+ if (lottie['ip'] === lottie['op']) {
86
+ warnings.push('"op" equals "ip" — animation has zero duration');
87
+ }
88
+ // Check each layer
89
+ const layers = lottie['layers'];
90
+ layers.forEach((layer, i) => {
91
+ if (!isObject(layer)) {
92
+ errors.push(`layers[${i}] is not an object`);
93
+ return;
94
+ }
95
+ const l = layer;
96
+ if (typeof l['ty'] !== 'number') {
97
+ errors.push(`layers[${i}] missing "ty" (type, must be number)`);
98
+ }
99
+ if (!isObject(l['ks'])) {
100
+ errors.push(`layers[${i}] missing "ks" (transform object)`);
101
+ }
102
+ else {
103
+ const ks = l['ks'];
104
+ for (const prop of ['a', 'p', 's', 'r', 'o']) {
105
+ if (ks[prop]) {
106
+ checkAnimatedProperty(ks[prop], `layers[${i}].ks.${prop}`, errors);
107
+ }
108
+ }
109
+ }
110
+ if (typeof l['ip'] !== 'number') {
111
+ errors.push(`layers[${i}] missing "ip" (in-point)`);
112
+ }
113
+ if (typeof l['op'] !== 'number') {
114
+ errors.push(`layers[${i}] missing "op" (out-point)`);
115
+ }
116
+ if (l['ddd'] === undefined) {
117
+ warnings.push(`layers[${i}] missing "ddd" field`);
118
+ }
119
+ // Shape layer checks
120
+ if (l['ty'] === 4) {
121
+ if (!Array.isArray(l['shapes']) || l['shapes'].length === 0) {
122
+ errors.push(`layers[${i}] (shape layer) has no "shapes" array`);
123
+ }
124
+ else {
125
+ const shapes = l['shapes'];
126
+ shapes.forEach((shape, j) => {
127
+ if (isObject(shape) && shape['ty'] === 'gr') {
128
+ checkGroup(shape, `layers[${i}].shapes[${j}]`, errors);
129
+ }
130
+ });
131
+ }
132
+ }
133
+ // Check colors recursively
134
+ checkColors(l, `layers[${i}]`, warnings);
135
+ });
136
+ return { valid: errors.length === 0, errors, warnings };
137
+ }
138
+ /** Auto-fix common AI generation mistakes (returns a deep clone) */
139
+ export function autoFix(json) {
140
+ const fixed = JSON.parse(JSON.stringify(json));
141
+ // Add version if missing
142
+ if (typeof fixed['v'] !== 'string') {
143
+ fixed['v'] = '5.5.2';
144
+ }
145
+ // Add assets if missing
146
+ if (!Array.isArray(fixed['assets'])) {
147
+ fixed['assets'] = [];
148
+ }
149
+ // Fix layers
150
+ if (Array.isArray(fixed['layers'])) {
151
+ for (const layer of fixed['layers']) {
152
+ if (!isObject(layer))
153
+ continue;
154
+ // Add ddd if missing
155
+ if (layer['ddd'] === undefined) {
156
+ layer['ddd'] = 0;
157
+ }
158
+ // Add st if missing
159
+ if (layer['st'] === undefined) {
160
+ layer['st'] = 0;
161
+ }
162
+ // Normalize 0-255 colors to 0-1
163
+ normalizeColors(layer);
164
+ }
165
+ }
166
+ return fixed;
167
+ }
168
+ function normalizeColors(obj) {
169
+ if (!isObject(obj))
170
+ return;
171
+ if (obj['ty'] === 'fl' || obj['ty'] === 'st') {
172
+ const c = obj['c'];
173
+ if (isObject(c)) {
174
+ const k = c['k'];
175
+ if (Array.isArray(k) && k.length >= 3) {
176
+ const hasHighValues = k.slice(0, 3).some((v) => typeof v === 'number' && v > 1);
177
+ if (hasHighValues) {
178
+ for (let i = 0; i < 3 && i < k.length; i++) {
179
+ if (typeof k[i] === 'number') {
180
+ k[i] = Math.round((k[i] / 255) * 1000) / 1000;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ }
187
+ for (const val of Object.values(obj)) {
188
+ if (Array.isArray(val)) {
189
+ val.forEach(item => normalizeColors(item));
190
+ }
191
+ else if (isObject(val)) {
192
+ normalizeColors(val);
193
+ }
194
+ }
195
+ }