UNPKG

lab13-sdk

Version:

JS13K tools and game infrastructure SDK.

717 lines (690 loc) 32.5 kB
import { PartySocket } from "https://js13kgames.com/2025/online/partysocket.js"; //#region src/audio/player.ts const noteToFrequency = (note, baseNote = "i", baseFreq = 440) => note === "0" ? 0 : baseFreq * Math.pow(2, (-baseNote.charCodeAt(0) + note.charCodeAt(0)) / 12); const playSingleNote = (audioContext, frequency, start, options) => { const { noteLengthMs = 200, volume = .1 } = options || {}; if (frequency === 0) return; const ctx = audioContext; const oscillator = ctx.createOscillator(); const gainNode = ctx.createGain(); const envelope = ctx.createGain(); oscillator.connect(envelope); envelope.connect(gainNode); gainNode.connect(ctx.destination); oscillator.frequency.setValueAtTime(frequency, start); oscillator.type = "sine"; gainNode.gain.setValueAtTime(volume, start); envelope.gain.setValueAtTime(.5, start); envelope.gain.setTargetAtTime(.001, start + .1, .05); const noteLengthSeconds = noteLengthMs / 1e3; oscillator.start(start); oscillator.stop(start + noteLengthSeconds - .01); oscillator.onended = () => { oscillator.disconnect(); envelope.disconnect(); gainNode.disconnect(); }; }; const createSongPlayer = (song) => { let audioContext; let loopInterval; let currentTime = 0; const timeoutIds = /* @__PURE__ */ new Set(); const play = (options) => { const { noteLengthMs = 200, loop = true, volume = .1, baseNote = "i", onNotesPlayed } = { ...options }; const noteLengthSeconds = noteLengthMs / 1e3; const maxCols = Math.max(...song.map((part) => part.length)); const totalDurationSeconds = maxCols * noteLengthSeconds; if (!audioContext || audioContext.state === "closed") audioContext = new AudioContext(); currentTime = audioContext.currentTime; const playOnce = () => { console.log("Playing song", song, "starting at", currentTime); for (let col = 0; col < maxCols; col++) { const startTime = currentTime + col * noteLengthSeconds; for (const part of song) { const note = part[col]; if (!note) continue; const frequency = noteToFrequency(note, baseNote); playSingleNote(audioContext, frequency, startTime, { noteLengthMs, volume }); } if (onNotesPlayed) { const timeoutId = setTimeout(() => onNotesPlayed(col), col * noteLengthMs); timeoutIds.add(timeoutId); } } currentTime += totalDurationSeconds; }; playOnce(); if (loop) loopInterval = setInterval(playOnce, totalDurationSeconds * 1e3); }; const stop = () => { timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId)); timeoutIds.clear(); if (loopInterval) clearInterval(loopInterval); if (audioContext && audioContext.state !== "closed") audioContext.close(); }; return { play, stop }; }; //#endregion //#region src/demo/index.ts const useDemo = (options) => { const { iframeWidth = 640, iframeHeight = 480, buttonText = "+ Add", buttonStyle = {}, containerStyle = {}, iframeStyle = {} } = options || {}; const isDemoMode = location.search.includes("demo"); if (!isDemoMode) return { isDemoMode: false }; const containerStyles = { display: "flex", flexWrap: "wrap", gap: "10px", padding: "10px", ...containerStyle }; const buttonStyles = { position: "fixed", top: "20px", right: "20px", width: "80px", height: "40px", background: "#4caf50", color: "white", border: "none", borderRadius: "8px", fontSize: "16px", cursor: "pointer", zIndex: "1000", ...buttonStyle }; const sliderStyles = { position: "fixed", top: "70px", right: "20px", width: "80px", height: "40px", background: "#2196f3", color: "white", border: "none", borderRadius: "8px", fontSize: "12px", cursor: "pointer", zIndex: "1000", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column" }; const iframeStyles = { border: "2px solid #333", borderRadius: "8px", ...iframeStyle }; const container = document.createElement("div"); container.id = "demo-container"; Object.assign(container.style, containerStyles); const addButton = document.createElement("button"); addButton.className = "add-btn"; addButton.textContent = buttonText; Object.assign(addButton.style, buttonStyles); const sizeSlider = document.createElement("div"); sizeSlider.className = "size-slider"; const savedSize = localStorage.getItem("demo-iframe-size"); const initialSize = savedSize ? parseInt(savedSize) : iframeWidth; sizeSlider.innerHTML = ` <input type="range" min="200" max="800" value="${initialSize}" style="width: 60px; margin: 2px;"> <span style="font-size: 10px;">Size</span> `; Object.assign(sizeSlider.style, sliderStyles); const canvas = document.getElementById("c"); if (canvas) canvas.style.display = "none"; document.body.style.overflow = "auto"; document.body.style.height = "auto"; document.documentElement.style.overflow = "auto"; document.documentElement.style.height = "auto"; document.body.appendChild(container); document.body.appendChild(addButton); document.body.appendChild(sizeSlider); const getCurrentSize = () => { const slider$1 = sizeSlider.querySelector("input"); return parseInt(slider$1.value); }; const saveSize = (size) => { localStorage.setItem("demo-iframe-size", size.toString()); }; const resizeAllIframes = () => { const size = getCurrentSize(); const aspectRatio = iframeHeight / iframeWidth; const newHeight = Math.round(size * aspectRatio); const iframes = container.querySelectorAll(".demo-iframe"); iframes.forEach((iframe) => { iframe.width = size.toString(); iframe.height = newHeight.toString(); }); }; const addIframe = () => { const iframeWrapper = document.createElement("div"); iframeWrapper.style.position = "relative"; iframeWrapper.style.display = "inline-block"; const iframe = document.createElement("iframe"); iframe.src = location.pathname; const size = getCurrentSize(); const aspectRatio = iframeHeight / iframeWidth; const height = Math.round(size * aspectRatio); iframe.width = size.toString(); iframe.height = height.toString(); iframe.className = "demo-iframe"; Object.assign(iframe.style, iframeStyles); const trashIcon = document.createElement("div"); trashIcon.innerHTML = "🗑️"; trashIcon.style.cssText = ` position: absolute; top: 5px; right: 5px; background: rgba(255, 0, 0, 0.8); color: white; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 12px; opacity: 0; transition: opacity 0.2s; z-index: 10; `; iframeWrapper.addEventListener("mouseenter", () => { iframe.style.border = "3px solid #4caf50"; iframe.style.boxShadow = "0 0 10px rgba(76, 175, 80, 0.5)"; trashIcon.style.opacity = "1"; }); iframeWrapper.addEventListener("mouseleave", () => { iframe.style.border = "3px solid #000"; iframe.style.boxShadow = "0 0 10px rgba(0,0,0, 0.5)"; trashIcon.style.opacity = "0"; }); trashIcon.addEventListener("click", (e) => { e.stopPropagation(); iframeWrapper.remove(); const remainingCount = container.children.length; localStorage.setItem("demo-iframe-count", remainingCount.toString()); }); iframeWrapper.appendChild(iframe); iframeWrapper.appendChild(trashIcon); container.appendChild(iframeWrapper); }; addButton.addEventListener("click", () => { addIframe(); const currentCount = container.children.length; localStorage.setItem("demo-iframe-count", currentCount.toString()); }); const slider = sizeSlider.querySelector("input"); slider.addEventListener("input", () => { const size = getCurrentSize(); resizeAllIframes(); saveSize(size); }); const savedCount = localStorage.getItem("demo-iframe-count"); const initialCount = savedCount ? parseInt(savedCount) : 1; for (let i = 0; i < initialCount; i++) addIframe(); return { isDemoMode: true, addIframe, container, addButton, sizeSlider, resizeAllIframes }; }; //#endregion //#region src/online/message.ts const sendMessage = (message, socket = window.socket) => { socket.send(message); }; const sendMessageToClient = (clientId, message, socket = window.socket) => { sendMessage(`@${clientId}|${message}`, socket); }; //#endregion //#region src/online/command.ts const onCommandMessage = (command, callback, socket = window.socket) => { socket.addEventListener("message", (event) => { const data = event.data.toString(); if (data.startsWith(command)) callback(data.slice(command.length)); }); }; const sendCommandMessageToClient = (clientId, command, data, socket = window.socket) => { sendMessageToClient(clientId, `${command}${data}`, socket); }; const sendCommandMessageToAll = (command, data, socket = window.socket) => { sendMessage(`${command}${data}`, socket); }; const labCommand = (command) => `_${command}`; //#endregion //#region src/online/core-events.ts const onClientJoined = (callback, socket = window.socket) => { onCommandMessage(`+`, callback, socket); }; const onClientLeft = (callback, socket = window.socket) => { onCommandMessage(`-`, callback, socket); }; const onClientIdUpdated = (callback, socket = window.socket) => { onCommandMessage(`@`, callback, socket); }; //#endregion //#region src/online/deepCopy.ts const deepCopy = (obj) => JSON.parse(JSON.stringify(obj)); //#endregion //#region src/online/generateUUID.ts const generateUUID = () => Math.random().toString(36).substring(2, 15); //#endregion //#region src/online/myId.ts const useMyId = (options) => { const { socket = window.socket } = options || {}; let myId = socket.id; onClientIdUpdated((clientId) => { myId = clientId; }, socket); return { getMyId: () => myId }; }; const onMyIdUpdated = onClientIdUpdated; //#endregion //#region src/online/presence.ts const sendIdentToClient = (recipientClientId, clientId, socket = window.socket) => { sendCommandMessageToClient(recipientClientId, labCommand(`i`), clientId, socket); }; const onIdentReceived = (callback, socket = window.socket) => { onCommandMessage(labCommand(`i`), callback, socket); }; const usePresence = (socket = window.socket) => { onClientJoined((clientId) => { console.log(`[${myClientId}] client joined`, clientId); if (myClientId) sendIdentToClient(clientId, myClientId, socket); }, socket); let myClientId = null; onClientIdUpdated((clientId) => { console.log(`[${myClientId}] player id updated`, clientId); myClientId = clientId; }, socket); }; //#endregion //#region src/online/socket.ts const onOpen = (callback, socket = window.socket) => { socket.addEventListener("open", callback); }; const onClose = (callback, socket = window.socket) => { socket.addEventListener("close", callback); }; const onError = (callback, socket = window.socket) => { socket.addEventListener("error", callback); }; //#endregion //#region src/online/state/merge.ts const PRIVATE_KEY_PREFIX = "_"; const PRIVATE_KEY_COLLECTION_KEY = `${PRIVATE_KEY_PREFIX}keys`; const ENTITY_COLLECTION_PREFIX = "@"; const PLAYER_ENTITY_COLLECTION_KEY = `${ENTITY_COLLECTION_PREFIX}players`; const tombstones = /* @__PURE__ */ new Set(); function mergeDeep(target, delta, deleteNulls = false, parentKey) { const changes = {}; const deleteKey = (key) => { if (deleteNulls) delete target[key]; else target[key] = null; changes[key] = null; if (parentKey?.startsWith(ENTITY_COLLECTION_PREFIX)) tombstones.add(key); }; const isTombstoned = (key) => parentKey?.startsWith(ENTITY_COLLECTION_PREFIX) && tombstones.has(key); Object.keys(delta).forEach((key) => { if (isTombstoned(key)) { deleteKey(key); return; } const deltaValue = delta[key]; const targetValue = target[key]; if (deltaValue === null) deleteKey(key); else if (deltaValue && typeof deltaValue === "object" && !Array.isArray(deltaValue) && targetValue && typeof targetValue === "object" && !Array.isArray(targetValue)) { const nestedChanges = mergeDeep(targetValue, deltaValue, deleteNulls, key); if (Object.keys(nestedChanges).length > 0) changes[key] = nestedChanges; } else if (deltaValue !== targetValue) { target[key] = deltaValue; changes[key] = deltaValue; } }); return changes; } //#endregion //#region src/online/state/copier.ts const createMyStateCopier = (myIdGetter) => (currentState, newState) => { const myId = myIdGetter(); if (!myId) return newState; return { ...newState, [PLAYER_ENTITY_COLLECTION_KEY]: { ...newState[PLAYER_ENTITY_COLLECTION_KEY], [myId]: currentState[PLAYER_ENTITY_COLLECTION_KEY]?.[myId] || {} } }; }; //#endregion //#region src/online/state/normalize.ts const round = (n, precision = 0) => precision === 0 ? Math.round(n) : Math.round(n * Math.pow(10, precision)) / Math.pow(10, precision); const normalizeRad = (n) => { const twoPi = Math.PI * 2; n = n % twoPi; if (n < 0) n += twoPi; return n; }; const normalizeDeg = (n) => { n = n % 360; if (n < 0) n += 360; return n; }; const createKeyNormalizer = (normalizeFn) => { const walk = (target, parentKeys = []) => { if (typeof target !== "object" || target === null) return target; for (const key in target) { if (!Object.prototype.hasOwnProperty.call(target, key)) continue; const value = target[key]; if (typeof value === "object" && value !== null) walk(value, [...parentKeys, key]); else target[key] = normalizeFn(target, key, value, parentKeys); } return target; }; return walk; }; const createPositionNormalizer = (precision = 0) => createKeyNormalizer((obj, key, value) => [ "x", "y", "z" ].includes(key) ? round(value, precision) : value); const createVelocityNormalizer = (precision = 2) => createKeyNormalizer((obj, key, value) => [ "vx", "vy", "vz" ].includes(key) ? round(value, precision) : value); const createRotationNormalizer = (precision = 2, useDegrees = false) => createKeyNormalizer((obj, key, value) => { if ([ "rx", "ry", "rz" ].includes(key)) { const round$1 = (n) => precision === 0 ? Math.round(n) : Math.round(n * Math.pow(10, precision)) / Math.pow(10, precision); return round$1(useDegrees ? normalizeDeg(value) : normalizeRad(value)); } return value; }); //#endregion //#region src/online/state/onStateDeltaMessage.ts const onStateDeltaMessage = (callback, socket = window.socket) => { const wrappedCallback = (data) => { const delta = JSON.parse(data); callback(delta); }; onCommandMessage(labCommand(`d`), wrappedCallback, socket); }; //#endregion //#region src/online/state/onStateMessage.ts const onStateMessage = (callback, socket = window.socket) => { const wrappedCallback = (data) => { const state = JSON.parse(data); callback(state); }; onCommandMessage(labCommand(`s`), wrappedCallback, socket); }; //#endregion //#region src/online/state/debug.ts const mkdbg = (id, debug) => (...args) => { if (!debug) return; console.log(`[${id}]`, ...args); }; //#endregion //#region src/online/state/useEasyState.ts const useEasyState = (options) => { const { positionPrecision = 0, rotationPrecision = 2, rotationUnits = "d", debug = false } = options || {}; const { getMyId } = useMyId(); const dbg = mkdbg(getMyId(), debug); const myStateCopier = createMyStateCopier(getMyId); const playerStatesReported = /* @__PURE__ */ new Set(); const positionNormalizer = createPositionNormalizer(positionPrecision); const rotationNormalizer = createRotationNormalizer(rotationPrecision, rotationUnits === "d"); const addMissingPlayers = (state) => { for (const id in state[PLAYER_ENTITY_COLLECTION_KEY]) { if (id === getMyId()) continue; if (playerStatesReported.has(id)) continue; playerStatesReported.add(id); dbg(`Adding missing player`, id); const player = state[PLAYER_ENTITY_COLLECTION_KEY][id]; options?.onPlayerStateAvailable?.(id, player); } }; const stateManager = useState({ onBeforeSendDelta: (delta) => { return positionNormalizer(rotationNormalizer(delta)); }, onBeforeSendState: (state) => { return positionNormalizer(rotationNormalizer(state)); }, onStateReceived: (currentState, newState) => { return myStateCopier(currentState, newState); }, onAfterStateUpdated: (state, delta) => { dbg(`onAfterStateUpdated`, JSON.stringify(state, null, 2), JSON.stringify(delta, null, 2)); addMissingPlayers(state); }, ...options }); onClientLeft((id) => { console.log(`onClientLeft`, id); stateManager.updatePlayerState(id, null); }); return stateManager; }; //#endregion //#region src/online/state/filterPrivateKeys.ts const filterPrivateKeys = (obj) => { if (typeof obj !== "object" || obj === null) return obj; const filtered = { ...obj }; Object.keys(filtered).forEach((key) => { if (key.startsWith(PRIVATE_KEY_PREFIX)) delete filtered[key]; else if (typeof filtered[key] === "object" && filtered[key] !== null) filtered[key] = filterPrivateKeys(filtered[key]); }); return filtered; }; //#endregion //#region src/online/state/useState.ts const useState = (options) => { const { onBeforeSendState = (state) => state, onStateReceived = (currentState, newState) => newState, onDeltaReceived = (delta) => delta, onBeforeSendDelta = (delta) => delta, onAfterStateUpdated = (state, changes) => {}, socket = window.socket, deltaThrottleMs = 50, debug = false } = options || {}; let localState = { [PLAYER_ENTITY_COLLECTION_KEY]: {} }; onClientJoined((clientId) => { dbg("Client joined:", clientId); sendStateToClient(clientId); }, socket); onClientLeft((clientId) => { dbg("Client left:", clientId); updatePlayerState(clientId, null); }, socket); const sendStateToClient = (clientId) => { const stateCopy = onBeforeSendState(deepCopy(localState)); const filteredState = filterPrivateKeys(stateCopy); dbg("Sending state to client", clientId, JSON.stringify(filteredState, null, 2)); sendCommandMessageToClient(clientId, labCommand(`s`), JSON.stringify(filteredState), socket); }; let pendingDeltaTimeout = null; let pendingDelta = {}; const maybeSendPendingDeltaToAll = (socket$1 = window.socket) => { if (Object.keys(pendingDelta).length === 0 || pendingDeltaTimeout) return; const deltaCopy = onBeforeSendDelta(deepCopy(pendingDelta)); const filteredDelta = filterPrivateKeys(deltaCopy); dbg("Sending delta to all", JSON.stringify(filteredDelta, null, 2)); sendCommandMessageToAll(labCommand(`d`), JSON.stringify(filteredDelta), socket$1); pendingDelta = {}; pendingDeltaTimeout = setTimeout(() => { pendingDeltaTimeout = null; dbg(`in timeout, pending delta is`, JSON.stringify(pendingDelta, null, 2)); maybeSendPendingDeltaToAll(socket$1); }, deltaThrottleMs); }; const { getMyId } = useMyId({ socket }); const dbg = mkdbg(getMyId(), debug); onStateMessage((newState) => { dbg("Received new state from peer", JSON.stringify(newState, null, 2)); const normalizedState = onStateReceived(localState, newState); dbg("Normalized state from peer", JSON.stringify(normalizedState, null, 2)); localState = normalizedState; onAfterStateUpdated(localState, normalizedState); }, socket); onStateDeltaMessage((delta) => { const normalizedDelta = onDeltaReceived(delta); dbg("Received delta from client", JSON.stringify(normalizedDelta, null, 2)); updateState(normalizedDelta, false); }, socket); const getState = (copy = false) => copy ? deepCopy(localState) : localState; const getPlayerStates = (copy = false) => { return copy ? deepCopy(localState[PLAYER_ENTITY_COLLECTION_KEY]) : localState[PLAYER_ENTITY_COLLECTION_KEY]; }; const getPlayerState = (clientId, copy = false) => { const playerState = localState[PLAYER_ENTITY_COLLECTION_KEY]?.[clientId] || {}; return copy ? deepCopy(playerState) : playerState; }; const getMyState = (copy = false) => { const myId = getMyId(); return getPlayerState(myId, copy); }; const updatePlayerState = (clientId, delta) => { updateState({ [PLAYER_ENTITY_COLLECTION_KEY]: { [clientId]: delta } }); }; const updateMyState = (delta) => { const myId = getMyId(); updatePlayerState(myId, delta); }; const updateState = (delta, send = true) => { const changes = mergeDeep(localState, delta, true); if (Object.keys(changes).length > 0) { dbg("Updating state", JSON.stringify({ localState, delta, changes }, null, 2)); onAfterStateUpdated(localState, changes); } if (!send) return; mergeDeep(pendingDelta, changes); if (Object.keys(pendingDelta).length > 0) dbg("Pending delta", JSON.stringify(pendingDelta, null, 2)); maybeSendPendingDeltaToAll(socket); }; return { getState, getMyState, getPlayerStates, getPlayerState, updateMyState, updatePlayerState, updateState }; }; //#endregion //#region src/online/head.ts const useHead = () => { window.PartySocket = PartySocket; }; //#endregion //#region src/online/useOnline.ts const useOnline = (room, options) => { useHead(); const roomParts = room.split("/"); const { host = "relay.js13kgames.com", global = true } = options || {}; const socket = new window.PartySocket({ host, party: roomParts[0], room: roomParts.join("/"), id: generateUUID() }); if (global) window.socket = socket; return socket; }; //#endregion //#region src/w/useKeyboard.ts const useKeyboard = () => { const keys = {}; document.addEventListener("keydown", (e) => { keys[e.key.toLowerCase()] = true; }); document.addEventListener("keyup", (e) => { keys[e.key.toLowerCase()] = false; }); return { getKeys: () => keys, isKeyPressed: (key) => keys[key.toLowerCase()] }; }; //#endregion //#region src/w/usePointerLock.ts const usePointerLock = (options) => { const { onMove = () => {}, element = document.body } = options ?? {}; element.addEventListener("click", () => { element.requestPointerLock(); }); document.addEventListener("mousemove", (e) => { if (document.pointerLockElement) onMove(e); }); }; //#endregion //#region src/w/useResizer.ts const useResizer = () => { const c = document.getElementById("c"); const resizeCanvas = () => { c.width = window.innerWidth; c.height = window.innerHeight; if (W.gl) { W.gl.viewport(0, 0, c.width, c.height); if (W.projection) { const fov = 30; W.projection = new DOMMatrix([ 1 / Math.tan(fov * Math.PI / 180) / (c.width / c.height), 0, 0, 0, 0, 1 / Math.tan(fov * Math.PI / 180), 0, 0, 0, 0, -1001 / 999, -1, 0, 0, -2002 / 999, 0 ]); } } }; resizeCanvas(); window.addEventListener("resize", resizeCanvas); }; //#endregion //#region src/w/useSpeedThrottledRaf.ts const useSpeedThrottledRaf = (moveSpeed, moveFunction) => { let lastTime = 0; const animate = (currentTime) => { const deltaTime = (currentTime - lastTime) / 1e3; lastTime = currentTime; moveFunction(moveSpeed * deltaTime); requestAnimationFrame(animate); }; requestAnimationFrame(animate); }; //#endregion //#region src/w/useW.ts const useW = () => { const s = document.createElement("script"); s.innerHTML = `debug=0;W={models:{},reset:e=>{W.lastFrame=0,W.canvas=e,W.objs=0,W.current={},W.next={},W.textures={},W.gl=e.getContext("webgl2"),W.gl.blendFunc(770,771),W.gl.activeTexture(33984),W.program=W.gl.createProgram(),W.gl.enable(2884),W.gl.shaderSource(t=W.gl.createShader(35633),"#version 300 es\\nprecision highp float;in vec4 pos,col,uv,normal;uniform mat4 pv,eye,m,im;uniform vec4 bb;out vec4 v_pos,v_col,v_uv,v_normal;void main(){gl_Position=pv*(v_pos=bb.z>0.?m[3]+eye*(pos*bb):m*pos);v_col=col;v_uv=uv;v_normal=transpose(inverse(m))*normal;}"),W.gl.compileShader(t),W.gl.attachShader(W.program,t),W.gl.shaderSource(t=W.gl.createShader(35632),"#version 300 es\\nprecision highp float;in vec4 v_pos,v_col,v_uv,v_normal;uniform vec3 light;uniform vec4 o;uniform sampler2D sampler;out vec4 c;void main(){c=mix(texture(sampler,v_uv.xy),v_col,o[3]);if(o[1]>0.){c=vec4(c.rgb*(dot(light,-normalize(o[0]>0.?vec3(v_normal.xyz):cross(dFdx(v_pos.xyz),dFdy(v_pos.xyz))))+o[2]),c.a);}}"),W.gl.compileShader(t),W.gl.attachShader(W.program,t),W.gl.linkProgram(W.program),W.gl.useProgram(W.program),W.gl.clearColor(1,1,1,1),W.clearColor=e=>W.gl.clearColor(...W.col(e)),W.clearColor("fff"),W.gl.enable(2929),W.light({y:-1}),W.camera({fov:30}),setTimeout(()=>requestAnimationFrame(W.draw),16)},setState:(e,t,r,o,a=[],n,l,i,s,m,g,d,c)=>{e.n||="o"+W.objs++,e.size&&(e.w=e.h=e.d=e.size),e.t&&e.t.width&&!W.textures[e.t.id]&&(r=W.gl.createTexture(),W.gl.pixelStorei(37441,!0),W.gl.bindTexture(3553,r),W.gl.pixelStorei(37440,1),W.gl.texImage2D(3553,0,6408,6408,5121,e.t),W.gl.generateMipmap(3553),W.textures[e.t.id]=r),e.fov&&(W.projection=new DOMMatrix([1/Math.tan(e.fov*Math.PI/180)/(W.canvas.width/W.canvas.height),0,0,0,0,1/Math.tan(e.fov*Math.PI/180),0,0,0,0,-1001/999,-1,0,0,-2002/999,0])),e={type:t,...W.current[e.n]=W.next[e.n]||{w:1,h:1,d:1,x:0,y:0,z:0,rx:0,ry:0,rz:0,b:"888",mode:4,mix:0},...e,f:0},W.models[e.type]?.vertices&&!W.models?.[e.type].verticesBuffer&&(W.gl.bindBuffer(34962,W.models[e.type].verticesBuffer=W.gl.createBuffer()),W.gl.bufferData(34962,new Float32Array(W.models[e.type].vertices),35044),!W.models[e.type].normals&&W.smooth&&W.smooth(e),W.models[e.type].normals&&(W.gl.bindBuffer(34962,W.models[e.type].normalsBuffer=W.gl.createBuffer()),W.gl.bufferData(34962,new Float32Array(W.models[e.type].normals.flat()),35044))),W.models[e.type]?.uv&&!W.models[e.type].uvBuffer&&(W.gl.bindBuffer(34962,W.models[e.type].uvBuffer=W.gl.createBuffer()),W.gl.bufferData(34962,new Float32Array(W.models[e.type].uv),35044)),W.models[e.type]?.indices&&!W.models[e.type].indicesBuffer&&(W.gl.bindBuffer(34963,W.models[e.type].indicesBuffer=W.gl.createBuffer()),W.gl.bufferData(34963,new Uint16Array(W.models[e.type].indices),35044)),e.t?e.t&&!e.mix&&(e.mix=0):e.mix=1,W.next[e.n]=e},draw:(e,t,r,o,a=[])=>{for(o in t=e-W.lastFrame,W.lastFrame=e,requestAnimationFrame(W.draw),W.next.camera.g&&W.render(W.next[W.next.camera.g],t,1),r=W.animation("camera"),W.next?.camera?.g&&r.preMultiplySelf(W.next[W.next.camera.g].M||W.next[W.next.camera.g].m),W.gl.uniformMatrix4fv(W.gl.getUniformLocation(W.program,"eye"),!1,r.toFloat32Array()),r.invertSelf(),r.preMultiplySelf(W.projection),W.gl.uniformMatrix4fv(W.gl.getUniformLocation(W.program,"pv"),!1,r.toFloat32Array()),W.gl.clear(16640),W.next)W.next[o].t||1!=W.col(W.next[o].b)[3]?a.push(W.next[o]):W.render(W.next[o],t);for(o of(a.sort(((e,t)=>W.dist(t)-W.dist(e))),W.gl.enable(3042),a))["plane","billboard"].includes(o.type)&&W.gl.depthMask(0),W.render(o,t),W.gl.depthMask(1);W.gl.disable(3042),W.gl.uniform3f(W.gl.getUniformLocation(W.program,"light"),W.lerp("light","x"),W.lerp("light","y"),W.lerp("light","z"))},render:(e,t,r=["camera","light","group"].includes(e.type),o)=>{e.t&&(W.gl.bindTexture(3553,W.textures[e.t.id]),W.gl.uniform1i(W.gl.getUniformLocation(W.program,"sampler"),0)),e.f<e.a&&(e.f+=t),e.f>e.a&&(e.f=e.a),W.next[e.n].m=W.animation(e.n),W.next[e.g]&&W.next[e.n].m.preMultiplySelf(W.next[e.g].M||W.next[e.g].m),W.gl.uniformMatrix4fv(W.gl.getUniformLocation(W.program,"m"),!1,(W.next[e.n].M||W.next[e.n].m).toFloat32Array()),W.gl.uniformMatrix4fv(W.gl.getUniformLocation(W.program,"im"),!1,new DOMMatrix(W.next[e.n].M||W.next[e.n].m).invertSelf().toFloat32Array()),r||(W.gl.bindBuffer(34962,W.models[e.type].verticesBuffer),W.gl.vertexAttribPointer(o=W.gl.getAttribLocation(W.program,"pos"),3,5126,!1,0,0),W.gl.enableVertexAttribArray(o),W.models[e.type].uvBuffer&&(W.gl.bindBuffer(34962,W.models[e.type].uvBuffer),W.gl.vertexAttribPointer(o=W.gl.getAttribLocation(W.program,"uv"),2,5126,!1,0,0),W.gl.enableVertexAttribArray(o)),(e.s||W.models[e.type].customNormals)&&W.models[e.type].normalsBuffer&&(W.gl.bindBuffer(34962,W.models[e.type].normalsBuffer),W.gl.vertexAttribPointer(o=W.gl.getAttribLocation(W.program,"normal"),3,5126,!1,0,0),W.gl.enableVertexAttribArray(o)),W.gl.uniform4f(W.gl.getUniformLocation(W.program,"o"),e.s,(e.mode>3||W.gl[e.mode]>3)&&!e.ns?1:0,W.ambientLight||.2,e.mix),W.gl.uniform4f(W.gl.getUniformLocation(W.program,"bb"),e.w,e.h,"billboard"==e.type,0),W.models[e.type].indicesBuffer&&W.gl.bindBuffer(34963,W.models[e.type].indicesBuffer),W.gl.vertexAttrib4fv(W.gl.getAttribLocation(W.program,"col"),W.col(e.b)),W.models[e.type].indicesBuffer?W.gl.drawElements(+e.mode||W.gl[e.mode],W.models[e.type].indices.length,5123,0):W.gl.drawArrays(+e.mode||W.gl[e.mode],0,W.models[e.type].vertices.length/3))},lerp:(e,t)=>W.next[e]?.a?W.current[e][t]+(W.next[e][t]-W.current[e][t])*(W.next[e].f/W.next[e].a):W.next[e][t],animation:(e,t=new DOMMatrix)=>W.next[e]?t.translateSelf(W.lerp(e,"x"),W.lerp(e,"y"),W.lerp(e,"z")).rotateSelf(W.lerp(e,"rx"),W.lerp(e,"ry"),W.lerp(e,"rz")).scaleSelf(W.lerp(e,"w"),W.lerp(e,"h"),W.lerp(e,"d")):t,dist:(e,t=W.next.camera)=>e?.m&&t?.m?(t.m.m41-e.m.m41)**2+(t.m.m42-e.m.m42)**2+(t.m.m43-e.m.m43)**2:0,ambient:e=>W.ambientLight=e,col:e=>[...e.replace("#","").match(e.length<5?/./g:/../g).map((t=>("0x"+t)/(e.length<5?15:255))),1],add:(e,t)=>{W.models[e]=t,t.normals&&(W.models[e].customNormals=1),W[e]=t=>W.setState(t,e)},group:e=>W.setState(e,"group"),move:(e,t)=>setTimeout((()=>{W.setState(e)}),t||1),delete:(e,t)=>setTimeout((()=>{delete W.next[e]}),t||1),camera:(e,t)=>setTimeout((()=>{W.setState(e,e.n="camera")}),t||1),light:(e,t)=>t?setTimeout((()=>{W.setState(e,e.n="light")}),t):W.setState(e,e.n="light")},W.smooth=(e,t={},r=[],o,a,n,l,i,s,m,g,d,c,p)=>{for(W.models[e.type].normals=[],n=0;n<W.models[e.type].vertices.length;n+=3)r.push(W.models[e.type].vertices.slice(n,n+3));for((o=W.models[e.type].indices)?a=1:(o=r,a=0),n=0;n<2*o.length;n+=3)l=n%o.length,i=r[g=a?W.models[e.type].indices[l]:l],s=r[d=a?W.models[e.type].indices[l+1]:l+1],m=r[c=a?W.models[e.type].indices[l+2]:l+2],AB=[s[0]-i[0],s[1]-i[1],s[2]-i[2]],BC=[m[0]-s[0],m[1]-s[1],m[2]-s[2]],p=n>l?[0,0,0]:[AB[1]*BC[2]-AB[2]*BC[1],AB[2]*BC[0]-AB[0]*BC[2],AB[0]*BC[1]-AB[1]*BC[0]],t[i[0]+"_"+i[1]+"_"+i[2]]||=[0,0,0],t[s[0]+"_"+s[1]+"_"+s[2]]||=[0,0,0],t[m[0]+"_"+m[1]+"_"+m[2]]||=[0,0,0],W.models[e.type].normals[g]=t[i[0]+"_"+i[1]+"_"+i[2]]=t[i[0]+"_"+i[1]+"_"+i[2]].map(((e,t)=>e+p[t])),W.models[e.type].normals[d]=t[s[0]+"_"+s[1]+"_"+s[2]]=t[s[0]+"_"+s[1]+"_"+s[2]].map(((e,t)=>e+p[t])),W.models[e.type].normals[c]=t[m[0]+"_"+m[1]+"_"+m[2]]=t[m[0]+"_"+m[1]+"_"+m[2]].map(((e,t)=>e+p[t]))},W.add("plane",{vertices:[.5,.5,0,-.5,.5,0,-.5,-.5,0,.5,.5,0,-.5,-.5,0,.5,-.5,0],uv:[1,1,0,1,0,0,1,1,0,0,1,0]}),W.add("billboard",W.models.plane),W.add("cube",{vertices:[.5,.5,.5,-.5,.5,.5,-.5,-.5,.5,.5,.5,.5,-.5,-.5,.5,.5,-.5,.5,.5,.5,-.5,.5,.5,.5,.5,-.5,.5,.5,.5,-.5,.5,-.5,.5,.5,-.5,-.5,.5,.5,-.5,-.5,.5,-.5,-.5,.5,.5,.5,.5,-.5,-.5,.5,.5,.5,.5,.5,-.5,.5,.5,-.5,.5,-.5,-.5,-.5,-.5,-.5,.5,.5,-.5,-.5,-.5,-.5,-.5,.5,-.5,.5,-.5,.5,.5,-.5,.5,-.5,-.5,-.5,.5,-.5,.5,-.5,-.5,-.5,-.5,-.5,.5,-.5,.5,-.5,-.5,.5,-.5,-.5,-.5,.5,-.5,.5,-.5,-.5,-.5,.5,-.5,-.5],uv:[1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0]}),W.cube=e=>W.setState(e,"cube"),W.add("pyramid",{vertices:[-.5,-.5,.5,.5,-.5,.5,0,.5,0,.5,-.5,.5,.5,-.5,-.5,0,.5,0,.5,-.5,-.5,-.5,-.5,-.5,0,.5,0,-.5,-.5,-.5,-.5,-.5,.5,0,.5,0,.5,-.5,.5,-.5,-.5,.5,-.5,-.5,-.5,.5,-.5,.5,-.5,-.5,-.5,.5,-.5,-.5],uv:[0,0,1,0,.5,1,0,0,1,0,.5,1,0,0,1,0,.5,1,0,0,1,0,.5,1,1,1,0,1,0,0,1,1,0,0,1,0]}),((e,t,r,o,a,n,l=[],i=[],s=[],m=20)=>{for(r=0;r<=m;r++)for(o=r*Math.PI/m,e=0;e<=m;e++)t=2*e*Math.PI/m,l.push(+(Math.sin(t)*Math.sin(o)/2).toFixed(6),+(Math.cos(o)/2).toFixed(6),+(Math.cos(t)*Math.sin(o)/2).toFixed(6)),s.push(3.5*Math.sin(e/m),-Math.sin(r/m)),e<m&&r<m&&i.push(a=r*(m+1)+e,n=a+(m+1),a+1,a+1,n,n+1);W.add("sphere",{vertices:l,uv:s,indices:i})})()`; document.head.appendChild(s); }; //#endregion export { ENTITY_COLLECTION_PREFIX, PLAYER_ENTITY_COLLECTION_KEY, PRIVATE_KEY_COLLECTION_KEY, PRIVATE_KEY_PREFIX, createKeyNormalizer, createMyStateCopier, createPositionNormalizer, createRotationNormalizer, createSongPlayer, createVelocityNormalizer, deepCopy, generateUUID, labCommand, mergeDeep, normalizeDeg, normalizeRad, noteToFrequency, onClientIdUpdated, onClientJoined, onClientLeft, onClose, onCommandMessage, onError, onIdentReceived, onMyIdUpdated, onOpen, onStateDeltaMessage, onStateMessage, playSingleNote, round, sendCommandMessageToAll, sendCommandMessageToClient, sendIdentToClient, sendMessage, sendMessageToClient, useDemo, useEasyState, useKeyboard, useMyId, useOnline, usePointerLock, usePresence, useResizer, useSpeedThrottledRaf, useState, useW }; //# sourceMappingURL=index.js.map