NarraLeaf

LayeredDefinition

The src of a layered image, see Image.

export type LayeredDefinition<L extends LayerGroupDefinition = LayerGroupDefinition> = {
    /**
     * The layer stack, from bottom to top. Array order is the stacking order.
     */
    layers: L;
    /**
     * One tag per group, in any order. Layers that offer the same tag set share a group and so
     * share that one default.
     */
    defaults: readonly LayerTagsOf<L>[];
};
/**
 * One slot of a layered image, from bottom to top. Either a constant src, `null`,
 * a variant map, or a resolver.
 */
export type LayerSlot = string | null | LayerVariants | LayerResolver;
export type LayerGroupDefinition = readonly LayerSlot[];

/**
 * A set of mutually exclusive variants for one layer, keyed by tag.
 *
 * `null` means the layer draws nothing for that tag.
 *
 * The tag set is the group's identity: every layer offering the *same* set of tags is driven by
 * one group, which is how a single `char(["angry"])` moves the brows, the eyes and the mouth at
 * once. To follow a group, repeat its whole tag set on the layer and use `null` for the tags
 * where that layer draws nothing. Offering only part of a set instead declares a *different*
 * group, and the shared tags then collide.
 */
export type LayerVariants = Record<string, string | null>;

/**
 * Derives a layer's src from the currently active tags. Declares no tags of its own.
 *
 * A resolver is opaque, so the srcs it can return are invisible to the preloader and are fetched
 * on first use. Prefer LayerVariants — including for a layer that follows another layer's
 * group, which no longer needs a resolver — and keep resolvers for sources that genuinely cannot
 * be enumerated.
 */
export type LayerResolver = (tags: ReadonlySet<string>) => string | null;

/**
 * The union of every tag declared by a layer stack.
 */
export type LayerTagsOf<L> = L extends readonly (infer E)[]
    ? (E extends LayerResolver ? never : E extends LayerVariants ? keyof E & string : never)
    : never;

Tags are inferred from the definition, so no explicit type argument is needed:

const yuko = new Image({
    src: {
        layers: [
            "yuko/body.png",
            {uniform: "yuko/uniform.png", casual: "yuko/casual.png"},
            {happy: "yuko/happy.png", sad: "yuko/sad.png"},
        ],
        defaults: ["uniform", "happy"],
    },
});

yuko.char(["sad"]);  // ok
yuko.char(["sda"]);  // rejected at compile time