Created
August 5, 2025 08:25
-
-
Save tompng/79b3ebca79e7dc7706cca0e61079a442 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <body> | |
| <script> | |
| const canvas = document.createElement('canvas') | |
| document.body.appendChild(canvas) | |
| const size = canvas.width = canvas.height = 800 | |
| const ctx = canvas.getContext('2d') | |
| const circles = [ | |
| { x: 0.2, y: 0.6, r: 0.1 }, | |
| { x: 0.7, y: 0.3, r: 0.07 }, | |
| { x: 0.6, y: 0.8, r: 0.1 }, | |
| { x: 0.3, y: 0.2, r: 0.05 }, | |
| { x: 0.8, y: 0.5, r: 0.13 }, | |
| { x: 0.4, y: 0.4, r: 0.1 }, | |
| { x: 0.1, y: 0.9, r: 0.08 }, | |
| { x: 0.9, y: 0.1, r: 0.1 }, | |
| { x: 0.5, y: 0.2, r: 0.06 }, | |
| { x: 0.1, y: 0.2, r: 0.09 }, | |
| ] | |
| function drawCircle(circle) { | |
| ctx.beginPath() | |
| const lineWidth = 8 | |
| ctx.arc(circle.x * size, circle.y * size, circle.r * size - lineWidth / 2, 0, Math.PI * 2) | |
| ctx.strokeStyle = 'black' | |
| ctx.fillStyle = 'black' | |
| ctx.lineWidth = lineWidth | |
| ctx.globalAlpha = 0.5 | |
| ctx.fill() | |
| ctx.globalAlpha = 1 | |
| ctx.stroke() | |
| } | |
| function trace(x, y, dx, dy) { | |
| let tmin = 10 | |
| let dx2 = dx, dy2 = dy | |
| let x2 = x + tmin * dx2 | |
| let y2 = y + tmin * dy2 | |
| for (const c of circles) { | |
| const ax = x - c.x | |
| const bx = y - c.y | |
| const c2 = dx * dx + dy * dy | |
| const c1 = 2 * (ax * dx + bx * dy) | |
| const c0 = ax * ax + bx * bx - c.r * c.r | |
| const d = c1 * c1 - 4 * c2 * c0 | |
| if (d < 0) continue | |
| const t = (-c1 - Math.sqrt(d)) / c2 / 2 | |
| if (t < 0) continue | |
| if (t < tmin) { | |
| tmin = t | |
| x2 = x + t * dx | |
| y2 = y + t * dy | |
| const nx = (x2 - c.x) / c.r | |
| const ny = (y2 - c.y) / c.r | |
| const dot = dx * nx + dy * ny | |
| dx2 = dx - 2 * dot * nx | |
| dy2 = dy - 2 * dot * ny | |
| } | |
| } | |
| for (const v of [0, 1]) { | |
| const tx = (v - x) / dx | |
| const ty = (v - y) / dy | |
| const dir = 1 - 2 * v | |
| if (tx > 0 && tx < tmin && dx * dir < 0) { | |
| tmin = tx | |
| x2 = v | |
| y2 = y + tx * dy | |
| dx2 = -dx | |
| dy2 = dy | |
| } | |
| if (ty > 0 && ty < tmin && dy * dir < 0) { | |
| tmin = ty | |
| x2 = x + ty * dx | |
| y2 = v | |
| dx2 = dx | |
| dy2 = -dy | |
| } | |
| } | |
| return { x: x2, y: y2, dx: dx2, dy: dy2 } | |
| } | |
| function drawLight(cx, cy) { | |
| const n = 1024 | |
| const rays = [] | |
| const maxLevel = 4 | |
| for (let i = 0; i < n; i++) { | |
| const angle = 2 * Math.PI * i / n | |
| let dx = Math.cos(angle) | |
| let dy = Math.sin(angle) | |
| let x = cx, y = cy | |
| const ray = [{ x, y, dx, dy }] | |
| for (let j = 0; j < maxLevel; j++) { | |
| const next = trace(x, y, dx, dy) | |
| ray.push(next) | |
| ;({ x, y, dx, dy } = next) | |
| } | |
| rays.push(ray) | |
| } | |
| for (let level = 0; level < maxLevel; level++) { | |
| for (let i = 0; i < n; i++) { | |
| const a0 = rays[i][level] | |
| const a1 = rays[i][level + 1] | |
| const b0 = rays[(i + 1) % n][level] | |
| const b1 = rays[(i + 1) % n][level + 1] | |
| // (a0.x + a0.dx*t - b0.x) * b0.dy - (a0.y + a0.dy*t - b0.y) * b0.dx == 0 | |
| const t = ((b0.x - a0.x) * b0.dy - (b0.y - a0.y) * b0.dx) / (a0.dx * b0.dy - a0.dy * b0.dx) | |
| const center = { x: a0.x + t * a0.dx, y: a0.y + t * a0.dy } | |
| const theta = Math.acos(a0.dx * b0.dx + a0.dy * b0.dy) | |
| const maxR = Math.max( | |
| Math.hypot(a0.x - center.x, a0.y - center.y), | |
| Math.hypot(b0.x - center.x, b0.y - center.y), | |
| Math.hypot(a1.x - center.x, a1.y - center.y), | |
| Math.hypot(b1.x - center.x, b1.y - center.y) | |
| ) | |
| const gradient = ctx.createRadialGradient(center.x * size, center.y * size, 0, center.x * size, center.y * size, 2 * size) | |
| const step = 16 | |
| for (let i = 0; i <= step; i++) { | |
| const alpha = 4 / theta / n * (1 / (i + 0.5) - 1 / (step + 0.5)) / Math.pow(1.5, level) | |
| gradient.addColorStop(i / step, `rgba(255, 255, 255, ${alpha})`) | |
| } | |
| ctx.fillStyle = gradient | |
| ctx.beginPath() | |
| ctx.moveTo(a0.x * size, a0.y * size) | |
| ctx.lineTo(a1.x * size, a1.y * size) | |
| ctx.lineTo(b1.x * size, b1.y * size) | |
| ctx.lineTo(b0.x * size, b0.y * size) | |
| ctx.fill() | |
| } | |
| } | |
| } | |
| const mouse = { x: 0.5, y: 0.5 } | |
| function draw() { | |
| ctx.fillStyle = 'rgb(16, 16, 16)' | |
| ctx.fillRect(0, 0, size, size) | |
| drawLight(mouse.x, mouse.y) | |
| circles.forEach(drawCircle) | |
| } | |
| draw() | |
| document.addEventListener('mousemove', e => { | |
| const rect = canvas.getBoundingClientRect() | |
| mouse.x = (e.clientX - rect.left) / rect.width | |
| mouse.y = (e.clientY - rect.top) / rect.height | |
| draw() | |
| }) | |
| </script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment