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")'