From 7553d059ddb64333908a7986699ec8fea1a4f44e Mon Sep 17 00:00:00 2001
From: johnruina
Date: Tue, 9 Jun 2026 10:06:23 -0400
Subject: [PATCH] aero and wave
---
aero.html | 707 +++++++++++++++++++++++++++++++++++++++++++++++++++++
draw.html | 123 +++++++++-
index.html | 2 +
wave.html | 574 +++++++++++++++++++++++++++++++++++++++++++
4 files changed, 1395 insertions(+), 11 deletions(-)
create mode 100644 aero.html
create mode 100644 wave.html
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Aerodynamics
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Display
+
+
+
+
+
+
+
+
Obstacle
+
+
+
+
+
+
+
+
+
+ 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);
+
+
+
+
+
+
+
+
Wave Simulation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index.html b/index.html
index 621deec..d00855e 100644
--- a/index.html
+++ b/index.html
@@ -87,6 +87,8 @@
diff --git a/wave.html b/wave.html
new file mode 100644
index 0000000..93f6d1f
--- /dev/null
+++ b/wave.html
@@ -0,0 +1,574 @@
+
+
+