@hyperframes/parsers 0.7.71 → 0.7.72

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.
@@ -122,6 +122,17 @@ function isRecord(value) {
122
122
  function isColorGradingVariableRef(value) {
123
123
  return typeof value === "string" && VARIABLE_REF.test(value.trim());
124
124
  }
125
+ function unknownKeysHint(path, unknown) {
126
+ if (path !== "grading") return `Correct or remove the unsupported "${path}" keys.`;
127
+ const sections = new Set(
128
+ unknown.flatMap(
129
+ (key) => OBJECT_SECTIONS.filter(([, keys]) => keys.includes(key)).map(
130
+ ([section]) => section
131
+ )
132
+ )
133
+ );
134
+ return sections.size === 1 ? `Move those controls under "${[...sections][0]}".` : "Use only the documented media-treatment keys at the top level.";
135
+ }
125
136
  function validateObject(value, path, keys, issues) {
126
137
  if (isColorGradingVariableRef(value)) return null;
127
138
  if (!isRecord(value)) {
@@ -131,7 +142,11 @@ function validateObject(value, path, keys, issues) {
131
142
  const allowed = new Set(keys);
132
143
  const unknown = Object.keys(value).filter((key) => !allowed.has(key));
133
144
  if (unknown.length > 0) {
134
- issues.push({ path, message: `has unsupported key(s): ${unknown.join(", ")}` });
145
+ issues.push({
146
+ path,
147
+ message: `has unsupported key(s): ${unknown.join(", ")}`,
148
+ hint: unknownKeysHint(path, unknown)
149
+ });
135
150
  }
136
151
  return value;
137
152
  }
@@ -150,13 +165,18 @@ function validateNumericSection(value, path, keys, limitFor, issues) {
150
165
  }
151
166
  function validatePalette(value, issues) {
152
167
  if (value === void 0 || value === null || isColorGradingVariableRef(value)) return;
168
+ const hint = 'Use 2 to 6 colors in the intended mapping order, each written as exact "#RRGGBB", or use a project variable reference.';
153
169
  if (!Array.isArray(value) || value.length < 2 || value.length > 6) {
154
- issues.push({ path: "palette", message: "must contain 2 to 6 hex colors" });
170
+ issues.push({ path: "palette", message: "must contain 2 to 6 hex colors", hint });
155
171
  return;
156
172
  }
157
173
  value.forEach((color, index) => {
158
174
  if (typeof color !== "string" || !PALETTE_COLOR.test(color)) {
159
- issues.push({ path: `palette[${index}]`, message: "must be a six-digit hex color" });
175
+ issues.push({
176
+ path: `palette[${index}]`,
177
+ message: "must be a six-digit hex color",
178
+ hint
179
+ });
160
180
  }
161
181
  });
162
182
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/colorGradingContract.ts"],"sourcesContent":["export const COLOR_GRADING_CONTRACT_VERSION = 1;\nexport const COLOR_GRADING_COLOR_SPACE = \"rec709\";\n\nexport const COLOR_GRADING_TOP_LEVEL_KEYS = [\n \"enabled\",\n \"preset\",\n \"intensity\",\n \"adjust\",\n \"details\",\n \"effects\",\n \"palette\",\n \"lut\",\n \"colorSpace\",\n] as const;\n\nexport const COLOR_GRADING_ADJUST_KEYS = [\n \"exposure\",\n \"contrast\",\n \"highlights\",\n \"shadows\",\n \"whites\",\n \"blacks\",\n \"temperature\",\n \"tint\",\n \"vibrance\",\n \"saturation\",\n] as const;\n\nexport const COLOR_GRADING_DETAIL_KEYS = [\n \"vignette\",\n \"vignetteMidpoint\",\n \"vignetteRoundness\",\n \"vignetteFeather\",\n \"grain\",\n \"grainSize\",\n \"grainRoughness\",\n] as const;\n\nexport const COLOR_GRADING_EFFECT_KEYS = [\n \"blur\",\n \"pixelate\",\n \"chromaBleed\",\n \"tapeDamage\",\n \"tapeTracking\",\n \"tapeNoise\",\n \"tapeSpeed\",\n \"filmArtifacts\",\n \"halftone\",\n \"halftoneSize\",\n \"twoInkPrint\",\n \"twoInkPrintSize\",\n \"ascii\",\n \"asciiSize\",\n \"asciiInvert\",\n \"asciiStyle\",\n \"asciiColor\",\n \"asciiRotation\",\n \"dither\",\n \"ditherSize\",\n \"bloom\",\n \"bloomRadius\",\n \"monoScreen\",\n \"monoScreenSize\",\n \"monoScreenAngle\",\n \"monoScreenSpread\",\n \"monoScreenShape\",\n \"monoScreenInvert\",\n \"scanlines\",\n \"scanlineCount\",\n \"scanlineSoftness\",\n \"chromaticAberration\",\n \"chromaticAngle\",\n \"crtCurvature\",\n \"digitalGlitch\",\n \"digitalGlitchColorSplit\",\n \"digitalGlitchLineTear\",\n \"digitalGlitchPixelate\",\n \"digitalGlitchBlockAmount\",\n \"digitalGlitchBlockDisplacement\",\n \"digitalGlitchBlockOpacity\",\n \"digitalGlitchSpeed\",\n \"engraving\",\n \"engravingSpacing\",\n \"engravingMinThickness\",\n \"engravingMaxThickness\",\n \"engravingAngle\",\n \"engravingContrast\",\n \"engravingSharpness\",\n \"engravingWave\",\n \"engravingWaveFrequency\",\n \"crosshatch\",\n \"crosshatchSpacing\",\n \"crosshatchThickness\",\n \"crosshatchAngle\",\n \"crosshatchContrast\",\n \"crosshatchEdges\",\n \"crosshatchLineWeight\",\n \"crosshatchWave\",\n \"crosshatchWaveFrequency\",\n \"kuwahara\",\n \"kuwaharaRadius\",\n \"kuwaharaSharpness\",\n \"kuwaharaSaturation\",\n] as const;\n\nexport const COLOR_GRADING_LUT_KEYS = [\"src\", \"intensity\"] as const;\n\ntype NumericLimit = Readonly<{ min: number; max: number }>;\n\nconst UNIT_LIMIT: NumericLimit = { min: 0, max: 1 };\nconst SIGNED_UNIT_LIMIT: NumericLimit = { min: -1, max: 1 };\nconst EFFECT_LIMIT_OVERRIDES: Readonly<Record<string, NumericLimit>> = {\n asciiStyle: { min: 0, max: 7 },\n bloom: { min: 0, max: 3 },\n bloomRadius: { min: 1, max: 100 },\n monoScreenShape: { min: 0, max: 4 },\n};\nconst VARIABLE_REF = /^\\$(?:\\{[A-Za-z0-9_.:-]+\\}|[A-Za-z0-9_.:-]+)$/;\nconst PALETTE_COLOR = /^#[0-9a-f]{6}$/i;\n\nconst OBJECT_SECTIONS = [\n [\"adjust\", COLOR_GRADING_ADJUST_KEYS],\n [\"details\", COLOR_GRADING_DETAIL_KEYS],\n [\"effects\", COLOR_GRADING_EFFECT_KEYS],\n [\"lut\", COLOR_GRADING_LUT_KEYS],\n] as const;\n\nexport interface ColorGradingContractIssue {\n path: string;\n message: string;\n hint?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function isColorGradingVariableRef(value: unknown): value is string {\n return typeof value === \"string\" && VARIABLE_REF.test(value.trim());\n}\n\nfunction validateObject(\n value: unknown,\n path: string,\n keys: readonly string[],\n issues: ColorGradingContractIssue[],\n): Record<string, unknown> | null {\n if (isColorGradingVariableRef(value)) return null;\n if (!isRecord(value)) {\n issues.push({ path, message: \"must be an object or variable reference\" });\n return null;\n }\n const allowed = new Set(keys);\n const unknown = Object.keys(value).filter((key) => !allowed.has(key));\n if (unknown.length > 0) {\n issues.push({ path, message: `has unsupported key(s): ${unknown.join(\", \")}` });\n }\n return value;\n}\n\nfunction validateNumericField(\n value: Record<string, unknown>,\n key: string,\n path: string,\n limit: NumericLimit,\n issues: ColorGradingContractIssue[],\n): void {\n const candidate = value[key];\n if (candidate === undefined || isColorGradingVariableRef(candidate)) return;\n if (\n typeof candidate !== \"number\" ||\n !Number.isFinite(candidate) ||\n candidate < limit.min ||\n candidate > limit.max\n ) {\n issues.push({\n path: path ? `${path}.${key}` : key,\n message: `must be a finite number from ${limit.min} through ${limit.max}`,\n });\n }\n}\n\nfunction validateNumericSection(\n value: Record<string, unknown>,\n path: string,\n keys: readonly string[],\n limitFor: (key: string) => NumericLimit,\n issues: ColorGradingContractIssue[],\n): void {\n for (const key of keys) validateNumericField(value, key, path, limitFor(key), issues);\n}\n\nfunction validatePalette(value: unknown, issues: ColorGradingContractIssue[]): void {\n if (value === undefined || value === null || isColorGradingVariableRef(value)) return;\n if (!Array.isArray(value) || value.length < 2 || value.length > 6) {\n issues.push({ path: \"palette\", message: \"must contain 2 to 6 hex colors\" });\n return;\n }\n value.forEach((color, index) => {\n if (typeof color !== \"string\" || !PALETTE_COLOR.test(color)) {\n issues.push({ path: `palette[${index}]`, message: \"must be a six-digit hex color\" });\n }\n });\n}\n\nfunction validateLut(\n value: unknown,\n object: Record<string, unknown> | null,\n issues: ColorGradingContractIssue[],\n): void {\n if (typeof value === \"string\") {\n if (!value.trim()) issues.push({ path: \"lut\", message: \"must not be empty\" });\n return;\n }\n if (!object) return;\n if (\n !isColorGradingVariableRef(object.src) &&\n (typeof object.src !== \"string\" || !object.src.trim())\n ) {\n issues.push({\n path: \"lut.src\",\n message: \"must be a non-empty string or variable reference\",\n });\n }\n validateNumericField(object, \"intensity\", \"lut\", UNIT_LIMIT, issues);\n}\n\nfunction validateEnabled(\n grading: Record<string, unknown>,\n issues: ColorGradingContractIssue[],\n): void {\n if (\n grading.enabled !== undefined &&\n !isColorGradingVariableRef(grading.enabled) &&\n typeof grading.enabled !== \"boolean\"\n ) {\n issues.push({ path: \"enabled\", message: \"must be a boolean or variable reference\" });\n }\n}\n\nfunction validatePreset(\n grading: Record<string, unknown>,\n issues: ColorGradingContractIssue[],\n): void {\n if (\n grading.preset !== undefined &&\n grading.preset !== null &&\n !isColorGradingVariableRef(grading.preset) &&\n (typeof grading.preset !== \"string\" || !grading.preset.trim())\n ) {\n issues.push({\n path: \"preset\",\n message: \"must be a non-empty string, null, or variable reference\",\n });\n }\n}\n\nfunction validateColorSpace(\n grading: Record<string, unknown>,\n issues: ColorGradingContractIssue[],\n): void {\n if (\n grading.colorSpace !== undefined &&\n !isColorGradingVariableRef(grading.colorSpace) &&\n grading.colorSpace !== COLOR_GRADING_COLOR_SPACE\n ) {\n issues.push({\n path: \"colorSpace\",\n message: `must be \"${COLOR_GRADING_COLOR_SPACE}\" or a variable reference`,\n });\n }\n}\n\nfunction validateTopLevel(\n grading: Record<string, unknown>,\n issues: ColorGradingContractIssue[],\n): void {\n validateEnabled(grading, issues);\n validateNumericField(grading, \"intensity\", \"\", UNIT_LIMIT, issues);\n validatePreset(grading, issues);\n validateColorSpace(grading, issues);\n}\n\nfunction validateSection(\n grading: Record<string, unknown>,\n key: (typeof OBJECT_SECTIONS)[number][0],\n keys: readonly string[],\n issues: ColorGradingContractIssue[],\n): void {\n const section = grading[key];\n if (section === undefined || (key === \"lut\" && section === null)) return;\n if (key === \"lut\" && typeof section === \"string\") {\n validateLut(section, null, issues);\n return;\n }\n const object = validateObject(section, key, keys, issues);\n if (!object) return;\n if (key === \"lut\") return validateLut(section, object, issues);\n\n const limitFor = (control: string): NumericLimit => {\n if (key === \"adjust\" && control === \"exposure\") return { min: -2, max: 2 };\n if (key === \"adjust\" || (key === \"details\" && control === \"vignetteRoundness\")) {\n return SIGNED_UNIT_LIMIT;\n }\n return key === \"effects\" ? (EFFECT_LIMIT_OVERRIDES[control] ?? UNIT_LIMIT) : UNIT_LIMIT;\n };\n validateNumericSection(object, key, keys, limitFor, issues);\n}\n\nfunction validateSections(\n grading: Record<string, unknown>,\n issues: ColorGradingContractIssue[],\n): void {\n for (const [key, keys] of OBJECT_SECTIONS) validateSection(grading, key, keys, issues);\n}\n\n/** Browser-safe structural validation shared by Lint, CLI, and Core consumers. */\nexport function validateColorGradingContract(value: unknown): ColorGradingContractIssue[] {\n if (typeof value === \"string\") {\n return value.trim() ? [] : [{ path: \"grading\", message: \"is empty\" }];\n }\n\n const issues: ColorGradingContractIssue[] = [];\n const grading = validateObject(value, \"grading\", COLOR_GRADING_TOP_LEVEL_KEYS, issues);\n if (!grading) return issues;\n\n validateTopLevel(grading, issues);\n validateSections(grading, issues);\n validatePalette(grading.palette, issues);\n return issues;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/colorGradingContract.ts"],"sourcesContent":["export const COLOR_GRADING_CONTRACT_VERSION = 1;\nexport const COLOR_GRADING_COLOR_SPACE = \"rec709\";\n\nexport const COLOR_GRADING_TOP_LEVEL_KEYS = [\n \"enabled\",\n \"preset\",\n \"intensity\",\n \"adjust\",\n \"details\",\n \"effects\",\n \"palette\",\n \"lut\",\n \"colorSpace\",\n] as const;\n\nexport const COLOR_GRADING_ADJUST_KEYS = [\n \"exposure\",\n \"contrast\",\n \"highlights\",\n \"shadows\",\n \"whites\",\n \"blacks\",\n \"temperature\",\n \"tint\",\n \"vibrance\",\n \"saturation\",\n] as const;\n\nexport const COLOR_GRADING_DETAIL_KEYS = [\n \"vignette\",\n \"vignetteMidpoint\",\n \"vignetteRoundness\",\n \"vignetteFeather\",\n \"grain\",\n \"grainSize\",\n \"grainRoughness\",\n] as const;\n\nexport const COLOR_GRADING_EFFECT_KEYS = [\n \"blur\",\n \"pixelate\",\n \"chromaBleed\",\n \"tapeDamage\",\n \"tapeTracking\",\n \"tapeNoise\",\n \"tapeSpeed\",\n \"filmArtifacts\",\n \"halftone\",\n \"halftoneSize\",\n \"twoInkPrint\",\n \"twoInkPrintSize\",\n \"ascii\",\n \"asciiSize\",\n \"asciiInvert\",\n \"asciiStyle\",\n \"asciiColor\",\n \"asciiRotation\",\n \"dither\",\n \"ditherSize\",\n \"bloom\",\n \"bloomRadius\",\n \"monoScreen\",\n \"monoScreenSize\",\n \"monoScreenAngle\",\n \"monoScreenSpread\",\n \"monoScreenShape\",\n \"monoScreenInvert\",\n \"scanlines\",\n \"scanlineCount\",\n \"scanlineSoftness\",\n \"chromaticAberration\",\n \"chromaticAngle\",\n \"crtCurvature\",\n \"digitalGlitch\",\n \"digitalGlitchColorSplit\",\n \"digitalGlitchLineTear\",\n \"digitalGlitchPixelate\",\n \"digitalGlitchBlockAmount\",\n \"digitalGlitchBlockDisplacement\",\n \"digitalGlitchBlockOpacity\",\n \"digitalGlitchSpeed\",\n \"engraving\",\n \"engravingSpacing\",\n \"engravingMinThickness\",\n \"engravingMaxThickness\",\n \"engravingAngle\",\n \"engravingContrast\",\n \"engravingSharpness\",\n \"engravingWave\",\n \"engravingWaveFrequency\",\n \"crosshatch\",\n \"crosshatchSpacing\",\n \"crosshatchThickness\",\n \"crosshatchAngle\",\n \"crosshatchContrast\",\n \"crosshatchEdges\",\n \"crosshatchLineWeight\",\n \"crosshatchWave\",\n \"crosshatchWaveFrequency\",\n \"kuwahara\",\n \"kuwaharaRadius\",\n \"kuwaharaSharpness\",\n \"kuwaharaSaturation\",\n] as const;\n\nexport const COLOR_GRADING_LUT_KEYS = [\"src\", \"intensity\"] as const;\n\ntype NumericLimit = Readonly<{ min: number; max: number }>;\n\nconst UNIT_LIMIT: NumericLimit = { min: 0, max: 1 };\nconst SIGNED_UNIT_LIMIT: NumericLimit = { min: -1, max: 1 };\nconst EFFECT_LIMIT_OVERRIDES: Readonly<Record<string, NumericLimit>> = {\n asciiStyle: { min: 0, max: 7 },\n bloom: { min: 0, max: 3 },\n bloomRadius: { min: 1, max: 100 },\n monoScreenShape: { min: 0, max: 4 },\n};\nconst VARIABLE_REF = /^\\$(?:\\{[A-Za-z0-9_.:-]+\\}|[A-Za-z0-9_.:-]+)$/;\nconst PALETTE_COLOR = /^#[0-9a-f]{6}$/i;\n\nconst OBJECT_SECTIONS = [\n [\"adjust\", COLOR_GRADING_ADJUST_KEYS],\n [\"details\", COLOR_GRADING_DETAIL_KEYS],\n [\"effects\", COLOR_GRADING_EFFECT_KEYS],\n [\"lut\", COLOR_GRADING_LUT_KEYS],\n] as const;\n\nexport interface ColorGradingContractIssue {\n path: string;\n message: string;\n hint?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function isColorGradingVariableRef(value: unknown): value is string {\n return typeof value === \"string\" && VARIABLE_REF.test(value.trim());\n}\n\nfunction unknownKeysHint(path: string, unknown: readonly string[]): string {\n if (path !== \"grading\") return `Correct or remove the unsupported \"${path}\" keys.`;\n const sections = new Set(\n unknown.flatMap((key) =>\n OBJECT_SECTIONS.filter(([, keys]) => (keys as readonly string[]).includes(key)).map(\n ([section]) => section,\n ),\n ),\n );\n return sections.size === 1\n ? `Move those controls under \"${[...sections][0]}\".`\n : \"Use only the documented media-treatment keys at the top level.\";\n}\n\nfunction validateObject(\n value: unknown,\n path: string,\n keys: readonly string[],\n issues: ColorGradingContractIssue[],\n): Record<string, unknown> | null {\n if (isColorGradingVariableRef(value)) return null;\n if (!isRecord(value)) {\n issues.push({ path, message: \"must be an object or variable reference\" });\n return null;\n }\n const allowed = new Set(keys);\n const unknown = Object.keys(value).filter((key) => !allowed.has(key));\n if (unknown.length > 0) {\n issues.push({\n path,\n message: `has unsupported key(s): ${unknown.join(\", \")}`,\n hint: unknownKeysHint(path, unknown),\n });\n }\n return value;\n}\n\nfunction validateNumericField(\n value: Record<string, unknown>,\n key: string,\n path: string,\n limit: NumericLimit,\n issues: ColorGradingContractIssue[],\n): void {\n const candidate = value[key];\n if (candidate === undefined || isColorGradingVariableRef(candidate)) return;\n if (\n typeof candidate !== \"number\" ||\n !Number.isFinite(candidate) ||\n candidate < limit.min ||\n candidate > limit.max\n ) {\n issues.push({\n path: path ? `${path}.${key}` : key,\n message: `must be a finite number from ${limit.min} through ${limit.max}`,\n });\n }\n}\n\nfunction validateNumericSection(\n value: Record<string, unknown>,\n path: string,\n keys: readonly string[],\n limitFor: (key: string) => NumericLimit,\n issues: ColorGradingContractIssue[],\n): void {\n for (const key of keys) validateNumericField(value, key, path, limitFor(key), issues);\n}\n\nfunction validatePalette(value: unknown, issues: ColorGradingContractIssue[]): void {\n if (value === undefined || value === null || isColorGradingVariableRef(value)) return;\n const hint =\n 'Use 2 to 6 colors in the intended mapping order, each written as exact \"#RRGGBB\", or use a project variable reference.';\n if (!Array.isArray(value) || value.length < 2 || value.length > 6) {\n issues.push({ path: \"palette\", message: \"must contain 2 to 6 hex colors\", hint });\n return;\n }\n value.forEach((color, index) => {\n if (typeof color !== \"string\" || !PALETTE_COLOR.test(color)) {\n issues.push({\n path: `palette[${index}]`,\n message: \"must be a six-digit hex color\",\n hint,\n });\n }\n });\n}\n\nfunction validateLut(\n value: unknown,\n object: Record<string, unknown> | null,\n issues: ColorGradingContractIssue[],\n): void {\n if (typeof value === \"string\") {\n if (!value.trim()) issues.push({ path: \"lut\", message: \"must not be empty\" });\n return;\n }\n if (!object) return;\n if (\n !isColorGradingVariableRef(object.src) &&\n (typeof object.src !== \"string\" || !object.src.trim())\n ) {\n issues.push({\n path: \"lut.src\",\n message: \"must be a non-empty string or variable reference\",\n });\n }\n validateNumericField(object, \"intensity\", \"lut\", UNIT_LIMIT, issues);\n}\n\nfunction validateEnabled(\n grading: Record<string, unknown>,\n issues: ColorGradingContractIssue[],\n): void {\n if (\n grading.enabled !== undefined &&\n !isColorGradingVariableRef(grading.enabled) &&\n typeof grading.enabled !== \"boolean\"\n ) {\n issues.push({ path: \"enabled\", message: \"must be a boolean or variable reference\" });\n }\n}\n\nfunction validatePreset(\n grading: Record<string, unknown>,\n issues: ColorGradingContractIssue[],\n): void {\n if (\n grading.preset !== undefined &&\n grading.preset !== null &&\n !isColorGradingVariableRef(grading.preset) &&\n (typeof grading.preset !== \"string\" || !grading.preset.trim())\n ) {\n issues.push({\n path: \"preset\",\n message: \"must be a non-empty string, null, or variable reference\",\n });\n }\n}\n\nfunction validateColorSpace(\n grading: Record<string, unknown>,\n issues: ColorGradingContractIssue[],\n): void {\n if (\n grading.colorSpace !== undefined &&\n !isColorGradingVariableRef(grading.colorSpace) &&\n grading.colorSpace !== COLOR_GRADING_COLOR_SPACE\n ) {\n issues.push({\n path: \"colorSpace\",\n message: `must be \"${COLOR_GRADING_COLOR_SPACE}\" or a variable reference`,\n });\n }\n}\n\nfunction validateTopLevel(\n grading: Record<string, unknown>,\n issues: ColorGradingContractIssue[],\n): void {\n validateEnabled(grading, issues);\n validateNumericField(grading, \"intensity\", \"\", UNIT_LIMIT, issues);\n validatePreset(grading, issues);\n validateColorSpace(grading, issues);\n}\n\nfunction validateSection(\n grading: Record<string, unknown>,\n key: (typeof OBJECT_SECTIONS)[number][0],\n keys: readonly string[],\n issues: ColorGradingContractIssue[],\n): void {\n const section = grading[key];\n if (section === undefined || (key === \"lut\" && section === null)) return;\n if (key === \"lut\" && typeof section === \"string\") {\n validateLut(section, null, issues);\n return;\n }\n const object = validateObject(section, key, keys, issues);\n if (!object) return;\n if (key === \"lut\") return validateLut(section, object, issues);\n\n const limitFor = (control: string): NumericLimit => {\n if (key === \"adjust\" && control === \"exposure\") return { min: -2, max: 2 };\n if (key === \"adjust\" || (key === \"details\" && control === \"vignetteRoundness\")) {\n return SIGNED_UNIT_LIMIT;\n }\n return key === \"effects\" ? (EFFECT_LIMIT_OVERRIDES[control] ?? UNIT_LIMIT) : UNIT_LIMIT;\n };\n validateNumericSection(object, key, keys, limitFor, issues);\n}\n\nfunction validateSections(\n grading: Record<string, unknown>,\n issues: ColorGradingContractIssue[],\n): void {\n for (const [key, keys] of OBJECT_SECTIONS) validateSection(grading, key, keys, issues);\n}\n\n/** Browser-safe structural validation shared by Lint, CLI, and Core consumers. */\nexport function validateColorGradingContract(value: unknown): ColorGradingContractIssue[] {\n if (typeof value === \"string\") {\n return value.trim() ? [] : [{ path: \"grading\", message: \"is empty\" }];\n }\n\n const issues: ColorGradingContractIssue[] = [];\n const grading = validateObject(value, \"grading\", COLOR_GRADING_TOP_LEVEL_KEYS, issues);\n if (!grading) return issues;\n\n validateTopLevel(grading, issues);\n validateSections(grading, issues);\n validatePalette(grading.palette, issues);\n return issues;\n}\n"],"mappings":"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","names":[]}
@@ -87,14 +87,18 @@ var SUPPORTED_EASES = [
87
87
  "bounce.in",
88
88
  "bounce.out",
89
89
  "bounce.inOut",
90
+ "circ.inOut",
90
91
  "expo.in",
91
92
  "expo.out",
92
93
  "expo.inOut",
94
+ "elastic.out(1,0.3)",
95
+ "elastic.inOut(1,0.3)",
93
96
  "spring-gentle",
94
97
  "spring-bouncy",
95
98
  "spring-stiff",
96
99
  "spring-wobbly",
97
100
  "spring-heavy",
101
+ "hold",
98
102
  "steps(1)"
99
103
  ];
100
104
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/gsapConstants.ts"],"sourcesContent":["/**\n * GSAP property and ease constants.\n *\n * Extracted into a standalone module so browser code can import them\n * without pulling in gsapParser (which depends on recast / @babel/parser).\n */\n\nexport const SUPPORTED_PROPS = [\n // 2D Transforms\n \"x\",\n \"y\",\n \"scale\",\n \"scaleX\",\n \"scaleY\",\n \"rotation\",\n \"skewX\",\n \"skewY\",\n // 3D Transforms\n \"z\",\n \"rotationX\",\n \"rotationY\",\n \"rotationZ\",\n \"perspective\",\n \"transformPerspective\",\n \"transformOrigin\",\n // Visibility\n \"opacity\",\n \"visibility\",\n \"autoAlpha\",\n // Dimensions\n \"width\",\n \"height\",\n // Colors\n \"color\",\n \"backgroundColor\",\n \"borderColor\",\n // Box model\n \"borderRadius\",\n // Typography\n \"fontSize\",\n \"letterSpacing\",\n // Filter & Clipping\n \"filter\",\n \"clipPath\",\n // DOM content (number counters, text roll-ups)\n \"innerText\",\n];\n\n// ── Property Groups ─────────────────────────────────────────────────────────\n// Each group maps to an independent GSAP tween so editing one property\n// (e.g. drag → x/y) never contaminates another (e.g. scale, rotation).\n\nexport type PropertyGroupName = \"position\" | \"scale\" | \"size\" | \"rotation\" | \"visual\" | \"other\";\n\nexport const PROPERTY_GROUPS: Record<PropertyGroupName, ReadonlySet<string>> = {\n position: new Set([\"x\", \"y\", \"xPercent\", \"yPercent\"]),\n scale: new Set([\"scale\", \"scaleX\", \"scaleY\"]),\n size: new Set([\"width\", \"height\"]),\n rotation: new Set([\"rotation\", \"skewX\", \"skewY\"]),\n visual: new Set([\"opacity\", \"autoAlpha\"]),\n other: new Set<string>(),\n};\n\nconst PROP_TO_GROUP = new Map<string, PropertyGroupName>();\nfor (const [group, props] of Object.entries(PROPERTY_GROUPS) as [\n PropertyGroupName,\n ReadonlySet<string>,\n][]) {\n for (const p of props) PROP_TO_GROUP.set(p, group);\n}\n\nexport function classifyPropertyGroup(prop: string): PropertyGroupName {\n return PROP_TO_GROUP.get(prop) ?? \"other\";\n}\n\nexport function classifyTweenPropertyGroup(\n properties: Record<string, unknown>,\n): PropertyGroupName | undefined {\n const groups = new Set<PropertyGroupName>();\n for (const key of Object.keys(properties)) {\n // transformOrigin is a modifier; `_auto` is Studio's internal endpoint marker;\n // `data` is GSAP-reserved (carries the Studio hold-set tag). None is an animated\n // property, so none should affect the group.\n if (key === \"transformOrigin\" || key === \"_auto\" || key === \"data\") continue;\n const g = classifyPropertyGroup(key);\n groups.add(g);\n }\n if (groups.size === 1) return groups.values().next().value;\n return undefined;\n}\n\nexport const SUPPORTED_EASES = [\n \"none\",\n \"power1.in\",\n \"power1.out\",\n \"power1.inOut\",\n \"power2.in\",\n \"power2.out\",\n \"power2.inOut\",\n \"power3.in\",\n \"power3.out\",\n \"power3.inOut\",\n \"power4.in\",\n \"power4.out\",\n \"power4.inOut\",\n \"back.in\",\n \"back.out\",\n \"back.inOut\",\n \"elastic.in\",\n \"elastic.out\",\n \"elastic.inOut\",\n \"bounce.in\",\n \"bounce.out\",\n \"bounce.inOut\",\n \"expo.in\",\n \"expo.out\",\n \"expo.inOut\",\n \"spring-gentle\",\n \"spring-bouncy\",\n \"spring-stiff\",\n \"spring-wobbly\",\n \"spring-heavy\",\n \"steps(1)\",\n];\n"],"mappings":";AAOO,IAAM,kBAAkB;AAAA;AAAA,EAE7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAQO,IAAM,kBAAkE;AAAA,EAC7E,UAAU,oBAAI,IAAI,CAAC,KAAK,KAAK,YAAY,UAAU,CAAC;AAAA,EACpD,OAAO,oBAAI,IAAI,CAAC,SAAS,UAAU,QAAQ,CAAC;AAAA,EAC5C,MAAM,oBAAI,IAAI,CAAC,SAAS,QAAQ,CAAC;AAAA,EACjC,UAAU,oBAAI,IAAI,CAAC,YAAY,SAAS,OAAO,CAAC;AAAA,EAChD,QAAQ,oBAAI,IAAI,CAAC,WAAW,WAAW,CAAC;AAAA,EACxC,OAAO,oBAAI,IAAY;AACzB;AAEA,IAAM,gBAAgB,oBAAI,IAA+B;AACzD,WAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,eAAe,GAGtD;AACH,aAAW,KAAK,MAAO,eAAc,IAAI,GAAG,KAAK;AACnD;AAEO,SAAS,sBAAsB,MAAiC;AACrE,SAAO,cAAc,IAAI,IAAI,KAAK;AACpC;AAEO,SAAS,2BACd,YAC+B;AAC/B,QAAM,SAAS,oBAAI,IAAuB;AAC1C,aAAW,OAAO,OAAO,KAAK,UAAU,GAAG;AAIzC,QAAI,QAAQ,qBAAqB,QAAQ,WAAW,QAAQ,OAAQ;AACpE,UAAM,IAAI,sBAAsB,GAAG;AACnC,WAAO,IAAI,CAAC;AAAA,EACd;AACA,MAAI,OAAO,SAAS,EAAG,QAAO,OAAO,OAAO,EAAE,KAAK,EAAE;AACrD,SAAO;AACT;AAEO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/gsapConstants.ts"],"sourcesContent":["/**\n * GSAP property and ease constants.\n *\n * Extracted into a standalone module so browser code can import them\n * without pulling in gsapParser (which depends on recast / @babel/parser).\n */\n\nexport const SUPPORTED_PROPS = [\n // 2D Transforms\n \"x\",\n \"y\",\n \"scale\",\n \"scaleX\",\n \"scaleY\",\n \"rotation\",\n \"skewX\",\n \"skewY\",\n // 3D Transforms\n \"z\",\n \"rotationX\",\n \"rotationY\",\n \"rotationZ\",\n \"perspective\",\n \"transformPerspective\",\n \"transformOrigin\",\n // Visibility\n \"opacity\",\n \"visibility\",\n \"autoAlpha\",\n // Dimensions\n \"width\",\n \"height\",\n // Colors\n \"color\",\n \"backgroundColor\",\n \"borderColor\",\n // Box model\n \"borderRadius\",\n // Typography\n \"fontSize\",\n \"letterSpacing\",\n // Filter & Clipping\n \"filter\",\n \"clipPath\",\n // DOM content (number counters, text roll-ups)\n \"innerText\",\n];\n\n// ── Property Groups ─────────────────────────────────────────────────────────\n// Each group maps to an independent GSAP tween so editing one property\n// (e.g. drag → x/y) never contaminates another (e.g. scale, rotation).\n\nexport type PropertyGroupName = \"position\" | \"scale\" | \"size\" | \"rotation\" | \"visual\" | \"other\";\n\nexport const PROPERTY_GROUPS: Record<PropertyGroupName, ReadonlySet<string>> = {\n position: new Set([\"x\", \"y\", \"xPercent\", \"yPercent\"]),\n scale: new Set([\"scale\", \"scaleX\", \"scaleY\"]),\n size: new Set([\"width\", \"height\"]),\n rotation: new Set([\"rotation\", \"skewX\", \"skewY\"]),\n visual: new Set([\"opacity\", \"autoAlpha\"]),\n other: new Set<string>(),\n};\n\nconst PROP_TO_GROUP = new Map<string, PropertyGroupName>();\nfor (const [group, props] of Object.entries(PROPERTY_GROUPS) as [\n PropertyGroupName,\n ReadonlySet<string>,\n][]) {\n for (const p of props) PROP_TO_GROUP.set(p, group);\n}\n\nexport function classifyPropertyGroup(prop: string): PropertyGroupName {\n return PROP_TO_GROUP.get(prop) ?? \"other\";\n}\n\nexport function classifyTweenPropertyGroup(\n properties: Record<string, unknown>,\n): PropertyGroupName | undefined {\n const groups = new Set<PropertyGroupName>();\n for (const key of Object.keys(properties)) {\n // transformOrigin is a modifier; `_auto` is Studio's internal endpoint marker;\n // `data` is GSAP-reserved (carries the Studio hold-set tag). None is an animated\n // property, so none should affect the group.\n if (key === \"transformOrigin\" || key === \"_auto\" || key === \"data\") continue;\n const g = classifyPropertyGroup(key);\n groups.add(g);\n }\n if (groups.size === 1) return groups.values().next().value;\n return undefined;\n}\n\nexport const SUPPORTED_EASES = [\n \"none\",\n \"power1.in\",\n \"power1.out\",\n \"power1.inOut\",\n \"power2.in\",\n \"power2.out\",\n \"power2.inOut\",\n \"power3.in\",\n \"power3.out\",\n \"power3.inOut\",\n \"power4.in\",\n \"power4.out\",\n \"power4.inOut\",\n \"back.in\",\n \"back.out\",\n \"back.inOut\",\n \"elastic.in\",\n \"elastic.out\",\n \"elastic.inOut\",\n \"bounce.in\",\n \"bounce.out\",\n \"bounce.inOut\",\n \"circ.inOut\",\n \"expo.in\",\n \"expo.out\",\n \"expo.inOut\",\n \"elastic.out(1,0.3)\",\n \"elastic.inOut(1,0.3)\",\n \"spring-gentle\",\n \"spring-bouncy\",\n \"spring-stiff\",\n \"spring-wobbly\",\n \"spring-heavy\",\n \"hold\",\n \"steps(1)\",\n];\n"],"mappings":";AAOO,IAAM,kBAAkB;AAAA;AAAA,EAE7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAQO,IAAM,kBAAkE;AAAA,EAC7E,UAAU,oBAAI,IAAI,CAAC,KAAK,KAAK,YAAY,UAAU,CAAC;AAAA,EACpD,OAAO,oBAAI,IAAI,CAAC,SAAS,UAAU,QAAQ,CAAC;AAAA,EAC5C,MAAM,oBAAI,IAAI,CAAC,SAAS,QAAQ,CAAC;AAAA,EACjC,UAAU,oBAAI,IAAI,CAAC,YAAY,SAAS,OAAO,CAAC;AAAA,EAChD,QAAQ,oBAAI,IAAI,CAAC,WAAW,WAAW,CAAC;AAAA,EACxC,OAAO,oBAAI,IAAY;AACzB;AAEA,IAAM,gBAAgB,oBAAI,IAA+B;AACzD,WAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,eAAe,GAGtD;AACH,aAAW,KAAK,MAAO,eAAc,IAAI,GAAG,KAAK;AACnD;AAEO,SAAS,sBAAsB,MAAiC;AACrE,SAAO,cAAc,IAAI,IAAI,KAAK;AACpC;AAEO,SAAS,2BACd,YAC+B;AAC/B,QAAM,SAAS,oBAAI,IAAuB;AAC1C,aAAW,OAAO,OAAO,KAAK,UAAU,GAAG;AAIzC,QAAI,QAAQ,qBAAqB,QAAQ,WAAW,QAAQ,OAAQ;AACpE,UAAM,IAAI,sBAAsB,GAAG;AACnC,WAAO,IAAI,CAAC;AAAA,EACd;AACA,MAAI,OAAO,SAAS,EAAG,QAAO,OAAO,OAAO,EAAE,KAAK,EAAE;AACrD,SAAO;AACT;AAEO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;","names":[]}
@@ -77,10 +77,9 @@ declare function removeKeyframeFromScript(script: string, animationId: string, p
77
77
  /**
78
78
  * Retime a keyframe: move the keyframe at `fromPercentage` to `toPercentage`,
79
79
  * PRESERVING its properties and per-keyframe ease (the Studio "Move to Playhead"
80
- * gesture). Re-sorts keyframes by percentage. If a keyframe already exists at
81
- * `toPercentage`, it is overwritten by the moved one (no duplicate). No-op when
82
- * the animation/keyframe isn't found, the tween has no object-form keyframes, or
83
- * the move resolves onto the same keyframe. Acorn twin: moveKeyframeInScript.
80
+ * gesture). Re-sorts keyframes by percentage. No-op when the animation/keyframe
81
+ * isn't found, the tween has no object-form keyframes, the move resolves onto the
82
+ * same keyframe, or the destination is occupied. Acorn twin: moveKeyframeInScript.
84
83
  */
85
84
  declare function moveKeyframeInScript(script: string, animationId: string, fromPercentage: number, toPercentage: number): string;
86
85
  /**
@@ -91,18 +91,39 @@ var SUPPORTED_EASES = [
91
91
  "bounce.in",
92
92
  "bounce.out",
93
93
  "bounce.inOut",
94
+ "circ.inOut",
94
95
  "expo.in",
95
96
  "expo.out",
96
97
  "expo.inOut",
98
+ "elastic.out(1,0.3)",
99
+ "elastic.inOut(1,0.3)",
97
100
  "spring-gentle",
98
101
  "spring-bouncy",
99
102
  "spring-stiff",
100
103
  "spring-wobbly",
101
104
  "spring-heavy",
105
+ "hold",
102
106
  "steps(1)"
103
107
  ];
104
108
 
105
109
  // src/gsapSerialize.ts
110
+ function mergePercentageKeyframes(keyframes) {
111
+ const byPercentage = /* @__PURE__ */ new Map();
112
+ for (const keyframe of keyframes) {
113
+ const existing = byPercentage.get(keyframe.percentage);
114
+ if (!existing) {
115
+ byPercentage.set(keyframe.percentage, {
116
+ ...keyframe,
117
+ properties: { ...keyframe.properties }
118
+ });
119
+ continue;
120
+ }
121
+ existing.properties = { ...existing.properties, ...keyframe.properties };
122
+ if (keyframe.ease !== void 0) existing.ease = keyframe.ease;
123
+ if (keyframe.auto !== void 0) existing.auto = keyframe.auto;
124
+ }
125
+ return [...byPercentage.values()].sort((a, b) => a.percentage - b.percentage);
126
+ }
106
127
  function serializeGsapAnimations(animations, timelineVar = "tl", options) {
107
128
  const sorted = [...animations].sort((a, b) => {
108
129
  const aNum = a.resolvedStart ?? (typeof a.position === "number" ? a.position : Number.MAX_SAFE_INTEGER);
@@ -330,6 +351,61 @@ function resolveConversionProps(anim, resolvedFromValues) {
330
351
  return { fromProps: { ...anim.fromProperties ?? {} }, toProps };
331
352
  }
332
353
 
354
+ // src/gsapObjectArrayTiming.ts
355
+ var roundPercentage = (percentage) => Math.round(percentage * 10) / 10;
356
+ var OBJECT_ARRAY_PERCENTAGE_TOLERANCE = 2;
357
+ function getObjectArrayKeyframeTiming(durations) {
358
+ const hasAuthoredDuration = durations.some((duration) => duration !== void 0);
359
+ if (hasAuthoredDuration) {
360
+ if (!durations.every(
361
+ (duration) => typeof duration === "number" && Number.isFinite(duration) && duration > 0
362
+ )) {
363
+ return null;
364
+ }
365
+ const totalDuration = durations.reduce((sum, duration) => sum + duration, 0);
366
+ let cumulative = 0;
367
+ return {
368
+ percentages: durations.map((duration) => {
369
+ cumulative += duration;
370
+ return roundPercentage(cumulative / totalDuration * 100);
371
+ }),
372
+ totalDuration
373
+ };
374
+ }
375
+ const lastIndex = durations.length - 1;
376
+ return {
377
+ percentages: durations.map(
378
+ (_, index) => lastIndex > 0 ? roundPercentage(index / lastIndex * 100) : 0
379
+ )
380
+ };
381
+ }
382
+ function getCompatibleObjectArrayKeyframeTiming(durations, outerDuration) {
383
+ const timing = getObjectArrayKeyframeTiming(durations);
384
+ if (!timing) return null;
385
+ if (timing.totalDuration === void 0 || outerDuration === void 0) return timing;
386
+ if (typeof outerDuration === "number" && Math.abs(outerDuration - timing.totalDuration) <= Number.EPSILON) {
387
+ return timing;
388
+ }
389
+ return null;
390
+ }
391
+ function findObjectArrayKeyframeIndex(durations, percentage, options) {
392
+ if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) return null;
393
+ const timing = getObjectArrayKeyframeTiming(durations);
394
+ if (!timing) return null;
395
+ const { percentages } = timing;
396
+ let match = null;
397
+ let bestDistance = Number.POSITIVE_INFINITY;
398
+ for (let index = 0; index < percentages.length; index++) {
399
+ const distance = Math.abs(percentages[index] - percentage);
400
+ if (distance < bestDistance) {
401
+ match = index;
402
+ bestDistance = distance;
403
+ }
404
+ }
405
+ const tolerance = options?.tolerance ?? OBJECT_ARRAY_PERCENTAGE_TOLERANCE;
406
+ return bestDistance <= tolerance || options?.fallbackToNearest ? match : null;
407
+ }
408
+
333
409
  // src/springEase.ts
334
410
  var SPRING_PRESETS = [
335
411
  { name: "spring-gentle", label: "Gentle", mass: 1, stiffness: 100, damping: 15 },
@@ -776,6 +852,8 @@ function parsePercentageKeyframes(node, scope) {
776
852
  for (const [k, v] of Object.entries(record)) {
777
853
  if (k === "ease" && typeof v === "string") {
778
854
  kfEase = v;
855
+ } else if (k === "duration") {
856
+ continue;
779
857
  } else if (typeof v === "number" || typeof v === "string") {
780
858
  properties[k] = v;
781
859
  }
@@ -800,13 +878,13 @@ function computeKeyframesTotalDuration(varsNode, scope) {
800
878
  (p) => (p.key?.name ?? p.key?.value) === "keyframes"
801
879
  )?.value;
802
880
  if (!kfNode || kfNode.type !== "ArrayExpression") return void 0;
803
- let total = 0;
881
+ const durations = [];
804
882
  for (const el of kfNode.elements ?? []) {
805
883
  if (!el || el.type !== "ObjectExpression") continue;
806
884
  const r = objectExpressionToRecord(el, scope);
807
- if (typeof r.duration === "number") total += r.duration;
885
+ durations.push(r.duration);
808
886
  }
809
- return total > 0 ? total : void 0;
887
+ return getObjectArrayKeyframeTiming(durations)?.totalDuration;
810
888
  }
811
889
  function parseObjectArrayKeyframes(node, scope) {
812
890
  const elements = node.elements ?? [];
@@ -820,7 +898,7 @@ function parseObjectArrayKeyframes(node, scope) {
820
898
  let duration;
821
899
  let ease;
822
900
  for (const [k, v] of Object.entries(record)) {
823
- if (k === "duration" && typeof v === "number") {
901
+ if (k === "duration") {
824
902
  duration = v;
825
903
  } else if (k === "ease" && typeof v === "string") {
826
904
  ease = v;
@@ -830,30 +908,16 @@ function parseObjectArrayKeyframes(node, scope) {
830
908
  }
831
909
  raw.push({ properties, duration, ease });
832
910
  }
833
- const totalDuration = raw.reduce((sum, r) => sum + (r.duration ?? 0), 0);
834
- const keyframes = [];
835
- if (totalDuration > 0) {
836
- let cumulative = 0;
837
- for (const entry of raw) {
838
- cumulative += entry.duration ?? 0;
839
- const percentage = Math.round(cumulative / totalDuration * 100);
840
- keyframes.push({
841
- percentage,
842
- properties: entry.properties,
843
- ...entry.ease ? { ease: entry.ease } : {}
844
- });
845
- }
846
- } else {
847
- for (let i = 0; i < raw.length; i++) {
848
- const entry = raw[i];
849
- const percentage = raw.length > 1 ? Math.round(i / (raw.length - 1) * 100) : 0;
850
- keyframes.push({
851
- percentage,
852
- properties: entry.properties,
853
- ...entry.ease ? { ease: entry.ease } : {}
854
- });
855
- }
856
- }
911
+ const timing = getObjectArrayKeyframeTiming(raw.map((entry) => entry.duration));
912
+ if (!timing) return void 0;
913
+ const { percentages } = timing;
914
+ const keyframes = raw.map(
915
+ (entry, index) => ({
916
+ percentage: percentages[index],
917
+ properties: entry.properties,
918
+ ...entry.ease ? { ease: entry.ease } : {}
919
+ })
920
+ );
857
921
  return { format: "object-array", keyframes };
858
922
  }
859
923
  function parseSimpleArrayKeyframes(node, scope) {
@@ -1680,7 +1744,7 @@ function keyframePropsToCode(kf) {
1680
1744
  return Object.entries(kf.properties).map(([k, v]) => `${safeJsKey(k)}: ${serializeValue(v)}`);
1681
1745
  }
1682
1746
  function buildKeyframeObjectCode(keyframes, options) {
1683
- const entries = keyframes.map((kf) => {
1747
+ const entries = mergePercentageKeyframes(keyframes).map((kf) => {
1684
1748
  const props = keyframePropsToCode(kf);
1685
1749
  if (kf.ease) props.push(`ease: ${JSON.stringify(kf.ease)}`);
1686
1750
  if (kf.auto) props.push(`_auto: 1`);
@@ -1719,6 +1783,17 @@ function buildKeyframeValueNode(properties, ease) {
1719
1783
  if (effectiveEase) entries.push(`ease: ${JSON.stringify(effectiveEase)}`);
1720
1784
  return parseExpr(`{ ${entries.join(", ")} }`);
1721
1785
  }
1786
+ function setObjectExpressionEase(node, ease) {
1787
+ if (node?.type !== "ObjectExpression") return false;
1788
+ const props = node.properties ?? [];
1789
+ const easeIdx = props.findIndex(
1790
+ (property) => isObjectProperty(property) && propKeyName(property) === "ease"
1791
+ );
1792
+ const easeNode = parseExpr(`({ ease: ${JSON.stringify(ease)} })`).properties[0];
1793
+ if (easeIdx >= 0) props[easeIdx] = easeNode;
1794
+ else props.push(easeNode);
1795
+ return true;
1796
+ }
1722
1797
  function locateAnimation(script, animationId) {
1723
1798
  let parsed;
1724
1799
  try {
@@ -1765,7 +1840,7 @@ function findKeyframesObjectNode(varsArg) {
1765
1840
  const node = findPropertyNode(varsArg, "keyframes");
1766
1841
  return node?.type === "ObjectExpression" ? node : null;
1767
1842
  }
1768
- function convertArrayKeyframesToObjectNode(varsArg) {
1843
+ function convertArrayKeyframesToObjectNode(varsArg, scope) {
1769
1844
  if (varsArg?.type !== "ObjectExpression") return null;
1770
1845
  const prop = (varsArg.properties ?? []).find(
1771
1846
  (p) => isObjectProperty(p) && propKeyName(p) === "keyframes"
@@ -1774,11 +1849,22 @@ function convertArrayKeyframesToObjectNode(varsArg) {
1774
1849
  const els = (prop.value.elements ?? []).filter(
1775
1850
  (e) => !!e && e.type === "ObjectExpression"
1776
1851
  );
1777
- const n = els.length;
1778
- if (n === 0) return null;
1852
+ if (els.length === 0) return null;
1853
+ const records = els.map((element) => objectExpressionToRecord(element, scope));
1854
+ const outerDuration = objectExpressionToRecord(varsArg, scope).duration;
1855
+ const timing = getCompatibleObjectArrayKeyframeTiming(
1856
+ records.map((record) => record.duration),
1857
+ outerDuration
1858
+ );
1859
+ if (!timing) return null;
1860
+ if (timing.totalDuration !== void 0 && findPropertyNode(varsArg, "duration") === void 0) {
1861
+ setVarsKey(varsArg, "duration", timing.totalDuration);
1862
+ }
1779
1863
  const entries = els.map((el, i) => {
1780
- const pct = n > 1 ? Math.round(i / (n - 1) * 1e3) / 10 : 0;
1781
- return `${JSON.stringify(`${pct}%`)}: ${recast.print(el).code}`;
1864
+ el.properties = (el.properties ?? []).filter(
1865
+ (property) => !isObjectProperty(property) || propKeyName(property) !== "duration"
1866
+ );
1867
+ return `${JSON.stringify(`${timing.percentages[i]}%`)}: ${recast.print(el).code}`;
1782
1868
  });
1783
1869
  prop.value = parseExpr(`{ ${entries.join(", ")} }`);
1784
1870
  return prop.value;
@@ -1809,7 +1895,9 @@ function addKeyframeToScript(script, animationId, percentage, properties, ease,
1809
1895
  let loc = locateAnimationWithFallback(script, animationId);
1810
1896
  if (!loc) return script;
1811
1897
  let kfNode = findKeyframesObjectNode(loc.target.call.varsArg);
1812
- if (!kfNode) kfNode = convertArrayKeyframesToObjectNode(loc.target.call.varsArg);
1898
+ if (!kfNode) {
1899
+ kfNode = convertArrayKeyframesToObjectNode(loc.target.call.varsArg, loc.parsed.scope);
1900
+ }
1813
1901
  if (!kfNode) {
1814
1902
  script = convertToKeyframesInScript(script, animationId);
1815
1903
  loc = locateAnimationWithFallback(script, animationId);
@@ -1894,19 +1982,12 @@ function removeKeyframeFromScript(script, animationId, percentage) {
1894
1982
  const elements = (arrVal.elements ?? []).filter(
1895
1983
  (e) => !!e && e.type === "ObjectExpression"
1896
1984
  );
1897
- const n = elements.length;
1898
- if (n === 0) return script;
1899
- let matchIdx = -1;
1900
- let bestDist = Number.POSITIVE_INFINITY;
1901
- for (let i = 0; i < n; i++) {
1902
- const pct = n > 1 ? i / (n - 1) * 100 : 0;
1903
- const dist = Math.abs(pct - percentage);
1904
- if (dist <= PCT_TOLERANCE && dist < bestDist) {
1905
- matchIdx = i;
1906
- bestDist = dist;
1907
- }
1908
- }
1909
- if (matchIdx === -1) return script;
1985
+ if (elements.length === 0) return script;
1986
+ const durations = elements.map(
1987
+ (element) => objectExpressionToRecord(element, arrLoc.parsed.scope).duration
1988
+ );
1989
+ const matchIdx = findObjectArrayKeyframeIndex(durations, percentage);
1990
+ if (matchIdx === null) return script;
1910
1991
  const remaining = elements.filter((_, i) => i !== matchIdx);
1911
1992
  if (remaining.length < 2) {
1912
1993
  const sole = remaining[0];
@@ -1935,18 +2016,17 @@ function removeKeyframeFromScript(script, animationId, percentage) {
1935
2016
  function moveKeyframeInScript(script, animationId, fromPercentage, toPercentage) {
1936
2017
  const loc = locateAnimationWithFallback(script, animationId);
1937
2018
  if (!loc) return script;
1938
- const kfNode = findKeyframesObjectNode(loc.target.call.varsArg) ?? convertArrayKeyframesToObjectNode(loc.target.call.varsArg);
2019
+ const kfNode = findKeyframesObjectNode(loc.target.call.varsArg) ?? convertArrayKeyframesToObjectNode(loc.target.call.varsArg, loc.parsed.scope);
1939
2020
  if (!kfNode) return script;
1940
2021
  const match = findKeyframePropByPct(kfNode, fromPercentage);
1941
2022
  if (!match) return script;
1942
2023
  if (Math.abs(fromPercentage - toPercentage) < MOVE_NOOP_EPSILON_PCT) return script;
1943
2024
  const dest = findKeyframePropByPct(kfNode, toPercentage);
1944
- const collision = dest && dest.prop !== match.prop ? dest : null;
2025
+ if (dest && dest.prop !== match.prop) return script;
1945
2026
  const movedValue = match.prop.value;
1946
2027
  const entries = [];
1947
2028
  for (const prop of filterPercentageProps(kfNode)) {
1948
2029
  if (prop === match.prop) continue;
1949
- if (collision && prop === collision.prop) continue;
1950
2030
  const pct = percentageFromKey(propKeyName(prop) ?? "");
1951
2031
  if (Number.isNaN(pct)) continue;
1952
2032
  entries.push({ pct, value: prop.value });
@@ -1963,7 +2043,7 @@ function moveKeyframeInScript(script, animationId, fromPercentage, toPercentage)
1963
2043
  function resizeKeyframedTweenInScript(script, animationId, newPosition, newDuration, pctRemap) {
1964
2044
  const loc = locateAnimationWithFallback(script, animationId);
1965
2045
  if (!loc) return script;
1966
- const kfNode = findKeyframesObjectNode(loc.target.call.varsArg) ?? convertArrayKeyframesToObjectNode(loc.target.call.varsArg);
2046
+ const kfNode = findKeyframesObjectNode(loc.target.call.varsArg) ?? convertArrayKeyframesToObjectNode(loc.target.call.varsArg, loc.parsed.scope);
1967
2047
  if (!kfNode) return script;
1968
2048
  const seen = /* @__PURE__ */ new Set();
1969
2049
  for (const { from, to } of pctRemap) {
@@ -1972,7 +2052,12 @@ function resizeKeyframedTweenInScript(script, animationId, newPosition, newDurat
1972
2052
  seen.add(match.prop);
1973
2053
  match.prop.key = parseExpr(`{ ${JSON.stringify(`${to}%`)}: 0 }`).properties[0].key;
1974
2054
  }
1975
- applyUpdatesToCall(loc.target.call, { position: newPosition, duration: newDuration });
2055
+ applyUpdatesToCall(loc.target.call, {
2056
+ position: newPosition,
2057
+ // Resizing is an explicit duration-authoring gesture. Promote GSAP's
2058
+ // implicit default so the dragged window is the window that plays.
2059
+ duration: newDuration
2060
+ });
1976
2061
  return recast.print(loc.parsed.ast).code;
1977
2062
  }
1978
2063
  function updateKeyframeInScript(script, animationId, percentage, properties, ease) {
@@ -1982,41 +2067,63 @@ function updateKeyframeInScript(script, animationId, percentage, properties, eas
1982
2067
  const elements = (arrVal.elements ?? []).filter(
1983
2068
  (e) => !!e && e.type === "ObjectExpression"
1984
2069
  );
1985
- const n = elements.length;
1986
- if (n === 0) return script;
1987
- let matchIdx = -1;
1988
- let bestDist = Number.POSITIVE_INFINITY;
1989
- for (let i = 0; i < n; i++) {
1990
- const pct = n > 1 ? i / (n - 1) * 100 : 0;
1991
- const dist = Math.abs(pct - percentage);
1992
- if (dist <= PCT_TOLERANCE && dist < bestDist) {
1993
- matchIdx = i;
1994
- bestDist = dist;
1995
- }
1996
- }
1997
- if (matchIdx === -1) return script;
1998
- const realIdx = arrVal.elements.indexOf(elements[matchIdx]);
1999
- arrVal.elements[realIdx] = buildKeyframeValueNode(properties, ease);
2070
+ if (elements.length === 0) return script;
2071
+ const records = elements.map(
2072
+ (element) => objectExpressionToRecord(element, arrLoc.parsed.scope)
2073
+ );
2074
+ const matchIdx = findObjectArrayKeyframeIndex(
2075
+ records.map((record) => record.duration),
2076
+ percentage,
2077
+ { fallbackToNearest: true }
2078
+ );
2079
+ if (matchIdx === null) return script;
2080
+ const matchEl = elements[matchIdx];
2081
+ if (!matchEl) return script;
2082
+ const realIdx = arrVal.elements.indexOf(matchEl);
2083
+ if (Object.keys(properties).length === 0 && ease && setObjectExpressionEase(matchEl, ease)) {
2084
+ return recast.print(arrLoc.parsed.ast).code;
2085
+ }
2086
+ const merged = {};
2087
+ for (const [key, value] of Object.entries(records[matchIdx] ?? {})) {
2088
+ if (typeof value === "number" || typeof value === "string") merged[key] = value;
2089
+ }
2090
+ Object.assign(merged, properties);
2091
+ arrVal.elements[realIdx] = buildKeyframeValueNode(merged, ease);
2000
2092
  return recast.print(arrLoc.parsed.ast).code;
2001
2093
  }
2094
+ if (arrLoc && !arrVal && arrLoc.target.animation.arcPath?.enabled) {
2095
+ const propertyKeys = Object.keys(properties);
2096
+ if (propertyKeys.some((key) => key !== "x" && key !== "y")) return script;
2097
+ let next = script;
2098
+ if (propertyKeys.length > 0) {
2099
+ const waypoints = extractArcWaypoints(arrLoc.target.animation);
2100
+ if (waypoints.length < 2) return script;
2101
+ const pointIndex = Math.max(
2102
+ 0,
2103
+ Math.min(waypoints.length - 1, Math.round(percentage / 100 * (waypoints.length - 1)))
2104
+ );
2105
+ const current = waypoints[pointIndex];
2106
+ if (!current) return script;
2107
+ const x = properties.x ?? current.x;
2108
+ const y = properties.y ?? current.y;
2109
+ if (typeof x !== "number" || typeof y !== "number") return script;
2110
+ next = updateMotionPathPointInScript(next, animationId, pointIndex, { x, y });
2111
+ }
2112
+ if (ease !== void 0) {
2113
+ const updated = locateAnimationWithFallback(next, animationId);
2114
+ if (!updated) return script;
2115
+ applyUpdatesToCall(updated.target.call, { ease });
2116
+ next = recast.print(updated.parsed.ast).code;
2117
+ }
2118
+ return next;
2119
+ }
2002
2120
  const ctx = locateKeyframeCtx(script, animationId, percentage);
2003
2121
  if (!ctx) return script;
2004
2122
  const { loc, kfNode } = ctx;
2005
2123
  const match = findKeyframePropByPct(kfNode, percentage);
2006
2124
  if (!match) return script;
2007
2125
  if (Object.keys(properties).length === 0 && ease) {
2008
- const existing2 = match.prop.value;
2009
- if (existing2?.type === "ObjectExpression") {
2010
- const props = existing2.properties ?? [];
2011
- const easeIdx = props.findIndex(
2012
- (p) => isObjectProperty(p) && propKeyName(p) === "ease"
2013
- );
2014
- const easeNode = parseExpr(`({ ease: ${JSON.stringify(ease)} })`).properties[0];
2015
- if (easeIdx >= 0) {
2016
- props[easeIdx] = easeNode;
2017
- } else {
2018
- props.push(easeNode);
2019
- }
2126
+ if (setObjectExpressionEase(match.prop.value, ease)) {
2020
2127
  return recast.print(loc.parsed.ast).code;
2021
2128
  }
2022
2129
  return script;
@@ -2089,13 +2196,21 @@ function convertToKeyframesInScript(script, animationId, resolvedFromValues, set
2089
2196
  function removeAllKeyframesFromScript(script, animationId) {
2090
2197
  let loc = locateAnimationWithFallback(script, animationId);
2091
2198
  if (!loc) return script;
2092
- const kfNode = findKeyframesObjectNode(loc.target.call.varsArg) ?? convertArrayKeyframesToObjectNode(loc.target.call.varsArg);
2093
- if (!kfNode) return script;
2094
- const kfEntries = filterPercentageProps(kfNode).map((p) => ({ pct: percentageFromKey(propKeyName(p)), prop: p })).filter((e) => !Number.isNaN(e.pct)).sort((a, b) => a.pct - b.pct);
2095
- if (kfEntries.length === 0) return script;
2199
+ const kfNode = findKeyframesObjectNode(loc.target.call.varsArg) ?? convertArrayKeyframesToObjectNode(loc.target.call.varsArg, loc.parsed.scope);
2096
2200
  const method = loc.target.call.method;
2097
- const collapseEntry = method === "from" ? kfEntries[0] : kfEntries[kfEntries.length - 1];
2098
- const record = objectExpressionToRecord(collapseEntry.prop.value, loc.parsed.scope);
2201
+ let record;
2202
+ if (kfNode) {
2203
+ const kfEntries = filterPercentageProps(kfNode).map((p) => ({ pct: percentageFromKey(propKeyName(p)), prop: p })).filter((e) => !Number.isNaN(e.pct)).sort((a, b) => a.pct - b.pct);
2204
+ if (kfEntries.length === 0) return script;
2205
+ const collapseEntry = method === "from" ? kfEntries[0] : kfEntries[kfEntries.length - 1];
2206
+ record = objectExpressionToRecord(collapseEntry.prop.value, loc.parsed.scope);
2207
+ } else {
2208
+ const synthetic = loc.target.animation.arcPath?.enabled ? loc.target.animation.keyframes?.keyframes : void 0;
2209
+ if (!synthetic?.length) return script;
2210
+ const sorted = [...synthetic].sort((a, b) => a.percentage - b.percentage);
2211
+ record = (method === "from" ? sorted[0] : sorted[sorted.length - 1]).properties;
2212
+ removeVarsKey(loc.target.call.varsArg, "motionPath");
2213
+ }
2099
2214
  collapseKeyframesToFlat(loc.target.call.varsArg, record);
2100
2215
  removeVarsKey(loc.target.call.varsArg, "ease");
2101
2216
  setVarsKey(loc.target.call.varsArg, "duration", 0);