// ============================================================================
// Filter Effects
// ============================================================================
///|
/// Filter types
pub(all) enum Filter {
Blur(Double) // Blur radius
DropShadow(Double, Double, Double, Color) // offsetX, offsetY, blur, color
Brightness(Double) // Factor (1.0 = normal)
Contrast(Double) // Factor (1.0 = normal)
Grayscale(Double) // Amount (0.0-1.0)
Sepia(Double) // Amount (0.0-1.0)
HueRotate(Double) // Angle in degrees
Invert(Double) // Amount (0.0-1.0)
Saturate(Double) // Factor (1.0 = normal, 0.0 = grayscale, 2.0 = double)
ColorMatrix(FixedArray[Double]) // 5x4 matrix (20 values) for feColorMatrix
}
///|
/// Apply blur filter to a region of pixels
fn apply_blur(
pixels : Array[Array[Color]],
radius : Int,
) -> Array[Array[Color]] {
if radius <= 0 || pixels.is_empty() {
return pixels
}
let height = pixels.length()
let width = if height > 0 { pixels[0].length() } else { 0 }
// Create output buffer
let output : Array[Array[Color]] = []
for _ in 0.. Int {
if v < min {
min
} else if v > max {
max
} else {
v
}
}
///|
/// Apply brightness filter to a color
fn apply_brightness(color : Color, factor : Double) -> Color {
{
r: clamp_int((color.r.to_double() * factor).to_int(), 0, 255),
g: clamp_int((color.g.to_double() * factor).to_int(), 0, 255),
b: clamp_int((color.b.to_double() * factor).to_int(), 0, 255),
a: color.a,
}
}
///|
/// Apply grayscale filter to a color
fn apply_grayscale(color : Color, amount : Double) -> Color {
let gray = (color.r.to_double() * 0.299 +
color.g.to_double() * 0.587 +
color.b.to_double() * 0.114).to_int()
{
r: lerp(color.r.to_double(), gray.to_double(), amount).to_int(),
g: lerp(color.g.to_double(), gray.to_double(), amount).to_int(),
b: lerp(color.b.to_double(), gray.to_double(), amount).to_int(),
a: color.a,
}
}
///|
/// Apply contrast filter to a color
fn apply_contrast(color : Color, factor : Double) -> Color {
// Contrast formula: ((value - 128) * factor) + 128
let r = ((color.r.to_double() - 128.0) * factor + 128.0).to_int()
let g = ((color.g.to_double() - 128.0) * factor + 128.0).to_int()
let b = ((color.b.to_double() - 128.0) * factor + 128.0).to_int()
{
r: clamp_int(r, 0, 255),
g: clamp_int(g, 0, 255),
b: clamp_int(b, 0, 255),
a: color.a,
}
}
///|
/// Apply sepia filter to a color
fn apply_sepia(color : Color, amount : Double) -> Color {
// Sepia matrix coefficients
let r = color.r.to_double()
let g = color.g.to_double()
let b = color.b.to_double()
// Sepia tone calculation
let sepia_r = r * 0.393 + g * 0.769 + b * 0.189
let sepia_g = r * 0.349 + g * 0.686 + b * 0.168
let sepia_b = r * 0.272 + g * 0.534 + b * 0.131
// Interpolate between original and sepia
{
r: clamp_int(lerp(r, sepia_r, amount).to_int(), 0, 255),
g: clamp_int(lerp(g, sepia_g, amount).to_int(), 0, 255),
b: clamp_int(lerp(b, sepia_b, amount).to_int(), 0, 255),
a: color.a,
}
}
///|
/// Apply hue rotation to a color
fn apply_hue_rotate(color : Color, angle_degrees : Double) -> Color {
// Convert to radians
let angle = angle_degrees * 3.14159265358979 / 180.0
let cos_a = cos_approx(angle)
let sin_a = sin_approx(angle)
let r = color.r.to_double() / 255.0
let g = color.g.to_double() / 255.0
let b = color.b.to_double() / 255.0
// Hue rotation matrix (based on SVG spec)
let matrix_00 = 0.213 + cos_a * 0.787 - sin_a * 0.213
let matrix_01 = 0.715 - cos_a * 0.715 - sin_a * 0.715
let matrix_02 = 0.072 - cos_a * 0.072 + sin_a * 0.928
let matrix_10 = 0.213 - cos_a * 0.213 + sin_a * 0.143
let matrix_11 = 0.715 + cos_a * 0.285 + sin_a * 0.140
let matrix_12 = 0.072 - cos_a * 0.072 - sin_a * 0.283
let matrix_20 = 0.213 - cos_a * 0.213 - sin_a * 0.787
let matrix_21 = 0.715 - cos_a * 0.715 + sin_a * 0.715
let matrix_22 = 0.072 + cos_a * 0.928 + sin_a * 0.072
let new_r = r * matrix_00 + g * matrix_01 + b * matrix_02
let new_g = r * matrix_10 + g * matrix_11 + b * matrix_12
let new_b = r * matrix_20 + g * matrix_21 + b * matrix_22
{
r: clamp_int((new_r * 255.0).to_int(), 0, 255),
g: clamp_int((new_g * 255.0).to_int(), 0, 255),
b: clamp_int((new_b * 255.0).to_int(), 0, 255),
a: color.a,
}
}
///|
/// Approximate cosine function
fn cos_approx(x : Double) -> Double {
let pi = 3.14159265358979
let pi2 = 6.28318530717959
// Normalize to [0, 2π]
let mut normalized = x
while normalized < 0.0 {
normalized = normalized + pi2
}
while normalized >= pi2 {
normalized = normalized - pi2
}
// Reduce to [-π, π] for better Taylor accuracy
if normalized > pi {
normalized = normalized - pi2
}
// Taylor series approximation for cos (accurate near 0)
let x2 = normalized * normalized
let x4 = x2 * x2
let x6 = x4 * x2
let x8 = x4 * x4
let x10 = x4 * x6
1.0 - x2 / 2.0 + x4 / 24.0 - x6 / 720.0 + x8 / 40320.0 - x10 / 3628800.0
}
///|
/// Approximate sine function
fn sin_approx(x : Double) -> Double {
let pi = 3.14159265358979
let pi2 = 6.28318530717959
// Normalize to [0, 2π]
let mut normalized = x
while normalized < 0.0 {
normalized = normalized + pi2
}
while normalized >= pi2 {
normalized = normalized - pi2
}
// Reduce to [-π, π]
if normalized > pi {
normalized = normalized - pi2
}
// Taylor series for sin: x - x³/6 + x⁵/120 - x⁷/5040 + x⁹/362880
let x2 = normalized * normalized
let x3 = normalized * x2
let x5 = x3 * x2
let x7 = x5 * x2
let x9 = x7 * x2
normalized - x3 / 6.0 + x5 / 120.0 - x7 / 5040.0 + x9 / 362880.0
}
///|
/// Apply invert filter to a color
fn apply_invert(color : Color, amount : Double) -> Color {
let inv_r = 255 - color.r
let inv_g = 255 - color.g
let inv_b = 255 - color.b
{
r: lerp(color.r.to_double(), inv_r.to_double(), amount).to_int(),
g: lerp(color.g.to_double(), inv_g.to_double(), amount).to_int(),
b: lerp(color.b.to_double(), inv_b.to_double(), amount).to_int(),
a: color.a,
}
}
///|
/// Apply saturate filter to a color
fn apply_saturate(color : Color, factor : Double) -> Color {
// Saturation matrix based on luminance
let r = color.r.to_double() / 255.0
let g = color.g.to_double() / 255.0
let b = color.b.to_double() / 255.0
// Luminance coefficients
let lum_r = 0.2126
let lum_g = 0.7152
let lum_b = 0.0722
// Saturation matrix
let sr = (1.0 - factor) * lum_r + factor
let sg = (1.0 - factor) * lum_g
let sb = (1.0 - factor) * lum_b
let new_r = r * sr + g * sg + b * sb
let new_g = r * ((1.0 - factor) * lum_r) +
g * ((1.0 - factor) * lum_g + factor) +
b * ((1.0 - factor) * lum_b)
let new_b = r * ((1.0 - factor) * lum_r) +
g * ((1.0 - factor) * lum_g) +
b * ((1.0 - factor) * lum_b + factor)
{
r: clamp_int((new_r * 255.0).to_int(), 0, 255),
g: clamp_int((new_g * 255.0).to_int(), 0, 255),
b: clamp_int((new_b * 255.0).to_int(), 0, 255),
a: color.a,
}
}
///|
/// Apply color matrix filter (feColorMatrix)
/// Matrix is 5x4 (20 values) in row-major order:
/// [R'] = [a00 a01 a02 a03 a04] [R]
/// [G'] = [a10 a11 a12 a13 a14] [G]
/// [B'] = [a20 a21 a22 a23 a24] [B]
/// [A'] = [a30 a31 a32 a33 a34] [A]
/// [1]
fn apply_color_matrix(color : Color, matrix : FixedArray[Double]) -> Color {
if matrix.length() != 20 {
return color // Invalid matrix
}
let r = color.r.to_double() / 255.0
let g = color.g.to_double() / 255.0
let b = color.b.to_double() / 255.0
let a = color.a.to_double() / 255.0
let new_r = r * matrix[0] +
g * matrix[1] +
b * matrix[2] +
a * matrix[3] +
matrix[4]
let new_g = r * matrix[5] +
g * matrix[6] +
b * matrix[7] +
a * matrix[8] +
matrix[9]
let new_b = r * matrix[10] +
g * matrix[11] +
b * matrix[12] +
a * matrix[13] +
matrix[14]
let new_a = r * matrix[15] +
g * matrix[16] +
b * matrix[17] +
a * matrix[18] +
matrix[19]
{
r: clamp_int((new_r * 255.0).round().to_int(), 0, 255),
g: clamp_int((new_g * 255.0).round().to_int(), 0, 255),
b: clamp_int((new_b * 255.0).round().to_int(), 0, 255),
a: clamp_int((new_a * 255.0).round().to_int(), 0, 255),
}
}
///|
/// Apply drop shadow to an image and return new image with shadow
fn apply_drop_shadow(
image : Image,
offset_x : Int,
offset_y : Int,
blur_radius : Int,
shadow_color : Color,
) -> Image {
// Create output with extra space for shadow
let max_offset = if offset_x.abs() > offset_y.abs() {
offset_x.abs()
} else {
offset_y.abs()
}
let margin = blur_radius + max_offset
let new_width = image.width + margin * 2
let new_height = image.height + margin * 2
let output = Image::new(new_width, new_height)
// First, render the shadow (offset copy with color)
for y in 0.. 0 {
let shadow_x = margin + x + offset_x
let shadow_y = margin + y + offset_y
if shadow_x >= 0 &&
shadow_x < new_width &&
shadow_y >= 0 &&
shadow_y < new_height {
// Use original alpha to modulate shadow
let alpha = (shadow_color.a.to_double() *
src_color.a.to_double() /
255.0).to_int()
output.set_pixel(shadow_x, shadow_y, { ..shadow_color, a: alpha })
}
}
}
}
// Apply blur to shadow
if blur_radius > 0 {
output.apply_blur_in_place(blur_radius)
}
// Then overlay the original image on top
for y in 0.. 0 {
let dst_x = margin + x
let dst_y = margin + y
// Alpha blend
let bg = output.get_pixel(dst_x, dst_y)
let blended = alpha_blend(src_color, bg)
output.set_pixel(dst_x, dst_y, blended)
}
}
}
output
}
///|
/// Alpha blend foreground over background
fn alpha_blend(fg : Color, bg : Color) -> Color {
let fg_a = fg.a.to_double() / 255.0
let bg_a = bg.a.to_double() / 255.0
let out_a = fg_a + bg_a * (1.0 - fg_a)
if out_a < 0.001 {
return Color::transparent()
}
let r = (fg.r.to_double() * fg_a + bg.r.to_double() * bg_a * (1.0 - fg_a)) /
out_a
let g = (fg.g.to_double() * fg_a + bg.g.to_double() * bg_a * (1.0 - fg_a)) /
out_a
let b = (fg.b.to_double() * fg_a + bg.b.to_double() * bg_a * (1.0 - fg_a)) /
out_a
Color::rgba(
r.round().to_int(),
g.round().to_int(),
b.round().to_int(),
(out_a * 255.0).round().to_int(),
)
}
///|
/// Apply a filter to an image (returns new image)
fn apply_filter(image : Image, filter : Filter) -> Image {
match filter {
Blur(radius) => {
let result = image.clone()
result.apply_blur_in_place(radius.to_int())
result
}
DropShadow(offset_x, offset_y, blur, color) =>
apply_drop_shadow(
image,
offset_x.to_int(),
offset_y.to_int(),
blur.to_int(),
color,
)
Brightness(factor) => {
let result = image.clone()
result.apply_brightness_in_place(factor)
result
}
Contrast(factor) => {
let result = image.clone()
result.apply_contrast_in_place(factor)
result
}
Grayscale(amount) => {
let result = image.clone()
result.apply_grayscale_in_place(amount)
result
}
Sepia(amount) => {
let result = image.clone()
result.apply_sepia_in_place(amount)
result
}
HueRotate(angle) => {
let result = image.clone()
result.apply_hue_rotate_in_place(angle)
result
}
Invert(amount) => {
let result = image.clone()
result.apply_invert_in_place(amount)
result
}
Saturate(factor) => {
let result = image.clone()
result.apply_saturate_in_place(factor)
result
}
ColorMatrix(matrix) => {
let result = image.clone()
result.apply_color_matrix_in_place(matrix)
result
}
}
}