Skip to content

Commit

Permalink
lib: implement drawing image with affine transform, add pivot to image
Browse files Browse the repository at this point in the history
  • Loading branch information
and3rson committed Mar 11, 2024
1 parent 8531301 commit 7d6a61f
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 78 deletions.
21 changes: 15 additions & 6 deletions firmware/keira/src/apps/demos/transform.cpp
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
#include "transform.h"

#include <math.h>

TransformApp::TransformApp() : App("Transform") {
}

void TransformApp::run() {
lilka::Image* face = lilka::resources.loadImage("/sd/face.bmp");
lilka::Image* face = lilka::resources.loadImage("/sd/face.bmp", canvas->color565(0, 0, 0), 32, 32);

int x = canvas->width() / 2;
int y = canvas->height() / 2;

int x = canvas->width() / 2 - face->width / 2;
int y = canvas->height() / 2 - face->height / 2;
Serial.println("Drawing face at " + String(x) + ", " + String(y));

int angle = 0;

while (1) {
canvas->fillScreen(canvas->color565(0, 0, 0));
lilka::Transform transform = lilka::Transform().rotate(angle);
canvas->fillScreen(canvas->color565(0, 64, 0));
// canvas->drawImage(face, x, y);
lilka::Transform transform = lilka::Transform().rotate(angle).scale(sin(angle / 24.0), cos(angle / 50.0));
// lilka::Transform transform = lilka::Transform().rotate(30).scale(1.5, 1);
uint64_t start = micros();
canvas->drawImageTransformed(face, x, y, transform);
uint64_t end = micros();
Serial.println("Drawing took " + String(end - start) + " us");
queueDraw();
angle++;
angle += 8;

lilka::State state = lilka::controller.getState();
if (state.a.justPressed) {
Expand Down
2 changes: 1 addition & 1 deletion firmware/keira/src/apps/statusbar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ void StatusBarApp::run() {
canvas->draw16bitRGBBitmapWithTranColor(144, 0, wifi_offline, 0, 24, 24);
} else if (networkService->getNetworkState() == NETWORK_STATE_CONNECTING) {
canvas->draw16bitRGBBitmapWithTranColor(144, 0, wifi_connecting, 0, 24, 24);
} else {
} else if (networkService->getNetworkState() == NETWORK_STATE_ONLINE) {
canvas->draw16bitRGBBitmapWithTranColor(144, 0, icons[networkService->getSignalStrength()], 0, 24, 24);
}
}
Expand Down
161 changes: 94 additions & 67 deletions sdk/lib/lilka/src/lilka/display.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,44 +93,49 @@ void Display::setSplash(const uint16_t* splash) {

void Display::drawImage(Image* image, int16_t x, int16_t y) {
if (image->transparentColor == -1) {
draw16bitRGBBitmap(x, y, image->pixels, image->width, image->height);
draw16bitRGBBitmap(x - image->pivotX, y - image->pivotY, image->pixels, image->width, image->height);
} else {
draw16bitRGBBitmapWithTranColor(x, y, image->pixels, image->transparentColor, image->width, image->height);
draw16bitRGBBitmapWithTranColor(
x - image->pivotX, y - image->pivotY, image->pixels, image->transparentColor, image->width, image->height
);
}
}

void Display::drawImageTransformed(Image* image, int16_t x, int16_t y, Transform transform) {
int16_t w = image->width;
int16_t h = image->height;
int16_t x0 = transform.matrix[0][0] * x + transform.matrix[0][1] * y;
int16_t y0 = transform.matrix[1][0] * x + transform.matrix[1][1] * y;
int16_t x1 = transform.matrix[0][0] * (x + w) + transform.matrix[0][1] * y;
int16_t y1 = transform.matrix[1][0] * (x + w) + transform.matrix[1][1] * y;
int16_t x2 = transform.matrix[0][0] * (x + w) + transform.matrix[0][1] * (y + h);
int16_t y2 = transform.matrix[1][0] * (x + w) + transform.matrix[1][1] * (y + h);
int16_t x3 = transform.matrix[0][0] * x + transform.matrix[0][1] * (y + h);
int16_t y3 = transform.matrix[1][0] * x + transform.matrix[1][1] * (y + h);
int16_t minX = min(min(x0, x1), min(x2, x3));
int16_t minY = min(min(y0, y1), min(y2, y3));
int16_t maxX = max(max(x0, x1), max(x2, x3));
int16_t maxY = max(max(y0, y1), max(y2, y3));
int16_t destWidth = maxX - minX;
int16_t destHeight = maxY - minY;
Image dest(destWidth, destHeight, 0);
for (int y = minY; y < maxY; y++) {
for (int x = minX; x < maxX; x++) {
int16_t srcX = transform.matrix[0][0] * x + transform.matrix[0][1] * y;
int16_t srcY = transform.matrix[1][0] * x + transform.matrix[1][1] * y;
if (srcX >= x && srcX < x + w) {
if (srcY >= y && srcY < y + h) {
dest.pixels[x - minX + (y - minY) * destWidth] = image->pixels[srcX - x + (srcY - y) * w];
} else {
dest.pixels[x - minX + (y - minY) * destWidth] = image->transparentColor;
}
void Display::drawImageTransformed(Image* image, int16_t destX, int16_t destY, Transform transform) {
// Transform image around its pivot.
// Draw the rotated image at the specified position.

// Calculate the coordinates of the four corners of the destination rectangle.
IntVector v1 = transform.apply(IntVector(-image->pivotX, -image->pivotY));
IntVector v2 = transform.apply(IntVector(image->width - image->pivotX, -image->pivotY));
IntVector v3 = transform.apply(IntVector(-image->pivotX, image->height - image->pivotY));
IntVector v4 = transform.apply(IntVector(image->width - image->pivotX, image->height - image->pivotY));

// Find the bounding box of the transformed image.
IntVector topLeft = IntVector(min(min(v1.x, v2.x), min(v3.x, v4.x)), min(min(v1.y, v2.y), min(v3.y, v4.y)));
IntVector bottomRight = IntVector(max(max(v1.x, v2.x), max(v3.x, v4.x)), max(max(v1.y, v2.y), max(v3.y, v4.y)));

// Create a new image to hold the transformed image.
Image destImage(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y, image->transparentColor, 0, 0);

// Draw the transformed image to the new image.
Transform inverse = transform.inverse();
for (int y = topLeft.y; y < bottomRight.y; y++) {
for (int x = topLeft.x; x < bottomRight.x; x++) {
IntVector v = inverse.apply(IntVector(x, y));
// Apply pivot offset
v.x += image->pivotX;
v.y += image->pivotY;
if (v.x >= 0 && v.x < image->width && v.y >= 0 && v.y < image->height) {
destImage.pixels[x - topLeft.x + (y - topLeft.y) * destImage.width] =
image->pixels[v.x + v.y * image->width];
} else {
destImage.pixels[x - topLeft.x + (y - topLeft.y) * destImage.width] = image->transparentColor;
}
}
}
drawImage(&dest, minX, minY);

drawImage(&destImage, destX + topLeft.x, destY + topLeft.y);
}

// Чомусь в Arduino_GFX немає варіанту цього методу для const uint16_t[] - є лише для uint16_t.
Expand Down Expand Up @@ -166,47 +171,50 @@ Canvas::Canvas(uint16_t x, uint16_t y, uint16_t width, uint16_t height) :

void Canvas::drawImage(Image* image, int16_t x, int16_t y) {
if (image->transparentColor == -1) {
draw16bitRGBBitmap(x, y, image->pixels, image->width, image->height);
draw16bitRGBBitmap(x - image->pivotX, y - image->pivotY, image->pixels, image->width, image->height);
} else {
draw16bitRGBBitmapWithTranColor(x, y, image->pixels, image->transparentColor, image->width, image->height);
draw16bitRGBBitmapWithTranColor(
x - image->pivotX, y - image->pivotY, image->pixels, image->transparentColor, image->width, image->height
);
}
}

void Canvas::drawImageTransformed(Image* image, int16_t x, int16_t y, Transform transform) {
int16_t w = image->width;
int16_t h = image->height;
int16_t x0 = transform.matrix[0][0] * x + transform.matrix[0][1] * y;
int16_t y0 = transform.matrix[1][0] * x + transform.matrix[1][1] * y;
int16_t x1 = transform.matrix[0][0] * (x + w) + transform.matrix[0][1] * y;
int16_t y1 = transform.matrix[1][0] * (x + w) + transform.matrix[1][1] * y;
int16_t x2 = transform.matrix[0][0] * (x + w) + transform.matrix[0][1] * (y + h);
int16_t y2 = transform.matrix[1][0] * (x + w) + transform.matrix[1][1] * (y + h);
int16_t x3 = transform.matrix[0][0] * x + transform.matrix[0][1] * (y + h);
int16_t y3 = transform.matrix[1][0] * x + transform.matrix[1][1] * (y + h);
int16_t minX = min(min(x0, x1), min(x2, x3));
int16_t minY = min(min(y0, y1), min(y2, y3));
int16_t maxX = max(max(x0, x1), max(x2, x3));
int16_t maxY = max(max(y0, y1), max(y2, y3));
int16_t destWidth = maxX - minX;
int16_t destHeight = maxY - minY;
Image dest(destWidth, destHeight, 0);
for (int y = minY; y < maxY; y++) {
for (int x = minX; x < maxX; x++) {
int16_t srcX = transform.matrix[0][0] * x + transform.matrix[0][1] * y;
int16_t srcY = transform.matrix[1][0] * x + transform.matrix[1][1] * y;
if (srcX >= x && srcX < x + w) {
if (srcY >= y && srcY < y + h) {
// Піксель вихідного зображення знаходиться в межах вхідного зображення
dest.pixels[x - minX + (y - minY) * destWidth] = image->pixels[srcX - x + (srcY - y) * w];
} else {
// Піксель вихідного зображення знаходиться поза межами вхідного зображення,
// тому він повинен бути прозорим.
dest.pixels[x - minX + (y - minY) * destWidth] = image->transparentColor;
}
void Canvas::drawImageTransformed(Image* image, int16_t destX, int16_t destY, Transform transform) {
// Transform image around its pivot.
// Draw the rotated image at the specified position.

// Calculate the coordinates of the four corners of the destination rectangle.
IntVector v1 = transform.apply(IntVector(-image->pivotX, -image->pivotY));
IntVector v2 = transform.apply(IntVector(image->width - image->pivotX, -image->pivotY));
IntVector v3 = transform.apply(IntVector(-image->pivotX, image->height - image->pivotY));
IntVector v4 = transform.apply(IntVector(image->width - image->pivotX, image->height - image->pivotY));

// Find the bounding box of the transformed image.
IntVector topLeft = IntVector(min(min(v1.x, v2.x), min(v3.x, v4.x)), min(min(v1.y, v2.y), min(v3.y, v4.y)));
IntVector bottomRight = IntVector(max(max(v1.x, v2.x), max(v3.x, v4.x)), max(max(v1.y, v2.y), max(v3.y, v4.y)));

// Create a new image to hold the transformed image.
Image destImage(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y, image->transparentColor, 0, 0);

// Draw the transformed image to the new image.
Transform inverse = transform.inverse();
for (int y = topLeft.y; y < bottomRight.y; y++) {
for (int x = topLeft.x; x < bottomRight.x; x++) {
IntVector v = inverse.apply(IntVector(x, y));
// Apply pivot offset
v.x += image->pivotX;
v.y += image->pivotY;
if (v.x >= 0 && v.x < image->width && v.y >= 0 && v.y < image->height) {
destImage.pixels[x - topLeft.x + (y - topLeft.y) * destImage.width] =
image->pixels[v.x + v.y * image->width];
} else {
destImage.pixels[x - topLeft.x + (y - topLeft.y) * destImage.width] = image->transparentColor;
}
}
}
drawImage(&dest, minX, minY);

// TODO: Draw directly to the canvas?
drawImage(&destImage, destX + topLeft.x, destY + topLeft.y);
}

void Canvas::draw16bitRGBBitmapWithTranColor(
Expand All @@ -229,8 +237,8 @@ int16_t Canvas::y() {
return _output_y;
}

Image::Image(uint32_t width, uint32_t height, int32_t transparentColor) :
width(width), height(height), transparentColor(transparentColor) {
Image::Image(uint32_t width, uint32_t height, int32_t transparentColor, int16_t pivotX, int16_t pivotY) :
width(width), height(height), transparentColor(transparentColor), pivotX(pivotX), pivotY(pivotY) {
// Allocate pixels in PSRAM
pixels = static_cast<uint16_t*>(ps_malloc(width * height * sizeof(uint16_t)));
}
Expand Down Expand Up @@ -329,6 +337,25 @@ Transform Transform::scale(float sx, float sy) {
return t.multiply(*this);
}

Transform Transform::inverse() {
// Calculate the inverse of this transform
Transform t;
float det = matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
t.matrix[0][0] = matrix[1][1] / det;
t.matrix[0][1] = -matrix[0][1] / det;
t.matrix[1][0] = -matrix[1][0] / det;
t.matrix[1][1] = matrix[0][0] / det;
return t;
}

IntVector Transform::apply(IntVector v) {
// Apply this transform to a vector
return IntVector(matrix[0][0] * v.x + matrix[0][1] * v.y, matrix[1][0] * v.x + matrix[1][1] * v.y);
}

IntVector::IntVector(int32_t x, int32_t y) : x(x), y(y) {
}

Display display;

} // namespace lilka
18 changes: 17 additions & 1 deletion sdk/lib/lilka/src/lilka/display.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace lilka {
class Canvas;
class Image;
class Transform;
class IntVector;

/// Клас для роботи з дисплеєм.
///
Expand Down Expand Up @@ -311,7 +312,7 @@ class Canvas : public Arduino_Canvas {
/// @note Основна відмінність Image від поняття "bitmap" погялає в тому, що Image містить масив пікселів, розміри зображення і прозорий колір, в той час як "bitmap" - це просто масив пікселів.
class Image {
public:
Image(uint32_t width, uint32_t height, int32_t transparentColor = -1);
Image(uint32_t width, uint32_t height, int32_t transparentColor = -1, int16_t pivotX = 0, int16_t pivotY = 0);
~Image();
/// Обернути зображення на заданий кут (в градусах) і записати результат в `dest`.
///
Expand Down Expand Up @@ -344,6 +345,8 @@ class Image {
uint32_t height;
/// 16-бітний колір (5-6-5), який буде прозорим. За замовчуванням -1 (прозорість відсутня).
int32_t transparentColor;
int16_t pivotX;
int16_t pivotY;
uint16_t* pixels;
};

Expand Down Expand Up @@ -380,10 +383,23 @@ class Transform {
/// @param other Інше перетворення.
Transform multiply(Transform other);

/// Інвертувати перетворення.
/// @note Інвертне перетворення - це таке перетворення, яке скасує це перетворення, тобто є зворотнім до цього.
Transform inverse();

IntVector apply(IntVector vector);

// Матриця перетворення
float matrix[2][2]; // [рядок][стовпець]
};

class IntVector {
public:
IntVector(int32_t x, int32_t y);
int32_t x;
int32_t y;
};

/// Екземпляр класу `Display`, який можна використовувати для роботи з дисплеєм.
/// Вам не потрібно інстанціювати `Display` вручну.
extern Display display;
Expand Down
4 changes: 2 additions & 2 deletions sdk/lib/lilka/src/lilka/resources.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace lilka {

Image* Resources::loadImage(String filename, int32_t transparentColor) {
Image* Resources::loadImage(String filename, int32_t transparentColor, int32_t pivotX, int32_t pivotY) {
FILE* file = fopen(filename.c_str(), "r");
if (!file) {
// serial_err("File not found: %s\n", filename.c_str());
Expand Down Expand Up @@ -37,7 +37,7 @@ Image* Resources::loadImage(String filename, int32_t transparentColor) {
return 0;
}

Image* image = new Image(width, height, transparentColor);
Image* image = new Image(width, height, transparentColor, pivotX, pivotY);
fseek(file, dataOffset, SEEK_SET);
uint8_t row[width * bytesPerPixel];
for (int y = height - 1; y >= 0; y--) {
Expand Down
4 changes: 3 additions & 1 deletion sdk/lib/lilka/src/lilka/resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class Resources {
///
/// \param filename Шлях до файлу.
/// \param transparentColor 16-бітний колір (5-6-5), який буде прозорим. За замовчуванням -1 (прозорість відсутня).
/// \param pivotX X-координата точки, яка буде центром зображення. За замовчуванням 0.
/// \param pivotY Y-координата точки, яка буде центром зображення. За замовчуванням 0.
/// \return Вказівник на зображення.
///
/// \warning Пам'ять для зображення виділяється динамічно. Після використання зображення, його потрібно видалити за допомогою `delete`.
Expand All @@ -32,7 +34,7 @@ class Resources {
/// // Звільнити пам'ять
/// delete image;
/// \endcode
Image* loadImage(String filename, int32_t transparentColor = -1);
Image* loadImage(String filename, int32_t transparentColor = -1, int32_t pivotX = 0, int32_t pivotY = 0);
/// Прочитати вміст файлу.
///
/// TODO: Update sdcard/filesystem stuff
Expand Down

0 comments on commit 7d6a61f

Please sign in to comment.