I’ve been working on a back design for playing cards, and decided to make a tool to draw ornate spiral figures. To make things frustrating challenging, I’m using Reason / React to generate SVG.
I’m still learning how to organize React components in Reason React. I haven’t been able to figure out how to split up the sliders into a subcomponent yet, so the code quality is a bit disappointing.
I noticed some strange things along the way. The <input type="range">
slider has some interesting types, with min
being typed as an int
, and max
being typed as a string
.
I struggled with building an SVG path which wants an array of points as a long string. A method to join an array of strings isn’t present in the standard (non-Javascript) library. Initially, I converted the array(point)
to a list and ran List.fold_left to flatten it down to a string. But in the end, I used Js.Array.joinWith(" ")
which is probably faster.
SpirographGenerator.res
type action =
| SetAngleA ( string )
| SetAngleB ( string )
| SetAngleC ( string )
| SetAngleD ( string )
| SetScaleA ( string )
| SetScaleB ( string )
| SetScaleC ( string )
| SetScaleD ( string )
| SetOffset ( string )
| SetRepeatCount ( string )
| SetRepeatOffset ( string )
| SetThickness ( string )
| SetScale ( string )
;
type state = {
angleA : int ,
angleB : int ,
angleC : int ,
angleD : int ,
scaleA : int ,
scaleB : int ,
scaleC : int ,
scaleD : int ,
offset : int ,
repeatCount : int ,
repeatOffset : int ,
thickness : int ,
scale : int ,
};
type point = {
x : float ,
y : float
};
module SvgPath = {
@ react . component
let make = ( ~ state ) => {
let moveto = ( p ) => Js . Float . toString ( p . x ) ++ "," ++ Js . Float . toString ( p . y );
let points = ( step ) => {
let count = 360 * 8 ;
let offset = float_of_int ( state . offset ) /. 500 . 0 *. float_of_int ( step );
let repeatOffset = float_of_int ( state . repeatOffset ) /. 50 . 0 *. float_of_int ( step );
let angleA = float_of_int ( state . angleA );
let angleB = float_of_int ( state . angleB );
let angleC = float_of_int ( state . angleC );
let angleD = float_of_int ( state . angleD );
let scaleA = float_of_int ( state . scaleA );
let scaleB = float_of_int ( state . scaleB );
let scaleC = float_of_int ( state . scaleC );
let scaleD = float_of_int ( state . scaleD );
let scale = float_of_int ( state . scale ) /. 100 . 0 ;
let points = [{ x : 0 . 0 , y : 0 . 0 }]
let radian_conv = 0 . 01745329251 ;
for ( i in 0 to count ) {
let degrees = float_of_int ( i ) /. float_of_int ( count ) *. 360 . 0 ;
let x = sin ( degrees *. radian_conv *. angleA ) *. scaleA ;
let y = cos ( degrees *. radian_conv *. angleA ) *. scaleA ;
let xx = x +. sin ( degrees *. radian_conv *. angleB +. offset ) *. scaleB ;
let yy = y +. cos ( degrees *. radian_conv *. angleB +. offset ) *. scaleB ;
let xxx = xx +. sin ( degrees *. radian_conv *. angleC ) *. ( scaleC );
let yyy = yy +. cos ( degrees *. radian_conv *. angleC ) *. ( scaleC );
let xxxx = xxx +. sin ( degrees *. radian_conv *. angleD ) *. ( scaleD +. repeatOffset );
let yyyy = yyy +. cos ( degrees *. radian_conv *. angleD ) *. ( scaleD +. repeatOffset );
points [ i ] = {
x : xxxx *. scale ,
y : yyyy *. scale
}
}
let joined = Array . map ( points , ( p ) => moveto ( p ))
|> Js . Array . joinWith ( " " );
"M" ++ joined ++ "z"
};
let count = state . repeatCount ;
let repeatCounter = [] ;
for ( i in 0 to count ) {
repeatCounter [ i ] = i ;
};
( React . array (
Array . map ( repeatCounter , step => {
let thickness = state . thickness ;
let lineThickness = ( thickness > 0 ) ? Js . Float . toString ( float_of_int ( step ) *. float_of_int ( state . thickness ) /. float_of_int ( count )) : "1.0" ;
< path
transform = { "translate(" ++ string_of_int ( 640 / 2 ) ++ " " ++ string_of_int ( 640 / 2 ) ++ ")" }
d = points ( step )
stroke = "black"
strokeWidth = lineThickness
fill = "none"
/>
}
)
))
}
}
module SvgRender = {
@ react . component
let make = ( ~ state ) => {
< svg width = "640" height = "640" >
< rect
width = "640"
height = "640"
style = ReactDOM . Style . make (
~ fill = "#fff" ,
~ border = "1px solid #ccc" ,
~ padding = "20px" , () )
/>
< SvgPath state />
</ svg >
}
}
@ react . component
let make = () => {
let ( state , dispatch ) =
React . useReducer (
( state , action ) =>
switch ( action ) {
| SetAngleA ( n ) => { ... state , angleA : int_of_string ( n )}
| SetAngleB ( n ) => { ... state , angleB : int_of_string ( n )}
| SetAngleC ( n ) => { ... state , angleC : int_of_string ( n )}
| SetAngleD ( n ) => { ... state , angleD : int_of_string ( n )}
| SetScaleA ( n ) => { ... state , scaleA : int_of_string ( n )}
| SetScaleB ( n ) => { ... state , scaleB : int_of_string ( n )}
| SetScaleC ( n ) => { ... state , scaleC : int_of_string ( n )}
| SetScaleD ( n ) => { ... state , scaleD : int_of_string ( n )}
| SetOffset ( n ) => { ... state , offset : int_of_string ( n )}
| SetRepeatOffset ( n ) => { ... state , repeatOffset : int_of_string ( n )}
| SetRepeatCount ( n ) => { ... state , repeatCount : int_of_string ( n )}
| SetThickness ( n ) => { ... state , thickness : int_of_string ( n )}
| SetScale ( n ) => { ... state , scale : int_of_string ( n )}
} ,
{
angleA : - 8 ,
angleB : 17 ,
angleC : 7 ,
angleD : 0 ,
scaleA : 82 ,
scaleB : 41 ,
scaleC : 144 ,
scaleD : 0 ,
offset : 0 ,
repeatOffset : 45 ,
repeatCount : 5 ,
thickness : 1 ,
scale : 100 ,
} ,
);
< div >
< form style = ReactDOM . Style . make ( ~ position = "absolute" , ~ left = "20px" , ~ width = "160px" , ~ margin = "10px" , () ) >
< div >
< label htmlFor = "angleA" > { React . string ( "Angle A" )}
{ React . string ( ": " ++ string_of_int ( state . angleA ))} </ label >< br />
< input
id = "angleA"
type_ = "range"
value = string_of_int ( state . angleA )
name = "angleA"
min = "-72"
max = "72"
onChange = (
event => dispatch ( SetAngleA ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "angleB" > { React . string ( "Angle B" )}
{ React . string ( ": " ++ string_of_int ( state . angleB ))} </ label >< br />
< input
id = "angleB"
type_ = "range"
value = string_of_int ( state . angleB )
name = "angleB"
min = "-72"
max = "72"
onChange = (
event => dispatch ( SetAngleB ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "angleC" > { React . string ( "Angle C" )}
{ React . string ( ": " ++ string_of_int ( state . angleC ))} </ label >< br />
< input
id = "angleC"
type_ = "range"
value = string_of_int ( state . angleC )
name = "angleC"
min = "-72"
max = "72"
onChange = (
event => dispatch ( SetAngleC ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "angleD" > { React . string ( "Angle D" )}
{ React . string ( ": " ++ string_of_int ( state . angleD ))} </ label >< br />
< input
id = "angleD"
type_ = "range"
value = string_of_int ( state . angleD )
name = "angleD"
min = "-72"
max = "72"
onChange = (
event => dispatch ( SetAngleD ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "scaleA" > { React . string ( "Scale A" )}
{ React . string ( ": " ++ string_of_int ( state . scaleA ))} </ label >< br />
< input
id = "scaleA"
type_ = "range"
value = string_of_int ( state . scaleA )
name = "scaleA"
min = "-360"
max = "360"
onChange = (
event => dispatch ( SetScaleA ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "scaleB" > { React . string ( "Scale B" )}
{ React . string ( ": " ++ string_of_int ( state . scaleB ))} </ label >< br />
< input
id = "scaleB"
type_ = "range"
value = string_of_int ( state . scaleB )
name = "scaleB"
min = "-360"
max = "360"
onChange = (
event => dispatch ( SetScaleB ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "scaleC" > { React . string ( "Scale C" )}
{ React . string ( ": " ++ string_of_int ( state . scaleC ))} </ label >< br />
< input
id = "scaleC"
type_ = "range"
value = string_of_int ( state . scaleC )
name = "scaleC"
min = "-360"
max = "360"
onChange = (
event => dispatch ( SetScaleC ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "scaleD" > { React . string ( "Scale D" )}
{ React . string ( ": " ++ string_of_int ( state . scaleD ))} </ label >< br />
< input
id = "scaleD"
type_ = "range"
value = string_of_int ( state . scaleD )
name = "scaleD"
min = "-360"
max = "360"
onChange = (
event => dispatch ( SetScaleD ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "offset" > { React . string ( "Offset" )} { React . string ( ": " ++ string_of_int ( state . offset ))} </ label >< br />
< input
id = "offset"
type_ = "range"
value = string_of_int ( state . offset )
name = "offset"
min = "-360"
max = "360"
onChange = (
event => dispatch ( SetOffset ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "offset" > { React . string ( "Repeat Offset" )} { React . string ( ": " ++ string_of_int ( state . repeatOffset ))} </ label >< br />
< input
id = "repeatOffset"
type_ = "range"
value = string_of_int ( state . repeatOffset )
name = "repeatOffset"
min = "-720"
max = "720"
onChange = (
event => dispatch ( SetRepeatOffset ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "offset" > { React . string ( "Repeat Count" )} { React . string ( ": " ++ string_of_int ( state . repeatCount ))} </ label >< br />
< input
id = "repeatCount"
type_ = "range"
value = string_of_int ( state . repeatCount )
name = "repeatCount"
min = "0"
max = "50"
onChange = (
event => dispatch ( SetRepeatCount ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "thickness" > { React . string ( "Line Thickness" )} { React . string ( ": " ++ string_of_int ( state . thickness ))} </ label >< br />
< input
id = "thickness"
type_ = "range"
value = string_of_int ( state . thickness )
name = "thickness"
min = "0"
max = "10"
onChange = (
event => dispatch ( SetThickness ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
< div >
< label htmlFor = "scale" > { React . string ( "Scale" )} { React . string ( ": " ++ string_of_int ( state . scale ))} </ label >< br />
< input
id = "scale"
type_ = "range"
value = string_of_int ( state . scale )
name = "scale"
min = "0"
max = "400"
onChange = (
event => dispatch ( SetScale ( ReactEvent . Form . target ( event )[ "value" ]))
)
style = ReactDOM . Style . make ( ~ width = "100%" , () )
/>
</ div >
</ form >
< SvgRender state />
</ div >;
};