aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes')
-rw-r--r--docs/prototypes/README.org20
-rw-r--r--docs/prototypes/gallery-widget.el166
-rw-r--r--docs/prototypes/panel-widget-gallery.html2
3 files changed, 186 insertions, 2 deletions
diff --git a/docs/prototypes/README.org b/docs/prototypes/README.org
index c9b2fd7..3f2e3be 100644
--- a/docs/prototypes/README.org
+++ b/docs/prototypes/README.org
@@ -16,6 +16,24 @@ point at.
- [[file:2026-07-03-sound-panel-prototype.html][2026-07-03-sound-panel-prototype.html]] — the audio/pulsemixer console; layout
reference for [[file:../specs/2026-07-03-audio-panel-spec.org][the audio-panel spec]].
- [[file:panel-widget-gallery.html][panel-widget-gallery.html]] — the shared instrument-console
- widget kit (lamps, engraved sections, console keys, needle gauges).
+ widget kit (lamps, engraved sections, console keys, needle gauges). The
+ living catalogue: every widget here is the visual + behavioral spec for
+ its reusable-component ports.
+
+* Design tokens (single source, three targets)
+
+[[file:tokens.json][tokens.json]] is the source of truth for the design tokens (palette, amber
+family, glows, pulse rate). [[file:gen_tokens.py][gen_tokens.py]] regenerates all three targets
+(=python3 gen_tokens.py=):
+
+- the =:root= block inside the gallery HTML (web CSS custom properties)
+- [[file:tokens-waybar.css][tokens-waybar.css]] — GTK =@define-color= declarations for the waybar panels
+- [[file:tokens.el][tokens.el]] — an elisp alist for svg.el renderers
+
+[[file:gallery-widget.el][gallery-widget.el]] is the Emacs renderer (proof widget: the needle gauge,
+gallery card 10) — it reads tokens.el and emits the widget as SVG via
+svg.el, so the same look renders inside Emacs. Tests:
+=tests/gallery-tokens/= (generator, unittest) and =tests/gallery-widgets/=
+(renderer, ERT), both in =make test-unit=.
- [[file:2026-07-03-waybar-redesign-prototype.html][2026-07-03-waybar-redesign-prototype.html]] — three directions for sprucing up
waybar in the dupre instrument-console aesthetic (future work).
diff --git a/docs/prototypes/gallery-widget.el b/docs/prototypes/gallery-widget.el
new file mode 100644
index 0000000..61cd78a
--- /dev/null
+++ b/docs/prototypes/gallery-widget.el
@@ -0,0 +1,166 @@
+;;; gallery-widget.el --- svg.el renderers for the panel widget gallery -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Proof-of-concept Emacs target for the panel widget gallery. The web
+;; gallery (panel-widget-gallery.html) is the visual spec; tokens.json is
+;; the shared source of truth, and gen_tokens.py emits tokens.el (the
+;; `gallery-tokens' alist this file reads). Same tokens, same geometry,
+;; different renderer — that's the reuse model: shared values and shared
+;; SVG shapes, per-target code.
+;;
+;; The first widget is the needle gauge (gallery card 10): a semicircular
+;; arc in the wash color, three steel ticks at -60/0/+60 degrees, an amber
+;; needle with a soft glow (a real feGaussianBlur underlay, which librsvg —
+;; Emacs's own SVG renderer — rasterizes), a gold hub, and a value readout.
+;;
+;; Usage:
+;; (gallery-widget-needle-gauge 72) ; => svg dom object
+;; (gallery-widget-svg-string (gallery-widget-needle-gauge 72))
+;; (gallery-widget-write-svg (gallery-widget-needle-gauge 72) "/tmp/g.svg")
+;; In a live Emacs buffer:
+;; (insert-image (svg-image (gallery-widget-needle-gauge 72)))
+
+;;; Code:
+
+(require 'svg)
+(require 'dom)
+
+(load (expand-file-name "tokens.el"
+ (file-name-directory (or load-file-name buffer-file-name)))
+ nil t)
+
+(defun gallery-widget-token (name)
+ "Return the color/value string for token NAME from `gallery-tokens'.
+Signal an error for an unknown NAME rather than silently returning nil —
+a typo'd token must fail loudly, not render black."
+ (or (cdr (assq name gallery-tokens))
+ (error "Unknown gallery token: %s" name)))
+
+;;; --- needle gauge (gallery card 10) ---
+
+(defconst gallery-widget--gauge-min-deg -60.0
+ "Needle angle at value 0, degrees from vertical (negative = left).")
+(defconst gallery-widget--gauge-max-deg 60.0
+ "Needle angle at value 100.")
+
+(defun gallery-widget--needle-angle (value)
+ "Map VALUE (0-100, clamped) onto the gauge's needle angle in degrees."
+ (unless (numberp value)
+ (error "Gauge value must be a number, got %S" value))
+ (let ((v (min 100.0 (max 0.0 (float value)))))
+ (+ gallery-widget--gauge-min-deg
+ (* (/ v 100.0)
+ (- gallery-widget--gauge-max-deg gallery-widget--gauge-min-deg)))))
+
+(defun gallery-widget--polar (cx cy radius angle-deg)
+ "Point at RADIUS from (CX . CY) at ANGLE-DEG from vertical, as (X . Y).
+Positive angles swing right, matching the gauge's needle travel."
+ (let ((rad (* float-pi (/ angle-deg 180.0))))
+ (cons (+ cx (* radius (sin rad)))
+ (- cy (* radius (cos rad))))))
+
+(defun gallery-widget--fmt (n)
+ "Format coordinate N with two decimals for stable, readable SVG output."
+ (format "%.2f" n))
+
+(defun gallery-widget--node (svg tag &rest attrs)
+ "Append a TAG element with ATTRS plist to SVG and return it."
+ (let ((node (dom-node tag
+ (cl-loop for (k v) on attrs by #'cddr
+ collect (cons (intern (substring (symbol-name k) 1))
+ v)))))
+ (svg--append svg node)
+ node))
+
+(defun gallery-widget-needle-gauge (value)
+ "Render the gallery's needle gauge at VALUE (0-100) as an svg.el object.
+Geometry mirrors the web card: 96px-wide semicircular dial, pivot at its
+bottom center, 40px needle sweeping -60..+60 degrees, readout below."
+ (let* ((w 96) (h 64) (cx 48.0) (cy 48.0)
+ (angle (gallery-widget--needle-angle value))
+ (tip (gallery-widget--polar cx cy 40.0 angle))
+ (svg (svg-create w h :viewBox (format "0 0 %d %d" w h))))
+ ;; glow filter: the SVG equivalent of the web card's box-shadow bloom
+ (svg--append
+ svg
+ (dom-node 'defs nil
+ (dom-node 'filter
+ '((id . "gw-glow") (x . "-75%") (y . "-75%")
+ (width . "250%") (height . "250%"))
+ (dom-node 'feGaussianBlur
+ '((in . "SourceGraphic")
+ (stdDeviation . "2"))))))
+ ;; arc: 2px wash stroke, centerline radius 47 (96px circle, 2px border)
+ (gallery-widget--node svg 'path
+ :d "M 1 48 A 47 47 0 0 1 95 48"
+ :fill "none"
+ :stroke (gallery-widget-token 'wash)
+ :stroke-width "2")
+ ;; ticks: 8px steel radials at -60 / 0 / +60
+ (dolist (deg '(-60.0 0.0 60.0))
+ (let ((outer (gallery-widget--polar cx cy 47.0 deg))
+ (inner (gallery-widget--polar cx cy 39.0 deg)))
+ (gallery-widget--node svg 'line
+ :class "tick"
+ :x1 (gallery-widget--fmt (car inner))
+ :y1 (gallery-widget--fmt (cdr inner))
+ :x2 (gallery-widget--fmt (car outer))
+ :y2 (gallery-widget--fmt (cdr outer))
+ :stroke (gallery-widget-token 'steel)
+ :stroke-width "1.5")))
+ ;; needle glow underlay (blurred, translucent), then the crisp needle
+ (gallery-widget--node svg 'line
+ :class "needle-glow"
+ :x1 (gallery-widget--fmt cx)
+ :y1 (gallery-widget--fmt cy)
+ :x2 (gallery-widget--fmt (car tip))
+ :y2 (gallery-widget--fmt (cdr tip))
+ :stroke (gallery-widget-token 'gold-hi)
+ :stroke-width "4"
+ :stroke-linecap "round"
+ :opacity "0.55"
+ :filter "url(#gw-glow)")
+ (gallery-widget--node svg 'line
+ :class "needle"
+ :x1 (gallery-widget--fmt cx)
+ :y1 (gallery-widget--fmt cy)
+ :x2 (gallery-widget--fmt (car tip))
+ :y2 (gallery-widget--fmt (cdr tip))
+ :stroke (gallery-widget-token 'gold-hi)
+ :stroke-width "2"
+ :stroke-linecap "round")
+ ;; hub: half-dome on the dial's bottom edge (the web card clips the
+ ;; lower half of its 8px circle; here the dome is drawn directly)
+ (gallery-widget--node svg 'path
+ :class "hub"
+ :d "M 44 48 A 4 4 0 0 1 52 48 Z"
+ :fill (gallery-widget-token 'gold))
+ ;; value readout, matching the web card's cream bold figure
+ (svg-text svg (format "%d%%" (round value))
+ :x 48 :y 62
+ :fill (gallery-widget-token 'cream)
+ ;; the token's CSS font stack uses double quotes, which would
+ ;; break the XML attribute — SVG accepts single-quoted names
+ :font-family (replace-regexp-in-string
+ "\"" "'" (gallery-widget-token 'mono))
+ :font-size "12"
+ :font-weight "700"
+ :text-anchor "middle")
+ svg))
+
+;;; --- output helpers ---
+
+(defun gallery-widget-svg-string (svg)
+ "Return SVG (an svg.el dom object) serialized as an XML string."
+ (with-temp-buffer
+ (svg-print svg)
+ (buffer-string)))
+
+(defun gallery-widget-write-svg (svg file)
+ "Write SVG to FILE and return FILE."
+ (with-temp-file file
+ (svg-print svg))
+ file)
+
+(provide 'gallery-widget)
+;;; gallery-widget.el ends here
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index 1d95488..c56a21f 100644
--- a/docs/prototypes/panel-widget-gallery.html
+++ b/docs/prototypes/panel-widget-gallery.html
@@ -128,7 +128,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.gauge{width:96px;cursor:ns-resize;touch-action:none}
.gauge .dial{position:relative;height:48px;overflow:hidden}
.gauge .arc{position:absolute;inset:0 0 -48px 0;border:2px solid var(--wash);border-radius:50%}
-.gauge .tk{position:absolute;left:50%;bottom:0;width:1.5px;height:8px;background:var(--steel);transform-origin:50% 48px}
+.gauge .tk{position:absolute;left:50%;top:1px;width:1.5px;height:8px;margin-left:-.75px;background:var(--steel);transform-origin:50% 47px}
.gauge .ndl{position:absolute;left:50%;bottom:0;width:2px;height:40px;background:var(--gold-hi);
transform-origin:50% 100%;transform:rotate(0deg);border-radius:2px;box-shadow:0 0 6px rgba(var(--glow-hi),.5);
transition:transform .12s cubic-bezier(.3,1.3,.5,1)}