From bd90c458cc2226afe29057012a04d18e63d9afc0 Mon Sep 17 00:00:00 2001 From: LaoLittle Date: Mon, 30 May 2022 20:52:15 +0800 Subject: [PATCH] add marble --- src/main/kotlin/math/ImageMath.kt | 37 ++++ src/main/kotlin/math/Noise.kt | 304 ++++++++++++++++++++++++++++++ src/main/kotlin/meme/Marble.kt | 100 ++++++++++ 3 files changed, 441 insertions(+) create mode 100644 src/main/kotlin/math/ImageMath.kt create mode 100644 src/main/kotlin/math/Noise.kt create mode 100644 src/main/kotlin/meme/Marble.kt diff --git a/src/main/kotlin/math/ImageMath.kt b/src/main/kotlin/math/ImageMath.kt new file mode 100644 index 0000000..6ce10db --- /dev/null +++ b/src/main/kotlin/math/ImageMath.kt @@ -0,0 +1,37 @@ +package org.laolittle.plugin.draw.math + +fun bilinearInterpolate(x: Float, y: Float, nw: Int, ne: Int, sw: Int, se: Int): Int { + var m0: Float + var m1: Float + val a0 = nw shr 24 and 0xff + val r0 = nw shr 16 and 0xff + val g0 = nw shr 8 and 0xff + val b0 = nw and 0xff + val a1 = ne shr 24 and 0xff + val r1 = ne shr 16 and 0xff + val g1 = ne shr 8 and 0xff + val b1 = ne and 0xff + val a2 = sw shr 24 and 0xff + val r2 = sw shr 16 and 0xff + val g2 = sw shr 8 and 0xff + val b2 = sw and 0xff + val a3 = se shr 24 and 0xff + val r3 = se shr 16 and 0xff + val g3 = se shr 8 and 0xff + val b3 = se and 0xff + val cx = 1.0f - x + val cy = 1.0f - y + m0 = cx * a0 + x * a1 + m1 = cx * a2 + x * a3 + val a = (cy * m0 + y * m1).toInt() + m0 = cx * r0 + x * r1 + m1 = cx * r2 + x * r3 + val r = (cy * m0 + y * m1).toInt() + m0 = cx * g0 + x * g1 + m1 = cx * g2 + x * g3 + val g = (cy * m0 + y * m1).toInt() + m0 = cx * b0 + x * b1 + m1 = cx * b2 + x * b3 + val b = (cy * m0 + y * m1).toInt() + return a shl 24 or (r shl 16) or (g shl 8) or b +} \ No newline at end of file diff --git a/src/main/kotlin/math/Noise.kt b/src/main/kotlin/math/Noise.kt new file mode 100644 index 0000000..dd79e52 --- /dev/null +++ b/src/main/kotlin/math/Noise.kt @@ -0,0 +1,304 @@ +package org.laolittle.plugin.draw.math + +import java.util.* +import kotlin.math.abs +import kotlin.math.sqrt + + +/** + * Perlin Noise functions + */ +object Noise { + fun evaluate(x: Float): Float { + return noise1(x) + } + + fun evaluate(x: Float, y: Float): Float { + return noise2(x, y) + } + + fun evaluate(x: Float, y: Float, z: Float): Float { + return noise3(x, y, z) + } + + private val randomGenerator = Random() + + /** + * Compute turbulence using Perlin noise. + * @param x the x value + * @param y the y value + * @param octaves number of octaves of turbulence + * @return turbulence value at (x,y) + */ + fun turbulence2(x: Float, y: Float, octaves: Float): Float { + var t = 0.0f + var f = 1.0f + while (f <= octaves) { + t += abs(noise2(f * x, f * y)) / f + f *= 2f + } + return t + } + + /** + * Compute turbulence using Perlin noise. + * @param x the x value + * @param y the y value + * @param octaves number of octaves of turbulence + * @return turbulence value at (x,y) + */ + fun turbulence3(x: Float, y: Float, z: Float, octaves: Float): Float { + var t = 0.0f + var f = 1.0f + while (f <= octaves) { + t += abs(noise3(f * x, f * y, f * z)) / f + f *= 2f + } + return t + } + + private const val B = 0x100 + private const val BM = 0xff + private const val N = 0x1000 + private val p = IntArray(B + B + 2) + private val g3 = Array(B + B + 2) { + FloatArray( + 3 + ) + } + private val g2 = Array(B + B + 2) { + FloatArray( + 2 + ) + } + private val g1 = FloatArray(B + B + 2) + private var start = true + private fun sCurve(t: Float): Float { + return t * t * (3.0f - 2.0f * t) + } + + /** + * Compute 1-dimensional Perlin noise. + * @param x the x value + * @return noise value at x in the range -1..1 + */ + fun noise1(x: Float): Float { + val bx0: Int + val rx0: Float + if (start) { + start = false + init() + } + val t: Float = x + N + bx0 = t.toInt() and BM + val bx1: Int = bx0 + 1 and BM + rx0 = t - t.toInt() + val rx1: Float = rx0 - 1.0f + val sx: Float = sCurve(rx0) + val u: Float = rx0 * g1[p[bx0]] + val v: Float = rx1 * g1[p[bx1]] + return 2.3f * lerp(sx, u, v) + } + + /** + * Compute 2-dimensional Perlin noise. + * @param x the x coordinate + * @param y the y coordinate + * @return noise value at (x,y) + */ + fun noise2(x: Float, y: Float): Float { + val bx0: Int + val by0: Int + val b00: Int + val b10: Int + val b01: Int + val b11: Int + val rx0: Float + val ry0: Float + val a: Float + val b: Float + var u: Float + var v: Float + val j: Int + if (start) { + start = false + init() + } + var t: Float = x + N + bx0 = t.toInt() and BM + val bx1: Int = bx0 + 1 and BM + rx0 = t - t.toInt() + val rx1: Float = rx0 - 1.0f + t = y + N + by0 = t.toInt() and BM + val by1: Int = by0 + 1 and BM + ry0 = t - t.toInt() + val ry1: Float = ry0 - 1.0f + val i: Int = p[bx0] + j = p[bx1] + b00 = p[i + by0] + b10 = p[j + by0] + b01 = p[i + by1] + b11 = p[j + by1] + val sx: Float = sCurve(rx0) + val sy: Float = sCurve(ry0) + var q: FloatArray = g2[b00] + u = rx0 * q[0] + ry0 * q[1] + q = g2[b10] + v = rx1 * q[0] + ry0 * q[1] + a = lerp(sx, u, v) + q = g2[b01] + u = rx0 * q[0] + ry1 * q[1] + q = g2[b11] + v = rx1 * q[0] + ry1 * q[1] + b = lerp(sx, u, v) + return 1.5f * lerp(sy, a, b) + } + + /** + * Compute 3-dimensional Perlin noise. + * @param x the x coordinate + * @param y the y coordinate + * @param y the y coordinate + * @return noise value at (x,y,z) + */ + fun noise3(x: Float, y: Float, z: Float): Float { + val bx0: Int + val by0: Int + val bz0: Int + val b00: Int + val b10: Int + val b01: Int + val b11: Int + val rx0: Float + val ry0: Float + val rz0: Float + var a: Float + var b: Float + val c: Float + val d: Float + var u: Float + var v: Float + val j: Int + if (start) { + start = false + init() + } + var t: Float = x + N + bx0 = t.toInt() and BM + val bx1: Int = bx0 + 1 and BM + rx0 = t - t.toInt() + val rx1: Float = rx0 - 1.0f + t = y + N + by0 = t.toInt() and BM + val by1: Int = by0 + 1 and BM + ry0 = t - t.toInt() + val ry1: Float = ry0 - 1.0f + t = z + N + bz0 = t.toInt() and BM + val bz1: Int = bz0 + 1 and BM + rz0 = t - t.toInt() + val rz1: Float = rz0 - 1.0f + val i: Int = p[bx0] + j = p[bx1] + b00 = p[i + by0] + b10 = p[j + by0] + b01 = p[i + by1] + b11 = p[j + by1] + t = sCurve(rx0) + val sy: Float = sCurve(ry0) + val sz: Float = sCurve(rz0) + var q: FloatArray = g3[b00 + bz0] + u = rx0 * q[0] + ry0 * q[1] + rz0 * q[2] + q = g3[b10 + bz0] + v = rx1 * q[0] + ry0 * q[1] + rz0 * q[2] + a = lerp(t, u, v) + q = g3[b01 + bz0] + u = rx0 * q[0] + ry1 * q[1] + rz0 * q[2] + q = g3[b11 + bz0] + v = rx1 * q[0] + ry1 * q[1] + rz0 * q[2] + b = lerp(t, u, v) + c = lerp(sy, a, b) + q = g3[b00 + bz1] + u = rx0 * q[0] + ry0 * q[1] + rz1 * q[2] + q = g3[b10 + bz1] + v = rx1 * q[0] + ry0 * q[1] + rz1 * q[2] + a = lerp(t, u, v) + q = g3[b01 + bz1] + u = rx0 * q[0] + ry1 * q[1] + rz1 * q[2] + q = g3[b11 + bz1] + v = rx1 * q[0] + ry1 * q[1] + rz1 * q[2] + b = lerp(t, u, v) + d = lerp(sy, a, b) + return 1.5f * lerp(sz, c, d) + } + + fun lerp(t: Float, a: Float, b: Float): Float { + return a + t * (b - a) + } + + private fun normalize2(v: FloatArray) { + val s = sqrt((v[0] * v[0] + v[1] * v[1]).toDouble()).toFloat() + v[0] = v[0] / s + v[1] = v[1] / s + } + + fun normalize3(v: FloatArray) { + val s = sqrt((v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).toDouble()).toFloat() + v[0] = v[0] / s + v[1] = v[1] / s + v[2] = v[2] / s + } + + private fun random(): Int { + return randomGenerator.nextInt() and 0x7fffffff + } + + private fun init() { + var j: Int + var k: Int + var i = 0 + while (i < B) { + p[i] = i + g1[i] = (random() % (B + B) - B).toFloat() / B + j = 0 + while (j < 2) { + g2[i][j] = (random() % (B + B) - B).toFloat() / B + j++ + } + normalize2(g2[i]) + j = 0 + while (j < 3) { + g3[i][j] = (random() % (B + B) - B).toFloat() / B + j++ + } + normalize3(g3[i]) + i++ + } + i = B - 1 + while (i >= 0) { + k = p[i] + p[i] = p[random() % B.also { + j = it + }] + p[j] = k + i-- + } + i = 0 + while (i < B + 2) { + p[B + i] = p[i] + g1[B + i] = g1[i] + j = 0 + while (j < 2) { + g2[B + i][j] = g2[i][j] + j++ + } + j = 0 + while (j < 3) { + g3[B + i][j] = g3[i][j] + j++ + } + i++ + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/meme/Marble.kt b/src/main/kotlin/meme/Marble.kt new file mode 100644 index 0000000..d20cfd7 --- /dev/null +++ b/src/main/kotlin/meme/Marble.kt @@ -0,0 +1,100 @@ +package org.laolittle.plugin.draw.meme + +import org.jetbrains.skia.Bitmap +import org.jetbrains.skia.IRect +import org.jetbrains.skia.Image +import org.laolittle.plugin.draw.math.Noise +import org.laolittle.plugin.draw.math.bilinearInterpolate +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.floor +import kotlin.math.sin + +fun marble(image: Image, marble: MarbleFilter = MarbleFilter()): Bitmap { + //val surface = Surface.makeRaster(image.imageInfo) + val bitmap = Bitmap.makeFromImage(image) + + val dst = Bitmap().apply { + allocPixels(image.imageInfo) + } + + val h = bitmap.height + val w = bitmap.width + + val h1 = h - 1 + val w1 = w - 1 + + val out = FloatArray(2) + + for (y in 0 until h) for (x in 0 until w) { + marble.transformInverse(x, y, out) + val srcX = floor(out[0]).toInt() + val srcY = floor(out[1]).toInt() + + val xWeight = out[0] - srcX + val yWeight = out[1] - srcY + + val nw: Int + val ne: Int + val sw: Int + val se: Int + + if (srcX in 0 until w1 && srcY in 0 until h1) { + nw = bitmap.getColor(srcX, srcY) + ne = bitmap.getColor(srcX + 1, srcY) + sw = bitmap.getColor(srcX, srcY + 1) + se = bitmap.getColor(srcX + 1, srcY + 1) + } else { + nw = bitmap.pixel(srcX, srcY, w, h) + ne = bitmap.pixel(srcX + 1, srcY, w, h) + sw = bitmap.pixel(srcX, srcY + 1, w, h) + se = bitmap.pixel(srcX + 1, srcY + 1, w, h) + } + + val pixel = bilinearInterpolate(xWeight, yWeight, nw, ne, sw, se) + + dst.erase(pixel, IRect.makeXYWH(x, y, 1, 1)) + } + + return dst +} + +private fun Bitmap.pixel(x: Int, y: Int, w: Int, h: Int): Int { + val clampX = x.coerceAtLeast(0).coerceAtMost(w - 1) + val clampY = y.coerceAtLeast(0).coerceAtMost(h - 1) + + return getColor(clampX, clampY) +} + +private const val TWO_PI = PI * 2 + +class MarbleFilter( + private val xScale: Float = 4f, + private val yScale: Float = 4f, + private val turbulence: Float = 1f +) { + private val sinTable = FloatArray(256) + private val cosTable = FloatArray(256) + + private fun displacementMap(x: Int, y: Int): Int { + return clamp((127 * (1 + Noise.noise2(x / xScale, y / xScale))).toInt()) + } + + fun transformInverse(x: Int, y: Int, out: FloatArray) { + val displacement = displacementMap(x, y) + out[0] = x + sinTable[displacement] + out[1] = y + cosTable[displacement] + } + + init { + repeat(256) { + val angle = (TWO_PI * it / 256f) * turbulence + sinTable[it] = (-yScale * sin(angle)).toFloat() + cosTable[it] = (yScale * cos(angle)).toFloat() + } + } + + companion object { + fun clamp(value: Int) = value.coerceAtLeast(0).coerceAtMost(255) + } +} \ No newline at end of file