Skip to content

Add an abstract moon face to compare against the lunar photo#261

Merged
johannesjo merged 8 commits into
mainfrom
claude/noon-image-design-eq7l3t
Jul 25, 2026
Merged

Add an abstract moon face to compare against the lunar photo#261
johannesjo merged 8 commits into
mainfrom
claude/noon-image-design-eq7l3t

Conversation

@johannesjo

Copy link
Copy Markdown
Owner

The moon is the app's only photograph, in a UI that is otherwise gradients
and line icons. Its crater detail also gives the eye something to examine on
a surface whose whole job is somewhere to rest, and at the sizes the disc
actually renders - 80px for a phone intervention, ~42px for the resting
companion - that detail falls below the pixel grid and reads as grey noise,
i.e. a duller disc rather than a richer one.

So: an alternative face built from gradients alone, behind a
.minded-moon-abstract ancestor. Opt-in only - nothing in the app sets it,
only the styleguide, so the shipped moon is untouched until we pick one.

What it has to keep is that the moon reads unmistakably as the moon, never a
dimmer sun, or the evening cue it carries says nothing. The recognisable cue
at a glance is the maria shading, not the craters - but the tidy version of
that (a pair of fields up top, one below) becomes a smiley face at 42px, so
these are big overlapping asymmetric pools instead, following the real
near-side's lopsided look.

To compare:

  • Styleguide > Interactions > "Moon face - photo vs. abstract" puts both at
    the three sizes the disc really renders, on a fixed night backdrop (a moon
    can only be judged against a dark sky).
  • The sun stage below it gains a face toggle, since what actually decides
    this is how each face holds up through the morphs, not at rest.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01CbXxMDVD22xYwHq5pd4XtR

claude added 8 commits July 25, 2026 17:32
The moon is the app's only photograph, in a UI that is otherwise gradients
and line icons. Its crater detail also gives the eye something to examine on
a surface whose whole job is somewhere to rest, and at the sizes the disc
actually renders - 80px for a phone intervention, ~42px for the resting
companion - that detail falls below the pixel grid and reads as grey noise,
i.e. a duller disc rather than a richer one.

So: an alternative face built from gradients alone, behind a
`.minded-moon-abstract` ancestor. Opt-in only - nothing in the app sets it,
only the styleguide, so the shipped moon is untouched until we pick one.

What it has to keep is that the moon reads unmistakably as the moon, never a
dimmer sun, or the evening cue it carries says nothing. The recognisable cue
at a glance is the maria shading, not the craters - but the tidy version of
that (a pair of fields up top, one below) becomes a smiley face at 42px, so
these are big overlapping asymmetric pools instead, following the real
near-side's lopsided look.

To compare:
- Styleguide > Interactions > "Moon face - photo vs. abstract" puts both at
  the three sizes the disc really renders, on a fixed night backdrop (a moon
  can only be judged against a dark sky).
- The sun stage below it gains a `face` toggle, since what actually decides
  this is how each face holds up *through* the morphs, not at rest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbXxMDVD22xYwHq5pd4XtR
The gradient moon was the original look, and the history says it lost twice
already: #92 replaced it with a photo and was reverted by #93; #95 landed a
photo again with a better image, explicitly because the first one "read as a
generic gray cratered sphere rather than the recognizable full moon" - the
bar the .moon-face comment sets.

Rendering all three side by side at 120/80/42px on a night backdrop settles
it: the gradient face is a pale blob with soft smudges, and it is no better
at the 42px companion size, which was the whole argument for building it. The
photo's real structure does work that soft shading cannot fake.

So revert b6e892a and spend it on a comment instead, since the reasoning for
the photo lived in commit messages and PR history where nobody reading
Sun.scss would find it - which is how it got re-proposed in the first place.

This reverts commit b6e892a, keeping only the note.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbXxMDVD22xYwHq5pd4XtR
I reverted this a commit ago after reading "looks like before" as a verdict on
the design. It wasn't - the preview build showed no change, which was the
actual complaint. Two reasons it showed nothing, both mine:

- The revert had already landed, so the preview at that point was a comment
  and nothing else.
