diff options
Diffstat (limited to 'animism-align/frontend/app')
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, }) |
