69 lines
2.6 KiB
TypeScript
69 lines
2.6 KiB
TypeScript
export interface KeyframeStep {
|
|
o: Record<string, string | number>;
|
|
}
|
|
|
|
export interface ScrollSceneOptions {
|
|
selector: string;
|
|
keyframes: Record<string, KeyframeStep | Record<string, string | number>>;
|
|
range?: string;
|
|
timeline?: "view" | "scroll" | string;
|
|
animation?: string;
|
|
duration?: string;
|
|
easing?: string;
|
|
fill?: string;
|
|
}
|
|
|
|
export interface TransitionPortalOptions {
|
|
name: string;
|
|
old?: Record<string, string | number>;
|
|
new?: Record<string, string | number>;
|
|
duration?: string;
|
|
easing?: string;
|
|
}
|
|
|
|
function declarations(values: Record<string, string | number>): string {
|
|
return Object.entries(values).map(([property, value]) => `${property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}: ${value};`).join(" ");
|
|
}
|
|
|
|
/** Builds declarative native CSS scenes and view transitions without a client runtime. */
|
|
export class StageManager {
|
|
private readonly scenes: ScrollSceneOptions[] = [];
|
|
private readonly portals: TransitionPortalOptions[] = [];
|
|
|
|
scene(name: string, options: Omit<ScrollSceneOptions, "animation">): this {
|
|
this.scenes.push({ ...options, animation: name });
|
|
return this;
|
|
}
|
|
|
|
portal(name: string, options: Omit<TransitionPortalOptions, "name"> = {}): this {
|
|
this.portals.push({ name, ...options });
|
|
return this;
|
|
}
|
|
|
|
css(): string {
|
|
return [
|
|
...this.scenes.flatMap((scene) => {
|
|
const animation = scene.animation ?? "muddle-scene";
|
|
const timeline = scene.timeline ?? "view";
|
|
return [
|
|
`@keyframes ${animation} { ${Object.entries(scene.keyframes).map(([offset, step]) => {
|
|
const values = "to" in step && typeof step.to === "object" ? step.to : step as Record<string, string | number>;
|
|
return `${offset} { ${declarations(values)} }`;
|
|
}).join(" ")} }`,
|
|
`${scene.selector} { animation-name: ${animation}; animation-duration: ${scene.duration ?? "1ms"}; animation-timing-function: ${scene.easing ?? "linear"}; animation-fill-mode: ${scene.fill ?? "both"}; animation-timeline: ${timeline === "view" ? "view()" : timeline === "scroll" ? "scroll()" : timeline}; animation-range: ${scene.range ?? "entry 0% cover 100%"}; }`,
|
|
];
|
|
}),
|
|
...this.portals.flatMap((portal) => {
|
|
const timing = `animation-duration: ${portal.duration ?? "240ms"}; animation-timing-function: ${portal.easing ?? "ease"};`;
|
|
return [
|
|
portal.old ? `::view-transition-old(${portal.name}) { ${timing} ${declarations(portal.old)} }` : "",
|
|
portal.new ? `::view-transition-new(${portal.name}) { ${timing} ${declarations(portal.new)} }` : "",
|
|
].filter(Boolean);
|
|
}),
|
|
].join("\n");
|
|
}
|
|
}
|
|
|
|
export function stage(): StageManager {
|
|
return new StageManager();
|
|
}
|