add brush library

This commit is contained in:
Skyler Lehmkuhl 2026-03-04 16:42:43 -05:00
parent 63a8080e60
commit 292328bf87
18 changed files with 2691 additions and 90 deletions

View File

@ -15,17 +15,12 @@
//! The GPU compute shader (`brush_dab.wgsl`) is the authoritative implementation. //! The GPU compute shader (`brush_dab.wgsl`) is the authoritative implementation.
//! //!
//! ### Dab placement //! ### Dab placement
//! Dabs are placed along the stroke polyline at intervals of //! Spacing = 1 / max(dabs_per_basic_radius/radius, dabs_per_actual_radius/actual_radius).
//! `spacing = radius * dabs_per_radius`. Fractional remainder is tracked across //! Fractional remainder is tracked across consecutive calls via `StrokeState`.
//! consecutive calls via `StrokeState`.
//! //!
//! ### Blending //! ### Blending
//! Normal mode uses the standard "over" operator on premultiplied RGBA: //! Normal mode uses the standard "over" operator on premultiplied RGBA.
//! ```text //! Erase mode subtracts from destination alpha.
//! result_a = opa_a + (1 - opa_a) * bottom_a
//! result_rgb = opa_a * top_rgb + (1 - opa_a) * bottom_rgb
//! ```
//! Erase mode: subtract `opa_a` from the destination alpha and premultiply.
use image::RgbaImage; use image::RgbaImage;
use crate::raster_layer::{RasterBlendMode, StrokeRecord}; use crate::raster_layer::{RasterBlendMode, StrokeRecord};
@ -65,20 +60,46 @@ pub struct GpuDab {
/// Blend mode: 0 = Normal, 1 = Erase, 2 = Smudge /// Blend mode: 0 = Normal, 1 = Erase, 2 = Smudge
pub blend_mode: u32, pub blend_mode: u32,
pub _pad0: u32, /// Elliptical dab aspect ratio (1.0 = circle)
pub _pad1: u32, pub elliptical_dab_ratio: f32,
pub _pad2: u32, /// Elliptical dab rotation angle in radians
pub elliptical_dab_angle: f32,
/// Lock alpha: 0.0 = modify alpha normally, 1.0 = don't modify destination alpha
pub lock_alpha: f32,
} }
/// Transient brush stroke state (tracks partial dab position between segments) /// Transient brush stroke state (tracks position and randomness between segments)
pub struct StrokeState { pub struct StrokeState {
/// Distance along the path already "consumed" toward the next dab (in pixels) /// Distance along the path already "consumed" toward the next dab (in pixels)
pub distance_since_last_dab: f32, pub distance_since_last_dab: f32,
/// Exponentially-smoothed cursor X for slow_tracking
pub smooth_x: f32,
/// Exponentially-smoothed cursor Y for slow_tracking
pub smooth_y: f32,
/// Whether smooth_x/y have been initialised yet
pub smooth_initialized: bool,
/// xorshift32 seed for jitter and radius variation
pub rng_seed: u32,
/// Accumulated per-dab hue shift
pub color_h_phase: f32,
/// Accumulated per-dab value shift
pub color_v_phase: f32,
/// Accumulated per-dab saturation shift
pub color_s_phase: f32,
} }
impl StrokeState { impl StrokeState {
pub fn new() -> Self { pub fn new() -> Self {
Self { distance_since_last_dab: 0.0 } Self {
distance_since_last_dab: 0.0,
smooth_x: 0.0,
smooth_y: 0.0,
smooth_initialized: false,
rng_seed: 0xDEAD_BEEF,
color_h_phase: 0.0,
color_v_phase: 0.0,
color_s_phase: 0.0,
}
} }
} }
@ -86,37 +107,96 @@ impl Default for StrokeState {
fn default() -> Self { Self::new() } fn default() -> Self { Self::new() }
} }
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// xorshift32 — fast, no-alloc PRNG. Returns a value in [0, 1).
#[inline]
fn xorshift(seed: &mut u32) -> f32 {
let mut s = *seed;
s ^= s << 13;
s ^= s >> 17;
s ^= s << 5;
*seed = s;
(s as f32) / (u32::MAX as f32)
}
/// Convert linear RGB (premultiplied, alpha already separated) to HSV.
/// Input: r, g, b in [0, 1] (not premultiplied; caller divides by alpha first).
fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min;
let v = max;
let s = if max > 1e-6 { delta / max } else { 0.0 };
let h = if delta < 1e-6 {
0.0
} else if max == r {
((g - b) / delta).rem_euclid(6.0) / 6.0
} else if max == g {
((b - r) / delta + 2.0) / 6.0
} else {
((r - g) / delta + 4.0) / 6.0
};
(h, s, v)
}
/// Convert HSV to linear RGB.
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
let h6 = h.rem_euclid(1.0) * 6.0;
let i = h6.floor() as i32;
let f = h6 - i as f32;
let p = v * (1.0 - s);
let q = v * (1.0 - s * f);
let t = v * (1.0 - s * (1.0 - f));
match i % 6 {
0 => (v, t, p),
1 => (q, v, p),
2 => (p, v, t),
3 => (p, q, v),
4 => (t, p, v),
_ => (v, p, q),
}
}
/// Pure-Rust MyPaint-style Gaussian dab brush engine /// Pure-Rust MyPaint-style Gaussian dab brush engine
pub struct BrushEngine; pub struct BrushEngine;
impl BrushEngine { impl BrushEngine {
/// Compute the list of GPU dabs for a stroke segment. /// Compute the list of GPU dabs for a stroke segment.
/// ///
/// Uses the same dab-spacing logic as [`apply_stroke_with_state`] but produces /// Uses the MyPaint dab-spacing formula and produces [`GpuDab`] structs for
/// [`GpuDab`] structs for upload to the GPU compute pipeline instead of painting /// upload to the GPU compute pipeline.
/// into a pixel buffer.
/// ///
/// Also returns the union bounding box of all dabs as `(x0, y0, x1, y1)` in /// Also returns the union bounding box of all dabs as `(x0, y0, x1, y1)` in
/// integer canvas pixel coordinates (clamped to non-negative values; `x0==i32::MAX` /// integer canvas pixel coordinates (`x0==i32::MAX` when the Vec is empty).
/// when the returned Vec is empty).
pub fn compute_dabs( pub fn compute_dabs(
stroke: &StrokeRecord, stroke: &StrokeRecord,
state: &mut StrokeState, state: &mut StrokeState,
) -> (Vec<GpuDab>, (i32, i32, i32, i32)) { ) -> (Vec<GpuDab>, (i32, i32, i32, i32)) {
let mut dabs: Vec<GpuDab> = Vec::new(); let mut dabs: Vec<GpuDab> = Vec::new();
let mut bbox = (i32::MAX, i32::MAX, i32::MIN, i32::MIN); let mut bbox = (i32::MAX, i32::MAX, i32::MIN, i32::MIN);
let bs = &stroke.brush_settings;
let blend_mode_u = match stroke.blend_mode { // Determine blend mode, allowing brush settings to override Normal
let base_blend = match stroke.blend_mode {
RasterBlendMode::Normal if bs.eraser > 0.5 => RasterBlendMode::Erase,
RasterBlendMode::Normal if bs.smudge > 0.5 => RasterBlendMode::Smudge,
other => other,
};
let blend_mode_u = match base_blend {
RasterBlendMode::Normal => 0u32, RasterBlendMode::Normal => 0u32,
RasterBlendMode::Erase => 1u32, RasterBlendMode::Erase => 1u32,
RasterBlendMode::Smudge => 2u32, RasterBlendMode::Smudge => 2u32,
}; };
let push_dab = |dabs: &mut Vec<GpuDab>, let push_dab = |dabs: &mut Vec<GpuDab>,
bbox: &mut (i32, i32, i32, i32), bbox: &mut (i32, i32, i32, i32),
x: f32, y: f32, x: f32, y: f32,
radius: f32, opacity: f32, radius: f32, opacity: f32,
ndx: f32, ndy: f32, smudge_dist: f32| { cr: f32, cg: f32, cb: f32,
ndx: f32, ndy: f32, smudge_dist: f32| {
let r_fringe = radius + 1.0; let r_fringe = radius + 1.0;
bbox.0 = bbox.0.min((x - r_fringe).floor() as i32); bbox.0 = bbox.0.min((x - r_fringe).floor() as i32);
bbox.1 = bbox.1.min((y - r_fringe).floor() as i32); bbox.1 = bbox.1.min((y - r_fringe).floor() as i32);
@ -124,26 +204,33 @@ impl BrushEngine {
bbox.3 = bbox.3.max((y + r_fringe).ceil() as i32); bbox.3 = bbox.3.max((y + r_fringe).ceil() as i32);
dabs.push(GpuDab { dabs.push(GpuDab {
x, y, radius, x, y, radius,
hardness: stroke.brush_settings.hardness, hardness: bs.hardness,
opacity, opacity,
color_r: stroke.color[0], color_r: cr,
color_g: stroke.color[1], color_g: cg,
color_b: stroke.color[2], color_b: cb,
color_a: stroke.color[3], color_a: stroke.color[3],
ndx, ndy, smudge_dist, ndx, ndy, smudge_dist,
blend_mode: blend_mode_u, blend_mode: blend_mode_u,
_pad0: 0, _pad1: 0, _pad2: 0, elliptical_dab_ratio: bs.elliptical_dab_ratio.max(1.0),
elliptical_dab_angle: bs.elliptical_dab_angle.to_radians(),
lock_alpha: bs.lock_alpha,
}); });
}; };
if stroke.points.len() < 2 { if stroke.points.len() < 2 {
if let Some(pt) = stroke.points.first() { if let Some(pt) = stroke.points.first() {
let r = stroke.brush_settings.radius_at_pressure(pt.pressure); let r = bs.radius_at_pressure(pt.pressure);
let raw_o = stroke.brush_settings.opacity_at_pressure(pt.pressure); // Default dpr for a single tap: prefer actual_radius spacing
let o = 1.0 - (1.0 - raw_o).powf(stroke.brush_settings.dabs_per_radius * 0.5); let dpr = if bs.dabs_per_radius > 0.0 { bs.dabs_per_radius }
// Single-tap smudge has no direction — skip (same as CPU engine) else { bs.dabs_per_actual_radius.max(0.01) };
if !matches!(stroke.blend_mode, RasterBlendMode::Smudge) { let raw_o = bs.opacity_at_pressure(pt.pressure);
push_dab(&mut dabs, &mut bbox, pt.x, pt.y, r, o, 0.0, 0.0, 0.0); let o = (1.0 - (1.0 - raw_o).powf(dpr * 0.5)
* (1.0 + bs.opaque_multiply)).clamp(0.0, 1.0);
if !matches!(base_blend, RasterBlendMode::Smudge) {
let (cr, cg, cb) = (stroke.color[0], stroke.color[1], stroke.color[2]);
push_dab(&mut dabs, &mut bbox, pt.x, pt.y, r, o, cr, cg, cb,
0.0, 0.0, 0.0);
} }
state.distance_since_last_dab = 0.0; state.distance_since_last_dab = 0.0;
} }
@ -162,8 +249,18 @@ impl BrushEngine {
let mut t = 0.0f32; let mut t = 0.0f32;
while t < 1.0 { while t < 1.0 {
let pressure = p0.pressure + t * (p1.pressure - p0.pressure); let pressure = p0.pressure + t * (p1.pressure - p0.pressure);
let radius = stroke.brush_settings.radius_at_pressure(pressure); let radius2 = bs.radius_at_pressure(pressure);
let spacing = (radius * stroke.brush_settings.dabs_per_radius).max(0.5);
// Spacing: densest wins between basic-radius and actual-radius methods.
// dabs_per_basic_radius = N dabs per basic_radius pixels → spacing = basic_r / N
// dabs_per_actual_radius = N dabs per actual_radius pixels → spacing = actual_r / N
let spacing_basic = if bs.dabs_per_radius > 0.0 {
radius2 / bs.dabs_per_radius
} else { f32::MAX };
let spacing_actual = if bs.dabs_per_actual_radius > 0.0 {
radius2 / bs.dabs_per_actual_radius
} else { f32::MAX };
let spacing = spacing_basic.min(spacing_actual).max(0.5);
let dist_to_next = spacing - state.distance_since_last_dab; let dist_to_next = spacing - state.distance_since_last_dab;
let seg_t_to_next = (dist_to_next / seg_len).max(0.0); let seg_t_to_next = (dist_to_next / seg_len).max(0.0);
@ -174,27 +271,87 @@ impl BrushEngine {
} }
t += seg_t_to_next; t += seg_t_to_next;
let pressure2 = p0.pressure + t * (p1.pressure - p0.pressure);
// Stroke threshold gating
if pressure2 < bs.stroke_threshold {
state.distance_since_last_dab = 0.0;
continue;
}
let mut radius2 = bs.radius_at_pressure(pressure2);
// Opacity — normalised so dense dabs don't saturate faster than sparse ones
let dpr = radius2 / spacing; // effective dabs per radius
let raw_opacity = bs.opacity_at_pressure(pressure2);
let mut opacity2 = 1.0 - (1.0 - raw_opacity).powf(dpr * 0.5);
opacity2 = (opacity2 * (1.0 + bs.opaque_multiply)).clamp(0.0, 1.0);
// Slow tracking: exponential position smoothing
let x2 = p0.x + t * dx; let x2 = p0.x + t * dx;
let y2 = p0.y + t * dy; let y2 = p0.y + t * dy;
let pressure2 = p0.pressure + t * (p1.pressure - p0.pressure); if !state.smooth_initialized {
let radius2 = stroke.brush_settings.radius_at_pressure(pressure2); state.smooth_x = x2; state.smooth_y = y2;
let raw_opacity = stroke.brush_settings.opacity_at_pressure(pressure2); state.smooth_initialized = true;
// Normalize per-dab opacity so dense dabs don't saturate faster than sparse ones. }
// Formula: per_dab = 1 (1 raw)^(dabs_per_radius / 2) let k = if bs.slow_tracking > 0.0 {
// Derivation: N = 2/dabs_per_radius dabs cover one full diameter at the centre; (-spacing / bs.slow_tracking.max(0.1)).exp()
// accumulated = 1 (1 per_dab)^N = raw → per_dab = 1 (1raw)^(dabs_per_radius/2) } else { 0.0 };
let opacity2 = 1.0 - (1.0 - raw_opacity).powf(stroke.brush_settings.dabs_per_radius * 0.5); state.smooth_x = state.smooth_x * k + x2 * (1.0 - k);
state.smooth_y = state.smooth_y * k + y2 * (1.0 - k);
let mut ex = state.smooth_x;
let mut ey = state.smooth_y;
if matches!(stroke.blend_mode, RasterBlendMode::Smudge) { // Radius jitter (log-scale)
if bs.radius_by_random != 0.0 {
let r_rng = xorshift(&mut state.rng_seed) * 2.0 - 1.0;
radius2 = (radius2 * (bs.radius_by_random * r_rng).exp()).clamp(0.5, 500.0);
}
// Position jitter + fixed offset
if bs.offset_by_random != 0.0 || bs.offset_x != 0.0 || bs.offset_y != 0.0 {
let jitter = bs.offset_by_random * radius2;
ex += (xorshift(&mut state.rng_seed) * 2.0 - 1.0) * jitter
+ bs.offset_x * radius2;
ey += (xorshift(&mut state.rng_seed) * 2.0 - 1.0) * jitter
+ bs.offset_y * radius2;
}
// Per-dab color phase shifts
state.color_h_phase += bs.change_color_h;
state.color_v_phase += bs.change_color_v;
state.color_s_phase += bs.change_color_hsv_s;
let (mut cr, mut cg, mut cb) = (
stroke.color[0], stroke.color[1], stroke.color[2],
);
let ca = stroke.color[3];
if ca > 1e-6 {
// un-premultiply for HSV conversion
let (ur, ug, ub) = (cr / ca, cg / ca, cb / ca);
let (mut h, mut s, mut v) = rgb_to_hsv(ur, ug, ub);
if bs.change_color_h != 0.0 || bs.change_color_v != 0.0
|| bs.change_color_hsv_s != 0.0 {
h = (h + state.color_h_phase).rem_euclid(1.0);
v = (v + state.color_v_phase).clamp(0.0, 1.0);
s = (s + state.color_s_phase).clamp(0.0, 1.0);
let (r2, g2, b2) = hsv_to_rgb(h, s, v);
cr = r2 * ca; cg = g2 * ca; cb = b2 * ca;
}
}
if matches!(base_blend, RasterBlendMode::Smudge) {
let ndx = dx / seg_len; let ndx = dx / seg_len;
let ndy = dy / seg_len; let ndy = dy / seg_len;
let smudge_dist = let smudge_dist =
(radius2 * stroke.brush_settings.dabs_per_radius).max(1.0); (radius2 * dpr).max(1.0) * bs.smudge_radius_log.exp();
push_dab(&mut dabs, &mut bbox, push_dab(&mut dabs, &mut bbox,
x2, y2, radius2, opacity2, ndx, ndy, smudge_dist); ex, ey, radius2, opacity2, cr, cg, cb,
ndx, ndy, smudge_dist);
} else { } else {
push_dab(&mut dabs, &mut bbox, push_dab(&mut dabs, &mut bbox,
x2, y2, radius2, opacity2, 0.0, 0.0, 0.0); ex, ey, radius2, opacity2, cr, cg, cb,
0.0, 0.0, 0.0);
} }
state.distance_since_last_dab = 0.0; state.distance_since_last_dab = 0.0;

View File

@ -1,63 +1,215 @@
//! Brush settings for the raster paint engine //! Brush settings for the raster paint engine
//! //!
//! Settings that describe the appearance and behavior of a paint brush. //! Settings that describe the appearance and behavior of a paint brush.
//! Compatible with MyPaint .myb brush file format (subset). //! Compatible with MyPaint .myb brush file format.
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
/// Settings for a paint brush /// Settings for a paint brush — mirrors the MyPaint .myb settings schema.
///
/// All fields correspond directly to MyPaint JSON keys. Fields marked
/// "parse-only" are stored so that .myb files round-trip cleanly; they will
/// be used when the dynamic-input system is wired up in a future task.
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BrushSettings { pub struct BrushSettings {
// ── Core shape ──────────────────────────────────────────────────────────
/// log(radius) base value; actual radius = exp(radius_log) /// log(radius) base value; actual radius = exp(radius_log)
pub radius_log: f32, pub radius_log: f32,
/// Edge hardness 0.0 (fully soft/gaussian) to 1.0 (hard edge) /// Edge hardness 0.0 (fully soft/gaussian) to 1.0 (hard edge)
pub hardness: f32, pub hardness: f32,
/// Base opacity 0.01.0 /// Base opacity 0.01.0
pub opaque: f32, pub opaque: f32,
/// Dab spacing as fraction of radius (smaller = denser strokes) /// Additional opacity multiplier (opaque_multiply)
pub opaque_multiply: f32,
/// Dabs per basic_radius distance (MyPaint: dabs_per_basic_radius)
pub dabs_per_radius: f32, pub dabs_per_radius: f32,
/// Dabs per actual (pressure-modified) radius distance
pub dabs_per_actual_radius: f32,
// ── Elliptical dab ──────────────────────────────────────────────────────
/// Dab aspect ratio ≥ 1.0 (1.0 = circle, 3.0 = 3:1 ellipse)
pub elliptical_dab_ratio: f32,
/// Elliptical dab rotation angle in degrees (0180)
pub elliptical_dab_angle: f32,
// ── Jitter / offset ─────────────────────────────────────────────────────
/// Random radius variation (log-scale, 0 = none)
pub radius_by_random: f32,
/// Random positional jitter in units of radius
pub offset_by_random: f32,
/// Fixed X offset in units of radius
pub offset_x: f32,
/// Fixed Y offset in units of radius
pub offset_y: f32,
// ── Position tracking ───────────────────────────────────────────────────
/// Slow position tracking — higher = brush lags behind cursor more
pub slow_tracking: f32,
/// Per-dab position tracking smoothing
pub slow_tracking_per_dab: f32,
// ── Color ───────────────────────────────────────────────────────────────
/// HSV hue (0.01.0); usually overridden by stroke color /// HSV hue (0.01.0); usually overridden by stroke color
pub color_h: f32, pub color_h: f32,
/// HSV saturation (0.01.0) /// HSV saturation (0.01.0)
pub color_s: f32, pub color_s: f32,
/// HSV value (0.01.0) /// HSV value (0.01.0)
pub color_v: f32, pub color_v: f32,
/// Per-dab hue shift (accumulates over the stroke)
pub change_color_h: f32,
/// Per-dab HSV value shift
pub change_color_v: f32,
/// Per-dab HSV saturation shift
pub change_color_hsv_s: f32,
/// Per-dab HSL lightness shift
pub change_color_l: f32,
/// Per-dab HSL saturation shift
pub change_color_hsl_s: f32,
// ── Blend ───────────────────────────────────────────────────────────────
/// Lock alpha channel (0 = off, 1 = on — don't modify destination alpha)
pub lock_alpha: f32,
/// Eraser strength (>0.5 activates erase blend when tool mode is Normal)
pub eraser: f32,
// ── Smudge ──────────────────────────────────────────────────────────────
/// Smudge amount (>0.5 activates smudge blend when tool mode is Normal)
pub smudge: f32,
/// How quickly the smudge color updates (0 = instant, 1 = slow)
pub smudge_length: f32,
/// Smudge pickup radius offset (log-scale added to radius_log)
pub smudge_radius_log: f32,
// ── Stroke gating ───────────────────────────────────────────────────────
/// Minimum pressure required to emit dabs (0 = always emit)
pub stroke_threshold: f32,
// ── Pressure dynamics ───────────────────────────────────────────────────
/// How much pressure increases/decreases radius /// How much pressure increases/decreases radius
/// Final radius = exp(radius_log + pressure_radius_gain * pressure)
pub pressure_radius_gain: f32, pub pressure_radius_gain: f32,
/// How much pressure increases/decreases opacity /// How much pressure increases/decreases opacity
/// Final opacity = opaque * (1 + pressure_opacity_gain * (pressure - 0.5))
pub pressure_opacity_gain: f32, pub pressure_opacity_gain: f32,
// ── Parse-only: future input curve system ───────────────────────────────
pub opaque_linearize: f32,
pub anti_aliasing: f32,
pub dabs_per_second: f32,
pub offset_by_speed: f32,
pub offset_by_speed_slowness: f32,
pub speed1_slowness: f32,
pub speed2_slowness: f32,
pub speed1_gamma: f32,
pub speed2_gamma: f32,
pub direction_filter: f32,
pub stroke_duration_log: f32,
pub stroke_holdtime: f32,
pub pressure_gain_log: f32,
pub smudge_transparency: f32,
pub smudge_length_log: f32,
pub smudge_bucket: f32,
pub paint_mode: f32,
pub colorize: f32,
pub posterize: f32,
pub posterize_num: f32,
pub snap_to_pixel: f32,
pub custom_input: f32,
pub custom_input_slowness: f32,
pub gridmap_scale: f32,
pub gridmap_scale_x: f32,
pub gridmap_scale_y: f32,
pub restore_color: f32,
pub offset_angle: f32,
pub offset_angle_asc: f32,
pub offset_angle_view: f32,
pub offset_angle_2: f32,
pub offset_angle_2_asc: f32,
pub offset_angle_2_view: f32,
pub offset_angle_adj: f32,
pub offset_multiplier: f32,
} }
impl BrushSettings { impl BrushSettings {
/// Default soft round brush (smooth Gaussian falloff) /// Default soft round brush (smooth Gaussian falloff)
pub fn default_round_soft() -> Self { pub fn default_round_soft() -> Self {
Self { Self {
radius_log: 2.0, // radius ≈ 7.4 px radius_log: 2.0,
hardness: 0.1, hardness: 0.1,
opaque: 0.8, opaque: 0.8,
dabs_per_radius: 0.25, opaque_multiply: 0.0,
dabs_per_radius: 2.0,
dabs_per_actual_radius: 2.0,
elliptical_dab_ratio: 1.0,
elliptical_dab_angle: 90.0,
radius_by_random: 0.0,
offset_by_random: 0.0,
offset_x: 0.0,
offset_y: 0.0,
slow_tracking: 0.0,
slow_tracking_per_dab: 0.0,
color_h: 0.0, color_h: 0.0,
color_s: 0.0, color_s: 0.0,
color_v: 0.0, color_v: 0.0,
change_color_h: 0.0,
change_color_v: 0.0,
change_color_hsv_s: 0.0,
change_color_l: 0.0,
change_color_hsl_s: 0.0,
lock_alpha: 0.0,
eraser: 0.0,
smudge: 0.0,
smudge_length: 0.5,
smudge_radius_log: 0.0,
stroke_threshold: 0.0,
pressure_radius_gain: 0.5, pressure_radius_gain: 0.5,
pressure_opacity_gain: 1.0, pressure_opacity_gain: 1.0,
opaque_linearize: 0.9,
anti_aliasing: 1.0,
dabs_per_second: 0.0,
offset_by_speed: 0.0,
offset_by_speed_slowness: 1.0,
speed1_slowness: 0.04,
speed2_slowness: 0.8,
speed1_gamma: 4.0,
speed2_gamma: 4.0,
direction_filter: 2.0,
stroke_duration_log: 4.0,
stroke_holdtime: 0.0,
pressure_gain_log: 0.0,
smudge_transparency: 0.0,
smudge_length_log: 0.0,
smudge_bucket: 0.0,
paint_mode: 1.0,
colorize: 0.0,
posterize: 0.0,
posterize_num: 0.05,
snap_to_pixel: 0.0,
custom_input: 0.0,
custom_input_slowness: 0.0,
gridmap_scale: 0.0,
gridmap_scale_x: 1.0,
gridmap_scale_y: 1.0,
restore_color: 0.0,
offset_angle: 0.0,
offset_angle_asc: 0.0,
offset_angle_view: 0.0,
offset_angle_2: 0.0,
offset_angle_2_asc: 0.0,
offset_angle_2_view: 0.0,
offset_angle_adj: 0.0,
offset_multiplier: 0.0,
} }
} }
/// Default hard round brush (sharp edge) /// Default hard round brush (sharp edge)
pub fn default_round_hard() -> Self { pub fn default_round_hard() -> Self {
Self { Self {
radius_log: 2.0,
hardness: 0.9, hardness: 0.9,
opaque: 1.0, opaque: 1.0,
dabs_per_radius: 0.2, dabs_per_radius: 2.0,
color_h: 0.0,
color_s: 0.0,
color_v: 0.0,
pressure_radius_gain: 0.3, pressure_radius_gain: 0.3,
pressure_opacity_gain: 0.8, pressure_opacity_gain: 0.8,
..Self::default_round_soft()
} }
} }
@ -73,10 +225,10 @@ impl BrushSettings {
o.clamp(0.0, 1.0) o.clamp(0.0, 1.0)
} }
/// Parse a MyPaint .myb JSON brush file (subset). /// Parse a MyPaint .myb JSON brush file.
/// ///
/// Reads `radius_logarithmic`, `hardness`, `opaque`, `dabs_per_basic_radius`, /// Reads all known settings from `settings[key].base_value`.
/// `color_h`, `color_s`, `color_v` from the `settings` key's `base_value` fields. /// Unknown keys are silently ignored for forward compatibility.
pub fn from_myb(json: &str) -> Result<Self, String> { pub fn from_myb(json: &str) -> Result<Self, String> {
let v: serde_json::Value = let v: serde_json::Value =
serde_json::from_str(json).map_err(|e| format!("JSON parse error: {e}"))?; serde_json::from_str(json).map_err(|e| format!("JSON parse error: {e}"))?;
@ -92,15 +244,13 @@ impl BrushSettings {
.unwrap_or(default) .unwrap_or(default)
}; };
// Pressure dynamics: read from the "inputs" mapping of radius/opacity // Pressure dynamics: approximate from the pressure input curve endpoints
// For simplicity, look for the pressure input point in radius_logarithmic
let pressure_radius_gain = settings let pressure_radius_gain = settings
.get("radius_logarithmic") .get("radius_logarithmic")
.and_then(|s| s.get("inputs")) .and_then(|s| s.get("inputs"))
.and_then(|inp| inp.get("pressure")) .and_then(|inp| inp.get("pressure"))
.and_then(|pts| pts.as_array()) .and_then(|pts| pts.as_array())
.and_then(|arr| { .and_then(|arr| {
// arr = [[x0,y0],[x1,y1],...] approximate as linear gain at x=1.0
if arr.len() >= 2 { if arr.len() >= 2 {
let y0 = arr[0].get(1)?.as_f64()? as f32; let y0 = arr[0].get(1)?.as_f64()? as f32;
let y1 = arr[arr.len() - 1].get(1)?.as_f64()? as f32; let y1 = arr[arr.len() - 1].get(1)?.as_f64()? as f32;
@ -128,15 +278,81 @@ impl BrushSettings {
.unwrap_or(1.0); .unwrap_or(1.0);
Ok(Self { Ok(Self {
radius_log: read_base("radius_logarithmic", 2.0), // Core shape
hardness: read_base("hardness", 0.5).clamp(0.0, 1.0), radius_log: read_base("radius_logarithmic", 2.0),
opaque: read_base("opaque", 1.0).clamp(0.0, 1.0), hardness: read_base("hardness", 0.8).clamp(0.0, 1.0),
dabs_per_radius: read_base("dabs_per_basic_radius", 0.25).clamp(0.01, 10.0), opaque: read_base("opaque", 1.0).clamp(0.0, 2.0),
color_h: read_base("color_h", 0.0), opaque_multiply: read_base("opaque_multiply", 0.0),
color_s: read_base("color_s", 0.0), dabs_per_radius: read_base("dabs_per_basic_radius", 0.0).max(0.0),
color_v: read_base("color_v", 0.0), dabs_per_actual_radius: read_base("dabs_per_actual_radius", 2.0).max(0.0),
// Elliptical dab
elliptical_dab_ratio: read_base("elliptical_dab_ratio", 1.0).max(1.0),
elliptical_dab_angle: read_base("elliptical_dab_angle", 90.0),
// Jitter / offset
radius_by_random: read_base("radius_by_random", 0.0),
offset_by_random: read_base("offset_by_random", 0.0),
offset_x: read_base("offset_x", 0.0),
offset_y: read_base("offset_y", 0.0),
// Tracking
slow_tracking: read_base("slow_tracking", 0.0),
slow_tracking_per_dab: read_base("slow_tracking_per_dab", 0.0),
// Color
color_h: read_base("color_h", 0.0),
color_s: read_base("color_s", 0.0),
color_v: read_base("color_v", 0.0),
change_color_h: read_base("change_color_h", 0.0),
change_color_v: read_base("change_color_v", 0.0),
change_color_hsv_s: read_base("change_color_hsv_s", 0.0),
change_color_l: read_base("change_color_l", 0.0),
change_color_hsl_s: read_base("change_color_hsl_s", 0.0),
// Blend
lock_alpha: read_base("lock_alpha", 0.0).clamp(0.0, 1.0),
eraser: read_base("eraser", 0.0).clamp(0.0, 1.0),
// Smudge
smudge: read_base("smudge", 0.0).clamp(0.0, 1.0),
smudge_length: read_base("smudge_length", 0.5).clamp(0.0, 1.0),
smudge_radius_log: read_base("smudge_radius_log", 0.0),
// Stroke gating
stroke_threshold: read_base("stroke_threshold", 0.0).clamp(0.0, 0.5),
// Pressure dynamics
pressure_radius_gain, pressure_radius_gain,
pressure_opacity_gain, pressure_opacity_gain,
// Parse-only
opaque_linearize: read_base("opaque_linearize", 0.9),
anti_aliasing: read_base("anti_aliasing", 1.0),
dabs_per_second: read_base("dabs_per_second", 0.0),
offset_by_speed: read_base("offset_by_speed", 0.0),
offset_by_speed_slowness: read_base("offset_by_speed_slowness", 1.0),
speed1_slowness: read_base("speed1_slowness", 0.04),
speed2_slowness: read_base("speed2_slowness", 0.8),
speed1_gamma: read_base("speed1_gamma", 4.0),
speed2_gamma: read_base("speed2_gamma", 4.0),
direction_filter: read_base("direction_filter", 2.0),
stroke_duration_log: read_base("stroke_duration_logarithmic", 4.0),
stroke_holdtime: read_base("stroke_holdtime", 0.0),
pressure_gain_log: read_base("pressure_gain_log", 0.0),
smudge_transparency: read_base("smudge_transparency", 0.0),
smudge_length_log: read_base("smudge_length_log", 0.0),
smudge_bucket: read_base("smudge_bucket", 0.0),
paint_mode: read_base("paint_mode", 1.0),
colorize: read_base("colorize", 0.0),
posterize: read_base("posterize", 0.0),
posterize_num: read_base("posterize_num", 0.05),
snap_to_pixel: read_base("snap_to_pixel", 0.0),
custom_input: read_base("custom_input", 0.0),
custom_input_slowness: read_base("custom_input_slowness", 0.0),
gridmap_scale: read_base("gridmap_scale", 0.0),
gridmap_scale_x: read_base("gridmap_scale_x", 1.0),
gridmap_scale_y: read_base("gridmap_scale_y", 1.0),
restore_color: read_base("restore_color", 0.0),
offset_angle: read_base("offset_angle", 0.0),
offset_angle_asc: read_base("offset_angle_asc", 0.0),
offset_angle_view: read_base("offset_angle_view", 0.0),
offset_angle_2: read_base("offset_angle_2", 0.0),
offset_angle_2_asc: read_base("offset_angle_2_asc", 0.0),
offset_angle_2_view: read_base("offset_angle_2_view", 0.0),
offset_angle_adj: read_base("offset_angle_adj", 0.0),
offset_multiplier: read_base("offset_multiplier", 0.0),
}) })
} }
} }
@ -146,3 +362,41 @@ impl Default for BrushSettings {
Self::default_round_soft() Self::default_round_soft()
} }
} }
// ---------------------------------------------------------------------------
// Bundled brush presets
// ---------------------------------------------------------------------------
/// A named brush preset backed by a bundled .myb file.
pub struct BrushPreset {
pub name: &'static str,
pub settings: BrushSettings,
}
/// Returns the list of bundled brush presets (parsed once from embedded .myb files).
///
/// Sources: mypaint/mypaint-brushes — CC0 1.0 Universal (Public Domain)
pub fn bundled_brushes() -> &'static [BrushPreset] {
static CACHE: OnceLock<Vec<BrushPreset>> = OnceLock::new();
CACHE.get_or_init(|| {
let mut v = Vec::new();
macro_rules! brush {
($name:literal, $path:literal) => {
if let Ok(s) = BrushSettings::from_myb(include_str!($path)) {
v.push(BrushPreset { name: $name, settings: s });
}
};
}
brush!("Pencil", "../../../src/assets/brushes/pencil.myb");
brush!("Pen", "../../../src/assets/brushes/pen.myb");
brush!("Charcoal", "../../../src/assets/brushes/charcoal.myb");
brush!("Brush", "../../../src/assets/brushes/brush.myb");
brush!("Dry Brush", "../../../src/assets/brushes/dry_brush.myb");
brush!("Ink", "../../../src/assets/brushes/ink_blot.myb");
brush!("Calligraphy", "../../../src/assets/brushes/calligraphy.myb");
brush!("Airbrush", "../../../src/assets/brushes/airbrush.myb");
brush!("Chalk", "../../../src/assets/brushes/chalk.myb");
brush!("Liner", "../../../src/assets/brushes/liner.myb");
v
})
}

View File

@ -766,6 +766,8 @@ struct EditorApp {
brush_hardness: f32, // brush hardness 0.01.0 brush_hardness: f32, // brush hardness 0.01.0
brush_spacing: f32, // dabs_per_radius (fraction of radius per dab) brush_spacing: f32, // dabs_per_radius (fraction of radius per dab)
brush_use_fg: bool, // true = paint with FG (stroke) color, false = BG (fill) color brush_use_fg: bool, // true = paint with FG (stroke) color, false = BG (fill) color
/// Full brush settings for the currently active preset (carries elliptical, jitter, etc.)
active_brush_settings: lightningbeam_core::brush_settings::BrushSettings,
// Audio engine integration // Audio engine integration
#[allow(dead_code)] // Must be kept alive to maintain audio output #[allow(dead_code)] // Must be kept alive to maintain audio output
audio_stream: Option<cpal::Stream>, audio_stream: Option<cpal::Stream>,
@ -1047,6 +1049,7 @@ impl EditorApp {
brush_hardness: 0.5, brush_hardness: 0.5,
brush_spacing: 0.1, brush_spacing: 0.1,
brush_use_fg: true, brush_use_fg: true,
active_brush_settings: lightningbeam_core::brush_settings::BrushSettings::default(),
audio_stream, audio_stream,
audio_controller, audio_controller,
audio_event_rx, audio_event_rx,
@ -5503,6 +5506,7 @@ impl eframe::App for EditorApp {
brush_hardness: &mut self.brush_hardness, brush_hardness: &mut self.brush_hardness,
brush_spacing: &mut self.brush_spacing, brush_spacing: &mut self.brush_spacing,
brush_use_fg: &mut self.brush_use_fg, brush_use_fg: &mut self.brush_use_fg,
active_brush_settings: &mut self.active_brush_settings,
audio_controller: self.audio_controller.as_ref(), audio_controller: self.audio_controller.as_ref(),
video_manager: &self.video_manager, video_manager: &self.video_manager,
playback_time: &mut self.playback_time, playback_time: &mut self.playback_time,

View File

@ -11,6 +11,7 @@
/// - Document settings (when nothing is focused) /// - Document settings (when nothing is focused)
use eframe::egui::{self, DragValue, Ui}; use eframe::egui::{self, DragValue, Ui};
use lightningbeam_core::brush_settings::{bundled_brushes, BrushSettings};
use lightningbeam_core::actions::{SetDocumentPropertiesAction, SetShapePropertiesAction}; use lightningbeam_core::actions::{SetDocumentPropertiesAction, SetShapePropertiesAction};
use lightningbeam_core::layer::{AnyLayer, LayerTrait}; use lightningbeam_core::layer::{AnyLayer, LayerTrait};
use lightningbeam_core::selection::FocusSelection; use lightningbeam_core::selection::FocusSelection;
@ -25,6 +26,8 @@ pub struct InfopanelPane {
tool_section_open: bool, tool_section_open: bool,
/// Whether the shape properties section is expanded /// Whether the shape properties section is expanded
shape_section_open: bool, shape_section_open: bool,
/// Index of the selected brush preset (None = custom / unset)
selected_brush_preset: Option<usize>,
} }
impl InfopanelPane { impl InfopanelPane {
@ -32,6 +35,7 @@ impl InfopanelPane {
Self { Self {
tool_section_open: true, tool_section_open: true,
shape_section_open: true, shape_section_open: true,
selected_brush_preset: None,
} }
} }
} }
@ -302,6 +306,12 @@ impl InfopanelPane {
// Raster paint tools // Raster paint tools
Tool::Draw | Tool::Erase | Tool::Smudge if is_raster_paint_tool => { Tool::Draw | Tool::Erase | Tool::Smudge if is_raster_paint_tool => {
// Brush preset picker (Draw tool only)
if matches!(tool, Tool::Draw) {
self.render_brush_preset_grid(ui, shared);
ui.add_space(2.0);
}
// Color source toggle (Draw tool only) // Color source toggle (Draw tool only)
if matches!(tool, Tool::Draw) { if matches!(tool, Tool::Draw) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
@ -351,6 +361,81 @@ impl InfopanelPane {
}); });
} }
/// Render the brush preset thumbnail grid for the Draw raster tool.
fn render_brush_preset_grid(&mut self, ui: &mut Ui, shared: &mut SharedPaneState) {
let presets = bundled_brushes();
if presets.is_empty() { return; }
let gap = 3.0;
let cols = 2usize;
let cell_w = ((ui.available_width() - gap * (cols as f32 - 1.0)) / cols as f32).max(50.0);
let cell_h = 80.0;
for (row_idx, chunk) in presets.chunks(cols).enumerate() {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = gap;
for (col_idx, preset) in chunk.iter().enumerate() {
let idx = row_idx * cols + col_idx;
let is_selected = self.selected_brush_preset == Some(idx);
let (rect, resp) = ui.allocate_exact_size(
egui::vec2(cell_w, cell_h),
egui::Sense::click(),
);
let painter = ui.painter();
let bg = if is_selected {
egui::Color32::from_rgb(45, 65, 95)
} else if resp.hovered() {
egui::Color32::from_rgb(45, 50, 62)
} else {
egui::Color32::from_rgb(32, 36, 44)
};
painter.rect_filled(rect, 4.0, bg);
if is_selected {
painter.rect_stroke(
rect, 4.0,
egui::Stroke::new(1.5, egui::Color32::from_rgb(80, 140, 220)),
egui::StrokeKind::Middle,
);
}
// Dab preview (upper portion, leaving 18 px for the name)
let preview_rect = egui::Rect::from_min_size(
rect.min + egui::vec2(4.0, 4.0),
egui::vec2(cell_w - 8.0, cell_h - 22.0),
);
paint_brush_dab(painter, preview_rect, &preset.settings);
// Name
painter.text(
egui::pos2(rect.center().x, rect.max.y - 9.0),
egui::Align2::CENTER_CENTER,
preset.name,
egui::FontId::proportional(9.5),
if is_selected {
egui::Color32::from_rgb(140, 190, 255)
} else {
egui::Color32::from_gray(160)
},
);
if resp.clicked() {
self.selected_brush_preset = Some(idx);
let s = &preset.settings;
*shared.brush_radius = s.radius_at_pressure(0.5).clamp(1.0, 200.0);
*shared.brush_opacity = s.opaque.clamp(0.0, 1.0);
*shared.brush_hardness = s.hardness.clamp(0.0, 1.0);
*shared.brush_spacing = s.dabs_per_radius;
*shared.active_brush_settings = s.clone();
}
}
});
ui.add_space(gap);
}
}
// Transform section: deferred to Phase 2 (DCEL elements don't have instance transforms) // Transform section: deferred to Phase 2 (DCEL elements don't have instance transforms)
/// Render shape properties section (fill/stroke) /// Render shape properties section (fill/stroke)
@ -864,6 +949,40 @@ impl InfopanelPane {
} }
} }
/// Draw a brush dab preview into `rect` approximating the brush falloff shape.
///
/// Renders N concentric filled circles from outermost to innermost. Because each
/// inner circle overwrites the pixels of all outer circles beneath it, the visible
/// alpha at distance `d` from the centre equals the alpha of the innermost circle
/// whose radius ≥ `d`. This step-approximates the actual brush falloff formula:
/// `opa = ((1 r) / (1 hardness))²` for `r > hardness`, 1 inside the hard core.
fn paint_brush_dab(painter: &egui::Painter, rect: egui::Rect, s: &BrushSettings) {
let center = rect.center();
let max_r = (rect.width().min(rect.height()) / 2.0 - 2.0).max(1.0);
let h = s.hardness;
let a = s.opaque;
const N: usize = 12;
for i in 0..N {
// t: normalized radial position of this ring, 1.0 = outermost edge
let t = 1.0 - i as f32 / N as f32;
let r = max_r * t;
let opa_weight = if h >= 1.0 || t <= h {
1.0f32
} else {
let x = (1.0 - t) / (1.0 - h).max(1e-4);
(x * x).min(1.0)
};
let alpha = (opa_weight * a * 220.0).min(220.0) as u8;
painter.circle_filled(
center, r,
egui::Color32::from_rgba_unmultiplied(200, 200, 220, alpha),
);
}
}
/// Convert MIDI note number to note name (e.g. 60 -> "C4") /// Convert MIDI note number to note name (e.g. 60 -> "C4")
fn midi_note_name(note: u8) -> String { fn midi_note_name(note: u8) -> String {
const NAMES: [&str; 12] = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; const NAMES: [&str; 12] = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];

View File

@ -194,6 +194,8 @@ pub struct SharedPaneState<'a> {
pub brush_spacing: &'a mut f32, pub brush_spacing: &'a mut f32,
/// Whether the brush paints with the foreground (fill) color (true) or background (stroke) color (false) /// Whether the brush paints with the foreground (fill) color (true) or background (stroke) color (false)
pub brush_use_fg: &'a mut bool, pub brush_use_fg: &'a mut bool,
/// Full brush settings for the active preset (carries elliptical, jitter, slow_tracking, etc.)
pub active_brush_settings: &'a mut lightningbeam_core::brush_settings::BrushSettings,
/// Audio engine controller for playback control (wrapped in Arc<Mutex<>> for thread safety) /// Audio engine controller for playback control (wrapped in Arc<Mutex<>> for thread safety)
pub audio_controller: Option<&'a std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>, pub audio_controller: Option<&'a std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
/// Video manager for video decoding and frame caching /// Video manager for video decoding and frame caching

View File

@ -20,7 +20,7 @@ struct GpuDab {
x: f32, y: f32, radius: f32, hardness: f32, // bytes 015 x: f32, y: f32, radius: f32, hardness: f32, // bytes 015
opacity: f32, color_r: f32, color_g: f32, color_b: f32, // bytes 1631 opacity: f32, color_r: f32, color_g: f32, color_b: f32, // bytes 1631
color_a: f32, ndx: f32, ndy: f32, smudge_dist: f32, // bytes 3247 color_a: f32, ndx: f32, ndy: f32, smudge_dist: f32, // bytes 3247
blend_mode: u32, _pad0: u32, _pad1: u32, _pad2: u32, // bytes 4863 blend_mode: u32, elliptical_dab_ratio: f32, elliptical_dab_angle: f32, lock_alpha: f32, // bytes 4863
} }
struct Params { struct Params {
@ -76,7 +76,20 @@ fn bilinear_sample(px: f32, py: f32) -> vec4<f32> {
fn apply_dab(current: vec4<f32>, dab: GpuDab, px: i32, py: i32) -> vec4<f32> { fn apply_dab(current: vec4<f32>, dab: GpuDab, px: i32, py: i32) -> vec4<f32> {
let dx = f32(px) + 0.5 - dab.x; let dx = f32(px) + 0.5 - dab.x;
let dy = f32(py) + 0.5 - dab.y; let dy = f32(py) + 0.5 - dab.y;
let rr = (dx * dx + dy * dy) / (dab.radius * dab.radius);
// Normalised squared distance supports circular and elliptical dabs.
var rr: f32;
if dab.elliptical_dab_ratio > 1.001 {
// Rotate into the dab's local frame.
// Major axis is along dab.elliptical_dab_angle; minor axis is compressed by ratio.
let c = cos(dab.elliptical_dab_angle);
let s = sin(dab.elliptical_dab_angle);
let dx_r = dx * c + dy * s; // along major axis
let dy_r = (-dx * s + dy * c) * dab.elliptical_dab_ratio; // minor axis compressed
rr = (dx_r * dx_r + dy_r * dy_r) / (dab.radius * dab.radius);
} else {
rr = (dx * dx + dy * dy) / (dab.radius * dab.radius);
}
if rr > 1.0 { return current; } if rr > 1.0 { return current; }
// Quadratic falloff: flat inner core, smooth quadratic outer zone. // Quadratic falloff: flat inner core, smooth quadratic outer zone.
@ -94,15 +107,17 @@ fn apply_dab(current: vec4<f32>, dab: GpuDab, px: i32, py: i32) -> vec4<f32> {
} }
if dab.blend_mode == 0u { if dab.blend_mode == 0u {
// Normal: "over" operator // Normal: "over" operator on premultiplied RGBA.
// If lock_alpha > 0.5, preserve the destination alpha unchanged.
let dab_a = opa_weight * dab.opacity * dab.color_a; let dab_a = opa_weight * dab.opacity * dab.color_a;
if dab_a <= 0.0 { return current; } if dab_a <= 0.0 { return current; }
let ba = 1.0 - dab_a; let ba = 1.0 - dab_a;
let out_a = select(dab_a + ba * current.a, current.a, dab.lock_alpha > 0.5);
return vec4<f32>( return vec4<f32>(
dab_a * dab.color_r + ba * current.r, dab_a * dab.color_r + ba * current.r,
dab_a * dab.color_g + ba * current.g, dab_a * dab.color_g + ba * current.g,
dab_a * dab.color_b + ba * current.b, dab_a * dab.color_b + ba * current.b,
dab_a + ba * current.a, out_a,
); );
} else if dab.blend_mode == 1u { } else if dab.blend_mode == 1u {
// Erase: multiplicative alpha reduction // Erase: multiplicative alpha reduction

View File

@ -4720,18 +4720,14 @@ impl StagePane {
if !is_raster { return; } if !is_raster { return; }
let brush = { let brush = {
use lightningbeam_core::brush_settings::BrushSettings; // Start from the active preset (carries elliptical ratio/angle, jitter, etc.)
BrushSettings { // then override the four parameters the user controls via UI sliders.
radius_log: shared.brush_radius.ln(), let mut b = shared.active_brush_settings.clone();
hardness: *shared.brush_hardness, b.radius_log = shared.brush_radius.ln();
opaque: *shared.brush_opacity, b.hardness = *shared.brush_hardness;
dabs_per_radius: *shared.brush_spacing, b.opaque = *shared.brush_opacity;
color_h: 0.0, b.dabs_per_radius = *shared.brush_spacing;
color_s: 0.0, b
color_v: 0.0,
pressure_radius_gain: 0.3,
pressure_opacity_gain: 0.8,
}
}; };
let color = if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::Erase) { let color = if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::Erase) {

View File

@ -0,0 +1,9 @@
Brush presets sourced from the mypaint/mypaint-brushes repository.
https://github.com/mypaint/mypaint-brushes
License: CC0 1.0 Universal (Public Domain Dedication)
https://creativecommons.org/publicdomain/zero/1.0/
Contributors:
classic/ brushes — original MyPaint contributors
deevad/ brushes — David Revoy (deevad), http://www.davidrevoy.com

View File

@ -0,0 +1,204 @@
{
"comment": "MyPaint brush file",
"group": "",
"description": "An airbrush",
"notes": "A brush preset part of the Brushkit v0.6 \n created in october 2012 by David Revoy ( aka Deevad ) \n source: http://www.davidrevoy.com/article142/ressource-mypaint-brushes \n license: CC-Zero/Public-Domain",
"parent_brush_name": "",
"settings": {
"anti_aliasing": {
"base_value": 1.0,
"inputs": {}
},
"change_color_h": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsl_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsv_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_l": {
"base_value": 0.0,
"inputs": {}
},
"change_color_v": {
"base_value": 0.0,
"inputs": {}
},
"color_h": {
"base_value": 0.08902229845626071,
"inputs": {}
},
"color_s": {
"base_value": 1.0,
"inputs": {}
},
"color_v": {
"base_value": 0.0,
"inputs": {}
},
"colorize": {
"base_value": 0.0,
"inputs": {}
},
"custom_input": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 1.0]
]
}
},
"custom_input_slowness": {
"base_value": 0.71,
"inputs": {}
},
"dabs_per_actual_radius": {
"base_value": 5.75,
"inputs": {}
},
"dabs_per_basic_radius": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_second": {
"base_value": 0.0,
"inputs": {}
},
"direction_filter": {
"base_value": 2.99,
"inputs": {}
},
"elliptical_dab_angle": {
"base_value": 90.0,
"inputs": {
"direction": [
[0.0, 0.0],
[180.0, 180.0]
]
}
},
"elliptical_dab_ratio": {
"base_value": 1.0,
"inputs": {}
},
"eraser": {
"base_value": 0.0,
"inputs": {}
},
"hardness": {
"base_value": 0.48,
"inputs": {}
},
"lock_alpha": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_random": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed_slowness": {
"base_value": 1.0,
"inputs": {}
},
"opaque": {
"base_value": 0.52,
"inputs": {}
},
"opaque_linearize": {
"base_value": 2.0,
"inputs": {}
},
"opaque_multiply": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[0.111111, 0.5],
[0.308642, 0.833333],
[1.0, 1.0]
]
}
},
"radius_by_random": {
"base_value": 0.0,
"inputs": {}
},
"radius_logarithmic": {
"base_value": 4.7,
"inputs": {
"custom": [
[-2.0, 0.45],
[2.0, -0.45]
]
}
},
"restore_color": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking_per_dab": {
"base_value": 0.0,
"inputs": {}
},
"smudge": {
"base_value": 0.0,
"inputs": {}
},
"smudge_length": {
"base_value": 0.5,
"inputs": {}
},
"smudge_radius_log": {
"base_value": 0.0,
"inputs": {}
},
"speed1_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed1_slowness": {
"base_value": 0.04,
"inputs": {}
},
"speed2_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed2_slowness": {
"base_value": 0.8,
"inputs": {}
},
"stroke_duration_logarithmic": {
"base_value": 4.0,
"inputs": {}
},
"stroke_holdtime": {
"base_value": 0.0,
"inputs": {}
},
"stroke_threshold": {
"base_value": 0.0,
"inputs": {}
},
"tracking_noise": {
"base_value": 0.0,
"inputs": {}
}
},
"version": 3
}

View File

@ -0,0 +1,203 @@
{
"comment": "MyPaint brush file",
"group": "",
"parent_brush_name": "classic/brush",
"settings": {
"anti_aliasing": {
"base_value": 1.0,
"inputs": {}
},
"change_color_h": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsl_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsv_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_l": {
"base_value": 0.0,
"inputs": {}
},
"change_color_v": {
"base_value": 0.0,
"inputs": {}
},
"color_h": {
"base_value": 0.0,
"inputs": {}
},
"color_s": {
"base_value": 0.0,
"inputs": {}
},
"color_v": {
"base_value": 0.0,
"inputs": {}
},
"colorize": {
"base_value": 0.0,
"inputs": {}
},
"custom_input": {
"base_value": 0.0,
"inputs": {}
},
"custom_input_slowness": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_actual_radius": {
"base_value": 5.82,
"inputs": {}
},
"dabs_per_basic_radius": {
"base_value": 0.51,
"inputs": {}
},
"dabs_per_second": {
"base_value": 70.0,
"inputs": {}
},
"direction_filter": {
"base_value": 2.0,
"inputs": {}
},
"elliptical_dab_angle": {
"base_value": 90.0,
"inputs": {}
},
"elliptical_dab_ratio": {
"base_value": 1.0,
"inputs": {}
},
"eraser": {
"base_value": 0.0,
"inputs": {}
},
"hardness": {
"base_value": 0.89,
"inputs": {}
},
"lock_alpha": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_random": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed_slowness": {
"base_value": 1.0,
"inputs": {}
},
"opaque": {
"base_value": 1.0,
"inputs": {
"pressure": [
[0.0, -0.989583],
[0.38253, -0.59375],
[0.656627, 0.041667],
[1.0, 1.0]
]
}
},
"opaque_linearize": {
"base_value": 0.44,
"inputs": {}
},
"opaque_multiply": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[0.015, 0.0],
[0.069277, 0.9375],
[0.25, 1.0],
[1.0, 1.0]
]
}
},
"radius_by_random": {
"base_value": 0.0,
"inputs": {}
},
"radius_logarithmic": {
"base_value": 1.01,
"inputs": {
"pressure": [
[0.0, -1.86375],
[0.237952, -1.42],
[0.5, -0.355],
[0.76506, 1.42],
[1.0, 2.13]
]
}
},
"restore_color": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking": {
"base_value": 4.47,
"inputs": {}
},
"slow_tracking_per_dab": {
"base_value": 2.48,
"inputs": {}
},
"smudge": {
"base_value": 0.0,
"inputs": {}
},
"smudge_length": {
"base_value": 0.5,
"inputs": {}
},
"smudge_radius_log": {
"base_value": 0.0,
"inputs": {}
},
"speed1_gamma": {
"base_value": 2.87,
"inputs": {}
},
"speed1_slowness": {
"base_value": 0.04,
"inputs": {}
},
"speed2_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed2_slowness": {
"base_value": 0.8,
"inputs": {}
},
"stroke_duration_logarithmic": {
"base_value": 4.0,
"inputs": {}
},
"stroke_holdtime": {
"base_value": 0.0,
"inputs": {}
},
"stroke_threshold": {
"base_value": 0.0,
"inputs": {}
},
"tracking_noise": {
"base_value": 0.0,
"inputs": {}
}
},
"version": 3
}

