地震模拟器 · Earthquake Simulator
by FlashMaker22853 lines33.2 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>地震模拟器 · Earthquake Simulator</title>
<style>
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body {
margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden;
background: #050a10; color: #dbe9f5;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
user-select: none;
}
canvas { display: block; position: absolute; inset: 0; cursor: crosshair; }
.panel {
position: absolute;
background: rgba(9, 18, 29, 0.80);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border: 1px solid rgba(94, 200, 240, 0.15);
border-radius: 11px;
padding: 9px 10px;
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.62), inset 0 1px 0 rgba(255,255,255,0.05);
}
#left { top: 10px; left: 10px; width: 172px; }
#right {
top: 10px; right: 10px; width: 194px;
max-height: calc(100vh - 20px);
display: flex; flex-direction: column;
}
h1 {
margin: 0 0 1px; font-size: 12.5px; font-weight: 700; letter-spacing: 0.3px;
background: linear-gradient(90deg, #5ec8f0, #a855f7);
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent;
}
.sub {
font-size: 6.5px; letter-spacing: 1.6px; color: #4a7fa5;
margin-bottom: 8px; font-weight: 600;
}
.ctrl { margin-bottom: 8px; }
.ctrl label {
display: flex; justify-content: space-between; align-items: baseline;
font-size: 9.5px; color: #8bb4d4; margin-bottom: 4px;
}
.ctrl .val {
font-size: 13px; font-weight: 800; color: #5ec8f0;
font-variant-numeric: tabular-nums;
text-shadow: 0 0 12px rgba(94, 200, 240, 0.55);
font-family: "SF Mono", Menlo, Consolas, monospace;
}
.ctrl .val.warn { color: #ff7043; text-shadow: 0 0 12px rgba(255,112,67,0.6); }
input[type=range] {
-webkit-appearance: none; appearance: none;
width: 100%; height: 4px; border-radius: 2px; outline: none;
background: linear-gradient(90deg, #1d3b52, #3d7ab8);
margin: 0;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 13px; height: 13px; border-radius: 50%;
background: #5ec8f0; border: 2.5px solid #08131e;
box-shadow: 0 0 9px rgba(94, 200, 240, 0.9);
cursor: pointer; transition: transform 0.12s;
}
input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.18); }
input[type=range]::-moz-range-thumb {
width: 10px; height: 10px; border-radius: 50%;
background: #5ec8f0; border: 2.5px solid #08131e;
box-shadow: 0 0 9px rgba(94, 200, 240, 0.9); cursor: pointer;
}
.scale {
display: flex; justify-content: space-between;
font-size: 7px; color: #3f6b8a; margin-top: 2px;
font-family: monospace;
}
.btns { display: flex; gap: 5px; margin-top: 9px; }
button {
flex: 1; padding: 6px 4px; border-radius: 7px; cursor: pointer;
border: 1px solid rgba(94, 200, 240, 0.25);
background: rgba(94, 200, 240, 0.08);
color: #a8d8f0; font-size: 9.5px; font-weight: 600;
font-family: inherit; transition: all 0.18s;
}
button:hover {
background: rgba(94, 200, 240, 0.22);
border-color: rgba(94, 200, 240, 0.6);
color: #e8f6ff; transform: translateY(-1px);
}
button:active { transform: translateY(0); }
button.danger {
border-color: rgba(255, 80, 80, 0.35);
background: rgba(255, 60, 60, 0.1);
color: #ff8f8f;
}
button.danger:hover {
background: rgba(255, 60, 60, 0.25);
border-color: rgba(255, 80, 80, 0.7);
color: #ffd0d0;
}
.rlabel { font-size: 7.5px; letter-spacing: 1.2px; color: #4a7fa5; font-weight: 700; }
#deaths {
font-size: 23px; font-weight: 900; line-height: 1.05;
font-variant-numeric: tabular-nums;
font-family: "SF Mono", Menlo, Consolas, monospace;
color: #ff4d4d;
text-shadow: 0 0 18px rgba(255, 60, 60, 0.55);
letter-spacing: -0.6px;
transition: color 0.3s;
margin-top: 1px;
}
#meta {
font-size: 8.5px; color: #6f9cbd; margin-top: 4px;
padding-bottom: 6px; border-bottom: 1px solid rgba(94,200,240,0.11);
line-height: 1.45;
}
#meta b { color: #a8d8f0; font-weight: 600; }
.lhead {
display: flex; justify-content: space-between;
font-size: 7.5px; letter-spacing: 0.5px; color: #4a7fa5;
font-weight: 700; padding: 7px 0 4px;
}
#list { overflow-y: auto; flex: 1; min-height: 0; margin: 0 -4px; padding: 0 4px; }
#list::-webkit-scrollbar { width: 3px; }
#list::-webkit-scrollbar-thumb { background: rgba(94,200,240,0.3); border-radius: 2px; }
.row {
display: flex; align-items: center; gap: 4px;
padding: 3.5px 4px; border-radius: 6px;
font-size: 9.5px; margin-bottom: 1.5px;
background: rgba(255,255,255,0.02);
animation: slideIn 0.3s cubic-bezier(0.2,0.9,0.3,1) both;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(10px); }
to { opacity: 1; transform: translateX(0); }
}
.row .dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; box-shadow: 0 0 7px currentColor; }
.row .name { flex: 1; color: #c8dcec; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row .mi {
font-family: monospace; font-size: 8px; font-weight: 700;
padding: 0.5px 3px; border-radius: 3px;
background: rgba(255,255,255,0.07); color: #9fc4dd;
min-width: 22px; text-align: center;
}
.row .d {
font-family: "SF Mono", Menlo, monospace;
font-size: 8.5px; font-weight: 700; color: #ff7a7a;
min-width: 38px; text-align: right;
}
.row .d.zero { color: #3f6b8a; }
.empty {
text-align: center; color: #3f6b8a; font-size: 9.5px;
padding: 16px 6px; line-height: 1.6;
}
#hint {
position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%);
font-size: 9.5px; color: #5b8aa8; letter-spacing: 0.6px;
background: rgba(9,18,29,0.70);
padding: 5px 13px; border-radius: 14px;
border: 1px solid rgba(94,200,240,0.13);
backdrop-filter: blur(9px);
pointer-events: none; white-space: nowrap;
animation: float 3s ease-in-out infinite;
}
@keyframes float { 0%,100% { opacity: 0.55; } 50% { opacity: 0.95; } }
.foot {
font-size: 7px; color: #33566e; margin-top: 7px; line-height: 1.45;
border-top: 1px solid rgba(94,200,240,0.09); padding-top: 6px;
}
@media (max-width: 760px) {
#left { width: 142px; padding: 8px 9px; top: 7px; left: 7px; }
#right { width: 154px; padding: 8px 9px; top: 7px; right: 7px; }
h1 { font-size: 11px; }
.sub { display: none; }
#deaths { font-size: 18px; }
.ctrl { margin-bottom: 7px; }
.ctrl label { font-size: 8.5px; }
.ctrl .val { font-size: 11.5px; }
.btns { flex-direction: column; gap: 4px; margin-top: 8px; }
button { padding: 5px 4px; font-size: 9px; }
#hint { font-size: 8.5px; padding: 4px 10px; bottom: 8px; }
.row { font-size: 9px; padding: 3px 3px; }
.row .d { min-width: 32px; font-size: 8px; }
.row .mi { font-size: 7.5px; min-width: 19px; }
.foot { display: none; }
#meta { font-size: 8px; }
.lhead { font-size: 7px; }
.rlabel { font-size: 7px; }
}
</style>
</head>
<body>
<canvas id="map"></canvas>
<div class="panel" id="left">
<h1>🌍 地震模拟器</h1>
<div class="sub">EARTHQUAKE SIMULATOR</div>
<div class="ctrl">
<label>地震等级 <span class="val" id="magVal">7.0</span></label>
<input type="range" id="mag" min="3" max="9.5" step="0.1" value="7">
<div class="scale"><span>3.0 微震</span><span>9.5 灭世</span></div>
</div>
<div class="ctrl">
<label>震源深度 <span class="val" id="depVal">10 km</span></label>
<input type="range" id="dep" min="5" max="700" step="5" value="10">
<div class="scale"><span>5 浅源</span><span>700 深源</span></div>
</div>
<div class="btns">
<button id="randomBtn">🎲 随机地震</button>
<button id="worstBtn" class="danger">💀 世纪大震</button>
</div>
<div class="foot">
点击地图任意位置设置震中 · 模型基于经验烈度衰减公式,仅供演示
</div>
</div>
<div class="panel" id="right">
<div class="rlabel">预估死亡人数</div>
<div id="deaths">0</div>
<div id="meta">震中:--</div>
<div class="lhead">
<span>受影响地区</span>
<span>烈度 / 死亡</span>
</div>
<div id="list"></div>
</div>
<div id="hint">点击地图任意位置设置震中</div>
<script>
/* =========================================================
1. 细化地理数据(高精度海岸线 + 岛屿)
========================================================= */
const LAND = [
// ---------- 北美洲 ----------
[[-168,65],[-166,68],[-160,70],[-156,71],[-150,70.5],[-145,70],[-141,70],[-136,69],[-131,70],
[-128,70.5],[-125,70],[-120,69.5],[-117,69],[-112,68.5],[-108,68],[-104,68.5],[-100,68],
[-97,68],[-94,68.5],[-90,69],[-86,68.5],[-83,69],[-80,70],[-78,71],[-76,72],[-73,73],
[-70,72],[-67,71],[-64,69],[-62,66],[-63,64],[-65,61],[-63,58],[-60,55],[-57,53],[-56,52],
[-60,50],[-63,49],[-66,48],[-69,47],[-66,45],[-68,44],[-70,43],[-72,42],[-74,40.5],[-75,39],
[-76,38],[-76.5,36],[-77,35],[-79,33],[-81,31],[-81,29],[-80,27],[-80.5,25.5],[-81,25],
[-83,28],[-84,29.5],[-85,30],[-87,30.5],[-89,29.5],[-91,29],[-94,29.5],[-96,28.5],[-97,26],
[-97.5,24],[-97.5,22],[-97,20.5],[-98,19.5],[-100,19],[-102,19.5],[-104,20.5],[-105,20],
[-106,22],[-106,24],[-108,25],[-109,26],[-110,24],[-111,25],[-112,27],[-113,29],[-114,30],
[-115,31],[-117,32],[-118,33],[-120,34],[-121,36],[-122,37],[-123,39],[-124,41],[-124,43],
[-124,46],[-125,48],[-127,50],[-130,53],[-133,55],[-135,57],[-138,58.5],[-141,60],[-145,60.5],
[-148,60],[-151,59.5],[-154,58.5],[-158,57],[-160,56.5],[-162,58],[-164,60],[-166,62]],
// ---------- 格陵兰 ----------
[[-45,60],[-40,62],[-36,64],[-33,66],[-30,68],[-26,70],[-22,72],[-20,74],[-19,76],[-22,78],
[-26,79],[-30,81],[-36,82],[-44,82.5],[-52,82],[-58,81],[-62,79],[-64,77],[-62,74],
[-58,71],[-54,68],[-50,65],[-47,62]],
// ---------- 南美洲 ----------
[[-81,7],[-79,9],[-77,9.5],[-75,10.5],[-72,11.5],[-69,12],[-65,11],[-62,10.5],[-60,9],[-57,7],
[-54,6],[-51,4],[-50,1],[-49,-1],[-47,-2],[-44,-3],[-41,-3],[-38,-5],[-35,-6],[-37,-9],
[-38,-12],[-39,-15],[-39,-18],[-41,-21],[-44,-23],[-47,-25],[-49,-27],[-52,-31],[-54,-34],
[-57,-37],[-59,-39],[-62,-40],[-63,-42],[-65,-44],[-66,-46],[-68,-49],[-69,-52],[-70,-54],
[-71,-55],[-73,-54],[-74,-51],[-74,-48],[-73,-45],[-73,-42],[-72,-38],[-71,-34],[-71,-30],
[-70,-27],[-70,-24],[-71,-22],[-70,-19],[-71,-17],[-73,-15],[-76,-14],[-78,-11],[-79,-8],
[-80,-5],[-81,-3],[-81,-1],[-80,1],[-79,3],[-78,5]],
// ---------- 非洲 ----------
[[-17,15],[-17,17],[-16,19],[-16,21],[-14,23],[-13,25],[-12,27],[-10,29],[-9,31],[-8,33],
[-6,35],[-4,36],[-1,36],[2,36.5],[6,37],[10,37],[13,36],[16,34],[19,33],[22,32.5],[25,32],
[28,31.5],[31,31],[33,30],[34,28],[35,26],[37,24],[38,22],[40,20],[42,18],[43,15],[43,13],
[44,11],[46,10],[48,9],[50,10],[51,11],[49,7],[47,5],[45,3],[43,1],[41,-1],[40,-3],[40,-6],
[40,-9],[39,-12],[38,-15],[36,-18],[35,-21],[33,-24],[32,-27],[30,-30],[28,-32],[26,-34],
[22,-34.5],[19,-35],[17,-34],[15,-31],[14,-28],[13,-25],[12,-22],[12,-19],[11,-16],[9,-13],
[9,-10],[7,-7],[5,-5],[3,-3],[2,0],[1,3],[0,5],[-2,5],[-4,5],[-6,5],[-8,5],[-10,6],[-12,8],
[-14,10],[-16,12],[-17,14]],
// ---------- 欧亚大陆 ----------
[[-10,36],[-9,38],[-9,40],[-8,42],[-8.5,43.5],[-7,44],[-4,44],[-2,44.5],[0,46],[1,47],[3,48],
[4,50],[5,52],[6,53],[7,54],[8,55],[9,56],[10,57],[10,58],[8,59],[6,60],[7,62],[10,64],
[13,66],[16,68],[20,69],[24,70],[28,71],[32,70],[36,69],[40,68.5],[44,68],[48,68.5],[52,69],
[56,69.5],[60,70],[64,71],[68,72],[72,73],[76,73.5],[80,74],[85,75],[90,76],[95,76.5],
[100,77],[105,77.5],[110,77],[115,75],[120,74],[125,73.5],[130,73],[134,72],[138,71],
[142,70.5],[146,70],[150,70],[154,70],[158,69.5],[162,69],[166,68.5],[170,68],[174,67],
[178,66],[180,65],[180,62],[176,62],[172,61.5],[168,60.5],[164,60],[160,59.5],[156,58],
[152,56],[148,54],[144,52],[140,50],[137,48],[134,46],[131,44],[128,42],[126,40],[124,38],
[122,36],[120,33],[119,30],[117,27],[114,24],[111,21],[108,17],[106,14],[104,11],[103,8],
[102,5],[100,3],[99,6],[97,9],[95,12],[93,15],[91,18],[89,21],[87,22],[84,22],[81,20],
[78,17],[75,14],[73,17],[71,20],[69,22],[66,24],[63,25],[60,25],[57,23],[54,21],[51,19],
[48,17],[46,15],[44,13],[43,16],[42,19],[40,22],[38,25],[36,28],[34,31],[32,33],[30,35],
[28,36],[26,38],[24,40],[22,41],[20,40],[18,39],[16,38],[15,40],[14,42],[12,44],[10,44],
[8,44],[6,43],[4,43],[2,42],[0,41],[-2,39],[-4,38],[-6,37]],
// ---------- 澳大利亚 ----------
[[114,-22],[114,-24],[115,-26],[115,-28],[116,-30],[117,-32],[118,-34],[120,-34],[122,-34],
[124,-33],[126,-33],[128,-32],[130,-32],[132,-32],[134,-33],[135,-35],[137,-35],[139,-36],
[141,-38],[143,-39],[145,-38],[147,-38],[149,-37],[150,-36],[152,-33],[153,-30],[153,-28],
[152,-26],[150,-23],[148,-20],[146,-18],[144,-16],[142,-14],[141,-12],[140,-11],[138,-12],
[136,-12],[134,-11],[132,-11],[130,-12],[128,-14],[126,-14],[124,-16],[122,-17],[120,-18],
[118,-20],[116,-21]],
// ---------- 南极洲 ----------
[[-180,-72],[-160,-72],[-140,-73],[-120,-73],[-100,-74],[-80,-73],[-70,-71],[-60,-63],[-55,-63],
[-45,-65],[-40,-70],[-30,-72],[-20,-72],[-10,-70],[0,-70],[10,-70],[20,-70],[30,-69],[40,-68],
[50,-67],[60,-67],[70,-67],[80,-66],[90,-66],[100,-66],[110,-66],[120,-66],[130,-66],[140,-66],
[150,-68],[160,-70],[170,-72],[180,-72],[180,-90],[-180,-90]],
// ---------- 日本列岛 ----------
[[131,34],[133,34.5],[135,34],[136.5,35],[137,36],[138,37],[139.5,38],[140.5,39],[141,40],
[141.5,41],[141,41.5],[140.5,41.5],[140,40.5],[139,39.5],[138,38],[137,37],[136,36],[135,35.5],
[134,34.5],[133,33.5],[132,33.5]],
[[140,42],[141,42.5],[142,43.5],[143,44],[144.5,44.5],[145,44],[145.5,43.5],[144.5,43],
[143,42.5],[141.5,42],[140.5,41.5]],
[[129.5,33],[130,33.5],[131,33.5],[131.5,32.5],[131,31.5],[130.5,31],[130,31.5],[129.5,32]],
[[132,33],[133,33.5],[134,33.5],[134.5,33],[134,32.5],[133,32.5],[132,32.5]],
// ---------- 库页岛 ----------
[[142,46],[143,47],[143,49],[143,51],[142.5,53],[142,54],[141.5,53],[142,51],[142,48],[141.5,46.5]],
// ---------- 不列颠群岛 ----------
[[-5,50],[-4,51],[-3,51.5],[-2,52.5],[-1,53],[-1.5,54],[-2.5,55],[-3,55.5],[-2,56.5],[-3,57.5],
[-4,58.5],[-5,58.5],[-5.5,57.5],[-6,56.5],[-5,55],[-4.5,54],[-3.5,53.5],[-4,53],[-5,52],[-5.5,51]],
[[-6,52],[-6,53],[-6.5,54],[-7.5,55],[-8,55],[-9,54.5],[-10,54],[-10,53],[-9,52],[-8,51.5],[-7,52]],
// ---------- 冰岛 ----------
[[-24,65],[-22,66],[-18,66.5],[-15,66],[-13.5,65],[-14,64],[-16,63.5],[-19,63.5],[-22,64]],
// ---------- 马达加斯加 ----------
[[43,-12],[45,-12],[48,-13],[50,-15],[50,-17],[49,-20],[48,-22],[47,-25],[45,-25],[44,-24],
[43.5,-22],[43,-19],[43,-16],[43,-14]],
// ---------- 新西兰 ----------
[[172.5,-34.5],[174,-35],[175,-36],[176,-37],[177,-38],[178,-38.5],[178.5,-39.5],[177.5,-40],
[176,-41],[175,-41.5],[174.5,-41],[174,-40],[173,-39],[172.5,-38],[173,-37],[172.5,-36]],
[[172,-40.5],[173,-41],[174,-41.5],[174.5,-42],[173.5,-43],[172,-44],[171,-45],[170,-46],
[168.5,-46.5],[167,-46],[166,-45.5],[167,-44.5],[168,-44],[169.5,-43.5],[171,-42.5],[171.5,-41.5]],
// ---------- 台湾岛 / 海南岛 ----------
[[120,22],[121,22.5],[122,24],[121.5,25],[121,25.2],[120.5,24],[120,23]],
[[109,18],[110,19],[110.5,20],[109.5,20],[109,19.5]],
// ---------- 斯里兰卡 ----------
[[80,9.5],[81,9],[81.5,8],[82,7],[81.5,6],[80.5,6],[80,7],[79.8,8]],
// ---------- 苏门答腊 / 爪哇 / 婆罗洲 / 苏拉威西 ----------
[[95,5.5],[97,4],[99,2],[101,0],[103,-2],[105,-4],[106,-5.5],[105,-5.8],[103,-4.5],
[101,-2.5],[99,-1],[97,1],[95.5,3]],
[[105,-6],[107,-6.5],[110,-6.8],[112,-6.9],[114,-7.5],[114.5,-8.5],[113,-8.5],[111,-8],
[109,-7.5],[107,-7.2],[105.5,-6.8]],
[[109,2],[110,1],[112,0],[114,-1],[116,-2],[117,-3.5],[116.5,-4],[115,-3.5],[114,-3],
[112.5,-3.5],[111,-3],[110,-2],[109.5,0],[109,1]],
[[119,-1],[120,-2],[121,-3],[122,-4],[123,-4.5],[123,-5.5],[122,-5],[121,-4],[120,-3],
[119.5,-2],[118.5,-1.5]],
// ---------- 新几内亚 ----------
[[131,-1],[133,-1.5],[136,-2],[138,-2.5],[141,-3],[144,-4],[147,-5.5],[150,-6],[150.5,-8],
[149,-9],[147,-8.5],[145,-8],[143,-9],[141,-9],[139,-8],[137,-8],[135,-7],[133,-5],[132,-3],[131,-2]],
// ---------- 菲律宾 ----------
[[120,14],[121,15],[122,16],[122,18],[121,18.5],[120.5,17],[120,16],[119.5,15],[119.5,14]],
[[122,6],[123,7],[125,8],[126,9],[126,7],[125,6],[124,5.5],[123,6]],
[[122,10],[123,11],[124,11],[125,10],[124,9.5],[123,9],[122,9.5]],
// ---------- 加勒比 ----------
[[-84,22],[-82,23],[-80,23],[-78,22.5],[-75,21],[-74,20],[-77,20],[-80,21],[-82,21.5]],
[[-74,19],[-72,20],[-70,19.5],[-68,19],[-69,18],[-71,18],[-73,18]],
[[-67,18.5],[-65.5,18.4],[-65.5,18],[-67,18]],
// ---------- 堪察加半岛 ----------
[[156,51],[158,52],[160,53],[162,55],[163,57],[163,59],[162,60],[160,59],[158,57],[156,55],[155,53]],
// ---------- 夏威夷 / 加那利 ----------
[[-156,19],[-155,19.5],[-154.5,20],[-155,20.5],[-156,20],[-156.5,19.5]],
[[-16,28],[-15,28.5],[-14,28],[-15,27.5]]
];
// 城市数据:[名称, 纬度, 经度, 人口(百万), 建筑脆弱度]
const CITIES = [
['东京',35.68,139.69,37,0.07],['大阪',34.69,135.50,19,0.07],['名古屋',35.18,136.91,9,0.07],
['首尔',37.57,126.98,25,0.08],['釜山',35.18,129.08,3.5,0.09],
['北京',39.90,116.41,21,0.13],['上海',31.23,121.47,25,0.12],['广州',23.13,113.26,18,0.14],
['深圳',22.54,114.06,17,0.11],['香港',22.32,114.17,7.5,0.09],['台北',25.03,121.57,7,0.10],
['重庆',29.56,106.55,16,0.18],['成都',30.57,104.07,16,0.18],['西安',34.34,108.94,12,0.17],
['昆明',25.04,102.71,6,0.25],['乌鲁木齐',43.83,87.62,4,0.22],['哈尔滨',45.80,126.53,9,0.16],
['马尼拉',14.60,120.98,14,0.42],['雅加达',-6.21,106.85,11,0.50],['泗水',-7.25,112.75,3,0.46],
['曼谷',13.76,100.50,10,0.26],['河内',21.03,105.85,8,0.34],['胡志明市',10.82,106.63,9,0.33],
['新加坡',1.35,103.82,6,0.06],['吉隆坡',3.14,101.69,7,0.19],['仰光',16.87,96.20,5.5,0.52],
['达卡',23.81,90.41,21,0.62],['加尔各答',22.57,88.36,15,0.44],['德里',28.61,77.21,29,0.40],
['孟买',19.08,72.88,20,0.36],['卡拉奇',24.86,67.01,16,0.50],['拉合尔',31.55,74.34,13,0.50],
['加德满都',27.72,85.32,1.5,0.68],['科伦坡',6.93,79.86,2.5,0.40],
['德黑兰',35.69,51.39,9,0.42],['伊斯坦布尔',41.01,28.98,15,0.30],['巴格达',33.31,44.36,7,0.50],
['喀布尔',34.53,69.17,4,0.65],['安曼',31.95,35.93,2.2,0.25],['利雅得',24.71,46.68,7,0.12],
['迪拜',25.20,55.27,3.5,0.06],['开罗',30.04,31.24,21,0.42],['阿尔及尔',36.75,3.06,3,0.36],
['卡萨布兰卡',33.57,-7.59,3.7,0.34],['拉各斯',6.52,3.38,15,0.44],['金沙萨',-4.44,15.27,15,0.52],
['内罗毕',-1.29,36.82,5,0.42],['亚的斯亚贝巴',9.03,38.74,5,0.46],
['约翰内斯堡',-26.20,28.05,6,0.22],['开普敦',-33.92,18.42,4.6,0.19],
['莫斯科',55.76,37.62,12,0.14],['基辅',50.45,30.52,3,0.18],['布加勒斯特',44.43,26.10,2,0.22],
['雅典',37.98,23.73,3,0.18],['罗马',41.90,12.50,4,0.18],['巴黎',48.86,2.35,11,0.08],
['伦敦',51.51,-0.13,9,0.08],['马德里',40.42,-3.70,6,0.11],['里斯本',38.72,-9.14,2.9,0.14],
['纽约',40.71,-74.01,19,0.06],['洛杉矶',34.05,-118.24,13,0.07],['旧金山',37.77,-122.42,4.7,0.06],
['西雅图',47.61,-122.33,4,0.06],['温哥华',49.28,-123.12,2.6,0.06],
['墨西哥城',19.43,-99.13,21,0.34],['危地马拉城',14.63,-90.51,3,0.55],['圣萨尔瓦多',13.69,-89.19,2,0.55],
['波哥大',4.71,-74.07,11,0.34],['基多',-0.18,-78.47,2,0.34],['利马',-12.05,-77.04,10,0.42],
['圣地亚哥',-33.45,-70.67,7,0.22],['布宜诺斯艾利斯',-34.60,-58.38,15,0.19],
['圣保罗',-23.55,-46.63,22,0.19],['里约热内卢',-22.91,-43.17,13,0.26],['加拉加斯',10.48,-66.90,5,0.36],
['悉尼',-33.87,151.21,5.3,0.06],['墨尔本',-37.81,144.96,5,0.06],['珀斯',-31.95,115.86,2,0.06],
['奥克兰',-36.85,174.76,1.6,0.06],['惠灵顿',-41.29,174.78,0.4,0.08],
['火奴鲁鲁',21.31,-157.86,1,0.06],['安克雷奇',61.22,-149.90,0.4,0.07],
['阿拉木图',43.24,76.89,2,0.26],['塔什干',41.30,69.24,2.5,0.34]
];
/* =========================================================
2. 画布与投影
========================================================= */
const canvas = document.getElementById('map');
const ctx = canvas.getContext('2d');
let W = 0, H = 0;
let landPath = null;
function project(lon, lat) {
return [(lon + 180) / 360 * W, (90 - lat) / 180 * H];
}
function unproject(x, y) {
return [x / W * 360 - 180, 90 - y / H * 180];
}
function buildLandPath() {
landPath = new Path2D();
for (const poly of LAND) {
for (let i = 0; i < poly.length; i++) {
const [x, y] = project(poly[i][0], poly[i][1]);
i === 0 ? landPath.moveTo(x, y) : landPath.lineTo(x, y);
}
landPath.closePath();
}
}
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth;
H = window.innerHeight;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
buildLandPath();
}
/* =========================================================
3. 工具函数
========================================================= */
function haversine(lat1, lon1, lat2, lon2) {
const R = 6371, p = Math.PI / 180;
const dLat = (lat2 - lat1) * p, dLon = (lon2 - lon1) * p;
const a = Math.sin(dLat / 2) ** 2 +
Math.cos(lat1 * p) * Math.cos(lat2 * p) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.min(1, Math.sqrt(a)));
}
const ROMAN = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII'];
function toRoman(v) {
return ROMAN[Math.max(1, Math.min(12, Math.round(v)))];
}
function hexToRgb(h) {
return [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
}
function lerpColor(a, b, t) {
const A = hexToRgb(a), B = hexToRgb(b);
return `rgb(${Math.round(A[0]+(B[0]-A[0])*t)},${Math.round(A[1]+(B[1]-A[1])*t)},${Math.round(A[2]+(B[2]-A[2])*t)})`;
}
const MMI_STOPS = [
[3, '#4a90d9'], [4, '#5ec8f0'], [5, '#4ee0a0'], [6, '#f5e663'],
[7, '#ffb74d'], [8, '#ff7043'], [9, '#ff3d3d'], [10, '#e11d74'],
[11, '#a855f7'], [12, '#7c3aed']
];
function mmiColor(i) {
if (i <= 3) return MMI_STOPS[0][1];
if (i >= 12) return MMI_STOPS[MMI_STOPS.length - 1][1];
for (let k = 0; k < MMI_STOPS.length - 1; k++) {
if (i >= MMI_STOPS[k][0] && i <= MMI_STOPS[k + 1][0]) {
const t = (i - MMI_STOPS[k][0]) / (MMI_STOPS[k + 1][0] - MMI_STOPS[k][0]);
return lerpColor(MMI_STOPS[k][1], MMI_STOPS[k + 1][1], t);
}
}
return '#ffffff';
}
function fmtNum(n) {
if (n >= 1e8) return (n / 1e8).toFixed(2) + '亿';
if (n >= 1e4) return (n / 1e4).toFixed(1) + '万';
return Math.round(n).toLocaleString('en-US');
}
function pointInPoly(x, y, poly) {
let inside = false;
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
const xi = poly[i][0], yi = poly[i][1], xj = poly[j][0], yj = poly[j][1];
if (((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) inside = !inside;
}
return inside;
}
function isLand(lon, lat) {
for (const poly of LAND) if (pointInPoly(lon, lat, poly)) return true;
return false;
}
/* =========================================================
4. 模拟核心
========================================================= */
const sim = {
lon: 139.69, lat: 35.68,
mag: 7.0, depth: 10,
results: [], total: 0, displayed: 0, waveStart: 0
};
function surfaceRadius(M, I, depth) {
const d = Math.pow(10, (1.5 * M + 2.5 - I) / 3.3);
const r2 = d * d - depth * depth;
return r2 > 0 ? Math.sqrt(r2) : 0;
}
function computeSim() {
const M = sim.mag, depth = sim.depth, eLon = sim.lon, eLat = sim.lat;
const res = [];
let total = 0;
for (const [name, lat, lon, pop, vul] of CITIES) {
const dist = haversine(eLat, eLon, lat, lon);
const cityR = 6 * Math.sqrt(pop);
const de = Math.sqrt(dist * dist + cityR * cityR);
const hyp = Math.sqrt(de * de + depth * depth);
const I = Math.max(0, Math.min(12, 1.5 * M - 3.3 * Math.log10(Math.max(hyp, 5)) + 2.5));
if (I < 3.5) continue;
const frac = Math.min(0.7, 1 - Math.exp(-(I - 3.5) / 2.5));
const mortality = Math.min(0.5, vul * Math.pow(10, (I - 11) / 1.5));
const jitter = 0.72 + Math.random() * 0.56;
const deaths = pop * 1e6 * frac * mortality * jitter;
total += deaths;
res.push({ name, lat, lon, pop, vul, dist, I, deaths });
}
res.sort((a, b) => b.deaths - a.deaths);
sim.results = res;
sim.total = total;
sim.waveStart = performance.now();
renderList();
updateMeta();
}
function nearestName() {
let best = null, bd = Infinity;
for (const c of CITIES) {
const d = haversine(sim.lat, sim.lon, c[1], c[2]);
if (d < bd) { bd = d; best = c; }
}
const ns = sim.lat >= 0 ? 'N' : 'S';
const ew = sim.lon >= 0 ? 'E' : 'W';
return {
pos: `${Math.abs(sim.lat).toFixed(2)}°${ns}, ${Math.abs(sim.lon).toFixed(2)}°${ew}`,
near: best ? `${best[0]} 附近 ${Math.round(bd)} km` : '--'
};
}
function updateMeta() {
const n = nearestName();
document.getElementById('meta').innerHTML =
`震中:<b>${n.pos}</b><br>位置:<b>${n.near}</b><br>` +
`震级 <b>M ${sim.mag.toFixed(1)}</b> · 深度 <b>${sim.depth} km</b>`;
}
/* =========================================================
5. 绘制(更细致的全球地图)
========================================================= */
function drawOcean() {
const g = ctx.createRadialGradient(W * 0.5, H * 0.42, 0, W * 0.5, H * 0.42, Math.max(W, H) * 0.8);
g.addColorStop(0, '#0b1a29');
g.addColorStop(0.6, '#071220');
g.addColorStop(1, '#04080f');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
}
function drawGrid() {
ctx.strokeStyle = 'rgba(94,200,240,0.035)';
ctx.lineWidth = 0.7;
ctx.beginPath();
for (let lon = -180; lon <= 180; lon += 15) {
const x = (lon + 180) / 360 * W;
ctx.moveTo(x, 0); ctx.lineTo(x, H);
}
for (let lat = -75; lat <= 75; lat += 15) {
const y = (90 - lat) / 180 * H;
ctx.moveTo(0, y); ctx.lineTo(W, y);
}
ctx.stroke();
ctx.strokeStyle = 'rgba(94,200,240,0.075)';
ctx.lineWidth = 0.9;
ctx.beginPath();
for (let lon = -180; lon <= 180; lon += 30) {
const x = (lon + 180) / 360 * W;
ctx.moveTo(x, 0); ctx.lineTo(x, H);
}
for (let lat = -60; lat <= 60; lat += 30) {
const y = (90 - lat) / 180 * H;
ctx.moveTo(0, y); ctx.lineTo(W, y);
}
ctx.stroke();
ctx.strokeStyle = 'rgba(94,200,240,0.16)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, H / 2); ctx.lineTo(W, H / 2);
ctx.stroke();
}
function drawLand() {
if (!landPath) return;
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#1c3f57');
g.addColorStop(0.5, '#17364c');
g.addColorStop(1, '#122b40');
ctx.fillStyle = g;
ctx.fill(landPath);
ctx.save();
ctx.shadowColor = 'rgba(94,200,240,0.55)';
ctx.shadowBlur = 5;
ctx.strokeStyle = 'rgba(120,205,250,0.42)';
ctx.lineWidth = 1;
ctx.stroke(landPath);
ctx.restore();
ctx.strokeStyle = 'rgba(150,220,255,0.55)';
ctx.lineWidth = 0.7;
ctx.stroke(landPath);
}
function drawIsochrons(wave) {
const M = sim.mag, depth = sim.depth;
const levels = [4, 5, 6, 7, 8, 9, 10, 11, 12];
const [cx, cy] = project(sim.lon, sim.lat);
const latRad = sim.lat * Math.PI / 180;
const cosL = Math.max(0.16, Math.cos(latRad));
for (let k = 0; k < levels.length; k++) {
const I = levels[k];
const R = surfaceRadius(M, I, depth);
if (R <= 0 || R > 12000) continue;
const appear = Math.max(0, Math.min(1, (wave * 1.15 - k / levels.length * 0.6) * 2.2));
if (appear <= 0.01) continue;
const rx = R / (111.32 * cosL) * (W / 360);
const ry = R / 110.57 * (H / 180);
if (rx > W * 6 || ry > H * 6) continue;
const color = mmiColor(I);
ctx.save();
ctx.beginPath();
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.globalAlpha = appear * 0.055;
ctx.fill();
ctx.globalAlpha = appear * 0.34;
ctx.strokeStyle = color;
ctx.lineWidth = I >= 8 ? 1.6 : 1;
ctx.stroke();
ctx.restore();
}
}
function drawWavefront(wave) {
if (wave >= 0.999) return;
const M = sim.mag, depth = sim.depth;
const maxR = Math.max(300, surfaceRadius(M, 4, depth) * 1.05);
const [cx, cy] = project(sim.lon, sim.lat);
const latRad = sim.lat * Math.PI / 180;
const cosL = Math.max(0.16, Math.cos(latRad));
for (let i = 0; i < 3; i++) {
const p = (wave + i / 3) % 1;
const R = p * maxR;
if (R < 1) continue;
const rx = R / (111.32 * cosL) * (W / 360);
const ry = R / 110.57 * (H / 180);
if (rx > W * 6) continue;
ctx.save();
ctx.beginPath();
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255,120,90,${(1 - p) * 0.42 * (1 - wave * 0.6)})`;
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
}
function drawCityMarkers() {
const shown = sim.results.filter(r => r.I >= 4.2).slice(0, 16);
const labels = new Set(shown.slice(0, 9).map(r => r.name));
for (const r of shown) {
const [x, y] = project(r.lon, r.lat);
const color = mmiColor(r.I);
const rad = 1.8 + Math.log10(r.pop * 12) * 1.9;
const g = ctx.createRadialGradient(x, y, 0, x, y, rad * 3.2);
g.addColorStop(0, color.replace('rgb', 'rgba').replace(')', ',0.5)'));
g.addColorStop(1, color.replace('rgb', 'rgba').replace(')', ',0)'));
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(x, y, rad * 3.2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, rad, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.62)';
ctx.lineWidth = 0.9;
ctx.stroke();
if (labels.has(r.name)) {
ctx.font = '600 10px -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const w = ctx.measureText(r.name).width;
const tx = x + rad + 4, ty = y;
ctx.fillStyle = 'rgba(5,12,20,0.7)';
ctx.beginPath();
ctx.roundRect(tx - 2, ty - 7, w + 7, 14, 3.5);
ctx.fill();
ctx.fillStyle = color;
ctx.fillText(r.name, tx + 1.5, ty + 0.5);
}
}
}
function drawEpicenter(time) {
const [x, y] = project(sim.lon, sim.lat);
const ph = (time % 1600) / 1600;
const pr = 5 + ph * 30;
ctx.beginPath();
ctx.arc(x, y, pr, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255,70,70,${(1 - ph) * 0.85})`;
ctx.lineWidth = 1.8;
ctx.stroke();
const g = ctx.createRadialGradient(x, y, 0, x, y, 22);
g.addColorStop(0, 'rgba(255,80,60,0.85)');
g.addColorStop(0.45, 'rgba(255,50,40,0.3)');
g.addColorStop(1, 'rgba(255,40,30,0)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(x, y, 22, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 4.6, 0, Math.PI * 2);
ctx.fillStyle = '#ff3b30';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.6;
ctx.stroke();
ctx.strokeStyle = 'rgba(255,120,110,0.6)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x - 14, y); ctx.lineTo(x - 8, y);
ctx.moveTo(x + 8, y); ctx.lineTo(x + 14, y);
ctx.moveTo(x, y - 14); ctx.lineTo(x, y - 8);
ctx.moveTo(x, y + 8); ctx.lineTo(x, y + 14);
ctx.stroke();
}
/* =========================================================
6. 主循环
========================================================= */
function frame(now) {
const waveProgress = Math.min(1, (now - sim.waveStart) / 2400);
const wave = 1 - Math.pow(1 - waveProgress, 2.3);
const diff = sim.total - sim.displayed;
if (Math.abs(diff) > 0.5) {
sim.displayed += diff * 0.10;
if (Math.abs(sim.total - sim.displayed) < 1) sim.displayed = sim.total;
updateDeathDisplay();
}
ctx.clearRect(0, 0, W, H);
drawOcean();
drawGrid();
drawLand();
drawIsochrons(wave);
drawWavefront(wave);
drawCityMarkers();
drawEpicenter(now);
requestAnimationFrame(frame);
}
/* =========================================================
7. UI 更新
========================================================= */
const deathsEl = document.getElementById('deaths');
const listEl = document.getElementById('list');
function updateDeathDisplay() {
const n = Math.round(sim.displayed);
deathsEl.textContent = fmtNum(n);
if (n > 100000) { deathsEl.style.color = '#ff2020'; deathsEl.style.textShadow = '0 0 26px rgba(255,30,30,0.8)'; }
else if (n > 10000) { deathsEl.style.color = '#ff4d4d'; deathsEl.style.textShadow = '0 0 22px rgba(255,60,60,0.65)'; }
else if (n > 1000) { deathsEl.style.color = '#ff7a4d'; deathsEl.style.textShadow = '0 0 20px rgba(255,120,70,0.6)'; }
else if (n > 0) { deathsEl.style.color = '#ffab40'; deathsEl.style.textShadow = '0 0 16px rgba(255,170,64,0.5)'; }
else { deathsEl.style.color = '#3f6b8a'; deathsEl.style.textShadow = 'none'; }
}
function renderList() {
const shown = sim.results.filter(r => r.I >= 4.2).slice(0, 16);
if (shown.length === 0) {
listEl.innerHTML = '<div class="empty">🌊 此处附近没有大型城市<br>换个位置试试,或调高震级</div>';
return;
}
listEl.innerHTMLGame Source: 地震模拟器 · Earthquake Simulator
Creator: FlashMaker22
Libraries: none
Complexity: complex (853 lines, 33.2 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: earthquake-simulator-flashmaker22" to link back to the original. Then publish at arcadelab.ai/publish.