Skip to content

Add calendar-driven geometric background texture - #52

Closed
ewanc26 wants to merge 9 commits into
mainfrom
feature/fractal-background
Closed

ewanc26 wants to merge 9 commits into
mainfrom
feature/fractal-background

Conversation

@ewanc26

@ewanc26 ewanc26 commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds a full-spread background texture: flowing contour lines, like the lines of a relief map, behind the page. A date-driven "degree" tilts the whole field by that many degrees and sets how far it morphs, from long parallel wavy bands into a lattice of closed loops, as the day, month, season and Moon change. It is recalculated in the browser on load and every minute, and redrawn when the window is resized.

Nothing ever switches. The texture is a continuous function of the degree, so it can only transition: lines creep, bend, pinch off and merge. There are no tiles to flip, no grid, and no seams. Lines never cross.

The degree is varied. Over a year, smoothed over a week, it sweeps from about 21° to 44° (raw: about 19° to 51°), so the texture looks clearly different from season to season.

Everything is anchored on 22 August. The year starts on 22 Aug, and every "month" runs from one 22nd to the next. The day-of-month, month and season rules all count from that anchor, set by one constant, ANCHOR, in texture.ts. The week, the hour, the Moon and the Sabbats are real and do not move. The solstice and equinox Sabbats (21 Mar, 21 Jun, 21 Sep, 21 Dec) fall on the day before a 22nd, so they land on the day-of-month rule's rising edge and stack with it, which gives the year a regular rhythm of peaks.

Rules (start at 30°; each adds its delta × its weight; in DEGREE_RULES in src/lib/utils/texture.ts):

Rule Δ Window (its weight is 1 inside, eases to 0 at the edges)
day ≤ 15 +6 the first 15 days of the month (the 22nd to the 6th/7th), edges ease over 3 days
month ≤ 6 −6 the first six months of the year (22 Aug to 21 Feb), edges ease over about 3 weeks
season ≤ 2 / season ≥ 3 +3 / −3 the first / second half of the year, in quarters from 22 Aug (add up to exactly 1)
Monday −2 Monday, easing over a day
weekend +1 Saturday and Sunday
hour < 12 / ≥ 12 +1 / −1 morning / afternoon, easing over 4 hours (add up to 1)
moon waxing / waning +1 / −1 first / second half of the cycle (add up to 1)
full moon +2 swells as the lit fraction goes from 85% to 99%
Sabbat day +12 a swell that peaks on the day and fades within 4 days either side

The slow rules (day of month, month, season, Sabbat) are strong, so the year has a clear shape; the fast ones (weekday, hour, Moon) are gentler, so they add life without making each day jerk. The rules add up to a swing of −13 to +26 around 30°. The "season" in a rule's name is a quarter of the anchored year; the season labels shown in the preview stay the real northern-hemisphere ones.

The texture

  • One smooth field. A main wave plus two more at 60° and 120° that fade in with the morph (all three at full weight make a hexagonal lattice of blobs), seen through a small domain warp so the lines flow instead of forming a regular grid.
  • Degree → tilt and morph. The degree turns the field about the window centre (its own angle, in degrees, clamped to 0–60°) and sets the morph from 0 at 10° to 1 at 50°. At about 21° it is long wavy bands; at about 35° it is a regular lattice of nested loops.
  • Contour lines. Marching squares on a grid of about wavelength/16 (about 10px), at six fixed levels, chained into polylines and drawn as smooth quadratic curves. The levels skip 0, where the lattice's saddles sit, so lines meet only at the instant a band pinches off.
  • Never crosses. Contours of a field with one value at each point cannot cross, whatever the angle or morph.
  • The whole texture is one SVG path (about 100–200 KB depending on the window), built in about 10 ms.

Low-key, part of the paper: drawn in the theme's --ink at 6% opacity, so it reads as an impression in the paper in light and dark. It sits above the body paper but below .shell-main, ignores pointer events, and is hidden under prefers-contrast: more and forced colours.

