Skip to content

Instantly share code, notes, and snippets.

@Alexhha
Created May 17, 2026 17:24
Show Gist options
  • Select an option

  • Save Alexhha/4ac16518e39bc34ace7882defabfea26 to your computer and use it in GitHub Desktop.

Select an option

Save Alexhha/4ac16518e39bc34ace7882defabfea26 to your computer and use it in GitHub Desktop.
<style>
* { box-sizing: border-box; margin: 0; padding: 0; touch-action: none; user-select: none; }
#app { display: flex; flex-direction: column; height: 100vh; min-height: 500px; background: #fff; }
#toolbar {
display: flex; align-items: center; gap: 6px;
padding: 8px 10px;
background: #f9f7f2;
border-bottom: 2px solid #e8e3d8;
flex-wrap: nowrap; overflow-x: auto;
}
.tool-btn {
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-width: 52px; height: 52px; padding: 4px;
border-radius: 12px; border: 2px solid transparent;
background: #fff; cursor: pointer;
transition: transform 0.1s, border-color 0.1s; gap: 2px; flex-shrink: 0;
}
.tool-btn:active { transform: scale(0.93); }
.tool-btn.active { border-color: #378add; background: #e6f1fb; }
.tool-icon { font-size: 24px; line-height: 1; }
.tool-label { font-size: 10px; color: #888; font-family: sans-serif; white-space: nowrap; }
.tool-btn.active .tool-label { color: #185fa5; }
.sep { width: 1px; height: 40px; background: #e0dbd0; margin: 0 2px; flex-shrink: 0; }
#size-wrap { display: flex; flex-direction: column; align-items: center; gap: 2px; flex-shrink: 0; }
#size-slider { width: 80px; accent-color: #378add; }
#size-label { font-size: 11px; color: #888; font-family: sans-serif; }
#palette { display: flex; gap: 5px; align-items: center; flex-wrap: wrap; max-width: 170px; }
.color-dot {
width: 30px; height: 30px; border-radius: 50%;
border: 3px solid transparent; cursor: pointer;
transition: transform 0.1s; flex-shrink: 0;
}
.color-dot.active { border-color: #333; transform: scale(1.25); }
#color-picker { width: 36px; height: 36px; border-radius: 50%; border: 2px solid #ddd; cursor: pointer; padding: 1px; background: none; flex-shrink: 0; }
#clear-btn {
min-width: 44px; height: 44px; border-radius: 12px;
border: 2px solid #fca5a5; background: #fff; cursor: pointer;
font-size: 20px; display: flex; align-items: center; justify-content: center;
transition: transform 0.1s; flex-shrink: 0;
}
#clear-btn:active { transform: scale(0.93); background: #fee2e2; }
#upload-btn {
min-width: 52px; height: 52px; border-radius: 12px;
border: 2px solid #9FE1CB; background: #fff; cursor: pointer;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 2px; flex-shrink: 0; transition: transform 0.1s;
}
#upload-btn:active { transform: scale(0.93); background: #E1F5EE; }
#upload-btn .tool-label { color: #0F6E56; }
#file-input { display: none; }
#canvas-wrap { flex: 1; overflow: hidden; position: relative; background: #fff; cursor: crosshair; }
canvas { display: block; width: 100%; height: 100%; }
#drop-hint {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 12px;
pointer-events: none; opacity: 0.35;
}
#drop-hint .big { font-size: 64px; }
#drop-hint .txt { font-size: 16px; font-family: sans-serif; color: #888; }
</style>
<h2 class="sr-only">Рисовалка для детей с загрузкой фона для раскрашивания</h2>
<div id="app">
<div id="toolbar">
<button class="tool-btn active" id="btn-pencil" data-tool="pencil">
<span class="tool-icon">✏️</span><span class="tool-label">Карандаш</span>
</button>
<button class="tool-btn" id="btn-brush" data-tool="brush">
<span class="tool-icon">🖌️</span><span class="tool-label">Кисточка</span>
</button>
<button class="tool-btn" id="btn-marker" data-tool="marker">
<span class="tool-icon">🖊️</span><span class="tool-label">Маркер</span>
</button>
<button class="tool-btn" id="btn-chalk" data-tool="chalk">
<span class="tool-icon">🖍️</span><span class="tool-label">Мелок</span>
</button>
<button class="tool-btn" id="btn-eraser" data-tool="eraser">
<span class="tool-icon">🧹</span><span class="tool-label">Ластик</span>
</button>
<div class="sep"></div>
<div id="size-wrap">
<input type="range" id="size-slider" min="2" max="50" value="8">
<span id="size-label">Размер: 8</span>
</div>
<div class="sep"></div>
<div id="palette"></div>
<input type="color" id="color-picker" value="#1a1a1a" title="Свой цвет">
<div class="sep"></div>
<button id="upload-btn" title="Загрузить картинку для раскраски">
<span class="tool-icon">🖼️</span>
<span class="tool-label">Раскраска</span>
</button>
<input type="file" id="file-input" accept="image/*">
<div class="sep"></div>
<button id="clear-btn" title="Очистить рисунок">🗑️</button>
</div>
<div id="canvas-wrap">
<canvas id="bg-canvas"></canvas>
<canvas id="canvas" style="position:absolute;inset:0;"></canvas>
<div id="drop-hint">
<div class="big">🎨</div>
<div class="txt">Нажми 🖼️ чтобы загрузить раскраску</div>
</div>
</div>
</div>
<script>
const COLORS = ['#1a1a1a','#e24b4a','#ef9f27','#3b6d11','#378add','#d4537e','#7f77dd','#1d9e75','#ffffff','#a0522d'];
const palette = document.getElementById('palette');
let currentColor = '#1a1a1a';
let currentTool = 'pencil';
COLORS.forEach(c => {
const dot = document.createElement('div');
dot.className = 'color-dot' + (c === currentColor ? ' active' : '');
dot.style.background = c;
if (c === '#ffffff') dot.style.boxShadow = 'inset 0 0 0 1px #ccc';
dot.addEventListener('pointerdown', e => {
e.stopPropagation();
document.querySelectorAll('.color-dot').forEach(d => d.classList.remove('active'));
dot.classList.add('active');
currentColor = c;
document.getElementById('color-picker').value = c;
if (currentTool === 'eraser') setTool('pencil');
});
palette.appendChild(dot);
});
document.getElementById('color-picker').addEventListener('input', e => {
currentColor = e.target.value;
document.querySelectorAll('.color-dot').forEach(d => d.classList.remove('active'));
if (currentTool === 'eraser') setTool('pencil');
});
function setTool(t) {
currentTool = t;
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
const btn = document.getElementById('btn-' + t);
if (btn) btn.classList.add('active');
}
document.querySelectorAll('.tool-btn').forEach(btn => {
btn.addEventListener('pointerdown', e => { e.preventDefault(); setTool(btn.dataset.tool); });
});
document.getElementById('clear-btn').addEventListener('pointerdown', e => {
e.preventDefault();
ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
});
const sizeSlider = document.getElementById('size-slider');
const sizeLabel = document.getElementById('size-label');
sizeSlider.addEventListener('input', () => { sizeLabel.textContent = 'Размер: ' + sizeSlider.value; });
const wrap = document.getElementById('canvas-wrap');
const bgCanvas = document.getElementById('bg-canvas');
const bgCtx = bgCanvas.getContext('2d');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const hint = document.getElementById('drop-hint');
let bgImage = null;
function resize() {
const dpr = window.devicePixelRatio || 1;
const w = wrap.offsetWidth; const h = wrap.offsetHeight;
const tmp = ctx.getImageData(0, 0, canvas.width, canvas.height);
[bgCanvas, canvas].forEach(c => {
c.width = w * dpr; c.height = h * dpr;
c.style.width = w + 'px'; c.style.height = h + 'px';
c.getContext('2d').scale(dpr, dpr);
});
if (bgImage) drawBg();
ctx.putImageData(tmp, 0, 0);
}
function drawBg() {
const w = bgCanvas.offsetWidth, h = bgCanvas.offsetHeight;
bgCtx.clearRect(0, 0, w, h);
bgCtx.fillStyle = '#ffffff';
bgCtx.fillRect(0, 0, w, h);
const iw = bgImage.naturalWidth, ih = bgImage.naturalHeight;
const scale = Math.min(w / iw, h / ih);
const dw = iw * scale, dh = ih * scale;
const dx = (w - dw) / 2, dy = (h - dh) / 2;
bgCtx.drawImage(bgImage, dx, dy, dw, dh);
}
resize();
window.addEventListener('resize', resize);
document.getElementById('upload-btn').addEventListener('pointerdown', e => {
e.preventDefault();
document.getElementById('file-input').click();
});
document.getElementById('file-input').addEventListener('change', e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = ev => {
const img = new Image();
img.onload = () => {
bgImage = img;
drawBg();
ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
hint.style.display = 'none';
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
e.target.value = '';
});
let drawing = false, lastX = 0, lastY = 0;
function getPos(e) {
const rect = canvas.getBoundingClientRect();
return {
x: (e.clientX - rect.left) * (canvas.offsetWidth / rect.width),
y: (e.clientY - rect.top) * (canvas.offsetHeight / rect.height)
};
}
function getPressure(e) {
if (e.pressure > 0 && e.pointerType !== 'mouse') return e.pressure;
return 0.5;
}
function drawStroke(x1, y1, x2, y2, e) {
const base = parseInt(sizeSlider.value);
const pressure = getPressure(e);
if (currentTool === 'eraser') {
ctx.globalCompositeOperation = 'destination-out';
ctx.globalAlpha = 1;
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.lineWidth = base * 2.5;
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
ctx.globalCompositeOperation = 'source-over';
return;
}
ctx.globalCompositeOperation = 'source-over';
if (currentTool === 'pencil') {
ctx.globalAlpha = 0.85;
ctx.strokeStyle = currentColor;
ctx.lineWidth = Math.max(1, base * 0.5 * pressure * 2);
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
} else if (currentTool === 'brush') {
ctx.globalAlpha = 0.12;
const size = base * 1.8 * pressure;
const steps = Math.ceil(Math.hypot(x2-x1, y2-y1) / (size * 0.3)) + 1;
for (let i = 0; i <= steps; i++) {
const t = steps === 0 ? 0 : i / steps;
ctx.fillStyle = currentColor;
ctx.beginPath();
ctx.arc(x1+(x2-x1)*t, y1+(y2-y1)*t, size, 0, Math.PI*2);
ctx.fill();
}
} else if (currentTool === 'marker') {
ctx.globalAlpha = 0.55;
ctx.strokeStyle = currentColor;
ctx.lineWidth = base * 1.6 * pressure;
ctx.lineCap = 'square'; ctx.lineJoin = 'miter';
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
} else if (currentTool === 'chalk') {
const size = base * 0.9 * pressure;
const dist = Math.hypot(x2-x1, y2-y1);
const steps = Math.max(1, Math.ceil(dist / 2));
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const bx = x1+(x2-x1)*t + (Math.random()-0.5)*size*0.8;
const by = y1+(y2-y1)*t + (Math.random()-0.5)*size*0.8;
ctx.globalAlpha = Math.random()*0.4+0.1;
ctx.fillStyle = currentColor;
ctx.beginPath();
ctx.ellipse(bx, by, size*0.4+Math.random()*size*0.3, size*0.2, Math.random()*Math.PI, 0, Math.PI*2);
ctx.fill();
}
}
ctx.globalAlpha = 1;
}
canvas.addEventListener('pointerdown', e => {
e.preventDefault(); drawing = true;
canvas.setPointerCapture(e.pointerId);
const pos = getPos(e); lastX = pos.x; lastY = pos.y;
drawStroke(pos.x, pos.y, pos.x+0.1, pos.y+0.1, e);
});
canvas.addEventListener('pointermove', e => {
if (!drawing) return; e.preventDefault();
const pos = getPos(e);
drawStroke(lastX, lastY, pos.x, pos.y, e);
lastX = pos.x; lastY = pos.y;
});
canvas.addEventListener('pointerup', () => { drawing = false; });
canvas.addEventListener('pointercancel', () => { drawing = false; });
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment