Skip to content

Commit

Permalink
move to mutable state, dirty-checking and renderloop
Browse files Browse the repository at this point in the history
This commit makes the code quite simpler removing all the callback
passing and uses a mutable opaque type as the state. Edit operations on
the state mark it as dirty, causing a redraw during a rAF render loop.

On my device this results in noticeably better response times by the canvas
when drawing new lines.
  • Loading branch information
pigoz committed Oct 8, 2018
1 parent d835c4a commit 3aefd6b
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 174 deletions.
95 changes: 68 additions & 27 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,84 @@
import { Newtype, _iso } from './newtype';

export type Point = {
x: number;
y: number;
readonly x: number;
readonly y: number;
};

type Priv = Array<Point[]>;
type State = Priv & { _T: 'State' };
type S = {
lines: Array<Point[]>;
drawing: Point[];
dirty: boolean;
};

const store = window.localStorage || window.sessionStorage;
export interface State extends Newtype<{ readonly State: unique symbol }, S> {}

export const load = (): State => JSON.parse(store.getItem('state') || '[]');
const iso = _iso<State>();
const db = window.localStorage || window.sessionStorage;
const dump = JSON.stringify;
const parse = JSON.parse;

export const save = (x: State): void => {
store.setItem('state', JSON.stringify(x));
};
function save(x: S): void {
db.setItem('state', dump(x));
}

export const empty = (): State => JSON.parse('[]');
export function load(): State {
const item = db.getItem('state');
return item ? iso.wrap({ ...parse(item), dirty: true }) : empty();
}

const cast = (x: Priv): State => x as State;
export function empty(): State {
const result: S = {
lines: [],
drawing: [],
dirty: true,
};

export const map = (state: State, cb: (p: Point) => Point): State =>
cast(state.map(l => l.map(z => cb(z))));
save(result);
return iso.wrap(result);
}

export const iterator = (state: State) => (cb: (p: Point[]) => void): void => {
state.forEach(cb);
};
export function map(s: State, cb: (x: Point) => Point): State {
const state = iso.unwrap(s);
const dup: S = parse(dump(state));
dup.lines = dup.lines.map(l => l.map(cb));
return iso.wrap(dup);
}

export const handleAdd = (state: State) => (l: Point[]) => {
state.push(l);
export function undo(s: State): void {
const state = iso.unwrap(s);
state.lines.splice(-1, 1);
state.dirty = true;
save(state);
};
}

export const handleUndo = (state: State) => () => {
state.splice(-1, 1);
export function clear(s: State): void {
const state = iso.unwrap(s);
state.lines.splice(0, state.lines.length);
state.dirty = true;
save(state);
return iterator(state);
};
}

export const handleClear = (state: State) => () => {
state.splice(0, state.length);
export function addDrawingPoint(s: State, p: Point): void {
const state = iso.unwrap(s);
state.drawing.push(p);
state.dirty = true;
save(state);
return iterator(state);
};
}

export function addLastDrawingPoing(s: State, p: Point): void {
const state = iso.unwrap(s);
state.drawing.push(p);
state.lines.push(state.drawing);
state.drawing = [];
state.dirty = true;
save(state);
}

export function willdisplay(s: State, cb: (lines: Array<Point[]>) => boolean) {
const state = iso.unwrap(s);
if (state.dirty) {
const successful = cb([...state.lines, state.drawing]);
state.dirty = !successful;
}
}
12 changes: 5 additions & 7 deletions src/back.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as hs from 'hyperscript';
import * as styles from './styles';
import { redraw, DEFAULT_CONFIG } from './draw';
import { load, map, iterator } from './app';
import { render } from './dom';
import { render, rendercanvas, DEFAULT_CONFIG } from './render';
import { map, load } from './app';
import { CANVAS_SIZE, RESULT_SIZE } from './constants';

