Models 44

Claude Haiku 5.5

by AnthropicReasoningClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
2 min 22 s
Cost
$0.017
Output tokens
33,783
Spent thinking
22,624 tokens
Speed
241.1 tok/s
Console errors
0
First token after
1.9 s
First visible text after
1 min 49 s
Prompt tokens
855
Code size
20.4 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-haiku-5.5
Reasoning sent
{"effort":"high"}
Max output
128,000 tokens
Generated
Nov 10, 2026

What it was asked

The prompt
Build a low-poly 3D scene of a campfire in a forest clearing at night, with Three.js in a single HTML file. Load Three.js only through this import map (do not use any other URL or version): <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script> Requirements: - A ground with a few low-poly rocks and logs around a campfire in the center. - At least 30 low-poly trees placed randomly around the clearing (not on the fire). - The campfire has animated flames and a warm point light that flickers and casts shadows. - Sparks rising from the fire as a simple particle system. - A dark night sky with stars and a moon, plus fog that fades the distant trees. - OrbitControls to look around, with limits so the camera stays above the ground. - The renderer fills the window and handles resizing correctly.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 618 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Low-poly Campfire at Night</title>
<style>
  html, body {
    margin: 0;
    height: 100%;
    overflow: hidden;
    background: #0b0f1c;
  }
  #app {
    position: fixed;
    inset: 0;
  }
  canvas {
    display: block;
    width: 100%;
    height: 100%;
  }
  #hud {
    position: fixed;
    left: 18px;
    bottom: 16px;
    color: #c9d3ea;
    font: 500 13px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    pointer-events: none;
    text-shadow: 0 1px 6px #000;
    user-select: none;
  }
  #hud h1 {
    margin: 0 0 2px;
    font-size: 15px;
    font-weight: 600;
    letter-spacing: 0.3px;
    color: #ffd9a0;
  }
  #hud p {
    margin: 0;
    opacity: 0.75;
  }
  @media (max-width: 480px) {
    #hud { left: 10px; bottom: 10px; font-size: 11px; }
    #hud h1 { font-size: 13px; }
  }
</style>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>
</head>
<body>
<div id="app"></div>
<div id="hud">
  <h1>Campfire · Forest Clearing at Night</h1>
  <p>Drag to orbit · Scroll to zoom</p>
</div>

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

/* ---------- Seeded random so the layout is stable every load ---------- */
function mulberry32(a) {
  return function () {
    a |= 0; a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}
const rand = mulberry32(20240611);
const rr = (a, b) => a + (b - a) * rand();

/* ---------- Renderer, scene, camera ---------- */
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
document.getElementById('app').appendChild(renderer.domElement);

const scene = new THREE.Scene();
const FOG_COLOR = new THREE.Color(0x121a2e);
scene.background = FOG_COLOR.clone();
scene.fog = new THREE.Fog(FOG_COLOR, 16, 92);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 2.6, 11);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.1, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.enablePan = false;
controls.minDistance = 4;
controls.maxDistance = 30;
controls.minPolarAngle = 0.25;              // don't look straight down
controls.maxPolarAngle = Math.PI * 0.47;    // never dip below the ground
controls.autoRotate = true;
controls.autoRotateSpeed = 0.35;
controls.addEventListener('start', () => { controls.autoRotate = false; });
controls.update();

/* ---------- Helpers ---------- */
function radialTexture(stops, size = 256) {
  const c = document.createElement('canvas');
  c.width = c.height = size;
  const g = c.getContext('2d');
  const grd = g.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
  stops.forEach(([o, col]) => grd.addColorStop(o, col));
  g.fillStyle = grd;
  g.fillRect(0, 0, size, size);
  const tex = new THREE.CanvasTexture(c);
  tex.colorSpace = THREE.SRGBColorSpace;
  return tex;
}

// Ground height: flat clearing around the fire, gentle rolling hills outside
function heightAt(x, z) {
  const r = Math.hypot(x, z);
  const f = THREE.MathUtils.smoothstep(r, 6, 16);
  const h = 0.45 * Math.sin(x * 0.19 + 1.3) * Math.cos(z * 0.16)
          + 0.30 * Math.sin(x * 0.47 + z * 0.37)
          + 0.18 * Math.cos(z * 0.9 - x * 0.6);
  return h * f;
}

/* ---------- Sky dome (vertical gradient) ---------- */
{
  const geo = new THREE.SphereGeometry(460, 32, 16);
  const p = geo.attributes.position;
  const cols = new Float32Array(p.count * 3);
  const zenith = new THREE.Color(0x03060f);
  const c = new THREE.Color();
  for (let i = 0; i < p.count; i++) {
    const t = THREE.MathUtils.clamp(p.getY(i) / 460, 0, 1);
    c.copy(FOG_COLOR).lerp(zenith, Math.pow(t, 0.55));
    cols[i * 3] = c.r; cols[i * 3 + 1] = c.g; cols[i * 3 + 2] = c.b;
  }
  geo.setAttribute('color', new THREE.BufferAttribute(cols, 3));
  const mat = new THREE.MeshBasicMaterial({
    vertexColors: true, side: THREE.BackSide, fog: false, depthWrite: false
  });
  const dome = new THREE.Mesh(geo, mat);
  dome.renderOrder = -10;
  scene.add(dome);
}

