thi-ng/umbrella

View on GitHub
packages/hiccup-canvas/src/internal/state.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import type { IObjectOf, Maybe } from "@thi.ng/api";
import { isArrayLike } from "@thi.ng/checks/is-arraylike";
import type { DrawState } from "../api.js";
import { resolveGradientOrColor } from "../color.js";

const DEFAULTS: any = {
    align: "left",
    alpha: 1,
    baseline: "alphabetic",
    compose: "source-over",
    dash: [],
    dashOffset: 0,
    direction: "inherit",
    fill: "#000",
    filter: "none",
    font: "10px sans-serif",
    lineCap: "butt",
    lineJoin: "miter",
    miterLimit: 10,
    shadowBlur: 0,
    shadowColor: "rgba(0,0,0,0)",
    shadowX: 0,
    shadowY: 0,
    smooth: true,
    stroke: "#000",
    weight: 1,
};

const CTX_ATTRIBS: IObjectOf<string> = {
    align: "textAlign",
    alpha: "globalAlpha",
    baseline: "textBaseline",
    clip: "clip",
    compose: "globalCompositeOperation",
    dash: "setLineDash",
    dashOffset: "lineDashOffset",
    direction: "direction",
    fill: "fillStyle",
    fillRule: "fillRule",
    filter: "filter",
    font: "font",
    lineCap: "lineCap",
    lineJoin: "lineJoin",
    miterLimit: "miterLimit",
    shadowBlur: "shadowBlur",
    shadowColor: "shadowColor",
    shadowX: "shadowOffsetX",
    shadowY: "shadowOffsetY",
    smooth: "imageSmoothingEnabled",
    stroke: "strokeStyle",
    weight: "lineWidth",
};

/** @internal */
const __newState = (state: DrawState, restore = false) => ({
    attribs: { ...state.attribs },
    grads: { ...state.grads },
    edits: [],
    restore,
});

/** @internal */
export const __mergeState = (
    ctx: CanvasRenderingContext2D,
    state: DrawState,
    attribs: IObjectOf<any>
) => {
    let res: Maybe<DrawState>;
    if (!attribs) return;
    if (__applyTransform(ctx, attribs)) {
        res = __newState(state, true);
    }
    for (let id in attribs) {
        const k = CTX_ATTRIBS[id];
        if (k) {
            const v = attribs[id];
            if (v != null && state.attribs[id] !== v) {
                !res && (res = __newState(state));
                res.attribs[id] = v;
                res.edits!.push(id);
                __setAttrib(ctx, state, id, k, v);
            }
        } else if (id === "__background" || id === "__clear") {
            ctx.save();
            ctx.resetTransform();
            if (id === "__clear") {
                attribs[id] &&
                    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
            } else {
                ctx.fillStyle = resolveGradientOrColor(state, attribs[id]);
                ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
            }
            ctx.restore();
        }
    }
    return res;
};

/** @internal */
export const __restoreState = (
    ctx: CanvasRenderingContext2D,
    prev: DrawState,
    curr: DrawState
) => {
    if (curr.restore) {
        ctx.restore();
        return;
    }
    const edits = curr.edits;
    const attribs = prev.attribs;
    for (let i = edits.length; i-- > 0; ) {
        const id = edits[i];
        const v = attribs[id];
        __setAttrib(
            ctx,
            prev,
            id,
            CTX_ATTRIBS[id],
            v != null ? v : DEFAULTS[id]
        );
    }
};

/** @internal */
export const __registerGradient = (
    state: DrawState,
    id: string,
    g: CanvasGradient
) => {
    !state.grads && (state.grads = {});
    state.grads[id] = g;
};

/** @internal */
const __setAttrib = (
    ctx: CanvasRenderingContext2D,
    state: DrawState,
    id: string,
    k: string,
    val: any
) => {
    switch (id) {
        case "fill":
        case "stroke":
        case "shadowColor":
            (<any>ctx)[k] = resolveGradientOrColor(state, val);
            break;
        case "dash":
            (<any>ctx)[k].call(ctx, val);
            break;
        case "clip":
        case "fillRule":
            break;
        default:
            (<any>ctx)[k] = val;
    }
};

/** @internal */
const __applyTransform = (
    ctx: CanvasRenderingContext2D,
    attribs: IObjectOf<any>
) => {
    let v: any;
    if (
        (v = attribs.transform) ||
        attribs.setTransform ||
        attribs.translate ||
        attribs.scale ||
        attribs.rotate
    ) {
        ctx.save();
        if (v) {
            ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
        } else if ((v = attribs.setTransform)) {
            ctx.setTransform(v[0], v[1], v[2], v[3], v[4], v[5]);
        } else {
            (v = attribs.translate) && ctx.translate(v[0], v[1]);
            (v = attribs.rotate) && ctx.rotate(v);
            (v = attribs.scale) &&
                (isArrayLike(v) ? ctx.scale(v[0], v[1]) : ctx.scale(v, v));
        }
        return true;
    }
    return false;
};