diff --git a/aero.html b/aero.html new file mode 100644 index 0000000..26d853d --- /dev/null +++ b/aero.html @@ -0,0 +1,707 @@ + + + + + + awesome johnruina site + + + + + + +
+ ← Back +
+ +
+
+ +
+
+
Aerodynamics
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
Display
+
+ + +
+
+ +
+
Obstacle
+
+ + +
+
+ +
+
Palette
+
+
+ +
+ Particles: 0 +
+ + +
+
+ + + + diff --git a/draw.html b/draw.html index 22a4ab0..53825ba 100644 --- a/draw.html +++ b/draw.html @@ -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 @@
+
+
@@ -277,6 +299,7 @@ +
@@ -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); diff --git a/index.html b/index.html index 621deec..d00855e 100644 --- a/index.html +++ b/index.html @@ -87,6 +87,8 @@
FRACTAL FLUID SIM + WAVE + AERO DRAW
diff --git a/wave.html b/wave.html new file mode 100644 index 0000000..93f6d1f --- /dev/null +++ b/wave.html @@ -0,0 +1,574 @@ + + + + + + awesome johnruina site + + + + + + +
+ ← Back +
+ +
+
+ +
+
+
Wave Simulation
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
Palette
+
+
+ + +
+
+ + + +