+
+
+
+
+
+ {rendererMode === "linocut" && rig ? (
+ <>
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+
+ {rendererMode === "linocut" && rig ? (
+ <>
+
로드 상태: {linocutLoadState}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ) : null}
+
+
+
+
+
+
+
+
+ 채널 미터 ({CHANNEL_IDS.length}개)
+
+
+
+ | 채널 |
+ 값 |
+
+
+
+ {CHANNEL_IDS.map((id) => (
+
+ | {id} |
+ {
+ meterRefs.current[id] = el;
+ }}
+ >
+ 0.00
+ |
+
+ ))}
+
+
+
+
+
+ 엔진 스냅샷
+ 활성 클립
+
+ {(snapshot?.activeClips ?? []).map((c, i) => (
+ -
+ {c.id} · started={c.startedMs.toFixed(0)} · weight={c.weight.toFixed(2)} · mirror={String(c.mirror)}
+
+ ))}
+
+ 예약된 cue
+
+ {(snapshot?.scheduled ?? []).map((c, i) => (
+ -
+ {c.clip} · at={c.atMs.toFixed(0)} · {c.source}
+
+ ))}
+
+ 누설 활성: {snapshot?.leakActive ? "예" : "아니오"}
+
+
+ );
+}
diff --git a/apps/web/src/pages/avatar-lab.css b/apps/web/src/pages/avatar-lab.css
new file mode 100644
index 0000000..afde540
--- /dev/null
+++ b/apps/web/src/pages/avatar-lab.css
@@ -0,0 +1,197 @@
+.al {
+ padding: 24px;
+ max-width: 1100px;
+ margin: 0 auto;
+ color: #2a231b;
+ font-size: 14px;
+}
+
+.al__head h1 {
+ margin: 0 0 4px;
+ font-size: 22px;
+}
+
+.al__head p {
+ margin: 0 0 20px;
+ color: #6b5f50;
+}
+
+.al__controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ align-items: center;
+ padding: 12px 16px;
+ border: 1px solid #ddd2bd;
+ border-radius: 12px;
+ margin-bottom: 20px;
+}
+
+.al__controls label {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ font-size: 12px;
+ color: #6b5f50;
+}
+
+.al__toggle {
+ flex-direction: row !important;
+ align-items: center;
+ gap: 6px !important;
+}
+
+.al__stage {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ gap: 24px;
+ margin-bottom: 24px;
+}
+
+.al__renderer {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 10px;
+ border: 1px solid #ddd2bd;
+ border-radius: 8px;
+ padding: 4px 10px;
+ font-size: 12px;
+ color: #6b5f50;
+}
+
+.al__linocut-main {
+ width: 420px;
+ height: 420px;
+ flex: none;
+}
+
+.al__preview-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+ align-items: flex-end;
+ margin-bottom: 24px;
+}
+
+.al__preview-cell {
+ flex: none;
+ background: #f3ead9;
+}
+
+.al__preview-cell--bust-circle {
+ width: 200px;
+ height: 200px;
+ border-radius: 50%;
+ overflow: hidden;
+}
+
+.al__preview-cell--face-96 {
+ width: 96px;
+ height: 96px;
+ border-radius: 50%;
+ overflow: hidden;
+}
+
+.al__preview-cell--face-48 {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ overflow: hidden;
+}
+
+.al__preview-cell--portrait {
+ width: 240px;
+}
+
+.al__panel {
+ margin-bottom: 28px;
+ padding: 16px;
+ border: 1px solid #ddd2bd;
+ border-radius: 12px;
+}
+
+.al__panel h2 {
+ margin: 0 0 10px;
+ font-size: 16px;
+}
+
+.al__intensity {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ margin-bottom: 10px;
+ font-size: 12px;
+ color: #6b5f50;
+}
+
+.al__grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.al__chip {
+ padding: 6px 12px;
+ border: 1px solid #ccbfa4;
+ border-radius: 999px;
+ background: #fbf7ef;
+ cursor: pointer;
+ font-size: 12px;
+}
+
+.al__chip--active {
+ background: #6b5e7d;
+ color: #fff;
+ border-color: #6b5e7d;
+}
+
+.al__speech-input {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 8px;
+ border: 1px solid #ccbfa4;
+ border-radius: 8px;
+ font-family: inherit;
+ font-size: 13px;
+}
+
+.al__actions {
+ display: flex;
+ gap: 10px;
+ margin: 10px 0;
+}
+
+.al__parse-result ul {
+ margin: 4px 0 12px;
+ padding-left: 18px;
+ font-size: 12px;
+}
+
+.al__meter {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 12px;
+}
+
+.al__meter th,
+.al__meter td {
+ border-bottom: 1px solid #eee2cd;
+ padding: 3px 6px;
+ text-align: left;
+}
+
+.al__audio-file {
+ align-items: center;
+}
+
+.al__audio-file-name {
+ font-size: 12px;
+ color: #6b5f50;
+}
+
+.al__speech-shape {
+ margin-bottom: 12px;
+ max-width: 360px;
+}