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 @@ + + + + + + + + + + + + +abcdefgx labellabel + + + + \ 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