canvas.js (7.1 KB)


  1 const view = document.getElementById('view');
  2 const vctx = view.getContext('2d', { alpha: false });
  3 
  4 const CHUNK = 256;
  5 let frameSeq = 0;
  6 // frames carry a stable id so collaborators can name the same frame even
  7 // as the list shifts under inserts and deletes
  8 function newFrameMap(id) {
  9 	const m = new Map();
 10 	m.id = id || ('l' + (++frameSeq));
 11 	return m;
 12 }
 13 let frames = [newFrameMap()];
 14 let frameIdx = 0;
 15 let chunks = frames[0];
 16 let dir = 1; // playback direction: 1 forward, -1 reverse
 17 let onionskin = true;
 18 // how anything belonging to another frame reads: onionskinned content and
 19 // the brushes of painters standing on it
 20 const GHOST_ALPHA = 0.35;
 21 let playing = false;
 22 let playTimer = null;
 23 let frameInterval = 250;
 24 let lastTap = 0;
 25 
 26 function chunkKey(cx, cy) { return cx + ',' + cy; }
 27 
 28 // remote strokes redirect every paint write into the frame they belong to
 29 let paintMap = null;
 30 
 31 function getOrCreateChunk(cx, cy, map) {
 32 	map = map || paintMap || chunks;
 33 	const k = chunkKey(cx, cy);
 34 	let c = map.get(k);
 35 	if (c) return c;
 36 	const cnv = document.createElement('canvas');
 37 	cnv.width = CHUNK;
 38 	cnv.height = CHUNK;
 39 	// overlay chunks stay transparent so they composite over the frame
 40 	const cctx = cnv.getContext('2d', { alpha: !!map.overlay });
 41 	if (!map.overlay) {
 42 		cctx.fillStyle = '#000';
 43 		cctx.fillRect(0, 0, CHUNK, CHUNK);
 44 	}
 45 	c = { canvas: cnv, ctx: cctx };
 46 	map.set(k, c);
 47 	return c;
 48 }
 49 
 50 // unacked local strokes paint here, on top of the frame, until their echo
 51 // commits them into it in the room's order
 52 function overlayFor(fid) {
 53 	let m = net.overlays.get(fid);
 54 	if (!m) {
 55 		m = new Map();
 56 		m.id = fid;
 57 		m.overlay = true;
 58 		net.overlays.set(fid, m);
 59 	}
 60 	return m;
 61 }
 62 
 63 function paintRect(wx, wy, w, h, color) {
 64 	const x0 = wx, y0 = wy, x1 = wx + w, y1 = wy + h;
 65 	const cx0 = Math.floor(x0 / CHUNK);
 66 	const cy0 = Math.floor(y0 / CHUNK);
 67 	const cx1 = Math.floor((x1 - 1) / CHUNK);
 68 	const cy1 = Math.floor((y1 - 1) / CHUNK);
 69 	for (let cy = cy0; cy <= cy1; cy++) {
 70 		for (let cx = cx0; cx <= cx1; cx++) {
 71 			const c = getOrCreateChunk(cx, cy);
 72 			const lx = Math.max(x0, cx * CHUNK) - cx * CHUNK;
 73 			const ly = Math.max(y0, cy * CHUNK) - cy * CHUNK;
 74 			const rx = Math.min(x1, (cx + 1) * CHUNK) - cx * CHUNK;
 75 			const ry = Math.min(y1, (cy + 1) * CHUNK) - cy * CHUNK;
 76 			c.ctx.fillStyle = color;
 77 			c.ctx.fillRect(lx, ly, rx - lx, ry - ly);
 78 		}
 79 	}
 80 }
 81 
 82 function readPixel(wx, wy) {
 83 	const cx = Math.floor(wx / CHUNK);
 84 	const cy = Math.floor(wy / CHUNK);
 85 	const k = chunkKey(cx, cy);
 86 	const lx = wx - cx * CHUNK;
 87 	const ly = wy - cy * CHUNK;
 88 	// an unacked stroke sits on the overlay; pick what's actually on screen
 89 	const ov = net.overlays.get(chunks.id);
 90 	const oc = ov && ov.get(k);
 91 	if (oc) {
 92 		const d = oc.ctx.getImageData(lx, ly, 1, 1).data;
 93 		if (d[3] === 255) return [d[0], d[1], d[2]];
 94 	}
 95 	const c = chunks.get(k);
 96 	if (!c) return [0, 0, 0];
 97 	const d = c.ctx.getImageData(lx, ly, 1, 1).data;
 98 	return [d[0], d[1], d[2]];
 99 }
