Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions apps_source_code/paint/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Changelog

## 1.4

- Fix the pen getting stuck down after pressing Ok together with a direction key: drawing mode was toggled by a long Ok press with no visual indication, and a short Ok press could not lift it
- Replace the hidden drawing flag with three modes cycled by a long Ok press: Move, Draw and Erase; the cursor shape shows the active mode (small dot, full cell, hollow frame)
- A short Ok press toggles the cell under the cursor in Move mode and lifts the pen or eraser in Draw and Erase modes
- Hold a direction key to keep moving (with the pen down this draws or erases a line); previously held keys did nothing
- Save the canvas on exit and restore it on the next start; a failed save shows a storage error message instead of silently losing the drawing
- Hold Back to clear the canvas and lift the pen
- Start with the cursor in the center of the canvas instead of the top-left corner

## 1.3

- Version bump for catalog compatibility (no functional changes)

## 1.2

- Version bump for catalog compatibility (no functional changes)

## 1.1

- Version bump for catalog compatibility (no functional changes)

## 1.0

- Initial release: draw on a 32x16 board with a dot cursor, hold Ok to toggle continuous drawing, hold Back to clear the screen
4 changes: 2 additions & 2 deletions apps_source_code/paint/application.fam
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ App(
fap_category="Games",
fap_author="@n-o-T-I-n-s-a-n-e",
fap_weburl="https://github.com/n-o-T-I-n-s-a-n-e",
fap_version="1.3",
fap_description="A basic Paint app, Click Ok to draw dot, hold Ok to enable drawing continuously, hold Back to clear the screen",
fap_version="1.4",
fap_description="A basic Paint app. Ok: toggle dot or lift the pen, hold Ok: cycle Move/Draw/Erase mode, hold Back: clear, Back: save and exit. The canvas is restored on the next start",
)
247 changes: 168 additions & 79 deletions apps_source_code/paint/paint.c
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
#include <furi.h>
#include <furi_hal.h>
#include <dialogs/dialogs.h>
#include <gui/gui.h>
#include <input/input.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <stdbool.h> // Header-file for boolean data-type.
#include <storage/storage.h>
#include <toolbox/saved_struct.h>
#include <string.h>

#define BOARD_WIDTH 32
#define BOARD_HEIGHT 16
#define CELL_SIZE 4

#define PAINT_SAVE_PATH APP_DATA_PATH("canvas.bin")
#define PAINT_SAVE_TMP_PATH APP_DATA_PATH("canvas.bin.tmp")

//saved_struct tags the file with this magic + version and a checksum, so a
//foreign, truncated or corrupt save is rejected on load
#define PAINT_SAVE_MAGIC 0x50
#define PAINT_SAVE_VERSION 1

typedef enum {
PaintModeMove, //cursor moves without touching the board
PaintModeDraw, //cursor paints every cell it visits
PaintModeErase, //cursor clears every cell it visits
PaintModeCount,
} PaintMode;

