A lightweight API for using the HTML5 Canvas as a diagram.
Last active
February 18, 2018 23:13
-
-
Save mathdoodle/2922b500e6c4b365aa850e456f0caa4d to your computer and use it in GitHub Desktop.
HTML5 Canvas Diagram
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function toHexString(c: number): string { | |
const hex = c.toString(16) | |
return hex.length === 1 ? "0" + hex : hex | |
} | |
export function rgb(r: number, g: number, b: number): string { | |
return "#" + toHexString(r) + toHexString(g) + toHexString(b) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { rgb } from './color.js' | |
import { G2 } from 'davinci-units' | |
export interface Drawable { | |
X: UNITS.G2 | |
R: UNITS.G2 | |
draw(): void | |
} | |
export interface Diagram extends Drawable { | |
context: CanvasRenderingContext2D | |
add(drawable: Drawable): void | |
arc(center: G2, radius: number): void | |
clear(): void | |
lineTo(X: G2): void | |
moveTo(X: G2): void | |
point(X: G2): void | |
pointSize: number | |
rotate(θ: number): void | |
scaleFactor: G2 | |
translate(X: G2): void | |
} | |
function toPixelsX(X: G2, scaleFactor: G2, canvas: HTMLCanvasElement): number { | |
return +(X / scaleFactor).x * canvas.width / 2 | |
} | |
function toPixelsY(X: G2, scaleFactor: G2, canvas: HTMLCanvasElement): number { | |
return -(X / scaleFactor).y * canvas.height / 2 | |
} | |
export function createDiagram(elementId: string): Diagram { | |
const canvas = <HTMLCanvasElement> document.getElementById(elementId) | |
const context = canvas.getContext('2d') as CanvasRenderingContext2D | |
const drawables: Drawable[] = [] | |
const position = G2.zero | |
const attitude = G2.one | |
const origin = G2.vector(canvas.width / 2, canvas.height / 2) | |
let pointSize = 12 | |
let scaleFactor = G2.one | |
const add = function(drawable: Drawable) { | |
drawables.push(drawable) | |
} | |
const clear = function() { | |
context.fillStyle = rgb(255, 255, 255) | |
context.fillRect(0, 0, canvas.width, canvas.height) | |
} | |
const draw = function() { | |
drawables.forEach((drawable) => { drawable.draw() }) | |
} | |
const arc = function(center: G2, radius: number) { | |
context.arc(origin.x + center.x, origin.y + center.y, radius, 0, 2 * Math.PI, true) | |
} | |
const lineTo = function(X: G2) { | |
const x = toPixelsX(X, scaleFactor, canvas) | |
const y = toPixelsY(X, scaleFactor, canvas) | |
context.lineTo(origin.x + x, origin.y + y) | |
} | |
const moveTo = function(X: G2) { | |
const x = toPixelsX(X, scaleFactor, canvas) | |
const y = toPixelsY(X, scaleFactor, canvas) | |
context.moveTo(origin.x + x, origin.y + y) | |
} | |
const point = function(X: G2) { | |
const radius = pointSize / 2 | |
context.save() | |
context.beginPath() | |
const x = toPixelsX(X, scaleFactor, canvas) | |
const y = toPixelsY(X, scaleFactor, canvas) | |
context.arc(origin.x + x, origin.y + y, radius, 0, 2 * Math.PI, true) | |
context.fillStyle = rgb(0, 0, 0) | |
context.fill() | |
context.lineWidth = 2 | |
context.strokeStyle = rgb(0, 0, 0) | |
context.stroke() | |
context.restore() | |
} | |
const rotate = function(θ: number) { | |
context.translate(+origin.x, +origin.y) | |
context.rotate(θ) | |
context.translate(-origin.x, -origin.y) | |
} | |
const translate = function(X: G2) { | |
const x = toPixelsX(X, scaleFactor, canvas) | |
const y = toPixelsY(X, scaleFactor, canvas) | |
context.translate(x, y) | |
} | |
const diag: Diagram = { | |
get context() { | |
return context | |
}, | |
get X() { | |
return position | |
}, | |
get pointSize() { | |
return pointSize | |
}, | |
set pointSize(value) { | |
pointSize = value | |
}, | |
get R() { | |
return attitude | |
}, | |
get scaleFactor() { | |
return scaleFactor | |
}, | |
set scaleFactor(value: G2) { | |
scaleFactor = value | |
}, | |
add, | |
arc, | |
clear, | |
draw, | |
lineTo, | |
moveTo, | |
point, | |
rotate, | |
translate | |
} | |
return Object.freeze(diag) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Drawable, Diagram } from './diagram.js' | |
import { createDirectedLineSegment } from './DirectedLineSegment.js' | |
import { e1, e2 } from './math.js' | |
import { G2 } from 'davinci-units' | |
export interface DirectedAreaElement extends Drawable { | |
/** | |
* Vector along the first side of the DirectedAreaElement. | |
*/ | |
a: G2 | |
/** | |
* Vector along the second side of the DirectedAreaElement. | |
*/ | |
b: G2 | |
} | |
export function createDirectedAreaElement(diagram: Diagram): DirectedAreaElement { | |
const L1 = createDirectedLineSegment(diagram, true) | |
L1.a = e1 | |
L1.centered = false | |
const L2 = createDirectedLineSegment(diagram, true) | |
L2.a = e2 | |
L2.centered = false | |
let position = G2.zero | |
const centered = true | |
let attitude = G2.one | |
const draw = function() { | |
const context = diagram.context | |
context.save() | |
const θ = attitude.angle().b | |
diagram.translate(position) | |
diagram.rotate(θ) | |
context.beginPath() | |
if (centered) { | |
const s = (L1.a + L2.a) / 2 | |
const t = (L1.a - L2.a) / 2 | |
diagram.moveTo(-s) | |
diagram.lineTo(+t) | |
diagram.lineTo(+s) | |
diagram.lineTo(-t) | |
} | |
else { | |
diagram.moveTo(position) | |
diagram.lineTo(position + L1.a) | |
diagram.lineTo(position + L1.a + L2.a) | |
diagram.lineTo(position + L2.a) | |
} | |
context.closePath() | |
context.fillStyle = '#d3bea5' | |
context.fill() | |
context.lineWidth = 2 | |
context.stroke() | |
if (centered) { | |
L1.X = (-L1.a - L2.a) / 2 | |
L2.X = (-L2.a + L1.a) / 2 | |
} | |
else { | |
L1.X = position | |
L2.X = position | |
} | |
L1.draw() | |
L2.draw() | |
context.restore() | |
} | |
const that: DirectedAreaElement = { | |
get a() { | |
return L1.a | |
}, | |
set a(value) { | |
// Copy is not a function? | |
L1.a = value | |
}, | |
get b() { | |
return L2.a | |
}, | |
set b(value) { | |
// Copy is not a function? | |
L2.a = value | |
}, | |
get X() { | |
return position | |
}, | |
set X(value: UNITS.G2) { | |
position = value | |
}, | |
get R() { | |
return attitude | |
}, | |
set R(value) { | |
attitude = value | |
}, | |
draw | |
} | |
diagram.add(that) | |
return Object.freeze(that) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Drawable, Diagram } from './diagram.js' | |
import { G2 } from 'davinci-units' | |
export interface DirectedLineSegment extends Drawable { | |
/** | |
* Vector that is represented by the directed line segment. | |
*/ | |
a: G2 | |
centered: boolean | |
} | |
function drawHead(s: G2, diagram: Diagram) { | |
const context = diagram.context | |
const canvas = diagram.context.canvas | |
const unit = s.direction() | |
/** | |
* Perpendicular (unit vector) to the directed line segment direction. | |
*/ | |
const perp = s.rotate((G2.I * Math.PI / 4).exp()).direction() | |
/** | |
* Vector comparable in size to a point. | |
*/ | |
const n = perp * diagram.pointSize * diagram.scaleFactor / canvas.width | |
const m = 2 * 1.618 * unit * diagram.pointSize * diagram.scaleFactor / canvas.width | |
context.beginPath() | |
diagram.moveTo(s - n - m) | |
diagram.lineTo(s) | |
diagram.lineTo(s + n - m) | |
context.closePath() | |
context.fillStyle = '#000000' | |
context.fill() | |
context.stroke() | |
} | |
export function createDirectedLineSegment(diagram: Diagram, standalone = false): DirectedLineSegment { | |
let a = G2.e1 | |
let position = G2.zero | |
let centered = true | |
let attitude = G2.one | |
const draw = function() { | |
const context = diagram.context | |
// const canvas = context.canvas | |
context.save() | |
const θ = attitude.angle().b | |
diagram.translate(position) | |
diagram.rotate(θ) | |
if (centered) { | |
const s = a / 2 | |
context.beginPath() | |
diagram.moveTo(-s) | |
diagram.lineTo(+s) | |
context.closePath() | |
context.fillStyle = '#d3bea5' | |
context.fill() | |
context.lineWidth = 2 | |
context.stroke() | |
drawHead(s, diagram) | |
} | |
else { | |
context.beginPath() | |
diagram.moveTo(G2.zero) | |
diagram.lineTo(a) | |
context.closePath() | |
context.fillStyle = '#d3bea5' | |
context.fill() | |
context.lineWidth = 2 | |
context.stroke() | |
drawHead(a, diagram) | |
} | |
context.restore() | |
} | |
const that: DirectedLineSegment = { | |
get a() { | |
return a | |
}, | |
set a(value) { | |
// Copy is not a function? | |
a = value | |
}, | |
get centered() { | |
return centered | |
}, | |
set centered(value) { | |
centered = value | |
}, | |
get X() { | |
return position | |
}, | |
set X(value: UNITS.G2) { | |
position = value | |
}, | |
get R() { | |
return attitude | |
}, | |
set R(value) { | |
attitude = value | |
}, | |
draw | |
} | |
if (!standalone) { | |
diagram.add(that) | |
} | |
return Object.freeze(that) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<base href='/'> | |
<script src="https://jspm.io/[email protected]"></script> | |
<link rel='stylesheet' href='style.css'> | |
</head> | |
<body> | |
<canvas id='diagram' width='500' height='500'></canvas> | |
<script> | |
System.defaultJSExtensions = true | |
System.import('./script') | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { G2, exp as exponential } from 'davinci-units' | |
/** | |
* Unit vector to the right. | |
*/ | |
export const e1 = G2.e1 | |
/** | |
* Unit vector to the upwards. | |
*/ | |
export const e2 = G2.e2 | |
/** | |
* The identity element for multiplication. | |
*/ | |
export const one = G2.one | |
export const exp = exponential |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"uuid": "ba223414-560d-4bac-b702-894c8e9a3788", | |
"description": "HTML5 Canvas Diagram", | |
"dependencies": { | |
"davinci-units": "1.5.5" | |
}, | |
"name": "html5-canvas-diagram", | |
"version": "1.0.0", | |
"keywords": [ | |
"HTML5", | |
"Canvas", | |
"Diagram", | |
"2D", | |
"mathdoodle" | |
], | |
"author": "David Geo Holmes", | |
"operatorOverloading": true, | |
"hideConfigFiles": true, | |
"linting": true | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Drawable, Diagram } from './diagram.js' | |
import { G2 } from 'davinci-units' | |
export interface Parallelogram extends Drawable { | |
/** | |
* Vector along the first side of the Parallelogram. | |
*/ | |
a: G2 | |
/** | |
* Vector along the second side of the Parallelogram. | |
*/ | |
b: UNITS.G2 | |
} | |
export function createParallelogram(diagram: Diagram): Parallelogram { | |
let a = G2.e1 * 100 | |
let b = G2.e2 * 100 | |
let position = G2.zero | |
const centered = true | |
let attitude = G2.one | |
const draw = function() { | |
const context = diagram.context | |
context.save() | |
const θ = attitude.angle().b | |
diagram.translate(position) | |
diagram.rotate(θ) | |
context.beginPath() | |
if (centered) { | |
const s = (a + b) / 2 | |
const t = (a - b) / 2 | |
diagram.moveTo(-s) | |
diagram.lineTo(+t) | |
diagram.lineTo(+s) | |
diagram.lineTo(-t) | |
} | |
else { | |
diagram.moveTo(position) | |
diagram.lineTo(position + a) | |
diagram.lineTo(position + a + b) | |
diagram.lineTo(position + b) | |
} | |
context.closePath() | |
context.fillStyle = '#d3bea5' | |
context.fill() | |
context.stroke() | |
context.restore() | |
} | |
const that: Parallelogram = { | |
get a() { | |
return a | |
}, | |
set a(value) { | |
// Copy is not a function? | |
a = value | |
}, | |
get b() { | |
return b | |
}, | |
set b(value) { | |
// Copy is not a function? | |
b = value | |
}, | |
get X() { | |
return position | |
}, | |
set X(value: G2) { | |
position = value | |
}, | |
get R() { | |
return attitude | |
}, | |
set R(value) { | |
attitude = value | |
}, | |
draw | |
} | |
diagram.add(that) | |
return Object.freeze(that) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { G2 } from 'davinci-units' | |
export const kilogram = G2.kilogram | |
export const meter = G2.meter | |
export const second = G2.second | |
export const mega = G2.one * 1e+6 | |
export const kilo = G2.one * 1e+3 | |
export const milli = G2.one * 1e-3 | |
export const micro = G2.one * 1e-6 | |
export const nano = G2.one * 1e-9 | |
export const pico = G2.one * 1e-12 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Drawable, Diagram } from './diagram.js' | |
import { G2 } from 'davinci-units' | |
export interface Point extends Drawable { | |
} | |
export function createPoint(diagram: Diagram): Point { | |
let position = G2.zero | |
// const attitude = G2.one; | |
const draw = function() { | |
// const context = diagram.context; | |
diagram.point(position) | |
} | |
const that: Point = { | |
get X() { | |
return position | |
}, | |
set X(value) { | |
position = value | |
}, | |
get R() { | |
return G2.one | |
}, | |
draw | |
} | |
diagram.add(that) | |
return Object.freeze(that) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createDiagram } from './diagram.js' | |
import { createDirectedLineSegment } from './DirectedLineSegment.js' | |
import { createDirectedAreaElement } from './DirectedAreaElement.js' | |
import { createPoint } from './Point.js' | |
import { e1, e2 } from './math.js' | |
const diagram = createDiagram('diagram') | |
const dArea = createDirectedAreaElement(diagram) | |
createPoint(diagram) | |
const dLine = createDirectedLineSegment(diagram) | |
dLine.centered = false | |
dArea.a = e1 + 0.5 * e2 | |
dArea.b = e2 - 0.2 * e1 | |
dLine.a = -e2 | |
const animate = function() { | |
diagram.clear() | |
diagram.draw() | |
requestAnimationFrame(animate) | |
} | |
requestAnimationFrame(animate) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
background-color: blue; | |
} | |
#diagram { | |
background-color: transparent; | |
position: absolute; | |
left: 15px; | |
top: 15px; | |
border: 1px solid black; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Show hidden characters
{ | |
"allowJs": true, | |
"checkJs": true, | |
"declaration": true, | |
"emitDecoratorMetadata": true, | |
"experimentalDecorators": true, | |
"jsx": "react", | |
"module": "system", | |
"noImplicitAny": true, | |
"noImplicitReturns": true, | |
"noImplicitThis": true, | |
"noUnusedLocals": true, | |
"noUnusedParameters": true, | |
"preserveConstEnums": true, | |
"removeComments": false, | |
"skipLibCheck": true, | |
"sourceMap": true, | |
"strictNullChecks": true, | |
"suppressImplicitAnyIndexErrors": true, | |
"target": "es5", | |
"traceResolution": true | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"rules": { | |
"array-type": [ | |
true, | |
"array" | |
], | |
"curly": false, | |
"comment-format": [ | |
true, | |
"check-space" | |
], | |
"eofline": true, | |
"forin": true, | |
"jsdoc-format": true, | |
"new-parens": true, | |
"no-conditional-assignment": false, | |
"no-consecutive-blank-lines": true, | |
"no-construct": true, | |
"no-for-in-array": true, | |
"no-inferrable-types": [ | |
true | |
], | |
"no-magic-numbers": false, | |
"no-shadowed-variable": true, | |
"no-string-throw": true, | |
"no-trailing-whitespace": [ | |
true, | |
"ignore-jsdoc" | |
], | |
"no-var-keyword": true, | |
"one-variable-per-declaration": [ | |
true, | |
"ignore-for-loop" | |
], | |
"prefer-const": true, | |
"prefer-for-of": true, | |
"prefer-function-over-method": false, | |
"prefer-method-signature": true, | |
"radix": true, | |
"semicolon": [ | |
true, | |
"never" | |
], | |
"trailing-comma": [ | |
true, | |
{ | |
"multiline": "never", | |
"singleline": "never" | |
} | |
], | |
"triple-equals": true, | |
"use-isnan": true | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment