Creative
PIXEL ART EDITOR
Draw pixel art with pencil, fill, and eraser tools. Export your creation as PNG.
Recent:
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
const canvas = document.getElementById('pixelCanvas');
const ctx = canvas.getContext('2d');
let gridSize = 16, cellSize, currentColor = '#ff2200', currentTool = 'pencil';
let pixels = [], drawing = false;
let history = [], historyIdx = -1;
let recentColors = ['#ff2200','#ffffff','#000000','#00c896','#f5c518'];
const defaultPalette = [
'#000000','#ffffff','#ff2200','#ff6600','#ffcc00','#00c896','#0088ff',
'#8844ff','#ff44aa','#884422','#666666','#aaaaaa','#ff5555','#55ff55',
'#5555ff','#ffff55'
];
function init() {
const maxW = Math.min(500, window.innerWidth - 48);
cellSize = Math.floor(maxW / gridSize);
canvas.width = cellSize * gridSize;
canvas.height = cellSize * gridSize;
pixels = Array.from({ length: gridSize }, () => Array(gridSize).fill(null));
history = []; historyIdx = -1;
saveState();
draw();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Checkerboard background for transparency
for (let y = 0; y < gridSize; y++) {
for (let x = 0; x < gridSize; x++) {
ctx.fillStyle = (x + y) % 2 === 0 ? '#1a1a1a' : '#222222';
ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
if (pixels[y][x]) {
ctx.fillStyle = pixels[y][x];
ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
}
}
}
if (document.getElementById('gridToggle').checked && cellSize > 4) {
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 0.5;
for (let i = 0; i <= gridSize; i++) {
ctx.beginPath(); ctx.moveTo(i * cellSize, 0); ctx.lineTo(i * cellSize, canvas.height); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, i * cellSize); ctx.lineTo(canvas.width, i * cellSize); ctx.stroke();
}
}
}
function getCell(e) {
const rect = canvas.getBoundingClientRect();
const cx = (e.clientX || e.touches[0].clientX) - rect.left;
const cy = (e.clientY || e.touches[0].clientY) - rect.top;
return { x: Math.floor(cx / cellSize), y: Math.floor(cy / cellSize) };
}
function applyTool(x, y) {
if (x < 0 || y < 0 || x >= gridSize || y >= gridSize) return;
if (currentTool === 'pencil') {
pixels[y][x] = currentColor;
addRecent(currentColor);
} else if (currentTool === 'eraser') {
pixels[y][x] = null;
} else if (currentTool === 'fill') {
floodFill(x, y, pixels[y][x], currentColor);
addRecent(currentColor);
} else if (currentTool === 'picker') {
if (pixels[y][x]) {
currentColor = pixels[y][x];
document.getElementById('colorPicker').value = currentColor;
}
}
draw();
}
function floodFill(x, y, target, fill) {
if (target === fill) return;
const stack = [[x, y]];
while (stack.length) {
const [cx, cy] = stack.pop();
if (cx < 0 || cy < 0 || cx >= gridSize || cy >= gridSize) continue;
if (pixels[cy][cx] !== target) continue;
pixels[cy][cx] = fill;
stack.push([cx+1,cy],[cx-1,cy],[cx,cy+1],[cx,cy-1]);
}
}
canvas.addEventListener('mousedown', e => { drawing = true; const c = getCell(e); applyTool(c.x, c.y); });
canvas.addEventListener('mousemove', e => { if (drawing && (currentTool === 'pencil' || currentTool === 'eraser')) { const c = getCell(e); applyTool(c.x, c.y); } });
canvas.addEventListener('mouseup', () => { if (drawing) { drawing = false; saveState(); } });
canvas.addEventListener('mouseleave', () => { if (drawing) { drawing = false; saveState(); } });
canvas.addEventListener('touchstart', e => { e.preventDefault(); drawing = true; const c = getCell(e); applyTool(c.x, c.y); }, { passive: false });
canvas.addEventListener('touchmove', e => { e.preventDefault(); if (drawing && (currentTool === 'pencil' || currentTool === 'eraser')) { const c = getCell(e); applyTool(c.x, c.y); } }, { passive: false });
canvas.addEventListener('touchend', () => { if (drawing) { drawing = false; saveState(); } });
function setTool(t) {
currentTool = t;
document.querySelectorAll('.tool-btn').forEach(b => b.classList.toggle('active', b.dataset.tool === t));
}
function saveState() {
historyIdx++;
history = history.slice(0, historyIdx);
history.push(pixels.map(r => [...r]));
if (history.length > 50) { history.shift(); historyIdx--; }
}
function undo() {
if (historyIdx > 0) { historyIdx--; pixels = history[historyIdx].map(r => [...r]); draw(); }
}
function redo() {
if (historyIdx < history.length - 1) { historyIdx++; pixels = history[historyIdx].map(r => [...r]); draw(); }
}
function clearCanvas() {
pixels = Array.from({ length: gridSize }, () => Array(gridSize).fill(null));
draw(); saveState();
}
function changeGrid(size) {
gridSize = size; init();
}
function exportPNG() {
const scale = Math.max(1, Math.floor(512 / gridSize));
const c = document.createElement('canvas');
c.width = gridSize * scale; c.height = gridSize * scale;
const cx = c.getContext('2d');
for (let y = 0; y < gridSize; y++) {
for (let x = 0; x < gridSize; x++) {
if (pixels[y][x]) {
cx.fillStyle = pixels[y][x];
cx.fillRect(x * scale, y * scale, scale, scale);
}
}
}
const a = document.createElement('a');
a.download = 'pixelart.png';
a.href = c.toDataURL('image/png');
a.click();
}
function addRecent(color) {
recentColors = [color, ...recentColors.filter(c => c !== color)].slice(0, 10);
renderRecent();
}
function renderRecent() {
const wrap = document.getElementById('recentColors');
wrap.innerHTML = recentColors.map(c =>
`<div class="recent-swatch" style="background:${c}" onclick="currentColor='${c}';document.getElementById('colorPicker').value='${c}'"></div>`
).join('');
}
// Palette
const paletteEl = document.getElementById('palette');
defaultPalette.forEach(c => {
const s = document.createElement('div');
s.className = 'palette-swatch';
s.style.background = c;
s.onclick = () => { currentColor = c; document.getElementById('colorPicker').value = c; };
paletteEl.appendChild(s);
});
renderRecent();
init();