Custom Code Experiment •
Dragon 2: ThreeJS
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Y Ddraig Goch — Three.js</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x080000);
scene.fog = new THREE.FogExp2(0x0a0000, 0.035);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200);
camera.position.set(0, 2, 18);
camera.lookAt(0, 1, 0);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
const ambientLight = new THREE.AmbientLight(0x1a0000, 1.0);
scene.add(ambientLight);
const fireLight = new THREE.PointLight(0xff5500, 8, 14);
fireLight.position.set(-4.5, 1.2, 1.5);
fireLight.castShadow = true;
scene.add(fireLight);
const fillLight = new THREE.DirectionalLight(0xff2200, 0.4);
fillLight.position.set(2, 6, 4);
scene.add(fillLight);
const rimLight = new THREE.DirectionalLight(0x220000, 0.6);
rimLight.position.set(-4, 3, -3);
scene.add(rimLight);
function makeFlag() {
const group = new THREE.Group();
const redGeo = new THREE.PlaneGeometry(10, 4.5);
const redMat = new THREE.MeshLambertMaterial({ color: 0xcc0010, side: THREE.DoubleSide });
const redMesh = new THREE.Mesh(redGeo, redMat);
redMesh.position.set(0, 1.125, 0);
redMesh.receiveShadow = true;
group.add(redMesh);
const creamGeo = new THREE.PlaneGeometry(10, 4.5);
const creamMat = new THREE.MeshLambertMaterial({ color: 0xede7cc, side: THREE.DoubleSide });
const creamMesh = new THREE.Mesh(creamGeo, creamMat);
creamMesh.position.set(0, -1.125, 0);
creamMesh.receiveShadow = true;
group.add(creamMesh);
const borderMat = new THREE.MeshLambertMaterial({ color: 0x4a3000 });
const bW = 10.25, bH = 9.1;
const borders = [
{ s:[bW,0.08,0.08], p:[0, 4.55, 0.01] },
{ s:[bW,0.08,0.08], p:[0,-4.55, 0.01] },
{ s:[0.08,bH,0.08], p:[-5.125, 0, 0.01] },
{ s:[0.08,bH,0.08], p:[ 5.125, 0, 0.01] },
];
borders.forEach(b => {
const m = new THREE.Mesh(new THREE.BoxGeometry(...b.s), borderMat);
m.position.set(...b.p);
group.add(m);
});
group.position.set(0, 0, 0);
return group;
}
const flagGroup = makeFlag();
scene.add(flagGroup);
const dragonMat = new THREE.MeshPhongMaterial({
color: 0xdd0018,
emissive: 0x220000,
specular: 0x440000,
shininess: 30,
side: THREE.DoubleSide,
});
const darkMat = new THREE.MeshPhongMaterial({
color: 0x880010,
emissive: 0x100000,
specular: 0x220000,
shininess: 15,
side: THREE.DoubleSide,
});
const wingMat = new THREE.MeshPhongMaterial({
color: 0xaa0014,
emissive: 0x150000,
specular: 0x330000,
shininess: 10,
transparent: true,
opacity: 0.88,
side: THREE.DoubleSide,
});
function extrudeShape(points, depth) {
const shape = new THREE.Shape();
shape.moveTo(points[0][0], points[0][1]);
for (let i = 1; i < points.length; i++) shape.lineTo(points[i][0], points[i][1]);
shape.closePath();
return new THREE.ExtrudeGeometry(shape, {
depth,
bevelEnabled: true,
bevelThickness: 0.04,
bevelSize: 0.04,
bevelSegments: 2,
});
}
function makeEllipsoid(rx, ry, rz) {
const g = new THREE.SphereGeometry(1, 24, 16);
g.scale(rx, ry, rz);
return g;
}
const dragon = new THREE.Group();
const bodyGeo = makeEllipsoid(2.2, 1.5, 0.9);
const body = new THREE.Mesh(bodyGeo, dragonMat);
body.position.set(0.2, 0, 0.1);
body.castShadow = true;
dragon.add(body);
const neckPts = [[-0.45,0],[0.45,0],[0.35,2.2],[-0.25,2.2]];
const neckGeo = extrudeShape(neckPts, 0.55);
const neck = new THREE.Mesh(neckGeo, dragonMat);
neck.position.set(-1.35, 0.5, -0.2);
neck.rotation.z = 0.35;
neck.castShadow = true;
dragon.add(neck);
const headGeo = makeEllipsoid(1.15, 0.75, 0.65);
const head = new THREE.Mesh(headGeo, dragonMat);
head.position.set(-2.8, 1.65, 0);
head.castShadow = true;
dragon.add(head);
const snoutGeo = makeEllipsoid(0.9, 0.42, 0.38);
const snout = new THREE.Mesh(snoutGeo, dragonMat);
snout.position.set(-3.65, 1.35, 0);
snout.castShadow = true;
dragon.add(snout);
const jawGeo = makeEllipsoid(0.82, 0.28, 0.3);
const jaw = new THREE.Mesh(jawGeo, dragonMat);
jaw.position.set(-3.6, 1.06, 0);
jaw.rotation.z = 0.18;
dragon.add(jaw);
function makeHorn(rx, ry, rz, px, py, pz, rotZ) {
const g = new THREE.ConeGeometry(0.12, 0.9, 8);
const m = new THREE.Mesh(g, darkMat);
m.scale.set(rx, ry, rz);
m.position.set(px, py, pz);
m.rotation.z = rotZ;
return m;
}
dragon.add(makeHorn(1,1,0.6, -2.4, 2.62, 0.1, 0.3));
dragon.add(makeHorn(1,1,0.6, -2.7, 2.55, 0.1, -0.1));
dragon.add(makeHorn(1,1,0.6, -2.2, 2.48,-0.2, 0.5));
const eyeGeo = new THREE.SphereGeometry(0.16, 12, 12);
const eyeMat = new THREE.MeshPhongMaterial({ color: 0xff6600, emissive: 0xff3300, shininess: 80 });
const eye = new THREE.Mesh(eyeGeo, eyeMat);
eye.position.set(-2.58, 1.88, 0.54);
dragon.add(eye);
const pupilGeo = new THREE.CylinderGeometry(0.03, 0.03, 0.28, 8);
const pupilMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
const pupil = new THREE.Mesh(pupilGeo, pupilMat);
pupil.rotation.x = Math.PI / 2;
pupil.position.set(-2.58, 1.88, 0.7);
dragon.add(pupil);
const toothMat = new THREE.MeshPhongMaterial({ color: 0xf0e8cc, shininess: 60 });
[-0.2, 0, 0.2].forEach((z, i) => {
const g = new THREE.ConeGeometry(0.06, 0.28, 6);
const m = new THREE.Mesh(g, toothMat);
m.position.set(-4.32, 1.06, z);
m.rotation.z = Math.PI;
dragon.add(m);
});
const wingPts = [
[0,0],[0.3,0.5],[2.5,2.8],[4,1.8],[3.2,1.2],[3.8,0.5],
[3.0,0.2],[3.4,-0.4],[2.4,0],[1.8,-0.6],[0.5,-0.3]
];
const wingGeo = extrudeShape(wingPts, 0.04);
const wingMesh = new THREE.Mesh(wingGeo, wingMat);
wingMesh.position.set(-0.8, 0.5, 0.45);
wingMesh.rotation.y = -0.15;
wingMesh.castShadow = true;
dragon.add(wingMesh);
const spineMat = new THREE.MeshPhongMaterial({ color: 0x770010 });
[[0,0,0, 2.5,2.8,0],[0,0,0, 4,1.8,0],[0,0,0, 3.8,0.5,0],[0,0,0, 3.4,-0.4,0]].forEach(([x1,y1,z1,x2,y2,z2]) => {
const dir = new THREE.Vector3(x2-x1, y2-y1, z2-z1);
const len = dir.length();
const mid = new THREE.Vector3((x1+x2)/2, (y1+y2)/2, (z1+z2)/2);
const cyl = new THREE.CylinderGeometry(0.04, 0.02, len, 6);
const mesh = new THREE.Mesh(cyl, spineMat);
mesh.position.set(mid.x - 0.8, mid.y + 0.5, mid.z + 0.55);
mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0), dir.normalize());
dragon.add(mesh);
});
const tailGroup = new THREE.Group();
const tailPts = [
[-0.4,0],[0.4,0],[2.0,0.4],[3.4,0.1],[4.2,-0.5],
[4.0,-1.0],[3.2,-0.7],[2.8,-0.4],[1.8,0],[0.1,-0.35]
];
const tailGeo = extrudeShape(tailPts, 0.45);
const tailMesh = new THREE.Mesh(tailGeo, dragonMat);
tailMesh.position.set(1.2, -0.4, -0.2);
tailMesh.castShadow = true;
tailGroup.add(tailMesh);
[1.8,2.6,3.4].forEach((tx, i) => {
const g = new THREE.ConeGeometry(0.08, 0.45, 6);
const m = new THREE.Mesh(g, darkMat);
m.position.set(tx + 1.2, 0.4 - i * 0.12, -0.2);
m.rotation.z = -0.3 + i * 0.15;
tailGroup.add(m);
});
dragon.add(tailGroup);
function makeLeg(px, py, pz, rx, ry) {
const g = new THREE.Group();
const upper = new THREE.Mesh(new THREE.CylinderGeometry(0.22,0.16,1.0,10), dragonMat);
upper.position.set(0, -0.5, 0);
upper.rotation.z = rx; upper.rotation.x = ry;
g.add(upper);
const lower = new THREE.Mesh(new THREE.CylinderGeometry(0.15,0.1,0.9,10), dragonMat);
lower.position.set(Math.sin(rx)*0.9, -1.15, 0);
lower.rotation.z = rx * 0.5;
g.add(lower);
[-0.12, 0, 0.12].forEach(cz => {
const c = new THREE.Mesh(new THREE.ConeGeometry(0.06, 0.28, 6), darkMat);
c.position.set(Math.sin(rx)*0.9, -1.72, cz);
c.rotation.z = Math.PI + 0.2;
g.add(c);
});
g.position.set(px, py, pz);
return g;
}
dragon.add(makeLeg(-0.8, -0.9, 0.7, 0.5, 0));
dragon.add(makeLeg( 1.2, -1.0, 0.7, -0.3, 0));
[-1.2, -0.4, 0.4, 1.2].forEach((bx, i) => {
const g = new THREE.ConeGeometry(0.07, 0.55 - i*0.05, 6);
const m = new THREE.Mesh(g, darkMat);
m.position.set(bx, 1.5 + i * 0.04, 0);
dragon.add(m);
});
for (let i = 0; i < 5; i++) {
const g = new THREE.SphereGeometry(0.22, 8, 6);
g.scale(1, 0.35, 1);
const m = new THREE.Mesh(g, new THREE.MeshPhongMaterial({ color: 0xbb0018, emissive: 0x0a0000 }));
m.position.set(-0.6 + i * 0.45, -0.9 + Math.abs(i-2)*0.1, 0.88);
dragon.add(m);
}
dragon.position.set(0.5, 0.5, 0.5);
scene.add(dragon);
const FLAME_COUNT = 1200;
const SPARK_COUNT = 220;
const SMOKE_COUNT = 160;
function makeSprite(stops) {
const c = document.createElement('canvas');
c.width = 128; c.height = 128;
const cx = c.getContext('2d');
const grd = cx.createRadialGradient(64,64,0, 64,64,64);
stops.forEach(s => grd.addColorStop(s.pos, s.color));
cx.fillStyle = grd;
cx.fillRect(0,0,128,128);
return new THREE.CanvasTexture(c);
}
const flameTex = makeSprite([
{pos:0.00, color:'rgba(255,255,220,1)'},
{pos:0.20, color:'rgba(255,180,20,0.85)'},
{pos:0.50, color:'rgba(255,60,0,0.5)'},
{pos:0.80, color:'rgba(180,0,0,0.15)'},
{pos:1.00, color:'rgba(0,0,0,0)'},
]);
const sparkTex = makeSprite([
{pos:0.00, color:'rgba(255,255,180,1)'},
{pos:0.30, color:'rgba(255,140,0,0.7)'},
{pos:1.00, color:'rgba(0,0,0,0)'},
]);
const smokeTex = makeSprite([
{pos:0.00, color:'rgba(50,15,10,0.55)'},
{pos:0.50, color:'rgba(25,5,5,0.2)'},
{pos:1.00, color:'rgba(0,0,0,0)'},
]);
const emberTex = makeSprite([
{pos:0.00, color:'rgba(255,200,80,1)'},
{pos:0.40, color:'rgba(255,80,0,0.6)'},
{pos:1.00, color:'rgba(0,0,0,0)'},
]);
function makeParticleSystem(count, tex) {
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 4);
const sizes = new Float32Array(count);
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.BufferAttribute(colors, 4));
geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const mat = new THREE.ShaderMaterial({
uniforms: { tex: { value: tex } },
vertexShader: `
attribute float size;
attribute vec4 color;
varying vec4 vColor;
void main(){
vColor = color;
vec4 mv = modelViewMatrix * vec4(position,1.0);
gl_PointSize = size * (380.0 / -mv.z);
gl_Position = projectionMatrix * mv;
}
`,
fragmentShader: `
uniform sampler2D tex;
varying vec4 vColor;
void main(){
vec4 t = texture2D(tex, gl_PointCoord);
gl_FragColor = vec4(vColor.rgb, vColor.a * t.a);
if(gl_FragColor.a < 0.005) discard;
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
const points = new THREE.Points(geo, mat);
points.frustumCulled = false;
return { points, geo, positions, colors, sizes };
}
const flames = makeParticleSystem(FLAME_COUNT, flameTex);
const sparks = makeParticleSystem(SPARK_COUNT, sparkTex);
const smokes = makeParticleSystem(SMOKE_COUNT, smokeTex);
const embers = makeParticleSystem(SPARK_COUNT, emberTex);
scene.add(smokes.points);
scene.add(flames.points);
scene.add(sparks.points);
scene.add(embers.points);
const MOUTH_LOCAL = new THREE.Vector3(-4.55, 1.2, 0.0);
function getMouthWorld() {
return MOUTH_LOCAL.clone().applyMatrix4(dragon.matrixWorld);
}
function noise1(x) { return Math.sin(x * 1.7) * 0.5 + Math.sin(x * 4.3 + 1.2) * 0.3 + Math.sin(x * 9.7 + 2.8) * 0.2; }
function noise2(x) { return Math.sin(x * 2.3 + 0.5) * 0.5 + Math.sin(x * 5.9 + 3.1) * 0.3 + Math.sin(x * 11.3 + 0.9) * 0.2; }
function spawnParticle(role, randomLife) {
const m = getMouthWorld();
const ox = m.x + (Math.random()-0.5)*0.18;
const oy = m.y + (Math.random()-0.5)*0.14;
const oz = m.z + (Math.random()-0.5)*0.18;
let vx, vy, vz, maxLife, size, drag, riseRate;
if (role === 'jet') {
const speed = 3.5 + Math.random() * 4.5;
const hSpread = 0.18 + Math.random() * 0.12;
const vSpread = 0.14 + Math.random() * 0.08;
vx = -speed;
vy = (Math.random()-0.5) * speed * vSpread;
vz = (Math.random()-0.5) * speed * hSpread;
maxLife = 0.55 + Math.random() * 0.55;
size = 0.22 + Math.random() * 0.28;
drag = 0.18;
riseRate = 0.25;
} else if (role === 'bloom') {
const speed = 1.2 + Math.random() * 2.2;
const hSpread = 0.45 + Math.random() * 0.35;
const vSpread = 0.40 + Math.random() * 0.30;
vx = -(speed * (0.5 + Math.random() * 0.5));
vy = (Math.random()-0.5) * speed * vSpread * 1.4;
vz = (Math.random()-0.5) * speed * hSpread * 1.8;
maxLife = 0.9 + Math.random() * 0.9;
size = 0.45 + Math.random() * 0.7;
drag = 0.28;
riseRate = 0.55;
} else if (role === 'spark') {
const speed = 5 + Math.random() * 7;
const angle = Math.PI + (Math.random()-0.5) * 0.7;
vx = Math.cos(angle) * speed;
vy = (Math.random() * 0.6 - 0.1) * speed;
vz = (Math.random()-0.5) * speed * 0.55;
maxLife = 0.4 + Math.random() * 0.7;
size = 0.06 + Math.random() * 0.09;
drag = 0.12;
riseRate = -0.4;
} else if (role === 'ember') {
const speed = 0.6 + Math.random() * 1.4;
vx = -speed * (0.3 + Math.random() * 0.7);
vy = (Math.random() * 0.8 - 0.1) * speed;
vz = (Math.random()-0.5) * speed * 1.2;
maxLife = 1.4 + Math.random() * 1.6;
size = 0.04 + Math.random() * 0.07;
drag = 0.05;
riseRate = 0.1;
} else {
const speed = 0.3 + Math.random() * 0.5;
vx = -speed * (0.2 + Math.random() * 0.4);
vy = (Math.random()-0.5) * 0.3;
vz = (Math.random()-0.5) * 0.5;
maxLife = 1.8 + Math.random() * 2.2;
size = 0.6 + Math.random() * 1.1;
drag = 0.55;
riseRate = 0.55;
}
return {
x: ox, y: oy, z: oz,
vx, vy, vz,
life: randomLife ? Math.random() * maxLife : 0,
maxLife, size, drag, riseRate,
noiseOffset: Math.random() * 100,
noiseScale: 0.8 + Math.random() * 1.6,
role,
};
}
const flameState = Array.from({length: FLAME_COUNT}, (_, i) =>
spawnParticle(i < FLAME_COUNT * 0.6 ? 'jet' : 'bloom', true));
const sparkState = Array.from({length: SPARK_COUNT}, () => spawnParticle('spark', true));
const smokeState = Array.from({length: SMOKE_COUNT}, () => spawnParticle('smoke', true));
const emberState = Array.from({length: SPARK_COUNT}, () => spawnParticle('ember', true));
function flameRGB(t, isBloom) {
const boost = isBloom ? 0.08 : 0;
const tt = Math.min(1, t + boost);
if (tt < 0.10) return [1.00, 1.00, 0.92];
if (tt < 0.28) { const s=(tt-0.10)/0.18; return [1.00, 1.0-s*0.45, 0.0]; }
if (tt < 0.58) { const s=(tt-0.28)/0.30; return [1.00, (1-s)*0.55, 0.0]; }
if (tt < 0.82) { const s=(tt-0.58)/0.24; return [1.0-s*0.3, 0.0, 0.0]; }
{ const s=(tt-0.82)/0.18; return [0.7-s*0.5, 0.0, 0.0]; }
}
function flameAlpha(t) {
if (t < 0.06) return t / 0.06;
if (t < 0.55) return 1.0;
return 1.0 - (t - 0.55) / 0.45;
}
function updatePool(stateArr, sys, dt, elapsed) {
const pos = sys.positions;
const col = sys.colors;
const sz = sys.sizes;
for (let i = 0; i < stateArr.length; i++) {
let p = stateArr[i];
p.life += dt;
if (p.life >= p.maxLife) {
stateArr[i] = spawnParticle(p.role, false);
p = stateArr[i];
}
const t = p.life / p.maxLife;
const nT = elapsed * p.noiseScale + p.noiseOffset;
const turbY = noise1(nT) * 0.55 * (1 + t * 2.5);
const turbZ = noise2(nT + 5.3) * 0.55 * (1 + t * 2.0);
const dragFac = 1 - p.drag * dt;
p.vx *= dragFac;
const fanFactor = t * 0.4;
p.vy += turbY * dt * (1 + fanFactor * 3);
p.vz += turbZ * dt * (1 + fanFactor * 3);
p.vy += p.riseRate * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.z += p.vz * dt;
const i3 = i*3, i4 = i*4;
pos[i3] = p.x; pos[i3+1] = p.y; pos[i3+2] = p.z;
const role = p.role;
if (role === 'smoke') {
const a = t < 0.15 ? t/0.15 : t > 0.65 ? 1-(t-0.65)/0.35 : 1.0;
col[i4]=0.07; col[i4+1]=0.02; col[i4+2]=0.01; col[i4+3] = a * 0.28;
sz[i] = p.size * (1 + t * 3.5);
} else if (role === 'spark') {
const a = t < 0.12 ? t/0.12 : Math.max(0, 1-t);
const [r,g,b] = t < 0.25 ? [1,1,0.65] : t < 0.55 ? [1,0.55,0] : [0.75,0.05,0];
col[i4]=r; col[i4+1]=g; col[i4+2]=b; col[i4+3]=Math.max(0,a);
sz[i] = p.size * (1 - t * 0.4);
} else if (role === 'ember') {
const a = t < 0.1 ? t/0.1 : t > 0.7 ? 1-(t-0.7)/0.3 : 1.0;
const pulse = 0.7 + 0.3 * Math.sin(elapsed * 8 + p.noiseOffset);
col[i4]=1; col[i4+1]=0.55*pulse; col[i4+2]=0; col[i4+3]=a*pulse*0.9;
sz[i] = p.size * (1 + 0.2 * Math.sin(elapsed * 12 + p.noiseOffset));
} else {
const isBloom = role === 'bloom';
const a = flameAlpha(t) * (isBloom ? 0.70 : 0.88);
const [r,g,b] = flameRGB(t, isBloom);
col[i4]=r; col[i4+1]=g; col[i4+2]=b; col[i4+3]=Math.max(0,a);
const growFactor = isBloom ? (1 + t * 3.5) : (1 + t * 1.2);
sz[i] = p.size * growFactor;
}
}
sys.geo.attributes.position.needsUpdate = true;
sys.geo.attributes.color.needsUpdate = true;
sys.geo.attributes.size.needsUpdate = true;
}
let lastTime = performance.now();
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
const t = clock.getElapsedTime();
dragon.position.y = 0.5 + Math.sin(t * 1.1) * 0.08;
dragon.rotation.z = Math.sin(t * 0.9) * 0.018;
tailGroup.rotation.y = Math.sin(t * 1.4) * 0.22;
wingMesh.rotation.z = Math.sin(t * 1.1) * 0.08;
wingMesh.rotation.x = Math.sin(t * 0.9) * 0.05;
const eyeFlicker = 0.7 + Math.random() * 0.3;
eyeMat.emissiveIntensity = eyeFlicker;
updatePool(flameState, flames, dt, t);
updatePool(sparkState, sparks, dt, t);
updatePool(smokeState, smokes, dt, t);
updatePool(emberState, embers, dt, t);
fireLight.intensity = 6 + Math.sin(t * 17) * 1.5 + Math.sin(t * 31) * 1.0 + Math.random() * 1.5;
fireLight.color.setHSL(0.05 + Math.random() * 0.02, 1, 0.5 + Math.random() * 0.08);
const mw = getMouthWorld();
fireLight.position.lerp(new THREE.Vector3(mw.x - 0.5, mw.y + 0.1, mw.z + 0.5), 0.1);
const pulse = 0.06 + Math.abs(Math.sin(t * 8)) * 0.05 + Math.random() * 0.03;
scene.background.setRGB(pulse * 0.6, 0, 0);
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>