UNPKG

kaplay

Version:

KAPLAY is a JavaScript & TypeScript game library that helps you make games fast and fun! (formerly known as Kaboom.js)

4 lines 910 kB
{ "version": 3, "sources": ["../src/kaplay.ts", "../package.json", "../src/math/clamp.ts", "../src/math/color.ts", "../src/math/math.ts", "../src/constants.ts", "../src/events/events.ts", "../src/utils/asserts.ts", "../src/utils/binaryheap.ts", "../src/utils/dataURL.ts", "../src/utils/deepEq.ts", "../src/utils/log.ts", "../src/utils/numbers.ts", "../src/utils/overload.ts", "../src/utils/runes.ts", "../src/utils/sets.ts", "../src/utils/uid.ts", "../src/data/gamepad.json", "../src/gfx/viewport.ts", "../src/app/inputBindings.ts", "../src/app/app.ts", "../src/app/frame.ts", "../src/gfx/anchor.ts", "../src/math/easings.ts", "../src/math/navigation.ts", "../src/math/navigationmesh.ts", "../src/math/various.ts", "../src/gfx/bg.ts", "../src/gfx/stack.ts", "../src/gfx/texPacker.ts", "../src/assets/utils.ts", "../src/assets/asset.ts", "../src/assets/sprite.ts", "../src/assets/aseprite.ts", "../src/assets/font.ts", "../src/assets/bitmapFont.ts", "../src/assets/pedit.ts", "../src/assets/shader.ts", "../src/assets/sound.ts", "../src/assets/spriteAtlas.ts", "../src/gfx/draw/drawRaw.ts", "../src/gfx/draw/drawPolygon.ts", "../src/gfx/draw/drawEllipse.ts", "../src/gfx/draw/drawCircle.ts", "../src/gfx/draw/drawLine.ts", "../src/gfx/draw/drawCurve.ts", "../src/gfx/draw/drawBezier.ts", "../src/gfx/gfx.ts", "../src/gfx/formatText.ts", "../src/gfx/draw/drawUVQuad.ts", "../src/gfx/draw/drawFormattedText.ts", "../src/gfx/draw/drawRect.ts", "../src/gfx/draw/drawUnscaled.ts", "../src/gfx/draw/drawInspectText.ts", "../src/gfx/draw/drawTriangle.ts", "../src/gfx/draw/drawDebug.ts", "../src/gfx/draw/drawFrame.ts", "../src/gfx/draw/drawLoadingScreen.ts", "../src/gfx/draw/drawStenciled.ts", "../src/gfx/draw/drawMasked.ts", "../src/gfx/draw/drawTexture.ts", "../src/gfx/draw/drawSprite.ts", "../src/gfx/draw/drawSubstracted.ts", "../src/gfx/draw/drawText.ts", "../src/gfx/gfxApp.ts", "../src/game/utils.ts", "../src/components/draw/circle.ts", "../src/components/draw/color.ts", "../src/components/draw/drawon.ts", "../src/components/draw/fadeIn.ts", "../src/components/draw/mask.ts", "../src/components/draw/opacity.ts", "../src/components/draw/outline.ts", "../src/components/draw/particles.ts", "../src/components/draw/polygon.ts", "../src/components/draw/raycast.ts", "../src/components/draw/rect.ts", "../src/components/draw/shader.ts", "../src/game/level.ts", "../src/events/globalEvents.ts", "../src/game/camera.ts", "../src/game/make.ts", "../src/game/game.ts", "../src/game/gravity.ts", "../src/audio/audio.ts", "../src/audio/playMusic.ts", "../src/audio/play.ts", "../src/audio/burp.ts", "../src/audio/volume.ts", "../src/game/initEvents.ts", "../src/game/kaboom.ts", "../src/game/layers.ts", "../src/game/object.ts", "../src/game/scenes.ts", "../src/components/draw/sprite.ts", "../src/components/draw/text.ts", "../src/components/draw/uvquad.ts", "../src/components/level/agent.ts", "../src/components/level/pathfinder.ts", "../src/components/level/patrol.ts", "../src/components/level/sentry.ts", "../src/components/level/tile.ts", "../src/components/misc/animate.ts", "../src/components/misc/boom.ts", "../src/components/misc/health.ts", "../src/components/misc/lifespan.ts", "../src/components/misc/named.ts", "../src/components/misc/state.ts", "../src/components/misc/stay.ts", "../src/components/misc/textInput.ts", "../src/components/misc/timer.ts", "../src/components/physics/area.ts", "../src/components/physics/body.ts", "../src/components/physics/doubleJump.ts", "../src/components/physics/effectors.ts", "../src/components/transform/anchor.ts", "../src/components/transform/fixed.ts", "../src/components/transform/follow.ts", "../src/components/transform/layer.ts", "../src/components/transform/move.ts", "../src/components/transform/offscreen.ts", "../src/components/transform/pos.ts", "../src/components/transform/rotate.ts", "../src/components/transform/scale.ts", "../src/components/transform/z.ts"], "sourcesContent": ["import packageJsonData from \"../package.json\";\n\nconst VERSION = packageJsonData.version;\n\nimport { type ButtonsDef, initApp } from \"./app\";\n\nimport {\n center,\n drawBezier,\n drawCircle,\n drawCurve,\n drawDebug,\n drawEllipse,\n drawFormattedText,\n drawFrame,\n drawLine,\n drawLines,\n drawLoadScreen,\n drawMasked,\n drawPolygon,\n drawRect,\n drawSprite,\n drawSubtracted,\n drawText,\n drawTexture,\n drawTriangle,\n drawUnscaled,\n drawUVQuad,\n flush,\n formatText,\n FrameBuffer,\n getBackground,\n height,\n initAppGfx,\n initGfx,\n popTransform,\n pushMatrix,\n pushRotate,\n pushScale,\n pushTransform,\n pushTranslate,\n setBackground,\n updateViewport,\n width,\n} from \"./gfx\";\n\nimport {\n Asset,\n getAsset,\n getBitmapFont,\n getFailedAssets,\n getFont,\n getShader,\n getSound,\n getSprite,\n initAssets,\n load,\n loadAseprite,\n loadBean,\n loadBitmapFont,\n loadFont,\n loadJSON,\n loadMusic,\n loadPedit,\n loadProgress,\n loadRoot,\n loadShader,\n loadShaderURL,\n loadSound,\n loadSprite,\n loadSpriteAtlas,\n SoundData,\n SpriteData,\n type Uniform,\n} from \"./assets\";\n\nimport {\n ASCII_CHARS,\n BG_GRID_SIZE,\n DBG_FONT,\n DEF_HASH_GRID_SIZE,\n EVENT_CANCEL_SYMBOL,\n LOG_MAX,\n MAX_TEXT_CACHE_SIZE,\n} from \"./constants\";\n\nimport {\n bezier,\n cardinal,\n catmullRom,\n chance,\n choose,\n chooseMultiple,\n Circle,\n Color,\n curveLengthApproximation,\n deg2rad,\n easingCubicBezier,\n easingLinear,\n easingSteps,\n Ellipse,\n evaluateBezier,\n evaluateBezierFirstDerivative,\n evaluateBezierSecondDerivative,\n evaluateCatmullRom,\n evaluateCatmullRomFirstDerivative,\n evaluateQuadratic,\n evaluateQuadraticFirstDerivative,\n evaluateQuadraticSecondDerivative,\n hermite,\n hsl2rgb,\n isConvex,\n kochanekBartels,\n lerp,\n Line,\n map,\n mapc,\n Mat4,\n NavMesh,\n normalizedCurve,\n Point,\n Polygon,\n Quad,\n quad,\n rad2deg,\n rand,\n randi,\n randSeed,\n Rect,\n rgb,\n RNG,\n sat,\n shuffle,\n testCirclePolygon,\n testLineCircle,\n testLineLine,\n testLinePoint,\n testRectLine,\n testRectPoint,\n testRectRect,\n triangulate,\n Vec2,\n vec2,\n wave,\n} from \"./math\";\n\nimport easings from \"./math/easings\";\n\nimport {\n download,\n downloadBlob,\n downloadJSON,\n downloadText,\n KEvent,\n KEventController,\n KEventHandler,\n} from \"./utils\";\n\nimport type {\n Debug,\n GameObj,\n KAPLAYCtx,\n KAPLAYInternal,\n KAPLAYOpt,\n KAPLAYPlugin,\n MergePlugins,\n PluginList,\n Recording,\n} from \"./types\";\n\nimport {\n agent,\n anchor,\n animate,\n area,\n type AreaComp,\n areaEffector,\n body,\n buoyancyEffector,\n circle,\n color,\n constantForce,\n doubleJump,\n drawon,\n fadeIn,\n fixed,\n follow,\n health,\n layer,\n lifespan,\n mask,\n move,\n named,\n offscreen,\n opacity,\n outline,\n particles,\n pathfinder,\n patrol,\n platformEffector,\n pointEffector,\n polygon,\n pos,\n raycast,\n rect,\n rotate,\n scale,\n sentry,\n serializeAnimation,\n shader,\n sprite,\n state,\n stay,\n surfaceEffector,\n text,\n textInput,\n tile,\n timer,\n usesArea,\n uvquad,\n z,\n} from \"./components\";\n\nimport { dt, fixedDt, restDt } from \"./app\";\nimport { burp, getVolume, initAudio, play, setVolume, volume } from \"./audio\";\n\nimport {\n addKaboom,\n addLevel,\n camFlash,\n camPos,\n camRot,\n camScale,\n camTransform,\n destroy,\n flash,\n getCamPos,\n getCamRot,\n getCamScale,\n getCamTransform,\n getDefaultLayer,\n getGravity,\n getGravityDirection,\n getLayers,\n getSceneName,\n getTreeRoot,\n go,\n initEvents,\n initGame,\n layers,\n make,\n on,\n onAdd,\n onClick,\n onCollide,\n onCollideEnd,\n onCollideUpdate,\n onDestroy,\n onDraw,\n onError,\n onFixedUpdate,\n onHover,\n onHoverEnd,\n onHoverUpdate,\n onLoad,\n onLoadError,\n onLoading,\n onResize,\n onSceneLeave,\n onTag,\n onUntag,\n onUnuse,\n onUpdate,\n onUse,\n scene,\n setCamPos,\n setCamRot,\n setCamScale,\n setGravity,\n setGravityDirection,\n setLayers,\n shake,\n toScreen,\n toWorld,\n trigger,\n} from \"./game\";\n\nimport boomSpriteSrc from \"./kassets/boom.png\";\nimport kaSpriteSrc from \"./kassets/ka.png\";\nimport { clamp } from \"./math/clamp.js\";\n\n// Internal data, shared between all modules\nexport const _k = {\n k: null,\n globalOpt: null,\n gfx: null,\n game: null,\n app: null,\n assets: null,\n fontCacheCanvas: null,\n fontCacheC2d: null,\n debug: null,\n audio: null,\n pixelDensity: null,\n canvas: null,\n gscale: null,\n kaSprite: null,\n boomSprite: null,\n} as unknown as KAPLAYInternal;\n\n/**\n * Initialize KAPLAY context. The starting point of all KAPLAY games.\n *\n * @example\n * ```js\n * // Start KAPLAY with default options (will create a fullscreen canvas under <body>)\n * kaplay()\n *\n * // Init with some options\n * kaplay({\n * width: 320,\n * height: 240,\n * font: \"sans-serif\",\n * canvas: document.querySelector(\"#mycanvas\"),\n * background: [ 0, 0, 255, ],\n * })\n *\n * // All KAPLAY functions are imported to global after calling kaplay()\n * add()\n * onUpdate()\n * onKeyPress()\n * vec2()\n *\n * // If you want to prevent KAPLAY from importing all functions to global and use a context handle for all KAPLAY functions\n * const k = kaplay({ global: false })\n *\n * k.add(...)\n * k.onUpdate(...)\n * k.onKeyPress(...)\n * k.vec2(...)\n * ```\n *\n * @group Start\n */\nconst kaplay = <\n TPlugins extends PluginList<unknown> = [undefined],\n TButtons extends ButtonsDef = {},\n TButtonsName extends string = keyof TButtons & string,\n>(\n gopt: KAPLAYOpt<TPlugins, TButtons> = {\n tagsAsComponents: true,\n },\n): TPlugins extends [undefined] ? KAPLAYCtx<TButtons, TButtonsName>\n : KAPLAYCtx<TButtons, TButtonsName> & MergePlugins<TPlugins> =>\n{\n if (_k.k) {\n console.warn(\n \"KAPLAY already initialized, you are calling kaplay() multiple times, it may lead bugs!\",\n );\n _k.k.quit();\n }\n\n _k.globalOpt = gopt;\n const root = gopt.root ?? document.body;\n\n // if root is not defined (which falls back to <body>) we assume user is using kaboom on a clean page, and modify <body> to better fit a full screen canvas\n if (root === document.body) {\n document.body.style[\"width\"] = \"100%\";\n document.body.style[\"height\"] = \"100%\";\n document.body.style[\"margin\"] = \"0px\";\n document.documentElement.style[\"width\"] = \"100%\";\n document.documentElement.style[\"height\"] = \"100%\";\n }\n\n // create a <canvas> if user didn't provide one\n const canvas = gopt.canvas\n ?? root.appendChild(document.createElement(\"canvas\"));\n _k.canvas = canvas;\n\n // global pixel scale\n const gscale = gopt.scale ?? 1;\n _k.gscale = gscale;\n const fixedSize = gopt.width && gopt.height && !gopt.stretch\n && !gopt.letterbox;\n\n // adjust canvas size according to user size / viewport settings\n if (fixedSize) {\n canvas.width = gopt.width! * gscale;\n canvas.height = gopt.height! * gscale;\n }\n else {\n canvas.width = canvas.parentElement!.offsetWidth;\n canvas.height = canvas.parentElement!.offsetHeight;\n }\n\n // canvas css styles\n const styles = [\n \"outline: none\",\n \"cursor: default\",\n ];\n\n if (fixedSize) {\n const cw = canvas.width;\n const ch = canvas.height;\n styles.push(`width: ${cw}px`);\n styles.push(`height: ${ch}px`);\n }\n else {\n styles.push(\"width: 100%\");\n styles.push(\"height: 100%\");\n }\n\n if (gopt.crisp) {\n // chrome only supports pixelated and firefox only supports crisp-edges\n styles.push(\"image-rendering: pixelated\");\n styles.push(\"image-rendering: crisp-edges\");\n }\n\n canvas.style.cssText = styles.join(\";\");\n\n const pixelDensity = gopt.pixelDensity || 1;\n _k.pixelDensity = pixelDensity;\n\n canvas.width *= pixelDensity;\n canvas.height *= pixelDensity;\n // make canvas focusable\n canvas.tabIndex = 0;\n\n const fontCacheCanvas = document.createElement(\"canvas\");\n fontCacheCanvas.width = MAX_TEXT_CACHE_SIZE;\n fontCacheCanvas.height = MAX_TEXT_CACHE_SIZE;\n _k.fontCacheCanvas = fontCacheCanvas;\n const fontCacheC2d = fontCacheCanvas.getContext(\"2d\", {\n willReadFrequently: true,\n });\n _k.fontCacheC2d = fontCacheC2d;\n\n const app = initApp({\n canvas: canvas,\n touchToMouse: gopt.touchToMouse,\n gamepads: gopt.gamepads,\n pixelDensity: gopt.pixelDensity,\n maxFPS: gopt.maxFPS,\n buttons: gopt.buttons,\n });\n _k.app = app;\n\n const gc: Array<() => void> = [];\n\n const canvasContext = app.canvas\n .getContext(\"webgl\", {\n antialias: true,\n depth: true,\n stencil: true,\n alpha: true,\n preserveDrawingBuffer: true,\n });\n\n if (!canvasContext) throw new Error(\"WebGL not supported\");\n\n const gl = canvasContext;\n\n const ggl = initGfx(gl, {\n texFilter: gopt.texFilter,\n });\n\n const gfx = initAppGfx(gopt, ggl);\n _k.gfx = gfx;\n const audio = initAudio();\n _k.audio = audio;\n const assets = initAssets(ggl, gopt.spriteAtlasPadding ?? 0);\n _k.assets = assets;\n const game = initGame();\n _k.game = game;\n\n game.root.use(timer());\n\n function makeCanvas(w: number, h: number) {\n const fb = new FrameBuffer(ggl, w, h);\n return {\n clear: () => fb.clear(),\n free: () => fb.free(),\n toDataURL: () => fb.toDataURL(),\n toImageData: () => fb.toImageData(),\n width: fb.width,\n height: fb.height,\n draw: (action: () => void) => {\n flush();\n fb.bind();\n action();\n flush();\n fb.unbind();\n },\n get fb() {\n return fb;\n },\n };\n }\n\n // start a rendering frame, reset some states\n function frameStart() {\n // clear backbuffer\n gl.clear(gl.COLOR_BUFFER_BIT);\n gfx.frameBuffer.bind();\n // clear framebuffer\n gl.clear(gl.COLOR_BUFFER_BIT);\n\n if (!gfx.bgColor) {\n drawUnscaled(() => {\n drawUVQuad({\n width: width(),\n height: height(),\n quad: new Quad(\n 0,\n 0,\n width() / BG_GRID_SIZE,\n height() / BG_GRID_SIZE,\n ),\n tex: gfx.bgTex,\n fixed: true,\n });\n });\n }\n\n gfx.renderer.numDraws = 0;\n gfx.fixed = false;\n gfx.transformStack.length = 0;\n gfx.transform = new Mat4();\n }\n\n function usePostEffect(name: string, uniform?: Uniform | (() => Uniform)) {\n gfx.postShader = name;\n gfx.postShaderUniform = uniform ?? null;\n }\n\n function frameEnd() {\n // TODO: don't render debug UI with framebuffer\n // TODO: polish framebuffer rendering / sizing issues\n flush();\n gfx.lastDrawCalls = gfx.renderer.numDraws;\n gfx.frameBuffer.unbind();\n gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);\n\n const ow = gfx.width;\n const oh = gfx.height;\n gfx.width = gl.drawingBufferWidth / pixelDensity;\n gfx.height = gl.drawingBufferHeight / pixelDensity;\n\n drawTexture({\n flipY: true,\n tex: gfx.frameBuffer.tex,\n pos: new Vec2(gfx.viewport.x, gfx.viewport.y),\n width: gfx.viewport.width,\n height: gfx.viewport.height,\n shader: gfx.postShader,\n uniform: typeof gfx.postShaderUniform === \"function\"\n ? gfx.postShaderUniform()\n : gfx.postShaderUniform,\n fixed: true,\n });\n\n flush();\n gfx.width = ow;\n gfx.height = oh;\n }\n\n // TODO: cache formatted text\n // format text and return a list of chars with their calculated position\n\n // get game root\n\n let debugPaused = false;\n\n const debug: Debug = {\n inspect: false,\n set timeScale(timeScale: number) {\n _k.app.state.timeScale = timeScale;\n },\n get timeScale() {\n return _k.app.state.timeScale;\n },\n showLog: true,\n fps: () => app.fps(),\n numFrames: () => app.numFrames(),\n stepFrame: updateFrame,\n drawCalls: () => gfx.lastDrawCalls,\n clearLog: () => game.logs = [],\n log: (...msgs) => {\n const max = gopt.logMax ?? LOG_MAX;\n const msg = msgs.length > 1 ? msgs.concat(\" \").join(\" \") : msgs[0];\n\n game.logs.unshift({\n msg: msg,\n time: app.time(),\n });\n if (game.logs.length > max) {\n game.logs = game.logs.slice(0, max);\n }\n },\n error: (msg) =>\n debug.log(new Error(msg.toString ? msg.toString() : msg as string)),\n curRecording: null,\n numObjects: () => get(\"*\", { recursive: true }).length,\n get paused() {\n return debugPaused;\n },\n set paused(v) {\n debugPaused = v;\n if (v) {\n audio.ctx.suspend();\n }\n else {\n audio.ctx.resume();\n }\n },\n };\n\n _k.debug = debug;\n\n function getData<T>(key: string, def?: T): T | null {\n try {\n return JSON.parse(window.localStorage[key]);\n } catch {\n if (def) {\n setData(key, def);\n return def;\n }\n else {\n return null;\n }\n }\n }\n\n function setData(key: string, data: any) {\n window.localStorage[key] = JSON.stringify(data);\n }\n\n function plug<T extends Record<string, any>>(\n plugin: KAPLAYPlugin<T>,\n ...args: any\n ): KAPLAYCtx & T {\n const funcs = plugin(ctx);\n let funcsObj: T;\n if (typeof funcs === \"function\") {\n const plugWithOptions = funcs(...args);\n funcsObj = plugWithOptions(ctx);\n }\n else {\n funcsObj = funcs;\n }\n\n for (const key in funcsObj) {\n ctx[key as keyof typeof ctx] = funcsObj[key];\n\n if (gopt.global !== false) {\n window[key as any] = funcsObj[key];\n }\n }\n return ctx as unknown as KAPLAYCtx & T;\n }\n\n function record(frameRate?: number): Recording {\n const stream = app.canvas.captureStream(frameRate);\n const audioDest = audio.ctx.createMediaStreamDestination();\n\n audio.masterNode.connect(audioDest);\n\n // TODO: Enabling audio results in empty video if no audio received\n // const audioStream = audioDest.stream\n // const [firstAudioTrack] = audioStream.getAudioTracks()\n\n // stream.addTrack(firstAudioTrack);\n\n const recorder = new MediaRecorder(stream);\n const chunks: any[] = [];\n\n recorder.ondataavailable = (e) => {\n if (e.data.size > 0) {\n chunks.push(e.data);\n }\n };\n\n recorder.onerror = () => {\n audio.masterNode.disconnect(audioDest);\n stream.getTracks().forEach(t => t.stop());\n };\n\n recorder.start();\n\n return {\n resume() {\n recorder.resume();\n },\n\n pause() {\n recorder.pause();\n },\n\n stop(): Promise<Blob> {\n recorder.stop();\n // cleanup\n audio.masterNode.disconnect(audioDest);\n stream.getTracks().forEach(t => t.stop());\n return new Promise((resolve) => {\n recorder.onstop = () => {\n resolve(\n new Blob(chunks, {\n type: \"video/mp4\",\n }),\n );\n };\n });\n },\n\n download(filename = \"kaboom.mp4\") {\n this.stop().then((blob) => downloadBlob(filename, blob));\n },\n };\n }\n\n function isFocused(): boolean {\n return document.activeElement === app.canvas;\n }\n\n // aliases for root game obj operations\n const add = game.root.add.bind(game.root);\n const readd = game.root.readd.bind(game.root);\n const destroyAll = game.root.removeAll.bind(game.root);\n const get = game.root.get.bind(game.root);\n const wait = game.root.wait.bind(game.root);\n const loop = game.root.loop.bind(game.root);\n const query = game.root.query.bind(game.root);\n const tween = game.root.tween.bind(game.root);\n\n const kaSprite = loadSprite(null, kaSpriteSrc);\n const boomSprite = loadSprite(null, boomSpriteSrc);\n\n _k.kaSprite = kaSprite;\n _k.boomSprite = boomSprite;\n\n function fixedUpdateFrame() {\n // update every obj\n game.root.fixedUpdate();\n }\n\n function updateFrame() {\n // update every obj\n game.root.update();\n }\n\n class Collision {\n source: GameObj;\n target: GameObj;\n normal: Vec2;\n distance: number;\n resolved: boolean = false;\n constructor(\n source: GameObj,\n target: GameObj,\n normal: Vec2,\n distance: number,\n resolved = false,\n ) {\n this.source = source;\n this.target = target;\n this.normal = normal;\n this.distance = distance;\n this.resolved = resolved;\n }\n get displacement() {\n return this.normal.scale(this.distance);\n }\n reverse() {\n return new Collision(\n this.target,\n this.source,\n this.normal.scale(-1),\n this.distance,\n this.resolved,\n );\n }\n hasOverlap() {\n return !this.displacement.isZero();\n }\n isLeft() {\n return this.displacement.cross(game.gravity || vec2(0, 1)) > 0;\n }\n isRight() {\n return this.displacement.cross(game.gravity || vec2(0, 1)) < 0;\n }\n isTop() {\n return this.displacement.dot(game.gravity || vec2(0, 1)) > 0;\n }\n isBottom() {\n return this.displacement.dot(game.gravity || vec2(0, 1)) < 0;\n }\n preventResolution() {\n this.resolved = true;\n }\n }\n\n function checkFrame() {\n if (!usesArea()) {\n return;\n }\n\n // TODO: persistent grid?\n // start a spatial hash grid for more efficient collision detection\n const grid: Record<number, Record<number, GameObj<AreaComp>[]>> = {};\n const cellSize = gopt.hashGridSize || DEF_HASH_GRID_SIZE;\n\n // current transform\n let tr = new Mat4();\n\n // a local transform stack\n const stack: any[] = [];\n\n function checkObj(obj: GameObj) {\n stack.push(tr.clone());\n\n // Update object transform here. This will be the transform later used in rendering.\n if (obj.pos) tr.translate(obj.pos);\n if (obj.scale) tr.scale(obj.scale);\n if (obj.angle) tr.rotate(obj.angle);\n obj.transform = tr.clone();\n\n if (obj.c(\"area\") && !obj.paused) {\n // TODO: only update worldArea if transform changed\n const aobj = obj as GameObj<AreaComp>;\n const area = aobj.worldArea();\n const bbox = area.bbox();\n\n // Get spatial hash grid coverage\n const xmin = Math.floor(bbox.pos.x / cellSize);\n const ymin = Math.floor(bbox.pos.y / cellSize);\n const xmax = Math.ceil((bbox.pos.x + bbox.width) / cellSize);\n const ymax = Math.ceil((bbox.pos.y + bbox.height) / cellSize);\n\n // Cache objs that are already checked\n const checked = new Set();\n\n // insert & check against all covered grids\n for (let x = xmin; x <= xmax; x++) {\n for (let y = ymin; y <= ymax; y++) {\n if (!grid[x]) {\n grid[x] = {};\n grid[x][y] = [aobj];\n }\n else if (!grid[x][y]) {\n grid[x][y] = [aobj];\n }\n else {\n const cell = grid[x][y];\n check: for (const other of cell) {\n if (other.paused) continue;\n if (!other.exists()) continue;\n if (checked.has(other.id)) continue;\n for (const tag of aobj.collisionIgnore) {\n if (other.is(tag)) {\n continue check;\n }\n }\n for (const tag of other.collisionIgnore) {\n if (aobj.is(tag)) {\n continue check;\n }\n }\n // TODO: cache the world area here\n const res = sat(\n aobj.worldArea(),\n other.worldArea(),\n );\n if (res) {\n // TODO: rehash if the object position is changed after resolution?\n const col1 = new Collision(\n aobj,\n other,\n res.normal,\n res.distance,\n );\n aobj.trigger(\"collideUpdate\", other, col1);\n const col2 = col1.reverse();\n // resolution only has to happen once\n col2.resolved = col1.resolved;\n other.trigger(\"collideUpdate\", aobj, col2);\n }\n checked.add(other.id);\n }\n cell.push(aobj);\n }\n }\n }\n }\n\n obj.children.forEach(checkObj);\n tr = stack.pop();\n }\n\n checkObj(game.root);\n }\n\n function handleErr(err: Error) {\n console.error(err);\n audio.ctx.suspend();\n const errorMessage = err.message ?? String(err)\n ?? \"Unknown error, check console for more info\";\n\n // TODO: this should only run once\n app.run(\n () => {},\n () => {\n frameStart();\n\n drawUnscaled(() => {\n const pad = 32;\n const gap = 16;\n const gw = width();\n const gh = height();\n\n const textStyle = {\n size: 36,\n width: gw - pad * 2,\n letterSpacing: 4,\n lineSpacing: 4,\n font: DBG_FONT,\n fixed: true,\n };\n\n drawRect({\n width: gw,\n height: gh,\n color: rgb(0, 0, 255),\n fixed: true,\n });\n\n const title = formatText({\n ...textStyle,\n text: \"Error\",\n pos: vec2(pad),\n color: rgb(255, 128, 0),\n fixed: true,\n });\n\n drawFormattedText(title);\n\n drawText({\n ...textStyle,\n text: errorMessage,\n pos: vec2(pad, pad + title.height + gap),\n fixed: true,\n });\n\n popTransform();\n game.events.trigger(\"error\", err);\n });\n\n frameEnd();\n },\n );\n }\n\n function onCleanup(action: () => void) {\n gc.push(action);\n }\n\n function quit() {\n game.events.onOnce(\"frameEnd\", () => {\n app.quit();\n\n // clear canvas\n gl.clear(\n gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT\n | gl.STENCIL_BUFFER_BIT,\n );\n\n // unbind everything\n const numTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);\n\n for (let unit = 0; unit < numTextureUnits; unit++) {\n gl.activeTexture(gl.TEXTURE0 + unit);\n gl.bindTexture(gl.TEXTURE_2D, null);\n gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);\n }\n\n gl.bindBuffer(gl.ARRAY_BUFFER, null);\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);\n gl.bindRenderbuffer(gl.RENDERBUFFER, null);\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n\n // run all scattered gc events\n ggl.destroy();\n gc.forEach((f) => f());\n });\n }\n\n let isFirstFrame = true;\n\n // main game loop\n app.run(\n () => {\n try {\n if (assets.loaded) {\n if (!debug.paused) fixedUpdateFrame();\n checkFrame();\n }\n } catch (e) {\n handleErr(e as Error);\n }\n },\n (processInput, resetInput) => {\n try {\n processInput();\n\n if (!assets.loaded) {\n if (loadProgress() === 1 && !isFirstFrame) {\n assets.loaded = true;\n getFailedAssets().forEach(details =>\n game.events.trigger(\"loadError\", ...details)\n );\n game.events.trigger(\"load\");\n }\n }\n\n if (\n !assets.loaded && gopt.loadingScreen !== false\n || isFirstFrame\n ) {\n frameStart();\n // TODO: Currently if assets are not initially loaded no updates or timers will be run, however they will run if loadingScreen is set to false. What's the desired behavior or should we make them consistent?\n drawLoadScreen();\n frameEnd();\n }\n else {\n if (!debug.paused) updateFrame();\n checkFrame();\n frameStart();\n drawFrame();\n if (gopt.debug !== false) drawDebug();\n frameEnd();\n }\n\n if (isFirstFrame) {\n isFirstFrame = false;\n }\n\n game.events.trigger(\"frameEnd\");\n\n resetInput();\n } catch (e) {\n handleErr(e as Error);\n }\n },\n );\n\n updateViewport();\n initEvents();\n\n // the exported ctx handle\n const ctx: KAPLAYCtx = {\n _k,\n VERSION,\n // asset load\n loadRoot,\n loadProgress,\n loadSprite,\n loadSpriteAtlas,\n loadSound,\n loadMusic,\n loadBitmapFont,\n loadFont,\n loadShader,\n loadShaderURL,\n loadAseprite,\n loadPedit,\n loadBean,\n loadJSON,\n load,\n getSound,\n getFont,\n getBitmapFont,\n getSprite,\n getShader,\n getAsset,\n Asset,\n SpriteData,\n SoundData,\n // query\n width,\n height,\n center,\n dt,\n fixedDt,\n restDt,\n time: app.time,\n screenshot: app.screenshot,\n record,\n isFocused,\n setCursor: app.setCursor,\n getCursor: app.getCursor,\n setCursorLocked: app.setCursorLocked,\n isCursorLocked: app.isCursorLocked,\n setFullscreen: app.setFullscreen,\n isFullscreen: app.isFullscreen,\n isTouchscreen: app.isTouchscreen,\n onLoad,\n onLoadError,\n onLoading,\n onResize,\n onGamepadConnect: app.onGamepadConnect,\n onGamepadDisconnect: app.onGamepadDisconnect,\n onError,\n onCleanup,\n // misc\n flash: flash,\n setCamPos: setCamPos,\n getCamPos: getCamPos,\n setCamRot: setCamRot,\n getCamRot: getCamRot,\n setCamScale: setCamScale,\n getCamScale: getCamScale,\n getCamTransform: getCamTransform,\n camPos,\n camScale,\n camFlash,\n camRot,\n camTransform,\n shake,\n toScreen,\n toWorld,\n setGravity,\n getGravity,\n setGravityDirection,\n getGravityDirection,\n setBackground,\n getBackground,\n getGamepads: app.getGamepads,\n // obj\n getTreeRoot,\n add,\n make,\n destroy,\n destroyAll,\n get,\n query,\n readd,\n // comps\n pos,\n scale,\n rotate,\n color,\n opacity,\n anchor,\n area,\n sprite,\n text,\n polygon,\n rect,\n circle,\n uvquad,\n outline,\n particles,\n body,\n platformEffector: platformEffector,\n surfaceEffector,\n areaEffector,\n pointEffector,\n buoyancyEffector,\n constantForce,\n doubleJump,\n shader,\n textInput,\n timer,\n fixed,\n stay,\n health,\n lifespan,\n named,\n state,\n z,\n layer,\n move,\n offscreen,\n follow,\n fadeIn,\n mask,\n drawon,\n raycast,\n tile,\n animate,\n serializeAnimation,\n agent,\n sentry,\n patrol,\n pathfinder,\n // group events\n trigger,\n on: on as KAPLAYCtx[\"on\"], // our internal on should be strict, user shouldn't\n onFixedUpdate,\n onUpdate,\n onDraw,\n onAdd,\n onDestroy,\n onTag,\n onUntag,\n onUse,\n onUnuse,\n onClick,\n onCollide,\n onCollideUpdate,\n onCollideEnd,\n onHover,\n onHoverUpdate,\n onHoverEnd,\n // input\n onKeyDown: app.onKeyDown,\n onKeyPress: app.onKeyPress,\n onKeyPressRepeat: app.onKeyPressRepeat,\n onKeyRelease: app.onKeyRelease,\n onMouseDown: app.onMouseDown,\n onMousePress: app.onMousePress,\n onMouseRelease: app.onMouseRelease,\n onMouseMove: app.onMouseMove,\n onCharInput: app.onCharInput,\n onTouchStart: app.onTouchStart,\n onTouchMove: app.onTouchMove,\n onTouchEnd: app.onTouchEnd,\n onScroll: app.onScroll,\n onHide: app.onHide,\n onShow: app.onShow,\n onGamepadButtonDown: app.onGamepadButtonDown,\n onGamepadButtonPress: app.onGamepadButtonPress,\n onGamepadButtonRelease: app.onGamepadButtonRelease,\n onGamepadStick: app.onGamepadStick,\n onButtonPress: app.onButtonPress,\n onButtonDown: app.onButtonDown,\n onButtonRelease: app.onButtonRelease,\n mousePos: app.mousePos,\n mouseDeltaPos: app.mouseDeltaPos,\n isKeyDown: app.isKeyDown,\n isKeyPressed: app.isKeyPressed,\n isKeyPressedRepeat: app.isKeyPressedRepeat,\n isKeyReleased: app.isKeyReleased,\n isMouseDown: app.isMouseDown,\n isMousePressed: app.isMousePressed,\n isMouseReleased: app.isMouseReleased,\n isMouseMoved: app.isMouseMoved,\n isGamepadButtonPressed: app.isGamepadButtonPressed,\n isGamepadButtonDown: app.isGamepadButtonDown,\n isGamepadButtonReleased: app.isGamepadButtonReleased,\n getGamepadStick: app.getGamepadStick,\n isButtonPressed: app.isButtonPressed,\n isButtonDown: app.isButtonDown,\n isButtonReleased: app.isButtonReleased,\n setButton: app.setButton,\n getButton: app.getButton,\n pressButton: app.pressButton,\n releaseButton: app.releaseButton,\n getLastInputDeviceType: app.getLastInputDeviceType,\n charInputted: app.charInputted,\n // timer\n loop,\n wait,\n // audio\n play,\n setVolume: setVolume,\n getVolume: getVolume,\n volume,\n burp,\n audioCtx: audio.ctx,\n // math\n Line,\n Rect,\n Circle,\n Ellipse,\n Point,\n Polygon,\n Vec2,\n Color,\n Mat4,\n Quad,\n RNG,\n rand,\n randi,\n randSeed,\n vec2,\n rgb,\n hsl2rgb,\n quad,\n choose,\n chooseMultiple,\n shuffle,\n chance,\n lerp,\n tween,\n easings,\n map,\n mapc,\n wave,\n deg2rad,\n rad2deg,\n clamp,\n evaluateQuadratic,\n evaluateQuadraticFirstDerivative,\n evaluateQuadraticSecondDerivative,\n evaluateBezier,\n evaluateBezierFirstDerivative,\n evaluateBezierSecondDerivative,\n evaluateCatmullRom,\n evaluateCatmullRomFirstDerivative,\n curveLengthApproximation,\n normalizedCurve,\n hermite,\n cardinal,\n catmullRom,\n bezier,\n kochanekBartels,\n easingSteps,\n easingLinear,\n easingCubicBezier,\n testLineLine,\n testRectRect,\n testRectLine,\n testRectPoint,\n testCirclePolygon,\n testLinePoint,\n testLineCircle,\n isConvex,\n triangulate,\n NavMesh,\n // raw draw\n drawSprite,\n drawText,\n formatText,\n drawRect,\n drawLine,\n drawLines,\n drawTriangle,\n drawCircle,\n drawEllipse,\n drawUVQuad,\n drawPolygon,\n drawCurve,\n drawBezier,\n drawFormattedText,\n drawMasked,\n drawSubtracted,\n pushTransform,\n popTransform,\n pushTranslate,\n pushScale,\n pushRotate,\n pushMatrix,\n usePostEffect,\n makeCanvas,\n // debug\n debug,\n // scene\n scene,\n getSceneName,\n go,\n onSceneLeave,\n // layers\n layers: layers,\n getLayers: getLayers,\n setLayers: setLayers,\n getDefaultLayer: getDefaultLayer,\n // level\n addLevel,\n // storage\n getData,\n setData,\n download,\n downloadJSON,\n downloadText,\n downloadBlob,\n // plugin\n plug,\n // char sets\n ASCII_CHARS,\n // dom\n canvas: app.canvas,\n // misc\n addKaboom,\n // dirs\n LEFT: Vec2.LEFT,\n RIGHT: Vec2.RIGHT,\n UP: Vec2.UP,\n DOWN: Vec2.DOWN,\n // colors\n RED: Color.RED,\n GREEN: Color.GREEN,\n BLUE: Color.BLUE,\n YELLOW: Color.YELLOW,\n MAGENTA: Color.MAGENTA,\n CYAN: Color.CYAN,\n WHITE: Color.WHITE,\n BLACK: Color.BLACK,\n quit,\n // helpers\n KEvent,\n KEventHandler,\n KEventController,\n cancel: () => EVENT_CANCEL_SYMBOL,\n };\n\n _k.k = ctx;\n\n const plugins = gopt.plugins as KAPLAYPlugin<Record<string, unknown>>[];\n\n if (plugins) {\n plugins.forEach(plug);\n }\n\n // export everything to window if global is set\n if (gopt.global !== false) {\n for (const key in ctx) {\n (<any> window[<any> key]) = ctx[key as keyof KAPLAYCtx];\n }\n }\n\n if (gopt.focus !== false) {\n app.canvas.focus();\n }\n\n return ctx as unknown as TPlugins extends [undefined]\n ? KAPLAYCtx<TButtons, TButtonsName>\n : KAPLAYCtx<TButtons, TButtonsName> & MergePlugins<TPlugins>;\n};\n\nexport { kaplay };\nexport default kaplay;\n", "{\n \"name\": \"kaplay\",\n \"description\": \"KAPLAY is a JavaScript & TypeScript game library that helps you make games fast and fun! (formerly known as Kaboom.js)\",\n \"version\": \"3001.0.19\",\n \"license\": \"MIT\",\n \"homepage\": \"https://kaplayjs.com/\",\n \"bugs\": {\n \"url\": \"https://github.com/kaplayjs/kaplay/issues\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/kaplay\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/kaplayjs/kaplay.git\"\n },\n \"type\": \"module\",\n \"main\": \"./dist/kaplay.cjs\",\n \"module\": \"./dist/kaplay.mjs\",\n \"types\": \"./dist/doc.d.ts\",\n \"readme\": \"./README.md\",\n \"exports\": {\n \".\": {\n \"import\": {\n \"types\": \"./dist/doc.d.ts\",\n \"default\": \"./dist/kaplay.mjs\"\n },\n \"require\": {\n \"types\": \"./dist/doc.d.ts\",\n \"default\": \"./dist/kaplay.cjs\"\n }\n },\n \"./global\": \"./dist/declaration/global.js\"\n },\n \"typesVersions\": {\n \"*\": {\n \"global\": [\n \"./dist/declaration/global.d.ts\"\n ]\n }\n },\n \"keywords\": [\n \"game development\",\n \"javascript\",\n \"typescript\",\n \"game engine\",\n \"2d games\",\n \"physics engine\",\n \"webgl\",\n \"canvas\",\n \"game library\",\n \"kaplay\",\n \"kaboom\",\n \"kaboomjs\",\n \"kaboom.js\"\n ],\n \"files\": [\n \"dist/\",\n \"kaplay.webp\",\n \"CHANGELOG.md\"\n ],\n \"scripts\": {\n \"dev\": \"NODE_ENV=development node scripts/dev.js\",\n \"win:dev\": \"set NODE_ENV=development && node scripts/dev.js\",\n \"build\": \"node scripts/generateIndex.js && npm run doc-dts && node scripts/build.js\",\n \"build:fast\": \"node scripts/buildFast.js\",\n \"check\": \"tsc\",\n \"fmt\": \"dprint fmt\",\n \"test\": \"node scripts/test.js\",\n \"doc-dts\": \"dts-bundle-generator -o dist/doc.d.ts src/index.ts\",\n \"test:vite\": \"vitest --typecheck\",\n \"desktop\": \"tauri dev\",\n \"prepare\": \"npm run build\",\n \"publish:next\": \"npm publish --tag next\"\n },\n \"devDependencies\": {\n \"@kaplayjs/dprint-config\": \"^1.2.0\",\n \"@types/jest\": \"^29.5.14\",\n \"dprint\": \"^0.49.1\",\n \"dts-bundle-generator\": \"^9.5.1\",\n \"ejs\": \"^3.1.10\",\n \"esbuild\": \"^0.25.2\",\n \"express\": \"^5.1.0\",\n \"puppeteer\": \"^22.15.0\",\n \"tar-fs\": \"3.0.8\",\n \"typescript\": \"5.6.3\",\n \"vite\": \"5.4.16\",\n \"vitest\": \"^3.1.1\",\n \"vitest-environment-puppeteer\": \"^11.0.3\",\n \"vitest-puppeteer\": \"^11.0.3\"\n },\n \"engines\": {\n \"node\": \">=20.0.0\"\n },\n \"packageManager\": \"pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1\"\n}\n", "export function clamp(\n val: number,\n min: number,\n max: number,\n): number {\n if (min > max) {\n return clamp(val, max, min);\n }\n return Math.min(Math.max(val, min), max);\n}\n", "import { clamp } from \"./clamp.js\";\nimport { lerp } from \"./math\";\n\nexport type RGBValue = [number, number, number];\nexport type RGBAValue = [number, number, number, number];\n\n/**\n * 0-255 RGBA color.\n *\n * @group Math\n */\nexport class Color {\n /** Red (0-255. */\n r: number = 255;\n /** Green (0-255). */\n g: number = 255;\n /** Blue (0-255). */\n b: number = 255;\n\n constructor(r: number, g: number, b: number) {\n this.r = clamp(r, 0, 255);\n this.g = clamp(g, 0, 255);\n this.b = clamp(b, 0, 255);\n }\n\n // TODO: Type arr as tuple (no in ts-strict branch yet)\n static fromArray(arr: number[]) {\n return new Color(arr[0], arr[1], arr[2]);\n }\n\n /**\n * Create color from hex string or literal.\n *\n * @example\n * ```js\n * Color.fromHex(0xfcef8d)\n * Color.fromHex(\"#5ba675\")\n * Color.fromHex(\"d46eb3\")\n * ```\n *\n * @since v3000.0\n */\n static fromHex(hex: string | number) {\n if (typeof hex === \"number\") {\n return new Color(\n (hex >> 16) & 0xff,\n (hex >> 8) & 0xff,\n (hex >> 0) & 0xff,\n );\n }\n else if (typeof hex === \"string\") {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(\n hex,\n );\n\n if (!result) throw new Error(\"Invalid hex color format\");\n\n return new Color(\n parseInt(result[1], 16),\n parseInt(result[2], 16),\n parseInt(result[3], 16),\n );\n }\n else {\n throw new Error(\"Invalid hex color format\");\n }\n }\n\n // TODO: use range of [0, 360] [0, 100] [0, 100]?\n static fromHSL(h: number, s: number, l: number) {\n if (s == 0) {\n return new Color(255 * l, 255 * l, 255 * l);\n }\n\n const hue2rgb = (p: number, q: number, t: number) => {\n if (t < 0) t += 1;\n if (t > 1) t -= 1;\n if (t < 1 / 6) return p + (q - p) * 6 * t;\n if (t < 1 / 2) return q;\n if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;\n return p;\n };\n\n const q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n const p = 2 * l - q;\n const r = hue2rgb(p, q, h + 1 / 3);\n const g = hue2rgb(p, q, h);\n const b = hue2rgb(p, q, h - 1 / 3);\n\n return new Color(\n Math.round(r * 255),\n Math.round(g * 255),\n Math.round(b * 255),\n );\n }\n\n static RED = new Color(255, 0, 0);\n static GREEN = new Color(0, 255, 0);\n static BLUE = new Color(0, 0, 255);\n static YELLOW = new Color(255, 255, 0);\n static MAGENTA = new Color(255, 0, 255);\n static CYAN = new Color(0, 255, 255);\n static WHITE = new Color(255, 255, 255);\n static BLACK = new Color(0, 0, 0);\n\n clone(): Color {\n return new Color(this.r, this.g, this.b);\n }\n\n /** Lighten the color (adds RGB by n). */\n lighten(a: number): Color {\n return new Color(this.r + a, this.g + a, this.b + a);\n }\n\n /** Darkens the color (subtracts RGB by n). */\n darken(a: number): Color {\n return this.lighten(-a);\n }\n\n invert(): Color {\n return new Color(255 - this.r, 255 - this.g, 255 - this.b);\n }\n\n mult(other: Color): Color {\n return new Color(\n this.r * other.r / 255,\n this.g * other.g / 255,\n this.b * other.b / 255,\n );\n }\n\n /**\n * Linear interpolate to a destination color.\n *\n * @since v3000.0\n */\n lerp(dest: Color, t: number): Color {\n return new Color(\n lerp(this.r, dest.r, t),\n lerp(this.g, dest.g, t),\n lerp(this.b, dest.b, t),\n );\n }\n\n /**\n * Convert color into HSL format.\n *\n * @since v3001.0\n */\n toHSL(): [number, number, number] {\n const r = this.r / 255;\n const g = this.g / 255;\n const b = this.b / 255;\n const max = Math.max(r, g, b), min = Math.min(r, g, b);\n let h = (max + min) / 2;\n let s = h;\n const l = h;\n if (max == min) {\n h = s = 0;\