summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJules Laplace <julescarbon@gmail.com>2020-08-28 20:24:11 +0200
committerJules Laplace <julescarbon@gmail.com>2020-08-28 20:24:11 +0200
commit15d5f168363b9592cd5f201fe2ce2a31d3d692bd (patch)
tree0f79d47a5fbaff9a3858d0aa1037aebcb6a1c66a
parent6dea2acdf558f39c868be92c5657190d9fb6bd46 (diff)
tween-scroll the transcript
-rw-r--r--animism-align/frontend/app/utils/index.js2
-rw-r--r--animism-align/frontend/app/utils/oktween.js163
-rw-r--r--animism-align/frontend/app/views/paragraph/components/paragraph.list.js4
-rw-r--r--animism-align/frontend/app/views/viewer/player/components.media/media.vitrine.js2
-rw-r--r--animism-align/frontend/app/views/viewer/transcript/components/elementTypes.gallery.js3
-rw-r--r--animism-align/frontend/app/views/viewer/transcript/components/elementTypes.image.js3
-rw-r--r--animism-align/frontend/app/views/viewer/transcript/components/elementTypes.text.js4
-rw-r--r--animism-align/frontend/app/views/viewer/transcript/components/elementTypes.video.js3
-rw-r--r--animism-align/frontend/app/views/viewer/transcript/transcript.container.js88
9 files changed, 267 insertions, 5 deletions
diff --git a/animism-align/frontend/app/utils/index.js b/animism-align/frontend/app/utils/index.js
index 0f5a1dd..855fcaf 100644
--- a/animism-align/frontend/app/utils/index.js
+++ b/animism-align/frontend/app/utils/index.js
@@ -8,6 +8,7 @@ export const formatDateTime = dateStr => format(new Date(dateStr), 'd MMM yyyy H
export const formatDate = dateStr => format(new Date(dateStr), 'd MMM yyyy')
export const formatTime = dateStr => format(new Date(dateStr), 'H:mm')
export const formatAge = dateStr => formatDistance(new Date(), new Date(dateStr)) + ' ago.'
+export const toArray = a => Array.from(a)
/* Mobile check */
@@ -93,6 +94,7 @@ export const clamp = (n, a=0, b=1) => n < a ? a : n < b ? n : b
export const dist = (x1, y1, x2, y2) => Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))
export const mod = (n, m) => n - (m * Math.floor(n / m))
export const angle = (x1, y1, x2, y2) => Math.atan2(y2 - y1, x2 - x1)
+export const lerp = (n,a,b) => (b-a)*n+a
export const floatLT = (a,b) => ((a*10|0) < (b*10|0))
export const floatLTE = (a,b) => ((a*10|0) === (b*10|0) || floatLT(a,b))
export const floatGT = (a,b) => ((a*10|0) > (b*10|0))
diff --git a/animism-align/frontend/app/utils/oktween.js b/animism-align/frontend/app/utils/oktween.js
new file mode 100644
index 0000000..4388ad4
--- /dev/null
+++ b/animism-align/frontend/app/utils/oktween.js
@@ -0,0 +1,163 @@
+/*
+ oktween.add({
+ obj: el.style,
+ units: "px",
+ from: { left: 0 },
+ to: { left: 100 },
+ duration: 1000,
+ easing: oktween.easing.circ_out,
+ update: function(obj){
+ console.log(obj.left)
+ }
+ finished: function(){
+ console.log("done")
+ }
+ })
+*/
+
+import { lerp } from 'app/utils'
+
+const oktween = {}
+let tweens = []
+
+let last_t = 0
+let id = 0
+
+oktween.speed = 1
+oktween.add = (tween) => {
+ tween.id = id++
+ tween.obj = tween.obj || {}
+ if (tween.easing) {
+ if (typeof tween.easing === "string") {
+ tween.easing = oktween.easing[tween.easing]
+ }
+ } else {
+ tween.easing = oktween.easing.linear
+ }
+ if (!('from' in tween) && !('to' in tween)) {
+ tween.keys = []
+ } else if (!('from' in tween)) {
+ tween.from = {}
+ tween.keys = Object.keys(tween.to)
+ tween.keys.forEach(function(prop) {
+ tween.from[prop] = parseFloat(tween.obj[prop])
+ })
+ } else {
+ tween.keys = Object.keys(tween.from)
+ }
+ tween.delay = tween.delay || 0
+ tween.start = last_t + tween.delay
+ tween.done = false
+ tween.after = tween.after || []
+ tween.then = (fn) => { tween.after.push(fn); return tween }
+ tween.tick = 0
+ tween.skip = tween.skip || 1
+ tween.dt = 0
+ tweens.push(tween)
+ return tween
+}
+oktween.update = (t) => {
+ let done = false
+ requestAnimationFrame(oktween.update)
+ last_t = t * oktween.speed
+ if (tweens.length === 0) return
+ tweens.forEach((tween, i) => {
+ const dt = Math.min(1.0, (t - tween.start) / tween.duration)
+ tween.tick++
+ if (dt < 0 || (dt < 1 && (tween.tick % tween.skip != 0))) return
+ const ddt = tween.easing(dt)
+ tween.dt = ddt
+ tween.keys.forEach((prop) => {
+ let val = lerp(ddt, tween.from[prop], tween.to[prop])
+ if (tween.round) val = Math.round(val)
+ if (tween.units) val = (Math.round(val)) + tween.units
+ tween.obj[prop] = val
+ })
+ if (tween.update) {
+ tween.update(tween.obj, dt)
+ }
+ if (dt === 1) {
+ if (tween.finished) {
+ tween.finished(tween)
+ }
+ if (tween.after.length) {
+ const twn = tween.after.shift()
+ twn.obj = twn.obj || tween.obj
+ twn.after = tween.after
+ oktween.add(twn)
+ }
+ if (tween.loop) {
+ tween.start = t + tween.delay
+ }
+ else {
+ done = true
+ tween.done = true
+ }
+ }
+ })
+ if (done) {
+ tweens = tweens.filter(tween => !tween.done)
+ }
+}
+
+requestAnimationFrame(oktween.update)
+
+oktween.easing = {
+ linear: (t) => {
+ return t
+ },
+ circ_out: (t) => {
+ return Math.sqrt(1 - (t = t - 1) * t)
+ },
+ circ_in: (t) => {
+ return -(Math.sqrt(1 - (t * t)) - 1)
+ },
+ circ_in_out: (t) => {
+ return ((t*=2) < 1) ? -0.5 * (Math.sqrt(1 - t * t) - 1) : 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1)
+ },
+ quad_in: (n) => {
+ return Math.pow(n, 2)
+ },
+ quad_out: (n) => {
+ return n * (n - 2) * -1
+ },
+ quad_in_out: (n) => {
+ n = n * 2
+ if (n < 1) { return Math.pow(n, 2) / 2 }
+ return -1 * ((--n) * (n - 2) - 1) / 2
+ },
+ cubic_bezier: (mX1, mY1, mX2, mY2) => {
+ function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1 }
+ function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1 }
+ function C(aA1) { return 3.0 * aA1 }
+
+ // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
+ function CalcBezier(aT, aA1, aA2) {
+ return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT
+ }
+
+ // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
+ function GetSlope(aT, aA1, aA2) {
+ return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1)
+ }
+
+ function GetTForX(aX) {
+ // Newton raphson iteration
+ let aGuessT = aX
+ for (let i = 0; i < 10; ++i) {
+ const currentSlope = GetSlope(aGuessT, mX1, mX2)
+ if (currentSlope == 0.0) return aGuessT
+ const currentX = CalcBezier(aGuessT, mX1, mX2) - aX
+ aGuessT -= currentX / currentSlope
+ }
+ return aGuessT
+ }
+
+ return function (aX) {
+ if (mX1 == mY1 && mX2 == mY2) return aX // linear
+ return CalcBezier(aX, mY1, mY2)
+ }
+ }
+}
+
+export default oktween
diff --git a/animism-align/frontend/app/views/paragraph/components/paragraph.list.js b/animism-align/frontend/app/views/paragraph/components/paragraph.list.js
index 2cc54a0..3b3baaf 100644
--- a/animism-align/frontend/app/views/paragraph/components/paragraph.list.js
+++ b/animism-align/frontend/app/views/paragraph/components/paragraph.list.js
@@ -76,8 +76,8 @@ class ParagraphList extends Component {
onDoubleClick={onParagraphDoubleClick}
/>
)
- } else {
- return <div key={paragraph.id}>{'(' + capitalize(paragraph.type) + ')'}</div>
+ // } else {
+ // return <div key={paragraph.id}>{'(' + capitalize(paragraph.type) + ')'}</div>
}
})
}
diff --git a/animism-align/frontend/app/views/viewer/player/components.media/media.vitrine.js b/animism-align/frontend/app/views/viewer/player/components.media/media.vitrine.js
index 2033465..4fc3964 100644
--- a/animism-align/frontend/app/views/viewer/player/components.media/media.vitrine.js
+++ b/animism-align/frontend/app/views/viewer/player/components.media/media.vitrine.js
@@ -6,7 +6,7 @@ import actions from 'app/actions'
export const Vitrine = ({ media }) => {
const { image_order, image_lookup, thumbnail_lookup } = media.settings
- const width = (Math.floor(100 / image_order.length * 2) - 2) + 'vw'
+ const width = (Math.floor(100 / image_order.length * 2) - 2) + '%'
// console.log(width)
return (
<div className='vitrine-items'>
diff --git a/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.gallery.js b/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.gallery.js
index 6d6db2d..d21cc79 100644
--- a/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.gallery.js
+++ b/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.gallery.js
@@ -14,6 +14,9 @@ export const MediaGallery = ({ paragraph, media, currentParagraph, currentAnnota
return (
<div
className={className}
+ data-startts={paragraph.start_ts}
+ data-endts={paragraph.end_ts}
+ data-media={true}
onClick={e => onAnnotationClick(e, paragraph, annotation)}
>
{"["}
diff --git a/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.image.js b/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.image.js
index 64de3a2..6eef6a0 100644
--- a/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.image.js
+++ b/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.image.js
@@ -39,6 +39,9 @@ export const MediaImage = ({ paragraph, media, currentParagraph, currentAnnotati
return (
<div
className={className}
+ data-startts={paragraph.start_ts}
+ data-endts={paragraph.end_ts}
+ data-media={true}
onClick={e => onAnnotationClick(e, paragraph, annotation)}
>
{"["}
diff --git a/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.text.js b/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.text.js
index 292eec8..5ea12da 100644
--- a/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.text.js
+++ b/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.text.js
@@ -9,6 +9,8 @@ export const Paragraph = ({ paragraph, currentParagraph, currentAnnotation, onAn
return (
<div
className={className}
+ data-startts={paragraph.start_ts}
+ data-endts={paragraph.end_ts}
>
{paragraph.annotations.map(annotation => (
<span
@@ -29,6 +31,8 @@ export const ParagraphHeading = ({ paragraph, currentParagraph, currentAnnotatio
return (
<div
className={className}
+ data-startts={paragraph.start_ts}
+ data-endts={paragraph.end_ts}
onClick={e => onAnnotationClick(e, paragraph, firstAnnotation)}
>
<span>{ROMAN_NUMERALS[paragraph.sectionIndex]}{'. '}{text}</span>
diff --git a/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.video.js b/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.video.js
index 423a055..1c1b3cc 100644
--- a/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.video.js
+++ b/animism-align/frontend/app/views/viewer/transcript/components/elementTypes.video.js
@@ -14,6 +14,9 @@ export const MediaVideo = ({ paragraph, media, currentParagraph, currentAnnotati
return (
<div
className={className}
+ data-startts={paragraph.start_ts}
+ data-endts={paragraph.end_ts}
+ data-media={true}
onClick={e => onAnnotationClick(e, paragraph, annotation)}
>
{"["}
diff --git a/animism-align/frontend/app/views/viewer/transcript/transcript.container.js b/animism-align/frontend/app/views/viewer/transcript/transcript.container.js
index 2dd8402..3cc23bd 100644
--- a/animism-align/frontend/app/views/viewer/transcript/transcript.container.js
+++ b/animism-align/frontend/app/views/viewer/transcript/transcript.container.js
@@ -5,24 +5,107 @@ import { connect } from 'react-redux'
import actions from 'app/actions'
+import { toArray, floatInRange } from 'app/utils'
+import oktween from 'app/utils/oktween'
import ParagraphList from 'app/views/paragraph/components/paragraph.list'
import { transcriptElementLookup } from './components'
class Transcript extends Component {
+ state = {
+ built: false,
+ paragraphs: [],
+ windowHeight: 0.0,
+ scrolling: false,
+ }
+
constructor(props){
super(props)
this.handleClose = this.handleClose.bind(this)
this.handleAnnotationClick = this.handleAnnotationClick.bind(this)
this.handleParagraphDoubleClick = this.handleParagraphDoubleClick.bind(this)
+ this.containerRef = React.createRef()
}
componentDidMount() {
actions.transcript.buildAllParagraphs()
+ this.setState({
+ built: true,
+ windowHeight: window.innerHeight * 0.6 - 48,
+ })
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (this.state.built !== prevState.built) {
+ setTimeout(() => {
+ this.cacheScrollPositions()
+ }, 1000)
+ }
+ if (!this.state.scrolling) {
+ this.updateScrollPosition(this.props.play_ts)
+ }
+ }
+
+ cacheScrollPositions() {
+ let paragraphs = toArray(this.containerRef.current.querySelectorAll('.content > div')).map(el => {
+ const isHeading = el.classList.contains('section_heading')
+ const isMedia = el.dataset.media === 'true'
+ const scrollTop = isHeading ? el.offsetTop : el.offsetTop - 16 // 1.5rem
+ const start_ts = parseFloat(el.dataset.startts)
+ let end_ts = parseFloat(el.dataset.endts)
+ if (isHeading) console.log(scrollTop, start_ts, end_ts)
+ if (!start_ts || !end_ts) return null
+ if (end_ts < start_ts) {
+ end_ts = start_ts + 0.5
+ }
+ return { start_ts, end_ts, scrollTop }
+ }).filter(el => !!el)
+ this.setState({ paragraphs })
+ }
+
+ updateScrollPosition(play_ts, forceScroll) {
+ const { paragraphs, windowHeight, currentParagraph } = this.state
+ const scrollTop = this.containerRef.current.scrollTop
+ if (!forceScroll && currentParagraph && floatInRange(currentParagraph.start_ts, play_ts, currentParagraph.end_ts)) return
+ let nextParagraph;
+ const insideParagraph = paragraphs.some(paragraph => {
+ if (floatInRange(paragraph.start_ts, play_ts, paragraph.end_ts)) {
+ nextParagraph = paragraph
+ return true
+ }
+ return false
+ })
+ if (insideParagraph && nextParagraph) {
+ console.log(nextParagraph.scrollTop)
+ if (!floatInRange(scrollTop, nextParagraph.scrollTop, scrollTop + windowHeight) || forceScroll) {
+ this.setState({ currentParagraph: nextParagraph, scrolling: true })
+ this.scrollToParagraph(scrollTop, nextParagraph.scrollTop)
+ } else {
+ this.setState({ currentParagraph: nextParagraph })
+ }
+ }
+ }
+
+ scrollToParagraph(scrollFrom, scrollTo) {
+ if (this.state.scrolling) return
+ console.log('scrolling!', scrollFrom, scrollTo)
+ oktween.add({
+ from: { scrollTop: scrollFrom },
+ to: { scrollTop: scrollTo },
+ duration: 1000,
+ easing: oktween.easing.quad_in_out,
+ update: obj => {
+ this.containerRef.current.scrollTo(0, obj.scrollTop)
+ },
+ finished: tween => {
+ this.containerRef.current.scrollTo(0, tween.obj.scrollTop)
+ this.setState({ scrolling: false })
+ }
+ })
}
handleAnnotationClick(e, paragraph, annotation) {
- // console.log(annotation)
actions.viewer.seekToTimestamp(paragraph.start_ts)
+ this.updateScrollPosition(paragraph.start_ts + 0.1, annotation.type === 'section_heading')
}
handleParagraphDoubleClick(e, paragraph) {
@@ -37,7 +120,7 @@ class Transcript extends Component {
const { viewer, paragraphs } = this.props
return (
<div className="transcript">
- <div className='content'>
+ <div className='content' ref={this.containerRef}>
<ParagraphList
paragraphs={paragraphs}
paragraphElementLookup={transcriptElementLookup}
@@ -59,6 +142,7 @@ class Transcript extends Component {
const mapStateToProps = state => ({
viewer: state.viewer,
+ play_ts: state.audio.play_ts,
paragraphs: state.paragraph.paragraphs,
})