lab13-sdk
Version:
JS13K tools and game infrastructure SDK.
717 lines (690 loc) • 32.5 kB
JavaScript
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