# 08. D3RO-VOICE 디자인 시스템 > 시안 A (Meteorological Instrument)와 시안 B (VoiceOps Dashboard)를 융합한 전용 디자인 시스템. > 03-db-and-ui.md의 MUI 테마를 완전 교체한다. --- ## 1. 컬러 팔레트 ### 1.1 CSS 커스텀 프로퍼티 ```css :root { /* ── 배경 (시안 B 기반) ─────────────────────────── */ --bg-app: #19191b; --bg-card: #242427; --bg-card-hover: #2a2a2d; --bg-elevated: #2e2e32; --bg-input: #1e1e21; /* ── 보더 ───────────────────────────────────────── */ --border-subtle: rgba(255, 255, 255, 0.04); --border-default: rgba(255, 255, 255, 0.08); --border-strong: rgba(255, 255, 255, 0.12); /* ── 악센트: 앰버 (시안 A) ─────────────────────── */ --accent-amber: #f25b29; --accent-amber-dim: rgba(242, 91, 41, 0.15); --accent-amber-glow: rgba(242, 91, 41, 0.6); /* ── 상태 태그 (시안 B) ─────────────────────────── */ --tag-purple: #b854f5; --tag-purple-bg: rgba(184, 84, 245, 0.12); --tag-orange: #f59e0b; --tag-orange-bg: rgba(245, 158, 11, 0.12); --tag-red: #ef4444; --tag-red-bg: rgba(239, 68, 68, 0.12); --tag-green: #22c55e; --tag-green-bg: rgba(34, 197, 94, 0.12); /* ── 텍스트 계층 (시안 B) ──────────────────────── */ --text-primary: #ffffff; --text-secondary: #8e8e93; --text-label: #7c7c82; --text-disabled: #4a4a4e; /* ── CRT 인광 (시안 A, RecordingTip 전용) ──────── */ --crt-phosphor: #f25b29; --crt-phosphor-dim: #c44a22; --crt-scanline: rgba(0, 0, 0, 0.15); --crt-bg: #242528; /* ── 시맨틱 ─────────────────────────────────────── */ --color-error: #ef4444; --color-success: #22c55e; --color-warning: #f59e0b; --color-info: #b854f5; } ``` ### 1.2 태그 색상 매핑 | 태그 | 배경 | 텍스트 | 용도 | |------|------|--------|------| | STT | `--tag-purple-bg` | `--tag-purple` | STT 관련 서비스/상태 | | TTS | `--tag-orange-bg` | `--tag-orange` | TTS 관련 서비스/상태 | | LIVE | `--tag-red-bg` | `--tag-red` | 실시간 녹음/스트리밍 | | COMPLETE | `--tag-green-bg` | `--tag-green` | 완료 상태 | | PROCESSING | `--tag-orange-bg` | `--tag-orange` | 처리 중 | | ERROR | `--tag-red-bg` | `--tag-red` | 에러 상태 | --- ## 2. 타이포그래피 ### 2.1 폰트 스택 ```css :root { /* 본문 (시안 B) */ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; /* 상태/모노 (시안 A) */ --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; } ``` ### 2.2 크기 체계 | 토큰 | 크기 | 행간 | 용도 | |------|------|------|------| | `--text-hero` | 28px | 1.2 | 대시보드 메인 수치 | | `--text-title` | 22px | 1.3 | 섹션 타이틀 | | `--text-subtitle` | 18px | 1.4 | 카드 헤딩, 라이브 전사 | | `--text-body` | 14px | 1.5 | 본문 텍스트 | | `--text-small` | 12px | 1.4 | 보조 텍스트 | | `--text-label` | 11px | 1.2 | 라벨, 태그 | ### 2.3 라벨 스타일 ```css .label { font-family: var(--font-sans); font-size: 11px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-label); } ``` ### 2.4 모노 숫자 스타일 ```css .mono-value { font-family: var(--font-mono); font-size: 14px; font-weight: 500; letter-spacing: 0.02em; color: var(--text-primary); font-variant-numeric: tabular-nums; } ``` --- ## 3. 컴포넌트 스타일가이드 ### 3.1 Card 시안 B 기반. 전체 레이아웃의 기본 컨테이너. ```css .card { background: var(--bg-card); border-radius: 22px; border-top: 1px solid var(--border-subtle); /* rgba(255,255,255,0.04) */ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); padding: 24px; transition: background-color 0.2s ease; } .card:hover { background: var(--bg-card-hover); } ``` ### 3.2 Tag / Badge 시안 B 기반. pill 형태의 상태 태그. ```css .tag { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 999px; /* pill */ font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; line-height: 1; } /* 변형 */ .tag--purple { background: var(--tag-purple-bg); color: var(--tag-purple); } .tag--orange { background: var(--tag-orange-bg); color: var(--tag-orange); } .tag--red { background: var(--tag-red-bg); color: var(--tag-red); } .tag--green { background: var(--tag-green-bg); color: var(--tag-green); } ``` ### 3.3 Button 시안 A의 물리 버튼 느낌. 눌림 피드백 중시. ```css .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 20px; border: none; border-radius: 10px; font-family: var(--font-sans); font-size: 14px; font-weight: 600; cursor: pointer; transition: transform 0.05s linear, box-shadow 0.05s linear; box-shadow: 0 2px 0 rgba(0, 0, 0, 0.4), /* 돌출 그림자 */ inset 0 1px 0 rgba(255, 255, 255, 0.06); /* 상단 하이라이트 */ } .btn:active { transform: translateY(2px); box-shadow: 0 0 0 rgba(0, 0, 0, 0.4), inset 0 2px 4px rgba(0, 0, 0, 0.3); /* 눌림 inset */ } /* Primary: 앰버 악센트 */ .btn--primary { background: var(--accent-amber); color: #fff; } .btn--primary:hover { background: #d94f24; } /* Secondary: 다크 */ .btn--secondary { background: var(--bg-elevated); color: var(--text-primary); } .btn--secondary:hover { background: #353539; } ``` ### 3.4 LED Indicator 시안 A 기반. 서비스 상태 표시등. ```css .led { width: 8px; height: 8px; border-radius: 50%; background: var(--text-disabled); flex-shrink: 0; transition: background 0.3s ease, box-shadow 0.3s ease; } .led--active { background: var(--tag-green); box-shadow: 0 0 6px var(--tag-green), 0 0 12px rgba(34, 197, 94, 0.3); } .led--warning { background: var(--tag-orange); box-shadow: 0 0 6px var(--tag-orange), 0 0 12px rgba(245, 158, 11, 0.3); } .led--error { background: var(--tag-red); box-shadow: 0 0 6px var(--tag-red), 0 0 12px rgba(239, 68, 68, 0.3); } .led--recording { background: var(--accent-amber); box-shadow: 0 0 6px var(--accent-amber-glow), 0 0 16px var(--accent-amber-dim); animation: led-pulse 1.5s ease-in-out infinite; } @keyframes led-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } ``` ### 3.5 WaveformBar 시안 B의 7개 바 레이아웃 + 시안 A의 앰버색. ```css .waveform { display: flex; align-items: center; gap: 3px; height: 32px; } .waveform__bar { width: 4px; min-height: 3px; border-radius: 2px; background: var(--accent-amber); transition: height 100ms ease-out; } /* 7개 바 코사인 분포 가중치 */ /* JS에서 scaleY로 높이 제어 */ ``` ```javascript // 웨이브폼 파라미터 const WAVEFORM = { BAR_COUNT: 7, UPDATE_MS: 100, MIN_SCALE: 0.1, // scaleY 최솟값 MAX_SCALE: 1.0, // scaleY 최댓값 SMOOTHING: 0.5, RANDOM_FACTOR: 0.35, }; // 코사인 분포 가중치 (중앙 최대) const weights = Array.from({ length: WAVEFORM.BAR_COUNT }, (_, n) => { const center = (WAVEFORM.BAR_COUNT - 1) / 2; // 3 const normalized = (n - center) / center; // -1 ~ +1 return Math.cos(normalized * Math.PI / 2); }); // 결과: [0, 0.50, 0.87, 1.0, 0.87, 0.50, 0] function updateWaveform(bars, audioLevel, currentScales) { for (let i = 0; i < WAVEFORM.BAR_COUNT; i++) { const base = audioLevel * weights[i]; const randomized = base * (1 + (Math.random() - 0.5) * 2 * WAVEFORM.RANDOM_FACTOR); const target = Math.max(WAVEFORM.MIN_SCALE, Math.min(WAVEFORM.MAX_SCALE, randomized)); currentScales[i] += (target - currentScales[i]) * WAVEFORM.SMOOTHING; bars[i].style.transform = `scaleY(${currentScales[i]})`; } } ``` ### 3.6 RecordingTip (CRT 모드) 시안 A의 CRT 인스트루먼트 느낌을 RecordingTip 팝업에 적용. ```css .recording-tip--crt { background: var(--crt-bg); border-radius: 12px; padding: 10px 14px; position: relative; overflow: hidden; box-shadow: 0 0 20px rgba(242, 91, 41, 0.15), inset 0 0 60px rgba(0, 0, 0, 0.3); } /* 스캔라인 오버레이 */ .recording-tip--crt::before { content: ''; position: absolute; inset: 0; background: repeating-linear-gradient( 0deg, transparent, transparent 2px, var(--crt-scanline) 2px, var(--crt-scanline) 4px ); pointer-events: none; z-index: 2; } /* 비네팅 오버레이 */ .recording-tip--crt::after { content: ''; position: absolute; inset: 0; background: radial-gradient( ellipse at center, transparent 50%, rgba(0, 0, 0, 0.4) 100% ); pointer-events: none; z-index: 3; } /* CRT 모드 웨이브바: 앰버 인광색 */ .recording-tip--crt .waveform__bar { background: var(--crt-phosphor); box-shadow: 0 0 4px var(--accent-amber-glow); } /* CRT 모드 텍스트 */ .recording-tip--crt .duration, .recording-tip--crt .thinking-label { font-family: var(--font-mono); font-size: 12px; color: var(--crt-phosphor); text-shadow: 0 0 6px var(--accent-amber-glow); letter-spacing: 1.5px; } /* 노이즈 텍스처 */ .recording-tip--crt .noise-overlay { position: absolute; inset: 0; opacity: 0.03; mix-blend-mode: overlay; pointer-events: none; z-index: 4; /* JS: canvas로 랜덤 픽셀 노이즈 생성 */ } ``` ### 3.7 Dashboard 레이아웃 시안 B의 카드 그리드 + 헤더 구조. ```css .dashboard { max-width: 1200px; margin: 0 auto; padding: 40px; } .dashboard__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px; } .dashboard__header-title { font-size: var(--text-title); font-weight: 700; color: var(--text-primary); } .dashboard__header-subtitle { font-size: var(--text-body); color: var(--text-secondary); margin-top: 4px; } ``` ### 3.8 StatusPanel 시안 B의 서비스 상태 리스트 카드. ```css .status-panel { /* .card 스타일 상속 */ } .status-item { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border-subtle); } .status-item:last-child { border-bottom: none; } .status-item__name { font-size: var(--text-body); font-weight: 500; color: var(--text-primary); flex: 1; } .status-item__value { font-family: var(--font-mono); font-size: 12px; color: var(--text-secondary); } ``` ### 3.9 ProcessingOverlay 시안 B의 blur + spinner 처리 중 표시. ```css .processing-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background: rgba(25, 25, 27, 0.6); backdrop-filter: blur(4px); border-radius: 22px; /* 부모 card와 동일 */ z-index: 10; } .processing-overlay__spinner { width: 24px; height: 24px; border: 2px solid var(--border-strong); border-top-color: var(--accent-amber); border-radius: 50%; animation: spin 1s linear infinite; } .processing-overlay__label { font-size: var(--text-label); font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-secondary); } @keyframes spin { to { transform: rotate(360deg); } } ``` --- ## 4. 애니메이션 스펙 ### 4.1 웨이브폼 애니메이션 시안 B의 `scaleY` pulse + 시안 A의 코사인 가중치. ``` 방식: setInterval 100ms + smoothing 보간 높이 계산: audioLevel * cosWeight[i] * (1 ± 35% random) 보간: current += (target - current) * 0.5 CSS: transform: scaleY(n), transition: transform 100ms ease-out 바 개수: 7 ``` ### 4.2 글리치 전환 (RecordingTip CRT) 시안 A의 glitchAmount decay. 상태 전환 시 트리거. ``` 트리거: 상태 전환 (recording → thinking, thinking → error 등) 초기값: glitchAmount = 1.0 감쇠: glitchAmount *= 0.85 (매 프레임) 종료 조건: glitchAmount < 0.01 효과: RGB 채널 분리 (셰이더) 또는 CSS translateX 떨림 CSS 폴백: transform: translateX(random(-3, 3)px), opacity 깜빡임 ``` ### 4.3 카드 hover ```css transition: background-color 0.2s ease; ``` ### 4.4 버튼 press ```css transition: transform 0.05s linear, box-shadow 0.05s linear; /* active: translateY(2px) */ ``` ### 4.5 스피너 ```css animation: spin 1s linear infinite; /* rotate(360deg) */ ``` ### 4.6 프로그레스 바 (점근 수렴) ```javascript // 시간 기반: 95%에서 정체, 완료 시 100%로 snap const elapsed = (performance.now() - startTime) / 1000; const progress = Math.min(95, (1 - 1 / (1 + 1.5 * elapsed)) * 100); // 완료 시 progressBar.style.transition = 'width 200ms ease-out'; progressBar.style.width = '100%'; ``` ### 4.7 LED pulse ```css @keyframes led-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } /* duration: 1.5s, timing: ease-in-out, iteration: infinite */ ``` --- ## 5. 레이아웃 그리드 ### 5.1 메인 그리드 시안 B 기반. `max-width: 1200px`, 패딩 40px. ```css .layout { display: grid; max-width: 1200px; margin: 0 auto; padding: 40px; gap: 24px; } /* 상단 영역: 녹음 컨트롤(2fr) + 상태 패널(1fr) */ .layout__top { display: grid; grid-template-columns: 2fr 1fr; gap: 24px; } /* 카드 그리드: auto-fill */ .layout__cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 24px; } ``` ### 5.2 반응형 ```css /* 태블릿 (< 900px) */ @media (max-width: 900px) { .layout { padding: 24px; } .layout__top { grid-template-columns: 1fr; } } /* 모바일 (< 600px) */ @media (max-width: 600px) { .layout { padding: 16px; gap: 16px; } .layout__cards { grid-template-columns: 1fr; } } ``` --- ## 6. MUI 테마 03-db-and-ui.md의 `createTheme()`을 이 디자인 시스템으로 완전 교체한다. 라이트 테마를 제거하고 다크 전용으로 운영한다. ```typescript // src/renderer/theme.ts import { createTheme } from '@mui/material/styles'; // ── CSS 변수와 동기화된 팔레트 상수 ────────────────── const palette = { bg: { app: '#19191b', card: '#242427', cardHover: '#2a2a2d', elevated: '#2e2e32', input: '#1e1e21', }, accent: { amber: '#f25b29', amberDim: 'rgba(242, 91, 41, 0.15)', }, tag: { purple: '#b854f5', orange: '#f59e0b', red: '#ef4444', green: '#22c55e', }, text: { primary: '#ffffff', secondary: '#8e8e93', label: '#7c7c82', disabled: '#4a4a4e', }, border: { subtle: 'rgba(255, 255, 255, 0.04)', default: 'rgba(255, 255, 255, 0.08)', strong: 'rgba(255, 255, 255, 0.12)', }, } as const; const fontSans = [ '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif', ].join(','); const fontMono = [ 'ui-monospace', 'SFMono-Regular', '"SF Mono"', 'Menlo', 'Consolas', '"Liberation Mono"', 'monospace', ].join(','); // ── 테마 정의 ──────────────────────────────────────── export const theme = createTheme({ palette: { mode: 'dark', primary: { main: palette.accent.amber, // #f25b29 light: '#ff7a4d', dark: '#c44a22', contrastText: '#ffffff', }, secondary: { main: palette.tag.purple, // #b854f5 light: '#d084ff', dark: '#8a3cc4', }, error: { main: palette.tag.red, // #ef4444 light: '#ff7961', }, warning: { main: palette.tag.orange, // #f59e0b }, success: { main: palette.tag.green, // #22c55e light: '#4ade80', }, background: { default: palette.bg.app, // #19191b paper: palette.bg.card, // #242427 }, text: { primary: palette.text.primary, // #ffffff secondary: palette.text.secondary, // #8e8e93 disabled: palette.text.disabled, // #4a4a4e }, divider: palette.border.default, // rgba(255,255,255,0.08) }, typography: { fontFamily: fontSans, h4: { fontWeight: 700, fontSize: '22px', lineHeight: 1.3 }, h5: { fontWeight: 700, fontSize: '18px', lineHeight: 1.4 }, h6: { fontWeight: 600, fontSize: '14px', lineHeight: 1.5 }, subtitle1: { fontWeight: 500, fontSize: '18px', lineHeight: 1.4, }, body1: { fontSize: '14px', lineHeight: 1.5 }, body2: { fontSize: '12px', lineHeight: 1.4 }, button: { textTransform: 'none' as const, fontWeight: 600, fontSize: '14px', }, caption: { fontSize: '11px', fontWeight: 600, letterSpacing: '0.1em', textTransform: 'uppercase' as const, color: palette.text.label, }, overline: { fontSize: '11px', fontWeight: 600, letterSpacing: '0.1em', textTransform: 'uppercase' as const, lineHeight: 1.2, }, }, shape: { borderRadius: 22, // 카드 기본 radius }, components: { MuiCssBaseline: { styleOverrides: { body: { backgroundColor: palette.bg.app, color: palette.text.primary, }, }, }, MuiButton: { defaultProps: { disableElevation: true, }, styleOverrides: { root: { textTransform: 'none', fontWeight: 600, borderRadius: 10, padding: '10px 20px', transition: 'transform 0.05s linear, box-shadow 0.05s linear', boxShadow: '0 2px 0 rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.06)', '&:active': { transform: 'translateY(2px)', boxShadow: '0 0 0 rgba(0,0,0,0.4), inset 0 2px 4px rgba(0,0,0,0.3)', }, }, containedPrimary: { '&:hover': { backgroundColor: '#d94f24', }, }, containedSecondary: { backgroundColor: palette.bg.elevated, color: palette.text.primary, '&:hover': { backgroundColor: '#353539', }, }, }, }, MuiCard: { defaultProps: { elevation: 0, }, styleOverrides: { root: { backgroundColor: palette.bg.card, borderRadius: 22, borderTop: `1px solid ${palette.border.subtle}`, boxShadow: '0 8px 30px rgba(0, 0, 0, 0.3)', transition: 'background-color 0.2s ease', '&:hover': { backgroundColor: palette.bg.cardHover, }, }, }, }, MuiChip: { styleOverrides: { root: { borderRadius: 999, fontWeight: 700, fontSize: '11px', letterSpacing: '0.1em', textTransform: 'uppercase', height: 24, }, colorPrimary: { backgroundColor: palette.accent.amberDim, color: palette.accent.amber, }, colorSecondary: { backgroundColor: 'rgba(184, 84, 245, 0.12)', color: palette.tag.purple, }, colorSuccess: { backgroundColor: 'rgba(34, 197, 94, 0.12)', color: palette.tag.green, }, colorError: { backgroundColor: 'rgba(239, 68, 68, 0.12)', color: palette.tag.red, }, colorWarning: { backgroundColor: 'rgba(245, 158, 11, 0.12)', color: palette.tag.orange, }, }, }, MuiDrawer: { styleOverrides: { paper: { width: 240, backgroundColor: palette.bg.app, borderRight: `1px solid ${palette.border.subtle}`, }, }, }, MuiListItemButton: { styleOverrides: { root: { borderRadius: 10, marginLeft: 8, marginRight: 8, '&.Mui-selected': { backgroundColor: palette.accent.amberDim, color: palette.accent.amber, fontWeight: 600, '&:hover': { backgroundColor: 'rgba(242, 91, 41, 0.2)', }, }, }, }, }, MuiDialog: { styleOverrides: { paper: { backgroundColor: palette.bg.card, borderRadius: 22, border: `1px solid ${palette.border.subtle}`, boxShadow: '0 16px 48px rgba(0, 0, 0, 0.5)', }, }, }, MuiTextField: { defaultProps: { size: 'small', variant: 'outlined', }, styleOverrides: { root: { '& .MuiOutlinedInput-root': { backgroundColor: palette.bg.input, borderRadius: 10, '& fieldset': { borderColor: palette.border.default, }, '&:hover fieldset': { borderColor: palette.border.strong, }, '&.Mui-focused fieldset': { borderColor: palette.accent.amber, }, }, }, }, }, MuiTooltip: { defaultProps: { arrow: true, }, styleOverrides: { tooltip: { backgroundColor: palette.bg.elevated, fontSize: '12px', borderRadius: 8, border: `1px solid ${palette.border.subtle}`, }, }, }, MuiTabs: { styleOverrides: { indicator: { backgroundColor: palette.accent.amber, }, }, }, MuiTab: { styleOverrides: { root: { textTransform: 'none', fontWeight: 500, fontSize: '14px', '&.Mui-selected': { color: palette.accent.amber, fontWeight: 600, }, }, }, }, }, }); // 팔레트 상수를 외부에서 참조할 수 있도록 export export { palette as d3roPalette, fontMono as d3roFontMono }; ``` --- ## 7. RecordingTip CRT 효과 스펙 RecordingTip 팝업에 시안 A의 WebGL CRT 효과를 적용하기 위한 상세 스펙. ### 7.1 구현 전략 RecordingTip은 경량 팝업이므로 WebGL 대신 **CSS + Canvas 2D** 조합으로 구현한다. 성능 문제 발생 시 WebGL fragment shader로 전환 가능하도록 인터페이스를 분리한다. - 스캔라인: CSS `repeating-linear-gradient` (::before) - 비네팅: CSS `radial-gradient` (::after) - 노이즈: Canvas 2D 랜덤 픽셀 (mix-blend-mode: overlay) - 글리치: CSS transform + opacity 떨림 (JS 제어) ### 7.2 WebGL Fragment Shader (고급 옵션) 성능이 허용되거나 더 정교한 효과가 필요할 때 사용. ```glsl // crt.frag precision mediump float; uniform float u_time; // elapsed seconds uniform float u_glitch; // 0.0 ~ 1.0, 상태 전환 시 1.0에서 시작, ×0.85/frame 감쇠 uniform float u_amplitude; // 오디오 레벨 0.0 ~ 1.0 uniform float u_frequency; // 웨이브 주파수 (기본 4.0) uniform vec2 u_resolution; // 캔버스 해상도 varying vec2 v_uv; // 의사 난수 float rand(vec2 co) { return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453); } void main() { vec2 uv = v_uv; // ── 글리치: 수평 오프셋 ─────────────────────── float glitchOffset = u_glitch * (rand(vec2(uv.y * 13.0, u_time)) - 0.5) * 0.05; uv.x += glitchOffset; // ── 스캔라인 ────────────────────────────────── float scanline = sin(uv.y * u_resolution.y * 1.5) * 0.04; // ── 앰버 인광색 기본 ────────────────────────── vec3 amber = vec3(0.949, 0.357, 0.161); // #f25b29 // ── 웨이브 표현 (간략) ──────────────────────── float wave = sin(uv.x * u_frequency * 6.2832 + u_time * 2.0) * u_amplitude * 0.5 + 0.5; // ── 비네팅 ──────────────────────────────────── float dist = distance(uv, vec2(0.5)); float vignette = smoothstep(0.7, 0.3, dist); // ── 노이즈 ──────────────────────────────────── float noise = rand(uv + u_time * 0.01) * 0.05; // ── 합성 ────────────────────────────────────── float intensity = wave * vignette - scanline + noise; intensity = clamp(intensity, 0.0, 1.0); // ── RGB 채널 분리 (글리치 시) ────────────────── float r = intensity; float g = intensity; float b = intensity; if (u_glitch > 0.01) { float shift = u_glitch * 0.01; r = clamp(intensity + shift, 0.0, 1.0); b = clamp(intensity - shift, 0.0, 1.0); } gl_FragColor = vec4(amber * vec3(r, g, b), intensity * 0.8); } ``` ### 7.3 Uniform 변수 제어 ```typescript interface CrtUniforms { u_time: number; // performance.now() / 1000 u_glitch: number; // 상태 전환 시 1.0 → ×0.85/frame → 0.0 u_amplitude: number; // audioLevel (0.0 ~ 1.0) u_frequency: number; // 기본 4.0 u_resolution: [number, number]; // canvas 크기 } // 글리치 감쇠 로직 let glitchAmount = 0; function triggerGlitch() { glitchAmount = 1.0; } function updateGlitch() { glitchAmount *= 0.85; if (glitchAmount < 0.01) glitchAmount = 0; } // updateGlitch()는 매 rAF에서 호출 ``` ### 7.4 CSS 폴백 파라미터 (WebGL 미사용 시) | 효과 | CSS 구현 | 파라미터 | |------|----------|----------| | 스캔라인 | `repeating-linear-gradient(0deg, transparent 0 2px, rgba(0,0,0,0.15) 2px 4px)` | 간격 4px, 투명도 0.15 | | 비네팅 | `radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.4) 100%)` | 중심 50% 투명 | | 노이즈 | Canvas 2D: 4x4 블록, 랜덤 alpha 0~0.03, mix-blend-mode: overlay | 30fps 갱신 | | 글리치 | `transform: translateX(${(Math.random()-0.5)*6*glitch}px)`, opacity 깜빡임 | 감쇠율 0.85/frame | | 인광 glow | `text-shadow: 0 0 6px rgba(242,91,41,0.6)`, `box-shadow: 0 0 4px` | spread 4~6px | --- ## 부록: 시안 A/B 요소 매핑 요약 | 컴포넌트 | 시안 A 요소 | 시안 B 요소 | D3RO-VOICE 적용 | |----------|------------|------------|----------------| | Card | 다크 섀시 #242528 | 카드 #242427, radius 22px | 시안 B 카드 스타일 | | Button | 물리 눌림 (inset shadow) | - | 시안 A 물리 버튼 느낌 | | Tag/Badge | - | pill, 색상코딩 | 시안 B 태그 시스템 | | LED | 8px, glow | - | 시안 A LED | | WaveformBar | cos 가중치 | 7개 바, scaleY | 시안 B 구조 + 시안 A 앰버색 | | RecordingTip | CRT, 스캔라인, 글리치 | - | 시안 A CRT 전적용 | | Dashboard | - | 카드 그리드, auto-fill | 시안 B 레이아웃 | | StatusPanel | 각인 텍스트 | 서비스 상태 리스트 | 시안 B 리스트 + 시안 A 모노폰트 | | ProcessingOverlay | - | blur(4px) + spinner | 시안 B | | 컬러: 배경 | #242528 | #19191b, #242427 | 시안 B | | 컬러: 악센트 | 앰버 #f25b29 | - | 시안 A | | 타이포: 본문 | - | 시스템 폰트 | 시안 B | | 타이포: 상태 | 모노스페이스, 각인 | - | 시안 A |