Skip to content

Instantly share code, notes, and snippets.

@pleabargain
Created March 11, 2026 09:43
Show Gist options
  • Select an option

  • Save pleabargain/41ba97f506a39770896c21961f67eec7 to your computer and use it in GitHub Desktop.

Select an option

Save pleabargain/41ba97f506a39770896c21961f67eec7 to your computer and use it in GitHub Desktop.
html pixelator for fun
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Image Pixelation Slider</title>
<style>
:root {
--bg: #101319;
--panel: #181c25;
--accent: #5b8aff;
--accent-soft: rgba(91, 138, 255, 0.2);
--text: #f5f7ff;
--text-muted: #a2a9c3;
--border: #262b3a;
}
* {
box-sizing: border-box;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top, #1b2540 0, #05060a 50%, #020308 100%);
color: var(--text);
display: flex;
align-items: stretch;
justify-content: center;
padding: 24px;
}
.app {
width: 100%;
max-width: 1100px;
background: linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01));
border-radius: 18px;
border: 1px solid rgba(255,255,255,0.05);
box-shadow:
0 30px 80px rgba(0,0,0,0.65),
0 0 0 1px rgba(255,255,255,0.02);
padding: 20px 22px;
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(260px, 2fr);
gap: 20px;
backdrop-filter: blur(20px) saturate(130%);
}
@media (max-width: 800px) {
.app {
grid-template-columns: 1fr;
}
}
.left, .right {
display: flex;
flex-direction: column;
gap: 16px;
}
.title {
font-size: 20px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--text-muted);
}
.subtitle {
font-size: 13px;
color: var(--text-muted);
}
.canvas-wrap {
position: relative;
border-radius: 14px;
border: 1px solid var(--border);
background: radial-gradient(circle at top left, #242b3d 0, #10131b 45%, #05060a 100%);
flex: 1;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
min-height: 260px;
}
.canvas-wrap.dropping::before {
content: "Drop image here to pixelate";
position: absolute;
inset: 0;
border-radius: inherit;
border: 2px dashed rgba(144,180,255,0.9);
background: rgba(10,16,35,0.8);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 13px;
letter-spacing: 0.12em;
text-transform: uppercase;
z-index: 2;
}
canvas {
max-width: 100%;
max-height: 100%;
background: #05060a;
}
.canvas-placeholder {
text-align: center;
color: var(--text-muted);
pointer-events: none;
padding: 24px;
}
.canvas-placeholder span {
display: inline-block;
margin-bottom: 8px;
padding: 6px 10px;
border-radius: 999px;
font-size: 11px;
letter-spacing: 0.15em;
text-transform: uppercase;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(7,11,24,0.9);
}
.control-panel {
background: radial-gradient(circle at top left, rgba(91,138,255,0.16), rgba(15,18,30,0.95));
border-radius: 14px;
border: 1px solid rgba(91,138,255,0.28);
padding: 16px 15px 14px;
display: flex;
flex-direction: column;
gap: 12px;
position: relative;
overflow: hidden;
}
.control-panel::before {
content: "";
position: absolute;
inset: -40%;
background:
radial-gradient(circle at 0% 0%, rgba(144,180,255,0.2) 0, transparent 60%),
radial-gradient(circle at 100% 0%, rgba(120,105,255,0.18) 0, transparent 55%);
opacity: 0.65;
mix-blend-mode: soft-light;
pointer-events: none;
}
.control-header {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
}
.control-header h2 {
margin: 0;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.badge {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.16em;
padding: 3px 9px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.25);
background: rgba(7,11,24,0.7);
color: var(--text-muted);
}
.field {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 4px;
}
label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--text-muted);
}
.file-input-row {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
input[type="file"] {
font-size: 11px;
color: var(--text-muted);
}
.slider-row {
display: flex;
gap: 10px;
align-items: center;
}
input[type="range"] {
flex: 1;
-webkit-appearance: none;
height: 5px;
border-radius: 999px;
background: rgba(8,11,22,0.7);
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 999px;
background: var(--accent);
box-shadow: 0 0 0 4px var(--accent-soft);
cursor: pointer;
border: 1px solid rgba(255,255,255,0.6);
}
input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 999px;
background: var(--accent);
border: 1px solid rgba(255,255,255,0.6);
box-shadow: 0 0 0 4px var(--accent-soft);
cursor: pointer;
}
.slider-value {
min-width: 58px;
text-align: right;
font-variant-numeric: tabular-nums;
font-size: 12px;
color: var(--text-muted);
}
.hint {
font-size: 11px;
color: var(--text-muted);
line-height: 1.35;
}
.hint strong {
color: var(--text);
font-weight: 500;
}
.button-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
button {
position: relative;
z-index: 1;
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(4,7,18,0.9);
color: var(--text);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.16em;
padding: 7px 14px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: background 120ms ease, transform 80ms ease, box-shadow 120ms ease, border-color 120ms ease;
}
button:hover {
background: rgba(19,25,54,0.95);
border-color: rgba(144,180,255,0.6);
box-shadow: 0 0 0 1px rgba(144,180,255,0.6), 0 0 18px rgba(144,180,255,0.3);
transform: translateY(-1px);
}
button:active {
transform: translateY(0);
box-shadow: none;
}
.button-primary {
background: linear-gradient(120deg, #5b8aff, #8a7dff);
border-color: transparent;
box-shadow: 0 10px 30px rgba(91,138,255,0.4);
}
.button-primary:hover {
box-shadow: 0 12px 38px rgba(91,138,255,0.6);
}
.testing-panel {
position: relative;
z-index: 1;
margin-top: 4px;
padding-top: 8px;
border-top: 1px dashed rgba(200,210,255,0.25);
display: flex;
flex-direction: column;
gap: 6px;
}
.testing-panel-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--text-muted);
}
.testing-note {
font-size: 11px;
color: var(--text-muted);
}
.status-line {
font-size: 11px;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
</style>
</head>
<body>
<div class="app">
<div class="left">
<div>
<div class="title">Pixelation Studio</div>
<div class="subtitle">Load any image and scrub the slider to control how blocky it becomes.</div>
</div>
<div class="canvas-wrap">
<canvas id="canvas"></canvas>
<div id="placeholder" class="canvas-placeholder">
<span>Step 1</span>
<div>Choose an image on the right to begin, then drag the slider to adjust pixel size.</div>
</div>
</div>
</div>
<div class="right">
<div class="control-panel">
<div class="control-header">
<h2>Controls</h2>
<div class="badge">HTML + Canvas</div>
</div>
<!-- File input -->
<div class="field">
<label for="fileInput">Source Image</label>
<div class="file-input-row">
<input type="file" id="fileInput" accept="image/*" />
<button id="loadSampleBtn" type="button">Load Sample</button>
</div>
<div class="hint">
<strong>Tip:</strong> Drag &amp; drop an image onto the large area on the left, or choose a file here.
</div>
</div>
<!-- Quick-load known files in same folder -->
<div class="field">
<label>Quick load from this folder</label>
<div class="button-row">
<button id="loadDgdMainBtn" type="button">Load DGD (WhatsApp)</button>
<button id="loadDgdAltBtn" type="button">Load DGD (Untitled)</button>
</div>
<div class="hint">
These buttons try to load images that live next to <strong>pixelate.html</strong>. They work when the
filenames match and you open the file directly from that folder.
</div>
</div>
<!-- Slider -->
<div class="field">
<label for="pixelSlider">Pixel size</label>
<div class="slider-row">
<input
id="pixelSlider"
type="range"
min="1"
max="80"
value="8"
/>
<div class="slider-value">
<span id="pixelValue">8</span> px
</div>
</div>
<div class="hint">
1 px = original detail, larger values = stronger pixelation. Changes apply live.
</div>
</div>
<!-- Actions -->
<div class="field">
<label>Actions</label>
<div class="button-row">
<button id="resetBtn" type="button">Reset</button>
<button id="downloadBtn" type="button" class="button-primary">
Export PNG
</button>
</div>
<div class="status-line" id="statusLine">
Waiting for image…
</div>
</div>
<!-- Testing / debug block -->
<div class="testing-panel">
<div class="testing-panel-title">Testing presets</div>
<div class="button-row">
<button data-preset="4" type="button">Soft (4 px)</button>
<button data-preset="12" type="button">Medium (12 px)</button>
<button data-preset="30" type="button">Heavy (30 px)</button>
</div>
<div class="testing-note">
These buttons are a small testing helper: they snap the slider to known values so you can quickly eyeball whether the pixelation behaves as expected.
</div>
</div>
</div>
</div>
</div>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const canvasWrap = document.querySelector(".canvas-wrap");
const fileInput = document.getElementById("fileInput");
const pixelSlider = document.getElementById("pixelSlider");
const pixelValue = document.getElementById("pixelValue");
const placeholder = document.getElementById("placeholder");
const statusLine = document.getElementById("statusLine");
const resetBtn = document.getElementById("resetBtn");
const downloadBtn = document.getElementById("downloadBtn");
const loadSampleBtn = document.getElementById("loadSampleBtn");
const loadDgdMainBtn = document.getElementById("loadDgdMainBtn");
const loadDgdAltBtn = document.getElementById("loadDgdAltBtn");
const presetButtons = document.querySelectorAll(".testing-panel [data-preset]");
let originalImage = null; // HTMLImageElement
let imageLoaded = false;
function setStatus(message) {
statusLine.textContent = message;
}
function fitCanvasToImage(img) {
const maxWidth = canvas.parentElement.clientWidth * window.devicePixelRatio;
const maxHeight = canvas.parentElement.clientHeight * window.devicePixelRatio;
let { width, height } = img;
const aspect = width / height;
if (width > maxWidth) {
width = maxWidth;
height = width / aspect;
}
if (height > maxHeight) {
height = maxHeight;
width = height * aspect;
}
const displayScale = 1 / window.devicePixelRatio;
canvas.width = Math.round(width);
canvas.height = Math.round(height);
canvas.style.width = Math.round(width * displayScale) + "px";
canvas.style.height = Math.round(height * displayScale) + "px";
}
function renderPixelated(pixelSize) {
if (!imageLoaded || !originalImage) return;
const w = canvas.width;
const h = canvas.height;
if (w === 0 || h === 0) return;
const blockSize = Math.max(1, Math.floor(pixelSize));
const scaledW = Math.max(1, Math.floor(w / blockSize));
const scaledH = Math.max(1, Math.floor(h / blockSize));
const offscreen = document.createElement("canvas");
offscreen.width = scaledW;
offscreen.height = scaledH;
const offctx = offscreen.getContext("2d");
offctx.drawImage(originalImage, 0, 0, scaledW, scaledH);
ctx.imageSmoothingEnabled = false;
ctx.clearRect(0, 0, w, h);
ctx.drawImage(offscreen, 0, 0, scaledW, scaledH, 0, 0, w, h);
placeholder.style.display = "none";
setStatus(`Image loaded · pixel size: ${blockSize}px · ${w}×${h} canvas`);
}
function handleSliderChange() {
const value = Number(pixelSlider.value);
pixelValue.textContent = value.toString();
if (imageLoaded) {
renderPixelated(value);
}
}
function loadImageFromFile(file) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
originalImage = img;
imageLoaded = true;
fitCanvasToImage(img);
handleSliderChange();
};
img.onerror = () => {
setStatus("Failed to load image. Please try another file.");
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
setStatus("Loading image…");
}
function loadImageFromUrl(url, labelForStatus) {
const img = new Image();
img.onload = () => {
originalImage = img;
imageLoaded = true;
fitCanvasToImage(img);
handleSliderChange();
setStatus(`Loaded ${labelForStatus || "image"} from same folder as pixelate.html.`);
};
img.onerror = () => {
setStatus(`Could not load ${labelForStatus || "image"} from relative path. Check filename and location.`);
};
img.src = url;
setStatus(`Loading ${labelForStatus || "image"}…`);
}
fileInput.addEventListener("change", () => {
const file = fileInput.files && fileInput.files[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
setStatus("Selected file is not an image.");
return;
}
loadImageFromFile(file);
});
// Drag & drop onto canvas area
["dragenter", "dragover"].forEach((evtName) => {
canvasWrap.addEventListener(evtName, (e) => {
e.preventDefault();
e.stopPropagation();
canvasWrap.classList.add("dropping");
setStatus("Drop image file to load…");
});
});
["dragleave", "drop"].forEach((evtName) => {
canvasWrap.addEventListener(evtName, (e) => {
e.preventDefault();
e.stopPropagation();
canvasWrap.classList.remove("dropping");
});
});
canvasWrap.addEventListener("drop", (e) => {
const dt = e.dataTransfer;
if (!dt) return;
const files = dt.files;
if (!files || files.length === 0) {
setStatus("No file found in drop.");
return;
}
const file = files[0];
if (!file.type.startsWith("image/")) {
setStatus("Dropped file is not an image.");
return;
}
loadImageFromFile(file);
});
pixelSlider.addEventListener("input", handleSliderChange);
resetBtn.addEventListener("click", () => {
pixelSlider.value = 8;
handleSliderChange();
if (!imageLoaded) {
setStatus("Reset slider. Waiting for image…");
} else {
setStatus("Reset slider to 8px.");
}
});
downloadBtn.addEventListener("click", () => {
if (!imageLoaded) {
setStatus("Load an image before exporting.");
return;
}
const link = document.createElement("a");
link.download = "pixelated.png";
link.href = canvas.toDataURL("image/png");
link.click();
setStatus("Exported current pixelated view as PNG.");
});
presetButtons.forEach((btn) => {
btn.addEventListener("click", () => {
const value = Number(btn.getAttribute("data-preset"));
pixelSlider.value = value;
handleSliderChange();
setStatus(`Preset applied: ${value}px block size.`);
});
});
// Quick-load buttons for specific images in same directory as pixelate.html
if (loadDgdMainBtn) {
loadDgdMainBtn.addEventListener("click", () => {
loadImageFromUrl("DGDWhatsApp Image 2025-11-11 at 11.58.35 AM.png", "DGDWhatsApp image");
});
}
if (loadDgdAltBtn) {
loadDgdAltBtn.addEventListener("click", () => {
loadImageFromUrl("DGD doing his thing Untitled.png", "DGD Untitled image");
});
}
loadSampleBtn.addEventListener("click", () => {
const sample = new Image();
const gradientSize = 400;
const off = document.createElement("canvas");
off.width = gradientSize;
off.height = gradientSize;
const gctx = off.getContext("2d");
const g = gctx.createLinearGradient(0, 0, gradientSize, gradientSize);
g.addColorStop(0, "#f97316");
g.addColorStop(0.3, "#eab308");
g.addColorStop(0.6, "#22c55e");
g.addColorStop(1, "#0ea5e9");
gctx.fillStyle = g;
gctx.fillRect(0, 0, gradientSize, gradientSize);
sample.onload = () => {
originalImage = sample;
imageLoaded = true;
fitCanvasToImage(sample);
handleSliderChange();
setStatus("Sample gradient loaded. Adjust the slider or presets.");
};
sample.src = off.toDataURL("image/png");
});
pixelValue.textContent = pixelSlider.value;
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment