Implement player-markers

This commit is contained in:
Blue (Lukas Rieger) 2021-01-28 19:57:54 +01:00
parent 31c159d9ae
commit a4ee07aa5c
No known key found for this signature in database
GPG Key ID: 904C4995F9E1F800
14 changed files with 454 additions and 271 deletions

View File

@ -7,7 +7,7 @@
<link rel="shortcut icon" href="favicon.png">
</head>
<body style="margin: 0; padding: 0;">
<div id="map-container" style="position: absolute; width: 96%; height: 90%; margin: 2%;"></div>
<div id="map-container" style="position: absolute; width: 100%; height: 100%;"></div>
<script type="text/javascript" src="js/three.js"></script>
<script type="text/javascript" src="js/hammer.js"></script>
@ -22,13 +22,21 @@
// load map
let maps = [];
let markerManager = null;
let playerManager = null;
BlueMap.loadMaps("data/", bluemap.events).then(loadedMaps => {
maps = loadedMaps;
bluemap.setMap(maps[0]);
markerManager = maps[0].createMarkerFileManager(bluemap.markerScene);
markerManager = new BlueMap.MarkerFileManager(bluemap.markerScene, "data/markers.json", maps[0].id, bluemap.events);
markerManager.update();
markerManager.setAutoUpdateInterval(1000 * 10);
playerManager = new BlueMap.PlayerMarkerManager(bluemap.markerScene, "live/players", "", bluemap.events);
playerManager.update();
playerManager.setAutoUpdateInterval(1000);
bluemap.setMap(maps[0]).then(() => {
playerManager.worldId = maps[0].world;
});
});
</script>
</body>

View File

@ -1 +1 @@
{"players":[{"uuid":"c61be8fb-0a66-4411-aae5-0c67bb4808d4","name":"TBlueF","world":"118a8ec2-ccb1-4e84-be88-1ff568e413ca","position":{"x":-130.62644738873905,"y":72.53260088333643,"z":138.27086962845027}}]}
{"players":[{"uuid":"c61be8fb-0a66-4411-aae5-0c67bb4808d4","name":"TBlueF","world":"118a8ec2-ccb1-4e84-be88-1ff568e413ca","position":{"x":130.62644738873905,"y":82.53260088333643,"z":-138.27086962845027}}]}

View File

