Giraffes Coloring Pages

Animals › Giraffes

Stretch into fun with 40 free printable giraffe coloring pages. From tall adult giraffes grazing among acacia trees to playful baby giraffes with their mothers, stylized cartoon giraffes, and scenic African savanna landscapes — this collection celebrates nature’s tallest mammal.

' ); pwin.document.close(); }); }// ============================================================ // ZOOM // ============================================================ function setZoom(level, anchorX, anchorY) { var oldZoom = zoomLevel; zoomLevel = Math.max(minZoom, Math.min(maxZoom, Math.round(level * 100) / 100)); if (zoomLevel === oldZoom) return; var newW = Math.round(baseDisplayW * zoomLevel); var newH = Math.round(baseDisplayH * zoomLevel); canvas.style.width = newW + 'px'; canvas.style.height = newH + 'px'; zoomLabel.textContent = Math.round(zoomLevel * 100) + '%'; if (typeof anchorX === 'number' && typeof anchorY === 'number') { var ratio = zoomLevel / oldZoom; wrap.scrollLeft = (wrap.scrollLeft + anchorX) * ratio - anchorX; wrap.scrollTop = (wrap.scrollTop + anchorY) * ratio - anchorY; } if (hint) { if (zoomLevel > 1) { hint.textContent = 'Hold Space to pan \u00b7 Scroll to zoom'; hint.classList.add('zoomed'); } else { hint.textContent = 'Hold Space to pan'; hint.classList.remove('zoomed'); } } } function calcBaseDisplay() { var maxW = wrap.clientWidth - 32; var maxH = wrap.clientHeight - 32; var scale = Math.min(maxW / canvas.width, maxH / canvas.height, 1); baseDisplayW = Math.round(canvas.width * scale); baseDisplayH = Math.round(canvas.height * scale); } wrap.addEventListener('wheel', function(e) { if (!ct.classList.contains('active')) return; e.preventDefault(); var delta = e.deltaY > 0 ? -0.15 : 0.15; var rect = wrap.getBoundingClientRect(); setZoom(zoomLevel + delta, e.clientX - rect.left, e.clientY - rect.top); }, { passive: false }); wrap.addEventListener('keydown', function(e) { if (e.key === ' ') e.preventDefault(); }, { passive: false });// ============================================================ // TOUCH GESTURES // ============================================================ function applyPendingScroll() { rafScheduled = false; if (pendingScrollX !== null) { wrap.scrollLeft = pendingScrollX; pendingScrollX = null; } if (pendingScrollY !== null) { wrap.scrollTop = pendingScrollY; pendingScrollY = null; } } function scheduleScroll(x, y) { pendingScrollX = x; pendingScrollY = y; if (!rafScheduled) { rafScheduled = true; requestAnimationFrame(applyPendingScroll); } } function cancelActiveStroke() { if (isDrawing) { isDrawing = false; ctx.globalAlpha = 1.0; } if (isPanning) { isPanning = false; wrap.classList.remove('panning-active'); } } wrap.addEventListener('touchstart', function(e) { if (e.touches.length === 2) { cancelActiveStroke(); touchMode = 'gesture'; lastPinchDist = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); lastPinchMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2; lastPinchMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2; } }, { passive: true }); wrap.addEventListener('touchmove', function(e) { if (e.touches.length === 2 && touchMode === 'gesture') { e.preventDefault(); var dist = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); var midX = (e.touches[0].clientX + e.touches[1].clientX) / 2; var midY = (e.touches[0].clientY + e.touches[1].clientY) / 2; var rect = wrap.getBoundingClientRect(); var panDX = midX - lastPinchMidX; var panDY = midY - lastPinchMidY; var newSL = wrap.scrollLeft - panDX; var newST = wrap.scrollTop - panDY; if (lastPinchDist > 0) { var scale = dist / lastPinchDist; if (Math.abs(scale - 1) > 0.01) { wrap.scrollLeft = newSL; wrap.scrollTop = newST; setZoom(zoomLevel * scale, midX - rect.left, midY - rect.top); } else { scheduleScroll(newSL, newST); } } else { scheduleScroll(newSL, newST); } lastPinchDist = dist; lastPinchMidX = midX; lastPinchMidY = midY; } }, { passive: false }); wrap.addEventListener('touchend', function(e) { if (touchMode === 'gesture' && e.touches.length < 2) { touchMode = 'none'; lastPinchDist = 0; } }, { passive: true }); wrap.addEventListener('touchcancel', function(e) { if (e.touches.length < 2) { touchMode = 'none'; lastPinchDist = 0; } }, { passive: true });// ============================================================ // PAN // ============================================================ function startPan(e) { isPanning = true; panStartX = (typeof e.clientX === 'number') ? e.clientX : (e.touches ? e.touches[0].clientX : 0); panStartY = (typeof e.clientY === 'number') ? e.clientY : (e.touches ? e.touches[0].clientY : 0); panScrollX = wrap.scrollLeft; panScrollY = wrap.scrollTop; wrap.classList.add('panning-active'); } function doPan(e) { if (!isPanning) return; var x = (typeof e.clientX === 'number') ? e.clientX : (e.touches ? e.touches[0].clientX : 0); var y = (typeof e.clientY === 'number') ? e.clientY : (e.touches ? e.touches[0].clientY : 0); scheduleScroll(panScrollX - (x - panStartX), panScrollY - (y - panStartY)); } function endPan() { isPanning = false; wrap.classList.remove('panning-active'); } canvas.addEventListener('mousedown', function(e) { if (e.button === 1) { e.preventDefault(); startPan(e); } });// ============================================================ // CURSOR // ============================================================ function updateCursor(e) { if (!ct.classList.contains('active')) return; cursorEl.style.left = (e.clientX || 0) + 'px'; cursorEl.style.top = (e.clientY || 0) + 'px'; var rect = canvas.getBoundingClientRect(); var displaySize = brushSize; if (rect.width > 0 && canvas.width > 0) { displaySize = brushSize * (rect.width / canvas.width); } displaySize = Math.max(displaySize, 6); cursorEl.style.width = displaySize + 'px'; cursorEl.style.height = displaySize + 'px'; } function updateCursorStyle() { cursorEl.className = 'cpg-ct-cursor'; if (inPanMode()) { cursorEl.classList.add('pan'); canvas.style.cursor = 'grab'; wrap.classList.add('panning'); } else if (currentTool === 'fill') { cursorEl.classList.add('fill'); canvas.style.cursor = 'cell'; wrap.classList.remove('panning'); } else { cursorEl.classList.add(currentTool); canvas.style.cursor = 'none'; wrap.classList.remove('panning'); } if (currentTool === 'eraser') { cursorEl.style.borderColor = 'rgba(255,0,0,.4)'; } else { cursorEl.style.borderColor = currentColor.toUpperCase() === '#FFFFFF' ? 'rgba(0,0,0,.3)' : currentColor; } } document.addEventListener('mousemove', function(e) { updateCursor(e); if (isPanning) doPan(e); }); document.addEventListener('mouseup', function(e) { if (isPanning) endPan(); }); canvas.addEventListener('mouseenter', function() { if (!inPanMode() && currentTool !== 'fill') cursorEl.style.display = 'block'; }); canvas.addEventListener('mouseleave', function() { cursorEl.style.display = 'none'; });// ============================================================ // SIZE PREVIEW // ============================================================ var sizeSlider = ct.querySelector('.cpg-ct-size-slider'); var sizePreview = ct.querySelector('.cpg-ct-size-preview'); function updateSizePreview() { var px = Math.max(brushSize, 4); px = Math.min(px, 40); sizePreview.style.width = px + 'px'; sizePreview.style.height = px + 'px'; sizePreview.style.background = currentColor; } sizeSlider.addEventListener('input', function() { brushSize = parseInt(this.value); updateSizePreview(); });// ============================================================ // MOBILE HINT // ============================================================ function maybeShowMobileHint() { if (!isTouchDevice) return; var seen = false; try { seen = window.localStorage && localStorage.getItem(HINT_KEY); } catch(e) {} if (seen) return; mobileHint.classList.add('show'); } mobileHintOk.addEventListener('click', function() { mobileHint.classList.remove('show'); try { localStorage.setItem(HINT_KEY, '1'); } catch(e) {} });// ============================================================ // COLOR SELECTION // ============================================================ function setActiveColorButton(targetBtn) { ct.querySelectorAll('.cpg-ct-color').forEach(function(b) { b.classList.remove('active'); }); if (targetBtn) targetBtn.classList.add('active'); } function selectColor(hex, sourceBtn) { currentColor = hex; setActiveColorButton(sourceBtn || null); updateCursorStyle(); updateSizePreview(); } paletteEl.addEventListener('click', function(e) { var btn = e.target.closest('.cpg-ct-color'); if (!btn || !paletteEl.contains(btn)) return; var color = btn.getAttribute('data-color'); if (color) selectColor(color, btn); });// ============================================================ // CUSTOM COLOR PICKER // ============================================================ var pickerModal = document.getElementById('cpg-ct-picker-modal'); var pickerBtn = document.getElementById('cpg-ct-picker-btn'); var pickerSv = document.getElementById('cpg-ct-picker-sv'); var pickerSvMark = document.getElementById('cpg-ct-picker-sv-marker'); var pickerHue = document.getElementById('cpg-ct-picker-hue'); var pickerHueMark = document.getElementById('cpg-ct-picker-hue-marker'); var pickerPreview = document.getElementById('cpg-ct-picker-preview'); var pickerHex = document.getElementById('cpg-ct-picker-hex'); var pickerClose = document.getElementById('cpg-ct-picker-close'); var pickerOk = document.getElementById('cpg-ct-picker-ok');var pH = 0, pS = 1, pV = 1;function hsvToRgb(h, s, v) { var c = v * s; var hp = (h / 60) % 6; var x = c * (1 - Math.abs(hp % 2 - 1)); var m = v - c; var r = 0, g = 0, b = 0; if (hp >= 0 && hp < 1) { r = c; g = x; } else if (hp < 2) { r = x; g = c; } else if (hp < 3) { g = c; b = x; } else if (hp < 4) { g = x; b = c; } else if (hp < 5) { r = x; b = c; } else { r = c; b = x; } return { r: Math.round((r + m) * 255), g: Math.round((g + m) * 255), b: Math.round((b + m) * 255) }; } function rgbToHsv(r, g, b) { r /= 255; g /= 255; b /= 255; var max = Math.max(r, g, b), min = Math.min(r, g, b); var d = max - min; var h = 0, s = max === 0 ? 0 : d / max, v = max; if (d !== 0) { if (max === r) h = ((g - b) / d) % 6; else if (max === g) h = (b - r) / d + 2; else h = (r - g) / d + 4; h *= 60; if (h < 0) h += 360; } return { h: h, s: s, v: v }; } function rgbToHex(r, g, b) { return '#' + [r, g, b].map(function(c) { var s = c.toString(16); return s.length === 1 ? '0' + s : s; }).join('').toUpperCase(); } function hexToRgb(hex) { hex = hex.replace('#',''); if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; if (!/^[0-9A-Fa-f]{6}$/.test(hex)) return null; var n = parseInt(hex, 16); return { r:(n>>16)&255, g:(n>>8)&255, b:n&255 }; } function getPickerHex() { var rgb = hsvToRgb(pH, pS, pV); return rgbToHex(rgb.r, rgb.g, rgb.b); } function refreshPickerUI() { pickerSv.style.background = 'linear-gradient(to top, #000, transparent),' + 'linear-gradient(to right, #fff, hsl(' + pH + ', 100%, 50%))'; pickerSvMark.style.left = (pS * 100) + '%'; pickerSvMark.style.top = ((1 - pV) * 100) + '%'; pickerHueMark.style.left = ((pH / 360) * 100) + '%'; var hex = getPickerHex(); pickerPreview.style.background = hex; pickerHex.value = hex.replace('#', ''); pickerHex.classList.remove('invalid'); } function openPicker() { var rgb = hexToRgb(currentColor); if (rgb) { var hsv = rgbToHsv(rgb.r, rgb.g, rgb.b); pH = hsv.h; pS = hsv.s; pV = hsv.v; } refreshPickerUI(); pickerModal.classList.add('show'); pickerModal.setAttribute('aria-hidden', 'false'); } function closePicker() { pickerModal.classList.remove('show'); pickerModal.setAttribute('aria-hidden', 'true'); } pickerBtn.addEventListener('click', openPicker); pickerClose.addEventListener('click', closePicker); pickerModal.addEventListener('click', function(e) { if (e.target === pickerModal) closePicker(); }); pickerOk.addEventListener('click', function() { addCustomColor(getPickerHex()); closePicker(); });function getEventPoint(e) { if (e.touches && e.touches.length > 0) return { x: e.touches[0].clientX, y: e.touches[0].clientY }; return { x: e.clientX, y: e.clientY }; } function attachDrag(el, onMove) { var dragging = false; function start(e) { dragging = true; if (e.cancelable) e.preventDefault(); onMove(e, el); } function move(e) { if (!dragging) return; if (e.cancelable) e.preventDefault(); onMove(e, el); } function end() { dragging = false; } el.addEventListener('mousedown', start); document.addEventListener('mousemove', move); document.addEventListener('mouseup', end); el.addEventListener('touchstart', start, { passive: false }); document.addEventListener('touchmove', move, { passive: false }); document.addEventListener('touchend', end); document.addEventListener('touchcancel', end); } attachDrag(pickerSv, function(e, el) { var rect = el.getBoundingClientRect(); var p = getEventPoint(e); var x = Math.max(0, Math.min(rect.width, p.x - rect.left)); var y = Math.max(0, Math.min(rect.height, p.y - rect.top)); pS = x / rect.width; pV = 1 - (y / rect.height); refreshPickerUI(); }); attachDrag(pickerHue, function(e, el) { var rect = el.getBoundingClientRect(); var p = getEventPoint(e); var x = Math.max(0, Math.min(rect.width, p.x - rect.left)); pH = (x / rect.width) * 360; if (pH >= 360) pH = 359.99; refreshPickerUI(); }); pickerHex.addEventListener('input', function() { var v = this.value.trim().replace('#', ''); var rgb = hexToRgb(v); if (rgb) { var hsv = rgbToHsv(rgb.r, rgb.g, rgb.b); if (hsv.s > 0) pH = hsv.h; pS = hsv.s; pV = hsv.v; pickerSv.style.background = 'linear-gradient(to top, #000, transparent),' + 'linear-gradient(to right, #fff, hsl(' + pH + ', 100%, 50%))'; pickerSvMark.style.left = (pS * 100) + '%'; pickerSvMark.style.top = ((1 - pV) * 100) + '%'; pickerHueMark.style.left = ((pH / 360) * 100) + '%'; pickerPreview.style.background = '#' + v.toUpperCase(); this.classList.remove('invalid'); } else { this.classList.add('invalid'); } }); pickerHex.addEventListener('blur', function() { refreshPickerUI(); }); pickerHex.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); pickerOk.click(); } });function addCustomColor(hex) { hex = hex.toUpperCase(); var existing = customPaletteEl.querySelector('.cpg-ct-color[data-color="' + hex + '"]'); if (existing) { selectColor(hex, existing); return; } var defaultExisting = defaultPaletteEl.querySelector('.cpg-ct-color[data-color="' + hex + '"]'); if (defaultExisting) { selectColor(hex, defaultExisting); return; } customColors.unshift(hex); if (customColors.length > maxCustomColors) customColors.pop(); customPaletteEl.innerHTML = ''; customColors.forEach(function(c) { var btn = document.createElement('button'); btn.className = 'cpg-ct-color cpg-ct-color-custom'; btn.setAttribute('data-color', c); btn.setAttribute('title', c); btn.style.background = c; if (c.toUpperCase() === '#FFFFFF') btn.style.borderColor = '#ccc'; customPaletteEl.appendChild(btn); }); var newBtn = customPaletteEl.querySelector('.cpg-ct-color[data-color="' + hex + '"]'); selectColor(hex, newBtn); }// ============================================================ // OPEN / CLOSE TOOL // ============================================================ window.pcOpenColoringTool = function(imageUrl, options) { if (!imageUrl) return; options = options || {};currentImageUrl = imageUrl; currentPageId = options.pageId || imageUrl.split('/').pop().replace(/\.[^.]+$/, ''); currentPageTitle = options.pageTitle || document.title || 'Coloring Page'; currentCategory = options.category || null;var initialPalette = 'classic'; if (currentCategory && CATEGORY_PALETTE_MAP[currentCategory.toLowerCase()]) { initialPalette = CATEGORY_PALETTE_MAP[currentCategory.toLowerCase()]; } renderPalette(initialPalette); currentColor = PALETTE_PRESETS[initialPalette].colors[0];customColors = []; customPaletteEl.innerHTML = ''; pageTitleEl.textContent = currentPageTitle;ct.classList.add('active'); ct.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; updateCursorStyle(); updateSizePreview(); updateDraftBadge();// Kick off logo preload in background so Download/Print are instant ensureBrandLogoLoaded();var img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function() { var maxDim = 3000; var scale = Math.min(maxDim / img.width, maxDim / img.height, 1); canvas.width = Math.round(img.width * scale); canvas.height = Math.round(img.height * scale);ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height);baseImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); undoStack = []; saveUndo();calcBaseDisplay(); zoomLevel = 1; canvas.style.width = baseDisplayW + 'px'; canvas.style.height = baseDisplayH + 'px'; zoomLabel.textContent = '100%';wrap.scrollLeft = (canvas.offsetWidth - wrap.clientWidth) / 2; wrap.scrollTop = (canvas.offsetHeight - wrap.clientHeight) / 2;maybeShowMobileHint();// If drafts exist for this page, show clickable toast var n = pageDrafts().length; if (n > 0) { setTimeout(function() { showToast( 'You have ' + n + ' saved draft' + (n === 1 ? '' : 's') + ' — tap to view', { onClick: openDraftsModal, duration: 5000 } ); }, 500); } }; img.onerror = function() { showToast('Could not load the image — please try again', { error: true }); closeColoringTool(); }; img.src = imageUrl; };function closeColoringTool() { ct.classList.remove('active'); ct.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; cursorEl.style.display = 'none'; wrap.classList.remove('panning', 'panning-active'); mobileHint.classList.remove('show'); closePicker(); closeDraftsModal(); hideToast(); touchMode = 'none'; cancelActiveStroke(); }ct.querySelectorAll('.cpg-ct-close').forEach(function(el) { el.addEventListener('click', closeColoringTool); });// ============================================================ // UNDO / CLEAR // ============================================================ function saveUndo() { if (undoStack.length >= maxUndo) undoStack.shift(); undoStack.push(ctx.getImageData(0, 0, canvas.width, canvas.height)); } function undo() { if (undoStack.length > 1) { undoStack.pop(); ctx.putImageData(undoStack[undoStack.length - 1], 0, 0); } } function clearCanvas() { if (!baseImageData) return; if (!confirm('Start over? This will erase all your coloring on this canvas. Saved drafts are not affected.')) return; ctx.putImageData(baseImageData, 0, 0); undoStack = []; saveUndo(); }// ============================================================ // TOOL SELECTION + ACTION BUTTONS // ============================================================ ct.querySelectorAll('.cpg-ct-tool').forEach(function(btn) { btn.addEventListener('click', function() { ct.querySelectorAll('.cpg-ct-tool').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); currentTool = btn.getAttribute('data-tool'); updateCursorStyle(); }); }); ct.querySelectorAll('.cpg-ct-action').forEach(function(btn) { btn.addEventListener('click', function() { var action = btn.getAttribute('data-action'); if (action === 'undo') undo(); if (action === 'clear') clearCanvas(); if (action === 'zoomin') setZoom(zoomLevel + 0.25); if (action === 'zoomout') setZoom(zoomLevel - 0.25); if (action === 'zoomreset') setZoom(1); }); }); ct.querySelectorAll('.cpg-ct-action-btn, .cpg-ct-save').forEach(function(btn) { btn.addEventListener('click', function() { var action = btn.getAttribute('data-action'); if (action === 'save-draft') handleSaveDraft(); else if (action === 'my-drafts') openDraftsModal(); else if (action === 'download') handleDownload(); else if (action === 'print') handlePrint(); }); });// ============================================================ // BRUSH BEHAVIORS // ============================================================ function getPos(e) { var rect = canvas.getBoundingClientRect(); var scaleX = canvas.width / rect.width; var scaleY = canvas.height / rect.height; var clientX, clientY; if (e.touches && e.touches.length > 0) { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY }; } function drawMarker(x1, y1, x2, y2) { ctx.globalAlpha = 1.0; ctx.globalCompositeOperation = 'source-over'; ctx.strokeStyle = currentColor; ctx.lineWidth = brushSize; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); } function drawPencil(x1, y1, x2, y2) { ctx.globalAlpha = 0.35; ctx.globalCompositeOperation = 'source-over'; ctx.strokeStyle = currentColor; ctx.lineWidth = brushSize * 0.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; for (var i = 0; i < 3; i++) { var jx = (Math.random() - 0.5) * brushSize * 0.3; var jy = (Math.random() - 0.5) * brushSize * 0.3; ctx.beginPath(); ctx.moveTo(x1 + jx, y1 + jy); ctx.lineTo(x2 + jx, y2 + jy); ctx.stroke(); } ctx.globalAlpha = 1.0; } function drawCrayon(x1, y1, x2, y2) { ctx.globalCompositeOperation = 'source-over'; var dist = Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)); var steps = Math.max(Math.ceil(dist / 2), 1); for (var s = 0; s < steps; s++) { var t = s / steps; var cx = x1 + (x2 - x1) * t; var cy = y1 + (y2 - y1) * t; for (var i = 0; i < 3; i++) { var ox = (Math.random() - 0.5) * brushSize * 0.7; var oy = (Math.random() - 0.5) * brushSize * 0.7; var radius = brushSize * 0.12 + Math.random() * brushSize * 0.12; ctx.globalAlpha = 0.06 + Math.random() * 0.1; ctx.fillStyle = currentColor; ctx.beginPath(); ctx.arc(cx + ox, cy + oy, radius, 0, Math.PI * 2); ctx.fill(); } } ctx.globalAlpha = 1.0; } function drawEraser(x1, y1, x2, y2) { ctx.globalAlpha = 1.0; ctx.globalCompositeOperation = 'source-over'; ctx.strokeStyle = '#FFFFFF'; ctx.lineWidth = brushSize; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); } function drawStroke(x1, y1, x2, y2) { switch (currentTool) { case 'marker': drawMarker(x1, y1, x2, y2); break; case 'pencil': drawPencil(x1, y1, x2, y2); break; case 'crayon': drawCrayon(x1, y1, x2, y2); break; case 'eraser': drawEraser(x1, y1, x2, y2); break; } } function drawDot(x, y) { ctx.globalAlpha = 1.0; ctx.globalCompositeOperation = 'source-over'; if (currentTool === 'crayon') { for (var i = 0; i < 4; i++) { var ox = (Math.random() - 0.5) * brushSize * 0.5; var oy = (Math.random() - 0.5) * brushSize * 0.5; ctx.globalAlpha = 0.06 + Math.random() * 0.08; ctx.fillStyle = currentColor; ctx.beginPath(); ctx.arc(x + ox, y + oy, brushSize * 0.15, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1.0; } else if (currentTool === 'pencil') { ctx.globalAlpha = 0.3; ctx.fillStyle = currentColor; ctx.beginPath(); ctx.arc(x, y, brushSize * 0.25, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1.0; } else { ctx.fillStyle = currentTool === 'eraser' ? '#FFFFFF' : currentColor; ctx.beginPath(); ctx.arc(x, y, brushSize / 2, 0, Math.PI * 2); ctx.fill(); } }// ============================================================ // DRAWING EVENTS // ============================================================ function startDraw(e) { if (touchMode === 'gesture') return; if (inPanMode()) { startPan(e); return; } if (currentTool === 'fill') { var pos = getPos(e); floodFill(Math.round(pos.x), Math.round(pos.y), currentColor); saveUndo(); return; } isDrawing = true; var pos2 = getPos(e); lastX = pos2.x; lastY = pos2.y; drawDot(lastX, lastY); } function draw(e) { if (touchMode === 'gesture') return; if (isPanning) { doPan(e); return; } if (!isDrawing) return; if (e.cancelable) e.preventDefault(); var pos = getPos(e); drawStroke(lastX, lastY, pos.x, pos.y); lastX = pos.x; lastY = pos.y; } function endDraw() { if (isPanning) { endPan(); return; } if (isDrawing) { isDrawing = false; ctx.globalAlpha = 1.0; saveUndo(); } } canvas.addEventListener('mousedown', function(e) { if (e.button === 0) startDraw(e); }); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', endDraw); canvas.addEventListener('mouseleave', endDraw); canvas.addEventListener('touchstart', function(e) { if (e.touches.length >= 2) { cancelActiveStroke(); return; } if (touchMode === 'gesture') return; if (e.touches.length === 1) { touchMode = 'draw'; e.preventDefault(); startDraw(e); } }, { passive: false }); canvas.addEventListener('touchmove', function(e) { if (touchMode === 'gesture' || e.touches.length !== 1) return; e.preventDefault(); draw(e); }, { passive: false }); canvas.addEventListener('touchend', function(e) { if (touchMode === 'draw') { touchMode = 'none'; endDraw(); } }); canvas.addEventListener('touchcancel', function(e) { touchMode = 'none'; endDraw(); });// ============================================================ // FLOOD FILL // ============================================================ function floodFill(startX, startY, fillColor) { var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); var data = imageData.data; var w = canvas.width, h = canvas.height; var startIdx = (startY * w + startX) * 4; var targetR = data[startIdx], targetG = data[startIdx+1], targetB = data[startIdx+2], targetA = data[startIdx+3]; var fill = hexToRgb(fillColor); if (!fill) return; if (targetR === fill.r && targetG === fill.g && targetB === fill.b) return; var tolerance = 32; var stack = [[startX, startY]]; var visited = new Uint8Array(w * h); function matches(idx) { return Math.abs(data[idx] - targetR) <= tolerance && Math.abs(data[idx+1] - targetG) <= tolerance && Math.abs(data[idx+2] - targetB) <= tolerance && Math.abs(data[idx+3] - targetA) <= tolerance; } while (stack.length > 0) { var p = stack.pop(); var x = p[0], y = p[1]; if (x < 0 || x >= w || y < 0 || y >= h) continue; var pi = y * w + x; if (visited[pi]) continue; var idx = pi * 4; if (!matches(idx)) continue; visited[pi] = 1; data[idx] = fill.r; data[idx+1] = fill.g; data[idx+2] = fill.b; data[idx+3] = 255; stack.push([x+1,y],[x-1,y],[x,y+1],[x,y-1]); } ctx.putImageData(imageData, 0, 0); }// ============================================================ // KEYBOARD // ============================================================ document.addEventListener('keydown', function(e) { if (!ct.classList.contains('active')) return; if (document.activeElement === pickerHex) return;if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') { e.preventDefault(); handleSaveDraft(); return; } if (e.key === ' ') { e.preventDefault(); if (!spaceHeld) { spaceHeld = true; toolBeforeSpace = currentTool; updateCursorStyle(); } return; } if ((e.ctrlKey || e.metaKey) && e.key === 'z') { e.preventDefault(); undo(); } if (e.key === 'Escape') { if (pickerModal.classList.contains('show')) closePicker(); else if (draftsModal.classList.contains('show')) closeDraftsModal(); else closeColoringTool(); } if (e.key === 'm') selectTool('marker'); if (e.key === 'p') selectTool('pencil'); if (e.key === 'c') selectTool('crayon'); if (e.key === 'f') selectTool('fill'); if (e.key === 'e') selectTool('eraser'); if (e.key === 'h') selectTool('pan'); if (e.key === '+' || e.key === '=') setZoom(zoomLevel + 0.25); if (e.key === '-') setZoom(zoomLevel - 0.25); if (e.key === '0') setZoom(1); }); document.addEventListener('keyup', function(e) { if (!ct.classList.contains('active')) return; if (e.key === ' ' && spaceHeld) { e.preventDefault(); spaceHeld = false; if (isPanning) endPan(); updateCursorStyle(); } }); function selectTool(tool) { ct.querySelectorAll('.cpg-ct-tool').forEach(function(b) { b.classList.remove('active'); }); var btn = ct.querySelector('.cpg-ct-tool[data-tool="'+tool+'"]'); if (btn) { btn.classList.add('active'); currentTool = tool; } updateCursorStyle(); }// ============================================================ // INITIAL RENDER // ============================================================ renderPalette('classic');})();