raylib 5.5 + java jni probe
download raylib release tar package, graalvm
curl -JLO https://ghproxy.cn/https://github.com/raysan5/raylib/releases/download/5.5/raylib-5.5_linux_amd64.tar.gz
curl -JLO https://ghproxy.cn/https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-24.0.2/graalvm-community-jdk-24.0.2_linux-x64_bin.tar.gz
1. 先定义PaintEditor.java(usage)
public class PaintEditor {
static {
System.loadLibrary("painteditor");
}
public static final int TOOL_NONE = 0;
public static final int TOOL_PENCIL = 1;
public static final int TOOL_LINE = 2;
public static final int TOOL_RECTANGLE = 3;
public static final int TOOL_CIRCLE = 4;
public static final int TOOL_ERASER = 5;
public static final int TOOL_FILL = 6;
private native void init(int width, int height, String title);
private native void update();
private native boolean shouldClose();
private native void close();
private native void saveImage(String filePath);
public static void main(String[] args) {
PaintEditor editor = new PaintEditor();
editor.init(2560, 1600, "Simple Paint Editor");
while (!editor.shouldClose()) {
editor.update();
}
editor.close();
}
}
2. 然后生成头文件PaintEditor.h
# 生成JNI头文件
javac -h . PaintEditor.java
3. 实现定义, paint_editor_jni.c
#include "PaintEditor.h"
#include <raylib.h>
#include <math.h>
#include <string.h>
#include <stdlib.h>
#define KEY_1 49
#define KEY_2 50
#define KEY_3 51
#define KEY_4 52
#define KEY_5 53
#define KEY_6 54
#define KEY_7 55
typedef enum {
TOOL_SELECTION,
TOOL_PENCIL,
TOOL_LINE,
TOOL_RECTANGLE,
TOOL_CIRCLE,
TOOL_ERASER,
TOOL_FILL
} ToolType;
static ToolType currentTool = TOOL_PENCIL;
static Color currentColor = BLACK;
static int brushSize = 2;
static Vector2 startPos = {0};
static Vector2 currentPos = {0};
static bool isDrawing = false;
static RenderTexture2D canvas;
static bool showColorPicker = false;
static Color colorPalette[16] = {
BLACK, WHITE, GRAY, LIGHTGRAY,
RED, MAROON, ORANGE, YELLOW,
GREEN, LIME, DARKGREEN, BLUE,
DARKBLUE, PURPLE, PINK, BROWN
};
static const char* getCString(JNIEnv* env, jstring jstr) {
if (jstr == NULL) return NULL;
return (*env)->GetStringUTFChars(env, jstr, NULL);
}
static void releaseCString(JNIEnv* env, jstring jstr, const char* cstr) {
if (jstr != NULL && cstr != NULL) {
(*env)->ReleaseStringUTFChars(env, jstr, cstr);
}
}
static float calculateDistance(Vector2 v1, Vector2 v2) {
float dx = v2.x - v1.x;
float dy = v2.y - v1.y;
return sqrtf(dx*dx + dy*dy);
}
static void fillArea(Vector2 pos, Color fillColor) {
Image canvasImage = LoadImageFromTexture(canvas.texture);
Color* pixels = (Color*)canvasImage.data;
if (!pixels) {
UnloadImage(canvasImage);
return;
}
int width = canvasImage.width;
int height = canvasImage.height;
int x = (int)pos.x;
int y = (int)pos.y;
if (x < 0 || x >= width || y < 0 || y >= height) {
UnloadImage(canvasImage);
return;
}
Color targetColor = pixels[y * width + x];
if (memcmp(&targetColor, &fillColor, sizeof(Color)) == 0) {
UnloadImage(canvasImage);
return;
}
Vector2* stack = (Vector2*)malloc(width * height * sizeof(Vector2));
int stackSize = 0;
stack[stackSize++] = (Vector2){(float)x, (float)y};
pixels[y * width + x] = fillColor;
while (stackSize > 0) {
Vector2 p = stack[--stackSize];
int px = (int)p.x;
int py = (int)p.y;
if (py > 0 && memcmp(&pixels[(py-1)*width + px], &targetColor, sizeof(Color)) == 0) {
pixels[(py-1)*width + px] = fillColor;
stack[stackSize++] = (Vector2){(float)px, (float)(py-1)};
}
if (py < height-1 && memcmp(&pixels[(py+1)*width + px], &targetColor, sizeof(Color)) == 0) {
pixels[(py+1)*width + px] = fillColor;
stack[stackSize++] = (Vector2){(float)px, (float)(py+1)};
}
if (px > 0 && memcmp(&pixels[py*width + (px-1)], &targetColor, sizeof(Color)) == 0) {
pixels[py*width + (px-1)] = fillColor;
stack[stackSize++] = (Vector2){(float)(px-1), (float)py};
}
if (px < width-1 && memcmp(&pixels[py*width + (px+1)], &targetColor, sizeof(Color)) == 0) {
pixels[py*width + (px+1)] = fillColor;
stack[stackSize++] = (Vector2){(float)(px+1), (float)py};
}
}
UpdateTexture(canvas.texture, canvasImage.data);
free(stack);
UnloadImage(canvasImage);
}
JNIEXPORT void JNICALL Java_PaintEditor_init
(JNIEnv* env, jobject obj, jint width, jint height, jstring title) {
const char* cTitle = getCString(env, title);
InitWindow(width, height, cTitle);
releaseCString(env, title, cTitle);
canvas = LoadRenderTexture(width, height);
BeginTextureMode(canvas);
ClearBackground(WHITE);
EndTextureMode();
SetTargetFPS(60);
}
JNIEXPORT void JNICALL Java_PaintEditor_update
(JNIEnv* env, jobject obj) {
currentPos = GetMousePosition();
Color drawColor = (currentTool == TOOL_ERASER) ? WHITE : currentColor;
if (IsKeyPressed(KEY_ESCAPE)) CloseWindow();
if (IsKeyPressed(KEY_1)) currentTool = TOOL_SELECTION;
if (IsKeyPressed(KEY_2)) currentTool = TOOL_PENCIL;
if (IsKeyPressed(KEY_3)) currentTool = TOOL_LINE;
if (IsKeyPressed(KEY_4)) currentTool = TOOL_RECTANGLE;
if (IsKeyPressed(KEY_5)) currentTool = TOOL_CIRCLE;
if (IsKeyPressed(KEY_6)) currentTool = TOOL_ERASER;
if (IsKeyPressed(KEY_7)) currentTool = TOOL_FILL;
if (IsKeyPressed(KEY_KP_ADD) || IsKeyPressed(KEY_EQUAL)) {
brushSize = (brushSize < 50) ? brushSize + 1 : 50;
}
if (IsKeyPressed(KEY_KP_SUBTRACT) || IsKeyPressed(KEY_MINUS)) {
brushSize = (brushSize > 1) ? brushSize - 1 : 1;
}
if (IsKeyPressed(KEY_C)) {
showColorPicker = !showColorPicker;
}
if (IsKeyPressed(KEY_DELETE)) {
BeginTextureMode(canvas);
ClearBackground(WHITE);
EndTextureMode();
}
if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_S)) {
Image screenshot = LoadImageFromTexture(canvas.texture);
ImageFlipVertical(&screenshot);
ExportImage(screenshot, "drawing.png");
UnloadImage(screenshot);
}
if (showColorPicker && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
for (int i = 0; i < 16; i++) {
int x = 20 + (i % 4) * 30;
int y = 40 + (i / 4) * 30;
if (CheckCollisionPointRec(currentPos, (Rectangle){(float)x, (float)y, 25, 25})) {
currentColor = colorPalette[i];
showColorPicker = false;
break;
}
}
}
if (currentTool == TOOL_FILL && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
fillArea(currentPos, currentColor);
}
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && currentTool != TOOL_SELECTION && currentTool != TOOL_FILL) {
startPos = currentPos;
isDrawing = true;
if (currentTool == TOOL_PENCIL) {
BeginTextureMode(canvas);
DrawCircleV(currentPos, brushSize/2.0f, drawColor);
EndTextureMode();
}
}
else if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) && isDrawing) {
isDrawing = false;
BeginTextureMode(canvas);
if (currentTool == TOOL_LINE) {
DrawLineEx(startPos, currentPos, brushSize, drawColor);
}
else if (currentTool == TOOL_RECTANGLE) {
DrawRectangleLinesEx((Rectangle){
fminf(startPos.x, currentPos.x), fminf(startPos.y, currentPos.y),
fabsf(currentPos.x - startPos.x), fabsf(currentPos.y - startPos.y)
}, brushSize, drawColor);
}
else if (currentTool == TOOL_CIRCLE) {
float radius = calculateDistance(startPos, currentPos);
DrawCircleLinesV(startPos, radius, drawColor);
}
EndTextureMode();
}
if (currentTool == TOOL_PENCIL && isDrawing && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
BeginTextureMode(canvas);
DrawLineEx(startPos, currentPos, brushSize, drawColor);
EndTextureMode();
startPos = currentPos;
}
BeginDrawing();
ClearBackground(LIGHTGRAY);
DrawTextureRec(canvas.texture,
(Rectangle){0, 0, canvas.texture.width, -canvas.texture.height},
(Vector2){0, 30}, WHITE);
DrawRectangle(0, 0, GetScreenWidth(), 30, DARKGRAY);
DrawText("Tools: [1]Sel [2]Pencil [3]Line [4]Rect [5]Circle [6]Eraser [7]Fill", 10, 5, 12, WHITE);
DrawText(TextFormat("Size: %d (+/-) Color: [C] Clear: Del Save: Ctrl+S", brushSize), 400, 5, 12, WHITE);
const char* toolNames[] = {"Selection", "Pencil", "Line", "Rectangle", "Circle", "Eraser", "Fill"};
DrawText(TextFormat("Current: %s", toolNames[currentTool]), 700, 5, 12, WHITE);
if (showColorPicker) {
DrawRectangle(10, 40, 140, 160, WHITE);
DrawRectangleLines(10, 40, 140, 160, BLACK);
DrawText("Select Color:", 20, 25, 12, BLACK);
for (int i = 0; i < 16; i++) {
int x = 20 + (i % 4) * 30;
int y = 40 + (i / 4) * 30;
DrawRectangle(x, y, 25, 25, colorPalette[i]);
DrawRectangleLines(x, y, 25, 25, BLACK);
}
}
if (isDrawing && currentTool != TOOL_PENCIL) {
if (currentTool == TOOL_LINE) {
DrawLineEx(startPos, currentPos, brushSize, drawColor);
}
else if (currentTool == TOOL_RECTANGLE) {
DrawRectangleLinesEx((Rectangle){
fminf(startPos.x, currentPos.x), fminf(startPos.y, currentPos.y),
fabsf(currentPos.x - startPos.x), fabsf(currentPos.y - startPos.y)
}, brushSize, drawColor);
}
else if (currentTool == TOOL_CIRCLE) {
float radius = calculateDistance(startPos, currentPos);
DrawCircleLinesV(startPos, radius, drawColor);
}
}
DrawCircleLinesV(currentPos, brushSize/2.0f, BLACK);
EndDrawing();
}
JNIEXPORT jboolean JNICALL Java_PaintEditor_shouldClose
(JNIEnv* env, jobject obj) {
return WindowShouldClose() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT void JNICALL Java_PaintEditor_close
(JNIEnv* env, jobject obj) {
UnloadRenderTexture(canvas);
CloseWindow();
}
最后整体build.sh,自己修改你的jdk和raylib
#!/bin/bash
set -xe
export JAVA_INCLUDE_DIR=/etc/alternatives/java_sdk_24
export RAYLIB_LIB_DIR=./lib
export RAYLIB_INC_DIR=./include
javac -h . PaintEditor.java
gcc -fPIC -shared -Wall \
-I${JAVA_INCLUDE_DIR}/include \
-I${JAVA_INCLUDE_DIR}/include/linux \
-I${RAYLIB_INC_DIR} \
-L${RAYLIB_LIB_DIR} \
paint_editor_jni.c \
-o libpainteditor.so \
-lraylib -lm -lpthread -ldl -lrt -lX11
export LD_LIBRARY_PATH=${RAYLIB_LIB_DIR}:$LD_LIBRARY_PATH
java -Djava.library.path=.:${RAYLIB_LIB_DIR} --enable-native-access=ALL-UNNAMED PaintEditor
graalvm 编译成elf文件
./graalvm-community-openjdk-24.0.2+11.1/bin/native-image --enable-native-access=ALL-UNNAMED PaintEditor
ldd painteditor
./painteditor
damn, 非常慢