Skip to main content

wassily_effects/
textures.rs

1//! # Procedural Textures
2//!
3//! Tools for creating procedural textures and pattern-based paints.
4//! This module provides utilities for generating complex textures algorithmically
5//! and converting them into paint objects for use with shapes and fills.
6//!
7//! ## Key Components
8//!
9//! - **[`PatternPaint`]**: Convert textures into reusable paint patterns
10//! - **Texture Generators**: Functions to create various procedural textures
11//! - **Pattern Integration**: Seamless integration with wassily's paint system
12//!
13//! ## Basic Usage
14//!
15//! ```no_run
16//! use wassily_effects::*;
17//! use wassily_core::*;
18//!
19//! // Create a texture
20//! let texture = stipple_texture(256, 256, *BLACK, 8.0);
21//!
22//! // Convert to pattern paint
23//! let bbox = Rect::from_xywh(0.0, 0.0, 256.0, 256.0).unwrap();
24//! let mut pattern_paint = PatternPaint::new(&texture, bbox, 256, 256);
25//! let paint = pattern_paint.paint();
26//!
27//! // Use with shapes
28//! Shape::new()
29//!     .rect_xywh(pt(100, 100), pt(200, 200))
30//!     .fill_paint(&paint)
31//!     .draw(&mut canvas);
32//! ```
33//!
34//! ## Texture Types
35//!
36//! - **Stipple Textures**: Dot patterns with various distributions
37//! - **Noise Textures**: Procedural noise-based surface patterns
38//! - **Geometric Textures**: Mathematical patterns and shapes
39//! - **Custom Textures**: Build your own texture generators
40//!
41//! ## Applications
42//!
43//! - **Surface Treatments**: Add texture to filled shapes
44//! - **Pattern Fills**: Repeating patterns across large areas
45//! - **Artistic Effects**: Stippling, hatching, cross-hatching
46//! - **Material Simulation**: Wood grain, fabric, stone textures
47use wassily_core::{canvas::*, shape::*, points::pt};
48use wassily_color::*;
49use wassily_noise::*;
50use crate::stipple::uniform;
51use noise::core::worley::distance_functions::euclidean_squared;
52use noise::core::worley::ReturnType;
53use noise::*;
54use tiny_skia::*;
55pub struct PatternPaint<'a> {
56    pub texture_canvas: &'a Canvas,
57    pub pattern_canvas: Canvas,
58    pub bbox: Rect,
59}
60
61impl<'a> PatternPaint<'a> {
62    pub fn new(texture_canvas: &'a Canvas, bbox: Rect, width: u32, height: u32) -> Self {
63        let pattern_canvas = Canvas::new(width, height);
64        Self {
65            texture_canvas,
66            pattern_canvas,
67            bbox,
68        }
69    }
70
71    pub fn paint(&'a mut self) -> Paint<'a> {
72        let x = self.bbox.x();
73        let y = self.bbox.y();
74        self.pattern_canvas.pixmap.draw_pixmap(
75            x as i32,
76            y as i32,
77            self.texture_canvas.pixmap.as_ref(),
78            &PixmapPaint::default(),
79            Transform::identity(),
80            None,
81        );
82        let pattern = Pattern::new(
83            self.pattern_canvas.pixmap.as_ref(),
84            SpreadMode::Pad,
85            FilterQuality::Bicubic,
86            1.0,
87            Transform::identity(),
88        );
89        paint_shader(pattern)
90    }
91}
92
93pub fn stipple_texture(width: u32, height: u32, color: Color, spacing: f32) -> Canvas {
94    let mut canvas = Canvas::new(width, height);
95    let n = canvas.w_f32() * canvas.h_f32() / spacing;
96    let dots = uniform(width as f32, height as f32, n as u32, 0);
97    for d in dots {
98        canvas.dot(d.x, d.y, color);
99    }
100    canvas
101}
102
103pub fn horizontal_stripe(
104    width: u32,
105    height: u32,
106    color1: Color,
107    color2: Color,
108    spacing: f32,
109) -> Canvas {
110    let mut canvas = Canvas::new(width, height);
111    let mut l = 0.0;
112    canvas.pixmap.fill(color1);
113    while l < height as f32 {
114        Shape::new()
115            .line(pt(0.0, l), pt(width, l))
116            .stroke_color(color2)
117            .stroke_weight(4.0)
118            .draw(&mut canvas);
119        l += spacing;
120    }
121    canvas
122}
123
124pub fn ridge(width: u32, height: u32, color1: Color, color2: Color, scale: f32) -> Canvas {
125    let mut canvas = Canvas::new(width, height);
126    let nf = RidgedMulti::<Perlin>::default();
127    let opts = NoiseOpts::with_wh(width, height).scales(scale);
128    for i in 0..width {
129        for j in 0..height {
130            let a = noise2d_01(&nf, &opts, i as f32, j as f32);
131            let c = color1.lerp(&color2, a);
132            canvas.dot(i as f32, j as f32, c);
133        }
134    }
135    canvas
136}
137
138pub fn foam(
139    width: u32,
140    height: u32,
141    color1: Color,
142    color2: Color,
143    color3: Color,
144    scale: f32,
145    seed: u32,
146) -> Canvas {
147    let mut canvas = Canvas::new(width, height);
148    let nf = Worley::default()
149        .set_distance_function(euclidean_squared)
150        .set_return_type(ReturnType::Distance)
151        .set_seed(seed);
152    let opts = NoiseOpts::with_wh(width, height).scales(scale);
153    for i in 0..width {
154        for j in 0..height {
155            let a = noise2d_01(&nf, &opts, i as f32, j as f32);
156            let mut c = color1.lerp(&color2, a);
157            if a > 0.48 && a < 0.52 {
158                c = color3
159            }
160            canvas.dot(i as f32, j as f32, c);
161        }
162    }
163    canvas
164}
165
166pub fn marble(
167    width: u32,
168    height: u32,
169    color1: Color,
170    color2: Color,
171    scale: f32,
172    seed: u32,
173) -> Canvas {
174    let mut canvas = Canvas::new(width, height);
175    let nf = Fbm::<Perlin>::default().set_seed(seed);
176    let opts = NoiseOpts::with_wh(width, height).scales(scale);
177    for i in 0..width {
178        for j in 0..height {
179            let a = noise2d(&nf, &opts, i as f32, j as f32);
180            let b =
181                0.5 * (((j as f32 + a * 1000.0) * std::f32::consts::PI * 2.0 / 200.0).sin() + 1.0);
182            let c = color1.lerp(&color2, b);
183            canvas.dot(i as f32, j as f32, c);
184        }
185    }
186    canvas
187}
188
189// From noise-rs. https://github.com/Razaekel/noise-rs/blob/develop/examples/texturewood.rs
190pub fn wood(width: u32, height: u32, color1: Color, color2: Color, scale: f32) -> Canvas {
191    let mut canvas = Canvas::new(width, height);
192    let base_wood = Cylinders::new().set_frequency(16.0);
193
194    // Basic Multifractal noise to use for the wood grain.
195    let wood_grain_noise = BasicMulti::<Perlin>::new(0)
196        .set_frequency(48.0)
197        .set_persistence(0.5)
198        .set_lacunarity(2.20703125)
199        .set_octaves(3);
200
201    // Stretch the perlin noise in the same direction as the center of the log. Should
202    // produce a nice wood-grain texture.
203    let scaled_base_wood_grain = ScalePoint::new(wood_grain_noise).set_z_scale(0.25);
204
205    // Scale the wood-grain values so that they can be added to the base wood texture.
206    let wood_grain = ScaleBias::new(scaled_base_wood_grain)
207        .set_scale(0.25)
208        .set_bias(0.125);
209
210    // Add the wood grain texture to the base wood texture.
211    let combined_wood = Add::new(base_wood, wood_grain);
212
213    // Slightly perturb the wood to create a more realistic texture.
214    let perturbed_wood = Turbulence::<_, Perlin>::new(combined_wood)
215        .set_seed(1)
216        .set_frequency(4.0)
217        .set_power(1.0 / 256.0)
218        .set_roughness(4);
219
220    let nf = Turbulence::<_, Perlin>::new(perturbed_wood)
221        .set_seed(2)
222        .set_frequency(2.0)
223        .set_power(1.0 / 64.0)
224        .set_roughness(4);
225    let opts = NoiseOpts::with_wh(width, height).scales(scale);
226    for i in 0..width {
227        for j in 0..height {
228            let b = noise2d_01(
229                &nf,
230                &opts,
231                i as f32 - width as f32 / 2.0,
232                j as f32 - height as f32 / 2.0,
233            );
234            let c = color1.lerp(&color2, b);
235            canvas.dot(i as f32, j as f32, c);
236        }
237    }
238    canvas
239}
240
241// From noise-rs. https://github.com/Razaekel/noise-rs/blob/develop/examples/texturegranite.rs
242pub fn granite(
243    width: u32,
244    height: u32,
245    color1: Color,
246    color2: Color,
247    scale: f32,
248    seed: u32,
249) -> Canvas {
250    let mut canvas = Canvas::new(width, height);
251    // Primary granite texture. This generates the "roughness" of the texture
252    // when lit by a light source.
253    let primary_granite = Billow::<Perlin>::new(0)
254        .set_frequency(8.0)
255        .set_persistence(0.625)
256        .set_lacunarity(2.18359375)
257        .set_octaves(6)
258        .set_seed(seed);
259
260    // Use Worley polygons to produce the small grains for the granite texture.
261    let base_grains = Worley::new(1)
262        .set_frequency(16.0)
263        .set_return_type(ReturnType::Distance);
264
265    // Scale the small grain values so that they can be added to the base
266    // granite texture. Worley polygons normally generate pits, so apply a
267    // negative scaling factor to produce bumps instead.
268    let scaled_grains = ScaleBias::new(base_grains).set_scale(-0.5).set_bias(0.0);
269
270    // Combine the primary granite texture with the small grain texture.
271    let combined_granite = Add::new(primary_granite, scaled_grains);
272
273    // Finally, perturb the granite texture to add realism.
274    let nf = Turbulence::<_, Perlin>::new(combined_granite)
275        .set_seed(2)
276        .set_frequency(4.0)
277        .set_power(1.0 / 8.0)
278        .set_roughness(6);
279    let opts = NoiseOpts::with_wh(width, height).scales(scale);
280    for i in 0..width {
281        for j in 0..height {
282            let b = noise2d_01(&nf, &opts, i as f32, j as f32);
283            let c = color1.lerp(&color2, b);
284            canvas.dot(i as f32, j as f32, c);
285        }
286    }
287    canvas
288}