Skip to content

Commit

Permalink
Added a "solar flare" shader and modifier (android#1453)
Browse files Browse the repository at this point in the history
The Kotlin side of this is very similar to the existing yellow striped
modifier, but the shader is entirely different. Right now this isn't
used anywhere but can be tested by swapping the yellowBackground with
solarFlareShaderBackground.
  • Loading branch information
IanGClifton authored Sep 4, 2024
2 parents 8656c87 + 05bd1b6 commit d346a06
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.jetlagged.backgrounds.yellowBackground
import com.example.jetlagged.backgrounds.movingStripesBackground
import com.example.jetlagged.data.JetLaggedHomeScreenViewModel
import com.example.jetlagged.heartrate.HeartRateCard
import com.example.jetlagged.sleep.JetLaggedHeader
Expand All @@ -63,7 +63,7 @@ fun JetLaggedScreen(
.verticalScroll(rememberScrollState())
.background(Color.White)
) {
Column(modifier = Modifier.yellowBackground()) {
Column(modifier = Modifier.movingStripesBackground()) {
JetLaggedHeader(
modifier = Modifier.fillMaxWidth(),
onDrawerClicked = onDrawerClicked
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.jetlagged.backgrounds

import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Brush
import com.example.jetlagged.ui.theme.White
import com.example.jetlagged.ui.theme.Yellow
import com.example.jetlagged.ui.theme.YellowVariant

fun Modifier.simpleGradient(): Modifier =
drawWithCache {
val gradientBrush = Brush.verticalGradient(listOf(Yellow, YellowVariant, White))
onDrawBehind {
drawRect(gradientBrush, alpha = 1f)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.jetlagged.backgrounds

import android.graphics.RuntimeShader
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import com.example.jetlagged.ui.theme.Yellow
import kotlinx.coroutines.launch
import org.intellij.lang.annotations.Language

/**
* Background modifier that displays a custom shader for Android T and above and a linear gradient
* for older versions of Android
*/
fun Modifier.solarFlareShaderBackground(): Modifier =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
this.then(SolarFlareShaderBackgroundElement)
} else {
this.then(Modifier.simpleGradient())
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private data object SolarFlareShaderBackgroundElement :
ModifierNodeElement<SolarFlairShaderBackgroundNode>() {
override fun create() = SolarFlairShaderBackgroundNode()
override fun update(node: SolarFlairShaderBackgroundNode) {
}
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private class SolarFlairShaderBackgroundNode : DrawModifierNode, Modifier.Node() {
private val shader = RuntimeShader(SHADER)
private val shaderBrush = ShaderBrush(shader)
private val time = mutableFloatStateOf(0f)

init {
shader.setColorUniform(
"baseColor",
android.graphics.Color.valueOf(Yellow.red, Yellow.green, Yellow.blue, Yellow.alpha)
)
}

override fun ContentDrawScope.draw() {
shader.setFloatUniform("resolution", size.width, size.height)
shader.setFloatUniform("time", time.floatValue)

drawRect(shaderBrush)
drawContent()
}

override fun onAttach() {
coroutineScope.launch {
while (isAttached) {
withInfiniteAnimationFrameMillis {
time.floatValue = it / 1000f
}
}
}
}
}

@Language("AGSL")
private val SHADER = """
uniform float2 resolution;
uniform float time;
layout(color) uniform half4 baseColor;
const int ITERATIONS = 2;
const float INTENSITY = 100.0;
const float TIME_MULTIPLIER = 0.25;
float4 main(in float2 fragCoord) {
// Slow down the animation to be more soothing
float calculatedTime = time * TIME_MULTIPLIER;
// Coords
float2 uv = fragCoord / resolution.xy;
float2 uvCalc = (uv * 5.0) - (INTENSITY * 2.0);
// Values to adjust per iteration
float2 iterationChange = float2(uvCalc);
float colorPart = 1.0;
for (int i = 0; i < ITERATIONS; i++) {
iterationChange = uvCalc + float2(
cos(calculatedTime + iterationChange.x) +
sin(calculatedTime - iterationChange.y),
cos(calculatedTime - iterationChange.x) +
sin(calculatedTime + iterationChange.y)
);
colorPart += 0.8 / length(
float2(uvCalc.x / (cos(iterationChange.x + calculatedTime) * INTENSITY),
uvCalc.y / (sin(iterationChange.y + calculatedTime) * INTENSITY)
)
);
}
colorPart = 1.6 - (colorPart / float(ITERATIONS));
// Fade out the bottom on a curve
float alpha = 1.0 - (uv.y * uv.y);
// Mix calculated color with the incoming base color
float4 color = float4(colorPart * baseColor.r, colorPart * baseColor.g, colorPart * baseColor.b, alpha);
// Keep all channels within valid bounds of 0.0 and 1.0
return clamp(color, 0.0, 1.0);
}
""".trimIndent()
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,24 @@ import androidx.annotation.RequiresApi
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import com.example.jetlagged.ui.theme.White
import com.example.jetlagged.ui.theme.Yellow
import com.example.jetlagged.ui.theme.YellowVariant
import kotlinx.coroutines.launch
import org.intellij.lang.annotations.Language

private data object YellowBackgroundElement : ModifierNodeElement<YellowBackgroundNode>() {
private data object MovingStripesBackgroundElement :
ModifierNodeElement<MovingStripesBackgroundNode>() {
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun create() = YellowBackgroundNode()
override fun update(node: YellowBackgroundNode) {
override fun create() = MovingStripesBackgroundNode()
override fun update(node: MovingStripesBackgroundNode) {
}
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private class YellowBackgroundNode : DrawModifierNode, Modifier.Node() {
private class MovingStripesBackgroundNode : DrawModifierNode, Modifier.Node() {

private val shader = RuntimeShader(SHADER)
private val shaderBrush = ShaderBrush(shader)
Expand All @@ -59,7 +56,6 @@ private class YellowBackgroundNode : DrawModifierNode, Modifier.Node() {
override fun ContentDrawScope.draw() {
shader.setFloatUniform("resolution", size.width, size.height)
shader.setFloatUniform("time", time.floatValue)
shader.setFloatUniform("waves", 7f)

drawRect(shaderBrush)

Expand All @@ -77,23 +73,17 @@ private class YellowBackgroundNode : DrawModifierNode, Modifier.Node() {
}
}

fun Modifier.yellowBackground(): Modifier =
fun Modifier.movingStripesBackground(): Modifier =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
this.then(YellowBackgroundElement)
this.then(MovingStripesBackgroundElement)
} else {
drawWithCache {
val gradientBrush = Brush.verticalGradient(listOf(Yellow, YellowVariant, White))
onDrawBehind {
drawRect(gradientBrush, alpha = 1f)
}
}
this.then(Modifier.simpleGradient())
}

@Language("AGSL")
val SHADER = """
private val SHADER = """
uniform float2 resolution;
uniform float time;
uniform float waves;
layout(color) uniform half4 color;
float calculateColorMultiplier(float yCoord, float factor) {
Expand All @@ -104,7 +94,7 @@ val SHADER = """
// Config values
const float speedMultiplier = 1.5;
const float waveDensity = 1.0;
//const float loops = iWaves;
const float waves = 7.0;
const float energy = 0.6;
// Calculated values
Expand All @@ -114,8 +104,7 @@ val SHADER = """
float hAdjustment = uv.x * 4.3;
float3 loopColor = vec3(1.0 - rgbColor.r, 1.0 - rgbColor.g, 1.0 - rgbColor.b) / waves;
for (float i = 1.0; i <= 100; i += 1.0) {
if (i > waves) break;
for (float i = 1.0; i <= waves; i += 1.0) {
float loopFactor = i * 0.1;
float sinInput = (timeOffset + hAdjustment) * energy;
float curve = sin(sinInput) * (1.0 - loopFactor) * 0.05;
Expand Down

0 comments on commit d346a06

Please sign in to comment.