// Called only from stroke_to(). Calculate everything needed to // draw the dab, then let the surface do the actual drawing. // // This is only gets called right after update_states_and_setting_values(). // Returns true if the surface was modified. bool prepare_and_draw_dab (Surface * surface) { float x, y, opaque; float radius; // ensure we don't get a positive result with two negative opaque values if (settings_value[BRUSH_OPAQUE] < 0) settings_value[BRUSH_OPAQUE] = 0; opaque = settings_value[BRUSH_OPAQUE] * settings_value[BRUSH_OPAQUE_MULTIPLY]; opaque = CLAMP(opaque, 0.0, 1.0); //if (opaque == 0.0) return false; <-- cannot do that, since we need to update smudge state. if (settings_value[BRUSH_OPAQUE_LINEARIZE]) { // OPTIMIZE: no need to recalculate this for each dab float alpha, beta, alpha_dab, beta_dab; float dabs_per_pixel; // dabs_per_pixel is just estimated roughly, I didn't think hard // about the case when the radius changes during the stroke dabs_per_pixel = ( settings[BRUSH_DABS_PER_ACTUAL_RADIUS]->base_value + settings[BRUSH_DABS_PER_BASIC_RADIUS]->base_value ) * 2.0; // the correction is probably not wanted if the dabs don't overlap if (dabs_per_pixel < 1.0) dabs_per_pixel = 1.0; // interpret the user-setting smoothly dabs_per_pixel = 1.0 + settings[BRUSH_OPAQUE_LINEARIZE]->base_value*(dabs_per_pixel-1.0); // see doc/brushdab_saturation.png // beta = beta_dab^dabs_per_pixel // <==> beta_dab = beta^(1/dabs_per_pixel) alpha = opaque; beta = 1.0-alpha; beta_dab = powf(beta, 1.0/dabs_per_pixel); alpha_dab = 1.0-beta_dab; opaque = alpha_dab; } x = states[STATE_ACTUAL_X]; y = states[STATE_ACTUAL_Y]; float base_radius = expf(settings[BRUSH_RADIUS_LOGARITHMIC]->base_value); if (settings_value[BRUSH_OFFSET_BY_SPEED]) { x += states[STATE_NORM_DX_SLOW] * settings_value[BRUSH_OFFSET_BY_SPEED] * 0.1 * base_radius; y += states[STATE_NORM_DY_SLOW] * settings_value[BRUSH_OFFSET_BY_SPEED] * 0.1 * base_radius; } if (settings_value[BRUSH_OFFSET_BY_RANDOM]) { float amp = settings_value[BRUSH_OFFSET_BY_RANDOM]; if (amp < 0.0) amp = 0.0; x += rand_gauss (rng) * amp * base_radius; y += rand_gauss (rng) * amp * base_radius; } radius = states[STATE_ACTUAL_RADIUS]; if (settings_value[BRUSH_RADIUS_BY_RANDOM]) { float radius_log, alpha_correction; // go back to logarithmic radius to add the noise radius_log = settings_value[BRUSH_RADIUS_LOGARITHMIC]; radius_log += rand_gauss (rng) * settings_value[BRUSH_RADIUS_BY_RANDOM]; radius = expf(radius_log); radius = CLAMP(radius, ACTUAL_RADIUS_MIN, ACTUAL_RADIUS_MAX); alpha_correction = states[STATE_ACTUAL_RADIUS] / radius; alpha_correction = SQR(alpha_correction); if (alpha_correction <= 1.0) { opaque *= alpha_correction; } } // color part float color_h = settings[BRUSH_COLOR_H]->base_value; float color_s = settings[BRUSH_COLOR_S]->base_value; float color_v = settings[BRUSH_COLOR_V]->base_value; float eraser_target_alpha = 1.0; if (settings_value[BRUSH_SMUDGE] > 0.0) { // mix (in RGB) the smudge color with the brush color hsv_to_rgb_float (&color_h, &color_s, &color_v); float fac = settings_value[BRUSH_SMUDGE]; if (fac > 1.0) fac = 1.0; // If the smudge color somewhat transparent, then the resulting // dab will do erasing towards that transparency level. // see also ../doc/smudge_math.png eraser_target_alpha = (1-fac)*1.0 + fac*states[STATE_SMUDGE_A]; // fix rounding errors (they really seem to happen in the previous line) eraser_target_alpha = CLAMP(eraser_target_alpha, 0.0, 1.0); if (eraser_target_alpha > 0) { color_h = (fac*states[STATE_SMUDGE_RA] + (1-fac)*color_h) / eraser_target_alpha; color_s = (fac*states[STATE_SMUDGE_GA] + (1-fac)*color_s) / eraser_target_alpha; color_v = (fac*states[STATE_SMUDGE_BA] + (1-fac)*color_v) / eraser_target_alpha; } else { // we are only erasing; the color does not matter color_h = 1.0; color_s = 0.0; color_v = 0.0; } rgb_to_hsv_float (&color_h, &color_s, &color_v); } if (settings_value[BRUSH_SMUDGE_LENGTH] < 1.0 and // optimization, since normal brushes have smudge_length == 0.5 without actually smudging (settings_value[BRUSH_SMUDGE] != 0.0 or not settings[BRUSH_SMUDGE]->is_constant())) { float fac = settings_value[BRUSH_SMUDGE_LENGTH]; if (fac < 0.01) fac = 0.01; int px, py; px = ROUND(x); py = ROUND(y); // Calling get_color() is almost as expensive as rendering a // dab. Because of this we use the previous value if it is not // expected to hurt quality too much. We call it at most every // second dab. float r, g, b, a; states[STATE_LAST_GETCOLOR_RECENTNESS] *= fac; if (states[STATE_LAST_GETCOLOR_RECENTNESS] < 0.5*fac) { states[STATE_LAST_GETCOLOR_RECENTNESS] = 1.0; float smudge_radius = radius * expf(settings_value[BRUSH_SMUDGE_RADIUS_LOG]); smudge_radius = CLAMP(smudge_radius, ACTUAL_RADIUS_MIN, ACTUAL_RADIUS_MAX); surface->get_color (px, py, smudge_radius, &r, &g, &b, &a); states[STATE_LAST_GETCOLOR_R] = r; states[STATE_LAST_GETCOLOR_G] = g; states[STATE_LAST_GETCOLOR_B] = b; states[STATE_LAST_GETCOLOR_A] = a; } else { r = states[STATE_LAST_GETCOLOR_R]; g = states[STATE_LAST_GETCOLOR_G]; b = states[STATE_LAST_GETCOLOR_B]; a = states[STATE_LAST_GETCOLOR_A]; } // updated the smudge color (stored with premultiplied alpha) states[STATE_SMUDGE_A ] = fac*states[STATE_SMUDGE_A ] + (1-fac)*a; // fix rounding errors states[STATE_SMUDGE_A ] = CLAMP(states[STATE_SMUDGE_A], 0.0, 1.0); states[STATE_SMUDGE_RA] = fac*states[STATE_SMUDGE_RA] + (1-fac)*r*a; states[STATE_SMUDGE_GA] = fac*states[STATE_SMUDGE_GA] + (1-fac)*g*a; states[STATE_SMUDGE_BA] = fac*states[STATE_SMUDGE_BA] + (1-fac)*b*a; } // eraser if (settings_value[BRUSH_ERASER]) { eraser_target_alpha *= (1.0-settings_value[BRUSH_ERASER]); } // HSV color change color_h += settings_value[BRUSH_CHANGE_COLOR_H]; color_s += settings_value[BRUSH_CHANGE_COLOR_HSV_S]; color_v += settings_value[BRUSH_CHANGE_COLOR_V]; // HSL color change if (settings_value[BRUSH_CHANGE_COLOR_L] || settings_value[BRUSH_CHANGE_COLOR_HSL_S]) { // (calculating way too much here, can be optimized if neccessary) // this function will CLAMP the inputs hsv_to_rgb_float (&color_h, &color_s, &color_v); rgb_to_hsl_float (&color_h, &color_s, &color_v); color_v += settings_value[BRUSH_CHANGE_COLOR_L]; color_s += settings_value[BRUSH_CHANGE_COLOR_HSL_S]; hsl_to_rgb_float (&color_h, &color_s, &color_v); rgb_to_hsv_float (&color_h, &color_s, &color_v); } float hardness = CLAMP(settings_value[BRUSH_HARDNESS], 0.0, 1.0); // anti-aliasing attempt (works surprisingly well for ink brushes) float current_fadeout_in_pixels = radius * (1.0 - hardness); float min_fadeout_in_pixels = settings_value[BRUSH_ANTI_ALIASING]; if (current_fadeout_in_pixels < min_fadeout_in_pixels) { // need to soften the brush (decrease hardness), but keep optical radius // so we tune both radius and hardness, to get the desired fadeout_in_pixels float current_optical_radius = radius - (1.0-hardness)*radius/2.0; // Equation 1: (new fadeout must be equal to min_fadeout) // min_fadeout_in_pixels = radius_new*(1.0 - hardness_new) // Equation 2: (optical radius must remain unchanged) // current_optical_radius = radius_new - (1.0-hardness_new)*radius_new/2.0 // // Solved Equation 1 for hardness_new, using Equation 2: (thanks to mathomatic) float hardness_new = ((current_optical_radius - (min_fadeout_in_pixels/2.0))/(current_optical_radius + (min_fadeout_in_pixels/2.0))); // Using Equation 1: float radius_new = (min_fadeout_in_pixels/(1.0 - hardness_new)); hardness = hardness_new; radius = radius_new; } // the functions below will CLAMP most inputs hsv_to_rgb_float (&color_h, &color_s, &color_v); return surface->draw_dab (x, y, radius, color_h, color_s, color_v, opaque, hardness, eraser_target_alpha, states[STATE_ACTUAL_ELLIPTICAL_DAB_RATIO], states[STATE_ACTUAL_ELLIPTICAL_DAB_ANGLE], settings_value[BRUSH_LOCK_ALPHA]); }
// Called only from stroke_to(). Calculate everything needed to // draw the dab, then let the surface do the actual drawing. // // This is only gets called right after update_states_and_setting_values(). // Returns true if the surface was modified. bool prepare_and_draw_dab (Surface * surface) { float x, y, opaque; float radius; opaque = settings_value[BRUSH_OPAQUE] * settings_value[BRUSH_OPAQUE_MULTIPLY]; if (opaque >= 1.0) opaque = 1.0; if (opaque <= 0.0) opaque = 0.0; //if (opaque == 0.0) return false; <-- cannot do that, since we need to update smudge state. if (settings_value[BRUSH_OPAQUE_LINEARIZE]) { // OPTIMIZE: no need to recalculate this for each dab float alpha, beta, alpha_dab, beta_dab; float dabs_per_pixel; // dabs_per_pixel is just estimated roughly, I didn't think hard // about the case when the radius changes during the stroke dabs_per_pixel = ( settings[BRUSH_DABS_PER_ACTUAL_RADIUS]->base_value + settings[BRUSH_DABS_PER_BASIC_RADIUS]->base_value ) * 2.0; // the correction is probably not wanted if the dabs don't overlap if (dabs_per_pixel < 1.0) dabs_per_pixel = 1.0; // interpret the user-setting smoothly dabs_per_pixel = 1.0 + settings[BRUSH_OPAQUE_LINEARIZE]->base_value*(dabs_per_pixel-1.0); // see doc/brushdab_saturation.png // beta = beta_dab^dabs_per_pixel // <==> beta_dab = beta^(1/dabs_per_pixel) alpha = opaque; beta = 1.0-alpha; beta_dab = powf(beta, 1.0/dabs_per_pixel); alpha_dab = 1.0-beta_dab; opaque = alpha_dab; } x = states[STATE_ACTUAL_X]; y = states[STATE_ACTUAL_Y]; float base_radius = expf(settings[BRUSH_RADIUS_LOGARITHMIC]->base_value); if (settings_value[BRUSH_OFFSET_BY_SPEED]) { x += states[STATE_NORM_DX_SLOW] * settings_value[BRUSH_OFFSET_BY_SPEED] * 0.1 * base_radius; y += states[STATE_NORM_DY_SLOW] * settings_value[BRUSH_OFFSET_BY_SPEED] * 0.1 * base_radius; } if (settings_value[BRUSH_OFFSET_BY_RANDOM]) { x += rand_gauss (rng) * settings_value[BRUSH_OFFSET_BY_RANDOM] * base_radius; y += rand_gauss (rng) * settings_value[BRUSH_OFFSET_BY_RANDOM] * base_radius; } radius = states[STATE_ACTUAL_RADIUS]; if (settings_value[BRUSH_RADIUS_BY_RANDOM]) { float radius_log, alpha_correction; // go back to logarithmic radius to add the noise radius_log = settings_value[BRUSH_RADIUS_LOGARITHMIC]; radius_log += rand_gauss (rng) * settings_value[BRUSH_RADIUS_BY_RANDOM]; radius = expf(radius_log); if (radius < ACTUAL_RADIUS_MIN) radius = ACTUAL_RADIUS_MIN; if (radius > ACTUAL_RADIUS_MAX) radius = ACTUAL_RADIUS_MAX; alpha_correction = states[STATE_ACTUAL_RADIUS] / radius; alpha_correction = SQR(alpha_correction); if (alpha_correction <= 1.0) { opaque *= alpha_correction; } } // color part float color_h = settings[BRUSH_COLOR_H]->base_value; float color_s = settings[BRUSH_COLOR_S]->base_value; float color_v = settings[BRUSH_COLOR_V]->base_value; float eraser_target_alpha = 1.0; if (settings_value[BRUSH_SMUDGE] > 0.0) { // mix (in RGB) the smudge color with the brush color hsv_to_rgb_float (&color_h, &color_s, &color_v); float fac = settings_value[BRUSH_SMUDGE]; if (fac > 1.0) fac = 1.0; // If the smudge color somewhat transparent, then the resulting // dab will do erasing towards that transparency level. // see also ../doc/smudge_math.png eraser_target_alpha = (1-fac)*1.0 + fac*states[STATE_SMUDGE_A]; // fix rounding errors (they really seem to happen in the previous line) eraser_target_alpha = CLAMP(eraser_target_alpha, 0.0, 1.0); if (eraser_target_alpha > 0) { color_h = (fac*states[STATE_SMUDGE_RA] + (1-fac)*color_h) / eraser_target_alpha; color_s = (fac*states[STATE_SMUDGE_GA] + (1-fac)*color_s) / eraser_target_alpha; color_v = (fac*states[STATE_SMUDGE_BA] + (1-fac)*color_v) / eraser_target_alpha; } else { // we are only erasing; the color does not matter color_h = 1.0; color_s = 0.0; color_v = 0.0; } rgb_to_hsv_float (&color_h, &color_s, &color_v); } if (settings_value[BRUSH_SMUDGE_LENGTH] < 1.0 && // optimization, since normal brushes have smudge_length == 0.5 without actually smudging (settings_value[BRUSH_SMUDGE] != 0.0 || !settings[BRUSH_SMUDGE]->is_constant())) { float fac = settings_value[BRUSH_SMUDGE_LENGTH]; if (fac < 0.0) fac = 0; int px, py; px = ROUND(x); py = ROUND(y); float r, g, b, a; surface->get_color (px, py, radius, &r, &g, &b, &a); // updated the smudge color (stored with premultiplied alpha) states[STATE_SMUDGE_A ] = fac*states[STATE_SMUDGE_A ] + (1-fac)*a; // fix rounding errors states[STATE_SMUDGE_A ] = CLAMP(states[STATE_SMUDGE_A], 0.0, 1.0); states[STATE_SMUDGE_RA] = fac*states[STATE_SMUDGE_RA] + (1-fac)*r*a; states[STATE_SMUDGE_GA] = fac*states[STATE_SMUDGE_GA] + (1-fac)*g*a; states[STATE_SMUDGE_BA] = fac*states[STATE_SMUDGE_BA] + (1-fac)*b*a; } // eraser if (settings_value[BRUSH_ERASER]) { eraser_target_alpha *= (1.0-settings_value[BRUSH_ERASER]); } // HSV color change color_h += settings_value[BRUSH_CHANGE_COLOR_H]; color_s += settings_value[BRUSH_CHANGE_COLOR_HSV_S]; color_v += settings_value[BRUSH_CHANGE_COLOR_V]; // HSL color change if (settings_value[BRUSH_CHANGE_COLOR_L] || settings_value[BRUSH_CHANGE_COLOR_HSL_S]) { // (calculating way too much here, can be optimized if neccessary) // this function will CLAMP the inputs hsv_to_rgb_float (&color_h, &color_s, &color_v); rgb_to_hsl_float (&color_h, &color_s, &color_v); color_v += settings_value[BRUSH_CHANGE_COLOR_L]; color_s += settings_value[BRUSH_CHANGE_COLOR_HSL_S]; hsl_to_rgb_float (&color_h, &color_s, &color_v); rgb_to_hsv_float (&color_h, &color_s, &color_v); } float hardness = settings_value[BRUSH_HARDNESS]; // the functions below will CLAMP most inputs hsv_to_rgb_float (&color_h, &color_s, &color_v); return surface->draw_dab (x, y, radius, color_h, color_s, color_v, opaque, hardness, eraser_target_alpha, states[STATE_ACTUAL_ELLIPTICAL_DAB_RATIO], states[STATE_ACTUAL_ELLIPTICAL_DAB_ANGLE]); }