Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/processing_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ workspace = true

[dependencies]
bevy = { workspace = true }
rand = { workspace = true }
raw-window-handle = "0.6"
thiserror = "2"
tracing = "0.1"
10 changes: 10 additions & 0 deletions crates/processing_core/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ pub const OKLCH: &str = "oklch";
pub const LAB: &str = "lab";
pub const LCH: &str = "lch";
pub const XYZ: &str = "xyz";
// Processing's names for the same spaces
pub const RGB: &str = SRGB;
pub const HSB: &str = HSV;

pub const ARROW: &str = "arrow";
pub const CROSS: &str = "cross";
pub const HAND: &str = "hand";
pub const MOVE: &str = "move";
pub const TEXT: &str = "text";
pub const WAIT: &str = "wait";

pub const PBR_NEUTRAL: &str = "pbr_neutral"; // tonemapping
pub const AGX: &str = "agx";
Expand Down
1 change: 1 addition & 0 deletions crates/processing_core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod config;
pub mod constants;
pub mod error;
pub mod noise;

use std::cell::RefCell;
use std::sync::OnceLock;
Expand Down
101 changes: 101 additions & 0 deletions crates/processing_core/src/noise.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! Processing's `noise()`: value noise over a table of 4096 random values with
//! cosine smoothing, summed over octaves. A port of `PApplet.noise`, including
//! its half-degree cosine table, so sketches look the way they do in Processing.

use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};

const YWRAPB: u32 = 4;
const YWRAP: u32 = 1 << YWRAPB;
const ZWRAPB: u32 = 8;
const ZWRAP: u32 = 1 << ZWRAPB;
const SIZE: u32 = 4095;

pub struct Noise {
table: Vec<f32>,
octaves: u32,
falloff: f32,
}

impl Noise {
pub fn new(seed: u64) -> Self {
let mut rng = StdRng::seed_from_u64(seed);
Self {
table: (0..=SIZE).map(|_| rng.random::<f32>()).collect(),
octaves: 4,
falloff: 0.5,
}
}

/// Reseeds the table, keeping the detail settings.
pub fn seed(&mut self, seed: u64) {
let mut rng = StdRng::seed_from_u64(seed);
self.table.iter_mut().for_each(|v| *v = rng.random());
}

/// Non-positive values leave the current setting unchanged, as in Processing.
pub fn detail(&mut self, octaves: i32, falloff: Option<f32>) {
if octaves > 0 {
self.octaves = octaves as u32;
}
if let Some(falloff) = falloff.filter(|f| *f > 0.0) {
self.falloff = falloff;
}
}

pub fn get(&self, x: f32, y: f32, z: f32) -> f32 {
let (x, y, z) = (x.abs(), y.abs(), z.abs());
let (mut xi, mut yi, mut zi) = (x as u32, y as u32, z as u32);
let (mut xf, mut yf, mut zf) = (x - xi as f32, y - yi as f32, z - zi as f32);
let at = |i: u32| self.table[(i & SIZE) as usize];

let mut r = 0.0;
let mut amplitude = 0.5;
for _ in 0..self.octaves {
let mut of = xi
.wrapping_add(yi.wrapping_shl(YWRAPB))
.wrapping_add(zi.wrapping_shl(ZWRAPB));
let rxf = smooth(xf);
let ryf = smooth(yf);

let mut n1 = at(of);
n1 += rxf * (at(of.wrapping_add(1)) - n1);
let mut n2 = at(of.wrapping_add(YWRAP));
n2 += rxf * (at(of.wrapping_add(YWRAP + 1)) - n2);
n1 += ryf * (n2 - n1);

of = of.wrapping_add(ZWRAP);
n2 = at(of);
n2 += rxf * (at(of.wrapping_add(1)) - n2);
let mut n3 = at(of.wrapping_add(YWRAP));
n3 += rxf * (at(of.wrapping_add(YWRAP + 1)) - n3);
n2 += ryf * (n3 - n2);

n1 += smooth(zf) * (n2 - n1);

r += n1 * amplitude;
amplitude *= self.falloff;
(xi, yi, zi) = (xi.wrapping_shl(1), yi.wrapping_shl(1), zi.wrapping_shl(1));
(xf, yf, zf) = (xf * 2.0, yf * 2.0, zf * 2.0);
if xf >= 1.0 {
xi = xi.wrapping_add(1);
xf -= 1.0;
}
if yf >= 1.0 {
yi = yi.wrapping_add(1);
yf -= 1.0;
}
if zf >= 1.0 {
zi = zi.wrapping_add(1);
zf -= 1.0;
}
}
r
}
}