View File

@ -0,0 +1,215 @@
{
"comment": "MyPaint brush file",
"description": "",
"group": "",
"notes": "",
"parent_brush_name": "classic/calligraphy",
"settings": {
"anti_aliasing": {
"base_value": 3.53,
"inputs": {}
},
"change_color_h": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsl_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsv_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_l": {
"base_value": 0.0,
"inputs": {}
},
"change_color_v": {
"base_value": 0.0,
"inputs": {}
},
"color_h": {
"base_value": 0.0,
"inputs": {}
},
"color_s": {
"base_value": 0.0,
"inputs": {}
},
"color_v": {
"base_value": 0.0,
"inputs": {}
},
"colorize": {
"base_value": 0.0,
"inputs": {}
},
"custom_input": {
"base_value": 0.0,
"inputs": {}
},
"custom_input_slowness": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_actual_radius": {
"base_value": 2.2,
"inputs": {}
},
"dabs_per_basic_radius": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_second": {
"base_value": 0.0,
"inputs": {}
},
"direction_filter": {
"base_value": 2.0,
"inputs": {}
},
"elliptical_dab_angle": {
"base_value": 45.92,
"inputs": {}
},
"elliptical_dab_ratio": {
"base_value": 5.46,
"inputs": {}
},
"eraser": {
"base_value": 0.0,
"inputs": {}
},
"hardness": {
"base_value": 0.74,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 0.05]
],
"speed1": [
[0.0, -0.0],
[1.0, -0.04]
]
}
},
"lock_alpha": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_random": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed_slowness": {
"base_value": 1.0,
"inputs": {}
},
"opaque": {
"base_value": 1.0,
"inputs": {}
},
"opaque_linearize": {
"base_value": 0.0,
"inputs": {}
},
"opaque_multiply": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[0.015, 0.0],
[0.015, 1.0],
[1.0, 1.0]
]
}
},
"pressure_gain_log": {
"base_value": 0.0,
"inputs": {}
},
"radius_by_random": {
"base_value": 0.0,
"inputs": {}
},
"radius_logarithmic": {
"base_value": 2.02,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 0.5]
],
"speed1": [
[0.0, -0.0],
[1.0, -0.12]
]
}
},
"restore_color": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking": {
"base_value": 0.65,
"inputs": {}
},
"slow_tracking_per_dab": {
"base_value": 0.8,
"inputs": {}
},
"smudge": {
"base_value": 0.0,
"inputs": {}
},
"smudge_length": {
"base_value": 0.5,
"inputs": {}
},
"smudge_radius_log": {
"base_value": 0.0,
"inputs": {}
},
"snap_to_pixel": {
"base_value": 0.0,
"inputs": {}
},
"speed1_gamma": {
"base_value": 2.87,
"inputs": {}
},
"speed1_slowness": {
"base_value": 0.04,
"inputs": {}
},
"speed2_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed2_slowness": {
"base_value": 0.8,
"inputs": {}
},
"stroke_duration_logarithmic": {
"base_value": 4.0,
"inputs": {}
},
"stroke_holdtime": {
"base_value": 0.0,
"inputs": {}
},
"stroke_threshold": {
"base_value": 0.0,
"inputs": {}
},
"tracking_noise": {
"base_value": 0.0,
"inputs": {}
}
},
"version": 3
}