- Before that, the gallery was a Subsection buried inside Interactions, with
  no TOC entry. On a page this long that is easy to miss entirely.

So: restore it, and give it its own top-level "Moon face" section with a TOC
chip, since the preview root *is* the styleguide and this is the one thing on
the branch meant to be looked at.

Two changes beyond a plain restore:

- Add the pre-#92 gradient moon as a third column (`.legacy-moon`, lifted
  verbatim from 660d89a^). It is the control this comparison was missing:
  the photo replaced it, and #95 rejected a gradient-ish photo again for
  missing the "unmistakably the moon" bar. Anyone proposing a gradient moon
  should see what lost, at real size, first. The section copy states that
  history so the page argues its own case.
- Lay the gallery out as a matrix - faces across, sizes down - instead of
  three cards. The styleguide caps at 1100px, so three cards wrap and you end
  up comparing 120px against 42px a screen apart. In a matrix the comparison
  that matters reads along one row.

Verified against a real `npm run styleguide:build` + headless load, not just
the unit tests: section renders, TOC chip present, no page errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbXxMDVD22xYwHq5pd4XtR
Not behind a flag - this is the moon now, on every web surface: the
companion at rest, interventions, the breath, urge-surfing, the wind-down.

The photo was richer at 120px, but it was the app's only photograph in a UI
that is otherwise gradients and line icons - a style seam on the one object
meant to read as continuous across every surface - and its crater detail gave
the eye something to examine on a surface whose whole job is somewhere to
rest. At the size the disc actually spends most of its life (~42px resting on
the bottom bar) that detail is below the pixel grid anyway.

The face is a body whose light pools up-left and cools into the lower-right
limb, under wide overlapping tonal fields where the real near-side's maria
sit. Two constraints, both learned by getting them wrong first and recorded
in the comment so the next attempt doesn't:

- The maria must be big, overlapping and asymmetric. The obvious layout - a
  pair up top, one below - reads as a smiley face at 42px. That is also what
  the pre-#92 gradient moon did, where it read as polka dots.
- Below roughly 7% luminance contrast the disc stops reading as a moon and
  just looks like a smudged pearl.

Also drops what the photo cost: the ~20KB asset and the module-load preload
from #246, which existed only because the first paint was a blank white disc
before the texture arrived. Nothing fetches an image for the moon now.

Still on the photo: the two home-screen widgets (Android
ic_sun_widget_night.webp, iOS MoonWidget). Android's native LittleSun already
draws a flat cool disc, so it is unaffected. The widgets need the same
gradients ported to a VectorDrawable and SwiftUI to keep one moon everywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbXxMDVD22xYwHq5pd4XtR
The app's moon stopped being a photograph in the previous commit, which left
the two home-screen widgets still shipping the lunar photo - a photo moon on
the home screen against a drawn one in the app reads as two different objects,
and there is only ever one moon.

Android: ic_sun_widget_night.webp -> a vector, ic_sun_widget_night.xml. The
maria are ported by mapping Sun.scss's percentages onto the disc's box
(x = 18 + pct * 72 in the 108 viewport). A VectorDrawable radial gradient is
always circular, so each mare is a circle of radius ry squashed by its group's
scaleX about its own centre. No clip-path: every gradient reaches zero inside
the rim (the widest, Procellarum, dies at x=19 against the disc edge at 18),
so a clip would cost work on every frame and cut nothing.

iOS: the same numbers again in SwiftUI, as unit fractions of the disc, using
the same squashed-circle trick via scaleEffect. Drops the MoonWidget image set
and with it Media.xcassets, which existed only to hold that photo - so the
widget now ships no disc images at all, only the card skies.

The halo stays cool on both. That is THE HALO RULE's documented exception: the
day sun goes amber out here because it floats on the user's wallpaper and has
to announce itself, but the moon never warms, on any surface.

Also swept up what the photo left behind:

- The styleguide's widget preview generator only understood flat <path> lists,
  so it would have drawn the maria as circles - the one place the preview could
  silently disagree with the device. It now honours <group> scale/pivot as an
  SVG transform, and night converts from its vector like day always has.
