summaryrefslogtreecommitdiff
path: root/animism-align/frontend
diff options
context:
space:
mode:
authorJules Laplace <julescarbon@gmail.com>2020-06-30 20:11:14 +0200
committerJules Laplace <julescarbon@gmail.com>2020-06-30 20:11:14 +0200
commit250527589e003420a84f36c4191d2e574f1ad28c (patch)
treec8ae5ae829c8de1427c2cbb83178cb6c85f71eb7 /animism-align/frontend
parent7bc1723499503800cbdd446b27e202898fc32b9e (diff)
seeking and zooming the waveform. playing the audio
Diffstat (limited to 'animism-align/frontend')
-rw-r--r--animism-align/frontend/actions.js2
-rw-r--r--animism-align/frontend/store.js2
-rw-r--r--animism-align/frontend/types.js4
-rw-r--r--animism-align/frontend/views/align/align.actions.js7
-rw-r--r--animism-align/frontend/views/align/align.css24
-rw-r--r--animism-align/frontend/views/align/align.reducer.js19
-rw-r--r--animism-align/frontend/views/align/components/cursor.component.js2
-rw-r--r--animism-align/frontend/views/align/components/playButton.component.js31
-rw-r--r--animism-align/frontend/views/align/components/playCursor.component.js43
-rw-r--r--animism-align/frontend/views/align/components/timeline.component.js83
-rw-r--r--animism-align/frontend/views/align/components/waveform.component.js2
-rw-r--r--animism-align/frontend/views/audio/audio.actions.js40
-rw-r--r--animism-align/frontend/views/audio/audio.reducer.js30
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
+ }
+}