fruta 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +367 -26
  3. package/dist/animation/anim.d.ts +13 -0
  4. package/dist/animation/animate.d.ts +101 -0
  5. package/dist/audio/audio.d.ts +29 -0
  6. package/dist/fruta.d.ts +302 -0
  7. package/dist/fruta.js +19 -0
  8. package/dist/frutaGl.d.ts +277 -0
  9. package/dist/game/behaviors.d.ts +38 -0
  10. package/dist/game/charts.d.ts +127 -0
  11. package/dist/game/play.d.ts +4 -0
  12. package/dist/game/project.d.ts +54 -0
  13. package/dist/input/gamepad.d.ts +38 -0
  14. package/dist/math/math.d.ts +37 -0
  15. package/dist/math/pathfind.d.ts +7 -0
  16. package/dist/math/physics.d.ts +38 -0
  17. package/dist/math/pool.d.ts +11 -0
  18. package/dist/math/spatial.d.ts +10 -0
  19. package/dist/render/create/FontCreator.d.ts +16 -0
  20. package/dist/render/create/SaveRestore.d.ts +5 -0
  21. package/dist/render/create/ShapeCreator.d.ts +49 -0
  22. package/dist/render/create/canvasTarget.d.ts +5 -0
  23. package/dist/render/shaders.d.ts +29 -0
  24. package/dist/render/webgl.d.ts +67 -0
  25. package/dist/renderer.d.ts +42 -0
  26. package/dist/types.d.ts +9 -0
  27. package/dist/utils/logStyle.d.ts +1 -0
  28. package/dist/world/camera.d.ts +24 -0
  29. package/dist/world/entities.d.ts +45 -0
  30. package/dist/world/particles.d.ts +42 -0
  31. package/dist/world/tilemap.d.ts +44 -0
  32. package/dist/world/timers.d.ts +11 -0
  33. package/package.json +35 -35
  34. package/DOCUMENTATION.MD +0 -874
  35. package/dist/main.js +0 -1
  36. package/index.html +0 -9
  37. package/settings.json +0 -6
  38. package/src/core/create/_fontCreator.js +0 -11
  39. package/src/core/create/_saveOrRestore.js +0 -22
  40. package/src/core/create/_shapeCreator.js +0 -20
  41. package/src/core/create/fontCreatorMixin.js +0 -167
  42. package/src/core/create/shapeCreatorMixin.js +0 -656
  43. package/src/core/fruta.js +0 -22
  44. package/src/core/game/game.js +0 -30
  45. package/src/core/game/scene.js +0 -29
  46. package/src/core/game/tick.js +0 -42
  47. package/src/core/utils/logStyle.js +0 -8
  48. package/src/core/utils/utils.js +0 -0
  49. package/src/methods/constants.js +0 -0
  50. package/src/methods/creator/_scene.js +0 -0
  51. package/src/methods/creator/creator.js +0 -0
  52. package/webpack.config.js +0 -47
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Jhornan Colina
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.
package/README.md CHANGED
@@ -1,48 +1,389 @@
1
- # Fruta 🍒 - Game Framework
1
+ # Fruta 🍒
2
2
 