View File

@ -0,0 +1,219 @@
{
"comment": "MyPaint brush file",
"description": "A chalk brush attempt, using many tiny particles on canvas to simulate grain",
"group": "",
"notes": "A brush preset part of the Brushkit v0.6 \n created in october 2012 by David Revoy ( aka Deevad ) \n source: http://www.davidrevoy.com/article142/ressource-mypaint-brushes \n license: CC-Zero/Public-Domain",
"parent_brush_name": "deevad/chalk",
"settings": {
"anti_aliasing": {
"base_value": 1.0,
"inputs": {}
},
"change_color_h": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsl_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsv_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_l": {
"base_value": 0.0,
"inputs": {}
},
"change_color_v": {
"base_value": 0.0,
"inputs": {}
},
"color_h": {
"base_value": 0.0,
"inputs": {}
},
"color_s": {
"base_value": 0.0,
"inputs": {}
},
"color_v": {
"base_value": 0.0,
"inputs": {}
},
"colorize": {
"base_value": 0.0,
"inputs": {}
},
"custom_input": {
"base_value": 0.0,
"inputs": {}
},
"custom_input_slowness": {
"base_value": 0.69,
"inputs": {}
},
"dabs_per_actual_radius": {
"base_value": 3.93,
"inputs": {}
},
"dabs_per_basic_radius": {
"base_value": 5.07,
"inputs": {}
},
"dabs_per_second": {
"base_value": 0.0,
"inputs": {}
},
"direction_filter": {
"base_value": 2.0,
"inputs": {}
},
"elliptical_dab_angle": {
"base_value": 90.0,
"inputs": {}
},
"elliptical_dab_ratio": {
"base_value": 1.0,
"inputs": {}
},
"eraser": {
"base_value": 0.0,
"inputs": {}
},
"hardness": {
"base_value": 0.67,
"inputs": {
"pressure": [
[0.0, -0.4],
[0.667722, -0.0625],
[1.0, 0.6]
]
}
},
"lock_alpha": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_random": {
"base_value": 2.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, -2.0]
],
"speed1": [
[0.0, -0.2142857142857142],
[4.0, 1.5]
],
"speed2": [
[0.0, -0.2142857142857142],
[4.0, 1.5]
]
}
},
"offset_by_speed": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed_slowness": {
"base_value": 1.0,
"inputs": {}
},
"opaque": {
"base_value": 0.2,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 0.4]
]
}
},
"opaque_linearize": {
"base_value": 0.0,
"inputs": {}
},
"opaque_multiply": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 1.0]
]
}
},
"pressure_gain_log": {
"base_value": 0.0,
"inputs": {}
},
"radius_by_random": {
"base_value": 0.0,
"inputs": {}
},
"radius_logarithmic": {
"base_value": 0.58,
"inputs": {}
},
"restore_color": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking": {
"base_value": 2.0,
"inputs": {}
},
"slow_tracking_per_dab": {
"base_value": 0.0,
"inputs": {}
},
"smudge": {
"base_value": 0.0,
"inputs": {}
},
"smudge_length": {
"base_value": 0.5,
"inputs": {}
},
"smudge_radius_log": {
"base_value": 0.0,
"inputs": {}
},
"snap_to_pixel": {
"base_value": 0.0,
"inputs": {}
},
"speed1_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed1_slowness": {
"base_value": 0.04,
"inputs": {}
},
"speed2_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed2_slowness": {
"base_value": 0.8,
"inputs": {}
},
"stroke_duration_logarithmic": {
"base_value": 4.0,
"inputs": {}
},
"stroke_holdtime": {
"base_value": 0.0,
"inputs": {}
},
"stroke_threshold": {
"base_value": 0.0,
"inputs": {}
},
"tracking_noise": {
"base_value": 0.0,
"inputs": {}
}
},
"version": 3
}

