diff --git a/d2renderers/d2sketch/fillpattern.svg b/d2renderers/d2sketch/fillpattern.svg
deleted file mode 100644
index 0bcc3f228..000000000
--- a/d2renderers/d2sketch/fillpattern.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/d2renderers/d2sketch/sketch.go b/d2renderers/d2sketch/sketch.go
index 19c68c4a8..e5ceab8aa 100644
--- a/d2renderers/d2sketch/sketch.go
+++ b/d2renderers/d2sketch/sketch.go
@@ -17,9 +17,6 @@ import (
"oss.terrastruct.com/util-go/go2"
)
-//go:embed fillpattern.svg
-var fillPattern string
-
//go:embed rough.js
var roughJS string
@@ -51,17 +48,23 @@ func InitSketchVM() (*Runner, error) {
return &r, nil
}
-// DefineFillPattern adds a reusable pattern that is overlayed on shapes with
+// DefineFillPatterns adds reusable patterns that are overlayed on shapes with
// fill. This gives it a subtle streaky effect that subtly looks hand-drawn but
// not distractingly so.
-func DefineFillPattern() string {
- return fmt.Sprintf(`
-
- %s
-
-`, fillPattern)
+func DefineFillPatterns() string {
+ out := ""
+ out += defineFillPattern("bright", "rgba(0, 0, 0, 0.1)")
+ out += defineFillPattern("normal", "rgba(0, 0, 0, 0.16)")
+ out += defineFillPattern("dark", "rgba(0, 0, 0, 0.32)")
+ out += defineFillPattern("darker", "rgba(255, 255, 255, 0.24)")
+ out += ""
+ return out
+}
+
+func defineFillPattern(luminanceCategory, fill string) string {
+ return fmt.Sprintf(`
+
+ `, luminanceCategory, fill)
}
func Rect(r *Runner, shape d2target.Shape) (string, error) {
@@ -85,10 +88,17 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
pathEl.D = p
output += pathEl.Render()
}
- output += fmt.Sprintf(
- ``,
- shape.Pos.X, shape.Pos.Y, shape.Width, shape.Height,
- )
+
+ sketchOEl := svg_style.NewThemableElement("rect")
+ sketchOEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
+ sketchOEl.Width = float64(shape.Width)
+ sketchOEl.Height = float64(shape.Height)
+ renderedSO, err := svg_style.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
+ if err != nil {
+ return "", err
+ }
+ output += renderedSO
+
return output, nil
}
@@ -113,10 +123,20 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) {
pathEl.D = p
output += pathEl.Render()
}
- output += fmt.Sprintf(
- ``,
- shape.Pos.X+shape.Width/2, shape.Pos.Y+shape.Height/2, shape.Width/2, shape.Height/2,
- )
+
+ soElement := svg_style.NewThemableElement("ellipse")
+ soElement.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X+shape.Width/2, shape.Pos.Y+shape.Height/2)
+ soElement.Rx = float64(shape.Width / 2)
+ soElement.Ry = float64(shape.Height / 2)
+ renderedSO, err := svg_style.NewThemableSketchOverlay(
+ soElement,
+ pathEl.Fill,
+ ).Render()
+ if err != nil {
+ return "", err
+ }
+ output += renderedSO
+
return output, nil
}
@@ -142,11 +162,18 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
pathEl.D = p
output += pathEl.Render()
}
+
+ soElement := svg_style.NewThemableElement("path")
for _, p := range sketchPaths {
- output += fmt.Sprintf(
- ``,
- p,
- )
+ soElement.D = p
+ renderedSO, err := svg_style.NewThemableSketchOverlay(
+ soElement,
+ pathEl.Fill,
+ ).Render()
+ if err != nil {
+ return "", err
+ }
+ output += renderedSO
}
}
return output, nil
@@ -299,10 +326,17 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
output += pathEl.Render()
}
}
- output += fmt.Sprintf(
- ``,
- shape.Pos.X, shape.Pos.Y, shape.Width, shape.Height,
- )
+
+ sketchOEl := svg_style.NewThemableElement("rect")
+ sketchOEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
+ sketchOEl.Width = float64(shape.Width)
+ sketchOEl.Height = float64(shape.Height)
+ renderedSO, err := svg_style.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
+ if err != nil {
+ return "", err
+ }
+ output += renderedSO
+
return output, nil
}
@@ -353,10 +387,15 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
output += pathEl.Render()
}
- output += fmt.Sprintf(
- ``,
- shape.Pos.X, shape.Pos.Y, shape.Width, headerBox.Height,
- )
+ sketchOEl := svg_style.NewThemableElement("rect")
+ sketchOEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y)
+ sketchOEl.Width = float64(shape.Width)
+ sketchOEl.Height = headerBox.Height
+ renderedSO, err := svg_style.NewThemableSketchOverlay(sketchOEl, pathEl.Fill).Render()
+ if err != nil {
+ return "", err
+ }
+ output += renderedSO
if shape.Label != "" {
tl := label.InsideMiddleCenter.GetPointOnBox(
diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index e2b4d23c3..2c1b82bf7 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -56,9 +56,6 @@ var LinkIcon string
//go:embed style.css
var baseStylesheet string
-//go:embed sketchstyle.css
-var sketchStyleCSS string
-
//go:embed github-markdown.css
var mdCSS string
@@ -1248,12 +1245,11 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
backgroundEl.Fill = color.N7
// generate elements that will be appended to the SVG tag
- themeStylesheet := themeCSS(themeID, darkThemeID)
- sketchStylesheet := ""
- if sketchRunner != nil {
- sketchStylesheet = "\n" + sketchStyleCSS
+ themeStylesheet, err := themeCSS(themeID, darkThemeID)
+ if err != nil {
+ return nil, err
}
- svgOut := fmt.Sprintf(``, baseStylesheet, themeStylesheet, sketchStylesheet)
+ svgOut := fmt.Sprintf(``, baseStylesheet, themeStylesheet)
// this script won't run in --watch mode because script tags are ignored when added via el.innerHTML = element
// https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
svgOut += fmt.Sprintf(``, fitToScreenScript)
@@ -1268,7 +1264,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
svgOut += fmt.Sprintf(``, mdCSS)
}
if sketchRunner != nil {
- svgOut += d2sketch.DefineFillPattern()
+ svgOut += d2sketch.DefineFillPatterns()
}
svgOut += embedFonts(buf, diagram.FontFamily)
@@ -1282,20 +1278,29 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
return []byte(docRendered), nil
}
-func themeCSS(themeID, darkThemeID int64) (stylesheet string) {
- out := singleThemeRulesets(themeID)
-
- if darkThemeID != math.MaxInt64 {
- out += fmt.Sprintf("@media screen and (prefers-color-scheme:dark){%s}", singleThemeRulesets(darkThemeID))
+// TODO include only colors that are being used to reduce size
+func themeCSS(themeID, darkThemeID int64) (stylesheet string, err error) {
+ out, err := singleThemeRulesets(themeID)
+ if err != nil {
+ return "", err
}
- return out
+ if darkThemeID != math.MaxInt64 {
+ darkOut, err := singleThemeRulesets(darkThemeID)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf("@media screen and (prefers-color-scheme:dark){%s}", darkOut)
+ }
+
+ return out, nil
}
-func singleThemeRulesets(themeID int64) (rulesets string) {
+func singleThemeRulesets(themeID int64) (rulesets string, err error) {
out := ""
theme := d2themescatalog.Find(themeID)
+ // Global theme colors
for _, property := range []string{"fill", "stroke", "background-color", "color"} {
out += fmt.Sprintf(".%s-N1{%s:%s;}.%s-N2{%s:%s;}.%s-N3{%s:%s;}.%s-N4{%s:%s;}.%s-N5{%s:%s;}.%s-N6{%s:%s;}.%s-N7{%s:%s;}.%s-B1{%s:%s;}.%s-B2{%s:%s;}.%s-B3{%s:%s;}.%s-B4{%s:%s;}.%s-B5{%s:%s;}.%s-B6{%s:%s;}.%s-AA2{%s:%s;}.%s-AA4{%s:%s;}.%s-AA5{%s:%s;}.%s-AB4{%s:%s;}.%s-AB5{%s:%s;}",
property, property, theme.Colors.Neutrals.N1,
@@ -1319,6 +1324,7 @@ func singleThemeRulesets(themeID int64) (rulesets string) {
)
}
+ // Markdown specific rulesets
out += fmt.Sprintf(".md{--color-fg-default:%s;--color-fg-muted:%s;--color-fg-subtle:%s;--color-canvas-default:%s;--color-canvas-subtle:%s;--color-border-default:%s;--color-border-muted:%s;--color-neutral-muted:%s;--color-accent-fg:%s;--color-accent-emphasis:%s;--color-attention-subtle:%s;--color-danger-fg:%s;}",
theme.Colors.Neutrals.N1, theme.Colors.Neutrals.N2, theme.Colors.Neutrals.N3,
theme.Colors.Neutrals.N7, theme.Colors.Neutrals.N6,
@@ -1329,7 +1335,91 @@ func singleThemeRulesets(themeID int64) (rulesets string) {
"red",
)
- return out
+ // Sketch style specific rulesets
+ lc, err := color.LuminanceCategory(theme.Colors.B1)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B1, lc, blendMode(lc))
+ lc, err = color.LuminanceCategory(theme.Colors.B2)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B2, lc, blendMode(lc))
+ lc, err = color.LuminanceCategory(theme.Colors.B3)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B3, lc, blendMode(lc))
+ lc, err = color.LuminanceCategory(theme.Colors.B4)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B4, lc, blendMode(lc))
+ lc, err = color.LuminanceCategory(theme.Colors.B5)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B5, lc, blendMode(lc))
+ lc, err = color.LuminanceCategory(theme.Colors.B6)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B6, lc, blendMode(lc))
+
+ lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N1)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N1, lc, blendMode(lc))
+ lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N2)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N2, lc, blendMode(lc))
+ lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N3)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N3, lc, blendMode(lc))
+ lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N4)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N4, lc, blendMode(lc))
+ lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N5)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N5, lc, blendMode(lc))
+ lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N6)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N6, lc, blendMode(lc))
+ lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N7)
+ if err != nil {
+ return "", err
+ }
+ out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N7, lc, blendMode(lc))
+
+ // TODO Add the rest of the colors so we can allow the user to specify theme colors too
+
+ return out, nil
+}
+
+func blendMode(lc string) string {
+ switch lc {
+ case "bright":
+ return "darken"
+ case "normal":
+ return "color-burn"
+ case "dark":
+ return "overlay"
+ case "darker":
+ return "lighten"
+ }
+ panic("invalid luminance category")
}
type DiagramObject interface {
diff --git a/d2renderers/d2svg/sketchstyle.css b/d2renderers/d2svg/sketchstyle.css
deleted file mode 100644
index 33654aba2..000000000
--- a/d2renderers/d2svg/sketchstyle.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.sketch-overlay {
- fill: url(#streaks);
- mix-blend-mode: overlay;
-}
diff --git a/d2renderers/d2svg/style.css b/d2renderers/d2svg/style.css
index 7312ba8de..cb85e5a9d 100644
--- a/d2renderers/d2svg/style.css
+++ b/d2renderers/d2svg/style.css
@@ -10,3 +10,20 @@
mix-Blend-mode: multiply;
opacity: 0.5;
}
+
+.sketch-overlay-bright {
+ fill: url(#streaks-bright);
+ mix-blend-mode: darken;
+}
+.sketch-overlay-normal {
+ fill: url(#streaks-normal);
+ mix-blend-mode: color-burn;
+}
+.sketch-overlay-dark {
+ fill: url(#streaks-dark);
+ mix-blend-mode: overlay;
+}
+.sketch-overlay-darker {
+ fill: url(#streaks-darker);
+ mix-blend-mode: lighten;
+}
diff --git a/lib/color/color.go b/lib/color/color.go
index 1bb7d854d..ae3a07f55 100644
--- a/lib/color/color.go
+++ b/lib/color/color.go
@@ -1,16 +1,21 @@
package color
import (
+ "fmt"
"regexp"
"github.com/lucasb-eyer/go-colorful"
"github.com/mazznoer/csscolorparser"
)
-var themeRegex = regexp.MustCompile("^B[1-6]$")
+var themeColorRegex = regexp.MustCompile(`^N[1-7]|B[1-6]|AA[245]|AB[45]$`)
+
+func IsThemeColor(colorString string) bool {
+ return themeColorRegex.Match([]byte(colorString))
+}
func Darken(colorString string) (string, error) {
- if themeRegex.MatchString(colorString) {
+ if IsThemeColor(colorString) {
switch colorString[1] {
case '1':
return B1, nil
@@ -24,13 +29,15 @@ func Darken(colorString string) (string, error) {
return B4, nil
case '6':
return B5, nil
+ default:
+ return "", fmt.Errorf("darkening color \"%s\" is not yet supported", colorString) // TODO Add the rest of the colors so we can allow the user to specify theme colors too
}
}
- return DarkenCSS(colorString)
+ return darkenCSS(colorString)
}
-func DarkenCSS(colorString string) (string, error) {
+func darkenCSS(colorString string) (string, error) {
c, err := csscolorparser.Parse(colorString)
if err != nil {
return "", err
@@ -40,6 +47,38 @@ func DarkenCSS(colorString string) (string, error) {
return colorful.Hsl(h, s, l-.1).Clamped().Hex(), nil
}
+func LuminanceCategory(colorString string) (string, error) {
+ l, err := Luminance(colorString)
+ if err != nil {
+ return "", err
+ }
+
+ switch {
+ case l >= .88:
+ return "bright", nil
+ case l >= .55:
+ return "normal", nil
+ case l >= .30:
+ return "dark", nil
+ default:
+ return "darker", nil
+ }
+}
+
+func Luminance(colorString string) (float64, error) {
+ c, err := csscolorparser.Parse(colorString)
+ if err != nil {
+ return 0, err
+ }
+
+ l := float64(
+ float64(0.299)*float64(c.R) +
+ float64(0.587)*float64(c.G) +
+ float64(0.114)*float64(c.B),
+ )
+ return l, nil
+}
+
const (
N1 = "N1"
N2 = "N2"
diff --git a/lib/svg/style/common.go b/lib/svg/style/common.go
new file mode 100644
index 000000000..6ff9fe302
--- /dev/null
+++ b/lib/svg/style/common.go
@@ -0,0 +1,52 @@
+package style
+
+import (
+ "fmt"
+
+ "oss.terrastruct.com/d2/d2target"
+ "oss.terrastruct.com/d2/lib/svg"
+)
+
+func ShapeStyle(shape d2target.Shape) string {
+ out := ""
+
+ out += fmt.Sprintf(`opacity:%f;`, shape.Opacity)
+ out += fmt.Sprintf(`stroke-width:%d;`, shape.StrokeWidth)
+ if shape.StrokeDash != 0 {
+ dashSize, gapSize := svg.GetStrokeDashAttributes(float64(shape.StrokeWidth), shape.StrokeDash)
+ out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
+ }
+
+ return out
+}
+
+func ShapeTheme(shape d2target.Shape) (fill, stroke string) {
+ if shape.Type == d2target.ShapeSQLTable || shape.Type == d2target.ShapeClass {
+ // Fill is used for header fill in these types
+ // This fill property is just background of rows
+ fill = shape.Stroke
+ // Stroke (border) of these shapes should match the header fill
+ stroke = shape.Fill
+ } else {
+ fill = shape.Fill
+ stroke = shape.Stroke
+ }
+ return fill, stroke
+}
+
+func ConnectionStyle(connection d2target.Connection) string {
+ out := ""
+
+ out += fmt.Sprintf(`opacity:%f;`, connection.Opacity)
+ out += fmt.Sprintf(`stroke-width:%d;`, connection.StrokeWidth)
+ if connection.StrokeDash != 0 {
+ dashSize, gapSize := svg.GetStrokeDashAttributes(float64(connection.StrokeWidth), connection.StrokeDash)
+ out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
+ }
+
+ return out
+}
+
+func ConnectionTheme(connection d2target.Connection) (stroke string) {
+ return connection.Stroke
+}
diff --git a/lib/svg/style/themable_element.go b/lib/svg/style/element.go
similarity index 70%
rename from lib/svg/style/themable_element.go
rename to lib/svg/style/element.go
index f457705c3..a8edaf37d 100644
--- a/lib/svg/style/themable_element.go
+++ b/lib/svg/style/element.go
@@ -3,57 +3,10 @@ package style
import (
"fmt"
"math"
- "regexp"
- "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/color"
- "oss.terrastruct.com/d2/lib/svg"
)
-func ShapeStyle(shape d2target.Shape) string {
- out := ""
-
- out += fmt.Sprintf(`opacity:%f;`, shape.Opacity)
- out += fmt.Sprintf(`stroke-width:%d;`, shape.StrokeWidth)
- if shape.StrokeDash != 0 {
- dashSize, gapSize := svg.GetStrokeDashAttributes(float64(shape.StrokeWidth), shape.StrokeDash)
- out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
- }
-
- return out
-}
-
-func ShapeTheme(shape d2target.Shape) (fill, stroke string) {
- if shape.Type == d2target.ShapeSQLTable || shape.Type == d2target.ShapeClass {
- // Fill is used for header fill in these types
- // This fill property is just background of rows
- fill = shape.Stroke
- // Stroke (border) of these shapes should match the header fill
- stroke = shape.Fill
- } else {
- fill = shape.Fill
- stroke = shape.Stroke
- }
- return fill, stroke
-}
-
-func ConnectionStyle(connection d2target.Connection) string {
- out := ""
-
- out += fmt.Sprintf(`opacity:%f;`, connection.Opacity)
- out += fmt.Sprintf(`stroke-width:%d;`, connection.StrokeWidth)
- if connection.StrokeDash != 0 {
- dashSize, gapSize := svg.GetStrokeDashAttributes(float64(connection.StrokeWidth), connection.StrokeDash)
- out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
- }
-
- return out
-}
-
-func ConnectionTheme(connection d2target.Connection) (stroke string) {
- return connection.Stroke
-}
-
// ThemableElement is a helper class for creating new XML elements.
// This should be preffered over formatting and must be used
// whenever Fill, Stroke, BackgroundColor or Color contains a color from a theme.
@@ -128,8 +81,6 @@ func NewThemableElement(tag string) *ThemableElement {
}
func (el *ThemableElement) Render() string {
- re := regexp.MustCompile(`^N[1-7]|B[1-6]|AA[245]|AB[45]$`)
-
out := "<" + el.tag
if el.X != math.MaxFloat64 {
@@ -195,22 +146,22 @@ func (el *ThemableElement) Render() string {
style := el.Style
// Add class {property}-{theme color} if the color is from a theme, set the property otherwise
- if re.MatchString(el.Stroke) {
+ if color.IsThemeColor(el.Stroke) {
class += fmt.Sprintf(" stroke-%s", el.Stroke)
} else if len(el.Stroke) > 0 {
out += fmt.Sprintf(` stroke="%s"`, el.Stroke)
}
- if re.MatchString(el.Fill) {
+ if color.IsThemeColor(el.Fill) {
class += fmt.Sprintf(" fill-%s", el.Fill)
} else if len(el.Fill) > 0 {
out += fmt.Sprintf(` fill="%s"`, el.Fill)
}
- if re.MatchString(el.BackgroundColor) {
+ if color.IsThemeColor(el.BackgroundColor) {
class += fmt.Sprintf(" background-color-%s", el.BackgroundColor)
} else if len(el.BackgroundColor) > 0 {
out += fmt.Sprintf(` background-color="%s"`, el.BackgroundColor)
}
- if re.MatchString(el.Color) {
+ if color.IsThemeColor(el.Color) {
class += fmt.Sprintf(" color-%s", el.Color)
} else if len(el.Color) > 0 {
out += fmt.Sprintf(` color="%s"`, el.Color)
diff --git a/lib/svg/style/sketch_overlay.go b/lib/svg/style/sketch_overlay.go
new file mode 100644
index 000000000..1c488be8f
--- /dev/null
+++ b/lib/svg/style/sketch_overlay.go
@@ -0,0 +1,34 @@
+package style
+
+import (
+ "fmt"
+
+ "oss.terrastruct.com/d2/lib/color"
+)
+
+type ThemableSketchOverlay struct {
+ el *ThemableElement
+ fill string
+}
+
+func NewThemableSketchOverlay(el *ThemableElement, fill string) *ThemableSketchOverlay {
+ return &ThemableSketchOverlay{
+ el,
+ fill,
+ }
+}
+
+// WARNING: Do not reuse the element afterwards as this function changes the Class propery
+func (o *ThemableSketchOverlay) Render() (string, error) {
+ if color.IsThemeColor(o.fill) {
+ o.el.Class += fmt.Sprintf(" sketch-overlay-%s", o.fill) // e.g. sketch-overlay-B3
+ } else {
+ lc, err := color.LuminanceCategory(o.fill)
+ if err != nil {
+ return "", err
+ }
+ o.el.Class += fmt.Sprintf(" sketch-overlay-%s", lc) // e.g. sketch-overlay-dark
+ }
+
+ return o.el.Render(), nil
+}