diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index 15253b895..918b6c2bf 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -495,58 +495,49 @@ func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) strin
)
}
-// Given control points p1, p2, p3, p4, calculate the segment of this bezier curve from t0 -> t1 where {0 <= t0 < t1 <= 1}
-func bezierCurveSegment(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, geo.Point, geo.Point, geo.Point) {
- u0, u1 := 1-t0, 1-t1
+// Gets a certain line/curve's SVG path string. offsetIdx and pathData provides the points needed
+func getSVGPathString(path *string, pathType *string, offsetIdx int, pathData []string) (string, error) {
+ var returnPath string
- q1 := geo.Point{
- X: (u0*u0*u0)*p1.X + (3*t0*u0*u0)*p2.X + (3*t0*t0*u0)*p3.X + t0*t0*t0*p4.X,
- Y: (u0*u0*u0)*p1.Y + (3*t0*u0*u0)*p2.Y + (3*t0*t0*u0)*p3.Y + t0*t0*t0*p4.Y,
- }
- q2 := geo.Point{
- X: (u0*u0*u1)*p1.X + (2*t0*u0*u1+u0*u0*t1)*p2.X + (t0*t0*u1+2*u0*t0*t1)*p3.X + t0*t0*t1*p4.X,
- Y: (u0*u0*u1)*p1.Y + (2*t0*u0*u1+u0*u0*t1)*p2.Y + (t0*t0*u1+2*u0*t0*t1)*p3.Y + t0*t0*t1*p4.Y,
- }
- q3 := geo.Point{
- X: (u0*u1*u1)*p1.X + (t0*u1*u1+2*u0*t1*u1)*p2.X + (2*t0*t1*u1+u0*t1*t1)*p3.X + t0*t1*t1*p4.X,
- Y: (u0*u1*u1)*p1.Y + (t0*u1*u1+2*u0*t1*u1)*p2.Y + (2*t0*t1*u1+u0*t1*t1)*p3.Y + t0*t1*t1*p4.Y,
- }
- q4 := geo.Point{
- X: (u1*u1*u1)*p1.X + (3*t1*u1*u1)*p2.X + (3*t1*t1*u1)*p3.X + t1*t1*t1*p4.X,
- Y: (u1*u1*u1)*p1.Y + (3*t1*u1*u1)*p2.Y + (3*t1*t1*u1)*p3.Y + t1*t1*t1*p4.Y,
+ switch *pathType {
+ case "M":
+ returnPath = fmt.Sprintf("M %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2])
+ case "L":
+ returnPath = fmt.Sprintf("L %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2])
+ case "C":
+ returnPath = 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])
+ case "S":
+ returnPath = fmt.Sprintf("S %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4])
+ default:
+ return "", fmt.Errorf("unknown svg path command \"%s\"", pathData[offsetIdx])
}
- return q1, q2, q3, q4
+ return returnPath, nil
}
-// Adds a certain line/curve to an existing SVG path string. offsetIdx and pathData provides the points needed
-func addToPath(path *string, pathType *string, offsetIdx int, pathData []string) int {
+// Gets how much to increment by on an SVG string to get to the next path command
+func getPathStringIncrement(pathType *string) (int, error) {
var increment int
switch *pathType {
case "M":
- *path += fmt.Sprintf("M %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2])
increment = 3
case "L":
- *path += fmt.Sprintf("L %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2])
increment = 3
case "C":
- *path += 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])
increment = 7
case "S":
- *path += fmt.Sprintf("S %s %s %s %s ", pathData[offsetIdx+1], pathData[offsetIdx+2], pathData[offsetIdx+3], pathData[offsetIdx+4])
increment = 5
default:
- panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[offsetIdx]))
+ return 0, fmt.Errorf("unknown svg path command \"%s\"", *pathType)
}
- return increment
+ return increment, nil
}
// This function finds the length of a path in SVG notation
-func pathLength(pathData []string) float64 {
+func pathLength(pathData []string) (float64, error) {
var x, y, pathLength float64
- var increment int
var prevPosition geo.Point
for i := 0; i < len(pathData); {
@@ -554,50 +545,51 @@ func pathLength(pathData []string) float64 {
case "M":
x, _ = strconv.ParseFloat(pathData[i+1], 64)
y, _ = strconv.ParseFloat(pathData[i+2], 64)
-
- increment = 3
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)
-
- increment = 3
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)
-
- increment = 7
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)
-
- increment = 5
default:
- panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i]))
+ return 0, fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
}
prevPosition = geo.Point{X: x, Y: y}
+ increment, err := getPathStringIncrement(&pathData[i])
+
+ if err != nil {
+ return 0, err
+ }
+
i += increment
}
- return pathLength
+ return pathLength, nil
}
// Splits a SVG path into two SVG paths, with the first path being ~{percentage}% of the path
-func splitPath(path string, percentage float64) (string, string) {
+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 := pathLength(pathData)
+ pathLen, err := pathLength(pathData)
+
+ if err != nil {
+ return "", "", err
+ }
i := 0
@@ -623,13 +615,24 @@ func splitPath(path string, percentage float64) (string, string) {
curPathLen = geo.EuclideanDistance(prevPosition.X, prevPosition.Y, x, y)
default:
- panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i]))
+ return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
}
if pastHalf {
- increment = addToPath(&path2, &pathData[i], i, pathData)
+ curPath, err := getSVGPathString(&path2, &pathData[i], i, pathData)
+ path2 += curPath
+
+ if err != nil {
+ return "", "", err
+ }
+
} else if sumPathLens+curPathLen < pathLen*percentage {
- increment = addToPath(&path1, &pathData[i], i, pathData)
+ curPath, err := getSVGPathString(&path2, &pathData[i], i, pathData)
+ path1 += curPath
+
+ if err != nil {
+ return "", "", err
+ }
}
sumPathLens += curPathLen
@@ -641,8 +644,6 @@ func splitPath(path string, percentage float64) (string, string) {
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)
-
- increment = 3
case "C":
h1x, _ := strconv.ParseFloat(pathData[i+1], 64)
h1y, _ := strconv.ParseFloat(pathData[i+2], 64)
@@ -653,31 +654,33 @@ func splitPath(path string, percentage float64) (string, string) {
heading2 := geo.Point{X: h2x, Y: h2y}
nextPoint := geo.Point{X: x, Y: y}
- q1, q2, q3, q4 := bezierCurveSegment(&prevPosition, &heading1, &heading2, &nextPoint, 0, 0.5)
+ 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 = bezierCurveSegment(&prevPosition, &heading1, &heading2, &nextPoint, 0.5, 1)
+ 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)
-
- increment = 7
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])
-
- increment = 5
default:
- panic(fmt.Sprintf("unknown svg path command \"%s\"", pathData[i]))
+ return "", "", fmt.Errorf("unknown svg path command \"%s\"", pathData[i])
}
pastHalf = true
}
+ increment, err := getPathStringIncrement(&pathData[i])
+
+ if err != nil {
+ return "", "", err
+ }
+
i += increment
prevPosition = geo.Point{X: x, Y: y}
}
- return path1, path2
+ 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) {
@@ -766,7 +769,11 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask)
fmt.Fprint(writer, pathEl.Render())
} else {
- path1, path2 := splitPath(path, 0.5)
+ path1, path2, err := splitPath(path, 0.5)
+
+ if err != nil {
+ return "", err
+ }
pathEl1 := d2themes.NewThemableElement("path")
pathEl1.D = path1
diff --git a/e2etests/testdata/txtar.txt b/e2etests/testdata/txtar.txt
index b5b0bece6..706a29a47 100644
--- a/e2etests/testdata/txtar.txt
+++ b/e2etests/testdata/txtar.txt
@@ -220,6 +220,7 @@ a <-> c: {style.animated: true}
a <-> d: {style.animated: true}
a <-> e: {style.animated: true}
f <-> g: {style.animated: true}
+x -- x: {style.animated: true}
-- opacity-zero-route --
grid: {
diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json
index 1c4c39898..16c3d88b9 100644
--- a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json
+++ b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/board.exp.json
@@ -289,6 +289,47 @@
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
+ },
+ {
+ "id": "x",
+ "type": "rectangle",
+ "pos": {
+ "x": 566,
+ "y": 0
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
}
],
"connections": [
@@ -526,6 +567,89 @@
"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,
+ "route": [
+ {
+ "x": 619.166015625,
+ "y": 16
+ },
+ {
+ "x": 639.9660034179688,
+ "y": 3.1989998817443848
+ },
+ {
+ "x": 646.5,
+ "y": 0
+ },
+ {
+ "x": 648.5,
+ "y": 0
+ },
+ {
+ "x": 650.5,
+ "y": 0
+ },
+ {
+ "x": 653.166015625,
+ "y": 6.599999904632568
+ },
+ {
+ "x": 655.166015625,
+ "y": 16.5
+ },
+ {
+ "x": 657.166015625,
+ "y": 26.399999618530273
+ },
+ {
+ "x": 657.166015625,
+ "y": 39.599998474121094
+ },
+ {
+ "x": 655.166015625,
+ "y": 49.5
+ },
+ {
+ "x": 653.166015625,
+ "y": 59.400001525878906
+ },
+ {
+ "x": 639.9660034179688,
+ "y": 62.79999923706055
+ },
+ {
+ "x": 619.166015625,
+ "y": 50
+ }
+ ],
+ "isCurve": true,
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
}
],
"root": {
diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg
index 5c7dc4c21..030120280 100644
--- a/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg
+++ b/e2etests/testdata/txtar/bidirectional_connection_animation/dagre/sketch.exp.svg
@@ -1,16 +1,16 @@
-
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json
index e121ae7ac..85d12fc27 100644
--- a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json
+++ b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/board.exp.json
@@ -289,6 +289,47 @@
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
+ },
+ {
+ "id": "x",
+ "type": "rectangle",
+ "pos": {
+ "x": 427,
+ "y": 12
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "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": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
}
],
"connections": [
@@ -513,6 +554,52 @@
"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,
+ "route": [
+ {
+ "x": 427.5,
+ "y": 34
+ },
+ {
+ "x": 377.5,
+ "y": 34
+ },
+ {
+ "x": 377.5,
+ "y": 56
+ },
+ {
+ "x": 427.5,
+ "y": 56
+ }
+ ],
+ "animated": true,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
}
],
"root": {
diff --git a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg
index 2beadaab6..5bdfdd2c5 100644
--- a/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg
+++ b/e2etests/testdata/txtar/bidirectional_connection_animation/elk/sketch.exp.svg
@@ -1,16 +1,16 @@
-abcdefg
-
+ .d2-2574831872 .fill-N1{fill:#0A0F25;}
+ .d2-2574831872 .fill-N2{fill:#676C7E;}
+ .d2-2574831872 .fill-N3{fill:#9499AB;}
+ .d2-2574831872 .fill-N4{fill:#CFD2DD;}
+ .d2-2574831872 .fill-N5{fill:#DEE1EB;}
+ .d2-2574831872 .fill-N6{fill:#EEF1F8;}
+ .d2-2574831872 .fill-N7{fill:#FFFFFF;}
+ .d2-2574831872 .fill-B1{fill:#0D32B2;}
+ .d2-2574831872 .fill-B2{fill:#0D32B2;}
+ .d2-2574831872 .fill-B3{fill:#E3E9FD;}
+ .d2-2574831872 .fill-B4{fill:#E3E9FD;}
+ .d2-2574831872 .fill-B5{fill:#EDF0FD;}
+ .d2-2574831872 .fill-B6{fill:#F7F8FE;}
+ .d2-2574831872 .fill-AA2{fill:#4A6FF3;}
+ .d2-2574831872 .fill-AA4{fill:#EDF0FD;}
+ .d2-2574831872 .fill-AA5{fill:#F7F8FE;}
+ .d2-2574831872 .fill-AB4{fill:#EDF0FD;}
+ .d2-2574831872 .fill-AB5{fill:#F7F8FE;}
+ .d2-2574831872 .stroke-N1{stroke:#0A0F25;}
+ .d2-2574831872 .stroke-N2{stroke:#676C7E;}
+ .d2-2574831872 .stroke-N3{stroke:#9499AB;}
+ .d2-2574831872 .stroke-N4{stroke:#CFD2DD;}
+ .d2-2574831872 .stroke-N5{stroke:#DEE1EB;}
+ .d2-2574831872 .stroke-N6{stroke:#EEF1F8;}
+ .d2-2574831872 .stroke-N7{stroke:#FFFFFF;}
+ .d2-2574831872 .stroke-B1{stroke:#0D32B2;}
+ .d2-2574831872 .stroke-B2{stroke:#0D32B2;}
+ .d2-2574831872 .stroke-B3{stroke:#E3E9FD;}
+ .d2-2574831872 .stroke-B4{stroke:#E3E9FD;}
+ .d2-2574831872 .stroke-B5{stroke:#EDF0FD;}
+ .d2-2574831872 .stroke-B6{stroke:#F7F8FE;}
+ .d2-2574831872 .stroke-AA2{stroke:#4A6FF3;}
+ .d2-2574831872 .stroke-AA4{stroke:#EDF0FD;}
+ .d2-2574831872 .stroke-AA5{stroke:#F7F8FE;}
+ .d2-2574831872 .stroke-AB4{stroke:#EDF0FD;}
+ .d2-2574831872 .stroke-AB5{stroke:#F7F8FE;}
+ .d2-2574831872 .background-color-N1{background-color:#0A0F25;}
+ .d2-2574831872 .background-color-N2{background-color:#676C7E;}
+ .d2-2574831872 .background-color-N3{background-color:#9499AB;}
+ .d2-2574831872 .background-color-N4{background-color:#CFD2DD;}
+ .d2-2574831872 .background-color-N5{background-color:#DEE1EB;}
+ .d2-2574831872 .background-color-N6{background-color:#EEF1F8;}
+ .d2-2574831872 .background-color-N7{background-color:#FFFFFF;}
+ .d2-2574831872 .background-color-B1{background-color:#0D32B2;}
+ .d2-2574831872 .background-color-B2{background-color:#0D32B2;}
+ .d2-2574831872 .background-color-B3{background-color:#E3E9FD;}
+ .d2-2574831872 .background-color-B4{background-color:#E3E9FD;}
+ .d2-2574831872 .background-color-B5{background-color:#EDF0FD;}
+ .d2-2574831872 .background-color-B6{background-color:#F7F8FE;}
+ .d2-2574831872 .background-color-AA2{background-color:#4A6FF3;}
+ .d2-2574831872 .background-color-AA4{background-color:#EDF0FD;}
+ .d2-2574831872 .background-color-AA5{background-color:#F7F8FE;}
+ .d2-2574831872 .background-color-AB4{background-color:#EDF0FD;}
+ .d2-2574831872 .background-color-AB5{background-color:#F7F8FE;}
+ .d2-2574831872 .color-N1{color:#0A0F25;}
+ .d2-2574831872 .color-N2{color:#676C7E;}
+ .d2-2574831872 .color-N3{color:#9499AB;}
+ .d2-2574831872 .color-N4{color:#CFD2DD;}
+ .d2-2574831872 .color-N5{color:#DEE1EB;}
+ .d2-2574831872 .color-N6{color:#EEF1F8;}
+ .d2-2574831872 .color-N7{color:#FFFFFF;}
+ .d2-2574831872 .color-B1{color:#0D32B2;}
+ .d2-2574831872 .color-B2{color:#0D32B2;}
+ .d2-2574831872 .color-B3{color:#E3E9FD;}
+ .d2-2574831872 .color-B4{color:#E3E9FD;}
+ .d2-2574831872 .color-B5{color:#EDF0FD;}
+ .d2-2574831872 .color-B6{color:#F7F8FE;}
+ .d2-2574831872 .color-AA2{color:#4A6FF3;}
+ .d2-2574831872 .color-AA4{color:#EDF0FD;}
+ .d2-2574831872 .color-AA5{color:#F7F8FE;}
+ .d2-2574831872 .color-AB4{color:#EDF0FD;}
+ .d2-2574831872 .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}]]>abcdefgx
+
@@ -104,4 +104,5 @@
+
\ No newline at end of file
diff --git a/lib/svg/path.go b/lib/svg/path.go
index cc3ac8ef2..eddbb970f 100644
--- a/lib/svg/path.go
+++ b/lib/svg/path.go
@@ -114,3 +114,28 @@ func GetStrokeDashAttributes(strokeWidth, dashGapSize float64) (float64, float64
scaledGapSize := scale * scaledDashSize
return scaledDashSize, scaledGapSize
}
+
+// Given control points p1, p2, p3, p4, calculate the segment of this bezier curve from t0 -> t1 where {0 <= t0 < t1 <= 1}.
+// Uses De Casteljau's algorithm, referenced: https://stackoverflow.com/questions/11703283/cubic-bezier-curve-segment/11704152#11704152
+func BezierCurveSegment(p1, p2, p3, p4 *geo.Point, t0, t1 float64) (geo.Point, geo.Point, geo.Point, geo.Point) {
+ u0, u1 := 1-t0, 1-t1
+
+ q1 := geo.Point{
+ X: (u0*u0*u0)*p1.X + (3*t0*u0*u0)*p2.X + (3*t0*t0*u0)*p3.X + t0*t0*t0*p4.X,
+ Y: (u0*u0*u0)*p1.Y + (3*t0*u0*u0)*p2.Y + (3*t0*t0*u0)*p3.Y + t0*t0*t0*p4.Y,
+ }
+ q2 := geo.Point{
+ X: (u0*u0*u1)*p1.X + (2*t0*u0*u1+u0*u0*t1)*p2.X + (t0*t0*u1+2*u0*t0*t1)*p3.X + t0*t0*t1*p4.X,
+ Y: (u0*u0*u1)*p1.Y + (2*t0*u0*u1+u0*u0*t1)*p2.Y + (t0*t0*u1+2*u0*t0*t1)*p3.Y + t0*t0*t1*p4.Y,
+ }
+ q3 := geo.Point{
+ X: (u0*u1*u1)*p1.X + (t0*u1*u1+2*u0*t1*u1)*p2.X + (2*t0*t1*u1+u0*t1*t1)*p3.X + t0*t1*t1*p4.X,
+ Y: (u0*u1*u1)*p1.Y + (t0*u1*u1+2*u0*t1*u1)*p2.Y + (2*t0*t1*u1+u0*t1*t1)*p3.Y + t0*t1*t1*p4.Y,
+ }
+ q4 := geo.Point{
+ X: (u1*u1*u1)*p1.X + (3*t1*u1*u1)*p2.X + (3*t1*t1*u1)*p3.X + t1*t1*t1*p4.X,
+ Y: (u1*u1*u1)*p1.Y + (3*t1*u1*u1)*p2.Y + (3*t1*t1*u1)*p3.Y + t1*t1*t1*p4.Y,
+ }
+
+ return q1, q2, q3, q4
+}