100 
101 function setFrame(i) {
102 	frameIdx = ((i % frames.length) + frames.length) % frames.length;
103 	chunks = frames[frameIdx];
104 	// a held pick tracks the frame under it as frames change
105 	if (picking) pickAt(curX, curY);
106 	requestDraw();
107 }
108 
109 // frame structure changes round-trip through the server when connected, so
110 // every painter ends up with the same list in the same order
111 function addFrame() {
112 	const at = dir === 1 ? frameIdx + 1 : frameIdx;
113 	if (net.on) { net.send({ t: 'af', at }); return; }
114 	applyAddFrame(at, null, true);
115 }
116 
117 function applyAddFrame(at, id, mine) {
118 	at = clamp(at, 0, frames.length);
119 	const cur = chunks;
120 	frames.splice(at, 0, newFrameMap(id));
121 	setFrame(mine ? at : frames.indexOf(cur));
122 }
123 
124 function deleteFrame() {
125 	if (frames.length === 1) return;
126 	if (net.on) { net.send({ t: 'df', id: chunks.id }); return; }
127 	applyDeleteFrame(chunks.id);
128 }
129 
130 function applyDeleteFrame(id) {
131 	if (frames.length === 1) return;
132 	const i = frames.findIndex((f) => f.id === id);
133 	if (i < 0) return;
134 	const cur = chunks;
135 	frames.splice(i, 1);
136 	if (playing && frames.length === 1) stopPlayback();
137 	const j = frames.indexOf(cur);
138 	setFrame(j >= 0 ? j : Math.min(i, frames.length - 1));
139 }
140 
141 function stopPlayback() {
142 	playing = false;
143 	clearTimeout(playTimer);
144 	requestDraw();
145 }
146 
147 function startPlayback() {
148 	if (frames.length === 1) return;
149 	playing = true;
150 	playTimer = setTimeout(function step() {
151 		setFrame(frameIdx + dir);
152 		playTimer = setTimeout(step, frameInterval);
153 	}, frameInterval);
154 }
155 
156 function tapArrow(d) {
157 	dir = d;
158 	const now = performance.now();
159 	if (lastTap && now - lastTap <= 10000) {
160 		frameInterval = now - lastTap;
161 		// stored, not applied to anyone live: whoever joins next inherits it
162 		net.send({ t: 'interval', v: Math.round(frameInterval) });
163 	}
164 	lastTap = now;
165 	if (playing) { stopPlayback(); return; }
166 	setFrame(frameIdx + d);
167 }
168 
169 const keys = {};
170 let painting = false;
171 let picking = false;
172 let lastPaintX = null, lastPaintY = null;
173 let dirty = false;
174 
175 function brushTopLeft(cx, cy) {
176 	const off = Math.floor(brush / 2);
177 	return { x: cx - off, y: cy - off };
178 }
179 
180 function paintAt(cx, cy) {
181 	dirty = true;
182 	const tl = brushTopLeft(cx, cy);
183 	if (roundness === 0) {
184 		paintRect(tl.x, tl.y, brush, brush, 'rgb(' + r + ',' + g + ',' + b + ')');
185 		return;
186 	}
187 	const sprite = getBrushShape().sprite;
188 	const cx0 = Math.floor(tl.x / CHUNK);
189 	const cy0 = Math.floor(tl.y / CHUNK);
190 	const cx1 = Math.floor((tl.x + brush - 1) / CHUNK);
191 	const cy1 = Math.floor((tl.y + brush - 1) / CHUNK);
192 	for (let ccy = cy0; ccy <= cy1; ccy++) {
193 		for (let ccx = cx0; ccx <= cx1; ccx++) {
194 			const c = getOrCreateChunk(ccx, ccy);
195 			c.ctx.drawImage(sprite, tl.x - ccx * CHUNK, tl.y - ccy * CHUNK);
196 		}
197 	}
198 }
199 
200 // every mark the local user makes goes out as a segment; a dot
201 // is just a zero-length one. in a room the pixels land on the frame's
202 // overlay - the frame itself only takes strokes in the server's order
203 function localPaintAt(x, y) {
204 	if (net.on) paintMap = overlayFor(chunks.id);
205 	try {
206 		paintAt(x, y);
207 	} finally {
208 		paintMap = null;
209 	}
210 	net.paint(x, y, x, y);
211 }
212 
213 function localPaintLine(x0, y0, x1, y1) {
214 	if (net.on) paintMap = overlayFor(chunks.id);
215 	try {
216 		paintLine(x0, y0, x1, y1);
217 	} finally {
218 		paintMap = null;
219 	}
220 	net.paint(x0, y0, x1, y1);
221 }
222 
223 // replay someone else's segment into whichever frame it belongs to, with
224 // their brush, without disturbing the local one
225 function applyPaintOp(op) {
226 	const f = frames.find((fr) => fr.id === op.f);
227 	if (!f || !op.o || !op.c) return;
228 	const pm = paintMap, pr = r, pg = g, pb = b, pbrush = brush, pround = roundness;
229 	paintMap = f;
230 	r = op.c[0]; g = op.c[1]; b = op.c[2];
231 	brush = op.b; roundness = op.s;
232 	try {
233 		for (let i = 0; i + 3 < op.o.length; i += 4) {
234 			paintLine(op.o[i], op.o[i + 1], op.o[i + 2], op.o[i + 3]);
235 		}
236 	} finally {
237 		paintMap = pm;
238 		r = pr; g = pg; b = pb;
239 		brush = pbrush; roundness = pround;
240 	}
241 	if (f === chunks) requestDraw();
242 }
243 
244 function paintLine(x0, y0, x1, y1) {
245 	let dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
246 	let dy = -Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
247 	let err = dx + dy;
248 	let x = x0, y = y0;
249 	while (true) {
250 		paintAt(x, y);
251 		if (x === x1 && y === y1) break;
252 		const e2 = 2 * err;
253 		if (e2 >= dy) { err += dy; x += sx; }
254 		if (e2 <= dx) { err += dx; y += sy; }
255 	}
256 }