/* ---------- Stars ---------- */
const stars = (() => {
  const N = 1600;
  const pos = new Float32Array(N * 3);
  const col = new Float32Array(N * 3);
  const R = 390;
  for (let i = 0; i < N; i++) {
    const y = rr(0.05, 1);
    const th = rand() * Math.PI * 2;
    const s = Math.sqrt(1 - y * y);
    pos[i * 3] = Math.cos(th) * s * R;
    pos[i * 3 + 1] = y * R;
    pos[i * 3 + 2] = Math.sin(th) * s * R;
    const b = 0.45 + 0.55 * Math.pow(rand(), 3);
    const tint = rand();
    col[i * 3]     = b * (tint < 0.2 ? 1.0 : 0.85 + tint * 0.15);
    col[i * 3 + 1] = b * (0.9 + rand() * 0.1);
    col[i * 3 + 2] = b * (tint < 0.25 ? 0.75 : 1.0);
  }
  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
  const mat = new THREE.PointsMaterial({
    size: 1.7, sizeAttenuation: false, vertexColors: true,
    transparent: true, depthWrite: false, fog: false
  });
  const pts = new THREE.Points(geo, mat);
  pts.frustumCulled = false;
  scene.add(pts);
  return pts;
})();

/* ---------- Moon (faceted sphere + halo) ---------- */
const moonDir = new THREE.Vector3(-0.55, 0.5, -0.66).normalize();
const moonPos = moonDir.clone().multiplyScalar(330);
{
  const geo = new THREE.IcosahedronGeometry(12, 2);
  const n = geo.attributes.position.count;
  const cols = new Float32Array(n * 3);
  let shade = 1;
  for (let i = 0; i < n; i++) {
    if (i % 3 === 0) {
      shade = 0.9 + 0.1 * rand();
      if (rand() < 0.18) shade *= 0.72; // a few darker "maria"
    }
    cols[i * 3] = shade; cols[i * 3 + 1] = shade; cols[i * 3 + 2] = shade * 1.02;
  }
  geo.setAttribute('color', new THREE.BufferAttribute(cols, 3));
  const mat = new THREE.MeshBasicMaterial({
    vertexColors: true, flatShading: true, fog: false, toneMapped: false,
    color: 0xeef2ff
  });
  const moon = new THREE.Mesh(geo, mat);
  moon.position.copy(moonPos);
  scene.add(moon);

  const halo = new THREE.Sprite(new THREE.SpriteMaterial({
    map: radialTexture([
      [0.0, 'rgba(225,233,255,0.65)'],
      [0.22, 'rgba(190,205,245,0.22)'],
      [1.0, 'rgba(120,140,200,0)']
    ]),
    blending: THREE.AdditiveBlending, depthWrite: false,
    transparent: true, fog: false
  }));
  halo.position.copy(moonPos);
  halo.scale.set(120, 120, 1);
  scene.add(halo);
}

/* ---------- Lights ---------- */
// Hemisphere: cold night sky fill
scene.add(new THREE.HemisphereLight(0x4a64a0, 0x0d1408, 0.35));

// Moonlight with shadows
const moonLight = new THREE.DirectionalLight(0x9fb6ff, 0.75);
moonLight.position.copy(moonDir).multiplyScalar(60);
moonLight.target.position.set(0, 0, 0);
moonLight.castShadow = true;
moonLight.shadow.mapSize.set(2048, 2048);
const sc = moonLight.shadow.camera;
sc.left = -60; sc.right = 60; sc.top = 60; sc.bottom = -60;
sc.near = 1; sc.far = 200;
moonLight.shadow.bias = -0.0008;
moonLight.shadow.normalBias = 0.02;
scene.add(moonLight);
scene.add(moonLight.target);

