diff options
Diffstat (limited to 'app/client')
| -rw-r--r-- | app/client/common/fileViewer.component.js | 87 | ||||
| -rw-r--r-- | app/client/common/index.js | 5 | ||||
| -rw-r--r-- | app/client/common/timeline.component.js | 166 | ||||
| -rw-r--r-- | app/client/modules/pix2pixhd/views/sequence.editor.js | 131 | ||||
| -rw-r--r-- | app/client/util/format.js | 11 | ||||
| -rw-r--r-- | app/client/util/math.js | 16 |
6 files changed, 307 insertions, 109 deletions
diff --git a/app/client/common/fileViewer.component.js b/app/client/common/fileViewer.component.js index d98073b..4938650 100644 --- a/app/client/common/fileViewer.component.js +++ b/app/client/common/fileViewer.component.js @@ -21,11 +21,37 @@ const video_types = { 'mp4': 'video/mp4', } +const THROTTLE_FETCH_TIME = 200 + class FileViewer extends Component { state = { loading: false, stale: false, - buffer: {} + buffer: {}, + url: null, + } + + componentDidMount(){ + this.fetch() + } + + componentDidUpdate(prevProps){ + if (this.props.file !== prevProps.file) { + this.deferFetch() + } + } + + componentWillUnmount(){ + if (this.state.url) { + window.URL.revokeObjectURL(this.state.url) + } + } + + deferFetch(){ + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.fetch() + }, THROTTLE_FETCH_TIME) } fetch() { @@ -40,48 +66,56 @@ class FileViewer extends Component { this.setState({ buffer: null, loading: true }) if (thumbnail) { - console.log('fetch thumbnail', fn) + // console.log('fetch thumbnail', fn) const size = parseInt(thumbnail) || 200 actions.socket .thumbnail({ module, fn, size }) .then(this.loadBuffer.bind(this)) } else { - console.log('fetch file', fn) + // console.log('fetch file', fn) actions.socket .read_file({ module, fn }) .then(this.loadBuffer.bind(this)) } } + loadBuffer(buffer) { - console.log('fetched buffer', buffer) + // console.log('fetched buffer', buffer) + const { name, buf } = buffer + const ext = extension(name) + if (this.state.url) { + window.URL.revokeObjectURL(this.state.url) + } + let url + if (buf) { + if (ext in image_types) { + url = getURLFor(buf, image_types[ext]) + } else if (ext in audio_types) { + url = getURLFor(buf, audio_types[ext]) + } else if (ext in video_types) { + url = getURLFor(buf, video_types[ext]) + } else { + url = ab2str(buf) + } + } const { stale } = this.state - this.setState({ buffer, loading: false, stale: false, }, () => { - console.log('loaded') + this.setState({ ext, url, buffer, loading: false, stale: false, }, () => { + // console.log('loaded') if (stale) { - console.log('stale, fetching...') + // console.log('stale, fetching...') this.fetch() } }) } - componentDidMount(){ - this.fetch() - } - - componentDidUpdate(nextProps){ - if (this.props.file !== nextProps.file) { - this.fetch() - } - } - render() { const { file } = this.props if (!file) { return <div className='fileViewer'></div> } - const { loading, buffer } = this.state + const { loading, buffer, url, ext } = this.state if (loading) { - return <div className='fileViewer'>Loading...</div> + return <div className='fileViewer'><span>Loading...</span></div> } const { error, @@ -90,24 +124,23 @@ class FileViewer extends Component { buf, } = buffer if (error) { - return <div className='fileViewer'>{error}</div> + return <div className='fileViewer'><span>{error}</span></div> } if (!name) { return <div className='fileViewer'></div> } - if (!buf) { - return <div className='fileViewer'>File empty</div> + if (!buf || !url) { + return <div className='fileViewer'><span>File empty</span></div> } - const ext = extension(name) let tag; if (ext in image_types) { - tag = <img src={getURLFor(buf, image_types[ext])} /> + tag = <img src={url} /> } else if (ext in audio_types) { - tag = <audio src={getURLFor(buf, audio_types[ext])} controls autoplay /> + tag = <audio src={url} controls autoplay /> } else if (ext in video_types) { - tag = <video src={getURLFor(buf, video_types[ext])} controls autoplay /> + tag = <video src={url} controls autoplay /> } else { - tag = <div className='text'>{ab2str(buf)}</div> + tag = <div className='text'>{url}</div> } return ( <div className='fileViewer'>{tag}</div> diff --git a/app/client/common/index.js b/app/client/common/index.js index e120597..15511fd 100644 --- a/app/client/common/index.js +++ b/app/client/common/index.js @@ -21,8 +21,9 @@ import Progress from './progress.component' import Select from './select.component' import SelectGroup from './selectGroup.component' import Slider from './slider.component' -import TextInput from './textInput.component' import TaskList from './taskList.component' +import TextInput from './textInput.component' +import Timeline from './timeline.component' import * as Views from './views' export { @@ -34,5 +35,5 @@ export { TextInput, NumberInput, Slider, Select, SelectGroup, Button, Checkbox, CurrentTask, TaskList, - ButtonGrid, AugmentationGrid, + ButtonGrid, AugmentationGrid, Timeline, }
\ No newline at end of file diff --git a/app/client/common/timeline.component.js b/app/client/common/timeline.component.js new file mode 100644 index 0000000..3008c37 --- /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.2, 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 }} /> + ) + } +} diff --git a/app/client/modules/pix2pixhd/views/sequence.editor.js b/app/client/modules/pix2pixhd/views/sequence.editor.js index e66aebf..3bf2d63 100644 --- a/app/client/modules/pix2pixhd/views/sequence.editor.js +++ b/app/client/modules/pix2pixhd/views/sequence.editor.js @@ -3,28 +3,19 @@ import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { Route, Link } from 'react-router-dom' -import { Loading, FileList, FileViewer } from '../../../common' +import util from '../../../util' + +import { FileViewer, Timeline, Param, Button } from '../../../common' import actions from '../../../actions' const initialState = { dir: '/', - frameA: null, - frameB: null, + cursor: null, selection: null, - loading: true } /* - when the sequence editor loads, - reset the selection - reset the two frames - set the two frames to the beginning and end of the video - when mousing over the video - ideally you would see a tiny thumbnail preview of the frame :) - - click to start a selection, drag over, mouseup to end the selection - this should update the start/end frame - ... so there are two things you could do with this 1) create an entirely new dataset 2) add frames to an existing dataset @@ -49,94 +40,90 @@ class SequenceEditor extends Component { constructor() { super() - this.handleMouseDown = this.handleMouseDown.bind(this) - this.handleMouseMove = this.handleMouseMove.bind(this) - this.handleMouseEnter = this.handleMouseEnter.bind(this) - this.handleMouseLeave = this.handleMouseLeave.bind(this) - this.handleMouseUp = this.handleMouseUp.bind(this) + this.handleCursor = this.handleCursor.bind(this) + this.handleSelect = this.handleSelect.bind(this) } componentDidMount() { const { checkpoint } = this.props - window.addEventListener('mousemove', this.handleMouseMove) - window.addEventListener('mouseup', this.handleMouseUp) - if (checkpoint && checkpoint.sequence) { - console.log(checkpoint) - const frameA = checkpoint.sequence[0] - const frameB = checkpoint.sequence[checkpoint.sequence.length-1] - this.setState({ - ...initialState, - frameA, - frameB, - }) + if (checkpoint) { + this.reset() } } componentDidUpdate(prevProps) { const { checkpoint } = this.props if (checkpoint !== prevProps.checkpoint) { - console.log(checkpoint) - const frameA = checkpoint.sequence[0] - const frameB = checkpoint.sequence[checkpoint.sequence.length-1] - this.setState({ - ...initialState, - frameA, - frameB, - }) + this.reset() } } - componentWillUnmount(){ - window.removeEventListener('mouseup', this.handleMouseUp) - window.removeEventListener('mousemove', this.handleMouseMove) + reset(){ + const { checkpoint } = this.props + if (!(checkpoint && checkpoint.sequence)) return + console.log(checkpoint) + this.setState({ + ...initialState, + }) } - handleMouseDown(e) { - this.setState({ dragging: true }) - } - handleMouseMove(e) { - } - handleMouseEnter(e) { - } - handleMouseLeave(e) { - } - handleMouseUp(e) { - this.setState({ dragging: false }) + handleCursor(cursor) { + this.setState({ cursor }) } - handlePick(file) { - console.log(file) - // this.setState({ dir, file: null, loading: true }) + handleSelect(selection) { + this.setState({ selection }) } render() { const { app, checkpoint } = this.props - const { - loading, - selection, - frameA, frameB, - } = this.state - // console.log(this.props, this.state) - const width = 200 + const { cursor, selection } = this.state const path = "sequences/" + checkpoint.name return ( <div className='sequenceEditor'> - <div - className='timeline' - style={{ width }} - mouseDown={this.handleSelectionStart} - mouseEnter={this.handleMouseEnter} - mouseLeave={this.handleMouseLeave} - > - {selection && <div className='selection' style={selection}></div>} - </div> - <FileViewer thumbnail path={path} file={this.state.frameA} /> - <FileViewer thumbnail path={path} file={this.state.frameB} /> + <Timeline + sequence={checkpoint.sequence} + onCursor={this.handleCursor} + onSelect={this.handleSelect} + /> + <Frame label='Cursor' path={path} frame={cursor} /> + {selection && selection.start && + <Frame label='Selection Start' path={path} frame={selection.start} /> + } + {selection && selection.end && + <Frame label='Selection End' path={path} frame={selection.end} /> + } + {selection && + <div className='form'> + <Param + title='Selection length' + >{selection.end.i - selection.start.i}{' frames'}</Param> + <Param + title='Duration' + >{util.frameTimestamp(selection.end.i - selection.start.i)}</Param> + <Button + title='Create a new dataset?' + >Create</Button> + </div> + } </div> ) } } +function Frame ({ label, path, frame }) { + if (!frame) return <div class='frame'></div> + return ( + <div class='frame'> + <FileViewer thumbnail={140} path={path} file={frame.frame} /> + <div class='spaced'> + <span>{label}</span> + <span>{'#'}{frame.i} {util.frameTimestamp(frame.i)}</span> + </div> + </div> + ) +} + const mapStateToProps = state => ({ app: state.system.app, }) diff --git a/app/client/util/format.js b/app/client/util/format.js index ee1f47f..2c94de1 100644 --- a/app/client/util/format.js +++ b/app/client/util/format.js @@ -1,3 +1,13 @@ +const FRAME_RATE = 25 +export function frameTimestamp(n = 0) { + n /= FRAME_RATE + let s = pad((n % 60).toFixed(1)) + n = Math.floor(n / 60) + if (n > 60) { + return Math.floor(n / 60) + ':' + pad(n % 60) + ':' + s + } + return (n % 60) + ':' + s +} export function timeInSeconds(n){ return (n / 10).toFixed(1) + ' s.' } @@ -144,3 +154,4 @@ export function get_age (t) { return r(age) + "y" } export function courtesy_s (n, s) { return n == 1 ? "" : (s || "s") } +export function pad(n, r){ return n < 10 ? '0' + n : n } diff --git a/app/client/util/math.js b/app/client/util/math.js index c301ffd..064d37c 100644 --- a/app/client/util/math.js +++ b/app/client/util/math.js @@ -4,14 +4,14 @@ export const norm = (n,a,b) => (n-a) / (b-a) export const lerp = (n,a,b) => (b-a)*n+a export const mix = (n,a,b) => a*(1-n)+b*n export const randint = (n) => Math.floor(Math.random()*n) -export function randrange(a,b){ return Math.random() * (b-a) + a } -export function randsign(){ return Math.random() >= 0.5 ? -1 : 1 } -export function choice (a){ return a[ Math.floor(Math.random() * a.length) ] } -export function angle(x0,y0,x1,y1){ return Math.atan2(y1-y0,x1-x0) } -export function dist(x0,y0,x1,y1){ return Math.sqrt(Math.pow(x1-x0,2)+Math.pow(y1-y0,2)) } -export function xor(a,b){ a=!!a; b=!!b; return (a||b) && !(a&&b) } -export function quantize(a,b){ return Math.floor(a/b)*b } -export function shuffle(a){ +export const randrange = (a,b) => Math.random() * (b-a) + a +export const randsign = () => Math.random() >= 0.5 ? -1 : 1 +export const choice = (a) => a[ Math.floor(Math.random() * a.length) ] +export const angle = (x0,y0,x1,y1) => Math.atan2(y1-y0, x1-x0) +export const dist = (x0,y0,x1,y1) => Math.sqrt(Math.pow(x1-x0, 2) + Math.pow(y1-y0, 2)) +export const xor = (a,b) => { a=!!a; b=!!b; return (a||b) && !(a&&b) } +export const quantize = (a,b) => Math.floor(a/b)*b +export const shuffle = (a) => { for (var i = a.length; i > 0; i--){ var r = randint(i) var swap = a[i-1] |
