Add SVG cover art to SX Manifesto as s-expression
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m1s

Soviet constructivist poster with paper texture filters, grid lines,
aged stain spots, and "(<x>)" 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 13:34:58 +00:00
parent ccd9b969ea
commit 2663dfb095
3 changed files with 122 additions and 2 deletions

View File

@@ -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";

View File

@@ -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",

View File

@@ -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: (<x>) in red ---
' (text :x "600" :y "720" :font-family "Georgia, serif"'
' :font-size "200" :font-weight "bold" :text-anchor "middle"'
' :fill "#d20f0f" "(<x>)")'
# --- 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")'