Skip to main content

fx/
render.rs

1// render -- effect group "render" (Rendern): Clouds, Julia Fractal, Mandelbrot Fractal,
2// Turbulence                                                                [src/fx/fx_render.s]
3//
4//   FXTAB            the group table
5//
6// All four are per-pixel functions of (x, y, image size, parameters).
7//   clouds      paint.net 3.x algorithm, own lattice noise: dx = 2x - w, dy = 2y - h; octaves
8//               i = 0, 1, ... while i < 12, amp > 0.03 and div > 0 (amp = power^i, div = scale
9//               >> i): val += perlin(dx / div + ox_i, dy / div + oy_i, seed, i) amp;
10//               t = clamp((val + 1) / 2, 0, 1)
11//   turbulence  octaves i < detail while div > 0, amp = roughness^i; val = sum |perlin| amp;
12//               t = clamp(2 val / sum amp, 0, 1)
13//               Colour = round(c1 + (c2 - c1) t) per channel (f32), blended over the source
14//               with the blend mode (gfx::blendmode)
15//   julia_fractal / mandelbrot_fractal  paint.net 3.x algorithms, count samples per pixel,
16//               escape iteration per sample (the asm runs 6 / 12 packed lanes; every sample
17//               does the same IEEE operations), colour schemes classic / grayscale / palette /
18//               rainbow, channel sums divided by count (truncating)
19
20use gfx::blendmode::bm_row;
21use rt::fx::{cvt, cvtd, cvttd, cvttd64, maxsd, minsd};
22
23use crate::perlin::{perlin_row, PerlinRow};
24use alloc::vec;
25use simd::{Kernel, Simd, Sse2};
26use crate::warp::{log, sincos};
27use crate::*;
28
29const NZ_SEG: usize = 256;
30const NZ_MAXOCT: usize = 12;
31const DEG2RAD: f64 = 0.017453292519943295769;
32
33static OFS: [[f64; 2]; 12] = [
34    [0.0, 0.0],
35    [0.6180339887498949, 0.75487766624669272],
36    [0.23606797749978981, 0.50975533249338545],
37    [0.85410196624968471, 0.26463299874007795],
38    [0.47213595499957961, 0.019510664986770898],
39    [0.090169943749474513, 0.77438833123346384],
40    [0.70820393249936942, 0.5292659974801559],
41    [0.32623792124926432, 0.28414366372684885],
42    [0.94427190999915922, 0.039021329973541796],
43    [0.56230589874905412, 0.79389899622023474],
44    [0.18033988749894903, 0.54877666246692769],
45    [0.79837387624884393, 0.30365432871361975],
46];
47
48/// straight colour -> 4 x f32 (b, g, r, a)  [COLVEC]
49#[inline]
50fn colvec(c: u32) -> [f32; 4] {
51    [(c & 255) as f32, (c >> 8 & 255) as f32, (c >> 16 & 255) as f32, (c >> 24) as f32]
52}
53
54/// t (f64) -> straight colour c1 + dc t (f32 lanes, rounded, saturated)
55#[inline]
56fn ramp(t: f64, c1: &[f32; 4], dc: &[f32; 4]) -> [i32; 4] {
57    let tf = t as f32;
58    core::array::from_fn(|c| cvt(tf * dc[c] + c1[c]))
59}
60
61#[inline]
62fn pack(v: &[i32; 4]) -> u32 {
63    let mut p = 0u32;
64    for (c, &x) in v.iter().enumerate() {
65        p |= (x.clamp(-32768, 32767).clamp(0, 255) as u32) << (8 * c);
66    }
67    p
68}
69
70// ---------------------------------------------------------------- Clouds / Turbulence
71
72fn noise_render(a: &FxArgs, dst: &mut Dst, turb: bool) -> i32 {
73    let p = a.params;
74    let mode = p.int(0, 0, 13) as u32 | 0xFF00;
75    let mut div = p.int(1, 2, 1000) as u32;
76    let power = p.dbl(2, 0.0, 1.0);
77    let (maxoct, ampmin, seed, cs) = if turb {
78        (p.int(3, 1, NZ_MAXOCT as i32) as usize, -1.0, p.0[4] as u32, 5)
79    } else {
80        (NZ_MAXOCT, 0.03, p.0[3] as u32, 4)
81    };
82    let mut inv = [0f64; NZ_MAXOCT];
83    let mut ampv = [0f64; NZ_MAXOCT];
84    let mut n = 0usize;
85    let mut amp = 1.0f64;
86    let mut sum = 0.0f64;
87    while n < maxoct && amp > ampmin && div != 0 {
88        inv[n] = 1.0 / div as f64;
89        ampv[n] = amp;
90        sum += amp;
91        amp *= power;
92        div >>= 1;
93        n += 1;
94    }
95    let norm = 2.0 / sum;
96    let c1 = colvec(p.0[cs] as u32);
97    let c2 = colvec(p.0[cs + 1] as u32);
98    let dc: [f32; 4] = core::array::from_fn(|c| c2[c] - c1[c]);
99    let (w, h) = (a.src.w, a.src.h);
100    let mut acc = [0f64; NZ_SEG];
101    let mut buf = [0u8; NZ_SEG * 4];
102    for y in a.y0..a.y1 {
103        if a.cancelled() {
104            return ERR_CANCEL;
105        }
106        let dy = (y + y - h) as f64;
107        let srow = a.src_row(y);
108        let drow = dst.row(y);
109        let mut x = a.x0;
110        while a.x1 - x > 0 {
111            let ns = ((a.x1 - x) as usize).min(NZ_SEG);
112            acc[..ns].fill(0.0);
113            for i in 0..n {
114                let rec = PerlinRow {
115                    n: ns,
116                    x0: x + x - w,
117                    step: 2,
118                    abs: turb,
119                    seed,
120                    stream: i as u32,
121                    inv: inv[i],
122                    ofsx: OFS[i][0],
123                    y: inv[i] * dy + OFS[i][1],
124                    amp: ampv[i],
125                };
126                perlin_row(&rec, &mut acc);
127            }
128            for j in 0..ns {
129                let v = acc[j];
130                let t = if turb { v * norm } else { (v + 1.0) * 0.5 };
131                let t = minsd(maxsd(t, 0.0), 1.0);
132                put(&mut buf, j, pack(&ramp(t, &c1, &dc)));
133            }
134            let o = (x - a.x0) as usize * 4;
135            bm_row(&mut drow[o..o + ns * 4], &srow[x as usize * 4..x as usize * 4 + ns * 4], &buf[..ns * 4], mode);
136            x += ns as i32;
137        }
138    }
139    0
140}
141
142fn clouds_render(a: &FxArgs, dst: &mut Dst) -> i32 {
143    noise_render(a, dst, false)
144}
145
146fn turb_render(a: &FxArgs, dst: &mut Dst) -> i32 {
147    noise_render(a, dst, true)
148}
149
150// ---------------------------------------------------------------- Julia / Mandelbrot
151
152const FF_CHUNK: usize = 32; // pixels per sample batch
153const FF_MAXCNT: usize = 26; // samples per pixel (q = 5: 25 + 1)
154const NREG: usize = 3; // registers of lanes per step (the asm: three f64 pairs)
155const MAXL: usize = 12;
156
157/// the escape iteration of a batch of samples on packed f64 lanes  [fr_lanes, fr_lanes_avx]
158/// Stop at n = kmax or when |z|^2 >= r2 (both checked before each step); record n and
159/// |z|^2.  A lane takes the next sample as soon as its current one stops; a global step
160/// counter g replaces per-lane counters (a lane started at g0 has n = g - g0, its deadline is
161/// g0 + kmax).  Every sample does the same IEEE operations as a scalar loop would: the lane
162/// count (6 with SSE2, 12 with AVX2) changes no bit.
163struct Lanes<'a> {
164    sx: &'a [f64],
165    sy: &'a [f64],
166    inside: &'a [bool],
167    nf: &'a mut [f64],
168    sf: &'a mut [f64],
169    mand: bool,
170    r2: f64,
171    kmax: i32,
172}
173
174/// f64 per lane, kept as bytes so that a register of lanes is one load / store (perf-fx: the
175/// copies through a small array became element inserts at opt-level 2)
176struct F64s([u8; 8 * MAXL]);
177
178impl F64s {
179    #[inline(always)]
180    fn get(&self, c: usize) -> f64 {
181        f64::from_le_bytes(self.0[c * 8..c * 8 + 8].try_into().unwrap_or([0; 8]))
182    }
183    #[inline(always)]
184    fn set(&mut self, c: usize, v: f64) {
185        self.0[c * 8..c * 8 + 8].copy_from_slice(&v.to_le_bytes());
186    }
187    /// the lanes of register r (S::BYTES / 8 of them)
188    #[inline(always)]
189    fn ld<S: Simd>(&self, s: S, r: usize) -> S::V {
190        s.load(&self.0[r * S::BYTES..])
191    }
192    #[inline(always)]
193    fn st<S: Simd>(&mut self, s: S, r: usize, v: S::V) {
194        s.store(&mut self.0[r * S::BYTES..], v)
195    }
196}
197
198/// the samples a lane takes from
199struct Src<'a> {
200    sx: &'a [f64],
201    sy: &'a [f64],
202    inside: &'a [bool],
203    mand: bool,
204    kmax: i32,
205}
206
207/// the state of the lanes (LN_* / LA_*)
208struct LaneSt {
209    zx: F64s,
210    zy: F64s,
211    cx: F64s,
212    cy: F64s,
213    idx: [i32; MAXL],
214    dl: [i32; MAXL],
215    next: usize,
216}
217
218impl LaneSt {
219    /// lane c takes the next sample that is not inside, or becomes idle (z = c = 0, no
220    /// deadline)  [LA_TAKE]
221    #[inline(always)]
222    fn take(&mut self, c: usize, lb: &Src, g: i32) {
223        let n = lb.sx.len();
224        while self.next < n && lb.inside[self.next] {
225            self.next += 1;
226        }
227        let j = self.next;
228        if j < n {
229            self.idx[c] = j as i32;
230            self.dl[c] = g + lb.kmax;
231            let (zx, zy, cx, cy) = if lb.mand { (0.0, 0.0, lb.sx[j], lb.sy[j]) } else { (lb.sx[j], lb.sy[j], 0.3125, 0.03) };
232            self.zx.set(c, zx);
233            self.zy.set(c, zy);
234            self.cx.set(c, cx);
235            self.cy.set(c, cy);
236            self.next += 1;
237        } else {
238            self.zx.set(c, 0.0);
239            self.zy.set(c, 0.0);
240            self.cx.set(c, 0.0);
241            self.cy.set(c, 0.0);
242            self.dl[c] = i32::MAX;
243            self.idx[c] = -1;
244        }
245    }
246
247    /// lane c (busy) stops at step g: SF = |z|^2, NF = g - (deadline - kmax)  [LA_RECORD]
248    #[inline(always)]
249    fn record(&self, c: usize, m: f64, g: i32, kmax: i32, nf: &mut [f64], sf: &mut [f64]) {
250        let j = self.idx[c] as usize;
251        sf[j] = m;
252        nf[j] = (g - (self.dl[c] - kmax)) as f64;
253    }
254}
255
256impl Kernel for Lanes<'_> {
257    type Out = ();
258    #[inline(always)]
259    fn run<S: Simd>(self, s: S) {
260        let l = S::BYTES / 8;
261        let nl = NREG * l;
262        let Lanes { sx, sy, inside, nf, sf, mand, r2, kmax } = self;
263        let src = Src { sx, sy, inside, mand, kmax };
264        let mut ls = LaneSt {
265            zx: F64s([0; 8 * MAXL]),
266            zy: F64s([0; 8 * MAXL]),
267            cx: F64s([0; 8 * MAXL]),
268            cy: F64s([0; 8 * MAXL]),
269            idx: [-1; MAXL],
270            dl: [i32::MAX; MAXL],
271            next: 0,
272        };
273        let mut g = 0i32;
274        let full = if S::BYTES == 32 { u32::MAX } else { 0xFFFF };
275        let vr2 = s.splatd(r2);
276        // every lane takes a sample (or stays idle)
277        for c in 0..nl {
278            ls.take(c, &src, g);
279        }
280        loop {
281            let mut mindl = i32::MAX;
282            for &d in &ls.dl[..nl] {
283                mindl = mindl.min(d);
284            }
285            if mindl == i32::MAX {
286                break;
287            }
288            let mut x = [s.zero(); NREG];
289            let mut y = [s.zero(); NREG];
290            let mut vcx = [s.zero(); NREG];
291            let mut vcy = [s.zero(); NREG];
292            for r in 0..NREG {
293                x[r] = ls.zx.ld(s, r);
294                y[r] = ls.zy.ld(s, r);
295                vcx[r] = ls.cx.ld(s, r);
296                vcy[r] = ls.cy.ld(s, r);
297            }
298            // perf-fx: the byte masks of |z|^2 < r2 per register when a lane escaped
299            let mut esc = [full; NREG];
300            while g < mindl {
301                let mut x2 = [s.zero(); NREG];
302                let mut y2 = [s.zero(); NREG];
303                let mut all = full;
304                for r in 0..NREG {
305                    x2[r] = s.muld(x[r], x[r]);
306                    y2[r] = s.muld(y[r], y[r]);
307                    esc[r] = s.movemask8(s.cmpltd(s.addd(x2[r], y2[r]), vr2));
308                    all &= esc[r];
309                }
310                if all != full {
311                    break;
312                }
313                for r in 0..NREG {
314                    let nx = s.addd(s.subd(x2[r], y2[r]), vcx[r]);
315                    y[r] = s.addd(s.muld(s.addd(x[r], x[r]), y[r]), vcy[r]);
316                    x[r] = nx;
317                }
318                g += 1;
319            }
320            for r in 0..NREG {
321                ls.zx.st(s, r, x[r]);
322                ls.zy.st(s, r, y[r]);
323            }
324            if g < mindl {
325                // perf-fx: escapes (no lane is at its limit): only the escaped lanes stop and
326                // take the next sample, as fr_lanes_avx .la_esc (idle lanes have z = 0 and
327                // never escape; the scalar |z|^2 equals the lane's)
328                for r in 0..NREG {
329                    for k in 0..l {
330                        let c = r * l + k;
331                        if esc[r] >> (8 * k) & 1 == 0 && ls.idx[c] >= 0 {
332                            let (zx, zy) = (ls.zx.get(c), ls.zy.get(c));
333                            let m = zx * zx + zy * zy;
334                            ls.record(c, m, g, kmax, nf, sf);
335                            ls.take(c, &src, g);
336                        }
337                    }
338                }
339            } else {
340                // a lane reached its limit: stop every lane at its limit or escaped
341                for c in 0..nl {
342                    if ls.idx[c] < 0 {
343                        continue;
344                    }
345                    let (zx, zy) = (ls.zx.get(c), ls.zy.get(c));
346                    let m = zx * zx + zy * zy;
347                    if g >= ls.dl[c] || !(m < r2) {
348                        ls.record(c, m, g, kmax, nf, sf);
349                        ls.take(c, &src, g);
350                    }
351                }
352            }
353        }
354    }
355}
356
357/// rainbow: c >= 1023 or inside: opaque black; else hue 6 frac(c / 64) at full saturation
358/// [fr_rainbow]
359fn rainbow(c: f64, inside: bool) -> [i32; 4] {
360    if inside || c >= 1023.0 || c.is_nan() {
361        return [0, 0, 0, 255];
362    }
363    let hh = maxsd(c * 0.015625, -1.0e9);
364    let mut t = cvttd64(hh);
365    if t as f64 > hh {
366        t -= 1;
367    }
368    let mut f = (hh - t as f64) * 6.0;
369    let s = cvttd(f);
370    f -= s as f64;
371    let ff = cvtd(f * 255.0);
372    let d = 255 - ff;
373    let (r, g, b) = match s {
374        0 => (255, ff, 0),
375        1 => (d, 255, 0),
376        2 => (0, 255, ff),
377        3 => (0, d, 255),
378        4 => (ff, 0, 255),
379        _ => (255, 0, d),
380    };
381    [b, g, r, 255]
382}
383
384fn fractal_render(a: &FxArgs, dst: &mut Dst, mand: bool) -> i32 {
385    let p = a.params;
386    let (fact, q, invz, ang, scheme, cs, count, kmax, r2, invert, k1, k2);
387    if !mand {
388        fact = p.dbl(0, 1.0, 10.0);
389        q = p.int(1, 1, 5);
390        invz = 1.0 / p.dbl(2, 0.01, 50.0);
391        ang = p.dbl(3, -36000.0, 36000.0);
392        scheme = p.int(4, 0, 3);
393        cs = 5usize;
394        count = q * q;
395        kmax = 256;
396        r2 = 10000.0;
397        invert = false;
398        k1 = [768.0, 512.0];
399        k2 = [256.0, 0.0];
400    } else {
401        let f = p.int(0, 1, 10);
402        fact = f as f64;
403        kmax = (1023 / f as u32 + 1) as i32;
404        r2 = 100000.0;
405        q = p.int(1, 1, 5);
406        invz = 1.0 / (p.int(2, 0, 50) * 20 + 1) as f64;
407        invert = p.int(4, 0, 1) != 0;
408        scheme = p.int(5, 0, 3);
409        ang = p.dbl(3, -36000.0, 36000.0);
410        cs = 6;
411        count = q * q + 1;
412        k1 = [256.0, 512.0];
413        k2 = [768.0, 0.0];
414    }
415    let invq = 1.0 / q as f64;
416    let invcnt = 1.0 / count as f64;
417    let invh = 1.0 / a.src.h as f64;
418    let (sn, cn) = sincos(ang * DEG2RAD);
419    let (c1v, c2v) = if scheme == 1 { (0xFF00_0000, 0xFFFF_FFFF) } else { (p.0[cs] as u32, p.0[cs + 1] as u32) };
420    let c1 = colvec(c1v);
421    let c2 = colvec(c2v);
422    let dc: [f32; 4] = core::array::from_fn(|c| c2[c] - c1[c]);
423    let (w, h) = (a.src.w, a.src.h);
424    let ns = FF_CHUNK * FF_MAXCNT;
425    let (mut sx, mut sy, mut nf, mut sf) = (vec![0f64; ns], vec![0f64; ns], vec![0f64; ns], vec![0f64; ns]);
426    let mut ins = vec![false; ns];
427    let cnt = count as usize;
428    let f64x2 = |p: f64, q: f64| {
429        let mut b = [0u8; 16];
430        b[..8].copy_from_slice(&p.to_le_bytes());
431        b[8..].copy_from_slice(&q.to_le_bytes());
432        Sse2.load(&b)
433    };
434    let (vk1, vk2) = (f64x2(k1[0], k1[1]), f64x2(k2[0], k2[1]));
435    let v255 = Sse2.splatd(255.0);
436    let vcf = Sse2.splatd(count as f64);
437    let (vdc, vc1) = (Sse2.load_f32(&dc), Sse2.load_f32(&c1));
438    for y in a.y0..a.y1 {
439        if a.cancelled() {
440            return ERR_CANCEL;
441        }
442        let by = (y as f64 + y as f64) - h as f64;
443        let dx0 = dst.x0;
444        let drow = dst.row(y);
445        let mut xa = a.x0;
446        while a.x1 - xa > 0 {
447            let npx = ((a.x1 - xa) as usize).min(FF_CHUNK);
448            // the samples of the chunk
449            let mut j = 0usize;
450            for k in 0..npx {
451                let x = xa + k as i32;
452                let bx = (x as f64 + x as f64) - w as f64;
453                for i in 0..cnt {
454                    let fi = i as i32 as f64;
455                    let u = (fi * invcnt + bx) * invh;
456                    let t = fi * invq;
457                    let v = (t - cvttd(t) as f64 + by) * invh;
458                    let ur = u * cn - v * sn;
459                    let vr = u * sn + v * cn;
460                    let (mut px, mut py) = (ur * invz, vr * invz);
461                    ins[j] = false;
462                    if mand {
463                        px += -0.7453;
464                        py += 0.1127;
465                        let xq = px - 0.25;
466                        let y2 = py * py;
467                        let qq = xq * xq + y2;
468                        if !((xq + qq) * qq > y2 * 0.25) {
469                            ins[j] = true;
470                        } else {
471                            let x1 = px + 1.0;
472                            ins[j] = !(x1 * x1 + py * py > 0.0625);
473                        }
474                    }
475                    sx[j] = px;
476                    sy[j] = py;
477                    j += 1;
478                }
479            }
480            simd::dispatch(Lanes { sx: &sx[..j], sy: &sy[..j], inside: &ins[..j], nf: &mut nf, sf: &mut sf, mand, r2, kmax });
481            // perf-fx: the colours and the per-pixel division on packed lanes, as the asm
482            // (.ff_colour: subpd / maxpd / minpd / cvttpd2dq, mulps / addps / cvtps2dq;
483            // .ff_px_done: cvtdq2pd / divpd / cvttpd2dq / packssdw / packuswb)
484            let x = Sse2;
485            let mut j = 0usize;
486            for k in 0..npx {
487                let mut sum = x.zero();
488                for _ in 0..cnt {
489                    let inside = ins[j];
490                    let val = if !mand {
491                        (nf[j] - (2.0 - 18.420680743952367 / log(sf[j]))) * fact
492                    } else if inside {
493                        0.0
494                    } else {
495                        (nf[j] - log(sf[j]) * 0.086858896380650365) * fact + 64.0
496                    };
497                    let col = match scheme {
498                        0 => {
499                            if inside {
500                                x.splat32(255)
501                            } else {
502                                let v = x.splatd(val);
503                                let z = x.zero();
504                                let lo = x.cvtt_f64_i32(x.mind(x.maxd(x.subd(v, vk1), z), v255));
505                                let hi = x.cvtt_f64_i32(x.mind(x.maxd(x.subd(v, vk2), z), v255));
506                                x.shufps::<0x44>(lo, hi)
507                            }
508                        }
509                        3 => x.load_i32(&rainbow(val, inside)),
510                        _ => {
511                            let c = if inside { 1024.0 } else { val };
512                            let t = minsd(maxsd(c * 0.0009765625, 0.0), 1.0);
513                            x.cvtr_f32_i32(x.addf(x.mulf(x.splatf(t as f32), vdc), vc1))
514                        }
515                    };
516                    sum = x.add32(sum, col);
517                    j += 1;
518                }
519                let lo = x.cvtt_f64_i32(x.divd(x.cvt_i32_f64(sum), vcf));
520                let hi = x.cvtt_f64_i32(x.divd(x.cvt_i32_f64(x.shuf32::<0x0E>(sum)), vcf));
521                let q = x.packs32(x.shufps::<0x44>(lo, hi), x.zero());
522                let mut pxv = x.to_i32(x.packus16(q, q)) as u32;
523                if invert {
524                    pxv ^= 0x00FF_FFFF;
525                }
526                put(drow, (xa + k as i32 - dx0) as usize, pxv);
527            }
528            xa += npx as i32;
529        }
530    }
531    0
532}
533
534fn julia_render(a: &FxArgs, dst: &mut Dst) -> i32 {
535    fractal_render(a, dst, false)
536}
537
538fn mandel_render(a: &FxArgs, dst: &mut Dst) -> i32 {
539    fractal_render(a, dst, true)
540}
541
542// ---------------------------------------------------------------- tables
543
544static BLENDS: [&str; 14] = [
545    "normal", "multiply", "additive", "color_burn", "color_dodge", "reflect", "glow", "overlay", "difference", "negation", "lighten",
546    "darken", "screen", "xor",
547];
548static SCHEMES: [&str; 4] = ["classic", "grayscale", "palette", "rainbow"];
549
550static P_CLOUDS: [FxProp; 6] = [
551    FxProp::choice(0, "blend_mode", 13.0, 0.0, &BLENDS),
552    FxProp::new(FP_INT, 0, 1, 0, "scale", 2.0, 1000.0, 250.0),
553    FxProp::new(FP_DEC, 2, 2, 0, "power", 0.0, 1.0, 0.5),
554    FxProp::new(FP_SEED, 0, 3, 0, "seed", 0.0, 0.0, 0.0),
555    FxProp::color(4, "primary_color", 0xFF00_0000),
556    FxProp::color(5, "secondary_color", 0xFFFF_FFFF),
557];
558static P_TURB: [FxProp; 7] = [
559    FxProp::choice(0, "blend_mode", 13.0, 0.0, &BLENDS),
560    FxProp::new(FP_INT, 0, 1, 0, "scale", 2.0, 1000.0, 250.0),
561    FxProp::new(FP_DEC, 2, 2, 0, "roughness", 0.0, 1.0, 0.5),
562    FxProp::new(FP_INT, 0, 3, 0, "detail", 1.0, 12.0, 8.0),
563    FxProp::new(FP_SEED, 0, 4, 0, "seed", 0.0, 0.0, 0.0),
564    FxProp::color(5, "primary_color", 0xFF00_0000),
565    FxProp::color(6, "secondary_color", 0xFFFF_FFFF),
566];
567static P_JULIA: [FxProp; 7] = [
568    FxProp::new(FP_DEC, 2, 0, 0, "factor", 1.0, 10.0, 4.0),
569    FxProp::new(FP_INT, 0, 1, 0, "quality", 1.0, 5.0, 2.0),
570    FxProp::new(FP_DEC, 2, 2, 0, "zoom", 0.0, 50.0, 1.0),
571    FxProp::new(FP_ANGLE, 2, 3, 0, "angle", -180.0, 180.0, 0.0),
572    FxProp::choice(4, "color_scheme", 3.0, 0.0, &SCHEMES),
573    FxProp::color(5, "primary_color", 0xFF00_0000),
574    FxProp::color(6, "secondary_color", 0xFFFF_FFFF),
575];
576static P_MANDEL: [FxProp; 8] = [
577    FxProp::new(FP_INT, 0, 0, 0, "factor", 1.0, 10.0, 1.0),
578    FxProp::new(FP_INT, 0, 1, 0, "quality", 1.0, 5.0, 2.0),
579    FxProp::new(FP_INT, 0, 2, 0, "zoom", 0.0, 50.0, 10.0),
580    FxProp::new(FP_ANGLE, 2, 3, 0, "angle", -180.0, 180.0, 0.0),
581    FxProp::new(FP_BOOL, 0, 4, 0, "invert_colors", 0.0, 1.0, 0.0),
582    FxProp::choice(5, "color_scheme", 3.0, 0.0, &SCHEMES),
583    FxProp::color(6, "primary_color", 0xFF00_0000),
584    FxProp::color(7, "secondary_color", 0xFFFF_FFFF),
585];
586
587const fn desc(id: u32, flags: u32, name: &'static str, render: RenderFn, props: &'static [FxProp]) -> FxDesc {
588    FxDesc { id, flags, name, prepare: None, render, props, category: FXC_RENDER, user: 0 }
589}
590
591/// the group table  [fxtab_render]
592pub static FXTAB: [FxDesc; 4] = [
593    desc(FX_CLOUDS, FXF_SEED, "clouds", clouds_render, &P_CLOUDS),
594    desc(FX_JULIA, FXF_SLOW, "julia_fractal", julia_render, &P_JULIA),
595    desc(FX_MANDELBROT, FXF_SLOW, "mandelbrot_fractal", mandel_render, &P_MANDEL),
596    desc(FX_TURBULENCE, FXF_SEED, "turbulence", turb_render, &P_TURB),
597];