DeepSeek V4.1 Flash 1017
by DeepSeekReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 27.7 s
- Cost
- $0.0051
- Output tokens
- 9,341
- Spent thinking
- 1 tokens
- Speed
- 344.4 tok/s
- Console errors
- 0
- First token after
- 534 ms
- First visible text after
- 534 ms
- Prompt tokens
- 598
- Code size
- 27.7 KB
- Finish reason
- stop
- Served by
- StepFun
- Model id
deepseek/deepseek-v4.1-flash-1017- Reasoning sent
{"effort":"max"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 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 · 851 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Low-Poly Campfire in the Forest</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #05070f;
}
canvas { display: block; }
#hint {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
padding: 9px 18px;
border-radius: 999px;
background: rgba(10, 14, 26, 0.62);
border: 1px solid rgba(255, 190, 120, 0.28);
color: #ffd9ac;
font: 500 13px/1.4 "Trebuchet MS", system-ui, sans-serif;
letter-spacing: 0.04em;
text-shadow: 0 1px 3px rgba(0,0,0,0.9);
backdrop-filter: blur(4px);
pointer-events: none;
white-space: nowrap;
transition: opacity 0.8s ease;
z-index: 10;
}
#hint.hidden { opacity: 0; }
#title {
position: fixed;
left: 50%;
top: 22px;
transform: translateX(-50%);
color: #cfe0ff;
font: 600 15px/1.3 "Trebuchet MS", system-ui, sans-serif;
letter-spacing: 0.22em;
text-transform: uppercase;
text-shadow: 0 0 18px rgba(120, 170, 255, 0.35), 0 2px 6px #000;
opacity: 0.75;
pointer-events: none;
z-index: 10;
white-space: nowrap;
}
@media (max-width: 520px) {
#title { font-size: 11px; letter-spacing: 0.14em; }
#hint { font-size: 11px; padding: 7px 13px; bottom: 16px; }
}
</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="title">Campfire Clearing</div>
<div id="hint">Drag to orbit · Scroll to zoom</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
/* ------------------------------------------------------------------ *
* Renderer / Scene / Camera
* ------------------------------------------------------------------ */
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
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.15;
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const NIGHT = new THREE.Color(0x060a16);
scene.background = NIGHT;
scene.fog = new THREE.FogExp2(0x070c18, 0.028);
const camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 400);
camera.position.set(8.5, 5.0, 10.5);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.1, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.rotateSpeed = 0.62;
controls.zoomSpeed = 0.75;
controls.minDistance = 4.5;
controls.maxDistance = 26;
controls.maxPolarAngle = Math.PI * 0.5 - 0.055; // stay above the ground
controls.minPolarAngle = 0.22;
controls.enablePan = false;
controls.update();
/* ------------------------------------------------------------------ *
* Lights
* ------------------------------------------------------------------ */
const ambient = new THREE.AmbientLight(0x2a3a66, 0.55);
scene.add(ambient);
const moonLight = new THREE.DirectionalLight(0x8fb4ff, 0.55);
moonLight.position.set(-26, 30, -18);
moonLight.castShadow = true;
moonLight.shadow.mapSize.set(2048, 2048);
moonLight.shadow.camera.near = 1;
moonLight.shadow.camera.far = 90;
moonLight.shadow.camera.left = -26;
moonLight.shadow.camera.right = 26;
moonLight.shadow.camera.top = 26;
moonLight.shadow.camera.bottom = -26;
moonLight.shadow.bias = -0.0012;
moonLight.shadow.normalBias = 0.03;
scene.add(moonLight);
// warm hemisphere fill so the fire-lit side reads nicely
const hemi = new THREE.HemisphereLight(0x22335c, 0x1a1207, 0.35);
scene.add(hemi);
/* Fire light — flickers and casts shadows */
const fireLight = new THREE.PointLight(0xff8a2e, 22, 34, 1.9);
fireLight.position.set(0, 1.5, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.35;
fireLight.shadow.camera.far = 34;
fireLight.shadow.bias = -0.004;
fireLight.shadow.normalBias = 0.04;
scene.add(fireLight);
const fireCoreLight = new THREE.PointLight(0xffd08a, 6, 8, 2.0);
fireCoreLight.position.set(0, 0.9, 0);
scene.add(fireCoreLight);
/* ------------------------------------------------------------------ *
* Helpers
* ------------------------------------------------------------------ */
function rand(a, b) { return a + Math.random() * (b - a); }
const groundMat = new THREE.MeshStandardMaterial({
color: 0x27331f, roughness: 1.0, metalness: 0.0, flatShading: true
});
const rockMat = new THREE.MeshStandardMaterial({ color: 0x5a5f6b, roughness: 0.95, flatShading: true });
const woodMat = new THREE.MeshStandardMaterial({ color: 0x53351e, roughness: 0.95, flatShading: true });
const charMat = new THREE.MeshStandardMaterial({ color: 0x241a12, roughness: 1.0, flatShading: true });
/* ------------------------------------------------------------------ *
* Ground — gently undulating low-poly disc
* ------------------------------------------------------------------ */
function makeGround() {
const size = 150;
const seg = 58;
const geo = new THREE.PlaneGeometry(size, size, seg, seg);
geo.rotateX(-Math.PI / 2);
const pos = geo.attributes.position;
const colors = [];
const base = new THREE.Color(0x2b3a21);
const dark = new THREE.Color(0x1a2417);
const lit = new THREE.Color(0x37482a);
const tmp = new THREE.Color();
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const z = pos.getZ(i);
const d = Math.sqrt(x * x + z * z);
// keep the clearing flat around the fire
const flatten = THREE.MathUtils.smoothstep(d, 3.0, 13.0);
const h =
(Math.sin(x * 0.16) * Math.cos(z * 0.13) * 1.25 +
Math.sin(x * 0.41 + 1.7) * 0.45 +
Math.cos(z * 0.37 - 0.6) * 0.5) * flatten;
pos.setY(i, h - 0.02);
const t = THREE.MathUtils.clamp((h + 1.2) / 2.6, 0, 1);
tmp.copy(dark).lerp(base, t);
if (Math.random() > 0.82) tmp.lerp(lit, 0.55);
// subtle warm tint near the fire
const glow = Math.max(0, 1 - d / 7);
tmp.lerp(new THREE.Color(0x3d2f1f), glow * 0.55);
colors.push(tmp.r, tmp.g, tmp.b);
}
geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
geo.computeVertexNormals();
const mat = new THREE.MeshStandardMaterial({
vertexColors: true, roughness: 1.0, metalness: 0.0, flatShading: true
});
const mesh = new THREE.Mesh(geo, mat);
mesh.receiveShadow = true;
return mesh;
}
scene.add(makeGround());
/* A darker clearing patch right around the fire for contrast */
{
const patchGeo = new THREE.CircleGeometry(3.6, 16);
patchGeo.rotateX(-Math.PI / 2);
const patchMat = new THREE.MeshStandardMaterial({ color: 0x241d15, roughness: 1.0, flatShading: true });
const patch = new THREE.Mesh(patchGeo, patchMat);
patch.position.y = 0.012;
patch.receiveShadow = true;
scene.add(patch);
}
/* ------------------------------------------------------------------ *
* Stars
* ------------------------------------------------------------------ */
function makeStars() {
const count = 1400;
const positions = new Float32Array(count * 3);
const sizes = new Float32Array(count);
const col = new THREE.Color();
for (let i = 0; i < count; i++) {
// distribute on a sphere shell, upper hemisphere biased
const r = rand(120, 190);
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(rand(-0.12, 1)); // mostly above horizon
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
positions[i * 3 + 1] = r * Math.cos(phi) * 0.85 + 6;
positions[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);
sizes[i] = rand(0.7, 2.6);
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
// round soft sprite drawn on a canvas
const c = document.createElement('canvas');
c.width = c.height = 64;
const ctx = c.getContext('2d');
const g = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
g.addColorStop(0, 'rgba(255,255,255,1)');
g.addColorStop(0.35, 'rgba(255,255,255,0.78)');
g.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(32, 32, 32, 0, Math.PI * 2);
ctx.fill();
const tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
const mat = new THREE.ShaderMaterial({
uniforms: { uMap: { value: tex }, uTime: { value: 0 } },
vertexShader: `
attribute float size;
varying float vSize;
varying float vSeed;
uniform float uTime;
void main() {
vSize = size;
vSeed = position.x * 0.13 + position.z * 0.07;
vec4 mv = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mv;
gl_PointSize = size * 2.4;
}
`,
fragmentShader: `
uniform sampler2D uMap;
uniform float uTime;
varying float vSize;
varying float vSeed;
void main() {
vec4 t = texture2D(uMap, gl_PointCoord);
float tw = 0.66 + 0.34 * sin(uTime * 1.7 + vSeed * 22.0);
vec3 tint = mix(vec3(0.80, 0.87, 1.0), vec3(1.0, 0.95, 0.86), fract(vSeed * 3.31));
gl_FragColor = vec4(tint * tw, t.a * tw);
if (gl_FragColor.a < 0.02) discard;
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
const pts = new THREE.Points(geo, mat);
pts.frustumCulled = false;
return pts;
}
const stars = makeStars();
scene.add(stars);
/* ------------------------------------------------------------------ *
* Moon
* ------------------------------------------------------------------ */
function makeMoon() {
const group = new THREE.Group();
const moonGeo = new THREE.IcosahedronGeometry(7, 2);
const moonMat = new THREE.MeshStandardMaterial({
color: 0xf2f4ff, emissive: 0xb9ccff, emissiveIntensity: 1.55,
roughness: 0.9, metalness: 0.0, flatShading: true
});
const moon = new THREE.Mesh(moonGeo, moonMat);
group.add(moon);
// soft halo
const c = document.createElement('canvas');
c.width = c.height = 128;
const ctx = c.getContext('2d');
const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
g.addColorStop(0, 'rgba(200,220,255,0.75)');
g.addColorStop(0.28, 'rgba(150,185,255,0.30)');
g.addColorStop(1, 'rgba(110,150,255,0)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, 128, 128);
const tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
const halo = new THREE.Sprite(new THREE.SpriteMaterial({
map: tex, blending: THREE.AdditiveBlending, depthWrite: false, transparent: true, opacity: 0.95
}));
halo.scale.set(46, 46, 1);
group.add(halo);
group.position.set(-46, 44, -78);
return group;
}
scene.add(makeMoon());
/* ------------------------------------------------------------------ *
* Trees — low poly cones + trunk
* ------------------------------------------------------------------ */
const treeColors = [0x1d3a24, 0x21452a, 0x1a3120, 0x274b2c, 0x173025, 0x2a4a2e];
const trunkColors = [0x3a2617, 0x2f2114, 0x452d1b];
function buildTree() {
const g = new THREE.Group();
const h = rand(4.2, 9.2);
const trunkH = h * rand(0.24, 0.34);
const trunkR = h * rand(0.055, 0.085);
const trunk = new THREE.Mesh(
new THREE.CylinderGeometry(trunkR * 0.72, trunkR, trunkH, 5, 1),
new THREE.MeshStandardMaterial({
color: trunkColors[(Math.random() * trunkColors.length) | 0],
roughness: 1.0, flatShading: true
})
);
trunk.position.y = trunkH * 0.5;
trunk.castShadow = true;
trunk.receiveShadow = true;
g.add(trunk);
const foliageMat = new THREE.MeshStandardMaterial({
color: treeColors[(Math.random() * treeColors.length) | 0],
roughness: 1.0, flatShading: true
});
const tiers = 3 + ((Math.random() * 2) | 0);
let y = trunkH * 0.86;
let radius = h * rand(0.24, 0.32);
for (let i = 0; i < tiers; i++) {
const t = i / tiers;
const coneH = (h - trunkH) * (0.62 - t * 0.1) * rand(0.9, 1.15);
const coneR = radius * (1 - t * 0.42);
const cone = new THREE.Mesh(
new THREE.ConeGeometry(coneR, coneH, 6, 1),
foliageMat
);
cone.position.y = y + coneH * 0.5;
cone.rotation.y = Math.random() * Math.PI;
cone.castShadow = true;
cone.receiveShadow = true;
g.add(cone);
y += coneH * rand(0.58, 0.74);
}
return g;
}
const TREE_COUNT = 44;
let placed = 0, guard = 0;
const treeField = [];
while (placed < TREE_COUNT && guard < 4000) {
guard++;
const angle = Math.random() * Math.PI * 2;
const dist = rand(9.5, 42);
const x = Math.cos(angle) * dist;
const z = Math.sin(angle) * dist;
// avoid the fire / clearing
if (Math.hypot(x, z) < 8.6) continue;
// simple spacing check
let ok = true;
for (const p of treeField) {
if ((p.x - x) ** 2 + (p.z - z) ** 2 < 5.2) { ok = false; break; }
}
if (!ok) continue;
const tree = buildTree();
tree.position.set(x, 0, z);
tree.rotation.y = Math.random() * Math.PI * 2;
const s = rand(0.8, 1.35);
tree.scale.setScalar(s);
scene.add(tree);
treeField.push({ x, z, r: s });
placed++;
}
/* A few extra tiny saplings / bushes nearer the clearing edge */
for (let i = 0; i < 12; i++) {
const a = Math.random() * Math.PI * 2;
const d = rand(6.2, 9.2);
const bush = new THREE.Mesh(
new THREE.IcosahedronGeometry(rand(0.34, 0.68), 0),
new THREE.MeshStandardMaterial({
color: treeColors[(Math.random() * treeColors.length) | 0],
roughness: 1.0, flatShading: true
})
);
bush.position.set(Math.cos(a) * d, rand(0.15, 0.32), Math.sin(a) * d);
bush.rotation.set(Math.random(), Math.random(), Math.random());
bush.scale.y = rand(0.65, 0.95);
bush.castShadow = true;
bush.receiveShadow = true;
scene.add(bush);
}
/* ------------------------------------------------------------------ *
* Rocks
* ------------------------------------------------------------------ */
const rockGroup = new THREE.Group();
scene.add(rockGroup);
function makeRock(radius, detail) {
const geo = new THREE.DodecahedronGeometry(radius, detail);
const p = geo.attributes.position;
const v = new THREE.Vector3();
for (let i = 0; i < p.count; i++) {
v.fromBufferAttribute(p, i);
v.multiplyScalar(rand(0.82, 1.16));
p.setXYZ(i, v.x, v.y * rand(0.62, 0.92), v.z);
}
geo.computeVertexNormals();
const m = new THREE.Mesh(geo, rockMat);
m.castShadow = true;
m.receiveShadow = true;
return m;
}
// fire-ring stones
for (let i = 0; i < 11; i++) {
const a = (i / 11) * Math.PI * 2 + rand(-0.08, 0.08);
const r = rand(1.05, 1.28);
const rock = makeRock(rand(0.2, 0.33), 0);
rock.position.set(Math.cos(a) * r, rand(0.06, 0.16), Math.sin(a) * r);
rock.rotation.set(rand(0, 3), rand(0, 3), rand(0, 3));
rockGroup.add(rock);
}
// scattered big rocks
for (let i = 0; i < 16; i++) {
const a = Math.random() * Math.PI * 2;
const d = rand(3.0, 24);
const rock = makeRock(rand(0.42, 1.35), 0);
rock.position.set(Math.cos(a) * d, rand(0.05, 0.3), Math.sin(a) * d);
rock.rotation.set(rand(0, 3), rand(0, 3), rand(0, 3));
rockGroup.add(rock);
}
/* ------------------------------------------------------------------ *
* Logs around the fire (both seating logs and fire fuel)
* ------------------------------------------------------------------ */
function makeLog(len, rad) {
const geo = new THREE.CylinderGeometry(rad * 0.86, rad, len, 7, 1);
const m = new THREE.Mesh(geo, woodMat);
m.castShadow = true;
m.receiveShadow = true;
// small end-cap detail
const capMat = new THREE.MeshStandardMaterial({ color: 0x8a6134, roughness: 0.95, flatShading: true });
const capGeo = new THREE.CircleGeometry(rad * 0.85, 7);
for (const s of [1, -1]) {
const cap = new THREE.Mesh(capGeo, capMat);
cap.position.y = s * len * 0.5 * 0.995;
cap.rotation.x = s > 0 ? -Math.PI / 2 : Math.PI / 2;
m.add(cap);
}
return m;
}
// seating logs
for (let i = 0; i < 4; i++) {
const a = (i / 4) * Math.PI * 2 + 0.55;
const log = makeLog(rand(2.1, 2.9), rand(0.2, 0.27));
log.rotation.z = Math.PI / 2;
log.rotation.y = -a + Math.PI / 2 + rand(-0.15, 0.15);
log.position.set(Math.cos(a) * 2.85, 0.22, Math.sin(a) * 2.85);
scene.add(log);
}
// fire fuel logs — teepee arrangement
for (let i = 0; i < 5; i++) {
const a = (i / 5) * Math.PI * 2 + 0.3;
const log = makeLog(rand(0.95, 1.25), rand(0.075, 0.1));
log.material = Math.random() > 0.5 ? woodMat : charMat;
log.rotation.z = rand(0.55, 0.75);
log.rotation.y = -a;
log.position.set(Math.cos(a) * 0.42, 0.42, Math.sin(a) * 0.42);
scene.add(log);
}
// glowing ember bed
{
const emberGeo = new THREE.CircleGeometry(0.82, 12);
emberGeo.rotateX(-Math.PI / 2);
const emberMat = new THREE.MeshBasicMaterial({ color: 0xff5a15, transparent: true, opacity: 0.9 });
const embers = new THREE.Mesh(emberGeo, emberMat);
embers.position.y = 0.06;
scene.add(embers);
const glowGeo = new THREE.CircleGeometry(1.9, 20);
glowGeo.rotateX(-Math.PI / 2);
const glowMat = new THREE.MeshBasicMaterial({
color: 0xff8f3a, transparent: true, opacity: 0.18,
blending: THREE.AdditiveBlending, depthWrite: false
});
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.position.y = 0.055;
scene.add(glow);
}
/* ------------------------------------------------------------------ *
* Flames — layered animated low-poly cones
* ------------------------------------------------------------------ */
const flames = [];
const flameGroup = new THREE.Group();
scene.add(flameGroup);
const flamePalette = [0xff3a00, 0xff6a10, 0xff9c22, 0xffc94d, 0xffe08a];
function buildFlame(index, total) {
const t = index / Math.max(1, total - 1);
const geo = new THREE.ConeGeometry(1, 1, 5, 2, true);
// taper the cone a bit for a flame silhouette
const p = geo.attributes.position;
for (let i = 0; i < p.count; i++) {
const y = p.getY(i);
const s = 0.55 + (y + 0.5) * 0.75;
p.setX(i, p.getX(i) * s);
p.setZ(i, p.getZ(i) * s);
}
geo.computeVertexNormals();
const mat = new THREE.MeshBasicMaterial({
color: flamePalette[Math.min(flamePalette.length - 1, index)],
transparent: true,
opacity: 0.92 - t * 0.2,
blending: THREE.AdditiveBlending,
depthWrite: false,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geo, mat);
const size = 0.92 - t * 0.55;
mesh.scale.set(size, size * rand(1.15, 1.7), size);
mesh.position.y = 0.18 + t * 0.3;
mesh.rotation.y = Math.random() * Math.PI * 2;
flameGroup.add(mesh);
flames.push({
mesh,
baseY: mesh.position.y,
baseScale: size,
speed: rand(2.6, 4.6) + t * 2.0,
phase: Math.random() * Math.PI * 2,
sway: rand(0.05, 0.16) + t * 0.1
});
}
for (let i = 0; i < 9; i++) buildFlame(i, 9);
/* ------------------------------------------------------------------ *
* Sparks — instanced-ish point particle system
* ------------------------------------------------------------------ */
const SPARK_COUNT = 220;
const sparkPositions = new Float32Array(SPARK_COUNT * 3);
const sparkColors = new Float32Array(SPARK_COUNT * 3);
const sparkSizes = new Float32Array(SPARK_COUNT);
const sparkData = [];
const sparkGeo = new THREE.BufferGeometry();
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));
sparkGeo.setAttribute('color', new THREE.BufferAttribute(sparkColors, 3));
sparkGeo.setAttribute('aSize', new THREE.BufferAttribute(sparkSizes, 1));
const sparkCanvas = document.createElement('canvas');
sparkCanvas.width = sparkCanvas.height = 64;
{
const ctx = sparkCanvas.getContext('2d');
const g = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
g.addColorStop(0, 'rgba(255,255,255,1)');
g.addColorStop(0.25, 'rgba(255,235,180,0.95)');
g.addColorStop(0.6, 'rgba(255,150,40,0.42)');
g.addColorStop(1, 'rgba(255,90,0,0)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, 64, 64);
}
const sparkTex = new THREE.CanvasTexture(sparkCanvas);
sparkTex.colorSpace = THREE.SRGBColorSpace;
const sparkMat = new THREE.ShaderMaterial({
uniforms: { uMap: { value: sparkTex } },
vertexShader: `
attribute float aSize;
varying vec3 vColor;
void main() {
vColor = color;
vec4 mv = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mv;
gl_PointSize = aSize * (300.0 / -mv.z);
}
`,
fragmentShader: `
uniform sampler2D uMap;
varying vec3 vColor;
void main() {
vec4 t = texture2D(uMap, gl_PointCoord);
gl_FragColor = vec4(vColor * 1.5, 1.0) * t;
if (gl_FragColor.a < 0.01) discard;
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true
});
function spawnSpark(i, fresh) {
const a = Math.random() * Math.PI * 2;
const r = Math.random() * 0.55;
sparkData[i] = {
x: Math.cos(a) * r,
y: fresh ? rand(0.15, 1.2) : rand(0.2, 0.5),
z: Math.sin(a) * r,
vx: rand(-0.22, 0.22),
vy: rand(0.9, 2.3),
vz: rand(-0.22, 0.22),
life: 0,
maxLife: rand(1.1, 2.6),
size: rand(0.6, 2.4),
wob: Math.random() * Math.PI * 2,
wobSpeed: rand(1.4, 3.4)
};
}
for (let i = 0; i < SPARK_COUNT; i++) spawnSpark(i, true);
function updateSparks(dt, time) {
for (let i = 0; i < SPARK_COUNT; i++) {
const s = sparkData[i];
s.life += dt;
if (s.life > s.maxLife) { spawnSpark(i, false); continue; }
const f = s.life / s.maxLife;
s.vy -= dt * 0.22; // gentle gravity
s.x += (s.vx + Math.sin(time * s.wobSpeed + s.wob) * 0.42) * dt;
s.y += s.vy * dt;
s.z += (s.vz + Math.cos(time * s.wobSpeed * 0.8 + s.wob) * 0.42) * dt;
const i3 = i * 3;
sparkPositions[i3] = s.x;
sparkPositions[i3 + 1] = s.y;
sparkPositions[i3 + 2] = s.z;
// warm white -> orange -> red as it dies
const heat = 1.0 - f;
const cr = 1.0;
const cg = 0.32 + heat * 0.62;
const cb = 0.02 + heat * heat * 0.45;
const fade = Math.min(1, (1 - f) * 2.2) * Math.min(1, f * 9 + 0.15);
sparkColors[i3] = cr * fade;
sparkColors[i3 + 1] = cg * fade;
sparkColors[i3 + 2] = cb * fade;
sparkSizes[i] = s.size * (0.45 + heat * 0.95);
}
sparkGeo.attributes.position.needsUpdate = true;
sparkGeo.attributes.color.needsUpdate = true;
sparkGeo.attributes.aSize.needsUpdate = true;
}
const sparks = new THREE.Points(sparkGeo, sparkMat);
sparks.frustumCulled = false;
scene.add(sparks);
/* ------------------------------------------------------------------ *
* Actually a soft additive glow billboard for the fire base
* ------------------------------------------------------------------ */
{
const c = document.createElement('canvas');
c.width = c.height = 128;
const ctx = c.getContext('2d');
const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
g.addColorStop(0, 'rgba(255,190,110,0.85)');
g.addColorStop(0.35, 'rgba(255,120,30,0.34)');
g.addColorStop(1, 'rgba(255,70,0,0)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, 128, 128);
const tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: tex, blending: THREE.AdditiveBlending, depthWrite: false, transparent: true, opacity: 0.9
}));
sprite.scale.set(5.4, 5.4, 1);
sprite.position.set(0, 0.95, 0);
scene.add(sprite);
window.__fireGlow = sprite;
}
/* ------------------------------------------------------------------ *
* A little smoke above the fire
* ------------------------------------------------------------------ */
const smokePuffs = [];
{
const smokeMat = new THREE.MeshBasicMaterial({
color: 0x2b2a30, transparent: true, opacity: 0.16, depthWrite: false, flatShading: true
});
for (let i = 0; i < 9; i++) {
const m = new THREE.Mesh(new THREE.IcosahedronGeometry(0.45, 0), smokeMat.clone());
m.position.set(rand(-0.2, 0.2), rand(1.6, 3.0), rand(-0.2, 0.2));
scene.add(m);
smokePuffs.push({ mesh: m, t: Math.random() });
}
}
/* ------------------------------------------------------------------ *
* Camera hint fade
* ------------------------------------------------------------------ */
const hintEl = document.getElementById('hint');
let interacted = false;
function markInteracted() {
if (interacted) return;
interacted = true;
hintEl.classList.add('hidden');
}
renderer.domElement.addEventListener('pointerdown', markInteracted, { passive: true });
renderer.domElement.addEventListener('wheel', markInteracted, { passive: true });
window.addEventListener('keydown', markInteracted, { passive: true });
setTimeout(() => hintEl.classList.add('hidden'), 9000);
/* ------------------------------------------------------------------ *
* Animation
* ------------------------------------------------------------------ */
const clock = new THREE.Clock();
let flickerSeed = 0;
function animate() {
const dt = Math.min(clock.getDelta(), 0.05);
const t = clock.elapsedTime;
/* --- flames --- */
for (const f of flames) {
const n = Math.sin(t * f.speed + f.phase) * 0.5 + 0.5;
const n2 = Math.sin(t * f.speed * 1.83 + f.phase * 2.1) * 0.5 + 0.5;
const sy = f.baseScale * (1.15 + n * 0.55 + n2 * 0.22);
const sxz = f.baseScale * (0.82 + n2 * 0.32 - n * 0.12);
f.mesh.scale.set(sxz, sy, sxz);
f.mesh.position.y = f.baseY + n * 0.1;
f.mesh.position.x = Math.sin(t * f.speed * 0.72 + f.phase) * f.sway;
f.mesh.position.z = Math.cos(t * f.speed * 0.61 + f.phase * 1.4) * f.sway;
f.mesh.rotation.y += dt * (0.6 + f.speed * 0.18);
f.mesh.material.opacity = (0.9 - f.baseScale * 0.12) * (0.68 + n * 0.42);
}
/* --- fire light flicker --- */
flickerSeed += dt;
const fl =
Math.sin(flickerSeed * 11.3) * 0.5 +
Math.sin(flickerSeed * 23.7 + 1.3) * 0.3 +
Math.sin(flickerSeed * 5.1 + 0.7) * 0.2;
const baseIntensity = 21;
fireLight.intensity = baseIntensity + fl * 7.5;
fireLight.position.set(
Math.sin(flickerSeed * 3.1) * 0.22,
1.45 + Math.sin(flickerSeed * 4.7 + 0.4) * 0.16,
Math.cos(flickerSeed * 2.7 + 1.1) * 0.22
);
fireLight.color.setHSL(0.062 + fl * 0.012, 0.95, 0.54);
fireCoreLight.intensity = 5 + Math.sin(flickerSeed * 9.1 + 2.2) * 2.2;
fireCoreLight.position.y = 0.85 + Math.sin(flickerSeed * 6.3) * 0.12;
if (window.__fireGlow) {
const s = 5.1 + Math.sin(flickerSeed * 7.7) * 0.55 + Math.sin(flickerSeed * 3.1) * 0.35;
window.__fireGlow.scale.set(s, s, 1);
window.__fireGlow.material.opacity = 0.72 + Math.sin(flickerSeed * 8.9) * 0.16;
}
/* --- sparks --- */
updateSparks(dt, t);
/* --- smoke drift --- */
for (const p of smokePuffs) {
p.t += dt * 0.16;
if (p.t > 1) p.t -= 1;
const k = p.t;
p.mesh.position.y = 1.55 + k * 4.2;
p.mesh.position.x = Math.sin(k * 4.1 + p.mesh.id) * 0.55 * k + k * 0.4;
p.mesh.position.z = Math.cos(k * 3.3 + p.mesh.id * 1.7) * 0.5 * k;
const sc = 0.55 + k * 2.1;
p.mesh.scale.setScalar(sc);
p.mesh.material.opacity = 0.17 * Math.sin(k * Math.PI);
p.mesh.rotation.y += dt * 0.25;
p.mesh.rotation.x += dt * 0.13;
}
/* --- slow star twinkle --- */
stars.material.uniforms.uTime.value = t;
/* --- very slow scene breathing for trees? subtle sway --- */
// (kept static for performance & style)
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
/* ------------------------------------------------------------------ *
* Resize
* ------------------------------------------------------------------ */
window.addEventListener('resize', () => {
const w = window.innerWidth;
const h = window.innerHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(w, h);
});
</script>
</body>
</html>