From a81ab2d73ef5a8b5ac8d177b91cfddf7f90d4bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Fo=C5=A1n=C3=A1r?= Date: Mon, 9 Jan 2023 19:16:28 +0100 Subject: [PATCH] support for prefers-color-scheme --- d2exporter/export.go | 37 +- d2graph/d2graph.go | 68 ++-- d2lib/d2.go | 3 +- d2renderers/d2sketch/sketch.go | 295 ++++++++-------- d2renderers/d2sketch/sketch_test.go | 6 +- d2renderers/d2svg/class.go | 89 ++--- d2renderers/d2svg/d2svg.go | 517 +++++++++++++++------------- d2renderers/d2svg/style.css | 452 +++++++++++++++++++++++- d2renderers/d2svg/table.go | 92 ++--- d2target/d2target.go | 8 +- lib/color/color.go | 31 ++ lib/svg/style/themable_element.go | 233 +++++++++++++ main.go | 5 +- 13 files changed, 1306 insertions(+), 530 deletions(-) create mode 100644 lib/svg/style/themable_element.go diff --git a/d2exporter/export.go b/d2exporter/export.go index 9706365f0..6c3168b84 100644 --- a/d2exporter/export.go +++ b/d2exporter/export.go @@ -7,14 +7,11 @@ import ( "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2target" - "oss.terrastruct.com/d2/d2themes" - "oss.terrastruct.com/d2/d2themes/d2themescatalog" + "oss.terrastruct.com/d2/lib/color" "oss.terrastruct.com/util-go/go2" ) -func Export(ctx context.Context, g *d2graph.Graph, themeID int64, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) { - theme := d2themescatalog.Find(themeID) - +func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) { diagram := d2target.NewDiagram() if fontFamily == nil { fontFamily = go2.Pointer(d2fonts.SourceSansPro) @@ -23,27 +20,27 @@ func Export(ctx context.Context, g *d2graph.Graph, themeID int64, fontFamily *d2 diagram.Shapes = make([]d2target.Shape, len(g.Objects)) for i := range g.Objects { - diagram.Shapes[i] = toShape(g.Objects[i], &theme) + diagram.Shapes[i] = toShape(g.Objects[i]) } diagram.Connections = make([]d2target.Connection, len(g.Edges)) for i := range g.Edges { - diagram.Connections[i] = toConnection(g.Edges[i], &theme) + diagram.Connections[i] = toConnection(g.Edges[i]) } return diagram, nil } -func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Theme) { - shape.Stroke = obj.GetStroke(theme, shape.StrokeDash) - shape.Fill = obj.GetFill(theme) +func applyTheme(shape *d2target.Shape, obj *d2graph.Object) { + shape.Stroke = obj.GetStroke(shape.StrokeDash) + shape.Fill = obj.GetFill() if obj.Attributes.Shape.Value == d2target.ShapeText { - shape.Color = theme.Colors.Neutrals.N1 + shape.Color = color.N1 } if obj.Attributes.Shape.Value == d2target.ShapeSQLTable || obj.Attributes.Shape.Value == d2target.ShapeClass { - shape.PrimaryAccentColor = theme.Colors.B2 - shape.SecondaryAccentColor = theme.Colors.AA2 - shape.NeutralAccentColor = theme.Colors.Neutrals.N2 + shape.PrimaryAccentColor = color.B2 + shape.SecondaryAccentColor = color.AA2 + shape.NeutralAccentColor = color.N2 } } @@ -95,7 +92,7 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) { } } -func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape { +func toShape(obj *d2graph.Object) d2target.Shape { shape := d2target.BaseShape() shape.SetType(obj.Attributes.Shape.Value) shape.ID = obj.AbsID() @@ -120,8 +117,8 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape { } applyStyles(shape, obj) - applyTheme(shape, obj, theme) - shape.Color = text.GetColor(theme, shape.Italic) + applyTheme(shape, obj) + shape.Color = text.GetColor(shape.Italic) applyStyles(shape, obj) switch obj.Attributes.Shape.Value { @@ -153,7 +150,7 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape { return *shape } -func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection { +func toConnection(edge *d2graph.Edge) d2target.Connection { connection := d2target.BaseConnection() connection.ID = edge.AbsID() connection.ZIndex = edge.ZIndex @@ -202,7 +199,7 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection if edge.Attributes.Style.StrokeDash != nil { connection.StrokeDash, _ = strconv.ParseFloat(edge.Attributes.Style.StrokeDash.Value, 64) } - connection.Stroke = edge.GetStroke(theme, connection.StrokeDash) + connection.Stroke = edge.GetStroke(connection.StrokeDash) if edge.Attributes.Style.Stroke != nil { connection.Stroke = edge.Attributes.Style.Stroke.Value } @@ -231,7 +228,7 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection connection.Italic, _ = strconv.ParseBool(edge.Attributes.Style.Italic.Value) } - connection.Color = text.GetColor(theme, connection.Italic) + connection.Color = text.GetColor(connection.Italic) if edge.Attributes.Style.FontColor != nil { connection.Color = edge.Attributes.Style.FontColor.Value } diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go index 21361304a..1dd1791a6 100644 --- a/d2graph/d2graph.go +++ b/d2graph/d2graph.go @@ -16,7 +16,7 @@ import ( "oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2latex" "oss.terrastruct.com/d2/d2target" - "oss.terrastruct.com/d2/d2themes" + "oss.terrastruct.com/d2/lib/color" "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/textmeasure" ) @@ -321,14 +321,14 @@ func (l ContainerLevel) LabelSize() int { return d2fonts.FONT_SIZE_M } -func (obj *Object) GetFill(theme *d2themes.Theme) string { +func (obj *Object) GetFill() string { level := int(obj.Level()) if obj.IsSequenceDiagramNote() { - return theme.Colors.Neutrals.N7 + return color.N7 } else if obj.IsSequenceDiagramGroup() { - return theme.Colors.Neutrals.N5 + return color.N5 } else if obj.Parent.IsSequenceDiagram() { - return theme.Colors.B5 + return color.B5 } // fill for spans @@ -336,19 +336,19 @@ func (obj *Object) GetFill(theme *d2themes.Theme) string { if sd != nil { level -= int(sd.Level()) if level == 1 { - return theme.Colors.B3 + return color.B3 } else if level == 2 { - return theme.Colors.B4 + return color.B4 } else if level == 3 { - return theme.Colors.B5 + return color.B5 } else if level == 4 { - return theme.Colors.Neutrals.N6 + return color.N6 } - return theme.Colors.Neutrals.N7 + return color.N7 } if obj.IsSequenceDiagram() { - return theme.Colors.Neutrals.N7 + return color.N7 } shape := obj.Attributes.Shape.Value @@ -356,65 +356,65 @@ func (obj *Object) GetFill(theme *d2themes.Theme) string { if shape == "" || strings.EqualFold(shape, d2target.ShapeSquare) || strings.EqualFold(shape, d2target.ShapeCircle) || strings.EqualFold(shape, d2target.ShapeOval) || strings.EqualFold(shape, d2target.ShapeRectangle) { if level == 1 { if !obj.IsContainer() { - return theme.Colors.B6 + return color.B6 } - return theme.Colors.B4 + return color.B4 } else if level == 2 { - return theme.Colors.B5 + return color.B5 } else if level == 3 { - return theme.Colors.B6 + return color.B6 } - return theme.Colors.Neutrals.N7 + return color.N7 } if strings.EqualFold(shape, d2target.ShapeCylinder) || strings.EqualFold(shape, d2target.ShapeStoredData) || strings.EqualFold(shape, d2target.ShapePackage) { if level == 1 { - return theme.Colors.AA4 + return color.AA4 } - return theme.Colors.AA5 + return color.AA5 } if strings.EqualFold(shape, d2target.ShapeStep) || strings.EqualFold(shape, d2target.ShapePage) || strings.EqualFold(shape, d2target.ShapeDocument) { if level == 1 { - return theme.Colors.AB4 + return color.AB4 } - return theme.Colors.AB5 + return color.AB5 } if strings.EqualFold(shape, d2target.ShapePerson) { - return theme.Colors.B3 + return color.B3 } if strings.EqualFold(shape, d2target.ShapeDiamond) { - return theme.Colors.Neutrals.N4 + return color.N4 } if strings.EqualFold(shape, d2target.ShapeCloud) || strings.EqualFold(shape, d2target.ShapeCallout) { - return theme.Colors.Neutrals.N7 + return color.N7 } if strings.EqualFold(shape, d2target.ShapeQueue) || strings.EqualFold(shape, d2target.ShapeParallelogram) || strings.EqualFold(shape, d2target.ShapeHexagon) { - return theme.Colors.Neutrals.N5 + return color.N5 } if strings.EqualFold(shape, d2target.ShapeSQLTable) || strings.EqualFold(shape, d2target.ShapeClass) { - return theme.Colors.Neutrals.N1 + return color.N1 } - return theme.Colors.Neutrals.N7 + return color.N7 } -func (obj *Object) GetStroke(theme *d2themes.Theme, dashGapSize interface{}) string { +func (obj *Object) GetStroke(dashGapSize interface{}) string { shape := obj.Attributes.Shape.Value if strings.EqualFold(shape, d2target.ShapeCode) || strings.EqualFold(shape, d2target.ShapeText) { - return theme.Colors.Neutrals.N1 + return color.N1 } if strings.EqualFold(shape, d2target.ShapeClass) || strings.EqualFold(shape, d2target.ShapeSQLTable) { - return theme.Colors.Neutrals.N7 + return color.N7 } if dashGapSize != 0.0 { - return theme.Colors.B2 + return color.B2 } - return theme.Colors.B1 + return color.B1 } func (obj *Object) Level() ContainerLevel { @@ -867,11 +867,11 @@ type EdgeReference struct { ScopeObj *Object `json:"-"` } -func (e *Edge) GetStroke(theme *d2themes.Theme, dashGapSize interface{}) string { +func (e *Edge) GetStroke(dashGapSize interface{}) string { if dashGapSize != 0.0 { - return theme.Colors.B2 + return color.B2 } - return theme.Colors.B1 + return color.B1 } func (e *Edge) ArrowString() string { diff --git a/d2lib/d2.go b/d2lib/d2.go index bfe9713a9..0642c8fa7 100644 --- a/d2lib/d2.go +++ b/d2lib/d2.go @@ -29,7 +29,6 @@ type CompileOptions struct { // - pre-measured (web setting) // TODO maybe some will want to configure code font too, but that's much lower priority FontFamily *d2fonts.FontFamily - ThemeID int64 } func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target.Diagram, *d2graph.Graph, error) { @@ -68,7 +67,7 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target } } - diagram, err := d2exporter.Export(ctx, g, opts.ThemeID, opts.FontFamily) + diagram, err := d2exporter.Export(ctx, g, opts.FontFamily) return diagram, g, err } diff --git a/d2renderers/d2sketch/sketch.go b/d2renderers/d2sketch/sketch.go index 4c4913a2b..19c68c4a8 100644 --- a/d2renderers/d2sketch/sketch.go +++ b/d2renderers/d2sketch/sketch.go @@ -3,16 +3,17 @@ package d2sketch import ( "encoding/json" "fmt" - "strings" _ "embed" "github.com/dop251/goja" "oss.terrastruct.com/d2/d2target" + "oss.terrastruct.com/d2/lib/color" "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/svg" + svg_style "oss.terrastruct.com/d2/lib/svg/style" "oss.terrastruct.com/util-go/go2" ) @@ -63,43 +64,26 @@ func DefineFillPattern() string { `, fillPattern) } -func shapeStyle(shape d2target.Shape) string { - out := "" - - if shape.Type == d2target.ShapeSQLTable || shape.Type == d2target.ShapeClass { - out += fmt.Sprintf(`fill:%s;`, shape.Stroke) - out += fmt.Sprintf(`stroke:%s;`, shape.Fill) - } else { - out += fmt.Sprintf(`fill:%s;`, shape.Fill) - out += fmt.Sprintf(`stroke:%s;`, shape.Stroke) - } - 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 Rect(r *Runner, shape d2target.Shape) (string, error) { js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { - fill: "%s", - stroke: "%s", + fill: "#000", + stroke: "#000", strokeWidth: %d, %s - });`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) + });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps) paths, err := computeRoughPaths(r, js) if err != nil { return "", err } output := "" + pathEl := svg_style.NewThemableElement("path") + pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y) + pathEl.Fill, pathEl.Stroke = svg_style.ShapeTheme(shape) + pathEl.Class = "shape" + pathEl.Style = svg_style.ShapeStyle(shape) for _, p := range paths { - output += fmt.Sprintf( - ``, - shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape), - ) + pathEl.D = p + output += pathEl.Render() } output += fmt.Sprintf( ``, @@ -110,21 +94,24 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) { func Oval(r *Runner, shape d2target.Shape) (string, error) { js := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, { - fill: "%s", - stroke: "%s", + fill: "#000", + stroke: "#000", strokeWidth: %d, %s - });`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) + });`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps) paths, err := computeRoughPaths(r, js) if err != nil { return "", err } output := "" + pathEl := svg_style.NewThemableElement("path") + pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y) + pathEl.Fill, pathEl.Stroke = svg_style.ShapeTheme(shape) + pathEl.Class = "shape" + pathEl.Style = svg_style.ShapeStyle(shape) for _, p := range paths { - output += fmt.Sprintf( - ``, - shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape), - ) + pathEl.D = p + output += pathEl.Render() } output += fmt.Sprintf( ``, @@ -138,20 +125,22 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) { output := "" for _, path := range paths { js := fmt.Sprintf(`node = rc.path("%s", { - fill: "%s", - stroke: "%s", + fill: "#000", + stroke: "#000", strokeWidth: %d, %s - });`, path, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) + });`, path, shape.StrokeWidth, baseRoughProps) sketchPaths, err := computeRoughPaths(r, js) if err != nil { return "", err } + pathEl := svg_style.NewThemableElement("path") + pathEl.Fill, pathEl.Stroke = svg_style.ShapeTheme(shape) + pathEl.Class = "shape" + pathEl.Style = svg_style.ShapeStyle(shape) for _, p := range sketchPaths { - output += fmt.Sprintf( - ``, - p, shapeStyle(shape), - ) + pathEl.D = p + output += pathEl.Render() } for _, p := range sketchPaths { output += fmt.Sprintf( @@ -163,20 +152,6 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) { return output, nil } -func connectionStyle(connection d2target.Connection) string { - out := "" - - out += fmt.Sprintf(`stroke:%s;`, connection.Stroke) - 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 Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) { roughness := 1.0 js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness) @@ -185,11 +160,15 @@ func Connection(r *Runner, connection d2target.Connection, path, attrs string) ( return "", err } output := "" + pathEl := svg_style.NewThemableElement("path") + pathEl.Fill = color.None + pathEl.Stroke = svg_style.ConnectionTheme(connection) + pathEl.Class = "connection" + pathEl.Style = svg_style.ConnectionStyle(connection) + pathEl.Attributes = attrs for _, p := range paths { - output += fmt.Sprintf( - ``, - p, connectionStyle(connection), attrs, - ) + pathEl.D = p + output += pathEl.Render() } return output, nil } @@ -198,20 +177,23 @@ func Connection(r *Runner, connection d2target.Connection, path, attrs string) ( func Table(r *Runner, shape d2target.Shape) (string, error) { output := "" js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { - fill: "%s", - stroke: "%s", + fill: "#000", + stroke: "#000", strokeWidth: %d, %s - });`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) + });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps) paths, err := computeRoughPaths(r, js) if err != nil { return "", err } + pathEl := svg_style.NewThemableElement("path") + pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y) + pathEl.Fill, pathEl.Stroke = svg_style.ShapeTheme(shape) + pathEl.Class = "shape" + pathEl.Style = svg_style.ShapeStyle(shape) for _, p := range paths { - output += fmt.Sprintf( - ``, - shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape), - ) + pathEl.D = p + output += pathEl.Render() } box := geo.NewBox( @@ -223,18 +205,20 @@ func Table(r *Runner, shape d2target.Shape) (string, error) { headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight) js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, { - fill: "%s", + fill: "#000", %s - });`, shape.Width, rowHeight, shape.Fill, baseRoughProps) + });`, shape.Width, rowHeight, baseRoughProps) paths, err = computeRoughPaths(r, js) if err != nil { return "", err } + pathEl = svg_style.NewThemableElement("path") + pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y) + pathEl.Fill = shape.Fill + pathEl.Class = "class_header" for _, p := range paths { - output += fmt.Sprintf( - ``, - shape.Pos.X, shape.Pos.Y, p, shape.Fill, - ) + pathEl.D = p + output += pathEl.Render() } if shape.Label != "" { @@ -245,17 +229,16 @@ func Table(r *Runner, shape d2target.Shape) (string, error) { float64(shape.LabelHeight), ) - output += fmt.Sprintf(`%s`, - "text", - tl.X, - tl.Y+float64(shape.LabelHeight)*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", - "start", - 4+shape.FontSize, - shape.Stroke, - ), - svg.EscapeText(shape.Label), + textEl := svg_style.NewThemableElement("text") + textEl.X = tl.X + textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4 + textEl.Fill = shape.Stroke + textEl.Class = "text" + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", + "start", 4+shape.FontSize, ) + textEl.Content = svg.EscapeText(shape.Label) + output += textEl.Render() } var longestNameWidth int @@ -279,26 +262,26 @@ func Table(r *Runner, shape d2target.Shape) (string, error) { float64(shape.FontSize), ) - output += strings.Join([]string{ - fmt.Sprintf(`%s`, - nameTL.X, - nameTL.Y+float64(shape.FontSize)*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", float64(shape.FontSize), shape.PrimaryAccentColor), - svg.EscapeText(f.Name.Label), - ), - fmt.Sprintf(`%s`, - nameTL.X+float64(longestNameWidth)+2*d2target.NamePadding, - nameTL.Y+float64(shape.FontSize)*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", float64(shape.FontSize), shape.NeutralAccentColor), - svg.EscapeText(f.Type.Label), - ), - fmt.Sprintf(`%s`, - constraintTR.X, - constraintTR.Y+float64(shape.FontSize)*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s;letter-spacing:2px;", "end", float64(shape.FontSize), shape.SecondaryAccentColor), - f.ConstraintAbbr(), - ), - }, "\n") + textEl := svg_style.NewThemableElement("text") + textEl.X = nameTL.X + textEl.Y = nameTL.Y + float64(shape.FontSize)*3/4 + textEl.Fill = shape.PrimaryAccentColor + textEl.Class = "text" + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", float64(shape.FontSize)) + textEl.Content = svg.EscapeText(f.Name.Label) + output += textEl.Render() + + textEl.X = nameTL.X + float64(longestNameWidth) + 2*d2target.NamePadding + textEl.Fill = shape.NeutralAccentColor + textEl.Content = svg.EscapeText(f.Type.Label) + output += textEl.Render() + + textEl.X = constraintTR.X + textEl.Y = constraintTR.Y + float64(shape.FontSize)*3/4 + textEl.Fill = shape.SecondaryAccentColor + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx;letter-spacing:2px", "end", float64(shape.FontSize)) + textEl.Content = f.ConstraintAbbr() + output += textEl.Render() rowBox.TopLeft.Y += rowHeight @@ -309,11 +292,11 @@ func Table(r *Runner, shape d2target.Shape) (string, error) { if err != nil { return "", err } + pathEl := svg_style.NewThemableElement("path") + pathEl.Fill = shape.Fill for _, p := range paths { - output += fmt.Sprintf( - ``, - p, shape.Fill, - ) + pathEl.D = p + output += pathEl.Render() } } output += fmt.Sprintf( @@ -326,20 +309,22 @@ func Table(r *Runner, shape d2target.Shape) (string, error) { func Class(r *Runner, shape d2target.Shape) (string, error) { output := "" js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { - fill: "%s", - stroke: "%s", + fill: "#000", + stroke: "#000", strokeWidth: %d, %s - });`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) + });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps) paths, err := computeRoughPaths(r, js) if err != nil { return "", err } + pathEl := svg_style.NewThemableElement("path") + pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y) + pathEl.Fill, pathEl.Stroke = svg_style.ShapeTheme(shape) + pathEl.Class = "shape" for _, p := range paths { - output += fmt.Sprintf( - ``, - shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape), - ) + pathEl.D = p + output += pathEl.Render() } box := geo.NewBox( @@ -352,18 +337,20 @@ func Class(r *Runner, shape d2target.Shape) (string, error) { headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight) js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, { - fill: "%s", + fill: "#000", %s - });`, shape.Width, headerBox.Height, shape.Fill, baseRoughProps) + });`, shape.Width, headerBox.Height, baseRoughProps) paths, err = computeRoughPaths(r, js) if err != nil { return "", err } + pathEl = svg_style.NewThemableElement("path") + pathEl.Transform = fmt.Sprintf("translate(%d %d)", shape.Pos.X, shape.Pos.Y) + pathEl.Fill = shape.Fill + pathEl.Class = "class_header" for _, p := range paths { - output += fmt.Sprintf( - ``, - shape.Pos.X, shape.Pos.Y, p, shape.Fill, - ) + pathEl.D = p + output += pathEl.Render() } output += fmt.Sprintf( @@ -379,17 +366,17 @@ func Class(r *Runner, shape d2target.Shape) (string, error) { float64(shape.LabelHeight), ) - output += fmt.Sprintf(`%s`, - "text-mono", - tl.X+float64(shape.LabelWidth)/2, - tl.Y+float64(shape.LabelHeight)*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", - "middle", - 4+shape.FontSize, - shape.Stroke, - ), - svg.EscapeText(shape.Label), + textEl := svg_style.NewThemableElement("text") + textEl.X = tl.X + float64(shape.LabelWidth)/2 + textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4 + textEl.Fill = shape.Stroke + textEl.Class = "text-mono" + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", + "middle", + 4+shape.FontSize, ) + textEl.Content = svg.EscapeText(shape.Label) + output += textEl.Render() } rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight) @@ -406,11 +393,12 @@ func Class(r *Runner, shape d2target.Shape) (string, error) { if err != nil { return "", err } + pathEl = svg_style.NewThemableElement("path") + pathEl.Fill = shape.Fill + pathEl.Class = "class_header" for _, p := range paths { - output += fmt.Sprintf( - ``, - p, shape.Fill, - ) + pathEl.D = p + output += pathEl.Render() } for _, m := range shape.Methods { @@ -436,28 +424,27 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str fontSize, ) - output += strings.Join([]string{ - fmt.Sprintf(`%s`, - prefixTL.X, - prefixTL.Y+fontSize*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.PrimaryAccentColor), - prefix, - ), + textEl := svg_style.NewThemableElement("text") + textEl.X = prefixTL.X + textEl.Y = prefixTL.Y + fontSize*3/4 + textEl.Fill = shape.PrimaryAccentColor + textEl.Class = "text-mono" + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", fontSize) + textEl.Content = prefix + output += textEl.Render() - fmt.Sprintf(`%s`, - prefixTL.X+d2target.PrefixWidth, - prefixTL.Y+fontSize*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.Fill), - svg.EscapeText(nameText), - ), + textEl.X = prefixTL.X + d2target.PrefixWidth + textEl.Fill = shape.Fill + textEl.Content = svg.EscapeText(nameText) + output += textEl.Render() + + textEl.X = typeTR.X + textEl.Y = typeTR.Y + fontSize*3/4 + textEl.Fill = shape.SecondaryAccentColor + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "end", fontSize) + textEl.Content = svg.EscapeText(typeText) + output += textEl.Render() - fmt.Sprintf(`%s`, - typeTR.X, - typeTR.Y+fontSize*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s;", "end", fontSize, shape.SecondaryAccentColor), - svg.EscapeText(typeText), - ), - }, "\n") return output } diff --git a/d2renderers/d2sketch/sketch_test.go b/d2renderers/d2sketch/sketch_test.go index c2b8dadea..06b693b3f 100644 --- a/d2renderers/d2sketch/sketch_test.go +++ b/d2renderers/d2sketch/sketch_test.go @@ -313,7 +313,6 @@ func run(t *testing.T, tc testCase) { diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ Ruler: ruler, - ThemeID: 0, Layout: d2dagrelayout.DefaultLayout, FontFamily: go2.Pointer(d2fonts.HandDrawn), }) @@ -325,8 +324,9 @@ func run(t *testing.T, tc testCase) { pathGotSVG := filepath.Join(dataPath, "sketch.got.svg") svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{ - Pad: d2svg.DEFAULT_PADDING, - Sketch: true, + Pad: d2svg.DEFAULT_PADDING, + Sketch: true, + ThemeID: 0, }) assert.Success(t, err) err = os.MkdirAll(dataPath, 0755) diff --git a/d2renderers/d2svg/class.go b/d2renderers/d2svg/class.go index 85205db32..7808ca879 100644 --- a/d2renderers/d2svg/class.go +++ b/d2renderers/d2svg/class.go @@ -3,17 +3,21 @@ package d2svg import ( "fmt" "io" - "strings" "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/svg" + svg_style "oss.terrastruct.com/d2/lib/svg/style" ) func classHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string { - str := fmt.Sprintf(``, - box.TopLeft.X, box.TopLeft.Y, box.Width, box.Height, shape.Fill) + rectEl := svg_style.NewThemableElement("rect") + rectEl.X, rectEl.Y = box.TopLeft.X, box.TopLeft.Y + rectEl.Width, rectEl.Height = box.Width, box.Height + rectEl.Fill = shape.Fill + rectEl.Class = "class_header" + str := rectEl.Render() if text != "" { tl := label.InsideMiddleCenter.GetPointOnBox( @@ -23,17 +27,16 @@ func classHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, tex textHeight, ) - str += fmt.Sprintf(`%s`, - "text-mono", - tl.X+textWidth/2, - tl.Y+textHeight*3/4, - fmt.Sprintf(`text-anchor:%s;font-size:%vpx;fill:%s`, - "middle", - 4+fontSize, - shape.Stroke, - ), - svg.EscapeText(text), + textEl := svg_style.NewThemableElement("text") + textEl.X = tl.X + textWidth/2 + textEl.Y = tl.Y + textHeight*3/4 + textEl.Fill = shape.Stroke + textEl.Class = "text-mono" + textEl.Style = fmt.Sprintf(`text-anchor:%s;font-size:%vpx;`, + "middle", 4+fontSize, ) + textEl.Content = svg.EscapeText(text) + str += textEl.Render() } return str } @@ -54,33 +57,39 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str fontSize, ) - return strings.Join([]string{ - fmt.Sprintf(`%s`, - prefixTL.X, - prefixTL.Y+fontSize*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.PrimaryAccentColor), - prefix, - ), + textEl := svg_style.NewThemableElement("text") + textEl.X = prefixTL.X + textEl.Y = prefixTL.Y + fontSize*3/4 + textEl.Fill = shape.PrimaryAccentColor + textEl.Class = "text-mono" + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", fontSize) + textEl.Content = prefix + out := textEl.Render() - fmt.Sprintf(`%s`, - prefixTL.X+d2target.PrefixWidth, - prefixTL.Y+fontSize*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.Fill), - svg.EscapeText(nameText), - ), + textEl.X = prefixTL.X + d2target.PrefixWidth + textEl.Fill = shape.Fill + textEl.Content = svg.EscapeText(nameText) + out += textEl.Render() - fmt.Sprintf(`%s`, - typeTR.X, - typeTR.Y+fontSize*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "end", fontSize, shape.SecondaryAccentColor), - svg.EscapeText(typeText), - ), - }, "\n") + textEl.X = typeTR.X + textEl.Y = typeTR.Y + fontSize*3/4 + textEl.Fill = shape.SecondaryAccentColor + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "end", fontSize) + textEl.Content = svg.EscapeText(typeText) + out += textEl.Render() + + return out } func drawClass(writer io.Writer, targetShape d2target.Shape) { - fmt.Fprintf(writer, ``, - targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, shapeStyle(targetShape)) + el := svg_style.NewThemableElement("rect") + el.X = float64(targetShape.Pos.X) + el.Y = float64(targetShape.Pos.Y) + el.Width = float64(targetShape.Width) + el.Height = float64(targetShape.Height) + el.Fill, el.Stroke = svg_style.ShapeTheme(targetShape) + el.Style = svg_style.ShapeStyle(targetShape) + fmt.Fprint(writer, el.Render()) box := geo.NewBox( geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)), @@ -103,10 +112,12 @@ func drawClass(writer io.Writer, targetShape d2target.Shape) { rowBox.TopLeft.Y += rowHeight } - fmt.Fprintf(writer, ``, - rowBox.TopLeft.X, rowBox.TopLeft.Y, - rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, - fmt.Sprintf("stroke-width:1;stroke:%v", targetShape.Fill)) + lineEl := svg_style.NewThemableElement("line") + lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y + lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y + lineEl.Stroke = targetShape.Fill + lineEl.Style = "stroke-width:1" + fmt.Fprint(writer, lineEl.Render()) for _, m := range targetShape.Methods { fmt.Fprint(writer, diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index d18c26b1c..a6c2afea2 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -27,12 +27,12 @@ import ( "oss.terrastruct.com/d2/d2renderers/d2latex" "oss.terrastruct.com/d2/d2renderers/d2sketch" "oss.terrastruct.com/d2/d2target" - "oss.terrastruct.com/d2/d2themes/d2themescatalog" "oss.terrastruct.com/d2/lib/color" "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/shape" "oss.terrastruct.com/d2/lib/svg" + svg_style "oss.terrastruct.com/d2/lib/svg/style" "oss.terrastruct.com/d2/lib/textmeasure" ) @@ -68,21 +68,12 @@ type RenderOpts struct { ThemeID int64 } -func setViewbox(writer io.Writer, diagram *d2target.Diagram, pad int, bgColor string, fgColor string) (width int, height int) { +func dimensions(writer io.Writer, diagram *d2target.Diagram, pad int) (width, height int, topLeft, bottomRight d2target.Point) { tl, br := diagram.BoundingBox() w := br.X - tl.X + pad*2 h := br.Y - tl.Y + pad*2 - // TODO minify - // TODO background stuff. e.g. dotted, grid, colors - fmt.Fprintf(writer, ` -`, bgColor, fgColor, w, h, tl.X-pad, tl.Y-pad, w, h) - - return w, h + return w, h, tl, br } func arrowheadMarkerID(isTarget bool, connection d2target.Connection) string { @@ -94,7 +85,7 @@ func arrowheadMarkerID(isTarget bool, connection d2target.Connection) string { } return fmt.Sprintf("mk-%s", hash(fmt.Sprintf("%s,%t,%d,%s", - arrowhead, isTarget, connection.StrokeWidth, connection.Stroke, + arrowhead, isTarget, connection.StrokeWidth, svg_style.ConnectionTheme(connection), ))) } @@ -137,119 +128,136 @@ func arrowheadMarker(isTarget bool, id string, bgColor string, connection d2targ var path string switch arrowhead { case d2target.ArrowArrowhead: - attrs := fmt.Sprintf(`class="connection" fill="%s" stroke-width="%d"`, connection.Stroke, connection.StrokeWidth) + polygonEl := svg_style.NewThemableElement("polygon") + polygonEl.Fill = svg_style.ConnectionTheme(connection) + polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth) + if isTarget { - path = fmt.Sprintf(``, - attrs, + polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f", 0., 0., width, height/2, 0., height, width/4, height/2, ) } else { - path = fmt.Sprintf(``, - attrs, + polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f", 0., height/2, width, 0., width*3/4, height/2, width, height, ) } + path = polygonEl.Render() case d2target.TriangleArrowhead: - attrs := fmt.Sprintf(`class="connection" fill="%s" stroke-width="%d"`, connection.Stroke, connection.StrokeWidth) + polygonEl := svg_style.NewThemableElement("polygon") + polygonEl.Fill = svg_style.ConnectionTheme(connection) + polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth) + if isTarget { - path = fmt.Sprintf(``, - attrs, + polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f", 0., 0., width, height/2.0, 0., height, ) } else { - path = fmt.Sprintf(``, - attrs, + polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f", width, 0., 0., height/2.0, width, height, ) } + path = polygonEl.Render() case d2target.LineArrowhead: - attrs := fmt.Sprintf(`class="connection" fill="none" stroke="%s" stroke-width="%d"`, connection.Stroke, connection.StrokeWidth) + polylineEl := svg_style.NewThemableElement("polyline") + polylineEl.Fill = color.None + polylineEl.Stroke = svg_style.ConnectionTheme(connection) + polylineEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth) + if isTarget { - path = fmt.Sprintf(``, - attrs, + polylineEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f", strokeWidth/2, strokeWidth/2, width-strokeWidth/2, height/2, strokeWidth/2, height-strokeWidth/2, ) } else { - path = fmt.Sprintf(``, - attrs, + polylineEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f", width-strokeWidth/2, strokeWidth/2, strokeWidth/2, height/2, width-strokeWidth/2, height-strokeWidth/2, ) } + path = polylineEl.Render() case d2target.FilledDiamondArrowhead: - attrs := fmt.Sprintf(`class="connection" fill="%s" stroke-width="%d"`, connection.Stroke, connection.StrokeWidth) + polygonEl := svg_style.NewThemableElement("polygon") + polygonEl.Fill = svg_style.ConnectionTheme(connection) + polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth) + if isTarget { - path = fmt.Sprintf(``, - attrs, + polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f", 0., height/2.0, width/2.0, 0., width, height/2.0, width/2.0, height, ) } else { - path = fmt.Sprintf(``, - attrs, + polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f", 0., height/2.0, width/2.0, 0., width, height/2.0, width/2.0, height, ) } + path = polygonEl.Render() case d2target.DiamondArrowhead: - attrs := fmt.Sprintf(`class="connection" fill="%s" stroke="%s" stroke-width="%d"`, bgColor, connection.Stroke, connection.StrokeWidth) + polygonEl := svg_style.NewThemableElement("polygon") + polygonEl.Fill = bgColor + polygonEl.Stroke = svg_style.ConnectionTheme(connection) + polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth) + if isTarget { - path = fmt.Sprintf(``, - attrs, + polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f", 0., height/2.0, width/2, height/8, width, height/2.0, width/2.0, height*0.9, ) } else { - path = fmt.Sprintf(``, - attrs, + polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f", width/8, height/2.0, width*0.6, height/8, width*1.1, height/2.0, width*0.6, height*7/8, ) } + path = polygonEl.Render() case d2target.CfOne, d2target.CfMany, d2target.CfOneRequired, d2target.CfManyRequired: - attrs := fmt.Sprintf(`class="connection" stroke="%s" stroke-width="%d" fill="%s"`, connection.Stroke, connection.StrokeWidth, bgColor) offset := 4.0 + float64(connection.StrokeWidth*2) - var modifier string + + var modifierEl *svg_style.ThemableElement if arrowhead == d2target.CfOneRequired || arrowhead == d2target.CfManyRequired { - modifier = fmt.Sprintf(``, - attrs, + modifierEl := svg_style.NewThemableElement("path") + modifierEl.D = fmt.Sprintf("M%f,%f %f,%f", offset, 0., offset, height, ) + modifierEl.Fill = bgColor + modifierEl.Stroke = svg_style.ConnectionTheme(connection) + modifierEl.Class = "connection" + modifierEl.Style = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth) } else { - modifier = fmt.Sprintf(``, - attrs, - offset/2.0+1.0, height/2.0, - offset/2.0, - ) - } - if !isTarget { - attrs = fmt.Sprintf(`%s transform="scale(-1) translate(-%f, -%f)"`, attrs, width, height) + modifierEl := svg_style.NewThemableElement("circle") + modifierEl.Cx = offset/2.0 + 1.0 + modifierEl.Cy = height / 2.0 + modifierEl.R = offset / 2.0 + modifierEl.Fill = bgColor + modifierEl.Stroke = svg_style.ConnectionTheme(connection) + modifierEl.Class = "connection" + modifierEl.Style = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth) } + + childPathEl := svg_style.NewThemableElement("path") if arrowhead == d2target.CfMany || arrowhead == d2target.CfManyRequired { - path = fmt.Sprintf(`%s`, - attrs, modifier, + childPathEl.D = fmt.Sprintf("M%f,%f %f,%f M%f,%f %f,%f M%f,%f %f,%f", width-3.0, height/2.0, width+offset, height/2.0, offset+2.0, height/2.0, @@ -258,14 +266,26 @@ func arrowheadMarker(isTarget bool, id string, bgColor string, connection d2targ width+offset, height, ) } else { - path = fmt.Sprintf(`%s`, - attrs, modifier, + childPathEl.D = fmt.Sprintf("M%f,%f %f,%f M%f,%f %f,%f", width-3.0, height/2.0, width+offset, height/2.0, offset*1.8, 0., offset*1.8, height, ) } + + gEl := svg_style.NewThemableElement("g") + gEl.Fill = bgColor + gEl.Stroke = svg_style.ConnectionTheme(connection) + gEl.Class = "connection" + gEl.Style = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth) + if !isTarget { + gEl.Transform = fmt.Sprintf("scale(-1) translate(-%f, -%f)", width, height) + } + gEl.Content = fmt.Sprintf("%s%s", + modifierEl.Render(), childPathEl.Render(), + ) + path = gEl.Render() default: return "" } @@ -462,10 +482,16 @@ func drawConnection(writer io.Writer, bgColor string, fgColor string, labelMaskI if err != nil { return "", err } - fmt.Fprintf(writer, out) + fmt.Fprint(writer, out) } else { - fmt.Fprintf(writer, ``, - path, connectionStyle(connection), attrs) + pathEl := svg_style.NewThemableElement("path") + pathEl.D = path + pathEl.Fill = color.None + pathEl.Stroke = svg_style.ConnectionTheme(connection) + pathEl.Class = "connection" + pathEl.Style = svg_style.ConnectionStyle(connection) + pathEl.Attributes = attrs + fmt.Fprint(writer, pathEl.Render()) } if connection.Label != "" { @@ -475,24 +501,27 @@ func drawConnection(writer io.Writer, bgColor string, fgColor string, labelMaskI } else if connection.Italic { fontClass += "-italic" } - fontColor := "black" - if connection.Color != "" { + fontColor := color.N1 + if connection.Color != color.Empty { fontColor = connection.Color } - if connection.Fill != "" { - fmt.Fprintf(writer, ``, - labelTL.X, labelTL.Y, connection.LabelWidth, connection.LabelHeight, connection.Fill) + if connection.Fill != color.Empty { + rectEl := svg_style.NewThemableElement("rect") + rectEl.X, rectEl.Y = labelTL.X, labelTL.Y + rectEl.Width, rectEl.Height = float64(connection.LabelWidth), float64(connection.LabelHeight) + rectEl.Fill = connection.Fill + fmt.Fprint(writer, rectEl.Render()) } - textStyle := fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "middle", connection.FontSize, fontColor) - x := labelTL.X + float64(connection.LabelWidth)/2 - y := labelTL.Y + float64(connection.FontSize) - fmt.Fprintf(writer, `%s`, - fontClass, - x, y, - textStyle, - RenderText(connection.Label, x, float64(connection.LabelHeight)), - ) + + textEl := svg_style.NewThemableElement("text") + textEl.X = labelTL.X + float64(connection.LabelWidth)/2 + textEl.Y = labelTL.Y + float64(connection.FontSize) + textEl.Fill = fontColor + textEl.Class = fontClass + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", connection.FontSize) + textEl.Content = RenderText(connection.Label, textEl.X, float64(connection.LabelHeight)) + fmt.Fprint(writer, textEl.Render()) } length := geo.Route(connection.Route).Length() @@ -521,22 +550,25 @@ func drawConnection(writer io.Writer, bgColor string, fgColor string, labelMaskI func renderArrowheadLabel(fgColor string, connection d2target.Connection, text string, position, width, height float64) string { labelTL := label.UnlockedTop.GetPointOnRoute(connection.Route, float64(connection.StrokeWidth), position, width, height) - textStyle := fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "middle", connection.FontSize, fgColor) - x := labelTL.X + width/2 - y := labelTL.Y + float64(connection.FontSize) - return fmt.Sprintf(`%s`, - x, y, - textStyle, - RenderText(text, x, height), - ) + textEl := svg_style.NewThemableElement("text") + textEl.X = labelTL.X + width/2 + textEl.Y = labelTL.Y + float64(connection.FontSize) + textEl.Fill = fgColor + textEl.Class = "text-italic" + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", connection.FontSize) + textEl.Content = RenderText(text, textEl.X, height) + return textEl.Render() } -func renderOval(tl *geo.Point, width, height float64, style string) string { - rx := width / 2 - ry := height / 2 - cx := tl.X + rx - cy := tl.Y + ry - return fmt.Sprintf(``, cx, cy, rx, ry, style) +func renderOval(tl *geo.Point, width, height float64, fill, stroke, style string) string { + el := svg_style.NewThemableElement("ellipse") + el.Rx = width / 2 + el.Ry = height / 2 + el.Cx = tl.X + el.Rx + el.Cy = tl.Y + el.Ry + el.Class = "shape" + el.Style = style + return el.Render() } func defineShadowFilter(writer io.Writer) { @@ -583,11 +615,13 @@ func render3dRect(targetShape d2target.Shape) string { borderSegments = append(borderSegments, lineTo(d2target.Point{X: targetShape.Width + threeDeeOffset, Y: -threeDeeOffset}), ) - border := targetShape - border.Fill = "none" - borderStyle := shapeStyle(border) - renderedBorder := fmt.Sprintf(``, - strings.Join(borderSegments, " "), borderStyle) + border := svg_style.NewThemableElement("path") + border.D = strings.Join(borderSegments, " ") + _, borderStroke := svg_style.ShapeTheme(targetShape) + border.Stroke = borderStroke + borderStyle := svg_style.ShapeStyle(targetShape) + border.Style = borderStyle + renderedBorder := border.Render() // create mask from border stroke, to cut away from the shape fills maskID := fmt.Sprintf("border-mask-%v", svg.EscapeText(targetShape.ID)) @@ -603,11 +637,16 @@ func render3dRect(targetShape d2target.Shape) string { }, "\n") // render the main rectangle without stroke and the border mask - mainShape := targetShape - mainShape.Stroke = "none" - mainRect := fmt.Sprintf(``, - targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, shapeStyle(mainShape), maskID, - ) + mainShape := svg_style.NewThemableElement("rect") + mainShape.X = float64(targetShape.Pos.X) + mainShape.Y = float64(targetShape.Pos.Y) + mainShape.Width = float64(targetShape.Width) + mainShape.Height = float64(targetShape.Height) + mainShape.Mask = fmt.Sprintf("url(#%s)", maskID) + mainShapeFill, _ := svg_style.ShapeTheme(targetShape) + mainShape.Fill = mainShapeFill + mainShape.Style = svg_style.ShapeStyle(targetShape) + mainShapeRendered := mainShape.Render() // render the side shapes in the darkened color without stroke and the border mask var sidePoints []string @@ -623,17 +662,20 @@ func render3dRect(targetShape d2target.Shape) string { fmt.Sprintf("%d,%d", v.X+targetShape.Pos.X, v.Y+targetShape.Pos.Y), ) } - darkerColor, err := color.Darken(targetShape.Fill) - if err != nil { - darkerColor = targetShape.Fill - } - sideShape := targetShape + // TODO make darker color part of the theme? + darkerColor := targetShape.Fill + // darkerColor, err := color.Darken(targetShape.Fill) + // if err != nil { + // darkerColor = targetShape.Fill + // } + sideShape := svg_style.NewThemableElement("polygon") sideShape.Fill = darkerColor - sideShape.Stroke = "none" - renderedSides := fmt.Sprintf(``, - strings.Join(sidePoints, " "), shapeStyle(sideShape), maskID) + sideShape.Points = strings.Join(sidePoints, " ") + sideShape.Mask = fmt.Sprintf("url(#%s)", maskID) + sideShape.Style = svg_style.ShapeStyle(targetShape) + renderedSides := mainShape.Render() - return borderMask + mainRect + renderedSides + renderedBorder + return borderMask + mainShapeRendered + renderedSides + renderedBorder } func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, err error) { @@ -646,7 +688,8 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)) width := float64(targetShape.Width) height := float64(targetShape.Height) - style := shapeStyle(targetShape) + fill, stroke := svg_style.ShapeTheme(targetShape) + style := svg_style.ShapeStyle(targetShape) shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[targetShape.Type] s := shape.NewShape(shapeType, geo.NewBox(tl, width, height)) @@ -682,12 +725,12 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske if err != nil { return "", err } - fmt.Fprintf(writer, out) + fmt.Fprint(writer, out) } else { drawClass(writer, targetShape) } fmt.Fprintf(writer, ``) - fmt.Fprintf(writer, closingTag) + fmt.Fprint(writer, closingTag) return labelMask, nil case d2target.ShapeSQLTable: if sketchRunner != nil { @@ -695,31 +738,38 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske if err != nil { return "", err } - fmt.Fprintf(writer, out) + fmt.Fprint(writer, out) } else { drawTable(writer, targetShape) } fmt.Fprintf(writer, ``) - fmt.Fprintf(writer, closingTag) + fmt.Fprint(writer, closingTag) return labelMask, nil case d2target.ShapeOval: if targetShape.Multiple { - fmt.Fprint(writer, renderOval(multipleTL, width, height, style)) + fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, stroke, style)) } if sketchRunner != nil { out, err := d2sketch.Oval(sketchRunner, targetShape) if err != nil { return "", err } - fmt.Fprintf(writer, out) + fmt.Fprint(writer, out) } else { - fmt.Fprint(writer, renderOval(tl, width, height, style)) + fmt.Fprint(writer, renderOval(tl, width, height, fill, stroke, style)) } case d2target.ShapeImage: - fmt.Fprintf(writer, ``, - html.EscapeString(targetShape.Icon.String()), - targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, style) + el := svg_style.NewThemableElement("image") + el.X = float64(targetShape.Pos.X) + el.Y = float64(targetShape.Pos.Y) + el.Width = float64(targetShape.Width) + el.Height = float64(targetShape.Height) + el.Href = html.EscapeString(targetShape.Icon.String()) + el.Fill = fill + el.Stroke = stroke + el.Style = style + fmt.Fprint(writer, el.Render()) // TODO should standardize "" to rectangle case d2target.ShapeRectangle, d2target.ShapeSequenceDiagram, "": @@ -727,26 +777,45 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske fmt.Fprint(writer, render3dRect(targetShape)) } else { if targetShape.Multiple { - fmt.Fprintf(writer, ``, - targetShape.Pos.X+10, targetShape.Pos.Y-10, targetShape.Width, targetShape.Height, style) + el := svg_style.NewThemableElement("rect") + el.X = float64(targetShape.Pos.X + 10) + el.Y = float64(targetShape.Pos.Y - 10) + el.Width = float64(targetShape.Width) + el.Height = float64(targetShape.Height) + el.Fill = fill + el.Stroke = stroke + el.Style = style + fmt.Fprint(writer, el.Render()) } if sketchRunner != nil { out, err := d2sketch.Rect(sketchRunner, targetShape) if err != nil { return "", err } - fmt.Fprintf(writer, out) + fmt.Fprint(writer, out) } else { - fmt.Fprintf(writer, ``, - targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, style) + el := svg_style.NewThemableElement("rect") + el.X = float64(targetShape.Pos.X) + el.Y = float64(targetShape.Pos.Y) + el.Width = float64(targetShape.Width) + el.Height = float64(targetShape.Height) + el.Fill = fill + el.Stroke = stroke + el.Style = style + fmt.Fprint(writer, el.Render()) } } case d2target.ShapeText, d2target.ShapeCode: default: if targetShape.Multiple { multiplePathData := shape.NewShape(shapeType, geo.NewBox(multipleTL, width, height)).GetSVGPathData() + el := svg_style.NewThemableElement("path") + el.Fill = fill + el.Stroke = stroke + el.Style = style for _, pathData := range multiplePathData { - fmt.Fprintf(writer, ``, pathData, style) + el.D = pathData + fmt.Fprint(writer, el.Render()) } } @@ -755,10 +824,15 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske if err != nil { return "", err } - fmt.Fprintf(writer, out) + fmt.Fprint(writer, out) } else { + el := svg_style.NewThemableElement("path") + el.Fill = fill + el.Stroke = stroke + el.Style = style for _, pathData := range s.GetSVGPathData() { - fmt.Fprintf(writer, ``, pathData, style) + el.D = pathData + fmt.Fprint(writer, el.Render()) } } } @@ -826,11 +900,14 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske } svgStyles := styleToSVG(style) - containerStyle := fmt.Sprintf(`stroke: %s;fill:%s`, targetShape.Stroke, style.Get(chroma.Background).Background.String()) - fmt.Fprintf(writer, ``, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity) - fmt.Fprintf(writer, ``, - targetShape.Width, targetShape.Height, containerStyle) + rectEl := svg_style.NewThemableElement("rect") + rectEl.Width = float64(targetShape.Width) + rectEl.Height = float64(targetShape.Height) + rectEl.Stroke = targetShape.Stroke + rectEl.Class = "shape" + rectEl.Style = fmt.Sprintf(`fill:%s`, style.Get(chroma.Background).Background.String()) + fmt.Fprint(writer, rectEl.Render()) // Padding fmt.Fprintf(writer, ``) @@ -867,31 +944,32 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske // we need the self closing form in this svg/xhtml context render = strings.ReplaceAll(render, "
", "
") - var mdStyle string - if targetShape.Fill != "" { - mdStyle = fmt.Sprintf("background-color:%s;", targetShape.Fill) + mdEl := svg_style.NewThemableElement("div") + mdEl.Xmlns = "http://www.w3.org/1999/xhtml" + mdEl.Class = "md" + mdEl.Content = render + if targetShape.Fill != color.Empty { + mdEl.BackgroundColor = targetShape.Fill } - if targetShape.Stroke != "" { - mdStyle += fmt.Sprintf("color:%s;", targetShape.Stroke) + if targetShape.Stroke != color.Empty { + mdEl.Color = targetShape.Stroke } - - fmt.Fprintf(writer, `
%v
`, mdStyle, render) + fmt.Fprint(writer, mdEl.Render()) fmt.Fprint(writer, ``) } else { - fontColor := "black" - if targetShape.Color != "" { + fontColor := color.N1 + if targetShape.Color != color.Empty { fontColor = targetShape.Color } - textStyle := fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "middle", targetShape.FontSize, fontColor) - x := labelTL.X + float64(targetShape.LabelWidth)/2. + textEl := svg_style.NewThemableElement("text") + textEl.X = labelTL.X + float64(targetShape.LabelWidth)/2 // text is vertically positioned at its baseline which is at labelTL+FontSize - y := labelTL.Y + float64(targetShape.FontSize) - fmt.Fprintf(writer, `%s`, - fontClass, - x, y, - textStyle, - RenderText(targetShape.Label, x, float64(targetShape.LabelHeight)), - ) + textEl.Y = labelTL.Y + float64(targetShape.FontSize) + textEl.Fill = fontColor + textEl.Class = fontClass + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", targetShape.FontSize) + textEl.Content = RenderText(targetShape.Label, textEl.X, float64(targetShape.LabelHeight)) + fmt.Fprint(writer, textEl.Render()) if targetShape.Blend { labelMask = makeLabelMask(labelTL, targetShape.LabelWidth, targetShape.LabelHeight-d2graph.INNER_LABEL_PADDING) } @@ -917,7 +995,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske ) } - fmt.Fprintf(writer, closingTag) + fmt.Fprint(writer, closingTag) return labelMask, nil } @@ -942,46 +1020,9 @@ func RenderText(text string, x, height float64) string { return strings.Join(rendered, "") } -func shapeStyle(shape d2target.Shape) string { - out := "" - - 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 - out += fmt.Sprintf(`fill:%s;`, shape.Stroke) - // Stroke (border) of these shapes should match the header fill - out += fmt.Sprintf(`stroke:%s;`, shape.Fill) - } else { - out += fmt.Sprintf(`fill:%s;`, shape.Fill) - out += fmt.Sprintf(`stroke:%s;`, shape.Stroke) - } - 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 connectionStyle(connection d2target.Connection) string { - out := "" - - out += fmt.Sprintf(`stroke:%s;`, connection.Stroke) - 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 embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) { +func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) string { content := buf.String() - buf.WriteString(``) + out += `]]>` + return out } //go:embed fitToScreen.js var fitToScreenScript string +const ( + BG_COLOR = color.N7 + FG_COLOR = color.N1 +) + // TODO minify output at end func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { var sketchRunner *d2sketch.Runner @@ -1120,36 +1167,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { } } - theme := d2themescatalog.Find(opts.ThemeID) - bgColor := theme.Colors.Neutrals.N7 - fgColor := theme.Colors.Neutrals.N1 - buf := &bytes.Buffer{} - w, h := setViewbox(buf, diagram, pad, bgColor, fgColor) - - styleCSS2 := "" - if sketchRunner != nil { - styleCSS2 = "\n" + sketchStyleCSS - } - buf.WriteString(fmt.Sprintf(``, styleCSS, styleCSS2)) - - // 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 - buf.WriteString(fmt.Sprintf(``, fitToScreenScript)) - - hasMarkdown := false - for _, s := range diagram.Shapes { - if s.Label != "" && s.Type == d2target.ShapeText { - hasMarkdown = true - break - } - } - if hasMarkdown { - fmt.Fprintf(buf, ``, mdCSS) - } - if sketchRunner != nil && sketchBg { - fmt.Fprint(buf, d2sketch.DefineFillPattern()) - } // only define shadow filter if a shape uses it for _, s := range diagram.Shapes { @@ -1184,7 +1202,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { markers := map[string]struct{}{} for _, obj := range allObjects { if c, is := obj.(d2target.Connection); is { - labelMask, err := drawConnection(buf, bgColor, fgColor, labelMaskID, c, markers, idToShape, sketchRunner) + labelMask, err := drawConnection(buf, BG_COLOR, FG_COLOR, labelMaskID, c, markers, idToShape, sketchRunner) if err != nil { return nil, err } @@ -1204,6 +1222,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { } // Note: we always want this since we reference it on connections even if there end up being no masked labels + w, h, tl, _ := dimensions(buf, diagram, pad) fmt.Fprint(buf, strings.Join([]string{ fmt.Sprintf(``, labelMaskID, -pad, -pad, w, h, @@ -1215,10 +1234,48 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { ``, }, "\n")) - embedFonts(buf, diagram.FontFamily) + // TODO minify + // TODO background stuff. e.g. dotted, grid, colors + containerEl := svg_style.NewThemableElement("rect") + containerEl.X = float64(tl.X - pad - 10) // TODO the background is not rendered all over the image + containerEl.Y = float64(tl.Y - pad - 10) // so I had to add 10 to the size - someone smarter than me please fix this + containerEl.Width = float64(w + 10*2) + containerEl.Height = float64(h + 10*2) + containerEl.Fill = color.N7 + // containerEl.Color = color.N1 TODO this is useless as this element has no children - buf.WriteString(``) - return buf.Bytes(), nil + // generate elements that will be appended to the SVG tag + styleCSS2 := "" + if sketchRunner != nil { + styleCSS2 = "\n" + sketchStyleCSS + } + svgOut := fmt.Sprintf(``, styleCSS, styleCSS2) + // 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) + hasMarkdown := false + for _, s := range diagram.Shapes { + if s.Label != "" && s.Type == d2target.ShapeText { + hasMarkdown = true + break + } + } + if hasMarkdown { + svgOut += fmt.Sprintf(``, mdCSS) + } + if sketchRunner != nil && sketchBg { + svgOut += d2sketch.DefineFillPattern() + } + svgOut += embedFonts(buf, diagram.FontFamily) + + // render the document + docRendered := fmt.Sprintf(`%s%s%s`, + w, h, tl.X-pad, tl.Y-pad, w, h, + svgOut, + containerEl.Render(), + buf.String(), + ) + return []byte(docRendered), nil } type DiagramObject interface { diff --git a/d2renderers/d2svg/style.css b/d2renderers/d2svg/style.css index 2476c6ae7..a5a8e6c39 100644 --- a/d2renderers/d2svg/style.css +++ b/d2renderers/d2svg/style.css @@ -7,6 +7,456 @@ stroke-linejoin: round; } .blend { - mix-blend-mode: multiply; + mix-Blend-mode: multiply; opacity: 0.5; } + +/* +.fill +.stroke + +.background-color +.color +*/ + +.fill-N1 { + fill: #0A0F25; +} +.fill-N2 { + fill: #676C7E; +} +.fill-N3 { + fill: #9499AB; +} +.fill-N4 { + fill: #CFD2DD; +} +.fill-N5 { + fill: #DEE1EB; +} +.fill-N6 { + fill: #EEF1F8; +} +.fill-N7 { + fill: #FFFFFF; +} +.fill-B1 { + fill: #0D32B2; +} +.fill-B2 { + fill: #0D32B2; +} +.fill-B3 { + fill: #E3E9FD; +} +.fill-B4 { + fill: #E3E9FD; +} +.fill-B5 { + fill: #EDF0FD; +} +.fill-B6 { + fill: #F7F8FE; +} +.fill-AA2 { + fill: #4A6FF3; +} +.fill-AA4 { + fill: #EDF0FD; +} +.fill-AA5 { + fill: #F7F8FE; +} +.fill-AB4 { + fill: #DEE1EB; +} +.fill-AB5 { + fill: #F7F8FE; +} + +.stroke-N1 { + stroke: #0A0F25; +} +.stroke-N2 { + stroke: #676C7E; +} +.stroke-N3 { + stroke: #9499AB; +} +.stroke-N4 { + stroke: #CFD2DD; +} +.stroke-N5 { + stroke: #DEE1EB; +} +.stroke-N6 { + stroke: #EEF1F8; +} +.stroke-N7 { + stroke: #FFFFFF; +} +.stroke-B1 { + stroke: #0D32B2; +} +.stroke-B2 { + stroke: #0D32B2; +} +.stroke-B3 { + stroke: #E3E9FD; +} +.stroke-B4 { + stroke: #E3E9FD; +} +.stroke-B5 { + stroke: #EDF0FD; +} +.stroke-B6 { + stroke: #F7F8FE; +} +.stroke-AA2 { + stroke: #4A6FF3; +} +.stroke-AA4 { + stroke: #EDF0FD; +} +.stroke-AA5 { + stroke: #F7F8FE; +} +.stroke-AB4 { + stroke: #DEE1EB; +} +.stroke-AB5 { + stroke: #F7F8FE; +} + +.background-color-N1 { + background-color: #0A0F25; +} +.background-color-N2 { + background-color: #676C7E; +} +.background-color-N3 { + background-color: #9499AB; +} +.background-color-N4 { + background-color: #CFD2DD; +} +.background-color-N5 { + background-color: #DEE1EB; +} +.background-color-N6 { + background-color: #EEF1F8; +} +.background-color-N7 { + background-color: #FFFFFF; +} +.background-color-B1 { + background-color: #0D32B2; +} +.background-color-B2 { + background-color: #0D32B2; +} +.background-color-B3 { + background-color: #E3E9FD; +} +.background-color-B4 { + background-color: #E3E9FD; +} +.background-color-B5 { + background-color: #EDF0FD; +} +.background-color-B6 { + background-color: #F7F8FE; +} +.background-color-AA2 { + background-color: #4A6FF3; +} +.background-color-AA4 { + background-color: #EDF0FD; +} +.background-color-AA5 { + background-color: #F7F8FE; +} +.background-color-AB4 { + background-color: #DEE1EB; +} +.background-color-AB5 { + background-color: #F7F8FE; +} + +.color-N1 { + color: #0A0F25; +} +.color-N2 { + color: #676C7E; +} +.color-N3 { + color: #9499AB; +} +.color-N4 { + color: #CFD2DD; +} +.color-N5 { + color: #DEE1EB; +} +.color-N6 { + color: #EEF1F8; +} +.color-N7 { + color: #FFFFFF; +} +.color-B1 { + color: #0D32B2; +} +.color-B2 { + color: #0D32B2; +} +.color-B3 { + color: #E3E9FD; +} +.color-B4 { + color: #E3E9FD; +} +.color-B5 { + color: #EDF0FD; +} +.color-B6 { + color: #F7F8FE; +} +.color-AA2 { + color: #4A6FF3; +} +.color-AA4 { + color: #EDF0FD; +} +.color-AA5 { + color: #F7F8FE; +} +.color-AB4 { + color: #DEE1EB; +} +.color-AB5 { + color: #F7F8FE; +} + +@media screen and (prefers-color-scheme: dark) { + .fill-N1 { + fill: #cdd6f4; + } + .fill-N2 { + fill: #bac2de; + } + .fill-N3 { + fill: #a6adc8; + } + .fill-N4 { + fill: #585b70; + } + .fill-N5 { + fill: #45475a; + } + .fill-N6 { + fill: #313244; + } + .fill-N7 { + fill: #1e1e2e; + } + .fill-B1 { + fill: #cba6f7; + } + .fill-B2 { + fill: #cba6f7; + } + .fill-B3 { + fill: #6c7086; + } + .fill-B4 { + fill: #585b70; + } + .fill-B5 { + fill: #45475a; + } + .fill-B6 { + fill: #313244; + } + .fill-AA2 { + fill: #f38ba8; + } + .fill-AA4 { + fill: #45475a; + } + .fill-AA5 { + fill: #313244; + } + .fill-AB4 { + fill: #45475a; + } + .fill-AB5 { + fill: #313244; + } + + .stroke-N1 { + stroke: #cdd6f4; + } + .stroke-N2 { + stroke: #bac2de; + } + .stroke-N3 { + stroke: #a6adc8; + } + .stroke-N4 { + stroke: #585b70; + } + .stroke-N5 { + stroke: #45475a; + } + .stroke-N6 { + stroke: #313244; + } + .stroke-N7 { + stroke: #1e1e2e; + } + .stroke-B1 { + stroke: #cba6f7; + } + .stroke-B2 { + stroke: #cba6f7; + } + .stroke-B3 { + stroke: #6c7086; + } + .stroke-B4 { + stroke: #585b70; + } + .stroke-B5 { + stroke: #45475a; + } + .stroke-B6 { + stroke: #313244; + } + .stroke-AA2 { + stroke: #f38ba8; + } + .stroke-AA4 { + stroke: #45475a; + } + .stroke-AA5 { + stroke: #313244; + } + .stroke-AB4 { + stroke: #45475a; + } + .stroke-AB5 { + stroke: #313244; + } + + .background-color-N1 { + background-color: #cdd6f4; + } + .background-color-N2 { + background-color: #bac2de; + } + .background-color-N3 { + background-color: #a6adc8; + } + .background-color-N4 { + background-color: #585b70; + } + .background-color-N5 { + background-color: #45475a; + } + .background-color-N6 { + background-color: #313244; + } + .background-color-N7 { + background-color: #1e1e2e; + } + .background-color-B1 { + background-color: #cba6f7; + } + .background-color-B2 { + background-color: #cba6f7; + } + .background-color-B3 { + background-color: #6c7086; + } + .background-color-B4 { + background-color: #585b70; + } + .background-color-B5 { + background-color: #45475a; + } + .background-color-B6 { + background-color: #313244; + } + .background-color-AA2 { + background-color: #f38ba8; + } + .background-color-AA4 { + background-color: #45475a; + } + .background-color-AA5 { + background-color: #313244; + } + .background-color-AB4 { + background-color: #45475a; + } + .background-color-AB5 { + background-color: #313244; + } + + .color-N1 { + color: #cdd6f4; + } + .color-N2 { + color: #bac2de; + } + .color-N3 { + color: #a6adc8; + } + .color-N4 { + color: #585b70; + } + .color-N5 { + color: #45475a; + } + .color-N6 { + color: #313244; + } + .color-N7 { + color: #1e1e2e; + } + .color-B1 { + color: #cba6f7; + } + .color-B2 { + color: #cba6f7; + } + .color-B3 { + color: #6c7086; + } + .color-B4 { + color: #585b70; + } + .color-B5 { + color: #45475a; + } + .color-B6 { + color: #313244; + } + .color-AA2 { + color: #f38ba8; + } + .color-AA4 { + color: #45475a; + } + .color-AA5 { + color: #313244; + } + .color-AB4 { + color: #45475a; + } + .color-AB5 { + color: #313244; + } +} diff --git a/d2renderers/d2svg/table.go b/d2renderers/d2svg/table.go index 46bdef37c..110c95c70 100644 --- a/d2renderers/d2svg/table.go +++ b/d2renderers/d2svg/table.go @@ -3,18 +3,22 @@ package d2svg import ( "fmt" "io" - "strings" "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/svg" + svg_style "oss.terrastruct.com/d2/lib/svg/style" "oss.terrastruct.com/util-go/go2" ) func tableHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string { - str := fmt.Sprintf(``, - box.TopLeft.X, box.TopLeft.Y, box.Width, box.Height, shape.Fill) + rectEl := svg_style.NewThemableElement("rect") + rectEl.X, rectEl.Y = box.TopLeft.X, box.TopLeft.Y + rectEl.Width, rectEl.Height = box.Width, box.Height + rectEl.Fill = shape.Fill + rectEl.Class = "class_header" + str := rectEl.Render() if text != "" { tl := label.InsideMiddleLeft.GetPointOnBox( @@ -24,17 +28,16 @@ func tableHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, tex textHeight, ) - str += fmt.Sprintf(`%s`, - "text", - tl.X, - tl.Y+textHeight*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", - "start", - 4+fontSize, - shape.Stroke, - ), - svg.EscapeText(text), + textEl := svg_style.NewThemableElement("text") + textEl.X = tl.X + textEl.Y = tl.Y + textHeight*3/4 + textEl.Fill = shape.Stroke + textEl.Class = "text" + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", + "start", 4+fontSize, ) + textEl.Content = svg.EscapeText(text) + str += textEl.Render() } return str } @@ -55,33 +58,40 @@ func tableRow(shape d2target.Shape, box *geo.Box, nameText, typeText, constraint fontSize, ) - return strings.Join([]string{ - fmt.Sprintf(`%s`, - nameTL.X, - nameTL.Y+fontSize*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.PrimaryAccentColor), - svg.EscapeText(nameText), - ), + textEl := svg_style.NewThemableElement("text") + textEl.X = nameTL.X + textEl.Y = nameTL.Y + fontSize*3/4 + textEl.Fill = shape.PrimaryAccentColor + textEl.Class = "text" + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", fontSize) + textEl.Content = svg.EscapeText(nameText) + out := textEl.Render() - fmt.Sprintf(`%s`, - nameTL.X+longestNameWidth+2*d2target.NamePadding, - nameTL.Y+fontSize*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.NeutralAccentColor), - svg.EscapeText(typeText), - ), + textEl.X = nameTL.X + longestNameWidth + 2*d2target.NamePadding + textEl.Fill = shape.NeutralAccentColor + textEl.Content = svg.EscapeText(typeText) + out += textEl.Render() - fmt.Sprintf(`%s`, - constraintTR.X, - constraintTR.Y+fontSize*3/4, - fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s;letter-spacing:2px;", "end", fontSize, shape.SecondaryAccentColor), - constraintText, - ), - }, "\n") + textEl.X = constraintTR.X + textEl.Y = constraintTR.Y + fontSize*3/4 + textEl.Fill = shape.SecondaryAccentColor + textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx;letter-spacing:2px", "end", fontSize) + textEl.Content = constraintText + out += textEl.Render() + + return out } func drawTable(writer io.Writer, targetShape d2target.Shape) { - fmt.Fprintf(writer, ``, - targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, shapeStyle(targetShape)) + rectEl := svg_style.NewThemableElement("rect") + rectEl.X = float64(targetShape.Pos.X) + rectEl.Y = float64(targetShape.Pos.Y) + rectEl.Width = float64(targetShape.Width) + rectEl.Height = float64(targetShape.Height) + rectEl.Fill, rectEl.Stroke = svg_style.ShapeTheme(targetShape) + rectEl.Class = "shape" + rectEl.Style = svg_style.ShapeStyle(targetShape) + fmt.Fprint(writer, rectEl.Render()) box := geo.NewBox( geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)), @@ -108,10 +118,12 @@ func drawTable(writer io.Writer, targetShape d2target.Shape) { tableRow(targetShape, rowBox, f.Name.Label, f.Type.Label, f.ConstraintAbbr(), float64(targetShape.FontSize), float64(longestNameWidth)), ) rowBox.TopLeft.Y += rowHeight - fmt.Fprintf(writer, ``, - rowBox.TopLeft.X, rowBox.TopLeft.Y, - rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, - targetShape.Fill, - ) + + lineEl := svg_style.NewThemableElement("line") + lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y + lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y + lineEl.Stroke = targetShape.Fill + lineEl.Style = "stroke-width:2" + fmt.Fprint(writer, lineEl.Render()) } } diff --git a/d2target/d2target.go b/d2target/d2target.go index b426adfd9..0ba454aeb 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -11,7 +11,7 @@ import ( "oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/d2/d2renderers/d2fonts" - "oss.terrastruct.com/d2/d2themes" + "oss.terrastruct.com/d2/lib/color" "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/shape" @@ -416,11 +416,11 @@ func NewTextDimensions(w, h int) *TextDimensions { return &TextDimensions{Width: w, Height: h} } -func (text MText) GetColor(theme *d2themes.Theme, isItalic bool) string { +func (text MText) GetColor(isItalic bool) string { if isItalic { - return theme.Colors.Neutrals.N2 + return color.N2 } - return theme.Colors.Neutrals.N1 + return color.N1 } var DSL_SHAPE_TO_SHAPE_TYPE = map[string]string{ diff --git a/lib/color/color.go b/lib/color/color.go index 398430f35..3f1109a82 100644 --- a/lib/color/color.go +++ b/lib/color/color.go @@ -14,3 +14,34 @@ func Darken(colorString string) (string, error) { // decrease luminance by 10% return colorful.Hsl(h, s, l-.1).Clamped().Hex(), nil } + +const ( + N1 = "N1" + N2 = "N2" + N3 = "N3" + N4 = "N4" + N5 = "N5" + N6 = "N6" + N7 = "N7" + + // Base Colors: used for containers + B1 = "B1" + B2 = "B2" + B3 = "B3" + B4 = "B4" + B5 = "B5" + B6 = "B6" + + // Alternative colors A + AA2 = "AA2" + AA4 = "AA4" + AA5 = "AA5" + + // Alternative colors B + AB4 = "AB4" + AB5 = "AB4" + + // Special + Empty = "" + None = "none" +) diff --git a/lib/svg/style/themable_element.go b/lib/svg/style/themable_element.go new file mode 100644 index 000000000..f457705c3 --- /dev/null +++ b/lib/svg/style/themable_element.go @@ -0,0 +1,233 @@ +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. +// i.e. N[1-7] | B[1-6] | AA[245] | AB[45] +type ThemableElement struct { + tag string + + X float64 + X1 float64 + X2 float64 + Y float64 + Y1 float64 + Y2 float64 + Width float64 + Height float64 + R float64 + Rx float64 + Ry float64 + Cx float64 + Cy float64 + + D string + Mask string + Points string + Transform string + Href string + Xmlns string + + Fill string + Stroke string + BackgroundColor string + Color string + + Class string + Style string + Attributes string + + Content string +} + +func NewThemableElement(tag string) *ThemableElement { + return &ThemableElement{ + tag, + math.MaxFloat64, + math.MaxFloat64, + math.MaxFloat64, + math.MaxFloat64, + math.MaxFloat64, + math.MaxFloat64, + math.MaxFloat64, + math.MaxFloat64, + math.MaxFloat64, + math.MaxFloat64, + math.MaxFloat64, + math.MaxFloat64, + math.MaxFloat64, + "", + "", + "", + "", + "", + "", + color.Empty, + color.Empty, + color.Empty, + color.Empty, + "", + "", + "", + "", + } +} + +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 { + out += fmt.Sprintf(` x="%f"`, el.X) + } + if el.X1 != math.MaxFloat64 { + out += fmt.Sprintf(` x1="%f"`, el.X1) + } + if el.X2 != math.MaxFloat64 { + out += fmt.Sprintf(` x2="%f"`, el.X2) + } + if el.Y != math.MaxFloat64 { + out += fmt.Sprintf(` y="%f"`, el.Y) + } + if el.Y1 != math.MaxFloat64 { + out += fmt.Sprintf(` y1="%f"`, el.Y1) + } + if el.Y2 != math.MaxFloat64 { + out += fmt.Sprintf(` y2="%f"`, el.Y2) + } + if el.Width != math.MaxFloat64 { + out += fmt.Sprintf(` width="%f"`, el.Width) + } + if el.Height != math.MaxFloat64 { + out += fmt.Sprintf(` height="%f"`, el.Height) + } + if el.R != math.MaxFloat64 { + out += fmt.Sprintf(` r="%f"`, el.R) + } + if el.Rx != math.MaxFloat64 { + out += fmt.Sprintf(` rx="%f"`, el.Rx) + } + if el.Ry != math.MaxFloat64 { + out += fmt.Sprintf(` ry="%f"`, el.Ry) + } + if el.Cx != math.MaxFloat64 { + out += fmt.Sprintf(` cx="%f"`, el.Cx) + } + if el.Cy != math.MaxFloat64 { + out += fmt.Sprintf(` cy="%f"`, el.Cy) + } + + if len(el.D) > 0 { + out += fmt.Sprintf(` d="%s"`, el.D) + } + if len(el.Mask) > 0 { + out += fmt.Sprintf(` mask="%s"`, el.Mask) + } + if len(el.Points) > 0 { + out += fmt.Sprintf(` points="%s"`, el.Points) + } + if len(el.Transform) > 0 { + out += fmt.Sprintf(` transform="%s"`, el.Transform) + } + if len(el.Href) > 0 { + out += fmt.Sprintf(` href="%s"`, el.Href) + } + if len(el.Xmlns) > 0 { + out += fmt.Sprintf(` xmlns="%s"`, el.Xmlns) + } + + class := el.Class + style := el.Style + + // Add class {property}-{theme color} if the color is from a theme, set the property otherwise + if re.MatchString(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) { + 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) { + 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) { + class += fmt.Sprintf(" color-%s", el.Color) + } else if len(el.Color) > 0 { + out += fmt.Sprintf(` color="%s"`, el.Color) + } + + if len(class) > 0 { + out += fmt.Sprintf(` class="%s"`, class) + } + if len(style) > 0 { + out += fmt.Sprintf(` style="%s"`, style) + } + if len(el.Attributes) > 0 { + out += fmt.Sprintf(` %s`, el.Attributes) + } + + if len(el.Content) > 0 { + return fmt.Sprintf("%s>%s", out, el.Content, el.tag) + } + return out + " />" +} diff --git a/main.go b/main.go index 659260cec..948c59357 100644 --- a/main.go +++ b/main.go @@ -238,9 +238,8 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc layout := plugin.Layout opts := &d2lib.CompileOptions{ - Layout: layout, - Ruler: ruler, - ThemeID: themeID, + Layout: layout, + Ruler: ruler, } if sketch { opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)