// Fire point light with flicker and shadows
const BASE_FIRE_I = 55;
const fireLight = new THREE.PointLight(0xff8a30, BASE_FIRE_I, 0, 2);
fireLight.position.set(0, 1.1, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(512, 512);
fireLight.shadow.camera.near = 0.1;
fireLight.shadow.camera.far = 40;
fireLight.shadow.bias = -0.002;
fireLight.shadow.normalBias = 0.03;
scene.add(fireLight);

/* ---------- Ground ---------- */
const GROUND_SIZE = 260;
const GROUND_SEG = 90;
{
  const geo = new THREE.PlaneGeometry(GROUND_SIZE, GROUND_SIZE, GROUND_SEG, GROUND_SEG);
  geo.rotateX(-Math.PI / 2);
  const p = geo.attributes.position;
  const cols = new Float32Array(p.count * 3);
  const cDirt = new THREE.Color(0x3a2e22);
  const cGrassA = new THREE.Color(0x1c2e1a);
  const cGrassB = new THREE.Color(0x2c3e23);
  const grass = new THREE.Color();
  const out = new THREE.Color();
  for (let i = 0; i < p.count; i++) {
    const x = p.getX(i), z = p.getZ(i);
    const r = Math.hypot(x, z);
    const edge = Math.abs(x) > GROUND_SIZE / 2 - 0.01 || Math.abs(z) > GROUND_SIZE / 2 - 0.01;
    const jitter = edge ? 0 : (rand() - 0.5) * 0.28 * THREE.MathUtils.smoothstep(r, 6, 14);
    p.setY(i, heightAt(x, z) + jitter);

    grass.copy(cGrassA).lerp(cGrassB, rand());
    const t = THREE.MathUtils.smoothstep(r, 4.5, 9.5);
    out.copy(cDirt).lerp(grass, t);
    cols[i * 3] = out.r; cols[i * 3 + 1] = out.g; cols[i * 3 + 2] = out.b;
  }
  geo.setAttribute('color', new THREE.BufferAttribute(cols, 3));
  geo.computeVertexNormals();
  const mat = new THREE.MeshStandardMaterial({
    vertexColors: true, flatShading: true, roughness: 0.97, metalness: 0
  });
  const ground = new THREE.Mesh(geo, mat);
  ground.receiveShadow = true;
  scene.add(ground);
}

/* ---------- Shared materials & geometries ---------- */
const barkMat = new THREE.MeshStandardMaterial({ color: 0x4b2f1d, roughness: 0.9, flatShading: true });
const fireLogMat = new THREE.MeshStandardMaterial({
  color: 0x3a2214, roughness: 0.9, flatShading: true,
  emissive: 0xff4a0a, emissiveIntensity: 0.28
});
const stoneMat = new THREE.MeshStandardMaterial({ color: 0x5d6168, roughness: 0.95, flatShading: true });
const rockMats = [0x5d6169, 0x4b525a, 0x6b6e72].map(c =>
  new THREE.MeshStandardMaterial({ color: c, roughness: 0.95, flatShading: true })
);
const rockGeo = new THREE.IcosahedronGeometry(1, 0);
const stoneGeo = new THREE.DodecahedronGeometry(1, 0);

const obstacles = []; // used to keep trees away from props

/* ---------- Campfire: stone ring ---------- */
for (let i = 0; i < 10; i++) {
  const a = (i / 10) * Math.PI * 2 + rr(-0.12, 0.12);
  const R = 1.15 + rr(-0.05, 0.05);
  const s = rr(0.26, 0.36);
  const m = new THREE.Mesh(stoneGeo, stoneMat);
  m.position.set(Math.cos(a) * R, 0.1, Math.sin(a) * R);
  m.scale.set(s * rr(1, 1.3), s * rr(0.6, 0.8), s * rr(1, 1.3));
  m.rotation.set(rand() * 3, rand() * 3, rand() * 3);
  m.castShadow = true;
  m.receiveShadow = true;
  scene.add(m);
}

/* ---------- Campfire: crossed logs ---------- */
for (let i = 0; i < 4; i++) {
  const g = new THREE.Group();
  g.position.set(0, i % 2 === 0 ? 0.1 : 0.22, 0);
  g.rotation.y = i * Math.PI / 4 + 0.2;
  g.rotation.x = (i % 2 === 0 ? 0.06 : -0.06);
  const m = new THREE.Mesh(new THREE.CylinderGeometry(0.11, 0.13, 1.6, 7), fireLogMat);
  m.rotation.z = Math.PI / 2;
  m.position.y = 0.11;
  m.castShadow = true;
  m.receiveShadow = true;
  g.add(m);
  scene.add(g);
}

/* ---------- Seating logs around the fire ---------- */
const seatLogs = [];
for (let i = 0; i < 5; i++) {
  const a = (i / 5) * Math.PI * 2 + rr(-0.25, 0.25) + 0.4;
  const R = 3.5;
  const x = Math.cos(a) * R, z = Math.sin(a) * R;
  const g = new THREE.Group();
  g.position.set(x, heightAt(x, z) + 0.2, z);
  g.rotation.y = -a - Math.PI / 2 + rr(-0.25, 0.25);
  const len = rr(2.0, 2.6);
  const m = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.22, len, 7), barkMat);
  m.rotation.z = Math.PI / 2;
  m.castShadow = true;
  m.receiveShadow = true;
  g.add(m);
  scene.add(g);
  obstacles.push({ x, z, r: len * 0.55 + 0.3 });
  seatLogs.push(g);
}

/* ---------- Scattered rocks ---------- */
for (let i = 0; i < 36; i++) {
  const a = rand() * Math.PI * 2;
  const d = rr(5.5, 36);
  const x = Math.cos(a) * d, z = Math.sin(a) * d;
  if (obstacles.some(o => Math.hypot(o.x - x, o.z - z) < o.r + 1)) continue;
  const s = rr(0.35, 1.2);
  const m = new THREE.Mesh(rockGeo, rockMats[i % rockMats.length]);
  m.scale.set(s * rr(0.9, 1.3), s * rr(0.5, 0.8), s * rr(0.9, 1.3));
  m.rotation.set(rand() * 3, rand() * 3, rand() * 3);
  m.position.set(x, heightAt(x, z) + s * 0.25, z);
  m.castShadow = true;
  m.receiveShadow = true;
  scene.add(m);
  obstacles.push({ x, z, r: s * 1.1 });
}

