706 lines
21 KiB
HTML
706 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>awesome johnruina site</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@600;800;900&display=swap" rel="stylesheet">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
min-height: 100vh;
|
|
background: #0f0f0f;
|
|
font-family: 'Inter', sans-serif;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.draw-layout {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
gap: 1.5rem;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.draw-canvas-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 1;
|
|
min-width: 0;
|
|
align-self: stretch;
|
|
}
|
|
|
|
.draw-canvas-wrap canvas {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
border-radius: 4px;
|
|
box-shadow: 0 0 60px rgba(74,74,138,0.15);
|
|
cursor: crosshair;
|
|
background: #fff;
|
|
}
|
|
|
|
.top-bar {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem 1.5rem;
|
|
z-index: 10;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.top-bar > * { pointer-events: auto; }
|
|
|
|
.back-btn {
|
|
text-decoration: none;
|
|
color: #888;
|
|
font-family: 'Inter', sans-serif;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.5px;
|
|
text-transform: uppercase;
|
|
transition: color 0.2s;
|
|
background: rgba(15,15,15,0.7);
|
|
padding: 0.4rem 0.8rem;
|
|
border-radius: 3px;
|
|
border: 1px solid #333;
|
|
}
|
|
|
|
.back-btn:hover { color: #fff; border-color: #555; }
|
|
|
|
/* ─── sidebar ─── */
|
|
|
|
.draw-settings {
|
|
width: 180px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.85rem;
|
|
padding: 1.2rem;
|
|
background: rgba(20,20,35,0.85);
|
|
border: 1px solid #2a2a4a;
|
|
border-radius: 4px;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
flex-shrink: 0;
|
|
align-items: center;
|
|
}
|
|
|
|
.draw-settings::-webkit-scrollbar { width: 4px; }
|
|
.draw-settings::-webkit-scrollbar-thumb { background: #333; border-radius: 1px; }
|
|
|
|
.settings-title {
|
|
color: #aaa;
|
|
font-weight: 800;
|
|
font-size: 0.65rem;
|
|
letter-spacing: 1.5px;
|
|
text-transform: uppercase;
|
|
border-bottom: 1px solid #2a2a4a;
|
|
padding-bottom: 0.6rem;
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
#wheelCanvas {
|
|
border-radius: 50%;
|
|
cursor: crosshair;
|
|
width: 150px;
|
|
height: 150px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.color-preview {
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 50%;
|
|
border: 2px solid #333;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.slider-group {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.2rem;
|
|
}
|
|
|
|
.slider-group label {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
color: #888;
|
|
font-size: 0.55rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.5px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.slider-group label span {
|
|
color: #bbb;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.slider-group input[type="range"] {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 100%;
|
|
height: 3px;
|
|
background: #2a2a4a;
|
|
border-radius: 1px;
|
|
outline: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.slider-group input[type="range"]::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 12px;
|
|
height: 12px;
|
|
background: #4a4a8a;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.slider-group input[type="range"]::-webkit-slider-thumb:hover {
|
|
background: #6a6aaa;
|
|
}
|
|
|
|
.slider-group input[type="range"]::-moz-range-thumb {
|
|
width: 12px;
|
|
height: 12px;
|
|
background: #4a4a8a;
|
|
border-radius: 50%;
|
|
border: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.brush-row {
|
|
display: flex;
|
|
gap: 4px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.brush-btn {
|
|
padding: 0.35rem 0.7rem;
|
|
background: transparent;
|
|
color: #666;
|
|
border: 1px solid #333;
|
|
border-radius: 3px;
|
|
font-family: 'Inter', sans-serif;
|
|
font-weight: 600;
|
|
font-size: 0.55rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.3px;
|
|
}
|
|
|
|
.brush-btn:hover { color: #ccc; border-color: #555; }
|
|
.brush-btn.active { color: #fff; border-color: #4a4a8a; background: #1a1a2e; }
|
|
.brush-btn.eraser-active { color: #f66; border-color: #a44; background: #2e1a1a; }
|
|
|
|
.recent-row {
|
|
display: flex;
|
|
gap: 3px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.recent-swatch {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 2px;
|
|
border: 2px solid transparent;
|
|
cursor: pointer;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.recent-swatch:hover { border-color: #888; }
|
|
|
|
.settings-btn {
|
|
width: 100%;
|
|
padding: 0.5rem 0;
|
|
background: transparent;
|
|
color: #666;
|
|
border: 1px solid #333;
|
|
border-radius: 3px;
|
|
font-family: 'Inter', sans-serif;
|
|
font-weight: 700;
|
|
font-size: 0.6rem;
|
|
cursor: pointer;
|
|
transition: all 0.25s;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
text-align: center;
|
|
}
|
|
|
|
.settings-btn:hover { color: #ccc; border-color: #555; background: rgba(255,255,255,0.03); }
|
|
|
|
@media (max-width: 700px) {
|
|
.draw-layout { flex-direction: column; padding: 0.75rem; padding-top: 3rem; gap: 0.5rem; }
|
|
.draw-settings { width: 100%; max-height: 170px; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 0.4rem; padding: 0.6rem; }
|
|
.draw-settings .settings-title { width: 100%; font-size: 0.55rem; }
|
|
#wheelCanvas { width: 70px; height: 70px; }
|
|
.color-preview { width: 35px; height: 35px; }
|
|
.slider-group { flex: 1; min-width: 70px; }
|
|
.slider-group label { font-size: 0.5rem; }
|
|
.brush-btn { font-size: 0.5rem; padding: 0.3rem 0.5rem; }
|
|
.settings-btn { font-size: 0.5rem; padding: 0.35rem 0; }
|
|
}
|
|
@media (max-width: 480px) {
|
|
.draw-layout { padding: 0.4rem; padding-top: 2.6rem; gap: 0.4rem; }
|
|
.draw-settings { max-height: 130px; padding: 0.4rem; gap: 0.3rem; }
|
|
#wheelCanvas { width: 55px; height: 55px; }
|
|
.color-preview { width: 28px; height: 28px; }
|
|
.slider-group { min-width: 55px; }
|
|
.slider-group input[type="range"]::-webkit-slider-thumb { width: 16px; height: 16px; }
|
|
.slider-group input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; }
|
|
.recent-swatch { width: 16px; height: 16px; }
|
|
}
|
|
</style>
|
|
<script src="https://pl30181304.effectivecpmnetwork.com/d6/d5/f0/d6d5f003a7751f24b1bf67fe9f7faf32.js"></script>
|
|
<script async="async" data-cfasync="false" src="https://pl30181305.effectivecpmnetwork.com/0b7109847f86d21e6500c09f845ba559/invoke.js"></script>
|
|
<div id="container-0b7109847f86d21e6500c09f845ba559"></div>
|
|
<script src="https://pl30181307.effectivecpmnetwork.com/2f/11/46/2f1146f3da3a358a1844e38992327882.js"></script>
|
|
</head>
|
|
<body>
|
|
<div class="top-bar">
|
|
<a class="back-btn" href="index.html">← Back</a>
|
|
</div>
|
|
|
|
<div class="draw-layout">
|
|
<div class="draw-canvas-wrap">
|
|
<canvas id="drawCanvas"></canvas>
|
|
</div>
|
|
<div class="draw-settings">
|
|
<div class="settings-title">Drawing Tools</div>
|
|
|
|
<canvas id="wheelCanvas" width="300" height="300"></canvas>
|
|
|
|
<div class="color-preview" id="colorPreview"></div>
|
|
|
|
<div class="recent-row" id="recentColors"></div>
|
|
|
|
<div class="slider-group">
|
|
<label>Lightness <span id="lightVal">50%</span></label>
|
|
<input type="range" id="lightSlider" min="0" max="100" step="1" value="50">
|
|
</div>
|
|
|
|
<div class="slider-group">
|
|
<label>Size <span id="sizeVal">10</span></label>
|
|
<input type="range" id="sizeSlider" min="1" max="100" step="1" value="10">
|
|
</div>
|
|
|
|
<div class="slider-group">
|
|
<label>Opacity <span id="opacityVal">100%</span></label>
|
|
<input type="range" id="opacitySlider" min="5" max="100" step="1" value="100">
|
|
</div>
|
|
|
|
<div class="brush-row" id="brushTypes">
|
|
<button class="brush-btn active" data-brush="round">Round</button>
|
|
<button class="brush-btn" data-brush="soft">Soft</button>
|
|
<button class="brush-btn" data-brush="square">Square</button>
|
|
<button class="brush-btn" data-brush="spray">Spray</button>
|
|
<button class="brush-btn" data-brush="eraser">Eraser</button>
|
|
</div>
|
|
|
|
<button class="settings-btn" id="clearCanvas">Clear Canvas</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ─── color utilities ───
|
|
function hslToRgb(h, s, l) {
|
|
let r, g, b;
|
|
if (s === 0) {
|
|
r = g = b = l;
|
|
} else {
|
|
const hue2rgb = (p, q, t) => {
|
|
if (t < 0) t += 1;
|
|
if (t > 1) t -= 1;
|
|
if (t < 1/6) return p + (q - p) * 6 * t;
|
|
if (t < 1/2) return q;
|
|
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
|
return p;
|
|
};
|
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
const p = 2 * l - q;
|
|
r = hue2rgb(p, q, h + 1/3);
|
|
g = hue2rgb(p, q, h);
|
|
b = hue2rgb(p, q, h - 1/3);
|
|
}
|
|
return [r, g, b];
|
|
}
|
|
|
|
// ─── drawing canvas ───
|
|
const canvas = document.getElementById('drawCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
function resizeCanvas() {
|
|
const wrap = document.querySelector('.draw-canvas-wrap');
|
|
const rect = wrap.getBoundingClientRect();
|
|
let size = Math.floor(Math.min(rect.width, rect.height, 900));
|
|
if (size < 50) size = 600;
|
|
if (canvas.width !== size || canvas.height !== size) {
|
|
const temp = document.createElement('canvas');
|
|
temp.width = canvas.width;
|
|
temp.height = canvas.height;
|
|
const tctx = temp.getContext('2d');
|
|
tctx.drawImage(canvas, 0, 0);
|
|
canvas.width = size;
|
|
canvas.height = size;
|
|
ctx.drawImage(temp, 0, 0, size, size);
|
|
}
|
|
}
|
|
|
|
// ─── color wheel ───
|
|
const wheelCanvas = document.getElementById('wheelCanvas');
|
|
const wheelCtx = wheelCanvas.getContext('2d');
|
|
const wheelSize = 300;
|
|
const wheelRadius = wheelSize / 2 - 6;
|
|
|
|
let currentHue = 0;
|
|
let currentSat = 1;
|
|
let currentLight = 0.5;
|
|
let currentColor = 'rgb(255,0,0)';
|
|
|
|
function drawColorWheel() {
|
|
const cx = wheelSize / 2;
|
|
const cy = wheelSize / 2;
|
|
const r = wheelRadius;
|
|
const imgData = wheelCtx.createImageData(wheelSize, wheelSize);
|
|
const data = imgData.data;
|
|
|
|
for (let y = 0; y < wheelSize; y++) {
|
|
for (let x = 0; x < wheelSize; x++) {
|
|
const dx = x - cx;
|
|
const dy = y - cy;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist <= r) {
|
|
const hue = (Math.atan2(dy, dx) / Math.PI * 180 + 180) / 360;
|
|
const sat = Math.min(1, dist / r);
|
|
const [r2, g, b] = hslToRgb(hue, sat, currentLight);
|
|
const idx = (y * wheelSize + x) * 4;
|
|
data[idx] = Math.round(r2 * 255);
|
|
data[idx + 1] = Math.round(g * 255);
|
|
data[idx + 2] = Math.round(b * 255);
|
|
data[idx + 3] = 255;
|
|
}
|
|
}
|
|
}
|
|
wheelCtx.putImageData(imgData, 0, 0);
|
|
wheelCtx.beginPath();
|
|
wheelCtx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
wheelCtx.strokeStyle = '#333';
|
|
wheelCtx.lineWidth = 2;
|
|
wheelCtx.stroke();
|
|
}
|
|
|
|
function colorFromWheel(clientX, clientY) {
|
|
const rect = wheelCanvas.getBoundingClientRect();
|
|
const x = (clientX - rect.left) * (wheelSize / rect.width);
|
|
const y = (clientY - rect.top) * (wheelSize / rect.height);
|
|
const cx = wheelSize / 2;
|
|
const cy = wheelSize / 2;
|
|
const dx = x - cx;
|
|
const dy = y - cy;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist > wheelRadius) return null;
|
|
const hue = (Math.atan2(dy, dx) / Math.PI * 180 + 180) / 360;
|
|
const sat = dist / wheelRadius;
|
|
return { hue, sat, dist };
|
|
}
|
|
|
|
function pickColor(clientX, clientY) {
|
|
const c = colorFromWheel(clientX, clientY);
|
|
if (!c) return;
|
|
currentHue = c.hue;
|
|
currentSat = c.sat;
|
|
updateCurrentColor();
|
|
}
|
|
|
|
function updateCurrentColor() {
|
|
const [r, g, b] = hslToRgb(currentHue, currentSat, currentLight);
|
|
currentColor = `rgb(${Math.round(r*255)},${Math.round(g*255)},${Math.round(b*255)})`;
|
|
document.getElementById('colorPreview').style.background = currentColor;
|
|
}
|
|
|
|
let wheelDown = false;
|
|
|
|
wheelCanvas.addEventListener('mousedown', (e) => {
|
|
pickColor(e.clientX, e.clientY);
|
|
wheelDown = true;
|
|
});
|
|
|
|
window.addEventListener('mousemove', (e) => {
|
|
if (wheelDown) pickColor(e.clientX, e.clientY);
|
|
});
|
|
|
|
window.addEventListener('mouseup', () => { wheelDown = false; });
|
|
|
|
wheelCanvas.addEventListener('touchstart', (e) => {
|
|
e.preventDefault();
|
|
const t = e.touches[0];
|
|
pickColor(t.clientX, t.clientY);
|
|
wheelDown = true;
|
|
}, { passive: false });
|
|
|
|
wheelCanvas.addEventListener('touchmove', (e) => {
|
|
e.preventDefault();
|
|
if (wheelDown) {
|
|
const t = e.touches[0];
|
|
pickColor(t.clientX, t.clientY);
|
|
}
|
|
}, { passive: false });
|
|
|
|
wheelCanvas.addEventListener('touchend', (e) => {
|
|
e.preventDefault();
|
|
wheelDown = false;
|
|
}, { passive: false });
|
|
|
|
// ─── lightness ───
|
|
const lightSlider = document.getElementById('lightSlider');
|
|
const lightVal = document.getElementById('lightVal');
|
|
|
|
lightSlider.addEventListener('input', () => {
|
|
currentLight = parseFloat(lightSlider.value) / 100;
|
|
lightVal.textContent = Math.round(currentLight * 100) + '%';
|
|
drawColorWheel();
|
|
updateCurrentColor();
|
|
});
|
|
|
|
// ─── brush size ───
|
|
const sizeSlider = document.getElementById('sizeSlider');
|
|
const sizeVal = document.getElementById('sizeVal');
|
|
|
|
sizeSlider.addEventListener('input', () => {
|
|
sizeVal.textContent = sizeSlider.value;
|
|
});
|
|
|
|
// ─── opacity ───
|
|
const opacitySlider = document.getElementById('opacitySlider');
|
|
const opacityVal = document.getElementById('opacityVal');
|
|
|
|
opacitySlider.addEventListener('input', () => {
|
|
opacityVal.textContent = opacitySlider.value + '%';
|
|
});
|
|
|
|
// ─── recent colors ───
|
|
const recentColors = [];
|
|
|
|
function addRecentColor(color) {
|
|
const idx = recentColors.indexOf(color);
|
|
if (idx >= 0) recentColors.splice(idx, 1);
|
|
recentColors.unshift(color);
|
|
if (recentColors.length > 8) recentColors.pop();
|
|
renderRecentColors();
|
|
}
|
|
|
|
function renderRecentColors() {
|
|
const container = document.getElementById('recentColors');
|
|
container.innerHTML = '';
|
|
recentColors.forEach(c => {
|
|
const swatch = document.createElement('div');
|
|
swatch.className = 'recent-swatch';
|
|
swatch.style.background = c;
|
|
swatch.addEventListener('click', () => {
|
|
isEraser = false;
|
|
document.querySelectorAll('#brushTypes .brush-btn').forEach(b => {
|
|
b.classList.toggle('active', b.dataset.brush === brushType);
|
|
b.classList.remove('eraser-active');
|
|
});
|
|
currentColor = c;
|
|
document.getElementById('colorPreview').style.background = c;
|
|
});
|
|
container.appendChild(swatch);
|
|
});
|
|
}
|
|
|
|
// ─── brush type ───
|
|
let brushType = 'round';
|
|
let isEraser = false;
|
|
const brushBtns = document.querySelectorAll('#brushTypes .brush-btn');
|
|
|
|
brushBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
brushBtns.forEach(b => {
|
|
b.classList.remove('active');
|
|
b.classList.remove('eraser-active');
|
|
});
|
|
if (btn.dataset.brush === 'eraser') {
|
|
isEraser = true;
|
|
btn.classList.add('eraser-active');
|
|
} else {
|
|
isEraser = false;
|
|
brushType = btn.dataset.brush;
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── undo ───
|
|
const undoStack = [];
|
|
const MAX_UNDO = 50;
|
|
|
|
function saveState() {
|
|
if (canvas.width === 0 || canvas.height === 0) return;
|
|
undoStack.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
|
|
if (undoStack.length > MAX_UNDO) undoStack.shift();
|
|
}
|
|
|
|
function undo() {
|
|
if (undoStack.length === 0) return;
|
|
const state = undoStack.pop();
|
|
ctx.putImageData(state, 0, 0);
|
|
}
|
|
|
|
// ─── drawing ───
|
|
let isDown = false;
|
|
let lastX = -1, lastY = -1;
|
|
|
|
function drawBrush(x, y, size) {
|
|
const opacity = parseFloat(opacitySlider.value) / 100;
|
|
|
|
if (isEraser) {
|
|
ctx.globalCompositeOperation = 'destination-out';
|
|
ctx.globalAlpha = opacity;
|
|
} else {
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
ctx.globalAlpha = opacity;
|
|
ctx.fillStyle = currentColor;
|
|
addRecentColor(currentColor);
|
|
}
|
|
|
|
if (brushType === 'soft' && !isEraser) {
|
|
const grad = ctx.createRadialGradient(x, y, 0, x, y, size / 2);
|
|
grad.addColorStop(0, currentColor);
|
|
grad.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = grad;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
} else if (brushType === 'square') {
|
|
ctx.fillRect(x - size / 2, y - size / 2, size, size);
|
|
} else if (brushType === 'spray') {
|
|
const count = Math.max(5, size * 0.4);
|
|
for (let i = 0; i < count; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const radius = Math.random() * size / 2;
|
|
const dotSize = Math.max(1, 1 + Math.random() * 2);
|
|
ctx.fillRect(
|
|
x + Math.cos(angle) * radius,
|
|
y + Math.sin(angle) * radius,
|
|
dotSize, dotSize
|
|
);
|
|
}
|
|
} else {
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
function drawLine(x1, y1, x2, y2) {
|
|
const size = parseFloat(sizeSlider.value);
|
|
const dist = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
|
const steps = Math.max(1, Math.floor(dist / 1.5));
|
|
for (let i = 0; i <= steps; i++) {
|
|
const t = i / steps;
|
|
drawBrush(
|
|
x1 + (x2 - x1) * t,
|
|
y1 + (y2 - y1) * t,
|
|
size
|
|
);
|
|
}
|
|
}
|
|
|
|
function canvasCoords(e) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
return {
|
|
x: (e.clientX - rect.left) * (canvas.width / rect.width),
|
|
y: (e.clientY - rect.top) * (canvas.height / rect.height),
|
|
};
|
|
}
|
|
|
|
function startDraw(e) {
|
|
const p = canvasCoords(e);
|
|
lastX = p.x; lastY = p.y;
|
|
isDown = true;
|
|
drawBrush(lastX, lastY, parseFloat(sizeSlider.value));
|
|
}
|
|
|
|
function moveDraw(e) {
|
|
if (!isDown) return;
|
|
const p = canvasCoords(e);
|
|
drawLine(lastX, lastY, p.x, p.y);
|
|
lastX = p.x; lastY = p.y;
|
|
}
|
|
|
|
function endDraw() {
|
|
if (isDown) saveState();
|
|
isDown = false;
|
|
lastX = -1; lastY = -1;
|
|
}
|
|
|
|
canvas.addEventListener('mousedown', startDraw);
|
|
canvas.addEventListener('mousemove', moveDraw);
|
|
window.addEventListener('mouseup', endDraw);
|
|
|
|
canvas.addEventListener('touchstart', (e) => {
|
|
e.preventDefault();
|
|
const t = e.touches[0];
|
|
startDraw(t);
|
|
}, { passive: false });
|
|
|
|
canvas.addEventListener('touchmove', (e) => {
|
|
e.preventDefault();
|
|
const t = e.touches[0];
|
|
moveDraw(t);
|
|
}, { passive: false });
|
|
|
|
canvas.addEventListener('touchend', (e) => {
|
|
e.preventDefault();
|
|
endDraw();
|
|
}, { passive: false });
|
|
|
|
// ─── clear ───
|
|
document.getElementById('clearCanvas').addEventListener('click', () => {
|
|
saveState();
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
});
|
|
|
|
// ─── keyboard ───
|
|
document.addEventListener('keydown', (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
|
|
e.preventDefault();
|
|
undo();
|
|
}
|
|
});
|
|
|
|
// ─── init ───
|
|
drawColorWheel();
|
|
updateCurrentColor();
|
|
resizeCanvas();
|
|
saveState();
|
|
window.addEventListener('resize', resizeCanvas);
|
|
</script>
|
|
</body>
|
|
</html>
|