Files
blog/content/webapps/project_mana.html
2026-02-03 17:31:38 +05:30

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>