Lamine Yamal Coloring Pages

Football Players › Lamine Yamal

Discover the future of football with 13 free printable Lamine Yamal coloring pages. Capture the young Barcelona and Spain superstar in his most exciting moments: dazzling dribbling, debut goals, breakthrough performances, and the joy of becoming one of the youngest players to make history at the highest level.

' ); 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');})();