/* ---------- Low-poly trees ---------- */
const trunkGeo = new THREE.CylinderGeometry(0.14, 0.22, 2.2, 5);
const coneGeoCache = new Map();
function coneGeo(r, h) {
  const key = r.toFixed(2) + '_' + h.toFixed(2);
  if (!coneGeoCache.has(key)) coneGeoCache.set(key, new THREE.ConeGeometry(r, h, 6));
  return coneGeoCache.get(key);
}
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x3a2716, roughness: 0.95, flatShading: true });
const foliageMats = [0x1e3a22, 0x23452a, 0x1a2f20, 0x2b4a2e].map(c =>
  new THREE.MeshStandardMaterial({ color: c, roughness: 0.9, flatShading: true })
);

const trees = [];
let attempts = 0;
while (trees.length < 80 && attempts < 9000) {
  attempts++;
  const a = rand() * Math.PI * 2;
  const d = Math.sqrt(rand()) * 40 + 8.5;   // keep the clearing free
  const x = Math.cos(a) * d, z = Math.sin(a) * d;
  const s = rr(0.85, 1.45);
  const rad = 1.55 * s;
  if (trees.some(t => Math.hypot(t.x - x, t.z - z) < (t.r + rad) * 0.95)) continue;
  if (obstacles.some(o => Math.hypot(o.x - x, o.z - z) < o.r + rad)) continue;
  trees.push({ x, z, s, r: rad });
}

trees.forEach((t, i) => {
  const g = new THREE.Group();
  g.position.set(t.x, heightAt(t.x, t.z) - 0.15, t.z);
  g.rotation.y = rand() * Math.PI * 2;
  g.scale.setScalar(t.s);

  const trunk = new THREE.Mesh(trunkGeo, trunkMat);
  trunk.position.y = 1.0;
  trunk.castShadow = true;
  g.add(trunk);

  const fol = foliageMats[i % foliageMats.length];
  const layers = [
    { y: 2.6, r: 1.5, h: 2.4 },
    { y: 3.65, r: 1.15, h: 2.1 },
    { y: 4.7, r: 0.8, h: 1.8 }
  ];
  for (const L of layers) {
    const m = new THREE.Mesh(coneGeo(L.r, L.h), fol);
    m.position.y = L.y;
    m.rotation.y = rand() * Math.PI;
    m.castShadow = true;
    g.add(m);
  }
  scene.add(g);
});

/* ---------- Fire: flames (layered, additive, animated) ---------- */
const flameMats = {
  outer: new THREE.MeshBasicMaterial({ color: 0xff5a14, transparent: true, opacity: 0.5,
    blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, toneMapped: false }),
  mid:   new THREE.MeshBasicMaterial({ color: 0xffa030, transparent: true, opacity: 0.68,
    blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, toneMapped: false }),
  inner: new THREE.MeshBasicMaterial({ color: 0xffd45c, transparent: true, opacity: 0.82,
    blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, toneMapped: false }),
  core:  new THREE.MeshBasicMaterial({ color: 0xfff2c0, transparent: true, opacity: 0.9,
    blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, toneMapped: false })
};

const flames = [];
function addFlame(kind, count, r, h, offset) {
  for (let i = 0; i < count; i++) {
    const rad = r * rr(0.85, 1.15);
    const hh = h * rr(0.85, 1.2);
    const geo = new THREE.ConeGeometry(rad, hh, 7, 1, true);
    geo.translate(0, hh / 2, 0);
    const m = new THREE.Mesh(geo, flameMats[kind]);
    const a = rand() * Math.PI * 2;
    const off = offset * rand();
    m.userData = {
      ox: Math.cos(a) * off,
      oz: Math.sin(a) * off,
      ph: rand() * Math.PI * 2,
      sp: rr(4.5, 7.5),
      dir: rand() < 0.5 ? -1 : 1
    };
    m.position.set(m.userData.ox, 0.32, m.userData.oz);
    m.castShadow = false;
    scene.add(m);
    flames.push(m);
  }
}
addFlame('outer', 6, 0.42, 1.5, 0.22);
addFlame('mid',   5, 0.28, 1.15, 0.15);
addFlame('inner', 4, 0.17, 0.8, 0.08);
addFlame('core',  1, 0.1, 0.45, 0.0);

// Ember bed on the ground
const ember = new THREE.Mesh(
  new THREE.CircleGeometry(1.0, 10),
  new THREE.MeshBasicMaterial({
    color: 0xff5a10, transparent: true, opacity: 0.55,
    blending: THREE.AdditiveBlending, depthWrite: false, toneMapped: false
  })
);
ember.rotation.x = -Math.PI / 2;
ember.position.y = 0.03;
scene.add(ember);

// Warm glow billboard around the fire
const fireGlow = new THREE.Sprite(new THREE.SpriteMaterial({
  map: radialTexture([
    [0.0, 'rgba(255,160,70,0.95)'],
    [0.35, 'rgba(255,110,30,0.35)'],
    [1.0, 'rgba(255,80,0,0)']
  ]),
  blending: THREE.AdditiveBlending, depthWrite: false, transparent: true, fog: false
}));
fireGlow.position.set(0, 0.9, 0);
fireGlow.scale.set(5.5, 5.5, 1);
scene.add(fireGlow);

