iwmlib/lib/card/highlight.js

386 lines
13 KiB
JavaScript
Raw Normal View History

2019-07-12 14:33:15 +02:00
/* eslint-disable no-console */
let _HighlightEnabled = true
let _CircleIds = 0
/** Helper method to round values with one digit precision */
function round(value) {
return Math.round(parseFloat(value) * 10) / 10
}
/**
* A namespace with static functions to expand and shrink highlighted image regions.
* Assumes an SVG image with the following structure:
2019-07-18 12:26:39 +02:00
*
2019-07-12 14:33:15 +02:00
* <svg viewbox="0 0 100 100">
* <!-- The defs section must be defined and cannot be generated in JavaScript -->
* <defs>
* </defs>
* <image width="100" height="100" xlink:href="../assets/chess.jpg"/>
* <circle onclick="Highlight.animateZoom(event)" cx="47" cy="18" r="8" stroke-width="0.5" />
* <circle onclick="Highlight.animateZoom(event)" cx="60" cy="67" r="8" stroke-width="0.5" />
* </svg>
2019-07-18 12:26:39 +02:00
*
2019-07-12 14:33:15 +02:00
* The SVG root element should use a viewbox with 0 0 100 100 to ensure that the positions and size of the
* circles can be represented in percent.
2019-07-18 12:26:39 +02:00
*
2019-07-12 14:33:15 +02:00
* @class Highlight
* @extends {Object}
*/
2020-02-07 20:08:44 +01:00
const SCALE = 1.5
2019-07-16 09:27:48 +02:00
export default class Highlight extends Object {
2019-07-12 14:33:15 +02:00
static disableAnimations() {
_HighlightEnabled = false
let expanded = document.querySelectorAll('.' + this.expandedClass)
2019-07-12 14:33:15 +02:00
for (let obj of expanded) {
this.shrink(obj)
}
}
static enableAnimations() {
_HighlightEnabled = true
}
static removeAnimations(svgRoot) {
let expanded = svgRoot.querySelectorAll('.' + this.expandedClass)
2019-07-12 14:33:15 +02:00
for (let obj of expanded) {
TweenLite.set(obj, { scale: 1 })
obj.classList.remove('zooming')
obj.classList.remove(this.expandedClass)
2019-07-12 14:33:15 +02:00
}
let defs = svgRoot.querySelector('defs')
while (defs.firstChild) {
defs.firstChild.remove()
}
let maskImages = svgRoot.querySelectorAll('.addedImage')
for (let m of maskImages) {
m.remove()
}
let circles = svgRoot.querySelectorAll('circle')
for (let circle of circles) {
if (circle.classList.length == 0) {
circle.removeAttribute('class')
}
2019-07-30 16:56:29 +02:00
if (circle.hasAttribute('id') && circle.getAttribute('id').startsWith('@@')) {
2019-07-12 14:33:15 +02:00
circle.removeAttribute('id')
}
circle.removeAttribute('data-svg-origin')
circle.removeAttribute('transform')
circle.removeAttribute('style')
let cx = circle.getAttribute('cx')
let cy = circle.getAttribute('cy')
let r = circle.getAttribute('r')
circle.setAttribute('cx', round(cx))
circle.setAttribute('cy', round(cy))
circle.setAttribute('r', round(r))
}
}
2020-02-07 20:08:44 +01:00
static expand(obj, { scale = SCALE, duration = 3, stroke = 2, onComplete = null } = {}) {
2019-07-18 12:26:39 +02:00
if (obj == null) return
//console.log("expand")
2019-07-12 14:33:15 +02:00
obj.classList.add('zooming')
TweenLite.to(obj, duration, {
2020-02-07 20:08:44 +01:00
scale,
2019-07-12 14:33:15 +02:00
onUpdate: () => {
let scale = obj._gsTransform.scaleX
obj.setAttribute('stroke-width', stroke / scale)
},
onComplete: () => {
console.log('expand complete')
obj.classList.remove('zooming')
obj.classList.add(this.expandedClass)
2019-07-12 14:33:15 +02:00
obj.setAttribute('stroke-width', stroke / scale)
if (onComplete) onComplete()
}
})
}
static shrink(obj, { duration = 0.5, stroke = 2 } = {}) {
//console.log("shrink")
2019-07-18 12:26:39 +02:00
if (obj == null) return
2019-07-12 14:33:15 +02:00
obj.classList.add('zooming')
TweenLite.to(obj, duration, {
scale: 1,
onUpdate: () => {
let scale = obj._gsTransform.scaleX
obj.setAttribute('stroke-width', stroke / scale)
},
onComplete: () => {
//console.log("shrink complete")
obj.classList.remove('zooming')
obj.classList.remove(this.expandedClass)
2019-07-12 14:33:15 +02:00
obj.setAttribute('stroke-width', stroke)
}
})
}
static animateCircle(target, callback) {
console.log('ANIMATE CIRCLE', this)
// ** DEBUG OUTPUTS **
2019-07-15 14:42:01 +02:00
2019-07-12 14:33:15 +02:00
let circle = target
// We need a unique id to ensure correspondence between circle, mask, and maskImage
2019-07-12 14:33:15 +02:00
if (!circle.hasAttribute('id')) {
_CircleIds += 1
circle.setAttribute('id', '@@' + _CircleIds)
}
let id = circle.getAttribute('id')
TweenLite.set(circle, { transformOrigin: '50% 50%' })
/*if (circle.classList.contains('zooming')) {
2020-02-07 20:08:44 +01:00
console.log("already zooming")
return
}*/
2019-07-12 14:33:15 +02:00
let svgRoot = circle.closest('svg')
let circleGroup = circle.parentNode
let image = svgRoot.querySelector('image')
let stroke = parseFloat(circleGroup.getAttribute('stroke-width') || 6)
let defs = svgRoot.querySelector('defs')
if (defs == null) {
defs = document.createElementNS(svgRoot, 'defs')
svgRoot.insertBefore(defs, image)
}
// // We need direct children, therefore we cannot use querySelectorAll
let maskImageId = 'maskImage' + id
let maskImage = svgRoot.getElementById(maskImageId)
if (circle.classList.contains(this.expandedClass)) {
2019-07-12 14:33:15 +02:00
if (!circle.classList.contains('zooming')) {
this.shrink(circle, { stroke })
this.shrink(maskImage, { stroke })
return
}
2019-07-18 12:26:39 +02:00
//console.log("animate called while zooming out -> expand")
} else if (circle.classList.contains('zooming')) {
2019-07-12 14:33:15 +02:00
//console.log("animate called while zooming in -> shrink")
this.shrink(circle, { stroke })
this.shrink(maskImage, { stroke })
return
}
2019-07-30 16:56:29 +02:00
let circles = Array.from(circleGroup.children).filter(e => e.tagName == 'circle')
2019-07-12 14:33:15 +02:00
for (let c of circles) {
//console.log("shrinking all circles")
this.shrink(c, { stroke })
}
let maskImages = circleGroup.querySelectorAll('.addedImage')
for (let m of maskImages) {
this.shrink(m, { stroke })
}
this._createSVGMask(svgRoot, image, id)
2019-07-12 14:33:15 +02:00
// TweenLite.set(maskImage, { transformOrigin: `${tx}% ${ty}%` })
this.expand(circle, { stroke, onComplete: callback })
this.expand(maskImage)
return false
}
2020-02-07 20:08:44 +01:00
static openHighlight(target, { animation = 0.5, scale = SCALE, onExpanded = null } = {}) {
if (this._isExpanded(target)) {
2019-07-12 14:33:15 +02:00
console.log('Target is already expanded!')
return
} else {
let targetId = target.getAttribute('id')
2019-07-15 14:42:01 +02:00
if (targetId && targetId.startsWith('@@')) {
2019-07-12 14:33:15 +02:00
let id = targetId.slice(2)
2019-07-15 14:42:01 +02:00
const imageId = '#maskImage' + id
2019-07-12 14:33:15 +02:00
const parent = target.parentNode
2019-07-15 14:42:01 +02:00
if (parent != null) {
2019-07-12 14:33:15 +02:00
let image = parent.querySelector(imageId)
2019-07-15 14:42:01 +02:00
if (image) {
2019-07-12 14:33:15 +02:00
this._bringToFront(image)
2019-07-30 16:56:29 +02:00
} else console.error('Could not find corresponding image element.')
2019-07-15 14:42:01 +02:00
} else console.log('Element was no parent:', target)
2019-07-12 14:33:15 +02:00
}
this._bringToFront(target)
let svgRoot = target.closest('svg')
2019-07-21 21:42:03 +02:00
if (svgRoot == null) {
return
}
2019-07-12 14:33:15 +02:00
let image = svgRoot.querySelector('image')
2019-07-24 12:45:55 +02:00
2019-07-12 14:33:15 +02:00
// eslint-disable-next-line no-unused-vars
let [mask, maskImage] = this._getSVGMask(target, {
2019-07-18 12:26:39 +02:00
svgRoot,
image
})
let center = this._calculateCenterRelativeTo(target, image)
2019-07-24 12:45:55 +02:00
console.log('_calculateCenterRelativeTo', center)
2019-07-18 12:26:39 +02:00
TweenLite.set(maskImage, {
2019-07-24 11:45:03 +02:00
transformOrigin: `${center.x} ${center.y}`
2019-07-18 12:26:39 +02:00
})
2019-07-12 14:33:15 +02:00
TweenLite.set(target, { transformOrigin: '50% 50%' })
TweenLite.to([target, maskImage], animation, {
scale,
onComplete: onExpanded
})
target.classList.add(this.expandedClass)
2019-07-12 14:33:15 +02:00
}
2019-07-15 14:42:01 +02:00
}
2019-07-12 14:33:15 +02:00
static toggleHighlight(node, options = {}) {
console.log('toggleHighlight', this._isExpanded(node))
if (this._isExpanded(node)) {
this.closeHighlight(node, options)
2019-07-18 12:26:39 +02:00
} else {
this.openHighlight(node, options)
2019-07-15 14:42:01 +02:00
}
2019-07-12 14:33:15 +02:00
}
2019-07-15 14:42:01 +02:00
static _bringToFront(target) {
2019-07-12 14:33:15 +02:00
const parent = target.parentNode
2019-07-15 14:42:01 +02:00
if (target && parent) {
2019-07-12 14:33:15 +02:00
parent.removeChild(target)
parent.appendChild(target)
2019-07-30 16:56:29 +02:00
} else console.error('Could not bring to front. Either no target or no parent.', target, parent)
2019-07-12 14:33:15 +02:00
}
static _getSVGMask(circle, { svgRoot = null, image = null } = {}) {
const id = this._retrieveId(circle)
const maskId = 'mask' + id
const maskImageId = 'maskImage' + id
if (!svgRoot) svgRoot = circle.closest('svg')
let mask = svgRoot.getElementById(maskId)
let maskImage = svgRoot.getElementById(maskImageId)
if (!mask || !maskImage)
[mask, maskImage] = this._createSVGMask(circle, {
2019-07-18 12:26:39 +02:00
svgRoot,
image,
id
})
2019-07-12 14:33:15 +02:00
return [mask, maskImage]
}
/**
* Creates an SVG mask for a provided svgElement.
*
* @static
* @param {SVGElement} element - Element that should be masked.
* @param {object} opts - Optional parameters to avoid unnecessary fetching of elements.
* @param {SVGElement} opts.svgRoot - The root <svg> element of the element.
* @param {SVGImageElement} opts.image - The image that is used in the mask.
* @param {number} opts.id - The id of the mask.
* @returns
* @memberof Highlight
*/
2019-07-30 16:56:29 +02:00
static _createSVGMask(element, { svgRoot = null, image = null, id = null } = {}) {
2019-07-12 14:33:15 +02:00
// We can fetch these values here, but it's more efficient to
// simply pass them in, as it's likely they were already retrieved beforehand.
if (svgRoot == null) svgRoot = element.closest('svg')
if (image == null) image = svgRoot.querySelector('image')
if (id == null) id = this._retrieveId(element)
let svg = 'http://www.w3.org/2000/svg'
let xlink = 'http://www.w3.org/1999/xlink'
let svgGroup = element.parentNode
let src = image.getAttributeNS(xlink, 'href')
let maskId = 'mask' + id
let maskImageId = 'maskImage' + id
let mask = svgRoot.getElementById(maskId)
let maskImage = svgRoot.getElementById(maskImageId)
let defs = svgRoot.querySelector('defs')
if (defs == null) {
defs = document.createElementNS(svgRoot, 'defs')
svgRoot.insertBefore(defs, image)
}
if (mask == null) {
mask = document.createElementNS(svg, 'mask')
mask.setAttribute('id', maskId)
let maskCircle = element.cloneNode(true)
mask.appendChild(maskCircle)
defs.appendChild(mask)
}
let bbox = svgRoot.getElementsByTagName('image')[0].getBBox()
let width = bbox.width
let height = bbox.height
if (maskImage == null) {
maskImage = document.createElementNS(svg, 'image')
maskImage.style.pointerEvents = 'none'
maskImage.setAttribute('id', maskImageId)
maskImage.setAttributeNS(xlink, 'href', src)
maskImage.setAttribute('width', width)
maskImage.setAttribute('height', height)
maskImage.setAttribute('class', 'addedImage')
svgGroup.insertBefore(maskImage, element) // image.nextSibling)
TweenLite.set(maskImage, { scale: 1 })
maskImage.style.mask = 'url(#' + maskId + ')'
}
svgGroup.appendChild(maskImage)
// svgGroup.appendChild(element)
2019-07-12 14:33:15 +02:00
return [mask, maskImage]
}
static _calculateCenterRelativeTo(target, image) {
let bbox = image.getBBox()
let width = bbox.width
let height = bbox.height
let cx = target.getAttribute('cx')
let cy = target.getAttribute('cy')
2019-07-24 11:45:03 +02:00
let x = cx.endsWith('%') ? cx : round((cx / width) * 100) + '%'
let y = cy.endsWith('%') ? cy : round((cy / height) * 100) + '%'
return { x, y }
2019-07-12 14:33:15 +02:00
}
static _isExpanded(target) {
return target.classList.contains(this.expandedClass)
2019-07-12 14:33:15 +02:00
}
static closeHighlight(target, { animation = 0.5 } = {}) {
target.classList.remove(this.expandedClass)
// eslint-disable-next-line no-unused-vars
let [mask, maskImage] = this._getSVGMask(target)
// console.log('Close Highlight', maskImage)
2019-07-12 14:33:15 +02:00
TweenLite.to([target, maskImage], animation, {
scale: 1
})
}
static animate(event) {
2019-07-18 12:26:39 +02:00
if (!_HighlightEnabled) return
2019-07-12 14:33:15 +02:00
event.stopPropagation()
this.animateCircle(event.target)
2019-07-12 14:33:15 +02:00
return false
}
static _retrieveId(target) {
let id = target.getAttribute('id')
// We need a unique id to ensure correspondence between circle, mask, and maskImage
2019-07-12 14:33:15 +02:00
if (!id) {
_CircleIds += 1
target.setAttribute('id', '@@' + _CircleIds)
id = _CircleIds
} else {
id = parseInt(id.substring(2))
}
return id
}
}
Highlight.expandedClass = 'expanded'