Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ in full every frame.

| Method | Draws |
| --- | --- |
| `add(x, y, r, g, b)` | Adds light to one pixel, for stars, sparks and glints |
| `add(x, y, r, g, b)` | Adds light to one pixel, for glints on things that move with a creature |
| `point(x, y, r, g, b, size?, glide?)` | A still point of light, such as a star or a distant lamp; on terminal cells a round dot glyph (see below) |
| `splat(x, y, r, g, b)` | Adds light shared between the four pixels around (x, y), for moving specks: snowflakes, sparks, motes |
| `blend(i, r, g, b, a)` | Mixes a color into the pixel at `hdr` index `i` by `a`, inside your own loops |
| `disc(cx, cy, radius, r, g, b, a)` | An anti-aliased circle |
| `ellipse(cx, cy, rx, ry, r, g, b, a)` | An anti-aliased axis-aligned ellipse |
Expand All @@ -132,6 +134,13 @@ For per-pixel effects such as water, sand or fog, loop over `hdr` directly, and
other rows of the scene into water rows with drifting ripples, a tint and a per-pixel weight; call it each frame over
only the water's rows (as the beach and the prehistoric lake do), or once in `layout()` for still water such as ice.

A lone bright pixel turns into a hard square block on terminal cells, and a speck drawn with `add` hops a whole
pixel at a time. So draw still points with `point()`: on cells the engine shows each as a round dot glyph (`·`, `∙`
or `•` by size) where its cell is otherwise flat, and as a plain pixel elsewhere. Draw them before anything that may
pass in front of them, since whatever is drawn over a point keeps it a plain pixel. Set `glide` for points that drift
sideways, such as space's stars, so their dot fades from cell to cell. Draw moving specks with `splat()`, so they glide
and a faint one tints its cell instead of flipping its pattern.

### Shapes

`shape()` takes a list of `Part`s:
Expand Down Expand Up @@ -163,8 +172,8 @@ details), and values in between soften it.
- `paintSky(hdr, W, H, stops, orb)` fills the canvas with a vertical gradient through `[fraction of height, color]`
stops, a glow around the sun or moon, and the orb itself. An `Orb` has x (fraction of width), y and r (fractions
of height), `core` and `glow` colors, `near` and `wide` glow strengths, and `moon: true` for a cratered moon.
- `makeStars(count, top)` and `paintStars(hdr, W, H, stars, time, top, brightness)` draw slowly twinkling stars that
fade toward `top`. Stars are tinted from `STAR_TINTS`, never white.
- `makeStars(count, top)` and `paintStars(hdr, W, H, stars, time, top, brightness, this.points)` draw slowly twinkling
stars that fade toward `top`, as points (see Drawing). Stars are tinted from `STAR_TINTS`, never white.
- `makeClouds(count, kind, y0, y1)`, `driftClouds(clouds, A, dt)` and `paintClouds(hdr, W, H, clouds, top, bottom,
alpha, orb)` give clouds of one kind: `cumulus`, `towering` (standing on y, for a horizon), `cirrus`, `wisp`,
`stratus` or `ash`. They are lit from the orb: the brighter of `top` and `bottom` faces it, and clouds near it are
Expand Down
11 changes: 7 additions & 4 deletions dev/cells.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// bun dev/cells.ts dump.json out.png
import { OCTANTS } from "../src/octants"
import { SHAPES } from "../src/smooth"
import { glyph } from "./font"
import { DOTS, glyph } from "./font"
import { encodePng } from "./png"

