fix: include native module sources in git tracking
Fix .gitignore to only exclude root android/ and ios/ (Expo prebuild output) while keeping modules/ native code. Add Android WallpaperService Kotlin sources, GLSL shaders, manifest, and Gradle config.
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.expo/
|
.expo/
|
||||||
android/
|
/android/
|
||||||
ios/
|
/ios/
|
||||||
dist/
|
dist/
|
||||||
.env
|
.env
|
||||||
*.jks
|
*.jks
|
||||||
|
|||||||
27
modules/wallpaper-android/android/build.gradle.kts
Normal file
27
modules/wallpaper-android/android/build.gradle.kts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.lively.wallpaper"
|
||||||
|
compileSdk = 35
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":expo-modules-core"))
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib")
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.SET_WALLPAPER" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.live_wallpaper"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<service
|
||||||
|
android:name=".LiveWallpaperService"
|
||||||
|
android:label="Lively Wallpaper"
|
||||||
|
android:permission="android.permission.BIND_WALLPAPER"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.wallpaper"
|
||||||
|
android:resource="@xml/wallpaper" />
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package com.lively.wallpaper
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.hardware.Sensor
|
||||||
|
import android.hardware.SensorEvent
|
||||||
|
import android.hardware.SensorEventListener
|
||||||
|
import android.hardware.SensorManager
|
||||||
|
import android.opengl.GLSurfaceView
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.service.wallpaper.WallpaperService
|
||||||
|
import android.view.SurfaceHolder
|
||||||
|
import org.json.JSONObject
|
||||||
|
import javax.microedition.khronos.egl.EGLConfig
|
||||||
|
import javax.microedition.khronos.opengles.GL10
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live Wallpaper service that renders animated shaders via OpenGL ES 2.0.
|
||||||
|
*
|
||||||
|
* Reads wallpaper config from SharedPreferences (written by WallpaperAndroidModule).
|
||||||
|
* Config format: { shaderId, imagePath, depthMapPath?, uniforms: { intensity, speed, direction } }
|
||||||
|
*/
|
||||||
|
class LiveWallpaperService : WallpaperService() {
|
||||||
|
|
||||||
|
override fun onCreateEngine(): Engine = LiveEngine()
|
||||||
|
|
||||||
|
inner class LiveEngine : WallpaperService.Engine(), SensorEventListener {
|
||||||
|
|
||||||
|
private var renderer: ShaderRenderer? = null
|
||||||
|
private var glSurface: WallpaperGLSurfaceView? = null
|
||||||
|
|
||||||
|
// Config
|
||||||
|
private var shaderId = "ken-burns"
|
||||||
|
private var imagePath = ""
|
||||||
|
private var depthMapPath: String? = null
|
||||||
|
private var intensity = 0.5f
|
||||||
|
private var speed = 0.3f
|
||||||
|
private var direction = 0f
|
||||||
|
private var targetFps = 24
|
||||||
|
|
||||||
|
// State
|
||||||
|
private var startTime = 0L
|
||||||
|
private var gyroX = 0f
|
||||||
|
private var gyroY = 0f
|
||||||
|
private var isVisible = false
|
||||||
|
|
||||||
|
// Sensors
|
||||||
|
private var sensorManager: SensorManager? = null
|
||||||
|
private var gyroscope: Sensor? = null
|
||||||
|
|
||||||
|
// Render loop
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private val renderRunnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (isVisible) {
|
||||||
|
glSurface?.requestRender()
|
||||||
|
handler.postDelayed(this, (1000L / targetFps))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(surfaceHolder: SurfaceHolder) {
|
||||||
|
super.onCreate(surfaceHolder)
|
||||||
|
setTouchEventsEnabled(false)
|
||||||
|
|
||||||
|
loadConfig()
|
||||||
|
|
||||||
|
sensorManager = getSystemService(Context.SENSOR_SERVICE) as? SensorManager
|
||||||
|
gyroscope = sensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSurfaceCreated(holder: SurfaceHolder) {
|
||||||
|
super.onSurfaceCreated(holder)
|
||||||
|
|
||||||
|
glSurface = WallpaperGLSurfaceView(this@LiveWallpaperService).also { view ->
|
||||||
|
view.setEGLContextClientVersion(2)
|
||||||
|
view.setRenderer(WallpaperRenderer())
|
||||||
|
view.renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVisibilityChanged(visible: Boolean) {
|
||||||
|
super.onVisibilityChanged(visible)
|
||||||
|
isVisible = visible
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
handler.post(renderRunnable)
|
||||||
|
gyroscope?.let {
|
||||||
|
sensorManager?.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handler.removeCallbacks(renderRunnable)
|
||||||
|
sensorManager?.unregisterListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSurfaceDestroyed(holder: SurfaceHolder) {
|
||||||
|
super.onSurfaceDestroyed(holder)
|
||||||
|
isVisible = false
|
||||||
|
handler.removeCallbacks(renderRunnable)
|
||||||
|
sensorManager?.unregisterListener(this)
|
||||||
|
renderer?.release()
|
||||||
|
renderer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSensorChanged(event: SensorEvent?) {
|
||||||
|
event?.let {
|
||||||
|
if (it.sensor.type == Sensor.TYPE_GYROSCOPE) {
|
||||||
|
// Accumulate with damping
|
||||||
|
gyroX += it.values[0] * 0.05f
|
||||||
|
gyroY += it.values[1] * 0.05f
|
||||||
|
// Damping toward zero
|
||||||
|
gyroX *= 0.95f
|
||||||
|
gyroY *= 0.95f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
||||||
|
|
||||||
|
private fun loadConfig() {
|
||||||
|
val prefs = getSharedPreferences("lively_wallpaper", Context.MODE_PRIVATE)
|
||||||
|
val configStr = prefs.getString("config", null) ?: return
|
||||||
|
|
||||||
|
try {
|
||||||
|
val json = JSONObject(configStr)
|
||||||
|
shaderId = json.optString("animation", "ken-burns")
|
||||||
|
imagePath = json.optString("sourceUri", "")
|
||||||
|
depthMapPath = json.optString("depthMapUri", null)
|
||||||
|
targetFps = json.optInt("fps", 24)
|
||||||
|
|
||||||
|
val uniforms = json.optJSONObject("uniforms")
|
||||||
|
if (uniforms != null) {
|
||||||
|
intensity = uniforms.optDouble("intensity", 0.5).toFloat()
|
||||||
|
speed = uniforms.optDouble("speed", 0.3).toFloat()
|
||||||
|
direction = uniforms.optDouble("direction", 0.0).toFloat()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Fallback to defaults on parse error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class WallpaperRenderer : GLSurfaceView.Renderer {
|
||||||
|
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
|
||||||
|
renderer = ShaderRenderer().also { r ->
|
||||||
|
r.initialize(shaderId)
|
||||||
|
if (imagePath.isNotEmpty()) {
|
||||||
|
r.loadTexture(imagePath)
|
||||||
|
}
|
||||||
|
depthMapPath?.let { path ->
|
||||||
|
if (path.isNotEmpty()) r.loadDepthTexture(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
|
||||||
|
android.opengl.GLES20.glViewport(0, 0, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrawFrame(gl: GL10?) {
|
||||||
|
val elapsed = (System.currentTimeMillis() - startTime) / 1000f
|
||||||
|
val holder = surfaceHolder
|
||||||
|
val width = holder.surfaceFrame.width()
|
||||||
|
val height = holder.surfaceFrame.height()
|
||||||
|
|
||||||
|
renderer?.draw(
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
time = elapsed,
|
||||||
|
intensity = intensity,
|
||||||
|
speed = speed,
|
||||||
|
direction = direction,
|
||||||
|
gyroX = gyroX,
|
||||||
|
gyroY = gyroY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom GLSurfaceView that uses the WallpaperService's SurfaceHolder.
|
||||||
|
*/
|
||||||
|
inner class WallpaperGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||||
|
override fun getHolder(): SurfaceHolder = this@LiveEngine.surfaceHolder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
package com.lively.wallpaper
|
||||||
|
|
||||||
|
import android.opengl.GLES20
|
||||||
|
import android.opengl.GLUtils
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.nio.FloatBuffer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenGL ES 2.0 renderer that applies fragment shaders to a texture.
|
||||||
|
* Each animation type maps to a GLSL fragment shader equivalent to the SkSL version.
|
||||||
|
*/
|
||||||
|
class ShaderRenderer {
|
||||||
|
|
||||||
|
private var programId = 0
|
||||||
|
private var textureId = 0
|
||||||
|
private var depthTextureId = 0
|
||||||
|
private var vertexBuffer: FloatBuffer? = null
|
||||||
|
|
||||||
|
// Uniform locations
|
||||||
|
private var uResolution = -1
|
||||||
|
private var uTime = -1
|
||||||
|
private var uIntensity = -1
|
||||||
|
private var uSpeed = -1
|
||||||
|
private var uDirection = -1
|
||||||
|
private var uImage = -1
|
||||||
|
private var uDepthMap = -1
|
||||||
|
private var uGyroX = -1
|
||||||
|
private var uGyroY = -1
|
||||||
|
private var uParticleType = -1
|
||||||
|
|
||||||
|
private val vertexShaderCode = """
|
||||||
|
attribute vec4 aPosition;
|
||||||
|
attribute vec2 aTexCoord;
|
||||||
|
varying vec2 vTexCoord;
|
||||||
|
void main() {
|
||||||
|
gl_Position = aPosition;
|
||||||
|
vTexCoord = aTexCoord;
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
// Fullscreen quad vertices (position + texcoord)
|
||||||
|
private val quadVertices = floatArrayOf(
|
||||||
|
// X, Y, U, V
|
||||||
|
-1.0f, -1.0f, 0.0f, 1.0f,
|
||||||
|
1.0f, -1.0f, 1.0f, 1.0f,
|
||||||
|
-1.0f, 1.0f, 0.0f, 0.0f,
|
||||||
|
1.0f, 1.0f, 1.0f, 0.0f,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun initialize(shaderId: String) {
|
||||||
|
val bb = ByteBuffer.allocateDirect(quadVertices.size * 4)
|
||||||
|
bb.order(ByteOrder.nativeOrder())
|
||||||
|
vertexBuffer = bb.asFloatBuffer().apply {
|
||||||
|
put(quadVertices)
|
||||||
|
position(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val fragmentShaderCode = getFragmentShader(shaderId)
|
||||||
|
programId = createProgram(vertexShaderCode, fragmentShaderCode)
|
||||||
|
|
||||||
|
GLES20.glUseProgram(programId)
|
||||||
|
|
||||||
|
uResolution = GLES20.glGetUniformLocation(programId, "uResolution")
|
||||||
|
uTime = GLES20.glGetUniformLocation(programId, "uTime")
|
||||||
|
uIntensity = GLES20.glGetUniformLocation(programId, "uIntensity")
|
||||||
|
uSpeed = GLES20.glGetUniformLocation(programId, "uSpeed")
|
||||||
|
uDirection = GLES20.glGetUniformLocation(programId, "uDirection")
|
||||||
|
uImage = GLES20.glGetUniformLocation(programId, "uImage")
|
||||||
|
uDepthMap = GLES20.glGetUniformLocation(programId, "uDepthMap")
|
||||||
|
uGyroX = GLES20.glGetUniformLocation(programId, "uGyroX")
|
||||||
|
uGyroY = GLES20.glGetUniformLocation(programId, "uGyroY")
|
||||||
|
uParticleType = GLES20.glGetUniformLocation(programId, "uParticleType")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadTexture(imagePath: String): Boolean {
|
||||||
|
val bitmap = BitmapFactory.decodeFile(imagePath) ?: return false
|
||||||
|
|
||||||
|
val textures = IntArray(1)
|
||||||
|
GLES20.glGenTextures(1, textures, 0)
|
||||||
|
textureId = textures[0]
|
||||||
|
|
||||||
|
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
|
||||||
|
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
|
||||||
|
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
|
||||||
|
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
|
||||||
|
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
|
||||||
|
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
|
||||||
|
bitmap.recycle()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadDepthTexture(depthMapPath: String): Boolean {
|
||||||
|
val bitmap = BitmapFactory.decodeFile(depthMapPath) ?: return false
|
||||||
|
|
||||||
|
val textures = IntArray(1)
|
||||||
|
GLES20.glGenTextures(1, textures, 0)
|
||||||
|
depthTextureId = textures[0]
|
||||||
|
|
||||||
|
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, depthTextureId)
|
||||||
|
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
|
||||||
|
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
|
||||||
|
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
|
||||||
|
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
|
||||||
|
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
|
||||||
|
bitmap.recycle()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun draw(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
time: Float,
|
||||||
|
intensity: Float,
|
||||||
|
speed: Float,
|
||||||
|
direction: Float,
|
||||||
|
gyroX: Float = 0f,
|
||||||
|
gyroY: Float = 0f,
|
||||||
|
particleType: Float = 0f
|
||||||
|
) {
|
||||||
|
GLES20.glUseProgram(programId)
|
||||||
|
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
|
||||||
|
|
||||||
|
// Bind image texture to unit 0
|
||||||
|
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
||||||
|
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
|
||||||
|
if (uImage >= 0) GLES20.glUniform1i(uImage, 0)
|
||||||
|
|
||||||
|
// Bind depth map to unit 1 (if available)
|
||||||
|
if (depthTextureId != 0) {
|
||||||
|
GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
|
||||||
|
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, depthTextureId)
|
||||||
|
if (uDepthMap >= 0) GLES20.glUniform1i(uDepthMap, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set uniforms
|
||||||
|
if (uResolution >= 0) GLES20.glUniform2f(uResolution, width.toFloat(), height.toFloat())
|
||||||
|
if (uTime >= 0) GLES20.glUniform1f(uTime, time)
|
||||||
|
if (uIntensity >= 0) GLES20.glUniform1f(uIntensity, intensity)
|
||||||
|
if (uSpeed >= 0) GLES20.glUniform1f(uSpeed, speed)
|
||||||
|
if (uDirection >= 0) GLES20.glUniform1f(uDirection, direction)
|
||||||
|
if (uGyroX >= 0) GLES20.glUniform1f(uGyroX, gyroX)
|
||||||
|
if (uGyroY >= 0) GLES20.glUniform1f(uGyroY, gyroY)
|
||||||
|
if (uParticleType >= 0) GLES20.glUniform1f(uParticleType, particleType)
|
||||||
|
|
||||||
|
// Draw fullscreen quad
|
||||||
|
val posHandle = GLES20.glGetAttribLocation(programId, "aPosition")
|
||||||
|
val texHandle = GLES20.glGetAttribLocation(programId, "aTexCoord")
|
||||||
|
|
||||||
|
vertexBuffer?.let { buf ->
|
||||||
|
buf.position(0)
|
||||||
|
GLES20.glEnableVertexAttribArray(posHandle)
|
||||||
|
GLES20.glVertexAttribPointer(posHandle, 2, GLES20.GL_FLOAT, false, 16, buf)
|
||||||
|
|
||||||
|
buf.position(2)
|
||||||
|
GLES20.glEnableVertexAttribArray(texHandle)
|
||||||
|
GLES20.glVertexAttribPointer(texHandle, 2, GLES20.GL_FLOAT, false, 16, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
|
||||||
|
|
||||||
|
GLES20.glDisableVertexAttribArray(posHandle)
|
||||||
|
GLES20.glDisableVertexAttribArray(texHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
if (programId != 0) {
|
||||||
|
GLES20.glDeleteProgram(programId)
|
||||||
|
programId = 0
|
||||||
|
}
|
||||||
|
if (textureId != 0) {
|
||||||
|
GLES20.glDeleteTextures(1, intArrayOf(textureId), 0)
|
||||||
|
textureId = 0
|
||||||
|
}
|
||||||
|
if (depthTextureId != 0) {
|
||||||
|
GLES20.glDeleteTextures(1, intArrayOf(depthTextureId), 0)
|
||||||
|
depthTextureId = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createProgram(vertexSource: String, fragmentSource: String): Int {
|
||||||
|
val vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexSource)
|
||||||
|
val fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource)
|
||||||
|
|
||||||
|
val program = GLES20.glCreateProgram()
|
||||||
|
GLES20.glAttachShader(program, vertexShader)
|
||||||
|
GLES20.glAttachShader(program, fragmentShader)
|
||||||
|
GLES20.glLinkProgram(program)
|
||||||
|
|
||||||
|
val linkStatus = IntArray(1)
|
||||||
|
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0)
|
||||||
|
if (linkStatus[0] == 0) {
|
||||||
|
val log = GLES20.glGetProgramInfoLog(program)
|
||||||
|
GLES20.glDeleteProgram(program)
|
||||||
|
throw RuntimeException("Program link failed: $log")
|
||||||
|
}
|
||||||
|
|
||||||
|
GLES20.glDeleteShader(vertexShader)
|
||||||
|
GLES20.glDeleteShader(fragmentShader)
|
||||||
|
|
||||||
|
return program
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compileShader(type: Int, source: String): Int {
|
||||||
|
val shader = GLES20.glCreateShader(type)
|
||||||
|
GLES20.glShaderSource(shader, source)
|
||||||
|
GLES20.glCompileShader(shader)
|
||||||
|
|
||||||
|
val compileStatus = IntArray(1)
|
||||||
|
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0)
|
||||||
|
if (compileStatus[0] == 0) {
|
||||||
|
val log = GLES20.glGetShaderInfoLog(shader)
|
||||||
|
GLES20.glDeleteShader(shader)
|
||||||
|
throw RuntimeException("Shader compile failed: $log")
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Maps animation IDs to GLSL fragment shaders.
|
||||||
|
* These are GLSL ES 2.0 equivalents of the SkSL shaders in src/shaders/.
|
||||||
|
*/
|
||||||
|
fun getFragmentShader(shaderId: String): String = when (shaderId) {
|
||||||
|
"ken-burns" -> """
|
||||||
|
precision mediump float;
|
||||||
|
varying vec2 vTexCoord;
|
||||||
|
uniform sampler2D uImage;
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uIntensity;
|
||||||
|
uniform float uSpeed;
|
||||||
|
uniform float uDirection;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float t = uTime * uSpeed * 0.1;
|
||||||
|
float zoom = 1.0 + uIntensity * 0.15 * (0.5 + 0.5 * sin(t));
|
||||||
|
float panX = cos(uDirection) * uIntensity * 0.05 * sin(t * 0.7);
|
||||||
|
float panY = sin(uDirection) * uIntensity * 0.05 * cos(t * 0.7);
|
||||||
|
vec2 center = vec2(0.5);
|
||||||
|
vec2 uv = (vTexCoord - center) / zoom + center;
|
||||||
|
uv += vec2(panX, panY);
|
||||||
|
uv = clamp(uv, 0.0, 1.0);
|
||||||
|
gl_FragColor = texture2D(uImage, uv);
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
"color-shift" -> """
|
||||||
|
precision mediump float;
|
||||||
|
varying vec2 vTexCoord;
|
||||||
|
uniform sampler2D uImage;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uIntensity;
|
||||||
|
uniform float uSpeed;
|
||||||
|
|
||||||
|
vec3 rgb2hsl(vec3 c) {
|
||||||
|
float mx = max(c.r, max(c.g, c.b));
|
||||||
|
float mn = min(c.r, min(c.g, c.b));
|
||||||
|
float l = (mx + mn) * 0.5;
|
||||||
|
float s = 0.0, h = 0.0;
|
||||||
|
if (mx != mn) {
|
||||||
|
float d = mx - mn;
|
||||||
|
s = l > 0.5 ? d / (2.0 - mx - mn) : d / (mx + mn);
|
||||||
|
if (mx == c.r) h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
|
||||||
|
else if (mx == c.g) h = (c.b - c.r) / d + 2.0;
|
||||||
|
else h = (c.r - c.g) / d + 4.0;
|
||||||
|
h /= 6.0;
|
||||||
|
}
|
||||||
|
return vec3(h, s, l);
|
||||||
|
}
|
||||||
|
|
||||||
|
float hue2rgb(float p, float q, float t) {
|
||||||
|
float tt = t;
|
||||||
|
if (tt < 0.0) tt += 1.0;
|
||||||
|
if (tt > 1.0) tt -= 1.0;
|
||||||
|
if (tt < 1.0/6.0) return p + (q - p) * 6.0 * tt;
|
||||||
|
if (tt < 1.0/2.0) return q;
|
||||||
|
if (tt < 2.0/3.0) return p + (q - p) * (2.0/3.0 - tt) * 6.0;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hsl2rgb(vec3 hsl) {
|
||||||
|
if (hsl.y == 0.0) return vec3(hsl.z);
|
||||||
|
float q = hsl.z < 0.5 ? hsl.z * (1.0 + hsl.y) : hsl.z + hsl.y - hsl.z * hsl.y;
|
||||||
|
float p = 2.0 * hsl.z - q;
|
||||||
|
return vec3(hue2rgb(p,q,hsl.x+1.0/3.0), hue2rgb(p,q,hsl.x), hue2rgb(p,q,hsl.x-1.0/3.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 color = texture2D(uImage, vTexCoord);
|
||||||
|
vec3 hsl = rgb2hsl(color.rgb);
|
||||||
|
float t = uTime * uSpeed * 0.15;
|
||||||
|
hsl.x = fract(hsl.x + uIntensity * 0.1 * sin(t));
|
||||||
|
hsl.y = clamp(hsl.y + uIntensity * 0.15 * sin(t * 1.3), 0.0, 1.0);
|
||||||
|
hsl.z = clamp(hsl.z + uIntensity * 0.03 * sin(t * 0.8), 0.0, 1.0);
|
||||||
|
gl_FragColor = vec4(hsl2rgb(hsl), color.a);
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
"particles" -> """
|
||||||
|
precision mediump float;
|
||||||
|
varying vec2 vTexCoord;
|
||||||
|
uniform sampler2D uImage;
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uIntensity;
|
||||||
|
uniform float uSpeed;
|
||||||
|
uniform float uParticleType;
|
||||||
|
|
||||||
|
float hash(vec2 p) {
|
||||||
|
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
||||||
|
p3 += dot(p3, p3.yzx + 33.33);
|
||||||
|
return fract((p3.x + p3.y) * p3.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
float snow(vec2 uv, float layer) {
|
||||||
|
float t = uTime * uSpeed * 0.4 + layer * 12.345;
|
||||||
|
vec2 grid = floor(uv * (8.0 + layer * 6.0));
|
||||||
|
vec2 f = fract(uv * (8.0 + layer * 6.0));
|
||||||
|
float rnd = hash(grid);
|
||||||
|
vec2 center = vec2(0.5 + 0.3*sin(t+rnd*6.28), fract(rnd+t*(0.1+rnd*0.08)));
|
||||||
|
float d = length(f - center);
|
||||||
|
float size = 0.04 + rnd * 0.06;
|
||||||
|
return smoothstep(size, size*0.3, d) * (0.4+0.6*rnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
float bokeh(vec2 uv, float layer) {
|
||||||
|
float t = uTime * uSpeed * 0.2 + layer * 5.67;
|
||||||
|
vec2 grid = floor(uv * (3.0 + layer * 2.0));
|
||||||
|
vec2 f = fract(uv * (3.0 + layer * 2.0));
|
||||||
|
float rnd = hash(grid);
|
||||||
|
vec2 center = vec2(0.5+0.2*sin(t*0.5+rnd*6.28), 0.5+0.2*cos(t*0.3+rnd*6.28));
|
||||||
|
float d = length(f - center);
|
||||||
|
float size = 0.1 + rnd * 0.15;
|
||||||
|
float ring = smoothstep(size,size-0.02,d) - smoothstep(size-0.03,size-0.06,d);
|
||||||
|
float fill = smoothstep(size,size*0.2,d) * 0.3;
|
||||||
|
return (ring+fill) * (0.3+0.7*rnd) * step(0.3, rnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 color = texture2D(uImage, vTexCoord);
|
||||||
|
float particles = 0.0;
|
||||||
|
for (float i = 0.0; i < 3.0; i += 1.0) {
|
||||||
|
if (uParticleType < 0.5) particles += snow(vTexCoord, i);
|
||||||
|
else particles += bokeh(vTexCoord, i);
|
||||||
|
}
|
||||||
|
particles *= uIntensity;
|
||||||
|
gl_FragColor = vec4(clamp(color.rgb + vec3(particles), 0.0, 1.0), color.a);
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
"vignette-pulse" -> """
|
||||||
|
precision mediump float;
|
||||||
|
varying vec2 vTexCoord;
|
||||||
|
uniform sampler2D uImage;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uIntensity;
|
||||||
|
uniform float uSpeed;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 color = texture2D(uImage, vTexCoord);
|
||||||
|
vec2 center = vTexCoord - 0.5;
|
||||||
|
float dist = length(center);
|
||||||
|
float t = uTime * uSpeed * 0.3;
|
||||||
|
float strength = uIntensity * (0.6 + 0.4 * sin(t));
|
||||||
|
float vignette = 1.0 - smoothstep(0.2, 0.85, dist * (1.0 + strength));
|
||||||
|
vec3 tinted = color.rgb * vignette;
|
||||||
|
float tint = (1.0 - vignette) * 0.15 * uIntensity;
|
||||||
|
tinted.b += tint * 0.3;
|
||||||
|
gl_FragColor = vec4(clamp(tinted, 0.0, 1.0), color.a);
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
"glitch" -> """
|
||||||
|
precision mediump float;
|
||||||
|
varying vec2 vTexCoord;
|
||||||
|
uniform sampler2D uImage;
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uIntensity;
|
||||||
|
uniform float uSpeed;
|
||||||
|
|
||||||
|
float hash(float n) { return fract(sin(n) * 43758.5453); }
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float t = uTime * uSpeed * 0.5;
|
||||||
|
float trigger = step(0.92, sin(t*2.0)*sin(t*7.3));
|
||||||
|
float amount = uIntensity * trigger;
|
||||||
|
float shift = uIntensity * 0.003 + amount * 0.015;
|
||||||
|
vec2 uvR = vec2(vTexCoord.x + shift, vTexCoord.y);
|
||||||
|
vec2 uvB = vec2(vTexCoord.x - shift, vTexCoord.y);
|
||||||
|
uvR = clamp(uvR, 0.0, 1.0);
|
||||||
|
uvB = clamp(uvB, 0.0, 1.0);
|
||||||
|
float r = texture2D(uImage, uvR).r;
|
||||||
|
float g = texture2D(uImage, vTexCoord).g;
|
||||||
|
float b = texture2D(uImage, uvB).b;
|
||||||
|
gl_FragColor = vec4(r, g, b, 1.0);
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
"depth-parallax" -> """
|
||||||
|
precision mediump float;
|
||||||
|
varying vec2 vTexCoord;
|
||||||
|
uniform sampler2D uImage;
|
||||||
|
uniform sampler2D uDepthMap;
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uIntensity;
|
||||||
|
uniform float uSpeed;
|
||||||
|
uniform float uGyroX;
|
||||||
|
uniform float uGyroY;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float depth = texture2D(uDepthMap, vTexCoord).r;
|
||||||
|
float t = uTime * uSpeed * 0.2;
|
||||||
|
vec2 offset = vec2(
|
||||||
|
uGyroX * 0.8 + sin(t) * 0.3,
|
||||||
|
uGyroY * 0.8 + cos(t * 0.7) * 0.3
|
||||||
|
);
|
||||||
|
vec2 displacement = offset * depth * uIntensity * 0.04;
|
||||||
|
vec2 uv = clamp(vTexCoord + displacement, 0.0, 1.0);
|
||||||
|
gl_FragColor = texture2D(uImage, uv);
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
else -> """
|
||||||
|
precision mediump float;
|
||||||
|
varying vec2 vTexCoord;
|
||||||
|
uniform sampler2D uImage;
|
||||||
|
void main() { gl_FragColor = texture2D(uImage, vTexCoord); }
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.lively.wallpaper
|
||||||
|
|
||||||
|
import android.app.WallpaperManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import expo.modules.kotlin.modules.Module
|
||||||
|
import expo.modules.kotlin.modules.ModuleDefinition
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class WallpaperAndroidModule : Module() {
|
||||||
|
|
||||||
|
override fun definition() = ModuleDefinition {
|
||||||
|
Name("WallpaperAndroid")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save wallpaper configuration to SharedPreferences.
|
||||||
|
* The LiveWallpaperService reads this on start.
|
||||||
|
*/
|
||||||
|
AsyncFunction("saveConfig") { configJson: String ->
|
||||||
|
val context = appContext.reactContext ?: throw Exception("Context unavailable")
|
||||||
|
val prefs = context.getSharedPreferences("lively_wallpaper", Context.MODE_PRIVATE)
|
||||||
|
prefs.edit().putString("config", configJson).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the system live wallpaper picker with our service pre-selected.
|
||||||
|
*/
|
||||||
|
AsyncFunction("setLiveWallpaper") {
|
||||||
|
val context = appContext.reactContext ?: throw Exception("Context unavailable")
|
||||||
|
val intent = Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER).apply {
|
||||||
|
putExtra(
|
||||||
|
WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,
|
||||||
|
ComponentName(context.packageName, LiveWallpaperService::class.java.name)
|
||||||
|
)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if live wallpapers are supported on this device.
|
||||||
|
*/
|
||||||
|
Function("isLiveWallpaperSupported") {
|
||||||
|
val context = appContext.reactContext ?: return@Function false
|
||||||
|
val pm = context.packageManager
|
||||||
|
pm.hasSystemFeature("android.software.live_wallpaper")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently active wallpaper config (if it's ours).
|
||||||
|
*/
|
||||||
|
Function("getCurrentConfig") {
|
||||||
|
val context = appContext.reactContext ?: return@Function null
|
||||||
|
val prefs = context.getSharedPreferences("lively_wallpaper", Context.MODE_PRIVATE)
|
||||||
|
prefs.getString("config", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<wallpaper
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:description="@string/app_name"
|
||||||
|
android:thumbnail="@mipmap/ic_launcher" />
|
||||||
Reference in New Issue
Block a user