View File

@ -0,0 +1,195 @@
{
"comment": "MyPaint brush file",
"group": "",
"parent_brush_name": "",
"settings": {
"anti_aliasing": {
"base_value": 0.0,
"inputs": {}
},
"change_color_h": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsl_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsv_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_l": {
"base_value": 0.0,
"inputs": {}
},
"change_color_v": {
"base_value": 0.0,
"inputs": {}
},
"color_h": {
"base_value": 0.6354166666666666,
"inputs": {}
},
"color_s": {
"base_value": 0.8807339449541285,
"inputs": {}
},
"color_v": {
"base_value": 0.42745098039215684,
"inputs": {}
},
"colorize": {
"base_value": 0.0,
"inputs": {}
},
"custom_input": {
"base_value": 0.0,
"inputs": {}
},
"custom_input_slowness": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_actual_radius": {
"base_value": 5.0,
"inputs": {}
},
"dabs_per_basic_radius": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_second": {
"base_value": 0.0,
"inputs": {}
},
"direction_filter": {
"base_value": 2.0,
"inputs": {}
},
"elliptical_dab_angle": {
"base_value": 90.0,
"inputs": {}
},
"elliptical_dab_ratio": {
"base_value": 1.0,
"inputs": {}
},
"eraser": {
"base_value": 0.0,
"inputs": {}
},
"hardness": {
"base_value": 0.2,
"inputs": {}
},
"lock_alpha": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_random": {
"base_value": 1.6,
"inputs": {
"pressure": [
[0, 0],
[1.0, -1.4]
]
}
},
"offset_by_speed": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed_slowness": {
"base_value": 1.0,
"inputs": {}
},
"opaque": {
"base_value": 0.4,
"inputs": {
"pressure": [
[0, 0],
[1.0, 0.4]
]
}
},
"opaque_linearize": {
"base_value": 0.0,
"inputs": {}
},
"opaque_multiply": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0, 0],
[1.0, 1.0]
]
}
},
"radius_by_random": {
"base_value": 0.0,
"inputs": {}
},
"radius_logarithmic": {
"base_value": 0.7,
"inputs": {}
},
"restore_color": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking": {
"base_value": 2.0,
"inputs": {}
},
"slow_tracking_per_dab": {
"base_value": 0.0,
"inputs": {}
},
"smudge": {
"base_value": 0.0,
"inputs": {}
},
"smudge_length": {
"base_value": 0.5,
"inputs": {}
},
"smudge_radius_log": {
"base_value": 0.0,
"inputs": {}
},
"speed1_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed1_slowness": {
"base_value": 0.04,
"inputs": {}
},
"speed2_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed2_slowness": {
"base_value": 0.8,
"inputs": {}
},
"stroke_duration_logarithmic": {
"base_value": 4.0,
"inputs": {}
},
"stroke_holdtime": {
"base_value": 0.0,
"inputs": {}
},
"stroke_threshold": {
"base_value": 0.0,
"inputs": {}
},
"tracking_noise": {
"base_value": 0.0,
"inputs": {}
}
},
"version": 3
}

View File

@ -0,0 +1,210 @@
{
"comment": "MyPaint brush file",
"description": "",
"group": "",
"notes": "",
"parent_brush_name": "classic/dry_brush",
"settings": {
"anti_aliasing": {
"base_value": 0.0,
"inputs": {}
},
"change_color_h": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsl_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsv_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_l": {
"base_value": 0.0,
"inputs": {}
},
"change_color_v": {
"base_value": 0.0,
"inputs": {}
},
"color_h": {
"base_value": 0.0,
"inputs": {}
},
"color_s": {
"base_value": 0.0,
"inputs": {}
},
"color_v": {
"base_value": 0.0,
"inputs": {}
},
"colorize": {
"base_value": 0.0,
"inputs": {}
},
"custom_input": {
"base_value": 0.0,
"inputs": {}
},
"custom_input_slowness": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_actual_radius": {
"base_value": 6.0,
"inputs": {}
},
"dabs_per_basic_radius": {
"base_value": 6.0,
"inputs": {}
},
"dabs_per_second": {
"base_value": 0.0,
"inputs": {}
},
"direction_filter": {
"base_value": 2.0,
"inputs": {}
},
"elliptical_dab_angle": {
"base_value": 90.0,
"inputs": {}
},
"elliptical_dab_ratio": {
"base_value": 1.0,
"inputs": {}
},
"eraser": {
"base_value": 0.0,
"inputs": {}
},
"hardness": {
"base_value": 0.2,
"inputs": {}
},
"lock_alpha": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_random": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 1.4]
]
}
},
"offset_by_speed": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed_slowness": {
"base_value": 1.0,
"inputs": {}
},
"opaque": {
"base_value": 0.8,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 0.2]
]
}
},
"opaque_linearize": {
"base_value": 0.0,
"inputs": {}
},
"opaque_multiply": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 1.0]
]
}
},
"pressure_gain_log": {
"base_value": 0.0,
"inputs": {}
},
"radius_by_random": {
"base_value": 0.1,
"inputs": {}
},
"radius_logarithmic": {
"base_value": 0.6,
"inputs": {
"speed2": [
[0.0, 0.042857],
[4.0, -0.3]
]
}
},
"restore_color": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking": {
"base_value": 2.0,
"inputs": {}
},
"slow_tracking_per_dab": {
"base_value": 0.0,
"inputs": {}
},
"smudge": {
"base_value": 0.0,
"inputs": {}
},
"smudge_length": {
"base_value": 0.5,
"inputs": {}
},
"smudge_radius_log": {
"base_value": 0.0,
"inputs": {}
},
"snap_to_pixel": {
"base_value": 0.0,
"inputs": {}
},
"speed1_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed1_slowness": {
"base_value": 0.04,
"inputs": {}
},
"speed2_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed2_slowness": {
"base_value": 0.8,
"inputs": {}
},
"stroke_duration_logarithmic": {
"base_value": 4.0,
"inputs": {}
},
"stroke_holdtime": {
"base_value": 0.0,
"inputs": {}
},
"stroke_threshold": {
"base_value": 0.0,
"inputs": {}
},
"tracking_noise": {
"base_value": 0.0,
"inputs": {}
}
},
"version": 3
}