const QUADS = [0x20, 0x2598, 0x259d, 0x2580, 0x2596, 0x258c, 0x259e, 0x259b, 0x2597, 0x259a, 0x2590, 0x259c, 0x2584, 0x2599, 0x259f, 0x2588]
Expand All @@ -24,12 +24,15 @@ export function cellsToPng(cells: Cells) {
const oct = ch === 0x20 ? -1 : OCTANTS.indexOf(ch)
const quad = QUADS.indexOf(ch)
const shape = oct < 0 ? SHAPES.get(ch) : undefined
const gl = oct < 0 && quad < 0 && !shape && ch > 32 ? glyph(ch) : undefined
const dot = DOTS.get(ch)
const gl = oct < 0 && quad < 0 && !shape && !dot && ch > 32 ? glyph(ch) : undefined
for (let y = 0; y < CH; y++)
for (let x = 0; x < CW; x++) {
// Smooth glyphs are anti-aliased, as Ghostty and kitty draw them: mix by coverage of a 4x4 grid per pixel.
// Smooth glyphs are anti-aliased, as Ghostty and kitty draw them: mix by coverage of a 4x4 grid per pixel;
// dots likewise, as a font renders them.
let cover = 0
if (shape) for (let s = 0; s < 16; s++) cover += shape((x + ((s & 3) + 0.5) / 4) / CW, (y + ((s >> 2) + 0.5) / 4) / CH) ? 1 / 16 : 0
if (dot) cover = Math.min(1, Math.max(0, dot.r + 0.5 - Math.hypot(x + 0.5 - CW / 2, y + 0.5 - dot.y)))
const lit =
oct > 0
? !!(oct & (1 << (Math.floor((y * 4) / CH) * 2 + (x < CW / 2 ? 0 : 1))))
Expand All @@ -38,7 +41,7 @@ export function cellsToPng(cells: Cells) {
: gl
? x >= 1 && x < 6 && y >= 3 && y < 11 && !!(gl[x - 1] & (1 << (y - 3)))
: ch > 127 && x >= 2 && x <= 4 && y >= 5 && y <= 8
const k = shape ? cover : lit ? 1 : 0
const k = shape || dot ? cover : lit ? 1 : 0
const o = ((cy * CH + y) * W + cx * CW + x) * 4
for (let c = 0; c < 3; c++) img[o + c] = (cells.bg[i * 4 + c] & 255) * (1 - k) + (cells.fg[i * 4 + c] & 255) * k
img[o + 3] = 255
Expand Down
8 changes: 8 additions & 0 deletions dev/font.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ export function glyph(code: number): Uint8Array | undefined {
if (code < 32 || code > 126) return undefined
return glyphs[code - 32]
}

// Round dot glyphs the engine draws stars with (· ∙ •): radius and center height in pixels of an 8x16 cell, roughly
// as a typical monospace font draws them.
export const DOTS = new Map([
[0xb7, { r: 1.1, y: 8.5 }],
[0x2219, { r: 1.5, y: 8.5 }],
[0x2022, { r: 2.2, y: 8.5 }],
])
Binary file modified screenshots/city-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/city-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/city.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/desert-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/desert-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/desert.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/farm-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/jungle-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/jungle-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/jungle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/ocean-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/ocean-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/ocean.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/prehistoric-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/prehistoric.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/tundra-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/tundra-sunset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/tundra.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/zen-night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 20 additions & 1 deletion src/canvas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AgentEvent, TextMap } from "./wallpaper"
import { clamp, rand, smoothstep, type RGB } from "./math"
import { Points, splat } from "./points"
import { trackOrbs } from "./orbs"
import { clearShadows, keepShadows } from "./shadow"
import { addBox, trackTouched } from "./touched"
Expand Down Expand Up @@ -67,6 +68,8 @@ export abstract class Canvas {
readonly changed: number[] = []
// Whether this frame started with restore(); a scene that never calls it can have drawn anywhere.
private restored = false
// Still points of light registered this frame (see point()), for the engine to draw as dot glyphs on cells.
readonly points = new Points()
// The sun and moon discs layout() painted, [cx, cy, r, ...] in pixels (see src/orbs.ts).
readonly orbs: number[] = []

Expand Down Expand Up @@ -195,6 +198,7 @@ export abstract class Canvas {
this.W = W
this.H = H
this.cells = cells
this.points.on = cells
this.A = W / H
this.hdr = new Float32Array(W * H * 3)
trackTouched(this.hdr, this.touched)
Expand Down Expand Up @@ -305,6 +309,19 @@ export abstract class Canvas {
this.touch(xi, yi, xi, yi)
}

// Adds a still point of light, such as a star or a distant lamp: one pixel, or on terminal cells a round dot glyph
// (size 1 small, 2 medium, 3 large) where its cell is otherwise flat. Set glide for points that drift sideways. Draw
// points before anything that may pass in front of them: whatever is drawn over one keeps it a plain pixel.
protected point(x: number, y: number, r: number, g: number, b: number, size = 1, glide = false) {
this.points.add(this.hdr, this.W, this.H, x, y, r, g, b, size, glide)
}

// Adds light for a moving speck (a snowflake, a spark, a firefly's core), shared between the pixels around it so
// it glides rather than hopping a pixel at a time.
protected splat(x: number, y: number, r: number, g: number, b: number) {
splat(this.hdr, this.W, this.H, x, y, r, g, b)
}

// Mixes a color over the pixel at hdr index i by coverage a.
protected blend(i: number, r: number, g: number, b: number, a: number) {
const h = this.hdr
Expand Down Expand Up @@ -357,8 +374,10 @@ export abstract class Canvas {
}

// Bloom, vignette and ACES tone mapping from hdr into pixels. Only the blocks that differ from the still frame, or
// whose bloom does, are worked out again; the rest keep the still frame's pixels.
// whose bloom does, are worked out again; the rest keep the still frame's pixels. On terminal cells, still points
// come back out of hdr first (see point()).
protected finish(exposure = 1.25) {
this.points.resolve(this.hdr, this.W, this.vignette, exposure)
clearShadows(this.hdr)
const { W, H, bw, bh, retone } = this
const fresh = exposure !== this.stillExposure
Expand Down
128 changes: 128 additions & 0 deletions src/dots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Draws the still points of light scenes register (see src/points.ts) on terminal cells: as a round dot glyph in the
// point's color over the cell's own color where the cell is otherwise flat, and elsewhere (an edge, a creature, the
// text scrim) painted into the cell's sub-pixels before they are fitted, as a plain pixel would be.
import { POINT_STRIDE } from "./points"

// · ∙ • by point size.
const GLYPHS = [0xb7, 0x2219, 0x2022]
// Squared error (0-255 channel units, summed over a cell's eight sub-pixels) under which a cell counts as flat. A cell
// already showing a dot keeps it up to twice that, so a point at the limit doesn't flip between dot and block.
const FLAT = 400
// Scrim strength past which a point is painted rather than dotted, keeping dots out of the scrim's strongest part.
const SCRIM = 0.5
// A gliding point fades into the next cell over the outer twelfth of its cell on either side: a quick crossfade
// instead of a hop, and seldom long enough to show as two dots.
const GLIDE = 0.08
// A dot covers less of its cell than the block a lone sub-pixel turns into, so smaller dots are drawn brighter.
const GAINS = [2, 1.5, 1.1]
// The unsharp mask lifts a lone bright sub-pixel by about this much; painted points get the same lift.
const SHARPENED = 1.5
// previousMask value for a cell showing a dot.
export const DOT = -2

export class Dots {
// Per cell: the dot's size (0 for none), the sub-pixel it lies in (0-7, row-major) and the light it adds (display
// units, after desaturation and shade).
size = new Uint8Array(0)
sub = new Uint8Array(0)
light = new Float32Array(0)

clear(cells: number) {
if (this.size.length !== cells) {
this.size = new Uint8Array(cells)
this.sub = new Uint8Array(cells)
this.light = new Float32Array(cells * 3)
return
}
this.size.fill(0)
this.light.fill(0)
}

// Adds a scene's points, faded by alpha (during a crossfade), for a W x H grid of cells over a PW x PH scene.
gather(points: { readonly count: number; readonly data: Float32Array } | undefined, alpha: number, PW: number, PH: number, W: number, H: number, desaturate: number, shade: number) {
if (!points?.count || alpha <= 0) return
gatherPoints(this.size, this.sub, this.light, points.data, points.count, alpha, W / PW, H / PH, W, H, desaturate, shade)
}
}

function gatherPoints(size: Uint8Array, sub: Uint8Array, light: Float32Array, d: Float32Array, count: number, alpha: number, sx: number, sy: number, W: number, H: number, desaturate: number, shade: number) {
for (let p = 0; p < count * POINT_STRIDE; p += POINT_STRIDE) {
const gx = d[p] * sx
const gy = d[p + 1] * sy
const cx = Math.floor(gx)
const cy = Math.floor(gy)
if (cx < 0 || cy < 0 || cx >= W || cy >= H) continue
const s = Math.min(3, Math.floor((gy - cy) * 4)) * 2 + ((gx - cx) * 2 >= 1 ? 1 : 0)
const l = d[p + 4] * 0.2126 + d[p + 5] * 0.7152 + d[p + 6] * 0.0722
const r = (d[p + 4] + (l - d[p + 4]) * desaturate) * shade * alpha
const g = (d[p + 5] + (l - d[p + 5]) * desaturate) * shade * alpha
const b = (d[p + 6] + (l - d[p + 6]) * desaturate) * shade * alpha
const u = gx - cx - 0.5
const share = d[p + 3] && Math.abs(u) > 0.5 - GLIDE ? (Math.abs(u) - (0.5 - GLIDE)) / (GLIDE * 2) : 0
const row = cy * W
addDot(size, sub, light, row + cx, d[p + 2], s, r * (1 - share), g * (1 - share), b * (1 - share))
const nx = cx + (u > 0 ? 1 : -1)
// The neighbour's sub-pixel is the one on the shared edge.
if (share > 0 && nx >= 0 && nx < W) addDot(size, sub, light, row + nx, d[p + 2], (s & 6) | (u > 0 ? 0 : 1), r * share, g * share, b * share)
}
}

function addDot(size: Uint8Array, sub: Uint8Array, light: Float32Array, i: number, s: number, at: number, r: number, g: number, b: number) {
if (!size[i] || r + g + b > light[i * 3] + light[i * 3 + 1] + light[i * 3 + 2]) sub[i] = at
size[i] = Math.max(size[i], Math.min(3, Math.max(1, Math.round(s))))
light[i * 3] += r
light[i * 3 + 1] += g
light[i * 3 + 2] += b
}

// Writes cell i as a dot if it is flat enough and clear of the scrim's strongest part: the cell's mean color behind,
// the point's color in front. q holds the cell's eight sub-pixels as displayed (curve and scrim applied), grid the
// scene before the curve, st and sb the scrim over the top and bottom halves. held says it showed a dot last frame.
export function placeDot(dots: Dots, i: number, x: number, y: number, q: Float32Array, grid: Float32Array, GW: number, st: number, sb: number, curve: Uint8Array, held: boolean, char: Uint32Array, fg: Uint16Array, bg: Uint16Array, o: number) {
if (st > SCRIM || sb > SCRIM) return false
let r = 0
let g = 0
let b = 0
for (let s = 0; s < 8; s++) {
r += q[s * 3]
g += q[s * 3 + 1]
b += q[s * 3 + 2]
}
r /= 8
g /= 8
b /= 8
let err = 0
for (let s = 0; s < 8; s++) err += (q[s * 3] - r) ** 2 + (q[s * 3 + 1] - g) ** 2 + (q[s * 3 + 2] - b) ** 2
if (err > (held ? FLAT * 2 : FLAT)) return false
const k = (1 - (st + sb) / 2) * GAINS[dots.size[i] - 1]
char[i] = GLYPHS[dots.size[i] - 1]
bg[o] = r
bg[o + 1] = g
bg[o + 2] = b
bg[o + 3] = 255
for (let c = 0; c < 3; c++) {
let under = 0
for (let s = 0; s < 8; s++) under += grid[((y * 4 + (s >> 1)) * GW + x * 2 + (s & 1)) * 3 + c]
under /= 8
lift[c] = (curve[Math.min(255, under + dots.light[i * 3 + c]) | 0] - curve[under | 0]) * k
}
// Scaled back as a whole if a channel would clip, so the dot keeps its tint instead of washing toward white.
const fit = Math.min(1, (255 - r) / Math.max(1e-3, lift[0]), (255 - g) / Math.max(1e-3, lift[1]), (255 - b) / Math.max(1e-3, lift[2]))
fg[o] = r + lift[0] * fit
fg[o + 1] = g + lift[1] * fit
fg[o + 2] = b + lift[2] * fit
fg[o + 3] = 255
return true
}
const lift = new Float32Array(3)

// Paints cell i's point into its sub-pixel q, as it would look drawn into the scene.
export function lightDot(dots: Dots, i: number, x: number, y: number, q: Float32Array, grid: Float32Array, GW: number, st: number, sb: number, curve: Uint8Array) {
const s = dots.sub[i]
const a = ((y * 4 + (s >> 1)) * GW + x * 2 + (s & 1)) * 3
const k = 1 - (s < 4 ? st : sb)
for (let c = 0; c < 3; c++) {
const under = grid[a + c]
q[s * 3 + c] += (curve[Math.min(255, under + dots.light[i * 3 + c] * SHARPENED) | 0] - curve[under | 0]) * k
}
}
24 changes: 20 additions & 4 deletions src/engine.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Context } from "@opencode/plugin/tui/context"
import { MouseButton, NativeImagePool, Renderable, type KittyImageTransport, type MouseEvent, type OptimizedBuffer, type RenderContext } from "@opentui/core"
import { appendFileSync } from "node:fs"
import { DOT, Dots, lightDot, placeDot } from "./dots"
import { watchEgg } from "./egg"
import { lerp, smoothstep, type RGB } from "./math"
import { OCTANTS } from "./octants"
Expand Down Expand Up @@ -798,6 +799,7 @@ export function createEngine(
let lastBlend = ""
let lastScrim: Scrim | undefined
let lastCurve: Uint8Array | undefined
const dots = new Dots()

let imagePixels = new Uint8Array(0)
let pool: NativeImagePool | undefined
Expand Down Expand Up @@ -1011,6 +1013,11 @@ export function createEngine(
const desaturate = lerp(DESATURATE[was.settings.time], DESATURATE[settings!.time], t)
// Blending in the previous frame fades at the same speed per second whatever the frame rate.
const persistence = interval > 1000 / FPS ? PERSISTENCE ** (interval / (1000 / FPS)) : PERSISTENCE
if (advanced) {
dots.clear(W * H)
dots.gather(scene.points, t, scene.W, scene.H, W, H, desaturate, shade)
dots.gather(from?.scene.points, 1 - t, scene.W, scene.H, W, H, desaturate, shade)
}
paint(buf, from ? mixed : scene.pixels, scene.W, scene.H, advanced, pixels, desaturate, shade, persistence, scrim, from ? undefined : scene.changed, from ? undefined : scene.touched, scene.orbs)
if (mapFresh) {
mapFresh = false
Expand Down Expand Up @@ -1190,7 +1197,7 @@ export function createEngine(
for (let x = 0; x < W; x++) {
const i = y * W + x
// Cells where something moves fit differently (see tolerance), so they are never reused.
const reusable = fitted[i] === 1 && !refitAll && refit[i] === 0 && moving[i] === 0
const reusable = fitted[i] === 1 && !refitAll && refit[i] === 0 && moving[i] === 0 && dots.size[i] === 0
fitted[i] = 0
if (imageCell[i]) continue
const o = i * 4
Expand Down Expand Up @@ -1238,8 +1245,17 @@ export function createEngine(
}
if (fits) {
const was = previousMask[i]
// A dot glyph where a still point of light falls on an otherwise flat cell; a cell with a point is fitted
// afresh every frame, and once more after, since the point isn't part of the grid.
if (dots.size[i] && placeDot(dots, i, x, y, q, grid, GW, st, sb, curve, was === DOT, char, fg, bg, o)) {
previousMask[i] = DOT
applyTint(bg, o, cellTint[i])
applyTint(fg, o, cellTint[i])
continue
}
if (dots.size[i]) lightDot(dots, i, x, y, q, grid, GW, st, sb, curve)
if (octants) {
const octant = writeOctant(q, order, was < FIRST ? was : -1, olderMask[i], moving[i], char, fg, bg, i, o)
const octant = writeOctant(q, order, was >= 0 && was < FIRST ? was : -1, olderMask[i], moving[i], char, fg, bg, i, o)
// Soft cells keep their gradient band; still edges may take a smooth glyph instead of the octant, but not
// where something moves (it would flip between wedge and octant as it glides) or over the sun and moon.
previousMask[i] = smooth && !softFit && !round[i] && !moving[i] ? refineEdge(q, was, octant, char, fg, bg, i, o, x, y, deepR, deepG, deepB, st, sb) : octant
Expand All @@ -1248,8 +1264,8 @@ export function createEngine(
rememberFit(i, o, char, fg, bg, fitChar, fitFg, fitBg)
fitTop[i] = st
fitBottom[i] = sb
// A fit made while something moved (see tolerance) isn't reused next frame.
fitted[i] = moving[i] ? 0 : 1
// A fit made with a point lit in, or while something moved (see tolerance), isn't reused next frame.
fitted[i] = dots.size[i] || moving[i] ? 0 : 1
applyTint(bg, o, cellTint[i])
if (char[i] !== SPACE) applyTint(fg, o, cellTint[i])
continue
Expand Down
11 changes: 10 additions & 1 deletion src/orbs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// The sun and moon discs a scene's layout() paints, [cx, cy, r, ...] in pixels, per scene buffer (see Canvas.orbs):
// round shapes that the engine keeps smooth wedge glyphs off, since they would turn the disc into a polygon.
// round shapes that the engine keeps smooth wedge glyphs off, since they would turn the disc into a polygon, and that
// stars don't shine through.
const lists = new WeakMap<Float32Array, number[]>()

export function trackOrbs(hdr: Float32Array, list: number[]) {
Expand All @@ -9,3 +10,11 @@ export function trackOrbs(hdr: Float32Array, list: number[]) {
export function addOrb(hdr: Float32Array, cx: number, cy: number, r: number) {
lists.get(hdr)?.push(cx, cy, r)
}

// Whether (x, y) lies on an orb of hdr's canvas, or within margin pixels of one.
export function onOrb(hdr: Float32Array, x: number, y: number, margin = 1) {
const list = lists.get(hdr)
if (!list) return false
for (let i = 0; i < list.length; i += 3) if ((x - list[i]) ** 2 + (y - list[i + 1]) ** 2 <= (list[i + 2] + margin) ** 2) return true
return false
}
Loading
Loading