diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index 4d8f0f23e7..cbd3a5d4b1 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -1,6 +1,9 @@
#### Features ๐
- exports: gif exports work with `animate: true` keyword [#2663](https://github.com/terrastruct/d2/pull/2663)
+- animations:
+ - unidirectional connections with an icon and `animate: true` animate the icon [#2666](https://github.com/terrastruct/d2/pull/2666)
+ - unidirectional connections with no `stroke-dash` and `animate: true` animate the path growing [#2666](https://github.com/terrastruct/d2/pull/2666)
#### Improvements ๐งน
@@ -13,6 +16,8 @@
- `animate-interval` is no longer required, defaults to 1000ms for gifs [#2663](https://github.com/terrastruct/d2/pull/2663)
- renders:
- remote images are fetched more reliably [#2659](https://github.com/terrastruct/d2/pull/2659)
+- vars:
+ - `animate-interval` may be set as a `d2-config` variable [#2666](https://github.com/terrastruct/d2/pull/2666)
#### Bugfixes โ๏ธ
diff --git a/d2cli/main.go b/d2cli/main.go
index 6e49ad0615..7cd6d6a838 100644
--- a/d2cli/main.go
+++ b/d2cli/main.go
@@ -337,15 +337,16 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
}
renderOpts := d2svg.RenderOpts{
- Pad: padFlag,
- Sketch: sketchFlag,
- Center: centerFlag,
- ThemeID: themeFlag,
- DarkThemeID: darkThemeFlag,
- Scale: scale,
- NoXMLTag: noXMLTagFlag,
- Salt: saltFlag,
- OmitVersion: omitVersionFlag,
+ Pad: padFlag,
+ Sketch: sketchFlag,
+ Center: centerFlag,
+ ThemeID: themeFlag,
+ DarkThemeID: darkThemeFlag,
+ Scale: scale,
+ NoXMLTag: noXMLTagFlag,
+ Salt: saltFlag,
+ OmitVersion: omitVersionFlag,
+ AnimateInterval: int(*animateIntervalFlag),
}
if *watchFlag {
diff --git a/d2compiler/compile.go b/d2compiler/compile.go
index 66f6497a54..672e7ec46d 100644
--- a/d2compiler/compile.go
+++ b/d2compiler/compile.go
@@ -1547,6 +1547,12 @@ func compileConfig(ir *d2ir.Map) (*d2target.Config, error) {
config.Center = &val
}
+ f = configMap.GetField(d2ast.FlatUnquotedString("animate-interval"))
+ if f != nil {
+ val, _ := strconv.Atoi(f.Primary().Value.ScalarString())
+ config.AnimateInterval = go2.Pointer(int64(val))
+ }
+
f = configMap.GetField(d2ast.FlatUnquotedString("theme-overrides"))
if f != nil {
overrides, err := compileThemeOverrides(f.Map())
diff --git a/d2ir/compile.go b/d2ir/compile.go
index 9cda79abee..6524b36f85 100644
--- a/d2ir/compile.go
+++ b/d2ir/compile.go
@@ -212,7 +212,7 @@ func (c *compiler) validateConfigs(configs *Field) {
c.errorf(f.LastRef().AST(), `%d is not a valid theme ID`, valInt)
continue
}
- case "pad":
+ case "pad", "animate-interval":
_, err := strconv.Atoi(val)
if err != nil {
c.errorf(f.LastRef().AST(), `expected an integer for "%s", got "%s"`, f.Name.ScalarString(), val)
diff --git a/d2renderers/d2sketch/sketch.go b/d2renderers/d2sketch/sketch.go
index 62d7a008fd..710a227843 100644
--- a/d2renderers/d2sketch/sketch.go
+++ b/d2renderers/d2sketch/sketch.go
@@ -371,48 +371,56 @@ func Paths(r jsrunner.JSRunner, shape d2target.Shape, diagramHash string, paths
}
func Connection(r jsrunner.JSRunner, connection d2target.Connection, path, attrs string) (string, error) {
+ animatedGrow := connection.Animated && connection.Icon == nil && connection.StrokeDash == 0 && !connection.IsBidirectional()
animatedClass := ""
- if connection.Animated {
+ if connection.Animated && connection.Icon == nil && (connection.StrokeDash != 0 || connection.IsBidirectional()) {
animatedClass = " animated-connection"
+ } else if animatedGrow {
+ animatedClass = " animated-connection-grow"
}
- if connection.Animated {
- // If connection is animated and bidirectional
- if (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
- path1, path2, err := svg.SplitPath(path, 0.5)
+ // If connection is animated and bidirectional
+ if connection.Animated && connection.Icon == nil && connection.IsBidirectional() {
+ // There is no pure CSS way to animate bidirectional connections in two directions, so we split it up
+ path1, path2, err := svg.SplitPath(path, 0.5)
- if err != nil {
- return "", err
- }
+ if err != nil {
+ return "", err
+ }
- pathEl1 := d2themes.NewThemableElement("path", nil)
- 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 = attrs
-
- pathEl2 := d2themes.NewThemableElement("path", nil)
- pathEl2.D = path2
- pathEl2.Fill = color.None
- pathEl2.Stroke = connection.Stroke
- pathEl2.ClassName = fmt.Sprintf("connection%s", animatedClass)
- pathEl2.Style = connection.CSSStyle()
- pathEl2.Attributes = attrs
- return pathEl1.Render() + " " + pathEl2.Render(), nil
- } else {
- pathEl := d2themes.NewThemableElement("path", nil)
- pathEl.D = path
- pathEl.Fill = color.None
- pathEl.Stroke = connection.Stroke
- pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
- pathEl.Style = connection.CSSStyle()
- pathEl.Attributes = attrs
- return pathEl.Render(), nil
+ pathEl1 := d2themes.NewThemableElement("path", nil)
+ 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 = attrs
+
+ pathEl2 := d2themes.NewThemableElement("path", nil)
+ pathEl2.D = path2
+ pathEl2.Fill = color.None
+ pathEl2.Stroke = connection.Stroke
+ pathEl2.ClassName = fmt.Sprintf("connection%s", animatedClass)
+ pathEl2.Style = connection.CSSStyle()
+ pathEl2.Attributes = attrs
+ return pathEl1.Render() + " " + pathEl2.Render(), nil
+ } else if connection.Animated && connection.Icon == nil {
+ pathEl := d2themes.NewThemableElement("path", nil)
+ pathEl.D = path
+ pathEl.Fill = color.None
+ pathEl.Stroke = connection.Stroke
+ pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
+ pathEl.Style = connection.CSSStyle()
+ if animatedGrow {
+ pathData := strings.Split(strings.TrimSpace(path), " ")
+ pathLen, err := svg.PathLength(pathData)
+ if err == nil {
+ pathEl.Style += fmt.Sprintf("stroke-dasharray:%f;stroke-dashoffset:%f;", pathLen, pathLen)
+ }
}
+ pathEl.Attributes = attrs
+ return pathEl.Render(), nil
} else {
roughness := 0.5
js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness)
@@ -749,19 +757,23 @@ func computeRoughPaths(r jsrunner.JSRunner, js string) ([]roughPath, error) {
return extractRoughPaths(r)
}
-type attrs struct {
+func ComputeRoughPaths(r jsrunner.JSRunner, js string) ([]roughPath, error) {
+ return computeRoughPaths(r, js)
+}
+
+type Attrs struct {
D string `json:"d"`
}
-type style struct {
+type Style struct {
Stroke string `json:"stroke,omitempty"`
StrokeWidth string `json:"strokeWidth,omitempty"`
Fill string `json:"fill,omitempty"`
}
type roughPath struct {
- Attrs attrs `json:"attrs"`
- Style style `json:"style"`
+ Attrs Attrs `json:"attrs"`
+ Style Style `json:"style"`
}
func (rp roughPath) StyleCSS() string {
diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index 5e5b5028b3..e1198dd6d0 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -90,10 +90,11 @@ type RenderOpts struct {
// 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
- MasterID string
- NoXMLTag *bool
- Salt *string
- OmitVersion *bool
+ MasterID string
+ NoXMLTag *bool
+ Salt *string
+ OmitVersion *bool
+ AnimateInterval int
}
func dimensions(diagram *d2target.Diagram, pad int) (left, top, width, height int) {
@@ -426,7 +427,7 @@ func renderLegendConnectionIcon(c d2target.Connection, x, y int, theme *d2themes
fmt.Fprintf(finalBuf, ``,
x, y, 1.0/sizeFactor)
- _, err := drawConnection(buf, legendHash, legendConn, markers, idToShape, nil, theme)
+ _, err := drawConnection(buf, legendHash, legendConn, markers, idToShape, nil, theme, 0)
if err != nil {
return "", err
}
@@ -1003,7 +1004,7 @@ func makeBorderLabelMask(labelPosition label.Position, labelTL *geo.Point, label
)
}
-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) {
+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, animateInterval int) (labelMask string, _ error) {
opacityStyle := ""
if connection.Opacity != 1.0 {
opacityStyle = fmt.Sprintf(" style='opacity:%f'", connection.Opacity)
@@ -1042,23 +1043,7 @@ func drawConnection(writer io.Writer, diagramHash string, connection d2target.Co
markerEnd = fmt.Sprintf(`marker-end="url(#%s)" `, id)
}
- if connection.Icon != nil {
- iconPos := connection.GetIconPosition()
- if iconPos != nil {
- connectionIconClipPath := ""
- if connection.IconBorderRadius != 0 {
- connectionIconClipPath = fmt.Sprintf(` clip-path="inset(0 round %fpx)"`, connection.IconBorderRadius)
- }
- fmt.Fprintf(writer, ``,
- html.EscapeString(connection.Icon.String()),
- iconPos.X,
- iconPos.Y,
- d2target.DEFAULT_ICON_SIZE,
- d2target.DEFAULT_ICON_SIZE,
- connectionIconClipPath,
- )
- }
- }
+ animatedIcon := connection.Animated && connection.Icon != nil
var labelTL *geo.Point
if connection.Label != "" {
@@ -1070,7 +1055,7 @@ func drawConnection(writer io.Writer, diagramHash string, connection d2target.Co
width := connection.LabelWidth
height := connection.LabelHeight
- if connection.Icon != nil {
+ if connection.Icon != nil && !animatedIcon {
width += d2target.CONNECTION_ICON_LABEL_GAP + d2target.DEFAULT_ICON_SIZE
maskTL.X -= float64(d2target.CONNECTION_ICON_LABEL_GAP + d2target.DEFAULT_ICON_SIZE)
}
@@ -1080,7 +1065,7 @@ func drawConnection(writer io.Writer, diagramHash string, connection d2target.Co
} else {
labelMask = makeLabelMask(maskTL, width, height, 0.75)
}
- } else if connection.Icon != nil {
+ } else if connection.Icon != nil && !animatedIcon {
iconPos := connection.GetIconPosition()
if iconPos != nil {
maskTL := &geo.Point{
@@ -1106,20 +1091,60 @@ func drawConnection(writer io.Writer, diagramHash string, connection d2target.Co
}
fmt.Fprint(writer, out)
- // render sketch arrowheads separately
- arrowPaths, err := d2sketch.Arrowheads(jsRunner, connection, srcAdj, dstAdj)
- if err != nil {
- return "", err
+ animatedGrow := connection.Animated && connection.Icon == nil && connection.StrokeDash == 0 && !connection.IsBidirectional()
+ if animatedGrow && connection.DstArrow != d2target.NoArrowhead {
+ arrowJS, extraJS := d2sketch.ArrowheadJS(jsRunner, connection.DstArrow, connection.Stroke, connection.StrokeWidth)
+ if arrowJS != "" {
+ roughPaths, err := d2sketch.ComputeRoughPaths(jsRunner, arrowJS)
+ if err != nil {
+ return "", err
+ }
+ if extraJS != "" {
+ extraPaths, err := d2sketch.ComputeRoughPaths(jsRunner, extraJS)
+ if err != nil {
+ return "", err
+ }
+ roughPaths = append(roughPaths, extraPaths...)
+ }
+
+ arrowStyle := fmt.Sprintf(`offset-path: path('%s'); offset-rotate: auto; offset-distance: 0%%;`, path)
+ for _, rp := range roughPaths {
+ pathEl := d2themes.NewThemableElement("path", inlineTheme)
+ pathEl.D = rp.Attrs.D
+ pathEl.Fill = rp.Style.Fill
+ pathEl.Stroke = rp.Style.Stroke
+ pathEl.ClassName = "connection animated-arrowhead"
+ pathEl.Style = arrowStyle + rp.StyleCSS()
+ fmt.Fprint(writer, pathEl.Render())
+ }
+ }
+
+ modifiedConnection := connection
+ modifiedConnection.DstArrow = d2target.NoArrowhead
+ arrowPaths, err := d2sketch.Arrowheads(jsRunner, modifiedConnection, srcAdj, dstAdj)
+ if err != nil {
+ return "", err
+ }
+ fmt.Fprint(writer, arrowPaths)
+ } else {
+ // render sketch arrowheads separately
+ arrowPaths, err := d2sketch.Arrowheads(jsRunner, connection, srcAdj, dstAdj)
+ if err != nil {
+ return "", err
+ }
+ fmt.Fprint(writer, arrowPaths)
}
- fmt.Fprint(writer, arrowPaths)
} else {
+ animatedGrow := connection.Animated && connection.Icon == nil && connection.StrokeDash == 0 && !connection.IsBidirectional()
animatedClass := ""
- if connection.Animated {
+ if connection.Animated && connection.Icon == nil && (connection.StrokeDash != 0 || connection.IsBidirectional()) {
animatedClass = " animated-connection"
+ } else if animatedGrow {
+ animatedClass = " animated-connection-grow"
}
// 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)) {
+ if connection.Animated && connection.Icon == nil && connection.IsBidirectional() {
// There is no pure CSS way to animate bidirectional connections in two directions, so we split it up
path1, path2, err := svg.SplitPath(path, 0.5)
@@ -1152,8 +1177,70 @@ func drawConnection(writer io.Writer, diagramHash string, connection d2target.Co
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)
+
+ pathAttrs := fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask)
+ if animatedGrow {
+ if connection.DstArrow != d2target.NoArrowhead {
+ pathAttrs = fmt.Sprintf("%s%s", markerStart, mask)
+ }
+ }
+ if animatedGrow {
+ pathData := strings.Split(strings.TrimSpace(path), " ")
+ pathLen, err := svg.PathLength(pathData)
+ if err == nil {
+ pathEl.Style += fmt.Sprintf("stroke-dasharray:%f;stroke-dashoffset:%f;", pathLen, pathLen)
+ }
+ }
+ pathEl.Attributes = pathAttrs
fmt.Fprint(writer, pathEl.Render())
+
+ if animatedGrow && connection.DstArrow != d2target.NoArrowhead {
+ arrowWidth, arrowHeight := connection.DstArrow.Dimensions(float64(connection.StrokeWidth))
+
+ var arrowShape string
+ arrowStyle := fmt.Sprintf(`offset-path: path('%s'); offset-rotate: auto; offset-distance: 0%%;`, path)
+ switch connection.DstArrow {
+ case d2target.TriangleArrowhead:
+ polygonEl := d2themes.NewThemableElement("polygon", inlineTheme)
+ polygonEl.Fill = connection.Stroke
+ polygonEl.ClassName = "connection animated-arrowhead"
+ polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
+ polygonEl.Style = arrowStyle
+ polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
+ -arrowWidth/2, -arrowHeight/2,
+ arrowWidth/2, 0.,
+ -arrowWidth/2, arrowHeight/2,
+ )
+ arrowShape = polygonEl.Render()
+ case d2target.ArrowArrowhead:
+ polygonEl := d2themes.NewThemableElement("polygon", inlineTheme)
+ polygonEl.Fill = connection.Stroke
+ polygonEl.ClassName = "connection animated-arrowhead"
+ polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
+ polygonEl.Style = arrowStyle
+ polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
+ -arrowWidth/2, -arrowHeight/2,
+ arrowWidth/2, 0.,
+ -arrowWidth/2, arrowHeight/2,
+ -arrowWidth/2+arrowWidth/4, 0.,
+ )
+ arrowShape = polygonEl.Render()
+ default:
+ polygonEl := d2themes.NewThemableElement("polygon", inlineTheme)
+ polygonEl.Fill = connection.Stroke
+ polygonEl.ClassName = "connection animated-arrowhead"
+ polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
+ polygonEl.Style = arrowStyle
+ polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
+ -arrowWidth/2, -arrowHeight/2,
+ arrowWidth/2, 0.,
+ -arrowWidth/2, arrowHeight/2,
+ )
+ arrowShape = polygonEl.Render()
+ }
+
+ fmt.Fprint(writer, arrowShape)
+ }
}
}
@@ -1302,6 +1389,45 @@ func drawConnection(writer io.Writer, diagramHash string, connection d2target.Co
if connection.DstLabel != nil && connection.DstLabel.Label != "" {
fmt.Fprint(writer, renderArrowheadLabel(connection, connection.DstLabel.Label, true, inlineTheme))
}
+
+ if animatedIcon {
+ iconCenterOffset := float64(d2target.DEFAULT_ICON_SIZE) / 2
+ animationPath := path
+
+ connectionIconClipPath := ""
+ if connection.IconBorderRadius != 0 {
+ connectionIconClipPath = fmt.Sprintf(` clip-path="inset(0 round %fpx)"`, connection.IconBorderRadius)
+ }
+
+ iconStyle := fmt.Sprintf(`offset-path: path('%s'); offset-rotate: 0deg; offset-distance: 0%%;`, animationPath)
+
+ fmt.Fprintf(writer, ``,
+ html.EscapeString(connection.Icon.String()),
+ -iconCenterOffset,
+ -iconCenterOffset,
+ d2target.DEFAULT_ICON_SIZE,
+ d2target.DEFAULT_ICON_SIZE,
+ connectionIconClipPath,
+ iconStyle,
+ )
+ } else if connection.Icon != nil {
+ iconPos := connection.GetIconPosition()
+ if iconPos != nil {
+ connectionIconClipPath := ""
+ if connection.IconBorderRadius != 0 {
+ connectionIconClipPath = fmt.Sprintf(` clip-path="inset(0 round %fpx)"`, connection.IconBorderRadius)
+ }
+ fmt.Fprintf(writer, ``,
+ html.EscapeString(connection.Icon.String()),
+ iconPos.X,
+ iconPos.Y,
+ d2target.DEFAULT_ICON_SIZE,
+ d2target.DEFAULT_ICON_SIZE,
+ connectionIconClipPath,
+ )
+ }
+ }
+
fmt.Fprintf(writer, ``)
return
}
@@ -2522,6 +2648,60 @@ func EmbedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fon
`,
)
+ appendOnTrigger(
+ buf,
+ source,
+ []string{
+ `animated-connection-grow`,
+ },
+ `
+@keyframes pathdraw {
+ to {
+ stroke-dashoffset: 0;
+ }
+}
+.animated-connection-grow {
+ animation: pathdraw 1s linear infinite;
+}
+`,
+ )
+
+ appendOnTrigger(
+ buf,
+ source,
+ []string{
+ `animated-arrowhead`,
+ },
+ `
+@keyframes arrowheadpath {
+ to {
+ offset-distance: 100%;
+ }
+}
+.animated-arrowhead {
+ animation: arrowheadpath 1s linear infinite;
+}
+`,
+ )
+
+ appendOnTrigger(
+ buf,
+ source,
+ []string{
+ `animated-icon`,
+ },
+ `
+@keyframes iconpath {
+ to {
+ offset-distance: 100%;
+ }
+}
+.animated-icon {
+ animation: iconpath 1s linear infinite;
+}
+`,
+ )
+
appendOnTrigger(
buf,
source,
@@ -2736,6 +2916,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
themeID := d2themescatalog.NeutralDefault.ID
darkThemeID := DEFAULT_DARK_THEME
var scale *float64
+ animateInterval := 0
if opts != nil {
if opts.Pad != nil {
pad = int(*opts.Pad)
@@ -2752,10 +2933,15 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
}
darkThemeID = opts.DarkThemeID
scale = opts.Scale
+ animateInterval = opts.AnimateInterval
} else {
opts = &RenderOpts{}
}
+ if diagram.Config != nil && diagram.Config.AnimateInterval != nil {
+ animateInterval = int(*diagram.Config.AnimateInterval)
+ }
+
buf := &bytes.Buffer{}
// only define shadow filter if a shape uses it
@@ -2829,7 +3015,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
}
for _, obj := range allObjects {
if c, is := obj.(d2target.Connection); is {
- labelMask, err := drawConnection(buf, isolatedDiagramHash, c, markers, idToShape, jsRunner, inlineTheme)
+ labelMask, err := drawConnection(buf, isolatedDiagramHash, c, markers, idToShape, jsRunner, inlineTheme, animateInterval)
if err != nil {
return nil, err
}
diff --git a/d2target/d2target.go b/d2target/d2target.go
index 63fcfacbf3..9e434c5e92 100644
--- a/d2target/d2target.go
+++ b/d2target/d2target.go
@@ -48,6 +48,7 @@ type Config struct {
Pad *int64 `json:"pad"`
Center *bool `json:"center"`
LayoutEngine *string `json:"layoutEngine"`
+ AnimateInterval *int64 `json:"animateInterval,omitempty"`
ThemeOverrides *ThemeOverrides `json:"themeOverrides,omitempty"`
DarkThemeOverrides *ThemeOverrides `json:"darkThemeOverrides,omitempty"`
// Data is a data structure for holding user-defined data
@@ -788,19 +789,24 @@ func (c Connection) GetFontColor() string {
return color.N1
}
+func (c Connection) IsBidirectional() bool {
+ return (c.SrcArrow == NoArrowhead) == (c.DstArrow == NoArrowhead)
+}
+
func (c Connection) CSSStyle() string {
out := ""
out += fmt.Sprintf(`stroke-width:%d;`, c.StrokeWidth)
strokeDash := c.StrokeDash
- if strokeDash == 0 && c.Animated {
+ animated := c.Animated && c.Icon == nil
+ if strokeDash == 0 && animated && c.IsBidirectional() {
strokeDash = 5
}
if strokeDash != 0 {
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(c.StrokeWidth), strokeDash)
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
- if c.Animated {
+ if animated {
dashOffset := -10
if c.SrcArrow != NoArrowhead && c.DstArrow == NoArrowhead {
dashOffset = 10
diff --git a/e2etests-cli/main_test.go b/e2etests-cli/main_test.go
index 8d2913f6e9..fdc1e29d7b 100644
--- a/e2etests-cli/main_test.go
+++ b/e2etests-cli/main_test.go
@@ -975,6 +975,24 @@ bank.Equities.app14522 -> bank.Fixed Income.app14500: security reference
testdataIgnoreDiff(t, ".gif", gifBytes)
},
},
+ {
+ name: "animated-connections-gif",
+ // skipCI: true,
+ run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
+ writeFile(t, dir, "in.d2", `a -> b: {style.animated: true; icon: https://icons.terrastruct.com/essentials%2F213-alarm.svg}
+a -> c: {style.animated: true}
+a <-> d: {style.animated: true; style.stroke-dash: 2}
+a <-> e: {style.animated: true}
+f <-> g: {style.animated: true}
+x -- x: {style.animated: true}
+`)
+ err := runTestMain(t, ctx, dir, env, "--animate-interval=1000", "in.d2", "out.gif")
+ assert.Success(t, err)
+
+ gifBytes := readFile(t, dir, "out.gif")
+ testdataIgnoreDiff(t, ".gif", gifBytes)
+ },
+ },
{
name: "stdin",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
diff --git a/e2etests-cli/testdata/TestCLI_E2E/animated-connections-gif.exp.gif b/e2etests-cli/testdata/TestCLI_E2E/animated-connections-gif.exp.gif
new file mode 100644
index 0000000000..8ca25a8974
Binary files /dev/null and b/e2etests-cli/testdata/TestCLI_E2E/animated-connections-gif.exp.gif differ
diff --git a/e2etests/testdata/stable/animated/dagre/sketch.exp.svg b/e2etests/testdata/stable/animated/dagre/sketch.exp.svg
index f801d2fb8c..d779976903 100644
--- a/e2etests/testdata/stable/animated/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/animated/dagre/sketch.exp.svg
@@ -5,6 +5,24 @@
}
}
+@keyframes pathdraw {
+ to {
+ stroke-dashoffset: 0;
+ }
+}
+.animated-connection-grow {
+ animation: pathdraw 1s linear infinite;
+}
+
+@keyframes arrowheadpath {
+ to {
+ offset-distance: 100%;
+ }
+}
+.animated-arrowhead {
+ animation: arrowheadpath 1s linear infinite;
+}
+
.d2-2154070564 .text-bold {
font-family: "d2-2154070564-font-bold";
}
@@ -102,7 +120,7 @@
.d2-2154070564 .color-AA4{color:#EDF0FD;}
.d2-2154070564 .color-AA5{color:#F7F8FE;}
.d2-2154070564 .color-AB4{color:#EDF0FD;}
- .d2-2154070564 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-2154070564);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-2154070564);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-2154070564);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-2154070564);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-2154070564);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-2154070564);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-2154070564);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
+ .d2-2154070564 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-2154070564);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-2154070564);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-2154070564);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-2154070564);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-2154070564);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-2154070564);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-2154070564);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-2154070564);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
\ No newline at end of file
diff --git a/e2etests/testdata/stable/animated/elk/sketch.exp.svg b/e2etests/testdata/stable/animated/elk/sketch.exp.svg
index dcfe9b1067..8c8546c9c9 100644
--- a/e2etests/testdata/stable/animated/elk/sketch.exp.svg
+++ b/e2etests/testdata/stable/animated/elk/sketch.exp.svg
@@ -5,6 +5,24 @@
}
}
+@keyframes pathdraw {
+ to {
+ stroke-dashoffset: 0;
+ }
+}
+.animated-connection-grow {
+ animation: pathdraw 1s linear infinite;
+}
+
+@keyframes arrowheadpath {
+ to {
+ offset-distance: 100%;
+ }
+}
+.animated-arrowhead {
+ animation: arrowheadpath 1s linear infinite;
+}
+
.d2-4012398488 .text-bold {
font-family: "d2-4012398488-font-bold";
}
@@ -102,7 +120,7 @@
.d2-4012398488 .color-AA4{color:#EDF0FD;}
.d2-4012398488 .color-AA5{color:#F7F8FE;}
.d2-4012398488 .color-AB4{color:#EDF0FD;}
- .d2-4012398488 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-4012398488);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-4012398488);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-4012398488);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-4012398488);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-4012398488);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-4012398488);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-4012398488);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
+ .d2-4012398488 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-4012398488);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-4012398488);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-4012398488);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-4012398488);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-4012398488);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-4012398488);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-4012398488);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-4012398488);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/animated-connection-icon/dagre/board.exp.json b/e2etests/testdata/txtar/animated-connection-icon/dagre/board.exp.json
new file mode 100644
index 0000000000..5991fc7294
--- /dev/null
+++ b/e2etests/testdata/txtar/animated-connection-icon/dagre/board.exp.json
@@ -0,0 +1,841 @@
+{
+ "name": "",
+ "config": {
+ "sketch": true,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "HandDrawn",
+ "monoFontFamily": "SourceCodePro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 115,
+ "y": 0
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "a",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 187
+ },
+ "width": 55,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "b",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 10,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "c",
+ "type": "rectangle",
+ "pos": {
+ "x": 115,
+ "y": 187
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "c",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "d",
+ "type": "rectangle",
+ "pos": {
+ "x": 229,
+ "y": 187
+ },
+ "width": 55,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "d",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 10,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "e",
+ "type": "rectangle",
+ "pos": {
+ "x": 344,
+ "y": 187
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "e",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "f",
+ "type": "rectangle",
+ "pos": {
+ "x": 457,
+ "y": 0
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "f",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "g",
+ "type": "rectangle",
+ "pos": {
+ "x": 457,
+ "y": 187
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "g",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "x",
+ "type": "rectangle",
+ "pos": {
+ "x": 571,
+ "y": 0
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "x",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a -> b)[0]",
+ "src": "a",
+ "srcArrow": "none",
+ "dst": "b",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 115.5,
+ "y": 51.5
+ },
+ {
+ "x": 25.89900016784668,
+ "y": 111.5
+ },
+ {
+ "x": 6.699999809265137,
+ "y": 138.6999969482422
+ },
+ {
+ "x": 19.5,
+ "y": 187.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": {
+ "Scheme": "https",
+ "Opaque": "",
+ "User": null,
+ "Host": "icons.terrastruct.com",
+ "Path": "/essentials/213-alarm.svg",
+ "RawPath": "/essentials%2F213-alarm.svg",
+ "OmitHost": false,
+ "ForceQuery": false,
+ "RawQuery": "",
+ "Fragment": "",
+ "RawFragment": ""
+ },
+ "iconPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0
+ },
+ {
+ "id": "(a -> c)[0]",
+ "src": "a",
+ "srcArrow": "none",
+ "dst": "c",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 130.75,
+ "y": 65.5
+ },
+ {
+ "x": 113.1500015258789,
+ "y": 114.30000305175781
+ },
+ {
+ "x": 113.1500015258789,
+ "y": 138.6999969482422
+ },
+ {
+ "x": 130.75,
+ "y": 187.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a -> b)[1]",
+ "src": "a",
+ "srcArrow": "none",
+ "dst": "b",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "label",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 36,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 114.75,
+ "y": 64.5
+ },
+ {
+ "x": 71.55000305175781,
+ "y": 114.0999984741211
+ },
+ {
+ "x": 56.349998474121094,
+ "y": 138.6999969482422
+ },
+ {
+ "x": 38.75,
+ "y": 187.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": {
+ "Scheme": "https",
+ "Opaque": "",
+ "User": null,
+ "Host": "icons.terrastruct.com",
+ "Path": "/essentials/213-alarm.svg",
+ "RawPath": "/essentials%2F213-alarm.svg",
+ "OmitHost": false,
+ "ForceQuery": false,
+ "RawQuery": "",
+ "Fragment": "",
+ "RawFragment": ""
+ },
+ "iconPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0
+ },
+ {
+ "id": "(a -> c)[1]",
+ "src": "a",
+ "srcArrow": "none",
+ "dst": "c",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "label",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 36,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 153.25,
+ "y": 65.5
+ },
+ {
+ "x": 170.85000610351562,
+ "y": 114.30000305175781
+ },
+ {
+ "x": 170.85000610351562,
+ "y": 138.6999969482422
+ },
+ {
+ "x": 153.25,
+ "y": 187.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> d)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "d",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 2,
+ "strokeWidth": 2,
+ "stroke": "B2",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 168.5,
+ "y": 55.5
+ },
+ {
+ "x": 238.89999389648438,
+ "y": 112.30000305175781
+ },
+ {
+ "x": 256.5,
+ "y": 138.6999969482422
+ },
+ {
+ "x": 256.5,
+ "y": 187.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> e)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "e",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 168.5,
+ "y": 44.5
+ },
+ {
+ "x": 330.1000061035156,
+ "y": 110.0999984741211
+ },
+ {
+ "x": 370.5,
+ "y": 138.6999969482422
+ },
+ {
+ "x": 370.5,
+ "y": 187.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(f <-> g)[0]",
+ "src": "f",
+ "srcArrow": "triangle",
+ "dst": "g",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 484,
+ "y": 65.5
+ },
+ {
+ "x": 484,
+ "y": 114.30000305175781
+ },
+ {
+ "x": 484,
+ "y": 138.6999969482422
+ },
+ {
+ "x": 484,
+ "y": 187.5
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(x -- x)[0]",
+ "src": "x",
+ "srcArrow": "none",
+ "dst": "x",
+ "dstArrow": "none",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 624.666015625,
+ "y": 16
+ },
+ {
+ "x": 646.2659912109375,
+ "y": 3.1989998817443848
+ },
+ {
+ "x": 653,
+ "y": 0
+ },
+ {
+ "x": 655,
+ "y": 0
+ },
+ {
+ "x": 657,
+ "y": 0
+ },
+ {
+ "x": 659.666015625,
+ "y": 6.599999904632568
+ },
+ {
+ "x": 661.666015625,
+ "y": 16.5
+ },
+ {
+ "x": 663.666015625,
+ "y": 26.399999618530273
+ },
+ {
+ "x": 663.666015625,
+ "y": 39.599998474121094
+ },
+ {
+ "x": 661.666015625,
+ "y": 49.5
+ },
+ {
+ "x": 659.666015625,
+ "y": 59.400001525878906
+ },
+ {
+ "x": 646.2659912109375,
+ "y": 62.79999923706055
+ },
+ {
+ "x": 624.666015625,
+ "y": 50
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/txtar/animated-connection-icon/dagre/sketch.exp.svg b/e2etests/testdata/txtar/animated-connection-icon/dagre/sketch.exp.svg
new file mode 100644
index 0000000000..d3bd0bd0b0
--- /dev/null
+++ b/e2etests/testdata/txtar/animated-connection-icon/dagre/sketch.exp.svg
@@ -0,0 +1,148 @@
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/animated-connection-icon/elk/board.exp.json b/e2etests/testdata/txtar/animated-connection-icon/elk/board.exp.json
new file mode 100644
index 0000000000..c6c9898a87
--- /dev/null
+++ b/e2etests/testdata/txtar/animated-connection-icon/elk/board.exp.json
@@ -0,0 +1,773 @@
+{
+ "name": "",
+ "config": {
+ "sketch": true,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "HandDrawn",
+ "monoFontFamily": "SourceCodePro",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "rectangle",
+ "pos": {
+ "x": 50,
+ "y": 12
+ },
+ "width": 240,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "a",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "b",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 404
+ },
+ "width": 80,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "b",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 10,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "c",
+ "type": "rectangle",
+ "pos": {
+ "x": 130,
+ "y": 404
+ },
+ "width": 80,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "c",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "d",
+ "type": "rectangle",
+ "pos": {
+ "x": 238,
+ "y": 258
+ },
+ "width": 55,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "d",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 10,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "e",
+ "type": "rectangle",
+ "pos": {
+ "x": 313,
+ "y": 258
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "e",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "f",
+ "type": "rectangle",
+ "pos": {
+ "x": 386,
+ "y": 12
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "f",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "g",
+ "type": "rectangle",
+ "pos": {
+ "x": 386,
+ "y": 258
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "g",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "x",
+ "type": "rectangle",
+ "pos": {
+ "x": 510,
+ "y": 12
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "x",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(a -> b)[0]",
+ "src": "a",
+ "srcArrow": "none",
+ "dst": "b",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 84.28500366210938,
+ "y": 78
+ },
+ {
+ "x": 84.28500366210938,
+ "y": 118
+ },
+ {
+ "x": 22.5,
+ "y": 118
+ },
+ {
+ "x": 22.5,
+ "y": 404
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": {
+ "Scheme": "https",
+ "Opaque": "",
+ "User": null,
+ "Host": "icons.terrastruct.com",
+ "Path": "/essentials/213-alarm.svg",
+ "RawPath": "/essentials%2F213-alarm.svg",
+ "OmitHost": false,
+ "ForceQuery": false,
+ "RawQuery": "",
+ "Fragment": "",
+ "RawFragment": ""
+ },
+ "iconPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0
+ },
+ {
+ "id": "(a -> c)[0]",
+ "src": "a",
+ "srcArrow": "none",
+ "dst": "c",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 140.5,
+ "y": 78
+ },
+ {
+ "x": 140.5,
+ "y": 404
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a -> b)[1]",
+ "src": "a",
+ "srcArrow": "none",
+ "dst": "b",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "label",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 36,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 118.57099914550781,
+ "y": 78
+ },
+ {
+ "x": 118.57099914550781,
+ "y": 168
+ },
+ {
+ "x": 81.5,
+ "y": 168
+ },
+ {
+ "x": 81.5,
+ "y": 404
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": {
+ "Scheme": "https",
+ "Opaque": "",
+ "User": null,
+ "Host": "icons.terrastruct.com",
+ "Path": "/essentials/213-alarm.svg",
+ "RawPath": "/essentials%2F213-alarm.svg",
+ "OmitHost": false,
+ "ForceQuery": false,
+ "RawQuery": "",
+ "Fragment": "",
+ "RawFragment": ""
+ },
+ "iconPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0
+ },
+ {
+ "id": "(a -> c)[1]",
+ "src": "a",
+ "srcArrow": "none",
+ "dst": "c",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "label",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 36,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 199.5,
+ "y": 78
+ },
+ {
+ "x": 199.5,
+ "y": 404
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> d)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "d",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 2,
+ "strokeWidth": 2,
+ "stroke": "B2",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 221.42799377441406,
+ "y": 78
+ },
+ {
+ "x": 221.42799377441406,
+ "y": 168
+ },
+ {
+ "x": 266,
+ "y": 168
+ },
+ {
+ "x": 266,
+ "y": 258
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(a <-> e)[0]",
+ "src": "a",
+ "srcArrow": "triangle",
+ "dst": "e",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 255.71400451660156,
+ "y": 78
+ },
+ {
+ "x": 255.71400451660156,
+ "y": 118
+ },
+ {
+ "x": 340,
+ "y": 118
+ },
+ {
+ "x": 340,
+ "y": 258
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(f <-> g)[0]",
+ "src": "f",
+ "srcArrow": "triangle",
+ "dst": "g",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 413.5,
+ "y": 78
+ },
+ {
+ "x": 413.5,
+ "y": 258
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ },
+ {
+ "id": "(x -- x)[0]",
+ "src": "x",
+ "srcArrow": "none",
+ "dst": "x",
+ "dstArrow": "none",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 510.5,
+ "y": 34
+ },
+ {
+ "x": 460.5,
+ "y": 34
+ },
+ {
+ "x": 460.5,
+ "y": 56
+ },
+ {
+ "x": 510.5,
+ "y": 56
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/txtar/animated-connection-icon/elk/sketch.exp.svg b/e2etests/testdata/txtar/animated-connection-icon/elk/sketch.exp.svg
new file mode 100644
index 0000000000..06d0fee15c
--- /dev/null
+++ b/e2etests/testdata/txtar/animated-connection-icon/elk/sketch.exp.svg
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+abcdefgx labellabel
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/connection-icons/dagre/sketch.exp.svg b/e2etests/testdata/txtar/connection-icons/dagre/sketch.exp.svg
index e1c37c881a..2a62fa12c6 100644
--- a/e2etests/testdata/txtar/connection-icons/dagre/sketch.exp.svg
+++ b/e2etests/testdata/txtar/connection-icons/dagre/sketch.exp.svg
@@ -96,7 +96,7 @@
.d2-970759274 .color-AA4{color:#EDF0FD;}
.d2-970759274 .color-AA5{color:#F7F8FE;}
.d2-970759274 .color-AB4{color:#EDF0FD;}
- .d2-970759274 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-970759274);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-970759274);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-970759274);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-970759274);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-970759274);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-970759274);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-970759274);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>abcproducerconsumer hello
+ .d2-970759274 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-970759274);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-970759274);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-970759274);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-970759274);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-970759274);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-970759274);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-970759274);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-970759274);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>abcproducerconsumer hello
diff --git a/e2etests/testdata/txtar/connection-icons/elk/sketch.exp.svg b/e2etests/testdata/txtar/connection-icons/elk/sketch.exp.svg
index 639b58551a..88c8390dec 100644
--- a/e2etests/testdata/txtar/connection-icons/elk/sketch.exp.svg
+++ b/e2etests/testdata/txtar/connection-icons/elk/sketch.exp.svg
@@ -96,7 +96,7 @@
.d2-674489035 .color-AA4{color:#EDF0FD;}
.d2-674489035 .color-AA5{color:#F7F8FE;}
.d2-674489035 .color-AB4{color:#EDF0FD;}
- .d2-674489035 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-674489035);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-674489035);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-674489035);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-674489035);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-674489035);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-674489035);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-674489035);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>abcproducerconsumer hello
+ .d2-674489035 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-674489035);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-674489035);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-674489035);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-674489035);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-674489035);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-674489035);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-674489035);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-674489035);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>abcproducerconsumer hello
diff --git a/e2etests/testdata/txtar/md-tables/dagre/sketch.exp.svg b/e2etests/testdata/txtar/md-tables/dagre/sketch.exp.svg
index 15abf0516e..409b3420a2 100644
--- a/e2etests/testdata/txtar/md-tables/dagre/sketch.exp.svg
+++ b/e2etests/testdata/txtar/md-tables/dagre/sketch.exp.svg
@@ -16,6 +16,24 @@
}
}
+@keyframes pathdraw {
+ to {
+ stroke-dashoffset: 0;
+ }
+}
+.animated-connection-grow {
+ animation: pathdraw 1s linear infinite;
+}
+
+@keyframes arrowheadpath {
+ to {
+ offset-distance: 100%;
+ }
+}
+.animated-arrowhead {
+ animation: arrowheadpath 1s linear infinite;
+}
+
.d2-1180799665 .text-italic {
font-family: "d2-1180799665-font-italic";
}
@@ -1147,7 +1165,7 @@
- Triggers MonitorsAggregatesAffectsImpactsInformsAssigns
+ Triggers MonitorsAggregatesAffectsImpactsInformsAssigns
diff --git a/e2etests/testdata/txtar/md-tables/elk/sketch.exp.svg b/e2etests/testdata/txtar/md-tables/elk/sketch.exp.svg
index 34a15fe256..ba95eb8983 100644
--- a/e2etests/testdata/txtar/md-tables/elk/sketch.exp.svg
+++ b/e2etests/testdata/txtar/md-tables/elk/sketch.exp.svg
@@ -16,6 +16,24 @@
}
}
+@keyframes pathdraw {
+ to {
+ stroke-dashoffset: 0;
+ }
+}
+.animated-connection-grow {
+ animation: pathdraw 1s linear infinite;
+}
+
+@keyframes arrowheadpath {
+ to {
+ offset-distance: 100%;
+ }
+}
+.animated-arrowhead {
+ animation: arrowheadpath 1s linear infinite;
+}
+
.d2-3839636311 .text-italic {
font-family: "d2-3839636311-font-italic";
}
@@ -1147,7 +1165,7 @@
- Triggers MonitorsAggregatesAffectsImpactsInformsAssigns
+ Triggers MonitorsAggregatesAffectsImpactsInformsAssigns
diff --git a/e2etests/txtar.txt b/e2etests/txtar.txt
index f933cad5eb..d4737842b9 100644
--- a/e2etests/txtar.txt
+++ b/e2etests/txtar.txt
@@ -1758,3 +1758,17 @@ style: {fill-pattern: dots; fill:"radial-gradient(#fbfbf8, #e3e3f0)"; stroke: "#
a->b
+-- animated-connection-icon --
+vars: {
+ d2-config: {
+ sketch: true
+ }
+}
+a -> b: {style.animated: true; icon: https://icons.terrastruct.com/essentials%2F213-alarm.svg}
+a -> c: {style.animated: true}
+a -> b: label {style.animated: true; icon: https://icons.terrastruct.com/essentials%2F213-alarm.svg}
+a -> c: label {style.animated: true}
+a <-> d: {style.animated: true; style.stroke-dash: 2}
+a <-> e: {style.animated: true}
+f <-> g: {style.animated: true}
+x -- x: {style.animated: true}
diff --git a/lib/svg/path.go b/lib/svg/path.go
index 93418f3bba..be99d26d9b 100644
--- a/lib/svg/path.go
+++ b/lib/svg/path.go
@@ -179,7 +179,7 @@ func getPathStringIncrement(pathType string) (int, error) {
}
// This function finds the length of a path in SVG notation
-func pathLength(pathData []string) (float64, error) {
+func PathLength(pathData []string) (float64, error) {
var x, y, pathLength float64
var prevPosition geo.Point
var increment int
@@ -231,7 +231,7 @@ func SplitPath(path string, percentage float64) (string, string, error) {
pastHalf := false
pathData := strings.Split(path, " ")
- pathLen, err := pathLength(pathData)
+ pathLen, err := PathLength(pathData)
if err != nil {
return "", "", err