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 }