console
const { createStore, combineReducers } = Redux;
const { connect, Provider } = ReactRedux;
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><svg width='400' height='400'></p>
<p className='ident1'><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'/></p>
<p></svg></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);
})();
const Canvas = (() => {
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);
})();
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() {
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);
})();
const store = (() => {
const initialState = {
sx: 70,
sy: 200,
c1x: 200,
c1y: 330,
c2x: 200,
c2y: 70,
ex: 330,
ey: 200,
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);
})();
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;
}
}