Planet-Earth
htmlv1MITنُشر ٧ أكتوبر ٢٠٢٦· ♥ 0 · ⬇ 2 · 💬 0
كوكب الأرض 3D
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#04060f">
<title>الأرض 3D | دقة عالية</title>
<style>
:root{
--bg-0:#03050c; --bg-1:#070d1c;
--accent:#4ea8ff; --accent-2:#6ef2c0; --accent-3:#ffb45e;
--panel:rgba(9,16,32,.68); --panel-solid:#0a1224;
--border:rgba(110,170,255,.20); --border-strong:rgba(110,170,255,.45);
--text:#e9f2ff; --muted:#8ba3c7; --danger:#ff6b6b;
--radius:16px; --shadow:0 18px 50px rgba(0,0,0,.55);
}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{
width:100%;height:100%;overflow:hidden;
background:radial-gradient(ellipse at 50% 40%, #0a1430 0%, var(--bg-1) 45%, var(--bg-0) 100%);
color:var(--text);
font-family:"Segoe UI","Tahoma",system-ui,-apple-system,sans-serif;
font-size:15px; user-select:none;
}
#app{position:fixed;inset:0;overflow:hidden}
#app canvas{display:block;width:100%;height:100%;outline:none;cursor:grab}
#app canvas:active{cursor:grabbing}
#glowBg{
position:fixed;inset:0;pointer-events:none;z-index:0;
background:
radial-gradient(circle at 22% 18%, rgba(78,168,255,.13), transparent 42%),
radial-gradient(circle at 78% 82%, rgba(110,242,192,.08), transparent 46%);
mix-blend-mode:screen;
}
#loader{
position:fixed;inset:0;z-index:100;
display:flex;align-items:center;justify-content:center;
background:radial-gradient(ellipse at center,#08101f 0%,#03050c 100%);
transition:opacity .7s ease, visibility .7s ease;
}
#loader.hidden{opacity:0;visibility:hidden;pointer-events:none}
.loader-inner{
display:flex;flex-direction:column;align-items:center;gap:16px;
padding:0 24px;text-align:center;max-width:520px;
}
.planet-loader{
width:96px;height:96px;border-radius:50%;
background:radial-gradient(circle at 32% 30%,#7ec8ff 0%,#2a6fd6 35%,#0d2a63 70%,#05132e 100%);
box-shadow:0 0 42px rgba(78,168,255,.55), inset -14px -10px 34px rgba(0,0,0,.75);
position:relative; animation:spinPlanet 3.4s linear infinite;
}
.planet-loader::after{
content:"";position:absolute;inset:-14px;border-radius:50%;
border:1px solid rgba(110,180,255,.35);
border-top-color:rgba(110,242,192,.85);
animation:spinRing 1.6s linear infinite;
}
@keyframes spinPlanet{from{transform:rotate(0)}to{transform:rotate(360deg)}}
@keyframes spinRing{from{transform:rotate(0)}to{transform:rotate(-360deg)}}
.loader-inner h1{font-size:20px;font-weight:600;letter-spacing:.4px}
#loadStatus{font-size:13px;color:var(--muted);min-height:18px}
.bar{
width:300px;height:5px;border-radius:99px;
background:rgba(120,170,255,.14);overflow:hidden;position:relative;
}
.bar i{
display:block;height:100%;width:0%;border-radius:99px;
background:linear-gradient(90deg,var(--accent),var(--accent-2));
transition:width .25s ease; box-shadow:0 0 14px rgba(78,168,255,.85);
}
#loadPct{font-size:12px;color:var(--muted);letter-spacing:1px;font-variant-numeric:tabular-nums}
#topbar{
position:fixed;top:0;left:0;right:0;z-index:20;
display:flex;align-items:center;justify-content:space-between;
padding:14px 20px;pointer-events:none;
}
.brand{
display:flex;align-items:center;gap:12px;
background:var(--panel);backdrop-filter:blur(14px);
border:1px solid var(--border);border-radius:var(--radius);
padding:10px 16px;box-shadow:var(--shadow);pointer-events:auto;
}
.brand .dot{
width:10px;height:10px;border-radius:50%;
background:var(--accent-2); box-shadow:0 0 12px var(--accent-2);
animation:pulseDot 2.2s ease-in-out infinite;
}
@keyframes pulseDot{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.8)}}
.brand strong{display:block;font-size:14px;font-weight:600;line-height:1.25}
.brand small{display:block;font-size:11px;color:var(--muted);line-height:1.3}
.stats{display:flex;gap:8px;pointer-events:auto}
.stats span{
display:flex;align-items:center;gap:6px;
background:var(--panel);backdrop-filter:blur(14px);
border:1px solid var(--border);border-radius:12px;
padding:8px 13px;font-size:11px;color:var(--muted);letter-spacing:.3px;
}
.stats b{
color:var(--accent);font-weight:600;font-size:13px;
font-variant-numeric:tabular-nums;min-width:26px;text-align:center;
}
.stats span:last-child b{color:var(--accent-2)}
#panel{
position:fixed;top:88px;right:20px;z-index:30;
width:290px;max-height:calc(100vh - 130px);
background:var(--panel);backdrop-filter:blur(16px);
border:1px solid var(--border);border-radius:var(--radius);
box-shadow:var(--shadow);
display:flex;flex-direction:column;overflow:hidden;
transition:transform .38s cubic-bezier(.4,0,.2,1), opacity .3s;
}
#panel.collapsed{transform:translateX(calc(100% + 34px));opacity:0;pointer-events:none}
.panel-head{
display:flex;align-items:center;justify-content:space-between;
padding:14px 16px;border-bottom:1px solid var(--border);
background:rgba(255,255,255,.02);
}
.panel-head h2{font-size:13px;font-weight:600;letter-spacing:.4px;display:flex;align-items:center;gap:8px}
.panel-head h2::before{
content:"";width:3px;height:14px;border-radius:2px;
background:linear-gradient(180deg,var(--accent),var(--accent-2));
}
.panel-body{padding:8px 16px 18px;overflow-y:auto;flex:1}
.panel-body::-webkit-scrollbar{width:5px}
.panel-body::-webkit-scrollbar-thumb{background:rgba(110,170,255,.3);border-radius:9px}
.panel-body::-webkit-scrollbar-track{background:transparent}
.group{margin-top:14px}
.group-title{
font-size:10.5px;text-transform:uppercase;letter-spacing:1.6px;
color:var(--muted);margin-bottom:10px;display:flex;align-items:center;gap:8px;
}
.group-title::after{content:"";flex:1;height:1px;background:var(--border)}
.row{margin-bottom:14px}
.row-top{
display:flex;justify-content:space-between;align-items:center;
margin-bottom:7px;font-size:12.5px;color:#c6d8f2;
}
.row-top b{
color:var(--accent);font-weight:600;font-size:11.5px;
font-variant-numeric:tabular-nums;
background:rgba(78,168,255,.10);
padding:2px 8px;border-radius:7px;border:1px solid rgba(78,168,255,.18);
}
input[type=range]{
-webkit-appearance:none;appearance:none;
width:100%;height:4px;border-radius:99px;
background:rgba(120,170,255,.16);outline:none;cursor:pointer;
}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;appearance:none;
width:15px;height:15px;border-radius:50%;
background:linear-gradient(145deg,#8fd0ff,var(--accent));
border:2px solid #0a1830;
box-shadow:0 0 10px rgba(78,168,255,.85);
transition:transform .16s ease;
}
input[type=range]::-webkit-slider-thumb:hover{transform:scale(1.22)}
input[type=range]::-moz-range-thumb{
width:14px;height:14px;border-radius:50%;
background:linear-gradient(145deg,#8fd0ff,var(--accent));
border:2px solid #0a1830;box-shadow:0 0 10px rgba(78,168,255,.85);cursor:pointer;
}
.toggle{
display:flex;align-items:center;justify-content:space-between;
padding:9px 2px;font-size:13px;color:#c6d8f2;cursor:pointer;
border-bottom:1px solid rgba(110,170,255,.07);
}
.toggle:last-child{border-bottom:none}
.toggle:hover{color:#fff}
.switch{
position:relative;width:40px;height:22px;flex-shrink:0;
border-radius:99px;background:rgba(120,170,255,.16);
border:1px solid rgba(110,170,255,.20);
transition:background .25s, border-color .25s;
}
.switch::after{
content:"";position:absolute;top:2px;left:2px;
width:16px;height:16px;border-radius:50%;
background:#8ba3c7;
transition:transform .25s cubic-bezier(.4,0,.2,1), background .25s;
}
.toggle input{display:none}
.toggle input:checked + .switch{background:rgba(78,168,255,.32);border-color:var(--border-strong)}
.toggle input:checked + .switch::after{
transform:translateX(18px);
background:linear-gradient(145deg,#8fd0ff,var(--accent-2));
box-shadow:0 0 10px rgba(110,242,192,.8);
}
html[dir="rtl"] .switch::after{left:auto;right:2px}
html[dir="rtl"] .toggle input:checked + .switch::after{transform:translateX(-18px)}
.btns{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:6px}
.btn{
display:flex;align-items:center;justify-content:center;gap:6px;
padding:10px 8px;font-size:12px;font-family:inherit;font-weight:500;
color:#cfe1fa;background:rgba(110,170,255,.08);
border:1px solid var(--border);border-radius:11px;
cursor:pointer;transition:all .2s ease;
}
.btn:hover{background:rgba(78,168,255,.20);border-color:var(--border-strong);color:#fff;transform:translateY(-1px)}
.btn:active{transform:translateY(0) scale(.98)}
.btn.primary{
background:linear-gradient(135deg,rgba(78,168,255,.28),rgba(110,242,192,.20));
border-color:var(--border-strong);color:#fff;
}
.btn.wide{grid-column:1 / -1}
.btn:disabled{opacity:.55;cursor:wait}
#panelToggle{
position:fixed;top:88px;right:20px;z-index:29;
width:44px;height:44px;border-radius:13px;
background:var(--panel);backdrop-filter:blur(14px);
border:1px solid var(--border);box-shadow:var(--shadow);
display:none;align-items:center;justify-content:center;
cursor:pointer;color:var(--accent);font-size:18px;
transition:transform .2s;
}
#panelToggle:hover{transform:scale(1.06)}
#panelToggle.show{display:flex}
#hint{
position:fixed;bottom:18px;left:50%;transform:translateX(-50%);
z-index:20;pointer-events:none;
background:var(--panel);backdrop-filter:blur(14px);
border:1px solid var(--border);border-radius:99px;
padding:9px 20px;font-size:12px;color:var(--muted);
white-space:nowrap;box-shadow:var(--shadow);transition:opacity .5s;
}
#hint b{color:var(--accent);font-weight:500}
#hint.fade{opacity:0}
#toast{
position:fixed;bottom:76px;left:50%;transform:translate(-50%,18px);
z-index:60;pointer-events:none;opacity:0;
background:rgba(10,20,40,.92);backdrop-filter:blur(14px);
border:1px solid var(--border-strong);border-radius:12px;
padding:10px 20px;font-size:13px;color:var(--text);
box-shadow:var(--shadow);transition:opacity .3s, transform .3s;
}
#toast.show{opacity:1;transform:translate(-50%,0)}
@media (max-width: 860px){
#panel{top:auto;bottom:0;right:0;left:0;width:100%;max-height:60vh;border-radius:20px 20px 0 0;border-bottom:none}
#panel.collapsed{transform:translateY(calc(100% + 20px))}
#panelToggle{top:auto;bottom:20px;right:20px;display:flex}
#panelToggle.show{display:flex}
#hint{display:none}
.brand small{display:none}
}
@media (max-width: 460px){
.brand{padding:8px 12px}
.brand strong{font-size:12.5px}
.stats span{font-size:10px;padding:6px 9px}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="glowBg"></div>
<div id="loader">
<div class="loader-inner">
<div class="planet-loader"></div>
<h1>جارٍ بناء كوكب الأرض بدقة عالية…</h1>
<p id="loadStatus">تهيئة المشهد</p>
<div class="bar"><i id="loadBar"></i></div>
<span id="loadPct">0%</span>
</div>
</div>
<header id="topbar">
<div class="brand">
<span class="dot"></span>
<div>
<strong>الأرض ثلاثية الأبعاد</strong>
<small>محاكاة عالية الدقة · Three.js</small>
</div>
</div>
<div class="stats">
<span>FPS <b id="fps">--</b></span>
<span>مسافة <b id="dist">0</b></span>
</div>
</header>
<div id="panelToggle" title="إظهار لوحة التحكم">⚙</div>
<aside id="panel">
<div class="panel-head">
<h2>لوحة التحكم</h2>
<div class="btn" id="collapseBtn" style="padding:5px 11px;font-size:14px;border-radius:9px">✕</div>
</div>
<div class="panel-body">
<div class="group">
<div class="group-title">الحركة والدوران</div>
<div class="row">
<div class="row-top"><span>سرعة دوران الأرض</span><b id="vSpin">1.0</b></div>
<input type="range" id="spin" min="0" max="5" step="0.1" value="1">
</div>
<div class="row">
<div class="row-top"><span>سرعة السحاب</span><b id="vCloud">1.0</b></div>
<input type="range" id="cloud" min="0" max="4" step="0.1" value="1">
</div>
<div class="row">
<div class="row-top"><span>سرعة دوران الكاميرا</span><b id="vAuto">0.0</b></div>
<input type="range" id="auto" min="0" max="3" step="0.1" value="0">
</div>
<div class="row">
<div class="row-top"><span>سرعة القمر</span><b id="vMoon">1.0</b></div>
<input type="range" id="moonSpeed" min="0" max="4" step="0.1" value="1">
</div>
</div>
<div class="group">
<div class="group-title">عناصر المشهد</div>
<label class="toggle"><span>الغلاف الجوي (الوهج)</span><input type="checkbox" id="tAtmo" checked><i class="switch"></i></label>
<label class="toggle"><span>طبقة السحاب</span><input type="checkbox" id="tClouds" checked><i class="switch"></i></label>
<label class="toggle"><span>النجوم</span><input type="checkbox" id="tStars" checked><i class="switch"></i></label>
<label class="toggle"><span>القمر</span><input type="checkbox" id="tMoon" checked><i class="switch"></i></label>
<label class="toggle"><span>الشمس (النجم البعيد)</span><input type="checkbox" id="tSun" checked><i class="switch"></i></label>
<label class="toggle"><span>الخطوط الشبكية</span><input type="checkbox" id="tGrid"><i class="switch"></i></label>
</div>
<div class="group">
<div class="group-title">الإضاءة والمؤثرات</div>
<div class="row">
<div class="row-top"><span>قوة إضاءة الشمس</span><b id="vLight">1.6</b></div>
<input type="range" id="light" min="0" max="4" step="0.1" value="1.6">
</div>
<div class="row">
<div class="row-top"><span>الإضاءة المحيطة</span><b id="vAmb">0.18</b></div>
<input type="range" id="ambient" min="0" max="1" step="0.02" value="0.18">
</div>
<div class="row">
<div class="row-top"><span>لمعان المحيطات</span><b id="vShine">55</b></div>
<input type="range" id="shine" min="0" max="140" step="1" value="55">
</div>
<div class="row">
<div class="row-top"><span>كثافة الغلاف الجوي</span><b id="vGlow">1.0</b></div>
<input type="range" id="glow" min="0" max="3" step="0.05" value="1">
</div>
<div class="row">
<div class="row-top"><span>خشونة التضاريس (Bump)</span><b id="vBump">0.028</b></div>
<input type="range" id="bump" min="0" max="0.09" step="0.002" value="0.028">
</div>
</div>
<div class="group">
<div class="group-title">أدوات</div>
<div class="btns">
<button class="btn" id="btnReset">↺ إعادة الضبط</button>
<button class="btn" id="btnRandom">🎲 كوكب جديد</button>
<button class="btn wide primary" id="btnShot">📸 حفظ صورة PNG</button>
<button class="btn wide" id="btnFull">⛶ ملء الشاشة</button>
</div>
</div>
</div>
</aside>
<div id="hint">
<b>اسحب</b> للدوران · <b>عجلة الفأرة</b> للتقريب · <b>نقرة مزدوجة</b> لإعادة الضبط
</div>
<div id="toast"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
"use strict";
/* ================================================================
0) إعدادات الدقة العالية — يمكن تعديلها حسب جهازك
================================================================ */
const QUALITY = {
/* خرائط الأرض — رفعنا من 1024×512 إلى 3072×1536 (9 أضعاف البكسلات) */
EARTH_W: 3072,
EARTH_H: 1536,
/* خرائط السحاب — رفعنا من 1024×512 إلى 2048×1024 */
CLOUD_W: 2048,
CLOUD_H: 1024,
/* خريطة القمر — رفعنا من 512×256 إلى 2048×1024 */
MOON_W: 2048,
MOON_H: 1024,
/* شرائح الأجسام الكروية (Segments) */
EARTH_SEG_W: 256,
EARTH_SEG_H: 160,
CLOUD_SEG_W: 192,
CLOUD_SEG_H: 112,
ATMO_SEG_W: 128,
ATMO_SEG_H: 96,
MOON_SEG_W: 128,
MOON_SEG_H: 80,
/* عدد طبقات fBm للتضاريس (5 سابقاً → 7 الآن) */
FBM_OCT: 7,
/* أقصى نسبة بكسلات للشاشة (كانت 2) */
MAX_PIXEL_RATIO: 3,
/* أنيزوتروبي (كان 16 افتراضيًا) */
ANISOTROPY: 16,
/* عدد النجوم */
STARS: 14000
};
/* ================================================================
1) أدوات مساعدة
================================================================ */
const $ = (s) => document.querySelector(s);
const clamp = (v,a,b) => v<a?a:v>b?b:v;
const lerp = (a,b,t) => a+(b-a)*t;
const smoothstep = (e0,e1,x) => {
const t = clamp((x-e0)/(e1-e0),0,1);
return t*t*(3-2*t);
};
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const nextFrame = () => new Promise(r => requestAnimationFrame(() => r()));
const rand = (a,b) => a + Math.random()*(b-a);
let toastTimer = null;
function toast(msg) {
const el = $("#toast");
el.textContent = msg;
el.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove("show"), 1800);
}
/* ================================================================
2) ضوضاء محسّنة (Value Noise 3D + fBm)
================================================================ */
function hash3(i,j,k) {
let n = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(k, 1274126177);
n = Math.imul(n ^ (n >>> 13), 1274126177);
n ^= n >>> 16;
return (n >>> 0) / 4294967295;
}
function noise3(x,y,z) {
const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z);
const xf = x - xi, yf = y - yi, zf = z - zi;
const u = xf*xf*(3-2*xf), v = yf*yf*(3-2*yf), w = zf*zf*(3-2*zf);
const n000 = hash3(xi, yi, zi);
const n100 = hash3(xi+1, yi, zi);
const n010 = hash3(xi, yi+1, zi);
const n110 = hash3(xi+1, yi+1, zi);
const n001 = hash3(xi, yi, zi+1);
const n101 = hash3(xi+1, yi, zi+1);
const n011 = hash3(xi, yi+1, zi+1);
const n111 = hash3(xi+1, yi+1, zi+1);
const x00 = n000 + (n100-n000)*u;
const x10 = n010 + (n110-n010)*u;
const x01 = n001 + (n101-n001)*u;
const x11 = n011 + (n111-n011)*u;
const y0 = x00 + (x10-x00)*v;
const y1 = x01 + (x11-x01)*v;
return y0 + (y1-y0)*w;
}
function fbm(x,y,z, oct, lac, gain) {
oct = oct || QUALITY.FBM_OCT;
lac = lac || 2.0;
gain = gain || 0.5;
let amp = 0.5, freq = 1, sum = 0, norm = 0;
for (let i = 0; i < oct; i++) {
sum += amp * noise3(x*freq, y*freq, z*freq);
norm += amp;
amp *= gain;
freq *= lac;
}
return sum / norm;
}
/* ================================================================
3) خرائط الكوكب — دقة عالية
================================================================ */
const planetCfg = {
seed: Math.random()*500,
seaLevel: 0.085,
landScale: 1.35,
roughness: 1.0,
iceEdge: 0.74,
cloudCover: 0.50
};
async function buildEarthMaps(W,H,onProgress) {
const colorCv = document.createElement("canvas");
colorCv.width = W; colorCv.height = H;
const cctx = colorCv.getContext("2d");
const cImg = cctx.createImageData(W,H);
const cpx = cImg.data;
const heightCv = document.createElement("canvas");
heightCv.width = W; heightCv.height = H;
const hctx = heightCv.getContext("2d");
const hImg = hctx.createImageData(W,H);
const hpx = hImg.data;
const specCv = document.createElement("canvas");
specCv.width = W; specCv.height = H;
const sctx = specCv.getContext("2d");
const sImg = sctx.createImageData(W,H);
const spx = sImg.data;
const S = planetCfg.seed;
const latStep = Math.PI / H;
const reportEvery = Math.max(1, Math.floor(H / 60));
for (let y = 0; y < H; y++) {
const lat = Math.PI/2 - (y+0.5)*latStep;
const cosLat = Math.cos(lat);
const sinLat = Math.sin(lat);
const latAbs = Math.abs(lat) / (Math.PI/2);
for (let x = 0; x < W; x++) {
const lon = ((x+0.5)/W)*Math.PI*2 - Math.PI;
const nx = cosLat*Math.cos(lon);
const ny = sinLat;
const nz = cosLat*Math.sin(lon);
/* تشويه المجال */
const wx = noise3(nx*2.1+S+51.3, ny*2.1+S+37.7, nz*2.1+S+11.1) - 0.5;
const wy = noise3(nx*2.1+S+91.3, ny*2.1+S+77.7, nz*2.1+S+31.1) - 0.5;
const wz = noise3(nx*2.1+S+19.3, ny*2.1+S+23.7, nz*2.1+S+67.1) - 0.5;
const px = nx + wx*0.30;
const py = ny + wy*0.30;
const pz = nz + wz*0.30;
/* قارات كبرى */
const continent = fbm(
px*0.85+S+3.3,
py*0.85+S+7.7,
pz*0.85+S+1.1,
3, 2.0, 0.5
);
/* تضاريس أساسية (7 طبقات fBm) */
const base = fbm(
px*planetCfg.landScale+S+21.7,
py*planetCfg.landScale+S+13.1,
pz*planetCfg.landScale+S+5.9,
QUALITY.FBM_OCT, 2.0, 0.5
);
/* طبقة تفاصيل دقيقة جداً (micro-detail) */
const micro = fbm(
px*7.5 + S + 301,
py*7.5 + S + 401,
pz*7.5 + S + 501,
4, 2.2, 0.55
);
let e = (base - 0.5) * 2.4 * planetCfg.roughness;
e += (continent - 0.5) * 0.95;
e += (micro - 0.5) * 0.16; /* إضافة نتوءات دقيقة */
const sea = planetCfg.seaLevel;
const det = noise3(nx*40+S, ny*40+S, nz*40+S);
let r,g,b,spec;
if (e < sea) {
/* محيط */
const depth = clamp((sea-e)/0.75, 0, 1);
const d2 = depth*depth;
r = lerp(28, 2, d2);
g = lerp(98, 14, d2);
b = lerp(166, 56, d2);
const tint = (det-0.5)*14;
r += tint*0.4; g += tint*0.7; b += tint;
spec = 245;
} else {
/* يابسة */
const h = clamp((e-sea)/0.62, 0, 1);
if (h < 0.055) {
const t = h/0.055;
r = lerp(220, 172, t);
g = lerp(204, 172, t);
b = lerp(152, 120, t);
} else if (h < 0.26) {
const t = (h-0.055)/0.205;
r = lerp(88, 58, t);
g = lerp(136, 106, t);
b = lerp(62, 54, t);
} else if (h < 0.55) {
const t = (h-0.26)/0.29;
r = lerp(58, 114, t);
g = lerp(106, 100, t);
b = lerp(54, 70, t);
} else {
const t = clamp((h-0.55)/0.45, 0, 1);
r = lerp(114, 162, t);
g = lerp(100, 150, t);
b = lerp(70, 134, t);
}
/* ثلج على القمم */
const snow = smoothstep(0.70, 0.90, h);
r = lerp(r, 246, snow);
g = lerp(g, 250, snow);
b = lerp(b, 255, snow);
/* صحارى */
const dryness = fbm(px*3.2+S+71, py*3.2+S+17, pz*3.2+S+43, 4, 2, 0.5);
const desert = smoothstep(0.58, 0.78, dryness) * (1-snow) * (1-smoothstep(0.35,0.6,h));
r = lerp(r, 198, desert*0.78);
g = lerp(g, 168, desert*0.78);
b = lerp(b, 106, desert*0.78);
/* تفاصيل لونية دقيقة (من micro) */
const tint = (det-0.5)*22;
r += tint; g += tint*0.95; b += tint*0.85;
/* ظل الجبال */
const sh = smoothstep(0.55, 0.95, h) * (base - 0.5) * 40;
r += sh; g += sh; b += sh;
spec = 20;
}
/* جليد قطبي */
const iceNoise = noise3(nx*5.5+S, ny*5.5+S, nz*5.5+S);
const iceStart = planetCfg.iceEdge + (iceNoise-0.5)*0.16;
const ice = smoothstep(iceStart, iceStart+0.15, latAbs);
if (ice > 0) {
r = lerp(r, 240, ice);
g = lerp(g, 248, ice);
b = lerp(b, 255, ice);
spec = lerp(spec, 80, ice);
}
const idx = (y*W + x)*4;
cpx[idx] = clamp(r,0,255);
cpx[idx+1] = clamp(g,0,255);
cpx[idx+2] = clamp(b,0,255);
cpx[idx+3] = 255;
/* ارتفاع — نحفظ حتى التفاصيل الدقيقة */
const hVal = clamp((e+0.6)/1.6, 0, 1);
const hByte = Math.round(hVal*255);
hpx[idx] = hpx[idx+1] = hpx[idx+2] = hByte;
hpx[idx+3] = 255;
const sByte = Math.round(clamp(spec, 0, 255));
spx[idx] = spx[idx+1] = spx[idx+2] = sByte;
spx[idx+3] = 255;
}
if (y % reportEvery === 0) {
if (onProgress) onProgress(y/H);
await nextFrame();
}
}
cctx.putImageData(cImg, 0, 0);
hctx.putImageData(hImg, 0, 0);
sctx.putImageData(sImg, 0, 0);
return { colorCv, heightCv, specCv };
}
async function buildCloudMap(W,H,onProgress) {
const cv = document.createElement("canvas");
cv.width = W; cv.height = H;
const ctx = cv.getContext("2d");
const img = ctx.createImageData(W,H);
const px = img.data;
const S = planetCfg.seed + 137;
const latStep = Math.PI / H;
const reportEvery = Math.max(1, Math.floor(H/40));
for (let y = 0; y < H; y++) {
const lat = Math.PI/2 - (y+0.5)*latStep;
const cosLat = Math.cos(lat);
const sinLat = Math.sin(lat);
for (let x = 0; x < W; x++) {
const lon = ((x+0.5)/W)*Math.PI*2 - Math.PI;
const nx = cosLat*Math.cos(lon);
const ny = sinLat;
const nz = cosLat*Math.sin(lon);
const wx = noise3(nx*3.4+S, ny*3.4+S, nz*3.4+S) - 0.5;
const wz = noise3(nx*3.4+S+61, ny*3.4+S+61, nz*3.4+S+61) - 0.5;
/* 6 طبقات للتفاصيل الغنية */
let c = fbm(
nx*2.6 + wx*0.5 + S,
ny*2.6 + S,
nz*2.6 + wz*0.5 + S,
6, 2.15, 0.55
);
/* تفاصيل سحاب دقيقة */
const micro = fbm(nx*9 + S + 91, ny*9 + S + 91, nz*9 + S + 91, 3, 2.3, 0.55);
c += (micro - 0.5) * 0.09;
c += 0.055 * Math.sin(ny*Math.PI*5.5);
c += 0.035 * Math.sin(ny*Math.PI*11.0);
c -= Math.abs(ny) * 0.05;
const th = planetCfg.cloudCover;
let a = smoothstep(th, th+0.22, c);
a = clamp(a,0,1);
a = a*a*(3-2*a);
const idx = (y*W+x)*4;
px[idx] = 255;
px[idx+1] = 255;
px[idx+2] = 255;
px[idx+3] = Math.round(a*255);
}
if (y % reportEvery === 0) {
if (onProgress) onProgress(y/H);
await nextFrame();
}
}
ctx.putImageData(img, 0, 0);
return cv;
}
function buildMoonMap(W,H) {
const cv = document.createElement("canvas");
cv.width = W; cv.height = H;
const ctx = cv.getContext("2d");
const img = ctx.createImageData(W,H);
const px = img.data;
const S = planetCfg.seed + 909;
const latStep = Math.PI / H;
for (let y = 0; y < H; y++) {
const lat = Math.PI/2 - (y+0.5)*latStep;
const cosLat = Math.cos(lat);
const sinLat = Math.sin(lat);
for (let x = 0; x < W; x++) {
const lon = ((x+0.5)/W)*Math.PI*2 - Math.PI;
const nx = cosLat*Math.cos(lon);
const ny = sinLat;
const nz = cosLat*Math.sin(lon);
/* طبقات أكثر لتفاصيل الفوهات */
const base = fbm(nx*3.2+S, ny*3.2+S, nz*3.2+S, 5, 2.1, 0.55);
const craters = fbm(nx*13+S, ny*13+S, nz*13+S, 4, 2.3, 0.6);
const micro = fbm(nx*32+S, ny*32+S, nz*32+S, 3, 2.4, 0.6);
let v = 118 + (base-0.5)*82;
v += (craters-0.5)*44;
v += (micro-0.5)*14;
v = clamp(v, 40, 232);
/* بقع داكنة (ماريا) */
const maria = smoothstep(0.55, 0.72, base);
v = lerp(v, 60, maria*0.78);
/* فوهات دقيقة داكنة */
const craterDot = smoothstep(0.72, 0.82, craters);
v = lerp(v, 82, craterDot*0.5);
const idx = (y*W+x)*4;
px[idx] = v;
px[idx+1] = v*0.99;
px[idx+2] = v*0.95;
px[idx+3] = 255;
}
}
ctx.putImageData(img, 0, 0);
return cv;
}
function buildGlowTexture(size) {
const cv = document.createElement("canvas");
cv.width = cv.height = size;
const ctx = cv.getContext("2d");
const g = ctx.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
g.addColorStop(0.00, "rgba(255,255,255,1)");
g.addColorStop(0.12, "rgba(255,246,214,0.95)");
g.addColorStop(0.30, "rgba(255,206,120,0.42)");
g.addColorStop(0.60, "rgba(255,168,72,0.12)");
g.addColorStop(1.00, "rgba(255,140,40,0)");
ctx.fillStyle = g;
ctx.fillRect(0, 0, size, size);
return cv;
}
/* بقعة دائرية ناعمة للنجوم عالية الدقة */
function buildStarSprite(size) {
const cv = document.createElement("canvas");
cv.width = cv.height = size;
const ctx = cv.getContext("2d");
const g = ctx.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
g.addColorStop(0.00, "rgba(255,255,255,1)");
g.addColorStop(0.25, "rgba(255,255,255,0.75)");
g.addColorStop(0.55, "rgba(255,255,255,0.16)");
g.addColorStop(1.00, "rgba(255,255,255,0)");
ctx.fillStyle = g;
ctx.fillRect(0, 0, size, size);
return cv;
}
/* ================================================================
4) إعداد المشهد
================================================================ */
let renderer, scene, camera;
let earthGroup, earthMesh, cloudMesh, atmosphereMesh, moonPivot, moonMesh;
let starField, sunSprite, sunLight, ambientLight, hemiLight, gridHelper;
const cam = {
theta: 0.75, phi: 1.32, radius: 3.55,
tTheta: 0.75, tPhi: 1.32, tRadius: 3.55,
vTheta: 0, vPhi: 0,
dragging: false, lastPinch: 0
};
const pointers = new Map();
const motion = { spin: 1.0, cloud: 1.0, auto: 0.0, moon: 1.0 };
const EARTH_RADIUS = 1;
function setupRenderer() {
const container = $("#app");
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
preserveDrawingBuffer: true,
powerPreference: "high-performance"
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, QUALITY.MAX_PIXEL_RATIO));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;
renderer.setClearColor(0x03050c, 1);
container.appendChild(renderer.domElement);
}
function setupScene() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 0.05, 4000);
camera.position.set(0, 0, 3.55);
ambientLight = new THREE.AmbientLight(0x3a5a8c, 0.18);
scene.add(ambientLight);
hemiLight = new THREE.HemisphereLight(0x8fc4ff, 0x0a1428, 0.22);
scene.add(hemiLight);
sunLight = new THREE.DirectionalLight(0xfff3dd, 1.6);
sunLight.position.set(6, 2.2, 4.5);
scene.add(sunLight);
const rim = new THREE.DirectionalLight(0x2a5fa8, 0.22);
rim.position.set(-6, -2, -5);
scene.add(rim);
}
function buildStars() {
const COUNT = QUALITY.STARS;
const geo = new THREE.BufferGeometry();
const pos = new Float32Array(COUNT*3);
const col = new Float32Array(COUNT*3);
const palette = [
[1.00,1.00,1.00],
[0.78,0.86,1.00],
[1.00,0.93,0.78],
[1.00,0.80,0.68],
[0.86,0.94,1.00]
];
for (let i = 0; i < COUNT; i++) {
const u = Math.random()*2 - 1;
const t = Math.random()*Math.PI*2;
const s = Math.sqrt(1-u*u);
const R = rand(300, 900);
pos[i*3] = Math.cos(t)*s*R;
pos[i*3+1] = u*R;
pos[i*3+2] = Math.sin(t)*s*R;
const c = palette[(Math.random()*palette.length)|0];
const bright = rand(0.45, 1.0);
col[i*3] = c[0]*bright;
col[i*3+1] = c[1]*bright;
col[i*3+2] = c[2]*bright;
}
geo.setAttribute("position", new THREE.BufferAttribute(pos, 3));
geo.setAttribute("color", new THREE.BufferAttribute(col, 3));
const starTex = new THREE.CanvasTexture(buildStarSprite(64));
starTex.encoding = THREE.sRGBEncoding;
const mat = new THREE.PointsMaterial({
size: 2.0,
vertexColors: true,
transparent: true,
opacity: 0.95,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
map: starTex
});
starField = new THREE.Points(geo, mat);
scene.add(starField);
return starField;
}
function buildSunSprite() {
const tex = new THREE.CanvasTexture(buildGlowTexture(512));
tex.encoding = THREE.sRGBEncoding;
const mat = new THREE.SpriteMaterial({
map: tex, color: 0xffffff, transparent: true,
blending: THREE.AdditiveBlending, depthWrite: false, depthTest: false,
opacity: 0.9
});
sunSprite = new THREE.Sprite(mat);
const dir = sunLight.position.clone().normalize();
sunSprite.position.copy(dir.multiplyScalar(320));
sunSprite.scale.setScalar(82);
scene.add(sunSprite);
return sunSprite;
}
const ATMO_VERT = `
varying vec3 vViewNormal;
varying vec3 vWorldNormal;
void main() {
vViewNormal = normalize(normalMatrix * normal);
vWorldNormal = normalize(mat3(modelMatrix) * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const ATMO_FRAG = `
uniform vec3 uColor;
uniform vec3 uLightDir;
uniform float uStrength;
varying vec3 vViewNormal;
varying vec3 vWorldNormal;
void main() {
float rim = pow(max(0.62 - dot(vViewNormal, vec3(0.0,0.0,1.0)), 0.0), 3.0);
rim = clamp(rim, 0.0, 1.0);
float lit = smoothstep(-0.42, 0.42, dot(normalize(vWorldNormal), normalize(uLightDir)));
float alpha = rim * mix(0.10, 1.0, lit) * uStrength;
gl_FragColor = vec4(uColor, alpha);
}
`;
function buildAtmosphere() {
const geo = new THREE.SphereGeometry(
EARTH_RADIUS * 1.185,
QUALITY.ATMO_SEG_W, QUALITY.ATMO_SEG_H
);
const mat = new THREE.ShaderMaterial({
uniforms: {
uColor: { value: new THREE.Color(0x5aa9ff) },
uLightDir: { value: sunLight.position.clone().normalize() },
uStrength: { value: 1.0 }
},
vertexShader: ATMO_VERT,
fragmentShader: ATMO_FRAG,
side: THREE.BackSide,
blending: THREE.AdditiveBlending,
transparent: true,
depthWrite: false
});
atmosphereMesh = new THREE.Mesh(geo, mat);
scene.add(atmosphereMesh);
return atmosphereMesh;
}
function buildGrid() {
gridHelper = new THREE.Group();
const g1 = new THREE.SphereGeometry(EARTH_RADIUS * 1.002, 48, 24);
const m1 = new THREE.MeshBasicMaterial({
color: 0x3f8ee0, wireframe: true, transparent: true,
opacity: 0.13, depthWrite: false
});
gridHelper.add(new THREE.Mesh(g1, m1));
const ringGeo = new THREE.RingGeometry(EARTH_RADIUS*1.006, EARTH_RADIUS*1.008, 192);
const ringMat = new THREE.MeshBasicMaterial({
color: 0x6ef2c0, transparent: true, opacity: 0.24,
side: THREE.DoubleSide, depthWrite: false
});
const eq = new THREE.Mesh(ringGeo, ringMat);
eq.rotation.x = Math.PI/2;
gridHelper.add(eq);
gridHelper.visible = false;
scene.add(gridHelper);
return gridHelper;
}
function buildMoon() {
moonPivot = new THREE.Group();
moonPivot.rotation.z = THREE.MathUtils.degToRad(5.1);
scene.add(moonPivot);
const moonTex = new THREE.CanvasTexture(
buildMoonMap(QUALITY.MOON_W, QUALITY.MOON_H)
);
moonTex.encoding = THREE.sRGBEncoding;
moonTex.wrapS = THREE.RepeatWrapping;
moonTex.anisotropy = QUALITY.ANISOTROPY;
const geo = new THREE.SphereGeometry(
0.2725,
QUALITY.MOON_SEG_W, QUALITY.MOON_SEG_H
);
const mat = new THREE.MeshPhongMaterial({
map: moonTex,
shininess: 3,
specular: 0x111111,
bumpMap: moonTex,
bumpScale: 0.018
});
moonMesh = new THREE.Mesh(geo, mat);
moonMesh.position.set(4.6, 0, 0);
moonPivot.add(moonMesh);
return moonMesh;
}
/* ================================================================
5) إنشاء الأرض
================================================================ */
async function createEarth(onProgress) {
const W = QUALITY.EARTH_W, H = QUALITY.EARTH_H;
const maps = await buildEarthMaps(W, H, (p) => {
if (onProgress) onProgress(p*0.60, "رسم القارات والمحيطات بدقة عالية…");
});
const maxAniso = Math.min(
QUALITY.ANISOTROPY,
renderer.capabilities.getMaxAnisotropy()
);
const colorTex = new THREE.CanvasTexture(maps.colorCv);
colorTex.encoding = THREE.sRGBEncoding;
colorTex.wrapS = THREE.RepeatWrapping;
colorTex.anisotropy = maxAniso;
colorTex.minFilter = THREE.LinearMipMapLinearFilter;
colorTex.magFilter = THREE.LinearFilter;
colorTex.generateMipmaps = true;
const bumpTex = new THREE.CanvasTexture(maps.heightCv);
bumpTex.wrapS = THREE.RepeatWrapping;
bumpTex.anisotropy = maxAniso;
bumpTex.minFilter = THREE.LinearMipMapLinearFilter;
bumpTex.magFilter = THREE.LinearFilter;
const specTex = new THREE.CanvasTexture(maps.specCv);
specTex.wrapS = THREE.RepeatWrapping;
specTex.anisotropy = maxAniso;
const cloudCv = await buildCloudMap(QUALITY.CLOUD_W, QUALITY.CLOUD_H, (p) => {
if (onProgress) onProgress(0.60 + p*0.22, "تكوين السحاب بدقة عالية…");
});
const cloudTex = new THREE.CanvasTexture(cloudCv);
cloudTex.encoding = THREE.sRGBEncoding;
cloudTex.wrapS = THREE.RepeatWrapping;
cloudTex.anisotropy = maxAniso;
cloudTex.minFilter = THREE.LinearMipMapLinearFilter;
cloudTex.magFilter = THREE.LinearFilter;
if (onProgress) onProgress(0.84, "تجميع الكوكب…");
earthGroup = new THREE.Group();
earthGroup.rotation.z = THREE.MathUtils.degToRad(23.44);
scene.add(earthGroup);
/* جسم الأرض — شرائح أعلى بكثير */
const earthGeo = new THREE.SphereGeometry(
EARTH_RADIUS,
QUALITY.EARTH_SEG_W, QUALITY.EARTH_SEG_H
);
const earthMat = new THREE.MeshPhongMaterial({
map: colorTex,
bumpMap: bumpTex,
bumpScale: 0.028,
specularMap: specTex,
specular: new THREE.Color(0x9fc3ff),
shininess: 55
});
earthMesh = new THREE.Mesh(earthGeo, earthMat);
earthGroup.add(earthMesh);
/* طبقة السحاب — شرائح أعلى */
const cloudGeo = new THREE.SphereGeometry(
EARTH_RADIUS * 1.008,
QUALITY.CLOUD_SEG_W, QUALITY.CLOUD_SEG_H
);
const cloudMat = new THREE.MeshPhongMaterial({
map: cloudTex,
transparent: true,
opacity: 0.88,
depthWrite: false,
shininess: 0,
specular: 0x000000
});
cloudMesh = new THREE.Mesh(cloudGeo, cloudMat);
earthGroup.add(cloudMesh);
buildAtmosphere();
buildGrid();
buildMoon();
buildStars();
buildSunSprite();
if (onProgress) onProgress(0.94, "تجهيز الإضاءة…");
await sleep(60);
if (onProgress) onProgress(1.0, "جاهز!");
await sleep(280);
}
/* ================================================================
6) التحكم بالكاميرا
================================================================ */
function setupControls() {
const el = renderer.domElement;
el.addEventListener("pointerdown", (e) => {
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
try { el.setPointerCapture(e.pointerId); } catch(_) {}
cam.dragging = true;
cam.vTheta = 0; cam.vPhi = 0;
if (pointers.size === 2) cam.lastPinch = 0;
});
el.addEventListener("pointermove", (e) => {
if (!pointers.has(e.pointerId)) return;
const prev = pointers.get(e.pointerId);
const dx = e.clientX - prev.x;
const dy = e.clientY - prev.y;
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (pointers.size === 1) {
const k = 0.0052;
cam.tTheta -= dx*k;
cam.tPhi -= dy*k;
cam.tPhi = clamp(cam.tPhi, 0.14, Math.PI-0.14);
cam.vTheta = -dx*0.0016;
cam.vPhi = -dy*0.0016;
} else if (pointers.size === 2) {
const pts = Array.from(pointers.values());
const d = Math.hypot(pts[0].x-pts[1].x, pts[0].y-pts[1].y);
if (cam.lastPinch > 0 && d > 0) {
cam.tRadius = clamp(cam.tRadius*(cam.lastPinch/d), 1.5, 14);
}
cam.lastPinch = d;
}
});
const endPointer = (e) => {
pointers.delete(e.pointerId);
if (pointers.size === 0) cam.dragging = false;
if (pointers.size < 2) cam.lastPinch = 0;
};
el.addEventListener("pointerup", endPointer);
el.addEventListener("pointercancel", endPointer);
el.addEventListener("pointerleave", endPointer);
el.addEventListener("wheel", (e) => {
e.preventDefault();
const f = 1 + clamp(e.deltaY, -120, 120)*0.0013;
cam.tRadius = clamp(cam.tRadius*f, 1.5, 14);
}, { passive: false });
el.addEventListener("dblclick", () => {
resetCamera();
toast("تم إعادة ضبط الكاميرا");
});
el.addEventListener("contextmenu", (e) => e.preventDefault());
}
function resetCamera() {
cam.tTheta = 0.75;
cam.tPhi = 1.32;
cam.tRadius = 3.55;
cam.vTheta = 0; cam.vPhi = 0;
}
/* ================================================================
7) ربط الواجهة
================================================================ */
function setupUI() {
const bind = (id, valId, fmt, apply) => {
const input = $("#" + id);
const label = valId ? $("#" + valId) : null;
const update = () => {
const v = parseFloat(input.value);
if (label) label.textContent = fmt ? fmt(v) : v.toFixed(1);
if (apply) apply(v);
};
input.addEventListener("input", update);
update();
return input;
};
bind("spin", "vSpin", (v)=>v.toFixed(1), (v)=>{ motion.spin = v; });
bind("cloud", "vCloud", (v)=>v.toFixed(1), (v)=>{ motion.cloud = v; });
bind("auto", "vAuto", (v)=>v.toFixed(1), (v)=>{ motion.auto = v; });
bind("moonSpeed", "vMoon", (v)=>v.toFixed(1), (v)=>{ motion.moon = v; });
bind("light", "vLight", (v)=>v.toFixed(1), (v)=>{
sunLight.intensity = v;
if (sunSprite) sunSprite.material.opacity = clamp(v/3.2, 0.25, 0.95);
});
bind("ambient", "vAmb", (v)=>v.toFixed(2), (v)=>{
ambientLight.intensity = v;
hemiLight.intensity = v*1.15;
});
bind("shine", "vShine", (v)=>v.toFixed(0), (v)=>{
if (earthMesh) earthMesh.material.shininess = v;
});
bind("glow", "vGlow", (v)=>v.toFixed(2), (v)=>{
if (atmosphereMesh) atmosphereMesh.material.uniforms.uStrength.value = v;
});
bind("bump", "vBump", (v)=>v.toFixed(3), (v)=>{
if (earthMesh) earthMesh.material.bumpScale = v;
});
const toggles = [
["tAtmo", (on)=>{ if (atmosphereMesh) atmosphereMesh.visible = on; }],
["tClouds", (on)=>{ if (cloudMesh) cloudMesh.visible = on; }],
["tStars", (on)=>{ if (starField) starField.visible = on; }],
["tMoon", (on)=>{ if (moonPivot) moonPivot.visible = on; }],
["tSun", (on)=>{ if (sunSprite) sunSprite.visible = on; }],
["tGrid", (on)=>{ if (gridHelper) gridHelper.visible = on; }]
];
toggles.forEach(([id, fn]) => {
const el = $("#" + id);
el.addEventListener("change", () => fn(el.checked));
fn(el.checked);
});
$("#btnReset").addEventListener("click", () => {
resetCamera();
toast("تم إعادة ضبط الكاميرا");
});
$("#btnRandom").addEventListener("click", async () => {
toast("جارٍ توليد كوكب جديد بدقة عالية…");
await regeneratePlanet();
});
$("#btnShot").addEventListener("click", () => {
try {
renderer.render(scene, camera);
const url = renderer.domElement.toDataURL("image/png");
const a = document.createElement("a");
a.href = url;
a.download = "earth-3d-hd-" + Date.now() + ".png";
a.click();
toast("تم حفظ الصورة ✔");
} catch (err) {
toast("تعذّر حفظ الصورة");
}
});
$("#btnFull").addEventListener("click", () => {
const el = document.documentElement;
if (!document.fullscreenElement) {
(el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen)
.call(el).catch(() => toast("ملء الشاشة غير مدعوم"));
} else {
document.exitFullscreen();
}
});
const panel = $("#panel");
const toggleBtn = $("#panelToggle");
$("#collapseBtn").addEventListener("click", () => {
panel.classList.add("collapsed");
toggleBtn.classList.add("show");
});
toggleBtn.addEventListener("click", () => {
panel.classList.remove("collapsed");
toggleBtn.classList.remove("show");
});
setTimeout(() => {
const h = $("#hint");
if (h) h.classList.add("fade");
}, 9000);
renderer.domElement.addEventListener("pointerdown", () => {
const h = $("#hint");
if (h) h.classList.add("fade");
}, { once: true });
}
/* ================================================================
8) توليد كوكب جديد
================================================================ */
async function regeneratePlanet() {
const btn = $("#btnRandom");
btn.disabled = true;
planetCfg.seed = Math.random()*500;
planetCfg.seaLevel = rand(0.02, 0.16);
planetCfg.landScale = rand(1.05, 1.85);
planetCfg.roughness = rand(0.85, 1.25);
planetCfg.iceEdge = rand(0.66, 0.82);
planetCfg.cloudCover = rand(0.44, 0.58);
const maps = await buildEarthMaps(QUALITY.EARTH_W, QUALITY.EARTH_H, null);
const cloudCv = await buildCloudMap(QUALITY.CLOUD_W, QUALITY.CLOUD_H, null);
const maxAniso = Math.min(
QUALITY.ANISOTROPY,
renderer.capabilities.getMaxAnisotropy()
);
const newColor = new THREE.CanvasTexture(maps.colorCv);
newColor.encoding = THREE.sRGBEncoding;
newColor.wrapS = THREE.RepeatWrapping;
newColor.anisotropy = maxAniso;
const newBump = new THREE.CanvasTexture(maps.heightCv);
newBump.wrapS = THREE.RepeatWrapping;
newBump.anisotropy = maxAniso;
const newSpec = new THREE.CanvasTexture(maps.specCv);
newSpec.wrapS = THREE.RepeatWrapping;
const newCloud = new THREE.CanvasTexture(cloudCv);
newCloud.encoding = THREE.sRGBEncoding;
newCloud.wrapS = THREE.RepeatWrapping;
newCloud.anisotropy = maxAniso;
const old = earthMesh.material;
if (old.map) old.map.dispose();
if (old.bumpMap) old.bumpMap.dispose();
if (old.specularMap) old.specularMap.dispose();
earthMesh.material.map = newColor;
earthMesh.material.bumpMap = newBump;
earthMesh.material.specularMap = newSpec;
earthMesh.material.needsUpdate = true;
if (cloudMesh.material.map) cloudMesh.material.map.dispose();
cloudMesh.material.map = newCloud;
cloudMesh.material.needsUpdate = true;
btn.disabled = false;
toast("تم توليد كوكب جديد ✔");
}
/* ================================================================
9) حلقة الرسم
================================================================ */
let clock;
let fpsFrames = 0, fpsTimer = 0;
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);
if (!cam.dragging) {
if (Math.abs(cam.vTheta) > 0.00002 || Math.abs(cam.vPhi) > 0.00002) {
cam.tTheta += cam.vTheta;
cam.tPhi += cam.vPhi;
cam.tPhi = clamp(cam.tPhi, 0.14, Math.PI-0.14);
cam.vTheta *= 0.935;
cam.vPhi *= 0.935;
} else {
cam.vTheta = 0; cam.vPhi = 0;
}
if (motion.auto > 0) {
cam.tTheta += motion.auto * 0.00075 * (60*dt);
}
}
const ease = 1 - Math.pow(0.0008, dt);
cam.theta = lerp(cam.theta, cam.tTheta, ease);
cam.phi = lerp(cam.phi, cam.tPhi, ease);
cam.radius = lerp(cam.radius, cam.tRadius, ease);
const sp = new THREE.Vector3().setFromSphericalCoords(cam.radius, cam.phi, cam.theta);
camera.position.copy(sp);
camera.lookAt(0, 0, 0);
if (earthMesh) {
earthMesh.rotation.y += 0.0016 * motion.spin * (60*dt);
}
if (cloudMesh) {
cloudMesh.rotation.y += 0.0016 * motion.spin * (60*dt);
cloudMesh.rotation.y += 0.00035 * motion.cloud * (60*dt);
}
if (gridHelper && gridHelper.visible && earthMesh) {
gridHelper.rotation.y = earthMesh.rotation.y;
}
if (moonPivot && moonPivot.visible) {
moonPivot.rotation.y += 0.00055 * motion.moon * (60*dt);
if (moonMesh) moonMesh.rotation.y += 0.0004 * (60*dt);
}
if (starField && starField.visible) {
starField.rotation.y += 0.000012 * (60*dt);
}
if (atmosphereMesh) {
atmosphereMesh.material.uniforms.uLightDir.value
.copy(sunLight.position).normalize();
}
renderer.render(scene, camera);
fpsFrames++;
fpsTimer += dt;
if (fpsTimer >= 0.5) {
const fps = Math.round(fpsFrames / fpsTimer);
$("#fps").textContent = fps;
$("#dist").textContent = cam.radius.toFixed(1);
fpsFrames = 0;
fpsTimer = 0;
}
}
/* ================================================================
10) تغيير حجم النافذة
================================================================ */
function onResize() {
if (!renderer || !camera) return;
const w = window.innerWidth, h = window.innerHeight;
camera.aspect = w/h;
camera.updateProjectionMatrix();
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, QUALITY.MAX_PIXEL_RATIO));
renderer.setSize(w, h);
}
/* ================================================================
11) شاشة التحميل
================================================================ */
function setLoading(p, status) {
const pct = Math.round(clamp(p, 0, 1) * 100);
$("#loadBar").style.width = pct + "%";
$("#loadPct").textContent = pct + "%";
if (status) $("#loadStatus").textContent = status;
}
/* ================================================================
12) نقطة البداية
================================================================ */
async function init() {
try {
const testCanvas = document.createElement("canvas");
const gl = testCanvas.getContext("webgl") || testCanvas.getContext("experimental-webgl");
if (!gl) {
$("#loadStatus").textContent = "متصفحك لا يدعم WebGL — الرجاء استخدام متصفح حديث.";
return;
}
setLoading(0.03, "تهيئة محرك الرسم…");
await nextFrame();
setupRenderer();
setupScene();
setupControls();
setLoading(0.08, "توليد تضاريس الكوكب بدقة عالية… (قد يستغرق بضع ثوانٍ)");
await nextFrame();
await createEarth((p, status) => setLoading(0.08 + p*0.88, status));
setupUI();
clock = new THREE.Clock();
setLoading(1, "جاهز!");
await sleep(420);
$("#loader").classList.add("hidden");
animate();
setTimeout(() => toast("مرحباً بك في الأرض بدقة عالية 🌍"), 900);
} catch (err) {
console.error(err);
$("#loadStatus").textContent = "حدث خطأ: " + (err && err.message ? err.message : err);
$("#loadPct").textContent = "!";
}
}
window.addEventListener("resize", onResize);
window.addEventListener("orientationchange", () => setTimeout(onResize, 220));
window.addEventListener("keydown", (e) => {
switch (e.key) {
case "r": case "R": resetCamera(); break;
case "h": case "H": {
const panel = $("#panel");
const tb = $("#panelToggle");
if (panel.classList.contains("collapsed")) {
panel.classList.remove("collapsed");
tb.classList.remove("show");
} else {
panel.classList.add("collapsed");
tb.classList.add("show");
}
break;
}
case " ": {
e.preventDefault();
const spinEl = $("#spin");
spinEl.value = spinEl.value > 0 ? "0" : "1";
spinEl.dispatchEvent(new Event("input"));
break;
}
}
});
init();
</script>
</body>
</html>
التعليقات (2)
عااش متعوب عليها 🐦
يا رجل انت لست مطور عادي انت مهندس رسوميات ❤️
سجّل الدخول للتعليق.