/* ---------- Sparks: simple particle system ---------- */
const SN = 220;
const sPos = new Float32Array(SN * 3);
const sCol = new Float32Array(SN * 3);
const sVel = new Float32Array(SN * 3);
const sLife = new Float32Array(SN);
const sAge = new Float32Array(SN);

function respawnSpark(i) {
  const i3 = i * 3;
  sPos[i3]     = rr(-0.15, 0.15);
  sPos[i3 + 1] = rr(0.4, 0.55);
  sPos[i3 + 2] = rr(-0.15, 0.15);
  sVel[i3]     = rr(-0.25, 0.25);
  sVel[i3 + 1] = rr(1.0, 2.4);
  sVel[i3 + 2] = rr(-0.25, 0.25);
  sLife[i] = rr(1.6, 3.2);
  sAge[i] = 0;
}
for (let i = 0; i < SN; i++) {
  respawnSpark(i);
  sAge[i] = rand() * sLife[i]; // stagger so sparks don't start together
}

const sparkGeo = new THREE.BufferGeometry();
const sparkPosAttr = new THREE.BufferAttribute(sPos, 3);
const sparkColAttr = new THREE.BufferAttribute(sCol, 3);
sparkPosAttr.setUsage(THREE.DynamicDrawUsage);
sparkColAttr.setUsage(THREE.DynamicDrawUsage);
sparkGeo.setAttribute('position', sparkPosAttr);
sparkGeo.setAttribute('color', sparkColAttr);

const sparkMat = new THREE.PointsMaterial({
  size: 0.11,
  map: radialTexture([
    [0.0, 'rgba(255,255,255,1)'],
    [0.4, 'rgba(255,255,255,0.6)'],
    [1.0, 'rgba(255,255,255,0)']
  ]),
  vertexColors: true,
  transparent: true,
  depthWrite: false,
  blending: THREE.AdditiveBlending
});
const sparks = new THREE.Points(sparkGeo, sparkMat);
sparks.frustumCulled = false;
scene.add(sparks);

function updateSparks(dt, t) {
  const wind = 0.12 * Math.sin(t * 0.45) + 0.08;
  for (let i = 0; i < SN; i++) {
    const i3 = i * 3;
    sAge[i] += dt;
    if (sAge[i] >= sLife[i]) respawnSpark(i);

    // gentle wind + wobble, slowly decelerating rise
    sVel[i3]     += ((wind - sVel[i3]) * 0.5 + Math.sin(t * 4 + i * 1.7) * 0.4) * dt;
    sVel[i3 + 1] -= 0.55 * dt;
    sVel[i3 + 2] += Math.cos(t * 3.3 + i * 2.1) * 0.25 * dt;

    sPos[i3]     += sVel[i3] * dt;
    sPos[i3 + 1] += sVel[i3 + 1] * dt;
    sPos[i3 + 2] += sVel[i3 + 2] * dt;

    // colour: bright yellow-white -> orange -> red, fading to black (invisible when additive)
    const k = sAge[i] / sLife[i];
    const fade = Math.pow(1 - k, 1.2);
    sCol[i3]     = 1.0 * fade;
    sCol[i3 + 1] = THREE.MathUtils.lerp(0.92, 0.22, k) * fade;
    sCol[i3 + 2] = THREE.MathUtils.lerp(0.55, 0.02, k) * fade;
  }
  sparkPosAttr.needsUpdate = true;
  sparkColAttr.needsUpdate = true;
}

/* ---------- Resize ---------- */
function onResize() {
  const w = window.innerWidth, h = window.innerHeight;
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.setSize(w, h);
  camera.aspect = w / h;
  camera.fov = (w / h) < 0.9 ? 68 : 55; // wider view on portrait/mobile
  camera.updateProjectionMatrix();
}
window.addEventListener('resize', onResize);
onResize();

