Dashboard Temp Share Shortlinks Frames API

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>