Download lib/example.js from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 7.45 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/example.js
- Command line
-
hf download hf://spaces/SharpMeow/unroll/lib/example.js
-
curl -L -o example.js https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/example.js
7.45 kB
| // A small synthetic scroll, generated in the browser: three windings of a spiral with UVs, | |
| // one OBJ per winding (and one PLY), X-ray, ink and overlay images in PNG, JPG and TIFF. | |
| // Used for the "Download example package" button and for testing "Open your own". | |
| import { writeZip } from './formats.js'; | |
| const IDS = ['s03', 's02', 's01']; // outermost first | |
| const H = 30, TURN = 1.08, R0 = 13, PITCH = 2.2; | |
| function spiral(j) { | |
| // winding j (0 outermost): a little more than one turn of an Archimedean spiral, wavy like real papyrus | |
| const C = 72, R = 13, v = [], t = [], f = []; | |
| for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) { | |
| const u = c / (C - 1), h = r / (R - 1); | |
| const th = -(j + u * TURN) * 2 * Math.PI; | |
| const rad = R0 - PITCH * (j + u * TURN) + 0.25 * Math.sin(u * 23 + h * 5); | |
| v.push([rad * Math.cos(th), (0.5 - h) * H + 0.6 * Math.sin(u * 9), rad * Math.sin(th)]); | |
| t.push([u, 1 - h]); | |
| } | |
| for (let r = 0; r + 1 < R; r++) for (let c = 0; c + 1 < C; c++) { | |
| const a = r * C + c; | |
| f.push([a, a + C, a + 1], [a + 1, a + C, a + C + 1]); | |
| } | |
| return { v, t, f }; | |
| } | |
| function obj({ v, t, f }, id) { | |
| const L = [`# synthetic winding ${id}, millimeters, roll axis along y`]; | |
| for (const p of v) L.push(`v ${p.map((x) => x.toFixed(4)).join(' ')}`); | |
| for (const q of t) L.push(`vt ${q.map((x) => x.toFixed(5)).join(' ')}`); | |
| for (const k of f) L.push(`f ${k.map((i) => `${i + 1}/${i + 1}`).join(' ')}`); | |
| return new TextEncoder().encode(L.join('\n') + '\n'); | |
| } | |
| function ply({ v, t, f }) { | |
| const head = `ply\nformat binary_little_endian 1.0\ncomment synthetic winding\nelement vertex ${v.length}\nproperty float x\nproperty float y\nproperty float z\nproperty float s\nproperty float t\nelement face ${f.length}\nproperty list uchar int vertex_indices\nend_header\n`; | |
| const hb = new TextEncoder().encode(head); | |
| const buf = new ArrayBuffer(hb.length + v.length * 20 + f.length * 13); | |
| new Uint8Array(buf).set(hb); | |
| const dv = new DataView(buf); let o = hb.length; | |
| v.forEach((p, i) => { for (const x of [...p, ...t[i]]) { dv.setFloat32(o, x, true); o += 4; } }); | |
| for (const k of f) { dv.setUint8(o++, 3); for (const i of k) { dv.setInt32(o, i, true); o += 4; } } | |
| return new Uint8Array(buf); | |
| } | |
| function canvas(w, h) { const c = document.createElement('canvas'); c.width = w; c.height = h; return [c, c.getContext('2d')]; } | |
| let seed = 7; | |
| const rnd = () => ((seed = (seed * 16807) % 2147483647) / 2147483647); | |
| function ctImage(w, h) { | |
| const [c, g] = canvas(w, h); | |
| g.fillStyle = '#2a2a2a'; g.fillRect(0, 0, w, h); | |
| for (let i = 0; i < 900; i++) { const y = rnd() * h; g.strokeStyle = `rgba(${150 + rnd() * 100 | 0},${150 + rnd() * 100 | 0},${150 + rnd() * 100 | 0},${0.08 + rnd() * 0.2})`; g.lineWidth = 1 + rnd() * 2; g.beginPath(); g.moveTo(0, y); g.lineTo(w, y + (rnd() - 0.5) * 6); g.stroke(); } | |
| for (let i = 0; i < 400; i++) { const x = rnd() * w; g.strokeStyle = `rgba(200,200,200,${0.04 + rnd() * 0.1})`; g.beginPath(); g.moveTo(x, 0); g.lineTo(x + (rnd() - 0.5) * 6, h); g.stroke(); } | |
| return c; | |
| } | |
| function inkImage(w, h, text, blur) { | |
| const [c, g] = canvas(w, h); | |
| g.fillStyle = '#000'; g.fillRect(0, 0, w, h); | |
| g.fillStyle = '#fff'; g.filter = blur ? `blur(${blur}px)` : 'none'; | |
| g.font = `${Math.round(h * 0.13)}px Georgia, serif`; | |
| const lines = text.split('\n'); | |
| lines.forEach((l, i) => g.fillText(l, w * 0.06, h * (0.2 + i * 0.17))); | |
| return c; | |
| } | |
| const png = (c) => new Promise((ok) => c.toBlob(async (b) => ok(new Uint8Array(await b.arrayBuffer())), 'image/png')); | |
| const jpg = (c) => new Promise((ok) => c.toBlob(async (b) => ok(new Uint8Array(await b.arrayBuffer())), 'image/jpeg', 0.9)); | |
| function tif(c) { | |
| // 8-bit grayscale, uncompressed, one strip | |
| const g = c.getContext('2d').getImageData(0, 0, c.width, c.height).data; | |
| const W = c.width, Hh = c.height, n = W * Hh, tags = [[256, 3, W], [257, 3, Hh], [258, 3, 8], [259, 3, 1], [262, 3, 1], [273, 4, 8 + 2 + 10 * 12 + 4], [277, 3, 1], [278, 3, Hh], [279, 4, n], [284, 3, 1]]; | |
| const buf = new ArrayBuffer(8 + 2 + tags.length * 12 + 4 + n); const dv = new DataView(buf); | |
| dv.setUint16(0, 0x4949); dv.setUint16(2, 42, true); dv.setUint32(4, 8, true); dv.setUint16(8, tags.length, true); | |
| tags.forEach(([t, ty, v], i) => { const o = 10 + i * 12; dv.setUint16(o, t, true); dv.setUint16(o + 2, ty, true); dv.setUint32(o + 4, 1, true); if (ty === 3) dv.setUint16(o + 8, v, true); else dv.setUint32(o + 8, v, true); }); | |
| const px = new Uint8Array(buf, 8 + 2 + tags.length * 12 + 4); | |
| for (let i = 0; i < n; i++) px[i] = g[i * 4]; | |
| return new Uint8Array(buf); | |
| } | |
| export const EXAMPLE_TEXT = { s03: 'ΒΙΟΣ ΓΑΡ\nΟΥ ΒΙΩΤΟΣ\nΑΝΕΞΕΤΑΣΤΟΣ', s02: 'ΗΔΟΝΗ ΚΑΙ\nΑΤΑΡΑΞΙΑ\nΦΙΛΟΔΗΜΟΣ', s01: 'ΤΕΛΟΣ ΤΟΥ\nΒΙΒΛΙΟΥ\nΕΡΡΩΣΘΕ' }; | |
| export async function exampleFiles() { | |
| seed = 7; | |
| const files = []; | |
| const add = (name, bytes) => files.push({ name: `synthetic-scroll/${name}`, bytes }); | |
| for (const [j, id] of IDS.entries()) { | |
| const s = spiral(j); | |
| add(`meshes/${id}.${id === 's02' ? 'ply' : 'obj'}`, id === 's02' ? ply(s) : obj(s, id)); | |
| const w = Math.round(2 * Math.PI * (R0 - PITCH * (j + TURN / 2)) * TURN * 8), h = H * 8; | |
| add(`xray/${id}.png`, await png(ctImage(w, h))); | |
| const ink = inkImage(w, h, EXAMPLE_TEXT[id], 0); | |
| add(`ink/${id}.${id === 's01' ? 'tif' : id === 's02' ? 'jpg' : 'png'}`, id === 's01' ? tif(ink) : id === 's02' ? await jpg(ink) : await png(ink)); | |
| if (id === 's02') add(`overlay/${id}.png`, await png(inkImage(w, h, EXAMPLE_TEXT[id], 3))); | |
| } | |
| const manifest = { | |
| schema: 'unroll-scroll/1', | |
| id: 'synthetic-example', | |
| name: 'Synthetic example scroll', | |
| description: 'Three windings of a made-up spiral, with fake X-ray fibers and Greek text as ink. For testing "Open your own".', | |
| voxelMM: 0.01, | |
| geometry: { format: 'mesh', units: 'mm', axis: 'y' }, | |
| windings: [{ id: 's03', mesh: 'meshes/s03.obj' }, { id: 's02', mesh: 'meshes/s02.ply' }, { id: 's01', mesh: 'meshes/s01.obj' }], | |
| outermost: 'first', | |
| layers: [ | |
| { id: 'xray', role: 'ct', label: 'X-ray', texture: 'xray/{id}.png', source: 'Synthetic fiber noise', resolutionUM: 125 }, | |
| { id: 'ink', role: 'ink', label: 'Ink', texture: 'ink/{id}.png', source: 'Synthetic text', resolutionUM: 125 }, | |
| { id: 'model', role: 'overlay', label: 'Blurred ink', windings: ['s02'], texture: 'overlay/{id}.png', source: 'The ink layer, blurred, to show the compare swipe', resolutionUM: 125 }, | |
| ], | |
| credits: { text: 'Generated by the Unroll page', url: 'https://huggingface.co/spaces/SharpMeow/unroll' }, | |
| dataTerms: { license: 'CC0 1.0', url: 'https://creativecommons.org/publicdomain/zero/1.0/' }, | |
| }; | |
| // the ink files use three formats on purpose: per-winding paths are listed in "files" overrides | |
| manifest.layers[1].texture = 'ink/{id}.png'; | |
| manifest.layers[1].files = { s02: 'ink/s02.jpg', s01: 'ink/s01.tif' }; | |
| add('manifest.json', new TextEncoder().encode(JSON.stringify(manifest, null, 1))); | |
| add('README.txt', new TextEncoder().encode('Synthetic example for Unroll (huggingface.co/spaces/SharpMeow/unroll).\nOpen the page, press + (Open your own) and drop this zip or the whole folder.\nEverything stays in your browser. See docs/ADDING_A_SCROLL.md for the manifest format.\n')); | |
| return files; | |
| } | |
| export async function exampleZip() { return writeZip(await exampleFiles()); } | |