UNPKG

@thi.ng/hiccup-canvas

Version:

Hiccup shape tree renderer for vanilla Canvas 2D contexts

161 lines (160 loc) 3.91 kB
import { adaptDPI } from "@thi.ng/canvas"; import { isArrayLike } from "@thi.ng/checks/is-arraylike"; import { resolveGradientOrColor } from "../color.js"; const DEFAULTS = { 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 = { 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" }; const __newState = (state, restore = false) => ({ attribs: { ...state.attribs }, grads: { ...state.grads }, edits: [], restore }); const __mergeState = (ctx, state, attribs) => { let res; if (!attribs) return; const canvas = ctx.canvas; const dpr = attribs.__dpr; if (dpr && dpr != canvas.dataset.dpr) { adaptDPI( canvas, +(canvas.dataset.origWidth ?? canvas.width), +(canvas.dataset.origHeight ?? canvas.height), dpr ); ctx.scale(dpr, dpr); } if (__applyTransform(ctx, attribs)) { res = __newState(state, true); } if (attribs.__background || attribs.__clear) { ctx.save(); ctx.resetTransform(); const { width, height } = canvas; if (attribs.__clear) { ctx.clearRect(0, 0, width, height); } else { ctx.fillStyle = resolveGradientOrColor(state, attribs.__background); ctx.fillRect(0, 0, width, height); } ctx.restore(); } for (const 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); } } } return res; }; const __restoreState = (ctx, prev, curr) => { 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] ); } }; const __registerGradient = (state, id, g) => { !state.grads && (state.grads = {}); state.grads[id] = g; }; const __setAttrib = (ctx, state, id, k, val) => { switch (id) { case "fill": case "stroke": case "shadowColor": ctx[k] = resolveGradientOrColor(state, val); break; case "dash": ctx[k].call(ctx, val); break; case "clip": case "fillRule": break; default: ctx[k] = val; } }; const __applyTransform = (ctx, attribs) => { let v; 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; }; export { __mergeState, __registerGradient, __restoreState };