component
WS2812B-2020
Public Unreviewedby AravK
Worldsemi · Intelligent Control LED · 2020 (2.0 × 2.0mm) · Built-in IC + RGB dies
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WS2812B-2020 — Component Characterization</title>
<style>
@font-face { font-family: 'Familjen Grotesk'; src: url('https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-700-normal.woff2') format('woff2'); font-weight: 700; }
@font-face { font-family: 'Familjen Grotesk'; src: url('https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-500-normal.woff2') format('woff2'); font-weight: 500; }
@font-face { font-family: 'Satoshi'; src: url('https://adom.inc/fonts/satoshi/satoshi-400-normal.woff2') format('woff2'); font-weight: 400; }
@font-face { font-family: 'Satoshi'; src: url('https://adom.inc/fonts/satoshi/satoshi-500-normal.woff2') format('woff2'); font-weight: 500; }
@font-face { font-family: 'JetBrains Mono'; src: url('https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx3cwhsk.woff2') format('woff2'); font-weight: 400; }
:root {
--bg: #0d1117; --surface: #161b22; --elevated: #1c2128; --border: #30363d; --border-muted: #21262d;
--text: #e6edf3; --text-muted: #8b949e; --text-dim: #484f58;
--accent: #00b8b0; --accent-bright: #00e6dc; --accent-dim: rgba(0,184,176,0.12); --accent-border: rgba(0,184,176,0.25);
--green: #3fb950; --orange: #d29922; --red: #f85149; --purple: #8C6BF7; --blue: #58a6ff;
--rgb-r: #DC143C; --rgb-g: #32CD32; --rgb-b: #4169E1;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Satoshi', sans-serif; background: var(--bg); color: var(--text); font-size: 13px; line-height: 1.6; overflow-y: auto; }
h1,h2,h3 { font-family: 'Familjen Grotesk', sans-serif; font-weight: 700; }
code { font-family: 'JetBrains Mono', monospace; font-size: 12px; }
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-bright); text-decoration: underline; }
.header { background: var(--surface); border-bottom: 1px solid var(--border-muted); padding: 20px 24px; }
.header-top { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
.header .brand { color: var(--accent); font-family: 'Familjen Grotesk', sans-serif; font-size: 12px; font-weight: 600; }
.header .vsep { width: 1px; height: 16px; background: var(--border); }
.header .crumb { color: var(--text-dim); font-size: 12px; }
.header h1 { font-size: 22px; margin-bottom: 2px; }
.header h1 .val { background: linear-gradient(90deg, #DC143C, #32CD32, #4169E1); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.header .subtitle { color: var(--text-muted); font-size: 13px; }
.header .tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
.tag { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; }
.tag.pkg { background: rgba(0,184,176,0.12); color: var(--accent); }
.tag.mfr { background: rgba(88,166,255,0.12); color: var(--blue); }
.tag.series { background: rgba(140,107,247,0.12); color: var(--purple); }
.tag.rohs { background: rgba(63,185,80,0.12); color: var(--green); }
.tag.app { background: linear-gradient(90deg, rgba(220,20,60,0.15), rgba(50,205,50,0.15), rgba(65,105,225,0.15)); color: #e6edf3; }
.container { max-width: 1100px; margin: 0 auto; padding: 24px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 800px) { .grid-2 { grid-template-columns: 1fr; } }
.section { margin-bottom: 28px; }
.section-title { font-size: 14px; color: var(--text); margin-bottom: 12px; padding-bottom: 6px; border-bottom: 1px solid var(--border-muted); display: flex; align-items: center; gap: 8px; }
.card { background: var(--surface); border: 1px solid var(--border-muted); border-radius: 8px; padding: 16px; }
.card:hover { border-color: var(--accent-border); }
.spec-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; }
.spec-card { background: var(--surface); border: 1px solid var(--border-muted); border-radius: 8px; padding: 12px 14px; text-align: center; }
.spec-card .sc-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-dim); margin-bottom: 4px; }
.spec-card .sc-value { font-size: 22px; font-weight: 700; font-family: 'Familjen Grotesk', sans-serif; }
.spec-card .sc-detail { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.spec-card.highlight { border-color: var(--accent-border); background: linear-gradient(180deg, rgba(0,184,176,0.06), var(--surface)); }
.spec-card.highlight .sc-value { color: var(--accent); }
.detail-table { width: 100%; border-collapse: collapse; }
.detail-table tr { border-bottom: 1px solid var(--border-muted); }
.detail-table tr:last-child { border-bottom: none; }
.detail-table td { padding: 8px 0; }
.detail-table td:first-child { color: var(--text-muted); font-size: 12px; width: 45%; }
.detail-table td:last-child { font-family: 'JetBrains Mono', monospace; font-size: 13px; text-align: right; }
.chart-wrap { background: var(--surface); border: 1px solid var(--border-muted); border-radius: 8px; padding: 16px; }
.chart-wrap h3 { font-size: 13px; color: var(--text); margin-bottom: 12px; }
.chart-wrap .chart-note { font-size: 11px; color: var(--text-dim); margin-top: 8px; }
svg text { font-family: 'JetBrains Mono', monospace; }
.env-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
@media (max-width: 700px) { .env-grid { grid-template-columns: 1fr; } }
.env-card { background: var(--surface); border: 1px solid var(--border-muted); border-radius: 8px; padding: 12px 14px; }
.env-card .env-name { font-size: 12px; font-weight: 600; margin-bottom: 4px; }
.env-card .env-spec { font-size: 11px; color: var(--text-muted); }
.env-card .env-detail { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text); margin-top: 4px; }
.open-full-banner { display: flex; align-items: center; justify-content: center; padding: 12px 24px; background: linear-gradient(90deg, rgba(0,184,176,0.12), rgba(0,184,176,0.06)); border-bottom: 1px solid var(--accent-border); }
.open-full-btn { display: inline-flex; align-items: center; gap: 8px; padding: 8px 20px; background: var(--accent); color: var(--bg); font-family: 'Familjen Grotesk', sans-serif; font-size: 13px; font-weight: 600; border-radius: 6px; text-decoration: none; transition: background 0.15s; cursor: pointer; border: none; }
.open-full-btn:hover { background: var(--accent-bright); text-decoration: none; color: var(--bg); }
.source { text-align: center; color: var(--text-dim); font-size: 11px; padding: 24px 0 12px; border-top: 1px solid var(--border-muted); margin-top: 20px; }
.dl-btn { display: inline-block; margin-top: 10px; padding: 6px 14px; background: var(--accent-dim); border: 1px solid var(--accent-border); border-radius: 6px; color: var(--accent); font-size: 11px; font-weight: 500; font-family: 'Satoshi', sans-serif; text-decoration: none; transition: background 0.15s, border-color 0.15s; }
.dl-btn:hover { background: rgba(0,184,176,0.2); border-color: var(--accent); text-decoration: none; color: var(--accent-bright); }
/* RGB Mixer */
.mixer-card { background: var(--surface); border: 1px solid var(--accent-border); border-radius: 8px; padding: 20px; }
.mixer-card h3 { font-size: 14px; color: var(--accent); margin-bottom: 16px; font-family: 'Familjen Grotesk', sans-serif; }
.mixer-sliders { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin-bottom: 16px; }
.mixer-slider { text-align: center; }
.mixer-slider label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; font-weight: 600; }
.mixer-slider input[type=range] { width: 100%; height: 8px; -webkit-appearance: none; border-radius: 4px; outline: none; }
.mixer-slider input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; cursor: pointer; border: 2px solid var(--bg); }
.mixer-slider .val { font-family: 'JetBrains Mono', monospace; font-size: 16px; font-weight: 700; margin-top: 4px; }
.mixer-preview { width: 100%; height: 100px; border-radius: 8px; margin-bottom: 12px; transition: background-color 0.1s; display: flex; align-items: center; justify-content: center; }
.mixer-results { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
.mixer-result { background: var(--bg); border: 1px solid var(--border-muted); border-radius: 6px; padding: 10px 12px; text-align: center; }
.mixer-result .mr-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-dim); margin-bottom: 2px; }
.mixer-result .mr-value { font-size: 16px; font-weight: 700; font-family: 'Familjen Grotesk', sans-serif; color: var(--accent); }
.mixer-result .mr-detail { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
</style>
</head>
<body>
<div class="open-full-banner" id="fullPageBanner" style="display:none">
<span style="color:var(--text-muted);font-size:12px;margin-right:8px">Full page:</span>
<code style="color:var(--accent);font-size:11px;background:var(--elevated);padding:4px 8px;border-radius:4px;user-select:all">https://wiki-ufypy5dpx93o.adom.cloud/static/libraries/ws2812b-2020/WS2812B-2020.html</code>
<button class="open-full-btn" id="copyFullUrl" style="margin-left:8px">Copy Link</button>
</div>
<script>
if(window.self!==window.top){document.getElementById('fullPageBanner').style.display='flex'}
document.getElementById('copyFullUrl').addEventListener('click',function(){var t=document.createElement('textarea');t.value='https://wiki-ufypy5dpx93o.adom.cloud/static/libraries/ws2812b-2020/WS2812B-2020.html';document.body.appendChild(t);t.select();document.execCommand('copy');document.body.removeChild(t);this.textContent='Copied!';var b=this;setTimeout(function(){b.textContent='Copy Link'},1500)});
</script>
<div class="header">
<div class="header-top">
<div class="brand">Adom</div>
<div class="vsep"></div>
<div class="crumb">Components › LEDs › Addressable RGB</div>
</div>
<h1>WS2812B-2020 — <span class="val">Addressable RGB</span></h1>
<div class="subtitle">Worldsemi · Intelligent Control LED · 2020 (2.0 × 2.0mm) · Built-in IC + RGB dies</div>
<div class="tags">
<span class="tag pkg">2020</span>
<span class="tag mfr">Worldsemi</span>
<span class="tag series">WS2812B</span>
<span class="tag rohs">RoHS</span>
<span class="tag app">Addressable RGB</span>
</div>
</div>
<div class="container">
<!-- Key Specs -->
<div class="section">
<div class="spec-cards">
<div class="spec-card highlight">
<div class="sc-label">VDD</div>
<div class="sc-value">5V</div>
<div class="sc-detail">3.7-5.3V range</div>
</div>
<div class="spec-card">
<div class="sc-label">Protocol</div>
<div class="sc-value" style="font-size:16px">NRZ</div>
<div class="sc-detail">800Kbps single-wire</div>
</div>
<div class="spec-card">
<div class="sc-label">Colors</div>
<div class="sc-value" style="font-size:16px">16.7M</div>
<div class="sc-detail">24-bit (8 per ch)</div>
</div>
<div class="spec-card">
<div class="sc-label">Refresh</div>
<div class="sc-value" style="font-size:18px">2KHz</div>
<div class="sc-detail">No flicker</div>
</div>
<div class="spec-card">
<div class="sc-label">Cascade</div>
<div class="sc-value" style="font-size:16px">1024+</div>
<div class="sc-detail">@ 30fps</div>
</div>
<div class="spec-card">
<div class="sc-label">Package</div>
<div class="sc-value" style="font-size:16px">2020</div>
<div class="sc-detail">2.0×2.0×0.84mm</div>
</div>
</div>
</div>
<div class="grid-2">
<!-- Left column -->
<div>
<!-- IC & Electrical Characteristics -->
<div class="section">
<h2 class="section-title">IC & Electrical Characteristics</h2>
<div class="card">
<table class="detail-table">
<tr><td>Supply Voltage (VDD)</td><td>3.7–5.3V (rated 5V)</td></tr>
<tr><td>Rated VDD Range</td><td>+3.7V to +5.3V</td></tr>
<tr><td>Input Current (DI)</td><td>±1μA max</td></tr>
<tr><td>Logic High (VIH)</td><td>0.7×VDD to VDD+0.7V</td></tr>
<tr><td>Logic Low (VIL)</td><td>max 0.3 × VDD</td></tr>
<tr><td>Data Rate</td><td>800 Kbps (NRZ)</td></tr>
<tr><td>PWM Frequency</td><td>2 KHz</td></tr>
<tr><td>Color Depth</td><td>24-bit (8 per R/G/B)</td></tr>
<tr><td>Reset Time</td><td>>280μs</td></tr>
<tr><td>Max Cascade</td><td>1024 pixels @ 30fps</td></tr>
</table>
</div>
</div>
<!-- LED Characteristics -->
<div class="section">
<h2 class="section-title">LED Characteristics @ VDD=5V</h2>
<div class="card">
<table class="detail-table">
<tr><td>Red Luminous Intensity</td><td style="color:#DC143C">300–500 mcd @ 16mA</td></tr>
<tr><td>Red Wavelength</td><td style="color:#DC143C">620–630nm</td></tr>
<tr><td>Green Luminous Intensity</td><td style="color:#32CD32">800–1500 mcd @ 16mA</td></tr>
<tr><td>Green Wavelength</td><td style="color:#32CD32">515–525nm</td></tr>
<tr><td>Blue Luminous Intensity</td><td style="color:#4169E1">200–300 mcd @ 16mA</td></tr>
<tr><td>Blue Wavelength</td><td style="color:#4169E1">465–475nm</td></tr>
<tr><td>White (all on)</td><td>CIE x=0.21, y=0.23</td></tr>
<tr><td>White CCT</td><td>~25000K (very cool)</td></tr>
<tr><td>White Current</td><td>48mA (16mA × 3)</td></tr>
</table>
</div>
</div>
</div>
<!-- Right column -->
<div>
<!-- Pin Configuration -->
<div class="section">
<h2 class="section-title">Pin Configuration</h2>
<div class="card" style="text-align:center">
<svg width="100%" height="240" viewBox="0 0 400 240">
<!-- IC body -->
<rect x="120" y="40" width="160" height="160" rx="8" fill="#1c2128" stroke="#30363d" stroke-width="2"/>
<!-- Pin 1 indicator -->
<circle cx="140" cy="60" r="4" fill="#484f58"/>
<!-- RGB dies inside -->
<rect x="155" y="80" width="30" height="30" rx="3" fill="rgba(220,20,60,0.3)" stroke="#DC143C" stroke-width="1"/>
<text x="170" y="100" fill="#DC143C" font-size="10" text-anchor="middle">R</text>
<rect x="195" y="80" width="30" height="30" rx="3" fill="rgba(50,205,50,0.3)" stroke="#32CD32" stroke-width="1"/>
<text x="210" y="100" fill="#32CD32" font-size="10" text-anchor="middle">G</text>
<rect x="235" y="80" width="30" height="30" rx="3" fill="rgba(65,105,225,0.3)" stroke="#4169E1" stroke-width="1"/>
<text x="250" y="100" fill="#4169E1" font-size="10" text-anchor="middle">B</text>
<!-- IC block -->
<rect x="165" y="125" width="70" height="35" rx="3" fill="rgba(0,184,176,0.1)" stroke="#00b8b0" stroke-width="1"/>
<text x="200" y="147" fill="#00b8b0" font-size="9" text-anchor="middle">WS2812B IC</text>
<!-- Pin 1: DO (top-left) -->
<line x1="120" y1="70" x2="80" y2="70" stroke="#e6edf3" stroke-width="1.5"/>
<circle cx="80" cy="70" r="3" fill="#e6edf3"/>
<text x="72" y="66" fill="#8b949e" font-size="10" text-anchor="end">1</text>
<text x="72" y="78" fill="#d29922" font-size="10" text-anchor="end">DO</text>
<!-- Pin 2: GND (bottom-left) -->
<line x1="120" y1="170" x2="80" y2="170" stroke="#e6edf3" stroke-width="1.5"/>
<circle cx="80" cy="170" r="3" fill="#e6edf3"/>
<text x="72" y="166" fill="#8b949e" font-size="10" text-anchor="end">2</text>
<text x="72" y="178" fill="#8b949e" font-size="10" text-anchor="end">GND</text>
<!-- Pin 3: DI (bottom-right) -->
<line x1="280" y1="170" x2="320" y2="170" stroke="#e6edf3" stroke-width="1.5"/>
<circle cx="320" cy="170" r="3" fill="#e6edf3"/>
<text x="328" y="166" fill="#8b949e" font-size="10">3</text>
<text x="328" y="178" fill="#3fb950" font-size="10">DI</text>
<!-- Pin 4: VDD (top-right) -->
<line x1="280" y1="70" x2="320" y2="70" stroke="#e6edf3" stroke-width="1.5"/>
<circle cx="320" cy="70" r="3" fill="#e6edf3"/>
<text x="328" y="66" fill="#8b949e" font-size="10">4</text>
<text x="328" y="78" fill="#f85149" font-size="10">VDD</text>
<!-- Title -->
<text x="200" y="25" fill="#e6edf3" font-size="11" text-anchor="middle" font-weight="bold">WS2812B-2020 (Top View)</text>
<!-- Bottom note -->
<text x="200" y="225" fill="#484f58" font-size="9" text-anchor="middle">4-pin QFN-like · 2.0 × 2.0 × 0.84mm</text>
</svg>
</div>
</div>
</div>
</div>
<!-- Data Protocol Timing Diagram -->
<div class="section">
<h2 class="section-title">NRZ Data Protocol Timing</h2>
<div class="chart-wrap">
<h3>Single-Wire NRZ Encoding — 800Kbps</h3>
<svg width="100%" height="280" viewBox="0 0 700 280">
<rect x="30" y="10" width="640" height="260" fill="#0d1117" rx="4"/>
<!-- 0-code waveform -->
<text x="50" y="40" fill="#e6edf3" font-size="11" font-weight="bold">0 Code:</text>
<line x1="120" y1="80" x2="120" y2="45" stroke="#3fb950" stroke-width="2.5"/>
<line x1="120" y1="45" x2="220" y2="45" stroke="#3fb950" stroke-width="2.5"/>
<line x1="220" y1="45" x2="220" y2="80" stroke="#3fb950" stroke-width="2.5"/>
<line x1="220" y1="80" x2="420" y2="80" stroke="#3fb950" stroke-width="2.5"/>
<!-- T0H -->
<line x1="120" y1="90" x2="220" y2="90" stroke="#d29922" stroke-width="0.5"/>
<line x1="120" y1="86" x2="120" y2="94" stroke="#d29922" stroke-width="0.5"/>
<line x1="220" y1="86" x2="220" y2="94" stroke="#d29922" stroke-width="0.5"/>
<text x="170" y="105" fill="#d29922" font-size="9" text-anchor="middle">T0H: 220-380ns</text>
<!-- T0L -->
<line x1="220" y1="90" x2="420" y2="90" stroke="#58a6ff" stroke-width="0.5"/>
<line x1="420" y1="86" x2="420" y2="94" stroke="#58a6ff" stroke-width="0.5"/>
<text x="320" y="105" fill="#58a6ff" font-size="9" text-anchor="middle">T0L: 580ns-1μs</text>
<!-- Period -->
<text x="560" y="68" fill="#484f58" font-size="9" text-anchor="middle">≥1.25μs total</text>
<!-- 1-code waveform -->
<text x="50" y="140" fill="#e6edf3" font-size="11" font-weight="bold">1 Code:</text>
<line x1="120" y1="180" x2="120" y2="145" stroke="#8C6BF7" stroke-width="2.5"/>
<line x1="120" y1="145" x2="330" y2="145" stroke="#8C6BF7" stroke-width="2.5"/>
<line x1="330" y1="145" x2="330" y2="180" stroke="#8C6BF7" stroke-width="2.5"/>
<line x1="330" y1="180" x2="420" y2="180" stroke="#8C6BF7" stroke-width="2.5"/>
<!-- T1H -->
<line x1="120" y1="190" x2="330" y2="190" stroke="#d29922" stroke-width="0.5"/>
<line x1="120" y1="186" x2="120" y2="194" stroke="#d29922" stroke-width="0.5"/>
<line x1="330" y1="186" x2="330" y2="194" stroke="#d29922" stroke-width="0.5"/>
<text x="225" y="205" fill="#d29922" font-size="9" text-anchor="middle">T1H: 580ns-1μs</text>
<!-- T1L -->
<line x1="330" y1="190" x2="420" y2="190" stroke="#58a6ff" stroke-width="0.5"/>
<line x1="420" y1="186" x2="420" y2="194" stroke="#58a6ff" stroke-width="0.5"/>
<text x="375" y="205" fill="#58a6ff" font-size="9" text-anchor="middle">T1L: 220ns-420ns</text>
<!-- Reset -->
<text x="50" y="238" fill="#e6edf3" font-size="11" font-weight="bold">Reset:</text>
<line x1="120" y1="245" x2="500" y2="245" stroke="#f85149" stroke-width="2.5"/>
<text x="310" y="260" fill="#f85149" font-size="9" text-anchor="middle">RES: >280μs low</text>
<!-- High/Low labels -->
<text x="110" y="48" fill="#3fb950" font-size="8" text-anchor="end">HIGH</text>
<text x="110" y="83" fill="#3fb950" font-size="8" text-anchor="end">LOW</text>
<text x="110" y="148" fill="#8C6BF7" font-size="8" text-anchor="end">HIGH</text>
<text x="110" y="183" fill="#8C6BF7" font-size="8" text-anchor="end">LOW</text>
<!-- Data format -->
<rect x="470" y="30" width="180" height="90" rx="4" fill="rgba(0,184,176,0.08)" stroke="rgba(0,184,176,0.2)" stroke-width="0.5"/>
<text x="560" y="50" fill="#00b8b0" font-size="10" text-anchor="middle" font-weight="bold">24-bit Frame</text>
<text x="560" y="68" fill="#32CD32" font-size="9" text-anchor="middle">G7..G0 (8 bits)</text>
<text x="560" y="82" fill="#DC143C" font-size="9" text-anchor="middle">R7..R0 (8 bits)</text>
<text x="560" y="96" fill="#4169E1" font-size="9" text-anchor="middle">B7..B0 (8 bits)</text>
<text x="560" y="112" fill="#484f58" font-size="8" text-anchor="middle">MSB first per channel</text>
</svg>
<div class="chart-note">Single-wire NRZ protocol at 800Kbps. Each bit is encoded by the ratio of high/low time within a 1.25μs period. A reset pulse (>280μs low) latches data. Frame order is G-R-B, MSB first.</div>
</div>
</div>
<!-- Cascade / Daisy Chain Diagram -->
<div class="section">
<h2 class="section-title">Cascade / Daisy-Chain Topology</h2>
<div class="chart-wrap">
<h3>DI→DO chaining — up to 1024+ pixels</h3>
<svg width="100%" height="180" viewBox="0 0 700 180">
<rect x="10" y="10" width="680" height="160" fill="#0d1117" rx="4"/>
<!-- MCU -->
<rect x="30" y="55" width="70" height="60" rx="4" fill="rgba(0,184,176,0.12)" stroke="#00b8b0" stroke-width="1.5"/>
<text x="65" y="80" fill="#00b8b0" font-size="10" text-anchor="middle">MCU</text>
<text x="65" y="95" fill="#484f58" font-size="8" text-anchor="middle">GPIO</text>
<!-- Arrow MCU to LED1 -->
<line x1="100" y1="85" x2="150" y2="85" stroke="#3fb950" stroke-width="1.5"/>
<polygon points="150,85 145,81 145,89" fill="#3fb950"/>
<text x="125" y="78" fill="#3fb950" font-size="8" text-anchor="middle">DIN</text>
<!-- LED 1 -->
<rect x="155" y="50" width="80" height="70" rx="6" fill="rgba(220,20,60,0.08)" stroke="#DC143C" stroke-width="1"/>
<text x="195" y="72" fill="#e6edf3" font-size="9" text-anchor="middle" font-weight="bold">LED #0</text>
<rect x="170" y="82" width="14" height="14" rx="2" fill="rgba(220,20,60,0.3)" stroke="#DC143C" stroke-width="0.5"/>
<rect x="188" y="82" width="14" height="14" rx="2" fill="rgba(50,205,50,0.3)" stroke="#32CD32" stroke-width="0.5"/>
<rect x="206" y="82" width="14" height="14" rx="2" fill="rgba(65,105,225,0.3)" stroke="#4169E1" stroke-width="0.5"/>
<text x="168" y="112" fill="#3fb950" font-size="7">DI</text>
<text x="218" y="112" fill="#d29922" font-size="7">DO</text>
<!-- Arrow LED1 to LED2 -->
<line x1="235" y1="85" x2="285" y2="85" stroke="#d29922" stroke-width="1.5"/>
<polygon points="285,85 280,81 280,89" fill="#d29922"/>
<!-- LED 2 -->
<rect x="290" y="50" width="80" height="70" rx="6" fill="rgba(50,205,50,0.08)" stroke="#32CD32" stroke-width="1"/>
<text x="330" y="72" fill="#e6edf3" font-size="9" text-anchor="middle" font-weight="bold">LED #1</text>
<rect x="305" y="82" width="14" height="14" rx="2" fill="rgba(220,20,60,0.3)" stroke="#DC143C" stroke-width="0.5"/>
<rect x="323" y="82" width="14" height="14" rx="2" fill="rgba(50,205,50,0.3)" stroke="#32CD32" stroke-width="0.5"/>
<rect x="341" y="82" width="14" height="14" rx="2" fill="rgba(65,105,225,0.3)" stroke="#4169E1" stroke-width="0.5"/>
<text x="303" y="112" fill="#3fb950" font-size="7">DI</text>
<text x="353" y="112" fill="#d29922" font-size="7">DO</text>
<!-- Arrow LED2 to LED3 -->
<line x1="370" y1="85" x2="420" y2="85" stroke="#d29922" stroke-width="1.5"/>
<polygon points="420,85 415,81 415,89" fill="#d29922"/>
<!-- LED 3 -->
<rect x="425" y="50" width="80" height="70" rx="6" fill="rgba(65,105,225,0.08)" stroke="#4169E1" stroke-width="1"/>
<text x="465" y="72" fill="#e6edf3" font-size="9" text-anchor="middle" font-weight="bold">LED #2</text>
<rect x="440" y="82" width="14" height="14" rx="2" fill="rgba(220,20,60,0.3)" stroke="#DC143C" stroke-width="0.5"/>
<rect x="458" y="82" width="14" height="14" rx="2" fill="rgba(50,205,50,0.3)" stroke="#32CD32" stroke-width="0.5"/>
<rect x="476" y="82" width="14" height="14" rx="2" fill="rgba(65,105,225,0.3)" stroke="#4169E1" stroke-width="0.5"/>
<!-- Dots -->
<line x1="505" y1="85" x2="530" y2="85" stroke="#d29922" stroke-width="1.5"/>
<circle cx="545" cy="85" r="2" fill="#484f58"/>
<circle cx="560" cy="85" r="2" fill="#484f58"/>
<circle cx="575" cy="85" r="2" fill="#484f58"/>
<!-- LED N -->
<rect x="590" y="55" width="70" height="60" rx="6" fill="rgba(140,107,247,0.08)" stroke="#8C6BF7" stroke-width="1"/>
<text x="625" y="80" fill="#e6edf3" font-size="9" text-anchor="middle" font-weight="bold">LED #N</text>
<text x="625" y="95" fill="#484f58" font-size="8" text-anchor="middle">N ≤ 1024</text>
<!-- VDD/GND bus -->
<line x1="155" y1="140" x2="660" y2="140" stroke="#f85149" stroke-width="1" stroke-dasharray="4,2"/>
<text x="140" y="143" fill="#f85149" font-size="8" text-anchor="end">VDD (5V)</text>
<line x1="155" y1="152" x2="660" y2="152" stroke="#484f58" stroke-width="1" stroke-dasharray="4,2"/>
<text x="140" y="155" fill="#484f58" font-size="8" text-anchor="end">GND</text>
</svg>
<div class="chart-note">Each LED consumes its 24-bit frame (GRB) and passes remaining data from DO to the next LED's DI. After all data is sent, a >280μs reset pulse latches values. Up to 1024 LEDs at 30fps.</div>
</div>
</div>
<!-- Per-Channel Brightness Bar Chart -->
<div class="section">
<h2 class="section-title">Per-Channel Brightness @ 16mA</h2>
<div class="chart-wrap">
<h3>Luminous Intensity by Color Channel</h3>
<svg width="100%" height="250" viewBox="0 0 500 250">
<rect x="60" y="10" width="380" height="190" fill="#0d1117" rx="4"/>
<!-- Grid -->
<line x1="60" y1="200" x2="440" y2="200" stroke="#21262d" stroke-width="1"/>
<line x1="60" y1="10" x2="60" y2="200" stroke="#21262d" stroke-width="1"/>
<line x1="60" y1="162" x2="440" y2="162" stroke="#21262d" stroke-width="0.5" stroke-dasharray="4,4"/>
<line x1="60" y1="124" x2="440" y2="124" stroke="#21262d" stroke-width="0.5" stroke-dasharray="4,4"/>
<line x1="60" y1="86" x2="440" y2="86" stroke="#21262d" stroke-width="0.5" stroke-dasharray="4,4"/>
<line x1="60" y1="48" x2="440" y2="48" stroke="#21262d" stroke-width="0.5" stroke-dasharray="4,4"/>
<line x1="60" y1="10" x2="440" y2="10" stroke="#21262d" stroke-width="0.5" stroke-dasharray="4,4"/>
<!-- Red bar: 400 mcd (midpoint 300-500) => height = 400/1500 * 190 = 51 -->
<rect x="100" y="149" width="60" height="51" rx="3" fill="rgba(220,20,60,0.6)" stroke="#DC143C" stroke-width="1"/>
<text x="130" y="142" fill="#DC143C" font-size="12" text-anchor="middle" font-weight="bold">400</text>
<text x="130" y="218" fill="#DC143C" font-size="10" text-anchor="middle">Red</text>
<text x="130" y="230" fill="#484f58" font-size="8" text-anchor="middle">620-630nm</text>
<!-- Green bar: 1150 mcd (midpoint 800-1500) => height = 1150/1500 * 190 = 146 -->
<rect x="220" y="54" width="60" height="146" rx="3" fill="rgba(50,205,50,0.6)" stroke="#32CD32" stroke-width="1"/>
<text x="250" y="47" fill="#32CD32" font-size="12" text-anchor="middle" font-weight="bold">1150</text>
<text x="250" y="218" fill="#32CD32" font-size="10" text-anchor="middle">Green</text>
<text x="250" y="230" fill="#484f58" font-size="8" text-anchor="middle">515-525nm</text>
<!-- Blue bar: 250 mcd (midpoint 200-300) => height = 250/1500 * 190 = 32 -->
<rect x="340" y="168" width="60" height="32" rx="3" fill="rgba(65,105,225,0.6)" stroke="#4169E1" stroke-width="1"/>
<text x="370" y="161" fill="#4169E1" font-size="12" text-anchor="middle" font-weight="bold">250</text>
<text x="370" y="218" fill="#4169E1" font-size="10" text-anchor="middle">Blue</text>
<text x="370" y="230" fill="#484f58" font-size="8" text-anchor="middle">465-475nm</text>
<!-- Y axis -->
<text x="55" y="203" fill="#484f58" font-size="9" text-anchor="end">0</text>
<text x="55" y="165" fill="#484f58" font-size="9" text-anchor="end">300</text>
<text x="55" y="127" fill="#484f58" font-size="9" text-anchor="end">600</text>
<text x="55" y="89" fill="#484f58" font-size="9" text-anchor="end">900</text>
<text x="55" y="51" fill="#484f58" font-size="9" text-anchor="end">1200</text>
<text x="55" y="13" fill="#484f58" font-size="9" text-anchor="end">1500</text>
<text x="15" y="110" fill="#484f58" font-size="9" text-anchor="middle" transform="rotate(-90,15,110)">Iv (mcd)</text>
<text x="250" y="248" fill="#484f58" font-size="9" text-anchor="middle">All channels @ I<tspan baseline-shift="sub" font-size="7">f</tspan> = 16mA, VDD = 5V</text>
</svg>
<div class="chart-note">Green channel is brightest at 800–1500 mcd, followed by red at 300–500 mcd and blue at 200–300 mcd. This reflects typical human eye sensitivity (scotopic peak ~507nm). Green dominates perceived brightness in color mixing.</div>
</div>
</div>
<!-- RGB Color Mixer -->
<div class="section">
<h2 class="section-title">RGB Color Mixer & Power Calculator</h2>
<div class="mixer-card">
<h3>🎨 24-bit Color Picker</h3>
<div class="mixer-sliders">
<div class="mixer-slider">
<label style="color:#DC143C">Red</label>
<input type="range" id="mix-r" min="0" max="255" value="255" style="background:linear-gradient(90deg,#000,#DC143C)">
<div class="val" id="mix-r-val" style="color:#DC143C">255</div>
</div>
<div class="mixer-slider">
<label style="color:#32CD32">Green</label>
<input type="range" id="mix-g" min="0" max="255" value="128" style="background:linear-gradient(90deg,#000,#32CD32)">
<div class="val" id="mix-g-val" style="color:#32CD32">128</div>
</div>
<div class="mixer-slider">
<label style="color:#4169E1">Blue</label>
<input type="range" id="mix-b" min="0" max="255" value="0" style="background:linear-gradient(90deg,#000,#4169E1)">
<div class="val" id="mix-b-val" style="color:#4169E1">0</div>
</div>
</div>
<div class="mixer-preview" id="mix-preview" style="background-color:rgb(255,128,0); box-shadow: 0 4px 30px rgba(255,128,0,0.4);">
<span id="mix-preview-text" style="font-size:24px;font-weight:700;font-family:'Familjen Grotesk',sans-serif;text-shadow:0 2px 8px rgba(0,0,0,0.5)"></span>
</div>
<div class="mixer-results">
<div class="mixer-result">
<div class="mr-label">Hex Code</div>
<div class="mr-value" id="mix-hex">#FF8000</div>
<div class="mr-detail" id="mix-grb">GRB: 0x80FF00</div>
</div>
<div class="mixer-result">
<div class="mr-label">Power @ 5V</div>
<div class="mr-value" id="mix-power">90.0 mW</div>
<div class="mr-detail" id="mix-current">18.0 mA total</div>
</div>
<div class="mixer-result">
<div class="mr-label">24-bit Data</div>
<div class="mr-value" id="mix-binary" style="font-size:11px;word-break:break-all">10000000 11111111 00000000</div>
<div class="mr-detail">G R B (MSB first)</div>
</div>
</div>
</div>
</div>
<!-- Symbol, Footprint & 3D Model --> <!-- Symbol, Footprint & 3D View -->
<div class="section">
<h2 class="section-title">Symbol, Footprint & 3D Model</h2>
<div class="grid-2" style="margin-bottom:12px">
<!-- Interactive SymView -->
<div class="card" style="padding:0;overflow:hidden">
<div style="font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-dim);padding:12px 16px 0">Schematic Symbol</div>
<iframe src="WS2812B-2020-symview.html" style="width:100%;height:340px;border:none;display:block" allow="fullscreen"></iframe>
</div>
<!-- Interactive FpView -->
<div class="card" style="padding:0;overflow:hidden">
<div style="font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-dim);padding:12px 16px 0">Footprint (2020)</div>
<iframe src="WS2812B-2020-fpview.html" style="width:100%;height:340px;border:none;display:block" allow="fullscreen"></iframe>
</div>
</div>
<!-- 3D Model -->
<div class="card" style="text-align:center;margin-bottom:12px">
<div style="font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-dim);margin-bottom:12px">3D Model (interactive)</div>
<canvas id="viewer3d" width="600" height="300" style="width:100%;height:300px;border-radius:4px;cursor:grab"></canvas>
<div style="font-size:10px;color:var(--text-dim);margin-top:6px">Click & drag to rotate · Scroll to zoom</div>
</div>
<!-- Download / Export Bar -->
<div class="card" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<span style="font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px">Export:</span>
<a class="dl-btn" href="https://wiki-ufypy5dpx93o.adom.cloud/static/libraries/ws2812b-2020/WS2812B-2020.kicad_sym" download>KiCad Symbol (.kicad_sym)</a>
<a class="dl-btn" href="https://wiki-ufypy5dpx93o.adom.cloud/static/libraries/ws2812b-2020/WS2812B-2020.kicad_mod" download>KiCad Footprint (.kicad_mod)</a>
<a class="dl-btn" href="https://wiki-ufypy5dpx93o.adom.cloud/static/libraries/ws2812b-2020/WS2812B-2020.lbr" download>Fusion 360 (.lbr)</a>
<a class="dl-btn" href="https://wiki-ufypy5dpx93o.adom.cloud/static/libraries/ws2812b-2020/WS2812B-2020.step" download>3D Model (.step)</a>
</div>
</div>
<!-- Documents -->
<div class="section">
<h2 class="section-title">Documents</h2>
<div class="card">
<table class="detail-table">
<tr><td>Datasheet</td><td><a href="https://www.mouser.com/datasheet/2/744/WS2812B_2020-3024288.pdf" target="_blank">WS2812B-2020 Datasheet (PDF) →</a></td></tr>
<tr><td>Series</td><td>WS2812B</td></tr>
<tr><td>Manufacturer</td><td>Worldsemi</td></tr>
</table>
</div>
</div>
<div class="source">
WS2812B-2020 characterization · Data from Worldsemi WS2812B-2020 datasheet · 2026-06-03
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/pmrem/PMREMGenerator.js"></script>
<script>
(function() {
// ==================== RGB Color Mixer ====================
var sliderR = document.getElementById('mix-r');
var sliderG = document.getElementById('mix-g');
var sliderB = document.getElementById('mix-b');
function updateMixer() {
var r = parseInt(sliderR.value);
var g = parseInt(sliderG.value);
var b = parseInt(sliderB.value);
document.getElementById('mix-r-val').textContent = r;
document.getElementById('mix-g-val').textContent = g;
document.getElementById('mix-b-val').textContent = b;
var hex = '#' + r.toString(16).padStart(2,'0').toUpperCase() +
g.toString(16).padStart(2,'0').toUpperCase() +
b.toString(16).padStart(2,'0').toUpperCase();
var grb = '0x' + g.toString(16).padStart(2,'0').toUpperCase() +
r.toString(16).padStart(2,'0').toUpperCase() +
b.toString(16).padStart(2,'0').toUpperCase();
var preview = document.getElementById('mix-preview');
preview.style.backgroundColor = 'rgb(' + r + ',' + g + ',' + b + ')';
preview.style.boxShadow = '0 4px 30px rgba(' + r + ',' + g + ',' + b + ',0.4)';
// Text color: dark on bright backgrounds
var lum = (0.299 * r + 0.587 * g + 0.114 * b);
var textEl = document.getElementById('mix-preview-text');
textEl.style.color = lum > 128 ? '#111' : '#fff';
textEl.textContent = hex;
document.getElementById('mix-hex').textContent = hex;
document.getElementById('mix-grb').textContent = 'GRB: ' + grb;
// Power: each channel draws up to 16mA at 255, linear
var iR = (r / 255) * 16;
var iG = (g / 255) * 16;
var iB = (b / 255) * 16;
var iTotal = iR + iG + iB;
var pTotal = 5.0 * iTotal; // mW
document.getElementById('mix-power').textContent = pTotal.toFixed(1) + ' mW';
document.getElementById('mix-current').textContent = iTotal.toFixed(1) + ' mA total';
// Binary
var binG = g.toString(2).padStart(8, '0');
var binR = r.toString(2).padStart(8, '0');
var binB = b.toString(2).padStart(8, '0');
document.getElementById('mix-binary').textContent = binG + ' ' + binR + ' ' + binB;
}
sliderR.addEventListener('input', updateMixer);
sliderG.addEventListener('input', updateMixer);
sliderB.addEventListener('input', updateMixer);
updateMixer();
// ==================== 3D Viewer — WS2812B-2020 ====================
var canvas = document.getElementById('viewer3d');
if (canvas && window.THREE) {
var scene = new THREE.Scene();
var renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true, logarithmicDepthBuffer: true });
renderer.setSize(canvas.width, canvas.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
renderer.physicallyCorrectLights = true;
canvas.style.background = 'linear-gradient(180deg, #5a6b7e 0%, #2a3340 100%)';
var camera = new THREE.PerspectiveCamera(35, canvas.width / canvas.height, 0.01, 1000);
var pmremGen = new THREE.PMREMGenerator(renderer);
pmremGen.compileEquirectangularShader();
var envScene2 = new THREE.Scene();
envScene2.background = new THREE.Color(0x3a4550);
var el1 = new THREE.DirectionalLight(0xffffff, 2); el1.position.set(1, 3, 2); envScene2.add(el1);
var el2 = new THREE.DirectionalLight(0x8899bb, 1); el2.position.set(-2, 1, -1); envScene2.add(el2);
scene.environment = pmremGen.fromScene(envScene2, 0.04).texture;
scene.add(new THREE.HemisphereLight(0xe6edf3, 0xb3a699, 0.5));
var kl = new THREE.DirectionalLight(0xffffff, 1.5); kl.position.set(2, 5, 3); scene.add(kl);
var fl = new THREE.DirectionalLight(0x8899aa, 0.5); fl.position.set(-3, 2, -2); scene.add(fl);
var S = 1.2;
var bodyW = 2.0, bodyD = 2.0, bodyH = 0.84;
var windowH = 0.2;
// White body
var bodyMat = new THREE.MeshStandardMaterial({ color: 0xe8e8e0, metalness: 0.0, roughness: 0.35 });
var body = new THREE.Mesh(new THREE.BoxGeometry(bodyW*S, (bodyH-windowH)*S, bodyD*S), bodyMat);
body.position.y = (bodyH-windowH)*S/2; scene.add(body);
// Translucent window on top
var windowMat = new THREE.MeshStandardMaterial({
color: 0xffffff, metalness: 0.0, roughness: 0.1,
transparent: true, opacity: 0.4
});
var win = new THREE.Mesh(new THREE.BoxGeometry(bodyW*0.7*S, windowH*S, bodyD*0.7*S), windowMat);
win.position.y = (bodyH - windowH/2)*S; scene.add(win);
// RGB dies visible through window
var rDie = new THREE.Mesh(new THREE.BoxGeometry(0.3*S, 0.05*S, 0.3*S),
new THREE.MeshStandardMaterial({ color: 0xDC143C, emissive: 0xDC143C, emissiveIntensity: 0.8 }));
rDie.position.set(-0.35*S, (bodyH-windowH+0.03)*S, 0); scene.add(rDie);
var gDie = new THREE.Mesh(new THREE.BoxGeometry(0.3*S, 0.05*S, 0.3*S),
new THREE.MeshStandardMaterial({ color: 0x32CD32, emissive: 0x32CD32, emissiveIntensity: 0.8 }));
gDie.position.set(0, (bodyH-windowH+0.03)*S, 0); scene.add(gDie);
var bDie = new THREE.Mesh(new THREE.BoxGeometry(0.3*S, 0.05*S, 0.3*S),
new THREE.MeshStandardMaterial({ color: 0x4169E1, emissive: 0x4169E1, emissiveIntensity: 0.8 }));
bDie.position.set(0.35*S, (bodyH-windowH+0.03)*S, 0); scene.add(bDie);
// 4 pads
var padMat = new THREE.MeshStandardMaterial({ color: 0xc9a04c, metalness: 1.0, roughness: 0.22 });
var padSize = 0.5;
var pOff = 0.55;
[[-pOff, pOff], [pOff, pOff], [-pOff, -pOff], [pOff, -pOff]].forEach(function(pos) {
var pad = new THREE.Mesh(new THREE.BoxGeometry(padSize*S, 0.03*S, padSize*S), padMat);
pad.position.set(pos[0]*S, 0.005*S, pos[1]*S);
scene.add(pad);
});
// PCB
var pcbMat = new THREE.MeshStandardMaterial({ color: 0x0a2a0a, metalness: 0.0, roughness: 0.6 });
var pcb = new THREE.Mesh(new THREE.BoxGeometry(10*S, 0.08*S, 8*S), pcbMat);
pcb.position.y = -0.04*S; scene.add(pcb);
// Grid
var gridMat = new THREE.LineBasicMaterial({ color: 0x0d3d0d, transparent: true, opacity: 0.3 });
for (var i = -8; i <= 8; i++) {
var p1 = [new THREE.Vector3(i*0.4*S, 0.045*S, -4*S), new THREE.Vector3(i*0.4*S, 0.045*S, 4*S)];
scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(p1), gridMat));
var p2 = [new THREE.Vector3(-4*S, 0.045*S, i*0.4*S), new THREE.Vector3(4*S, 0.045*S, i*0.4*S)];
scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(p2), gridMat));
}
var isDragging = false, prevX = 0, prevY = 0;
var theta = Math.PI/4, phi = Math.PI/5.5, radius = 5.0;
var target = new THREE.Vector3(0, 0.5, 0);
function updateCamera() {
camera.position.x = radius * Math.sin(theta) * Math.cos(phi);
camera.position.y = radius * Math.sin(phi) + target.y;
camera.position.z = radius * Math.cos(theta) * Math.cos(phi);
camera.lookAt(target);
}
updateCamera();
canvas.addEventListener('mousedown', function(e) { isDragging = true; prevX = e.clientX; prevY = e.clientY; canvas.style.cursor = 'grabbing'; });
window.addEventListener('mouseup', function() { isDragging = false; canvas.style.cursor = 'grab'; });
canvas.addEventListener('mousemove', function(e) {
if (!isDragging) return;
theta -= (e.clientX - prevX) * 0.008;
phi = Math.max(0.05, Math.min(Math.PI/2.2, phi + (e.clientY - prevY) * 0.008));
prevX = e.clientX; prevY = e.clientY;
updateCamera();
});
canvas.addEventListener('wheel', function(e) {
e.preventDefault();
radius = Math.max(2, Math.min(12, radius + e.deltaY * 0.004));
updateCamera();
}, { passive: false });
function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); }
animate();
}
})();
</script>
</body>
</html>