From a1eec24f176f9f6609c1f5db22d1bd7350bfc011 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 2 Sep 2022 13:54:53 +0200 Subject: [PATCH] first pass at pandora-scroll element --- app/static/css/partials/_pandora_scroll.scss | 125 +++++++++++++++++ app/static/css/style.scss | 1 + app/static/js/ascroll.js | 96 ++++++++++--- app/static/js/pandora-scroll.js | 136 +++++++++++++++++++ app/templates/text.html | 1 + 5 files changed, 337 insertions(+), 22 deletions(-) create mode 100644 app/static/css/partials/_pandora_scroll.scss create mode 100644 app/static/js/pandora-scroll.js diff --git a/app/static/css/partials/_pandora_scroll.scss b/app/static/css/partials/_pandora_scroll.scss new file mode 100644 index 0000000..84023bc --- /dev/null +++ b/app/static/css/partials/_pandora_scroll.scss @@ -0,0 +1,125 @@ +.pandora-scroll.player { + iframe { + width: 100%; + min-height: calc(100vh - 64px); + } +} +.pandora-scroll { + font-family: "noto_sans"; + width: 100%; + color: #ddd; + a { + color: var(--color-link); + text-decoration: none; + } + + h1 { + padding-left: var(--spacing-2); + padding-right: var(--spacing-2); + padding-top: 32px; + font-size: 20px; + letter-spacing: 1px; + font-weight: bold; + max-width: var(--container-width); + margin: auto; + } + h2 { + padding: 4px var(--spacing-2); + max-width: var(--container-width); + margin: auto; + } + .intro { + padding: 4px var(--spacing-2) 64px; + max-width: var(--container-width); + margin: auto; + } + .vbox { + max-width: var(--container-width); + margin: auto; + background: red; + } + .player { + position: absolute; + display: none; + width: 100%; + height: auto; + max-width: var(--container-width); + max-height: 88vh; + margin: auto; + //transition: opacity 0.4s; + + } + .annotation { + //min-height: 100vh; + .frame { + max-width: var(--container-width); + margin: auto; + figure { + text-align: center; + } + img { + width: 100%; + height: auto; + max-height: 88vh; + margin: auto; + object-fit: contain; + } + figcaption { + text-align: right; + padding-right: 4px; + font-size: 14px; + } + } + .text { + max-width: var(--container-width); + margin: auto; + padding: 20px 20px; + font-size: 20px; + line-height: 24px; + + a { + word-wrap: anywhere; + } + + figure { + width: 100%; + text-align: center; + } + + img { + margin: auto; + max-width: 100%; + } + } + } + + .annotation-icon-wrapper { + border: 3px solid rgba(#fff, 0.8); + border-radius: 50%; + background: var(--gradient-body); + background-size: 300% 100%; + width: 52px; + height: 52px; + display: flex; + align-items: center; + justify-content: center; + animation: background_animation 30s ease-in-out infinite; + opacity: 0.9; + + &:hover, &:focus { + opacity: 1; + } + } + + .annotation-icon { + color: rgba(#fff, 1); + font-size: 38px; + } + + .related-film { + padding: calc(var(--spacing) * 4) var(--spacing-2); + max-width: var(--container-width); + margin: auto; + line-height: 1.4; + } +} diff --git a/app/static/css/style.scss b/app/static/css/style.scss index 2daecc0..221d109 100755 --- a/app/static/css/style.scss +++ b/app/static/css/style.scss @@ -7,6 +7,7 @@ @import "partials/film"; @import "partials/text"; @import "partials/ascroll"; +@import "partials/pandora_scroll"; @import "partials/player"; @import "partials/about"; @import "partials/contact"; diff --git a/app/static/js/ascroll.js b/app/static/js/ascroll.js index b9a49f0..d67f7a1 100755 --- a/app/static/js/ascroll.js +++ b/app/static/js/ascroll.js @@ -29,10 +29,17 @@ function resize() { } } -function updatePlayer(video, frame, currentTime, out, src) { - var rect = frame.getBoundingClientRect(); +function updatePlayer(video, frame, currentTime, out, src, config) { + var top, rect = frame.getBoundingClientRect(); video.style.opacity = 0 - video.style.top = (rect.top + window.scrollY) + 'px' + if (config.root) { + var root_rect = config.root.getBoundingClientRect(); + //console.log('rect.top', rect.top, 'window.scrollY', window.scrollY, 'root_rect.top', root_rect.top) + top = rect.top - root_rect.top + } else { + top = rect.top + window.scrollY + } + video.style.top = top + 'px' video.style.display = 'block'; if (src) { setVideoSrc(video, src) @@ -129,16 +136,22 @@ function timeupdate(event) { function formatInfo(config, ascroll) { var info = document.createElement('div') - var h1 = document.createElement('h1') - h1.innerHTML = config.title - info.appendChild(h1) - var h2 = document.createElement('h2') - h2.innerHTML = config.byline - info.appendChild(h2) - var div = document.createElement('div') - div.classList.add('intro') - div.innerHTML = config.body - info.appendChild(div) + if (config.title) { + var h1 = document.createElement('h1') + h1.innerHTML = config.title + info.appendChild(h1) + } + if (config.byline) { + var h2 = document.createElement('h2') + h2.innerHTML = config.byline + info.appendChild(h2) + } + if (config.body) { + var div = document.createElement('div') + div.classList.add('intro') + div.innerHTML = config.body + info.appendChild(div) + } ascroll.appendChild(info) return info } @@ -170,7 +183,9 @@ function showOverlay(event) { overlay.innerHTML = on } overlay.addEventListener('click', event=> { - video.muted = !video.muted + const muted = video.muted + document.querySelectorAll('pandora-scroll').forEach(el => el.mute() ) + video.muted = !muted if (video.muted) { overlay.innerHTML = off } else { @@ -193,6 +208,7 @@ function showOverlay(event) { } function renderAnnotation(config, video, ascroll, clip) { + console.log("renderAnnotation", clip) var div = document.createElement('div') div.classList.add('annotation') var annotation = clip.annotations[0] @@ -231,14 +247,15 @@ function renderAnnotation(config, video, ascroll, clip) { src = `${streamPrefix}/${annotation.id.split('/')[0]}/480p.webm` } if (config.loaded && visible) { - updatePlayer(video, frame, annotation['in'], annotation['out'], src) + updatePlayer(video, frame, annotation['in'], annotation['out'], src, config) } })) } function renderAnnotations(config) { - var ascroll = document.querySelector('#ascroll') + 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') video.classList.add('player') @@ -270,14 +287,27 @@ function renderAnnotations(config) { if (config.edit) { src = `${streamPrefix}/${config.first.id.split('/')[0]}/480p.webm` } - updatePlayer(video, frame, config.first['in'], config.first['out'], src) + updatePlayer(video, frame, config.first['in'], config.first['out'], src, 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: [ @@ -319,23 +349,45 @@ async function loadClips(annotations) { } function loadAnnotations(config) { - var layers = config.layer + var layers = config.layer || config.layers || [] if (!Array.isArray(layers)) { layers = [layers] } - if (config.item) { + if (config.item && !layers.length) { + // load player only view + config.annotations = [ + { + "in": config["in"], + "out": config["out"], + "id": config["item"] + "/" + config["in"].toFixed(3) + '-'+ config.out.toFixed(3), + "annotations": [{ + "id": config["item"] + "/" + config["in"].toFixed(3) + '-'+ config.out.toFixed(3), + "value": "", + "in": config["in"], + "out": config["out"], + }], + "color1": "", + "color2": "", + } + ] + renderAnnotations(config) + } else if (config.item) { pandoraAPI('get', {id: config.item, keys: [ 'layers' ]}).then(response => { var annotations = [] layers.forEach(layer => { - response.data.layers[layer].forEach(annotation => { - if (!(config.user && annotation.user != config.user)) { + if (!response.data.layers[layer]) { + console.log("ERROR", config.item, layer, "missing", config.root) + } + response.data.layers[layer] && response.data.layers[layer].forEach(annotation => { + if (!(config.user && annotation.user != config.user) && isInside(config, annotation)) { annotations.push(annotation) } }) }) loadClips(annotations).then(annotations => { + console.log('got', annotations) config.annotations = annotations.filter(annotation => { if (config.only_e) { if (annotation.value.slice(0, 2) == 'E:') { @@ -394,7 +446,7 @@ function loadAnnotations(config) { config.annotations = [] } renderAnnotations(config) - } + } } } diff --git a/app/static/js/pandora-scroll.js b/app/static/js/pandora-scroll.js new file mode 100644 index 0000000..8cd50fc --- /dev/null +++ b/app/static/js/pandora-scroll.js @@ -0,0 +1,136 @@ +class PandoraScroll extends HTMLElement { + constructor() { + super() + + if (this.attributes.src && this.attributes.src.value) { + const attributes = parseIframeURL(this.attributes.src.value) + Object.keys(attributes).forEach(key => { + if (key[0] != '_') { + this.setAttribute(key, attributes[key]) + } + }) + } + const shadow = this.attachShadow({mode: 'open'}) + const link = document.createElement('link') + link.rel = 'stylesheet' + link.href = document.head.querySelector('link[rel="stylesheet"]').href + const style = document.createElement('style') + const body = document.createElement('div') + body.classList.add('pandora-scroll') + shadow.appendChild(link) + shadow.appendChild(style) + shadow.appendChild(body) + this.mute = function() { + shadow.querySelectorAll('video').forEach(video => { + video.muted = true + }) + } + } + + + 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.'); + } + + 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 + } + } + return config + } + _loadAnnotations() { + var config = this._config() + config.root = this.shadowRoot.querySelector('.pandora-scroll') + console.log(config) + loadAnnotations(config) + } +} + +customElements.define("pandora-scroll", PandoraScroll); + + + +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/', '/') + value = value.replace('/editor/', '/') + var data = value.split('#embed?') + function decodeValue(value) { + return decodeURIComponent(value) + .replace(/_/g, ' ').replace(/\t/g, '_') + .replace(/\x0E/g, '<').replace(/\x0F/g, '>'); + } + var args = data[1].replace('&', '&').split('&').map(kv => { + kv = kv.split('=') + console.log(kv, decodeValue(kv[1])) + return [kv[0], JSON.parse(decodeValue(kv[1]))] + }).reduce((k, v) => { + k[v[0]] = v[1] + return k + }, {}) + var parts = data[0].split('/') + config.item = parts[3] + var ts = parts[4].split(',') + config['in'] = ts[0] + config['out'] = ts[1] + config._args = args + if (args.showLayers) { + config.layers = args.showLayers.join(' ') + } + if (args.showTimeline) { + config.timeline = "slitscan" + } + return config +} diff --git a/app/templates/text.html b/app/templates/text.html index 8e1b4f0..e096ee6 100644 --- a/app/templates/text.html +++ b/app/templates/text.html @@ -33,6 +33,7 @@ {% if text.data.view == 'player' %} {% else %} + {% endif %} {% endblock %}