/* ---------- Animation loop ---------- */
const clock = new THREE.Clock();
function animate() {
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;

  // Flicker the warm point light
  const flick = 1
    + 0.07 * Math.sin(t * 9.1)
    + 0.05 * Math.sin(t * 17.3 + 1.7)
    + 0.035 * Math.sin(t * 31.9 + 0.4)
    + (Math.random() - 0.5) * 0.06;
  fireLight.intensity = BASE_FIRE_I * flick;

  // Flames
  for (const f of flames) {
    const u = f.userData;
    const w = Math.sin(t * u.sp + u.ph);
    const sy = 1 + 0.2 * w + 0.1 * Math.sin(t * u.sp * 2.7 + u.ph * 2);
    const sx = 1 + 0.12 * Math.sin(t * u.sp * 1.6 + u.ph * 3);
    f.scale.set(sx, Math.max(0.3, sy), sx);
    f.position.set(
      u.ox + 0.03 * Math.sin(t * u.sp * 0.8 + u.ph),
      0.32,
      u.oz + 0.03 * Math.cos(t * u.sp * 0.9 + u.ph)
    );
    f.rotation.set(0.05 * Math.sin(t * u.sp + u.ph * 5), t * 0.3 * u.dir + u.ph, 0);
  }

  fireGlow.material.opacity = 0.7 + 0.25 * Math.sin(t * 8.3) * Math.sin(t * 3.1 + 0.6) + 0.05;
  const gs = 5.2 + 0.4 * Math.sin(t * 6.1);
  fireGlow.scale.set(gs, gs, 1);

  stars.material.opacity = 0.8 + 0.2 * Math.sin(t * 0.9);

  updateSparks(dt, t);

  controls.update();
  if (camera.position.y < 0.5) camera.position.y = 0.5; // safety: never go under the ground

  renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
</script>
</body>
</html>
Reasoning trace (22,624 tokens)
I'm setting up a single HTML file that loads Three.js r170 through an import map and pulls in OrbitControls, with no errors or alerts allowed. The scene needs a ground, rocks, logs, a central campfire, 30+ non-burning trees, animated flames, a flickering warm light with shadows, sparks, a starry night sky with a moon, fog, and limited orbit controls.

I'm working out the remaining pieces: polar angle and distance limits for the orbit controls, keeping the camera above the ground, and handling window resizes. The renderer will use antialiasing, PCF soft shadows, ACES tone mapping, and sRGB output, with a dark navy background and exponential fog that matches the sky tone so distant trees fade out. The ground will be a large low-poly plane with jittered vertices, flat shading, and a dark green-brown color that receives shadows.

I'm weighing trees next: low-poly trunks with stacked cones, roughly 70 of them placed randomly outside a clearing around the fire, and I'm checking whether the point light's shadow pass across all those meshes is affordable.

Still setting up the trees and the logs and rocks around the fire.

Still working out the campfire: a ring of stones, crossed logs, and layered flames with additive blending that bob and flicker over time.

I'm placing a point light at the fire's center, about 1.2 units up, with shadows enabled at a 512 map size and a small bias to avoid acne. Its flicker will be a base intensity modulated by layered noise. For physically correct lighting, I'll start around 60 candela with no distance cutoff and decay 2, then check how it reads at a few meters. A dim blue hemisphere light will cover the ambient fill.

Still working through the sparks: a particle system with a few hundred points that rise, drift, and fade from yellow toward red as they age.

Still building the campfire's sparks and the night sky: a circular canvas texture for the points, with additive blending so the particles fade to black as they age. For the stars, I'm placing points on an upper-hemisphere shell and putting a flat-shaded, emissive moon low in the sky.

I'm settling the fog so distant trees and ground fade into the sky, using a fog near of 25 and far of 120. The stars and moon glow need fog turned off so they stay bright, and the camera's far plane goes out to 1000 to fit the sky.

Next I'm placing the trees, picking random angles and radii with the distribution weighted outward, keeping them clear of the center and spaced apart. Then I'm setting up the camera and orbit controls, with distance limits and a polar cap so the view stays above the ground.

I'm setting the camera's polar limits so it can't dip below the ground, since with the target at about 1.2 units up, a polar angle under half a pi keeps the camera higher than the target. I'll also add a minimum-height clamp as a safeguard, disable panning, and try a slow auto-rotate that stops once the user starts dragging.

Next I'm shaping the flames as open-ended, six-sided cones, moved so their bases sit at the origin, and animating each layer's scale and a slight sideways wobble over time. I'm planning about five cone layers in different sizes, with the outer ones in orange and the inner ones brighter.

Still settling the flame materials: basic material, transparent at about 0.85 opacity, additive and depth-write off, double-sided, with tone mapping turned off so the colors stay bright. I'm also skipping a separate glow sphere around the fire, and maybe adding a few embers on the logs. Next are the crossed logs, short brown cylinders with a faint orange emissive tint on the lower ones.

Next I'm placing the logs as seats around the fire, four or five of them lying on the ground about 3.2 units out and angled roughly tangent to the circle. Then about 25 rocks scattered between radius 4 and 30, a ring of ten gray dodecahedrons around the fire at ground level, and the ground itself as a jittered plane that stays flat near the center and rises gently farther out.

I'm making the ground's vertex colors a dark green-brown with slight variation, using flat shading and a rough standard material. Trees will sit on the ground at heights taken from a deterministic height function, so I'm defining that function as small sine waves scaled by a clearing factor.

Still setting up the ground: vertices on a 80×80 grid, displaced by a few sine terms and kept flat near the fire, with the trees planted at whatever height that function gives. I'm also defining a tree builder that takes a scale.

Still writing the trees: a trunk with three stacked cones in a few shared green materials, scaled randomly and turned at random angles. The trees will cast shadows while the ground receives them.

Still working on the scene's stars and moon: a thin hemisphere of points for the sky, plus a flat-shaded moon placed off to one side. I'm also planning a directional light that comes from the moon's direction so the scene's shadows line up with it.

Still setting up the sparks: a pool of a couple hundred particles with position, velocity, life, and age arrays, respawned near the fire base whenever they die.

Still writing the spark system: each particle, once it dies, respawns near the fire base with a rising velocity and a short random life. Its color shifts from pale yellow toward red as it ages while it dims, and I'm keeping the point size attenuated.

Still writing the render loop, the resize handler, and the narrow-screen camera adjustment.

I'm adjusting the camera's field of view on resize for the narrow-screen case, then setting a slightly farther start position for mobile. Next comes a simple overlay hint and a title, with CSS that makes the body fill the viewport with a dark background. I'm also adding a faint orange glow disc on the ground for the ember bed, and a lighter dirt ring around the center where the ground is kept flat.

I'm settling the ground colors: the vertices near the fire will blend from brown dirt into green grass, so I don't need a separate ground disc. For the fog, I'm using a dark bluish color that matches the background, so the horizon fades into the night with a near of 18 and far of 95. The trees at about 48 units will end up fully fogged.

Now I'm adding a gradient sky dome, navy at the top and a lighter blue at the horizon, with fog turned off on it. I'm matching the fog color to that horizon tone so the ground fades smoothly into the sky.

Still working on the sky dome: I'm coloring its vertices with a gradient from the horizon tone up to a darker zenith, using a lerp on the colors, and turning fog off for it.

Setting the shadow setup now: the renderer gets shadows enabled with soft PCF filtering, and the fire's point light gets a 512×512 shadow map with its own near and far planes, a small bias, and a normal bias. The light sits about a unit up, inside the fire, so the flame meshes themselves should be excluded from casting shadows.

Still writing the logs: short cylinders lying horizontally around the fire at radius 3.4, turned to follow the circle, and seated on the ground height.

Still writing the logs: the rings of stones and the crossed logs at the center come next.

Still writing the fire logs: four or five logs in a crossed pattern at the center, each turned to a different angle and set at two alternating heights, with a slight tilt.

Still working on the fire: six outer flames in orange, five mid flames in yellow-orange, and a white-yellow core, each scaled to animate over time.

Still writing the fire flames: a glow sprite around the fire, shared materials for each layer, and the animation for the flame tips.

Still writing the HTML head and page styles: a dark full-screen body with a fixed canvas container and a bottom-left HUD with a title in light text.

Still writing the HTML head and page styles: the HUD now has its title, hint line, and a narrow-screen rule. Next I'm starting the module script that loads Three.js and OrbitControls, and I'll use a seeded random generator so the layout stays the same on every load.

Still writing the renderer setup: a WebGL renderer with antialiasing, pixel ratio capped at 2, sized to the window, and shadow maps switched on.

Still writing the renderer setup: I'm enabling the soft shadow map, adding tone mapping, and appending the canvas to the page. Next is the scene itself, with a dark blue-navy fog that matches the background, and a perspective camera sized to the window.

Still writing the controls: I'm setting the orbit target, damping, zoom limits, and polar angle range, with auto-rotate that stops once the user starts dragging.

Still writing the height function: I'm using a radius-based smoothstep that keeps the area near the fire flat and lets the terrain roll in gently with sine and cosine waves further out.

Still writing the ground: I'm building a 260-unit plane with 90 segments and rotating it flat, and I'm setting up a color array to blend dirt tones with two grass greens across the vertices.

Still writing the ground: I'm jittering the grid vertices slightly, leaving the outer edges fixed, and recomputing each height from the displaced position so the terrain stays consistent.

Still writing the ground: I'm skipping any jitter on the x and z positions so the grid points stay exactly on the height function, and I'm adding only a small random offset to the height, scaled down near the fire.

Still writing the ground colors: I'm blending dirt into grass with a smoothstep on distance from the fire, using random picks between the two greens.

Still writing the sky dome: I'm setting up a large sphere around the scene and getting ready to color its vertices with the gradient.

Still writing the sky dome: I'm sizing the background sphere and filling its vertex colors with a gradient that runs from the fog-matched horizon up to a dark zenith. I'll turn off depth writing and fog for it so it stays behind everything else.

Now I'm adding the stars and the moon, with the dome's depth writing turned off so everything draws over it.

Still writing the stars: I'm placing them uniformly across the upper hemisphere at a large radius, with random brightness and a slight blue, white, or warm tint. The point material is set up to ignore fog and depth so they draw cleanly over the sky dome, and I'm considering a light twinkle on their opacity.

Next I'm setting up the moon: a flat-shaded icosahedron placed low in the sky, with a cool white color that ignores fog and tone mapping. I'm also thinking about giving its faces slightly darker shades so it reads as having craters.

Still writing the moon: I'm giving each triangle of the low-poly icosahedron a slightly different shade so it reads as cratered, and then I'm adding a soft halo around it using a radial gradient texture.

Still writing the moon: I'm adding a soft halo sprite around it, then placing the disc itself at its position in the sky.

Still working on the moon: I'm placing a directional moonlight at the moon's direction, scaled out to 60 units, aiming it at the origin, with a shadow camera covering the scene area.

Still writing the lighting: I'm adding a hemisphere light for soft sky and ground fill, then a warm point light at the fire's center that casts shadows.

Still tuning the flicker light: I'm settling on an intensity of 55 for the fire point light, since 70 looks too bright for the nearby rocks and tree bases.

Still writing the fire lighting and campfire logs: the flicker adds a little random jitter each frame, and I'm giving the burnt logs a separate emissive material.

Still writing the rocks: I'm scattering low-poly stones across the ground at random angles, scaling each one unevenly and sinking it slightly into the terrain.

Still writing the logs: I'm working out the rotation so each one lies flat along the tangent of the ring around the fire.

Still writing the logs: I'm placing each one on a ring around the fire, turning it to lie along the tangent with a little random yaw, and lifting it so its center sits just above the terrain.

Still writing the trees: I'm placing obstacles for the rocks and logs so the trees keep their distance, aiming for around 75 trees in a ring starting out past the clearing.

Still writing the trees: I'm building each one as a group with a scaled trunk and three stacked cones, picking one of four foliage greens by index.

Still writing the trees: I'm finishing the tree builder, stacking three cones of decreasing size over a short trunk and placing each cone at its layer height.

Still writing the trees: I'm placing each one with a random yaw and a scale, keeping them well clear of the clearing and spacing them so none overlap.

Still writing the trees: I'm checking that each one clears the rocks, logs, and other trees before placing it, and I'm planning the flame group next.

Still writing the flames: I'm placing each cone with its base at the top of the logs and a small x/z offset, scaling it over time, and setting up outer, mid, inner, and core materials as additive, unlit layers.

Still writing the flames: I'm setting up the outer, mid, inner, and core layers at decreasing sizes, then animating each one's scale with overlapping sine waves so the fire flickers.

Still writing the flames: I'm nudging each flame's x position and tilting it slightly as it sways, then setting up a soft radial glow sprite at the fire's center that pulses additively.

Still writing the fire glow: I'm adding a soft additive sprite at the campfire center that flickers in opacity, and I'm placing a flat glowing ember bed on the ground just above the terrain.

Still writing the sparks: I'm allocating buffers for about 220 particles with positions, colors, velocities, and lifetimes, then setting up a dynamic position attribute and a color attribute for them.

Still writing the sparks: I'm setting up the particle material with a soft radial texture, additive blending, and vertex colors, then adding the points to the scene.

Still writing the sparks: I'm respawning each particle with a random start near the fire, an upward velocity, and a short random lifetime, then updating positions each frame with drag and a shared wind that drifts them sideways.

Still writing the sparks: I'm updating each particle's age and respawning it once its lifetime runs out, then moving it with a shared wind and fading its color as it ages.

Still writing the sparks: I'm setting each particle's initial age at random so they start out staggered, and I'm writing the per-frame update.

Still writing the animation loop: I'm clamping the camera's height after the controls update, then running the loop through the renderer's animation hook. Next I'm starting on the resize handler, which updates the renderer size and aspect and widens the field of view on narrow screens.

Still writing the camera and resize handling: I'm checking the field of view for narrow screens and confirming the window resize covers iframe sizing too.

Still checking the shadow setup: the directional light's shadow camera covers ±60 and the trees sit within 48, so nothing gets clipped. I'm also making sure the light's target is added to the scene, and that the point light sits at the root rather than inside a group.

Still writing the fire logs: I'm placing four of them at evenly spaced angles around the center, each one at its own rotation.

Still writing the fire logs: I'm placing the four logs in a crossed pile, lifting every other one so the stack reads as a teepee, and giving each a bit of tilt so they overlap at the center.

Still writing the scene: I'm setting up the rock generation with three flat-shaded grey materials on an icosahedron base, and I'm keeping a list of obstacles so later placements avoid them.

Still writing the rocks: I'm scattering about 34 of them at least 5.5 units from the center, scaling each one and stretching it unevenly, then sitting it on the terrain and adding it to the obstacle list.

Still writing the trees: I'm placing each one only where it clears the rocks and logs by its radius, and I'm drawing random positions out past the clearing until I have around 80.

Still writing the trees: I'm checking each candidate against the other trees, the rocks, and the logs, and skipping any spot that's too crowded.

Still writing the animation: I'm computing the flicker from overlapping sine waves and advancing the fire, sparks, and light each frame.

Still writing the animation: I'm adding the flame layers' scale and tilt updates, the glow sprite's pulse, the star twinkle, and a clamp on the camera's height before rendering.

Still writing the animation: I'm updating the flame layers' scale with overlapping sine waves, so each one flickers at its own rate and phase.

Still writing the flames: I'm updating each one's scale, position, and tilt over time so they sway and flicker out of phase with each other.

Still writing the flames: I'm setting the flame base just above the logs at 0.32, scattering their offsets in a small circle, and placing a soft additive glow sprite at the fire's center. I'm also adding a point light there and writing the sky dome, with the moon's faceted shading picked per triangle.

Still writing the sky: I'm checking how the moon's faceted shading is assigned per triangle, since the geometry might be indexed and that would change how I walk the vertices.

Now I'm setting the moon's faceted shading with a light and dark tone per triangle, and checking that vertex colors multiply correctly with the basic material.

Still writing the lighting: I'm adding the point light at the center of the fire with a warm color, an intensity near 55, and no falloff. I'm also giving it a shadow map and setting its shadow camera's near and far planes.

Still writing the sparks: I'm updating the wind drift and stepping through each particle's age and position.

Still writing the sparks: I'm easing each one's sideways speed toward the wind, and slowing its upward speed over time.

Still writing the sparks: I'm easing each one's upward speed down over time, nudging it sideways toward the wind, and shifting its color from bright yellow to dim red as it ages and fades.

Still writing the sparks: I'm adding a small sideways wobble to each one's velocity, then respawning it near the fire base and giving each a random starting age so they stagger.