support for prefers-color-scheme

This commit is contained in:
Vojtěch Fošnár 2023-01-09 19:16:28 +01:00
parent bcb128962e
commit a81ab2d73e
No known key found for this signature in database
GPG key ID: 657727E71C40859A
13 changed files with 1306 additions and 530 deletions

View file

@ -7,14 +7,11 @@ import (
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes" "oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
) )
func Export(ctx context.Context, g *d2graph.Graph, themeID int64, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) { func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamily) (*d2target.Diagram, error) {
theme := d2themescatalog.Find(themeID)
diagram := d2target.NewDiagram() diagram := d2target.NewDiagram()
if fontFamily == nil { if fontFamily == nil {
fontFamily = go2.Pointer(d2fonts.SourceSansPro) 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)) diagram.Shapes = make([]d2target.Shape, len(g.Objects))
for i := range 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)) diagram.Connections = make([]d2target.Connection, len(g.Edges))
for i := range 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 return diagram, nil
} }
func applyTheme(shape *d2target.Shape, obj *d2graph.Object, theme *d2themes.Theme) { func applyTheme(shape *d2target.Shape, obj *d2graph.Object) {
shape.Stroke = obj.GetStroke(theme, shape.StrokeDash) shape.Stroke = obj.GetStroke(shape.StrokeDash)
shape.Fill = obj.GetFill(theme) shape.Fill = obj.GetFill()
if obj.Attributes.Shape.Value == d2target.ShapeText { 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 { if obj.Attributes.Shape.Value == d2target.ShapeSQLTable || obj.Attributes.Shape.Value == d2target.ShapeClass {
shape.PrimaryAccentColor = theme.Colors.B2 shape.PrimaryAccentColor = color.B2
shape.SecondaryAccentColor = theme.Colors.AA2 shape.SecondaryAccentColor = color.AA2
shape.NeutralAccentColor = theme.Colors.Neutrals.N2 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 := d2target.BaseShape()
shape.SetType(obj.Attributes.Shape.Value) shape.SetType(obj.Attributes.Shape.Value)
shape.ID = obj.AbsID() shape.ID = obj.AbsID()
@ -120,8 +117,8 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
} }
applyStyles(shape, obj) applyStyles(shape, obj)
applyTheme(shape, obj, theme) applyTheme(shape, obj)
shape.Color = text.GetColor(theme, shape.Italic) shape.Color = text.GetColor(shape.Italic)
applyStyles(shape, obj) applyStyles(shape, obj)
switch obj.Attributes.Shape.Value { switch obj.Attributes.Shape.Value {
@ -153,7 +150,7 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
return *shape return *shape
} }
func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection { func toConnection(edge *d2graph.Edge) d2target.Connection {
connection := d2target.BaseConnection() connection := d2target.BaseConnection()
connection.ID = edge.AbsID() connection.ID = edge.AbsID()
connection.ZIndex = edge.ZIndex connection.ZIndex = edge.ZIndex
@ -202,7 +199,7 @@ func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection
if edge.Attributes.Style.StrokeDash != nil { if edge.Attributes.Style.StrokeDash != nil {
connection.StrokeDash, _ = strconv.ParseFloat(edge.Attributes.Style.StrokeDash.Value, 64) 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 { if edge.Attributes.Style.Stroke != nil {
connection.Stroke = edge.Attributes.Style.Stroke.Value 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.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 { if edge.Attributes.Style.FontColor != nil {
connection.Color = edge.Attributes.Style.FontColor.Value connection.Color = edge.Attributes.Style.FontColor.Value
} }

View file

@ -16,7 +16,7 @@ import (
"oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex" "oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2target" "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/geo"
"oss.terrastruct.com/d2/lib/textmeasure" "oss.terrastruct.com/d2/lib/textmeasure"
) )
@ -321,14 +321,14 @@ func (l ContainerLevel) LabelSize() int {
return d2fonts.FONT_SIZE_M return d2fonts.FONT_SIZE_M
} }
func (obj *Object) GetFill(theme *d2themes.Theme) string { func (obj *Object) GetFill() string {
level := int(obj.Level()) level := int(obj.Level())
if obj.IsSequenceDiagramNote() { if obj.IsSequenceDiagramNote() {
return theme.Colors.Neutrals.N7 return color.N7
} else if obj.IsSequenceDiagramGroup() { } else if obj.IsSequenceDiagramGroup() {
return theme.Colors.Neutrals.N5 return color.N5
} else if obj.Parent.IsSequenceDiagram() { } else if obj.Parent.IsSequenceDiagram() {
return theme.Colors.B5 return color.B5
} }
// fill for spans // fill for spans
@ -336,19 +336,19 @@ func (obj *Object) GetFill(theme *d2themes.Theme) string {
if sd != nil { if sd != nil {
level -= int(sd.Level()) level -= int(sd.Level())
if level == 1 { if level == 1 {
return theme.Colors.B3 return color.B3
} else if level == 2 { } else if level == 2 {
return theme.Colors.B4 return color.B4
} else if level == 3 { } else if level == 3 {
return theme.Colors.B5 return color.B5
} else if level == 4 { } else if level == 4 {
return theme.Colors.Neutrals.N6 return color.N6
} }
return theme.Colors.Neutrals.N7 return color.N7
} }
if obj.IsSequenceDiagram() { if obj.IsSequenceDiagram() {
return theme.Colors.Neutrals.N7 return color.N7
} }
shape := obj.Attributes.Shape.Value 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 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 level == 1 {
if !obj.IsContainer() { if !obj.IsContainer() {
return theme.Colors.B6 return color.B6
} }
return theme.Colors.B4 return color.B4
} else if level == 2 { } else if level == 2 {
return theme.Colors.B5 return color.B5
} else if level == 3 { } 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 strings.EqualFold(shape, d2target.ShapeCylinder) || strings.EqualFold(shape, d2target.ShapeStoredData) || strings.EqualFold(shape, d2target.ShapePackage) {
if level == 1 { 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 strings.EqualFold(shape, d2target.ShapeStep) || strings.EqualFold(shape, d2target.ShapePage) || strings.EqualFold(shape, d2target.ShapeDocument) {
if level == 1 { if level == 1 {
return theme.Colors.AB4 return color.AB4
} }
return theme.Colors.AB5 return color.AB5
} }
if strings.EqualFold(shape, d2target.ShapePerson) { if strings.EqualFold(shape, d2target.ShapePerson) {
return theme.Colors.B3 return color.B3
} }
if strings.EqualFold(shape, d2target.ShapeDiamond) { 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) { 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) { 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) { 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 shape := obj.Attributes.Shape.Value
if strings.EqualFold(shape, d2target.ShapeCode) || if strings.EqualFold(shape, d2target.ShapeCode) ||
strings.EqualFold(shape, d2target.ShapeText) { strings.EqualFold(shape, d2target.ShapeText) {
return theme.Colors.Neutrals.N1 return color.N1
} }
if strings.EqualFold(shape, d2target.ShapeClass) || if strings.EqualFold(shape, d2target.ShapeClass) ||
strings.EqualFold(shape, d2target.ShapeSQLTable) { strings.EqualFold(shape, d2target.ShapeSQLTable) {
return theme.Colors.Neutrals.N7 return color.N7
} }
if dashGapSize != 0.0 { if dashGapSize != 0.0 {
return theme.Colors.B2 return color.B2
} }
return theme.Colors.B1 return color.B1
} }
func (obj *Object) Level() ContainerLevel { func (obj *Object) Level() ContainerLevel {
@ -867,11 +867,11 @@ type EdgeReference struct {
ScopeObj *Object `json:"-"` ScopeObj *Object `json:"-"`
} }
func (e *Edge) GetStroke(theme *d2themes.Theme, dashGapSize interface{}) string { func (e *Edge) GetStroke(dashGapSize interface{}) string {
if dashGapSize != 0.0 { if dashGapSize != 0.0 {
return theme.Colors.B2 return color.B2
} }
return theme.Colors.B1 return color.B1
} }
func (e *Edge) ArrowString() string { func (e *Edge) ArrowString() string {

View file

@ -29,7 +29,6 @@ type CompileOptions struct {
// - pre-measured (web setting) // - pre-measured (web setting)
// TODO maybe some will want to configure code font too, but that's much lower priority // TODO maybe some will want to configure code font too, but that's much lower priority
FontFamily *d2fonts.FontFamily FontFamily *d2fonts.FontFamily
ThemeID int64
} }
func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target.Diagram, *d2graph.Graph, error) { 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 return diagram, g, err
} }

View file

@ -3,16 +3,17 @@ package d2sketch
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
_ "embed" _ "embed"
"github.com/dop251/goja" "github.com/dop251/goja"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/svg" "oss.terrastruct.com/d2/lib/svg"
svg_style "oss.terrastruct.com/d2/lib/svg/style"
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
) )
@ -63,43 +64,26 @@ func DefineFillPattern() string {
</defs>`, fillPattern) </defs>`, 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) { func Rect(r *Runner, shape d2target.Shape) (string, error) {
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
fill: "%s", fill: "#000",
stroke: "%s", stroke: "#000",
strokeWidth: %d, strokeWidth: %d,
%s %s
});`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
paths, err := computeRoughPaths(r, js) paths, err := computeRoughPaths(r, js)
if err != nil { if err != nil {
return "", err return "", err
} }
output := "" 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 { for _, p := range paths {
output += fmt.Sprintf( pathEl.D = p
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`, output += pathEl.Render()
shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape),
)
} }
output += fmt.Sprintf( output += fmt.Sprintf(
`<rect class="sketch-overlay" transform="translate(%d %d)" width="%d" height="%d" />`, `<rect class="sketch-overlay" transform="translate(%d %d)" width="%d" height="%d" />`,
@ -110,21 +94,24 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
func Oval(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, { js := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
fill: "%s", fill: "#000",
stroke: "%s", stroke: "#000",
strokeWidth: %d, strokeWidth: %d,
%s %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) paths, err := computeRoughPaths(r, js)
if err != nil { if err != nil {
return "", err return "", err
} }
output := "" 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 { for _, p := range paths {
output += fmt.Sprintf( pathEl.D = p
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`, output += pathEl.Render()
shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape),
)
} }
output += fmt.Sprintf( output += fmt.Sprintf(
`<ellipse class="sketch-overlay" transform="translate(%d %d)" rx="%d" ry="%d" />`, `<ellipse class="sketch-overlay" transform="translate(%d %d)" rx="%d" ry="%d" />`,
@ -138,20 +125,22 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
output := "" output := ""
for _, path := range paths { for _, path := range paths {
js := fmt.Sprintf(`node = rc.path("%s", { js := fmt.Sprintf(`node = rc.path("%s", {
fill: "%s", fill: "#000",
stroke: "%s", stroke: "#000",
strokeWidth: %d, strokeWidth: %d,
%s %s
});`, path, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) });`, path, shape.StrokeWidth, baseRoughProps)
sketchPaths, err := computeRoughPaths(r, js) sketchPaths, err := computeRoughPaths(r, js)
if err != nil { if err != nil {
return "", err 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 { for _, p := range sketchPaths {
output += fmt.Sprintf( pathEl.D = p
`<path class="shape" d="%s" style="%s" />`, output += pathEl.Render()
p, shapeStyle(shape),
)
} }
for _, p := range sketchPaths { for _, p := range sketchPaths {
output += fmt.Sprintf( output += fmt.Sprintf(
@ -163,20 +152,6 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
return output, nil 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) { func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) {
roughness := 1.0 roughness := 1.0
js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness) 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 return "", err
} }
output := "" 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 { for _, p := range paths {
output += fmt.Sprintf( pathEl.D = p
`<path class="connection" fill="none" d="%s" style="%s" %s/>`, output += pathEl.Render()
p, connectionStyle(connection), attrs,
)
} }
return output, nil 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) { func Table(r *Runner, shape d2target.Shape) (string, error) {
output := "" output := ""
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
fill: "%s", fill: "#000",
stroke: "%s", stroke: "#000",
strokeWidth: %d, strokeWidth: %d,
%s %s
});`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
paths, err := computeRoughPaths(r, js) paths, err := computeRoughPaths(r, js)
if err != nil { if err != nil {
return "", err 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 { for _, p := range paths {
output += fmt.Sprintf( pathEl.D = p
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`, output += pathEl.Render()
shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape),
)
} }
box := geo.NewBox( box := geo.NewBox(
@ -223,18 +205,20 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight) headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight)
js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, { js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, {
fill: "%s", fill: "#000",
%s %s
});`, shape.Width, rowHeight, shape.Fill, baseRoughProps) });`, shape.Width, rowHeight, baseRoughProps)
paths, err = computeRoughPaths(r, js) paths, err = computeRoughPaths(r, js)
if err != nil { if err != nil {
return "", err 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 { for _, p := range paths {
output += fmt.Sprintf( pathEl.D = p
`<path class="class_header" transform="translate(%d %d)" d="%s" style="fill:%s" />`, output += pathEl.Render()
shape.Pos.X, shape.Pos.Y, p, shape.Fill,
)
} }
if shape.Label != "" { if shape.Label != "" {
@ -245,17 +229,16 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
float64(shape.LabelHeight), float64(shape.LabelHeight),
) )
output += fmt.Sprintf(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`, textEl := svg_style.NewThemableElement("text")
"text", textEl.X = tl.X
tl.X, textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4
tl.Y+float64(shape.LabelHeight)*3/4, textEl.Fill = shape.Stroke
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", textEl.Class = "text"
"start", textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx",
4+shape.FontSize, "start", 4+shape.FontSize,
shape.Stroke,
),
svg.EscapeText(shape.Label),
) )
textEl.Content = svg.EscapeText(shape.Label)
output += textEl.Render()
} }
var longestNameWidth int var longestNameWidth int
@ -279,26 +262,26 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
float64(shape.FontSize), float64(shape.FontSize),
) )
output += strings.Join([]string{ textEl := svg_style.NewThemableElement("text")
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`, textEl.X = nameTL.X
nameTL.X, textEl.Y = nameTL.Y + float64(shape.FontSize)*3/4
nameTL.Y+float64(shape.FontSize)*3/4, textEl.Fill = shape.PrimaryAccentColor
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", float64(shape.FontSize), shape.PrimaryAccentColor), textEl.Class = "text"
svg.EscapeText(f.Name.Label), textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", float64(shape.FontSize))
), textEl.Content = svg.EscapeText(f.Name.Label)
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`, output += textEl.Render()
nameTL.X+float64(longestNameWidth)+2*d2target.NamePadding,
nameTL.Y+float64(shape.FontSize)*3/4, textEl.X = nameTL.X + float64(longestNameWidth) + 2*d2target.NamePadding
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", float64(shape.FontSize), shape.NeutralAccentColor), textEl.Fill = shape.NeutralAccentColor
svg.EscapeText(f.Type.Label), textEl.Content = svg.EscapeText(f.Type.Label)
), output += textEl.Render()
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
constraintTR.X, textEl.X = constraintTR.X
constraintTR.Y+float64(shape.FontSize)*3/4, textEl.Y = 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), textEl.Fill = shape.SecondaryAccentColor
f.ConstraintAbbr(), textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx;letter-spacing:2px", "end", float64(shape.FontSize))
), textEl.Content = f.ConstraintAbbr()
}, "\n") output += textEl.Render()
rowBox.TopLeft.Y += rowHeight rowBox.TopLeft.Y += rowHeight
@ -309,11 +292,11 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
pathEl := svg_style.NewThemableElement("path")
pathEl.Fill = shape.Fill
for _, p := range paths { for _, p := range paths {
output += fmt.Sprintf( pathEl.D = p
`<path d="%s" style="fill:%s" />`, output += pathEl.Render()
p, shape.Fill,
)
} }
} }
output += fmt.Sprintf( 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) { func Class(r *Runner, shape d2target.Shape) (string, error) {
output := "" output := ""
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
fill: "%s", fill: "#000",
stroke: "%s", stroke: "#000",
strokeWidth: %d, strokeWidth: %d,
%s %s
});`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps)
paths, err := computeRoughPaths(r, js) paths, err := computeRoughPaths(r, js)
if err != nil { if err != nil {
return "", err 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 { for _, p := range paths {
output += fmt.Sprintf( pathEl.D = p
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`, output += pathEl.Render()
shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape),
)
} }
box := geo.NewBox( 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) headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight)
js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, { js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, {
fill: "%s", fill: "#000",
%s %s
});`, shape.Width, headerBox.Height, shape.Fill, baseRoughProps) });`, shape.Width, headerBox.Height, baseRoughProps)
paths, err = computeRoughPaths(r, js) paths, err = computeRoughPaths(r, js)
if err != nil { if err != nil {
return "", err 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 { for _, p := range paths {
output += fmt.Sprintf( pathEl.D = p
`<path class="class_header" transform="translate(%d %d)" d="%s" style="fill:%s" />`, output += pathEl.Render()
shape.Pos.X, shape.Pos.Y, p, shape.Fill,
)
} }
output += fmt.Sprintf( output += fmt.Sprintf(
@ -379,17 +366,17 @@ func Class(r *Runner, shape d2target.Shape) (string, error) {
float64(shape.LabelHeight), float64(shape.LabelHeight),
) )
output += fmt.Sprintf(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`, textEl := svg_style.NewThemableElement("text")
"text-mono", textEl.X = tl.X + float64(shape.LabelWidth)/2
tl.X+float64(shape.LabelWidth)/2, textEl.Y = tl.Y + float64(shape.LabelHeight)*3/4
tl.Y+float64(shape.LabelHeight)*3/4, textEl.Fill = shape.Stroke
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", textEl.Class = "text-mono"
"middle", textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx",
4+shape.FontSize, "middle",
shape.Stroke, 4+shape.FontSize,
),
svg.EscapeText(shape.Label),
) )
textEl.Content = svg.EscapeText(shape.Label)
output += textEl.Render()
} }
rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight) 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 { if err != nil {
return "", err return "", err
} }
pathEl = svg_style.NewThemableElement("path")
pathEl.Fill = shape.Fill
pathEl.Class = "class_header"
for _, p := range paths { for _, p := range paths {
output += fmt.Sprintf( pathEl.D = p
`<path class="class_header" d="%s" style="fill:%s" />`, output += pathEl.Render()
p, shape.Fill,
)
} }
for _, m := range shape.Methods { for _, m := range shape.Methods {
@ -436,28 +424,27 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str
fontSize, fontSize,
) )
output += strings.Join([]string{ textEl := svg_style.NewThemableElement("text")
fmt.Sprintf(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`, textEl.X = prefixTL.X
prefixTL.X, textEl.Y = prefixTL.Y + fontSize*3/4
prefixTL.Y+fontSize*3/4, textEl.Fill = shape.PrimaryAccentColor
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.PrimaryAccentColor), textEl.Class = "text-mono"
prefix, textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", fontSize)
), textEl.Content = prefix
output += textEl.Render()
fmt.Sprintf(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`, textEl.X = prefixTL.X + d2target.PrefixWidth
prefixTL.X+d2target.PrefixWidth, textEl.Fill = shape.Fill
prefixTL.Y+fontSize*3/4, textEl.Content = svg.EscapeText(nameText)
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.Fill), output += textEl.Render()
svg.EscapeText(nameText),
), 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(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`,
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 return output
} }

View file

@ -313,7 +313,6 @@ func run(t *testing.T, tc testCase) {
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler, Ruler: ruler,
ThemeID: 0,
Layout: d2dagrelayout.DefaultLayout, Layout: d2dagrelayout.DefaultLayout,
FontFamily: go2.Pointer(d2fonts.HandDrawn), FontFamily: go2.Pointer(d2fonts.HandDrawn),
}) })
@ -325,8 +324,9 @@ func run(t *testing.T, tc testCase) {
pathGotSVG := filepath.Join(dataPath, "sketch.got.svg") pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{ svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: d2svg.DEFAULT_PADDING, Pad: d2svg.DEFAULT_PADDING,
Sketch: true, Sketch: true,
ThemeID: 0,
}) })
assert.Success(t, err) assert.Success(t, err)
err = os.MkdirAll(dataPath, 0755) err = os.MkdirAll(dataPath, 0755)