This replaces the earlier fractal-tree, nested-square and arc-tile versions on this branch. The tile versions had to pick an orientation per tile, so even with a cross-fade a tile still flipped.

Smoothness

  • Smooth rules. Each rule is a 0–1 weight on continuous time. smoothWindow is a periodic window whose edges ease along Perlin's smootherstep (zero first and second derivatives at both ends, see Smoothstep). It depends only on distance from the window's centre, so it is continuous everywhere including where a cycle wraps. A rule keeps its original meaning inside its window; opposite windows sum to exactly 1.
  • Continuous across the anchor. Positions are measured in milliseconds from the last 22nd, so they stay continuous across month and year boundaries (a month is 28 to 31 days; the windows wrap on the cycle length).
  • A continuous Moon. The existing getMoonIllumination().phase jumps by up to about 0.03 at new and full moon (it picks waxing or waning from the sign of an angle that flips while the Moon is still a few degrees off the Sun–Earth line). This PR adds getMoonCycle to moonPhase.ts (the Moon's ecliptic longitude minus the Sun's), which never jumps. The existing functions are untouched.
  • Time-lapse averaging. averageDegreeFor averages the degree over a span (sampled every 6 hours) for fast playback, where a weekly rule would otherwise swing the degree more than once a second. A 7-day span cancels the weekly rhythm exactly. The live site uses the raw degree.

At these strengths the largest change in the degree over ten minutes, across all of 2026, is 0.22° (a step function would jump by at least 1°), which is under 0.03° a minute at the site's one-minute refresh.

Changes

  • src/lib/utils/texture.ts: anchored calendar, smooth rules, the field, contour lines, averaging
  • src/lib/utils/moonPhase.ts: adds getMoonCycle (existing functions unchanged)
  • src/lib/components/TextureBackground.svelte: the texture component (client-only, so no hydration mismatch)
  • src/routes/+layout.svelte: mounts the component
  • src/lib/utils/texture.test.ts and a test:texture script

Testing

  • pnpm test:texture: 22 tests pass in about two seconds. They cover smootherstep's flat ends; windows being 1 inside, 0 outside and continuous (including the wrap and half-period case); opposite windows summing to exactly 1; the anchored calendar (the year starts at midnight on 22 Aug, the second before is the end of the year before, every month starts on the 22nd, positions stay continuous); each rule keeping its original meaning mid-window; the degree never moving more than 0.3° in ten minutes through a whole year; the moon cycle moving steadily with no jump at new or full moon; a 7-day average cancelling the weekly rhythm; the degree being varied (more than 20° of smoothed range over the year, and most of the morph); the field staying within [−1, 1]; a 0.01° change in the degree moving every point of the field, and every contour line, by only a hair; no two contour lines crossing at any degree in the range; every line sitting on its level; open lines running off the window; and the window being fully covered.
  • Mutation-checked: a hard switch in the morph fails the continuity test; a miswired marching-squares case fails five tests; the old, gentler strengths fail the variety test. The continuity test found the Moon quirk above on its first run.
  • Component compiles with Svelte 5.57.1 with no warnings; tsc --strict passes on the module; new and changed files are Prettier-clean.
  • Rendered the texture at desktop and phone widths in light and dark, and at four dates across the year, to check it reads as flowing geometry, that the seasons look clearly different, and that text stays legible over it. Played the year at its fastest speed in a real browser at a steady 60 fps.
  • Not run: pnpm check and pnpm build, because pnpm install could not fetch the atmentions GitHub dependency in my sandbox (403). Please let CI confirm these.

+layout.svelte already fails Prettier on main, so I only added the two lines I needed rather than reformatting it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VM4gJUQX6c2kEEuFLdEQW2

A fixed SVG binary tree sits behind the page content. Its branch angle (the
"degree") is the sum of date rules: day, month, season, weekday, hour, moon
phase, full moon and Sabbat days. It refreshes every minute in the browser.

