SOURCE

console 命令行工具 X clear

                    
>
console
/* React and SVG is used for display, 
 * Redux is used for state handling and 
 * RxJS is handling the drag & drop interaction
 */

/* Import { createStore } from 'redux' */
const { createStore, combineReducers } = Redux;

/* Import { connect, Provider } from 'react-redux' */
const { connect, Provider } = ReactRedux;

/*********
* React: Root Component
**********/
const Component = (() => {
  class UnconnectedComponent extends React.Component {
    render() {
      return (
        <div id='container'>
          <Canvas>
            <line className="helper"
              x1={this.props.sx} y1={this.props.sy}
              x2={this.props.c1x} y2={this.props.c1y} />
            <line className="helper"
              x1={this.props.c2x} y1={this.props.c2y}
              x2={this.props.ex} y2={this.props.ey} />
            <path id="curve" d={`
              M ${this.props.sx} ${this.props.sy} 
              C ${this.props.c1x} ${this.props.c1y}, 
                ${this.props.c2x} ${this.props.c2y}, 
                ${this.props.ex} ${this.props.ey}`
            } />
            <Draggable x={this.props.sx} y={this.props.sy} changeCoord={this.props.setStartPoint}/>
            <Draggable x={this.props.c1x} y={this.props.c1y} changeCoord={this.props.setControlPoint1}/>
            <Draggable x={this.props.c2x} y={this.props.c2y} changeCoord={this.props.setControlPoint2}/>
            <Draggable x={this.props.ex} y={this.props.ey} changeCoord={this.props.setEndPoint}/>
          </Canvas>
          <div className='code'>
            <pre>
              <p>&lt;svg width='400' height='400'&gt;</p>
              <p className='ident1'>&lt;path d='M {this.props.sx} {this.props.sy} C {this.props.c1x} {this.props.c1y}, {this.props.c2x} {this.props.c2y}, {this.props.ex} {this.props.ey}'</p>
              <p className='ident2'>stroke='white' stroke-width='20' fill='transparent'/&gt;</p>
              <p>&lt;/svg&gt;</p>
            </pre>
          </div>
        </div>
      );
    }
  }

  const componentMapStateToProps = (state, ownProps) => {
    return {
      sx: state.sx,
      sy: state.sy,
      c1x: state.c1x,
      c1y: state.c1y,
      c2x: state.c2x,
      c2y: state.c2y,
      ex: state.ex,
      ey: state.ey
    }
  }

  const componentMapDispatchToProps = (dispatch, ownProps) => {
    return {
      setStartPoint: (x, y) => {
        dispatch({ type: 'S', x, y });
      },
      setControlPoint1: (x, y) => {
        dispatch({ type: 'C1', x, y});
      },
      setControlPoint2: (x, y) => {
        dispatch({ type: 'C2', x, y});
      },
      setEndPoint: (x, y) => {
        dispatch({ type: 'E', x, y});
      }
    }
  }
  
  return connect(componentMapStateToProps, componentMapDispatchToProps)(UnconnectedComponent);
})();

/*********
* React: Canvas
**********/
const Canvas = (() => {
  // An SVG wrapper that adaps to the available space
  class UnconnectedCanvas extends React.Component {
    render() {
      return (
        <svg xmlns="http://www.w3.org/2000/svg" version="1.1"
          width={this.props.canvasWidth} height={this.props.canvasHeight} 
          viewBox={`0 0 ${this.props.width} ${this.props.height}`} >
          {this.props.children}
        </svg>
      );
    }

    componentDidMount() {
      let resize = Rx.Observable.fromEvent(window, 'resize').map(() => ({
        width: window.innerWidth, 
        height: window.innerHeight
      }));

      resize.forEach(({width, height}) => {
        this.props.resize(width, height);
      });
    }
  }

  const canvasMapStateToProps = (state, ownProps) => {
    return {
      canvasWidth: state.canvasWidth,
      canvasHeight: state.canvasHeight,
      width: state.width,
      height: state.height
    }
  }

  const canvasMapDispatchToProps = (dispatch, ownProps) => {
    return {
      resize: (width, height) => {
        dispatch({
          type: 'RESIZE',
          width,
          height
        })
      }
    }
  }
  return connect(canvasMapStateToProps, canvasMapDispatchToProps)(UnconnectedCanvas);
})();

