BlueMap/BlueMapCommon/webapp/src/js/util/Utils.js

399 lines
12 KiB
JavaScript

/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils, Vector2, Vector3} from "three";
export const VEC2_ZERO = new Vector2(0, 0);
export const VEC3_ZERO = new Vector3(0, 0, 0);
export const VEC3_X = new Vector3(1, 0, 0);
export const VEC3_Y = new Vector3(0, 1, 0);
export const VEC3_Z = new Vector3(0, 0, 1);
/**
* Converts a url-encoded image string to an actual image-element
* @param string {string}
* @returns {HTMLImageElement}
*/
export const stringToImage = string => {
let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');
image.src = string;
return image;
};
/**
* Creates an optimized path from x,z coordinates used by bluemap to save tiles
* @param x {number}
* @param z {number}
* @returns {string}
*/
export const pathFromCoords = (x, z) => {
let path = 'x';
path += splitNumberToPath(x);
path += 'z';
path += splitNumberToPath(z);
path = path.substring(0, path.length - 1);
return path;
};
/**
* Splits a number into an optimized folder-path used to save bluemap-tiles
* @param num {number}
* @returns {string}
*/
const splitNumberToPath = num => {
let path = '';
if (num < 0) {
num = -num;
path += '-';
}
let s = num.toString();
for (let i = 0; i < s.length; i++) {
path += s.charAt(i) + '/';
}
return path;
};
/**
* Hashes tile-coordinates to be saved in a map
* @param x {number}
* @param z {number}
* @returns {string}
*/
export const hashTile = (x, z) => `x${x}z${z}`;
export const generateCacheHash = () => {
return Math.round(Math.random() * 1000000);
}
/**
* Dispatches an event to the element of this map-viewer
* @param element {EventTarget} the element on that the event is dispatched
* @param event {string}
* @param detail {object}
* @returns {undefined|void|boolean}
*/
export const dispatchEvent = (element, event, detail = {}) => {
if (!element || !element.dispatchEvent) return;
return element.dispatchEvent(new CustomEvent(event, {
detail: detail
}));
}
/**
* Sends a "bluemapAlert" event with a message and a level.
* The level can be anything, but the app uses the levels
* - debug
* - fine
* - info
* - warning
* - error
* @param element {EventTarget} the element on that the event is dispatched
* @param message {object}
* @param level {string}
*/
export const alert = (element, message, level = "info") => {
// alert event
let printToConsole = dispatchEvent(element, "bluemapAlert", {
message: message,
level: level
});
// log alert to console
if (printToConsole !== false) {
if (level === "info") {
console.log(`[BlueMap/${level}]`, message);
} else if (level === "warning") {
console.warn(`[BlueMap/${level}]`, message);
} else if (level === "error") {
console.error(`[BlueMap/${level}]`, message);
} else {
console.debug(`[BlueMap/${level}]`, message);
}
}
}
/**
* Source: https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
*
* @param html {string} representing a single element
* @return {Element}
*/
export const htmlToElement = html => {
let template = document.createElement('template');
template.innerHTML = html.trim();
return template.content.firstChild;
}
/**
* Source: https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
*
* @param html {string} representing any number of sibling elements
* @return {NodeList}
*/
export const htmlToElements = html => {
let template = document.createElement('template');
template.innerHTML = html;
return template.content.childNodes;
}
/**
* Schedules an animation
* @param durationMs {number} the duration of the animation in ms
* @param animationFrame {function(progress: number, deltaTime: number)} a function that is getting called each frame with the parameters (progress (0-1), deltaTime)
* @param postAnimation {function(finished: boolean)} a function that gets called once after the animation is finished or cancelled. The function accepts one bool-parameter whether the animation was finished (true) or canceled (false)
* @returns {{cancel: function()}} the animation object
*/
export const animate = function (animationFrame, durationMs = 1000, postAnimation = null) {
let animation = {
animationStart: -1,
lastFrame: -1,
cancelled: false,
frame: function (time) {
if (this.cancelled) return;
if (this.animationStart === -1) {
this.animationStart = time;
this.lastFrame = time;
}
let progress = durationMs === 0 ? 1 : MathUtils.clamp((time - this.animationStart) / durationMs, 0, 1);
let deltaTime = time - this.lastFrame;
animationFrame(progress, deltaTime);
if (progress < 1) window.requestAnimationFrame(time => this.frame(time));
else if (postAnimation) postAnimation(true);
this.lastFrame = time;
},
cancel: function () {
this.cancelled = true;
if (postAnimation) postAnimation(false);
}
};
if (durationMs !== 0) {
window.requestAnimationFrame(time => animation.frame(time));
} else {
animation.frame(0);
}
return animation;
}
/**
* Source: https://gist.github.com/gre/1650294
* @type {{
* easeOutCubic: (function(number): number),
* linear: (function(number): number),
* easeOutQuint: (function(number): number),
* easeInQuart: (function(number): number),
* easeInOutQuint: (function(number): number),
* easeInQuad: (function(number): number),
* easeOutQuart: (function(number): number),
* easeInCubic: (function(number): number),
* easeInQuint: (function(number): number),
* easeOutQuad: (function(number): number),
* easeInOutQuad: (function(number): number),
* easeInOutCubic: (function(number): number),
* easeInOutQuart: (function(number): number)
* }}
*/
export const EasingFunctions = {
// no easing, no acceleration
linear: t => t,
// accelerating from zero velocity
easeInQuad: t => t*t,
// decelerating to zero velocity
easeOutQuad: t => t*(2-t),
// acceleration until halfway, then deceleration
easeInOutQuad: t => t<.5 ? 2*t*t : -1+(4-2*t)*t,
// accelerating from zero velocity
easeInCubic: t => t*t*t,
// decelerating to zero velocity
easeOutCubic: t => (--t)*t*t+1,
// acceleration until halfway, then deceleration
easeInOutCubic: t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1,
// accelerating from zero velocity
easeInQuart: t => t*t*t*t,
// decelerating to zero velocity
easeOutQuart: t => 1-(--t)*t*t*t,
// acceleration until halfway, then deceleration
easeInOutQuart: t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t,
// accelerating from zero velocity
easeInQuint: t => t*t*t*t*t,
// decelerating to zero velocity
easeOutQuint: t => 1+(--t)*t*t*t*t,
// acceleration until halfway, then deceleration
easeInOutQuint: t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t
}
/**
* Returns the offset position of an element
*
* Source: https://plainjs.com/javascript/styles/get-the-position-of-an-element-relative-to-the-document-24/
*
* @param element {Element}
* @returns {{top: number, left: number}}
*/
export const elementOffset = element => {
let rect = element.getBoundingClientRect(),
scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
scrollTop = window.pageYOffset || document.documentElement.scrollTop;
return { top: rect.top + scrollTop, left: rect.left + scrollLeft }
}
/**
* Very simple deep equals, should not be used for complex objects. Is designed for comparing parsed json-objects.
* @param object1 {object}
* @param object2 {object}
* @returns {boolean}
*/
export const deepEquals = (object1, object2) => {
if (Object.is(object1, object2)) return true;
let type = typeof object1;
if (type !== typeof object2) return false;
if (type === 'number' || type === 'boolean' || type === 'string') return false;
if (Array.isArray(object1)){
let len = object1.length;
if (len !== object2.length) return false;
for (let i = 0; i < len; i++) {
if (!deepEquals(object1[i], object2[i])) return false;
}
return true;
}
for (let property in object1) {
if (!object1.hasOwnProperty(property)) continue;
if (!deepEquals(object1[property], object2[property])) return false;
}
return true;
}
/**
* Adds one listener to multiple events
* @param target {EventTarget}
* @param types {string|string[]}
* @param listener {EventListenerOrEventListenerObject | null}
* @param options {boolean | AddEventListenerOptions?}
*/
export const addEventListeners = (target, types, listener, options) => {
if (!Array.isArray(types)){
types = types.trim().split(" ");
}
types.forEach(type => target.addEventListener(type, listener, options));
}
/**
* Removes one listener to multiple events
* @param target {EventTarget}
* @param types {string|string[]}
* @param listener {EventListenerOrEventListenerObject | null}
* @param options {boolean | EventListenerOptions?}
*/
export const removeEventListeners = (target, types, listener, options) => {
if (!Array.isArray(types)){
types = types.trim().split(" ");
}
types.forEach(type => target.removeEventListener(type, listener, options));
}
/**
* Softly clamps towards a minimum value
* @param value {number}
* @param min {number}
* @param stiffness {number}
* @returns {number}
*/
export const softMin = (value, min, stiffness) => {
if (value >= min) return value;
let delta = min - value;
if (delta < 0.0001) return min;
return value + delta * stiffness;
}
/**
* Softly clamps towards a maximum value
* @param value {number}
* @param max {number}
* @param stiffness {number}
* @returns {number}
*/
export const softMax = (value, max, stiffness) => {
if (value <= max) return value;
let delta = value - max;
if (delta < 0.0001) return max;
return value - delta * stiffness;
}
/**
* Softly clamps towards a minimum and maximum value
* @param value {number}
* @param min {number}
* @param max {number}
* @param stiffness {number}
* @returns {number}
*/
export const softClamp = (value, min, max, stiffness) => {
return softMax(softMin(value, min, stiffness), max, stiffness);
}
export const vecArrToObj = (val, useZ = false) => {
if (val && val.length >= 2) {
if (useZ) return {x: val[0], z: val[1]};
return {x: val[0], y: val[1]};
}
return {};
}
const pixel = document.createElement('canvas');
pixel.width = 1;
pixel.height = 1;
const pixelContext = pixel.getContext('2d', {
willReadFrequently: true
});
export const getPixel = (img, x, y) => {
pixelContext.drawImage(img, Math.floor(x), Math.floor(y), 1, 1, 0, 0, 1, 1);
return pixelContext.getImageData(0, 0, 1, 1).data;
}