Ben Smith @binjimint |
That's it :-)
body {
position: absolute;
display: flex;
flex-direction: column;
background-color: #fff;
margin: 0;
width: 100%;
height: 100%;
canvas {
object-fit: contain;
width: 100%;
height: 100%;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
The canvas element itself is 150x150 pixels,
const w = 150, h = 150;
(async function start() {
const response = await fetch('match3.wasm');
const moduleBytes = await response.arrayBuffer();
const {module, instance} =
await WebAssembly.instantiate(moduleBytes);
const exports = instance.exports;
const buffer = exports.mem.buffer;
const canvasData = new Uint8Array(buffer, 0x10000, w*h*4);
// ...
// ...
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
const imageData = context.createImageData(w, h);
(function update() {
requestAnimationFrame(update);
exports.run();
imageData.data.set(canvasData);
context.putImageData(imageData, 0, 0);
})();
})();
;; Memory map:
;;
;; [0x10000 .. 0x25f90) 150x150xRGBA data (4 bytes/pixel)
(memory (export "mem") 3)
(func (export "run")
)
(func $clear-screen (param $color i32)
(local $i i32)
(loop $loop
;; mem[0x10000 + i] = color
(i32.store offset=0x10000
(local.get $i) (local.get $color))
;; i += 4
(local.set $i
(i32.add (local.get $i) (i32.const 4)))
;; loop if i < 90000
(br_if $loop
(i32.lt_s (local.get $i) (i32.const 90000)))
)
)
(func (export "run")
(call $clear-screen
(i32.const 0xff_00_00_ff)) ;; ABGR format
)
Clear the screen to red
(func $put-pixel
(param $x i32) (param $y i32) (param $color i32)
;; mem[0x10000 + (y * 150 + x) * 4] = color
(i32.store offset=0x10000
(i32.mul
(i32.add
(i32.mul (local.get $y) (i32.const 150))
(local.get $x))
(i32.const 4))
(local.get $color))
)
(func (export "run")
(call $put-pixel
(i32.const 100) (i32.const 100)
(i32.const 0xff_00_00_ff))
)
Draw a red pixel at (100,100)
const input = new Uint8Array(exports.mem.buffer, 0x0000, 3);
function mouseEventHandler(event) {
// ...
input[0] = event.offsetX;
input[1] = event.offsetY;
input[2] = event.buttons;
}
canvas.addEventListener('mousemove', mouseEventHandler);
canvas.addEventListener('mousedown', mouseEventHandler);
canvas.addEventListener('mouseup', mouseEventHandler);
;; [0x0..0x0) X mouse position
;; [0x1..0x1) Y mouse position
;; [0x2..0x2) mouse buttons
(func (export "run")
// ...
(call $put-pixel
(i32.load8_u (i32.const 0)) ;; X
(i32.load8_u (i32.const 1)) ;; Y
(select
(i32.const 0xff_00_00_ff) ;; Red
(i32.const 0xff_ff_00_00) ;; Blue
(i32.load8_u (i32.const 2))) ;; Buttons
)
)
function fillRect(x, y, w, h, color) {
var i, j;
for (j = 0; j < h; j++) {
for (i = 0; i < w; i++) {
putPixel(x + i, y + j, color);
}
}
}
How you might write fillRect in JavaScript
(func $fill-rect (param $x i32) (param $y i32)
(param $w i32) (param $h i32)
(param $color i32)
(local $i i32) (local $j i32)
(loop $y
(local.set $i (i32.const 0))
(loop $x
(call $put-pixel
(i32.add (local.get $x) (local.get $i))
(i32.add (local.get $y) (local.get $j))
(local.get $color))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $x (i32.lt_s (local.get $i) (local.get $w))))
(local.set $j (i32.add (local.get $j) (i32.const 1)))
(br_if $y (i32.lt_s (local.get $j) (local.get $h)))))
(func $draw-sprite (param $x i32) (param $y i32)
(param $w i32) (param $h i32)
(param $src i32)
;; ...
)
Start with $fill-rect, but change the
$color parameter to $src
;; ...
;; put-pixel(x + i, y + j, mem[src + (w * j + i) * 4])
(call $put-pixel
(i32.add (local.get $x) (local.get $i))
(i32.add (local.get $y) (local.get $j))
(i32.load
(i32.add
(local.get $src)
(i32.mul
(i32.add
(i32.mul (local.get $w) (local.get $j))
(local.get $i))
(i32.const 4)))))
;; ...
;; Sprite Data 16x16x4 = 1024 bytes
(data (i32.const 0x100)
"\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"
"\00\00\00\00\00\00\00\00\df\71\26\ff\df\71\26\ff"
"\df\71\26\ff\df\71\26\ff\00\00\00\00\00\00\00\00"
"\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"
"\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"
"\df\71\26\ff\df\71\26\ff\fb\f2\36\ff\fb\f2\36\ff"
"\fb\f2\36\ff\fb\f2\36\ff\df\71\26\ff\df\71\26\ff"
"\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"
"\00\00\00\00\00\00\00\00\00\00\00\00\df\71\26\ff"
"\fb\f2\36\ff\fb\f2\36\ff\fb\f2\36\ff\fb\f2\36\ff"
"\fb\f2\36\ff\fb\f2\36\ff\fb\f2\36\ff\fb\f2\36\ff"
...
)
Store 1024 bytes of sprite data at 0x100
(func $put-pixel (param $x i32) (param $y i32)
(param $color i32)
;; return if the x/y coordinate is out of bounds
(br_if 0
(i32.or
(i32.ge_u (local.get $x) (i32.const 150))
(i32.ge_u (local.get $y) (i32.const 150))))
...
)
(func $draw-sprite ...
;; pixel = mem[src + (w * j + i) * 4]
(local.set $pixel (i32.load ...)))
;; if (pixel != 0)
(if (local.get $pixel)
(then
(call $put-pixel ...))
)
Load the pixel, but only draw it if it is non-zero.
β© Step 10: Draw Sprites With Palette
(func $draw-sprite (param $x i32) (param $y i32)
(param $src i32)
(param $sw i32) (param $sh i32)
(param $dw i32) (param $dh i32)
(local $dx f32)
(local $dy f32)
;; dx = sw / dw
(local.set $dx
(f32.div (f32.convert_i32_s (local.get $sw))
(f32.convert_i32_s (local.get $dw))))
;; dy = sh / dh
(local.set $dy
(f32.div (f32.convert_i32_s (local.get $sh))
(f32.convert_i32_s (local.get $dh))))
;; pixel = mem[src + (sw * j * dy + i * dx)]
(local.set $pixel
(i32.load
(i32.add
(local.get $src)
(i32.add
(i32.mul
(local.get $sw)
(i32.trunc_f32_s
(f32.mul (f32.convert_i32_s (local.get $j))
(local.get $dy))))
(i32.trunc_f32_s
(f32.mul (f32.convert_i32_s (local.get $i))
(local.get $dx)))))))
Use scale factors when reading source pixel
Using 1 byte per cell
Using 1 bit per cell instead
(func $draw-grid (param $grid i64) (param $gfx-src i32)
(local $i i32)
(loop $loop
;; Exit the function if $grid is zero
(br_if 1 (i64.eqz (local.get $grid)))
;; Get the index of the lowest set bit
(local.set $i (i32.wrap_i64 (i64.ctz (local.get $grid))))
;; Draw the cell at that index
(call $draw-cell ...)
;; Clear the lowest set bit: bits &= bits - 1
(local.set $grid
(i64.and (local.get $grid)
(i64.sub (local.get $grid) (i64.const 1))))
(br $loop)))
Linear interpolation
(func $ilerp (param $a i32) (param $b i32) (param $t f32)
(result i32)
;; return a + (b - a) * t
(i32.add
(local.get $a)
(i32.trunc_f32_s
(f32.mul
(f32.convert_i32_s
(i32.sub (local.get $b) (local.get $a)))
(local.get $t))))
)
Ease out cubic
(func $ease-out-cubic (param $t f32) (result f32)
;; return t * (3 + t * (t - 3))
(f32.mul
(local.get $t)
(f32.add
(f32.const 3)
(f32.mul
(local.get $t)
(f32.sub (local.get $t) (f32.const 3)))))
)
...
(call $ilerp (i32.const 10) (i32.const 30)
(call $ease-out-cubic (f32.const 0.5)))
;; struct Cell { s8 x, y, w, h; };
;; [0x3200..0x3300) current offset Cell[64]
;; [0x3300..0x3400) start offset Cell[64]
;; [0x3400..0x3500) end offset Cell[64]
;; [0x3500..0x3600) time [0..1) f32[64]
...
;; t = t[i]
(local.set $t (f32.load offset=0x3500 (local.get $t-addr)))
;; current[i] = ilerp(start[i], end[i], easeOutCubic(t))
(i32.store8 offset=0x3200
(local.get $i-addr)
(call $ilerp
(i32.load8_s offset=0x3300 (local.get $i-addr))
(i32.load8_s offset=0x3400 (local.get $i-addr))
(call $ease-out-cubic (local.get $t))))
β© Step 19: Dragging an Emoji
β© Step 20: Clamping to 4 Adjacent Cells
β© Step 21: Swap Animation
β© Step 24: Swap Cells After Drag
i64.and
to check if all 3 matchi64.or
to add it to the
result
(if (i32.and
(i32.wrap_i64
(i64.and (local.get $valid) (i64.const 1)))
(i64.eq
(i64.and (local.get $grid) (local.get $pattern))
(local.get $pattern)))
(then
(local.set $result
(i64.or (local.get $result) (local.get $pattern)))))
(local.set $pattern
(i64.shl (local.get $pattern) (i64.const 1)))
(local.set $valid
(i64.shr_u (local.get $valid) (i64.const 1)))
(i32 ;; ........ ........ pattern
;; ........ x.......
;; ........ x.......
;; xxx..... x.......
0x00000007 0x00010101)
(i64 ;; xxxxxx.. ........ valid mask
;; xxxxxx.. ........
;; xxxxxx.. xxxxxxxx
;; xxxxxx.. xxxxxxxx
;; xxxxxx.. xxxxxxxx
;; xxxxxx.. xxxxxxxx
;; xxxxxx.. xxxxxxxx
;; xxxxxx.. xxxxxxxx
0x3f3f3f3f3f3f3f3f 0x0000ffffffffffff)
β© Step 26: Swap Back If No Match
;; Get the index of the lowest set bit
(local.set $i (i64.ctz (local.get $empty)))
;; Find the next cell above that is not empty:
;; invert the empty pattern and mask it with a column,
;; shifted by i.
(local.set $above-bits
(i64.and
(i64.xor (local.get $empty) (i64.const -1))
(i64.shl (i64.const 0x0101010101010101) (local.get $i))))
;; Now find the lowest set bit
(local.set $above-idx (i64.ctz (local.get $above-bits)))
;; If there is a cell above this one...
(if (i64.ne (local.get $above-bits) (i64.const 0))
(then
;; Move the cell above down...
β© Step 28: Randomize Board at Start
β© Step 29: Check Matches After Dropping
β© Step 30: Animate Match Removal
(i32 ;; ..x..... .x...... x....... xx......
;; xx...... x.x..... .xx..... ..x.....
0x00000403 0x00000205 0x00000106 0x00000304
;; x.x..... .xx..... ........ ........
;; .x...... x....... xx.x.... x.xx....
0x00000502 0x00000601 0x0000000b 0x0000000d )
(i64 0x003f3f3f3f3f3f3f ;; ........ xxxxx...
0x003f3f3f3f3f3f3f ;; xxxxxx.. xxxxx...
0x003f3f3f3f3f3f3f ;; xxxxxx.. xxxxx...
0x003f3f3f3f3f3f3f ;; xxxxxx.. xxxxx...
0x003f3f3f3f3f3f3f ;; xxxxxx.. xxxxx...
0x003f3f3f3f3f3f3f ;; xxxxxx.. xxxxx...
0x1f1f1f1f1f1f1f1f ;; xxxxxx.. xxxxx...
0x1f1f1f1f1f1f1f1f ) ;; xxxxxx.. *6 xxxxx... *2
right?