Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div
     id="app"
     class="ad-App">
</div>

              
            
!

CSS

              
                @use postcss-cssnext;
@import url(https://fonts.googleapis.com/css?family=Open+Sans:400,600);

:root { --ad-Color-sec: #0da;
        --ad-Color-hov: linear-gradient(to top, #044, #088, #044);
        --ad-Color-del: linear-gradient(to top, #222, #666, #222);
        --ad-Color-prim: #111;
        --ad-Color-foot: #122;
}

html {
    font-size: 16px;
    font-family: "Open Sans", sans-serif;
}

html,
body {
    height: 100%;
}
.ad-no {text-transform: none;}
.ad-App {
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

/* основной макет */
.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;
            background-image: linear-gradient(to top, #ddd, #f3f3f3, #ddd);
        }
    
    .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-foot);
        }
        .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;
        }

/* управление справа */
.ad-Controls {
    overflow: auto;
    flex: 1;
    padding: 2rem;
}
    .ad-Controls :first-child {
        margin-top: 0;
    }
    .ad-Controls-title {
        margin: 1rem 0;
        font-size: .8rem;
        font-weight: bold;
        color: #fff;
    }
        .ad-Controls-container {
            display: flex;
        }
        .ad-Controls-container + .ad-Controls-container {
            margin-top: 1rem;
        }

.ad-Control {
    flex: 1;
}
    .ad-Control-label {
        display: block;
        margin-bottom: .5rem;
        /*text-transform: uppercase;*/
        font-size: .7rem;
        font-weight: bold;
        color: color(var(--ad-Color-prim) l(+75%));
    }

.ad-Result {
    height: 5rem;
    padding: 1rem;
    background: var(--ad-Color-foot);
    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;
    }

/* элементы управления */
.ad-Button {
    padding: .8rem 1rem;
    background: var(--ad-Color-del);
    border: none;
    border-radius: 25px;
    cursor: pointer;
    transition: background .2s;
    /*text-transform: uppercase;*/
    font-family: "Open Sans", sans-serif;
    font-weight: bold;
    font-size: 1rem;
    letter-spacing: .08rem;
    color: #fff;
}
.ad-Button:focus,
.ad-Button:hover {
    outline: 0;
    background: var(--ad-Color-hov);
}
.ad-Button--delete {
    background: var(--ad-Color-del);
}
.ad-Button--delete:focus,
.ad-Button--delete:hover {
    background: color(var(--ad-Color-hov) l(+2%));
}
.ad-Button--reset {
    background: var(--ad-Color-del);
}
.ad-Button--reset:focus,
.ad-Button--reset:hover {
    background: color(var(--ad-Color-hov) l(+2%));
}

.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);
    }
              
            
!

JS

              
                const Component = React.Component
const render = ReactDOM.render

class Container extends Component {
    state = {
        w: 800,
        h: 600,
        grid: {
            show: true,
            snap: true,
            size: 25
        },
        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
        
        // не первая точка
        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">Клик</span><span className="ad-no"> - выбрать точку</span>
                </li>
                <li className="ad-Foot-item">
                    <span className="ad-Foot-highlight">Ctrl + клик</span><span className="ad-no"> - добавить точку</span>
                </li>
            </ul>
            <div className="ad-Foot-meta">
                <a href="https://twitter.com/a_dugois">Подпишись на меня в Твиттере</a><br />
                или <a href="http://anthonydugois.com/svg-path-builder/">проверь онлайн-версию</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-рендеринг
 */

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>
    )
}

/**
 * Контрольные точки
 */

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="X позиция флага"
                    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="Y позиция флага"
                    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="X позиция 1 флага"
                    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="Y позиция 1 флага"
                    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="X позиция 2 флага"
                    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="Y позиция 2 флага"
                    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 Радиус"
                    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 Радиус"
                    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="Вращение"
                    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 
                    
                    type="checkbox" name="Выбор хорды"
                    checked={ active.a.laf }
                    onChange={ (e) => props.setArcParam("laf", e) } />
            </div>
        )
        params.push(
            <div className="ad-Controls-container">
                <Control 
                    name="Инверсия"
                    type="checkbox"
                    checked={ active.a.sf }
                    onChange={ (e) => props.setArcParam("sf", e) } />
            </div>
        )
    }
        
    return (
        <div className="ad-Controls">
            <h3 className="ad-Controls-title">
                Параметры холста и сетки
            </h3>
                <div className="ad-Controls-container">
                <Control className="ad-no"
                    name="Сетка"
                    type="checkbox"
                    checked={ props.grid.show }
                    onChange={ (e) => props.setGridShow(e) } />
                <Control className="ad-no"
                    name="Привязка"
                    type="checkbox"
                    checked={ props.grid.snap }
                    onChange={ (e) => props.setGridSnap(e) } />
                
                <Control className="ad-no"
                    name="Закрыть путь"
                    type="checkbox"
                    value={ props.closePath }
                    onChange={ (e) => props.setClosePath(e) } />
            </div>
            <div className="ad-Controls-container">
                <Control className="ad-no"
                    name="Размер сетки"
                    type="text"
                    value={ props.grid.size }
                    onChange={ (e) => props.setGridSize(e) } />
                <Control className="ad-no"
                    name="Ширина"
                    type="text"
                    value={ props.w }
                    onChange={ (e) => props.setWidth(e) } />
                <Control className="ad-no"
                    name="Высота"
                    type="text"
                    value={ props.h }
                    onChange={ (e) => props.setHeight(e) } />
               
            </div>
            <div className="ad-Controls-container">
                <Control className="ad-no"
                    type="button"
                    action="reset"
                    value="Сбросить путь"
                    onClick={ (e) => props.reset(e) } />
            </div>
                    
            <h3 className="ad-Controls-title">Точка в фокусе</h3>
            
            { props.activePoint !== 0 && (
                <div className="ad-Controls-container">
                    <Control className="ad-no"
                        name="Тип точки"
                        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 className="ad-no"
                    name="X позиция точки"
                    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 className="ad-no"
                    name="Y позиция точки"
                    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 className="ad-no"
                        type="button"
                        action="delete"
                        value="Удалить точку"
                        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")
)
              
            
!
999px

Console