Skip to content

Scene API ​

The Scene API gives you direct access to that scene so you can work with Three.js in full — alongside the UI Fold already generates.

Using Scene API calls inside the onReady callback is good practice and guarantees the scene is fully initialised and all methods below will work.

js
Fold.init({
  onReady: () => {
    // Scene API is always available here
  },
});

API Index ​

Lifecycle ​

  • refresh(options?) — Re-scan the DOM and sync the 3D scene with changes your own JS made (dynamic content, SPA route swaps, etc.)

Scene & Renderer ​

  • getThree() — Three.js constructor namespace for creating meshes, materials, and lights
  • getThreeState() — Live scene state: scene, camera, gl renderer, raycaster, clock

Positioning ​

Scene Groups ​

  • getGroupScroll() — Main scroll group; objects added here move with page scroll
  • getGroupFixed() — Viewport-anchored group; objects stay fixed regardless of scroll

DOM Element Selection ​

XR ​

  • getXr() — Current WebXR session state: isActive and mode

Other Bundled Libraries ↓ ​

  • getGltfLoader() — Three.js GLTFLoader class for loading .glb / .gltf models
  • getDracoLoader() — Three.js DRACOLoader class for Draco-compressed geometry
  • getSvgLoader() — Three.js SVGLoader class for loading SVGs as 3D geometry
  • getRgbeLoader() — Three.js RGBELoader class for loading equirectangular .hdr images
  • getTroikaTextLib() — troika-three-text namespace: Text, preloadFont, selection utilities

refresh(options?) ​

ts
Fold.refresh();

Fold reads the DOM once at init() and doesn't watch for changes on its own. If your own JavaScript mutates content Fold has already rendered — updating text, toggling classes, changing attributes, or adding/removing elements — call Fold.refresh() afterward to re-scan and bring the 3D scene back in sync.

It does this without tearing down the WebGL context, renderer, or React root: elements that are new or changed are (re-)rendered in 3D, elements that were removed are unregistered and disposed, and anything untouched is left alone — no context loss, no re-parsing, no flash.

When to call it ​

  • Dynamic content updates — text or markup you change with .innerHTML / .textContent:

    js
    function updateStatus() {
      statusEl.textContent = 'Active section: ' + activeName;
      Fold.refresh({ refreshScroll: false }); // show the new text without resetting scroll
    }
  • SPA route changes — call Fold.init() once for the life of the app, and refresh() after each route's DOM has rendered (e.g. inside Angular's afterNextRender). See the SPA Router Refresh demo and the Angular best-practice example in the Install Guide.

Options ​

ts
Fold.refresh({ refreshScroll: false }); // preserve scroll/camera position
Fold.refresh({ refreshScroll: false, refreshCamera: true }); // keep scroll, reset the free camera
Fold.refresh({ light: true });            // fast content-only rescan
  • refreshScroll (default true) — reset scroll to the top of the page, the way a fresh page load would. Set to false for most in-place content updates (like the sticky-status example above) and for SPA navigations where you want to preserve scroll rather than jump to top.
  • refreshCamera (defaults to refreshScroll) — return the free camera (cameraControls) to its baseline pose, like the RESET CAMERA button. An active Fold.viewTo() / view-link framing is always dismissed by refresh(), whatever this is set to.
  • light (default false) — only rescan the DOM: no scroll or camera reset, no view-link dismissal, no view-layout re-run. Other options are ignored. Cheap enough to call many times a second, for example while a slider is dragged; for anything faster, batch calls to once per animation frame.

getThree() ​

Returns the Three.js namespace Fold uses, so objects you create (meshes, geometries, materials, lights) are compatible with the scene.

js
Fold.init({
  onReady: () => {
    const THREE = Fold.getThree();
    // THREE.Mesh, THREE.BoxGeometry, THREE.MeshStandardMaterial,
    // THREE.DirectionalLight, THREE.ShaderMaterial … everything is here
  },
});

