From b8562d8b815d122ba5efbb905c68772f25bfd048 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Mon, 19 Jun 2023 12:27:32 +0300 Subject: [PATCH] Reimplement the Canvas transformation functions via the corresponding Skia functions, rather than by concatenating with a transformation matrix. --- .../kotlin/org/jetbrains/skia/Canvas.kt | 44 ++++++-- .../kotlin/org/jetbrains/skia/CanvasTest.kt | 106 +++++++++++++++++- skiko/src/jvmMain/cpp/common/Canvas.cc | 24 ++++ skiko/src/nativeJsMain/cpp/Canvas.cc | 28 +++++ 4 files changed, 193 insertions(+), 9 deletions(-) diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skia/Canvas.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skia/Canvas.kt index a8f1aabe5..f72d846e1 100644 --- a/skiko/src/commonMain/kotlin/org/jetbrains/skia/Canvas.kt +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skia/Canvas.kt @@ -1080,11 +1080,19 @@ open class Canvas internal constructor(ptr: NativePointer, managed: Boolean, int } fun translate(dx: Float, dy: Float): Canvas { - return concat(Matrix33.makeTranslate(dx, dy)) + interopScope { + Stats.onNativeCall() + _nTranslate(_ptr, dx, dy) + } + return this } fun scale(sx: Float, sy: Float): Canvas { - return concat(Matrix33.makeScale(sx, sy)) + interopScope { + Stats.onNativeCall() + _nScale(_ptr, sx, sy) + } + return this } /** @@ -1092,15 +1100,27 @@ open class Canvas internal constructor(ptr: NativePointer, managed: Boolean, int * @return this */ fun rotate(deg: Float): Canvas { - return concat(Matrix33.makeRotate(deg)) + interopScope { + Stats.onNativeCall() + _nRotate(_ptr, deg, 0f, 0f) + } + return this } fun rotate(deg: Float, x: Float, y: Float): Canvas { - return concat(Matrix33.makeRotate(deg, x, y)) + interopScope { + Stats.onNativeCall() + _nRotate(_ptr, deg, x, y) + } + return this } fun skew(sx: Float, sy: Float): Canvas { - return concat(Matrix33.makeSkew(sx, sy)) + interopScope { + Stats.onNativeCall() + _nSkew(_ptr, sx, sy) + } + return this } fun concat(matrix: Matrix33): Canvas { @@ -1542,14 +1562,24 @@ private external fun _nClipPath(ptr: NativePointer, nativePath: NativePointer, m @ExternalSymbolName("org_jetbrains_skia_Canvas__1nClipRegion") private external fun _nClipRegion(ptr: NativePointer, nativeRegion: NativePointer, mode: Int) +@ExternalSymbolName("org_jetbrains_skia_Canvas__1nTranslate") +private external fun _nTranslate(ptr: NativePointer, dx: Float, dy: Float) + +@ExternalSymbolName("org_jetbrains_skia_Canvas__1nScale") +private external fun _nScale(ptr: NativePointer, sx: Float, sy: Float) + +@ExternalSymbolName("org_jetbrains_skia_Canvas__1nRotate") +private external fun _nRotate(ptr: NativePointer, deg: Float, x: Float, y: Float) + +@ExternalSymbolName("org_jetbrains_skia_Canvas__1nSkew") +private external fun _nSkew(ptr: NativePointer, sx: Float, sy: Float) + @ExternalSymbolName("org_jetbrains_skia_Canvas__1nConcat") private external fun _nConcat(ptr: NativePointer, matrix: InteropPointer) - @ExternalSymbolName("org_jetbrains_skia_Canvas__1nConcat44") private external fun _nConcat44(ptr: NativePointer, matrix: InteropPointer) - @ExternalSymbolName("org_jetbrains_skia_Canvas__1nReadPixels") private external fun _nReadPixels(ptr: NativePointer, bitmapPtr: NativePointer, srcX: Int, srcY: Int): Boolean diff --git a/skiko/src/commonTest/kotlin/org/jetbrains/skia/CanvasTest.kt b/skiko/src/commonTest/kotlin/org/jetbrains/skia/CanvasTest.kt index f1cbd423c..6aaedb3d1 100644 --- a/skiko/src/commonTest/kotlin/org/jetbrains/skia/CanvasTest.kt +++ b/skiko/src/commonTest/kotlin/org/jetbrains/skia/CanvasTest.kt @@ -1,16 +1,18 @@ +@file:Suppress("RemoveRedundantCallsOfConversionMethods") + package org.jetbrains.skia import org.jetbrains.skia.tests.makeFromResource import org.jetbrains.skia.util.assertContentSame import org.jetbrains.skia.util.imageFromIntArray -import org.jetbrains.skiko.tests.SkipJsTarget -import org.jetbrains.skiko.tests.SkipNativeTarget import org.jetbrains.skiko.tests.runTest import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertTrue + class CanvasTest { + @Test fun drawVertices() { val surface = Surface.makeRasterN32Premul(8, 8) @@ -189,4 +191,104 @@ class CanvasTest { assertContentSame(expected = expected, got = surface.makeImageSnapshot(), sensitivity = 0.25) } + + + @Test + fun testTranslate() { + val surface = whiteSurface(4, 4) + surface.canvas.translate(2f, 2f) + surface.canvas.drawBlackPixel(0, 0) + + surface.assertSingleBlackPixelAt(2, 2) + } + + + @Test + fun testScale() { + val surface = whiteSurface(4, 4) + surface.canvas.scale(2f, 2f) + surface.canvas.drawBlackPixel(1, 1) + + surface.assertPixelsMatch( + IntArray(16){ index -> + when (index) { + 10, 11, 14, 15 -> 0xff000000.toInt() + else -> 0xffffffff.toInt() + } + } + ) + } + + + @Test + fun testRotate() { + val surface = whiteSurface(4, 4) + surface.canvas.rotate(deg = 90f) + surface.canvas.drawBlackPixel(3, -1) + + surface.assertSingleBlackPixelAt(0, 3) + } + + + @Test + fun testRotateXY() { + val surface = whiteSurface(4, 4) + surface.canvas.rotate(deg = 90f, x = 2f, y=2f) + surface.canvas.drawBlackPixel(0, 0) + + surface.assertSingleBlackPixelAt(3, 0) + } + + + @Test + fun testSkew() { + val surface = whiteSurface(4, 4) + surface.canvas.skew(sx = 0.5f, sy = 0f) + surface.canvas.drawBlackPixel(0, 2) + + surface.assertPixelsMatch( + IntArray(16){ index -> + when (index) { + // Skewing skews the shape of the pixel itself, so it becomes a parallelogram + 9 -> 0xff3f3f3f.toInt() + 10 -> 0xffbfbfbf.toInt() + else -> 0xffffffff.toInt() + } + } + ) + } + + + private fun whiteSurface(width: Int, height: Int): Surface { + val surface = Surface.makeRasterN32Premul(width, height) + val white = Paint().also { it.setARGB(255, 255, 255, 255) } + surface.canvas.drawRect(Rect(0f, 0f, width.toFloat(), height.toFloat()), white) + return surface + } + + + private fun Canvas.drawBlackPixel(x: Int, y: Int) { + val black = Paint().also { it.setARGB(255, 0, 0, 0) } + drawRect(Rect(x.toFloat(), y.toFloat(), (x + 1).toFloat(), (y + 1).toFloat()), black) + } + + + private fun Surface.assertPixelsMatch(pixArray: IntArray) { + assertContentSame( + expected = imageFromIntArray( + pixArray = pixArray, + imageWidth = width + ), + got = makeImageSnapshot(), + sensitivity = 0.0 + ) + } + + + private fun Surface.assertSingleBlackPixelAt(x: Int, y: Int) { + val pixArray = IntArray(width * height){ 0xffffffff.toInt() } + pixArray[y*width + x] = 0xff000000.toInt() + + assertPixelsMatch(pixArray) + } } diff --git a/skiko/src/jvmMain/cpp/common/Canvas.cc b/skiko/src/jvmMain/cpp/common/Canvas.cc index 1364ee816..2e03930b0 100644 --- a/skiko/src/jvmMain/cpp/common/Canvas.cc +++ b/skiko/src/jvmMain/cpp/common/Canvas.cc @@ -278,6 +278,30 @@ extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_CanvasKt__1nConcat44 canvas->concat(*m); } +extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_CanvasKt__1nTranslate + (JNIEnv* env, jclass jclass, jlong ptr, jfloat dx, jfloat dy) { + SkCanvas* canvas = reinterpret_cast(static_cast(ptr)); + canvas->translate(dx, dy); +} + +extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_CanvasKt__1nScale + (JNIEnv* env, jclass jclass, jlong ptr, jfloat sx, jfloat sy) { + SkCanvas* canvas = reinterpret_cast(static_cast(ptr)); + canvas->scale(sx, sy); +} + +extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_CanvasKt__1nRotate + (JNIEnv* env, jclass jclass, jlong ptr, jfloat deg, jfloat x, jfloat y) { + SkCanvas* canvas = reinterpret_cast(static_cast(ptr)); + canvas->rotate(deg, x, y); +} + +extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_CanvasKt__1nSkew + (JNIEnv* env, jclass jclass, jlong ptr, jfloat sx, jfloat sy) { + SkCanvas* canvas = reinterpret_cast(static_cast(ptr)); + canvas->skew(sx, sy); +} + extern "C" JNIEXPORT jint JNICALL Java_org_jetbrains_skia_CanvasKt__1nReadPixels (JNIEnv* env, jclass jclass, jlong ptr, jlong bitmapPtr, jint srcX, jint srcY) { SkCanvas* canvas = reinterpret_cast(static_cast(ptr)); diff --git a/skiko/src/nativeJsMain/cpp/Canvas.cc b/skiko/src/nativeJsMain/cpp/Canvas.cc index 0beab2868..ce7a20a27 100644 --- a/skiko/src/nativeJsMain/cpp/Canvas.cc +++ b/skiko/src/nativeJsMain/cpp/Canvas.cc @@ -269,6 +269,34 @@ SKIKO_EXPORT void org_jetbrains_skia_Canvas__1nConcat44 } +SKIKO_EXPORT void org_jetbrains_skia_Canvas__1nTranslate + (KNativePointer canvasPtr, KFloat dx, KFloat dy) { + SkCanvas* canvas = reinterpret_cast((canvasPtr)); + canvas->translate(dx, dy); +} + + +SKIKO_EXPORT void org_jetbrains_skia_Canvas__1nScale + (KNativePointer canvasPtr, KFloat sx, KFloat sy) { + SkCanvas* canvas = reinterpret_cast((canvasPtr)); + canvas->scale(sx, sy); +} + + +SKIKO_EXPORT void org_jetbrains_skia_Canvas__1nRotate + (KNativePointer canvasPtr, KFloat deg, KFloat x, KFloat y) { + SkCanvas* canvas = reinterpret_cast((canvasPtr)); + canvas->rotate(deg, x, y); +} + + +SKIKO_EXPORT void org_jetbrains_skia_Canvas__1nSkew + (KNativePointer canvasPtr, KFloat sx, KFloat sy) { + SkCanvas* canvas = reinterpret_cast((canvasPtr)); + canvas->skew(sx, sy); +} + + SKIKO_EXPORT KInt org_jetbrains_skia_Canvas__1nReadPixels (KNativePointer ptr, KNativePointer bitmapPtr, KInt srcX, KInt srcY) { SkCanvas* canvas = reinterpret_cast((ptr));