[html]<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Скретч — стираемый слой над картинкой</title>
<style>
:root {
--frame-width: 520px; /* можно менять */
--frame-padding: 12px;
--bg-color: #f0f0f0;
}
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #ffffff, #f7f7f7);
padding: 24px;
box-sizing: border-box;
}
.scratch-frame {
width: var(--frame-width);
max-width: 95vw;
background: white;
padding: var(--frame-padding);
border-radius: 12px;
box-shadow: 0 6px 20px rgba(15,15,15,0.12);
border: 1px solid rgba(0,0,0,0.06);
}
.scratch-title {
margin: 0 0 10px 0;
font-weight: 700;
font-size: 18px;
text-align: center;
}
.scratch-container {
position: relative;
overflow: hidden;
border-radius: 8px;
border: 1px solid #e0e0e0;
background: #ddd;
user-select: none;
touch-action: none; /* чтобы не прокручивалось при свайпе на мобильных */
}
/* картинка, которую вы подставляете */
.scratch-container img {
display: block;
width: 100%;
height: auto;
object-fit: cover;
vertical-align: middle;
pointer-events: none; /* клики/тачи идут на canvas */
}
/* холст поверх картинки */
.scratch-canvas {
position: absolute;
inset: 0;
display: block;
}
.controls {
margin-top: 10px;
display:flex;
gap:8px;
justify-content:center;
}
.btn {
border: none;
background: #1976d2;
color: white;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
.btn.ghost {
background: #fafafa;
color: #222;
border: 1px solid #ddd;
}
.hint {
margin-top:8px;
font-size:13px;
color:#555;
text-align:center;
}
</style>
</head>
<body>
<div class="scratch-frame" role="region" aria-label="Стираем серый слой, чтобы увидеть картинку">
<h2 class="scratch-title">Проведи мышкой (или пальцем) чтобы стереть</h2>
<div id="scratch" class="scratch-container" style="width:100%;">
<!--
ЗДЕСЬ ПОДСТАВЬТЕ ВАШУ КАРТИНКУ:
Замените значение src="your-image.jpg" на путь/URL вашей картинки.
Например: src="https://example.com/photo.jpg" или src="/images/pic.png"
-->
<img id="revealImage" src="https://avatars.mds.yandex.net/i?id=64261a69575830f8e4821e4b3074d322cdea58f7-10353822-images-thumbs&n=13" alt="Скрытая картинка (замените src)" />
<!-- canvas поверх картинки: скрипт подгонит размер автоматически -->
<canvas id="scratchCanvas" class="scratch-canvas" aria-hidden="true"></canvas>
</div>
<div class="controls" aria-hidden="false">
<button id="resetBtn" class="btn ghost" type="button">Сбросить</button>
<button id="revealBtn" class="btn" type="button">Открыть полностью</button>
</div>
<div class="hint">Кисть: <span id="brushSizeLabel">28</span> px — можно менять в коде</div>
</div>
<script>
(function(){
const img = document.getElementById('revealImage');
const canvas = document.getElementById('scratchCanvas');
const resetBtn = document.getElementById('resetBtn');
const revealBtn = document.getElementById('revealBtn');
const brushLabel = document.getElementById('brushSizeLabel');
// НАСТРОЙКИ:
let brushSize = 28; // размер кисти в пикселях (меняйте по вкусу)
const overlayColor = '#9e9e9e'; // цвет верхнего слоя
const minStartDistance = 2; // минимальный шаг для рисования линии
brushLabel.textContent = brushSize;
let isDrawing = false;
let lastPoint = null;
// Подгонка размера canvas под картинку и поддержка Retina
function resizeCanvasToImage() {
const rect = img.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
const ctx = canvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
fillOverlay();
}
// Заполнить серым (или градиентом) верхний слой
function fillOverlay() {
const ctx = canvas.getContext('2d');
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.globalCompositeOperation = 'source-over';
// можно сделать однотонный, или градиент:
ctx.fillStyle = overlayColor;
ctx.fillRect(0,0,canvas.width,canvas.height);
// добавить текст подсказки (необязательно)
ctx.fillStyle = 'rgba(255,255,255,0.06)';
ctx.font = '18px system-ui, Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
//ctx.fillText('Потри, чтобы открыть', canvas.width/2 / (window.devicePixelRatio||1), canvas.height/2 / (window.devicePixelRatio||1));
}
// Преобразование координат события к координатам canvas (учитывает позиционирование)
function getCanvasPoint(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
return {
x: clientX - rect.left,
y: clientY - rect.top
};
}
// Стереть (на самом деле нарисовать с globalCompositeOperation='destination-out')
function eraseAt(point) {
const ctx = canvas.getContext('2d');
ctx.save();
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath();
ctx.arc(point.x, point.y, brushSize/2, 0, Math.PI * 2, true);
ctx.fill();
ctx.restore();
}
// Плавная линия между точками (чтобы не были точки, а линия)
function eraseLine(p1, p2) {
const ctx = canvas.getContext('2d');
ctx.save();
ctx.globalCompositeOperation = 'destination-out';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = brushSize;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
ctx.restore();
}
function pointerDown(e) {
e.preventDefault();
isDrawing = true;
const p = getCanvasPoint(e.clientX, e.clientY);
lastPoint = p;
eraseAt(p);
}
function pointerMove(e) {
if (!isDrawing) return;
e.preventDefault();
const p = getCanvasPoint(e.clientX, e.clientY);
const dx = p.x - lastPoint.x;
const dy = p.y - lastPoint.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist > minStartDistance) {
eraseLine(lastPoint, p);
lastPoint = p;
} else {
eraseAt(p);
}
}
function pointerUp(e) {
if (!isDrawing) return;
isDrawing = false;
lastPoint = null;
// опционально: можно проверять процент стерто и автоматически открыть картинку
// if (getClearedPercent() > 0.6) revealFully();
}
// вычислить сколько пикселей стало прозрачными (приближённо)
function getClearedPercent() {
const ctx = canvas.getContext('2d');
try {
const data = ctx.getImageData(0,0,canvas.width,canvas.height).data;
let cleared = 0;
// берем каждый 4-й пиксель (альфа канал) для ускорения
for (let i = 3; i < data.length; i += 8) {
if (data[i] === 0) cleared++;
}
const totalSamples = data.length / 8;
return cleared / totalSamples;
} catch (err) {
// безопасность: если cors/security мешает доступ к пикселям — возвращаем 0
return 0;
}
}
function revealFully() {
const ctx = canvas.getContext('2d');
ctx.clearRect(0,0,canvas.width,canvas.height);
canvas.style.pointerEvents = 'none';
}
function resetScratch() {
canvas.style.pointerEvents = 'auto';
fillOverlay();
}
// Поддержка pointer events (работает и для мыши и для тача)
function addPointerListeners() {
canvas.addEventListener('pointerdown', pointerDown);
window.addEventListener('pointermove', pointerMove);
window.addEventListener('pointerup', pointerUp);
// предотвратить выделение текста/переход при долгом нажатии на мобильном
canvas.addEventListener('touchstart', e => e.preventDefault(), {passive:false});
}
// Инициализация после загрузки картинки: подгоняем размер
function init() {
// Убедимся, что картинка загружена, затем задаем размеры
if (img.complete && img.naturalWidth !== 0) {
resizeCanvasToImage();
} else {
img.addEventListener('load', resizeCanvasToImage, {once:true});
}
// при ресайзе окна — пересчитать
window.addEventListener('resize', () => {
// немного дебаунсам
clearTimeout(window._scratchResizeTimer);
window._scratchResizeTimer = setTimeout(resizeCanvasToImage, 120);
});
addPointerListeners();
resetBtn.addEventListener('click', () => {
resetScratch();
});
revealBtn.addEventListener('click', () => {
revealFully();
});
// можно управлять размером кисти через Wheel (опционально)
canvas.addEventListener('wheel', (e) => {
if (e.shiftKey) { // чтобы не мешать прокрутке страницы
e.preventDefault();
if (e.deltaY > 0) brushSize = Math.max(6, brushSize - 2);
else brushSize = Math.min(120, brushSize + 2);
brushLabel.textContent = brushSize;
}
}, {passive:false});
// дополнительная логика: если много стерто — автоматически открыть
let checkInterval = setInterval(() => {
const pct = getClearedPercent();
// если обнаружим >60% стерто — показываем полностью
if (pct > 0.60) {
revealFully();
clearInterval(checkInterval);
}
}, 1000);
}
// Старт
init();
})();
</script>
</body>
</html>[/html]