Munche-768 / index.html
ij's picture
Improve mobile layout and 3D navigation
1563d6d verified
Raw
History Blame Contribute Delete
59.6 kB
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#050608">
<meta name="description" content="내용과 고유명사의 영향을 줄이고 한국어 소설의 문체를 비교하는 768차원 임베딩 모델입니다.">
<title>Munche-768 | Korean Fiction Style Embedding</title>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.css">
<style>
:root {
color-scheme: dark;
--bg: #050608;
--bg-soft: #0a0c10;
--panel: #0d1015;
--panel-hi: #12161d;
--line: rgba(255, 255, 255, 0.12);
--line-strong: rgba(255, 255, 255, 0.24);
--text: #f7f7f4;
--text-2: #d3d8de;
--muted: var(--text-2);
--dim: var(--text-2);
--accent: #00efb3;
--accent-blue: #5b8cff;
--warning: #ffb020;
--accent-warm: #ff704d;
--radius: 22px;
--max: 1480px;
--fs-display: clamp(18px, 4vw, 56px);
--fs-lead: 24px;
--fs-body: 17px;
--fs-meta: 14px;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; background: var(--bg); }
body {
margin: 0;
background:
radial-gradient(circle at 50% -15%, rgba(0, 239, 179, 0.08), transparent 31rem),
var(--bg);
color: var(--text);
font-family: "Pretendard Variable", Pretendard, system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: var(--fs-body);
font-weight: 450;
line-height: 1.55;
word-break: keep-all;
overflow-wrap: break-word;
overflow-x: hidden;
}
button, a { font: inherit; }
a { color: inherit; text-decoration: none; }
button { color: inherit; }
::selection { color: #020806; background: var(--accent); }
.shell { width: min(calc(100% - 40px), var(--max)); margin-inline: auto; }
.nav {
position: fixed;
z-index: 50;
top: 18px;
left: 50%;
transform: translateX(-50%);
width: min(calc(100% - 32px), 920px);
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 20px;
min-height: 58px;
padding: 0 18px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 18px;
background: rgba(6, 8, 11, 0.76);
backdrop-filter: blur(22px) saturate(135%);
box-shadow: 0 12px 42px rgba(0, 0, 0, 0.34);
}
.brand { display: flex; align-items: center; gap: 10px; white-space: nowrap; }
.brand img {
width: 34px;
height: 34px;
border-radius: 9px;
object-fit: cover;
}
.brand-copy { display: grid; gap: 1px; line-height: 1; }
.brand-copy small { color: var(--text-2); font-size: 10px; font-weight: 650; letter-spacing: 0.05em; }
.brand-copy strong { font-size: var(--fs-meta); font-weight: 780; letter-spacing: 0.01em; }
.brand-copy strong span { color: var(--accent); }
.nav-links { display: flex; gap: 24px; color: var(--text-2); font-size: var(--fs-meta); font-weight: 600; }
.nav-links a:hover, .nav-links a:focus-visible { color: var(--text); }
.nav-status { justify-self: end; display: flex; align-items: center; gap: 8px; font-size: var(--fs-meta); color: var(--text-2); font-weight: 600; }
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 16px var(--accent); }
.hero {
min-height: 92svh;
display: grid;
place-items: center;
padding: 150px 0 84px;
text-align: center;
}
.hero-inner { width: min(100%, 1180px); }
.model-name {
margin: 0 0 22px;
color: var(--text);
font-size: clamp(54px, 9vw, 132px);
font-weight: 800;
line-height: .9;
letter-spacing: -0.055em;
}
h1 {
margin: 0;
font-size: var(--fs-display);
font-weight: 780;
line-height: 1.04;
letter-spacing: -0.01em;
text-wrap: balance;
}
h1 .line { display: block; white-space: nowrap; }
h1 .quiet { color: var(--text-2); }
.hero-copy {
width: min(100%, 720px);
margin: 38px auto 0;
color: var(--text-2);
font-size: var(--fs-body);
text-wrap: pretty;
}
.hero-meta { display: flex; justify-content: center; flex-wrap: wrap; gap: 10px; margin-top: 34px; }
.chip {
display: inline-flex;
align-items: center;
min-height: 34px;
padding: 0 13px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(255, 255, 255, 0.025);
color: var(--text-2);
font-size: var(--fs-meta);
}
.scroll-cue { margin-top: 80px; color: var(--text-2); font-size: var(--fs-meta); letter-spacing: .08em; }
.scroll-cue::after { content: ""; display: block; width: 1px; height: 54px; margin: 13px auto 0; background: linear-gradient(var(--line-strong), transparent); }
.section { padding: 150px 0; }
.section-head { display: block; margin-bottom: 54px; }
.kicker { margin: 0 0 14px; color: var(--accent); font-size: var(--fs-meta); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
h2 { margin: 0; max-width: 980px; font-size: var(--fs-display); line-height: 1.04; letter-spacing: -0.01em; text-wrap: balance; }
h2 .line { display: block; }
h3 { font-size: var(--fs-lead); line-height: 1.25; letter-spacing: -0.01em; }
.section-note { max-width: 760px; margin: 24px 0 0; color: var(--text-2); font-size: var(--fs-body); text-wrap: pretty; }
.overview-grid {
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(420px, .95fr);
gap: 90px;
align-items: start;
}
.overview-copy p { max-width: 62ch; margin: 28px 0 0; color: var(--text-2); font-size: var(--fs-body); }
.spec-list { margin: 0; border-top: 1px solid var(--line); }
.spec-row {
display: grid;
grid-template-columns: minmax(150px, .7fr) minmax(0, 1.3fr);
gap: 24px;
padding: 20px 0;
border-bottom: 1px solid var(--line);
}
.spec-row dt { color: var(--text-2); font-size: var(--fs-meta); font-weight: 600; }
.spec-row dd { margin: 0; color: var(--text); font-size: var(--fs-body); }
.training-layout {
display: grid;
grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr);
gap: 80px;
align-items: start;
}
.training-copy { position: sticky; top: 120px; }
.training-copy .section-note { max-width: 54ch; margin-top: 28px; }
.training-details { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid var(--line); border-radius: 24px; overflow: hidden; }
.training-item { min-height: 250px; padding: 30px; background: var(--panel); box-shadow: inset -1px -1px 0 var(--line); }
.training-item strong { display: block; margin-bottom: 18px; font-size: var(--fs-lead); }
.training-item p { margin: 0; color: var(--text-2); font-size: var(--fs-body); }
.training-item .stat { display: block; margin: 0 0 12px; color: var(--accent); font-size: var(--fs-meta); font-weight: 700; line-height: 1; letter-spacing: 0; }
.use-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); border: 1px solid var(--line); border-radius: 24px; overflow: hidden; }
.use-item { min-height: 270px; padding: 28px; background: var(--panel); box-shadow: inset -1px -1px 0 var(--line); }
.use-item b { display: block; margin-bottom: 64px; color: var(--accent); font-size: var(--fs-meta); }
.use-item h3 { margin: 0 0 16px; font-size: var(--fs-lead); line-height: 1.2; letter-spacing: -0.01em; }
.use-item p { margin: 0; color: var(--text-2); font-size: var(--fs-body); }
.stage-wrap {
position: relative;
min-height: 780px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 30px;
background:
radial-gradient(circle at 50% 45%, rgba(15, 28, 28, .52), transparent 42%),
#030405;
box-shadow: 0 40px 100px rgba(0, 0, 0, .46);
isolation: isolate;
}
.plot-viewport { position: absolute; inset: 0; overflow: hidden; }
.stage-wrap::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
opacity: .38;
background-image:
linear-gradient(rgba(255,255,255,.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: radial-gradient(circle at center, #000 22%, transparent 76%);
}
#plot { position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; cursor: grab; }
#plot:active { cursor: grabbing; }
.stage-top {
position: absolute;
z-index: 5;
top: 22px;
left: 22px;
right: 22px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 24px;
pointer-events: none;
}
.stage-label { font-size: var(--fs-meta); color: var(--text-2); }
.stage-label strong { display: block; color: var(--text); font-size: var(--fs-lead); font-weight: 700; }
.plot-help { display: block; margin-top: 5px; color: var(--text-2); font-size: 12px; font-weight: 550; }
.stage-actions { display: flex; gap: 8px; pointer-events: auto; }
.layer-switch { display: flex; gap: 4px; padding: 4px; border: 1px solid var(--line); border-radius: 14px; background: rgba(7, 9, 12, .72); backdrop-filter: blur(14px); }
.icon-button, .legend-button, .filter-button {
border: 1px solid var(--line);
background: rgba(7, 9, 12, .72);
backdrop-filter: blur(14px);
cursor: pointer;
transition: border-color .2s ease, background .2s ease, color .2s ease;
}
.icon-button:hover, .icon-button:focus-visible, .legend-button:hover, .filter-button:hover { border-color: var(--line-strong); background: rgba(20, 24, 30, .9); }
.icon-button { min-height: 40px; padding: 0 13px; border-radius: 12px; color: var(--text); font-size: var(--fs-meta); font-weight: 600; }
.icon-button[aria-pressed="true"] { color: #02100c; border-color: var(--accent); background: var(--accent); }
.layer-switch .icon-button { min-height: 32px; border-color: transparent; background: transparent; }
.layer-switch .icon-button[aria-pressed="true"] { color: #080a0d; background: #f3f5f7; }
.legend, .filters { transition: opacity .2s ease; }
.layer-controls-muted { visibility: hidden; opacity: 0; pointer-events: none; }
.legend {
position: absolute;
z-index: 6;
left: 22px;
bottom: 22px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
width: min(440px, calc(100% - 44px));
padding: 10px;
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(5, 7, 9, .78);
backdrop-filter: blur(18px);
}
.legend-button { display: flex; align-items: center; gap: 9px; min-width: 0; padding: 8px 9px; border-radius: 9px; color: var(--text); font-size: var(--fs-meta); font-weight: 600; text-align: left; }
.legend-button.off { text-decoration: line-through; }
.legend-button.off .swatch { background: transparent; border: 1px solid var(--swatch); box-shadow: none; }
.swatch { flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%; background: var(--swatch); box-shadow: 0 0 11px color-mix(in srgb, var(--swatch), transparent 45%); }
.legend-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legend-count { margin-left: auto; color: var(--dim); font-variant-numeric: tabular-nums; }
.filters {
position: absolute;
z-index: 6;
right: 22px;
bottom: 22px;
display: flex;
max-width: calc(100% - 44px);
gap: 6px;
padding: 7px;
overflow-x: auto;
border: 1px solid var(--line);
border-radius: 15px;
background: rgba(5, 7, 9, .78);
backdrop-filter: blur(18px);
}
.filter-button { flex: 0 0 auto; min-height: 34px; padding: 0 11px; border-radius: 9px; color: var(--text-2); font-size: var(--fs-meta); font-weight: 600; }
.filter-button.active { color: #020907; border-color: var(--accent); background: var(--accent); }
.tooltip {
position: absolute;
z-index: 8;
display: none;
min-width: 168px;
padding: 11px 13px;
border: 1px solid var(--line-strong);
border-radius: 12px;
background: rgba(4, 6, 8, .92);
box-shadow: 0 18px 50px rgba(0,0,0,.45);
pointer-events: none;
font-size: var(--fs-meta);
}
.tooltip b { display: block; margin-bottom: 3px; }
.tooltip span { color: var(--muted); }
.privacy-badge {
position: absolute;
z-index: 4;
right: 24px;
top: 88px;
display: flex;
align-items: center;
gap: 8px;
color: #8f969f;
font-size: var(--fs-meta);
pointer-events: none;
}
.privacy-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: #fff; }
.bento {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 0;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 26px;
background: var(--line);
}
.metric-card {
min-height: 320px;
padding: 34px;
background: var(--panel);
box-shadow: inset -1px -1px 0 var(--line);
}
.metric-card:nth-child(1) { grid-column: span 5; }
.metric-card:nth-child(2) { grid-column: span 3; }
.metric-card:nth-child(3) { grid-column: span 4; }
.metric-card:nth-child(4) { grid-column: span 4; }
.metric-card:nth-child(5) { grid-column: span 8; }
.metric-label { color: var(--text-2); font-size: var(--fs-body); font-weight: 700; }
.metric-index { margin-bottom: 18px; color: var(--accent); font-size: var(--fs-meta); font-weight: 700; letter-spacing: .08em; }
.metric-value { margin-top: 42px; font-size: var(--fs-display); font-weight: 780; line-height: 1; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }
.metric-value small { font-size: var(--fs-meta); letter-spacing: 0; color: var(--text-2); }
.metric-copy { max-width: 43ch; margin: 24px 0 0; color: var(--text-2); font-size: var(--fs-body); }
.metric-card.accent { background: var(--accent); color: #001a12; }
.metric-card.accent .metric-index, .metric-card.accent .metric-label, .metric-card.accent .metric-copy { color: #08312a; }
.metric-card.warning { background: #17130b; }
.metric-card.warning .metric-value { color: var(--warning); }
.metric-card.wide { display: grid; grid-template-columns: minmax(0, .8fr) minmax(320px, 1.2fr); gap: 42px; align-items: center; }
.baseline-card {
display: grid;
grid-template-columns: minmax(280px, .75fr) minmax(0, 1.25fr);
gap: 64px;
align-items: center;
margin-top: 24px;
padding: 38px;
border: 1px solid var(--line);
border-radius: 22px;
background: var(--panel);
}
.baseline-card h3 { margin: 0; }
.comparison-list { display: grid; gap: 16px; }
.comparison-row { display: grid; grid-template-columns: 220px minmax(0, 1fr) 160px; gap: 16px; align-items: center; color: var(--text); font-size: var(--fs-meta); }
.bar { height: 8px; border-radius: 999px; background: rgba(255,255,255,.08); overflow: hidden; }
.bar i { display: block; width: calc(var(--value) * 100%); height: 100%; border-radius: inherit; background: linear-gradient(90deg, #59616c, #f4f6f4); }
.comparison-row strong { text-align: right; font-variant-numeric: tabular-nums; }
.benchmark-chart {
padding: 38px;
border: 1px solid var(--line);
border-radius: 26px;
background: var(--panel);
}
.chart-legend { display: flex; justify-content: center; gap: 28px; margin-bottom: 34px; font-size: var(--fs-body); font-weight: 650; }
.chart-legend span { display: flex; align-items: center; gap: 10px; }
.chart-legend i { width: 16px; height: 16px; border-radius: 3px; background: #59616c; }
.chart-legend .tuned { background: var(--accent); }
.chart-plot {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 26px;
min-height: 430px;
padding: 26px 18px 0;
background: repeating-linear-gradient(to top, var(--line) 0 1px, transparent 1px 25%);
border-bottom: 1px solid var(--line-strong);
}
.chart-group { display: grid; grid-template-rows: 340px auto; gap: 18px; min-width: 0; }
.chart-bars { display: flex; justify-content: center; align-items: end; gap: 8px; height: 340px; }
.chart-bar { position: relative; width: min(62px, 42%); height: var(--score); min-height: 2px; border-radius: 7px 7px 0 0; background: #59616c; }
.chart-bar.tuned { background: var(--accent); }
.chart-bar strong { position: absolute; left: 50%; bottom: calc(100% + 10px); transform: translateX(-50%); color: var(--text); font-size: var(--fs-body); font-variant-numeric: tabular-nums; white-space: nowrap; }
.chart-label { min-height: 54px; color: var(--text); font-size: var(--fs-meta); font-weight: 650; text-align: center; }
.metric-definitions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin-top: 22px; overflow: hidden; border: 1px solid var(--line); border-radius: 20px; background: var(--line); }
.metric-definition { padding: 24px; background: var(--panel); }
.metric-definition b { display: block; margin-bottom: 10px; font-size: var(--fs-body); }
.metric-definition p { margin: 0; color: var(--text-2); font-size: var(--fs-meta); }
.length-claim { margin: 34px 0 0; text-align: center; font-size: var(--fs-lead); font-weight: 700; }
.additional-metrics { margin-top: 28px; }
.additional-metrics table { table-layout: fixed; }
.performance { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); gap: 64px; align-items: start; }
.performance-copy { position: sticky; top: 120px; }
.inline-cloud { display: inline-grid; grid-template-columns: repeat(5, 5px); gap: 4px; vertical-align: .17em; margin: 0 .1em; }
.inline-cloud i { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); opacity: calc(.25 + var(--o) * .13); }
.prose { color: var(--text-2); font-size: var(--fs-body); }
.prose strong { color: var(--text); font-weight: 650; }
.table-wrap { overflow: hidden; border: 1px solid var(--line); border-radius: 22px; }
#evaluation table { min-width: 0; }
table { width: 100%; border-collapse: collapse; background: var(--panel); }
th, td { padding: 20px 22px; border-bottom: 1px solid var(--line); text-align: left; }
th { color: var(--text); font-size: var(--fs-meta); font-weight: 700; letter-spacing: 0; }
td { color: var(--text-2); font-size: var(--fs-body); }
td:nth-child(2), td:nth-child(3), td:nth-child(4), td:nth-child(5) { font-variant-numeric: tabular-nums; }
tr:last-child td { border-bottom: 0; }
.table-note { margin: 18px 0 0; color: var(--text-2); font-size: var(--fs-meta); }
.auroc-strip { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 22px; overflow: hidden; border: 1px solid var(--line); border-radius: 18px; }
.auroc-item { padding: 24px; background: var(--panel); box-shadow: inset -1px 0 0 var(--line); }
.auroc-item span { display: block; color: var(--text-2); font-size: var(--fs-meta); }
.auroc-item strong { display: block; margin-top: 12px; font-size: var(--fs-display); line-height: 1; font-variant-numeric: tabular-nums; }
.auroc-item small { color: var(--text-2); font-size: var(--fs-meta); }
.probe-grid {
display: grid;
grid-template-columns: minmax(0, .8fr) minmax(420px, 1.2fr);
gap: 54px;
align-items: center;
margin-top: 34px;
padding: 42px;
border: 1px solid var(--line);
border-radius: 24px;
background: var(--panel);
}
.probe-grid h3 { margin: 0; }
.probe-summary { margin: 18px 0 0; color: var(--text); font-size: var(--fs-body); font-weight: 700; }
.probe-score { display: flex; gap: 40px; margin-top: 30px; }
.probe-score span { color: var(--text); font-size: var(--fs-body); font-weight: 700; }
.probe-score strong { display: block; margin-top: 6px; color: var(--text); font-size: var(--fs-display); line-height: 1; }
.probe-score small { display: block; max-width: 28ch; margin-top: 10px; color: var(--text-2); font-size: var(--fs-meta); font-weight: 600; }
.confusion { width: 100%; border-collapse: separate; border-spacing: 8px; background: transparent; }
.confusion th { color: var(--text); font-size: var(--fs-body); font-weight: 700; text-align: center; }
.confusion th[scope="row"] { text-align: left; }
.confusion .cell {
height: 112px;
text-align: center;
vertical-align: middle;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--panel-hi);
color: var(--text);
font-size: var(--fs-lead);
font-weight: 780;
font-variant-numeric: tabular-nums;
}
.confusion .cell.hit { border-color: color-mix(in srgb, var(--accent), transparent 45%); background: color-mix(in srgb, var(--accent), var(--panel-hi) 88%); }
.confusion .cell.miss { border-color: color-mix(in srgb, var(--warning), transparent 48%); background: color-mix(in srgb, var(--warning), var(--panel-hi) 91%); }
.confusion .cell small { display: block; color: var(--text-2); font-size: var(--fs-meta); font-weight: 600; }
.method-grid { display: grid; grid-template-columns: minmax(300px, .7fr) minmax(0, 1.3fr); gap: 80px; align-items: start; }
.method-intro { position: sticky; top: 120px; }
.stack { position: relative; display: grid; gap: 24px; }
.method-card {
position: sticky;
top: calc(118px + var(--i) * 18px);
min-height: 440px;
padding: 38px;
border: 1px solid var(--line);
border-radius: 26px;
background: color-mix(in srgb, var(--panel-hi) calc(88% + var(--i) * 2%), #050608);
box-shadow: 0 32px 80px rgba(0,0,0,.36);
}
.method-index { display: flex; justify-content: space-between; color: var(--text-2); font-size: var(--fs-meta); }
.method-card h3 { margin: 86px 0 18px; font-size: var(--fs-lead); line-height: 1.2; letter-spacing: -0.01em; }
.method-card p { max-width: 54ch; margin: 0; color: var(--text-2); font-size: var(--fs-body); }
.method-spec { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 34px; }
.principles {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
overflow: hidden;
border: 1px solid var(--line);
border-radius: 24px;
background: var(--line);
gap: 1px;
}
.principle { min-height: 260px; padding: 32px; background: var(--panel); }
.principle b { display: block; margin-bottom: 28px; font-size: var(--fs-body); }
.principle p { margin: 0; color: var(--text-2); font-size: var(--fs-body); }
.training-centered { width: min(820px, 100%); margin-inline: auto; }
.training-centered .kicker { text-align: center; }
.training-note { margin: 34px auto 0; color: var(--text-2); font-size: var(--fs-body); }
.footer { padding: 40px 0 44px; border-top: 1px solid var(--line); }
.footer-cta {
padding: 70px;
border-radius: 28px;
background: var(--text);
color: #080a0c;
}
.footer-cta h2 { max-width: 1120px; }
.footer-cta p { max-width: 720px; margin: 28px 0 0; color: #283138; font-size: var(--fs-body); }
.cta-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 34px; }
.cta {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 0 17px;
border: 1px solid #111;
border-radius: 12px;
font-size: var(--fs-meta);
font-weight: 700;
}
.cta.primary { background: #080a0c; color: #fff; }
.cta.secondary { background: transparent; color: #080a0c; }
.footer-bottom { display: flex; justify-content: space-between; gap: 30px; color: var(--text-2); font-size: var(--fs-meta); }
.footer-links { display: flex; gap: 22px; }
.footer-links a:hover, .footer-links a:focus-visible { color: var(--text); }
@media (max-width: 1040px) {
.stage-wrap { min-height: 690px; }
.section-head, .performance, .method-grid, .overview-grid, .training-layout, .baseline-card { grid-template-columns: 1fr; }
.performance-copy, .method-intro, .training-copy { position: static; }
.metric-card:nth-child(n) { grid-column: span 6; }
.metric-card.wide { grid-template-columns: 1fr; }
.principles { grid-template-columns: 1fr; }
.use-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.probe-grid { grid-template-columns: 1fr; }
.chart-plot { gap: 16px; }
}
@media (max-width: 720px) {
.shell { width: min(calc(100% - 24px), var(--max)); }
.nav {
top: 0;
left: 0;
transform: none;
width: 100%;
min-height: 52px;
grid-template-columns: 1fr auto;
gap: 10px;
padding: 0 12px;
border-width: 0 0 1px;
border-radius: 0;
background: rgba(6, 8, 11, .94);
}
.brand { gap: 8px; }
.brand img { width: 28px; height: 28px; border-radius: 7px; }
.brand-copy { display: flex; align-items: baseline; gap: 5px; }
.brand-copy small { font-size: 9px; }
.brand-copy strong { font-size: 12px; }
.nav-status { gap: 5px; font-size: 10px; }
.status-dot { width: 5px; height: 5px; }
.nav-links { display: none; }
h2 { font-size: var(--fs-lead); }
.hero { min-height: 0; padding: 104px 0 58px; }
.model-name { margin-bottom: 16px; font-size: clamp(42px, 14vw, 56px); }
.hero-copy { margin-top: 24px; }
.hero-meta { margin-top: 24px; }
.scroll-cue { display: none; }
.section { padding: 68px 0; }
.section-head { margin-bottom: 28px; }
.section-note { margin-top: 18px; }
.stage-wrap {
min-height: 0;
display: grid;
grid-template-rows: 460px auto auto;
overflow: hidden;
border-radius: 20px;
}
.plot-viewport { position: relative; inset: auto; min-height: 460px; }
.stage-top { top: 14px; left: 14px; right: 14px; flex-direction: column; gap: 10px; }
.stage-actions { flex-wrap: wrap; }
.privacy-badge { top: 78px; right: 16px; }
.legend {
position: relative;
left: auto;
right: auto;
bottom: auto;
display: flex;
width: 100%;
padding: 8px;
overflow-x: auto;
border-width: 1px 0 0;
border-radius: 0;
background: rgba(5, 7, 9, .96);
backdrop-filter: none;
scrollbar-width: thin;
}
.legend-button { flex: 0 0 auto; min-height: 38px; padding-inline: 10px; }
.legend-count { display: none; }
.filters {
position: relative;
left: auto;
right: auto;
bottom: auto;
width: 100%;
max-width: none;
padding: 8px;
border-width: 1px 0 0;
border-radius: 0;
background: rgba(5, 7, 9, .96);
backdrop-filter: none;
scrollbar-width: thin;
}
.metric-card:nth-child(n) { grid-column: 1 / -1; min-height: 190px; padding: 24px; }
.metric-value { margin-top: 28px; }
.comparison-row { grid-template-columns: minmax(0, 1fr) auto; gap: 10px 18px; }
.comparison-row .bar { grid-column: 1 / -1; grid-row: 2; }
.comparison-row strong { grid-column: 2; grid-row: 1; }
th, td { padding: 16px 13px; }
.benchmark-chart { padding: 22px 14px; }
.chart-legend { gap: 16px; margin-bottom: 24px; font-size: var(--fs-meta); }
.chart-plot {
grid-template-columns: 1fr;
gap: 18px;
min-height: 0;
padding: 0;
border-bottom: 0;
background: none;
}
.chart-group {
grid-template-columns: minmax(94px, .8fr) minmax(0, 1.5fr);
grid-template-rows: 58px;
gap: 12px;
align-items: center;
}
.chart-label {
grid-column: 1;
grid-row: 1;
min-height: 0;
text-align: left;
}
.chart-bars {
grid-column: 2;
grid-row: 1;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: repeat(2, 1fr);
align-items: center;
justify-items: start;
gap: 7px;
height: 58px;
padding-right: 42px;
}
.chart-bar {
width: var(--score);
height: 100%;
border-radius: 0 5px 5px 0;
}
.chart-bar strong {
left: calc(100% + 7px);
bottom: 50%;
transform: translateY(50%);
font-size: 12px;
}
.metric-definitions { grid-template-columns: 1fr; }
.metric-definition { padding: 20px; }
.length-claim { margin-top: 26px; }
.method-card { min-height: 0; padding: 24px; }
.training-details, .use-grid { grid-template-columns: 1fr; }
.training-item, .use-item { min-height: 0; padding: 24px; }
.use-item b { margin-bottom: 28px; }
.footer-cta { padding: 38px 26px; }
.footer-bottom { flex-direction: column; }
.probe-grid { gap: 30px; margin-top: 24px; padding: 24px; }
.probe-score { flex-direction: column; gap: 24px; }
.confusion .cell { min-height: 86px; }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
}
</style>
</head>
<body>
<nav class="nav" aria-label="주요 메뉴">
<a class="brand" href="#top" aria-label="Baragi AI Munche-768">
<img src="./assets/baragi-ai.png" alt="">
<span class="brand-copy"><small>Baragi AI</small><strong>Munche<span>-768</span></strong></span>
</a>
<div class="nav-links">
<a href="#performance">성능</a>
<a href="#distribution">문체 공간</a>
<a href="#training">학습</a>
<a href="#limitations">한계</a>
</div>
<div class="nav-status"><i class="status-dot"></i> EmbeddingGemma 300M</div>
</nav>
<main id="top">
<header class="hero shell">
<div class="hero-inner">
<p class="model-name">Munche-768</p>
<h1>
<span class="line">최초의 한국어 소설 문체 임베딩 모델</span>
</h1>
<p class="hero-copy">각 작가의 고유한 문체를 768차원 벡터로 표현하다.</p>
<div class="hero-meta">
<span class="chip">EmbeddingGemma 300M</span>
<span class="chip">768 dimensions</span>
</div>
</div>
</header>
<section class="section shell" id="performance">
<div class="section-head">
<p class="kicker">Test results</p>
<h2>작가 탐지 정확도 88.2%</h2>
<p class="section-note">42개 test 작품에서 추출한 348개 질의 중, 동일 장르의 다른 작품을 후보로 두고 같은 작가의 작품을 1순위로 찾은 비율입니다.</p>
</div>
<div class="benchmark-chart" role="img" aria-label="원본 EmbeddingGemma와 Munche-768의 네 가지 평가 지표 비교">
<div class="chart-legend">
<span><i></i>원본 EmbeddingGemma</span>
<span><i class="tuned"></i>Munche-768</span>
</div>
<div class="chart-plot">
<div class="chart-group">
<div class="chart-bars"><div class="chart-bar" style="--score:56.7%"><strong>56.7</strong></div><div class="chart-bar tuned" style="--score:84.0%"><strong>84.0</strong></div></div>
<div class="chart-label">동일 작품 검색<br>mAP@R</div>
</div>
<div class="chart-group">
<div class="chart-bars"><div class="chart-bar" style="--score:59.6%"><strong>59.6</strong></div><div class="chart-bar tuned" style="--score:79.9%"><strong>79.9</strong></div></div>
<div class="chart-label">동일 작가 검색<br>mAP@R</div>
</div>
<div class="chart-group">
<div class="chart-bars"><div class="chart-bar" style="--score:22.6%"><strong>22.6</strong></div><div class="chart-bar tuned" style="--score:83.1%"><strong>83.1</strong></div></div>
<div class="chart-label">내용 강건성<br>정확도</div>
</div>
<div class="chart-group">
<div class="chart-bars"><div class="chart-bar" style="--score:0%"><strong>0.0</strong></div><div class="chart-bar tuned" style="--score:79.5%"><strong>79.5</strong></div></div>
<div class="chart-label">동일 내용 분리<br>정확도</div>
</div>
</div>
</div>
<div class="metric-definitions">
<div class="metric-definition"><b>동일 작품 검색</b><p>동일 장르 후보군에서 같은 작품의 비중첩 장면을 검색한 mAP@R입니다.</p></div>
<div class="metric-definition"><b>동일 작가 검색</b><p>동일 장르의 다른 작품 중 같은 작가의 작품을 검색한 mAP@R입니다.</p></div>
<div class="metric-definition"><b>내용 강건성</b><p>내용이 가장 유사한 타 작품 4개보다 같은 작품의 다른 장면을 가깝게 배치한 비율입니다.</p></div>
<div class="metric-definition"><b>동일 내용 분리</b><p>동일 내용을 유지한 LLM 재작성본보다 같은 작품의 다른 장면을 가깝게 배치한 비율입니다.</p></div>
</div>
<p class="length-claim">최대 2,048토큰(약 3,600자)만으로 소설의 문체를 구분할 수 있습니다.</p>
<div class="table-wrap additional-metrics">
<table>
<thead><tr><th>추가 지표</th><th>결과</th><th>평가 조건</th></tr></thead>
<tbody>
<tr><td>동일 작품 Hit@1</td><td>95.6%</td><td>동일 장르 · 1,089 질의</td></tr>
<tr><td>동일 작가 Hit@1</td><td>88.2%</td><td>동일 장르 · 다른 작품 · 348 질의</td></tr>
<tr><td>가명 치환 질의 mAP@R</td><td>81.1%</td><td>가명 질의 · 원문 후보군</td></tr>
<tr><td>문단·문장부호 정규화 mAP@R</td><td>68.3%</td><td>형식 신호 통제</td></tr>
</tbody>
</table>
</div>
</section>
<section class="section shell" id="distribution">
<div class="section-head">
<div>
<h2><span class="line">1,113가지 소설의 문체를</span><span class="line">3D 공간에서 확인해보세요.</span></h2>
</div>
<p class="section-note">AI와 인간, 그리고 장르별 분포를 확인해보세요. 전체 데이터는 6개 장르의 인간 소설 783편과 언어 모델 11종의 생성 소설 330편으로 구성됩니다.</p>
</div>
<div class="stage-wrap" id="plot-stage">
<div class="plot-viewport" id="plot-viewport">
<canvas id="plot" aria-label="한국어 소설 문체 임베딩 3차원 UMAP 산점도"></canvas>
<div class="stage-top">
<div class="stage-label">
<strong>UMAP · 3D</strong>
<span id="visible-count">1,113개 표본</span>
<small class="plot-help">드래그 회전 · 두 손가락 이동 · 핀치 확대</small>
</div>
<div class="stage-actions">
<button class="icon-button" id="rotate-toggle" type="button" aria-pressed="true">자동 회전</button>
<button class="icon-button" id="reset-view" type="button">시점 초기화</button>
</div>
</div>
<div class="tooltip" id="tooltip"></div>
</div>
<div class="legend" id="legend" aria-label="모델 범례"></div>
<div class="filters" id="genre-filters" aria-label="장르 필터"></div>
</div>
<div class="probe-grid" aria-label="인간 및 AI 변형본 선형 분류 결과">
<div>
<p class="kicker">문체 판별</p>
<h2>같은 내용, 다른 문체를 분리합니다.</h2>
<p class="section-note">소설의 내용을 유지하고 AI로 새롭게 작성했을 때, 91.3%의 확률로 AI와 인간을 구분해냅니다.</p>
<p class="section-note">단 10분의 추가 훈련만으로 같은 내용에서도 AI와 인간을 구분할 수 있습니다.</p>
<p class="probe-summary">AUROC 0.967 · 균형 정확도 91.3%</p>
<div class="probe-score">
<div><span>인간 recall</span><strong>95.8%</strong><small>인간 원문 190편</small></div>
<div><span>LLM recall</span><strong>86.8%</strong><small>LLM 재작성본 190편</small></div>
</div>
</div>
<table class="confusion" aria-label="인간 원문과 AI 재작성본 판별 결과">
<thead><tr><th>실제 원천</th><th scope="col">인간 예측</th><th scope="col">LLM 예측</th></tr></thead>
<tbody>
<tr><th scope="row">인간 원문 190편</th><td class="cell hit">182<small>정분류</small></td><td class="cell miss">8<small>오분류</small></td></tr>
<tr><th scope="row">LLM 재작성본 190편</th><td class="cell miss">25<small>오분류</small></td><td class="cell hit">165<small>정분류</small></td></tr>
</tbody>
</table>
</div>
</section>
<section class="section shell" id="evaluation">
<div class="performance">
<div class="performance-copy">
<p class="kicker">Detailed evaluation</p>
<h2><span class="line">검색·강건성·분리 성능을</span><span class="line">독립 test split에서 검증했습니다.</span></h2>
<div class="prose">
<p>작품 및 저자 단위로 train·validation·test를 분리했습니다. 검색 평가는 동일 장르 후보군에서 수행했으며, 아래 수치는 체크포인트 선택에 사용되지 않은 test split 결과입니다.</p>
<p><strong>mAP@R</strong>은 정답 수만큼의 상위 결과에서 측정한 평균 정밀도입니다. 반사실 순위 정확도는 동일 내용의 재작성본보다 동일 작품의 다른 장면을 가깝게 배치한 비율입니다.</p>
</div>
</div>
<div>
<div class="table-wrap">
<table>
<thead><tr><th>테스트 조건</th><th>지표</th><th>결과</th><th>표본</th></tr></thead>
<tbody>
<tr><td>동일 작품·동일 장르 검색</td><td>mAP@R</td><td>0.840</td><td>1,089 질의</td></tr>
<tr><td>동일 저자·작품 간 검색</td><td>mAP@R</td><td>0.799</td><td>348 질의</td></tr>
<tr><td>가명 치환 질의에서 원문 검색</td><td>mAP@R</td><td>0.811</td><td>1,089 질의</td></tr>
<tr><td>내용이 유사한 타 작품 4개 분리</td><td>4개 전부 분리 정확도</td><td>83.1%</td><td>1,216 기준점</td></tr>
<tr><td>동일 내용 LLM 재작성본 순위</td><td>Triplet accuracy</td><td>79.5%</td><td>190쌍</td></tr>
<tr><td>문단·문장부호 정규화 후 검색</td><td>mAP@R</td><td>0.683</td><td>1,089 질의</td></tr>
</tbody>
</table>
</div>
<p class="table-note">작품과 저자 단위 분할을 사용했으며, test split은 체크포인트 선택에 사용하지 않았습니다.</p>
</div>
</div>
</section>
<section class="section shell" id="training">
<div class="training-centered">
<p class="kicker">Training configuration</p>
<dl class="spec-list">
<div class="spec-row"><dt>인간 양성쌍</dt><dd>64,000쌍 · 동일 회차 8K, 근거리 20K, 원거리 24K, 동일 저자·타 작품 12K</dd></div>
<div class="spec-row"><dt>내용 유사 음성</dt><dd>24,000 기준점 · Kiwi 형태소 TF-IDF 기반 타 작품 후보</dd></div>
<div class="spec-row"><dt>반사실 쌍</dt><dd>1,306쌍 · train 913, validation 203, test 190</dd></div>
<div class="spec-row"><dt>기반 모델</dt><dd>google/embeddinggemma-300m</dd></div>
<div class="spec-row"><dt>적응 방식</dt><dd>Rank 32 LoRA · q/k/v/o 및 MLP projection</dd></div>
<div class="spec-row"><dt>학습 파라미터</dt><dd>8,355,840 · 기반 모델, pooling, dense projection 고정</dd></div>
<div class="spec-row"><dt>출력</dt><dd>Mean pooling · Dense projection · 768D · L2 normalization</dd></div>
<div class="spec-row"><dt>입력 길이</dt><dd>최대 2,048 tokens</dd></div>
</dl>
<p class="training-note">동일 작품 및 동일 저자의 텍스트를 양성 표본으로, 내용 유사 타 작품과 동일 내용 LLM 재작성본을 명시적 음성 표본으로 구성했습니다. 한국어 입력은 원문 25%, 가명 치환 50%, 고유명사 마스킹 25% 비율로 노출했습니다.</p>
</div>
</section>
<section class="section shell" id="uses">
<div class="section-head">
<div>
<h2><span class="line">검색부터 코퍼스 진단까지,</span><span class="line">문체를 수치로 비교합니다.</span></h2>
</div>
<p class="section-note">문체 검색, 저자 간 일관성 분석, 코퍼스 분포 진단과 생성 모델 비교에 사용할 수 있습니다.</p>
</div>
<div class="use-grid">
<article class="use-item"><b>01</b><h3>문체 유사도 검색</h3><p>입력 텍스트와 문체적으로 유사한 텍스트를 검색합니다.</p></article>
<article class="use-item"><b>02</b><h3>작가 간 비교</h3><p>서로 다른 작품 사이의 문체적 일관성을 분석합니다.</p></article>
<article class="use-item"><b>03</b><h3>장르 조건부 군집 분석</h3><p>장르를 통제한 상태에서 작품 간 문체 거리와 군집 구조를 분석합니다.</p></article>
<article class="use-item"><b>04</b><h3>코퍼스 분포 진단</h3><p>인간·생성 코퍼스의 문체 분포, 편중 및 이상치를 점검합니다.</p></article>
<article class="use-item"><b>05</b><h3>생성 모델 비교</h3><p>언어 모델별 소설 출력의 문체 분포를 비교합니다.</p></article>
</div>
</section>
<section class="section shell" id="limitations">
<div class="section-head">
<p class="kicker">Limitations</p>
</div>
<div class="principles">
<article class="principle">
<b>01 · 형식 신호 의존</b>
<p>문단 경계와 문장부호를 통일하면 동일 작품 검색 mAP@R이 0.840에서 0.683으로 하락합니다. 줄바꿈과 부호 사용이 문체 임베딩의 일부를 차지합니다.</p>
</article>
<article class="principle">
<b>02 · 한국어 소설 중심 평가</b>
<p>PAN21 저자 검증 AUROC는 0.895이나 PAN 삼중항은 학습 자료에 포함되었습니다. 미학습 영어 문체 속성을 평가한 SynthSTEL 정확도는 1.9%로 일반화 성능이 확인되지 않았습니다.</p>
</article>
<article class="principle">
<b>03 · LLM 원천 판별 범위</b>
<p>AUROC 0.967은 지정된 4종의 생성 모델이 특정 절차로 재작성한 표본에 대한 결과이며, 새로운 모델이나 다른 생성 조건의 AI 텍스트 탐지 성능을 보장하지 않습니다.</p>
</article>
</div>
</section>
</main>
<footer class="footer shell">
<div class="footer-bottom">
<span>Baragi AI | Munche-768</span>
<span class="footer-links">
<a href="https://huggingface.co/Baragi-AI/Munche-768" target="_blank" rel="noreferrer">Hugging Face 모델</a>
<a href="#top">맨 위로</a>
</span>
</div>
</footer>
<script src="./points-g2.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>
<script>
(() => {
const dataset = window.STYLE_EMBEDDING_DATA;
if (!dataset || !Array.isArray(dataset.points)) return;
const MODEL_LABELS = {
human: '인간 소설',
'google/gemini-3.5-flash': 'Gemini 3.5 Flash',
'unsloth/gemma-4-12b-it': 'Gemma 4 12B',
'google/gemma-4-31b-it': 'Gemma 4 31B',
'anthropic/claude-sonnet-5': 'Claude Sonnet 5',
'anthropic/claude-sonnet-5-low': 'Claude Sonnet 5 Low',
'deepseek/deepseek-v3.2': 'DeepSeek v3.2',
'deepseek/deepseek-v4-pro-cloud': 'DeepSeek V4 Pro',
'openai/gpt-5.4-mini': 'GPT-5.4 mini',
'openai/gpt-5.6-sol-high': 'GPT 5.6 Sol High',
'z-ai/glm-5.2-cloud': 'GLM 5.2',
'minimax/minimax-m3-cloud': 'MiniMax M3'
};
const MODEL_COLORS = {
human: '#ffffff',
'anthropic/claude-sonnet-5': '#ff704d',
'anthropic/claude-sonnet-5-low': '#ff9b7a',
'deepseek/deepseek-v3.2': '#5b8cff',
'deepseek/deepseek-v4-pro-cloud': '#88adff',
'google/gemini-3.5-flash': '#36cbb5',
'google/gemma-4-31b-it': '#00efb3',
'minimax/minimax-m3-cloud': '#3b75db',
'openai/gpt-5.4-mini': '#ff9a3d',
'openai/gpt-5.6-sol-high': '#ffc05a',
'unsloth/gemma-4-12b-it': '#78dbc8',
'z-ai/glm-5.2-cloud': '#7ca7ff'
};
const GENRE_LABELS = {
all: '전체', fantasy: '판타지', hyunpan: '현대판타지', muhyup: '무협',
mystery: '미스터리', rofan: '로맨스판타지', sf_apoc: 'SF·아포칼립스',
sports_ent: '스포츠·연예', game: '게임', drama: '드라마', apoc: '아포칼립스',
althist: '대체역사', sf: 'SF', etc: '기타'
};
const canvas = document.querySelector('#plot');
const stage = document.querySelector('#plot-stage');
const viewport = document.querySelector('#plot-viewport');
const ctx = canvas.getContext('2d', { alpha: true });
const tooltip = document.querySelector('#tooltip');
const visibleCount = document.querySelector('#visible-count');
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
const enabledModels = new Set(Object.keys(MODEL_LABELS));
let selectedGenre = 'all';
let autoRotate = !reduceMotion;
let rotationX = -0.22;
let rotationY = -0.56;
let cameraDistance = 5.1;
let panX = 0;
let panY = 0;
let dragging = false;
let lastX = 0;
let lastY = 0;
let moved = false;
let projected = [];
let lastTime = performance.now();
let cssWidth = 0;
let cssHeight = 0;
const activePointers = new Map();
let gestureCenter = null;
let gestureDistance = 0;
const points = dataset.points.map((point, index) => ({ ...point, index }));
const presentModels = new Set(points.map(point => point.source));
const models = Object.keys(MODEL_LABELS).filter(model => presentModels.has(model));
const genres = [...new Set(points.map(p => p.genre))].sort((a, b) => (GENRE_LABELS[a] || a).localeCompare(GENRE_LABELS[b] || b, 'ko'));
function countBy(items, key) {
return items.reduce((map, item) => map.set(item[key], (map.get(item[key]) || 0) + 1), new Map());
}
function buildLegend() {
const counts = countBy(points, 'source');
const legend = document.querySelector('#legend');
models.forEach(model => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'legend-button';
button.dataset.model = model;
button.innerHTML = `<i class="swatch" style="--swatch:${MODEL_COLORS[model] || '#f2f2f2'}"></i><span class="legend-name">${MODEL_LABELS[model] || model}</span><span class="legend-count">${counts.get(model)}</span>`;
button.addEventListener('click', () => {
if (enabledModels.has(model)) enabledModels.delete(model); else enabledModels.add(model);
button.classList.toggle('off', !enabledModels.has(model));
updateVisibleCount();
});
legend.appendChild(button);
});
}
function buildFilters() {
const container = document.querySelector('#genre-filters');
['all', ...genres].forEach(genre => {
const button = document.createElement('button');
button.type = 'button';
button.className = `filter-button${genre === 'all' ? ' active' : ''}`;
button.textContent = GENRE_LABELS[genre] || genre;
button.addEventListener('click', () => {
selectedGenre = genre;
container.querySelectorAll('button').forEach(item => item.classList.toggle('active', item === button));
updateVisibleCount();
});
container.appendChild(button);
});
}
function isVisible(point) {
return enabledModels.has(point.source) && (selectedGenre === 'all' || point.genre === selectedGenre);
}
function updateVisibleCount() {
const count = points.filter(isVisible).length;
visibleCount.textContent = `소설 분포 · ${count.toLocaleString('ko-KR')}개 표본`;
}
function resize() {
const rect = viewport.getBoundingClientRect();
const dpr = Math.min(devicePixelRatio || 1, 2);
cssWidth = Math.max(1, rect.width);
cssHeight = Math.max(1, rect.height);
canvas.width = Math.round(cssWidth * dpr);
canvas.height = Math.round(cssHeight * dpr);
canvas.style.width = `${cssWidth}px`;
canvas.style.height = `${cssHeight}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function rotatePoint([x, y, z]) {
const cosY = Math.cos(rotationY), sinY = Math.sin(rotationY);
const x1 = x * cosY - z * sinY;
const z1 = x * sinY + z * cosY;
const cosX = Math.cos(rotationX), sinX = Math.sin(rotationX);
return [x1, y * cosX - z1 * sinX, y * sinX + z1 * cosX];
}
function project(coords) {
const [x, y, z] = rotatePoint(coords);
const depth = Math.max(1.7, cameraDistance - z);
const focal = Math.min(cssWidth, cssHeight) * 1.55;
const scale = focal / depth;
return { x: cssWidth / 2 + panX + x * scale, y: cssHeight / 2 + panY - y * scale, z, depth, perspective: cameraDistance / depth };
}
function drawFrame() {
const bounds = 1.9;
const corners = [];
for (const x of [-bounds, bounds]) for (const y of [-bounds, bounds]) for (const z of [-bounds, bounds]) corners.push([x, y, z]);
const projectedCorners = corners.map(project);
const edges = [];
for (let i = 0; i < corners.length; i++) {
for (let j = i + 1; j < corners.length; j++) {
const diff = [0, 1, 2].filter(axis => corners[i][axis] !== corners[j][axis]).length;
if (diff === 1) edges.push([i, j]);
}
}
ctx.save();
ctx.strokeStyle = 'rgba(255,255,255,.08)';
ctx.lineWidth = 1;
edges.forEach(([a, b]) => {
ctx.beginPath();
ctx.moveTo(projectedCorners[a].x, projectedCorners[a].y);
ctx.lineTo(projectedCorners[b].x, projectedCorners[b].y);
ctx.stroke();
});
ctx.restore();
}
function render(now) {
const delta = Math.min(40, now - lastTime);
lastTime = now;
if (autoRotate && !dragging) rotationY += delta * 0.000035;
ctx.clearRect(0, 0, cssWidth, cssHeight);
drawFrame();
const mainProjected = points.filter(isVisible).map(point => ({ point, ...project(point.xyz) }));
projected = mainProjected.sort((a, b) => b.depth - a.depth);
function drawLayer(items) {
items.sort((a, b) => b.depth - a.depth).forEach(item => {
const isHuman = item.point.source === 'human';
const radius = Math.max(isHuman ? 1.05 : 2.2, (isHuman ? 1.85 : 3.6) * item.perspective);
const color = MODEL_COLORS[item.point.source] || '#f2f2f2';
ctx.save();
if (!isHuman) {
const halo = ctx.createRadialGradient(item.x, item.y, 0, item.x, item.y, radius * 3.1);
halo.addColorStop(0, color + '42');
halo.addColorStop(1, color + '00');
ctx.fillStyle = halo;
ctx.beginPath();
ctx.arc(item.x, item.y, radius * 3.1, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = isHuman ? Math.min(.86, .42 + item.perspective * .26) : .96;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(item.x, item.y, radius, 0, Math.PI * 2);
ctx.fill();
if (!isHuman) {
ctx.globalAlpha = .72;
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = .55;
ctx.stroke();
}
ctx.restore();
item.radius = radius;
});
}
drawLayer(mainProjected);
requestAnimationFrame(render);
}
function pointerPosition(event) {
const rect = canvas.getBoundingClientRect();
return { x: event.clientX - rect.left, y: event.clientY - rect.top };
}
function updateTooltip(event) {
if (dragging || moved) { tooltip.style.display = 'none'; return; }
const pos = pointerPosition(event);
let hit = null;
let best = Infinity;
for (let i = projected.length - 1; i >= 0; i--) {
const item = projected[i];
const distance = Math.hypot(item.x - pos.x, item.y - pos.y);
if (distance < Math.max(8, item.radius + 4) && distance < best) { hit = item; best = distance; }
}
if (!hit) { tooltip.style.display = 'none'; return; }
const label = MODEL_LABELS[hit.point.source] || hit.point.source;
const detail = GENRE_LABELS[hit.point.genre] || hit.point.genre;
tooltip.innerHTML = `<b>${label}</b><span>${detail}</span>`;
tooltip.style.display = 'block';
const left = Math.min(cssWidth - 190, Math.max(12, pos.x + 16));
const top = Math.min(cssHeight - 80, Math.max(12, pos.y + 16));
tooltip.style.transform = `translate(${left}px, ${top}px)`;
}
const clampDistance = value => Math.max(3.1, Math.min(8.2, value));
const clampPan = () => {
panX = Math.max(-cssWidth * .75, Math.min(cssWidth * .75, panX));
panY = Math.max(-cssHeight * .75, Math.min(cssHeight * .75, panY));
};
const pointerGeometry = () => {
const pointers = [...activePointers.values()];
if (pointers.length < 2) return null;
const [a, b] = pointers;
return {
center: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 },
distance: Math.hypot(a.x - b.x, a.y - b.y)
};
};
const endPointer = event => {
activePointers.delete(event.pointerId);
dragging = activePointers.size > 0;
const remaining = activePointers.values().next().value;
if (remaining) {
lastX = remaining.x;
lastY = remaining.y;
}
gestureCenter = null;
gestureDistance = 0;
if (canvas.hasPointerCapture(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
};
canvas.addEventListener('pointerdown', event => {
activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
dragging = true;
moved = false;
lastX = event.clientX;
lastY = event.clientY;
const geometry = pointerGeometry();
if (geometry) {
gestureCenter = geometry.center;
gestureDistance = geometry.distance;
}
canvas.setPointerCapture(event.pointerId);
tooltip.style.display = 'none';
});
canvas.addEventListener('pointermove', event => {
if (!activePointers.has(event.pointerId)) {
updateTooltip(event);
return;
}
activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
const geometry = pointerGeometry();
if (geometry) {
if (gestureCenter) {
panX += geometry.center.x - gestureCenter.x;
panY += geometry.center.y - gestureCenter.y;
cameraDistance = clampDistance(cameraDistance - (geometry.distance - gestureDistance) * .008);
clampPan();
}
gestureCenter = geometry.center;
gestureDistance = geometry.distance;
moved = true;
return;
}
const dx = event.clientX - lastX, dy = event.clientY - lastY;
if (Math.abs(dx) + Math.abs(dy) > 1) moved = true;
if (event.shiftKey) {
panX += dx;
panY += dy;
clampPan();
} else {
rotationY -= dx * .006;
rotationX += dy * .006;
}
lastX = event.clientX;
lastY = event.clientY;
});
canvas.addEventListener('pointerup', endPointer);
canvas.addEventListener('pointercancel', endPointer);
canvas.addEventListener('pointerleave', () => { if (!dragging) tooltip.style.display = 'none'; });
canvas.addEventListener('wheel', event => {
event.preventDefault();
if (event.ctrlKey || event.metaKey) {
cameraDistance = clampDistance(cameraDistance + event.deltaY * .003);
} else {
panX -= event.deltaX;
panY -= event.deltaY;
clampPan();
}
}, { passive: false });
document.querySelector('#rotate-toggle').addEventListener('click', event => {
autoRotate = !autoRotate;
event.currentTarget.setAttribute('aria-pressed', String(autoRotate));
});
document.querySelector('#reset-view').addEventListener('click', () => {
rotationX = -0.22; rotationY = -0.56; cameraDistance = 5.1; panX = 0; panY = 0;
});
new ResizeObserver(resize).observe(viewport);
buildLegend();
buildFilters();
updateVisibleCount();
resize();
requestAnimationFrame(render);
if (window.gsap && window.ScrollTrigger && !reduceMotion) {
gsap.registerPlugin(ScrollTrigger);
gsap.from('.hero .model-name, .hero h1 .line, .hero-copy, .hero-meta', {
y: 28, opacity: 0, duration: 1.05, stagger: .09, ease: 'power3.out'
});
gsap.from('.stage-wrap', {
scrollTrigger: { trigger: '.stage-wrap', start: 'top 86%', once: true },
scale: .965, opacity: 0, duration: 1.15, ease: 'power3.out'
});
gsap.utils.toArray('.training-item, .use-item').forEach((card, index) => {
gsap.from(card, {
scrollTrigger: { trigger: card, start: 'top 90%', once: true },
y: 42 + index * 3, opacity: 0, duration: .8, ease: 'power2.out'
});
});
}
})();
</script>
</body>
</html>