d2/d2renderers/d2svg/d2svg.go

2966 lines
91 KiB
Go
Raw Normal View History

// d2svg implements an SVG renderer for d2 diagrams.
// The input is d2exporter's output
package d2svg
import (
"bytes"
_ "embed"
"encoding/base64"
"errors"
"fmt"
"hash/fnv"
2022-12-24 20:45:12 +00:00
"html"
"io"
"sort"
"strings"
"math"
2023-03-04 04:08:13 +00:00
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
2024-09-15 16:43:10 +00:00
"oss.terrastruct.com/d2/d2ast"
2022-12-05 20:48:03 +00:00
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
2022-11-27 18:11:14 +00:00
"oss.terrastruct.com/d2/d2renderers/d2latex"
2022-12-21 07:43:45 +00:00
"oss.terrastruct.com/d2/d2renderers/d2sketch"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
2022-11-10 19:21:14 +00:00
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo"
2025-01-15 23:30:17 +00:00
"oss.terrastruct.com/d2/lib/jsrunner"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape"
2022-12-21 07:43:45 +00:00
"oss.terrastruct.com/d2/lib/svg"
"oss.terrastruct.com/d2/lib/textmeasure"
2023-03-11 16:34:31 +00:00
"oss.terrastruct.com/d2/lib/version"
2024-10-09 18:09:46 +00:00
"oss.terrastruct.com/util-go/go2"
)
const (
2023-04-17 19:06:17 +00:00
DEFAULT_PADDING = 100
2022-12-27 07:56:23 +00:00
2022-12-29 00:19:30 +00:00
appendixIconRadius = 16
2025-03-11 21:30:05 +00:00
// Legend constants
LEGEND_PADDING = 20
LEGEND_ITEM_SPACING = 15
LEGEND_ICON_SIZE = 24
LEGEND_FONT_SIZE = 14
LEGEND_CORNER_PADDING = 10
)
var multipleOffset = geo.NewVector(d2target.MULTIPLE_OFFSET, -d2target.MULTIPLE_OFFSET)
2023-01-19 07:12:26 +00:00
2022-12-27 07:56:23 +00:00
//go:embed tooltip.svg
2022-12-28 20:07:01 +00:00
var TooltipIcon string
2022-12-27 07:56:23 +00:00
2022-12-29 00:19:30 +00:00
//go:embed link.svg
var LinkIcon string
2022-11-12 18:29:21 +00:00
//go:embed style.css
2023-03-23 20:37:28 +00:00
var BaseStylesheet string
2022-11-12 18:29:21 +00:00
//go:embed github-markdown.css
2023-03-23 20:37:28 +00:00
var MarkdownCSS string
2023-03-14 06:01:33 +00:00
//go:embed dots.txt
var dots string
2023-03-16 05:53:12 +00:00
//go:embed lines.txt
var lines string
//go:embed grain.txt
var grain string
2023-03-18 22:43:28 +00:00
//go:embed paper.txt
var paper string
2022-12-21 07:43:45 +00:00
type RenderOpts struct {
2023-12-13 20:17:22 +00:00
Pad *int64
Sketch *bool
Center *bool
ThemeID *int64
DarkThemeID *int64
ThemeOverrides *d2target.ThemeOverrides
DarkThemeOverrides *d2target.ThemeOverrides
Font string
// the svg will be scaled by this factor, if unset the svg will fit to screen
Scale *float64
2023-03-23 20:37:28 +00:00
// MasterID is passed when the diagram should use something other than its own hash for unique targeting
// Currently, that's when multi-boards are collapsed
2025-04-02 19:57:06 +00:00
MasterID string
NoXMLTag *bool
Salt *string
OmitVersion *bool
2022-12-21 07:43:45 +00:00
}
2023-02-19 11:32:44 +00:00
func dimensions(diagram *d2target.Diagram, pad int) (left, top, width, height int) {
tl, br := diagram.BoundingBox()
2023-02-15 19:33:13 +00:00
left = tl.X - pad
top = tl.Y - pad
width = br.X - tl.X + pad*2
height = br.Y - tl.Y + pad*2
2023-02-15 19:33:13 +00:00
return left, top, width, height
}
2025-03-11 21:30:05 +00:00
func renderLegend(buf *bytes.Buffer, diagram *d2target.Diagram, diagramHash string, theme *d2themes.Theme) error {
if diagram.Legend == nil || (len(diagram.Legend.Shapes) == 0 && len(diagram.Legend.Connections) == 0) {
return nil
}
_, br := diagram.BoundingBox()
ruler, err := textmeasure.NewRuler()
if err != nil {
return err
}
totalHeight := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING
maxLabelWidth := 0
itemCount := 0
for _, s := range diagram.Legend.Shapes {
if s.Label == "" {
continue
}
mtext := &d2target.MText{
Text: s.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
maxLabelWidth = go2.IntMax(maxLabelWidth, dims.Width)
totalHeight += go2.IntMax(dims.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING
itemCount++
}
for _, c := range diagram.Legend.Connections {
if c.Label == "" {
continue
}
mtext := &d2target.MText{
Text: c.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
maxLabelWidth = go2.IntMax(maxLabelWidth, dims.Width)
totalHeight += go2.IntMax(dims.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING
itemCount++
}
if itemCount > 0 {
totalHeight -= LEGEND_ITEM_SPACING / 2
}
if itemCount > 0 && len(diagram.Legend.Connections) > 0 {
totalHeight += LEGEND_PADDING * 1.5
} else {
totalHeight += LEGEND_PADDING * 1.2
}
legendWidth := LEGEND_PADDING*2 + LEGEND_ICON_SIZE + LEGEND_PADDING + maxLabelWidth
legendX := br.X + LEGEND_CORNER_PADDING
tl, _ := diagram.BoundingBox()
legendY := br.Y - totalHeight
if legendY < tl.Y {
legendY = tl.Y
}
shadowEl := d2themes.NewThemableElement("rect", theme)
shadowEl.Fill = "#F7F7FA"
shadowEl.Stroke = "#DEE1EB"
shadowEl.Style = "stroke-width: 1px; filter: drop-shadow(0px 2px 3px rgba(0, 0, 0, 0.1))"
shadowEl.X = float64(legendX)
shadowEl.Y = float64(legendY)
shadowEl.Width = float64(legendWidth)
shadowEl.Height = float64(totalHeight)
shadowEl.Rx = 4
fmt.Fprint(buf, shadowEl.Render())
legendEl := d2themes.NewThemableElement("rect", theme)
legendEl.Fill = "#ffffff"
legendEl.Stroke = "#DEE1EB"
legendEl.Style = "stroke-width: 1px"
legendEl.X = float64(legendX)
legendEl.Y = float64(legendY)
legendEl.Width = float64(legendWidth)
legendEl.Height = float64(totalHeight)
legendEl.Rx = 4
fmt.Fprint(buf, legendEl.Render())
fmt.Fprintf(buf, `<text class="text-bold" x="%d" y="%d" style="font-size: %dpx;">Legend</text>`,
legendX+LEGEND_PADDING, legendY+LEGEND_PADDING+LEGEND_FONT_SIZE, LEGEND_FONT_SIZE+2)
currentY := legendY + LEGEND_PADDING*2 + LEGEND_FONT_SIZE
shapeCount := 0
for _, s := range diagram.Legend.Shapes {
if s.Label == "" {
continue
}
iconX := legendX + LEGEND_PADDING
iconY := currentY
shapeIcon, err := renderLegendShapeIcon(s, iconX, iconY, diagramHash, theme)
if err != nil {
return err
}
fmt.Fprint(buf, shapeIcon)
mtext := &d2target.MText{
Text: s.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
rowHeight := go2.IntMax(dims.Height, LEGEND_ICON_SIZE)
textY := currentY + rowHeight/2 + int(float64(dims.Height)*0.3)
fmt.Fprintf(buf, `<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>`,
iconX+LEGEND_ICON_SIZE+LEGEND_PADDING, textY, LEGEND_FONT_SIZE,
html.EscapeString(s.Label))
currentY += rowHeight + LEGEND_ITEM_SPACING
shapeCount++
}
if shapeCount > 0 && len(diagram.Legend.Connections) > 0 {
currentY += LEGEND_ITEM_SPACING / 2
separatorEl := d2themes.NewThemableElement("line", theme)
separatorEl.X1 = float64(legendX + LEGEND_PADDING)
separatorEl.Y1 = float64(currentY)
separatorEl.X2 = float64(legendX + legendWidth - LEGEND_PADDING)
separatorEl.Y2 = float64(currentY)
separatorEl.Stroke = "#DEE1EB"
separatorEl.StrokeDashArray = "2,2"
fmt.Fprint(buf, separatorEl.Render())
currentY += LEGEND_ITEM_SPACING
}
for _, c := range diagram.Legend.Connections {
if c.Label == "" {
continue
}
iconX := legendX + LEGEND_PADDING
iconY := currentY + LEGEND_ICON_SIZE/2
connIcon, err := renderLegendConnectionIcon(c, iconX, iconY, theme)
if err != nil {
return err
}
fmt.Fprint(buf, connIcon)
mtext := &d2target.MText{
Text: c.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
rowHeight := go2.IntMax(dims.Height, LEGEND_ICON_SIZE)
textY := currentY + rowHeight/2 + int(float64(dims.Height)*0.2)
fmt.Fprintf(buf, `<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>`,
iconX+LEGEND_ICON_SIZE+LEGEND_PADDING, textY, LEGEND_FONT_SIZE,
html.EscapeString(c.Label))
currentY += rowHeight + LEGEND_ITEM_SPACING
}
if shapeCount > 0 && len(diagram.Legend.Connections) > 0 {
currentY += LEGEND_PADDING / 2
} else {
currentY += LEGEND_PADDING / 4
}
return nil
}
func renderLegendShapeIcon(s d2target.Shape, x, y int, diagramHash string, theme *d2themes.Theme) (string, error) {
iconShape := s
const sizeFactor = 5
iconShape.Pos.X = 0
iconShape.Pos.Y = 0
iconShape.Width = LEGEND_ICON_SIZE * sizeFactor
iconShape.Height = LEGEND_ICON_SIZE * sizeFactor
iconShape.Label = ""
buf := &bytes.Buffer{}
appendixBuf := &bytes.Buffer{}
finalBuf := &bytes.Buffer{}
fmt.Fprintf(finalBuf, `<g transform="translate(%d, %d) scale(%f)">`,
x, y, 1.0/sizeFactor)
_, err := drawShape(buf, appendixBuf, diagramHash, iconShape, nil, theme)
if err != nil {
return "", err
}
fmt.Fprint(finalBuf, buf.String())
fmt.Fprint(finalBuf, `</g>`)
return finalBuf.String(), nil
}
func renderLegendConnectionIcon(c d2target.Connection, x, y int, theme *d2themes.Theme) (string, error) {
finalBuf := &bytes.Buffer{}
buf := &bytes.Buffer{}
const sizeFactor = 2
legendConn := *d2target.BaseConnection()
legendConn.ID = c.ID
legendConn.SrcArrow = c.SrcArrow
legendConn.DstArrow = c.DstArrow
legendConn.StrokeDash = c.StrokeDash
legendConn.StrokeWidth = c.StrokeWidth
legendConn.Stroke = c.Stroke
legendConn.Fill = c.Fill
legendConn.BorderRadius = c.BorderRadius
legendConn.Opacity = c.Opacity
legendConn.Animated = c.Animated
startX := 0.0
midY := 0.0
width := float64(LEGEND_ICON_SIZE * sizeFactor)
legendConn.Route = []*geo.Point{
{X: startX, Y: midY},
{X: startX + width, Y: midY},
}
legendHash := fmt.Sprintf("legend-%s", hash(fmt.Sprintf("%s-%d-%d", c.ID, x, y)))
markers := make(map[string]struct{})
idToShape := make(map[string]d2target.Shape)
fmt.Fprintf(finalBuf, `<g transform="translate(%d, %d) scale(%f)">`,
x, y, 1.0/sizeFactor)
_, err := drawConnection(buf, legendHash, legendConn, markers, idToShape, nil, theme)
if err != nil {
return "", err
}
fmt.Fprint(finalBuf, buf.String())
fmt.Fprint(finalBuf, `</g>`)
return finalBuf.String(), nil
}
2025-01-26 21:18:00 +00:00
func arrowheadMarkerID(diagramHash string, isTarget bool, connection d2target.Connection) string {
var arrowhead d2target.Arrowhead
if isTarget {
arrowhead = connection.DstArrow
} else {
arrowhead = connection.SrcArrow
}
2025-01-26 21:18:00 +00:00
return fmt.Sprintf("mk-%s-%s", diagramHash, hash(fmt.Sprintf("%s,%t,%d,%s",
arrowhead, isTarget, connection.StrokeWidth, connection.Stroke,
)))
}
2024-10-09 18:09:46 +00:00
func arrowheadMarker(isTarget bool, id string, connection d2target.Connection, inlineTheme *d2themes.Theme) string {
arrowhead := connection.DstArrow
if !isTarget {
arrowhead = connection.SrcArrow
}
strokeWidth := float64(connection.StrokeWidth)
2023-04-17 19:06:17 +00:00
width, height := arrowhead.Dimensions(strokeWidth)
var path string
switch arrowhead {
case d2target.ArrowArrowhead:
2024-10-09 18:09:46 +00:00
polygonEl := d2themes.NewThemableElement("polygon", inlineTheme)
polygonEl.Fill = connection.Stroke
polygonEl.ClassName = "connection"
2023-01-09 18:16:28 +00:00
polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
if isTarget {
2023-01-09 18:16:28 +00:00
polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
0., 0.,
width, height/2,
0., height,
width/4, height/2,
)
} else {
2023-01-09 18:16:28 +00:00
polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
0., height/2,
width, 0.,
width*3/4, height/2,
width, height,
)
}
2023-01-09 18:16:28 +00:00
path = polygonEl.Render()
2023-11-08 00:57:43 +00:00
case d2target.UnfilledTriangleArrowhead:
2024-10-09 18:09:46 +00:00
polygonEl := d2themes.NewThemableElement("polygon", inlineTheme)
2023-11-08 00:57:43 +00:00
polygonEl.Fill = d2target.BG_COLOR
polygonEl.Stroke = connection.Stroke
polygonEl.ClassName = "connection"
polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
2023-11-08 01:46:26 +00:00
inset := strokeWidth / 2
2023-11-08 00:57:43 +00:00
if isTarget {
polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
2023-11-08 01:46:26 +00:00
inset, inset,
width-inset, height/2.0,
inset, height-inset,
2023-11-08 00:57:43 +00:00
)
} else {
polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
2023-11-08 01:46:26 +00:00
width-inset, inset,
inset, height/2.0,
width-inset, height-inset,
2023-11-08 00:57:43 +00:00
)
}
path = polygonEl.Render()
case d2target.TriangleArrowhead:
2024-10-09 18:09:46 +00:00
polygonEl := d2themes.NewThemableElement("polygon", inlineTheme)
polygonEl.Fill = connection.Stroke
polygonEl.ClassName = "connection"
2023-01-09 18:16:28 +00:00
polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
if isTarget {
2023-01-09 18:16:28 +00:00
polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
0., 0.,
width, height/2.0,
0., height,
)
} else {
2023-01-09 18:16:28 +00:00
polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
width, 0.,
0., height/2.0,
width, height,
)
}
2023-01-09 18:16:28 +00:00
path = polygonEl.Render()
case d2target.LineArrowhead:
2024-10-09 18:09:46 +00:00
polylineEl := d2themes.NewThemableElement("polyline", inlineTheme)
2023-01-09 18:16:28 +00:00
polylineEl.Fill = color.None
polylineEl.ClassName = "connection"
polylineEl.Stroke = connection.Stroke
2023-01-09 18:16:28 +00:00
polylineEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
if isTarget {
2023-01-09 18:16:28 +00:00
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 {
2023-01-09 18:16:28 +00:00
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,
)
}
2023-01-09 18:16:28 +00:00
path = polylineEl.Render()
case d2target.FilledDiamondArrowhead:
2024-10-09 18:09:46 +00:00
polygonEl := d2themes.NewThemableElement("polygon", inlineTheme)
polygonEl.ClassName = "connection"
polygonEl.Fill = connection.Stroke
2023-01-09 18:16:28 +00:00
polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
if isTarget {
2023-01-09 18:16:28 +00:00
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 {
2023-01-09 18:16:28 +00:00
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,
)
}
2023-01-09 18:16:28 +00:00
path = polygonEl.Render()
case d2target.DiamondArrowhead:
2024-10-09 18:09:46 +00:00
polygonEl := d2themes.NewThemableElement("polygon", inlineTheme)
polygonEl.ClassName = "connection"
2023-02-26 19:41:50 +00:00
polygonEl.Fill = d2target.BG_COLOR
polygonEl.Stroke = connection.Stroke
2023-01-09 18:16:28 +00:00
polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
if isTarget {
2023-01-09 18:16:28 +00:00
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 {
2023-01-09 18:16:28 +00:00
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,
)
}
2023-01-09 18:16:28 +00:00
path = polygonEl.Render()
2023-01-10 03:44:45 +00:00
case d2target.FilledCircleArrowhead:
2023-01-19 19:19:29 +00:00
radius := width / 2
2023-01-27 20:08:01 +00:00
2024-10-09 18:09:46 +00:00
circleEl := d2themes.NewThemableElement("circle", inlineTheme)
2023-01-27 20:08:01 +00:00
circleEl.Cy = radius
2023-02-22 17:50:06 +00:00
circleEl.R = radius - strokeWidth/2
2023-01-27 20:08:01 +00:00
circleEl.Fill = connection.Stroke
circleEl.ClassName = "connection"
2023-01-27 20:08:01 +00:00
circleEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
2023-01-10 03:44:45 +00:00
if isTarget {
2023-01-27 20:08:01 +00:00
circleEl.Cx = radius + strokeWidth/2
2023-01-10 03:44:45 +00:00
} else {
2023-02-22 17:50:06 +00:00
circleEl.Cx = radius - strokeWidth/2
2023-01-10 03:44:45 +00:00
}
2023-01-27 20:08:01 +00:00
path = circleEl.Render()
2023-01-10 03:44:45 +00:00
case d2target.CircleArrowhead:
2023-01-19 19:19:29 +00:00
radius := width / 2
2023-01-27 20:08:01 +00:00
2024-10-09 18:09:46 +00:00
circleEl := d2themes.NewThemableElement("circle", inlineTheme)
2023-01-27 20:08:01 +00:00
circleEl.Cy = radius
circleEl.R = radius - strokeWidth
2023-02-26 19:41:50 +00:00
circleEl.Fill = d2target.BG_COLOR
2023-01-27 20:08:01 +00:00
circleEl.Stroke = connection.Stroke
circleEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
2023-01-10 03:44:45 +00:00
if isTarget {
2023-01-27 20:08:01 +00:00
circleEl.Cx = radius + strokeWidth/2
2023-01-10 03:44:45 +00:00
} else {
2023-01-27 20:08:01 +00:00
circleEl.Cx = radius - strokeWidth/2
2023-01-10 03:44:45 +00:00
}
2023-01-27 20:08:01 +00:00
path = circleEl.Render()
2024-12-18 17:40:01 +00:00
case d2target.FilledBoxArrowhead:
polygonEl := d2themes.NewThemableElement("polygon", inlineTheme)
polygonEl.ClassName = "connection"
polygonEl.Fill = connection.Stroke
polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
if isTarget {
polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
0., 0.,
0., height,
width, height,
width, 0.,
)
} else {
polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
0., 0.,
0., height,
width, height,
width, 0.,
)
}
2025-02-04 13:25:28 +00:00
2024-12-18 17:40:01 +00:00
path = polygonEl.Render()
case d2target.BoxArrowhead:
polygonEl := d2themes.NewThemableElement("polygon", inlineTheme)
polygonEl.ClassName = "connection"
polygonEl.Fill = d2target.BG_COLOR
polygonEl.Stroke = connection.Stroke
polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
2025-02-04 13:25:28 +00:00
polygonEl.Style = fmt.Sprintf("%sstroke-linejoin:miter;", polygonEl.Style)
2024-12-18 17:40:01 +00:00
2025-02-04 13:25:28 +00:00
inset := strokeWidth / 2
2024-12-18 17:40:01 +00:00
if isTarget {
polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
2025-02-04 13:25:28 +00:00
inset, inset,
inset, height-inset,
width-inset, height-inset,
width-inset, inset,
2024-12-18 17:40:01 +00:00
)
} else {
polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
2025-02-04 13:25:28 +00:00
inset, inset,
inset, height-inset,
width-inset, height-inset,
width-inset, inset,
2024-12-18 17:40:01 +00:00
)
}
path = polygonEl.Render()
case d2target.CfOne, d2target.CfMany, d2target.CfOneRequired, d2target.CfManyRequired:
2023-02-10 08:57:15 +00:00
offset := 3.0 + float64(connection.StrokeWidth)*1.8
2023-01-09 18:16:28 +00:00
var modifierEl *d2themes.ThemableElement
if arrowhead == d2target.CfOneRequired || arrowhead == d2target.CfManyRequired {
2024-10-09 18:09:46 +00:00
modifierEl = d2themes.NewThemableElement("path", inlineTheme)
2023-01-09 18:16:28 +00:00
modifierEl.D = fmt.Sprintf("M%f,%f %f,%f",
offset, 0.,
offset, height,
)
2023-02-26 19:41:50 +00:00
modifierEl.Fill = d2target.BG_COLOR
modifierEl.Stroke = connection.Stroke
modifierEl.ClassName = "connection"
modifierEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
} else {
2024-10-09 18:09:46 +00:00
modifierEl = d2themes.NewThemableElement("circle", inlineTheme)
2023-02-19 11:32:44 +00:00
modifierEl.Cx = offset/2.0 + 2.0
2023-01-09 18:16:28 +00:00
modifierEl.Cy = height / 2.0
modifierEl.R = offset / 2.0
2023-02-26 19:41:50 +00:00
modifierEl.Fill = d2target.BG_COLOR
modifierEl.Stroke = connection.Stroke
modifierEl.ClassName = "connection"
modifierEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
}
2023-01-09 18:16:28 +00:00
2024-10-09 18:09:46 +00:00
childPathEl := d2themes.NewThemableElement("path", inlineTheme)
if arrowhead == d2target.CfMany || arrowhead == d2target.CfManyRequired {
2023-01-09 18:16:28 +00:00
childPathEl.D = fmt.Sprintf("M%f,%f %f,%f M%f,%f %f,%f M%f,%f %f,%f",
2023-01-01 10:31:45 +00:00
width-3.0, height/2.0,
width+offset, height/2.0,
2023-02-10 08:57:15 +00:00
offset+3.0, height/2.0,
width+offset, 0.,
2023-02-10 08:57:15 +00:00
offset+3.0, height/2.0,
width+offset, height,
)
} else {
2023-01-09 18:16:28 +00:00
childPathEl.D = fmt.Sprintf("M%f,%f %f,%f M%f,%f %f,%f",
2023-01-01 10:31:45 +00:00
width-3.0, height/2.0,
width+offset, height/2.0,
2023-02-10 08:57:15 +00:00
offset*2.0, 0.,
offset*2.0, height,
)
}
2023-01-09 18:16:28 +00:00
2024-10-09 18:09:46 +00:00
gEl := d2themes.NewThemableElement("g", inlineTheme)
2023-01-09 18:16:28 +00:00
if !isTarget {
gEl.Transform = fmt.Sprintf("scale(-1) translate(-%f, -%f)", width, height)
}
2023-02-26 19:41:50 +00:00
gEl.Fill = d2target.BG_COLOR
gEl.Stroke = connection.Stroke
gEl.ClassName = "connection"
gEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
2023-01-09 18:16:28 +00:00
gEl.Content = fmt.Sprintf("%s%s",
modifierEl.Render(), childPathEl.Render(),
)
path = gEl.Render()
default:
return ""
}
var refX float64
refY := height / 2
switch arrowhead {
case d2target.DiamondArrowhead:
if isTarget {
refX = width - 0.6*strokeWidth
} else {
refX = width/8 + 0.6*strokeWidth
}
width *= 1.1
default:
if isTarget {
refX = width - 1.5*strokeWidth
} else {
refX = 1.5 * strokeWidth
}
}
return strings.Join([]string{
fmt.Sprintf(`<marker id="%s" markerWidth="%f" markerHeight="%f" refX="%f" refY="%f"`,
id, width, height, refX, refY,
),
fmt.Sprintf(`viewBox="%f %f %f %f"`, 0., 0., width, height),
`orient="auto" markerUnits="userSpaceOnUse">`,
path,
"</marker>",
}, " ")
}
// compute the (dx, dy) adjustment to apply to get the arrowhead-adjusted end point
func arrowheadAdjustment(start, end *geo.Point, arrowhead d2target.Arrowhead, edgeStrokeWidth, shapeStrokeWidth int) *geo.Point {
distance := (float64(edgeStrokeWidth) + float64(shapeStrokeWidth)) / 2.0
if arrowhead != d2target.NoArrowhead {
distance += float64(edgeStrokeWidth)
}
v := geo.NewVector(end.X-start.X, end.Y-start.Y)
return v.Unit().Multiply(-distance).ToPoint()
}
func getArrowheadAdjustments(connection d2target.Connection, idToShape map[string]d2target.Shape) (srcAdj, dstAdj *geo.Point) {
route := connection.Route
srcShape := idToShape[connection.Src]
dstShape := idToShape[connection.Dst]
sourceAdjustment := arrowheadAdjustment(route[1], route[0], connection.SrcArrow, connection.StrokeWidth, srcShape.StrokeWidth)
targetAdjustment := arrowheadAdjustment(route[len(route)-2], route[len(route)-1], connection.DstArrow, connection.StrokeWidth, dstShape.StrokeWidth)
return sourceAdjustment, targetAdjustment
}
// returns the path's d attribute for the given connection
func pathData(connection d2target.Connection, srcAdj, dstAdj *geo.Point) string {
var path []string
route := connection.Route
path = append(path, fmt.Sprintf("M %f %f",
route[0].X+srcAdj.X,
route[0].Y+srcAdj.Y,
))
if connection.IsCurve {
i := 1
for ; i < len(route)-3; i += 3 {
path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
route[i].X, route[i].Y,
route[i+1].X, route[i+1].Y,
route[i+2].X, route[i+2].Y,
))
}
// final curve target adjustment
path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
route[i].X, route[i].Y,
route[i+1].X, route[i+1].Y,
route[i+2].X+dstAdj.X,
route[i+2].Y+dstAdj.Y,
))
} else {
for i := 1; i < len(route)-1; i++ {
prevSource := route[i-1]
prevTarget := route[i]
currTarget := route[i+1]
prevVector := prevSource.VectorTo(prevTarget)
currVector := prevTarget.VectorTo(currTarget)
dist := geo.EuclideanDistance(prevTarget.X, prevTarget.Y, currTarget.X, currTarget.Y)
2023-02-28 04:06:24 +00:00
connectionBorderRadius := connection.BorderRadius
units := math.Min(connectionBorderRadius, dist/2)
prevTranslations := prevVector.Unit().Multiply(units).ToPoint()
currTranslations := currVector.Unit().Multiply(units).ToPoint()
path = append(path, fmt.Sprintf("L %f %f",
prevTarget.X-prevTranslations.X,
prevTarget.Y-prevTranslations.Y,
))
// If the segment length is too small, instead of drawing 2 arcs, just skip this segment and bezier curve to the next one
2023-02-28 04:06:24 +00:00
if units < connectionBorderRadius && i < len(route)-2 {
nextTarget := route[i+2]
nextVector := geo.NewVector(nextTarget.X-currTarget.X, nextTarget.Y-currTarget.Y)
i++
nextTranslations := nextVector.Unit().Multiply(units).ToPoint()
// These 2 bezier control points aren't just at the corner -- they are reflected at the corner, which causes the curve to be ~tangent to the corner,
// which matches how the two arcs look
path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
// Control point
prevTarget.X+prevTranslations.X,
prevTarget.Y+prevTranslations.Y,
// Control point
currTarget.X-nextTranslations.X,
currTarget.Y-nextTranslations.Y,
// Where curve ends
currTarget.X+nextTranslations.X,
currTarget.Y+nextTranslations.Y,
))
} else {
path = append(path, fmt.Sprintf("S %f %f %f %f",
prevTarget.X,
prevTarget.Y,
prevTarget.X+currTranslations.X,
prevTarget.Y+currTranslations.Y,
))
}
}
lastPoint := route[len(route)-1]
path = append(path, fmt.Sprintf("L %f %f",
lastPoint.X+dstAdj.X,
lastPoint.Y+dstAdj.Y,
))
}
return strings.Join(path, " ")
}
2023-06-08 22:54:17 +00:00
func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) string {
fill := "black"
if opacity != 1 {
fill = fmt.Sprintf("rgba(0,0,0,%.2f)", opacity)
}
return fmt.Sprintf(`<rect x="%f" y="%f" width="%d" height="%d" fill="%s"></rect>`,
2025-03-24 22:35:11 +00:00
labelTL.X-2, labelTL.Y,
width+4,
2022-12-05 19:57:16 +00:00
height,
2023-06-08 22:54:17 +00:00
fill,
2022-12-03 06:47:54 +00:00
)
}
2025-01-26 21:18:00 +00:00
func drawConnection(writer io.Writer, diagramHash string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape, jsRunner jsrunner.JSRunner, inlineTheme *d2themes.Theme) (labelMask string, _ error) {
2023-01-19 08:46:30 +00:00
opacityStyle := ""
if connection.Opacity != 1.0 {
opacityStyle = fmt.Sprintf(" style='opacity:%f'", connection.Opacity)
}
2023-02-06 21:32:08 +00:00
classes := []string{base64.URLEncoding.EncodeToString([]byte(svg.EscapeText(connection.ID)))}
classes = append(classes, connection.Classes...)
classStr := fmt.Sprintf(` class="%s"`, strings.Join(classes, " "))
2025-02-04 17:25:03 +00:00
fmt.Fprintf(writer, `<g%s%s>`, classStr, opacityStyle)
var markerStart string
if connection.SrcArrow != d2target.NoArrowhead {
2025-01-26 21:18:00 +00:00
id := arrowheadMarkerID(diagramHash, false, connection)
if _, in := markers[id]; !in {
2024-10-09 18:09:46 +00:00
marker := arrowheadMarker(false, id, connection, inlineTheme)
if marker == "" {
panic(fmt.Sprintf("received empty arrow head marker for: %#v", connection))
}
fmt.Fprint(writer, marker)
markers[id] = struct{}{}
}
markerStart = fmt.Sprintf(`marker-start="url(#%s)" `, id)
}
var markerEnd string
if connection.DstArrow != d2target.NoArrowhead {
2025-01-26 21:18:00 +00:00
id := arrowheadMarkerID(diagramHash, true, connection)
if _, in := markers[id]; !in {
2024-10-09 18:09:46 +00:00
marker := arrowheadMarker(true, id, connection, inlineTheme)
if marker == "" {
panic(fmt.Sprintf("received empty arrow head marker for: %#v", connection))
}
fmt.Fprint(writer, marker)
markers[id] = struct{}{}
}
markerEnd = fmt.Sprintf(`marker-end="url(#%s)" `, id)
}
2025-02-17 04:49:49 +00:00
if connection.Icon != nil {
iconPos := connection.GetIconPosition()
if iconPos != nil {
connectionIconClipPath := ""
2025-03-19 17:32:49 +00:00
if connection.IconBorderRadius != 0 {
connectionIconClipPath = fmt.Sprintf(` clip-path="inset(0 round %fpx)"`, connection.IconBorderRadius)
2025-03-19 17:32:49 +00:00
}
fmt.Fprintf(writer, `<image href="%s" x="%f" y="%f" width="%d" height="%d"%s />`,
html.EscapeString(connection.Icon.String()),
iconPos.X,
iconPos.Y,
d2target.DEFAULT_ICON_SIZE,
d2target.DEFAULT_ICON_SIZE,
connectionIconClipPath,
)
2025-02-17 04:49:49 +00:00
}
}
var labelTL *geo.Point
if connection.Label != "" {
labelTL = connection.GetLabelTopLeft()
labelTL.X = math.Round(labelTL.X)
labelTL.Y = math.Round(labelTL.Y)
2025-02-17 04:49:49 +00:00
maskTL := labelTL.Copy()
width := connection.LabelWidth
height := connection.LabelHeight
if connection.Icon != nil {
width += d2target.CONNECTION_ICON_LABEL_GAP + d2target.DEFAULT_ICON_SIZE
maskTL.X -= float64(d2target.CONNECTION_ICON_LABEL_GAP + d2target.DEFAULT_ICON_SIZE)
}
2023-07-17 21:21:36 +00:00
if label.FromString(connection.LabelPosition).IsOnEdge() {
2025-02-17 04:49:49 +00:00
labelMask = makeLabelMask(maskTL, width, height, 1)
} else {
2025-02-17 04:49:49 +00:00
labelMask = makeLabelMask(maskTL, width, height, 0.75)
}
} else if connection.Icon != nil {
iconPos := connection.GetIconPosition()
if iconPos != nil {
maskTL := &geo.Point{
X: iconPos.X,
Y: iconPos.Y,
}
if label.FromString(connection.IconPosition).IsOnEdge() {
labelMask = makeLabelMask(maskTL, d2target.DEFAULT_ICON_SIZE, d2target.DEFAULT_ICON_SIZE, 1)
} else {
labelMask = makeLabelMask(maskTL, d2target.DEFAULT_ICON_SIZE, d2target.DEFAULT_ICON_SIZE, 0.75)
}
}
}
srcAdj, dstAdj := getArrowheadAdjustments(connection, idToShape)
path := pathData(connection, srcAdj, dstAdj)
2025-01-26 21:18:00 +00:00
mask := fmt.Sprintf(`mask="url(#%s)"`, diagramHash)
2024-05-14 21:08:25 +00:00
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out, err := d2sketch.Connection(jsRunner, connection, path, mask)
2022-12-21 07:43:45 +00:00
if err != nil {
return "", err
}
2023-01-09 18:16:28 +00:00
fmt.Fprint(writer, out)
2023-01-12 20:01:49 +00:00
// render sketch arrowheads separately
2025-01-15 23:30:17 +00:00
arrowPaths, err := d2sketch.Arrowheads(jsRunner, connection, srcAdj, dstAdj)
2023-01-13 02:27:53 +00:00
if err != nil {
return "", err
2023-01-12 20:01:49 +00:00
}
2023-01-13 02:27:53 +00:00
fmt.Fprint(writer, arrowPaths)
2022-12-21 07:43:45 +00:00
} else {
2023-01-16 11:15:59 +00:00
animatedClass := ""
if connection.Animated {
animatedClass = " animated-connection"
}
2024-05-18 16:52:04 +00:00
// If connection is animated and bidirectional
if connection.Animated && ((connection.DstArrow == d2target.NoArrowhead && connection.SrcArrow == d2target.NoArrowhead) || (connection.DstArrow != d2target.NoArrowhead && connection.SrcArrow != d2target.NoArrowhead)) {
// There is no pure CSS way to animate bidirectional connections in two directions, so we split it up
2024-05-20 11:39:06 +00:00
path1, path2, err := svg.SplitPath(path, 0.5)
2024-05-17 03:14:15 +00:00
if err != nil {
return "", err
}
2024-05-14 20:28:44 +00:00
2024-10-09 18:09:46 +00:00
pathEl1 := d2themes.NewThemableElement("path", inlineTheme)
2024-05-14 20:28:44 +00:00
pathEl1.D = path1
pathEl1.Fill = color.None
pathEl1.Stroke = connection.Stroke
pathEl1.ClassName = fmt.Sprintf("connection%s", animatedClass)
pathEl1.Style = connection.CSSStyle()
pathEl1.Style += "animation-direction: reverse;"
pathEl1.Attributes = fmt.Sprintf("%s%s", markerStart, mask)
fmt.Fprint(writer, pathEl1.Render())
2024-10-09 18:09:46 +00:00
pathEl2 := d2themes.NewThemableElement("path", inlineTheme)
2024-05-14 20:28:44 +00:00
pathEl2.D = path2
pathEl2.Fill = color.None
pathEl2.Stroke = connection.Stroke
pathEl2.ClassName = fmt.Sprintf("connection%s", animatedClass)
pathEl2.Style = connection.CSSStyle()
pathEl2.Attributes = fmt.Sprintf("%s%s", markerEnd, mask)
fmt.Fprint(writer, pathEl2.Render())
2024-05-18 16:52:04 +00:00
} else {
2024-10-09 18:09:46 +00:00
pathEl := d2themes.NewThemableElement("path", inlineTheme)
2024-05-18 16:52:04 +00:00
pathEl.D = path
pathEl.Fill = color.None
pathEl.Stroke = connection.Stroke
pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
pathEl.Style = connection.CSSStyle()
pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask)
fmt.Fprint(writer, pathEl.Render())
2024-05-14 20:28:44 +00:00
}
2022-12-21 07:43:45 +00:00
}
if connection.Label != "" {
fontClass := "text"
2023-03-13 23:14:50 +00:00
if connection.FontFamily == "mono" {
fontClass = "text-mono"
}
if connection.Bold {
fontClass += "-bold"
} else if connection.Italic {
fontClass += "-italic"
}
2024-02-15 00:46:47 +00:00
if connection.Underline {
fontClass += " text-underline"
}
2023-01-09 18:16:28 +00:00
if connection.Fill != color.Empty {
2024-10-09 18:09:46 +00:00
rectEl := d2themes.NewThemableElement("rect", inlineTheme)
2024-09-14 23:38:47 +00:00
rectEl.Rx = 10
rectEl.X, rectEl.Y = labelTL.X-4, labelTL.Y-3
rectEl.Width, rectEl.Height = float64(connection.LabelWidth)+8, float64(connection.LabelHeight)+6
2023-01-09 18:16:28 +00:00
rectEl.Fill = connection.Fill
fmt.Fprint(writer, rectEl.Render())
}
2023-01-09 18:16:28 +00:00
2024-10-09 18:09:46 +00:00
textEl := d2themes.NewThemableElement("text", inlineTheme)
2023-01-09 18:16:28 +00:00
textEl.X = labelTL.X + float64(connection.LabelWidth)/2
textEl.Y = labelTL.Y + float64(connection.FontSize)
textEl.ClassName = fontClass
2023-01-09 18:16:28 +00:00
textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", connection.FontSize)
textEl.Content = RenderText(connection.Label, textEl.X, float64(connection.LabelHeight))
2024-05-30 01:05:52 +00:00
if connection.Link != "" {
2024-05-30 01:45:36 +00:00
textEl.ClassName += " text-underline text-link"
2024-05-30 01:05:52 +00:00
fmt.Fprintf(writer, `<a href="%s" xlink:href="%[1]s">`, svg.EscapeText(connection.Link))
2024-05-30 01:45:36 +00:00
} else {
textEl.Fill = connection.GetFontColor()
2024-05-30 01:05:52 +00:00
}
2023-01-09 18:16:28 +00:00
fmt.Fprint(writer, textEl.Render())
2024-05-30 01:05:52 +00:00
if connection.Link != "" {
fmt.Fprintf(writer, "</a>")
}
}
2022-11-24 04:14:46 +00:00
2023-04-14 18:51:48 +00:00
if connection.SrcLabel != nil && connection.SrcLabel.Label != "" {
2024-10-09 18:09:46 +00:00
fmt.Fprint(writer, renderArrowheadLabel(connection, connection.SrcLabel.Label, false, inlineTheme))
2022-11-24 04:14:46 +00:00
}
2023-04-14 18:51:48 +00:00
if connection.DstLabel != nil && connection.DstLabel.Label != "" {
2024-10-09 18:09:46 +00:00
fmt.Fprint(writer, renderArrowheadLabel(connection, connection.DstLabel.Label, true, inlineTheme))
2022-11-24 04:14:46 +00:00
}
fmt.Fprintf(writer, `</g>`)
2022-12-03 06:47:54 +00:00
return
}
2024-10-09 18:09:46 +00:00
func renderArrowheadLabel(connection d2target.Connection, text string, isDst bool, inlineTheme *d2themes.Theme) string {
2023-04-15 02:08:29 +00:00
var width, height float64
if isDst {
width = float64(connection.DstLabel.LabelWidth)
height = float64(connection.DstLabel.LabelHeight)
} else {
width = float64(connection.SrcLabel.LabelWidth)
height = float64(connection.SrcLabel.LabelHeight)
}
2023-04-17 19:06:17 +00:00
labelTL := connection.GetArrowheadLabelPosition(isDst)
2023-04-15 02:08:29 +00:00
// svg text is positioned with the center of its baseline
baselineCenter := geo.Point{
X: labelTL.X + width/2.,
Y: labelTL.Y + float64(connection.FontSize),
}
2022-11-24 04:40:24 +00:00
2024-10-09 18:09:46 +00:00
textEl := d2themes.NewThemableElement("text", inlineTheme)
textEl.X = baselineCenter.X
textEl.Y = baselineCenter.Y
2023-02-26 19:41:50 +00:00
textEl.Fill = d2target.FG_COLOR
if isDst {
if connection.DstLabel.Color != "" {
textEl.Fill = connection.DstLabel.Color
}
} else {
if connection.SrcLabel.Color != "" {
textEl.Fill = connection.SrcLabel.Color
}
}
textEl.ClassName = "text-italic"
2023-04-15 02:08:29 +00:00
textEl.Style = fmt.Sprintf("text-anchor:middle;font-size:%vpx", connection.FontSize)
2023-01-09 18:16:28 +00:00
textEl.Content = RenderText(text, textEl.X, height)
return textEl.Render()
2022-11-24 04:40:24 +00:00
}
2024-10-09 18:09:46 +00:00
func renderOval(tl *geo.Point, width, height float64, fill, fillPattern, stroke, style string, inlineTheme *d2themes.Theme) string {
el := d2themes.NewThemableElement("ellipse", inlineTheme)
2023-01-09 18:16:28 +00:00
el.Rx = width / 2
el.Ry = height / 2
el.Cx = tl.X + el.Rx
el.Cy = tl.Y + el.Ry
2023-01-27 21:30:44 +00:00
el.Fill, el.Stroke = fill, stroke
2023-03-14 06:01:33 +00:00
el.FillPattern = fillPattern
el.ClassName = "shape"
2023-01-09 18:16:28 +00:00
el.Style = style
return el.Render()
2022-11-09 03:40:20 +00:00
}
2024-10-09 18:09:46 +00:00
func renderDoubleOval(tl *geo.Point, width, height float64, fill, fillStroke, stroke, style string, inlineTheme *d2themes.Theme) string {
2023-01-22 10:40:47 +00:00
var innerTL *geo.Point = tl.AddVector(geo.NewVector(d2target.INNER_BORDER_OFFSET, d2target.INNER_BORDER_OFFSET))
2024-10-09 18:09:46 +00:00
return renderOval(tl, width, height, fill, fillStroke, stroke, style, inlineTheme) + renderOval(innerTL, width-10, height-10, fill, "", stroke, style, inlineTheme)
2022-12-30 09:14:44 +00:00
}
2024-09-27 21:49:10 +00:00
func defineGradients(writer io.Writer, cssGradient string) {
gradient, _ := color.ParseGradient(cssGradient)
fmt.Fprint(writer, fmt.Sprintf(`<defs>%s</defs>`, color.GradientToSVG(gradient)))
}
2022-11-09 19:10:51 +00:00
func defineShadowFilter(writer io.Writer) {
fmt.Fprint(writer, `<defs>
<filter id="shadow-filter" width="200%" height="200%" x="-50%" y="-50%">
<feGaussianBlur stdDeviation="1.7 " in="SourceGraphic"></feGaussianBlur>
2022-11-09 19:14:31 +00:00
<feFlood flood-color="#3d4574" flood-opacity="0.4" result="ShadowFeFlood" in="SourceGraphic"></feFlood>
<feComposite in="ShadowFeFlood" in2="SourceAlpha" operator="in" result="ShadowFeComposite"></feComposite>
<feOffset dx="3" dy="5" result="ShadowFeOffset" in="ShadowFeComposite"></feOffset>
<feBlend in="SourceGraphic" in2="ShadowFeOffset" mode="normal" result="ShadowFeBlend"></feBlend>
2022-11-09 19:10:51 +00:00
</filter>
</defs>`)
}
2025-01-26 21:11:53 +00:00
func render3DRect(diagramHash string, targetShape d2target.Shape, inlineTheme *d2themes.Theme) string {
moveTo := func(p d2target.Point) string {
2022-11-30 00:39:38 +00:00
return fmt.Sprintf("M%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
}
lineTo := func(p d2target.Point) string {
2022-11-30 00:39:38 +00:00
return fmt.Sprintf("L%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
}
// draw border all in one path to prevent overlapping sections
var borderSegments []string
borderSegments = append(borderSegments,
moveTo(d2target.Point{X: 0, Y: 0}),
)
for _, v := range []d2target.Point{
2023-01-19 19:51:30 +00:00
{X: d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET},
{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET},
{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: targetShape.Height - d2target.THREE_DEE_OFFSET},
{X: targetShape.Width, Y: targetShape.Height},
{X: 0, Y: targetShape.Height},
{X: 0, Y: 0},
{X: targetShape.Width, Y: 0},
{X: targetShape.Width, Y: targetShape.Height},
} {
borderSegments = append(borderSegments, lineTo(v))
}
// move to top right to draw last segment without overlapping
borderSegments = append(borderSegments,
moveTo(d2target.Point{X: targetShape.Width, Y: 0}),
)
borderSegments = append(borderSegments,
2023-01-19 19:51:30 +00:00
lineTo(d2target.Point{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET}),
)
2024-10-09 18:09:46 +00:00
border := d2themes.NewThemableElement("path", inlineTheme)
2023-01-09 18:16:28 +00:00
border.D = strings.Join(borderSegments, " ")
border.Fill = color.None
_, borderStroke := d2themes.ShapeTheme(targetShape)
2023-01-09 18:16:28 +00:00
border.Stroke = borderStroke
2023-01-15 20:36:43 +00:00
borderStyle := targetShape.CSSStyle()
2023-01-09 18:16:28 +00:00
border.Style = borderStyle
renderedBorder := border.Render()
// create mask from border stroke, to cut away from the shape fills
2025-01-26 21:11:53 +00:00
maskID := fmt.Sprintf("border-mask-%v-%v", diagramHash, svg.EscapeText(targetShape.ID))
borderMask := strings.Join([]string{
fmt.Sprintf(`<defs><mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
2023-01-19 19:51:30 +00:00
maskID, targetShape.Pos.X, targetShape.Pos.Y-d2target.THREE_DEE_OFFSET, targetShape.Width+d2target.THREE_DEE_OFFSET, targetShape.Height+d2target.THREE_DEE_OFFSET,
),
fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`,
2023-01-19 19:51:30 +00:00
targetShape.Pos.X, targetShape.Pos.Y-d2target.THREE_DEE_OFFSET, targetShape.Width+d2target.THREE_DEE_OFFSET, targetShape.Height+d2target.THREE_DEE_OFFSET,
),
fmt.Sprintf(`<path d="%s" style="%s;stroke:#000;fill:none;opacity:1;"/></mask></defs>`,
strings.Join(borderSegments, ""), borderStyle),
}, "\n")
// render the main rectangle without stroke and the border mask
2024-10-09 18:09:46 +00:00
mainShape := d2themes.NewThemableElement("rect", inlineTheme)
2023-01-09 18:16:28 +00:00
mainShape.X = float64(targetShape.Pos.X)
mainShape.Y = float64(targetShape.Pos.Y)
mainShape.Width = float64(targetShape.Width)
mainShape.Height = float64(targetShape.Height)
mainShape.SetMaskUrl(maskID)
mainShapeFill, _ := d2themes.ShapeTheme(targetShape)
2023-01-09 18:16:28 +00:00
mainShape.Fill = mainShapeFill
2023-03-14 06:01:33 +00:00
mainShape.FillPattern = targetShape.FillPattern
mainShape.Stroke = color.None
2023-01-15 20:36:43 +00:00
mainShape.Style = targetShape.CSSStyle()
2023-01-09 18:16:28 +00:00
mainShapeRendered := mainShape.Render()
// render the side shapes in the darkened color without stroke and the border mask
var sidePoints []string
for _, v := range []d2target.Point{
{X: 0, Y: 0},
2023-01-19 19:51:30 +00:00
{X: d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET},
{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET},
{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: targetShape.Height - d2target.THREE_DEE_OFFSET},
{X: targetShape.Width, Y: targetShape.Height},
{X: targetShape.Width, Y: 0},
} {
sidePoints = append(sidePoints,
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
}
2024-10-09 18:09:46 +00:00
sideShape := d2themes.NewThemableElement("polygon", inlineTheme)
sideShape.Fill = darkerColor
sideShape.Points = strings.Join(sidePoints, " ")
sideShape.SetMaskUrl(maskID)
sideShape.Style = targetShape.CSSStyle()
renderedSides := sideShape.Render()
return borderMask + mainShapeRendered + renderedSides + renderedBorder
}
2025-01-26 21:11:53 +00:00
func render3DHexagon(diagramHash string, targetShape d2target.Shape, inlineTheme *d2themes.Theme) string {
moveTo := func(p d2target.Point) string {
return fmt.Sprintf("M%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
}
lineTo := func(p d2target.Point) string {
return fmt.Sprintf("L%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
}
scale := func(n int, f float64) int {
return int(float64(n) * f)
}
halfYFactor := 43.6 / 87.3
// draw border all in one path to prevent overlapping sections
var borderSegments []string
// start from the top-left
borderSegments = append(borderSegments,
moveTo(d2target.Point{X: scale(targetShape.Width, 0.25), Y: 0}),
)
Y_OFFSET := d2target.THREE_DEE_OFFSET / 2
// The following iterates through the sidepoints in clockwise order from top-left, then the main points in clockwise order from bottom-right
for _, v := range []d2target.Point{
{X: scale(targetShape.Width, 0.25) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
{X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: scale(targetShape.Height, halfYFactor) - Y_OFFSET},
{X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: targetShape.Height - Y_OFFSET},
{X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
{X: scale(targetShape.Width, 0.25), Y: targetShape.Height},
{X: 0, Y: scale(targetShape.Height, halfYFactor)},
{X: scale(targetShape.Width, 0.25), Y: 0},
{X: scale(targetShape.Width, 0.75), Y: 0},
{X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
{X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
} {
borderSegments = append(borderSegments, lineTo(v))
}
for _, v := range []d2target.Point{
{X: scale(targetShape.Width, 0.75), Y: 0},
{X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
{X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
} {
borderSegments = append(borderSegments, moveTo(v))
borderSegments = append(borderSegments, lineTo(
d2target.Point{X: v.X + d2target.THREE_DEE_OFFSET, Y: v.Y - Y_OFFSET},
))
}
2024-10-09 18:09:46 +00:00
border := d2themes.NewThemableElement("path", inlineTheme)
border.D = strings.Join(borderSegments, " ")
border.Fill = color.None
_, borderStroke := d2themes.ShapeTheme(targetShape)
border.Stroke = borderStroke
borderStyle := targetShape.CSSStyle()
border.Style = borderStyle
renderedBorder := border.Render()
var mainPoints []string
for _, v := range []d2target.Point{
{X: scale(targetShape.Width, 0.25), Y: 0},
{X: scale(targetShape.Width, 0.75), Y: 0},
{X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
{X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
{X: scale(targetShape.Width, 0.25), Y: targetShape.Height},
{X: 0, Y: scale(targetShape.Height, halfYFactor)},
} {
mainPoints = append(mainPoints,
fmt.Sprintf("%d,%d", v.X+targetShape.Pos.X, v.Y+targetShape.Pos.Y),
)
}
mainPointsPoly := strings.Join(mainPoints, " ")
// create mask from border stroke, to cut away from the shape fills
2025-01-26 21:11:53 +00:00
maskID := fmt.Sprintf("border-mask-%v-%v", diagramHash, svg.EscapeText(targetShape.ID))
borderMask := strings.Join([]string{
fmt.Sprintf(`<defs><mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
maskID, targetShape.Pos.X, targetShape.Pos.Y-d2target.THREE_DEE_OFFSET, targetShape.Width+d2target.THREE_DEE_OFFSET, targetShape.Height+d2target.THREE_DEE_OFFSET,
),
fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`,
targetShape.Pos.X, targetShape.Pos.Y-d2target.THREE_DEE_OFFSET, targetShape.Width+d2target.THREE_DEE_OFFSET, targetShape.Height+d2target.THREE_DEE_OFFSET,
),
fmt.Sprintf(`<path d="%s" style="%s;stroke:#000;fill:none;opacity:1;"/></mask></defs>`,
strings.Join(borderSegments, ""), borderStyle),
}, "\n")
// render the main hexagon without stroke and the border mask
2024-10-09 18:09:46 +00:00
mainShape := d2themes.NewThemableElement("polygon", inlineTheme)
mainShape.X = float64(targetShape.Pos.X)
mainShape.Y = float64(targetShape.Pos.Y)
mainShape.Points = mainPointsPoly
mainShape.SetMaskUrl(maskID)
mainShapeFill, _ := d2themes.ShapeTheme(targetShape)
2023-03-14 06:01:33 +00:00
mainShape.FillPattern = targetShape.FillPattern
mainShape.Fill = mainShapeFill
mainShape.Stroke = color.None
mainShape.Style = targetShape.CSSStyle()
mainShapeRendered := mainShape.Render()
// render the side shapes in the darkened color without stroke and the border mask
var sidePoints []string
for _, v := range []d2target.Point{
{X: scale(targetShape.Width, 0.25) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
{X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: scale(targetShape.Height, halfYFactor) - Y_OFFSET},
{X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: targetShape.Height - Y_OFFSET},
{X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
{X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
{X: scale(targetShape.Width, 0.75), Y: 0},
{X: scale(targetShape.Width, 0.25), Y: 0},
} {
sidePoints = append(sidePoints,
fmt.Sprintf("%d,%d", v.X+targetShape.Pos.X, v.Y+targetShape.Pos.Y),
)
}
// TODO make darker color part of the theme? or just keep this bypass
darkerColor, err := color.Darken(targetShape.Fill)
if err != nil {
darkerColor = targetShape.Fill
}
2024-10-09 18:09:46 +00:00
sideShape := d2themes.NewThemableElement("polygon", inlineTheme)
sideShape.Fill = darkerColor
2023-01-09 18:16:28 +00:00
sideShape.Points = strings.Join(sidePoints, " ")
sideShape.SetMaskUrl(maskID)
2023-01-15 20:36:43 +00:00
sideShape.Style = targetShape.CSSStyle()
renderedSides := sideShape.Render()
2023-01-09 18:16:28 +00:00
return borderMask + mainShapeRendered + renderedSides + renderedBorder
}
2025-01-15 23:30:17 +00:00
func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape d2target.Shape, jsRunner jsrunner.JSRunner, inlineTheme *d2themes.Theme) (labelMask string, err error) {
2022-12-29 00:19:30 +00:00
closingTag := "</g>"
if targetShape.Link != "" {
2023-03-07 04:07:26 +00:00
fmt.Fprintf(writer, `<a href="%s" xlink:href="%[1]s">`, svg.EscapeText(targetShape.Link))
2022-12-29 00:19:30 +00:00
closingTag += "</a>"
}
2023-01-19 08:46:30 +00:00
// Opacity is a unique style, it applies to everything for a shape
opacityStyle := ""
if targetShape.Opacity != 1.0 {
opacityStyle = fmt.Sprintf(" style='opacity:%f'", targetShape.Opacity)
}
2023-03-06 05:00:40 +00:00
// this clipPath must be defined outside `g` element
if targetShape.BorderRadius != 0 && (targetShape.Type == d2target.ShapeClass || targetShape.Type == d2target.ShapeSQLTable) {
2023-03-10 01:54:35 +00:00
fmt.Fprint(writer, clipPathForBorderRadius(diagramHash, targetShape))
2023-03-06 05:00:40 +00:00
}
var iconClipPathID string
if targetShape.IconBorderRadius != 0 && (targetShape.Type == d2target.ShapeImage) {
// Set the icon's border-radius to half of it's smaller dimension in case it exceeds that
// https://www.w3.org/Style/CSS/Tracker/issues/29?changelog
targetShape.IconBorderRadius = min(targetShape.IconBorderRadius, min(targetShape.Width, targetShape.Height)/2)
iconClipPathID = fmt.Sprintf("%v-%v-icon", diagramHash, svg.SVGID(targetShape.ID))
fmt.Fprint(writer, applyIconBorderRadius(iconClipPathID, targetShape))
}
classes := []string{base64.URLEncoding.EncodeToString([]byte(svg.EscapeText(targetShape.ID)))}
2024-12-15 02:35:18 +00:00
if targetShape.Animated {
classes = append(classes, "animated-shape")
2023-02-06 21:32:08 +00:00
}
classes = append(classes, targetShape.Classes...)
classStr := fmt.Sprintf(` class="%s"`, strings.Join(classes, " "))
2025-02-04 17:25:03 +00:00
fmt.Fprintf(writer, `<g%s%s>`, classStr, opacityStyle)
tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y))
width := float64(targetShape.Width)
height := float64(targetShape.Height)
fill, stroke := d2themes.ShapeTheme(targetShape)
2023-01-12 19:22:53 +00:00
style := targetShape.CSSStyle()
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[targetShape.Type]
s := shape.NewShape(shapeType, geo.NewBox(tl, width, height))
2023-11-17 02:08:33 +00:00
if shapeType == shape.CLOUD_TYPE && targetShape.ContentAspectRatio != nil {
s.SetInnerBoxAspectRatio(*targetShape.ContentAspectRatio)
}
2022-11-10 03:24:27 +00:00
var shadowAttr string
2022-11-10 19:21:14 +00:00
if targetShape.Shadow {
2022-11-10 03:24:27 +00:00
switch targetShape.Type {
case d2target.ShapeText,
d2target.ShapeCode,
d2target.ShapeClass,
d2target.ShapeSQLTable:
default:
shadowAttr = `filter="url(#shadow-filter)" `
}
}
2022-12-05 19:22:16 +00:00
var blendModeClass string
if targetShape.Blend {
blendModeClass = " blend"
}
fmt.Fprintf(writer, `<g class="shape%s" %s>`, blendModeClass, shadowAttr)
2022-11-10 03:24:27 +00:00
2022-11-09 03:40:20 +00:00
var multipleTL *geo.Point
if targetShape.Multiple {
multipleTL = tl.AddVector(multipleOffset)
2022-11-09 03:40:20 +00:00
}
switch targetShape.Type {
case d2target.ShapeClass:
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out, err := d2sketch.Class(jsRunner, targetShape)
2022-12-22 19:32:41 +00:00
if err != nil {
return "", err
}
2023-01-09 18:16:28 +00:00
fmt.Fprint(writer, out)
2022-12-22 19:32:41 +00:00
} else {
2024-10-09 18:09:46 +00:00
drawClass(writer, diagramHash, targetShape, inlineTheme)
2022-12-22 19:32:41 +00:00
}
2025-01-28 16:16:14 +00:00
addAppendixItems(appendixWriter, diagramHash, targetShape, s)
fmt.Fprint(writer, `</g>`)
2023-01-09 18:16:28 +00:00
fmt.Fprint(writer, closingTag)
2022-12-05 19:57:16 +00:00
return labelMask, nil
case d2target.ShapeSQLTable:
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out, err := d2sketch.Table(jsRunner, targetShape)
2022-12-22 19:06:57 +00:00
if err != nil {
return "", err
}
2023-01-09 18:16:28 +00:00
fmt.Fprint(writer, out)
2022-12-22 19:06:57 +00:00
} else {
2024-10-09 18:09:46 +00:00
drawTable(writer, diagramHash, targetShape, inlineTheme)
2022-12-22 19:06:57 +00:00
}
2025-01-28 16:16:14 +00:00
addAppendixItems(appendixWriter, diagramHash, targetShape, s)
fmt.Fprint(writer, `</g>`)
2023-01-09 18:16:28 +00:00
fmt.Fprint(writer, closingTag)
2022-12-05 19:57:16 +00:00
return labelMask, nil
case d2target.ShapeOval:
2023-01-19 07:12:26 +00:00
if targetShape.DoubleBorder {
if targetShape.Multiple {
2024-10-09 18:09:46 +00:00
fmt.Fprint(writer, renderDoubleOval(multipleTL, width, height, fill, "", stroke, style, inlineTheme))
2022-12-21 07:43:45 +00:00
}
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out, err := d2sketch.DoubleOval(jsRunner, targetShape)
if err != nil {
return "", err
}
fmt.Fprint(writer, out)
} else {
2024-10-09 18:09:46 +00:00
fmt.Fprint(writer, renderDoubleOval(tl, width, height, fill, targetShape.FillPattern, stroke, style, inlineTheme))
2022-12-21 07:43:45 +00:00
}
} else {
if targetShape.Multiple {
2024-10-09 18:09:46 +00:00
fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, "", stroke, style, inlineTheme))
}
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out, err := d2sketch.Oval(jsRunner, targetShape)
if err != nil {
return "", err
}
fmt.Fprint(writer, out)
} else {
2024-10-09 18:09:46 +00:00
fmt.Fprint(writer, renderOval(tl, width, height, fill, targetShape.FillPattern, stroke, style, inlineTheme))
}
2022-12-21 07:43:45 +00:00
}
case d2target.ShapeImage:
2024-10-09 18:09:46 +00:00
el := d2themes.NewThemableElement("image", inlineTheme)
2023-01-09 18:16:28 +00:00
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
if targetShape.IconBorderRadius != 0 {
el.ClipPath = iconClipPathID
}
2023-01-09 18:16:28 +00:00
fmt.Fprint(writer, el.Render())
2022-11-10 03:24:27 +00:00
// TODO should standardize "" to rectangle
2024-03-15 17:00:40 +00:00
case d2target.ShapeRectangle, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "":
2023-04-06 16:53:54 +00:00
borderRadius := math.MaxFloat64
if targetShape.BorderRadius != 0 {
borderRadius = float64(targetShape.BorderRadius)
2023-04-06 16:53:54 +00:00
}
2022-11-10 19:21:14 +00:00
if targetShape.ThreeDee {
2025-01-26 21:11:53 +00:00
fmt.Fprint(writer, render3DRect(diagramHash, targetShape, inlineTheme))
} else {
2022-12-31 07:57:22 +00:00
if !targetShape.DoubleBorder {
if targetShape.Multiple {
2024-10-09 18:09:46 +00:00
el := d2themes.NewThemableElement("rect", inlineTheme)
2023-01-27 21:30:44 +00:00
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
2023-04-06 16:53:54 +00:00
el.Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt.Fprint(writer, el.Render())
2022-12-31 07:57:22 +00:00
}
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out, err := d2sketch.Rect(jsRunner, targetShape)
2022-12-31 07:57:22 +00:00
if err != nil {
return "", err
}
2023-01-27 21:30:44 +00:00
fmt.Fprint(writer, out)
2022-12-31 07:57:22 +00:00
} else {
2024-10-09 18:09:46 +00:00
el := d2themes.NewThemableElement("rect", inlineTheme)
2023-01-27 21:30:44 +00:00
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
2023-03-14 06:01:33 +00:00
el.FillPattern = targetShape.FillPattern
2023-01-27 21:30:44 +00:00
el.Stroke = stroke
el.Style = style
2023-04-06 16:53:54 +00:00
el.Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt.Fprint(writer, el.Render())
2022-12-21 07:43:45 +00:00
}
} else {
2022-12-31 07:57:22 +00:00
if targetShape.Multiple {
2024-10-09 18:09:46 +00:00
el := d2themes.NewThemableElement("rect", inlineTheme)
2023-01-27 21:30:44 +00:00
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
2023-03-14 06:01:33 +00:00
el.FillPattern = targetShape.FillPattern
2023-01-27 21:30:44 +00:00
el.Stroke = stroke
el.Style = style
2023-04-06 16:53:54 +00:00
el.Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt.Fprint(writer, el.Render())
2024-10-09 18:09:46 +00:00
el = d2themes.NewThemableElement("rect", inlineTheme)
2023-01-27 21:30:44 +00:00
el.X = float64(targetShape.Pos.X + 10 + d2target.INNER_BORDER_OFFSET)
el.Y = float64(targetShape.Pos.Y - 10 + d2target.INNER_BORDER_OFFSET)
el.Width = float64(targetShape.Width - 2*d2target.INNER_BORDER_OFFSET)
el.Height = float64(targetShape.Height - 2*d2target.INNER_BORDER_OFFSET)
el.Fill = fill
el.Stroke = stroke
el.Style = style
2023-04-06 16:53:54 +00:00
el.Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt.Fprint(writer, el.Render())
2022-12-31 07:57:22 +00:00
}
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out, err := d2sketch.DoubleRect(jsRunner, targetShape)
2022-12-31 07:57:22 +00:00
if err != nil {
return "", err
}
2023-01-27 21:30:44 +00:00
fmt.Fprint(writer, out)
2022-12-31 07:57:22 +00:00
} else {
2024-10-09 18:09:46 +00:00
el := d2themes.NewThemableElement("rect", inlineTheme)
2023-01-27 21:30:44 +00:00
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
2023-03-14 06:01:33 +00:00
el.FillPattern = targetShape.FillPattern
2023-01-27 21:30:44 +00:00
el.Stroke = stroke
el.Style = style
2023-04-06 16:53:54 +00:00
el.Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt.Fprint(writer, el.Render())
2024-10-09 18:09:46 +00:00
el = d2themes.NewThemableElement("rect", inlineTheme)
2023-01-27 21:30:44 +00:00
el.X = float64(targetShape.Pos.X + d2target.INNER_BORDER_OFFSET)
el.Y = float64(targetShape.Pos.Y + d2target.INNER_BORDER_OFFSET)
el.Width = float64(targetShape.Width - 2*d2target.INNER_BORDER_OFFSET)
el.Height = float64(targetShape.Height - 2*d2target.INNER_BORDER_OFFSET)
2023-03-14 06:12:41 +00:00
el.Fill = "transparent"
2023-01-27 21:30:44 +00:00
el.Stroke = stroke
el.Style = style
2023-04-06 16:53:54 +00:00
el.Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt.Fprint(writer, el.Render())
2022-12-31 07:57:22 +00:00
}
2022-12-21 07:43:45 +00:00
}
2022-11-09 03:40:20 +00:00
}
case d2target.ShapeHexagon:
if targetShape.ThreeDee {
2025-01-26 21:11:53 +00:00
fmt.Fprint(writer, render3DHexagon(diagramHash, targetShape, inlineTheme))
} else {
if targetShape.Multiple {
multiplePathData := shape.NewShape(shapeType, geo.NewBox(multipleTL, width, height)).GetSVGPathData()
2024-10-09 18:09:46 +00:00
el := d2themes.NewThemableElement("path", inlineTheme)
el.Fill = fill
el.Stroke = stroke
el.Style = style
for _, pathData := range multiplePathData {
el.D = pathData
fmt.Fprint(writer, el.Render())
}
}
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out, err := d2sketch.Paths(jsRunner, targetShape, s.GetSVGPathData())
if err != nil {
return "", err
}
fmt.Fprint(writer, out)
} else {
2024-10-09 18:09:46 +00:00
el := d2themes.NewThemableElement("path", inlineTheme)
el.Fill = fill
2023-03-14 06:01:33 +00:00
el.FillPattern = targetShape.FillPattern
el.Stroke = stroke
el.Style = style
for _, pathData := range s.GetSVGPathData() {
el.D = pathData
fmt.Fprint(writer, el.Render())
}
}
}
2022-11-10 03:24:27 +00:00
case d2target.ShapeText, d2target.ShapeCode:
default:
2022-11-09 03:40:20 +00:00
if targetShape.Multiple {
multiplePathData := shape.NewShape(shapeType, geo.NewBox(multipleTL, width, height)).GetSVGPathData()
2024-10-09 18:09:46 +00:00
el := d2themes.NewThemableElement("path", inlineTheme)
2023-01-09 18:16:28 +00:00
el.Fill = fill
el.Stroke = stroke
el.Style = style
2022-11-09 03:40:20 +00:00
for _, pathData := range multiplePathData {
2023-01-09 18:16:28 +00:00
el.D = pathData
fmt.Fprint(writer, el.Render())
2022-11-09 03:40:20 +00:00
}
}
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out, err := d2sketch.Paths(jsRunner, targetShape, s.GetSVGPathData())
2022-12-21 07:43:45 +00:00
if err != nil {
return "", err
}
2023-01-09 18:16:28 +00:00
fmt.Fprint(writer, out)
2022-12-21 07:43:45 +00:00
} else {
2024-10-09 18:09:46 +00:00
el := d2themes.NewThemableElement("path", inlineTheme)
2023-01-09 18:16:28 +00:00
el.Fill = fill
2023-03-14 06:01:33 +00:00
el.FillPattern = targetShape.FillPattern
2023-01-09 18:16:28 +00:00
el.Stroke = stroke
el.Style = style
2022-12-21 07:43:45 +00:00
for _, pathData := range s.GetSVGPathData() {
2023-01-09 18:16:28 +00:00
el.D = pathData
fmt.Fprint(writer, el.Render())
2022-12-21 07:43:45 +00:00
}
}
}
2023-11-16 04:02:22 +00:00
// // to examine shape's innerBox
// innerBox := s.GetInnerBox()
2024-10-09 18:09:46 +00:00
// el := d2themes.NewThemableElement("rect", inlineTheme)
2023-11-16 04:02:22 +00:00
// el.X = float64(innerBox.TopLeft.X)
// el.Y = float64(innerBox.TopLeft.Y)
// el.Width = float64(innerBox.Width)
// el.Height = float64(innerBox.Height)
// el.Style = "fill:rgba(255,0,0,0.5);"
// fmt.Fprint(writer, el.Render())
2023-02-11 00:19:19 +00:00
2022-12-29 00:19:30 +00:00
// Closes the class=shape
fmt.Fprint(writer, `</g>`)
2022-11-10 03:24:27 +00:00
if targetShape.Icon != nil && targetShape.Type != d2target.ShapeImage && targetShape.Opacity != 0 {
2023-07-17 21:21:36 +00:00
iconPosition := label.FromString(targetShape.IconPosition)
var box *geo.Box
if iconPosition.IsOutside() {
box = s.GetBox()
} else {
box = s.GetInnerBox()
}
2023-02-13 18:42:47 +00:00
iconSize := d2target.GetIconSize(box, targetShape.IconPosition)
tl := iconPosition.GetPointOnBox(box, label.PADDING, float64(iconSize), float64(iconSize))
shapeIconClipPath := ""
if targetShape.IconBorderRadius != 0 {
shapeIconClipPath = fmt.Sprintf(` clip-path="inset(0 round %dpx)"`, targetShape.IconBorderRadius)
2025-03-18 17:33:35 +00:00
}
fmt.Fprintf(writer, `<image href="%s" x="%f" y="%f" width="%d" height="%d"%s />`,
html.EscapeString(targetShape.Icon.String()),
tl.X,
tl.Y,
iconSize,
iconSize,
shapeIconClipPath,
)
}
if targetShape.Label != "" && targetShape.Opacity != 0 {
2023-07-17 21:21:36 +00:00
labelPosition := label.FromString(targetShape.LabelPosition)
var box *geo.Box
if labelPosition.IsOutside() {
2023-05-25 02:39:39 +00:00
box = s.GetBox().Copy()
// if it is 3d/multiple, place label using box around those
2023-05-24 02:25:27 +00:00
if targetShape.ThreeDee {
2023-05-25 02:39:39 +00:00
offsetY := d2target.THREE_DEE_OFFSET
if targetShape.Type == d2target.ShapeHexagon {
offsetY /= 2
}
box.TopLeft.Y -= float64(offsetY)
box.Height += float64(offsetY)
2023-05-24 02:25:27 +00:00
box.Width += d2target.THREE_DEE_OFFSET
} else if targetShape.Multiple {
box.TopLeft.Y -= d2target.MULTIPLE_OFFSET
box.Height += d2target.MULTIPLE_OFFSET
2023-05-24 02:25:27 +00:00
box.Width += d2target.MULTIPLE_OFFSET
}
} else {
box = s.GetInnerBox()
}
labelTL := labelPosition.GetPointOnBox(box, label.PADDING,
float64(targetShape.LabelWidth),
float64(targetShape.LabelHeight),
)
labelMask = makeLabelMask(labelTL, targetShape.LabelWidth, targetShape.LabelHeight, 0.75)
fontClass := "text"
2023-03-07 06:21:23 +00:00
if targetShape.FontFamily == "mono" {
fontClass = "text-mono"
2023-07-12 05:04:01 +00:00
}
if targetShape.Bold {
fontClass += "-bold"
} else if targetShape.Italic {
fontClass += "-italic"
}
if targetShape.Underline {
fontClass += " text-underline"
}
if targetShape.Language == "latex" {
render, err := d2latex.Render(targetShape.Label)
if err != nil {
return labelMask, err
}
gEl := d2themes.NewThemableElement("g", inlineTheme)
2025-03-02 05:22:33 +00:00
labelPosition := label.FromString(targetShape.LabelPosition)
if labelPosition == label.Unset {
labelPosition = label.InsideMiddleCenter
}
var box *geo.Box
if labelPosition.IsOutside() {
box = s.GetBox()
} else {
box = s.GetInnerBox()
}
labelTL := labelPosition.GetPointOnBox(box, label.PADDING,
float64(targetShape.LabelWidth),
float64(targetShape.LabelHeight),
)
gEl.SetTranslate(labelTL.X, labelTL.Y)
gEl.Color = targetShape.Stroke
gEl.Content = render
fmt.Fprint(writer, gEl.Render())
} else if targetShape.Language == "markdown" {
render, err := textmeasure.RenderMarkdown(targetShape.Label)
if err != nil {
return labelMask, err
}
2025-03-02 05:22:33 +00:00
labelPosition := label.FromString(targetShape.LabelPosition)
if labelPosition == label.Unset {
labelPosition = label.InsideMiddleCenter
}
var box *geo.Box
if labelPosition.IsOutside() {
box = s.GetBox()
} else {
box = s.GetInnerBox()
}
labelTL := labelPosition.GetPointOnBox(box, label.PADDING,
float64(targetShape.LabelWidth),
float64(targetShape.LabelHeight),
)
fmt.Fprintf(writer, `<g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="%f" y="%f" width="%d" height="%d">`,
2025-03-02 05:22:33 +00:00
labelTL.X, labelTL.Y, targetShape.LabelWidth, targetShape.LabelHeight,
)
2025-03-02 05:22:33 +00:00
// we need the self closing form in this svg/xhtml context
render = strings.ReplaceAll(render, "<hr>", "<hr />")
2024-11-08 21:58:03 +00:00
mdEl := d2themes.NewThemableElement("div", inlineTheme)
mdEl.Content = render
// We have to set with styles since within foreignObject, we're in html
// land and not SVG attributes
var styles []string
2025-03-16 15:49:00 +00:00
var classes []string = []string{"md"}
if targetShape.FontSize != textmeasure.MarkdownFontSize {
styles = append(styles, fmt.Sprintf("font-size:%vpx", targetShape.FontSize))
}
2025-03-16 15:49:00 +00:00
if targetShape.Fill != "" && targetShape.Fill != "transparent" {
2025-03-16 15:49:00 +00:00
if color.IsThemeColor(targetShape.Fill) {
classes = append(classes, fmt.Sprintf("fill-%s", targetShape.Fill))
} else {
styles = append(styles, fmt.Sprintf(`background-color:%s`, targetShape.Fill))
}
}
2025-03-16 15:49:00 +00:00
if !color.IsThemeColor(targetShape.Color) {
styles = append(styles, fmt.Sprintf(`color:%s`, targetShape.Color))
2025-03-04 01:33:00 +00:00
} else {
2025-03-16 15:49:00 +00:00
classes = append(classes, fmt.Sprintf("color-%s", targetShape.Color))
}
2025-03-16 15:49:00 +00:00
mdEl.ClassName = strings.Join(classes, " ")
// When using dark theme, inlineTheme is nil and we rely on CSS variables
mdEl.Style = strings.Join(styles, ";")
fmt.Fprint(writer, mdEl.Render())
fmt.Fprint(writer, `</foreignObject></g>`)
} else if targetShape.Language != "" {
2022-11-27 21:54:41 +00:00
lexer := lexers.Get(targetShape.Language)
if lexer == nil {
2023-03-04 05:25:06 +00:00
lexer = lexers.Fallback
2022-11-27 21:54:41 +00:00
}
2023-03-04 05:02:02 +00:00
for _, isLight := range []bool{true, false} {
theme := "github"
if !isLight {
theme = "catppuccin-mocha"
}
style := styles.Get(theme)
if style == nil {
return labelMask, errors.New(`code snippet style "github" not found`)
}
formatter := formatters.Get("svg")
if formatter == nil {
return labelMask, errors.New(`code snippet formatter "svg" not found`)
}
iterator, err := lexer.Tokenise(nil, targetShape.Label)
if err != nil {
return labelMask, err
}
2022-11-27 21:54:41 +00:00
2023-03-04 05:02:02 +00:00
svgStyles := styleToSVG(style)
class := "light-code"
if !isLight {
class = "dark-code"
}
var fontSize string
if targetShape.FontSize != d2fonts.FONT_SIZE_M {
fontSize = fmt.Sprintf(` style="font-size:%v"`, targetShape.FontSize)
}
fmt.Fprintf(writer, `<g transform="translate(%f %f)" class="%s"%s>`,
box.TopLeft.X, box.TopLeft.Y, class, fontSize,
)
2024-10-09 18:09:46 +00:00
rectEl := d2themes.NewThemableElement("rect", inlineTheme)
2023-03-04 05:02:02 +00:00
rectEl.Width = float64(targetShape.Width)
rectEl.Height = float64(targetShape.Height)
rectEl.Stroke = targetShape.Stroke
rectEl.ClassName = "shape"
2023-06-19 21:17:34 +00:00
rectEl.Style = fmt.Sprintf(`fill:%s;stroke-width:%d;`,
style.Get(chroma.Background).Background.String(),
targetShape.StrokeWidth,
)
2023-03-04 05:02:02 +00:00
fmt.Fprint(writer, rectEl.Render())
2023-06-19 21:17:34 +00:00
// Padding = 0.5em
padding := float64(targetShape.FontSize) / 2.
fmt.Fprintf(writer, `<g transform="translate(%f %f)">`, padding, padding)
2023-03-04 05:02:02 +00:00
2023-06-19 22:25:14 +00:00
lineHeight := textmeasure.CODE_LINE_HEIGHT
2023-03-04 05:02:02 +00:00
for index, tokens := range chroma.SplitTokensIntoLines(iterator.Tokens()) {
2023-06-19 22:25:14 +00:00
fmt.Fprintf(writer, "<text class=\"text-mono\" x=\"0\" y=\"%fem\">", 1+float64(index)*lineHeight)
2023-03-04 05:02:02 +00:00
for _, token := range tokens {
text := svgEscaper.Replace(token.String())
attr := styleAttr(svgStyles, token.Type)
if attr != "" {
text = fmt.Sprintf("<tspan %s>%s</tspan>", attr, text)
}
fmt.Fprint(writer, text)
2022-11-27 21:54:41 +00:00
}
2023-03-04 05:02:02 +00:00
fmt.Fprint(writer, "</text>")
2022-11-27 21:54:41 +00:00
}
2023-03-04 05:02:02 +00:00
fmt.Fprint(writer, "</g></g>")
2022-11-27 21:54:41 +00:00
}
} else {
2023-02-09 22:14:31 +00:00
if targetShape.LabelFill != "" {
2024-10-09 18:09:46 +00:00
rectEl := d2themes.NewThemableElement("rect", inlineTheme)
2023-02-19 11:32:44 +00:00
rectEl.X = labelTL.X
rectEl.Y = labelTL.Y
rectEl.Width = float64(targetShape.LabelWidth)
rectEl.Height = float64(targetShape.LabelHeight)
rectEl.Fill = targetShape.LabelFill
fmt.Fprint(writer, rectEl.Render())
2023-02-09 22:14:31 +00:00
}
2024-10-09 18:09:46 +00:00
textEl := d2themes.NewThemableElement("text", inlineTheme)
2023-01-09 18:16:28 +00:00
textEl.X = labelTL.X + float64(targetShape.LabelWidth)/2
// text is vertically positioned at its baseline which is at labelTL+FontSize
2023-01-09 18:16:28 +00:00
textEl.Y = labelTL.Y + float64(targetShape.FontSize)
2023-02-25 04:26:40 +00:00
textEl.Fill = targetShape.GetFontColor()
textEl.ClassName = fontClass
2023-01-09 18:16:28 +00:00
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())
2022-12-05 23:53:43 +00:00
if targetShape.Blend {
2023-06-08 22:54:17 +00:00
labelMask = makeLabelMask(labelTL, targetShape.LabelWidth, targetShape.LabelHeight-d2graph.INNER_LABEL_PADDING, 1)
}
}
}
2023-07-27 05:46:28 +00:00
if targetShape.Tooltip != "" {
fmt.Fprintf(writer, `<title>%s</title>`,
svg.EscapeText(targetShape.Tooltip),
)
}
2025-01-28 16:16:14 +00:00
addAppendixItems(appendixWriter, diagramHash, targetShape, s)
2023-01-13 16:16:18 +00:00
fmt.Fprint(writer, closingTag)
2023-01-13 16:16:18 +00:00
return labelMask, nil
}
2025-03-29 16:39:11 +00:00
func applyIconBorderRadius(clipPathID string, shape d2target.Shape) string {
box := geo.NewBox(
geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)),
float64(shape.Width),
float64(shape.Height),
)
topX, topY := box.TopLeft.X+box.Width, box.TopLeft.Y
2025-03-29 16:39:11 +00:00
out := fmt.Sprintf(`<clipPath id="%s">`, clipPathID)
out += fmt.Sprintf(`<path d="M %f %f L %f %f S %f %f %f %f `, box.TopLeft.X, box.TopLeft.Y+float64(shape.IconBorderRadius), box.TopLeft.X, box.TopLeft.Y+float64(shape.IconBorderRadius), box.TopLeft.X, box.TopLeft.Y, box.TopLeft.X+float64(shape.IconBorderRadius), box.TopLeft.Y)
out += fmt.Sprintf(`L %f %f L %f %f `, box.TopLeft.X+box.Width-float64(shape.IconBorderRadius), box.TopLeft.Y, topX-float64(shape.IconBorderRadius), topY)
out += fmt.Sprintf(`S %f %f %f %f `, topX, topY, topX, topY+float64(shape.IconBorderRadius))
out += fmt.Sprintf(`L %f %f `, topX, topY+box.Height-float64(shape.IconBorderRadius))
out += fmt.Sprintf(`S %f % f %f %f `, topX, topY+box.Height, topX-float64(shape.IconBorderRadius), topY+box.Height)
out += fmt.Sprintf(`L %f %f `, box.TopLeft.X+float64(shape.IconBorderRadius), box.TopLeft.Y+box.Height)
out += fmt.Sprintf(`S %f %f %f %f`, box.TopLeft.X, box.TopLeft.Y+box.Height, box.TopLeft.X, box.TopLeft.Y+box.Height-float64(shape.IconBorderRadius))
out += fmt.Sprintf(`L %f %f`, box.TopLeft.X, box.TopLeft.Y+float64(shape.IconBorderRadius))
out += fmt.Sprintf(`Z %f %f" `, box.TopLeft.X, box.TopLeft.Y)
return out + `fill="none" /> </clipPath>`
}
2025-01-28 16:16:14 +00:00
func addAppendixItems(writer io.Writer, diagramHash string, targetShape d2target.Shape, s shape.Shape) {
var p1, p2 *geo.Point
if targetShape.Tooltip != "" || targetShape.Link != "" {
bothIcons := targetShape.Tooltip != "" && targetShape.Link != ""
corner := geo.NewPoint(float64(targetShape.Pos.X+targetShape.Width), float64(targetShape.Pos.Y))
center := geo.NewPoint(
float64(targetShape.Pos.X)+float64(targetShape.Width)/2.,
float64(targetShape.Pos.Y)+float64(targetShape.Height)/2.,
)
offset := geo.Vector{-2 * appendixIconRadius, 0}
var leftOnShape bool
2023-07-04 01:15:57 +00:00
switch s.GetType() {
case shape.STEP_TYPE, shape.HEXAGON_TYPE, shape.QUEUE_TYPE, shape.PAGE_TYPE:
2023-07-04 01:15:57 +00:00
// trace straight left for these
center.Y = float64(targetShape.Pos.Y)
case shape.PACKAGE_TYPE:
// trace straight down
center.X = float64(targetShape.Pos.X + targetShape.Width)
case shape.CIRCLE_TYPE, shape.OVAL_TYPE, shape.DIAMOND_TYPE,
shape.PERSON_TYPE, shape.CLOUD_TYPE, shape.CYLINDER_TYPE:
if bothIcons {
leftOnShape = true
corner = corner.AddVector(offset)
}
2023-07-04 01:15:57 +00:00
}
v1 := center.VectorTo(corner)
p1 = shape.TraceToShapeBorder(s, corner, corner.AddVector(v1))
if bothIcons {
if leftOnShape {
// these shapes should have p1 on shape border
p2 = p1.AddVector(offset.Reverse())
p1, p2 = p2, p1
} else {
p2 = p1.AddVector(offset)
}
}
}
if targetShape.Tooltip != "" {
x := int(math.Ceil(p1.X))
y := int(math.Ceil(p1.Y))
2023-07-27 05:46:28 +00:00
fmt.Fprintf(writer, `<g transform="translate(%d %d)" class="appendix-icon"><title>%s</title>%s</g>`,
x-appendixIconRadius,
y-appendixIconRadius,
2023-07-27 05:46:28 +00:00
svg.EscapeText(targetShape.Tooltip),
2025-01-28 16:16:14 +00:00
fmt.Sprintf(TooltipIcon, diagramHash, svg.SVGID(targetShape.ID)),
2022-12-27 07:56:23 +00:00
)
}
if targetShape.Link != "" {
if p2 == nil {
p2 = p1
}
x := int(math.Ceil(p2.X))
y := int(math.Ceil(p2.Y))
2022-12-29 00:19:30 +00:00
fmt.Fprintf(writer, `<g transform="translate(%d %d)" class="appendix-icon">%s</g>`,
x-appendixIconRadius,
y-appendixIconRadius,
2025-01-28 17:21:42 +00:00
fmt.Sprintf(LinkIcon, diagramHash, svg.SVGID(targetShape.ID)),
2022-12-29 00:19:30 +00:00
)
}
}
2022-12-28 04:29:51 +00:00
func RenderText(text string, x, height float64) string {
if !strings.Contains(text, "\n") {
2022-12-22 19:06:57 +00:00
return svg.EscapeText(text)
}
rendered := []string{}
lines := strings.Split(text, "\n")
for i, line := range lines {
dy := height / float64(len(lines))
if i == 0 {
dy = 0
}
2022-12-22 19:06:57 +00:00
escaped := svg.EscapeText(line)
if escaped == "" {
// if there are multiple newlines in a row we still need text for the tspan to render
escaped = " "
}
rendered = append(rendered, fmt.Sprintf(`<tspan x="%f" dy="%f">%s</tspan>`, x, dy, escaped))
}
return strings.Join(rendered, "")
}
2023-03-29 17:03:44 +00:00
func EmbedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fonts.FontFamily, corpus string) {
fmt.Fprint(buf, `<style type="text/css"><![CDATA[`)
appendOnTrigger(
buf,
source,
[]string{
`class="text"`,
`class="text `,
`class="md"`,
2025-03-16 15:49:00 +00:00
`class="md `,
},
fmt.Sprintf(`
.%s .text {
font-family: "%s-font-regular";
2023-02-21 08:33:13 +00:00
}
@font-face {
font-family: %s-font-regular;
2023-02-21 08:33:13 +00:00
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
2023-03-29 17:03:44 +00:00
fontFamily.Font(0, d2fonts.FONT_STYLE_REGULAR).GetEncodedSubset(corpus),
),
)
2023-01-12 18:51:26 +00:00
2023-04-10 21:38:34 +00:00
appendOnTrigger(
buf,
source,
2025-03-16 15:49:00 +00:00
[]string{
`class="md"`,
`class="md `,
},
2023-04-10 21:38:34 +00:00
fmt.Sprintf(`
@font-face {
font-family: %s-font-semibold;
src: url("%s");
}`,
2023-04-10 21:38:34 +00:00
diagramHash,
fontFamily.Font(0, d2fonts.FONT_STYLE_SEMIBOLD).GetEncodedSubset(corpus),
),
)
appendOnTrigger(
buf,
source,
[]string{
`text-underline`,
},
`
2023-02-21 08:33:13 +00:00
.text-underline {
text-decoration: underline;
}`,
)
2022-12-27 07:56:23 +00:00
2024-05-30 01:45:36 +00:00
appendOnTrigger(
buf,
source,
[]string{
`text-link`,
},
`
2024-05-31 04:57:31 +00:00
.text-link {
fill: blue;
}
2024-05-31 04:44:40 +00:00
2024-05-31 04:57:31 +00:00
.text-link:visited {
fill: purple;
}`,
2024-05-30 01:45:36 +00:00
)
appendOnTrigger(
buf,
source,
[]string{
`animated-connection`,
},
`
2023-02-21 08:33:13 +00:00
@keyframes dashdraw {
from {
stroke-dashoffset: 0;
}
}
`,
)
2024-12-15 02:35:18 +00:00
appendOnTrigger(
buf,
source,
[]string{
`animated-shape`,
},
`
@keyframes shapeappear {
0%, 100% { transform: translateY(0); filter: drop-shadow(0px 0px 0px rgba(0,0,0,0)); }
50% { transform: translateY(-4px); filter: drop-shadow(0px 12.6px 25.2px rgba(50,50,93,0.25)) drop-shadow(0px 7.56px 15.12px rgba(0,0,0,0.1)); }
}
.animated-shape {
animation: shapeappear 1s linear infinite;
}
`,
)
appendOnTrigger(
buf,
source,
[]string{
`appendix-icon`,
},
`
2023-02-21 08:33:13 +00:00
.appendix-icon {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
}`,
)
appendOnTrigger(
buf,
source,
[]string{
`class="text-bold`,
`<b>`,
`<strong>`,
},
fmt.Sprintf(`
.%s .text-bold {
font-family: "%s-font-bold";
2023-02-21 08:33:13 +00:00
}
@font-face {
font-family: %s-font-bold;
2023-02-21 08:33:13 +00:00
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
2023-03-29 17:03:44 +00:00
fontFamily.Font(0, d2fonts.FONT_STYLE_BOLD).GetEncodedSubset(corpus),
),
)
appendOnTrigger(
buf,
source,
[]string{
`class="text-italic`,
`<em>`,
`<dfn>`,
},
fmt.Sprintf(`
.%s .text-italic {
font-family: "%s-font-italic";
2023-02-21 08:33:13 +00:00
}
@font-face {
font-family: %s-font-italic;
2023-02-21 08:33:13 +00:00
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
2023-03-29 17:03:44 +00:00
fontFamily.Font(0, d2fonts.FONT_STYLE_ITALIC).GetEncodedSubset(corpus),
),
)
2022-11-07 19:52:01 +00:00
appendOnTrigger(
buf,
source,
[]string{
`class="text-mono`,
`<pre>`,
`<code>`,
`<kbd>`,
`<samp>`,
},
fmt.Sprintf(`
.%s .text-mono {
font-family: "%s-font-mono";
2023-02-21 08:33:13 +00:00
}
@font-face {
font-family: %s-font-mono;
2023-02-21 08:33:13 +00:00
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
2023-03-29 17:03:44 +00:00
d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_REGULAR).GetEncodedSubset(corpus),
),
)
2022-11-07 19:52:01 +00:00
appendOnTrigger(
buf,
source,
[]string{
2023-03-07 06:21:23 +00:00
`class="text-mono-bold`,
},
fmt.Sprintf(`
.%s .text-mono-bold {
font-family: "%s-font-mono-bold";
2023-02-21 08:33:13 +00:00
}
@font-face {
font-family: %s-font-mono-bold;
2023-02-21 08:33:13 +00:00
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
2023-03-29 17:03:44 +00:00
d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_BOLD).GetEncodedSubset(corpus),
),
)
2023-01-29 22:17:34 +00:00
appendOnTrigger(
buf,
source,
[]string{
2023-03-07 06:21:23 +00:00
`class="text-mono-italic`,
},
fmt.Sprintf(`
.%s .text-mono-italic {
font-family: "%s-font-mono-italic";
2023-02-21 08:33:13 +00:00
}
@font-face {
font-family: %s-font-mono-italic;
2023-02-21 08:33:13 +00:00
src: url("%s");
}`,
diagramHash,
diagramHash,
diagramHash,
2023-03-29 17:03:44 +00:00
d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_ITALIC).GetEncodedSubset(corpus),
),
)
2023-01-29 22:17:34 +00:00
appendOnTrigger(
buf,
source,
[]string{
`sketch-overlay-bright`,
},
2025-01-29 00:39:53 +00:00
fmt.Sprintf(`
2023-02-21 08:33:13 +00:00
.sketch-overlay-bright {
2025-01-29 00:39:53 +00:00
fill: url(#streaks-bright-%s);
2023-02-21 08:33:13 +00:00
mix-blend-mode: darken;
2025-01-29 00:39:53 +00:00
}`, diagramHash),
)
2023-01-29 22:17:34 +00:00
appendOnTrigger(
buf,
source,
[]string{
`sketch-overlay-normal`,
},
2025-01-29 00:39:53 +00:00
fmt.Sprintf(`
2023-02-21 08:33:13 +00:00
.sketch-overlay-normal {
2025-01-29 00:39:53 +00:00
fill: url(#streaks-normal-%s);
2023-02-21 08:33:13 +00:00
mix-blend-mode: color-burn;
2025-01-29 00:39:53 +00:00
}`, diagramHash),
)
2023-01-30 11:22:14 +00:00
appendOnTrigger(
buf,
source,
[]string{
`sketch-overlay-dark`,
},
2025-01-29 00:39:53 +00:00
fmt.Sprintf(`
2023-02-21 08:33:13 +00:00
.sketch-overlay-dark {
2025-01-29 00:39:53 +00:00
fill: url(#streaks-dark-%s);
2023-02-21 08:33:13 +00:00
mix-blend-mode: overlay;
2025-01-29 00:39:53 +00:00
}`, diagramHash),
)
2023-01-30 11:22:14 +00:00
appendOnTrigger(
buf,
source,
[]string{
`sketch-overlay-darker`,
},
2025-01-29 00:39:53 +00:00
fmt.Sprintf(`
2023-02-21 08:33:13 +00:00
.sketch-overlay-darker {
2025-01-29 00:39:53 +00:00
fill: url(#streaks-darker-%s);
2023-02-21 08:33:13 +00:00
mix-blend-mode: lighten;
2025-01-29 00:39:53 +00:00
}`, diagramHash),
)
2023-01-30 11:22:14 +00:00
fmt.Fprint(buf, `]]></style>`)
2023-01-30 11:22:14 +00:00
}
2023-01-30 11:06:54 +00:00
func appendOnTrigger(buf *bytes.Buffer, source string, triggers []string, newContent string) {
for _, trigger := range triggers {
if strings.Contains(source, trigger) {
fmt.Fprint(buf, newContent)
2023-01-30 11:06:54 +00:00
break
}
}
}
2023-02-19 14:33:40 +00:00
var DEFAULT_DARK_THEME *int64 = nil // no theme selected
2022-12-21 07:43:45 +00:00
func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
2025-01-15 23:30:17 +00:00
var jsRunner jsrunner.JSRunner
2022-12-21 07:43:45 +00:00
pad := DEFAULT_PADDING
2023-07-14 20:08:26 +00:00
themeID := d2themescatalog.NeutralDefault.ID
darkThemeID := DEFAULT_DARK_THEME
var scale *float64
2022-12-21 07:43:45 +00:00
if opts != nil {
2023-07-14 20:08:26 +00:00
if opts.Pad != nil {
pad = int(*opts.Pad)
}
if opts.Sketch != nil && *opts.Sketch {
2025-01-15 23:30:17 +00:00
jsRunner = jsrunner.NewJSRunner()
err := d2sketch.LoadJS(jsRunner)
2022-12-21 07:43:45 +00:00
if err != nil {
return nil, err
}
}
2023-07-14 20:08:26 +00:00
if opts.ThemeID != nil {
themeID = *opts.ThemeID
}
darkThemeID = opts.DarkThemeID
scale = opts.Scale
2024-12-29 21:19:32 +00:00
} else {
opts = &RenderOpts{}
2022-12-21 07:43:45 +00:00
}
buf := &bytes.Buffer{}
2022-11-09 19:10:51 +00:00
// only define shadow filter if a shape uses it
for _, s := range diagram.Shapes {
if s.Shadow {
defineShadowFilter(buf)
break
}
}
2024-09-27 21:49:10 +00:00
if color.IsGradient(diagram.Root.Fill) {
defineGradients(buf, diagram.Root.Fill)
}
if color.IsGradient(diagram.Root.Stroke) {
defineGradients(buf, diagram.Root.Stroke)
}
for _, s := range diagram.Shapes {
if color.IsGradient(s.Fill) {
defineGradients(buf, s.Fill)
}
if color.IsGradient(s.Stroke) {
defineGradients(buf, s.Stroke)
}
if color.IsGradient(s.Color) {
defineGradients(buf, s.Color)
}
}
for _, c := range diagram.Connections {
if color.IsGradient(c.Stroke) {
defineGradients(buf, c.Stroke)
}
if color.IsGradient(c.Fill) {
defineGradients(buf, c.Fill)
}
2024-09-27 21:49:10 +00:00
}
2023-03-23 20:37:28 +00:00
// Apply hash on IDs for targeting, to be specific for this diagram
2025-01-30 21:48:06 +00:00
diagramHash, err := diagram.HashID(opts.Salt)
2022-12-06 06:32:23 +00:00
if err != nil {
return nil, err
}
2023-03-23 20:37:28 +00:00
// Some targeting is still per-board, like masks for connections
isolatedDiagramHash := diagramHash
if opts != nil && opts.MasterID != "" {
diagramHash = opts.MasterID
}
2022-12-06 06:32:23 +00:00
// SVG has no notion of z-index. The z-index is effectively the order it's drawn.
// So draw from the least nested to most nested
idToShape := make(map[string]d2target.Shape)
2022-11-30 21:15:33 +00:00
allObjects := make([]DiagramObject, 0, len(diagram.Shapes)+len(diagram.Connections))
for _, s := range diagram.Shapes {
idToShape[s.ID] = s
allObjects = append(allObjects, s)
}
for _, c := range diagram.Connections {
allObjects = append(allObjects, c)
}
2022-11-30 19:22:43 +00:00
sortObjects(allObjects)
2023-07-03 23:45:05 +00:00
appendixItemBuf := &bytes.Buffer{}
2022-12-03 06:47:54 +00:00
var labelMasks []string
markers := map[string]struct{}{}
2024-10-09 18:09:46 +00:00
var inlineTheme *d2themes.Theme
2024-10-09 21:14:47 +00:00
// We only want to inline when no dark theme is specified, otherwise the inline style will override the CSS
if darkThemeID == nil {
2024-10-09 18:09:46 +00:00
inlineTheme = go2.Pointer(d2themescatalog.Find(themeID))
inlineTheme.ApplyOverrides(opts.ThemeOverrides)
}
for _, obj := range allObjects {
if c, is := obj.(d2target.Connection); is {
2025-01-15 23:30:17 +00:00
labelMask, err := drawConnection(buf, isolatedDiagramHash, c, markers, idToShape, jsRunner, inlineTheme)
2022-12-21 07:43:45 +00:00
if err != nil {
return nil, err
}
2022-12-06 03:50:42 +00:00
if labelMask != "" {
2022-12-03 06:47:54 +00:00
labelMasks = append(labelMasks, labelMask)
}
} else if s, is := obj.(d2target.Shape); is {
2025-01-15 23:30:17 +00:00
labelMask, err := drawShape(buf, appendixItemBuf, diagramHash, s, jsRunner, inlineTheme)
2022-12-06 03:50:42 +00:00
if err != nil {
return nil, err
2022-12-05 19:57:16 +00:00
} else if labelMask != "" {
labelMasks = append(labelMasks, labelMask)
}
} else {
2022-12-17 01:15:58 +00:00
return nil, fmt.Errorf("unknown object of type %T", obj)
}
}
2023-07-03 23:45:05 +00:00
// add all appendix items afterwards so they are always on top
fmt.Fprint(buf, appendixItemBuf)
2025-03-11 21:30:05 +00:00
if diagram.Legend != nil && (len(diagram.Legend.Shapes) > 0 || len(diagram.Legend.Connections) > 0) {
legendBuf := &bytes.Buffer{}
err := renderLegend(legendBuf, diagram, diagramHash, inlineTheme)
if err != nil {
return nil, err
}
fmt.Fprint(buf, legendBuf)
}
2022-12-17 01:15:37 +00:00
// Note: we always want this since we reference it on connections even if there end up being no masked labels
2023-02-19 11:32:44 +00:00
left, top, w, h := dimensions(diagram, pad)
2025-03-11 21:30:05 +00:00
if diagram.Legend != nil && (len(diagram.Legend.Shapes) > 0 || len(diagram.Legend.Connections) > 0) {
tl, br := diagram.BoundingBox()
totalHeight := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING
maxLabelWidth := 0
itemCount := 0
ruler, _ := textmeasure.NewRuler()
if ruler != nil {
for _, s := range diagram.Legend.Shapes {
if s.Label == "" {
continue
}
mtext := &d2target.MText{
Text: s.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
maxLabelWidth = go2.IntMax(maxLabelWidth, dims.Width)
totalHeight += go2.IntMax(dims.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING
itemCount++
}
for _, c := range diagram.Legend.Connections {
if c.Label == "" {
continue
}
mtext := &d2target.MText{
Text: c.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
maxLabelWidth = go2.IntMax(maxLabelWidth, dims.Width)
totalHeight += go2.IntMax(dims.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING
itemCount++
}
if itemCount > 0 {
totalHeight -= LEGEND_ITEM_SPACING / 2
}
totalHeight += LEGEND_PADDING
if totalHeight > 0 && maxLabelWidth > 0 {
legendWidth := LEGEND_PADDING*2 + LEGEND_ICON_SIZE + LEGEND_PADDING + maxLabelWidth
legendY := br.Y - totalHeight
if legendY < tl.Y {
legendY = tl.Y
}
legendRight := br.X + LEGEND_CORNER_PADDING + legendWidth
if left+w < legendRight {
w = legendRight - left + pad/2
}
if legendY < top {
diffY := top - legendY
top -= diffY
h += diffY
}
legendBottom := legendY + totalHeight
if top+h < legendBottom {
h = legendBottom - top + pad/2
}
}
}
}
2022-12-17 01:15:37 +00:00
fmt.Fprint(buf, strings.Join([]string{
fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
2023-03-23 20:37:28 +00:00
isolatedDiagramHash, left, top, w, h,
2022-12-17 01:15:37 +00:00
),
fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`,
2023-02-15 19:33:13 +00:00
left, top, w, h,
2022-12-17 01:15:37 +00:00
),
strings.Join(labelMasks, "\n"),
`</mask>`,
}, "\n"))
2022-12-03 06:47:54 +00:00
// generate style elements that will be appended to the SVG tag
upperBuf := &bytes.Buffer{}
2023-03-23 20:37:28 +00:00
if opts.MasterID == "" {
2023-03-29 20:50:00 +00:00
EmbedFonts(upperBuf, diagramHash, buf.String(), diagram.FontFamily, diagram.GetCorpus()) // EmbedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf`
2023-12-13 20:17:22 +00:00
themeStylesheet, err := ThemeCSS(diagramHash, &themeID, darkThemeID, opts.ThemeOverrides, opts.DarkThemeOverrides)
2023-03-23 20:37:28 +00:00
if err != nil {
return nil, err
}
fmt.Fprintf(upperBuf, `<style type="text/css"><![CDATA[%s%s]]></style>`, BaseStylesheet, themeStylesheet)
2023-01-26 20:40:53 +00:00
2023-03-23 20:37:28 +00:00
hasMarkdown := false
for _, s := range diagram.Shapes {
if s.Language == "markdown" {
2023-03-23 20:37:28 +00:00
hasMarkdown = true
break
}
}
if hasMarkdown {
css := MarkdownCSS
css = strings.ReplaceAll(css, ".md", fmt.Sprintf(".%s .md", diagramHash))
2023-03-23 20:37:28 +00:00
css = strings.ReplaceAll(css, "font-italic", fmt.Sprintf("%s-font-italic", diagramHash))
css = strings.ReplaceAll(css, "font-bold", fmt.Sprintf("%s-font-bold", diagramHash))
css = strings.ReplaceAll(css, "font-mono", fmt.Sprintf("%s-font-mono", diagramHash))
css = strings.ReplaceAll(css, "font-regular", fmt.Sprintf("%s-font-regular", diagramHash))
css = strings.ReplaceAll(css, "font-semibold", fmt.Sprintf("%s-font-semibold", diagramHash))
2023-03-23 20:37:28 +00:00
fmt.Fprintf(upperBuf, `<style type="text/css">%s</style>`, css)
2023-01-09 18:16:28 +00:00
}
2023-03-14 06:01:33 +00:00
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
2025-01-29 00:39:53 +00:00
d2sketch.DefineFillPatterns(upperBuf, diagramHash)
2023-03-23 20:37:28 +00:00
}
2023-02-28 03:26:19 +00:00
}
2023-01-09 18:16:28 +00:00
2023-02-26 19:41:50 +00:00
// This shift is for background el to envelop the diagram
left -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
top -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
w += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
h += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
2024-10-09 18:09:46 +00:00
backgroundEl := d2themes.NewThemableElement("rect", inlineTheme)
2023-02-26 19:41:50 +00:00
// We don't want to change the document viewbox, only the background el
backgroundEl.X = float64(left)
backgroundEl.Y = float64(top)
backgroundEl.Width = float64(w)
backgroundEl.Height = float64(h)
2023-02-26 19:41:50 +00:00
backgroundEl.Fill = diagram.Root.Fill
backgroundEl.Stroke = diagram.Root.Stroke
2023-03-14 06:01:33 +00:00
backgroundEl.FillPattern = diagram.Root.FillPattern
backgroundEl.Rx = float64(diagram.Root.BorderRadius)
2023-02-26 19:41:50 +00:00
if diagram.Root.StrokeDash != 0 {
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(diagram.Root.StrokeWidth), diagram.Root.StrokeDash)
backgroundEl.StrokeDashArray = fmt.Sprintf("%f, %f", dashSize, gapSize)
}
backgroundEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, diagram.Root.StrokeWidth)
// This shift is for viewbox to envelop the background el
left -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
top -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
w += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
h += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
doubleBorderElStr := ""
if diagram.Root.DoubleBorder {
offset := d2target.INNER_BORDER_OFFSET
left -= int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.)) + offset
top -= int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.)) + offset
w += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.)*2.) + 2*offset
h += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.)*2.) + 2*offset
backgroundEl2 := backgroundEl.Copy()
// No need to double-paint
backgroundEl.Fill = "transparent"
backgroundEl2.X = float64(left)
backgroundEl2.Y = float64(top)
backgroundEl2.Width = float64(w)
backgroundEl2.Height = float64(h)
doubleBorderElStr = backgroundEl2.Render()
left -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
top -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
w += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
h += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
}
2023-03-14 06:01:33 +00:00
bufStr := buf.String()
2023-03-16 05:53:12 +00:00
patternDefs := ""
2024-09-15 16:43:10 +00:00
for _, pattern := range d2ast.FillPatterns {
2023-03-18 22:43:28 +00:00
if strings.Contains(bufStr, fmt.Sprintf("%s-overlay", pattern)) || diagram.Root.FillPattern == pattern {
2023-03-16 05:53:12 +00:00
if patternDefs == "" {
fmt.Fprint(upperBuf, `<style type="text/css"><![CDATA[`)
}
switch pattern {
case "dots":
2025-01-29 00:23:40 +00:00
patternDefs += fmt.Sprintf(dots, diagramHash)
2023-03-16 05:53:12 +00:00
case "lines":
2025-01-29 00:23:40 +00:00
patternDefs += fmt.Sprintf(lines, diagramHash)
2023-03-16 05:53:12 +00:00
case "grain":
2025-01-29 00:23:40 +00:00
patternDefs += fmt.Sprintf(grain, diagramHash)
2023-03-18 22:43:28 +00:00
case "paper":
2025-01-29 00:23:40 +00:00
patternDefs += fmt.Sprintf(paper, diagramHash)
2023-03-16 05:53:12 +00:00
}
2023-07-03 23:18:02 +00:00
fmt.Fprintf(upperBuf, `
2023-03-16 05:53:12 +00:00
.%s-overlay {
2025-01-29 00:23:40 +00:00
fill: url(#%s-%s);
2023-03-14 06:01:33 +00:00
mix-blend-mode: multiply;
2025-01-29 00:23:40 +00:00
}`, pattern, pattern, diagramHash)
2023-03-16 05:53:12 +00:00
}
}
if patternDefs != "" {
2023-03-14 06:01:33 +00:00
fmt.Fprint(upperBuf, `]]></style>`)
fmt.Fprint(upperBuf, "<defs>")
2023-07-03 23:18:02 +00:00
fmt.Fprint(upperBuf, patternDefs)
2023-03-14 06:01:33 +00:00
fmt.Fprint(upperBuf, "</defs>")
}
var dimensions string
if scale != nil {
dimensions = fmt.Sprintf(` width="%d" height="%d"`,
int(math.Ceil((*scale)*float64(w))),
int(math.Ceil((*scale)*float64(h))),
)
}
2023-03-18 05:29:51 +00:00
alignment := "xMinYMin"
2023-07-14 20:08:26 +00:00
if opts.Center != nil && *opts.Center {
2023-03-18 05:29:51 +00:00
alignment = "xMidYMid"
}
2023-03-23 20:37:28 +00:00
fitToScreenWrapperOpening := ""
xmlTag := ""
fitToScreenWrapperClosing := ""
idAttr := ""
tag := "g"
// Many things change when this is rendering for animation
if opts.MasterID == "" {
2025-04-02 19:57:06 +00:00
dataD2Version := ""
if opts.OmitVersion == nil || !*opts.OmitVersion {
dataD2Version = fmt.Sprintf(`data-d2-version="%s"`, version.Version)
}
fitToScreenWrapperOpening = fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" %s preserveAspectRatio="%s meet" viewBox="0 0 %d %d"%s>`,
dataD2Version,
2023-03-23 20:37:28 +00:00
alignment,
w, h,
dimensions,
)
2025-01-24 05:27:34 +00:00
if opts.NoXMLTag == nil || !*opts.NoXMLTag {
xmlTag = `<?xml version="1.0" encoding="utf-8"?>`
}
2023-03-23 20:37:28 +00:00
fitToScreenWrapperClosing = "</svg>"
2025-01-27 01:28:28 +00:00
idAttr = `d2-svg`
2023-03-23 20:37:28 +00:00
tag = "svg"
}
// TODO minify
2025-01-27 01:28:28 +00:00
docRendered := fmt.Sprintf(`%s%s<%s class="%s" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s</%s>%s`,
2023-03-23 20:37:28 +00:00
xmlTag,
fitToScreenWrapperOpening,
tag,
2025-01-27 01:28:28 +00:00
strings.Join([]string{diagramHash, idAttr}, " "),
2023-02-19 11:32:44 +00:00
w, h, left, top, w, h,
2023-02-26 19:41:50 +00:00
doubleBorderElStr,
backgroundEl.Render(),
upperBuf.String(),
2023-01-09 18:16:28 +00:00
buf.String(),
2023-03-23 20:37:28 +00:00
tag,
fitToScreenWrapperClosing,
2023-01-09 18:16:28 +00:00
)
return []byte(docRendered), nil
}
// TODO include only colors that are being used to reduce size
2023-12-13 20:17:22 +00:00
func ThemeCSS(diagramHash string, themeID *int64, darkThemeID *int64, overrides, darkOverrides *d2target.ThemeOverrides) (stylesheet string, err error) {
2023-07-14 20:08:26 +00:00
if themeID == nil {
themeID = &d2themescatalog.NeutralDefault.ID
}
2023-12-13 20:17:22 +00:00
out, err := singleThemeRulesets(diagramHash, *themeID, overrides)
if err != nil {
return "", err
}
2023-02-19 14:33:40 +00:00
if darkThemeID != nil {
2023-12-13 20:17:22 +00:00
darkOut, err := singleThemeRulesets(diagramHash, *darkThemeID, darkOverrides)
if err != nil {
return "", err
}
out += fmt.Sprintf("@media screen and (prefers-color-scheme:dark){%s}", darkOut)
}
return out, nil
}
2023-12-13 20:17:22 +00:00
func singleThemeRulesets(diagramHash string, themeID int64, overrides *d2target.ThemeOverrides) (rulesets string, err error) {
out := ""
theme := d2themescatalog.Find(themeID)
2023-12-13 20:17:22 +00:00
theme.ApplyOverrides(overrides)
// Global theme colors
for _, property := range []string{"fill", "stroke", "background-color", "color"} {
out += fmt.Sprintf(`
.%s .%s-N1{%s:%s;}
.%s .%s-N2{%s:%s;}
.%s .%s-N3{%s:%s;}
.%s .%s-N4{%s:%s;}
.%s .%s-N5{%s:%s;}
.%s .%s-N6{%s:%s;}
.%s .%s-N7{%s:%s;}
.%s .%s-B1{%s:%s;}
.%s .%s-B2{%s:%s;}
.%s .%s-B3{%s:%s;}
.%s .%s-B4{%s:%s;}
.%s .%s-B5{%s:%s;}
.%s .%s-B6{%s:%s;}
.%s .%s-AA2{%s:%s;}
.%s .%s-AA4{%s:%s;}
.%s .%s-AA5{%s:%s;}
.%s .%s-AB4{%s:%s;}
.%s .%s-AB5{%s:%s;}`,
diagramHash,
property, property, theme.Colors.Neutrals.N1,
diagramHash,
property, property, theme.Colors.Neutrals.N2,
diagramHash,
property, property, theme.Colors.Neutrals.N3,
diagramHash,
property, property, theme.Colors.Neutrals.N4,
diagramHash,
property, property, theme.Colors.Neutrals.N5,
diagramHash,
property, property, theme.Colors.Neutrals.N6,
diagramHash,
property, property, theme.Colors.Neutrals.N7,
diagramHash,
property, property, theme.Colors.B1,
diagramHash,
property, property, theme.Colors.B2,
diagramHash,
property, property, theme.Colors.B3,
diagramHash,
property, property, theme.Colors.B4,
diagramHash,
property, property, theme.Colors.B5,
diagramHash,
property, property, theme.Colors.B6,
diagramHash,
property, property, theme.Colors.AA2,
diagramHash,
property, property, theme.Colors.AA4,
diagramHash,
property, property, theme.Colors.AA5,
diagramHash,
property, property, theme.Colors.AB4,
diagramHash,
property, property, theme.Colors.AB5,
)
}
2023-01-28 00:43:24 +00:00
// Appendix
out += fmt.Sprintf(".appendix text.text{fill:%s}", theme.Colors.Neutrals.N1)
2023-01-28 00:43:24 +00:00
// Markdown specific rulesets
out += fmt.Sprintf(".md{--color-fg-default:%s;--color-fg-muted:%s;--color-fg-subtle:%s;--color-canvas-default:%s;--color-canvas-subtle:%s;--color-border-default:%s;--color-border-muted:%s;--color-neutral-muted:%s;--color-accent-fg:%s;--color-accent-emphasis:%s;--color-attention-subtle:%s;--color-danger-fg:%s;}",
theme.Colors.Neutrals.N1, theme.Colors.Neutrals.N2, theme.Colors.Neutrals.N3,
theme.Colors.Neutrals.N7, theme.Colors.Neutrals.N6,
theme.Colors.B1, theme.Colors.B2,
2023-01-10 10:34:42 +00:00
theme.Colors.Neutrals.N6,
theme.Colors.B2, theme.Colors.B2,
theme.Colors.Neutrals.N2, // TODO or N3 --color-attention-subtle
"red",
)
// Sketch style specific rulesets
2023-01-12 10:19:34 +00:00
// B
lc, err := color.LuminanceCategory(theme.Colors.B1)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B1, lc, diagramHash, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.B2)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B2, lc, diagramHash, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.B3)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B3, lc, diagramHash, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.B4)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B4, lc, diagramHash, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.B5)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B5, lc, diagramHash, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.B6)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B6, lc, diagramHash, blendMode(lc))
2023-01-12 10:19:34 +00:00
// AA
lc, err = color.LuminanceCategory(theme.Colors.AA2)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.AA2, lc, diagramHash, blendMode(lc))
2023-01-12 10:19:34 +00:00
lc, err = color.LuminanceCategory(theme.Colors.AA4)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.AA4, lc, diagramHash, blendMode(lc))
2023-01-12 10:19:34 +00:00
lc, err = color.LuminanceCategory(theme.Colors.AA5)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.AA5, lc, diagramHash, blendMode(lc))
2023-01-12 10:19:34 +00:00
// AB
lc, err = color.LuminanceCategory(theme.Colors.AB4)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.AB4, lc, diagramHash, blendMode(lc))
2023-01-12 10:19:34 +00:00
lc, err = color.LuminanceCategory(theme.Colors.AB5)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.AB5, lc, diagramHash, blendMode(lc))
2023-01-12 10:19:34 +00:00
// Neutrals
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N1)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N1, lc, diagramHash, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N2)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N2, lc, diagramHash, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N3)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N3, lc, diagramHash, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N4)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N4, lc, diagramHash, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N5)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N5, lc, diagramHash, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N6)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N6, lc, diagramHash, blendMode(lc))
lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N7)
if err != nil {
return "", err
}
2025-01-29 00:39:53 +00:00
out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N7, lc, diagramHash, blendMode(lc))
2023-03-04 05:02:02 +00:00
if theme.IsDark() {
2023-07-03 23:18:02 +00:00
out += ".light-code{display: none}"
out += ".dark-code{display: block}"
2023-03-04 05:02:02 +00:00
} else {
2023-07-03 23:18:02 +00:00
out += ".light-code{display: block}"
out += ".dark-code{display: none}"
2023-03-04 05:02:02 +00:00
}
return out, nil
}
func blendMode(lc string) string {
switch lc {
case "bright":
return "darken"
case "normal":
return "color-burn"
case "dark":
return "overlay"
case "darker":
return "lighten"
}
panic("invalid luminance category")
}
2022-11-30 21:15:33 +00:00
type DiagramObject interface {
GetID() string
GetZIndex() int
}
2022-11-30 19:22:43 +00:00
// sortObjects sorts all diagrams objects (shapes and connections) in the desired drawing order
// the sorting criteria is:
// 1. zIndex, lower comes first
// 2. two shapes with the same zIndex are sorted by their level (container nesting), containers come first
// 3. two shapes with the same zIndex and same level, are sorted in the order they were exported
// 4. shape and edge, shapes come first
2022-11-30 21:15:33 +00:00
func sortObjects(allObjects []DiagramObject) {
2022-11-30 19:22:43 +00:00
sort.SliceStable(allObjects, func(i, j int) bool {
// first sort by zIndex
iZIndex := allObjects[i].GetZIndex()
jZIndex := allObjects[j].GetZIndex()
if iZIndex != jZIndex {
return iZIndex < jZIndex
}
2022-11-30 21:15:33 +00:00
// then, if both are shapes, parents come before their children
2022-11-30 19:22:43 +00:00
iShape, iIsShape := allObjects[i].(d2target.Shape)
jShape, jIsShape := allObjects[j].(d2target.Shape)
if iIsShape && jIsShape {
return iShape.Level < jShape.Level
}
// then, shapes come before connections
_, jIsConnection := allObjects[j].(d2target.Connection)
return iIsShape && jIsConnection
})
}
func hash(s string) string {
const secret = "lalalas"
h := fnv.New32a()
h.Write([]byte(fmt.Sprintf("%s%s", s, secret)))
return fmt.Sprint(h.Sum32())
}
func RenderMultiboard(diagram *d2target.Diagram, opts *RenderOpts) ([][]byte, error) {
var boards [][]byte
for _, dl := range diagram.Layers {
childrenBoards, err := RenderMultiboard(dl, opts)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Scenarios {
childrenBoards, err := RenderMultiboard(dl, opts)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
for _, dl := range diagram.Steps {
childrenBoards, err := RenderMultiboard(dl, opts)
if err != nil {
return nil, err
}
boards = append(boards, childrenBoards...)
}
if !diagram.IsFolderOnly {
out, err := Render(diagram, opts)
if err != nil {
return boards, err
}
boards = append([][]byte{out}, boards...)
return boards, nil
}
return boards, nil
}