mirror of
https://github.com/asimonson1125/asimonson1125.github.io.git
synced 2026-09-22 11:35:33 -05:00
contour map
This commit is contained in:
@@ -1,105 +1,293 @@
|
||||
const balls = [];
|
||||
const density = 0.00005;
|
||||
let screenWidth = window.innerWidth + 10;
|
||||
let screenHeight = window.innerHeight + 10;
|
||||
/**
|
||||
* Ambient background: "Contour Field"
|
||||
*
|
||||
* Thin nested contour lines trace level curves of one continuous,
|
||||
* slowly-drifting 2D value-noise surface, marched with a classic
|
||||
* marching-squares pass on a coarse grid. Every line on screen is a
|
||||
* cross-section of the SAME field at a fixed elevation, so neighboring
|
||||
* contours are geometrically correlated (they nest, they never cross,
|
||||
* they bulge and pinch together) -- it reads as one continuous terrain
|
||||
* rather than a scatter of independent strokes. Nods to the site
|
||||
* owner's geospatial / data-surface work (watershed + elevation
|
||||
* surfaces) without being literal about it.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const MAX_DIST = 150;
|
||||
const MAX_DIST_SQUARED = MAX_DIST * MAX_DIST;
|
||||
var container = document.getElementById("bg");
|
||||
if (!container) return;
|
||||
|
||||
class Ball {
|
||||
constructor(x, y, size, speed, angle) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.size = size;
|
||||
this.speed = speed;
|
||||
this.angle = angle;
|
||||
this.calcChange();
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.setAttribute("aria-hidden", "true");
|
||||
container.appendChild(canvas);
|
||||
var ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
var reduceMotion =
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Value noise (hash lattice + smooth interpolation), no dependency.
|
||||
// ---------------------------------------------------------------
|
||||
var PERM_SIZE = 256;
|
||||
var PERM_MASK = PERM_SIZE - 1;
|
||||
var perm = new Uint8Array(PERM_SIZE);
|
||||
(function buildPermutation() {
|
||||
var table = new Uint8Array(PERM_SIZE);
|
||||
for (var i = 0; i < PERM_SIZE; i++) table[i] = i;
|
||||
for (var j = PERM_SIZE - 1; j > 0; j--) {
|
||||
var k = (Math.random() * (j + 1)) | 0;
|
||||
var tmp = table[j];
|
||||
table[j] = table[k];
|
||||
table[k] = tmp;
|
||||
}
|
||||
perm.set(table);
|
||||
})();
|
||||
|
||||
function lattice(ix, iy) {
|
||||
var a = perm[ix & PERM_MASK];
|
||||
return perm[(a + iy) & PERM_MASK] / 255;
|
||||
}
|
||||
|
||||
calcChange() {
|
||||
const radians = (this.angle * Math.PI) / 180
|
||||
this.xSpeed = this.speed * Math.sin(radians);
|
||||
this.ySpeed = this.speed * Math.cos(radians);
|
||||
function fade(t) {
|
||||
return t * t * t * (t * (t * 6 - 15) + 10);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.xSpeed;
|
||||
this.y += this.ySpeed;
|
||||
if (this.x > screenWidth) {
|
||||
this.x -= screenWidth;
|
||||
} else if (this.x < 0) {
|
||||
this.x += screenWidth;
|
||||
}
|
||||
if (this.y > screenHeight) {
|
||||
this.y -= screenHeight;
|
||||
} else if (this.y < 0) {
|
||||
this.y += screenHeight;
|
||||
}
|
||||
this.draw();
|
||||
function noise2D(x, y) {
|
||||
var xi = Math.floor(x);
|
||||
var yi = Math.floor(y);
|
||||
var xf = x - xi;
|
||||
var yf = y - yi;
|
||||
var u = fade(xf);
|
||||
var v = fade(yf);
|
||||
var n00 = lattice(xi, yi);
|
||||
var n10 = lattice(xi + 1, yi);
|
||||
var n01 = lattice(xi, yi + 1);
|
||||
var n11 = lattice(xi + 1, yi + 1);
|
||||
var nx0 = n00 + (n10 - n00) * u;
|
||||
var nx1 = n01 + (n11 - n01) * u;
|
||||
return nx0 + (nx1 - nx0) * v;
|
||||
}
|
||||
|
||||
draw() {
|
||||
stroke(200, 100);
|
||||
strokeWeight(2);
|
||||
fill(0);
|
||||
ellipse(this.x, this.y, this.size, this.size);
|
||||
}
|
||||
}
|
||||
|
||||
function setup() {
|
||||
frameRate(15);
|
||||
const pixels = screenHeight * screenWidth;
|
||||
const canvas = createCanvas(screenWidth, screenHeight);
|
||||
canvas.parent('bg');
|
||||
for (let i = 0; i < pixels * density; i++) {
|
||||
balls.push(new Ball(
|
||||
random(screenWidth),
|
||||
random(screenHeight),
|
||||
random(6) + 3,
|
||||
Math.exp(random(4) + 3) / 1000 + 1,
|
||||
random(360)
|
||||
));
|
||||
}
|
||||
stroke(255);
|
||||
}
|
||||
|
||||
function windowResized() {
|
||||
screenWidth = window.innerWidth + 10;
|
||||
screenHeight = window.innerHeight + 10;
|
||||
resizeCanvas(screenWidth, screenHeight);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
background(24);
|
||||
|
||||
for (let i = 0; i < balls.length; i++) {
|
||||
balls[i].update();
|
||||
// Two-octave fractal sum, warped slightly so the field folds and
|
||||
// breathes rather than just sliding sideways.
|
||||
var FREQ_1 = 1 / 460; // px per full noise cycle, octave 1
|
||||
var FREQ_2 = FREQ_1 * 2.3; // octave 2
|
||||
function fieldValue(x, y, driftX, driftY, warpX, warpY) {
|
||||
var n1 = noise2D((x + driftX) * FREQ_1, (y + driftY) * FREQ_1);
|
||||
var n2 = noise2D(
|
||||
(x + driftX + warpX) * FREQ_2,
|
||||
(y + driftY + warpY) * FREQ_2
|
||||
);
|
||||
var fbm = n1 * 0.68 + n2 * 0.32; // ~[0,1]
|
||||
return fbm * 2 - 1; // ~[-1,1], centered
|
||||
}
|
||||
|
||||
// Draw connection lines with additive blending so overlaps brighten
|
||||
blendMode(ADD);
|
||||
strokeWeight(2);
|
||||
// ---------------------------------------------------------------
|
||||
// Marching squares
|
||||
// Corner bit order: v0=top-left, v1=top-right, v2=bottom-right, v3=bottom-left
|
||||
// Edge order: 0=top, 1=right, 2=bottom, 3=left
|
||||
// ---------------------------------------------------------------
|
||||
var CASE_EDGES = [
|
||||
null, // 0
|
||||
[0, 3], // 1
|
||||
[0, 1], // 2
|
||||
[1, 3], // 3
|
||||
[1, 2], // 4
|
||||
[0, 3, 1, 2], // 5 (saddle, two segments)
|
||||
[0, 2], // 6
|
||||
[2, 3], // 7
|
||||
[2, 3], // 8
|
||||
[0, 2], // 9
|
||||
[0, 1, 2, 3], // 10 (saddle, two segments)
|
||||
[1, 2], // 11
|
||||
[1, 3], // 12
|
||||
[0, 1], // 13
|
||||
[0, 3], // 14
|
||||
null, // 15
|
||||
];
|
||||
|
||||
for (let i = 0; i < balls.length - 1; i++) {
|
||||
const a = balls[i];
|
||||
for (let j = i + 1; j < balls.length; j++) {
|
||||
const b = balls[j];
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
const distSquared = dx * dx + dy * dy;
|
||||
|
||||
if (distSquared < MAX_DIST_SQUARED) {
|
||||
const distance = Math.sqrt(distSquared);
|
||||
if (distance < 75) {
|
||||
stroke(255, 85);
|
||||
} else {
|
||||
const chance = 0.3 ** (((random(0.2) + 0.8) * distance) / MAX_DIST);
|
||||
stroke(255, chance < 0.5 ? 40 : 75);
|
||||
function edgePoint(edge, x, y, cell, v0, v1, v2, v3, threshold) {
|
||||
var t;
|
||||
switch (edge) {
|
||||
case 0: // top: v0 -> v1
|
||||
t = (threshold - v0) / (v1 - v0 || 1e-6);
|
||||
return [x + cell * t, y];
|
||||
case 1: // right: v1 -> v2
|
||||
t = (threshold - v1) / (v2 - v1 || 1e-6);
|
||||
return [x + cell, y + cell * t];
|
||||
case 2: // bottom: v3 -> v2
|
||||
t = (threshold - v3) / (v2 - v3 || 1e-6);
|
||||
return [x + cell * t, y + cell];
|
||||
case 3: // left: v0 -> v3
|
||||
t = (threshold - v0) / (v3 - v0 || 1e-6);
|
||||
return [x, y + cell * t];
|
||||
}
|
||||
line(a.x, a.y, b.x, b.y);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Layout state
|
||||
// ---------------------------------------------------------------
|
||||
var dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 2));
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
var cellSize = 28;
|
||||
var cols = 0;
|
||||
var rows = 0;
|
||||
var grid = null; // Float32Array of (cols+1)*(rows+1)
|
||||
|
||||
var LEVELS = [-0.5, -0.333, -0.167, 0, 0.167, 0.333, 0.5];
|
||||
var BASE_COLOR = "233,230,223"; // --ink
|
||||
var ACCENT_COLOR = "217,100,92"; // --accent-bright, reserved for the zero contour
|
||||
|
||||
function resize() {
|
||||
width = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
canvas.width = Math.round(width * dpr);
|
||||
canvas.height = Math.round(height * dpr);
|
||||
canvas.style.width = width + "px";
|
||||
canvas.style.height = height + "px";
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
var targetCols = 68;
|
||||
cellSize = Math.max(22, width / targetCols);
|
||||
cols = Math.ceil(width / cellSize) + 1;
|
||||
rows = Math.ceil(height / cellSize) + 1;
|
||||
grid = new Float32Array((cols + 1) * (rows + 1));
|
||||
}
|
||||
|
||||
function sampleGrid(driftX, driftY, warpX, warpY) {
|
||||
var idx = 0;
|
||||
for (var ry = 0; ry <= rows; ry++) {
|
||||
var y = ry * cellSize;
|
||||
for (var rx = 0; rx <= cols; rx++) {
|
||||
var x = rx * cellSize;
|
||||
grid[idx++] = fieldValue(x, y, driftX, driftY, warpX, warpY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blendMode(BLEND);
|
||||
}
|
||||
function drawContours() {
|
||||
var stride = cols + 1;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.lineCap = "round";
|
||||
|
||||
for (var li = 0; li < LEVELS.length; li++) {
|
||||
var threshold = LEVELS[li];
|
||||
var isZero = threshold === 0;
|
||||
var depth = Math.abs(threshold) / 0.5; // 0 (center) .. 1 (outer)
|
||||
var alpha = isZero ? 0.5 : 0.3 + 0.16 * (1 - depth);
|
||||
ctx.strokeStyle = isZero
|
||||
? "rgba(" + ACCENT_COLOR + "," + alpha + ")"
|
||||
: "rgba(" + BASE_COLOR + "," + alpha + ")";
|
||||
ctx.lineWidth = isZero ? 1.3 : 1;
|
||||
|
||||
ctx.beginPath();
|
||||
for (var cy = 0; cy < rows; cy++) {
|
||||
var rowOff = cy * stride;
|
||||
var rowOffNext = rowOff + stride;
|
||||
var y = cy * cellSize;
|
||||
for (var cx = 0; cx < cols; cx++) {
|
||||
var x = cx * cellSize;
|
||||
var v0 = grid[rowOff + cx];
|
||||
var v1 = grid[rowOff + cx + 1];
|
||||
var v2 = grid[rowOffNext + cx + 1];
|
||||
var v3 = grid[rowOffNext + cx];
|
||||
|
||||
var caseIndex =
|
||||
(v0 >= threshold ? 1 : 0) |
|
||||
(v1 >= threshold ? 2 : 0) |
|
||||
(v2 >= threshold ? 4 : 0) |
|
||||
(v3 >= threshold ? 8 : 0);
|
||||
|
||||
var edges = CASE_EDGES[caseIndex];
|
||||
if (!edges) continue;
|
||||
|
||||
for (var s = 0; s < edges.length; s += 2) {
|
||||
var p0 = edgePoint(edges[s], x, y, cellSize, v0, v1, v2, v3, threshold);
|
||||
var p1 = edgePoint(
|
||||
edges[s + 1],
|
||||
x,
|
||||
y,
|
||||
cellSize,
|
||||
v0,
|
||||
v1,
|
||||
v2,
|
||||
v3,
|
||||
threshold
|
||||
);
|
||||
ctx.moveTo(p0[0], p0[1]);
|
||||
ctx.lineTo(p1[0], p1[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function render(driftX, driftY, warpX, warpY) {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
sampleGrid(driftX, driftY, warpX, warpY);
|
||||
drawContours();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Animation: slow drift, throttled to a modest frame rate since the
|
||||
// motion itself unfolds over tens of seconds -- no need to sample it
|
||||
// faster than that.
|
||||
// ---------------------------------------------------------------
|
||||
var DRIFT_VX = 3.2; // px/sec
|
||||
var DRIFT_VY = -2.1; // px/sec
|
||||
var startTime = null;
|
||||
var lastFrameTime = 0;
|
||||
var FRAME_INTERVAL = 1000 / 15; // ~15fps grid refresh, plenty for slow drift
|
||||
var rafId = null;
|
||||
|
||||
function frame(now) {
|
||||
rafId = requestAnimationFrame(frame);
|
||||
if (now - lastFrameTime < FRAME_INTERVAL) return;
|
||||
lastFrameTime = now;
|
||||
if (startTime === null) startTime = now;
|
||||
var t = (now - startTime) / 1000;
|
||||
|
||||
var driftX = t * DRIFT_VX;
|
||||
var driftY = t * DRIFT_VY;
|
||||
var warpX = Math.sin(t * 0.013) * 60;
|
||||
var warpY = Math.cos(t * 0.009) * 60;
|
||||
render(driftX, driftY, warpX, warpY);
|
||||
}
|
||||
|
||||
function start() {
|
||||
resize();
|
||||
if (reduceMotion) {
|
||||
render(0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
rafId = requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
var resizeTimer = null;
|
||||
window.addEventListener("resize", function () {
|
||||
if (resizeTimer) clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function () {
|
||||
resize();
|
||||
if (reduceMotion) render(0, 0, 0, 0);
|
||||
}, 150);
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (reduceMotion) return;
|
||||
if (document.hidden) {
|
||||
if (rafId !== null) {
|
||||
cancelAnimationFrame(rafId);
|
||||
rafId = null;
|
||||
}
|
||||
} else if (rafId === null) {
|
||||
lastFrameTime = 0;
|
||||
rafId = requestAnimationFrame(frame);
|
||||
}
|
||||
});
|
||||
|
||||
start();
|
||||
})();
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<!-- Resource hints for performance -->
|
||||
<link rel="preconnect" href="https://www.googletagmanager.com" />
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
|
||||
<meta name="description" content="{{ var['description'] }}" />
|
||||
<meta property="og:title" content="Andrew Simonson" />
|
||||
<meta property="og:description" content="{{ var['description'] }}" />
|
||||
@@ -76,7 +75,6 @@
|
||||
<link rel="canonical" href="{{ request.url_root | trim('/') }}{{ var['canonical'] }}" />
|
||||
<script defer src="{{ url_for('static', filename='js/responsive.js') }}"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/idler.js') }}"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/p5@1.4.1/lib/p5.min.js"></script>
|
||||
<title>{{ var['title'] }}</title>
|
||||
</head>
|
||||
{% block header %}
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
<span>{{ f.summary or f.tagline }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="section" aria-labelledby="site-heading">
|
||||
|
||||
Reference in New Issue
Block a user