Skip to content

Commit

Permalink
Tweak implementation of shader based painters
Browse files Browse the repository at this point in the history
Unify declaration and implementation of shader based fill / decoration painters.

Fill painters tracked in #4
Decoration painters tracked in #7
  • Loading branch information
kirill-grouchnikov committed Dec 31, 2021
1 parent 135ba80 commit bd80dfb
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 42 deletions.
Expand Up @@ -29,7 +29,16 @@
*/
package org.pushingpixels.aurora.theming.painter.decoration

import org.pushingpixels.aurora.theming.utils.getBrushedMetalShader
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.Density
import org.jetbrains.skia.Data
import org.jetbrains.skia.Shader
import org.pushingpixels.aurora.theming.colorscheme.AuroraColorScheme
import org.pushingpixels.aurora.theming.utils.getBrushedMetalBaseShader
import org.pushingpixels.aurora.theming.utils.getDuotoneEffect
import java.nio.ByteBuffer
import java.nio.ByteOrder

/**
* Implementation of [AuroraDecorationPainter] that uses brushed metal
Expand All @@ -38,14 +47,32 @@ import org.pushingpixels.aurora.theming.utils.getBrushedMetalShader
* @author Kirill Grouchnikov
*/
class BrushedMetalDecorationPainter : ShaderWrapperDecorationPainter(
shaderGenerator = {
getBrushedMetalShader(
colorLight = it.lightColor,
colorDark = it.darkColor,
alpha = 0.2f
)
},
runtimeEffect = getDuotoneEffect(),
baseShader = getBrushedMetalBaseShader(),
baseDecorationPainter = ArcDecorationPainter()
) {
override val displayName = "Brushed Metal"

override fun getShaderData(
density: Density,
componentSize: Size,
offsetFromRoot: Offset,
fillScheme: AuroraColorScheme
): Data {
val dataBuffer = ByteBuffer.allocate(36).order(ByteOrder.LITTLE_ENDIAN)
// RGBA colorLight
dataBuffer.putFloat(0, fillScheme.lightColor.red)
dataBuffer.putFloat(4, fillScheme.lightColor.green)
dataBuffer.putFloat(8, fillScheme.lightColor.blue)
dataBuffer.putFloat(12, fillScheme.lightColor.alpha)
// RGBA colorDark
dataBuffer.putFloat(16, fillScheme.darkColor.red)
dataBuffer.putFloat(20, fillScheme.darkColor.green)
dataBuffer.putFloat(24, fillScheme.darkColor.blue)
dataBuffer.putFloat(28, fillScheme.darkColor.alpha)
// Alpha
dataBuffer.putFloat(32, 0.2f)

return Data.makeFromBytes(dataBuffer.array())
}
}
Expand Up @@ -29,7 +29,15 @@
*/
package org.pushingpixels.aurora.theming.painter.decoration

import org.pushingpixels.aurora.theming.utils.getNoiseShader
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.Density
import org.jetbrains.skia.Data
import org.jetbrains.skia.Shader
import org.pushingpixels.aurora.theming.colorscheme.AuroraColorScheme
import org.pushingpixels.aurora.theming.utils.getDuotoneEffect
import java.nio.ByteBuffer
import java.nio.ByteOrder

