1- /*
1+ export interface Point {
2+ // Cartesian coordinates (starting at [0,0] in the bottom left).
3+ x : number ;
4+ y : number ;
25
3- TODO
4- - points relative to (bottom-right/center)? by default
5- - angles in degrees
6- - angle relative to horizontal (3 o'clock + positive is counterclockwise)
7- - draw path
8- - convert size to x/y
6+ // Optional cubic bezier handle configuration.
7+ handles ?: {
8+ // Direction of the outgoing path in degrees. Value is relative to the 3:00 position
9+ // on a clock and the positive direction is counter-clockwise.
10+ angle : number ;
911
10- */
12+ // Distance between each handle and the point.
13+ out : number ;
14+ in : number ;
15+ } ;
16+ }
1117
12- interface Point {
18+ interface InternalPoint {
19+ // Coordinates of the point in the SVG viewport.
1320 x : number ;
1421 y : number ;
15- handles ?: {
22+
23+ // Cubic bezier handle configuration.
24+ handles : {
25+ // Direction of the outgoing path in radians. Value is relative to the 9:00 position
26+ // on a clock and the positive direction is counter-clockwise.
1627 angle : number ;
17- in : number ;
28+
29+ // Distance between each handle and the point.
1830 out : number ;
31+ in : number ;
1932 } ;
2033}
2134
22- interface RenderOptions {
23- size : number ;
24- center ?: boolean ;
25- rotation ?: number ;
35+ export interface RenderOptions {
36+ // Viewport size.
37+ width : number ;
38+ height : number ;
39+
40+ // Transformation applied to all drawn points.
41+ transform ?: string ;
42+
43+ // Output path styling.
2644 fill ?: string ;
2745 stroke ?: string ;
2846 strokeWidth ?: number ;
29- handles ?: boolean ;
47+
48+ // Option to render guides (points, handles and viewport).
49+ guides ?: boolean ;
3050}
3151
32- const loop = < T > ( arr : T [ ] ) => ( i : number ) : T => {
52+ // Safe array access at any index using a modulo operation that will always be positive.
53+ const loopAccess = < T > ( arr : T [ ] ) => ( i : number ) : T => {
3354 return arr [ ( ( i % arr . length ) + arr . length ) % arr . length ] ;
34- }
55+ } ;
56+
57+ // Translates a point's [x,y] cartesian coordinates into values relative to the viewport.
58+ // Translates the angle from degrees to radians and moves the start angle a half rotation.
59+ const cleanupPoint = ( point : Point , opt : RenderOptions ) : InternalPoint => {
60+ const handles = point . handles || { angle : 0 , out : 0 , in : 0 } ;
61+ handles . angle = Math . PI + ( 2 * Math . PI * handles . angle / 360 ) ;
62+ return {
63+ x : point . x ,
64+ y : opt . height - point . y ,
65+ handles,
66+ } ;
67+ } ;
3568
3669// Renders a closed shape made up of the input points.
37- const render = ( points : Point [ ] , opt : RenderOptions ) : string => {
38- const count = points . length ;
39- const handles : { x1 : number , y1 : number , x2 : number , y2 : number } [ ] = [ ] ;
70+ const renderClosed = ( p : Point [ ] , opt : RenderOptions ) : string => {
71+ const points = p . map ( ( point ) => cleanupPoint ( point , opt ) ) ;
4072
41- for ( let i = 0 ; i < count ; i ++ ) {
42- const { x, y, handles : hands } = points [ i ] ;
73+ // Compute guides from input point data.
74+ const handles : { x1 : number , y1 : number , x2 : number , y2 : number } [ ] = [ ] ;
75+ for ( let i = 0 ; i < points . length ; i ++ ) {
76+ const { x, y, handles : hands } = loopAccess ( points ) ( i ) ;
4377
44- const next = loop ( points ) ( i + 1 ) ;
45- const nextHandles = next . handles || { angle : 0 , in : 0 , out : 0 } ;
78+ const next = loopAccess ( points ) ( i + 1 ) ;
79+ const nextHandles = next . handles ;
4680
4781 if ( hands === undefined ) {
4882 handles . push ( { x1 : x , y1 : y , x2 : next . x , y2 : next . y } ) ;
@@ -57,10 +91,12 @@ const render = (points: Point[], opt: RenderOptions): string => {
5791 } ) ;
5892 }
5993
94+ // Render path data attribute from points and handles. Must loop more times than the
95+ // number of points in order to correctly close the path.
6096 let path = "" ;
61- for ( let i = 0 ; i <= count ; i ++ ) {
62- const point = loop ( points ) ( i ) ;
63- const hands = loop ( handles ) ( i - 1 ) ;
97+ for ( let i = 0 ; i <= points . length ; i ++ ) {
98+ const point = loopAccess ( points ) ( i ) ;
99+ const hands = loopAccess ( handles ) ( i - 1 ) ;
64100
65101 // Start at the first point's coordinates.
66102 if ( i === 0 ) {
@@ -72,46 +108,62 @@ const render = (points: Point[], opt: RenderOptions): string => {
72108 path += `C${ hands . x1 } ,${ hands . y1 } ,${ hands . x2 } ,${ hands . y2 } ,${ point . x } ,${ point . y } ` ;
73109 }
74110
75- return `
76- <svg width="${ opt . size } " height="${ opt . size } " viewBox="0 0 ${ opt . size } ${ opt . size } " xmlns="http://www.w3.org/2000/svg">
77- <g transform="
78- ${ opt . center ? `translate(${ opt . size / 2 } , ${ opt . size / 2 } )` : "" }
79- rotate(${ opt . rotation || 0 } )
80- ">
111+ // Render guides if configured to do so.
112+ let guides = "" ;
113+ if ( opt . guides ) {
114+ // Bounding box.
115+ guides += `
116+ <rect x="0" y="0" width="${ opt . width } " height="${ opt . height } "
117+ fill="none" stroke="black" stroke-width="1" stroke-dasharray="2" />` ;
118+
119+ // Points and handles.
120+ for ( let i = 0 ; i < points . length ; i ++ ) {
121+ const { x, y} = loopAccess ( points ) ( i ) ;
122+ const hands = loopAccess ( handles ) ( i ) ;
123+ const nextPoint = loopAccess ( points ) ( i + 1 ) ;
124+
125+ guides += `
126+ <line x1="${ x } " y1="${ y } " x2="${ hands . x1 } " y2="${ hands . y1 } "
127+ stroke-width="1" stroke="black" />
128+ <line x1="${ nextPoint . x } " y1="${ nextPoint . y } " x2="${ hands . x2 } " y2="${ hands . y2 } "
129+ stroke-width="1" stroke="black" stroke-dasharray="2" />
130+ <circle cx="${ hands . x1 } " cy="${ hands . y1 } " r="1"
131+ fill="black" />
132+ <circle cx="${ hands . x2 } " cy="${ hands . y2 } " r="1"
133+ fill="black" />
134+ <circle cx="${ x } " cy="${ y } " r="2" fill="black" />` ;
135+ }
136+ }
137+
138+ return ( `
139+ <svg
140+ width="${ opt . width } "
141+ height="${ opt . height } "
142+ viewBox="0 0 ${ opt . width } ${ opt . height } "
143+ xmlns="http://www.w3.org/2000/svg"
144+ >
145+ <g transform="${ opt . transform || "" } ">
81146 <path
82147 stroke="${ opt . stroke || "none" } "
83148 stroke-width="${ opt . strokeWidth || 0 } "
84149 fill="${ opt . fill || "none" } "
85150 d="${ path } "
86151 />
87- ${ ! opt . handles ? "" : points . map ( ( { x, y} , i ) => {
88- const color = i === 0 ? "red" : "grey" ;
89- const handle = handles [ i ] ;
90- const nextPoint = loop ( points ) ( i + 1 ) ;
91- return `
92- <g id="point-handle-${ i } ">
93- <line x1="${ x } " y1="${ y } " x2="${ handle . x1 } " y2="${ handle . y1 } " stroke-width="1" stroke="${ color } " />
94- <line x1="${ nextPoint . x } " y1="${ nextPoint . y } " x2="${ handle . x2 } " y2="${ handle . y2 } " stroke-width="1" stroke="${ color } " stroke-dasharray="2" />
95- <circle cx="${ handle . x1 } " cy="${ handle . y1 } " r="1" fill="${ color } " />
96- <circle cx="${ handle . x2 } " cy="${ handle . y2 } " r="1" fill="${ color } " />
97- <circle cx="${ x } " cy="${ y } " r="2" fill="${ color } " />
98- </g>
99- ` ;
100- } ) . join ( "" ) }
152+ ${ guides }
101153 </g>
102154 </svg>
103- ` ;
155+ ` ) . replace ( / \s + / g , " " ) ;
104156} ;
105157
106- console . log ( render ( [
107- { x : 200 , y : 200 , handles : { angle : - Math . PI * 7 / 4 , in : 60 , out : 80 } } ,
108- { x : - 200 , y : 200 , handles : { angle : Math . PI * 7 / 4 , in : 60 , out : 80 } } ,
109- { x : - 200 , y : - 200 , handles : { angle : Math . PI * 5 / 4 , in : 60 , out : 80 } } ,
110- { x : 200 , y : - 200 , handles : { angle : - Math . PI * 5 / 4 , in : 60 , out : 80 } } ,
158+ console . log ( renderClosed ( [
159+ { x : 700 , y : 200 , handles : { angle : - 135 , out : 80 , in : 80 } } ,
160+ { x : 300 , y : 200 , handles : { angle : 135 , out : 80 , in : 80 } } ,
161+ { x : 300 , y : 600 , handles : { angle : 45 , out : 80 , in : 80 } } ,
162+ { x : 700 , y : 600 , handles : { angle : - 45 , out : 80 , in : 80 } } ,
111163] , {
112- size : 1000 ,
113- center : true ,
114- handles : true ,
115- stroke : "green" ,
164+ width : 1000 ,
165+ height : 800 ,
166+ stroke : "blue" ,
116167 strokeWidth : 1 ,
168+ guides : true ,
117169} ) ) ;
0 commit comments