- The content script's asset-URL rewriter is gone. It repointed url(/assets/webp/)
  at the extension origin so the moon wouldn't render as a see-through hole on
  host pages; moon.webp was the only webp, so it now rewrites nothing.
- Comments that describe the moon as a photograph: the MOON_REST_GLOW rationale
  in Sun.tsx (the floor is kept deliberately, not inherited by accident), the
  iOS README and add_widget_target.rb resource list, docs/sun-halo.md and
  docs/sun-companion-widget.md.

Verified by rendering the generated SVG of the real vector on light and dark
backdrops, and the styleguide's widget gallery from an actual styleguide:build.
The Swift is unverified by a compiler - there is no toolchain here - so it wants
one glance from the macOS CI archive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbXxMDVD22xYwHq5pd4XtR
The conclusion was right but the stated reason was false, and a comment that
asserts a checkable fact wrongly is worse than no comment.

It read: "every gradient fades to zero well inside the disc (the widest,
Procellarum, dies at x=19 against the disc edge at 18)". That compares two
points at different y - at Procellarum's y=50.4 the rim is at x=18.18, not 18 -
and the closest approach isn't on the x-axis anyway. Sweeping the whole
zero-alpha ellipse (centre 33.12,50.4, semi-axes 14.04 x 21.34) against the
r=36 disc, it reaches 36.065 at about 220 degrees: 0.065 units OUTSIDE the rim.

Skipping clip-path is still correct - that boundary is alpha 0 by construction,
so the overhang is under 0.001 opacity across 0.06% of the tile and cannot
render at any size, while a clip-path would cost a saveLayer every frame. Say
that instead, and also note two things the old comment left implicit: the mare
paths overhang the disc on purpose (hidden only because tileMode clamps past
the last stop), and iOS clips where Android doesn't, deliberately.

Found by review; no rendering change, comment only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbXxMDVD22xYwHq5pd4XtR
Two review passes over the branch. No build-breaking defects and no web
regression; the geometry was re-derived independently and all 42 numbers agree
across the three platforms. What they did find:

Stale comments that still call the moon a photograph. The worst was
_variables.scss, where it was load-bearing: --little-sun-bg justified diverging
from the in-app moon because craters would hurt the timer number's legibility.
That reason is gone, and what is left is two similar gradients on an object the
app insists is one continuous thing across the corner hand-off. Recorded as the
open question it now is rather than quietly reconciling them - this disc needs a
flat field behind a number, which the face's maria are not, so it should be
decided on the hand-off, not on either disc alone. Also two "photo disc" lines
in Sun.tsx.

A mirror test for the moon's face. The change duplicated 7 maria x 6 values into
an Android vector and a Swift array under two different mappings, with nothing
holding them in step - and this repo already guards exactly that kind of
cross-language duplication (sunHaloMirror, widgetPromptsMirror). It pins both
ports to Sun.scss, and additionally guards the two constraints that were learned
by getting them wrong: no two maria may mirror each other (a symmetric layout
reads as a smiley face at companion size) and none may fall under the contrast
floor. Verified it fails on a seeded drift, not just that it passes.

Generator hardening. Its tokenizer only matched paired <path>...</path>, so a
self-closing path would have run on to the next </path>, swallowing any groups
between and pairing one path's geometry with another's gradient - silently, as a
plausible-looking wrong SVG. Other drawables in this repo already use that form.
Also keeps a group stack instead of a single slot (a nested group dropped its
parent's transform rather than restoring it) and handles rotation/translate
instead of discarding them. Output is byte-identical.

Put back a bug the diff erased. The deleted URL-rewriting helper carried the only
record that root-relative url(/assets/…) 404s inside the content script's shadow
root. That is still live for the two woff2 faces, so Inter and Newsreader fall
back to the system stack inside an intervention overlay - and Newsreader is the
voice the app speaks in. Pre-existing, not caused here, but the knowledge went
with the comment, so it is now recorded at the injection site with the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbXxMDVD22xYwHq5pd4XtR
@johannesjo
johannesjo merged commit 0aa36bd into main Jul 25, 2026
3 checks passed
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