first pass at pandora-scroll element
This commit is contained in:
parent
a334dc6b4f
commit
a1eec24f17
5 changed files with 337 additions and 22 deletions
125
app/static/css/partials/_pandora_scroll.scss
Normal file
125
app/static/css/partials/_pandora_scroll.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
@import "partials/film";
|
@import "partials/film";
|
||||||
@import "partials/text";
|
@import "partials/text";
|
||||||
@import "partials/ascroll";
|
@import "partials/ascroll";
|
||||||
|
@import "partials/pandora_scroll";
|
||||||
@import "partials/player";
|
@import "partials/player";
|
||||||
@import "partials/about";
|
@import "partials/about";
|
||||||
@import "partials/contact";
|
@import "partials/contact";
|
||||||
|
|
|
@ -29,10 +29,17 @@ function resize() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePlayer(video, frame, currentTime, out, src) {
|
function updatePlayer(video, frame, currentTime, out, src, config) {
|
||||||
var rect = frame.getBoundingClientRect();
|
var top, rect = frame.getBoundingClientRect();
|
||||||
video.style.opacity = 0
|
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';
|
video.style.display = 'block';
|
||||||
if (src) {
|
if (src) {
|
||||||
setVideoSrc(video, src)
|
setVideoSrc(video, src)
|
||||||
|
@ -129,16 +136,22 @@ function timeupdate(event) {
|
||||||
|
|
||||||
function formatInfo(config, ascroll) {
|
function formatInfo(config, ascroll) {
|
||||||
var info = document.createElement('div')
|
var info = document.createElement('div')
|
||||||
var h1 = document.createElement('h1')
|
if (config.title) {
|
||||||
h1.innerHTML = config.title
|
var h1 = document.createElement('h1')
|
||||||
info.appendChild(h1)
|
h1.innerHTML = config.title
|
||||||
var h2 = document.createElement('h2')
|
info.appendChild(h1)
|
||||||
h2.innerHTML = config.byline
|
}
|
||||||
info.appendChild(h2)
|
if (config.byline) {
|
||||||
var div = document.createElement('div')
|
var h2 = document.createElement('h2')
|
||||||
div.classList.add('intro')
|
h2.innerHTML = config.byline
|
||||||
div.innerHTML = config.body
|
info.appendChild(h2)
|
||||||
info.appendChild(div)
|
}
|
||||||
|
if (config.body) {
|
||||||
|
var div = document.createElement('div')
|
||||||
|
div.classList.add('intro')
|
||||||
|
div.innerHTML = config.body
|
||||||
|
info.appendChild(div)
|
||||||
|
}
|
||||||
ascroll.appendChild(info)
|
ascroll.appendChild(info)
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
@ -170,7 +183,9 @@ function showOverlay(event) {
|
||||||
overlay.innerHTML = on
|
overlay.innerHTML = on
|
||||||
}
|
}
|
||||||
overlay.addEventListener('click', event=> {
|
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) {
|
if (video.muted) {
|
||||||
overlay.innerHTML = off
|
overlay.innerHTML = off
|
||||||
} else {
|
} else {
|
||||||
|
@ -193,6 +208,7 @@ function showOverlay(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAnnotation(config, video, ascroll, clip) {
|
function renderAnnotation(config, video, ascroll, clip) {
|
||||||
|
console.log("renderAnnotation", clip)
|
||||||
var div = document.createElement('div')
|
var div = document.createElement('div')
|
||||||
div.classList.add('annotation')
|
div.classList.add('annotation')
|
||||||
var annotation = clip.annotations[0]
|
var annotation = clip.annotations[0]
|
||||||
|
@ -231,14 +247,15 @@ function renderAnnotation(config, video, ascroll, clip) {
|
||||||
src = `${streamPrefix}/${annotation.id.split('/')[0]}/480p.webm`
|
src = `${streamPrefix}/${annotation.id.split('/')[0]}/480p.webm`
|
||||||
}
|
}
|
||||||
if (config.loaded && visible) {
|
if (config.loaded && visible) {
|
||||||
updatePlayer(video, frame, annotation['in'], annotation['out'], src)
|
updatePlayer(video, frame, annotation['in'], annotation['out'], src, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAnnotations(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
|
config.loaded = false
|
||||||
var video = document.createElement('video')
|
var video = document.createElement('video')
|
||||||
video.classList.add('player')
|
video.classList.add('player')
|
||||||
|
@ -270,14 +287,27 @@ function renderAnnotations(config) {
|
||||||
if (config.edit) {
|
if (config.edit) {
|
||||||
src = `${streamPrefix}/${config.first.id.split('/')[0]}/480p.webm`
|
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) {
|
async function loadClips(annotations) {
|
||||||
var items = annotations.map(annotation => annotation.id.split('/')[0])
|
var items = annotations.map(annotation => annotation.id.split('/')[0])
|
||||||
items = [...new Set(items)]
|
items = [...new Set(items)]
|
||||||
|
console.log('loadClips', annotations, items)
|
||||||
return pandoraAPI('findClips', {itemsQuery: {
|
return pandoraAPI('findClips', {itemsQuery: {
|
||||||
conditions: [{key: 'id', operator: '&', value: items}]
|
conditions: [{key: 'id', operator: '&', value: items}]
|
||||||
}, range: [0, 10000], keys: [
|
}, range: [0, 10000], keys: [
|
||||||
|
@ -319,23 +349,45 @@ async function loadClips(annotations) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadAnnotations(config) {
|
function loadAnnotations(config) {
|
||||||
var layers = config.layer
|
var layers = config.layer || config.layers || []
|
||||||
if (!Array.isArray(layers)) {
|
if (!Array.isArray(layers)) {
|
||||||
layers = [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: [
|
pandoraAPI('get', {id: config.item, keys: [
|
||||||
'layers'
|
'layers'
|
||||||
]}).then(response => {
|
]}).then(response => {
|
||||||
var annotations = []
|
var annotations = []
|
||||||
layers.forEach(layer => {
|
layers.forEach(layer => {
|
||||||
response.data.layers[layer].forEach(annotation => {
|
if (!response.data.layers[layer]) {
|
||||||
if (!(config.user && annotation.user != config.user)) {
|
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)
|
annotations.push(annotation)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
loadClips(annotations).then(annotations => {
|
loadClips(annotations).then(annotations => {
|
||||||
|
console.log('got', annotations)
|
||||||
config.annotations = annotations.filter(annotation => {
|
config.annotations = annotations.filter(annotation => {
|
||||||
if (config.only_e) {
|
if (config.only_e) {
|
||||||
if (annotation.value.slice(0, 2) == 'E:') {
|
if (annotation.value.slice(0, 2) == 'E:') {
|
||||||
|
@ -394,7 +446,7 @@ function loadAnnotations(config) {
|
||||||
config.annotations = []
|
config.annotations = []
|
||||||
}
|
}
|
||||||
renderAnnotations(config)
|
renderAnnotations(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
136
app/static/js/pandora-scroll.js
Normal file
136
app/static/js/pandora-scroll.js
Normal 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('&', '&').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
|
||||||
|
}
|
|
@ -33,6 +33,7 @@
|
||||||
{% if text.data.view == 'player' %}
|
{% if text.data.view == 'player' %}
|
||||||
<script src="/static/js/player.js?2021112223"></script>
|
<script src="/static/js/player.js?2021112223"></script>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<script src="/static/js/pandora-scroll.js?2021112223"></script>
|
||||||
<script src="/static/js/ascroll.js?2021112223"></script>
|
<script src="/static/js/ascroll.js?2021112223"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Reference in a new issue