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:
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