SharpMeow commited on
Commit
e584c2a
·
verified ·
1 Parent(s): a7e40db

One page scroll bar and American English

Browse files
Files changed (4) hide show
  1. app.py +38 -21
  2. fcmodel.py +1 -1
  3. fibercheck.py +8 -8
  4. onescroll.html +62 -0
app.py CHANGED
@@ -39,6 +39,7 @@ CT_KEEP = int(8e9) # CT chunks kept between runs; the oldest go
39
  GAME = HERE / "assets" / "game.npz"
40
  GUIDE = HERE / "guide" / "index.html"
41
  CONNECT_HTML = HERE / "connect.html"
 
42
  PATCHES = [("Quick: 60 patches, about 2 minutes", 60), ("Thorough: 150 patches, about 5 minutes", 150),
43
  ("Whole segment: up to 400 patches, about 15 minutes", 400)]
44
  CLAUDE_PATCHES, CLAUDE_CHECK_N = 40, 10
@@ -341,8 +342,9 @@ def guide_html():
341
  if not GUIDE.is_file():
342
  return "<p>The welcome page is missing from this copy.</p>"
343
  page = GUIDE.read_text()
344
- # its pictures point at Gradio's file route (a <base> cannot do it); in-page anchors scroll inside the frame,
345
- # ?tab= links switch the Space's tabs, other links open a new tab (as Lasagna Fields does)
 
346
  page = page.replace("'assets/", "'/gradio_api/file=assets/").replace('"assets/', '"/gradio_api/file=assets/')
347
  fix = ('<script>document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a");'
348
  'if(!a)return;var h=a.getAttribute("href")||"";if(h.charAt(0)==="#"){e.preventDefault();'
@@ -438,7 +440,7 @@ class Game:
438
  return FC.controls(on, rng)[kind]
439
 
440
  def judge_pair(self, a, b, ref):
441
- """P(the free judge picks a), both scaled by the on render's grey scale as in the checker."""
442
  if self.judge is None:
443
  self.judge = FC.LocalJudge(str(WEIGHTS))
444
  p, _ = self.judge.prefer([FC.to_input(a, ref)], [FC.to_input(b, ref)])
@@ -513,7 +515,7 @@ texture: it never reads or finds text.
513
  <h3 id="read">2. Read the result</h3>
514
 
515
  - **The map** is the segment as traced, flattened on its own mesh grid. Yellow and green: clear fibers. Purple: weak.
516
- Grey: no sign of fibers. Slate: traced but not scored (holes nearby, masked CT, or not sampled). Black: no surface.
517
  - **Fibers show on** is the share of patches with evidence 0.75 or more. **Median evidence** is the typical patch.
518
  - **Self-check**: on 40 of your patches, the judge must prefer the real render over a noised, a blurred and a
519
  scrambled copy at least 80% of the time. If it fails, do not trust that run.
@@ -643,9 +645,9 @@ CSS = """
643
  .fc-untested { font-size:13.5px; margin-top:8px; } .fc-untested summary { cursor:pointer; }
644
  .fc-legend { font-size:12.5px; color: var(--body-text-color-subdued); }
645
  .fc-legend i { display:inline-block; width:11px; height:11px; border-radius:3px; margin:0 4px -1px 10px; }
646
- .fc-report { width:100%; height:80vh; border:0; border-radius:12px; }
647
- .fc-guide iframe { width:100%; height:calc(100vh - 150px); min-height:640px; border:0; border-radius:12px;
648
- background:#f6f1e7; }
649
  .fc-howto img { border-radius:10px; border:1px solid var(--border-color-primary); max-width:min(100%, 820px);
650
  display:block; margin:10px 0 4px; }
651
  .fc-howto figure { margin:10px 0 18px; } .fc-howto figcaption { font-size:13px; color: var(--body-text-color-subdued); }
@@ -666,12 +668,17 @@ CSS = """
666
  .fc-credit { text-align:center; font-size:13px; color: var(--body-text-color-subdued); padding:18px 0 6px; }
667
  .fc-credit a { color: var(--fc-accent) !important; }
668
  #fc-dl .empty { min-height: 64px !important; height: 64px !important; }
 
 
 
 
 
669
  @media (max-width: 640px) {
670
  .fc-head h1 { font-size:24px; }
671
  .fc-head span { display:none; }
672
  #fc-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
673
- .fc-guide iframe { height:calc(100vh - 120px); min-height:520px; }
674
  .fc-strip { grid-template-columns:repeat(2,minmax(0,1fr)); }
 
675
  }
676
  """
677
 
@@ -688,6 +695,7 @@ model or by Claude with your own key. Model output, not a reading.">
688
  <link rel="preconnect" href="https://fonts.googleapis.com">
689
  <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650&display=swap" rel="stylesheet">
690
  """
 
691
 
692
  TAB_JS = """
693
  () => {
@@ -699,7 +707,13 @@ TAB_JS = """
699
  const btn = btns.find((b) => b.getAttribute('role') === 'tab') || btns[btns.length - 1];
700
  if (btn) btn.click();
701
  };
