From 2a17c138ba261593569e8f1a143dc195b147733a Mon Sep 17 00:00:00 2001 From: daniele-NA Date: Thu, 12 Feb 2026 10:12:38 +0100 Subject: [PATCH 1/3] INIT --- .idea/.gitignore | 8 + android/.gitignore | 15 + android/README.md | 0 android/app/build.gradle.kts | 54 + android/app/src/main/AndroidManifest.xml | 22 + android/app/src/main/jni/CMakeLists.txt | 21 + android/app/src/main/jni/main.c | 145 +++ android/app/src/main/jni/olive.c | 1022 +++++++++++++++++ android/app/src/main/jni/vc.c | 565 +++++++++ .../main/kotlin/com/me/app/MainActivity.kt | 65 ++ android/app/src/main/res/drawable/main_ic.xml | 82 ++ android/app/src/main/res/drawable/mpm_bg.xml | 4 + .../app/src/main/res/layout/activity_main.xml | 21 + .../src/main/res/mipmap-anydpi-v26/mpm_ic.xml | 6 + android/app/src/main/res/values/themes.xml | 12 + android/build.gradle.kts | 4 + android/gradle.properties | 5 + android/gradle/libs.versions.toml | 9 + android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + android/gradlew | 161 +++ android/gradlew.bat | 89 ++ android/settings.gradle.kts | 28 + 23 files changed, 2343 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 android/.gitignore create mode 100644 android/README.md create mode 100644 android/app/build.gradle.kts create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/jni/CMakeLists.txt create mode 100644 android/app/src/main/jni/main.c create mode 100644 android/app/src/main/jni/olive.c create mode 100644 android/app/src/main/jni/vc.c create mode 100644 android/app/src/main/kotlin/com/me/app/MainActivity.kt create mode 100644 android/app/src/main/res/drawable/main_ic.xml create mode 100644 android/app/src/main/res/drawable/mpm_bg.xml create mode 100644 android/app/src/main/res/layout/activity_main.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/mpm_ic.xml create mode 100644 android/app/src/main/res/values/themes.xml create mode 100644 android/build.gradle.kts create mode 100644 android/gradle.properties create mode 100644 android/gradle/libs.versions.toml create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/settings.gradle.kts diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..dc73e33 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +.idea +.idea/ +.DS_Store +/build +/app/release +/captures +.externalNativeBuild +.cxx +local.properties +.kotlin +test.jks \ No newline at end of file diff --git a/android/README.md b/android/README.md new file mode 100644 index 0000000..e69de29 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..e2c2e58 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,54 @@ +@file:Suppress("UnstableApiUsage","NewerVersionAvailable","UseTomlInstead","GradleDependency") +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.me.app" + compileSdk = 36 + + ndkVersion = "29.0.13846066" + defaultConfig { + applicationId = "com.me.app" + minSdk = 29 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildTypes{ + release { + isMinifyEnabled = true + isShrinkResources = true + } + } + + buildFeatures { + viewBinding = true + } + + externalNativeBuild { + cmake { + path = file("src/main/jni/CMakeLists.txt") + } + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.17.0") + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.constraintlayout:constraintlayout:2.2.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.3") + implementation("androidx.core:core-splashscreen:1.0.1") +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..345980d --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/jni/CMakeLists.txt b/android/app/src/main/jni/CMakeLists.txt new file mode 100644 index 0000000..1358347 --- /dev/null +++ b/android/app/src/main/jni/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.18.2) +project(olivest LANGUAGES C) + +set(CMAKE_C_STANDARD 17) +set(CMAKE_CXX_STANDARD 17) + +add_library(olivest SHARED + main.c + olive.c + vc.c +) + +find_library(jnigraphics-lib jnigraphics) +find_library(android-lib android) +find_library(log-lib log) + +target_link_libraries(olivest + ${jnigraphics-lib} + ${android-lib} + ${log-lib} +) \ No newline at end of file diff --git a/android/app/src/main/jni/main.c b/android/app/src/main/jni/main.c new file mode 100644 index 0000000..5577948 --- /dev/null +++ b/android/app/src/main/jni/main.c @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include + +#include + +#define OLIVEC_IMPLEMENTATION + +#include "olive.c" + +/* Canvas */ +#define WIDTH 1100 +#define HEIGHT 1100 + +/* Scene */ +#define BACKGROUND_COLOR 0xFF181818 +#define CIRCLE_RADIUS 100 +#define CIRCLE_COLOR 0x99AA2020 + +#define PI 3.14159265359f + +static float triangle_angle = 0.0f; +static float circle_x = WIDTH / 2.0f; +static float circle_y = HEIGHT / 2.0f; +static float circle_dx = 100.0f; +static float circle_dy = 100.0f; + +/* Delta time placeholder */ +static const float dt = 1.0f / 120.0f; + +/* Rotate a point around the center */ +static void rotate_point(float *x, float *y) { + float dx = *x - WIDTH / 2.0f; + float dy = *y - HEIGHT / 2.0f; + float mag = sqrtf(dx * dx + dy * dy); + float dir = atan2f(dy, dx) + triangle_angle; + *x = cosf(dir) * mag + WIDTH / 2.0f; + *y = sinf(dir) * mag + HEIGHT / 2.0f; +} + + + +/* + -------------------- X + | + | + | + | + | + | + | + Y + */ + +JNIEXPORT void JNICALL +Java_com_me_app_MainActivity_drawLines(JNIEnv *env, jobject thiz, jobject bmp) { + uint32_t pixels[WIDTH * HEIGHT]; + const Olivec_Canvas oc = olivec_canvas(pixels, WIDTH, HEIGHT, WIDTH); + + olivec_fill(oc, 0xFFFFFFFF); + + const int base_y = 50; + + /* E */ +// olivec_line(oc, 200, base_y, 300, base_y, 0xFFFF00FF); +// olivec_line(oc, 200, base_y * 2, 300, base_y * 2, 0xFF00FFFF); +// olivec_line(oc, 200, base_y * 3, 300, base_y * 3, 0xFFFF0000); +// olivec_line(oc, 200, base_y, 200, base_y * 3, 0xFFFF0000); +// +// /* A */ +// olivec_line(oc, 400, base_y, 350, base_y * 3, 0xFFFF00FF); +// olivec_line(oc, 400, base_y, 450, base_y * 3, 0xFFFF00FF); +// olivec_line(oc, 350, base_y * 2, 450, base_y * 2, 0xFFFF00FF); +// + + + + /* N */ + olivec_line(oc, 600, 600, 600, 800, 0xFF00FF00); + + olivec_line(oc, 650, 800, 600, 600, 0xFF00FF00); + + olivec_line(oc, 650, 800, 650, 600, 0xFF00FF00); + + + + + /* Rectangle */ + //olivec_rect(oc, 400, 200, 100, 150, 0xFF00FF00); + + void *bitmapPixels; + AndroidBitmap_lockPixels(env, bmp, &bitmapPixels); + memcpy(bitmapPixels, pixels, sizeof(pixels)); + AndroidBitmap_unlockPixels(env, bmp); +} + +JNIEXPORT void JNICALL +Java_com_me_app_MainActivity_drawTriangle(JNIEnv *env, jobject thiz, jobject bmp) { + uint32_t pixels[WIDTH * HEIGHT]; + const Olivec_Canvas oc = olivec_canvas(pixels, WIDTH, HEIGHT, WIDTH); + + olivec_fill(oc, BACKGROUND_COLOR); + + /* Triangle */ + triangle_angle += 0.7f * PI * dt; + + float x1 = WIDTH / 2.0f, y1 = HEIGHT * 3.0f / 20; + float x2 = WIDTH * 3.0f / 20, y2 = HEIGHT / 2.0f; + float x3 = WIDTH * 17.0f / 20, y3 = HEIGHT * 17.0f / 20; + + rotate_point(&x1, &y1); + rotate_point(&x2, &y2); + rotate_point(&x3, &y3); + + + + olivec_triangle3c(oc, (int) x1, (int) y1, (int) x2, (int) y2, (int) x3, (int) y3, + 0xFF2020FF, 0xFF20FF20, 0xFFFF2020); + + /* Circle */ + float nx = circle_x + circle_dx * dt; + if (nx - CIRCLE_RADIUS < 0 || nx + CIRCLE_RADIUS >= WIDTH) { + circle_dx *= -1; + } else { + circle_x = nx; + } + + float ny = circle_y + circle_dy * dt; + if (ny - CIRCLE_RADIUS < 0 || ny + CIRCLE_RADIUS >= HEIGHT) { + circle_dy *= -1; + } else { + circle_y = ny; + } + + // == DRAWING TH CIRCLE WITHIN THE TRIANGLE == // + olivec_circle(oc, (int) circle_x, (int) circle_y, CIRCLE_RADIUS, CIRCLE_COLOR); + + // == CREATE THE BITMAP WITH THE GIVEN POINTER == // + void *bitmapPixels; + AndroidBitmap_lockPixels(env, bmp, &bitmapPixels); + memcpy(bitmapPixels, pixels, sizeof(pixels)); + AndroidBitmap_unlockPixels(env, bmp); +} diff --git a/android/app/src/main/jni/olive.c b/android/app/src/main/jni/olive.c new file mode 100644 index 0000000..fe06754 --- /dev/null +++ b/android/app/src/main/jni/olive.c @@ -0,0 +1,1022 @@ +// Copyright 2022 Alexey Kutepov +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#ifndef OLIVE_C_ +#define OLIVE_C_ + +#include +#include +#include + +#ifndef OLIVECDEF +#define OLIVECDEF static inline +#endif + +#ifndef OLIVEC_AA_RES +#define OLIVEC_AA_RES 2 +#endif + +#define OLIVEC_SWAP(T, a, b) do { T t = a; a = b; b = t; } while (0) +#define OLIVEC_SIGN(T, x) ((T)((x) > 0) - (T)((x) < 0)) +#define OLIVEC_ABS(T, x) (OLIVEC_SIGN(T, x)*(x)) + +typedef struct { + size_t width, height; + const char *glyphs; +} Olivec_Font; + +#define OLIVEC_DEFAULT_FONT_HEIGHT 6 +#define OLIVEC_DEFAULT_FONT_WIDTH 6 +// TODO: allocate proper descender and acender areas for the default font +static char olivec_default_glyphs[128][OLIVEC_DEFAULT_FONT_HEIGHT][OLIVEC_DEFAULT_FONT_WIDTH] = { + ['a'] = { + {0, 0, 0, 0, 0}, + {0, 1, 1, 0, 0}, + {0, 0, 0, 1, 0}, + {0, 1, 1, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 1, 0}, + }, + ['b'] = { + {1, 0, 0, 0, 0}, + {1, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 1, 1, 0, 0}, + }, + ['c'] = { + {0, 0, 0, 0, 0}, + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 0, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + ['d'] = { + {0, 0, 0, 1, 0}, + {0, 1, 1, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 1, 0}, + }, + ['e'] = { + {0, 0, 0, 0, 0}, + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 1, 1, 1, 0}, + {1, 0, 0, 0, 0}, + {0, 1, 1, 1, 0}, + }, + ['f'] = { + {0, 0, 1, 1, 0}, + {0, 1, 0, 0, 0}, + {1, 1, 1, 1, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + }, + ['g'] = { + {0, 1, 1, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 1, 0}, + {0, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + ['h'] = { + {1, 0, 0, 0, 0}, + {1, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + }, + ['i'] = { + {0, 0, 1, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 1, 0, 0}, + }, + ['j'] = { + {0, 0, 1, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 1, 0, 0}, + {1, 0, 1, 0, 0}, + {0, 1, 1, 0, 0}, + }, + ['k'] = { + {1, 0, 0, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 1, 0, 0}, + {1, 1, 0, 0, 0}, + {1, 0, 1, 0, 0}, + {1, 0, 0, 1, 0}, + }, + ['l'] = { + {0, 1, 1, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 1, 1, 1, 0}, + }, + ['m'] = { + {0, 0, 0, 0, 0}, + {0, 1, 0, 1, 1}, + {1, 0, 1, 0, 1}, + {1, 0, 1, 0, 1}, + {1, 0, 1, 0, 1}, + {1, 0, 1, 0, 1}, + }, + ['n'] = { + {0, 0, 0, 0, 0}, + {0, 1, 1, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + }, + ['o'] = { + {0, 0, 0, 0, 0}, + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + ['p'] = { + {1, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 1, 1, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0}, + }, + ['q'] = { + {0, 1, 1, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 1, 0}, + {0, 0, 0, 1, 0}, + {0, 0, 0, 1, 0}, + }, + ['r'] = { + {0, 0, 0, 0, 0}, + {1, 0, 1, 1, 0}, + {1, 1, 0, 0, 1}, + {1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0}, + }, + ['s'] = { + {0, 0, 0, 0, 0}, + {0, 1, 1, 1, 0}, + {1, 0, 0, 0, 0}, + {1, 1, 1, 1, 0}, + {0, 0, 0, 1, 0}, + {1, 1, 1, 0, 0}, + }, + ['t'] = { + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {1, 1, 1, 1, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + ['u'] = { + {0, 0, 0, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 1, 0}, + }, + ['v'] = { + {0, 0, 0, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + ['w'] = { + {0, 0, 0, 0, 0}, + {1, 0, 0, 0, 1}, + {1, 0, 1, 0, 1}, + {1, 0, 1, 0, 1}, + {1, 0, 1, 0, 1}, + {0, 1, 1, 1, 1}, + }, + ['x'] = { + {0, 0, 0, 0, 0}, + {1, 0, 1, 0, 0}, + {1, 0, 1, 0, 0}, + {0, 1, 0, 0, 0}, + {1, 0, 1, 0, 0}, + {1, 0, 1, 0, 0}, + }, + ['y'] = { + {0, 0, 0, 0, 0}, + {1, 0, 1, 0, 0}, + {1, 0, 1, 0, 0}, + {1, 0, 1, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + }, + ['z'] = { + {0, 0, 0, 0, 0}, + {1, 1, 1, 1, 0}, + {0, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 1, 1, 1, 0}, + }, + + ['A'] = {0}, + ['B'] = {0}, + ['C'] = {0}, + ['D'] = {0}, + ['E'] = {0}, + ['F'] = {0}, + ['G'] = {0}, + ['H'] = {0}, + ['I'] = {0}, + ['J'] = {0}, + ['K'] = {0}, + ['L'] = {0}, + ['M'] = {0}, + ['N'] = {0}, + ['O'] = {0}, + ['P'] = {0}, + ['Q'] = {0}, + ['R'] = {0}, + ['S'] = {0}, + ['T'] = {0}, + ['U'] = {0}, + ['V'] = {0}, + ['W'] = {0}, + ['X'] = {0}, + ['Y'] = {0}, + ['Z'] = {0}, + + ['0'] = { + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + ['1'] = { + {0, 0, 1, 0, 0}, + {0, 1, 1, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 1, 0, 0}, + {0, 1, 1, 1, 0}, + }, + ['2'] = { + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {0, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 1, 1, 1, 0}, + }, + ['3'] = { + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + ['4'] = { + {0, 0, 1, 1, 0}, + {0, 1, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 1, 1, 1, 1}, + {0, 0, 0, 1, 0}, + {0, 0, 0, 1, 0}, + }, + ['5'] = { + {1, 1, 1, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 1, 1, 0, 0}, + {0, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + ['6'] = { + {0, 1, 1, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + ['7'] = { + {1, 1, 1, 1, 0}, + {0, 0, 0, 1, 0}, + {0, 0, 1, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + }, + ['8'] = { + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + + }, + ['9'] = { + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 1, 0}, + {0, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + + [','] = { + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 1, 0}, + {0, 0, 1, 0, 0}, + }, + + ['.'] = { + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 1, 0, 0}, + }, + ['-'] = { + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {1, 1, 1, 1, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + }, +}; + +static Olivec_Font olivec_default_font = { + .glyphs = &olivec_default_glyphs[0][0][0], + .width = OLIVEC_DEFAULT_FONT_WIDTH, + .height = OLIVEC_DEFAULT_FONT_HEIGHT, +}; + +// WARNING! Always initialize your Canvas with a color that has Non-Zero Alpha Channel! +// A lot of functions use `olivec_blend_color()` function to blend with the Background +// which preserves the original Alpha of the Background. So you may easily end up with +// a result that is perceptually transparent if the Alpha is Zero. +typedef struct { + uint32_t *pixels; + size_t width; + size_t height; + size_t stride; +} Olivec_Canvas; + +#define OLIVEC_CANVAS_NULL ((Olivec_Canvas) {0}) +#define OLIVEC_PIXEL(oc, x, y) (oc).pixels[(y)*(oc).stride + (x)] + +OLIVECDEF Olivec_Canvas olivec_canvas(uint32_t *pixels, size_t width, size_t height, size_t stride); +OLIVECDEF Olivec_Canvas olivec_subcanvas(Olivec_Canvas oc, int x, int y, int w, int h); +OLIVECDEF bool olivec_in_bounds(Olivec_Canvas oc, int x, int y); +OLIVECDEF void olivec_blend_color(uint32_t *c1, uint32_t c2); +OLIVECDEF void olivec_fill(Olivec_Canvas oc, uint32_t color); +OLIVECDEF void olivec_rect(Olivec_Canvas oc, int x, int y, int w, int h, uint32_t color); +OLIVECDEF void olivec_frame(Olivec_Canvas oc, int x, int y, int w, int h, size_t thiccness, uint32_t color); +OLIVECDEF void olivec_circle(Olivec_Canvas oc, int cx, int cy, int r, uint32_t color); +OLIVECDEF void olivec_ellipse(Olivec_Canvas oc, int cx, int cy, int rx, int ry, uint32_t color); +// TODO: lines with different thiccness +OLIVECDEF void olivec_line(Olivec_Canvas oc, int x1, int y1, int x2, int y2, uint32_t color); +OLIVECDEF bool olivec_normalize_triangle(size_t width, size_t height, int x1, int y1, int x2, int y2, int x3, int y3, int *lx, int *hx, int *ly, int *hy); +OLIVECDEF bool olivec_barycentric(int x1, int y1, int x2, int y2, int x3, int y3, int xp, int yp, int *u1, int *u2, int *det); +OLIVECDEF void olivec_triangle(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, uint32_t color); +OLIVECDEF void olivec_triangle3c(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, uint32_t c1, uint32_t c2, uint32_t c3); +OLIVECDEF void olivec_triangle3z(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float z1, float z2, float z3); +OLIVECDEF void olivec_triangle3uv(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float tx1, float ty1, float tx2, float ty2, float tx3, float ty3, float z1, float z2, float z3, Olivec_Canvas texture); +OLIVECDEF void olivec_triangle3uv_bilinear(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float tx1, float ty1, float tx2, float ty2, float tx3, float ty3, float z1, float z2, float z3, Olivec_Canvas texture); +OLIVECDEF void olivec_text(Olivec_Canvas oc, const char *text, int x, int y, Olivec_Font font, size_t size, uint32_t color); +OLIVECDEF void olivec_sprite_blend(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite); +OLIVECDEF void olivec_sprite_copy(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite); +OLIVECDEF void olivec_sprite_copy_bilinear(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite); +OLIVECDEF uint32_t olivec_pixel_bilinear(Olivec_Canvas sprite, int nx, int ny, int w, int h); + +typedef struct { + // Safe ranges to iterate over. + int x1, x2; + int y1, y2; + + // Original uncut ranges some parts of which may be outside of the canvas boundaries. + int ox1, ox2; + int oy1, oy2; +} Olivec_Normalized_Rect; + +// The point of this function is to produce two ranges x1..x2 and y1..y2 that are guaranteed to be safe to iterate over the canvas of size pixels_width by pixels_height without any boundary checks. +// +// Olivec_Normalized_Rect nr = {0}; +// if (olivec_normalize_rect(x, y, w, h, WIDTH, HEIGHT, &nr)) { +// for (int x = nr.x1; x <= nr.x2; ++x) { +// for (int y = nr.y1; y <= nr.y2; ++y) { +// OLIVEC_PIXEL(oc, x, y) = 0x69696969; +// } +// } +// } else { +// // Rectangle is invisible cause it's completely out-of-bounds +// } +OLIVECDEF bool olivec_normalize_rect(int x, int y, int w, int h, + size_t canvas_width, size_t canvas_height, + Olivec_Normalized_Rect *nr); + +#endif // OLIVE_C_ + +#ifdef OLIVEC_IMPLEMENTATION + +OLIVECDEF Olivec_Canvas olivec_canvas(uint32_t *pixels, size_t width, size_t height, size_t stride) +{ + Olivec_Canvas oc = { + .pixels = pixels, + .width = width, + .height = height, + .stride = stride, + }; + return oc; +} + +OLIVECDEF bool olivec_normalize_rect(int x, int y, int w, int h, + size_t canvas_width, size_t canvas_height, + Olivec_Normalized_Rect *nr) +{ + // No need to render empty rectangle + if (w == 0) return false; + if (h == 0) return false; + + nr->ox1 = x; + nr->oy1 = y; + + // Convert the rectangle to 2-points representation + nr->ox2 = nr->ox1 + OLIVEC_SIGN(int, w)*(OLIVEC_ABS(int, w) - 1); + if (nr->ox1 > nr->ox2) OLIVEC_SWAP(int, nr->ox1, nr->ox2); + nr->oy2 = nr->oy1 + OLIVEC_SIGN(int, h)*(OLIVEC_ABS(int, h) - 1); + if (nr->oy1 > nr->oy2) OLIVEC_SWAP(int, nr->oy1, nr->oy2); + + // Cull out invisible rectangle + if (nr->ox1 >= (int) canvas_width) return false; + if (nr->ox2 < 0) return false; + if (nr->oy1 >= (int) canvas_height) return false; + if (nr->oy2 < 0) return false; + + nr->x1 = nr->ox1; + nr->y1 = nr->oy1; + nr->x2 = nr->ox2; + nr->y2 = nr->oy2; + + // Clamp the rectangle to the boundaries + if (nr->x1 < 0) nr->x1 = 0; + if (nr->x2 >= (int) canvas_width) nr->x2 = (int) canvas_width - 1; + if (nr->y1 < 0) nr->y1 = 0; + if (nr->y2 >= (int) canvas_height) nr->y2 = (int) canvas_height - 1; + + return true; +} + +OLIVECDEF Olivec_Canvas olivec_subcanvas(Olivec_Canvas oc, int x, int y, int w, int h) +{ + Olivec_Normalized_Rect nr = {0}; + if (!olivec_normalize_rect(x, y, w, h, oc.width, oc.height, &nr)) return OLIVEC_CANVAS_NULL; + oc.pixels = &OLIVEC_PIXEL(oc, nr.x1, nr.y1); + oc.width = nr.x2 - nr.x1 + 1; + oc.height = nr.y2 - nr.y1 + 1; + return oc; +} + +// TODO: custom pixel formats +// Maybe we can store pixel format info in Olivec_Canvas +#define OLIVEC_RED(color) (((color)&0x000000FF)>>(8*0)) +#define OLIVEC_GREEN(color) (((color)&0x0000FF00)>>(8*1)) +#define OLIVEC_BLUE(color) (((color)&0x00FF0000)>>(8*2)) +#define OLIVEC_ALPHA(color) (((color)&0xFF000000)>>(8*3)) +#define OLIVEC_RGBA(r, g, b, a) ((((r)&0xFF)<<(8*0)) | (((g)&0xFF)<<(8*1)) | (((b)&0xFF)<<(8*2)) | (((a)&0xFF)<<(8*3))) + +OLIVECDEF void olivec_blend_color(uint32_t *c1, uint32_t c2) +{ + uint32_t r1 = OLIVEC_RED(*c1); + uint32_t g1 = OLIVEC_GREEN(*c1); + uint32_t b1 = OLIVEC_BLUE(*c1); + uint32_t a1 = OLIVEC_ALPHA(*c1); + + uint32_t r2 = OLIVEC_RED(c2); + uint32_t g2 = OLIVEC_GREEN(c2); + uint32_t b2 = OLIVEC_BLUE(c2); + uint32_t a2 = OLIVEC_ALPHA(c2); + + r1 = (r1*(255 - a2) + r2*a2)/255; if (r1 > 255) r1 = 255; + g1 = (g1*(255 - a2) + g2*a2)/255; if (g1 > 255) g1 = 255; + b1 = (b1*(255 - a2) + b2*a2)/255; if (b1 > 255) b1 = 255; + + *c1 = OLIVEC_RGBA(r1, g1, b1, a1); +} + +OLIVECDEF void olivec_fill(Olivec_Canvas oc, uint32_t color) +{ + for (size_t y = 0; y < oc.height; ++y) { + for (size_t x = 0; x < oc.width; ++x) { + OLIVEC_PIXEL(oc, x, y) = color; + } + } +} + +OLIVECDEF void olivec_rect(Olivec_Canvas oc, int x, int y, int w, int h, uint32_t color) +{ + Olivec_Normalized_Rect nr = {0}; + if (!olivec_normalize_rect(x, y, w, h, oc.width, oc.height, &nr)) return; + for (int x = nr.x1; x <= nr.x2; ++x) { + for (int y = nr.y1; y <= nr.y2; ++y) { + olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), color); + } + } +} + +OLIVECDEF void olivec_frame(Olivec_Canvas oc, int x, int y, int w, int h, size_t t, uint32_t color) +{ + if (t == 0) return; // Nothing to render + + // Convert the rectangle to 2-points representation + int x1 = x; + int y1 = y; + int x2 = x1 + OLIVEC_SIGN(int, w)*(OLIVEC_ABS(int, w) - 1); + if (x1 > x2) OLIVEC_SWAP(int, x1, x2); + int y2 = y1 + OLIVEC_SIGN(int, h)*(OLIVEC_ABS(int, h) - 1); + if (y1 > y2) OLIVEC_SWAP(int, y1, y2); + + olivec_rect(oc, x1 - t/2, y1 - t/2, (x2 - x1 + 1) + t/2*2, t, color); // Top + olivec_rect(oc, x1 - t/2, y1 - t/2, t, (y2 - y1 + 1) + t/2*2, color); // Left + olivec_rect(oc, x1 - t/2, y2 + t/2, (x2 - x1 + 1) + t/2*2, -t, color); // Bottom + olivec_rect(oc, x2 + t/2, y1 - t/2, -t, (y2 - y1 + 1) + t/2*2, color); // Right +} + +OLIVECDEF void olivec_ellipse(Olivec_Canvas oc, int cx, int cy, int rx, int ry, uint32_t color) +{ + Olivec_Normalized_Rect nr = {0}; + int rx1 = rx + OLIVEC_SIGN(int, rx); + int ry1 = ry + OLIVEC_SIGN(int, ry); + if (!olivec_normalize_rect(cx - rx1, cy - ry1, 2*rx1, 2*ry1, oc.width, oc.height, &nr)) return; + + for (int y = nr.y1; y <= nr.y2; ++y) { + for (int x = nr.x1; x <= nr.x2; ++x) { + float nx = (x + 0.5 - nr.x1)/(2.0f*rx1); + float ny = (y + 0.5 - nr.y1)/(2.0f*ry1); + float dx = nx - 0.5; + float dy = ny - 0.5; + if (dx*dx + dy*dy <= 0.5*0.5) { + OLIVEC_PIXEL(oc, x, y) = color; + } + } + } +} + +OLIVECDEF void olivec_circle(Olivec_Canvas oc, int cx, int cy, int r, uint32_t color) +{ + Olivec_Normalized_Rect nr = {0}; + int r1 = r + OLIVEC_SIGN(int, r); + if (!olivec_normalize_rect(cx - r1, cy - r1, 2*r1, 2*r1, oc.width, oc.height, &nr)) return; + + for (int y = nr.y1; y <= nr.y2; ++y) { + for (int x = nr.x1; x <= nr.x2; ++x) { + int count = 0; + for (int sox = 0; sox < OLIVEC_AA_RES; ++sox) { + for (int soy = 0; soy < OLIVEC_AA_RES; ++soy) { + // TODO: switch to 64 bits to make the overflow less likely + // Also research the probability of overflow + int res1 = (OLIVEC_AA_RES + 1); + int dx = (x*res1*2 + 2 + sox*2 - res1*cx*2 - res1); + int dy = (y*res1*2 + 2 + soy*2 - res1*cy*2 - res1); + if (dx*dx + dy*dy <= res1*res1*r*r*2*2) count += 1; + } + } + uint32_t alpha = ((color&0xFF000000)>>(3*8))*count/OLIVEC_AA_RES/OLIVEC_AA_RES; + uint32_t updated_color = (color&0x00FFFFFF)|(alpha<<(3*8)); + olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), updated_color); + } + } +} + +OLIVECDEF bool olivec_in_bounds(Olivec_Canvas oc, int x, int y) +{ + return 0 <= x && x < (int) oc.width && 0 <= y && y < (int) oc.height; +} + +// TODO: AA for line +OLIVECDEF void olivec_line(Olivec_Canvas oc, int x1, int y1, int x2, int y2, uint32_t color) +{ + int dx = x2 - x1; + int dy = y2 - y1; + + // If both of the differences are 0 there will be a division by 0 below. + if (dx == 0 && dy == 0) { + if (olivec_in_bounds(oc, x1, y1)) { + olivec_blend_color(&OLIVEC_PIXEL(oc, x1, y1), color); + } + return; + } + + if (OLIVEC_ABS(int, dx) > OLIVEC_ABS(int, dy)) { + if (x1 > x2) { + OLIVEC_SWAP(int, x1, x2); + OLIVEC_SWAP(int, y1, y2); + } + + for (int x = x1; x <= x2; ++x) { + int y = dy*(x - x1)/dx + y1; + // TODO: move boundary checks out side of the loops in olivec_draw_line + if (olivec_in_bounds(oc, x, y)) { + olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), color); + } + } + } else { + if (y1 > y2) { + OLIVEC_SWAP(int, x1, x2); + OLIVEC_SWAP(int, y1, y2); + } + + for (int y = y1; y <= y2; ++y) { + int x = dx*(y - y1)/dy + x1; + // TODO: move boundary checks out side of the loops in olivec_draw_line + if (olivec_in_bounds(oc, x, y)) { + olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), color); + } + } + } +} + +OLIVECDEF uint32_t mix_colors2(uint32_t c1, uint32_t c2, int u1, int det) +{ + // TODO: estimate how much overflows are an issue in integer only environment + int64_t r1 = OLIVEC_RED(c1); + int64_t g1 = OLIVEC_GREEN(c1); + int64_t b1 = OLIVEC_BLUE(c1); + int64_t a1 = OLIVEC_ALPHA(c1); + + int64_t r2 = OLIVEC_RED(c2); + int64_t g2 = OLIVEC_GREEN(c2); + int64_t b2 = OLIVEC_BLUE(c2); + int64_t a2 = OLIVEC_ALPHA(c2); + + if (det != 0) { + int u2 = det - u1; + int64_t r4 = (r1*u2 + r2*u1)/det; + int64_t g4 = (g1*u2 + g2*u1)/det; + int64_t b4 = (b1*u2 + b2*u1)/det; + int64_t a4 = (a1*u2 + a2*u1)/det; + + return OLIVEC_RGBA(r4, g4, b4, a4); + } + + return 0; +} + +OLIVECDEF uint32_t mix_colors3(uint32_t c1, uint32_t c2, uint32_t c3, int u1, int u2, int det) +{ + // TODO: estimate how much overflows are an issue in integer only environment + int64_t r1 = OLIVEC_RED(c1); + int64_t g1 = OLIVEC_GREEN(c1); + int64_t b1 = OLIVEC_BLUE(c1); + int64_t a1 = OLIVEC_ALPHA(c1); + + int64_t r2 = OLIVEC_RED(c2); + int64_t g2 = OLIVEC_GREEN(c2); + int64_t b2 = OLIVEC_BLUE(c2); + int64_t a2 = OLIVEC_ALPHA(c2); + + int64_t r3 = OLIVEC_RED(c3); + int64_t g3 = OLIVEC_GREEN(c3); + int64_t b3 = OLIVEC_BLUE(c3); + int64_t a3 = OLIVEC_ALPHA(c3); + + if (det != 0) { + int u3 = det - u1 - u2; + int64_t r4 = (r1*u1 + r2*u2 + r3*u3)/det; + int64_t g4 = (g1*u1 + g2*u2 + g3*u3)/det; + int64_t b4 = (b1*u1 + b2*u2 + b3*u3)/det; + int64_t a4 = (a1*u1 + a2*u2 + a3*u3)/det; + + return OLIVEC_RGBA(r4, g4, b4, a4); + } + + return 0; +} + +// NOTE: we imply u3 = det - u1 - u2 +OLIVECDEF bool olivec_barycentric(int x1, int y1, int x2, int y2, int x3, int y3, int xp, int yp, int *u1, int *u2, int *det) +{ + *det = ((x1 - x3)*(y2 - y3) - (x2 - x3)*(y1 - y3)); + *u1 = ((y2 - y3)*(xp - x3) + (x3 - x2)*(yp - y3)); + *u2 = ((y3 - y1)*(xp - x3) + (x1 - x3)*(yp - y3)); + int u3 = *det - *u1 - *u2; + return ( + (OLIVEC_SIGN(int, *u1) == OLIVEC_SIGN(int, *det) || *u1 == 0) && + (OLIVEC_SIGN(int, *u2) == OLIVEC_SIGN(int, *det) || *u2 == 0) && + (OLIVEC_SIGN(int, u3) == OLIVEC_SIGN(int, *det) || u3 == 0) + ); +} + +OLIVECDEF bool olivec_normalize_triangle(size_t width, size_t height, int x1, int y1, int x2, int y2, int x3, int y3, int *lx, int *hx, int *ly, int *hy) +{ + *lx = x1; + *hx = x1; + if (*lx > x2) *lx = x2; + if (*lx > x3) *lx = x3; + if (*hx < x2) *hx = x2; + if (*hx < x3) *hx = x3; + if (*lx < 0) *lx = 0; + if ((size_t) *lx >= width) return false;; + if (*hx < 0) return false;; + if ((size_t) *hx >= width) *hx = width-1; + + *ly = y1; + *hy = y1; + if (*ly > y2) *ly = y2; + if (*ly > y3) *ly = y3; + if (*hy < y2) *hy = y2; + if (*hy < y3) *hy = y3; + if (*ly < 0) *ly = 0; + if ((size_t) *ly >= height) return false;; + if (*hy < 0) return false;; + if ((size_t) *hy >= height) *hy = height-1; + + return true; +} + +OLIVECDEF void olivec_triangle3c(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, + uint32_t c1, uint32_t c2, uint32_t c3) +{ + int lx, hx, ly, hy; + if (olivec_normalize_triangle(oc.width, oc.height, x1, y1, x2, y2, x3, y3, &lx, &hx, &ly, &hy)) { + for (int y = ly; y <= hy; ++y) { + for (int x = lx; x <= hx; ++x) { + int u1, u2, det; + if (olivec_barycentric(x1, y1, x2, y2, x3, y3, x, y, &u1, &u2, &det)) { + olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), mix_colors3(c1, c2, c3, u1, u2, det)); + } + } + } + } +} + +OLIVECDEF void olivec_triangle3z(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float z1, float z2, float z3) +{ + int lx, hx, ly, hy; + if (olivec_normalize_triangle(oc.width, oc.height, x1, y1, x2, y2, x3, y3, &lx, &hx, &ly, &hy)) { + for (int y = ly; y <= hy; ++y) { + for (int x = lx; x <= hx; ++x) { + int u1, u2, det; + if (olivec_barycentric(x1, y1, x2, y2, x3, y3, x, y, &u1, &u2, &det)) { + float z = z1*u1/det + z2*u2/det + z3*(det - u1 - u2)/det; + OLIVEC_PIXEL(oc, x, y) = *(uint32_t*)&z; + } + } + } + } +} + +OLIVECDEF void olivec_triangle3uv(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float tx1, float ty1, float tx2, float ty2, float tx3, float ty3, float z1, float z2, float z3, Olivec_Canvas texture) +{ + int lx, hx, ly, hy; + if (olivec_normalize_triangle(oc.width, oc.height, x1, y1, x2, y2, x3, y3, &lx, &hx, &ly, &hy)) { + for (int y = ly; y <= hy; ++y) { + for (int x = lx; x <= hx; ++x) { + int u1, u2, det; + if (olivec_barycentric(x1, y1, x2, y2, x3, y3, x, y, &u1, &u2, &det)) { + int u3 = det - u1 - u2; + float z = z1*u1/det + z2*u2/det + z3*(det - u1 - u2)/det; + float tx = tx1*u1/det + tx2*u2/det + tx3*u3/det; + float ty = ty1*u1/det + ty2*u2/det + ty3*u3/det; + + int texture_x = tx/z*texture.width; + if (texture_x < 0) texture_x = 0; + if ((size_t) texture_x >= texture.width) texture_x = texture.width - 1; + + int texture_y = ty/z*texture.height; + if (texture_y < 0) texture_y = 0; + if ((size_t) texture_y >= texture.height) texture_y = texture.height - 1; + OLIVEC_PIXEL(oc, x, y) = OLIVEC_PIXEL(texture, (int)texture_x, (int)texture_y); + } + } + } + } +} + +OLIVECDEF void olivec_triangle3uv_bilinear(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float tx1, float ty1, float tx2, float ty2, float tx3, float ty3, float z1, float z2, float z3, Olivec_Canvas texture) +{ + int lx, hx, ly, hy; + if (olivec_normalize_triangle(oc.width, oc.height, x1, y1, x2, y2, x3, y3, &lx, &hx, &ly, &hy)) { + for (int y = ly; y <= hy; ++y) { + for (int x = lx; x <= hx; ++x) { + int u1, u2, det; + if (olivec_barycentric(x1, y1, x2, y2, x3, y3, x, y, &u1, &u2, &det)) { + int u3 = det - u1 - u2; + float z = z1*u1/det + z2*u2/det + z3*(det - u1 - u2)/det; + float tx = tx1*u1/det + tx2*u2/det + tx3*u3/det; + float ty = ty1*u1/det + ty2*u2/det + ty3*u3/det; + + float texture_x = tx/z*texture.width; + if (texture_x < 0) texture_x = 0; + if (texture_x >= (float) texture.width) texture_x = texture.width - 1; + + float texture_y = ty/z*texture.height; + if (texture_y < 0) texture_y = 0; + if (texture_y >= (float) texture.height) texture_y = texture.height - 1; + + int precision = 100; + OLIVEC_PIXEL(oc, x, y) = olivec_pixel_bilinear( + texture, + texture_x*precision, texture_y*precision, + precision, precision); + } + } + } + } +} + +// TODO: AA for triangle +OLIVECDEF void olivec_triangle(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, uint32_t color) +{ + int lx, hx, ly, hy; + if (olivec_normalize_triangle(oc.width, oc.height, x1, y1, x2, y2, x3, y3, &lx, &hx, &ly, &hy)) { + for (int y = ly; y <= hy; ++y) { + for (int x = lx; x <= hx; ++x) { + int u1, u2, det; + if (olivec_barycentric(x1, y1, x2, y2, x3, y3, x, y, &u1, &u2, &det)) { + olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), color); + } + } + } + } +} + +OLIVECDEF void olivec_text(Olivec_Canvas oc, const char *text, int tx, int ty, Olivec_Font font, size_t glyph_size, uint32_t color) +{ + for (size_t i = 0; *text; ++i, ++text) { + int gx = tx + i*font.width*glyph_size; + int gy = ty; + const char *glyph = &font.glyphs[(*text)*sizeof(char)*font.width*font.height]; + for (int dy = 0; (size_t) dy < font.height; ++dy) { + for (int dx = 0; (size_t) dx < font.width; ++dx) { + int px = gx + dx*glyph_size; + int py = gy + dy*glyph_size; + if (0 <= px && px < (int) oc.width && 0 <= py && py < (int) oc.height) { + if (glyph[dy*font.width + dx]) { + olivec_rect(oc, px, py, glyph_size, glyph_size, color); + } + } + } + } + } +} + +OLIVECDEF void olivec_sprite_blend(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite) +{ + if (sprite.width == 0) return; + if (sprite.height == 0) return; + + Olivec_Normalized_Rect nr = {0}; + if (!olivec_normalize_rect(x, y, w, h, oc.width, oc.height, &nr)) return; + + int xa = nr.ox1; + if (w < 0) xa = nr.ox2; + int ya = nr.oy1; + if (h < 0) ya = nr.oy2; + for (int y = nr.y1; y <= nr.y2; ++y) { + for (int x = nr.x1; x <= nr.x2; ++x) { + size_t nx = (x - xa)*((int) sprite.width)/w; + size_t ny = (y - ya)*((int) sprite.height)/h; + olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), OLIVEC_PIXEL(sprite, nx, ny)); + } + } +} + +OLIVECDEF void olivec_sprite_copy(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite) +{ + if (sprite.width == 0) return; + if (sprite.height == 0) return; + + // TODO: consider introducing flip parameter instead of relying on negative width and height + // Similar to how SDL_RenderCopyEx does that + Olivec_Normalized_Rect nr = {0}; + if (!olivec_normalize_rect(x, y, w, h, oc.width, oc.height, &nr)) return; + + int xa = nr.ox1; + if (w < 0) xa = nr.ox2; + int ya = nr.oy1; + if (h < 0) ya = nr.oy2; + for (int y = nr.y1; y <= nr.y2; ++y) { + for (int x = nr.x1; x <= nr.x2; ++x) { + size_t nx = (x - xa)*((int) sprite.width)/w; + size_t ny = (y - ya)*((int) sprite.height)/h; + OLIVEC_PIXEL(oc, x, y) = OLIVEC_PIXEL(sprite, nx, ny); + } + } +} + +// TODO: olivec_pixel_bilinear does not check for out-of-bounds +// But maybe it shouldn't. Maybe it's a responsibility of the caller of the function. +OLIVECDEF uint32_t olivec_pixel_bilinear(Olivec_Canvas sprite, int nx, int ny, int w, int h) +{ + int px = nx%w; + int py = ny%h; + + int x1 = nx/w, x2 = nx/w; + int y1 = ny/h, y2 = ny/h; + if (px < w/2) { + // left + px += w/2; + x1 -= 1; + if (x1 < 0) x1 = 0; + } else { + // right + px -= w/2; + x2 += 1; + if ((size_t) x2 >= sprite.width) x2 = sprite.width - 1; + } + + if (py < h/2) { + // top + py += h/2; + y1 -= 1; + if (y1 < 0) y1 = 0; + } else { + // bottom + py -= h/2; + y2 += 1; + if ((size_t) y2 >= sprite.height) y2 = sprite.height - 1; + } + + return mix_colors2(mix_colors2(OLIVEC_PIXEL(sprite, x1, y1), + OLIVEC_PIXEL(sprite, x2, y1), + px, w), + mix_colors2(OLIVEC_PIXEL(sprite, x1, y2), + OLIVEC_PIXEL(sprite, x2, y2), + px, w), + py, h); +} + +OLIVECDEF void olivec_sprite_copy_bilinear(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite) +{ + // TODO: support negative size in olivec_sprite_copy_bilinear() + if (w <= 0) return; + if (h <= 0) return; + + Olivec_Normalized_Rect nr = {0}; + if (!olivec_normalize_rect(x, y, w, h, oc.width, oc.height, &nr)) return; + + for (int y = nr.y1; y <= nr.y2; ++y) { + for (int x = nr.x1; x <= nr.x2; ++x) { + size_t nx = (x - nr.ox1)*sprite.width; + size_t ny = (y - nr.oy1)*sprite.height; + OLIVEC_PIXEL(oc, x, y) = olivec_pixel_bilinear(sprite, nx, ny, w, h); + } + } +} + +#endif // OLIVEC_IMPLEMENTATION + +// TODO: Benchmarking +// TODO: SIMD implementations +// TODO: bezier curves +// TODO: olivec_ring +// TODO: fuzzer +// TODO: Stencil diff --git a/android/app/src/main/jni/vc.c b/android/app/src/main/jni/vc.c new file mode 100644 index 0000000..521dce8 --- /dev/null +++ b/android/app/src/main/jni/vc.c @@ -0,0 +1,565 @@ +// C implementation of the Virtual Console (VC) for demos. +// +// # Usage +// ```c +// // demo.c +// // vc.c expectes render() to be defined and also supplies it's own entry point +// // if needed (some platforms like WASM_PLATFORM do not have the main() +// // entry point) +// #include "vc.c" +// +// #define WIDTH 800 +// #define HEIGHT 600 +// static uint32_t pixels[WIDTH*HEIGHT]; +// +// static Olivec_Canvas vc_render(float dt) +// { +// Olivec_Canvas oc = olivec_canvas(pixels, WIDTH, HEIGHT, WIDTH); +// // ... +// // ... render into oc ... +// // ... +// return oc; +// } +// ``` +// +// # Build +// ```console +// $ clang -o demo.sdl -DVC_PLATFORM=VC_SDL_PLATFORM demo.c -lSDL2 +// $ clang -o demo.term -DVC_PLATFORM=VC_TERM_PLATFORM demo.c +// $ clang -fno-builtin --target=wasm32 --no-standard-libraries -Wl,--no-entry -Wl,--export=render -Wl,--allow-undefined -o demo.wasm -DVC_PLATFORM=VC_WASM_PLATFORM demo.c +// ``` + +#define OLIVEC_IMPLEMENTATION +#include "olive.c" + +Olivec_Canvas vc_render(float dt); + +#ifndef VC_PLATFORM +#endif + +// Possible values of VC_PLATFORM +#define VC_WASM_PLATFORM 0 +#define VC_SDL_PLATFORM 1 +#define VC_TERM_PLATFORM 2 + +#if VC_PLATFORM == VC_SDL_PLATFORM +#include +#include + +#define return_defer(value) do { result = (value); goto defer; } while (0) + +static SDL_Texture *vc_sdl_texture = NULL; +static size_t vc_sdl_actual_width = 0; +static size_t vc_sdl_actual_height = 0; + +static bool vc_sdl_resize_texture(SDL_Renderer *renderer, size_t new_width, size_t new_height) +{ + if (vc_sdl_texture != NULL) SDL_DestroyTexture(vc_sdl_texture); + vc_sdl_actual_width = new_width; + vc_sdl_actual_height = new_height; + vc_sdl_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, vc_sdl_actual_width, vc_sdl_actual_height); + if (vc_sdl_texture == NULL) return false; + return true; +} + +int main(void) +{ + int result = 0; + + SDL_Window *window = NULL; + SDL_Renderer *renderer = NULL; + + { + if (SDL_Init(SDL_INIT_VIDEO) < 0) return_defer(1); + + window = SDL_CreateWindow("Olivec", 0, 0, 0, 0, 0); + if (window == NULL) return_defer(1); + + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + if (renderer == NULL) return_defer(1); + + Uint32 prev = SDL_GetTicks(); + bool pause = false; + for (;;) { + // Compute Delta Time + Uint32 curr = SDL_GetTicks(); + float dt = (curr - prev)/1000.f; + prev = curr; + + // Flush the events + SDL_Event event; + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_QUIT: { + return_defer(0); + } break; + case SDL_KEYDOWN: { + if (event.key.keysym.sym == SDLK_SPACE) pause = !pause; + } break; + } + } + + SDL_Rect window_rect = {0, 0, vc_sdl_actual_width, vc_sdl_actual_height}; + + if (!pause) { + // Render the texture + Olivec_Canvas oc_src = vc_render(dt); + if (oc_src.width != vc_sdl_actual_width || oc_src.height != vc_sdl_actual_height) { + if (!vc_sdl_resize_texture(renderer, oc_src.width, oc_src.height)) return_defer(1); + SDL_SetWindowSize(window, vc_sdl_actual_width, vc_sdl_actual_height); + } + void *pixels_dst; + int pitch; + if (SDL_LockTexture(vc_sdl_texture, &window_rect, &pixels_dst, &pitch) < 0) return_defer(1); + for (size_t y = 0; y < vc_sdl_actual_height; ++y) { + // TODO: it would be cool if Olivec_Canvas supported pitch in bytes instead of pixels + // It would be more flexible and we could draw on the locked texture memory directly + memcpy((char*)pixels_dst + y*pitch, oc_src.pixels + y*vc_sdl_actual_width, vc_sdl_actual_width*sizeof(uint32_t)); + } + SDL_UnlockTexture(vc_sdl_texture); + } + + // Display the texture + if (SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) < 0) return_defer(1); + if (SDL_RenderClear(renderer) < 0) return_defer(1); + if (SDL_RenderCopy(renderer, vc_sdl_texture, &window_rect, &window_rect) < 0) return_defer(1); + SDL_RenderPresent(renderer); + } + } + +defer: + switch (result) { + case 0: + printf("OK\n"); + break; + default: + fprintf(stderr, "SDL ERROR: %s\n", SDL_GetError()); + } + if (vc_sdl_texture) SDL_DestroyTexture(vc_sdl_texture); + if (renderer) SDL_DestroyRenderer(renderer); + if (window) SDL_DestroyWindow(window); + SDL_Quit(); + return result; +} +#elif VC_PLATFORM == VC_TERM_PLATFORM + +#include +#include +#include +#include +#include +#include +#include +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" + +static size_t vc_term_actual_width = 0; +static size_t vc_term_actual_height = 0; +static size_t vc_term_scaled_down_width = 0; +static size_t vc_term_scaled_down_height = 0; +static int *vc_term_char_canvas = 0; + +int hsl256[][3] = { + {0, 0, 0}, + {0, 100, 25}, + {120, 100, 25}, + {60, 100, 25}, + {240, 100, 25}, + {300, 100, 25}, + {180, 100, 25}, + {0, 0, 75}, + {0, 0, 50}, + {0, 100, 50}, + {120, 100, 50}, + {60, 100, 50}, + {240, 100, 50}, + {300, 100, 50}, + {180, 100, 50}, + {0, 0, 100}, + {0, 0, 0}, + {240, 99, 18}, + {240, 100, 26}, + {240, 100, 34}, + {240, 100, 42}, + {240, 100, 50}, + {120, 99, 18}, + {180, 99, 18}, + {197, 100, 26}, + {207, 100, 34}, + {213, 100, 42}, + {217, 100, 50}, + {120, 100, 26}, + {162, 100, 26}, + {180, 100, 26}, + {193, 100, 34}, + {202, 100, 42}, + {208, 100, 50}, + {120, 100, 34}, + {152, 100, 34}, + {166, 100, 34}, + {180, 100, 34}, + {191, 100, 42}, + {198, 100, 50}, + {120, 100, 42}, + {146, 100, 42}, + {157, 100, 42}, + {168, 100, 42}, + {180, 100, 42}, + {189, 100, 50}, + {120, 100, 50}, + {142, 100, 50}, + {151, 100, 50}, + {161, 100, 50}, + {170, 100, 50}, + {180, 100, 50}, + {0, 99, 18}, + {300, 99, 18}, + {282, 100, 26}, + {272, 100, 34}, + {266, 100, 42}, + {262, 100, 50}, + {60, 99, 18}, + {0, 0, 37}, + {240, 17, 45}, + {240, 33, 52}, + {240, 60, 60}, + {240, 100, 68}, + {77, 100, 26}, + {120, 17, 45}, + {180, 17, 45}, + {210, 33, 52}, + {220, 60, 60}, + {225, 100, 68}, + {87, 100, 34}, + {120, 33, 52}, + {150, 33, 52}, + {180, 33, 52}, + {200, 60, 60}, + {210, 100, 68}, + {93, 100, 42}, + {120, 60, 60}, + {140, 60, 60}, + {160, 60, 60}, + {180, 60, 60}, + {195, 100, 68}, + {97, 100, 50}, + {120, 100, 68}, + {135, 100, 68}, + {150, 100, 68}, + {165, 100, 68}, + {180, 100, 68}, + {0, 100, 26}, + {317, 100, 26}, + {300, 100, 26}, + {286, 100, 34}, + {277, 100, 42}, + {271, 100, 50}, + {42, 100, 26}, + {0, 17, 45}, + {300, 17, 45}, + {270, 33, 52}, + {260, 60, 60}, + {255, 100, 68}, + {60, 100, 26}, + {60, 17, 45}, + {0, 0, 52}, + {240, 20, 60}, + {240, 50, 68}, + {240, 100, 76}, + {73, 100, 34}, + {90, 33, 52}, + {120, 20, 60}, + {180, 20, 60}, + {210, 50, 68}, + {220, 100, 76}, + {82, 100, 42}, + {100, 60, 60}, + {120, 50, 68}, + {150, 50, 68}, + {180, 50, 68}, + {200, 100, 76}, + {88, 100, 50}, + {105, 100, 68}, + {120, 100, 76}, + {140, 100, 76}, + {160, 100, 76}, + {180, 100, 76}, + {0, 100, 34}, + {327, 100, 34}, + {313, 100, 34}, + {300, 100, 34}, + {288, 100, 42}, + {281, 100, 50}, + {32, 100, 34}, + {0, 33, 52}, + {330, 33, 52}, + {300, 33, 52}, + {280, 60, 60}, + {270, 100, 68}, + {46, 100, 34}, + {30, 33, 52}, + {0, 20, 60}, + {300, 20, 60}, + {270, 50, 68}, + {260, 100, 76}, + {60, 100, 34}, + {60, 33, 52}, + {60, 20, 60}, + {0, 0, 68}, + {240, 33, 76}, + {240, 100, 84}, + {71, 100, 42}, + {80, 60, 60}, + {90, 50, 68}, + {120, 33, 76}, + {180, 33, 76}, + {210, 100, 84}, + {78, 100, 50}, + {90, 100, 68}, + {100, 100, 76}, + {120, 100, 84}, + {150, 100, 84}, + {180, 100, 84}, + {0, 100, 42}, + {333, 100, 42}, + {322, 100, 42}, + {311, 100, 42}, + {300, 100, 42}, + {290, 100, 50}, + {26, 100, 42}, + {0, 60, 60}, + {340, 60, 60}, + {320, 60, 60}, + {300, 60, 60}, + {285, 100, 68}, + {37, 100, 42}, + {20, 60, 60}, + {0, 50, 68}, + {330, 50, 68}, + {300, 50, 68}, + {280, 100, 76}, + {48, 100, 42}, + {40, 60, 60}, + {30, 50, 68}, + {0, 33, 76}, + {300, 33, 76}, + {270, 100, 84}, + {60, 100, 42}, + {60, 60, 60}, + {60, 50, 68}, + {60, 33, 76}, + {0, 0, 84}, + {240, 100, 92}, + {69, 100, 50}, + {75, 100, 68}, + {80, 100, 76}, + {90, 100, 84}, + {120, 100, 92}, + {180, 100, 92}, + {0, 100, 50}, + {337, 100, 50}, + {328, 100, 50}, + {318, 100, 50}, + {309, 100, 50}, + {300, 100, 50}, + {22, 100, 50}, + {0, 100, 68}, + {345, 100, 68}, + {330, 100, 68}, + {315, 100, 68}, + {300, 100, 68}, + {31, 100, 50}, + {15, 100, 68}, + {0, 100, 76}, + {340, 100, 76}, + {320, 100, 76}, + {300, 100, 76}, + {41, 100, 50}, + {30, 100, 68}, + {20, 100, 76}, + {0, 100, 84}, + {330, 100, 84}, + {300, 100, 84}, + {50, 100, 50}, + {45, 100, 68}, + {40, 100, 76}, + {30, 100, 84}, + {0, 100, 92}, + {300, 100, 92}, + {60, 100, 50}, + {60, 100, 68}, + {60, 100, 76}, + {60, 100, 84}, + {60, 100, 92}, + {0, 0, 100}, + {0, 0, 3}, + {0, 0, 7}, + {0, 0, 10}, + {0, 0, 14}, + {0, 0, 18}, + {0, 0, 22}, + {0, 0, 26}, + {0, 0, 30}, + {0, 0, 34}, + {0, 0, 38}, + {0, 0, 42}, + {0, 0, 46}, + {0, 0, 50}, + {0, 0, 54}, + {0, 0, 58}, + {0, 0, 61}, + {0, 0, 65}, + {0, 0, 69}, + {0, 0, 73}, + {0, 0, 77}, + {0, 0, 81}, + {0, 0, 85}, + {0, 0, 89}, + {0, 0, 93}, +}; + +int distance_hsl256(int i, int h, int s, int l) +{ + int dh = h - hsl256[i][0]; + int ds = s - hsl256[i][1]; + int dl = l - hsl256[i][2]; + return dh*dh + ds*ds + dl*dl; +} + +// TODO: bring find_ansi_index_by_rgb from image2term +int find_ansi_index_by_hsl(int h, int s, int l) +{ + int index = 0; + for (int i = 0; i < 256; ++i) { + if (distance_hsl256(i, h, s, l) < distance_hsl256(index, h, s, l)) { + index = i; + } + } + return index; +} + +static uint32_t vc_term_compress_pixels_chunk(Olivec_Canvas oc) +{ + size_t r = 0; + size_t g = 0; + size_t b = 0; + size_t a = 0; + + for (size_t y = 0; y < oc.height; ++y) { + for (size_t x = 0; x < oc.width; ++x) { + r += OLIVEC_RED(OLIVEC_PIXEL(oc, x, y)); + g += OLIVEC_GREEN(OLIVEC_PIXEL(oc, x, y)); + b += OLIVEC_BLUE(OLIVEC_PIXEL(oc, x, y)); + a += OLIVEC_ALPHA(OLIVEC_PIXEL(oc, x, y)); + } + } + + r /= oc.width*oc.height; + g /= oc.width*oc.height; + b /= oc.width*oc.height; + a /= oc.width*oc.height; + + return OLIVEC_RGBA(r, g, b, a); +} + +#ifndef VC_TERM_SCALE_DOWN_FACTOR +#define VC_TERM_SCALE_DOWN_FACTOR 20 +#endif // VC_TERM_SCALE_DOWN_FACTOR + +static void vc_term_resize_char_canvas(size_t new_width, size_t new_height) +{ + // TODO: warn the user if vc_term_actual_width does not fit into the screen + // TODO: can we just do something so the divisibility is not important? + // Like round the stuff or something? + // Or we can resize the frame on the fly similarly to how we resize sprites in olivec_sprite_*() functions. + assert(new_width%VC_TERM_SCALE_DOWN_FACTOR == 0 && "Width must be divisible by VC_TERM_SCALE_DOWN_FACTOR"); + assert(new_height%VC_TERM_SCALE_DOWN_FACTOR == 0 && "Height must be divisible by VC_TERM_SCALE_DOWN_FACTOR"); + vc_term_actual_width = new_width; + vc_term_actual_height = new_height; + vc_term_scaled_down_width = vc_term_actual_width/VC_TERM_SCALE_DOWN_FACTOR; + vc_term_scaled_down_height = vc_term_actual_height/VC_TERM_SCALE_DOWN_FACTOR; + free(vc_term_char_canvas); + vc_term_char_canvas = malloc(sizeof(*vc_term_char_canvas)*vc_term_scaled_down_width*vc_term_scaled_down_height); + assert(vc_term_char_canvas != NULL && "Just buy more RAM"); +} + +void rgb_to_hsl(int r, int g, int b, int *h, int *s, int *l) +{ + float r01 = r/255.0f; + float g01 = g/255.0f; + float b01 = b/255.0f; + float cmax = r01; + if (g01 > cmax) cmax = g01; + if (b01 > cmax) cmax = b01; + float cmin = r01; + if (g01 < cmin) cmin = g01; + if (b01 < cmin) cmin = b01; + float delta = cmax - cmin; + float epsilon = 1e-6; + float hf = 0; + if (delta < epsilon) hf = 0; + else if (cmax == r01) hf = 60.0f*fmod((g01 - b01)/delta, 6.0f); + else if (cmax == g01) hf = 60.0f*((b01 - r01)/delta + 2); + else if (cmax == b01) hf = 60.0f*((r01 - g01)/delta + 4); + else assert(0 && "unreachable"); + + float lf = (cmax + cmin)/2; + + float sf = 0; + if (delta < epsilon) sf = 0; + else sf = delta/(1 - fabsf(2*lf - 1)); + + *h = fmodf(fmodf(hf, 360.0f) + 360.0f, 360.0f); + *s = sf*100.0f; + *l = lf*100.0f; +} + +static void vc_term_compress_pixels(Olivec_Canvas oc) +{ + if (vc_term_actual_width != oc.width || vc_term_actual_height != oc.height) { + vc_term_resize_char_canvas(oc.width, oc.height); + } + + for (size_t y = 0; y < vc_term_scaled_down_height; ++y) { + for (size_t x = 0; x < vc_term_scaled_down_width; ++x) { + Olivec_Canvas soc = olivec_subcanvas(oc, x*VC_TERM_SCALE_DOWN_FACTOR, y*VC_TERM_SCALE_DOWN_FACTOR, VC_TERM_SCALE_DOWN_FACTOR, VC_TERM_SCALE_DOWN_FACTOR); + uint32_t cp = vc_term_compress_pixels_chunk(soc); + int r = OLIVEC_RED(cp); + int g = OLIVEC_GREEN(cp); + int b = OLIVEC_BLUE(cp); + int a = OLIVEC_ALPHA(cp); + r = a*r/255; + g = a*g/255; + b = a*b/255; + int h, s, l; + rgb_to_hsl(r, g, b, &h, &s, &l); + vc_term_char_canvas[y*vc_term_scaled_down_width + x] = find_ansi_index_by_hsl(h, s, l); + } + } +} + +int main(void) +{ + for (;;) { + vc_term_compress_pixels(vc_render(1.f/60.f)); + for (size_t y = 0; y < vc_term_scaled_down_height; ++y) { + for (size_t x = 0; x < vc_term_scaled_down_width; ++x) { + // TODO: explore the idea of figuring out aspect ratio of the character using escape ANSI codes of the terminal and rendering the image accordingly + printf("\033[48;5;%dm ", vc_term_char_canvas[y*vc_term_scaled_down_width + x]); + } + printf("\033[0m\n"); + } + + usleep(1000*1000/60); + printf("\033[%zuA", vc_term_scaled_down_height); + printf("\033[%zuD", vc_term_scaled_down_width); + } + return 0; +} +#elif VC_PLATFORM == VC_WASM_PLATFORM +// Do nothing because all the work is done in ../js/vc.js +#else +#error "Unknown VC platform" +#endif // VC_SDL_PLATFORM \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/me/app/MainActivity.kt b/android/app/src/main/kotlin/com/me/app/MainActivity.kt new file mode 100644 index 0000000..fa2eece --- /dev/null +++ b/android/app/src/main/kotlin/com/me/app/MainActivity.kt @@ -0,0 +1,65 @@ +package com.me.app + +import android.graphics.Bitmap +import android.os.Bundle +import android.view.WindowManager +import androidx.appcompat.app.AppCompatActivity +import androidx.core.graphics.createBitmap +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.lifecycle.lifecycleScope +import com.me.app.databinding.ActivityMainBinding +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.lang.Thread.sleep + + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + + // == TAKEN FROM C SIDE == // + private val dimen = 1100 + + init { + System.loadLibrary("olivest") + } + + + external fun drawLines(bmp: Bitmap) + external fun drawTriangle(bmp: Bitmap) + + + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen().setKeepOnScreenCondition { + sleep(800) + false + } + super.onCreate(savedInstanceState) + WindowInsetsControllerCompat(window, window.decorView).let { controller -> + controller.hide(WindowInsetsCompat.Type.systemBars()) + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + + val bitmap = createBitmap(dimen, dimen) + + // == CALL OLIVE.c AND FILL IN THE BUFFER == // + drawTriangle(bitmap) + binding.surface.setImageBitmap(bitmap) + + lifecycleScope.launch { + repeat(Int.MAX_VALUE) { + delay(10) + drawTriangle(bitmap) + binding.surface.setImageBitmap(bitmap) + + } + } + } +} diff --git a/android/app/src/main/res/drawable/main_ic.xml b/android/app/src/main/res/drawable/main_ic.xml new file mode 100644 index 0000000..08819ec --- /dev/null +++ b/android/app/src/main/res/drawable/main_ic.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/mpm_bg.xml b/android/app/src/main/res/drawable/mpm_bg.xml new file mode 100644 index 0000000..72644bf --- /dev/null +++ b/android/app/src/main/res/drawable/mpm_bg.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..691960f --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/mpm_ic.xml b/android/app/src/main/res/mipmap-anydpi-v26/mpm_ic.xml new file mode 100644 index 0000000..51f04d2 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/mpm_ic.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..2500e8e --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,12 @@ + + + + #181818 + + + + \ No newline at end of file diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..06e4514 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false +} \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..3d2bd1c --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official +android.nonTransitiveRClass=true +android.enableJetifier=true \ No newline at end of file diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml new file mode 100644 index 0000000..8667ea6 --- /dev/null +++ b/android/gradle/libs.versions.toml @@ -0,0 +1,9 @@ +[versions] +agp = "8.12.2" +kotlin = "2.2.10" + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } + diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2733ed5 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 0000000..39b8bff --- /dev/null +++ b/android/gradlew @@ -0,0 +1,161 @@ +#!/usr/bin/env sh + +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..45c9d23 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,28 @@ +@file:Suppress("UnstableApiUsage") +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + maven("https://jitpack.io") + maven("https://repo.eclipse.org/content/repositories/paho-releases/") + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} + +rootProject.name = "android" +include(":app") + \ No newline at end of file From 6287780117fbcf8122b890b0d2fa9b9a34f9a206 Mon Sep 17 00:00:00 2001 From: daniele-NA Date: Thu, 12 Feb 2026 19:28:16 +0100 Subject: [PATCH 2/3] android/README.md --- .gitignore | 3 +- android/README.md | 33 + android/app/src/main/jni/CMakeLists.txt | 3 +- android/app/src/main/jni/main.c | 4 +- android/app/src/main/jni/olive.c | 1022 ----------------- android/app/src/main/jni/vc.c | 565 --------- .../main/kotlin/com/me/app/MainActivity.kt | 5 +- 7 files changed, 42 insertions(+), 1593 deletions(-) delete mode 100644 android/app/src/main/jni/olive.c delete mode 100644 android/app/src/main/jni/vc.c diff --git a/.gitignore b/.gitignore index 4d2c64e..ab60f05 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ build/ *.swp nobuild nob -*.old \ No newline at end of file +*.old +.idea \ No newline at end of file diff --git a/android/README.md b/android/README.md index e69de29..910f335 100644 --- a/android/README.md +++ b/android/README.md @@ -0,0 +1,33 @@ +# Olive.c – Android Example + +

