import { Points } from '../../utils.js' export class ObjectUtils { /** * Finds the previous key in an object. * * @param {string} targetKey - The target key, of whom the predecessor is wanted. */ static findPredecessor(targetKey, obj) { // Initialize pointers ... let prev = null let first = null // ... and a hit flag. let hit = false //Iterate over all available items. for (let key in obj) if (obj.hasOwnProperty(key)) { if (!hit) { //Assign both values, //while not hit prev = first first = key //When hit, set flag. if (key == targetKey) { hit = true if (prev) return prev //If it was not hit on the first item, we can return prev. } } else { //Otherwise, when first item was hit, we iterate over the entire object. first = key } } return first } /** * Finds the successing key in an object. * * @param {string} targetKey - The key of whom the successor key should be found. * @param {object} obj - The object, that is scanned for the successor. */ static findSuccessor(targetKey, obj) { let first = null let next = false for (let key in obj) if (obj.hasOwnProperty(key)) { if (!first) first = key if (next) { return key } if (targetKey == key) { next = true } } return first } static fromPath(obj, path, separator = '.') { let arr = path.split(separator) let result = obj for (let i = 0; i < arr.length; i++) { if (result[arr[i]] !== null) { result = result[arr[i]] } else return null } return result } } export class PathUtils { static fixTrailingSlash(url) { return url.replace(/\/?$/, '/') } } export class TimeUtils { static minutesToMs(minutes) { return TimeUtils.minutesToSeconds(minutes) * 1000 } static minutesToSeconds(minutes) { return minutes * 60 } } export class StringUtils { /* Used from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript */ static capitalize(string) { return string.charAt(0).toUpperCase() + string.slice(1) } } export class GeometryUtils { static RandomRange(min, max) { return GeometryUtils.toRadians(Math.random() * (max - min) + min) } static RandomSign() { return Math.sign(Math.random() - 0.5) } static toDegrees(radians) { return (radians * 180) / Math.PI } static toRadians(degrees) { return (degrees / 180) * Math.PI } /* Recursion is buggy. */ static subdivide(polygon, factor = 0.5, n = 0) { console.log(polygon.length, factor) let points = [] for (let i = 0; i < polygon.length; i++) { let prev = i - 1 < 0 ? polygon[polygon.length - 1] : polygon[i - 1] let next = i + 1 < polygon.length ? polygon[i + 1] : polygon[0] let current = polygon[i] points.push(GeometryUtils.linearInterpolation(prev, current, factor)) points.push(GeometryUtils.linearInterpolation(current, next, 1 - factor)) } if (n > 0) { n-- points = GeometryUtils.subdivide(points, n) } return points } static linearInterpolation(first, second, n) { let point = new PIXI.Point() point.x = first.x * n + second.x * (1 - n) point.y = first.y * n + second.y * (1 - n) return point } } export class Matrix { constructor(a, b, d, e, c = 0, f = 0) { Object.assign(this, { a, b, c, d, e, f, g: 0, h: 0, i: 1 }) } static Transpose(m) { let first = ['b', 'c', 'f'] let second = ['d', 'g', 'h'] for (let i = 0; i < first.length; i++) { let tmp = m[first[i]] first[i] = second[i] second[i] = tmp } } static Rotation(deg) { let rad = (2 * Math.PI * deg) / 180 return new Matrix(Math.cos(rad), -Math.sin(rad), Math.sin(rad), Math.cos(rad)) } static Transform(x, y) { return new Matrix(1, 0, 0, 1, x, y) } static MultiplyPoint(m, p) { x = p.x * m.a + m.b * p.y + m.c * 1 y = p.x * m.d + p.y * m.e + m.f * 1 return { x, y } } } export class DomUtils { static getCenter(element) { let x = (element.clientLeft + element.clientWidth) / 2 let y = (element.clientTop + element.clientHeight) / 2 return { x, y } } static getTransformedPosition(element) { let mat = this.getCSSMatrix(element) return { x: mat[4], y: mat[5] } } static getCSSMatrix(element) { let matrix = window.getComputedStyle(element).transform if (matrix == 'none') { return [1, 0, 0, 1, 0, 0] } else { const pre = 'matrix(' let values = matrix.substring(pre.length, matrix.length - 1).split(',') for (let i in values) { values[i] = parseFloat(values[i]) } return values } } static positionOnElement(element, position) { const matrix = this.getCSSMatrix(element) const inX = element.offsetWidth * (position.x - 0.5) const inY = element.offsetHeight * (position.y - 0.5) let outX = matrix[0] * inX + matrix[2] * inY + matrix[4] let outY = matrix[1] * inX + matrix[3] * inY + matrix[5] let out = { x: outX, y: outY } return out } static applyTransform(target, transformedPosition, size = 0) { return { x: target.offsetLeft + target.offsetWidth / 2 + transformedPosition.x - size / 2, y: target.offsetTop + target.offsetHeight / 2 + transformedPosition.y - size / 2 } } } export class Vector { static length(vector) { return Math.sqrt(vector.x * vector.x + vector.y * vector.y) } static normalize(vector) { return Points.multiplyScalar(vector, 1 / this.length(vector)) } static scaleTo(vector, length) { let normalized = this.normalize(vector) return Points.multiplyScalar(normalized, length) } } export class PIXIUtils { /* * Transform a pixi text to it's actual screensize, * ignoring it's local transforms */ static toScreenFontSize(pixiText, fontSize = null) { pixiText._recursivePostUpdateTransform(); let normalizedScale = { x: pixiText.scale.x / pixiText.transform.worldTransform.a, y: pixiText.scale.x / pixiText.transform.worldTransform.d }; pixiText.scale = { x: normalizedScale.x, y: normalizedScale.y }; if (fontSize) pixiText.style.fontSize = fontSize; } static saveFill(graphics) { return { fill: graphics.fill.color, alpha: graphics.fill.alpha } } } /** * The EventHandler class is used to take care of a event based design * pattern. Callbacks can subscribe to an event and these unknown sources * get notified whenever the event changes. * * @export * @class EventHandler */ export class EventHandler { /** * Creates an instance of EventHandler. * @param {any} name * @param {any} [{ * listeners = [] - With the listnerers parameter the user can specify a function, array of functions or null (no function - useful when used in constructor with optional parameter). * }={}] * @memberof EventHandler */ constructor(name, { listeners = [] } = {}) { this.name = name this.listeners = [] this.onces = [] /** * One may initialize the eventListener using a parameter * that is either passed or null. */ if (listeners == null) { // Null is a valid value as the EventHandler assumes no listener is passed on purpose. // This is useful, when a default parameter is passed as null. } else if (Array.isArray(listeners)) this.listeners = listeners else if (typeof listeners == 'function') { this.listeners = [] this.add(listeners) } else { console.warn( "The provided 'listeners' is neither an Array of functions, nor a function. No eventcallback was added!", listeners, this ) } } addMultiple(...callbacks) { for (let callback of callbacks) { this.listeners.push(callback) } } add(callback) { this.listeners.push(callback) return callback } once(callback) { this.onces.push(callback) } remove(func) { for (const [idx, listener] of this.listeners.entries()) { if (listener === func) { this.listeners.splice(idx, 1) return true } } return false } empty() { this.listeners = [] } call(context, ...args) { if (context == null) { this.listeners.forEach(listener => listener(...args)) this.onces.forEach(listener => listener(...args)) } else { this.listeners.forEach(listener => listener.call(context, ...args)) this.onces.forEach(listener => listener.call(context, ...args)) } this.onces = [] } get length() { return this.listeners.length + this.onces.length } } export class Dom { /** * Popups should be displayed right over the text. * Normally we would expect the popup to appear right over * the center of the text. A problem in HTML is, that it's hard * to determine the position of a text link, when it has a line-break * in it. * * This function solves this problem in the (so far) only possible way. * * 1. It removes the link from the dom tree. * 2. It adds an empty copy A of the link to the dom tree. (Copy is important, as the same styles have to be applied.) * 3. The contents of the link are added one by one to A. * 4. If the resulting boundingRect is bigger than the previous one, a line break is detected. * 5. The old line is tested, if the point was inside that boundingBox. If so save that bounding box (Goto: 7), else: * 6. Saves the content to a preceding clone B. And repeats from 3. * 7. Replace A with the initial content * 8. Return the found BoundingBox. If none found. Return the last bounding box. */ static getTextHitRect(link, point) { // We cannot use it as it produces axis aligned bounding boxes /* if (true) { let rects = link.getClientRects() let target = null for (let [idx, rect] of Object.entries(rects)) { target = rect if (Rect.contains(rect, point)) break } return target } else {*/ let processedText = link.cloneNode(true) let content = processedText.innerHTML let words = content.split(/ /g) processedText.innerHTML = '' event.target.innerHTML = '' // let lineRect = event.target.getBoundingClientRect() let local = Points.fromPageToNode(event.target.parentNode, point) console.log(local) let target = event.target let height = 0 while (words.length > 0) { let word = words.pop() target.innerHTML += word + ' ' if (target.height != height) { // New line was reached. console.log('NEW LINE WAS REACHED!') } } return { top: 0, left: 0, right: 100, bottom: 100, width: 100, height: 100 } // let total = words.length // while (words.length > 0) { // let lastRect = lineRect // let lastContent = event.target.innerHTML // let added = words.length == total ? "" : " " // added += words.shift() // event.target.innerHTML += added // lineRect = event.target.getBoundingClientRect() // // When new line or last line: // if (lineRect.height != lastRect.height) { // //Reconstructure last line. // event.target.innerHTML = lastContent // //Create Rect from last line. // lineRect = event.target.getBoundingClientRect() // rects.push(lineRect) // //Copy last line content to processed text. // processedText.innerHTML += lastContent // // Create content of new line. // event.target.innerHTML = added // if (!processedText.parentNode) event.target.parentNode.insertBefore(processedText, event.target) // if (Rect.contains(lineRect, point)) { // break // } // } // if (words.length == 0) { // lineRect = event.target.getBoundingClientRect() // processedText.innerHTML += event.target.innerHTML // rects.push(lineRect) // } // } // event.target.innerHTML = content // if (processedText.parentNode) processedText.parentNode.removeChild(processedText) // return lineRect // } } static printDomRect(rect, { color = 'red', pad = 0, parent = document.body }) { let element = document.createElement('div') Object.assign(element.style, { padding: pad + 'px', borderColor: color, borderWidth: '2px', borderStyle: 'solid', top: rect.top + 'px', left: rect.left + 'px', width: rect.width + 'px', height: rect.height + 'px', position: 'absolute', zIndex: 10000, opacity: 0.3 }) parent.appendChild(element) } static printDomPoint(point, { color = 'red', parent = document.body }) { let element = document.createElement('div') Object.assign(element.style, { position: 'absolute', top: point.y + 'px', left: point.x + 'px', width: '10px', height: '10px', backgroundColor: color, zIndex: 10000 }) parent.appendChild(element) } }