/* 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:
*
*
*
* 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.
*
* @class Highlight
* @extends {Object}
*/
export default class Highlight extends Object {
static disableAnimations() {
_HighlightEnabled = false
let expanded = document.querySelectorAll('.' + Highlight.expandedClass)
for (let obj of expanded) {
this.shrink(obj)
}
}
static enableAnimations() {
_HighlightEnabled = true
}
static removeAnimations(svgRoot) {
let expanded = svgRoot.querySelectorAll('.' + Highlight.expandedClass)
for (let obj of expanded) {
TweenLite.set(obj, { scale: 1 })
obj.classList.remove('zooming')
obj.classList.remove(Highlight.expandedClass)
}
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')
}
if (circle.hasAttribute('id') && circle.getAttribute('id').startsWith('@@')) {
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))
}
}
static expand(obj, { scale = 2, duration = 3, stroke = 2, onComplete = null } = {}) {
if (obj == null) return
//console.log("expand")
obj.classList.add('zooming')
TweenLite.to(obj, duration, {
scale: scale,
onUpdate: () => {
let scale = obj._gsTransform.scaleX
obj.setAttribute('stroke-width', stroke / scale)
},
onComplete: () => {
console.log('expand complete')
obj.classList.remove('zooming')
obj.classList.add(Highlight.expandedClass)
obj.setAttribute('stroke-width', stroke / scale)
if (onComplete) onComplete()
}
})
}
static shrink(obj, { duration = 0.5, stroke = 2 } = {}) {
//console.log("shrink")
if (obj == null) return
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(Highlight.expandedClass)
obj.setAttribute('stroke-width', stroke)
}
})
}
static animateCircle(target, callback) {
console.log('ANIMATE CIRCLE', this)
// ** DEBUG OUTPUTS **
let circle = target
// We need a unique id to ensure correspondence between circle, mask, and maskImage
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')) {
console.log("already zooming")
return
}*/
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(Highlight.expandedClass)) {
if (!circle.classList.contains('zooming')) {
this.shrink(circle, { stroke })
this.shrink(maskImage, { stroke })
return
}
//console.log("animate called while zooming out -> expand")
} else if (circle.classList.contains('zooming')) {
//console.log("animate called while zooming in -> shrink")
this.shrink(circle, { stroke })
this.shrink(maskImage, { stroke })
return
}
let circles = Array.from(circleGroup.children).filter(e => e.tagName == 'circle')
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 })
}
Highlight._createSVGMask(svgRoot, image, id)
// TweenLite.set(maskImage, { transformOrigin: `${tx}% ${ty}%` })
this.expand(circle, { stroke, onComplete: callback })
this.expand(maskImage)
return false
}
static openHighlight(target, { animation = 0.5, scale = 2, onExpanded = null } = {}) {
if (Highlight._isExpanded(target)) {
console.log('Target is already expanded!')
return
} else {
let targetId = target.getAttribute('id')
if (targetId && targetId.startsWith('@@')) {
let id = targetId.slice(2)
const imageId = '#maskImage' + id
const parent = target.parentNode
if (parent != null) {
let image = parent.querySelector(imageId)
if (image) {
this._bringToFront(image)
} else console.error('Could not find corresponding image element.')
} else console.log('Element was no parent:', target)
}
this._bringToFront(target)
let svgRoot = target.closest('svg')
if (svgRoot == null) {
return
}
let image = svgRoot.querySelector('image')
// eslint-disable-next-line no-unused-vars
let [mask, maskImage] = Highlight._getSVGMask(target, {
svgRoot,
image
})
let center = Highlight._calculateCenterRelativeTo(target, image)
console.log('_calculateCenterRelativeTo', center)
TweenLite.set(maskImage, {
transformOrigin: `${center.x} ${center.y}`
})
TweenLite.set(target, { transformOrigin: '50% 50%' })
TweenLite.to([target, maskImage], animation, {
scale,
onComplete: onExpanded
})
target.classList.add(Highlight.expandedClass)
}
}
static toggleHighlight(node) {
console.log('toggleHighlight', Highlight._isExpanded(node))
if (Highlight._isExpanded(node)) {
Highlight.closeHighlight(node)
} else {
Highlight.openHighlight(node)
}
}
static _bringToFront(target) {
const parent = target.parentNode
if (target && parent) {
parent.removeChild(target)
parent.appendChild(target)
} else console.error('Could not bring to front. Either no target or no parent.', target, parent)
}
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] = Highlight._createSVGMask(circle, {
svgRoot,
image,
id
})
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