Main
Select All
import { Num, El } from '../../modules/tools.js'
import { startScripts, popStateScripts, colorTheme, starryBackgroundAtts } from '../../template/init.js'
//` Helper methods
function curURLpageID() { return new URLSearchParams(window.location.search).get('file') ?? null };
function redirectIframe(pageID) {
//! Update iframe's source (without using src, so that browser history is unaffected)
El.q('#previewWindow').contentWindow.location.replace(`${location.origin}/${pageID}?pageOnly=true`)
}
function updatePageElementsBasedOnLink(link) {
//! Update menu color here
El.l('.boxItem > .contents').forEach(el => el.classList.toggle('selected', el === link.element))
//! Update scriptViewer link
El.q('#scriptViewerLink').href = `/scriptViewer/${link.pageID}`
}
/** @type {{element:HTMLElement, pageID:string, elementID:string}} */
let misc = { element: null, pageID: 'misc', elementID: null }
/** @type {typeof misc} */ let curPage;
/** @type {Object<string,typeof misc>} */ let links = { misc };
startScripts.push(() => {
// Misc within misc menu - on click, redirect page and push a state to history
document.addEventListener('miscpage_sitelinkClicked', e => {
requestAnimationFrame(() => {
El.q('#previewWindow').contentWindow.location.replace(`${location.origin}/${e.detail.page}?pageOnly=true`)
window.history.pushState({}, document.title, `?file=${e.detail.page}`)
scrollToElem(links[e.detail.page])
updatePageElementsBasedOnLink(links[e.detail.page])
})
})
// Collect all pagelinks and add click handlers
El.l('.boxItem > .contents').forEach(element => {
const pageID = element?.id.replace('page_', '')
const link = { element, pageID, elementID: element.id }
links[pageID] = link;
El.ev(element, 'dblclick', () => { window.location.href = `/${pageID}` })
El.ev(element, 'click', function () {
element.scrollIntoView({behavior: 'smooth'})
if (pageID !== curURLpageID()) {
redirectIframe(link.pageID)
updatePageElementsBasedOnLink(link)
window.history.pushState({}, document.title, `?file=${link.pageID}`)
}
})
})
// Enable toggle bar on link list
El.makeClassToggleButton('hidden', '#pageListSideHandle', [El.q('.boxItemList > .contents')])
// When color theme is toggled, post message to preview iframe
El.ev(colorTheme, 'toggle', () => El.q('#previewWindow').contentWindow.postMessage({ type: 'themeToggle', theme: colorTheme.theme }))
// Add animation styles to menu links
pageListHoverAnimations();
starryBackgroundAtts.interactive = false;
// Set current page, init the link and replace browser history to include the file query
initCurPage();
window.history.replaceState({}, document.title, `?file=${curPage.pageID}`)
})
function scrollToElem(link) {
const contents = El.q('.boxItemList > .contents')
contents.scrollTop = (link?.element?.offsetTop - contents.offsetTop - 5) ?? 0
El.q('.boxItemList > .contents').scrollLeft = (link?.element?.offsetLeft - contents.offsetLeft - 5) ?? 0;
}
function initCurPage() {
curPage = links[curURLpageID() ?? 'misc']
scrollToElem(curPage);
redirectIframe(curPage.pageID)
updatePageElementsBasedOnLink(curPage)
}
popStateScripts.push(initCurPage)
function pageListHoverAnimations() {
El.l('.boxItem').forEach(box => {
const content = El.q(box, '.contents')
/** @type {Animation} */ let upgrade
/** @type {Animation} */ let downgrade
let max; let genmax; (genmax = () => max = Num.randomDecimal(2, 4))()
let updur; let genupdur; (genupdur = () => updur = Num.randomDecimal(300, 700))()
let dndur; let gendndur; (gendndur = () => dndur = Num.randomDecimal(1800, 1800))()
let downstart;
El.ev(content, 'pointermove', () => {
if (!upgrade) {
let m;
if (downgrade) {
m = ((dndur - (performance.now() - downstart)) / dndur) * max;
downgrade.cancel();
downgrade = null;
}
const atts = {
duration: genupdur(),
easing: 'ease-in-out',
fill: 'forwards',
iterations: 1,
}
upgrade = box.animate([{ '--rockmult': m ?? 0 }, { '--rockmult': genmax() }], atts)
upgrade.onfinish = () => {
upgrade = null;
downgrade = box.animate([{ '--rockmult': max }, { '--rockmult': 0 }], {
duration: gendndur(), easing: 'ease-in-out', fill: 'forwards', iterations: 1
})
downstart = performance.now();
downgrade.onfinish = () => {
downgrade = null;
}
}
}
})
})
}
Site Template / Common
Select All
import { Style, El, Cookie, Page } from '../modules/tools.js'
import { List, RGBA, RGB } from '../modules/array.js'
import { Starry } from '../modules/siteElements/misc.js'
export function preventStarryBackground() {Starry.preventStart = true}
export const starryBackgroundAtts = {
numStars: 250,
interactive: { withCursor: false, withStarHighlighting: true, withStarLines: false },
randColor: { enabled: true, uniques: true, maxRGB: { r: 0, g: 255, b: 255 } },
animation: {
jitter: false,
orbit: true,
chaos: true,
flicker: true,
haze: false,
},
}
export const startScripts = [];
export const pageShowScripts = [];
export const popStateScripts = [];
window.addEventListener('load', () => {
//! Initialize fullscreen button in header
initializeFullscreenToggle();
//! Set whether page is top-level
document.body.classList.toggle('inIframe', Page.inIframe())
//! Initialize colortheme toggle button
colorTheme.initElems()
//! Add site navigation button click events
navButtonDrawer.init()
//! Run each startScript
startScripts.forEach(f => f())
})
window.addEventListener('pageshow', () => {
pageShowScripts.forEach(f => f())
if (El.q('body>main') && !Starry.preventStart && !Page.inIframe()) {
new Starry(El.q('body>main'), { starryBackground: starryBackgroundAtts })
}
})
window.addEventListener('popstate', () => {
popStateScripts.forEach(f => f())
})
//! Messages from a parent menu page to the iframe that is currently displaying this page
window.addEventListener('message', e => {
if (e.origin !== window.origin) return;
const { type } = e.data;
switch (type) {
case 'themeToggle': colorTheme.renderTheme(e.data.theme); break;
}
})
function initializeFullscreenToggle() {
const btn = El.ev('#svgIcon_sizeBox', 'click', () => {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.documentElement.requestFullscreen();
}
})
El.ev(document, 'fullscreenchange', () => btn.classList.toggle('sizeDown', !!document.fullscreenElement))
}
class ColorTheme extends EventTarget {
static DARK = 'dark'
static LIGHT = 'light'
constructor() {
super();
/* Prefer theme set stored in cookie but honor light preference and default to dark */
const csq = window.matchMedia('(prefers-color-scheme: light)');
const theme = Cookie.get('theme') ?? (csq.matches ? ColorTheme.LIGHT : ColorTheme.DARK);
this.renderTheme(theme)
El.ev(csq, 'change', e=>{this.toggleTheme(e.matches ? ColorTheme.LIGHT : ColorTheme.DARK, false)})
}
renderTheme(theme) {
this.theme = theme;
document.documentElement.setAttribute('data-theme', theme)
}
toggleTheme(theme) {
this.renderTheme(theme);
Cookie.set('theme', theme, 365)
this.dispatchEvent(new Event('toggle'))
}
initElems() {
this.button = El.ev('#svgIcon_sunMoon', 'click', this.toggle.bind(this))
}
toggle() {
this.toggleTheme(this.theme === ColorTheme.DARK ? ColorTheme.LIGHT : ColorTheme.DARK);
}
}
ColorTheme.themes = {
DARK: ColorTheme.DARK,
LIGHT: ColorTheme.LIGHT
}
class NavButtonDrawer {
visible = false;
constructor() { }
init() {
this.button = El.q('#svgIcon_hamburger');
this.menu = El.q('#siteMenu')
El.ev(this.button, 'click', (e) => { this.toggleVisible(); e.stopPropagation(); })
El.ev(this.menu, 'click', e => { e.stopPropagation(); })
El.ev(document.body, 'click', e => { if (this.visible) { this.toggleVisible() } })
}
toggleVisible() {
this.menu.classList.toggle('visible', this.visible = !this.visible)
//TODO if visible, move focus to first link and trap focus within menu. Restore focus to original on menu close.
}
}
export const colorTheme = new ColorTheme();
export const navButtonDrawer = new NavButtonDrawer();
Select All
import { List, Nums, RGB } from './array.js'
//` EMPTY DEFS OF STANDARD JS OBJECTS/TYPES TO QUIETLY DISMISS ERRORS THAT WOULD OTHERWISE OCCUR WHEN IMPORTED ON NODE, WHERE THESE OBJECTS/TYPES DO NOT EXIST
if (typeof window === 'undefined') { globalThis.window = globalThis }
if (typeof Path2D === 'undefined') { globalThis.Path2D = class { } }
//* HTML / DOCUMENT / CLIENT
export class El {
/** @param {HTMLElement | string} el */ static isHidden(el) { return El.q(el).classList.contains('hidden') }
/** @param {HTMLElement | string} el */ static hide(el, hide = true) { if (Array.isArray(el)) {el.forEach(e => El.hide(e, hide)); return} El.q(el).classList.toggle('hidden', hide) }
/** @param {HTMLElement | string} el */ static show(el, show = true) { if (Array.isArray(el)) {el.forEach(e => El.show(e, show)); return} El.q(el).classList.toggle('hidden', !show) }
/**
* @param {HTMLElement | HTMLElement[] | string | string[]} elementsToHide
* @param {HTMLElement | HTMLElement[] | string | string[]} elementsToShow
*/
static swapShown(elementsToShow,elementsToHide) {
Arr.ensureList(elementsToShow).forEach(e => El.show(e));
Arr.ensureList(elementsToHide).forEach(e => El.hide(e));
}
/** @param {HTMLElement | string} el */ static isSelected(el) { return El.q(el).classList.contains('selected') }
/** @param {HTMLElement | string} el */ static select(el, select = true) { if (Array.isArray(el)) {el.forEach(e => El.select(e, select)); return} El.q(el).classList.toggle('selected', select) }
/** @param {HTMLElement | string} el */ static unselect(el, unselect = true) { if (Array.isArray(el)) {el.forEach(e => El.unselect(e, unselect)); return} El.q(el).classList.toggle('selected', !unselect) }
static toggleHidden(el) { El.q(el).classList.toggle('hidden') }
/** Recursive query selector
* @param {string | HTMLElement} p @param {...string} q @returns {HTMLElement} */
static qr(p, ...q) {
if (q.length) {
return El.qr(El.qr(p).querySelector(q[0]), ...q.slice(1));
} else {
return typeof p === 'string' ? document.querySelector(p) : p
}
}
/**
* Element query selector
* @overload
* @param {HTMLElement | string} a
* @returns {HTMLElement}
*
* @overload
* @param {HTMLElement[] | string[]} a
* @returns {HTMLElement[]}
*
* @overload
* @param {HTMLElement | string} a
* @param {HTMLElement | string | undefined} b
* @returns {HTMLElement}
* @overload
*
* @param {HTMLElement[] | string[]} a
* @param {HTMLElement[] | string[] | undefined} b
* @returns {HTMLElement[]}
*/
static q(a, b) {
/* ORIGINAL */
// return typeof a === 'string' ? (b ? El.q(b) : document).querySelector(a) : a // <-- original
/* NEW */
if (!b) {
// No secondary argument - parse for the first arguement only at document level
if (typeof a === 'string') { return document.querySelector(a) }
if (a instanceof HTMLElement) { return a }
if (Array.isArray(a)) { return List.from(a).map(el => El.q(el)) }
} else {
// Parsing for the secondary argument rather than the first
if (typeof a === 'string') {
// Parse b as child of unknown a
if (b instanceof HTMLElement) { return b }
const _a = El.q(a)
if (typeof b === 'string') { return El.q(_a, b) }
if (Array.isArray(b)) { return List.from(b).map(_b => El.q(_a, _b)) }
}
if (a instanceof HTMLElement) {
// Parse b as child of element a
if (typeof b === 'string') { return a.querySelector(b) }
if (b instanceof HTMLElement) { return b }
if (Array.isArray(b)) { return List.from(b).map(_b => El.q(a, _b)) }
}
if (Array.isArray(a)) { return List.from(a).map(_a => El.q(_a, b)) }
}
return b ?? a;
}
/**
* Can take one or two arguments:
*
* ONE ARGUMENT:
* - If arg is string (list selector), returns a list of all elements matching the query selection string supplied
* - If arg is array, the function may be being used as a parser: parse each item in list
* TWO ARGUMENTS:
* - Arg 1 must be string (singular selector) or element, which is parsed and used as the root of B
* - If arg 2 is a string, returns list of all elements matching selector within A
* - If arg 2 is an array, recursively parse each B selector or element within the context of A
* @returns {HTMLElement[]}
* */
static l(a, b) {
// /* ORIGINAL */
// return typeof a === 'string' ? Array.from((b ? El.q(b) : document).querySelectorAll(a)) : a
/* NEW */
if (!b) {
if (typeof a === 'string') { return List.from(document.querySelectorAll(a)) }
if (Array.isArray(a)) { return List.from(a.map(_a => El.q(_a))) }
} else {
// Second argment denotes children of the singular selector, A
if (Array.isArray(a)) {
return List.from(a.map(_a => El.l(_a, b)))
} else {
if (typeof b === 'string') { return List.from(El.q(a)?.querySelectorAll(b)) }
if (Array.isArray(b)) { const _a = El.q(a); return List.from(b.map(_b => El.l(_a, _b))) }
}
}
return b ? [b] : [a]
}
static v(q, v) { const el = El.q(q); if (v !== undefined) { el.value = v; } else { return el.value; } }
static nv(q) { return parseFloat(El.v(q)) }
static vEqual(p, q) { return El.v(p) === El.v(q) }
static ev(o, ...event) { const el = El.q(o); el?.addEventListener(...event); return el; }
/** @param {string} type @param {ElementCreationAtts} atts */
static new(type, atts = {}, children = []) {
const el = document.createElement(type);
Obj.forEach(atts, (v, key) => {
switch (key) {
case 'style':
if (typeof v === 'string') { el.style = v }
else { Obj.forEach(v, (styleVal, styleKey) => { el.style[styleKey] = styleVal }) }
break;
case 'class': el.classList.add(v); break;
case 'classList': if (v && v.length) { el.classList.add(...v.filter(v => v.length)); } break;
case 'children': v.forEach(c => el.appendChild(c)); break;
case 'attributes': Obj.forEach(v, (val, key) => el.setAttribute(key, String(val))); break;
// The following I'm unsure whether I need to use setAttribute on. TODO test these.
case 'for': el.setAttribute('for', v); break;
default:
// If anything isn't working, use it within "attributes".
el[key] = v;
}
})
children?.filter(c => !!c).forEach(c => el.appendChild(c))
return el;
}
static apndNew(parent, type, atts = {}, children = []) {
return El.q(parent).appendChild(El.new(type, atts, children));
}
static copyClasses(source, target) {
const classes = Array.from(El.q(source).classList);
if (target) { classes.forEach(c => { target.classList.add(c) }) }
return classes
}
static elemCreationAtts_addClasses(classes, atts) {
const appendClasses = Array.isArray(classes) ? classes : [classes]
const givenClassList = atts?.classList ?? [];
const newAtts = Obj.join({}, atts)
newAtts.classList = [...appendClasses, ...givenClassList]
return newAtts;
}
static addClassesToAtts(atts, ...classes) {
const a = Obj.join({}, atts)
a.classList = [...(a.classList ?? []), ...classes]
return a;
}
static div(atts = {}, children = []) { return El.new('div', atts, children) }
static span(atts = {}, children = []) { return El.new('span', atts, children) }
static br(atts = {}) { return El.new('br', atts) }
static hr(atts = {}) { return El.new('hr', atts) }
static tabbedClickElement(elem, callback) { const el = El.q(elem); el.setAttribute('tabIndex', "0"); El.ev(el, 'keydown', e => { if (e.key === 'Enter' || e.key === ' ') { el.click(); callback?.(e) } }) }
constructor(q, p) { this.el = El.q(q, p) }
hide(hide = true) { El.hide(this.el, hide) }
show(show = true) { El.show(this.el, show) }
// q(q) { return El.q(q, this.el) }
// l(q) { return El.l(q, this.el) }
v(v) { return El.v(this.el, v) }
nv() { return El.nv(this.el) }
get isHidden() { return El.isHidden(this.el) }
text(t) { this.el.innerText = t }
ctog(c, b) { this.el.classList.toggle(c, b) }
save() {
this.savedStyle = this.el.style.cssText;
this.savedClass = this.el.className;
}
restore() {
this.el.style.cssText = this.savedStyle
this.el.className = this.savedClass
}
static makeClassToggleButton(className, button, elementsToToggle, callback) {
const elems = elementsToToggle.map(el => El.q(el))
const f = e => {
elems.forEach(el => el.classList.toggle(className))
callback?.(e);
}
El.ev(button, 'click', f)
return {
el: button,
toggle: f
}
}
/** @param {{itemID: tooltipID}} */
static linkToolTips(map, delay = 500) {
/*
Tooltip structure:
<div><content></content><div>
*/
Object.entries(map).forEach(([itemID, tooltipID]) => {
let timeout, tooltipHover, itemHover
const toolTip = El.q(`#${tooltipID}`);
const hoverable = El.q(`#${itemID}`);
toolTip.classList.add('toolTip')
let content;
if (toolTip.children.length) {
(content = toolTip.children[0]).classList.add('content')
}
if (!content) {
const innerText = toolTip.textContent; toolTip.textContent = "";
toolTip.appendChild(content = El.div({ class: 'content', innerText }))
}
content.before(El.new('header', {}, [El.div({ innerText: '!' })]))
const tryRemove = () => { if (!itemHover && !tooltipHover) { toolTip.classList.remove('visible') } }
const scheduleRemove = () => { timeout = setTimeout(tryRemove, 50) }
El.ev(hoverable, 'pointerover', () => {
const doAt = performance.now() + delay;
itemHover = true;
if (!toolTip.classList.contains('visible')) {
timeout = setTimeout(() => {
toolTip.classList.add('visible')
}, doAt - performance.now())
}
})
El.ev(hoverable, 'pointerout', () => {
itemHover = false;
if (toolTip.classList.contains('visible')) { scheduleRemove() } else {
// Left before tooltip could appear
clearTimeout(timeout)
}
})
El.ev(hoverable, 'pointerdown', () => {
// Remove on mousedown
if (toolTip.classList.contains('visible')) { toolTip.classList.remove('visible') } else { clearTimeout(timeout) }
})
El.ev(toolTip, 'pointerover', () => { tooltipHover = true })
El.ev(toolTip, 'pointerout', () => { tooltipHover = false; scheduleRemove(); })
})
}
}
export const Events = {
/** @param {Event} e */ stopPropagation(e) { e.stopPropagation(); },
/** @param {Event} e */ preventDefault(e) { e.preventDefault(); },
/** @param {Event} e */ stop(e) { e.stopPropagation(); e.preventDefault(); },
}
export const Page = {
inIframe: () => window.self !== window.top,
}
export const Clip = {
copyToClipBoard(text) {
try {
navigator.clipboard.writeText(text)
} catch (err) {
console.error('Clipboard write failure: ', err)
}
}
}
export const Style = {
setVar(varname, value, el = document?.documentElement) {
El.q(el).style.setProperty(varname.startsWith('--') ? '' : '--' + varname, value)
},
getVar(varname, el = document?.documentElement) {
return getComputedStyle(El.q(el)).getPropertyValue(varname.startsWith('--') ? '' : '--' + varname)
},
fetchCSS: async function (cssHref) {
if (Style._sheetPromise) return Style._sheetPromise; // memoize
if (!('adoptedStyleSheets' in Document.prototype)) {
// Fallback: inject <link> once
await ensurePanelStyles(cssHref);
return null;
}
Style._sheetPromise = fetch(cssHref)
.then(r => r.text())
.then(cssText => {
const sheet = new CSSStyleSheet();
sheet.replaceSync(cssText);
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
sheet,
];
return sheet;
});
return Style._sheetPromise;
},
addStyleToPage(cssString) {
const docStyle = document.createElement('style');
docStyle.innerHTML = cssString;
document.head.appendChild(docStyle);
},
applyToElement(el, style) { Object.entries(style).forEach(([key, val]) => { el.style[key] = val; }) },
applyToElements(style, ...elements) { Object.entries(style).forEach(([key, val]) => { elements.forEach(el => { el.style[key] = val }) }) },
clearElementStyle(el) {
const element = El.q(el)
const style = element.style;
for (let i = style.length - 1; i >= 0; i--) {
const name = style[i];
style.removeProperty(name);
}
return element;
}
}
export const Cookie = {
/**
*
* @param {string} name
* @param {*} value
* @param {days} expiration
*/
make: function (name, value, expiration) {
const d = new Date();
d.setTime(d.getTime() + (expiration * 24 * 60 * 60 * 1000));
let expires = "expires=" + d.toUTCString();
document.cookie = name + "=" + value + ";" + expires + "; path=/";
},
get: function (cname) {
let name = cname + "=";
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
}
Cookie.set = Cookie.make;
export const ClientDevice = {
checkIsMobile() {
const a = (navigator.userAgent || navigator.vendor || window.opera)
return !!(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4)))
},
checkIsTouchDevice() {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
const a = (navigator.userAgent || navigator.vendor || window.opera)
return !!(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4)))
}
}
//* QUICK FUNCTIONS
export const For = {
i(n, f) { for (let i = 0; i < n; i++) { f(i) } },
iRev(n, f) { for (let i = n - 1; i >= 0; i--) { f(i) } },
rec2(sizes, fn, inherited = []) {
const i = sizes[0]
if (sizes.length > 1) {
const nextInds = sizes.slice(1)
for (let _i = 0; _i < i; _i++) { For.rec2(nextInds, fn, [...inherited, _i]) }
} else {
for (let _i = 0; _i < i; _i++) { fn(...inherited, _i) }
}
},
/** Recursive for loop. IE For.rec([1,2,3], (i,j,k)=>{})
* This is the more efficient version than rec2.
*/
rec(sizes, fn) {
const indices = new Array(sizes.length)
function recurse(depth) {
if (depth === sizes.length) { return fn(...indices) }
for (let i = 0; i < sizes[depth]; i++) {
indices[depth] = i
recurse(depth + 1);
}
}
recurse(0)
},
}
//* STANDARD OBJECT TYPE METHODS
export const Arr = {
/** @returns {*[]} */
ensureList: a => { return Array.isArray(a) ? a : [a] },
/**
* @param {Array} a Array of key values
* @param {Function} f f(key) Function of key that computes values respective to each key in returned object
* @returns {Object} Object with keys matching the values in array a and values defined by function of keys, f
*/
keysToObject: (a, f) => { const d = {}; a.forEach(k => { d[k] = f(k) }); return d },
/**
* @param {Number} n Number of elements in array
* @param {*} f f(index) Function of index that defines element in returned array
* @returns {*[]} N-sized array of elements defined by function of index
*/
make: (n, f, dups) => Array.from({ length: n }, (_, i) => typeof f === 'function' ? f(i) : (dups ? f : Obj.clone(f))),
/**
* @param {Number} n Number of elements in array
* @param {*} o Default object to be cloned as each element in returned array
* @returns {*[]} N-sized array of where each element is a clone of o
*/
templateObjs: (n, o) => Arr.make(n, o, false),
/**
* @param {Number} r Number of rows in grid
* @param {Number} c Number of columns in grid
* @param {Function} frc f(row,col) Function of row and column that defines element in returned grid
* @returns {*[][]} RxC-sized grid of elements defined by function of row and column
*/
makeGrid: (r, c, frc) => Arr.make(r, _r => Arr.make(c, _c => frc(_r, _c))),
/**
* @param {Number} d Number of rows and columns in grid
* @param {Function} frc f(row,col) Function of row and column that defines element in returned grid
* @returns {*[][]} dxd-sized grid of elements defined by function of row and column
*/
makeSquareGrid: (d, frc) => Arr.make(d, _r => Arr.make(d, _c => frc(_r, _c))),
/**
* @description One-liner 0 to n-1 for loop
* @param {Number} n Number of elements in array
* @param {Function} f f(index) Function of index to perform n times
*/
forEach: (n, f) => Array(n).fill(0).forEach((_, i) => f(i)),
/**
* @description Get random index from array
* @param {Array} arr Array to get random index from
*/
randomIndex: arr => Num.randomInteger(0, arr.length - 1),
/**
* @description Get random item from array
* @param {Array} arr Array to get random item from
*/
randomItem: arr => arr[Arr.randomIndex(arr)],
/**
* @description Get next index in array
* @param {Array} arr Array to get next index from
*/
nextIndex: (arr, i, goAround = true) => i < (arr.length - 1) ? i + 1 : (goAround ? 0 : false),
/**
* @description Get previous index in array
* @param {Array} arr Array to get previous index from
*/
prevIndex: (arr, i, goAround = true) => i ? i - 1 : (goAround ? arr.length - 1 : false),
/**
* @description Get next item in array
* @param {Array} arr Array to get next item from
*/
nextItem: (arr, i, goAround = true) => i < (arr.length - 1) ? arr[i + 1] : (goAround ? arr[0] : false),
/**
* @description Get previous item in array
* @param {Array} arr Array to get previous item from
*/
prevItem: (arr, i, goAround = true) => i ? arr[i - 1] : (goAround ? arr[arr.length - 1] : false),
/**
* @param {Array} arr Array of simple values
* @returns {Array} Array with duplicates removed
*/
removeDups: (arr) => arr.filter((v, i, a) => a.indexOf(v) === i),
/**
* @param {Array} arr Array of objects
* @returns {Array} Array with duplicates removed
*/
removeDupsDeep: a => {
const data = {}
a.forEach(v => { data[JSON.stringify(v)] = v; })
return Obj.toArray(data)
},
/** */
removeAtIndex: (arr, i, preserveOrder = false) => {
let removed;
if (preserveOrder) {
removed = arr.splice(i, 1);
} else {
removed = arr[i];
arr[i] = arr[arr.length - 1];
arr.pop();
}
return removed;
},
removeIfExists: (arr, o, preserveOrder = false) => {
let i = arr.findIndex(e => e === o);
if (i > -1) { return Arr.removeAtIndex(arr, i, preserveOrder) }
},
/**
* @param {Array} arr Array
* @returns {Array} Array with only unique elements
*/
uniques: a => {
const counter = {}
a.forEach(v => {
const str = JSON.stringify(v);
if (counter[str]) { counter[str] += 1 } else { counter[str] = 1 }
})
const ret = []
for (let jstr in counter) { if (counter[jstr] < 2) { ret.push(JSON.parse(jstr)) } }
return ret;
},
/**
* @param {Array} arr Array
* @returns {Boolean} True if all elements are unique
*/
allUnique: (a) => {
let unique = true;
const o = {};
for (let i = 0; i < a.length; i++) {
if (o[a[i]]) {
unique = false;
break;
} else {
o[a[i]] = true;
}
}
return unique;
},
/**
* @param {Array} arr Array
* @param {*} i End index offset
* @returns {*} Item from array i elements from the end
*/
last: (arr, i = 0) => (arr?.length && arr.length > (1 + i)) ? arr[arr.length - 1 - i] : undefined,
/**
* @description Push an element to an array but remove the first element to preserve length
* @param {Array} arr Array
* @returns {Array} Array with first element removed and a newly appended final element
*/
shiftPush: (arr, val) => { arr.shift(); arr.push(val); return arr },
/**
* @param {Array} arr Array
* @param {Number} start Start index
* @param {Number} stop End index (unincluded in average calculation)
* @returns {Number} Average of array
*/
average: (arr, start, stop) => Nums.mean(arr.slice(start, stop)),
/**
* @param {Array} arr Array
* @returns {Number} Min and max values in array
*/
minmax: (a) => { return { min: Math.min(...a), max: Math.max(...a) } },
/**
* @param {Array} arr Array
* @returns {Number} Index of max value in array
*/
indexOfMax: (arr) => { const max = Math.max(...arr); return arr.findIndex(v => v == max) },
/**
* @param {Array} arr Array
* @returns {Number} Index of min value in array
*/
indexOfMin: (arr) => { const min = Math.min(...arr); return arr.findIndex(v => v == min) },
/**
* @param {Array} arr Array
* @returns {Array} Array transposed (rotated) 90 degrees counter clockwise
*/
transposeCCW: (a) => {
const rows = a.length, cols = a[0].length;
const result = Array.from({ length: cols }, () => Array(rows));
for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { result[cols - 1 - c][r] = a[r][c]; } }
return result;
},
/**
* @param {Array} arr Array
* @returns {Array} Array transposed (rotated) 90 degrees clockwise
*/
transposeCW: (a) => {
const rows = a.length, cols = a[0].length;
const result = Array.from({ length: cols }, () => Array(rows));
for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { result[c][rows - 1 - r] = a[r][c]; } }
return result;
},
/**
* @param {Array} arr NxN Array
* @returns {Array} NxN copy of array, mirrored horizontally
*/
flipHorizontally: (a) => a.map(row => row.slice().reverse()),
/**
* @param {Array} arr NxN Array
* @returns {Array} NxN copy of array, mirrored vertically
*/
flipVertically: (a) => a.slice().reverse(),
/**
* @description Shuffles an array
* @param {Array} arr Array
* @returns {Array} Reference to the same array, but shuffled
*/
shuffle: (a) => {
let ci = a.length;
while (ci > 0) {
const ri = Math.floor(Math.random() * ci--);
[a[ci], a[ri]] = [a[ri], a[ci]];
}
return a;
},
/**
* @param {Array} arr Array
* @returns {Array} Array cycled forward (push first element to the end, second to first, etc)
*/
cycledForward: a => [...a.slice(1), a[0]],
/**
* @param {Array} arr Array
* @returns {Array} Array cycled backward (push last element to the start, first to second, etc)
*/
cycledBackward: a => [a[a.length - 1], ...a.slice(0, -1)],
/**
* @description Cycles an array forward (push first element to the end, second to first, etc)
* @param {Array} arr Array
* @returns {Array} Reference to the same array, but cycled forward
*/
cycleForward: (a) => {
a.push(...Arr.cycledForward(a.splice(0, a.length)))
return a
},
/**
* @description Cycles an array backward (push last element to the start, first to second, etc)
* @param {Array} arr Array
* @returns {Array} Reference to the same array, but cycled backward
*/
cycleBackward: (a) => {
a.push(...Arr.cycledBackward(a.splice(0, a.length)))
return a
},
/**
* @description Pairs elements from an array sequentially and returns the pairs as an array
* @param {Array} arr Array
* @returns {Array} Array with elements as pairs
*/
pairFlattenedList: a => {
const arr = [];
for (let i = 0; i < a.length; i += 2) {
arr.push([a[i], a[i + 1]]);
}
return arr;
},
/**
* @description Generates an array from a range of numbers
* @param {Number} start Starting number
* @param {Number} end End number
* @param {Number} step Step size
*/
range: (start, end, step = 1) => Array.from({ length: (end - start) / step + 1 }, (_, i) => start + i * step),
}
export const Obj = {
isPlainObject: obj => obj?.constructor === Object,
/**
* @template {{}} A @template {{}} B @template {{}} C @template {{}} D @template {{}} E
* @param {[A,B,C,D,E]} args
* @returns {A & B & C & D & E}
* */
join: function (...args) {
const merged = {};
const joinProps = obj => {
for (const prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
const op = obj[prop]; const mp = merged[prop];
if (Obj.isPlainObject(op) && Obj.isPlainObject(mp)) {
// Both are plain objects -> deep merge
merged[prop] = Obj.join(mp, op);
} else {
// Anything else (including class instances) -> overwrite
merged[prop] = op;
}
}
}
};
for (let i = 0; i < args.length; i++) {
if (Obj.isPlainObject(args[i])) { joinProps(args[i]); }
}
return merged;
},
/** Create an object that represents the differences between two input objects */
differentiate: function (a, b, attributes) {
const atts = Obj.join({ asObject: true, getParts: 0 }, attributes)
let ret = undefined
const addToRet = (k, va, vb) => {
if (ret === undefined) { ret = {} };
ret[k] = atts.getParts ? (atts.getParts > 1 ? vb : va) : (atts.asObject ? { objA: va, objB: vb } : [va, vb])
}
for (let k in a) {
if (b.hasOwnProperty(k)) {
if (typeof a[k] === 'object' && typeof b[k] === 'object' && a[k] !== null && b[k] !== null) {
const diff = Obj.differentiate(a[k], b[k], atts)
if (diff !== undefined) { if (ret === undefined) { ret = {} }; ret[k] = diff };
} else if (a[k] !== b[k]) {
addToRet(k, a[k], b[k])
}
} else {
addToRet(k, a[k], 'missing')
}
}
for (let k in b) {
if (!a.hasOwnProperty(k)) {
addToRet(k, 'missing', b[k])
}
}
return ret
},
/**
* @param {*} o
* @param {*} f ( val, key, index ) => {}
*/
find: function (o, f) {
return o[Object.keys(o).find((k, i) => f(o[k], k, i))];
},
/**
* @param {*} o
* @param {function(val,key,index)} f ( val, key, index ) => {}
*/
map: function (o, f) { return Object.entries(o).map(([k, v], i) => f(v, k, i)) },
omap: function (o, f) { return Object.fromEntries(Object.entries(o).map(([k, v], i) => [k, f(v, k, i)])) },
/**
* @param {*} o
* @param {*} f ( val, key, index ) => {}
*/
forEach: function (o, f) { Object.entries(o).forEach(([k, v], i) => f(v, k, i)); },
length: function (o) { return Object.keys(o).length },
/**
* @param {*} o
* @param {*} f ( val, key ) => {}
*/
keyMap(o, f) {
const r = {};
for (let key in o) { r[key] = f(o[key], key) }
return r;
},
toArray(o) {
return Object.keys(o).map(key => o[key])
},
/** Given a list of keys and a list of values, create an object of respective keys and values by index
* ie keys = ['a', 'b', 'c'] and values = [1, 2, 3] returns { a: 1, b: 2, c: 3 }
*/
fromArrays(keys, values) { return keys.reduce((o, k, i) => { o[k] = values[i]; return o }, {}) },
ofClones(keys, o) { return Obj.fromArrays(keys, Arr.templateObjs(keys.length, o)) },
/**
* @param {*} o
* @param {*} f ( obj[key], key, index ) => {}
*/
some: function (o, f) {
return Object.keys(o).some((k, i) => f(o[k], k, i));
},
/** @template O @param {O} o @returns {O} */
clone(o) { return JSON.parse(JSON.stringify(o)) },
}
//* STANDARD CONSTANT TYPE METHODS
export const Parse = {
func: (f, ...a) => typeof f === 'function' ? f(...a) : f,
doIfTruthy: (o, f, ...a) => { if (!!o) { return f(o, ...a) } },
}
/** @type {Number} "Infinity" for practical purposes (equal to Number.MAX_SAFE_INTEGER) */ export const Inf = Number.MAX_SAFE_INTEGER;
/** @type {Number} "Negative Infinity" for practical purposes (equal to Number.MIN_SAFE_INTEGER) */ export const Ninf = Number.MIN_SAFE_INTEGER;
export class Num {
static parse(v) {
if (typeof v === 'string') {
return new this(v.replaceAll(/[ a-zA-Z]/g, ''))
} else {
return new this(v)
}
}
get standardDisplayDecimals() { return 2 }
static units = ''
get units() { return this.constructor.units }
constructor(val) { this.val = Number(val) }
disp(numDec) { return `${Num.roundToNumDec(this.val, numDec ?? this.standardDisplayDecimals)}${this.units}` }
static quickDisp(v, numDec) { return new this(v).disp(numDec) }
/**
* @overload
* @param {number} t ratio
* @param {number} iMin Input (x) range minimum
* @param {number} iMax Input (x) range maximum
* @param {number} oMin Output (y) range minimum
* @param {number} oMax Output (y) range maximum
* @returns {number}
*
* @overload
* @param {number} t ratio
* @param {number} iMin Input (x) range minimum
* @param {number} oMin Output (y) range minimum
* @param {number} slope Slope of range spans (output / input)
* @returns {number}
*/
static lerp(t, ...args) {
if (args.length === 4) {
const [minInput, maxInput, minOutput, maxOutput] = args;
return minOutput + (t - minInput) * (maxOutput - minOutput) / (maxInput - minInput)
} else {
const [minInput, minOutput, slope] = args;
return minOutput + (t - minInput) * slope;
}
}
clamp(min, max) { this.val = this.constructor.clamp(this.val, min, max); return this; }
static clamp = (n, min, max) => { return Math.max(min ?? Ninf, Math.min(max ?? Inf, n)) }
padClamp(pad, min, max) { this.val = this.constructor.padClamp(this.val, pad, min, max); return this; }
static padClamp = (n, pad, min, max) => { return Math.max(min + pad, Math.min(max - pad, n)) }
clampAround(min, max) { this.val = this.constructor.clampAround(this.val, min, max); return this; }
static clampAround = (v, min, max) => {
if (Num.within(v, min, max)) { return v; }
const d = max - min;
let vv = v - min;
while (vv < 0) { vv += d }
while (vv > d) { vv -= d }
return vv + min;
}
get orderOfMagnitude() { return this.constructor.orderOfMagnitude(this.val) }
static orderOfMagnitude(v) {
if (!v) return undefined;
if (!isFinite(v)) return Infinity;
return Math.floor(Math.log10(Math.abs(v)));
}
get magnitudeFactor() { return this.constructor.magnitudeFactor(this.val) }
static magnitudeFactor(v) {
const e = this.orderOfMagnitude(v);
return (e === undefined || e === Infinity) ? e : 10 ** e;
}
get dataSI() { return this.constructor.getPrefixSIData(this.val) }
static getPrefixSIData = (number, numSigFigs = 4) => {
const orderOfMagnitude = Num.orderOfMagnitude(number);
const baseNumber = number / (10 ** (Math.floor(orderOfMagnitude / 3) * 3))
const prefix = (orderOfMagnitude > 0 ? ' kMGTPEZYRQ' : ' mμnpfazyrq')[(orderOfMagnitude > 0 ? Math.floor : Math.ceil)(Math.abs(orderOfMagnitude) / 3)]
const baseInSigFigs = baseNumber.toFixed(Math.max(0, (numSigFigs - 1) - Num.orderOfMagnitude(baseNumber)))
return { number, orderOfMagnitude, baseNumber, prefix, baseInSigFigs }
}
/** Checks if value lies within a range [min,max], INCLUSIVE of min and max */
within(min, max) { return this.constructor.within(this.val, min, max) }
/** Checks if value lies within a range [min,max], INCLUSIVE of min and max */
static within(v, min, max) { return (v >= min) && (v <= max) }
/** Checks if value lies within a range [min,max], EXCLUSIVE of min and max */
between(min, max) { return this.constructor.between(this.val, min, max) }
/** Checks if value lies within a range [min,max], EXCLUSIVE of min and max */
static between(v, min, max) { return (v > min) && (v < max) }
/** Checks if value lies within a range [min,max], with parameters to determine if min / max should be included */
inside(min, max, incMin, incMax) { return this.constructor.inside(this.val, min, max, incMin, incMax) }
/** Checks if value lies within a range [min,max], with parameters to determine if min / max should be included */
static inside(v, min, max, incMin, incMax) { return (incMin ? v >= min : v > min) && (incMax ? v <= max : v < max) }
randomAround(t) { return new this.constructor(this.constructor.randomAround(this.val, t)) }
static randomAround(v, t) { return v + (Num.randomDecimal(-t, t)) }
get even() { return this.constructor.even(this.val) }
static even(n) {
if (!Num.isNum(n)) { return }
return n % 2 === 0;
}
get odd() { return this.constructor.odd(this.val) }
static odd(n) {
if (!Num.isNum(n)) { return }
return !Num.even(n)
}
roundToNumDec(d = 8) { this.val = this.constructor.roundToNumDec(this.val, d); return this; }
static roundToNumDec(n, d = 8) {
const m = 10 ** d;
return Math.round(n * m) / m;
}
/** Convert base 10 to padded hex string @returns {string} */
static toHex(v) {
const hex = Math.round(v).toString(16);
return hex.length === 1 ? '0' + hex : hex;
}
static forceNumeric(v, def = 0) {
const val = parseFloat(v);
return isNaN(val) ? def : val;
}
static isNum = v => Number.isFinite(v)
static PRECISION = 1e-6
static zeroPad = (n, d = 2, placeholder = '0') => `${List.make(16, _ => placeholder).join('')}${n}`.slice(-d)
static random = (min, max) => Math.floor(Math.random() * (1 + parseInt(max) - parseInt(min))) + parseInt(min)
/** Random integer number in range inclusive of min and max */
static randomInteger = (min = 0, max = 10) => Num.random(min, max)
/** Random decimal number in range inclusive of min and max */
static randomDecimal = (min = 0, max = 1) => { return Math.random() * (max - min) + min }
}
export class Px extends Num {
static units = 'px'
get standardDisplayDecimals() { return 0 }
static str(v) { return new Px(v).disp() }
}
export class MaxedNum extends Num {
static typicalMax = 1;
static random(max) { return Num.randomDecimal(0, max ?? this.typicalMax) }
}
export class Pct extends MaxedNum {
static units = '%'
constructor(a, b) { super((a ?? 0) / (b ?? 1)) }
disp(numDec) { return `${Num.roundToNumDec(this.percent, numDec ?? this.standardDisplayDecimals)}${this.units}` }
get percent() { return this.val * 100 }
get standardDisplayDecimals() { return 2 }
}
export class Rad extends MaxedNum {
static units = 'rad'
static Pi2 = Math.PI * 2
static typicalMax = this.Pi2
get deg() { return Unit.deg(this.val) }
get cDeg() { return new Deg(this.deg) }
}
export class Deg extends MaxedNum {
static units = '°'
static typicalMax = 360
get standardDisplayDecimals() { return 1 }
get rad() { return Unit.rad(this.val) }
get cRad() { return new Rad(this.rad) }
}
const defaultStrRandomizationAtts = {
lower: true,
upper: false,
numeric: false,
urlsafechars: false,
operators: false,
braces: false,
punctuation: false,
}
export class Str {
static sentenceCase(s) {
return `${s.charAt(0).toUpperCase()}${s.slice(1).toLowerCase()}`
}
static titleCase(s) {
return s.split(' ').map(w => Str.sentenceCase(w)).join(' ')
}
static randomFrom(s, numchars) {
return Array.from({length: numchars}, () => s.charAt(Math.floor(Math.random() * s.length))).join('');
}
static defaultRandomizationAtts = defaultStrRandomizationAtts
/** @param {defaultStrRandomizationAtts} _atts */
static random(length, _atts) {
const atts = Obj.join(defaultStrRandomizationAtts, _atts)
const str = [
atts.lower ? Str.chars.lower : '',
atts.upper ? Str.chars.upper : '',
atts.numeric ? Str.chars.numeric : '',
atts.urlsafechars ? Str.chars.urlsafechars : '',
atts.operators ? Str.chars.operators : '',
atts.braces ? Str.chars.braces : '',
atts.punctuation ? Str.chars.punctuation : '',
].join('')
return Str.randomFrom(str, length)
}
static reverse(s) {
return s.split('').reverse().join('');
}
static alphabeticSort(a, b) {
if (a < b) { return -1; }
if (a > b) { return 1; }
return 0
}
static blank(length) {
return List.make(length, _ => ' ').join('')
}
static chars = {
lower: 'abcdefghijklmnopqrstuvwxyz',
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
numeric: '1234567890',
urlsafechars: '~-_.',
operators: '@#$%^&*~-+=',
braces: '()<>[]{}',
punctuation: '_,.?!;:\'"/\\|',
}
static containsNumbers(str) { return /\d/.test(str) }
}
Str.allChars = Object.values(Str.chars).join('')
const defaultIDRandomizationAtts = {
lower: false,
upper: true,
numeric: false,
safechars: false,
operators: false,
braces: false,
punctuation: false,
}
export const ID = {
defaultRandomizationAtts: defaultIDRandomizationAtts,
/** @param {defaultIDRandomizationAtts} atts_ */
generate: function (container, length, atts_) {
const atts = Obj.join(defaultIDRandomizationAtts, atts_)
let id; do {id = Str.random(length, atts)} while (id in container); return id;
},
}
export const Bool = {
xor(a, b) { return !!a !== !!b },
xnor(a, b) { return !!a === !!b }
}
export const Unit = {
/** Suffixes a number with 'px' */
numToPx(n) { return `${n}px` },
/** Converts kilometers to meters */
km_m: function (v) { return v * 1000 },
/** Converts meters to kilometers */
m_km: function (v) { return v / 1000 },
/** Converts meters to centimeters */
m_cm: function (v) { return v * 100 },
/** Converts meters to inches */
m_in: function (v) { return Unit.cm_in(Unit.m_cm(v)) },
/** Converts centimeters to meters */
cm_m: function (v) { return v / 100 },
/** Converts kilometers to centimeters */
km_cm: function (v) { return Unit.m_cm(Unit.km_m(v)) },
/** Converts centimeters to kilometers */
cm_km: function (v) { return Unit.m_km(Unit.cm_m(v)) },
/** Converts centimeters to inches */
cm_in: function (v) { return v / 2.54 },
/** Converts inches to centimeters */
in_cm: function (v) { return v * 2.54 },
/** Converts inches to feet */
in_ft: function (v) { return v / 12 },
/** Converts inches to meters */
in_m: function (v) { return Unit.cm_m(Unit.in_cm(v)) },
/** Converts feet to inches */
ft_in: function (v) { return v * 12 },
/** Converts feet to miles */
ft_mi: function (v) { return v / 5280 },
/** Converts miles to feet */
mi_ft: function (v) { return v * 5280 },
/** Converts kilometers to miles */
km_mi: function (v) { return Unit.ft_mi(Unit.in_ft(Unit.cm_in(Unit.km_cm(v)))) },
/** Converts radians to degrees */
deg: (r) => r * 180 / Math.PI,
/** Converts degrees to radians */
rad: (d) => d * Math.PI / 180,
}
export class Time {
/** @returns {seconds} Seconds since epoch time (This is just Date.now()/1000) */
static stamp() {
return Date.now() / 1000;
}
/** @returns {seconds} Seconds since epoch time (This is just Date.now()/1000) */
static now() {
return Date.now() / 1000;
}
/** @returns {Date} */
static toDate(stamp) {
return new Date(parseFloat(stamp) * 1000)
}
constructor(...args) {
this.stamp = args[0] ? parseFloat(args[0]) : Time.stamp()
}
toDate() { return Time.toDate(this.stamp) }
toReadable() { return Time.toReadable(this.stamp) }
static toReadable(stamp) {
const d = Time.toDate(stamp || Time.stamp());
const kn = Num.zeroPad;
return `${d.getFullYear()
}/${kn(d.getMonth() + 1)
}/${kn(d.getDate())
} ${kn(d.getHours())
}:${kn(d.getMinutes())
}:${kn(d.getSeconds())
}`;
}
static daysToMs(d) {
return d * Time.msInADay
}
static daysToS(d) {
return d * Time.sInADay
}
/** */
static toYYYYMMDD(dt, sep = '.') {
const d = dt || new Date();
return [d.getFullYear(), d.getMonth() + 1, d.getDate()].map(n => Num.zeroPad(n, 2)).join(sep)
}
}
Time.sInADay = 60 * 60 * 24;
Time.msInADay = Time.sInADay * 1000;
export class Color {
/** @param {byte8} r @param {byte8} g @param {byte8} b @returns {[hue,sat,lum]} */
static rgb2hsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h * 360, s, l];
}
/** @param {hue} h @param {sat} s @param {lum} l @returns {rgb} */
static hsl2rgb(h, s, l) {
// normalize & clamp
h = ((h % 360) + 360) % 360;
s = Math.max(0, Math.min(1, s));
l = Math.max(0, Math.min(1, l));
if (s <= 1e-10) {
const v = Math.round(l * 255);
return [v, v, v];
}
const hr = h / 360;
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
const hue2rgb = (t) => {
t = (t + 1) % 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
return [hue2rgb(hr + 1 / 3), hue2rgb(hr), hue2rgb(hr - 1 / 3)].map(v => v * 255);
}
static hex2rgb(_hex) {
const hex = `${_hex}`.replaceAll(' ', '').replaceAll('#', '')
return [0, 2, 4].map(i => parseInt(hex.slice(i, i + 2), 16))
}
static hex2rgba(_hex) {
const hex = `${_hex}`.replaceAll(' ', '').replaceAll('#', '')
return [...Color.hex2rgb(hex), hex.length > 6 ? parseInt(hex.slice(6, 8), 16) : 1]
}
static rgb2hexNumber(r, g, b) { return (r << 16) + (g << 8) + b }
static hexNumber2rgb(h) { return (h >> 16) & 0xff, (h >> 8) & 0xff, h & 0xff }
static rgba2hexNumber(r, g, b) { return (r << 24) + (g << 16) + (b << 8) + a }
static hexNumber2rgba(h) { return (h >> 24) & 0xff, (h >> 16) & 0xff, (h >> 8) & 0xff, h & 0xff }
static rgb2hexString(r, g, b) { return `#${Num.toHex(r)}${Num.toHex(g)}${Num.toHex(b)}` }
static rgba2hexString(r, g, b, a) { return `#${Num.toHex(r)}${Num.toHex(g)}${Num.toHex(b)}${Num.toHex(a)}` }
static name2hexString(name) {
if (!this.name_parser_canvas) {
this.name_parser_canvas = typeof window === 'undefined' ? new OffscreenCanvas(1, 1) : document.createElement('canvas');
this.name_parser_ctx = this.name_parser_canvas.getContext('2d')
}
this.name_parser_ctx.fillStyle = name;
let hex = this.name_parser_ctx.fillStyle;
//todo check if this includes a hashtag
return hex.startsWith('#') ? hex : `#${hex}`
}
static name2rgb(name) { return Color.hex2rgb(Color.name2hexString(name)) }
static rgbCSS(r, g, b) { return `rgb(${r},${g},${b})` }
static rgbaCSS(r, g, b, a) { return `rgba(${r},${g},${b},${a})` }
static achromaticRGBContrastOfRGB(r, g, b) {
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return (brightness > 128) ? [0, 0, 0] : [255, 255, 255];
}
static achromaticRGBContrastOfHSL(h, s, l) {
return l < 0.5 ? [255, 255, 255] : [0, 0, 0];
}
// static list = {
// red: new RGB(255, 0, 0),
// orange: new RGB(255, 165, 0),
// yellow: new RGB(255, 255, 0),
// green: new RGB(0, 128, 0),
// limegreen: new RGB(50, 205, 50),
// lightblue: new RGB(173, 216, 230),
// indigo: new RGB(75, 0, 130),
// violet: new RGB(238, 130, 238),
// blueviolet: new RGB(138, 43, 226),
// purple: new RGB(128, 0, 128),
// brown: new RGB(110, 77, 17),
// gold: new RGB(255, 215, 0),
// blue: new RGB(0, 0, 255),
// black: new RGB(0, 0, 0),
// darkgray: new RGB(50, 50, 50),
// gray: new RGB(125, 125, 125),
// lightgray: new RGB(200, 200, 200),
// white: new RGB(255, 255, 255),
// }
}
/** @typedef {number} degree Value in range [-360,360] */
/** @typedef {number} radian Value in range [-360,360] */
/** @typedef {[degree, degree]} DD Decimal Degrees coordinate pair */
/** @typedef {number} meters Length in meters */
export const GPS = {
/**
* Returns the distance between two coordinates in meters using the Haversine Formula
* @param {degree} latitudeA
* @param {degree} longitudeA
* @param {degree} latitudeB
* @param {degree} longitudeB
* @returns {meters} Distance between the points
*/
distance: function (latitudeA, longitudeA, latitudeB, longitudeB) {
const latA = Unit.rad(latitudeA);
const latB = Unit.rad(latitudeB);
const a = (Math.sin((latB - latA) / 2)) ** 2 + Math.cos(latA) * Math.cos(latB) * (Math.sin((Unit.rad(longitudeB) - Unit.rad(longitudeA)) / 2)) ** 2;
return 6371000 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
},
/**
* Returns the GPS coordinate (in decimal degrees) that is a particular distance and bearing away from another coordinate
* @param {degree} latitude
* @param {degree} longitude
* @param {degree} bearing
* @param {meters} distance
* @returns {[latitude:degree, longitude:degree]} Returns degree values of latitude and longitude as a length 2 array
**/
destination: function (latitude, longitude, bearing, distance) {
const arc = distance / 6371000; // distance in radians
const lat = Unit.rad(latitude);
const bear = Unit.rad(bearing)
const sin_lat = Math.sin(lat)
const cos_arc = Math.cos(arc)
const cos_lat = Math.cos(lat)
const sin_arc = Math.sin(arc)
const latB = Math.asin(Num.clamp(sin_lat * cos_arc + cos_lat * sin_arc * Math.cos(bear), -1, 1));
return [
Unit.deg(latB),
Unit.deg(Unit.rad(longitude) + Math.atan2(Math.sin(bear) * sin_arc * cos_lat, cos_arc - sin_lat * Math.sin(latB)))
]
},
/**
* Returns a list of decimal degree coordinates that are equidistance around a given coordinate
* @param {degree} latitude
* @param {degree} longitude
* @param {meters} radius
* @param {number} numPoints
* @returns {List<DD>} List of decimal degree coordinates
*/
coordinateCircle(latitude, longitude, radius, numPoints = 60) {
return List.make(numPoints, i => GPS.destination(latitude, longitude, 360 / numPoints * i, radius))
}
}
//* DEBUG BASIS CLASS
//* LOGGING
export class Logger {
lastLogTime = 0;
minLogInterval = 500;
constructor() { }
log(...args) {
if (performance.now() - this.lastLogTime > this.minLogInterval) {
console.log(...args);
this.lastLogTime = performance.now();
}
}
flush() { this.lastLogTime = 0; }
flog(...args) { this.flush(); this.log(...args) }
}
//* OBJECT BASIS AND MANAGEMENT
export const StackedClassAtts = {
debug: false,
}
/** @template {StackedClassAtts} A Default attribute set */
export class StackedClass extends EventTarget {
static defaultAtts = StackedClassAtts
#topLevelConstructor;
/** @type {A} */ atts
/** @param {A} atts */
constructor(atts) {
super();
this.atts = Obj.join(this.constructor.defaultAtts, atts)
try { this.name = this.atts.name } catch (err) { if (!(err instanceof TypeError)) { throw err } }
this.init();
this.#topLevelConstructor = new.target
if (this.atts.debug) { StackedClass.dispDebugWarning() }
}
/** @type {string} */ #name; get name() { return this.#name } set name(n) { this.#name = n }
static dispDebugWarning() {
if (!this.__dispDebugWarningDisplayed) {
this.__dispDebugWarningDisplayed = true
console.warn('Debug is set to true on some StackedClass object');
// console.trace();
}
}
debug(...args) {
if (this.atts.debug) {
console.log(this.name ?? 'StackedClass Debug')
args.forEach(arg => console.log(arg))
if (!this.name) { console.trace() }
}
}
/**
* This should be called at the end of the constructor that desires postConstruction methods
* and is intended where those postconstruction methods have processes that are overwritten
* in higher order classes, and may need to be performed only at the end of the construction
* chain. When calling the method, use the actual class that is calling as the argument.
*
* IE in the following, postConstruct will only be called at the end of the constructor defined
* within class B, and will be skipped when called from the constructor of A beforehand.
* - * is StackedClass for the example.
*
* class A extends * {constructor() { this.doPostConstruction(A) }}
* class B extends A {constructor() { this.doPostConstruction(B) }}
* new B()
*/
doPostConstruction(topLevelConstructor) { if (topLevelConstructor === this.#topLevelConstructor) { this.postConstruct(); } }
postConstruct() { }
init() { }
}
// export const NEWCLASS_Atts = Obj.join(StackedClassAtts,{})
// /** @template {NEWCLASS_Atts} A @extends StackedClass<A> */
// export class NEWCLASS_ extends StackedClass {
// static defaultAtts = NEWCLASS_Atts
// /** @param {A} atts */ constructor(atts) {super(atts)}
// }
const ObjectManagerAtts_ = {
ids: {
addToObjects: true,
length: 10,
atts: { lower: true, upper: true }
},
}
/** @type {StackedClassAtts & ObjectManagerAtts_} */
export const ObjectManagerAtts = Obj.join(StackedClassAtts, ObjectManagerAtts_)
/**
* @template {ObjectManagerAtts} A Default attribute set
* @template {Object} T Typical managed type
* @extends StackedClass<A>
*/
export class ObjectManager extends StackedClass {
static defaultAtts = ObjectManagerAtts
/** @type {Object.<T>} */ get $() { return this.objects }
/** @type {Object.<T>} */ objects = {};
/** @type {Set<string>} */ ids = new Set()
/** @type {T[]} */ list = [];
listsByType = new Map();
/** @param {constructor} */
hasListOfType(type) { return this.listsByType.has(type) }
/** @param {constructor} */
listOf(type) { return this.listsByType.get(type) ?? [] }
hasID(id) { return !!id && this.ids.has(id) }
generator(o, _id) {
let id = _id ?? o.id;
if (!id) {
do { id = ID.generate(this.objects, this.atts.ids.length, this.atts.ids.atts) }
while (this.hasID(id))
}
return id;
}
/** @param {T} o @returns {string} */
add(o, _id, addID) {
// Default to secondary ID if provided
// Determine if already exists
let id = _id ?? o?.id;
if (this.hasID(id)) { return this.refresh(o, _id, addID) }
id = this.generator(o, _id);
this.ids.add(id);
// Prevent or allow id addition by object by providing addID as parameter
const setID = addID ?? this.atts.ids.addToObjects
if (setID) { o.id = id }
this.objects[id] = o;
this.list.push(o)
if (this.hasListOfType(o.constructor)) { this.listsByType.get(o.constructor).push(o) }
else { this.listsByType.set(o.constructor, [o]) }
this.onAdd?.(o, id);
return id;
}
/** @param {T} o @returns {string} */
refresh(o, _id, addID) {
this.remove(o, _id);
this.add(o, _id, addID);
}
/**
* @param {T} o
* @param {string} [_id]
* @returns {string}
*/
remove(o, _id) {
const id = _id ?? o?.id
const obj = this.objects[id];
if (this.hasID(id)) {
this.ids.delete(id);
Arr.removeIfExists(this.list, obj, false);
if (this.hasListOfType(obj.constructor)) { Arr.removeIfExists(this.listsByType.get(obj.constructor), obj, false) }
this.onRemove?.(obj, _id);
delete this.objects[id]
return obj;
}
}
removeByID(id) { return this.remove({}, id) }
/** @param {T} o @param {string} id */
onAdd(o, id) { /* To be overridden */ }
/** @param {T} o @param {string} id */
onRemove(o, id) { /* To be overridden */ }
clear() {
this.objects = {};
this.ids = new Set();
this.list = [];
this.listsByType = new Map();
this.onClear?.();
}
}
const ExManagerAtts = Obj.join(ObjectManagerAtts, { exVal: 'example value' })
/** @extends ObjectManager<V2,ExManagerAtts> */
class ExManager extends ObjectManager {
static defaultAtts = ExManagerAtts
/** @param {ExManagerAtts} atts */
constructor(atts) {
super(atts)
this.debug('ExManager created with exVal:', this.atts.exVal)
}
}
export const BalancedRandomizerAtts = Obj.join(StackedClass, {
minItemFreq: 2,
numIndices: 0,
minAcceleration: 1,
jerk: 2, // lol programmers will never understand
maxItemFreq: 1e6,
})
/** @extends StackedClass<BalancedRandomizerAtts> */
export class BalancedRandomizer extends StackedClass {
static defaultAtts = BalancedRandomizerAtts
/** @param {BalancedRandomizerAtts} atts */
constructor(atts) {
super(atts);
this.reset();
}
reset(numIndices) {
if (numIndices) { this.atts.numIndices = numIndices }
this.indexFrequencies = Nums.make(this.atts.numIndices, _ => this.atts.minItemFreq)
this.indexAccelerations = Nums.make(this.atts.numIndices, _ => this.atts.minAcceleration)
// Prime the sequence
For.i(this.atts.numIndices * 10000, this.chooseRandomIndex.bind(this))
}
/** @type {Nums} */ indexFrequencies;
/** @type {Nums} */ indexAccelerations;
/**
* Custom randomization method.
*
* Maintain a list of frequencies of each list index that will be selected. Selections are made from a list of elements
* comprised of (f) instance of each index, where f is the respective frequency for each index for that selection. After
* each selection, the frequency of each index that was not selected increases. The amount of increase for each index
* accelerates (doubles) each time that the index is not selected, so that the likeihood of any index' selection is
* proportional to the number of selection rounds have passed since the index was last selected
* @returns {Number}
*/
chooseRandomIndex() {
/** @type {{min,max}[]} */
let indexRanges = [];
this.indexFrequencies.map((f, i) => {
const min = !i ? 0 : (indexRanges[i - 1].max + 1)
indexRanges.push({ min, max: min + f - 1 })
})
const randSelection = Num.randomInteger(0, this.indexFrequencies.sum - 1)
const index = indexRanges.findIndex(r => Num.within(randSelection, r.min, r.max))
this.indexFrequencies.plus(this.indexAccelerations).setComp(index, this.atts.minItemFreq).clamp(this.atts.minItemFreq, this.atts.maxItemFreq, false)
this.indexAccelerations.plusU(this.atts.jerk).setComp(index, this.atts.minAcceleration)
return index;
}
}
//* GAME LOOPING
export class Action {
/** @type {function()} */ action;
/** @type {number} */ time;
/** @type {string} */ id;
constructor(f, delayMS = 0, id = null) {
this.f = f;
this.time = performance.now() + delayMS;
this.id = id;
}
}
export class TickAction {
constructor(method) {/** @type {function()} */ this.method = method; }
/** Method is run on ticks */ active = false;
/** How many ticks occurred between start() and the current tick() */ index = -1;
start() { if (!this.active) { this.active = true; this._onStart(); } }
_onStart() { this.index = -1; this.onStart() }
onStart() { this.index = -1; }
stop() { this.active = false; }
restart() { this.active = false; this.start(); }
checkShouldRunTick() { return this.active }
tick(dt) { this.index++; if (this.checkShouldRunTick()) { return this.method(dt); } }
pause() { this.pausedIndex = this.index; this.active = false; this.onPause() } onPause() { }
resume() { this.index = this.pausedIndex; this.active = true; this.onResume() } onResume() { }
}
export class TickIntervalAction extends TickAction {
/** Tick interval to run method on @type {Number} */ interval
constructor(method, interval) { super(method); this.interval = interval; }
checkShouldRunTick() { return super.checkShouldRunTick() && ((this.index % this.interval) === 0) }
}
// export class TimedAction {
// start() {}
// }
export class ActionQueue {
/** @type {Action[]} */ q = [];
/** @param {Action} a */
/** @type {{string: Action}} */
ids = {}
add(a) {
this.q.push(a);
if (!a.id) { a.id = ID.generate(this.ids, 10) }
else if (this.ids[a.id]) {
console.warn(`ActionQueue: replacing existing action with id "${a.id}"`);
this.cancel(a.id)
}
this.ids[a.id] = a;
this.q.sort((a, b) => a.time - b.time);
}
runDue(now) {
while (this.q.length && this.q[0].time <= now) {
const action = this.q.shift()
delete this.ids[action.id]
action.f();
}
}
cancel(id) {
const a = this.ids[id];
delete this.ids[id]
this.q = this.q.filter(qa => qa !== a)
return a;
}
reschedule(id, fn, delayMs) {
this.cancel(id);
this.add(new Action(fn, delayMs, id))
}
}
export const RenderLoopAtts = Obj.join(StackedClassAtts, {
fps: 60,
})
/** @template {RenderLoopAtts} A @extends StackedClass<A> */
export class RenderLoop extends StackedClass {
static defaultAtts = RenderLoopAtts
/** @type {ActionQueue} */ queue
/** @param {A} atts */
constructor(atts = {}) {
super(atts)
this.fps = this.atts.fps
this.queue = new ActionQueue()
this.systems = []; // update systems run every logic step
this.renderers = []; // renderers run once per RAF after updates
this.inputBuffer = []; // queued input intents
this.running = false;
this.acc = 0; // accumulator for fixed step
/** Last timestamp */ this.last = 0;
this.raf = 0;
}
get fps() { return this._fps } set fps(v) { this._fps = v; this.setTargetFPS(v) }
setTargetFPS(fps) { this.stepMs = 1000 / fps }
/** @param {function(inputs,now,dt)} fn */
onUpdate(fn) { this.systems.push(fn); return () => this._off(this.systems, fn); }
onRender(fn) { this.renderers.push(fn); return () => this._off(this.renderers, fn); }
_off(arr, fn) { const i = arr.indexOf(fn); if (i >= 0) arr.splice(i, 1); }
schedule(fn, delayMs = 0, id = null) { this.queue.add(new Action(fn, delayMs, id)) }
pushInput(intent) { this.inputBuffer.push(intent); } // e.g., {type:'move', dx:1}
reschedule(id, delayMs) { const action = this.queue.ids[id]; if (action) { this.queue.reschedule(action.id, action.f, delayMs) } }
/** @param {Number} _now optional performance.now() for improved accuracy */
pause(_now) {
if (!this.running) { return }
this.running = false;
cancelAnimationFrame(this.raf);
this._pausedAt = _now ?? performance.now();
}
/** @param {Number} _now optional performance.now() for improved accuracy */
resume(_now) {
if (this.running) { return }
this.running = true;
const now = _now ?? performance.now();
const d = now - (this._pausedAt ?? now);
for (const a of this.queue.q) { a.time += d }
this.acc = 0;
this.last = now;
this.#reqFrame();
}
#reqFrame() { this.raf = requestAnimationFrame(this.#tick.bind(this)) }
tickIndex = 0;
start() {
if (this.running) { return }
this.running = true;
this.acc = 0;
this.last = performance.now();
this.#reqFrame()
}
#tick(now) {
if (!this.running) return;
// 1) run scheduled one-shots
this.queue.runDue(now);
// 2) fixed-step simulation
let dt = now - this.last;
this.last = now;
this.acc += dt;
while (this.acc >= this.stepMs) {
// drain input buffer deterministically at the start of a logic step
const inputs = this.inputBuffer; this.inputBuffer = [];
for (const sys of this.systems) { sys(inputs, now, dt); }
this.acc -= this.stepMs;
}
// 3) render once per RAF (optional: alpha for interpolation)
for (const r of this.renderers) { r(now, dt); }
this.tickIndex++;
this.raf = requestAnimationFrame(this.#tick.bind(this));
}
stop() { this.running = false; cancelAnimationFrame(this.raf) }
}
export const Sok = {
addStandardHandlers(s) {
s.on('set_cookie', d => Cookie.make(d.name, d.value, d.days))
s.on('redirect', l => window.location.href = l)
s.on('reload', () => location.reload())
s.on('alert', () => alert(...arguments))
},
}
export class WorkerPool {
workers = [];
queue = [];
activeWorkers = 0;
constructor(url, poolSize = 4, initCommand = false, options = {}) {
this.url = url;
this.poolSize = poolSize;
this.workers = Arr.make(poolSize, _ => {
const worker = new Worker(url, options)
if (initCommand) { worker.postMessage(initCommand) }
return worker;
})
}
// Return true with any intermittent messages, return false to resolve the task
_onmessage() { return false }
runTask(data) {
return new Promise((resolve, reject) => {
this.queue.push({ data, resolve, reject });
this.processQueue();
})
}
processQueue() {
if (!this.queue.length || !this.workers.length) return;
const worker = this.workers.pop();
const { data, resolve, reject } = this.queue.shift();
this.activeWorkers++;
worker.postMessage(data);
worker.onmessage = (event) => {
if (!this._onmessage(event)) {
this.workers.push(worker);
this.activeWorkers--;
resolve(event.data);
this.processQueue();
}
};
worker.onerror = (error) => {
this.workers.push(worker);
this.activeWorkers--;
reject(error);
this.processQueue();
};
}
terminateAll() {
this.workers.forEach(worker => worker.terminate());
this.workers = [];
}
}
Select All
import { El, Inf, Ninf, Num, Str, Unit, Color } from './tools.js'
//` CUSTOM ARRAYS
/**
* @template T
* @extends Array<T>
* */
export class List extends Array {
/** @type {number|null} */
static defaultSize = null;
/**
* @template O
* @template C
* @this C
*
* @overload
* @param {number} a
* @param {(n:number)=>O} func
* @returns {InstanceType<C>}
*
* @overload
* @param {number | ((n:number)=>O)} a
* @returns {InstanceType<C>}
*/
static make(a, func) {
const [n, f] = this.defaultSize ? [this.defaultSize, a] : [a, func];
const list = new this();
if (typeof f === 'function') {
for (let i = 0; i < n; i++) { list.push(f(i)) }
} else {
for (let i = 0; i < n; i++) { list.push(f) }
}
return list;
}
/** @template C @this C @returns {InstanceType<C>} */
static join(...arrays) {
const l = new this();
arrays.forEach(a => l.push(...a))
return l;
}
/** @template O @param {Object.<string,O>} obj @returns {List<O>} */
static ofObjectValues(obj) { return List.from(Object.values(obj)) }
/** LIST INDEXING / ORDERING */
get _index() { return this.__index ?? 0 } set _index(i) { this.__index = i }
/** @returns {T} */ get first() { return this[0] }
/** @returns {T} */ get last() { return this.at(-1) }
/** @returns {Number} */ static nextIndex(i, l, wrap) { return i === (l.length - 1) ? (wrap ? 0 : undefined) : i + 1 }
/** @returns {Number} */ static prevIndex(i, l, wrap) { return i === 0 ? (wrap ? l.length - 1 : undefined) : i - 1 }
/** @template V @param {Number} i @param {Array<V>} l @returns {V} */ static next(i, l, wrap = false) { return i === (l.length - 1) ? (wrap ? l[0] : undefined) : l[i + 1] }
/** @template V @param {Number} i @param {Array<V>} l @returns {V} */ static prev(i, l, wrap = false) { return i === 0 ? (wrap ? l[l.length - 1] : undefined) : l[i - 1] }
/** @returns {T} */ next(wrap = false) { return this._index === (this.length - 1) ? (wrap ? this[this._index = 0] : undefined) : this[this._index++] }
/** @returns {T} */ prev(wrap = false) { return this._index === 0 ? (wrap ? this[this._index = (this.length - 1)] : undefined) : this[this._index--] }
/** @returns {Number} */ nextIndex(wrap = false) { return (this._index === this.length) ? (wrap ? (this._index = 0) : null) : this._index++ }
/** @returns {Number} */ prevIndex(wrap = false) { return (this._index === 0) ? (wrap ? (this._index = (this.length - 1)) : null) : this._index++ }
/** @returns {this} */ cycleForward() { return this.reset(...this.slice(1), this[0]) }
/** @returns {this} */ cycleBackward() { return this.reset(this[this.length - 1], ...this.slice(0, -1)) }
/** @returns {this} */ shuffle() { let ci = this.length; while (ci > 0) { const ri = Math.floor(Math.random() * ci--);[this[ci], this[ri]] = [this[ri], this[ci]] } return this._resetCache() }
/** @template C @this C @returns {InstanceType<C>} */ static shuffle(a) { return this.from(a).shuffle() }
/** Just a reduce call @param {(item:T)=>Number} f @returns {Number} */
sum(f) { return this.reduce((sum, item) => sum + (f(item) ?? 0), 0) }
/** REMOVAL / REPLACEMENT */
/** @returns {T} With optional push val to back */ shift(val) { if (val) { this.push(val) } return super.shift() }
/** @returns {T} With optional push val to front */ pop(val) { if (val) { this.unshift(val) } return super.pop() }
/** @returns {T} */ popRandom(preserveOrder = true) { return this.removeIndex(this.randomIndex(), preserveOrder) }
/** @returns {T} */ removeIndex(i, preserveOrder = false) {
let removed;
removed = this[i];
if (preserveOrder) {
this.reset(...this.slice(0, i), ...this.slice(i + 1))
} else {
this[i] = this[this.length - 1];
this.pop();
}
return removed;
}
/** LIST COMPARISON */
/** @returns {Boolean} */ eachIsUnique() { const uniques = this.slice().uniques(); return uniques.length === this.length }
/** @returns {Boolean} */ isSame(v) { return !this.some((val, i) => v[i] !== val) }
/** INSTANCE & STATIC */
/** @template C @this C @returns {InstanceType<C>} all values, but without duplication */ static uniques(l) { return l.filter((v, i, a) => a.indexOf(v) === i) }
/** @template C @this C @returns {InstanceType<C>} only the values that have duplicates */ static duplicateVals(l) { return this.uniques(l.filter((v, i, a) => a.indexOf(v) !== i)) }
/** @template C @this C @returns {InstanceType<C>} values that only occur once */ static individuals(l) { return l.filter((v, i, a) => a.indexOf(v) === [...a].lastIndexOf(v)) }
/** @returns {this} all values, but without duplication */ get uniques() { return this.constructor.uniques(this) }
/** @returns {this} only the values that have duplicates */ get duplicateVals() { return this.constructor.duplicateVals(this) }
/** @returns {this} values that only occur once */ get individuals() { return this.constructor.individuals(this) }
/** @template V @param {Array<V>} l @returns {V} */ static randomItem(l) { return l[this.randomIndex(l)] }
/** @param {Array} l @returns {Number} */ static randomIndex(a) { return Num.randomInteger(0, a.length - 1) }
/** @returns {T} */ randomItem() { return this[this.randomIndex()] }
/** @returns {Number} */ randomIndex() { return Num.randomInteger(0, this.length - 1) }
/** @returns {this} */ sortByName() { return this.sort((a, b) => Str.alphabeticSort(a.name, b.name)) }
/** SETTING / COPYING VALUES */
_cached = {}
/** @returns {this} */ _resetCache() { this._cached = {}; return this; } // TODO THIS ONE
/** @returns {this} */ copyCache(l) { Object.entries(l._cached).forEach(([k, v]) => { this._cached[k] = v instanceof List ? v.copy : v }); return this; } // todo THIS ONE
/** @returns {this} */ toEachPart(f) { this.forEach((v, i) => this[i] = f(v, i)); return this._resetCache() }
/** @returns {this} */ toComp(i, f) { this[i] = f(this[i]); return this._resetCache() }
/** @type {this} Copy with all cached values */ get clone() { return this.copy.copyCache(this) }
// /** @returns {this} Copy with only base values **/ get copy() { return new this.constructor(...this) }
/** @returns {this} Copy with only base values **/ get copy() { return this.slice() }
/** @returns {this} Copy as reverse */ get reverseCopy() { return this.copy.reverse() }
/** @returns {this} Empty and refill */ reset(...vals) { this.length = 0; this.push(...vals); return this._resetCache() }
/** @returns {this} Commonly set all */ setU(u) { return this.set(this.map(_ => u)) }
/** @returns {this} Set one component */ setComp(i, s) { this[i] = s; return this._resetCache() }
/** @returns {this} Set vals from another */ set(v, includeCache) {
for (let i = 0; i < this.length; i++) { this[i] = v[i] };
return includeCache && v instanceof this.constructor ? this.copyCache(v) : this._resetCache()
}
get previous() { return this._previous }
/** @returns {this} */ setAndSave(v) { this._previous = this.copy; return this.set(v) }
/** @type {EventTarget} */ emitter;
setupUpdateEvents(fchange) {
if (!this.emitter) { this.emitter = new EventTarget() }
this._resetCache = this.constructor._resetCacheWithEvents.bind(this)
this.copyCache = this.constructor._copyCacheWithEvents.bind(this)
if (fchange) { El.ev(this.emitter, 'change', fchange) }
return this.emitter;
}
static _resetCacheWithEvents() {
this.constructor.prototype._resetCache.call(this);
/** @type {EventTarget} */ this.emitter;
this.emitter.dispatchEvent(new Event('change'))
return this;
}
static _copyCacheWithEvents(l) {
this.constructor.prototype.copyCache.call(this, l);
/** @type {EventTarget} */ this.emitter;
this.emitter.dispatchEvent(new Event('change'))
return this;
}
static benchTest() {
console.log('\n\n\nBench Test: Nums')
console.log('uniform', Nums.uniform(1, 10))
console.log('zero2', V2.zero())
console.log('areFiniteF', Nums.areFinite([1, 1, 1, Infinity]))
console.log('areFiniteT', Nums.areFinite([1, 1, 1]))
console.log('areNumericF', Nums.areNumeric([1, 1, 1, {}]))
console.log('areNumericT', Nums.areNumeric([1, 1, 1]))
console.log('areIntegersF', Nums.areIntegers([1, 1, 1, 1.1]))
console.log('areIntegersT', Nums.areIntegers([1, 1, 1]))
console.log('min=1', Nums.min([1, 2, 3]))
console.log('max=3', Nums.max([1, 2, 3]))
console.log('range=2', Nums.range([1, 2, 3]))
console.log('mean=2', Nums.mean([1, 2, 3]))
console.log('maxdex=2', Nums.maxdex([1, 2, 3]))
console.log('mindex=0', Nums.mindex([1, 2, 3]))
console.log('uniques=[1,2,4,3]', Nums.uniques([1, 2, 4, 3, 4, 4]))
console.log('duplicateVals=[4]', Nums.duplicateVals([1, 2, 4, 3, 4, 4]))
console.log('individuals=[1,2,3]', Nums.individuals([1, 2, 4, 3, 4, 4]))
console.log('inBoundsT', Nums.inBounds([1, 1], [0, 0], [2, 2]))
console.log('inBoundsF', Nums.inBounds([2, 3], [0, 0], [2, 2]))
console.log('inBoxBoundsT', Nums.inBoxBounds([1, 1], [0, 2], [2, 0]))
console.log('inBoxBoundsF', Nums.inBoxBounds([2, 3], [0, 2], [2, 0]))
console.log('plus=[3,3]', Nums.plus([1, 1], [2, 2]))
console.log('minus=[1,1]', Nums.minus([3, 3], [2, 2]))
console.log('multiply=[4,6]', Nums.multiply([2, 3], [2, 2]))
console.log('divide=[2,3]', Nums.divide([4, 6], [2, 2]))
console.log('scale=2', Nums.scale(2, [1, 1]))
console.log('to=[1,2]', Nums.to([1, 2], [2, 4]))
console.log('power=[1,4]', Nums.power([1, 2], [2, 2]))
console.log('and=[T,T]', Nums.and([1, 1], [1, 1]))
console.log('and=[F,T]', Nums.and([0, 1], [1, 1]))
console.log('or=[T,T]', Nums.or([1, 1], [0, 1]))
console.log('or=[F,T]', Nums.or([0, 0], [0, 1]))
console.log('nullish=[5,0]', Nums.nullish([null, 0], [5, 4]))
console.log('ternary=[5,2]', Nums.ternary([1, 0], [5, 1], [2, 2]))
const ones = new Nums(1, 1, 1)
const twos = ones.copy.scale(2)
console.log('isSame=T', ones.isSame([1, 1, 1]))
console.log('isSame=F', ones.isSame([2, 1, 1]))
console.log('isSimilar=T', ones.isSimilar(0.3, [1, 1, 1.2]))
console.log('isSimilar=F', ones.isSimilar(0.1, [1, 1, 1.2]))
console.log('set=1,1,1', new Nums(1, 2, 3).set(ones))
console.log('plus=3,3,3', Nums.from(ones).plus(twos))
console.log('minus=1,1,1', Nums.from(twos).minus(ones))
console.log('multiply=1,2,3', Nums.from(ones).multiply([1, 2, 3]))
console.log('divide=1/3,1/2,1', Nums.from(ones).divide([3, 2, 1]))
console.log('scale=2,2,2', Nums.from(ones).scale(2))
console.log('negate', ones.copy.negate())
console.log('to=2,3,4', ones.copy.to([3, 4, 5]))
console.log('lerp=1.5,1.5,1.5', ones.copy.lerp(0.5, [2, 2, 2]))
console.log('midpoint=1.5,1.5,1.5', ones.copy.midPoint([2, 2, 2]))
console.log('randomize5,10,0', ones.copy.multiply([5, 10, 0]).randomize(true))
// console.log('staticRand,len3', Nums.random(3))
console.log('clamp=[0,1,0.5]', ones.copy.multiply([-5, 5, 0.5]).clamp([0, 0, 0], [1, 1, 1000], false))
console.log('prop_sum=6', twos.sum)
console.log('prop_avg=2', twos.avg)
console.log('prop_product=8', twos.product)
console.log('prop_sumSquares=12', twos.sumSquares)
console.log('prop_squares=4,4,4', twos.squares)
const v1 = new V2(1, 1)
const v2 = new V2(2, 2)
console.log('orderOfMagnitude = 1.414', v1.mag)
console.log('dist 1 2 = 1.414', v1.dist(v2))
console.log('dot 1 2 = 4', v1.dot(v2))
console.log('unitMove1=5,3', v1.clone.moveDistAlongUnit(4, [1, 0.5]))
console.log('fromradAng0=[1,0]', V2.radAng(1, 0))
console.log('fromradAng45=[.707,.707]', V2.radAng(1, Unit.rad(45)))
console.log('angle=45', Unit.deg(new V2(1, 1).angle))
console.log('normal=0,-1', V2.normal([1, 0]))
console.log('cross=-13', new V2(2, 5).cross([3, 1]))
console.log('angleTo=45', Unit.deg(new V2(1, 0).angleTo([1, 1])))
console.log('offset=[1,4]', new V2(1, 1).offset(3, Unit.rad(90)))
console.log('rotaround=[-3,6]', new V2(3, 0).rotateAround(Unit.rad(90), [-3, 0]))
}
}
/** @extends {List<Number>} */
export class Nums extends List {
static valueNames = [['value']]
static defaultValues = []
static defaultSize = 0;
/** @template C @this C @returns {InstanceType<C>} */
static parse(obj) {
const def = this.defaultValues
if (Array.isArray(obj)) {
const nums = new this()
for (let i = 0; i < Math.max(obj.length, def.length); i++) { nums.push(obj[i] ?? def[i]) }
return nums;
} else {
return this.fromObject(obj)
}
}
/** @template C @this C @returns {InstanceType<C>} */
static forceToNums(nums) {
return this.from(nums.map((n, i) => Num.forceNumeric(n, this.defaultValues[i] ?? 0)))
}
/** @template V @param {Array<V>} l @returns {V[]} */
static setFromObj(o, l = []) {
if (l.length) { l.length = 0 }
this.valueNames.forEach((names, i) => {
let v = this.defaultValues[i];
if (o) { for (let n of names) { if (n in o) { v = o[n]; break } } }
l.push(v);
})
return l;
}
/** @template C @this C @returns {InstanceType<C>} */ static u(v, size) { return this.uniform(v, size) }
/** @template C @this C @returns {InstanceType<C>} */ static uniform(v, size) { return this.from({ length: size ?? this.defaultSize }, _ => v) }
/** @template C @this C @returns {InstanceType<C>} */ static zero() { this._setZero(); return this._0.copy }
/** @template C @this C @returns {InstanceType<C>} */ static get _zero() { this._setZero(); return this._0 }
/** @template C @this C @returns {InstanceType<C>} */ static _setZero() { if (!this._0) { this._0 = this.from(this.defaultValues) } }
/** @template C @this C @returns {InstanceType<C>} */ static fromObject(o) { return this.setFromObj(o, new this()) }
/** @returns {this} */ setFromObj(o) { return this.set(this.constructor.setFromObj(o), false) }
// Item number type checking
/** @returns {Boolean} */ static areFinite(a) { for (let v of a) { if (!Number.isFinite(v)) { return false } } return true }
/** @returns {Boolean} */ static areNumeric(a) { for (let v of a) { if (typeof v !== 'number') { return false } } return true }
/** @returns {Boolean} */ static areIntegers(a) { for (let v of a) { if (!Number.isInteger(v)) { return false } } return true }
/** @type {Boolean} */ get isFinite() { return Nums.areFinite(this) }
/** @type {Boolean} */ get areNumeric() { return Nums.areNumeric(this) }
/** @type {Boolean} */ get areIntegers() { return Nums.areIntegers(this) }
// Numeric list properties
/** @returns {Number} @param {number[]} l */ static min(l) { return Math.min(...l) }
/** @returns {Number} @param {number[]} l */ static max(l) { return Math.max(...l) }
/** @returns {Number} @param {number[]} l */ static range(l) { return Math.max(...l) - Math.min(...l) }
/** @returns {Number} @param {number[]} l */ static mean(l) { return l.length ? this.sum(l) / l.length : null }
/** @returns {Number} @param {number[]} l */ static median(l) {
const ar = l.toSorted((a, b) => a - b);
return Num.odd(ar.length) ? ar[(ar.length - 1) / 2] : this.mean([ar[ar.length / 2 - 1], ar[ar.length / 2]])
}
/** @returns {Number} @param {number[]} */ static modeDistribution(l) {
const frq = {};
l.forEach(v => frq[v] = (frq[v] ?? 0) + 1)
return Object.entries(frq).map(([value, frequency]) => { return { value, frequency } }).sort((b, a) => a.frequency - b.frequency)
}
/** @returns {Number[]} @param {number[]} l */ static mode(l) {
const dist = this.modeDistribution(l)
if (!dist.length) { return null }
return dist.filter(d => d.frequency === dist[0].frequency).map(d => d.value)
}
/** @returns {Number} @param {number[]} l */ static variance(l) {const avg = this.mean(l); return Nums.mean(l.map(v => (v - avg) ** 2))}
/** @returns {Number} @param {number[]} l */ static stDev(l) { return Math.sqrt(this.variance(l)) }
/** @returns {Number} */ static maxdex(l) { const max = Math.max(...l); return l.findIndex(v => v === max) }
/** @returns {Number} */ static mindex(l) { const min = Math.min(...l); return l.findIndex(v => v === min) }
/** @returns {Nums} */ static mins(p, q) { return p.map((c, i) => Math.min(c, q[i])) }
/** @returns {Nums} */ static maxs(p, q) { return p.map((c, i) => Math.max(c, q[i])) }
/** @type {Number} */ get min() { return Nums.min(this) }
/** @type {Number} */ get max() { return Nums.max(this) }
/** @type {Number} */ get range() { return Nums.range(this) }
/** @type {Number} */ get mean() { return Nums.mean(this) }
/** @type {Number} */ get median() { return Nums.median(this) }
/** @type {Number[]} */ get mode() { return Nums.mode(this) }
/** @type {Number} */ get variance() { return Nums.variance(this) }
/** @type {Number} */ get stDev() { return Nums.stDev(this) }
/** @type {Number} */ get maxdex() { return Nums.maxdex(this) }
/** @type {Number} */ get mindex() { return Nums.mindex(this) }
/** @returns {this} */ mins(p) { return this.toEachPart((c, i) => Math.min(c, p[i])) }
/** @returns {this} */ maxs(p) { return this.toEachPart((c, i) => Math.max(c, p[i])) }
// Numeric list bounds operations
/** @returns {Boolean} */ static inBounds(l, lower, upper) { return this.from(l).inBounds(lower, upper) }
/** @returns {Boolean} */ static inBoxBounds(l, q, p) { return this.from(l).inBounds(...this.formBounds(q, p)) }
/** @returns {List<this>} */ static formBounds(q, p) { return new List(this.lowerBound(q, p), this.upperBound(q, p)) }
/** @template C @this C @returns {InstanceType<C>} */ static lowerBound(q, p) { return this.from(q.map((v, i) => Math.min(v, p[i]))) }
/** @template C @this C @returns {InstanceType<C>} */ static upperBound(q, p) { return this.from(q.map((v, i) => Math.max(v, p[i]))) }
/** @returns {Boolean} */ static above(l, lower) { return this.from(l).above(lower) }
/** @returns {Boolean} */ static below(l, lower) { return this.from(l).below(lower) }
/** @returns {Boolean} */ inBounds(lower, upper) { return this.above(lower) && this.below(upper) }
/** @returns {Boolean} */ inBoxBounds(q, p) { return this.inBounds(...this.constructor.formBounds(q, p)) }
/** @returns {List<this>} */ formBounds(p) { return this.constructor.formBounds(this, p) }
/** @returns {this} */ lowerOfTwo(p) { return this.reset(...this.constructor.lowerOfTwo(this, p)) }
/** @returns {this} */ upperOfTwo(p) { return this.reset(...this.constructor.upperOfTwo(this, p)) }
/** @returns {Boolean} */ above(lower) {
if (isFinite(lower)) {
// Lower is single numeric value
for (let val of this) { if (val < lower) { return false } }
} else if (Array.isArray(lower)) {
// Lower is list of same size
for (let i in this) { if (this[i] < lower[i]) { return false } }
} else if (typeof lower === 'function') {
// Lower is function
for (let i in this) { if (this[i] < lower(i)) { return false } }
}
return true;
}
/** @returns {Boolean} */ below(upper) {
let v;
if (isFinite(upper)) {
// Upper is single numeric value
for (v of this) { if (v > upper) { return false } }
} else if (Array.isArray(upper)) {
// Upper is list of same size
for (let i in this) { if (this[i] > upper[i]) { return false } }
} else if (typeof upper === 'function') {
// Upper is function
for (let i in this) { if ((v = this[i]) > upper(v, i)) { return false } }
}
return true;
}
// STATIC TRANSFORM / BOOLEAN OPERATIONS
/** a+b @template C @this C @returns {InstanceType<C>} */ static plus(a, b) { return this.from(a.map((v, i) => v + b[i])) }
/** a-b @template C @this C @returns {InstanceType<C>} */ static minus(a, b) { return this.from(a.map((v, i) => v - b[i])) }
/** a*b @template C @this C @returns {InstanceType<C>} */ static multiply(a, b) { return this.from(a.map((v, i) => v * b[i])) }
/** a/b @template C @this C @returns {InstanceType<C>} */ static divide(a, b) { return this.from(a.map((v, i) => v / b[i])) }
/** a.*s @template C @this C @returns {InstanceType<C>} */ static scale(s, a) { return this.from(a.map(c => c * s)) }
/** b-a @template C @this C @returns {InstanceType<C>} */ static to(a, b) { return this.from(a.map((v, i) => b[i] - v)) }
/** a^b @template C @this C @returns {InstanceType<C>} */ static power(a, b) { return this.from(a.map((v, i) => v ** b[i])) }
/** a&&b @template C @this C @returns {InstanceType<C>} */ static and(a, b) { return this.from(a.map((v, i) => v && b[i])) }
/** a||b @template C @this C @returns {InstanceType<C>} */ static or(a, b) { return this.from(a.map((v, i) => v || b[i])) }
/** a??b @template C @this C @returns {InstanceType<C>} */ static nullish(a, b) { return this.from(List.make(Math.max(a.length, b.length), i => a[i] ?? b[i])) }
/** ?a:b @template C @this C @returns {InstanceType<C>} */ static ternary(cond, a, b) { return this.from(a.map((v, i) => cond[i] ? v : b[i])) }
/** a??d @template C @this C @returns {InstanceType<C>} */ static uniformDefault(d, a) { return this.from(a.map((v, i) => v ?? d)) }
/** a??d @template C @this C @returns {InstanceType<C>} */ static default(a, d) { return this.nullish(a, d) }
// VALUE DISPLAY
/** @returns {this} */ dispRounded(m = 0) { return this.map(v => v.toFixed(m)).join(',') }
// COMPARISON
/** @returns {Boolean} */ isSimilar(t, v) { return !this.some((val, i) => Math.abs(v[i] - val) > t) }
/** @returns {Boolean} */ isAt(p) { return this.isSame(p) }
/** @returns {Boolean} */ isNear(dist, p) { return this.isSimilar(dist, p) }
// SETTERS
/** @returns {this} Set vals from another, using defaults if available */ setSafe(v, includeCache) {
const vs = this.constructor.parse(v)
for (let i = 0; i < this.length; i++) { this[i] = vs[i] ?? this.constructor.defaultValues[i] ?? 0 };
return includeCache && vs instanceof this.constructor ? this.copyCache(vs) : this._resetCache()
}
/** @returns {this} */ plus(v) { return this.toEachPart((c, i) => c + v[i]) }
/** @returns {this} */ plusU(u) { return this.plus(this.map(_ => u)) }
/** @returns {this} */ plusComp(i, s) { return this.toComp(i, c => c + s) }
/** @returns {this} */ minus(v) { return this.toEachPart((c, i) => c - v[i]) }
/** @returns {this} */ minusU(u) { return this.minus(this.map(_ => u)) }
/** @returns {this} */ minusComp(i, s) { return this.toComp(i, c => c - s) }
/** @returns {this} */ multiply(v) { return this.toEachPart((c, i) => c * v[i]) }
/** @returns {this} */ multiplyComp(i, s) { return this.toComp(i, c => c * s) }
/** @returns {this} */ divide(v) { return this.toEachPart((c, i) => c / v[i]) }
/** @returns {this} */ divideU(u) { return this.divide(this.map(_ => u)) }
/** @returns {this} */ divideComp(i, s) { return this.toComp(i, c => c / s) }
/** @returns {this} */ negate() { return this.scale(-1) }
/** @returns {this} */ unscale(s) { return this.toEachPart(c => c / s) }
/** @returns {this} */ scale(s) { return this.toEachPart(c => c * s) }
/** @returns {this} */ scaleComp(i, s) { return this.toComp(i, c => c * s) }
/** @returns {this} */ to(v) { return this.toEachPart((c, i) => v[i] - c) }
/** @returns {this} */ lerp(t, v) { return this.plus(Nums.to(this, v).scale(t)) }
/** @returns {this} */ midPoint(v) { return this.toEachPart((c, i) => (c + v[i]) / 2) }
/** Make random list based on the input list as a maximum (or min/max where min is the negation of the max)
* @template C @this C @returns {InstanceType<C>} */
static randomize(l, oneSided) {
return this.from(l.map(v => Num.randomDecimal(oneSided ? 0 : -v, v)))
}
/** @template C @this C @returns {InstanceType<C>} */ static random(...maxs) {
let vals = [];
for (let i = 0; i < this.defaultSize ?? maxs.length; i++) {
vals.push(Num.randomDecimal(0, maxs[i] ?? this.defaultValues[i] ?? 1))
}
return this.from(vals)
// const f = _ => Num.randomDecimal();
// return this.make(...(this.defaultSize ? [f] : [size, f]))
// if (!!this.defaultSize) {return this.make(_=>Num.randomDecimal)} else {return this.make(size,_=>Num.randomDecimal)}
}
/** Assuming mins and maxs are arrays of the same dimension, returns
* a list of random numbers between each respective min and max value
* @param {Number[]} mins
* @param {Number[]} maxs
* @template C @this C @returns {InstanceType<C>} */
static randomInRange(mins, maxs) {
let vals = [];
const len = this.defaultSize ?? Math.max(mins.length, maxs.length)
for (let i = 0; i < len; i++) { vals.push(Num.randomDecimal(mins[i] ?? 0, maxs[i] ?? 1)) }
return this.from(vals)
}
// static randomInRange(mins, maxs) { return this.plus(mins, Nums.from(mins).to(maxs).randomize(true)) }
/** Set the values of this list to random values less than or equal to their current values.
* @param {Boolean} oneSided If true, only values of the same sign as each number will be generated
* @returns {this} */ randomize(oneSided) { return this.toEachPart((_, i) => Num.randomDecimal(oneSided ? 0 : -this[i], this[i])) }
/** @returns {this} */ clamp(min, max, wrap) {
const nV = Array.isArray(min);
const xV = Array.isArray(max);
const f = wrap ? Num.clampAround : Num.clamp;
return this.toEachPart((c, i) => f(c, nV ? min[i] : min, xV ? max[i] : max))
}
/** @returns {this} */ clampComp(i, min, max, wrap) { return this.toComp(i, c => (wrap ? Num.clampAround : Num.clamp)(c, min, max, wrap)) }
/** @type {Number} */ get sum() { return this._cached.sum ?? (this._cached.sum = Nums.sum(this)) }
/** @type {Number} */ get avg() { return this._cached.avg ?? (this._cached.avg = Nums.avg(this)) }
/** @type {Number} */ get product() { return this._cached.product ?? (this._cached.product = Nums.product(this)) }
/** @type {Number} */ get sumSquares() { return this._cached.sumSquares ?? (this._cached.sumSquares = Nums.sumSquares(this)) }
/** @type {this} */ get squares() { return this._cached.squares ?? (this._cached.squares = Nums.squares(this)) }
/** @returns {Number} */ static sum(l) { return l.reduce((s, v) => s + v, 0) }
/** @returns {Number} */ static avg(l) { return Nums.sum(l) / l.length; }
/** @returns {Number} */ static product(l) { return l.reduce((s, v) => s * v, 1) }
/** @returns {Number} */ static sumSquares(l) { return l.reduce((s, v) => s + v * v, 0) }
/** @template C @this C @returns {InstanceType<C>} */ static squares(l) { return l.map(v => v * v) }
get dependents() { return this._dependents }
/**
* @param {function():Number[]} t Transform Function. This should return an array of numbers, as its return will be immediately used to set the dependent's value whenever standard Nums methods are used on this [parent] list.
* TODO: Possibly just modify prototype functions to include an update flag, and then store dependents IN the cache, treating them similar to something like a unit vector.
* @returns {this}
* */
createDependent(t) {
if (!this.dependents) {
this._dependents = []
this._resetCache = this.constructor.__parentTransform_resetCache.bind(this)
this.copyCache = this.constructor.__parentTransform_copyCache.bind(this)
}
const dep = this.copy.set(t.call(this))
function dependentUpdate() { return this.set(this.transformMethod()) }
dep.transformMethod = t.bind(this)
dep.dependentUpdate = dependentUpdate.bind(dep)
this.dependents.push(dep)
return dep;
}
/** @type {function():this} @returns {this} */ dependentUpdate
/** @template C @this C @returns {InstanceType<C>} */
static __parentTransform_resetCache() {
this.constructor.prototype._resetCache.call(this);
this._dependents.forEach(dep => dep.dependentUpdate());
return this
}
/** @template C @this C @returns {InstanceType<C>} */
static __parentTransform_copyCache(l) {
this.constructor.prototype.copyCache.call(this, l);
this._dependents.forEach(dep => dep.dependentUpdate());
return this
}
}
//* VECTORS
export class Vect extends Nums {
/** @returns {Number} */ dist(v) { return this.copy.minus(v).mag }
/** @returns {Number} */ dot(v) { return this.map((val, i) => val * v[i]).reduce((s, val) => s + val, 0) }
/** @returns {this} */ moveDistAlongUnit(distance, unitVector) { return this.plus(this.constructor.from(unitVector).scale(distance)) }
/** Update scalar properties */
/** @returns {Number} */ _dist(...v) { return this.dist(this.constructor.from(v)) }
/** @returns {Number} */ _dot(...v) { return this.dot(this.constructor.from(v)) }
toUnit() { const u = this.unit; u.forEach((v, i) => { this[i] = v }); return this._resetCache() }
/** Common scalar properties */
/** @type {this} */ get unit() { return this._cached.unit ?? (this._cached.unit = this.constructor.unit(this)) }
/** @type {Number} */ get mag() { return this._cached.mag ?? (this._cached.mag = Math.hypot(...this)) }
/** @template C @this C @returns {InstanceType<C>} */ static unit(v) { return this.scale(1 / Vect.mag(v), v) }
/** @type {Number} */ static mag(v) { return Math.hypot(...v) }
}
export class Vxy extends Vect {
static defaultValues = [0, 0]
static defaultSize = 2;
static valueNames = [['x', '_x'], ['y', '_y']]
/** @returns {Number} */ get x() { return this[0] } set x(v) { this[0] = v }
/** @returns {Number} */ get y() { return this[1] } set y(v) { this[1] = v }
/** @returns {{x:Number,y:Number}} */ get xy() { return { x: this[0], y: this[1] } }
/** @returns {this} */ plusX(n) { return this.plusComp(0, n) }
/** @returns {this} */ plusY(n) { return this.plusComp(1, n) }
/** @returns {this} */ minusX(n) { return this.minusComp(0, n) }
/** @returns {this} */ minusY(n) { return this.minusComp(1, n) }
/** @returns {this} */ setX(n) { return this.setComp(0, n) }
/** @returns {this} */ setY(n) { return this.setComp(1, n) }
/** @returns {this} */ restrictX(min, max, wrap) { return this.clampComp(0, min, max, wrap) }
/** @returns {this} */ restrictY(min, max, wrap) { return this.clampComp(1, min, max, wrap) }
}
export class Vxyz extends Vxy {
static defaultValues = [...Vxy.defaultValues, 0]
static defaultSize = 3;
static valueNames = [...Vxy.valueNames, ['z', '_z']]
/** @returns {Number} */ get z() { return this[2] } set z(v) { this[2] = v }
/** @returns {{x:Number,y:Number,z:Number}} */ get xyz() { return { x: this[0], y: this[1], z: this[2] } }
/** @returns {this} */ plusZ(n) { return this.plusComp(2, n) }
/** @returns {this} */ minusZ(n) { return this.minusComp(2, n) }
/** @returns {this} */ setZ(n) { return this.setComp(2, n) }
/** @returns {this} */ restrictZ(min, max, wrap) { return this.clampComp(2, min, max, wrap) }
}
export class Vxyzt extends Vxyz {
static defaultValues = [...Vxyz.defaultValues, 0]
static defaultSize = 4;
static valueNames = [...Vxyz.valueNames, ['t', '_t']]
/** @returns {Number} */ get t() { return this[3] } set t(v) { this[3] = v }
/** @returns {{x:Number,y:Number,z:Number,t:Number}} */ get xyzt() { return { x: this[0], y: this[1], z: this[2], t: this[3] } }
/** @returns {this} */ plusT(n) { return this.plusComp(3, n) }
/** @returns {this} */ minusT(n) { return this.minusComp(3, n) }
/** @returns {this} */ restrictT(min, max, wrap) { return this.clampComp(3, min, max, wrap) }
}
export class Vxyzw extends Vxyz {
static defaultValues = [...Vxyz.defaultValues, 1]
static defaultSize = 4;
static valueNames = [...Vxyz.valueNames, ['w', '_w']]
/** @returns {Number} */ get w() { return this[3] } set w(v) { this[3] = v }
/** @returns {{x:Number,y:Number,z:Number,t:Number}} */ get xyzw() { return { x: this[0], y: this[1], z: this[2], w: this[3] } }
/** @returns {this} */ plusW(n) { return this.plusComp(3, n) }
/** @returns {this} */ minusW(n) { return this.minusComp(3, n) }
/** @returns {this} */ restrictW(min, max, wrap) { return this.clampComp(3, min, max, wrap) }
}
export class V2 extends Vxy {
/** @template C @this C @returns {InstanceType<C>[]} */
static get udlr() { return [new this(0, -1), new this(0, 1), new this(-1, 0), new this(1, 0)] }
/** @returns {Number} */ get angle() { return this._cached.angle ?? (this._cached.angle = V2.angle(this)) }
/** @returns {V2} */ get normal() { return this._cached.normal ?? (this._cached.normal = V2.normal(this)) }
/** @returns {Number} */ static angle(v) { return Math.atan2(v[1], v[0]) }
/** @template C @this C @returns {InstanceType<C>} */ static normal(v) { return new this(-v[1], v[0]) }
/** @returns {Number} */ cross(v) { return this[0] * v[1] - this[1] * v[0] }
/** @returns {Number} */ angleTo(p) { return Math.acos(this.dot(p) / (this.mag * (p instanceof V2 ? p.mag : Vect.mag(p)))) }
/** @returns {V2} */ offset(dist, angle) { return this.plus([dist * Math.cos(angle), dist * Math.sin(angle)]) }
/** @returns {V2} */ rotate(angle) { const sa = Math.sin(angle); const ca = Math.cos(angle); return this.set([this[0] * ca - this[1] * sa, this[0] * sa + this[1] * ca]) }
/** @returns {V2} */ rotranslate(angle, t) { const sa = Math.sin(angle); const ca = Math.cos(angle); return this.set([this[0] * ca - this[1] * sa + t[0], this[0] * sa + this[1] * ca + t[1]]) }
/** @returns {V2} */ rotateAround(angle, p) { return this.minus(p).rotate(angle).plus(p) }
/** @returns {V2} */ toNormal() { return this.set(this.normal) }
/** @template C @this C @returns {InstanceType<C>} */
static radAng(r, a) { return new this(r * Math.cos(a), r * Math.sin(a)) }
/** @template C @this C @returns {InstanceType<C>} */
static rotate(p, angle) { const sa = Math.sin(angle); const ca = Math.cos(angle); return new this(p[0] * ca - p[1] * sa, p[0] * sa + p[1] * ca) }
/**
* Create multiple points that are rotated out from this point efficiently
* @param {[x:number,y:number]} p
* @param {Number[]} angles
* @returns {List<V2>}
*/
rotateAroundAtAngles(angles, p) {
const [cx, cy] = p ?? [0, 0]
const dx = this[0] - cx;
const dy = this[1] - cy;
return List.make(angles.length, i => {
const ca = Math.cos(angles[i]);
const sa = Math.sin(angles[i]);
return new V2(
cx + dx * ca - dy * sa,
cy + dx * sa + dy * ca
)
})
}
/** @returns {Number} */ _angleTo(...p) { return this.angleTo(this.constructor.from(p)) }
/** @returns {Number} */ _cross(...v) { return this.cross(this.constructor.from(v)) }
/** @returns {V2} */ _rotateAround(angle, ...p) { return this.rotateAround(angle, this.constructor.from(p)) }
/** @typedef {-1} CW */
/** @typedef {1} CCW */
/** @typedef {0} Collinear */
/** @typedef {CW|CCW|Collinear} TurnDirection */
/**
* Coerces arguments to V2 and returns the orientation that turns towards c after moving from point a to b.
* @param {*} a @param {*} b @param {*} c
* @returns {TurnDirection} 1:CCW, -1: CW, 0: Collinear
*/
static _turnDirection(a, b, c) { return V2.turnDirection(V2.parse(a), V2.parse(b), V2.parse(c)) }
/**
* Returns the orientation that turns towards C after moving from point A to B.
* @param {V2 | Number[]} A @param {V2 | Number[]} B @param {V2 | Number[]} C
* @returns {TurnDirection} Values Returned: 1:CCW, -1: CW, 0: Collinear
*/
static turnDirection(A, B, C) { return Math.sign((B[0] - A[0]) * (C[1] - B[1]) - (B[1] - A[1]) * (C[0] - B[0])) }
static TurnDirections = { CCW: 1, CW: -1, COLLINEAR: 0 }
/**
* Coerces arguments to V2 and returns whether p lies within the minmax bounds of bounds1 and bounds2
* @param {*} p @param {*} bounds1 @param {*} bounds2 @returns {Boolean}
*/
static _inBounds(p, bounds1, bounds2) { return V2.inBounds(V2.parse(p), V2.parse(bounds1), V2.parse(bounds2)) }
/**
* Returns whether p lies within the minmax bounds of bounds1 and bounds2
* @param {V2|Number[]} p @param {V2|Number[]} BOUNDS1 @param {V2|Number[]} BOUNDS2 @returns {Boolean}
*/
static inBounds(p, bounds1, bounds2) {
return p[0] <= Math.max(bounds1[0], bounds2[0]) && p[0] >= Math.min(bounds1[0], bounds2[0])
&& p[1] <= Math.max(bounds1[1], bounds2[1]) && p[1] >= Math.min(bounds1[1], bounds2[1])
}
}
export class V3 extends Vxyz {
/** @returns {Quat} */ quatTo(v) { return Quat.betweenVectors(this, v) }
/** @returns {this} */
cross(v) {
const [v0, v1, v2] = this
return this.set([v1 * v[2] - v2 * v[1], v2 * v[0] - v0 * v[2], v0 * v[1] - v1 * v[0]])
}
/** @returns {this} */
applyQuat(q) {
const [tx, ty, tz] = this;
const [qx, qy, qz, qw] = q;
const ix = qw * tx + qy * tz - qz * ty;
const iy = qw * ty + qz * tx - qx * tz;
const iz = qw * tz + qx * ty - qy * tx;
const iw = -qx * tx - qy * ty - qz * tz;
return this.set([
ix * qw + iw * -qx + iy * -qz - iz * -qy,
iy * qw + iw * -qy + iz * -qx - ix * -qz,
iz * qw + iw * -qz + ix * -qy - iy * -qx
])
}
/** @returns {this} */ _cross(...v) { return this.cross(V3.from(v)) }
/** @returns {this} */ _applyQuat(...q) { return this.applyQuat(Quat.from(q)) }
// CONVERSIONS
/** @returns {Eul} */ toEuler(order) { return Eul.from(this).setOrder(order) }
}
//* 3D SPACE MISC
export class Eul extends V3 {
static orderIndices = { x: 0, y: 1, z: 2, X: 0, Y: 1, Z: 2 }
/** @returns {this} */
setFromObj(o) {
super.setFromObj(o);
this.setOrder(o.order)
return this;
}
/** @returns {this} */
setOrder(order) { this.order = (order ?? 'xyz').toLowerCase().slice(0, 3); return this }
/** @returns {Quat} */
toQuaternion(ord) {
const cx = Math.cos(this[0] / 2);
const cy = Math.cos(this[1] / 2);
const cz = Math.cos(this[2] / 2);
const sx = Math.sin(this[0] / 2);
const sy = Math.sin(this[1] / 2);
const sz = Math.sin(this[2] / 2);
const sxcy = sx * cy;
const cxsy = cx * sy;
const cxcy = cx * cy;
const sxsy = sx * sy;
switch ((ord ?? this.order).toLowerCase() ?? 'xyz') {
case 'xyz': return new Quat(sxcy * cz + cxsy * sz, cxsy * cz - sxcy * sz, cxcy * sz + sxsy * cz, cxcy * cz - sxsy * sz)
case 'yxz': return new Quat(sxcy * cz + cxsy * sz, cxsy * cz - sxcy * sz, cxcy * sz - sxsy * cz, cxcy * cz + sxsy * sz)
case 'zxy': return new Quat(sxcy * cz - cxsy * sz, cxsy * cz + sxcy * sz, cxcy * sz + sxsy * cz, cxcy * cz - sxsy * sz)
case 'zyx': return new Quat(sxcy * cz - cxsy * sz, cxsy * cz + sxcy * sz, cxcy * sz - sxsy * cz, cxcy * cz + sxsy * sz)
case 'yzx': return new Quat(sxcy * cz + cxsy * sz, cxsy * cz + sxcy * sz, cxcy * sz - sxsy * cz, cxcy * cz - sxsy * sz)
case 'xzy': return new Quat(sxcy * cz - cxsy * sz, cxsy * cz - sxcy * sz, cxcy * sz + sxsy * cz, cxcy * cz + sxsy * sz)
}
}
/** @template C @this C @returns {InstanceType<C>} */
static fromQuat(order, ...q) { return Quat.from(q).toEuler(order) }
/**
* @returns {Number}
* Given the nth component, return the index related to the nth component in this rotation order */
parseIndex(n) { return Eul.orderIndices[this.order[n]] }
/** @template C @this C @returns {InstanceType<C>} */
static make(order, ...args) { return Eul.from(args).setOrder(order) }
/** @returns {Number[]} */
get orderIndices() { return this.order.split('').map(l => Eul.orderIndices[l]) }
}
export class Mtx { // todo extend this from array
#construct(f, dims, dim, ...indices) {
return dim < dims.length ? List.make(dims[dim], i => this.#construct(f, dims, dim + 1, ...indices, i)) : f(...indices)
}
/** @template C @this C @returns {InstanceType<C>} */
static _identity(n) { return Array.from({ length: n }, x => Array.from({ length: n }, y => Number(y === x))) }
/** @template C @this C @returns {InstanceType<C>} */
static identity(n) { const m = new this(); m.values = Mtx._identity(n); m._cachedDims = [n, n]; return m; }
constructor(...args) {
let d;
switch (args.length) {
case 0:
this.values = []; break;
case 1:
let a = args[0];
if (a !== null) {
if (a instanceof Mtx) {
this.values = a.values.slice();
}
else if (Array.isArray(a)) {
this.values = a.slice();
}
else if (Number.isFinite(a)) {
this.values = Mtx._identity(a);
this._cachedDims = [a, a]
}
}
default:
let f, d;
const isGen1 = typeof (f = args[0]) === 'function' && (d = args.slice(1))
const isGen2 = !isGen1 && typeof (f = args.at(-1)) === 'function' && (d = args.slice(0, -1))
if (isGen1 || isGen2) {
if (Array.isArray(d[0])) { d = d[0] }
this.values = List.make(d[0], i => this.#construct(f, d, 1, i))
this._cachedDims = d;
}
break;
}
}
_cachedDims;
get dimensions() { if (!this._cachedDims) { this._update() } return this._cachedDims }
set dimensions(dims) { this.values = List.make(dims[0], i => this.#construct(() => 0, dims, 1, i)) }
_update() {
let dim, row = this.values;
const dims = []
while (Array.isArray(row) && Number.isFinite(dim = row.length)) { dims.push(dim); row = row[0] }
this._cachedDims = dims;
}
*[Symbol.iterator]() { for (const item of this.values) { yield item } }
}
export class TMat extends Mtx {
static _scale0 = new V3(1, 1, 1)
static _pos0 = new V3(0, 0, 0)
static _identity = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
/** @template C @this C @returns {InstanceType<C>} */
static identity() { const t = new TMat(); t.values = TMat._identity; t._cachedDims = [4, 4]; return t; }
constructor(...args) { super(...args) }
get '0'() { return this.values[0] } set '0'(a) { this.values[0] = a }
get '1'() { return this.values[1] } set '1'(a) { this.values[1] = a }
get '2'() { return this.values[2] } set '2'(a) { this.values[2] = a }
get '3'() { return this.values[3] } set '3'(a) { this.values[3] = a }
/** @template C @this C @returns {InstanceType<C>} */
static _fromPQS(pos, quat, scale) { return TMat.fromPQS(V3.parse(pos), Quat.parse(quat), V3.parse(scale)) }
/** @template C @this C @returns {InstanceType<C>} */
static fromPQS(pos, quat, scale) {
//! VERIFIED WORKS AS EXPECTED
const [x, y, z, w] = quat
const x2 = x + x, y2 = y + y, z2 = z + z;
const xx = x * x2, xy = x * y2, xz = x * z2;
const yy = y * y2, yz = y * z2, zz = z * z2;
const wx = w * x2, wy = w * y2, wz = w * z2;
const [sx, sy, sz] = scale;
const m = new TMat();
m.values = [
[(1 - (yy + zz)) * sx, (xy + wz) * sx, (xz - wy) * sx, 0],
[(xy - wz) * sy, (1 - (xx + zz)) * sy, (yz + wx) * sy, 0],
[(xz + wy) * sz, (yz - wx) * sz, (1 - (xx + yy)) * sz, 0],
[...pos, 1]
]
return m;
}
/** @returns {Eul} */
#toEuler(order, dir, det, ay1, ax1, ay3, ax3, by1, bx1) {
//! VERIFIED WORKS AS EXPECTED
const p = new Eul(0, 0, 0).setOrder(order)
const [k1, k2, k3] = p.order.split('').map(l => Eul.orderIndices[l])
p[k2] = Math.asin(dir * Num.clamp(det, -1, 1));
if (Math.abs(det) < 0.9999999) {
p[k1] = Math.atan2(ay1, ax1);
p[k3] = Math.atan2(ay3, ax3);
} else {
p[k1] = Math.atan2(by1, bx1);
}
return p;
}
/** @returns {Eul} */
toEuler(order = 'xyz') {
//! VERIFIED WORKS AS EXPECTED
const [t0, t1, t2] = this.values;
const _order = order.toLowerCase()
switch (_order) {
case 'yxz': return this.#toEuler(_order, -1, t2[1], t2[0], t2[2], t0[1], t1[1], -t0[2], t0[0]);
case 'xyz': return this.#toEuler(_order, 1, t2[0], -t2[1], t2[2], -t1[0], t0[0], t1[2], t1[1]);
case 'xzy': return this.#toEuler(_order, -1, t1[0], t1[2], t1[1], t2[0], t0[0], -t2[1], t2[2]);
case 'yzx': return this.#toEuler(_order, 1, t0[1], -t0[2], t0[0], -t2[1], t1[1], t2[0], t2[2]);
case 'zxy': return this.#toEuler(_order, 1, t1[2], -t1[0], t1[1], -t0[2], t2[2], t0[1], t0[0]);
case 'zyx': return this.#toEuler(_order, -1, t0[2], t0[1], t0[0], t1[2], t2[2], -t1[0], t1[1]);
}
}
}
export class Quat extends Vxyzw {
static defaultValues = [0, 0, 0, 1]
static defaultSize = 4;
static valueNames = [...Vxyz.valueNames, ['w', '_w']]
static identity() { return new Quat(0, 0, 0, 1) }
static _identity = Quat.identity()
static _fromAxisAngle(angle, ...axis) { return Quat.fromAxisAngle(angle, V3.from(axis)) }
static fromAxisAngle(angle, axis) {
const ha = angle / 2;
const s = Math.sin(ha)
const ax = axis.map(v => v * s)
return new Quat(...ax, Math.cos(ha))
}
/** @returns {Quat} */
static fromEuler(e, ...angles) { return ((e instanceof Eul) ? e : Eul.from(angles).setOrder(e)).toQuaternion(); }
/** @param {V3Like} a @param {V3Like} b */
static betweenVectors(a, b) {
const v1 = (a instanceof V3 ? a.copy : V3.parse(a)).unit;
const v2 = (b instanceof V3 ? b.copy : V3.parse(b)).unit;
const dot = v1.dot(v2);
// If vectors are the same, return identity quaternion
if (dot > 1 - Num.PRECISION) { return Quat.zero() }
if (dot < -1 + Num.PRECISION) {
const aa = new V3(1, 0, 0);
const axis = aa.copy.cross(v1);
if (axis.length < Num.PRECISION) { axis._set([0, 1, 0]).cross(v1) }
const q = Quat.fromAxisAngle(Math.PI, axis.copy.unit);
return q
}
// Else, create quaternion using axis-angle method from cross product and dot
const s = Math.sqrt((1 + dot) * 2);
return new this(...v1.cross(v2).scale(1 / s), s * 0.5)
}
_applyToV3(v) { return this.applyToV3(V3.parse(v)) }
_quaternionTo(q) { return this.quaternionTo(Quat.parse(q), true) }
_multiply(q) { return this.multiply(this.constructor.parse(q)) }
_slerp(t, q) { return this.slerp(t, this.constructor.parse(q)) }
applyToV3(v) { return v._applyQuat(this) }
quaternionTo(q) { return this.multiply(q.copy.conjugate()).toUnit() }
multiply(q) {
const [ax, ay, az, aw] = this;
const [bx, by, bz, bw] = q;
this[0] = ax * bw + aw * bx + ay * bz - az * by;
this[1] = ay * bw + aw * by + az * bx - ax * bz;
this[2] = az * bw + aw * bz + ax * by - ay * bx;
this[3] = aw * bw - ax * bx - ay * by - az * bz;
return this
}
slerp(t, q) {
//! VALID AND WORKS!
let [qx, qy, qz, qw] = q
const [tx, ty, tz, tw] = this;
let cosAng = tx * qx + ty * qy + tz * qz + tw * qw;
if (cosAng < 0) { qx *= -1; qy *= -1; qz *= -1; qw *= -1; cosAng *= -1; }
if (cosAng >= 1.0) { return this.copy }
const sinAng = Math.sqrt(1.0 - cosAng * cosAng);
// if (sinAng < (1*Num.PRECISION)) {return this.plus(q).scale(0.5)} //! REMOVED DUE TO ISSUES
const ang = Math.acos(cosAng);
const ratioA = Math.sin((1 - t) * ang) / sinAng;
const ratioB = Math.sin(t * ang) / sinAng;
return new this.constructor(
tx * ratioA + qx * ratioB,
ty * ratioA + qy * ratioB,
tz * ratioA + qz * ratioB,
tw * ratioA + qw * ratioB
);
}
conjugate() { for (let i = 0; i < 3; i++) { this[i] *= -1 } return this.toUnit(); }
invert() { return this.conjugate() }
toEuler(order = 'xyz') { return this.toTransformMatrix().toEuler(order) }
toTransformMatrix() { return TMat._fromPQS(TMat._pos0, this, TMat._scale0) }
}
//* SEGMENTS
const COLLINEAR_INTERSECTION = -1
const NONCOLLINEAR_INTERSECTION = 1
const NO_INTERSECTION = 0
/** @template {Vect} T @extends {Array<T>} */
export class Seg extends Array {
midPoint() { return this[0].constructor.midPoint(this[1]) }
}
/** @template {V2} T @extends {Seg<T>} */
export class Seg2 extends Seg {
parse(a, b) { const seg = new Seg2(V2.parse(a), V2.parse(b)) }
_hasIntersectionWith(...s) { return this.hasIntersectionWith(this.constructor.from(s)) }
_lineIntersection(...s) { return this.lineIntersection(this.constructor.from(s)) }
_differentSlopes(...s) { return this._differentSlopes(this.constructor.from(s)) }
_checkIsLineCollinear(...s) { return this.checkIsLineCollinear(this.constructor.from(s)) }
_getIntersections(...s) { return this.getIntersections(this.constructor.from(s), s.at(-1)) }
/** Determine if this segment has an intersection with another segment
* @param {Seg2} s @returns {=NONCOLLINEAR_INTERSECTION, COLLINEAR_INTERSECTION, NO_INTERSECTION} */
hasIntersectionWith(seg) {
const [[p, q], [r, s]] = [this, seg]
let o1 = V2.turnDirection(p, q, r);
let o2 = V2.turnDirection(p, q, s);
let o3 = V2.turnDirection(r, s, p);
let o4 = V2.turnDirection(r, s, q);
if (o1 !== o2 && o3 !== o4) { return NONCOLLINEAR_INTERSECTION }
// Check if the segments are collinear and intersecting by checking if any third turn direction points were collinear with their first
// two direction points and if so, checking if that third lies within the bounds defined by the minmax values of the first two points.
if (!o1 && V2.inBounds(r, p, q)
|| !o2 && V2.inBounds(s, p, q)
|| !o3 && V2.inBounds(p, r, s)
|| !o4 && V2.inBounds(q, r, s)) { return COLLINEAR_INTERSECTION }
return 0;
}
/** Finds the intersection point between two lines, input as segments. Only returns undefined if the lines
* are parallel or collinear. If the intersection of two SEGMENTS is desired, this should not be used.
* @param {Seg2} seg @returns {V2} Intersection point of the two lines */
lineIntersection(seg) {
const [det, abx, aby, cdx, cdy] = this.differentSlopes(seg) ?? [false]
if (det) {
const e = aby * this[0][0] - abx * this[0][1];
const f = cdy * seg[0][0] - cdx * seg[0][1];
return new V2((abx * f - cdx * e) / det, (aby * f - cdy * e) / det)
}
}
/** Checks if this slope matches segment slope. If it does, return null. If not, return the xy components
* of each slope as a length 5 list, starting with the calculated determinate [det,ax,ay,bx,by]
* @param {Seg2} seg * @returns {[[det,dx1,dy1,dx2,dy2] | false]} */
differentSlopes(seg) {
const [[a, b], [c, d]] = [this, seg]
const aby = b[1] - a[1];
const abx = b[0] - a[0];
const cdy = d[1] - c[1];
const cdx = d[0] - c[0];
const det = cdy * abx - aby * cdx;
return !det ? null : [det, abx, aby, cdx, cdy]
}
/** Check if this lies collinear with another segment without determining if
* an intersection point / range exists. IE as if checking against a line.
* @param {Seg2} seg @returns {Boolean} */
checkIsLineCollinear(seg) {
return !V2.turnDirection(...this, seg[0]) && !V2.turnDirection(...this, seg[1])
}
get maxY() { return Math.max(this[0][1], this[1][1]) }
get minY() { return Math.min(this[0][1], this[1][1]) }
get maxX() { return Math.max(this[0][0], this[1][0]) }
get minX() { return Math.min(this[0][0], this[1][0]) }
/** Get the intersection point (if noncollinear, or collinear and intersects at endpoint)
* or the intersection range (if collinear) if either exists between two segments
* @param {Seg2} seg @returns {V2[]} */
getIntersections(seg,) {
const intersectionType = this.hasIntersectionWith(seg);
if (intersectionType === NO_INTERSECTION) { return [] }
if (intersectionType === NONCOLLINEAR_INTERSECTION) {
return [this.lineIntersection(seg)];
} else {
const [[a1, a2], [b1, b2]] = [this, seg]
const [a1x, a1y] = a1;
const [a2x, a2y] = a2;
const [b1x, b1y] = b1;
const [b2x, b2y] = b2;
let points;
const [aMin, aMax] = (a1x === a2x ? a1y < a2y : a1x < a2x) ? [a1, a2] : [a2, a1];
const [bMin, bMax] = (a1x === a2x ? b1y < b2y : b1x < b2x) ? [b1, b2] : [b2, b1];
if (bMax[0] > aMax[0]) {
points = (bMin[0] === aMax[0]) ? [bMin] : (bMin[0] > aMin[0] ? [bMin, aMax] : [aMin, aMax])
// if (bMin[0] === aMax[0]) { // ONLY 1 SET OF ENDPOINTS TOUCH: xMinB == xMaxA
// points = [bMin]
// } else if (bMin[0] > aMin[0]) { // xMinB to xMaxA
// points = [bMin, aMax]
// } else { // Full range of A
// points = [aMin, aMax]
// }
} else if (bMax[0] === aMax[0]) {
points = bMin[0] > aMin[0] ? [bMin, bMax] : [aMin, aMax]
// if (bMin[0] > aMin[0]) { // Full range of B
// points = [bMin, bMax]
// } else { // Full range of A
// points = [aMin, aMax]
// }
} else {
points = bMax[0] === aMin[0] ? [aMin] : (bMin[0] > aMin[0] ? [bMin, bMax] : [aMin, bMax])
// if (bMax[0] === aMin[0]) { // ONLY 1 SET OF ENDPOINTS TOUCH: xMaxB == xMinA
// points = [aMin]
// } else if (bMin[0] > aMin[0]) { // Full range of B
// points = [bMin, bMax]
// } else { // Range from xMinA to xMaxB
// points = [aMin, bMax]
// }
}
return points.map(p => V2.from(p))
}
}
}
/** @template {V3} T @extends {Seg<T>} */
export class Seg3 extends Seg { }
//* PATHS
/** @template {Vect} T @extends {List<T>} */
export class Path extends List {
midPoint() {
const lengths = []
const tot = 0;
const sums = []
this.forEach((v, i) => {
if (i) {
const l = v._dist(this[i - 1]);
tot += l;
sums += tot;
lengths.push(l)
}
})
let half = tot / 2;
for (let i = 0; i < sums.length; i++) {
if (sums[i] > half) {
return this[i].copy._lerp((sums[i] - half) / lengths[i], this[i + 1])
}
}
}
}
/** @template {V2} T @extends {Path<T>} */
export class Path2 extends Path {
/** @type {Path2<T>} */ static get LRUD() { if (!this._LRUD) { this._LRUD = new this(new V2(-1, 0), new V2(1, 0), new V2(0, 1), new V2(0, -1)) } return this._LRUD }
/** @type {Path2<T>} */ static get UD() { if (!this._UD) { this._UD = new this(new V2(0, 1), new V2(0, -1)) } return this._UD }
}
/** @template {V3} T @extends {Path<T>} */
export class Path3 extends Path { }
//* N-D SHAPES
/** @template {Vect} T @extends {List<T>} */
export class VectGroup extends List {
get centroid() {
if (!this.length) return;
const sum = this[0].copy;
for (let i = 1; i < this.length; i++) { sum.plus(this[i]) }
return sum.scale(1 / this.length);
}
}
//* 2D SHAPES
/** @template {V2} T @extends {VectGroup<T>} */
export class Polygon extends VectGroup {
static numSides = null;
makeSides() {
this.sides = [];
for (let i = 0; i < this.length; i++) { this.sides.push(new Seg2(this[i], List.next(i, this, true))) }
return this;
}
rotate(angle) {
this.forEach(v => v.rotateAround(angle, this.centroid))
}
rotateAround(angle, pt) {
this.forEach(v => v.rotateAround(angle, pt))
}
translate(t) { this.forEach(v => v.plus(t)) }
}
/** @template {V2} T @extends {Polygon<T>} */
export class Rectangle extends Polygon {
static numSides = 4;
/** @template C @this C @returns {InstanceType<C>} */
static byWidthHeight(w, h) {
const r = new this();
const dw = w / 2;
const dh = h / 2;
;[[dw, dh], [-dw, dh], [-dw, -dh], [dw, -dh]].forEach(p => r.push(V2.from(p)))
return r.makeSides()
}
flipVertically() { this.reverse(); return this.makeSides(); }
flipHorizontally() {
this.reset(this[1], this[0], this[3], this[2]);
return this.makeSides();
}
get t() { return this.sides[0] }
get b() { return this.sides[2] }
get l() { return this.sides[1] }
get r() { return this.sides[3] }
get u() { return this.t }
get d() { return this.b }
get TR() { return this[0] }
get TL() { return this[1] }
get BL() { return this[2] }
get BR() { return this[3] }
}
/** @template {V2} T @extends {Rectangle<T>} */
export class Square extends Rectangle {
/** @param {Number} l @template C @this C @returns {InstanceType<C>} */
static bySideLength(l) { return this.byWidthHeight(l, l) }
}
//* 3D SHAPES
/** @template {V3} T @extends {VectGroup<T>} */
export class Polyhedron extends VectGroup { }
//* GRID-BASED ARRAY CLASSES
export class Vyx extends Vect {
static defaultValues = [0, 0]
static defaultSize = 2;
static valueNames = [['y', '_y'], ['x', '_x']]
/** @returns {Number} */ get y() { return this[0] } set y(v) { this[0] = v }
/** @returns {Number} */ get x() { return this[1] } set x(v) { this[1] = v }
/** @returns {{x:Number,y:Number}} */ get xy() { return { x: this[0], y: this[1] } }
/** @returns {this} */ plusY(n) { return this.plusComp(0, n) }
/** @returns {this} */ plusX(n) { return this.plusComp(1, n) }
/** @returns {this} */ minusY(n) { return this.minusComp(0, n) }
/** @returns {this} */ minusX(n) { return this.minusComp(1, n) }
/** @returns {this} */ setY(n) { return this.setComp(0, n) }
/** @returns {this} */ setX(n) { return this.setComp(1, n) }
/** @returns {this} */ restrictY(min, max, wrap) { return this.clampComp(0, min, max, wrap) }
/** @returns {this} */ restrictX(min, max, wrap) { return this.clampComp(1, min, max, wrap) }
}
export class RC extends Vyx {
static valueNames = [
['c', 'col', ...Vyx.valueNames[0]],
['r', 'row', ...Vyx.valueNames[1]]
]
get r() { return this[0] } set r(v) { this[0] = v }
get c() { return this[1] } set c(v) { this[1] = v }
isAt(rc) { return !this.some((val, i) => parseInt(rc[i]) !== parseInt(val)) }
}
export class GridPath extends Path { }
/** @template {any} T @extends List<List[T]> */
export class Grid extends List {
/** @param {Number} rows @param {Number} cols @param {(row:Number, col:Number) => T} f @template C @this C @returns {InstanceType<C>} */
static make(rows, cols, f) { return new this().reset(rows, cols, f); }
/** @param {Number} rows @param {Number} cols @param {(row:Number, col:Number) => T} f @returns {this} */
reset(rows, cols, f) {
this.length = 0;
if (rows ?? 0) { if (f) { this.push(...List.make(rows, r => List.make((cols ?? 0), c => f(r, c)))) } }
return this;
}
/** @returns {T} */ at(r, c) { const row = super.at(r); return Number.isFinite(c) ? row?.at?.(c) : row; }
/** @returns {List<T>} */ row(r) { return super.at(r) }
/** @returns {List<T>} */ col(c) { return List.from(this.map(row => row.at?.(c))) }
/** @param {Number} c @param {Number} r @returns {T} */ cr(c, r) { return this[r]?.[c] }
/** @param {Number} r @param {Number} c @returns {T} */ rc(r, c) { return this[r]?.[c] }
get r() { return this.length }
get c() { return this[0]?.length ?? 0 }
get w() { return this.c }
get h() { return this.r }
get size() { return this.c * this.r }
get dimension() { return new V2(this.c, this.r) }
/** @returns {this} */ get copy() { return this.map(r => r.copy) }
/** @template R @param {(item: T, row:Number, col:Number) => R} f @returns {Grid<R>} */
gridmap(f) { return Grid.make(this.r, this.c, (r, c) => f(this[r][c], r, c)) }
/** @template R @param {(item: T, row:Number, col:Number) => R} f @returns {List<R>} */
toList(f) { return List.from(this.gridmap(f ?? ((v) => v)).flat()) }
/** @param {(value:T, row:Number, col:Number) => T} f */
forEachItem(f) {
this.forEach((row, r) => row.forEach((v, c) => f(v, r, c)));
return this;
}
transpose() {
const transposed = this[0].map((v, i) => List.from(this, r => r[i]))
return this.reset(this.c, this.r, (r, c) => transposed[r][c])
}
flipHorizontally() {
this.forEach(row => row.reverse())
return this;
}
#prepRotation() {
const cnew = this.r;
const rnew = this.c;
const rotated = List.make(rnew, _ => List.make(cnew, _ => null))
return { cnew, rnew, rotated }
}
/** @returns {this} */ rotateCCW() {
const { rnew, rotated } = this.#prepRotation()
this.forEach((row, r) => row.forEach((v, c) => rotated[rnew - 1 - c][r] = v))
return this.reset(this.c, this.r, (r, c) => rotated[r][c])
}
/** @returns {this} */ rotateCW() {
const { cnew, rotated } = this.#prepRotation()
this.forEach((row, r) => row.forEach((v, c) => rotated[c][cnew - 1 - r] = v))
return this.reset(this.c, this.r, (r, c) => rotated[r][c])
}
/** @param {function(v:T,r,c)} f */
string(f = v => String(v), join = '\t') { return this.map((row, r) => (row.map((v, c) => f(v, r, c)).join(join) + join)).join('\n'); }
}
export class GridXY extends List {
/** @param {Number} w @param {Number} h @param {(x:Number, y:Number) => T} f @template C @this C @returns {InstanceType<C>} */
static make(w = 0, h = 0, f = () => { }) { return new this().reset(w, h, f); }
/** @param {Number} w @param {Number} h @param {(x:Number, y:Number) => T} f @returns {this} */
reset(w = 0, h = 0, f = () => { }) {
this.length = 0;
if (h) { if (f) { this.push(...List.make(h, r => List.make(w, c => f(r, c)))) } }
return this;
}
/** @returns {T} */ at(x, y) { const row = super.at(y); return Number.isFinite(c) ? row?.at?.(x) : row; }
/** @returns {List<T>} */ row(y) { return super.at(y) }
/** @returns {List<T>} */ col(x) { return List.from(this.map(row => row.at?.(x))) }
/** @param {Number} x @param {Number} y @returns {T} */ xy(x, y) { return this[y]?.[x] }
get h() { return this.length }
get w() { return this[0]?.length ?? 0 }
get size() { return this.w * this.h }
get dimension() { return new V2(this.w, this.h) }
/** @returns {this} */ get copy() { return this.map(row => row.copy) }
}
export function gridBenchtest() {
console.log('\n\n\n\nGrid Bench Test')
const g = new Grid().reset(5, 5, (r, c) => new RC(r, c))
console.log(g)
}
//* NUMERIC RANGES
export class Domain extends Nums {
static defaultValues = [0, 1]
static defaultSize = 2;
static valueNames = [['min', '_min'], ['max', '_max']]
/** @returns {Number} */ get min() { return this[0] } set min(v) { this[0] = v }
/** @returns {Number} */ get max() { return this[1] } set max(v) { this[1] = v }
/** @returns {Number} */ get span() { return this.max - this.min }
/** @returns {Number} */ get mid() { return (this.min + this.max) / 2 }
/** @returns {Boolean} */ contains(v) { return Domain.contains(v, this) }
/** @returns {Number} */ normalize(v) { return Domain.normalize(v, this) }
/** @returns {Number} */ denormalize(v) { return Domain.denormalize(v, this) }
/** @returns {Number} */ clamp(v) { return Num.clamp(v, this.min, this.max) }
/** @returns {Number} */ padClamp(v, pad = 0) { return Num.padClamp(v, pad, this.min, this.max) }
/** @returns {Boolean} */ overlaps(d) { return Domain.overlapExists(d, this) }
/** @returns {Boolean} */ encompassedBy(d) { return d[0] <= this.min && d[1] >= this.max }
/** @returns {Boolean} */ encompasses(d) { return d[0] >= this.min && d[1] <= this.max }
static contains(v, d) { return v >= d[0] && v <= d[1] }
static normalize(v, d) { return (v - d[0]) / (d[1] - d[0]) }
static denormalize(v, d) { return v * (d[1] - d[0]) + d[0] }
static clamp(v, d) { return Num.clamp(v, d[0], d[1]) }
static padClamp(v, d, pad) { return Num.padClamp(v, pad, d[0], d[1]) }
static overlapExists(d1, d2) { return d1[0] <= d2[0] ? d1[1] >= d2[0] : d1[0] <= d2[1] }
/** Returns list of numbers that are equivalently spaced in the domain [min,max]. The total number of */
stepList(n) {
return List.make()
}
}
/** @extends Array<Domain> */
export class BBox extends Array {
static fillDims(dims) {
const { l, r, t, b, w, h } = dims;
if (l != null) { if (r != null) { dims.w = r - l } else { dims.r = l + w } } else { dims.l = r - w }
if (b != null) { if (t != null) { dims.h = b - t } else { dims.t = b - h } } else { dims.b = t + h }
return dims;
}
/** @template C @this C @returns {InstanceType<C>} */
static XYWH(x, y, w, h) { return new BBox([x, x + w], [y, y + h]) }
/**
* @overload
* @param {number} minX Min X numerical value
* @param {number} minY Min Y numerical value
* @param {number} maxX Max X numerical value
* @param {number} maxY Max Y numerical value
* @template C @this C @returns {InstanceType<C>}
*
* @overload
* @param {number} min Min XY Pair
* @param {number} maxX Max X numerical value
* @param {number} maxY Max Y numerical value
* @template C @this C @returns {InstanceType<C>}
*
* @overload
* @param {number} minX Max X numerical value
* @param {number} minY Max Y numerical value
* @param {number} max Max XY Pair
* @template C @this C @returns {InstanceType<C>}
*
* @overload
* @param {V2|number[]} min Min XY Pair
* @param {V2|number[]} max Max XY Pair
* @template C @this C @returns {InstanceType<C>}
*/
static MinMax(a, b, c, d) {
let min, max;
if (Array.isArray(a)) {
min = V2.from(a);
if (Array.isArray(b)) {
max = V2.from(b);
} else {
max = V2.forceToNums([b, c])
}
} else if (Nums.areNumeric([a, b])) {
min = new V2(a, b)
if (Array.isArray(c)) {
max = V2.from(c)
} else {
max = V2.forceToNums([c, d])
}
}
return new this([min[0], max[0]], [min[1], max[1]])
}
/** @template C @this C @returns {InstanceType<C>} */
static Inf() { return new this([Ninf, Inf], [Ninf, Inf]) }
constructor(x, y) { super(Domain.from(x), Domain.from(y)) }
/** @type {Domain} */get x() { return this[0] } set x(v) { this[0] = v }
/** @type {Domain} */get y() { return this[1] } set y(v) { this[1] = v }
/** @type {V2} */ get min() { return new V2(this.x.min, this.y.min) }
/** @type {V2} */ get max() { return new V2(this.x.max, this.y.max) }
/** @type {V2} */ get span() { return new V2(this.x.span, this.y.span) }
/** @type {V2} */ get mid() { return new V2(this.x.mid, this.y.mid) }
contains(p) { return this.x.contains(p[0]) && this.y.contains(p[1]) }
normalize(p) { return [this.x.normalize(p[0]), this.y.normalize(p[1])] }
denormalize(p) { return [this.x.denormalize(p[0]), this.y.denormalize(p[1])] }
clamp(p) { return [this.x.clamp(p[0]), this.y.clamp(p[1])] }
padClamp(p, vPad) { const pad = Array.isArray(vPad) ? vPad : [vPad, vPad]; return [this.x.padClamp(p[0], vPad[0]), this.y.padClamp(p[1], vPad[1])] }
overlapExists(d) { return this[0].overlapExists(d[0]) && this[1].overlapExists(d[1]) }
encompassedBy(d) { return this[0].encompassedBy(d[0]) && this[1].encompassedBy(d[1]) }
encompasses(d) { return this[0].encompasses(d[0]) && this[1].encompasses(d[1]) }
/** @returns {this} */ scale(s) { this.forEach(d => d.scale(s)); return this; }
/** @returns {this} */ multiply(v) { this.forEach((d, i) => d.scale(v[i])); return this; }
/** @returns {this} */ divide(v) { this.forEach((d, i) => d.scale(1 / v[i])); return this; }
/** @returns {this} */ unscale(s) { this.forEach(d => d.unscale(s)); return this; }
/** @returns {this} */ get copy() { return new this.constructor(this[0].copy, this[1].copy) }
}
//* VECT-BASED COLOR CLASSES
export class RGB extends V3 {
/** @template C @this C @returns {InstanceType<C>} */
static parse(obj) {
const def = this.defaultValues
if (Array.isArray(obj)) {
const nums = new this()
for (let i = 0; i < Math.max(obj.length, def.length); i++) { nums.push(obj[i] ?? def[i]) }
return nums;
} else if (typeof obj === 'string') {
return this.fromString(obj)
} else {
return this.fromObject(obj)
}
}
static defaultValues = [255, 255, 255]
static valueNames = [['r', '_r'], ['g', '_g'], ['b', '_b']]
get r() { return this[0] } set r(v) { this[0] = v }
get g() { return this[1] } set g(v) { this[1] = v }
get b() { return this[2] } set b(v) { this[2] = v }
get rgb() { return Color.rgbCSS(...this) }
/** @returns {this} */ plusR(n) { return this.plusComp(0, n) }
/** @returns {this} */ plusG(n) { return this.plusComp(1, n) }
/** @returns {this} */ plusB(n) { return this.plusComp(2, n) }
/** @returns {this} */ minusR(n) { return this.minusComp(0, n) }
/** @returns {this} */ minusG(n) { return this.minusComp(1, n) }
/** @returns {this} */ minusB(n) { return this.minusComp(2, n) }
/** @returns {this} */ setR(n) { return this.setComp(0, n) }
/** @returns {this} */ setG(n) { return this.setComp(1, n) }
/** @returns {this} */ setB(n) { return this.setComp(2, n) }
/** @returns {this} */ restrictR(min, max, wrap) { return this.clampComp(0, min, max, wrap) }
/** @returns {this} */ restrictG(min, max, wrap) { return this.clampComp(1, min, max, wrap) }
/** @returns {this} */ restrictB(min, max, wrap) { return this.clampComp(2, min, max, wrap) }
/** @returns {RGBA} */ toRGBA(a) { return new RGBA(...this, a) }
/** @returns {string} rounded css string */ rgbDisplay(decRGB = 0) { return Color.rgbCSS(...[...this].map(v => v.toFixed(decRGB))) }
/** @returns {string} */ get hexstring() { return Color.rgb2hexString(...this) }
/** @returns {Number} */ get hexnumber() { return Color.rgb2hexNumber(...this) }
/** @template C @this C @returns {InstanceType<C>} */ static fromHexNumber(h) { return new this(...Color.hexNumber2rgba(h)) }
/** @template C @this C @returns {InstanceType<C>} */ static fromHexString(hex) { return new this(...Color.hex2rgb(hex)); }
/** @template C @this C @returns {InstanceType<C>} */ static fromHSL(hsl) { return new this(...Color.hsl2rgb(...hsl)) }
/** @template C @this C @returns {InstanceType<C>} */
static fromString(s) {
const possHex = s.startsWith('#') || ((s.length > 5 && s.length < 8) && Str.containsNumbers(s))
if (possHex) {
return this.fromHexString(s)
} else {
return this.fromName(s)
}
}
/** @template C @this C @returns {InstanceType<C>} */ static fromName(str) { return new this(...Color.name2rgb(str)) }
get rgbBasedContrastingColor() { return new this.constructor(...Color.achromaticRGBContrastOfRGB(...this)) }
get hslBasedContrastingColor() { return new this.constructor(...Color.achromaticRGBContrastOfHSL(...Color.rgb2hsl(...this))) }
/** @template C @this C @returns {List<InstanceType<C>>} */
static gradient(steps, c1, c2) {
/** @type {RGB} */ const color1 = this.parse(c1)
/** @type {RGB} */ const color2 = this.parse(c2)
return List.make(steps, i => color1.copy.lerp(i / (steps - 1), color2))
}
}
RGB.list = {
/** rgb( 0, 0, 0) */ black: new RGB(0, 0, 0),
/** rgb(255,255,255) */ white: new RGB(255, 255, 255),
/** rgb(211,211,211) */ lightgray: new RGB(211, 211, 211),
/** rgb(211,211,211) */ lightgrey: new RGB(211, 211, 211),
/** rgb(192,192,192) */ silver: new RGB(192, 192, 192),
/** rgb(169,169,169) */ darkgray: new RGB(169, 169, 169),
/** rgb(169,169,169) */ darkgrey: new RGB(169, 169, 169),
/** rgb(128,128,128) */ gray: new RGB(128, 128, 128),
/** rgb(128,128,128) */ grey: new RGB(128, 128, 128),
/** rgb(105,105,105) */ dimgray: new RGB(105, 105, 105),
/** rgb(105,105,105) */ dimgrey: new RGB(105, 105, 105),
/** rgb(255, 0, 0) */ red: new RGB(255, 0, 0),
/** rgb(255,165, 0) */ orange: new RGB(255, 165, 0),
/** rgb(255,255, 0) */ yellow: new RGB(255, 255, 0),
/** rgb( 0,255, 0) */ green: new RGB(0, 255, 0),
/** rgb( 0, 0,255) */ blue: new RGB(0, 0, 255),
/** rgb( 75, 0,130) */ indigo: new RGB(75, 0, 130),
/** rgb(238,130,238) */ violet: new RGB(238, 130, 238),
/** rgb(128, 0,128) */ purple: new RGB(128, 0, 128),
/** rgb(165, 42, 42) */ brown: new RGB(165, 42, 42),
/** rgb( 0,255,255) */ cyan: new RGB(0, 255, 255),
/** rgb(255, 0,255) */ magenta: new RGB(255, 0, 255),
/** rgb(255,192,203) */ pink: new RGB(255, 192, 203),
/** rgb( 0,128,128) */ teal: new RGB(0, 128, 128),
/** rgb(128, 0, 0) */ maroon: new RGB(128, 0, 0),
/** rgb(255,215, 0) */ gold: new RGB(255, 215, 0),
/** rgb( 0, 0,128) */ navy: new RGB(0, 0, 128),
/** rgb(255, 0,255) */ fuchsia: new RGB(255, 0, 255),
/** rgb(240,230,140) */ khaki: new RGB(240, 230, 140),
/** rgb( 0,255, 0) */ lime: new RGB(0, 255, 0),
/** rgb(220, 20, 60) */ crimson: new RGB(220, 20, 60),
/** rgba( 0,100, 0) */ darkgreen: new RGB(0, 100, 0),
/** rgba( 0, 0,100) */ darkblue: new RGB(0, 0, 100),
/** rgba(100, 0, 0) */ darkred: new RGB(100, 0, 0),
/** rgba(100,100, 0) */ darkyellow: new RGB(100, 100, 0),
/** rgb( 0,255,255) */ aqua: new RGB(0, 255, 255),
/** rgb(128,128, 0) */ olive: new RGB(128, 128, 0),
/** rgb(245,245,220) */ beige: new RGB(245, 245, 220),
/** rgb(255,228,196) */ bisque: new RGB(255, 228, 196),
/** rgb(255,235,205) */ blanchedalmond: new RGB(255, 235, 205),
/** rgb(222,184,135) */ burlywood: new RGB(222, 184, 135),
/** rgb(255,127, 80) */ coral: new RGB(255, 127, 80),
/** rgb(255,248,220) */ cornsilk: new RGB(255, 248, 220),
/** rgb(184,134, 11) */ darkgoldenrod: new RGB(184, 134, 11),
/** rgb(189,183,107) */ darkkhaki: new RGB(189, 183, 107),
/** rgb(255,250,240) */ floralwhite: new RGB(255, 250, 240),
/** rgb(240,255,240) */ honeydew: new RGB(240, 255, 240),
/** rgb(255,255,240) */ ivory: new RGB(255, 255, 240),
/** rgb(240,230,140) */ khaki: new RGB(240, 230, 140),
/** rgb(230,230,250) */ lavender: new RGB(230, 230, 250),
/** rgb(255,240,245) */ lavenderblush: new RGB(255, 240, 245),
/** rgb(255,250,205) */ lemonchiffon: new RGB(255, 250, 205),
/** rgb(173,216,230) */ lightblue: new RGB(173, 216, 230),
/** rgb(240,128,128) */ lightcoral: new RGB(240, 128, 128),
/** rgb(224,255,255) */ lightcyan: new RGB(224, 255, 255),
/** rgb(250,250,210) */ lightgoldenrodyellow: new RGB(250, 250, 210),
/** rgb(144,238,144) */ lightgreen: new RGB(144, 238, 144),
/** rgb(211,211,211) */ lightgrey: new RGB(211, 211, 211),
/** rgb(255,182,193) */ lightpink: new RGB(255, 182, 193),
/** rgb(255,160,122) */ lightsalmon: new RGB(255, 160, 122),
/** rgb( 32,165,166) */ lightseagreen: new RGB(32, 165, 166),
/** rgb(135,206,250) */ lightskyblue: new RGB(135, 206, 250),
/** rgb(119,136,153) */ lightslategray: new RGB(119, 136, 153),
/** rgb(119,136,153) */ lightslategrey: new RGB(119, 136, 153),
/** rgb(176,196,222) */ lightsteelblue: new RGB(176, 196, 222),
/** rgb(255,255,224) */ lightyellow: new RGB(255, 255, 224),
/** rgb(102,205,170) */ mediumaquamarine: new RGB(102, 205, 170),
/** rgb( 0, 0,205) */ mediumblue: new RGB(0, 0, 205),
/** rgb(186, 85,211) */ mediumorchid: new RGB(186, 85, 211),
/** rgb(147,112,219) */ mediumpurple: new RGB(147, 112, 219),
/** rgb( 60,179,113) */ mediumseagreen: new RGB(60, 179, 113),
/** rgb(123,104,238) */ mediumslateblue: new RGB(123, 104, 238),
/** rgb( 0,250,154) */ mediumspringgreen: new RGB(0, 250, 154),
/** rgb( 72,209,204) */ mediumturquoise: new RGB(72, 209, 204),
/** rgb(199, 21,133) */ mediumvioletred: new RGB(199, 21, 133),
/** rgb(245,255,250) */ mintcream: new RGB(245, 255, 250),
/** rgb(255,228,225) */ mistyrose: new RGB(255, 228, 225),
/** rgb(255,228,181) */ moccasin: new RGB(255, 228, 181),
/** rgb(255,222,173) */ navajowhite: new RGB(255, 222, 173),
/** rgb(253,245,230) */ oldlace: new RGB(253, 245, 230),
/** rgb(107,142, 35) */ olivedrab: new RGB(107, 142, 35),
/** rgb(255, 69, 0) */ orangered: new RGB(255, 69, 0),
/** rgb(218,112,214) */ orchid: new RGB(218, 112, 214),
/** rgb(238,232,170) */ palegoldenrod: new RGB(238, 232, 170),
/** rgb(152,251,152) */ palegreen: new RGB(152, 251, 152),
/** rgb(175,238,238) */ paleturquoise: new RGB(175, 238, 238),
/** rgb(219,112,147) */ palevioletred: new RGB(219, 112, 147),
/** rgb(255,239,213) */ papayawhip: new RGB(255, 239, 213),
/** rgb(255,218,185) */ peachpuff: new RGB(255, 218, 185),
/** rgb(205,133, 63) */ peru: new RGB(205, 133, 63),
/** rgb(255,192,203) */ pink: new RGB(255, 192, 203),
/** rgb(221,160,221) */ plum: new RGB(221, 160, 221),
/** rgb(176,224,230) */ powderblue: new RGB(176, 224, 230),
/** rgb(188,143,143) */ rosybrown: new RGB(188, 143, 143),
/** rgb( 65,105,225) */ royalblue: new RGB(65, 105, 225),
/** rgb(139, 69, 19) */ saddlebrown: new RGB(139, 69, 19),
/** rgb(250,128,114) */ salmon: new RGB(250, 128, 114),
/** rgb(244,164, 96) */ sandybrown: new RGB(244, 164, 96),
/** rgb( 46,139, 87) */ seagreen: new RGB(46, 139, 87),
/** rgb(255,245,238) */ seashell: new RGB(255, 245, 238),
/** rgb(160, 82, 45) */ sienna: new RGB(160, 82, 45),
/** rgb(106, 90,205) */ slateblue: new RGB(106, 90, 205),
/** rgb( 72, 61,139) */ darkslateblue: new RGB(72, 61, 139),
/** rgb(127,255,212) */ aquamarine: new RGB(127, 255, 212),
/** rgb( 47, 79, 79) */ darkslategray: new RGB(47, 79, 79),
/** rgb( 47, 79, 79) */ darkslategrey: new RGB(47, 79, 79),
/** rgb(119,136,153) */ lightslategray: new RGB(119, 136, 153),
/** rgb(119,136,153) */ lightslategrey: new RGB(119, 136, 153),
// /** #F0F8FF */ AliceBlue: RGB.fromHexString('#F0F8FF'),
// /** #FAEBD7 */ AntiqueWhite: RGB.fromHexString('#FAEBD7'),
// /** #00FFFF */ Aqua: RGB.fromHexString('#00FFFF'),
// /** #7FFFD4 */ Aquamarine: RGB.fromHexString('#7FFFD4'),
// /** #F0FFFF */ Azure: RGB.fromHexString('#F0FFFF'),
// /** #F5F5DC */ Beige: RGB.fromHexString('#F5F5DC'),
// /** #FFE4C4 */ Bisque: RGB.fromHexString('#FFE4C4'),
// /** #000000 */ Black: RGB.fromHexString('#000000'),
// /** #FFEBCD */ BlanchedAlmond: RGB.fromHexString('#FFEBCD'),
// /** #0000FF */ Blue: RGB.fromHexString('#0000FF'),
// /** #8A2BE2 */ BlueViolet: RGB.fromHexString('#8A2BE2'),
// /** #A52A2A */ Brown: RGB.fromHexString('#A52A2A'),
// /** #DEB887 */ BurlyWood: RGB.fromHexString('#DEB887'),
// /** #5F9EA0 */ CadetBlue: RGB.fromHexString('#5F9EA0'),
// /** #7FFF00 */ Chartreuse: RGB.fromHexString('#7FFF00'),
// /** #D2691E */ Chocolate: RGB.fromHexString('#D2691E'),
// /** #FF7F50 */ Coral: RGB.fromHexString('#FF7F50'),
// /** #6495ED */ CornflowerBlue: RGB.fromHexString('#6495ED'),
// /** #FFF8DC */ Cornsilk: RGB.fromHexString('#FFF8DC'),
// /** #DC143C */ Crimson: RGB.fromHexString('#DC143C'),
// /** #00FFFF */ Cyan: RGB.fromHexString('#00FFFF'),
// /** #00008B */ DarkBlue: RGB.fromHexString('#00008B'),
// /** #008B8B */ DarkCyan: RGB.fromHexString('#008B8B'),
// /** #B8860B */ DarkGoldenRod: RGB.fromHexString('#B8860B'),
// /** #A9A9A9 */ DarkGray: RGB.fromHexString('#A9A9A9'),
// /** #A9A9A9 */ DarkGrey: RGB.fromHexString('#A9A9A9'),
// /** #006400 */ DarkGreen: RGB.fromHexString('#006400'),
// /** #BDB76B */ DarkKhaki: RGB.fromHexString('#BDB76B'),
// /** #8B008B */ DarkMagenta: RGB.fromHexString('#8B008B'),
// /** #556B2F */ DarkOliveGreen: RGB.fromHexString('#556B2F'),
// /** #FF8C00 */ DarkOrange: RGB.fromHexString('#FF8C00'),
// /** #9932CC */ DarkOrchid: RGB.fromHexString('#9932CC'),
// /** #8B0000 */ DarkRed: RGB.fromHexString('#8B0000'),
// /** #E9967A */ DarkSalmon: RGB.fromHexString('#E9967A'),
// /** #8FBC8F */ DarkSeaGreen: RGB.fromHexString('#8FBC8F'),
// /** #483D8B */ DarkSlateBlue: RGB.fromHexString('#483D8B'),
// /** #2F4F4F */ DarkSlateGray: RGB.fromHexString('#2F4F4F'),
// /** #2F4F4F */ DarkSlateGrey: RGB.fromHexString('#2F4F4F'),
// /** #00CED1 */ DarkTurquoise: RGB.fromHexString('#00CED1'),
// /** #9400D3 */ DarkViolet: RGB.fromHexString('#9400D3'),
// /** #FF1493 */ DeepPink: RGB.fromHexString('#FF1493'),
// /** #00BFFF */ DeepSkyBlue: RGB.fromHexString('#00BFFF'),
// /** #696969 */ DimGray: RGB.fromHexString('#696969'),
// /** #696969 */ DimGrey: RGB.fromHexString('#696969'),
// /** #1E90FF */ DodgerBlue: RGB.fromHexString('#1E90FF'),
// /** #B22222 */ FireBrick: RGB.fromHexString('#B22222'),
// /** #FFFAF0 */ FloralWhite: RGB.fromHexString('#FFFAF0'),
// /** #228B22 */ ForestGreen: RGB.fromHexString('#228B22'),
// /** #FF00FF */ Fuchsia: RGB.fromHexString('#FF00FF'),
// /** #DCDCDC */ Gainsboro: RGB.fromHexString('#DCDCDC'),
// /** #F8F8FF */ GhostWhite: RGB.fromHexString('#F8F8FF'),
// /** #FFD700 */ Gold: RGB.fromHexString('#FFD700'),
// /** #DAA520 */ GoldenRod: RGB.fromHexString('#DAA520'),
// /** #808080 */ Gray: RGB.fromHexString('#808080'),
// /** #808080 */ Grey: RGB.fromHexString('#808080'),
// /** #008000 */ Green: RGB.fromHexString('#008000'),
// /** #ADFF2F */ GreenYellow: RGB.fromHexString('#ADFF2F'),
// /** #F0FFF0 */ HoneyDew: RGB.fromHexString('#F0FFF0'),
// /** #FF69B4 */ HotPink: RGB.fromHexString('#FF69B4'),
// /** #CD5C5C */ IndianRed: RGB.fromHexString('#CD5C5C'),
// /** #4B0082 */ Indigo: RGB.fromHexString('#4B0082'),
// /** #FFFFF0 */ Ivory: RGB.fromHexString('#FFFFF0'),
// /** #F0E68C */ Khaki: RGB.fromHexString('#F0E68C'),
// /** #E6E6FA */ Lavender: RGB.fromHexString('#E6E6FA'),
// /** #FFF0F5 */ LavenderBlush: RGB.fromHexString('#FFF0F5'),
// /** #7CFC00 */ LawnGreen: RGB.fromHexString('#7CFC00'),
// /** #FFFACD */ LemonChiffon: RGB.fromHexString('#FFFACD'),
// /** #ADD8E6 */ LightBlue: RGB.fromHexString('#ADD8E6'),
// /** #F08080 */ LightCoral: RGB.fromHexString('#F08080'),
// /** #E0FFFF */ LightCyan: RGB.fromHexString('#E0FFFF'),
// /** #FAFAD2 */ LightGoldenRodYellow: RGB.fromHexString('#FAFAD2'),
// /** #D3D3D3 */ LightGray: RGB.fromHexString('#D3D3D3'),
// /** #D3D3D3 */ LightGrey: RGB.fromHexString('#D3D3D3'),
// /** #90EE90 */ LightGreen: RGB.fromHexString('#90EE90'),
// /** #FFB6C1 */ LightPink: RGB.fromHexString('#FFB6C1'),
// /** #FFA07A */ LightSalmon: RGB.fromHexString('#FFA07A'),
// /** #20B2AA */ LightSeaGreen: RGB.fromHexString('#20B2AA'),
// /** #87CEFA */ LightSkyBlue: RGB.fromHexString('#87CEFA'),
// /** #778899 */ LightSlateGray: RGB.fromHexString('#778899'),
// /** #778899 */ LightSlateGrey: RGB.fromHexString('#778899'),
// /** #B0C4DE */ LightSteelBlue: RGB.fromHexString('#B0C4DE'),
// /** #FFFFE0 */ LightYellow: RGB.fromHexString('#FFFFE0'),
// /** #00FF00 */ Lime: RGB.fromHexString('#00FF00'),
// /** #32CD32 */ LimeGreen: RGB.fromHexString('#32CD32'),
// /** #FAF0E6 */ Linen: RGB.fromHexString('#FAF0E6'),
// /** #FF00FF */ Magenta: RGB.fromHexString('#FF00FF'),
// /** #800000 */ Maroon: RGB.fromHexString('#800000'),
// /** #66CDAA */ MediumAquaMarine: RGB.fromHexString('#66CDAA'),
// /** #0000CD */ MediumBlue: RGB.fromHexString('#0000CD'),
// /** #BA55D3 */ MediumOrchid: RGB.fromHexString('#BA55D3'),
// /** #9370DB */ MediumPurple: RGB.fromHexString('#9370DB'),
// /** #3CB371 */ MediumSeaGreen: RGB.fromHexString('#3CB371'),
// /** #7B68EE */ MediumSlateBlue: RGB.fromHexString('#7B68EE'),
// /** #00FA9A */ MediumSpringGreen: RGB.fromHexString('#00FA9A'),
// /** #48D1CC */ MediumTurquoise: RGB.fromHexString('#48D1CC'),
// /** #C71585 */ MediumVioletRed: RGB.fromHexString('#C71585'),
// /** #191970 */ MidnightBlue: RGB.fromHexString('#191970'),
// /** #F5FFFA */ MintCream: RGB.fromHexString('#F5FFFA'),
// /** #FFE4E1 */ MistyRose: RGB.fromHexString('#FFE4E1'),
// /** #FFE4B5 */ Moccasin: RGB.fromHexString('#FFE4B5'),
// /** #FFDEAD */ NavajoWhite: RGB.fromHexString('#FFDEAD'),
// /** #000080 */ Navy: RGB.fromHexString('#000080'),
// /** #FDF5E6 */ OldLace: RGB.fromHexString('#FDF5E6'),
// /** #808000 */ Olive: RGB.fromHexString('#808000'),
// /** #6B8E23 */ OliveDrab: RGB.fromHexString('#6B8E23'),
// /** #FFA500 */ Orange: RGB.fromHexString('#FFA500'),
// /** #FF4500 */ OrangeRed: RGB.fromHexString('#FF4500'),
// /** #DA70D6 */ Orchid: RGB.fromHexString('#DA70D6'),
// /** #EEE8AA */ PaleGoldenRod: RGB.fromHexString('#EEE8AA'),
// /** #98FB98 */ PaleGreen: RGB.fromHexString('#98FB98'),
// /** #AFEEEE */ PaleTurquoise: RGB.fromHexString('#AFEEEE'),
// /** #DB7093 */ PaleVioletRed: RGB.fromHexString('#DB7093'),
// /** #FFEFD5 */ PapayaWhip: RGB.fromHexString('#FFEFD5'),
// /** #FFDAB9 */ PeachPuff: RGB.fromHexString('#FFDAB9'),
// /** #CD853F */ Peru: RGB.fromHexString('#CD853F'),
// /** #FFC0CB */ Pink: RGB.fromHexString('#FFC0CB'),
// /** #DDA0DD */ Plum: RGB.fromHexString('#DDA0DD'),
// /** #B0E0E6 */ PowderBlue: RGB.fromHexString('#B0E0E6'),
// /** #800080 */ Purple: RGB.fromHexString('#800080'),
// /** #663399 */ RebeccaPurple: RGB.fromHexString('#663399'),
// /** #FF0000 */ Red: RGB.fromHexString('#FF0000'),
// /** #BC8F8F */ RosyBrown: RGB.fromHexString('#BC8F8F'),
// /** #4169E1 */ RoyalBlue: RGB.fromHexString('#4169E1'),
// /** #8B4513 */ SaddleBrown: RGB.fromHexString('#8B4513'),
// /** #FA8072 */ Salmon: RGB.fromHexString('#FA8072'),
// /** #F4A460 */ SandyBrown: RGB.fromHexString('#F4A460'),
// /** #2E8B57 */ SeaGreen: RGB.fromHexString('#2E8B57'),
// /** #FFF5EE */ SeaShell: RGB.fromHexString('#FFF5EE'),
// /** #A0522D */ Sienna: RGB.fromHexString('#A0522D'),
// /** #C0C0C0 */ Silver: RGB.fromHexString('#C0C0C0'),
// /** #87CEEB */ SkyBlue: RGB.fromHexString('#87CEEB'),
// /** #6A5ACD */ SlateBlue: RGB.fromHexString('#6A5ACD'),
// /** #708090 */ SlateGray: RGB.fromHexString('#708090'),
// /** #708090 */ SlateGrey: RGB.fromHexString('#708090'),
// /** #FFFAFA */ Snow: RGB.fromHexString('#FFFAFA'),
// /** #00FF7F */ SpringGreen: RGB.fromHexString('#00FF7F'),
// /** #4682B4 */ SteelBlue: RGB.fromHexString('#4682B4'),
// /** #D2B48C */ Tan: RGB.fromHexString('#D2B48C'),
// /** #008080 */ Teal: RGB.fromHexString('#008080'),
// /** #D8BFD8 */ Thistle: RGB.fromHexString('#D8BFD8'),
// /** #FF6347 */ Tomato: RGB.fromHexString('#FF6347'),
// /** #40E0D0 */ Turquoise: RGB.fromHexString('#40E0D0'),
// /** #EE82EE */ Violet: RGB.fromHexString('#EE82EE'),
// /** #F5DEB3 */ Wheat: RGB.fromHexString('#F5DEB3'),
// /** #FFFFFF */ White: RGB.fromHexString('#FFFFFF'),
// /** #F5F5F5 */ WhiteSmoke: RGB.fromHexString('#F5F5F5'),
// /** #FFFF00 */ Yellow: RGB.fromHexString('#FFFF00'),
// /** #9ACD32 */ YellowGreen: RGB.fromHexString('#9ACD32'),
}
RGB.ROYGBIV = [
RGB.list.red,
RGB.list.orange,
RGB.list.yellow,
RGB.list.green,
RGB.list.blue,
RGB.list.indigo,
RGB.list.violet
]
export class RGBA extends RGB {
static defaultValues = [...super.defaultValues, 1]
static defaultSize = 4;
get a() { return this[3] } set a(v) { this[3] = v }
static valueNames = [...super.valueNames, ['a', '_a']]
/** @returns {this} */ plusA(n) { return this.plusComp(3, n) }
/** @returns {this} */ minusA(n) { return this.minusComp(3, n) }
/** @returns {this} */ setA(n) { return this.setComp(3, n) }
/** @returns {this} */ restrictA(min, max, wrap) { return this.clampComp(3, min, max, wrap) }
get rgba() { return Color.rgbaCSS(...this) }
get effectiveRGB() { return this.toRGBA(1) }
/** @returns {RGB} */ toRGB() { return new RGB(this[0], this[1], this[2]) }
/** @returns {RGBA} */ toRGBA(a) { return new RGBA(this[0], this[1], this[2], a) }
/** @returns {string} rounded css string */ rgbaDisplay(decRGB = 0, decA = 2) { return Color.rgbaCSS(this.r.toFixed(decRGB), this.g.toFixed(decRGB), this.b.toFixed(decRGB), this.a.toFixed(decA)) }
/** @returns {string} */get hexstring() {
const a = Math.round(this.a * 255);
if (a === 255) {
return Color.rgb2hexString(...this)
} else {
return Color.rgba2hexString(this[0], this[1], this[2], a)
}
}
/** @returns {Number} */get hexnumber() { return Color.rgba2hexNumber(...this) }
/** @template C @this C @returns {InstanceType<C>} */ static fromHexNumber(h) { return new this(...Color.hexNumber2rgba(h)) }
/** @template C @this C @returns {InstanceType<C>} */ static fromHexString(hex) { return new this(...Color.hex2rgba(hex)); }
/** @template C @this C @returns {InstanceType<C>} */ static fromName(str) { return RGB.fromName(str).toRGBA(1) }
/** @template C @this C @returns {InstanceType<C>} */ static fromHSL(hsl, a = 1) { return new this(...Color.hsl2rgb(...hsl), a) }
}
RGBA.list = {}
Object.entries(RGB.list).forEach(([key, val]) => { RGBA.list[key] = val.toRGBA(1) })
RGBA.list.transparent = new RGBA(0, 0, 0, 0);
RGBA.ROYGBIV = RGB.ROYGBIV.map(v => v.toRGBA(1))
export const rgbl = RGB.list;
export const rgbal = RGBA.list;
// class A {
// /** @template C @this C @returns {InstanceType<C>} */
// static f(abc) {return new this();}
// foo() {}
// }
// class B extends A {faa() {}}
// const d = B.f()
Select All
import { Num, El, Events, Style, For, Obj, Px, Unit, Color, StackedClassAtts, StackedClass, ObjectManagerAtts, ObjectManager } from './tools.js'
import { List, V2, Polygon, RGB, RGBA } from './array.js'
//* User clicks and touches
export class ClickXY extends V2 {
/** @template C @this C @param {MouseEvent} e @returns {InstanceType<C>} */
static parseClient(e, carryOverParams = {}) {
const c = new this(e.clientX, e.clientY)
c.data = carryOverParams;
return c;
}
/** @template C @this C @param {MouseEvent} e @param {HTMLElement} elem @returns {InstanceType<C>} */
static within(e, elem) {return this.withinRect(e, elem.getBoundingClientRect())}
/** @template C @this C @param {MouseEvent} e @param {{left:number,top:number}} rect @returns {InstanceType<C>} */
static withinRect(e, rect) {return new this(e.clientX - rect.left, e.clientY - rect.top)}
lrudDirectionFrom(origin = [0,0]) {const dir = this.copy.minus(origin); return Math.abs(dir.x)>Math.abs(dir.y) ? (dir.x > 0 ? 'r' : 'l') : (dir.y > 0 ? 'd' : 'u')}
}
export class TouchXY extends ClickXY {
setupSwipePattern(element, start, move, end) {
El.ev(this)
}
}
/** @extends List<TouchXY> */
export class Touches extends List {
/**
* @param {TouchEvent} e
* @param {HTMLElement} elem
* @returns {Touches}
**/
static within(e, elem) {
const rect = elem.getBoundingClientRect();
return this.from(Array.from(e.touches).map(touch=>TouchXY.withinRect(touch, rect)))
}
}
const ValueStateAtts = Obj.join(StackedClassAtts, {
})
/** @template {ValueStateAtts} A @extends StackedClass<A> */
export class ValueState extends StackedClass {
static defaultAtts = ValueStateAtts
constructor(atts) { super(atts) }
get value() { return this._value } set value(v) { this._lastValue = this.value; this._value = v }
get lastValue() { return this._lastValue }
}
export const ButtonStateAtts = Obj.join(ValueStateAtts, {
dblClickThreshold: 200
})
/** @template {ButtonStateAtts} A @extends ValueState<A> */
export class ButtonState extends ValueState {
static defaultAtts = ButtonStateAtts
constructor(atts) { super(atts) }
/** @type {boolean} */ dblClick = false;
/** @type {boolean} */ dblDown = false;
/** @type {number} */ timeDown = false;
// /** @type {boolean} */ dblUp = false;
/** @type {number} */ timeUp = false;
/** @returns {boolean} */ get down() { return this.value ?? 0 }
get dblClickThreshold() { return this.atts.dblClickThreshold }
onlyButtonDown;
downEvent() {
let t = Date.now();
this.value = 1;
if (this.timeUp > this.timeDown) { this.dblDown = (t - this.timeDown) < this.dblClickThreshold; }
this.dblClick = false;
this.timeDown = t;
}
upEvent() {
let t = Date.now();
this.value = 0;
this.dblClick = (t - this.timeUp) < this.dblClickThreshold;
this.dblDown = false;
this.timeUp = t;
}
clearState() {
this.dblClick = false;
this.dblDown = false;
this.timeUp = 0;
this.timeDown = 0;
this.value = 0;
}
}
export const MouseButtonManagerAtts = Obj.join(ObjectManagerAtts, {
withEvents: true,
dblClickThreshold: 200,
numButtons: 5,
buttonTarget: window.document ?? null,
})
/** @template {MouseButtonManagerAtts}A @template {ButtonState} T @extends {ObjectManager<A,T>} */
export class MouseButtonManager extends ObjectManager {
static ButtonMain = '0';
static ButtonWheel = '1';
static ButtonAlt = '2';
static ButtonBack = '3';
static ButtonFwd = '4';
static defaultAtts = MouseButtonManagerAtts
get buttonTarget() { return this.atts.buttonTarget }
get dblClickThreshold() { return this.atts.dblClickThreshold }
/** @param {A} atts */
constructor(atts) {
super(atts);
if (this.atts.withEvents) { this.addEvents() }
For.i(this.atts.numButtons, i => {
this.add(new ButtonState({
dblClickThreshold: this.dblClickThreshold
}), i.toString(), true)
})
}
onlyButtonDown = null;
_onMouseDown(e) {
this.someButtonDown = true;
let mouseButton = this.$[e.button];
mouseButton?.downEvent();
this._onMouseEvent(e, mouseButton);
this.onMouseDown?.(e, mouseButton);
}
_onMouseUp(e) {
this.someButtonDown = false;
let mouseButton = this.$[e.button];
mouseButton?.upEvent();
this._onMouseEvent(e, mouseButton);
this.onMouseUp?.(e, mouseButton);
}
_onMouseMove(e) {
this.onMouseMove?.(e, this);
}
_onMouseEvent(e) {
// Check if there is a singular mouse button down
let clicked = this.list.filter(b => b.down);
this.singleButtonDown = clicked?.length === 1 ? clicked[0] : null;
this.list.forEach(b => { b.onlyButtonDown = b === this.singleButtonDown })
}
addEvents() {
El.ev(this.buttonTarget, 'mousedown', e => this._onMouseDown(e))
El.ev(window, 'mouseup', e => this._onMouseUp(e))
El.ev(this.buttonTarget, 'mousemove', e => this._onMouseMove(e))
}
// clearAllStates() {this.list.forEach(b => b.clearState())}
get main() { return this.$[MouseButtonManager.ButtonMain] }
get alt() { return this.$[MouseButtonManager.ButtonAlt] }
get wheel() { return this.$[MouseButtonManager.ButtonWheel] }
get back() { return this.$[MouseButtonManager.ButtonBack] }
get fwd() { return this.$[MouseButtonManager.ButtonFwd] }
}
export const MBM = MouseButtonManager;
export const KeyStateAtts = Obj.join(ButtonStateAtts, {
code: null,
keys: null,
modifiers: null,
})
/** @template {KeyStateAtts} A @extends ButtonState<A> */
export class KeyState extends ButtonState {
static defaultAtts = KeyStateAtts
get code() { return this.atts.code }
get keys() { return this.atts.keys }
get modifiers() { return this.atts.modifiers }
/** @param {A} atts */ constructor(atts) { super(atts) }
static code = {
F1: 'F1', F2: 'F2', F3: 'F3', F4: 'F4', F5: 'F5', F6: 'F6', F7: 'F7', F8: 'F8', F9: 'F9', F10: 'F10', F11: 'F11', F12: 'F12',
D1: 'Digit1', D2: 'Digit2', D3: 'Digit3', D4: 'Digit4', D5: 'Digit5', D6: 'Digit6', D7: 'Digit7', D8: 'Digit8', D9: 'Digit9', D0: 'Digit0',
N1: 'Numpad1', N2: 'Numpad2', N3: 'Numpad3', N4: 'Numpad4', N5: 'Numpad5', N6: 'Numpad6', N7: 'Numpad7', N8: 'Numpad8', N9: 'Numpad9', N0: 'Numpad0',
A: 'KeyA', B: 'KeyB', C: 'KeyC', D: 'KeyD', E: 'KeyE', F: 'KeyF', G: 'KeyG', H: 'KeyH', I: 'KeyI', J: 'KeyJ', K: 'KeyK', L: 'KeyL', M: 'KeyM', N: 'KeyN', O: 'KeyO', P: 'KeyP', Q: 'KeyQ', R: 'KeyR', S: 'KeyS', T: 'KeyT', U: 'KeyU', V: 'KeyV', W: 'KeyW', X: 'KeyX', Y: 'KeyY', Z: 'KeyZ',
Period: 'Period', Comma: 'Comma', Semicolon: 'Semicolon', Slash: 'Slash', Backslash: 'Backslash', Minus: 'Minus', Equal: 'Equal', BracketL: 'BracketLeft', BracketR: 'BracketRight', Quote: 'Quote', Backquote: 'Backquote',
Tab: 'Tab', Space: 'Space', Enter: 'Enter', Backspace: 'Backspace', Delete: 'Delete',
Up: 'ArrowUp', Down: 'ArrowDown', Left: 'ArrowLeft', Right: 'ArrowRight',
PageUp: 'PageUp', PageDown: 'PageDown', Home: 'Home', End: 'End',
ContextMenu: 'ContextMenu', Insert: 'Insert', Pause: 'Pause', Escape: 'Escape',
AltL: 'AltLeft', AltR: 'AltRight', ControlL: 'ControlLeft', ControlR: 'ControlRight', ShiftL: 'ShiftLeft', ShiftR: 'ShiftRight', MetaL: 'MetaLeft',
CapsLock: 'CapsLock', NumLock: 'NumLock', ScrollLock: 'ScrollLock',
}
}
export const KeyStateManagerAtts = Obj.join(ObjectManagerAtts, {
withEvents: true,
dblClickThreshold: 200,
})
/** @template {KeyStateManagerAtts} A @template {KeyState} T @extends ObjectManager<A,T> */
export class KeyStateManager extends ObjectManager {
static defaultAtts = KeyStateManagerAtts
get dblClickThreshold() { return this.atts.dblClickThreshold }
/** @param {A} atts */
constructor(atts) {
super(atts);
if (this.atts.withEvents) { this.addEvents() }
Obj.forEach(Obj.clone(KeyStateManager.codes), (data, code) => {
this.add(new KeyState({
code, keys: data.keys, modifiers: data.modifiers,
dblClickThreshold: this.dblClickThreshold,
}), code, true)
})
}
/** @returns {boolean} */ get ctrl() { return this._ctrl } set ctrl(b) { this._ctrl = b; }
/** @returns {boolean} */ get alt() { return this._alt } set alt(b) { this._alt = b; }
/** @returns {boolean} */ get shift() { return this._shift } set shift(b) { this._shift = b; }
/** @returns {boolean} */ get meta() { return this._meta } set meta(b) { this._meta = b; }
#onKeyUp; #onKeyDown;
addEvents() {
this.#onKeyUp = this._onKeyUp.bind(this)
this.#onKeyDown = this._onKeyDown.bind(this)
El.ev(document, 'keyup', this.#onKeyUp);
El.ev(document, 'keydown', this.#onKeyDown);
}
dispose() {
document.removeEventListener('keyup', this.#onKeyUp)
document.removeEventListener('keydown', this.#onKeyDown)
}
_onKeyDown(e) {
let code;
if (e.code && e.code !== '') { code = this.codes[e.code]; }
if (code) {
code.downEvent();
this._onKeyEvent(e, code);
this.onKeyDown(e, code);
} else {
// console.warn('KeyStateManager not configured to handle input with event data: ', e)
}
}
_onKeyUp(e) {
let code;
if (e.code && e.code !== '') { code = this.codes[e.code]; }
if (code) {
code.upEvent();
this._onKeyEvent(e, code);
this.onKeyUp(e, code);
} else {
// console.warn('KeyStateManager not configured to handle input with event data: ', e)
}
}
_onKeyEvent(e) {
this.ctrl = e.ctrlKey;
this.alt = e.altKey;
this.shift = e.shiftKey;
this.meta = e.metaKey;
}
/** @param {KeyboardEvent} e @param {KeyState} key */ onKeyDown(e, key) { }
/** @param {KeyboardEvent} e @param {KeyState} key */ onKeyUp(e, key) { }
get f1() { return this.codes['F1'] } get f2() { return this.codes['F2'] } get f3() { return this.codes['F3'] } get f4() { return this.codes['F4'] } get f5() { return this.codes['F5'] } get f6() { return this.codes['F6'] } get f7() { return this.codes['F7'] } get f8() { return this.codes['F8'] } get f9() { return this.codes['F9'] } get f10() { return this.codes['F10'] } get f11() { return this.codes['F11'] } get f12() { return this.codes['F12'] }
get d1() { return this.codes['Digit1'] } get d2() { return this.codes['Digit2'] } get d3() { return this.codes['Digit3'] } get d4() { return this.codes['Digit4'] } get d5() { return this.codes['Digit5'] } get d6() { return this.codes['Digit6'] } get d7() { return this.codes['Digit7'] } get d8() { return this.codes['Digit8'] } get d9() { return this.codes['Digit9'] } get d0() { return this.codes['Digit0'] }
get n1() { return this.codes['Numpad1'] } get n2() { return this.codes['Numpad2'] } get n3() { return this.codes['Numpad3'] } get n4() { return this.codes['Numpad4'] } get n5() { return this.codes['Numpad5'] } get n6() { return this.codes['Numpad6'] } get n7() { return this.codes['Numpad7'] } get n8() { return this.codes['Numpad8'] } get n9() { return this.codes['Numpad9'] } get n0() { return this.codes['Numpad0'] }
get a() { return this.codes['KeyA'] } get b() { return this.codes['KeyB'] } get c() { return this.codes['KeyC'] } get d() { return this.codes['KeyD'] } get e() { return this.codes['KeyE'] } get f() { return this.codes['KeyF'] } get g() { return this.codes['KeyG'] } get h() { return this.codes['KeyH'] } get i() { return this.codes['KeyI'] } get j() { return this.codes['KeyJ'] } get k() { return this.codes['KeyK'] } get l() { return this.codes['KeyL'] } get m() { return this.codes['KeyM'] } get n() { return this.codes['KeyN'] } get o() { return this.codes['KeyO'] } get p() { return this.codes['KeyP'] } get q() { return this.codes['KeyQ'] } get r() { return this.codes['KeyR'] } get s() { return this.codes['KeyS'] } get t() { return this.codes['KeyT'] } get u() { return this.codes['KeyU'] } get v() { return this.codes['KeyV'] } get w() { return this.codes['KeyW'] } get x() { return this.codes['KeyX'] } get y() { return this.codes['KeyY'] } get z() { return this.codes['KeyZ'] }
get period() { return this.codes['Period'] } get comma() { return this.codes['Comma'] } get semicolon() { return this.codes['Semicolon'] } get slash() { return this.codes['Slash'] } get backslash() { return this.codes['Backslash'] } get minus() { return this.codes['Minus'] } get equal() { return this.codes['Equal'] } get bracketLeft() { return this.codes['BracketLeft'] } get bracketRight() { return this.codes['BracketRight'] } get quote() { return this.codes['Quote'] } get backquote() { return this.codes['Backquote'] }
get tab() { return this.codes['Tab'] } get space() { return this.codes['Space'] } get enter() { return this.codes['Enter'] } get backspace() { return this.codes['Backspace'] } get delete() { return this.codes['Delete'] }
get up() { return this.codes['ArrowUp'] } get down() { return this.codes['ArrowDown'] } get left() { return this.codes['ArrowLeft'] } get right() { return this.codes['ArrowRight'] }
get pageUp() { return this.codes['PageUp'] } get pageDown() { return this.codes['PageDown'] } get home() { return this.codes['Home'] } get end() { return this.codes['End'] }
get contextMenu() { return this.codes['ContextMenu'] } get insert() { return this.codes['Insert'] } get pause() { return this.codes['Pause'] } get escape() { return this.codes['Escape'] }
get altLeft() { return this.codes['AltLeft'] } get altRight() { return this.codes['AltRight'] } get controlLeft() { return this.codes['ControlLeft'] } get controlRight() { return this.codes['ControlRight'] } get shiftLeft() { return this.codes['ShiftLeft'] } get shiftRight() { return this.codes['ShiftRight'] } get metaLeft() { return this.codes['MetaLeft'] }
get capsLock() { return this.codes['CapsLock'] } get numLock() { return this.codes['NumLock'] } get scrollLock() { return this.codes['ScrollLock'] }
/** @type {Object<string, KeyState>} */
get codes() { return this.$ }
static codes = {
Digit1: { keys: ['1', '!'] },
Digit2: { keys: ['2', '@'] },
Digit3: { keys: ['3', '#'] },
Digit4: { keys: ['4', '$'] },
Digit5: { keys: ['5', '%'] },
Digit6: { keys: ['6', '^'] },
Digit7: { keys: ['7', '&'] },
Digit8: { keys: ['8', '*'] },
Digit9: { keys: ['9', '('] },
Digit0: { keys: ['0', ')'] },
KeyQ: { keys: ['q', 'Q'] },
KeyW: { keys: ['w', 'W'] },
KeyE: { keys: ['e', 'E'] },
KeyR: { keys: ['r', 'R'] },
KeyT: { keys: ['t', 'T'] },
KeyY: { keys: ['y', 'Y'] },
KeyU: { keys: ['u', 'U'] },
KeyI: { keys: ['i', 'I'] },
KeyO: { keys: ['o', 'O'] },
KeyP: { keys: ['p', 'P'] },
KeyA: { keys: ['a', 'A'] },
KeyS: { keys: ['s', 'S'] },
KeyD: { keys: ['d', 'D'] },
KeyF: { keys: ['f', 'F'] },
KeyG: { keys: ['g', 'G'] },
KeyH: { keys: ['h', 'H'] },
KeyJ: { keys: ['j', 'J'] },
KeyK: { keys: ['k', 'K'] },
KeyL: { keys: ['l', 'L'] },
KeyZ: { keys: ['z', 'Z'] },
KeyX: { keys: ['x', 'X'] },
KeyC: { keys: ['c', 'C'] },
KeyV: { keys: ['v', 'V'] },
KeyB: { keys: ['b', 'B'] },
KeyN: { keys: ['n', 'N'] },
KeyM: { keys: ['m', 'M'] },
Period: { keys: ['.', '>'] },
Comma: { keys: [',', '<'] },
Semicolon: { keys: [';', ':'] },
Slash: { keys: ['/', '?'] },
Backslash: { keys: ['\\', '|'] },
Minus: { keys: ['-', '_'] },
Equal: { keys: ['=', '+'] },
BracketLeft: { keys: ['[', '{'] },
BracketRight: { keys: [']', '}'] },
Quote: { keys: ['\'', '"'] },
Backquote: { keys: ['`', '~'] },
Tab: { keys: ['Tab'] },
Space: { keys: [' '] },
Enter: { keys: ['Enter'] },
Backspace: { keys: ['Backspace'] },
Delete: { keys: ['Delete'] },
Home: { keys: ['Home'] },
End: { keys: ['End'] },
PageUp: { keys: ['PageUp'] },
PageDown: { keys: ['PageDown'] },
ContextMenu: { keys: ['ContextMenu'] },
Insert: { keys: ['Insert'] },
Pause: { keys: ['Pause'] },
Escape: { keys: ['Escape'] },
AltLeft: { keys: ['Alt'] },
AltRight: { keys: ['Alt'] },
ControlLeft: { keys: ['Control'] },
ControlRight: { keys: ['Control'] },
ShiftLeft: { keys: ['Shift'] },
ShiftRight: { keys: ['Shift'] },
MetaLeft: { keys: ['Meta'] },
CapsLock: { keys: ['CapsLock'] },
NumLock: { keys: ['NumLock'] },
ScrollLock: { keys: ['ScrollLock'] },
Numpad1: { keys: ['1', 'End'] },
Numpad2: { keys: ['2', 'ArrowDown'] },
Numpad3: { keys: ['3', 'PageDown'] },
Numpad4: { keys: ['4', 'ArrowLeft'] },
Numpad5: { keys: ['5', 'Clear'] },
Numpad6: { keys: ['6', 'ArrowRight'] },
Numpad7: { keys: ['7', 'Home'] },
Numpad8: { keys: ['8', 'ArrowUp'] },
Numpad9: { keys: ['9', 'PageUp'] },
Numpad0: { keys: ['0', 'Insert'] },
NumpadDecimal: { keys: ['.', 'Delete'] },
NumpadDivide: { keys: ['/'] },
NumpadMultiply: { keys: ['*'] },
NumpadSubtract: { keys: ['-'] },
NumpadAdd: { keys: ['+'] },
NumpadEnter: { keys: ['Enter'] },
ArrowUp: { keys: ['ArrowUp'] },
ArrowLeft: { keys: ['ArrowLeft'] },
ArrowDown: { keys: ['ArrowDown'] },
ArrowRight: { keys: ['ArrowRight'] },
F1: { keys: ['F1'] },
F2: { keys: ['F2'] },
F3: { keys: ['F3'] },
F4: { keys: ['F4'] },
F5: { keys: ['F5'] },
F6: { keys: ['F6'] },
F7: { keys: ['F7'] },
F8: { keys: ['F8'] },
F9: { keys: ['F9'] },
F10: { keys: ['F10'] },
F11: { keys: ['F11'] },
F12: { keys: ['F12'] },
}
}
export const SelectionManagerAtts = Obj.join(ObjectManagerAtts, {
multiselect: false,
requireSelection: true,
})
/** @template {SelectionManagerAtts} A @template {{id:string}} T @extends {ObjectManager<A,T>} */
export class SelectionManager extends ObjectManager {
static defaultAtts = SelectionManagerAtts
get selectedItem() { return this.selectedItems[0] }
get selectedItems() { return Array.from(this.selectedIDs).map(id => this.objects[id]) }
/** Set requireSelection value */
get requireSelection() { return this.atts.requireSelection } set requireSelection(b) {
const prev = this.atts.requireSelection;
this.atts.requireSelection = b;
if (b !== prev && b && !this.selectedIDs.size) { this.selectDefault() }
}
/** Set multiselect value */
get multiselect() { return this.atts.multiselect } set multiselect(b) {
const prev = this.atts.multiselect;
this.atts.multiselect = b;
// If multiselect value has changed AND is false
if (b !== prev && !b) {
this.selectedIDsPrevious = new Set(this.selectedIDs)
const n = this.selectedIDs.size;
if (!this.selectedIDs.size) {
this.selectDefault();
} else if (this.selectedIDs.size > 1) {
const selected = Array.from(this.selectedIDs)
const keep = this.lastSelectedID ?? selected[0].id;
selected.forEach(id => { if (id !== keep) { this.selectedIDs.delete(id) } })
this.dispatchEvent(new CustomEvent('selectionChange'))
}
}
}
/** @type {Set<string>} */ selectedIDs = new Set()
/** @type {T} */ defaultSelection;
lastSelectedID = true;
/** Attempt to select default, and if successful, dispatch selectionChange event */
selectDefault() { if (this.#selectDefault()) { this.dispatchEvent(new CustomEvent('selectionChange')) } }
#selectDefault() {
if (this.requireSelection) {
const newSelection = this.defaultSelection ?? this.list[0]
if (newSelection) {
this.selectedIDs.add(newSelection.id);
this.lastSelectedID = newSelection.id;
return true;
}
}
}
/** @param {T} o @returns {string} */
onAdd(o, id) {
// If first item, multiselect is not enabled, and requireSelection is enabled, then select this first item
if ((this.list.length === 1) && !this.multiselect) { this.#selectDefault() }
}
/** @param {T} o @returns {string} */
onRemove(o, id) {
if (this.lastSelectedID === o.id) { this.lastSelectedID = null }
if (this.defaultSelection === o) { this.defaultSelection = null }
if (this.selectedIDs.has(o.id)) {
this.selectedIDsPrevious = new Set(this.selectedIDs)
this.selectedIDs.delete(o.id)
// If not multiselect and item was selected, now another needs to be selected
if (!this.multiselect) { this.#selectDefault() }
this.dispatchEvent(new CustomEvent('selectionChange'))
}
}
toggleItem(item) {
if (item && this.hasID(item.id)) {
if (this.selectedIDs.has(item.id)) {
this.unselectItem(item)
} else {
this.selectItem(item)
}
}
}
/** @param {T} item */
selectItem(item) {
if (item && this.hasID(item.id)) {
this.selectedIDsPrevious = new Set(this.selectedIDs)
// Add item to selection
this.selectedIDs.add(item.id)
this.lastSelectedID = item.id;
// If multiselect is not enabled, previous item(s)
if (!this.multiselect) { this.selectedIDsPrevious.forEach(id => { if (id !== item.id) { this.selectedIDs.delete(id) } }) }
this.dispatchEvent(new CustomEvent('selectionChange'))
}
}
selectID(id) { this.selectItem(this.objects[id]) }
/** @param {T} item */
unselectItem(item) {
if (item && this.hasID(item.id)) {
if (!this.multiselect && this.requireSelection && this.selectedIDs.has(item.id)) { return }
this.selectedIDsPrevious = new Set(this.selectedIDs)
// Remove item from selection
this.selectedIDs.delete(item.id)
if (this.lastSelectedID === item.id) { this.lastSelectedID = null }
this.dispatchEvent(new CustomEvent('selectionChange'))
}
}
unselectID(id) { this.unselectItem(this.objects[id]) }
toggleAll(toState) {
if (this.multiselect) {
this.selectedIDsPrevious = new Set(this.selectedIDs)
console.log(this.selectedIDs.size, this.list.length)
console.log(!toState)
if ((toState === false) || (!toState && (this.selectedIDs.size == this.list.length))) {
// All selected - unselect all
console.log('unselecting all')
this.list.forEach(item => this.selectedIDs.delete(item.id))
} else {
console.log('selecting all')
// Not all selected - select the remaining
this.list.forEach(item => this.selectedIDs.add(item.id))
}
this.dispatchEvent(new CustomEvent('selectionChange'))
}
}
}
//* DOCUMENT ELEMENTS
// // export const SplitPaneHandleType = {
// // click: 'click',
// // drag: 'drag',
// // }
// export const SplitPaneAtts = Obj.join(StackedClassAtts, {
// container: null,
// dependent: null,
// handle: null,
// top: null,
// left: null,
// right: null,
// bottom: null,
// mobileSwap: false,
// drag: true,
// open: true,
// minSize: 0,
// maxSize: null,
// })
// /** @template {SplitPaneAtts} T @extends StackedClass<T> */
// export class SplitPane extends StackedClass {
// static defaultAtts = SplitPaneAtts
// /** @param {T} atts */
// constructor(atts) {
// super(atts);
// this.container = new El(this.atts.container)
// this.dependent = new El(this.atts.dependent)
// if (this.atts.handle) {
// this.handle = new El(this.atts.handle)
// } else {
// this.handle = new El(El.hr())
// this.dependent.after(this.handle.el)
// }
// // this.handle.ctog('resize', true)
// this.container.ctog('dividedPane', true)
// this.container.ctog('l', this.atts.left)
// this.container.ctog('r', this.atts.right)
// this.container.ctog('t', this.atts.top)
// this.container.ctog('b', this.atts.bottom)
// this.container.ctog('mobileSwap', this.atts.mobileSwap)
// this.minSize = this.atts.minSize ?? 0;
// this.maxSize = this.atts.maxSize ?? Infinity;
// this.dim = window.getComputedStyle(this.container)
// }
// tog(d) {const _d = d.toLowerCase(); 'lrtb'.split('').forEach(l=>this.container.ctog(l,_d.includes(l)))}
// /** @type {El} */ get container() { return this._container } set container(c) { if (this._container) { this._container.restore() } this._container = c; this.container.save() }
// /** @type {El} */ get dependent() { return this._dependent } set dependent(c) { if (this._dependent) { this._dependent.restore() } this._dependent = c; this.dependent.save() }
// /** @type {El} */ get handle() { return this._handle } set handle(c) { if (this._handle) { this._handle.restore() } this._handle = c; this.handle.save() }
// get dim() {return this._dim} set dim(d) {this._dim = Num.clamp(d, this.minSize, this.maxSize); Style.setVar('dim', this.dim + 'px', this.dependent)}
// get isVertical() {return Style.getVar('vertical', this.container)}
// get isOpen() {
// return
// }
// initDrag() {
// El.ev(this.handle, 'pointerdown', e=>{
// })
// }
// }
// export function paneOpenerButton(buttonElement, paneElement) {
// const btn = El.q(buttonElement);
// const pane = El.q(paneElement);
// El.ev(btn, 'click', El.toggleHidden.bind(null, pane))
// }
Select All
import { El, Px, Obj, Parse, Inf, StackedClassAtts, StackedClass, ObjectManager, ObjectManagerAtts } from '../tools.js'
import { List, Nums, V2, Domain, BBox } from '../array.js'
import { ClickXY, TouchXY, Touches } from '../input.js'
/** Extended Path2D class for additional functionality
* @extends {Path2D}
* */
export class CanvasPath extends Path2D {
/** @param {CanvasRenderingContext2D} context Canvas context element @param {ContextStyle | ()=>ContextStyle} style Style object or anonymous function that returns a style object */
static applyStyle(context, style, ...a) { const s = Parse.func(style); for (let att in s) { context[att] = Parse.func(s[att], ...a) } }
moveToLineTo(points, close) { CanvasPath.moveToLineTo(this, points, close); return this; }
/** @param {Path2D} path2D */
static moveToLineTo(path2D, points, close = false) {
if (points.length) {
points.forEach((pt, j) => { if (j) { path2D.lineTo(...pt) } else { path2D.moveTo(...pt) } })
if (close) { path2D.closePath(); }
}
}
static circle(x, y, radius) { const g = new CanvasPath(); g.arc(x, y, radius, 0, 2 * Math.PI); return g; }
}
/** Enumeration of types to define the draw style of a Graphic instance
* @enum {typeof GRAPHIC_METHODS[keyof typeof GRAPHIC_METHODS]}
* */
export const GRAPHIC_METHODS = /** @type {const} */ {
FILL: 0,
STROKE: 1,
FILL_STROKE: 2,
STROKE_FILL: 3,
}
export const GraphicAtts = Obj.join(StackedClassAtts, {
style: false,
revertStyle: false,
preDraw: false,
postDraw: false,
})
/** Graphics store individual Path2D elements to avoid recreating paths when changing styles / fill-stroke methods.
* Emits 'preDraw' and 'postDraw' events in draw method
* @template {GraphicAtts} A @extends StackedClass<A>
* */
export class Graphic extends StackedClass {
static defaultAtts = GraphicAtts
/** @type {Path2D} */ path
/** @type {Number} */ method
/** @type {ContextStyle} */ style
/** @type {Boolean} */ revertStyle
/** @param {Path2D} path @param {Number} method enum value from Graphic.METHODS @param {A} atts */
constructor(method, path, atts) {
super(atts);
this.method = method;
this.path = path;
this.style = this.atts.style
this.revertStyle = this.atts.revertStyle;
if (this.atts.preDraw) { El.ev(this, 'preDraw', ()=>{this.atts.preDraw()}) }
if (this.atts.postDraw) { El.ev(this, 'postDraw', ()=>{this.atts.postDraw()}) }
}
/**@param {keyof GRAPHIC_METHODS} n @param {Path2D|GraphicAtts} a @param {GraphicAtts} b @returns {Graphic}*/
static _fromMethod(n, a, b) { const args = a instanceof Path2D ? [a, b] : [new CanvasPath(), a]; return new Graphic(n, ...args) }
/**@overload @param {Path2D} path @param {GraphicAtts} atts @returns {Graphic} @overload @param {GraphicAtts} atts @returns {Graphic}*/ static fill(...a) { return this._fromMethod(GRAPHIC_METHODS.FILL, ...a) }
/**@overload @param {Path2D} path @param {GraphicAtts} atts @returns {Graphic} @overload @param {GraphicAtts} atts @returns {Graphic}*/ static stroke(...a) { return this._fromMethod(GRAPHIC_METHODS.STROKE, ...a) }
/**@overload @param {Path2D} path @param {GraphicAtts} atts @returns {Graphic} @overload @param {GraphicAtts} atts @returns {Graphic}*/ static fillStroke(...a) { return this._fromMethod(GRAPHIC_METHODS.FILL_STROKE, ...a) }
/**@overload @param {Path2D} path @param {GraphicAtts} atts @returns {Graphic} @overload @param {GraphicAtts} atts @returns {Graphic}*/ static strokeFill(...a) { return this._fromMethod(GRAPHIC_METHODS.STROKE_FILL, ...a) }
/** @param {PannableCanvas} rend */
_draw(rend) {
this.dispatchEvent(new Event('preDraw'))
if (this.revertStyle) {rend.context.save()};
if (this.style) { CanvasPath.applyStyle(rend.context, this.style, rend) }
this.draw(rend);
this.dispatchEvent(new Event('postDraw'))
if (this.revertStyle) { rend.context.restore() }
}
draw(rend) {
switch (this.method) {
case GRAPHIC_METHODS.FILL: rend.context.fill(this.path); break;
case GRAPHIC_METHODS.STROKE: rend.context.stroke(this.path); break;
case GRAPHIC_METHODS.FILL_STROKE: rend.context.fill(this.path); rend.context.stroke(this.path); break;
case GRAPHIC_METHODS.STROKE_FILL: rend.context.stroke(this.path); rend.context.fill(this.path); break;
}
}
}
export const CanvasLayerAtts = Obj.join(StackedClassAtts, {
yUp: false,
unitX: 1,
unitY: 1,
visible: true,
defaultStyle: {
strokeStyle: 'white',
fillStyle: 'black',
lineWidth: 1,
},
origin: new V2(0, 0),
rotationAngle: 0,
})
/** @template {CanvasLayerAtts} A @extends StackedClass<A> */
export class CanvasLayer extends StackedClass {
static defaultAtts = CanvasLayerAtts
get detectReconstruction() {return false}
get visible() {return this._visible} set visible(v) {this._visible = v; this.needsReconstruction = true;}
get yMult() { return this.yUp ? -1 : 1 }
/** @param {A} atts */
constructor(atts) {
super(atts)
/** @type {Boolean} */ this.yUp = this.atts.yUp;
/** @type {V2} */ this.unit = new V2(this.atts.unitX, this.atts.unitY)
/** @type {V2} */ this.unitScale = this.unit.copy.multiplyComp(1, this.yMult)
/** @type {V2} */ this.origin = this.atts.origin ?? new V2(0, 0)
/** @type {Number} */ this.rotationAngle = this.atts.rotationAngle ?? 0
/** @type {ContextStyle} */ this.defaultStyle = this.atts.defaultStyle
this._visible = this.atts.visible ?? true;
}
/** @type {Graphic[]} */ elements = []
/** @param {keyof GRAPHIC_METHODS} n @param {Path2D|GraphicAtts} a @param {GraphicAtts} b @returns {Graphic} */
_newDrawnPath(n, a, b) {
const drawn = Graphic._fromMethod(n, a, b);
this.elements.push(drawn);
return drawn;
}
/** @overload @param {GraphicAtts} atts @returns {Graphic} @overload @param {Path2D} path @param {GraphicAtts} atts @returns {Graphic} */ fill(...a) { return this._newDrawnPath(GRAPHIC_METHODS.FILL, ...a) }
/** @overload @param {GraphicAtts} atts @returns {Graphic} @overload @param {Path2D} path @param {GraphicAtts} atts @returns {Graphic} */ stroke(...a) { return this._newDrawnPath(GRAPHIC_METHODS.STROKE, ...a) }
/** @overload @param {GraphicAtts} atts @returns {Graphic} @overload @param {Path2D} path @param {GraphicAtts} atts @returns {Graphic} */ fillStroke(...a) { return this._newDrawnPath(GRAPHIC_METHODS.FILL_STROKE, ...a) }
/** @overload @param {GraphicAtts} atts @returns {Graphic} @overload @param {Path2D} path @param {GraphicAtts} atts @returns {Graphic} */ strokeFill(...a) { return this._newDrawnPath(GRAPHIC_METHODS.STROKE_FILL, ...a) }
/** @type {function[]} */ preConstruction = []
/** @type {function[]} */ postConstruction = []
reconstruct() {
this.elements.length = 0;
this.preConstruction.forEach(f => f())
this.construct()
this.postConstruction.forEach(f => f())
this.needsReconstruction = false
}
needsReconstruction = false
get construct() { return this._construct } set construct(f) { this._construct = f.bind(this); this.reconstruct() }; _construct() { }
// /** @type {function(Canvas<CanvasAtts>)} */ preprocessOrigin(){}
}
export const CanvasAtts = Obj.join(StackedClassAtts, {
sizing: {
aspectRatio: null,
padding: 0,
resetOnWindowResize: true,
/** @type {Boolean} */ useParent: true,
/** @type {Boolean | QElem} */ useElement: null,
/** @type {function(PannableCanvas)} */ preResize: null,
/** @type {function(PannableCanvas)} */ postResize: null,
},
draw: {
/** @type {function(PannableCanvas)} */ preDraw: null,
/** @type {function(PannableCanvas)} */ postDraw: null,
}
})
/** @template {CanvasAtts} A @extends StackedClass<A> */
export class Canvas extends StackedClass {
static defaultAtts = CanvasAtts
/** @param {HTMLCanvasElement | string} canvas @param {CanvasLayer | CanvasLayer[]} layers @param {CanvasAtts} atts */
constructor(canvas, layers, atts) {
super(atts)
/** @type {HTMLCanvasElement} */ this.canvas = El.q(canvas);
/** @type {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d', { alpha: 1 });
/** @type {Number} */ this.dpr = window.devicePixelRatio || 1;
/** @type {CanvasLayer[]} */ this.layers = List.from(layers instanceof CanvasLayer ? [layers] : layers);
/** @type {V2} */ this.wh = new V2(0, 0);
this.parent = this.atts.sizing.parent ? El.q(this.atts.sizing.parent) : this.canvas.parentElement;
/** @type {HTMLElement} */ this.sizingElement = this.atts.sizing.useElement ? El.q(this.atts.sizing.useElement) : this.atts.sizing.useParent ? this.parent : null;
this.aspectRatio = this.atts.sizing.aspectRatio;
this.sizePadding = this.atts.sizing.padding ?? 0;
if (this.atts.sizing?.getBoudingRect) {this.getBoudingRect = this.atts.sizing.getBoudingRect}
if (this.atts.sizing?.preResize) { this.preResize = this.atts.sizing.preResize}
if (this.atts.sizing?.postResize) {this.postResize = this.atts.sizing.postResize}
if (this.atts.draw?.preDraw) { this.preDraw = this.atts.draw.preDraw }
if (this.atts.draw?.postDraw) { this.postDraw = this.atts.draw.postDraw }
if (this.atts.sizing.resetOnWindowResize) { El.ev(window, 'resize', this.sizeReset.bind(this)) }
this.doPostConstruction(Canvas)
}
static createAndInitialize(canvas, layers, atts) {
const c = new this(canvas, layers, atts);
c.sizeReset();
return c;
}
postConstruct() { this.sizeReset(true) }
draw() {
this.orientZero();
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.reorientFrame()
this.preDraw?.();
this.dispatchEvent(new Event('preDraw'))
this.layers.forEach(layer => {
if (!Parse.func(layer.visible)) {return}
if (layer.needsReconstruction || layer.detectReconstruction) { layer.reconstruct(); }
if (layer.preprocessOrigin) { layer.preprocessOrigin(this) }
this.reorientFrame(layer.origin, layer.rotationAngle)
CanvasPath.applyStyle(this.context, layer.defaultStyle, this)
layer.elements.forEach(/**@param {Graphic} el*/el => Parse.func(el, this)?._draw?.(this))
})
this.postDraw?.();
this.dispatchEvent(new Event('postDraw'))
}
reorientFrame(position, angle) {
if (position || angle) {
if (position) { this.context.setTransform(this.dpr, 0, 0, this.dpr, ...V2.scale(this.dpr, position)) } else { this.orientZero() }
if (angle) { this.context.rotate(angle) }
} else { this.orientZero() }
}
getBoudingRect() {
const c = this.canvas;
const wh = this.sizingElement ? {
width: this.sizingElement.clientWidth,
height: this.sizingElement.clientHeight,
} : c.getBoundingClientRect();
wh.w = wh.width - 2 * this.sizePadding;
wh.h = wh.height - 2 * this.sizePadding;
return wh;
}
sizeReset() {
this.preResize?.();
this.dispatchEvent(new Event('preResize'))
this.dpr = window.devicePixelRatio || 1;
const wh = this.getBoudingRect()
this.width = wh.w ?? wh.width;
this.height = wh.h ?? wh.height;
this.wh = new V2(this.width, this.height)
this.postResize?.();
this.dispatchEvent(new Event('postResize'))
if (!this.notFirstTimeResetting) { this.notFirstTimeResetting = true } else {
this.reconstructLayers();
this.draw();
}
}
_layerIndex = 0;
get activeLayer() { return this.layers[this._layerIndex ?? 0] }
get baseLayer() { return this.layers[0] }
get width() { return this._width; } set width(w) { this._width = w; this.canvas.width = Math.max(1, Math.round(w * this.dpr)); }
get height() { return this._height; } set height(h) { this._height = h; this.canvas.height = Math.max(1, Math.round(h * this.dpr)); }
orientZero() { this.context.setTransform(this.dpr, 0, 0, this.dpr, 0, 0) }
reconstructLayers() {
this.layers.forEach(layer => layer.reconstruct())
this.postReconstruct?.();
}
drawArGrid(x, y) {
let [gu, gv] = x * y ? [x, y] : [0, !x ? (!y ? 1 : y) : x / (this.aspectRatio ?? 1)]
if (!gu) { while (Math.abs((gu = (gv++) * this.aspectRatio) - Math.round(gu)) > 1e-10) { } }
List.make(gu, i => new V2(i * this.width / gu, 0)).forEach(x => this.baseLayer.stroke({ style: { strokeStyle: 'white' } }).path.moveToLineTo([x, x.copy.plus([0, this.height])]))
List.make(--gv, i => new V2(0, i * this.height / gv)).forEach(x => this.baseLayer.stroke({ style: { strokeStyle: 'white' } }).path.moveToLineTo([x.copy.plus([this.width, 0]), x]))
}
}
export const CanvasPanZoomControlsAtts = Obj.join(StackedClassAtts, {
zoom: {
/** @type {Boolean} */ enabled: true,
/** @type {BoundLike} */ limit: null, // ie [0,Number.MAX_SAFE_INTEGER]
},
pan: {
/** @type {Boolean} */ enabled: true,
// /** @type {BBox} */ limit: null, // ie [[-10,10],[-10,10]]
oneFingerPan: false,
},
rotate: {
/** @type {Boolean} */ enabled: true,
// /** @type {BoundLike} */ limit: null, // IE [-2*Math.PI,2*Math.PI]
},
})
export const PannableCanvasAtts = Obj.join(CanvasAtts, {
zoomLevel: 1,
panCenter: [0, 0],
rotationAngle: 0,
bounds: null,
boundaryBorder: null,
controls: CanvasPanZoomControlsAtts,
})
/** @template {PannableCanvasAtts} A @extends Canvas<A> */
export class PannableCanvas extends Canvas {
static defaultAtts = PannableCanvasAtts
/** @param {HTMLCanvasElement | string} canvas @param {CanvasLayer} layer_s @param {PannableCanvasAtts} atts */
constructor(canvas, layer_s, atts) {
super(canvas, layer_s, atts);
/** @type {Number} */ this.rotationAngle = this.atts.rotationAngle ?? 0;
/** @type {Number} */ this.zoomLevel = this.atts.zoomLevel ?? 1;
/** @type {V2} */ this.panCenter = V2.forceToNums(this.atts.panCenter ?? [0, 0])
/**
* Center of the canvas in frame coordinates
* val = [w/2, h/2] ** this is w and h not multiplied by dpr
* @type {V2}
* */ this.canvasCenter = new V2(0, 0);
this.bounds = !this.atts.bounds ? null : (this.atts.bounds instanceof BBox ? this.atts.bounds : BBox.from(this.atts.bounds));
if (this.atts.boundaryBorder) {
this.boundaryBorder = Obj.join(this.defaultStyle, this.atts.boundaryBorder)
this.baseLayer.preConstruction.push(this.makeBoundaryBorder.bind(this))
}
this.doPostConstruction(PannableCanvas)
}
postConstruct() {
super.postConstruct();
this.fitToBounds();
if (this.atts.controls) { this.controls = new CanvasPanZoomControls(this, this.atts.controls) }
}
reorientFrame(position, rotation) {
const vs = this.worldScaleVector;
this.context.setTransform(this.dpr * vs[0], 0, 0, this.dpr*vs[1], ...this.canvasCenter.copy.minus(V2.minus(this.panCenter, position??[0,0]).multiply(vs)).scale(this.dpr))
if (this.rotationAngle || rotation) {this.context.rotate((this.rotationAngle??0) + (rotation??0))}
}
postResize() {this.canvasCenter = new V2(this.width / 2, this.height / 2)}
postReconstruct() {this.fitToBounds()}
/** @type {BBox} */ get bounds() { return this._bounds } set bounds(b) { this._bounds = b; this.updateScaledBounds(); }
updateScaledBounds() { this.scaledBounds = !this._bounds ? null : this._bounds.copy.multiply(this.baseLayer.unit) }
/** @type {V2} */
get worldScaleVector() { return this.baseLayer.unitScale.copy.scale(this.zoomLevel) };
worldToFrame([x, y], pan) { return new V2(x, y).minus(pan ?? this.panCenter).scale(this.zoomLevel).multiply(this.baseLayer.unitScale).plus(this.canvasCenter) }
frameToWorld([x, y], pan) { return new V2(x, y).minus(this.canvasCenter).unscale(this.zoomLevel).divide(this.baseLayer.unitScale).plus(pan ?? this.panCenter) }
fitToBounds(c) {
if (!this.bounds) {return}
let cc = c ? V2.forceToNums(c) : this.panCenter;
const wrld = this.bounds;
const scrn = wrld.copy.multiply(this.baseLayer.unit).scale(this.zoomLevel);
// If world dim in screen units <= screen dim, lock frame center to bounds center. Else, clamp the center so that frame sits within bounds in world coordinates
cc = this.wh.copy.toEachPart((v, i) => scrn[i].span <= v ? wrld[i].mid : wrld[i].padClamp(cc[i], v / (2 * this.zoomLevel * this.baseLayer.unit[i])));
this.panCenter.set(cc)
this.reorientFrame()
}
zoomFit() {
this.zoomLevel = V2.divide(this.wh, Nums.maxs([1e-9, 1e-9], this.scaledBounds.span)).min;
this.panCenter.set(this.bounds?.mid ?? [0, 0]);
this.fitToBounds();
this.draw();
console.log('zooming to fit')
this.dispatchEvent(new Event('zoomToFit'))
}
/**
* Returns an anonymous function to define a constant-px size line independent of the renderer's zoomLevel or the illustration's pixel unit size
* Unit size is determined by the minimum of the illustration's unit vector
*
* Example use for a 2px wide line: Illustration.defaultStyle.lineWidth = PannableCanvas.unscaledLineWidth(2)
*
* @param {Number} px Number of pixels wide the line should be
* @returns {()=>Number}
*/
unscaledLineWidth(px = 1) { return () => px / (this.zoomLevel * this.baseLayer.unit.min) }
makeBoundaryBorder() {
const rect = new CanvasPath();
rect.rect(...this.bounds.min, ...this.bounds.span);
const path = Graphic._fromMethod(GRAPHIC_METHODS.STROKE, rect, { style: this.baseLayer.defaultStyle });
this.baseLayer.elements.push(path)
}
}
/** @template {CanvasPanZoomControlsAtts} A @extends StackedClass<A> */
export class CanvasPanZoomControls extends StackedClass {
static defaultAtts = CanvasPanZoomControlsAtts
/** @type {PannableCanvas} */ frame
/** @param {PannableCanvas} frame @param {A} a */
static parseFeatureAtts(a) { return !a ? {} : (a === true ? { enabled: true } : a) }
constructor(frame, atts) {
atts.zoom = CanvasPanZoomControls.parseFeatureAtts(atts.zoom);
atts.pan = CanvasPanZoomControls.parseFeatureAtts(atts.pan);
atts.rotate = CanvasPanZoomControls.parseFeatureAtts(atts.rotate);
super(atts)
this.zoomLimit = Domain.forceToNums(this.atts.zoom.limit ?? [0, Inf])
this.frame = frame;
this.addEvents();
this.frame.canvas.style.touchAction = 'none';
}
/** @type {ClickXY} */ initClick = null;
addEvents() {
if (this.atts.pan.enabled) {
El.ev(window, 'mouseup', this.mouseUp.bind(this))
El.ev(this.frame.canvas, 'mousedown', this.mouseDown.bind(this))
El.ev(window, 'mousemove', this.mouseMovePan.bind(this))
}
if (this.atts.zoom.enabled) {
El.ev(this.frame.canvas, 'wheel', this.mouseWheelZoom.bind(this), { passive: false })
}
if (this.atts.pan.enabled || this.atts.zoom.enabled) {
El.ev(this.frame.canvas, 'touchstart', this.touchStart.bind(this), { passive: false })
El.ev(window, 'touchmove', this.touchMove.bind(this), { passive: false })
El.ev(window, 'touchend', this.touchEnd.bind(this))
}
El.ev(this.frame.canvas, 'pointerdown', (e)=>{
console.log(e)
// console.log(e)
})
// For ignoring Safari's additional mess:
const appleGestures = ['gesturestart', 'gesturechange', 'gestureend']
appleGestures.forEach(type => {
window.addEventListener(type, (e) => e.preventDefault(), { passive: false });
});
}
mouseUp(e) { this.initClick = null; this.preState = null; }
mouseDown(e) {
this.initClick = ClickXY.within(e, this.frame.canvas)
this.preState = { panCenter: this.frame.panCenter.copy }
}
mouseMovePan(e) {
if (this.initClick) {
const offset = ClickXY.within(e, this.frame.canvas).minus(this.initClick).divide(this.frame.worldScaleVector)//.scale(this.frame.dpr)
this.frame.panCenter.set(offset.to(this.preState.panCenter));
this.frame.fitToBounds();
this.frame.draw();
e.preventDefault();
}
}
mouseWheelZoom(e) {
// Get mouse offset from center and respect world scale (currently unitScale only handles yMult being -1 or 1)
const mouseUnitOffsetFromCenter = ClickXY.within(e, this.frame.canvas).minus(this.frame.canvasCenter).divide(this.frame.baseLayer.unitScale)
const offset_preZoom = mouseUnitOffsetFromCenter.copy.unscale(this.frame.zoomLevel)
this.frame.zoomLevel = this.zoomLimit.clamp(this.frame.zoomLevel * (1 - e.deltaY / 1000));
const offset_postZoom = mouseUnitOffsetFromCenter.copy.unscale(this.frame.zoomLevel)
this.frame.panCenter.plus(offset_preZoom.minus(offset_postZoom));
this.frame.fitToBounds();
this.frame.draw();
e.preventDefault();
}
mouseWheelZoom_old(e) {
// Get mouse xy in world coordinates [W] prior to changing frame zoom level
const mouse = ClickXY.within(e, this.frame.canvas);
const initialWorldFocus = this.frame.frameToWorld(mouse)
// Set zoom level prior to setting the new pan center
this.frame.zoomLevel = this.zoomLimit.clamp(this.frame.zoomLevel * (1 - e.deltaY / 1000));
// Get mouse offset to canvas center in the new relative world coordinates [C]
const worldTranslation = mouse.copy.minus(this.frame.canvasCenter).divide(this.frame.worldScaleVector)
// New pan center is W - C
// (original mouse world coordinates minus mouse offset to canvas center in zoomed world coordinates)
this.frame.panCenter.set(initialWorldFocus.minus(worldTranslation));
this.frame.fitToBounds();
this.frame.draw();
e.preventDefault();
}
preState = {
/** @type {V2} */ panCenter: null,
/** @type {number} */ zoom: 1,
/** @type {V2} */ datumPinchCenter: null
};
touchEnd(e) {
this.touches = null;
this.initPinchGeom = null;
}
touchStart(e) {
const t = Touches.within(e, this.frame.canvas);
if (t.length && t.length<3) {
this.touches = t;
this.preState = {panCenter: this.frame.panCenter.copy}
if (t.length === 2) {
this.initPinchGeom = this.getPinchGeom(t)
this.preState.zoom = this.frame.zoomLevel;
this.preState.datumPinchCenter = this.frame.frameToWorld(this.initPinchGeom.center);
}
}
}
touchMove(e) {
if (this.touches) {
const newTouches = Touches.within(e, this.frame.canvas);
if (!this.initPinchGeom) {
if (!this.atts.pan.oneFingerPan) {return}
const offset = newTouches[0].minus(this.touches[0]).divide(this.frame.worldScaleVector)
this.frame.panCenter.set(offset.to(this.preState.panCenter))
} else {
if (newTouches.length <2 ) {return}
const curPinchGeom = this.getPinchGeom(newTouches)
this.frame.zoomLevel = this.zoomLimit.clamp(this.preState.zoom * curPinchGeom.spread / this.initPinchGeom.spread)
// Get world coordinate, but don't pan by center (use 0,0 as pan instead)
const translatedPinchCenter = this.frame.frameToWorld(curPinchGeom.center, [0,0])
this.frame.panCenter.set(this.preState.datumPinchCenter.copy.minus(translatedPinchCenter))
}
this.frame.fitToBounds();
this.frame.draw();
e.preventDefault();
}
}
getPinchGeom(t) { return {
center: t[0].copy.midPoint(t[1]),
spread: t[0].copy.dist(t[1])
}}
}
Select All
import {El, Obj, Num, Rad, Unit, StackedClassAtts, StackedClass, ID, } from '../tools.js'
import { V2, RGB } from '../array.js';
import { CanvasLayerAtts, CanvasLayer, Canvas } from './canvas.js'
const unitRange = [[0, 0], [1, 1]]
export const StarAtts = Obj.join(StackedClassAtts, {
color: null
})
/** @class @template {StarAtts} A @extends StackedClass<A> */
export class Star extends StackedClass {
static defaultAtts = StarAtts;
/** @type {V2} */ position;
/** @type {V2} */ unitPosition;
/** @type {RGB} */ color;
/** @type {Number} */ size;
/** @type {Starry} */ starry;
/** @param {Starry} starry @param {A} atts */
constructor(starry, atts) {
super(atts)
this.starry = starry;
this.color = Array.isArray(this.atts.color) ? RGB.from(this.atts.color) : this.starry.randColor();
this.size = Num.randomDecimal(0.1, 1.2);
this.unitPosition = V2.randomInRange(...unitRange);
const bgf = this.starry.bgFrame;
this.position = this.unitPosition.createDependent(function () { return V2.multiply(V2.plus(this, [0, 0]), [bgf.canvas.width, bgf.canvas.height]); })
}
/** @param {CanvasRenderingContext2D} ctx */
draw(ctx) {
ctx.fillStyle = this.color.rgb;
ctx.beginPath();
ctx.arc(...this.position, this.size * ((this.highlight && this.starry.bgFrame.withStarHighlighting) ? 2.5 : 1), 0, 2 * Math.PI)
ctx.fill()
}
}
export const StarCursorAtts = Obj.join(StackedClassAtts, {
withCursor: true,
withStarHighlighting: true,
withStarLines: true,
})
/** @template {StarCursorAtts} A @extends StackedClass<A> */
export class StarCursor extends StackedClass {
static defaultAtts = StarCursorAtts;
/** @type {Starry} */ starry
/** @type {Canvas} */ fgFrame;
/** @param {Starry} starry @param {A} atts */
constructor(starry, atts) {
super(atts);
this.starry = starry;
this.position = new V2(0, 0);
this.touch = false;
// If atts === true, then all features are turned on
if (this.atts === true) { this.atts = Obj.clone(StarCursorAtts) }
// Create foreground canvas to draw the cursor and star lines
this.fgFrame = new Canvas(this.starry.main.appendChild(Starry.makeCanvas('fgCanvas')), this.starry)
this.fgFrame.canvas.style.zIndex = 999999;
/**After a touchstart, touchend sequence (without touchmove) a 'mousemove' event is fired.
* This is here in case it is a computer with a touchscreen and both events are necessary
*/
const preventTouch = () => { this.touch = true; }
El.ev(window, 'touchmove', preventTouch)
El.ev(window, 'touchstart', preventTouch)
El.ev(window, 'touchend', preventTouch)
const withCursor = this.atts?.withCursor;
const withStarHighlighting = this.atts?.withStarHighlighting;
const withStarLines = this.atts?.withStarLines;
if (withCursor) {
document.body.style.cursor = 'none';
El.ev(this.fgFrame, 'preDraw', this._drawWithCursor.bind(this))
}
if (withStarHighlighting || withStarLines) {
this.starry.bgFrame.withStarHighlighting = true;
El.ev(window, 'mousemove', e => {
this.cursorOutOfFrame = false;
this.position.set([e.clientX, e.clientY])
this.highlightStars(e);
this.fgFrame.draw();
if (!this.starry.atts.starryBackground.interactive) { this.starry.bgFrame.draw() }
})
El.ev(document, 'mouseout', () => { this.cursorOutOfFrame = true; })
if (!withCursor && withStarLines) { El.ev(this.fgFrame, 'preDraw', this._drawWithLinesOnly.bind(this)) }
}
}
_drawWithLinesOnly() { if (this.touch) { return } this.drawStarLines() }
_drawWithCursor() {
if (this.touch) { return }
const fgContext = this.fgFrame.context;
if (this.atts.withStarLines) { this.drawStarLines() }
fgContext.fillStyle = 'yellow';
fgContext.beginPath();
fgContext.arc(...this.position, 3, 0, Rad.Pi2)
fgContext.fill()
}
highlightStars(e) {
this.starry.stars.forEach(star => {
star.highlight = !this.cursorOutOfFrame && ((star.distanceToCursor = star.position.dist(this.position)) < 50)
})
}
drawStarLines() {
this.fgFrame.context.strokeStyle = 'white';
this.starry.stars.filter(star => star.highlight).sort((a, b) => a.distanceToCursor - b.distanceToCursor).slice(0, 5).forEach(star => {
this.fgFrame.context.lineWidth = star.size * 1.5;
this.fgFrame.context.beginPath()
this.fgFrame.context.moveTo(...star.position);
this.fgFrame.context.lineTo(...this.position);
this.fgFrame.context.stroke();
})
}
}
export const StarryBackgroundAtts = Obj.join(CanvasLayerAtts, {
name: 'starry',
defaultStyle: {
strokeStyle: 'white',
lineWidth: 1,
fillStyle: RGB.random(),
},
starryBackground: {
numStars: 500,
randColor: {
enabled: true,
uniques: true,
minRGB: { r: 0, g: 0, b: 0 },
maxRGB: { r: 255, g: 255, b: 255 }
},
interactive: StarCursorAtts,
animation: { jitter: false, orbit: true, chaos: true, haze: true, chaosMag: 10, flicker: 8 },
starColor: 'white',
},
})
/** @class @template {StarryBackgroundAtts} A @extends CanvasLayer<A> */
export class Starry extends CanvasLayer {
static preventStart = false;
isStarry = true;
static defaultAtts = StarryBackgroundAtts;
/** @type {Star[]} */ stars;
/** @type {HTMLElement} */ main
/** @type {Canvas} */ bgFrame;
preDraw() {
const bgContext = this.bgFrame.context;
if (this.cursor?.atts.withStarHighlighting) { this.cursor.highlightStars() }
this.stars.forEach(star => star.draw(bgContext))
}
postResize() { this.stars?.forEach(star => star.position.dependentUpdate()) }
/** @param {HTMLElement} main @param {A} atts */
constructor(main, atts) {
super(atts);
this.main = main;
const starryAtts = this.atts.starryBackground;
// Create background canvas to draw stars
const newCanv = Starry.makeCanvas('bgCanvas')
document.body.children[0].before(newCanv)
this.bgFrame = new Canvas(newCanv, this, {})
El.ev(this.bgFrame, 'postResize', this.postResize.bind(this))
El.ev(this.bgFrame, 'preDraw', this.preDraw.bind(this))
// Create stars
this.minRandRGB = starryAtts?.randColor?.minRGB ?? { r: 0, g: 0, b: 0 }
this.maxRandRGB = starryAtts?.randColor?.maxRGB ?? { r: 255, g: 255, b: 255 }
const color = !starryAtts.randColor.enabled ? starryAtts.starColor : (starryAtts.randColor.uniques ? null : this.randColor())
this.stars = Array.from({ length: this.atts.starryBackground.numStars }, () => new Star(this, { color }))
// Setup animation
this.animationAtts = starryAtts.animation;
if (Obj.length(this.animationAtts)) {
if (this.animationAtts.chaos) {
this.stars.forEach(star => { star.chaosAngle = Rad.random() })
this.chaosMag = Unit.rad((this.atts.starryBackground.animation.chaosMag ?? 10) / 2);
}
this.orbitAng = Rad.random();
if (this.animationAtts.haze) {
newCanv.before(this.hazeDiv = El.div({
style: {
position: 'absolute', pointerEvents: 'none', zIndex: '-999999',
top: '0px', left: '0px', width: '100%', height: '100%',
}
}));
this.hazeClass = addHazyAnimatedBackground(this.hazeDiv, {
direction: Unit.deg(this.orbitAng) + 90,
span: 2,
speed: 2,
alpha: 0.02
}).cssClass
}
this.maxFlickerVal = this.randColor().unit.scale(5);
this.startAnimation();
}
// Setup cursor interaction
if (starryAtts.interactive) { this.cursor = new StarCursor(this, starryAtts.interactive) }
this.bgFrame.draw()
}
startAnimation() {
this.interval = setInterval(() => {
const m = 0.0001;
const f = this.animationAtts.flicker ?? 0;
const fn = [-f, -f, -f]; const fm = [f, f, f]
this.stars.forEach(star => {
if (star.highlight && (star.distanceToCursor ?? 50) < 15) { return }
const xy = star.unitPosition.copy;
const ms = m * star.size
if (this.animationAtts.flicker) { star.color.plus(RGB.randomInRange(fn, fm)).clamp(this.minRandRGB, this.maxRandRGB) }
if (this.animationAtts.jitter) { xy.plus(V2.radAng(1.5 * ms, Rad.random())) }
if (this.animationAtts.orbit) { xy.plus(V2.radAng(ms, this.orbitAng)) }
if (this.animationAtts.chaos) {
xy.plus(V2.radAng(ms, star.chaosAngle))
star.chaosAngle += Num.randomDecimal(-this.chaosMag, this.chaosMag)
}
xy.clamp(0, 1, true)
star.unitPosition.set(xy)
})
this.bgFrame.draw();
}, 10)
}
/** @returns {RGB} */ get minRandRGB() { return this._minRandRGB } set minRandRGB(rgb) { this._minRandRGB = RGB.fromObject(rgb) }
/** @returns {RGB} */ get maxRandRGB() { return this._maxRandRGB } set maxRandRGB(rgb) { this._maxRandRGB = RGB.fromObject(rgb) }
/** @returns {RGB} */
randColor() { return RGB.randomInRange(this.minRandRGB, this.maxRandRGB); }
static canvasStyle = `
width: 100%; height: 100%;
position: absolute;
left: 0px; top: 0px;
pointer-events: none;
z-index: -999999;
`
static makeCanvas(id) { return El.new('canvas', { id, style: Starry.canvasStyle }) }
}
export const CustomLettering = {
unitWidth: 6,
unitHeight: 8,
letters: {
/** @type {LetterData} */ A: { outside: [[0, 0], [6, 0], [6, 8], [4, 8], [4, 6], [2, 6], [2, 8], [0, 8]], inside: [[[2, 2], [4, 2], [4, 4], [2, 4]]] },
/** @type {LetterData} */ B: { outside: [[0, 0], [5, 0], [6, 1], [6, 3], [5, 4], [6, 5], [6, 7], [5, 8], [0, 8]], inside: [[[2, 1.5], [4, 1.5], [4, 3], [2, 3]], [[2, 5], [4, 5], [4, 6.5], [2, 6.5]]] },
/** @type {LetterData} */ C: { outside: [[0, 0], [6, 0], [6, 2], [2, 2], [2, 6], [6, 6], [6, 8], [0, 8]] },
/** @type {LetterData} */ D: { outside: [[0, 0], [5, 0], [6, 1], [6, 7], [5, 8], [0, 8]], inside: [[[2, 2], [3.5, 2], [4, 2.5], [4, 5.5], [3.5, 6], [2, 6]]] },
/** @type {LetterData} */ E: { outside: [[0, 0], [6, 0], [6, 2], [2, 2], [2, 3], [4, 3], [4, 5], [2, 5], [2, 6], [6, 6], [6, 8], [0, 8]] },
/** @type {LetterData} */ F: { outside: [[0, 0], [6, 0], [6, 2], [2, 2], [2, 3], [4, 3], [4, 5], [2, 5], [2, 8], [0, 8]] },
/** @type {LetterData} */ G: { outside: [[0, 0], [6, 0], [6, 2], [2, 2], [2, 6], [4, 6], [4, 5], [3, 5], [3, 3], [6, 3], [6, 8], [0, 8]] },
/** @type {LetterData} */ H: { outside: [[0, 0], [2, 0], [2, 3], [4, 3], [4, 0], [6, 0], [6, 8], [4, 8], [4, 5], [2, 5], [2, 8], [0, 8]] },
/** @type {LetterData} */ I: { outside: [[0, 0], [6, 0], [6, 2], [4, 2], [4, 6], [6, 6], [6, 8], [0, 8], [0, 6], [2, 6], [2, 2], [0, 2]] },
/** @type {LetterData} */ J: { outside: [[0, 0], [6, 0], [6, 2], [4.5, 2], [4.5, 8], [0, 8], [0, 6], [2.5, 6], [2.5, 2], [0, 2]] },
/** @type {LetterData} */ K: { outside: [[0, 0], [2, 0], [2, 3.5], [4.5, 0], [6, 0], [6, 1], [4 - .25, 3.5 + .25 * 7 / 3], [6, 7], [6, 8], [4, 8], [2, 5], [2, 8], [0, 8]] },
/** @type {LetterData} */ L: { outside: [[0, 0], [2, 0], [2, 6], [6, 6], [6, 8], [0, 8]] },
/** @type {LetterData} */ M: { outside: [[0, 0], [2, 0], [3, 2], [4, 0], [6, 0], [6, 8], [4, 8], [4, 4], [3, 6], [2, 4], [2, 8], [0, 8]] },
/** @type {LetterData} */ N: { outside: [[0, 0], [2, 0], [4, 4], [4, 0], [6, 0], [6, 8], [4, 8], [2, 4], [2, 8], [0, 8]] },
/** @type {LetterData} */ O: { outside: [[0, 0], [6, 0], [6, 8], [0, 8]], inside: [[[2, 2], [4, 2], [4, 6], [2, 6]]] },
/** @type {LetterData} */ P: { outside: [[0, 0], [6, 0], [6, 6], [2, 6], [2, 8], [0, 8]], inside: [[[2, 2], [4, 2], [4, 4], [2, 4]]] },
/** @type {LetterData} */ Q: { outside: [[0, 0], [6, 0], [6, 5], [5, 6], [6, 7], [5, 8], [4, 7], [3, 8], [0, 8]], inside: [[[2, 2], [4, 2], [4, 4], [2, 6]]] },
/** @type {LetterData} */ R: { outside: [[0, 0], [6, 0], [6, 6], [5, 6], [6, 8], [4, 8], [3, 6], [2, 6], [2, 8], [0, 8]], inside: [[[2, 2], [4, 2], [4, 4], [2, 4]]] },
/** @type {LetterData} */ S: { outside: [[0, 0], [6, 0], [6, 2], [2, 2], [2, 3], [6, 3], [6, 8], [0, 8], [0, 6], [4, 6], [4, 5], [0, 5]] },
/** @type {LetterData} */ T: { outside: [[0, 0], [6, 0], [6, 2], [4, 2], [4, 8], [2, 8], [2, 2], [0, 2]] },
/** @type {LetterData} */ U: { outside: [[0, 0], [2, 0], [2, 6], [4, 6], [4, 0], [6, 0], [6, 8], [0, 8]] },
/** @type {LetterData} */ V: { outside: [[0, 0], [2, 0], [3, 5], [4, 0], [6, 0], [4, 8], [2, 8]] },
/** @type {LetterData} */ W: { outside: [[0, 0], [2, 0], [2, 6], [3, 5], [4, 6], [4, 0], [6, 0], [6, 8], [4, 8], [3, 7], [2, 8], [0, 8]] },
/** @type {LetterData} */ X: { outside: [[0, 0], [2, 0], [3, 2], [4, 0], [6, 0], [4, 4], [6, 8], [4, 8], [3, 6], [2, 8], [0, 8], [2, 4]] },
/** @type {LetterData} */ Y: { outside: [[0, 0], [2, 0], [3, 2], [4, 0], [6, 0], [4, 4], [4, 8], [2, 8], [2, 4]] },
/** @type {LetterData} */ Z: { outside: [[0, 0], [6, 0], [6, 2], [3, 6], [6, 6], [6, 8], [0, 8], [0, 6], [3, 2], [0, 2]] },
}
}
/**
*
* @param {HTMLElement} element
* @param {deg} direction
* @param {s} duration
* @param {RGB[] | string[]} colorList
* @param {number[]} colorStops Must match length of colorList, or default settings will apply
*/
export function addColorRollAnimation(element, direction, duration, colorList, colorStops, options) {
const opts = Obj.join({ span: 1, cssClass: false }, options)
// Register a unique property in CSS
if (!addColorRollAnimation.propertiesRegistered) { addColorRollAnimation.propertiesRegistered = {} }
const uniquePropName = 'COLOR_ROLL_ANIMATION_PROPERTY_' + ID.generate(addColorRollAnimation.propertiesRegistered, 3)
// Pull values together to form the color and stop related portions of the linear-gradient colorstring
const stopVals = (colorStops && colorStops.length === colorList.length) ? colorStops : colorList.map((_, i) => i / colorList.length)
const primaryData = colorList.map((c, i) => { return { stop: stopVals[i], color: c instanceof RGB ? c.hexstring : c } })
const prefixData = primaryData.map(p => { return { stop: p.stop - opts.span, color: p.color } })
const finalData = Obj.join(primaryData[0], { stop: opts.span })
const gradient_colorstrings = [...prefixData, ...primaryData, finalData].map(d => `${d.color} calc(var(--${uniquePropName}) + ${(d.stop * 100).toFixed(0)}%)`).join(',')
// const dir = Num.clampAround(direction, -360, 360)
const gradient_string = `linear-gradient(${direction}deg,${gradient_colorstrings})`;
const endStopPct = `${(finalData.stop * 100).toFixed(0)}%`
let cssClass;
if (opts.cssClass) {
cssClass = 'class_' + uniquePropName;
const newStyle = El.new('style')
const animationName = 'anim_' + uniquePropName;
newStyle.textContent = `
@property --${uniquePropName} {
syntax: '<percentage>';
initial-value: 0%;
inherits: true;
}
@keyframes ${animationName} {
0% {--${uniquePropName}: 0%}
100% {--${uniquePropName}: ${endStopPct}}
}
.${cssClass} {
background: ${gradient_string};
animation-name: ${animationName};
animation-duration: ${duration}s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
`
document.head.appendChild(newStyle)
}
// Style the element
const el = El.q(element);
if (el) {
if (cssClass) {
el.classList.add(cssClass)
} else {
window.CSS.registerProperty({
name: `--${uniquePropName}`,
syntax: '<percentage>',
inherits: true,
initialValue: '0%',
});
el.style.background = gradient_string;
// Kickoff the element animation
el.animate([
{ [`--${uniquePropName}`]: '0%' },
{ [`--${uniquePropName}`]: endStopPct },
], {
duration: duration * 1000, // 2 seconds
easing: 'linear',
iterations: Infinity,
})
}
}
return {
element: el,
propertyName: uniquePropName,
cssClass: cssClass
}
}
addColorRollAnimation.rainbowSet = {
colors: [
RGB.list.red,
RGB.list.yellow,
RGB.list.green,
RGB.list.cyan,
RGB.list.blue,
RGB.list.magenta
],
stops: [0, .167, .333, .5, .667, .833]
}
export function addHazyAnimatedBackground(element, options) {
const { direction, span, speed, alpha, cssClass } = Obj.join({
cssClass: false,
direction: 0,
span: 2,
speed: 1,
alpha: 0.03
}, options)
const hazeStops = addColorRollAnimation.rainbowSet.stops.map(v => v * span)
const hazeColors = addColorRollAnimation.rainbowSet.colors.map(c => RGB.parse(c).toRGBA(alpha))
return addColorRollAnimation(element, direction, 60 / speed, hazeColors, hazeStops, { cssClass, span })
}