+ +

+ +Minimal Android demo using **Olive.c** as a CPU renderer. + +## Idea + +Olive.c renders directly into the memory of an Android `Bitmap`. + +* lock bitmap → get pixels pointer +* use pointer as `Olivec_Canvas` +* draw +* unlock → display + +No copies. No return values. The bitmap memory is the render target. + +## Loop + +* refresh every ~10 ms +* render frame +* apply rotation + +## Native (pseudo) + +```c +void *bitmapPixels; +AndroidBitmap_lockPixels(env, bmp, &bitmapPixels); +memcpy(bitmapPixels, pixels, sizeof(pixels)); +AndroidBitmap_unlockPixels(env, bmp); +``` \ No newline at end of file diff --git a/android/app/src/main/jni/CMakeLists.txt b/android/app/src/main/jni/CMakeLists.txt index 1358347..0d30743 100644 --- a/android/app/src/main/jni/CMakeLists.txt +++ b/android/app/src/main/jni/CMakeLists.txt @@ -6,8 +6,7 @@ set(CMAKE_CXX_STANDARD 17) add_library(olivest SHARED main.c - olive.c - vc.c + ../../../../../olive.c ) find_library(jnigraphics-lib jnigraphics) diff --git a/android/app/src/main/jni/main.c b/android/app/src/main/jni/main.c index 5577948..5025ec7 100644 --- a/android/app/src/main/jni/main.c +++ b/android/app/src/main/jni/main.c @@ -8,7 +8,7 @@ #define OLIVEC_IMPLEMENTATION -#include "olive.c" +#include "../../../../../olive.c" /* Canvas */ #define WIDTH 1100 @@ -134,7 +134,7 @@ Java_com_me_app_MainActivity_drawTriangle(JNIEnv *env, jobject thiz, jobject bmp circle_y = ny; } - // == DRAWING TH CIRCLE WITHIN THE TRIANGLE == // + // == DRAWING THE CIRCLE WITHIN THE TRIANGLE == // olivec_circle(oc, (int) circle_x, (int) circle_y, CIRCLE_RADIUS, CIRCLE_COLOR); // == CREATE THE BITMAP WITH THE GIVEN POINTER == // diff --git a/android/app/src/main/jni/olive.c b/android/app/src/main/jni/olive.c deleted file mode 100644 index fe06754..0000000 --- a/android/app/src/main/jni/olive.c +++ /dev/null @@ -1,1022 +0,0 @@ -// Copyright 2022 Alexey Kutepov -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#ifndef OLIVE_C_ -#define OLIVE_C_ - -#include -#include -#include - -#ifndef OLIVECDEF -#define OLIVECDEF static inline -#endif - -#ifndef OLIVEC_AA_RES -#define OLIVEC_AA_RES 2 -#endif - -#define OLIVEC_SWAP(T, a, b) do { T t = a; a = b; b = t; } while (0) -#define OLIVEC_SIGN(T, x) ((T)((x) > 0) - (T)((x) < 0)) -#define OLIVEC_ABS(T, x) (OLIVEC_SIGN(T, x)*(x)) - -typedef struct { - size_t width, height; - const char *glyphs; -} Olivec_Font; - -#define OLIVEC_DEFAULT_FONT_HEIGHT 6 -#define OLIVEC_DEFAULT_FONT_WIDTH 6 -// TODO: allocate proper descender and acender areas for the default font -static char olivec_default_glyphs[128][OLIVEC_DEFAULT_FONT_HEIGHT][OLIVEC_DEFAULT_FONT_WIDTH] = { - ['a'] = { - {0, 0, 0, 0, 0}, - {0, 1, 1, 0, 0}, - {0, 0, 0, 1, 0}, - {0, 1, 1, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 1, 0}, - }, - ['b'] = { - {1, 0, 0, 0, 0}, - {1, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 1, 1, 0, 0}, - }, - ['c'] = { - {0, 0, 0, 0, 0}, - {0, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 0, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - }, - ['d'] = { - {0, 0, 0, 1, 0}, - {0, 1, 1, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 1, 0}, - }, - ['e'] = { - {0, 0, 0, 0, 0}, - {0, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 1, 1, 1, 0}, - {1, 0, 0, 0, 0}, - {0, 1, 1, 1, 0}, - }, - ['f'] = { - {0, 0, 1, 1, 0}, - {0, 1, 0, 0, 0}, - {1, 1, 1, 1, 0}, - {0, 1, 0, 0, 0}, - {0, 1, 0, 0, 0}, - {0, 1, 0, 0, 0}, - }, - ['g'] = { - {0, 1, 1, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 1, 0}, - {0, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - }, - ['h'] = { - {1, 0, 0, 0, 0}, - {1, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - }, - ['i'] = { - {0, 0, 1, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 1, 0, 0}, - {0, 0, 1, 0, 0}, - {0, 0, 1, 0, 0}, - {0, 0, 1, 0, 0}, - }, - ['j'] = { - {0, 0, 1, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 1, 0, 0}, - {0, 0, 1, 0, 0}, - {1, 0, 1, 0, 0}, - {0, 1, 1, 0, 0}, - }, - ['k'] = { - {1, 0, 0, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 1, 0, 0}, - {1, 1, 0, 0, 0}, - {1, 0, 1, 0, 0}, - {1, 0, 0, 1, 0}, - }, - ['l'] = { - {0, 1, 1, 0, 0}, - {0, 0, 1, 0, 0}, - {0, 0, 1, 0, 0}, - {0, 0, 1, 0, 0}, - {0, 0, 1, 0, 0}, - {0, 1, 1, 1, 0}, - }, - ['m'] = { - {0, 0, 0, 0, 0}, - {0, 1, 0, 1, 1}, - {1, 0, 1, 0, 1}, - {1, 0, 1, 0, 1}, - {1, 0, 1, 0, 1}, - {1, 0, 1, 0, 1}, - }, - ['n'] = { - {0, 0, 0, 0, 0}, - {0, 1, 1, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - }, - ['o'] = { - {0, 0, 0, 0, 0}, - {0, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - }, - ['p'] = { - {1, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 1, 1, 0, 0}, - {1, 0, 0, 0, 0}, - {1, 0, 0, 0, 0}, - }, - ['q'] = { - {0, 1, 1, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 1, 0}, - {0, 0, 0, 1, 0}, - {0, 0, 0, 1, 0}, - }, - ['r'] = { - {0, 0, 0, 0, 0}, - {1, 0, 1, 1, 0}, - {1, 1, 0, 0, 1}, - {1, 0, 0, 0, 0}, - {1, 0, 0, 0, 0}, - {1, 0, 0, 0, 0}, - }, - ['s'] = { - {0, 0, 0, 0, 0}, - {0, 1, 1, 1, 0}, - {1, 0, 0, 0, 0}, - {1, 1, 1, 1, 0}, - {0, 0, 0, 1, 0}, - {1, 1, 1, 0, 0}, - }, - ['t'] = { - {0, 1, 0, 0, 0}, - {0, 1, 0, 0, 0}, - {1, 1, 1, 1, 0}, - {0, 1, 0, 0, 0}, - {0, 1, 0, 1, 0}, - {0, 1, 1, 0, 0}, - }, - ['u'] = { - {0, 0, 0, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 1, 0}, - }, - ['v'] = { - {0, 0, 0, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - }, - ['w'] = { - {0, 0, 0, 0, 0}, - {1, 0, 0, 0, 1}, - {1, 0, 1, 0, 1}, - {1, 0, 1, 0, 1}, - {1, 0, 1, 0, 1}, - {0, 1, 1, 1, 1}, - }, - ['x'] = { - {0, 0, 0, 0, 0}, - {1, 0, 1, 0, 0}, - {1, 0, 1, 0, 0}, - {0, 1, 0, 0, 0}, - {1, 0, 1, 0, 0}, - {1, 0, 1, 0, 0}, - }, - ['y'] = { - {0, 0, 0, 0, 0}, - {1, 0, 1, 0, 0}, - {1, 0, 1, 0, 0}, - {1, 0, 1, 0, 0}, - {0, 1, 0, 0, 0}, - {0, 1, 0, 0, 0}, - }, - ['z'] = { - {0, 0, 0, 0, 0}, - {1, 1, 1, 1, 0}, - {0, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - {1, 0, 0, 0, 0}, - {1, 1, 1, 1, 0}, - }, - - ['A'] = {0}, - ['B'] = {0}, - ['C'] = {0}, - ['D'] = {0}, - ['E'] = {0}, - ['F'] = {0}, - ['G'] = {0}, - ['H'] = {0}, - ['I'] = {0}, - ['J'] = {0}, - ['K'] = {0}, - ['L'] = {0}, - ['M'] = {0}, - ['N'] = {0}, - ['O'] = {0}, - ['P'] = {0}, - ['Q'] = {0}, - ['R'] = {0}, - ['S'] = {0}, - ['T'] = {0}, - ['U'] = {0}, - ['V'] = {0}, - ['W'] = {0}, - ['X'] = {0}, - ['Y'] = {0}, - ['Z'] = {0}, - - ['0'] = { - {0, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - }, - ['1'] = { - {0, 0, 1, 0, 0}, - {0, 1, 1, 0, 0}, - {0, 0, 1, 0, 0}, - {0, 0, 1, 0, 0}, - {0, 0, 1, 0, 0}, - {0, 1, 1, 1, 0}, - }, - ['2'] = { - {0, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {0, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - {1, 0, 0, 0, 0}, - {1, 1, 1, 1, 0}, - }, - ['3'] = { - {0, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {0, 0, 1, 0, 0}, - {0, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - }, - ['4'] = { - {0, 0, 1, 1, 0}, - {0, 1, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {1, 1, 1, 1, 1}, - {0, 0, 0, 1, 0}, - {0, 0, 0, 1, 0}, - }, - ['5'] = { - {1, 1, 1, 0, 0}, - {1, 0, 0, 0, 0}, - {1, 1, 1, 0, 0}, - {0, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - }, - ['6'] = { - {0, 1, 1, 0, 0}, - {1, 0, 0, 0, 0}, - {1, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - }, - ['7'] = { - {1, 1, 1, 1, 0}, - {0, 0, 0, 1, 0}, - {0, 0, 1, 0, 0}, - {0, 1, 0, 0, 0}, - {0, 1, 0, 0, 0}, - {0, 1, 0, 0, 0}, - }, - ['8'] = { - {0, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - - }, - ['9'] = { - {0, 1, 1, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 1, 0}, - {0, 1, 1, 1, 0}, - {0, 0, 0, 1, 0}, - {0, 1, 1, 0, 0}, - }, - - [','] = { - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 1, 0}, - {0, 0, 1, 0, 0}, - }, - - ['.'] = { - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 1, 0, 0}, - }, - ['-'] = { - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {1, 1, 1, 1, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - }, -}; - -static Olivec_Font olivec_default_font = { - .glyphs = &olivec_default_glyphs[0][0][0], - .width = OLIVEC_DEFAULT_FONT_WIDTH, - .height = OLIVEC_DEFAULT_FONT_HEIGHT, -}; - -// WARNING! Always initialize your Canvas with a color that has Non-Zero Alpha Channel! -// A lot of functions use `olivec_blend_color()` function to blend with the Background -// which preserves the original Alpha of the Background. So you may easily end up with -// a result that is perceptually transparent if the Alpha is Zero. -typedef struct { - uint32_t *pixels; - size_t width; - size_t height; - size_t stride; -} Olivec_Canvas; - -#define OLIVEC_CANVAS_NULL ((Olivec_Canvas) {0}) -#define OLIVEC_PIXEL(oc, x, y) (oc).pixels[(y)*(oc).stride + (x)] - -OLIVECDEF Olivec_Canvas olivec_canvas(uint32_t *pixels, size_t width, size_t height, size_t stride); -OLIVECDEF Olivec_Canvas olivec_subcanvas(Olivec_Canvas oc, int x, int y, int w, int h); -OLIVECDEF bool olivec_in_bounds(Olivec_Canvas oc, int x, int y); -OLIVECDEF void olivec_blend_color(uint32_t *c1, uint32_t c2); -OLIVECDEF void olivec_fill(Olivec_Canvas oc, uint32_t color); -OLIVECDEF void olivec_rect(Olivec_Canvas oc, int x, int y, int w, int h, uint32_t color); -OLIVECDEF void olivec_frame(Olivec_Canvas oc, int x, int y, int w, int h, size_t thiccness, uint32_t color); -OLIVECDEF void olivec_circle(Olivec_Canvas oc, int cx, int cy, int r, uint32_t color); -OLIVECDEF void olivec_ellipse(Olivec_Canvas oc, int cx, int cy, int rx, int ry, uint32_t color); -// TODO: lines with different thiccness -OLIVECDEF void olivec_line(Olivec_Canvas oc, int x1, int y1, int x2, int y2, uint32_t color); -OLIVECDEF bool olivec_normalize_triangle(size_t width, size_t height, int x1, int y1, int x2, int y2, int x3, int y3, int *lx, int *hx, int *ly, int *hy); -OLIVECDEF bool olivec_barycentric(int x1, int y1, int x2, int y2, int x3, int y3, int xp, int yp, int *u1, int *u2, int *det); -OLIVECDEF void olivec_triangle(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, uint32_t color); -OLIVECDEF void olivec_triangle3c(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, uint32_t c1, uint32_t c2, uint32_t c3); -OLIVECDEF void olivec_triangle3z(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float z1, float z2, float z3); -OLIVECDEF void olivec_triangle3uv(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float tx1, float ty1, float tx2, float ty2, float tx3, float ty3, float z1, float z2, float z3, Olivec_Canvas texture); -OLIVECDEF void olivec_triangle3uv_bilinear(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float tx1, float ty1, float tx2, float ty2, float tx3, float ty3, float z1, float z2, float z3, Olivec_Canvas texture); -OLIVECDEF void olivec_text(Olivec_Canvas oc, const char *text, int x, int y, Olivec_Font font, size_t size, uint32_t color); -OLIVECDEF void olivec_sprite_blend(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite); -OLIVECDEF void olivec_sprite_copy(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite); -OLIVECDEF void olivec_sprite_copy_bilinear(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite); -OLIVECDEF uint32_t olivec_pixel_bilinear(Olivec_Canvas sprite, int nx, int ny, int w, int h); - -typedef struct { - // Safe ranges to iterate over. - int x1, x2; - int y1, y2; - - // Original uncut ranges some parts of which may be outside of the canvas boundaries. - int ox1, ox2; - int oy1, oy2; -} Olivec_Normalized_Rect; - -// The point of this function is to produce two ranges x1..x2 and y1..y2 that are guaranteed to be safe to iterate over the canvas of size pixels_width by pixels_height without any boundary checks. -// -// Olivec_Normalized_Rect nr = {0}; -// if (olivec_normalize_rect(x, y, w, h, WIDTH, HEIGHT, &nr)) { -// for (int x = nr.x1; x <= nr.x2; ++x) { -// for (int y = nr.y1; y <= nr.y2; ++y) { -// OLIVEC_PIXEL(oc, x, y) = 0x69696969; -// } -// } -// } else { -// // Rectangle is invisible cause it's completely out-of-bounds -// } -OLIVECDEF bool olivec_normalize_rect(int x, int y, int w, int h, - size_t canvas_width, size_t canvas_height, - Olivec_Normalized_Rect *nr); - -#endif // OLIVE_C_ - -#ifdef OLIVEC_IMPLEMENTATION - -OLIVECDEF Olivec_Canvas olivec_canvas(uint32_t *pixels, size_t width, size_t height, size_t stride) -{ - Olivec_Canvas oc = { - .pixels = pixels, - .width = width, - .height = height, - .stride = stride, - }; - return oc; -} - -OLIVECDEF bool olivec_normalize_rect(int x, int y, int w, int h, - size_t canvas_width, size_t canvas_height, - Olivec_Normalized_Rect *nr) -{ - // No need to render empty rectangle - if (w == 0) return false; - if (h == 0) return false; - - nr->ox1 = x; - nr->oy1 = y; - - // Convert the rectangle to 2-points representation - nr->ox2 = nr->ox1 + OLIVEC_SIGN(int, w)*(OLIVEC_ABS(int, w) - 1); - if (nr->ox1 > nr->ox2) OLIVEC_SWAP(int, nr->ox1, nr->ox2); - nr->oy2 = nr->oy1 + OLIVEC_SIGN(int, h)*(OLIVEC_ABS(int, h) - 1); - if (nr->oy1 > nr->oy2) OLIVEC_SWAP(int, nr->oy1, nr->oy2); - - // Cull out invisible rectangle - if (nr->ox1 >= (int) canvas_width) return false; - if (nr->ox2 < 0) return false; - if (nr->oy1 >= (int) canvas_height) return false; - if (nr->oy2 < 0) return false; - - nr->x1 = nr->ox1; - nr->y1 = nr->oy1; - nr->x2 = nr->ox2; - nr->y2 = nr->oy2; - - // Clamp the rectangle to the boundaries - if (nr->x1 < 0) nr->x1 = 0; - if (nr->x2 >= (int) canvas_width) nr->x2 = (int) canvas_width - 1; - if (nr->y1 < 0) nr->y1 = 0; - if (nr->y2 >= (int) canvas_height) nr->y2 = (int) canvas_height - 1; - - return true; -} - -OLIVECDEF Olivec_Canvas olivec_subcanvas(Olivec_Canvas oc, int x, int y, int w, int h) -{ - Olivec_Normalized_Rect nr = {0}; - if (!olivec_normalize_rect(x, y, w, h, oc.width, oc.height, &nr)) return OLIVEC_CANVAS_NULL; - oc.pixels = &OLIVEC_PIXEL(oc, nr.x1, nr.y1); - oc.width = nr.x2 - nr.x1 + 1; - oc.height = nr.y2 - nr.y1 + 1; - return oc; -} - -// TODO: custom pixel formats -// Maybe we can store pixel format info in Olivec_Canvas -#define OLIVEC_RED(color) (((color)&0x000000FF)>>(8*0)) -#define OLIVEC_GREEN(color) (((color)&0x0000FF00)>>(8*1)) -#define OLIVEC_BLUE(color) (((color)&0x00FF0000)>>(8*2)) -#define OLIVEC_ALPHA(color) (((color)&0xFF000000)>>(8*3)) -#define OLIVEC_RGBA(r, g, b, a) ((((r)&0xFF)<<(8*0)) | (((g)&0xFF)<<(8*1)) | (((b)&0xFF)<<(8*2)) | (((a)&0xFF)<<(8*3))) - -OLIVECDEF void olivec_blend_color(uint32_t *c1, uint32_t c2) -{ - uint32_t r1 = OLIVEC_RED(*c1); - uint32_t g1 = OLIVEC_GREEN(*c1); - uint32_t b1 = OLIVEC_BLUE(*c1); - uint32_t a1 = OLIVEC_ALPHA(*c1); - - uint32_t r2 = OLIVEC_RED(c2); - uint32_t g2 = OLIVEC_GREEN(c2); - uint32_t b2 = OLIVEC_BLUE(c2); - uint32_t a2 = OLIVEC_ALPHA(c2); - - r1 = (r1*(255 - a2) + r2*a2)/255; if (r1 > 255) r1 = 255; - g1 = (g1*(255 - a2) + g2*a2)/255; if (g1 > 255) g1 = 255; - b1 = (b1*(255 - a2) + b2*a2)/255; if (b1 > 255) b1 = 255; - - *c1 = OLIVEC_RGBA(r1, g1, b1, a1); -} - -OLIVECDEF void olivec_fill(Olivec_Canvas oc, uint32_t color) -{ - for (size_t y = 0; y < oc.height; ++y) { - for (size_t x = 0; x < oc.width; ++x) { - OLIVEC_PIXEL(oc, x, y) = color; - } - } -} - -OLIVECDEF void olivec_rect(Olivec_Canvas oc, int x, int y, int w, int h, uint32_t color) -{ - Olivec_Normalized_Rect nr = {0}; - if (!olivec_normalize_rect(x, y, w, h, oc.width, oc.height, &nr)) return; - for (int x = nr.x1; x <= nr.x2; ++x) { - for (int y = nr.y1; y <= nr.y2; ++y) { - olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), color); - } - } -} - -OLIVECDEF void olivec_frame(Olivec_Canvas oc, int x, int y, int w, int h, size_t t, uint32_t color) -{ - if (t == 0) return; // Nothing to render - - // Convert the rectangle to 2-points representation - int x1 = x; - int y1 = y; - int x2 = x1 + OLIVEC_SIGN(int, w)*(OLIVEC_ABS(int, w) - 1); - if (x1 > x2) OLIVEC_SWAP(int, x1, x2); - int y2 = y1 + OLIVEC_SIGN(int, h)*(OLIVEC_ABS(int, h) - 1); - if (y1 > y2) OLIVEC_SWAP(int, y1, y2); - - olivec_rect(oc, x1 - t/2, y1 - t/2, (x2 - x1 + 1) + t/2*2, t, color); // Top - olivec_rect(oc, x1 - t/2, y1 - t/2, t, (y2 - y1 + 1) + t/2*2, color); // Left - olivec_rect(oc, x1 - t/2, y2 + t/2, (x2 - x1 + 1) + t/2*2, -t, color); // Bottom - olivec_rect(oc, x2 + t/2, y1 - t/2, -t, (y2 - y1 + 1) + t/2*2, color); // Right -} - -OLIVECDEF void olivec_ellipse(Olivec_Canvas oc, int cx, int cy, int rx, int ry, uint32_t color) -{ - Olivec_Normalized_Rect nr = {0}; - int rx1 = rx + OLIVEC_SIGN(int, rx); - int ry1 = ry + OLIVEC_SIGN(int, ry); - if (!olivec_normalize_rect(cx - rx1, cy - ry1, 2*rx1, 2*ry1, oc.width, oc.height, &nr)) return; - - for (int y = nr.y1; y <= nr.y2; ++y) { - for (int x = nr.x1; x <= nr.x2; ++x) { - float nx = (x + 0.5 - nr.x1)/(2.0f*rx1); - float ny = (y + 0.5 - nr.y1)/(2.0f*ry1); - float dx = nx - 0.5; - float dy = ny - 0.5; - if (dx*dx + dy*dy <= 0.5*0.5) { - OLIVEC_PIXEL(oc, x, y) = color; - } - } - } -} - -OLIVECDEF void olivec_circle(Olivec_Canvas oc, int cx, int cy, int r, uint32_t color) -{ - Olivec_Normalized_Rect nr = {0}; - int r1 = r + OLIVEC_SIGN(int, r); - if (!olivec_normalize_rect(cx - r1, cy - r1, 2*r1, 2*r1, oc.width, oc.height, &nr)) return; - - for (int y = nr.y1; y <= nr.y2; ++y) { - for (int x = nr.x1; x <= nr.x2; ++x) { - int count = 0; - for (int sox = 0; sox < OLIVEC_AA_RES; ++sox) { - for (int soy = 0; soy < OLIVEC_AA_RES; ++soy) { - // TODO: switch to 64 bits to make the overflow less likely - // Also research the probability of overflow - int res1 = (OLIVEC_AA_RES + 1); - int dx = (x*res1*2 + 2 + sox*2 - res1*cx*2 - res1); - int dy = (y*res1*2 + 2 + soy*2 - res1*cy*2 - res1); - if (dx*dx + dy*dy <= res1*res1*r*r*2*2) count += 1; - } - } - uint32_t alpha = ((color&0xFF000000)>>(3*8))*count/OLIVEC_AA_RES/OLIVEC_AA_RES; - uint32_t updated_color = (color&0x00FFFFFF)|(alpha<<(3*8)); - olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), updated_color); - } - } -} - -OLIVECDEF bool olivec_in_bounds(Olivec_Canvas oc, int x, int y) -{ - return 0 <= x && x < (int) oc.width && 0 <= y && y < (int) oc.height; -} - -// TODO: AA for line -OLIVECDEF void olivec_line(Olivec_Canvas oc, int x1, int y1, int x2, int y2, uint32_t color) -{ - int dx = x2 - x1; - int dy = y2 - y1; - - // If both of the differences are 0 there will be a division by 0 below. - if (dx == 0 && dy == 0) { - if (olivec_in_bounds(oc, x1, y1)) { - olivec_blend_color(&OLIVEC_PIXEL(oc, x1, y1), color); - } - return; - } - - if (OLIVEC_ABS(int, dx) > OLIVEC_ABS(int, dy)) { - if (x1 > x2) { - OLIVEC_SWAP(int, x1, x2); - OLIVEC_SWAP(int, y1, y2); - } - - for (int x = x1; x <= x2; ++x) { - int y = dy*(x - x1)/dx + y1; - // TODO: move boundary checks out side of the loops in olivec_draw_line - if (olivec_in_bounds(oc, x, y)) { - olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), color); - } - } - } else { - if (y1 > y2) { - OLIVEC_SWAP(int, x1, x2); - OLIVEC_SWAP(int, y1, y2); - } - - for (int y = y1; y <= y2; ++y) { - int x = dx*(y - y1)/dy + x1; - // TODO: move boundary checks out side of the loops in olivec_draw_line - if (olivec_in_bounds(oc, x, y)) { - olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), color); - } - } - } -} - -OLIVECDEF uint32_t mix_colors2(uint32_t c1, uint32_t c2, int u1, int det) -{ - // TODO: estimate how much overflows are an issue in integer only environment - int64_t r1 = OLIVEC_RED(c1); - int64_t g1 = OLIVEC_GREEN(c1); - int64_t b1 = OLIVEC_BLUE(c1); - int64_t a1 = OLIVEC_ALPHA(c1); - - int64_t r2 = OLIVEC_RED(c2); - int64_t g2 = OLIVEC_GREEN(c2); - int64_t b2 = OLIVEC_BLUE(c2); - int64_t a2 = OLIVEC_ALPHA(c2); - - if (det != 0) { - int u2 = det - u1; - int64_t r4 = (r1*u2 + r2*u1)/det; - int64_t g4 = (g1*u2 + g2*u1)/det; - int64_t b4 = (b1*u2 + b2*u1)/det; - int64_t a4 = (a1*u2 + a2*u1)/det; - - return OLIVEC_RGBA(r4, g4, b4, a4); - } - - return 0; -} - -OLIVECDEF uint32_t mix_colors3(uint32_t c1, uint32_t c2, uint32_t c3, int u1, int u2, int det) -{ - // TODO: estimate how much overflows are an issue in integer only environment - int64_t r1 = OLIVEC_RED(c1); - int64_t g1 = OLIVEC_GREEN(c1); - int64_t b1 = OLIVEC_BLUE(c1); - int64_t a1 = OLIVEC_ALPHA(c1); - - int64_t r2 = OLIVEC_RED(c2); - int64_t g2 = OLIVEC_GREEN(c2); - int64_t b2 = OLIVEC_BLUE(c2); - int64_t a2 = OLIVEC_ALPHA(c2); - - int64_t r3 = OLIVEC_RED(c3); - int64_t g3 = OLIVEC_GREEN(c3); - int64_t b3 = OLIVEC_BLUE(c3); - int64_t a3 = OLIVEC_ALPHA(c3); - - if (det != 0) { - int u3 = det - u1 - u2; - int64_t r4 = (r1*u1 + r2*u2 + r3*u3)/det; - int64_t g4 = (g1*u1 + g2*u2 + g3*u3)/det; - int64_t b4 = (b1*u1 + b2*u2 + b3*u3)/det; - int64_t a4 = (a1*u1 + a2*u2 + a3*u3)/det; - - return OLIVEC_RGBA(r4, g4, b4, a4); - } - - return 0; -} - -// NOTE: we imply u3 = det - u1 - u2 -OLIVECDEF bool olivec_barycentric(int x1, int y1, int x2, int y2, int x3, int y3, int xp, int yp, int *u1, int *u2, int *det) -{ - *det = ((x1 - x3)*(y2 - y3) - (x2 - x3)*(y1 - y3)); - *u1 = ((y2 - y3)*(xp - x3) + (x3 - x2)*(yp - y3)); - *u2 = ((y3 - y1)*(xp - x3) + (x1 - x3)*(yp - y3)); - int u3 = *det - *u1 - *u2; - return ( - (OLIVEC_SIGN(int, *u1) == OLIVEC_SIGN(int, *det) || *u1 == 0) && - (OLIVEC_SIGN(int, *u2) == OLIVEC_SIGN(int, *det) || *u2 == 0) && - (OLIVEC_SIGN(int, u3) == OLIVEC_SIGN(int, *det) || u3 == 0) - ); -} - -OLIVECDEF bool olivec_normalize_triangle(size_t width, size_t height, int x1, int y1, int x2, int y2, int x3, int y3, int *lx, int *hx, int *ly, int *hy) -{ - *lx = x1; - *hx = x1; - if (*lx > x2) *lx = x2; - if (*lx > x3) *lx = x3; - if (*hx < x2) *hx = x2; - if (*hx < x3) *hx = x3; - if (*lx < 0) *lx = 0; - if ((size_t) *lx >= width) return false;; - if (*hx < 0) return false;; - if ((size_t) *hx >= width) *hx = width-1; - - *ly = y1; - *hy = y1; - if (*ly > y2) *ly = y2; - if (*ly > y3) *ly = y3; - if (*hy < y2) *hy = y2; - if (*hy < y3) *hy = y3; - if (*ly < 0) *ly = 0; - if ((size_t) *ly >= height) return false;; - if (*hy < 0) return false;; - if ((size_t) *hy >= height) *hy = height-1; - - return true; -} - -OLIVECDEF void olivec_triangle3c(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, - uint32_t c1, uint32_t c2, uint32_t c3) -{ - int lx, hx, ly, hy; - if (olivec_normalize_triangle(oc.width, oc.height, x1, y1, x2, y2, x3, y3, &lx, &hx, &ly, &hy)) { - for (int y = ly; y <= hy; ++y) { - for (int x = lx; x <= hx; ++x) { - int u1, u2, det; - if (olivec_barycentric(x1, y1, x2, y2, x3, y3, x, y, &u1, &u2, &det)) { - olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), mix_colors3(c1, c2, c3, u1, u2, det)); - } - } - } - } -} - -OLIVECDEF void olivec_triangle3z(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float z1, float z2, float z3) -{ - int lx, hx, ly, hy; - if (olivec_normalize_triangle(oc.width, oc.height, x1, y1, x2, y2, x3, y3, &lx, &hx, &ly, &hy)) { - for (int y = ly; y <= hy; ++y) { - for (int x = lx; x <= hx; ++x) { - int u1, u2, det; - if (olivec_barycentric(x1, y1, x2, y2, x3, y3, x, y, &u1, &u2, &det)) { - float z = z1*u1/det + z2*u2/det + z3*(det - u1 - u2)/det; - OLIVEC_PIXEL(oc, x, y) = *(uint32_t*)&z; - } - } - } - } -} - -OLIVECDEF void olivec_triangle3uv(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float tx1, float ty1, float tx2, float ty2, float tx3, float ty3, float z1, float z2, float z3, Olivec_Canvas texture) -{ - int lx, hx, ly, hy; - if (olivec_normalize_triangle(oc.width, oc.height, x1, y1, x2, y2, x3, y3, &lx, &hx, &ly, &hy)) { - for (int y = ly; y <= hy; ++y) { - for (int x = lx; x <= hx; ++x) { - int u1, u2, det; - if (olivec_barycentric(x1, y1, x2, y2, x3, y3, x, y, &u1, &u2, &det)) { - int u3 = det - u1 - u2; - float z = z1*u1/det + z2*u2/det + z3*(det - u1 - u2)/det; - float tx = tx1*u1/det + tx2*u2/det + tx3*u3/det; - float ty = ty1*u1/det + ty2*u2/det + ty3*u3/det; - - int texture_x = tx/z*texture.width; - if (texture_x < 0) texture_x = 0; - if ((size_t) texture_x >= texture.width) texture_x = texture.width - 1; - - int texture_y = ty/z*texture.height; - if (texture_y < 0) texture_y = 0; - if ((size_t) texture_y >= texture.height) texture_y = texture.height - 1; - OLIVEC_PIXEL(oc, x, y) = OLIVEC_PIXEL(texture, (int)texture_x, (int)texture_y); - } - } - } - } -} - -OLIVECDEF void olivec_triangle3uv_bilinear(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, float tx1, float ty1, float tx2, float ty2, float tx3, float ty3, float z1, float z2, float z3, Olivec_Canvas texture) -{ - int lx, hx, ly, hy; - if (olivec_normalize_triangle(oc.width, oc.height, x1, y1, x2, y2, x3, y3, &lx, &hx, &ly, &hy)) { - for (int y = ly; y <= hy; ++y) { - for (int x = lx; x <= hx; ++x) { - int u1, u2, det; - if (olivec_barycentric(x1, y1, x2, y2, x3, y3, x, y, &u1, &u2, &det)) { - int u3 = det - u1 - u2; - float z = z1*u1/det + z2*u2/det + z3*(det - u1 - u2)/det; - float tx = tx1*u1/det + tx2*u2/det + tx3*u3/det; - float ty = ty1*u1/det + ty2*u2/det + ty3*u3/det; - - float texture_x = tx/z*texture.width; - if (texture_x < 0) texture_x = 0; - if (texture_x >= (float) texture.width) texture_x = texture.width - 1; - - float texture_y = ty/z*texture.height; - if (texture_y < 0) texture_y = 0; - if (texture_y >= (float) texture.height) texture_y = texture.height - 1; - - int precision = 100; - OLIVEC_PIXEL(oc, x, y) = olivec_pixel_bilinear( - texture, - texture_x*precision, texture_y*precision, - precision, precision); - } - } - } - } -} - -// TODO: AA for triangle -OLIVECDEF void olivec_triangle(Olivec_Canvas oc, int x1, int y1, int x2, int y2, int x3, int y3, uint32_t color) -{ - int lx, hx, ly, hy; - if (olivec_normalize_triangle(oc.width, oc.height, x1, y1, x2, y2, x3, y3, &lx, &hx, &ly, &hy)) { - for (int y = ly; y <= hy; ++y) { - for (int x = lx; x <= hx; ++x) { - int u1, u2, det; - if (olivec_barycentric(x1, y1, x2, y2, x3, y3, x, y, &u1, &u2, &det)) { - olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), color); - } - } - } - } -} - -OLIVECDEF void olivec_text(Olivec_Canvas oc, const char *text, int tx, int ty, Olivec_Font font, size_t glyph_size, uint32_t color) -{ - for (size_t i = 0; *text; ++i, ++text) { - int gx = tx + i*font.width*glyph_size; - int gy = ty; - const char *glyph = &font.glyphs[(*text)*sizeof(char)*font.width*font.height]; - for (int dy = 0; (size_t) dy < font.height; ++dy) { - for (int dx = 0; (size_t) dx < font.width; ++dx) { - int px = gx + dx*glyph_size; - int py = gy + dy*glyph_size; - if (0 <= px && px < (int) oc.width && 0 <= py && py < (int) oc.height) { - if (glyph[dy*font.width + dx]) { - olivec_rect(oc, px, py, glyph_size, glyph_size, color); - } - } - } - } - } -} - -OLIVECDEF void olivec_sprite_blend(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite) -{ - if (sprite.width == 0) return; - if (sprite.height == 0) return; - - Olivec_Normalized_Rect nr = {0}; - if (!olivec_normalize_rect(x, y, w, h, oc.width, oc.height, &nr)) return; - - int xa = nr.ox1; - if (w < 0) xa = nr.ox2; - int ya = nr.oy1; - if (h < 0) ya = nr.oy2; - for (int y = nr.y1; y <= nr.y2; ++y) { - for (int x = nr.x1; x <= nr.x2; ++x) { - size_t nx = (x - xa)*((int) sprite.width)/w; - size_t ny = (y - ya)*((int) sprite.height)/h; - olivec_blend_color(&OLIVEC_PIXEL(oc, x, y), OLIVEC_PIXEL(sprite, nx, ny)); - } - } -} - -OLIVECDEF void olivec_sprite_copy(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite) -{ - if (sprite.width == 0) return; - if (sprite.height == 0) return; - - // TODO: consider introducing flip parameter instead of relying on negative width and height - // Similar to how SDL_RenderCopyEx does that - Olivec_Normalized_Rect nr = {0}; - if (!olivec_normalize_rect(x, y, w, h, oc.width, oc.height, &nr)) return; - - int xa = nr.ox1; - if (w < 0) xa = nr.ox2; - int ya = nr.oy1; - if (h < 0) ya = nr.oy2; - for (int y = nr.y1; y <= nr.y2; ++y) { - for (int x = nr.x1; x <= nr.x2; ++x) { - size_t nx = (x - xa)*((int) sprite.width)/w; - size_t ny = (y - ya)*((int) sprite.height)/h; - OLIVEC_PIXEL(oc, x, y) = OLIVEC_PIXEL(sprite, nx, ny); - } - } -} - -// TODO: olivec_pixel_bilinear does not check for out-of-bounds -// But maybe it shouldn't. Maybe it's a responsibility of the caller of the function. -OLIVECDEF uint32_t olivec_pixel_bilinear(Olivec_Canvas sprite, int nx, int ny, int w, int h) -{ - int px = nx%w; - int py = ny%h; - - int x1 = nx/w, x2 = nx/w; - int y1 = ny/h, y2 = ny/h; - if (px < w/2) { - // left - px += w/2; - x1 -= 1; - if (x1 < 0) x1 = 0; - } else { - // right - px -= w/2; - x2 += 1; - if ((size_t) x2 >= sprite.width) x2 = sprite.width - 1; - } - - if (py < h/2) { - // top - py += h/2; - y1 -= 1; - if (y1 < 0) y1 = 0; - } else { - // bottom - py -= h/2; - y2 += 1; - if ((size_t) y2 >= sprite.height) y2 = sprite.height - 1; - } - - return mix_colors2(mix_colors2(OLIVEC_PIXEL(sprite, x1, y1), - OLIVEC_PIXEL(sprite, x2, y1), - px, w), - mix_colors2(OLIVEC_PIXEL(sprite, x1, y2), - OLIVEC_PIXEL(sprite, x2, y2), - px, w), - py, h); -} - -OLIVECDEF void olivec_sprite_copy_bilinear(Olivec_Canvas oc, int x, int y, int w, int h, Olivec_Canvas sprite) -{ - // TODO: support negative size in olivec_sprite_copy_bilinear() - if (w <= 0) return; - if (h <= 0) return; - - Olivec_Normalized_Rect nr = {0}; - if (!olivec_normalize_rect(x, y, w, h, oc.width, oc.height, &nr)) return; - - for (int y = nr.y1; y <= nr.y2; ++y) { - for (int x = nr.x1; x <= nr.x2; ++x) { - size_t nx = (x - nr.ox1)*sprite.width; - size_t ny = (y - nr.oy1)*sprite.height; - OLIVEC_PIXEL(oc, x, y) = olivec_pixel_bilinear(sprite, nx, ny, w, h); - } - } -} - -#endif // OLIVEC_IMPLEMENTATION - -// TODO: Benchmarking -// TODO: SIMD implementations -// TODO: bezier curves -// TODO: olivec_ring -// TODO: fuzzer -// TODO: Stencil diff --git a/android/app/src/main/jni/vc.c b/android/app/src/main/jni/vc.c deleted file mode 100644 index 521dce8..0000000 --- a/android/app/src/main/jni/vc.c +++ /dev/null @@ -1,565 +0,0 @@ -// C implementation of the Virtual Console (VC) for demos. -// -// # Usage -// ```c -// // demo.c -// // vc.c expectes render() to be defined and also supplies it's own entry point -// // if needed (some platforms like WASM_PLATFORM do not have the main() -// // entry point) -// #include "vc.c" -// -// #define WIDTH 800 -// #define HEIGHT 600 -// static uint32_t pixels[WIDTH*HEIGHT]; -// -// static Olivec_Canvas vc_render(float dt) -// { -// Olivec_Canvas oc = olivec_canvas(pixels, WIDTH, HEIGHT, WIDTH); -// // ... -// // ... render into oc ... -// // ... -// return oc; -// } -// ``` -// -// # Build -// ```console -// $ clang -o demo.sdl -DVC_PLATFORM=VC_SDL_PLATFORM demo.c -lSDL2 -// $ clang -o demo.term -DVC_PLATFORM=VC_TERM_PLATFORM demo.c -// $ clang -fno-builtin --target=wasm32 --no-standard-libraries -Wl,--no-entry -Wl,--export=render -Wl,--allow-undefined -o demo.wasm -DVC_PLATFORM=VC_WASM_PLATFORM demo.c -// ``` - -#define OLIVEC_IMPLEMENTATION -#include "olive.c" - -Olivec_Canvas vc_render(float dt); - -#ifndef VC_PLATFORM -#endif - -// Possible values of VC_PLATFORM -#define VC_WASM_PLATFORM 0 -#define VC_SDL_PLATFORM 1 -#define VC_TERM_PLATFORM 2 - -#if VC_PLATFORM == VC_SDL_PLATFORM -#include -#include - -#define return_defer(value) do { result = (value); goto defer; } while (0) - -static SDL_Texture *vc_sdl_texture = NULL; -static size_t vc_sdl_actual_width = 0; -static size_t vc_sdl_actual_height = 0; - -static bool vc_sdl_resize_texture(SDL_Renderer *renderer, size_t new_width, size_t new_height) -{ - if (vc_sdl_texture != NULL) SDL_DestroyTexture(vc_sdl_texture); - vc_sdl_actual_width = new_width; - vc_sdl_actual_height = new_height; - vc_sdl_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, vc_sdl_actual_width, vc_sdl_actual_height); - if (vc_sdl_texture == NULL) return false; - return true; -} - -int main(void) -{ - int result = 0; - - SDL_Window *window = NULL; - SDL_Renderer *renderer = NULL; - - { - if (SDL_Init(SDL_INIT_VIDEO) < 0) return_defer(1); - - window = SDL_CreateWindow("Olivec", 0, 0, 0, 0, 0); - if (window == NULL) return_defer(1); - - renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); - if (renderer == NULL) return_defer(1); - - Uint32 prev = SDL_GetTicks(); - bool pause = false; - for (;;) { - // Compute Delta Time - Uint32 curr = SDL_GetTicks(); - float dt = (curr - prev)/1000.f; - prev = curr; - - // Flush the events - SDL_Event event; - while (SDL_PollEvent(&event)) { - switch (event.type) { - case SDL_QUIT: { - return_defer(0); - } break; - case SDL_KEYDOWN: { - if (event.key.keysym.sym == SDLK_SPACE) pause = !pause; - } break; - } - } - - SDL_Rect window_rect = {0, 0, vc_sdl_actual_width, vc_sdl_actual_height}; - - if (!pause) { - // Render the texture - Olivec_Canvas oc_src = vc_render(dt); - if (oc_src.width != vc_sdl_actual_width || oc_src.height != vc_sdl_actual_height) { - if (!vc_sdl_resize_texture(renderer, oc_src.width, oc_src.height)) return_defer(1); - SDL_SetWindowSize(window, vc_sdl_actual_width, vc_sdl_actual_height); - } - void *pixels_dst; - int pitch; - if (SDL_LockTexture(vc_sdl_texture, &window_rect, &pixels_dst, &pitch) < 0) return_defer(1); - for (size_t y = 0; y < vc_sdl_actual_height; ++y) { - // TODO: it would be cool if Olivec_Canvas supported pitch in bytes instead of pixels - // It would be more flexible and we could draw on the locked texture memory directly - memcpy((char*)pixels_dst + y*pitch, oc_src.pixels + y*vc_sdl_actual_width, vc_sdl_actual_width*sizeof(uint32_t)); - } - SDL_UnlockTexture(vc_sdl_texture); - } - - // Display the texture - if (SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) < 0) return_defer(1); - if (SDL_RenderClear(renderer) < 0) return_defer(1); - if (SDL_RenderCopy(renderer, vc_sdl_texture, &window_rect, &window_rect) < 0) return_defer(1); - SDL_RenderPresent(renderer); - } - } - -defer: - switch (result) { - case 0: - printf("OK\n"); - break; - default: - fprintf(stderr, "SDL ERROR: %s\n", SDL_GetError()); - } - if (vc_sdl_texture) SDL_DestroyTexture(vc_sdl_texture); - if (renderer) SDL_DestroyRenderer(renderer); - if (window) SDL_DestroyWindow(window); - SDL_Quit(); - return result; -} -#elif VC_PLATFORM == VC_TERM_PLATFORM - -#include -#include -#include -#include -#include -#include -#include -#include - -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include "stb_image_write.h" - -static size_t vc_term_actual_width = 0; -static size_t vc_term_actual_height = 0; -static size_t vc_term_scaled_down_width = 0; -static size_t vc_term_scaled_down_height = 0; -static int *vc_term_char_canvas = 0; - -int hsl256[][3] = { - {0, 0, 0}, - {0, 100, 25}, - {120, 100, 25}, - {60, 100, 25}, - {240, 100, 25}, - {300, 100, 25}, - {180, 100, 25}, - {0, 0, 75}, - {0, 0, 50}, - {0, 100, 50}, - {120, 100, 50}, - {60, 100, 50}, - {240, 100, 50}, - {300, 100, 50}, - {180, 100, 50}, - {0, 0, 100}, - {0, 0, 0}, - {240, 99, 18}, - {240, 100, 26}, - {240, 100, 34}, - {240, 100, 42}, - {240, 100, 50}, - {120, 99, 18}, - {180, 99, 18}, - {197, 100, 26}, - {207, 100, 34}, - {213, 100, 42}, - {217, 100, 50}, - {120, 100, 26}, - {162, 100, 26}, - {180, 100, 26}, - {193, 100, 34}, - {202, 100, 42}, - {208, 100, 50}, - {120, 100, 34}, - {152, 100, 34}, - {166, 100, 34}, - {180, 100, 34}, - {191, 100, 42}, - {198, 100, 50}, - {120, 100, 42}, - {146, 100, 42}, - {157, 100, 42}, - {168, 100, 42}, - {180, 100, 42}, - {189, 100, 50}, - {120, 100, 50}, - {142, 100, 50}, - {151, 100, 50}, - {161, 100, 50}, - {170, 100, 50}, - {180, 100, 50}, - {0, 99, 18}, - {300, 99, 18}, - {282, 100, 26}, - {272, 100, 34}, - {266, 100, 42}, - {262, 100, 50}, - {60, 99, 18}, - {0, 0, 37}, - {240, 17, 45}, - {240, 33, 52}, - {240, 60, 60}, - {240, 100, 68}, - {77, 100, 26}, - {120, 17, 45}, - {180, 17, 45}, - {210, 33, 52}, - {220, 60, 60}, - {225, 100, 68}, - {87, 100, 34}, - {120, 33, 52}, - {150, 33, 52}, - {180, 33, 52}, - {200, 60, 60}, - {210, 100, 68}, - {93, 100, 42}, - {120, 60, 60}, - {140, 60, 60}, - {160, 60, 60}, - {180, 60, 60}, - {195, 100, 68}, - {97, 100, 50}, - {120, 100, 68}, - {135, 100, 68}, - {150, 100, 68}, - {165, 100, 68}, - {180, 100, 68}, - {0, 100, 26}, - {317, 100, 26}, - {300, 100, 26}, - {286, 100, 34}, - {277, 100, 42}, - {271, 100, 50}, - {42, 100, 26}, - {0, 17, 45}, - {300, 17, 45}, - {270, 33, 52}, - {260, 60, 60}, - {255, 100, 68}, - {60, 100, 26}, - {60, 17, 45}, - {0, 0, 52}, - {240, 20, 60}, - {240, 50, 68}, - {240, 100, 76}, - {73, 100, 34}, - {90, 33, 52}, - {120, 20, 60}, - {180, 20, 60}, - {210, 50, 68}, - {220, 100, 76}, - {82, 100, 42}, - {100, 60, 60}, - {120, 50, 68}, - {150, 50, 68}, - {180, 50, 68}, - {200, 100, 76}, - {88, 100, 50}, - {105, 100, 68}, - {120, 100, 76}, - {140, 100, 76}, - {160, 100, 76}, - {180, 100, 76}, - {0, 100, 34}, - {327, 100, 34}, - {313, 100, 34}, - {300, 100, 34}, - {288, 100, 42}, - {281, 100, 50}, - {32, 100, 34}, - {0, 33, 52}, - {330, 33, 52}, - {300, 33, 52}, - {280, 60, 60}, - {270, 100, 68}, - {46, 100, 34}, - {30, 33, 52}, - {0, 20, 60}, - {300, 20, 60}, - {270, 50, 68}, - {260, 100, 76}, - {60, 100, 34}, - {60, 33, 52}, - {60, 20, 60}, - {0, 0, 68}, - {240, 33, 76}, - {240, 100, 84}, - {71, 100, 42}, - {80, 60, 60}, - {90, 50, 68}, - {120, 33, 76}, - {180, 33, 76}, - {210, 100, 84}, - {78, 100, 50}, - {90, 100, 68}, - {100, 100, 76}, - {120, 100, 84}, - {150, 100, 84}, - {180, 100, 84}, - {0, 100, 42}, - {333, 100, 42}, - {322, 100, 42}, - {311, 100, 42}, - {300, 100, 42}, - {290, 100, 50}, - {26, 100, 42}, - {0, 60, 60}, - {340, 60, 60}, - {320, 60, 60}, - {300, 60, 60}, - {285, 100, 68}, - {37, 100, 42}, - {20, 60, 60}, - {0, 50, 68}, - {330, 50, 68}, - {300, 50, 68}, - {280, 100, 76}, - {48, 100, 42}, - {40, 60, 60}, - {30, 50, 68}, - {0, 33, 76}, - {300, 33, 76}, - {270, 100, 84}, - {60, 100, 42}, - {60, 60, 60}, - {60, 50, 68}, - {60, 33, 76}, - {0, 0, 84}, - {240, 100, 92}, - {69, 100, 50}, - {75, 100, 68}, - {80, 100, 76}, - {90, 100, 84}, - {120, 100, 92}, - {180, 100, 92}, - {0, 100, 50}, - {337, 100, 50}, - {328, 100, 50}, - {318, 100, 50}, - {309, 100, 50}, - {300, 100, 50}, - {22, 100, 50}, - {0, 100, 68}, - {345, 100, 68}, - {330, 100, 68}, - {315, 100, 68}, - {300, 100, 68}, - {31, 100, 50}, - {15, 100, 68}, - {0, 100, 76}, - {340, 100, 76}, - {320, 100, 76}, - {300, 100, 76}, - {41, 100, 50}, - {30, 100, 68}, - {20, 100, 76}, - {0, 100, 84}, - {330, 100, 84}, - {300, 100, 84}, - {50, 100, 50}, - {45, 100, 68}, - {40, 100, 76}, - {30, 100, 84}, - {0, 100, 92}, - {300, 100, 92}, - {60, 100, 50}, - {60, 100, 68}, - {60, 100, 76}, - {60, 100, 84}, - {60, 100, 92}, - {0, 0, 100}, - {0, 0, 3}, - {0, 0, 7}, - {0, 0, 10}, - {0, 0, 14}, - {0, 0, 18}, - {0, 0, 22}, - {0, 0, 26}, - {0, 0, 30}, - {0, 0, 34}, - {0, 0, 38}, - {0, 0, 42}, - {0, 0, 46}, - {0, 0, 50}, - {0, 0, 54}, - {0, 0, 58}, - {0, 0, 61}, - {0, 0, 65}, - {0, 0, 69}, - {0, 0, 73}, - {0, 0, 77}, - {0, 0, 81}, - {0, 0, 85}, - {0, 0, 89}, - {0, 0, 93}, -}; - -int distance_hsl256(int i, int h, int s, int l) -{ - int dh = h - hsl256[i][0]; - int ds = s - hsl256[i][1]; - int dl = l - hsl256[i][2]; - return dh*dh + ds*ds + dl*dl; -} - -// TODO: bring find_ansi_index_by_rgb from image2term -int find_ansi_index_by_hsl(int h, int s, int l) -{ - int index = 0; - for (int i = 0; i < 256; ++i) { - if (distance_hsl256(i, h, s, l) < distance_hsl256(index, h, s, l)) { - index = i; - } - } - return index; -} - -static uint32_t vc_term_compress_pixels_chunk(Olivec_Canvas oc) -{ - size_t r = 0; - size_t g = 0; - size_t b = 0; - size_t a = 0; - - for (size_t y = 0; y < oc.height; ++y) { - for (size_t x = 0; x < oc.width; ++x) { - r += OLIVEC_RED(OLIVEC_PIXEL(oc, x, y)); - g += OLIVEC_GREEN(OLIVEC_PIXEL(oc, x, y)); - b += OLIVEC_BLUE(OLIVEC_PIXEL(oc, x, y)); - a += OLIVEC_ALPHA(OLIVEC_PIXEL(oc, x, y)); - } - } - - r /= oc.width*oc.height; - g /= oc.width*oc.height; - b /= oc.width*oc.height; - a /= oc.width*oc.height; - - return OLIVEC_RGBA(r, g, b, a); -} - -#ifndef VC_TERM_SCALE_DOWN_FACTOR -#define VC_TERM_SCALE_DOWN_FACTOR 20 -#endif // VC_TERM_SCALE_DOWN_FACTOR - -static void vc_term_resize_char_canvas(size_t new_width, size_t new_height) -{ - // TODO: warn the user if vc_term_actual_width does not fit into the screen - // TODO: can we just do something so the divisibility is not important? - // Like round the stuff or something? - // Or we can resize the frame on the fly similarly to how we resize sprites in olivec_sprite_*() functions. - assert(new_width%VC_TERM_SCALE_DOWN_FACTOR == 0 && "Width must be divisible by VC_TERM_SCALE_DOWN_FACTOR"); - assert(new_height%VC_TERM_SCALE_DOWN_FACTOR == 0 && "Height must be divisible by VC_TERM_SCALE_DOWN_FACTOR"); - vc_term_actual_width = new_width; - vc_term_actual_height = new_height; - vc_term_scaled_down_width = vc_term_actual_width/VC_TERM_SCALE_DOWN_FACTOR; - vc_term_scaled_down_height = vc_term_actual_height/VC_TERM_SCALE_DOWN_FACTOR; - free(vc_term_char_canvas); - vc_term_char_canvas = malloc(sizeof(*vc_term_char_canvas)*vc_term_scaled_down_width*vc_term_scaled_down_height); - assert(vc_term_char_canvas != NULL && "Just buy more RAM"); -} - -void rgb_to_hsl(int r, int g, int b, int *h, int *s, int *l) -{ - float r01 = r/255.0f; - float g01 = g/255.0f; - float b01 = b/255.0f; - float cmax = r01; - if (g01 > cmax) cmax = g01; - if (b01 > cmax) cmax = b01; - float cmin = r01; - if (g01 < cmin) cmin = g01; - if (b01 < cmin) cmin = b01; - float delta = cmax - cmin; - float epsilon = 1e-6; - float hf = 0; - if (delta < epsilon) hf = 0; - else if (cmax == r01) hf = 60.0f*fmod((g01 - b01)/delta, 6.0f); - else if (cmax == g01) hf = 60.0f*((b01 - r01)/delta + 2); - else if (cmax == b01) hf = 60.0f*((r01 - g01)/delta + 4); - else assert(0 && "unreachable"); - - float lf = (cmax + cmin)/2; - - float sf = 0; - if (delta < epsilon) sf = 0; - else sf = delta/(1 - fabsf(2*lf - 1)); - - *h = fmodf(fmodf(hf, 360.0f) + 360.0f, 360.0f); - *s = sf*100.0f; - *l = lf*100.0f; -} - -static void vc_term_compress_pixels(Olivec_Canvas oc) -{ - if (vc_term_actual_width != oc.width || vc_term_actual_height != oc.height) { - vc_term_resize_char_canvas(oc.width, oc.height); - } - - for (size_t y = 0; y < vc_term_scaled_down_height; ++y) { - for (size_t x = 0; x < vc_term_scaled_down_width; ++x) { - Olivec_Canvas soc = olivec_subcanvas(oc, x*VC_TERM_SCALE_DOWN_FACTOR, y*VC_TERM_SCALE_DOWN_FACTOR, VC_TERM_SCALE_DOWN_FACTOR, VC_TERM_SCALE_DOWN_FACTOR); - uint32_t cp = vc_term_compress_pixels_chunk(soc); - int r = OLIVEC_RED(cp); - int g = OLIVEC_GREEN(cp); - int b = OLIVEC_BLUE(cp); - int a = OLIVEC_ALPHA(cp); - r = a*r/255; - g = a*g/255; - b = a*b/255; - int h, s, l; - rgb_to_hsl(r, g, b, &h, &s, &l); - vc_term_char_canvas[y*vc_term_scaled_down_width + x] = find_ansi_index_by_hsl(h, s, l); - } - } -} - -int main(void) -{ - for (;;) { - vc_term_compress_pixels(vc_render(1.f/60.f)); - for (size_t y = 0; y < vc_term_scaled_down_height; ++y) { - for (size_t x = 0; x < vc_term_scaled_down_width; ++x) { - // TODO: explore the idea of figuring out aspect ratio of the character using escape ANSI codes of the terminal and rendering the image accordingly - printf("\033[48;5;%dm ", vc_term_char_canvas[y*vc_term_scaled_down_width + x]); - } - printf("\033[0m\n"); - } - - usleep(1000*1000/60); - printf("\033[%zuA", vc_term_scaled_down_height); - printf("\033[%zuD", vc_term_scaled_down_width); - } - return 0; -} -#elif VC_PLATFORM == VC_WASM_PLATFORM -// Do nothing because all the work is done in ../js/vc.js -#else -#error "Unknown VC platform" -#endif // VC_SDL_PLATFORM \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/me/app/MainActivity.kt b/android/app/src/main/kotlin/com/me/app/MainActivity.kt index fa2eece..989a04a 100644 --- a/android/app/src/main/kotlin/com/me/app/MainActivity.kt +++ b/android/app/src/main/kotlin/com/me/app/MainActivity.kt @@ -36,11 +36,15 @@ class MainActivity : AppCompatActivity() { false } super.onCreate(savedInstanceState) + + // == HIDE SYSTEM BARS == // WindowInsetsControllerCompat(window, window.decorView).let { controller -> controller.hide(WindowInsetsCompat.Type.systemBars()) controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } + + // == KEEP THE SCREEN ON == // window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) binding = ActivityMainBinding.inflate(layoutInflater) @@ -58,7 +62,6 @@ class MainActivity : AppCompatActivity() { delay(10) drawTriangle(bitmap) binding.surface.setImageBitmap(bitmap) - } } } From 9dcf2a93c3c3e52420ae58a40cc186f25aa382ab Mon Sep 17 00:00:00 2001 From: daniele-NA Date: Thu, 12 Feb 2026 19:29:05 +0100 Subject: [PATCH 3/3] android/README.md --- android/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/README.md b/android/README.md index 910f335..e613b0f 100644 --- a/android/README.md +++ b/android/README.md @@ -23,9 +23,9 @@ No copies. No return values. The bitmap memory is the render target. * render frame * apply rotation -## Native (pseudo) +## Native -```c +``` void *bitmapPixels; AndroidBitmap_lockPixels(env, bmp, &bitmapPixels); memcpy(bitmapPixels, pixels, sizeof(pixels));