A small 2D game framework you script in tigr. Open a window, draw a sprite, export to desktop or the browser.
purr runs your script once, calls init to set things up, then calls update and draw every frame. Those three functions are the whole interface.
// bounce.tg โ 100 bouncing balls in 25 lines
balls := [];
init := fn() {
balls = for[] (i,0..100) {
${
p: Vec.random(0, 0, Gfx.width(), Gfx.height()),
v: Vec.random(-1, -1, 1, 1),
r: Random.int(1, 10),
c: Color.random()
}
};
};
update := fn(dt) {
for (ball, balls) {
ball.p = Vec.add(ball.p, ball.v);
if ball.p.x < 0 || ball.p.x > Gfx.width() { ball.v.x *= -1 };
if ball.p.y < 0 || ball.p.y > Gfx.height() { ball.v.y *= -1 };
};
};
draw := fn() {
Gfx.clear(43, 34, 28);
Gfx.blend('add');
for (ball, balls) {
Gfx.color(ball.c);
Gfx.circle_fill(ball.p.x, ball.p.y, ball.r);
};
};
Why purr
Here are a couple of purr's cool features.
Save the file and the running game picks up the change, state intact. Drop the console down with the backtick key to read or tweak values live while it plays.
The same script exports to Windows, macOS and Linux, or to a single bundle that runs in the browser.
Press F8 for a PNG, F9 to record a GIF. Built in, so grabbing a clip for a devlog takes one key.
See it move
Every snippet below is a complete program. Click one to run it, right here on the page.
Clear the screen, pick a colour, print some text and draw an image. About as small as a program gets.
msg := 'hello, purr!';
img := null;
init := fn() {
img = Gfx.load_image('hello_world.png');
};
draw := fn() {
Gfx.clear(43, 34, 28);
Gfx.color(246, 201, 154);
msg_x := int((Gfx.width() - Gfx.text_width(msg)) / 2);
Gfx.print(msg, msg_x, 50);
img_x := int((Gfx.width() - Gfx.image_size(img).w) / 2);
Gfx.draw(img, img_x, 100);
};
Read bound actions and move a position by the frame's delta time. Rebind the keys later without touching this code.
cat := ${ x: 160, y: 90 };
update := fn(dt) {
speed := 120 * dt;
if Input.down('right') { cat.x = cat.x + speed };
if Input.down('left') { cat.x = cat.x - speed };
if Input.down('up') { cat.y = cat.y - speed };
if Input.down('down') { cat.y = cat.y + speed };
};
draw := fn() {
Gfx.clear(43, 34, 28);
Gfx.color(229, 130, 56);
Gfx.rect_fill(cat.x - 6, cat.y - 6, 12, 12);
};
Each click plants a patch. A green thread tweens every blossom up with an easing curve, waits a beat, then pops it back into the soil.
TAU := Math.PI * 2;
flowers := [];
plant := fn(cx, cy, stagger) {
f := ${
x: cx,
y: cy,
size: 6 + rand() * 11,
petals: Random.int(5, 8),
rot: rand() * TAU,
scale: 0,
petal: Color.hsv(rand() * 360, 0.5 + rand() * 0.4, 0.95),
eye: Color.hsv(48 + rand() * 12, 0.85, 1.0),
dead: false,
};
Array.push(flowers, f);
go fn() {
wait(stagger);
Tween.to(f, 'scale', 1, 0.55, 'out_back');
wait(0.9 + rand() * 0.5);
Tween.to(f, 'scale', 0, 0.30, 'in_back');
f.dead = true;
};
};
plant_patch := fn(cx, cy) {
for (i, 0..Random.int(5, 9)) {
off := Vec.random(-60, -60, 60, 60);
plant(cx + off.x, cy + off.y, i * 0.04)
};
};
draw_flower := fn(f) {
Gfx.push();
Gfx.translate(f.x, f.y);
Gfx.rotate(f.rot);
Gfx.scale(f.scale, f.scale);
Gfx.color(f.petal);
for (i, 0..f.petals) {
a := (i / f.petals) * TAU;
Gfx.circle_fill(Math.cos(a) * f.size, Math.sin(a) * f.size, f.size * 0.62);
};
Gfx.color(f.eye);
Gfx.circle_fill(0, 0, f.size * 0.55);
Gfx.pop();
};
update := fn() {
if Input.mouse_pressed('left') {
plant_patch(Input.mouse_x(), Input.mouse_y());
};
flowers = Array.filter(flowers, fn(f) { !f.dead });
};
draw := fn() {
Gfx.clear(43, 34, 28);
for (f, flowers) {
draw_flower(f);
};
Gfx.color(120, 140, 118);
Gfx.print('click to plant a patch', 12, 12);
};
The toolkit
Each is a flat namespace of functions, no classes to wire up and nothing to import. Pick one to jump into its reference.