/** * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Paul Luna <paulluna0215@gmail.com> */ import PointComponent from './point-component'; import * as React from 'react'; import { Vec2 } from 'mol-math/linear-algebra'; interface LineGraphComponentState { points: Vec2[], selected?: number, copyPoint: any, updatedX: number, updatedY: number, } export default class LineGraphComponent extends React.Component<any, LineGraphComponentState> { private myRef:any; private height: number; private width: number; private padding: number; constructor(props: any) { super(props); this.myRef = React.createRef(); this.state = { points:[ Vec2.create(0, 0), Vec2.create(1, 0) ], selected: undefined, copyPoint: undefined, updatedX: 0, updatedY: 0, }; this.height = 400; this.width = 600; this.padding = 70; for (const point of this.props.data){ this.state.points.push(point); } this.state.points.sort((a, b) => { if(a[0] === b[0]){ if(a[0] === 0){ return a[1]-b[1]; } if(a[1] === 1){ return b[1]-a[1]; } return a[1]-b[1]; } return a[0] - b[0]; }); this.handleDrag = this.handleDrag.bind(this); this.handleDoubleClick = this.handleDoubleClick.bind(this); this.refCallBack = this.refCallBack.bind(this); this.handlePointUpdate = this.handlePointUpdate.bind(this); this.change = this.change.bind(this); } public render() { const points = this.renderPoints(); const ghostPoint = this.state.copyPoint; return ([ <div key="LineGraph"> <svg className="msp-canvas" ref={this.refCallBack} viewBox={`0 0 ${this.width+this.padding} ${this.height+this.padding}`} onMouseMove={this.handleDrag} onMouseUp={this.handlePointUpdate} onDoubleClick={this.handleDoubleClick}> <g stroke="black" fill="black"> <Poly data={this.state.points} k={0.5} height={this.height} width={this.width} padding={this.padding}/> {points} {ghostPoint} </g> <defs> <linearGradient id="Gradient"> <stop offset="0%" stopColor="#d30000"/> <stop offset="30%" stopColor="#ffff05"/> <stop offset="50%" stopColor="#05ff05"/> <stop offset="70%" stopColor="#05ffff"/> <stop offset="100%" stopColor="#041ae0"/> </linearGradient> </defs> </svg> </div>, <div key="modal" id="modal-root" /> ]); } private change(points: Vec2[]){ let copyPoints = points.slice(); copyPoints.shift(); copyPoints.pop(); this.props.onChange(copyPoints); } private handleMouseDown = (id:number) => (event: any) => { if(id === 0 || id === this.state.points.length-1){ return; } const copyPoint: Vec2 = this.normalizePoint(Vec2.create(this.state.points[id][0], this.state.points[id][1])); this.setState({ selected: id, copyPoint: "ready", updatedX: copyPoint[0], updatedY: copyPoint[1], }); event.preventDefault(); } private handleDrag(event: any) { if(this.state.copyPoint === undefined){ return } const pt = this.myRef.createSVGPoint(); let updatedCopyPoint; const padding = this.padding/2; pt.x = event.clientX; pt.y = event.clientY; const svgP = pt.matrixTransform(this.myRef.getScreenCTM().inverse()); if( svgP.x < (padding) || svgP.x > (this.width+(padding)) || svgP.y > (this.height+(padding)) || svgP.y < (padding)) { return; } updatedCopyPoint = Vec2.create(svgP.x, svgP.y); this.setState({ updatedX: updatedCopyPoint[0], updatedY: updatedCopyPoint[1], }); const unNormalizePoint = this.unNormalizePoint(updatedCopyPoint); this.setState({ copyPoint: <PointComponent selected={false} key="copy" x={updatedCopyPoint[0]} y={updatedCopyPoint[1]} nX={unNormalizePoint[0]} nY={unNormalizePoint[1]} delete={this.deletePoint} onmouseover={this.props.onHover}/> }); this.props.onDrag(unNormalizePoint); event.preventDefault() } private handlePointUpdate(event: any) { const selected = this.state.selected; if(selected === undefined || selected === 0 || selected === this.state.points.length-1) { this.setState({ selected: undefined, copyPoint: undefined, }); return } const updatedPoint = this.unNormalizePoint(Vec2.create(this.state.updatedX, this.state.updatedY)); const points = this.state.points.filter((_,i) => i !== this.state.selected); points.push(updatedPoint);; points.sort((a, b) => { if(a[0] === b[0]){ if(a[0] === 0){ return a[1]-b[1]; } if(a[1] === 1){ return b[1]-a[1]; } return a[1]-b[1]; } return a[0] - b[0]; }); this.setState({ points, selected: undefined, copyPoint: undefined, }); this.change(points); event.preventDefault(); } private handleDoubleClick(event: any) { let newPoint; const pt = this.myRef.createSVGPoint(); pt.x = event.clientX; pt.y = event.clientY; const svgP = pt.matrixTransform(this.myRef.getScreenCTM().inverse()); const points = this.state.points; const padding = this.padding/2; if( svgP.x < (padding) || svgP.x > (this.width+(padding)) || svgP.y > (this.height+(padding)) || svgP.y < (this.padding/2)) { return; } newPoint = this.unNormalizePoint(Vec2.create(svgP.x, svgP.y)); points.push(newPoint); points.sort((a, b) => { if(a[0] === b[0]){ if(a[0] === 0){ return a[1]-b[1]; } if(a[1] === 1){ return b[1]-a[1]; } return a[1]-b[1]; } return a[0] - b[0]; }); this.setState({points}) this.change(points); event.preventDefault(); } private deletePoint = (i:number) => (event: any) => { if(i===0 || i===this.state.points.length-1){ return}; const points = this.state.points.filter((_,j) => j !== i); points.sort((a, b) => { if(a[0] === b[0]){ if(a[0] === 0){ return a[1]-b[1]; } if(a[1] === 1){ return b[1]-a[1]; } return a[1]-b[1]; } return a[0] - b[0]; }); this.setState({points}); this.change(points); event.stopPropagation(); } private normalizePoint(point: Vec2) { const min = this.padding/2; const maxX = this.width+min; const maxY = this.height+min; const normalizedX = (point[0]*(maxX-min))+min; const normalizedY = (point[1]*(maxY-min))+min; const reverseY = (this.height+this.padding)-normalizedY; const newPoint = Vec2.create(normalizedX, reverseY); return newPoint; } private unNormalizePoint(point: Vec2) { const min = this.padding/2; const maxX = this.width+min; const maxY = this.height+min; const unNormalizedX = (point[0]-min)/(maxX-min); // we have to take into account that we reversed y when we first normalized it. const unNormalizedY = ((this.height+this.padding)-point[1]-min)/(maxY-min); return Vec2.create(unNormalizedX, unNormalizedY); } private refCallBack(element: any) { if(element){ this.myRef = element; } } private renderPoints() { const points: any[] = []; let point: Vec2; for (let i = 0; i < this.state.points.length; i++){ if(i != 0 && i != this.state.points.length-1){ point = this.normalizePoint(this.state.points[i]); points.push(<PointComponent key={i} id={i} x={point[0]} y={point[1]} nX={this.state.points[i][0]} nY={this.state.points[i][1]} selected={false} delete={this.deletePoint} onmouseover={this.props.onHover} onMouseDown={this.handleMouseDown(i)} />); } } return points; } } function Poly(props: any) { const points: Vec2[] = []; let min:number; let maxX:number; let maxY: number; let normalizedX: number; let normalizedY: number; let reverseY: number; for(const point of props.data){ min = parseInt(props.padding, 10)/2; maxX = parseInt(props.width, 10)+min; maxY = parseInt(props.height, 10)+min; normalizedX = (point[0]*(maxX-min))+min; normalizedY = (point[1]*(maxY-min))+min; reverseY = (props.height+props.padding)-normalizedY; points.push(Vec2.create(normalizedX, reverseY)); } if (props.k == null) {props.k = 0.3}; const data = points; const size = data.length; const last = size - 2; let path = "M" + [data[0][0], data[0][1]]; for (let i=0; i<size-1;i++){ const x0 = i ? data[i-1][0] : data[0][0]; const y0 = i ? data[i-1][1] : data[0][1]; const x1 = data[i][0]; const y1 = data[i][1]; const x2 = data[i+1][0]; const y2 = data[i+1][1]; const x3 = i !== last ? data[i+2][0] : x2; const y3 = i !== last ? data[i+2][1] : y2; const cp1x = x1 + (x2 - x0)/6 * props.k; const cp1y = y1 + (y2 -y0)/6 * props.k; const cp2x = x2 - (x3 -x1)/6 * props.k; const cp2y = y2 - (y3 - y1)/6 * props.k; path += "C" + [cp1x, cp1y, cp2x, cp2y, x2, y2]; } return <path d={path} strokeWidth="5" stroke="#cec9ba" fill="none"/> }