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()); }