/* The GIMP -- an image manipulation program * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "config.h" #include #include #include "paint-types.h" #include "base/pixel-region.h" #include "base/temp-buf.h" #include "base/tile.h" #include "base/tile-manager.h" #include "paint-funcs/paint-funcs.h" #include "core/gimpdrawable.h" #include "core/gimpimage.h" #include "gimpinkoptions.h" #include "gimpink.h" #include "gimpink-blob.h" #include "gimpink-undo.h" #include "gimp-intl.h" #define SUBSAMPLE 8 /* local function prototypes */ static void gimp_ink_finalize (GObject *object); static void gimp_ink_paint (GimpPaintCore *paint_core, GimpDrawable *drawable, GimpPaintOptions *paint_options, GimpPaintState paint_state, guint32 time); static TempBuf * gimp_ink_get_paint_area (GimpPaintCore *paint_core, GimpDrawable *drawable, GimpPaintOptions *paint_options); static void gimp_ink_motion (GimpPaintCore *paint_core, GimpDrawable *drawable, GimpPaintOptions *paint_options, guint32 time); static Blob * ink_pen_ellipse (GimpInkOptions *options, gdouble x_center, gdouble y_center, gdouble pressure, gdouble xtilt, gdouble ytilt, gdouble velocity); static void time_smoother_add (GimpInk *ink, guint32 value); static guint32 time_smoother_result (GimpInk *ink); static void time_smoother_init (GimpInk *ink, guint32 initval); static void dist_smoother_add (GimpInk *ink, gdouble value); static gdouble dist_smoother_result (GimpInk *ink); static void dist_smoother_init (GimpInk *ink, gdouble initval); static void render_blob (Blob *blob, PixelRegion *dest); G_DEFINE_TYPE (GimpInk, gimp_ink, GIMP_TYPE_PAINT_CORE) #define parent_class gimp_ink_parent_class void gimp_ink_register (Gimp *gimp, GimpPaintRegisterCallback callback) { (* callback) (gimp, GIMP_TYPE_INK, GIMP_TYPE_INK_OPTIONS, "gimp-ink", _("Ink"), "gimp-tool-ink"); } static void gimp_ink_class_init (GimpInkClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GimpPaintCoreClass *paint_core_class = GIMP_PAINT_CORE_CLASS (klass); object_class->finalize = gimp_ink_finalize; paint_core_class->paint = gimp_ink_paint; paint_core_class->get_paint_area = gimp_ink_get_paint_area; paint_core_class->push_undo = gimp_ink_push_undo; } static void gimp_ink_init (GimpInk *ink) { } static void gimp_ink_finalize (GObject *object) { GimpInk *ink = GIMP_INK (object); if (ink->start_blob) { g_free (ink->start_blob); ink->start_blob = NULL; } if (ink->last_blob) { g_free (ink->last_blob); ink->last_blob = NULL; } G_OBJECT_CLASS (parent_class)->finalize (object); } static void gimp_ink_paint (GimpPaintCore *paint_core, GimpDrawable *drawable, GimpPaintOptions *paint_options, GimpPaintState paint_state, guint32 time) { GimpInk *ink = GIMP_INK (paint_core); switch (paint_state) { case GIMP_PAINT_STATE_INIT: if (paint_core->cur_coords.x == paint_core->last_coords.x && paint_core->cur_coords.y == paint_core->last_coords.y) { /* start with new blobs if we're not interpolating */ if (ink->start_blob) { g_free (ink->start_blob); ink->start_blob = NULL; } if (ink->last_blob) { g_free (ink->last_blob); ink->last_blob = NULL; } } else if (ink->last_blob) { /* save the start blob of the line for undo otherwise */ if (ink->start_blob) g_free (ink->start_blob); ink->start_blob = blob_duplicate (ink->last_blob); } break; case GIMP_PAINT_STATE_MOTION: gimp_ink_motion (paint_core, drawable, paint_options, time); break; case GIMP_PAINT_STATE_FINISH: break; } } static TempBuf * gimp_ink_get_paint_area (GimpPaintCore *paint_core, GimpDrawable *drawable, GimpPaintOptions *paint_options) { GimpInk *ink = GIMP_INK (paint_core); gint x, y; gint width, height; gint dwidth, dheight; gint x1, y1, x2, y2; gint bytes; bytes = gimp_drawable_bytes_with_alpha (drawable); blob_bounds (ink->cur_blob, &x, &y, &width, &height); dwidth = gimp_item_width (GIMP_ITEM (drawable)); dheight = gimp_item_height (GIMP_ITEM (drawable)); x1 = CLAMP (x / SUBSAMPLE - 1, 0, dwidth); y1 = CLAMP (y / SUBSAMPLE - 1, 0, dheight); x2 = CLAMP ((x + width) / SUBSAMPLE + 2, 0, dwidth); y2 = CLAMP ((y + height) / SUBSAMPLE + 2, 0, dheight); /* configure the canvas buffer */ if ((x2 - x1) && (y2 - y1)) paint_core->canvas_buf = temp_buf_resize (paint_core->canvas_buf, bytes, x1, y1, (x2 - x1), (y2 - y1)); else return NULL; return paint_core->canvas_buf; } static void gimp_ink_motion (GimpPaintCore *paint_core, GimpDrawable *drawable, GimpPaintOptions *paint_options, guint32 time) { GimpInk *ink = GIMP_INK (paint_core); GimpInkOptions *options = GIMP_INK_OPTIONS (paint_options); GimpContext *context = GIMP_CONTEXT (paint_options); GimpImage *image; Blob *blob_union = NULL; Blob *blob_to_render; TempBuf *area; guchar col[MAX_CHANNELS]; PixelRegion blob_maskPR; image = gimp_item_get_image (GIMP_ITEM (drawable)); if (! ink->last_blob) { ink->last_blob = ink_pen_ellipse (options, paint_core->cur_coords.x, paint_core->cur_coords.y, paint_core->cur_coords.pressure, paint_core->cur_coords.xtilt, paint_core->cur_coords.ytilt, 10.0); if (ink->start_blob) g_free (ink->start_blob); ink->start_blob = blob_duplicate (ink->last_blob); time_smoother_init (ink, time); ink->last_time = time; dist_smoother_init (ink, 0.0); ink->init_velocity = TRUE; blob_to_render = ink->last_blob; } else { Blob *blob; gdouble dist; gdouble velocity; guint32 lasttime = ink->last_time; guint32 thistime; time_smoother_add (ink, time); thistime = ink->last_time = time_smoother_result (ink); /* The time resolution on X-based GDK motion events is bloody * awful, hence the use of the smoothing function. Sadly this * also means that there is always the chance of having an * indeterminite velocity since this event and the previous * several may still appear to issue at the same * instant. -ADM */ if (thistime == lasttime) thistime = lasttime + 1; dist = sqrt ((paint_core->last_coords.x - paint_core->cur_coords.x) * (paint_core->last_coords.x - paint_core->cur_coords.x) + (paint_core->last_coords.y - paint_core->cur_coords.y) * (paint_core->last_coords.y - paint_core->cur_coords.y)); if (ink->init_velocity) { dist_smoother_init (ink, dist); ink->init_velocity = FALSE; } else { dist_smoother_add (ink, dist); dist = dist_smoother_result (ink); } velocity = 10.0 * sqrt ((dist) / (gdouble) (thistime - lasttime)); blob = ink_pen_ellipse (options, paint_core->cur_coords.x, paint_core->cur_coords.y, paint_core->cur_coords.pressure, paint_core->cur_coords.xtilt, paint_core->cur_coords.ytilt, velocity); blob_union = blob_convex_union (ink->last_blob, blob); g_free (ink->last_blob); ink->last_blob = blob; blob_to_render = blob_union; } /* Get the the buffer */ ink->cur_blob = blob_to_render; area = gimp_paint_core_get_paint_area (paint_core, drawable, paint_options); ink->cur_blob = NULL; if (! area) return; gimp_image_get_foreground (image, drawable, context, col); /* set the alpha channel */ col[paint_core->canvas_buf->bytes - 1] = OPAQUE_OPACITY; /* color the pixels */ color_pixels (temp_buf_data (paint_core->canvas_buf), col, area->width * area->height, area->bytes); gimp_paint_core_validate_canvas_tiles (paint_core, paint_core->canvas_buf->x, paint_core->canvas_buf->y, paint_core->canvas_buf->width, paint_core->canvas_buf->height); /* draw the blob directly to the canvas_tiles */ pixel_region_init (&blob_maskPR, paint_core->canvas_tiles, paint_core->canvas_buf->x, paint_core->canvas_buf->y, paint_core->canvas_buf->width, paint_core->canvas_buf->height, TRUE); render_blob (blob_to_render, &blob_maskPR); /* draw the canvas_buf using the just rendered canvas_tiles as mask */ pixel_region_init (&blob_maskPR, paint_core->canvas_tiles, paint_core->canvas_buf->x, paint_core->canvas_buf->y, paint_core->canvas_buf->width, paint_core->canvas_buf->height, FALSE); gimp_paint_core_paste (paint_core, &blob_maskPR, drawable, GIMP_OPACITY_OPAQUE, gimp_context_get_opacity (context), gimp_context_get_paint_mode (context), GIMP_PAINT_CONSTANT); if (blob_union) g_free (blob_union); } static Blob * ink_pen_ellipse (GimpInkOptions *options, gdouble x_center, gdouble y_center, gdouble pressure, gdouble xtilt, gdouble ytilt, gdouble velocity) { BlobFunc blob_function; gdouble size; gdouble tsin, tcos; gdouble aspect, radmin; gdouble x,y; gdouble tscale; gdouble tscale_c; gdouble tscale_s; /* Adjust the size depending on pressure. */ size = options->size * (1.0 + options->size_sensitivity * (2.0 * pressure - 1.0)); /* Adjust the size further depending on pointer velocity and * velocity-sensitivity. These 'magic constants' are 'feels * natural' tigert-approved. --ADM */ if (velocity < 3.0) velocity = 3.0; #ifdef VERBOSE g_printerr ("%g (%g) -> ", size, velocity); #endif size = (options->vel_sensitivity * ((4.5 * size) / (1.0 + options->vel_sensitivity * (2.0 * velocity))) + (1.0 - options->vel_sensitivity) * size); #ifdef VERBOSE g_printerr ("%g\n", (gfloat) size); #endif /* Clamp resulting size to sane limits */ if (size > options->size * (1.0 + options->size_sensitivity)) size = options->size * (1.0 + options->size_sensitivity); if (size * SUBSAMPLE < 1.0) size = 1.0 / SUBSAMPLE; /* Add brush angle/aspect to tilt vectorially */ /* I'm not happy with the way the brush widget info is combined with * tilt info from the brush. My personal feeling is that * representing both as affine transforms would make the most * sense. -RLL */ tscale = options->tilt_sensitivity * 10.0; tscale_c = tscale * cos (gimp_deg_to_rad (options->tilt_angle)); tscale_s = tscale * sin (gimp_deg_to_rad (options->tilt_angle)); x = (options->blob_aspect * cos (options->blob_angle) + xtilt * tscale_c - ytilt * tscale_s); y = (options->blob_aspect * sin (options->blob_angle) + ytilt * tscale_c + xtilt * tscale_s); #ifdef VERBOSE g_printerr ("angle %g aspect %g; %g %g; %g %g\n", options->blob_angle, options->blob_aspect, tscale_c, tscale_s, x, y); #endif aspect = sqrt (x * x + y * y); if (aspect != 0) { tcos = x / aspect; tsin = y / aspect; } else { tsin = sin (options->blob_angle); tcos = cos (options->blob_angle); } aspect = CLAMP (aspect, 1.0, 10.0); radmin = MAX (1.0, SUBSAMPLE * size / aspect); switch (options->blob_type) { case GIMP_INK_BLOB_TYPE_ELLIPSE: blob_function = blob_ellipse; break; case GIMP_INK_BLOB_TYPE_SQUARE: blob_function = blob_square; break; case GIMP_INK_BLOB_TYPE_DIAMOND: blob_function = blob_diamond; break; default: g_return_val_if_reached (NULL); break; } return (* blob_function) (x_center * SUBSAMPLE, y_center * SUBSAMPLE, radmin * aspect * tcos, radmin * aspect * tsin, -radmin * tsin, radmin * tcos); } static void time_smoother_init (GimpInk *ink, guint32 initval) { gint i; ink->ts_index = 0; for (i = 0; i < TIME_SMOOTHER_BUFFER; i++) ink->ts_buffer[i] = initval; } static guint32 time_smoother_result (GimpInk *ink) { gint i; guint64 result = 0; for (i = 0; i < TIME_SMOOTHER_BUFFER; i++) result += ink->ts_buffer[i]; return (result / (guint64) TIME_SMOOTHER_BUFFER); } static void time_smoother_add (GimpInk *ink, guint32 value) { guint64 long_value = (guint64) value; /* handle wrap-around of time values */ if (long_value < ink->ts_buffer[ink->ts_index]) long_value += (guint64) + G_MAXUINT32; ink->ts_buffer[ink->ts_index++] = long_value; ink->ts_buffer[ink->ts_index++] = value; if (ink->ts_index == TIME_SMOOTHER_BUFFER) ink->ts_index = 0; } static void dist_smoother_init (GimpInk *ink, gdouble initval) { gint i; ink->dt_index = 0; for (i = 0; i < DIST_SMOOTHER_BUFFER; i++) ink->dt_buffer[i] = initval; } static gdouble dist_smoother_result (GimpInk *ink) { gint i; gdouble result = 0.0; for (i = 0; i < DIST_SMOOTHER_BUFFER; i++) result += ink->dt_buffer[i]; return (result / (gdouble) DIST_SMOOTHER_BUFFER); } static void dist_smoother_add (GimpInk *ink, gdouble value) { ink->dt_buffer[ink->dt_index++] = value; if (ink->dt_index == DIST_SMOOTHER_BUFFER) ink->dt_index = 0; } /*********************************/ /* Rendering functions */ /*********************************/ /* Some of this stuff should probably be combined with the * code it was copied from in paint_core.c; but I wanted * to learn this stuff, so I've kept it simple. * * The following only supports CONSTANT mode. Incremental * would, I think, interact strangely with the way we * do things. But it wouldn't be hard to implement at all. */ enum { ROW_START, ROW_STOP }; /* The insertion sort here, for SUBSAMPLE = 8, tends to beat out * qsort() by 4x with CFLAGS=-O2, 2x with CFLAGS=-g */ static void insert_sort (gint *data, gint n) { gint i, j, k; gint tmp1, tmp2; for (i = 2; i < 2 * n; i += 2) { tmp1 = data[i]; tmp2 = data[i + 1]; j = 0; while (data[j] < tmp1) j += 2; for (k = i; k > j; k -= 2) { data[k] = data[k - 2]; data[k + 1] = data[k - 1]; } data[j] = tmp1; data[j + 1] = tmp2; } } static void fill_run (guchar *dest, guchar alpha, gint w) { if (alpha == 255) { memset (dest, 255, w); } else { while (w--) { *dest = MAX (*dest, alpha); dest++; } } } static void render_blob_line (Blob *blob, guchar *dest, gint x, gint y, gint width) { gint buf[4 * SUBSAMPLE]; gint *data = buf; gint n = 0; gint i, j; gint current = 0; /* number of filled rows at this point * in the scan line */ gint last_x; /* Sort start and ends for all lines */ j = y * SUBSAMPLE - blob->y; for (i = 0; i < SUBSAMPLE; i++) { if (j >= blob->height) break; if ((j > 0) && (blob->data[j].left <= blob->data[j].right)) { data[2 * n] = blob->data[j].left; data[2 * n + 1] = ROW_START; data[2 * SUBSAMPLE + 2 * n] = blob->data[j].right; data[2 * SUBSAMPLE + 2 * n + 1] = ROW_STOP; n++; } j++; } /* If we have less than SUBSAMPLE rows, compress */ if (n < SUBSAMPLE) { for (i = 0; i < 2 * n; i++) data[2 * n + i] = data[2 * SUBSAMPLE + i]; } /* Now count start and end separately */ n *= 2; insert_sort (data, n); /* Discard portions outside of tile */ while ((n > 0) && (data[0] < SUBSAMPLE*x)) { if (data[1] == ROW_START) current++; else current--; data += 2; n--; } while ((n > 0) && (data[2*(n-1)] >= SUBSAMPLE*(x+width))) n--; /* Render the row */ last_x = 0; for (i = 0; i < n;) { gint cur_x = data[2 * i] / SUBSAMPLE - x; gint pixel; /* Fill in portion leading up to this pixel */ if (current && cur_x != last_x) fill_run (dest + last_x, (255 * current) / SUBSAMPLE, cur_x - last_x); /* Compute the value for this pixel */ pixel = current * SUBSAMPLE; while (ih; s = dest->data; for (i=0; ix, dest->y + i, dest->w); s += dest->rowstride; } } }