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

View file

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

View file

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

View file

@ -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 {
</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) {
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(
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape),
)
pathEl.D = p
output += pathEl.Render()
}
output += fmt.Sprintf(
`<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) {
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(
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape),
)
pathEl.D = p
output += pathEl.Render()
}
output += fmt.Sprintf(
`<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 := ""
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(
`<path class="shape" d="%s" style="%s" />`,
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(
`<path class="connection" fill="none" d="%s" style="%s" %s/>`,
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(
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
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(
`<path class="class_header" transform="translate(%d %d)" d="%s" style="fill:%s" />`,
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(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`,
"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(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
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(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
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(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
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(
`<path d="%s" style="fill:%s" />`,
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(
`<path class="shape" transform="translate(%d %d)" d="%s" style="%s" />`,
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(
`<path class="class_header" transform="translate(%d %d)" d="%s" style="fill:%s" />`,
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(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`,
"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(
`<path class="class_header" d="%s" style="fill:%s" />`,
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(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`,
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(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`,
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(`<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
}

View file

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

View file

@ -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(`<rect class="class_header" x="%f" y="%f" width="%f" height="%f" fill="%s" />`,
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(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`,
"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(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`,
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(`<text class="text-mono" x="%f" y="%f" style="%s">%s</text>`,
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(`<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")
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, `<rect class="shape" x="%d" y="%d" width="%d" height="%d" style="%s"/>`,
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, `<line x1="%f" y1="%f" x2="%f" y2="%f" style="%s" />`,
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,

View file

@ -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, `<?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
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(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`,
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(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`,
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(`<polygon %s points="%f,%f %f,%f %f,%f" />`,
attrs,
polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
0., 0.,
width, height/2.0,
0., height,
)
} else {
path = fmt.Sprintf(`<polygon %s points="%f,%f %f,%f %f,%f" />`,
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(`<polyline %s points="%f,%f %f,%f %f,%f"/>`,
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(`<polyline %s points="%f,%f %f,%f %f,%f"/>`,
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(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`,
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(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`,
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(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`,
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(`<polygon %s points="%f,%f %f,%f %f,%f %f,%f" />`,
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(`<path %s d="M%f,%f %f,%f"/>`,
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(`<circle %s cx="%f" cy="%f" r="%f"/>`,
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(`<g %s>%s<path d="M%f,%f %f,%f M%f,%f %f,%f M%f,%f %f,%f"/></g>`,
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(`<g %s>%s<path d="M%f,%f %f,%f M%f,%f %f,%f"/></g>`,
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 d="%s" class="connection" style="fill:none;%s" %s/>`,
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, `<rect x="%f" y="%f" width="%d" height="%d" style="fill:%s" />`,
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, `<text class="%s" x="%f" y="%f" style="%s">%s</text>`,
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(`<text class="text-italic" x="%f" y="%f" style="%s">%s</text>`,
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(`<ellipse class="shape" cx="%f" cy="%f" rx="%f" ry="%f" style="%s" />`, 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(`<path d="%s" style="%s"/>`,
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(`<rect x="%d" y="%d" width="%d" height="%d" style="%s" mask="url(#%s)"/>`,
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(`<polygon points="%s" style="%s" mask="url(#%s)"/>`,
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, `</g>`)
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, `</g>`)
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, `<image href="%s" x="%d" y="%d" width="%d" height="%d" style="%s" />`,
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, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
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, `<rect x="%d" y="%d" width="%d" height="%d" style="%s" />`,
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, `<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 {
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, `<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)
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, `<rect class="shape" width="%d" height="%d" style="%s" />`,
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, `<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
render = strings.ReplaceAll(render, "<hr>", "<hr />")
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, `<div xmlns="http://www.w3.org/1999/xhtml" class="md" style="%s">%v</div>`, mdStyle, render)
fmt.Fprint(writer, mdEl.Render())
fmt.Fprint(writer, `</foreignObject></g>`)
} 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, `<text class="%s" x="%f" y="%f" style="%s">%s</text>`,
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(`<style type="text/css"><![CDATA[`)
out := `<style type="text/css"><![CDATA[`
triggers := []string{
`class="text"`,
@ -991,7 +1032,7 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
for _, t := range triggers {
if strings.Contains(content, t) {
fmt.Fprintf(buf, `
out += fmt.Sprintf(`
.text {
font-family: "font-regular";
}
@ -1010,10 +1051,10 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
for _, t := range triggers {
if strings.Contains(content, t) {
buf.WriteString(`
out += `
.text-underline {
text-decoration: underline;
}`)
}`
break
}
}
@ -1024,23 +1065,23 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
for _, t := range triggers {
if strings.Contains(content, t) {
buf.WriteString(`
out += `
.appendix-icon {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
}`)
}`
break
}
}
triggers = []string{
`class="text-bold"`,
`class="text-bold`,
`<b>`,
`<strong>`,
}
for _, t := range triggers {
if strings.Contains(content, t) {
fmt.Fprintf(buf, `
out += fmt.Sprintf(`
.text-bold {
font-family: "font-bold";
}
@ -1054,14 +1095,14 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
}
triggers = []string{
`class="text-italic"`,
`class="text-italic`,
`<em>`,
`<dfn>`,
}
for _, t := range triggers {
if strings.Contains(content, t) {
fmt.Fprintf(buf, `
out += fmt.Sprintf(`
.text-italic {
font-family: "font-italic";
}
@ -1075,7 +1116,7 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
}
triggers = []string{
`class="text-mono"`,
`class="text-mono`,
`<pre>`,
`<code>`,
`<kbd>`,
@ -1084,7 +1125,7 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) {
for _, t := range triggers {
if strings.Contains(content, t) {
fmt.Fprintf(buf, `
out += fmt.Sprintf(`
.text-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
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(`<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
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(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
labelMaskID, -pad, -pad, w, h,
@ -1215,10 +1234,48 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
`</mask>`,
}, "\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>`)
return buf.Bytes(), nil
// generate elements that will be appended to the SVG tag
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 {

View file

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

View file

@ -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(`<rect class="class_header" x="%f" y="%f" width="%f" height="%f" fill="%s" />`,
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(`<text class="%s" x="%f" y="%f" style="%s">%s</text>`,
"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(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
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(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
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(`<text class="text" x="%f" y="%f" style="%s">%s</text>`,
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, `<rect class="shape" x="%d" y="%d" width="%d" height="%d" style="%s"/>`,
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, `<line x1="%f" y1="%f" x2="%f" y2="%f" style="stroke-width:2;stroke:%s" />`,
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())
}
}

View file

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

View file

@ -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"
)

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
opts := &d2lib.CompileOptions{
Layout: layout,
Ruler: ruler,
ThemeID: themeID,
Layout: layout,
Ruler: ruler,
}
if sketch {
opts.FontFamily = go2.Pointer(d2fonts.HandDrawn)