https://upforme.ru/uploads/001c/84/76/5/439548.png
[html]<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Раскраска — адаптивная под любые пропорции</title>
<style>
* {
box-sizing: border-box;
user-select: none;
}
body {
background: transparent;
margin: 0;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
font-family: system-ui, 'Segoe UI', 'Roboto', sans-serif;
}
.coloring-main {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.canvas-area {
background: transparent;
padding: 0;
display: flex;
justify-content: center;
}
canvas {
display: block;
cursor: pointer;
background: white;
border: 2px solid black;
max-width: 100%;
height: auto;
width: auto;
}
.tools-panel {
background: transparent;
padding: 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 8px;
}
#fullColorPicker {
width: 44px;
height: 44px;
border: 1px solid black;
cursor: pointer;
background: #cccccc;
padding: 0;
}
.tool-btn {
background: #d0d0d0;
border: 1px solid black;
font-weight: bold;
padding: 6px 12px;
cursor: pointer;
font-family: inherit;
font-size: 0.85rem;
transition: 0.05s linear;
white-space: nowrap;
color: black;
}
.tool-btn.active {
background: #a0a0a0;
color: white;
border-color: black;
}
.action-btn {
background: #c0c0c0;
border: 1px solid black;
font-weight: bold;
padding: 6px 12px;
cursor: pointer;
font-family: inherit;
font-size: 0.85rem;
white-space: nowrap;
color: black;
}
.info-stats {
background: #e0e0e0;
border: 1px solid black;
padding: 6px 12px;
font-weight: bold;
font-size: 0.85rem;
text-align: center;
white-space: nowrap;
color: black;
}
@media (max-width: 720px) {
.tools-panel {
gap: 6px;
}
.tool-btn, .action-btn, .info-stats {
padding: 4px 8px;
font-size: 0.75rem;
}
#fullColorPicker {
width: 36px;
height: 36px;
}
}
</style>
</head>
<body>
<div class="coloring-main">
<div class="canvas-area">
<canvas id="coloringCanvas"></canvas>
</div>
<div class="tools-panel">
<input type="color" id="fullColorPicker" value="#E34234">
<button id="brushModeBtn" class="tool-btn active">Кисть</button>
<button id="eyedropperBtn" class="tool-btn">Пипетка</button>
<button id="resetBtn" class="action-btn">Сбросить всё</button>
<div class="info-stats" id="statsDisplay">Осталось: --</div>
</div>
</div>
<script>
(function(){
// Элементы
const canvas = document.getElementById('coloringCanvas');
const ctx = canvas.getContext('2d');
// Глобальные переменные
let currentColor = "#E34234";
let currentTool = "brush";
let imageLoaded = false;
let originalImageData = null;
let contourMask = null;
let segmentMap = null;
let segmentColors = [];
let segmentCount = 0;
let currentWidth = 0, currentHeight = 0;
const STORAGE_KEY = "coloring_progress_v6";
// 🔽🔽🔽 СЮДА ВСТАВЬТЕ СВОЮ ССЫЛКУ НА ИЗОБРАЖЕНИЕ 🔽🔽🔽
const DEFAULT_IMAGE_URL = "https://upforme.ru/uploads/001c/84/76/5/41563.png";
// 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
// ---------- ЗАГРУЗКА ИЗОБРАЖЕНИЯ ----------
function loadImageFromData(imgElement) {
const imgW = imgElement.width;
const imgH = imgElement.height;
// Ограничиваем отображаемый размер до 800px по большей стороне
const maxDimension = 800;
let displayW = imgW;
let displayH = imgH;
if (imgW > maxDimension || imgH > maxDimension) {
const scale = maxDimension / Math.max(imgW, imgH);
displayW = Math.floor(imgW * scale);
displayH = Math.floor(imgH * scale);
}
canvas.width = imgW;
canvas.height = imgH;
canvas.style.width = `${displayW}px`;
canvas.style.height = `${displayH}px`;
currentWidth = imgW;
currentHeight = imgH;
ctx.clearRect(0, 0, imgW, imgH);
ctx.drawImage(imgElement, 0, 0, imgW, imgH);
originalImageData = ctx.getImageData(0, 0, imgW, imgH);
contourMask = new Uint8Array(imgW * imgH);
segmentMap = new Uint32Array(imgW * imgH);
detectSegmentsAndContours();
loadProgressFromStorage();
imageLoaded = true;
drawColoring();
updateStats();
console.log(`✅ Изображение загружено: ${imgW}x${imgH}, сегментов: ${segmentCount}`);
}
function loadImageFromUrl(url) {
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function() {
loadImageFromData(img);
};
img.onerror = () => {
console.error("❌ Не удалось загрузить картинку по URL. Проверьте ссылку и CORS.");
// Создаём тестовое изображение-заглушку
const testCanvas = document.createElement('canvas');
const testCtx = testCanvas.getContext('2d');
testCanvas.width = 600;
testCanvas.height = 400;
testCtx.fillStyle = "white";
testCtx.fillRect(0, 0, 600, 400);
testCtx.fillStyle = "black";
testCtx.fillRect(50, 50, 500, 300);
testCtx.fillStyle = "white";
testCtx.fillRect(100, 100, 400, 200);
const testImg = new Image();
testImg.onload = () => loadImageFromData(testImg);
testImg.src = testCanvas.toDataURL();
};
img.src = url;
}
// ---------- АНАЛИЗ КОНТУРОВ ----------
function detectSegmentsAndContours() {
if(!originalImageData) return;
const data = originalImageData.data;
const width = currentWidth, height = currentHeight;
contourMask.fill(0);
for(let i = 0; i < width * height; i++) {
const idx = i * 4;
const r = data[idx];
const g = data[idx+1];
const b = data[idx+2];
const a = data[idx+3];
if(a > 200 && (r + g + b) < 150) {
contourMask[i] = 1;
}
}
segmentMap.fill(0);
segmentColors = [];
segmentCount = 0;
function isFillable(x, y) {
if(x < 0 || x >= width || y < 0 || y >= height) return false;
return contourMask[y * width + x] === 0;
}
function floodFill(startX, startY, newId) {
const stack = [{x: startX, y: startY}];
while(stack.length) {
const {x, y} = stack.pop();
if(x < 0 || x >= width || y < 0 || y >= height) continue;
const idx = y * width + x;
if(segmentMap[idx] !== 0) continue;
if(!isFillable(x, y)) continue;
segmentMap[idx] = newId;
stack.push({x: x+1, y}, {x: x-1, y}, {x, y: y+1}, {x, y: y-1});
}
}
for(let y = 0; y < height; y++) {
for(let x = 0; x < width; x++) {
const idx = y * width + x;
if(segmentMap[idx] === 0 && isFillable(x, y)) {
segmentCount++;
floodFill(x, y, segmentCount);
segmentColors[segmentCount] = "#FFFFFF";
}
}
}
}
// ---------- СОХРАНЕНИЕ ПРОГРЕССА ----------
function saveProgressToStorage() {
if(!imageLoaded || segmentCount === 0) return;
const progress = [];
for(let i = 1; i <= segmentCount; i++) {
progress.push(segmentColors[i]);
}
const saveData = {
version: 6,
colors: progress,
width: currentWidth,
height: currentHeight,
timestamp: Date.now()
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(saveData));
}
function loadProgressFromStorage() {
if(!segmentCount) return;
const raw = localStorage.getItem(STORAGE_KEY);
if(!raw) return;
try {
const data = JSON.parse(raw);
if (data.version === 6 && data.width === currentWidth && data.height === currentHeight && Array.isArray(data.colors)) {
const savedColors = data.colors;
for(let i = 1; i <= Math.min(segmentCount, savedColors.length); i++) {
if(savedColors[i-1] && savedColors[i-1] !== "#FFFFFF") {
segmentColors[i] = savedColors[i-1];
}
}
console.log("✅ Восстановлены цвета из сохранения");
}
} catch(e) {}
}
// ---------- ОТРИСОВКА ----------
function drawColoring() {
if(!originalImageData) return;
const width = currentWidth, height = currentHeight;
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
for(let i = 0; i < width * height; i++) {
const segId = segmentMap[i];
if(segId > 0 && segmentColors[segId]) {
const hex = segmentColors[segId];
const r = parseInt(hex.slice(1,3), 16);
const g = parseInt(hex.slice(3,5), 16);
const b = parseInt(hex.slice(5,7), 16);
const x = i % width;
const y = Math.floor(i / width);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x, y, 1, 1);
}
}
const data = originalImageData.data;
for(let y = 0; y < height; y++) {
for(let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
const r = data[idx];
const g = data[idx+1];
const b = data[idx+2];
const a = data[idx+3];
if(a > 200 && (r + g + b) < 150) {
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x, y, 1, 1);
}
}
}
}
// ---------- ЛОГИКА ЗАЛИВКИ ----------
function fillSegment(segId, color) {
if(segId > 0 && segmentColors[segId] !== color) {
segmentColors[segId] = color;
drawColoring();
saveProgressToStorage();
updateStats();
}
}
function getSegmentAtPixel(x, y) {
if(x >= 0 && x < currentWidth && y >= 0 && y < currentHeight) {
return segmentMap[y * currentWidth + x];
}
return 0;
}
function pickColorAtPixel(x, y) {
if(!imageLoaded) return;
const pixel = ctx.getImageData(x, y, 1, 1).data;
const hex = `#${((1 << 24) + (pixel[0] << 16) + (pixel[1] << 8) + pixel[2]).toString(16).slice(1)}`;
currentColor = hex;
document.getElementById('fullColorPicker').value = hex;
}
function handleCanvasClick(e) {
if(!imageLoaded) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
let mx = (e.clientX - rect.left) * scaleX;
let my = (e.clientY - rect.top) * scaleY;
mx = Math.min(Math.max(0, mx), currentWidth - 1);
my = Math.min(Math.max(0, my), currentHeight - 1);
const xf = Math.floor(mx);
const yf = Math.floor(my);
if(currentTool === "eyedropper") {
pickColorAtPixel(xf, yf);
} else {
const segId = getSegmentAtPixel(xf, yf);
if(segId > 0) {
fillSegment(segId, currentColor);
} else {
alert("❌ Это контур. Кликни строго внутри области.");
}
}
}
function countRemainingSegments() {
let remain = 0;
for(let i = 1; i <= segmentCount; i++) {
if(segmentColors[i] === "#FFFFFF") remain++;
}
return remain;
}
function updateStats() {
if(!imageLoaded) {
document.getElementById('statsDisplay').innerHTML = "Осталось: --";
return;
}
const remaining = countRemainingSegments();
document.getElementById('statsDisplay').innerHTML = `Осталось: ${remaining}`;
if(remaining === 0) {
document.getElementById('statsDisplay').style.background = "#b0d0b0";
} else {
document.getElementById('statsDisplay').style.background = "#e0e0e0";
}
}
function resetColoring() {
if(!imageLoaded) return;
for(let i = 1; i <= segmentCount; i++) {
segmentColors[i] = "#FFFFFF";
}
drawColoring();
saveProgressToStorage();
updateStats();
}
// ---------- ПЕРЕКЛЮЧЕНИЕ РЕЖИМОВ ----------
const brushBtn = document.getElementById('brushModeBtn');
const eyedropperBtn = document.getElementById('eyedropperBtn');
function setTool(tool) {
currentTool = tool;
if(tool === "brush") {
brushBtn.classList.add('active');
eyedropperBtn.classList.remove('active');
canvas.style.cursor = "pointer";
} else {
eyedropperBtn.classList.add('active');
brushBtn.classList.remove('active');
canvas.style.cursor = "crosshair";
}
}
brushBtn.addEventListener('click', () => setTool("brush"));
eyedropperBtn.addEventListener('click', () => setTool("eyedropper"));
const colorPicker = document.getElementById('fullColorPicker');
colorPicker.addEventListener('input', (e) => {
currentColor = e.target.value;
setTool("brush");
});
canvas.addEventListener('click', handleCanvasClick);
document.getElementById('resetBtn').addEventListener('click', resetColoring);
// Загружаем изображение по умолчанию
loadImageFromUrl(DEFAULT_IMAGE_URL);
})();
</script>
</body>
</html>[/html]
[hideprofile]