View File

@ -0,0 +1,200 @@
{
"comment": "MyPaint brush file",
"description": "",
"group": "",
"notes": "",
"parent_brush_name": "classic/ink_blot",
"settings": {
"anti_aliasing": {
"base_value": 1.0,
"inputs": {}
},
"change_color_h": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsl_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsv_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_l": {
"base_value": 0.0,
"inputs": {}
},
"change_color_v": {
"base_value": 0.0,
"inputs": {}
},
"color_h": {
"base_value": 0.0,
"inputs": {}
},
"color_s": {
"base_value": 0.0,
"inputs": {}
},
"color_v": {
"base_value": 0.0,
"inputs": {}
},
"colorize": {
"base_value": 0.0,
"inputs": {}
},
"custom_input": {
"base_value": 0.0,
"inputs": {}
},
"custom_input_slowness": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_actual_radius": {
"base_value": 3.32,
"inputs": {}
},
"dabs_per_basic_radius": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_second": {
"base_value": 15.0,
"inputs": {}
},
"direction_filter": {
"base_value": 2.0,
"inputs": {}
},
"elliptical_dab_angle": {
"base_value": 90.0,
"inputs": {}
},
"elliptical_dab_ratio": {
"base_value": 1.0,
"inputs": {}
},
"eraser": {
"base_value": 0.0,
"inputs": {}
},
"hardness": {
"base_value": 0.28,
"inputs": {}
},
"lock_alpha": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_random": {
"base_value": 0.17,
"inputs": {}
},
"offset_by_speed": {
"base_value": 0.02,
"inputs": {
"custom": [
[-2.0, 0.0],
[2.0, 0.0]
]
}
},
"offset_by_speed_slowness": {
"base_value": 1.0,
"inputs": {}
},
"opaque": {
"base_value": 1.0,
"inputs": {}
},
"opaque_linearize": {
"base_value": 0.9,
"inputs": {}
},
"opaque_multiply": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 1.0]
]
}
},
"pressure_gain_log": {
"base_value": 0.0,
"inputs": {}
},
"radius_by_random": {
"base_value": 0.63,
"inputs": {}
},
"radius_logarithmic": {
"base_value": 2.5,
"inputs": {}
},
"restore_color": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking_per_dab": {
"base_value": 0.0,
"inputs": {}
},
"smudge": {
"base_value": 0.0,
"inputs": {}
},
"smudge_length": {
"base_value": 0.5,
"inputs": {}
},
"smudge_radius_log": {
"base_value": 0.0,
"inputs": {}
},
"snap_to_pixel": {
"base_value": 0.0,
"inputs": {}
},
"speed1_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed1_slowness": {
"base_value": 0.04,
"inputs": {}
},
"speed2_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed2_slowness": {
"base_value": 0.8,
"inputs": {}
},
"stroke_duration_logarithmic": {
"base_value": 4.0,
"inputs": {}
},
"stroke_holdtime": {
"base_value": 0.0,
"inputs": {}
},
"stroke_threshold": {
"base_value": 0.0,
"inputs": {}
},
"tracking_noise": {
"base_value": 0.0,
"inputs": {}
}
},
"version": 3
}