/**
* Implementation of [AuroraDecorationPainter] that uses marble noise
Expand All @@ -38,19 +46,41 @@ import org.pushingpixels.aurora.theming.utils.getNoiseShader
* @author Kirill Grouchnikov
*/
class MarbleNoiseDecorationPainter(
textureAlpha: Float,
val textureAlpha: Float,
baseDecorationPainter: AuroraDecorationPainter? = null
) :
ShaderWrapperDecorationPainter(
shaderGenerator = {
getNoiseShader(
colorLight = it.lightColor,
colorDark = it.darkColor,
alpha = textureAlpha,
baseFrequency = 0.25f
)
},
baseDecorationPainter = baseDecorationPainter
) {
) : ShaderWrapperDecorationPainter(
runtimeEffect = getDuotoneEffect(),
baseShader = Shader.makeFractalNoise(
baseFrequencyX = 0.25f,
baseFrequencyY = 0.25f,
numOctaves = 1,
seed = 0.0f
),
baseDecorationPainter = baseDecorationPainter
) {

override val displayName = "Marble Noise"

override fun getShaderData(
density: Density,
componentSize: Size,
offsetFromRoot: Offset,
fillScheme: AuroraColorScheme
): Data {
val dataBuffer = ByteBuffer.allocate(36).order(ByteOrder.LITTLE_ENDIAN)
// RGBA colorLight
dataBuffer.putFloat(0, fillScheme.lightColor.red)
dataBuffer.putFloat(4, fillScheme.lightColor.green)
dataBuffer.putFloat(8, fillScheme.lightColor.blue)
dataBuffer.putFloat(12, fillScheme.lightColor.alpha)
// RGBA colorDark
dataBuffer.putFloat(16, fillScheme.darkColor.red)
dataBuffer.putFloat(20, fillScheme.darkColor.green)
dataBuffer.putFloat(24, fillScheme.darkColor.blue)
dataBuffer.putFloat(28, fillScheme.darkColor.alpha)
// Alpha
dataBuffer.putFloat(32, textureAlpha)

return Data.makeFromBytes(dataBuffer.array())
}
}
Expand Up @@ -21,6 +21,9 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.unit.Density
import org.jetbrains.skia.Data
import org.jetbrains.skia.RuntimeEffect
import org.jetbrains.skia.Shader
import org.pushingpixels.aurora.theming.DecorationAreaType
import org.pushingpixels.aurora.theming.colorscheme.AuroraColorScheme
Expand All @@ -32,10 +35,19 @@ import org.pushingpixels.aurora.theming.colorscheme.AuroraColorScheme
* @author Kirill Grouchnikov
*/
abstract class ShaderWrapperDecorationPainter(
val shaderGenerator: (AuroraColorScheme) -> Shader,
val runtimeEffect: RuntimeEffect,
baseShader: Shader? = null,
val baseDecorationPainter: AuroraDecorationPainter? = null
) : AuroraDecorationPainter {
private val shaders = hashMapOf<String, Shader>()
private val shaderChildren: Array<Shader?>? =
if (baseShader != null) arrayOf(baseShader) else null

abstract fun getShaderData(
density: Density,
componentSize: Size,
offsetFromRoot: Offset,
fillScheme: AuroraColorScheme
): Data

override fun paintDecorationArea(
drawScope: DrawScope,
Expand Down Expand Up @@ -65,11 +77,12 @@ abstract class ShaderWrapperDecorationPainter(
)
}

var shader = shaders[colorScheme.displayName]
if (shader == null) {
shader = shaderGenerator.invoke(colorScheme)
shaders[colorScheme.displayName] = shader
}
val shader = runtimeEffect.makeShader(
uniforms = getShaderData(drawScope, componentSize, offsetFromRoot, colorScheme),
children = shaderChildren,
localMatrix = null,
isOpaque = false
)

val clipPath = Path()
clipPath.addOutline(outline)
Expand Down
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.unit.Density
import org.jetbrains.skia.Data
import org.jetbrains.skia.RuntimeEffect
import org.jetbrains.skia.Shader
import org.pushingpixels.aurora.theming.colorscheme.AuroraColorScheme

/**
Expand All @@ -36,8 +37,12 @@ import org.pushingpixels.aurora.theming.colorscheme.AuroraColorScheme
*/
abstract class ShaderWrapperFillPainter(
val runtimeEffect: RuntimeEffect,
baseShader: Shader? = null,
val baseFillPainter: AuroraFillPainter
) : AuroraFillPainter {
private val shaderChildren: Array<Shader?>? =
if (baseShader != null) arrayOf(baseShader) else null

abstract fun getShaderData(
density: Density,
outline: Outline,
Expand All @@ -63,7 +68,7 @@ abstract class ShaderWrapperFillPainter(

val shader = runtimeEffect.makeShader(
uniforms = getShaderData(drawScope, outline, fillScheme, alpha),
children = null,
children = shaderChildren,
localMatrix = null,
isOpaque = false
)
Expand Down
Expand Up @@ -36,18 +36,18 @@ class SpecularRectangularFillPainter(base: AuroraFillPainter, val baseAlpha: Flo
fillScheme: AuroraColorScheme,
alpha: Float
): Data {
val duotoneDataBuffer = ByteBuffer.allocate(40).order(ByteOrder.LITTLE_ENDIAN)
val dataBuffer = ByteBuffer.allocate(40).order(ByteOrder.LITTLE_ENDIAN)
// RGBA colorLight
val color = fillScheme.extraLightColor
duotoneDataBuffer.putFloat(0, color.red)
duotoneDataBuffer.putFloat(4, color.green)
duotoneDataBuffer.putFloat(8, color.blue)
duotoneDataBuffer.putFloat(12, color.alpha)
dataBuffer.putFloat(0, color.red)
dataBuffer.putFloat(4, color.green)
dataBuffer.putFloat(8, color.blue)
dataBuffer.putFloat(12, color.alpha)
// Alpha
duotoneDataBuffer.putFloat(16, alpha * baseAlpha)
dataBuffer.putFloat(16, alpha * baseAlpha)
// Width and height
duotoneDataBuffer.putFloat(20, outline.bounds.width)
duotoneDataBuffer.putFloat(24, outline.bounds.height)
dataBuffer.putFloat(20, outline.bounds.width)
dataBuffer.putFloat(24, outline.bounds.height)

// This is not ideal, but supporting Path-based outlines would mean having to pass that
// information to the underlying shader.
Expand All @@ -63,13 +63,13 @@ class SpecularRectangularFillPainter(base: AuroraFillPainter, val baseAlpha: Flo
topRightRadius = 0.0f
}
}
duotoneDataBuffer.putFloat(28, topLeftRadius)
duotoneDataBuffer.putFloat(32, topRightRadius)
dataBuffer.putFloat(28, topLeftRadius)
dataBuffer.putFloat(32, topRightRadius)

val minDimension = minOf(outline.bounds.width, outline.bounds.height)
val gapBase = if (minDimension < (16f * density.density)) 0.5f else 1.5f
duotoneDataBuffer.putFloat(36, gapBase * density.density)
dataBuffer.putFloat(36, gapBase * density.density)

return Data.makeFromBytes(duotoneDataBuffer.array())
return Data.makeFromBytes(dataBuffer.array())
}
}
Expand Up @@ -90,6 +90,61 @@ fun getNoiseShader(
return duotoneShader
}

internal fun getDuotoneEffect(): RuntimeEffect {
// Duotone shader
val duotoneDesc = """
uniform shader shaderInput;
uniform vec4 colorLight;
uniform vec4 colorDark;
uniform float alpha;
half4 main(vec2 fragcoord) {
vec4 inputColor = shaderInput.eval(fragcoord);
float luma = dot(inputColor.rgb, vec3(0.299, 0.587, 0.114));
vec4 duotone = mix(colorLight, colorDark, luma);
return vec4(duotone.r * alpha, duotone.g * alpha, duotone.b * alpha, alpha);
}
"""

return RuntimeEffect.makeForShader(duotoneDesc)
}

fun getBrushedMetalBaseShader(): Shader {
// Fractal noise shader
val noiseShader = Shader.makeFractalNoise(
baseFrequencyX = 0.45f,
baseFrequencyY = 0.45f,
numOctaves = 4,
seed = 0.0f
)

// Brushed metal shader
val brushedMetalDesc = """
uniform shader shaderInput;
half4 main(vec2 fragcoord) {
vec4 inputColor = shaderInput.eval(vec2(0, fragcoord.y));
// Compute the luma at the first pixel in this row
float luma = dot(inputColor.rgb, vec3(0.299, 0.587, 0.114));
// Apply modulation to stretch and shift the texture for the brushed metal look
float modulated = abs(cos((0.004 + 0.02 * luma) * (fragcoord.x + 200) + 0.26 * luma)
* sin((0.06 - 0.25 * luma) * (fragcoord.x + 85) + 0.75 * luma));
// Map 0.0-1.0 range to inverse 0.15-0.3
float modulated2 = 0.3 - modulated / 6.5;
half4 result = half4(modulated2, modulated2, modulated2, 1.0);
return result;
}
"""
val brushedMetalEffect = RuntimeEffect.makeForShader(brushedMetalDesc)
val brushedMetalShader = brushedMetalEffect.makeShader(
uniforms = null,
children = arrayOf(noiseShader),
localMatrix = null,
isOpaque = false
)
return brushedMetalShader
}

fun getBrushedMetalShader(colorLight: Color, colorDark: Color, alpha: Float = 1.0f): Shader {
// Fractal noise shader
val noiseShader = Shader.makeFractalNoise(
Expand Down

0 comments on commit bd80dfb

Please sign in to comment.