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}