About this technique →
toon/sketch.js
// Toon Shading — Cel shading with inverted-hull outline
//
// MeshToonMaterial + a custom DataTexture gradient map gives clean
// discrete tone bands (posterized diffuse lighting).
// An inverted-hull outline mesh (BackSide + normal-offset vertex shader)
// adds the characteristic inked silhouette.
//
// The gradient texture is regenerated when the bands count changes.
// Shape is rebuilt when shape or bands change (rebuildOnChange).

import * as THREE from 'three';

export const PARAMS = {
  shape:        { value: "Torus Knot", options: ["Torus Knot", "Icosahedron", "Octahedron", "Torus"], label: "Shape", folder: "Structure", rebuildOnChange: true },
  bands:        { value: 4, min: 2, max: 6, step: 1, label: "Tone Bands", folder: "Appearance", rebuildOnChange: true },
  outlineThick: { value: 0.04, min: 0, max: 0.15, step: 0.005, label: "Outline Thickness", folder: "Appearance" },
  hue:          { value: 200, min: 0, max: 360, step: 1, label: "Base Hue", folder: "Appearance" },
  rotateSpeed:  { value: 0.3, min: 0, max: 2, step: 0.01, label: "Rotation Speed", folder: "Behavior" },
};

function makeGradientMap(THREE, bands) {
  const data = new Uint8Array(bands);
  for (let i = 0; i < bands; i++) {
    data[i] = Math.floor((i + 1) * 255 / bands);
  }
  const tex = new THREE.DataTexture(data, bands, 1, THREE.RedFormat);
  tex.minFilter = THREE.NearestFilter;
  tex.magFilter = THREE.NearestFilter;
  tex.needsUpdate = true;
  return tex;
}

function buildGeometry(THREE, shape) {
  switch (shape) {
    case "Icosahedron": return new THREE.IcosahedronGeometry(1.3, 1);
    case "Octahedron":  return new THREE.OctahedronGeometry(1.5, 1);
    case "Torus":       return new THREE.TorusGeometry(1.1, 0.4, 16, 48);
    default:            return new THREE.TorusKnotGeometry(1, 0.35, 128, 16);
  }
}

function buildScene(THREE, scene, params) {
  // Clear previous scene objects (except lights we'll re-add)
  while (scene.children.length > 0) scene.remove(scene.children[0]);

  scene.background = new THREE.Color(0xf8f8f8);

  // Lighting — toon needs at least a directional light
  const ambLight = new THREE.AmbientLight(0xffffff, 0.2);
  scene.add(ambLight);
  const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
  dirLight.position.set(3, 5, 4);
  scene.add(dirLight);

  const geo = buildGeometry(THREE, params.shape);
  const gradientMap = makeGradientMap(THREE, params.bands | 0);

  const toonMat = new THREE.MeshToonMaterial({
    color: new THREE.Color().setHSL(params.hue / 360, 0.6, 0.55),
    gradientMap,
  });

  const mesh = new THREE.Mesh(geo, toonMat);
  scene.add(mesh);

  // Inverted hull outline
  const outlineMat = new THREE.ShaderMaterial({
    uniforms: {
      u_thickness: { value: params.outlineThick },
      u_color: { value: new THREE.Color(0x0a0a0a) },
    },
    vertexShader: `
      uniform float u_thickness;
      void main() {
        vec3 pos = position + normal * u_thickness;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
      }
    `,
    fragmentShader: `
      uniform vec3 u_color;
      void main() {
        gl_FragColor = vec4(u_color, 1.0);
      }
    `,
    side: THREE.BackSide,
  });

  const outlineMesh = new THREE.Mesh(geo, outlineMat);
  scene.add(outlineMesh);

  return { mesh, outlineMesh, toonMat, outlineMat, gradientMap };
}

export function sceneSetup(THREE, scene, camera, renderer, params, seed) {
  camera.position.set(0, 0, 4);
  camera.lookAt(0, 0, 0);

  return buildScene(THREE, scene, params);
}

export function sceneAnimate(THREE, scene, camera, state, params, time, delta) {
  const { mesh, outlineMesh, toonMat, outlineMat } = state;

  // Rotate both meshes together
  const angle = time * params.rotateSpeed;
  mesh.rotation.y = angle;
  mesh.rotation.x = angle * 0.4;
  outlineMesh.rotation.y = angle;
  outlineMesh.rotation.x = angle * 0.4;

  // Live updates — hue and outline thickness don't need a rebuild
  toonMat.color.setHSL(params.hue / 360, 0.6, 0.55);
  outlineMat.uniforms.u_thickness.value = params.outlineThick;
}