Branches never overlap: the shrink ratio is 0.5 and the angle is clamped to a
range the tests verify by brute force. The drawing is a faint ink impression
(6% opacity, theme ink colour) and is hidden under prefers-contrast: more.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM4gJUQX6c2kEEuFLdEQW2
@vercel

vercel Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
website Ready Ready Preview Oct 16, 2026 1:51pm UTC

Replace the single bottom-rooted tree with an SVG pattern that covers the
whole viewport. Each tree is scaled to sit inside its own padded cell and the
cell width follows the tree's proportions, so trees pack tightly. Rows are
offset by half a cell (brick layout) and cells never intersect, so neither
branches nor neighbouring trees overlap. The wallpaper re-tiles on resize.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM4gJUQX6c2kEEuFLdEQW2
@ewanc26 ewanc26 changed the title Add calendar-driven fractal paper background Add calendar-driven fractal wallpaper background Oct 16, 2026
Link each tree to its neighbours so the wallpaper reads as part fractal, part
geometric pattern. Crowns meet their row neighbours at their outermost tips,
and each trunk base forks to the two nearest crown tips in the next row,
closing a triangle with the row link between them. Links leave each tree from
its extreme left, right or base point, heading away from it, so they cannot
cross a branch. Links that cross the tile edge are drawn in full plus a
wrapped copy so the pattern is seamless.

Cells are 1.5x the tree width (CELL_SPREAD) so the triangles are wide enough to
read; tests cover non-crossing, connectivity, and fail against mutated links.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM4gJUQX6c2kEEuFLdEQW2
@ewanc26 ewanc26 changed the title Add calendar-driven fractal wallpaper background Add calendar-driven fractal lattice wallpaper Oct 16, 2026
Replace the fractal tree wallpaper with a geometric texture. Each cell holds
concentric squares, each turned by the date-driven degree from the one
outside it and scaled so its corners land exactly on its parent's edges.
Lines therefore only meet at corners and never cross at any angle. Neighbouring
cells turn in opposite directions (a 2x2 tile), and the tile is shifted half a
pixel so the cell borders render crisp.

The date rules (day, month, season, weekday, hour, Moon, Sabbats) are unchanged
and still set the degree. Files are renamed fractal -> texture to match, and
the tests cover the inscribed geometry, containment, and no crossings across a
3x3 block of tiles, and fail against deliberately broken geometry.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM4gJUQX6c2kEEuFLdEQW2
@ewanc26 ewanc26 changed the title Add calendar-driven fractal lattice wallpaper Add calendar-driven geometric background texture Oct 16, 2026
The nested-square cells read as a grid. Replace them with Truchet-style arc
tiles: each tile holds six quarter-circle arcs that meet every edge at the
same three points, so curves run on unbroken across tiles and no cell borders
are ever drawn. Each tile's flip is a fixed hash of its position.

The date-driven degree now tilts the whole field by that many degrees and sets
the share of flipped tiles, sliding the texture between long ordered diagonal
waves and a looping maze. Arc spacing keeps opposite-corner arcs clear of each
other, so no two arcs ever cross. Tiles are generated to cover the tilted
window rather than repeated from an SVG pattern.

Tests cover the edge points, continuity across tile edges, no crossing or
near-touching between arcs, window coverage at any tilt, and the flip share,
and fail against deliberately broken geometry.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM4gJUQX6c2kEEuFLdEQW2
The degree jittered. Each rule was an on/off switch (day <= 15, hour < 12,
moon waxing), so the degree jumped whenever one flipped, and tiles popped
between orientations as the flip share crossed their hash.

Rules are now smooth weights from 0 to 1 on continuous time. Each is a window
on its own cycle whose edges ease along Perlin's smootherstep, so a rule keeps
its original meaning inside its window and fades in and out at the edges.
Windows that are opposites (season <= 2 / >= 3, hour < 12 / >= 12, moon
waxing / waning) add up to exactly 1. The Sabbat rule is a swell that peaks on
the day and fades within four days either side.