702
- window.addEventListener('message', (e) => { if (e.data && e.data.fcTab) pick(e.data.fcTab); });
 
 
 
 
 
 
703
  const q = new URLSearchParams(window.location.search).get('tab');
704
  if (q) setTimeout(() => pick(q), 300);
705
  }
@@ -854,13 +868,13 @@ def build_demo():
854
  summary = gr.HTML("<p class='fc-meta'>Your map and summary appear here. 60 patches take "
855
  "about two minutes; try <b>Fiber or not?</b> while you wait.</p>")
856
  tiles = gr.Gallery(label="Clearest and weakest patches (the renders the judge saw)",
857
- columns=6, height=300, object_fit="contain", allow_preview=True)
858
  dl = gr.File(label="Download: fiber.json, fiber.tif, fiber_map.png, report.html, tiles/",
859
- height=88, elem_id="fc-dl")
860
  with gr.Accordion("Full report (report.html)", open=False):
861
  report = gr.HTML()
862
  with gr.Accordion("Record (fiber.json)", open=False):
863
- record = gr.JSON()
864
  with gr.Accordion("About the judges", open=False):
865
  gr.Markdown(ABOUT_JUDGE.format(sha=WEIGHTS_SHA256[:16] + "…", cp=CLAUDE_PATCHES))
866
  with gr.Tab("Fiber or not?", id="game") as game_tab:
@@ -886,7 +900,7 @@ def build_demo():
886
  cgo = gr.Button("Compare", variant="primary")
887
  cside = gr.HTML()
888
  with gr.Accordion("Record (JSON)", open=False):
889
- cout = gr.JSON(label="Side by side")
890
  with gr.Tab("How-to guides", id="howto"):
891
  gr.Markdown(with_figures(HOW_TO), elem_classes="fc-howto")
892
  with gr.Tab("Agents and API", id="agents"):
@@ -911,19 +925,22 @@ def build_demo():
911
  segment.input(on_segment, [sample, segment], volume, **hidden)
912
  judge.change(on_judge, judge, claude_box, **hidden)
913
  outs = [pic, summary, tiles, dl, report, record]
914
- go.click(guard(on_check), [sample, segment, volume, patches, judge, key, max_usd], outs,
915
- concurrency_id="check", concurrency_limit=2, **hidden)
916
- go_up.click(guard(on_upload), [upload, up_vol, patches, judge, key, max_usd], outs,
917
- concurrency_id="check", concurrency_limit=2, **hidden)
918
- nxt.click(guard(new_pair), st, [img_a, img_b, reveal, score_md, st], **hidden)
 
 
 
919
 
920
  def first_pair(s):
921
  if (s or {}).get("i") is not None or not game.ok:
922
  return gr.update(), gr.update(), gr.update(), gr.update(), s
923
  return new_pair(s)
924
- game_tab.select(guard(first_pair), st, [img_a, img_b, reveal, score_md, st], **hidden)
925
- pick_a.click(lambda s: answer("A", s), st, [reveal, score_md, st], **hidden)
926
- pick_b.click(lambda s: answer("B", s), st, [reveal, score_md, st], **hidden)
927
  def on_compare(a, b):
928
  res = compare_results(a, b)
929
  return compare_html(res), res
 
39
  GAME = HERE / "assets" / "game.npz"
