diff options
Diffstat (limited to 'app/client/common/timeline.component.js')
| -rw-r--r-- | app/client/common/timeline.component.js | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/app/client/common/timeline.component.js b/app/client/common/timeline.component.js new file mode 100644 index 0000000..d3739b1 --- /dev/null +++ b/app/client/common/timeline.component.js @@ -0,0 +1,166 @@ +import { h, Component } from 'preact' +import { clamp, norm } from '../util/math' + +const initialState = { + dir: '/', + start: null, + end: null, + cursor: null, + selection: null, + width: 0, + ratio: 0, + loading: true +} + +export default class Timeline extends Component { + state = { ...initialState } + + constructor() { + super() + this.handleMouseDown = this.handleMouseDown.bind(this) + this.handleMouseMove = this.handleMouseMove.bind(this) + this.handleMouseUp = this.handleMouseUp.bind(this) + this.computeOffset = this.computeOffset.bind(this) + } + + componentDidMount() { + const { sequence } = this.props + window.addEventListener('resize', this.computeOffset) + window.addEventListener('mousemove', this.handleMouseMove) + window.addEventListener('mouseup', this.handleMouseUp) + this.computeOffset() + if (sequence) { + this.reset() + } + } + + componentDidUpdate(prevProps) { + const { sequence } = this.props + if (sequence !== prevProps.sequence) { + this.reset() + } + } + + componentWillUnmount(){ + window.removeEventListener('resize', this.computeOffset) + window.removeEventListener('mousemove', this.handleMouseMove) + window.removeEventListener('mouseup', this.handleMouseUp) + } + + reset(){ + const { sequence } = this.props + if (!sequence || !sequence.length) return + const len = sequence.length - 1 + const start = { frame: sequence[0], i: 0 } + const end = { frame: sequence[len], i: len } + const width = Math.sqrt(clamp(sequence.length / 15000, 0.3, 1.0)) * Math.min(window.innerWidth - 40, 1000) + const ratio = width / sequence.length + setTimeout(() => this.computeOffset()) + this.setState({ + ...initialState, + width, + ratio, + start, + end, + }) + } + + computeOffset(){ + if (this.ref) { + this.offset = this.ref.getBoundingClientRect() + } + } + + computeFrame(e){ + const { sequence } = this.props + if (!sequence || !sequence.length) return null + let x = (e.pageX - this.offset.left) / this.offset.width + let y = (e.pageY - this.offset.top) / this.offset.height + if (this.state.dragging) { + x = clamp(x, 0, 1) + y = clamp(y, 0, 1) + } + if (0 <= x && x <= 1 && 0 <= y && y <= 1) { + const index = Math.floor(x * (sequence.length-1)) + return { frame: sequence[index], i: index } + } + return null + } + + handleMouseDown(e) { + this.setState({ dragging: true }) + const frame = this.computeFrame(e) + if (frame) { + this.props.onPick && this.props.onPick(frame) + this.props.onSelect && this.props.onSelect({ start: frame, end: frame }) + this.setState({ + start: frame, + end: frame, + }) + } + } + + handleMouseMove(e) { + const frame = this.computeFrame(e) + if (frame) { + this.props.onCursor && this.props.onCursor(frame) + if (this.state.dragging) { + let start = this.state.start + let end = frame + this.setState({ + cursor: frame, + end: frame, + }) + if (this.props.onSelect) { + if (end.i < start.i) { + [start, end] = [end, start] + } + this.props.onSelect({ start, end }) + } + } else { + this.setState({ + cursor: frame + }) + } + } + } + + handleMouseUp(e) { + if (!this.state.dragging) return + let { start, end } = this.state + if (end.i < start.i) { + [start, end] = [end, start] + } + this.props.onSelect && this.props.onSelect({ start, end }) + this.setState({ dragging: false }) + } + + render() { + const { sequence } = this.props + const { loading, start, end, cursor, width, ratio } = this.state + return ( + <div + className='timeline' + style={{ width }} + ref={ref => this.ref = ref} + onMouseDown={this.handleMouseDown} + > + {ratio && start && end && this.renderSelection(start, end, ratio)} + {ratio && cursor && this.renderCursor(cursor, ratio)} + </div> + ) + } + renderCursor(cursor, ratio){ + const left = cursor.i * ratio + return ( + <div key='cursor' className='cursor' style={{ left }} /> + ) + } + renderSelection(start, end, ratio){ + const left = Math.min(start.i, end.i) * ratio + const width = Math.max(Math.abs(start.i - end.i) * ratio, 1) + return ( + <div key='selection' className='selection' style={{ left, width }} /> + ) + } +} |