The Moon's waxing/waning phase from getMoonIllumination jumps by up to about
0.03 at new and full moon (it picks a side from the sign of an angle that flips
while the Moon is still off the Sun-Earth line). Add getMoonCycle, the
difference of the Moon's and Sun's ecliptic longitudes, which never jumps.

Tiles now cross-fade between orientations as the share passes their hash. A
tile's two layers sum to full strength, so density stays steady.

averageDegreeFor averages the degree over a span for time-lapse playback,
where a weekly rule would otherwise flicker.

Over a year at ten-minute steps the largest change fell from 4.5 degrees (769
steps over 0.2) to 0.095 degrees (none).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM4gJUQX6c2kEEuFLdEQW2
Tiles had to pick an orientation, so even with a cross-fade a tile still
flipped. Replace the tile arcs with the contour lines of one smooth field: a
main wave plus two more at 60 and 120 degrees that fade in with the morph (all
three make a hexagonal lattice of blobs), seen through a small domain warp.
The degree tilts the field and sets the morph, from parallel wavy bands at the
low end to closed loops at the high end. The field is a continuous function of
the degree, so the texture can only transition.

Lines come from marching squares on a grid, chained into polylines and drawn as
smooth quadratic curves. Lines at different levels cannot cross, because the
field has one value at each point; level 0, where the lattice's saddles sit, is
not drawn, so lines meet only at the instant a band pinches off.

There is no grid and no tile edge. The site component is one path.

Tests: the field is bounded; a 0.01 degree change moves every point and every
line by a hair (a hard switch in the morph fails this); no two lines cross at
any degree in the range; every line sits on its level; open lines run off the
window; the window is fully covered. Both a hard flip and a miswired
marching-squares case fail the suite.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM4gJUQX6c2kEEuFLdEQW2
The smoothed degree only ranged from 18.6 to 22.4 over a year, so the texture
looked nearly the same all year. Strengthen the rules and widen the range.

The slow rules (day of month, month, season, Sabbat) are six times stronger,
so the year has a clear shape; the fast rules (weekday, hour, Moon) are only
twice as strong, so they add life without making each day jerk. The base moves
from 20 to 30 degrees, the tilt range widens to 0-60, and the morph from bands
to loops now runs over 10-50 degrees. The rules still add up to a swing of -13
to +26, so the degree runs from the high teens to the mid fifties.

Smoothed over a week, the degree now sweeps 21 to 44 degrees across the year
(standard deviation 4.2, up from 0.8), and the morph covers most of its range.
The largest change in ten minutes is 0.22 degrees (a step function would jump
by at least a degree), which is under 0.03 degrees a minute.

A new test pins the variety so it cannot quietly shrink again; it fails against
the old strengths.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM4gJUQX6c2kEEuFLdEQW2
The year now starts on 22 August and every month runs from one 22nd to the
next, so the day-of-month, month and season rules all count from the anchor.
The week, hour, Moon and Sabbats are real and do not move. The anchor is one
constant (ANCHOR) in texture.ts.

The season rules are quarters of the anchored year; the season labels shown in
the preview stay the real northern-hemisphere ones.

Positions are measured in milliseconds from the last 22nd, so they stay
continuous across month and year boundaries (a month is 28 to 31 days; the
windows wrap on the cycle length). The solstice and equinox Sabbats, which fall
on the 21st, now land on the day-of-month rule's rising edge and stack with it,
so the year has a regular rhythm of peaks. The smoothed degree still spans about
21 to 44 degrees, and the largest ten-minute change is unchanged.

Tests cover the anchored positions (the year starts at midnight on 22 Aug, the
second before is the end of the year before, every month starts on the 22nd)
and move each rule's plateau dates onto the new months.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM4gJUQX6c2kEEuFLdEQW2

This branch was successfully deployed

1 active deployment
Preview — 9c61a7f3 Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants