core/evaluators/card.evaluator.ts

core/evaluators/card.evaluator.ts is a file in Bane's Lab Social Share. 99 lines of code and 15 definitions.

import type {
    CardSpec,
    Effect,
    EffectKind,
    Expr,
    FrameContext,
    Layer,
    Placement,
    Profile,
    ResolvedCard,
    ResolvedLayer,
    ResolvedPlacement,
} from "#types/card.types";
import { SUBJECT_SEPARATOR, UNKNOWN_EFFECT } from "#configuration/strings/card.strings";

const FILTER_SEPARATOR = " ";

const FILTERS: ReadonlyMap<EffectKind, (amount: number) => string> = new Map<EffectKind, (amount: number) => string>([
    ["blur", (amount) => `blur(${String(amount)}px)`],
    ["brightness", (amount) => `brightness(${String(amount)})`],
    ["contrast", (amount) => `contrast(${String(amount)})`],
    ["glow", (amount) => `drop-shadow(0 0 ${String(amount)}px currentColor)`],
    ["hue", (amount) => `hue-rotate(${String(amount)}deg)`],
    ["saturate", (amount) => `saturate(${String(amount)})`],
]);

const isComputed = function isComputed<T>(expression: Expr<T>): expression is (context: FrameContext) => T {
    return typeof expression === "function";
};

export const valueAt = function valueAt<T>(expression: Expr<T>, frame: FrameContext): T {
    return isComputed(expression) ? expression(frame) : expression;
};

export const frameContext = function frameContext(spec: CardSpec, profile: Profile, frame: number): FrameContext {
    const { fps, frames } = spec.timeline;
    return { frame, frames, profile, progress: frames <= 1 ? 0 : frame / frames, t: frame / fps };
};

const placementOf = function placementOf(placement: Placement, frame: FrameContext): ResolvedPlacement {
    return {
        anchor: valueAt(placement.anchor ?? "start", frame),
        height: valueAt(placement.height ?? 0, frame),
        rotate: valueAt(placement.rotate ?? 0, frame),
        scale: valueAt(placement.scale ?? 1, frame),
        width: valueAt(placement.width ?? 0, frame),
        x: valueAt(placement.x, frame),
        y: valueAt(placement.y, frame),
    };
};

const filterOf = function filterOf(effects: readonly Effect[], frame: FrameContext): string {
    return effects
        .map((effect) => {
            const filter = FILTERS.get(effect.kind);
            if (filter === undefined) {
                throw new Error(UNKNOWN_EFFECT + SUBJECT_SEPARATOR + effect.kind);
            }
            return filter(valueAt(effect.amount, frame));
        })
        .join(FILTER_SEPARATOR);
};

const styleOf = function styleOf(layer: Layer, frame: FrameContext): Readonly<Record<string, string>> {
    return Object.fromEntries(
        Object.entries(layer.style ?? {}).map(([property, value]) => [property, String(valueAt(value, frame))]),
    );
};

const contentOf = function contentOf(layer: Layer, frame: FrameContext): string {
    if (layer.kind === "text") {
        return valueAt(layer.text, frame);
    }
    if (layer.kind === "image" || layer.kind === "animation") {
        return layer.source;
    }
    return layer.kind === "shader" ? layer.shader : "";
};

const uniformsOf = function uniformsOf(layer: Layer, frame: FrameContext): readonly number[] {
    return layer.kind === "shader" ? Object.values(layer.uniforms).map((value) => valueAt(value, frame)) : [];
};

export const resolveLayer = function resolveLayer(layer: Layer, frame: FrameContext): ResolvedLayer {
    return {
        alt: layer.kind === "image" || layer.kind === "animation" ? layer.alt : "",
        className: layer.className ?? "",
        content: contentOf(layer, frame),
        filter: filterOf(layer.effects ?? [], frame),
        id: layer.id,
        kind: layer.kind,
        opacity: valueAt(layer.opacity ?? 1, frame),
        placement: placementOf(layer.placement, frame),
        style: styleOf(layer, frame),
        uniforms: uniformsOf(layer, frame),
    };
};

export const resolveCard = function resolveCard(spec: CardSpec, profile: Profile, frame: number): ResolvedCard {
    const context = frameContext(spec, profile, frame);
    return {
        frame,
        id: spec.id,
        layers: spec.layers.map((layer) => resolveLayer(layer, context)),
        profile,
        progress: context.progress,
        stylesheet: spec.stylesheet,
        time: context.t,
        tone: spec.tone,
    };
};