Files
hyperframes/packages/studio/src/components/editor/manualEditsDom.ts
T
Miguel Ángel 037266e72b feat(studio): timeline revamp with active-clip highlighting and hide controls (#2017)
Timeline UI
- Highlight clips visible at the playhead in the primary color; others share one neutral color
- Minimalist rounded clips, single-color track rows, no gutter icons or superscript labels
- Per-track eye toggle and a per-element hide button in the design panel
- Ruler zoom fixes: sub-second tick intervals and correct label formatting at high zoom
- Sticky gutter so track controls stay visible while scrolling

WYSIWYG visibility (data-hidden)
- Runtime honors data-hidden (display:none), so hiding affects the render, not just the preview
- HTML stays the source of truth; hide state persists and round-trips on reload

Split several studio files to stay under the 600-line cap; pure relocations, no behavior change.
2026-07-07 04:26:56 -04:00

584 lines
24 KiB
TypeScript

import {
STUDIO_OFFSET_X_PROP,
STUDIO_OFFSET_Y_PROP,
STUDIO_WIDTH_PROP,
STUDIO_HEIGHT_PROP,
STUDIO_ROTATION_PROP,
STUDIO_PATH_OFFSET_ATTR,
STUDIO_MANUAL_EDIT_GESTURE_ATTR,
STUDIO_BOX_SIZE_ATTR,
STUDIO_ROTATION_ATTR,
STUDIO_ORIGINAL_TRANSLATE_ATTR,
STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR,
STUDIO_ORIGINAL_WIDTH_ATTR,
STUDIO_ORIGINAL_HEIGHT_ATTR,
STUDIO_ORIGINAL_MIN_WIDTH_ATTR,
STUDIO_ORIGINAL_MIN_HEIGHT_ATTR,
STUDIO_ORIGINAL_MAX_WIDTH_ATTR,
STUDIO_ORIGINAL_MAX_HEIGHT_ATTR,
STUDIO_ORIGINAL_FLEX_BASIS_ATTR,
STUDIO_ORIGINAL_FLEX_GROW_ATTR,
STUDIO_ORIGINAL_FLEX_SHRINK_ATTR,
STUDIO_ORIGINAL_BOX_SIZING_ATTR,
STUDIO_ORIGINAL_SCALE_ATTR,
STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR,
STUDIO_ORIGINAL_DISPLAY_ATTR,
STUDIO_ORIGINAL_ROTATE_ATTR,
STUDIO_ORIGINAL_INLINE_ROTATE_ATTR,
STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
STUDIO_ROTATION_DRAFT_ATTR,
STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
STUDIO_ROTATION_TRANSFORM_ORIGIN,
} from "./manualEditsTypes";
import { roundRotationAngle } from "./manualEditsParsing";
import { applyStudioMotionFromDom } from "./studioMotion";
import { gsapAnimatesProperty } from "./gsapAnimatesProperty";
import { splitTopLevelWhitespace } from "./manualEditsStyleHelpers";
/* ── Gesture tracking ─────────────────────────────────────────────── */
let studioManualEditGestureId = 0;
export function beginStudioManualEditGesture(element: HTMLElement): string {
studioManualEditGestureId += 1;
const token = `gesture-${studioManualEditGestureId}`;
element.setAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR, token);
return token;
}
export function endStudioManualEditGesture(element: HTMLElement, token?: string): void {
if (token && element.getAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR) !== token) return;
element.removeAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
}
function isStudioManualEditGestureActive(element: HTMLElement): boolean {
return element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
}
export function isStudioManualEditGestureCurrent(element: HTMLElement, token: string): boolean {
return element.getAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR) === token;
}
/* ── CSS custom-property readers ──────────────────────────────────── */
function readPxCustomProperty(element: HTMLElement, property: string): number {
const value = Number.parseFloat(element.style.getPropertyValue(property));
return Number.isFinite(value) ? value : 0;
}
export function readStudioPathOffset(element: HTMLElement): { x: number; y: number } {
return {
x: readPxCustomProperty(element, STUDIO_OFFSET_X_PROP),
y: readPxCustomProperty(element, STUDIO_OFFSET_Y_PROP),
};
}
/**
* The path offset ACTUALLY applied right now. The `--hf-studio-offset` vars can
* linger after GSAP re-bakes the element's transform (`translate:"none"`), so the
* raw var isn't a safe drag base — using it re-commits a phantom offset and flings
* the element off-screen. The offset only counts when the inline `translate` is the
* studio var-translate; otherwise it's dormant and the applied offset is zero.
*/
export function readAppliedStudioPathOffset(element: HTMLElement): { x: number; y: number } {
return (element.style.translate || "").includes(STUDIO_OFFSET_X_PROP)
? readStudioPathOffset(element)
: { x: 0, y: 0 };
}
export function readStudioBoxSize(element: HTMLElement): { width: number; height: number } {
return {
width: readPxCustomProperty(element, STUDIO_WIDTH_PROP),
height: readPxCustomProperty(element, STUDIO_HEIGHT_PROP),
};
}
export function readStudioRotation(element: HTMLElement): { angle: number } {
const value = Number.parseFloat(element.style.getPropertyValue(STUDIO_ROTATION_PROP));
return { angle: Number.isFinite(value) ? value : 0 };
}
/* ── Internal style helpers ───────────────────────────────────────── */
function safeComputedStyleProperty(element: HTMLElement, property: string): string {
try {
return (
element.ownerDocument.defaultView?.getComputedStyle(element).getPropertyValue(property) ?? ""
);
} catch {
return "";
}
}
function readStyleOrComputed(element: HTMLElement, property: string): string {
return element.style.getPropertyValue(property) || safeComputedStyleProperty(element, property);
}
function readTransformLonghandBase(element: HTMLElement, property: "translate" | "rotate"): string {
const value = readStyleOrComputed(element, property).trim();
return value === "none" ? "" : value;
}
export function styleUsesStudioOffset(value: string): boolean {
return value.includes(STUDIO_OFFSET_X_PROP) || value.includes(STUDIO_OFFSET_Y_PROP);
}
export function styleUsesStudioSize(value: string): boolean {
return value.includes(STUDIO_WIDTH_PROP) || value.includes(STUDIO_HEIGHT_PROP);
}
export function styleUsesStudioRotation(value: string): boolean {
return value.includes(STUDIO_ROTATION_PROP);
}
function compactStyleValue(value: string): string {
return value.replace(/\s+/g, "").toLowerCase();
}
function styleMatchesStudioRotationDraft(element: HTMLElement, value: string): boolean {
if (!element.hasAttribute(STUDIO_ROTATION_DRAFT_ATTR)) return false;
const rotation = element.style.getPropertyValue(STUDIO_ROTATION_PROP).trim();
if (!rotation || !value.trim()) return false;
return (
compactStyleValue(value) === compactStyleValue(composeStudioRotationValue(element, rotation))
);
}
/* ── Inline promotion ─────────────────────────────────────────────── */
function promoteInlineForTransform(element: HTMLElement): void {
const computedDisplay = safeComputedStyleProperty(element, "display");
if (computedDisplay !== "inline") return;
if (!element.hasAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR)) {
element.setAttribute(
STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
element.style.getPropertyValue("display"),
);
}
element.style.setProperty("display", "inline-block");
}
export function restoreInlineDisplay(element: HTMLElement): void {
const original = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
if (original == null) return;
if (original === "") element.style.removeProperty("display");
else element.style.setProperty("display", original);
element.removeAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
}
/* ── Translate helpers ────────────────────────────────────────────── */
function composeTranslateValue(element: HTMLElement, x: string, y: string): string {
const original = element.getAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR)?.trim();
if (!original || original === "none") return `${x} ${y}`;
const parts = splitTopLevelWhitespace(original);
if (parts.length === 1) return `calc(${parts[0]} + ${x}) ${y}`;
if (parts.length === 2) return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y})`;
if (parts.length === 3) {
return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y}) ${parts[2]}`;
}
return `${x} ${y}`;
}
function prepareStudioPathOffsetBase(element: HTMLElement, updateBase: boolean): void {
const inlineTranslate = element.style.getPropertyValue("translate");
const currentTranslate = readTransformLonghandBase(element, "translate");
const hasMarker = element.hasAttribute(STUDIO_PATH_OFFSET_ATTR);
const wasResetByAnimation = !styleUsesStudioOffset(currentTranslate);
if (!hasMarker) {
element.setAttribute(
STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR,
styleUsesStudioOffset(inlineTranslate) ? "" : inlineTranslate,
);
element.setAttribute(
STUDIO_ORIGINAL_TRANSLATE_ATTR,
wasResetByAnimation ? currentTranslate : "",
);
} else if (updateBase && wasResetByAnimation && !isStudioManualEditGestureActive(element)) {
element.setAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR, currentTranslate);
}
}
function writeStudioPathOffsetVars(
element: HTMLElement,
offset: { x: number; y: number },
options: { updateBase?: boolean } = {},
): void {
prepareStudioPathOffsetBase(element, options.updateBase ?? true);
element.setAttribute(STUDIO_PATH_OFFSET_ATTR, "true");
element.style.setProperty(STUDIO_OFFSET_X_PROP, `${Math.round(offset.x)}px`);
element.style.setProperty(STUDIO_OFFSET_Y_PROP, `${Math.round(offset.y)}px`);
}
/* ── Path offset apply ────────────────────────────────────────────── */
// GSAP 3.x reads the resolved CSS `translate` individual property at initialization and bakes it
// into element.style.transform (as a matrix) on every seek. When the studio's reapply hook also
// writes `translate`, both properties compose additively, doubling the visual offset.
//
// This helper subtracts only the baked studio offset from m41/m42, preserving any GSAP animation
// contribution (e.g. a tween animating y: -20). The studio offset is read from the CSS custom
// properties which tell us exactly how much was baked from the CSS translate.
function isIdentityAfterTranslateStrip(m: DOMMatrix): boolean {
return m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1;
}
function stripGsapTranslateFromTransform(element: HTMLElement): void {
if (element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR)) return;
const transform = element.style.getPropertyValue("transform");
if (!transform || transform === "none") return;
const DOMMatrixCtor = (element.ownerDocument.defaultView as (Window & typeof globalThis) | null)
?.DOMMatrix;
if (!DOMMatrixCtor) return;
try {
const m = new DOMMatrixCtor(transform);
if (m.m41 === 0 && m.m42 === 0) return;
const offsetX = readPxCustomProperty(element, STUDIO_OFFSET_X_PROP);
const offsetY = readPxCustomProperty(element, STUDIO_OFFSET_Y_PROP);
const angle = Math.atan2(m.b, m.a);
const cos = Math.cos(angle);
const sin = Math.sin(angle);
m.m41 -= offsetX * cos - offsetY * sin;
m.m42 -= offsetX * sin + offsetY * cos;
if (Math.abs(m.m41) < 0.01 && Math.abs(m.m42) < 0.01 && isIdentityAfterTranslateStrip(m)) {
element.style.removeProperty("transform");
} else {
element.style.setProperty("transform", m.toString());
}
} catch {
/* non-parseable transform — leave as-is */
}
}
// GSAP owns the element's `transform` (it bakes x/y into a matrix and writes
// `translate: none` every tick). Folding the drag offset into a CSS `translate`
// — as the non-GSAP path does — composes ON TOP of GSAP's transform, and the
// subsequent strip/reapply math compounds into a runaway matrix that flings the
// element off-canvas. So for GSAP-animated elements we keep `translate: none`
// and push the offset straight into GSAP's x/y via gsap.set; the var() offset is
// still persisted (buildPathOffsetPatches), and GSAP re-reads it at init on
// reload. Returns true when handled as GSAP (caller must skip the CSS path).
function applyStudioPathOffsetViaGsap(
element: HTMLElement,
offset: { x: number; y: number },
): boolean {
if (!gsapAnimatesProperty(element, "x", "y")) return false;
element.style.setProperty("translate", "none");
const win = element.ownerDocument.defaultView as
| (Window & {
gsap?: {
set: (el: Element, vars: Record<string, unknown>) => void;
getProperty: (el: Element, prop: string) => number;
};
})
| null;
if (win?.gsap) {
const baseX = Number.parseFloat(element.getAttribute("data-hf-drag-gsap-base-x") ?? "");
const baseY = Number.parseFloat(element.getAttribute("data-hf-drag-gsap-base-y") ?? "");
const origX = Number.parseFloat(element.getAttribute("data-hf-drag-initial-offset-x") ?? "");
const origY = Number.parseFloat(element.getAttribute("data-hf-drag-initial-offset-y") ?? "");
const gsapBaseX = Number.isFinite(baseX)
? baseX
: (win.gsap.getProperty(element, "x") as number);
const gsapBaseY = Number.isFinite(baseY)
? baseY
: (win.gsap.getProperty(element, "y") as number);
if (!Number.isFinite(baseX))
element.setAttribute("data-hf-drag-gsap-base-x", String(gsapBaseX));
if (!Number.isFinite(baseY))
element.setAttribute("data-hf-drag-gsap-base-y", String(gsapBaseY));
const deltaX = offset.x - (Number.isFinite(origX) ? origX : 0);
const deltaY = offset.y - (Number.isFinite(origY) ? origY : 0);
win.gsap.set(element, { x: gsapBaseX + deltaX, y: gsapBaseY + deltaY });
}
return true;
}
export function applyStudioPathOffset(
element: HTMLElement,
offset: { x: number; y: number },
options: { updateBase?: boolean } = {},
): void {
promoteInlineForTransform(element);
writeStudioPathOffsetVars(element, offset, { updateBase: options.updateBase ?? true });
// GSAP elements: route through gsap.set, NOT a CSS translate (would corrupt the
// matrix). Symmetrical with applyStudioPathOffsetDraft — the commit path used to
// skip this branch, which is what flung dragged GSAP elements off-canvas.
if (applyStudioPathOffsetViaGsap(element, offset)) return;
element.style.setProperty(
"translate",
composeTranslateValue(
element,
`var(${STUDIO_OFFSET_X_PROP}, 0px)`,
`var(${STUDIO_OFFSET_Y_PROP}, 0px)`,
),
);
stripGsapTranslateFromTransform(element);
}
export function applyStudioPathOffsetDraft(
element: HTMLElement,
offset: { x: number; y: number },
): void {
promoteInlineForTransform(element);
writeStudioPathOffsetVars(element, offset, { updateBase: false });
if (applyStudioPathOffsetViaGsap(element, offset)) return;
// Non-GSAP elements: use CSS translate as before.
element.style.setProperty(
"translate",
composeTranslateValue(element, `${Math.round(offset.x)}px`, `${Math.round(offset.y)}px`),
);
stripGsapTranslateFromTransform(element);
}
/* ── Box size apply ───────────────────────────────────────────────── */
function readParentFlexBasisPixels(
element: HTMLElement,
size: { width: number; height: number },
): number | null {
const parent = element.parentElement;
if (!parent) return null;
const display = readStyleOrComputed(parent, "display").trim();
if (display !== "flex" && display !== "inline-flex") return null;
const direction = readStyleOrComputed(parent, "flex-direction").trim();
return Math.round(Math.max(1, direction.startsWith("column") ? size.height : size.width));
}
function restoreStaleStudioScaleResize(element: HTMLElement): void {
if (!element.hasAttribute(STUDIO_ORIGINAL_SCALE_ATTR)) return;
const origScale = element.getAttribute(STUDIO_ORIGINAL_SCALE_ATTR);
if (origScale == null || origScale === "") element.style.removeProperty("scale");
else element.style.setProperty("scale", origScale);
element.removeAttribute(STUDIO_ORIGINAL_SCALE_ATTR);
const origOrigin = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR);
if (origOrigin == null || origOrigin === "") element.style.removeProperty("transform-origin");
else element.style.setProperty("transform-origin", origOrigin);
element.removeAttribute(STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR);
}
function writeStudioBoxSizeVars(
element: HTMLElement,
size: { width: number; height: number },
): void {
if (!element.hasAttribute(STUDIO_BOX_SIZE_ATTR)) {
element.setAttribute(STUDIO_ORIGINAL_WIDTH_ATTR, element.style.getPropertyValue("width"));
element.setAttribute(STUDIO_ORIGINAL_HEIGHT_ATTR, element.style.getPropertyValue("height"));
element.setAttribute(
STUDIO_ORIGINAL_MIN_WIDTH_ATTR,
element.style.getPropertyValue("min-width"),
);
element.setAttribute(
STUDIO_ORIGINAL_MIN_HEIGHT_ATTR,
element.style.getPropertyValue("min-height"),
);
element.setAttribute(
STUDIO_ORIGINAL_MAX_WIDTH_ATTR,
element.style.getPropertyValue("max-width"),
);
element.setAttribute(
STUDIO_ORIGINAL_MAX_HEIGHT_ATTR,
element.style.getPropertyValue("max-height"),
);
element.setAttribute(
STUDIO_ORIGINAL_FLEX_BASIS_ATTR,
element.style.getPropertyValue("flex-basis"),
);
element.setAttribute(
STUDIO_ORIGINAL_FLEX_GROW_ATTR,
element.style.getPropertyValue("flex-grow"),
);
element.setAttribute(
STUDIO_ORIGINAL_FLEX_SHRINK_ATTR,
element.style.getPropertyValue("flex-shrink"),
);
element.setAttribute(
STUDIO_ORIGINAL_BOX_SIZING_ATTR,
element.style.getPropertyValue("box-sizing"),
);
element.setAttribute(STUDIO_ORIGINAL_SCALE_ATTR, element.style.getPropertyValue("scale"));
element.setAttribute(
STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR,
element.style.getPropertyValue("transform-origin"),
);
element.setAttribute(STUDIO_ORIGINAL_DISPLAY_ATTR, element.style.getPropertyValue("display"));
}
element.setAttribute(STUDIO_BOX_SIZE_ATTR, "true");
element.style.setProperty(STUDIO_WIDTH_PROP, `${Math.round(Math.max(1, size.width))}px`);
element.style.setProperty(STUDIO_HEIGHT_PROP, `${Math.round(Math.max(1, size.height))}px`);
}
function applyStudioBoxSizeDimensions(
element: HTMLElement,
size: { width: number; height: number },
): void {
writeStudioBoxSizeVars(element, size);
restoreStaleStudioScaleResize(element);
const width = Math.round(Math.max(1, size.width));
const height = Math.round(Math.max(1, size.height));
element.style.setProperty("box-sizing", "border-box");
element.style.setProperty("width", `${width}px`);
element.style.setProperty("height", `${height}px`);
element.style.setProperty("min-width", "0px");
element.style.setProperty("min-height", "0px");
element.style.setProperty("max-width", "none");
element.style.setProperty("max-height", "none");
const flexBasis = readParentFlexBasisPixels(element, size);
if (flexBasis != null) {
element.style.setProperty("flex-basis", `${flexBasis}px`);
element.style.setProperty("flex-grow", "0");
element.style.setProperty("flex-shrink", "0");
}
const computedDisplay = safeComputedStyleProperty(element, "display");
if (computedDisplay === "inline") {
element.style.setProperty("display", "inline-block");
}
}
export function applyStudioBoxSize(
element: HTMLElement,
size: { width: number; height: number },
): void {
promoteInlineForTransform(element);
applyStudioBoxSizeDimensions(element, size);
}
export function applyStudioBoxSizeDraft(
element: HTMLElement,
size: { width: number; height: number },
): void {
promoteInlineForTransform(element);
applyStudioBoxSizeDimensions(element, size);
}
/* ── Rotation apply ───────────────────────────────────────────────── */
function isSimpleRotateAngle(value: string): boolean {
return /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
}
function composeStudioRotationValue(element: HTMLElement, rotationValue: string): string {
const original = element.getAttribute(STUDIO_ORIGINAL_ROTATE_ATTR)?.trim();
if (!original || original === "none" || !isSimpleRotateAngle(original)) {
return rotationValue;
}
return `calc(${original} + ${rotationValue})`;
}
function prepareStudioRotationBase(element: HTMLElement, updateBase: boolean): void {
const inlineRotate = element.style.getPropertyValue("rotate");
const currentRotate = readTransformLonghandBase(element, "rotate");
const hasMarker = element.hasAttribute(STUDIO_ROTATION_ATTR);
const wasResetByAnimation =
!styleUsesStudioRotation(currentRotate) &&
!styleMatchesStudioRotationDraft(element, currentRotate);
if (!hasMarker) {
element.setAttribute(
STUDIO_ORIGINAL_INLINE_ROTATE_ATTR,
styleUsesStudioRotation(inlineRotate) ? "" : inlineRotate,
);
element.setAttribute(STUDIO_ORIGINAL_ROTATE_ATTR, wasResetByAnimation ? currentRotate : "");
} else if (updateBase && wasResetByAnimation && !isStudioManualEditGestureActive(element)) {
element.setAttribute(STUDIO_ORIGINAL_ROTATE_ATTR, currentRotate);
}
if (!element.hasAttribute(STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR)) {
element.setAttribute(
STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
element.style.getPropertyValue("transform-origin"),
);
}
}
function writeStudioRotationVars(
element: HTMLElement,
rotation: { angle: number },
options: { updateBase?: boolean } = {},
): void {
prepareStudioRotationBase(element, options.updateBase ?? true);
element.setAttribute(STUDIO_ROTATION_ATTR, "true");
element.style.setProperty(STUDIO_ROTATION_PROP, `${roundRotationAngle(rotation.angle)}deg`);
element.style.setProperty("transform-origin", STUDIO_ROTATION_TRANSFORM_ORIGIN);
}
export function applyStudioRotation(element: HTMLElement, rotation: { angle: number }): void {
promoteInlineForTransform(element);
writeStudioRotationVars(element, rotation);
element.removeAttribute(STUDIO_ROTATION_DRAFT_ATTR);
element.style.setProperty(
"rotate",
composeStudioRotationValue(element, `var(${STUDIO_ROTATION_PROP}, 0deg)`),
);
}
export function applyStudioRotationDraft(element: HTMLElement, rotation: { angle: number }): void {
promoteInlineForTransform(element);
writeStudioRotationVars(element, rotation, { updateBase: false });
element.setAttribute(STUDIO_ROTATION_DRAFT_ATTR, "true");
element.style.setProperty(
"rotate",
composeStudioRotationValue(element, `${roundRotationAngle(rotation.angle)}deg`),
);
}
/* ── Seek reapply (position + motion) ────────────────────────────── */
function queryStudioElements(doc: Document, attr: string): HTMLElement[] {
const ctor = doc.defaultView?.HTMLElement;
if (!ctor) return [];
const elements = Array.from(doc.querySelectorAll(`[${attr}="true"]`)).filter(
(el): el is HTMLElement => el instanceof ctor,
);
// Handle legacy HTML files where attributes were persisted with a double data- prefix
const legacyAttr = `data-${attr}`;
for (const el of doc.querySelectorAll(`[${legacyAttr}="true"]`)) {
if (el instanceof ctor && !el.hasAttribute(attr)) {
el.setAttribute(attr, "true");
el.removeAttribute(legacyAttr);
elements.push(el);
}
}
return elements;
}
function reapplyPathOffsets(doc: Document): void {
for (const el of queryStudioElements(doc, STUDIO_PATH_OFFSET_ATTR)) {
const gsapSkip = gsapAnimatesProperty(el, "x", "y");
const x = el.style.getPropertyValue(STUDIO_OFFSET_X_PROP);
const y = el.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
if (gsapSkip) continue;
if (x || y) {
applyStudioPathOffset(
el,
{
x: Number.parseFloat(x) || 0,
y: Number.parseFloat(y) || 0,
},
{ updateBase: false },
);
}
}
}
function reapplyBoxSizes(doc: Document): void {
for (const el of queryStudioElements(doc, STUDIO_BOX_SIZE_ATTR)) {
if (gsapAnimatesProperty(el, "width", "height")) continue;
const w = Number.parseFloat(el.style.getPropertyValue(STUDIO_WIDTH_PROP));
const h = Number.parseFloat(el.style.getPropertyValue(STUDIO_HEIGHT_PROP));
if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
applyStudioBoxSize(el, { width: w, height: h });
}
}
}
function reapplyRotations(doc: Document): void {
for (const el of queryStudioElements(doc, STUDIO_ROTATION_ATTR)) {
const angle = Number.parseFloat(el.style.getPropertyValue(STUDIO_ROTATION_PROP));
if (Number.isFinite(angle)) {
applyStudioRotation(el, { angle });
}
}
}
export function reapplyPositionEditsAfterSeek(doc: Document): void {
reapplyPathOffsets(doc);
reapplyBoxSizes(doc);
reapplyRotations(doc);
applyStudioMotionFromDom(doc);
}