From 2663dfb09558cfcd54767220320eb103387a6690 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 4 Mar 2026 13:34:58 +0000 Subject: [PATCH] Add SVG cover art to SX Manifesto as s-expression Soviet constructivist poster with paper texture filters, grid lines, aged stain spots, and "()" symbol in red. Add missing SVG filter primitive tags to both server (html.py) and client (sx.js): feTurbulence, feColorMatrix, feBlend, feComponentTransfer, feFuncR/G/B/A, feDisplacementMap, feComposite, feFlood, feImage, feMorphology, feSpecularLighting, feDiffuseLighting, fePointLight, feSpotLight, feDistantLight. Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx.js | 16 +++++- shared/sx/html.py | 5 ++ sx/sxc/pages/__init__.py | 103 ++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/shared/static/scripts/sx.js b/shared/static/scripts/sx.js index 85e4b7b..cfa98e7 100644 --- a/shared/static/scripts/sx.js +++ b/shared/static/scripts/sx.js @@ -1020,7 +1020,13 @@ "img picture source iframe embed object param video audio track canvas map area " + "svg math path circle ellipse line polygon polyline rect g defs use text tspan " + "clipPath mask linearGradient radialGradient stop filter " + - "feGaussianBlur feOffset feMerge feMergeNode animate animateTransform " + + "feGaussianBlur feOffset feMerge feMergeNode " + + "feTurbulence feColorMatrix feBlend " + + "feComponentTransfer feFuncR feFuncG feFuncB feFuncA " + + "feDisplacementMap feComposite feFlood feImage " + + "feMorphology feSpecularLighting feDiffuseLighting " + + "fePointLight feSpotLight feDistantLight " + + "animate animateTransform " + "table thead tbody tfoot tr th td caption colgroup col " + "form fieldset legend label input button select option optgroup textarea output " + "datalist progress meter details summary dialog template slot" @@ -1040,7 +1046,13 @@ var SVG_TAGS = makeSet( "svg path circle ellipse line polygon polyline rect g defs use text tspan " + "clipPath mask linearGradient radialGradient stop filter " + - "feGaussianBlur feOffset feMerge feMergeNode animate animateTransform" + "feGaussianBlur feOffset feMerge feMergeNode " + + "feTurbulence feColorMatrix feBlend " + + "feComponentTransfer feFuncR feFuncG feFuncB feFuncA " + + "feDisplacementMap feComposite feFlood feImage " + + "feMorphology feSpecularLighting feDiffuseLighting " + + "fePointLight feSpotLight feDistantLight " + + "animate animateTransform" ); var SVG_NS = "http://www.w3.org/2000/svg"; diff --git a/shared/sx/html.py b/shared/sx/html.py index 510499a..2c78f09 100644 --- a/shared/sx/html.py +++ b/shared/sx/html.py @@ -94,6 +94,11 @@ HTML_TAGS = frozenset({ "g", "defs", "use", "text", "tspan", "clipPath", "mask", "linearGradient", "radialGradient", "stop", "filter", "feGaussianBlur", "feOffset", "feMerge", "feMergeNode", + "feTurbulence", "feColorMatrix", "feBlend", + "feComponentTransfer", "feFuncR", "feFuncG", "feFuncB", "feFuncA", + "feDisplacementMap", "feComposite", "feFlood", "feImage", + "feMorphology", "feSpecularLighting", "feDiffuseLighting", + "fePointLight", "feSpotLight", "feDistantLight", "animate", "animateTransform", # Table "table", "thead", "tbody", "tfoot", "tr", "th", "td", diff --git a/sx/sxc/pages/__init__.py b/sx/sxc/pages/__init__.py index 8be3253..56604b9 100644 --- a/sx/sxc/pages/__init__.py +++ b/sx/sxc/pages/__init__.py @@ -2439,6 +2439,106 @@ def _essay_sx_native() -> str: ) +def _manifesto_cover() -> str: + """SVG cover art for the SX Manifesto — Soviet constructivist poster style.""" + # Helper to generate grid lines + def vlines(x1: int, x2: int, ys: list[int], attr: str = "") -> str: + return " ".join( + f'(line :x1 "{x1}" :y1 "{y}" :x2 "{x2}" :y2 "{y}"{attr})' + for y in ys + ) + + def hlines(y1: int, y2: int, xs: list[int], attr: str = "") -> str: + return " ".join( + f'(line :x1 "{x}" :y1 "{y1}" :x2 "{x}" :y2 "{y2}"{attr})' + for x in xs + ) + + return ( + '(div :class "flex justify-center mb-10"' + ' (svg :xmlns "http://www.w3.org/2000/svg" :viewBox "0 0 1200 1828"' + ' :width "400" :height "609" :class "rounded shadow-lg"' + + # --- Defs: filters --- + ' (defs' + ' (filter :id "paper-texture"' + ' (feTurbulence :type "fractalNoise" :baseFrequency "0.65"' + ' :numOctaves "3" :stitchTiles "stitch" :result "noise")' + ' (feColorMatrix :type "saturate" :values "0" :in "noise" :result "grayNoise")' + ' (feBlend :in "SourceGraphic" :in2 "grayNoise" :mode "multiply" :result "blended")' + ' (feComponentTransfer :in "blended"' + ' (feFuncA :type "linear" :slope "1")))' + ' (filter :id "aged"' + ' (feTurbulence :type "turbulence" :baseFrequency "0.02"' + ' :numOctaves "4" :result "noise")' + ' (feDisplacementMap :in "SourceGraphic" :in2 "noise"' + ' :scale "2" :xChannelSelector "R" :yChannelSelector "G")))' + + # --- Red background --- + ' (rect :width "1200" :height "1828" :fill "#d20f0f")' + + # --- Red panel grid lines --- + ' (g :opacity "0.35"' + + " ".join( + f' (line :x1 "{x}" :y1 "0" :x2 "{x}" :y2 "1828" :stroke "#b00" :stroke-width "3")' + for x in [0, 300, 600, 900, 1200] + ) + + " ".join( + f' (line :x1 "0" :y1 "{y}" :x2 "1200" :y2 "{y}" :stroke "#b00" :stroke-width "3")' + for y in [0, 457, 914, 1371, 1828] + ) + + ')' + + # --- White/cream inner panel --- + ' (rect :x "90" :y "70" :width "1020" :height "1688"' + ' :fill "#f5f0e8" :filter "url(#paper-texture)")' + + # --- Subtle grid on white panel --- + ' (g :opacity "0.07" :stroke "#888" :stroke-width "1"' + + " ".join( + f' (line :x1 "{x}" :y1 "70" :x2 "{x}" :y2 "1758")' + for x in [90, 210, 330, 450, 570, 690, 810, 930, 1050, 1110] + ) + + " ".join( + f' (line :x1 "90" :y1 "{y}" :x2 "1110" :y2 "{y}")' + for y in [190, 310, 430, 550, 670, 790, 910, 1030, 1150, 1270, 1390, 1510, 1630, 1758] + ) + + ')' + + # --- Aged stain spots --- + ' (ellipse :cx "200" :cy "1500" :rx "30" :ry "18" :fill "#c8b89a" :opacity "0.3")' + ' (ellipse :cx "980" :cy "1600" :rx "20" :ry "12" :fill "#c8b89a" :opacity "0.25")' + ' (ellipse :cx "150" :cy "900" :rx "15" :ry "10" :fill "#c8b89a" :opacity "0.2")' + + # --- Title text --- + ' (text :x "600" :y "260" :font-family "Georgia, serif"' + ' :font-size "118" :font-weight "bold" :text-anchor "middle"' + ' :fill "#1a1a1a" :letter-spacing "12" "THE SX")' + ' (text :x "600" :y "400" :font-family "Georgia, serif"' + ' :font-size "118" :font-weight "bold" :text-anchor "middle"' + ' :fill "#1a1a1a" :letter-spacing "8" "MANIFESTO")' + + # --- Symbol: () in red --- + ' (text :x "600" :y "720" :font-family "Georgia, serif"' + ' :font-size "200" :font-weight "bold" :text-anchor "middle"' + ' :fill "#d20f0f" "()")' + + # --- Author names --- + ' (text :x "600" :y "980" :font-family "Georgia, serif"' + ' :font-size "95" :font-weight "bold" :text-anchor "middle"' + ' :fill "#1a1a1a" :letter-spacing "6" "CARL MARKDOWN")' + ' (text :x "600" :y "1110" :font-family "Georgia, serif"' + ' :font-size "80" :font-weight "bold" :text-anchor "middle"' + ' :fill "#1a1a1a" :letter-spacing "14" "AND")' + ' (text :x "600" :y "1250" :font-family "Georgia, serif"' + ' :font-size "95" :font-weight "bold" :text-anchor "middle"' + ' :fill "#1a1a1a" :letter-spacing "6" "FRIEDRICH")' + ' (text :x "600" :y "1390" :font-family "Georgia, serif"' + ' :font-size "82" :font-weight "bold" :text-anchor "middle"' + ' :fill "#1a1a1a" :letter-spacing "4" "ANGLEBRACKETS")))' + ) + + def _essay_sx_manifesto() -> str: p = '(p :class "text-stone-600"' em = '(span :class "italic"' @@ -2454,8 +2554,11 @@ def _essay_sx_manifesto() -> str: f' :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true"' f' :class "{L}" "{text}")') + cover = _manifesto_cover() + return ( '(~doc-page :title "The SX Manifesto"' + f' {cover}' ' (p :class "text-stone-500 text-sm italic mb-8"' ' "Carl Markdown and Friedrich Anglebrackets")'