d2render: support gradient values

This commit is contained in:
Alexander Wang 2024-09-27 15:49:10 -06:00
parent 7f2e1f4fce
commit 09cf9ae7d1
No known key found for this signature in database
GPG key ID: BE3937D0D52D8927
10 changed files with 908 additions and 6 deletions

View file

@ -253,16 +253,16 @@ func (s *Style) Apply(key, value string) error {
if s.Stroke == nil {
break
}
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
return errors.New(`expected "stroke" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
if !color.ValidColor(value) {
return errors.New(`expected "stroke" to be a valid named color ("orange"), a hex code ("#f0ff3a"), or a gradient ("linear-gradient(red, blue)")`)
}
s.Stroke.Value = value
case "fill":
if s.Fill == nil {
break
}
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
return errors.New(`expected "fill" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
if !color.ValidColor(value) {
return errors.New(`expected "fill" to be a valid named color ("orange"), a hex code ("#f0ff3a"), or a gradient ("linear-gradient(red, blue)")`)
}
s.Fill.Value = value
case "fill-pattern":
@ -348,8 +348,8 @@ func (s *Style) Apply(key, value string) error {
if s.FontColor == nil {
break
}
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
return errors.New(`expected "font-color" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
if !color.ValidColor(value) {
return errors.New(`expected "font-color" to be a valid named color ("orange"), a hex code ("#f0ff3a"), or a gradient ("linear-gradient(red, blue)")`)
}
s.FontColor.Value = value
case "animated":

View file

@ -706,6 +706,11 @@ func renderDoubleOval(tl *geo.Point, width, height float64, fill, fillStroke, st
return renderOval(tl, width, height, fill, fillStroke, stroke, style) + renderOval(innerTL, width-10, height-10, fill, "", stroke, style)
}
func defineGradients(writer io.Writer, cssGradient string) {
gradient, _ := color.ParseGradient(cssGradient)
fmt.Fprint(writer, fmt.Sprintf(`<defs>%s</defs>`, color.GradientToSVG(gradient)))
}
func defineShadowFilter(writer io.Writer) {
fmt.Fprint(writer, `<defs>
<filter id="shadow-filter" width="200%" height="200%" x="-50%" y="-50%">
@ -1824,6 +1829,29 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
}
}
if color.IsGradient(diagram.Root.Fill) {
defineGradients(buf, diagram.Root.Fill)
}
if color.IsGradient(diagram.Root.Stroke) {
defineGradients(buf, diagram.Root.Stroke)
}
for _, s := range diagram.Shapes {
if color.IsGradient(s.Fill) {
defineGradients(buf, s.Fill)
}
if color.IsGradient(s.Stroke) {
defineGradients(buf, s.Stroke)
}
if color.IsGradient(s.Color) {
defineGradients(buf, s.Color)
}
}
for _, c := range diagram.Connections {
if color.IsGradient(c.Stroke) {
defineGradients(buf, c.Stroke)
}
}
// Apply hash on IDs for targeting, to be specific for this diagram
diagramHash, err := diagram.HashID()
if err != nil {

View file

@ -178,11 +178,17 @@ func (el *ThemableElement) Render() string {
if color.IsThemeColor(el.Stroke) {
class += fmt.Sprintf(" stroke-%s", el.Stroke)
} else if len(el.Stroke) > 0 {
if color.IsGradient(el.Stroke) {
el.Stroke = fmt.Sprintf("url('#%s')", color.UniqueGradientID(el.Stroke))
}
out += fmt.Sprintf(` stroke="%s"`, el.Stroke)
}
if color.IsThemeColor(el.Fill) {
class += fmt.Sprintf(" fill-%s", el.Fill)
} else if len(el.Fill) > 0 {
if color.IsGradient(el.Fill) {
el.Fill = fmt.Sprintf("url('#%s')", color.UniqueGradientID(el.Fill))
}
out += fmt.Sprintf(` fill="%s"`, el.Fill)
}
if color.IsThemeColor(el.BackgroundColor) {

178
e2etests/testdata/txtar/gradient/dagre/board.exp.json generated vendored Normal file
View file

@ -0,0 +1,178 @@
{
"name": "",
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "gradient",
"type": "rectangle",
"pos": {
"x": 0,
"y": 0
},
"width": 106,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "linear-gradient(#f69d3c, #3f87a6)",
"stroke": "linear-gradient(to top right, red, blue)",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "gradient",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "radial-gradient(red, yellow, green, cyan, blue)",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 61,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "colors",
"type": "rectangle",
"pos": {
"x": 9,
"y": 166
},
"width": 89,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "linear-gradient(45deg, rgba(255,0,0,0.5) 0%, rgba(0,0,255,0.5) 100%)",
"stroke": "linear-gradient(to right, red, blue, green)",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "colors",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "linear-gradient(to bottom right, red 0%, yellow 25%, green 50%, cyan 75%, blue 100%)",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 44,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
}
],
"connections": [
{
"id": "(gradient -> colors)[0]",
"src": "gradient",
"srcArrow": "none",
"dst": "colors",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 53,
"y": 66
},
{
"x": 53,
"y": 106
},
{
"x": 53,
"y": 126
},
{
"x": 53,
"y": 166
}
],
"isCurve": true,
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
}
],
"root": {
"id": "",
"type": "",
"pos": {
"x": 0,
"y": 0
},
"width": 0,
"height": 0,
"opacity": 0,
"strokeDash": 0,
"strokeWidth": 0,
"borderRadius": 0,
"fill": "radial-gradient(circle, white 0%, #8A2BE2 60%, #4B0082 100%)",
"stroke": "",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 0,
"fontFamily": "",
"language": "",
"color": "",
"italic": false,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"zIndex": 0,
"level": 0
}
}

View file

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.6-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 108 234"><svg id="d2-svg" class="d2-387748570" width="108" height="234" viewBox="-1 -1 108 234"><rect x="-1.000000" y="-1.000000" width="108.000000" height="234.000000" rx="0.000000" fill="url('#grad-748b4596c77533b881df8829b0adfb302f4dd5d0')" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-387748570 .text-bold {
font-family: "d2-387748570-font-bold";
}
@font-face {
font-family: d2-387748570-font-bold;
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
.connection {
stroke-linecap: round;
stroke-linejoin: round;
}
.blend {
mix-blend-mode: multiply;
opacity: 0.5;
}
.d2-387748570 .fill-N1{fill:#0A0F25;}
.d2-387748570 .fill-N2{fill:#676C7E;}
.d2-387748570 .fill-N3{fill:#9499AB;}
.d2-387748570 .fill-N4{fill:#CFD2DD;}
.d2-387748570 .fill-N5{fill:#DEE1EB;}
.d2-387748570 .fill-N6{fill:#EEF1F8;}
.d2-387748570 .fill-N7{fill:#FFFFFF;}
.d2-387748570 .fill-B1{fill:#0D32B2;}
.d2-387748570 .fill-B2{fill:#0D32B2;}
.d2-387748570 .fill-B3{fill:#E3E9FD;}
.d2-387748570 .fill-B4{fill:#E3E9FD;}
.d2-387748570 .fill-B5{fill:#EDF0FD;}
.d2-387748570 .fill-B6{fill:#F7F8FE;}
.d2-387748570 .fill-AA2{fill:#4A6FF3;}
.d2-387748570 .fill-AA4{fill:#EDF0FD;}
.d2-387748570 .fill-AA5{fill:#F7F8FE;}
.d2-387748570 .fill-AB4{fill:#EDF0FD;}
.d2-387748570 .fill-AB5{fill:#F7F8FE;}
.d2-387748570 .stroke-N1{stroke:#0A0F25;}
.d2-387748570 .stroke-N2{stroke:#676C7E;}
.d2-387748570 .stroke-N3{stroke:#9499AB;}
.d2-387748570 .stroke-N4{stroke:#CFD2DD;}
.d2-387748570 .stroke-N5{stroke:#DEE1EB;}
.d2-387748570 .stroke-N6{stroke:#EEF1F8;}
.d2-387748570 .stroke-N7{stroke:#FFFFFF;}
.d2-387748570 .stroke-B1{stroke:#0D32B2;}
.d2-387748570 .stroke-B2{stroke:#0D32B2;}
.d2-387748570 .stroke-B3{stroke:#E3E9FD;}
.d2-387748570 .stroke-B4{stroke:#E3E9FD;}
.d2-387748570 .stroke-B5{stroke:#EDF0FD;}
.d2-387748570 .stroke-B6{stroke:#F7F8FE;}
.d2-387748570 .stroke-AA2{stroke:#4A6FF3;}
.d2-387748570 .stroke-AA4{stroke:#EDF0FD;}
.d2-387748570 .stroke-AA5{stroke:#F7F8FE;}
.d2-387748570 .stroke-AB4{stroke:#EDF0FD;}
.d2-387748570 .stroke-AB5{stroke:#F7F8FE;}
.d2-387748570 .background-color-N1{background-color:#0A0F25;}
.d2-387748570 .background-color-N2{background-color:#676C7E;}
.d2-387748570 .background-color-N3{background-color:#9499AB;}
.d2-387748570 .background-color-N4{background-color:#CFD2DD;}
.d2-387748570 .background-color-N5{background-color:#DEE1EB;}
.d2-387748570 .background-color-N6{background-color:#EEF1F8;}
.d2-387748570 .background-color-N7{background-color:#FFFFFF;}
.d2-387748570 .background-color-B1{background-color:#0D32B2;}
.d2-387748570 .background-color-B2{background-color:#0D32B2;}
.d2-387748570 .background-color-B3{background-color:#E3E9FD;}
.d2-387748570 .background-color-B4{background-color:#E3E9FD;}
.d2-387748570 .background-color-B5{background-color:#EDF0FD;}
.d2-387748570 .background-color-B6{background-color:#F7F8FE;}
.d2-387748570 .background-color-AA2{background-color:#4A6FF3;}
.d2-387748570 .background-color-AA4{background-color:#EDF0FD;}
.d2-387748570 .background-color-AA5{background-color:#F7F8FE;}
.d2-387748570 .background-color-AB4{background-color:#EDF0FD;}
.d2-387748570 .background-color-AB5{background-color:#F7F8FE;}
.d2-387748570 .color-N1{color:#0A0F25;}
.d2-387748570 .color-N2{color:#676C7E;}
.d2-387748570 .color-N3{color:#9499AB;}
.d2-387748570 .color-N4{color:#CFD2DD;}
.d2-387748570 .color-N5{color:#DEE1EB;}
.d2-387748570 .color-N6{color:#EEF1F8;}
.d2-387748570 .color-N7{color:#FFFFFF;}
.d2-387748570 .color-B1{color:#0D32B2;}
.d2-387748570 .color-B2{color:#0D32B2;}
.d2-387748570 .color-B3{color:#E3E9FD;}
.d2-387748570 .color-B4{color:#E3E9FD;}
.d2-387748570 .color-B5{color:#EDF0FD;}
.d2-387748570 .color-B6{color:#F7F8FE;}
.d2-387748570 .color-AA2{color:#4A6FF3;}
.d2-387748570 .color-AA4{color:#EDF0FD;}
.d2-387748570 .color-AA5{color:#F7F8FE;}
.d2-387748570 .color-AB4{color:#EDF0FD;}
.d2-387748570 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]></style><defs><radialGradient id="grad-748b4596c77533b881df8829b0adfb302f4dd5d0">
<stop offset="0%" stop-color="white" />
<stop offset="60%" stop-color="#8A2BE2" />
<stop offset="100%" stop-color="#4B0082" />
</radialGradient></defs><defs><linearGradient id="grad-984dfec132f72578000afe61cf8daaed70c7c3de" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0.00%" stop-color="#f69d3c" />
<stop offset="100.00%" stop-color="#3f87a6" />
</linearGradient></defs><defs><linearGradient id="grad-38ebdc3aba5158fdec998f13cadd2b81d5d3e467" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0.00%" stop-color="red" />
<stop offset="100.00%" stop-color="blue" />
</linearGradient></defs><defs><radialGradient id="grad-df376e8348556a67f106fb4c1ec75fc6e38aabea">
<stop offset="0.00%" stop-color="red" />
<stop offset="25.00%" stop-color="yellow" />
<stop offset="50.00%" stop-color="green" />
<stop offset="75.00%" stop-color="cyan" />
<stop offset="100.00%" stop-color="blue" />
</radialGradient></defs><defs><linearGradient id="grad-867086f31fcac752507fbbbe0405e35050a4704e" x1="50.00%" y1="50.00%" x2="85.36%" y2="85.36%">
<stop offset="0%" stop-color="rgba(255,0,0,0.5)" />
<stop offset="100%" stop-color="rgba(0,0,255,0.5)" />
</linearGradient></defs><defs><linearGradient id="grad-6a6578853b2dd58addb4313cedbcddecb53a2df1" x1="0%" y1="50%" x2="100%" y2="50%">
<stop offset="0.00%" stop-color="red" />
<stop offset="50.00%" stop-color="blue" />
<stop offset="100.00%" stop-color="green" />
</linearGradient></defs><defs><linearGradient id="grad-ac2d0b347164c939a2dee5c122da0c2ada91319c" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="red" />
<stop offset="25%" stop-color="yellow" />
<stop offset="50%" stop-color="green" />
<stop offset="75%" stop-color="cyan" />
<stop offset="100%" stop-color="blue" />
</linearGradient></defs><g id="gradient"><g class="shape" ><rect x="0.000000" y="0.000000" width="106.000000" height="66.000000" stroke="url('#grad-38ebdc3aba5158fdec998f13cadd2b81d5d3e467')" fill="url('#grad-984dfec132f72578000afe61cf8daaed70c7c3de')" style="stroke-width:2;" /></g><text x="53.000000" y="38.500000" fill="url('#grad-df376e8348556a67f106fb4c1ec75fc6e38aabea')" class="text-bold" style="text-anchor:middle;font-size:16px">gradient</text></g><g id="colors"><g class="shape" ><rect x="9.000000" y="166.000000" width="89.000000" height="66.000000" stroke="url('#grad-6a6578853b2dd58addb4313cedbcddecb53a2df1')" fill="url('#grad-867086f31fcac752507fbbbe0405e35050a4704e')" style="stroke-width:2;" /></g><text x="53.500000" y="204.500000" fill="url('#grad-ac2d0b347164c939a2dee5c122da0c2ada91319c')" class="text-bold" style="text-anchor:middle;font-size:16px">colors</text></g><g id="(gradient -&gt; colors)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 53.000000 68.000000 C 53.000000 106.000000 53.000000 126.000000 53.000000 162.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-387748570)" /></g><mask id="d2-387748570" maskUnits="userSpaceOnUse" x="-1" y="-1" width="108" height="234">
<rect x="-1" y="-1" width="108" height="234" fill="white"></rect>
<rect x="22.500000" y="22.500000" width="61" height="21" fill="rgba(0,0,0,0.75)"></rect>
<rect x="31.500000" y="188.500000" width="44" height="21" fill="rgba(0,0,0,0.75)"></rect>
</mask></svg></svg>

After

Width:  |  Height:  |  Size: 13 KiB

169
e2etests/testdata/txtar/gradient/elk/board.exp.json generated vendored Normal file
View file

@ -0,0 +1,169 @@
{
"name": "",
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "gradient",
"type": "rectangle",
"pos": {
"x": 12,
"y": 12
},
"width": 106,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "linear-gradient(#f69d3c, #3f87a6)",
"stroke": "linear-gradient(to top right, red, blue)",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "gradient",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "radial-gradient(red, yellow, green, cyan, blue)",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 61,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "colors",
"type": "rectangle",
"pos": {
"x": 20,
"y": 148
},
"width": 89,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "linear-gradient(45deg, rgba(255,0,0,0.5) 0%, rgba(0,0,255,0.5) 100%)",
"stroke": "linear-gradient(to right, red, blue, green)",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "colors",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "linear-gradient(to bottom right, red 0%, yellow 25%, green 50%, cyan 75%, blue 100%)",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 44,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
}
],
"connections": [
{
"id": "(gradient -> colors)[0]",
"src": "gradient",
"srcArrow": "none",
"dst": "colors",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 65,
"y": 78
},
{
"x": 65,
"y": 148
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
}
],
"root": {
"id": "",
"type": "",
"pos": {
"x": 0,
"y": 0
},
"width": 0,
"height": 0,
"opacity": 0,
"strokeDash": 0,
"strokeWidth": 0,
"borderRadius": 0,
"fill": "radial-gradient(circle, white 0%, #8A2BE2 60%, #4B0082 100%)",
"stroke": "",
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 0,
"fontFamily": "",
"language": "",
"color": "",
"italic": false,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"zIndex": 0,
"level": 0
}
}

View file

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="v0.6.6-HEAD" preserveAspectRatio="xMinYMin meet" viewBox="0 0 108 204"><svg id="d2-svg" class="d2-3100767741" width="108" height="204" viewBox="11 11 108 204"><rect x="11.000000" y="11.000000" width="108.000000" height="204.000000" rx="0.000000" fill="url('#grad-748b4596c77533b881df8829b0adfb302f4dd5d0')" stroke-width="0" /><style type="text/css"><![CDATA[
.d2-3100767741 .text-bold {
font-family: "d2-3100767741-font-bold";
}
@font-face {
font-family: d2-3100767741-font-bold;
src: url("data:application/font-woff;base64,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");
}]]></style><style type="text/css"><![CDATA[.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
.connection {
stroke-linecap: round;
stroke-linejoin: round;
}
.blend {
mix-blend-mode: multiply;
opacity: 0.5;
}
.d2-3100767741 .fill-N1{fill:#0A0F25;}
.d2-3100767741 .fill-N2{fill:#676C7E;}
.d2-3100767741 .fill-N3{fill:#9499AB;}
.d2-3100767741 .fill-N4{fill:#CFD2DD;}
.d2-3100767741 .fill-N5{fill:#DEE1EB;}
.d2-3100767741 .fill-N6{fill:#EEF1F8;}
.d2-3100767741 .fill-N7{fill:#FFFFFF;}
.d2-3100767741 .fill-B1{fill:#0D32B2;}
.d2-3100767741 .fill-B2{fill:#0D32B2;}
.d2-3100767741 .fill-B3{fill:#E3E9FD;}
.d2-3100767741 .fill-B4{fill:#E3E9FD;}
.d2-3100767741 .fill-B5{fill:#EDF0FD;}
.d2-3100767741 .fill-B6{fill:#F7F8FE;}
.d2-3100767741 .fill-AA2{fill:#4A6FF3;}
.d2-3100767741 .fill-AA4{fill:#EDF0FD;}
.d2-3100767741 .fill-AA5{fill:#F7F8FE;}
.d2-3100767741 .fill-AB4{fill:#EDF0FD;}
.d2-3100767741 .fill-AB5{fill:#F7F8FE;}
.d2-3100767741 .stroke-N1{stroke:#0A0F25;}
.d2-3100767741 .stroke-N2{stroke:#676C7E;}
.d2-3100767741 .stroke-N3{stroke:#9499AB;}
.d2-3100767741 .stroke-N4{stroke:#CFD2DD;}
.d2-3100767741 .stroke-N5{stroke:#DEE1EB;}
.d2-3100767741 .stroke-N6{stroke:#EEF1F8;}
.d2-3100767741 .stroke-N7{stroke:#FFFFFF;}
.d2-3100767741 .stroke-B1{stroke:#0D32B2;}
.d2-3100767741 .stroke-B2{stroke:#0D32B2;}
.d2-3100767741 .stroke-B3{stroke:#E3E9FD;}
.d2-3100767741 .stroke-B4{stroke:#E3E9FD;}
.d2-3100767741 .stroke-B5{stroke:#EDF0FD;}
.d2-3100767741 .stroke-B6{stroke:#F7F8FE;}
.d2-3100767741 .stroke-AA2{stroke:#4A6FF3;}
.d2-3100767741 .stroke-AA4{stroke:#EDF0FD;}
.d2-3100767741 .stroke-AA5{stroke:#F7F8FE;}
.d2-3100767741 .stroke-AB4{stroke:#EDF0FD;}
.d2-3100767741 .stroke-AB5{stroke:#F7F8FE;}
.d2-3100767741 .background-color-N1{background-color:#0A0F25;}
.d2-3100767741 .background-color-N2{background-color:#676C7E;}
.d2-3100767741 .background-color-N3{background-color:#9499AB;}
.d2-3100767741 .background-color-N4{background-color:#CFD2DD;}
.d2-3100767741 .background-color-N5{background-color:#DEE1EB;}
.d2-3100767741 .background-color-N6{background-color:#EEF1F8;}
.d2-3100767741 .background-color-N7{background-color:#FFFFFF;}
.d2-3100767741 .background-color-B1{background-color:#0D32B2;}
.d2-3100767741 .background-color-B2{background-color:#0D32B2;}
.d2-3100767741 .background-color-B3{background-color:#E3E9FD;}
.d2-3100767741 .background-color-B4{background-color:#E3E9FD;}
.d2-3100767741 .background-color-B5{background-color:#EDF0FD;}
.d2-3100767741 .background-color-B6{background-color:#F7F8FE;}
.d2-3100767741 .background-color-AA2{background-color:#4A6FF3;}
.d2-3100767741 .background-color-AA4{background-color:#EDF0FD;}
.d2-3100767741 .background-color-AA5{background-color:#F7F8FE;}
.d2-3100767741 .background-color-AB4{background-color:#EDF0FD;}
.d2-3100767741 .background-color-AB5{background-color:#F7F8FE;}
.d2-3100767741 .color-N1{color:#0A0F25;}
.d2-3100767741 .color-N2{color:#676C7E;}
.d2-3100767741 .color-N3{color:#9499AB;}
.d2-3100767741 .color-N4{color:#CFD2DD;}
.d2-3100767741 .color-N5{color:#DEE1EB;}
.d2-3100767741 .color-N6{color:#EEF1F8;}
.d2-3100767741 .color-N7{color:#FFFFFF;}
.d2-3100767741 .color-B1{color:#0D32B2;}
.d2-3100767741 .color-B2{color:#0D32B2;}
.d2-3100767741 .color-B3{color:#E3E9FD;}
.d2-3100767741 .color-B4{color:#E3E9FD;}
.d2-3100767741 .color-B5{color:#EDF0FD;}
.d2-3100767741 .color-B6{color:#F7F8FE;}
.d2-3100767741 .color-AA2{color:#4A6FF3;}
.d2-3100767741 .color-AA4{color:#EDF0FD;}
.d2-3100767741 .color-AA5{color:#F7F8FE;}
.d2-3100767741 .color-AB4{color:#EDF0FD;}
.d2-3100767741 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]></style><defs><radialGradient id="grad-748b4596c77533b881df8829b0adfb302f4dd5d0">
<stop offset="0%" stop-color="white" />
<stop offset="60%" stop-color="#8A2BE2" />
<stop offset="100%" stop-color="#4B0082" />
</radialGradient></defs><defs><linearGradient id="grad-984dfec132f72578000afe61cf8daaed70c7c3de" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0.00%" stop-color="#f69d3c" />
<stop offset="100.00%" stop-color="#3f87a6" />
</linearGradient></defs><defs><linearGradient id="grad-38ebdc3aba5158fdec998f13cadd2b81d5d3e467" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0.00%" stop-color="red" />
<stop offset="100.00%" stop-color="blue" />
</linearGradient></defs><defs><radialGradient id="grad-df376e8348556a67f106fb4c1ec75fc6e38aabea">
<stop offset="0.00%" stop-color="red" />
<stop offset="25.00%" stop-color="yellow" />
<stop offset="50.00%" stop-color="green" />
<stop offset="75.00%" stop-color="cyan" />
<stop offset="100.00%" stop-color="blue" />
</radialGradient></defs><defs><linearGradient id="grad-867086f31fcac752507fbbbe0405e35050a4704e" x1="50.00%" y1="50.00%" x2="85.36%" y2="85.36%">
<stop offset="0%" stop-color="rgba(255,0,0,0.5)" />
<stop offset="100%" stop-color="rgba(0,0,255,0.5)" />
</linearGradient></defs><defs><linearGradient id="grad-6a6578853b2dd58addb4313cedbcddecb53a2df1" x1="0%" y1="50%" x2="100%" y2="50%">
<stop offset="0.00%" stop-color="red" />
<stop offset="50.00%" stop-color="blue" />
<stop offset="100.00%" stop-color="green" />
</linearGradient></defs><defs><linearGradient id="grad-ac2d0b347164c939a2dee5c122da0c2ada91319c" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="red" />
<stop offset="25%" stop-color="yellow" />
<stop offset="50%" stop-color="green" />
<stop offset="75%" stop-color="cyan" />
<stop offset="100%" stop-color="blue" />
</linearGradient></defs><g id="gradient"><g class="shape" ><rect x="12.000000" y="12.000000" width="106.000000" height="66.000000" stroke="url('#grad-38ebdc3aba5158fdec998f13cadd2b81d5d3e467')" fill="url('#grad-984dfec132f72578000afe61cf8daaed70c7c3de')" style="stroke-width:2;" /></g><text x="65.000000" y="50.500000" fill="url('#grad-df376e8348556a67f106fb4c1ec75fc6e38aabea')" class="text-bold" style="text-anchor:middle;font-size:16px">gradient</text></g><g id="colors"><g class="shape" ><rect x="20.000000" y="148.000000" width="89.000000" height="66.000000" stroke="url('#grad-6a6578853b2dd58addb4313cedbcddecb53a2df1')" fill="url('#grad-867086f31fcac752507fbbbe0405e35050a4704e')" style="stroke-width:2;" /></g><text x="64.500000" y="186.500000" fill="url('#grad-ac2d0b347164c939a2dee5c122da0c2ada91319c')" class="text-bold" style="text-anchor:middle;font-size:16px">colors</text></g><g id="(gradient -&gt; colors)[0]"><marker id="mk-3488378134" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" class="connection fill-B1" stroke-width="2" /> </marker><path d="M 65.000000 80.000000 L 65.000000 144.000000" fill="none" class="connection stroke-B1" style="stroke-width:2;" marker-end="url(#mk-3488378134)" mask="url(#d2-3100767741)" /></g><mask id="d2-3100767741" maskUnits="userSpaceOnUse" x="11" y="11" width="108" height="204">
<rect x="11" y="11" width="108" height="204" fill="white"></rect>
<rect x="34.500000" y="34.500000" width="61" height="21" fill="rgba(0,0,0,0.75)"></rect>
<rect x="42.500000" y="170.500000" width="44" height="21" fill="rgba(0,0,0,0.75)"></rect>
</mask></svg></svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -455,3 +455,17 @@ bob -> alice: The ability to play bridge or\ngolf as if they were games.
◎: |md
◎ foo bar
|
-- gradient --
style.fill: "radial-gradient(circle, white 0%, #8A2BE2 60%, #4B0082 100%)"
gradient: {
style.fill: "linear-gradient(#f69d3c, #3f87a6)"
style.stroke: "linear-gradient(to top right, red, blue)"
style.font-color: "radial-gradient(red, yellow, green, cyan, blue)"
}
colors: {
style.fill: "linear-gradient(45deg, rgba(255,0,0,0.5) 0%, rgba(0,0,255,0.5) 100%)"
style.stroke: "linear-gradient(to right, red, blue, green)"
style.font-color: "linear-gradient(to bottom right, red 0%, yellow 25%, green 50%, cyan 75%, blue 100%)"
}
gradient -> colors

View file

@ -9,6 +9,7 @@ import (
"github.com/lucasb-eyer/go-colorful"
"github.com/mazznoer/csscolorparser"
"oss.terrastruct.com/util-go/go2"
)
var themeColorRegex = regexp.MustCompile(`^(N[1-7]|B[1-6]|AA[245]|AB[45])$`)
@ -503,3 +504,11 @@ var NamedColors = []string{
}
var ColorHexRegex = regexp.MustCompile(`^#(([0-9a-fA-F]{2}){3}|([0-9a-fA-F]){3})$`)
func ValidColor(color string) bool {
if !go2.Contains(NamedColors, strings.ToLower(color)) && !ColorHexRegex.MatchString(color) && !IsGradient(color) {
return false
}
return true
}

248
lib/color/gradient.go Normal file
View file

@ -0,0 +1,248 @@
package color
import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"
)
type Gradient struct {
Type string
Direction string
ColorStops []ColorStop
ID string
}
type ColorStop struct {
Color string
Position string
}
func ParseGradient(cssGradient string) (Gradient, error) {
cssGradient = strings.TrimSpace(cssGradient)
re := regexp.MustCompile(`^(linear-gradient|radial-gradient)\((.*)\)$`)
matches := re.FindStringSubmatch(cssGradient)
if matches == nil {
return Gradient{}, errors.New("invalid gradient syntax")
}
gradientType := matches[1]
params := matches[2]
gradient := Gradient{
Type: strings.TrimSuffix(gradientType, "-gradient"),
}
paramList := splitParams(params)
if len(paramList) == 0 {
return Gradient{}, errors.New("no parameters in gradient")
}
firstParam := strings.TrimSpace(paramList[0])
if gradient.Type == "linear" && (strings.HasSuffix(firstParam, "deg") || strings.HasPrefix(firstParam, "to ")) {
gradient.Direction = firstParam
colorStops := paramList[1:]
if len(colorStops) == 0 {
return Gradient{}, errors.New("no color stops in gradient")
}
gradient.ColorStops = parseColorStops(colorStops)
} else if gradient.Type == "radial" && (firstParam == "circle" || firstParam == "ellipse") {
gradient.Direction = firstParam
colorStops := paramList[1:]
if len(colorStops) == 0 {
return Gradient{}, errors.New("no color stops in gradient")
}
gradient.ColorStops = parseColorStops(colorStops)
} else {
gradient.ColorStops = parseColorStops(paramList)
}
gradient.ID = UniqueGradientID(cssGradient)
return gradient, nil
}
func splitParams(params string) []string {
var parts []string
var buf strings.Builder
nesting := 0
for _, r := range params {
switch r {
case ',':
if nesting == 0 {
parts = append(parts, buf.String())
buf.Reset()
continue
}
case '(':
nesting++
case ')':
if nesting > 0 {
nesting--
}
}
buf.WriteRune(r)
}
if buf.Len() > 0 {
parts = append(parts, buf.String())
}
return parts
}
func parseColorStops(params []string) []ColorStop {
var colorStops []ColorStop
for _, p := range params {
p = strings.TrimSpace(p)
parts := strings.Fields(p)
switch len(parts) {
case 1:
colorStops = append(colorStops, ColorStop{Color: parts[0]})
case 2:
colorStops = append(colorStops, ColorStop{Color: parts[0], Position: parts[1]})
default:
continue
}
}
return colorStops
}
func GradientToSVG(gradient Gradient) string {
switch gradient.Type {
case "linear":
return LinearGradientToSVG(gradient)
case "radial":
return RadialGradientToSVG(gradient)
default:
return ""
}
}
func LinearGradientToSVG(gradient Gradient) string {
x1, y1, x2, y2 := parseLinearGradientDirection(gradient.Direction)
var sb strings.Builder
sb.WriteString(fmt.Sprintf(`<linearGradient id="%s" `, gradient.ID))
sb.WriteString(fmt.Sprintf(`x1="%s" y1="%s" x2="%s" y2="%s">`, x1, y1, x2, y2))
sb.WriteString("\n")
totalStops := len(gradient.ColorStops)
for i, cs := range gradient.ColorStops {
offset := cs.Position
if offset == "" {
offsetValue := float64(i) / float64(totalStops-1) * 100
offset = fmt.Sprintf("%.2f%%", offsetValue)
}
sb.WriteString(fmt.Sprintf(`<stop offset="%s" stop-color="%s" />`, offset, cs.Color))
sb.WriteString("\n")
}
sb.WriteString(`</linearGradient>`)
return sb.String()
}
func parseLinearGradientDirection(direction string) (x1, y1, x2, y2 string) {
x1, y1, x2, y2 = "0%", "0%", "0%", "100%"
direction = strings.TrimSpace(direction)
if strings.HasPrefix(direction, "to ") {
dir := strings.TrimPrefix(direction, "to ")
dir = strings.TrimSpace(dir)
parts := strings.Fields(dir)
xStart, yStart := "50%", "50%"
xEnd, yEnd := "50%", "50%"
xDirSet, yDirSet := false, false
for _, part := range parts {
switch part {
case "left":
xStart = "100%"
xEnd = "0%"
xDirSet = true
case "right":
xStart = "0%"
xEnd = "100%"
xDirSet = true
case "top":
yStart = "100%"
yEnd = "0%"
yDirSet = true
case "bottom":
yStart = "0%"
yEnd = "100%"
yDirSet = true
}
}
if !xDirSet {
xStart = "50%"
xEnd = "50%"
}
if !yDirSet {
yStart = "50%"
yEnd = "50%"
}
x1, y1 = xStart, yStart
x2, y2 = xEnd, yEnd
} else if strings.HasSuffix(direction, "deg") {
angleStr := strings.TrimSuffix(direction, "deg")
angle, err := strconv.ParseFloat(strings.TrimSpace(angleStr), 64)
if err == nil {
cssAngle := angle
svgAngle := (90 - cssAngle) * (math.Pi / 180)
x1f := 50.0
y1f := 50.0
x2f := x1f + 50*math.Cos(svgAngle)
y2f := y1f + 50*math.Sin(svgAngle)
x1 = fmt.Sprintf("%.2f%%", x1f)
y1 = fmt.Sprintf("%.2f%%", y1f)
x2 = fmt.Sprintf("%.2f%%", x2f)
y2 = fmt.Sprintf("%.2f%%", y2f)
}
}
return x1, y1, x2, y2
}
func RadialGradientToSVG(gradient Gradient) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf(`<radialGradient id="%s">`, gradient.ID))
sb.WriteString("\n")
totalStops := len(gradient.ColorStops)
for i, cs := range gradient.ColorStops {
offset := cs.Position
if offset == "" {
offsetValue := float64(i) / float64(totalStops-1) * 100
offset = fmt.Sprintf("%.2f%%", offsetValue)
}
sb.WriteString(fmt.Sprintf(`<stop offset="%s" stop-color="%s" />`, offset, cs.Color))
sb.WriteString("\n")
}
sb.WriteString(`</radialGradient>`)
return sb.String()
}
func UniqueGradientID(cssGradient string) string {
h := sha1.New()
h.Write([]byte(cssGradient))
hash := hex.EncodeToString(h.Sum(nil))
return "grad-" + hash
}
var GradientRegex = regexp.MustCompile(`^(linear|radial)-gradient\((.+)\)$`)
func IsGradient(color string) bool {
return GradientRegex.MatchString(color)
}