View File

@ -0,0 +1,189 @@
{
"comment": "MyPaint brush file",
"group": "",
"description": "A small brush to trace regular lines",
"notes": "A brush preset part of the Brushkit v0.6 \n created in october 2012 by David Revoy ( aka Deevad ) \n source: http://www.davidrevoy.com/article142/ressource-mypaint-brushes \n license: CC-Zero/Public-Domain",
"parent_brush_name": "",
"settings": {
"anti_aliasing": {
"base_value": 2.0,
"inputs": {}
},
"change_color_h": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsl_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsv_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_l": {
"base_value": 0.0,
"inputs": {}
},
"change_color_v": {
"base_value": 0.0,
"inputs": {}
},
"color_h": {
"base_value": 0.1289192800566187,
"inputs": {}
},
"color_s": {
"base_value": 1.0,
"inputs": {}
},
"color_v": {
"base_value": 0.0,
"inputs": {}
},
"colorize": {
"base_value": 0.0,
"inputs": {}
},
"custom_input": {
"base_value": 0.0,
"inputs": {}
},
"custom_input_slowness": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_actual_radius": {
"base_value": 4.43,
"inputs": {}
},
"dabs_per_basic_radius": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_second": {
"base_value": 0.0,
"inputs": {}
},
"direction_filter": {
"base_value": 2.0,
"inputs": {}
},
"elliptical_dab_angle": {
"base_value": 90.0,
"inputs": {}
},
"elliptical_dab_ratio": {
"base_value": 1.0,
"inputs": {}
},
"eraser": {
"base_value": 0.0,
"inputs": {}
},
"hardness": {
"base_value": 0.8,
"inputs": {}
},
"lock_alpha": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_random": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed_slowness": {
"base_value": 1.0,
"inputs": {}
},
"opaque": {
"base_value": 1.0,
"inputs": {}
},
"opaque_linearize": {
"base_value": 0.0,
"inputs": {}
},
"opaque_multiply": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[0.015, 0.0],
[0.015, 1.0],
[1.0, 1.0]
]
}
},
"radius_by_random": {
"base_value": 0.0,
"inputs": {}
},
"radius_logarithmic": {
"base_value": 0.7999999999999998,
"inputs": {}
},
"restore_color": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking_per_dab": {
"base_value": 0.0,
"inputs": {}
},
"smudge": {
"base_value": 0.0,
"inputs": {}
},
"smudge_length": {
"base_value": 0.5,
"inputs": {}
},
"smudge_radius_log": {
"base_value": 0.0,
"inputs": {}
},
"speed1_gamma": {
"base_value": 2.87,
"inputs": {}
},
"speed1_slowness": {
"base_value": 0.04,
"inputs": {}
},
"speed2_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed2_slowness": {
"base_value": 0.8,
"inputs": {}
},
"stroke_duration_logarithmic": {
"base_value": 1.18,
"inputs": {}
},
"stroke_holdtime": {
"base_value": 10.0,
"inputs": {}
},
"stroke_threshold": {
"base_value": 0.0,
"inputs": {}
},
"tracking_noise": {
"base_value": 0.0,
"inputs": {}
}
},
"version": 3
}

