3D Projection System

PinePaper includes a standalone 3D projection library for rendering 3D primitives on the 2D canvas. The library is zero-dependency (~18KB minified), outputs plain polygon descriptors, and can be used independently of Paper.js.

Architecture

js/3d/
  core/          Point3D, Matrix4, Quaternion (math primitives)
  geometry/      Face, Mesh, Primitives (cube, sphere, cylinder, torus, cone)
  projection/    Projections (5 types), Camera3D (orbital camera)
  pipeline/      Scene3D (orchestrator), Lighting, DepthSorter
  gpu/           ProjectionGPU (WebGPU/WebGL2 acceleration)
  index.js       Barrel export for standalone usage

Design Principles:

  • Zero DOM or Paper.js dependencies in the 3D library itself
  • All data structures are plain JavaScript objects (no classes) for serialization
  • Consumer-agnostic polygon output: any 2D renderer can consume the results
  • GPU acceleration is optional with automatic fallback to CPU

PinePaper API

Access 3D features through the PinePaper instance:

createObject3D(type, params)

Create a 3D primitive and register it as a canvas item.

const cubeId = app.createObject3D('cube', {
  size: 1.5,
  color: '#3b82f6',
  x: 0, y: 0, z: 0,
  rotationY: 45,
  animationType: 'rotate',
  animationSpeed: 0.5
});

Parameters:

Param Type Default Description
type string 'cube', 'sphere', 'cylinder', 'torus', 'cone'
size number 1 Cube size (cube only)
radius number 0.5 Radius (sphere, cylinder, torus, cone)
height number 1 Height (cylinder, cone)
tube number 0.2 Tube radius (torus only)
color string ‘#cccccc’ Face color
x, y, z number 0 World position
rotationX/Y/Z number 0 Rotation in degrees
scale number 1 Uniform scale
widthSegments number 12 Longitude divisions (sphere)
heightSegments number 8 Latitude divisions (sphere)
segments number 16 Circumference divisions (cylinder, cone)
radialSegments number 12 Around tube (torus)
tubularSegments number 16 Around ring (torus)
animationType string 'rotate' for auto-rotation
animationSpeed number 1 Rotation speed multiplier

Returns: string — Item registry ID (e.g., 'item_42')

createGlossySphere(params)

Create a glossy 2D sphere effect using layered radial gradients with shadow.

const sphere = app.createGlossySphere({
  x: 400, y: 300,
  radius: 60,
  color: '#F97316',
  lightDirection: 'top-left',
  glossiness: 0.8,
  castShadow: true
});

Parameters:

Param Type Default Description
x number center X position
y number center Y position
radius number 50 Sphere radius
color string ‘#4a9eff’ Base color
lightDirection string ‘top-left’ Light source direction
glossiness number 0.7 Glossiness level (0-1)
castShadow boolean true Whether to cast shadow
shadowBlur number 15 Shadow blur amount

Light Directions: top-left, top-right, top, left, right, bottom-left, bottom-right

Returns: paper.Group — Group containing sphere elements

Primitives

Primitive Faces (default) Key Params
cube 6 quads size
sphere ~96 radius, widthSegments, heightSegments
cylinder ~34 radius, height, segments
torus ~192 radius, tube, radialSegments, tubularSegments
cone ~17 radius, height, segments

Standalone Usage

The 3D library can be used independently of PinePaper. Import from the barrel export:

import * as Scene3D from './js/3d/pipeline/Scene3D.js';
import * as Primitives from './js/3d/geometry/Primitives.js';
import * as Camera3D from './js/3d/projection/Camera3D.js';

// Create a scene
const scene = Scene3D.createScene({
  width: 800,
  height: 600,
  projection: 'perspective'
});

// Add primitives
const cube = Primitives.cube({ size: 1, color: '#3b82f6' });
Scene3D.addMesh(scene, cube);

const sphere = Primitives.sphere({ radius: 0.5, color: '#ef4444' });
Scene3D.addMesh(scene, sphere);

// Set camera
const camera = Camera3D.createCamera({
  position: { x: 3, y: 2, z: 5 },
  target: { x: 0, y: 0, z: 0 }
});
Scene3D.setCamera(scene, camera);

// Render — returns plain polygon descriptors
const polygons = Scene3D.render(scene);

// Each polygon: { vertices2D, color, depth, meshIndex }
// Render with any 2D system (Canvas2D, SVG, Paper.js, etc.)
for (const poly of polygons) {
  ctx.beginPath();
  poly.vertices2D.forEach((v, i) => {
    i === 0 ? ctx.moveTo(v.x, v.y) : ctx.lineTo(v.x, v.y);
  });
  ctx.closePath();
  ctx.fillStyle = poly.color;
  ctx.fill();
}

Render Pipeline

The rendering pipeline follows these stages:

  1. Transform — Apply mesh world transform (4x4 matrix) to face vertices and normals
  2. Back-face Cull — Skip faces where the normal points away from the camera
  3. Shade — Apply directional lighting with ambient floor (brightness = ambient + max(0, N·L) * intensity)
  4. Project — Convert 3D vertices to 2D screen coordinates using the active projection
  5. Depth Sort — Painter’s algorithm (back-to-front) for correct z-ordering

Module Map

Module Purpose
Point3D 3D vector math (add, sub, dot, cross, normalize, lerp, transformMat4)
Matrix4 4x4 matrix operations (multiply, invert, translate, scale, rotate, lookAt, perspective)
Quaternion Rotation representation and interpolation
Face Triangle/quad face with auto-computed normal
Mesh Collection of faces with world transform matrix
Primitives Geometry generators (cube, sphere, cylinder, torus, cone)
Projections 5 projection types (perspective, orthographic, isometric, cabinet, cavalier)
Camera3D LookAt camera with orbital navigation
Scene3D Pipeline orchestrator (transform, cull, shade, project, sort)
Lighting Directional light with ambient floor
DepthSorter Painter’s algorithm depth sorting
ProjectionGPU GPU-accelerated vertex projection with automatic fallback

Scene Statistics

const stats = Scene3D.getStats(scene);
// { meshes: 2, faces: 102, vertices: 306 }

// Via PinePaper bridge:
const stats = app.threeD.getStats();
// { meshes: 2, faces: 102, vertices: 306, gpuMode: 'webgpu', objectCount: 2 }

Paper.js Community Usage

The 3D library outputs renderer-agnostic polygon descriptors:

{
  vertices2D: [{x: 100, y: 50}, {x: 200, y: 50}, {x: 150, y: 150}],
  color: '#5588cc',    // Pre-shaded CSS color
  depth: 3.14,         // For painter's algorithm sorting
  meshIndex: 0         // Source mesh identifier
}

Any Paper.js project can consume these directly:

import * as Scene3D from 'pinepaper-3d';

const polygons = Scene3D.render(scene);
for (const poly of polygons) {
  new paper.Path({
    segments: poly.vertices2D.map(v => new paper.Point(v.x, v.y)),
    fillColor: poly.color,
    closed: true
  });
}