HTMLify
moon.html
Views: 45 | Author: amar
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Accurate 2D Moon Phases (per-pixel shading)</title> <style> html,body{height:100%;margin:0;background:radial-gradient(circle at top,#081027,#030412);display:flex;align-items:center;justify-content:center;color:#fff;font-family:Arial} .wrap{display:flex;flex-direction:column;align-items:center} canvas{background:transparent;margin-bottom:14px} input[type=range]{width:360px} .label{margin-top:8px;text-align:center} </style> </head> <body> <div class="wrap"> <canvas id="c" width="380" height="380"></canvas> <input id="slider" type="range" min="0" max="100" value="0"> <div class="label"><span id="name">New Moon</span> — <span id="pct">0</span>%</div> </div> <script> const canvas = document.getElementById('c'); const ctx = canvas.getContext('2d'); const slider = document.getElementById('slider'); const nameEl = document.getElementById('name'); const pctEl = document.getElementById('pct'); // Use your texture (change URL if needed) const img = new Image(); img.crossOrigin = "anonymous"; img.src = "https://htmlify.me/amar/moon-1255265_1280%20(1).png"; // visual size const W = canvas.width; const H = canvas.height; const cx = W/2; const cy = H/2; const R = Math.min(W,H)*0.34; // radius in px const phaseNames = [ "New Moon","Waxing Crescent","First Quarter","Waxing Gibbous", "Full Moon","Waning Gibbous","Last Quarter","Waning Crescent" ]; img.onload = () => { drawPhase(parseInt(slider.value,10)); }; slider.addEventListener('input', () => { const s = parseInt(slider.value,10); pctEl.textContent = s; nameEl.textContent = phaseNameFor(s); drawPhase(s); }); function phaseNameFor(s){ // map slider to 8 bins like your reference const idx = Math.floor((s/100) * 8) % 8; return phaseNames[idx]; } // main render: per-pixel lighting of textured moon disk function drawPhase(s) { ctx.clearRect(0,0,W,H); // draw a circular textured moon into an offscreen canvas matching diameter const D = Math.ceil(R*2); const off = document.createElement('canvas'); off.width = D; off.height = D; const octx = off.getContext('2d'); // draw the texture clipped to the circle octx.save(); octx.beginPath(); octx.arc(D/2, D/2, R, 0, Math.PI*2); octx.clip(); // draw image scaled to fit circle octx.drawImage(img, 0, 0, img.width, img.height, 0, 0, D, D); octx.restore(); // read pixels const imageData = octx.getImageData(0,0,D,D); const data = imageData.data; // compute phase angle φ // φ = π * (1 - 2 * s/100) const phi = Math.PI * (1 - 2 * (s / 100)); // light vector L = (sin φ, 0, cos φ) const Lx = Math.sin(phi); const Lz = Math.cos(phi); const ambient = 0.06; // earthshine-like ambient const diffuseScale = 1 - ambient; // loop over pixels and shade only inside disk for (let j=0; j<D; j++){ const y = (j - D/2) / R; // normalized [-1,1] for (let i=0; i<D; i++){ const x = (i - D/2) / R; const idx = (j*D + i) * 4; const r2 = x*x + y*y; if (r2 > 1) continue; // outside moon // normal n = (nx, ny, nz) on sphere const nx = x; const ny = y; const nz = Math.sqrt(Math.max(0, 1 - r2)); // dot product n·L (L has no y component) let dot = nx * Lx + nz * Lz; // optionally smooth the terminator a bit // apply smoothstep near 0 to make the terminator soft: const smoothRange = 0.08; // width of smoothing around zero if (dot < -smoothRange) { dot = -1; // fully dark (we'll clamp to 0 later) } else if (dot > smoothRange) { dot = 1; } else { // linear / smoothstep blend dot = (dot + smoothRange) / (2 * smoothRange); // maps [-s..s] to [0,1] } // final intensity let intensity = ambient + diffuseScale * Math.max(0, dot); // apply slight extra dimming for very dark side, so earthshine visible if (intensity < ambient + 0.02) intensity = ambient + 0.02; // fetch original color const r = data[idx]; const g = data[idx+1]; const b = data[idx+2]; const a = data[idx+3]; // apply intensity and a tiny color correction to keep texture natural data[idx] = Math.min(255, Math.round(r * intensity)); data[idx+1] = Math.min(255, Math.round(g * intensity)); data[idx+2] = Math.min(255, Math.round(b * intensity)); data[idx+3] = a; } } // put shaded texture back octx.putImageData(imageData, 0, 0); // final draw: place the offscreen canvas onto main canvas centered ctx.save(); ctx.drawImage(off, cx - R, cy - R); ctx.restore(); // optional: draw a faint rim / ambient glow to match reference style ctx.save(); ctx.globalAlpha = 0.06; ctx.beginPath(); ctx.arc(cx, cy, R + 2, 0, Math.PI*2); ctx.fillStyle = "#ffffff"; ctx.fill(); ctx.restore(); } </script> </body> </html> |