40
  GUIDE = HERE / "guide" / "index.html"
41
  CONNECT_HTML = HERE / "connect.html"
42
+ ONESCROLL = HERE / "onescroll.html" # one page scroll bar: frames fit their content (kit spaceconnect render)
43
  PATCHES = [("Quick: 60 patches, about 2 minutes", 60), ("Thorough: 150 patches, about 5 minutes", 150),
44
  ("Whole segment: up to 400 patches, about 15 minutes", 400)]
45
  CLAUDE_PATCHES, CLAUDE_CHECK_N = 40, 10
 
342
  if not GUIDE.is_file():
343
  return "<p>The welcome page is missing from this copy.</p>"
344
  page = GUIDE.read_text()
345
+ # its pictures point at Gradio's file route (a <base> cannot do it); in-page anchors scroll the page to their
346
+ # section (the frame fits its content, onescroll.html), ?tab= links switch the Space's tabs, other links open a
347
+ # new tab (as Lasagna Fields does)
348
  page = page.replace("'assets/", "'/gradio_api/file=assets/").replace('"assets/', '"/gradio_api/file=assets/')
349
  fix = ('<script>document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a");'
350
  'if(!a)return;var h=a.getAttribute("href")||"";if(h.charAt(0)==="#"){e.preventDefault();'
 
440
  return FC.controls(on, rng)[kind]
441
 
442
  def judge_pair(self, a, b, ref):
443
+ """P(the free judge picks a), both scaled by the on render's gray scale as in the checker."""
444
  if self.judge is None:
445
  self.judge = FC.LocalJudge(str(WEIGHTS))
446
  p, _ = self.judge.prefer([FC.to_input(a, ref)], [FC.to_input(b, ref)])
 
515
  <h3 id="read">2. Read the result</h3>
516
 
517
  - **The map** is the segment as traced, flattened on its own mesh grid. Yellow and green: clear fibers. Purple: weak.
518
+ Gray: no sign of fibers. Slate: traced but not scored (holes nearby, masked CT, or not sampled). Black: no surface.
519
  - **Fibers show on** is the share of patches with evidence 0.75 or more. **Median evidence** is the typical patch.
520
  - **Self-check**: on 40 of your patches, the judge must prefer the real render over a noised, a blurred and a
521
  scrambled copy at least 80% of the time. If it fails, do not trust that run.
 
645
  .fc-untested { font-size:13.5px; margin-top:8px; } .fc-untested summary { cursor:pointer; }
646
  .fc-legend { font-size:12.5px; color: var(--body-text-color-subdued); }
647
  .fc-legend i { display:inline-block; width:11px; height:11px; border-radius:3px; margin:0 4px -1px 10px; }
648
+ /* no fixed heights on the frames: onescroll.html fits each to its content, so only the page scrolls */
649
+ .fc-report { width:100%; border:0; border-radius:12px; }
650
+ .fc-guide iframe { width:100%; border:0; border-radius:12px; background:#f6f1e7; }
651
  .fc-howto img { border-radius:10px; border:1px solid var(--border-color-primary); max-width:min(100%, 820px);
652
  display:block; margin:10px 0 4px; }
653
  .fc-howto figure { margin:10px 0 18px; } .fc-howto figcaption { font-size:13px; color: var(--body-text-color-subdued); }
 
668
  .fc-credit { text-align:center; font-size:13px; color: var(--body-text-color-subdued); padding:18px 0 6px; }
669
  .fc-credit a { color: var(--fc-accent) !important; }
670
  #fc-dl .empty { min-height: 64px !important; height: 64px !important; }
671
+ /* the twelve patches grow the gallery instead of scrolling inside it (Gradio's grid-wrap always has a scroll bar) */
672
+ #fc-tiles .grid-wrap { height:auto !important; min-height:0 !important; max-height:none !important;
673
+ overflow-y:visible !important; }
674
+ #fc-tiles .grid-container { grid-template-columns:repeat(6,minmax(0,1fr)) !important; grid-template-rows:none !important;
675
+ grid-auto-rows:auto !important; }
676
  @media (max-width: 640px) {
677
  .fc-head h1 { font-size:24px; }
678
  .fc-head span { display:none; }
679
  #fc-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
 
680
  .fc-strip { grid-template-columns:repeat(2,minmax(0,1fr)); }
681
+ #fc-tiles .grid-container { grid-template-columns:repeat(3,minmax(0,1fr)) !important; }
682
  }
