Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/studio/src/components/editor/cropResize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ describe("crop during a resize", () => {
Object.assign(window, { __timelines: undefined });
});

it("leaves a crop alone when GSAP tweens it by object-of-arrays keyframes", () => {
const tweened = sizedElement(300, 200, "inset(0px 60px 0px 0px)");
const kf = { clipPath: ["inset(0px 60px 0px 0px)", "inset(0px 120px 0px 0px)"] };
const child = { targets: () => [tweened], vars: { keyframes: kf } };
Object.assign(window, { __timelines: { main: { getChildren: () => [child] } } });
const stage = prepareCropResize(tweened);
tweened.style.width = "450px";
expect(stage()).toBeNull();
Object.assign(window, { __timelines: undefined });
});

it("decides before the write, which may add a width tween of its own", () => {
const el = sizedElement(300, 200, "inset(0px 60px 0px 0px)");
const stage = prepareCropResize(el);
Expand Down
5 changes: 2 additions & 3 deletions packages/studio/src/components/editor/cropResize.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { PatchOperation } from "../../utils/sourcePatcher";
import type { CommitMutation } from "../../hooks/gsapScriptCommitTypes";
import type { DomEditSelection } from "./domEditingTypes";
import { gsapAnimatesProperty } from "./gsapAnimatesProperty";
import { elementHasNonHoldTween } from "../../hooks/gsapRuntimeKeyframes";
import { elementHasNonHoldTween, gsapWritesChannels } from "../../hooks/gsapRuntimeKeyframes";
import { buildInsetClipPathSides, type ParsedInsetClipPathSides } from "./clipPathHelpers";
import { hasCropInsets, readElementCropInsets } from "./domEditOverlayCrop";
import { forgetStudioBoxSizeDraftBase, readStudioBoxSizeDraftBase } from "./manualEditsDom";
Expand All @@ -21,7 +20,7 @@ type Follows = Record<keyof Box, boolean>;

/** Per axis, whether a crop follows a resize: not when GSAP drives the clip or tweens that axis's size. */
function cropFollows(element: HTMLElement): Follows {
const clip = gsapAnimatesProperty(element, "clipPath");
const clip = gsapWritesChannels(element, ["clipPath"]);
return {
width: !clip && !elementHasNonHoldTween(element, ["width"]),
height: !clip && !elementHasNonHoldTween(element, ["height"]),
Expand Down
54 changes: 0 additions & 54 deletions packages/studio/src/components/editor/gsapAnimatesProperty.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,2 @@
export const gsapRendersTransform = (el: Element) =>
!!(el as { _gsap?: { renderTransform?: unknown } })._gsap?.renderTransform;

/**
* Checks whether GSAP actively animates one or more CSS/GSAP properties on
* the given element by inspecting all registered `__timelines`.
*/
// fallow-ignore-next-line complexity
export function gsapAnimatesProperty(el: HTMLElement, ...props: string[]): boolean {
const win = el.ownerDocument.defaultView as
| (Window & {
__timelines?: Record<
string,
{
getChildren?: (
deep: boolean,
) => Array<{ targets?: () => Element[]; vars?: Record<string, unknown> }>;
}
>;
})
| null;
if (!win?.__timelines) return false;
const propSet = new Set(props);
for (const tl of Object.values(win.__timelines)) {
if (!tl?.getChildren) continue;
try {
for (const child of tl.getChildren(true)) {
if (!child.targets || !child.vars) continue;
let targetsEl = false;
for (const t of child.targets()) {
if (t === el || (el.id && t.id === el.id)) {
targetsEl = true;
break;
}
}
if (!targetsEl) continue;
const vars = child.vars;
for (const p of propSet) {
if (p in vars) return true;
}
if (vars.keyframes && typeof vars.keyframes === "object") {
for (const kfVal of Object.values(vars.keyframes as Record<string, unknown>)) {
if (kfVal && typeof kfVal === "object") {
for (const p of propSet) {
if (p in (kfVal as Record<string, unknown>)) return true;
}
}
}
}
}
} catch {
/* */
}
}
return false;
}
4 changes: 2 additions & 2 deletions packages/studio/src/components/editor/manualEditsDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
STUDIO_ROTATION_TRANSFORM_ORIGIN,
} from "./manualEditsTypes";
import { gsapAnimatesProperty } from "./gsapAnimatesProperty";
import { gsapWritesChannels } from "../../hooks/gsapRuntimeKeyframes";
import { splitTopLevelWhitespace } from "./manualEditsStyleHelpers";
import { roundTo3, roundToLayoutPx } from "../../utils/rounding";
import { BOX_SIZE_STYLE_PROPS } from "./manualEditsDomPatches";
Expand Down Expand Up @@ -317,7 +317,7 @@ function applyStudioPathOffsetViaGsap(
element: HTMLElement,
offset: { x: number; y: number },
): boolean {
if (!gsapAnimatesProperty(element, "x", "y")) return false;
if (!gsapWritesChannels(element, ["x", "y"])) return false;
element.style.setProperty("translate", "none");
const win = element.ownerDocument.defaultView as
| (Window & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { applyStudioBoxSize, applyStudioPathOffset, applyStudioRotation } from "./manualEditsDom";
import { applyStudioMotionFromDom } from "./studioMotion";
import { STUDIO_MOTION_ATTR, STUDIO_MOTION_TIMELINE_ID } from "./studioMotionTypes";
import { gsapAnimatesProperty } from "./gsapAnimatesProperty";
import { gsapWritesChannels } from "../../hooks/gsapRuntimeKeyframes";

function queryStudioElements(doc: Document, attr: string): HTMLElement[] {
const ctor = doc.defaultView?.HTMLElement;
Expand All @@ -35,7 +35,7 @@ function queryStudioElements(doc: Document, attr: string): HTMLElement[] {
function reapplyPathOffsets(doc: Document): void {
for (const el of queryStudioElements(doc, STUDIO_PATH_OFFSET_ATTR)) {
// Unlike size below, the offset channels add up: applying both doubles the move.
if (gsapAnimatesProperty(el, "x", "y")) continue;
if (gsapWritesChannels(el, ["x", "y"])) continue;
const x = el.style.getPropertyValue(STUDIO_OFFSET_X_PROP);
const y = el.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
if (!x && !y) continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { reapplyPositionEditsAfterSeek } from "./manualEditsSeekReapply";
import {
STUDIO_OFFSET_X_PROP,
STUDIO_OFFSET_Y_PROP,
STUDIO_PATH_OFFSET_ATTR,
STUDIO_ROTATION_ATTR,
STUDIO_ROTATION_PROP,
Expand All @@ -14,6 +16,29 @@ describe("reapplyPositionEditsAfterSeek", () => {
vi.restoreAllMocks();
});

it("keeps an old offset off an element GSAP moves by object-of-arrays keyframes, not off its neighbour", () => {
const offset = `${STUDIO_OFFSET_X_PROP}: 300px; ${STUDIO_OFFSET_Y_PROP}: 100px; translate: none`;
document.body.innerHTML = ["moved", "still"]
.map((id) => `<div id="${id}" ${STUDIO_PATH_OFFSET_ATTR}="true" style="${offset}"></div>`)
.join("");
const moved = document.getElementById("moved") as HTMLElement;
const tween = { targets: () => [moved], vars: { keyframes: { x: [0, 200] } } };
const win = window as unknown as { __timelines?: unknown; gsap?: unknown };
const gsap = { set: vi.fn(), getProperty: () => 0 };
Object.assign(win, { __timelines: { main: { getChildren: () => [tween] } }, gsap });
try {
reapplyPositionEditsAfterSeek(document);
} finally {
delete win.__timelines;
delete win.gsap;
}

expect(moved.style.getPropertyValue("translate")).toBe("none");
expect(gsap.set).not.toHaveBeenCalled();
const still = document.getElementById("still") as HTMLElement;
expect(still.style.getPropertyValue("translate")).toContain(STUDIO_OFFSET_X_PROP);
});

it("does no per-edit work on a film Studio never edited", () => {
document.body.innerHTML = '<div id="a"></div><div id="b"></div>';
const queryAll = vi.spyOn(document, "querySelectorAll");
Expand Down
2 changes: 1 addition & 1 deletion packages/studio/src/hooks/gsapRuntimeKeyframes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ export const GSAP_TRANSFORM_KEYS = new Set(
);

/** Whether a live timeline tween or hold writes any of `channels` on `el`. Sync, no fetch. */
function gsapWritesChannels(el: Element, channels: string[]): boolean {
export function gsapWritesChannels(el: Element, channels: string[]): boolean {
const win = el.ownerDocument.defaultView as { __timelines?: Record<string, RuntimeTimeline> };
return Object.values(win?.__timelines ?? {}).some((tl) =>
(tl?.getChildren?.(true) ?? []).some(
Expand Down
Loading