/*********
* React: Draggable
**********/
const Draggable = (() => {
  class UnconnectedDraggable extends React.Component {
    constructor({x, y, zoom, changeCoord}) {
      super({x, y, zoom, changeCoord});

      this.state = {
        dragging: false,
      };
      this.size = 30;
    }

    render() {
      return (
        <g className={this.state.dragging ? "dragging" : "draggable"} 
          ref={(draggable) => { this.draggable = draggable; }}
          transform={`translate(${this.props.x},${this.props.y})`}>
          <circle x={0} y={0} r={this.size/2} />
          <text x={this.size/2} y={-this.size/2} textAnchor="left" stroke="none">
            {`${this.props.x}, ${this.props.y}`} 
          </text>
        </g>
      );
    }

    componentDidMount() {
      // Event handling using RxJS
      // If you want to know how this part works and how can you handle complex user interactions, read this:
      // http://codepen.io/HunorMarton/post/handling-complex-mouse-and-touch-events-with-rxjs
      const mouseEventToCoordinate = mouseEvent => ({x: mouseEvent.clientX, y: mouseEvent.clientY});
      const touchEventToCoordinate = touchEvent => {
        touchEvent.preventDefault();
        return {x: touchEvent.touches[0].clientX, y: touchEvent.touches[0].clientY};
      };

      let mouseDowns = Rx.Observable.fromEvent(this.draggable, "mousedown").map(mouseEventToCoordinate);
      let mouseMoves = Rx.Observable.fromEvent(window, "mousemove").map(mouseEventToCoordinate);
      let mouseUps = Rx.Observable.fromEvent(window, "mouseup");

      let touchStarts = Rx.Observable.fromEvent(this.draggable, "touchstart").map(touchEventToCoordinate);
      let touchMoves = Rx.Observable.fromEvent(this.draggable, "touchmove").map(touchEventToCoordinate);
      let touchEnds = Rx.Observable.fromEvent(window, "touchend");

      let dragStarts = mouseDowns.merge(touchStarts);
      let moves = mouseMoves.merge(touchMoves);
      let dragEnds = mouseUps.merge(touchEnds);

      let drags = dragStarts.concatMap(dragStartEvent => {
        const xDelta = this.props.x - dragStartEvent.x*this.props.zoom;
        const yDelta = this.props.y - dragStartEvent.y*this.props.zoom;
        return moves.takeUntil(dragEnds).map(dragEvent => {
          const x = dragEvent.x*this.props.zoom + xDelta;
          const y = dragEvent.y*this.props.zoom + yDelta;
          return {x, y};
        })
      });

      dragStarts.forEach(() => {
        this.setState({dragging: true});
      });

      drags.forEach(coordinate => {
        this.props.changeCoord(coordinate.x, coordinate.y);
      });

      dragEnds.forEach(() => {
        this.setState({dragging: false});
      });
    }
  }

  const draggableMapStateToProps = (state, ownProps) => {
    return {
      zoom: state.zoom
    }
  }
  
  return connect(draggableMapStateToProps)(UnconnectedDraggable);
})();

/*********
* Redux: Store
**********/
const store = (() => {
  const initialState = {
    // Component specific state
    sx: 70,
    sy: 200,
    c1x: 200,
    c1y: 330,
    c2x: 200,
    c2y: 70,
    ex: 330,
    ey: 200,

    // Canvas and draggable specific state
    canvasWidth: 500,
    canvasHeight: 500,
    width: 400,
    height: 400,
    zoom: 1 
  };

  const reducer = (state = initialState, action) => {
    function overrideCoord(x, y) {
      x = Math.min(x, state.width);
      y = Math.min(y, state.height);
      x = Math.max(x, 0);
      y = Math.max(y, 0);
      x = Math.round(x);
      y = Math.round(y);
      return {x, y};
    }
    switch (action.type) {
      case 'S': {
          const {x, y} = overrideCoord(action.x, action.y);
          return Object.assign({}, state, { 
            sx: x,
            sy: y
          });
        }
      case 'C1': {
          const {x, y} = overrideCoord(action.x, action.y);
          return Object.assign({}, state, {
            c1x: x,
            c1y: y
          });
        }
      case 'C2': {
          const {x, y} = overrideCoord(action.x, action.y);
          return Object.assign({}, state, {
            c2x: x,
            c2y: y
          });
        }
      case 'E': {
          const {x, y} = overrideCoord(action.x, action.y);
          return Object.assign({}, state, {
            ex: x,
            ey: y
          });
        }
      case 'RESIZE':
        const minSize = Math.min(action.width, action.height);
        if(minSize <= 500) {
          return Object.assign({}, state, {
            canvasWidth: minSize,
            canvasHeight: minSize,
            zoom: state.width/minSize
          });
        }else{
          return state;
        }
      default:
        return state;
      }
  };
  
  return createStore(reducer);
})();

/*********
* REACT DOM
**********/
ReactDOM.render(
  <Provider store={store}>
    <Component />
  </Provider>,
  document.getElementById('app')
);
<div id="app">Loading...</div>
@import url('https://fonts.googleapis.com/css?family=Roboto+Mono');

$primary-color: #F7EDEE;
$secondary-color: #BA1F36;
$tertiary-color: #911729;
$background-color: #42171E;

html {
  background-color: #222;
  font-family: 'Roboto Mono', monospace;
  font-size: 0.8em;
  color: $primary-color;
}

#container {
  display: flex;
  flex-wrap: wrap;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

svg {
  background-color: $background-color;
}

.draggable {
  fill: rgba($tertiary-color, 0.8);
  cursor: grab;
  text {
    fill: rgba($primary-color, 0.6);
  }
}
.draggable:hover, .dragging {
  fill: rgba($secondary-color, 0.8);
  cursor: grabbing;
  text {
    fill: rgba(white, 0.8);
  }
}
text {
  font-size: 0.8em;
  .draggable &, .dragging & {
    user-select: none;
  }
}
path#curve {
  stroke: $primary-color;
  stroke-width: 20px;
  fill: transparent;
}
line.helper {
  stroke: rgba($tertiary-color, 0.8);
  stroke-width: 2px;
  fill: transparent;
}

.code {
  margin: 50px;
}
pre {
  font-family: 'Roboto Mono', monospace;
  p {
    margin-top: 10px;
    margin-bottom: 10px;
  }
  .ident1 {
    margin-left: 10px;
  }
  .ident2 {
    margin-left: 30px;
  }
}

本项目引用的自定义外部资源