/* Roll Credits: type names, pick a style, watch the roll, download a 15-second WebM or GIF. All in the page. */ (function () { 'use strict'; var W0 = 640, H0 = 360, INSET = 28, DUR = 15; var SAMPLE_TITLE = 'Roll Credits'; var SAMPLE_NAMES = [ '# For team leads and organizers', 'Thank-yous, for a launch, a trip, a bake-off or a last day', '# You do', 'Type, a title and the names', 'Pick, a style and a speed', '# It does', 'Your browser, draws the credits live', 'WebM or GIF, 15 seconds, yours to keep', '# It costs', 'Nothing, and nothing is uploaded' ].join('\n'); /* ---------- parsing: "Name, role" per line; "# text" is a heading ---------- */ function parse(text) { var out = []; String(text).split('\n').forEach(function (raw) { var l = raw.trim(), i; if (!l) return; if (l[0] === '#') { l = l.replace(/^#+\s*/, ''); if (l) out.push({ k: 'head', t: l }); return; } i = l.indexOf(','); out.push(i < 0 ? { k: 'e', name: l, role: '' } : { k: 'e', name: l.slice(0, i).trim(), role: l.slice(i + 1).trim() }); }); return out; } function hh(it) { return it.k === 'title' ? 70 : it.k === 'head' ? 46 : it.role ? 54 : 38; } function total(items) { return items.reduce(function (a, it) { return a + hh(it); }, 0); } function rollLength(items, speed) { return (H0 - 2 * INSET + total(items)) / speed; } /* ---------- styles ---------- */ var MONO = 'ui-monospace, Menlo, Consolas, "Courier New", monospace'; var SERIF = 'Georgia, "Times New Roman", serif'; var SANS = '"Trebuchet MS", Verdana, "Segoe UI", sans-serif'; function perim(step, f) { // walks a rectangle 12px inside the canvas edge var a = 12, b = W0 - 12, c = H0 - 12, i = 0, x, y; for (x = a; x < b; x += step) f(x, a, i++); for (y = a; y < c; y += step) f(b, y, i++); for (x = b; x > a; x -= step) f(x, c, i++); for (y = c; y > a; y -= step) f(a, y, i++); } function vgrad(c, a, b) { var g = c.createLinearGradient(0, 0, 0, H0); g.addColorStop(0, a); g.addColorStop(1, b); c.fillStyle = g; c.fillRect(0, 0, W0, H0); } var STYLES = { arcade: { fam: MONO, wt: 'bold ', rs: '', up: true, glow: true, c: { title: '#ffe14a', name: '#39f5ff', role: '#ff4fd8' }, bg: function (c) { c.fillStyle = '#07021a'; c.fillRect(0, 0, W0, H0); c.fillStyle = 'rgba(0,0,0,.28)'; for (var y = 0; y < H0; y += 4) c.fillRect(0, y, W0, 2); }, frame: function (c, t) { var k = Math.floor(t * 6); perim(16, function (x, y, i) { c.fillStyle = (i + k) % 3 === 0 ? '#ff4fd8' : '#39f5ff'; c.fillRect(x - 4, y - 4, 8, 8); }); } }, epic: { fam: SERIF, wt: '', rs: 'italic ', up: false, glow: false, c: { title: '#f2c75c', name: '#f5e6c4', role: '#c9a35a' }, bg: function (c) { vgrad(c, '#2a1a0a', '#120a04'); }, frame: function (c) { c.strokeStyle = '#d4a93f'; c.lineWidth = 2; c.strokeRect(10, 10, W0 - 20, H0 - 20); c.lineWidth = 1; c.strokeRect(17, 17, W0 - 34, H0 - 34); c.fillStyle = '#f2c75c'; [[10, 10], [W0 - 10, 10], [10, H0 - 10], [W0 - 10, H0 - 10]].forEach(function (p) { c.beginPath(); c.moveTo(p[0], p[1] - 8); c.lineTo(p[0] + 8, p[1]); c.lineTo(p[0], p[1] + 8); c.lineTo(p[0] - 8, p[1]); c.closePath(); c.fill(); }); } }, sitcom: { fam: SANS, wt: 'bold ', rs: '', up: false, glow: false, c: { title: '#ffd23f', name: '#ffffff', role: '#ffd9a8' }, bg: function (c) { vgrad(c, '#12818f', '#0b4f5c'); }, frame: function (c, t) { var k = Math.floor(t * 3); c.strokeStyle = '#ff6b4a'; c.lineWidth = 5; c.strokeRect(12, 12, W0 - 24, H0 - 24); perim(24, function (x, y, i) { c.fillStyle = (i + k) % 2 ? '#ffd23f' : '#9a6b12'; c.beginPath(); c.arc(x, y, 5, 0, 6.2832); c.fill(); }); } } }; /* ---------- drawing: draw(ctx, pixelWidth, pixelHeight, seconds, model) is the single path for preview, WebM and GIF ---------- */ function txt(c, s, x, y, px, pre, fam, col, maxW, S) { var p = px; if (S.up && pre !== S.rs) s = s.toUpperCase(); for (; p > 8; p--) { c.font = pre + p + 'px ' + fam; if (c.measureText(s).width <= maxW) break; } c.font = pre + p + 'px ' + fam; c.fillStyle = col; c.shadowColor = col; c.shadowBlur = S.glow ? 6 : 0; c.fillText(s, x, y); c.shadowBlur = 0; } function draw(c, W, H, t, m) { var S = STYLES[m.style] || STYLES.arcade, cx = W0 / 2, mw = W0 - 2 * INSET - 30; var len = rollLength(m.items, m.speed), y = H0 - INSET - m.speed * (t % len); c.setTransform(W / W0, 0, 0, W / W0, 0, 0); c.textAlign = 'center'; c.textBaseline = 'middle'; S.bg(c, t); c.save(); c.beginPath(); c.rect(INSET, INSET, W0 - 2 * INSET, H0 - 2 * INSET); c.clip(); m.items.forEach(function (it) { var h = hh(it); if (y + h > INSET && y < H0 - INSET) { if (it.k === 'title') txt(c, it.t, cx, y + 35, 34, S.wt, S.fam, S.c.title, mw, S); else if (it.k === 'head') txt(c, it.t, cx, y + 28, 17, S.wt, S.fam, S.c.role, mw, S); else { txt(c, it.name, cx, y + 20, 22, S.wt, S.fam, S.c.name, mw, S); if (it.role) txt(c, it.role, cx, y + 42, 13, S.rs, S.fam, S.c.role, mw, S); } } y += h; }); c.restore(); S.frame(c, t); } /* ---------- GIF89a encoder: fixed 3-3-2 colour palette, looping ---------- */ // shortcut: fixed 256-colour palette bands gradients, upgrade to per-frame quantisation if it looks bad function lzw(px) { var out = [], cur = 0, bits = 0, size = 9, next = 258, dict = new Map(), p = px[0], i, k, c; function put(code) { cur |= code << bits; bits += size; while (bits >= 8) { out.push(cur & 255); cur >>= 8; bits -= 8; } } put(256); for (i = 1; i < px.length; i++) { k = p << 8 | px[i]; c = dict.get(k); if (c !== undefined) { p = c; continue; } put(p); dict.set(k, next++); if (next > (1 << size) && size < 12) size++; if (next === 4096) { put(256); dict = new Map(); next = 258; size = 9; } p = px[i]; } put(p); if (next >= (1 << size) && size < 12) size++; put(257); if (bits) out.push(cur & 255); return out; } function gifEncoder(w, h, delayCs) { var parts = [], i, pal = new Uint8Array(768); for (i = 0; i < 256; i++) { pal[i * 3] = (i >> 5) * 255 / 7; pal[i * 3 + 1] = ((i >> 2) & 7) * 255 / 7; pal[i * 3 + 2] = (i & 3) * 85; } parts.push(new Uint8Array([71, 73, 70, 56, 57, 97, w & 255, w >> 8, h & 255, h >> 8, 0xF7, 0, 0]), pal, new Uint8Array([0x21, 0xFF, 11, 78, 69, 84, 83, 67, 65, 80, 69, 50, 46, 48, 3, 1, 0, 0, 0])); return { add: function (rgba) { var idx = new Uint8Array(w * h), j, d, b = []; for (j = 0; j < idx.length; j++) idx[j] = (rgba[j * 4] & 0xE0) | ((rgba[j * 4 + 1] & 0xE0) >> 3) | (rgba[j * 4 + 2] >> 6); d = lzw(idx); b.push(0x21, 0xF9, 4, 0, delayCs & 255, delayCs >> 8, 0, 0, 0x2C, 0, 0, 0, 0, w & 255, w >> 8, h & 255, h >> 8, 0, 8); for (j = 0; j < d.length; j += 255) { var n = Math.min(255, d.length - j); b.push(n); for (var q = 0; q < n; q++) b.push(d[j + q]); } b.push(0); parts.push(new Uint8Array(b)); }, done: function () { parts.push(new Uint8Array([0x3B])); return parts; } }; } if (typeof document === 'undefined') { module.exports = { parse: parse, draw: draw, lzw: lzw, gifEncoder: gifEncoder, STYLES: STYLES, rollLength: rollLength, SAMPLE_NAMES: SAMPLE_NAMES }; return; } /* ---------- the page ---------- */ var root = document.getElementById('try'); root.innerHTML = '