November 12th, 2024

Recipe: Sticker Effect in SwiftUI

I was once again experimenting with shaders and SwiftUI. What started as a small test for version 2 of Storma ended up as an open-source Swift package.

Sticker effect on a lightning-shaped icon

The result: Sticker, a tool that lets you add a "Pokémon card" effect to any view. Available now on my GitHub, this package is easy to use. If you like the result, give it a try, share your feedback, and feel free to drop a star on the repo. ⭐

While developing Sticker, I aimed to make shaders more accessible, as shader code can be daunting. If you're just starting, I highly recommend experimenting with visual editors like Godot or Unity, where you can connect blocks visually, making the process much easier to grasp. For the curious, here’s how you can recreate this effect step by step.

1. Create a Color Gradient

Start by writing a pseudo-random function based on pixel positions.

// Generate pseudo-random noise
float random(float2 uv) {
    return fract(sin(dot(uv.xy, float2(12.9898, 78.233))) * 43758.5453);
}

Then use sine and cosine to add some dynamic shifts to the gradient.

float2 uv = position;
float contrast = 0.9;
float gradientNoise = random(position) * 0.1;
 
half r = half(contrast + 0.25 * sin(uv.x * 10.0 + gradientNoise));
half g = half(contrast + 0.25 * cos(uv.y * 10.0 + gradientNoise));
half b = half(contrast + 0.25 * sin((uv.x + uv.y) * 10.0 - gradientNoise));
 
half4 foilColor = half4(r, g, b, 1.0);

2. Add a Diamond Grid

To enhance realism, create a grid of squares and rotate it 45°.

// Checker pattern function for a diamond grid effect
float checkerPattern(float2 uv, float scale, float degreesAngle) {
    float radiansAngle = degreesAngle * M_PI_F / 180;
 
    // Scale UV coordinates
    uv *= scale;
 
    // Rotate UV coordinates by the given angle
    float cosAngle = cos(radiansAngle);
    float sinAngle = sin(radiansAngle);
    float2 rotatedUV = float2(
        cosAngle * uv.x - sinAngle * uv.y,
        sinAngle * uv.x + cosAngle * uv.y
    );
 
    // Determine the tile color (black or white)
    return fmod(floor(rotatedUV.x) + floor(rotatedUV.y), 2.0) == 0.0 ? 0.0 : 1.0;
}

3. Add Grain Texture

To add texture to your effect, generate some noise.

float noisePattern(float2 uv) {
    float2 i = floor(uv);
    float2 f = fract(uv);
 
    // Four corners of a tile in 2D
    float a = random(i);
    float b = random(i + float2(1.0, 0.0));
    float c = random(i + float2(0.0, 1.0));
    float d = random(i + float2(1.0, 1.0));
 
    // Smooth interpolation
    float2 u = smoothstep(0.0, 1.0, f);
 
    // Mix percentages of the four corners
    return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}

4. Prepare to Mix

Define a function to calculate a color’s brightness.

// Helper function to calculate brightness
float calculateBrightness(half4 color) {
    return (color.r * 0.299 + color.g * 0.587 + color.b * 0.114);
}

Create a utility function to blend two colors based on the brightness of the base color.

// Function to blend colors with more intensity for brighter areas
half4 lightnessMix(half4 baseColor, half4 overlayColor, float intensity, float baselineFactor) {
    float brightness = calculateBrightness(baseColor);
 
    float adjustedMixFactor = max(smoothstep(0.2, 1.0, brightness) * intensity, baselineFactor);
 
    return mix(baseColor, overlayColor, adjustedMixFactor);
}

Lastly, define a function to increase contrast.

// Function to enhance contrast using a pattern value
half4 increaseContrast(half4 source, float pattern, float intensity) {
    float brightness = calculateBrightness(source);
 
    float contrastFactor = mix(1.0, intensity, pattern * brightness);
 
    half4 contrastedColor = (source - half4(0.5)) * contrastFactor + half4(0.5);
 
    return contrastedColor;
}

5. Mix It All Together

Combine everything in the following order:

  1. Blend the input color with your gradient, taking input brightness into account.
  2. Add the diamond grid effect using a contrast-increasing mix.
  3. Add the noise texture with another contrast-increasing mix.
half4 mixedFoilColor = lightnessMix(color, foilColor, intensity, 0.3);
half4 checkerFoil = increaseContrast(mixedFoilColor, checker, checkerIntensity);
half4 noiseCheckerFoil = increaseContrast(checkerFoil, noise, noiseIntensity);
 
return noiseCheckerFoil;

The result is a more pronounced effect on bright colors and a subtle shimmer on darker colors, adding realism to your design.

Comparison of icons with and without the effect Comparison of icons with and without the effect. Notice the stronger effect on lighter colors.

Feel free to experiment and let me know what you create with it!