From 2e4be0cf921940915cf668516cea7ace2fcf06ec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:05:51 +0000 Subject: [PATCH] Share one real canvas path for sample, drop, and GIF export --- ventures/doodleloop/app.js | 392 +++++++++++++++---------------------- 1 file changed, 162 insertions(+), 230 deletions(-) diff --git a/ventures/doodleloop/app.js b/ventures/doodleloop/app.js index 1db841f..f2f0576 100644 --- a/ventures/doodleloop/app.js +++ b/ventures/doodleloop/app.js @@ -1,5 +1,6 @@ // Doodleloop — local canvas animation. Sample and user photos share one path. -// GIF bytes are encoded here (LZW + a stored ZIP via CompressionStream). No upload. +// GIF bytes are encoded here (LZW). A deflate ZIP of those bytes is inflated +// with DecompressionStream so the export is checked, not simulated. (function () { "use strict"; @@ -21,7 +22,7 @@ ]; function DoodleloopApp() { - this.source = null; // ImageData of the fitted drawing (the real input) + this.source = null; this.sourceCanvas = null; this.motionStyle = "ink-ripple"; this.intensity = 50; @@ -29,7 +30,6 @@ this.playing = false; this.raf = 0; this.t0 = 0; - this.frame = 0; this.exporting = false; this.snow = []; this.bubbles = []; @@ -46,10 +46,6 @@ this.originalCanvas.height = SIZE; this.animationCanvas.width = SIZE; this.animationCanvas.height = SIZE; - this.work = document.createElement("canvas"); - this.work.width = SIZE; - this.work.height = SIZE; - this.workCtx = this.work.getContext("2d", { willReadFrequently: true }); this.statusEl = document.getElementById("loop-status"); this.bind(); this.seedParticles(); @@ -65,6 +61,7 @@ var speedSlider = document.getElementById("speed-slider"); var exportBtn = document.getElementById("export-btn"); var sampleBtn = document.getElementById("sample-btn"); + var galleryBtn = document.getElementById("gallery-sample"); var webmBtn = document.getElementById("export-webm"); dropZone.addEventListener("dragover", function (e) { @@ -96,6 +93,7 @@ self.speed = parseInt(e.target.value, 10); }); sampleBtn.addEventListener("click", function () { self.loadSample(); }); + if (galleryBtn) galleryBtn.addEventListener("click", function () { self.loadSample(); }); exportBtn.addEventListener("click", function () { self.exportGIF(); }); if (webmBtn) webmBtn.addEventListener("click", function (e) { e.preventDefault(); @@ -196,17 +194,14 @@ if (this.raf) cancelAnimationFrame(this.raf); this.playing = true; this.t0 = performance.now(); - this.frame = 0; this.tick(); }; DoodleloopApp.prototype.tick = function () { var self = this; if (!this.playing || !this.source) return; - var now = performance.now(); - var elapsed = (now - this.t0) / 1000; + var elapsed = (performance.now() - this.t0) / 1000; this.renderFrame(this.animationCtx, elapsed, this.motionStyle, this.intensity, this.speed); - this.frame += 1; this.raf = requestAnimationFrame(function () { self.tick(); }); }; @@ -216,7 +211,6 @@ var spd = 0.35 + clamp01(speed / 100) * 1.85; var t = elapsed * spd; var src = this.source; - ctx.clearRect(0, 0, SIZE, SIZE); ctx.save(); switch (style) { case "ink-ripple": this.fxRipple(ctx, src, t, amt); break; @@ -242,7 +236,7 @@ var c = document.createElement("canvas"); c.width = w; c.height = h; - var g = c.getContext("2d"); + var g = c.getContext("2d", { willReadFrequently: true }); g.fillStyle = "#f4efe4"; g.fillRect(0, 0, w, h); var ir = img.naturalWidth / img.naturalHeight; @@ -264,19 +258,19 @@ } function sampleAt(data, w, h, x, y) { - x = Math.max(0, Math.min(w - 1, x | 0)); - y = Math.max(0, Math.min(h - 1, y | 0)); - var i = (y * w + x) * 4; + var xi = x < 0 ? 0 : x >= w ? w - 1 : x | 0; + var yi = y < 0 ? 0 : y >= h ? h - 1 : y | 0; + var i = (yi * w + xi) * 4; return [data[i], data[i + 1], data[i + 2], data[i + 3]]; } - // Displace every source pixel. This is the real warp used by several styles. + // Displace every source pixel. Preview and export share this warp. function warpPixels(src, map) { var w = src.width, h = src.height; var s = src.data; var out = new ImageData(w, h); var d = out.data; - var x, y, i, j, nx, ny, p; + var x, y, i, nx, ny, p; for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { var m = map(x, y); @@ -297,18 +291,14 @@ var amp = 2 + amt * 10; var warped = warpPixels(src, function (x, y) { var dx = x - SIZE / 2, dy = y - SIZE / 2; - var dist = Math.sqrt(dx * dx + dy * dy); + var dist = Math.sqrt(dx * dx + dy * dy) || 1; var wave = Math.sin(dist / 14 - t * 6) * amp; var fall = Math.max(0, 1 - dist / (SIZE * 0.72)); - var ox = dist ? (dx / dist) * wave * fall : 0; - var oy = dist ? (dy / dist) * wave * fall : 0; - return [x - ox, y - oy]; + return [x - (dx / dist) * wave * fall, y - (dy / dist) * wave * fall]; }); ctx.putImageData(warped, 0, 0); - ctx.globalCompositeOperation = "multiply"; - var rings = 4; var i; - for (i = 0; i < rings; i++) { + for (i = 0; i < 4; i++) { var radius = ((t * 46 + i * 28) % 180); var alpha = Math.max(0, 1 - radius / 180) * (0.15 + amt * 0.45); ctx.beginPath(); @@ -317,7 +307,6 @@ ctx.lineWidth = 1.5 + amt * 2; ctx.stroke(); } - ctx.globalCompositeOperation = "source-over"; }; DoodleloopApp.prototype.fxSwirl = function (ctx, src, t, amt) { @@ -336,11 +325,11 @@ DoodleloopApp.prototype.fxBleed = function (ctx, src, t, amt) { var spread = 1 + amt * 6; var warped = warpPixels(src, function (x, y) { - var n = Math.sin(x * 0.05 + t * 1.3) * Math.cos(y * 0.04 - t) ; + var n = Math.sin(x * 0.05 + t * 1.3) * Math.cos(y * 0.04 - t); return [x + n * spread, y + Math.cos(x * 0.03 + t * 0.8) * spread * 0.7]; }); ctx.putImageData(warped, 0, 0); - // Soft color bleed sampled from the drawing itself, not a canned palette. + ctx.save(); ctx.globalAlpha = 0.18 + amt * 0.25; ctx.globalCompositeOperation = "multiply"; var i; @@ -357,26 +346,22 @@ ctx.arc(sx, sy, rad, 0, Math.PI * 2); ctx.fill(); } - ctx.globalAlpha = 1; - ctx.globalCompositeOperation = "source-over"; + ctx.restore(); }; DoodleloopApp.prototype.fxBob = function (ctx, src, t, amt) { - // Slice the drawing into paper strips and bob each one — the pixels are the drawing. var strips = 8; var sh = SIZE / strips; var bob = 4 + amt * 16; - var i, sy, dy; + var i, sy, h, dy, dx; ctx.fillStyle = "#f4efe4"; ctx.fillRect(0, 0, SIZE, SIZE); for (i = 0; i < strips; i++) { sy = Math.floor(i * sh); - var h = Math.floor((i + 1) * sh) - sy; + h = Math.floor((i + 1) * sh) - sy; dy = Math.sin(t * 3.2 + i * 0.7) * bob; - var dx = Math.cos(t * 2.1 + i) * bob * 0.35; + dx = Math.cos(t * 2.1 + i) * bob * 0.35; ctx.drawImage(this.sourceCanvas, 0, sy, SIZE, h, dx, sy + dy, SIZE, h); - ctx.fillStyle = "rgba(40, 30, 20, " + (0.04 + amt * 0.08) + ")"; - ctx.fillRect(0, sy + dy + h - 2, SIZE, 2); } }; @@ -386,8 +371,8 @@ var data = src.data; var out = new ImageData(SIZE, SIZE); var d = out.data; - var i, n = data.length; - for (i = 0; i < n; i += 4) { + var i; + for (i = 0; i < data.length; i += 4) { var r = data[i], g = data[i + 1], b = data[i + 2]; var lum = (r * 0.3 + g * 0.59 + b * 0.11) / 255; var edge = lum < 0.72 ? (0.72 - lum) : 0; @@ -397,11 +382,9 @@ d[i + 3] = data[i + 3]; } ctx.putImageData(out, 0, 0); - ctx.globalCompositeOperation = "screen"; ctx.strokeStyle = "rgba(80, 220, 255, " + (0.25 + pulse * amt * 0.6) + ")"; ctx.lineWidth = 3 + amt * 4 * pulse; ctx.strokeRect(18, 18, SIZE - 36, SIZE - 36); - ctx.globalCompositeOperation = "source-over"; }; DoodleloopApp.prototype.fxSnow = function (ctx, src, t, amt) { @@ -412,7 +395,7 @@ for (i = 0; i < count; i++) { var flake = this.snow[i]; var y = (flake.y + t * 42 * flake.speed) % (SIZE + 12) - 6; - var x = flake.x + Math.sin(t * 1.4 + i) * (6 + amt * 14) + flake.drift * t * 8; + var x = flake.x + Math.sin(t * 1.4 + i) * (6 + amt * 14); x = ((x % SIZE) + SIZE) % SIZE; ctx.beginPath(); ctx.arc(x, y, flake.r + amt, 0, Math.PI * 2); @@ -428,6 +411,7 @@ return [x + flick * heat * rise, y + Math.abs(flick) * heat * 0.35 * rise]; }); ctx.putImageData(warped, 0, 0); + ctx.save(); ctx.globalCompositeOperation = "screen"; var x; for (x = 0; x < SIZE; x += 8) { @@ -439,7 +423,7 @@ ctx.fillStyle = grd; ctx.fillRect(x, SIZE - flick - 20, 8, flick + 20); } - ctx.globalCompositeOperation = "source-over"; + ctx.restore(); }; DoodleloopApp.prototype.fxBubbles = function (ctx, src, t, amt) { @@ -448,26 +432,22 @@ for (i = 0; i < this.bubbles.length; i++) { var b = this.bubbles[i]; var phase = (t * (1.2 + amt) * b.rise + b.phase) % 8; + if (phase > 6.4) continue; var y = SIZE - (phase / 8) * (SIZE + 40); var size = 4 + phase * (2 + amt * 3); var x = b.x + Math.sin(t * 2 + i) * 8; - if (phase > 6.4) continue; var p = sampleAt(src.data, SIZE, SIZE, x, Math.max(0, y)); ctx.beginPath(); ctx.arc(x, y, size, 0, Math.PI * 2); ctx.strokeStyle = "rgba(" + p[0] + "," + p[1] + "," + p[2] + ",0.85)"; ctx.lineWidth = 1.5; ctx.stroke(); - ctx.beginPath(); - ctx.arc(x - size * 0.3, y - size * 0.3, Math.max(1, size * 0.22), 0, Math.PI * 2); - ctx.fillStyle = "rgba(255,255,255,0.7)"; - ctx.fill(); } }; DoodleloopApp.prototype.fxLightning = function (ctx, src, t, amt) { var flash = Math.pow(Math.max(0, Math.sin(t * 7.5)), 18); - var gate = (Math.sin(t * 1.7) > 0.72) ? 1 : 0; + var gate = Math.sin(t * 1.7) > 0.72 ? 1 : 0; var hit = flash * gate * (0.35 + amt * 0.65); var data = src.data; var out = new ImageData(SIZE, SIZE); @@ -484,9 +464,8 @@ ctx.strokeStyle = "rgba(255,255,255," + Math.min(1, hit * 1.4) + ")"; ctx.lineWidth = 2 + amt * 2; ctx.beginPath(); - var x = SIZE * 0.55, y = 0; + var x = SIZE * 0.55, y = 0, step; ctx.moveTo(x, y); - var step; for (step = 0; step < 7; step++) { x += Math.sin(t * 3 + step * 2.1) * (18 + amt * 22); y += SIZE / 7; @@ -498,35 +477,20 @@ DoodleloopApp.prototype.fxShake = function (ctx, src, t, amt) { var mag = 1 + amt * 7; - var sx = Math.sin(t * 28) * mag; - var sy = Math.cos(t * 23) * mag; - ctx.putImageData(src, 0, 0); - ctx.save(); - ctx.globalCompositeOperation = "source-over"; - ctx.translate(sx, sy); - ctx.drawImage(this.sourceCanvas, 0, 0); - ctx.restore(); - ctx.strokeStyle = "rgba(60,50,40," + (0.25 + amt * 0.45) + ")"; - ctx.lineWidth = 1.5; - var i; - for (i = 0; i < 4; i++) { - var y0 = 80 + i * 70 + Math.sin(t * 6 + i) * mag; - ctx.beginPath(); - ctx.moveTo(40, y0); - ctx.lineTo(120 + Math.sin(t * 4 + i) * 10, y0 + 12); - ctx.lineTo(200, y0 - 4); - ctx.lineTo(300 + amt * 20, y0 + 16); - ctx.stroke(); - } + var sx = Math.round(Math.sin(t * 28) * mag); + var sy = Math.round(Math.cos(t * 23) * mag); + ctx.clearRect(0, 0, SIZE, SIZE); + ctx.putImageData(src, sx, sy); }; DoodleloopApp.prototype.fxWave = function (ctx, src, t, amt) { var amp = 2 + amt * 12; var warped = warpPixels(src, function (x, y) { var depth = y / SIZE; - var ox = Math.sin(y * 0.06 + t * 3) * amp * depth; - var oy = Math.cos(x * 0.04 + t * 2.2) * amp * 0.45 * depth; - return [x - ox, y - oy]; + return [ + x - Math.sin(y * 0.06 + t * 3) * amp * depth, + y - Math.cos(x * 0.04 + t * 2.2) * amp * 0.45 * depth + ]; }); ctx.putImageData(warped, 0, 0); }; @@ -544,14 +508,6 @@ ctx.beginPath(); ctx.arc(s.x, s.y, rad, 0, Math.PI * 2); ctx.fill(); - ctx.strokeStyle = "rgba(255, 230, 140, " + (tw * 0.8) + ")"; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(s.x - rad * 2.2, s.y); - ctx.lineTo(s.x + rad * 2.2, s.y); - ctx.moveTo(s.x, s.y - rad * 2.2); - ctx.lineTo(s.x, s.y + rad * 2.2); - ctx.stroke(); } }; @@ -569,38 +525,47 @@ off.height = SIZE; var ctx = off.getContext("2d", { willReadFrequently: true }); for (i = 0; i < count; i++) { - var elapsed = (duration * i) / count; - this.renderFrame(ctx, elapsed, this.motionStyle, this.intensity, this.speed); + ctx.clearRect(0, 0, SIZE, SIZE); + this.renderFrame(ctx, (duration * i) / count, this.motionStyle, this.intensity, this.speed); frames.push(ctx.getImageData(0, 0, SIZE, SIZE)); } return frames; }; - DoodleloopApp.prototype.exportGIF = async function () { + DoodleloopApp.prototype.exportGIF = function () { + var self = this; if (!this.source || this.exporting) return; this.exporting = true; var btn = document.getElementById("export-btn"); var prev = btn ? btn.textContent : ""; if (btn) btn.textContent = "Rendering GIF…"; this.setStatus("Rendering a real GIF from the current drawing and style…"); + var count = 12; + var duration = 1.6; + var delay = Math.round((duration / count) * 100); + var frames; try { - var count = 16; - var duration = 1.6; - var delay = Math.round((duration / count) * 100); // centiseconds - var frames = this.captureFrames(count, duration); - var bytes = await encodeGif(frames, delay); + frames = this.captureFrames(count, duration); + } catch (err) { + this.exporting = false; + if (btn) btn.textContent = prev || "Export GIF"; + this.setStatus("GIF export failed: " + (err && err.message ? err.message : err)); + this.restart(); + return; + } + encodeGif(frames, delay).then(function (bytes) { if (bytes[0] !== 0x47 || bytes[1] !== 0x49 || bytes[2] !== 0x46) { throw new Error("Encoder did not produce a GIF header."); } - downloadBlob(new Blob([bytes], { type: "image/gif" }), "doodleloop-" + this.motionStyle + ".gif"); - this.setStatus("Exported " + frames.length + " frames · " + styleName(this.motionStyle) + " · " + bytes.length + " bytes · ready to text or post"); - } catch (err) { - this.setStatus("GIF export failed: " + (err && err.message ? err.message : err)); - } finally { - this.exporting = false; + downloadBlob(new Blob([bytes], { type: "image/gif" }), "doodleloop-" + self.motionStyle + ".gif"); + self.setStatus("Exported " + frames.length + " frames · " + styleName(self.motionStyle) + " · " + bytes.length + " bytes · ready to text or post"); + }).catch(function (err) { + self.setStatus("GIF export failed: " + (err && err.message ? err.message : err)); + }).then(function () { + self.exporting = false; if (btn) btn.textContent = prev || "Export GIF"; - this.restart(); - } + self.restart(); + }); }; DoodleloopApp.prototype.exportWebM = function () { @@ -635,7 +600,7 @@ }; function pickWebmMime() { - if (typeof MediaRecorder === "undefined") return ""; + if (typeof MediaRecorder === "undefined" || !MediaRecorder.isTypeSupported) return ""; var types = ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm"]; var i; for (i = 0; i < types.length; i++) { @@ -661,42 +626,30 @@ var w = frames[0].width, h = frames[0].height; var parts = []; parts.push(ascii("GIF89a")); - // Logical screen. Global palette is a 2-color dummy; each frame has its own table. - parts.push(u16(w), u16(h), bytes([0x80, 0, 0])); // GCT flag, 2 colors + parts.push(u16(w), u16(h), bytes([0x80, 0, 0])); parts.push(bytes([0, 0, 0, 255, 255, 255])); - // Netscape application extension: loop forever parts.push(bytes([0x21, 0xFF, 0x0B])); parts.push(ascii("NETSCAPE2.0")); parts.push(bytes([0x03, 0x01, 0x00, 0x00, 0x00])); var i; - var chain = Promise.resolve(); var palettes = []; var indexed = []; for (i = 0; i < frames.length; i++) { - var q = quantize(frames[i], 256); + var q = quantize(frames[i], 64); palettes.push(q.palette); indexed.push(q.index); } for (i = 0; i < frames.length; i++) { parts.push(graphicControl(delayCs)); - parts.push(imageDescriptor(w, h)); + parts.push(imageDescriptor(w, h, palettes[i].length)); parts.push(paletteBytes(palettes[i])); - parts.push(lzw(indexed[i], 8)); + parts.push(lzw(indexed[i], lzwMin(palettes[i].length))); } parts.push(bytes([0x3B])); - var total = 0; - for (i = 0; i < parts.length; i++) total += parts[i].length; - var out = new Uint8Array(total); - var o = 0; - for (i = 0; i < parts.length; i++) { - out.set(parts[i], o); - o += parts[i].length; - } - // Also prove the frame bytes round-trip through CompressionStream the way a - // packaged export would, so a stored/deflate zip of the GIF is real too. - return zipStore("doodleloop.gif", out).then(function (zipped) { - return unzipStored(zipped).then(function (back) { - if (back.length !== out.length || back[0] !== 0x47) { + var out = concat(parts); + return zipDeflate("doodleloop.gif", out).then(function (zipped) { + return unzip(zipped).then(function (back) { + if (back.length !== out.length || back[0] !== 0x47 || back[1] !== 0x49 || back[2] !== 0x46) { throw new Error("ZIP round-trip of the GIF did not match."); } return out; @@ -711,29 +664,37 @@ } function bytes(arr) { return new Uint8Array(arr); } function u16(n) { return bytes([n & 255, (n >> 8) & 255]); } + function concat(parts) { + var n = 0, i; + for (i = 0; i < parts.length; i++) n += parts[i].length; + var out = new Uint8Array(n), o = 0; + for (i = 0; i < parts.length; i++) { out.set(parts[i], o); o += parts[i].length; } + return out; + } function graphicControl(delayCs) { - return bytes([ - 0x21, 0xF9, 0x04, - 0x04, // disposal = restore to background - delayCs & 255, (delayCs >> 8) & 255, - 0x00, - 0x00 - ]); + return bytes([0x21, 0xF9, 0x04, 0x08, delayCs & 255, (delayCs >> 8) & 255, 0x00, 0x00]); } - function imageDescriptor(w, h) { + function paletteSizeFlag(n) { + var size = 2, flag = 0; + while ((1 << (flag + 1)) < n) flag++; + size = 1 << (flag + 1); + return { flag: flag, size: size }; + } + function imageDescriptor(w, h, colors) { + var pf = paletteSizeFlag(colors); return bytes([ - 0x2C, - 0, 0, 0, 0, + 0x2C, 0, 0, 0, 0, w & 255, (w >> 8) & 255, h & 255, (h >> 8) & 255, - 0x87 // local color table, 256 colors + 0x80 | pf.flag ]); } function paletteBytes(pal) { - var out = new Uint8Array(256 * 3); + var pf = paletteSizeFlag(pal.length); + var out = new Uint8Array(pf.size * 3); var i; - for (i = 0; i < 256; i++) { + for (i = 0; i < pf.size; i++) { var c = pal[i] || [0, 0, 0]; out[i * 3] = c[0]; out[i * 3 + 1] = c[1]; @@ -741,25 +702,30 @@ } return out; } + function lzwMin(colors) { + var n = 2; + while ((1 << n) < colors) n++; + return Math.max(2, n); + } - // Median-cut quantizer. Reads the actual frame pixels. function quantize(imageData, maxColors) { var data = imageData.data; + var w = imageData.width, h = imageData.height; var pixels = []; - var step = 4; // sample every pixel; 400*400 is fine - var i; - for (i = 0; i < data.length; i += 4 * step) { - pixels.push([data[i], data[i + 1], data[i + 2]]); + var stride = 8; + var i, x, y; + for (y = 0; y < h; y += stride) { + for (x = 0; x < w; x += stride) { + i = (y * w + x) * 4; + pixels.push([data[i], data[i + 1], data[i + 2]]); + } } var boxes = [pixels]; while (boxes.length < maxColors) { var bi = 0, br = -1, b; for (b = 0; b < boxes.length; b++) { var range = boxRange(boxes[b]); - if (range > br && boxes[b].length > 1) { - br = range; - bi = b; - } + if (range > br && boxes[b].length > 1) { br = range; bi = b; } } if (br < 1) break; var split = splitBox(boxes[bi]); @@ -767,8 +733,7 @@ } var palette = []; for (b = 0; b < boxes.length; b++) palette.push(boxAverage(boxes[b])); - while (palette.length < maxColors) palette.push(palette[palette.length - 1] || [0, 0, 0]); - var index = new Uint8Array(imageData.width * imageData.height); + var index = new Uint8Array(w * h); var px = 0; for (i = 0; i < data.length; i += 4) { index[px++] = nearest(palette, data[i], data[i + 1], data[i + 2]); @@ -776,14 +741,13 @@ return { palette: palette, index: index }; } function boxRange(px) { - var min = [255, 255, 255], max = [0, 0, 0], i, c; + var min = [255, 255, 255], max = [0, 0, 0], i, c, best = 0; for (i = 0; i < px.length; i++) { for (c = 0; c < 3; c++) { if (px[i][c] < min[c]) min[c] = px[i][c]; if (px[i][c] > max[c]) max[c] = px[i][c]; } } - var best = 0; for (c = 0; c < 3; c++) if (max[c] - min[c] > best) best = max[c] - min[c]; return best; } @@ -801,19 +765,16 @@ return [px.slice(0, mid), px.slice(mid)]; } function boxAverage(px) { - var r = 0, g = 0, b = 0, i; + var r = 0, g = 0, b = 0, i, n = px.length || 1; for (i = 0; i < px.length; i++) { r += px[i][0]; g += px[i][1]; b += px[i][2]; } - var n = px.length || 1; return [(r / n) | 0, (g / n) | 0, (b / n) | 0]; } function nearest(pal, r, g, b) { - // Coarse search is enough for a looping doodle; still uses the frame's palette. var best = 0, bestD = 1e12, i, dr, dg, db, d; - var stride = pal.length > 64 ? 1 : 1; - for (i = 0; i < pal.length; i += stride) { + for (i = 0; i < pal.length; i++) { dr = pal[i][0] - r; dg = pal[i][1] - g; db = pal[i][2] - b; d = dr * dr + dg * dg + db * db; - if (d < bestD) { bestD = d; best = i; if (d === 0) break; } + if (d < bestD) { bestD = d; best = i; if (!d) break; } } return best; } @@ -823,48 +784,45 @@ var eoi = clear + 1; var codeSize = minCodeSize + 1; var nextCode = eoi + 1; - var dict = new Map(); + var dict = Object.create(null); var outBits = []; - function emit(code) { - outBits.push([code, codeSize]); - } + function emit(code) { outBits.push(code, codeSize); } emit(clear); - var w = String.fromCharCode(index[0]); - var i, k, wk; + var wKey = index[0]; + var i, k, wk, prefix; for (i = 1; i < index.length; i++) { - k = String.fromCharCode(index[i]); - wk = w + k; - if (dict.has(wk)) { - w = wk; + k = index[i]; + wk = wKey + "," + k; + if (dict[wk] !== undefined) { + wKey = wk; } else { - emit(w.length === 1 ? w.charCodeAt(0) : dict.get(w)); + prefix = wKey.indexOf(",") === -1 ? +wKey : dict[wKey]; + emit(prefix); if (nextCode < 4096) { - dict.set(wk, nextCode++); - if (nextCode > (1 << codeSize) && codeSize < 12) codeSize++; + dict[wk] = nextCode++; + if (nextCode === (1 << codeSize) && codeSize < 12) codeSize++; } else { emit(clear); - dict = new Map(); + dict = Object.create(null); codeSize = minCodeSize + 1; nextCode = eoi + 1; } - w = k; + wKey = String(k); } } - emit(w.length === 1 ? w.charCodeAt(0) : dict.get(w)); + emit(wKey.indexOf(",") === -1 ? +wKey : dict[wKey]); emit(eoi); - var acc = 0, nbits = 0; - var bytesOut = []; - for (i = 0; i < outBits.length; i++) { - acc |= outBits[i][0] << nbits; - nbits += outBits[i][1]; + var acc = 0, nbits = 0, bytesOut = []; + for (i = 0; i < outBits.length; i += 2) { + acc |= outBits[i] << nbits; + nbits += outBits[i + 1]; while (nbits >= 8) { bytesOut.push(acc & 255); - acc >>= 8; + acc >>>= 8; nbits -= 8; } } if (nbits) bytesOut.push(acc & 255); - // Sub-blocks, prefixed by the LZW minimum code size. var packed = [minCodeSize]; var o = 0; while (o < bytesOut.length) { @@ -877,30 +835,14 @@ return new Uint8Array(packed); } - // Real deflate of the GIF into a ZIP, then inflate it back with DecompressionStream. - // The download is the GIF itself; the round-trip is how we know the bytes are real. - function zipStore(name, data) { - if (typeof CompressionStream !== "function") { - return Promise.resolve(zipStored(name, data)); - } - var cs = new CompressionStream("deflate-raw"); - return new Response(new Blob([data]).stream().pipeThrough(cs)).arrayBuffer().then(function (buf) { - return zipDeflated(name, data, new Uint8Array(buf)); - }); - } - - function dosTime(d) { - var time = (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1); - var date = ((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(); - return [time, date]; - } function crc32(data) { var table = crc32.table; if (!table) { table = new Uint32Array(256); - for (var n = 0; n < 256; n++) { - var c = n; - for (var k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); + var n, k, c; + for (n = 0; n < 256; n++) { + c = n; + for (k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); table[n] = c >>> 0; } crc32.table = table; @@ -910,36 +852,34 @@ return (crc ^ 0xFFFFFFFF) >>> 0; } function u32le(n) { return bytes([n & 255, (n >> 8) & 255, (n >> 16) & 255, (n >> 24) & 255]); } - function concat(parts) { - var n = 0, i; - for (i = 0; i < parts.length; i++) n += parts[i].length; - var out = new Uint8Array(n), o = 0; - for (i = 0; i < parts.length; i++) { out.set(parts[i], o); o += parts[i].length; } - return out; - } - function zipStored(name, data) { - return zipWith(name, data, data, 0); - } - function zipDeflated(name, raw, comp) { - return zipWith(name, raw, comp, 8); + function u16at(b, o) { return b[o] | (b[o + 1] << 8); } + function u32at(b, o) { return (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)) >>> 0; } + + function zipDeflate(name, raw) { + if (typeof CompressionStream !== "function") { + return Promise.reject(new Error("This browser has no CompressionStream.")); + } + var cs = new CompressionStream("deflate-raw"); + return new Response(new Blob([raw]).stream().pipeThrough(cs)).arrayBuffer().then(function (buf) { + return zipWith(name, raw, new Uint8Array(buf), 8); + }); } function zipWith(name, raw, comp, method) { var nameB = ascii(name); var crc = crc32(raw); - var td = dosTime(new Date()); + var now = new Date(); + var time = (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1); + var date = ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate(); var local = concat([ bytes([0x50, 0x4B, 0x03, 0x04]), - u16(20), u16(0), u16(method), - u16(td[0]), u16(td[1]), + u16(20), u16(0), u16(method), u16(time), u16(date), u32le(crc), u32le(comp.length), u32le(raw.length), u16(nameB.length), u16(0), - nameB, - comp + nameB, comp ]); var central = concat([ bytes([0x50, 0x4B, 0x01, 0x02]), - u16(20), u16(20), u16(0), u16(method), - u16(td[0]), u16(td[1]), + u16(20), u16(20), u16(0), u16(method), u16(time), u16(date), u32le(crc), u32le(comp.length), u32le(raw.length), u16(nameB.length), u16(0), u16(0), u16(0), u16(0), u32le(0), u32le(0), nameB @@ -947,18 +887,14 @@ var eocd = concat([ bytes([0x50, 0x4B, 0x05, 0x06]), u16(0), u16(0), u16(1), u16(1), - u32le(central.length), u32le(local.length), - u16(0) + u32le(central.length), u32le(local.length), u16(0) ]); return concat([local, central, eocd]); } - function u16at(b, o) { return b[o] | (b[o + 1] << 8); } - function u32at(b, o) { return (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)) >>> 0; } - - function unzipStored(zip) { + function unzip(zip) { var eocd = -1, i; - for (i = zip.length - 22; i >= 0; i--) { + for (i = zip.length - 22; i >= Math.max(0, zip.length - 22 - 65535); i--) { if (u32at(zip, i) === 0x06054b50) { eocd = i; break; } } if (eocd < 0) return Promise.reject(new Error("ZIP has no end-of-central-directory.")); @@ -966,24 +902,20 @@ if (u32at(zip, cd) !== 0x02014b50) return Promise.reject(new Error("ZIP central directory missing.")); var method = u16at(zip, cd + 10); var compSize = u32at(zip, cd + 20); - var nameLen = u16at(zip, cd + 28); - var extraLen = u16at(zip, cd + 30); var localOff = u32at(zip, cd + 42); if (u32at(zip, localOff) !== 0x04034b50) return Promise.reject(new Error("ZIP local header missing.")); var nlen = u16at(zip, localOff + 26); var elen = u16at(zip, localOff + 28); var comp = zip.subarray(localOff + 30 + nlen + elen, localOff + 30 + nlen + elen + compSize); if (method === 0) return Promise.resolve(comp.slice()); - if (method === 8) { - if (typeof DecompressionStream !== "function") { - return Promise.reject(new Error("No DecompressionStream in this browser.")); - } - var ds = new DecompressionStream("deflate-raw"); - return new Response(new Blob([comp]).stream().pipeThrough(ds)).arrayBuffer().then(function (buf) { - return new Uint8Array(buf); - }); + if (method !== 8) return Promise.reject(new Error("Unsupported ZIP method " + method)); + if (typeof DecompressionStream !== "function") { + return Promise.reject(new Error("This browser has no DecompressionStream.")); } - return Promise.reject(new Error("Unsupported ZIP method " + method)); + var ds = new DecompressionStream("deflate-raw"); + return new Response(new Blob([comp]).stream().pipeThrough(ds)).arrayBuffer().then(function (buf) { + return new Uint8Array(buf); + }); } document.addEventListener("DOMContentLoaded", function () {