Skip to content

Instantly share code, notes, and snippets.

@Garciat
Last active December 5, 2018 22:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Garciat/28062d6de32094bc18a64923e4bdf231 to your computer and use it in GitHub Desktop.
Save Garciat/28062d6de32094bc18a64923e4bdf231 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Mandala</title>
<style type="text/css">
html, body {
overflow: hidden;
width: 100%;
height: 100%;
margin: 0px;
}
body {
background: #888888;
}
</style>
</head>
<body>
<script>
'use strict';
const TAU = 2 * Math.PI;
// ---
class Canvas {
static fromDimensions(w, h) {
let canvas = document.createElement('canvas');
canvas.style.touchAction = 'none';
canvas.width = w;
canvas.height = h;
return canvas;
}
static fromImage(image) {
let copy = Canvas.fromDimensions(image.width, image.height);
copy.getContext('2d').drawImage(image, 0, 0);
return copy;
}
static fromCanvas(canvas) {
let copy = Canvas.fromDimensions(canvas.width, canvas.height);
copy.getContext('2d').drawImage(canvas, 0, 0);
return copy;
}
static async fromURL(url) {
return Canvas.fromImage(await loadImageAsync(url));
}
static async toURL(canvas) {
return canvas.toDataURL('image/png');
}
}
function loadImageAsync(url) {
return new Promise((resolve, reject) => {
let img = new window.Image();
img.onload = () => resolve(img);
img.src = url;
});
}
async function asyncArrayMap(xs, f) {
let ys = [];
for (let x of xs) {
ys.push(await f(x));
}
return ys;
}
function drawImageVec(ctx, src, spos, sdiag, dpos, ddiag) {
ctx.drawImage(
src,
spos.x,
-spos.y,
sdiag.x,
-sdiag.y,
dpos.x,
-dpos.y,
ddiag.x,
-ddiag.y
);
ctx.fillStyle = 'red';
ctx.fillText(spos.toString(), 0, 10);
ctx.fillText(sdiag.toString(), 0, 30);
}
function fillCanvas(canvas, color) {
let ctx = canvas.getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function fillCircle(ctx, pos, r) {
ctx.beginPath();
ctx.arc(pos.x, -pos.y, r, 0, TAU);
ctx.fill();
}
// ---
class Layer {
constructor(canvas) {
this.canvas = canvas;
}
get w() { return this.canvas.width; }
get h() { return this.canvas.height; }
get diag() {
return Vec2.fromWH(this.w, this.h);
}
clone() {
return new Layer(Canvas.fromCanvas(this.canvas));
}
static fromDimensions(w, h) {
return new Layer(Canvas.fromDimensions(w, h));
}
// ---
async toJSON() {
return {
canvasURL: await Canvas.toURL(this.canvas)
};
}
static async fromJSON({canvasURL}) {
return new Layer(await Canvas.fromURL(canvasURL));
}
}
// ---
class LayerStack {
constructor(layers) {
this.layers = layers || [];
}
push(layer) {
this.layers.push(layer);
}
clone() {
return new LayerStack(
this.layers.map(layer => layer.clone())
);
}
*[Symbol.iterator]() {
for (let layer of this.layers) {
yield layer;
}
}
// ---
async toJSON() {
return {
layers: await asyncArrayMap(this.layers, layer => layer.toJSON())
};
}
static async fromJSON({layers}) {
return new LayerStack(
await asyncArrayMap(layers, layer => Layer.fromJSON(layer))
);
}
}
// ---
const BACKDROP_COLOR = '#888888';
class Viewport {
constructor(x, y, zoom) {
this.pos = new Vec2(x, y);
this.zoom = zoom;
}
get x() { return this.pos.x; }
get y() { return this.pos.y; }
panBy(ds) {
this.pos.mutSub(ds);
}
zoomAtOriginBy(origin, dz) {
this.zoom *= dz;
this.pos.mutAdd(origin);
this.pos.mutScale(1 / dz);
this.pos.mutSub(origin);
}
render(layerStack, display) {
display.ctx.clearRect(0, 0, display.w, display.h);
for (let layer of layerStack) {
drawImageVec(
display.ctx,
layer.canvas,
this.pos.scaled(this.zoom),
display.diag.scaled(this.zoom),
Vec2.ZERO,
display.diag
);
}
}
// ---
async toJSON() {
return {
x: this.x,
y: this.y,
zoom: this.zoom
};
}
static async fromJSON({x, y, zoom}) {
return new Viewport(x, y, zoom);
}
}
// ---
// Resolution is 1:1 with device screen
class Display {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
}
get w() { return this.canvas.width; }
get h() { return this.canvas.height; }
get diag() {
return Vec2.fromWH(this.w, this.h);
}
resize(w, h) {
this.canvas.width = w;
this.canvas.height = h;
}
}
// ---
class Minimap {
constructor(canvas) {
this.canvas = canvas;
}
get w() { return this.canvas.width; }
get h() { return this.canvas.height; }
}
// ---
class PointerBuffer {
constructor() {
this.items = new CircularBuffer(5);
}
addEvent(ev) {
this.items.prepend(Vec2.fromPointerEvent(ev));
}
get curr() {
return this.items.at(0);
}
get prev() {
return this.items.at(1) || this.curr;
}
get delta() {
return this.curr.sub(this.prev);
}
static fromEvent(ev) {
let buffer = new PointerBuffer();
buffer.addEvent(ev);
return buffer;
}
}
// ---
class Application {
constructor() {
this.display = new Display(Canvas.fromDimensions(500, 500));
this.viewport = new Viewport(0, 0, 1);
this.layerStack = new LayerStack();
}
async initialize() {
let w = window.innerWidth
let h = window.innerHeight;
this.display.resize(w, h);
// this.layerStack.push(new Layer(await Canvas.fromURL('big.jpg')));
let background = Canvas.fromDimensions(500, 500);
fillCanvas(background, 'white');
this.layerStack.push(new Layer(background));
// this.viewport.pos.x = -w/2 + 250;
// this.viewport.pos.y = h/2 - 250;
document.body.appendChild(this.display.canvas);
}
render() {
this.viewport.render(this.layerStack, this.display);
}
}
// ---
function isZoomJitter(dz) {
return Math.abs(1 - dz) <= 0.01;
}
window.onerror = function(message, source, lineno, colno, error) {
alert(message);
};
window.addEventListener('load', async function () {
let app = new Application();
await app.initialize();
let buffers = new Map();
let paintBuffer = new CircularBuffer(100);
app.display.canvas.addEventListener('pointerdown', ev => {
app.display.canvas.setPointerCapture(ev.pointerId);
buffers.set(ev.pointerId, PointerBuffer.fromEvent(ev));
});
app.display.canvas.addEventListener('pointermove', ev => {
if (!buffers.has(ev.pointerId)) return;
switch (buffers.size) {
case 1: {
if (ev.pointerType === 'pen') {
paintBuffer.append(
{ev: ev, f: ev.pressure}
);
} else {
// app.viewport.panBy(bufferA.delta);
}
} break;
case 2: {
let bufferA = buffers.get(ev.pointerId);
bufferA.addEvent(ev);
let [bufferP, bufferQ] = buffers.values();
let currP = bufferP.curr;
let currQ = bufferQ.curr;
let prevP = bufferP.prev;
let prevQ = bufferQ.prev;
let currC = currP.midwayWith(currQ);
let dz = prevP.distanceTo(prevQ) / currP.distanceTo(currQ);
if (!isZoomJitter(dz)) {
app.viewport.zoomAtOriginBy(currC, dz);
}
if (ev.isPrimary) {
let prevC = prevP.midwayWith(prevQ);
app.viewport.panBy(currC.sub(prevC));
}
} break;
}
});
app.display.canvas.addEventListener('pointerup', ev => {
app.display.canvas.releasePointerCapture(ev.pointerId);
buffers.delete(ev.pointerId);
});
app.display.canvas.addEventListener('pointercancel', ev => {
app.display.canvas.releasePointerCapture(ev.pointerId);
buffers.delete(ev.pointerId);
});
function loop() {
for (let pp of paintBuffer) {
let p = Vec2.fromPointerEvent(pp.ev);
let ctx = app.layerStack.layers[0].canvas.getContext('2d');
let p_ = p.add(app.viewport.pos).scaled(app.viewport.zoom);
ctx.fillStyle = 'black';
fillCircle(ctx, p_, 3 * pp.f);
}
paintBuffer.clear();
app.render();
requestAnimationFrame(loop);
}
loop();
});
// ---
class Vec2 {
constructor(x, y) {
this.x = x;
this.y = y;
}
clone() {
return new Vec2(this.x, this.y);
}
neg() {
return this.clone().mutNeg();
}
add(v) {
return this.clone().mutAdd(v);
}
sub(v) {
return this.clone().mutSub(v);
}
scaled(k) {
return this.clone().mutScale(k);
}
mutNeg() {
this.x *= -1;
this.y *= -1;
return this;
}
mutAdd(v) {
this.x += v.x;
this.y += v.y;
return this;
}
mutSub(v) {
this.x -= v.x;
this.y -= v.y;
return this;
}
mutScale(k) {
this.x *= k;
this.y *= k;
return this;
}
atan2() {
return Math.atan2(this.y, this.x);
}
angle() {
let a = this.atan2();
return a >= 0 ? a : (TAU + a);
}
lengthSq() {
return this.x*this.x + this.y*this.y;
}
length() {
return Math.sqrt(this.lengthSq());
}
distanceTo(v) {
return this.sub(v).length();
}
midwayWith(v) {
return this.add(v).scaled(0.5);
}
toString() {
return `Vec2(${this.x.toFixed(2)}, ${this.y.toFixed(2)})`;
}
static fromRad(r) {
return new Vec2(Math.cos(r), Math.sin(r));
}
static fromPointerEvent(ev) {
return Vec2.fromWH(ev.clientX, ev.clientY);
}
static fromWH(w, h) {
return new Vec2(w, -h);
}
}
Vec2.ZERO = new Vec2(0, 0);
Vec2.X = new Vec2(1, 0);
Vec2.Y = new Vec2(0, 1);
// ---
class CircularBuffer {
constructor(size) {
this.size = size;
this.items = [];
}
get length() {
return this.items.length;
}
at(ix) {
return this.items[ix];
}
prepend(item) {
this.items.unshift(item);
if (this.items.length > this.size)
this.items.pop();
}
append(item) {
this.items.push(item);
if (this.items.length > this.size)
this.items.shift();
}
clear() {
this.items.length = 0;
}
*[Symbol.iterator]() {
for (let item of this.items) {
yield item;
}
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment