The demo below is a 2-D analogy of how Gaussian Splatting builds an image. Each splat is a 2-D Gaussian: a colored blob with a position, a radius, and an opacity. They are sorted back-to-front and composited with standard alpha blending — exactly the operation 3DGS performs after projecting 3-D Gaussians onto the screen.
<!-- Canvas + controls for the 2-D Gaussian splat playground -->
<div class="controls">
<button id="addBtn" type="button">Add splat</button>
<button id="clearBtn" type="button" class="ghost">Clear</button>
</div>
<canvas id="canvas" width="480" height="300" aria-label="2D Gaussian splat canvas"></canvas>
<div class="splat-controls" id="splatControls" style="display:none">
<div class="ctrl-row">
<label for="radiusSlider">Radius</label>
<input id="radiusSlider" type="range" min="10" max="120" value="40">
<span id="radiusVal">40</span>
</div>
<div class="ctrl-row">
<label for="opacitySlider">Opacity</label>
<input id="opacitySlider" type="range" min="5" max="100" value="70">
<span id="opacityVal">70%</span>
</div>
<div class="ctrl-row">
<label for="colorPicker">Color</label>
<input id="colorPicker" type="color" value="#4a90d9">
</div>
<p class="formula">Alpha-compositing formula: C = ∑ α<sub>i</sub> c<sub>i</sub> ∏<sub>j<i</sub>(1−α<sub>j</sub>)</p>
</div>
<p class="info" id="info">Click ’Add splat’ to place a Gaussian on the canvas.</p>
/* Styles for the Gaussian splatting demo canvas and controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .4rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:hover { opacity: .88; }
#canvas { display: block; border-radius: 10px; border: 1px solid #cdd9e3;
background: #0a0f14; cursor: crosshair; max-width: 100%; }
.splat-controls { margin-top: .6rem; background: #f4f7fa; border-radius: 8px;
padding: .6rem .8rem; }
.ctrl-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .35rem; }
.ctrl-row label { width: 70px; font-size: .85rem; color: #444; }
.ctrl-row input[type=range] { flex: 1; }
.ctrl-row span { width: 40px; font-size: .85rem; font-variant-numeric: tabular-nums; }
#colorPicker { width: 36px; height: 28px; border: none; padding: 0; cursor: pointer;
border-radius: 4px; }
.formula { font-size: .82rem; color: #555; margin: .3rem 0 0; font-style: italic; }
.info { font-size: .85rem; color: #666; margin: .4rem 0 0; min-height: 1.2em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 2-D Gaussian Splatting demo
// Each splat is a radial gradient (Gaussian approximation) composited with globalAlpha.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const splats = []; // array of splat objects: { x, y, radius, opacity, color, z }
let selected = null; // index of the currently selected splat, or null
const radiusSlider = document.getElementById('radiusSlider');
const opacitySlider = document.getElementById('opacitySlider');
const colorPicker = document.getElementById('colorPicker');
const radiusVal = document.getElementById('radiusVal');
const opacityVal = document.getElementById('opacityVal');
const splatControls = document.getElementById('splatControls');
const infoEl = document.getElementById('info');
// Convert hex color string to { r, g, b } object
function hexToRgb(hex) {
const r = parseInt(hex.slice(1,3),16);
const g = parseInt(hex.slice(3,5),16);
const b = parseInt(hex.slice(5,7),16);
return { r, g, b };
}
// Draw a single splat as a radial gradient on the canvas.
// The gradient goes from full color at the center to transparent at the edge, mimicking a 2-D Gaussian.
function drawSplat(s) {
const { r, g, b } = hexToRgb(s.color);
const grad = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.radius);
grad.addColorStop(0, `rgba(${r},${g},${b},${s.opacity})`);
grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(s.x, s.y, s.radius, 0, Math.PI * 2);
ctx.fill();
}
// Re-render all splats.
// Sort by ascending z (back-to-front) then draw each splat — this is the depth-sorted alpha compositing at the heart of 3DGS.
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const sorted = [...splats].sort((a, b) => a.z - b.z); // back-to-front depth sort
for (const s of sorted) drawSplat(s);
// draw dashed selection ring around the active splat
if (selected !== null) {
const s = splats[selected];
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.beginPath();
ctx.arc(s.x, s.y, s.radius, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
}
updateInfo();
}
function updateInfo() {
if (splats.length === 0) {
infoEl.textContent = 'Click ’Add splat’ to place a Gaussian on the canvas.';
} else if (selected !== null) {
infoEl.textContent = fmt('Splat %selected%%v% of %length% selected — adjust radius, opacity, or color.', { selected: selected, v: 1, length: splats.length });
} else {
infoEl.textContent = fmt('%length% splats — click a splat to select it.', { length: splats.length });
}
}
// Add a new splat at a random position with a random color and size.
function addSplat() {
const margin = 60;
const x = margin + Math.random() * (canvas.width - 2 * margin);
const y = margin + Math.random() * (canvas.height - 2 * margin);
const colors = ['#e63946','#457b9d','#2a9d8f','#e9c46a','#f4a261','#c77dff','#90e0ef'];
const color = colors[Math.floor(Math.random() * colors.length)];
const splat = { x, y, radius: 40 + Math.random() * 30, opacity: 0.4 + Math.random() * 0.4,
color, z: Math.random() };
splats.push(splat);
selected = splats.length - 1;
syncControls();
splatControls.style.display = 'block';
render();
}
// Update the slider/picker UI to reflect the selected splat’s current values.
function syncControls() {
if (selected === null) return;
const s = splats[selected];
radiusSlider.value = Math.round(s.radius);
radiusVal.textContent = Math.round(s.radius);
const pct = Math.round(s.opacity * 100);
opacitySlider.value = pct;
opacityVal.textContent = pct + '%';
colorPicker.value = s.color;
}
document.getElementById('addBtn').addEventListener('click', addSplat);
document.getElementById('clearBtn').addEventListener('click', () => {
splats.length = 0;
selected = null;
splatControls.style.display = 'none';
render();
});
// Click on the canvas to select the topmost splat under the cursor.
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
// Test from front to back; first hit wins
let hit = -1;
for (let i = splats.length - 1; i >= 0; i--) {
const s = splats[i];
const dx = mx - s.x, dy = my - s.y;
if (dx * dx + dy * dy <= s.radius * s.radius) { hit = i; break; }
}
selected = (hit === -1) ? null : hit;
if (selected !== null) { syncControls(); splatControls.style.display = 'block'; }
render();
});
radiusSlider.addEventListener('input', () => {
if (selected === null) return;
splats[selected].radius = +radiusSlider.value;
radiusVal.textContent = radiusSlider.value;
render();
});
opacitySlider.addEventListener('input', () => {
if (selected === null) return;
splats[selected].opacity = +opacitySlider.value / 100;
opacityVal.textContent = opacitySlider.value + '%';
render();
});
colorPicker.addEventListener('input', () => {
if (selected === null) return;
splats[selected].color = colorPicker.value;
render();
});
render();
<!-- Canvas + controls for the 2-D Gaussian splat playground -->
<div class="controls">
<button id="addBtn" type="button">Add splat</button>
<button id="clearBtn" type="button" class="ghost">Clear</button>
</div>
<canvas id="canvas" width="480" height="300" aria-label="2D Gaussian splat canvas"></canvas>
<div class="splat-controls" id="splatControls" style="display:none">
<div class="ctrl-row">
<label for="radiusSlider">Radius</label>
<input id="radiusSlider" type="range" min="10" max="120" value="40">
<span id="radiusVal">40</span>
</div>
<div class="ctrl-row">
<label for="opacitySlider">Opacity</label>
<input id="opacitySlider" type="range" min="5" max="100" value="70">
<span id="opacityVal">70%</span>
</div>
<div class="ctrl-row">
<label for="colorPicker">Color</label>
<input id="colorPicker" type="color" value="#4a90d9">
</div>
<p class="formula">Alpha-compositing formula: C = ∑ α<sub>i</sub> c<sub>i</sub> ∏<sub>j<i</sub>(1−α<sub>j</sub>)</p>
</div>
<p class="info" id="info">Click ’Add splat’ to place a Gaussian on the canvas.</p>
/* Styles for the Gaussian splatting demo canvas and controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .4rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:hover { opacity: .88; }
#canvas { display: block; border-radius: 10px; border: 1px solid #cdd9e3;
background: #0a0f14; cursor: crosshair; max-width: 100%; }
.splat-controls { margin-top: .6rem; background: #f4f7fa; border-radius: 8px;
padding: .6rem .8rem; }
.ctrl-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .35rem; }
.ctrl-row label { width: 70px; font-size: .85rem; color: #444; }
.ctrl-row input[type=range] { flex: 1; }
.ctrl-row span { width: 40px; font-size: .85rem; font-variant-numeric: tabular-nums; }
#colorPicker { width: 36px; height: 28px; border: none; padding: 0; cursor: pointer;
border-radius: 4px; }
.formula { font-size: .82rem; color: #555; margin: .3rem 0 0; font-style: italic; }
.info { font-size: .85rem; color: #666; margin: .4rem 0 0; min-height: 1.2em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 2-D Gaussian Splatting demo
// Each splat is a radial gradient (Gaussian approximation) composited with globalAlpha.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const splats = []; // array of splat objects: { x, y, radius, opacity, color, z }
let selected = null; // index of the currently selected splat, or null
const radiusSlider = document.getElementById('radiusSlider');
const opacitySlider = document.getElementById('opacitySlider');
const colorPicker = document.getElementById('colorPicker');
const radiusVal = document.getElementById('radiusVal');
const opacityVal = document.getElementById('opacityVal');
const splatControls = document.getElementById('splatControls');
const infoEl = document.getElementById('info');
// Convert hex color string to { r, g, b } object
function hexToRgb(hex) {
const r = parseInt(hex.slice(1,3),16);
const g = parseInt(hex.slice(3,5),16);
const b = parseInt(hex.slice(5,7),16);
return { r, g, b };
}
// Draw a single splat as a radial gradient on the canvas.
// The gradient goes from full color at the center to transparent at the edge, mimicking a 2-D Gaussian.
function drawSplat(s) {
const { r, g, b } = hexToRgb(s.color);
const grad = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.radius);
grad.addColorStop(0, `rgba(${r},${g},${b},${s.opacity})`);
grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(s.x, s.y, s.radius, 0, Math.PI * 2);
ctx.fill();
}
// Re-render all splats.
// Sort by ascending z (back-to-front) then draw each splat — this is the depth-sorted alpha compositing at the heart of 3DGS.
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const sorted = [...splats].sort((a, b) => a.z - b.z); // back-to-front depth sort
for (const s of sorted) drawSplat(s);
// draw dashed selection ring around the active splat
if (selected !== null) {
const s = splats[selected];
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.beginPath();
ctx.arc(s.x, s.y, s.radius, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
}
updateInfo();
}
function updateInfo() {
if (splats.length === 0) {
infoEl.textContent = 'Click ’Add splat’ to place a Gaussian on the canvas.';
} else if (selected !== null) {
infoEl.textContent = fmt('Splat %selected%%v% of %length% selected — adjust radius, opacity, or color.', { selected: selected, v: 1, length: splats.length });
} else {
infoEl.textContent = fmt('%length% splats — click a splat to select it.', { length: splats.length });
}
}
// Add a new splat at a random position with a random color and size.
function addSplat() {
const margin = 60;
const x = margin + Math.random() * (canvas.width - 2 * margin);
const y = margin + Math.random() * (canvas.height - 2 * margin);
const colors = ['#e63946','#457b9d','#2a9d8f','#e9c46a','#f4a261','#c77dff','#90e0ef'];
const color = colors[Math.floor(Math.random() * colors.length)];
const splat = { x, y, radius: 40 + Math.random() * 30, opacity: 0.4 + Math.random() * 0.4,
color, z: Math.random() };
splats.push(splat);
selected = splats.length - 1;
syncControls();
splatControls.style.display = 'block';
render();
}
// Update the slider/picker UI to reflect the selected splat’s current values.
function syncControls() {
if (selected === null) return;
const s = splats[selected];
radiusSlider.value = Math.round(s.radius);
radiusVal.textContent = Math.round(s.radius);
const pct = Math.round(s.opacity * 100);
opacitySlider.value = pct;
opacityVal.textContent = pct + '%';
colorPicker.value = s.color;
}
document.getElementById('addBtn').addEventListener('click', addSplat);
document.getElementById('clearBtn').addEventListener('click', () => {
splats.length = 0;
selected = null;
splatControls.style.display = 'none';
render();
});
// Click on the canvas to select the topmost splat under the cursor.
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
// Test from front to back; first hit wins
let hit = -1;
for (let i = splats.length - 1; i >= 0; i--) {
const s = splats[i];
const dx = mx - s.x, dy = my - s.y;
if (dx * dx + dy * dy <= s.radius * s.radius) { hit = i; break; }
}
selected = (hit === -1) ? null : hit;
if (selected !== null) { syncControls(); splatControls.style.display = 'block'; }
render();
});
radiusSlider.addEventListener('input', () => {
if (selected === null) return;
splats[selected].radius = +radiusSlider.value;
radiusVal.textContent = radiusSlider.value;
render();
});
opacitySlider.addEventListener('input', () => {
if (selected === null) return;
splats[selected].opacity = +opacitySlider.value / 100;
opacityVal.textContent = opacitySlider.value + '%';
render();
});
colorPicker.addEventListener('input', () => {
if (selected === null) return;
splats[selected].color = colorPicker.value;
render();
});
render();
Comments
Loading comments...