# core/evaluators/card.evaluator.ts

> 99 lines of code and 15 definitions.

Tree: Bane's Lab Social Share
Language: typescript
Layer: runtime
Canonical: https://banes-lab.com/anatomy/social-share#file-social-share-core-evaluators-card-evaluator-ts
Source text: https://banes-lab.com/source/social-share/core/evaluators/card.evaluator.ts.txt

Listed in [core/evaluators](https://banes-lab.com/api/source/social-share/core/evaluators.md), before [core/evaluators/card.fragment.evaluator.ts](https://banes-lab.com/source/social-share/core/evaluators/card.fragment.evaluator.ts.md).

## Definitions

- `valueAt` (lexical_declaration, line 31, exported)
- `resolveCard` (lexical_declaration, line 99, exported)
- `placementOf` (lexical_declaration, line 40)
- `resolveLayer` (lexical_declaration, line 84, exported)
- `filterOf` (lexical_declaration, line 52)
- `styleOf` (lexical_declaration, line 64)
- `contentOf` (lexical_declaration, line 70)
- `uniformsOf` (lexical_declaration, line 80)
- `isComputed` (lexical_declaration, line 27)
- `filter` (lexical_declaration, line 55)
- `FILTER_SEPARATOR` (lexical_declaration, line 16)
- `FILTERS` (lexical_declaration, line 18)
- `frameContext` (lexical_declaration, line 35, exported)
- `{ fps, frames }` (lexical_declaration, line 36, exported)
- `context` (lexical_declaration, line 100, exported)

## Contained in

- [core/evaluators](https://banes-lab.com/anatomy/social-share/folder-social-share-core-evaluators.md)

## Used by

- [core/persistence/image.persistence.ts](https://banes-lab.com/source/social-share/core/persistence/image.persistence.ts.md)
- [core/renderers/grid.renderer.ts](https://banes-lab.com/source/social-share/core/renderers/grid.renderer.ts.md)
- [core/validators/card.validator.ts](https://banes-lab.com/source/social-share/core/validators/card.validator.ts.md)
- [core/validators/page.validator.ts](https://banes-lab.com/source/social-share/core/validators/page.validator.ts.md)
- [runtime/entrypoints/stage.entrypoint.ts](https://banes-lab.com/source/social-share/runtime/entrypoints/stage.entrypoint.ts.md)

## Linked from

- [core/evaluators](https://banes-lab.com/anatomy/social-share/folder-social-share-core-evaluators.md)
- [core/persistence](https://banes-lab.com/anatomy/social-share/folder-social-share-core-persistence.md)
- [core/renderers](https://banes-lab.com/anatomy/social-share/folder-social-share-core-renderers.md)
- [core/validators](https://banes-lab.com/anatomy/social-share/folder-social-share-core-validators.md)
- [runtime/entrypoints](https://banes-lab.com/anatomy/social-share/folder-social-share-runtime-entrypoints.md)

## Source

```typescript
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,
    };
};
```
