// Earthquake House Collapse Simulator v3 // Fixed physics + realistic building appearance let scene, camera, renderer; let buildingParts = []; let isSimulating = false; let simulationTime = 0; let capturedImages = []; let rescueMarkers = []; let groundMesh, pathMesh; let cameraBasePos = { x: 0, y: 0, z: 0 }; let quakeDir = 0; // random earthquake primary direction each run let quakeSeed = 0; // random seed per simulation for variation let config = { buildingStyle: 'european', stories: 2, material: 'concrete', foundation: 'rock', magnitude: 7.0, distance: 5, duration: 15, buildingAge: 30 }; const materialProps = { concrete: { density: 2400, strength: 30, brittleness: 0.6, color: 0xd4cfc4, wallColor: 0xe8e4dc }, unreinforcedMasonry: { density: 1800, strength: 8, brittleness: 0.9, color: 0xc4956a, wallColor: 0xd4a574 }, wood: { density: 600, strength: 12, brittleness: 0.3, color: 0x8B6914, wallColor: 0xf5e6d0 }, steel: { density: 7800, strength: 250, brittleness: 0.2, color: 0x708090, wallColor: 0xdcdcdc }, adobe: { density: 1500, strength: 3, brittleness: 0.95, color: 0xc9a86c, wallColor: 0xd4b896 } }; const foundationFactors = { rock: 1.0, stiffSoil: 1.3, softSoil: 1.8, liquefiable: 2.5 }; const styleConfigs = { european: { roofType: 'pitched', roofColor: 0x3d3d3d, hasShutters: true }, chinese: { roofType: 'curved', roofColor: 0x4a4a4a, hasShutters: false }, japanese: { roofType: 'hip', roofColor: 0x2a3a5a, hasShutters: false }, american: { roofType: 'gable', roofColor: 0x4a4a4a, hasShutters: true }, middleEast: { roofType: 'flat', roofColor: 0xc9a86c, hasShutters: false } }; // ==================== SCENE SETUP ==================== function initScene() { const viewport = document.getElementById('viewport'); scene = new THREE.Scene(); scene.background = new THREE.Color(0x87CEEB); scene.fog = new THREE.Fog(0x87CEEB, 60, 120); camera = new THREE.PerspectiveCamera(50, viewport.clientWidth / viewport.clientHeight, 0.1, 500); camera.position.set(18, 14, 18); camera.lookAt(0, 3, 0); renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); renderer.setSize(viewport.clientWidth, viewport.clientHeight); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; viewport.appendChild(renderer.domElement); // Lights scene.add(new THREE.AmbientLight(0x606060, 0.6)); const sun = new THREE.DirectionalLight(0xfff8e0, 1.0); sun.position.set(8, 15, 10); sun.castShadow = true; sun.shadow.mapSize.set(2048, 2048); sun.shadow.camera.left = -20; sun.shadow.camera.right = 20; sun.shadow.camera.top = 20; sun.shadow.camera.bottom = -20; scene.add(sun); scene.add(new THREE.HemisphereLight(0x87CEEB, 0x3a5f0b, 0.3)); // Ground groundMesh = new THREE.Mesh( new THREE.PlaneGeometry(80, 80), new THREE.MeshStandardMaterial({ color: 0x4a7c3f, roughness: 0.95 }) ); groundMesh.rotation.x = -Math.PI / 2; groundMesh.receiveShadow = true; scene.add(groundMesh); // Pathway pathMesh = new THREE.Mesh( new THREE.PlaneGeometry(2.5, 10), new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.9 }) ); pathMesh.rotation.x = -Math.PI / 2; pathMesh.position.set(0, 0.01, 8); scene.add(pathMesh); setupCameraControls(viewport); } function setupCameraControls(el) { let dragging = false; let prev = { x: 0, y: 0 }; let theta = Math.PI / 4; // horizontal angle let phi = Math.PI / 3.5; // vertical angle (0=top, PI/2=horizon) let radius = 25; let target = new THREE.Vector3(0, 3, 0); // orbit center function updateCameraOrbit() { camera.position.set( target.x + radius * Math.sin(phi) * Math.cos(theta), target.y + radius * Math.cos(phi), target.z + radius * Math.sin(phi) * Math.sin(theta) ); camera.lookAt(target); } // Expose for earthquake shaking to read current position window.getCameraOrbitPos = function() { return { x: target.x + radius * Math.sin(phi) * Math.cos(theta), y: target.y + radius * Math.cos(phi), z: target.z + radius * Math.sin(phi) * Math.sin(theta) }; }; window.resetCameraOrbit = function() { updateCameraOrbit(); }; el.addEventListener('mousedown', function(e) { if (e.target.tagName !== 'CANVAS') return; dragging = true; prev = { x: e.clientX, y: e.clientY }; e.preventDefault(); }); el.addEventListener('mousemove', function(e) { if (!dragging) return; var dx = e.clientX - prev.x; var dy = e.clientY - prev.y; prev = { x: e.clientX, y: e.clientY }; if (e.shiftKey || e.buttons === 4) { // Pan (shift+drag or middle mouse) var panSpeed = 0.03 * radius / 25; var right = new THREE.Vector3(); var up = new THREE.Vector3(0, 1, 0); right.crossVectors(camera.getWorldDirection(new THREE.Vector3()), up).normalize(); target.add(right.multiplyScalar(-dx * panSpeed)); target.y += dy * panSpeed; } else { // Orbit: horizontal drag rotates around Y axis theta += dx * 0.008; // Vertical drag: drag up = go higher, drag down = go lower phi = Math.max(0.2, Math.min(Math.PI / 2 - 0.05, phi - dy * 0.006)); } updateCameraOrbit(); }); el.addEventListener('mouseup', function() { dragging = false; }); el.addEventListener('mouseleave', function() { dragging = false; }); el.addEventListener('wheel', function(e) { if (e.target.tagName !== 'CANVAS') return; radius = Math.max(8, Math.min(55, radius + e.deltaY * 0.025)); updateCameraOrbit(); e.preventDefault(); }); // Touch support for mobile var lastTouch = null; el.addEventListener('touchstart', function(e) { if (e.touches.length === 1) { lastTouch = { x: e.touches[0].clientX, y: e.touches[0].clientY }; } }); el.addEventListener('touchmove', function(e) { if (!lastTouch || e.touches.length !== 1) return; var dx = e.touches[0].clientX - lastTouch.x; var dy = e.touches[0].clientY - lastTouch.y; lastTouch = { x: e.touches[0].clientX, y: e.touches[0].clientY }; theta += dx * 0.008; phi = Math.max(0.2, Math.min(Math.PI / 2 - 0.05, phi - dy * 0.006)); updateCameraOrbit(); e.preventDefault(); }); el.addEventListener('touchend', function() { lastTouch = null; }); updateCameraOrbit(); } // ==================== BUILD HOUSE ==================== function buildHouse() { clearAll(); const style = styleConfigs[config.buildingStyle]; const mat = materialProps[config.material]; const stories = parseInt(config.stories); const H = 3.0, W = 10, D = 7; // Foundation (never collapses) addBlock(0, 0.15, 0, W+0.4, 0.3, D+0.4, 0x555555, 'foundation', -1); // Build each story for (let f = 0; f < stories; f++) { const by = f * H + 0.3; buildStory(by, H, W, D, mat, style, f); } // Roof buildRoof(stories * H + 0.3, W, D, style); // Exterior details buildDetails(W, D, stories, H, style); // Room layout for rescue var layout = { groundFloor: [ { name: 'Living Room', x: -2, z: 0, w: 4, d: 5 }, { name: 'Kitchen', x: 3, z: -1, w: 3, d: 3 }, { name: 'Bathroom', x: 3, z: 2, w: 2, d: 2 } ], floors: [] }; for (var fi = 1; fi < stories; fi++) { layout.floors.push([ { name: 'Bedroom ' + fi, x: -2, z: 0, w: 4, d: 4, floor: fi }, { name: (fi === 1 ? 'Master Bedroom' : 'Bedroom ' + (fi + 1)), x: 3, z: 0, w: 3, d: 3, floor: fi }, { name: 'Hallway F' + (fi + 1), x: 0.5, z: 3, w: 8, d: 1.5, floor: fi } ]); } window.roomLayout = layout; } function buildStory(by, H, W, D, mat, style, floorIdx) { if (config.buildingStyle === 'chinese') { buildChineseStory(by, H, W, D, floorIdx); } else if (config.buildingStyle === 'japanese') { buildJapaneseStory(by, H, W, D, floorIdx); } else if (config.buildingStyle === 'american') { buildAmericanStory(by, H, W, D, floorIdx); } else { buildWesternStory(by, H, W, D, mat, style, floorIdx); } } function buildChineseStory(by, H, W, D, floorIdx) { var colColor = 0xaa2222; // vermilion red columns var beamColor = 0x6b2020; // dark red beams var latticeColor = 0x1a5c5c; // teal/green lattice panels var baseColor = 0x888888; // gray stone base var bandColor = 0x1a7a5c; // green decorative band var colR = 0.2; // column radius (using box) var colSpacing = W / 4; // 5 columns across front // Stone platform base (for ground floor only) if (floorIdx === 0) { addBlock(0, by + 0.15, 0, W + 1.0, 0.3, D + 1.0, baseColor, 'foundation', -1); } // Floor slab if (floorIdx > 0) { addBlock(0, by + 0.1, 0, W + 0.5, 0.2, D + 0.5, 0x777777, 'floor', floorIdx); } // ---- COLUMNS (the main structural system) ---- // Front and back rows of columns for (var i = 0; i <= 4; i++) { var cx = -W/2 + i * colSpacing; // Front columns addBlock(cx, by + H/2, D/2, colR*2, H, colR*2, colColor, 'column', floorIdx); // Back columns addBlock(cx, by + H/2, -D/2, colR*2, H, colR*2, colColor, 'column', floorIdx); } // Side columns (middle) addBlock(-W/2, by + H/2, 0, colR*2, H, colR*2, colColor, 'column', floorIdx); addBlock(W/2, by + H/2, 0, colR*2, H, colR*2, colColor, 'column', floorIdx); // ---- BEAMS (horizontal, connecting columns at top) ---- // Front beam addBlock(0, by + H - 0.15, D/2, W, 0.3, colR*2, beamColor, 'beam', floorIdx); // Back beam addBlock(0, by + H - 0.15, -D/2, W, 0.3, colR*2, beamColor, 'beam', floorIdx); // Side beams addBlock(-W/2, by + H - 0.15, 0, colR*2, 0.3, D, beamColor, 'beam', floorIdx); addBlock(W/2, by + H - 0.15, 0, colR*2, 0.3, D, beamColor, 'beam', floorIdx); // ---- DECORATIVE BAND (green/teal) under beams ---- addBlock(0, by + H - 0.45, D/2, W, 0.2, 0.05, bandColor, 'trim', floorIdx); addBlock(0, by + H - 0.45, -D/2, W, 0.2, 0.05, bandColor, 'trim', floorIdx); // ---- LATTICE PANELS between columns (front) ---- for (var i = 0; i < 4; i++) { var px = -W/2 + i * colSpacing + colSpacing/2; var panelH = H * 0.7; var panelY = by + panelH/2 + 0.3; if (floorIdx === 0 && i === 2) { // Door opening in center addBlock(px, by + 1.2, D/2, colSpacing - colR*3, 2.4, 0.08, 0x8b3a1a, 'door', floorIdx); } else { // Lattice window panel addBlock(px, panelY, D/2, colSpacing - colR*3, panelH, 0.06, latticeColor, 'lattice', floorIdx); // Small bottom wall panel (kickplate) addBlock(px, by + 0.15, D/2, colSpacing - colR*3, 0.3, 0.1, baseColor, 'front_wall', floorIdx); } } // ---- LATTICE PANELS (back wall - more solid) ---- for (var i = 0; i < 4; i++) { var px = -W/2 + i * colSpacing + colSpacing/2; addBlock(px, by + H*0.4, -D/2, colSpacing - colR*3, H*0.7, 0.15, 0xd4a574, 'back_wall', floorIdx); } // ---- SIDE WALLS (solid with small windows) ---- addBlock(-W/2, by + H/2, 0, 0.15, H, D - 0.5, 0xd4a574, 'left_wall', floorIdx); addBlock(W/2, by + H/2, 0, 0.15, H, D - 0.5, 0xd4a574, 'right_wall', floorIdx); // Ceiling/floor beams (cross beams visible from below) addBlock(0, by + H - 0.05, 0, W, 0.1, D, 0x8b5a2b, 'ceiling', floorIdx); } function buildJapaneseStory(by, H, W, D, floorIdx) { var frameColor = 0x3d2b1f; // dark brown timber frame var shojiColor = 0xf5e8d0; // warm cream shoji panels var wallColor = 0xe8dcc8; // light plaster walls var balconyColor = 0x6b4c2a; // medium brown wood var colR = 0.15; var colSpacing = W / 5; // 6 columns across // Floor platform if (floorIdx === 0) { addBlock(0, by + 0.1, 0, W + 0.6, 0.2, D + 0.6, 0x5c4030, 'floor', floorIdx); } else { addBlock(0, by + 0.1, 0, W + 0.8, 0.15, D + 0.8, balconyColor, 'floor', floorIdx); } // ---- COLUMNS (dark timber frame) ---- for (var i = 0; i <= 5; i++) { var cx = -W/2 + i * colSpacing; addBlock(cx, by + H/2, D/2, colR*2, H, colR*2, frameColor, 'column', floorIdx); addBlock(cx, by + H/2, -D/2, colR*2, H, colR*2, frameColor, 'column', floorIdx); } // Corner and middle side columns addBlock(-W/2, by + H/2, 0, colR*2, H, colR*2, frameColor, 'column', floorIdx); addBlock(W/2, by + H/2, 0, colR*2, H, colR*2, frameColor, 'column', floorIdx); // ---- HORIZONTAL BEAMS (dark frame) ---- // Top beam (front/back) addBlock(0, by + H - 0.1, D/2, W, 0.2, colR*2, frameColor, 'beam', floorIdx); addBlock(0, by + H - 0.1, -D/2, W, 0.2, colR*2, frameColor, 'beam', floorIdx); // Mid-height beam (front) - nageshi addBlock(0, by + H*0.65, D/2, W, 0.1, colR, frameColor, 'beam', floorIdx); // Bottom rail addBlock(0, by + 0.25, D/2, W, 0.1, colR, frameColor, 'beam', floorIdx); // Side beams addBlock(-W/2, by + H - 0.1, 0, colR*2, 0.2, D, frameColor, 'beam', floorIdx); addBlock(W/2, by + H - 0.1, 0, colR*2, 0.2, D, frameColor, 'beam', floorIdx); // ---- SHOJI / WALL PANELS (front) ---- for (var i = 0; i < 5; i++) { var px = -W/2 + i * colSpacing + colSpacing/2; var panelH = H * 0.62; var panelY = by + 0.3 + panelH/2; if (floorIdx === 0 && (i === 2 || i === 3)) { // Sliding door opening - translucent shoji var shojiMat = new THREE.MeshStandardMaterial({ color: 0xfff8e8, transparent: true, opacity: 0.6, roughness: 0.9, side: THREE.DoubleSide }); var shoji = new THREE.Mesh(new THREE.BoxGeometry(colSpacing - colR*3, panelH, 0.04), shojiMat); shoji.position.set(px, panelY, D/2); shoji.castShadow = true; shoji.receiveShadow = true; scene.add(shoji); buildingParts.push(makePart(shoji, 'shoji', floorIdx)); // Shoji grid lines (horizontal) addBlock(px, panelY, D/2 + 0.025, colSpacing - colR*3, 0.02, 0.01, frameColor, 'shoji_frame', floorIdx); addBlock(px, panelY - panelH*0.3, D/2 + 0.025, colSpacing - colR*3, 0.02, 0.01, frameColor, 'shoji_frame', floorIdx); addBlock(px, panelY + panelH*0.3, D/2 + 0.025, colSpacing - colR*3, 0.02, 0.01, frameColor, 'shoji_frame', floorIdx); } else { // Upper portion: translucent shoji or window var upperH = panelH * 0.55; var upperY = by + H*0.65 - upperH/2 - 0.1; var shojiMat2 = new THREE.MeshStandardMaterial({ color: shojiColor, transparent: true, opacity: 0.7, roughness: 0.95 }); var panel = new THREE.Mesh(new THREE.BoxGeometry(colSpacing - colR*3, upperH, 0.04), shojiMat2); panel.position.set(px, upperY, D/2); panel.castShadow = true; scene.add(panel); buildingParts.push(makePart(panel, 'shoji', floorIdx)); // Lower portion: opaque wall panel var lowerH = panelH * 0.4; addBlock(px, by + 0.3 + lowerH/2, D/2, colSpacing - colR*3, lowerH, 0.08, wallColor, 'front_wall', floorIdx); } } // ---- BACK WALL (more solid) ---- addBlock(0, by + H/2, -D/2, W, H*0.9, 0.12, wallColor, 'back_wall', floorIdx); // ---- SIDE WALLS (solid plaster with dark frame band at top) ---- addBlock(-W/2, by + H*0.45, 0, 0.12, H*0.8, D - 0.5, wallColor, 'left_wall', floorIdx); addBlock(W/2, by + H*0.45, 0, 0.12, H*0.8, D - 0.5, wallColor, 'right_wall', floorIdx); // Dark band at top of side walls addBlock(-W/2, by + H*0.85, 0, 0.14, H*0.12, D - 0.3, frameColor, 'trim', floorIdx); addBlock(W/2, by + H*0.85, 0, 0.14, H*0.12, D - 0.3, frameColor, 'trim', floorIdx); // ---- ENGAWA (veranda/balcony) on upper floors ---- if (floorIdx > 0) { // Balcony floor extending out front addBlock(0, by + 0.05, D/2 + 0.5, W + 0.4, 0.08, 1.0, balconyColor, 'balcony', floorIdx); // Railing addBlock(0, by + 0.7, D/2 + 0.95, W + 0.3, 0.05, 0.05, balconyColor, 'railing', floorIdx); // Railing posts for (var i = 0; i <= 4; i++) { addBlock(-W/2 + i*(W/4), by + 0.35, D/2 + 0.95, 0.06, 0.6, 0.06, balconyColor, 'railing', floorIdx); } } // Ceiling addBlock(0, by + H - 0.05, 0, W, 0.1, D, 0x5c4a3a, 'ceiling', floorIdx); } function buildAmericanStory(by, H, W, D, floorIdx) { var siding = 0xf0ece4; // white/cream clapboard siding var trimColor = 0xffffff; // bright white trim var windowFrame = 0x2a2a2a; // dark/black window frames var doorColor = 0x1a3a4a; // dark teal/navy door var t = 0.2; // wall thickness var winH = 1.6; // window height var winW = 1.1; // window width var winBot = H * 0.3; // window bottom from floor // Floor if (floorIdx > 0) { addBlock(0, by + 0.1, 0, W, 0.2, D, 0x999999, 'floor', floorIdx); } // ---- FRONT WALL (+Z) ---- if (floorIdx === 0) { // Build wall with door opening and 2 window openings var doorW = 1.3, doorH = 2.3; // Left of left window addBlock(-W/2 + 0.7, by + H/2, D/2, 1.4, H, t, siding, 'front_wall', floorIdx); // Between left window and door addBlock(-1.0, by + H/2, D/2, 1.0, H, t, siding, 'front_wall', floorIdx); // Between door and right window addBlock(1.0, by + H/2, D/2, 1.0, H, t, siding, 'front_wall', floorIdx); // Right of right window addBlock(W/2 - 0.7, by + H/2, D/2, 1.4, H, t, siding, 'front_wall', floorIdx); // Above door addBlock(0, by + doorH + 0.35, D/2, doorW + 0.2, H - doorH - 0.2, t, siding, 'front_wall', floorIdx); // Above left window addBlock(-2.5, by + winBot + winH + (H - winBot - winH)/2, D/2, winW + 0.3, H - winBot - winH, t, siding, 'front_wall', floorIdx); // Below left window addBlock(-2.5, by + winBot/2, D/2, winW + 0.3, winBot, t, siding, 'front_wall', floorIdx); // Above right window addBlock(2.5, by + winBot + winH + (H - winBot - winH)/2, D/2, winW + 0.3, H - winBot - winH, t, siding, 'front_wall', floorIdx); // Below right window addBlock(2.5, by + winBot/2, D/2, winW + 0.3, winBot, t, siding, 'front_wall', floorIdx); // Door addBlock(0, by + doorH/2, D/2 + 0.05, doorW, doorH, 0.08, doorColor, 'door', floorIdx); // Windows (visible because wall has openings) addBlock(-2.5, by + winBot + winH/2, D/2 + 0.05, winW, winH, 0.05, 0x6daed0, 'glass', floorIdx); addBlock(2.5, by + winBot + winH/2, D/2 + 0.05, winW, winH, 0.05, 0x6daed0, 'glass', floorIdx); // Window frames (dark) addBlock(-2.5, by + winBot + winH/2, D/2 + 0.1, winW + 0.15, winH + 0.15, 0.03, windowFrame, 'trim', floorIdx); addBlock(2.5, by + winBot + winH/2, D/2 + 0.1, winW + 0.15, winH + 0.15, 0.03, windowFrame, 'trim', floorIdx); // Window sills (white) addBlock(-2.5, by + winBot - 0.05, D/2 + 0.12, winW + 0.3, 0.08, 0.12, trimColor, 'trim', floorIdx); addBlock(2.5, by + winBot - 0.05, D/2 + 0.12, winW + 0.3, 0.08, 0.12, trimColor, 'trim', floorIdx); } else { // Upper floor front wall with 3 windows var winPositions = [-2.5, 0, 2.5]; // Wall segments between windows addBlock(-W/2 + 0.5, by + H/2, D/2, 1.0, H, t, siding, 'front_wall', floorIdx); addBlock(-1.25, by + H/2, D/2, 1.3, H, t, siding, 'front_wall', floorIdx); addBlock(1.25, by + H/2, D/2, 1.3, H, t, siding, 'front_wall', floorIdx); addBlock(W/2 - 0.5, by + H/2, D/2, 1.0, H, t, siding, 'front_wall', floorIdx); // Above and below each window winPositions.forEach(function(wx) { addBlock(wx, by + winBot/2, D/2, winW + 0.1, winBot, t, siding, 'front_wall', floorIdx); addBlock(wx, by + winBot + winH + (H - winBot - winH)/2, D/2, winW + 0.1, H - winBot - winH, t, siding, 'front_wall', floorIdx); // Glass addBlock(wx, by + winBot + winH/2, D/2 + 0.05, winW, winH, 0.05, 0x6daed0, 'glass', floorIdx); // Frame addBlock(wx, by + winBot + winH/2, D/2 + 0.1, winW + 0.12, winH + 0.12, 0.03, windowFrame, 'trim', floorIdx); // Sill addBlock(wx, by + winBot - 0.05, D/2 + 0.12, winW + 0.25, 0.08, 0.1, trimColor, 'trim', floorIdx); // Mullion (cross divider) addBlock(wx, by + winBot + winH/2, D/2 + 0.12, 0.04, winH, 0.02, windowFrame, 'trim', floorIdx); addBlock(wx, by + winBot + winH/2, D/2 + 0.12, winW, 0.04, 0.02, windowFrame, 'trim', floorIdx); }); } // ---- BACK WALL (-Z) with 2 windows ---- addBlock(-W/2 + 1.2, by + H/2, -D/2, 2.4, H, t, siding, 'back_wall', floorIdx); addBlock(0, by + H/2, -D/2, W - 5.6, H, t, siding, 'back_wall', floorIdx); addBlock(W/2 - 1.2, by + H/2, -D/2, 2.4, H, t, siding, 'back_wall', floorIdx); addBlock(-2.2, by + winBot/2, -D/2, winW + 0.1, winBot, t, siding, 'back_wall', floorIdx); addBlock(-2.2, by + winBot + winH + (H-winBot-winH)/2, -D/2, winW+0.1, H-winBot-winH, t, siding, 'back_wall', floorIdx); addBlock(2.2, by + winBot/2, -D/2, winW + 0.1, winBot, t, siding, 'back_wall', floorIdx); addBlock(2.2, by + winBot + winH + (H-winBot-winH)/2, -D/2, winW+0.1, H-winBot-winH, t, siding, 'back_wall', floorIdx); addBlock(-2.2, by + winBot + winH/2, -D/2 - 0.05, winW, winH, 0.05, 0x6daed0, 'glass', floorIdx); addBlock(2.2, by + winBot + winH/2, -D/2 - 0.05, winW, winH, 0.05, 0x6daed0, 'glass', floorIdx); // ---- SIDE WALLS with windows ---- // Left wall: 2 window openings addBlock(-W/2, by + H/2, -D/2 + 1.0, t, H, 2.0, siding, 'left_wall', floorIdx); addBlock(-W/2, by + H/2, 0, t, H, 1.0, siding, 'left_wall', floorIdx); addBlock(-W/2, by + H/2, D/2 - 1.0, t, H, 2.0, siding, 'left_wall', floorIdx); addBlock(-W/2, by + winBot/2, -1.5, t, winBot, 1.2, siding, 'left_wall', floorIdx); addBlock(-W/2, by + winBot+winH+(H-winBot-winH)/2, -1.5, t, H-winBot-winH, 1.2, siding, 'left_wall', floorIdx); addBlock(-W/2, by + winBot/2, 1.5, t, winBot, 1.2, siding, 'left_wall', floorIdx); addBlock(-W/2, by + winBot+winH+(H-winBot-winH)/2, 1.5, t, H-winBot-winH, 1.2, siding, 'left_wall', floorIdx); addBlock(-W/2 - 0.05, by + winBot + winH/2, -1.5, 0.05, winH, 1.0, 0x6daed0, 'glass', floorIdx); addBlock(-W/2 - 0.05, by + winBot + winH/2, 1.5, 0.05, winH, 1.0, 0x6daed0, 'glass', floorIdx); // Right wall: same pattern addBlock(W/2, by + H/2, -D/2 + 1.0, t, H, 2.0, siding, 'right_wall', floorIdx); addBlock(W/2, by + H/2, 0, t, H, 1.0, siding, 'right_wall', floorIdx); addBlock(W/2, by + H/2, D/2 - 1.0, t, H, 2.0, siding, 'right_wall', floorIdx); addBlock(W/2, by + winBot/2, -1.5, t, winBot, 1.2, siding, 'right_wall', floorIdx); addBlock(W/2, by + winBot+winH+(H-winBot-winH)/2, -1.5, t, H-winBot-winH, 1.2, siding, 'right_wall', floorIdx); addBlock(W/2, by + winBot/2, 1.5, t, winBot, 1.2, siding, 'right_wall', floorIdx); addBlock(W/2, by + winBot+winH+(H-winBot-winH)/2, 1.5, t, H-winBot-winH, 1.2, siding, 'right_wall', floorIdx); addBlock(W/2 + 0.05, by + winBot + winH/2, -1.5, 0.05, winH, 1.0, 0x6daed0, 'glass', floorIdx); addBlock(W/2 + 0.05, by + winBot + winH/2, 1.5, 0.05, winH, 1.0, 0x6daed0, 'glass', floorIdx); // ---- WHITE CORNER TRIM ---- addBlock(-W/2 - 0.08, by + H/2, D/2, 0.12, H, 0.12, trimColor, 'trim', floorIdx); addBlock(W/2 + 0.08, by + H/2, D/2, 0.12, H, 0.12, trimColor, 'trim', floorIdx); addBlock(-W/2 - 0.08, by + H/2, -D/2, 0.12, H, 0.12, trimColor, 'trim', floorIdx); addBlock(W/2 + 0.08, by + H/2, -D/2, 0.12, H, 0.12, trimColor, 'trim', floorIdx); // Internal wall addBlock(0, by + H/2, -D/6, 0.15, H, D*0.5, siding, 'int_wall', floorIdx); // Ceiling addBlock(0, by + H - 0.1, 0, W, 0.15, D, 0x999999, 'ceiling', floorIdx); } function buildWesternStory(by, H, W, D, mat, style, floorIdx) { const wc = mat.wallColor; const t = 0.3; if (floorIdx === 0) { addBlock(-W/2 + 1.5, by + H/2, D/2, W/2 - 1.5, H, t, wc, 'front_wall', floorIdx); addBlock(1.5, by + H/2, D/2, W/2 - 1.5, H, t, wc, 'front_wall', floorIdx); addBlock(0, by + 2.6, D/2, 2.0, 0.4, t, wc, 'front_wall', floorIdx); addBlock(0, by + 1.15, D/2 + 0.05, 1.8, 2.3, 0.08, 0x5c3a1e, 'door', floorIdx); } else { buildWallWithWindows(-W/2, by, D/2, W, H, t, wc, 'front_wall', floorIdx, 3, true); } buildWallWithWindows(-W/2, by, -D/2 - t, W, H, t, wc, 'back_wall', floorIdx, 3, true); buildWallWithWindows(-W/2 - t, by, -D/2, t, H, D, wc, 'left_wall', floorIdx, 2, false); buildWallWithWindows(W/2, by, -D/2, t, H, D, wc, 'right_wall', floorIdx, 2, false); addBlock(0, by + H/2, -D/6, 0.2, H, D*0.5, wc, 'int_wall', floorIdx); if (floorIdx > 0) { addBlock(0, by + 0.1, 0, W, 0.2, D, 0x999999, 'floor', floorIdx); } addBlock(0, by + H - 0.1, 0, W, 0.2, D, 0x999999, 'ceiling', floorIdx); } function buildWallWithWindows(x, y, z, w, h, d, color, label, floor, numWin, isXWall) { // Wall runs along X (isXWall=true) or along Z (isXWall=false) const len = isXWall ? w : d; const sections = numWin * 2 + 1; const secLen = len / sections; const winH = 1.3, winBottom = h * 0.35; for (let i = 0; i < sections; i++) { const isWin = (i % 2 === 1); if (isXWall) { const sx = x + i * secLen; const cx = sx + secLen / 2; const cz = z + d / 2; if (isWin) { // Below window addBlock(cx, y + winBottom/2, cz, secLen*0.98, winBottom, d, color, label, floor); // Above window const aboveH = h - winBottom - winH; addBlock(cx, y + winBottom + winH + aboveH/2, cz, secLen*0.98, aboveH, d, color, label, floor); // Window glass addBlock(cx, y + winBottom + winH/2, cz, secLen*0.7, winH*0.9, 0.03, 0x88ccdd, 'glass', floor); } else { addBlock(cx, y + h/2, cz, secLen*0.98, h*0.98, d, color, label, floor); } } else { const sz = z + i * secLen; const cz = sz + secLen / 2; const cx = x + w / 2; if (isWin) { addBlock(cx, y + winBottom/2, cz, w, winBottom, secLen*0.98, color, label, floor); const aboveH = h - winBottom - winH; addBlock(cx, y + winBottom + winH + aboveH/2, cz, w, aboveH, secLen*0.98, color, label, floor); addBlock(cx, y + winBottom + winH/2, cz, 0.03, winH*0.9, secLen*0.7, 0x88ccdd, 'glass', floor); } else { addBlock(cx, y + h/2, cz, w, h*0.98, secLen*0.98, color, label, floor); } } } } function buildRoof(baseY, W, D, style) { const rc = style.roofColor; const oh = 0.8; // overhang if (style.roofType === 'flat') { addBlock(0, baseY + 0.15, 0, W + oh*2, 0.3, D + oh*2, rc, 'roof', 2); } else if (style.roofType === 'pitched' || style.roofType === 'gable') { const rh = 2.5, segs = 8; for (let side = -1; side <= 1; side += 2) { for (let i = 0; i < segs; i++) { const t = (i + 0.5) / segs; const panelD = (D/2 + oh) / segs; const py = baseY + rh * (1 - t); const pz = side * t * (D/2 + oh); const angle = side * Math.atan2(rh, D/2 + oh); const m = createBoxMesh(W + oh*2, 0.12, panelD, rc); m.position.set(0, py, pz); m.rotation.x = angle; m.castShadow = true; m.receiveShadow = true; scene.add(m); buildingParts.push(makePart(m, 'roof', 2)); } } // Ridge addBlock(0, baseY + rh, 0, W + oh*2, 0.15, 0.15, 0x4a3728, 'roof', 2); } else if (style.roofType === 'hip') { // Japanese hip roof (irimoya-yane): deep blue-gray tiles, wide overhang, gentle slope const rh = 2.2; const roofW = W + 2.5; // wide overhang const roofD = D + 2.0; const segs = 8; const tileColor = 0x2a3a5a; // deep blue-gray // Front and back slopes for (let side = -1; side <= 1; side += 2) { for (let i = 0; i < segs; i++) { const t = (i + 0.5) / segs; const panelD = (roofD/2) / segs; // Slight concave curve for elegance const curveT = Math.pow(t, 0.85); const py = baseY + rh * (1 - curveT); const pz = side * t * (roofD/2); const panelW = roofW * (1 - t * 0.25); // narrows toward top const m = createBoxMesh(panelW, 0.1, panelD, tileColor); m.position.set(0, py, pz); m.rotation.x = side * Math.atan2(rh, roofD/2) * 0.85; m.castShadow = true; m.receiveShadow = true; scene.add(m); buildingParts.push(makePart(m, 'roof', 2)); } } // Ridge addBlock(0, baseY + rh + 0.05, 0, roofW * 0.6, 0.15, 0.15, 0x333333, 'roof', 2); // Ridge end ornament (鬼瓦 onigawara) addBlock(0, baseY + rh + 0.2, 0, 0.3, 0.3, 0.3, 0x333333, 'ornament', 2); // Fascia boards (dark trim at eave edges) addBlock(0, baseY + 0.1, roofD/2 - 0.1, roofW, 0.15, 0.08, 0x2a1f14, 'trim', 2); addBlock(0, baseY + 0.1, -roofD/2 + 0.1, roofW, 0.15, 0.08, 0x2a1f14, 'trim', 2); } else if (style.roofType === 'curved') { // Chinese traditional roof: solid curved surface with wide overhang const rh = 2.2; const ovW = W + 3.5; // generous side overhang const ovD = D + 3.0; // generous front/back overhang const slopeSegs = 5; // fewer, wider segments for solid look // Two slope sides (front slope and back slope) for (let side = -1; side <= 1; side += 2) { for (let i = 0; i < slopeSegs; i++) { const t0 = i / slopeSegs; const t1 = (i + 1) / slopeSegs; const tMid = (t0 + t1) / 2; // Concave curve: drops quickly at first, flattens, then kicks up at eave const y0 = rh * (1 - Math.pow(t0, 0.7)); const y1 = rh * (1 - Math.pow(t1, 0.7)); // Eave upturn for last segment const upturn = i === slopeSegs - 1 ? 0.3 : 0; const segDepth = (ovD / 2) / slopeSegs; const segY = baseY + (y0 + y1) / 2 + upturn; const segZ = side * tMid * (ovD / 2); const segW = ovW * (1 - tMid * 0.08); // slightly narrows toward ridge // Angle: slope of the curve at this point const angle = side * Math.atan2(y0 - y1 + upturn * 2, segDepth); const m = createBoxMesh(segW, 0.15, segDepth * 1.05, 0x4a4a4a); m.position.set(0, segY, segZ); m.rotation.x = angle; m.castShadow = true; m.receiveShadow = true; scene.add(m); buildingParts.push(makePart(m, 'roof', 2)); } } // Ridge beam on top (thick, decorative) addBlock(0, baseY + rh + 0.15, 0, ovW * 0.85, 0.25, 0.3, 0x3a3a3a, 'roof', 2); // Ridge end ornaments (chiwen / dragon tail) addBlock(-ovW * 0.42, baseY + rh + 0.35, 0, 0.3, 0.4, 0.3, 0x333333, 'ornament', 2); addBlock(ovW * 0.42, baseY + rh + 0.35, 0, 0.3, 0.4, 0.3, 0x333333, 'ornament', 2); // Red fascia/trim board under the eave edge (front and back) addBlock(0, baseY - 0.1, ovD/2 - 0.2, ovW, 0.2, 0.12, 0xaa2222, 'trim', 2); addBlock(0, baseY - 0.1, -ovD/2 + 0.2, ovW, 0.2, 0.12, 0xaa2222, 'trim', 2); // Side trim addBlock(-ovW/2 + 0.1, baseY - 0.1, 0, 0.12, 0.2, ovD, 0xaa2222, 'trim', 2); addBlock(ovW/2 - 0.1, baseY - 0.1, 0, 0.12, 0.2, ovD, 0xaa2222, 'trim', 2); } } function buildDetails(W, D, stories, H, style) { if (config.buildingStyle === 'chinese') { buildChineseDetails(W, D, stories, H); } else if (config.buildingStyle === 'japanese') { buildJapaneseDetails(W, D, stories, H); } else if (config.buildingStyle === 'american') { buildAmericanDetails(W, D, stories, H); } else { buildWesternDetails(W, D, stories, H, style); } } function buildChineseDetails(W, D, stories, H) { // Raised stone platform steps (wider, grander) for (var i = 0; i < 4; i++) { addBlock(0, 0.3 - i*0.08, D/2 + 0.6 + i*0.35, W*0.4, 0.1, 0.4, 0x888888, 'steps', -1); } // Red lanterns hanging from front beam var lanternColor = 0xff2200; var lanternPositions = [-W/4, 0, W/4]; lanternPositions.forEach(function(lx) { // Lantern body (sphere-ish box) addBlock(lx, stories*H - 0.3, D/2 + 0.3, 0.4, 0.5, 0.4, lanternColor, 'lantern', stories - 1); // Lantern top addBlock(lx, stories*H + 0.0, D/2 + 0.3, 0.15, 0.1, 0.15, 0xffcc00, 'lantern', stories - 1); }); // Stone base wall/fence in front addBlock(0, 0.3, D/2 + 2.5, W + 2, 0.6, 0.25, 0x999999, 'fence', -1); // Gate pillars addBlock(-1.5, 0.6, D/2 + 2.5, 0.4, 1.2, 0.4, 0x777777, 'gate_pillar', -1); addBlock(1.5, 0.6, D/2 + 2.5, 0.4, 1.2, 0.4, 0x777777, 'gate_pillar', -1); // Decorative roof ridge ornaments var roofTop = stories * H + 2.8; addBlock(0, roofTop, 0, 0.3, 0.4, 0.3, 0x444444, 'ornament', stories); addBlock(-W/2 - 0.5, stories*H + 0.3, D/2 + 1.0, 0.25, 0.3, 0.25, 0x444444, 'ornament', stories); addBlock(W/2 + 0.5, stories*H + 0.3, D/2 + 1.0, 0.25, 0.3, 0.25, 0x444444, 'ornament', stories); // Potted plants var bushGeo = new THREE.SphereGeometry(0.35, 8, 6); var bushMat = new THREE.MeshStandardMaterial({ color: 0x2d5a27, roughness: 0.9 }); [[-W/2 - 1, 0.5, D/2 + 1], [W/2 + 1, 0.5, D/2 + 1]].forEach(function(pos) { var b = new THREE.Mesh(bushGeo, bushMat); b.position.set(pos[0], pos[1], pos[2]); b.castShadow = true; scene.add(b); }); } function buildJapaneseDetails(W, D, stories, H) { // Genkan (entry area) - stepping stones for (var i = 0; i < 3; i++) { addBlock(0, 0.05, D/2 + 1.0 + i*0.8, 0.8, 0.1, 0.6, 0x666666, 'steps', -1); } // Stone garden elements addBlock(-W/2 - 1.5, 0.2, 0, 0.5, 0.4, 0.5, 0x777777, 'stone', -1); addBlock(W/2 + 1.2, 0.15, -1, 0.4, 0.3, 0.4, 0x888888, 'stone', -1); // Bamboo/garden plants var bushGeo = new THREE.SphereGeometry(0.3, 8, 6); var bushMat = new THREE.MeshStandardMaterial({ color: 0x3a6b2a, roughness: 0.85 }); [[-W/2 - 1, 0.6, 2], [W/2 + 1.2, 0.5, 1], [-W/2 - 0.8, 0.4, -2]].forEach(function(pos) { var b = new THREE.Mesh(bushGeo, bushMat); b.position.set(pos[0], pos[1], pos[2]); b.scale.set(0.8, 1.5, 0.8); // tall and narrow like bamboo b.castShadow = true; scene.add(b); }); // Small stone lantern (tōrō) addBlock(W/2 + 1.5, 0.25, 2, 0.3, 0.5, 0.3, 0x999999, 'ornament', -1); addBlock(W/2 + 1.5, 0.55, 2, 0.4, 0.1, 0.4, 0x888888, 'ornament', -1); addBlock(W/2 + 1.5, 0.7, 2, 0.25, 0.3, 0.25, 0xaaaa88, 'ornament', -1); // Wooden fence/wall section on one side addBlock(-W/2 - 0.3, 0.5, D/2 + 0.5, 0.08, 1.0, 3, 0x5c4030, 'fence', -1); } function buildAmericanDetails(W, D, stories, H) { var trimColor = 0xffffff; var porchW = W + 1.0; // ---- WRAP-AROUND FRONT PORCH ---- // Porch deck addBlock(0, 0.35, D/2 + 1.2, porchW, 0.12, 2.4, 0x8b7355, 'porch', 0); // Porch roof addBlock(0, H * 0.95 + 0.3, D/2 + 1.2, porchW + 0.3, 0.1, 2.8, 0x3a3a3a, 'porch_roof', 0); // Porch columns (thick white square columns) var colPositions = [-porchW/2 + 0.4, -porchW/4, 0, porchW/4, porchW/2 - 0.4]; colPositions.forEach(function(cx) { addBlock(cx, H*0.5 + 0.3, D/2 + 2.2, 0.25, H*0.9, 0.25, trimColor, 'porch_column', 0); }); // Porch railing between columns addBlock(0, 0.75, D/2 + 2.3, porchW - 1, 0.06, 0.06, trimColor, 'railing', 0); // Railing balusters for (var i = 0; i < 12; i++) { var bx = -porchW/2 + 1.0 + i * (porchW - 2) / 11; addBlock(bx, 0.55, D/2 + 2.3, 0.04, 0.4, 0.04, trimColor, 'railing', 0); } // Front steps (centered, wide) for (var i = 0; i < 3; i++) { addBlock(0, 0.3 - i*0.12, D/2 + 2.5 + i*0.3, 2.0, 0.12, 0.35, 0x888888, 'steps', -1); } // ---- CHIMNEY (stone/brick on the side) ---- var ch = stories * H + 3.5; addBlock(W/2 - 0.5, ch/2, -D/4, 0.8, ch, 0.8, 0xa08060, 'chimney', stories); addBlock(W/2 - 0.5, ch + 0.1, -D/4, 0.9, 0.2, 0.9, 0x666666, 'chimney', stories); // ---- LANDSCAPING ---- // Foundation shrubs (low boxwood hedge) var bushGeo = new THREE.SphereGeometry(0.35, 8, 6); var bushMat = new THREE.MeshStandardMaterial({ color: 0x2a5a22, roughness: 0.9 }); for (var i = 0; i < 6; i++) { var bx = -W/2 + 0.8 + i * (W - 1.6) / 5; var b = new THREE.Mesh(bushGeo, bushMat); b.position.set(bx, 0.3, D/2 + 0.3); b.scale.set(1.0, 0.7, 0.8); b.castShadow = true; scene.add(b); } // Corner accent plants var tallBush = new THREE.Mesh(new THREE.SphereGeometry(0.5, 8, 6), bushMat); tallBush.position.set(-W/2 - 0.5, 0.6, D/2 + 0.5); tallBush.scale.set(0.7, 1.2, 0.7); tallBush.castShadow = true; scene.add(tallBush); // Flower beds (small colored spheres) var flowerColors = [0xcc2233, 0xff6644, 0xee4488]; for (var i = 0; i < 8; i++) { var fx = -2.5 + i * 0.7; var fc = flowerColors[i % 3]; var flower = new THREE.Mesh( new THREE.SphereGeometry(0.15, 6, 6), new THREE.MeshStandardMaterial({ color: fc, roughness: 0.8 }) ); flower.position.set(fx, 0.2, D/2 + 2.8 + Math.sin(i)*0.2); scene.add(flower); } // White trim fascia on gable if (stories >= 2) { addBlock(0, stories*H + 0.3, D/2 + 0.12, W + 0.4, 0.12, 0.06, trimColor, 'trim', stories); } // Gable triangle face (white siding filling the front gable) var gableBaseY = stories * H + 0.3; var gableH = 2.5; // Build gable as stacked narrowing panels for (var g = 0; g < 6; g++) { var gt = g / 6; var gw = (W + 0.8) * (1 - gt); var gy = gableBaseY + gt * gableH + gableH/12; addBlock(0, gy, D/2, gw, gableH/6 * 0.95, 0.12, 0xf0ece4, 'gable', stories); } // Gable window (upper story window in the gable) addBlock(0, gableBaseY + gableH*0.4, D/2 + 0.05, 1.0, 1.2, 0.04, 0x88bbcc, 'glass', stories); addBlock(0, gableBaseY + gableH*0.4, D/2 + 0.07, 1.1, 1.3, 0.02, 0x2a2a2a, 'trim', stories); } function buildWesternDetails(W, D, stories, H, style) { // Front steps for (let i = 0; i < 3; i++) { addBlock(0, 0.3 - i*0.1, D/2 + 0.5 + i*0.3, 2.5, 0.12, 0.35, 0x777777, 'steps', -1); } // Chimney if (style.roofType !== 'flat' && style.roofType !== 'curved') { const ch = stories * H + 4; addBlock(-W/2 + 1.2, ch/2, 0, 0.7, ch, 0.7, 0x8B4513, 'chimney', 2); addBlock(-W/2 + 1.2, ch + 0.1, 0, 0.9, 0.2, 0.9, 0x555555, 'chimney', 2); } // Porch columns if (style.hasShutters) { addBlock(-1.3, 1.6, D/2 + 0.7, 0.18, 2.6, 0.18, 0xf0f0f0, 'porch', 0); addBlock(1.3, 1.6, D/2 + 0.7, 0.18, 2.6, 0.18, 0xf0f0f0, 'porch', 0); addBlock(0, 3.0, D/2 + 0.7, 3.2, 0.1, 1.0, 0x555555, 'porch_roof', 0); } // Bushes const bushGeo = new THREE.SphereGeometry(0.45, 8, 6); const bushMat = new THREE.MeshStandardMaterial({ color: 0x2d5a27, roughness: 0.9 }); [[-3.5, 0.35, D/2+1.5], [3.5, 0.35, D/2+1.5], [-W/2-1, 0.35, 1], [-W/2-1, 0.35, -1.5]].forEach(p => { const b = new THREE.Mesh(bushGeo, bushMat); b.position.set(p[0], p[1], p[2]); b.scale.y = 0.7; b.castShadow = true; scene.add(b); }); } // ==================== BLOCK HELPERS ==================== function createBoxMesh(w, h, d, color) { const geo = new THREE.BoxGeometry(w, h, d); const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.75, metalness: 0.05 }); return new THREE.Mesh(geo, mat); } function addBlock(x, y, z, w, h, d, color, label, floor) { const mesh = createBoxMesh(w, h, d, color); mesh.position.set(x, y, z); mesh.castShadow = true; mesh.receiveShadow = true; scene.add(mesh); buildingParts.push(makePart(mesh, label, floor)); } function makePart(mesh, label, floor) { return { mesh: mesh, label: label, floor: floor, ox: mesh.position.x, oy: mesh.position.y, oz: mesh.position.z, orx: mesh.rotation.x, ory: mesh.rotation.y, orz: mesh.rotation.z, vx: 0, vy: 0, vz: 0, avx: 0, avy: 0, avz: 0, state: 'static' // static | shaking | falling | landed }; } function clearAll() { buildingParts.forEach(p => scene.remove(p.mesh)); buildingParts = []; rescueMarkers.forEach(m => scene.remove(m)); rescueMarkers = []; rescueAnimObjects = []; document.getElementById('analysis-panel').style.display = 'none'; } // ==================== EARTHQUAKE SIMULATION ==================== // Ground motion is THE CAUSE. Parts accumulate stress from ground acceleration. // When stress > strength threshold → part breaks off. function startEarthquake() { if (isSimulating) return; isSimulating = true; simulationTime = 0; // Random earthquake characteristics - unique every run quakeDir = Math.random() * Math.PI * 2; quakeSeed = Math.random() * 1000; // Compute Peak Ground Acceleration (PGA in g's) - properly scaled so magnitude matters var mag = config.magnitude; var dist = config.distance; var foundAmp = foundationFactors[config.foundation]; // Realistic-ish attenuation. Gives clear visible differences across the full M4-M9 range: // M4/dist5: ~0.02g (barely felt), M5.5/dist5: ~0.12g (light damage) // M6.5/dist5: ~0.4g (moderate), M7.5/dist5: ~0.8g (heavy), M9/dist5: ~1.8g (catastrophic) var logPGA = 0.45 * mag - 1.0 * Math.log10(dist + 3) - 1.8; var pga = Math.pow(10, logPGA) * foundAmp; document.getElementById('status').textContent = 'Earthquake! PGA: ' + pga.toFixed(2) + 'g | Shaking...'; updateSimInfo(pga); // Store camera position for shaking var orbitPos = window.getCameraOrbitPos ? window.getCameraOrbitPos() : { x: camera.position.x, y: camera.position.y, z: camera.position.z }; cameraBasePos.x = orbitPos.x; cameraBasePos.y = orbitPos.y; cameraBasePos.z = orbitPos.z; // Initialize each part's stress tracking var weakCorner = { x: (Math.random()-0.5)*10, z: (Math.random()-0.5)*7 }; var weakSide = quakeDir; // the earthquake push direction is the weak side buildingParts.forEach(function(p) { if (p.floor < 0) { p.state = 'static'; return; } p.state = 'shaking'; p.stress = 0; // accumulated stress from ground motion // Each part has a random strength threshold var baseStrength = getPartStrength(p); // Add randomness: ±40% variation p.threshold = baseStrength * (0.6 + Math.random() * 0.8); // Parts closer to weak corner are weaker var dw = Math.sqrt(Math.pow(p.ox - weakCorner.x, 2) + Math.pow(p.oz - weakCorner.z, 2)); p.threshold *= (0.7 + dw / 15 * 0.5); // Random per-part frequency sensitivity (resonance) p.resonanceFreq = 5 + Math.random() * 15; p.resonanceAmp = 0.8 + Math.random() * 0.4; }); // Main simulation interval var simInterval = setInterval(function() { if (!isSimulating) { clearInterval(simInterval); return; } stepSimulation(pga, simInterval); }, 16); } function getPartStrength(p) { var mat = materialProps[config.material]; var matFactor = (mat.strength / 8) * (1.2 - mat.brittleness * 0.7); var ageFactor = Math.max(0.5, 1.0 - config.buildingAge / 200); var base = matFactor * ageFactor; // Structural role determines threshold if (p.label === 'glass') return base * 0.5; if (p.label === 'shoji') return base * 0.4; // paper panels very fragile if (p.label === 'shoji_frame') return base * 0.5; if (p.label === 'lattice') return base * 0.6; // lattice panels are light if (p.label === 'lantern') return base * 0.3; // lanterns fall easily if (p.label === 'railing') return base * 0.8; if (p.label === 'balcony') return base * 2.5; if (p.label === 'chimney') return base * 1.5; if (p.label === 'door') return base * 1.2; if (p.label === 'ornament') return base * 0.8; if (p.label === 'trim') return base * 0.7; if (p.label === 'porch' || p.label === 'porch_roof') return base * 1.8; if (p.label === 'porch_column') return base * 2.5; // porch columns are sturdy if (p.label === 'gable') return base * 3.0; // gable face attached to structure if (p.label === 'column') return base * 8.0; // columns are very strong if (p.label === 'beam') return base * 6.0; // beams are strong if (p.label === 'roof') return base * 4.0; if (p.label === 'int_wall') return base * 6.0; if (p.label.includes('wall')) return base * (7.0 - p.floor * 1.5); if (p.label === 'ceiling' || p.label === 'floor') return base * 5.0; return base * 4.0; } function stepSimulation(pga, interval) { var dt = 0.02; simulationTime += dt; var dur = config.duration; var quakeActive = simulationTime < dur; // ===== COMPUTE GROUND MOTION (this is the cause of everything) ===== var groundAccelX = 0, groundAccelZ = 0, groundAccelY = 0; var intensity = 0; if (quakeActive) { // Envelope: ramp up quickly, sustain, decay var env; var rampUp = Math.min(dur * 0.15, 2.0); var decay = dur * 0.3; if (simulationTime < rampUp) env = simulationTime / rampUp; else if (simulationTime < dur - decay) env = 1.0; else env = Math.max(0, (dur - simulationTime) / decay); intensity = env * pga; // Ground acceleration: sum of multiple frequency components (like real seismogram) var dirX = Math.cos(quakeDir); var dirZ = Math.sin(quakeDir); // Primary motion along earthquake direction var primary = Math.sin(simulationTime * 6.3 + quakeSeed) * 0.5 + Math.sin(simulationTime * 14.1 + quakeSeed * 0.3) * 0.3 + Math.sin(simulationTime * 2.7 + quakeSeed * 1.7) * 0.2; // Secondary (perpendicular) motion var secondary = Math.sin(simulationTime * 8.7 + quakeSeed * 2.1) * 0.3 + Math.cos(simulationTime * 4.5 + quakeSeed * 0.5) * 0.2; // Vertical (smaller) var vertical = Math.sin(simulationTime * 19 + quakeSeed * 3) * 0.15; groundAccelX = intensity * (dirX * primary + (-dirZ) * secondary); groundAccelZ = intensity * (dirZ * primary + dirX * secondary); groundAccelY = intensity * vertical; } // ===== APPLY GROUND MOTION VISUALLY ===== var gDispScale = 0.2; // displacement scale for visual var gx = groundAccelX * gDispScale; var gz = groundAccelZ * gDispScale; var gy = groundAccelY * gDispScale * 0.5; if (quakeActive) { groundMesh.position.set(gx, gy, gz); if (pathMesh) pathMesh.position.set(gx, 0.01 + gy, 8 + gz); // Camera shakes WITH the ground (you're standing on it) camera.position.x = cameraBasePos.x + gx * 0.7; camera.position.y = cameraBasePos.y + gy * 1.5; camera.position.z = cameraBasePos.z + gz * 0.7; camera.lookAt(gx * 0.3, 3 + gy * 0.5, gz * 0.3); } else { groundMesh.position.set(0, 0, 0); if (pathMesh) pathMesh.position.set(0, 0.01, 8); if (window.resetCameraOrbit) window.resetCameraOrbit(); } // ===== BUILDING PARTS: stress from ground motion causes collapse ===== var anyMoving = false; for (var i = 0; i < buildingParts.length; i++) { var p = buildingParts[i]; if (p.state === 'static' || p.state === 'landed') continue; if (p.state === 'shaking') { anyMoving = true; if (quakeActive) { // Part moves with the ground (it's attached to the building on the ground) // But upper parts amplify ground motion (whip effect) var heightAmp = 1.0 + p.oy * 0.15; // higher = more amplification var resonance = 1.0 + 0.3 * Math.sin(simulationTime * p.resonanceFreq) * p.resonanceAmp; var partDispX = gx * heightAmp * resonance; var partDispZ = gz * heightAmp * resonance; p.mesh.position.x = p.ox + partDispX; p.mesh.position.z = p.oz + partDispZ; p.mesh.position.y = p.oy + gy * heightAmp * 0.3; // ===== ACCUMULATE STRESS from ground acceleration ===== // Stress = integral of |acceleration| over time, weighted by height amplification var accelMag = Math.sqrt(groundAccelX * groundAccelX + groundAccelZ * groundAccelZ + groundAccelY * groundAccelY); p.stress += accelMag * heightAmp * resonance * dt; // ===== CHECK FAILURE: stress exceeds threshold → part breaks ===== if (p.stress > p.threshold) { p.state = 'falling'; // Ejection velocity proportional to current ground acceleration var ejectForce = 1.5 + intensity * 2.0; p.vx = groundAccelX * ejectForce * (0.7 + Math.random() * 0.6) + (Math.random()-0.5) * intensity; p.vy = Math.random() * 1.5 + groundAccelY * 2; p.vz = groundAccelZ * ejectForce * (0.7 + Math.random() * 0.6) + (Math.random()-0.5) * intensity; p.avx = (Math.random()-0.5) * 3 * intensity; p.avy = (Math.random()-0.5) * 2; p.avz = (Math.random()-0.5) * 3 * intensity; // Visual damage if (p.mesh.material && p.mesh.material.color) { p.mesh.material = p.mesh.material.clone(); p.mesh.material.color.multiplyScalar(0.5 + Math.random() * 0.2); } } } else { // Earthquake ended, return to rest if not broken p.mesh.position.x = p.ox; p.mesh.position.y = p.oy; p.mesh.position.z = p.oz; } } if (p.state === 'falling') { anyMoving = true; p.vy -= 15.0 * dt; p.vx *= 0.99; p.vz *= 0.99; p.mesh.position.x += p.vx * dt; p.mesh.position.y += p.vy * dt; p.mesh.position.z += p.vz * dt; p.mesh.rotation.x += p.avx * dt; p.mesh.rotation.y += p.avy * dt; p.mesh.rotation.z += p.avz * dt; var gnd = getGroundAt(p); if (p.mesh.position.y <= gnd) { p.mesh.position.y = gnd; if (Math.abs(p.vy) < 1.0) { p.state = 'landed'; p.vx = 0; p.vy = 0; p.vz = 0; p.avx = 0; p.avy = 0; p.avz = 0; } else { p.vy = -p.vy * 0.15; p.vx *= 0.4; p.vz *= 0.4; p.avx *= 0.3; p.avy *= 0.3; p.avz *= 0.3; } } } } // ===== SUPPORT CHECK: slabs/ceilings/roofs fall if walls below are gone ===== for (var i = 0; i < buildingParts.length; i++) { var p = buildingParts[i]; if (p.state !== 'shaking') continue; // Only check slabs, ceilings, floors, roofs, beams var isHorizontal = (p.label === 'ceiling' || p.label === 'floor' || p.label === 'roof' || p.label === 'porch_roof' || p.label === 'beam'); if (!isHorizontal) continue; if (p.floor < 0) continue; // Count supporting parts: must be on SAME floor (or floor below for roof), vertical, and still intact var supportCount = 0; for (var j = 0; j < buildingParts.length; j++) { var s = buildingParts[j]; if (s === p || s.state !== 'shaking') continue; // Must be a vertical structural element var isSupport = (s.label === 'column' || s.label.includes('wall') || s.label === 'porch_column' || s.label === 'int_wall'); if (!isSupport) continue; // Must be on same floor OR one floor below (roof sits on top floor walls) if (s.floor !== p.floor && s.floor !== p.floor - 1) continue; // Must be below this slab if (s.oy >= p.oy) continue; supportCount++; } // Slabs need at least 3 wall segments to stay up (a real building has many) if (supportCount < 3) { p.state = 'falling'; p.vx = (Math.random() - 0.5) * 0.3; p.vy = -1.0; p.vz = (Math.random() - 0.5) * 0.3; p.avx = (Math.random() - 0.5) * 0.3; p.avy = 0; p.avz = (Math.random() - 0.5) * 0.3; if (p.mesh.material && p.mesh.material.color) { p.mesh.material = p.mesh.material.clone(); p.mesh.material.color.multiplyScalar(0.6); } anyMoving = true; } } // Stop when everything settled after quake ends if (!quakeActive && !anyMoving) { isSimulating = false; clearInterval(interval); document.getElementById('status').textContent = 'Simulation complete - Capture images or analyze rescue paths'; } } function getGroundAt(part) { // Simple debris stacking: check if fallen parts are below var ground = 0.1; for (var i = 0; i < buildingParts.length; i++) { var o = buildingParts[i]; if (o === part || o.state !== 'landed') continue; var dx = Math.abs(part.mesh.position.x - o.mesh.position.x); var dz = Math.abs(part.mesh.position.z - o.mesh.position.z); if (dx < 1.2 && dz < 1.2) { var top = o.mesh.position.y + 0.25; if (top > ground) ground = top; } } return ground; } // ==================== INFO / ANALYSIS ==================== function updateSimInfo(pga) { var info = document.getElementById('sim-info'); var level = pga < 0.1 ? 'Light (I-V)' : pga < 0.3 ? 'Moderate (VI-VII)' : pga < 0.6 ? 'Strong (VII-VIII)' : pga < 1.0 ? 'Severe (IX)' : 'Extreme (X+)'; var pattern = pga < 0.1 ? 'Cosmetic damage, glass cracking' : pga < 0.3 ? 'Non-structural damage, chimney fall' : pga < 0.6 ? 'Structural damage, partial collapse' : pga < 1.0 ? 'Heavy damage, story collapse' : 'Catastrophic, total collapse possible'; info.innerHTML = '

