Merge pull request #1939 from 23danielsuh/issue1936

fix: makes bidirectional animations correct
This commit is contained in:
Alexander Wang 2024-05-19 09:29:41 -06:00 committed by GitHub
commit 0a54cb48f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1805 additions and 12 deletions

View file

@ -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 ⛑️

View file

@ -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 != "" {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -214,6 +214,14 @@ ok: {
dog1 -> dog3
}
-- bidirectional_connection_animation --
a <-> b: {style.animated: true}
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: {
*.style.opacity: 0

View file

@ -0,0 +1,695 @@
{
"name": "",
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",
"type": "rectangle",
"pos": {
"x": 170,
"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": "a",
"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": "b",
"type": "rectangle",
"pos": {
"x": 0,
"y": 166
},
"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": "b",
"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": "c",
"type": "rectangle",
"pos": {
"x": 113,
"y": 166
},
"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": "c",
"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": "d",
"type": "rectangle",
"pos": {
"x": 226,
"y": 166
},
"width": 54,
"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": "d",
"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": "e",
"type": "rectangle",
"pos": {
"x": 340,
"y": 166
},
"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": "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": 455,
"y": 0
},
"width": 51,
"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": "f",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 6,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "g",
"type": "rectangle",
"pos": {
"x": 453,
"y": 166
},
"width": 54,
"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": "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": 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": [
{
"id": "(a <-> b)[0]",
"src": "a",
"srcArrow": "triangle",
"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,
"route": [
{
"x": 169.75,
"y": 45.957000732421875
},
{
"x": 55.14899826049805,
"y": 101.99099731445312
},
{
"x": 26.5,
"y": 126
},
{
"x": 26.5,
"y": 166
}
],
"isCurve": true,
"animated": true,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(a <-> c)[0]",
"src": "a",
"srcArrow": "triangle",
"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,
"route": [
{
"x": 173.5,
"y": 66
},
{
"x": 146.3000030517578,
"y": 106
},
{
"x": 139.5,
"y": 126
},
{
"x": 139.5,
"y": 166
}
],
"isCurve": true,
"animated": true,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(a <-> d)[0]",
"src": "a",
"srcArrow": "triangle",
"dst": "d",
"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,
"route": [
{
"x": 219,
"y": 66
},
{
"x": 246.1999969482422,
"y": 106
},
{
"x": 253,
"y": 126
},
{
"x": 253,
"y": 166
}
],
"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,
"route": [
{
"x": 222.5,
"y": 46
},
{
"x": 337.70001220703125,
"y": 102
},
{
"x": 366.5,
"y": 126
},
{
"x": 366.5,
"y": 166
}
],
"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,
"route": [
{
"x": 480,
"y": 66
},
{
"x": 480,
"y": 106
},
{
"x": 480,
"y": 126
},
{
"x": 480,
"y": 166
}
],
"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,
"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": {
"id": "",
"type": "",
"pos": {
"x": 0,
"y": 0
},
"width": 0,
"height": 0,
"opacity": 0,
"strokeDash": 0,
"strokeWidth": 0,
"borderRadius": 0,
"fill": "N7",
"stroke": "",
"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
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,645 @@
{
"name": "",
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "a",
"type": "rectangle",
"pos": {
"x": 68,
"y": 12
},
"width": 160,
"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": "a",
"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": "b",
"type": "rectangle",
"pos": {
"x": 12,
"y": 208
},
"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": "b",
"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": "c",
"type": "rectangle",
"pos": {
"x": 85,
"y": 208
},
"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": "c",
"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": "d",
"type": "rectangle",
"pos": {
"x": 158,
"y": 208
},
"width": 54,
"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": "d",
"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": "e",
"type": "rectangle",
"pos": {
"x": 232,
"y": 208
},
"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": "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": 306,
"y": 12
},
"width": 51,
"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": "f",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 6,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "g",
"type": "rectangle",
"pos": {
"x": 305,
"y": 208
},
"width": 54,
"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": "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": 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": [
{
"id": "(a <-> b)[0]",
"src": "a",
"srcArrow": "triangle",
"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,
"route": [
{
"x": 100.25,
"y": 78
},
{
"x": 100.25,
"y": 118
},
{
"x": 38.5,
"y": 118
},
{
"x": 38.5,
"y": 208
}
],
"animated": true,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(a <-> c)[0]",
"src": "a",
"srcArrow": "triangle",
"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,
"route": [
{
"x": 132.25,
"y": 78
},
{
"x": 132.25,
"y": 168
},
{
"x": 111.5,
"y": 168
},
{
"x": 111.5,
"y": 208
}
],
"animated": true,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(a <-> d)[0]",
"src": "a",
"srcArrow": "triangle",
"dst": "d",
"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,
"route": [
{
"x": 164.25,
"y": 78
},
{
"x": 164.25,
"y": 168
},
{
"x": 185,
"y": 168
},
{
"x": 185,
"y": 208
}
],
"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,
"route": [
{
"x": 196.25,
"y": 78
},
{
"x": 196.25,
"y": 118
},
{
"x": 258.5,
"y": 118
},
{
"x": 258.5,
"y": 208
}
],
"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,
"route": [
{
"x": 332,
"y": 78
},
{
"x": 332,
"y": 208
}
],
"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,
"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": {
"id": "",
"type": "",
"pos": {
"x": 0,
"y": 0
},
"width": 0,
"height": 0,
"opacity": 0,
"strokeDash": 0,
"strokeWidth": 0,
"borderRadius": 0,
"fill": "N7",
"stroke": "",
"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
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -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
}