PixieR
- 4 Devlogs
- 8 Total hours
🎨 PixieR - Turn any image into pixel art instantly. 100% client-side, zero dependencies, realtime preview with compare slider, eyedropper, and export.
🎨 PixieR - Turn any image into pixel art instantly. 100% client-side, zero dependencies, realtime preview with compare slider, eyedropper, and export.
PixieR Project Devlog — Fixing the Stretching Bug & Other Issues
Cause: syncCmpCanvas() set cmp.style.width='100%' and cmp.style.height='100%' together with objectFit:'contain'. object-fit only works on replaced elements like <img> and <video> — it has no effect on <canvas>, so the canvas was forced to fill the wrapper’s full 100% x 100% box regardless of its actual pixel aspect ratio, stretching the pixel art.
Fix: Compute the display size manually based on the pixel canvas’s own aspect ratio (src.width / src.height), fit it within the wrapper width and a max height of 400px, then explicitly set cmp-canvas and cmp-orig-img width/height in pixels (with horizontal centering via marginLeft). Both layers now share identical dimensions so the slider overlay aligns correctly.
Cause: solo-canvas had a fixed inline max-width:100%;max-height:480px, while syncSoloCanvas() separately set style.width/style.height to width*zoom/height*zoom in px. At high zoom levels this could overflow the frame, and the max-width/max-height constraints distorted the aspect ratio independently on each axis.
Fix: Removed the conflicting inline max-width/max-height. syncSoloCanvas() now computes the zoomed size, then uniformly scales both dimensions down by the same factor if it exceeds the frame’s width or a 480px height cap, preserving aspect ratio.
Cause: pixelCanvas mousemove/click handlers divided mouse coordinates by the zoom slider value to find the pixel under the cursor. This breaks if the canvas’s rendered size ever differs from width * zoom (e.g. after future layout changes), giving wrong color picks.
Fix: Replaced the zoom-based division with a ratio of the canvas’s actual bitmap size to its rendered bounding box size (pixelCanvas.width / rect.width), which is robust to any CSS scaling.
Cause: No resize handler existed for the compare/pixel-only views, so resizing the browser left stale dimensions and a stretched look.
Fix: Added a resize listener that re-runs syncCmpCanvas() or syncSoloCanvas() depending on the active view.
Cause: The star popup markup was hardcoded in the page AND re-inserted via createPopupHTML() using insertAdjacentHTML, creating a duplicate #starPopup risk and unnecessary DOM bloat.
Fix: Removed the redundant insertAdjacentHTML injection since the markup already exists in the page; createPopupHTML() now just guards against duplicates.
Cause: showToast() inside the popup IIFE recreated a whole second #toast element with inline styles if the existing one wasn’t found — but it always exists in the page, making this branch dead and bug-prone (it referenced var(--text, ...) colors that may not exist).
Detailing My Project README.md and adding some detail
Hi Guys this is my project explaination how does the Pixelate work on steps:
Step 1: Shrink (this is where the magic happens)
jsconst small = document.createElement(‘canvas’);
small.width = 64;
small.height = 64;
small.getContext(‘2d’).drawImage(srcImg, 0, 0, 64, 64);
Imagine your photo is 1920×1080 pixels. You squish it down into a 64×64 canvas.
To fit a million pixrls into 64×64, the browser has to merge groups of pixels into one. It looks at a chunk of the original image (say, a 30×17 block) and blends them all into a single average color.
Result: you now have a real, tiny image — 64×64 actual pixels, each one a blended color from a chunk of the original.
Step 2: Blow it bacl up (with no smoothing)
jscanvas.style.width = ‘384px’; // 64 × 6
canvas.style.height = ‘384px’;
cssimage-rendering: pixelated;
Now you take that tiny 64×64 image and stretch it to be way bigger on screen.
Normally, browsers smooth out stretched images (blurry). The pixelated setting turns smoothing off. So each of the 64×64 pixels just gets copied into a big solid square — no blending, no blur.
Result: big, crisp, blocky squares = the pixel art look.
Putting It Together
big detailed photo
↓ (squish down — merges pixels, loses detail)
tiny 64×64 image
↓ (stretch up — no smoothing, just copy each pixel bigger)
big blocky pixel art
The squish is what creates the pixel art (real data, real file).
The stretch is just so you can see it clearly on screen — it adds zero new information.
Why the Download File is Small
When you hit download, it saves the tiny 64×64 image (from Step 1) — not the big stretched version. The “blow it back up” part was only for your screen.
And this done in client side, we dont collect your image or saving ur image
DEVLOG :
Project: PixieR
Version: 2.0.0
Type: Web App (100% client-side)
Stack: Vanilla JS + Canvas API + CSS
License: MIT
Status: Beta
[+] Built:
- Basic HTML structure with card-based UI
- File upload with drag & drop support
- Canvas setup for pixelation using Canvas API
- Dark/light theme toggle with CSS custom properties
- Interactive grid background with cursor glow effect
[+] Pixelation Engine:
- doPixelate() function that downsamples images
- Three scale modes: Fit, Stretch, Crop
- Realtime preview with 120ms debounce
- Custom grid size input (4×4 to 1024×1024)
- Preset chips: 16, 32, 64, 128, 256
- Linked dimensions toggle (🔗 / 🔓)
[+] How pixelation works:
Original Image
↓ drawImage(src, 0, 0, pixelW, pixelH)
Tiny Canvas (W×H pixels)
↓ CSS image-rendering: pixelated
Final display (zoomed with crisp edges)
[+] Core Code Snippet:
const small = document.createElement('canvas');
small.width = sw; small.height = sh;
const sc = small.getContext('2d');
sc.drawImage(srcImg, 0, 0, sw, sh);
out.getContext('2d').drawImage(small, 0, 0);
[+] Key Decisions:
- No frameworks → keep it lightweight (~18KB)
- Single HTML file → easy to share, no build step
- Canvas API → native browser support, fast
[+] Challenges & Solutions:
Challenge: Crop mode required calculating source coordinates
Solution: Used aspect ratio comparison to determine sx, sy, sW, sH
Challenge: Maintaining aspect ratio in Fit mode
Solution: Compare pw/ph vs srcRatio to determine scaling axis
Challenge: Making chips highlight correctly on value change
Solution: setChipActive() toggles .on class based on value
[+] View Modes:
- Side by Side (split-view)
- Compare Slider (drag to compare original vs pixelated)
- Pixel Only (fullscreen pixel art display)
[+] Compare Slider Implementation:
/* Clip the pixelated layer */
#compare-pixel {
clip-path: inset(0 50% 0 0);
}
Drag updates percentage → changes clip-path value dynamically.
Supports both mouse and touch events for mobile.
[+] Eyedropper Tool:
- Hover over any pixel to see hex color
- Click to copy hex code to clipboard
- Tooltip follows cursor with color preview
[+] Eyedropper Logic:
pixelCanvas.addEventListener('mousemove', (e) => {
const cx = Math.floor((e.clientX - rect.left) / zoom);
const cy = Math.floor((e.clientY - rect.top) / zoom);
const px = ctx.getImageData(cx, cy, 1, 1).data;
const hex = '#' + px[0].toString(16) + ...;
tip.innerHTML = `${hex} (${cx},${cy})`;
});
[+] Export Features:
- Download as PNG, JPG, or WebP
- Automatic filename: pixier_{W}x{H}.ext
[+] Zoom Slider:
- Range: 1× to 20×
- CSS-based scaling, doesn’t re-render
- Maintains crisp pixelated edges
[+] The “Pixelate!” Button Experience:
- Disabled until image loaded
- 5-second “processing” animation (UX theater)
- Controls unlock after first conversion
- Shimmer effect on completion
- After first use: realtime updates (120ms debounce)