brush.js (4.4 KB)


  1 let r = 255, g = 255, b = 255;
  2 let brush = 1;
  3 let roundness = 0;
  4 
  5 // keyed cache: the local brush and every peer's brush pull from the same one
  6 const brushShapes = new Map();
  7 const BRUSH_CACHE_MAX = 64;
  8 function getBrushShape() { return brushShapeFor(brush, roundness, r, g, b); }
  9 function brushShapeFor(n, round, sr, sg, sb) {
 10 	const key = n + ',' + round + ',' + sr + ',' + sg + ',' + sb;
 11 	const hit = brushShapes.get(key);
 12 	if (hit) return hit;
 13 	if (brushShapes.size >= BRUSH_CACHE_MAX) brushShapes.clear();
 14 	const inside = new Uint8Array(n * n);
 15 	const rad = round * (n / 2);
 16 	const rad2 = rad * rad;
 17 	const lo = rad - 0.5;
 18 	const hi = n - 0.5 - rad;
 19 	for (let dy = 0; dy < n; dy++) {
 20 		for (let dx = 0; dx < n; dx++) {
 21 			let qx = 0, qy = 0;
 22 			if (dx < lo) qx = lo - dx;
 23 			else if (dx > hi) qx = dx - hi;
 24 			if (dy < lo) qy = lo - dy;
 25 			else if (dy > hi) qy = dy - hi;
 26 			if (qx * qx + qy * qy <= rad2 + 1e-9) inside[dy * n + dx] = 1;
 27 		}
 28 	}
 29 	const sprite = document.createElement('canvas');
 30 	sprite.width = n;
 31 	sprite.height = n;
 32 	const sctx = sprite.getContext('2d');
 33 	const img = sctx.createImageData(n, n);
 34 	const data = img.data;
 35 	for (let i = 0; i < n * n; i++) {
 36 		if (inside[i]) {
 37 			data[i * 4] = sr;
 38 			data[i * 4 + 1] = sg;
 39 			data[i * 4 + 2] = sb;
 40 			data[i * 4 + 3] = 255;
 41 		}
 42 	}
 43 	sctx.putImageData(img, 0, 0);
 44 
 45 	const fillRuns = [];
 46 	for (let dy = 0; dy < n; dy++) {
 47 		let dx = 0;
 48 		while (dx < n) {
 49 			if (!inside[dy * n + dx]) { dx++; continue; }
 50 			let dx1 = dx + 1;
 51 			while (dx1 < n && inside[dy * n + dx1]) dx1++;
 52 			fillRuns.push(dx, dy, dx1 - dx);
 53 			dx = dx1;
 54 		}
 55 	}
 56 
 57 	// 1-pixel outline ring: cells outside the shape that are 8-way adjacent
 58 	// to any inside cell. stored as row-runs in an (n+2)x(n+2) grid with
 59 	// coordinates offset by -1 so they index directly in shape-local space.
 60 	const m = n + 2;
 61 	const ring = new Uint8Array(m * m);
 62 	const isIn = (x, y) => x >= 0 && y >= 0 && x < n && y < n && inside[y * n + x] === 1;
 63 	for (let y = -1; y <= n; y++) {
 64 		for (let x = -1; x <= n; x++) {
 65 			if (isIn(x, y)) continue;
 66 			let adj = false;
 67 			for (let oy = -1; oy <= 1 && !adj; oy++) {
 68 				for (let ox = -1; ox <= 1 && !adj; ox++) {
 69 					if (ox === 0 && oy === 0) continue;
 70 					if (isIn(x + ox, y + oy)) adj = true;
 71 				}
 72 			}
 73 			if (adj) ring[(y + 1) * m + (x + 1)] = 1;
 74 		}
 75 	}
 76 	const outlineRuns = [];
 77 	for (let y = 0; y < m; y++) {
 78 		let x = 0;
 79 		while (x < m) {
 80 			if (!ring[y * m + x]) { x++; continue; }
 81 			let x1 = x + 1;
 82 			while (x1 < m && ring[y * m + x1]) x1++;
 83 			outlineRuns.push(x - 1, y - 1, x1 - x);
 84 			x = x1;
 85 		}
 86 	}
 87 
 88 	const shape = { inside, sprite, fillRuns, outlineRuns, n };
 89 	brushShapes.set(key, shape);
 90 	return shape;
 91 }
 92 
 93 // outline blend: 0 = dark color (lighten with screen), 1 = light color (darken with multiply).
 94 let outlineLightness = 1;
 95 let outlineAnimStart = 0;
 96 let outlineAnimFrom = 1;
 97 let outlineAnimTo = 1;
 98 const OUTLINE_ANIM_MS = 250;
 99 function setOutlineTarget(target) {
100 	if (target === outlineAnimTo) return;
101 	outlineAnimFrom = outlineLightness;
102 	outlineAnimTo = target;
103 	outlineAnimStart = performance.now();
104 	requestDraw();
105 }
106 
107 // displayed cursor fill eases toward r/g/b; painting still uses r/g/b immediately.
108 let dispR = 255, dispG = 255, dispB = 255;
109 let colorAnimStart = 0;
110 let colorAnimFromR = 255, colorAnimFromG = 255, colorAnimFromB = 255;
111 let colorAnimToR = 255, colorAnimToG = 255, colorAnimToB = 255;
112 function setDisplayColorTarget(nr, ng, nb) {
113 	if (nr === colorAnimToR && ng === colorAnimToG && nb === colorAnimToB) return;
114 	colorAnimFromR = dispR; colorAnimFromG = dispG; colorAnimFromB = dispB;
115 	colorAnimToR = nr; colorAnimToG = ng; colorAnimToB = nb;
116 	colorAnimStart = performance.now();
117 	requestDraw();
118 }
119 
120 const faviconEl = document.getElementById('favicon');
121 function updateFavicon() {
122 	const svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'><rect width='1' height='1' fill='rgb(" + r + "," + g + "," + b + ")'/></svg>";
123 	faviconEl.href = 'data:image/svg+xml,' + encodeURIComponent(svg);
124 }
125 
126 function pickAt(cx, cy) {
127 	const c = readPixel(cx, cy);
128 	r = c[0]; g = c[1]; b = c[2];
129 	setOutlineTarget((r + g + b > 384) ? 1 : 0);
130 	setDisplayColorTarget(r, g, b);
131 	updateFavicon();
132 }
133 
134 function setColor(nr, ng, nb) {
135 	r = nr; g = ng; b = nb;
136 	setOutlineTarget((r + g + b > 384) ? 1 : 0);
137 	updateFavicon();
138 	dispR = r; dispG = g; dispB = b;
139 	colorAnimFromR = r; colorAnimFromG = g; colorAnimFromB = b;
140 	colorAnimToR = r; colorAnimToG = g; colorAnimToB = b;
141 }