diff options
Diffstat (limited to 'animism-align/frontend')
13 files changed, 251 insertions, 38 deletions
diff --git a/animism-align/frontend/actions.js b/animism-align/frontend/actions.js index 6ce2d02..1b5f15e 100644 --- a/animism-align/frontend/actions.js +++ b/animism-align/frontend/actions.js @@ -1,6 +1,7 @@ import { bindActionCreators } from 'redux' import { actions as crudActions } from './api' +import * as audioActions from './views/audio/audio.actions' import * as alignActions from './views/align/align.actions' import * as siteActions from './views/site/site.actions' @@ -12,6 +13,7 @@ export default .concat([ ['site', siteActions], ['align', alignActions], + ['audio', audioActions], ]) .map(p => [p[0], bindActionCreators(p[1], store.dispatch)]) .concat([ diff --git a/animism-align/frontend/store.js b/animism-align/frontend/store.js index 871d72c..accf1b1 100644 --- a/animism-align/frontend/store.js +++ b/animism-align/frontend/store.js @@ -7,6 +7,7 @@ import thunk from 'redux-thunk' import uploadReducer from './views/upload/upload.reducer' import alignReducer from './views/align/align.reducer' +import audioReducer from './views/audio/audio.reducer' import paragraphReducer from './views/paragraph/paragraph.reducer' import timestampReducer from './views/timestamp/timestamp.reducer' import siteReducer from './views/site/site.reducer' @@ -19,6 +20,7 @@ const createRootReducer = history => ( site: siteReducer, upload: uploadReducer, align: alignReducer, + audio: audioReducer, paragraph: paragraphReducer, timestamp: timestampReducer, // collection: collectionReducer, diff --git a/animism-align/frontend/types.js b/animism-align/frontend/types.js index d38cd68..9d16a4b 100644 --- a/animism-align/frontend/types.js +++ b/animism-align/frontend/types.js @@ -11,6 +11,10 @@ export const align = crud_type('align', [ 'set_display_setting', ]) +export const audio = with_type('audio', [ + 'play', 'pause', 'update_time', +]) + export const site = with_type('site', [ ]) diff --git a/animism-align/frontend/views/align/align.actions.js b/animism-align/frontend/views/align/align.actions.js index 7d8cbd1..2e1c795 100644 --- a/animism-align/frontend/views/align/align.actions.js +++ b/animism-align/frontend/views/align/align.actions.js @@ -1,8 +1,9 @@ import * as types from '../../types' -import { store, history } from '../../store' +import { store, history, dispatch } from '../../store' import { api, post, pad, preloadImage } from '../../util' import actions from '../../actions' import { session } from '../../session' +import throttle from 'lodash.throttle' import { ZOOM_STEPS } from './constants' @@ -16,6 +17,10 @@ export const setZoom = zoom => dispatch => { } } +export const throttledSetZoom = throttle(zoom => dispatch => { + setZoom(zoom)(dispatch) +}, 250, { leading: true }) + export const setCursor = cursor_ts => dispatch => ( dispatch({ type: types.align.set_display_setting, key: 'cursor_ts', value: cursor_ts }) ) diff --git a/animism-align/frontend/views/align/align.css b/animism-align/frontend/views/align/align.css index 153c481..38a1c8b 100644 --- a/animism-align/frontend/views/align/align.css +++ b/animism-align/frontend/views/align/align.css @@ -44,18 +44,40 @@ canvas { width: 100%; position: absolute; left: 0; + pointer-events: none; } .timeline .cursor .line { width: 100%; height: 1px; background: #00f; } +.timeline .cursor.playCursor .line { + background: #ddd; +} .timeline .cursor .tickLabel { position: absolute; + pointer-events: none; right: 6px; font-size: 12px; width: 40px; margin-top: -7px; text-align: right; text-shadow: 0 0 2px #000, 0 0 2px #000, 0 0 2px #000; -}
\ No newline at end of file +} +.playButton { + position: absolute; + top: 0; left: 0; + width: 3rem; height: 3rem; + padding: 1rem; + background: #000; + cursor: pointer; + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} +.playButton.playing { + background-image: url('/static/img/icons_pause_white.svg'); +} +.playButton.paused { + background-image: url('/static/img/icons_play_white.svg'); +} diff --git a/animism-align/frontend/views/align/align.reducer.js b/animism-align/frontend/views/align/align.reducer.js index f616dc2..5640cde 100644 --- a/animism-align/frontend/views/align/align.reducer.js +++ b/animism-align/frontend/views/align/align.reducer.js @@ -1,8 +1,6 @@ import * as types from '../../types' import { session, getDefault, getDefaultInt } from '../../session' -import { crudState, crudReducer } from '../../api/crud.reducer' - const initialState = { timeline: { cursor_ts: -1, @@ -18,14 +16,15 @@ export default function alignReducer(state = initialState, action) { // console.log(action.type, action) switch (action.type) { case types.peaks.loaded: - return { - ...state, - timeline: { - ...state.timeline, - duration: action.data.length / 20, - } - } - + console.log('peaks duration:', action.data.length / 20) + return state + // return { + // ...state, + // timeline: { + // ...state.timeline, + // duration: action.data.length / 20, + // } + // } case types.align.set_display_setting: return { ...state, diff --git a/animism-align/frontend/views/align/components/cursor.component.js b/animism-align/frontend/views/align/components/cursor.component.js index c92f807..a5ad438 100644 --- a/animism-align/frontend/views/align/components/cursor.component.js +++ b/animism-align/frontend/views/align/components/cursor.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import { ZOOM_STEPS } from '../constants' -import { clamp, timestamp } from '../../../util' +import { timestamp } from '../../../util' const Cursor = ({ timeline }) => { const { start_ts, zoom, cursor_ts, duration } = timeline diff --git a/animism-align/frontend/views/align/components/playButton.component.js b/animism-align/frontend/views/align/components/playButton.component.js new file mode 100644 index 0000000..fcb1422 --- /dev/null +++ b/animism-align/frontend/views/align/components/playButton.component.js @@ -0,0 +1,31 @@ +import React, { Component } from 'react' +// import { Link } from 'react-router-dom' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import actions from '../../../actions' +// import * as alignActions from '../align.actions' + +import { ZOOM_STEPS } from '../constants' +import { clamp } from '../../../util' + +const PlayButton = ({ audio }) => { + return ( + <div + className={audio.playing ? 'playButton playing' : 'playButton paused'} + onClick={() => { + audio.playing ? actions.audio.pause() : actions.audio.play() + }} + /> + ) +} + +const mapStateToProps = state => ({ + audio: state.audio, +}) + +const mapDispatchToProps = dispatch => ({ + // alignActions: bindActionCreators({ ...alignActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(PlayButton) diff --git a/animism-align/frontend/views/align/components/playCursor.component.js b/animism-align/frontend/views/align/components/playCursor.component.js new file mode 100644 index 0000000..f191bb1 --- /dev/null +++ b/animism-align/frontend/views/align/components/playCursor.component.js @@ -0,0 +1,43 @@ +import React, { Component } from 'react' +// import { Link } from 'react-router-dom' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import { ZOOM_STEPS } from '../constants' +import { timestamp } from '../../../util' + +const PlayCursor = ({ timeline, audio }) => { + const { start_ts, zoom, duration } = timeline + const { play_ts } = audio + const secondsPerPixel = ZOOM_STEPS[zoom] * 0.1 + const y = (play_ts - start_ts) / secondsPerPixel + // console.log(play_ts, y) + return ( + <div + className='cursor playCursor' + style={{ + top: y, + }} + > + <div className='line' /> + </div> + ) +} + +/* + <div className='tickLabel'> + {timestamp(cursor_ts, 1)} + </div> + +*/ + +const mapStateToProps = state => ({ + timeline: state.align.timeline, + audio: state.audio, +}) + +const mapDispatchToProps = dispatch => ({ + // alignActions: bindActionCreators({ ...alignActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(PlayCursor) diff --git a/animism-align/frontend/views/align/components/timeline.component.js b/animism-align/frontend/views/align/components/timeline.component.js index 86175cf..3b12cb5 100644 --- a/animism-align/frontend/views/align/components/timeline.component.js +++ b/animism-align/frontend/views/align/components/timeline.component.js @@ -9,6 +9,8 @@ import actions from '../../../actions' import Waveform from './waveform.component' import Ticks from './ticks.component' import Cursor from './cursor.component' +import PlayButton from './playButton.component' +import PlayCursor from './playCursor.component' import { ZOOM_STEPS } from '../constants' import { clamp } from '../../../util' @@ -19,6 +21,7 @@ class Timeline extends Component { this.handleKeydown = this.handleKeydown.bind(this) this.handleMouseMove = this.handleMouseMove.bind(this) this.handleWheel = this.handleWheel.bind(this) + this.handleClick = this.handleClick.bind(this) } componentDidMount() { this.bind() @@ -33,10 +36,31 @@ class Timeline extends Component { document.removeEventListener('keydown', this.handleKeydown) } handleKeydown(e) { - if (e.shiftKey && e.keyCode === 189) { - actions.align.setZoom(this.props.timeline.zoom - 1) - } else if (e.shiftKey && e.keyCode === 187) { - actions.align.setZoom(this.props.timeline.zoom + 1) + if (document.activeElement !== document.body) { + return + } + if (e.shiftKey) { + switch (e.keyCode) { + case 187: // plus + actions.align.setZoom(this.props.timeline.zoom - 1) + break + case 189: // minus + actions.align.setZoom(this.props.timeline.zoom + 1) + break + } + } else { + console.log(e.keyCode) + switch (e.keyCode) { + case 32: // spacebar + actions.audio.toggle() + break + case 38: // up + actions.audio.jump(- ZOOM_STEPS[this.props.timeline.zoom] * 0.1) + break + case 40: // down + actions.audio.jump(ZOOM_STEPS[this.props.timeline.zoom] * 0.1) + break + } } } handleWheel(e) { @@ -45,28 +69,26 @@ class Timeline extends Component { let widthTimeDuration = window.innerHeight * secondsPerPixel // secs per pixel start_ts = clamp(start_ts + e.deltaY * ZOOM_STEPS[zoom], 0, Math.max(0, duration - widthTimeDuration / 2)) - console.log(start_ts) - actions.align.setScrollPosition(start_ts) + if (e.shiftKey) { + if (Math.abs(e.deltaY) < 2) return + if (e.deltaY > 0) { + actions.align.throttledSetZoom(this.props.timeline.zoom + 1) + } else { + actions.align.throttledSetZoom(this.props.timeline.zoom - 1) + } + } else if (e.altKey) { + actions.audio.seek(start_ts) + } else { + actions.align.setScrollPosition(start_ts) + } } handleMouseMove(e) { - const { start_ts, zoom, duration } = this.props.timeline - const y = e.pageY - const height = window.innerHeight - - let secondsPerPixel = ZOOM_STEPS[zoom] * 0.1 - let widthTimeDuration = height * secondsPerPixel - - let timeMin = start_ts - let timeMax = Math.min(start_ts + widthTimeDuration, duration) - let timeWidth = timeMax - timeMin - - let cursor_ts = y * secondsPerPixel + start_ts - cursor_ts = clamp(cursor_ts, 0, timeMax) - + const cursor_ts = positionToTime(e.pageY, this.props.timeline) actions.align.setCursor(cursor_ts) - - // let pixelMin = timeMin / secondsPerPixel - // const ts = + } + handleClick(e) { + const play_ts = positionToTime(e.pageY, this.props.timeline) + actions.audio.seek(play_ts) } render() { return ( @@ -75,14 +97,27 @@ class Timeline extends Component { onWheel={this.handleWheel} onMouseMove={this.handleMouseMove} > - <Waveform /> + <Waveform onClick={this.handleClick} /> <Ticks timeline={this.props.timeline} /> <Cursor timeline={this.props.timeline} /> + <PlayCursor /> + <PlayButton /> </div> ) } } +const positionToTime = (y, { start_ts, zoom, duration }) => { + const secondsPerPixel = ZOOM_STEPS[zoom] * 0.1 + const widthTimeDuration = window.innerHeight * secondsPerPixel + + const timeMin = start_ts + const timeMax = Math.min(start_ts + widthTimeDuration, duration) + const timeWidth = timeMax - timeMin + + return clamp(y * secondsPerPixel + start_ts, 0, timeMax) +} + const mapStateToProps = state => ({ timeline: state.align.timeline, }) diff --git a/animism-align/frontend/views/align/components/waveform.component.js b/animism-align/frontend/views/align/components/waveform.component.js index 4cd9963..e454623 100644 --- a/animism-align/frontend/views/align/components/waveform.component.js +++ b/animism-align/frontend/views/align/components/waveform.component.js @@ -83,7 +83,7 @@ class Waveform extends Component { } render() { return ( - <canvas ref={this.canvasRef} /> + <canvas ref={this.canvasRef} onClick={this.props.onClick} /> ) } } diff --git a/animism-align/frontend/views/audio/audio.actions.js b/animism-align/frontend/views/audio/audio.actions.js new file mode 100644 index 0000000..963d8b0 --- /dev/null +++ b/animism-align/frontend/views/audio/audio.actions.js @@ -0,0 +1,40 @@ +import * as types from '../../types' +import { store, history, dispatch } from '../../store' +import actions from '../../actions' +import { session } from '../../session' + +const audioPlayer = document.createElement('audio') +audioPlayer.src = '/static/data_store/peaks/animismA200620.mp3' +audioPlayer.addEventListener('loadedmetadata', () => { + console.log('audio duration:', audioPlayer.duration) + dispatch({ type: types.align.set_display_setting, key: 'duration', value: audioPlayer.duration }) +}) +audioPlayer.addEventListener('play', () => { + dispatch({ type: types.audio.play }) +}) +audioPlayer.addEventListener('pause', () => { + dispatch({ type: types.audio.pause }) +}) +audioPlayer.addEventListener('timeupdate', () => { + dispatch({ type: types.audio.update_time, play_ts: audioPlayer.currentTime }) +}) + +export const play = () => dispatch => { + audioPlayer.play() +} +export const pause = () => dispatch => { + audioPlayer.pause() +} +export const seek = play_ts => dispatch => { + audioPlayer.currentTime = play_ts +} +export const jump = delta_ts => dispatch => { + audioPlayer.currentTime += delta_ts +} +export const toggle = () => dispatch => { + if (store.getState().audio.playing) { + pause()(dispatch) + } else { + play()(dispatch) + } +}
\ No newline at end of file diff --git a/animism-align/frontend/views/audio/audio.reducer.js b/animism-align/frontend/views/audio/audio.reducer.js new file mode 100644 index 0000000..e37933d --- /dev/null +++ b/animism-align/frontend/views/audio/audio.reducer.js @@ -0,0 +1,30 @@ +import * as types from '../../types' +import { session, getDefault, getDefaultInt } from '../../session' + +const initialState = { + playing: false, + play_ts: 0, +} + +export default function alignReducer(state = initialState, action) { + // console.log(action.type, action) + switch (action.type) { + case types.audio.play: + return { + ...state, + playing: true, + } + case types.audio.pause: + return { + ...state, + playing: false, + } + case types.audio.update_time: + return { + ...state, + play_ts: action.play_ts, + } + default: + return state + } +} |
