Qwen3.8 Flash
by QwenReasoning1 console error
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 31 s
- Cost
- $0.0032
- Output tokens
- 6,773
- Spent thinking
- 0 tokens
- Speed
- 75.6 tok/s
- Console errors
- 1
- First token after
- 1.6 s
- First visible text after
- 1.6 s
- Prompt tokens
- 593
- Code size
- 17.3 KB
- Finish reason
- stop
- Served by
- Alibaba
- Model id
qwen/qwen3.8-flash- Reasoning sent
{"effort":"none"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 2026
Console errors
Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.
Failed to load resource: the server responded with a status of 404 ()
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 · 493 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Campfire — Low-Poly Night Forest</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #05060f; }
canvas { display: block; }
.ui {
position: fixed;
left: 20px;
bottom: 18px;
z-index: 10;
color: rgba(232, 234, 246, 0.72);
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 12px;
line-height: 1.65;
letter-spacing: 0.03em;
pointer-events: none;
text-shadow: 0 1px 8px rgba(0,0,0,0.85);
}
.ui b { color: #ff9a4d; font-weight: 700; letter-spacing: 0.08em; }
.ui .dim { color: rgba(200,205,225,0.42); }
.hint {
position: fixed;
top: 18px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
color: rgba(214,220,240,0.55);
font-family: ui-monospace, Menlo, Consolas, monospace;
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
pointer-events: none;
text-shadow: 0 1px 6px rgba(0,0,0,0.8);
}
.vignette {
position: fixed;
inset: 0;
z-index: 5;
pointer-events: none;
background: radial-gradient(ellipse at 50% 42%, transparent 42%, rgba(0,0,0,0.55) 100%);
}
</style>
</head>
<body>
<div class="hint">drag to orbit · scroll to zoom</div>
<div class="ui"><b>CAMPFIRE</b><br><span class="dim">low-poly night forest clearing</span></div>
<div class="vignette"></div>
<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>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
/* ---------- deterministic RNG ---------- */
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 rng = mulberry32(20240617);
const rand=(a,b)=>a+rng()*(b-a);
const pick=(arr)=>arr[(rng()*arr.length)|0];
/* ---------- 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.15;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x070a16);
scene.fog = new THREE.FogExp2(0x0a1020, 0.018);
const camera = new THREE.PerspectiveCamera(52, window.innerWidth/window.innerHeight, 0.1, 600);
camera.position.set(9, 6, 12);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.target.set(0, 1.2, 0);
controls.minDistance = 5;
controls.maxDistance = 30;
controls.minPolarAngle = 0.15; // don't fly over the top
controls.maxPolarAngle = Math.PI*0.49; // stay above ground
controls.autoRotate = true; // self-demo so it's alive on load
controls.autoRotateSpeed = 0.35;
controls.update();
renderer.domElement.addEventListener('pointerdown', ()=>{ controls.autoRotate=false; }, { once:true });
/* ---------- helpers ---------- */
function flatMat(color, opts={}){
const m = new THREE.MeshStandardMaterial({ color, roughness:opts.rough??0.9, metalness:opts.metal??0.0, emissive:opts.emissive??0x000000, emissiveIntensity:opts.ei??1 });
return m;
}
function jitter(geo, amt){
const p = geo.attributes.position;
for(let i=0;i<p.count;i++){ p.setXYZ(i, p.getX(i)+rand(-amt,amt), p.getY(i)+rand(-amt,amt), p.getZ(i)+rand(-amt,amt)); }
p.needsUpdate = true;
geo.computeVertexNormals();
}
/* ---------- lighting ---------- */
const hemi = new THREE.HemisphereLight(0x2a3a66, 0x0a0e1c, 0.5);
scene.add(hemi);
const moon = new THREE.DirectionalLight(0x9fb4e8, 0.5);
moon.position.set(-40, 55, -25);
moon.castShadow = true;
moon.shadow.mapSize.set(2048, 2048);
const mc = moon.shadow.camera;
mc.near = 1; mc.far = 160; mc.left=-45; mc.right=45; mc.top=45; mc.bottom=-45;
moon.shadow.bias = -0.0004;
moon.shadow.normalBias = 0.03;
scene.add(moon);
// campfire point light (flickers + casts shadows)
const fireLight = new THREE.PointLight(0xff7b2e, 3.4, 46, 1.6);
fireLight.position.set(0, 1.3, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.3;
fireLight.shadow.camera.far = 42;
fireLight.shadow.bias = -0.002;
scene.add(fireLight);
// warm glow fill (no shadow) so nearby faces aren't pitch black
const glow = new THREE.PointLight(0xff9a4a, 0.9, 16, 2);
glow.position.set(0, 1.0, 0);
scene.add(glow);
/* ---------- ground ---------- */
const gGeo = new THREE.PlaneGeometry(120, 120, 48, 48);
gGeo.rotateX(-Math.PI/2);
{
const pos = gGeo.attributes.position;
for(let i=0;i<pos.count;i++){
const x=pos.getX(i), z=pos.getZ(i);
const d=Math.hypot(x,z);
let y = Math.sin(x*0.3)*0.12 + Math.cos(z*0.27)*0.12 + Math.sin((x+z)*0.15)*0.08;
if(d<7) y *= smoothstep(0.5,7,d); // flatten clearing
pos.setY(i, y);
}
pos.needsUpdate=true;
gGeo.computeVertexNormals();
}
const ground = new THREE.Mesh(gGeo, flatMat(0x17241a,{rough:1}));
ground.receiveShadow = true;
scene.add(ground);
function smoothstep(a,b,x){ x=Math.max(0,Math.min(1,(x-a)/(b-a))); return x*x*(3-2*x); }
/* ---------- trees ---------- */
const treeGroup = new THREE.Group();
scene.add(treeGroup);
const trunkCols=[0x3a2818,0x33241a,0x42301f,0x2e2015];
const leafCols=[0x1f3a24,0x27452b,0x2c5233,0x223f27,0x1b331f];
const leafMats = leafCols.map(c=>flatMat(c,{rough:0.95}));
const trunkMats = trunkCols.map(c=>flatMat(c,{rough:1}));
function makeTree(){
const g = new THREE.Group();
const th = rand(1.6,2.6), tr = rand(0.22,0.34);
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(tr*0.7,tr,th,6), pick(trunkMats));
trunk.position.y = th/2; trunk.castShadow=true; trunk.receiveShadow=true;
g.add(trunk);
const tiers = 3 + ((rng()*2)|0);
let baseY = th*0.85;
let r = rand(1.3,1.9);
let h = rand(1.4,1.8);
const mat = pick(leafMats);
for(let i=0;i<tiers;i++){
const cone = new THREE.Mesh(new THREE.ConeGeometry(r,h,6), mat);
cone.position.y = baseY + h*0.4;
cone.rotation.y = rand(0,Math.PI);
cone.castShadow=true; cone.receiveShadow=true;
g.add(cone);
baseY += h*0.62;
r *= 0.72;
h *= 0.85;
}
return g;
}
let placed=0, tries=0;
while(placed<36 && tries<4000){
tries++;
const a=rand(0,Math.PI*2), rr=rand(11,46);
const x=Math.cos(a)*rr, z=Math.sin(a)*rr;
if(rr<11) continue;
const s=rand(0.8,1.5);
const t=makeTree();
t.position.set(x,0,z);
t.scale.setScalar(s);
t.rotation.y=rand(0,Math.PI*2);
treeGroup.add(t);
placed++;
}
/* ---------- rocks ---------- */
const rockCols=[0x3b3f47,0x33363d,0x464a52,0x2c2f36];
for(let i=0;i<14;i++){
const g=new THREE.IcosahedronGeometry(rand(0.4,1.1),0);
jitter(g,0.28);
const m=new THREE.Mesh(g, flatMat(pick(rockCols),{rough:1}));
const a=rand(0,Math.PI*2), rr=rand(4.5,13);
m.position.set(Math.cos(a)*rr, rand(-0.15,0.1), Math.sin(a)*rr);
m.rotation.set(rand(0,3),rand(0,3),rand(0,3));
m.scale.y*=rand(0.6,0.9);
m.castShadow=true; m.receiveShadow=true;
scene.add(m);
}
/* ---------- logs around fire ---------- */
const logMat = flatMat(0x4a3320,{rough:1});
const logEndMat = flatMat(0x6b4a2c,{rough:1});
function makeLog(len,rad){
const g=new THREE.Group();
const body=new THREE.Mesh(new THREE.CylinderGeometry(rad,rad,len,7),logMat);
body.rotation.z=Math.PI/2; body.castShadow=true; body.receiveShadow=true;
g.add(body);
for(const sx of [-1,1]){
const e=new THREE.Mesh(new THREE.CircleGeometry(rad*0.98,7),logEndMat);
e.position.x=sx*len/2; e.rotation.y=sx*Math.PI/2; e.receiveShadow=true;
g.add(e);
}
return g;
}
for(let i=0;i<5;i++){
const a=i/5*Math.PI*2+0.4;
const L=makeLog(rand(1.6,2.2), rand(0.28,0.4));
L.position.set(Math.cos(a)*3.1, 0.35, Math.sin(a)*3.1);
L.rotation.y = a + Math.PI/2 + rand(-0.3,0.3);
scene.add(L);
}
/* ---------- stone ring around fire ---------- */
for(let i=0;i<11;i++){
const a=i/11*Math.PI*2;
const g=new THREE.IcosahedronGeometry(rand(0.4,0.6),0);
jitter(g,0.12);
const m=new THREE.Mesh(g, flatMat(pick(rockCols),{rough:1}));
m.position.set(Math.cos(a)*1.75, 0.18, Math.sin(a)*1.75);
m.rotation.set(rand(0,3),rand(0,3),rand(0,3));
m.scale.y*=0.7;
m.castShadow=true; m.receiveShadow=true;
scene.add(m);
}
/* ---------- firewood inside ring ---------- */
for(let i=0;i<3;i++){
const w=makeLog(1.3,0.16);
const a=i/3*Math.PI*2+0.5;
w.position.set(Math.cos(a)*0.3, 0.35, Math.sin(a)*0.3);
w.rotation.y=a;
w.rotation.z=rand(-0.25,0.25);
scene.add(w);
}
/* ---------- embers / glowing coals ---------- */
const emberMat = new THREE.MeshBasicMaterial({ color:0xff5a1e, fog:false });
const emberBase = new THREE.Color(0xff5a1e);
const embers=[];
for(let i=0;i<16;i++){
const m=new THREE.Mesh(new THREE.IcosahedronGeometry(rand(0.08,0.16),0), emberMat.clone());
const a=rand(0,Math.PI*2), rr=rand(0,0.9);
m.position.set(Math.cos(a)*rr, rand(0.12,0.3), Math.sin(a)*rr);
m.userData.ph=rand(0,Math.PI*2);
scene.add(m); embers.push(m);
}
/* ---------- flames ---------- */
const flameMat = new THREE.ShaderMaterial({
transparent:true, depthWrite:false, blending:THREE.AdditiveBlending, side:THREE.DoubleSide,
uniforms:{ uTime:{value:0}, uScale:{value:new THREE.Vector3(1,1,1)} },
vertexShader:`
uniform float uTime; uniform vec3 uScale;
varying vec2 vUv;
void main(){
vUv = uv;
vec3 p = position * uScale;
float up = clamp(p.y, 0.0, 1.0);
float wob = sin(uTime*3.0 + p.y*5.0 + p.x*3.0) * 0.10 * up;
p.x += wob; p.z += cos(uTime*2.6 + p.y*4.0)*0.08*up;
gl_Position = projectionMatrix * modelViewMatrix * vec4(p,1.0);
}`,
fragmentShader:`
precision highp float;
uniform float uTime; varying vec2 vUv;
void main(){
float v = vUv.y;
float n = fract(sin(dot(vUv,vec2(12.9898,78.233)))*43758.5453);
float flick = 0.85 + 0.15*sin(uTime*12.0 + n*6.2831);
vec3 cLow=vec3(0.45,0.08,0.0);
vec3 cMid=vec3(1.0,0.45,0.05);
vec3 cTop=vec3(1.0,0.85,0.45);
vec3 col=mix(cLow,cMid,smoothstep(0.0,0.4,v));
col=mix(col,cTop,smoothstep(0.4,0.9,v));
float edge=smoothstep(0.0,0.18,v)*smoothstep(1.0,0.7,v);
float alpha=edge*flick*0.95;
gl_FragColor=vec4(col,alpha);
}`
});
function makeFlame(h,r,w){
const g=new THREE.ConeGeometry(r,h,w,5,true);
g.translate(0,h/2,0);
const m=new THREE.Mesh(g,flameMat);
m.position.y=0.35;
return m;
}
const flames=[];
function addFlame(h,r,w,ang,dist,scaleSeed){
const f=makeFlame(h,r,w);
f.position.x=Math.cos(ang)*dist;
f.position.z=Math.sin(ang)*dist;
f.userData={h,r,base:h,seed:scaleSeed,ph:rand(0,Math.PI*2)};
scene.add(f); flames.push(f);
}
addFlame(1.7,0.42,6,0,0,1);
addFlame(1.15,0.30,5,Math.PI*0.4,0.28,1);
addFlame(1.05,0.28,5,Math.PI*1.15,0.26,1);
addFlame(0.85,0.24,5,Math.PI*1.7,0.22,1);
addFlame(0.75,0.22,5,Math.PI*0.05,-0.24,1);
/* soft additive core glow */
function radialTex(inner,outer){
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,inner); g.addColorStop(1,outer);
ctx.fillStyle=g; ctx.fillRect(0,0,128,128);
const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; return t;
}
const glowSprite=new THREE.Sprite(new THREE.SpriteMaterial({ map:radialTex('rgba(255,150,60,0.9)','rgba(255,120,40,0)'), blending:THREE.AdditiveBlending, depthWrite:false, fog:false }));
glowSprite.scale.set(3.2,3.2,1); glowSprite.position.set(0,1.0,0); scene.add(glowSprite);
/* ---------- sparks ---------- */
const sparkCount=180;
const sparkPos=new Float32Array(sparkCount*3);
const sparkVel=new Float32Array(sparkCount*3);
const sparkLife=new Float32Array(sparkCount);
const sparkMax=new Float32Array(sparkCount);
function respawnSpark(i){
const a=rand(0,Math.PI*2), rr=rand(0,0.5);
sparkPos[i*3]=Math.cos(a)*rr;
sparkPos[i*3+1]=rand(0.5,1.0);
sparkPos[i*3+2]=Math.sin(a)*rr;
sparkVel[i*3]=rand(-0.35,0.35);
sparkVel[i*3+1]=rand(1.1,2.4);
sparkVel[i*3+2]=rand(-0.35,0.35);
sparkMax[i]=rand(1.4,3.2);
sparkLife[i]=sparkMax[i]*rand(0.2,1.0);
}
for(let i=0;i<sparkCount;i++) respawnSpark(i);
const sparkGeo=new THREE.BufferGeometry();
sparkGeo.setAttribute('position',new THREE.BufferAttribute(sparkPos,3));
const sparkMat=new THREE.PointsMaterial({ size:0.07, map:radialTex('rgba(255,235,190,1)','rgba(255,180,80,0)'), color:0xffffff, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false, sizeAttenuation:true, fog:false });
const sparks=new THREE.Points(sparkGeo,sparkMat);
scene.add(sparks);
/* ---------- smoke ---------- */
const smokeCount=40;
const smokePos=new Float32Array(smokeCount*3);
const smokeData=[];
for(let i=0;i<smokeCount;i++){
smokeData.push({ vy:rand(0.5,1.0), ph:rand(0,Math.PI*2), max:rand(4,7), life:rand(0,7) });
respawnSmoke(i);
}
function respawnSmoke(i){
const a=rand(0,Math.PI*2), rr=rand(0,0.5);
smokePos[i*3]=Math.cos(a)*rr;
smokePos[i*3+1]=rand(1.6,2.2);
smokePos[i*3+2]=Math.sin(a)*rr;
}
const smokeGeo=new THREE.BufferGeometry();
smokeGeo.setAttribute('position',new THREE.BufferAttribute(smokePos,3));
const smokeMat=new THREE.PointsMaterial({ size:1.6, map:radialTex('rgba(120,120,130,0.5)','rgba(60,60,70,0)'), color:0x2a2a33, transparent:true, opacity:0.5, depthWrite:false, sizeAttenuation:true, fog:false });
const smoke=new THREE.Points(smokeGeo,smokeMat);
scene.add(smoke);
/* ---------- stars ---------- */
const starCount=1400;
const starPos=new Float32Array(starCount*3);
const starCol=new Float32Array(starCount*3);
for(let i=0;i<starCount;i++){
const u=rng(), v=rng();
const theta=u*Math.PI*2;
const phi=Math.acos(v); // upper hemisphere biased
const R=rand(150,220);
const y=Math.abs(Math.cos(phi))*R + 20;
starPos[i*3]=Math.sin(phi)*Math.cos(theta)*R;
starPos[i*3+1]=y;
starPos[i*3+2]=Math.sin(phi)*Math.sin(theta)*R;
const b=rand(0.5,1.0);
const tint=pick([[1,1,1],[0.8,0.85,1],[1,0.9,0.75]]);
starCol[i*3]=tint[0]*b; starCol[i*3+1]=tint[1]*b; starCol[i*3+2]=tint[2]*b;
}
const starGeo=new THREE.BufferGeometry();
starGeo.setAttribute('position',new THREE.BufferAttribute(starPos,3));
starGeo.setAttribute('color',new THREE.BufferAttribute(starCol,3));
const stars=new THREE.Points(starGeo,new THREE.PointsMaterial({ size:0.9, sizeAttenuation:true, vertexColors:true, transparent:true, opacity:0.9, fog:false, depthWrite:false }));
scene.add(stars);
/* ---------- moon ---------- */
const moonMesh=new THREE.Mesh(new THREE.IcosahedronGeometry(6,1), new THREE.MeshBasicMaterial({ color:0xdfe6f5, fog:false }));
moonMesh.position.set(-70,70,-60);
scene.add(moonMesh);
const moonHalo=new THREE.Sprite(new THREE.SpriteMaterial({ map:radialTex('rgba(200,215,255,0.7)','rgba(160,180,255,0)'), blending:THREE.AdditiveBlending, depthWrite:false, fog:false }));
moonHalo.scale.set(40,40,1); moonHalo.position.copy(moonMesh.position);
scene.add(moonHalo);
/* ---------- animation ---------- */
const clock=new THREE.Clock();
const _tmp=new THREE.Vector3();
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),0.05);
const t=clock.elapsedTime;
// flames sway + scale pulse
for(const f of flames){
const s=1 + 0.16*Math.sin(t*6+f.userData.ph) + 0.08*Math.sin(t*13+f.userData.seed);
f.userData.base=f.userData.h;
f.scale.set(1, s, 1);
f.rotation.y += dt*0.4;
f.material.uniforms.uTime.value=t;
}
flameMat.uniforms.uTime.value=t;
// fire light flicker
const fl=0.72 + 0.28*Math.sin(t*11) + 0.12*Math.sin(t*23+1.3) + 0.08*Math.sin(t*37);
fireLight.intensity=3.4*fl;
fireLight.position.x=Math.sin(t*7)*0.12;
fireLight.position.z=Math.cos(t*9)*0.12;
glow.intensity=0.9*(0.7+0.3*fl);
glowSprite.scale.setScalar(3.0 + 0.5*fl);
// embers pulsing
for(const e of embers){
const k=0.5+0.5*Math.sin(t*4+e.userData.ph);
e.material.color.copy(emberBase).multiplyScalar(0.6+0.9*k);
}
// sparks
for(let i=0;i<sparkCount;i++){
sparkLife[i]-=dt;
if(sparkLife[i]<=0){ respawnSpark(i); continue; }
sparkVel[i*3+1]+=dt*0.6; // buoyancy
sparkVel[i*3]+=Math.sin(t*2+i)*dt*0.4; // drift
sparkVel[i*3+2]+=Math.cos(t*2+i)*dt*0.4;
sparkPos[i*3]+=sparkVel[i*3]*dt;
sparkPos[i*3+1]+=sparkVel[i*3+1]*dt;
sparkPos[i*3+2]+=sparkVel[i*3+2]*dt;
}
sparkGeo.attributes.position.needsUpdate=true;
// smoke rise
for(let i=0;i<smokeCount;i++){
const d=smokeData[i];
d.life+=dt;
smokePos[i*3+1]+=d.vy*dt;
smokePos[i*3]+=Math.sin(t*0.5+d.ph)*dt*0.15;
smokePos[i*3+2]+=Math.cos(t*0.4+d.ph)*dt*0.15;
if(d.life>d.max){ d.life=0; respawnSmoke(i); }
}
smokeGeo.attributes.position.needsUpdate=true;
// gentle star twinkle via slow rotation
stars.rotation.y += dt*0.005;
controls.update();
renderer.render(scene,camera);
}
animate();
/* ---------- resize ---------- */
window.addEventListener('resize',()=>{
camera.aspect=window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth,window.innerHeight);
});
</script>
</body>
</html>