683
  """
684
 
 
695
  <link rel="preconnect" href="https://fonts.googleapis.com">
696
  <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650&display=swap" rel="stylesheet">
697
  """
698
+ HEAD += ONESCROLL.read_text() if ONESCROLL.is_file() else ""
699
 
700
  TAB_JS = """
701
  () => {
 
707
  const btn = btns.find((b) => b.getAttribute('role') === 'tab') || btns[btns.length - 1];
708
  if (btn) btn.click();
709
  };
710
+ window.addEventListener('message', (e) => {
711
+ if (!(e.data && e.data.fcTab)) return;
712
+ pick(e.data.fcTab);
713
+ // the welcome page grows the page (one scroll bar), so a link from it lands at the top of the new tab
714
+ const bar = document.getElementById('fc-tabs');
715
+ if (bar && bar.getBoundingClientRect().top < 0) bar.scrollIntoView({block: 'start'});
716
+ });
717
  const q = new URLSearchParams(window.location.search).get('tab');
718
  if (q) setTimeout(() => pick(q), 300);
719
  }
 
868
  summary = gr.HTML("<p class='fc-meta'>Your map and summary appear here. 60 patches take "
869
  "about two minutes; try <b>Fiber or not?</b> while you wait.</p>")
870
  tiles = gr.Gallery(label="Clearest and weakest patches (the renders the judge saw)",
871
+ columns=6, object_fit="contain", allow_preview=True, elem_id="fc-tiles")
872
  dl = gr.File(label="Download: fiber.json, fiber.tif, fiber_map.png, report.html, tiles/",
873
+ elem_id="fc-dl")
874
  with gr.Accordion("Full report (report.html)", open=False):
875
  report = gr.HTML()
876
  with gr.Accordion("Record (fiber.json)", open=False):
877
+ record = gr.JSON(max_height=None) # grows with the record: the page scrolls
878
  with gr.Accordion("About the judges", open=False):
879
  gr.Markdown(ABOUT_JUDGE.format(sha=WEIGHTS_SHA256[:16] + "…", cp=CLAUDE_PATCHES))
880
  with gr.Tab("Fiber or not?", id="game") as game_tab:
 
900
  cgo = gr.Button("Compare", variant="primary")
901
  cside = gr.HTML()
902
  with gr.Accordion("Record (JSON)", open=False):
903
+ cout = gr.JSON(label="Side by side", max_height=None)
904
  with gr.Tab("How-to guides", id="howto"):
905
  gr.Markdown(with_figures(HOW_TO), elem_classes="fc-howto")
906
  with gr.Tab("Agents and API", id="agents"):
 
925
  segment.input(on_segment, [sample, segment], volume, **hidden)
926
  judge.change(on_judge, judge, claude_box, **hidden)
927
  outs = [pic, summary, tiles, dl, report, record]
928
+ # progress on the visible outputs only: in the folded report and record it would overflow their boxes
929
+ run = dict(concurrency_id="check", concurrency_limit=2, show_progress_on=[pic, summary, tiles, dl], **hidden)
930
+ go.click(guard(on_check), [sample, segment, volume, patches, judge, key, max_usd], outs, **run)
931
+ go_up.click(guard(on_upload), [upload, up_vol, patches, judge, key, max_usd], outs, **run)
932
+ # the progress shows on the two pictures: in the one-line score and the reveal it would not fit, and they
933
+ # would scroll inside the page while the judge works
934
+ pair_progress = dict(show_progress_on=[img_a, img_b], **hidden)
935
+ nxt.click(guard(new_pair), st, [img_a, img_b, reveal, score_md, st], **pair_progress)
936
 
937
  def first_pair(s):
938
  if (s or {}).get("i") is not None or not game.ok:
939
  return gr.update(), gr.update(), gr.update(), gr.update(), s
940
  return new_pair(s)
941
+ game_tab.select(guard(first_pair), st, [img_a, img_b, reveal, score_md, st], **pair_progress)
942
+ pick_a.click(lambda s: answer("A", s), st, [reveal, score_md, st], show_progress="hidden", **hidden)
943
+ pick_b.click(lambda s: answer("B", s), st, [reveal, score_md, st], show_progress="hidden", **hidden)
944
  def on_compare(a, b):
945
  res = compare_results(a, b)
946
  return compare_html(res), res
fcmodel.py CHANGED
@@ -148,7 +148,7 @@ def train(patches, epochs, lr, batch=32, seed=SEED, say=print, negatives=("phase
148
 
149
 
150
  def scores(m, paths):
151
- """Fiber score of each image: the mean over the centre crop and its four quarter turns."""
152
  import torch
153
  out = {}
154
  with torch.no_grad():
 
148
 
149
 
150
  def scores(m, paths):
151
+ """Fiber score of each image: the mean over the center crop and its four quarter turns."""
152
  import torch
153
  out = {}
154
  with torch.no_grad():
fibercheck.py CHANGED
@@ -46,7 +46,7 @@ def read_tifxyz(folder):
46
 
47
 
48
  def normals(xyz, valid):
49
- """Unit normal per vertex from central differences; ok where it and its four neighbours are valid."""
50
  du = np.zeros_like(xyz)
51
  dv = np.zeros_like(xyz)
52
  du[:, 1:-1] = xyz[:, 2:] - xyz[:, :-2]
@@ -200,7 +200,7 @@ def render_patches(folder, ct, level, P, K, stride=None, max_patches=None, maske
200
 
201
 
202
  def to_input(img, ref):
203
- """The frozen PNG grey scale (1 and 99 percentiles of the on render), then z-scored, as the model was trained."""
204
  lo, hi = np.percentile(ref, [1, 99])
205
  a = np.clip((img - lo) / max(hi - lo, 1e-6), 0, 1)
206
  a = np.round(a * 255) / 255
@@ -327,7 +327,7 @@ def fiber_direction(img):
327
 
328
 
329
  def jump_hints(patches, grid_step):
330
- """Patches whose evidence is under 0.5 while every scored neighbour is over 0.8: a drop that may mark a sheet jump
331
  or a stretch off the sheet. Untested idea."""
332
  at = {(p["r"], p["c"]): p["evidence"] for p in patches}
333
  out = []
@@ -421,7 +421,7 @@ def write_maps(rec, out, valid=None):
421
  for p in rec["patches"]:
422
  m[p["r"]:p["r"] + P, p["c"]:p["c"] + P] = p["evidence"]
423
  tifffile.imwrite(out / "fiber.tif", m, compression="zlib")
424
- rgb = colour(m)
425
  if valid is not None:
426
  rgb[np.isnan(m) & valid] = (52, 54, 64) # traced but not scored: the segment's shape
427
  im = Image.fromarray(rgb)
@@ -429,8 +429,8 @@ def write_maps(rec, out, valid=None):
429
  im.resize((W * s, H * s), Image.NEAREST).save(out / "fiber_map.png")
430
 
431
 
432
- def colour(m):
433
- """Evidence 0.5 to 1 on a colour-blind-safe scale (grey below 0.5 means no sign of fiber), holes transparent-dark."""
434
  stops = np.array([[68, 1, 84], [59, 82, 139], [33, 145, 140], [94, 201, 98], [253, 231, 37]], float)
435
  v = np.clip((np.nan_to_num(m, nan=0) - 0.5) / 0.5, 0, 1)
436
  pos = v * (len(stops) - 1)
@@ -443,7 +443,7 @@ def colour(m):
443
 
444
 
445
  def _img_png(a, save=None):
446
- """PNG bytes of a render (1 and 99 percentile grey scale, drawn 2 pixels per voxel); also written to `save`."""
447
  from PIL import Image
448
  b = np.clip((a - np.percentile(a, 1)) / max(np.percentile(a, 99) - np.percentile(a, 1), 1e-6), 0, 1)
449
  im = Image.fromarray((b * 255).astype(np.uint8)).resize((a.shape[1] * 2, a.shape[0] * 2), Image.BILINEAR)
@@ -503,7 +503,7 @@ figure img{{width:100%;border-radius:6px}} figcaption{{font-size:13px;color:var(
503
  <div class="card"><div class="big {'ok' if ck['passed'] else 'bad'}">{'passed' if ck['passed'] else 'failed'}</div>self-check: noise {ck['against']['noise']:.0%}, blur {ck['against']['blur']:.0%}, scramble {ck['against']['scramble']:.0%}</div>
504
  </div>
505
  <h2>Fiber map</h2><div class="map"><img src="data:image/png;base64,{mp}" alt="fiber evidence over the segment">
506
- <div class="sub">Yellow: clear fiber. Purple: weak. Grey: no sign of fiber. Slate: traced but not scored. Black: no surface.</div></div>
507
  <h2>Clearest and weakest patches</h2><div class="grid">{''.join(tiles)}</div>
508
  <h2>Method</h2><p>The Progress Prize asks entrants to "show visually that papyrus fibers are visible on your output surface"
509
  (scrollprize.org/prizes). For each patch, the CT is rendered along the surface and the judge compares that render with
 
46
 
47
 
48
  def normals(xyz, valid):
49
+ """Unit normal per vertex from central differences; ok where it and its four neighbors are valid."""
50
  du = np.zeros_like(xyz)
51
  dv = np.zeros_like(xyz)
52
  du[:, 1:-1] = xyz[:, 2:] - xyz[:, :-2]
 
200
 
201
 
202
  def to_input(img, ref):
203
+ """The frozen PNG gray scale (1 and 99 percentiles of the on render), then z-scored, as the model was trained."""
204
  lo, hi = np.percentile(ref, [1, 99])
205
  a = np.clip((img - lo) / max(hi - lo, 1e-6), 0, 1)
206
  a = np.round(a * 255) / 255
 
327
 
328
 
329
  def jump_hints(patches, grid_step):
330
+ """Patches whose evidence is under 0.5 while every scored neighbor is over 0.8: a drop that may mark a sheet jump
331
  or a stretch off the sheet. Untested idea."""
332
  at = {(p["r"], p["c"]): p["evidence"] for p in patches}
333
  out = []
 
421
  for p in rec["patches"]:
422
  m[p["r"]:p["r"] + P, p["c"]:p["c"] + P] = p["evidence"]
423
  tifffile.imwrite(out / "fiber.tif", m, compression="zlib")
424
+ rgb = color(m)
425
  if valid is not None:
426
  rgb[np.isnan(m) & valid] = (52, 54, 64) # traced but not scored: the segment's shape
427
  im = Image.fromarray(rgb)
 
429
  im.resize((W * s, H * s), Image.NEAREST).save(out / "fiber_map.png")
430
 
431
 
432
+ def color(m):
433
+ """Evidence 0.5 to 1 on a color-blind-safe scale (gray below 0.5 means no sign of fiber), holes transparent-dark."""
434
  stops = np.array([[68, 1, 84], [59, 82, 139], [33, 145, 140], [94, 201, 98], [253, 231, 37]], float)
435
  v = np.clip((np.nan_to_num(m, nan=0) - 0.5) / 0.5, 0, 1)
436
  pos = v * (len(stops) - 1)
 
443
 
444
 
445
  def _img_png(a, save=None):
446
+ """PNG bytes of a render (1 and 99 percentile gray scale, drawn 2 pixels per voxel); also written to `save`."""
447
  from PIL import Image
448
  b = np.clip((a - np.percentile(a, 1)) / max(np.percentile(a, 99) - np.percentile(a, 1), 1e-6), 0, 1)
449
  im = Image.fromarray((b * 255).astype(np.uint8)).resize((a.shape[1] * 2, a.shape[0] * 2), Image.BILINEAR)
 
503
  <div class="card"><div class="big {'ok' if ck['passed'] else 'bad'}">{'passed' if ck['passed'] else 'failed'}</div>self-check: noise {ck['against']['noise']:.0%}, blur {ck['against']['blur']:.0%}, scramble {ck['against']['scramble']:.0%}</div>
504
  </div>
505
  <h2>Fiber map</h2><div class="map"><img src="data:image/png;base64,{mp}" alt="fiber evidence over the segment">
506
+ <div class="sub">Yellow: clear fiber. Purple: weak. Gray: no sign of fiber. Slate: traced but not scored. Black: no surface.</div></div>
507
  <h2>Clearest and weakest patches</h2><div class="grid">{''.join(tiles)}</div>
508
  <h2>Method</h2><p>The Progress Prize asks entrants to "show visually that papyrus fibers are visible on your output surface"
509
  (scrollprize.org/prizes). For each patch, the CT is rendered along the surface and the judge compares that render with
onescroll.html ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- space-onescroll:start -->
2
+ <!-- one page scroll bar: written by python -m kit spaceconnect render; do not edit -->
3
+ <style>iframe[srcdoc]{display:block;max-height:none!important}</style>
4
+ <script>
5
+ (function () {
6
+ if (window.__oneScroll) return;
7
+ window.__oneScroll = true;
8
+ var CSS = 'html,body{height:auto!important;min-height:0!important}html{overflow:hidden!important}';
9
+ function size(f) {
10
+ var d, st = f.__os;
11
+ try { d = f.contentDocument; } catch (e) { return; }
12
+ if (!d || !d.body || st.off) return;
13
+ var de = d.documentElement, r = Math.ceil(de.getBoundingClientRect().height), over = de.scrollHeight;
14
+ var ch = f.clientHeight, t = r;
15
+ if (over > ch + 1) t = Math.max(r, over);
16
+ else if (r >= st.r) t = Math.max(r, ch);
17
+ st.r = r;
18
+ if (Math.abs(t - ch) < 2) return;
19
+ var now = Date.now();
20
+ st.n = now - st.t0 < 4000 ? st.n + 1 : 1;
21
+ if (st.n === 1) st.t0 = now;
22
+ if (st.n > 40) { st.off = true; f.removeAttribute('scrolling'); var g = d.getElementById('space-onescroll'); if (g) g.remove(); return; }
23
+ f.style.setProperty('height', t + 'px', 'important');
24
+ }
25
+ function attach(f) {
26
+ var d, st = f.__os;
27
+ try { d = f.contentDocument; } catch (e) { return; }
28
+ if (!d || !d.documentElement || st.doc === d) return;
29
+ st.doc = d; st.r = Infinity; st.n = 0; st.off = false;
30
+ if (!d.getElementById('space-onescroll')) {
31
+ var s = d.createElement('style'); s.id = 'space-onescroll'; s.textContent = CSS;
32
+ (d.head || d.documentElement).appendChild(s);
33
+ }
34
+ f.setAttribute('scrolling', 'no');
35
+ f.style.setProperty('min-height', '0', 'important');
36
+ f.style.setProperty('max-height', 'none', 'important');
37
+ var w = d.defaultView, go = function () { size(f); };
38
+ if (w && w.ResizeObserver) { var ro = new w.ResizeObserver(go); ro.observe(d.documentElement); if (d.body) ro.observe(d.body); }
39
+ d.addEventListener('load', go, true);
40
+ go();
41
+ }
42
+ function bind(f) {
43
+ if (f.__os || f.getAttribute('data-scroll') === 'inner') return;
44
+ try { if (!f.contentDocument) return; } catch (e) { return; }
45
+ f.__os = {t0: 0, n: 0, r: 0};
46
+ f.addEventListener('load', function () { attach(f); });
47
+ if (f.contentDocument.readyState !== 'loading') attach(f);
48
+ else f.contentDocument.addEventListener('DOMContentLoaded', function () { attach(f); });
49
+ }
50
+ function scan() { var fs = document.getElementsByTagName('iframe'); for (var i = 0; i < fs.length; i++) bind(fs[i]); }
51
+ function start() {
52
+ scan();
53
+ new MutationObserver(scan).observe(document.documentElement, {childList: true, subtree: true});
54
+ window.addEventListener('resize', function () {
55
+ var fs = document.getElementsByTagName('iframe');
56
+ for (var i = 0; i < fs.length; i++) if (fs[i].__os) size(fs[i]);
57
+ });
58
+ }
59
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
60
+ })();
61
+ </script>
62
+ <!-- space-onescroll:end -->