File size: 7,450 Bytes
f6dcaa7 757768f f6dcaa7 757768f f6dcaa7 757768f f6dcaa7 757768f f6dcaa7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | // 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()); }
|