vary sketch overlay color depending on the background and small refactoring

This commit is contained in:
Vojtěch Fošnár 2023-01-11 22:18:14 +01:00
parent fb6eee9a24
commit 63a0c1e2b1
No known key found for this signature in database
GPG key ID: 657727E71C40859A
9 changed files with 328 additions and 111 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -56,9 +56,6 @@ var LinkIcon string
//go:embed style.css //go:embed style.css
var baseStylesheet string var baseStylesheet string
//go:embed sketchstyle.css
var sketchStyleCSS string
//go:embed github-markdown.css //go:embed github-markdown.css
var mdCSS string var mdCSS string
@ -1248,12 +1245,11 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
backgroundEl.Fill = color.N7 backgroundEl.Fill = color.N7
// generate elements that will be appended to the SVG tag // generate elements that will be appended to the SVG tag
themeStylesheet := themeCSS(themeID, darkThemeID) themeStylesheet, err := themeCSS(themeID, darkThemeID)
sketchStylesheet := "" if err != nil {
if sketchRunner != nil { return nil, err
sketchStylesheet = "\n" + sketchStyleCSS
} }
svgOut := fmt.Sprintf(`<style type="text/css"><![CDATA[%s%s%s]]></style>`, baseStylesheet, themeStylesheet, sketchStylesheet) svgOut := fmt.Sprintf(`<style type="text/css"><![CDATA[%s%s]]></style>`, baseStylesheet, themeStylesheet)
// this script won't run in --watch mode because script tags are ignored when added via el.innerHTML = element // 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 // https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
svgOut += fmt.Sprintf(`<script type="application/javascript"><![CDATA[%s]]></script>`, fitToScreenScript) svgOut += fmt.Sprintf(`<script type="application/javascript"><![CDATA[%s]]></script>`, fitToScreenScript)
@ -1268,7 +1264,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
svgOut += fmt.Sprintf(`<style type="text/css">%s</style>`, mdCSS) svgOut += fmt.Sprintf(`<style type="text/css">%s</style>`, mdCSS)
} }
if sketchRunner != nil { if sketchRunner != nil {
svgOut += d2sketch.DefineFillPattern() svgOut += d2sketch.DefineFillPatterns()
} }
svgOut += embedFonts(buf, diagram.FontFamily) svgOut += embedFonts(buf, diagram.FontFamily)
@ -1282,20 +1278,29 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
return []byte(docRendered), nil return []byte(docRendered), nil
} }
func themeCSS(themeID, darkThemeID int64) (stylesheet string) { // TODO include only colors that are being used to reduce size
out := singleThemeRulesets(themeID) func themeCSS(themeID, darkThemeID int64) (stylesheet string, err error) {
out, err := singleThemeRulesets(themeID)
if darkThemeID != math.MaxInt64 { if err != nil {
out += fmt.Sprintf("@media screen and (prefers-color-scheme:dark){%s}", singleThemeRulesets(darkThemeID)) 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 := "" out := ""
theme := d2themescatalog.Find(themeID) theme := d2themescatalog.Find(themeID)
// Global theme colors
for _, property := range []string{"fill", "stroke", "background-color", "color"} { 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;}", 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, 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;}", 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.N1, theme.Colors.Neutrals.N2, theme.Colors.Neutrals.N3,
theme.Colors.Neutrals.N7, theme.Colors.Neutrals.N6, theme.Colors.Neutrals.N7, theme.Colors.Neutrals.N6,
@ -1329,7 +1335,91 @@ func singleThemeRulesets(themeID int64) (rulesets string) {
"red", "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 { type DiagramObject interface {

View file

@ -1,4 +0,0 @@
.sketch-overlay {
fill: url(#streaks);
mix-blend-mode: overlay;
}

View file

@ -10,3 +10,20 @@
mix-Blend-mode: multiply; mix-Blend-mode: multiply;
opacity: 0.5; 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;
}

View file

@ -1,16 +1,21 @@
package color package color
import ( import (
"fmt"
"regexp" "regexp"
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
"github.com/mazznoer/csscolorparser" "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) { func Darken(colorString string) (string, error) {
if themeRegex.MatchString(colorString) { if IsThemeColor(colorString) {
switch colorString[1] { switch colorString[1] {
case '1': case '1':
return B1, nil return B1, nil
@ -24,13 +29,15 @@ func Darken(colorString string) (string, error) {
return B4, nil return B4, nil
case '6': case '6':
return B5, nil 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) c, err := csscolorparser.Parse(colorString)
if err != nil { if err != nil {
return "", err return "", err
@ -40,6 +47,38 @@ func DarkenCSS(colorString string) (string, error) {
return colorful.Hsl(h, s, l-.1).Clamped().Hex(), nil 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 ( const (
N1 = "N1" N1 = "N1"
N2 = "N2" N2 = "N2"

52
lib/svg/style/common.go Normal file
View file

@ -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
}

View file

@ -3,57 +3,10 @@ package style
import ( import (
"fmt" "fmt"
"math" "math"
"regexp"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/color" "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. // ThemableElement is a helper class for creating new XML elements.
// This should be preffered over formatting and must be used // This should be preffered over formatting and must be used
// whenever Fill, Stroke, BackgroundColor or Color contains a color from a theme. // 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 { func (el *ThemableElement) Render() string {
re := regexp.MustCompile(`^N[1-7]|B[1-6]|AA[245]|AB[45]$`)
out := "<" + el.tag out := "<" + el.tag
if el.X != math.MaxFloat64 { if el.X != math.MaxFloat64 {
@ -195,22 +146,22 @@ func (el *ThemableElement) Render() string {
style := el.Style style := el.Style
// Add class {property}-{theme color} if the color is from a theme, set the property otherwise // 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) class += fmt.Sprintf(" stroke-%s", el.Stroke)
} else if len(el.Stroke) > 0 { } else if len(el.Stroke) > 0 {
out += fmt.Sprintf(` stroke="%s"`, el.Stroke) out += fmt.Sprintf(` stroke="%s"`, el.Stroke)
} }
if re.MatchString(el.Fill) { if color.IsThemeColor(el.Fill) {
class += fmt.Sprintf(" fill-%s", el.Fill) class += fmt.Sprintf(" fill-%s", el.Fill)
} else if len(el.Fill) > 0 { } else if len(el.Fill) > 0 {
out += fmt.Sprintf(` fill="%s"`, el.Fill) 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) class += fmt.Sprintf(" background-color-%s", el.BackgroundColor)
} else if len(el.BackgroundColor) > 0 { } else if len(el.BackgroundColor) > 0 {
out += fmt.Sprintf(` background-color="%s"`, el.BackgroundColor) 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) class += fmt.Sprintf(" color-%s", el.Color)
} else if len(el.Color) > 0 { } else if len(el.Color) > 0 {
out += fmt.Sprintf(` color="%s"`, el.Color) out += fmt.Sprintf(` color="%s"`, el.Color)

View file

@ -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
}