Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138YqDj3ttfayRwuMKKrqVj
107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
import { easeOutBack, easeOutCubic, progress } from "../easing.js";
|
|
import type { MotionTemplate } from "../template.js";
|
|
import {
|
|
ctaButton,
|
|
el,
|
|
fitText,
|
|
fontStack,
|
|
fullBackground,
|
|
logoEl,
|
|
stringParam,
|
|
svgDocument,
|
|
textEl,
|
|
unit,
|
|
} from "../template.js";
|
|
|
|
export const outroLogo: MotionTemplate = {
|
|
info: {
|
|
id: "outro_logo",
|
|
kind: "outro",
|
|
name: "Logo outro",
|
|
minDurationMs: 1500,
|
|
maxDurationMs: 6000,
|
|
params: [{ name: "message", type: "string", required: false }],
|
|
},
|
|
frame(t, ctx) {
|
|
const u = unit(ctx);
|
|
const d = ctx.durationMs;
|
|
const cx = ctx.width / 2;
|
|
const cy = ctx.height * 0.44;
|
|
const background = easeOutCubic(progress(t, 0, Math.min(500, d * 0.25)));
|
|
const pop = progress(t, d * 0.1, d * 0.4);
|
|
const scale = pop <= 0 ? 0 : easeOutBack(pop);
|
|
const size = u * 0.3 * scale;
|
|
const mark =
|
|
ctx.logos.length > 0
|
|
? logoEl(ctx, { x: cx - size / 2, y: cy - size / 2, w: size, h: size })
|
|
: el("circle", { cx, cy, r: Math.max(size / 2, 0.01), fill: ctx.kit.colors.primary });
|
|
const message = stringParam(ctx, "message") ?? ctx.kit.name;
|
|
const appear = easeOutCubic(progress(t, d * 0.3, d * 0.55));
|
|
const fitted = fitText(message, ctx.width * 0.8, u * 0.07, u * 0.035);
|
|
const text = textEl(fitted.text, {
|
|
x: cx,
|
|
y: cy + u * 0.26 + (1 - appear) * u * 0.03,
|
|
"text-anchor": "middle",
|
|
"font-family": fontStack(ctx.fonts.heading, "sans-serif"),
|
|
"font-weight": 600,
|
|
"font-size": fitted.size,
|
|
fill: ctx.kit.colors.text,
|
|
opacity: appear,
|
|
});
|
|
return svgDocument(ctx, fullBackground(ctx, background) + mark + text);
|
|
},
|
|
};
|
|
|
|
export const outroSubscribe: MotionTemplate = {
|
|
info: {
|
|
id: "outro_subscribe",
|
|
kind: "outro",
|
|
name: "Call to action outro",
|
|
minDurationMs: 2000,
|
|
maxDurationMs: 8000,
|
|
params: [
|
|
{ name: "cta", type: "string", required: true },
|
|
{ name: "handle", type: "string", required: false },
|
|
],
|
|
},
|
|
frame(t, ctx) {
|
|
const u = unit(ctx);
|
|
const d = ctx.durationMs;
|
|
const cx = ctx.width / 2;
|
|
const background = easeOutCubic(progress(t, 0, Math.min(500, d * 0.2)));
|
|
const logoAppear = easeOutCubic(progress(t, d * 0.05, d * 0.3));
|
|
const size = u * 0.18;
|
|
const logo = logoEl(
|
|
ctx,
|
|
{ x: cx - size / 2, y: ctx.height * 0.18, w: size, h: size },
|
|
{ opacity: logoAppear },
|
|
);
|
|
const pop = progress(t, d * 0.15, d * 0.4);
|
|
// A gentle 1 Hz breathing pulse once the button has landed; no brightness change.
|
|
const pulse = pop >= 1 ? 1 + 0.03 * Math.sin(((t - d * 0.4) / 1000) * Math.PI * 2) : 1;
|
|
const button = ctaButton(
|
|
ctx,
|
|
stringParam(ctx, "cta") ?? "",
|
|
cx,
|
|
ctx.height * 0.55,
|
|
(pop <= 0 ? 0 : easeOutBack(pop)) * pulse,
|
|
);
|
|
const handle = stringParam(ctx, "handle");
|
|
let handleText = "";
|
|
if (handle) {
|
|
const appear = easeOutCubic(progress(t, d * 0.35, d * 0.6));
|
|
const fitted = fitText(handle, ctx.width * 0.8, u * 0.05, u * 0.03);
|
|
handleText = textEl(fitted.text, {
|
|
x: cx,
|
|
y: ctx.height * 0.55 + u * 0.16,
|
|
"text-anchor": "middle",
|
|
"font-family": fontStack(ctx.fonts.body, "sans-serif"),
|
|
"font-size": fitted.size,
|
|
fill: ctx.kit.colors.text,
|
|
opacity: appear,
|
|
});
|
|
}
|
|
return svgDocument(ctx, fullBackground(ctx, background) + logo + button + handleText);
|
|
},
|
|
};
|