diff --git a/src/core/animations/effect-preset-builder.ts b/src/core/animations/effect-preset-builder.ts index cf45ebb7..ff367d85 100644 --- a/src/core/animations/effect-preset-builder.ts +++ b/src/core/animations/effect-preset-builder.ts @@ -59,17 +59,19 @@ export class EffectPresetBuilder { case "slideUp": case "slideDown": { const isHorizontal = effectName === "slideLeft" || effectName === "slideRight"; - const startsPositive = effectName === "slideLeft" || effectName === "slideUp"; + // Left and down start on the positive side of their axis (offset y points up). + const startsPositive = effectName === "slideLeft" || effectName === "slideDown"; - const fittedSize = this.getFittedSize(editSize, clipSize); + // The render checks for overflow with the media fitted to the frame's other side, whatever the clip's fit. const editDimension = isHorizontal ? editSize.width : editSize.height; - const fittedDimension = isHorizontal ? fittedSize.width : fittedSize.height; + const fittedDimension = isHorizontal + ? (clipSize.width / clipSize.height) * editSize.height + : (clipSize.height / clipSize.width) * editSize.width; let targetOffset = this.getSlideStart(); - const minScale = editDimension + editDimension * targetOffset * 2; + const scaleFactor = 1 + targetOffset * 2; - if (fittedDimension < minScale) { - const scaleFactor = minScale / fittedDimension; + if (fittedDimension < editDimension * scaleFactor) { scaleKeyframes.push({ from: scaleFactor, to: scaleFactor, start, length, interpolation: "linear" }); } else { targetOffset = (fittedDimension - editDimension) / 2 / editDimension; @@ -111,36 +113,18 @@ export class EffectPresetBuilder { private getSlideStart(): number { const { name, speed } = this.effectPreset; + // Fractions of the frame, matching the render. if (name.startsWith("slide")) { switch (speed) { case "Slow": return 0.03; case "Fast": - return 0.2; - default: return 0.12; + default: + return 0.08; } } return 0; } - - private getFittedSize(editSize: Size, clipSize: Size): Size { - const fit = this.clipConfiguration.fit ?? "crop"; - - switch (fit) { - case "cover": - case "crop": { - const scale = Math.max(editSize.width / clipSize.width, editSize.height / clipSize.height); - return { width: clipSize.width * scale, height: clipSize.height * scale }; - } - case "contain": { - const scale = Math.min(editSize.width / clipSize.width, editSize.height / clipSize.height); - return { width: clipSize.width * scale, height: clipSize.height * scale }; - } - case "none": - default: - return clipSize; - } - } } diff --git a/tests/effect-preset-builder.test.ts b/tests/effect-preset-builder.test.ts new file mode 100644 index 00000000..75f1b100 --- /dev/null +++ b/tests/effect-preset-builder.test.ts @@ -0,0 +1,48 @@ +import { EffectPresetBuilder } from "@animations/effect-preset-builder"; +import { sec } from "@core/timing/types"; +import type { ResolvedClip } from "@schemas"; + +const EDIT = { width: 1920, height: 1080 }; + +const build = (effect: string, clipSize = EDIT, fit: ResolvedClip["fit"] = "crop") => + new EffectPresetBuilder({ + id: "clip-1", + asset: { type: "image", src: "https://example.com/a.png" }, + start: sec(0), + length: sec(5), + fit, + effect: effect as ResolvedClip["effect"] + }).buildRelative(EDIT, clipSize); + +describe("EffectPresetBuilder", () => { + it.each([ + ["slideLeft", 0.08], + ["slideLeftSlow", 0.03], + ["slideLeftFast", 0.12] + ])("pans %s by the render's %s of the frame, enlarged to cover it", (effect, distance) => { + const { offsetXKeyframes, scaleKeyframes } = build(effect); + + expect(offsetXKeyframes).toEqual([{ from: distance, to: -distance, start: 0, length: 5 }]); + expect(scaleKeyframes[0]).toMatchObject({ from: 1 + 2 * distance, to: 1 + 2 * distance }); + }); + + it.each([ + ["slideUp", -0.08, 0.08], + ["slideDown", 0.08, -0.08] + ])("pans %s the way its name says (offset y points up)", (effect, from, to) => { + expect(build(effect).offsetYKeyframes).toEqual([{ from, to, start: 0, length: 5 }]); + }); + + it("enlarges narrow media by the same amount whatever its fit", () => { + const { scaleKeyframes } = build("slideLeft", { width: 1080, height: 1920 }, "contain"); + + expect(scaleKeyframes[0]).toMatchObject({ from: 1.16, to: 1.16 }); + }); + + it.each(["crop", "contain"] as const)("pans the overflow of media fitted to the frame's height (%s)", fit => { + const { offsetXKeyframes, scaleKeyframes } = build("slideRight", { width: 4000, height: 1000 }, fit); + + expect(offsetXKeyframes).toEqual([{ from: -0.625, to: 0.625, start: 0, length: 5 }]); + expect(scaleKeyframes).toEqual([]); + }); +});