diff --git a/app/static/css/partials/_ascroll.scss b/app/static/css/partials/_ascroll.scss index 1b30add..ea40843 100755 --- a/app/static/css/partials/_ascroll.scss +++ b/app/static/css/partials/_ascroll.scss @@ -4,6 +4,9 @@ min-height: calc(100vh - 64px); } } +#video-overlay { + z-index: 11; +} #ascroll { font-family: "noto_sans"; width: 100%; @@ -46,8 +49,18 @@ max-height: 88vh; margin: auto; //transition: opacity 0.4s; + z-index: 10; } + .left { + float: left; + padding-right: 1vw; + width: 30%; + } + .clearpara { + clear: left; + padding-top: 1vh; + } .annotation { min-height: 100vh; .frame { diff --git a/app/static/js/ascroll.js b/app/static/js/ascroll.js index d67f7a1..ae90fff 100755 --- a/app/static/js/ascroll.js +++ b/app/static/js/ascroll.js @@ -1,35 +1,22 @@ var layer = 'keywords' var imageResolution = 480 -var videoExtension - -function setVideoSrc(video, src) { - var ext - if (!videoExtension) { - [ - ['video/mp4; codecs="avc1.42E01E, mp4a.40.2"', '.mp4'], - ['video/webm; codecs="vp8, vorbis"', '.webm'], - ].forEach(opt => { - if (videoExtension) { return } - if (video.canPlayType(opt[0]).replace('no', '')) { - videoExtension = opt[1] - } - }) - } - src = src.replace('.webm', videoExtension) - if (src != video.src) { - video.src = src - } -} function resize() { var video = document.querySelector('video') if (video && video._frame) { - var rect = video._frame.getBoundingClientRect(); - video.style.top = (rect.top + window.scrollY) + 'px' + var top, rect = video._frame.getBoundingClientRect(); + if (video._root) { + var root_rect = video._root.getBoundingClientRect(); + top = rect.top - root_rect.top + } else { + top = rect.top + window.scrollY + } + video.style.top = top + 'px' } } function updatePlayer(video, frame, currentTime, out, src, config) { + //console.log('update player', currentTime, out, src) var top, rect = frame.getBoundingClientRect(); video.style.opacity = 0 if (config.root) { @@ -41,6 +28,7 @@ function updatePlayer(video, frame, currentTime, out, src, config) { } video.style.top = top + 'px' video.style.display = 'block'; + video.style.maxWidth = rect.width + 'px' if (src) { setVideoSrc(video, src) } @@ -50,6 +38,7 @@ function updatePlayer(video, frame, currentTime, out, src, config) { video.currentTime = currentTime video.dataset.in = currentTime video.dataset.out = out + video._root = config.root video._frame = frame const show = event => { video.style.opacity = 1 @@ -60,51 +49,6 @@ function updatePlayer(video, frame, currentTime, out, src, config) { video.play() } -function isElementInViewport (el) { - var rect = el.getBoundingClientRect(); - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */ - Math.floor(rect.right) <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */ - ); -} - -function onVisibilityChange(el, callback) { - var old_visible; - return function () { - var visible = isElementInViewport(el); - if (visible != old_visible) { - old_visible = visible; - if (visible) { - el.classList.add('visible') - } else { - el.classList.remove('visible') - } - if (typeof callback == 'function') { - callback(visible); - } - } - } -} - -function scrollTo(element) { - var delta = element.offsetTop - document.scrollingElement.scrollTop, - duration = 1000, t = 40, n = duration / t, - step = delta / n; - - function scroll() { - if (document.scrollingElement.scrollTop + step > element.offsetTop) { - document.scrollingElement.scrollTop = element.offsetTop - n = 0 - } else { - document.scrollingElement.scrollTop += step - } - n-- - if (n) setTimeout(scroll, t) - } - scroll() -} function timeupdate(event) { if (event.target.dataset.out && event.target.dataset.in) { @@ -156,59 +100,7 @@ function formatInfo(config, ascroll) { return info } -function showOverlay(event) { - event.stopPropagation() - event.preventDefault() - document.querySelectorAll('#video-overlay').forEach(element => element.remove()) - var video = event.target - var rect = video.getBoundingClientRect(); - var overlay = document.createElement('div') - overlay.id = 'video-overlay' - overlay.style.top = video.style.top - overlay.style.width = rect.width + 'px' - overlay.style.height = rect.height + 'px' - overlay.style.position = 'absolute' - overlay.style.display = 'flex' - overlay.style.alignItems = 'center' - overlay.style.justifyContent = 'center' - //overlay.style.fontSize = '45px' - video.controls = false - - var off = `` - var on = `` - - if (video.muted) { - overlay.innerHTML = off - } else { - overlay.innerHTML = on - } - overlay.addEventListener('click', event=> { - const muted = video.muted - document.querySelectorAll('pandora-scroll').forEach(el => el.mute() ) - video.muted = !muted - if (video.muted) { - overlay.innerHTML = off - } else { - overlay.innerHTML = on - } - }) - var timeout = setTimeout(() => { - video.controls = false - overlay.remove() - }, 3000) - overlay.addEventListener('mousemove', event=> { - clearTimeout(timeout) - timeout = setTimeout(() => { - video.controls = false - overlay.remove() - }, 500) - }) - video.parentElement.appendChild(overlay) - -} - -function renderAnnotation(config, video, ascroll, clip) { - console.log("renderAnnotation", clip) +function renderAnnotation(config, video, root, clip) { var div = document.createElement('div') div.classList.add('annotation') var annotation = clip.annotations[0] @@ -239,22 +131,23 @@ function renderAnnotation(config, video, ascroll, clip) {
${values}
` - ascroll.appendChild(div) + root.appendChild(div) var frame = div.querySelector('.frame') document.addEventListener('scroll', onVisibilityChange(div.querySelector('.frame'), function(visible) { var src - if (config.edit) { + if (clip.src) { + src = clip.src + } else if (config.edit) { src = `${streamPrefix}/${annotation.id.split('/')[0]}/480p.webm` } if (config.loaded && visible) { updatePlayer(video, frame, annotation['in'], annotation['out'], src, config) } - })) } function renderAnnotations(config) { - console.log('renderAnnotations', config.item, config.annotations.length) + //console.log('renderAnnotations', config.item, config.annotations.length) var ascroll = config.root ? config.root : document.querySelector('#ascroll') config.loaded = false var video = document.createElement('video') @@ -292,22 +185,9 @@ function renderAnnotations(config) { } } -function isInside(config, annotation) { - if (!config['in'] && !config['out']) { - return true - } - if (annotation['in'] < config['out'] && annotation['out'] > config['in']) { - annotation['in'] = Math.min(annotation['in'], config['in']) - annotation['out'] = Math.min(annotation['out'], config['out']) - return true - } - return false -} - async function loadClips(annotations) { var items = annotations.map(annotation => annotation.id.split('/')[0]) items = [...new Set(items)] - console.log('loadClips', annotations, items) return pandoraAPI('findClips', {itemsQuery: { conditions: [{key: 'id', operator: '&', value: items}] }, range: [0, 10000], keys: [ @@ -348,7 +228,7 @@ async function loadClips(annotations) { }) } -function loadAnnotations(config) { +async function loadAnnotations(config) { var layers = config.layer || config.layers || [] if (!Array.isArray(layers)) { layers = [layers] @@ -370,9 +250,9 @@ function loadAnnotations(config) { "color2": "", } ] - renderAnnotations(config) + return config } else if (config.item) { - pandoraAPI('get', {id: config.item, keys: [ + return pandoraAPI('get', {id: config.item, keys: [ 'layers' ]}).then(response => { var annotations = [] @@ -386,8 +266,7 @@ function loadAnnotations(config) { } }) }) - loadClips(annotations).then(annotations => { - console.log('got', annotations) + return loadClips(annotations).then(annotations => { config.annotations = annotations.filter(annotation => { if (config.only_e) { if (annotation.value.slice(0, 2) == 'E:') { @@ -400,13 +279,13 @@ function loadAnnotations(config) { return annotation } }) - renderAnnotations(config) + return config }) }) } else { var cited = {} if (config.edit) { - pandoraAPI('getEdit', {id: config.edit, keys: []}).then(response => { + return pandoraAPI('getEdit', {id: config.edit, keys: []}).then(response => { var annotations = [] response.data.clips.forEach(clip => { cited[clip.item] = { @@ -435,17 +314,17 @@ function loadAnnotations(config) { }) }) }) - loadClips(annotations).then(annotations => { + return loadClips(annotations).then(annotations => { config.annotations = annotations config.cited = Object.values(cited) - renderAnnotations(config) + return config }) }) } else { if (!config.annotations) { config.annotations = [] } - renderAnnotations(config) + return config } } } @@ -455,7 +334,7 @@ config.layer = config.layer || layer if (false && config.annotations) { renderAnnotations(config) } else { - loadAnnotations(config) + loadAnnotations(config).then(renderAnnotations) } window.addEventListener('resize', resize, false); diff --git a/app/static/js/pandora-scroll.js b/app/static/js/pandora-scroll.js index 8cd50fc..562ab09 100644 --- a/app/static/js/pandora-scroll.js +++ b/app/static/js/pandora-scroll.js @@ -20,6 +20,10 @@ class PandoraScroll extends HTMLElement { shadow.appendChild(link) shadow.appendChild(style) shadow.appendChild(body) + window.addEventListener('resize', () => { + this._resize() + }, false) + this.mute = function() { shadow.querySelectorAll('video').forEach(video => { video.muted = true @@ -28,80 +32,92 @@ class PandoraScroll extends HTMLElement { } - static get observedAttributes() { return ['muted']; } + static get observedAttributes() { return ['muted']; } - connectedCallback() { - console.log('scroll connected') - this._loadAnnotations() - this._updateStyle() - } - disconnectedCallback() { - console.log('scroll disconnected') - } - adoptedCallback() { - console.log('Custom square element moved to new page.'); - } + connectedCallback() { + this._loadAnnotations() + this._updateStyle() + } + disconnectedCallback() { + //console.log('scroll disconnected') + } + adoptedCallback() { + //console.log('element moved to new page.'); + } - attributeChangedCallback(name, oldValue, newValue) { - console.log('value changed', name, oldValue, newValue); - this._updateStyle() - } - _updateStyle() { - const shadow = this.shadowRoot; - const childNodes = shadow.childNodes; - for (const node of childNodes) { - if (node.nodeName === 'STYLE') { - node.textContent = ` - .pandora-scroll { - position: relative; - display: block; - } - ` - } - } - } - - _config() { - var config = {} - for (var i=0; i -1) { - config[a.name] = parseTime(a.value) - } else { - config[a.name] = a.value + attributeChangedCallback(name, oldValue, newValue) { + //console.log('value changed', name, oldValue, newValue); + this._updateStyle() + } + _updateStyle() { + const shadow = this.shadowRoot; + const childNodes = shadow.childNodes; + for (const node of childNodes) { + if (node.nodeName === 'STYLE') { + node.textContent = ` + .pandora-scroll { + position: relative; + display: block; + } + ` + } } } - return config - } - _loadAnnotations() { - var config = this._config() - config.root = this.shadowRoot.querySelector('.pandora-scroll') - console.log(config) - loadAnnotations(config) + + _config() { + var config = {} + for (var i=0; i -1) { + config[a.name] = parseTime(a.value) + } else { + config[a.name] = a.value + } + } + return config + } + _loadAnnotations() { + var config = this._config() + config.root = this.shadowRoot.querySelector('.pandora-scroll') + config.video = document.querySelector('video') + loadAnnotations(config).then(config => { + config.annotations.forEach(annotation => { + annotation.src = `${streamPrefix}/${annotation.id.split('/')[0]}/480p.webm` + renderAnnotation(window.config, config.video, config.root, annotation) + }) + }) + } + + _resize() { + var video = this.shadowRoot.querySelector('video') + if (video && video._frame) { + var rect = video._frame.getBoundingClientRect(), + root_rect = video._root.getBoundingClientRect(), + top = rect.top - root_rect.top; + video.style.top = top + 'px' + } + } +} + +class PandoraItem extends HTMLElement { + constructor() { + super() + } +} +class PandoraEdit extends HTMLElement { + constructor() { + super() } } customElements.define("pandora-scroll", PandoraScroll); +customElements.define("pandora-item", PandoraItem); +customElements.define("pandora-edit", PandoraEdit); - -function parseTime(value) { - return value.split(":").map(p => parseFloat(p)).reduce((c, p) => { return c*60 + p}, 0) -} - -function isInside(config, annotation) { - if (!config['in'] && !config['out']) { - return true - } - if (annotation['in'] < config['out'] && annotation['out'] > config['in']) { - return true - } - return false -} - function parseIframeURL(value) { var config = {} value = value.replace('/player/', '/') @@ -114,7 +130,7 @@ function parseIframeURL(value) { } var args = data[1].replace('&', '&').split('&').map(kv => { kv = kv.split('=') - console.log(kv, decodeValue(kv[1])) + //console.log(kv, decodeValue(kv[1])) return [kv[0], JSON.parse(decodeValue(kv[1]))] }).reduce((k, v) => { k[v[0]] = v[1] diff --git a/app/static/js/utils.js b/app/static/js/utils.js new file mode 100644 index 0000000..0ea5180 --- /dev/null +++ b/app/static/js/utils.js @@ -0,0 +1,138 @@ + +var videoExtension + +function setVideoSrc(video, src) { + var ext + if (!videoExtension) { + [ + ['video/mp4; codecs="avc1.42E01E, mp4a.40.2"', '.mp4'], + ['video/webm; codecs="vp8, vorbis"', '.webm'], + ].forEach(opt => { + if (videoExtension) { return } + if (video.canPlayType(opt[0]).replace('no', '')) { + videoExtension = opt[1] + } + }) + } + src = src.replace('.webm', videoExtension) + if (src != video.src) { + video.src = src + } +} + +function scrollTo(element) { + var delta = element.offsetTop - document.scrollingElement.scrollTop, + duration = 1000, t = 40, n = duration / t, + step = delta / n; + + function scroll() { + if (document.scrollingElement.scrollTop + step > element.offsetTop) { + document.scrollingElement.scrollTop = element.offsetTop + n = 0 + } else { + document.scrollingElement.scrollTop += step + } + n-- + if (n) setTimeout(scroll, t) + } + scroll() +} + +function showOverlay(event) { + console.log('show overlay', event.target, event) + event.stopPropagation() + event.preventDefault() + document.querySelectorAll('#video-overlay').forEach(element => element.remove()) + var video = event.target + var rect = video.getBoundingClientRect(); + if (video._frame) { + rect = video._frame.getBoundingClientRect(); + } + var overlay = document.createElement('div') + overlay.id = 'video-overlay' + overlay.style.top = video.style.top + overlay.style.width = rect.width + 'px' + overlay.style.height = rect.height + 'px' + overlay.style.position = 'absolute' + overlay.style.display = 'flex' + overlay.style.alignItems = 'center' + overlay.style.justifyContent = 'center' + //overlay.style.fontSize = '45px' + video.controls = false + + var off = `` + var on = `` + + if (video.muted) { + overlay.innerHTML = off + } else { + overlay.innerHTML = on + } + overlay.addEventListener('click', event=> { + const muted = video.muted + document.querySelectorAll('pandora-scroll').forEach(el => el.mute() ) + video.muted = !muted + if (video.muted) { + overlay.innerHTML = off + } else { + overlay.innerHTML = on + } + }) + var timeout = setTimeout(() => { + video.controls = false + overlay.remove() + }, 3000) + overlay.addEventListener('mousemove', event=> { + clearTimeout(timeout) + timeout = setTimeout(() => { + video.controls = false + overlay.remove() + }, 500) + }) + video.parentElement.appendChild(overlay) + +} + +function isInside(config, annotation) { + if (!config['in'] && !config['out']) { + return true + } + if (annotation['in'] < config['out'] && annotation['out'] > config['in']) { + annotation['in'] = Math.min(annotation['in'], config['in']) + annotation['out'] = Math.min(annotation['out'], config['out']) + return true + } + return false +} + +function isElementInViewport (el) { + var rect = el.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */ + Math.floor(rect.right) <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */ + ); +} + +function onVisibilityChange(el, callback) { + var old_visible; + return function () { + var visible = isElementInViewport(el); + if (visible != old_visible) { + old_visible = visible; + if (visible) { + el.classList.add('visible') + } else { + el.classList.remove('visible') + } + if (typeof callback == 'function') { + callback(visible); + } + } + } +} + +function parseTime(value) { + return value.split(":").map(p => parseFloat(p)).reduce((c, p) => { return c*60 + p}, 0) +} diff --git a/app/templates/text.html b/app/templates/text.html index e096ee6..375f571 100644 --- a/app/templates/text.html +++ b/app/templates/text.html @@ -33,6 +33,7 @@ {% if text.data.view == 'player' %} {% else %} + {% endif %}