| <!DOCTYPE html> |
| <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 { |
| 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); } |
| |
| |
| .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 { |
| 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 { |
| 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, .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-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; } |
| |
| |
| .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); } |
| |
| |
| .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); } |
| |
| |
| .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; } |
| |
| |
| |
| .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-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); } |
| |
| |
| .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-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) !important; } |
| .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; } |
| |
| |
| .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-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; } |
| |
| |
| .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); } |
| |
| |
| .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); } |
| |
| |
| .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); |
| } |
| |
| |
| .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); } |
| |
| |
| .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); |
| } |
| |
| |
| .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); } |
| |
| |
| .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; } |
| |
| |
| |
| |
| |
| 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> |
| |
| |
| |
| |
| |
| |
| 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)' }; |
| |
| |
| 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, '>'); |
| } |
| |
| |
| 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) { |
| |
| return (i >> (2 * (5 - p))) & 3; |
| } |
| |
| |
| 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)); |
| |
| 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); } |
| |
| |
| 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(''); |
| } |
| |
| |
| |
| |
| |
| (function setupW1() { |
| |
| 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; |
| } |
| |
| |
| 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); |
| } |
| |
| |
| |
| 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); |
| } |
| } |
| |
| |
| |
| |
| 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); |
| |
| |
| const dRect = el.getBoundingClientRect(); |
| const destCx = (dRect.left + dRect.right) / 2; |
| const destCy = (dRect.top + dRect.bottom) / 2; |
| |
| |
| 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; |
| |
| |
| |
| 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; |
| |
| |
| 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')); |
| |
| |
| 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; |
| |
| |
| for (let i = 0; i < sourceChips.length; i++) { |
| if (i < cursor && !sourceChips[i].classList.contains('consumed')) { |
| sourceChips[i].classList.add('consumed'); |
| } |
| } |
| |
| |
| 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 { |
| |
| 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(); |
| |
| |
| 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> |