diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index 0d237aa22..2728467b1 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -3,6 +3,7 @@
#### Improvements 🧹
- Opacity 0 shapes no longer have a label mask which made any segment of connections going through them lower opacity [#1940](https://github.com/terrastruct/d2/pull/1940)
+- Bidirectional connections are now animated in opposite directions rather than one direction [#1939](https://github.com/terrastruct/d2/pull/1939)
#### Bugfixes ⛑️
diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index db551fc50..9faf2a361 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -11,6 +11,7 @@ import (
"html"
"io"
"sort"
+ "strconv"
"strings"
"math"
@@ -494,6 +495,178 @@ func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) strin
)
}
+// Gets a certain line/curve's SVG path string. offsetIdx and pathData provides the points needed
+func getSVGPathString(pathType string, offsetIdx int, pathData []string) (string, error) {
+ switch pathType {
+ case "M":
+ return fmt.Sprintf("M %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]), nil
+ case "L":
+ return fmt.Sprintf("L %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2]), nil
+ case "C":
+ return fmt.Sprintf("C %s %s %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4], pathData[offsetIdx+5], pathData[offsetIdx+6]), nil
+ case "S":
+ return fmt.Sprintf("S %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4]), nil
+ default:
+ return "", fmt.Errorf("unknown svg path command \"%s\"", pathData[offsetIdx])
+ }
+}
+
+// Gets how much to increment by on an SVG string to get to the next path command
+func getPathStringIncrement(pathType string) (int, error) {
+ switch pathType {
+ case "M":
+ return 3, nil
+ case "L":
+ return 3, nil
+ case "C":
+ return 7, nil
+ case "S":
+ return 5, nil
+ default:
+ return 0, fmt.Errorf("unknown svg path command \"%s\"", pathType)
+ }
+}
+
+// This function finds the length of a path in SVG notation
+func pathLength(pathData []string) (float64, error) {
+ var x, y, pathLength float64
+ var prevPosition geo.Point
+ var increment int
+
+ for i := 0; i < len(pathData); i += increment {
+ switch pathData[i] {
+ case "M":
+ x, _ = strconv.ParseFloat(pathData[i+1], 64)
+ y, _ = strconv.ParseFloat(pathData[i+2], 64)
+ case "L":
+ x, _ = strconv.ParseFloat(pathData[i+1], 64)
+ y, _ = strconv.ParseFloat(pathData[i+2], 64)
+
+ pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y)
+ case "C":
+ x, _ = strconv.ParseFloat(pathData[i+5], 64)
+ y, _ = strconv.ParseFloat(pathData[i+6], 64)
+
+ pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y)
+ case "S":
+ x, _ = strconv.ParseFloat(pathData[i+3], 64)
+ y, _ = strconv.ParseFloat(pathData[i+4], 64)
+
+ pathLength += geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y)
+ default:
+ return 0, fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
+ }
+
+ prevPosition = geo.Point{X: x, Y: y}
+
+ incr, err := getPathStringIncrement(pathData[i])
+
+ if err != nil {
+ return 0, err
+ }
+
+ increment = incr
+ }
+
+ return pathLength, nil
+}
+
+// Splits an SVG path into two SVG paths, with the first path being ~{percentage}% of the path
+func splitPath(path string, percentage float64) (string, string, error) {
+ var sumPathLens, curPathLen, x, y float64
+ var prevPosition geo.Point
+ var path1, path2 string
+ var increment int
+
+ pastHalf := false
+ pathData := strings.Split(path, " ")
+ pathLen, err := pathLength(pathData)
+
+ if err != nil {
+ return "", "", err
+ }
+
+ for i := 0; i < len(pathData); i += increment {
+ switch pathData[i] {
+ case "M":
+ x, _ = strconv.ParseFloat(pathData[i+1], 64)
+ y, _ = strconv.ParseFloat(pathData[i+2], 64)
+ case "L":
+ x, _ = strconv.ParseFloat(pathData[i+1], 64)
+ y, _ = strconv.ParseFloat(pathData[i+2], 64)
+
+ curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y)
+ case "C":
+ x, _ = strconv.ParseFloat(pathData[i+5], 64)
+ y, _ = strconv.ParseFloat(pathData[i+6], 64)
+
+ curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y)
+ case "S":
+ x, _ = strconv.ParseFloat(pathData[i+3], 64)
+ y, _ = strconv.ParseFloat(pathData[i+4], 64)
+
+ curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y)
+ default:
+ return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
+ }
+
+ curPath, err := getSVGPathString(pathData[i], i, pathData)
+ if err != nil {
+ return "", "", err
+ }
+
+ sumPathLens += curPathLen
+
+ if pastHalf { // add to path2
+ path2 += curPath
+ } else if sumPathLens < pathLen*percentage { // add to path1
+ path1 += curPath
+ } else { // transition from path1 -> path2
+ t := (pathLen*percentage - sumPathLens + curPathLen) / curPathLen
+
+ switch pathData[i] {
+ case "L":
+ path1 += fmt.Sprintf("L %f %f ", (x-prevPosition.X)*t+prevPosition.X, (y-prevPosition.Y)*t+prevPosition.Y)
+ path2 += fmt.Sprintf("M %f %f L %f %f ", (x-prevPosition.X)*t+prevPosition.X, (y-prevPosition.Y)*t+prevPosition.Y, x, y)
+ case "C":
+ h1x, _ := strconv.ParseFloat(pathData[i+1], 64)
+ h1y, _ := strconv.ParseFloat(pathData[i+2], 64)
+ h2x, _ := strconv.ParseFloat(pathData[i+3], 64)
+ h2y, _ := strconv.ParseFloat(pathData[i+4], 64)
+
+ heading1 := geo.Point{X: h1x, Y: h1y}
+ heading2 := geo.Point{X: h2x, Y: h2y}
+ nextPoint := geo.Point{X: x, Y: y}
+
+ q1, q2, q3, q4 := svg.BezierCurveSegment(&prevPosition, &heading1, &heading2, &nextPoint, 0, 0.5)
+ path1 += fmt.Sprintf("C %f %f %f %f %f %f ", q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y)
+
+ q1, q2, q3, q4 = svg.BezierCurveSegment(&prevPosition, &heading1, &heading2, &nextPoint, 0.5, 1)
+ path2 += fmt.Sprintf("M %f %f C %f %f %f %f %f %f ", q1.X, q1.Y, q2.X, q2.Y, q3.X, q3.Y, q4.X, q4.Y)
+ case "S":
+ // Skip S curves because they are shorter and we can split along the connection to the next path instead
+ path1 += fmt.Sprintf("S %s %s %s %s ", pathData[i+1], pathData[i+2], pathData[i+3], pathData[i+4])
+ path2 += fmt.Sprintf("M %s %s ", pathData[i+3], pathData[i+4])
+ default:
+ return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
+ }
+
+ pastHalf = true
+ }
+
+ incr, err := getPathStringIncrement(pathData[i])
+
+ if err != nil {
+ return "", "", err
+ }
+
+ increment = incr
+ prevPosition = geo.Point{X: x, Y: y}
+ }
+
+ return path1, path2, nil
+}
+
func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, _ error) {
opacityStyle := ""
if connection.Opacity != 1.0 {
@@ -549,6 +722,7 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
srcAdj, dstAdj := getArrowheadAdjustments(connection, idToShape)
path := pathData(connection, srcAdj, dstAdj)
mask := fmt.Sprintf(`mask="url(#%s)"`, labelMaskID)
+
if sketchRunner != nil {
out, err := d2sketch.Connection(sketchRunner, connection, path, mask)
if err != nil {
@@ -568,14 +742,43 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
animatedClass = " animated-connection"
}
- pathEl := d2themes.NewThemableElement("path")
- pathEl.D = path
- pathEl.Fill = color.None
- pathEl.Stroke = connection.Stroke
- pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
- pathEl.Style = connection.CSSStyle()
- pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask)
- fmt.Fprint(writer, pathEl.Render())
+ // If connection is animated and bidirectional
+ if connection.Animated && ((connection.DstArrow == d2target.NoArrowhead && connection.SrcArrow == d2target.NoArrowhead) || (connection.DstArrow != d2target.NoArrowhead && connection.SrcArrow != d2target.NoArrowhead)) {
+ // There is no pure CSS way to animate bidirectional connections in two directions, so we split it up
+ path1, path2, err := splitPath(path, 0.5)
+
+ if err != nil {
+ return "", err
+ }
+
+ pathEl1 := d2themes.NewThemableElement("path")
+ pathEl1.D = path1
+ pathEl1.Fill = color.None
+ pathEl1.Stroke = connection.Stroke
+ pathEl1.ClassName = fmt.Sprintf("connection%s", animatedClass)
+ pathEl1.Style = connection.CSSStyle()
+ pathEl1.Style += "animation-direction: reverse;"
+ pathEl1.Attributes = fmt.Sprintf("%s%s", markerStart, mask)
+ fmt.Fprint(writer, pathEl1.Render())
+
+ pathEl2 := d2themes.NewThemableElement("path")
+ pathEl2.D = path2
+ pathEl2.Fill = color.None
+ pathEl2.Stroke = connection.Stroke
+ pathEl2.ClassName = fmt.Sprintf("connection%s", animatedClass)
+ pathEl2.Style = connection.CSSStyle()
+ pathEl2.Attributes = fmt.Sprintf("%s%s", markerEnd, mask)
+ fmt.Fprint(writer, pathEl2.Render())
+ } else {
+ pathEl := d2themes.NewThemableElement("path")
+ pathEl.D = path
+ pathEl.Fill = color.None
+ pathEl.Stroke = connection.Stroke
+ pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
+ pathEl.Style = connection.CSSStyle()
+ pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask)
+ fmt.Fprint(writer, pathEl.Render())
+ }
}
if connection.Label != "" {
diff --git a/e2etests/testdata/stable/animated/dagre/sketch.exp.svg b/e2etests/testdata/stable/animated/dagre/sketch.exp.svg
index dc10385f0..94ca46c77 100644
--- a/e2etests/testdata/stable/animated/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/animated/dagre/sketch.exp.svg
@@ -102,7 +102,7 @@
.d2-3267239171 .color-AA4{color:#EDF0FD;}
.d2-3267239171 .color-AA5{color:#F7F8FE;}
.d2-3267239171 .color-AB4{color:#EDF0FD;}
- .d2-3267239171 .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);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
+ .d2-3267239171 .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);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
diff --git a/e2etests/testdata/stable/animated/elk/sketch.exp.svg b/e2etests/testdata/stable/animated/elk/sketch.exp.svg
index 02e88aef7..bffee0710 100644
--- a/e2etests/testdata/stable/animated/elk/sketch.exp.svg
+++ b/e2etests/testdata/stable/animated/elk/sketch.exp.svg
@@ -102,7 +102,7 @@
.d2-838869033 .color-AA4{color:#EDF0FD;}
.d2-838869033 .color-AA5{color:#F7F8FE;}
.d2-838869033 .color-AB4{color:#EDF0FD;}
- .d2-838869033 .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);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
+ .d2-838869033 .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);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>your love life will behappyharmoniousboredomimmortalityFridayMondayInsomniaSleepWakeDreamListenTalk hear
diff --git a/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg b/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg
index 0cd9b646b..a6b122b6a 100644
--- a/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg
@@ -98,7 +98,7 @@
.d2-3096218097 .color-AA4{color:#EDF0FD;}
.d2-3096218097 .color-AA5{color:#F7F8FE;}
.d2-3096218097 .color-AB4{color:#EDF0FD;}
- .d2-3096218097 .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);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>xyPKabFK I like turtles