2026-10-03 17:47:24 +00:00

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);
},
};