diff --git a/lightningbeam-ui/lightningbeam-core/src/brush_engine.rs b/lightningbeam-ui/lightningbeam-core/src/brush_engine.rs index 465d8a9..a40d181 100644 --- a/lightningbeam-ui/lightningbeam-core/src/brush_engine.rs +++ b/lightningbeam-ui/lightningbeam-core/src/brush_engine.rs @@ -15,17 +15,12 @@ //! The GPU compute shader (`brush_dab.wgsl`) is the authoritative implementation. //! //! ### Dab placement -//! Dabs are placed along the stroke polyline at intervals of -//! `spacing = radius * dabs_per_radius`. Fractional remainder is tracked across -//! consecutive calls via `StrokeState`. +//! Spacing = 1 / max(dabs_per_basic_radius/radius, dabs_per_actual_radius/actual_radius). +//! Fractional remainder is tracked across consecutive calls via `StrokeState`. //! //! ### Blending -//! Normal mode uses the standard "over" operator on premultiplied RGBA: -//! ```text -//! 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. +//! Normal mode uses the standard "over" operator on premultiplied RGBA. +//! Erase mode subtracts from destination alpha. use image::RgbaImage; use crate::raster_layer::{RasterBlendMode, StrokeRecord}; @@ -65,20 +60,46 @@ pub struct GpuDab { /// Blend mode: 0 = Normal, 1 = Erase, 2 = Smudge pub blend_mode: u32, - pub _pad0: u32, - pub _pad1: u32, - pub _pad2: u32, + /// Elliptical dab aspect ratio (1.0 = circle) + pub elliptical_dab_ratio: f32, + /// 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 { /// Distance along the path already "consumed" toward the next dab (in pixels) 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 { 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() } } +// --------------------------------------------------------------------------- +// 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 pub struct BrushEngine; impl BrushEngine { /// Compute the list of GPU dabs for a stroke segment. /// - /// Uses the same dab-spacing logic as [`apply_stroke_with_state`] but produces - /// [`GpuDab`] structs for upload to the GPU compute pipeline instead of painting - /// into a pixel buffer. + /// Uses the MyPaint dab-spacing formula and produces [`GpuDab`] structs for + /// upload to the GPU compute pipeline. /// /// 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` - /// when the returned Vec is empty). + /// integer canvas pixel coordinates (`x0==i32::MAX` when the Vec is empty). pub fn compute_dabs( stroke: &StrokeRecord, state: &mut StrokeState, ) -> (Vec, (i32, i32, i32, i32)) { let mut dabs: Vec = Vec::new(); 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::Erase => 1u32, RasterBlendMode::Smudge => 2u32, }; let push_dab = |dabs: &mut Vec, - bbox: &mut (i32, i32, i32, i32), - x: f32, y: f32, - radius: f32, opacity: f32, - ndx: f32, ndy: f32, smudge_dist: f32| { + bbox: &mut (i32, i32, i32, i32), + x: f32, y: f32, + radius: f32, opacity: f32, + cr: f32, cg: f32, cb: f32, + ndx: f32, ndy: f32, smudge_dist: f32| { let r_fringe = radius + 1.0; bbox.0 = bbox.0.min((x - 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); dabs.push(GpuDab { x, y, radius, - hardness: stroke.brush_settings.hardness, + hardness: bs.hardness, opacity, - color_r: stroke.color[0], - color_g: stroke.color[1], - color_b: stroke.color[2], + color_r: cr, + color_g: cg, + color_b: cb, color_a: stroke.color[3], ndx, ndy, smudge_dist, 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 let Some(pt) = stroke.points.first() { - let r = stroke.brush_settings.radius_at_pressure(pt.pressure); - let raw_o = stroke.brush_settings.opacity_at_pressure(pt.pressure); - let o = 1.0 - (1.0 - raw_o).powf(stroke.brush_settings.dabs_per_radius * 0.5); - // Single-tap smudge has no direction — skip (same as CPU engine) - if !matches!(stroke.blend_mode, RasterBlendMode::Smudge) { - push_dab(&mut dabs, &mut bbox, pt.x, pt.y, r, o, 0.0, 0.0, 0.0); + let r = bs.radius_at_pressure(pt.pressure); + // Default dpr for a single tap: prefer actual_radius spacing + let dpr = if bs.dabs_per_radius > 0.0 { bs.dabs_per_radius } + else { bs.dabs_per_actual_radius.max(0.01) }; + let raw_o = bs.opacity_at_pressure(pt.pressure); + 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; } @@ -162,8 +249,18 @@ impl BrushEngine { let mut t = 0.0f32; while t < 1.0 { let pressure = p0.pressure + t * (p1.pressure - p0.pressure); - let radius = stroke.brush_settings.radius_at_pressure(pressure); - let spacing = (radius * stroke.brush_settings.dabs_per_radius).max(0.5); + let radius2 = bs.radius_at_pressure(pressure); + + // 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 seg_t_to_next = (dist_to_next / seg_len).max(0.0); @@ -174,27 +271,87 @@ impl BrushEngine { } 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 y2 = p0.y + t * dy; - let pressure2 = p0.pressure + t * (p1.pressure - p0.pressure); - let radius2 = stroke.brush_settings.radius_at_pressure(pressure2); - let raw_opacity = stroke.brush_settings.opacity_at_pressure(pressure2); - // Normalize per-dab opacity so dense dabs don't saturate faster than sparse ones. - // Formula: per_dab = 1 − (1 − raw)^(dabs_per_radius / 2) - // Derivation: N = 2/dabs_per_radius dabs cover one full diameter at the centre; - // accumulated = 1 − (1 − per_dab)^N = raw → per_dab = 1 − (1−raw)^(dabs_per_radius/2) - let opacity2 = 1.0 - (1.0 - raw_opacity).powf(stroke.brush_settings.dabs_per_radius * 0.5); + if !state.smooth_initialized { + state.smooth_x = x2; state.smooth_y = y2; + state.smooth_initialized = true; + } + let k = if bs.slow_tracking > 0.0 { + (-spacing / bs.slow_tracking.max(0.1)).exp() + } else { 0.0 }; + 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 ndy = dy / seg_len; 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, - x2, y2, radius2, opacity2, ndx, ndy, smudge_dist); + ex, ey, radius2, opacity2, cr, cg, cb, + ndx, ndy, smudge_dist); } else { 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; diff --git a/lightningbeam-ui/lightningbeam-core/src/brush_settings.rs b/lightningbeam-ui/lightningbeam-core/src/brush_settings.rs index 2f3f96e..e17844f 100644 --- a/lightningbeam-ui/lightningbeam-core/src/brush_settings.rs +++ b/lightningbeam-ui/lightningbeam-core/src/brush_settings.rs @@ -1,63 +1,215 @@ //! Brush settings for the raster paint engine //! //! 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 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)] pub struct BrushSettings { + // ── Core shape ────────────────────────────────────────────────────────── /// log(radius) base value; actual radius = exp(radius_log) pub radius_log: f32, /// Edge hardness 0.0 (fully soft/gaussian) to 1.0 (hard edge) pub hardness: f32, /// Base opacity 0.0–1.0 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, + /// 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 (0–180) + 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.0–1.0); usually overridden by stroke color pub color_h: f32, /// HSV saturation (0.0–1.0) pub color_s: f32, /// HSV value (0.0–1.0) 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 - /// Final radius = exp(radius_log + pressure_radius_gain * pressure) pub pressure_radius_gain: f32, /// How much pressure increases/decreases opacity - /// Final opacity = opaque * (1 + pressure_opacity_gain * (pressure - 0.5)) 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 { /// Default soft round brush (smooth Gaussian falloff) pub fn default_round_soft() -> Self { Self { - radius_log: 2.0, // radius ≈ 7.4 px + radius_log: 2.0, hardness: 0.1, 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_s: 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_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) pub fn default_round_hard() -> Self { Self { - radius_log: 2.0, hardness: 0.9, opaque: 1.0, - dabs_per_radius: 0.2, - color_h: 0.0, - color_s: 0.0, - color_v: 0.0, + dabs_per_radius: 2.0, pressure_radius_gain: 0.3, pressure_opacity_gain: 0.8, + ..Self::default_round_soft() } } @@ -73,10 +225,10 @@ impl BrushSettings { 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`, - /// `color_h`, `color_s`, `color_v` from the `settings` key's `base_value` fields. + /// Reads all known settings from `settings[key].base_value`. + /// Unknown keys are silently ignored for forward compatibility. pub fn from_myb(json: &str) -> Result { let v: serde_json::Value = serde_json::from_str(json).map_err(|e| format!("JSON parse error: {e}"))?; @@ -92,15 +244,13 @@ impl BrushSettings { .unwrap_or(default) }; - // Pressure dynamics: read from the "inputs" mapping of radius/opacity - // For simplicity, look for the pressure input point in radius_logarithmic + // Pressure dynamics: approximate from the pressure input curve endpoints let pressure_radius_gain = settings .get("radius_logarithmic") .and_then(|s| s.get("inputs")) .and_then(|inp| inp.get("pressure")) .and_then(|pts| pts.as_array()) .and_then(|arr| { - // arr = [[x0,y0],[x1,y1],...] – approximate as linear gain at x=1.0 if arr.len() >= 2 { let y0 = arr[0].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); Ok(Self { - radius_log: read_base("radius_logarithmic", 2.0), - hardness: read_base("hardness", 0.5).clamp(0.0, 1.0), - opaque: read_base("opaque", 1.0).clamp(0.0, 1.0), - dabs_per_radius: read_base("dabs_per_basic_radius", 0.25).clamp(0.01, 10.0), - color_h: read_base("color_h", 0.0), - color_s: read_base("color_s", 0.0), - color_v: read_base("color_v", 0.0), + // Core shape + radius_log: read_base("radius_logarithmic", 2.0), + hardness: read_base("hardness", 0.8).clamp(0.0, 1.0), + opaque: read_base("opaque", 1.0).clamp(0.0, 2.0), + opaque_multiply: read_base("opaque_multiply", 0.0), + dabs_per_radius: read_base("dabs_per_basic_radius", 0.0).max(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_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() } } + +// --------------------------------------------------------------------------- +// 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> = 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 + }) +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 0ec8123..b6f13d4 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -766,6 +766,8 @@ struct EditorApp { brush_hardness: f32, // brush hardness 0.0–1.0 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 + /// Full brush settings for the currently active preset (carries elliptical, jitter, etc.) + active_brush_settings: lightningbeam_core::brush_settings::BrushSettings, // Audio engine integration #[allow(dead_code)] // Must be kept alive to maintain audio output audio_stream: Option, @@ -1047,6 +1049,7 @@ impl EditorApp { brush_hardness: 0.5, brush_spacing: 0.1, brush_use_fg: true, + active_brush_settings: lightningbeam_core::brush_settings::BrushSettings::default(), audio_stream, audio_controller, audio_event_rx, @@ -5503,6 +5506,7 @@ impl eframe::App for EditorApp { brush_hardness: &mut self.brush_hardness, brush_spacing: &mut self.brush_spacing, brush_use_fg: &mut self.brush_use_fg, + active_brush_settings: &mut self.active_brush_settings, audio_controller: self.audio_controller.as_ref(), video_manager: &self.video_manager, playback_time: &mut self.playback_time, diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs index 65176ad..2bb793f 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs @@ -11,6 +11,7 @@ /// - Document settings (when nothing is focused) use eframe::egui::{self, DragValue, Ui}; +use lightningbeam_core::brush_settings::{bundled_brushes, BrushSettings}; use lightningbeam_core::actions::{SetDocumentPropertiesAction, SetShapePropertiesAction}; use lightningbeam_core::layer::{AnyLayer, LayerTrait}; use lightningbeam_core::selection::FocusSelection; @@ -25,6 +26,8 @@ pub struct InfopanelPane { tool_section_open: bool, /// Whether the shape properties section is expanded shape_section_open: bool, + /// Index of the selected brush preset (None = custom / unset) + selected_brush_preset: Option, } impl InfopanelPane { @@ -32,6 +35,7 @@ impl InfopanelPane { Self { tool_section_open: true, shape_section_open: true, + selected_brush_preset: None, } } } @@ -302,6 +306,12 @@ impl InfopanelPane { // Raster paint tools 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) if matches!(tool, Tool::Draw) { 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) /// 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") fn midi_note_name(note: u8) -> String { const NAMES: [&str; 12] = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs index f226e72..9ef22be 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs @@ -194,6 +194,8 @@ pub struct SharedPaneState<'a> { pub brush_spacing: &'a mut f32, /// Whether the brush paints with the foreground (fill) color (true) or background (stroke) color (false) 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> for thread safety) pub audio_controller: Option<&'a std::sync::Arc>>, /// Video manager for video decoding and frame caching diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/brush_dab.wgsl b/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/brush_dab.wgsl index 0d57dc3..4c7f71c 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/brush_dab.wgsl +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/brush_dab.wgsl @@ -20,7 +20,7 @@ struct GpuDab { x: f32, y: f32, radius: f32, hardness: f32, // bytes 0–15 opacity: f32, color_r: f32, color_g: f32, color_b: f32, // bytes 16–31 color_a: f32, ndx: f32, ndy: f32, smudge_dist: f32, // bytes 32–47 - blend_mode: u32, _pad0: u32, _pad1: u32, _pad2: u32, // bytes 48–63 + blend_mode: u32, elliptical_dab_ratio: f32, elliptical_dab_angle: f32, lock_alpha: f32, // bytes 48–63 } struct Params { @@ -76,7 +76,20 @@ fn bilinear_sample(px: f32, py: f32) -> vec4 { fn apply_dab(current: vec4, dab: GpuDab, px: i32, py: i32) -> vec4 { let dx = f32(px) + 0.5 - dab.x; 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; } // Quadratic falloff: flat inner core, smooth quadratic outer zone. @@ -94,15 +107,17 @@ fn apply_dab(current: vec4, dab: GpuDab, px: i32, py: i32) -> vec4 { } 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; if dab_a <= 0.0 { return current; } let ba = 1.0 - dab_a; + let out_a = select(dab_a + ba * current.a, current.a, dab.lock_alpha > 0.5); return vec4( dab_a * dab.color_r + ba * current.r, dab_a * dab.color_g + ba * current.g, dab_a * dab.color_b + ba * current.b, - dab_a + ba * current.a, + out_a, ); } else if dab.blend_mode == 1u { // Erase: multiplicative alpha reduction diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index 4222df5..f5cc640 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -4720,18 +4720,14 @@ impl StagePane { if !is_raster { return; } let brush = { - use lightningbeam_core::brush_settings::BrushSettings; - BrushSettings { - radius_log: shared.brush_radius.ln(), - hardness: *shared.brush_hardness, - opaque: *shared.brush_opacity, - dabs_per_radius: *shared.brush_spacing, - color_h: 0.0, - color_s: 0.0, - color_v: 0.0, - pressure_radius_gain: 0.3, - pressure_opacity_gain: 0.8, - } + // Start from the active preset (carries elliptical ratio/angle, jitter, etc.) + // then override the four parameters the user controls via UI sliders. + let mut b = shared.active_brush_settings.clone(); + b.radius_log = shared.brush_radius.ln(); + b.hardness = *shared.brush_hardness; + b.opaque = *shared.brush_opacity; + b.dabs_per_radius = *shared.brush_spacing; + b }; let color = if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::Erase) { diff --git a/src/assets/brushes/CREDITS b/src/assets/brushes/CREDITS new file mode 100644 index 0000000..6ae8533 --- /dev/null +++ b/src/assets/brushes/CREDITS @@ -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 diff --git a/src/assets/brushes/airbrush.myb b/src/assets/brushes/airbrush.myb new file mode 100644 index 0000000..70e14b7 --- /dev/null +++ b/src/assets/brushes/airbrush.myb @@ -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 +} diff --git a/src/assets/brushes/brush.myb b/src/assets/brushes/brush.myb new file mode 100644 index 0000000..dfcbef1 --- /dev/null +++ b/src/assets/brushes/brush.myb @@ -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 +} diff --git a/src/assets/brushes/calligraphy.myb b/src/assets/brushes/calligraphy.myb new file mode 100644 index 0000000..9355080 --- /dev/null +++ b/src/assets/brushes/calligraphy.myb @@ -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 +} diff --git a/src/assets/brushes/chalk.myb b/src/assets/brushes/chalk.myb new file mode 100644 index 0000000..e7d32c9 --- /dev/null +++ b/src/assets/brushes/chalk.myb @@ -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 +} diff --git a/src/assets/brushes/charcoal.myb b/src/assets/brushes/charcoal.myb new file mode 100644 index 0000000..50e7d0a --- /dev/null +++ b/src/assets/brushes/charcoal.myb @@ -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 +} diff --git a/src/assets/brushes/dry_brush.myb b/src/assets/brushes/dry_brush.myb new file mode 100644 index 0000000..263c315 --- /dev/null +++ b/src/assets/brushes/dry_brush.myb @@ -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 +} diff --git a/src/assets/brushes/ink_blot.myb b/src/assets/brushes/ink_blot.myb new file mode 100644 index 0000000..d08d153 --- /dev/null +++ b/src/assets/brushes/ink_blot.myb @@ -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 +} diff --git a/src/assets/brushes/liner.myb b/src/assets/brushes/liner.myb new file mode 100644 index 0000000..985a68c --- /dev/null +++ b/src/assets/brushes/liner.myb @@ -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 +} diff --git a/src/assets/brushes/pen.myb b/src/assets/brushes/pen.myb new file mode 100644 index 0000000..f695e9d --- /dev/null +++ b/src/assets/brushes/pen.myb @@ -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 +} diff --git a/src/assets/brushes/pencil.myb b/src/assets/brushes/pencil.myb new file mode 100644 index 0000000..f8860ca --- /dev/null +++ b/src/assets/brushes/pencil.myb @@ -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 +}