first pass at pandora-scroll element

This commit is contained in:
j 2022-09-02 13:54:53 +02:00
parent a334dc6b4f
commit a1eec24f17
5 changed files with 337 additions and 22 deletions

View file

@ -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;
}
}

View file

@ -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";

View file

@ -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)
}
}
}
}

View file

@ -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<this.attributes.length; i++) {
var a = this.attributes[i]
config[a.name] = a.value
if (a.name == 'layers') {
config[a.name] = a.value.split(' ')
} else if (['in', 'out'].indexOf(a.name) > -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('&amp;', '&').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
}

View file

@ -33,6 +33,7 @@
{% if text.data.view == 'player' %}
<script src="/static/js/player.js?2021112223"></script>
{% else %}
<script src="/static/js/pandora-scroll.js?2021112223"></script>
<script src="/static/js/ascroll.js?2021112223"></script>
{% endif %}
{% endblock %}