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;
|
* Ambient background: "Contour Field"
|
||||||
let screenWidth = window.innerWidth + 10;
|
*
|
||||||
let screenHeight = window.innerHeight + 10;
|
* 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;
|
var container = document.getElementById("bg");
|
||||||
const MAX_DIST_SQUARED = MAX_DIST * MAX_DIST;
|
if (!container) return;
|
||||||
|
|
||||||
class Ball {
|
var canvas = document.createElement("canvas");
|
||||||
constructor(x, y, size, speed, angle) {
|
canvas.setAttribute("aria-hidden", "true");
|
||||||
this.x = x;
|
container.appendChild(canvas);
|
||||||
this.y = y;
|
var ctx = canvas.getContext("2d");
|
||||||
this.size = size;
|
if (!ctx) return;
|
||||||
this.speed = speed;
|
|
||||||
this.angle = angle;
|
var reduceMotion =
|
||||||
this.calcChange();
|
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() {
|
function fade(t) {
|
||||||
const radians = (this.angle * Math.PI) / 180
|
return t * t * t * (t * (t * 6 - 15) + 10);
|
||||||
this.xSpeed = this.speed * Math.sin(radians);
|
|
||||||
this.ySpeed = this.speed * Math.cos(radians);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
function noise2D(x, y) {
|
||||||
this.x += this.xSpeed;
|
var xi = Math.floor(x);
|
||||||
this.y += this.ySpeed;
|
var yi = Math.floor(y);
|
||||||
if (this.x > screenWidth) {
|
var xf = x - xi;
|
||||||
this.x -= screenWidth;
|
var yf = y - yi;
|
||||||
} else if (this.x < 0) {
|
var u = fade(xf);
|
||||||
this.x += screenWidth;
|
var v = fade(yf);
|
||||||
}
|
var n00 = lattice(xi, yi);
|
||||||
if (this.y > screenHeight) {
|
var n10 = lattice(xi + 1, yi);
|
||||||
this.y -= screenHeight;
|
var n01 = lattice(xi, yi + 1);
|
||||||
} else if (this.y < 0) {
|
var n11 = lattice(xi + 1, yi + 1);
|
||||||
this.y += screenHeight;
|
var nx0 = n00 + (n10 - n00) * u;
|
||||||
}
|
var nx1 = n01 + (n11 - n01) * u;
|
||||||
this.draw();
|
return nx0 + (nx1 - nx0) * v;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
// Two-octave fractal sum, warped slightly so the field folds and
|
||||||
stroke(200, 100);
|
// breathes rather than just sliding sideways.
|
||||||
strokeWeight(2);
|
var FREQ_1 = 1 / 460; // px per full noise cycle, octave 1
|
||||||
fill(0);
|
var FREQ_2 = FREQ_1 * 2.3; // octave 2
|
||||||
ellipse(this.x, this.y, this.size, this.size);
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// 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
|
||||||
|
];
|
||||||
|
|
||||||
|
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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setup() {
|
// ---------------------------------------------------------------
|
||||||
frameRate(15);
|
// Layout state
|
||||||
const pixels = screenHeight * screenWidth;
|
// ---------------------------------------------------------------
|
||||||
const canvas = createCanvas(screenWidth, screenHeight);
|
var dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 2));
|
||||||
canvas.parent('bg');
|
var width = 0;
|
||||||
for (let i = 0; i < pixels * density; i++) {
|
var height = 0;
|
||||||
balls.push(new Ball(
|
var cellSize = 28;
|
||||||
random(screenWidth),
|
var cols = 0;
|
||||||
random(screenHeight),
|
var rows = 0;
|
||||||
random(6) + 3,
|
var grid = null; // Float32Array of (cols+1)*(rows+1)
|
||||||
Math.exp(random(4) + 3) / 1000 + 1,
|
|
||||||
random(360)
|
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
|
||||||
stroke(255);
|
|
||||||
|
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 windowResized() {
|
function sampleGrid(driftX, driftY, warpX, warpY) {
|
||||||
screenWidth = window.innerWidth + 10;
|
var idx = 0;
|
||||||
screenHeight = window.innerHeight + 10;
|
for (var ry = 0; ry <= rows; ry++) {
|
||||||
resizeCanvas(screenWidth, screenHeight);
|
var y = ry * cellSize;
|
||||||
}
|
for (var rx = 0; rx <= cols; rx++) {
|
||||||
|
var x = rx * cellSize;
|
||||||
function draw() {
|
grid[idx++] = fieldValue(x, y, driftX, driftY, warpX, warpY);
|
||||||
background(24);
|
|
||||||
|
|
||||||
for (let i = 0; i < balls.length; i++) {
|
|
||||||
balls[i].update();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw connection lines with additive blending so overlaps brighten
|
|
||||||
blendMode(ADD);
|
|
||||||
strokeWeight(2);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
line(a.x, a.y, b.x, b.y);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 -->
|
<!-- Resource hints for performance -->
|
||||||
<link rel="preconnect" href="https://www.googletagmanager.com" />
|
<link rel="preconnect" href="https://www.googletagmanager.com" />
|
||||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
|
|
||||||
<meta name="description" content="{{ var['description'] }}" />
|
<meta name="description" content="{{ var['description'] }}" />
|
||||||
<meta property="og:title" content="Andrew Simonson" />
|
<meta property="og:title" content="Andrew Simonson" />
|
||||||
<meta property="og:description" content="{{ var['description'] }}" />
|
<meta property="og:description" content="{{ var['description'] }}" />
|
||||||
@@ -76,7 +75,6 @@
|
|||||||
<link rel="canonical" href="{{ request.url_root | trim('/') }}{{ var['canonical'] }}" />
|
<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/responsive.js') }}"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/idler.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>
|
<title>{{ var['title'] }}</title>
|
||||||
</head>
|
</head>
|
||||||
{% block header %}
|
{% block header %}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
<span>{{ f.summary or f.tagline }}</span>
|
<span>{{ f.summary or f.tagline }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section" aria-labelledby="site-heading">
|
<section class="section" aria-labelledby="site-heading">
|
||||||
|
|||||||
Reference in New Issue
Block a user