diff --git a/.prettierignore b/.prettierignore index c8786b7a31..d87b1d9297 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,5 @@ d2renderers/d2latex/mathjax.js d2renderers/d2latex/polyfills.js d2renderers/d2latex/setup.js -d2renderers/d2sketch/rough.js lib/png/generate_png.js d2js diff --git a/README.md b/README.md index d70bfc7d9f..07681ca701 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,9 @@ D2 intends to integrate with a variety of layout engines, e.g. `dot`, as well as single-purpose layout types like sequence diagrams. You can choose whichever layout engine you like and works best for the diagram you're making. +Sketch-mode rendering uses [rough-go](https://github.com/d2lang/rough-go), a native Go +port of Rough.js 4.0.4. + ## Comparison For a comparison against other popular text-to-diagram tools, see diff --git a/THIRD_PARTY_NOTICES.txt b/THIRD_PARTY_NOTICES.txt index 3bea45939e..cac36eb9aa 100644 --- a/THIRD_PARTY_NOTICES.txt +++ b/THIRD_PARTY_NOTICES.txt @@ -37,6 +37,38 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Rough-go +https://github.com/d2lang/rough-go + +Rough-go is a native Go port of Rough.js 4.0.4. The port is based on: + + Rough.js 4.0.4 + https://github.com/rough-stuff/rough + commit 35302b8f1119ad004b4550f9a4d4162d3d845b0a + +MIT License + +Copyright (c) 2019 Preet Shihn +Copyright (c) 2026 rough-go contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + elk-go https://github.com/d2lang/elk-go diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 9dff0c0fa8..6522546130 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -5,6 +5,7 @@ #### Improvements 🧹 - d2dagre: replace the embedded JavaScript runtime with the native Go Dagro port without changing layout output +- d2sketch: replace the embedded Rough.js runtime with the native Go rough-go port without changing sketch output - d2elk: replace the embedded JavaScript runtime with the native Go elk-go port, preserving layouts apart from sub-pixel floating-point rounding - d2ascii: - sql_table and uml class shapes are supported [#2623](https://github.com/d2lang/d2/pull/2623) diff --git a/d2js/js/THIRD_PARTY_NOTICES.txt b/d2js/js/THIRD_PARTY_NOTICES.txt index 3bea45939e..cac36eb9aa 100644 --- a/d2js/js/THIRD_PARTY_NOTICES.txt +++ b/d2js/js/THIRD_PARTY_NOTICES.txt @@ -37,6 +37,38 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Rough-go +https://github.com/d2lang/rough-go + +Rough-go is a native Go port of Rough.js 4.0.4. The port is based on: + + Rough.js 4.0.4 + https://github.com/rough-stuff/rough + commit 35302b8f1119ad004b4550f9a4d4162d3d845b0a + +MIT License + +Copyright (c) 2019 Preet Shihn +Copyright (c) 2026 rough-go contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + elk-go https://github.com/d2lang/elk-go diff --git a/d2renderers/d2sketch/NOTICE.txt b/d2renderers/d2sketch/NOTICE.txt new file mode 100644 index 0000000000..5ceedefc35 --- /dev/null +++ b/d2renderers/d2sketch/NOTICE.txt @@ -0,0 +1,4 @@ +The native sketch renderer uses rough-go, a Go port of Rough.js 4.0.4. + +Source and license details are included in the repository-root +THIRD_PARTY_NOTICES.txt and in https://github.com/d2lang/rough-go. diff --git a/d2renderers/d2sketch/rough.js b/d2renderers/d2sketch/rough.js deleted file mode 100644 index b19e8c7d72..0000000000 --- a/d2renderers/d2sketch/rough.js +++ /dev/null @@ -1,1673 +0,0 @@ -/*eslint-disable */ -// This is a slightly modified version of rough.js for D2 -// -// rough.js is from https://github.com/rough-stuff/rough. -// Attribution for this file is as follows: -// -// MIT License -// -// Copyright (c) 2019 Preet Shihn -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -const t = "http://www.w3.org/2000/svg"; -function e(t, e, s) { - if (t && t.length) { - const [n, i] = e, - a = (Math.PI / 180) * s, - o = Math.cos(a), - h = Math.sin(a); - t.forEach((t) => { - const [e, s] = t; - (t[0] = (e - n) * o - (s - i) * h + n), (t[1] = (e - n) * h + (s - i) * o + i); - }); - } -} -function s(t) { - const e = t[0], - s = t[1]; - return Math.sqrt(Math.pow(e[0] - s[0], 2) + Math.pow(e[1] - s[1], 2)); -} -function n(t, e) { - return t.type === e; -} -const i = { - A: 7, - a: 7, - C: 6, - c: 6, - H: 1, - h: 1, - L: 2, - l: 2, - M: 2, - m: 2, - Q: 4, - q: 4, - S: 4, - s: 4, - T: 4, - t: 2, - V: 1, - v: 1, - Z: 0, - z: 0, -}; -class a { - constructor(t) { - (this.COMMAND = 0), - (this.NUMBER = 1), - (this.EOD = 2), - (this.segments = []), - this.parseData(t), - this.processPoints(); - } - tokenize(t) { - const e = new Array(); - - for (; "" !== t; ) { - const a = t.match(/^([ \t\r\n,]+)/); - const b = t.match(/^([aAcChHlLmMqQsStTvVzZ])/); - const c = t.match(/^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/); - if (a) t = t.substr(a[0].length); - else if (b) - (e[e.length] = { type: this.COMMAND, text: b[0] }), (t = t.substr(b[0].length)); - else { - if (!c) return []; - (e[e.length] = { type: this.NUMBER, text: `${parseFloat(c[0])}` }), - (t = t.substr(c[0].length)); - } - } - return (e[e.length] = { type: this.EOD, text: "" }), e; - } - parseData(t) { - const e = this.tokenize(t); - let s = 0, - a = e[s], - o = "BOD"; - for (this.segments = new Array(); !n(a, this.EOD); ) { - let h; - const r = new Array(); - if ("BOD" === o) { - if ("M" !== a.text && "m" !== a.text) return void this.parseData("M0,0" + t); - s++, (h = i[a.text]), (o = a.text); - } else n(a, this.NUMBER) ? (h = i[o]) : (s++, (h = i[a.text]), (o = a.text)); - if (s + h < e.length) { - for (let t = s; t < s + h; t++) { - const s = e[t]; - if (!n(s, this.NUMBER)) - return void console.error("Param not a number: " + o + "," + s.text); - r[r.length] = +s.text; - } - if ("number" != typeof i[o]) return void console.error("Bad segment: " + o); - { - const t = { key: o, data: r }; - this.segments.push(t), - (s += h), - (a = e[s]), - "M" === o && (o = "L"), - "m" === o && (o = "l"); - } - } else console.error("Path data ended short"); - } - } - get closed() { - if (void 0 === this._closed) { - this._closed = !1; - for (const t of this.segments) "z" === t.key.toLowerCase() && (this._closed = !0); - } - return this._closed; - } - processPoints() { - let t = null, - e = [0, 0]; - for (let s = 0; s < this.segments.length; s++) { - const n = this.segments[s]; - switch (n.key) { - case "M": - case "L": - case "T": - n.point = [n.data[0], n.data[1]]; - break; - case "m": - case "l": - case "t": - n.point = [n.data[0] + e[0], n.data[1] + e[1]]; - break; - case "H": - n.point = [n.data[0], e[1]]; - break; - case "h": - n.point = [n.data[0] + e[0], e[1]]; - break; - case "V": - n.point = [e[0], n.data[0]]; - break; - case "v": - n.point = [e[0], n.data[0] + e[1]]; - break; - case "z": - case "Z": - t && (n.point = [t[0], t[1]]); - break; - case "C": - n.point = [n.data[4], n.data[5]]; - break; - case "c": - n.point = [n.data[4] + e[0], n.data[5] + e[1]]; - break; - case "S": - n.point = [n.data[2], n.data[3]]; - break; - case "s": - n.point = [n.data[2] + e[0], n.data[3] + e[1]]; - break; - case "Q": - n.point = [n.data[2], n.data[3]]; - break; - case "q": - n.point = [n.data[2] + e[0], n.data[3] + e[1]]; - break; - case "A": - n.point = [n.data[5], n.data[6]]; - break; - case "a": - n.point = [n.data[5] + e[0], n.data[6] + e[1]]; - } - ("m" !== n.key && "M" !== n.key) || (t = null), - n.point && ((e = n.point), t || (t = n.point)), - ("z" !== n.key && "Z" !== n.key) || (t = null); - } - } -} -class o { - constructor(t) { - (this._position = [0, 0]), - (this._first = null), - (this.bezierReflectionPoint = null), - (this.quadReflectionPoint = null), - (this.parsed = new a(t)); - } - get segments() { - return this.parsed.segments; - } - get closed() { - return this.parsed.closed; - } - get linearPoints() { - if (!this._linearPoints) { - const t = []; - let e = []; - for (const s of this.parsed.segments) { - const n = s.key.toLowerCase(); - (("m" !== n && "z" !== n) || (e.length && (t.push(e), (e = [])), "z" !== n)) && - s.point && - e.push(s.point); - } - e.length && (t.push(e), (e = [])), (this._linearPoints = t); - } - return this._linearPoints; - } - get first() { - return this._first; - } - set first(t) { - this._first = t; - } - setPosition(t, e) { - (this._position = [t, e]), this._first || (this._first = [t, e]); - } - get position() { - return this._position; - } - get x() { - return this._position[0]; - } - get y() { - return this._position[1]; - } -} -class h { - constructor(t, e, s, n, i, a) { - if ( - ((this._segIndex = 0), - (this._numSegs = 0), - (this._rx = 0), - (this._ry = 0), - (this._sinPhi = 0), - (this._cosPhi = 0), - (this._C = [0, 0]), - (this._theta = 0), - (this._delta = 0), - (this._T = 0), - (this._from = t), - t[0] === e[0] && t[1] === e[1]) - ) - return; - const o = Math.PI / 180; - (this._rx = Math.abs(s[0])), - (this._ry = Math.abs(s[1])), - (this._sinPhi = Math.sin(n * o)), - (this._cosPhi = Math.cos(n * o)); - const h = (this._cosPhi * (t[0] - e[0])) / 2 + (this._sinPhi * (t[1] - e[1])) / 2, - r = (-this._sinPhi * (t[0] - e[0])) / 2 + (this._cosPhi * (t[1] - e[1])) / 2; - let c = 0; - const l = - this._rx * this._rx * this._ry * this._ry - - this._rx * this._rx * r * r - - this._ry * this._ry * h * h; - if (l < 0) { - const t = Math.sqrt(1 - l / (this._rx * this._rx * this._ry * this._ry)); - (this._rx = this._rx * t), (this._ry = this._ry * t), (c = 0); - } else - c = - (i === a ? -1 : 1) * - Math.sqrt(l / (this._rx * this._rx * r * r + this._ry * this._ry * h * h)); - const u = (c * this._rx * r) / this._ry, - p = (-c * this._ry * h) / this._rx; - (this._C = [0, 0]), - (this._C[0] = this._cosPhi * u - this._sinPhi * p + (t[0] + e[0]) / 2), - (this._C[1] = this._sinPhi * u + this._cosPhi * p + (t[1] + e[1]) / 2), - (this._theta = this.calculateVectorAngle( - 1, - 0, - (h - u) / this._rx, - (r - p) / this._ry - )); - let d = this.calculateVectorAngle( - (h - u) / this._rx, - (r - p) / this._ry, - (-h - u) / this._rx, - (-r - p) / this._ry - ); - !a && d > 0 ? (d -= 2 * Math.PI) : a && d < 0 && (d += 2 * Math.PI), - (this._numSegs = Math.ceil(Math.abs(d / (Math.PI / 2)))), - (this._delta = d / this._numSegs), - (this._T = - ((8 / 3) * Math.sin(this._delta / 4) * Math.sin(this._delta / 4)) / - Math.sin(this._delta / 2)); - } - getNextSegment() { - if (this._segIndex === this._numSegs) return null; - const t = Math.cos(this._theta), - e = Math.sin(this._theta), - s = this._theta + this._delta, - n = Math.cos(s), - i = Math.sin(s), - a = [ - this._cosPhi * this._rx * n - this._sinPhi * this._ry * i + this._C[0], - this._sinPhi * this._rx * n + this._cosPhi * this._ry * i + this._C[1], - ], - o = [ - this._from[0] + - this._T * (-this._cosPhi * this._rx * e - this._sinPhi * this._ry * t), - this._from[1] + - this._T * (-this._sinPhi * this._rx * e + this._cosPhi * this._ry * t), - ], - h = [ - a[0] + this._T * (this._cosPhi * this._rx * i + this._sinPhi * this._ry * n), - a[1] + this._T * (this._sinPhi * this._rx * i - this._cosPhi * this._ry * n), - ]; - return ( - (this._theta = s), - (this._from = [a[0], a[1]]), - this._segIndex++, - { cp1: o, cp2: h, to: a } - ); - } - calculateVectorAngle(t, e, s, n) { - const i = Math.atan2(e, t), - a = Math.atan2(n, s); - return a >= i ? a - i : 2 * Math.PI - (i - a); - } -} -class r { - constructor(t, e) { - (this.sets = t), (this.closed = e); - } - fit(t) { - const e = []; - for (const s of this.sets) { - const n = s.length; - let i = Math.floor(t * n); - if (i < 5) { - if (n <= 5) continue; - i = 5; - } - e.push(this.reduce(s, i)); - } - let s = ""; - for (const t of e) { - for (let e = 0; e < t.length; e++) { - const n = t[e]; - s += 0 === e ? "M" + n[0] + "," + n[1] : "L" + n[0] + "," + n[1]; - } - this.closed && (s += "z "); - } - return s; - } - reduce(t, e) { - if (t.length <= e) return t; - const n = t.slice(0); - for (; n.length > e; ) { - let t = -1, - e = -1; - for (let i = 1; i < n.length - 1; i++) { - const a = s([n[i - 1], n[i]]), - o = s([n[i], n[i + 1]]), - h = s([n[i - 1], n[i + 1]]), - r = (a + o + h) / 2, - c = Math.sqrt(r * (r - a) * (r - o) * (r - h)); - (t < 0 || c < t) && ((t = c), (e = i)); - } - if (!(e > 0)) break; - n.splice(e, 1); - } - return n; - } -} -function c(t, s) { - const n = [0, 0], - i = Math.round(s.hachureAngle + 90); - i && e(t, n, i); - const a = (function (t, e) { - const s = [...t]; - s[0].join(",") !== s[s.length - 1].join(",") && s.push([s[0][0], s[0][1]]); - const n = []; - if (s && s.length > 2) { - let t = e.hachureGap; - t < 0 && (t = 4 * e.strokeWidth), (t = Math.max(t, 0.1)); - const i = []; - for (let t = 0; t < s.length - 1; t++) { - const e = s[t], - n = s[t + 1]; - if (e[1] !== n[1]) { - const t = Math.min(e[1], n[1]); - i.push({ - ymin: t, - ymax: Math.max(e[1], n[1]), - x: t === e[1] ? e[0] : n[0], - islope: (n[0] - e[0]) / (n[1] - e[1]), - }); - } - } - if ( - (i.sort((t, e) => - t.ymin < e.ymin - ? -1 - : t.ymin > e.ymin - ? 1 - : t.x < e.x - ? -1 - : t.x > e.x - ? 1 - : t.ymax === e.ymax - ? 0 - : (t.ymax - e.ymax) / Math.abs(t.ymax - e.ymax) - ), - !i.length) - ) - return n; - let a = [], - o = i[0].ymin; - for (; a.length || i.length; ) { - if (i.length) { - let t = -1; - for (let e = 0; e < i.length && !(i[e].ymin > o); e++) t = e; - i.splice(0, t + 1).forEach((t) => { - a.push({ s: o, edge: t }); - }); - } - if ( - ((a = a.filter((t) => !(t.edge.ymax <= o))), - a.sort((t, e) => - t.edge.x === e.edge.x - ? 0 - : (t.edge.x - e.edge.x) / Math.abs(t.edge.x - e.edge.x) - ), - a.length > 1) - ) - for (let t = 0; t < a.length; t += 2) { - const e = t + 1; - if (e >= a.length) break; - const s = a[t].edge, - i = a[e].edge; - n.push([ - [Math.round(s.x), o], - [Math.round(i.x), o], - ]); - } - (o += t), - a.forEach((e) => { - e.edge.x = e.edge.x + t * e.edge.islope; - }); - } - } - return n; - })(t, s); - return ( - i && - (e(t, n, -i), - (function (t, s, n) { - const i = []; - t.forEach((t) => i.push(...t)), e(i, s, n); - })(a, n, -i)), - a - ); -} -class l { - constructor(t) { - this.helper = t; - } - fillPolygon(t, e) { - return this._fillPolygon(t, e); - } - _fillPolygon(t, e, s = !1) { - const n = c(t, e); - return { type: "fillSketch", ops: this.renderLines(n, e, s) }; - } - renderLines(t, e, s) { - let n = [], - i = null; - for (const a of t) - (n = n.concat(this.helper.doubleLineOps(a[0][0], a[0][1], a[1][0], a[1][1], e))), - s && - i && - (n = n.concat(this.helper.doubleLineOps(i[0], i[1], a[0][0], a[0][1], e))), - (i = a[1]); - return n; - } -} -class u extends l { - fillPolygon(t, e) { - return this._fillPolygon(t, e, !0); - } -} -class p extends l { - fillPolygon(t, e) { - const s = this._fillPolygon(t, e), - n = Object.assign({}, e, { hachureAngle: e.hachureAngle + 90 }), - i = this._fillPolygon(t, n); - return (s.ops = s.ops.concat(i.ops)), s; - } -} -class d { - constructor(t) { - this.helper = t; - } - fillPolygon(t, e) { - const s = c( - t, - (e = Object.assign({}, e, { - curveStepCount: 4, - hachureAngle: 0, - roughness: 1, - })) - ); - return this.dotsOnLines(s, e); - } - dotsOnLines(t, e) { - let n = [], - i = e.hachureGap; - i < 0 && (i = 4 * e.strokeWidth), (i = Math.max(i, 0.1)); - let a = e.fillWeight; - a < 0 && (a = e.strokeWidth / 2); - for (const o of t) { - const t = s(o) / i, - h = Math.ceil(t) - 1, - r = Math.atan((o[1][1] - o[0][1]) / (o[1][0] - o[0][0])); - for (let t = 0; t < h; t++) { - const s = i * (t + 1), - h = s * Math.sin(r), - c = s * Math.cos(r), - l = [o[0][0] - c, o[0][1] + h], - u = this.helper.randOffsetWithRange(l[0] - i / 4, l[0] + i / 4, e), - p = this.helper.randOffsetWithRange(l[1] - i / 4, l[1] + i / 4, e), - d = this.helper.ellipse(u, p, a, a, e); - n = n.concat(d.ops); - } - } - return { type: "fillSketch", ops: n }; - } -} -class f { - constructor(t) { - this.helper = t; - } - fillPolygon(t, e) { - const s = c(t, e); - return { type: "fillSketch", ops: this.dashedLine(s, e) }; - } - dashedLine(t, e) { - const n = - e.dashOffset < 0 - ? e.hachureGap < 0 - ? 4 * e.strokeWidth - : e.hachureGap - : e.dashOffset, - i = - e.dashGap < 0 ? (e.hachureGap < 0 ? 4 * e.strokeWidth : e.hachureGap) : e.dashGap; - let a = []; - return ( - t.forEach((t) => { - const o = s(t), - h = Math.floor(o / (n + i)), - r = (o + i - h * (n + i)) / 2; - let c = t[0], - l = t[1]; - c[0] > l[0] && ((c = t[1]), (l = t[0])); - const u = Math.atan((l[1] - c[1]) / (l[0] - c[0])); - for (let t = 0; t < h; t++) { - const s = t * (n + i), - o = s + n, - h = [ - c[0] + s * Math.cos(u) + r * Math.cos(u), - c[1] + s * Math.sin(u) + r * Math.sin(u), - ], - l = [ - c[0] + o * Math.cos(u) + r * Math.cos(u), - c[1] + o * Math.sin(u) + r * Math.sin(u), - ]; - a = a.concat(this.helper.doubleLineOps(h[0], h[1], l[0], l[1], e)); - } - }), - a - ); - } -} -class g { - constructor(t) { - this.helper = t; - } - fillPolygon(t, e) { - const s = e.hachureGap < 0 ? 4 * e.strokeWidth : e.hachureGap, - n = e.zigzagOffset < 0 ? s : e.zigzagOffset, - i = c(t, (e = Object.assign({}, e, { hachureGap: s + n }))); - return { type: "fillSketch", ops: this.zigzagLines(i, n, e) }; - } - zigzagLines(t, e, n) { - let i = []; - return ( - t.forEach((t) => { - const a = s(t), - o = Math.round(a / (2 * e)); - let h = t[0], - r = t[1]; - h[0] > r[0] && ((h = t[1]), (r = t[0])); - const c = Math.atan((r[1] - h[1]) / (r[0] - h[0])); - for (let t = 0; t < o; t++) { - const s = 2 * t * e, - a = 2 * (t + 1) * e, - o = Math.sqrt(2 * Math.pow(e, 2)), - r = [h[0] + s * Math.cos(c), h[1] + s * Math.sin(c)], - l = [h[0] + a * Math.cos(c), h[1] + a * Math.sin(c)], - u = [ - r[0] + o * Math.cos(c + Math.PI / 4), - r[1] + o * Math.sin(c + Math.PI / 4), - ]; - (i = i.concat(this.helper.doubleLineOps(r[0], r[1], u[0], u[1], n))), - (i = i.concat(this.helper.doubleLineOps(u[0], u[1], l[0], l[1], n))); - } - }), - i - ); - } -} -const y = {}; -class _ { - constructor(t) { - this.seed = t; - } - next() { - return this.seed - ? ((Math.pow(2, 31) - 1) & (this.seed = Math.imul(48271, this.seed))) / - Math.pow(2, 31) - : Math.random(); - } -} -const M = { - randOffset: function (t, e) { - return z(t, e); - }, - randOffsetWithRange: function (t, e, s) { - return C(t, e, s); - }, - ellipse: function (t, e, s, n, i) { - const a = P(s, n, i); - return w(t, e, i, a).opset; - }, - doubleLineOps: function (t, e, s, n, i) { - return A(t, e, s, n, i); - }, -}; -function x(t, e, s, n, i) { - return { type: "path", ops: A(t, e, s, n, i) }; -} -function m(t, e, s) { - const n = (t || []).length; - if (n > 2) { - let i = []; - for (let e = 0; e < n - 1; e++) - i = i.concat(A(t[e][0], t[e][1], t[e + 1][0], t[e + 1][1], s)); - return ( - e && (i = i.concat(A(t[n - 1][0], t[n - 1][1], t[0][0], t[0][1], s))), - { type: "path", ops: i } - ); - } - return 2 === n ? x(t[0][0], t[0][1], t[1][0], t[1][1], s) : { type: "path", ops: [] }; -} -function k(t, e, s, n, i) { - return (function (t, e) { - return m(t, !0, e); - })( - [ - [t, e], - [t + s, e], - [t + s, e + n], - [t, e + n], - ], - i - ); -} -function b(t, e) { - const s = W(t, 1 * (1 + 0.2 * e.roughness), e), - n = W(t, 1.5 * (1 + 0.22 * e.roughness), e); - return { type: "path", ops: s.concat(n) }; -} -function P(t, e, s) { - const n = Math.sqrt( - 2 * Math.PI * Math.sqrt((Math.pow(t / 2, 2) + Math.pow(e / 2, 2)) / 2) - ), - i = Math.max(s.curveStepCount, (s.curveStepCount / Math.sqrt(200)) * n), - a = (2 * Math.PI) / i; - let o = Math.abs(t / 2), - h = Math.abs(e / 2); - const r = 1 - s.curveFitting; - return (o += z(o * r, s)), (h += z(h * r, s)), { increment: a, rx: o, ry: h }; -} -function w(t, e, s, n) { - const [i, a] = D( - n.increment, - t, - e, - n.rx, - n.ry, - 1, - n.increment * C(0.1, C(0.4, 1, s), s), - s - ), - [o] = D(n.increment, t, e, n.rx, n.ry, 1.5, 0, s), - h = R(i, null, s), - r = R(o, null, s); - return { estimatedPoints: a, opset: { type: "path", ops: h.concat(r) } }; -} -function v(t, e, s, n, i, a, o, h, r) { - const c = t, - l = e; - let u = Math.abs(s / 2), - p = Math.abs(n / 2); - (u += z(0.01 * u, r)), (p += z(0.01 * p, r)); - let d = i, - f = a; - for (; d < 0; ) (d += 2 * Math.PI), (f += 2 * Math.PI); - f - d > 2 * Math.PI && ((d = 0), (f = 2 * Math.PI)); - const g = (2 * Math.PI) / r.curveStepCount, - y = Math.min(g / 2, (f - d) / 2), - _ = I(y, c, l, u, p, d, f, 1, r), - M = I(y, c, l, u, p, d, f, 1.5, r); - let x = _.concat(M); - return ( - o && - (h - ? ((x = x.concat(A(c, l, c + u * Math.cos(d), l + p * Math.sin(d), r))), - (x = x.concat(A(c, l, c + u * Math.cos(f), l + p * Math.sin(f), r)))) - : (x.push({ op: "lineTo", data: [c, l] }), - x.push({ - op: "lineTo", - data: [c + u * Math.cos(d), l + p * Math.sin(d)], - }))), - { type: "path", ops: x } - ); -} -function S(t, e) { - const s = []; - if (t.length) { - const n = e.maxRandomnessOffset || 0, - i = t.length; - if (i > 2) { - s.push({ op: "move", data: [t[0][0] + z(n, e), t[0][1] + z(n, e)] }); - for (let a = 1; a < i; a++) - s.push({ op: "lineTo", data: [t[a][0] + z(n, e), t[a][1] + z(n, e)] }); - } - } - return { type: "fillPath", ops: s }; -} -function O(t, e) { - return (function (t, e) { - let s = t.fillStyle || "hachure"; - if (!y[s]) - switch (s) { - case "zigzag": - y[s] || (y[s] = new u(e)); - break; - case "cross-hatch": - y[s] || (y[s] = new p(e)); - break; - case "dots": - y[s] || (y[s] = new d(e)); - break; - case "dashed": - y[s] || (y[s] = new f(e)); - break; - case "zigzag-line": - y[s] || (y[s] = new g(e)); - break; - case "hachure": - default: - (s = "hachure"), y[s] || (y[s] = new l(e)); - } - return y[s]; - })(e, M).fillPolygon(t, e); -} -function T(t) { - return t.randomizer || (t.randomizer = new _(t.seed || 0)), t.randomizer.next(); -} -function C(t, e, s) { - return s.roughness * s.roughnessGain * (T(s) * (e - t) + t); -} -function z(t, e) { - return C(-t, t, e); -} -function A(t, e, s, n, i) { - const a = E(t, e, s, n, i, !0, !1), - o = E(t, e, s, n, i, !0, !0); - return a.concat(o); -} -function E(t, e, s, n, i, a, o) { - const h = Math.pow(t - s, 2) + Math.pow(e - n, 2), - r = Math.sqrt(h); - i.roughnessGain = r < 200 ? 1 : r > 500 ? 0.4 : -0.0016668 * r + 1.233334; - let c = i.maxRandomnessOffset || 0; - c * c * 100 > h && (c = r / 10); - const l = c / 2, - u = 0.2 + 0.2 * T(i); - let p = (i.bowing * i.maxRandomnessOffset * (n - e)) / 200, - d = (i.bowing * i.maxRandomnessOffset * (t - s)) / 200; - (p = z(p, i)), (d = z(d, i)); - const f = [], - g = () => z(l, i), - y = () => z(c, i); - return ( - a && - (o - ? f.push({ op: "move", data: [t + g(), e + g()] }) - : f.push({ op: "move", data: [t + z(c, i), e + z(c, i)] })), - o - ? f.push({ - op: "bcurveTo", - data: [ - p + t + (s - t) * u + g(), - d + e + (n - e) * u + g(), - p + t + 2 * (s - t) * u + g(), - d + e + 2 * (n - e) * u + g(), - s + g(), - n + g(), - ], - }) - : f.push({ - op: "bcurveTo", - data: [ - p + t + (s - t) * u + y(), - d + e + (n - e) * u + y(), - p + t + 2 * (s - t) * u + y(), - d + e + 2 * (n - e) * u + y(), - s + y(), - n + y(), - ], - }), - f - ); -} -function W(t, e, s) { - const n = []; - n.push([t[0][0] + z(e, s), t[0][1] + z(e, s)]), - n.push([t[0][0] + z(e, s), t[0][1] + z(e, s)]); - for (let i = 1; i < t.length; i++) - n.push([t[i][0] + z(e, s), t[i][1] + z(e, s)]), - i === t.length - 1 && n.push([t[i][0] + z(e, s), t[i][1] + z(e, s)]); - return R(n, null, s); -} -function R(t, e, s) { - const n = t.length; - let i = []; - if (n > 3) { - const a = [], - o = 1 - s.curveTightness; - i.push({ op: "move", data: [t[1][0], t[1][1]] }); - for (let e = 1; e + 2 < n; e++) { - const s = t[e]; - (a[0] = [s[0], s[1]]), - (a[1] = [ - s[0] + (o * t[e + 1][0] - o * t[e - 1][0]) / 6, - s[1] + (o * t[e + 1][1] - o * t[e - 1][1]) / 6, - ]), - (a[2] = [ - t[e + 1][0] + (o * t[e][0] - o * t[e + 2][0]) / 6, - t[e + 1][1] + (o * t[e][1] - o * t[e + 2][1]) / 6, - ]), - (a[3] = [t[e + 1][0], t[e + 1][1]]), - i.push({ - op: "bcurveTo", - data: [a[1][0], a[1][1], a[2][0], a[2][1], a[3][0], a[3][1]], - }); - } - if (e && 2 === e.length) { - const t = s.maxRandomnessOffset; - i.push({ op: "lineTo", data: [e[0] + z(t, s), e[1] + z(t, s)] }); - } - } else - 3 === n - ? (i.push({ op: "move", data: [t[1][0], t[1][1]] }), - i.push({ - op: "bcurveTo", - data: [t[1][0], t[1][1], t[2][0], t[2][1], t[2][0], t[2][1]], - })) - : 2 === n && (i = i.concat(A(t[0][0], t[0][1], t[1][0], t[1][1], s))); - return i; -} -function D(t, e, s, n, i, a, o, h) { - const r = [], - c = [], - l = z(0.5, h) - Math.PI / 2; - c.push([ - z(a, h) + e + 0.9 * n * Math.cos(l - t), - z(a, h) + s + 0.9 * i * Math.sin(l - t), - ]); - for (let o = l; o < 2 * Math.PI + l - 0.01; o += t) { - const t = [z(a, h) + e + n * Math.cos(o), z(a, h) + s + i * Math.sin(o)]; - r.push(t), c.push(t); - } - return ( - c.push([ - z(a, h) + e + n * Math.cos(l + 2 * Math.PI + 0.5 * o), - z(a, h) + s + i * Math.sin(l + 2 * Math.PI + 0.5 * o), - ]), - c.push([ - z(a, h) + e + 0.98 * n * Math.cos(l + o), - z(a, h) + s + 0.98 * i * Math.sin(l + o), - ]), - c.push([ - z(a, h) + e + 0.9 * n * Math.cos(l + 0.5 * o), - z(a, h) + s + 0.9 * i * Math.sin(l + 0.5 * o), - ]), - [c, r] - ); -} -function I(t, e, s, n, i, a, o, h, r) { - const c = a + z(0.1, r), - l = []; - l.push([ - z(h, r) + e + 0.9 * n * Math.cos(c - t), - z(h, r) + s + 0.9 * i * Math.sin(c - t), - ]); - for (let a = c; a <= o; a += t) - l.push([z(h, r) + e + n * Math.cos(a), z(h, r) + s + i * Math.sin(a)]); - return ( - l.push([e + n * Math.cos(o), s + i * Math.sin(o)]), - l.push([e + n * Math.cos(o), s + i * Math.sin(o)]), - R(l, null, r) - ); -} -function q(t, e, s, n, i, a, o, h) { - const r = [], - c = [h.maxRandomnessOffset || 1, (h.maxRandomnessOffset || 1) + 0.5]; - let l = [0, 0]; - for (let u = 0; u < 2; u++) - 0 === u - ? r.push({ op: "move", data: [o.x, o.y] }) - : r.push({ op: "move", data: [o.x + z(c[0], h), o.y + z(c[0], h)] }), - (l = [i + z(c[u], h), a + z(c[u], h)]), - r.push({ - op: "bcurveTo", - data: [ - t + z(c[u], h), - e + z(c[u], h), - s + z(c[u], h), - n + z(c[u], h), - l[0], - l[1], - ], - }); - return o.setPosition(l[0], l[1]), r; -} -function $(t, e, s, n) { - let i = []; - switch (e.key) { - case "M": - case "m": { - const s = "m" === e.key; - if (e.data.length >= 2) { - let a = +e.data[0], - o = +e.data[1]; - s && ((a += t.x), (o += t.y)); - const h = 1 * (n.maxRandomnessOffset || 0); - (a += z(h, n)), - (o += z(h, n)), - t.setPosition(a, o), - i.push({ op: "move", data: [a, o] }); - } - break; - } - case "L": - case "l": { - const s = "l" === e.key; - if (e.data.length >= 2) { - let a = +e.data[0], - o = +e.data[1]; - s && ((a += t.x), (o += t.y)), - (i = i.concat(A(t.x, t.y, a, o, n))), - t.setPosition(a, o); - } - break; - } - case "H": - case "h": { - const s = "h" === e.key; - if (e.data.length) { - let a = +e.data[0]; - s && (a += t.x), (i = i.concat(A(t.x, t.y, a, t.y, n))), t.setPosition(a, t.y); - } - break; - } - case "V": - case "v": { - const s = "v" === e.key; - if (e.data.length) { - let a = +e.data[0]; - s && (a += t.y), (i = i.concat(A(t.x, t.y, t.x, a, n))), t.setPosition(t.x, a); - } - break; - } - case "Z": - case "z": - t.first && - ((i = i.concat(A(t.x, t.y, t.first[0], t.first[1], n))), - t.setPosition(t.first[0], t.first[1]), - (t.first = null)); - break; - case "C": - case "c": { - const s = "c" === e.key; - if (e.data.length >= 6) { - let a = +e.data[0], - o = +e.data[1], - h = +e.data[2], - r = +e.data[3], - c = +e.data[4], - l = +e.data[5]; - s && ((a += t.x), (h += t.x), (c += t.x), (o += t.y), (r += t.y), (l += t.y)); - const u = q(a, o, h, r, c, l, t, n); - (i = i.concat(u)), (t.bezierReflectionPoint = [c + (c - h), l + (l - r)]); - } - break; - } - case "S": - case "s": { - const a = "s" === e.key; - if (e.data.length >= 4) { - let o = +e.data[0], - h = +e.data[1], - r = +e.data[2], - c = +e.data[3]; - a && ((o += t.x), (r += t.x), (h += t.y), (c += t.y)); - let l = o, - u = h; - const p = s ? s.key : ""; - let d = null; - ("c" !== p && "C" !== p && "s" !== p && "S" !== p) || - (d = t.bezierReflectionPoint), - d && ((l = d[0]), (u = d[1])); - const f = q(l, u, o, h, r, c, t, n); - (i = i.concat(f)), (t.bezierReflectionPoint = [r + (r - o), c + (c - h)]); - } - break; - } - case "Q": - case "q": { - const s = "q" === e.key; - if (e.data.length >= 4) { - let a = +e.data[0], - o = +e.data[1], - h = +e.data[2], - r = +e.data[3]; - s && ((a += t.x), (h += t.x), (o += t.y), (r += t.y)); - const c = 1 * (1 + 0.2 * n.roughness), - l = 1.5 * (1 + 0.22 * n.roughness); - i.push({ op: "move", data: [t.x + z(c, n), t.y + z(c, n)] }); - let u = [h + z(c, n), r + z(c, n)]; - i.push({ - op: "qcurveTo", - data: [a + z(c, n), o + z(c, n), u[0], u[1]], - }), - i.push({ op: "move", data: [t.x + z(l, n), t.y + z(l, n)] }), - (u = [h + z(l, n), r + z(l, n)]), - i.push({ - op: "qcurveTo", - data: [a + z(l, n), o + z(l, n), u[0], u[1]], - }), - t.setPosition(u[0], u[1]), - (t.quadReflectionPoint = [h + (h - a), r + (r - o)]); - } - break; - } - case "T": - case "t": { - const a = "t" === e.key; - if (e.data.length >= 2) { - let o = +e.data[0], - h = +e.data[1]; - a && ((o += t.x), (h += t.y)); - let r = o, - c = h; - const l = s ? s.key : ""; - let u = null; - ("q" !== l && "Q" !== l && "t" !== l && "T" !== l) || (u = t.quadReflectionPoint), - u && ((r = u[0]), (c = u[1])); - const p = 1 * (1 + 0.2 * n.roughness), - d = 1.5 * (1 + 0.22 * n.roughness); - i.push({ op: "move", data: [t.x + z(p, n), t.y + z(p, n)] }); - let f = [o + z(p, n), h + z(p, n)]; - i.push({ - op: "qcurveTo", - data: [r + z(p, n), c + z(p, n), f[0], f[1]], - }), - i.push({ op: "move", data: [t.x + z(d, n), t.y + z(d, n)] }), - (f = [o + z(d, n), h + z(d, n)]), - i.push({ - op: "qcurveTo", - data: [r + z(d, n), c + z(d, n), f[0], f[1]], - }), - t.setPosition(f[0], f[1]), - (t.quadReflectionPoint = [o + (o - r), h + (h - c)]); - } - break; - } - case "A": - case "a": { - const s = "a" === e.key; - if (e.data.length >= 7) { - const a = +e.data[0], - o = +e.data[1], - r = +e.data[2], - c = +e.data[3], - l = +e.data[4]; - let u = +e.data[5], - p = +e.data[6]; - if ((s && ((u += t.x), (p += t.y)), u === t.x && p === t.y)) break; - if (0 === a || 0 === o) (i = i.concat(A(t.x, t.y, u, p, n))), t.setPosition(u, p); - else - for (let e = 0; e < 1; e++) { - const e = new h([t.x, t.y], [u, p], [a, o], r, !!c, !!l); - let s = e.getNextSegment(); - for (; s; ) { - const a = q(s.cp1[0], s.cp1[1], s.cp2[0], s.cp2[1], s.to[0], s.to[1], t, n); - (i = i.concat(a)), (s = e.getNextSegment()); - } - } - } - break; - } - } - return i; -} -const N = "undefined" != typeof self, - L = "none"; -class B { - constructor(t, e) { - (this.defaultOptions = { - maxRandomnessOffset: 2, - roughness: 1, - bowing: 1, - stroke: "#000", - strokeWidth: 1, - curveTightness: 0, - curveFitting: 0.95, - curveStepCount: 9, - fillStyle: "hachure", - fillWeight: -1, - hachureAngle: -41, - hachureGap: -1, - dashOffset: -1, - dashGap: -1, - zigzagOffset: -1, - seed: 0, - roughnessGain: 1, - }), - (this.config = t || {}), - (this.surface = e), - this.config.options && (this.defaultOptions = this._options(this.config.options)); - } - static newSeed() { - return Math.floor(Math.random() * Math.pow(2, 31)); - } - _options(t) { - return t ? Object.assign({}, this.defaultOptions, t) : this.defaultOptions; - } - _drawable(t, e, s) { - return { shape: t, sets: e || [], options: s || this.defaultOptions }; - } - line(t, e, s, n, i) { - const a = this._options(i); - return this._drawable("line", [x(t, e, s, n, a)], a); - } - rectangle(t, e, s, n, i) { - const a = this._options(i), - o = [], - h = k(t, e, s, n, a); - if (a.fill) { - const i = [ - [t, e], - [t + s, e], - [t + s, e + n], - [t, e + n], - ]; - "solid" === a.fillStyle ? o.push(S(i, a)) : o.push(O(i, a)); - } - return a.stroke !== L && o.push(h), this._drawable("rectangle", o, a); - } - ellipse(t, e, s, n, i) { - const a = this._options(i), - o = [], - h = P(s, n, a), - r = w(t, e, a, h); - if (a.fill) - if ("solid" === a.fillStyle) { - const s = w(t, e, a, h).opset; - (s.type = "fillPath"), o.push(s); - } else o.push(O(r.estimatedPoints, a)); - return a.stroke !== L && o.push(r.opset), this._drawable("ellipse", o, a); - } - circle(t, e, s, n) { - const i = this.ellipse(t, e, s, s, n); - return (i.shape = "circle"), i; - } - linearPath(t, e) { - const s = this._options(e); - return this._drawable("linearPath", [m(t, !1, s)], s); - } - arc(t, e, s, n, i, a, o = !1, h) { - const r = this._options(h), - c = [], - l = v(t, e, s, n, i, a, o, !0, r); - if (o && r.fill) - if ("solid" === r.fillStyle) { - const o = v(t, e, s, n, i, a, !0, !1, r); - (o.type = "fillPath"), c.push(o); - } else - c.push( - (function (t, e, s, n, i, a, o) { - const h = t, - r = e; - let c = Math.abs(s / 2), - l = Math.abs(n / 2); - (c += z(0.01 * c, o)), (l += z(0.01 * l, o)); - let u = i, - p = a; - for (; u < 0; ) (u += 2 * Math.PI), (p += 2 * Math.PI); - p - u > 2 * Math.PI && ((u = 0), (p = 2 * Math.PI)); - const d = (p - u) / o.curveStepCount, - f = []; - for (let t = u; t <= p; t += d) - f.push([h + c * Math.cos(t), r + l * Math.sin(t)]); - return ( - f.push([h + c * Math.cos(p), r + l * Math.sin(p)]), f.push([h, r]), O(f, o) - ); - })(t, e, s, n, i, a, r) - ); - return r.stroke !== L && c.push(l), this._drawable("arc", c, r); - } - curve(t, e) { - const s = this._options(e); - return this._drawable("curve", [b(t, s)], s); - } - polygon(t, e) { - const s = this._options(e), - n = [], - i = m(t, !0, s); - return ( - s.fill && ("solid" === s.fillStyle ? n.push(S(t, s)) : n.push(O(t, s))), - s.stroke !== L && n.push(i), - this._drawable("polygon", n, s) - ); - } - path(t, e) { - const s = this._options(e), - n = []; - if (!t) return this._drawable("path", n, s); - const i = (function (t, e) { - t = (t || "").replace(/\n/g, " ").replace(/(-\s)/g, "-").replace("/(\s\s)/g", " "); - let s = new o(t); - if (e.simplification) { - const t = new r(s.linearPoints, s.closed).fit(e.simplification); - s = new o(t); - } - let n = []; - const i = s.segments || []; - for (let t = 0; t < i.length; t++) { - const a = $(s, i[t], t > 0 ? i[t - 1] : null, e); - a && a.length && (n = n.concat(a)); - } - return { type: "path", ops: n }; - })(t, s); - if (s.fill) - if ("solid" === s.fillStyle) { - const e = { type: "path2Dfill", path: t, ops: [] }; - n.push(e); - } else { - const e = this.computePathSize(t), - i = O( - [ - [0, 0], - [e[0], 0], - [e[0], e[1]], - [0, e[1]], - ], - s - ); - (i.type = "path2Dpattern"), (i.size = e), (i.path = t), n.push(i); - } - return s.stroke !== L && n.push(i), this._drawable("path", n, s); - } - computePathSize(e) { - let s = [0, 0]; - if (N && self.document) - try { - const n = self.document.createElementNS(t, "svg"); - n.setAttribute("width", "0"), n.setAttribute("height", "0"); - const i = self.document.createElementNS(t, "path"); - i.setAttribute("d", e), n.appendChild(i), self.document.body.appendChild(n); - const a = i.getBBox(); - a && ((s[0] = a.width || 0), (s[1] = a.height || 0)), - self.document.body.removeChild(n); - } catch (t) {} - const n = this.getCanvasSize(); - return s[0] * s[1] || (s = n), s; - } - getCanvasSize() { - const t = (t) => - t && "object" == typeof t && t.baseVal && t.baseVal.value - ? t.baseVal.value - : t || 100; - return this.surface ? [t(this.surface.width), t(this.surface.height)] : [100, 100]; - } - opsToPath(t) { - let e = ""; - for (const s of t.ops) { - const t = s.data; - switch (s.op) { - case "move": - e += `M${t[0]} ${t[1]} `; - break; - case "bcurveTo": - e += `C${t[0]} ${t[1]}, ${t[2]} ${t[3]}, ${t[4]} ${t[5]} `; - break; - case "qcurveTo": - e += `Q${t[0]} ${t[1]}, ${t[2]} ${t[3]} `; - break; - case "lineTo": - e += `L${t[0]} ${t[1]} `; - } - } - return e.trim(); - } - toPaths(t) { - const e = t.sets || [], - s = t.options || this.defaultOptions, - n = []; - for (const t of e) { - let e = null; - switch (t.type) { - case "path": - e = { - d: this.opsToPath(t), - stroke: s.stroke, - strokeWidth: s.strokeWidth, - fill: L, - }; - break; - case "fillPath": - e = { - d: this.opsToPath(t), - stroke: L, - strokeWidth: 0, - fill: s.fill || L, - }; - break; - case "fillSketch": - e = this.fillSketch(t, s); - break; - case "path2Dfill": - e = { d: t.path || "", stroke: L, strokeWidth: 0, fill: s.fill || L }; - break; - case "path2Dpattern": { - const n = t.size, - i = { - x: 0, - y: 0, - width: 1, - height: 1, - viewBox: `0 0 ${Math.round(n[0])} ${Math.round(n[1])}`, - patternUnits: "objectBoundingBox", - path: this.fillSketch(t, s), - }; - e = { d: t.path, stroke: L, strokeWidth: 0, pattern: i }; - break; - } - } - e && n.push(e); - } - return n; - } - fillSketch(t, e) { - let s = e.fillWeight; - return ( - s < 0 && (s = e.strokeWidth / 2), - { d: this.opsToPath(t), stroke: e.fill || L, strokeWidth: s, fill: L } - ); - } -} -const G = "undefined" != typeof document; -class V { - constructor(t, e) { - (this.canvas = t), - (this.ctx = this.canvas.getContext("2d")), - (this.gen = new B(e, this.canvas)); - } - draw(t) { - const e = t.sets || [], - s = t.options || this.getDefaultOptions(), - n = this.ctx; - for (const t of e) - switch (t.type) { - case "path": - n.save(), - (n.strokeStyle = "none" === s.stroke ? "transparent" : s.stroke), - (n.lineWidth = s.strokeWidth), - this._drawToContext(n, t), - n.restore(); - break; - case "fillPath": - n.save(), (n.fillStyle = s.fill || ""), this._drawToContext(n, t), n.restore(); - break; - case "fillSketch": - this.fillSketch(n, t, s); - break; - case "path2Dfill": { - this.ctx.save(), (this.ctx.fillStyle = s.fill || ""); - const e = new Path2D(t.path); - this.ctx.fill(e), this.ctx.restore(); - break; - } - case "path2Dpattern": { - const e = this.canvas.ownerDocument || (G && document); - if (e) { - const n = t.size, - i = e.createElement("canvas"), - a = i.getContext("2d"), - o = this.computeBBox(t.path); - o && (o.width || o.height) - ? ((i.width = this.canvas.width), - (i.height = this.canvas.height), - a.translate(o.x || 0, o.y || 0)) - : ((i.width = n[0]), (i.height = n[1])), - this.fillSketch(a, t, s), - this.ctx.save(), - (this.ctx.fillStyle = this.ctx.createPattern(i, "repeat")); - const h = new Path2D(t.path); - this.ctx.fill(h), this.ctx.restore(); - } else console.error("Pattern fill fail: No defs"); - break; - } - } - } - computeBBox(e) { - if (G) - try { - const s = document.createElementNS(t, "svg"); - s.setAttribute("width", "0"), s.setAttribute("height", "0"); - const n = self.document.createElementNS(t, "path"); - n.setAttribute("d", e), s.appendChild(n), document.body.appendChild(s); - const i = n.getBBox(); - return document.body.removeChild(s), i; - } catch (t) {} - return null; - } - fillSketch(t, e, s) { - let n = s.fillWeight; - n < 0 && (n = s.strokeWidth / 2), - t.save(), - (t.strokeStyle = s.fill || ""), - (t.lineWidth = n), - this._drawToContext(t, e), - t.restore(); - } - _drawToContext(t, e) { - t.beginPath(); - for (const s of e.ops) { - const e = s.data; - switch (s.op) { - case "move": - t.moveTo(e[0], e[1]); - break; - case "bcurveTo": - t.bezierCurveTo(e[0], e[1], e[2], e[3], e[4], e[5]); - break; - case "qcurveTo": - t.quadraticCurveTo(e[0], e[1], e[2], e[3]); - break; - case "lineTo": - t.lineTo(e[0], e[1]); - } - } - "fillPath" === e.type ? t.fill() : t.stroke(); - } - get generator() { - return this.gen; - } - getDefaultOptions() { - return this.gen.defaultOptions; - } - line(t, e, s, n, i) { - const a = this.gen.line(t, e, s, n, i); - return this.draw(a), a; - } - rectangle(t, e, s, n, i) { - const a = this.gen.rectangle(t, e, s, n, i); - return this.draw(a), a; - } - ellipse(t, e, s, n, i) { - const a = this.gen.ellipse(t, e, s, n, i); - return this.draw(a), a; - } - circle(t, e, s, n) { - const i = this.gen.circle(t, e, s, n); - return this.draw(i), i; - } - linearPath(t, e) { - const s = this.gen.linearPath(t, e); - return this.draw(s), s; - } - polygon(t, e) { - const s = this.gen.polygon(t, e); - return this.draw(s), s; - } - arc(t, e, s, n, i, a, o = !1, h) { - const r = this.gen.arc(t, e, s, n, i, a, o, h); - return this.draw(r), r; - } - curve(t, e) { - const s = this.gen.curve(t, e); - return this.draw(s), s; - } - path(t, e) { - const s = this.gen.path(t, e); - return this.draw(s), s; - } -} -const j = "undefined" != typeof document; -class Q { - constructor(t, e) { - (this.svg = t), (this.gen = new B(e, this.svg)); - } - get defs() { - const e = this.svg.ownerDocument || (j && document); - if (e && !this._defs) { - const s = e.createElementNS(t, "defs"); - this.svg.firstChild - ? this.svg.insertBefore(s, this.svg.firstChild) - : this.svg.appendChild(s), - (this._defs = s); - } - return this._defs || null; - } - draw(e) { - const s = e.sets || [], - n = e.options || this.getDefaultOptions(), - i = this.svg.ownerDocument || window.document, - a = i.createElementNS(t, "g"); - for (const e of s) { - let s = null; - switch (e.type) { - case "path": - (s = i.createElementNS(t, "path")), - s.setAttribute("d", this.opsToPath(e)), - (s.style.stroke = n.stroke), - (s.style.strokeWidth = n.strokeWidth + ""), - (s.style.fill = "none"); - break; - case "fillPath": - (s = i.createElementNS(t, "path")), - s.setAttribute("d", this.opsToPath(e)), - (s.style.stroke = "none"), - (s.style.strokeWidth = "0"), - (s.style.fill = n.fill || ""); - break; - case "fillSketch": - s = this.fillSketch(i, e, n); - break; - case "path2Dfill": - (s = i.createElementNS(t, "path")), - s.setAttribute("d", e.path || ""), - (s.style.stroke = "none"), - (s.style.strokeWidth = "0"), - (s.style.fill = n.fill || ""); - break; - case "path2Dpattern": - if (this.defs) { - const a = e.size, - o = i.createElementNS(t, "pattern"), - h = `rough-${Math.floor( - Math.random() * (Number.MAX_SAFE_INTEGER || 999999) - )}`; - o.setAttribute("id", h), - o.setAttribute("x", "0"), - o.setAttribute("y", "0"), - o.setAttribute("width", "1"), - o.setAttribute("height", "1"), - o.setAttribute("height", "1"), - o.setAttribute("viewBox", `0 0 ${Math.round(a[0])} ${Math.round(a[1])}`), - o.setAttribute("patternUnits", "objectBoundingBox"); - const r = this.fillSketch(i, e, n); - o.appendChild(r), - this.defs.appendChild(o), - (s = i.createElementNS(t, "path")), - s.setAttribute("d", e.path || ""), - (s.style.stroke = "none"), - (s.style.strokeWidth = "0"), - (s.style.fill = `url(#${h})`); - } else console.error("Pattern fill fail: No defs"); - } - s && a.appendChild(s); - } - return a; - } - fillSketch(e, s, n) { - let i = n.fillWeight; - i < 0 && (i = n.strokeWidth / 2); - const a = e.createElementNS(t, "path"); - return ( - a.setAttribute("d", this.opsToPath(s)), - (a.style.stroke = n.fill || ""), - (a.style.strokeWidth = i + ""), - (a.style.fill = "none"), - a - ); - } - get generator() { - return this.gen; - } - getDefaultOptions() { - return this.gen.defaultOptions; - } - opsToPath(t) { - return this.gen.opsToPath(t); - } - line(t, e, s, n, i) { - const a = this.gen.line(t, e, s, n, i); - return this.draw(a); - } - rectangle(t, e, s, n, i) { - const a = this.gen.rectangle(t, e, s, n, i); - return this.draw(a); - } - ellipse(t, e, s, n, i) { - const a = this.gen.ellipse(t, e, s, n, i); - return this.draw(a); - } - circle(t, e, s, n) { - const i = this.gen.circle(t, e, s, n); - return this.draw(i); - } - linearPath(t, e) { - const s = this.gen.linearPath(t, e); - return this.draw(s); - } - polygon(t, e) { - const s = this.gen.polygon(t, e); - return this.draw(s); - } - arc(t, e, s, n, i, a, o = !1, h) { - const r = this.gen.arc(t, e, s, n, i, a, o, h); - return this.draw(r); - } - curve(t, e) { - const s = this.gen.curve(t, e); - return this.draw(s); - } - path(t, e) { - const s = this.gen.path(t, e); - return this.draw(s); - } -} -var rough = { - canvas: (t, e) => new V(t, e), - svg: (t, e) => new Q(t, e), - generator: (t, e) => new B(t, e), - newSeed: () => B.newSeed(), -}; diff --git a/d2renderers/d2sketch/setup.js b/d2renderers/d2sketch/setup.js deleted file mode 100644 index 8814fbf359..0000000000 --- a/d2renderers/d2sketch/setup.js +++ /dev/null @@ -1,23 +0,0 @@ -const root = { - ownerDocument: { - createElementNS: (ns, tagName) => { - const children = []; - const attrs = {}; - const style = {}; - return { - style, - tagName, - attrs, - setAttribute: (key, value) => (attrs[key] = value), - appendChild: (node) => children.push(node), - children, - }; - }, - }, -}; -const rc = rough.svg(root, { seed: 1 }); -let node; - -if (typeof globalThis !== "undefined") { - globalThis.rc = rc; -} diff --git a/d2renderers/d2sketch/sketch.go b/d2renderers/d2sketch/sketch.go index b9f49bbb99..69e9bace25 100644 --- a/d2renderers/d2sketch/sketch.go +++ b/d2renderers/d2sketch/sketch.go @@ -2,7 +2,6 @@ package d2sketch import ( "bytes" - "encoding/json" "fmt" "regexp" "strings" @@ -13,27 +12,15 @@ import ( "github.com/d2lang/d2/d2themes" "github.com/d2lang/d2/lib/color" "github.com/d2lang/d2/lib/geo" - "github.com/d2lang/d2/lib/jsrunner" "github.com/d2lang/d2/lib/label" "github.com/d2lang/d2/lib/svg" + rough "github.com/d2lang/rough-go" "github.com/d2lang/util-go/go2" ) -//go:embed rough.js -var roughJS string - -//go:embed setup.js -var setupJS string - //go:embed streaks.txt var streaks string -var baseRoughProps = `fillWeight: 2.0, -hachureGap: 16, -fillStyle: "solid", -bowing: 2, -seed: 1,` - var floatRE = regexp.MustCompile(`(\d+)\.(\d+)`) const ( @@ -41,14 +28,21 @@ const ( FG_COLOR = color.N1 ) -func LoadJS(runner jsrunner.JSRunner) error { - if _, err := runner.RunString(roughJS); err != nil { - return err - } - if _, err := runner.RunString(setupJS); err != nil { - return err +func newGenerator() *rough.Generator { + return rough.NewGenerator(&rough.Config{Options: &rough.Options{Seed: rough.Float64(1)}}) +} + +func baseOptions(strokeWidth float64) *rough.Options { + return &rough.Options{ + Fill: rough.String("#000"), + Stroke: rough.String("#000"), + StrokeWidth: rough.Float64(strokeWidth), + FillWeight: rough.Float64(2), + HachureGap: rough.Float64(16), + FillStyle: rough.String("solid"), + Bowing: rough.Float64(2), + Seed: rough.Float64(1), } - return nil } // DefineFillPatterns adds reusable patterns that are overlayed on shapes with @@ -73,14 +67,9 @@ func defineFillPattern(buf *bytes.Buffer, source, diagramHash string, luminanceC } } -func Rect(r jsrunner.JSRunner, shape d2target.Shape, diagramHash string) (string, error) { - js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { - fill: "#000", - stroke: "#000", - strokeWidth: %d, - %s - });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps) - paths, err := computeRoughPathData(r, js) +func Rect(shape d2target.Shape, diagramHash string) (string, error) { + g := newGenerator() + paths, err := computeRoughPathData(g, g.Rectangle(0, 0, float64(shape.Width), float64(shape.Height), baseOptions(float64(shape.StrokeWidth)))) if err != nil { return "", err } @@ -119,24 +108,16 @@ func Rect(r jsrunner.JSRunner, shape d2target.Shape, diagramHash string) (string return output, nil } -func DoubleRect(r jsrunner.JSRunner, shape d2target.Shape, diagramHash string) (string, error) { - jsBigRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { - fill: "#000", - stroke: "#000", - strokeWidth: %d, - %s - });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps) - pathsBigRect, err := computeRoughPathData(r, jsBigRect) +func DoubleRect(shape d2target.Shape, diagramHash string) (string, error) { + g := newGenerator() + pathsBigRect, err := computeRoughPathData(g, g.Rectangle(0, 0, float64(shape.Width), float64(shape.Height), baseOptions(float64(shape.StrokeWidth)))) if err != nil { return "", err } - jsSmallRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { - fill: "#000", - stroke: "#000", - strokeWidth: %d, - %s - });`, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.StrokeWidth, baseRoughProps) - pathsSmallRect, err := computeRoughPathData(r, jsSmallRect) + pathsSmallRect, err := computeRoughPathData(g, g.Rectangle(0, 0, + float64(shape.Width-d2target.INNER_BORDER_OFFSET*2), + float64(shape.Height-d2target.INNER_BORDER_OFFSET*2), + baseOptions(float64(shape.StrokeWidth)))) if err != nil { return "", err } @@ -194,14 +175,11 @@ func DoubleRect(r jsrunner.JSRunner, shape d2target.Shape, diagramHash string) ( return output, nil } -func Oval(r jsrunner.JSRunner, shape d2target.Shape, diagramHash string) (string, error) { - js := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, { - fill: "#000", - stroke: "#000", - strokeWidth: %d, - %s - });`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps) - paths, err := computeRoughPathData(r, js) +func Oval(shape d2target.Shape, diagramHash string) (string, error) { + g := newGenerator() + paths, err := computeRoughPathData(g, g.Ellipse( + float64(shape.Width/2), float64(shape.Height/2), + float64(shape.Width), float64(shape.Height), baseOptions(float64(shape.StrokeWidth)))) if err != nil { return "", err } @@ -243,24 +221,19 @@ func Oval(r jsrunner.JSRunner, shape d2target.Shape, diagramHash string) (string return output, nil } -func DoubleOval(r jsrunner.JSRunner, shape d2target.Shape, diagramHash string) (string, error) { - jsBigCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, { - fill: "#000", - stroke: "#000", - strokeWidth: %d, - %s - });`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps) - jsSmallCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, { - fill: "#000", - stroke: "#000", - strokeWidth: %d, - %s - });`, shape.Width/2, shape.Height/2, shape.Width-d2target.INNER_BORDER_OFFSET*2, shape.Height-d2target.INNER_BORDER_OFFSET*2, shape.StrokeWidth, baseRoughProps) - pathsBigCircle, err := computeRoughPathData(r, jsBigCircle) +func DoubleOval(shape d2target.Shape, diagramHash string) (string, error) { + g := newGenerator() + pathsBigCircle, err := computeRoughPathData(g, g.Ellipse( + float64(shape.Width/2), float64(shape.Height/2), + float64(shape.Width), float64(shape.Height), baseOptions(float64(shape.StrokeWidth)))) if err != nil { return "", err } - pathsSmallCircle, err := computeRoughPathData(r, jsSmallCircle) + pathsSmallCircle, err := computeRoughPathData(g, g.Ellipse( + float64(shape.Width/2), float64(shape.Height/2), + float64(shape.Width-d2target.INNER_BORDER_OFFSET*2), + float64(shape.Height-d2target.INNER_BORDER_OFFSET*2), + baseOptions(float64(shape.StrokeWidth)))) if err != nil { return "", err } @@ -321,16 +294,11 @@ func DoubleOval(r jsrunner.JSRunner, shape d2target.Shape, diagramHash string) ( } // TODO need to personalize this per shape like we do in Terrastruct app -func Paths(r jsrunner.JSRunner, shape d2target.Shape, diagramHash string, paths []string) (string, error) { +func Paths(shape d2target.Shape, diagramHash string, paths []string) (string, error) { output := "" for _, path := range paths { - js := fmt.Sprintf(`node = rc.path("%s", { - fill: "#000", - stroke: "#000", - strokeWidth: %d, - %s - });`, path, shape.StrokeWidth, baseRoughProps) - sketchPaths, err := computeRoughPathData(r, js) + g := newGenerator() + sketchPaths, err := computeRoughPathData(g, g.Path(path, baseOptions(float64(shape.StrokeWidth)))) if err != nil { return "", err } @@ -370,7 +338,7 @@ func Paths(r jsrunner.JSRunner, shape d2target.Shape, diagramHash string, paths return output, nil } -func Connection(r jsrunner.JSRunner, connection d2target.Connection, path, attrs string) (string, error) { +func Connection(connection d2target.Connection, path, attrs string) (string, error) { animatedClass := "" if connection.Animated { animatedClass = " animated-connection" @@ -415,8 +383,11 @@ func Connection(r jsrunner.JSRunner, connection d2target.Connection, path, attrs } } else { roughness := 0.5 - js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness) - paths, err := computeRoughPathData(r, js) + g := newGenerator() + paths, err := computeRoughPathData(g, g.Path(path, &rough.Options{ + Roughness: rough.Float64(roughness), + Seed: rough.Float64(1), + })) if err != nil { return "", err } @@ -438,15 +409,10 @@ func Connection(r jsrunner.JSRunner, connection d2target.Connection, path, attrs } // TODO cleanup -func Table(r jsrunner.JSRunner, shape d2target.Shape) (string, error) { +func Table(shape d2target.Shape) (string, error) { output := "" - js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { - fill: "#000", - stroke: "#000", - strokeWidth: %d, - %s - });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps) - paths, err := computeRoughPathData(r, js) + g := newGenerator() + paths, err := computeRoughPathData(g, g.Rectangle(0, 0, float64(shape.Width), float64(shape.Height), baseOptions(float64(shape.StrokeWidth)))) if err != nil { return "", err } @@ -469,11 +435,7 @@ func Table(r jsrunner.JSRunner, shape d2target.Shape) (string, error) { rowHeight := box.Height / float64(1+len(shape.SQLTable.Columns)) headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight) - js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, { - fill: "#000", - %s - });`, shape.Width, rowHeight, baseRoughProps) - paths, err = computeRoughPathData(r, js) + paths, err = computeRoughPathData(g, g.Rectangle(0, 0, float64(shape.Width), rowHeight, baseOptions(1))) if err != nil { return "", err } @@ -551,10 +513,10 @@ func Table(r jsrunner.JSRunner, shape d2target.Shape) (string, error) { rowBox.TopLeft.Y += rowHeight - js = fmt.Sprintf(`node = rc.line(%f, %f, %f, %f, { - %s - });`, rowBox.TopLeft.X, rowBox.TopLeft.Y, rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, baseRoughProps) - paths, err = computeRoughPathData(r, js) + paths, err = computeRoughPathData(g, g.Line( + rowBox.TopLeft.X, rowBox.TopLeft.Y, + rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, + baseOptions(1))) if err != nil { return "", err } @@ -580,15 +542,10 @@ func Table(r jsrunner.JSRunner, shape d2target.Shape) (string, error) { return output, nil } -func Class(r jsrunner.JSRunner, shape d2target.Shape) (string, error) { +func Class(shape d2target.Shape) (string, error) { output := "" - js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { - fill: "#000", - stroke: "#000", - strokeWidth: %d, - %s - });`, shape.Width, shape.Height, shape.StrokeWidth, baseRoughProps) - paths, err := computeRoughPathData(r, js) + g := newGenerator() + paths, err := computeRoughPathData(g, g.Rectangle(0, 0, float64(shape.Width), float64(shape.Height), baseOptions(float64(shape.StrokeWidth)))) if err != nil { return "", err } @@ -612,11 +569,7 @@ func Class(r jsrunner.JSRunner, shape d2target.Shape) (string, error) { rowHeight := box.Height / float64(2+len(shape.Class.Fields)+len(shape.Class.Methods)) headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight) - js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, { - fill: "#000", - %s - });`, shape.Width, headerBox.Height, baseRoughProps) - paths, err = computeRoughPathData(r, js) + paths, err = computeRoughPathData(g, g.Rectangle(0, 0, float64(shape.Width), headerBox.Height, baseOptions(1))) if err != nil { return "", err } @@ -668,10 +621,10 @@ func Class(r jsrunner.JSRunner, shape d2target.Shape) (string, error) { rowBox.TopLeft.Y += rowHeight } - js = fmt.Sprintf(`node = rc.line(%f, %f, %f, %f, { -%s - });`, rowBox.TopLeft.X, rowBox.TopLeft.Y, rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, baseRoughProps) - paths, err = computeRoughPathData(r, js) + paths, err = computeRoughPathData(g, g.Line( + rowBox.TopLeft.X, rowBox.TopLeft.Y, + rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, + baseOptions(1))) if err != nil { return "", err } @@ -731,22 +684,24 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str return output } -func computeRoughPathData(r jsrunner.JSRunner, js string) ([]string, error) { - if _, err := r.RunString(js); err != nil { - return nil, err - } - roughPaths, err := extractRoughPaths(r) - if err != nil { - return nil, err - } - return extractPathData(roughPaths) +func computeRoughPathData(g *rough.Generator, drawable rough.Drawable) ([]string, error) { + return extractPathData(computeRoughPaths(g, drawable)) } -func computeRoughPaths(r jsrunner.JSRunner, js string) ([]roughPath, error) { - if _, err := r.RunString(js); err != nil { - return nil, err +func computeRoughPaths(g *rough.Generator, drawable rough.Drawable) []roughPath { + pathInfos := g.ToPaths(drawable) + paths := make([]roughPath, 0, len(pathInfos)) + for _, path := range pathInfos { + paths = append(paths, roughPath{ + Attrs: attrs{D: truncatePathData(path.D)}, + Style: style{ + Stroke: path.Stroke, + StrokeWidth: rough.NumberString(path.StrokeWidth), + Fill: path.Fill, + }, + }) } - return extractRoughPaths(r) + return paths } type attrs struct { @@ -772,30 +727,14 @@ func (rp roughPath) StyleCSS() string { return style } -func extractRoughPaths(r jsrunner.JSRunner) ([]roughPath, error) { - val, err := r.RunString("JSON.stringify(node.children, null, ' ')") - if err != nil { - return nil, err - } - - var roughPaths []roughPath - err = json.Unmarshal([]byte(val.String()), &roughPaths) - if err != nil { - return nil, err - } - +func truncatePathData(pathData string) string { // we want to have a fixed precision to the decimals in the path data - for i := range roughPaths { - // truncate all floats in path to only use up to 6 decimal places - roughPaths[i].Attrs.D = floatRE.ReplaceAllStringFunc(roughPaths[i].Attrs.D, func(floatStr string) string { - i := strings.Index(floatStr, ".") - decimalLen := len(floatStr) - i - 1 - end := i + go2.Min(decimalLen, 6) - return floatStr[:end+1] - }) - } - - return roughPaths, nil + return floatRE.ReplaceAllStringFunc(pathData, func(floatStr string) string { + i := strings.Index(floatStr, ".") + decimalLen := len(floatStr) - i - 1 + end := i + go2.Min(decimalLen, 6) + return floatStr[:end+1] + }) } func extractPathData(roughPaths []roughPath) ([]string, error) { @@ -806,133 +745,72 @@ func extractPathData(roughPaths []roughPath) ([]string, error) { return paths, nil } -func ArrowheadJS(r jsrunner.JSRunner, arrowhead d2target.Arrowhead, stroke string, strokeWidth int) (arrowJS, extraJS string) { +func arrowOptions(stroke string, strokeWidth int, seed float64) *rough.Options { + return &rough.Options{ + StrokeWidth: rough.Float64(float64(strokeWidth)), + Stroke: rough.String(stroke), + Seed: rough.Float64(seed), + } +} + +func solidArrowOptions(stroke, fill string, strokeWidth int, fillWeight, seed float64) *rough.Options { + o := arrowOptions(stroke, strokeWidth, seed) + o.Fill = rough.String(fill) + o.FillStyle = rough.String("solid") + if fillWeight >= 0 { + o.FillWeight = rough.Float64(fillWeight) + } + return o +} + +func arrowheadDrawable(g *rough.Generator, arrowhead d2target.Arrowhead, stroke string, strokeWidth int) (arrow rough.Drawable, extra *rough.Drawable, ok bool) { // Note: selected each seed that looks the good for consistent renders switch arrowhead { case d2target.ArrowArrowhead: - arrowJS = fmt.Sprintf( - `node = rc.linearPath(%s, { strokeWidth: %d, stroke: "%s", seed: 3 })`, - `[[-10, -4], [0, 0], [-10, 4]]`, - strokeWidth, - stroke, - ) + arrow = g.LinearPath([]rough.Point{{-10, -4}, {0, 0}, {-10, 4}}, arrowOptions(stroke, strokeWidth, 3)) case d2target.TriangleArrowhead: - arrowJS = fmt.Sprintf( - `node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 2 })`, - `[[-10, -4], [0, 0], [-10, 4]]`, - strokeWidth, - stroke, - stroke, - ) + arrow = g.Polygon([]rough.Point{{-10, -4}, {0, 0}, {-10, 4}}, solidArrowOptions(stroke, stroke, strokeWidth, -1, 2)) case d2target.UnfilledTriangleArrowhead: - arrowJS = fmt.Sprintf( - `node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 2 })`, - `[[-10, -4], [0, 0], [-10, 4]]`, - strokeWidth, - stroke, - BG_COLOR, - ) + arrow = g.Polygon([]rough.Point{{-10, -4}, {0, 0}, {-10, 4}}, solidArrowOptions(stroke, BG_COLOR, strokeWidth, -1, 2)) case d2target.DiamondArrowhead: - arrowJS = fmt.Sprintf( - `node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 1 })`, - `[[-20, 0], [-10, 5], [0, 0], [-10, -5], [-20, 0]]`, - strokeWidth, - stroke, - BG_COLOR, - ) + arrow = g.Polygon([]rough.Point{{-20, 0}, {-10, 5}, {0, 0}, {-10, -5}, {-20, 0}}, solidArrowOptions(stroke, BG_COLOR, strokeWidth, -1, 1)) case d2target.FilledDiamondArrowhead: - arrowJS = fmt.Sprintf( - `node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "zigzag", fillWeight: 4, seed: 1 })`, - `[[-20, 0], [-10, 5], [0, 0], [-10, -5], [-20, 0]]`, - strokeWidth, - stroke, - stroke, - ) + o := solidArrowOptions(stroke, stroke, strokeWidth, 4, 1) + o.FillStyle = rough.String("zigzag") + arrow = g.Polygon([]rough.Point{{-20, 0}, {-10, 5}, {0, 0}, {-10, -5}, {-20, 0}}, o) case d2target.CrossArrowhead: - arrowJS = fmt.Sprintf( - `node = rc.linearPath(%s, { strokeWidth: %d, stroke: "%s", seed: 3 })`, - `[[-6, -6], [6, 6], [0, 0], [-6, 6], [0, 0], [6, -6]]`, - strokeWidth, - stroke, - ) + arrow = g.LinearPath([]rough.Point{{-6, -6}, {6, 6}, {0, 0}, {-6, 6}, {0, 0}, {6, -6}}, arrowOptions(stroke, strokeWidth, 3)) case d2target.CfManyRequired: - arrowJS = fmt.Sprintf( - // TODO why does fillStyle: "zigzag" error with path - `node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 2 })`, - `"M-15,-10 -15,10 M0,10 -15,0 M0,-10 -15,0"`, - strokeWidth, - stroke, - stroke, - ) + arrow = g.Path("M-15,-10 -15,10 M0,10 -15,0 M0,-10 -15,0", solidArrowOptions(stroke, stroke, strokeWidth, 4, 2)) case d2target.CfMany: - arrowJS = fmt.Sprintf( - `node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 8 })`, - `"M0,10 -15,0 M0,-10 -15,0"`, - strokeWidth, - stroke, - stroke, - ) - extraJS = fmt.Sprintf( - `node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 1, seed: 4 })`, - strokeWidth, - stroke, - BG_COLOR, - ) + arrow = g.Path("M0,10 -15,0 M0,-10 -15,0", solidArrowOptions(stroke, stroke, strokeWidth, 4, 8)) + d := g.Circle(-20, 0, 8, solidArrowOptions(stroke, BG_COLOR, strokeWidth, 1, 4)) + extra = &d case d2target.CfOneRequired: - arrowJS = fmt.Sprintf( - `node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 2 })`, - `"M-15,-10 -15,10 M-10,-10 -10,10"`, - strokeWidth, - stroke, - stroke, - ) + arrow = g.Path("M-15,-10 -15,10 M-10,-10 -10,10", solidArrowOptions(stroke, stroke, strokeWidth, 4, 2)) case d2target.CfOne: - arrowJS = fmt.Sprintf( - `node = rc.path(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 4, seed: 3 })`, - `"M-10,-10 -10,10"`, - strokeWidth, - stroke, - stroke, - ) - extraJS = fmt.Sprintf( - `node = rc.circle(-20, 0, 8, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 1, seed: 5 })`, - strokeWidth, - stroke, - BG_COLOR, - ) + arrow = g.Path("M-10,-10 -10,10", solidArrowOptions(stroke, stroke, strokeWidth, 4, 3)) + d := g.Circle(-20, 0, 8, solidArrowOptions(stroke, BG_COLOR, strokeWidth, 1, 5)) + extra = &d case d2target.CircleArrowhead: - arrowJS = fmt.Sprintf( - `node = rc.circle(-2, -1, 8, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", fillWeight: 1, seed: 5 })`, - strokeWidth, - stroke, - BG_COLOR, - ) + arrow = g.Circle(-2, -1, 8, solidArrowOptions(stroke, BG_COLOR, strokeWidth, 1, 5)) case d2target.BoxArrowhead: - arrowJS = fmt.Sprintf( - `node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 1})`, - `[[0, -10], [0, 10], [-20, 10], [-20, -10]]`, - strokeWidth, - stroke, - BG_COLOR, - ) + arrow = g.Polygon([]rough.Point{{0, -10}, {0, 10}, {-20, 10}, {-20, -10}}, solidArrowOptions(stroke, BG_COLOR, strokeWidth, -1, 1)) case d2target.FilledBoxArrowhead: - arrowJS = fmt.Sprintf( - `node = rc.polygon(%s, { strokeWidth: %d, stroke: "%s", fill: "%s", fillStyle: "solid", seed: 1})`, - `[[0, -10], [0, 10], [-20, 10], [-20, -10]]`, - strokeWidth, - stroke, - stroke, - ) + arrow = g.Polygon([]rough.Point{{0, -10}, {0, 10}, {-20, 10}, {-20, -10}}, solidArrowOptions(stroke, stroke, strokeWidth, -1, 1)) + default: + return rough.Drawable{}, nil, false } - return + return arrow, extra, true } -func Arrowheads(r jsrunner.JSRunner, connection d2target.Connection, srcAdj, dstAdj *geo.Point) (string, error) { +func Arrowheads(connection d2target.Connection, srcAdj, dstAdj *geo.Point) (string, error) { arrowPaths := []string{} if connection.SrcArrow != d2target.NoArrowhead { - arrowJS, extraJS := ArrowheadJS(r, connection.SrcArrow, connection.Stroke, connection.StrokeWidth) - if arrowJS == "" { + g := newGenerator() + arrow, extra, ok := arrowheadDrawable(g, connection.SrcArrow, connection.Stroke, connection.StrokeWidth) + if !ok { return "", nil } @@ -944,16 +822,9 @@ func Arrowheads(r jsrunner.JSRunner, connection d2target.Connection, srcAdj, dst startingSegment.Start.X+srcAdj.X, startingSegment.Start.Y+srcAdj.Y, angle, ) - roughPaths, err := computeRoughPaths(r, arrowJS) - if err != nil { - return "", err - } - if extraJS != "" { - extraPaths, err := computeRoughPaths(r, extraJS) - if err != nil { - return "", err - } - roughPaths = append(roughPaths, extraPaths...) + roughPaths := computeRoughPaths(g, arrow) + if extra != nil { + roughPaths = append(roughPaths, computeRoughPaths(g, *extra)...) } pathEl := d2themes.NewThemableElement("path", nil) @@ -969,8 +840,9 @@ func Arrowheads(r jsrunner.JSRunner, connection d2target.Connection, srcAdj, dst } if connection.DstArrow != d2target.NoArrowhead { - arrowJS, extraJS := ArrowheadJS(r, connection.DstArrow, connection.Stroke, connection.StrokeWidth) - if arrowJS == "" { + g := newGenerator() + arrow, extra, ok := arrowheadDrawable(g, connection.DstArrow, connection.Stroke, connection.StrokeWidth) + if !ok { return "", nil } @@ -983,16 +855,9 @@ func Arrowheads(r jsrunner.JSRunner, connection d2target.Connection, srcAdj, dst endingSegment.End.X+dstAdj.X, endingSegment.End.Y+dstAdj.Y, angle, ) - roughPaths, err := computeRoughPaths(r, arrowJS) - if err != nil { - return "", err - } - if extraJS != "" { - extraPaths, err := computeRoughPaths(r, extraJS) - if err != nil { - return "", err - } - roughPaths = append(roughPaths, extraPaths...) + roughPaths := computeRoughPaths(g, arrow) + if extra != nil { + roughPaths = append(roughPaths, computeRoughPaths(g, *extra)...) } pathEl := d2themes.NewThemableElement("path", nil) diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 76582282f9..7c6caed0bf 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -31,7 +31,6 @@ import ( "github.com/d2lang/d2/d2themes/d2themescatalog" "github.com/d2lang/d2/lib/color" "github.com/d2lang/d2/lib/geo" - "github.com/d2lang/d2/lib/jsrunner" "github.com/d2lang/d2/lib/label" "github.com/d2lang/d2/lib/shape" "github.com/d2lang/d2/lib/svg" @@ -377,7 +376,7 @@ func renderLegendShapeIcon(s d2target.Shape, x, y int, diagramHash string, theme finalBuf := &bytes.Buffer{} fmt.Fprintf(finalBuf, ``, x, y, 1.0/sizeFactor) - _, err := drawShape(buf, appendixBuf, diagramHash, iconShape, nil, theme) + _, err := drawShape(buf, appendixBuf, diagramHash, iconShape, false, theme) if err != nil { return "", err } @@ -426,7 +425,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, false, theme) if err != nil { return "", err } @@ -1003,7 +1002,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, sketch bool, inlineTheme *d2themes.Theme) (labelMask string, _ error) { opacityStyle := "" if connection.Opacity != 1.0 { opacityStyle = fmt.Sprintf(" style='opacity:%f'", connection.Opacity) @@ -1099,15 +1098,15 @@ func drawConnection(writer io.Writer, diagramHash string, connection d2target.Co path := pathData(connection, srcAdj, dstAdj) mask := fmt.Sprintf(`mask="url(#%s)"`, diagramHash) - if jsRunner != nil { - out, err := d2sketch.Connection(jsRunner, connection, path, mask) + if sketch { + out, err := d2sketch.Connection(connection, path, mask) if err != nil { return "", err } fmt.Fprint(writer, out) // render sketch arrowheads separately - arrowPaths, err := d2sketch.Arrowheads(jsRunner, connection, srcAdj, dstAdj) + arrowPaths, err := d2sketch.Arrowheads(connection, srcAdj, dstAdj) if err != nil { return "", err } @@ -1599,7 +1598,7 @@ func render3DHexagon(diagramHash string, targetShape d2target.Shape, inlineTheme return borderMask + mainShapeRendered + renderedSides + renderedBorder } -func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape d2target.Shape, jsRunner jsrunner.JSRunner, inlineTheme *d2themes.Theme) (labelMask string, err error) { +func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape d2target.Shape, sketch bool, inlineTheme *d2themes.Theme) (labelMask string, err error) { closingTag := "" if targetShape.Link != "" { @@ -1669,8 +1668,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape switch targetShape.Type { case d2target.ShapeClass: - if jsRunner != nil { - out, err := d2sketch.Class(jsRunner, targetShape) + if sketch { + out, err := d2sketch.Class(targetShape) if err != nil { return "", err } @@ -1686,8 +1685,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape fmt.Fprint(writer, closingTag) return labelMask, nil case d2target.ShapeSQLTable: - if jsRunner != nil { - out, err := d2sketch.Table(jsRunner, targetShape) + if sketch { + out, err := d2sketch.Table(targetShape) if err != nil { return "", err } @@ -1707,8 +1706,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape if targetShape.Multiple { fmt.Fprint(writer, renderDoubleOval(multipleTL, width, height, fill, "", stroke, style, inlineTheme)) } - if jsRunner != nil { - out, err := d2sketch.DoubleOval(jsRunner, targetShape, diagramHash) + if sketch { + out, err := d2sketch.DoubleOval(targetShape, diagramHash) if err != nil { return "", err } @@ -1720,8 +1719,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape if targetShape.Multiple { fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, "", stroke, style, inlineTheme)) } - if jsRunner != nil { - out, err := d2sketch.Oval(jsRunner, targetShape, diagramHash) + if sketch { + out, err := d2sketch.Oval(targetShape, diagramHash) if err != nil { return "", err } @@ -1768,8 +1767,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape el.Rx = borderRadius fmt.Fprint(writer, el.Render()) } - if jsRunner != nil { - out, err := d2sketch.Rect(jsRunner, targetShape, diagramHash) + if sketch { + out, err := d2sketch.Rect(targetShape, diagramHash) if err != nil { return "", err } @@ -1817,8 +1816,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape el.Rx = borderRadius fmt.Fprint(writer, el.Render()) } - if jsRunner != nil { - out, err := d2sketch.DoubleRect(jsRunner, targetShape, diagramHash) + if sketch { + out, err := d2sketch.DoubleRect(targetShape, diagramHash) if err != nil { return "", err } @@ -1865,8 +1864,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape } } - if jsRunner != nil { - out, err := d2sketch.Paths(jsRunner, targetShape, diagramHash, s.GetSVGPathData()) + if sketch { + out, err := d2sketch.Paths(targetShape, diagramHash, s.GetSVGPathData()) if err != nil { return "", err } @@ -1897,8 +1896,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape } } - if jsRunner != nil { - out, err := d2sketch.Paths(jsRunner, targetShape, diagramHash, s.GetSVGPathData()) + if sketch { + out, err := d2sketch.Paths(targetShape, diagramHash, s.GetSVGPathData()) if err != nil { return "", err } @@ -1988,7 +1987,7 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape ) if labelPosition.IsBorder() { - if jsRunner != nil { + if sketch { labelMask = makeBorderLabelMask(labelPosition, labelTL, targetShape.LabelWidth, targetShape.LabelHeight, box, targetShape.StrokeWidth, 1.0, tl) } else { labelMask = makeBorderLabelMask(labelPosition, labelTL, targetShape.LabelWidth, targetShape.LabelHeight, box, targetShape.StrokeWidth, 1.0, nil) @@ -2731,7 +2730,7 @@ func appendOnTrigger(buf *bytes.Buffer, source string, triggers []string, newCon var DEFAULT_DARK_THEME *int64 = nil // no theme selected func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { - var jsRunner jsrunner.JSRunner + sketch := false pad := DEFAULT_PADDING themeID := d2themescatalog.NeutralDefault.ID darkThemeID := DEFAULT_DARK_THEME @@ -2741,11 +2740,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { pad = int(*opts.Pad) } if opts.Sketch != nil && *opts.Sketch { - jsRunner = jsrunner.NewJSRunner() - err := d2sketch.LoadJS(jsRunner) - if err != nil { - return nil, err - } + sketch = true } if opts.ThemeID != nil { themeID = *opts.ThemeID @@ -2829,7 +2824,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, sketch, inlineTheme) if err != nil { return nil, err } @@ -2837,7 +2832,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { labelMasks = append(labelMasks, labelMask) } } else if s, is := obj.(d2target.Shape); is { - labelMask, err := drawShape(buf, appendixItemBuf, diagramHash, s, jsRunner, inlineTheme) + labelMask, err := drawShape(buf, appendixItemBuf, diagramHash, s, sketch, inlineTheme) if err != nil { return nil, err } else if labelMask != "" { @@ -2976,7 +2971,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) { fmt.Fprintf(upperBuf, ``, css) } - if jsRunner != nil { + if sketch { d2sketch.DefineFillPatterns(upperBuf, diagramHash) } } diff --git a/go.mod b/go.mod index a8e6db3e92..865677c371 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/coder/websocket v1.8.12 github.com/d2lang/dagro v0.1.0 github.com/d2lang/elk-go v0.1.0 + github.com/d2lang/rough-go v0.1.0 github.com/d2lang/util-go v0.2.0 github.com/dop251/goja v0.0.0-20240927123429-241b342198c2 github.com/dsoprea/go-exif/v3 v3.0.1 diff --git a/go.sum b/go.sum index e7d2d1e88f..c3ec67864f 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/d2lang/dagro v0.1.0 h1:517f9fzr6p1eN5TdUXjUGy9DKZnHhPIScWOsmN0ot4w= github.com/d2lang/dagro v0.1.0/go.mod h1:GYboiXSkXU1tuj7jYRXD8+lvHJBBZk3SAbfVT6qJsLU= github.com/d2lang/elk-go v0.1.0 h1:lR0Fx6N90PQCybxAfO6F6RunI87PKDvNiAipJzlppF8= github.com/d2lang/elk-go v0.1.0/go.mod h1:tvu+zORoU3aAbBetbyvmwsF8Tjdvj/+fG9owqahZ+BQ= +github.com/d2lang/rough-go v0.1.0 h1:6FBZj+HXtt77RZChBKaMCocKi8fk1gcUq/R7OTuO0/o= +github.com/d2lang/rough-go v0.1.0/go.mod h1:jBrT0USd0UYX1c61RU2T8Uj8OLMp4NKdWpe7SEpZJ/4= github.com/d2lang/util-go v0.2.0 h1:X9+F4j2S0453cG3EpA5bm9KBEV4ZAanxH+5ELA2yPDk= github.com/d2lang/util-go v0.2.0/go.mod h1:9t8cKlmsQw6UlChU4HQE9tZr8efjKIGDHkDdDYUeJy0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=