/// `0.5 * (1 - cos(t * PI))`, quantized to half a degree like Processing's `cosLUT`.
fn smooth(t: f32) -> f32 {
let half_degrees = (t * 360.0) as u32 % 720;
0.5 * (1.0 - (half_degrees as f32 * 0.5).to_radians().cos())
}
39 changes: 39 additions & 0 deletions crates/processing_pyo3/processing_py/math.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Processing math methods and vector/quaternion types."""
import math as _math
import random as _random
from .processing_py import math as _native_math

# extend native math so glob imports work
Expand Down Expand Up @@ -131,3 +132,41 @@ def dist(*args):
dx, dy, dz = x2 - x1, y2 - y1, z2 - z1
return sqrt(dx * dx + dy * dy + dz * dz)
raise TypeError(f"dist() takes 4 or 6 arguments ({len(args)} given)")


def random(low, high=None):
"""`random(high)` is in [0, high), `random(low, high)` in [low, high)."""
if high is None:
low, high = 0.0, low
return low + _random.random() * (high - low)


def random_gaussian():
return _random.gauss(0.0, 1.0)


def random_seed(seed):
_random.seed(seed)


def bezier_point(a, b, c, d, t):
u = 1.0 - t
return a * u * u * u + 3 * b * t * u * u + 3 * c * t * t * u + d * t * t * t


def bezier_tangent(a, b, c, d, t):
return 3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b)


# Catmull-Rom, matching `curve()` (Processing's curveTightness(0))
def curve_point(a, b, c, d, t):
return 0.5 * (
2 * b
+ (-a + c) * t
+ (2 * a - 5 * b + 4 * c - d) * t * t
+ (-a + 3 * b - 3 * c + d) * t * t * t
)


def curve_tangent(a, b, c, d, t):
return 0.5 * ((-a + c) + 2 * (2 * a - 5 * b + 4 * c - d) * t + 3 * (-a + 3 * b - 3 * c + d) * t * t)
56 changes: 56 additions & 0 deletions crates/processing_pyo3/src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,62 @@ impl ColorLike {
}
}

/// A channel read back in the range `color_mode` set for its space, or the
/// space's native range when the mode is a different space.
fn channel_in_mode(mode: &ColorMode, space: ColorSpace, value: f32, ch: usize) -> f32 {
if mode.space == space {
value / space.default_maxes()[ch] * mode.max[ch]
} else {
value
}
}

pub fn red(color: Color, mode: &ColorMode) -> f32 {
channel_in_mode(mode, ColorSpace::Srgb, Srgba::from(color).red, 0)
}

pub fn green(color: Color, mode: &ColorMode) -> f32 {
channel_in_mode(mode, ColorSpace::Srgb, Srgba::from(color).green, 1)
}

pub fn blue(color: Color, mode: &ColorMode) -> f32 {
channel_in_mode(mode, ColorSpace::Srgb, Srgba::from(color).blue, 2)
}

pub fn alpha(color: Color, mode: &ColorMode) -> f32 {
color.alpha() * mode.max[3]
}

pub fn hue(color: Color, mode: &ColorMode) -> f32 {
channel_in_mode(mode, ColorSpace::Hsv, Hsva::from(color).hue, 0)
}

pub fn saturation(color: Color, mode: &ColorMode) -> f32 {
channel_in_mode(mode, ColorSpace::Hsv, Hsva::from(color).saturation, 1)
}

pub fn brightness(color: Color, mode: &ColorMode) -> f32 {
channel_in_mode(mode, ColorSpace::Hsv, Hsva::from(color).value, 2)
}

/// Interpolates in the `color_mode` space, as Processing's `lerpColor` does
/// for RGB and HSB.
pub fn lerp_color(from: Color, to: Color, amount: f32, mode: &ColorMode) -> Color {
let in_space = |c: Color| match mode.space {
ColorSpace::Srgb => Color::Srgba(c.into()),
ColorSpace::Linear => Color::LinearRgba(c.into()),
ColorSpace::Hsl => Color::Hsla(c.into()),
ColorSpace::Hsv => Color::Hsva(c.into()),
ColorSpace::Hwb => Color::Hwba(c.into()),
ColorSpace::Oklab => Color::Oklaba(c.into()),
ColorSpace::Oklch => Color::Oklcha(c.into()),
ColorSpace::Lab => Color::Laba(c.into()),
ColorSpace::Lch => Color::Lcha(c.into()),
ColorSpace::Xyz => Color::Xyza(c.into()),
};
in_space(from).mix(&in_space(to), amount.clamp(0.0, 1.0))
}

fn parse_hex(s: &str) -> PyResult<Color> {
Srgba::hex(s)
.map(Color::Srgba)
Expand Down
2 changes: 2 additions & 0 deletions crates/processing_pyo3/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
add!(m, WORD, CHAR);
add!(m, NEAREST, CLAMP, REPEAT, MIRROR);
add!(m, SRGB, LINEAR, HSL, HSV, HWB, OKLAB, OKLCH, LAB, LCH, XYZ);
add!(m, RGB, HSB);
add!(m, ARROW, CROSS, HAND, MOVE, TEXT, WAIT);
add!(
m,
PBR_NEUTRAL,
Expand Down
91 changes: 91 additions & 0 deletions crates/processing_pyo3/src/graphics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,10 @@ impl Drop for Graphics {
}

impl Graphics {
pub(crate) fn current_color_mode(&self) -> PyResult<ColorMode> {
graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

/// Create an offscreen graphics buffer in the already-running app (no window,
/// no `init`). Backs `create_graphics()` / `new_offscreen()`. The caller must
/// ensure the app exists (i.e. `size()` was called first).
Expand Down Expand Up @@ -1162,6 +1166,93 @@ impl Graphics {
.map(crate::color::PyColor::from)
}

pub fn red(&self, color: crate::color::ColorLike) -> PyResult<f32> {
Ok(crate::color::red(
color.into_color()?,
&self.current_color_mode()?,
))
}

pub fn green(&self, color: crate::color::ColorLike) -> PyResult<f32> {
Ok(crate::color::green(
color.into_color()?,
&self.current_color_mode()?,
))
}

pub fn blue(&self, color: crate::color::ColorLike) -> PyResult<f32> {
Ok(crate::color::blue(
color.into_color()?,
&self.current_color_mode()?,
))
}

pub fn alpha(&self, color: crate::color::ColorLike) -> PyResult<f32> {
Ok(crate::color::alpha(
color.into_color()?,
&self.current_color_mode()?,
))
}

pub fn hue(&self, color: crate::color::ColorLike) -> PyResult<f32> {
Ok(crate::color::hue(
color.into_color()?,
&self.current_color_mode()?,
))
}

pub fn saturation(&self, color: crate::color::ColorLike) -> PyResult<f32> {
Ok(crate::color::saturation(
color.into_color()?,
&self.current_color_mode()?,
))
}

pub fn brightness(&self, color: crate::color::ColorLike) -> PyResult<f32> {
Ok(crate::color::brightness(
color.into_color()?,
&self.current_color_mode()?,
))
}

pub fn lerp_color(
&self,
from: crate::color::ColorLike,
to: crate::color::ColorLike,
amount: f32,
) -> PyResult<crate::color::PyColor> {
let color = crate::color::lerp_color(
from.into_color()?,
to.into_color()?,
amount,
&self.current_color_mode()?,
);
Ok(color.into())
}

/// `ARROW`, `CROSS`, `HAND`, `MOVE`, `TEXT` or `WAIT`. Also shows a hidden cursor.
#[pyo3(signature = (kind=constants::ARROW))]
pub fn cursor(&self, kind: &str) -> PyResult<()> {
use bevy::window::SystemCursorIcon;
let icon = match kind {
constants::ARROW => SystemCursorIcon::Default,
constants::CROSS => SystemCursorIcon::Crosshair,
constants::HAND => SystemCursorIcon::Pointer,
constants::MOVE => SystemCursorIcon::Move,
constants::TEXT => SystemCursorIcon::Text,
constants::WAIT => SystemCursorIcon::Wait,
_ => return Err(PyValueError::new_err(format!("unknown cursor: {kind:?}"))),
};
input_set_cursor_visible(self.surface.entity, true)
.and_then(|_| input_set_cursor_icon(self.surface.entity, icon))
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

pub fn no_cursor(&self) -> PyResult<()> {
input_set_cursor_visible(self.surface.entity, false)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

#[pyo3(signature = (*args))]
pub fn background(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> {
let color = extract_color_with_mode(
Expand Down
Loading
Loading