typedef struct selected_position {
int x;
Expand All @@ -14,8 +33,8 @@ typedef struct selected_position {
typedef struct {
FuriMutex* mutex;
selected_position selected;
bool board[32][16];
bool isDrawing;
bool board[BOARD_WIDTH][BOARD_HEIGHT];
PaintMode mode;
} PaintData;

void paint_draw_callback(Canvas* canvas, void* ctx) {
Expand All @@ -25,23 +44,34 @@ void paint_draw_callback(Canvas* canvas, void* ctx) {

canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);
//draw the canvas(32x16) on screen(144x64) using 4x4 tiles
for(int y = 0; y < 16; y++) {
for(int x = 0; x < 32; x++) {
//draw the board(32x16) on screen(128x64) using 4x4 tiles
for(int y = 0; y < BOARD_HEIGHT; y++) {
for(int x = 0; x < BOARD_WIDTH; x++) {
if(paint_state->board[x][y]) {
canvas_draw_box(canvas, x * 4, y * 4, 4, 4);
canvas_draw_box(canvas, x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
}
}
}

//draw cursor as a 4x4 black box with a 2x2 white box inside
canvas_set_color(canvas, ColorBlack);
canvas_draw_box(canvas, paint_state->selected.x * 4, paint_state->selected.y * 4, 4, 4);
canvas_set_color(canvas, ColorWhite);
canvas_draw_box(
canvas, paint_state->selected.x * 4 + 1, paint_state->selected.y * 4 + 1, 2, 2);
//the cursor is drawn in XOR so it stays visible on both painted and
//empty cells; its shape shows the active mode
int cursor_x = paint_state->selected.x * CELL_SIZE;
int cursor_y = paint_state->selected.y * CELL_SIZE;
canvas_set_color(canvas, ColorXOR);
switch(paint_state->mode) {
case PaintModeMove: //small dot: the pen is up
canvas_draw_box(canvas, cursor_x + 1, cursor_y + 1, 2, 2);
break;
case PaintModeDraw: //full cell: the pen is down
canvas_draw_box(canvas, cursor_x, cursor_y, CELL_SIZE, CELL_SIZE);
break;
case PaintModeErase: //hollow frame: the eraser is down
canvas_draw_frame(canvas, cursor_x, cursor_y, CELL_SIZE, CELL_SIZE);
break;
default:
break;
}

//release the mutex
furi_mutex_release(paint_state->mutex);
}

Expand All @@ -51,16 +81,82 @@ void paint_input_callback(InputEvent* input_event, void* ctx) {
furi_message_queue_put(event_queue, input_event, FuriWaitForever);
}

//paint or erase the cell under the cursor according to the active mode
static void paint_apply_mode(PaintData* paint_state) {
if(paint_state->mode == PaintModeDraw) {
paint_state->board[paint_state->selected.x][paint_state->selected.y] = true;
} else if(paint_state->mode == PaintModeErase) {
paint_state->board[paint_state->selected.x][paint_state->selected.y] = false;
}
}

static void paint_move_cursor(PaintData* paint_state, int dx, int dy) {
//apply the mode to the cell being left as well, so a stroke
//includes the cell it started on
paint_apply_mode(paint_state);
paint_state->selected.x = CLAMP(paint_state->selected.x + dx, BOARD_WIDTH - 1, 0);
paint_state->selected.y = CLAMP(paint_state->selected.y + dy, BOARD_HEIGHT - 1, 0);
paint_apply_mode(paint_state);
}

//react to the initial press and keep moving while the key is held;
//InputTypeLong is included so movement does not stall between the
//press and the first repeat
static bool paint_is_move_event(InputType type) {
return type == InputTypePress || type == InputTypeLong || type == InputTypeRepeat;
}

static void paint_load(PaintData* paint_state) {
//a missing or corrupt save just leaves the initialized empty canvas
if(!saved_struct_load(
PAINT_SAVE_PATH,
paint_state->board,
sizeof(paint_state->board),
PAINT_SAVE_MAGIC,
PAINT_SAVE_VERSION)) {
memset(paint_state->board, 0, sizeof(paint_state->board));
}
}

static bool paint_save(const PaintData* paint_state) {
//write to a temporary file first, then rename it over the real save, so a
//failed write cannot destroy the previously saved canvas
if(!saved_struct_save(
PAINT_SAVE_TMP_PATH,
paint_state->board,
sizeof(paint_state->board),
PAINT_SAVE_MAGIC,
PAINT_SAVE_VERSION)) {
FURI_LOG_E("paint", "cannot write the canvas");
return false;
}

Storage* storage = furi_record_open(RECORD_STORAGE);
FS_Error error = storage_common_rename(storage, PAINT_SAVE_TMP_PATH, PAINT_SAVE_PATH);
furi_record_close(RECORD_STORAGE);
if(error != FSE_OK) {
FURI_LOG_E("paint", "cannot save the canvas: %s", storage_error_get_desc(error));
return false;
}
return true;
}

int32_t paint_app(void* p) {
UNUSED(p);
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));

PaintData* paint_state = malloc(sizeof(PaintData));
memset(paint_state->board, 0, sizeof(paint_state->board));
paint_state->selected.x = BOARD_WIDTH / 2;
paint_state->selected.y = BOARD_HEIGHT / 2;
paint_state->mode = PaintModeMove;
paint_load(paint_state);

paint_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
if(!paint_state->mutex) {
FURI_LOG_E("paint", "cannot create mutex\r\n");
free(paint_state);
furi_message_queue_free(event_queue);
return -1;
}

Expand All @@ -73,80 +169,73 @@ int32_t paint_app(void* p) {
Gui* gui = furi_record_open(RECORD_GUI);
gui_add_view_port(gui, view_port, GuiLayerFullscreen);

//NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);

InputEvent event;
bool running = true;

while(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk) {
//break out of the loop if the back key is pressed
if(event.type == InputTypeShort && event.key == InputKeyBack) {
break;
}
while(running &&
furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk) {
furi_mutex_acquire(paint_state->mutex, FuriWaitForever);

//check the key pressed and change x and y accordingly
if(event.type == InputTypeShort) {
switch(event.key) {
case InputKeyUp:
paint_state->selected.y -= 1;
break;
case InputKeyDown:
paint_state->selected.y += 1;
break;
case InputKeyLeft:
paint_state->selected.x -= 1;
break;
case InputKeyRight:
paint_state->selected.x += 1;
break;
case InputKeyOk:
paint_state->board[paint_state->selected.x][paint_state->selected.y] =
!paint_state->board[paint_state->selected.x][paint_state->selected.y];
break;

default:
break;
}

//check if cursor position is out of bounds and reset it to the closest position
if(paint_state->selected.x < 0) {
paint_state->selected.x = 0;
}
if(paint_state->selected.x > 31) {
paint_state->selected.x = 31;
}
if(paint_state->selected.y < 0) {
paint_state->selected.y = 0;
}
if(paint_state->selected.y > 15) {
paint_state->selected.y = 15;
}
if(paint_state->isDrawing == true) {
paint_state->board[paint_state->selected.x][paint_state->selected.y] = true;
}
view_port_update(view_port);
}
if(event.key == InputKeyBack && event.type == InputTypeLong) {
paint_state->board[1][1] = true;
for(int y = 0; y < 16; y++) {
for(int x = 0; x < 32; x++) {
paint_state->board[x][y] = false;
switch(event.key) {
case InputKeyUp:
if(paint_is_move_event(event.type)) paint_move_cursor(paint_state, 0, -1);
break;
case InputKeyDown:
if(paint_is_move_event(event.type)) paint_move_cursor(paint_state, 0, 1);
break;
case InputKeyLeft:
if(paint_is_move_event(event.type)) paint_move_cursor(paint_state, -1, 0);
break;
case InputKeyRight:
if(paint_is_move_event(event.type)) paint_move_cursor(paint_state, 1, 0);
break;
case InputKeyOk:
if(event.type == InputTypeShort) {
if(paint_state->mode == PaintModeMove) {
//toggle the cell under the cursor
paint_state->board[paint_state->selected.x][paint_state->selected.y] =
!paint_state->board[paint_state->selected.x][paint_state->selected.y];
} else {
//a short press lifts the pen or eraser
paint_state->mode = PaintModeMove;
}
} else if(event.type == InputTypeLong) {
paint_state->mode = (PaintMode)((paint_state->mode + 1) % PaintModeCount);
}
view_port_update(view_port);
}
if(event.key == InputKeyOk && event.type == InputTypeLong) {
paint_state->isDrawing = !paint_state->isDrawing;
paint_state->board[paint_state->selected.x][paint_state->selected.y] = true;
view_port_update(view_port);
break;
case InputKeyBack:
if(event.type == InputTypeLong) {
//start over with an empty canvas and the pen lifted
memset(paint_state->board, 0, sizeof(paint_state->board));
paint_state->mode = PaintModeMove;
} else if(event.type == InputTypeShort) {
running = false;
}
break;
default:
break;
}

furi_mutex_release(paint_state->mutex);
view_port_update(view_port);
}

//tear down the viewport before the (potentially slow) save so input
//events can no longer pile up in a queue nobody drains anymore
gui_remove_view_port(gui, view_port);
view_port_free(view_port);
furi_message_queue_free(event_queue);
furi_mutex_free(paint_state->mutex);
furi_record_close(RECORD_NOTIFICATION);
furi_record_close(RECORD_GUI);

if(!paint_save(paint_state)) {
//the drawing could not be written out - tell the user instead of
//silently losing it
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
dialog_message_show_storage_error(dialogs, "Cannot save\nthe canvas");
furi_record_close(RECORD_DIALOGS);
}

furi_mutex_free(paint_state->mutex);
free(paint_state);

return 0;
Expand Down
Loading