Build SVG paths easily using this GUI.
The main goal was to provide a quick way to get a path, without having to open tools like Adobe Illustrator. Made in 1000 lines using React v0.14.
A Pen by Anthony Dugois on CodePen.
| <div | |
| id="app" | |
| class="ad-App"> | |
| </div> |
| const Component = React.Component | |
| const render = ReactDOM.render | |
| class Container extends Component { | |
| state = { | |
| w: 800, | |
| h: 600, | |
| grid: { | |
| show: true, | |
| snap: true, | |
| size: 50 | |
| }, | |
| ctrl: false, | |
| points: [ | |
| { x: 100, y: 300 }, | |
| { x: 200, y: 300, q: { x: 150, y: 50 } }, | |
| { x: 300, y: 300, q: { x: 250, y: 550 } }, | |
| { x: 400, y: 300, q: { x: 350, y: 50 } }, | |
| { x: 500, y: 300, c: [{ x: 450, y: 550 }, { x: 450, y: 50 }] }, | |
| { x: 600, y: 300, c: [{ x: 550, y: 50 }, { x: 550, y: 550 }] }, | |
| { x: 700, y: 300, a: { rx: 50, ry: 50, rot: 0, laf: 1, sf: 1 } } | |
| ], | |
| activePoint: 2, | |
| draggedPoint: false, | |
| draggedQuadratic: false, | |
| draggedCubic: false, | |
| closePath: false | |
| }; | |
| componentWillMount() { | |
| document.addEventListener("keydown", this.handleKeyDown, false) | |
| document.addEventListener("keyup", this.handleKeyUp, false) | |
| } | |
| componentWillUnmount() { | |
| document.removeEventListener("keydown") | |
| document.removeEventListener("keyup") | |
| } | |
| positiveNumber(n) { | |
| n = parseInt(n) | |
| if (isNaN(n) || n < 0) n = 0 | |
| return n | |
| } | |
| setWidth = (e) => { | |
| let v = this.positiveNumber(e.target.value), min = 1 | |
| if (v < min) v = min | |
| this.setState({ w: v }) | |
| }; | |
| setHeight = (e) => { | |
| let v = this.positiveNumber(e.target.value), min = 1 | |
| if (v < min) v = min | |
| this.setState({ h: v }) | |
| }; | |
| setGridSize = (e) => { | |
| let grid = this.state.grid | |
| let v = this.positiveNumber(e.target.value) | |
| let min = 1 | |
| let max = Math.min(this.state.w, this.state.h) | |
| if (v < min) v = min | |
| if (v >= max) v = max / 2 | |
| grid.size = v | |
| this.setState({ grid }) | |
| }; | |
| setGridSnap = (e) => { | |
| let grid = this.state.grid | |
| grid.snap = e.target.checked | |
| this.setState({ grid }) | |
| }; | |
| setGridShow = (e) => { | |
| let grid = this.state.grid | |
| grid.show = e.target.checked | |
| this.setState({ grid }) | |
| }; | |
| setClosePath = (e) => { | |
| this.setState({ closePath: e.target.checked }) | |
| }; | |
| getMouseCoords = (e) => { | |
| const rect = ReactDOM.findDOMNode(this.refs.svg).getBoundingClientRect() | |
| let x = Math.round(e.pageX - rect.left) | |
| let y = Math.round(e.pageY - rect.top) | |
| if (this.state.grid.snap) { | |
| x = this.state.grid.size * Math.round(x / this.state.grid.size) | |
| y = this.state.grid.size * Math.round(y / this.state.grid.size) | |
| } | |
| return { x, y } | |
| }; | |
| setPointType = (e) => { | |
| const points = this.state.points | |
| const active = this.state.activePoint | |
| // not the first point | |
| if (active !== 0) { | |
| let v = e.target.value | |
| switch (v) { | |
| case "l": | |
| points[active] = { | |
| x: points[active].x, | |
| y: points[active].y | |
| } | |
| break | |
| case "q": | |
| points[active] = { | |
| x: points[active].x, | |
| y: points[active].y, | |
| q: { | |
| x: (points[active].x + points[active - 1].x) / 2, | |
| y: (points[active].y + points[active - 1].y) / 2 | |
| } | |
| } | |
| break | |
| case "c": | |
| points[active] = { | |
| x: points[active].x, | |
| y: points[active].y, | |
| c: [ | |
| { | |
| x: (points[active].x + points[active - 1].x - 50) / 2, | |
| y: (points[active].y + points[active - 1].y) / 2 | |
| }, | |
| { | |
| x: (points[active].x + points[active - 1].x + 50) / 2, | |
| y: (points[active].y + points[active - 1].y) / 2 | |
| } | |
| ] | |
| } | |
| break | |
| case "a": | |
| points[active] = { | |
| x: points[active].x, | |
| y: points[active].y, | |
| a: { | |
| rx: 50, | |
| ry: 50, | |
| rot: 0, | |
| laf: 1, | |
| sf: 1 | |
| } | |
| } | |
| break | |
| } | |
| this.setState({ points }) | |
| } | |
| }; | |
| setPointPosition = (coord, e) => { | |
| let coords = this.state.points[this.state.activePoint] | |
| let v = this.positiveNumber(e.target.value) | |
| if (coord === "x" && v > this.state.w) v = this.state.w | |
| if (coord === "y" && v > this.state.h) v = this.state.h | |
| coords[coord] = v | |
| this.setPointCoords(coords) | |
| }; | |
| setQuadraticPosition = (coord, e) => { | |
| let coords = this.state.points[this.state.activePoint].q | |
| let v = this.positiveNumber(e.target.value) | |
| if (coord === "x" && v > this.state.w) v = this.state.w | |
| if (coord === "y" && v > this.state.h) v = this.state.h | |
| coords[coord] = v | |
| this.setQuadraticCoords(coords) | |
| }; | |
| setCubicPosition = (coord, anchor, e) => { | |
| let coords = this.state.points[this.state.activePoint].c[anchor] | |
| let v = this.positiveNumber(e.target.value) | |
| if (coord === "x" && v > this.state.w) v = this.state.w | |
| if (coord === "y" && v > this.state.h) v = this.state.h | |
| coords[coord] = v | |
| this.setCubicCoords(coords, anchor) | |
| }; | |
| setPointCoords = (coords) => { | |
| const points = this.state.points | |
| const active = this.state.activePoint | |
| points[active].x = coords.x | |
| points[active].y = coords.y | |
| this.setState({ points }) | |
| }; | |
| setQuadraticCoords = (coords) => { | |
| const points = this.state.points | |
| const active = this.state.activePoint | |
| points[active].q.x = coords.x | |
| points[active].q.y = coords.y | |
| this.setState({ points }) | |
| }; | |
| setArcParam = (param, e) => { | |
| const points = this.state.points | |
| const active = this.state.activePoint | |
| let v | |
| if (["laf", "sf"].indexOf(param) > -1) { | |
| v = e.target.checked ? 1 : 0 | |
| } else { | |
| v = this.positiveNumber(e.target.value) | |
| } | |
| points[active].a[param] = v | |
| this.setState({ points }) | |
| }; | |
| setCubicCoords = (coords, anchor) => { | |
| const points = this.state.points | |
| const active = this.state.activePoint | |
| points[active].c[anchor].x = coords.x | |
| points[active].c[anchor].y = coords.y | |
| this.setState({ points }) | |
| }; | |
| setDraggedPoint = (index) => { | |
| if ( ! this.state.ctrl) { | |
| this.setState({ | |
| activePoint: index, | |
| draggedPoint: true | |
| }) | |
| } | |
| }; | |
| setDraggedQuadratic = (index) => { | |
| if ( ! this.state.ctrl) { | |
| this.setState({ | |
| activePoint: index, | |
| draggedQuadratic: true | |
| }) | |
| } | |
| }; | |
| setDraggedCubic = (index, anchor) => { | |
| if ( ! this.state.ctrl) { | |
| this.setState({ | |
| activePoint: index, | |
| draggedCubic: anchor | |
| }) | |
| } | |
| }; | |
| cancelDragging = (e) => { | |
| this.setState({ | |
| draggedPoint: false, | |
| draggedQuadratic: false, | |
| draggedCubic: false | |
| }) | |
| }; | |
| addPoint = (e) => { | |
| if (this.state.ctrl) { | |
| let coords = this.getMouseCoords(e) | |
| let points = this.state.points | |
| points.push(coords) | |
| this.setState({ | |
| points, | |
| activePoint: points.length - 1 | |
| }) | |
| } | |
| }; | |
| removeActivePoint = (e) => { | |
| let points = this.state.points | |
| let active = this.state.activePoint | |
| if (points.length > 1 && active !== 0) { | |
| points.splice(active, 1) | |
| this.setState({ | |
| points, | |
| activePoint: points.length - 1 | |
| }) | |
| } | |
| }; | |
| handleMouseMove = (e) => { | |
| if ( ! this.state.ctrl) { | |
| if (this.state.draggedPoint) { | |
| this.setPointCoords(this.getMouseCoords(e)) | |
| } else if (this.state.draggedQuadratic) { | |
| this.setQuadraticCoords(this.getMouseCoords(e)) | |
| } else if (this.state.draggedCubic !== false) { | |
| this.setCubicCoords(this.getMouseCoords(e), this.state.draggedCubic) | |
| } | |
| } | |
| }; | |
| handleKeyDown = (e) => { | |
| if (e.ctrlKey) this.setState({ ctrl: true }) | |
| }; | |
| handleKeyUp = (e) => { | |
| if ( ! e.ctrlKey) this.setState({ ctrl: false }) | |
| }; | |
| generatePath() { | |
| let { points, closePath } = this.state | |
| let d = "" | |
| points.forEach((p, i) => { | |
| if (i === 0) { | |
| // first point | |
| d += "M " | |
| } else if (p.q) { | |
| // quadratic | |
| d += `Q ${ p.q.x } ${ p.q.y } ` | |
| } else if (p.c) { | |
| // cubic | |
| d += `C ${ p.c[0].x } ${ p.c[0].y } ${ p.c[1].x } ${ p.c[1].y } ` | |
| } else if (p.a) { | |
| // arc | |
| d += `A ${ p.a.rx } ${ p.a.ry } ${ p.a.rot } ${ p.a.laf } ${ p.a.sf } ` | |
| } else { | |
| d += "L " | |
| } | |
| d += `${ p.x } ${ p.y } ` | |
| }) | |
| if (closePath) d += "Z" | |
| return d | |
| } | |
| reset = (e) => { | |
| let w = this.state.w, h = this.state.h | |
| this.setState({ | |
| points: [{ x: w / 2, y: h / 2 }], | |
| activePoint: 0 | |
| }) | |
| }; | |
| render() { | |
| return ( | |
| <div | |
| className="ad-Container" | |
| onMouseUp={ this.cancelDragging }> | |
| <div className="ad-Container-main"> | |
| <div className="ad-Container-svg"> | |
| <SVG | |
| ref="svg" | |
| path={ this.generatePath() } | |
| { ...this.state } | |
| addPoint={ this.addPoint } | |
| setDraggedPoint={ this.setDraggedPoint } | |
| setDraggedQuadratic={ this.setDraggedQuadratic } | |
| setDraggedCubic={ this.setDraggedCubic } | |
| handleMouseMove={ this.handleMouseMove } /> | |
| </div> | |
| <Foot /> | |
| </div> | |
| <div className="ad-Container-controls"> | |
| <Controls | |
| { ...this.state } | |
| reset={ this.reset } | |
| removeActivePoint={ this.removeActivePoint } | |
| setPointPosition={ this.setPointPosition } | |
| setQuadraticPosition={ this.setQuadraticPosition } | |
| setCubicPosition={ this.setCubicPosition } | |
| setArcParam={ this.setArcParam } | |
| setPointType={ this.setPointType } | |
| setWidth={ this.setWidth } | |
| setHeight={ this.setHeight } | |
| setGridSize={ this.setGridSize } | |
| setGridSnap={ this.setGridSnap } | |
| setGridShow={ this.setGridShow } | |
| setClosePath={ this.setClosePath } /> | |
| <Result path={ this.generatePath() } /> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| } | |
| function Foot(props) { | |
| return ( | |
| <div className="ad-Foot"> | |
| <ul className="ad-Foot-list"> | |
| <li className="ad-Foot-item"> | |
| <span className="ad-Foot-highlight">Click</span> to select a point | |
| </li> | |
| <li className="ad-Foot-item"> | |
| <span className="ad-Foot-highlight">Ctrl + Click</span> to add a point | |
| </li> | |
| </ul> | |
| <div className="ad-Foot-meta"> | |
| <a href="https://twitter.com/a_dugois">Follow me on Twitter</a><br /> | |
| or <a href="http://anthonydugois.com/svg-path-builder/">check the online version</a> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| function Result(props) { | |
| return ( | |
| <div className="ad-Result"> | |
| <textarea | |
| className="ad-Result-textarea" | |
| value={ props.path } | |
| onFocus={ (e) => e.target.select() } /> | |
| </div> | |
| ) | |
| } | |
| /** | |
| * SVG rendering | |
| */ | |
| class SVG extends Component { | |
| render() { | |
| const { | |
| path, | |
| w, | |
| h, | |
| grid, | |
| points, | |
| activePoint, | |
| addPoint, | |
| handleMouseMove, | |
| setDraggedPoint, | |
| setDraggedQuadratic, | |
| setDraggedCubic | |
| } = this.props | |
| let circles = points.map((p, i, a) => { | |
| let anchors = [] | |
| if (p.q) { | |
| anchors.push( | |
| <Quadratic | |
| index={ i } | |
| p1x={ a[i - 1].x } | |
| p1y={ a[i - 1].y } | |
| p2x={ p.x } | |
| p2y={ p.y } | |
| x={ p.q.x } | |
| y={ p.q.y } | |
| setDraggedQuadratic={ setDraggedQuadratic } /> | |
| ) | |
| } else if (p.c) { | |
| anchors.push( | |
| <Cubic | |
| index={ i } | |
| p1x={ a[i - 1].x } | |
| p1y={ a[i - 1].y } | |
| p2x={ p.x } | |
| p2y={ p.y } | |
| x1={ p.c[0].x } | |
| y1={ p.c[0].y } | |
| x2={ p.c[1].x } | |
| y2={ p.c[1].y } | |
| setDraggedCubic={ setDraggedCubic } /> | |
| ) | |
| } | |
| return ( | |
| <g className={ | |
| "ad-PointGroup" + | |
| (i === 0 ? " ad-PointGroup--first" : "") + | |
| (activePoint === i ? " is-active" : "") | |
| }> | |
| <Point | |
| index={ i } | |
| x={ p.x } | |
| y={ p.y } | |
| setDraggedPoint={ setDraggedPoint } /> | |
| { anchors } | |
| </g> | |
| ) | |
| }) | |
| return ( | |
| <svg | |
| className="ad-SVG" | |
| width={ w } | |
| height={ h } | |
| onClick={ (e) => addPoint(e) } | |
| onMouseMove={ (e) => handleMouseMove(e) }> | |
| <Grid | |
| w={ w } | |
| h={ h } | |
| grid={ grid } /> | |
| <path | |
| className="ad-Path" | |
| d={ path } /> | |
| <g className="ad-Points"> | |
| { circles } | |
| </g> | |
| </svg> | |
| ) | |
| } | |
| } | |
| function Cubic(props) { | |
| return ( | |
| <g className="ad-Anchor"> | |
| <line | |
| className="ad-Anchor-line" | |
| x1={ props.p1x } | |
| y1={ props.p1y } | |
| x2={ props.x1 } | |
| y2={ props.y1 } /> | |
| <line | |
| className="ad-Anchor-line" | |
| x1={ props.p2x } | |
| y1={ props.p2y } | |
| x2={ props.x2 } | |
| y2={ props.y2 } /> | |
| <circle | |
| className="ad-Anchor-point" | |
| onMouseDown={ (e) => props.setDraggedCubic(props.index, 0) } | |
| cx={ props.x1 } | |
| cy={ props.y1 } | |
| r={ 6 } /> | |
| <circle | |
| className="ad-Anchor-point" | |
| onMouseDown={ (e) => props.setDraggedCubic(props.index, 1) } | |
| cx={ props.x2 } | |
| cy={ props.y2 } | |
| r={ 6 } /> | |
| </g> | |
| ) | |
| } | |
| function Quadratic(props) { | |
| return ( | |
| <g className="ad-Anchor"> | |
| <line | |
| className="ad-Anchor-line" | |
| x1={ props.p1x } | |
| y1={ props.p1y } | |
| x2={ props.x } | |
| y2={ props.y } /> | |
| <line | |
| className="ad-Anchor-line" | |
| x1={ props.x } | |
| y1={ props.y } | |
| x2={ props.p2x } | |
| y2={ props.p2y } /> | |
| <circle | |
| className="ad-Anchor-point" | |
| onMouseDown={ (e) => props.setDraggedQuadratic(props.index) } | |
| cx={ props.x } | |
| cy={ props.y } | |
| r={ 6 } /> | |
| </g> | |
| ) | |
| } | |
| function Point(props) { | |
| return ( | |
| <circle | |
| className="ad-Point" | |
| onMouseDown={ (e) => props.setDraggedPoint(props.index) } | |
| cx={ props.x } | |
| cy={ props.y } | |
| r={ 8 } /> | |
| ) | |
| } | |
| function Grid(props) { | |
| const { show, snap, size } = props.grid | |
| let grid = [] | |
| for (let i = 1 ; i < (props.w / size) ; i++) { | |
| grid.push( | |
| <line | |
| x1={ i * size } | |
| y1={ 0 } | |
| x2={ i * size } | |
| y2={ props.h } /> | |
| ) | |
| } | |
| for (let i = 1 ; i < (props.h / size) ; i++) { | |
| grid.push( | |
| <line | |
| x1={ 0 } | |
| y1={ i * size } | |
| x2={ props.w } | |
| y2={ i * size } /> | |
| ) | |
| } | |
| return ( | |
| <g className={ | |
| "ad-Grid" + | |
| ( ! show ? " is-hidden" : "") | |
| }> | |
| { grid } | |
| </g> | |
| ) | |
| } | |
| /** | |
| * Controls | |
| */ | |
| function Controls(props) { | |
| const active = props.points[props.activePoint] | |
| const step = props.grid.snap ? props.grid.size : 1 | |
| let params = [] | |
| if (active.q) { | |
| params.push( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Control point X position" | |
| type="range" | |
| min={ 0 } | |
| max={ props.w } | |
| step={ step } | |
| value={ active.q.x } | |
| onChange={ (e) => props.setQuadraticPosition("x", e) } /> | |
| </div> | |
| ) | |
| params.push( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Control point Y position" | |
| type="range" | |
| min={ 0 } | |
| max={ props.h } | |
| step={ step } | |
| value={ active.q.y } | |
| onChange={ (e) => props.setQuadraticPosition("y", e) } /> | |
| </div> | |
| ) | |
| } else if (active.c) { | |
| params.push( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="First control point X position" | |
| type="range" | |
| min={ 0 } | |
| max={ props.w } | |
| step={ step } | |
| value={ active.c[0].x } | |
| onChange={ (e) => props.setCubicPosition("x", 0, e) } /> | |
| </div> | |
| ) | |
| params.push( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="First control point Y position" | |
| type="range" | |
| min={ 0 } | |
| max={ props.h } | |
| step={ step } | |
| value={ active.c[0].y } | |
| onChange={ (e) => props.setCubicPosition("y", 0, e) } /> | |
| </div> | |
| ) | |
| params.push( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Second control point X position" | |
| type="range" | |
| min={ 0 } | |
| max={ props.w } | |
| step={ step } | |
| value={ active.c[1].x } | |
| onChange={ (e) => props.setCubicPosition("x", 1, e) } /> | |
| </div> | |
| ) | |
| params.push( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Second control point Y position" | |
| type="range" | |
| min={ 0 } | |
| max={ props.h } | |
| step={ step } | |
| value={ active.c[1].y } | |
| onChange={ (e) => props.setCubicPosition("y", 1, e) } /> | |
| </div> | |
| ) | |
| } else if (active.a) { | |
| params.push( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="X Radius" | |
| type="range" | |
| min={ 0 } | |
| max={ props.w } | |
| step={ step } | |
| value={ active.a.rx } | |
| onChange={ (e) => props.setArcParam("rx", e) } /> | |
| </div> | |
| ) | |
| params.push( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Y Radius" | |
| type="range" | |
| min={ 0 } | |
| max={ props.h } | |
| step={ step } | |
| value={ active.a.ry } | |
| onChange={ (e) => props.setArcParam("ry", e) } /> | |
| </div> | |
| ) | |
| params.push( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Rotation" | |
| type="range" | |
| min={ 0 } | |
| max={ 360 } | |
| step={ 1 } | |
| value={ active.a.rot } | |
| onChange={ (e) => props.setArcParam("rot", e) } /> | |
| </div> | |
| ) | |
| params.push( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Large arc sweep flag" | |
| type="checkbox" | |
| checked={ active.a.laf } | |
| onChange={ (e) => props.setArcParam("laf", e) } /> | |
| </div> | |
| ) | |
| params.push( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Sweep flag" | |
| type="checkbox" | |
| checked={ active.a.sf } | |
| onChange={ (e) => props.setArcParam("sf", e) } /> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className="ad-Controls"> | |
| <h3 className="ad-Controls-title"> | |
| Parameters | |
| </h3> | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Width" | |
| type="text" | |
| value={ props.w } | |
| onChange={ (e) => props.setWidth(e) } /> | |
| <Control | |
| name="Height" | |
| type="text" | |
| value={ props.h } | |
| onChange={ (e) => props.setHeight(e) } /> | |
| <Control | |
| name="Close path" | |
| type="checkbox" | |
| value={ props.closePath } | |
| onChange={ (e) => props.setClosePath(e) } /> | |
| </div> | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Grid size" | |
| type="text" | |
| value={ props.grid.size } | |
| onChange={ (e) => props.setGridSize(e) } /> | |
| <Control | |
| name="Snap grid" | |
| type="checkbox" | |
| checked={ props.grid.snap } | |
| onChange={ (e) => props.setGridSnap(e) } /> | |
| <Control | |
| name="Show grid" | |
| type="checkbox" | |
| checked={ props.grid.show } | |
| onChange={ (e) => props.setGridShow(e) } /> | |
| </div> | |
| <div className="ad-Controls-container"> | |
| <Control | |
| type="button" | |
| action="reset" | |
| value="Reset path" | |
| onClick={ (e) => props.reset(e) } /> | |
| </div> | |
| <h3 className="ad-Controls-title"> | |
| Selected point | |
| </h3> | |
| { props.activePoint !== 0 && ( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Point type" | |
| type="choices" | |
| id="pointType" | |
| choices={[ | |
| { name: "L", value: "l", checked: (!active.q && !active.c && !active.a) }, | |
| { name: "Q", value: "q", checked: !!active.q }, | |
| { name: "C", value: "c", checked: !!active.c }, | |
| { name: "A", value: "a", checked: !!active.a } | |
| ]} | |
| onChange={ (e) => props.setPointType(e) } /> | |
| </div> | |
| )} | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Point X position" | |
| type="range" | |
| min={ 0 } | |
| max={ props.w } | |
| step={ step } | |
| value={ active.x } | |
| onChange={ (e) => props.setPointPosition("x", e) } /> | |
| </div> | |
| <div className="ad-Controls-container"> | |
| <Control | |
| name="Point Y position" | |
| type="range" | |
| min={ 0 } | |
| max={ props.h } | |
| step={ step } | |
| value={ active.y } | |
| onChange={ (e) => props.setPointPosition("y", e) } /> | |
| </div> | |
| { params } | |
| { props.activePoint !== 0 && ( | |
| <div className="ad-Controls-container"> | |
| <Control | |
| type="button" | |
| action="delete" | |
| value="Remove this point" | |
| onClick={ (e) => props.removeActivePoint(e) } /> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |
| function Control(props) { | |
| const { | |
| name, | |
| type, | |
| ..._props | |
| } = props | |
| let control = "", label = "" | |
| switch (type) { | |
| case "range": control = <Range { ..._props } /> | |
| break | |
| case "text": control = <Text { ..._props } /> | |
| break | |
| case "checkbox": control = <Checkbox { ..._props } /> | |
| break | |
| case "button": control = <Button { ..._props } /> | |
| break | |
| case "choices": control = <Choices { ..._props } /> | |
| break | |
| } | |
| if (name) { | |
| label = ( | |
| <label className="ad-Control-label"> | |
| { name } | |
| </label> | |
| ) | |
| } | |
| return ( | |
| <div className="ad-Control"> | |
| { label } | |
| { control } | |
| </div> | |
| ) | |
| } | |
| function Choices(props) { | |
| let choices = props.choices.map((c, i) => { | |
| return ( | |
| <label className="ad-Choice"> | |
| <input | |
| className="ad-Choice-input" | |
| type="radio" | |
| value={ c.value } | |
| checked={ c.checked } | |
| name={ props.id } | |
| onChange={ props.onChange } /> | |
| <div className="ad-Choice-fake"> | |
| { c.name } | |
| </div> | |
| </label> | |
| ) | |
| }) | |
| return ( | |
| <div className="ad-Choices"> | |
| { choices } | |
| </div> | |
| ) | |
| } | |
| function Button(props) { | |
| return ( | |
| <button | |
| className={ | |
| "ad-Button" + | |
| (props.action ? " ad-Button--" + props.action : "") | |
| } | |
| type="button" | |
| onClick={ props.onClick }> | |
| { props.value } | |
| </button> | |
| ) | |
| } | |
| function Checkbox(props) { | |
| return ( | |
| <label className="ad-Checkbox"> | |
| <input | |
| className="ad-Checkbox-input" | |
| type="checkbox" | |
| onChange={ props.onChange } | |
| checked={ props.checked } /> | |
| <div className="ad-Checkbox-fake" /> | |
| </label> | |
| ) | |
| } | |
| function Text(props) { | |
| return ( | |
| <input | |
| className="ad-Text" | |
| type="text" | |
| value={ props.value } | |
| onChange={ props.onChange } /> | |
| ) | |
| } | |
| function Range(props) { | |
| return ( | |
| <div className="ad-Range"> | |
| <input | |
| className="ad-Range-input" | |
| type="range" | |
| min={ props.min } | |
| max={ props.max } | |
| step={ props.step } | |
| value={ props.value } | |
| onChange={ props.onChange } /> | |
| <input | |
| className="ad-Range-text ad-Text" | |
| type="text" | |
| value={ props.value } | |
| onChange={ props.onChange } /> | |
| </div> | |
| ) | |
| } | |
| render( | |
| <Container />, | |
| document.querySelector("#app") | |
| ) |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.min.js"></script> |
| @use postcss-cssnext; | |
| @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,600); | |
| :root { | |
| --ad-Color-prim: #111; | |
| --ad-Color-sec: #00E676; | |
| --ad-Color-del: #E53935; | |
| } | |
| html { | |
| font-size: 16px; | |
| font-family: "Open Sans", sans-serif; | |
| } | |
| html, | |
| body { | |
| height: 100%; | |
| } | |
| .ad-App { | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| /* main layout */ | |
| .ad-Container { | |
| height: 100%; | |
| width: 100%; | |
| display: flex; | |
| background: #fff; | |
| } | |
| .ad-Container-main { | |
| height: 100%; | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .ad-Container-svg { | |
| height: 100%; | |
| flex: 1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: #f3f3f3; | |
| } | |
| .ad-Container-controls { | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| height: 100%; | |
| width: 20rem; | |
| background: var(--ad-Color-prim); | |
| } | |
| .ad-Container-controls ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .ad-Container-controls ::-webkit-scrollbar-thumb { | |
| border-radius: 30px; | |
| background: rgba(255, 255, 255, .3); | |
| } | |
| .ad-Foot { | |
| padding: 1.5rem 2rem; | |
| display: flex; | |
| background: #fff; | |
| border-top: 2px solid #eee; | |
| } | |
| .ad-Foot-list { | |
| flex: 1; | |
| } | |
| .ad-Foot-item { | |
| text-transform: uppercase; | |
| font-size: .7rem; | |
| color: var(--ad-Color-prim); | |
| } | |
| .ad-Foot-item + .ad-Foot-item { | |
| margin-top: .5rem; | |
| } | |
| .ad-Foot-highlight { | |
| padding-bottom: .04rem; | |
| border-bottom: 2px solid var(--ad-Color-sec); | |
| font-weight: bold; | |
| } | |
| .ad-Foot-meta { | |
| margin-left: 2rem; | |
| text-align: right; | |
| line-height: 1.4; | |
| font-size: .7rem; | |
| color: var(--ad-Color-prim); | |
| } | |
| .ad-Foot-meta a { | |
| text-decoration: underline; | |
| color: var(--ad-Color-prim); | |
| } | |
| .ad-SVG { | |
| display: block; | |
| background: #fff; | |
| border-radius: 4px; | |
| } | |
| .ad-Grid { | |
| fill: none; | |
| stroke: #eee; | |
| stroke-width: 1px; | |
| } | |
| .ad-Grid.is-hidden { | |
| opacity: 0; | |
| } | |
| .ad-Path { | |
| fill: none; | |
| stroke: #555; | |
| stroke-width: 4px; | |
| stroke-linecap: round; | |
| } | |
| .ad-Point { | |
| cursor: pointer; | |
| fill: #fff; | |
| stroke: #555; | |
| stroke-width: 5px; | |
| transition: fill .2s; | |
| } | |
| .ad-Point:hover, | |
| .ad-PointGroup.is-active .ad-Point { | |
| fill: var(--ad-Color-sec); | |
| } | |
| .ad-PointGroup--first .ad-Point { | |
| stroke: var(--ad-Color-sec); | |
| } | |
| .ad-Anchor { | |
| opacity: .5; | |
| } | |
| .ad-PointGroup.is-active .ad-Anchor { | |
| opacity: 1; | |
| } | |
| .ad-Anchor-point { | |
| cursor: pointer; | |
| fill: #fff; | |
| stroke: #888; | |
| stroke-width: 5px; | |
| } | |
| .ad-Anchor-line { | |
| stroke: #888; | |
| stroke-width: 1px; | |
| stroke-dasharray: 5 5; | |
| } | |
| /* controls on the right */ | |
| .ad-Controls { | |
| overflow: auto; | |
| flex: 1; | |
| padding: 2rem; | |
| } | |
| .ad-Controls :first-child { | |
| margin-top: 0; | |
| } | |
| .ad-Controls-title { | |
| margin: 3rem 0 1.5rem; | |
| font-size: .8rem; | |
| font-weight: bold; | |
| color: #fff; | |
| } | |
| .ad-Controls-container { | |
| display: flex; | |
| } | |
| .ad-Controls-container + .ad-Controls-container { | |
| margin-top: 1.5rem; | |
| } | |
| .ad-Control { | |
| flex: 1; | |
| } | |
| .ad-Control-label { | |
| display: block; | |
| margin-bottom: .5rem; | |
| text-transform: uppercase; | |
| font-size: .6rem; | |
| font-weight: bold; | |
| color: color(var(--ad-Color-prim) l(+75%)); | |
| } | |
| .ad-Result { | |
| height: 5rem; | |
| padding: 1.4rem 1.6rem; | |
| background: var(--ad-Color-prim); | |
| box-shadow: 0 -5px 10px rgba(0, 0, 0, .4); | |
| } | |
| .ad-Result-textarea { | |
| height: 100%; | |
| width: 100%; | |
| resize: none; | |
| border: none; | |
| background: none; | |
| text-transform: uppercase; | |
| font-family: "Open Sans", sans-serif; | |
| font-size: .7rem; | |
| font-weight: bold; | |
| line-height: 1.8; | |
| color: #fff; | |
| } | |
| .ad-Result-textarea:focus { | |
| outline: 0; | |
| } | |
| /* control elements */ | |
| .ad-Button { | |
| padding: .8rem 1.4rem; | |
| background: var(--ad-Color-sec); | |
| border: none; | |
| border-radius: 50px; | |
| cursor: pointer; | |
| transition: background .2s; | |
| text-transform: uppercase; | |
| font-family: "Open Sans", sans-serif; | |
| font-weight: bold; | |
| font-size: .6rem; | |
| letter-spacing: .08rem; | |
| color: #fff; | |
| } | |
| .ad-Button:focus, | |
| .ad-Button:hover { | |
| outline: 0; | |
| background: color(var(--ad-Color-sec) l(+5%)); | |
| } | |
| .ad-Button--delete { | |
| background: var(--ad-Color-del); | |
| } | |
| .ad-Button--delete:focus, | |
| .ad-Button--delete:hover { | |
| background: color(var(--ad-Color-del) l(+5%)); | |
| } | |
| .ad-Button--reset { | |
| background: color(var(--ad-Color-prim) l(+10%)); | |
| } | |
| .ad-Button--reset:focus, | |
| .ad-Button--reset:hover { | |
| background: color(var(--ad-Color-prim) l(+15%)); | |
| } | |
| .ad-Text { | |
| height: 18px; | |
| width: 2rem; | |
| background: color(var(--ad-Color-prim) l(+10%)); | |
| border: none; | |
| border-radius: 4px; | |
| text-align: center; | |
| font-family: "Open Sans", sans-serif; | |
| font-size: .6rem; | |
| color: #fff; | |
| } | |
| .ad-Text:focus { | |
| outline: 0; | |
| background: color(var(--ad-Color-prim) l(+20%)); | |
| } | |
| .ad-Checkbox-input { | |
| display: none; | |
| } | |
| .ad-Checkbox-fake { | |
| position: relative; | |
| height: 14px; | |
| width: 2rem; | |
| background: color(var(--ad-Color-prim) l(+10%)); | |
| border-radius: 30px; | |
| } | |
| .ad-Checkbox-fake::after { | |
| content: ""; | |
| box-sizing: border-box; | |
| display: block; | |
| position: absolute; | |
| top: -2px; | |
| left: 0; | |
| height: 18px; | |
| width: 18px; | |
| cursor: pointer; | |
| border: 4px solid #fff; | |
| background: color(var(--ad-Color-prim) l(+10%)); | |
| border-radius: 50%; | |
| } | |
| .ad-Checkbox-input:checked + .ad-Checkbox-fake::after { | |
| left: auto; | |
| right: 0; | |
| border-color: var(--ad-Color-sec); | |
| } | |
| .ad-Choices { | |
| display: flex; | |
| width: 12rem; | |
| } | |
| .ad-Choice { | |
| flex: 1; | |
| } | |
| .ad-Choice-input { | |
| display: none; | |
| } | |
| .ad-Choice-fake { | |
| padding: .6rem; | |
| background: color(var(--ad-Color-prim) l(+10%)); | |
| border: 4px solid transparent; | |
| transition: border .2s; | |
| cursor: pointer; | |
| text-align: center; | |
| text-transform: uppercase; | |
| font-family: "Open Sans", sans-serif; | |
| font-size: .8rem; | |
| font-weight: bold; | |
| color: #fff; | |
| } | |
| .ad-Choice:first-child .ad-Choice-fake { | |
| border-radius: 4px 0 0 4px; | |
| } | |
| .ad-Choice:last-child .ad-Choice-fake { | |
| border-radius: 0 4px 4px 0; | |
| } | |
| .ad-Choice-input:checked + .ad-Choice-fake { | |
| border-color: var(--ad-Color-sec); | |
| } | |
| .ad-Range { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .ad-Range-text { | |
| margin-left: .5rem; | |
| } | |
| .ad-Range-input { | |
| width: 100%; | |
| height: 14px; | |
| appearance: none; | |
| border-radius: 30px; | |
| background: color(var(--ad-Color-prim) l(+10%)); | |
| } | |
| .ad-Range-input:focus { | |
| outline: 0; | |
| background: color(var(--ad-Color-prim) l(+20%)); | |
| } | |
| /* thumb */ | |
| .ad-Range-input::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| border: 4px solid #fff; | |
| background: color(var(--ad-Color-prim) l(+10%)); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: border .2s; | |
| } | |
| .ad-Range-input::-moz-range-thumb { | |
| -webkit-appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| border: 4px solid #fff; | |
| background: color(var(--ad-Color-prim) l(+10%)); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: border .2s; | |
| } | |
| .ad-Range-input:hover::-webkit-slider-thumb, | |
| .ad-Range-input:focus::-webkit-slider-thumb { | |
| border-color: var(--ad-Color-sec); | |
| } | |
| .ad-Range-input:hover::-moz-range-thumb, | |
| .ad-Range-input:focus::-moz-range-thumb { | |
| border-color: var(--ad-Color-sec); | |
| } |
Build SVG paths easily using this GUI.
The main goal was to provide a quick way to get a path, without having to open tools like Adobe Illustrator. Made in 1000 lines using React v0.14.
A Pen by Anthony Dugois on CodePen.