@ -3,6 +3,8 @@ import {Map} from "./map/Map";
export { MapViewer } from "./MapViewer";
export * from "./util/Utils";
export { MarkerFileManager } from "./markers/MarkerFileManager";
export { PlayerMarkerManager } from "./markers/PlayerMarkerManager";
/**
* Loads and returns a promise with an array of Maps loaded from that root-path.<br>

View File

@ -287,6 +287,7 @@ export class MapViewer {
/**
* Changes / Sets the map that will be loaded and displayed
* @param map {Map}
* @returns Promise<void>
*/
setMap(map = null) {
if (this.map && this.map.isMap) this.map.unload();

View File

@ -1,116 +0,0 @@
import {alert} from "../util/Utils";
import {FileLoader} from "three";
export class PlayerMarkerManager {
constructor(mapViewer, events = null, livePlayerUrl = "/live/players") {
this.mapViewer = mapViewer;
this.events = events ? events : mapViewer.events;
this.playerUrl = livePlayerUrl;
this._playerData = {players:[]};
this.events.addEventListener("bluemapMapChanged", this.onMapChange);
}
update(){
return this.loadPlayerData()
.then(playerData => {
if (playerData && Array.isArray(playerData.players)) {
this._playerData = playerData;
this.updateMarkers();
}
})
.catch(reason => {
alert(this.events, reason, "warning");
});
}
updateMarkers() {
if (!this.mapViewer.map || !this.mapViewer.map.isLoaded) return;
let markerset = this.getMarkerset();
// map of uuid to playerdata
let playerMap = {};
this._playerData.players.forEach(player => {
if (!player.uuid) return;
// fill defaults
player = {
name: player.uuid,
world: "",
position: {},
rotation: {},
...player
};
player.position = {
x: 0,
y: 0,
z: 0,
...player.position
};
player.rotation = {
yaw: 0,
pitch: 0,
roll: 0,
...player.rotation
};
playerMap[player.uuid] = player;
});
//update existing markers
markerset.marker.forEach(marker => {
if (!marker.isPlayerMarker) return;
if (!playerMap[marker.playerUuid]) return;
let player = playerMap[marker.playerUuid];
});
}
getMarkerset() {
//init markerset
let markerset = this.mapViewer.map.markerManager.markerSets["bm-live-players"];
if (!markerset || !markerset.isMarkerSet){
markerset = this.mapViewer.map.markerManager.createMarkerSet("bm-live-players");
}
return markerset;
}
onMapChange = () => {
this.updateMarkers();
}
dispose() {
this.events.removeEventListener("bluemapMapChanged", this.onMapChange);
this._playerData = {players:[]};
this.updateMarkers();
}
/**
* Loads the playerdata
* @returns {Promise<Object>}
*/
loadPlayerData() {
return new Promise((resolve, reject) => {
//alert(this.events, `Loading players from '${this.playerUrl}'...`, "fine");
let loader = new FileLoader();
loader.setResponseType("json");
loader.load(this.playerUrl,
playerData => {
if (!playerData) reject(`Failed to parse '${this.playerUrl}'!`);
else resolve(playerData);
},
() => {},
() => reject(`Failed to load '${this.playerUrl}'!`)
)
});
}
}

View File

@ -7,6 +7,7 @@ import {alert, dispatchEvent, hashTile, stringToImage} from "../util/Utils";
import {TileManager} from "./TileManager";
import {TileLoader} from "./TileLoader";
import {MarkerFileManager} from "../markers/MarkerFileManager";
import {PlayerMarkerManager} from "../markers/PlayerMarkerManager";
export class Map {
@ -334,15 +335,6 @@ export class Map {
}
}
/**
* Creates a MarkerFileManager that is loading and updating the markers for this map.
* @param markerScene {Scene} - The scene to which all markers will be added
* @returns {MarkerFileManager}
*/
createMarkerFileManager(markerScene) {
return new MarkerFileManager(markerScene, this.dataUrl + "../markers.json", this.id, this.events);
}
dispose() {
this.unload();
}

View File

@ -19,7 +19,7 @@ export class HtmlMarker extends Marker {
this.fadeDistanceMax = Number.MAX_VALUE;
this.addEventListener( 'removed', () => {
this.element.parentNode.removeChild(this.element);
if (this.element.parentNode) this.element.parentNode.removeChild(this.element);
});
this.add(this.elementObject);

View File

@ -35,12 +35,7 @@ export class Marker extends Object3D {
* @returns {number} - opacity between 0 and 1
*/
static calculateDistanceOpacity(position, camera, fadeDistanceMin, fadeDistanceMax) {
//calculate "orthographic distance" to marker
Marker._posRelativeToCamera.subVectors(position, camera.position);
camera.getWorldDirection(Marker._cameraDirection);
let distance = Marker._posRelativeToCamera.dot(Marker._cameraDirection);
//calculate opacity based on (min/max)distance
let distance = Marker.calculateDistanceToCameraPlane(position, camera);
let minDelta = (distance - fadeDistanceMin) / fadeDistanceMin;
let maxDelta = (distance - fadeDistanceMax) / (fadeDistanceMax * 0.5);
return Math.min(
@ -49,4 +44,15 @@ export class Marker extends Object3D {
);
}
/**
* @param position {Vector3}
* @param camera {THREE.Camera}
* @returns {number}
*/
static calculateDistanceToCameraPlane (position, camera) {
Marker._posRelativeToCamera.subVectors(position, camera.position);
camera.getWorldDirection(Marker._cameraDirection);
return Marker._posRelativeToCamera.dot(Marker._cameraDirection);
}
}

View File

@ -1,4 +1,4 @@
import {FileLoader, Scene} from "three";
import {Scene} from "three";
import {MarkerSet} from "./MarkerSet";
import {ShapeMarker} from "./ShapeMarker";
import {alert} from "../util/Utils";
@ -6,11 +6,12 @@ import {ExtrudeMarker} from "./ExtrudeMarker";
import {LineMarker} from "./LineMarker";
import {HtmlMarker} from "./HtmlMarker";
import {PoiMarker} from "./PoiMarker";
import {MarkerManager} from "./MarkerManager";
/**
* A manager for loading and updating markers from a markers.json file
*/
export class MarkerFileManager {
export class MarkerFileManager extends MarkerManager {
/**
* @constructor
@ -20,120 +21,12 @@ export class MarkerFileManager {
* @param events {EventTarget}
*/
constructor(markerScene, fileUrl, mapId, events = null) {
super(markerScene, fileUrl, events);
Object.defineProperty(this, 'isMarkerFileManager', {value: true});
this.markerScene = markerScene;
this.fileUrl = fileUrl;
this.mapId = mapId;
this.events = events;
/** @type {Map<string, MarkerSet>} */
this.markerSets = new Map();
/** @type {Map<string, Marker>} */
this.markers = new Map();
/** @type {NodeJS.Timeout} */
this._updateInterval = null;
}
/**
* Sets the automatic-update frequency, setting this to 0 or negative disables automatic updates (default).
* This is better than using setInterval() on update() because this will wait for the update to finish before requesting the next update.
* @param ms - interval in milliseconds
*/
setAutoUpdateInterval(ms) {
if (this._updateInterval) clearInterval(this._updateInterval);
if (ms > 0) {
let autoUpdate = () => {
this.update().finally(() => {
this._updateInterval = setTimeout(autoUpdate, ms);
});
};
this._updateInterval = setTimeout(autoUpdate, ms);
}
}
/**
* Loads the marker-file and updates all managed markers.
* @returns {Promise<object>} - A promise completing when the markers finished updating
*/
update() {
return this.loadMarkerFile()
.then(markerFileData => this.updateFromData(markerFileData))
.catch(error => {
alert(this.events, error, "error");
});
}
/**
* Stops automatic-updates and disposes all markersets and markers managed by this manager
*/
dispose() {
this.setAutoUpdateInterval(0);
this.markerSets.forEach(markerSet => markerSet.dispose());
}
/**
* @private
* Adds a MarkerSet to this Manager, removing any existing markerSet with this id first.
* @param markerSet {MarkerSet}
*/
addMarkerSet(markerSet) {
this.removeMarkerSet(markerSet.markerSetId);
this.markerSets.set(markerSet.markerSetId, markerSet);
this.markerScene.add(markerSet)
}
/**
* @private
* Removes a MarkerSet from this Manager
* @param setId {string} - The id of the MarkerSet
*/
removeMarkerSet(setId) {
let markerSet = this.markerSets.get(setId);
if (markerSet) {
this.markerScene.remove(markerSet);
this.markerSets.delete(setId);
markerSet.dispose();
}
}
/**
* @private
* Adds a marker to this manager
* @param markerSet {MarkerSet}
* @param marker {Marker}
*/
addMarker(markerSet, marker) {
this.removeMarker(marker.markerId);
this.markers.set(marker.markerId, marker);
markerSet.add(marker);
}
/**
* @private
* Removes a marker from this manager
* @param markerId {string}
*/
removeMarker(markerId) {
let marker = this.markers.get(markerId);
if (marker) {
if (marker.parent) marker.parent.remove(marker);
this.markers.delete(markerId);
marker.dispose();
}
}
/**
* @private
* Updates all managed markers using the provided data.
* @param markerData {object} - The data object, usually parsed json from a markers.json
*/
updateFromData(markerData) {
if (!Array.isArray(markerData.markerSets)) return;
let updatedMarkerSets = new Set();
@ -144,7 +37,7 @@ export class MarkerFileManager {
let markerSet = this.updateMarkerSetFromData(markerSetData);
updatedMarkerSets.add(markerSet);
} catch (err) {
alert(this.events, "Failed to parse markerset-data: " + err, "fine");
alert(this.events, err, "fine");
}
});
@ -189,7 +82,7 @@ export class MarkerFileManager {
let marker = this.updateMarkerFromData(markerSet, markerData);
updatedMarkers.add(marker);
} catch (err) {
alert(this.events, "Failed to parse marker-data: " + err, "fine");
alert(this.events, err, "fine");
console.debug(err);
}
});
@ -241,24 +134,4 @@ export class MarkerFileManager {
return marker;
}
/**
* @private
* Loads the marker file
* @returns {Promise<Object>} - A promise completing with the parsed json object from the loaded file
*/
loadMarkerFile() {
return new Promise((resolve, reject) => {
let loader = new FileLoader();
loader.setResponseType("json");
loader.load(this.fileUrl,
markerFileData => {
if (!markerFileData) reject(`Failed to parse '${this.fileUrl}'!`);
else resolve(markerFileData);
},
() => {},
() => reject(`Failed to load '${this.fileUrl}'!`)
)
});
}
}

View File

@ -0,0 +1,158 @@
import {FileLoader, Scene} from "three";
import {MarkerSet} from "./MarkerSet";
import {alert} from "../util/Utils";
/**
* A manager for loading and updating markers from a file
*/
export class MarkerManager {
/**
* @constructor
* @param markerScene {Scene} - The scene to which all markers will be added
* @param fileUrl {string} - The marker file from which this manager updates its markers
* @param events {EventTarget}
*/
constructor(markerScene, fileUrl, events = null) {
Object.defineProperty(this, 'isMarkerManager', {value: true});
this.markerScene = markerScene;
this.fileUrl = fileUrl;
this.events = events;
/** @type {Map<string, MarkerSet>} */
this.markerSets = new Map();
/** @type {Map<string, Marker>} */
this.markers = new Map();
/** @type {NodeJS.Timeout} */
this._updateInterval = null;
}
/**
* Sets the automatic-update frequency, setting this to 0 or negative disables automatic updates (default).
* This is better than using setInterval() on update() because this will wait for the update to finish before requesting the next update.
* @param ms - interval in milliseconds
*/
setAutoUpdateInterval(ms) {
if (this._updateInterval) clearInterval(this._updateInterval);
if (ms > 0) {
let autoUpdate = () => {
this.update().finally(() => {
this._updateInterval = setTimeout(autoUpdate, ms);
});
};
this._updateInterval = setTimeout(autoUpdate, ms);
}
}
/**
* Loads the marker-file and updates all managed markers.
* @returns {Promise<object>} - A promise completing when the markers finished updating
*/
update() {
return this.loadMarkerFile()
.then(markerFileData => this.updateFromData(markerFileData))
.catch(error => {
alert(this.events, error, "warning");
});
}
/**
* Stops automatic-updates and disposes all markersets and markers managed by this manager
*/
dispose() {
this.setAutoUpdateInterval(0);
this.markerSets.forEach(markerSet => markerSet.dispose());
}
/**
* Removes all markers managed by this marker-manager
*/
clear() {
this.markerSets.forEach(markerSet => this.removeMarkerSet(markerSet.markerSetId));
}
/**
* @protected
* Adds a MarkerSet to this Manager, removing any existing markerSet with this id first.
* @param markerSet {MarkerSet}
*/
addMarkerSet(markerSet) {
this.removeMarkerSet(markerSet.markerSetId);
this.markerSets.set(markerSet.markerSetId, markerSet);
this.markerScene.add(markerSet)
}
/**
* @protected
* Removes a MarkerSet from this Manager
* @param setId {string} - The id of the MarkerSet
*/
removeMarkerSet(setId) {
let markerSet = this.markerSets.get(setId);
if (markerSet) {
this.markerScene.remove(markerSet);
this.markerSets.delete(setId);
markerSet.dispose();
}
}
/**
* @protected
* Adds a marker to this manager
* @param markerSet {MarkerSet}
* @param marker {Marker}
*/
addMarker(markerSet, marker) {
this.removeMarker(marker.markerId);
this.markers.set(marker.markerId, marker);
markerSet.add(marker);
}
/**
* @protected
* Removes a marker from this manager
* @param markerId {string}
*/
removeMarker(markerId) {
let marker = this.markers.get(markerId);
if (marker) {
if (marker.parent) marker.parent.remove(marker);
this.markers.delete(markerId);
marker.dispose();
}
}
/**
* Updates all managed markers using the provided data.
* @param markerData {object} - The data object, usually parsed json from a markers.json
*/
updateFromData(markerData) {}
/**
* @private
* Loads the marker file
* @returns {Promise<Object>} - A promise completing with the parsed json object from the loaded file
*/
loadMarkerFile() {
return new Promise((resolve, reject) => {
let loader = new FileLoader();
loader.setResponseType("json");
loader.load(this.fileUrl,
markerFileData => {
if (!markerFileData) reject(`Failed to parse '${this.fileUrl}'!`);
else resolve(markerFileData);
},
() => {},
() => reject(`Failed to load '${this.fileUrl}'!`)
)
});
}
}

111
src/markers/PlayerMarker.js Normal file
View File

@ -0,0 +1,111 @@
import {Marker} from "./Marker";
import {CSS2DObject} from "../util/CSS2DRenderer";
import {animate, EasingFunctions, htmlToElement} from "../util/Utils";
import {Vector3} from "three";
export class PlayerMarker extends Marker {
/**
* @param markerId {string}
* @param playerUuid {string}
*/
constructor(markerId, playerUuid) {
super(markerId);
Object.defineProperty(this, 'isPlayerMarker', {value: true});
this.markerType = "player";
this.playerUuid = playerUuid;
this.elementObject = new CSS2DObject(htmlToElement(`
<div id="bm-marker-${this.markerId}" class="bm-marker-${this.markerType}">
<img src="assets/playerheads/${this.playerUuid}.png" alt="playerhead" draggable="false">
<div class="bm-player-name"></div>
</div>
`));
this.elementObject.onBeforeRender = (renderer, scene, camera) => this.onBeforeRender(renderer, scene, camera);
this.playerHeadElement = this.element.getElementsByTagName("img")[0];
this.playerNameElement = this.element.getElementsByTagName("div")[0];
this.addEventListener( 'removed', () => {
if (this.element.parentNode) this.element.parentNode.removeChild(this.element);
});
this.playerHeadElement.addEventListener('error', () => {
this.playerHeadElement.src = "assets/playerheads/steve.png";
}, {once: true});
this.add(this.elementObject);
}
/**
* @returns {Element}
*/
get element() {
return this.elementObject.element;
}
onBeforeRender(renderer, scene, camera) {
this.element.setAttribute("distance-data", Marker.calculateDistanceToCameraPlane(this.position, camera).toString());
}
/**
* @typedef PlayerLike {{
* uuid: string,
* name: string,
* world: string,
* position: {x: number, y: number, z: number},
* rotation: {yaw: number, pitch: number, roll: number}
* }}
*/
/**
* @param markerData {PlayerLike}
*/
updateFromData(markerData) {
// animate position update
let pos = markerData.position || {};
if (!this.position.x && !this.position.y && !this.position.z) {
this.position.set(
pos.x || 0,
pos.y || 0,
pos.z || 0
);
} else {
let startPos = {
x: this.position.x,
y: this.position.y,
z: this.position.z,
}
let deltaPos = {
x: (pos.x || 0) - startPos.x,
y: (pos.y || 0) - startPos.y,
z: (pos.z || 0) - startPos.z,
}
if (deltaPos.x || deltaPos.y || deltaPos.z) {
animate(progress => {
let ease = EasingFunctions.easeInOutCubic(progress);
this.position.set(
startPos.x + deltaPos.x * ease || 0,
startPos.y + deltaPos.y * ease || 0,
startPos.z + deltaPos.z * ease || 0
);
}, 500);
}
}
// update name
let name = markerData.name || this.playerUuid;
if (this.playerNameElement.innerHTML !== name)
this.playerNameElement.innerHTML = name;
}
dispose() {
super.dispose();
if (this.element.parentNode) this.element.parentNode.removeChild(this.element);
}
}

View File

@ -0,0 +1,101 @@
import {MarkerManager} from "./MarkerManager";
import {alert} from "../util/Utils";
import {MarkerSet} from "./MarkerSet";
import {PlayerMarker} from "./PlayerMarker";
export class PlayerMarkerManager extends MarkerManager {
/**
* @constructor
* @param markerScene {THREE.Scene} - The scene to which all markers will be added
* @param playerDataUrl {string} - The marker file from which this manager updates its markers
* @param worldId {string} - The worldId of the world for which the markers should be loaded
* @param events {EventTarget}
*/
constructor(markerScene, playerDataUrl, worldId, events = null) {
super(markerScene, playerDataUrl, events);
Object.defineProperty(this, 'isPlayerMarkerManager', {value: true});
this.worldId = worldId;
this.getPlayerMarkerSet();
}
/**
* @param markerData {{players: PlayerLike[]}}
*/
updateFromData(markerData) {
/** @type Set<Marker> */
let updatedPlayerMarkers = new Set();
// update
if (Array.isArray(markerData.players)) {
markerData.players.forEach(playerData => {
try {
let playerMarker = this.updatePlayerMarkerFromData(playerData);
updatedPlayerMarkers.add(playerMarker);
} catch (err) {
alert(this.events, err, "fine");
}
});
}
// remove
this.markers.forEach((playerMarker, markerId) => {
if (!updatedPlayerMarkers.has(playerMarker)) {
this.removeMarker(markerId);
}
});
}
/**
* @private
* @param markerData {PlayerLike}
* @returns PlayerMarker
*/
updatePlayerMarkerFromData(markerData) {
let playerUuid = markerData.uuid;
if (!playerUuid) throw new Error("player-data has no uuid!");
let markerId = "bm-player-" + playerUuid;
/** @type PlayerMarker */
let marker = this.markers.get(markerId);
let markerSet = this.getPlayerMarkerSet();
// create new if not existent of wrong type
if (!marker || !marker.isPlayerMarker) {
marker = new PlayerMarker(markerId, playerUuid);
this.addMarker(markerSet, marker);
}
// make sure marker is in the correct MarkerSet
if (marker.parent !== markerSet) markerSet.add(marker);
// update
marker.updateFromData(markerData);
// hide if wrong world
marker.visible = markerData.world === this.worldId;
return marker;
}
/**
* @private
* @returns {MarkerSet}
*/
getPlayerMarkerSet() {
let playerMarkerSet = this.markerSets.get("bm-players");
if (!playerMarkerSet) {
playerMarkerSet = new MarkerSet("bm-players");
this.addMarkerSet(playerMarkerSet);
}
return playerMarkerSet;
}
}

View File

@ -102,7 +102,7 @@ var CSS2DRenderer = function () {
element.style.oTransform = style;
element.style.transform = style;
element.style.display = ( parentVisible && object.visible && vector.z >= - 1 && vector.z <= 1 && element.style.opacity > 0 ) ? '' : 'none';
element.style.display = ( parentVisible && object.visible && vector.z >= - 1 && vector.z <= 1 && element.style.opacity !== "0" ) ? '' : 'none';
var objectData = {
distanceToCameraSquared: getDistanceToSquared( camera, object )

View File

@ -84,7 +84,7 @@ export const dispatchEvent = (element, event, detail = {}) => {
* - warning
* - error
* @param element {EventTarget} the element on that the event is dispatched
* @param message {string}
* @param message {object}
* @param level {string}
*/
export const alert = (element, message, level = "info") => {
@ -176,6 +176,53 @@ export const animate = function (animationFrame, durationMs = 1000, postAnimatio
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
*