Back to Soybert 3D
SOURCE / PINNED RELEASE

Made of little things.

Soybert 3D

Release
4bf80e758af1…
Author-recorded commit
298fa2d0e8b1…
License
LICENSE
Author’s source reference
nostr://npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd/wss%3A%2F%2Fgit.napplet.soy%2F/n-9021e1b6a96

Archive hash verified: 7083223a53bbd320…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/title.ts
import { BufferAttribute, BufferGeometry, Group, Mesh, MeshStandardMaterial } from 'three';
import data from './assets/fredoka-title.json';

function attribute(encoded: string, normals = false) {
  const binary = atob(encoded), bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
  return normals ? new BufferAttribute(new Int16Array(bytes.buffer), 3, true) : new BufferAttribute(new Float32Array(bytes.buffer), 3);
}
/** User-requested splash title, extruded from the actual @fontsource/fredoka outlines. */
export function createTitle() {
  const group = new Group(); group.name = 'soybert · napplet.soy';
  const face = new MeshStandardMaterial({ color: '#f8d99a', roughness: .35, metalness: .08 });
  const sides = new MeshStandardMaterial({ color: '#98532b', roughness: .48, metalness: .12 });
  const geometries: BufferGeometry[] = [];
  function mesh(source: typeof data.title, front: MeshStandardMaterial) {
    const geometry = new BufferGeometry();
    geometry.setAttribute('position', attribute(source.positions)); geometry.setAttribute('normal', attribute(source.normals, true));
    const indexBytes = Uint8Array.from(atob(source.indices), c => c.charCodeAt(0));
    geometry.setIndex(new BufferAttribute(new Uint32Array(indexBytes.buffer), 1));
    source.groups.forEach(g => geometry.addGroup(g.start, g.count, g.materialIndex));
    geometries.push(geometry); const object = new Mesh(geometry, [front, sides]); group.add(object); return object;
  }
  const title = mesh(data.title, face), subtitle = mesh(data.subtitle, face);
  title.rotation.set(-.06, -.12, -.035); subtitle.rotation.copy(title.rotation);
  return { group,
    layout(aspect: number, height: number) {
      const distance = 6, viewHeight = 2 * Math.tan(38 * Math.PI / 360) * distance;
      const titleWidth = Math.min(height < 260 ? 2 : 2.4, viewHeight * aspect * .76);
      const titleScale = titleWidth / data.title.width;
      const subtitleScale = titleWidth * .58 / data.subtitle.width;
      const titleHeight = data.title.height * titleScale;
      const top = viewHeight * (height < 260 ? .30 : aspect >= .85 ? .405 : .33);
      group.position.set(0, top, -distance);
      title.scale.setScalar(titleScale);
      subtitle.scale.setScalar(subtitleScale);
      subtitle.position.y = -titleHeight * .75 - .045;
    },
    theme(light: boolean) {
      face.color.set(light ? '#e6b455' : '#f8d99a');
    },
    dispose() { geometries.forEach(g => g.dispose()); face.dispose(); sides.dispose(); },
  };
}