punter.js 1.0.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.
- package/.nvmrc +1 -0
- package/LICENSE.MD +21 -0
- package/README.MD +731 -0
- package/games/pong.html +260 -0
- package/images/pong/sprites/ball.png +0 -0
- package/images/pong/sprites/paddle.png +0 -0
- package/images/punterjs.png +0 -0
- package/package.json +33 -0
- package/punter.js +1514 -0
- package/tests/fixtures/index.html +30 -0
- package/tests/fixtures/silence.wav +0 -0
- package/tests/input-spec.js +104 -0
- package/tests/interface-spec.js +200 -0
- package/tests/jasmine/config.json +20 -0
- package/tests/jasmine/reporter.js +25 -0
- package/tests/jasmine/teardown.js +7 -0
- package/tests/jasmine/timeout.js +1 -0
- package/tests/scenes-spec.js +87 -0
- package/tests/setup.js +49 -0
- package/tests/sounds-spec.js +83 -0
- package/tests/sprites-spec.js +259 -0
package/README.MD
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
# punter.js
|
|
2
|
+
|
|
3
|
+
<p align="center"><img src="images/punterjs.png" width="290" height="174" /></p>
|
|
4
|
+
|
|
5
|
+
A simple, dependency-free 2D game engine for the browser.
|
|
6
|
+
|
|
7
|
+
**Why?** Back in the Amiga days, AMOS let you build games in BASIC without touching assembly - sprites, sound, and input just worked, so you could get a character moving in minutes. `punter.js` brings that same philosophy to the browser: no build step, no ES6, no dependencies - just a small single file with a readable API.
|
|
8
|
+
|
|
9
|
+
**Features**
|
|
10
|
+
* **Auto-scaling** - fixed internal coordinates scale and automatically reposition sprites on resize
|
|
11
|
+
* **Retina support** - renders at up to 2× device pixel ratio automatically
|
|
12
|
+
* **Pixel-accurate collision** - bounding boxes ignore transparent pixels
|
|
13
|
+
* **HTML state attributes** - `data-punter-*` attributes on `<html>` let you drive CSS from game state
|
|
14
|
+
* **CSS variables** - `--punter-vpw`/`--punter-vph` on `<html>`, updated on resize
|
|
15
|
+
|
|
16
|
+
## Contents
|
|
17
|
+
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Running the Example](#running-the-example)
|
|
20
|
+
- [Quick Start](#quick-start)
|
|
21
|
+
- [punter.init()](#punter-init)
|
|
22
|
+
- [Scenes](#scenes)
|
|
23
|
+
- [Events](#events)
|
|
24
|
+
- [Sprites](#sprites)
|
|
25
|
+
- [Creating a Sprite](#creating-a-sprite)
|
|
26
|
+
- [Sprite Properties](#sprite-properties)
|
|
27
|
+
- [Sprite Methods](#sprite-methods)
|
|
28
|
+
- [Collision Detection](#collision-detection)
|
|
29
|
+
- [Animation](#animation)
|
|
30
|
+
- [Scrolling & Parallax](#scrolling--parallax)
|
|
31
|
+
- [Sound](#sound)
|
|
32
|
+
- [Input](#input)
|
|
33
|
+
- [Game Loop Control](#game-loop-control)
|
|
34
|
+
- [Engine Properties](#engine-properties)
|
|
35
|
+
- [CSS & HTML Hooks](#css--html-hooks)
|
|
36
|
+
- [Debug Mode](#debug-mode)
|
|
37
|
+
- [Browser Support](#browser-support)
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Download `punter.js` and include it with a script tag - before your game code:
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<!doctype html>
|
|
45
|
+
<html>
|
|
46
|
+
<head>
|
|
47
|
+
<script src="punter.js"></script>
|
|
48
|
+
<script>
|
|
49
|
+
// your game code here
|
|
50
|
+
</script>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<canvas id="game"></canvas>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Running the Example
|
|
59
|
+
|
|
60
|
+
The repo includes a Pong game in `games/pong.html`. Because it loads local files, you need a simple web server rather than opening the file directly in a browser.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm start
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then open **http://localhost:3000/games/pong.html** in your browser.
|
|
67
|
+
|
|
68
|
+
> `npm start` uses `npx serve` — no install step needed beyond having Node.js.
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
punter.init({
|
|
74
|
+
canvas: '#game',
|
|
75
|
+
sprites: {
|
|
76
|
+
player: 'images/player.png'
|
|
77
|
+
},
|
|
78
|
+
sounds: {
|
|
79
|
+
jump: 'sounds/jump.mp3'
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// level 1 game loop
|
|
84
|
+
punter.scene('level1', function () {
|
|
85
|
+
|
|
86
|
+
// create a sprite
|
|
87
|
+
var player = punter.createSprite({
|
|
88
|
+
id: 'player',
|
|
89
|
+
key: 'player',
|
|
90
|
+
x: '50%',
|
|
91
|
+
y: '50%'
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// move it on ever frame if required
|
|
95
|
+
punter.on('update', function () {
|
|
96
|
+
if (punter.keys['ArrowRight']) player.moveX(3);
|
|
97
|
+
if (punter.keys['ArrowLeft']) player.moveX(-3);
|
|
98
|
+
if (punter.keys['ArrowUp']) player.moveY(-3);
|
|
99
|
+
if (punter.keys['ArrowDown']) player.moveY(3);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// draw it to every frame
|
|
103
|
+
punter.on('draw', function () {
|
|
104
|
+
player.draw(this);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// when the game engine ready, go to level 1
|
|
109
|
+
punter.on('ready', function () {
|
|
110
|
+
punter.go('level1');
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## `punter.init()`
|
|
115
|
+
|
|
116
|
+
This is the engine's start-up call. You tell it which canvas to draw on, which images to preload, and which sounds to load. Nothing else will work until `init` has finished loading everything - that's what the `ready` event is for.
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
punter.init({
|
|
120
|
+
canvas: '#game', // the canvas to draw on
|
|
121
|
+
debug: false, // set to true to show FPS, bounding boxes, and labels
|
|
122
|
+
sprites: {
|
|
123
|
+
player: 'images/player.png',
|
|
124
|
+
enemy: 'images/enemy.png',
|
|
125
|
+
coin: 'images/coin.png'
|
|
126
|
+
},
|
|
127
|
+
sounds: {
|
|
128
|
+
jump: 'sounds/jump.mp3',
|
|
129
|
+
collect: 'sounds/collect.mp3'
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
punter.on('ready', function () {
|
|
134
|
+
// safe to create sprites and go to a scene here
|
|
135
|
+
punter.go('menu');
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Config Options
|
|
140
|
+
|
|
141
|
+
Option | Type | Required | Description
|
|
142
|
+
----------|---------------------------------|----------|-------------------------------------------------------------------------
|
|
143
|
+
`canvas` | `string` or `HTMLCanvasElement` | Yes | A CSS selector (`'#game'`) or a direct canvas element reference
|
|
144
|
+
`debug` | `boolean` | No | Enables the debug overlay (FPS, sprite bounds, labels). Default: `false`
|
|
145
|
+
`sprites` | `object` | No | Key/URL pairs of images to preload: `{ key: 'path/to/image.png' }`
|
|
146
|
+
`sounds` | `object` | No | Key/URL pairs of audio files to preload: `{ key: 'path/to/sound.mp3' }`
|
|
147
|
+
|
|
148
|
+
## Scenes
|
|
149
|
+
|
|
150
|
+
Scenes are the different "screens" in your game - the main menu, level 1, a game over screen, etc. You define each scene as a named function, then switch between them with `punter.go()`.
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
// Define scenes anywhere before calling punter.go()
|
|
154
|
+
punter.scene('menu', function () {
|
|
155
|
+
|
|
156
|
+
var title = punter.createSprite({ id: 'title', key: 'titleImage', x: '50%', y: '30%' });
|
|
157
|
+
|
|
158
|
+
punter.on('draw', function () {
|
|
159
|
+
title.draw(this);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
punter.on('update', function () {
|
|
163
|
+
// switch to level1 when the screen is tapped or clicked
|
|
164
|
+
if (punter.mouse.clicked) {
|
|
165
|
+
punter.go('level1');
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
punter.scene('level1', function () {
|
|
172
|
+
// level setup here
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Switching scenes clears the previous update/draw handlers automatically
|
|
176
|
+
punter.go('menu');
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Methods
|
|
180
|
+
|
|
181
|
+
Method | Description
|
|
182
|
+
-------------------------|-------------------------------------------------------------------------------------------------------
|
|
183
|
+
`punter.scene(name, fn)` | Registers a scene. `name` is a string key; `fn` is the setup function that runs when the scene starts.
|
|
184
|
+
`punter.go(name)` | Switches to the named scene. Starts the game loop if it isn't already running. Clears all input state.
|
|
185
|
+
|
|
186
|
+
## Events
|
|
187
|
+
|
|
188
|
+
Events let your code react to things the engine does. The two you'll use most are `update` (run your game logic here) and `draw` (draw your sprites here). These are called automatically by the engine ~60 times per second.
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
punter.on('ready', function () {
|
|
192
|
+
// fires once - all images and sounds are loaded, safe to start
|
|
193
|
+
punter.go('level1');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
punter.on('update', function () {
|
|
197
|
+
// fires ~60 times/sec - move things, check collisions, handle input
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
punter.on('draw', function () {
|
|
201
|
+
// fires every frame - draw your sprites
|
|
202
|
+
// 'this' inside draw is the canvas 2D context
|
|
203
|
+
player.draw(this);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
punter.on('resize', function () {
|
|
207
|
+
// fires when the browser window or device orientation changes
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
punter.on('go', function (sceneName) {
|
|
211
|
+
// fires every time punter.go() is called - receives the new scene name
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Event Reference
|
|
216
|
+
|
|
217
|
+
Event | Callback signature | When it fires
|
|
218
|
+
---------|-----------------------|--------------------------------------------------------------
|
|
219
|
+
`ready` | `function()` | Once, after all sprites and sounds have finished loading
|
|
220
|
+
`update` | `function()` | Every logic tick (~60/sec) - put movement and game logic here
|
|
221
|
+
`draw` | `function()` | Every render frame - `this` is the canvas 2D context
|
|
222
|
+
`resize` | `function()` | When the browser window resizes or orientation changes
|
|
223
|
+
`go` | `function(sceneName)` | When `punter.go()` is called; receives the new scene name
|
|
224
|
+
|
|
225
|
+
> Each event can only have one handler at a time. Calling `punter.on('update', fn)` again replaces the previous handler. Switching scenes via `punter.go()` automatically clears the `update` and `draw` handlers.
|
|
226
|
+
|
|
227
|
+
## Sprites
|
|
228
|
+
|
|
229
|
+
A sprite is any image in your game - a character, an obstacle, a background, a coin. You create a sprite by giving it an ID, pointing it at a preloaded image, and setting its starting position. The engine handles scaling, cropping, and drawing it to the canvas.
|
|
230
|
+
|
|
231
|
+
### Creating a Sprite
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
var player = punter.createSprite({
|
|
235
|
+
id: 'player', // unique ID - no two sprites can share one
|
|
236
|
+
key: 'player', // matches a key from config.sprites
|
|
237
|
+
x: '50%', // horizontal position - number (pixels) or percent string
|
|
238
|
+
y: '80%' // vertical position - number (pixels) or percent string
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Sprite with a fixed size
|
|
242
|
+
var background = punter.createSprite({
|
|
243
|
+
id: 'bg',
|
|
244
|
+
key: 'background',
|
|
245
|
+
x: 0,
|
|
246
|
+
y: 0,
|
|
247
|
+
w: '100%', // width as percent of canvas width
|
|
248
|
+
h: '100%' // height as percent of canvas height
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Creation Options
|
|
253
|
+
|
|
254
|
+
Option | Type | Default | Description
|
|
255
|
+
-----------------|------------------------|------------|-----------------------------------------------------------------------------------------------------------
|
|
256
|
+
`id` | `string` | - | **Required.** Unique identifier for this sprite.
|
|
257
|
+
`key` | `string` or `string[]` | - | **Required.** Image key from `config.sprites`. Pass an array for animation frames: `['frame1', 'frame2']`.
|
|
258
|
+
`x` | `number` or `string` | - | **Required.** Horizontal position in pixels or as `'50%'`.
|
|
259
|
+
`y` | `number` or `string` | - | **Required.** Vertical position in pixels or as `'50%'`.
|
|
260
|
+
`w` | `number` or `string` | auto | Width in pixels or percent. Inferred from image if omitted.
|
|
261
|
+
`h` | `number` or `string` | auto | Height in pixels or percent. Inferred from image if omitted.
|
|
262
|
+
`preserveAspect` | `boolean` | `true` | Keep the image's aspect ratio when only one dimension is set.
|
|
263
|
+
`collidable` | `boolean` | `true` | Compute a pixel-accurate bounding box for collision checks.
|
|
264
|
+
`outline` | `string` | `null` | Draw a coloured border around the sprite (e.g. `'red'`). Useful for debugging.
|
|
265
|
+
`repeatX` | `boolean` | `false` | Tile the image horizontally across the full canvas width (e.g. for a floor).
|
|
266
|
+
`repeatY` | `boolean` | `false` | Tile the image vertically across the full canvas height (e.g. for a wall).
|
|
267
|
+
`clipHeight` | `number` | `null` | Limit how many pixels tall the sprite is drawn - useful for fill/progress bars.
|
|
268
|
+
`clipFrom` | `string` | `'bottom'` | Which end to clip from: `'top'` or `'bottom'`.
|
|
269
|
+
|
|
270
|
+
### Sprite Properties
|
|
271
|
+
|
|
272
|
+
After creation, these properties are available on the sprite object:
|
|
273
|
+
|
|
274
|
+
Property | Type | Writable | Description
|
|
275
|
+
--------------|----------------------|----------|---------------------------------------------------------------------------------------------
|
|
276
|
+
`x` | `number` | Yes | Current horizontal position in pixels.
|
|
277
|
+
`y` | `number` | Yes | Current vertical position in pixels.
|
|
278
|
+
`w` | `number` | Yes | Current width in pixels.
|
|
279
|
+
`h` | `number` | Yes | Current height in pixels.
|
|
280
|
+
`actualW` | `number` | No | Rendered width (floored). Read-only.
|
|
281
|
+
`actualH` | `number` | No | Rendered height, respecting `clipHeight`. Read-only.
|
|
282
|
+
`initialX` | `number` | Yes | The x position the sprite started at (used by `bounce()`).
|
|
283
|
+
`initialY` | `number` | Yes | The y position the sprite started at (used by `bounce()`).
|
|
284
|
+
`frame` | `number\ | null` | Yes | Override the current animation frame index. Set to `null` to let `animate()` control it.
|
|
285
|
+
`visible` | `boolean` | No | `true` if any part of the sprite is within the canvas bounds. Read-only.
|
|
286
|
+
`seen` | `boolean` | Yes | Becomes `true` once the sprite has been visible on screen. Can be reset manually.
|
|
287
|
+
`destroyed` | `boolean` | No | `true` after `destroy()` has been called. Read-only.
|
|
288
|
+
`bounds` | `object` | No | Pixel-accurate bounding box: `{ x, y, w, h }`. Updated each frame after `draw()`. Read-only.
|
|
289
|
+
`collidable` | `boolean` | Yes | Whether to compute collision bounds.
|
|
290
|
+
`clipHeight` | `number\ | null` | Yes | Current clip height.
|
|
291
|
+
`key` | `string\ | string[]` | Yes | The image key (or array of keys for animation).
|
|
292
|
+
`id` | `string` | No | The sprite's unique ID.
|
|
293
|
+
`aspectRatio` | `number` | No | Width-to-height ratio of the original image. Read-only.
|
|
294
|
+
|
|
295
|
+
### Sprite Methods
|
|
296
|
+
|
|
297
|
+
#### `sprite.draw(ctx)`
|
|
298
|
+
|
|
299
|
+
Draws the sprite to the canvas. Call this inside your `draw` event handler. `ctx` is the canvas 2D context passed as `this` in the `draw` callback.
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
punter.on('draw', function () {
|
|
303
|
+
player.draw(this);
|
|
304
|
+
enemy.draw(this);
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### `sprite.moveX(dx)` / `sprite.moveY(dy)`
|
|
309
|
+
|
|
310
|
+
Move the sprite by `dx` pixels horizontally or `dy` pixels vertically. Call these in your `update` handler.
|
|
311
|
+
|
|
312
|
+
```js
|
|
313
|
+
punter.on('update', function () {
|
|
314
|
+
if (punter.keys['ArrowRight']) player.moveX(3);
|
|
315
|
+
if (punter.keys['ArrowLeft']) player.moveX(-3);
|
|
316
|
+
if (punter.keys['Space']) player.moveY(-5); // jump
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
#### `sprite.center(offsetX?, offsetY?)`
|
|
321
|
+
|
|
322
|
+
Centers the sprite on the canvas. Optional pixel offsets shift it from the centre.
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
player.center(); // perfectly centred
|
|
326
|
+
player.center(0, -50); // centred, but 50px above the middle
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Also available as `sprite.centerX(offsetX?)` and `sprite.centerY(offsetY?)` to centre on one axis only.
|
|
330
|
+
|
|
331
|
+
#### `sprite.bounce(range?, speed?)`
|
|
332
|
+
|
|
333
|
+
Makes the sprite float up and down in a smooth sine-wave motion. Call every frame in `update`.
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
punter.on('update', function () {
|
|
337
|
+
coin.bounce(6, 12); // range: 6px, speed: 12 (higher = slower)
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Parameter | Type | Default | Description
|
|
342
|
+
----------|----------|---------|--------------------------------------------------
|
|
343
|
+
`range` | `number` | `8` | Max pixels to move up/down from `initialY`.
|
|
344
|
+
`speed` | `number` | `10` | Controls how fast the bounce is. Higher = slower.
|
|
345
|
+
|
|
346
|
+
#### `sprite.animate(delayMs)`
|
|
347
|
+
|
|
348
|
+
Advances to the next animation frame. Call every frame in `update`. Requires `key` to be an array of image keys.
|
|
349
|
+
|
|
350
|
+
```js
|
|
351
|
+
var run = punter.createSprite({
|
|
352
|
+
id: 'runner',
|
|
353
|
+
key: ['run1', 'run2', 'run3', 'run4'],
|
|
354
|
+
x: 100, y: 200
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
punter.on('update', function () {
|
|
358
|
+
run.animate(120); // switch frame every 120ms
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
> Set `sprite.frame` to a specific index to manually control the frame instead of letting `animate()` cycle through.
|
|
363
|
+
|
|
364
|
+
#### `sprite.destroy()`
|
|
365
|
+
|
|
366
|
+
Removes the sprite from the engine. Any subsequent `draw()` calls on it will be ignored. The `sprite.destroyed` property becomes `true`.
|
|
367
|
+
|
|
368
|
+
```js
|
|
369
|
+
if (player.isCollidingWith(enemy)) {
|
|
370
|
+
enemy.destroy();
|
|
371
|
+
punter.playSound('explosion');
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Collision Detection
|
|
376
|
+
|
|
377
|
+
Collision detection lets you know when two sprites are touching or overlapping. The engine uses each image's actual pixel content to build a tight bounding box - transparent areas around the image are ignored - so collisions feel accurate even for irregularly shaped sprites.
|
|
378
|
+
|
|
379
|
+
```js
|
|
380
|
+
punter.on('update', function () {
|
|
381
|
+
if (player.isCollidingWith(bullet)) {
|
|
382
|
+
punter.playSound('hit');
|
|
383
|
+
bullet.destroy();
|
|
384
|
+
// handle player damage
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### `sprite.isCollidingWith(otherSprite)`
|
|
390
|
+
|
|
391
|
+
Returns `true` if the bounding boxes of the two sprites overlap, `false` otherwise. Both sprites must have `collidable: true` (which is the default).
|
|
392
|
+
|
|
393
|
+
```js
|
|
394
|
+
var hit = player.isCollidingWith(enemy); // true or false
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
> Bounding boxes are automatically updated each frame when the sprite is drawn. Always call `sprite.draw()` before checking collisions.
|
|
398
|
+
|
|
399
|
+
### Animation
|
|
400
|
+
|
|
401
|
+
Sprites can cycle through multiple images to create animations. Pass an array of keys to `key` when creating the sprite, then call `animate()` each frame:
|
|
402
|
+
|
|
403
|
+
```js
|
|
404
|
+
punter.init({
|
|
405
|
+
sprites: {
|
|
406
|
+
idle1: 'images/idle1.png',
|
|
407
|
+
idle2: 'images/idle2.png',
|
|
408
|
+
idle3: 'images/idle3.png'
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
var character = punter.createSprite({
|
|
413
|
+
id: 'char',
|
|
414
|
+
key: ['idle1', 'idle2', 'idle3'],
|
|
415
|
+
x: 100, y: 100
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
punter.on('update', function () {
|
|
419
|
+
character.animate(150); // new frame every 150ms
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
punter.on('draw', function () {
|
|
423
|
+
character.draw(this);
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
To manually jump to a specific frame, set `character.frame = 2`. To hand control back to `animate()`, set it to `null`.
|
|
428
|
+
|
|
429
|
+
### Scrolling & Parallax
|
|
430
|
+
|
|
431
|
+
Three methods are available for scrolling behaviour on sprites used as backgrounds or moving scenery:
|
|
432
|
+
|
|
433
|
+
#### `sprite.parallaxScrollX(speed, respawnAfterMs, offset?)`
|
|
434
|
+
|
|
435
|
+
Moves the sprite left or right continuously. When it goes fully off-screen, it waits `respawnAfterMs` milliseconds then reappears from the opposite edge. Great for clouds, birds, or background layers.
|
|
436
|
+
|
|
437
|
+
```js
|
|
438
|
+
var cloud = punter.createSprite({ id: 'cloud1', key: 'cloud', x: 0, y: 50, w: 120, h: 60 });
|
|
439
|
+
|
|
440
|
+
punter.on('update', function () {
|
|
441
|
+
cloud.parallaxScrollX(-2, 1000); // move left at 2px/frame, respawn after 1 second
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Parameter | Type | Description
|
|
446
|
+
-----------------|----------|----------------------------------------------------------------------
|
|
447
|
+
`speed` | `number` | Pixels per frame. Negative = left, positive = right.
|
|
448
|
+
`respawnAfterMs` | `number` | Milliseconds to wait before reappearing from the opposite edge.
|
|
449
|
+
`offset` | `number` | Extra pixels beyond the screen edge before respawning. Default: `50`.
|
|
450
|
+
|
|
451
|
+
#### `sprite.parallaxScrollY(speed, respawnAfterMs, offset?)`
|
|
452
|
+
|
|
453
|
+
Same as `parallaxScrollX` but moves vertically.
|
|
454
|
+
|
|
455
|
+
```js
|
|
456
|
+
punter.on('update', function () {
|
|
457
|
+
rain.parallaxScrollY(4, 500); // fall down at 4px/frame, respawn after 0.5s
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### `sprite.loopScrollX(speed)`
|
|
462
|
+
|
|
463
|
+
Moves the sprite horizontally and immediately wraps it to the other side when it exits the canvas - no delay. Useful for seamlessly looping backgrounds.
|
|
464
|
+
|
|
465
|
+
```js
|
|
466
|
+
var ground = punter.createSprite({ id: 'ground', key: 'ground', x: 0, y: '90%', w: '100%', h: 64 });
|
|
467
|
+
|
|
468
|
+
punter.on('update', function () {
|
|
469
|
+
ground.loopScrollX(-3); // scroll left and wrap
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
## Sound
|
|
475
|
+
|
|
476
|
+
punter.js preloads audio files and lets you play and stop them by name. Works on mobile too - sounds are decoded using the Web Audio API, so they play instantly without delay.
|
|
477
|
+
|
|
478
|
+
```js
|
|
479
|
+
punter.init({
|
|
480
|
+
sounds: {
|
|
481
|
+
jump: 'sounds/jump.mp3',
|
|
482
|
+
music: 'sounds/theme.mp3'
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
punter.on('ready', function () {
|
|
487
|
+
// play background music, looping
|
|
488
|
+
punter.playSound('music', { loop: true, volume: 0.5 });
|
|
489
|
+
|
|
490
|
+
punter.go('level1');
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// play a one-shot sound effect
|
|
494
|
+
punter.playSound('jump');
|
|
495
|
+
|
|
496
|
+
// stop a sound that's playing
|
|
497
|
+
punter.stopSound('music');
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### `punter.playSound(name, options?)`
|
|
501
|
+
|
|
502
|
+
Plays a preloaded sound. If the sound is already playing, it stops and restarts it.
|
|
503
|
+
|
|
504
|
+
Option | Type | Default | Description
|
|
505
|
+
---------|-----------|---------|--------------------------------------------------------------------
|
|
506
|
+
`volume` | `number` | `1` | Volume from `0` (silent) to `1` (full).
|
|
507
|
+
`loop` | `boolean` | `false` | If `true`, the sound repeats indefinitely.
|
|
508
|
+
`once` | `boolean` | `false` | If `true`, the sound is not tracked - `stopSound()` cannot stop it.
|
|
509
|
+
`speed` | `number` | `1` | Playback speed multiplier. `0.5` = half speed, `2` = double speed.
|
|
510
|
+
|
|
511
|
+
### `punter.stopSound(name)`
|
|
512
|
+
|
|
513
|
+
Stops all currently playing instances of the named sound.
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
## Input
|
|
517
|
+
|
|
518
|
+
The engine tracks keyboard keys and mouse/touch taps for you. You check the current state of these in your `update` handler each frame. Touch events on mobile are automatically translated into mouse events, so the same code works on phones and desktops.
|
|
519
|
+
|
|
520
|
+
### Keyboard
|
|
521
|
+
|
|
522
|
+
`punter.keys` is a plain object where each key name maps to `true` when held down and `false` (or absent) when released.
|
|
523
|
+
|
|
524
|
+
```js
|
|
525
|
+
punter.on('update', function () {
|
|
526
|
+
if (punter.keys['ArrowLeft']) player.moveX(-3);
|
|
527
|
+
if (punter.keys['ArrowRight']) player.moveX(3);
|
|
528
|
+
if (punter.keys['ArrowUp']) player.moveY(-3);
|
|
529
|
+
if (punter.keys['ArrowDown']) player.moveY(3);
|
|
530
|
+
if (punter.keys[' ']) player.moveY(-8); // spacebar to jump
|
|
531
|
+
if (punter.keys['Escape']) punter.go('menu');
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Key names match the browser's [KeyboardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key) values: `'ArrowLeft'`, `'ArrowRight'`, `'ArrowUp'`, `'ArrowDown'`, `'Enter'`, `'Escape'`, `' '` (space), letter keys (`'a'`, `'A'`), etc.
|
|
536
|
+
|
|
537
|
+
### Mouse & Touch
|
|
538
|
+
|
|
539
|
+
`punter.mouse` tracks the current pointer position and whether a click or tap has occurred.
|
|
540
|
+
|
|
541
|
+
```js
|
|
542
|
+
punter.on('update', function () {
|
|
543
|
+
if (punter.mouse.clicked) {
|
|
544
|
+
// player tapped or clicked - punter.mouse.x / .y has the position
|
|
545
|
+
fireAt(punter.mouse.x, punter.mouse.y);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Property | Type | Description
|
|
551
|
+
-----------------------|-----------|--------------------------------------------------------------
|
|
552
|
+
`punter.mouse.x` | `number` | Horizontal position of the last click/tap in canvas pixels.
|
|
553
|
+
`punter.mouse.y` | `number` | Vertical position of the last click/tap in canvas pixels.
|
|
554
|
+
`punter.mouse.clicked` | `boolean` | `true` for one update tick after a click or tap, then auto-cleared.
|
|
555
|
+
|
|
556
|
+
> Coordinates are in canvas pixel space — the same coordinate system as `punter.width` / `punter.height` and sprite `x`/`y` values.
|
|
557
|
+
|
|
558
|
+
### `punter.clearInput()`
|
|
559
|
+
|
|
560
|
+
Resets all keyboard key states to `false` and clears `mouse.clicked`. Called automatically whenever `punter.go()` switches scenes.
|
|
561
|
+
|
|
562
|
+
```js
|
|
563
|
+
punter.on('update', function () {
|
|
564
|
+
if (punter.mouse.clicked) {
|
|
565
|
+
punter.go('level1');
|
|
566
|
+
// no need to clear - punter.go() does it automatically
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
## Game Loop Control
|
|
572
|
+
|
|
573
|
+
The game loop is what makes `update` and `draw` fire continuously. It starts automatically when you call `punter.go()`. These methods let you pause and resume it manually - for example, when showing a pause menu.
|
|
574
|
+
|
|
575
|
+
```js
|
|
576
|
+
// Pause the loop (stops update and draw from firing)
|
|
577
|
+
punter.pause();
|
|
578
|
+
|
|
579
|
+
// Resume from where it left off
|
|
580
|
+
punter.resume();
|
|
581
|
+
|
|
582
|
+
// Force a single redraw without running update logic
|
|
583
|
+
// Useful after a resize when the loop isn't running
|
|
584
|
+
punter.redraw();
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Method | Description
|
|
588
|
+
------------------|-------------------------------------------------------
|
|
589
|
+
`punter.pause()` | Stops the game loop. The screen freezes.
|
|
590
|
+
`punter.resume()` | Restarts the game loop.
|
|
591
|
+
`punter.redraw()` | Redraws all sprites once without advancing game logic.
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
## Engine Properties
|
|
595
|
+
|
|
596
|
+
These are read-only properties available on the `punter` object at any time:
|
|
597
|
+
|
|
598
|
+
Property | Type | Description
|
|
599
|
+
---------------------|----------------------------|----------------------------------------------------------------------------
|
|
600
|
+
`punter.width` | `number` | Internal canvas width in pixels (adjusted for device pixel ratio).
|
|
601
|
+
`punter.height` | `number` | Internal canvas height in pixels (adjusted for device pixel ratio).
|
|
602
|
+
`punter.dpr` | `number` | Device pixel ratio used for rendering (capped at `2`).
|
|
603
|
+
`punter.frame` | `number` | Current frame counter, cycling 1–60.
|
|
604
|
+
`punter.totalFrames` | `number` | Total number of frames rendered since the loop started.
|
|
605
|
+
`punter.running` | `boolean` | `true` if the game loop is currently running.
|
|
606
|
+
`punter.paused` | `boolean` | `true` if the loop has been paused.
|
|
607
|
+
`punter.resized` | `boolean` | `true` for one frame after a resize event.
|
|
608
|
+
`punter.isMobile` | `boolean` | `true` if the engine detected a mobile device.
|
|
609
|
+
`punter.isDesktop` | `boolean` | `true` if the engine detected a desktop device.
|
|
610
|
+
`punter.orientation` | `string` | `'portrait'` or `'landscape'` based on current window dimensions.
|
|
611
|
+
`punter.sceneName` | `string` | The name of the currently active scene, or `''` if none.
|
|
612
|
+
`punter.debug` | `boolean` | Whether debug mode is active. Can be set at runtime: `punter.debug = true`.
|
|
613
|
+
`punter.canvas` | `HTMLCanvasElement` | The raw canvas element.
|
|
614
|
+
`punter.ctx` | `CanvasRenderingContext2D` | The canvas 2D drawing context.
|
|
615
|
+
`punter.sprites` | `Sprite[]` | Array of all live (non-destroyed) sprites.
|
|
616
|
+
|
|
617
|
+
### `punter.getSprite(id)`
|
|
618
|
+
|
|
619
|
+
Retrieves a sprite by its ID. Returns `null` if not found.
|
|
620
|
+
|
|
621
|
+
```js
|
|
622
|
+
var player = punter.getSprite('player');
|
|
623
|
+
if (player) player.moveX(5);
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
## CSS & HTML Hooks
|
|
628
|
+
|
|
629
|
+
As the engine runs, it automatically sets CSS variables and HTML attributes on the `<html>` element. You can use these in your CSS to style the page differently based on the current game state - for example, hiding the loader once assets are ready, or applying a different layout in landscape mode.
|
|
630
|
+
|
|
631
|
+
### HTML Attributes
|
|
632
|
+
|
|
633
|
+
The engine adds these attributes to the `<html>` element:
|
|
634
|
+
|
|
635
|
+
Attribute | Values | Description
|
|
636
|
+
--------------------------|------------------------------|--------------------------------------------------------------
|
|
637
|
+
`data-punter-loading` | `"true"` / removed | Present while assets are loading; removed when `ready` fires.
|
|
638
|
+
`data-punter-debug` | `"true"` / removed | Present when debug mode is on.
|
|
639
|
+
`data-punter-device` | `"mobile"` / `"desktop"` | Set based on user agent detection.
|
|
640
|
+
`data-punter-orientation` | `"portrait"` / `"landscape"` | Updated on every resize.
|
|
641
|
+
`data-punter-scene` | scene name string | The name of the current scene; updated on `punter.go()`.
|
|
642
|
+
|
|
643
|
+
**Example - show a loading screen:**
|
|
644
|
+
|
|
645
|
+
```css
|
|
646
|
+
/* show loader until engine is ready */
|
|
647
|
+
#loader { display: block; }
|
|
648
|
+
html[data-punter-loading] #loader { display: block; }
|
|
649
|
+
html:not([data-punter-loading]) #loader { display: none; }
|
|
650
|
+
|
|
651
|
+
/* different layout per orientation */
|
|
652
|
+
html[data-punter-orientation="landscape"] .controls { flex-direction: row; }
|
|
653
|
+
html[data-punter-orientation="portrait"] .controls { flex-direction: column; }
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### CSS Variables
|
|
657
|
+
|
|
658
|
+
These are set on the `<html>` element and updated whenever the window resizes:
|
|
659
|
+
|
|
660
|
+
Variable | Description
|
|
661
|
+
---------------|------------------------------------------
|
|
662
|
+
`--punter-vpw` | Viewport width in pixels (e.g. `375px`).
|
|
663
|
+
`--punter-vph` | Viewport height in pixels (e.g. `667px`).
|
|
664
|
+
|
|
665
|
+
```css
|
|
666
|
+
.hud {
|
|
667
|
+
width: var(--punter-vpw);
|
|
668
|
+
height: var(--punter-vph);
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
## Debug Mode
|
|
674
|
+
|
|
675
|
+
Turning on debug mode overlays useful information directly onto the game canvas - handy when you're trying to understand why a sprite is in the wrong place or why a collision isn't triggering.
|
|
676
|
+
|
|
677
|
+
Enable it in `punter.init()`:
|
|
678
|
+
|
|
679
|
+
```js
|
|
680
|
+
punter.init({ canvas: '#game', debug: true, sprites: { ... } });
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Or toggle it at any time:
|
|
684
|
+
|
|
685
|
+
```js
|
|
686
|
+
punter.debug = true;
|
|
687
|
+
punter.debug = false;
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
When debug mode is active, the engine draws:
|
|
691
|
+
|
|
692
|
+
- **FPS counter** and current canvas resolution in the bottom-right corner
|
|
693
|
+
- **Red bounding boxes** around each collidable sprite's collision area
|
|
694
|
+
- **Position and size labels** (`x=100 y=200 (32x32)`) near each sprite
|
|
695
|
+
|
|
696
|
+
### Customising the Debug Overlay
|
|
697
|
+
|
|
698
|
+
The overlay colours and font are pulled from CSS variables on `:root`. Add these to your stylesheet to customise them:
|
|
699
|
+
|
|
700
|
+
```css
|
|
701
|
+
:root {
|
|
702
|
+
--punter-debug-background: rgba(0, 0, 0, 0.6);
|
|
703
|
+
--punter-debug-text: #00ff00;
|
|
704
|
+
--punter-debug-font: 11px 'Courier New', monospace;
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
Variable | Default | Description
|
|
709
|
+
----------------------------|-------------------------|----------------------------------------
|
|
710
|
+
`--punter-debug-background` | `rgba(255,255,255,0.7)` | Background colour of debug label boxes.
|
|
711
|
+
`--punter-debug-text` | `red` | Text colour of debug labels.
|
|
712
|
+
`--punter-debug-font` | `12px monospace` | Font for debug labels.
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
## Browser Support
|
|
716
|
+
|
|
717
|
+
Works with all major browsers _(Chrome, Firefox, Safari, Edge)_. For older browsers, polyfills for `fetch` and `Promise` are sufficient.
|
|
718
|
+
|
|
719
|
+
punter.js requires:
|
|
720
|
+
|
|
721
|
+
- `fetch` (for loading sounds)
|
|
722
|
+
- `Promise`
|
|
723
|
+
- `requestAnimationFrame`
|
|
724
|
+
- `AudioContext` / `webkitAudioContext`
|
|
725
|
+
- `canvas` 2D context with `getImageData`
|
|
726
|
+
|
|
727
|
+
NOTE: Don't manually resize the canvas - the engine owns that via `resize()`.
|
|
728
|
+
|
|
729
|
+
## License
|
|
730
|
+
|
|
731
|
+
[MIT License](LICENSE). Built by the [Orca Scan Team](https://orcascan.com/about-us) to power [OrcaCam](https://orcascan.com/game).
|