From 96bc42fafff425cf7690ad417cc6d4d4a757547b Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 19 Jul 2026 15:44:24 -0500 Subject: docs: file interface design references Record the reconciled weather work and future component direction.\nFile the clock and retro-interface reference materials for later design work. --- .../2026-07-17-clock-strip-concept-01.png | Bin 0 -> 1639047 bytes .../2026-07-17-clock-strip-concept-02.png | Bin 0 -> 1611392 bytes .../2026-07-17-clock-strip-concept-03.png | Bin 0 -> 1351841 bytes .../2026-07-17-clock-strip-concept-04.png | Bin 0 -> 1631303 bytes .../2026-07-17-clock-strip-concept-05.png | Bin 0 -> 1157684 bytes .../2026-07-17-clock-strip-variant-sheet-01.png | Bin 0 -> 1717647 bytes .../2026-07-17-clock-strip-variant-sheet-02.png | Bin 0 -> 1828740 bytes .../2026-07-17-clock-strip-variant-sheet-03.png | Bin 0 -> 1828740 bytes .../2026-07-18-chronograph-audemars-moonphase.png | Bin 0 -> 527486 bytes ...6-07-18-chronograph-blancpain-fifty-fathoms.png | Bin 0 -> 697946 bytes .../2026-07-18-chronograph-breitling-navitimer.png | Bin 0 -> 301267 bytes .../2026-07-18-chronograph-date-idea-detail.png | Bin 0 -> 29433 bytes .../2026-07-18-chronograph-date-idea.png | Bin 0 -> 1819745 bytes .../2026-07-18-chronograph-date-window-detail.png | Bin 0 -> 22156 bytes .../2026-07-18-chronograph-iwc-portugieser.png | Bin 0 -> 192781 bytes .../2026-07-18-chronograph-omega-speedmaster.png | Bin 0 -> 491327 bytes ...26-07-18-chronograph-panerai-luminor-marina.png | Bin 0 -> 128425 bytes .../2026-07-18-chronograph-patek-moonphase.png | Bin 0 -> 532449 bytes ...-07-18-chronograph-patek-perpetual-calendar.png | Bin 0 -> 198977 bytes .../2026-07-18-chronograph-patek-two-register.png | Bin 0 -> 175575 bytes .../2026-07-18-chronograph-reference-notes.txt | 20 + ...2026-07-18-chronograph-rolex-cyclops-detail.png | Bin 0 -> 28722 bytes ...026-07-18-chronograph-rolex-submariner-date.png | Bin 0 -> 492099 bytes working/clock-display-references/README.org | 6 + ...26-07-18-dual-concentric-knob-black-detail.jpeg | Bin 0 -> 11473 bytes .../2026-07-18-dual-concentric-knob-black.jpg | Bin 0 -> 13108 bytes ...6-07-18-dual-concentric-knob-fender-detail.jpeg | Bin 0 -> 12294 bytes .../2026-07-18-dual-concentric-knob-fender.webp | Bin 0 -> 95620 bytes ...6-07-18-dual-concentric-knob-guitar-detail.jpeg | Bin 0 -> 8710 bytes ...2026-07-18-dual-concentric-knob-guitar-pair.jpg | Bin 0 -> 88208 bytes ...026-07-18-dual-concentric-knob-style-sheet.jpeg | Bin 0 -> 4645 bytes ...2026-07-18-dual-concentric-knob-tone-volume.jpg | Bin 0 -> 7654 bytes .../2026-07-18-hotfx-split-flap-reference.org | 65 +++ .../2026-07-18-hotfx-split-flap-source.js | 455 +++++++++++++++++++++ .../references/2026-07-18-single-knob-black.webp | Bin 0 -> 33262 bytes .../2026-07-18-single-knob-index-arrow.jpeg | Bin 0 -> 3283 bytes .../references/2026-07-18-split-flap-header.webp | Bin 0 -> 44808 bytes .../references/2026-07-18-terminal-display.jpg | Bin 0 -> 4522828 bytes working/weather-kit-integration/README.org | 119 ++++++ working/weather-kit-integration/weather | 215 ++++++++++ working/weather-kit-integration/weather-chip.html | 97 +++++ working/weather-kit-integration/weather-glyphs.svg | 81 ++++ 42 files changed, 1058 insertions(+) create mode 100644 working/clock-display-references/2026-07-17-clock-strip-concept-01.png create mode 100644 working/clock-display-references/2026-07-17-clock-strip-concept-02.png create mode 100644 working/clock-display-references/2026-07-17-clock-strip-concept-03.png create mode 100644 working/clock-display-references/2026-07-17-clock-strip-concept-04.png create mode 100644 working/clock-display-references/2026-07-17-clock-strip-concept-05.png create mode 100644 working/clock-display-references/2026-07-17-clock-strip-variant-sheet-01.png create mode 100644 working/clock-display-references/2026-07-17-clock-strip-variant-sheet-02.png create mode 100644 working/clock-display-references/2026-07-17-clock-strip-variant-sheet-03.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-audemars-moonphase.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-blancpain-fifty-fathoms.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-breitling-navitimer.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-date-idea-detail.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-date-idea.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-date-window-detail.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-iwc-portugieser.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-omega-speedmaster.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-panerai-luminor-marina.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-patek-moonphase.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-patek-perpetual-calendar.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-patek-two-register.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-reference-notes.txt create mode 100644 working/clock-display-references/2026-07-18-chronograph-rolex-cyclops-detail.png create mode 100644 working/clock-display-references/2026-07-18-chronograph-rolex-submariner-date.png create mode 100644 working/clock-display-references/README.org create mode 100644 working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-black-detail.jpeg create mode 100644 working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-black.jpg create mode 100644 working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-fender-detail.jpeg create mode 100644 working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-fender.webp create mode 100644 working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-guitar-detail.jpeg create mode 100644 working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-guitar-pair.jpg create mode 100644 working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-style-sheet.jpeg create mode 100644 working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-tone-volume.jpg create mode 100644 working/retro-stereo-widgets/references/2026-07-18-hotfx-split-flap-reference.org create mode 100644 working/retro-stereo-widgets/references/2026-07-18-hotfx-split-flap-source.js create mode 100644 working/retro-stereo-widgets/references/2026-07-18-single-knob-black.webp create mode 100644 working/retro-stereo-widgets/references/2026-07-18-single-knob-index-arrow.jpeg create mode 100644 working/retro-stereo-widgets/references/2026-07-18-split-flap-header.webp create mode 100644 working/retro-stereo-widgets/references/2026-07-18-terminal-display.jpg create mode 100644 working/weather-kit-integration/README.org create mode 100755 working/weather-kit-integration/weather create mode 100644 working/weather-kit-integration/weather-chip.html create mode 100644 working/weather-kit-integration/weather-glyphs.svg (limited to 'working') diff --git a/working/clock-display-references/2026-07-17-clock-strip-concept-01.png b/working/clock-display-references/2026-07-17-clock-strip-concept-01.png new file mode 100644 index 0000000..6cd91c8 Binary files /dev/null and b/working/clock-display-references/2026-07-17-clock-strip-concept-01.png differ diff --git a/working/clock-display-references/2026-07-17-clock-strip-concept-02.png b/working/clock-display-references/2026-07-17-clock-strip-concept-02.png new file mode 100644 index 0000000..2edcebe Binary files /dev/null and b/working/clock-display-references/2026-07-17-clock-strip-concept-02.png differ diff --git a/working/clock-display-references/2026-07-17-clock-strip-concept-03.png b/working/clock-display-references/2026-07-17-clock-strip-concept-03.png new file mode 100644 index 0000000..fb14c0c Binary files /dev/null and b/working/clock-display-references/2026-07-17-clock-strip-concept-03.png differ diff --git a/working/clock-display-references/2026-07-17-clock-strip-concept-04.png b/working/clock-display-references/2026-07-17-clock-strip-concept-04.png new file mode 100644 index 0000000..d6bdd15 Binary files /dev/null and b/working/clock-display-references/2026-07-17-clock-strip-concept-04.png differ diff --git a/working/clock-display-references/2026-07-17-clock-strip-concept-05.png b/working/clock-display-references/2026-07-17-clock-strip-concept-05.png new file mode 100644 index 0000000..a2d3dda Binary files /dev/null and b/working/clock-display-references/2026-07-17-clock-strip-concept-05.png differ diff --git a/working/clock-display-references/2026-07-17-clock-strip-variant-sheet-01.png b/working/clock-display-references/2026-07-17-clock-strip-variant-sheet-01.png new file mode 100644 index 0000000..6ae249a Binary files /dev/null and b/working/clock-display-references/2026-07-17-clock-strip-variant-sheet-01.png differ diff --git a/working/clock-display-references/2026-07-17-clock-strip-variant-sheet-02.png b/working/clock-display-references/2026-07-17-clock-strip-variant-sheet-02.png new file mode 100644 index 0000000..eb82fd1 Binary files /dev/null and b/working/clock-display-references/2026-07-17-clock-strip-variant-sheet-02.png differ diff --git a/working/clock-display-references/2026-07-17-clock-strip-variant-sheet-03.png b/working/clock-display-references/2026-07-17-clock-strip-variant-sheet-03.png new file mode 100644 index 0000000..eb82fd1 Binary files /dev/null and b/working/clock-display-references/2026-07-17-clock-strip-variant-sheet-03.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-audemars-moonphase.png b/working/clock-display-references/2026-07-18-chronograph-audemars-moonphase.png new file mode 100644 index 0000000..bbf3404 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-audemars-moonphase.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-blancpain-fifty-fathoms.png b/working/clock-display-references/2026-07-18-chronograph-blancpain-fifty-fathoms.png new file mode 100644 index 0000000..f4026c0 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-blancpain-fifty-fathoms.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-breitling-navitimer.png b/working/clock-display-references/2026-07-18-chronograph-breitling-navitimer.png new file mode 100644 index 0000000..e518b2f Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-breitling-navitimer.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-date-idea-detail.png b/working/clock-display-references/2026-07-18-chronograph-date-idea-detail.png new file mode 100644 index 0000000..7f3c8a9 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-date-idea-detail.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-date-idea.png b/working/clock-display-references/2026-07-18-chronograph-date-idea.png new file mode 100644 index 0000000..8bc8a4b Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-date-idea.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-date-window-detail.png b/working/clock-display-references/2026-07-18-chronograph-date-window-detail.png new file mode 100644 index 0000000..4ae7212 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-date-window-detail.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-iwc-portugieser.png b/working/clock-display-references/2026-07-18-chronograph-iwc-portugieser.png new file mode 100644 index 0000000..d9d4616 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-iwc-portugieser.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-omega-speedmaster.png b/working/clock-display-references/2026-07-18-chronograph-omega-speedmaster.png new file mode 100644 index 0000000..c62bf25 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-omega-speedmaster.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-panerai-luminor-marina.png b/working/clock-display-references/2026-07-18-chronograph-panerai-luminor-marina.png new file mode 100644 index 0000000..b5a8d97 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-panerai-luminor-marina.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-patek-moonphase.png b/working/clock-display-references/2026-07-18-chronograph-patek-moonphase.png new file mode 100644 index 0000000..a701931 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-patek-moonphase.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-patek-perpetual-calendar.png b/working/clock-display-references/2026-07-18-chronograph-patek-perpetual-calendar.png new file mode 100644 index 0000000..11a5ac8 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-patek-perpetual-calendar.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-patek-two-register.png b/working/clock-display-references/2026-07-18-chronograph-patek-two-register.png new file mode 100644 index 0000000..3cfa626 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-patek-two-register.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-reference-notes.txt b/working/clock-display-references/2026-07-18-chronograph-reference-notes.txt new file mode 100644 index 0000000..b94cafe --- /dev/null +++ b/working/clock-display-references/2026-07-18-chronograph-reference-notes.txt @@ -0,0 +1,20 @@ +Chronograph / watch-face design inspiration — 12 reference images. + +Collected by Craig and dropped from a home session on 2026-07-18 for the +chronograph / clock-panel UI work. Each file is chronograph-inspiration-NN-.png. + +Identified: +01 Blancpain Fifty Fathoms +02 Omega Speedmaster Professional (Moonwatch) +03 Rolex Submariner Date +05 Panerai Luminor Marina +07 IWC Portugieser Chronograph +08 Breitling Navitimer +09 Patek Philippe perpetual calendar chronograph (resembles ref 1518) +10 Audemars Piguet perpetual calendar moonphase +11 Patek Philippe two-register chronograph (resembles ref 130) +12 Patek Philippe perpetual calendar moonphase (3940/5140 family) + +Uncertain from the crop (relabel if the model is known): +04 dial date-window detail — model unidentified +06 Rolex fluted bezel + Cyclops date — Datejust or Day-Date, not certain diff --git a/working/clock-display-references/2026-07-18-chronograph-rolex-cyclops-detail.png b/working/clock-display-references/2026-07-18-chronograph-rolex-cyclops-detail.png new file mode 100644 index 0000000..1050032 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-rolex-cyclops-detail.png differ diff --git a/working/clock-display-references/2026-07-18-chronograph-rolex-submariner-date.png b/working/clock-display-references/2026-07-18-chronograph-rolex-submariner-date.png new file mode 100644 index 0000000..e7bd485 Binary files /dev/null and b/working/clock-display-references/2026-07-18-chronograph-rolex-submariner-date.png differ diff --git a/working/clock-display-references/README.org b/working/clock-display-references/README.org new file mode 100644 index 0000000..f3b9bf2 --- /dev/null +++ b/working/clock-display-references/README.org @@ -0,0 +1,6 @@ +#+TITLE: Clock Display Reference Set + +Clock and chronograph reference material received during the 2026-07-19 inbox +zero pass. The concept renders establish the compact date/time-strip direction; +the watch photographs are reference-only for dial hierarchy, date treatment, +and complication spacing. They are not assets to reproduce or redistribute. diff --git a/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-black-detail.jpeg b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-black-detail.jpeg new file mode 100644 index 0000000..d723827 Binary files /dev/null and b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-black-detail.jpeg differ diff --git a/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-black.jpg b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-black.jpg new file mode 100644 index 0000000..8b2ed8e Binary files /dev/null and b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-black.jpg differ diff --git a/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-fender-detail.jpeg b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-fender-detail.jpeg new file mode 100644 index 0000000..6a07d1d Binary files /dev/null and b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-fender-detail.jpeg differ diff --git a/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-fender.webp b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-fender.webp new file mode 100644 index 0000000..8223796 Binary files /dev/null and b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-fender.webp differ diff --git a/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-guitar-detail.jpeg b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-guitar-detail.jpeg new file mode 100644 index 0000000..2f2d72c Binary files /dev/null and b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-guitar-detail.jpeg differ diff --git a/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-guitar-pair.jpg b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-guitar-pair.jpg new file mode 100644 index 0000000..fecefdd Binary files /dev/null and b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-guitar-pair.jpg differ diff --git a/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-style-sheet.jpeg b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-style-sheet.jpeg new file mode 100644 index 0000000..3962c9f Binary files /dev/null and b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-style-sheet.jpeg differ diff --git a/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-tone-volume.jpg b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-tone-volume.jpg new file mode 100644 index 0000000..e6e0cc0 Binary files /dev/null and b/working/retro-stereo-widgets/references/2026-07-18-dual-concentric-knob-tone-volume.jpg differ diff --git a/working/retro-stereo-widgets/references/2026-07-18-hotfx-split-flap-reference.org b/working/retro-stereo-widgets/references/2026-07-18-hotfx-split-flap-reference.org new file mode 100644 index 0000000..5fa7acd --- /dev/null +++ b/working/retro-stereo-widgets/references/2026-07-18-hotfx-split-flap-reference.org @@ -0,0 +1,65 @@ +#+TITLE: HotFX split-flap web component — technique reference +#+DATE: [2026-07-18 Sat] + +Reference for the split-flap display work (gallery card N20, and any flip/drum +date-time module the waybar chronograph grows). Craig supplied both links. + +- Annotated walkthrough: https://fx.hot.page/split-flap/source +- Source on GitHub: https://github.com/hot-page/fx/blob/main/hotfx-split-flap/hotfx-split-flap.js +- Local study copy: [[file:2026-07-18-hotfx-split-flap-source.js]] + +LICENSE CAUTION: the fx repo declares NO license (checked via the GitHub API +2026-07-18), so the code is all-rights-reserved by default. Study the +technique, re-derive our own implementation in the GW namespace — do not copy +code or CSS verbatim into the gallery or dotfiles. + +* The mechanics worth stealing (as ideas) + +** Four-layer character cell +Each character is four absolutely-stacked half-panels: =.top= / =.bottom= +(current char) and =.top-next= / =.bottom-next= (incoming char), the tops +clipped to the upper half and bottoms to the lower half, with a divider line +at 50%. =transform-style: preserve-3d= on the cell, +=backface-visibility: hidden= on flaps so a flap's reverse never shows. + +** One flip = two synchronized quarter-animations +Only two elements move per flip, via the Web Animations API with +=Promise.all= on their =.finished=: +- current =.top= rotates =rotateX(0deg)= → =rotateX(-180deg)= (falls forward) +- =.bottom-next= rotates =rotateX(180deg)= → =rotateX(0deg)= (lands) +Both =ease-in=, default 150ms per flap. After landing, the static halves get +the new character's text and the cycle repeats. This is the minimal honest +split-flap kinematic. + +** The charset IS the drum +It never jumps to the target character: it steps =currentIndex + 1= (wrapping) +through the declared alphabet until it arrives, one flip per step — the +Solari constraint that a board can only advance one flap at a time. The +alphabet is a declared attribute (default =' ABC…Z0-9!.,:?"'/$'=), so the +character order is data, exactly our R58 LAYOUT-as-data pattern: rewrite the +alphabet and the physical behavior follows. + +** Live-update semantics (relevant for a clock) +A MutationObserver rebuilds the target grid on text change; an in-flight flip +cascade aborts itself by identity-checking the target grid each step, and the +new animation starts only after awaiting every running animation's finish. +That's the answer to "the minute changed while the drum was still spinning" — +abort-and-retarget, never queue. + +** Also noted +IntersectionObserver defers the animation until the element is half in view; +=::part(flap-N-M)= attributes expose every flap and divider for external +styling. ~300 lines total for all of it. + +* How it maps to our targets + +- Gallery N20 (split-flap board): the four-layer cell + two-animation flip is + the web implementation; the step-through-charset rule and abort-on-retarget + are behaviors our probe checks can pin regardless of implementation. +- Waybar chronograph drums (the redesign mock's date/time): our drums are + odometer rollers, not flaps — different mechanism — but the charset-as-drum + ordering and abort-and-retarget rules carry over unchanged. +- Emacs svg.el port: no Web Animations API; the flip becomes frame-stepped + whole-image regeneration per the render contract, with the same + one-step-at-a-time rule. Reduced-motion gate applies (widget-owned + animation per the gallery tick contract). diff --git a/working/retro-stereo-widgets/references/2026-07-18-hotfx-split-flap-source.js b/working/retro-stereo-widgets/references/2026-07-18-hotfx-split-flap-source.js new file mode 100644 index 0000000..40539ad --- /dev/null +++ b/working/retro-stereo-widgets/references/2026-07-18-hotfx-split-flap-source.js @@ -0,0 +1,455 @@ +// Create a custom element by extending `HTMLElement`. +export class HotFXSplitFlap extends HTMLElement { + // The internal state of the component is handled in private instance + // variables, which in JavaScript become private when they start with `#`. + // + // `hasAnimated` is a flag that keeps track of whether the animation has + // already completed. + #hasAnimated = false + // `isVisible` is a flag that keeps track of whether the element is currently + // visible in the viewport. + #isVisible = false + // The `IntersectionObserver` fires a callback that lets us know if the element + // is visible in the window or not. + #intersectionObserver + // The `MutationObserver` tells the element when it's content changes, so we + // can re-render the grid and animate it again. + #mutationObserver + // The `#currentGrid` is the current state of the text. The characters are + // stored as a two-dimensional array: each row is an array of characters and + // the grid is an array of rows. + #currentGrid + // The `#targetGrid` shows the ultimate destination of the + #targetGrid + + // Here we tell the browser to fire `attributeChangedCallback` when one of + // the `width`, `height`, or `duration` attributes changes. + static get observedAttributes() { + return ['width', 'height', 'duration', 'characcters'] + } + + constructor() { + // Always call your `super()` or the landlord will be mad. + super() + // Create a shadow DOM for this element that will hold all the elements and + // stlyes needed to create the flaps. + this.attachShadow({ mode: 'open' }) + } + + connectedCallback() { + // First do some validation on the attributes. Although all of them are + // optional, they are required to be positive integers if set. + ['width', 'height', 'duration'].forEach(name => { + if (!this.hasAttribute(name)) return + const value = parseInt(this.getAttribute(name)) + if (isNaN(value) || value <= 0) this.#printNumberError(name) + }) + + // If this element has already been mounted elsewhere the `#hasAnimated` + // flag may be set even though we're coming in fresh here. Let's reset it. + this.#hasAnimated = false + // The `#render()` function will create the grid and the flaps inside the + // shadow DOM. + this.#render() + + // Setup our `IntersectionObserver`, which will fire a callback function + // when this element scrolls into view. + this.#intersectionObserver = new IntersectionObserver( + (entries) => { + // Since we are only observing one element, the callback should always + // fire with one value in `entries`. + this.#isVisible = entries[0].isIntersecting + // The animation should only run the first time that we scoll onto it + // so only trigger the animation it if #hasAnimated is false. + if (this.#isVisible && !this.#hasAnimated) this.#animate() + }, + // Fire the callback when 50% of this element has scrolled into view. + { threshold: 0.5 } + ) + // Observe this element + this.#intersectionObserver.observe(this) + + // Setup the `MutationObserver` that will re-run the animation when the + // text inside this element changes. + this.#mutationObserver = new MutationObserver(async () => { + // The `#targetGrid` is the string of text that the element will animate + // towards. + this.#updateTargetGrid() + // First wait for the current animations to finish so there's no glitch + // when the new animations start. + await Promise.all( + Array.from(this.shadowRoot.querySelectorAll('.char')) + .map(char => char.getAnimations()) + .flat() + .map(animation => animation.finish()) + ) + // If the element is currently in view, start the animation. + if (this.#isVisible) this.#animate() + }) + // Observe this element, along with the entire tree under it. + this.#mutationObserver.observe( + this, + { childList: true, characterData: true, subtree: true }, + ) + } + + // Clean up the observers so that memory doesn't leak out of the browser, + // causing it to lose all of it's life force. + disconnectedCallback() { + this.#intersectionObserver?.disconnect() + this.#intersectionObserver = undefined + this.#mutationObserver?.disconnect() + this.#mutationObserver = undefined + } + + // The browser will call this when one of the `observedAttributes` changes. + attributeChangedCallback(name, oldValue, newValue) { + // Do some validation of the new value to warn the page author if they've + // added an invalid value. + if (['width', 'height', 'duration'].includes(name)) { + const value = parseInt(this.getAttribute(name)) + if (isNaN(value) || value <= 0) return this.#printNumberError(name) + } + // If the attribute that was changed is `duration` we don't neeed to do + // anything because it will be picked up in the next animation. + if (name == 'duration' || oldValue == newValue) return + // If `width` or `height` change, we will need to re-render the structure + // of the shadow DOM to account for the new size. + this.#render() + // Finally, restart the animation if this element is currently in the + // viewport. After rendering, the tiles are all on the first letter which is + // the space character by default. + if (this.#isVisible) this.#animate() + } + + // Getter for `width` property called when reading `element.width`. + get width() { + // If there is a `#currentGrid`, report this size. + if (this.#currentGrid) return this.#currentGrid[0].length + // Otherwise, try to use the attribute if set. + if (this.hasAttribute('width')) { + const width = parseInt(this.getAttribute('width')) + if (Number.isInteger(width) && width > 0) return width + } + // Finally, if there is no grid and no attribute, we default to the size of + // the text inside this element, specifically the longest line. + return Math.max(...this.textContent.split('\n').map(line => line.length)) + } + + // Reflect the `.width` property back to the `width=` attribute. + set width(value) { + // Validate the new value which should be a positive integer. + if (!Number.isInteger(value) || value <= 0) this.#printNumberError('width') + // Reflect the new value to the attribute and re-render the shadow DOM + this.setAttribute('width', value) + this.#render() + // If the element is in the viewport, run the animation now. + if (this.#isVisible) this.#animate() + } + + // Getter for `.height` property similar to above for `width`, but returning + // the number of lines in the text content instead of the longest line. + get height() { + if (this.#currentGrid) return this.#currentGrid.length + if (this.hasAttribute('height')) { + const height = parseInt(this.getAttribute('height')) + if (Number.isInteger(height) && height > 0) return height + } + return this.textContent.split('\n').length + } + + // Setter for `.height` property, same as above for `width`. + set height(value) { + if (!Number.isInteger(value) || value <= 0) this.#printNumberError('height') + this.setAttribute('height', value) + this.#render() + if (this.#isVisible) this.#animate() + } + + // Get the duration of the animation which is how long it takes for one flap + // to fall down from top to bottom. + get duration() { + // The duration defaults to 150 milliseconds or 0.15 seconds for each flap + // to fall once. + let duration = 150 + // Check if there's a `duration` attribute and use that value if it's a + // positive integer. + if (this.hasAttribute('duration')) { + const durationAttr = parseInt(this.getAttribute('duration')) + if (Number.isInteger(durationAttr) && durationAttr > 0) duration = durationAttr + } + return duration + } + + // Setter for duration that reflects to the attribute with valdiation logic. + set duration(value) { + if (typeof value !== 'number' || value <= 0) return this.#printNumberError('duration') + this.setAttribute('duration', value) + } + + // Getter for `.characters` property. This is the alphabet used for the flaps. + // Here we're just pulling from the attribute which has the current value or + // using the default Latin alphabet. + get characters() { + return this.getAttribute('characters') || ' ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!.,:?"\'/$' + } + + // Setter for `.characters` property that reflects to the attribute. + set characters(value) { + this.setAttribute('characters', value) + } + + // Helper function that turns the text of this element into the grid of + // letters that we're going to animate towards. The grid is stored as a + // two-dimensional array of characters. + #updateTargetGrid() { + // Split the text on newline to get an array of lines. + const lines = this.textContent.split('\n') + // Note that we're using the `.width` and `.height` properties because the + // dimensions of the grid could have been set explicitly using attributes. + this.#targetGrid = Array.from({ length: this.height }).map((_, row) => { + const line = lines[row] || '' + return Array.from({ length: this.width }).map( + (_, col) => line[col]?.toUpperCase() || ' ', + ) + }) + } + + // Render the grid of letters into the shadow DOM. + #render() { + // First set `#currentGrid` to null because it will be used to calculate the + // width and height of the next grid and we need to start fresh. + this.#currentGrid = null + // The `#currentGrid` is the current state of the letters displayed. Since + // we're starting over fresh here, we just make it all space characters. + this.#currentGrid = Array.from({ length: this.height }, () => + Array.from({ length: this.width }, () => ' ') + ) + this.#updateTargetGrid() + + this.shadowRoot.innerHTML = [` + +
`, + // Create the grid of characters by mapping over the `#currentGrid` + // (in both dimensions) + this.#currentGrid.map((row, rowNumber) => { + // Row and column numbers used in `::part()` start at 1 as per CSS + // convention, e.g. `nth-child(1)` + rowNumber += 1 + return row.map((char, columnNumber) => { + columnNumber += 1 + const currentChar = char === ' ' ? ' ' : char + // Each character and its flaps have several part= attribute values + // so they can be addressed in CSS, for example + // - `::part(char-1)` for all the characters in the first column + // - `::part(flap-3-4)` for the flaps in the third row and + // fourth column + return ` + + + + + ${currentChar} + + + + + + ` + }).join('') + }).join(''), + `
` + ].join('') + } + + // To animate, the element must rotate each flap, then swap out the characters + // in the DOM and then repeat through the whole alphabet until it arrives at + // the correct end character. + #animate() { + // If the animation has already run, don't run it again. + this.#hasAnimated = true + + // Create a copy of the target grid so that we can check if it since been + // updated and the animation should be aborted (since another one will + // be triggered). + const targetGrid = this.#targetGrid + + // Animate each character. + this.shadowRoot.querySelectorAll('.char').forEach((char, index) => { + // Find the index of this charcter in the grid. + const row = Math.floor(index / this.width) + const col = index % this.width + + // The index of the target character in our alphabet. + const targetCharIndex = this.characters.includes(targetGrid[row][col]) + ? this.characters.indexOf(targetGrid[row][col]) + : 0 + + // The current character index in the alphabet. + let currentCharIndex = this.characters.indexOf(this.#currentGrid[row][col]) + + // If the indexes are the same, it's already finished and this character + // should not be animated at all + if (currentCharIndex == targetCharIndex) return + + // This function will run one flap and wait for it to finsih before + // calling itself again to animate the next flap. + const flap = async () => { + // If the target grid has changed since we started animating, abort + // this animation since another one will be triggered. + if (targetGrid != this.#targetGrid) return + + // Find the index of the character following the current one, wrapping + // around to the start if needed. + const nextCharIndex = currentCharIndex < this.characters.length - 1 + ? currentCharIndex + 1 + : 0 + + // The next character to animate towards + const nextChar = this.characters[nextCharIndex] + + // Set the text content of the top and bottom flaps to the next character + char.querySelector('.top-next').childNodes[0].textContent = nextChar + char.querySelector('.bottom-next').childNodes[0].textContent = nextChar + + // Anaimte the top flap and the bottom of the next flap to fall from the + // top. Using `await Promise.all()` to wait for both animations to + // finish. + const options = { duration: this.duration, easing: 'ease-in' } + await Promise.all([ + char.querySelector('.top').animate([ + { transform: 'rotateX(0deg)' }, + { transform: 'rotateX(-180deg)' } + ], options).finished, + char.querySelector('.bottom-next').animate([ + { transform: 'rotateX(180deg)' }, + { transform: 'rotateX(0deg)' } + ], options).finished + ]) + + // Swap out the top and bottom flaps and set the `currentCharIndex` value + // so that the next character is now the currect character. + char.querySelector('.top').childNodes[0].textContent = nextChar + char.querySelector('.bottom').childNodes[0].textContent = nextChar + currentCharIndex = nextCharIndex + // Update the #currentGrid with the new character. + this.#currentGrid[row][col] = this.characters[currentCharIndex] + // If the new currrent character is still not the target character, + // continue animating by calling this `flap()` function again. + if (currentCharIndex !== targetCharIndex) flap() + } + + // Trigger the first animation for this character. + flap() + }) + } + + // Helper function to print errors for invalid attributes. + #printNumberError(name) { + console.error(` ${name} attribute must be a positive integer; got "${this.getAttribute(name)}"`) + } + +} + +// Checks for a `?define` query parameter in the URL that was used to load this +// script. So, for instance, if you load the script with ` + + diff --git a/working/weather-kit-integration/weather-glyphs.svg b/working/weather-kit-integration/weather-glyphs.svg new file mode 100644 index 0000000..0cc3189 --- /dev/null +++ b/working/weather-kit-integration/weather-glyphs.svg @@ -0,0 +1,81 @@ + + + -- cgit v1.2.3