Download index.html from HuggingFaceBio/carbon-tokenization-02-tokenizers-compared: direct link, hf CLI and curl.
- Browser
- Download file 45.1 kB
-
https://huggingface.co/spaces/HuggingFaceBio/carbon-tokenization-02-tokenizers-compared/resolve/main/index.html
- Command line
-
hf download hf://spaces/HuggingFaceBio/carbon-tokenization-02-tokenizers-compared/index.html
-
curl -L -o index.html https://huggingface.co/spaces/HuggingFaceBio/carbon-tokenization-02-tokenizers-compared/resolve/main/index.html
45.1 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Per-base, BPE, and 6-mer on the same DNA sequence</title> | |
| <meta name="color-scheme" content="light"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap"> | |
| <style> | |
| :root { | |
| --bg: #f7f5ee; | |
| --ink: #1f1f1d; | |
| --ink-soft: #5b5b56; | |
| --ink-faint: #8a8a83; | |
| --rule: #e3e1d6; | |
| --hair: #eee; | |
| --green: #317f3f; | |
| --green-dark: #1f5024; | |
| --green-tint: #f4f8f4; | |
| --amber: #b8862c; | |
| --amber-dark: #6b4d18; | |
| --red: #b00020; | |
| --blue: #2c5aa0; | |
| --card: #fff; | |
| --soft-cream: #fafaf6; | |
| --base-a: #1A7A40; | |
| --base-t: #b00020; | |
| --base-c: #2c5aa0; | |
| --base-g: #b8862c; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| html { scroll-behavior: smooth; } | |
| body { | |
| font-family: "Inter", "Helvetica Neue", sans-serif; | |
| font-size: 14px; font-weight: 300; line-height: 1.7; | |
| color: var(--ink); | |
| background: var(--bg); | |
| } | |
| ::-webkit-scrollbar { width: 8px; height: 8px; } | |
| ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| /* --- Page header ----------------------------------------------------- */ | |
| .page-header { | |
| border-bottom: 1px solid var(--rule); | |
| background: var(--bg); | |
| position: sticky; top: 0; z-index: 50; | |
| backdrop-filter: saturate(180%) blur(6px); | |
| } | |
| .page-header__inner { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 14px 32px; | |
| display: flex; align-items: baseline; gap: 16px; | |
| } | |
| .wordmark { | |
| font-family: "JetBrains Mono", monospace; | |
| font-weight: 700; | |
| font-size: 16px; | |
| letter-spacing: 1px; | |
| } | |
| .wordmark .caret { color: var(--green); margin-right: 4px; } | |
| .wordmark__sub { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 11px; font-weight: 500; | |
| text-transform: uppercase; letter-spacing: 2px; | |
| color: var(--ink-soft); | |
| margin-left: 4px; | |
| } | |
| .page-header__spacer { flex: 1; } | |
| .page-header__crumbs { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 10px; letter-spacing: 1.4px; | |
| text-transform: uppercase; color: var(--ink-faint); | |
| } | |
| .page-header__crumbs a { color: var(--ink-soft); text-decoration: none; } | |
| .page-header__crumbs a:hover { color: var(--green); } | |
| /* --- Lede ----------------------------------------------------------- */ | |
| .tab-lede { | |
| max-width: 1200px; margin: 56px auto 0; | |
| padding: 0 32px; | |
| } | |
| .tab-lede__rail { | |
| border-left: 3px solid var(--green); | |
| padding: 4px 0 4px 22px; | |
| max-width: 820px; | |
| } | |
| .tab-lede__eyebrow { | |
| display: block; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 11px; font-weight: 500; | |
| letter-spacing: 0.22em; text-transform: uppercase; | |
| color: var(--green); margin-bottom: 12px; | |
| } | |
| .tab-lede__title { | |
| margin: 0 0 22px; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 34px; font-weight: 500; | |
| letter-spacing: -0.01em; line-height: 1.12; | |
| color: var(--ink); | |
| } | |
| .tab-lede__lead { | |
| margin: 0; max-width: 760px; | |
| font-family: "Inter", sans-serif; | |
| font-size: 19px; font-weight: 300; line-height: 1.5; | |
| letter-spacing: -0.005em; color: #2d2d2a; | |
| } | |
| /* --- Post body ------------------------------------------------------ */ | |
| .post { | |
| max-width: 760px; | |
| margin: 32px auto 0; | |
| padding: 0 32px 96px; | |
| } | |
| .post h2 { | |
| margin: 64px 0 18px; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 22px; font-weight: 500; | |
| letter-spacing: -0.005em; line-height: 1.3; | |
| color: var(--ink); | |
| padding-top: 12px; | |
| border-top: 1px solid var(--rule); | |
| } | |
| .post h2:first-child { border-top: none; padding-top: 0; } | |
| .post h3 { | |
| margin: 40px 0 12px; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 15px; font-weight: 500; | |
| letter-spacing: 0; | |
| color: var(--ink); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .post p { margin: 0 0 14px; } | |
| .post p > code, .post li > code { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 0.86em; | |
| background: var(--soft-cream); | |
| border: 1px solid var(--rule); | |
| padding: 1px 5px; | |
| border-radius: 2px; | |
| color: var(--green-dark); | |
| } | |
| .post pre { | |
| margin: 14px 0 18px; | |
| padding: 14px 16px; | |
| background: var(--card); | |
| border: 1px solid #ddd; | |
| overflow-x: auto; | |
| } | |
| .post pre code { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 12px; line-height: 1.6; | |
| color: var(--ink); | |
| background: transparent; border: none; padding: 0; | |
| } | |
| .post a { | |
| color: var(--green-dark); | |
| text-decoration: underline; | |
| text-decoration-thickness: 1px; | |
| text-underline-offset: 2px; | |
| } | |
| .post a:hover { color: var(--green); } | |
| .post strong { font-weight: 600; color: var(--ink); } | |
| .post-image { | |
| margin: 24px 0; | |
| } | |
| .post-image img { | |
| display: block; | |
| width: 100%; height: auto; | |
| border: 1px solid var(--rule); | |
| } | |
| /* --- Widget shell (carbon-demo style) ------------------------------- */ | |
| .widget { | |
| max-width: 920px; | |
| margin: 40px auto 8px; | |
| } | |
| .widget__head { | |
| display: flex; align-items: baseline; gap: 14px; | |
| margin-bottom: 6px; | |
| } | |
| .widget__eyebrow { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 10px; font-weight: 500; | |
| letter-spacing: 0.22em; text-transform: uppercase; | |
| color: var(--green); | |
| } | |
| .widget__title { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 14px; font-weight: 500; | |
| color: var(--ink); letter-spacing: 0; | |
| } | |
| .widget__caption { | |
| font-family: "Inter", sans-serif; | |
| font-size: 12px; color: var(--ink-soft); | |
| margin: 8px 4px 14px; | |
| line-height: 1.5; | |
| } | |
| .demo { | |
| background: var(--card); border: 1px solid #ddd; | |
| padding: 22px; margin: 8px 0; | |
| } | |
| .demo-toolbar { | |
| display: flex; gap: 8px; align-items: center; flex-wrap: wrap; | |
| margin-bottom: 14px; | |
| font-family: "JetBrains Mono", monospace; font-size: 10px; | |
| color: #666; text-transform: uppercase; letter-spacing: 1.4px; | |
| } | |
| .demo-toolbar .spacer { flex: 1; } | |
| .demo-label { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 9.5px; color: #6b7a6e; | |
| text-transform: uppercase; letter-spacing: 1.6px; | |
| margin: 6px 0 6px; | |
| } | |
| .pill, button.action { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 11px; font-weight: 400; | |
| padding: 5px 11px; border: 1px solid #ccc; border-radius: 3px; | |
| background: #fff; color: #555; cursor: pointer; | |
| text-transform: uppercase; letter-spacing: 1.5px; | |
| transition: all 0.12s; | |
| } | |
| .pill:hover, button.action:hover { border-color: #888; color: var(--ink); } | |
| .pill.active, button.action.primary { background: var(--ink); color: #fff; border-color: var(--ink); } | |
| .pill.active:hover, button.action.primary:hover { background: #000; } | |
| .pill.gh { background: var(--green); color: #fff; border-color: var(--green); } | |
| .pill.gh:hover { background: var(--green-dark); border-color: var(--green-dark); } | |
| .pills { display: inline-flex; flex-wrap: wrap; gap: 6px; } | |
| .status { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 10px; color: #666; | |
| text-transform: uppercase; letter-spacing: 1.5px; | |
| display: inline-flex; align-items: center; gap: 6px; | |
| margin-left: 8px; | |
| } | |
| .status .dot { | |
| display: inline-block; width: 6px; height: 6px; border-radius: 50%; | |
| background: var(--green); | |
| } | |
| input.seq-input, textarea.seq-input { | |
| width: 100%; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 13px; | |
| padding: 9px 12px; | |
| border: 1px solid #ccc; | |
| border-radius: 3px; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| color: var(--ink); | |
| background: var(--soft-cream); | |
| resize: vertical; | |
| } | |
| textarea.seq-input { letter-spacing: 0.5px; } | |
| input.seq-input:focus, textarea.seq-input:focus { | |
| outline: none; | |
| border-color: var(--green); | |
| background: #fff; | |
| } | |
| /* base + token chips */ | |
| .base, .tok { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 11px; | |
| letter-spacing: 0.5px; | |
| } | |
| .base { | |
| display: inline-block; | |
| padding: 2px 5px; | |
| font-weight: 500; | |
| margin: 1px; | |
| color: #fff; | |
| border-radius: 2px; | |
| min-width: 16px; | |
| text-align: center; | |
| } | |
| .base.A { background: var(--base-a); } | |
| .base.T { background: var(--base-t); } | |
| .base.C { background: var(--base-c); } | |
| .base.G { background: var(--base-g); } | |
| .base.dim { opacity: 0.4; } | |
| .base.outline { | |
| background: transparent; | |
| border: 1px solid currentColor; | |
| } | |
| .base.outline.A { color: var(--base-a); } | |
| .base.outline.T { color: var(--base-t); } | |
| .base.outline.C { color: var(--base-c); } | |
| .base.outline.G { color: var(--base-g); } | |
| .tok { | |
| display: inline-flex; | |
| align-items: center; | |
| padding: 4px 8px; | |
| margin: 2px; | |
| border: 1px solid #ccc; | |
| border-radius: 3px; | |
| background: #fff; | |
| color: var(--ink); | |
| } | |
| .tok.kmer { | |
| background: rgba(49,127,63,0.10); | |
| border-color: rgba(49,127,63,0.5); | |
| color: var(--green-dark); | |
| font-weight: 500; | |
| } | |
| .tok.bpe { | |
| background: rgba(184,134,44,0.10); | |
| border-color: rgba(184,134,44,0.5); | |
| color: var(--amber-dark); | |
| } | |
| .tok.text { | |
| background: #fff; | |
| border-color: #ccc; | |
| color: var(--ink); | |
| } | |
| .tok.boundary { | |
| background: var(--ink); | |
| color: #fff; | |
| border-color: var(--ink); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| font-size: 10px; | |
| } | |
| .tok.dim { color: #888; background: var(--soft-cream); } | |
| .tok.ok { background: var(--green); color: #fff; border-color: var(--green); } | |
| .tok.bad { color: var(--red); border-color: rgba(176,0,32,0.4); background: rgba(176,0,32,0.05); } | |
| .tok.selected { | |
| outline: 2px solid var(--green); | |
| outline-offset: 1px; | |
| } | |
| .token-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0; } | |
| /* count badges */ | |
| .count-badge { | |
| display: inline-block; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 10px; font-weight: 600; | |
| background: var(--ink); color: var(--bg); | |
| padding: 2px 7px; border-radius: 2px; | |
| letter-spacing: 1px; | |
| } | |
| .count-badge.green { background: var(--green); color: #fff; } | |
| .count-badge.amber { background: var(--amber); color: #fff; } | |
| /* W1 · animated tokenization (source on top, schemes drop down) ------ */ | |
| .tk-anim { margin-top: 18px; } | |
| .tk-source, .tk-scheme { | |
| display: grid; | |
| grid-template-columns: 124px 1fr; | |
| gap: 18px; | |
| align-items: start; | |
| padding: 8px 0; | |
| } | |
| .tk-scheme { padding: 10px 0; min-height: 32px; } | |
| .tk-source { padding-bottom: 14px; border-bottom: 1px solid var(--rule); margin-bottom: 6px; } | |
| .tk-meta { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 3px; | |
| padding-top: 2px; | |
| } | |
| .tk-label { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 9.5px; | |
| font-weight: 500; | |
| color: var(--ink-soft); | |
| text-transform: uppercase; | |
| letter-spacing: 1.6px; | |
| } | |
| .tk-stat { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 10px; | |
| color: var(--ink-faint); | |
| letter-spacing: 0.5px; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .tk-stat .n { | |
| font-weight: 600; | |
| color: var(--ink); | |
| } | |
| .tk-scheme[data-scheme="bpe"] .tk-stat .n { color: var(--amber-dark); } | |
| .tk-scheme[data-scheme="kmer"] .tk-stat .n { color: var(--green-dark); } | |
| /* Source: tight grid of small chips */ | |
| .tk-source-strip { | |
| display: flex; | |
| flex-wrap: wrap; | |
| row-gap: 2px; | |
| } | |
| .tk-source-strip .tk-base { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 13px; | |
| height: 17px; | |
| color: #fff; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 10px; | |
| font-weight: 500; | |
| border-radius: 1px; | |
| flex: 0 0 13px; | |
| transition: opacity 0.35s ease, filter 0.35s ease; | |
| } | |
| .tk-source-strip .tk-base + .tk-base { margin-left: 1px; } | |
| .tk-source-strip .tk-base.consumed { opacity: 0.18; filter: grayscale(0.6); } | |
| .tk-base.A { background: var(--base-a); } | |
| .tk-base.T { background: var(--base-t); } | |
| .tk-base.C { background: var(--base-c); } | |
| .tk-base.G { background: var(--base-g); } | |
| /* Scheme strips: hold dropped tokens with <> brackets */ | |
| .tk-scheme-strip { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 1px 8px; | |
| min-height: 20px; | |
| } | |
| .tk-token { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 12px; | |
| font-weight: 500; | |
| letter-spacing: 0.5px; | |
| display: inline-flex; | |
| align-items: center; | |
| opacity: 0; | |
| will-change: transform, opacity; | |
| } | |
| .tk-token.dropped { | |
| opacity: 1; | |
| transition: | |
| transform 0.48s cubic-bezier(0.22, 1, 0.36, 1), | |
| opacity 0.32s ease; | |
| } | |
| .tk-token .br { color: var(--ink-faint); font-weight: 400; } | |
| .tk-token.tail .br { color: var(--ink-faint); } | |
| .tk-token.tail { opacity: 0.55; } | |
| .tk-token.tail.dropped { opacity: 0.55; } | |
| /* Layouts */ | |
| .grid-3 { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr 1fr; | |
| gap: 14px; | |
| margin-top: 14px; | |
| } | |
| .grid-2 { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 14px; | |
| margin-top: 14px; | |
| } | |
| @media (max-width: 720px) { | |
| .grid-3, .grid-2 { grid-template-columns: 1fr; } | |
| } | |
| .col-card { | |
| background: var(--soft-cream); | |
| border: 1px solid var(--rule); | |
| padding: 14px; | |
| } | |
| .col-card .col-title { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 10px; font-weight: 500; | |
| letter-spacing: 1.6px; text-transform: uppercase; | |
| color: var(--ink-soft); | |
| margin-bottom: 8px; | |
| } | |
| .col-card .col-foot { | |
| margin-top: 10px; | |
| display: flex; align-items: center; gap: 8px; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 10px; color: var(--ink-soft); | |
| text-transform: uppercase; letter-spacing: 1.4px; | |
| } | |
| /* Bar chart helpers */ | |
| .bar-row { | |
| display: grid; | |
| grid-template-columns: 18px 1fr 64px; | |
| align-items: center; | |
| gap: 8px; | |
| margin: 3px 0; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 11px; | |
| } | |
| .bar-row .bar-label { color: var(--ink-soft); font-weight: 500; text-align: center; } | |
| .bar-row .bar-track { | |
| position: relative; | |
| height: 14px; | |
| background: var(--soft-cream); | |
| border: 1px solid var(--rule); | |
| } | |
| .bar-row .bar-fill { | |
| height: 100%; | |
| transition: width 220ms ease, background-color 220ms ease; | |
| } | |
| .bar-row .bar-val { text-align: right; color: var(--ink); font-variant-numeric: tabular-nums; } | |
| .bar-row.highlight .bar-track { box-shadow: 0 0 0 2px var(--green-tint); } | |
| /* 6-position grid (used in W4, W5, W6) */ | |
| .posgrid { | |
| display: grid; | |
| grid-template-columns: repeat(6, 1fr); | |
| gap: 8px; | |
| } | |
| .posgrid__col { | |
| background: var(--soft-cream); | |
| border: 1px solid var(--rule); | |
| border-radius: 2px; | |
| padding: 8px 6px 6px; | |
| } | |
| .posgrid__pos { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 9px; color: var(--ink-faint); | |
| text-transform: uppercase; letter-spacing: 1.4px; | |
| text-align: center; margin-bottom: 6px; | |
| } | |
| .posgrid__bars { | |
| display: flex; flex-direction: column; gap: 3px; | |
| } | |
| .posgrid .mini-row { | |
| display: grid; grid-template-columns: 12px 1fr 32px; | |
| align-items: center; gap: 4px; | |
| font-family: "JetBrains Mono", monospace; font-size: 10px; | |
| } | |
| .posgrid .mini-row .ml { color: var(--ink-soft); text-align: center; font-weight: 500; } | |
| .posgrid .mini-row .mt { position: relative; height: 10px; background: #fff; border: 1px solid var(--rule); } | |
| .posgrid .mini-row .mf { display: block; height: 100%; transition: width 200ms ease; } | |
| .posgrid .mini-row .mv { text-align: right; color: var(--ink); font-variant-numeric: tabular-nums; font-size: 9.5px; } | |
| .posgrid__col.argmax-A { box-shadow: inset 0 0 0 1px var(--base-a); } | |
| .posgrid__col.argmax-T { box-shadow: inset 0 0 0 1px var(--base-t); } | |
| .posgrid__col.argmax-C { box-shadow: inset 0 0 0 1px var(--base-c); } | |
| .posgrid__col.argmax-G { box-shadow: inset 0 0 0 1px var(--base-g); } | |
| .posgrid__col.locked { background: #fff; } | |
| .posgrid__col.frozen .posgrid__bars { opacity: 0.4; } | |
| .posgrid__sel { display: flex; gap: 3px; margin-top: 8px; } | |
| .cond-pick { | |
| flex: 1 1 0; min-width: 0; | |
| font-family: "JetBrains Mono", monospace; font-size: 10px; font-weight: 600; | |
| padding: 3px 0; border: 1px solid var(--rule); border-radius: 2px; | |
| background: #fff; cursor: pointer; | |
| transition: background 0.12s, border-color 0.12s, color 0.12s; | |
| } | |
| .cond-pick:hover { border-color: #888; } | |
| .cond-pick.active { color: #fff; } | |
| .cond-pick.active.b-A { background: var(--base-a); border-color: var(--base-a); } | |
| .cond-pick.active.b-T { background: var(--base-t); border-color: var(--base-t); } | |
| .cond-pick.active.b-C { background: var(--base-c); border-color: var(--base-c); } | |
| .cond-pick.active.b-G { background: var(--base-g); border-color: var(--base-g); } | |
| /* W3 · real Carbon-3B per-token scoring --------------------------- */ | |
| .w3-itok { cursor: pointer; transition: outline-color 0.12s; } | |
| .w3-itok:hover { border-color: #888; } | |
| .w3-sub { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 11px; color: var(--ink-soft); | |
| margin: 2px 0 10px; line-height: 1.7; | |
| } | |
| .w3-sub b { color: var(--ink); font-weight: 600; } | |
| .w3-sub .tok { margin: 0 2px; } | |
| .w3-toprow { | |
| display: grid; | |
| grid-template-columns: 92px 1fr 52px; | |
| gap: 12px; align-items: center; | |
| margin: 4px 0; | |
| font-family: "JetBrains Mono", monospace; font-size: 11px; | |
| } | |
| .w3-toprow .km { font-weight: 500; letter-spacing: 0.5px; } | |
| .w3-toprow .bar { height: 13px; background: var(--soft-cream); border: 1px solid var(--rule); } | |
| .w3-toprow .fill { height: 100%; background: var(--green); transition: width 0.35s ease; } | |
| .w3-toprow .pct { text-align: right; font-variant-numeric: tabular-nums; color: var(--ink); } | |
| .w3-toprow.observed .km { color: var(--ink); } | |
| .w3-toprow.observed .fill { background: var(--amber); } | |
| .w3-io { display: flex; align-items: flex-end; gap: 18px; flex-wrap: wrap; margin-top: 6px; } | |
| .w3-io__group { display: flex; flex-direction: column; gap: 6px; } | |
| .w3-io__lab { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 9.5px; letter-spacing: 1.6px; text-transform: uppercase; color: var(--ink-faint); | |
| } | |
| .w3-io__arrow { font-family: "JetBrains Mono", monospace; font-size: 16px; color: var(--ink-faint); padding-bottom: 5px; } | |
| .w3-gt-tok { background: rgba(184,134,44,0.12); border-color: rgba(184,134,44,0.5) ; } | |
| .w3-gt-tok { text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 2px; } | |
| .w3-chart { display: block; width: 100%; height: auto; margin-top: 6px; background: #fff; border: 1px solid #eee; } | |
| .w3-legend { | |
| display: flex; gap: 20px; flex-wrap: wrap; margin-top: 8px; | |
| font-family: "JetBrains Mono", monospace; font-size: 10px; | |
| color: var(--ink-soft); text-transform: uppercase; letter-spacing: 1px; | |
| } | |
| .w3-legend span { display: inline-flex; align-items: center; gap: 6px; } | |
| .w3-legend svg { width: 20px; height: 8px; display: inline-block; vertical-align: middle; } | |
| .w3-top { display: flex; gap: 6px; margin-top: 12px; } | |
| .w3-topchip { | |
| flex: 1 1 0; min-width: 0; | |
| display: inline-flex; flex-direction: column; align-items: center; gap: 2px; | |
| font-family: "JetBrains Mono", monospace; | |
| border: 1px solid var(--rule); background: var(--soft-cream); | |
| border-radius: 3px; padding: 6px 4px; | |
| } | |
| .w3-topchip .km { font-size: 12px; font-weight: 500; letter-spacing: 0.5px; } | |
| .w3-topchip.ground-truth .km { text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 2px; } | |
| .w3-topchip .pct { font-size: 9px; color: var(--ink-soft); font-variant-numeric: tabular-nums; } | |
| .posgrid__col.clickable { cursor: pointer; } | |
| .posgrid__col.sel { outline: 2px solid var(--ink); outline-offset: 2px; } | |
| .w3-allgrid { | |
| display: grid; | |
| grid-template-columns: repeat(128, 1fr); | |
| gap: 1px; | |
| margin-top: 8px; | |
| } | |
| .w3-cell { aspect-ratio: 1; background: #d3d0c4; transition: background 0.15s ease; } | |
| .w3-cell.first { | |
| outline: 2px solid var(--ink); | |
| outline-offset: 1px; | |
| position: relative; z-index: 2; | |
| } | |
| .w3-order { | |
| display: inline-flex; align-items: center; gap: 8px; | |
| font-family: "JetBrains Mono", monospace; font-size: 9.5px; | |
| text-transform: uppercase; letter-spacing: 1.4px; color: var(--ink-faint); | |
| } | |
| .w3-snake { width: 42px; height: auto; color: var(--ink-soft); display: block; } | |
| /* WCOND · conditional single-nucleotide decoding -------------------- */ | |
| .cond-slots { display: flex; gap: 6px; } | |
| .cond-slot { | |
| width: 40px; height: 40px; | |
| display: flex; align-items: center; justify-content: center; | |
| border: 1px solid var(--rule); border-radius: 3px; background: var(--soft-cream); | |
| font-family: "JetBrains Mono", monospace; font-size: 17px; font-weight: 600; | |
| color: var(--ink); | |
| } | |
| .cond-slot.active { outline: 2px solid var(--ink); outline-offset: 1px; color: var(--ink-faint); } | |
| .cond-slot.pending { color: var(--ink-faint); background: transparent; border-style: dashed; } | |
| .cond-slot.A { background: rgba(26,122,64,0.13); border-color: rgba(26,122,64,0.42); color: var(--base-a); } | |
| .cond-slot.T { background: rgba(176,0,32,0.09); border-color: rgba(176,0,32,0.36); color: var(--base-t); } | |
| .cond-slot.C { background: rgba(44,90,160,0.11); border-color: rgba(44,90,160,0.40); color: var(--base-c); } | |
| .cond-slot.G { background: rgba(184,134,44,0.14); border-color: rgba(184,134,44,0.42); color: var(--amber-dark); } | |
| .cond-choices { display: flex; gap: 10px; } | |
| .cond-choice { | |
| flex: 1 1 0; cursor: pointer; border: 1px solid var(--rule); border-radius: 3px; | |
| padding: 10px 12px; background: var(--soft-cream); | |
| display: flex; flex-direction: column; gap: 7px; | |
| transition: background 0.12s, border-color 0.12s; | |
| } | |
| .cond-choice:hover { background: #fff; border-color: #888; } | |
| .cond-choice__top { display: flex; justify-content: space-between; align-items: baseline; font-family: "JetBrains Mono", monospace; } | |
| .cond-choice__base { font-size: 16px; font-weight: 600; } | |
| .cond-choice__pct { font-size: 11px; color: var(--ink-soft); font-variant-numeric: tabular-nums; } | |
| .cond-choice__bar { height: 8px; background: #fff; border: 1px solid var(--rule); } | |
| .cond-choice__fill { display: block; height: 100%; transition: width 0.25s ease; } | |
| .cond-done { font-family: "JetBrains Mono", monospace; font-size: 13px; color: var(--ink); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } | |
| .cond-note { | |
| font-family: "Inter", sans-serif; font-size: 12px; color: var(--ink-soft); | |
| background: var(--soft-cream); border-left: 2px solid var(--green); | |
| padding: 9px 12px; margin-top: 14px; line-height: 1.5; | |
| } | |
| .cond-note .tok { margin: 0 2px; } | |
| /* FNS factorization figure ------------------------------------------ */ | |
| .fns-seq-input { | |
| width: 120px; | |
| text-align: center; | |
| text-transform: uppercase; | |
| } | |
| .fns-viz { | |
| font-family: "JetBrains Mono", monospace; | |
| padding: 10px 0 6px; | |
| } | |
| .fns-truth, .fns-row { | |
| display: grid; | |
| grid-template-columns: 222px repeat(6, 34px) 28px; | |
| align-items: center; | |
| column-gap: 7px; | |
| min-width: 480px; | |
| } | |
| .fns-truth { margin-bottom: 16px; } | |
| .fns-row { margin: 12px 0; } | |
| .fns-gtlabel { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 10px; | |
| font-weight: 400; | |
| letter-spacing: 1.6px; | |
| text-transform: uppercase; | |
| color: var(--ink-faint); | |
| } | |
| .fns-eq { | |
| font-size: 16px; | |
| color: var(--ink); | |
| text-align: right; | |
| white-space: nowrap; | |
| } | |
| .fns-eq sub, .fns-eq sup { font-size: 11px; } | |
| .fns-eq .sum { color: var(--ink-soft); } | |
| .fns-bracket { | |
| font-family: "Inter", sans-serif; | |
| font-size: 34px; | |
| font-weight: 200; | |
| line-height: 0; | |
| color: var(--ink-soft); | |
| vertical-align: middle; | |
| transform: scaleX(0.7); | |
| display: inline-block; | |
| } | |
| .fns-box { | |
| width: 30px; height: 30px; | |
| display: inline-flex; align-items: center; justify-content: center; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 14px; font-weight: 600; | |
| border-radius: 3px; border: 1px solid; | |
| transition: background 0.15s, border-color 0.15s, color 0.15s; | |
| } | |
| .fns-box.A { background: rgba(26,122,64,0.13); border-color: rgba(26,122,64,0.42); color: var(--base-a); } | |
| .fns-box.T { background: rgba(176,0,32,0.09); border-color: rgba(176,0,32,0.36); color: var(--base-t); } | |
| .fns-box.C { background: rgba(44,90,160,0.11); border-color: rgba(44,90,160,0.40); color: var(--base-c); } | |
| .fns-box.G { background: rgba(184,134,44,0.14); border-color: rgba(184,134,44,0.42); color: var(--amber-dark); } | |
| .fns-box.wild { opacity: 0.38; } | |
| .fns-loss { | |
| margin-top: 20px; | |
| padding-top: 16px; | |
| border-top: 1px solid var(--rule); | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 16px; | |
| color: var(--ink); | |
| text-align: center; | |
| } | |
| .fns-loss sup, .fns-loss sub { font-size: 11px; } | |
| .fns-loss .frac { display: inline-block; margin: 0 1px; } | |
| .fns-loss .fns-term { margin: 0 3px; white-space: nowrap; } | |
| .fns-explain { | |
| margin-top: 18px; | |
| padding-top: 16px; | |
| border-top: 1px solid var(--rule); | |
| font-family: "Inter", sans-serif; | |
| font-size: 13px; | |
| line-height: 1.62; | |
| color: var(--ink-soft); | |
| } | |
| .fns-explain p { margin: 0 0 11px; } | |
| .fns-explain p:last-child { margin-bottom: 0; } | |
| .fns-explain strong { color: var(--ink); font-weight: 600; } | |
| .fns-explain .mono { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 12px; | |
| color: var(--ink); | |
| } | |
| .fns-explain .mono sup, .fns-explain .mono sub { font-size: 9px; } | |
| /* Big stat tile */ | |
| .stat-tile { | |
| background: var(--soft-cream); | |
| border: 1px solid var(--rule); | |
| padding: 14px 16px; | |
| text-align: left; | |
| } | |
| .stat-tile__label { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 10px; color: var(--ink-soft); | |
| text-transform: uppercase; letter-spacing: 1.6px; | |
| margin-bottom: 6px; | |
| } | |
| .stat-tile__value { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 26px; font-weight: 500; | |
| color: var(--ink); | |
| } | |
| .stat-tile__sub { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 10px; color: var(--ink-soft); | |
| margin-top: 4px; letter-spacing: 0.5px; | |
| } | |
| .stat-tile.green .stat-tile__value { color: var(--green-dark); } | |
| .stat-tile.red .stat-tile__value { color: var(--red); } | |
| /* Footer */ | |
| .post-foot { | |
| max-width: 760px; | |
| margin: 64px auto 0; | |
| padding: 24px 32px 64px; | |
| border-top: 1px solid var(--rule); | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 11px; color: var(--ink-soft); | |
| text-transform: uppercase; letter-spacing: 1.4px; | |
| } | |
| .post-foot a { color: var(--green-dark); text-decoration: none; } | |
| .post-foot a:hover { color: var(--green); } | |
| /* Position picker (W7) */ | |
| .seq-track { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 14px; | |
| letter-spacing: 0; | |
| background: var(--soft-cream); | |
| border: 1px solid var(--rule); | |
| padding: 10px 12px; | |
| display: flex; flex-wrap: wrap; gap: 1px; | |
| user-select: none; | |
| } | |
| .seq-track__b { | |
| display: inline-flex; align-items: center; justify-content: center; | |
| width: 22px; height: 26px; | |
| cursor: pointer; | |
| border-radius: 2px; | |
| color: #fff; | |
| font-weight: 500; | |
| transition: transform 0.1s ease, box-shadow 0.1s ease; | |
| } | |
| .seq-track__b:hover { transform: translateY(-1px); } | |
| .seq-track__b.A { background: var(--base-a); } | |
| .seq-track__b.T { background: var(--base-t); } | |
| .seq-track__b.C { background: var(--base-c); } | |
| .seq-track__b.G { background: var(--base-g); } | |
| .seq-track__b.selected { | |
| box-shadow: 0 0 0 2px var(--ink), 0 0 0 4px var(--bg); | |
| } | |
| /* Per-base text color (used wherever DNA letters appear in prose) */ | |
| .b-A, .b-T, .b-C, .b-G { font-weight: 500; } | |
| .b-A { color: var(--base-a); } | |
| .b-T { color: var(--base-t); } | |
| .b-C { color: var(--base-c); } | |
| .b-G { color: var(--base-g); } | |
| /* k-mer token: colored bases inside <>, no chrome */ | |
| .kmer-token, .raw-seq { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 12px; | |
| font-weight: 500; | |
| letter-spacing: 0.5px; | |
| } | |
| .kmer-token { | |
| display: inline-flex; | |
| align-items: center; | |
| padding: 2px 0; | |
| white-space: nowrap; | |
| } | |
| .kmer-token .br { color: var(--ink-faint); font-weight: 400; } | |
| .kmer-token.muted { opacity: 0.45; } | |
| .raw-seq { display: inline-flex; padding: 2px 0; } | |
| .bpe-flow { | |
| display: flex; align-items: center; flex-wrap: wrap; | |
| gap: 18px; | |
| margin: 10px 0 8px; | |
| } | |
| .bpe-flow .arrow { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 16px; | |
| color: var(--ink-faint); | |
| } | |
| /* Line 2: source token, SVG fan-out, candidate rows ----------------- */ | |
| .bpe-branches { | |
| display: grid; | |
| grid-template-columns: auto 96px 1fr; | |
| grid-template-rows: auto 150px; | |
| gap: 0; | |
| margin-top: 12px; | |
| } | |
| .branch-source { | |
| grid-column: 1; grid-row: 2; | |
| align-self: center; | |
| display: flex; flex-direction: column; align-items: center; gap: 6px; | |
| padding-right: 8px; | |
| } | |
| .branch-source__hint { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 9px; color: var(--ink-faint); | |
| text-transform: uppercase; letter-spacing: 1.6px; | |
| } | |
| .branch-lines { | |
| grid-column: 2; grid-row: 2; | |
| width: 96px; height: 150px; | |
| display: block; | |
| } | |
| .branch-lines path { | |
| fill: none; | |
| stroke: #ccc; | |
| stroke-width: 1.4; | |
| transition: stroke 0.15s, stroke-width 0.15s; | |
| } | |
| .branch-lines path.selected { stroke: var(--green); stroke-width: 2.2; } | |
| .branch-header { | |
| grid-column: 3; grid-row: 1; | |
| display: grid; | |
| grid-template-columns: 100px 60px 60px; | |
| gap: 18px; | |
| padding: 0 12px 10px; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 9.5px; | |
| font-weight: 500; | |
| letter-spacing: 1.6px; | |
| text-transform: uppercase; | |
| color: var(--ink-soft); | |
| } | |
| .branch-header > span { text-align: center; } | |
| .branch-header > span:first-child { text-align: left; color: var(--ink-faint); } | |
| .branch-targets { | |
| grid-column: 3; grid-row: 2; | |
| display: grid; | |
| grid-template-rows: repeat(5, 26px); | |
| row-gap: 5px; | |
| align-content: center; | |
| justify-content: start; | |
| height: 150px; | |
| } | |
| .cand-row { | |
| display: grid; | |
| grid-template-columns: 100px 60px 60px; | |
| gap: 18px; | |
| align-items: center; | |
| padding: 2px 12px; | |
| cursor: pointer; | |
| border-radius: 2px; | |
| transition: background 0.12s; | |
| } | |
| .cand-row:hover { background: rgba(0,0,0,0.03); } | |
| .cand-row.selected { background: var(--green-tint); } | |
| .mark { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 14px; | |
| font-weight: 500; | |
| text-align: center; | |
| line-height: 1; | |
| user-select: none; | |
| } | |
| .mark.ok { color: var(--green); } | |
| .mark.bad { color: var(--red); } | |
| /* explanation note */ | |
| .note { | |
| font-family: "Inter", sans-serif; | |
| font-size: 12px; line-height: 1.55; | |
| color: var(--ink-soft); | |
| background: var(--soft-cream); | |
| border-left: 2px solid var(--green); | |
| padding: 10px 12px; | |
| margin-top: 14px; | |
| } | |
| .note code { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 11px; | |
| background: #fff; | |
| border: 1px solid var(--rule); | |
| padding: 0 4px; border-radius: 2px; | |
| color: var(--green-dark); | |
| } | |
| .formula { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 12px; | |
| background: var(--soft-cream); | |
| border: 1px solid var(--rule); | |
| padding: 10px 12px; | |
| margin-top: 10px; | |
| color: var(--ink); | |
| overflow-x: auto; | |
| } | |
| .formula .hl { color: var(--green-dark); font-weight: 600; } | |
| /* Iframe wrapper: white page, no widget chrome — the host page provides the | |
| heading and description, so we only ship the interactive demo card. */ | |
| body { padding: 20px 24px; background: #fff; } | |
| .widget { max-width: 880px; margin: 0 auto; padding: 0; border: 0; } | |
| .widget__head, .widget__caption { display: none; } | |
| </style> | |
| </head> | |
| <body> | |
| <aside class="widget" id="w1"> | |
| <div class="widget__head"> | |
| <span class="widget__eyebrow">§ Widget · 02</span> | |
| <span class="widget__title">Per-base, BPE, and 6-mer on the same DNA sequence</span> | |
| </div> | |
| <p class="widget__caption"> | |
| Press <strong>play</strong> to watch the same sequence get cut three different ways — per-base, with a toy BPE | |
| vocabulary (greedy longest-match), and with non-overlapping 6-mers. The cuts appear left-to-right on the same | |
| time axis, so divergence between the schemes is visible directly. | |
| </p> | |
| <div class="demo"> | |
| <div class="demo-toolbar"> | |
| <span>Sequence · <span id="w1-len">0 bp</span></span> | |
| <span class="spacer"></span> | |
| <button class="action primary" id="w1-play">▶ TOKENIZE</button> | |
| </div> | |
| <input class="seq-input" id="w1-seq" value="ACGTATCGTATAGGCTAACGGATCATGCTAACGGATCATGCTAGCTAATGCATGCATGCAATCGATCGGGCCTTAAGCTAGCTACGATCGTAGCAT"> | |
| <div class="tk-anim"> | |
| <div class="tk-source"> | |
| <div class="tk-meta"> | |
| <span class="tk-label">Sequence</span> | |
| <span class="tk-stat"><span class="n" id="w1-bp">0</span> bp</span> | |
| </div> | |
| <div class="tk-source-strip" id="w1-source"></div> | |
| </div> | |
| <div class="tk-scheme" data-scheme="base"> | |
| <div class="tk-meta"> | |
| <span class="tk-label">Per-base</span> | |
| <span class="tk-stat">Tokens: <span class="n" id="w1-base-count">0</span></span> | |
| <span class="tk-stat">Compression: <span class="n" id="w1-base-ratio">1.0</span></span> | |
| </div> | |
| <div class="tk-scheme-strip" id="w1-base"></div> | |
| </div> | |
| <div class="tk-scheme" data-scheme="bpe"> | |
| <div class="tk-meta"> | |
| <span class="tk-label">BPE</span> | |
| <span class="tk-stat">Tokens: <span class="n" id="w1-bpe-count">0</span></span> | |
| <span class="tk-stat">Compression: <span class="n" id="w1-bpe-ratio">−</span></span> | |
| </div> | |
| <div class="tk-scheme-strip" id="w1-bpe"></div> | |
| </div> | |
| <div class="tk-scheme" data-scheme="kmer"> | |
| <div class="tk-meta"> | |
| <span class="tk-label">6-mer</span> | |
| <span class="tk-stat">Tokens: <span class="n" id="w1-kmer-count">0</span></span> | |
| <span class="tk-stat">Compression: <span class="n" id="w1-kmer-ratio">−</span></span> | |
| </div> | |
| <div class="tk-scheme-strip" id="w1-kmer"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <script> | |
| // ============================================================ | |
| // Shared utilities | |
| // ============================================================ | |
| const BASES = ['A', 'T', 'C', 'G']; | |
| const BASE_IDX = {A: 0, T: 1, C: 2, G: 3}; | |
| const BASE_COLOR = { A: 'var(--base-a)', T: 'var(--base-t)', C: 'var(--base-c)', G: 'var(--base-g)' }; | |
| // FNV-1a 32-bit | |
| function hash32(s) { | |
| let h = 0x811c9dc5; | |
| for (let i = 0; i < s.length; i++) { | |
| h ^= s.charCodeAt(i); | |
| h = Math.imul(h, 0x01000193); | |
| } | |
| return h >>> 0; | |
| } | |
| function rand01(seed) { | |
| const h = hash32(String(seed)); | |
| return (h % 1000003) / 1000003; | |
| } | |
| function softmax(arr) { | |
| let m = -Infinity; | |
| for (let i = 0; i < arr.length; i++) if (arr[i] > m) m = arr[i]; | |
| const out = new Float64Array(arr.length); | |
| let s = 0; | |
| for (let i = 0; i < arr.length; i++) { | |
| out[i] = Math.exp(arr[i] - m); | |
| s += out[i]; | |
| } | |
| for (let i = 0; i < arr.length; i++) out[i] /= s; | |
| return out; | |
| } | |
| function cleanDNA(s) { | |
| return (s || '').toUpperCase().replace(/[^ACGT]/g, ''); | |
| } | |
| function escapeHTML(s) { | |
| return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); | |
| } | |
| // 6-mer integer <-> string helpers | |
| function kmerToIdx(k) { | |
| let i = 0; | |
| for (let p = 0; p < k.length; p++) { | |
| const b = BASE_IDX[k[p]]; | |
| if (b == null) return -1; | |
| i = i * 4 + b; | |
| } | |
| return i; | |
| } | |
| function idxToKmer(i, k = 6) { | |
| let s = ''; | |
| for (let p = k - 1; p >= 0; p--) { | |
| s = BASES[(i >> (2 * p)) & 3] + s; | |
| } | |
| return s; | |
| } | |
| function baseAt(i, p) { | |
| // p in [0..5], 0 is leftmost | |
| return (i >> (2 * (5 - p))) & 3; | |
| } | |
| // generate a 4096-d distribution conditioned on a string seed | |
| function distribution4096(seed) { | |
| const logits = new Float64Array(4096); | |
| for (let i = 0; i < 4096; i++) { | |
| const a = rand01(seed + ':a:' + i); | |
| const b = rand01(seed + ':b:' + (i * 7919)); | |
| const c = rand01(seed + ':c:' + (i * 65537)); | |
| // mix a few uniforms → roughly-normal logit centered near 0 | |
| logits[i] = (a + b + c - 1.5) * 3.2; | |
| } | |
| return softmax(logits); | |
| } | |
| function marginals6x4(probs) { | |
| const m = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]; | |
| for (let i = 0; i < 4096; i++) { | |
| const p = probs[i]; | |
| for (let pos = 0; pos < 6; pos++) { | |
| m[pos][baseAt(i, pos)] += p; | |
| } | |
| } | |
| return m; | |
| } | |
| function fmtPct(p) { return (p * 100).toFixed(2) + '%'; } | |
| function fmt(p, d=4) { return Number(p).toFixed(d); } | |
| // Render a row of per-base bars (used for variants of W4/W6/W7) | |
| function renderPosCol(parent, posIndex, dist4, mode) { | |
| const max = Math.max(0.0001, ...dist4); | |
| let argmax = 0; | |
| for (let i = 1; i < 4; i++) if (dist4[i] > dist4[argmax]) argmax = i; | |
| const col = document.createElement('div'); | |
| col.className = 'posgrid__col' + (mode === 'argmax' ? ' argmax-' + BASES[argmax] : ''); | |
| col.innerHTML = | |
| `<div class="posgrid__pos">pos ${posIndex + 1}</div>` + | |
| `<div class="posgrid__bars">` + | |
| BASES.map((b, j) => { | |
| const w = (dist4[j] / max) * 100; | |
| const isMax = j === argmax; | |
| return `<div class="mini-row"> | |
| <span class="ml">${b}</span> | |
| <span class="mt"><span class="mf" style="width:${w.toFixed(1)}%;background:${BASE_COLOR[b]};opacity:${isMax ? 1 : 0.55}"></span></span> | |
| <span class="mv">${(dist4[j] * 100).toFixed(1)}</span> | |
| </div>`; | |
| }).join('') + | |
| `</div>`; | |
| parent.appendChild(col); | |
| } | |
| function basesHTML(seq) { | |
| return Array.from(seq).map((c) => `<span class="base ${c}">${c}</span>`).join(''); | |
| } | |
| // ============================================================ | |
| // W2 · BPE prefix ambiguity | |
| // ============================================================ | |
| (function setupW1() { | |
| // toy BPE vocab: longer entries first for greedy match | |
| const BPE_VOCAB = [ | |
| 'ACGGATCATGCTA', 'ATGCATGCA', 'CGATCGGGCCTT', 'AAGCTAGCTA', 'CGATCGTAG', | |
| 'ACGGATCA', 'ATGCATGC', 'CGATCGGG', 'GGCCTTAA', 'AGCTAGCT', 'CGTAGCAT', | |
| 'ACGGAT', 'GATCAT', 'ATGCTA', 'CATGCA', 'CGATCG', 'GGCCTT', 'AAGCTA', 'CTAGCT', 'CGTAGC', | |
| 'ACGTA', 'TATAG', 'ATCGA', 'GATCA', 'TAGCT', 'AGCTA', 'CGATC', 'TGCTA', | |
| 'ATCG', 'GATC', 'TATA', 'AGCT', 'GCTA', 'CTAG', 'ACGT', 'TGCA', | |
| 'AT', 'CG', 'GC', 'TA', 'AA', 'TT', 'CC', 'GG', 'AC', 'GT', 'CT', 'GA', | |
| 'A', 'C', 'G', 'T', | |
| ]; | |
| function bpeTokens(seq) { | |
| const out = []; | |
| let i = 0; | |
| while (i < seq.length) { | |
| let hit = null; | |
| for (const v of BPE_VOCAB) if (seq.startsWith(v, i)) { hit = v; break; } | |
| out.push(hit || seq[i]); | |
| i += (hit ? hit.length : 1); | |
| } | |
| return out; | |
| } | |
| // tokensFor: returns [{ text, startPos, endPos, tail }] | |
| function tokensBase(seq) { | |
| return Array.from(seq).map((c, i) => ({ text: c, startPos: i, endPos: i + 1, tail: false })); | |
| } | |
| function tokensBPEArr(seq) { | |
| const out = []; let p = 0; | |
| for (const t of bpeTokens(seq)) { | |
| out.push({ text: t, startPos: p, endPos: p + t.length, tail: false }); | |
| p += t.length; | |
| } | |
| return out; | |
| } | |
| function tokensKmer(seq, k = 6) { | |
| const out = []; let p = 0; | |
| while (p + k <= seq.length) { | |
| out.push({ text: seq.slice(p, p + k), startPos: p, endPos: p + k, tail: false }); | |
| p += k; | |
| } | |
| if (p < seq.length) { | |
| out.push({ text: seq.slice(p), startPos: p, endPos: seq.length, tail: true }); | |
| } | |
| return out; | |
| } | |
| const SCHEMES = [ | |
| { id: 'base', el: document.getElementById('w1-base'), tokens: tokensBase, countEl: document.getElementById('w1-base-count'), ratioEl: document.getElementById('w1-base-ratio') }, | |
| { id: 'bpe', el: document.getElementById('w1-bpe'), tokens: tokensBPEArr, countEl: document.getElementById('w1-bpe-count'), ratioEl: document.getElementById('w1-bpe-ratio') }, | |
| { id: 'kmer', el: document.getElementById('w1-kmer'), tokens: tokensKmer, countEl: document.getElementById('w1-kmer-count'), ratioEl: document.getElementById('w1-kmer-ratio') }, | |
| ]; | |
| const seqInput = document.getElementById('w1-seq'); | |
| const lenEl = document.getElementById('w1-len'); | |
| const bpEl = document.getElementById('w1-bp'); | |
| const sourceEl = document.getElementById('w1-source'); | |
| const playBtn = document.getElementById('w1-play'); | |
| let rafId = null; | |
| function colored(s) { | |
| return Array.from(s).map((c) => 'ACGT'.includes(c) | |
| ? '<span class="b-' + c + '">' + c + '</span>' | |
| : c).join(''); | |
| } | |
| function tokenHTML(text) { | |
| return '<span class="br"><</span>' + colored(text) + '<span class="br">></span>'; | |
| } | |
| function ratio(seqLen, n) { | |
| if (!n) return '−'; | |
| const r = seqLen / n; | |
| return (r >= 10 ? r.toFixed(0) : r.toFixed(1)); | |
| } | |
| function countLabel(sc, seqLen, tks) { | |
| return (sc.id === 'kmer' && seqLen % 6 !== 0) | |
| ? Math.floor(seqLen / 6) + '+1' | |
| : String(tks.length); | |
| } | |
| // Build the source strip; render each scheme's tokens statically (resting | |
| // state) and set the final counts. play() then clears + re-animates. | |
| function rebuild() { | |
| if (rafId) { cancelAnimationFrame(rafId); rafId = null; } | |
| const seq = cleanDNA(seqInput.value); | |
| lenEl.textContent = seq.length + ' bp'; | |
| bpEl.textContent = seq.length; | |
| sourceEl.innerHTML = ''; | |
| for (let i = 0; i < seq.length; i++) { | |
| const sp = document.createElement('span'); | |
| sp.className = 'tk-base ' + seq[i]; | |
| sp.dataset.pos = i; | |
| sp.textContent = seq[i]; | |
| sourceEl.appendChild(sp); | |
| } | |
| for (const sc of SCHEMES) { | |
| sc.el.innerHTML = ''; | |
| const tks = sc.tokens(seq); | |
| for (const tk of tks) { | |
| const el = document.createElement('span'); | |
| el.className = 'tk-token dropped' + (tk.tail ? ' tail' : ''); | |
| el.innerHTML = tokenHTML(tk.text); | |
| sc.el.appendChild(el); | |
| } | |
| sc.countEl.textContent = countLabel(sc, seq.length, tks); | |
| sc.ratioEl.textContent = ratio(seq.length, tks.length); | |
| } | |
| } | |
| // FLIP-style emit: place the token at its final spot in the scheme strip, | |
| // then apply a transform that moves it back over the source chips and | |
| // transition that transform to identity so it visually flies into place. | |
| function emitToken(scheme, tk, sourceChips) { | |
| const el = document.createElement('span'); | |
| el.className = 'tk-token' + (tk.tail ? ' tail' : ''); | |
| el.innerHTML = tokenHTML(tk.text); | |
| scheme.el.appendChild(el); | |
| // Measure destination (the natural position after appending) | |
| const dRect = el.getBoundingClientRect(); | |
| const destCx = (dRect.left + dRect.right) / 2; | |
| const destCy = (dRect.top + dRect.bottom) / 2; | |
| // Measure source span (bounding box of contributing source chips) | |
| const first = sourceChips[tk.startPos]; | |
| const last = sourceChips[tk.endPos - 1]; | |
| if (!first || !last) { el.classList.add('dropped'); return; } | |
| const fR = first.getBoundingClientRect(); | |
| const lR = last.getBoundingClientRect(); | |
| const srcCx = (Math.min(fR.left, lR.left) + Math.max(fR.right, lR.right)) / 2; | |
| const srcCy = (Math.min(fR.top, lR.top) + Math.max(fR.bottom, lR.bottom)) / 2; | |
| const dx = srcCx - destCx; | |
| const dy = srcCy - destCy; | |
| // No transition for the snap-back; then add .dropped on next frame | |
| // so the transition kicks in for the journey back to identity. | |
| el.style.transform = 'translate(' + dx + 'px, ' + dy + 'px) scale(0.92)'; | |
| requestAnimationFrame(() => { | |
| el.classList.add('dropped'); | |
| el.style.transform = ''; | |
| }); | |
| } | |
| function play() { | |
| if (rafId) cancelAnimationFrame(rafId); | |
| const seq = cleanDNA(seqInput.value); | |
| if (!seq) return; | |
| // Reset: clear scheme strips, zero the counters, un-consume the source. | |
| for (const sc of SCHEMES) { | |
| sc.el.innerHTML = ''; | |
| sc.countEl.textContent = '0'; | |
| sc.ratioEl.textContent = '—'; | |
| } | |
| const sourceChips = Array.from(sourceEl.querySelectorAll('.tk-base')); | |
| sourceChips.forEach((c) => c.classList.remove('consumed')); | |
| // Pre-compute the token list for each scheme | |
| const schemeTokens = {}; | |
| for (const sc of SCHEMES) schemeTokens[sc.id] = sc.tokens(seq); | |
| const emitted = { base: 0, bpe: 0, kmer: 0 }; | |
| const emittedBp = { base: 0, bpe: 0, kmer: 0 }; | |
| const duration = Math.max(1800, seq.length * 95); | |
| const start = performance.now(); | |
| function step(now) { | |
| const t = Math.min(1, (now - start) / duration); | |
| const cursor = t * seq.length; | |
| // Dim source chips up to cursor | |
| for (let i = 0; i < sourceChips.length; i++) { | |
| if (i < cursor && !sourceChips[i].classList.contains('consumed')) { | |
| sourceChips[i].classList.add('consumed'); | |
| } | |
| } | |
| // Emit any token whose end position has been reached, counting in real time | |
| for (const sc of SCHEMES) { | |
| const tks = schemeTokens[sc.id]; | |
| while (emitted[sc.id] < tks.length && tks[emitted[sc.id]].endPos <= cursor) { | |
| const tk = tks[emitted[sc.id]]; | |
| emitToken(sc, tk, sourceChips); | |
| emitted[sc.id]++; | |
| emittedBp[sc.id] = tk.endPos; | |
| sc.countEl.textContent = String(emitted[sc.id]); | |
| sc.ratioEl.textContent = ratio(emittedBp[sc.id], emitted[sc.id]); | |
| } | |
| } | |
| if (t < 1) { | |
| rafId = requestAnimationFrame(step); | |
| } else { | |
| // Settle on the exact final figures | |
| for (const sc of SCHEMES) { | |
| const tks = schemeTokens[sc.id]; | |
| sc.countEl.textContent = countLabel(sc, seq.length, tks); | |
| sc.ratioEl.textContent = ratio(seq.length, tks.length); | |
| } | |
| rafId = null; | |
| } | |
| } | |
| rafId = requestAnimationFrame(step); | |
| } | |
| seqInput.addEventListener('input', rebuild); | |
| playBtn.addEventListener('click', play); | |
| rebuild(); | |
| // Auto-play once when the widget scrolls into view | |
| if ('IntersectionObserver' in window) { | |
| const io = new IntersectionObserver((entries) => { | |
| for (const e of entries) { | |
| if (e.isIntersecting) { io.disconnect(); setTimeout(play, 300); break; } | |
| } | |
| }, { threshold: 0.3 }); | |
| io.observe(document.getElementById('w1')); | |
| } else { | |
| setTimeout(play, 600); | |
| } | |
| })(); | |
| </script> | |
| </body> | |
| </html> |