ESM variant: Three.js is externalized — getThree() returns your app's own Three.js instance. You can use either Fold.getThree() or your own import * as THREE from 'three' interchangeably.


getThreeState() ​

Returns the live scene state — scene, camera, gl renderer, raycaster, clock — as a React Three Fiber RootState. Use it to inspect or change what's already in the scene.

Requires onReady — returns undefined before the scene mounts.

ts
const { scene, camera, gl, raycaster, clock } = Fold.getThreeState();

Adding directly to scene places objects in world space — unaffected by scroll or viewport anchoring:

js
Fold.init({
  onReady: () => {
    const THREE = Fold.getThree();
    const { scene, camera, gl } = Fold.getThreeState();

    const mesh = new THREE.Mesh(
      new THREE.BoxGeometry(Fold.getPixelSize(40), Fold.getPixelSize(40), Fold.getPixelSize(40)),
      new THREE.MeshStandardMaterial({ color: 'hotpink' }),
    );
    mesh.position.set(0, 0.7, -0.2);
    scene.add(mesh);

    console.log(gl.info.render);  // draw calls, triangles, etc.
    console.log(camera.position); // current camera position
  },
});

getPixelSize(px?) ​

Returns the pixel-to-world-unit scale factor Fold uses throughout the scene. Pass a CSS pixel value to get the equivalent Three.js world-space distance directly, or call with no argument to get the raw scale factor.

js
// Pass a pixel value — returns that value in world units
mesh.position.set(Fold.getPixelSize(200), -Fold.getPixelSize(100), 0);

// No argument — returns the raw scale factor
const px = Fold.getPixelSize();
mesh.position.set(200 * px, -100 * px, 0);

// Geometry sizes — always use the pixel-value form for clarity
const sphere = new THREE.Mesh(
  new THREE.SphereGeometry(Fold.getPixelSize(28), 32, 32),
  new THREE.MeshStandardMaterial({ color: 'cyan' }),
);

Coordinate system — positions in page space follow these conventions:

  • Origin (0, 0, 0) = top-left corner of the content area (scroll group) or viewport (fixed group)
  • x increases rightward — same as CSS
  • y increases upward in Three.js, so negate DOM pixel offsets: y = -domPx * px
  • z increases toward the viewer — use small positive values (e.g. 0.05) to place objects in front of page content

This differs from the data-attribute API, where Y follows CSS (positive = down). In the Scene API you work in Three.js space, so negate DOM pixel offsets yourself, as shown above.

Use getPixelSize() rather than a hardcoded constant — the scale factor may be adjusted in future versions of Fold and this method will always return the correct value.


getViewportSize() ​

Returns the current viewport dimensions already converted to Three.js world units. Use this for centering objects, aligning to edges, or updating positions on window resize — without having to manually multiply by getPixelSize().

js
const { width, height } = Fold.getViewportSize();
PropertyEquivalent
widthwindow.innerWidth * getPixelSize()
heightwindow.innerHeight * getPixelSize()

Centering in the scroll group — the scroll group origin is the top-left of the page. To center an object horizontally at the top of the first viewport:

js
Fold.init({
  onReady: () => {
    const THREE = Fold.getThree();
    const scrollGroup = Fold.getGroupScroll();

    const sphere = new THREE.Mesh(
      new THREE.SphereGeometry(Fold.getPixelSize(28), 32, 32),
      new THREE.MeshStandardMaterial({ color: 'cyan' }),
    );
    scrollGroup.add(sphere);

    function reposition() {
      const { width } = Fold.getViewportSize();
      sphere.position.set(
        width / 2,                  // horizontally centered in the viewport
        -Fold.getPixelSize(60),     // 60px from the top of the page
        0.05,
      );
    }
    reposition();
    window.addEventListener('resize', reposition);
  },
});

Aligning to the bottom-right corner in the fixed group:

js
function reposition() {
  const { width, height } = Fold.getViewportSize();
  const margin = Fold.getPixelSize(30);
  mesh.position.set(width - margin, -(height - margin), 0.05);
}
reposition();
window.addEventListener('resize', reposition);