215
src/assets/brushes/pen.myb Normal file
View File

@ -0,0 +1,215 @@
{
"comment": "MyPaint brush file",
"description": "",
"group": "",
"notes": "",
"parent_brush_name": "classic/pen",
"settings": {
"anti_aliasing": {
"base_value": 1.0,
"inputs": {}
},
"change_color_h": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsl_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsv_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_l": {
"base_value": 0.0,
"inputs": {}
},
"change_color_v": {
"base_value": 0.0,
"inputs": {}
},
"color_h": {
"base_value": 0.0,
"inputs": {}
},
"color_s": {
"base_value": 0.0,
"inputs": {}
},
"color_v": {
"base_value": 0.0,
"inputs": {}
},
"colorize": {
"base_value": 0.0,
"inputs": {}
},
"custom_input": {
"base_value": 0.0,
"inputs": {}
},
"custom_input_slowness": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_actual_radius": {
"base_value": 2.2,
"inputs": {}
},
"dabs_per_basic_radius": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_second": {
"base_value": 0.0,
"inputs": {}
},
"direction_filter": {
"base_value": 2.0,
"inputs": {}
},
"elliptical_dab_angle": {
"base_value": 90.0,
"inputs": {}
},
"elliptical_dab_ratio": {
"base_value": 1.0,
"inputs": {}
},
"eraser": {
"base_value": 0.0,
"inputs": {}
},
"hardness": {
"base_value": 0.9,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 0.05]
],
"speed1": [
[0.0, -0.0],
[1.0, -0.09]
]
}
},
"lock_alpha": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_random": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed_slowness": {
"base_value": 1.0,
"inputs": {}
},
"opaque": {
"base_value": 1.0,
"inputs": {}
},
"opaque_linearize": {
"base_value": 0.9,
"inputs": {}
},
"opaque_multiply": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[0.015, 0.0],
[0.015, 1.0],
[1.0, 1.0]
]
}
},
"pressure_gain_log": {
"base_value": 0.0,
"inputs": {}
},
"radius_by_random": {
"base_value": 0.0,
"inputs": {}
},
"radius_logarithmic": {
"base_value": 0.96,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 0.5]
],
"speed1": [
[0.0, -0.0],
[1.0, -0.15]
]
}
},
"restore_color": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking": {
"base_value": 0.65,
"inputs": {}
},
"slow_tracking_per_dab": {
"base_value": 0.8,
"inputs": {}
},
"smudge": {
"base_value": 0.0,
"inputs": {}
},
"smudge_length": {
"base_value": 0.5,
"inputs": {}
},
"smudge_radius_log": {
"base_value": 0.0,
"inputs": {}
},
"snap_to_pixel": {
"base_value": 0.0,
"inputs": {}
},
"speed1_gamma": {
"base_value": 2.87,
"inputs": {}
},
"speed1_slowness": {
"base_value": 0.04,
"inputs": {}
},
"speed2_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed2_slowness": {
"base_value": 0.8,
"inputs": {}
},
"stroke_duration_logarithmic": {
"base_value": 4.0,
"inputs": {}
},
"stroke_holdtime": {
"base_value": 0.0,
"inputs": {}
},
"stroke_threshold": {
"base_value": 0.0,
"inputs": {}
},
"tracking_noise": {
"base_value": 0.0,
"inputs": {}
}
},
"version": 3
}

View File

@ -0,0 +1,195 @@
{
"comment": "MyPaint brush file",
"group": "",
"parent_brush_name": "classic/pencil",
"settings": {
"anti_aliasing": {
"base_value": 0.0,
"inputs": {}
},
"change_color_h": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsl_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_hsv_s": {
"base_value": 0.0,
"inputs": {}
},
"change_color_l": {
"base_value": 0.0,
"inputs": {}
},
"change_color_v": {
"base_value": 0.0,
"inputs": {}
},
"color_h": {
"base_value": 0.0,
"inputs": {}
},
"color_s": {
"base_value": 0.0,
"inputs": {}
},
"color_v": {
"base_value": 0.0,
"inputs": {}
},
"colorize": {
"base_value": 0.0,
"inputs": {}
},
"custom_input": {
"base_value": 0.0,
"inputs": {}
},
"custom_input_slowness": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_actual_radius": {
"base_value": 4.0,
"inputs": {}
},
"dabs_per_basic_radius": {
"base_value": 0.0,
"inputs": {}
},
"dabs_per_second": {
"base_value": 0.0,
"inputs": {}
},
"direction_filter": {
"base_value": 2.0,
"inputs": {}
},
"elliptical_dab_angle": {
"base_value": 90.0,
"inputs": {}
},
"elliptical_dab_ratio": {
"base_value": 1.0,
"inputs": {}
},
"eraser": {
"base_value": 0.0,
"inputs": {}
},
"hardness": {
"base_value": 0.1,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 0.3]
]
}
},
"lock_alpha": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_random": {
"base_value": 0.5,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, -0.3]
]
}
},
"offset_by_speed": {
"base_value": 0.0,
"inputs": {}
},
"offset_by_speed_slowness": {
"base_value": 1.0,
"inputs": {}
},
"opaque": {
"base_value": 0.7,
"inputs": {}
},
"opaque_linearize": {
"base_value": 0.9,
"inputs": {}
},
"opaque_multiply": {
"base_value": 0.0,
"inputs": {
"pressure": [
[0.0, 0.0],
[1.0, 1.0]
]
}
},
"radius_by_random": {
"base_value": 0.0,
"inputs": {}
},
"radius_logarithmic": {
"base_value": 0.2,
"inputs": {}
},
"restore_color": {
"base_value": 0.0,
"inputs": {}
},
"slow_tracking": {
"base_value": 1.0,
"inputs": {}
},
"slow_tracking_per_dab": {
"base_value": 0.0,
"inputs": {}
},
"smudge": {
"base_value": 0.0,
"inputs": {}
},
"smudge_length": {
"base_value": 0.5,
"inputs": {}
},
"smudge_radius_log": {
"base_value": 0.0,
"inputs": {}
},
"speed1_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed1_slowness": {
"base_value": 0.04,
"inputs": {}
},
"speed2_gamma": {
"base_value": 4.0,
"inputs": {}
},
"speed2_slowness": {
"base_value": 0.8,
"inputs": {}
},
"stroke_duration_logarithmic": {
"base_value": 4.0,
"inputs": {}
},
"stroke_holdtime": {
"base_value": 0.0,
"inputs": {}
},
"stroke_threshold": {
"base_value": 0.0,
"inputs": {}
},
"tracking_noise": {
"base_value": 0.0,
"inputs": {}
}
},
"version": 3
}