|
| 1 | +function App () { |
| 2 | + const [state, dispatch] = React.useReducer(reducer, initState()); |
| 3 | + |
| 4 | + const svg = React.useRef(null); |
| 5 | + const [pt, setPT] = React.useState(undefined); |
| 6 | + React.useEffect(()=> { setPT(svg.current.createSVGPoint()) }, []); |
| 7 | + |
| 8 | + const [viewbox, setViewbox] = React.useState({ Xmin:0, Ymin:0, width:300, height:300}); |
| 9 | + const [crosshairs, setCrosshairs] = React.useState({x:0, y:0}); |
| 10 | + const [rawSVG, setRawSVG] = React.useState(`<rect width='100%' height='100%' fill='#eee' /> |
| 11 | +<g stroke='#333333'> |
| 12 | + <line x1='50%' x2='50%' y1='0' y2='100%' /> |
| 13 | + <line x1='0' x2='100%' y1='50%' y2='50%' /> |
| 14 | +</g> `); |
| 15 | + |
| 16 | + const [mode, setMode] = React.useState(null); |
| 17 | + const [segments, setSegments] = React.useState([]); |
| 18 | + const [activePoint, setActivePoint] = React.useState(null); |
| 19 | + const [pointR, setPointR] = React.useState(3); |
| 20 | + const [stroke, setStroke] = React.useState('#333333'); |
| 21 | + const [fill, setFill] = React.useState('none'); |
| 22 | + const [strokewidth, setStrokewidth] = React.useState(1); |
| 23 | + const [opacity, setOpacity] = React.useState(1); |
| 24 | + const [linecap, setLinecap] = React.useState('round'); //butt, round or square |
| 25 | + const [linejoin, setLinejoin] = React.useState('round'); //arcs | bevel |miter | miter-clip | round |
| 26 | + |
| 27 | + const Mode = ({mode}) => <button onClick={()=>setMode(mode)}>{mode}</button>; |
| 28 | + |
| 29 | + const Point = ({p,indx,ptKey}) => { |
| 30 | + return ( |
| 31 | + <circle cx={p.x} cy={p.y} r={pointR} stroke='red' strokeWidth={1} fill={ptKey.startsWith('cp') ? '#fff' : 'red'} |
| 32 | + onMouseDown={()=>setActivePoint(indx)} |
| 33 | + onContextMenu={e=> { |
| 34 | + e.preventDefault(); |
| 35 | + // only remove last segment |
| 36 | + setActivePoint(null); // necessary? |
| 37 | + if (state.points[indx].segment === (state.segments.length-1)) |
| 38 | + dispatch({mode:'eraseLastSegment', segment:state.points[indx].segment}); |
| 39 | + }} |
| 40 | + /> |
| 41 | + ) |
| 42 | + } |
| 43 | + |
| 44 | + const Crosshairs = ({x,y}) => { |
| 45 | + return ( |
| 46 | + <g id='crosshairs'> |
| 47 | + <line x1={viewbox.Xmin} x2={viewbox.width} y1={y} y2={y} /> |
| 48 | + <line x1={x} x2={x} y1={viewbox.Ymin} y2={viewbox.height} /> |
| 49 | + </g> |
| 50 | + ) |
| 51 | + } |
| 52 | + |
| 53 | + return ( |
| 54 | + <div id='app'> |
| 55 | + |
| 56 | + <svg ref={svg} |
| 57 | + viewBox={`${viewbox.Xmin} ${viewbox.Ymin} ${viewbox.width} ${viewbox.height}`} |
| 58 | + onMouseMove={e=>{ |
| 59 | + if (pt !== undefined) { |
| 60 | + [pt.x, pt.y] = [e.clientX, e.clientY]; |
| 61 | + let svgP = pt.matrixTransform(svg.current.getScreenCTM().inverse()); |
| 62 | + let [x,y] = [~~svgP.x, ~~svgP.y]; |
| 63 | + setCrosshairs({x:~~svgP.x, y:~~svgP.y}); |
| 64 | + if (activePoint!==null) |
| 65 | + dispatch({mode:'changePoint', indx:activePoint, x:x, y:y}); |
| 66 | + } |
| 67 | + }} |
| 68 | + |
| 69 | + onMouseUp={()=>setActivePoint(null)} |
| 70 | + |
| 71 | + onClick={()=>{dispatch({mode:mode, x:crosshairs.x, y:crosshairs.y})}}> |
| 72 | + |
| 73 | + <g dangerouslySetInnerHTML={{__html: rawSVG}}></g> |
| 74 | + |
| 75 | + <path stroke={stroke} fill={fill} strokeWidth={strokewidth} opacity={opacity} strokeLinecap={linecap} strokeLinejoin={linejoin} d={state.pathD} /> |
| 76 | + |
| 77 | + <Crosshairs viewbox={viewbox} x={crosshairs.x} y={crosshairs.y} /> |
| 78 | + |
| 79 | + {state.points.map( (p,indx) => <Point key={'point'+indx} p={p} indx={indx} ptKey={p.ptKey} /> )} |
| 80 | + |
| 81 | + </svg> |
| 82 | + |
| 83 | + |
| 84 | + |
| 85 | + |
| 86 | + <div id='controlsDIV'> |
| 87 | + <ViewboxInputs viewbox={viewbox} setViewbox={setViewbox} /> |
| 88 | + |
| 89 | + <textarea onChange={e=> setRawSVG(e.target.value)} value={rawSVG} /> |
| 90 | + |
| 91 | + <fieldset id='modeButtons'> |
| 92 | + <legend>Path Commands</legend> |
| 93 | + <Mode mode='M' /> <Mode mode='H' /> <Mode mode='V' /> <Mode mode='L' /> |
| 94 | + <Mode mode='Q' /> <Mode mode='C' /> |
| 95 | + |
| 96 | + {state.closed === '' ? |
| 97 | + <button onClick={()=>{dispatch({mode:'Close'})}}>Z</button> : |
| 98 | + <button style={{backgroundColor:'#ff0'}} onClick={()=>{dispatch({mode:'Open'})}}>Z</button> |
| 99 | + } |
| 100 | + |
| 101 | + <button onClick={()=>{dispatch({mode:'Clear'})}}>Clear</button> |
| 102 | + </fieldset> |
| 103 | + |
| 104 | + <fieldset id='pathAttributesFields'> |
| 105 | + <legend>Path Attributes</legend> |
| 106 | + |
| 107 | + <div id='pathAttributesDIV'> |
| 108 | + |
| 109 | + <div> |
| 110 | + <label htmlFor='widthSlider'>Width</label> |
| 111 | + <input id='widthSlider' type="range" value={strokewidth} onChange={e=>setStrokewidth(e.target.value)} /> |
| 112 | + </div> |
| 113 | + |
| 114 | + <div> |
| 115 | + <label htmlFor='opacitySlider'>Opacity</label> |
| 116 | + <input id='opacitySlider' type="range" min={0.0} max={1.0} step={.01} value={opacity} onChange={e=>setOpacity(e.target.value)} /> |
| 117 | + </div> |
| 118 | + |
| 119 | + <div> |
| 120 | + <label htmlFor='strokeColor'>Stroke Color</label> |
| 121 | + <input id='strokeColor' type="color" value={stroke} onChange={e=>setStroke(e.target.value)} /> |
| 122 | + </div> |
| 123 | + |
| 124 | + <div> |
| 125 | + <label htmlFor='fillColor'>Fill Color</label> |
| 126 | + <input id='fillColor' type="color" value={fill} onChange={e=>setFill(e.target.value)} /> |
| 127 | + <span onClick={()=>setFill('none')}>X</span> |
| 128 | + </div> |
| 129 | + |
| 130 | + <div> |
| 131 | + <label htmlFor='linecapMenu'>Linecap</label> |
| 132 | + <select name="linecapMenu" value={linecap} onChange={e=>setLinecap(e.target.value)}> |
| 133 | + <option value="butt">butt</option> |
| 134 | + <option value="round">round</option> |
| 135 | + <option value="square">square</option> |
| 136 | + </select> |
| 137 | + </div> |
| 138 | + |
| 139 | + <div> |
| 140 | + <label htmlFor='linejoinMenu'>Linejoin</label> |
| 141 | + <select name="linejoinMenu" value={linejoin} onChange={e=>setLinejoin(e.target.value)}> |
| 142 | + <option value="arcs">arcs</option> |
| 143 | + <option value="bevel">bevel</option> |
| 144 | + <option value="miter">miter</option> |
| 145 | + <option value="miter-clip">miter-clip</option> |
| 146 | + <option value="round">round</option> |
| 147 | + </select> |
| 148 | + </div> |
| 149 | + |
| 150 | + </div> |
| 151 | + </fieldset> |
| 152 | + |
| 153 | + <div> |
| 154 | + <label htmlFor='pointSlider'>Point Radius</label> |
| 155 | + <input id='pointSlider' type="range" value={pointR} onChange={e=>setPointR(e.target.value)} /> |
| 156 | + </div> |
| 157 | + |
| 158 | + <pre>{`<path stroke="${stroke}" stroke-width="${strokewidth}" fill="${fill}" opacity="${opacity}" stroke-linecap="${linecap}" stroke-linejoin="${linejoin}" d="${state.pathD}" />`}</pre> |
| 159 | + |
| 160 | + </div> |
| 161 | + |
| 162 | + </div> |
| 163 | + ) |
| 164 | +} |
| 165 | + |
| 166 | +ReactDOM.render(<App />, main); |
0 commit comments