@hyperframes/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/dist/adapters/gsap.d.ts +14 -0
- package/dist/adapters/gsap.d.ts.map +1 -0
- package/dist/adapters/gsap.js +25 -0
- package/dist/adapters/gsap.js.map +1 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/compiler/htmlBundler.d.ts +16 -0
- package/dist/compiler/htmlBundler.d.ts.map +1 -0
- package/dist/compiler/htmlBundler.js +448 -0
- package/dist/compiler/htmlBundler.js.map +1 -0
- package/dist/compiler/htmlCompiler.d.ts +18 -0
- package/dist/compiler/htmlCompiler.d.ts.map +1 -0
- package/dist/compiler/htmlCompiler.js +65 -0
- package/dist/compiler/htmlCompiler.js.map +1 -0
- package/dist/compiler/index.d.ts +5 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js +9 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/compiler/staticGuard.d.ts +8 -0
- package/dist/compiler/staticGuard.d.ts.map +1 -0
- package/dist/compiler/staticGuard.js +26 -0
- package/dist/compiler/staticGuard.js.map +1 -0
- package/dist/compiler/timingCompiler.d.ts +72 -0
- package/dist/compiler/timingCompiler.d.ts.map +1 -0
- package/dist/compiler/timingCompiler.js +191 -0
- package/dist/compiler/timingCompiler.js.map +1 -0
- package/dist/core.types.d.ts +314 -0
- package/dist/core.types.d.ts.map +1 -0
- package/dist/core.types.js +52 -0
- package/dist/core.types.js.map +1 -0
- package/dist/generators/hyperframes.d.ts +21 -0
- package/dist/generators/hyperframes.d.ts.map +1 -0
- package/dist/generators/hyperframes.js +572 -0
- package/dist/generators/hyperframes.js.map +1 -0
- package/dist/hyperframe.manifest.json +22 -0
- package/dist/hyperframe.runtime.iife.js +13 -0
- package/dist/hyperframe.runtime.mjs +13 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/inline-scripts/hyperframe.d.ts +13 -0
- package/dist/inline-scripts/hyperframe.d.ts.map +1 -0
- package/dist/inline-scripts/hyperframe.js +15 -0
- package/dist/inline-scripts/hyperframe.js.map +1 -0
- package/dist/inline-scripts/hyperframesRuntime.engine.d.ts +7 -0
- package/dist/inline-scripts/hyperframesRuntime.engine.d.ts.map +1 -0
- package/dist/inline-scripts/hyperframesRuntime.engine.js +31 -0
- package/dist/inline-scripts/hyperframesRuntime.engine.js.map +1 -0
- package/dist/inline-scripts/parityContract.d.ts +5 -0
- package/dist/inline-scripts/parityContract.d.ts.map +1 -0
- package/dist/inline-scripts/parityContract.js +43 -0
- package/dist/inline-scripts/parityContract.js.map +1 -0
- package/dist/inline-scripts/pickerApi.d.ts +32 -0
- package/dist/inline-scripts/pickerApi.d.ts.map +1 -0
- package/dist/inline-scripts/pickerApi.js +2 -0
- package/dist/inline-scripts/pickerApi.js.map +1 -0
- package/dist/inline-scripts/runtimeContract.d.ts +14 -0
- package/dist/inline-scripts/runtimeContract.d.ts.map +1 -0
- package/dist/inline-scripts/runtimeContract.js +21 -0
- package/dist/inline-scripts/runtimeContract.js.map +1 -0
- package/dist/lint/hyperframeLinter.d.ts +3 -0
- package/dist/lint/hyperframeLinter.d.ts.map +1 -0
- package/dist/lint/hyperframeLinter.js +621 -0
- package/dist/lint/hyperframeLinter.js.map +1 -0
- package/dist/lint/index.d.ts +3 -0
- package/dist/lint/index.d.ts.map +1 -0
- package/dist/lint/index.js +2 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/lint/types.d.ts +21 -0
- package/dist/lint/types.d.ts.map +1 -0
- package/dist/lint/types.js +2 -0
- package/dist/lint/types.js.map +1 -0
- package/dist/parsers/gsapParser.d.ts +50 -0
- package/dist/parsers/gsapParser.d.ts.map +1 -0
- package/dist/parsers/gsapParser.js +411 -0
- package/dist/parsers/gsapParser.js.map +1 -0
- package/dist/parsers/htmlParser.d.ts +29 -0
- package/dist/parsers/htmlParser.d.ts.map +1 -0
- package/dist/parsers/htmlParser.js +726 -0
- package/dist/parsers/htmlParser.js.map +1 -0
- package/dist/templates/base.d.ts +4 -0
- package/dist/templates/base.d.ts.map +1 -0
- package/dist/templates/base.js +20 -0
- package/dist/templates/base.js.map +1 -0
- package/dist/templates/constants.d.ts +7 -0
- package/dist/templates/constants.d.ts.map +1 -0
- package/dist/templates/constants.js +8 -0
- package/dist/templates/constants.js.map +1 -0
- package/docs/common-mistakes.md +73 -0
- package/docs/core.md +532 -0
- package/docs/core_notes.md +61 -0
- package/docs/quickstart-template.html +180 -0
- package/docs/versions/changelog.md +5 -0
- package/docs/versions/v0.1/core.md +326 -0
- package/package.json +83 -0
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import { CANVAS_DIMENSIONS } from "../core.types";
|
|
2
|
+
import { parseGsapScript, validateCompositionGsap, gsapAnimationsToKeyframes, getAnimationsForElement, } from "./gsapParser";
|
|
3
|
+
const MEDIA_TYPES = new Set(["video", "image", "audio"]);
|
|
4
|
+
function getElementType(el) {
|
|
5
|
+
const tag = el.tagName.toLowerCase();
|
|
6
|
+
if (tag === "video")
|
|
7
|
+
return "video";
|
|
8
|
+
if (tag === "img")
|
|
9
|
+
return "image";
|
|
10
|
+
if (tag === "audio")
|
|
11
|
+
return "audio";
|
|
12
|
+
// Check for explicit data-type attribute first
|
|
13
|
+
const dataType = el.getAttribute("data-type");
|
|
14
|
+
if (dataType === "composition")
|
|
15
|
+
return "composition";
|
|
16
|
+
if (dataType === "text")
|
|
17
|
+
return "text";
|
|
18
|
+
// Fall back to tag-based detection for backwards compatibility
|
|
19
|
+
if (tag === "div" || tag === "p" || tag === "h1" || tag === "h2" || tag === "h3" || tag === "span") {
|
|
20
|
+
return "text";
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function getElementName(el) {
|
|
25
|
+
const dataName = el.getAttribute("data-name");
|
|
26
|
+
if (dataName)
|
|
27
|
+
return dataName;
|
|
28
|
+
const type = getElementType(el);
|
|
29
|
+
if (type === "text") {
|
|
30
|
+
const text = el.textContent?.trim().slice(0, 30) || "Text";
|
|
31
|
+
return text.length === 30 ? text + "..." : text;
|
|
32
|
+
}
|
|
33
|
+
const src = el.getAttribute("src");
|
|
34
|
+
if (src) {
|
|
35
|
+
const filename = src.split("/").pop() || src;
|
|
36
|
+
return filename.split("?")[0] ?? filename;
|
|
37
|
+
}
|
|
38
|
+
return el.id || el.className?.toString().split(" ")[0] || "Element";
|
|
39
|
+
}
|
|
40
|
+
function getZIndex(el) {
|
|
41
|
+
const dataLayer = el.getAttribute("data-layer");
|
|
42
|
+
if (dataLayer)
|
|
43
|
+
return parseInt(dataLayer, 10) || 0;
|
|
44
|
+
const style = el.style?.zIndex;
|
|
45
|
+
if (style)
|
|
46
|
+
return parseInt(style, 10) || 0;
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
function parseResolutionFromCss(doc, cssText) {
|
|
50
|
+
const stage = doc.getElementById("stage") || doc.querySelector("#stage");
|
|
51
|
+
if (stage) {
|
|
52
|
+
const inlineStyle = stage.style;
|
|
53
|
+
if (inlineStyle?.width && inlineStyle?.height) {
|
|
54
|
+
const w = parseInt(inlineStyle.width, 10);
|
|
55
|
+
const h = parseInt(inlineStyle.height, 10);
|
|
56
|
+
if (w && h) {
|
|
57
|
+
return w > h ? "landscape" : "portrait";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (cssText) {
|
|
62
|
+
const stageMatch = cssText.match(/#stage\s*\{[^}]*width:\s*(\d+)px[^}]*height:\s*(\d+)px[^}]*\}/);
|
|
63
|
+
if (stageMatch) {
|
|
64
|
+
const w = parseInt(stageMatch[1] ?? "", 10);
|
|
65
|
+
const h = parseInt(stageMatch[2] ?? "", 10);
|
|
66
|
+
return w > h ? "landscape" : "portrait";
|
|
67
|
+
}
|
|
68
|
+
const stageMatchReverse = cssText.match(/#stage\s*\{[^}]*height:\s*(\d+)px[^}]*width:\s*(\d+)px[^}]*\}/);
|
|
69
|
+
if (stageMatchReverse) {
|
|
70
|
+
const h = parseInt(stageMatchReverse[1] ?? "", 10);
|
|
71
|
+
const w = parseInt(stageMatchReverse[2] ?? "", 10);
|
|
72
|
+
return w > h ? "landscape" : "portrait";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return "portrait";
|
|
76
|
+
}
|
|
77
|
+
function parseResolutionFromHtml(doc) {
|
|
78
|
+
const htmlEl = doc.documentElement;
|
|
79
|
+
const resolutionAttr = htmlEl.getAttribute("data-resolution");
|
|
80
|
+
if (resolutionAttr === "landscape" || resolutionAttr === "portrait") {
|
|
81
|
+
return resolutionAttr;
|
|
82
|
+
}
|
|
83
|
+
const widthAttr = htmlEl.getAttribute("data-composition-width");
|
|
84
|
+
const heightAttr = htmlEl.getAttribute("data-composition-height");
|
|
85
|
+
if (widthAttr && heightAttr) {
|
|
86
|
+
const width = parseInt(widthAttr, 10);
|
|
87
|
+
const height = parseInt(heightAttr, 10);
|
|
88
|
+
if (width && height) {
|
|
89
|
+
return width > height ? "landscape" : "portrait";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
export function parseHtml(html) {
|
|
95
|
+
const parser = new DOMParser();
|
|
96
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
97
|
+
const elements = [];
|
|
98
|
+
const keyframes = {};
|
|
99
|
+
let idCounter = 0;
|
|
100
|
+
const htmlEl = doc.documentElement;
|
|
101
|
+
const customStylesAttr = htmlEl.getAttribute("data-custom-styles");
|
|
102
|
+
let customStyles = null;
|
|
103
|
+
if (customStylesAttr) {
|
|
104
|
+
try {
|
|
105
|
+
customStyles = JSON.parse(customStylesAttr);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
customStyles = customStylesAttr;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const timedElements = doc.querySelectorAll("[data-start]");
|
|
112
|
+
timedElements.forEach((el) => {
|
|
113
|
+
const type = getElementType(el);
|
|
114
|
+
if (!type)
|
|
115
|
+
return;
|
|
116
|
+
const start = parseFloat(el.getAttribute("data-start") || "0");
|
|
117
|
+
const dataEnd = el.getAttribute("data-end");
|
|
118
|
+
let duration;
|
|
119
|
+
if (dataEnd) {
|
|
120
|
+
duration = Math.max(0, parseFloat(dataEnd) - start);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
duration = 5;
|
|
124
|
+
}
|
|
125
|
+
const id = el.id || `element-${++idCounter}`;
|
|
126
|
+
const name = getElementName(el);
|
|
127
|
+
const zIndex = getZIndex(el);
|
|
128
|
+
// Parse data-keyframes attribute if present
|
|
129
|
+
const keyframesAttr = el.getAttribute("data-keyframes");
|
|
130
|
+
if (keyframesAttr) {
|
|
131
|
+
try {
|
|
132
|
+
const parsedKeyframes = JSON.parse(keyframesAttr);
|
|
133
|
+
if (Array.isArray(parsedKeyframes) && parsedKeyframes.length > 0) {
|
|
134
|
+
keyframes[id] = parsedKeyframes;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// skip invalid keyframes
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Parse transform properties (x, y, scale, opacity)
|
|
142
|
+
const xAttr = el.getAttribute("data-x");
|
|
143
|
+
const yAttr = el.getAttribute("data-y");
|
|
144
|
+
const scaleAttr = el.getAttribute("data-scale");
|
|
145
|
+
const opacityAttr = el.getAttribute("data-opacity");
|
|
146
|
+
const x = xAttr ? parseFloat(xAttr) : undefined;
|
|
147
|
+
const y = yAttr ? parseFloat(yAttr) : undefined;
|
|
148
|
+
const scale = scaleAttr ? parseFloat(scaleAttr) : undefined;
|
|
149
|
+
const opacity = opacityAttr ? parseFloat(opacityAttr) : undefined;
|
|
150
|
+
if (type === "text") {
|
|
151
|
+
const textEl = el.firstElementChild;
|
|
152
|
+
const content = textEl?.textContent || name;
|
|
153
|
+
const color = el.getAttribute("data-color") || undefined;
|
|
154
|
+
const fontSizeAttr = el.getAttribute("data-font-size");
|
|
155
|
+
const fontSize = fontSizeAttr ? parseInt(fontSizeAttr, 10) : undefined;
|
|
156
|
+
const fontWeightAttr = el.getAttribute("data-font-weight");
|
|
157
|
+
const fontWeight = fontWeightAttr ? parseInt(fontWeightAttr, 10) : undefined;
|
|
158
|
+
const fontFamily = el.getAttribute("data-font-family") || undefined;
|
|
159
|
+
const textShadowAttr = el.getAttribute("data-text-shadow");
|
|
160
|
+
const textShadow = textShadowAttr === "false" ? false : undefined;
|
|
161
|
+
// Parse outline properties
|
|
162
|
+
const textOutlineAttr = el.getAttribute("data-text-outline");
|
|
163
|
+
const textOutline = textOutlineAttr === "true" ? true : undefined;
|
|
164
|
+
const textOutlineColor = el.getAttribute("data-text-outline-color") || undefined;
|
|
165
|
+
const textOutlineWidthAttr = el.getAttribute("data-text-outline-width");
|
|
166
|
+
const textOutlineWidth = textOutlineWidthAttr ? parseInt(textOutlineWidthAttr, 10) : undefined;
|
|
167
|
+
// Parse highlight properties
|
|
168
|
+
const textHighlightAttr = el.getAttribute("data-text-highlight");
|
|
169
|
+
const textHighlight = textHighlightAttr === "true" ? true : undefined;
|
|
170
|
+
const textHighlightColor = el.getAttribute("data-text-highlight-color") || undefined;
|
|
171
|
+
const textHighlightPaddingAttr = el.getAttribute("data-text-highlight-padding");
|
|
172
|
+
const textHighlightPadding = textHighlightPaddingAttr ? parseInt(textHighlightPaddingAttr, 10) : undefined;
|
|
173
|
+
const textHighlightRadiusAttr = el.getAttribute("data-text-highlight-radius");
|
|
174
|
+
const textHighlightRadius = textHighlightRadiusAttr ? parseInt(textHighlightRadiusAttr, 10) : undefined;
|
|
175
|
+
const textElement = {
|
|
176
|
+
id,
|
|
177
|
+
type: "text",
|
|
178
|
+
name,
|
|
179
|
+
content,
|
|
180
|
+
startTime: start,
|
|
181
|
+
duration,
|
|
182
|
+
zIndex,
|
|
183
|
+
x,
|
|
184
|
+
y,
|
|
185
|
+
scale,
|
|
186
|
+
opacity,
|
|
187
|
+
color,
|
|
188
|
+
fontSize,
|
|
189
|
+
fontWeight,
|
|
190
|
+
fontFamily,
|
|
191
|
+
textShadow,
|
|
192
|
+
textOutline,
|
|
193
|
+
textOutlineColor,
|
|
194
|
+
textOutlineWidth,
|
|
195
|
+
textHighlight,
|
|
196
|
+
textHighlightColor,
|
|
197
|
+
textHighlightPadding,
|
|
198
|
+
textHighlightRadius,
|
|
199
|
+
};
|
|
200
|
+
elements.push(textElement);
|
|
201
|
+
}
|
|
202
|
+
else if (type === "composition") {
|
|
203
|
+
// Composition is a div container with iframe inside
|
|
204
|
+
const iframe = el.querySelector("iframe");
|
|
205
|
+
const src = iframe?.getAttribute("src") || el.getAttribute("src") || "";
|
|
206
|
+
const compositionId = el.getAttribute("data-composition-id") || "";
|
|
207
|
+
const sourceDurationAttr = el.getAttribute("data-source-duration");
|
|
208
|
+
const sourceDuration = sourceDurationAttr ? parseFloat(sourceDurationAttr) : undefined;
|
|
209
|
+
const sourceWidthAttr = el.getAttribute("data-source-width");
|
|
210
|
+
const sourceWidth = sourceWidthAttr ? parseInt(sourceWidthAttr, 10) : undefined;
|
|
211
|
+
const sourceHeightAttr = el.getAttribute("data-source-height");
|
|
212
|
+
const sourceHeight = sourceHeightAttr ? parseInt(sourceHeightAttr, 10) : undefined;
|
|
213
|
+
// Parse variable values if present
|
|
214
|
+
const variableValuesAttr = el.getAttribute("data-variable-values");
|
|
215
|
+
let variableValues;
|
|
216
|
+
if (variableValuesAttr) {
|
|
217
|
+
try {
|
|
218
|
+
variableValues = JSON.parse(variableValuesAttr);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// skip invalid variable values
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const compositionElement = {
|
|
225
|
+
id,
|
|
226
|
+
type: "composition",
|
|
227
|
+
name,
|
|
228
|
+
src,
|
|
229
|
+
compositionId,
|
|
230
|
+
startTime: start,
|
|
231
|
+
duration,
|
|
232
|
+
zIndex,
|
|
233
|
+
x,
|
|
234
|
+
y,
|
|
235
|
+
scale,
|
|
236
|
+
opacity,
|
|
237
|
+
sourceDuration,
|
|
238
|
+
sourceWidth,
|
|
239
|
+
sourceHeight,
|
|
240
|
+
variableValues,
|
|
241
|
+
};
|
|
242
|
+
elements.push(compositionElement);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
if (!MEDIA_TYPES.has(type))
|
|
246
|
+
return;
|
|
247
|
+
const src = el.getAttribute("src") || "";
|
|
248
|
+
const mediaStartTimeAttr = el.getAttribute("data-media-start");
|
|
249
|
+
const mediaStartTime = mediaStartTimeAttr ? parseFloat(mediaStartTimeAttr) : undefined;
|
|
250
|
+
const sourceDurationAttr = el.getAttribute("data-source-duration");
|
|
251
|
+
const sourceDuration = sourceDurationAttr ? parseFloat(sourceDurationAttr) : undefined;
|
|
252
|
+
const isArollAttr = el.getAttribute("data-aroll");
|
|
253
|
+
const isAroll = isArollAttr === "true" ? true : undefined;
|
|
254
|
+
const volumeAttr = el.getAttribute("data-volume");
|
|
255
|
+
const volume = volumeAttr ? parseFloat(volumeAttr) : undefined;
|
|
256
|
+
const hasAudioAttr = el.getAttribute("data-has-audio");
|
|
257
|
+
const hasAudio = hasAudioAttr === "true" ? true : undefined;
|
|
258
|
+
const mediaElement = {
|
|
259
|
+
id,
|
|
260
|
+
type: type,
|
|
261
|
+
name,
|
|
262
|
+
src,
|
|
263
|
+
startTime: start,
|
|
264
|
+
duration,
|
|
265
|
+
zIndex,
|
|
266
|
+
x,
|
|
267
|
+
y,
|
|
268
|
+
scale,
|
|
269
|
+
opacity,
|
|
270
|
+
mediaStartTime,
|
|
271
|
+
sourceDuration,
|
|
272
|
+
isAroll,
|
|
273
|
+
volume,
|
|
274
|
+
hasAudio,
|
|
275
|
+
};
|
|
276
|
+
elements.push(mediaElement);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
const scriptTags = doc.querySelectorAll("script");
|
|
280
|
+
let gsapScript = null;
|
|
281
|
+
for (const script of scriptTags) {
|
|
282
|
+
const src = script.getAttribute("src");
|
|
283
|
+
if (src && src.includes("gsap"))
|
|
284
|
+
continue;
|
|
285
|
+
const content = script.textContent?.trim();
|
|
286
|
+
if (content && (content.includes("gsap") || content.includes("timeline"))) {
|
|
287
|
+
gsapScript = content;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Extract x/y positions and scale from GSAP script
|
|
292
|
+
if (gsapScript) {
|
|
293
|
+
const positionMap = extractPositionsFromGsap(gsapScript);
|
|
294
|
+
for (const element of elements) {
|
|
295
|
+
const pos = positionMap.get(element.id);
|
|
296
|
+
if (pos) {
|
|
297
|
+
if (pos.x !== undefined)
|
|
298
|
+
element.x = pos.x;
|
|
299
|
+
if (pos.y !== undefined)
|
|
300
|
+
element.y = pos.y;
|
|
301
|
+
if (pos.scale !== undefined &&
|
|
302
|
+
(element.type === "video" || element.type === "image" || element.type === "composition")) {
|
|
303
|
+
element.scale = pos.scale;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Normalize keyframes (clamp negative time, convert absolute -> relative if detected)
|
|
309
|
+
for (const element of elements) {
|
|
310
|
+
const elementKeyframes = keyframes[element.id];
|
|
311
|
+
if (!elementKeyframes || elementKeyframes.length === 0)
|
|
312
|
+
continue;
|
|
313
|
+
const baseX = element.x ?? 0;
|
|
314
|
+
const baseY = element.y ?? 0;
|
|
315
|
+
const baseScale = element.type === "video" || element.type === "image" || element.type === "composition"
|
|
316
|
+
? (element.scale ?? 1)
|
|
317
|
+
: 1;
|
|
318
|
+
keyframes[element.id] = normalizeKeyframes(elementKeyframes, baseX, baseY, baseScale);
|
|
319
|
+
}
|
|
320
|
+
const styleTags = doc.querySelectorAll("style");
|
|
321
|
+
const allStyles = Array.from(styleTags)
|
|
322
|
+
.map((s) => s.textContent?.trim())
|
|
323
|
+
.filter(Boolean)
|
|
324
|
+
.join("\n\n") || null;
|
|
325
|
+
const customStyleTags = Array.from(styleTags).filter((s) => s.getAttribute("data-hf-custom") === "true");
|
|
326
|
+
const customStylesFromTags = customStyleTags
|
|
327
|
+
.map((s) => s.textContent?.trim())
|
|
328
|
+
.filter(Boolean)
|
|
329
|
+
.join("\n\n") || null;
|
|
330
|
+
const styles = customStyles ?? customStylesFromTags ?? null;
|
|
331
|
+
const resolution = parseResolutionFromHtml(doc) ?? parseResolutionFromCss(doc, allStyles);
|
|
332
|
+
// Extract keyframes from GSAP animations for elements that don't have data-keyframes
|
|
333
|
+
if (gsapScript) {
|
|
334
|
+
const parsed = parseGsapScript(gsapScript);
|
|
335
|
+
for (const element of elements) {
|
|
336
|
+
// Only extract from GSAP if we don't have explicit data-keyframes
|
|
337
|
+
if (keyframes[element.id])
|
|
338
|
+
continue;
|
|
339
|
+
const elementAnimations = getAnimationsForElement(parsed.animations, element.id);
|
|
340
|
+
if (elementAnimations.length > 0) {
|
|
341
|
+
const elementKeyframes = gsapAnimationsToKeyframes(elementAnimations, element.startTime, {
|
|
342
|
+
baseX: element.x ?? 0,
|
|
343
|
+
baseY: element.y ?? 0,
|
|
344
|
+
baseScale: element.type === "video" || element.type === "image" || element.type === "composition"
|
|
345
|
+
? (element.scale ?? 1)
|
|
346
|
+
: 1,
|
|
347
|
+
clampTimeToZero: true,
|
|
348
|
+
skipBaseSet: true,
|
|
349
|
+
});
|
|
350
|
+
if (elementKeyframes.length > 0) {
|
|
351
|
+
keyframes[element.id] = elementKeyframes;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Parse stage zoom keyframes from zoom container
|
|
357
|
+
const stageZoomKeyframes = parseStageZoomKeyframes(doc);
|
|
358
|
+
return {
|
|
359
|
+
elements,
|
|
360
|
+
gsapScript,
|
|
361
|
+
styles,
|
|
362
|
+
resolution,
|
|
363
|
+
keyframes,
|
|
364
|
+
stageZoomKeyframes,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function parseStageZoomKeyframes(doc) {
|
|
368
|
+
const zoomContainer = doc.getElementById("stage-zoom-container");
|
|
369
|
+
if (!zoomContainer) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
const zoomKeyframesAttr = zoomContainer.getAttribute("data-zoom-keyframes");
|
|
373
|
+
if (!zoomKeyframesAttr) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const parsed = JSON.parse(zoomKeyframesAttr);
|
|
378
|
+
if (Array.isArray(parsed)) {
|
|
379
|
+
return parsed.filter((kf) => typeof kf === "object" &&
|
|
380
|
+
kf !== null &&
|
|
381
|
+
typeof kf.id === "string" &&
|
|
382
|
+
typeof kf.time === "number" &&
|
|
383
|
+
typeof kf.zoom === "object" &&
|
|
384
|
+
kf.zoom !== null &&
|
|
385
|
+
typeof kf.zoom.scale === "number" &&
|
|
386
|
+
typeof kf.zoom.focusX === "number" &&
|
|
387
|
+
typeof kf.zoom.focusY === "number");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// skip invalid zoom keyframes
|
|
392
|
+
}
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Extract x/y positions and scale from GSAP set() calls at position 0
|
|
397
|
+
* Returns a map of elementId -> { x, y, scale }
|
|
398
|
+
*/
|
|
399
|
+
function extractPositionsFromGsap(script) {
|
|
400
|
+
const positionMap = new Map();
|
|
401
|
+
try {
|
|
402
|
+
const parsed = parseGsapScript(script);
|
|
403
|
+
// Look for set() calls at position 0 with x/y/scale properties
|
|
404
|
+
for (const anim of parsed.animations) {
|
|
405
|
+
if (anim.method === "set" && anim.position === 0) {
|
|
406
|
+
// Extract element ID from selector (e.g., "#element-1" -> "element-1")
|
|
407
|
+
const selectorMatch = anim.targetSelector.match(/^#(.+)$/);
|
|
408
|
+
if (!selectorMatch)
|
|
409
|
+
continue;
|
|
410
|
+
const elementId = selectorMatch[1] ?? "";
|
|
411
|
+
const x = typeof anim.properties.x === "number" ? anim.properties.x : undefined;
|
|
412
|
+
const y = typeof anim.properties.y === "number" ? anim.properties.y : undefined;
|
|
413
|
+
const scale = typeof anim.properties.scale === "number" ? anim.properties.scale : undefined;
|
|
414
|
+
// Only add to map if x, y, or scale is defined and non-default
|
|
415
|
+
if ((x !== undefined && x !== 0) || (y !== undefined && y !== 0) || (scale !== undefined && scale !== 1)) {
|
|
416
|
+
const existing = positionMap.get(elementId) || {};
|
|
417
|
+
positionMap.set(elementId, {
|
|
418
|
+
x: x !== undefined ? x : existing.x,
|
|
419
|
+
y: y !== undefined ? y : existing.y,
|
|
420
|
+
scale: scale !== undefined ? scale : existing.scale,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// skip GSAP position parsing failure
|
|
428
|
+
}
|
|
429
|
+
return positionMap;
|
|
430
|
+
}
|
|
431
|
+
function normalizeKeyframes(keyframes, baseX, baseY, baseScale) {
|
|
432
|
+
const timeEpsilon = 0.001;
|
|
433
|
+
const valueEpsilon = 0.00001;
|
|
434
|
+
const hasBaseCheck = (value, base) => value !== undefined && Math.abs(value - base) <= valueEpsilon && Math.abs(base) > valueEpsilon;
|
|
435
|
+
const timeZeroKeyframes = keyframes.filter((kf) => Math.abs(kf.time) <= timeEpsilon);
|
|
436
|
+
const treatAsAbsolute = timeZeroKeyframes.some((kf) => {
|
|
437
|
+
const props = kf.properties || {};
|
|
438
|
+
if (hasBaseCheck(props.x, baseX) ||
|
|
439
|
+
hasBaseCheck(props.y, baseY) ||
|
|
440
|
+
(baseScale !== 1 && hasBaseCheck(props.scale, baseScale))) {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
});
|
|
445
|
+
return keyframes.map((kf) => {
|
|
446
|
+
const normalizedProps = {};
|
|
447
|
+
for (const [key, value] of Object.entries(kf.properties || {})) {
|
|
448
|
+
if (typeof value !== "number")
|
|
449
|
+
continue;
|
|
450
|
+
if (treatAsAbsolute && key === "x") {
|
|
451
|
+
normalizedProps.x = value - baseX;
|
|
452
|
+
}
|
|
453
|
+
else if (treatAsAbsolute && key === "y") {
|
|
454
|
+
normalizedProps.y = value - baseY;
|
|
455
|
+
}
|
|
456
|
+
else if (treatAsAbsolute && key === "scale") {
|
|
457
|
+
normalizedProps.scale = baseScale !== 0 ? value / baseScale : value;
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
normalizedProps[key] = value;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
...kf,
|
|
465
|
+
time: Math.max(0, kf.time),
|
|
466
|
+
properties: normalizedProps,
|
|
467
|
+
};
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
export function updateElementInHtml(html, elementId, updates) {
|
|
471
|
+
const parser = new DOMParser();
|
|
472
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
473
|
+
const el = doc.getElementById(elementId) || doc.querySelector(`[data-name="${elementId}"]`);
|
|
474
|
+
if (!el)
|
|
475
|
+
return html;
|
|
476
|
+
if (updates.startTime !== undefined) {
|
|
477
|
+
el.setAttribute("data-start", String(updates.startTime));
|
|
478
|
+
if (el.hasAttribute("data-end") && updates.duration !== undefined) {
|
|
479
|
+
el.setAttribute("data-end", String(updates.startTime + updates.duration));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (updates.duration !== undefined) {
|
|
483
|
+
const start = parseFloat(el.getAttribute("data-start") || "0");
|
|
484
|
+
el.setAttribute("data-end", String(start + updates.duration));
|
|
485
|
+
el.removeAttribute("data-duration"); // Clean up legacy
|
|
486
|
+
}
|
|
487
|
+
if (updates.name !== undefined) {
|
|
488
|
+
el.setAttribute("data-name", updates.name);
|
|
489
|
+
}
|
|
490
|
+
if (updates.zIndex !== undefined) {
|
|
491
|
+
el.setAttribute("data-layer", String(updates.zIndex));
|
|
492
|
+
}
|
|
493
|
+
// Handle media-specific property
|
|
494
|
+
if ("src" in updates && updates.src !== undefined) {
|
|
495
|
+
el.setAttribute("src", updates.src);
|
|
496
|
+
}
|
|
497
|
+
// Handle text-specific properties
|
|
498
|
+
if ("content" in updates && updates.content !== undefined) {
|
|
499
|
+
const textEl = el.firstElementChild;
|
|
500
|
+
if (textEl) {
|
|
501
|
+
textEl.textContent = updates.content;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if ("color" in updates && updates.color !== undefined) {
|
|
505
|
+
el.setAttribute("data-color", updates.color);
|
|
506
|
+
}
|
|
507
|
+
if ("fontSize" in updates && updates.fontSize !== undefined) {
|
|
508
|
+
el.setAttribute("data-font-size", String(updates.fontSize));
|
|
509
|
+
}
|
|
510
|
+
if ("textShadow" in updates) {
|
|
511
|
+
if (updates.textShadow === false) {
|
|
512
|
+
el.setAttribute("data-text-shadow", "false");
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
el.removeAttribute("data-text-shadow");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Handle volume property for audio/video
|
|
519
|
+
if ("volume" in updates) {
|
|
520
|
+
if (updates.volume !== undefined && updates.volume !== 1) {
|
|
521
|
+
el.setAttribute("data-volume", String(updates.volume));
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
el.removeAttribute("data-volume");
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// Handle hasAudio property for videos
|
|
528
|
+
if ("hasAudio" in updates) {
|
|
529
|
+
if (updates.hasAudio === true) {
|
|
530
|
+
el.setAttribute("data-has-audio", "true");
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
el.removeAttribute("data-has-audio");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return "<!DOCTYPE html>\n" + doc.documentElement.outerHTML;
|
|
537
|
+
}
|
|
538
|
+
export function addElementToHtml(html, element) {
|
|
539
|
+
const parser = new DOMParser();
|
|
540
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
541
|
+
// Prefer zoom container, fall back to stage, then container, then body
|
|
542
|
+
const container = doc.querySelector("#stage-zoom-container") ||
|
|
543
|
+
doc.querySelector(".container") ||
|
|
544
|
+
doc.querySelector("#stage") ||
|
|
545
|
+
doc.body;
|
|
546
|
+
const id = element.id || `element-${Date.now()}`;
|
|
547
|
+
let newEl;
|
|
548
|
+
switch (element.type) {
|
|
549
|
+
case "video": {
|
|
550
|
+
const mediaEl = element;
|
|
551
|
+
newEl = doc.createElement("video");
|
|
552
|
+
newEl.setAttribute("muted", "");
|
|
553
|
+
newEl.setAttribute("playsinline", "");
|
|
554
|
+
if (mediaEl.src)
|
|
555
|
+
newEl.setAttribute("src", mediaEl.src);
|
|
556
|
+
if (mediaEl.volume !== undefined && mediaEl.volume !== 1) {
|
|
557
|
+
newEl.setAttribute("data-volume", String(mediaEl.volume));
|
|
558
|
+
}
|
|
559
|
+
if (mediaEl.hasAudio) {
|
|
560
|
+
newEl.setAttribute("data-has-audio", "true");
|
|
561
|
+
}
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case "image": {
|
|
565
|
+
const mediaEl = element;
|
|
566
|
+
newEl = doc.createElement("img");
|
|
567
|
+
if (mediaEl.src)
|
|
568
|
+
newEl.setAttribute("src", mediaEl.src);
|
|
569
|
+
newEl.setAttribute("alt", element.name);
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case "audio": {
|
|
573
|
+
const mediaEl = element;
|
|
574
|
+
newEl = doc.createElement("audio");
|
|
575
|
+
if (mediaEl.src)
|
|
576
|
+
newEl.setAttribute("src", mediaEl.src);
|
|
577
|
+
if (mediaEl.volume !== undefined && mediaEl.volume !== 1) {
|
|
578
|
+
newEl.setAttribute("data-volume", String(mediaEl.volume));
|
|
579
|
+
}
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
case "text":
|
|
583
|
+
default: {
|
|
584
|
+
const textEl = element;
|
|
585
|
+
newEl = doc.createElement("div");
|
|
586
|
+
const textContent = doc.createElement("div");
|
|
587
|
+
textContent.textContent = textEl.content || element.name;
|
|
588
|
+
newEl.appendChild(textContent);
|
|
589
|
+
if (textEl.color) {
|
|
590
|
+
newEl.setAttribute("data-color", textEl.color);
|
|
591
|
+
}
|
|
592
|
+
if (textEl.fontSize) {
|
|
593
|
+
newEl.setAttribute("data-font-size", String(textEl.fontSize));
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
newEl.id = id;
|
|
599
|
+
newEl.setAttribute("data-start", String(element.startTime));
|
|
600
|
+
newEl.setAttribute("data-end", String(element.startTime + element.duration));
|
|
601
|
+
newEl.setAttribute("data-layer", String(element.zIndex));
|
|
602
|
+
newEl.setAttribute("data-name", element.name);
|
|
603
|
+
container.appendChild(newEl);
|
|
604
|
+
return {
|
|
605
|
+
html: "<!DOCTYPE html>\n" + doc.documentElement.outerHTML,
|
|
606
|
+
id,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
export function removeElementFromHtml(html, elementId) {
|
|
610
|
+
const parser = new DOMParser();
|
|
611
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
612
|
+
const el = doc.getElementById(elementId);
|
|
613
|
+
if (el) {
|
|
614
|
+
el.remove();
|
|
615
|
+
}
|
|
616
|
+
return "<!DOCTYPE html>\n" + doc.documentElement.outerHTML;
|
|
617
|
+
}
|
|
618
|
+
export function extractCompositionMetadata(html) {
|
|
619
|
+
const parser = new DOMParser();
|
|
620
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
621
|
+
const htmlEl = doc.documentElement;
|
|
622
|
+
const compositionId = htmlEl.getAttribute("data-composition-id");
|
|
623
|
+
const durationStr = htmlEl.getAttribute("data-composition-duration");
|
|
624
|
+
const compositionDuration = durationStr ? parseFloat(durationStr) : null;
|
|
625
|
+
const variables = parseCompositionVariables(htmlEl);
|
|
626
|
+
return {
|
|
627
|
+
compositionId,
|
|
628
|
+
compositionDuration: compositionDuration && isFinite(compositionDuration) ? compositionDuration : null,
|
|
629
|
+
variables,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function parseCompositionVariables(htmlEl) {
|
|
633
|
+
const variablesAttr = htmlEl.getAttribute("data-composition-variables");
|
|
634
|
+
if (!variablesAttr) {
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
const parsed = JSON.parse(variablesAttr);
|
|
639
|
+
if (!Array.isArray(parsed)) {
|
|
640
|
+
return [];
|
|
641
|
+
}
|
|
642
|
+
return parsed.filter((v) => {
|
|
643
|
+
if (typeof v !== "object" || v === null)
|
|
644
|
+
return false;
|
|
645
|
+
if (typeof v.id !== "string" || typeof v.label !== "string")
|
|
646
|
+
return false;
|
|
647
|
+
if (!["string", "number", "color", "boolean", "enum"].includes(v.type))
|
|
648
|
+
return false;
|
|
649
|
+
switch (v.type) {
|
|
650
|
+
case "string":
|
|
651
|
+
return typeof v.default === "string";
|
|
652
|
+
case "number":
|
|
653
|
+
return typeof v.default === "number";
|
|
654
|
+
case "color":
|
|
655
|
+
return typeof v.default === "string";
|
|
656
|
+
case "boolean":
|
|
657
|
+
return typeof v.default === "boolean";
|
|
658
|
+
case "enum":
|
|
659
|
+
return typeof v.default === "string" && Array.isArray(v.options);
|
|
660
|
+
default:
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
export function validateCompositionHtml(html) {
|
|
670
|
+
const errors = [];
|
|
671
|
+
const warnings = [];
|
|
672
|
+
const parser = new DOMParser();
|
|
673
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
674
|
+
const htmlEl = doc.documentElement;
|
|
675
|
+
const compositionId = htmlEl.getAttribute("data-composition-id");
|
|
676
|
+
if (!compositionId) {
|
|
677
|
+
errors.push("Missing data-composition-id attribute on <html> element");
|
|
678
|
+
}
|
|
679
|
+
const durationStr = htmlEl.getAttribute("data-composition-duration");
|
|
680
|
+
if (!durationStr) {
|
|
681
|
+
errors.push("Missing data-composition-duration attribute on <html> element");
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
const duration = parseFloat(durationStr);
|
|
685
|
+
if (!isFinite(duration) || duration <= 0) {
|
|
686
|
+
errors.push("data-composition-duration must be a positive finite number");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const stage = doc.getElementById("stage");
|
|
690
|
+
if (!stage) {
|
|
691
|
+
errors.push("Missing #stage element");
|
|
692
|
+
}
|
|
693
|
+
if (/\son\w+\s*=/i.test(html)) {
|
|
694
|
+
errors.push("Inline event handlers (onclick, onload, etc.) not allowed");
|
|
695
|
+
}
|
|
696
|
+
if (/javascript\s*:/i.test(html)) {
|
|
697
|
+
errors.push("javascript: URLs not allowed");
|
|
698
|
+
}
|
|
699
|
+
const scripts = doc.querySelectorAll("script");
|
|
700
|
+
if (scripts.length > 2) {
|
|
701
|
+
warnings.push("Multiple script tags detected - only GSAP CDN and main script expected");
|
|
702
|
+
}
|
|
703
|
+
const gsapScript = extractGsapScript(doc);
|
|
704
|
+
if (gsapScript) {
|
|
705
|
+
const gsapValidation = validateCompositionGsap(gsapScript);
|
|
706
|
+
errors.push(...gsapValidation.errors);
|
|
707
|
+
warnings.push(...gsapValidation.warnings);
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
valid: errors.length === 0,
|
|
711
|
+
errors,
|
|
712
|
+
warnings,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function extractGsapScript(doc) {
|
|
716
|
+
const scripts = doc.querySelectorAll("script");
|
|
717
|
+
for (const script of scripts) {
|
|
718
|
+
const content = script.textContent || "";
|
|
719
|
+
if (content.includes("gsap.timeline") || content.includes(".set(") || content.includes(".to(")) {
|
|
720
|
+
return content;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
export { CANVAS_DIMENSIONS };
|
|
726
|
+
//# sourceMappingURL=htmlParser.js.map
|