Skip to content

Commit

Permalink
feat(hdom-canvas): implement drawing state inheritance & restoration
Browse files Browse the repository at this point in the history
- add mergeState() & restoreState() to apply & undo only edited attribs
- add CTX_ATTRIBS alias mappings & DEFAULTS
- rename beginShape() => applyTransform()
- rename createTree() => drawTree()
- remove export flags from shape fns
- add @thi.ng/api dep
  • Loading branch information
postspectacular committed Sep 11, 2018
1 parent d93efa8 commit ccbf53c
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 79 deletions.
1 change: 1 addition & 0 deletions packages/hdom-canvas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"typescript": "^3.0.1"
},
"dependencies": {
"@thi.ng/api": "^4.1.1",
"@thi.ng/checks": "^1.5.8",
"@thi.ng/hdom": "^4.0.5",
"@thi.ng/vectors": "^1.1.0"
Expand Down
286 changes: 207 additions & 79 deletions packages/hdom-canvas/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,78 @@
import { IObjectOf } from "@thi.ng/api/api";
import { isArrayLike } from "@thi.ng/checks/is-arraylike";
import { HDOMImplementation } from "@thi.ng/hdom/api";
import { ReadonlyVec } from "@thi.ng/vectors/api";
import { TAU } from "@thi.ng/vectors/math";

interface DrawState {
attribs: IObjectOf<any>;
edits?: string[];
restore?: boolean;
}

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

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

export const canvas = (_, attribs, ...shapes: any[]) =>
["canvas", attribs,
["g", { __normalize: false, __diff: false, __impl: IMPL },
...shapes]];

export const createTree = (element: HTMLCanvasElement, tree: any) => {
export const drawTree = (element: HTMLCanvasElement, tree: any) => {
const ctx = element.getContext("2d");
ctx.clearRect(0, 0, element.width, element.height);
const shape = (s: any[]) => {
const attribs = s[1];
const shape = (s: any, pstate: DrawState) => {
if (!s) return;
const state = mergeState(ctx, pstate, s[1]);
const attribs = state ? state.attribs : pstate.attribs;
switch (s[0]) {
case "g":
const restore = beginShape(ctx, attribs);
for (let i = 2, n = s.length; i < n; i++) {
shape(s[i]);
for (let i = 2, n = s.length, __state = state || pstate; i < n; i++) {
shape(s[i], __state);
}
endShape(ctx, attribs, restore);
break;
case "line":
line(ctx, attribs, s[2], s[3]);
Expand Down Expand Up @@ -52,135 +105,210 @@ export const createTree = (element: HTMLCanvasElement, tree: any) => {
image(ctx, attribs, s[2], s[3]);
default:
}
state && restoreState(ctx, pstate, state);
};
shape(tree);
shape(tree, { attribs: {} });
return null;
};

export const IMPL: HDOMImplementation<any> = {
createTree: drawTree,
};

const mergeState = (ctx: CanvasRenderingContext2D,
state: DrawState,
attribs: IObjectOf<any>) => {

let res: DrawState;
if (!attribs) return;
if (applyTransform(ctx, attribs)) {
res = {
attribs: { ...state.attribs },
edits: [],
restore: true
};
}
for (let id in attribs) {
const k = CTX_ATTRIBS[id];
if (k) {
const v = attribs[id];
if (v != null && state.attribs[id] !== v) {
if (!res) {
res = {
attribs: { ...state.attribs },
edits: []
};
}
res.attribs[id] = v;
res.edits.push(id);
setAttrib(ctx, id, k, v);
}
}
}
return res;
};

const restoreState = (ctx: CanvasRenderingContext2D,
prev: DrawState,
curr: DrawState) => {

if (curr.restore) {
ctx.restore();
return;
}
const edits = curr.edits;
if (edits) {
for (let attribs = prev.attribs, i = edits.length - 1; i >= 0; i--) {
const id = edits[i];
const v = attribs[id];
setAttrib(ctx, id, CTX_ATTRIBS[id], v != null ? v : DEFAULTS[id]);
}
}
};

const setAttrib = (ctx: CanvasRenderingContext2D,
id: string,
k: string,
val: any) => {

switch (id) {
case "dash":
ctx[k].call(ctx, val);
break;
case "clip":
break;
default:
ctx[k] = val;
}
};

const applyTransform = (ctx: CanvasRenderingContext2D, attribs: IObjectOf<any>) => {
let v: any;
if ((v = attribs.transform) ||
attribs.translate ||
attribs.scale != null ||
attribs.rotate != null) {

ctx.save();
if (v) {
ctx.transform(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;
};

const endShape = (ctx: CanvasRenderingContext2D, attribs: IObjectOf<any>) => {
let v: any;
if ((v = attribs.fill) && v !== "none") {
ctx.fill();
}
if ((v = attribs.stroke) && v !== "none") {
ctx.stroke();
}
};

const line = (ctx: CanvasRenderingContext2D,
attribs: IObjectOf<any>,
a: ReadonlyVec,
b: ReadonlyVec) => {

export const line = (ctx: CanvasRenderingContext2D, attribs: any, a: ReadonlyVec, b: ReadonlyVec) => {
const restore = beginShape(ctx, attribs);
if (attribs.stroke === "none") {
return;
}
ctx.beginPath();
ctx.moveTo(a[0], a[1]);
ctx.lineTo(b[0], b[1]);
endShape(ctx, attribs, restore);
ctx.stroke();
};

export const polyline = (ctx: CanvasRenderingContext2D, attribs: any, pts: ReadonlyVec[]) => {
const polyline = (ctx: CanvasRenderingContext2D,
attribs: IObjectOf<any>,
pts: ReadonlyVec[]) => {

if (pts.length < 2) return;
let v: any;
if ((v = attribs.stroke)) {
if (v === "none") return;
ctx.strokeStyle = v;
// ctx.strokeStyle = v;
}
const restore = beginShape(ctx, attribs);
let p: ReadonlyVec = pts[0];
ctx.beginPath();
ctx.moveTo(p[0], p[1]);
for (let i = 1, n = pts.length; i < n; i++) {
p = pts[i];
ctx.lineTo(p[0], p[1]);
}
endShape(ctx, attribs, restore);
ctx.stroke();
};

export const polygon = (ctx: CanvasRenderingContext2D, attribs: any, pts: ReadonlyVec[]) => {
const polygon = (ctx: CanvasRenderingContext2D,
attribs: IObjectOf<any>,
pts: ReadonlyVec[]) => {

if (pts.length < 2) return;
let p: ReadonlyVec = pts[0];
const restore = beginShape(ctx, attribs);
ctx.beginPath();
ctx.moveTo(p[0], p[1]);
for (let i = 1, n = pts.length; i < n; i++) {
p = pts[i];
ctx.lineTo(p[0], p[1]);
}
ctx.closePath();
endShape(ctx, attribs, restore);
endShape(ctx, attribs);
};

export const arc = (ctx: CanvasRenderingContext2D, attribs: any, pos: ReadonlyVec, r: number, start = 0, end = TAU, antiCCW = false) => {
const restore = beginShape(ctx, attribs);
const arc = (ctx: CanvasRenderingContext2D,
attribs: IObjectOf<any>,
pos: ReadonlyVec,
r: number,
start = 0,
end = TAU,
antiCCW = false) => {

ctx.beginPath();
ctx.arc(pos[0], pos[1], r, start, end, antiCCW);
endShape(ctx, attribs, restore);
endShape(ctx, attribs);
};

export const rect = (ctx: CanvasRenderingContext2D, attribs: any, pos: ReadonlyVec, w: number, h: number) => {
const restore = beginShape(ctx, attribs);
const rect = (ctx: CanvasRenderingContext2D,
attribs: IObjectOf<any>,
pos: ReadonlyVec,
w: number,
h: number) => {

let v: any;
if ((v = attribs.fill) && v !== "none") {
ctx.fillStyle = v;
ctx.fillRect(pos[0], pos[1], w, h);
}
if ((v = attribs.stroke) && v !== "none") {
ctx.strokeStyle = v;
ctx.strokeRect(pos[0], pos[1], w, h);
}
restore && ctx.restore();
};

export const text = (ctx: CanvasRenderingContext2D, attribs: any, pos: ReadonlyVec, body: any) => {
const restore = beginShape(ctx, attribs);
const text = (ctx: CanvasRenderingContext2D,
attribs: IObjectOf<any>,
pos: ReadonlyVec,
body: any) => {

let v: any;
(v = attribs.font) && (ctx.font = v);
(v = attribs.align) && (ctx.textAlign = v);
(v = attribs.baseLine) && (ctx.textBaseline = v);
if (attribs.fill && attribs.fill !== "none") {
ctx.fillStyle = attribs.fill;
if ((v = attribs.fill) && v !== "none") {
ctx.fillText(body.toString(), pos[0], pos[1]);
}
if (attribs.stroke && attribs.stroke !== "none") {
ctx.strokeStyle = attribs.stroke;
if ((v = attribs.stroke) && v !== "none") {
ctx.strokeText(body.toString(), pos[0], pos[1]);
}
restore && ctx.restore();
};

export const image = (
const image = (
ctx: CanvasRenderingContext2D,
attribs: any,
_: IObjectOf<any>,
img: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | ImageBitmap,
pos: ReadonlyVec) => {
const restore = beginShape(ctx, attribs);
ctx.drawImage(img, pos[0], pos[1]);
restore && ctx.restore();
};

const beginShape = (ctx: CanvasRenderingContext2D, attribs: any) => {
let v: any;
if ((v = attribs.transform) || attribs.translate || attribs.scale != null || attribs.rotate != null) {
ctx.save();
if (v) {
ctx.transform(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;
};

const endShape = (ctx: CanvasRenderingContext2D, attribs: any, restore: boolean) => {
let v: any;
if ((v = attribs.fill) && v !== "none") {
ctx.fillStyle = v;
ctx.fill();
}
if ((v = attribs.stroke) && v !== "none") {
ctx.strokeStyle = v;
ctx.stroke();
}
restore && ctx.restore();
};

export const IMPL: HDOMImplementation<any> = {
createTree,
getChild: null,
removeChild: null,
replaceChild: null,
setAttrib: null,
removeAttribs: null,
setContent: null,
ctx.drawImage(img, pos[0], pos[1]);
};

0 comments on commit ccbf53c

Please sign in to comment.