├── examples ├── objectInit.js ├── fps.js ├── fonts │ ├── 4x4.png │ ├── zpix.ttf │ ├── 04b03.ttf │ ├── apl386.ttf │ ├── cga_8x8.png │ ├── 04b03_6x8.png │ ├── sink_6x8.png │ ├── Romantique.ttf │ ├── happy_28x36.png │ ├── proggy_7x13.png │ ├── unscii_8x8.png │ ├── Anton-Regular.ttf │ ├── FlowerSketches.ttf │ └── Nabla-Regular-VariableFont_EDPT,EHLT.ttf ├── sounds │ ├── bug.mp3 │ ├── hit.mp3 │ ├── off.mp3 │ ├── bell.mp3 │ ├── blip.mp3 │ ├── burp.mp3 │ ├── danger.mp3 │ ├── dune.mp3 │ ├── error.mp3 │ ├── knock.ogg │ ├── mystic.mp3 │ ├── notice.mp3 │ ├── portal.mp3 │ ├── robot.mp3 │ ├── score.mp3 │ ├── shoot.mp3 │ ├── signal.mp3 │ ├── spring.mp3 │ ├── weak.mp3 │ ├── wooosh.mp3 │ ├── computer.mp3 │ ├── explode.mp3 │ ├── powerup.mp3 │ ├── bean_voice.wav │ ├── kaboom2000.mp3 │ ├── mark_voice.wav │ └── OtherworldlyFoe.mp3 ├── sprites │ ├── k.png │ ├── ka.png │ ├── YOSHI.png │ ├── apple.png │ ├── bag.png │ ├── bean.png │ ├── bobo.png │ ├── boom.png │ ├── btfly.png │ ├── cloud.png │ ├── coin.png │ ├── dino.png │ ├── door.png │ ├── egg.png │ ├── grab.png │ ├── grape.png │ ├── grass.png │ ├── gun.png │ ├── heart.png │ ├── jumpy.png │ ├── key.png │ ├── mark.png │ ├── meat.png │ ├── moon.png │ ├── note.png │ ├── spike.png │ ├── steel.png │ ├── sun.png │ ├── sword.png │ ├── tga.png │ ├── you.png │ ├── 9slice.png │ ├── dungeon.png │ ├── ghosty.png │ ├── kaboom.png │ ├── portal.png │ ├── zombean.png │ ├── brick_wall.png │ ├── egg_crack.png │ ├── ghostiny.png │ ├── lightening.png │ ├── mushroom.png │ ├── pineapple.png │ ├── watermelon.png │ ├── dungeon-dino.png │ ├── gigagantrum.png │ ├── kaplay-dino.png │ ├── cursor_default.png │ ├── cursor_pointer.png │ ├── spritemerge_chest.png │ ├── particle_star_filled.png │ ├── spritemerge_corpus.png │ ├── particle_circle_filled.png │ ├── particle_circle_outline.png │ └── particle_hexagon_filled.png ├── videos │ ├── 3d.mp4 │ └── dance.mp4 ├── shaders │ ├── invert.frag │ ├── pixelate.frag │ ├── light.frag │ ├── blink.frag │ └── crt.frag ├── scaletest.js ├── kaboom.js ├── tiled.js ├── tsconfig.json ├── onLoadError.js ├── timer.js ├── patrol.js ├── size.js ├── layer.js ├── constraintsflip.js ├── burp.js ├── basicsStart.js ├── bbcode.js ├── frames.js ├── customCompDebug.js ├── slice9.js ├── retrieve.js ├── basicsGlobals.js ├── fadeIn.js ├── video.js ├── children.js ├── particleTrail.js ├── drawon.js ├── automaticCollider.js ├── layers.js ├── friction.js ├── shader.js ├── restitution.js ├── lifespan.js ├── shapeRect.js ├── drawoncanvas.js ├── polygongeneration.js ├── livequery.js ├── levelRaycast.js ├── particle.js ├── gamepadMulti.js ├── collisionshapes.js ├── movement.js ├── picture.js ├── out.js ├── basicsObject.js ├── textInput.js ├── gamepad.js ├── hover.js ├── basicEventsObject.js ├── basicsCompRender.js ├── rebinding.js ├── pong.js ├── tweenEasings.js └── gravity.js ├── kaplay.webp ├── dprint.json ├── scripts ├── dev.js ├── buildFast.js ├── build.js ├── constants.js ├── git-split.sh └── dev │ └── dev.js ├── src ├── data │ └── assets │ │ ├── ka.png │ │ ├── bean.png │ │ ├── boom.png │ │ ├── burp.mp3 │ │ ├── happy.png │ │ └── index.d.ts ├── utils │ ├── numbers.ts │ ├── benchmark.ts │ ├── asserts.ts │ ├── sets.ts │ ├── log.ts │ ├── deepEq.ts │ ├── types.ts │ ├── dataURL.ts │ └── overload.ts ├── math │ ├── lerpNumber.ts │ ├── clamp.ts │ ├── sort.ts │ ├── spatial │ │ └── index.ts │ ├── lerp.ts │ ├── vec3.ts │ └── minkowski.ts ├── shared.ts ├── gfx │ ├── draw │ │ ├── drawMasked.ts │ │ ├── drawSubstracted.ts │ │ ├── drawUnscaled.ts │ │ ├── drawCanvas.ts │ │ ├── drawBezier.ts │ │ ├── drawCurve.ts │ │ ├── drawFrame.ts │ │ ├── drawStenciled.ts │ │ ├── drawTriangle.ts │ │ ├── drawLoadingScreen.ts │ │ ├── drawCircle.ts │ │ └── drawInspectText.ts │ ├── bg.ts │ ├── canvasBuffer.ts │ └── anchor.ts ├── .env.d.ts ├── audio │ ├── burp.ts │ ├── volume.ts │ └── audio.ts ├── game │ ├── utils.ts │ ├── layers.ts │ └── gravity.ts ├── assets │ └── utils.ts ├── app │ ├── data.ts │ └── buttons.ts ├── core │ ├── fontCache.ts │ ├── plug.ts │ └── quit.ts ├── constants │ └── math.ts └── ecs │ ├── entity │ ├── utils.ts │ └── premade │ │ └── addLevel.ts │ ├── components │ ├── misc │ │ ├── boom.ts │ │ ├── named.ts │ │ ├── stay.ts │ │ └── lifespan.ts │ ├── draw │ │ ├── mask.ts │ │ ├── fadeIn.ts │ │ ├── picture.ts │ │ ├── raycast.ts │ │ ├── color.ts │ │ ├── drawon.ts │ │ ├── blend.ts │ │ └── shader.ts │ └── transform │ │ ├── fixed.ts │ │ ├── z.ts │ │ ├── follow.ts │ │ ├── move.ts │ │ └── anchor.ts │ └── systems │ └── systems.ts ├── tests ├── playtests │ ├── errorWithBrackets.js │ ├── invalidLayer.js │ ├── existsNested.js │ ├── jsconfig.json │ ├── mouseStretch.js │ ├── area_add_race.js │ ├── keyandkeycode.js │ ├── mouseScaleStretch.js │ ├── twokaplays.js │ ├── mouseLetterbox.js │ ├── mouseLetterbox2.js │ ├── mouse.js │ ├── mouseScale.js │ ├── mouseScaleLetterbox.js │ ├── mouseScaleLetterbox2.js │ ├── slice9zero.js │ ├── timeLeft.js │ ├── textPaused.js │ ├── colorCSS.js │ ├── prefab.js │ ├── compsMissing.js │ ├── insertionSort.js │ ├── wait1.js │ ├── compsMissingDynamic.js │ ├── raycastLevelTest.js │ ├── obj_dependencies_in_add_hook.js │ ├── debugTimeScale.js │ ├── sprite_anim_onend_restart.js │ ├── testnewmultibutton.js │ ├── prettyDebug.js │ ├── test536.js │ ├── paused.js │ ├── textBig.js │ ├── spriteAsFont.js │ ├── textWeirdTags.js │ ├── textItalic.js │ ├── textShader.js │ ├── textWrap.js │ ├── laglevel.js │ ├── hover.js │ ├── largeTexture.js │ ├── textTall.js │ ├── textStyleChangeFont.js │ ├── fastLoop.js │ ├── textStyleOverride.js │ ├── debugFramePhysics.js │ ├── parenttest.js │ ├── fixedUpdate.js │ └── scenepushandpop.js ├── .env.d.ts ├── tsconfig.json ├── types │ ├── plugins.test-d.ts │ ├── tafButtons.test-d.ts │ └── tafScenes.test-d.ts └── auto │ ├── kaplay.spec.ts │ ├── missingComps.spec.ts │ ├── plugin.spec.ts │ └── color.spec.ts ├── .gitignore ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feat.yml │ └── bug.yml ├── dependabot.yml ├── FUNDING.yml └── workflows │ ├── publish.yml │ └── sync-playground.yml ├── .vscode └── settings.json ├── vitest.config.ts ├── tsconfig.build.json ├── tsconfig.json ├── LICENSE ├── eslint.config.mjs └── help.json /examples/objectInit.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kaplay.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/kaplay.webp -------------------------------------------------------------------------------- /examples/fps.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | maxFPS: 70, 3 | }); 4 | 5 | debug.inspect = true; 6 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "node_modules/@kaplayjs/dprint-config/dprint.json" 3 | } 4 | -------------------------------------------------------------------------------- /examples/fonts/4x4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/4x4.png -------------------------------------------------------------------------------- /examples/fonts/zpix.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/zpix.ttf -------------------------------------------------------------------------------- /examples/sounds/bug.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/bug.mp3 -------------------------------------------------------------------------------- /examples/sounds/hit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/hit.mp3 -------------------------------------------------------------------------------- /examples/sounds/off.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/off.mp3 -------------------------------------------------------------------------------- /examples/sprites/k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/k.png -------------------------------------------------------------------------------- /examples/sprites/ka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/ka.png -------------------------------------------------------------------------------- /examples/videos/3d.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/videos/3d.mp4 -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { dev } from "./dev/dev.js"; 4 | 5 | await dev(); 6 | -------------------------------------------------------------------------------- /src/data/assets/ka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/src/data/assets/ka.png -------------------------------------------------------------------------------- /examples/fonts/04b03.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/04b03.ttf -------------------------------------------------------------------------------- /examples/fonts/apl386.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/apl386.ttf -------------------------------------------------------------------------------- /examples/fonts/cga_8x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/cga_8x8.png -------------------------------------------------------------------------------- /examples/sounds/bell.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/bell.mp3 -------------------------------------------------------------------------------- /examples/sounds/blip.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/blip.mp3 -------------------------------------------------------------------------------- /examples/sounds/burp.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/burp.mp3 -------------------------------------------------------------------------------- /examples/sounds/danger.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/danger.mp3 -------------------------------------------------------------------------------- /examples/sounds/dune.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/dune.mp3 -------------------------------------------------------------------------------- /examples/sounds/error.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/error.mp3 -------------------------------------------------------------------------------- /examples/sounds/knock.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/knock.ogg -------------------------------------------------------------------------------- /examples/sounds/mystic.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/mystic.mp3 -------------------------------------------------------------------------------- /examples/sounds/notice.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/notice.mp3 -------------------------------------------------------------------------------- /examples/sounds/portal.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/portal.mp3 -------------------------------------------------------------------------------- /examples/sounds/robot.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/robot.mp3 -------------------------------------------------------------------------------- /examples/sounds/score.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/score.mp3 -------------------------------------------------------------------------------- /examples/sounds/shoot.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/shoot.mp3 -------------------------------------------------------------------------------- /examples/sounds/signal.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/signal.mp3 -------------------------------------------------------------------------------- /examples/sounds/spring.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/spring.mp3 -------------------------------------------------------------------------------- /examples/sounds/weak.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/weak.mp3 -------------------------------------------------------------------------------- /examples/sounds/wooosh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/wooosh.mp3 -------------------------------------------------------------------------------- /examples/sprites/YOSHI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/YOSHI.png -------------------------------------------------------------------------------- /examples/sprites/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/apple.png -------------------------------------------------------------------------------- /examples/sprites/bag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/bag.png -------------------------------------------------------------------------------- /examples/sprites/bean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/bean.png -------------------------------------------------------------------------------- /examples/sprites/bobo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/bobo.png -------------------------------------------------------------------------------- /examples/sprites/boom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/boom.png -------------------------------------------------------------------------------- /examples/sprites/btfly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/btfly.png -------------------------------------------------------------------------------- /examples/sprites/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/cloud.png -------------------------------------------------------------------------------- /examples/sprites/coin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/coin.png -------------------------------------------------------------------------------- /examples/sprites/dino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/dino.png -------------------------------------------------------------------------------- /examples/sprites/door.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/door.png -------------------------------------------------------------------------------- /examples/sprites/egg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/egg.png -------------------------------------------------------------------------------- /examples/sprites/grab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/grab.png -------------------------------------------------------------------------------- /examples/sprites/grape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/grape.png -------------------------------------------------------------------------------- /examples/sprites/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/grass.png -------------------------------------------------------------------------------- /examples/sprites/gun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/gun.png -------------------------------------------------------------------------------- /examples/sprites/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/heart.png -------------------------------------------------------------------------------- /examples/sprites/jumpy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/jumpy.png -------------------------------------------------------------------------------- /examples/sprites/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/key.png -------------------------------------------------------------------------------- /examples/sprites/mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/mark.png -------------------------------------------------------------------------------- /examples/sprites/meat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/meat.png -------------------------------------------------------------------------------- /examples/sprites/moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/moon.png -------------------------------------------------------------------------------- /examples/sprites/note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/note.png -------------------------------------------------------------------------------- /examples/sprites/spike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/spike.png -------------------------------------------------------------------------------- /examples/sprites/steel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/steel.png -------------------------------------------------------------------------------- /examples/sprites/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/sun.png -------------------------------------------------------------------------------- /examples/sprites/sword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/sword.png -------------------------------------------------------------------------------- /examples/sprites/tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/tga.png -------------------------------------------------------------------------------- /examples/sprites/you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/you.png -------------------------------------------------------------------------------- /examples/videos/dance.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/videos/dance.mp4 -------------------------------------------------------------------------------- /src/data/assets/bean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/src/data/assets/bean.png -------------------------------------------------------------------------------- /src/data/assets/boom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/src/data/assets/boom.png -------------------------------------------------------------------------------- /src/data/assets/burp.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/src/data/assets/burp.mp3 -------------------------------------------------------------------------------- /src/data/assets/happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/src/data/assets/happy.png -------------------------------------------------------------------------------- /examples/fonts/04b03_6x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/04b03_6x8.png -------------------------------------------------------------------------------- /examples/fonts/sink_6x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/sink_6x8.png -------------------------------------------------------------------------------- /examples/sounds/computer.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/computer.mp3 -------------------------------------------------------------------------------- /examples/sounds/explode.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/explode.mp3 -------------------------------------------------------------------------------- /examples/sounds/powerup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/powerup.mp3 -------------------------------------------------------------------------------- /examples/sprites/9slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/9slice.png -------------------------------------------------------------------------------- /examples/sprites/dungeon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/dungeon.png -------------------------------------------------------------------------------- /examples/sprites/ghosty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/ghosty.png -------------------------------------------------------------------------------- /examples/sprites/kaboom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/kaboom.png -------------------------------------------------------------------------------- /examples/sprites/portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/portal.png -------------------------------------------------------------------------------- /examples/sprites/zombean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/zombean.png -------------------------------------------------------------------------------- /examples/fonts/Romantique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/Romantique.ttf -------------------------------------------------------------------------------- /examples/fonts/happy_28x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/happy_28x36.png -------------------------------------------------------------------------------- /examples/fonts/proggy_7x13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/proggy_7x13.png -------------------------------------------------------------------------------- /examples/fonts/unscii_8x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/unscii_8x8.png -------------------------------------------------------------------------------- /examples/sounds/bean_voice.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/bean_voice.wav -------------------------------------------------------------------------------- /examples/sounds/kaboom2000.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/kaboom2000.mp3 -------------------------------------------------------------------------------- /examples/sounds/mark_voice.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/mark_voice.wav -------------------------------------------------------------------------------- /examples/sprites/brick_wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/brick_wall.png -------------------------------------------------------------------------------- /examples/sprites/egg_crack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/egg_crack.png -------------------------------------------------------------------------------- /examples/sprites/ghostiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/ghostiny.png -------------------------------------------------------------------------------- /examples/sprites/lightening.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/lightening.png -------------------------------------------------------------------------------- /examples/sprites/mushroom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/mushroom.png -------------------------------------------------------------------------------- /examples/sprites/pineapple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/pineapple.png -------------------------------------------------------------------------------- /examples/sprites/watermelon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/watermelon.png -------------------------------------------------------------------------------- /examples/fonts/Anton-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/Anton-Regular.ttf -------------------------------------------------------------------------------- /examples/fonts/FlowerSketches.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/FlowerSketches.ttf -------------------------------------------------------------------------------- /examples/sprites/dungeon-dino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/dungeon-dino.png -------------------------------------------------------------------------------- /examples/sprites/gigagantrum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/gigagantrum.png -------------------------------------------------------------------------------- /examples/sprites/kaplay-dino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/kaplay-dino.png -------------------------------------------------------------------------------- /scripts/buildFast.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { build } from "./lib/build.js"; 4 | 5 | await build(true); 6 | -------------------------------------------------------------------------------- /src/utils/numbers.ts: -------------------------------------------------------------------------------- 1 | export function toFixed(n: number, f: number) { 2 | return Number(n.toFixed(f)); 3 | } 4 | -------------------------------------------------------------------------------- /tests/playtests/errorWithBrackets.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | onUpdate(() => { 3 | throw new Error("[blooey]"); 4 | }); 5 | -------------------------------------------------------------------------------- /examples/sounds/OtherworldlyFoe.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sounds/OtherworldlyFoe.mp3 -------------------------------------------------------------------------------- /examples/sprites/cursor_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/cursor_default.png -------------------------------------------------------------------------------- /examples/sprites/cursor_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/cursor_pointer.png -------------------------------------------------------------------------------- /examples/sprites/spritemerge_chest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/spritemerge_chest.png -------------------------------------------------------------------------------- /examples/sprites/particle_star_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/particle_star_filled.png -------------------------------------------------------------------------------- /examples/sprites/spritemerge_corpus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/spritemerge_corpus.png -------------------------------------------------------------------------------- /examples/sprites/particle_circle_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/particle_circle_filled.png -------------------------------------------------------------------------------- /examples/sprites/particle_circle_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/particle_circle_outline.png -------------------------------------------------------------------------------- /examples/sprites/particle_hexagon_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/sprites/particle_hexagon_filled.png -------------------------------------------------------------------------------- /tests/.env.d.ts: -------------------------------------------------------------------------------- 1 | import type { kaplay as KAPLAY } from "../src/kaplay"; 2 | 3 | declare global { 4 | const kaplay: typeof KAPLAY; 5 | } 6 | -------------------------------------------------------------------------------- /src/math/lerpNumber.ts: -------------------------------------------------------------------------------- 1 | export function lerpNumber( 2 | a: number, 3 | b: number, 4 | t: number, 5 | ) { 6 | return a + (b - a) * t; 7 | } 8 | -------------------------------------------------------------------------------- /examples/fonts/Nabla-Regular-VariableFont_EDPT,EHLT.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/HEAD/examples/fonts/Nabla-Regular-VariableFont_EDPT,EHLT.ttf -------------------------------------------------------------------------------- /tests/playtests/invalidLayer.js: -------------------------------------------------------------------------------- 1 | kaplay({ scale: 0.5 }); 2 | setLayers(["foo"], "foo"); 3 | debug.log( 4 | add([ 5 | layer("bar"), 6 | ]).layer, 7 | ); 8 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { build } from "./lib/build.js"; 4 | import { genGlobalDTS } from "./lib/globaldts.js"; 5 | 6 | await build(); 7 | await genGlobalDTS(); 8 | -------------------------------------------------------------------------------- /tests/playtests/existsNested.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | 3 | const x = add([]); 4 | const y = x.add([]); 5 | const z = y.add([]); 6 | x.destroy(); 7 | console.assert(!y.exists() && !z.exists()); 8 | -------------------------------------------------------------------------------- /scripts/constants.js: -------------------------------------------------------------------------------- 1 | export const DIST_DIR = "dist"; 2 | export const SRC_DIR = "src"; 3 | export const SRC_PATH = `${SRC_DIR}/kaplay.ts`; 4 | export const SRC_PATH_MINI = `${SRC_DIR}/modules/mini.ts`; 5 | -------------------------------------------------------------------------------- /tests/playtests/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "../../dist/declaration/global.d.ts" 5 | ], 6 | "lib": [ 7 | "ES2015" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | node_modules/ 4 | .idea/ 5 | desktop/ 6 | .env 7 | src-tauri/target/ 8 | .replit 9 | replit.nix 10 | /.obsidian 11 | .pnpm-store 12 | tsconfig.vitest-temp.json 13 | src/index.ts -------------------------------------------------------------------------------- /examples/shaders/invert.frag: -------------------------------------------------------------------------------- 1 | uniform float u_invert; 2 | 3 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 4 | vec4 c = def_frag(); 5 | return mix(c, vec4(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a), u_invert); 6 | } 7 | -------------------------------------------------------------------------------- /src/data/assets/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.mp3" { 2 | const value: Uint8Array; 3 | export default value; 4 | } 5 | 6 | declare module "*.png" { 7 | const value: string; 8 | export default value; 9 | } 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Please describe what issue(s) this PR fixes. 7 | 8 | ## Summary 9 | 10 | - [ ] Changeloged 11 | -------------------------------------------------------------------------------- /src/math/clamp.ts: -------------------------------------------------------------------------------- 1 | export const clamp = ( 2 | val: number, 3 | min: number, 4 | max: number, 5 | ): number => { 6 | if (min > max) { 7 | return clamp(val, max, min); 8 | } 9 | return Math.min(Math.max(val, min), max); 10 | }; 11 | -------------------------------------------------------------------------------- /src/shared.ts: -------------------------------------------------------------------------------- 1 | import type { Engine } from "./core/engine"; 2 | 3 | /** 4 | * KAPLAY.js internal data 5 | * 6 | * @ignore 7 | */ 8 | export let _k: Engine; 9 | 10 | /** @ignore */ 11 | export function updateEngine(e: Engine) { 12 | _k = e; 13 | } 14 | -------------------------------------------------------------------------------- /src/gfx/draw/drawMasked.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | import { drawStenciled } from "./drawStenciled"; 3 | 4 | export function drawMasked(content: () => void, mask: () => void) { 5 | const gl = _k.gfx.ggl.gl; 6 | 7 | drawStenciled(content, mask, gl.EQUAL); 8 | } 9 | -------------------------------------------------------------------------------- /src/gfx/draw/drawSubstracted.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | import { drawStenciled } from "./drawStenciled"; 3 | 4 | export function drawSubtracted(content: () => void, mask: () => void) { 5 | const gl = _k.gfx.ggl.gl; 6 | 7 | drawStenciled(content, mask, gl.NOTEQUAL); 8 | } 9 | -------------------------------------------------------------------------------- /src/.env.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module "*.mp3" { 7 | const value: Uint8Array; 8 | export default value; 9 | } 10 | 11 | interface Window { 12 | kaplayjs_assetsAliases: Record; 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.preferences.importModuleSpecifierEnding": "minimal", 4 | "typescript.preferences.importModuleSpecifier": "shortest", 5 | "cSpell.words": [ 6 | "KAPLAY" 7 | ], 8 | "dprint.path": "./node_modules/dprint/dprint" 9 | } 10 | -------------------------------------------------------------------------------- /tests/playtests/mouseStretch.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | 3 | const redDot = add([ 4 | anchor("center"), 5 | circle(3), 6 | color(RED), 7 | pos(), 8 | ]); 9 | 10 | onMouseMove(pos => { 11 | redDot.pos = toWorld(pos); 12 | }); 13 | 14 | onKeyPress("f", () => { 15 | setFullscreen(!isFullscreen()); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/playtests/area_add_race.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | 3 | onUpdate(() => { 4 | add([ 5 | pos(100, 100), 6 | rect(100, 100), 7 | // area(), 8 | { 9 | add() { 10 | this.use(area()); 11 | }, 12 | }, 13 | ]).destroy(); 14 | debug.log(debug.fps()); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/playtests/keyandkeycode.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | buttons: { 3 | foo: { 4 | keyboard: "q", 5 | keyboardCode: "KeyQ", 6 | }, 7 | }, 8 | }); 9 | 10 | onButtonPress("foo", () => { 11 | debug.log("ohhi"); 12 | }); 13 | 14 | onButtonRelease("foo", () => { 15 | debug.log("ohbye"); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/playtests/mouseScaleStretch.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | scale: 3, 3 | }); 4 | 5 | const redDot = add([ 6 | anchor("center"), 7 | circle(3), 8 | color(RED), 9 | pos(), 10 | ]); 11 | 12 | onMouseMove(pos => { 13 | redDot.pos = toWorld(pos); 14 | }); 15 | 16 | onKeyPress("f", () => { 17 | setFullscreen(!isFullscreen()); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/playtests/twokaplays.js: -------------------------------------------------------------------------------- 1 | const k = kaplay(); 2 | 3 | k.loadBean(); 4 | 5 | k.quit(); 6 | 7 | k.add([ 8 | text("hi"), 9 | ]); 10 | 11 | k.add([ 12 | sprite("bean"), 13 | ]); 14 | 15 | const k2 = kaplay(); 16 | 17 | k2.loadBean(); 18 | 19 | k2.add([ 20 | text("hi how are you?"), 21 | ]); 22 | 23 | k2.add([ 24 | sprite("bean"), 25 | ]); 26 | -------------------------------------------------------------------------------- /src/audio/burp.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import { type AudioPlay, type AudioPlayOpt, play } from "./play"; 3 | 4 | // core KAPLAY logic 5 | export function burp(opt?: AudioPlayOpt): AudioPlay { 6 | if (!_k.game.defaultAssets.burp) { 7 | throw new Error("You can't use burp in kaplay/mini"); 8 | } 9 | 10 | return play(_k.game.defaultAssets.burp, opt); 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Help 3 | url: https://github.com/kaplayjs/kaplay/discussions/new?category=q-a 4 | about: How to use certain kaplay feature, or how to achieve something with kaplay 5 | - name: Compiling/Contributing 6 | url: https://github.com/kaplayjs/kaplay/discussions/new?category=q-a 7 | about: How to compile kaplay/how to contribute to it 8 | -------------------------------------------------------------------------------- /tests/playtests/mouseLetterbox.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | width: 400, 3 | height: 200, 4 | letterbox: true, 5 | }); 6 | 7 | const redDot = add([ 8 | anchor("center"), 9 | circle(3), 10 | color(RED), 11 | pos(), 12 | ]); 13 | 14 | onMouseMove(pos => { 15 | redDot.pos = toWorld(pos); 16 | }); 17 | 18 | onKeyPress("f", () => { 19 | setFullscreen(!isFullscreen()); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/playtests/mouseLetterbox2.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | width: 200, 3 | height: 400, 4 | letterbox: true, 5 | }); 6 | 7 | const redDot = add([ 8 | anchor("center"), 9 | circle(3), 10 | color(RED), 11 | pos(), 12 | ]); 13 | 14 | onMouseMove(pos => { 15 | redDot.pos = toWorld(pos); 16 | }); 17 | 18 | onKeyPress("f", () => { 19 | setFullscreen(!isFullscreen()); 20 | }); 21 | -------------------------------------------------------------------------------- /examples/scaletest.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | letterbox: true, 3 | width: 640, 4 | height: 360, 5 | }); 6 | 7 | // load assets 8 | loadSprite("bean", "/sprites/bean.png"); 9 | 10 | onDraw(() => { 11 | drawSprite({ 12 | pos: vec2(40, 10), 13 | color: RED, 14 | sprite: "bean", 15 | scale: vec2(1), 16 | }); 17 | }); 18 | 19 | add([pos(vec2(10, 10)), sprite("bean")]); 20 | -------------------------------------------------------------------------------- /tests/playtests/mouse.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | width: 1280, 3 | height: 720, 4 | pixelDensity: 2, 5 | }); 6 | 7 | const redDot = add([ 8 | anchor("center"), 9 | circle(3), 10 | color(RED), 11 | pos(), 12 | fakeMouse(), 13 | ]); 14 | 15 | onMouseMove(pos => { 16 | redDot.pos = toWorld(pos); 17 | }); 18 | 19 | onKeyPress("f", () => { 20 | setFullscreen(!isFullscreen()); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/playtests/mouseScale.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | scale: 7, 3 | width: 200, 4 | height: 400, 5 | }); 6 | 7 | const redDot = add([ 8 | anchor("center"), 9 | circle(3), 10 | color(RED), 11 | pos(), 12 | fakeMouse(), 13 | ]); 14 | 15 | onMouseMove(pos => { 16 | redDot.pos = toWorld(pos); 17 | }); 18 | 19 | onKeyPress("f", () => { 20 | setFullscreen(!isFullscreen()); 21 | }); 22 | -------------------------------------------------------------------------------- /src/utils/benchmark.ts: -------------------------------------------------------------------------------- 1 | export function benchmark(task: () => void, times: number = 1) { 2 | const t1 = performance.now(); 3 | for (let i = 0; i < times; i++) { 4 | task(); 5 | } 6 | const t2 = performance.now(); 7 | return t2 - t1; 8 | } 9 | 10 | export function comparePerf(t1: () => void, t2: () => void, times: number = 1) { 11 | return benchmark(t2, times) / benchmark(t1, times); 12 | } 13 | -------------------------------------------------------------------------------- /tests/playtests/mouseScaleLetterbox.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | scale: 7, 3 | width: 400, 4 | height: 200, 5 | letterbox: true, 6 | }); 7 | 8 | const redDot = add([ 9 | anchor("center"), 10 | circle(3), 11 | color(RED), 12 | pos(), 13 | ]); 14 | 15 | onMouseMove(pos => { 16 | redDot.pos = toWorld(pos); 17 | }); 18 | 19 | onKeyPress("f", () => { 20 | setFullscreen(!isFullscreen()); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/playtests/mouseScaleLetterbox2.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | scale: 5, 3 | width: 200, 4 | height: 400, 5 | letterbox: true, 6 | }); 7 | 8 | const redDot = add([ 9 | anchor("center"), 10 | circle(3), 11 | color(RED), 12 | pos(), 13 | ]); 14 | 15 | onMouseMove(pos => { 16 | redDot.pos = toWorld(pos); 17 | }); 18 | 19 | onKeyPress("f", () => { 20 | setFullscreen(!isFullscreen()); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/playtests/slice9zero.js: -------------------------------------------------------------------------------- 1 | kaplay({ background: "black" }); 2 | loadSprite("9slice", "/sprites/9slice.png", { 3 | slice9: { 4 | left: 32, 5 | right: 32, 6 | top: 32, 7 | bottom: 32, 8 | }, 9 | }); 10 | const g = add([ 11 | sprite("9slice"), 12 | ]); 13 | onUpdate(() => { 14 | // Should not blink 15 | g.width = g.height = Math.round(wave(63, 65, time() * 4)); 16 | }); 17 | -------------------------------------------------------------------------------- /src/game/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GameObj } from "../types"; 2 | 3 | // Note: I will doom this soon 😈😈😈😈 4 | export function getRenderProps(obj: GameObj) { 5 | return { 6 | color: obj.color, 7 | opacity: obj.opacity, 8 | anchor: obj.anchor, 9 | outline: obj.outline, 10 | shader: obj.shader, 11 | uniform: obj.uniform, 12 | blend: obj.blend, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/utils.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import { isDataURL } from "../utils/dataURL"; 3 | 4 | export function fixURL(url: D): D { 5 | if (typeof url == "string" && window.kaplayjs_assetsAliases[url]) { 6 | url = (window.kaplayjs_assetsAliases[url] as unknown) as D; 7 | } 8 | 9 | if (typeof url !== "string" || isDataURL(url)) return url; 10 | return _k.assets.urlPrefix + url as D; 11 | } 12 | -------------------------------------------------------------------------------- /src/math/sort.ts: -------------------------------------------------------------------------------- 1 | export function insertionSort(a: T[], cmp: (a: T, b: T) => boolean) { 2 | for (let i = 1; i < a.length; i++) { 3 | for (let j = i - 1; j >= 0; j--) { 4 | if (cmp(a[j], a[j + 1])) break; 5 | swap(a, j, j + 1); 6 | } 7 | } 8 | } 9 | 10 | function swap(a: T[], i: number, j: number) { 11 | const temp = a[i]; 12 | a[i] = a[j]; 13 | a[j] = temp; 14 | } 15 | -------------------------------------------------------------------------------- /tests/playtests/timeLeft.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | 3 | loadBean(); 4 | const bean = add([sprite("bean"), pos()]); 5 | 6 | const tweening = tween( 7 | bean.pos, 8 | center(), 9 | 5, 10 | (p) => bean.pos = p, 11 | easings.easeOutQuint, 12 | ); 13 | 14 | // Also has a setter 15 | onClick(() => { 16 | tweening.timeLeft = 5; 17 | }); 18 | 19 | onUpdate(() => { 20 | debug.log(`${tweening.currentTime}/5`); 21 | }); 22 | -------------------------------------------------------------------------------- /src/gfx/draw/drawUnscaled.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | import { flush } from "../stack"; 3 | 4 | export function drawUnscaled(content: () => void) { 5 | flush(); 6 | const ow = _k.gfx.width; 7 | const oh = _k.gfx.height; 8 | _k.gfx.width = _k.gfx.viewport.width; 9 | _k.gfx.height = _k.gfx.viewport.height; 10 | content(); 11 | flush(); 12 | _k.gfx.width = ow; 13 | _k.gfx.height = oh; 14 | } 15 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | name: "KAPLAY.js", 6 | environment: "puppeteer", 7 | globalSetup: "vitest-environment-puppeteer/global-init", 8 | fileParallelism: false, 9 | typecheck: { 10 | ignoreSourceErrors: true, 11 | tsconfig: "./tests/tsconfig.json", 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /tests/playtests/textPaused.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Paused text 3 | * @description Test paused text objcts and their transforms. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | add([ 12 | text("this text should not be moving", { 13 | transform(i) { 14 | return { pos: vec2(0, wave(-10, 10, time() + i)) }; 15 | }, 16 | }), 17 | pos(100, 100), 18 | ]).paused = true; 19 | -------------------------------------------------------------------------------- /src/utils/asserts.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "../math/color"; 2 | import { Vec2 } from "../math/Vec2"; 3 | 4 | export function arrayIsColor(arr: unknown[]): arr is Color[] { 5 | return arr[0] instanceof Color; 6 | } 7 | 8 | export function arrayIsVec2(arr: unknown[]): arr is Vec2[] { 9 | return arr[0] instanceof Vec2; 10 | } 11 | 12 | export function arrayIsNumber(arr: unknown[]): arr is number[] { 13 | return typeof arr[0] === "number"; 14 | } 15 | -------------------------------------------------------------------------------- /examples/kaboom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Kaboom! 3 | * @description How to KABOOM! 4 | * @difficulty 0 5 | * @tags basics, effects 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | // KAPLAY born as the direct successor of Kaboom.js! 11 | 12 | kaplay(); 13 | 14 | // The addKaboom() effect is a fun way to add explosions to your game. 15 | addKaboom(center()); 16 | 17 | onKeyPress(() => addKaboom(mousePos())); 18 | onMouseMove(() => addKaboom(mousePos())); 19 | -------------------------------------------------------------------------------- /tests/playtests/colorCSS.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file CSS Colors 3 | * @description Use CSS-defined colors colors in KAPLAY. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | * @test 8 | */ 9 | 10 | kaplay(); 11 | loadHappy(); 12 | 13 | add([ 14 | rect(512, 512, { 15 | radius: [0, 96, 96, 96], 16 | }), 17 | color("rebeccapurple"), 18 | pos(40, 40), 19 | ]); 20 | 21 | add([ 22 | text("css", { size: 192, font: "happy" }), 23 | pos(90, 310), 24 | ]); 25 | -------------------------------------------------------------------------------- /tests/playtests/prefab.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | loadBean(); 3 | 4 | const bean = add([ 5 | pos(10, 10), 6 | move(RIGHT, 100), 7 | "friend", 8 | ]); 9 | 10 | const r = bean.add([ 11 | rect(10, 10), 12 | ]); 13 | 14 | r.add([ 15 | sprite("bean"), 16 | ]); 17 | 18 | const beanPrefab = bean.serialize(); 19 | 20 | onClick(() => { 21 | const bean2 = addPrefab(beanPrefab, [ 22 | pos(20, rand(40, height() - 40)), 23 | ]); 24 | 25 | debug.log(bean2.tags); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/shaders/pixelate.frag: -------------------------------------------------------------------------------- 1 | uniform float u_size; 2 | uniform vec2 u_resolution; 3 | 4 | // TODO: this is causing some extra pixels to appear at screen edge 5 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 6 | if (u_size <= 0.0) return def_frag(); 7 | vec2 nsize = vec2(u_size / u_resolution.x, u_size / u_resolution.y); 8 | float x = floor(uv.x / nsize.x + 0.5); 9 | float y = floor(uv.y / nsize.y + 0.5); 10 | vec4 c = texture2D(tex, vec2(x, y) * nsize); 11 | return c * color; 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "skipLibCheck": false, 8 | "resolveJsonModule": true, 9 | "emitDeclarationOnly": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "declarationDir": "dist/declaration", 13 | "strict": true 14 | }, 15 | "include": [ 16 | "src/index.ts", 17 | "src/.env.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/playtests/compsMissing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Missing comps 3 | * @description Test behaviour when missing components at add(). 4 | * @difficulty 3 5 | * @tags comps 6 | * @minver 3001.0 7 | */ 8 | 9 | // Test when a dependency is missing on obj creation 10 | 11 | const k = kaplay({}); 12 | 13 | k.onError((e) => { 14 | if (e == "Error: Component \"body\" requires component \"pos\"") { 15 | console.log("TEST PASSED"); 16 | } 17 | }); 18 | 19 | k.add([ 20 | k.body(), 21 | ]); 22 | -------------------------------------------------------------------------------- /src/app/data.ts: -------------------------------------------------------------------------------- 1 | // Related to load and save data 2 | 3 | export function getData(key: string, def?: T): T | null { 4 | try { 5 | return JSON.parse(window.localStorage[key]); 6 | } catch { 7 | if (def) { 8 | setData(key, def); 9 | return def; 10 | } 11 | else { 12 | return null; 13 | } 14 | } 15 | } 16 | 17 | export function setData(key: string, data: any) { 18 | window.localStorage[key] = JSON.stringify(data); 19 | } 20 | -------------------------------------------------------------------------------- /src/core/fontCache.ts: -------------------------------------------------------------------------------- 1 | import { MAX_TEXT_CACHE_SIZE } from "../constants/general"; 2 | 3 | export const createFontCache = () => { 4 | const fontCacheCanvas = document.createElement("canvas"); 5 | fontCacheCanvas.width = MAX_TEXT_CACHE_SIZE; 6 | fontCacheCanvas.height = MAX_TEXT_CACHE_SIZE; 7 | const fontCacheC2d = fontCacheCanvas.getContext("2d", { 8 | willReadFrequently: true, 9 | }); 10 | 11 | return { 12 | fontCacheCanvas, 13 | fontCacheC2d, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /tests/playtests/insertionSort.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | 3 | const t = add([ 4 | text(""), 5 | pos(100, 100), 6 | ]); 7 | 8 | var passes = 0, fails = 0; 9 | 10 | onUpdate(() => { 11 | const list = new Array(100).fill().map((_, i) => i); 12 | const correct = list.toString(); 13 | shuffle(list); 14 | insertionSort(list, (a, b) => b > a); 15 | const result = list.toString(); 16 | if (correct === result) passes++; 17 | else fails++; 18 | t.text = `${passes} passes\n${fails} fails`; 19 | }); 20 | -------------------------------------------------------------------------------- /examples/shaders/light.frag: -------------------------------------------------------------------------------- 1 | uniform float u_radius; 2 | uniform float u_blur; 3 | uniform vec2 u_resolution; 4 | uniform vec2 u_mouse; 5 | 6 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 7 | if (u_radius <= 0.0) return def_frag(); 8 | vec2 center = u_mouse / u_resolution * vec2(1, -1) + vec2(0, 1); 9 | float dist = distance(uv * u_resolution, center * u_resolution); 10 | float alpha = smoothstep(max((dist - u_radius) / u_blur, 0.0), 0.0, 1.0); 11 | return mix(vec4(0, 0, 0, 1), def_frag(), 1.0 - alpha); 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for more information: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # https://containers.dev/guide/dependabot 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "devcontainers" 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /examples/tiled.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Tiled 3 | * @description How to use sprites in tiled mode 4 | * @difficulty 1 5 | * @tags basics, game 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // Tiled sprites! 12 | 13 | kaplay(); 14 | 15 | loadSprite("bean", "/sprites/bean.png"); 16 | 17 | add([ 18 | pos(150, 150), 19 | sprite("bean", { 20 | tiled: true, 21 | width: 200, 22 | height: 200, 23 | }), 24 | anchor("center"), 25 | ]); 26 | 27 | debug.inspect = true; 28 | -------------------------------------------------------------------------------- /tests/playtests/wait1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Wait 3 | * @description Test the behaviour of wait(). 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | const txt = add([ 12 | pos(100, 100), 13 | text(""), 14 | ]); 15 | 16 | var theTime = 0; 17 | const foo = onUpdate(() => { 18 | theTime += dt(); 19 | txt.text = theTime; 20 | }); 21 | 22 | wait(1, () => { 23 | debug.log("callback"); 24 | }).onEnd(() => { 25 | debug.log("onEnd"); 26 | foo.cancel(); 27 | }); 28 | -------------------------------------------------------------------------------- /src/audio/volume.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import { deprecateMsg } from "../utils/log"; 3 | export function setVolume(v: number) { 4 | _k.audio.masterNode.gain.value = v; 5 | } 6 | 7 | export function getVolume() { 8 | return _k.audio.masterNode.gain.value; 9 | } 10 | 11 | // get / set master volume 12 | export function volume(v?: number): number { 13 | deprecateMsg("volume", "setVolume / getVolume"); 14 | 15 | if (v !== undefined) { 16 | setVolume(v); 17 | } 18 | return getVolume(); 19 | } 20 | -------------------------------------------------------------------------------- /src/constants/math.ts: -------------------------------------------------------------------------------- 1 | import { Mat4 } from "../math/Mat4"; 2 | import { Vec2 } from "../math/Vec2"; 3 | 4 | export const IDENTITY_MATRIX = new Mat4(); 5 | export const TOP_LEFT = new Vec2(-1, -1); 6 | export const TOP = new Vec2(0, -1); 7 | export const TOP_RIGHT = new Vec2(1, -1); 8 | export const LEFT = new Vec2(-1, 0); 9 | export const CENTER = new Vec2(0, 0); 10 | export const RIGHT = new Vec2(1, 0); 11 | export const BOTTOM_LEFT = new Vec2(-1, 1); 12 | export const BOTTOM = new Vec2(0, 1); 13 | export const BOTTOM_RIGHT = new Vec2(1, 1); 14 | -------------------------------------------------------------------------------- /tests/playtests/compsMissingDynamic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Missing components on run-time 3 | * @description Test missing component dependencies at run-time. 4 | * @difficulty 3 5 | * @tags comps 6 | * @minver 3001.0 7 | */ 8 | 9 | // Test when a dependency is missing on obj.use() 10 | 11 | const k = kaplay({}); 12 | 13 | k.onError((e) => { 14 | if (e == "Error: Component \"body\" requires component \"pos\"") { 15 | console.log("TEST PASSED"); 16 | } 17 | }); 18 | 19 | const dummy = k.add([]); 20 | 21 | dummy.use(body()); 22 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "../dist/declaration/global.d.ts" 5 | ], 6 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 7 | "module": "ESNext", /* Specify what module code is generated. */ 8 | "moduleResolution": "Bundler", 9 | "noEmit": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "allowJs": true, 12 | "moduleDetection": "force", 13 | "noImplicitAny": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/gfx/bg.ts: -------------------------------------------------------------------------------- 1 | import { type ColorArgs, rgb } from "../math/color"; 2 | import { _k } from "../shared"; 3 | 4 | export function setBackground(...args: ColorArgs) { 5 | const color = rgb(...args); 6 | const alpha = args[3] ?? 1; 7 | 8 | _k.gfx.bgColor = color; 9 | _k.gfx.bgAlpha = alpha; 10 | 11 | _k.gfx.ggl.gl.clearColor( 12 | color.r / 255, 13 | color.g / 255, 14 | color.b / 255, 15 | alpha, 16 | ); 17 | } 18 | 19 | export function getBackground() { 20 | return _k.gfx.bgColor?.clone?.() ?? null; 21 | } 22 | -------------------------------------------------------------------------------- /src/math/spatial/index.ts: -------------------------------------------------------------------------------- 1 | import type { AreaComp } from "../../ecs/components/physics/area"; 2 | import type { GameObj } from "../../types"; 3 | import type { Rect } from "../math"; 4 | 5 | export interface BroadPhaseAlgorithm { 6 | add(obj: GameObj): void; 7 | remove(obj: GameObj): void; 8 | clear(): void; 9 | update(): void; 10 | iterPairs( 11 | pairCb: (obj1: GameObj, obj2: GameObj) => void, 12 | ): void; 13 | retrieve(rect: Rect, retrieveCb: (obj: GameObj) => void): void; 14 | } 15 | -------------------------------------------------------------------------------- /examples/shaders/blink.frag: -------------------------------------------------------------------------------- 1 | uniform float u_time; 2 | uniform vec3 u_fore; 3 | uniform vec3 u_back; 4 | 5 | float map(float x, float a, float b, float c, float d) { 6 | return c + (d - c) * (x - a) / (b - a); 7 | } 8 | 9 | #define pi 3.141592653589 10 | 11 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 12 | vec4 pix = texture2D(tex, uv); 13 | float mixvalue = smoothstep(.3, .7, map(sin(u_time * pi), -1., 1., 0., 1.)); 14 | if (pix.a > 0.) mixvalue = 1. - mixvalue; 15 | return mix(vec4(u_back / 255., 1.), vec4(u_fore / 255., 1.), mixvalue); 16 | } 17 | -------------------------------------------------------------------------------- /src/ecs/entity/utils.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | import type { GameObj } from "../../types"; 3 | 4 | export function destroy(obj: GameObj) { 5 | obj.destroy(); 6 | } 7 | 8 | export function getTreeRoot(): GameObj { 9 | return _k.game.root; 10 | } 11 | 12 | export function isFixed(obj: GameObj): boolean { 13 | if (obj.fixed) return true; 14 | return obj.parent ? isFixed(obj.parent) : false; 15 | } 16 | 17 | export function isPaused(obj: GameObj): boolean { 18 | if (obj.paused) return true; 19 | return obj.parent ? isPaused(obj.parent) : false; 20 | } 21 | -------------------------------------------------------------------------------- /examples/onLoadError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Load Error 3 | * @description How to handle errors on load. 4 | * @difficulty 1 5 | * @tags loading 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay(); 11 | 12 | // this will not load (uncomment) 13 | // loadSprite("bobo", "notavalidURL"); 14 | 15 | // process the load error 16 | // you decide whether to ignore it, or throw an error and halt the game 17 | onLoadError((name, asset) => { 18 | // ignore it: 19 | debug.error(`${name} failed to load: ${asset.error}`); 20 | // throw an error: 21 | throw asset.error; 22 | }); 23 | -------------------------------------------------------------------------------- /examples/timer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Timer 3 | * @description How to make count time in KAPLAY. 4 | * @difficulty 0 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | kaplay(); 12 | 13 | loadSprite("bean", "/sprites/bean.png"); 14 | 15 | // Execute something after every 0.5 seconds. 16 | loop(0.5, () => { 17 | const bean = add([ 18 | sprite("bean"), 19 | pos(rand(vec2(0), vec2(width(), height()))), 20 | ]); 21 | 22 | // Execute something after 3 seconds. 23 | wait(3, () => { 24 | destroy(bean); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/playtests/raycastLevelTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Raycast Level Test 3 | * @description Ok MF explain this 4 | * @difficulty 3 5 | * @tags debug 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | const level = addLevel([ 12 | "a", 13 | ], { 14 | tileHeight: 100, 15 | tileWidth: 100, 16 | tiles: { 17 | a: () => [ 18 | rect(32, 32), 19 | area(), 20 | color(RED), 21 | ], 22 | }, 23 | }); 24 | try { 25 | level.raycast(vec2(50, 50), vec2(-50, -50)); 26 | } catch (e) { 27 | debug.error(e.stack); 28 | throw e; 29 | } 30 | -------------------------------------------------------------------------------- /tests/playtests/obj_dependencies_in_add_hook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Dependencies inside comp.add hooks 3 | * @description Test dependency between two components where the dependant is added in the add hook 4 | */ 5 | kaplay(); 6 | 7 | function comp1() { 8 | return { 9 | id: "comp1", 10 | require: ["comp2"], 11 | add() { 12 | this.use(comp2()); 13 | }, 14 | }; 15 | } 16 | 17 | function comp2() { 18 | return { 19 | id: "comp2", 20 | add() { 21 | debug.log("hi"); 22 | }, 23 | }; 24 | } 25 | 26 | add([ 27 | comp1(), 28 | ]); 29 | -------------------------------------------------------------------------------- /tests/playtests/debugTimeScale.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Time Scale 3 | * @description Test time scale and dt() difference. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | let firstFrameDt; 12 | 13 | wait(0.5, () => { 14 | firstFrameDt = dt(); 15 | debug.timeScale = 2; 16 | }); 17 | 18 | wait(1, () => { 19 | const newDt = dt(); 20 | 21 | if (newDt > firstFrameDt) { 22 | debug.log( 23 | `✔ TEST PASSED. 1st dt: ${firstFrameDt}, modified dt: ${newDt}`, 24 | ); 25 | } 26 | else { 27 | debug.log(`TEST FAILED.`); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /examples/patrol.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Patrol 3 | * @description How to patrol a sprite. 4 | * @difficulty 1 5 | * @tags physics 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay(); 11 | 12 | loadBean(); 13 | 14 | const bean = add([ 15 | sprite("bean"), 16 | pos(40, 30), 17 | patrol({ 18 | waypoints: [ 19 | vec2(100, 100), 20 | vec2(120, 170), 21 | vec2(50, 50), 22 | vec2(300, 100), 23 | ], 24 | }), 25 | ]); 26 | 27 | bean.onPatrolFinished(gb => { 28 | debug.log(`Bean reached the end of the patrol at ${gb.pos.x}, ${gb.pos.y}`); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/size.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Keep Aspect Ratio 3 | * @description How to keep aspect ratio using letterbox 4 | * @difficulty 0 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | kaplay({ 11 | // without specifying "width" and "height", kaplay will size to the container (document.body by default) 12 | width: 200, 13 | height: 100, 14 | // "letterbox" makes stretching keeps aspect ratio (leaves black bars on empty spaces), have no effect without "stretch" 15 | letterbox: true, 16 | }); 17 | 18 | loadBean(); 19 | 20 | add([ 21 | sprite("bean"), 22 | ]); 23 | 24 | onClick(() => addKaboom(mousePos())); 25 | -------------------------------------------------------------------------------- /tests/playtests/sprite_anim_onend_restart.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | 3 | loadSprite("dino", "/sprites/dungeon-dino.png", { 4 | sliceX: 9, 5 | anims: { 6 | idle: { 7 | from: 0, 8 | to: 3, 9 | speed: 5, 10 | }, 11 | run: { 12 | from: 4, 13 | to: 7, 14 | speed: 10, 15 | }, 16 | }, 17 | }); 18 | 19 | const s = add([ 20 | sprite("dino"), 21 | pos(100, 100), 22 | scale(10), 23 | ]); 24 | 25 | function anim() { 26 | s.play("idle", { 27 | onEnd() { 28 | s.play("run", { onEnd: anim }); 29 | }, 30 | }); 31 | } 32 | anim(); 33 | -------------------------------------------------------------------------------- /src/ecs/components/misc/boom.ts: -------------------------------------------------------------------------------- 1 | import { vec2 } from "../../../math/math"; 2 | import { _k } from "../../../shared"; 3 | import type { Comp, GameObj } from "../../../types"; 4 | import type { ScaleComp } from "../transform/scale"; 5 | 6 | export function boom(speed: number = 2, size: number = 1): Comp { 7 | let time = 0; 8 | return { 9 | require: ["scale"], 10 | update(this: GameObj) { 11 | const s = Math.sin(time * speed) * size; 12 | if (s < 0) { 13 | this.destroy(); 14 | } 15 | this.scale = vec2(s); 16 | time += _k.app.dt(); 17 | }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /examples/shaders/crt.frag: -------------------------------------------------------------------------------- 1 | uniform float u_flatness; 2 | uniform float u_scanline_height; 3 | uniform float u_screen_height; 4 | 5 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 6 | vec2 center = vec2(0.5, 0.5); 7 | vec2 off_center = uv - center; 8 | off_center *= 1.0 + pow(abs(off_center.yx), vec2(u_flatness)); 9 | vec2 uv2 = center + off_center; 10 | if (uv2.x > 1.0 || uv2.x < 0.0 || uv2.y > 1.0 || uv2.y < 0.0) { 11 | return vec4(0.0, 0.0, 0.0, 1.0); 12 | } else { 13 | vec4 c = vec4(texture2D(tex, uv2).rgb, 1.0); 14 | float fv = fract(uv2.y * 120.0); 15 | fv = min(1.0, 0.8 + 0.5 * min(fv, 1.0 - fv)); 16 | c.rgb *= fv; 17 | return c; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/playtests/testnewmultibutton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Multibutton Test 3 | * @description Testing for dynamic setButton() and multiple buttons for the same key. 4 | * @difficulty 3 5 | * @minver 4000.0 6 | */ 7 | 8 | kaplay({ 9 | buttons: { 10 | // a: { 11 | // keyboard: "enter", 12 | // }, 13 | // b: { 14 | // keyboard: ["e", "enter"], 15 | // } 16 | }, 17 | }); 18 | 19 | setButton("a", { 20 | keyboard: "enter", 21 | mouse: "left", 22 | }); 23 | setButton("b", { 24 | keyboard: ["e", "enter"], 25 | mouse: "left", 26 | }); 27 | 28 | add().onButtonPress((btn) => { 29 | debug.log("hi", btn); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "noImplicitThis": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "emitDeclarationOnly": true, 11 | "declaration": true, 12 | "declarationMap": true, 13 | "declarationDir": "dist/declaration", 14 | // always use import type for type only imports 15 | "verbatimModuleSyntax": true, 16 | "strict": true, 17 | "types": [ 18 | "../node_modules/vitest-puppeteer/dist/index.d.ts" 19 | ] 20 | }, 21 | "include": [ 22 | "./**/*" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/playtests/prettyDebug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Pretty Debug 3 | * @description Will see how pretty is our debug log 4 | * @difficulty 3 5 | * @tags debug 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | const pretty = { 12 | i: "am pretty", 13 | all: "own properties are shown", 14 | even: { 15 | nested: "objects", 16 | }, 17 | arrays: ["show", "like", "you", "would", "write", "them"], 18 | "own toString is used": vec2(10, 10), 19 | }; 20 | 21 | pretty.recursive = pretty; 22 | 23 | debug.log("Text in [brackets] doesn't cause issues"); 24 | debug.log(pretty); // recursive doesn't cause issues 25 | debug.error("This is an error message"); // errors in red 26 | -------------------------------------------------------------------------------- /tests/playtests/test536.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Offscreen test 3 | * @description Test for offscreening with camera. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | loadBean(); 12 | 13 | const bean = add([ 14 | sprite("bean"), 15 | pos(), 16 | offscreen({ pause: true, unpause: true }), 17 | rotate(), 18 | anchor("center"), 19 | { 20 | update() { 21 | this.angle += 400 * dt(); 22 | }, 23 | }, 24 | ]); 25 | 26 | camPos(0, 0); 27 | wait(1, () => { 28 | camPos(1000, 1000); 29 | wait(1, () => { 30 | camPos(0, 0); 31 | }); 32 | }); 33 | 34 | onUpdate(() => { 35 | debug.log(bean.isOffScreen()); 36 | }); 37 | -------------------------------------------------------------------------------- /examples/layer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Layer 3 | * @description How to use the z() component for layering 4 | * @difficulty 0 5 | * @tags basics, effects, comps 6 | * @ver 4000.0.0-alpha.18 7 | * @minver 3001.0 8 | * @category basics 9 | */ 10 | 11 | kaplay(); 12 | 13 | loadSprite("bean", "/sprites/bean.png"); 14 | 15 | // Create a parent node that won't be affected by camera (fixed) and will be drawn on top (z of 100) 16 | const ui = add([ 17 | fixed(), 18 | z(100), 19 | ]); 20 | 21 | // This will be on top, because the parent node has z(100) 22 | ui.add([ 23 | sprite("bean"), 24 | scale(5), 25 | color(0, 0, 255), 26 | ]); 27 | 28 | add([ 29 | sprite("bean"), 30 | pos(100, 100), 31 | scale(5), 32 | ]); 33 | -------------------------------------------------------------------------------- /src/gfx/draw/drawCanvas.ts: -------------------------------------------------------------------------------- 1 | import { vec2 } from "../../math/math"; 2 | import type { Canvas } from "../../types"; 3 | import { height } from "../stack"; 4 | import { drawUVQuad, type DrawUVQuadOpt } from "./drawUVQuad"; 5 | 6 | /** 7 | * @group Draw 8 | * @subgroup Types 9 | */ 10 | export type DrawCanvasOpt = DrawUVQuadOpt & { 11 | canvas: Canvas; 12 | }; 13 | 14 | export function drawCanvas(opt: DrawCanvasOpt) { 15 | const fb = opt.canvas.fb; 16 | drawUVQuad(Object.assign({}, opt, { 17 | tex: fb.tex, 18 | width: opt.width || fb.width, 19 | height: opt.height || fb.height, 20 | pos: (opt.pos || vec2()).add(0, height()), 21 | scale: (opt.scale || vec2(1)).scale(1, -1), 22 | })); 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "noImplicitThis": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "emitDeclarationOnly": true, 11 | "declaration": true, 12 | "declarationMap": true, 13 | "declarationDir": "dist/declaration", 14 | // always use import type for type only imports 15 | "verbatimModuleSyntax": true, 16 | "strict": true, 17 | "types": [ 18 | "./src/.env.d.ts" 19 | ] 20 | }, 21 | "include": [ 22 | "src/**/*", 23 | "scripts/**/*" 24 | ], 25 | "exclude": [ 26 | "src/index.ts", 27 | "./tests/**" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/constraintsflip.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | loadBean(); 3 | 4 | const spinningBean = add([ 5 | pos(100, 100), 6 | sprite("bean"), 7 | anchor("center"), 8 | rotate(), 9 | { 10 | update() { 11 | this.angle += 120 * dt(); 12 | }, 13 | }, 14 | ]); 15 | 16 | const scalingBean = add([ 17 | pos(200, 100), 18 | sprite("bean"), 19 | anchor("center"), 20 | rotate(), 21 | scale(), 22 | constraint.rotation(spinningBean, { scale: 1 }), 23 | ]); 24 | 25 | onUpdate(() => { 26 | const flip = Math.floor(time()) % 2 ? 1 : -1; 27 | console.log(Math.floor(time()), Math.floor(time()) % 2, flip); 28 | const zoom = wave(0, 1, time() * 3); 29 | scalingBean.scaleTo(zoom * flip, zoom); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/playtests/paused.js: -------------------------------------------------------------------------------- 1 | kaplay({ scale: 2 }); 2 | 3 | loadBean(); 4 | 5 | let countdown = 4; 6 | const bean = add([ 7 | pos(center()), 8 | sprite("bean"), 9 | anchor("center"), 10 | rotate(), 11 | area(), 12 | ]); 13 | 14 | // Listeners registered after paused will run regardless 15 | bean.paused = true; 16 | 17 | loop(1, () => { 18 | debug.log("will pause in", countdown -= 1); 19 | }, countdown).then(() => { 20 | bean.paused = true; 21 | debug.log("paused, shouldn't spin on space down"); 22 | }); 23 | 24 | bean.onKeyDown("space", () => { 25 | bean.angle += dt() * 100; 26 | }); 27 | 28 | bean.onUpdate(() => { 29 | debug.log("updateee"); 30 | }); 31 | 32 | bean.onClick(() => { 33 | debug.log("clicked"); 34 | }); 35 | -------------------------------------------------------------------------------- /src/utils/sets.ts: -------------------------------------------------------------------------------- 1 | export const isEqOrIncludes = (listOrSmt: T | T[], el: unknown): boolean => { 2 | if (Array.isArray(listOrSmt)) { 3 | return (listOrSmt as any[])?.includes(el); 4 | } 5 | 6 | return listOrSmt === el; 7 | }; 8 | 9 | export const setHasOrIncludes = ( 10 | set: Set, 11 | key: K | K[], 12 | ): boolean => { 13 | if (Array.isArray(key)) { 14 | return key.some((k) => set.has(k)); 15 | } 16 | 17 | return set.has(key); 18 | }; 19 | 20 | export const mapAddOrPush = ( 21 | map: Map, 22 | key: K, 23 | value: V, 24 | ): void => { 25 | if (map.has(key)) { 26 | map.get(key)?.push(value); 27 | } 28 | else { 29 | map.set(key, [value]); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/audio/audio.ts: -------------------------------------------------------------------------------- 1 | /** @ignore */ 2 | export interface InternalAudioCtx { 3 | ctx: AudioContext; 4 | masterNode: GainNode; 5 | } 6 | 7 | /** @ignore */ 8 | export function createEmptyAudioBuffer(ctx: AudioContext) { 9 | return ctx.createBuffer(1, 1, 44100); 10 | } 11 | 12 | /** @ignore */ 13 | export const initAudio = (): InternalAudioCtx => { 14 | const audio = (() => { 15 | const ctx = new ( 16 | window.AudioContext || (window as any).webkitAudioContext 17 | )() as AudioContext; 18 | 19 | const masterNode = ctx.createGain(); 20 | masterNode.connect(ctx.destination); 21 | 22 | return { 23 | ctx, 24 | masterNode, 25 | }; 26 | })(); 27 | 28 | return audio; 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | 3 | export const getErrorMessage = (error: unknown) => 4 | (error instanceof Error) ? error.message : String(error); 5 | 6 | export function deprecate( 7 | oldName: string, 8 | newName: string, 9 | newFunc: (...args: unknown[]) => any, 10 | ) { 11 | return (...args: unknown[]) => { 12 | deprecateMsg(oldName, newName); 13 | return newFunc(...args); 14 | }; 15 | } 16 | 17 | export function warn(msg: string) { 18 | if (!_k.game.warned.has(msg)) { 19 | _k.game.warned.add(msg); 20 | console.warn(msg); 21 | } 22 | } 23 | 24 | export function deprecateMsg(oldName: string, newName: string) { 25 | warn(`${oldName} is deprecated. Use ${newName} instead.`); 26 | } 27 | -------------------------------------------------------------------------------- /tests/playtests/textBig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Big Text 3 | * @description Testing for big big text. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | add([ 12 | text( 13 | "text text text text text text text text text text text text text text text\ntext [big]big[/big] text\ntext text text", 14 | { 15 | styles: { 16 | big() { 17 | return { 18 | scale: vec2(wave(3, 5, time())), 19 | stretchInPlace: false, 20 | }; 21 | }, 22 | }, 23 | width: width() / 2, 24 | }, 25 | ), 26 | pos(100, 100), 27 | area(), 28 | ]); 29 | debug.inspect = true; 30 | -------------------------------------------------------------------------------- /examples/burp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Burp 3 | * @description How to use burp, the engine core 4 | * @difficulty 0 5 | * @tags basics, audio 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | // Core KAPLAY features [💡] 11 | 12 | /* 💡 Burp 💡 13 | Burp is the engine core, it handles everything. 14 | Is not needed in most cases, unless you don't want your game crashing 15 | or freezing randomly. 16 | */ 17 | 18 | // Start the game in burp mode 19 | kaplay({ 20 | burp: true, 21 | background: "cc425e", 22 | }); 23 | 24 | // "b" triggers burp() on press 25 | add([ 26 | text("Press B to burp"), 27 | anchor("center"), 28 | pos(width() / 2, height() / 2), 29 | ]); 30 | 31 | // burp() on click / tap for our friends on mobile 32 | onClick(() => burp()); 33 | -------------------------------------------------------------------------------- /tests/playtests/spriteAsFont.js: -------------------------------------------------------------------------------- 1 | kaplay({ background: "black" }); 2 | 3 | loadSpriteAtlas("/sprites/dungeon.png", { 4 | "hero": { 5 | "x": 128, 6 | "y": 196, 7 | "width": 144, 8 | "height": 28, 9 | "sliceX": 9, 10 | }, 11 | }); 12 | 13 | loadBitmapFontFromSprite("hero", "abc"); 14 | 15 | add([ 16 | pos(100, 100), 17 | text("hello, I am [herofont]abcabc[/herofont]", { 18 | styles: { 19 | herofont: { 20 | font: "hero", 21 | scale: vec2(2), 22 | }, 23 | }, 24 | width: 300, 25 | transform: { 26 | scale: vec2(2), 27 | stretchInPlace: false, 28 | }, 29 | }), 30 | area(), 31 | ]); 32 | 33 | debug.inspect = true; 34 | -------------------------------------------------------------------------------- /tests/playtests/textWeirdTags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Text tags 3 | * @description Test the behaviour of text tags in certain cases. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | const txtEl = add([ 12 | text("", { 13 | styles: { 14 | pink: { 15 | color: MAGENTA, 16 | }, 17 | }, 18 | }), 19 | pos(100, 100), 20 | ]); 21 | const base = "[pink]hello\n[/pink]ohhi\n"; 22 | const txt = "foo\n\\[1]\nbar"; 23 | var i = -1; 24 | const c = loop(0.5, () => { 25 | if (txt[i] === "\\") i++; 26 | i++; 27 | txtEl.text = base + txt.slice(0, i) + "[pink]bye[/pink]"; 28 | if (i > txt.length) { 29 | console.log(txtEl.text); 30 | c.cancel(); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/git-split.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $# -ne 2 ] ; then 4 | echo "Usage: $0 original copy" 5 | exit 1 6 | fi 7 | 8 | if ! git diff --quiet || ! git diff --cached --quiet; then 9 | echo "Error: Unstaged changes present. Please commit or stash them before running this script." 10 | exit 1 11 | fi 12 | 13 | git mv "$1" "$2" 14 | git commit -n -m "chore: Split history $1 to $2 - rename file to target-name" 15 | REV=`git rev-parse HEAD` 16 | git reset --hard HEAD^ 17 | git mv "$1" temp 18 | git commit -n -m "chore: Split history $1 to $2 - rename source-file to temp" 19 | git merge $REV 20 | git commit -a -n -m "chore: Split history $1 to $2 - resolve conflict and keep both files" 21 | git mv temp "$1" 22 | git commit -n -m "chore: Split history $1 to $2 - restore name of source-file" 23 | -------------------------------------------------------------------------------- /tests/playtests/textItalic.js: -------------------------------------------------------------------------------- 1 | kaplay({ background: "black" }); 2 | 3 | add([ 4 | pos(100, 100), 5 | text( 6 | `this is [b]bold[/b] and this is [i]italicized[/i] woo! 7 | [b][i]have some of both![/i][/b] 8 | [o]and some oblique text[/o]`, 9 | { 10 | styles: { 11 | b: { 12 | color: WHITE, 13 | override: true, 14 | }, 15 | i: { 16 | skew: 20, 17 | }, 18 | o: { 19 | color: RED, 20 | skew: -20, 21 | override: true, 22 | }, 23 | }, 24 | transform: { 25 | color: rgb("gray"), 26 | }, 27 | }, 28 | ), 29 | ]); 30 | -------------------------------------------------------------------------------- /src/gfx/draw/drawBezier.ts: -------------------------------------------------------------------------------- 1 | import { evaluateBezier } from "../../math/math"; 2 | import { type Vec2 } from "../../math/Vec2"; 3 | import { drawCurve, type DrawCurveOpt } from "./drawCurve"; 4 | 5 | /** 6 | * @group Draw 7 | * @subgroup Types 8 | */ 9 | export type DrawBezierOpt = DrawCurveOpt & { 10 | /** 11 | * The first point. 12 | */ 13 | pt1: Vec2; 14 | /** 15 | * The the first control point. 16 | */ 17 | pt2: Vec2; 18 | /** 19 | * The the second control point. 20 | */ 21 | pt3: Vec2; 22 | /** 23 | * The second point. 24 | */ 25 | pt4: Vec2; 26 | }; 27 | 28 | export function drawBezier(opt: DrawBezierOpt) { 29 | drawCurve( 30 | t => evaluateBezier(opt.pt1, opt.pt2, opt.pt3, opt.pt4, t), 31 | opt, 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /tests/types/plugins.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, expectTypeOf, test } from "vitest"; 2 | import type { KAPLAYCtx } from "../../src/core/contextType"; 3 | import { kaplay } from "../../src/kaplay"; 4 | 5 | type ImplicitTestPlug = ReturnType; 6 | 7 | const implicitTestPlug = (k: KAPLAYCtx) => ({ 8 | getVersion() { 9 | return k.VERSION; 10 | }, 11 | }); 12 | 13 | describe("Type Inference from plugins", () => { 14 | // Inferred plugins 15 | 16 | test("type of plugin should be inferred from kaplay({ plugins: [ implicitTestPlug ] })", () => { 17 | const k = kaplay({ plugins: [implicitTestPlug] }); 18 | 19 | k.getVersion; 20 | 21 | expectTypeOf(k).toEqualTypeOf< 22 | KAPLAYCtx & ImplicitTestPlug 23 | >(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kaplayjs] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: 5 | open_collective: kaplay 6 | ko_fi: kaplay 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /src/gfx/canvasBuffer.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import type { Canvas } from "../types"; 3 | import { FrameBuffer } from "./FrameBuffer"; 4 | import { flush } from "./stack"; 5 | 6 | export const makeCanvas = (w: number, h: number): Canvas => { 7 | const fb = new FrameBuffer(_k.ggl, w, h); 8 | 9 | return { 10 | clear: () => fb.clear(), 11 | free: () => fb.free(), 12 | toDataURL: () => fb.toDataURL(), 13 | toImageData: () => fb.toImageData(), 14 | width: fb.width, 15 | height: fb.height, 16 | draw: (action: () => void) => { 17 | flush(); 18 | fb.bind(); 19 | action(); 20 | flush(); 21 | fb.unbind(); 22 | }, 23 | get fb() { 24 | return fb; 25 | }, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /tests/types/tafButtons.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from "vitest"; 2 | import type { Opt } from "../../src/core/taf"; 3 | import { kaplay, kaplayTypes } from "../../src/kaplay"; 4 | 5 | describe("Typed Buttons", () => { 6 | const k = kaplay({ 7 | background: "fff", 8 | buttons: { 9 | "jump": {}, 10 | }, 11 | types: kaplayTypes< 12 | Opt< 13 | { 14 | scenes: { 15 | "game": [score: number]; 16 | }; 17 | } 18 | > 19 | >(), 20 | }); 21 | 22 | k.scene("game", () => {}); 23 | 24 | test("in isButtonPressed(btn), btn is typed", () => { 25 | k.isButtonPressed("jump"); 26 | // @ts-expect-error 27 | k.isButtonPressed("f"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/basicsStart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Create your first game 3 | * @description All you need to know to get started 4 | * @difficulty 0 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category basics 8 | * @group basics 9 | * @groupOrder 0 10 | */ 11 | 12 | // Get started with KAPLAY [💡] 13 | 14 | /* 💡 KAPLAY Context 💡 15 | The kaplay() function is the entry point to your game. It sets up the game and 16 | exports all the functions and variables you need to use in your game, like 17 | add(), loadSprite(), debug.log(), pos(), and many more. This is called the context. 18 | */ 19 | 20 | // We initialize the context 21 | kaplay({ 22 | // We can optionally pass options to it! 23 | background: "#5ba675", // Set the background color 24 | }); 25 | 26 | // Now you have access to the context functions: 27 | debug.log("Hello from game!"); 28 | -------------------------------------------------------------------------------- /examples/bbcode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file BBCode Formatting 3 | * @description How to parameterize text formatting codes. 4 | * @difficulty 2 5 | * @tags text 6 | * @minver 4000.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay({ background: "black" }); 11 | 12 | add([ 13 | pos(100, 100), 14 | text( 15 | "[color=red]These[/color] [color=orange]colored[/color] [color=yellow]words[/color] [color=green]are[/color] [color=blue]all[/color] [color=purple]the[/color] [color=tan]same[/color] [color=brown]tag![/color]", 16 | { 17 | styles: { 18 | color(i, ch, param) { 19 | return { 20 | color: rgb(param), 21 | }; 22 | }, 23 | }, 24 | size: 32, 25 | width: 500, 26 | }, 27 | ), 28 | ]); 29 | -------------------------------------------------------------------------------- /examples/frames.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Frames 3 | * @description How to define frames in an animation. 4 | * @difficulty 0 5 | * @tags basics, animation 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | kaplay({ 12 | scale: 4, 13 | background: [0, 0, 0], 14 | }); 15 | 16 | // https:/ / (0x72).itch.io / dungeontileset - ii; 17 | loadSpriteAtlas("/sprites/dungeon.png", { 18 | wizard: { 19 | x: 128, 20 | y: 140, 21 | width: 144, 22 | height: 28, 23 | sliceX: 9, 24 | anims: { 25 | bouncy: { 26 | frames: [8, 5, 0, 3, 2, 3, 0, 5], 27 | speed: 10, 28 | loop: true, 29 | }, 30 | }, 31 | }, 32 | }); 33 | 34 | add([ 35 | sprite("wizard", { anim: "bouncy" }), 36 | pos(100, 100), 37 | ]); 38 | -------------------------------------------------------------------------------- /src/core/plug.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import type { KAPLAYPlugin } from "../types"; 3 | import type { KAPLAYCtx } from "./contextType"; 4 | 5 | export const plug = >( 6 | plugin: KAPLAYPlugin, 7 | ...args: any 8 | ): KAPLAYCtx & T => { 9 | const funcs = plugin(_k.k); 10 | let funcsObj: T; 11 | if (typeof funcs === "function") { 12 | const plugWithOptions = funcs(...args); 13 | funcsObj = plugWithOptions(_k.k); 14 | } 15 | else { 16 | funcsObj = funcs; 17 | } 18 | 19 | for (const key in funcsObj) { 20 | _k.k[key as keyof typeof _k.k] = funcsObj[key]; 21 | 22 | if (_k.globalOpt.global !== false) { 23 | window[key as any] = funcsObj[key]; 24 | } 25 | } 26 | 27 | return _k.k as unknown as KAPLAYCtx & T; 28 | }; 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feat.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for kaplayy. 3 | labels: ["enhancement"] 4 | title: "feat: your feature request here" 5 | type: "Feature" 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Is your feature request related to a problem? Please describe. 10 | description: A clear and concise description of what the problem is. Ex. I'm always frustated when.... 11 | placeholder: I HATE having to make 3D manually so we should add 3D... 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | attributes: 17 | label: Any more information? 18 | description: Have you found alternatives? How should we make the API for you feature request? Type it here. 19 | placeholder: So we could add something like addCube() then... 20 | validations: 21 | required: false -------------------------------------------------------------------------------- /src/utils/deepEq.ts: -------------------------------------------------------------------------------- 1 | export function deepEq(o1: any, o2: any): boolean { 2 | if (o1 === o2) { 3 | return true; 4 | } 5 | const t1 = typeof o1; 6 | const t2 = typeof o2; 7 | if (t1 !== t2) { 8 | return false; 9 | } 10 | if (t1 === "object" && t2 === "object" && o1 !== null && o2 !== null) { 11 | if (Array.isArray(o1) !== Array.isArray(o2)) { 12 | return false; 13 | } 14 | const k1 = Object.keys(o1); 15 | const k2 = Object.keys(o2); 16 | if (k1.length !== k2.length) { 17 | return false; 18 | } 19 | for (const k of k1) { 20 | const v1 = o1[k]; 21 | const v2 = o2[k]; 22 | if (!deepEq(v1, v2)) { 23 | return false; 24 | } 25 | } 26 | return true; 27 | } 28 | return false; 29 | } 30 | -------------------------------------------------------------------------------- /src/ecs/components/draw/mask.ts: -------------------------------------------------------------------------------- 1 | import type { Comp, Mask } from "../../../types"; 2 | 3 | /** 4 | * The serialized {@link mask `mask()`} component. 5 | * 6 | * @group Components 7 | * @subgroup Component Serialization 8 | */ 9 | export interface SerializedMaskComp { 10 | mask: Mask; 11 | } 12 | 13 | /** 14 | * The {@link mask `mask()`} component. 15 | * 16 | * @group Components 17 | * @subgroup Component Types 18 | */ 19 | export interface MaskComp extends Comp { 20 | mask: Mask; 21 | serialize(): SerializedMaskComp; 22 | } 23 | 24 | export function mask(m: Mask = "intersect"): MaskComp { 25 | return { 26 | id: "mask", 27 | mask: m, 28 | serialize() { 29 | return { mask: this.mask }; 30 | }, 31 | }; 32 | } 33 | 34 | export function maskFactory(data: SerializedMaskComp) { 35 | return mask(data.mask); 36 | } 37 | -------------------------------------------------------------------------------- /examples/customCompDebug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Custom Components Debug 3 | * @description How to display custom properties in custom components 4 | * @difficulty 0 5 | * @tags comps, debug 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | // Press F1 to enable debug mode and see how custom properties appear in the 11 | // inspect box 12 | 13 | kaplay({ scale: 2 }); 14 | 15 | loadBean(); 16 | 17 | // Our custom component 18 | function customComp() { 19 | return { 20 | id: "compy", 21 | customing: true, 22 | inspect() { 23 | return `customing: ${this.customing}`; 24 | }, 25 | }; 26 | } 27 | 28 | const bean = add([ 29 | sprite("bean"), 30 | anchor("center"), 31 | pos(center()), 32 | area(), 33 | customComp(), 34 | ]); 35 | 36 | bean.onClick(() => { 37 | bean.customing = !bean.customing; 38 | }); 39 | -------------------------------------------------------------------------------- /tests/playtests/textShader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Shader text with text styles 3 | * @description Test adding shaders to a text style. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay({ background: "#000000" }); 10 | 11 | loadShaderURL("blink", null, "../..//shaders/blink.frag"); 12 | 13 | add([ 14 | pos(100, 100), 15 | text( 16 | "text [blink]i blink[/blink] text\n\n\n" 17 | + "why are the spaces so tall".replaceAll(" ", "[blink] [/blink]"), 18 | { 19 | styles: { 20 | blink: (i) => ({ 21 | shader: "blink", 22 | uniform: { 23 | u_time: time() - i / 20, 24 | u_fore: WHITE, 25 | u_back: BLACK, 26 | }, 27 | }), 28 | }, 29 | }, 30 | ), 31 | ]); 32 | -------------------------------------------------------------------------------- /src/ecs/components/draw/fadeIn.ts: -------------------------------------------------------------------------------- 1 | import { map } from "../../../math/math"; 2 | import { _k } from "../../../shared"; 3 | import type { Comp, GameObj } from "../../../types"; 4 | import type { OpacityComp } from "./opacity"; 5 | 6 | export function fadeIn(time: number = 1): Comp { 7 | let finalOpacity: number; 8 | let t = 0; 9 | let done = false; 10 | 11 | return { 12 | require: ["opacity"], 13 | add(this: GameObj) { 14 | finalOpacity = this.opacity; 15 | this.opacity = 0; 16 | }, 17 | update(this: GameObj) { 18 | if (done) return; 19 | t += _k.app.dt(); 20 | this.opacity = map(t, 0, time, 0, finalOpacity); 21 | 22 | if (t >= time) { 23 | this.opacity = finalOpacity; 24 | done = true; 25 | } 26 | }, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /scripts/dev/dev.js: -------------------------------------------------------------------------------- 1 | // Used in npm dev script 2 | // @ts-check 3 | 4 | import esbuild from "esbuild"; 5 | import { config, fmts } from "../lib/build.js"; 6 | import { serve } from "./serve.js"; 7 | 8 | export async function dev() { 9 | serve(); 10 | 11 | const ctx = await esbuild.context({ 12 | ...config, 13 | ...fmts("kaplay")[0], 14 | sourcemap: true, 15 | minify: false, 16 | minifyIdentifiers: false, 17 | minifySyntax: false, 18 | minifyWhitespace: false, 19 | keepNames: true, 20 | plugins: [ 21 | { 22 | name: "logger", 23 | setup(b) { 24 | b.onEnd(() => { 25 | console.log(`-> ${fmts("kaplay")[0].outfile}`); 26 | }); 27 | }, 28 | }, 29 | ], 30 | }); 31 | 32 | await ctx.watch(); 33 | } 34 | -------------------------------------------------------------------------------- /tests/playtests/textWrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Text Wrap 3 | * @description Test text wrap. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay({ background: "#000000" }); 10 | 11 | const theText = `MAN PAGE 12 | Very long description paragraph. Very long description paragraph. Very long description paragraph. Very long description paragraph. Very long description paragraph. Very long description paragraph. Very long description paragraph. 13 | 14 | ANOTHER SECTION 15 | Yet some more text here. Yet some more text here. Yet some more text here. Yet some more text here.`; 16 | 17 | add([ 18 | pos(100, 100), 19 | text(theText, { 20 | size: 16, 21 | width: 17 * 16, 22 | }), 23 | ]); 24 | 25 | add([ 26 | pos(400, 100), 27 | text(theText, { 28 | size: 16, 29 | width: 17 * 16, 30 | indentAll: true, 31 | }), 32 | ]); 33 | -------------------------------------------------------------------------------- /examples/slice9.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Slice-9 3 | * @description How to make use of slice-9 sprites. 4 | * @difficulty 1 5 | * @tags animation, draw 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // 9 slice sprite scaling 12 | 13 | kaplay(); 14 | 15 | // Load a sprite that's made for 9 slice scaling 16 | loadSprite("9slice", "/sprites/9slice.png", { 17 | // Define the slice by the margins of 4 sides 18 | slice9: { 19 | left: 32, 20 | right: 32, 21 | top: 32, 22 | bottom: 32, 23 | }, 24 | }); 25 | 26 | const g = add([ 27 | pos(center()), 28 | sprite("9slice"), 29 | anchor("center"), 30 | ]); 31 | 32 | onMouseMove(() => { 33 | const size = mousePos().sub(center()); 34 | // Scaling the image will keep the aspect ratio of the sliced frames 35 | g.width = Math.abs(size.x) * 2; 36 | g.height = Math.abs(size.y) * 2; 37 | }); 38 | -------------------------------------------------------------------------------- /examples/retrieve.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | // broadPhaseCollisionAlgorithm: "quadtree", 3 | broadPhaseCollisionAlgorithm: "grid", 4 | // broadPhaseCollisionAlgorithm: "sap" 5 | // broadPhaseCollisionAlgorithm: "sapv", 6 | }); 7 | 8 | loadBean(); 9 | 10 | onLoad(() => { 11 | for (let i = 0; i < 32; i++) { 12 | add([ 13 | pos(rand(64, width() - 64), rand(64, height() - 64)), 14 | sprite("bean"), 15 | anchor("center"), 16 | color(WHITE), 17 | area(), 18 | ]); 19 | } 20 | }); 21 | 22 | onMouseMove(pos => { 23 | debug.log(pos); 24 | let beans = get("*"); 25 | debug.log("There are", beans.length, "beans"); 26 | for (bean of beans) { 27 | bean.color = WHITE; 28 | } 29 | beans = retrieve(new Rect(pos.sub(2, 2), 4, 4), bean => { 30 | debug.log(bean.id); 31 | bean.color = RED; 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/app/buttons.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import type { ButtonBinding } from "./inputBindings"; 3 | 4 | // Getting / Setting bindings 5 | 6 | export const getButtons = () => { 7 | return _k.app.state.buttons; 8 | }; 9 | 10 | export const getButton = (btn: string): ButtonBinding => { 11 | return _k.app.state.buttons?.[btn]; 12 | }; 13 | 14 | export const setButton = (btn: string, binding: ButtonBinding) => { 15 | _k.app.state.buttons[btn] = { 16 | ..._k.app.state.buttons[btn], 17 | ...binding, 18 | }; 19 | _k.app.state.buttonHandler.updateBinding(btn, binding); 20 | }; 21 | 22 | // Virtually pressing / releasing 23 | 24 | export const pressButton = (btn: string) => { 25 | _k.app.state.buttonHandler.state.press(btn, _k.app.state); 26 | }; 27 | 28 | export const releaseButton = (btn: string) => { 29 | _k.app.state.buttonHandler.state.release(btn, _k.app.state); 30 | }; 31 | -------------------------------------------------------------------------------- /src/math/lerp.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "./color"; 2 | import { Vec2 } from "./Vec2"; 3 | 4 | /** 5 | * A valid value for lerp. 6 | * 7 | * @group Math 8 | * @subgroup Tween 9 | */ 10 | export type LerpValue = number | Vec2 | Color; 11 | 12 | export function lerp( 13 | a: V, 14 | b: V, 15 | t: number, 16 | ): V { 17 | if (typeof a === "number" && typeof b === "number") { 18 | // we don't call lerpNumber just for performance, but should be the same 19 | return a + (b - a) * t as V; 20 | } 21 | // check for Vec2 22 | else if (a instanceof Vec2 && b instanceof Vec2) { 23 | return a.lerp(b, t) as V; 24 | } 25 | else if (a instanceof Color && b instanceof Color) { 26 | return a.lerp(b, t) as V; 27 | } 28 | 29 | throw new Error( 30 | `Bad value for lerp(): ${a}, ${b}. Only number, Vec2 and Color is supported.`, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /examples/basicsGlobals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file The KAPLAY Context 3 | * @description What's KAPLAY Context? What are globals? 4 | * @difficulty 0 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category basics 8 | * @group basics 9 | * @groupOrder 10 10 | */ 11 | 12 | // Learn about globals [💡] 13 | 14 | /* 💡 Context in Globals 💡 15 | The KAPLAY Context functions/variables are available in the global namespace by 16 | default, making them easy to use and great for learning. 17 | */ 18 | 19 | // You can use the context without adding them to the global namespace: 20 | 21 | // 1. Capture the context in a variable 22 | const k = kaplay({ 23 | global: false, // 2. Disable the export to the global namespace 24 | }); 25 | 26 | k.debug.log("Hello from the context!"); 27 | 28 | // This is a safe and better practice for larger projects. 29 | // Read more about this at: https://kaplayjs.com/guides/optimization/#avoid-global-namespace 30 | -------------------------------------------------------------------------------- /src/ecs/entity/premade/addLevel.ts: -------------------------------------------------------------------------------- 1 | import { vec2 } from "../../../math/math"; 2 | import type { Vec2 } from "../../../math/Vec2"; 3 | import { _k } from "../../../shared"; 4 | import type { GameObj } from "../../../types"; 5 | import { 6 | level, 7 | type LevelComp, 8 | type LevelCompOpt, 9 | } from "../../components/level/level"; 10 | import { pos, type PosComp } from "../../components/transform/pos"; 11 | 12 | /** 13 | * Options for the {@link addLevel `addLevel()`}. 14 | * 15 | * @group Game Obj 16 | * @subgroup Types 17 | */ 18 | export interface AddLevelOpt extends LevelCompOpt { 19 | /** 20 | * Position of the first block. 21 | */ 22 | pos?: Vec2; 23 | } 24 | 25 | export function addLevel( 26 | map: string[], 27 | opt: AddLevelOpt, 28 | parent: GameObj = _k.game.root, 29 | ): GameObj { 30 | return parent.add([pos(opt.pos ?? vec2(0)), level(map, opt)]); 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | push: 5 | tags: 6 | - '4000*' 7 | workflow_dispatch: 8 | inputs: 9 | version: 10 | description: "Version to publish" 11 | required: true 12 | default: "4000.0.0" 13 | 14 | jobs: 15 | publish: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Install Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | registry-url: "https://registry.npmjs.org" 24 | node-version: 20 25 | - uses: pnpm/action-setup@v4 26 | name: Install pnpm 27 | with: 28 | run_install: false 29 | - name: Install dependencies 30 | run: pnpm install 31 | - name: Publish to NPM 32 | run: npm publish --tag next 33 | env: 34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /src/ecs/components/draw/picture.ts: -------------------------------------------------------------------------------- 1 | import { getRenderProps } from "../../../game/utils"; 2 | import { drawPicture, type Picture } from "../../../gfx/draw/drawPicture"; 3 | import type { Comp, GameObj } from "../../../types"; 4 | 5 | /** 6 | * The {@link picture `picture()`} component. 7 | * 8 | * @group Components 9 | * @subgroup Component Types 10 | */ 11 | export interface PictureComp extends Comp { 12 | picture: Picture; 13 | } 14 | 15 | /** 16 | * Options for the {@link picture `picture()`} component. 17 | * 18 | * @group Components 19 | * @subgroup Component Types 20 | */ 21 | export type PictureCompOpt = { 22 | picture: Picture; 23 | }; 24 | 25 | export function picture(picture: Picture): PictureComp { 26 | return { 27 | id: "picture", 28 | picture: picture, 29 | draw(this: GameObj) { 30 | drawPicture(this.picture, getRenderProps(this)); 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/ecs/components/misc/named.ts: -------------------------------------------------------------------------------- 1 | import type { Comp } from "../../../types"; 2 | 3 | /** 4 | * The serialized {@link color `color()`} component. 5 | * 6 | * @group Components 7 | * @subgroup Component Serialization 8 | */ 9 | export interface SerializeNameComp { 10 | name: string; 11 | } 12 | 13 | /** 14 | * The {@link named `named()`} component. 15 | * 16 | * @group Components 17 | * @subgroup Component Types 18 | */ 19 | export interface NamedComp extends Comp { 20 | /** The name assigned to this object. */ 21 | name: string; 22 | serialize(): SerializeNameComp; 23 | } 24 | 25 | export function named(name: string): NamedComp { 26 | return { 27 | id: "named", 28 | name, 29 | serialize() { 30 | return { 31 | name: name, 32 | }; 33 | }, 34 | }; 35 | } 36 | 37 | export function nameFactory(data: any) { 38 | return named(data.name); 39 | } 40 | -------------------------------------------------------------------------------- /src/gfx/draw/drawCurve.ts: -------------------------------------------------------------------------------- 1 | import type { Vec2 } from "../../math/Vec2"; 2 | import type { RenderProps } from "../../types"; 3 | import { drawLines } from "./drawLine"; 4 | 5 | /** 6 | * @group Draw 7 | * @subgroup Types 8 | */ 9 | export type DrawCurveOpt = RenderProps & { 10 | /** 11 | * The amount of line segments to draw. 12 | */ 13 | segments?: number; 14 | /** 15 | * The width of the line. 16 | */ 17 | width?: number; 18 | }; 19 | 20 | export function drawCurve(curve: (t: number) => Vec2, opt: DrawCurveOpt) { 21 | const segments = opt.segments ?? 16; 22 | const p: Vec2[] = []; 23 | 24 | for (let i = 0; i <= segments; i++) { 25 | p.push(curve(i / segments)); 26 | } 27 | 28 | drawLines(Object.assign({}, opt, { 29 | pts: p, 30 | width: opt.width || 1, 31 | pos: opt.pos, 32 | color: opt.color, 33 | opacity: opt.opacity, 34 | })); 35 | } 36 | -------------------------------------------------------------------------------- /examples/fadeIn.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Fade In 3 | * @description How to fade in game objects 4 | * @difficulty 0 5 | * @tags visual, effects 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | // How to fade in an object 11 | 12 | kaplay(); 13 | 14 | loadBean(); 15 | 16 | // spawn a bean that takes a second to fade in 17 | const bean = add([ 18 | sprite("bean"), 19 | pos(120, 80), 20 | opacity(1), // opacity() component gives it opacity which is required for fadeIn 21 | ]); 22 | 23 | bean.fadeIn(1); // makes it fade in 24 | 25 | // spawn another bean that takes 5 seconds to fade in halfway 26 | // SPOOKY! 27 | let spookyBean = add([ 28 | sprite("bean"), 29 | pos(240, 80), 30 | opacity(0.5), // opacity() component gives it opacity which is required for fadeIn (set to 0.5 so it will be half transparent) 31 | ]); 32 | 33 | spookyBean.fadeIn(5); // makes it fade in (set to 5 so that it takes 5 seconds to fade in) 34 | -------------------------------------------------------------------------------- /examples/video.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Video 3 | * @description How to play videos 4 | * @difficulty 0 5 | * @tags animation 6 | * @minver 4000.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // Playing videos (🥊 included) 12 | 13 | kaplay({ scale: 2, background: "#a32858", font: "happy" }); 14 | 15 | loadHappy(); 16 | 17 | const vid = add([ 18 | pos(center()), 19 | // video() fetches the resource, we have to pass URL 20 | video("/videos/dance.mp4", { 21 | width: 320, 22 | height: 200, 23 | }), 24 | anchor("center"), 25 | ]); 26 | 27 | onClick(() => { 28 | vid.play(); 29 | }); 30 | 31 | /* 🥊 Challenge #1 🥊 32 | Videos are cool! Try replacing the video url by this one: 33 | 34 | //videos/3d.mp4 35 | 36 | And see how your mind blows 37 | */ 38 | 39 | // Other visual elements 40 | 41 | add([ 42 | pos(center().x, 50), 43 | text("click to play video"), 44 | anchor("center"), 45 | ]); 46 | -------------------------------------------------------------------------------- /src/ecs/components/transform/fixed.ts: -------------------------------------------------------------------------------- 1 | import type { Comp } from "../../../types"; 2 | 3 | /** 4 | * The serialized {@link fixed `fixed()`} component. 5 | * 6 | * @group Components 7 | * @subgroup Component Serialization 8 | */ 9 | export interface SerializedFixedComp { 10 | fixed?: boolean; 11 | } 12 | 13 | /** 14 | * The {@link fixed `fixed()`} component. 15 | * 16 | * @group Components 17 | * @subgroup Component Types 18 | */ 19 | export interface FixedComp extends Comp { 20 | /** 21 | * If the obj is unaffected by camera 22 | */ 23 | fixed: boolean; 24 | 25 | serialize(): SerializedFixedComp; 26 | } 27 | 28 | export function fixed(fixed = true): FixedComp { 29 | return { 30 | id: "fixed", 31 | fixed: fixed, 32 | serialize() { 33 | return { fixed: this.fixed }; 34 | }, 35 | }; 36 | } 37 | 38 | export function fixedFactory(data: SerializedFixedComp) { 39 | return fixed(data.fixed); 40 | } 41 | -------------------------------------------------------------------------------- /tests/playtests/laglevel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Lag Level 3 | * @description Assess how much lag area() causes. (higher score is better) 4 | */ 5 | 6 | kaplay({ 7 | background: "#4a3052", 8 | font: "happy-o", 9 | logMax: 1, 10 | }); 11 | 12 | loadBitmapFont("happy-o", "/crew/happy-o.png", 36, 45); 13 | 14 | let count = 0; 15 | let baseFPS = 0; 16 | 17 | loop(0.1, () => { 18 | add([ 19 | pos(rand(width()), rand(height())), 20 | rect(50, 50), 21 | opacity(0.8), 22 | area({ collisionIgnore: ["area"] }), 23 | "area", 24 | ]); 25 | 26 | count++; 27 | }); 28 | 29 | onUpdate(() => { 30 | if (debug.fps() <= baseFPS - 4) { 31 | debug.paused = true; 32 | 33 | add([ 34 | anchor("center"), 35 | pos(center()), 36 | text(count, { size: 120 }), 37 | ]); 38 | } 39 | 40 | debug.log(count, debug.fps()); 41 | }); 42 | 43 | wait(3, () => baseFPS = debug.fps()); 44 | -------------------------------------------------------------------------------- /src/gfx/draw/drawFrame.ts: -------------------------------------------------------------------------------- 1 | import { updateLastTransformVersion } from "../../ecs/entity/GameObjRaw"; 2 | import { lerp } from "../../math/lerp"; 3 | import { rand } from "../../math/math"; 4 | import { Vec2 } from "../../math/Vec2"; 5 | import { _k } from "../../shared"; 6 | import { center, flush } from "../stack"; 7 | 8 | export function transformFrame() { 9 | _k.game.root.transformTree(false); 10 | updateLastTransformVersion(); 11 | } 12 | 13 | export function drawFrame() { 14 | // calculate camera matrix 15 | const cam = _k.game.cam; 16 | const shake = Vec2.fromAngle(rand(0, 360)).scale(cam.shake); 17 | 18 | cam.shake = lerp(cam.shake, 0, 5 * _k.app.dt()); 19 | cam.transform.setIdentity() 20 | .translateSelfV(center()) 21 | .scaleSelfV(cam.scale) 22 | .rotateSelf(cam.angle) 23 | .translateSelfV((cam.pos ?? center()).scale(-1).add(shake)); 24 | 25 | _k.app.state.events.trigger("draw"); 26 | _k.game.root.draw(); 27 | flush(); 28 | } 29 | -------------------------------------------------------------------------------- /src/game/layers.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import { deprecateMsg } from "../utils/log"; 3 | 4 | // Layering 5 | 6 | export function setLayers(layerNames: string[], defaultLayer: string) { 7 | if (_k.game.layers) { 8 | throw Error("Layers can only be assigned once."); 9 | } 10 | const defaultLayerIndex = layerNames.indexOf(defaultLayer); 11 | if (defaultLayerIndex == -1) { 12 | throw Error( 13 | "The default layer name should be present in the layers list.", 14 | ); 15 | } 16 | _k.game.layers = layerNames; 17 | _k.game.defaultLayerIndex = defaultLayerIndex; 18 | } 19 | 20 | export function getLayers() { 21 | return _k.game.layers; 22 | } 23 | 24 | export function getDefaultLayer() { 25 | return _k.game.layers?.[_k.game.defaultLayerIndex] ?? null; 26 | } 27 | 28 | export function layers(layerNames: string[], defaultLayer: string) { 29 | deprecateMsg("layers", "setLayers"); 30 | setLayers(layerNames, defaultLayer); 31 | } 32 | -------------------------------------------------------------------------------- /tests/playtests/hover.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | background: "#f2ae99", 3 | }); 4 | 5 | loadBean(); 6 | 7 | const origPos = add([ 8 | anchor("center"), 9 | pos(center().x, 100), 10 | circle(4), 11 | color("#5ba675"), 12 | outline(4, { color: BLACK }), 13 | ]); 14 | 15 | const card = add([ 16 | anchor("center"), 17 | pos(origPos.pos), 18 | rect(300, 100, { radius: 16 }), 19 | color(WHITE), 20 | outline(8, BLACK), 21 | ]); 22 | 23 | const bean = card.add([ 24 | sprite("bean"), 25 | anchor("center"), 26 | scale(2), 27 | area(), 28 | z(1), 29 | ]); 30 | 31 | bean.onHover(() => { 32 | setCursor("pointer"); 33 | card.color = Color.fromHex("#5ba675"); 34 | }); 35 | 36 | bean.onHoverEnd(() => { 37 | setCursor("default"); 38 | card.color = WHITE; 39 | }); 40 | 41 | wait(2, () => { 42 | tween( 43 | card.pos.y, 44 | center().y, 45 | 0.25, 46 | y => card.pos.y = y, 47 | easings.easeOutBack, 48 | ); 49 | }); 50 | -------------------------------------------------------------------------------- /src/game/gravity.ts: -------------------------------------------------------------------------------- 1 | // Gravity manipulation 2 | 3 | import { vec2 } from "../math/math"; 4 | import { type Vec2 } from "../math/Vec2"; 5 | import { _k } from "../shared"; 6 | 7 | export function setGravity(g: number) { 8 | // If g > 0 use either the current direction or use (0, 1) 9 | // Else null 10 | _k.game.gravity = g 11 | ? (_k.game.gravity || vec2(0, 1)).unit().scale(g) 12 | : null; 13 | } 14 | 15 | export function getGravity() { 16 | // If gravity > 0 return magnitude 17 | // Else 0 18 | return _k.game.gravity ? _k.game.gravity.len() : 0; 19 | } 20 | 21 | export function setGravityDirection(d: Vec2) { 22 | // If gravity > 0 keep magnitude, otherwise use 1 23 | _k.game.gravity = d.unit().scale( 24 | _k.game.gravity ? _k.game.gravity.len() : 1, 25 | ); 26 | } 27 | 28 | export function getGravityDirection() { 29 | // If gravity != null return unit vector, otherwise return (0, 1) 30 | return _k.game.gravity ? _k.game.gravity.unit() : vec2(0, 1); 31 | } 32 | -------------------------------------------------------------------------------- /src/ecs/components/transform/z.ts: -------------------------------------------------------------------------------- 1 | import type { Comp } from "../../../types"; 2 | 3 | /** 4 | * The serialized {@link z `z()`} component. 5 | * 6 | * @group Components 7 | * @subgroup Component Serialization 8 | */ 9 | export interface SerializedZComp { 10 | z: number; 11 | } 12 | 13 | /** 14 | * The {@link z `z()`} component. 15 | * 16 | * @group Components 17 | * @subgroup Component Types 18 | */ 19 | export interface ZComp extends Comp { 20 | /** 21 | * Defines the z-index of this game obj 22 | */ 23 | z: number; 24 | /** 25 | * Serialize the current state comp 26 | */ 27 | serialize(): SerializedZComp; 28 | } 29 | 30 | export function z(z: number): ZComp { 31 | return { 32 | id: "z", 33 | z: z, 34 | inspect() { 35 | return `z: ${this.z}`; 36 | }, 37 | serialize() { 38 | return { z: this.z }; 39 | }, 40 | }; 41 | } 42 | 43 | export function zFactory(data: SerializedZComp) { 44 | return z(data.z); 45 | } 46 | -------------------------------------------------------------------------------- /examples/children.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Children 3 | * @description How to create children on game objects. 4 | * @difficulty 1 5 | * @tags basics, gobj 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | kaplay(); 11 | 12 | loadSprite("bean", "/sprites/bean.png"); 13 | loadSprite("ghosty", "/sprites/ghosty.png"); 14 | 15 | // Adds the nucleus for the other children to get added to, it just means this is their parent 16 | const nucleus = add([ 17 | sprite("ghosty"), 18 | pos(center()), 19 | anchor("center"), 20 | ]); 21 | 22 | // Add children 23 | for (let i = 12; i < 24; i++) { 24 | nucleus.add([ 25 | sprite("bean"), 26 | rotate(0), 27 | anchor(vec2(i).scale(0.25)), 28 | { 29 | speed: i * 8, 30 | }, 31 | ]); 32 | } 33 | 34 | // Runs every frame 35 | nucleus.onUpdate(() => { 36 | nucleus.pos = mousePos(); 37 | 38 | // update children 39 | nucleus.children.forEach((child) => { 40 | child.angle += child.speed * dt(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/ecs/components/draw/raycast.ts: -------------------------------------------------------------------------------- 1 | import type { RaycastResult } from "../../../math/math"; 2 | import type { Vec2 } from "../../../math/Vec2"; 3 | import { _k } from "../../../shared"; 4 | 5 | // this is not a component lol 6 | export function raycast( 7 | origin: Vec2, 8 | direction: Vec2, 9 | exclude?: string[], 10 | ) { 11 | let minHit: RaycastResult; 12 | 13 | const shapes = _k.game.root.get("area"); 14 | 15 | shapes.forEach(s => { 16 | if (exclude && exclude.some(tag => s.is(tag))) return; 17 | const shape = s.worldArea(); 18 | const hit = shape.raycast(origin, direction); 19 | if (hit) { 20 | if (minHit) { 21 | if (hit.fraction < minHit.fraction) { 22 | minHit = hit; 23 | minHit!.object = s; 24 | } 25 | } 26 | else { 27 | minHit = hit; 28 | minHit!.object = s; 29 | } 30 | } 31 | }); 32 | 33 | return minHit!; 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert a union type to an intersection type. 3 | */ 4 | export type UnionToIntersection = ( 5 | U extends any ? (k: U) => void : never 6 | ) extends (k: infer I) => void ? I 7 | : never; 8 | 9 | /** 10 | * It removes the properties that are undefined. 11 | */ 12 | export type Defined = T extends any 13 | ? Pick 14 | : never; 15 | 16 | /** 17 | * It obligates to TypeScript to Expand the type. 18 | * 19 | * Instead of being `{ id: 1 } | { name: "hi" }` 20 | * makes 21 | * It's `{ id: 1, name: "hi" }` 22 | * 23 | * https://www.totaltypescript.com/concepts/the-prettify-helper 24 | */ 25 | export type Expand = T extends infer U ? { [K in keyof U]: U[K] } : never; 26 | 27 | /** 28 | * It merges all the objects into one. 29 | */ 30 | export type MergeObj = Expand>>; 31 | 32 | export type TupleWithoutFirst = T extends [infer R, ...infer E] 33 | ? E 34 | : never; 35 | -------------------------------------------------------------------------------- /examples/particleTrail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Particle Trail 3 | * @description How to do a mouse-following trail with particles() 4 | * @difficulty 1 5 | * @tags effects 6 | * @minver 3001.0 7 | * @category concepts 8 | * @group particles 9 | * @groupOrder 1 10 | */ 11 | 12 | kaplay(); 13 | 14 | loadSprite("hexagon", "./sprites/particle_hexagon_filled.png"); 15 | 16 | onLoad(() => { 17 | const trail = add([ 18 | pos(), 19 | particles({ 20 | max: 20, 21 | speed: [200, 250], 22 | lifeTime: [0.2, 0.75], 23 | colors: [WHITE], 24 | opacities: [1.0, 0.0], 25 | angle: [0, 360], 26 | texture: getSprite("hexagon").data.tex, 27 | quads: [getSprite("hexagon").data.frames[0]], 28 | }, { 29 | rate: 5, 30 | direction: -90, 31 | spread: 2, 32 | }), 33 | ]); 34 | 35 | onMouseMove((pos, delta) => { 36 | trail.emitter.position = pos; 37 | trail.emit(1); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /examples/drawon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Drawon Component 3 | * @description How to use Frame Buffers 4 | * @difficulty 2 5 | * @tags draw 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay(); 11 | 12 | loadBean(); 13 | 14 | onLoad(() => { 15 | const pic = new Picture(); 16 | 17 | const cached = add([ 18 | drawon(pic, { childrenOnly: true, refreshOnly: true }), 19 | picture(pic), 20 | { 21 | draw() { 22 | console.log("draw parent"); 23 | }, 24 | }, 25 | ]); 26 | 27 | const screen = new Rect(vec2(100, 100), width() - 200, height() - 200); 28 | 29 | function addBean() { 30 | const bean = cached.add([ 31 | pos(screen.random()), 32 | sprite("bean"), 33 | { 34 | draw() { 35 | console.log("draw child"); 36 | }, 37 | }, 38 | ]); 39 | cached.refresh(); 40 | } 41 | 42 | addBean(); 43 | 44 | onKeyPress("space", addBean); 45 | }); 46 | -------------------------------------------------------------------------------- /src/gfx/draw/drawStenciled.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | import { flush } from "../stack"; 3 | 4 | export function drawStenciled( 5 | content: () => void, 6 | mask: () => void, 7 | test: number, 8 | ) { 9 | const gl = _k.gfx.ggl.gl; 10 | 11 | flush(); 12 | gl.clear(gl.STENCIL_BUFFER_BIT); 13 | gl.enable(gl.STENCIL_TEST); 14 | 15 | // don't perform test, pure write 16 | gl.stencilFunc( 17 | gl.NEVER, 18 | 1, 19 | 0xFF, 20 | ); 21 | 22 | // always replace since we're writing to the buffer 23 | gl.stencilOp( 24 | gl.REPLACE, 25 | gl.REPLACE, 26 | gl.REPLACE, 27 | ); 28 | 29 | mask(); 30 | flush(); 31 | 32 | // perform test 33 | gl.stencilFunc( 34 | test, 35 | 1, 36 | 0xFF, 37 | ); 38 | 39 | // don't write since we're only testing 40 | gl.stencilOp( 41 | gl.KEEP, 42 | gl.KEEP, 43 | gl.KEEP, 44 | ); 45 | 46 | content(); 47 | flush(); 48 | gl.disable(gl.STENCIL_TEST); 49 | } 50 | -------------------------------------------------------------------------------- /examples/automaticCollider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Create a polygon shape with a sprite 3 | * @description How to make a collider automaticly 4 | * @difficulty 0 5 | * @tags basics, colliders 6 | * @minver 4000.0 // TODO: Update to actual version 7 | * @category basics 8 | * @test 9 | */ 10 | 11 | kaplay(); 12 | 13 | loadSprite("apple", "sprites/apple.png"); 14 | loadSprite("tga", "sprites/tga.png"); 15 | 16 | onDraw("gm", (gm) => { 17 | drawCircle({ 18 | radius: 1, 19 | }); 20 | }); 21 | 22 | let poly; 23 | let apple; 24 | onLoad(() => { 25 | poly = getSpriteOutline("apple", 0, true, 5); 26 | 27 | apple = add([ 28 | sprite("apple"), 29 | area({ shape: poly }), 30 | pos(200, 400), 31 | "gm", 32 | ]); 33 | }); 34 | 35 | onDraw(() => { 36 | const moved = new Polygon( 37 | poly.pts.map(p => p.add(vec2(0, 0))), 38 | ); 39 | 40 | drawPolygon(moved, { 41 | color: rgb(255, 0, 0), // red outline 42 | filled: false, // just an outline 43 | width: 2, // line thickness 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/playtests/largeTexture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Large Texture 3 | * @description Test how KAPLAY handles large textures (big sprites). 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | // Loads a random 2500px image 12 | loadSprite("bigyoshi", "/sprites/YOSHI.png"); 13 | 14 | let cameraPosition = getCamPos(); 15 | 16 | add([ 17 | sprite("bigyoshi"), 18 | ]); 19 | 20 | // Adds a label 21 | const label = add([ 22 | text("Click and drag the mouse, scroll the wheel"), 23 | z(1), 24 | ]); 25 | 26 | add([ 27 | rect(label.width, label.height), 28 | color(0, 0, 0), 29 | z(0), 30 | ]); 31 | 32 | // Mouse handling 33 | onUpdate(() => { 34 | if (isMouseDown("left") && isMouseMoved()) { 35 | cameraPosition = cameraPosition.sub( 36 | mouseDeltaPos().scale(1 / cameraScale), 37 | ); 38 | setCamPos(cameraPosition); 39 | } 40 | }); 41 | 42 | let cameraScale = 1; 43 | 44 | onScroll((delta) => { 45 | cameraScale = cameraScale * (1 - 0.1 * Math.sign(delta.y)); 46 | setCamScale(cameraScale); 47 | }); 48 | -------------------------------------------------------------------------------- /examples/layers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Layers 3 | * @description How to use layer system 4 | * @difficulty 0 5 | * @tags basics, effects, ui 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | kaplay(); 11 | 12 | layers(["bg", "game", "ui"], "game"); 13 | 14 | // bg layer 15 | add([ 16 | rect(width(), height()), 17 | layer("bg"), 18 | color(rgb(64, 128, 255)), 19 | // opacity(0.5) 20 | ]).add([text("BG")]); 21 | 22 | // game layer implicit 23 | add([ 24 | pos(3 * width() / 5, 3 * height() / 5), 25 | rect(width() / 3, height() / 3), 26 | color(rgb(255, 128, 64)), 27 | ]).add([pos(width() / 3, height() / 3), text("GAME"), anchor("botright")]); 28 | 29 | // ui layer 30 | add([ 31 | pos(center()), 32 | rect(width() / 2, height() / 2), 33 | anchor("center"), 34 | layer("ui"), 35 | color(rgb(64, 255, 128)), 36 | ]).add([text("UI"), anchor("center")]); 37 | 38 | // game layer explicit 39 | add([ 40 | pos(width() / 5, height() / 5), 41 | rect(width() / 3, height() / 3), 42 | layer("game"), 43 | color(rgb(255, 128, 64)), 44 | ]).add([text("GAME")]); 45 | -------------------------------------------------------------------------------- /tests/auto/kaplay.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from "vitest"; 2 | 3 | describe("Context Initialization", () => { 4 | beforeAll(async () => { 5 | await page.addScriptTag({ path: "dist/kaplay.js" }); 6 | }); 7 | 8 | test( 9 | "VERSION constant shouldn't be defined in global scope when kaplay({ global: false })", 10 | async () => { 11 | const version = await page.evaluate(() => { 12 | kaplay({ global: false }); 13 | // @ts-ignore 14 | return window["VERSION"]; 15 | }); 16 | 17 | expect(version).toBeUndefined(); 18 | }, 19 | 20000, 20 | ); 21 | 22 | test( 23 | "VERSION constant should be defined in global scope wwhen kaplay()", 24 | async () => { 25 | const version = await page.evaluate(() => { 26 | kaplay(); 27 | // @ts-ignore 28 | return window["VERSION"]; 29 | }); 30 | 31 | expect(version).toBeDefined(); 32 | }, 33 | 20000, 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /examples/friction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Friction 3 | * @description How to apply friction to objects 4 | * @difficulty 0 5 | * @tags physics 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay({ scale: 0.5 }); 11 | loadSprite("bean", "/sprites/bean.png"); 12 | loadSprite("grass", "/sprites/grass.png"); 13 | setGravity(3200); 14 | const level = addLevel([ 15 | "@ = ", 16 | "", 17 | "======= ", 18 | " = ", 19 | " =========", 20 | ], { 21 | tileWidth: 64, 22 | tileHeight: 64, 23 | pos: vec2(100, 200), 24 | tiles: { 25 | "@": () => [ 26 | sprite("bean"), 27 | area({ friction: 0.02, restitution: 0 }), 28 | body(), 29 | anchor("bot"), 30 | "player", 31 | ], 32 | "=": () => [ 33 | sprite("grass"), 34 | area({ friction: 0.02, restitution: 0 }), 35 | body({ isStatic: true }), 36 | anchor("bot"), 37 | ], 38 | }, 39 | }); 40 | 41 | const player = level.get("player")[0]; 42 | player.vel.x = 480; 43 | debug.log(player.friction); 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 KAPLAY Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/playtests/textTall.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Tall fonts test 3 | * @description Test the rendering of tall fonts to ensure they're not cropped. 4 | * @difficulty 99 5 | * @tags basics 6 | * @minver 4000.0 7 | */ 8 | 9 | kaplay({ background: "#000000", crisp: true }); 10 | const nabla = loadFont( 11 | "Nabla", 12 | "/fonts/Nabla-Regular-VariableFont_EDPT,EHLT.ttf", 13 | { filter: "nearest" }, 14 | ); 15 | const anton = loadFont("Anton", "/fonts/Anton-Regular.ttf", { 16 | filter: "nearest", 17 | }); 18 | 19 | const obj = add([ 20 | pos(10, 10), 21 | text( 22 | "This is a tall font\nThat shouldn't \nget cropped by KAPLAY", 23 | { 24 | font: "Nabla", 25 | width: width(), 26 | size: 48, 27 | color: rgb(255, 255, 255), 28 | }, 29 | ), 30 | ]); 31 | 32 | add([ 33 | pos(10, 250), 34 | text( 35 | "This is another tall font\nThat also \nshouldn't get cropped", 36 | { 37 | font: "Anton", 38 | width: width(), 39 | size: 32, 40 | color: rgb(255, 255, 0), 41 | }, 42 | ), 43 | ]); 44 | -------------------------------------------------------------------------------- /tests/auto/missingComps.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from "vitest"; 2 | 3 | // [subject] should [behavior when condition] 4 | 5 | describe("Components validation in add()", async () => { 6 | beforeAll(async () => { 7 | await page.addScriptTag({ path: "dist/kaplay.js" }); 8 | }); 9 | 10 | test( 11 | "add() should throw an error when a body() without pos() is passed", 12 | async () => { 13 | async function useBodyWithoutPos() { 14 | return page.evaluate(() => { 15 | const k = kaplay(); 16 | 17 | return new Promise((res, rej) => { 18 | k.onError((e) => { 19 | console.log(e); 20 | rej(e.message); 21 | }); 22 | 23 | k.add([ 24 | k.body(), 25 | ]); 26 | }); 27 | }); 28 | } 29 | 30 | await expect(useBodyWithoutPos).rejects.toThrow(/requires/); 31 | }, 32 | 20000, 33 | ); 34 | }); 35 | -------------------------------------------------------------------------------- /examples/shader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Shaders 3 | * @description How to use shaders 4 | * @difficulty 1 5 | * @tags basics, effects 6 | * @minver 3001.0 7 | * @category basics 8 | * @test 9 | */ 10 | 11 | // Custom shader 12 | kaplay(); 13 | 14 | loadSprite("bean", "/sprites/bean.png"); 15 | 16 | // Load a shader with custom fragment shader code 17 | // The fragment shader should define a function "frag", which returns a color and receives the vertex position, texture coodinate, vertex color, and texture as arguments 18 | // There's also the def_frag() function which returns the default fragment color 19 | loadShader( 20 | "invert", 21 | null, 22 | ` 23 | uniform float u_time; 24 | 25 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 26 | vec4 c = def_frag(); 27 | float t = (sin(u_time * 4.0) + 1.0) / 2.0; 28 | return mix(c, vec4(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a), t); 29 | } 30 | `, 31 | ); 32 | 33 | add([ 34 | sprite("bean"), 35 | pos(80, 40), 36 | scale(8), 37 | // Use the shader with shader() component and pass uniforms 38 | shader("invert", () => ({ 39 | "u_time": time(), 40 | })), 41 | ]); 42 | -------------------------------------------------------------------------------- /src/ecs/components/draw/color.ts: -------------------------------------------------------------------------------- 1 | import { Color, type ColorArgs, rgb, type RGBValue } from "../../../math/color"; 2 | import type { Comp } from "../../../types"; 3 | 4 | /** 5 | * The serialized {@link color `color()`} component. 6 | * 7 | * @group Components 8 | * @subgroup Component Serialization 9 | */ 10 | export interface SerializedColorComp { 11 | color: { r: number; g: number; b: number }; 12 | } 13 | 14 | /** 15 | * The {@link color `color()`} component. 16 | * 17 | * @group Components 18 | * @subgroup Component Types 19 | */ 20 | export interface ColorComp extends Comp { 21 | color: Color; 22 | serialize(): SerializedColorComp; 23 | } 24 | 25 | export function color(...args: ColorArgs): ColorComp { 26 | return { 27 | id: "color", 28 | color: rgb(...args), 29 | inspect() { 30 | return `color: ${this.color.toString()}`; 31 | }, 32 | serialize() { 33 | return { 34 | color: this.color.serialize(), 35 | }; 36 | }, 37 | }; 38 | } 39 | 40 | export function colorFactory(data: any) { 41 | return color(Color.deserialize(data)); 42 | } 43 | -------------------------------------------------------------------------------- /examples/restitution.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Restitution 3 | * @description How to make objects bounce 4 | * @difficulty 0 5 | * @tags physics 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | kaplay({ scale: 0.5 }); 12 | 13 | loadSprite("bean", "/sprites/bean.png"); 14 | loadSprite("grass", "/sprites/grass.png"); 15 | 16 | setGravity(3200); 17 | 18 | const level = addLevel([ 19 | "@ = ", 20 | "", 21 | "======= ", 22 | " = ", 23 | " =========", 24 | ], { 25 | tileWidth: 64, 26 | tileHeight: 64, 27 | pos: vec2(100, 200), 28 | tiles: { 29 | "@": () => [ 30 | sprite("bean"), 31 | area({ friction: 0, restitution: 1 }), 32 | body(), 33 | anchor("bot"), 34 | "player", 35 | ], 36 | "=": () => [ 37 | sprite("grass"), 38 | area({ friction: 0, restitution: 1 }), 39 | body({ isStatic: true }), 40 | anchor("bot"), 41 | ], 42 | }, 43 | }); 44 | 45 | const player = level.get("player")[0]; 46 | player.vel.x = 480; 47 | debug.log(player.friction); 48 | -------------------------------------------------------------------------------- /examples/lifespan.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Lifespan 3 | * @description How to use the lifespan component. 4 | * @difficulty 0 5 | * @tags basics, comps 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | kaplay(); 11 | 12 | const sprites = [ 13 | "apple", 14 | "heart", 15 | "coin", 16 | "meat", 17 | "lightening", 18 | ]; 19 | 20 | sprites.forEach((spr) => { 21 | loadSprite(spr, `/sprites/${spr}.png`); 22 | }); 23 | 24 | setGravity(800); 25 | 26 | // Spawn one object every 0.1 second 27 | loop(0.1, () => { 28 | // Compose object properties with components 29 | const item = add([ 30 | pos(center()), 31 | sprite(choose(sprites)), 32 | anchor("center"), 33 | scale(rand(0.5, 1)), 34 | area({ collisionIgnore: ["fruit"] }), 35 | body(), 36 | // lifespan() comp destroys the object after desired seconds 37 | lifespan(1, { 38 | // it will fade after 0.5 seconds 39 | fade: 0.5, 40 | }), 41 | opacity(1), 42 | move(choose([LEFT, RIGHT]), rand(60, 240)), 43 | "fruit", 44 | ]); 45 | 46 | item.jump(rand(320, 640)); 47 | }); 48 | -------------------------------------------------------------------------------- /src/gfx/draw/drawTriangle.ts: -------------------------------------------------------------------------------- 1 | import type { Vec2 } from "../../math/Vec2"; 2 | import type { RenderProps } from "../../types"; 3 | import { drawPolygon } from "./drawPolygon"; 4 | 5 | /** 6 | * How the triangle should look like. 7 | * 8 | * @group Draw 9 | * @subgroup Types 10 | */ 11 | export type DrawTriangleOpt = RenderProps & { 12 | /** 13 | * First point of triangle. 14 | */ 15 | p1: Vec2; 16 | /** 17 | * Second point of triangle. 18 | */ 19 | p2: Vec2; 20 | /** 21 | * Third point of triangle. 22 | */ 23 | p3: Vec2; 24 | /** 25 | * If fill the shape with color (set this to false if you only want an outline). 26 | */ 27 | fill?: boolean; 28 | /** 29 | * The radius of each corner. 30 | */ 31 | radius?: number; 32 | }; 33 | 34 | export function drawTriangle(opt: DrawTriangleOpt) { 35 | if (!opt.p1 || !opt.p2 || !opt.p3) { 36 | throw new Error( 37 | "drawTriangle() requires properties \"p1\", \"p2\" and \"p3\".", 38 | ); 39 | } 40 | 41 | return drawPolygon(Object.assign({}, opt, { 42 | pts: [opt.p1, opt.p2, opt.p3], 43 | })); 44 | } 45 | -------------------------------------------------------------------------------- /src/ecs/components/misc/stay.ts: -------------------------------------------------------------------------------- 1 | import type { Comp } from "../../../types"; 2 | 3 | /** 4 | * The serialized {@link stay `stay()`} component. 5 | * 6 | * @group Components 7 | * @subgroup Component Serialization 8 | */ 9 | export interface SerializeStayComp { 10 | scenesToStay: string[]; 11 | } 12 | 13 | /** 14 | * The {@link stay `stay()`} component. 15 | * 16 | * @group Components 17 | * @subgroup Component Types 18 | */ 19 | export interface StayComp extends Comp { 20 | /** 21 | * If the obj should not be destroyed on scene switch. 22 | */ 23 | stay: boolean; 24 | /** 25 | * Array of scenes that the obj will stay on. 26 | */ 27 | scenesToStay?: string[]; 28 | serialize(): SerializeStayComp; 29 | } 30 | 31 | export function stay(scenesToStay?: string[]): StayComp { 32 | return { 33 | id: "stay", 34 | stay: true, 35 | scenesToStay, 36 | serialize() { 37 | return { 38 | scenesToStay: scenesToStay ?? [], 39 | }; 40 | }, 41 | }; 42 | } 43 | 44 | export function stayFactory(data: SerializeStayComp) { 45 | return stay(data.scenesToStay); 46 | } 47 | -------------------------------------------------------------------------------- /examples/shapeRect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Rect 3 | * @description The different options for the rect() component. 4 | * @difficulty 0 5 | * @tags basics, draw 6 | * @minver 3001.0 7 | * @category basics 8 | * @test 9 | */ 10 | 11 | kaplay(); 12 | 13 | add([ 14 | rect(100, 100, { radius: 20 }), 15 | pos(100, 100), 16 | rotate(0), 17 | anchor("center"), 18 | ]); 19 | 20 | add([ 21 | rect(100, 100, { radius: [10, 20, 30, 40] }), 22 | pos(250, 100), 23 | rotate(0), 24 | anchor("center"), 25 | ]); 26 | 27 | add([ 28 | rect(100, 100, { radius: [0, 20, 0, 20] }), 29 | pos(400, 100), 30 | rotate(0), 31 | anchor("center"), 32 | ]); 33 | 34 | add([ 35 | rect(100, 100, { radius: 20 }), 36 | pos(100, 250), 37 | rotate(0), 38 | anchor("center"), 39 | outline(4, BLACK), 40 | ]); 41 | 42 | add([ 43 | rect(100, 100, { radius: [10, 20, 30, 40] }), 44 | pos(250, 250), 45 | rotate(0), 46 | anchor("center"), 47 | outline(4, BLACK), 48 | ]); 49 | 50 | add([ 51 | rect(100, 100, { radius: [0, 20, 0, 20] }), 52 | pos(400, 250), 53 | rotate(0), 54 | anchor("center"), 55 | outline(4, BLACK), 56 | ]); 57 | -------------------------------------------------------------------------------- /src/ecs/components/draw/drawon.ts: -------------------------------------------------------------------------------- 1 | import type { Picture } from "../../../gfx/draw/drawPicture"; 2 | import type { FrameBuffer } from "../../../gfx/FrameBuffer"; 3 | import type { Comp, GameObj } from "../../../types"; 4 | 5 | /** 6 | * Options for the {@link drawon `drawon()`} component. 7 | * 8 | * @group Components 9 | * @subgroup Component Types 10 | */ 11 | export type DrawonCompOpt = { 12 | childrenOnly?: boolean; 13 | refreshOnly?: boolean; 14 | }; 15 | 16 | /** 17 | * The {@link drawon `drawon()`} component. 18 | * 19 | * @group Components 20 | * @subgroup Component Types 21 | */ 22 | export interface DrawonComp extends Comp { 23 | refresh(): void; 24 | } 25 | 26 | export function drawon(c: FrameBuffer | Picture, opt?: DrawonCompOpt) { 27 | return { 28 | add(this: GameObj) { 29 | this.target = { 30 | destination: c, 31 | childrenOnly: opt?.childrenOnly, 32 | refreshOnly: opt?.refreshOnly, 33 | }; 34 | }, 35 | refresh(this: GameObj) { 36 | if (this.target) { 37 | this.target.isFresh = false; 38 | } 39 | }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /tests/playtests/textStyleChangeFont.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Style change font 3 | * @description Test how text handles changing font on styles. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay({ background: "#000000", crisp: true }); 10 | loadFont("Romantique", "/fonts/Romantique.ttf", { filter: "nearest" }); 11 | 12 | const x = add([ 13 | text( 14 | "everything here is arial except the e's are romantique and red and stretched" 15 | .replaceAll("e", "[e]e[/e]"), 16 | { 17 | transform: { 18 | font: "Arial", 19 | stretchInPlace: false, 20 | }, 21 | styles: { 22 | e: (i, ch) => { 23 | const w = wave(0.5, 2, time() * 3 + i); 24 | return { 25 | font: "Romantique", 26 | color: RED, 27 | scale: vec2(w, 1), 28 | stretchInPlace: w > 1, 29 | }; 30 | }, 31 | }, 32 | width: width(), 33 | size: 75, 34 | }, 35 | ), 36 | ]); 37 | 38 | onUpdate(() => x.width = width()); 39 | -------------------------------------------------------------------------------- /.github/workflows/sync-playground.yml: -------------------------------------------------------------------------------- 1 | name: Bump kaplayjs/kaplayground submodule 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | workflow_dispatch: 7 | 8 | 9 | jobs: 10 | sync: 11 | permissions: write-all 12 | name: "Dispatch Event" 13 | runs-on: ubuntu-latest 14 | if: github.repository_owner == 'kaplayjs' 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - name: Check if there are changes in examples/ folder between HEAD and last commit 21 | id: check_changes 22 | run: | 23 | CHANGES=$(git diff --name-only HEAD^ HEAD -- examples/ | paste -sd "," -) 24 | echo "changes=$CHANGES" >> $GITHUB_OUTPUT 25 | - name: Dispatch Event 26 | uses: actions/github-script@v6 27 | if: steps.check_changes.outputs.changes != '' 28 | with: 29 | github-token: ${{ secrets.PAT_TOKEN }} 30 | script: | 31 | await github.rest.actions.createWorkflowDispatch({ 32 | owner: 'kaplayjs', 33 | repo: 'kaplayground', 34 | workflow_id: 'sync-submodules.yml', 35 | ref: 'master' 36 | }) -------------------------------------------------------------------------------- /examples/drawoncanvas.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | 3 | loadBean(); 4 | 5 | loadShader( 6 | "invert", 7 | null, 8 | ` 9 | uniform float u_time; 10 | 11 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 12 | vec4 c = def_frag(); 13 | float t = (sin(u_time * 4.0) + 1.0) / 2.0; 14 | return mix(c, vec4(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a), t); 15 | } 16 | `, 17 | ); 18 | 19 | onLoad(() => { 20 | const canvas = makeCanvas(width(), height()); 21 | 22 | const cached = add([ 23 | drawon(canvas.fb, { childrenOnly: true, refreshOnly: true }), 24 | { 25 | draw() { 26 | drawCanvas({ 27 | canvas, 28 | shader: "invert", 29 | uniform: { "u_time": time() }, 30 | }); 31 | }, 32 | }, 33 | shader("invert"), 34 | ]); 35 | 36 | const screen = new Rect(vec2(100, 100), width() - 200, height() - 200); 37 | 38 | function addBean() { 39 | const bean = cached.add([ 40 | pos(screen.random()), 41 | sprite("bean"), 42 | ]); 43 | cached.refresh(); 44 | } 45 | 46 | addBean(); 47 | 48 | onKeyPress("space", addBean); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/playtests/fastLoop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Fast loop 3 | * @description TBD 4 | * @difficulty 0 5 | * @tags ui, input 6 | * @minver 4000.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay(); 11 | 12 | loadBean(); 13 | 14 | const interval = 0.001; 15 | const delay = 1; 16 | 17 | loop(interval, () => { 18 | add([ 19 | sprite("bean"), 20 | pos(rand(vec2(0), vec2(width(), height()))), 21 | lifespan(delay), 22 | opacity(1), 23 | ]); 24 | }); 25 | 26 | const counter = add([ 27 | pos(10, 10), 28 | text(""), 29 | z(9999), 30 | ]); 31 | add([ 32 | pos(counter.pos), 33 | rect(counter.width, counter.height), 34 | color(BLACK), 35 | z(counter.z - 1), 36 | { 37 | update() { 38 | this.width = counter.width; 39 | this.height = counter.height; 40 | }, 41 | }, 42 | ]); 43 | 44 | var beanCount = 0; 45 | onAdd(() => beanCount++); 46 | onDestroy(() => beanCount--); 47 | loop(0.1, () => { 48 | const error = 100 * Math.abs((delay / interval) - beanCount) 49 | / (delay / interval); 50 | counter.text = `${beanCount.toFixed().padStart(5)} beans\nerror: ${ 51 | error.toFixed(2).padStart(5) 52 | }%`; 53 | }); 54 | -------------------------------------------------------------------------------- /tests/playtests/textStyleOverride.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Style override 3 | * @description Test overriding different styles in text objects 4 | * @difficulty 3 5 | * @tags text 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay({ background: "#000000" }); 10 | 11 | add([ 12 | pos(100, 100), 13 | text("No override: Hello [foo]styled[/foo] text", { 14 | transform: { 15 | color: WHITE.darken(200), 16 | }, 17 | styles: { 18 | foo: { 19 | color: RED, 20 | }, 21 | }, 22 | }), 23 | ]); 24 | 25 | add([ 26 | pos(100, 150), 27 | text("With override: Hello [foo]styled[/foo] text", { 28 | transform: { 29 | color: WHITE.darken(200), 30 | }, 31 | styles: { 32 | foo: { 33 | color: RED, 34 | override: true, 35 | }, 36 | }, 37 | }), 38 | ]); 39 | 40 | add([ 41 | pos(100, 200), 42 | text("With override and color(): Hello [foo]styled[/foo] text", { 43 | styles: { 44 | foo: { 45 | color: RED, 46 | override: true, 47 | }, 48 | }, 49 | }), 50 | color(WHITE.darken(200)), 51 | ]); 52 | -------------------------------------------------------------------------------- /examples/polygongeneration.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | 3 | const hex = createRegularPolygon(50, 6); 4 | 5 | add([ 6 | pos(100, 100), 7 | polygon(hex), 8 | ]); 9 | 10 | const star = createStarPolygon(50, 20, 16); 11 | 12 | add([ 13 | pos(200, 100), 14 | polygon(star), 15 | color(YELLOW), 16 | ]); 17 | 18 | const cog1 = createCogPolygon(50, 35, 32); 19 | const cog2 = createCogPolygon(35, 20, 24, 90); 20 | 21 | const blueCog = add([ 22 | pos(300, 100), 23 | polygon(cog1), 24 | color(BLUE), 25 | rotate(0), 26 | area(), 27 | "cog", 28 | ]); 29 | 30 | add([ 31 | pos(375, 100), 32 | polygon(cog2), 33 | color(RED), 34 | rotate(0), 35 | constraint.rotation(blueCog, { strength: 1, scale: -32 / 24 }), 36 | area(), 37 | "cog", 38 | ]); 39 | 40 | let obj; 41 | 42 | onClick("cog", (o) => { 43 | obj = o; 44 | }); 45 | 46 | onMouseMove((newPos, delta) => { 47 | if (obj && obj.has("rotate")) { 48 | const oldPos = newPos.sub(delta); 49 | const oldAngle = oldPos.sub(obj.pos).angle(); 50 | const newAngle = newPos.sub(obj.pos).angle(); 51 | const deltaAngle = newAngle - oldAngle; 52 | obj.angle += deltaAngle; 53 | } 54 | }); 55 | 56 | onMouseRelease(() => { 57 | obj = null; 58 | }); 59 | -------------------------------------------------------------------------------- /examples/livequery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Live query 3 | * @description How to live update a get() action. 4 | * @difficulty 1 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // How to keep a get() always updated 12 | 13 | kaplay(); 14 | 15 | loadSprite("ghosty", "/sprites/ghosty.png"); 16 | 17 | const q = get("area", { liveUpdate: true }); 18 | 19 | loop(5, () => { 20 | if (q.length < 10) { 21 | const x = rand(0, width()); 22 | const y = rand(0, height()); 23 | 24 | const ghost = add([ 25 | sprite("ghosty"), 26 | pos(x, y), 27 | area(), 28 | timer(), 29 | color(WHITE), 30 | "touchable", 31 | ]); 32 | ghost.wait(5, () => { 33 | ghost.unuse("area"); 34 | ghost.untag("touchable"); 35 | ghost.use(color(RED)); 36 | ghost.wait(5, () => { 37 | ghost.use(area()); 38 | ghost.tag("touchable"); 39 | ghost.use(color(WHITE)); 40 | }); 41 | }); 42 | } 43 | }); 44 | 45 | onClick("touchable", (ghost) => { 46 | ghost.destroy(); 47 | }); 48 | 49 | loop(1, () => { 50 | debug.log(`There are ${q.length} touchable ghosts`); 51 | }); 52 | -------------------------------------------------------------------------------- /src/utils/dataURL.ts: -------------------------------------------------------------------------------- 1 | export function base64ToArrayBuffer(base64: string): ArrayBuffer { 2 | const binstr = window.atob(base64); 3 | const len = binstr.length; 4 | const bytes = new Uint8Array(len); 5 | for (let i = 0; i < len; i++) { 6 | bytes[i] = binstr.charCodeAt(i); 7 | } 8 | return bytes.buffer; 9 | } 10 | 11 | export function dataURLToArrayBuffer(url: string): ArrayBuffer { 12 | return base64ToArrayBuffer(url.split(",")[1]); 13 | } 14 | 15 | export function download(filename: string, url: string) { 16 | const a = document.createElement("a"); 17 | a.href = url; 18 | a.download = filename; 19 | a.click(); 20 | } 21 | 22 | export function downloadText(filename: string, text: string) { 23 | download(filename, "data:text/plain;charset=utf-8," + text); 24 | } 25 | 26 | export function downloadJSON(filename: string, data: any) { 27 | downloadText(filename, JSON.stringify(data)); 28 | } 29 | 30 | export function downloadBlob(filename: string, blob: Blob) { 31 | const url = URL.createObjectURL(blob); 32 | download(filename, url); 33 | URL.revokeObjectURL(url); 34 | } 35 | 36 | export const isDataURL = (str: string) => str.match(/^data:\w+\/\w+;base64,.+/); 37 | 38 | export const getFileName = (p: string) => p.split(".").slice(0, -1).join("."); 39 | -------------------------------------------------------------------------------- /src/utils/overload.ts: -------------------------------------------------------------------------------- 1 | type Func = (...args: any[]) => any; 2 | 3 | export function overload2( 4 | fn1: A, 5 | fn2: B, 6 | ): A & B { 7 | return ((...args) => { 8 | const al = args.length; 9 | if (al === fn1.length) return fn1(...args); 10 | if (al === fn2.length) return fn2(...args); 11 | }) as A & B; 12 | } 13 | 14 | export function overload3< 15 | A extends Func, 16 | B extends Func, 17 | C extends Func, 18 | >(fn1: A, fn2: B, fn3: C): A & B & C { 19 | return ((...args) => { 20 | const al = args.length; 21 | if (al === fn1.length) return fn1(...args); 22 | if (al === fn2.length) return fn2(...args); 23 | if (al === fn3.length) return fn3(...args); 24 | }) as A & B & C; 25 | } 26 | 27 | export function overload4< 28 | A extends Func, 29 | B extends Func, 30 | C extends Func, 31 | D extends Func, 32 | >(fn1: A, fn2: B, fn3: C, fn4: D): A & B & C & D { 33 | return ((...args) => { 34 | const al = args.length; 35 | if (al === fn1.length) return fn1(...args); 36 | if (al === fn2.length) return fn2(...args); 37 | if (al === fn3.length) return fn3(...args); 38 | if (al === fn4.length) return fn4(...args); 39 | }) as A & B & C & D; 40 | } 41 | -------------------------------------------------------------------------------- /src/ecs/components/transform/follow.ts: -------------------------------------------------------------------------------- 1 | import { vec2 } from "../../../math/math"; 2 | import { Vec2 } from "../../../math/Vec2"; 3 | import type { Comp, GameObj } from "../../../types"; 4 | import type { PosComp } from "./pos"; 5 | 6 | /** 7 | * The {@link follow `follow()`} component. 8 | * 9 | * @group Components 10 | * @subgroup Component Types 11 | */ 12 | export interface FollowComp extends Comp { 13 | follow: { 14 | /** 15 | * The object to follow. 16 | */ 17 | obj: GameObj; 18 | /** 19 | * The offset to follow the object by. 20 | */ 21 | offset: Vec2; 22 | }; 23 | } 24 | 25 | export function follow(obj: GameObj, offset?: Vec2): FollowComp { 26 | return { 27 | id: "follow", 28 | require: ["pos"], 29 | follow: { 30 | obj: obj, 31 | offset: offset ?? vec2(0), 32 | }, 33 | add(this: GameObj) { 34 | if (obj.exists()) { 35 | this.pos = this.follow.obj.pos.add(this.follow.offset); 36 | } 37 | }, 38 | update(this: GameObj) { 39 | if (obj.exists()) { 40 | this.pos = this.follow.obj.pos.add(this.follow.offset); 41 | } 42 | }, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /tests/auto/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from "vitest"; 2 | import type { KAPLAYCtx } from "../../src/types"; 3 | 4 | describe("Plugin loading", () => { 5 | beforeAll(async () => { 6 | await page.addScriptTag({ path: "dist/kaplay.js" }); 7 | }); 8 | 9 | test("testPlugin methods should exist in context", async () => { 10 | const method = await page.evaluate(() => { 11 | const testPlugin = (k: KAPLAYCtx) => ({ 12 | myMethod() { 13 | return k.VERSION; 14 | }, 15 | }); 16 | 17 | const k = kaplay({ plugins: [testPlugin] }); 18 | 19 | return k.myMethod; 20 | }); 21 | 22 | expect(method).toBeDefined(); 23 | }, 20000); 24 | 25 | test("testPlugin methods should work in context", async () => { 26 | const [version, methodResult] = await page.evaluate(() => { 27 | const testPlugin = (k: KAPLAYCtx) => ({ 28 | myMethod() { 29 | return k.VERSION; 30 | }, 31 | }); 32 | 33 | const k = kaplay({ plugins: [testPlugin] }); 34 | 35 | return [k.VERSION, k.myMethod()]; 36 | }); 37 | 38 | expect(methodResult).toBe(version); 39 | }, 20000); 40 | }); 41 | -------------------------------------------------------------------------------- /src/ecs/components/draw/blend.ts: -------------------------------------------------------------------------------- 1 | import { BlendMode, type Comp } from "../../../types"; 2 | 3 | /** 4 | * The serialized {@link blend `blend()`} component. 5 | * 6 | * @group Components 7 | * @subgroup Component Serialization 8 | */ 9 | export interface SerializedBlendComp { 10 | blend: BlendMode; 11 | } 12 | 13 | /** 14 | * The {@link blend `blend()`} component. 15 | * 16 | * @group Components 17 | * @subgroup Component Types 18 | */ 19 | export interface BlendComp extends Comp { 20 | blend: BlendMode; 21 | serialize(): SerializedBlendComp; 22 | } 23 | 24 | export function blend(blend: BlendMode): BlendComp { 25 | return { 26 | id: "blend", 27 | blend: blend ?? BlendMode.Normal, 28 | inspect() { 29 | return `blend: ${ 30 | this.blend == BlendMode.Normal 31 | ? "normal" 32 | : this.blend == BlendMode.Add 33 | ? "add" 34 | : this.blend == BlendMode.Multiply 35 | ? "multiply" 36 | : "screen" 37 | }`; 38 | }, 39 | serialize() { 40 | return { blend: this.blend }; 41 | }, 42 | }; 43 | } 44 | 45 | export function blendFactory(data: SerializedBlendComp) { 46 | return blend(data.blend); 47 | } 48 | -------------------------------------------------------------------------------- /tests/playtests/debugFramePhysics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Step Frame 3 | * @description Test different physics using debug.stepFrame(). 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | // Build levels with addLevel() 10 | 11 | // Start game 12 | kaplay(); 13 | 14 | // Load assets 15 | loadSprite("coin", "/sprites/coin.png"); 16 | loadSprite("grass", "/sprites/grass.png"); 17 | 18 | setGravity(2400); 19 | 20 | addLevel([ 21 | // Design the level layout with symbols 22 | " ", 23 | " ", 24 | " ", 25 | " ", 26 | "=======", 27 | ], { 28 | // The size of each grid 29 | tileWidth: 64, 30 | tileHeight: 64, 31 | // The position of the top left block 32 | pos: vec2(100), 33 | // Define what each symbol means (in components) 34 | tiles: { 35 | "=": () => [ 36 | sprite("grass"), 37 | area(), 38 | body({ isStatic: true }), 39 | ], 40 | }, 41 | }); 42 | 43 | loop(0.2, () => { 44 | const coin = add([ 45 | pos(rand(100, 400), 0), 46 | sprite("coin"), 47 | area(), 48 | body(), 49 | "coin", 50 | ]); 51 | wait(3, () => coin.destroy()); 52 | }); 53 | 54 | debug.paused = true; 55 | 56 | onKeyPressRepeat("space", () => { 57 | debug.stepFrame(); 58 | }); 59 | -------------------------------------------------------------------------------- /src/core/quit.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | 3 | export const quit = () => { 4 | const { game, app, gfx, ggl, gc } = _k; 5 | game.events.onOnce("frameEnd", () => { 6 | app.quit(); 7 | 8 | // clear canvas 9 | gfx.gl.clear( 10 | gfx.gl.COLOR_BUFFER_BIT | gfx.gl.DEPTH_BUFFER_BIT 11 | | gfx.gl.STENCIL_BUFFER_BIT, 12 | ); 13 | 14 | // unbind everything 15 | const numTextureUnits = gfx.gl.getParameter( 16 | gfx.gl.MAX_TEXTURE_IMAGE_UNITS, 17 | ); 18 | 19 | for (let unit = 0; unit < numTextureUnits; unit++) { 20 | gfx.gl.activeTexture(gfx.gl.TEXTURE0 + unit); 21 | gfx.gl.bindTexture(gfx.gl.TEXTURE_2D, null); 22 | gfx.gl.bindTexture(gfx.gl.TEXTURE_CUBE_MAP, null); 23 | } 24 | 25 | gfx.gl.bindBuffer(gfx.gl.ARRAY_BUFFER, null); 26 | gfx.gl.bindBuffer(gfx.gl.ELEMENT_ARRAY_BUFFER, null); 27 | gfx.gl.bindRenderbuffer(gfx.gl.RENDERBUFFER, null); 28 | gfx.gl.bindFramebuffer(gfx.gl.FRAMEBUFFER, null); 29 | 30 | // run all scattered gc events 31 | ggl.destroy(); 32 | gc.forEach((f) => f()); 33 | 34 | // remove canvas 35 | app.canvas.remove(); 36 | }); 37 | }; 38 | 39 | export const onCleanup = (action: () => void) => { 40 | _k.gc.push(action); 41 | }; 42 | -------------------------------------------------------------------------------- /src/ecs/systems/systems.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | 3 | /** 4 | * @group Plugins 5 | */ 6 | export type System = { 7 | name: string; 8 | run: () => void; 9 | when: SystemPhase[]; 10 | }; 11 | 12 | export enum SystemPhase { 13 | BeforeUpdate, 14 | BeforeFixedUpdate, 15 | BeforeDraw, 16 | AfterUpdate, 17 | AfterFixedUpdate, 18 | AfterDraw, 19 | } 20 | 21 | export const system = ( 22 | name: string, 23 | action: () => void, 24 | when: SystemPhase[], 25 | ) => { 26 | const systems = _k.game.systems; 27 | const replacingSystemIdx = systems.findIndex((s) => s.name === name); 28 | 29 | // if existent system, remove it 30 | if (replacingSystemIdx != -1) { 31 | const replacingSystem = systems[replacingSystemIdx]; 32 | const when = replacingSystem.when; 33 | 34 | for (const loc of when) { 35 | const idx = _k.game.systemsByEvent[loc].findIndex( 36 | (s) => s.name === name, 37 | ); 38 | _k.game.systemsByEvent[loc].splice(idx, 1); 39 | } 40 | } 41 | 42 | const system: System = { 43 | name, 44 | run: action, 45 | when, 46 | }; 47 | 48 | for (const loc of when) { 49 | _k.game.systemsByEvent[loc].push(system); 50 | } 51 | 52 | systems.push({ name, run: action, when }); 53 | }; 54 | -------------------------------------------------------------------------------- /examples/levelRaycast.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Level Raycast 3 | * @description How to use raycasts in a level environment 4 | * @difficulty 1 5 | * @tags basics, comps 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | kaplay({ 11 | background: [31, 16, 42], 12 | }); 13 | 14 | loadSprite("grass", "/sprites/grass.png"); 15 | 16 | const level = addLevel([ 17 | "===", 18 | "= =", 19 | "===", 20 | ], { 21 | tileWidth: 64, 22 | tileHeight: 64, 23 | pos: vec2(256, 128), 24 | tiles: { 25 | "=": () => [ 26 | sprite("grass"), 27 | area(), 28 | ], 29 | }, 30 | }); 31 | level.use(rotate(45)); 32 | 33 | onLoad(() => { 34 | level.spawn([ 35 | pos( 36 | level.tileWidth() * 1.5, 37 | level.tileHeight() * 1.5, 38 | ), 39 | circle(6), 40 | color("#ea6262"), 41 | { 42 | add() { 43 | const rayHit = level.raycast( 44 | this.pos, 45 | Vec2.fromAngle(0).scale(100), 46 | ); 47 | 48 | debug.log( 49 | `${rayHit != null} ${ 50 | rayHit && rayHit.object ? rayHit.object.id : -1 51 | }`, 52 | ); 53 | }, 54 | }, 55 | ]); 56 | }); 57 | 58 | debug.inspect = true; 59 | -------------------------------------------------------------------------------- /src/gfx/anchor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BOTTOM, 3 | BOTTOM_LEFT, 4 | BOTTOM_RIGHT, 5 | CENTER, 6 | LEFT, 7 | RIGHT, 8 | TOP, 9 | TOP_LEFT, 10 | TOP_RIGHT, 11 | } from "../constants/math"; 12 | import { Vec2 } from "../math/Vec2"; 13 | import { type Anchor } from "../types"; 14 | import type { TextAlign } from "./draw/drawText"; 15 | 16 | // convert anchor string to a vec2 offset 17 | export function anchorPt(orig: Anchor | Vec2): Vec2 { 18 | switch (orig) { 19 | case "topleft": 20 | return TOP_LEFT; 21 | case "top": 22 | return TOP; 23 | case "topright": 24 | return TOP_RIGHT; 25 | case "left": 26 | return LEFT; 27 | case "center": 28 | return CENTER; 29 | case "right": 30 | return RIGHT; 31 | case "botleft": 32 | return BOTTOM_LEFT; 33 | case "bot": 34 | return BOTTOM; 35 | case "botright": 36 | return BOTTOM_RIGHT; 37 | default: 38 | return orig; 39 | } 40 | } 41 | 42 | export function alignPt(align: TextAlign): number { 43 | switch (align) { 44 | case "left": 45 | return 0; 46 | case "center": 47 | return 0.5; 48 | case "right": 49 | return 1; 50 | default: 51 | return 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/particle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Particle 3 | * @description How to use particles() 4 | * @difficulty 1 5 | * @tags effects 6 | * @minver 3001.0 7 | * @category concepts 8 | * @group particles 9 | * @groupOrder 0 10 | */ 11 | 12 | // Creating particles using Particle Component 13 | 14 | kaplay(); 15 | 16 | loadSprite("star", "./sprites/particle_star_filled.png"); 17 | 18 | onLoad(() => { 19 | go("game"); 20 | }); 21 | 22 | function woah() { 23 | const parts = add([ 24 | pos(center()), 25 | particles({ 26 | max: 20, 27 | speed: [50, 100], 28 | angle: [0, 360], 29 | angularVelocity: [45, 90], 30 | lifeTime: [1.0, 1.5], 31 | colors: [rgb(128, 128, 255), WHITE], 32 | opacities: [0.1, 1.0, 0.0], 33 | scales: [1, 2, 1], 34 | texture: getSprite("star").data.tex, 35 | quads: [getSprite("star").data.frames[0]], 36 | }, { 37 | lifetime: 1.5, 38 | rate: 0, 39 | direction: -90, 40 | spread: 40, 41 | }), 42 | ]); 43 | 44 | parts.emit(20); 45 | } 46 | 47 | scene("game", () => { 48 | onKeyPress("space", () => { 49 | woah(); 50 | }); 51 | 52 | onMousePress(() => { 53 | woah(); 54 | }); 55 | 56 | add([ 57 | text("press space for particles"), 58 | ]); 59 | }); 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve. 3 | labels: ["bug"] 4 | title: "bug: " 5 | type: Bug 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Bug Description 10 | description: A clear and concise description of what the bug is. 11 | placeholder: The objects are not objecting 😔... 12 | validations: 13 | required: true 14 | 15 | - type: input 16 | attributes: 17 | label: Version 18 | description: Running Version 19 | placeholder: What version are you running? ex. 3001.0.10/master 20 | validations: 21 | required: false 22 | 23 | - type: input 24 | attributes: 25 | label: Playground Link 26 | description: Link to the playground where the bug can be reproduced. 27 | placeholder: https://play.kaplayjs.com/... 28 | validations: 29 | required: false 30 | 31 | - type: textarea 32 | attributes: 33 | label: Extra information 34 | description: Is there a method to reliably reproduce it? Did you expect something else to happen? Please add screenshots/evidence if possible. 35 | placeholder: So, i was kaplaying normally, then all of sudden... 36 | validations: 37 | required: false 38 | 39 | - type: checkboxes 40 | attributes: 41 | label: Summary 42 | options: 43 | - label: Fixed in v4000 44 | - label: Fixed in v3001 45 | -------------------------------------------------------------------------------- /src/gfx/draw/drawLoadingScreen.ts: -------------------------------------------------------------------------------- 1 | import { loadProgress } from "../../assets/asset"; 2 | import { rgb } from "../../math/color"; 3 | import { vec2 } from "../../math/math"; 4 | import { _k } from "../../shared"; 5 | import { height, width } from "../stack"; 6 | import { drawRect } from "./drawRect"; 7 | import { drawUnscaled } from "./drawUnscaled"; 8 | 9 | export function drawLoadScreen() { 10 | const progress = loadProgress(); 11 | 12 | if (_k.game.events.numListeners("loading") > 0) { 13 | _k.game.events.trigger("loading", progress); 14 | } 15 | else { 16 | drawUnscaled(() => { 17 | const w = width() / 2; 18 | const h = 24; 19 | const pos = vec2(width() / 2, height() / 2).sub( 20 | vec2(w / 2, h / 2), 21 | ); 22 | drawRect({ 23 | pos: vec2(0), 24 | width: width(), 25 | height: height(), 26 | color: rgb(0, 0, 0), 27 | }); 28 | drawRect({ 29 | pos: pos, 30 | width: w, 31 | height: h, 32 | fill: false, 33 | outline: { 34 | width: 4, 35 | }, 36 | }); 37 | drawRect({ 38 | pos: pos, 39 | width: w * progress, 40 | height: h, 41 | }); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/gamepadMulti.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Multi-Gamepad 3 | * @description How to manage multiple gamepads at the same. 4 | * @difficulty 1 5 | * @tags input 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay(); 11 | setGravity(2400); 12 | setBackground(0, 0, 0); 13 | loadSprite("bean", "/sprites/bean.png"); 14 | 15 | const playerColors = [ 16 | rgb(252, 53, 43), 17 | rgb(0, 255, 0), 18 | rgb(43, 71, 252), 19 | rgb(255, 255, 0), 20 | rgb(255, 0, 255), 21 | ]; 22 | 23 | let playerCount = 0; 24 | 25 | function addPlayer(gamepad) { 26 | const player = add([ 27 | pos(center()), 28 | anchor("center"), 29 | sprite("bean"), 30 | color(playerColors[playerCount]), 31 | area(), 32 | body(), 33 | doubleJump(), 34 | ]); 35 | 36 | playerCount++; 37 | 38 | onUpdate(() => { 39 | const leftStick = gamepad.getStick("left"); 40 | 41 | if (gamepad.isPressed("south")) { 42 | player.doubleJump(); 43 | } 44 | 45 | if (leftStick.x !== 0) { 46 | player.move(leftStick.x * 400, 0); 47 | } 48 | }); 49 | } 50 | 51 | // platform 52 | add([ 53 | pos(0, height()), 54 | anchor("botleft"), 55 | rect(width(), 140), 56 | area(), 57 | body({ isStatic: true }), 58 | ]); 59 | 60 | // add players on every gamepad connect 61 | onGamepadConnect((gamepad) => { 62 | addPlayer(gamepad); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/collisionshapes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Collision Shapes 3 | * @description How to create different collision shapes. 4 | * @difficulty 1 5 | * @tags basics, comps, physics 6 | * @minver 4000.0 7 | * @category basics 8 | */ 9 | 10 | // How kaplay handles collisions with different shapes 11 | kaplay(); 12 | 13 | // Set the gravity acceleration (pixels per second) 14 | setGravity(300); 15 | 16 | // Adds a ground 17 | add([ 18 | pos(0, 400), 19 | rect(width(), 40), 20 | area(), 21 | body({ isStatic: true }), 22 | ]); 23 | 24 | // Continuous shapes 25 | loop(1, () => { 26 | // Adds an object with a random shape 27 | add([ 28 | pos(width() / 2 + rand(-50, 50), 100), 29 | choose([ 30 | rect(20, 20), 31 | circle(10), 32 | ellipse(20, 10), 33 | polygon([vec2(-15, 10), vec2(0, -10), vec2(15, 10)]), 34 | ]), 35 | color(RED), 36 | area(), 37 | body(), 38 | offscreen({ destroy: true, distance: 10 }), 39 | ]); 40 | 41 | // getTreeRoot() gets the root of the game, the object that holds every other object 42 | // This line basically means that if there are more than 20 objects, we destroy the last one 43 | if (getTreeRoot().children.length > 20) { 44 | destroy(getTreeRoot().children[1]); 45 | } 46 | 47 | /* The previous code can also be written as 48 | if (get("*").length > 20) { 49 | destroy(get("*")[1]); 50 | } 51 | */ 52 | }); 53 | -------------------------------------------------------------------------------- /examples/movement.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Movement 3 | * @description How to make basic movement. 4 | * @difficulty 0 5 | * @tags basics, input 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | // Input handling and basic player movement 10 | 11 | // Start kaplay 12 | kaplay(); 13 | 14 | // Load assets 15 | loadSprite("bean", "/sprites/bean.png"); 16 | 17 | // Define player movement speed (pixels per second) 18 | const SPEED = 320; 19 | 20 | // Add player game object 21 | const player = add([ 22 | sprite("bean"), 23 | // center() returns the center point vec2(width() / 2, height() / 2) 24 | pos(center()), 25 | ]); 26 | 27 | // onKeyDown() registers an event that runs every frame as long as user is holding a certain key 28 | onKeyDown("left", () => { 29 | // .move() is provided by pos() component, move by pixels per second 30 | player.move(-SPEED, 0); 31 | }); 32 | 33 | onKeyDown("right", () => { 34 | player.move(SPEED, 0); 35 | }); 36 | 37 | onKeyDown("up", () => { 38 | player.move(0, -SPEED); 39 | }); 40 | 41 | onKeyDown("down", () => { 42 | player.move(0, SPEED); 43 | }); 44 | 45 | // onClick() registers an event that runs once when left mouse is clicked 46 | onClick(() => { 47 | // .moveTo() is provided by pos() component, changes the position 48 | player.moveTo(mousePos()); 49 | }); 50 | 51 | add([ 52 | // text() component is similar to sprite() but renders text 53 | text("Press arrow keys", { width: width() / 2 }), 54 | pos(12, 12), 55 | ]); 56 | -------------------------------------------------------------------------------- /examples/picture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Picture 3 | * @description How to store static drawing data 4 | * @difficulty 0 5 | * @tags effects, optimization, draw 6 | * @minver 4000.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // Optimized drawing using Picture API [💡] 12 | 13 | /* 💡 Picture API 💡 14 | The Picture API is a way to store static drawing data. It allows you to 15 | draw a lot of sprites in a single draw call. This is useful for 16 | optimizing performance and reducing draw calls. 17 | */ 18 | 19 | kaplay(); 20 | 21 | loadSprite("bean", "sprites/bean.png"); 22 | 23 | onLoad(() => { 24 | // We create the picture 25 | beginPicture(new Picture()); 26 | 27 | for (let i = 0; i < 16; i++) { 28 | for (let j = 0; j < 16; j++) { 29 | // We draw a sprite at the given position, so we 30 | // "store" the drawing data in the picture 31 | drawSprite({ 32 | pos: vec2(64 + i * 32, 64 + j * 32), 33 | sprite: "bean", 34 | }); 35 | } 36 | } 37 | 38 | // We end the picture 39 | const picture = endPicture(); 40 | 41 | // Now all we have to do is draw the picture, this picture is cached 42 | // by default, so it's the most optimized way to draw a lot of sprites, 43 | // maps, etc. 44 | onDraw(() => { 45 | drawPicture(picture, { 46 | pos: vec2(400, 0), 47 | angle: 45, 48 | scale: vec2(0.5), 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/out.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Out of Screen 3 | * @description How to handle objects that are out of screen. 4 | * @difficulty 1 5 | * @tags comps 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // detect if obj is out of screen 12 | 13 | kaplay(); 14 | 15 | loadSprite("bean", "/sprites/bean.png"); 16 | 17 | // custom comp 18 | function handleout() { 19 | return { 20 | id: "handleout", 21 | require: ["pos"], 22 | update() { 23 | const spos = this.screenPos(); 24 | if ( 25 | spos.x < 0 26 | || spos.x > width() 27 | || spos.y < 0 28 | || spos.y > height() 29 | ) { 30 | // triggers a custom event when out 31 | this.trigger("out"); 32 | } 33 | }, 34 | }; 35 | } 36 | 37 | const SPEED = 640; 38 | 39 | function shoot() { 40 | const center = vec2(width() / 2, height() / 2); 41 | const mpos = mousePos(); 42 | add([ 43 | pos(center), 44 | sprite("bean"), 45 | anchor("center"), 46 | handleout(), 47 | "bean", 48 | { dir: mpos.sub(center).unit() }, 49 | ]); 50 | } 51 | 52 | onKeyPress("space", shoot); 53 | onClick(shoot); 54 | 55 | onUpdate("bean", (m) => { 56 | m.move(m.dir.scale(SPEED)); 57 | }); 58 | 59 | // binds a custom event "out" to tag group "bean" 60 | on("out", "bean", (m) => { 61 | addKaboom(m.pos); 62 | destroy(m); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/auto/color.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { Color, rgb } from "../../src/math/color"; 3 | 4 | describe("Color creation using class", () => { 5 | test("new Color(44, 44, 44) should return Color(44, 44, 44)", () => { 6 | const color = new Color(44, 44, 44); 7 | expect(color).toEqual({ r: 44, g: 44, b: 44 }); 8 | }); 9 | 10 | test("Color().fromArray([22, 43, 79]) should return Color(22, 43, 79)", () => { 11 | const color = Color.fromArray([22, 43, 79]); 12 | expect(color).toEqual({ r: 22, g: 43, b: 79 }); 13 | }); 14 | 15 | test("Color().fromHex(0x123456) should return Color(0x12, 0x34, 0x56)", () => { 16 | const color = Color.fromHex(0x123456); 17 | expect(color).toEqual({ r: 0x12, g: 0x34, b: 0x56 }); 18 | }); 19 | 20 | test("Color().fromHex('#234567') should return Color(0x23, 0x45, 0x67)", () => { 21 | const color = Color.fromHex("#234567"); 22 | expect(color).toEqual({ r: 0x23, g: 0x45, b: 0x67 }); 23 | }); 24 | }); 25 | 26 | describe("Color creation using rbg() utility", () => { 27 | test("rgb(255, 255, 255) should return Color(255, 255, 255)", () => { 28 | const color = rgb(255, 255, 255); 29 | expect(color).toEqual({ r: 255, g: 255, b: 255 }); 30 | }); 31 | 32 | test("rgb(11, 25, 6) should return Color(11, 25, 6)", () => { 33 | const color = rgb(11, 25, 6); 34 | expect(color).toEqual({ r: 11, g: 25, b: 6 }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/ecs/components/transform/move.ts: -------------------------------------------------------------------------------- 1 | import { type SerializedVec2, Vec2 } from "../../../math/Vec2"; 2 | import type { Comp, EmptyComp, GameObj } from "../../../types"; 3 | import type { PosComp } from "./pos"; 4 | 5 | /** 6 | * The serialized {@link move `move()`} component. 7 | * 8 | * @group Components 9 | * @subgroup Component Serialization 10 | */ 11 | interface SerializedMoveComp { 12 | dir: SerializedVec2 | number; 13 | speed: number; 14 | } 15 | 16 | /** 17 | * The {@link move `move()`} component. 18 | * 19 | * @group Components 20 | * @subgroup Component Types 21 | */ 22 | export interface MoveComp extends Comp { 23 | serialize: () => SerializedMoveComp; 24 | } 25 | 26 | export function move( 27 | dir: number | Vec2, 28 | speed: number, 29 | ): MoveComp { 30 | const d = typeof dir === "number" ? Vec2.fromAngle(dir) : dir.unit(); 31 | return { 32 | id: "move", 33 | require: ["pos"], 34 | update(this: GameObj) { 35 | this.move(d.scale(speed)); 36 | }, 37 | serialize() { 38 | return { 39 | dir: dir instanceof Vec2 ? dir.serialize() : dir, 40 | speed: speed, 41 | }; 42 | }, 43 | }; 44 | } 45 | 46 | export function moveFactory(data: SerializedMoveComp) { 47 | if (typeof data.dir == "object") { 48 | return move(new Vec2(data.dir.x, data.dir.y), data.speed); 49 | } 50 | else { 51 | return move(data.dir, data.speed); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/math/vec3.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A 3D vector. 3 | * 4 | * @group Math 5 | * @subgroup Vectors 6 | */ 7 | export class Vec3 { 8 | x: number; 9 | y: number; 10 | z: number; 11 | 12 | static LEFT = new Vec3(-1, 0, 0); 13 | static RIGHT = new Vec3(1, 0, 0); 14 | static UP = new Vec3(0, -1, 0); 15 | static DOWN = new Vec3(0, 1, 0); 16 | static FORWARD = new Vec3(0, 0, 1); 17 | static BACK = new Vec3(0, 0, -1); 18 | static ZERO = new Vec3(0, 0, 0); 19 | static ONE = new Vec3(1, 1, 1); 20 | 21 | constructor(x: number, y: number, z: number) { 22 | this.x = x; 23 | this.y = y; 24 | this.z = z; 25 | } 26 | 27 | dot(other: Vec3) { 28 | return this.x * other.x + this.y * other.y + this.z * other.z; 29 | } 30 | 31 | cross(other: Vec3) { 32 | return new Vec3( 33 | this.y * other.z - this.z * other.y, 34 | this.z * other.x - this.x * other.z, 35 | this.x * other.y - this.y * other.x, 36 | ); 37 | } 38 | 39 | toAxis(): Vec3 { 40 | const ax = Math.abs(this.x); 41 | const ay = Math.abs(this.y); 42 | const az = Math.abs(this.z); 43 | 44 | if (ax >= ay && ax >= az) { 45 | return this.x < 0 ? Vec3.LEFT : Vec3.RIGHT; 46 | } 47 | else if (ay >= az) { 48 | return this.y < 0 ? Vec3.UP : Vec3.DOWN; 49 | } 50 | else { 51 | return this.z < 0 ? Vec3.BACK : Vec3.FORWARD; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/basicsObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Add game objects 3 | * @description How to create game objects 4 | * @difficulty 0 5 | * @tags basics, gobj 6 | * @minver 3001.0 7 | * @category basics 8 | * @group basics 9 | * @groupOrder 20 10 | * 11 | * RIP: add.js, 5year being the first example. 12 | */ 13 | 14 | // Adding game objects to screen [💡, 🥊] 15 | 16 | /* 💡 Game Objects 💡 17 | A Game Object is the base of all entities in KAPLAY. It's composed by components. 18 | */ 19 | 20 | /* 💡 Components 💡 21 | A component is a function that adds behaviour, methods and properties to a 22 | Game Object. 23 | */ 24 | 25 | kaplay(); 26 | 27 | // We load sprites with loadSprite(), it receives the asset name and the path to 28 | // the asset. 29 | loadSprite("bean", "/sprites/bean.png"); // Bean, the frog! 30 | 31 | // You can add a game object to the screen using the add() function, which takes 32 | // an array of components. We can store the result object in a variable. 33 | const bean = add([ 34 | sprite("bean"), // sprite() is a rendering component that draws a sprite on the screen. 35 | // pos(120, 80), // pos() is a component that sets the position of the object on the screen. 36 | ]); 37 | 38 | debug.log(bean.sprite); // Prints the sprite name 39 | 40 | /* 🥊 Challenge 🥊 41 | Try uncommenting the pos() component, it receives and x and y coordinates 42 | and sets the position of the object on the screen. 43 | 44 | Then try logging the position coordinates of the object using 45 | 46 | debug.log(bean.pos); 47 | */ 48 | -------------------------------------------------------------------------------- /tests/playtests/parenttest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Parent Test 3 | * @description TBD. 4 | * @difficulty 1 5 | * @tags basics 6 | * @minver 4000.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | loadBean(); 12 | 13 | const centerBean = add([ 14 | pos(center()), 15 | anchor("center"), 16 | sprite("bean"), 17 | area(), 18 | rotate(0), 19 | scale(2), 20 | { 21 | update() { 22 | this.angle += 20 * dt(); 23 | }, 24 | }, 25 | ]); 26 | 27 | const orbitingBean = centerBean.add([ 28 | pos(vec2(100, 0)), 29 | anchor("center"), 30 | sprite("bean"), 31 | area(), 32 | rotate(0), 33 | scale(1), 34 | color(), 35 | { 36 | update() { 37 | this.angle = -this.parent.transform.getRotation(); 38 | if (this.isHovering()) { 39 | this.color = RED; 40 | } 41 | else { 42 | this.color = WHITE; 43 | } 44 | }, 45 | }, 46 | ]); 47 | 48 | onMousePress(() => { 49 | if (orbitingBean.parent === centerBean /* && orbitingBean.isHovering()*/) { 50 | orbitingBean.setParent(getTreeRoot(), { keep: KeepFlags.All }); 51 | } 52 | }); 53 | 54 | onMouseMove((pos, delta) => { 55 | if (orbitingBean.parent !== centerBean) { 56 | orbitingBean.pos = orbitingBean.pos.add(delta); 57 | } 58 | }); 59 | 60 | onMouseRelease(() => { 61 | if (orbitingBean.parent !== centerBean) { 62 | orbitingBean.setParent(centerBean, { keep: KeepFlags.All }); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /examples/textInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Text Input 3 | * @description How to take input and display it on text 4 | * @difficulty 0 5 | * @tags basics, ui 6 | * @minver 3001.0 7 | * @category basics 8 | * @test 9 | */ 10 | 11 | // Using textInput() component to catch user text input easily 12 | 13 | kaplay({ font: "happy", background: "#a6555f" }); 14 | 15 | loadHappy(); 16 | 17 | // We will ask something! 18 | add([ 19 | pos(width() / 2, 50), 20 | text("What's your favorite KAPLAY Crew member", { 21 | // Responsive friendly 22 | align: "center", 23 | width: width(), 24 | }), 25 | anchor("top"), 26 | ]); 27 | 28 | // This object will catch user input 29 | const crew = add([ 30 | text(""), 31 | // We pass true so it focus by default. You can also do crew.hasFocus = true; 32 | textInput(true, 20), // <- 20 chars at max 33 | pos(width() / 2, height() / 2), 34 | anchor("center"), 35 | ]); 36 | 37 | // Our response 38 | const response = add([ 39 | text("", { 40 | align: "center", 41 | width: width(), 42 | }), 43 | anchor("bot"), 44 | pos(width() / 2, height() - 50), 45 | ]); 46 | 47 | // Updating the response, depending on input 48 | response.onUpdate(() => { 49 | if (crew.text == "") { 50 | response.text = "..."; 51 | } 52 | else if (crew.text.toLowerCase() === "mark") { 53 | response.text = `Yep. Mark the best`; 54 | } 55 | else { 56 | response.text = `I like ${crew.text}, but Mark is better`; 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /src/ecs/components/draw/shader.ts: -------------------------------------------------------------------------------- 1 | import type { Uniform } from "../../../assets/shader"; 2 | import type { Comp } from "../../../types"; 3 | 4 | /** 5 | * The serialized {@link shader `shader()`} component. 6 | * 7 | * @group Components 8 | * @subgroup Component Serialization 9 | */ 10 | export interface SerializeShaderComp { 11 | shader: string; 12 | } 13 | 14 | /** 15 | * The {@link shader `shader()`} component. 16 | * 17 | * @group Components 18 | * @subgroup Component Types 19 | */ 20 | export interface ShaderComp extends Comp { 21 | /** 22 | * Uniform values to pass to the shader. 23 | */ 24 | uniform?: Uniform; 25 | /** 26 | * The shader ID. 27 | */ 28 | shader: string; 29 | serialize(): SerializeShaderComp; 30 | } 31 | 32 | export function shader( 33 | id: string, 34 | uniform?: Uniform | (() => Uniform), 35 | ): ShaderComp { 36 | return { 37 | id: "shader", 38 | shader: id, 39 | ...(typeof uniform === "function" 40 | ? { 41 | uniform: uniform(), 42 | update() { 43 | this.uniform = uniform(); 44 | }, 45 | } 46 | : { 47 | uniform: uniform, 48 | }), 49 | inspect() { 50 | return `shader: ${id}`; 51 | }, 52 | serialize() { 53 | return { shader: this.shader }; 54 | }, 55 | }; 56 | } 57 | 58 | export function shaderFactory(data: any) { 59 | return shader(data.shader); 60 | } 61 | -------------------------------------------------------------------------------- /src/ecs/components/misc/lifespan.ts: -------------------------------------------------------------------------------- 1 | import { easings } from "../../../math/easings"; 2 | import { _k } from "../../../shared"; 3 | import type { EmptyComp, GameObj } from "../../../types"; 4 | import type { OpacityComp } from "../draw/opacity"; 5 | 6 | /** 7 | * The {@link lifespan `lifespan()`} component. 8 | * 9 | * @group Components 10 | * @subgroup Component Types 11 | */ 12 | export interface LifespanCompOpt { 13 | /** 14 | * Fade out duration (default 0 which is no fade out). 15 | */ 16 | fade?: number; 17 | } 18 | 19 | export function lifespan(time: number, opt: LifespanCompOpt = {}): EmptyComp { 20 | if (time == null) { 21 | throw new Error("lifespan() requires time"); 22 | } 23 | const fade = opt.fade ?? 0; 24 | return { 25 | id: "lifespan", 26 | require: ["opacity"], 27 | add(this: GameObj) { 28 | _k.game.root.wait(time, () => { 29 | this.opacity = this.opacity ?? 1; 30 | 31 | if (fade > 0) { 32 | _k.game.root.tween( 33 | this.opacity, 34 | 0, 35 | fade, 36 | (a) => this.opacity = a, 37 | easings.linear, 38 | ).onEnd(() => { 39 | this.destroy(); 40 | }); 41 | } 42 | else { 43 | this.destroy(); 44 | } 45 | }); 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /tests/playtests/fixedUpdate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Fixed Update 3 | * @description TBD 4 | * @difficulty 1 5 | * @tags ui, input 6 | * @minver 4000.0 7 | */ 8 | // @ts-check 9 | 10 | kaplay({ 11 | fixedUpdateMode: "ludicrous", 12 | }); 13 | debug.inspect = true; 14 | 15 | const lag = false; 16 | if (lag) { 17 | loadBean(); 18 | for (let i = 0; i < 150; i++) { 19 | add([ 20 | sprite("bean"), 21 | area(), 22 | pos(rand(vec2(100))), 23 | ]); 24 | } 25 | } 26 | 27 | class FPSCounter { 28 | win = 10; 29 | history = new Array(this.win).fill(0); 30 | accumulator = 0; 31 | i = 0; 32 | fps = 0; 33 | count = 0; 34 | tick(dt) { 35 | this.accumulator += dt - this.history[this.i]; 36 | this.history[this.i] = dt; 37 | this.i = (this.i + 1) % this.win; 38 | this.count = Math.min(this.count + 1, this.win); 39 | this.fps = this.count / this.accumulator; 40 | } 41 | } 42 | 43 | const fixCounter = new FPSCounter(), normalCounter = new FPSCounter(); 44 | 45 | onFixedUpdate(() => { 46 | fixCounter.tick(dt()); 47 | }); 48 | 49 | onUpdate(() => { 50 | normalCounter.tick(dt()); 51 | debug.log( 52 | [ 53 | fixedDt(), 54 | fixCounter.fps, 55 | dt(), 56 | normalCounter.fps, 57 | ].map(x => x.toFixed(5)).join(" "), 58 | ); 59 | }); 60 | 61 | if (!lag) { 62 | loop(2, () => { 63 | setFixedSpeed(fixedDt() > 0.01 ? "ludicrous" : "friedPotato"); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /src/gfx/draw/drawCircle.ts: -------------------------------------------------------------------------------- 1 | import type { Color } from "../../math/color"; 2 | import type { Vec2 } from "../../math/Vec2"; 3 | import type { Anchor, RenderProps } from "../../types"; 4 | import { drawEllipse } from "./drawEllipse"; 5 | 6 | /** 7 | * How the circle should look like. 8 | * @group Draw 9 | * @subgroup Types 10 | */ 11 | export type DrawCircleOpt = Omit & { 12 | /** 13 | * Radius of the circle. 14 | */ 15 | radius: number; 16 | /** 17 | * Starting angle. 18 | */ 19 | start?: number; 20 | /** 21 | * Ending angle. 22 | */ 23 | end?: number; 24 | /** 25 | * If fill the shape with color (set this to false if you only want an outline). 26 | */ 27 | fill?: boolean; 28 | /** 29 | * Use gradient instead of solid color. 30 | * 31 | * @since v3000.0 32 | */ 33 | gradient?: [Color, Color]; 34 | /** 35 | * Multiplier for circle vertices resolution (default 1) 36 | */ 37 | resolution?: number; 38 | /** 39 | * The anchor point, or the pivot point. Default to "topleft". 40 | */ 41 | anchor?: Anchor | Vec2; 42 | }; 43 | 44 | export function drawCircle(opt: DrawCircleOpt) { 45 | if (typeof opt.radius !== "number") { 46 | throw new Error("drawCircle() requires property \"radius\"."); 47 | } 48 | 49 | if (opt.radius === 0) { 50 | return; 51 | } 52 | 53 | drawEllipse(Object.assign({}, opt, { 54 | radiusX: opt.radius, 55 | radiusY: opt.radius, 56 | angle: 0, 57 | })); 58 | } 59 | -------------------------------------------------------------------------------- /examples/gamepad.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Gamepad 3 | * @description How to manage gamepad input. 4 | * @difficulty 0 5 | * @tags basics, input 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay({ 11 | background: [0, 0, 0], 12 | }); 13 | loadSprite("bean", "/sprites/bean.png"); 14 | 15 | setGravity(2400); 16 | 17 | scene("nogamepad", () => { 18 | add([ 19 | text("Gamepad not found.\nConnect a gamepad and press a button!", { 20 | width: width() - 80, 21 | align: "center", 22 | }), 23 | pos(center()), 24 | anchor("center"), 25 | ]); 26 | onGamepadConnect(() => { 27 | go("game"); 28 | }); 29 | }); 30 | 31 | scene("game", () => { 32 | const player = add([ 33 | pos(center()), 34 | anchor("center"), 35 | sprite("bean"), 36 | area(), 37 | body(), 38 | ]); 39 | 40 | // platform 41 | add([ 42 | pos(0, height()), 43 | anchor("botleft"), 44 | rect(width(), 140), 45 | area(), 46 | body({ isStatic: true }), 47 | ]); 48 | 49 | onGamepadButtonPress((b) => { 50 | debug.log(b); 51 | }); 52 | 53 | onGamepadButtonPress(["south", "west"], () => { 54 | player.jump(); 55 | }); 56 | 57 | onGamepadStick("left", (v) => { 58 | player.move(v.x * 400, 0); 59 | }); 60 | 61 | onGamepadDisconnect(() => { 62 | go("nogamepad"); 63 | }); 64 | }); 65 | 66 | if (getGamepads().length > 0) { 67 | go("game"); 68 | } 69 | else { 70 | go("nogamepad"); 71 | } 72 | -------------------------------------------------------------------------------- /tests/types/tafScenes.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, expectTypeOf, test } from "vitest"; 2 | import type { Opt } from "../../src/core/taf"; 3 | import { kaplay, kaplayTypes } from "../../src/kaplay"; 4 | 5 | describe("Typed Scenes", () => { 6 | const k = kaplay({ 7 | background: "fff", 8 | buttons: { 9 | "jump": {}, 10 | }, 11 | types: kaplayTypes< 12 | Opt< 13 | { 14 | scenes: { 15 | "game": [score: number]; 16 | }; 17 | } 18 | > 19 | >(), 20 | }); 21 | 22 | const ks = kaplay({ 23 | background: "fff", 24 | buttons: { 25 | "jump": {}, 26 | }, 27 | types: kaplayTypes< 28 | Opt< 29 | { 30 | scenes: { 31 | "game": [score: number]; 32 | }; 33 | strictScenes: true; 34 | } 35 | > 36 | >(), 37 | }); 38 | 39 | test("scene(name) name is typed", () => { 40 | k.scene("game", (score) => { 41 | expectTypeOf(score).toEqualTypeOf; 42 | }); 43 | }); 44 | 45 | test("[Strict] in scene(name), name is typed and strict", () => { 46 | ks.scene("game", (score) => { 47 | expectTypeOf(score).toEqualTypeOf; 48 | }); 49 | 50 | // @ts-expect-error 51 | ks.scene("f", () => {}); 52 | // @ts-expect-error 53 | ks.go("game", 1, 2); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /examples/hover.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Hover 3 | * @description Understand the different hover methods 4 | * @difficulty 0 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | // Differeces between onHover and onHoverUpdate 11 | 12 | kaplay({ 13 | scale: 2, 14 | }); 15 | 16 | loadSprite("bean", "/sprites/bean.png"); 17 | 18 | add([ 19 | text("onHover()\nonHoverEnd()"), 20 | pos(80, 80), 21 | ]); 22 | 23 | add([ 24 | text("onHoverUpdate()"), 25 | pos(340, 80), 26 | ]); 27 | 28 | const redBean = add([ 29 | sprite("bean"), 30 | color(RED), 31 | pos(130, 180), 32 | anchor("center"), 33 | area(), 34 | ]); 35 | 36 | const blueBean = add([ 37 | sprite("bean"), 38 | color(BLUE), 39 | pos(380, 180), 40 | anchor("center"), 41 | area(), 42 | ]); 43 | 44 | // Only runs once when bean is hovered, and when bean is unhovered 45 | redBean.onHover(() => { 46 | debug.log("red bean hovered"); 47 | 48 | redBean.color = GREEN; 49 | }); 50 | redBean.onHoverEnd(() => { 51 | debug.log("red bean unhovered"); 52 | 53 | redBean.color = RED; 54 | }); 55 | 56 | // Runs every frame when blue bean is hovered 57 | blueBean.onHoverUpdate(() => { 58 | const t = time() * 10; 59 | blueBean.color = rgb( 60 | wave(0, 255, t), 61 | wave(0, 255, t + 2), 62 | wave(0, 255, t + 4), 63 | ); 64 | 65 | debug.log("blue bean on hover"); 66 | }); 67 | 68 | let cameraScale = 1; 69 | 70 | onScroll((delta) => { 71 | cameraScale = cameraScale * (1 - 0.1 * Math.sign(delta.y)); 72 | setCamScale(cameraScale); 73 | }); 74 | -------------------------------------------------------------------------------- /src/gfx/draw/drawInspectText.ts: -------------------------------------------------------------------------------- 1 | import { DBG_FONT } from "../../constants/general"; 2 | import { rgb } from "../../math/color"; 3 | import { vec2 } from "../../math/math"; 4 | import { type Vec2 } from "../../math/Vec2"; 5 | import { formatText } from "../formatText"; 6 | import { 7 | height, 8 | multTranslateV, 9 | popTransform, 10 | pushTransform, 11 | width, 12 | } from "../stack"; 13 | import { drawFormattedText } from "./drawFormattedText"; 14 | import { drawRect } from "./drawRect"; 15 | import { drawUnscaled } from "./drawUnscaled"; 16 | 17 | export function drawInspectText(pos: Vec2, txt: string) { 18 | drawUnscaled(() => { 19 | const pad = vec2(8); 20 | 21 | pushTransform(); 22 | multTranslateV(pos); 23 | 24 | const ftxt = formatText({ 25 | text: txt, 26 | font: DBG_FONT, 27 | size: 16, 28 | pos: pad, 29 | color: rgb(255, 255, 255), 30 | fixed: true, 31 | }); 32 | 33 | const bw = ftxt.width + pad.x * 2; 34 | const bh = ftxt.height + pad.x * 2; 35 | 36 | if (pos.x + bw >= width()) { 37 | multTranslateV(vec2(-bw, 0)); 38 | } 39 | 40 | if (pos.y + bh >= height()) { 41 | multTranslateV(vec2(0, -bh)); 42 | } 43 | 44 | drawRect({ 45 | width: bw, 46 | height: bh, 47 | color: rgb(0, 0, 0), 48 | radius: 4, 49 | opacity: 0.8, 50 | fixed: true, 51 | }); 52 | 53 | drawFormattedText(ftxt); 54 | popTransform(); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/math/minkowski.ts: -------------------------------------------------------------------------------- 1 | import type { Shape } from "../types"; 2 | import { Rect, vec2 } from "./math"; 3 | import { Vec2 } from "./Vec2"; 4 | 5 | function minkowskiRectDifference(r1: Rect, r2: Rect): Rect { 6 | return new Rect( 7 | vec2( 8 | r1.pos.x - (r2.pos.x + r2.width), 9 | r1.pos.y - (r2.pos.y + r2.height), 10 | ), 11 | r1.width + r2.width, 12 | r1.height + r2.height, 13 | ); 14 | } 15 | 16 | export function minkowskiRectShapeIntersection(shape1: Shape, shape2: Shape) { 17 | const s1 = shape1 instanceof Rect 18 | ? shape1 19 | : shape1.bbox(); 20 | const s2 = shape2 instanceof Rect 21 | ? shape2 22 | : shape2.bbox(); 23 | const res = minkowskiRectDifference(s1, s2); 24 | 25 | if (!res.contains(new Vec2())) { 26 | return null; 27 | } 28 | 29 | const distance = Math.min( 30 | Math.abs(res.pos.x), 31 | Math.abs(res.pos.x + res.width), 32 | Math.abs(res.pos.y), 33 | Math.abs(res.pos.y + res.height), 34 | ); 35 | 36 | let normal = vec2(); 37 | 38 | switch (distance) { 39 | case Math.abs(res.pos.x): 40 | normal = vec2(1, 0); 41 | break; 42 | case Math.abs(res.pos.x + res.width): 43 | normal = vec2(-1, 0); 44 | break; 45 | case Math.abs(res.pos.y): 46 | normal = vec2(0, 1); 47 | break; 48 | case Math.abs(res.pos.y + res.height): 49 | normal = vec2(0, -1); 50 | break; 51 | } 52 | 53 | return { 54 | normal, 55 | distance, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import jsdoc from "eslint-plugin-jsdoc"; 4 | import tseslint from "typescript-eslint"; 5 | 6 | export default tseslint.config( 7 | tseslint.configs.base, 8 | { 9 | plugins: { 10 | jsdoc, 11 | }, 12 | rules: { 13 | "jsdoc/require-hyphen-before-param-description": "error", 14 | "jsdoc/check-alignment": "warn", 15 | "jsdoc/check-tag-names": ["error", { 16 | "definedTags": ["group", "subgroup", "experimental"], 17 | }], 18 | "jsdoc/sort-tags": ["error", { 19 | tagSequence: [{ 20 | tags: [ 21 | "deprecated", 22 | "ignore", 23 | ], 24 | }, { 25 | tags: [ 26 | "param", 27 | ], 28 | }, { 29 | tags: [ 30 | "template", 31 | ], 32 | }, { 33 | tags: [ 34 | "example", 35 | ], 36 | }, { 37 | tags: [ 38 | "default", 39 | "readonly", 40 | "static", 41 | "returns", 42 | "since", 43 | "group", 44 | "subgroup", 45 | "experimental", 46 | ], 47 | }], 48 | }], 49 | }, 50 | files: ["./src/**/*.ts"], 51 | }, 52 | ); 53 | -------------------------------------------------------------------------------- /help.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/makeman@*/help.schema.json", 3 | "name": "KAPLAY", 4 | "description": "KAPLAY is a JavaScript & TypeScript game library that helps you make games fast and fun!", 5 | "styles": { 6 | "titleFont": "Standard", 7 | "titleColor": { 8 | "r": 171, 9 | "g": 221, 10 | "b": 100 11 | }, 12 | "titleBackground": { 13 | "r": 31, 14 | "g": 16, 15 | "b": 42 16 | } 17 | }, 18 | "targets": { 19 | "dev": { 20 | "description": "Run the development server" 21 | }, 22 | "win:dev": { 23 | "description": "Run the development server\n\tbut with a cmd capable environment variable" 24 | }, 25 | "build": { 26 | "description": "Build the project for production" 27 | }, 28 | "build:fast": { 29 | "description": "Build the project for development\n\tDoes not build .d.ts files" 30 | }, 31 | "check": { 32 | "description": "tsc type check" 33 | }, 34 | "fmt": { 35 | "description": "Format the code using dprint" 36 | }, 37 | "test": { 38 | "description": "Test using puppeteer" 39 | }, 40 | "test:vite": { 41 | "description": "Test using vitest" 42 | }, 43 | "doc-dts": { 44 | "description": "Generate .d.ts bundle" 45 | }, 46 | "desktop": { 47 | "description": "Run the desktop testing system with tauri" 48 | }, 49 | "prepare": { 50 | "description": "Alias for build" 51 | }, 52 | "publish:next": { 53 | "description": "Publish to npm with the next tag" 54 | }, 55 | "help": { 56 | "description": "Show this help message" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ecs/components/transform/anchor.ts: -------------------------------------------------------------------------------- 1 | import { anchorPt } from "../../../gfx/anchor"; 2 | import { vec2 } from "../../../math/math"; 3 | import { type SerializedVec2, Vec2 } from "../../../math/Vec2"; 4 | import type { Anchor, Comp } from "../../../types"; 5 | 6 | /** 7 | * The serialized {@link anchor `anchor()`} component. 8 | * 9 | * @group Components 10 | * @subgroup Component Serialization 11 | */ 12 | export interface SerializedAnchorComp { 13 | anchor: SerializedVec2; 14 | } 15 | 16 | /** 17 | * The {@link anchor `anchor()`} component. 18 | * 19 | * @group Components 20 | * @subgroup Component Types 21 | */ 22 | export interface AnchorComp extends Comp { 23 | /** 24 | * Anchor point for render. 25 | */ 26 | anchor: Anchor | Vec2; 27 | 28 | serialize(): SerializedAnchorComp; 29 | } 30 | 31 | export function anchor(o: Anchor | Vec2): AnchorComp { 32 | if (!o) { 33 | throw new Error("Please define an anchor"); 34 | } 35 | return { 36 | id: "anchor", 37 | anchor: o, 38 | inspect() { 39 | if (typeof this.anchor === "string") { 40 | return `anchor: ` + this.anchor; 41 | } 42 | else { 43 | return `anchor: ` + this.anchor.toString(); 44 | } 45 | }, 46 | serialize() { 47 | return { 48 | anchor: this.anchor instanceof Vec2 49 | ? this.anchor.serialize() 50 | : anchorPt(this.anchor).serialize(), 51 | }; 52 | }, 53 | }; 54 | } 55 | 56 | export function anchorFactory(data: SerializedAnchorComp) { 57 | return anchor(new Vec2(data.anchor.x, data.anchor.y)); 58 | } 59 | -------------------------------------------------------------------------------- /examples/basicEventsObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Object Events 3 | * @description Handling events on objects 4 | * @difficulty 0 5 | * @tags basics, comps 6 | * @minver 3001.0 7 | * @category basics 8 | * @group basics 9 | * @groupOrder 40 10 | */ 11 | 12 | // Before getting more into components, will see more about Game Objects [💡] 13 | 14 | /* 💡 Game Object Raw 💡 15 | It's the serie of methods and properties that are available on all game objects, 16 | like exists(), destroy(), 17 | */ 18 | 19 | /* 💡 Game Object Events 💡 20 | Game objects have their own events based on it's lifecycle and state. 21 | */ 22 | 23 | kaplay(); 24 | 25 | loadSprite("bean", "/sprites/bean.png"); 26 | 27 | const obj = add([ 28 | sprite("bean"), 29 | anchor("center"), 30 | pos(100, 100), 31 | ]); 32 | 33 | // obj.onUpdate() is called every frame while the object exists. 34 | obj.onUpdate(() => { 35 | debug.log("hi!"); 36 | }); 37 | 38 | // obj.onDestroy() is called when the object is destroyed. 39 | obj.onDestroy(() => { 40 | debug.log("bye cruel world!"); 41 | }); 42 | 43 | // We can destroy the object using obj.destroy() 44 | obj.onKeyPress("space", () => { 45 | addKaboom(obj.pos); // addKaboom() is a function that creates an explosion at the given position. 46 | obj.destroy(); 47 | }); 48 | 49 | // Notice we will use global onKeyPress() to handle the event. 50 | // This is because we want this running even if the object is destroyed. 51 | onKeyPress("enter", () => { 52 | // We can also if the object exists using obj.exists() 53 | if (obj.exists()) { 54 | console.log("The object exists!"); 55 | } 56 | else { 57 | console.log("The object doesn't exist!"); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /examples/basicsCompRender.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Render Components 3 | * @description Learn about components that render something 4 | * @difficulty 0 5 | * @tags basics, comps 6 | * @minver 3001.0 7 | * @category basics 8 | * @group basics 9 | * @groupOrder 60 10 | */ 11 | 12 | // All basic rendering related components. This include rendering shapes, 13 | // sprites and text and also modifying their properties, with color(), outline() 14 | // and opacity(). 15 | 16 | kaplay({ 17 | font: "happy", 18 | background: ["#873e84"], 19 | }); 20 | 21 | loadSprite("bean", "/sprites/bean.png"); 22 | // load a bitmap font 23 | loadBitmapFont("happy", "/fonts/happy_28x36.png", 28, 36); 24 | 25 | // Basic Rendering Components 26 | 27 | // sprite() is a component that renders a sprite. 28 | add([ 29 | sprite("bean"), 30 | pos(10, 10), 31 | ]); 32 | 33 | // rect() is a component that renders a rectangle. 34 | add([ 35 | rect(60, 60), 36 | pos(110, 10), 37 | // add an outline to the shape. 38 | outline(4), 39 | // set the color 40 | color(109, 128, 250), // r g b 41 | ]); 42 | 43 | // circle() is a component that renders a circle. 44 | add([ 45 | circle(30), 46 | pos(210, 10), 47 | outline(4), 48 | // set the pivot point, as default is center for circle() 49 | anchor("topleft"), 50 | // set the opacity 51 | opacity(0.5), 52 | ]); 53 | 54 | // text() is a component that renders a text. 55 | add([ 56 | text("hi!"), 57 | pos(310, 10), 58 | ]); 59 | 60 | // polygon() is a component that renders a polygon. 61 | add([ 62 | polygon([vec2(5, 0), vec2(50, 5), vec2(50, 50), vec2(0, 20)]), 63 | pos(410, 10), 64 | outline(4), 65 | color("#d46eb3"), // hex colors! 66 | ]); 67 | -------------------------------------------------------------------------------- /examples/rebinding.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Re-Bind Buttons 3 | * @description How to re-bind button mappings mid-game 4 | * @difficulty 3 5 | * @tags ui, input 6 | * @minver 4000 7 | * @test 8 | */ 9 | 10 | kaplay({ 11 | buttons: { 12 | hi: { 13 | keyboard: ["enter"], 14 | }, 15 | }, 16 | background: "black", 17 | }); 18 | 19 | onButtonDown("hi", () => { 20 | add([ 21 | text("hi"), 22 | pos(rand(vec2(200), vec2(500))), 23 | lifespan(1), 24 | opacity(), 25 | ]); 26 | }); 27 | 28 | const updateBtn = add([ 29 | rect(300, 50), 30 | pos(310, 100), 31 | anchor("center"), 32 | area(), 33 | ]); 34 | const label = updateBtn.add([ 35 | text("", { align: "center" }), 36 | color(BLACK), 37 | anchor("center"), 38 | ]); 39 | add([ 40 | text("say hi:"), 41 | pos(0, 100), 42 | anchor("left"), 43 | ]); 44 | 45 | // True is the user is currently inputting a button combo 46 | let isInputting = false; 47 | let combo = []; 48 | // record the combo 49 | onKeyPress(key => { 50 | if (isInputting) combo.push(key); 51 | }); 52 | // commit the combo if one was entered 53 | onKeyRelease(() => { 54 | isInputting = false; 55 | if (combo.length > 0) { 56 | setButton("hi", { keyboard: combo.join("+") }); 57 | } 58 | combo = []; 59 | }); 60 | // show the combo entering process on the button 61 | onUpdate(() => { 62 | if (isInputting) { 63 | label.color = BLUE; 64 | label.text = `> ${combo.join("+")} <`; 65 | } 66 | else { 67 | label.color = BLACK; 68 | label.text = "" + getButton("hi").keyboard; 69 | } 70 | }); 71 | // enter keybinding update mode when clicking the button 72 | updateBtn.onClick(() => { 73 | combo = []; 74 | isInputting = !isInputting; 75 | }); 76 | -------------------------------------------------------------------------------- /tests/playtests/scenepushandpop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file scenepushandpop 3 | * @description Testing for scene push and pop methods 4 | * @difficulty 1 5 | * @tags basics 6 | * @minver 3001.1 7 | */ 8 | 9 | kaplay(); 10 | 11 | loadBean(); 12 | 13 | add([ 14 | text( 15 | "(BLUE) Back from last scene\n(YELLOW) push to second scene\n(RED) Force error to a unexistent scene", 16 | { 17 | size: 18, 18 | }, 19 | ), 20 | pos(5, height() - 100), 21 | stay(), 22 | ]); 23 | 24 | // create two scenes as first test // 25 | 26 | scene("first", () => { 27 | add([ 28 | text("Oh Hi! from first scene, click on the bean", { 29 | size: 40, 30 | }), 31 | pos(center()), 32 | ]); 33 | 34 | const b = add([ 35 | rect(32, 32), 36 | pos(64, 64), 37 | color(BLUE), 38 | area(), 39 | stay(), 40 | ]); 41 | 42 | const bimpostor = add([ 43 | rect(32, 32), 44 | pos(100, 64), 45 | color(YELLOW), 46 | area(), 47 | stay(), 48 | ]); 49 | 50 | const berror = add([ 51 | rect(32, 32), 52 | pos(200, 64), 53 | color(RED), 54 | area(), 55 | stay(), 56 | ]); 57 | 58 | b.onClick(() => { 59 | debug.log("adding page"); 60 | pushScene("second"); 61 | }); 62 | 63 | bimpostor.onClick(() => { 64 | debug.log("back page"); 65 | popScene(); 66 | }); 67 | 68 | berror.onClick(() => { 69 | pushScene("unexistent"); 70 | }); 71 | 72 | // onKeyDown("1", () => debug.log("aaaa")) 73 | }); 74 | 75 | scene("second", () => { 76 | add([ 77 | pos(center()), 78 | text("Oh Hi! from second scene", { 79 | size: 40, 80 | }), 81 | ]); 82 | }); 83 | 84 | go("first"); 85 | 86 | // pushScene() 87 | -------------------------------------------------------------------------------- /examples/pong.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Pong 3 | * @description How to make pong in KAPLAY. 4 | * @difficulty 1 5 | * @tags basics, game 6 | * @minver 3001.0 7 | * @category games 8 | */ 9 | 10 | kaplay({ 11 | background: [255, 255, 128], 12 | }); 13 | 14 | // add paddles 15 | add([ 16 | pos(40, 0), 17 | rect(20, 80), 18 | outline(4), 19 | anchor("center"), 20 | area(), 21 | "paddle", 22 | ]); 23 | 24 | add([ 25 | pos(width() - 40, 0), 26 | rect(20, 80), 27 | outline(4), 28 | anchor("center"), 29 | area(), 30 | "paddle", 31 | ]); 32 | 33 | // move paddles with mouse 34 | onUpdate("paddle", (p) => { 35 | p.pos.y = mousePos().y; 36 | }); 37 | 38 | // score counter 39 | let score = 0; 40 | 41 | add([ 42 | text(score.toString()), 43 | pos(center()), 44 | anchor("center"), 45 | z(50), 46 | { 47 | update() { 48 | this.text = score.toString(); 49 | }, 50 | }, 51 | ]); 52 | 53 | // ball 54 | let speed = 480; 55 | 56 | const ball = add([ 57 | pos(center()), 58 | circle(16), 59 | outline(4), 60 | area({ shape: new Rect(vec2(-16), 32, 32) }), 61 | { vel: Vec2.fromAngle(rand(-20, 20)) }, 62 | ]); 63 | 64 | // move ball, bounce it when touche horizontal edges, respawn when touch vertical edges 65 | ball.onUpdate(() => { 66 | ball.move(ball.vel.scale(speed)); 67 | if (ball.pos.x < 0 || ball.pos.x > width()) { 68 | score = 0; 69 | ball.pos = center(); 70 | ball.vel = Vec2.fromAngle(rand(-20, 20)); 71 | speed = 320; 72 | } 73 | if (ball.pos.y < 0 || ball.pos.y > height()) { 74 | ball.vel.y = -ball.vel.y; 75 | } 76 | }); 77 | 78 | // bounce when touch paddle 79 | ball.onCollide("paddle", (p) => { 80 | speed += 60; 81 | ball.vel = Vec2.fromAngle(ball.pos.angle(p.pos)); 82 | score++; 83 | }); 84 | -------------------------------------------------------------------------------- /examples/tweenEasings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Tween Easings 3 | * @description See all different easings in tween() 4 | * @difficulty 0 5 | * @tags animation 6 | * @minver 3001.0 7 | * @category concepts 8 | * @group tween 9 | * @groupOrder 1 10 | */ 11 | 12 | // See all the tweeen easings 13 | 14 | kaplay({ 15 | background: "#a32858", 16 | font: "happy", 17 | }); 18 | 19 | loadHappy(); 20 | loadSprite("bean", "/sprites/bean.png"); 21 | 22 | const DURATION = 1; 23 | const EASINGS = Object.keys(easings); 24 | let curEasing = 0; 25 | 26 | const bean = add([ 27 | sprite("bean"), 28 | scale(2), 29 | pos(center()), 30 | rotate(0), 31 | anchor("center"), 32 | ]); 33 | 34 | const label = add([ 35 | text(EASINGS[curEasing], { size: 64 }), 36 | pos(24, 24), 37 | ]); 38 | 39 | add([ 40 | text("Click anywhere & use arrow keys", { width: width() }), 41 | anchor("botleft"), 42 | pos(24, height() - 24), 43 | ]); 44 | 45 | onKeyPress(["left", "a"], () => { 46 | curEasing = curEasing === 0 ? EASINGS.length - 1 : curEasing - 1; 47 | label.text = EASINGS[curEasing]; 48 | }); 49 | 50 | onKeyPress(["right", "d"], () => { 51 | curEasing = (curEasing + 1) % EASINGS.length; 52 | label.text = EASINGS[curEasing]; 53 | }); 54 | 55 | let curTween = null; 56 | 57 | onMousePress(() => { 58 | const easeType = EASINGS[curEasing]; 59 | 60 | // Stop the previos tween 61 | if (curTween) curTween.cancel(); 62 | 63 | // start the tween 64 | curTween = tween( 65 | // start value (accepts number, Vec2 and Color) 66 | bean.pos, 67 | // destination value 68 | mousePos(), 69 | // duration (in seconds) 70 | DURATION, 71 | // how value should be updated 72 | (val) => bean.pos = val, 73 | // interpolation function (defaults to easings.linear) 74 | easings[easeType], 75 | ); 76 | }); 77 | -------------------------------------------------------------------------------- /examples/gravity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Gravity 3 | * @description How to make use of gravity in KAPLAY. 4 | * @difficulty 0 5 | * @tags basics, physics 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | // Responding to gravity & jumping 11 | 12 | // Start kaplay 13 | kaplay(); 14 | 15 | // Load assets 16 | loadSprite("bean", "/sprites/bean.png"); 17 | 18 | // Set the gravity acceleration (pixels per second) 19 | setGravity(1600); 20 | 21 | // Add player game object 22 | const player = add([ 23 | sprite("bean"), 24 | pos(center()), 25 | area(), 26 | // body() component gives the ability to respond to gravity 27 | body(), 28 | ]); 29 | 30 | onKeyPress("space", () => { 31 | // .isGrounded() is provided by body() 32 | if (player.isGrounded()) { 33 | // .jump() is provided by body() 34 | player.jump(); 35 | } 36 | }); 37 | 38 | // .onGround() is provided by body(). It registers an event that runs whenever player hits the ground. 39 | player.onGround(() => { 40 | debug.log("ouch"); 41 | }); 42 | 43 | // Accelerate falling when player holding down arrow key 44 | onKeyDown("down", () => { 45 | if (!player.isGrounded()) { 46 | player.vel.y += dt() * 1200; 47 | } 48 | }); 49 | 50 | // Jump higher if space is held 51 | onKeyDown("space", () => { 52 | if (!player.isGrounded() && player.vel.y < 0) { 53 | player.vel.y -= dt() * 600; 54 | } 55 | }); 56 | 57 | // Add a platform to hold the player 58 | add([ 59 | rect(width(), 48), 60 | outline(4), 61 | area(), 62 | pos(0, height() - 48), 63 | // Give objects a body() component if you don't want other solid objects pass through 64 | body({ isStatic: true }), 65 | ]); 66 | 67 | add([ 68 | text("Press space key", { width: width() / 2 }), 69 | pos(12, 12), 70 | ]); 71 | 72 | // Check out https://kaplayjs.com/doc/BodyComp for everything body() provides 73 | --------------------------------------------------------------------------------