mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 07:38:24 +00:00
refactor: split complex render, CSS and WGSL checker functions (S3776, S107)
Extract helpers from curves_graph, draw_group, parse_css and the WGSL bitwise checker so each stays under the cognitive complexity limit. curves_graph now takes its pencil and natural flags as one CurveMode value, which brings it to seven parameters.
This commit is contained in:
6 files changed
+327
-236
No files matched your search
@@ -22,9 +22,22 @@ enum Tok<'a> {
|
||||
|
||||
const BITWISE: [&str; 3] = ["&", "|", "^"];
|
||||
|
||||
const OPS: [&str; 18] = ["&&", "||", "==", "!=", "<=", ">=", "<<", ">>", "<", ">", "+", "-", "*", "/", "%", "&", "|", "^"];
|
||||
|
||||
/// Drop comments and template argument lists (`vec3<f32>`, `array<vec4<u32>>`), whose
|
||||
/// angle brackets are not relational operators.
|
||||
fn strip(src: &str) -> String {
|
||||
let mut out = drop_comments(src);
|
||||
loop {
|
||||
let (next, changed) = drop_template_lists_once(&out);
|
||||
out = next;
|
||||
if !changed {
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drop_comments(src: &str) -> String {
|
||||
let mut out = String::with_capacity(src.len());
|
||||
let b = src.as_bytes();
|
||||
let mut i = 0;
|
||||
@@ -45,31 +58,98 @@ fn strip(src: &str) -> String {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
loop {
|
||||
let mut changed = false;
|
||||
let mut next = String::with_capacity(out.len());
|
||||
let c: Vec<char> = out.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < c.len() {
|
||||
if c[i] == '<' && i > 0 && (c[i - 1].is_alphanumeric() || c[i - 1] == '_') {
|
||||
let mut j = i + 1;
|
||||
while j < c.len() && (c[j].is_alphanumeric() || c[j] == '_' || c[j] == ',' || c[j] == ' ') {
|
||||
j += 1;
|
||||
}
|
||||
if j < c.len() && c[j] == '>' {
|
||||
i = j + 1;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// One pass over `out` dropping every template argument list. Returns whether one was dropped.
|
||||
fn drop_template_lists_once(out: &str) -> (String, bool) {
|
||||
let mut changed = false;
|
||||
let mut next = String::with_capacity(out.len());
|
||||
let c: Vec<char> = out.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < c.len() {
|
||||
if let Some(end) = template_list_end(&c, i) {
|
||||
i = end;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
next.push(c[i]);
|
||||
i += 1;
|
||||
}
|
||||
(next, changed)
|
||||
}
|
||||
|
||||
/// If `c[i]` opens a template argument list after an identifier, the index just past its `>`.
|
||||
fn template_list_end(c: &[char], i: usize) -> Option<usize> {
|
||||
if c[i] != '<' || i == 0 || !(c[i - 1].is_alphanumeric() || c[i - 1] == '_') {
|
||||
return None;
|
||||
}
|
||||
let mut j = i + 1;
|
||||
while j < c.len() && (c[j].is_alphanumeric() || c[j] == '_' || c[j] == ',' || c[j] == ' ') {
|
||||
j += 1;
|
||||
}
|
||||
(j < c.len() && c[j] == '>').then_some(j + 1)
|
||||
}
|
||||
|
||||
/// Scans the token starting at `*at` (not whitespace) and advances `*at` past it.
|
||||
fn next_token<'a>(s: &'a str, at: &mut usize) -> Tok<'a> {
|
||||
let b = s.as_bytes();
|
||||
let ch = b[*at];
|
||||
let mut i = *at;
|
||||
let tok = if ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'.' {
|
||||
// Identifier, member access or number (`1e-6`, `0x9E3779B9u`).
|
||||
let start = i;
|
||||
while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_' || b[i] == b'.') {
|
||||
if (b[i] == b'e' || b[i] == b'E')
|
||||
&& b[start].is_ascii_digit()
|
||||
&& !s[start..i].starts_with("0x")
|
||||
&& i + 1 < b.len()
|
||||
&& (b[i + 1] == b'-' || b[i + 1] == b'+')
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
next.push(c[i]);
|
||||
i += 1;
|
||||
}
|
||||
out = next;
|
||||
if !changed {
|
||||
return out;
|
||||
Tok::Operand
|
||||
} else if ch == b'(' || ch == b'[' {
|
||||
i += 1;
|
||||
Tok::Open
|
||||
} else if ch == b')' || ch == b']' {
|
||||
i += 1;
|
||||
Tok::Close
|
||||
} else if b[i..].starts_with(b"->") || b[i..].starts_with(b"++") || b[i..].starts_with(b"--") {
|
||||
i += 2;
|
||||
Tok::Sep
|
||||
} else if matches!(ch, b',' | b';' | b'{' | b'}' | b':' | b'@') {
|
||||
i += 1;
|
||||
Tok::Sep
|
||||
} else if let Some(op) = OPS.iter().find(|op| b[i..].starts_with(op.as_bytes())) {
|
||||
i += op.len();
|
||||
if b.get(i) == Some(&b'=') && !matches!(*op, "==" | "!=" | "<=" | ">=") {
|
||||
i += 1;
|
||||
Tok::Sep // compound assignment
|
||||
} else {
|
||||
Tok::Op(op)
|
||||
}
|
||||
} else if ch == b'=' {
|
||||
i += 1;
|
||||
Tok::Sep
|
||||
} else {
|
||||
i += 1;
|
||||
Tok::Prefix
|
||||
};
|
||||
*at = i;
|
||||
tok
|
||||
}
|
||||
|
||||
/// Records a binary operator at the current parenthesis level, reporting it if it mixes with
|
||||
/// one already seen there.
|
||||
fn note_operator<'a>(level: &mut Vec<&'a str>, op: &'a str, line: usize, out: &mut Vec<(usize, String)>) {
|
||||
let mixes = |a: &str, b: &str| a != b && (BITWISE.contains(&a) || BITWISE.contains(&b));
|
||||
if let Some(other) = level.iter().find(|o| mixes(o, op)) {
|
||||
out.push((line, format!("mixing '{other}' and '{op}' requires parentheses")));
|
||||
}
|
||||
level.push(op);
|
||||
}
|
||||
|
||||
/// `(line, message)` for every bitwise operator mixed with another binary operator at
|
||||
@@ -82,7 +162,6 @@ fn violations(src: &str) -> Vec<(usize, String)> {
|
||||
let mut levels: Vec<Vec<&str>> = vec![vec![]];
|
||||
let mut prev = Tok::Sep;
|
||||
let (mut i, mut line) = (0, 1);
|
||||
const OPS: [&str; 18] = ["&&", "||", "==", "!=", "<=", ">=", "<<", ">>", "<", ">", "+", "-", "*", "/", "%", "&", "|", "^"];
|
||||
while i < b.len() {
|
||||
let ch = b[i];
|
||||
if ch == b'\n' {
|
||||
@@ -94,53 +173,14 @@ fn violations(src: &str) -> Vec<(usize, String)> {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let tok = if ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'.' {
|
||||
// Identifier, member access or number (`1e-6`, `0x9E3779B9u`).
|
||||
let start = i;
|
||||
while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_' || b[i] == b'.') {
|
||||
if (b[i] == b'e' || b[i] == b'E')
|
||||
&& b[start].is_ascii_digit()
|
||||
&& !s[start..i].starts_with("0x")
|
||||
&& i + 1 < b.len()
|
||||
&& (b[i + 1] == b'-' || b[i + 1] == b'+')
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Tok::Operand
|
||||
} else if ch == b'(' || ch == b'[' {
|
||||
i += 1;
|
||||
levels.push(vec![]);
|
||||
Tok::Open
|
||||
} else if ch == b')' || ch == b']' {
|
||||
i += 1;
|
||||
if levels.len() > 1 {
|
||||
levels.pop();
|
||||
}
|
||||
Tok::Close
|
||||
} else if b[i..].starts_with(b"->") || b[i..].starts_with(b"++") || b[i..].starts_with(b"--") {
|
||||
i += 2;
|
||||
Tok::Sep
|
||||
} else if matches!(ch, b',' | b';' | b'{' | b'}' | b':' | b'@') {
|
||||
i += 1;
|
||||
Tok::Sep
|
||||
} else if let Some(op) = OPS.iter().find(|op| b[i..].starts_with(op.as_bytes())) {
|
||||
i += op.len();
|
||||
if b.get(i) == Some(&b'=') && !matches!(*op, "==" | "!=" | "<=" | ">=") {
|
||||
i += 1;
|
||||
Tok::Sep // compound assignment
|
||||
} else {
|
||||
Tok::Op(op)
|
||||
}
|
||||
} else if ch == b'=' {
|
||||
i += 1;
|
||||
Tok::Sep
|
||||
} else {
|
||||
i += 1;
|
||||
Tok::Prefix
|
||||
};
|
||||
let tok = next_token(&s, &mut i);
|
||||
match tok {
|
||||
Tok::Open => levels.push(vec![]),
|
||||
Tok::Close => {
|
||||
if levels.len() > 1 {
|
||||
levels.pop();
|
||||
}
|
||||
}
|
||||
Tok::Sep => {
|
||||
if let Some(l) = levels.last_mut() {
|
||||
l.clear();
|
||||
@@ -148,11 +188,7 @@ fn violations(src: &str) -> Vec<(usize, String)> {
|
||||
}
|
||||
Tok::Op(op) if matches!(prev, Tok::Operand | Tok::Close) => {
|
||||
let level = levels.last_mut().expect("level");
|
||||
let mixes = |a: &str, b: &str| a != b && (BITWISE.contains(&a) || BITWISE.contains(&b));
|
||||
if let Some(other) = level.iter().find(|o| mixes(o, op)) {
|
||||
out.push((line, format!("mixing '{other}' and '{op}' requires parentheses")));
|
||||
}
|
||||
level.push(op);
|
||||
note_operator(level, op, line, &mut out);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
+34
-28
@@ -470,6 +470,39 @@ fn strip_comments(s: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// Splits one simple selector (`tag.class#id`) into its tag, classes and id.
|
||||
fn parse_compound(one: &str) -> (Option<String>, Vec<String>, Option<String>) {
|
||||
let mut tag = None;
|
||||
let mut classes = vec![];
|
||||
let mut id = None;
|
||||
let mut cur = String::new();
|
||||
let mut kind = 't';
|
||||
let flush = |kind: char, cur: &mut String, tag: &mut Option<String>, classes: &mut Vec<String>, id: &mut Option<String>| {
|
||||
if !cur.is_empty() {
|
||||
match kind {
|
||||
'.' => classes.push(std::mem::take(cur)),
|
||||
'#' => *id = Some(std::mem::take(cur)),
|
||||
_ => {
|
||||
if cur != "*" {
|
||||
*tag = Some(std::mem::take(cur));
|
||||
}
|
||||
cur.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
for c in one.chars() {
|
||||
if c == '.' || c == '#' {
|
||||
flush(kind, &mut cur, &mut tag, &mut classes, &mut id);
|
||||
kind = c;
|
||||
} else {
|
||||
cur.push(c);
|
||||
}
|
||||
}
|
||||
flush(kind, &mut cur, &mut tag, &mut classes, &mut id);
|
||||
(tag, classes, id)
|
||||
}
|
||||
|
||||
/// Add the rules of one `<style>` sheet. Errs once the sheets exceed [`MAX_CSS_SELECTORS`] or
|
||||
/// [`MAX_CSS_DECLARATIONS`] (#2140).
|
||||
fn parse_css(css: &str, sheet: &mut StyleSheet) -> Result<(), Error> {
|
||||
@@ -505,34 +538,7 @@ fn parse_css(css: &str, sheet: &mut StyleSheet) -> Result<(), Error> {
|
||||
sheet.blocks.push(std::mem::take(&mut decls));
|
||||
i
|
||||
});
|
||||
let mut tag = None;
|
||||
let mut classes = vec![];
|
||||
let mut id = None;
|
||||
let mut cur = String::new();
|
||||
let mut kind = 't';
|
||||
let flush = |kind: char, cur: &mut String, tag: &mut Option<String>, classes: &mut Vec<String>, id: &mut Option<String>| {
|
||||
if !cur.is_empty() {
|
||||
match kind {
|
||||
'.' => classes.push(std::mem::take(cur)),
|
||||
'#' => *id = Some(std::mem::take(cur)),
|
||||
_ => {
|
||||
if cur != "*" {
|
||||
*tag = Some(std::mem::take(cur));
|
||||
}
|
||||
cur.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
for c in one.chars() {
|
||||
if c == '.' || c == '#' {
|
||||
flush(kind, &mut cur, &mut tag, &mut classes, &mut id);
|
||||
kind = c;
|
||||
} else {
|
||||
cur.push(c);
|
||||
}
|
||||
}
|
||||
flush(kind, &mut cur, &mut tag, &mut classes, &mut id);
|
||||
let (tag, classes, id) = parse_compound(one);
|
||||
let specificity = (id.is_some() as u32 * 100)
|
||||
.saturating_add(u32::try_from(classes.len()).unwrap_or(u32::MAX).saturating_mul(10))
|
||||
.saturating_add(tag.is_some() as u32);
|
||||
|
||||
+91
-79
@@ -6,7 +6,7 @@ use effectcraft_raster::{Image, Mask};
|
||||
use kurbo::{Affine, Point, Shape as _};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::{BlendMode, Doc, FillRule, Gradient, GradientKind, Group, Node, Paint, Shape, Spread};
|
||||
use crate::{BlendMode, Doc, FillRule, Gradient, GradientKind, Group, Node, Paint, Shape, SoftMask, Spread};
|
||||
|
||||
fn mat3(a: Affine) -> Mat3 {
|
||||
let c = a.as_coeffs();
|
||||
@@ -77,6 +77,11 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image, budget: &mut
|
||||
}
|
||||
return;
|
||||
}
|
||||
draw_offscreen_group(g, m, o, dst, budget);
|
||||
}
|
||||
|
||||
/// Draws a group through an offscreen buffer, then composites it onto `dst`.
|
||||
fn draw_offscreen_group(g: &Group, m: Affine, o: f64, dst: &mut Image, budget: &mut SurfaceBudget) {
|
||||
let only_clip = g.mask.is_none() && g.blend == BlendMode::Normal && o >= 1.0 && !g.knockout;
|
||||
// Draw the children on their own (isolated) or over a copy of the backdrop, keep what
|
||||
// every clip path covers and the soft mask lets through, then composite with the group's
|
||||
@@ -85,65 +90,24 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image, budget: &mut
|
||||
if !g.isolated && backdrop.is_none() {
|
||||
return;
|
||||
}
|
||||
let mut tmp = match &backdrop {
|
||||
Some(b) => match budget.image(dst.width, dst.height, Some(b)) {
|
||||
Some(image) => image,
|
||||
None => {
|
||||
budget.release(b);
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => match budget.image(dst.width, dst.height, None) {
|
||||
Some(image) => image,
|
||||
None => return,
|
||||
},
|
||||
let Some(mut tmp) = budget.image(dst.width, dst.height, backdrop.as_ref()) else {
|
||||
release_backdrop(budget, backdrop.as_ref());
|
||||
return;
|
||||
};
|
||||
draw_children(g, m, &mut tmp, backdrop.as_ref(), budget);
|
||||
if let Some(b) = &backdrop {
|
||||
if only_clip {
|
||||
// A non-isolated clipping group: the backdrop where the clip excludes the children.
|
||||
let cov = clip_coverage(g, m, dst.width, dst.height);
|
||||
dst.data.par_iter_mut().zip(tmp.data.par_iter()).zip(cov.par_iter()).for_each(|((d, t), c)| {
|
||||
for i in 0..4 {
|
||||
d[i] = d[i] * (1.0 - c) + t[i] * c;
|
||||
}
|
||||
});
|
||||
clip_onto_backdrop(g, m, dst, &tmp);
|
||||
budget.release(&tmp);
|
||||
drop(tmp);
|
||||
if let Some(b) = &backdrop {
|
||||
budget.release(b);
|
||||
}
|
||||
release_backdrop(budget, backdrop.as_ref());
|
||||
return;
|
||||
}
|
||||
// Remove the backdrop's contribution (ISO 32000-1 §11.4.8): with the group's own
|
||||
// alpha αg (its children alone), C = Cn + (Cn − C0)·(α0/αg − α0).
|
||||
let Some(mut solo) = budget.image(dst.width, dst.height, None) else {
|
||||
if !strip_backdrop(g, m, b, &mut tmp, budget) {
|
||||
budget.release(&tmp);
|
||||
drop(tmp);
|
||||
if let Some(b) = &backdrop {
|
||||
budget.release(b);
|
||||
}
|
||||
release_backdrop(budget, backdrop.as_ref());
|
||||
return;
|
||||
};
|
||||
draw_children(g, m, &mut solo, None, budget);
|
||||
tmp.data.par_iter_mut().zip(b.data.par_iter()).zip(solo.data.par_iter()).for_each(|((t, b0), s)| {
|
||||
let ag = s[3];
|
||||
if ag <= 0.0 {
|
||||
*t = [0.0; 4];
|
||||
return;
|
||||
}
|
||||
let un = |c: &[f32; 4]| if c[3] > 0.0 { [c[0] / c[3], c[1] / c[3], c[2] / c[3]] } else { [0.0; 3] };
|
||||
let (cn, c0, a0) = (un(t), un(b0), b0[3]);
|
||||
let k = a0 / ag - a0;
|
||||
let mut out = [0.0; 4];
|
||||
for i in 0..3 {
|
||||
out[i] = (cn[i] + (cn[i] - c0[i]) * k).clamp(0.0, 1.0) * ag;
|
||||
}
|
||||
out[3] = ag;
|
||||
*t = out;
|
||||
});
|
||||
budget.release(&solo);
|
||||
drop(solo);
|
||||
}
|
||||
}
|
||||
if !g.clip.is_empty() {
|
||||
let cov = clip_coverage(g, m, dst.width, dst.height);
|
||||
@@ -153,33 +117,12 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image, budget: &mut
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some(sm) = &g.mask {
|
||||
let Some(mut mk) = budget.image(dst.width, dst.height, None) else {
|
||||
budget.release(&tmp);
|
||||
drop(tmp);
|
||||
if let Some(b) = &backdrop {
|
||||
budget.release(b);
|
||||
}
|
||||
return;
|
||||
};
|
||||
draw_group(&sm.content, m, 1.0, &mut mk, budget);
|
||||
let bc = sm.backdrop.map(|v| v as f32);
|
||||
let lum = sm.luminosity;
|
||||
tmp.data.par_iter_mut().zip(mk.data.par_iter()).for_each(|(p, q)| {
|
||||
let k = if lum {
|
||||
// The mask group composited over its backdrop colour, then its luminosity.
|
||||
let c = [0, 1, 2].map(|i| q[i] + bc[i] * (1.0 - q[3]));
|
||||
0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
|
||||
} else {
|
||||
q[3]
|
||||
};
|
||||
let k = k.clamp(0.0, 1.0);
|
||||
for v in p.iter_mut() {
|
||||
*v *= k;
|
||||
}
|
||||
});
|
||||
budget.release(&mk);
|
||||
drop(mk);
|
||||
if let Some(sm) = &g.mask
|
||||
&& !apply_soft_mask(sm, m, &mut tmp, budget)
|
||||
{
|
||||
budget.release(&tmp);
|
||||
release_backdrop(budget, backdrop.as_ref());
|
||||
return;
|
||||
}
|
||||
if g.blend == BlendMode::Normal {
|
||||
over(dst, &tmp, o as f32);
|
||||
@@ -194,12 +137,81 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image, budget: &mut
|
||||
});
|
||||
}
|
||||
budget.release(&tmp);
|
||||
drop(tmp);
|
||||
if let Some(b) = &backdrop {
|
||||
release_backdrop(budget, backdrop.as_ref());
|
||||
}
|
||||
|
||||
fn release_backdrop(budget: &mut SurfaceBudget, backdrop: Option<&Image>) {
|
||||
if let Some(b) = backdrop {
|
||||
budget.release(b);
|
||||
}
|
||||
}
|
||||
|
||||
/// A clipping group over a backdrop: the children where the clip covers, the backdrop elsewhere.
|
||||
fn clip_onto_backdrop(g: &Group, m: Affine, dst: &mut Image, tmp: &Image) {
|
||||
let cov = clip_coverage(g, m, dst.width, dst.height);
|
||||
dst.data.par_iter_mut().zip(tmp.data.par_iter()).zip(cov.par_iter()).for_each(|((d, t), c)| {
|
||||
for i in 0..4 {
|
||||
d[i] = d[i] * (1.0 - c) + t[i] * c;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Remove the backdrop's contribution (ISO 32000-1 §11.4.8): with the group's own
|
||||
/// alpha αg (its children alone), C = Cn + (Cn − C0)·(α0/αg − α0). Returns false when no
|
||||
/// scratch buffer is available.
|
||||
fn strip_backdrop(g: &Group, m: Affine, backdrop: &Image, tmp: &mut Image, budget: &mut SurfaceBudget) -> bool {
|
||||
let Some(mut solo) = budget.image(tmp.width, tmp.height, None) else {
|
||||
return false;
|
||||
};
|
||||
draw_children(g, m, &mut solo, None, budget);
|
||||
tmp.data.par_iter_mut().zip(backdrop.data.par_iter()).zip(solo.data.par_iter()).for_each(|((t, b0), s)| {
|
||||
let ag = s[3];
|
||||
if ag <= 0.0 {
|
||||
*t = [0.0; 4];
|
||||
return;
|
||||
}
|
||||
let un = |c: &[f32; 4]| if c[3] > 0.0 { [c[0] / c[3], c[1] / c[3], c[2] / c[3]] } else { [0.0; 3] };
|
||||
let (cn, c0, a0) = (un(t), un(b0), b0[3]);
|
||||
let k = a0 / ag - a0;
|
||||
let mut out = [0.0; 4];
|
||||
for i in 0..3 {
|
||||
out[i] = (cn[i] + (cn[i] - c0[i]) * k).clamp(0.0, 1.0) * ag;
|
||||
}
|
||||
out[3] = ag;
|
||||
*t = out;
|
||||
});
|
||||
budget.release(&solo);
|
||||
drop(solo);
|
||||
true
|
||||
}
|
||||
|
||||
/// Multiplies the group's pixels by its soft mask. Returns false when no scratch buffer is
|
||||
/// available.
|
||||
fn apply_soft_mask(sm: &SoftMask, m: Affine, tmp: &mut Image, budget: &mut SurfaceBudget) -> bool {
|
||||
let Some(mut mk) = budget.image(tmp.width, tmp.height, None) else {
|
||||
return false;
|
||||
};
|
||||
draw_group(&sm.content, m, 1.0, &mut mk, budget);
|
||||
let bc = sm.backdrop.map(|v| v as f32);
|
||||
let lum = sm.luminosity;
|
||||
tmp.data.par_iter_mut().zip(mk.data.par_iter()).for_each(|(p, q)| {
|
||||
let k = if lum {
|
||||
// The mask group composited over its backdrop colour, then its luminosity.
|
||||
let c = [0, 1, 2].map(|i| q[i] + bc[i] * (1.0 - q[3]));
|
||||
0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
|
||||
} else {
|
||||
q[3]
|
||||
};
|
||||
let k = k.clamp(0.0, 1.0);
|
||||
for v in p.iter_mut() {
|
||||
*v *= k;
|
||||
}
|
||||
});
|
||||
budget.release(&mk);
|
||||
drop(mk);
|
||||
true
|
||||
}
|
||||
|
||||
/// The product of a group's clip-path coverages.
|
||||
fn clip_coverage(g: &Group, m: Affine, w: u32, h: u32) -> Vec<f32> {
|
||||
let mut out = vec![1.0f32; w as usize * h as usize];
|
||||
|
||||
@@ -971,7 +971,7 @@ fn curves_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter,
|
||||
let col = channel_color(ch);
|
||||
p.rect_filled(Rect::from_min_max(pos2(gr.min.x, gr.max.y + 3.0), pos2(gr.max.x, gr.max.y + 8.0)), 0.0, col.gamma_multiply(0.5));
|
||||
let id = egui::Id::unique(("ec-curves", euid, ch));
|
||||
let (resp, edit) = fw::curves_graph(ui, gr, &cur, ch, pencil, true, id, &t);
|
||||
let (resp, edit) = fw::curves_graph(ui, gr, &cur, ch, fw::CurveMode { pencil, natural: true }, id, &t);
|
||||
app.auto.add(&format!("effectControls.effect.{euid}.curves.graph"), gr, "Curves");
|
||||
let started = resp.drag_started() || resp.clicked();
|
||||
if let Some(np) = edit.changed {
|
||||
|
||||
@@ -193,7 +193,7 @@ fn rgb_curves(
|
||||
}
|
||||
let gr = Rect::from_min_size(pos2(x0, r.min.y + 30.0), vec2(size, size));
|
||||
let gid = egui::Id::unique(("lumetri-rgb", euid, ch));
|
||||
let (resp, edit) = fw::curves_graph(ui, gr, &cur, ch, false, false, gid, &t);
|
||||
let (resp, edit) = fw::curves_graph(ui, gr, &cur, ch, fw::CurveMode { pencil: false, natural: false }, gid, &t);
|
||||
app.auto.add(&format!("{prefix}.graph"), gr, "RGB Curves");
|
||||
register_points(app, &prefix, gr, &cur.0);
|
||||
let key = gesture_key(ui, gid, resp.drag_started() || resp.clicked());
|
||||
@@ -713,7 +713,7 @@ fn glow_map(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, laye
|
||||
}
|
||||
let gr = Rect::from_min_size(pos2(x0, r.min.y + 30.0), vec2(size, size));
|
||||
let gid = egui::Id::unique(("glow-map", euid, ch));
|
||||
let (resp, edit) = fw::curves_graph(ui, gr, &map[ch], ch + 1, false, false, gid, &t);
|
||||
let (resp, edit) = fw::curves_graph(ui, gr, &map[ch], ch + 1, fw::CurveMode { pencil: false, natural: false }, gid, &t);
|
||||
app.auto.add(&format!("{prefix}.graph"), gr, "Arbitrary Map");
|
||||
register_points(app, &prefix, gr, &map[ch].0);
|
||||
let key = gesture_key(ui, gid, resp.drag_started() || resp.clicked());
|
||||
|
||||
@@ -325,23 +325,56 @@ pub struct CurveEdit {
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
/// How the Curves graph takes input: `pencil` draws freehand, `natural` picks the natural spline.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CurveMode {
|
||||
pub pencil: bool,
|
||||
pub natural: bool,
|
||||
}
|
||||
|
||||
/// The Curves graph: grid, identity diagonal, the curve, its points. Click adds a point, drag
|
||||
/// moves one, dragging a point off the graph (or Alt-clicking it) removes it. In pencil mode a
|
||||
/// drag draws the curve freehand. `natural` draws the natural spline the Curves effect renders
|
||||
/// (`Curve::parse_natural`) instead of the monotone one.
|
||||
/// drag draws the curve freehand. `mode.natural` draws the natural spline the Curves effect
|
||||
/// renders (`Curve::parse_natural`) instead of the monotone one.
|
||||
pub fn curves_graph(
|
||||
ui: &mut egui::Ui,
|
||||
rect: Rect,
|
||||
pts: &CurvePoints,
|
||||
channel: usize,
|
||||
pencil: bool,
|
||||
natural: bool,
|
||||
mode: CurveMode,
|
||||
id: egui::Id,
|
||||
t: &Tokens,
|
||||
) -> (egui::Response, CurveEdit) {
|
||||
let resp = ui.interact(rect.expand(6.0), id, Sense::click_and_drag());
|
||||
let p = ui.painter().with_clip_rect(rect.expand(8.0));
|
||||
p.rect_filled(rect, 0.0, Color32::from_rgb(0x1c, 0x1c, 0x1c));
|
||||
draw_curve_grid(&p, rect, t);
|
||||
let col = curve_channel_color(channel);
|
||||
let spline =
|
||||
if mode.natural { effectcraft_engine::effects::Curve::parse_natural(&pts.format()) } else { effectcraft_engine::effects::Curve::parse(&pts.format()) };
|
||||
let eval = |x: f32| spline.as_ref().map(|c| c.eval(x)).unwrap_or(x);
|
||||
let line: Vec<Pos2> = (0..=96)
|
||||
.map(|i| {
|
||||
let x = i as f32 / 96.0;
|
||||
curve_to_screen(rect, x, eval(x).clamp(0.0, 1.0))
|
||||
})
|
||||
.collect();
|
||||
p.add(egui::Shape::line(line, Stroke::new(1.5, col)));
|
||||
let drag_id = id.with("drag");
|
||||
let dragging: Option<usize> = ui.data(|d| d.get_temp(drag_id));
|
||||
draw_curve_points(&p, rect, pts, col, dragging);
|
||||
if mode.pencil {
|
||||
let edit = curve_pencil(ui, &resp, rect, id, &eval);
|
||||
return (resp, edit);
|
||||
}
|
||||
let mut edit = CurveEdit::default();
|
||||
curve_drag(ui, &resp, rect, pts, drag_id, dragging, &mut edit);
|
||||
let alt = ui.input(|i| i.modifiers.alt);
|
||||
curve_click(&resp, rect, pts, alt, &mut edit);
|
||||
(resp, edit)
|
||||
}
|
||||
|
||||
fn draw_curve_grid(p: &egui::Painter, rect: Rect, t: &Tokens) {
|
||||
for k in 1..4 {
|
||||
let f = k as f32 / 4.0;
|
||||
let x = rect.min.x + rect.width() * f;
|
||||
@@ -351,37 +384,29 @@ pub fn curves_graph(
|
||||
}
|
||||
p.line_segment([rect.left_bottom(), rect.right_top()], Stroke::new(1.0, Color32::from_white_alpha(40)));
|
||||
p.rect_stroke(rect, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Outside);
|
||||
let col = match channel {
|
||||
}
|
||||
|
||||
fn curve_channel_color(channel: usize) -> Color32 {
|
||||
match channel {
|
||||
1 => Color32::from_rgb(0xf0, 0x60, 0x60),
|
||||
2 => Color32::from_rgb(0x60, 0xe0, 0x70),
|
||||
3 => Color32::from_rgb(0x60, 0x90, 0xff),
|
||||
4 => Color32::from_gray(0xb0),
|
||||
_ => Color32::from_gray(0xe8),
|
||||
};
|
||||
let to_s = |x: f32, y: f32| pos2(rect.min.x + x * rect.width(), rect.max.y - y * rect.height());
|
||||
let to_g = |s: Pos2| [((s.x - rect.min.x) / rect.width()), ((rect.max.y - s.y) / rect.height())];
|
||||
let spline =
|
||||
if natural { effectcraft_engine::effects::Curve::parse_natural(&pts.format()) } else { effectcraft_engine::effects::Curve::parse(&pts.format()) };
|
||||
let seed = || {
|
||||
(0..=64)
|
||||
.map(|i| {
|
||||
let x = i as f32 / 64.0;
|
||||
spline.as_ref().map(|c| c.eval(x)).unwrap_or(x)
|
||||
})
|
||||
.collect::<Vec<f32>>()
|
||||
};
|
||||
let line: Vec<Pos2> = (0..=96)
|
||||
.map(|i| {
|
||||
let x = i as f32 / 96.0;
|
||||
to_s(x, spline.as_ref().map(|c| c.eval(x)).unwrap_or(x).clamp(0.0, 1.0))
|
||||
})
|
||||
.collect();
|
||||
p.add(egui::Shape::line(line, Stroke::new(1.5, col)));
|
||||
let drag_id = id.with("drag");
|
||||
let pencil_id = id.with("pencil");
|
||||
let dragging: Option<usize> = ui.data(|d| d.get_temp(drag_id));
|
||||
}
|
||||
}
|
||||
|
||||
fn curve_to_screen(rect: Rect, x: f32, y: f32) -> Pos2 {
|
||||
pos2(rect.min.x + x * rect.width(), rect.max.y - y * rect.height())
|
||||
}
|
||||
|
||||
fn screen_to_curve(rect: Rect, s: Pos2) -> [f32; 2] {
|
||||
[((s.x - rect.min.x) / rect.width()), ((rect.max.y - s.y) / rect.height())]
|
||||
}
|
||||
|
||||
fn draw_curve_points(p: &egui::Painter, rect: Rect, pts: &CurvePoints, col: Color32, dragging: Option<usize>) {
|
||||
for (i, q) in pts.0.iter().enumerate() {
|
||||
let s = to_s(q[0], q[1]);
|
||||
let s = curve_to_screen(rect, q[0], q[1]);
|
||||
let r = Rect::from_center_size(s, vec2(7.0, 7.0));
|
||||
if dragging == Some(i) {
|
||||
p.rect_filled(r, 0.0, col);
|
||||
@@ -390,33 +415,41 @@ pub fn curves_graph(
|
||||
p.rect_stroke(r, 0.0, Stroke::new(1.2, col), StrokeKind::Inside);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pencil mode: a drag overwrites the sampled curve under the pointer.
|
||||
fn curve_pencil(ui: &mut egui::Ui, resp: &egui::Response, rect: Rect, id: egui::Id, eval: &dyn Fn(f32) -> f32) -> CurveEdit {
|
||||
let pencil_id = id.with("pencil");
|
||||
let seed = || (0..=64).map(|i| eval(i as f32 / 64.0)).collect::<Vec<f32>>();
|
||||
let mut edit = CurveEdit::default();
|
||||
let hit_r = 7.0 / rect.width();
|
||||
let alt = ui.input(|i| i.modifiers.alt);
|
||||
if pencil {
|
||||
if resp.drag_started() {
|
||||
ui.data_mut(|d| d.insert_temp(pencil_id, seed()));
|
||||
}
|
||||
if (resp.dragged() || resp.clicked())
|
||||
&& let Some(pt) = resp.interact_pointer_pos()
|
||||
{
|
||||
let mut samples: Vec<f32> = ui.data(|d| d.get_temp(pencil_id)).unwrap_or_else(seed);
|
||||
let g = to_g(pt);
|
||||
let i = (g[0].clamp(0.0, 1.0) * 64.0).round() as usize;
|
||||
samples[i.min(64)] = g[1].clamp(0.0, 1.0);
|
||||
ui.data_mut(|d| d.insert_temp(pencil_id, samples.clone()));
|
||||
edit.changed = Some(CurvePoints::from_samples(&samples));
|
||||
}
|
||||
if resp.drag_stopped() {
|
||||
ui.data_mut(|d| d.remove::<Vec<f32>>(pencil_id));
|
||||
edit.done = true;
|
||||
}
|
||||
return (resp, edit);
|
||||
if resp.drag_started() {
|
||||
ui.data_mut(|d| d.insert_temp(pencil_id, seed()));
|
||||
}
|
||||
if (resp.dragged() || resp.clicked())
|
||||
&& let Some(pt) = resp.interact_pointer_pos()
|
||||
{
|
||||
let mut samples: Vec<f32> = ui.data(|d| d.get_temp(pencil_id)).unwrap_or_else(seed);
|
||||
let g = screen_to_curve(rect, pt);
|
||||
let i = (g[0].clamp(0.0, 1.0) * 64.0).round() as usize;
|
||||
samples[i.min(64)] = g[1].clamp(0.0, 1.0);
|
||||
ui.data_mut(|d| d.insert_temp(pencil_id, samples.clone()));
|
||||
edit.changed = Some(CurvePoints::from_samples(&samples));
|
||||
}
|
||||
if resp.drag_stopped() {
|
||||
ui.data_mut(|d| d.remove::<Vec<f32>>(pencil_id));
|
||||
edit.done = true;
|
||||
}
|
||||
edit
|
||||
}
|
||||
|
||||
/// Drag a point to move it, or drag from empty space to insert one; dropping a point well off
|
||||
/// the graph removes it.
|
||||
fn curve_drag(ui: &mut egui::Ui, resp: &egui::Response, rect: Rect, pts: &CurvePoints, drag_id: egui::Id, dragging: Option<usize>, edit: &mut CurveEdit) {
|
||||
let hit_r = 7.0 / rect.width();
|
||||
if resp.drag_started()
|
||||
&& let Some(pt) = ui.input(|i| i.pointer.press_origin())
|
||||
{
|
||||
let g = to_g(pt);
|
||||
let g = screen_to_curve(rect, pt);
|
||||
let i = match pts.hit(g[0], g[1], hit_r) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
@@ -430,7 +463,7 @@ pub fn curves_graph(
|
||||
} else if resp.dragged()
|
||||
&& let (Some(i), Some(pt)) = (dragging, resp.interact_pointer_pos())
|
||||
{
|
||||
let g = to_g(pt);
|
||||
let g = screen_to_curve(rect, pt);
|
||||
let mut np = pts.clone();
|
||||
np.move_point(i, g[0], g[1]);
|
||||
if np != *pts {
|
||||
@@ -449,10 +482,15 @@ pub fn curves_graph(
|
||||
ui.data_mut(|d| d.remove::<usize>(drag_id));
|
||||
edit.done = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// A click adds a point on empty space; Alt-clicking a point removes it.
|
||||
fn curve_click(resp: &egui::Response, rect: Rect, pts: &CurvePoints, alt: bool, edit: &mut CurveEdit) {
|
||||
if resp.clicked()
|
||||
&& let Some(pt) = resp.interact_pointer_pos()
|
||||
{
|
||||
let g = to_g(pt);
|
||||
let g = screen_to_curve(rect, pt);
|
||||
let hit_r = 7.0 / rect.width();
|
||||
let mut np = pts.clone();
|
||||
match pts.hit(g[0], g[1], hit_r) {
|
||||
Some(i) if alt => {
|
||||
@@ -468,7 +506,6 @@ pub fn curves_graph(
|
||||
}
|
||||
edit.done = true;
|
||||
}
|
||||
(resp, edit)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user