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/text";
|
||||
@import "partials/ascroll";
|
||||
@import "partials/pandora_scroll";
|
||||
@import "partials/player";
|
||||
@import "partials/about";
|
||||
@import "partials/contact";
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
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' %}
|
||||
<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 %}
|
||||
|
|
Loading…
Reference in a new issue