Call getViewportSize() inside the resize handler — the returned object reflects the viewport at call time, not a cached value.


Placement overview ​

MethodMoves with scrollCoordinate origin
getGroupScroll()YesTop-left of page content
getGroupFixed()NoTop-left of viewport
getThreeState().sceneNoWorld space

getGroupScroll() ​

Returns the main scroll THREE.Group — the group that moves with page scroll and contains all standard page content. Objects added here are positioned in page space and scroll naturally with the page.

Requires onReady — returns null before the scene mounts.

js
Fold.init({
  onReady: () => {
    const THREE = Fold.getThree();
    const scrollGroup = Fold.getGroupScroll();
    const px = Fold.getPixelSize();

    // Sphere — 60px right, 50px down from top-left of page content
    const sphere = new THREE.Mesh(
      new THREE.SphereGeometry(Fold.getPixelSize(25), 32, 32),
      new THREE.MeshStandardMaterial({ color: 'cyan', roughness: 0.2, metalness: 0.4 }),
    );
    sphere.position.set(60 * px, -50 * px, 0.05);
    scrollGroup.add(sphere);
  },
});

getGroupFixed() ​

Returns the fixed THREE.Group — viewport-anchored, does not move with page scroll. Objects added here behave like CSS position: fixed elements, staying at a constant screen position regardless of scroll.

Requires onReady — returns null before the scene mounts.

js
Fold.init({
  onReady: () => {
    const THREE = Fold.getThree();
    const fixedGroup = Fold.getGroupFixed();
    const px = Fold.getPixelSize();

    const torus = new THREE.Mesh(
      new THREE.TorusGeometry(Fold.getPixelSize(30), Fold.getPixelSize(10), 16, 60),
      new THREE.MeshStandardMaterial({ color: 'orange', roughness: 0.3, metalness: 0.5 }),
    );
    torus.rotation.x = Math.PI / 4;
    // 100px from left, 200px from top of viewport
    torus.position.set(100 * px, -200 * px, 0.05);
    fixedGroup.add(torus);
  },
});

querySelector(selector) ​

Returns the Three.js Group that represents the first DOM element matching the given CSS selector — the same selector syntax as document.querySelector.

Requires onReady — returns null before the scene is ready.

Return value ​

ResultMeaning
nullSelector matched no element, or the matched element is not rendered by Fold
THREE.GroupStandard element — one Three.js group
THREE.Group[]Sticky element — two groups (see Sticky elements below)

Examples ​

Basic — rotate a standard element:

js
Fold.init({
  onReady: () => {
    const group = Fold.querySelector('.hero');
    if (group && !Array.isArray(group)) {
      group.rotation.y = Math.PI / 8;
    }
  },
});

Traversing children — a returned Group contains the element's background plane, text and image meshes, and nested groups for child elements. Use group.traverse to reach individual meshes:

js
Fold.init({
  onReady: () => {
    const THREE = Fold.getThree();
    const group = Fold.querySelector('#featured-card');
    if (!group || Array.isArray(group)) return;

    group.traverse((obj) => {
      if (obj.isMesh) {
        obj.material = new THREE.MeshStandardMaterial({ color: 0x88aaff, wireframe: true });
      }
    });
  },
});

Adding a custom ShaderMaterial overlay — create a plane the same size as the DOM element and add it to the group. The plane's local [0, 0, z] position places it at the element's center:

js
Fold.init({
  onReady: () => {
    const THREE = Fold.getThree();
    const group = Fold.querySelector('#featured-card');
    if (!group || Array.isArray(group)) return;

    const el   = document.querySelector('#featured-card');
    const rect = el.getBoundingClientRect();
    const psd  = Fold.getPixelSize();

    const uniforms = { time: { value: 0 } };
    const mat = new THREE.ShaderMaterial({
      uniforms,
      transparent: true,
      depthWrite: false,
      vertexShader: `
        uniform float time;
        varying vec2 vUv;
        void main() {
          vUv = uv;
          vec3 pos = position;
          pos.z += sin(pos.x * 12.0 + time * 2.0) * 0.0008;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
        }
      `,
      fragmentShader: `
        uniform float time;
        varying vec2 vUv;
        void main() {
          float scan = smoothstep(0.02, 0.0, abs(vUv.y - mod(time * 0.4, 1.0)));
          gl_FragColor = vec4(0.5, 0.8, 1.0, scan * 0.8);
        }
      `,
    });

    const geo  = new THREE.PlaneGeometry(rect.width * psd, rect.height * psd, 24, 24);
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.z = 0.003; // just in front of the element's background plane
    group.add(mesh);

    (function tick() {
      uniforms.time.value = Date.now() * 0.001;
      requestAnimationFrame(tick);
    })();
  },
});

Sticky elements ​

CSS position: sticky elements are rendered as two groups, so moving between scrolling and stuck is seamless. Both are returned as an array of length 2:

  • Index 0 — scroll-positioned instance (moves with page scroll, positioned at the sticky offset)
  • Index 1 — fixed/stuck instance (viewport-anchored, shown when the element is stuck)

Use Array.isArray(result) to detect sticky elements and apply effects to both instances.

Sticky element — apply an effect to both instances:

js
Fold.init({
  onReady: () => {
    const result = Fold.querySelector('.sticky-nav');
    const groups = Array.isArray(result) ? result : (result ? [result] : []);
    groups.forEach(g => { g.scale.setScalar(1.05); });
  },
});

querySelectorAll(selector) ​

Returns Three.js Groups for all DOM elements matching a CSS selector — the same selector syntax as document.querySelectorAll. Each entry in the returned array corresponds to one matched DOM element and follows the same Group | Group[] convention as querySelector. Elements that are not rendered by Fold are silently excluded from the result.

Requires onReady — returns an empty array before the scene is ready.

Return value ​

Array<THREE.Group | THREE.Group[]> — one entry per matched DOM element that Fold renders. Each entry is a plain THREE.Group for standard elements, or THREE.Group[] of length 2 for sticky elements.

Examples ​

Apply a rotation to every .card element:

js
Fold.init({
  onReady: () => {
    Fold.querySelectorAll('.card').forEach((entry) => {
      // Normalise to array — works for both standard and sticky elements
      const groups = Array.isArray(entry) ? entry : [entry];
      groups.forEach(g => { g.rotation.y = 0.08; });
    });
  },
});

Flat list of every THREE.Group — when you don't need to distinguish between standard and sticky elements, use flatMap to get a single array:

js
const allGroups = Fold.querySelectorAll('.card')
  .flatMap(entry => Array.isArray(entry) ? entry : [entry]);

allGroups.forEach(g => { g.scale.setScalar(0.95); });

Per-element ShaderMaterial — add a custom overlay to each matched element, reading its DOM bounds to size the plane:

