///|
/// Deterministic raster textures for preview generation and material studies.
pub(all) struct TextureRecipe {
name : String
width : Int
height : Int
seed : UInt
scale : Double
contrast : Double
brightness : Double
palette : Palette
} derive(Eq, Debug, ToJson)
///|
pub(all) enum TextureBlend {
TextureNormal
TextureMultiply
TextureScreen
TextureOverlay
TextureDifference
} derive(Eq, Debug, ToJson)
///|
pub fn TextureRecipe::new(
name : String,
width : Int,
height : Int,
seed : UInt,
palette : Palette,
) -> TextureRecipe {
{
name,
width: width.max(0),
height: height.max(0),
seed,
scale: 0.03,
contrast: 1.0,
brightness: 0.0,
palette,
}
}
///|
pub fn TextureRecipe::with_scale(
self : TextureRecipe,
scale : Double,
) -> TextureRecipe {
{ ..self, scale: scale.max(0.0001) }
}
///|
pub fn TextureRecipe::with_tone(
self : TextureRecipe,
contrast : Double,
brightness : Double,
) -> TextureRecipe {
{ ..self, contrast: contrast.max(0.0), brightness }
}
///|
fn tx_hash(x : Int, y : Int, seed : UInt) -> Double {
let mut value = seed.reinterpret_as_int() ^ (x * 374761393) ^ (y * 668265263)
value = (value ^ (value / 127)) * 1274126177
((value ^ (value / 8192)) % 100000).abs().to_double() / 50000.0 - 1.0
}
///|
fn tx_color_at(palette : Palette, index : Int) -> Color {
palette.at(index).unwrap_or(Color::rgb8(0, 0, 0))
}
///|
pub fn texture_noise(recipe : TextureRecipe) -> Raster {
let raster = Raster::new(recipe.width, recipe.height)
let sampler = TrigNoise::new(recipe.scale)
for y in 0.. Raster {
let raster = Raster::new(
recipe.width,
recipe.height,
fill=tx_color_at(recipe.palette, 0),
)
let amount = strength.max(0.0).min(1.0)
for y in 0.. 0.0 { 1 } else { 0 }
let _ = raster.set(x, y, tx_color_at(recipe.palette, index))
}
}
raster
}
///|
pub fn texture_stripes(
recipe : TextureRecipe,
angle : Double,
spacing : Double,
) -> Raster {
let raster = Raster::new(recipe.width, recipe.height)
let c = @math.cos(angle)
let s = @math.sin(angle)
let step = spacing.max(1.0)
for y in 0.. Raster {
let raster = Raster::new(recipe.width, recipe.height)
let size = cell.max(1)
for y in 0.. Raster {
let raster = Raster::new(recipe.width, recipe.height)
for y in 0.. Raster {
let raster = Raster::new(recipe.width, recipe.height)
for y in 0.. Array[Vec2] {
let result = []
let safe_margin = margin.max(0.0)
for index in 0.. Raster {
let width = base.width.min(top.width)
let height = base.height.min(top.height)
let result = base.copy()
let amount = opacity.max(0.0).min(1.0)
for y in 0.. above
TextureMultiply => color_blend_multiply(below, above)
TextureScreen => color_blend_screen(below, above)
TextureOverlay => color_blend_overlay(below, above)
TextureDifference => color_blend_difference(below, above)
}
let _ = result.set(x, y, below.mix(blended, amount))
}
}
result
}
///|
pub fn texture_tone(
raster : Raster,
contrast : Double,
brightness : Double,
) -> Raster {
raster.map_colors(fn(color) {
Color::rgba(
(color.r - 0.5) * contrast + 0.5 + brightness,
(color.g - 0.5) * contrast + 0.5 + brightness,
(color.b - 0.5) * contrast + 0.5 + brightness,
a=color.a,
)
})
}
///|
pub fn texture_posterize(raster : Raster, levels : Int) -> Raster {
let count = levels.max(2)
raster.map_colors(fn(color) {
let quantize = fn(value : Double) {
(value * (count - 1).to_double()).to_int().to_double() /
(count - 1).to_double()
}
Color::rgba(
quantize(color.r),
quantize(color.g),
quantize(color.b),
a=color.a,
)
})
}
///|
pub fn texture_colorize(raster : Raster, palette : Palette) -> Raster {
let normalized = raster.grayscale()
let result = normalized.copy()
for y in 0.. Raster {
let edges = raster.sobel_edges()
let result = raster.copy()
for y in 0.. threshold {
let _ = result.set(x, y, color)
}
}
}
result
}
///|
pub fn texture_paper(recipe : TextureRecipe, grain : Double) -> Raster {
let base = texture_noise(recipe)
let fibers = texture_fibers(recipe, grain, 11)
texture_blend(base, fibers, TextureOverlay, grain.max(0.0).min(1.0))
}
///|
pub fn texture_moire(
recipe : TextureRecipe,
frequency : Double,
phase : Double,
) -> Raster {
let raster = Raster::new(recipe.width, recipe.height)
for y in 0.. Raster {
if layers.length() == 0 {
Raster::new(0, 0)
} else {
let mut result = layers[0].0
for layer in layers[1:] {
result = texture_blend(result, layer.0, layer.1, layer.2)
}
result
}
}
///|
pub fn texture_edge_mask(
raster : Raster,
low : Double,
high : Double,
) -> Raster {
let edges = raster.sobel_edges()
edges.threshold(high.max(low))
}
///|
pub fn texture_recipe_report(recipe : TextureRecipe) -> String {
"name=" +
recipe.name +
" size=" +
recipe.width.to_string() +
"x" +
recipe.height.to_string() +
" seed=" +
recipe.seed.to_string() +
" scale=" +
recipe.scale.to_string() +
" contrast=" +
recipe.contrast.to_string() +
" brightness=" +
recipe.brightness.to_string() +
" palette=" +
recipe.palette.name
}
///|
pub fn texture_quality(recipe : TextureRecipe) -> RasterQualityMetrics {
raster_quality_metrics(texture_paper(recipe, 0.35), 0.04)
}
///|
pub fn texture_preview(recipe : TextureRecipe, max_size : Int) -> Raster {
let texture = texture_paper(recipe, 0.3)
let factor = texture.width.max(texture.height) / max_size.max(1)
if factor <= 1 {
texture
} else {
texture.downsample(factor)
}
}