View file

@ -3,17 +3,21 @@ package d2svg
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/svg" "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 { func classHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string {
str := fmt.Sprintf(`<rect class="class_header" x="%f" y="%f" width="%f" height="%f" fill="%s" />`, rectEl := svg_style.NewThemableElement("rect")
box.TopLeft.X, box.TopLeft.Y, box.Width, box.Height, shape.Fill) 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 != "" { if text != "" {
tl := label.InsideMiddleCenter.GetPointOnBox( tl := label.InsideMiddleCenter.GetPointOnBox(
@ -23,17 +27,16 @@ func classHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, tex
textHeight, textHeight,
) )
str += fmt.Sprintf(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`, textEl := svg_style.NewThemableElement("text")
"text-mono", textEl.X = tl.X + textWidth/2
tl.X+textWidth/2, textEl.Y = tl.Y + textHeight*3/4
tl.Y+textHeight*3/4, textEl.Fill = shape.Stroke
fmt.Sprintf(`text-anchor:%s;font-size:%vpx;fill:%s`, textEl.Class = "text-mono"
"middle", textEl.Style = fmt.Sprintf(`text-anchor:%s;font-size:%vpx;`,
4+fontSize, "middle", 4+fontSize,
shape.Stroke,
),
svg.EscapeText(text),
) )
textEl.Content = svg.EscapeText(text)
str += textEl.Render()
} }
return str return str
} }
@ -54,33 +57,39 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str
fontSize, fontSize,
) )
return strings.Join([]string{ textEl := svg_style.NewThemableElement("text")
fmt.Sprintf(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`, textEl.X = prefixTL.X
prefixTL.X, textEl.Y = prefixTL.Y + fontSize*3/4
prefixTL.Y+fontSize*3/4, textEl.Fill = shape.PrimaryAccentColor
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.PrimaryAccentColor), textEl.Class = "text-mono"
prefix, textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", fontSize)
), textEl.Content = prefix
out := textEl.Render()
fmt.Sprintf(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`, textEl.X = prefixTL.X + d2target.PrefixWidth
prefixTL.X+d2target.PrefixWidth, textEl.Fill = shape.Fill
prefixTL.Y+fontSize*3/4, textEl.Content = svg.EscapeText(nameText)
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.Fill), out += textEl.Render()
svg.EscapeText(nameText),
),
fmt.Sprintf(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`, textEl.X = typeTR.X
typeTR.X, textEl.Y = typeTR.Y + fontSize*3/4
typeTR.Y+fontSize*3/4, textEl.Fill = shape.SecondaryAccentColor
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "end", fontSize, shape.SecondaryAccentColor), textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "end", fontSize)
svg.EscapeText(typeText), textEl.Content = svg.EscapeText(typeText)
), out += textEl.Render()
}, "\n")
return out
} }
func drawClass(writer io.Writer, targetShape d2target.Shape) { func drawClass(writer io.Writer, targetShape d2target.Shape) {
fmt.Fprintf(writer, `<rect class="shape" x="%d" y="%d" width="%d" height="%d" style="%s"/>`, el := svg_style.NewThemableElement("rect")
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, shapeStyle(targetShape)) 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( box := geo.NewBox(
geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)), 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 rowBox.TopLeft.Y += rowHeight
} }
fmt.Fprintf(writer, `<line x1="%f" y1="%f" x2="%f" y2="%f" style="%s" />`, lineEl := svg_style.NewThemableElement("line")
rowBox.TopLeft.X, rowBox.TopLeft.Y, lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y
rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, lineEl.X2, lineEl.Y2 = rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y
fmt.Sprintf("stroke-width:1;stroke:%v", targetShape.Fill)) lineEl.Stroke = targetShape.Fill
lineEl.Style = "stroke-width:1"
fmt.Fprint(writer, lineEl.Render())
for _, m := range targetShape.Methods { for _, m := range targetShape.Methods {
fmt.Fprint(writer, fmt.Fprint(writer,

View file

@ -27,12 +27,12 @@ import (
"oss.terrastruct.com/d2/d2renderers/d2latex" "oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2renderers/d2sketch" "oss.terrastruct.com/d2/d2renderers/d2sketch"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/color" "oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape" "oss.terrastruct.com/d2/lib/shape"
"oss.terrastruct.com/d2/lib/svg" "oss.terrastruct.com/d2/lib/svg"
svg_style "oss.terrastruct.com/d2/lib/svg/style"
"oss.terrastruct.com/d2/lib/textmeasure" "oss.terrastruct.com/d2/lib/textmeasure"
) )
@ -68,21 +68,12 @@ type RenderOpts struct {
ThemeID int64 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() tl, br := diagram.BoundingBox()
w := br.X - tl.X + pad*2 w := br.X - tl.X + pad*2
h := br.Y - tl.Y + pad*2 h := br.Y - tl.Y + pad*2
// TODO minify
// TODO background stuff. e.g. dotted, grid, colors return w, h, tl, br
fmt.Fprintf(writer, `<?xml version="1.0" encoding="utf-8"?>
<svg
id="d2-svg"
style="background: %s; color: %s;"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="%d" height="%d" viewBox="%d %d %d %d">`, bgColor, fgColor, w, h, tl.X-pad, tl.Y-pad, w, h)
return w, h
} }
func arrowheadMarkerID(isTarget bool, connection d2target.Connection) string { 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", 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 var path string
switch arrowhead { switch arrowhead {
case d2target.ArrowArrowhead: 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 { if isTarget {
path = fmt.Sprintf(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`, polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
attrs,
0., 0., 0., 0.,
width, height/2, width, height/2,
0., height, 0., height,
width/4, height/2, width/4, height/2,
) )
} else { } else {
path = fmt.Sprintf(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`, polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
attrs,
0., height/2, 0., height/2,
width, 0., width, 0.,
width*3/4, height/2, width*3/4, height/2,
width, height, width, height,
) )
} }
path = polygonEl.Render()
case d2target.TriangleArrowhead: 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 { if isTarget {
path = fmt.Sprintf(`<polygon %s points="%f,%f %f,%f %f,%f" />`, polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
attrs,
0., 0., 0., 0.,
width, height/2.0, width, height/2.0,
0., height, 0., height,
) )
} else { } else {
path = fmt.Sprintf(`<polygon %s points="%f,%f %f,%f %f,%f" />`, polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
attrs,
width, 0., width, 0.,
0., height/2.0, 0., height/2.0,
width, height, width, height,
) )
} }
path = polygonEl.Render()
case d2target.LineArrowhead: 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 { if isTarget {
path = fmt.Sprintf(`<polyline %s points="%f,%f %f,%f %f,%f"/>`, polylineEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
attrs,
strokeWidth/2, strokeWidth/2, strokeWidth/2, strokeWidth/2,
width-strokeWidth/2, height/2, width-strokeWidth/2, height/2,
strokeWidth/2, height-strokeWidth/2, strokeWidth/2, height-strokeWidth/2,
) )
} else { } else {
path = fmt.Sprintf(`<polyline %s points="%f,%f %f,%f %f,%f"/>`, polylineEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
attrs,
width-strokeWidth/2, strokeWidth/2, width-strokeWidth/2, strokeWidth/2,
strokeWidth/2, height/2, strokeWidth/2, height/2,
width-strokeWidth/2, height-strokeWidth/2, width-strokeWidth/2, height-strokeWidth/2,
) )
} }
path = polylineEl.Render()
case d2target.FilledDiamondArrowhead: 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 { if isTarget {
path = fmt.Sprintf(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`, polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
attrs,
0., height/2.0, 0., height/2.0,
width/2.0, 0., width/2.0, 0.,
width, height/2.0, width, height/2.0,
width/2.0, height, width/2.0, height,
) )
} else { } else {
path = fmt.Sprintf(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`, polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
attrs,
0., height/2.0, 0., height/2.0,
width/2.0, 0., width/2.0, 0.,
width, height/2.0, width, height/2.0,
width/2.0, height, width/2.0, height,
) )
} }
path = polygonEl.Render()
case d2target.DiamondArrowhead: 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 { if isTarget {
path = fmt.Sprintf(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`, polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
attrs,
0., height/2.0, 0., height/2.0,
width/2, height/8, width/2, height/8,
width, height/2.0, width, height/2.0,
width/2.0, height*0.9, width/2.0, height*0.9,
) )
} else { } else {
path = fmt.Sprintf(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`, polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
attrs,
width/8, height/2.0, width/8, height/2.0,
width*0.6, height/8, width*0.6, height/8,
width*1.1, height/2.0, width*1.1, height/2.0,
width*0.6, height*7/8, width*0.6, height*7/8,
) )
} }
path = polygonEl.Render()
case d2target.CfOne, d2target.CfMany, d2target.CfOneRequired, d2target.CfManyRequired: 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) offset := 4.0 + float64(connection.StrokeWidth*2)
var modifier string
var modifierEl *svg_style.ThemableElement
if arrowhead == d2target.CfOneRequired || arrowhead == d2target.CfManyRequired { if arrowhead == d2target.CfOneRequired || arrowhead == d2target.CfManyRequired {
modifier = fmt.Sprintf(`<path %s d="M%f,%f %f,%f"/>`, modifierEl := svg_style.NewThemableElement("path")
attrs, modifierEl.D = fmt.Sprintf("M%f,%f %f,%f",
offset, 0., offset, 0.,
offset, height, offset, height,
) )
modifierEl.Fill = bgColor
modifierEl.Stroke = svg_style.ConnectionTheme(connection)
modifierEl.Class = "connection"
modifierEl.Style = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
} else { } else {
modifier = fmt.Sprintf(`<circle %s cx="%f" cy="%f" r="%f"/>`, modifierEl := svg_style.NewThemableElement("circle")
attrs, modifierEl.Cx = offset/2.0 + 1.0
offset/2.0+1.0, height/2.0, modifierEl.Cy = height / 2.0
offset/2.0, modifierEl.R = offset / 2.0
) modifierEl.Fill = bgColor
} modifierEl.Stroke = svg_style.ConnectionTheme(connection)
if !isTarget { modifierEl.Class = "connection"
attrs = fmt.Sprintf(`%s transform="scale(-1) translate(-%f, -%f)"`, attrs, width, height) modifierEl.Style = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
} }
childPathEl := svg_style.NewThemableElement("path")
if arrowhead == d2target.CfMany || arrowhead == d2target.CfManyRequired { if arrowhead == d2target.CfMany || arrowhead == d2target.CfManyRequired {
path = fmt.Sprintf(`<g %s>%s<path d="M%f,%f %f,%f M%f,%f %f,%f M%f,%f %f,%f"/></g>`, childPathEl.D = fmt.Sprintf("M%f,%f %f,%f M%f,%f %f,%f M%f,%f %f,%f",
attrs, modifier,
width-3.0, height/2.0, width-3.0, height/2.0,
width+offset, height/2.0, width+offset, height/2.0,
offset+2.0, 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, width+offset, height,
) )
} else { } else {
path = fmt.Sprintf(`<g %s>%s<path d="M%f,%f %f,%f M%f,%f %f,%f"/></g>`, childPathEl.D = fmt.Sprintf("M%f,%f %f,%f M%f,%f %f,%f",
attrs, modifier,
width-3.0, height/2.0, width-3.0, height/2.0,
width+offset, height/2.0, width+offset, height/2.0,
offset*1.8, 0., offset*1.8, 0.,
offset*1.8, height, 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: default:
return "" return ""
} }
@ -462,10 +482,16 @@ func drawConnection(writer io.Writer, bgColor string, fgColor string, labelMaskI
if err != nil { if err != nil {
return "", err return "", err
} }
fmt.Fprintf(writer, out) fmt.Fprint(writer, out)
} else { } else {
fmt.Fprintf(writer, `<path d="%s" class="connection" style="fill:none;%s" %s/>`, pathEl := svg_style.NewThemableElement("path")
path, connectionStyle(connection), attrs) 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 != "" { if connection.Label != "" {
@ -475,24 +501,27 @@ func drawConnection(writer io.Writer, bgColor string, fgColor string, labelMaskI
} else if connection.Italic { } else if connection.Italic {
fontClass += "-italic" fontClass += "-italic"
} }
fontColor := "black" fontColor := color.N1
if connection.Color != "" { if connection.Color != color.Empty {
fontColor = connection.Color fontColor = connection.Color
} }
if connection.Fill != "" { if connection.Fill != color.Empty {
fmt.Fprintf(writer, `<rect x="%f" y="%f" width="%d" height="%d" style="fill:%s" />`, rectEl := svg_style.NewThemableElement("rect")
labelTL.X, labelTL.Y, connection.LabelWidth, connection.LabelHeight, connection.Fill) 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 textEl := svg_style.NewThemableElement("text")
y := labelTL.Y + float64(connection.FontSize) textEl.X = labelTL.X + float64(connection.LabelWidth)/2
fmt.Fprintf(writer, `<text class="%s" x="%f" y="%f" style="%s">%s</text>`, textEl.Y = labelTL.Y + float64(connection.FontSize)
fontClass, textEl.Fill = fontColor
x, y, textEl.Class = fontClass
textStyle, textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", connection.FontSize)
RenderText(connection.Label, x, float64(connection.LabelHeight)), textEl.Content = RenderText(connection.Label, textEl.X, float64(connection.LabelHeight))
) fmt.Fprint(writer, textEl.Render())
} }
length := geo.Route(connection.Route).Length() 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 { 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) 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) textEl := svg_style.NewThemableElement("text")
x := labelTL.X + width/2 textEl.X = labelTL.X + width/2
y := labelTL.Y + float64(connection.FontSize) textEl.Y = labelTL.Y + float64(connection.FontSize)
return fmt.Sprintf(`<text class="text-italic" x="%f" y="%f" style="%s">%s</text>`, textEl.Fill = fgColor
x, y, textEl.Class = "text-italic"
textStyle, textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", connection.FontSize)
RenderText(text, x, height), textEl.Content = RenderText(text, textEl.X, height)
) return textEl.Render()
} }
func renderOval(tl *geo.Point, width, height float64, style string) string { func renderOval(tl *geo.Point, width, height float64, fill, stroke, style string) string {
rx := width / 2 el := svg_style.NewThemableElement("ellipse")
ry := height / 2 el.Rx = width / 2
cx := tl.X + rx el.Ry = height / 2
cy := tl.Y + ry el.Cx = tl.X + el.Rx
return fmt.Sprintf(`<ellipse class="shape" cx="%f" cy="%f" rx="%f" ry="%f" style="%s" />`, cx, cy, rx, ry, style) el.Cy = tl.Y + el.Ry
el.Class = "shape"
el.Style = style
return el.Render()
} }
func defineShadowFilter(writer io.Writer) { func defineShadowFilter(writer io.Writer) {
@ -583,11 +615,13 @@ func render3dRect(targetShape d2target.Shape) string {
borderSegments = append(borderSegments, borderSegments = append(borderSegments,
lineTo(d2target.Point{X: targetShape.Width + threeDeeOffset, Y: -threeDeeOffset}), lineTo(d2target.Point{X: targetShape.Width + threeDeeOffset, Y: -threeDeeOffset}),
) )
border := targetShape border := svg_style.NewThemableElement("path")
border.Fill = "none" border.D = strings.Join(borderSegments, " ")
borderStyle := shapeStyle(border) _, borderStroke := svg_style.ShapeTheme(targetShape)
renderedBorder := fmt.Sprintf(`<path d="%s" style="%s"/>`, border.Stroke = borderStroke
strings.Join(borderSegments, " "), borderStyle) borderStyle := svg_style.ShapeStyle(targetShape)
border.Style = borderStyle
renderedBorder := border.Render()
// create mask from border stroke, to cut away from the shape fills // create mask from border stroke, to cut away from the shape fills
maskID := fmt.Sprintf("border-mask-%v", svg.EscapeText(targetShape.ID)) maskID := fmt.Sprintf("border-mask-%v", svg.EscapeText(targetShape.ID))
@ -603,11 +637,16 @@ func render3dRect(targetShape d2target.Shape) string {
}, "\n") }, "\n")
// render the main rectangle without stroke and the border mask // render the main rectangle without stroke and the border mask
mainShape := targetShape mainShape := svg_style.NewThemableElement("rect")
mainShape.Stroke = "none" mainShape.X = float64(targetShape.Pos.X)
mainRect := fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" style="%s" mask="url(#%s)"/>`, mainShape.Y = float64(targetShape.Pos.Y)
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, shapeStyle(mainShape), maskID, 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 // render the side shapes in the darkened color without stroke and the border mask
var sidePoints []string 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), fmt.Sprintf("%d,%d", v.X+targetShape.Pos.X, v.Y+targetShape.Pos.Y),
) )
} }
darkerColor, err := color.Darken(targetShape.Fill) // TODO make darker color part of the theme?
if err != nil { darkerColor := targetShape.Fill
darkerColor = targetShape.Fill // darkerColor, err := color.Darken(targetShape.Fill)
} // if err != nil {
sideShape := targetShape // darkerColor = targetShape.Fill
// }
sideShape := svg_style.NewThemableElement("polygon")
sideShape.Fill = darkerColor sideShape.Fill = darkerColor
sideShape.Stroke = "none" sideShape.Points = strings.Join(sidePoints, " ")
renderedSides := fmt.Sprintf(`<polygon points="%s" style="%s" mask="url(#%s)"/>`, sideShape.Mask = fmt.Sprintf("url(#%s)", maskID)
strings.Join(sidePoints, " "), shapeStyle(sideShape), 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) { 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)) tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y))
width := float64(targetShape.Width) width := float64(targetShape.Width)
height := float64(targetShape.Height) 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] shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[targetShape.Type]
s := shape.NewShape(shapeType, geo.NewBox(tl, width, height)) 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 { if err != nil {
return "", err return "", err
} }
fmt.Fprintf(writer, out) fmt.Fprint(writer, out)
} else { } else {
drawClass(writer, targetShape) drawClass(writer, targetShape)
} }
fmt.Fprintf(writer, `</g>`) fmt.Fprintf(writer, `</g>`)
fmt.Fprintf(writer, closingTag) fmt.Fprint(writer, closingTag)
return labelMask, nil return labelMask, nil
case d2target.ShapeSQLTable: case d2target.ShapeSQLTable:
if sketchRunner != nil { if sketchRunner != nil {
@ -695,31 +738,38 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
if err != nil { if err != nil {
return "", err return "", err
} }
fmt.Fprintf(writer, out) fmt.Fprint(writer, out)
} else { } else {
drawTable(writer, targetShape) drawTable(writer, targetShape)
} }
fmt.Fprintf(writer, `</g>`) fmt.Fprintf(writer, `</g>`)
fmt.Fprintf(writer, closingTag) fmt.Fprint(writer, closingTag)
return labelMask, nil return labelMask, nil
case d2target.ShapeOval: case d2target.ShapeOval:
if targetShape.Multiple { if targetShape.Multiple {
fmt.Fprint(writer, renderOval(multipleTL, width, height, style)) fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, stroke, style))
} }
if sketchRunner != nil { if sketchRunner != nil {
out, err := d2sketch.Oval(sketchRunner, targetShape) out, err := d2sketch.Oval(sketchRunner, targetShape)
if err != nil { if err != nil {
return "", err return "", err
} }
fmt.Fprintf(writer, out) fmt.Fprint(writer, out)
} else { } else {
fmt.Fprint(writer, renderOval(tl, width, height, style)) fmt.Fprint(writer, renderOval(tl, width, height, fill, stroke, style))
} }
case d2target.ShapeImage: case d2target.ShapeImage:
fmt.Fprintf(writer, `<image href="%s" x="%d" y="%d" width="%d" height="%d" style="%s" />`, el := svg_style.NewThemableElement("image")
html.EscapeString(targetShape.Icon.String()), el.X = float64(targetShape.Pos.X)
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, style) 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 // TODO should standardize "" to rectangle
case d2target.ShapeRectangle, d2target.ShapeSequenceDiagram, "": case d2target.ShapeRectangle, d2target.ShapeSequenceDiagram, "":
@ -727,26 +777,45 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
fmt.Fprint(writer, render3dRect(targetShape)) fmt.Fprint(writer, render3dRect(targetShape))
} else { } else {
if targetShape.Multiple { if targetShape.Multiple {
fmt.Fprintf(writer, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`, el := svg_style.NewThemableElement("rect")
targetShape.Pos.X+10, targetShape.Pos.Y-10, targetShape.Width, targetShape.Height, style) 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 { if sketchRunner != nil {
out, err := d2sketch.Rect(sketchRunner, targetShape) out, err := d2sketch.Rect(sketchRunner, targetShape)
if err != nil { if err != nil {
return "", err return "", err
} }
fmt.Fprintf(writer, out) fmt.Fprint(writer, out)
} else { } else {
fmt.Fprintf(writer, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`, el := svg_style.NewThemableElement("rect")
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, style) 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: case d2target.ShapeText, d2target.ShapeCode:
default: default:
if targetShape.Multiple { if targetShape.Multiple {
multiplePathData := shape.NewShape(shapeType, geo.NewBox(multipleTL, width, height)).GetSVGPathData() 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 { for _, pathData := range multiplePathData {
fmt.Fprintf(writer, `<path d="%s" style="%s"/>`, 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 { if err != nil {
return "", err return "", err
} }
fmt.Fprintf(writer, out) fmt.Fprint(writer, out)
} else { } else {
el := svg_style.NewThemableElement("path")
el.Fill = fill
el.Stroke = stroke
el.Style = style
for _, pathData := range s.GetSVGPathData() { for _, pathData := range s.GetSVGPathData() {
fmt.Fprintf(writer, `<path d="%s" style="%s"/>`, 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) svgStyles := styleToSVG(style)
containerStyle := fmt.Sprintf(`stroke: %s;fill:%s`, targetShape.Stroke, style.Get(chroma.Background).Background.String())
fmt.Fprintf(writer, `<g transform="translate(%f %f)" style="opacity:%f">`, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity) fmt.Fprintf(writer, `<g transform="translate(%f %f)" style="opacity:%f">`, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity)
fmt.Fprintf(writer, `<rect class="shape" width="%d" height="%d" style="%s" />`, rectEl := svg_style.NewThemableElement("rect")
targetShape.Width, targetShape.Height, containerStyle) 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 // Padding
fmt.Fprintf(writer, `<g transform="translate(6 6)">`) fmt.Fprintf(writer, `<g transform="translate(6 6)">`)
@ -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 // we need the self closing form in this svg/xhtml context
render = strings.ReplaceAll(render, "<hr>", "<hr />") render = strings.ReplaceAll(render, "<hr>", "<hr />")
var mdStyle string mdEl := svg_style.NewThemableElement("div")
if targetShape.Fill != "" { mdEl.Xmlns = "http://www.w3.org/1999/xhtml"
mdStyle = fmt.Sprintf("background-color:%s;", targetShape.Fill) mdEl.Class = "md"
mdEl.Content = render
if targetShape.Fill != color.Empty {
mdEl.BackgroundColor = targetShape.Fill
} }
if targetShape.Stroke != "" { if targetShape.Stroke != color.Empty {
mdStyle += fmt.Sprintf("color:%s;", targetShape.Stroke) mdEl.Color = targetShape.Stroke
} }
fmt.Fprint(writer, mdEl.Render())
fmt.Fprintf(writer, `<div xmlns="http://www.w3.org/1999/xhtml" class="md" style="%s">%v</div>`, mdStyle, render)
fmt.Fprint(writer, `</foreignObject></g>`) fmt.Fprint(writer, `</foreignObject></g>`)
} else { } else {
fontColor := "black" fontColor := color.N1
if targetShape.Color != "" { if targetShape.Color != color.Empty {
fontColor = targetShape.Color fontColor = targetShape.Color
} }
textStyle := fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "middle", targetShape.FontSize, fontColor) textEl := svg_style.NewThemableElement("text")
x := labelTL.X + float64(targetShape.LabelWidth)/2. textEl.X = labelTL.X + float64(targetShape.LabelWidth)/2
// text is vertically positioned at its baseline which is at labelTL+FontSize // text is vertically positioned at its baseline which is at labelTL+FontSize
y := labelTL.Y + float64(targetShape.FontSize) textEl.Y = labelTL.Y + float64(targetShape.FontSize)
fmt.Fprintf(writer, `<text class="%s" x="%f" y="%f" style="%s">%s</text>`, textEl.Fill = fontColor
fontClass, textEl.Class = fontClass
x, y, textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", targetShape.FontSize)
textStyle, textEl.Content = RenderText(targetShape.Label, textEl.X, float64(targetShape.LabelHeight))
RenderText(targetShape.Label, x, float64(targetShape.LabelHeight)), fmt.Fprint(writer, textEl.Render())
)
if targetShape.Blend { if targetShape.Blend {
labelMask = makeLabelMask(labelTL, targetShape.LabelWidth, targetShape.LabelHeight-d2graph.INNER_LABEL_PADDING) 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 return labelMask, nil
} }
@ -942,46 +1020,9 @@ func RenderText(text string, x, height float64) string {
return strings.Join(rendered, "") return strings.Join(rendered, "")
} }
func shapeStyle(shape d2target.Shape) string { func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) 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) {
content := buf.String() content := buf.String()
buf.WriteString(`<style type="text/css"><![CDATA[`) out := `<style type="text/css"><![CDATA[`
triggers := []string{ triggers := []string{
`class="text"`, `class="text"`,
@ -991,7 +1032,7 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
for _, t := range triggers { for _, t := range triggers {
if strings.Contains(content, t) { if strings.Contains(content, t) {
fmt.Fprintf(buf, ` out += fmt.Sprintf(`
.text { .text {
font-family: "font-regular"; font-family: "font-regular";
} }
@ -1010,10 +1051,10 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
for _, t := range triggers { for _, t := range triggers {
if strings.Contains(content, t) { if strings.Contains(content, t) {
buf.WriteString(` out += `
.text-underline { .text-underline {
text-decoration: underline; text-decoration: underline;
}`) }`
break break
} }
} }
@ -1024,23 +1065,23 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
for _, t := range triggers { for _, t := range triggers {
if strings.Contains(content, t) { if strings.Contains(content, t) {
buf.WriteString(` out += `
.appendix-icon { .appendix-icon {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1)); filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
}`) }`
break break
} }
} }
triggers = []string{ triggers = []string{
`class="text-bold"`, `class="text-bold`,
`<b>`, `<b>`,
`<strong>`, `<strong>`,
} }
for _, t := range triggers { for _, t := range triggers {
if strings.Contains(content, t) { if strings.Contains(content, t) {
fmt.Fprintf(buf, ` out += fmt.Sprintf(`
.text-bold { .text-bold {
font-family: "font-bold"; font-family: "font-bold";
} }
@ -1054,14 +1095,14 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
} }
triggers = []string{ triggers = []string{
`class="text-italic"`, `class="text-italic`,
`<em>`, `<em>`,
`<dfn>`, `<dfn>`,
} }
for _, t := range triggers { for _, t := range triggers {
if strings.Contains(content, t) { if strings.Contains(content, t) {
fmt.Fprintf(buf, ` out += fmt.Sprintf(`
.text-italic { .text-italic {
font-family: "font-italic"; font-family: "font-italic";
} }
@ -1075,7 +1116,7 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
} }
triggers = []string{ triggers = []string{
`class="text-mono"`, `class="text-mono`,
`<pre>`, `<pre>`,
`<code>`, `<code>`,
`<kbd>`, `<kbd>`,
@ -1084,7 +1125,7 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
for _, t := range triggers { for _, t := range triggers {
if strings.Contains(content, t) { if strings.Contains(content, t) {
fmt.Fprintf(buf, ` out += fmt.Sprintf(`
.text-mono { .text-mono {
font-family: "font-mono"; font-family: "font-mono";
} }
@ -1097,12 +1138,18 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
} }
} }
buf.WriteString(`]]></style>`) out += `]]></style>`
return out
} }
//go:embed fitToScreen.js //go:embed fitToScreen.js
var fitToScreenScript string var fitToScreenScript string
const (
BG_COLOR = color.N7
FG_COLOR = color.N1
)
// TODO minify output at end // TODO minify output at end
func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
var sketchRunner *d2sketch.Runner 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{} buf := &bytes.Buffer{}
w, h := setViewbox(buf, diagram, pad, bgColor, fgColor)
styleCSS2 := ""
if sketchRunner != nil {
styleCSS2 = "\n" + sketchStyleCSS
}
buf.WriteString(fmt.Sprintf(`<style type="text/css"><![CDATA[%s%s]]></style>`, 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(`<script type="application/javascript"><![CDATA[%s]]></script>`, fitToScreenScript))
hasMarkdown := false
for _, s := range diagram.Shapes {
if s.Label != "" && s.Type == d2target.ShapeText {
hasMarkdown = true
break
}
}
if hasMarkdown {
fmt.Fprintf(buf, `<style type="text/css">%s</style>`, mdCSS)
}
if sketchRunner != nil && sketchBg {
fmt.Fprint(buf, d2sketch.DefineFillPattern())
}
// only define shadow filter if a shape uses it // only define shadow filter if a shape uses it
for _, s := range diagram.Shapes { for _, s := range diagram.Shapes {
@ -1184,7 +1202,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
markers := map[string]struct{}{} markers := map[string]struct{}{}
for _, obj := range allObjects { for _, obj := range allObjects {
if c, is := obj.(d2target.Connection); is { 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 { if err != nil {
return nil, err 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 // 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.Fprint(buf, strings.Join([]string{
fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`, fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
labelMaskID, -pad, -pad, w, h, labelMaskID, -pad, -pad, w, h,
@ -1215,10 +1234,48 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
`</mask>`, `</mask>`,
}, "\n")) }, "\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(`</svg>`) // generate elements that will be appended to the SVG tag
return buf.Bytes(), nil styleCSS2 := ""
if sketchRunner != nil {
styleCSS2 = "\n" + sketchStyleCSS
}
svgOut := fmt.Sprintf(`<style type="text/css"><![CDATA[%s%s]]></style>`, 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(`<script type="application/javascript"><![CDATA[%s]]></script>`, fitToScreenScript)
hasMarkdown := false
for _, s := range diagram.Shapes {
if s.Label != "" && s.Type == d2target.ShapeText {
hasMarkdown = true
break
}
}
if hasMarkdown {
svgOut += fmt.Sprintf(`<style type="text/css">%s</style>`, mdCSS)
}
if sketchRunner != nil && sketchBg {
svgOut += d2sketch.DefineFillPattern()
}
svgOut += embedFonts(buf, diagram.FontFamily)
// render the document
docRendered := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?><svg id="d2-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s</svg>`,
w, h, tl.X-pad, tl.Y-pad, w, h,
svgOut,
containerEl.Render(),
buf.String(),
)
return []byte(docRendered), nil
} }
type DiagramObject interface { type DiagramObject interface {

View file

@ -7,6 +7,456 @@
stroke-linejoin: round; stroke-linejoin: round;
} }
.blend { .blend {
mix-blend-mode: multiply; mix-Blend-mode: multiply;
opacity: 0.5; 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;
}
}

View file

@ -3,18 +3,22 @@ package d2svg
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/svg" "oss.terrastruct.com/d2/lib/svg"
svg_style "oss.terrastruct.com/d2/lib/svg/style"
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
) )
func tableHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string { func tableHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64) string {
str := fmt.Sprintf(`<rect class="class_header" x="%f" y="%f" width="%f" height="%f" fill="%s" />`, rectEl := svg_style.NewThemableElement("rect")
box.TopLeft.X, box.TopLeft.Y, box.Width, box.Height, shape.Fill) 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 != "" { if text != "" {
tl := label.InsideMiddleLeft.GetPointOnBox( tl := label.InsideMiddleLeft.GetPointOnBox(
@ -24,17 +28,16 @@ func tableHeader(shape d2target.Shape, box *geo.Box, text string, textWidth, tex
textHeight, textHeight,
) )
str += fmt.Sprintf(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`, textEl := svg_style.NewThemableElement("text")
"text", textEl.X = tl.X
tl.X, textEl.Y = tl.Y + textHeight*3/4
tl.Y+textHeight*3/4, textEl.Fill = shape.Stroke
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", textEl.Class = "text"
"start", textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx",
4+fontSize, "start", 4+fontSize,
shape.Stroke,
),
svg.EscapeText(text),
) )
textEl.Content = svg.EscapeText(text)
str += textEl.Render()
} }
return str return str
} }
@ -55,33 +58,40 @@ func tableRow(shape d2target.Shape, box *geo.Box, nameText, typeText, constraint
fontSize, fontSize,
) )
return strings.Join([]string{ textEl := svg_style.NewThemableElement("text")
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`, textEl.X = nameTL.X
nameTL.X, textEl.Y = nameTL.Y + fontSize*3/4
nameTL.Y+fontSize*3/4, textEl.Fill = shape.PrimaryAccentColor
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.PrimaryAccentColor), textEl.Class = "text"
svg.EscapeText(nameText), textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "start", fontSize)
), textEl.Content = svg.EscapeText(nameText)
out := textEl.Render()
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`, textEl.X = nameTL.X + longestNameWidth + 2*d2target.NamePadding
nameTL.X+longestNameWidth+2*d2target.NamePadding, textEl.Fill = shape.NeutralAccentColor
nameTL.Y+fontSize*3/4, textEl.Content = svg.EscapeText(typeText)
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, shape.NeutralAccentColor), out += textEl.Render()
svg.EscapeText(typeText),
),
fmt.Sprintf(`<text class="text" x="%f" y="%f" style="%s">%s</text>`, textEl.X = constraintTR.X
constraintTR.X, textEl.Y = constraintTR.Y + fontSize*3/4
constraintTR.Y+fontSize*3/4, textEl.Fill = shape.SecondaryAccentColor
fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s;letter-spacing:2px;", "end", fontSize, shape.SecondaryAccentColor), textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx;letter-spacing:2px", "end", fontSize)
constraintText, textEl.Content = constraintText
), out += textEl.Render()
}, "\n")
return out
} }
func drawTable(writer io.Writer, targetShape d2target.Shape) { func drawTable(writer io.Writer, targetShape d2target.Shape) {
fmt.Fprintf(writer, `<rect class="shape" x="%d" y="%d" width="%d" height="%d" style="%s"/>`, rectEl := svg_style.NewThemableElement("rect")
targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, shapeStyle(targetShape)) 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( box := geo.NewBox(
geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)), 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)), tableRow(targetShape, rowBox, f.Name.Label, f.Type.Label, f.ConstraintAbbr(), float64(targetShape.FontSize), float64(longestNameWidth)),
) )
rowBox.TopLeft.Y += rowHeight rowBox.TopLeft.Y += rowHeight
fmt.Fprintf(writer, `<line x1="%f" y1="%f" x2="%f" y2="%f" style="stroke-width:2;stroke:%s" />`,
rowBox.TopLeft.X, rowBox.TopLeft.Y, lineEl := svg_style.NewThemableElement("line")
rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, lineEl.X1, lineEl.Y1 = rowBox.TopLeft.X, rowBox.TopLeft.Y
targetShape.Fill, 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())
} }
} }

View file

@ -11,7 +11,7 @@ import (
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2renderers/d2fonts" "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/geo"
"oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape" "oss.terrastruct.com/d2/lib/shape"
@ -416,11 +416,11 @@ func NewTextDimensions(w, h int) *TextDimensions {
return &TextDimensions{Width: w, Height: h} 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 { 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{ var DSL_SHAPE_TO_SHAPE_TYPE = map[string]string{

View file

@ -14,3 +14,34 @@ func Darken(colorString string) (string, error) {
// decrease luminance by 10% // decrease luminance by 10%
return colorful.Hsl(h, s, l-.1).Clamped().Hex(), nil 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"
)

View file

@ -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</%s>", out, el.Content, el.tag)
}
return out + " />"
}

View file

@ -238,9 +238,8 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
layout := plugin.Layout layout := plugin.Layout
opts := &d2lib.CompileOptions{ opts := &d2lib.CompileOptions{
Layout: layout, Layout: layout,
Ruler: ruler, Ruler: ruler,
ThemeID: themeID,
} }
if sketch { if sketch {
opts.FontFamily = go2.Pointer(d2fonts.HandDrawn) opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)