js
Fold.init({
  onReady: () => {
    const THREE    = Fold.getThree();
    const cardEls  = document.querySelectorAll('.card');
    const uniforms = { time: { value: 0 } };

    const mat = new THREE.ShaderMaterial({
      uniforms,
      transparent: true,
      depthWrite: false,
      vertexShader:   `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
      fragmentShader: `
        uniform float time; varying vec2 vUv;
        void main() {
          float e = max(smoothstep(0.06,0.0,vUv.x), max(smoothstep(0.94,1.0,vUv.x),
                    max(smoothstep(0.06,0.0,vUv.y), smoothstep(0.94,1.0,vUv.y))));
          gl_FragColor = vec4(0.5+0.5*sin(time+vUv.x*6.28), 0.5+0.5*sin(time+vUv.x*6.28+2.09),
                              0.5+0.5*sin(time+vUv.x*6.28+4.19), e * 0.3);
        }
      `,
    });

    Fold.querySelectorAll('.card').forEach((entry, i) => {
      const groups = Array.isArray(entry) ? entry : [entry];
      const rect   = cardEls[i].getBoundingClientRect();
      const psd    = Fold.getPixelSize();
      groups.forEach(g => {
        const mesh = new THREE.Mesh(new THREE.PlaneGeometry(rect.width * psd, rect.height * psd), mat);
        mesh.position.z = 0.002;
        g.add(mesh);
      });
    });

    (function tick() { uniforms.time.value = Date.now() * 0.001; requestAnimationFrame(tick); })();
  },
});

MESH_TYPE ​

A frozen object of string constants identifying the type of every Fold-generated mesh. Every mesh Fold creates has obj.userData.foldType set to one of these values, so you can reliably target specific mesh types inside group.traverse() without relying on heuristics.

ConstantValueMesh
MESH_TYPE.BACKGROUND'background'Element background plane
MESH_TYPE.BORDER'border'Border / outline
MESH_TYPE.TEXT'text'Text (a Troika Text mesh)
MESH_TYPE.SHADOW'shadow'CSS box-shadow or text-shadow copy
MESH_TYPE.IMAGE'image'Image plane
MESH_TYPE.MARKER'marker'List bullet or number

Targeting the background plane ​

js
Fold.init({
  onReady: () => {
    const THREE = Fold.getThree();
    const group = Fold.querySelector('.card');
    if (!group || Array.isArray(group)) return;

    group.traverse((obj) => {
      if (obj.userData.foldType === Fold.MESH_TYPE.BACKGROUND) {
        obj.material = new THREE.MeshStandardMaterial({ color: 0x112244, roughness: 0.1, metalness: 0.8 });
      }
    });
  },
});

Targeting text ​

Text meshes are Troika Text objects, so you can set Troika properties on them directly:

js
Fold.init({
  onReady: () => {
    const group = Fold.querySelector('.card');
    if (!group || Array.isArray(group)) return;

    group.traverse((obj) => {
      if (obj.userData.foldType === Fold.MESH_TYPE.TEXT) {
        obj.color = '#ff6600'; // Troika Text property — triggers re-render
      }
    });
  },
});

Targeting both on the same element ​

js
Fold.init({
  onReady: () => {
    const THREE = Fold.getThree();
    const group = Fold.querySelector('#card6');
    if (!group || Array.isArray(group)) return;

    group.traverse((obj) => {
      if (obj.userData.foldType === Fold.MESH_TYPE.BACKGROUND) {
        // Replace background material with a custom shader
        obj.material = myBgShader;
      }
      if (obj.userData.foldType === Fold.MESH_TYPE.TEXT) {
        // Animate text color directly via the Troika Text API
        obj.color = '#00ffcc';
      }
    });
  },
});

Fold does not set userData.foldType on meshes you add yourself — the convention is one-way (Fold → consumer). Shadow text-shadow meshes are not tagged as 'text'; they are tagged as 'shadow'.


getXr() ​

Returns the current WebXR session state. Use this to poll XR status at any point after onReady, or to branch logic in resize/animation handlers based on whether the user is in headset.

ts
const { isActive, mode } = Fold.getXr();
PropertyTypeDescription
isActivebooleantrue while a WebXR session is running
mode'immersive-ar' | 'immersive-vr' | nullActive session mode, or null when inactive

For a reactive callback that fires on every change, use onXrChange in Fold.init().

js
Fold.init({
  onReady: () => {
    const { isActive, mode } = Fold.getXr();
    console.log('XR on load:', isActive, mode);
  },
  onXrChange: ({ isActive, mode }) => {
    if (isActive) {
      console.log('XR started:', mode);
    } else {
      console.log('XR ended');
    }
  },
});

Other Bundled Libraries ​

Fold bundles several Three.js ecosystem libraries to power its own rendering. These are exposed so you can use the same instances — same Three.js version, same memory space — without adding separate dependencies to your page.

All bundled-library accessors are available immediately after Fold.init() — you do not need to wait for onReady to call them. getGltfLoader(), getDracoLoader() and getSvgLoader() return a Promise: those loaders are separate on-demand files (fold.gltf.js, fold.svg.js) fetched the first time a model, SVG or one of these calls needs them, so pages that use none of them never download them.


getGltfLoader() ​

Resolves to the GLTFLoader class bundled with Fold (loaded on demand — see the note above). Use this to load .glb and .gltf 3D models without a separate Three.js import. Because it is the same Three.js instance as the Fold scene, loaded objects are directly compatible and can be added to any group.

js
Fold.init({
  onReady: async () => {
    const GLTFLoader = await Fold.getGltfLoader();
    const loader = new GLTFLoader();

    loader.load('/models/statue.glb', (gltf) => {
      const model = gltf.scene;
      model.position.set(0, -Fold.getPixelSize(300), -0.2);
      Fold.getGroupScroll().add(model);
    });
  },
});

To load Draco-compressed models, pair with getDracoLoader().


getDracoLoader() ​

Resolves to the DRACOLoader class bundled with Fold (loaded on demand together with getGltfLoader()). Draco is a geometry compression format that significantly reduces .glb file sizes. Pair it with getGltfLoader() to decode compressed models.

Draco decoding runs in a Web Worker and requires the Draco WASM decoder files to be served from your own domain. Copy the draco/ folder from the Three.js package to your public/ directory, then point setDecoderPath at it.

js
Fold.init({
  onReady: async () => {
    const GLTFLoader = await Fold.getGltfLoader();
    const DRACOLoader = await Fold.getDracoLoader();

    const draco = new DRACOLoader();
    draco.setDecoderPath('/draco/'); // path to Draco WASM decoder files

    const loader = new GLTFLoader();
    loader.setDRACOLoader(draco);

    loader.load('/models/compressed.glb', (gltf) => {
      Fold.getGroupScroll().add(gltf.scene);
    });
  },
});

Note: Models loaded with data-foldpage-model-src already decode Draco. You only need getDracoLoader() for your own GLTFLoader.


getSvgLoader() ​

Resolves to the SVGLoader class bundled with Fold (loaded on demand). Use it to load .svg files and convert their paths into Three.js geometry — flat filled shapes, extruded shapes, or stroke outlines.

js
Fold.init({
  onReady: async () => {
    const THREE = Fold.getThree();
    const SVGLoader = await Fold.getSvgLoader();
    const scrollGroup = Fold.getGroupScroll();
    const px = Fold.getPixelSize();

    const loader = new SVGLoader();
    loader.load('/logo.svg', (data) => {
      data.paths.forEach((path) => {
        const shapes = SVGLoader.createShapes(path);
        const color = path.color ?? 0xffffff;

        shapes.forEach((shape) => {
          // Flat filled shape
          const geo = new THREE.ShapeGeometry(shape);
          const mat = new THREE.MeshBasicMaterial({ color, side: THREE.DoubleSide });
          const mesh = new THREE.Mesh(geo, mat);

          // SVG coordinates are in pixel units — scale to world units and flip Y
          mesh.scale.set(px, -px, px);
          mesh.position.set(100 * px, -50 * px, 0.05);
          scrollGroup.add(mesh);
        });
      });
    });
  },
});

Extruded 3D shapes — use THREE.ExtrudeGeometry instead of ShapeGeometry:

js
const geo = new THREE.ExtrudeGeometry(shape, { depth: 10 * px, bevelEnabled: false });

Stroke outlines — use SVGLoader.pointsToStroke:

js
path.subPaths.forEach((subPath) => {
  const points = subPath.getPoints();
  const geo = SVGLoader.pointsToStroke(points, path.userData.style);
  if (geo) scrollGroup.add(new THREE.Mesh(geo, mat));
});

getRgbeLoader() ​

Returns the RGBELoader class bundled inside Fold. Use this to load equirectangular .hdr images for custom IBL setups without a separate Three.js dependency.

For the fully managed HDR workflow — automatic IBL, optional background, XR visibility control — use hdr in Fold.init() or the data-foldpage-hdr-src attribute instead. getRgbeLoader() is for cases where you need direct access to the texture and want to apply it yourself.

js
Fold.init({
  onReady: () => {
    const THREE        = Fold.getThree();
    const RGBELoader   = Fold.getRgbeLoader();
    const { gl, scene } = Fold.getThreeState();

    // Convert equirectangular HDR to a cubemap suitable for IBL
    const pmrem = new THREE.PMREMGenerator(gl);
    pmrem.compileEquirectangularShader();

    new RGBELoader().load('/environments/studio.hdr', (texture) => {
      const envMap = pmrem.fromEquirectangular(texture).texture;
      pmrem.dispose();

      // IBL for all PBR materials in the scene
      scene.environment = envMap;
      scene.environmentIntensity = 0.8; // Three.js >= r167

      // Optional: use as visible 360° background
      texture.mapping = THREE.EquirectangularReflectionMapping;
      scene.background = texture;
    });
  },
});

Supported format: equirectangular .hdr (Radiance RGBE format). OpenEXR (.exr) files are not supported by RGBELoader — use EXRLoader from Three.js separately if needed.

IBL and material colors: Once scene.environment is set, all MeshStandardMaterial and MeshPhysicalMaterial meshes in the scene respond to it. GLB models are lit by IBL immediately. Fold's own page elements only respond if they carry data-foldpage-lit. See the IBL and material colors section for full details.


getTroikaTextLib() ​

Returns the troika-three-text namespace bundled inside Fold — the same library Fold uses to render all text in the scene. The primary export is Text, a Three.js Mesh subclass that renders high-quality SDF text and can be added directly to any group or to the scene.

js
Fold.init({
  onReady: () => {
    const { Text } = Fold.getTroikaTextLib();
    const scrollGroup = Fold.getGroupScroll();
    const px = Fold.getPixelSize();

    const label = new Text();
    label.text = 'Ancient Woodland';
    label.fontSize = px * 48;            // 48px in world units
    label.color = 0xdeeac8;
    label.anchorX = 'center';
    label.anchorY = 'middle';
    label.position.set(px * 430, -px * 80, 0.05);
    label.sync();                        // apply property changes
    scrollGroup.add(label);
  },
});

Available exports ​

ExportDescription
TextThree.js Mesh subclass — the main text object
preloadFontPre-load a font before any Text object needs it
getSelectionRectsCompute bounding boxes for a text selection range
getCaretAtPointHit-test a point against rendered glyphs to find a caret position
configureTextBuilderAdvanced: configure the Troika worker and font resolution
createTextDerivedMaterialExtend an existing Three.js material with Troika's SDF text shader

Font loading ​

Troika loads fonts asynchronously. Call sync() after setting properties to trigger layout; the text appears once the font loads. To avoid a flash of invisible text, pre-load fonts before creating any Text object:

js
const { Text, preloadFont } = Fold.getTroikaTextLib();

preloadFont(
  { font: 'https://cdn.fold.page/latest/fonts/Lato/Lato-Regular.ttf' },
  () => {
    // font is cached — Text objects using this font render immediately
    const label = new Text();
    label.font = 'https://cdn.fold.page/latest/fonts/Lato/Lato-Regular.ttf';
    label.text = 'Ready instantly';
    label.fontSize = Fold.getPixelSize(32);
    label.sync();
    Fold.getGroupScroll().add(label);
  }
);

Common Text properties ​

js
const label = new Text();
label.text       = 'Hello';
label.font       = '/fonts/MyFont.ttf'; // optional — defaults to Roboto
label.fontSize   = Fold.getPixelSize(24);
label.color      = 0xffffff;
label.maxWidth   = Fold.getPixelSize(400); // wrap at 400px wide
label.lineHeight = 1.4;
label.anchorX    = 'center';   // 'left' | 'center' | 'right'
label.anchorY    = 'middle';   // 'top' | 'middle' | 'bottom'
label.textAlign  = 'center';
label.sync();

Call label.sync() every time you change a property — Troika re-lays out text asynchronously on each sync call.

Fold.page — 3D UI for the web.