PGA: ' + pga.toFixed(2) + 'g — ' + level + '

' + '

Expected: ' + pattern + '

' + '

Style: ' + config.buildingStyle + ' | Material: ' + config.material + '

' + '

Foundation: ' + config.foundation + ' (×' + foundationFactors[config.foundation] + ' amplification)

' + '

Quake: M' + config.magnitude + ' at ' + config.distance + 'km | Duration: ' + config.duration + 's

'; } function analyzeRescuePaths() { var panel = document.getElementById('analysis-panel'); panel.style.display = 'block'; // Estimate damage from how many parts actually fell var totalParts = 0, fallenParts = 0; buildingParts.forEach(function(p) { if (p.floor >= 0) { totalParts++; if (p.state === 'landed') fallenParts++; } }); var damage = totalParts > 0 ? fallenParts / totalParts : 0; var rooms = window.roomLayout; var html = '

🔍 Rescue Analysis

'; html += '
Actual Collapse: ' + (damage*100).toFixed(0) + '% of structure collapsed
'; html += '
Probable Survivor Locations:
'; var locs = getSurvivorLocations(damage, rooms); locs.forEach(function(l) { html += '
[' + l.pri.toUpperCase() + '] ' + l.desc + '
'; }); html += '
Rescue Approach:
'; var paths = getRescuePaths(damage); paths.forEach(function(p) { html += '
[' + p.s.toUpperCase() + '] ' + p.d + '
'; }); html += '
⚠️ Aftershock Risks:
'; getAftershockRisks(damage).forEach(function(r) { html += '
[DANGER] ' + r + '
'; }); panel.innerHTML = html; document.getElementById('closeAnalysis').onclick = function() { panel.style.display = 'none'; rescueMarkers.forEach(function(m) { scene.remove(m); }); rescueMarkers = []; rescueAnimObjects = []; }; showRescueMarkers(locs); } function getSurvivorLocations(damage, rooms) { var locs = []; var H = 3.0; rooms.groundFloor.forEach(function(r) { var pri = damage < 0.4 ? 'safe' : damage < 0.7 ? 'warning' : 'danger'; var desc = r.name + ' (Ground) - '; if (damage < 0.3) desc += 'Structure intact, self-evacuation likely.'; else if (r.name.includes('Bathroom')) desc += 'Small room void space - priority search area.'; else desc += 'Check near furniture for void spaces.'; var debrisY = getDebrisHeight(r.x, r.z); locs.push({ pri: pri, desc: desc, x: r.x, y: debrisY + 0.5, z: r.z }); }); if (rooms.floors) { rooms.floors.forEach(function(floorRooms, fi) { floorRooms.forEach(function(r) { var floorNum = fi + 1; var pri = damage > 0.5 ? 'danger' : 'warning'; var desc = r.name + ' (Floor ' + (floorNum + 1) + ') - '; if (r.name.includes('Bedroom')) desc += 'HIGH PRIORITY (night occupancy). '; if (damage > 0.7) desc += 'Pancake collapse - search bed-level voids.'; else if (damage > 0.4) desc += 'Lean-to collapse - check wall-adjacent areas.'; else desc += 'Check under doorframes.'; var debrisY = getDebrisHeight(r.x, r.z); locs.push({ pri: pri, desc: desc, x: r.x, y: debrisY + 0.5, z: r.z }); }); }); } return locs; } // Scan fallen debris to find the top surface height at a given x,z position function getDebrisHeight(x, z) { var maxY = 0.1; buildingParts.forEach(function(p) { if (p.state !== 'landed') return; var dx = Math.abs(p.mesh.position.x - x); var dz = Math.abs(p.mesh.position.z - z); if (dx < 2.0 && dz < 2.0) { var top = p.mesh.position.y + 0.3; if (top > maxY) maxY = top; } }); return maxY; } function getRescuePaths(damage) { var p = []; p.push({ s: 'safe', d: 'Approach from ' + (damage > 0.5 ? 'least-collapsed side' : 'main entrance') + '.' }); if (damage > 0.4) p.push({ s: 'warning', d: 'Shore before entry. Use listening devices.' }); if (config.foundation === 'liquefiable') p.push({ s: 'danger', d: 'Ground instability - watch for sinkholes.' }); if (config.stories >= 2 && damage > 0.5) p.push({ s: 'warning', d: 'Use exterior ladder for upper floors (' + config.stories + ' stories).' }); p.push({ s: 'safe', d: 'Priority: 1) Bedrooms 2) Bathrooms 3) Kitchen' }); return p; } function getAftershockRisks(damage) { var r = []; if (damage > 0.3) r.push('Standing walls may topple - keep 1.5× wall height distance.'); if (config.material === 'unreinforcedMasonry' || config.material === 'adobe') r.push('Brittle debris unpredictable - overhead protection required.'); if (config.stories >= 2 && damage > 0.5) r.push('Upper slabs (' + config.stories + ' floors) may be precariously balanced.'); if (config.foundation === 'liquefiable') r.push('Liquefaction continues during aftershocks.'); r.push('Establish exclusion zone. Assign movement spotter.'); return r; } function showRescueMarkers(locs) { rescueMarkers.forEach(function(m) { scene.remove(m); }); rescueMarkers = []; rescueAnimObjects = []; // Survivor location markers (animated pulsing) locs.forEach(function(l, idx) { var color = l.pri === 'safe' ? 0x40916c : l.pri === 'warning' ? 0xf4a261 : 0xe94560; var sphereMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.7 }); var sphere = new THREE.Mesh(new THREE.SphereGeometry(0.35, 12, 12), sphereMat); sphere.position.set(l.x, l.y, l.z); scene.add(sphere); rescueMarkers.push(sphere); rescueAnimObjects.push({ type: 'pulse', mesh: sphere, phase: idx * 0.8 }); // Animated expanding ring var ringMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5, side: THREE.DoubleSide }); var ring = new THREE.Mesh(new THREE.TorusGeometry(0.55, 0.05, 8, 24), ringMat); ring.position.set(l.x, l.y, l.z); ring.rotation.x = Math.PI / 2; scene.add(ring); rescueMarkers.push(ring); rescueAnimObjects.push({ type: 'ring', mesh: ring, phase: idx * 1.2 }); // Ground-level colored zone plane (visible from above) var zoneSize = 2.5; var zoneMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.25, side: THREE.DoubleSide }); var zone = new THREE.Mesh(new THREE.PlaneGeometry(zoneSize, zoneSize), zoneMat); zone.rotation.x = -Math.PI / 2; zone.position.set(l.x, 0.05, l.z); scene.add(zone); rescueMarkers.push(zone); // Zone border outline var borderMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.6 }); var bSize = zoneSize / 2; var bThick = 0.06; // Four edges of zone square var edges = [ { x: l.x, z: l.z - bSize, w: zoneSize, d: bThick }, { x: l.x, z: l.z + bSize, w: zoneSize, d: bThick }, { x: l.x - bSize, z: l.z, w: bThick, d: zoneSize }, { x: l.x + bSize, z: l.z, w: bThick, d: zoneSize } ]; edges.forEach(function(e) { var edgeMesh = new THREE.Mesh( new THREE.BoxGeometry(e.w, 0.05, e.d), borderMat ); edgeMesh.position.set(e.x, 0.08, e.z); scene.add(edgeMesh); rescueMarkers.push(edgeMesh); }); }); // ===== SAFE ZONE (green area away from building) ===== var W = 10, D = 7; var safeZoneMat = new THREE.MeshBasicMaterial({ color: 0x40916c, transparent: true, opacity: 0.15, side: THREE.DoubleSide }); // Safe zone on the approach side (large green area) var safeZone = new THREE.Mesh(new THREE.PlaneGeometry(12, 8), safeZoneMat); safeZone.rotation.x = -Math.PI / 2; safeZone.position.set(0, 0.03, D/2 + 14); scene.add(safeZone); rescueMarkers.push(safeZone); // Safe zone border var szBorder = new THREE.Mesh( new THREE.BoxGeometry(12, 0.04, 0.08), new THREE.MeshBasicMaterial({ color: 0x40916c, transparent: true, opacity: 0.5 }) ); szBorder.position.set(0, 0.06, D/2 + 10); scene.add(szBorder); rescueMarkers.push(szBorder); // ===== DANGER ZONE (red area around building) ===== var dangerZoneMat = new THREE.MeshBasicMaterial({ color: 0xe94560, transparent: true, opacity: 0.1, side: THREE.DoubleSide }); var dangerZone = new THREE.Mesh(new THREE.PlaneGeometry(W + 5, D + 5), dangerZoneMat); dangerZone.rotation.x = -Math.PI / 2; dangerZone.position.set(0, 0.04, 0); scene.add(dangerZone); rescueMarkers.push(dangerZone); // ===== RESCUE APPROACH PATHS ===== var pathColor = 0x00e5ff; // Determine least-collapsed side var sideFallen = { front: 0, back: 0, left: 0, right: 0 }; buildingParts.forEach(function(p) { if (p.state !== 'landed') return; if (p.mesh.position.z > D/2) sideFallen.front++; else if (p.mesh.position.z < -D/2) sideFallen.back++; if (p.mesh.position.x < -W/2) sideFallen.left++; else if (p.mesh.position.x > W/2) sideFallen.right++; }); var minSide = 'front', minCount = sideFallen.front; if (sideFallen.back < minCount) { minSide = 'back'; minCount = sideFallen.back; } if (sideFallen.left < minCount) { minSide = 'left'; minCount = sideFallen.left; } if (sideFallen.right < minCount) { minSide = 'right'; minCount = sideFallen.right; } // Define approach paths var paths = []; if (minSide === 'front' || minSide === 'back') { var zs = minSide === 'front' ? 1 : -1; paths.push([{x:0, z:zs*18}, {x:0, z:zs*14}, {x:0, z:zs*10}, {x:0, z:zs*6}, {x:0, z:zs*(D/2+1)}]); paths.push([{x:-5, z:zs*16}, {x:-3.5, z:zs*11}, {x:-W/2+1, z:zs*(D/2+1)}]); paths.push([{x:5, z:zs*16}, {x:3.5, z:zs*11}, {x:W/2-1, z:zs*(D/2+1)}]); } else { var xs = minSide === 'left' ? -1 : 1; paths.push([{x:xs*18, z:0}, {x:xs*14, z:0}, {x:xs*10, z:0}, {x:xs*6, z:0}, {x:xs*(W/2+1), z:0}]); paths.push([{x:xs*16, z:-3}, {x:xs*11, z:-2}, {x:xs*(W/2+1), z:-D/4}]); paths.push([{x:xs*16, z:3}, {x:xs*11, z:2}, {x:xs*(W/2+1), z:D/4}]); } // Draw paths with static road + animated flowing arrows paths.forEach(function(pathPts, pathIdx) { var isMain = pathIdx === 0; var thickness = isMain ? 0.15 : 0.1; var yLevel = 0.12; // Static path road surface for (var i = 0; i < pathPts.length - 1; i++) { var p1 = pathPts[i], p2 = pathPts[i+1]; var mx = (p1.x + p2.x) / 2; var mz = (p1.z + p2.z) / 2; var dx = p2.x - p1.x, dz = p2.z - p1.z; var len = Math.sqrt(dx*dx + dz*dz); var angle = Math.atan2(dx, dz); // Road base var road = new THREE.Mesh( new THREE.BoxGeometry(thickness * 3, 0.04, len), new THREE.MeshBasicMaterial({ color: 0x1a6070, transparent: true, opacity: 0.5 }) ); road.position.set(mx, yLevel, mz); road.rotation.y = angle; scene.add(road); rescueMarkers.push(road); // Edge lines for (var side = -1; side <= 1; side += 2) { var edge = new THREE.Mesh( new THREE.BoxGeometry(0.04, 0.06, len), new THREE.MeshBasicMaterial({ color: pathColor, transparent: true, opacity: 0.7 }) ); edge.position.set(mx + Math.cos(angle + Math.PI/2) * thickness * 1.4 * side, yLevel + 0.02, mz + Math.sin(angle + Math.PI/2) * thickness * 1.4 * side * -1); edge.rotation.y = angle; scene.add(edge); rescueMarkers.push(edge); } } // Animated flowing arrow particles along the path var numFlowArrows = isMain ? 6 : 4; for (var a = 0; a < numFlowArrows; a++) { // Pick a random segment to flow along var segIdx = a % (pathPts.length - 1); var p1 = pathPts[segIdx], p2 = pathPts[segIdx + 1]; var dx = p2.x - p1.x, dz = p2.z - p1.z; var angle = Math.atan2(dx, dz); var arrowMat = new THREE.MeshBasicMaterial({ color: pathColor, transparent: true, opacity: 0.8 }); var arrow = new THREE.Mesh(new THREE.ConeGeometry(0.15, 0.4, 5), arrowMat); arrow.position.set(p1.x, yLevel + 0.25, p1.z); arrow.rotation.x = Math.PI / 2; arrow.rotation.z = -angle; scene.add(arrow); rescueMarkers.push(arrow); rescueAnimObjects.push({ type: 'flow', mesh: arrow, start: { x: p1.x, z: p1.z }, end: { x: p2.x, z: p2.z }, speed: 0.4 + Math.random() * 0.2, offset: a / numFlowArrows }); } // Start point assembly marker if (pathIdx === 0) { var startMat = new THREE.MeshBasicMaterial({ color: 0x40916c, transparent: true, opacity: 0.8 }); var startRing = new THREE.Mesh(new THREE.TorusGeometry(0.7, 0.12, 8, 24), startMat); startRing.position.set(pathPts[0].x, 0.2, pathPts[0].z); startRing.rotation.x = Math.PI / 2; scene.add(startRing); rescueMarkers.push(startRing); rescueAnimObjects.push({ type: 'pulse', mesh: startRing, phase: 0 }); // Cross marker addBlock(pathPts[0].x, 0.3, pathPts[0].z, 1.0, 0.08, 0.15, 0x40916c, 'marker', -1); addBlock(pathPts[0].x, 0.3, pathPts[0].z, 0.15, 0.08, 1.0, 0x40916c, 'marker', -1); } }); // Animated danger zone perimeter (blinking red) var dangerDist = 2.5; var perim = [ [-W/2-dangerDist, D/2+dangerDist], [W/2+dangerDist, D/2+dangerDist], [W/2+dangerDist, -D/2-dangerDist], [-W/2-dangerDist, -D/2-dangerDist] ]; for (var i = 0; i < 4; i++) { var p1 = perim[i], p2 = perim[(i+1)%4]; var mx = (p1[0]+p2[0])/2, mz = (p1[1]+p2[1])/2; var dx = p2[0]-p1[0], dz = p2[1]-p1[1]; var len = Math.sqrt(dx*dx+dz*dz); var angle = Math.atan2(dx, dz); // Dashed segments for danger border var numDashes = Math.floor(len / 1.5); for (var d = 0; d < numDashes; d++) { var dt = (d + 0.5) / numDashes; var dashX = p1[0] + dx * dt; var dashZ = p1[1] + dz * dt; var dashMat = new THREE.MeshBasicMaterial({ color: 0xe94560, transparent: true, opacity: 0.6 }); var dash = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.08, len/numDashes * 0.6), dashMat); dash.position.set(dashX, 0.1, dashZ); dash.rotation.y = angle; scene.add(dash); rescueMarkers.push(dash); rescueAnimObjects.push({ type: 'blink', mesh: dash, phase: d * 0.5 + i * 2 }); } } } // ==================== CAPTURE & GALLERY ==================== function captureImage() { renderer.render(scene, camera); var url = renderer.domElement.toDataURL('image/png'); var totalParts = 0, fallenParts = 0; buildingParts.forEach(function(p) { if (p.floor >= 0) { totalParts++; if (p.state === 'landed' || p.state === 'falling') fallenParts++; } }); var actualDamage = totalParts > 0 ? fallenParts / totalParts : 0; capturedImages.push({ url: url, cfg: JSON.parse(JSON.stringify(config)), damage: actualDamage, time: new Date().toLocaleTimeString() }); document.getElementById('status').textContent = '📸 Captured (' + capturedImages.length + ' images)'; var flash = document.createElement('div'); flash.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;background:white;opacity:0.5;transition:opacity 0.3s;pointer-events:none;'; document.getElementById('viewport').appendChild(flash); setTimeout(function() { flash.style.opacity = '0'; }, 50); setTimeout(function() { flash.remove(); }, 400); } function showGallery() { var g = document.getElementById('gallery'); var grid = document.getElementById('galleryGrid'); if (capturedImages.length === 0) { grid.innerHTML = '

No images yet. Run simulation then click Capture.

'; } else { var h = ''; for (var i = 0; i < capturedImages.length; i++) { var img = capturedImages[i]; h += ''; } grid.innerHTML = h; } g.style.display = 'block'; } // ==================== RESET & RENDER LOOP ==================== function resetScene() { isSimulating = false; simulationTime = 0; clearAll(); // Reset ground position if (groundMesh) groundMesh.position.set(0, 0, 0); if (pathMesh) pathMesh.position.set(0, 0.01, 8); buildHouse(); document.getElementById('status').textContent = 'Ready - Configure and start simulation'; document.getElementById('sim-info').textContent = 'Set parameters and press Start.'; } var rescueAnimObjects = []; // objects that animate when rescue paths are shown function animate() { requestAnimationFrame(animate); // Animate rescue path elements if (rescueAnimObjects.length > 0) { var t = performance.now() * 0.001; rescueAnimObjects.forEach(function(obj) { if (obj.type === 'pulse') { // Pulsing survivor markers var s = 1.0 + Math.sin(t * 3 + obj.phase) * 0.2; obj.mesh.scale.set(s, s, s); obj.mesh.material.opacity = 0.5 + Math.sin(t * 2 + obj.phase) * 0.3; } else if (obj.type === 'flow') { // Flowing arrows along path var flowT = ((t * obj.speed + obj.offset) % 1.0); var p1 = obj.start; var p2 = obj.end; obj.mesh.position.x = p1.x + (p2.x - p1.x) * flowT; obj.mesh.position.z = p1.z + (p2.z - p1.z) * flowT; obj.mesh.material.opacity = Math.sin(flowT * Math.PI) * 0.9; } else if (obj.type === 'blink') { // Blinking danger perimeter obj.mesh.material.opacity = 0.3 + Math.sin(t * 4 + obj.phase) * 0.3; } else if (obj.type === 'ring') { // Expanding ring at survivor location var ringScale = 1.0 + ((t * 0.8 + obj.phase) % 2.0) * 0.5; var ringOpacity = Math.max(0, 0.6 - ((t * 0.8 + obj.phase) % 2.0) * 0.3); obj.mesh.scale.set(ringScale, ringScale, ringScale); obj.mesh.material.opacity = ringOpacity; } }); } renderer.render(scene, camera); } // ==================== UI ==================== function setupUI() { document.getElementById('magnitude').oninput = function(e) { config.magnitude = parseFloat(e.target.value); document.getElementById('magValue').textContent = config.magnitude.toFixed(1); }; document.getElementById('distance').oninput = function(e) { config.distance = parseInt(e.target.value); document.getElementById('distValue').textContent = config.distance; }; document.getElementById('duration').oninput = function(e) { config.duration = parseInt(e.target.value); document.getElementById('durValue').textContent = config.duration; }; document.getElementById('buildingAge').oninput = function(e) { config.buildingAge = parseInt(e.target.value); document.getElementById('ageValue').textContent = config.buildingAge; }; document.getElementById('buildingStyle').onchange = function(e) { config.buildingStyle = e.target.value; var m = { european: 'unreinforcedMasonry', chinese: 'wood', japanese: 'wood', american: 'wood', middleEast: 'adobe' }; config.material = m[e.target.value]; document.getElementById('material').value = config.material; resetScene(); }; document.getElementById('stories').onchange = function(e) { config.stories = parseInt(e.target.value); resetScene(); }; document.getElementById('material').onchange = function(e) { config.material = e.target.value; resetScene(); }; document.getElementById('foundation').onchange = function(e) { config.foundation = e.target.value; }; document.getElementById('startBtn').onclick = function() { resetScene(); setTimeout(startEarthquake, 200); }; document.getElementById('resetBtn').onclick = resetScene; document.getElementById('captureBtn').onclick = captureImage; document.getElementById('analyzeBtn').onclick = analyzeRescuePaths; document.getElementById('galleryBtn').onclick = showGallery; document.getElementById('closeGallery').onclick = function() { document.getElementById('gallery').style.display = 'none'; }; window.onresize = function() { var vp = document.getElementById('viewport'); camera.aspect = vp.clientWidth / vp.clientHeight; camera.updateProjectionMatrix(); renderer.setSize(vp.clientWidth, vp.clientHeight); }; } // ==================== INIT ==================== function init() { initScene(); buildHouse(); setupUI(); animate(); } window.addEventListener('DOMContentLoaded', init);