UNPKG

littlejsengine

Version:

LittleJS - Tiny and Fast HTML5 Game Engine

268 lines (237 loc) 9.7 kB
/** * LittleJS Utility Classes and Functions * - Timer - tracks time automatically with support for pause and real-time modes * - Time formatting helper * - JSON file fetching * - File saving (text, canvas, data URLs) * - Native share dialog support * - Local storage save data management * - Gradient noise (1D and 2D) * @namespace Utilities */ 'use strict'; /** * Timer object tracks how long has passed since it was set * @memberof Engine * @example * let a = new Timer; // creates a timer that is not set * a.set(3); // sets the timer to 3 seconds * * let b = new Timer(1); // creates a timer with 1 second left * b.unset(); // unset the timer */ class Timer { /** Create a timer object set time passed in * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset) * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */ constructor(timeLeft, useRealTime=false) { ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft); this.useRealTime = useRealTime; const globalTime = this.getGlobalTime(); this.time = timeLeft === undefined ? undefined : globalTime + timeLeft; this.setTime = timeLeft; } /** Set the timer with seconds passed in * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */ set(timeLeft=0) { ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft); const globalTime = this.getGlobalTime(); this.time = globalTime + timeLeft; this.setTime = timeLeft; } /** Set if the timer should keep running even when the game is paused * @param {boolean} [useRealTime] */ setUseRealTime(useRealTime=true) { ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.'); this.useRealTime = useRealTime; } /** Unset the timer */ unset() { this.time = undefined; } /** Returns true if set * @return {boolean} */ isSet() { return this.time !== undefined; } /** Returns true if set and has not elapsed * @return {boolean} */ active() { return this.getGlobalTime() < this.time; } /** Returns true if set and elapsed * @return {boolean} */ elapsed() { return this.getGlobalTime() >= this.time; } /** Get how long since elapsed, returns 0 if not set (returns negative if currently active) * @return {number} */ get() { return this.isSet()? this.getGlobalTime() - this.time : 0; } /** Get percentage elapsed based on time it was set to, returns 0 if not set. * Zero-duration timers report 1 (already elapsed). * @return {number} */ getPercent() { if (!this.isSet()) return 0; if (!this.setTime) return 1; return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime); } /** Get the time this timer was set to, returns 0 if not set * @return {number} */ getSetTime() { return this.isSet() ? this.setTime : 0; } /** Get the current global time this timer is based on * @return {number} */ getGlobalTime() { return this.useRealTime ? timeReal : time; } /** Returns this timer expressed as a string * @return {string} */ toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; } /** Get how long since elapsed, returns 0 if not set (returns negative if currently active) * @return {number} */ valueOf() { return this.get(); } } /////////////////////////////////////////////////////////////////////////////// /** Formats seconds to mm:ss style for display purposes * @param {number} t - time in seconds * @return {string} * @memberof Utilities */ function formatTime(t) { const signStr = t < 0 ? '-' : ''; t = abs(t)|0; return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60; } /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await! * @param {string} url - URL of JSON file * @return {Promise<object>} * @memberof Utilities */ async function fetchJSON(url) { const response = await fetch(url); if (!response.ok) throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`); return response.json(); } /////////////////////////////////////////////////////////////////////////////// /** Save a text file to disk * @param {string} text * @param {string} [filename] * @param {string} [type] * @memberof Utilities */ function saveText(text, filename='text', type='text/plain') { saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); } /** Save a canvas to disk * @param {HTMLCanvasElement|OffscreenCanvas} canvas * @param {string} [filename] * @param {string} [type] * @memberof Utilities */ function saveCanvas(canvas, filename='screenshot', type='image/png') { if (canvas instanceof OffscreenCanvas) { // copy to temporary canvas and save const saveCanvas = document.createElement('canvas'); saveCanvas.width = canvas.width; saveCanvas.height = canvas.height; saveCanvas.getContext('2d').drawImage(canvas, 0, 0); saveDataURL(saveCanvas.toDataURL(type), filename); } else saveDataURL(canvas.toDataURL(type), filename); } /** Save a data url to disk * @param {string} url * @param {string} [filename] * @param {number} [revokeTime] - how long before revoking the url * @memberof Utilities */ function saveDataURL(url, filename='download', revokeTime) { ASSERT(isStringLike(url), 'saveDataURL requires url string'); ASSERT(isStringLike(filename), 'saveDataURL requires filename string'); // create link for saving screenshots const link = document.createElement('a'); link.download = filename; link.href = url; link.click(); if (revokeTime !== undefined) setTimeout(()=> URL.revokeObjectURL(url), revokeTime); } /** Share content using the native share dialog if available * @param {string} title - title of the share * @param {string} url - url to share * @param {Function} [callback] - Called when share is complete * @memberof Utilities */ function shareURL(title, url, callback) { ASSERT(isStringLike(title), 'shareURL requires title string'); ASSERT(isStringLike(url), 'shareURL requires url string'); navigator.share?.({title, url}).then(()=>callback?.()); } /////////////////////////////////////////////////////////////////////////////// /** Read save data from local storage * @param {string} saveName - unique name for the game/save * @param {Object} [defaultSaveData] - default values for save * @return {Object} * @memberof Utilities */ function readSaveData(saveName, defaultSaveData) { ASSERT(isStringLike(saveName), 'loadData requires saveName string'); // tolerate localStorage being unavailable (iOS private mode, sandboxed // iframes) and corrupt JSON in stored data let loadedData = {}; try { const data = localStorage[saveName]; if (data) { try { loadedData = JSON.parse(data); } catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); } } } catch { LOG('readSaveData: localStorage unavailable — using defaults'); } return { ...defaultSaveData, ...loadedData }; } /** Write save data to local storage * @param {string} saveName - unique name for the game/save * @param {Object} saveData - object containing data to be saved * @memberof Utilities */ function writeSaveData(saveName, saveData) { ASSERT(isStringLike(saveName), 'saveData requires saveName string'); // tolerate localStorage being unavailable or quota exceeded try { localStorage[saveName] = JSON.stringify(saveData); } catch { LOG('writeSaveData: failed to write', saveName); } } /////////////////////////////////////////////////////////////////////////////// // Deterministic well-distributed hash of an integer lattice index to [0, 1). // Murmur3 finalizer — adjacent integers produce uncorrelated outputs. function noiseHash(i) { let h = (i | 0) ^ 0x9e3779b9; h = Math.imul(h ^ (h >>> 16), 0x85ebca6b); h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); h ^= h >>> 16; return (h >>> 0) / 2**32; } /** 1D gradient noise — returns a smooth value in [0, 1] for any real x. * Integer inputs land on deterministic lattice values; non-integer inputs * are interpolated with smoothStep for C1 continuity. * @param {number} x * @return {number} * @memberof Utilities */ function noise1D(x) { const i = floor(x); return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i)); } /** 2D gradient noise — returns a smooth value in [0, 1] for any real (x, y). * @param {number} x * @param {number} y * @return {number} * @memberof Utilities */ function noise2D(x, y) { const ix = floor(x), iy = floor(y); const fx = smoothStep(x - ix), fy = smoothStep(y - iy); // large prime decorrelates neighboring rows const h = (a, b) => noiseHash(a + b * 374761393); return lerp( lerp(h(ix, iy ), h(ix + 1, iy ), fx), lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx), fy); }