mirror of
https://github.com/lavafroth/lavafroth.github.io.git
synced 2026-06-09 12:11:21 -03:00
413 lines
12 KiB
HTML
413 lines
12 KiB
HTML
<div id="stage">
|
|
</div>
|
|
<style>
|
|
canvas {
|
|
background: transparent;
|
|
border-radius: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
</style>
|
|
<script type="frag" id="evolve-vert">
|
|
varying vec2 vUv;
|
|
|
|
void main() {
|
|
vUv = uv;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
vUv = gl_Position.xy / gl_Position.w;
|
|
vUv = (vUv + 1.0) * 0.5;
|
|
}
|
|
</script>
|
|
<script type="frag" id="evolve-frag">
|
|
varying vec2 vUv;
|
|
|
|
uniform sampler2D gbufferMask;
|
|
uniform sampler2D initBufferMask;
|
|
|
|
void main() {
|
|
gl_FragColor = vec4(texture(initBufferMask, vUv));
|
|
}
|
|
|
|
</script>
|
|
<script type="frag" id="outline-frag">
|
|
varying vec2 vUv;
|
|
varying vec2 clipMeshCenter;
|
|
varying vec2 glPos;
|
|
|
|
uniform float time;
|
|
uniform vec2 viewportSize;
|
|
|
|
#define LINE_WEIGHT 2.0
|
|
|
|
uniform sampler2D gbufferMask;
|
|
|
|
void main() {
|
|
float dx = (1.0 / viewportSize.x) * LINE_WEIGHT;
|
|
float dy = (1.0 / viewportSize.y) * LINE_WEIGHT;
|
|
|
|
vec2 uvCenter = vUv;
|
|
vec2 uvRight = vec2(uvCenter.x + dx, uvCenter.y);
|
|
vec2 uvLeft = vec2(uvCenter.x - dx, uvCenter.y);
|
|
vec2 uvTop = vec2(uvCenter.x, uvCenter.y - dy);
|
|
vec2 uvTopRight = vec2(uvCenter.x + dx, uvCenter.y - dy);
|
|
vec2 uvDown = vec2(uvCenter.x, uvCenter.y + dy);
|
|
vec2 uvDownLeft = vec2(uvCenter.x - dx, uvCenter.y + dy);
|
|
|
|
float mCenter = texture(gbufferMask, uvCenter).r;
|
|
float mTop = texture(gbufferMask, uvTop).r;
|
|
float mRight = texture(gbufferMask, uvRight).r;
|
|
float mTopRight = texture(gbufferMask, uvTopRight).r;
|
|
float mLeft = texture(gbufferMask, uvLeft).r;
|
|
float mDown = texture(gbufferMask, uvDown).r;
|
|
float mDownLeft = texture(gbufferMask, uvDownLeft).r;
|
|
|
|
float dT = abs(mCenter - mTop);
|
|
float dR = abs(mCenter - mRight);
|
|
float dTR = abs(mCenter - mTopRight);
|
|
float dD = abs(mCenter - mDown);
|
|
float dL = abs(mCenter - mLeft);
|
|
float dDL = abs(mCenter - mDownLeft);
|
|
|
|
float delta = 0.0;
|
|
delta = max(delta, dT);
|
|
delta = max(delta, dR);
|
|
delta = max(delta, dTR);
|
|
delta = max(delta, dD);
|
|
delta = max(delta, dL);
|
|
delta = max(delta, dDL);
|
|
|
|
float threshold = 0.0;
|
|
float isOutline = clamp((delta * 2.0) - threshold, 0.0, 1.0);
|
|
|
|
vec4 outline = vec4(isOutline);
|
|
gl_FragColor = outline;
|
|
}
|
|
</script>
|
|
<script type="vert" id="outline-vert">
|
|
varying vec2 vUv;
|
|
uniform vec3 meshCenter;
|
|
varying vec2 clipMeshCenter;
|
|
uniform float time;
|
|
varying vec2 glPos;
|
|
|
|
void main() {
|
|
clipMeshCenter = (projectionMatrix * modelViewMatrix * vec4(meshCenter, 1.0)).xy;
|
|
vUv = uv;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
vUv = gl_Position.xy / gl_Position.w;
|
|
vUv = (vUv + 1.0) * 0.5;
|
|
glPos = gl_Position.xy;
|
|
}
|
|
</script>
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.172.0/build/three.module.js",
|
|
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.172.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
|
|
|
|
const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
let renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
|
|
let width = document.querySelector('#stage').offsetWidth;
|
|
let height = Math.round(9/16 * width);
|
|
renderer.setSize(width, height);
|
|
document.querySelector('#stage').appendChild(renderer.domElement);
|
|
|
|
function get(path) {
|
|
return document.querySelector('#' + path).textContent;
|
|
};
|
|
|
|
const scene = new THREE.Scene();
|
|
const solidScene = new THREE.Scene();
|
|
const maskScene = new THREE.Scene();
|
|
const evolveScene = new THREE.Scene();
|
|
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
|
|
const color = 0xFFFFFF;
|
|
let ambientLight = new THREE.AmbientLight(color, dark ? 1: 10);
|
|
solidScene.add(ambientLight);
|
|
let light = new THREE.PointLight(color, 200);
|
|
light.position.set(10, 10, 15);
|
|
solidScene.add(light);
|
|
camera.position.set(6,8,14);
|
|
|
|
let buffer = new THREE.WebGLRenderTarget(width, height, {format: THREE.RGBAFormat})
|
|
let outlineBuffer = new THREE.WebGLRenderTarget(width, height, {format: THREE.RGBAFormat})
|
|
|
|
const orbit = new OrbitControls(camera, renderer.domElement);
|
|
orbit.update();
|
|
|
|
const geometry = new THREE.TorusGeometry( 10, 3, 20, 100 );
|
|
geometry.translate(2, 2, 2);
|
|
|
|
const shadowMesh = new THREE.Mesh(geometry);
|
|
const uniforms = {
|
|
gbufferMask: { value: buffer.texture },
|
|
viewportSize: { value: new THREE.Vector2(width, height) },
|
|
}
|
|
|
|
const material = new THREE.ShaderMaterial({
|
|
vertexShader: get('outline-vert'),
|
|
fragmentShader: get('outline-frag'),
|
|
uniforms,
|
|
transparent: true,
|
|
});
|
|
|
|
let solidMeshMaterial = new THREE.MeshPhysicalMaterial({
|
|
color: dark ? 0xaaeadb : 0xffffff,
|
|
metalness: 0.8,
|
|
clearcoat: 0.4,
|
|
clearcoatRoughness: 0.1,
|
|
});
|
|
|
|
const solidMesh = new THREE.Mesh(
|
|
geometry,
|
|
solidMeshMaterial,
|
|
);
|
|
|
|
const mesh = new THREE.Mesh(
|
|
geometry,
|
|
material
|
|
);
|
|
solidScene.add(solidMesh);
|
|
scene.add(mesh);
|
|
maskScene.add(shadowMesh);
|
|
|
|
|
|
const evoUniforms = {
|
|
initBufferMask: { value: null },
|
|
}
|
|
|
|
const evoMaterial = new THREE.ShaderMaterial({
|
|
// this is a copy shader
|
|
vertexShader: get('evolve-vert'),
|
|
fragmentShader: get('evolve-frag'),
|
|
uniforms: evoUniforms,
|
|
transparent: true,
|
|
});
|
|
|
|
const evolveMesh = new THREE.Mesh(
|
|
geometry,
|
|
evoMaterial
|
|
);
|
|
|
|
evolveScene.add(evolveMesh)
|
|
|
|
function continuity(bitmap, width, height) {
|
|
const visited = Array.from({length: height}, () => Array(width).fill(false));
|
|
|
|
function valid(row, col) {
|
|
return row >= 0 && row < height && col >= 0 && col <= width
|
|
}
|
|
|
|
function valid_1(row, col) {
|
|
return valid(row, col) ? 1 : 0
|
|
}
|
|
|
|
// for a point to be on the screen edge, it must have at least three invalid neighbors.
|
|
// It must have at least six valid neighbors.
|
|
function isSentinel(row, col) {
|
|
return (
|
|
valid_1(row - 1, col - 1) +
|
|
valid_1(row - 1, col) +
|
|
valid_1(row - 1, col + 1) +
|
|
|
|
valid_1(row, col - 1) +
|
|
valid_1(row, col + 1) +
|
|
|
|
valid_1(row + 1, col - 1) +
|
|
valid_1(row + 1, col) +
|
|
valid_1(row + 1, col + 1)
|
|
) < 6
|
|
}
|
|
|
|
var sentinels = [];
|
|
var cyclic = [];
|
|
|
|
function dfs_queue(rootRow, rootCol) {
|
|
var stack = [];
|
|
stack.push([rootRow, rootCol]);
|
|
|
|
var steps = 0;
|
|
|
|
for(; ; steps++) {
|
|
let p = stack.pop();
|
|
if (p === undefined) {
|
|
return
|
|
}
|
|
let [row, col] = p
|
|
if (isSentinel(row, col)) {
|
|
sentinels.push([row, col]);
|
|
return
|
|
}
|
|
|
|
if (steps > 0 && row == rootRow && col == rootCol) {
|
|
cyclic.push([row, col])
|
|
return
|
|
}
|
|
|
|
if (!valid(row, col) || visited[row][col]) {
|
|
return
|
|
}
|
|
|
|
// is this point switched off?
|
|
visited[row][col] = true;
|
|
var point = 4 * (row * width + col);
|
|
if (bitmap[point] == 0 && bitmap[point+1] == 0 && bitmap[point+2] == 0) {
|
|
continue;
|
|
}
|
|
|
|
stack.push([row + 1, col])
|
|
stack.push([row, col + 1])
|
|
stack.push([row - 1, col])
|
|
stack.push([row, col - 1])
|
|
|
|
}
|
|
}
|
|
|
|
for (let row = 0; row < height; row++) {
|
|
for(let col = 0; col < width; col++) {
|
|
if (visited[row][col]) {
|
|
continue;
|
|
}
|
|
var point = 4 * (row * width + col);
|
|
if (bitmap[point] == 1 && bitmap[point+1] == 1 && bitmap[point+2] == 1) {
|
|
dfs_queue(row, col);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Always prefer the sentinels to the cyclics
|
|
// since walking back a line also ends up in a cycle.
|
|
// Start waveforms from the sentinels (endpoints),
|
|
// mark all the visited points. If there remain unvisited
|
|
// points, those are legitimately parts of cyclic paths.
|
|
return sentinels.concat(cyclic);
|
|
}
|
|
|
|
var durationInSeconds = 5;
|
|
|
|
// @param {Float32Array[]} points
|
|
// @param {Uint32Array} allThePixels
|
|
// Number all the points as we stumble along the outline.
|
|
// Akin to a wavefront. The pixels switched on (value = 1)
|
|
// touching the wavefront at time t will have a value t + 2
|
|
//
|
|
// This way we can quickly zero out all the pixels below a
|
|
// threshold when timing the animation.
|
|
function dijkstraNumber(points, buf) {
|
|
var longestStrand = 2;
|
|
points.forEach((point) => {
|
|
longestStrand = Math.max(longestStrand, dijkstraPropagate(point, buf, 2))
|
|
})
|
|
return longestStrand
|
|
}
|
|
|
|
function dijkstraPropagate(point, buf, value) {
|
|
var queue = [[point]];
|
|
for (; ;) {
|
|
let neighbors = queue.pop()
|
|
if (neighbors === undefined) {
|
|
return value
|
|
}
|
|
neighbors.forEach(([row, col]) => {
|
|
let pos = 4 * (row * buffer.width + col);
|
|
if (buf[pos] != 1) {
|
|
return
|
|
}
|
|
|
|
buf[pos] = value;
|
|
buf[pos+1] = value;
|
|
buf[pos+2] = value;
|
|
buf[pos+3] = value;
|
|
value += 1
|
|
|
|
// package all the neighboring points and
|
|
// push them onto the stack
|
|
queue.push([
|
|
[row + 1, col],
|
|
[row + 1, col + 1],
|
|
[row, col + 1],
|
|
[row - 1, col + 1],
|
|
[row - 1, col],
|
|
[row - 1, col - 1],
|
|
[row, col - 1],
|
|
[row + 1, col - 1]
|
|
])
|
|
})
|
|
}
|
|
}
|
|
|
|
const clock = new THREE.Clock();
|
|
var longestPixelStrand = 0;
|
|
var init = true;
|
|
const allThePixels = new Uint8Array(buffer.width * buffer.height * 4);
|
|
const dijkstraBuffer = new Uint32Array(buffer.width * buffer.height * 4);
|
|
function animate() {
|
|
|
|
if (init) {
|
|
renderer.setRenderTarget(buffer);
|
|
renderer.render(maskScene, camera);
|
|
|
|
renderer.setRenderTarget(outlineBuffer);
|
|
renderer.render(scene, camera);
|
|
|
|
renderer.readRenderTargetPixels(outlineBuffer, 0, 0, buffer.width, buffer.height, allThePixels);
|
|
for (let i = 0; i < allThePixels.length; i++) {
|
|
dijkstraBuffer[i] = allThePixels[i] == 255 ? 1 : 0;
|
|
}
|
|
|
|
let points = continuity(dijkstraBuffer, width, height)
|
|
longestPixelStrand = dijkstraNumber(points, dijkstraBuffer)
|
|
|
|
let initBuffer = new Uint8Array(buffer.width * buffer.height * 4);
|
|
points.forEach((point) => {
|
|
let pos = 4 * (point[0] * buffer.width + point[1]);
|
|
initBuffer[pos] = 255;
|
|
initBuffer[pos+1] = 255;
|
|
initBuffer[pos+2] = 255;
|
|
initBuffer[pos+3] = 255;
|
|
})
|
|
|
|
let ephemeralTex = new THREE.DataTexture(initBuffer, width, height);
|
|
ephemeralTex.needsUpdate = true;
|
|
evoUniforms.initBufferMask.value = ephemeralTex;
|
|
init = false;
|
|
}
|
|
let fractionAnimated = (clock.getElapsedTime() % durationInSeconds) / durationInSeconds;
|
|
let pixelsAnimated = Math.round(longestPixelStrand * fractionAnimated);
|
|
let initBuffer = new Uint8Array(buffer.width * buffer.height * 4);
|
|
for (let row = 0; row < height; row++) {
|
|
for(let col=0; col<width; col++) {
|
|
var point = 4 * (row * width + col);
|
|
if (dijkstraBuffer[point] > 1 && dijkstraBuffer[point] < pixelsAnimated) {
|
|
initBuffer[point] = dark ? 255: 22;
|
|
initBuffer[point+1] = dark ? 255: 22;
|
|
initBuffer[point+2] = dark ? 255 : 22;
|
|
initBuffer[point+3] = 255;
|
|
}
|
|
}
|
|
}
|
|
let ephemeralTex = new THREE.DataTexture(initBuffer, width, height);
|
|
ephemeralTex.needsUpdate = true;
|
|
evoUniforms.initBufferMask.value = ephemeralTex;
|
|
|
|
renderer.setRenderTarget(null);
|
|
renderer.render(solidScene, camera);
|
|
renderer.autoClear = false;
|
|
renderer.clearDepth();
|
|
renderer.render(evolveScene, camera);
|
|
renderer.autoClear = true;
|
|
}
|
|
|
|
renderer.setAnimationLoop(animate);
|
|
|
|
orbit.addEventListener('change', function() {
|
|
init = true;
|
|
})
|
|
</script>
|