3
- FrutaJs is a 2D game engine based on Canvas and WebGL that allows you to create animations, dynamic elements, and video games. Its main objective is to provide an easy-to-understand and dynamic syntax, which helps people to easily enter the world of development.
3
+ [![npm version](https://img.shields.io/npm/v/fruta?color=cb3837&logo=npm)](https://www.npmjs.com/package/fruta)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-first-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
5
+ [![types included](https://img.shields.io/badge/types-included-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
6
+ ![ESM only](https://img.shields.io/badge/module-ESM-f7df1e?logo=javascript&logoColor=black)
7
+ ![size](https://img.shields.io/badge/min-~74%20KB-brightgreen)
8
+ ![NPM License](https://img.shields.io/npm/l/fruta)
4
9
 
5
- The idea behind FrutaJs is to provide an additional layer on top of the existing functionalities of Canvas and WebGL, making it easier to use and allowing developers to focus on creating games and animations without having to deal with the technical complexity of these underlying technologies.
10
+ **A tiny, semantic 2D graphics & game engine for the web.** One friendly object,
11
+ intent-named calls, sensible defaults. The goal: easier to learn than Phaser or Pixi,
12
+ while doing *real* games — sprites, tilemaps, **physics, pathfinding, collision, pooling**,
13
+ camera, scenes, audio, particles, entities, charts, and WebGL shaders.
6
14
 
7
- # Install
15
+ > **Written in TypeScript, types ship with the package** — every call, option and return value
16
+ > is typed, so your editor autocompletes the whole API and catches mistakes before you run.
17
+ > <picture><img alt="TypeScript" src="https://img.shields.io/badge/-3178C6?logo=typescript&logoColor=white" height="14"></picture>
8
18
 
9
- ```npm
10
- npm i fruta
19
+ ```ts
20
+ import Fruta from 'fruta'
21
+
22
+ const fruta = Fruta({ width: 500, height: 500 }).mount()
23
+
24
+ fruta.loop((dt, t) => {
25
+ fruta.background('#111')
26
+ fruta.circle({ x: fruta.mouse.x, y: fruta.mouse.y, r: 24, fill: 'tomato' })
27
+ })
28
+ ```
29
+
30
+ - **One object.** `const fruta = Fruta(...)` — everything hangs off it, fully typed.
31
+ - **Semantic.** `fruta.circle({ x, y, r, fill })`, not `ctx.beginPath()…`.
32
+ - **Immediate by default, retained when you want it** (`fruta.add(...)`).
33
+ - **Two backends, one API.** Canvas2D for the richest features; `Fruta.gl(...)` (WebGL) for tens of thousands of sprites — same sugar.
34
+ - **No build lock-in.** Bring your own `<canvas>` — works in React/Vue/Svelte or plain HTML.
35
+
36
+ > See it all running: clone the repo and `bun run dev` for the demo playground (platformers,
37
+ > a bullet-hell scale test, a Terraria-like chunked world, a Noita-like falling-sand sim, a
38
+ > Factorio-like factory, a level editor, an animation editor, and more — see **What you can build**).
39
+ > Recipes: [EXAMPLES.MD](EXAMPLES.MD) · Changes: [CHANGELOG.md](CHANGELOG.md).
40
+
41
+ ---
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ npm i fruta # or: bun add fruta
47
+ ```
48
+
49
+ ESM-only, zero dependencies, tree-shakeable (`sideEffects: false`), types bundled.
50
+
51
+ ## Setup
52
+
53
+ ```ts
54
+ const fruta = Fruta({ width, height, background?, clear?, mount?, canvas? })
55
+
56
+ fruta.mount(parent?) // append the canvas (defaults to <body>)
57
+ fruta.canvas // the HTMLCanvasElement
58
+ fruta.context // the raw CanvasRenderingContext2D (escape hatch)
59
+ fruta.destroy() // stop the loop + remove listeners (call on unmount)
11
60
  ```
12
61
 
13
- # Usage
62
+ Pass `{ canvas: el }` (element or CSS selector) to use your own canvas — the clean path for
63
+ frameworks. Omit it and Fruta makes one you `.mount()` yourself, or pass `{ mount: el }` to
64
+ attach it on creation.
14
65
 
66
+ If you set `background`, Fruta **repaints it at the start of every frame**, so your `loop`
67
+ body is just the scene. For trails or manual fades, opt out with `{ clear: false }`.
68
+
69
+ ## The loop
70
+
71
+ `loop(fn)` runs every frame. `dt` is seconds since the last frame (multiply movement by it
72
+ and speed is identical on any screen); `t` is seconds since start.
73
+
74
+ ```ts
75
+ let x = 0
76
+ fruta.loop((dt, t) => {
77
+ fruta.background('#111')
78
+ x += 200 * dt // 200 px/second
79
+ fruta.rect({ x, y: 100, w: 40, h: 40, fill: 'deepskyblue' })
80
+ })
81
+ fruta.stop()
82
+
83
+ fruta.timeScale = 0.2 // global slow-mo / freeze (0 = frozen) — hit-stop, bullet-time, Bash
15
84
  ```
16
- import Fruta from "fruta"
17
85
 
18
- const config = {
19
- scene: {
20
- w: 500,
21
- h: 500,
22
- id: 'main',
23
- },
24
- }
86
+ ## Drawing
87
+
88
+ Every shape takes a typed options object with sensible defaults (no `fill`/`stroke` → black fill).
89
+
90
+ ```ts
91
+ fruta.rect({ x, y, w, h, fill, stroke, strokeWidth, radius, rotation }) // rotation in degrees
92
+ fruta.circle({ x, y, r, fill, stroke, strokeWidth }) // x,y = center
93
+ fruta.ellipse({ x, y, rx, ry, rotation, fill, stroke, strokeWidth })
94
+ fruta.line({ x1, y1, x2, y2, stroke, strokeWidth })
95
+ fruta.polygon({ points: [{ x, y }, …], fill, stroke, strokeWidth, close })
96
+ fruta.text('hello', { x, y, fill, size, font, align, baseline, stroke, strokeWidth })
97
+ fruta.image('/pic.png', { x, y, w, h }) // cached + async; call inside loop()
98
+ fruta.background(color) · fruta.clear()
99
+ ```
100
+
101
+ **Gradients** (usable anywhere a `fill`/`stroke` goes), and **transforms** (`push`/`pop`,
102
+ nestable — translate, rotate in degrees, scale, fade):
103
+
104
+ ```ts
105
+ fruta.radialGradient(x, y, r, [[0, '#fff'], [1, 'navy']])
106
+ fruta.linearGradient(x1, y1, x2, y2, [[0, 'red'], [1, 'gold']])
107
+
108
+ fruta.push({ x: 250, y: 250, rotate: angle, scale: 1.5, alpha: 0.8 })
109
+ fruta.circle({ x: 120, y: 0, r: 12, fill: 'deepskyblue' })
110
+ fruta.pop()
111
+ ```
112
+
113
+ ## Animation
114
+
115
+ ```ts
116
+ // tween any numeric props (and colours!) over time → handle: pause()/resume()/cancel()
117
+ fruta.tween(obj, { to: { x: 300, fill: 'gold' }, duration: 0.8, ease: 'bounce',
118
+ delay, repeat, yoyo, onUpdate, onComplete })
25
119
 
26
- const { FontCreator, ShapeCreator, Game } = Fruta(config)
120
+ fruta.stagger(items, { to, duration, ease, each }) // animate a group, offset by `each`s
121
+
122
+ fruta.timeline()
123
+ .to(a, { to: { x: 100 }, duration: 1 })
124
+ .to(b, { to: { y: 50 }, duration: 0.5 }, { at: 0 }) // { at } = absolute time (parallel)
125
+ .call(fn)
126
+
127
+ // named sprite-animation states
128
+ const anim = fruta.anim({ idle: { frames: [0], fps: 1 }, walk: { frames: [1, 2], fps: 9 } })
129
+ anim.play('walk', t)
130
+ fruta.frameAt(t, 8, 4) // quick looping frame: floor(t*fps) % count
131
+ ```
132
+
133
+ **Easings:** `linear`, plus `easeIn/Out/InOut` and the families `quad · cubic · quart · sine ·
134
+ expo · circ · back · bounce · elastic` each as `…In` / `…Out` / `…InOut`. Or pass your own `(t) => number`.
135
+
136
+ ## Input
137
+
138
+ ```ts
139
+ fruta.keyDown('ArrowRight') // true while held (poll in loop)
140
+ fruta.onKey('Space', () => { … }) // on each press
141
+
142
+ fruta.mouse // { x, y } in canvas coords (correct under CSS scaling)
143
+ fruta.mouseDown
144
+ fruta.onClick(p => …) · fruta.onPress(p => …) · fruta.onRelease(p => …) · fruta.onMove(p => …)
145
+
146
+ const pad = fruta.gamepad({ buttons: { jump: 'A', shoot: 'X' }, deadzone: 0.12 })
147
+ pad.down('jump') · pad.pressed('jump') · pad.value('LT') · pad.stick('left')
148
+ ```
149
+
150
+ > Game keys (Space, arrows, Page/Home/End) no longer scroll the page — Fruta `preventDefault`s
151
+ > them (unless you're typing in a form field).
152
+
153
+ ## Audio
154
+
155
+ Web Audio under the hood — low latency, overlapping playback, and a synth `beep` so you can make
156
+ sound with **zero asset files**. Auto-unlocks on first input.
157
+
158
+ ```ts
159
+ await fruta.load({ jump: 'jump.mp3' })
160
+ fruta.play('jump', { volume: 0.5, loop, rate }) // → handle: stop()/volume()
161
+ fruta.beep({ freq: 440, duration: 0.15, type: 'square', volume: 0.3 })
162
+ fruta.volume(0.8) · fruta.mute()
163
+ ```
164
+
165
+ ## Collision & physics
166
+
167
+ **Detection** — overlap tests for rects (`{x,y,w,h}` top-left) and circles (`{x,y,r}` center), auto-detected:
168
+
169
+ ```ts
170
+ fruta.hits(a, b) // overlap?
171
+ fruta.inside(point, shape)
172
+ fruta.overlap(a, b, (a, b) => { … }) // a/b each one thing or a list; callback per overlapping pair
173
+ ```
174
+
175
+ **Response** — push a moving body out of a solid (the arcade collision behind platformers):
176
+
177
+ ```ts
178
+ // fruta.solve(body, solid) → 'x' | 'y' | null; zeroes velocity into the surface, sets body.onGround
179
+ player.vy += GRAVITY * dt
180
+ player.x += player.vx * dt; player.y += player.vy * dt
181
+ player.onGround = false
182
+ for (const wall of solids) fruta.solve(player, wall)
183
+ ```
184
+
185
+ **A real physics world** — `fruta.physics()` gives a `World`: AABB + circle bodies, gravity, and
186
+ body-vs-body collision (impulse solver with **friction + restitution + Baumgarte** stabilisation,
187
+ so stacks settle). The sugar: `world.add(obj)` **attaches physics to any object in place**.
188
+
189
+ ```ts
190
+ const world = fruta.physics({ gravity: 1400 })
191
+ world.static(0, H - 20, W, 20) // walls/floors (mass 0)
192
+ const box = world.box(100, 0, 30, 30, { friction: 0.5, restitution: 0.2 })
193
+ const ball = world.circle(200, 0, 16, { restitution: 0.6 })
194
+ world.add(myPlayer, { mass: 1 }) // give physics to YOUR object
195
+
196
+ fruta.loop((dt) => { world.step(dt); for (const b of world.bodies) draw(b) })
197
+ ```
27
198
 
28
- // Code :)
199
+ ## Pathfinding (A*)
29
200
 
201
+ ```ts
202
+ import { findPath } from 'fruta'
203
+ const path = findPath({ x: 0, y: 0 }, { x: 9, y: 6 }, cols, rows, (x, y) => isWall(x, y))
204
+ // or on a tilemap, around solid tiles:
205
+ const route = map.path({ x: 2, y: 2 }, { x: 18, y: 9 }) // → [{x,y}, …] | null
30
206
  ```
31
207
 
32
- # Current Short Doc
208
+ ## Scale: pooling & spatial hashing
209
+
210
+ The two patterns behind thousands of bullets/enemies at 60 fps — no per-frame allocation, near-O(n) collision.
211
+
212
+ ```ts
213
+ import { Pool, SpatialHash } from 'fruta'
214
+
215
+ const bullets = new Pool(2000, () => ({ x: 0, y: 0, vx: 0, vy: 0, dead: false }))
216
+ const b = bullets.spawn() // next free slot (or null when full); reset its fields
217
+ bullets.kill(i) // swap-remove the active body at index i
218
+
219
+ const hash = new SpatialHash(32) // cell size
220
+ hash.clear(); for (const e of enemies) hash.insert(e.x, e.y, e)
221
+ const near: Enemy[] = []
222
+ hash.query(b.x, b.y, 12, near) // only the enemies in nearby cells
223
+ ```
224
+
225
+ ## Curves & hex grids
226
+
227
+ ```ts
228
+ import { cubicBezier, bezierPath, hexToPixel, pixelToHex, hexNeighbors } from 'fruta'
229
+
230
+ cubicBezier(p0, p1, p2, p3, t) // a point on a cubic Bézier
231
+ const pts = bezierPath(p0, p1, p2, p3, 32) // sample it to a polyline (path / motion curve)
232
+
233
+ hexToPixel(q, r, size) // pointy-top axial → pixel
234
+ pixelToHex(x, y, size) // pixel → { q, r } (rounded)
235
+ hexNeighbors(q, r) // the 6 surrounding hexes
236
+ ```
237
+
238
+ ## Sprites & assets
239
+
240
+ ```ts
241
+ await fruta.load({ hero: '/hero.png' }) // preload → loading screen
242
+ fruta.addImage('hero', imageOrCanvas) // register an image/canvas (e.g. a generated sheet)
243
+ fruta.sprite('hero', { x, y, w, h, frame, frameW, frameH, cols, rotation, anchor: 'center', flipX })
244
+ ```
245
+
246
+ ## Camera
247
+
248
+ Draw the world between `begin()`/`end()`; draw HUD after (screen space).
249
+
250
+ ```ts
251
+ fruta.camera.follow(player, 0.14) // smooth follow (1 = instant)
252
+ fruta.camera.clamp(0, 0, world.w, world.h)
253
+ fruta.camera.zoom = 1.5
254
+ fruta.camera.begin(); /* world drawing */ fruta.camera.end()
255
+ fruta.camera.screenToWorld(fruta.mouse.x, fruta.mouse.y)
256
+ fruta.camera.shake(12, 0.3) // strength px, duration s
257
+ ```
258
+
259
+ ## Tilemaps & platformer physics
260
+
261
+ A grid level that draws itself and resolves arcade collisions against solid tiles.
262
+
263
+ ```ts
264
+ const map = fruta.tilemap({
265
+ size: 32,
266
+ data: [' o ', '#####'], // rows of chars
267
+ tiles: { '#': { solid: true, color: '#7d4a26' } }, // or { sprite, frame }
268
+ bevel: true, // edge light/shadow for depth (autotiling-lite)
269
+ })
270
+ map.draw()
271
+ map.move(player, dt) // moves by vx/vy, resolves vs tiles, sets player.onGround
272
+ map.path({ x: 1, y: 1 }, { x: 9, y: 3 }) // A* around solid tiles
273
+ map.isSolid(tx, ty) · map.at/set(tx, ty)
274
+ ```
275
+
276
+ ## Scenes, particles, timers, save
277
+
278
+ ```ts
279
+ fruta.scene('game', { enter() { reset() }, update(dt, t) { … }, leave() { … } })
280
+ fruta.start('game', 0.4) // switch with a 0.4s fade
281
+
282
+ fruta.burst({ x, y, count: 30, color: ['#ff5', '#5cf'], speed: [40, 200], spread: 360, life: 1, gravity: 250 })
283
+ const trail = fruta.emit({ x, y, rate: 60, color: '#6cf', life: 0.6 }); trail.x = fruta.mouse.x
284
+ fruta.drawParticles()
285
+
286
+ fruta.after(2, () => { … }) // once → handle.cancel()
287
+ fruta.every(0.5, () => { … }) // repeating
288
+ fruta.store('highscore', 9000); fruta.stored('highscore', 0) // persist (localStorage, JSON)
289
+ ```
290
+
291
+ ## Entities (opt-in retained mode)
292
+
293
+ Register an entity and the engine moves it (velocity + gravity + its own `update()`).
294
+
295
+ ```ts
296
+ const ball = fruta.add({
297
+ shape: 'circle', x, y, r: 10, vx: 80, gravity: 520, color: 'tomato',
298
+ bounds: 'bounce', // 'bounce' off walls or 'wrap' across them
299
+ update(self, dt, t) { if (self.y > 400) self.vy *= -0.8 },
300
+ })
301
+ fruta.drawEntities() · fruta.all · ball.remove()
302
+ ```
303
+
304
+ ## Live debug & charts
305
+
306
+ ```ts
307
+ fruta.debug(true) // overlay: FPS, entity/particle/tween counts, mouse
308
+ fruta.watch('player.x', player.x)
309
+
310
+ fruta.barChart({ data: [{ label: 'Mon', value: 40 }, …], max: 100 })
311
+ fruta.lineChart({ series: [{ name: 'Revenue', data: [...] }], points: true, legend: true })
312
+ fruta.pieChart({ data: [...], donut: 0.55 }) · fruta.radar({ axes, series }) · fruta.legend({ … })
313
+ ```
314
+
315
+ Charts handle scales/axes/legend; pass `progress` (0..1) for an animated reveal.
316
+
317
+ ## WebGL: same sugar, GPU speed, shaders
318
+
319
+ For thousands of objects, switch to the WebGL renderer — **identical API**, GPU-batched:
320
+
321
+ ```ts
322
+ const gl = Fruta.gl({ width: 800, height: 600, background: '#0a0a14' }).mount()
323
+ gl.loop((dt) => { gl.background(); for (const b of balls) gl.circle({ x: b.x, y: b.y, r: b.r, fill: b.color }) })
324
+ ```
325
+
326
+ `Fruta.gl` mirrors the full Canvas2D engine — draw, camera, input, **entities, tilemap + physics,
327
+ collision, particles, scenes, timers, save, audio, animation, `timeScale`, `solve`, `physics`** —
328
+ all GPU-batched. Plus **shaders, Fruta-style**:
329
+
330
+ ```ts
331
+ const bg = gl.shader(`void main(){ gl_FragColor = vec4(0.5+0.5*cos(uTime+gl_FragCoord.xyx), 1.0); }`)
332
+ gl.loop(() => bg.draw())
333
+
334
+ // post-processing: ~25 presets, one call
335
+ gl.effect('crt') · gl.effect('vignette', { intensity: 0.7 }) · gl.effect('chromatic') · gl.effect(null)
336
+ // or your own fragment (samples uScene at vUV):
337
+ gl.effect('uniform vec2 uCenter;void main(){ /* a custom shockwave */ }', { uCenter: [0.5, 0.5] })
338
+ ```
339
+
340
+ **~25 post presets:** color (`grayscale invert sepia tint brightness posterize threshold`),
341
+ retro (`scanlines crt vhs pixelate noise glitch`), distortion (`chromatic wave fisheye mirror
342
+ shockwave`), blur/glow (`blur sharpen bloom dream edge`), special (`vignette nightvision`).
343
+
344
+ > Canvas2D and WebGL can't share a canvas, so `Fruta.gl` is its own object. Both are full engines —
345
+ > pick Canvas2D for the richest 2D, WebGL for raw throughput. `WebGLBatch`/`Shader`/`PostFX` are exported.
346
+
347
+ ## Going lower-level
348
+
349
+ ```ts
350
+ fruta.context // raw CanvasRenderingContext2D
351
+ fruta.shapes // chainable Canvas wrappers
352
+ fruta.fonts // chainable text wrappers
353
+ fruta.state // context save/restore
354
+ ```
355
+
356
+ ## What you can build
357
+
358
+ Fruta is small, but it's been stress-tested against a suite of real game/app slices (in the
359
+ playground) — these are the genres it comfortably handles today:
33
360
 
34
- The documentation I have to offer is somewhat early, basically explaining the return of each wrapper made for each method in the canvas.
361
+ | You want… | Fruta gives you |
362
+ |---|---|
363
+ | **Platformers** (Ori-style) | `solve` collision, `timeScale` (hit-stop), tilemaps, camera, shaders for game-feel |
364
+ | **Bullet-hell / survivors** (thousands of entities) | `Pool` + `SpatialHash` + the GL batch renderer — 60 fps at thousands |
365
+ | **Top-down / colony sims** | `findPath` (A*) NPC AI, state machines, day/night, save/load |
366
+ | **Terraria-likes** (huge worlds) | chunked tile streaming, dynamic lighting, simple liquids, mining, procedural gen |
367
+ | **Noita-likes** (pixel physics) | cellular automata over a grid blitted as one texture (`context` + `drawImage`) |
368
+ | **Factorio-likes** (sim at scale) | a fixed-timestep loop + data-oriented machines (belts, inserters, recipes) |
369
+ | **Editor apps** (level / animation / node) | `onPress/onMove/onRelease/onKey` + `context` — zoom/pan, gizmos, undo/redo, timelines |
370
+ | **Data viz & dashboards** | one-call `barChart/lineChart/pieChart/radar` |
35
371
 
36
- It also includes examples where you can understand why things were done the way they were, even though we still need to continue improving what we have. 👽
372
+ The playground (`bun run dev`) ships runnable versions of all of these.
37
373
 
38
- [Shape, Font and Image Methods](https://github.com/karttofer/Fruta/blob/FEATURE/readme/DOCUMENTATION.MD)
374
+ ## TypeScript
39
375
 
40
- # Important
376
+ Fruta is **written in TypeScript and ships its own `.d.ts`** — no `@types/fruta` needed. Every
377
+ option object, return value and exported helper (`World`, `Pool`, `SpatialHash`, `findPath`,
378
+ `cubicBezier`, `hexToPixel`, `Vec`, `Hex`, …) is typed, so you get full autocomplete and
379
+ compile-time checks. It works just as well from plain JavaScript.
41
380
 
42
- I am working hard to have a version one of all this ready, and also to create an API to make it easier to dive into what we are doing.
381
+ ## Status
43
382
 
44
- So, don't despair, we will have more soon 😮
383
+ `0.1.0` alpha, the API is settling but solid. Canvas2D is draw-call bound (fine into the low
384
+ thousands); `Fruta.gl` (WebGL, batched) is the throughput path for tens of thousands. See
385
+ [CHANGELOG.md](CHANGELOG.md) and [ROADMAP.md](ROADMAP.md).
45
386
 
46
- # License
387
+ ## License
47
388
 
48
- This project was released under the [MIT license](https://opensource.org/license/mit/)
389
+ [MIT](https://opensource.org/license/mit/)
@@ -0,0 +1,13 @@
1
+ export interface AnimState {
2
+ frames: number[];
3
+ fps: number;
4
+ }
5
+ export declare class Anim {
6
+ private readonly states;
7
+ private current;
8
+ private startAt;
9
+ constructor(states: Record<string, AnimState>);
10
+ play(name: string, time: number): void;
11
+ frame(time: number): number;
12
+ get state(): string;
13
+ }
@@ -0,0 +1,101 @@
1
+ export type EaseFn = (t: number) => number;
2
+ export interface Updatable {
3
+ update(dt: number): boolean;
4
+ }
5
+ export declare const EASES: {
6
+ linear: (t: number) => number;
7
+ easeIn: (t: number) => number;
8
+ easeOut: (t: number) => number;
9
+ easeInOut: (t: number) => number;
10
+ quadIn: (t: number) => number;
11
+ quadOut: (t: number) => number;
12
+ quadInOut: (t: number) => number;
13
+ cubicIn: (t: number) => number;
14
+ cubicOut: (t: number) => number;
15
+ cubicInOut: (t: number) => number;
16
+ quartIn: (t: number) => number;
17
+ quartOut: (t: number) => number;
18
+ quartInOut: (t: number) => number;
19
+ sineIn: (t: number) => number;
20
+ sineOut: (t: number) => number;
21
+ sineInOut: (t: number) => number;
22
+ expoIn: (t: number) => number;
23
+ expoOut: (t: number) => number;
24
+ expoInOut: (t: number) => number;
25
+ circIn: (t: number) => number;
26
+ circOut: (t: number) => number;
27
+ circInOut: (t: number) => number;
28
+ backIn: (t: number) => number;
29
+ backOut: (t: number) => number;
30
+ backInOut: (t: number) => number;
31
+ bounceIn: (t: number) => number;
32
+ bounceOut: EaseFn;
33
+ bounceInOut: (t: number) => number;
34
+ elasticIn: (t: number) => number;
35
+ elasticOut: EaseFn;
36
+ elasticInOut: (t: number) => number;
37
+ bounce: EaseFn;
38
+ elastic: EaseFn;
39
+ };
40
+ export type EaseName = keyof typeof EASES;
41
+ export type RGBA = [number, number, number, number];
42
+ export declare function parseColor(c: string): RGBA;
43
+ export declare function lerpColor(a: RGBA, b: RGBA, t: number): string;
44
+ export interface TweenSpec {
45
+ to: Record<string, number>;
46
+ duration: number;
47
+ ease: EaseFn;
48
+ delay: number;
49
+ repeat: number;
50
+ yoyo: boolean;
51
+ onUpdate?: (target: Record<string, number>, progress: number) => void;
52
+ onComplete?: () => void;
53
+ }
54
+ export declare class Tween implements Updatable {
55
+ private readonly target;
56
+ private readonly spec;
57
+ private from;
58
+ private captured;
59
+ private elapsed;
60
+ private cancelled;
61
+ private paused;
62
+ constructor(target: Record<string, number>, spec: TweenSpec);
63
+ update(dt: number): boolean;
64
+ cancel(): void;
65
+ pause(): void;
66
+ resume(): void;
67
+ }
68
+ type TweenFactory = (target: Record<string, number>, opts: {
69
+ to?: Record<string, number | string>;
70
+ duration: number;
71
+ ease?: EaseName | EaseFn;
72
+ delay?: number;
73
+ repeat?: number;
74
+ yoyo?: boolean;
75
+ onUpdate?: (t: any, p: number) => void;
76
+ onComplete?: () => void;
77
+ }, absoluteDelay: number) => Tween;
78
+ export interface TimelinePlace {
79
+ at?: number;
80
+ }
81
+ export declare class Timeline implements Updatable {
82
+ private readonly factory;
83
+ private children;
84
+ private cursor;
85
+ private finishedCb?;
86
+ constructor(factory: TweenFactory);
87
+ to(target: Record<string, number>, opts: {
88
+ to: Record<string, number | string>;
89
+ duration: number;
90
+ ease?: EaseName | EaseFn;
91
+ delay?: number;
92
+ repeat?: number;
93
+ yoyo?: boolean;
94
+ onUpdate?: (t: any, p: number) => void;
95
+ onComplete?: () => void;
96
+ }, place?: TimelinePlace): this;
97
+ call(fn: () => void, place?: TimelinePlace): this;
98
+ then(fn: () => void): this;
99
+ update(dt: number): boolean;
100
+ }
101
+ export {};
@@ -0,0 +1,29 @@
1
+ export interface PlayOptions {
2
+ volume?: number;
3
+ loop?: boolean;
4
+ rate?: number;
5
+ }
6
+ export interface BeepOptions {
7
+ freq?: number;
8
+ duration?: number;
9
+ type?: OscillatorType;
10
+ volume?: number;
11
+ }
12
+ export interface Sound {
13
+ stop(): void;
14
+ volume(v: number): void;
15
+ }
16
+ export declare class AudioManager {
17
+ private ctx;
18
+ private master;
19
+ private buffers;
20
+ private muted;
21
+ private level;
22
+ private ensure;
23
+ resume(): void;
24
+ loadSound(name: string, src: string): Promise<void>;
25
+ play(name: string, opts?: PlayOptions): Sound;
26
+ beep(opts?: BeepOptions): void;
27
+ volume(v: number): void;
28
+ mute(on?: boolean): void;
29
+ }