const h = hs.context();
Expand All @@ -16,7 +15,6 @@ const canvas = h('canvas', {

render('ac-back', canvas);

const scaledstate = map(load(), z => ({ x: z.x * RATIO, y: z.y * RATIO }));
const scaledconfig = { lineWidth: DEFAULT_CONFIG.lineWidth * RATIO };
const redrawer = redraw(canvas, scaledconfig)(() => iterator(scaledstate));
redrawer();
const state = map(load(), z => ({ x: z.x * RATIO, y: z.y * RATIO }));
const config = { lineWidth: DEFAULT_CONFIG.lineWidth * RATIO };
rendercanvas(canvas, state, config);
15 changes: 0 additions & 15 deletions src/dom.ts

This file was deleted.

100 changes: 0 additions & 100 deletions src/draw.ts

This file was deleted.

91 changes: 67 additions & 24 deletions src/front.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { HDPI_FACTOR, CANVAS_SIZE } from './constants';
import * as hs from 'hyperscript';
import { render } from './dom';
import { render, rendercanvas } from './render';
import {
Point,
State,
empty,
addDrawingPoint,
addLastDrawingPoing,
undo,
clear,
} from './app';
import * as styles from './styles';
import { CANVAS_SIZE } from './constants';
import * as icons from './icons';

import { handleAdd, handleUndo, handleClear, save, empty } from './app';
import {
handleStart,
handleMove,
handleEnd,
handleCancel,
redraw,
} from './draw';

const h = hs.context();

const canvas = h('canvas', {
Expand All @@ -21,23 +21,66 @@ const canvas = h('canvas', {
height: CANVAS_SIZE,
});

const undo = h('button', { style: styles.action });
undo.innerHTML = icons.undo;
const clear = h('button', { style: styles.action });
clear.innerHTML = icons.clear;
const buttons = {
undo: h('button', { style: styles.action }),
clear: h('button', { style: styles.action }),
};

const actions = h('div', { style: styles.actions }, [clear, undo]);
const actions = h('div', { style: styles.actions }, Object.values(buttons));
const T = h('div', { style: styles.wrapper }, [canvas, actions]);

render('ac-front', T);

const state = empty();
save(state); // reset saved state on reinit

undo.addEventListener('click', redraw(canvas)(handleUndo(state)), false);
clear.addEventListener('click', redraw(canvas)(handleClear(state)), false);
canvas.addEventListener('touchstart', handleStart(canvas), false);
canvas.addEventListener('touchend', handleEnd(canvas, handleAdd(state)), false);
canvas.addEventListener('touchcancel', handleCancel(), false);
canvas.addEventListener('touchmove', handleMove(canvas), false);

const hdl = (
canvas: HTMLCanvasElement,
state: State,
action: (state: State, p: Point) => void,
) => (evt: Event): void => {
evt.preventDefault();

if (!(evt instanceof TouchEvent)) {
return;
}

const touches = evt.changedTouches;
const touch = touches[0];
const point: Point = {
x: (touch.pageX - canvas.offsetLeft) * HDPI_FACTOR,
y: (touch.pageY - canvas.offsetTop) * HDPI_FACTOR,
};

action(state, point);
};

canvas.addEventListener(
'touchstart',
hdl(canvas, state, addDrawingPoint),
false,
);

canvas.addEventListener(
'touchmove',
hdl(canvas, state, addDrawingPoint),
false,
);

canvas.addEventListener(
'touchend',
hdl(canvas, state, addLastDrawingPoing),
false,
);

function renderloop() {
rendercanvas(canvas, state);
requestAnimationFrame(renderloop);
}

renderloop();

canvas.addEventListener('click', e => e.preventDefault(), false);
buttons.undo.addEventListener('click', () => undo(state), false);
buttons.clear.addEventListener('click', () => clear(state), false);
buttons.undo.innerHTML = icons.undo;
buttons.clear.innerHTML = icons.clear;
23 changes: 23 additions & 0 deletions src/newtype.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// barebones version of https://github.com/gcanti/newtype-ts

export interface Newtype<URI, A> {
_URI: URI;
_A: A;
}

export interface Iso<S, A> {
unwrap: (s: S) => A;
wrap: (a: A) => S;
}

function identity<T>(x: T): T {
return x;
}

export const _iso = <S extends Newtype<any, any> = never>(): Iso<
S,
S['_A']
> => ({
unwrap: identity,
wrap: identity,
});
Loading

0 comments on commit 3aefd6b

Please # to comment.