Skip to content

Instantly share code, notes, and snippets.

@mathdoodle
Last active February 18, 2018 23:13
Show Gist options
  • Save mathdoodle/2922b500e6c4b365aa850e456f0caa4d to your computer and use it in GitHub Desktop.
Save mathdoodle/2922b500e6c4b365aa850e456f0caa4d to your computer and use it in GitHub Desktop.
HTML5 Canvas Diagram

HTML5 Canvas Diagram

Overview

A lightweight API for using the HTML5 Canvas as a diagram.

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)
}
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)
}
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)
}
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)
}
<!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>
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
{
"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
}
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)
}
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
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)
}
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)
body {
background-color: blue;
}
#diagram {
background-color: transparent;
position: absolute;
left: 15px;
top: 15px;
border: 1px solid black;
}
{
"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
}
{
"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