648 lines
21 KiB
JavaScript
648 lines
21 KiB
JavaScript
import { getId } from './utils.js'
|
|
import { DOMScatter } from './scatter.js'
|
|
import { InteractionMapper } from './interaction.js'
|
|
|
|
export class CardLoader {
|
|
constructor(
|
|
src,
|
|
{
|
|
x = 0,
|
|
y = 0,
|
|
width = 1000,
|
|
height = 800,
|
|
maxWidth = null,
|
|
maxHeight = null,
|
|
scale = 1,
|
|
minScale = 0.5,
|
|
maxScale = 1.5,
|
|
rotation = 0
|
|
} = {}
|
|
) {
|
|
this.src = src
|
|
this.x = x
|
|
this.y = y
|
|
this.scale = scale
|
|
this.rotation = 0
|
|
this.maxScale = maxScale
|
|
this.minScale = minScale
|
|
this.wantedWidth = width
|
|
this.wantedHeight = height
|
|
this.maxWidth = maxWidth != null ? maxWidth : window.innerWidth
|
|
this.maxHeight = maxHeight != null ? maxHeight : window.innerHeight
|
|
this.addedNode = null
|
|
console.log({
|
|
|
|
width,
|
|
height,
|
|
maxWidth,
|
|
maxHeight,
|
|
|
|
})
|
|
}
|
|
|
|
unload() {
|
|
if (this.addedNode) {
|
|
this.addedNode.remove()
|
|
this.addedNode = null
|
|
}
|
|
}
|
|
}
|
|
|
|
export class PDFLoader extends CardLoader {
|
|
constructor(src, { width = 1640, height = 800, scale = 1 } = {}) {
|
|
super(src, { width, height, scale })
|
|
if (typeof PDFJS == 'undefined') {
|
|
alert('PDF.js needed')
|
|
}
|
|
}
|
|
|
|
load(domNode) {
|
|
return new Promise((resolve, reject) => {
|
|
PDFJS.getDocument(this.src).then(pdf => {
|
|
pdf.getPage(1).then(page => {
|
|
let scale = this.scale * app.renderer.resolution
|
|
let invScale = 1 / scale
|
|
let viewport = page.getViewport(scale)
|
|
|
|
// Prepare canvas using PDF page dimensions.
|
|
let canvas = document.createElement('canvas')
|
|
let context = canvas.getContext('2d')
|
|
canvas.height = viewport.height
|
|
canvas.width = viewport.width
|
|
|
|
// Render PDF page into canvas context.
|
|
let renderContext = {
|
|
canvasContext: context,
|
|
viewport: viewport
|
|
}
|
|
page.render(renderContext)
|
|
domNode.appendChild(canvas)
|
|
this.wantedWidth = canvas.width
|
|
this.wantedHeight = canvas.height
|
|
this.scale = invScale
|
|
this.addedNode = canvas
|
|
resolve(this)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
export class ImageLoader extends CardLoader {
|
|
load(domNode) {
|
|
return new Promise((resolve, reject) => {
|
|
let isImage = domNode instanceof HTMLImageElement
|
|
let image = isImage ? domNode : document.createElement('img')
|
|
image.onload = e => {
|
|
if (!isImage) {
|
|
domNode.appendChild(image)
|
|
this.addedNode = image
|
|
}
|
|
this.wantedWidth = image.naturalWidth
|
|
this.wantedHeight = image.naturalHeight
|
|
|
|
let scaleW = this.maxWidth / image.naturalWidth
|
|
let scaleH = this.maxHeight / image.naturalHeight
|
|
this.scale = Math.min(this.maxScale, Math.min(scaleW, scaleH))
|
|
image.setAttribute('draggable', false)
|
|
image.width = image.naturalWidth
|
|
image.height = image.naturalHeight
|
|
resolve(this)
|
|
}
|
|
image.onerror = e => {
|
|
reject(this)
|
|
}
|
|
image.src = this.src
|
|
})
|
|
}
|
|
}
|
|
|
|
export class FrameLoader extends CardLoader {
|
|
load(domNode) {
|
|
return new Promise((resolve, reject) => {
|
|
let isFrame = domNode instanceof HTMLIFrameElement
|
|
let iframe = isFrame ? domNode : document.createElement('iframe')
|
|
console.log('FrameLoader.load', isFrame, iframe, this.src)
|
|
iframe.frameBorder = 0
|
|
iframe.style.scrolling = false
|
|
iframe.width = this.wantedWidth
|
|
iframe.height = this.wantedHeight
|
|
if (!isFrame) {
|
|
// Unlike img onload is only triggered if the iframe is part of the DOM tree
|
|
domNode.appendChild(iframe)
|
|
this.addedNode = iframe
|
|
}
|
|
iframe.onload = e => {
|
|
resolve(this)
|
|
}
|
|
iframe.onerror = e => {
|
|
reject(this)
|
|
}
|
|
iframe.src = this.src
|
|
})
|
|
}
|
|
}
|
|
|
|
export class HTMLLoader extends CardLoader {
|
|
load(domNode) {
|
|
return new Promise((resolve, reject) => {
|
|
let xhr = new XMLHttpRequest()
|
|
xhr.open('GET', this.src, false)
|
|
xhr.onload = e => {
|
|
domNode.innerHTML = xhr.response
|
|
this.addedNode = domNode.firstElementChild
|
|
let { width, height } = this.size(this.addedNode)
|
|
console.log("HTMLLoader.load", { added: this.addedNode, width, height })
|
|
if (width)
|
|
this.wantedWidth = width || this.wantedWidth
|
|
if (height)
|
|
this.wantedHeight = height || this.wantedHeight
|
|
resolve(this)
|
|
}
|
|
xhr.onerror = e => {
|
|
reject(this)
|
|
}
|
|
xhr.send()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Tries to determine the size of the addedNode.
|
|
* Checks for explicit width and height style attributes.
|
|
*
|
|
* Overwrite this method if you want to extract values from other infos.
|
|
*
|
|
* @returns { width: int, height: int }
|
|
* @memberof HTMLLoader
|
|
*/
|
|
size(node) {
|
|
let width = parseInt(node.style.width) || null
|
|
let height = parseInt(node.style.height) || null
|
|
return { width, height }
|
|
}
|
|
}
|
|
|
|
export class DOMFlip {
|
|
constructor(
|
|
domScatterContainer,
|
|
flipTemplate,
|
|
frontLoader,
|
|
backLoader,
|
|
{
|
|
closeOnMinScale = false,
|
|
flipDuration = 1,
|
|
fadeDuration = 0.2,
|
|
overdoScaling = 1,
|
|
autoLoad = false,
|
|
center = null,
|
|
preloadBack = false,
|
|
translatable = true,
|
|
scalable = true,
|
|
rotatable = true,
|
|
onFront = null,
|
|
onBack = null,
|
|
onClose = null,
|
|
onUpdate = null,
|
|
onRemoved = null,
|
|
onLoaded = null
|
|
} = {}
|
|
) {
|
|
this.domScatterContainer = domScatterContainer
|
|
this.id = getId()
|
|
this.flipDuration = flipDuration
|
|
this.fadeDuration = fadeDuration
|
|
this.closeOnMinScale = closeOnMinScale
|
|
this.flipTemplate = flipTemplate
|
|
this.frontLoader = frontLoader
|
|
this.backLoader = backLoader
|
|
this.translatable = translatable
|
|
this.scalable = scalable
|
|
this.rotatable = rotatable
|
|
this.onFrontFlipped = onFront
|
|
this.onBackFlipped = onBack
|
|
this.onClose = onClose
|
|
this.onRemoved = onRemoved
|
|
this.onUpdate = onUpdate
|
|
this.onLoaded = onLoaded
|
|
this.center = center
|
|
this.preloadBack = preloadBack
|
|
this.overdoScaling = overdoScaling
|
|
if (autoLoad) {
|
|
this.load()
|
|
}
|
|
}
|
|
|
|
load() {
|
|
return new Promise((resolve, reject) => {
|
|
let t = this.flipTemplate
|
|
let dom = this.domScatterContainer.element
|
|
let wrapper = t.content.querySelector('.flipWrapper')
|
|
wrapper.id = this.id
|
|
let clone = document.importNode(t.content, true)
|
|
dom.appendChild(clone)
|
|
// We cannot use the document fragment itself because it
|
|
// is not part of the main dom tree. After the appendChild
|
|
// call we can access the new dom element by id
|
|
this.cardWrapper = dom.querySelector('#' + this.id)
|
|
let front = this.cardWrapper.querySelector('.front')
|
|
this.frontLoader.load(front).then(loader => {
|
|
this.frontLoaded(loader).then((obj) => {
|
|
if (this.onLoaded) this.onLoaded()
|
|
resolve(this)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
frontLoaded(loader) {
|
|
return new Promise((resolve, reject) => {
|
|
let scatter = new DOMScatter(
|
|
this.cardWrapper,
|
|
this.domScatterContainer,
|
|
{
|
|
x: loader.x,
|
|
y: loader.y,
|
|
startScale: loader.scale,
|
|
scale: loader.scale,
|
|
maxScale: loader.maxScale,
|
|
minScale: loader.minScale,
|
|
width: loader.wantedWidth,
|
|
height: loader.wantedHeight,
|
|
rotation: loader.rotation,
|
|
translatable: this.translatable,
|
|
scalable: this.scalable,
|
|
rotatable: this.rotatable,
|
|
overdoScaling: this.overdoScaling
|
|
}
|
|
)
|
|
|
|
if (this.center) {
|
|
scatter.centerAt(this.center)
|
|
}
|
|
|
|
if (this.closeOnMinScale) {
|
|
|
|
const removeOnMinScale = function () {
|
|
if (scatter.scale <= scatter.minScale) {
|
|
this.flippable.close()
|
|
|
|
// 'Disable' overdoscaling to avoid weird jumps on close.
|
|
scatter.minScale /= scatter.overdoScaling
|
|
scatter.overdoScaling = 1
|
|
|
|
//Remove callback
|
|
if (scatter.onTransform) {
|
|
let callbackIdx = scatter.onTransform.indexOf(removeOnMinScale)
|
|
scatter.onTransform.splice(callbackIdx, 1)
|
|
}
|
|
}
|
|
|
|
}.bind(this)
|
|
|
|
|
|
|
|
scatter.addTransformEventCallback(removeOnMinScale)
|
|
}
|
|
|
|
let flippable = new DOMFlippable(this.cardWrapper, scatter, this)
|
|
let back = this.cardWrapper.querySelector('.back')
|
|
|
|
if (this.preloadBack) {
|
|
this.backLoader.load(back).then(loader => {
|
|
this.setupFlippable(flippable, loader)
|
|
})
|
|
}
|
|
this.flippable = flippable
|
|
resolve(this)
|
|
})
|
|
}
|
|
|
|
centerAt(p) {
|
|
this.center = p
|
|
this.flippable.centerAt(p)
|
|
}
|
|
|
|
zoom(scale) {
|
|
this.flippable.zoom(scale)
|
|
}
|
|
|
|
setupFlippable(flippable, loader) {
|
|
flippable.wantedWidth = loader.wantedWidth
|
|
flippable.wantedHeight = loader.wantedHeight
|
|
flippable.wantedScale = loader.scale
|
|
flippable.minScale = loader.minScale
|
|
flippable.maxScale = loader.maxScale
|
|
flippable.scaleButtons()
|
|
}
|
|
|
|
start({ targetCenter = null } = {}) {
|
|
console.log('DOMFlip.start', targetCenter)
|
|
if (this.preloadBack) this.flippable.start({ duration: this.flipDuration, targetCenter })
|
|
else {
|
|
let back = this.cardWrapper.querySelector('.back')
|
|
let flippable = this.flippable
|
|
this.backLoader.load(back).then(loader => {
|
|
this.setupFlippable(flippable, loader)
|
|
flippable.start({ duration: this.flipDuration, targetCenter })
|
|
})
|
|
}
|
|
}
|
|
|
|
fadeOutAndRemove() {
|
|
TweenLite.to(this.cardWrapper, this.fadeDuration, {
|
|
opacity: 0,
|
|
onComplete: () => {
|
|
this.cardWrapper.remove()
|
|
}
|
|
})
|
|
}
|
|
|
|
closed() {
|
|
this.unload()
|
|
}
|
|
|
|
unload() {
|
|
if (!this.preloadBack) {
|
|
this.backLoader.unload()
|
|
}
|
|
}
|
|
}
|
|
|
|
export class DOMFlippable {
|
|
constructor(element, scatter, flip) {
|
|
// Set log to console.log or a custom log function
|
|
// define data structures to store our touchpoints in
|
|
|
|
this.element = element
|
|
this.flip = flip
|
|
this.card = element.querySelector('.flipCard')
|
|
this.front = element.querySelector('.front')
|
|
this.back = element.querySelector('.back')
|
|
this.flipped = false
|
|
this.scatter = scatter
|
|
this.onFrontFlipped = flip.onFrontFlipped
|
|
this.onBackFlipped = flip.onBackFlipped
|
|
this.onClose = flip.onClose
|
|
this.onRemoved = flip.onRemoved
|
|
this.onUpdate = flip.onUpdate
|
|
|
|
this.flipDuration = flip.flipDuration
|
|
this.fadeDuration = flip.fadeDuration
|
|
scatter.addTransformEventCallback(this.scatterTransformed.bind(this))
|
|
console.log('lib.DOMFlippable', 5000)
|
|
TweenLite.set(this.element, { perspective: 5000 })
|
|
TweenLite.set(this.card, { transformStyle: 'preserve-3d' })
|
|
TweenLite.set(this.back, { rotationY: -180 })
|
|
TweenLite.set([this.back, this.front], {
|
|
backfaceVisibility: 'hidden',
|
|
perspective: 5000
|
|
})
|
|
TweenLite.set(this.front, { visibility: 'visible' })
|
|
this.infoBtn = element.querySelector('.infoBtn')
|
|
this.backBtn = element.querySelector('.backBtn')
|
|
this.closeBtn = element.querySelector('.closeBtn')
|
|
/* Buttons are not guaranteed to exist. */
|
|
if (this.infoBtn) {
|
|
InteractionMapper.on('tap', this.infoBtn, event => this.flip.start())
|
|
this.enable(this.infoBtn)
|
|
}
|
|
if (this.backBtn) {
|
|
InteractionMapper.on('tap', this.backBtn, event => this.start())
|
|
}
|
|
if (this.closeBtn) {
|
|
InteractionMapper.on('tap', this.closeBtn, event => this.close())
|
|
this.enable(this.closeBtn)
|
|
}
|
|
this.scaleButtons()
|
|
this.bringToFront()
|
|
}
|
|
|
|
close() {
|
|
this.disable(this.infoBtn)
|
|
this.disable(this.closeBtn)
|
|
if (this.onClose) {
|
|
this.onClose(this)
|
|
this.flip.closed()
|
|
} else {
|
|
this.scatter.zoom(0.1, {
|
|
animate: this.fadeDuration,
|
|
onComplete: () => {
|
|
this.element.remove()
|
|
this.flip.closed()
|
|
if (this.onRemoved) {
|
|
this.onRemoved.call(this)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
showFront() {
|
|
TweenLite.set(this.front, { visibility: 'visible' })
|
|
}
|
|
|
|
centerAt(p) {
|
|
this.scatter.centerAt(p)
|
|
}
|
|
|
|
zoom(scale) {
|
|
this.scatter.zoom(scale)
|
|
}
|
|
|
|
get buttonScale() {
|
|
let iscale = 1.0
|
|
|
|
if (this.scatter != null) {
|
|
let scale = this.scatter.scale || 1
|
|
iscale = 1.0 / scale
|
|
}
|
|
return iscale
|
|
}
|
|
|
|
scaleButtons() {
|
|
//This also works for svgs.
|
|
// if (this.infoBtn)
|
|
// this.infoBtn.style.transform = "scale(" + this.buttonScale + ")"
|
|
|
|
// if (this.backBtn)
|
|
// this.backBtn.style.transform = "scale(" + this.buttonScale + ")"
|
|
|
|
// if (this.closeBtn)
|
|
// this.closeBtn.style.transform = "scale(" + this.buttonScale + ")"
|
|
|
|
console.log(this.buttonScale)
|
|
//// This did not work with svgs!
|
|
TweenLite.set([this.infoBtn, this.backBtn, this.closeBtn], {
|
|
scale: this.buttonScale
|
|
})
|
|
}
|
|
|
|
bringToFront() {
|
|
this.scatter.bringToFront()
|
|
TweenLite.set(this.element, { zIndex: DOMScatter.zIndex++ })
|
|
}
|
|
|
|
clickInfo() {
|
|
this.bringToFront()
|
|
this.infoBtn.click()
|
|
}
|
|
|
|
scatterTransformed(event) {
|
|
this.scaleButtons()
|
|
}
|
|
|
|
targetRotation(alpha) {
|
|
let ortho = 90
|
|
let rest = alpha % ortho
|
|
let delta = 0.0
|
|
if (rest > ortho / 2.0) {
|
|
delta = ortho - rest
|
|
} else {
|
|
delta = -rest
|
|
}
|
|
return delta
|
|
}
|
|
|
|
infoValues(info) {
|
|
let startX = this.element._gsTransform.x
|
|
let startY = this.element._gsTransform.y
|
|
let startAngle = this.element._gsTransform.rotation
|
|
let startScale = this.element._gsTransform.scaleX
|
|
let w = this.element.style.width
|
|
let h = this.element.style.height
|
|
console.log(info, startX, startY, startAngle, startScale, w, h)
|
|
}
|
|
|
|
show(element, duration = 0, alpha = 1) {
|
|
if (element) {
|
|
TweenLite.to(element, duration, { autoAlpha: alpha }) // visibility: 'visible', display: 'initial'})
|
|
}
|
|
}
|
|
|
|
hide(element, duration = 0, alpha = 0) {
|
|
if (element) {
|
|
TweenLite.to(element, duration, { autoAlpha: alpha }) // {visibility: 'hidden', display: 'none'})
|
|
}
|
|
}
|
|
|
|
|
|
|
|
enable(button) {
|
|
this.show(button, this.fadeDuration)
|
|
if (button) {
|
|
TweenLite.set(button, { pointerEvents: 'auto' })
|
|
}
|
|
}
|
|
|
|
disable(button) {
|
|
this.hide(button, this.fadeDuration)
|
|
if (button) {
|
|
TweenLite.set(button, { pointerEvents: 'none' })
|
|
}
|
|
}
|
|
|
|
start({ targetCenter = null } = {}) {
|
|
this.bringToFront()
|
|
if (!this.flipped) {
|
|
this.startX = this.element._gsTransform.x
|
|
this.startY = this.element._gsTransform.y
|
|
this.startAngle = this.element._gsTransform.rotation
|
|
this.startScale = this.element._gsTransform.scaleX
|
|
this.startWidth = this.element.style.width
|
|
this.startHeight = this.element.style.height
|
|
this.scatterStartWidth = this.scatter.width
|
|
this.scatterStartHeight = this.scatter.height
|
|
this.show(this.back)
|
|
this.disable(this.infoBtn)
|
|
this.disable(this.closeBtn)
|
|
} else {
|
|
this.show(this.front, this.fadeDuration)
|
|
this.disable(this.backBtn)
|
|
}
|
|
let { scalable, translatable, rotatable } = this.scatter
|
|
this.saved = { scalable, translatable, rotatable }
|
|
this.scatter.scalable = false
|
|
this.scatter.translatable = false
|
|
this.scatter.rotatable = false
|
|
this.scatter.killAnimation()
|
|
|
|
this.flipped = !this.flipped
|
|
let targetY = this.flipped ? 180 : 0
|
|
let targetZ = this.flipped
|
|
? this.startAngle + this.targetRotation(this.startAngle)
|
|
: this.startAngle
|
|
let targetScale = this.flipped ? this.wantedScale : this.startScale
|
|
let w = this.flipped ? this.wantedWidth : this.startWidth
|
|
let h = this.flipped ? this.wantedHeight : this.startHeight
|
|
let dw = this.wantedWidth - this.scatter.width
|
|
let dh = this.wantedHeight - this.scatter.height
|
|
let tc = targetCenter
|
|
let xx = tc != null ? tc.x - w / 2 : this.startX - dw / 2
|
|
let yy = tc != null ? tc.y - h / 2 : this.startY - dh / 2
|
|
let x = this.flipped ? xx : this.startX
|
|
let y = this.flipped ? yy : this.startY
|
|
|
|
console.log("DOMFlippable.start", this.flipped, targetCenter, x, y, this.saved)
|
|
let onUpdate = this.onUpdate !== null ? () => this.onUpdate(this) : null
|
|
console.log(this.flipDuration)
|
|
TweenLite.to(this.card, this.flipDuration, {
|
|
rotationY: targetY,
|
|
ease: Power1.easeOut,
|
|
transformOrigin: '50% 50%',
|
|
onUpdate,
|
|
onComplete: e => {
|
|
if (this.flipped) {
|
|
//this.hide(this.front)
|
|
this.enable(this.backBtn)
|
|
this.show(this.backBtn)
|
|
|
|
if (this.onFrontFlipped) {
|
|
this.onFrontFlipped(this)
|
|
}
|
|
} else {
|
|
|
|
if (this.onBackFlipped == null) {
|
|
this.enable(this.infoBtn, this.fadeDuration)
|
|
this.enable(this.closeBtn, this.fadeDuration)
|
|
} else {
|
|
this.onBackFlipped(this)
|
|
}
|
|
this.flip.unload()
|
|
}
|
|
this.scatter.scale = targetScale
|
|
this.scaleButtons()
|
|
this.scatter.rotationDegrees = targetZ
|
|
this.scatter.width = this.flipped ? w : this.scatterStartWidth
|
|
this.scatter.height = this.flipped ? h : this.scatterStartHeight
|
|
|
|
let { scalable, translatable, rotatable } = this.saved
|
|
this.scatter.scalable = scalable
|
|
this.scatter.translatable = translatable
|
|
this.scatter.rotatable = rotatable
|
|
},
|
|
force3D: true
|
|
})
|
|
|
|
// See https://greensock.com/forums/topic/7997-rotate-the-shortest-way/
|
|
TweenLite.to(this.element, this.flipDuration / 2, {
|
|
scale: targetScale,
|
|
ease: Power1.easeOut,
|
|
rotationZ: targetZ + '_short',
|
|
transformOrigin: '50% 50%',
|
|
width: w,
|
|
height: h,
|
|
x: x,
|
|
y: y,
|
|
onComplete: e => {
|
|
if (this.flipped) {
|
|
this.hide(this.front)
|
|
// this.hide(this.infoBtn)
|
|
} else {
|
|
this.hide(this.back)
|
|
// this.show(this.infoBtn)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|