/* eslint-disable no-console */ /* global TweenLite */ 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 represnted in percent. * * @class Highlight * @extends {Object} */ class Highlight extends Object { static disableAnimations() { _HighlightEnabled = false let expanded = document.querySelectorAll('.expanded') for (let obj of expanded) { this.shrink(obj) } } static enableAnimations() { _HighlightEnabled = true } static removeAnimations(svgRoot) { let expanded = svgRoot.querySelectorAll('.expanded') for (let obj of expanded) { TweenLite.set(obj, { scale: 1 }) obj.classList.remove('zooming') obj.classList.remove('expanded') } 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('expanded') 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('expanded') 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('expanded')) { 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 } = {}) { console.log('Open Highlight!') if (Highlight._isExpanded(target)) { console.log('Target is already expanded!') return } else { let targetId = target.getAttribute('id') console.log(targetId) 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') 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) TweenLite.set(maskImage, { transformOrigin: `${center.x}% ${center.y}%` }) TweenLite.set(target, { transformOrigin: '50% 50%' }) TweenLite.to(target, 2, { }) TweenLite.to([target, maskImage], animation, { scale, onComplete: onExpanded }) target.classList.add('expanded') } return } 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 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 */ static _createSVGMask(element, { svgRoot = null, image = null, id = null } = {}) { // 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) 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') return { x: (cx / width) * 100, y: (cy / height) * 100 } } static _isExpanded(target) { return target.classList.contains(Highlight.expandedClass) } static _setExpanded(target) { target.classList.add(Highlight.expandedClass) } static _notExpanded(target) { target.classList.remove(Highlight.expandedClass) } static closeHighlight(target, { animation = 0.5 } = {}) { console.log('Close Highlight') Highlight._notExpanded(target) // eslint-disable-next-line no-unused-vars let [mask, maskImage] = Highlight._getSVGMask(target) TweenLite.to([target, maskImage], animation, { scale: 1 }) } static animate(event) { if (!_HighlightEnabled) return event.stopPropagation() Highlight.animateCircle(event.target) return false } static _retrieveId(target) { let id = target.getAttribute('id') // We need a unique id to ensure correspondence between circle, mask, and maskImage if (!id) { _CircleIds += 1 target.setAttribute('id', '@@' + _CircleIds) id = _CircleIds } else { id = parseInt(id.substring(2)) } return id } } Highlight.expandedClass = 'expanded' window.Highlight = Highlight