Skip to content

Instantly share code, notes, and snippets.

@tompng
Created August 5, 2025 08:25
Show Gist options
  • Select an option

  • Save tompng/79b3ebca79e7dc7706cca0e61079a442 to your computer and use it in GitHub Desktop.

Select an option

Save tompng/79b3ebca79e7dc7706cca0e61079a442 to your computer and use it in GitHub Desktop.
<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