├── README.md ├── index.html ├── main.js ├── test └── example2.txt └── vendor ├── rough.min.js └── viz-lite.js /README.md: -------------------------------------------------------------------------------- 1 | # Kafka Streams Topology Visualizer 2 | 3 | A tool helps visualizing stream topologies by generating nice looking diagrams from a kafka stream topology descriptions. 4 | 5 | ## [Try now](https://zz85.github.io/kafka-streams-viz) 6 | 7 | This was conceived during one of the lab days @ Zendesk Singapore. 8 | 9 | Thanks to the following libraries 10 | 1. [Viz.js](https://github.com/mdaines/viz.js/) an emscripten built of Graphviz 11 | 2. [rough.js](https://github.com/pshihn/rough/) for generating hand-drawn like diagrams. 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 31 | 32 | 33 | 34 |

Kafka Streams Topology Visualizer

35 | 36 | Converts an ASCII Kafka Topology description into a hand drawn diagram. Github link. 37 | 38 | 39 |
40 |

Input Kafka Topology

41 | 70 |
71 | 72 |
73 |

Output Sketch Diagram

74 | 75 |
76 |
> 77 | 78 | 79 |
80 | 81 |
82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author zz85 (https://github.com/zz85 | https://twitter.com/blurspline) 3 | */ 4 | 5 | var dpr, rc, ctx; 6 | const DEBUG = false; 7 | const STORAGE_KEY = 'kafka-streams-viz'; 8 | 9 | function processName(name) { 10 | return name.replace(/-/g, '-\\n'); 11 | } 12 | 13 | // converts kafka stream ascii topo description to DOT language 14 | function convertTopoToDot(topo) { 15 | var lines = topo.split('\n'); 16 | var results = []; 17 | var outside = []; 18 | var stores = new Set(); 19 | var topics = new Set(); 20 | var entityName; 21 | 22 | // dirty but quick parsing 23 | lines.forEach(line => { 24 | var sub = /Sub-topology: ([0-9]*)/; 25 | var match = sub.exec(line); 26 | 27 | if (match) { 28 | if (results.length) results.push(`}`); 29 | results.push(`subgraph cluster_${match[1]} { 30 | label = "${match[0]}"; 31 | 32 | style=filled; 33 | color=lightgrey; 34 | node [style=filled,color=white]; 35 | `); 36 | 37 | return; 38 | } 39 | 40 | match = /(Source\:|Processor\:|Sink:)\s+(\S+)\s+\((topics|topic|stores)\:(.*)\)/.exec(line) 41 | 42 | if (match) { 43 | entityName = processName(match[2]); 44 | var type = match[3]; // source, processor or sink 45 | var linkedNames = match[4]; 46 | linkedNames = linkedNames.replace(/\[|\]/g, ''); 47 | linkedNames.split(',').forEach(linkedName => { 48 | linkedName = processName(linkedName.trim()); 49 | 50 | if (linkedName === '') { 51 | // short circuit 52 | } 53 | else if (type === 'topics') { 54 | // from 55 | outside.push(`"${linkedName}" -> "${entityName}";`); 56 | topics.add(linkedName); 57 | } 58 | else if (type === 'topic') { 59 | // to 60 | outside.push(`"${entityName}" -> "${linkedName}";`); 61 | topics.add(linkedName); 62 | } 63 | else if (type === 'stores') { 64 | if (entityName.includes("JOIN")) { 65 | outside.push(`"${linkedName}" -> "${entityName}";`); 66 | } else { 67 | outside.push(`"${entityName}" -> "${linkedName}";`); 68 | } 69 | 70 | stores.add(linkedName); 71 | } 72 | }); 73 | 74 | return; 75 | } 76 | 77 | match = /\-\-\>\s+(.*)$/.exec(line); 78 | 79 | if (match && entityName) { 80 | var targets = match[1]; 81 | targets.split(',').forEach(name => { 82 | var linkedName = processName(name.trim()); 83 | if (linkedName === 'none') return; 84 | 85 | results.push(`"${entityName}" -> "${linkedName}";`); 86 | }); 87 | } 88 | }) 89 | 90 | if (results.length) results.push(`}`); 91 | 92 | results = results.concat(outside); 93 | 94 | stores.forEach(node => { 95 | results.push(`"${node}" [shape=cylinder];`) 96 | }); 97 | 98 | topics.forEach(node => { 99 | results.push(`"${node}" [shape=rect];`) 100 | }); 101 | 102 | return ` 103 | digraph G { 104 | label = "Kafka Streams Topology" 105 | 106 | ${results.join('\n')} 107 | } 108 | `; 109 | } 110 | 111 | function update() { 112 | var topo = input.value; 113 | var dotCode = convertTopoToDot(topo); 114 | if (DEBUG) console.log('dot code\n', dotCode); 115 | 116 | 117 | graphviz_code.value = dotCode; 118 | 119 | var params = { 120 | engine: 'dot', 121 | format: 'svg' 122 | }; 123 | 124 | var svgCode = Viz(dotCode, params); 125 | 126 | svg_container.innerHTML = svgCode; 127 | 128 | var svg = svg_container.querySelector('svg') 129 | dpr = window.devicePixelRatio 130 | canvas.width = svg.viewBox.baseVal.width * dpr | 0; 131 | canvas.height = svg.viewBox.baseVal.height * dpr | 0; 132 | canvas.style.width = `${svg.viewBox.baseVal.width}px`; 133 | canvas.style.height = `${svg.viewBox.baseVal.height}px`; 134 | 135 | rc = rough.canvas(canvas); 136 | ctx = rc.ctx 137 | ctx.scale(dpr, dpr); 138 | 139 | var g = svg.querySelector('g'); 140 | 141 | try { 142 | traverseSvgToRough(g); 143 | 144 | sessionStorage.setItem(STORAGE_KEY, topo); 145 | } 146 | catch (e) { 147 | console.error('Exception generating graph', e && e.stack || e); 148 | // TODO update Frontend 149 | } 150 | } 151 | 152 | function nullIfNone(attribute) { 153 | return attribute === 'none' ? null : attribute; 154 | } 155 | 156 | /** 157 | * The following part can be removed when rough.js adds support for rendering svgs. 158 | */ 159 | 160 | function getFillStroke(child) { 161 | var fill = nullIfNone(child.getAttribute('fill')); 162 | var stroke = nullIfNone(child.getAttribute('stroke')); 163 | var isBaseRectangle = child.nodeName === 'polygon' && child.parentNode.id === 'graph0'; 164 | 165 | return { 166 | fill: isBaseRectangle ? 'white' : fill, 167 | fillStyle: isBaseRectangle ? 'solid' : 'hachure', 168 | stroke: stroke 169 | }; 170 | } 171 | 172 | function splitToArgs(array, delimiter) { 173 | return array.split(delimiter || ',').map(v => +v); 174 | } 175 | 176 | // node traversal function 177 | function traverseSvgToRough(child) { 178 | 179 | if (child.nodeName === 'path') { 180 | var d = child.getAttribute('d'); 181 | var opts = getFillStroke(child); 182 | rc.path(d, opts); 183 | return; 184 | } 185 | 186 | if (child.nodeName === 'ellipse') { 187 | var cx = +child.getAttribute('cx'); 188 | var cy = +child.getAttribute('cy'); 189 | var rx = +child.getAttribute('rx'); 190 | var ry = +child.getAttribute('ry'); 191 | 192 | var opts = getFillStroke(child); 193 | 194 | rc.ellipse(cx, cy, rx * 1.5, ry * 1.5); 195 | return; 196 | } 197 | 198 | if (child.nodeName === 'text') { 199 | var fontFamily = child.getAttribute('font-family') 200 | var fontSize = +child.getAttribute('font-size') 201 | var anchor = child.getAttribute('text-anchor') 202 | 203 | if (anchor === 'middle') { 204 | ctx.textAlign = 'center'; 205 | } 206 | 207 | if (fontFamily) { 208 | ctx.fontFamily = fontFamily; 209 | } 210 | 211 | if (fontSize) { 212 | ctx.fontSize = fontSize; 213 | } 214 | 215 | ctx.fillText(child.textContent, child.getAttribute('x'), child.getAttribute('y')); 216 | return; 217 | } 218 | 219 | if (child.nodeName === 'polygon') { 220 | var pts = child.getAttribute('points') 221 | 222 | var opts = getFillStroke(child); 223 | rc.path(`M${pts}Z`, opts); 224 | 225 | return; 226 | } 227 | 228 | if (child.nodeName === 'g') { 229 | var transform = child.getAttribute('transform'); 230 | ctx.save(); 231 | 232 | if (transform) { 233 | var scale = /scale\(([^)]*)\)/.exec(transform); 234 | if (scale) { 235 | var args = scale[1].split(' ').map(parseFloat); 236 | ctx.scale(...args); 237 | } 238 | 239 | var rotate = /rotate\(([^)]*)\)/.exec(transform); 240 | if (rotate) { 241 | var args = rotate[1].split(' ').map(parseFloat); 242 | ctx.rotate(...args); 243 | } 244 | 245 | var translate = /translate\(([^)]*)\)/.exec(transform); 246 | if (translate) { 247 | var args = translate[1].split(' ').map(parseFloat); 248 | ctx.translate(...args); 249 | } 250 | } 251 | 252 | [...child.children].forEach(traverseSvgToRough); 253 | 254 | ctx.restore(); 255 | return; 256 | } 257 | } 258 | 259 | var pending; 260 | function scheduleUpdate() { 261 | if (pending) clearTimeout(pending); 262 | 263 | pending = setTimeout(() => { 264 | pending = null; 265 | update(); 266 | }, 200); 267 | } 268 | 269 | // startup 270 | var topo; 271 | 272 | if (window.location.hash.length > 1) { 273 | try { 274 | topo = atob(window.location.hash.substr(1)); 275 | } catch { 276 | console.log("Can not read topo from url hash"); 277 | window.location.hash = ""; 278 | } 279 | } 280 | 281 | if (!topo) { 282 | topo = sessionStorage.getItem(STORAGE_KEY); 283 | } 284 | 285 | if (topo) input.value = topo; 286 | update(); 287 | -------------------------------------------------------------------------------- /test/example2.txt: -------------------------------------------------------------------------------- 1 | Kafka Topology: Sub-topologies: 2 | Sub-topology: 0 3 | Source: KSTREAM-SOURCE-0000000000 (topics: [moochannel]) 4 | --> KSTREAM-KEY-SELECT-0000000003, KSTREAM-TRANSFORM-0000000001 5 | Processor: KSTREAM-KEY-SELECT-0000000003 (stores: []) 6 | --> KSTREAM-FILTER-0000000007 7 | <-- KSTREAM-SOURCE-0000000000 8 | Processor: KSTREAM-FILTER-0000000007 (stores: []) 9 | --> KSTREAM-SINK-0000000006 10 | <-- KSTREAM-KEY-SELECT-0000000003 11 | Processor: KSTREAM-TRANSFORM-0000000001 (stores: [conversation-state]) 12 | --> KSTREAM-SINK-0000000002 13 | <-- KSTREAM-SOURCE-0000000000 14 | Sink: KSTREAM-SINK-0000000002 (topic: moochannel-output) 15 | <-- KSTREAM-TRANSFORM-0000000001 16 | Sink: KSTREAM-SINK-0000000006 (topic: KSTREAM-AGGREGATE-STATE-STORE-0000000004-repartition) 17 | <-- KSTREAM-FILTER-0000000007 18 | Sub-topology: 1 19 | Source: KSTREAM-SOURCE-0000000008 (topics: [KSTREAM-AGGREGATE-STATE-STORE-0000000004-repartition]) 20 | --> KSTREAM-AGGREGATE-0000000005 21 | Processor: KSTREAM-AGGREGATE-0000000005 (stores: [KSTREAM-AGGREGATE-STATE-STORE-0000000004]) 22 | --> KTABLE-TOSTREAM-0000000009 23 | <-- KSTREAM-SOURCE-0000000008 24 | Processor: KTABLE-TOSTREAM-0000000009 (stores: []) 25 | --> KSTREAM-SINK-0000000010 26 | <-- KSTREAM-AGGREGATE-0000000005 27 | Sink: KSTREAM-SINK-0000000010 (topic: assignments) 28 | <-- KTABLE-TOSTREAM-0000000009 29 | Sub-topology: 2 30 | Source: Source (topics: [moochannel-output]) 31 | --> Process 32 | Processor: Process (stores: [assignment-state]) 33 | --> none 34 | <-- Source 35 | -------------------------------------------------------------------------------- /vendor/rough.min.js: -------------------------------------------------------------------------------- 1 | var rough=function(){"use strict";function a(){return{LEFT:0,RIGHT:1,INTERSECTS:2,AHEAD:3,BEHIND:4,SEPARATE:5,UNDEFINED:6}}var b=Math.tan,c=Math.pow,d=Math.cos,e=Math.sin,f=Math.PI,g=Math.sqrt,h=Math.max,j=Math.min,i=Math.abs,k=Number.MAX_VALUE;class l{constructor(b,c,d,e){this.RoughSegmentRelationConst=a(),this.px1=b,this.py1=c,this.px2=d,this.py2=e,this.xi=k,this.yi=k,this.a=e-c,this.b=b-d,this.c=d*c-b*e,this._undefined=0==this.a&&0==this.b&&0==this.c}isUndefined(){return this._undefined}compare(d){if(this.isUndefined()||d.isUndefined())return this.RoughSegmentRelationConst.UNDEFINED;var e=k,f=k,g=0,l=0,m=this.a,n=this.b,b=this.c;return(1e-5=j(d.py1,d.py2)&&this.py1<=h(d.py1,d.py2)?(this.xi=this.px1,this.yi=this.py1,this.RoughSegmentRelationConst.INTERSECTS):this.py2>=j(d.py1,d.py2)&&this.py2<=h(d.py1,d.py2)?(this.xi=this.px2,this.yi=this.py2,this.RoughSegmentRelationConst.INTERSECTS):this.RoughSegmentRelationConst.SEPARATE:this.RoughSegmentRelationConst.SEPARATE:(this.xi=this.px1,this.yi=f*this.xi+l,-1e-5>(this.py1-this.yi)*(this.yi-this.py2)||-1e-5>(d.py1-this.yi)*(this.yi-d.py2)?this.RoughSegmentRelationConst.SEPARATE:1e-5>i(d.a)?-1e-5>(d.px1-this.xi)*(this.xi-d.px2)?this.RoughSegmentRelationConst.SEPARATE:this.RoughSegmentRelationConst.INTERSECTS:this.RoughSegmentRelationConst.INTERSECTS):f==k?(this.xi=d.px1,this.yi=e*this.xi+g,-1e-5>(d.py1-this.yi)*(this.yi-d.py2)||-1e-5>(this.py1-this.yi)*(this.yi-this.py2)?this.RoughSegmentRelationConst.SEPARATE:1e-5>i(m)?-1e-5>(this.px1-this.xi)*(this.xi-this.px2)?this.RoughSegmentRelationConst.SEPARATE:this.RoughSegmentRelationConst.INTERSECTS:this.RoughSegmentRelationConst.INTERSECTS):e==f?g==l?this.px1>=j(d.px1,d.px2)&&this.px1<=h(d.py1,d.py2)?(this.xi=this.px1,this.yi=this.py1,this.RoughSegmentRelationConst.INTERSECTS):this.px2>=j(d.px1,d.px2)&&this.px2<=h(d.px1,d.px2)?(this.xi=this.px2,this.yi=this.py2,this.RoughSegmentRelationConst.INTERSECTS):this.RoughSegmentRelationConst.SEPARATE:this.RoughSegmentRelationConst.SEPARATE:(this.xi=(l-g)/(e-f),this.yi=e*this.xi+g,-1e-5>(this.px1-this.xi)*(this.xi-this.px2)||-1e-5>(d.px1-this.xi)*(this.xi-d.px2)?this.RoughSegmentRelationConst.SEPARATE:this.RoughSegmentRelationConst.INTERSECTS)}getLength(){return this._getLength(this.px1,this.py1,this.px2,this.py2)}_getLength(a,b,c,d){var e=c-a,f=d-b;return g(e*e+f*f)}}class m{constructor(a,b,c,d,e,f,g,h){this.top=a,this.bottom=b,this.left=c,this.right=d,this.gap=e,this.sinAngle=f,this.tanAngle=h,1e-4>i(f)?this.pos=c+e:.9999i(this.sinAngle)){if(this.posthis.right&&c>this.right;)if(this.pos+=this.hGap,b=this.pos-this.deltaX/2,c=this.pos+this.deltaX/2,this.pos>this.right+this.deltaX)return null;let f=new l(b,d,c,e);f.compare(this.sLeft)==a().INTERSECTS&&(b=f.xi,d=f.yi),f.compare(this.sRight)==a().INTERSECTS&&(c=f.xi,e=f.yi),0p){let a=g(1-p/(this._rx*this._rx*this._ry*this._ry));this._rx=a,this._ry=a,m=0}else m=(j==k?-1:1)*g(p/(this._rx*this._rx*o*o+this._ry*this._ry*n*n));let q=m*this._rx*o/this._ry,r=-m*this._ry*n/this._rx;this._C=[0,0],this._C[0]=this._cosPhi*q-this._sinPhi*r+(a[0]+b[0])/2,this._C[1]=this._sinPhi*q+this._cosPhi*r+(a[1]+b[1])/2,this._theta=this.calculateVectorAngle(1,0,(n-q)/this._rx,(o-r)/this._ry);let s=this.calculateVectorAngle((n-q)/this._rx,(o-r)/this._ry,(-n-q)/this._rx,(-o-r)/this._ry);!k&&0s&&(s+=2*f),this._numSegs=Math.ceil(i(s/(f/2))),this._delta=s/this._numSegs,this._T=8/3*e(this._delta/4)*e(this._delta/4)/e(this._delta/2),this._from=a}getNextSegment(){var a,b,c;if(this._segIndex==this._numSegs)return null;let f=d(this._theta),g=e(this._theta),h=this._theta+this._delta,i=d(h),j=e(h);return c=[this._cosPhi*this._rx*i-this._sinPhi*this._ry*j+this._C[0],this._sinPhi*this._rx*i+this._cosPhi*this._ry*j+this._C[1]],a=[this._from[0]+this._T*(-this._cosPhi*this._rx*g-this._sinPhi*this._ry*f),this._from[1]+this._T*(-this._sinPhi*this._rx*g+this._cosPhi*this._ry*f)],b=[c[0]+this._T*(this._cosPhi*this._rx*j+this._sinPhi*this._ry*i),c[1]+this._T*(this._sinPhi*this._rx*j-this._cosPhi*this._ry*i)],this._theta=h,this._from=[c[0],c[1]],this._segIndex++,{cp1:a,cp2:b,to:c}}calculateVectorAngle(a,b,c,d){var e=Math.atan2;let g=e(b,a),h=e(d,c);return h>=g?h-g:2*f-(g-h)}}class r{constructor(a,b){this.sets=a,this.closed=b}fit(a){let b=[];for(const c of this.sets){let d=c.length,e=Math.floor(a*d);if(5>e){if(5>=d)continue;e=5}b.push(this.reduce(c,e))}let c="";for(const d of b){for(let a,b=0;bb;){let e=-1,f=-1;for(let h=1;he||js;)s+=2*f,t+=2*f;t-s>2*f&&(s=0,t=2*f);let u=2*f/n.curveStepCount,v=j(u/2,(t-s)/2),w=this._arc(v,o,p,q,r,s,t,1,n),x=this._arc(v,o,p,q,r,s,t,1.5,n),y=w.concat(x);return l&&(m?(y=y.concat(this._doubleLine(o,p,o+q*d(s),p+r*e(s),n)),y=y.concat(this._doubleLine(o,p,o+q*d(t),p+r*e(t),n))):(y.push({op:"lineTo",data:[o,p]}),y.push({op:"lineTo",data:[o+q*d(s),p+r*e(s)]}))),{type:"path",ops:y}}hachureFillArc(a,b,c,g,h,j,k){let l=a,m=b,n=i(c/2),o=i(g/2);n+=this._getOffset(.01*-n,.01*n,k),o+=this._getOffset(.01*-o,.01*o,k);let p=h,q=j;for(;0>p;)p+=2*f,q+=2*f;q-p>2*f&&(p=0,q=2*f);let r=(q-p)/k.curveStepCount,s=[],t=[];for(let f=p;f<=q;f+=r)s.push(l+n*d(f)),t.push(m+o*e(f));return s.push(l+n*d(q)),t.push(m+o*e(q)),s.push(l),t.push(m),this.hachureFillShape(s,t,k)}solidFillShape(a,b,c){let d=[];if(a&&b&&a.length&&b.length&&a.length===b.length){let f=c.maxRandomnessOffset||0;const g=a.length;if(2q&&(q=4*g.strokeWidth),q=h(q,.1);const r=i%180*(f/180),s=d(r),t=e(r),u=b(r),v=new m(o-1,p+1,l-1,n+1,q,t,s,u);for(let b;null!=(b=v.getNextLine());){let d=this._getIntersectingLines(b,a,c);for(let a=0;a=n&&(n=4*h.strokeWidth);let o=h.fillWeight;0>o&&(o=h.strokeWidth/2);let p=b(m%180*(f/180)),q=l/k,r=g(q*p*q*p+1),s=q*p/r,t=1/r,u=n/(k*l/g(l*t*(l*t)+k*s*(k*s))/k),v=g(k*k-(a-k+u)*(a-k+u));for(var w=a-k+u;wf;f++)0===f?k.push({op:"move",data:[h.x,h.y]}):k.push({op:"move",data:[h.x+this._getOffset(-l[0],l[0],j),h.y+this._getOffset(-l[0],l[0],j)]}),m=[e+this._getOffset(-l[f],l[f],j),g+this._getOffset(-l[f],l[f],j)],k.push({op:"bcurveTo",data:[a+this._getOffset(-l[f],l[f],j),b+this._getOffset(-l[f],l[f],j),c+this._getOffset(-l[f],l[f],j),d+this._getOffset(-l[f],l[f],j),m[0],m[1]]});return h.setPosition(m[0],m[1]),k}_processSegment(a,b,c,d){let e=[];switch(b.key){case"M":case"m":{let c="m"===b.key;if(2<=b.data.length){let f=+b.data[0],g=+b.data[1];c&&(f+=a.x,g+=a.y);let h=1*(d.maxRandomnessOffset||0);f+=this._getOffset(-h,h,d),g+=this._getOffset(-h,h,d),a.setPosition(f,g),e.push({op:"move",data:[f,g]})}break}case"L":case"l":{let c="l"===b.key;if(2<=b.data.length){let f=+b.data[0],g=+b.data[1];c&&(f+=a.x,g+=a.y),e=e.concat(this._doubleLine(a.x,a.y,f,g,d)),a.setPosition(f,g)}break}case"H":case"h":{const c="h"===b.key;if(b.data.length){let f=+b.data[0];c&&(f+=a.x),e=e.concat(this._doubleLine(a.x,a.y,f,a.y,d)),a.setPosition(f,a.y)}break}case"V":case"v":{const c="v"===b.key;if(b.data.length){let f=+b.data[0];c&&(f+=a.y),e=e.concat(this._doubleLine(a.x,a.y,a.x,f,d)),a.setPosition(a.x,f)}break}case"Z":case"z":{a.first&&(e=e.concat(this._doubleLine(a.x,a.y,a.first[0],a.first[1],d)),a.setPosition(a.first[0],a.first[1]),a.first=null);break}case"C":case"c":{const c="c"===b.key;if(6<=b.data.length){let f=+b.data[0],g=+b.data[1],h=+b.data[2],i=+b.data[3],j=+b.data[4],k=+b.data[5];c&&(f+=a.x,h+=a.x,j+=a.x,g+=a.y,i+=a.y,k+=a.y);let l=this._bezierTo(f,g,h,i,j,k,a,d);e=e.concat(l),a.bezierReflectionPoint=[j+(j-h),k+(k-i)]}break}case"S":case"s":{const f="s"===b.key;if(4<=b.data.length){let h=+b.data[0],i=+b.data[1],j=+b.data[2],k=+b.data[3];f&&(h+=a.x,j+=a.x,i+=a.y,k+=a.y);let l=h,m=i,n=c?c.key:"";var g=null;("c"==n||"C"==n||"s"==n||"S"==n)&&(g=a.bezierReflectionPoint),g&&(l=g[0],m=g[1]);let o=this._bezierTo(l,m,h,i,j,k,a,d);e=e.concat(o),a.bezierReflectionPoint=[j+(j-h),k+(k-i)]}break}case"Q":case"q":{const c="q"===b.key;if(4<=b.data.length){let g=+b.data[0],h=+b.data[1],i=+b.data[2],j=+b.data[3];c&&(g+=a.x,i+=a.x,h+=a.y,j+=a.y);let k=1*(1+.2*d.roughness),l=1.5*(1+.22*d.roughness);e.push({op:"move",data:[a.x+this._getOffset(-k,k,d),a.y+this._getOffset(-k,k,d)]});let m=[i+this._getOffset(-k,k,d),j+this._getOffset(-k,k,d)];e.push({op:"qcurveTo",data:[g+this._getOffset(-k,k,d),h+this._getOffset(-k,k,d),m[0],m[1]]}),e.push({op:"move",data:[a.x+this._getOffset(-l,l,d),a.y+this._getOffset(-l,l,d)]}),m=[i+this._getOffset(-l,l,d),j+this._getOffset(-l,l,d)],e.push({op:"qcurveTo",data:[g+this._getOffset(-l,l,d),h+this._getOffset(-l,l,d),m[0],m[1]]}),a.setPosition(m[0],m[1]),a.quadReflectionPoint=[i+(i-g),j+(j-h)]}break}case"T":case"t":{const h="t"===b.key;if(2<=b.data.length){let i=+b.data[0],j=+b.data[1];h&&(i+=a.x,j+=a.y);let k=i,l=j,m=c?c.key:"";var g=null;("q"==m||"Q"==m||"t"==m||"T"==m)&&(g=a.quadReflectionPoint),g&&(k=g[0],l=g[1]);let n=1*(1+.2*d.roughness),o=1.5*(1+.22*d.roughness);e.push({op:"move",data:[a.x+this._getOffset(-n,n,d),a.y+this._getOffset(-n,n,d)]});let p=[i+this._getOffset(-n,n,d),j+this._getOffset(-n,n,d)];e.push({op:"qcurveTo",data:[k+this._getOffset(-n,n,d),l+this._getOffset(-n,n,d),p[0],p[1]]}),e.push({op:"move",data:[a.x+this._getOffset(-o,o,d),a.y+this._getOffset(-o,o,d)]}),p=[i+this._getOffset(-o,o,d),j+this._getOffset(-o,o,d)],e.push({op:"qcurveTo",data:[k+this._getOffset(-o,o,d),l+this._getOffset(-o,o,d),p[0],p[1]]}),a.setPosition(p[0],p[1]),a.quadReflectionPoint=[i+(i-k),j+(j-l)]}break}case"A":case"a":{const c="a"===b.key;if(7<=b.data.length){let f=+b.data[0],g=+b.data[1],h=+b.data[2],i=+b.data[3],j=+b.data[4],k=+b.data[5],l=+b.data[6];if(c&&(k+=a.x,l+=a.y),k==a.x&&l==a.y)break;if(0==f||0==g)e=e.concat(this._doubleLine(a.x,a.y,k,l,d)),a.setPosition(k,l);else{d.maxRandomnessOffset||0;for(let b=0;1>b;b++){let b=new p([a.x,a.y],[k,l],[f,g],h,!!i,!!j),c=b.getNextSegment();for(;c;){let f=this._bezierTo(c.cp1[0],c.cp1[1],c.cp2[0],c.cp2[1],c.to[0],c.to[1],a,d);e=e.concat(f),c=b.getNextSegment()}}}}break}default:}return e}_getOffset(a,b,c){return c.roughness*(Math.random()*(b-a)+a)}_affine(a,b,c,d,e,f,g){return[-c*f-d*e+c+f*a+e*b,g*(c*e-d*f)+d+-g*e*a+g*f*b]}_doubleLine(a,b,c,d,e){const f=this._line(a,b,c,d,e,!0,!1),g=this._line(a,b,c,d,e,!0,!0);return f.concat(g)}_line(a,b,d,e,f,h,i){const j=c(a-d,2)+c(b-e,2);let k=f.maxRandomnessOffset||0;100*(k*k)>j&&(k=g(j)/10);const l=k/2,m=.2+.2*Math.random();let n=f.bowing*f.maxRandomnessOffset*(e-b)/200,o=f.bowing*f.maxRandomnessOffset*(a-d)/200;n=this._getOffset(-n,n,f),o=this._getOffset(-o,o,f);let p=[];return h&&(i?p.push({op:"move",data:[a+this._getOffset(-l,l,f),b+this._getOffset(-l,l,f)]}):p.push({op:"move",data:[a+this._getOffset(-k,k,f),b+this._getOffset(-k,k,f)]})),i?p.push({op:"bcurveTo",data:[n+a+(d-a)*m+this._getOffset(-l,l,f),o+b+(e-b)*m+this._getOffset(-l,l,f),n+a+2*(d-a)*m+this._getOffset(-l,l,f),o+b+2*(e-b)*m+this._getOffset(-l,l,f),d+this._getOffset(-l,l,f),e+this._getOffset(-l,l,f)]}):p.push({op:"bcurveTo",data:[n+a+(d-a)*m+this._getOffset(-k,k,f),o+b+(e-b)*m+this._getOffset(-k,k,f),n+a+2*(d-a)*m+this._getOffset(-k,k,f),o+b+2*(e-b)*m+this._getOffset(-k,k,f),d+this._getOffset(-k,k,f),e+this._getOffset(-k,k,f)]}),p}_curve(a,c,d){const e=a.length;let f=[];if(3d&&(d=c.strokeWidth/2),a.save(),a.strokeStyle=c.fill,a.lineWidth=d,this._drawToContext(a,b),a.restore()}_drawToContext(a,b){a.beginPath();for(let c of b.ops){const b=c.data;switch(c.op){case"move":a.moveTo(b[0],b[1]);break;case"bcurveTo":a.bezierCurveTo(b[0],b[1],b[2],b[3],b[4],b[5]);break;case"qcurveTo":a.quadraticCurveTo(b[0],b[1],b[2],b[3]);break;case"lineTo":a.lineTo(b[0],b[1]);}}"fillPath"===b.type?a.fill():a.stroke()}}class w extends v{_init(a){this.gen=new u(a,this.canvas)}async line(a,b,c,e,f){let g=await this.gen.line(a,b,c,e,f);return this.draw(g),g}async rectangle(a,b,c,e,f){let g=await this.gen.rectangle(a,b,c,e,f);return this.draw(g),g}async ellipse(a,b,c,e,f){let g=await this.gen.ellipse(a,b,c,e,f);return this.draw(g),g}async circle(a,b,c,e){let f=await this.gen.circle(a,b,c,e);return this.draw(f),f}async linearPath(a,b){let c=await this.gen.linearPath(a,b);return this.draw(c),c}async polygon(a,b){let c=await this.gen.polygon(a,b);return this.draw(c),c}async arc(a,b,c,e,f,g,h,i){let j=await this.gen.arc(a,b,c,e,f,g,h,i);return this.draw(j),j}async curve(a,b){let c=await this.gen.curve(a,b);return this.draw(c),c}async path(a,b){let c=await this.gen.path(a,b);return this.draw(c),c}}var x={canvas(a,b){return b&&b.async?new w(a,b):new v(a,b)},createRenderer(){return v.createRenderer()},generator(a,b){return a&&a.async?new u(a,b):new t(a,b)}};return x}(); --------------------------------------------------------------------------------