aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/gallery-widget.el
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes/gallery-widget.el')
-rw-r--r--docs/prototypes/gallery-widget.el166
1 files changed, 166 insertions, 0 deletions
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