aero and wave

This commit is contained in:
2026-06-09 10:06:23 -04:00
parent 65a5b8bd83
commit 7553d059dd
4 changed files with 1395 additions and 11 deletions
+112 -11
View File
@@ -210,6 +210,26 @@
.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: 3px;
border: 2px solid transparent;
cursor: pointer;
transition: border-color 0.15s;
}
.recent-swatch:hover { border-color: #888; }
.settings-btn {
width: 100%;
@@ -257,6 +277,8 @@
<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">
@@ -277,6 +299,7 @@
<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>
@@ -460,32 +483,93 @@
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'));
btn.classList.add('active');
brushType = btn.dataset.brush;
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;
ctx.globalAlpha = opacity;
ctx.fillStyle = currentColor;
if (brushType === 'round') {
ctx.beginPath();
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
ctx.fill();
} else if (brushType === 'soft') {
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');
@@ -496,7 +580,7 @@
} 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.8);
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;
@@ -507,7 +591,13 @@
dotSize, dotSize
);
}
} else {
ctx.beginPath();
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1;
}
@@ -548,6 +638,7 @@
}
function endDraw() {
if (isDown) saveState();
isDown = false;
lastX = -1; lastY = -1;
}
@@ -575,13 +666,23 @@
// ─── 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>