Files
2026-07-04 10:19:06 -04:00

725 lines
22 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;
}
.layout {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
gap: 1.5rem;
padding: 1.5rem;
}
.canvas-wrap {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-width: 0;
align-self: stretch;
}
.canvas-wrap canvas {
max-width: 100%;
max-height: 100%;
aspect-ratio: 1;
border-radius: 2px;
box-shadow: 0 0 60px rgba(74,74,138,0.15);
cursor: crosshair;
}
.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; }
.settings {
width: 200px;
display: flex;
flex-direction: column;
gap: 0.85rem;
padding: 1.2rem;
background: rgba(20,20,35,0.85);
border: 1px solid #2a2a4a;
border-radius: 2px;
max-height: 90vh;
overflow-y: auto;
flex-shrink: 0;
}
.settings::-webkit-scrollbar { width: 4px; }
.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;
}
.s-group {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.s-group label {
display: flex;
justify-content: space-between;
color: #888;
font-size: 0.6rem;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.s-group label span { color: #bbb; font-weight: 800; }
.s-group input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 3px;
background: #2a2a4a;
border-radius: 1px;
outline: none;
cursor: pointer;
}
.s-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;
}
.s-group input[type="range"]::-webkit-slider-thumb:hover { background: #6a6aaa; }
.s-group input[type="range"]::-moz-range-thumb {
width: 12px; height: 12px;
background: #4a4a8a;
border-radius: 50%;
border: none;
cursor: pointer;
}
.subtitle {
color: #888;
font-size: 0.6rem;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.palette-row {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.palette-swatch {
width: 28px; height: 20px;
border-radius: 2px;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.2s;
}
.palette-swatch:hover { border-color: #666; transform: scale(1.1); }
.palette-swatch.active { border-color: #4a4a8a; box-shadow: 0 0 8px rgba(74,74,138,0.4); }
.info-row {
color: #555;
font-size: 0.55rem;
letter-spacing: 0.3px;
line-height: 1.5;
}
.info-row strong { color: #777; }
.s-btn {
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;
}
.s-btn:hover { color: #ccc; border-color: #555; background: rgba(255,255,255,0.03); }
.s-btn.active { color: #fff; border-color: #4a4a8a; background: #1a1a2e; }
@media (max-width: 800px) {
.layout { flex-direction: column; padding: 0.75rem; padding-top: 3rem; gap: 0.75rem; }
.settings { width: 100%; max-height: 170px; flex-direction: row; flex-wrap: wrap; align-items: flex-start; gap: 0.4rem; padding: 0.6rem; }
.settings .settings-title { width: 100%; font-size: 0.55rem; }
.settings .s-group { flex: 1; min-width: 80px; }
.settings .s-group label { font-size: 0.5rem; }
.settings .palette-row { width: 100%; }
.palette-swatch { width: 24px; height: 16px; }
.subtitle { font-size: 0.5rem; }
.s-btn { font-size: 0.5rem; padding: 0.35rem 0; }
}
@media (max-width: 480px) {
.layout { padding: 0.5rem; padding-top: 2.6rem; gap: 0.5rem; }
.settings { max-height: 130px; padding: 0.4rem; gap: 0.3rem; }
.settings .s-group { min-width: 60px; }
.canvas-wrap canvas { border-radius: 3px; }
.s-group input[type="range"]::-webkit-slider-thumb { width: 16px; height: 16px; }
.s-group input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; }
.info-row { font-size: 0.5rem; }
}
</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="layout">
<div class="canvas-wrap">
<canvas id="aeroCanvas"></canvas>
</div>
<div class="settings">
<div class="settings-title">Aerodynamics</div>
<div class="s-group">
<label>Viscosity <span id="viscVal">1.0e-6</span></label>
<input type="range" id="viscSlider" min="0" max="10" step="0.1" value="1">
</div>
<div class="s-group">
<label>Wind Speed <span id="windVal">3.0</span></label>
<input type="range" id="windSlider" min="0.5" max="15" step="0.5" value="3">
</div>
<div class="s-group">
<label>Particles <span id="partVal">600</span></label>
<input type="range" id="partSlider" min="100" max="2000" step="50" value="600">
</div>
<div>
<div class="subtitle">Display</div>
<div style="display:flex;gap:4px;margin-top:4px">
<button class="s-btn" style="flex:1" id="showSpeedBtn">Speed</button>
<button class="s-btn" style="flex:1" id="showDyeBtn">Dye</button>
</div>
</div>
<div>
<div class="subtitle">Obstacle</div>
<div style="display:flex;gap:4px;margin-top:4px">
<button class="s-btn active" style="flex:1" id="obsOnBtn">On</button>
<button class="s-btn" style="flex:1" id="obsOffBtn">Off</button>
</div>
</div>
<div>
<div class="subtitle">Palette</div>
<div class="palette-row" id="paletteRow"></div>
</div>
<div class="info-row">
Particles: <strong id="particleCount">0</strong>
</div>
<button class="s-btn" id="resetBtn">Reset</button>
</div>
</div>
<script>
// ─── palette ───
const palettes = [
{ name: 'neon', stops: [
[0,0,0], [10,10,50], [20,40,120], [60,120,200],
[100,200,220], [200,220,80], [240,140,40], [200,40,80], [100,10,60]
]},
{ name: 'fire', stops: [
[0,0,0], [40,10,0], [100,20,0], [180,60,0],
[240,140,20], [255,220,80], [255,255,200]
]},
{ name: 'ocean', stops: [
[0,0,10], [5,20,60], [10,60,120], [40,120,180],
[80,180,200], [160,220,220], [200,240,240]
]},
{ name: 'psych', stops: [
[0,0,0], [80,0,80], [200,0,100], [255,100,0],
[255,255,0], [0,200,100], [0,100,200], [80,0,120]
]},
];
let currentPalette = 0;
function buildPalette(name) {
const p = palettes.find(x => x.name === name) || palettes[0];
const arr = [];
const segs = p.stops.length - 1;
for (let i = 0; i < 256; i++) {
const t = i / 255;
const seg = Math.min(segs - 1, Math.floor(t * segs));
const f = (t * segs) - seg;
const a = p.stops[seg];
const b = p.stops[seg + 1];
arr.push([
Math.round(a[0] + (b[0] - a[0]) * f),
Math.round(a[1] + (b[1] - a[1]) * f),
Math.round(a[2] + (b[2] - a[2]) * f),
]);
}
return arr;
}
let palette = buildPalette('neon');
function getPaletteColor(t) {
const i = Math.max(0, Math.min(255, Math.floor(t * 255)));
return palette[i];
}
function setPalette(name) {
const idx = palettes.findIndex(x => x.name === name);
if (idx >= 0) currentPalette = idx;
palette = buildPalette(name);
document.querySelectorAll('#paletteRow .palette-swatch').forEach(s => {
s.classList.toggle('active', s.dataset.name === name);
});
}
// ─── key state ───
const keyState = {};
document.addEventListener('keydown', (e) => { keyState[e.key.toLowerCase()] = true; });
document.addEventListener('keyup', (e) => { keyState[e.key.toLowerCase()] = false; });
// ─── aero sim ───
const canvas = document.getElementById('aeroCanvas');
const ctx = canvas.getContext('2d');
const offscreen = document.createElement('canvas');
const offCtx = offscreen.getContext('2d');
const N = 160;
const S = N + 2;
const IX = (i, j) => i + j * S;
let u = new Float32Array(S * S);
let v = new Float32Array(S * S);
let d = new Float32Array(S * S);
let u0 = new Float32Array(S * S);
let v0 = new Float32Array(S * S);
let d0 = new Float32Array(S * S);
let visc = 1e-6;
let windSpeed = 3;
let targetParticles = 600;
let showSpeed = true;
let showDye = false;
let obstacleOn = true;
const particles = [];
let running = true;
let animId = null;
let startTime = 0;
const dt = 0.12;
// ─── slider refs ───
const viscSlider = document.getElementById('viscSlider');
const windSlider = document.getElementById('windSlider');
const partSlider = document.getElementById('partSlider');
const viscVal = document.getElementById('viscVal');
const windVal = document.getElementById('windVal');
const partVal = document.getElementById('partVal');
const particleCount = document.getElementById('particleCount');
function updateSliders() {
visc = parseFloat(viscSlider.value) * 1e-6;
windSpeed = parseFloat(windSlider.value);
targetParticles = parseInt(partSlider.value);
viscVal.textContent = visc.toExponential(1);
windVal.textContent = windSpeed.toFixed(1);
partVal.textContent = targetParticles;
}
viscSlider.addEventListener('input', updateSliders);
windSlider.addEventListener('input', updateSliders);
partSlider.addEventListener('input', updateSliders);
// ─── palette swatches ───
(function buildPaletteSwatches() {
const row = document.getElementById('paletteRow');
palettes.forEach((p) => {
const swatch = document.createElement('div');
swatch.className = 'palette-swatch' + (p.name === 'neon' ? ' active' : '');
const col = buildPalette(p.name);
const avg = col[Math.floor(col.length * 0.6)];
swatch.style.background = `rgb(${avg[0]},${avg[1]},${avg[2]})`;
swatch.title = p.name;
swatch.dataset.name = p.name;
swatch.addEventListener('click', () => setPalette(p.name));
row.appendChild(swatch);
});
})();
// ─── display mode ───
document.getElementById('showSpeedBtn').addEventListener('click', () => {
showSpeed = true; showDye = false;
document.getElementById('showSpeedBtn').classList.add('active');
document.getElementById('showDyeBtn').classList.remove('active');
});
document.getElementById('showDyeBtn').addEventListener('click', () => {
showSpeed = false; showDye = true;
document.getElementById('showDyeBtn').classList.add('active');
document.getElementById('showSpeedBtn').classList.remove('active');
});
// ─── obstacle toggle ───
document.getElementById('obsOnBtn').addEventListener('click', () => { obstacleOn = true;
document.getElementById('obsOnBtn').classList.add('active');
document.getElementById('obsOffBtn').classList.remove('active');
});
document.getElementById('obsOffBtn').addEventListener('click', () => { obstacleOn = false;
document.getElementById('obsOffBtn').classList.add('active');
document.getElementById('obsOnBtn').classList.remove('active');
});
// ─── fluid core ───
function setBnd(b, x) {
for (let i = 1; i <= N; i++) {
x[IX(0, i)] = b === 1 ? -x[IX(1, i)] : x[IX(1, i)];
x[IX(N+1, i)] = b === 1 ? -x[IX(N, i)] : x[IX(N, i)];
x[IX(i, 0)] = b === 2 ? -x[IX(i, 1)] : x[IX(i, 1)];
x[IX(i, N+1)] = b === 2 ? -x[IX(i, N)] : x[IX(i, N)];
}
x[IX(0, 0)] = 0.5 * (x[IX(1, 0)] + x[IX(0, 1)]);
x[IX(0, N+1)] = 0.5 * (x[IX(1, N+1)] + x[IX(0, N)]);
x[IX(N+1, 0)] = 0.5 * (x[IX(N, 0)] + x[IX(N+1, 1)]);
x[IX(N+1, N+1)] = 0.5 * (x[IX(N, N+1)] + x[IX(N+1, N)]);
}
function linSolve(b, x, x0, a, c, iter) {
const cRecip = 1 / c;
for (let k = 0; k < iter; k++) {
for (let j = 1; j <= N; j++) {
for (let i = 1; i <= N; i++) {
x[IX(i,j)] = (x0[IX(i,j)] + a * (
x[IX(i-1,j)] + x[IX(i+1,j)] + x[IX(i,j-1)] + x[IX(i,j+1)]
)) * cRecip;
}
}
setBnd(b, x);
}
}
function diffuse(b, x, x0, vsc) {
const a = dt * vsc * N * N;
linSolve(b, x, x0, a, 1 + 4 * a, 20);
}
function advect(b, d, d0, u, v) {
const dt0 = dt * N;
for (let j = 1; j <= N; j++) {
for (let i = 1; i <= N; i++) {
let x = i - dt0 * u[IX(i,j)];
let y = j - dt0 * v[IX(i,j)];
if (x < 0.5) x = 0.5; if (x > N + 0.5) x = N + 0.5;
if (y < 0.5) y = 0.5; if (y > N + 0.5) y = N + 0.5;
const i0 = Math.floor(x); const i1 = i0 + 1;
const j0 = Math.floor(y); const j1 = j0 + 1;
const s1 = x - i0; const s0 = 1 - s1;
const t1 = y - j0; const t0 = 1 - t1;
d[IX(i,j)] = s0 * (t0 * d0[IX(i0,j0)] + t1 * d0[IX(i0,j1)])
+ s1 * (t0 * d0[IX(i1,j0)] + t1 * d0[IX(i1,j1)]);
}
}
setBnd(b, d);
}
function project(u, v, p, div) {
for (let j = 1; j <= N; j++) {
for (let i = 1; i <= N; i++) {
div[IX(i,j)] = -0.5 * (
u[IX(i+1,j)] - u[IX(i-1,j)] + v[IX(i,j+1)] - v[IX(i,j-1)]
) / N;
p[IX(i,j)] = 0;
}
}
setBnd(0, div); setBnd(0, p);
linSolve(0, p, div, 1, 4, 20);
for (let j = 1; j <= N; j++) {
for (let i = 1; i <= N; i++) {
u[IX(i,j)] -= 0.5 * N * (p[IX(i+1,j)] - p[IX(i-1,j)]);
v[IX(i,j)] -= 0.5 * N * (p[IX(i,j+1)] - p[IX(i,j-1)]);
}
}
setBnd(1, u); setBnd(2, v);
}
function step() {
diffuse(1, u0, u, visc);
diffuse(2, v0, v, visc);
project(u0, v0, u, v);
advect(1, u, u0, u0, v0);
advect(2, v, v0, u0, v0);
project(u, v, u0, v0);
d0.set(d);
diffuse(0, d0, d, 1e-7);
const tmp = d; d = d0; d0 = tmp;
advect(0, d, d0, u, v);
for (let i = 0; i < S * S; i++) d[i] *= 0.995;
}
function applyInflow() {
for (let j = 1; j <= N; j++) {
u[IX(1, j)] = windSpeed;
d[IX(1, j)] = Math.max(d[IX(1, j)], 0.3 + Math.random() * 0.2);
}
}
function applyObstacle() {
if (!obstacleOn) return;
const cx = N / 2;
const cy = N / 2;
const r = N * 0.08;
for (let j = 1; j <= N; j++) {
for (let i = 1; i <= N; i++) {
const dist = Math.sqrt((i - cx) ** 2 + (j - cy) ** 2);
if (dist <= r) {
u[IX(i, j)] = 0;
v[IX(i, j)] = 0;
d[IX(i, j)] = 0;
}
}
}
}
// ─── particles ───
function spawnParticle() {
return {
x: Math.random() * 0.1,
y: 0.1 + Math.random() * 0.8,
age: 0,
};
}
function updateParticles() {
while (particles.length < targetParticles) {
particles.push(spawnParticle());
}
while (particles.length > targetParticles) {
particles.pop();
}
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.age++;
const gi = Math.floor(p.x * N) + 1;
const gj = Math.floor(p.y * N) + 1;
const ig = Math.min(N, Math.max(1, gi));
const jg = Math.min(N, Math.max(1, gj));
p.x += u[IX(ig, jg)] * dt / N;
p.y += v[IX(ig, jg)] * dt / N;
if (p.x > 1 || p.x < 0 || p.y > 1 || p.y < 0 || p.age > 500) {
particles.splice(i, 1);
}
}
}
// ─── render ───
function render() {
const w = canvas.width;
const h = canvas.height;
if (w === 0 || h === 0) return;
const paletteShift = (performance.now() - startTime) * 0.0001;
const imgData = offCtx.createImageData(N, N);
const data = imgData.data;
let maxVel = 0.001;
if (showSpeed) {
for (let j = 1; j <= N; j++) {
for (let i = 1; i <= N; i++) {
const vel = Math.sqrt(u[IX(i,j)] ** 2 + v[IX(i,j)] ** 2);
if (vel > maxVel) maxVel = vel;
}
}
}
for (let j = 0; j < N; j++) {
for (let i = 0; i < N; i++) {
const idx = (j * N + i) * 4;
let val;
if (showSpeed) {
const vel = Math.sqrt(u[IX(i+1, j+1)] ** 2 + v[IX(i+1, j+1)] ** 2);
val = Math.min(vel / maxVel, 1);
} else {
val = Math.min(d[IX(i+1, j+1)] * 0.5, 0.95);
}
const t = (val + paletteShift) % 1.0;
const c = getPaletteColor(t);
data[idx] = c[0];
data[idx + 1] = c[1];
data[idx + 2] = c[2];
data[idx + 3] = 255;
}
}
if (obstacleOn) {
const cx = N / 2;
const cy = N / 2;
const r = N * 0.08;
for (let j = 0; j < N; j++) {
for (let i = 0; i < N; i++) {
const dist = Math.sqrt((i - cx) ** 2 + (j - cy) ** 2);
if (dist <= r + 0.5) {
const idx = (j * N + i) * 4;
data[idx] = 30; data[idx+1] = 30; data[idx+2] = 40;
}
}
}
}
offCtx.putImageData(imgData, 0, 0);
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = '#05050a';
ctx.fillRect(0, 0, w, h);
ctx.imageSmoothingEnabled = true;
ctx.drawImage(offscreen, 0, 0, w, h);
// draw particles
ctx.fillStyle = 'rgba(200,220,255,0.6)';
const sx = w / N;
const sy = h / N;
for (const p of particles) {
const px = p.x * N * sx;
const py = p.y * N * sy;
const alpha = Math.min(1, 1 - p.age / 500);
ctx.globalAlpha = alpha * 0.7;
ctx.beginPath();
ctx.arc(px, py, 1.5, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
particleCount.textContent = particles.length;
}
function resizeSim() {
const wrap = document.querySelector('.canvas-wrap');
const rect = wrap.getBoundingClientRect();
let size = Math.floor(Math.min(rect.width, rect.height, 800));
if (size < 50) size = 400;
canvas.width = size;
canvas.height = size;
offscreen.width = N;
offscreen.height = N;
}
function loop() {
if (!running) return;
applyInflow();
step();
applyObstacle();
updateParticles();
render();
animId = requestAnimationFrame(loop);
}
function reset() {
u.fill(0); v.fill(0); d.fill(0);
u0.fill(0); v0.fill(0); d0.fill(0);
particles.length = 0;
}
// ─── events ───
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) / rect.width;
const my = (e.clientY - rect.top) / rect.height;
const gi = Math.floor(mx * N) + 1;
const gj = Math.floor(my * N) + 1;
const ig = Math.min(N, Math.max(1, gi));
const jg = Math.min(N, Math.max(1, gj));
u[IX(ig, jg)] += 5;
v[IX(ig, jg)] += 5;
});
document.getElementById('resetBtn').addEventListener('click', reset);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') window.location.href = 'index.html';
});
window.addEventListener('resize', resizeSim);
// ─── start ───
updateSliders();
resizeSim();
startTime = performance.now();
loop();
</script>
</body>
</html>