Skip to content

Instantly share code, notes, and snippets.

@rsms
Created April 24, 2026 16:42
Show Gist options
  • Select an option

  • Save rsms/331e72a7421799e89aa1844772615db0 to your computer and use it in GitHub Desktop.

Select an option

Save rsms/331e72a7421799e89aa1844772615db0 to your computer and use it in GitHub Desktop.
Run it with: pb run pixel-studio.c
#include <playbit/playbit.h>
#define CANVAS_MAX 64
#define PALETTE_COUNT 16
typedef enum {
Tool_Brush,
Tool_Rectangle,
Tool_Circle,
} Tool;
typedef struct {
int width;
int height;
u32 pixels[CANVAS_MAX * CANVAS_MAX];
} Canvas;
typedef struct {
Canvas canvas;
u32 brushColor;
int brushSize;
Tool tool;
bool eraseMode;
bool gridVisible;
bool pointerDown;
bool pointerErasing;
PBVector2 lastPoint;
int dragStartX;
int dragStartY;
int dragEndX;
int dragEndY;
PBRectangle canvasRect;
f32 displayScale;
f32 cellSize;
int cellPixels;
int canvasXpx;
int canvasYpx;
int canvasSizePx;
int hoverX;
int hoverY;
} State;
static State g = { 0 };
static const u32 kPalette[PALETTE_COUNT] = {
0x1a1c2cff, 0x5d275dff, 0xb13e53ff, 0xef7d57ff,
0xffcd75ff, 0xa7f070ff, 0x38b764ff, 0x257179ff,
0x29366fff, 0x3b5dc9ff, 0x41a6f6ff, 0x73eff7ff,
0xf4f4f4ff, 0x94b0c2ff, 0x566c86ff, 0x333c57ff,
};
static PBColor color(u32 rgba) {
return PBColorFromRGBAHex(rgba);
}
static bool rgba_is_visible(u32 rgba) {
return (rgba & 0xff) != 0;
}
static bool point_in_rect(PBVector2 p, PBRectangle r) {
return p.x >= r.x0 && p.x < r.x1 && p.y >= r.y0 && p.y < r.y1;
}
static int min_int(int a, int b) {
return a < b ? a : b;
}
static int max_int(int a, int b) {
return a > b ? a : b;
}
static int abs_int(int x) {
return x < 0 ? -x : x;
}
static const char* tool_name(void) {
switch (g.tool) {
case Tool_Rectangle:
return "rect";
case Tool_Circle:
return "circle";
case Tool_Brush:
default:
return "brush";
}
}
static PBRectangle rect(f32 x, f32 y, f32 w, f32 h) {
return PBRectangleMake(x, y, x + w, y + h);
}
static int round_to_px(f32 x) {
return (int)PBRoundF32(x * g.displayScale);
}
static f32 px_to_dp(int x) {
return (f32)x / g.displayScale;
}
static PBRectangle rect_px(int x, int y, int w, int h) {
return PBRectangleMake(px_to_dp(x), px_to_dp(y), px_to_dp(x + w), px_to_dp(y + h));
}
static void draw_outline_px(int x, int y, int w, int h, int thickness, PBColor c) {
PBDrawRect(rect_px(x, y, w, thickness), c);
PBDrawRect(rect_px(x, y + h - thickness, w, thickness), c);
PBDrawRect(rect_px(x, y, thickness, h), c);
PBDrawRect(rect_px(x + w - thickness, y, thickness, h), c);
}
static void fit_canvas(PBVector2 windowSize) {
PBWindow window = PBWindowMain();
g.displayScale = PBWindowGetScale(window);
if (g.displayScale <= 0) {
g.displayScale = 1;
}
int windowWpx = round_to_px(windowSize.x);
int windowHpx = round_to_px(windowSize.y);
int leftPx = round_to_px(232);
int topPx = round_to_px(24);
int rightPx = round_to_px(24);
int bottomPx = round_to_px(48);
int maxWpx = windowWpx - leftPx - rightPx;
int maxHpx = windowHpx - topPx - bottomPx;
int maxSizePx = min_int(maxWpx, maxHpx);
g.cellPixels = maxSizePx / g.canvas.width;
if (g.cellPixels < 1) {
g.cellPixels = 1;
}
g.canvasSizePx = g.cellPixels * g.canvas.width;
g.canvasXpx = leftPx + max_int(0, (maxWpx - g.canvasSizePx) / 2);
g.canvasYpx = topPx + max_int(0, (maxHpx - g.canvasSizePx) / 2);
g.cellSize = px_to_dp(g.cellPixels);
g.canvasRect = rect_px(g.canvasXpx, g.canvasYpx, g.canvasSizePx, g.canvasSizePx);
}
static void update_layout(void) {
PBVector2 windowSize = PBWindowGetSize(PBWindowMain());
fit_canvas(windowSize);
}
static void reset_canvas(int size) {
if (size < 8) {
size = 8;
}
if (size > CANVAS_MAX) {
size = CANVAS_MAX;
}
g.canvas.width = size;
g.canvas.height = size;
for (int i = 0; i < CANVAS_MAX * CANVAS_MAX; i += 1) {
g.canvas.pixels[i] = 0x00000000;
}
update_layout();
}
static int pixel_index(int x, int y) {
return y * g.canvas.width + x;
}
static PBRectangle cell_rect(int x, int y) {
return rect_px(
g.canvasXpx + x * g.cellPixels,
g.canvasYpx + y * g.cellPixels,
g.cellPixels,
g.cellPixels);
}
static void paint_pixel(int x, int y, bool erase) {
int half = g.brushSize / 2;
for (int yy = y - half; yy <= y + half; yy += 1) {
for (int xx = x - half; xx <= x + half; xx += 1) {
if (xx >= 0 && xx < g.canvas.width && yy >= 0 && yy < g.canvas.height) {
g.canvas.pixels[pixel_index(xx, yy)] = erase ? 0x00000000 : g.brushColor;
}
}
}
}
static void draw_preview_pixel(int x, int y, bool erase) {
u32 previewColor = erase ? 0x111111aa : ((g.brushColor & 0xffffff00) | 0xaa);
int half = g.brushSize / 2;
for (int yy = y - half; yy <= y + half; yy += 1) {
for (int xx = x - half; xx <= x + half; xx += 1) {
if (xx >= 0 && xx < g.canvas.width && yy >= 0 && yy < g.canvas.height) {
PBDrawRect(cell_rect(xx, yy), color(previewColor));
}
}
}
}
static void plot_pixel(int x, int y, bool erase, bool preview) {
if (preview) {
draw_preview_pixel(x, y, erase);
} else {
paint_pixel(x, y, erase);
}
}
static bool point_to_pixel(PBVector2 point, int* outX, int* outY) {
if (!point_in_rect(point, g.canvasRect)) {
return false;
}
int x = ((int)(point.x * g.displayScale) - g.canvasXpx) / g.cellPixels;
int y = ((int)(point.y * g.displayScale) - g.canvasYpx) / g.cellPixels;
if (x < 0 || x >= g.canvas.width || y < 0 || y >= g.canvas.height) {
return false;
}
*outX = x;
*outY = y;
return true;
}
static void paint_line_pixels(int x0, int y0, int x1, int y1, bool erase, bool preview) {
int dx = (int)PBAbsF32((f32)(x1 - x0));
int dy = -(int)PBAbsF32((f32)(y1 - y0));
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx + dy;
for (;;) {
plot_pixel(x0, y0, erase, preview);
if (x0 == x1 && y0 == y1) {
break;
}
int e2 = 2 * err;
if (e2 >= dy) {
err += dy;
x0 += sx;
}
if (e2 <= dx) {
err += dx;
y0 += sy;
}
}
}
static void paint_line(PBVector2 from, PBVector2 to, bool erase) {
int x0 = 0;
int y0 = 0;
int x1 = 0;
int y1 = 0;
if (!point_to_pixel(from, &x0, &y0)) {
if (!point_to_pixel(to, &x0, &y0)) {
return;
}
}
if (!point_to_pixel(to, &x1, &y1)) {
return;
}
paint_line_pixels(x0, y0, x1, y1, erase, false);
}
static void paint_rect_shape(int x0, int y0, int x1, int y1, bool erase, bool preview) {
int left = min_int(x0, x1);
int right = max_int(x0, x1);
int top = min_int(y0, y1);
int bottom = max_int(y0, y1);
paint_line_pixels(left, top, right, top, erase, preview);
paint_line_pixels(right, top, right, bottom, erase, preview);
paint_line_pixels(right, bottom, left, bottom, erase, preview);
paint_line_pixels(left, bottom, left, top, erase, preview);
}
static void plot_circle_points(int cx, int cy, int x, int y, bool erase, bool preview) {
plot_pixel(cx + x, cy + y, erase, preview);
plot_pixel(cx + y, cy + x, erase, preview);
plot_pixel(cx - y, cy + x, erase, preview);
plot_pixel(cx - x, cy + y, erase, preview);
plot_pixel(cx - x, cy - y, erase, preview);
plot_pixel(cx - y, cy - x, erase, preview);
plot_pixel(cx + y, cy - x, erase, preview);
plot_pixel(cx + x, cy - y, erase, preview);
}
static void paint_circle_shape(int x0, int y0, int x1, int y1, bool erase, bool preview) {
int radius = max_int(abs_int(x1 - x0), abs_int(y1 - y0));
int x = radius;
int y = 0;
int err = 0;
if (radius == 0) {
plot_pixel(x0, y0, erase, preview);
return;
}
while (x >= y) {
plot_circle_points(x0, y0, x, y, erase, preview);
y += 1;
if (err <= 0) {
err += 2 * y + 1;
}
if (err > 0) {
x -= 1;
err -= 2 * x + 1;
}
}
}
static void paint_shape(bool preview) {
bool erase = g.eraseMode || g.pointerErasing;
if (g.tool == Tool_Rectangle) {
paint_rect_shape(g.dragStartX, g.dragStartY, g.dragEndX, g.dragEndY, erase, preview);
} else if (g.tool == Tool_Circle) {
paint_circle_shape(g.dragStartX, g.dragStartY, g.dragEndX, g.dragEndY, erase, preview);
}
}
static PBRectangle tool_button_rect(int col) {
return rect(24 + (f32)col * 60, 248, 56, 32);
}
static PBRectangle panel_button_rect(int row) {
return rect(24, 290 + (f32)row * 42, 176, 32);
}
static PBRectangle palette_rect(int index) {
int col = index % 4;
int row = index / 4;
return rect(24 + (f32)col * 42, 74 + (f32)row * 42, 32, 32);
}
static bool button(PBRectangle bounds, PBStrSlice label, bool active) {
PBColor fill = active ? color(0x2d6cdfef) : color(0x242936ff);
PBColor stroke = active ? color(0x79a8ffff) : color(0x3d4355ff);
PBDrawRect(bounds, fill);
PBDrawRectInset(bounds, stroke, 1, PBV4(0, 0, 0, 0));
PBDrawText(
PBUIFontMake(PB_STR("Inter"), 13, 600),
label,
color(0xf2f4f8ff),
PBRectangleMake(bounds.x0 + 10, bounds.y0 + 8, bounds.x1 - 8, bounds.y1 - 4));
return false;
}
static void draw_panel(void) {
PBVector2 windowSize = PBWindowGetSize(PBWindowMain());
PBDrawRect(rect(0, 0, windowSize.x, windowSize.y), color(0x151820ff));
PBDrawRect(rect(0, 0, 208, windowSize.y), color(0x1d222dff));
PBDrawText(
PBUIFontMake(PB_STR("Inter"), 20, 700),
PB_STR("Pixel Studio"),
color(0xf6f7fbff),
rect(24, 22, 170, 28));
for (int i = 0; i < PALETTE_COUNT; i += 1) {
PBRectangle r = palette_rect(i);
PBDrawRect(r, color(kPalette[i]));
if (!g.eraseMode && g.brushColor == kPalette[i]) {
PBDrawRectInset(PBRectangleMake(r.x0 - 3, r.y0 - 3, r.x1 + 3, r.y1 + 3), color(0xffffffff), 2, PBV4(0, 0, 0, 0));
} else {
PBDrawRectInset(r, color(0x00000080), 1, PBV4(0, 0, 0, 0));
}
}
button(tool_button_rect(0), PB_STR("Brush"), g.tool == Tool_Brush);
button(tool_button_rect(1), PB_STR("Rect"), g.tool == Tool_Rectangle);
button(tool_button_rect(2), PB_STR("Circle"), g.tool == Tool_Circle);
button(panel_button_rect(0), g.eraseMode ? PB_STR("Eraser") : PB_STR("Paint"), g.eraseMode);
button(panel_button_rect(1), PB_STR("Clear"), false);
PBRectangle brush1 = rect(24, 382, 52, 32);
PBRectangle brush2 = rect(86, 382, 52, 32);
PBRectangle brush3 = rect(148, 382, 52, 32);
button(brush1, PB_STR("1"), g.brushSize == 1);
button(brush2, PB_STR("3"), g.brushSize == 3);
button(brush3, PB_STR("5"), g.brushSize == 5);
PBRectangle size16 = rect(24, 436, 52, 32);
PBRectangle size32 = rect(86, 436, 52, 32);
PBRectangle size64 = rect(148, 436, 52, 32);
button(size16, PB_STR("16"), g.canvas.width == 16);
button(size32, PB_STR("32"), g.canvas.width == 32);
button(size64, PB_STR("64"), g.canvas.width == 64);
button(rect(24, 490, 176, 32), g.gridVisible ? PB_STR("Grid on") : PB_STR("Grid off"), g.gridVisible);
PBDrawText(
PBUIFontMake(PB_STR("Inter"), 13, 500),
UIfmt("%s %dx%d b%d", tool_name(), g.canvas.width, g.canvas.height, g.brushSize),
color(0xaeb6c4ff),
rect(24, windowSize.y - 34, 176, 22));
}
static void draw_canvas(void) {
int padPx = round_to_px(12);
int minBgXpx = round_to_px(208);
int bgXpx = max_int(minBgXpx, g.canvasXpx - padPx);
int bgYpx = max_int(0, g.canvasYpx - padPx);
PBDrawRect(
rect_px(
bgXpx,
bgYpx,
g.canvasSizePx + padPx * 2 - (bgXpx - (g.canvasXpx - padPx)),
g.canvasSizePx + padPx * 2 - (bgYpx - (g.canvasYpx - padPx))),
color(0x0f1218ff));
for (int y = 0; y < g.canvas.height; y += 1) {
for (int x = 0; x < g.canvas.width; x += 1) {
PBRectangle cell = cell_rect(x, y);
u32 checker = ((x + y) & 1) ? 0xd8dde5ff : 0xf3f5f8ff;
PBDrawRect(cell, color(checker));
u32 pixel = g.canvas.pixels[pixel_index(x, y)];
if (rgba_is_visible(pixel)) {
PBDrawRect(cell, color(pixel));
}
}
}
if (g.gridVisible && g.cellPixels >= 8) {
PBColor gridColor = color(0x00000030);
for (int x = 1; x < g.canvas.width; x += 1) {
int lineX = g.canvasXpx + x * g.cellPixels;
PBDrawRect(rect_px(lineX, g.canvasYpx, 1, g.canvasSizePx), gridColor);
}
for (int y = 1; y < g.canvas.height; y += 1) {
int lineY = g.canvasYpx + y * g.cellPixels;
PBDrawRect(rect_px(g.canvasXpx, lineY, g.canvasSizePx, 1), gridColor);
}
}
if (g.pointerDown && g.tool != Tool_Brush) {
paint_shape(true);
}
draw_outline_px(g.canvasXpx, g.canvasYpx, g.canvasSizePx, g.canvasSizePx, 2, color(0xffffffff));
if (g.hoverX >= 0 && g.hoverY >= 0 && g.cellPixels >= 5) {
int half = g.brushSize / 2;
int x0 = g.hoverX - half;
int y0 = g.hoverY - half;
int x1 = g.hoverX + half + 1;
int y1 = g.hoverY + half + 1;
if (x0 < 0) {
x0 = 0;
}
if (y0 < 0) {
y0 = 0;
}
if (x1 > g.canvas.width) {
x1 = g.canvas.width;
}
if (y1 > g.canvas.height) {
y1 = g.canvas.height;
}
draw_outline_px(
g.canvasXpx + x0 * g.cellPixels,
g.canvasYpx + y0 * g.cellPixels,
(x1 - x0) * g.cellPixels,
(y1 - y0) * g.cellPixels,
2,
(g.eraseMode || g.pointerErasing) ? color(0x111111dd) : color(0xffffffff));
}
}
static void handle_panel_click(PBVector2 p) {
for (int i = 0; i < PALETTE_COUNT; i += 1) {
if (point_in_rect(p, palette_rect(i))) {
g.brushColor = kPalette[i];
g.eraseMode = false;
return;
}
}
if (point_in_rect(p, tool_button_rect(0))) {
g.tool = Tool_Brush;
} else if (point_in_rect(p, tool_button_rect(1))) {
g.tool = Tool_Rectangle;
} else if (point_in_rect(p, tool_button_rect(2))) {
g.tool = Tool_Circle;
} else if (point_in_rect(p, panel_button_rect(0))) {
g.eraseMode = !g.eraseMode;
} else if (point_in_rect(p, panel_button_rect(1))) {
for (int i = 0; i < CANVAS_MAX * CANVAS_MAX; i += 1) {
g.canvas.pixels[i] = 0x00000000;
}
} else if (point_in_rect(p, rect(24, 382, 52, 32))) {
g.brushSize = 1;
} else if (point_in_rect(p, rect(86, 382, 52, 32))) {
g.brushSize = 3;
} else if (point_in_rect(p, rect(148, 382, 52, 32))) {
g.brushSize = 5;
} else if (point_in_rect(p, rect(24, 436, 52, 32))) {
reset_canvas(16);
} else if (point_in_rect(p, rect(86, 436, 52, 32))) {
reset_canvas(32);
} else if (point_in_rect(p, rect(148, 436, 52, 32))) {
reset_canvas(64);
} else if (point_in_rect(p, rect(24, 490, 176, 32))) {
g.gridVisible = !g.gridVisible;
}
}
static bool pointer_event_erases(PBPointerEvent* event) {
return event->button == PBMouseButton_Right || (event->buttons & (1u << PBMouseButton_Right));
}
static void on_pointer(PBPointerEvent* event, PBEventType type) {
PBVector2 point = PBV2(event->x, event->y);
int px = -1;
int py = -1;
if (point_to_pixel(point, &px, &py)) {
g.hoverX = px;
g.hoverY = py;
} else {
g.hoverX = -1;
g.hoverY = -1;
}
if (type == PBEventType_POINTER_DOWN) {
if (point_in_rect(point, g.canvasRect)) {
g.pointerDown = true;
g.pointerErasing = pointer_event_erases(event);
g.lastPoint = point;
point_to_pixel(point, &g.dragStartX, &g.dragStartY);
g.dragEndX = g.dragStartX;
g.dragEndY = g.dragStartY;
if (g.tool == Tool_Brush) {
paint_line(point, point, g.eraseMode || g.pointerErasing);
}
} else {
g.pointerDown = false;
g.pointerErasing = false;
handle_panel_click(point);
}
} else if (type == PBEventType_POINTER_UP) {
if (g.pointerDown && g.tool != Tool_Brush && point_to_pixel(point, &px, &py)) {
g.dragEndX = px;
g.dragEndY = py;
paint_shape(false);
}
g.pointerDown = false;
g.pointerErasing = false;
} else if (type == PBEventType_POINTER_MOVE && g.pointerDown) {
if (pointer_event_erases(event)) {
g.pointerErasing = true;
}
if (g.tool == Tool_Brush) {
paint_line(g.lastPoint, point, g.eraseMode || g.pointerErasing);
} else if (px >= 0 && py >= 0) {
g.dragEndX = px;
g.dragEndY = py;
}
g.lastPoint = point;
}
}
static void on_event(const PBEvent* ev) {
if (ev->type == PBEventType_POINTER_DOWN || ev->type == PBEventType_POINTER_UP
|| ev->type == PBEventType_POINTER_MOVE)
{
on_pointer((PBPointerEvent*)ev, ev->type);
} else if (ev->type == PBEventType_KEY_DOWN) {
PBKeyboardEvent* event = (PBKeyboardEvent*)ev;
if (event->keyCode == PBKeyboardKey_E) {
g.eraseMode = !g.eraseMode;
} else if (event->keyCode == PBKeyboardKey_G) {
g.gridVisible = !g.gridVisible;
} else if (event->keyCode == PBKeyboardKey_B) {
g.tool = Tool_Brush;
} else if (event->keyCode == PBKeyboardKey_R) {
g.tool = Tool_Rectangle;
} else if (event->keyCode == PBKeyboardKey_C) {
g.tool = Tool_Circle;
} else if (event->keyCode == PBKeyboardKey_1) {
g.brushSize = 1;
} else if (event->keyCode == PBKeyboardKey_2) {
g.brushSize = 3;
} else if (event->keyCode == PBKeyboardKey_3) {
g.brushSize = 5;
}
} else if (ev->type == PBEventType_SIGNAL) {
PBSignalEvent* event = (PBSignalEvent*)ev;
if (event->signals & PBSysWindowSignal_RESIZE) {
update_layout();
}
}
}
static void render(void) {
update_layout();
draw_panel();
draw_canvas();
}
void main(void) {
PBWindow window = PBWindowMain();
PBWindowSetTitle(window, PB_STR("Pixel Studio"));
g.brushColor = kPalette[0];
g.brushSize = 1;
g.tool = Tool_Brush;
g.gridVisible = true;
g.hoverX = -1;
g.hoverY = -1;
reset_canvas(32);
PBApp.onEvent = on_event;
PBApp.onRender = render;
PBAppMain();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment