stage.js
6.8 kB · javascript · 171 lines
1import * as THREE from 'three';2import { OrbitControls } from 'three/addons/controls/OrbitControls.js';3import { ink, palette, blit, role } from './mrly.js';45export function stage(canvas) {6 const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });7 renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2));8 const scene = new THREE.Scene();9 const eye = new THREE.PerspectiveCamera(38, 1, 0.01, 100);10 const flat = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.01, 100);11 eye.position.set(2.5, 1.9, 2.8);12 flat.position.copy(eye.position);13 let camera = eye;14 const controls = new OrbitControls(camera, canvas);15 controls.enableDamping = true;16 scene.add(new THREE.HemisphereLight(palette.white, ink.line, 1.2));17 const key = new THREE.DirectionalLight(palette.white, 1.8);18 key.position.set(3, 4, 2);19 scene.add(key);20 const rim = new THREE.DirectionalLight(ink.blue, 0.6);21 rim.position.set(-3, -1, -2);22 scene.add(rim);23 const group = new THREE.Group();24 scene.add(group);25 const resize = () => {26 const w = canvas.clientWidth, h = canvas.clientHeight;27 renderer.setSize(w, h, false);28 const aspect = w / h;29 eye.aspect = aspect;30 eye.updateProjectionMatrix();31 const half = 1.7;32 flat.left = -half * aspect;33 flat.right = half * aspect;34 flat.top = half;35 flat.bottom = -half;36 flat.updateProjectionMatrix();37 };38 new ResizeObserver(resize).observe(canvas);39 resize();40 const st = { scene, renderer, controls, group, spin: 0 };41 st.clear = () => {42 for (const child of [...group.children]) {43 group.remove(child);44 child.geometry?.dispose();45 child.material?.map?.dispose();46 child.material?.dispose();47 }48 };49 st.add = (object) => group.add(object);50 st.show = (object) => {51 st.clear();52 group.add(object);53 };54 st.project = (mode) => {55 const next = mode === 'iso' ? flat : eye;56 if (next === camera) return;57 next.position.copy(camera.position);58 next.quaternion.copy(camera.quaternion);59 next.zoom = 1;60 next.updateProjectionMatrix();61 camera = next;62 controls.object = camera;63 controls.update();64 };65 st.view = (x, y, z) => {66 const direction = new THREE.Vector3(x, y, z).normalize();67 camera.position.copy(controls.target).addScaledVector(direction, 4.2);68 camera.zoom = 1;69 camera.updateProjectionMatrix();70 camera.lookAt(controls.target);71 controls.update();72 };73 const frame = () => {74 requestAnimationFrame(frame);75 group.rotation.y += st.spin;76 controls.update();77 renderer.render(scene, camera);78 };79 frame();80 return st;81}8283export function plane(pixels, frame, opacity = 1) {84 const canvas = document.createElement('canvas');85 blit(canvas, pixels);86 const texture = new THREE.CanvasTexture(canvas);87 texture.magFilter = THREE.NearestFilter;88 texture.generateMipmaps = false;89 texture.minFilter = THREE.LinearFilter;90 const { centre: c, u, v, width } = frame;91 const h = width / 2;92 const at = (a, b) => [c[0] + a * u[0] + b * v[0], c[1] + a * u[1] + b * v[1], c[2] + a * u[2] + b * v[2]];93 const [p00, p01, p10, p11] = [at(-h, -h), at(h, -h), at(-h, h), at(h, h)];94 const geometry = new THREE.BufferGeometry();95 geometry.setAttribute('position', new THREE.Float32BufferAttribute([...p00, ...p10, ...p11, ...p00, ...p11, ...p01], 3));96 geometry.setAttribute('uv', new THREE.Float32BufferAttribute([0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1], 2));97 const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true, opacity, alphaTest: 0.5, side: THREE.DoubleSide });98 return new THREE.Mesh(geometry, material);99}100101export function faces(buffer, color = ink.blue, opacity = 1) {102 const data = buffer.subarray(2, 2 + buffer[0]);103 const interleaved = new THREE.InterleavedBuffer(data, 6);104 const geometry = new THREE.BufferGeometry();105 geometry.setAttribute('position', new THREE.InterleavedBufferAttribute(interleaved, 3, 0));106 geometry.setAttribute('normal', new THREE.InterleavedBufferAttribute(interleaved, 3, 3));107 const clear = opacity < 1;108 const material = new THREE.MeshStandardMaterial({ color, roughness: 0.6, metalness: 0.05, transparent: clear, opacity, depthWrite: !clear });109 return new THREE.Mesh(geometry, material);110}111112export function cubes(cells, side, color = ink.orange) {113 const count = cells.length / 3;114 const unit = 2 / side;115 const geometry = new THREE.BoxGeometry(unit * 0.9, unit * 0.9, unit * 0.9);116 const material = new THREE.MeshStandardMaterial({ color, roughness: 0.55 });117 const mesh = new THREE.InstancedMesh(geometry, material, count);118 const m = new THREE.Matrix4();119 for (let i = 0; i < count; i++) {120 m.makeTranslation(121 (cells[3 * i] + 0.5) * unit - 1,122 (cells[3 * i + 1] + 0.5) * unit - 1,123 (cells[3 * i + 2] + 0.5) * unit - 1,124 );125 mesh.setMatrixAt(i, m);126 }127 return mesh;128}129130export function web(nodes, branches, roles, radius) {131 const n = nodes.length / 3;132 const tint = new THREE.Color();133 const hues = role();134 const balls = new THREE.InstancedMesh(new THREE.SphereGeometry(radius, 10, 7), new THREE.MeshStandardMaterial({ roughness: 0.5 }), n);135 for (let i = 0; i < n; i++) balls.setColorAt(i, tint.set(hues[roles ? roles[i] : 2]));136 const colors = new Float32Array(branches.length * 3);137 for (let k = 0; k < branches.length; k += 2) {138 tint.set(roles ? hues[Math.min(roles[branches[k]], roles[branches[k + 1]])] : ink.line);139 colors.set([tint.r, tint.g, tint.b, tint.r, tint.g, tint.b], k * 3);140 }141 const geometry = new THREE.BufferGeometry();142 geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(branches.length * 3), 3));143 geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));144 const lines = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.85 }));145 const m = new THREE.Matrix4();146 const place = (nodes) => {147 const low = [Infinity, Infinity, Infinity], high = [-Infinity, -Infinity, -Infinity];148 for (let i = 0; i < n; i++) {149 for (let a = 0; a < 3; a++) {150 low[a] = Math.min(low[a], nodes[3 * i + a]);151 high[a] = Math.max(high[a], nodes[3 * i + a]);152 }153 }154 const span = Math.max(high[0] - low[0], high[1] - low[1], high[2] - low[2]) || 1;155 const scale = 2 / span;156 const at = (i, a) => (nodes[3 * i + a] - (low[a] + high[a]) / 2) * scale;157 for (let i = 0; i < n; i++) {158 m.makeTranslation(at(i, 0), at(i, 1), at(i, 2));159 balls.setMatrixAt(i, m);160 }161 balls.instanceMatrix.needsUpdate = true;162 const pos = geometry.attributes.position.array;163 for (let k = 0; k < branches.length; k++) {164 for (let a = 0; a < 3; a++) pos[3 * k + a] = at(branches[k], a);165 }166 geometry.attributes.position.needsUpdate = true;167 geometry.computeBoundingSphere();168 };169 place(nodes);170 return { parts: [balls, lines], place };171}