PIKApp/app/paint/pikaink.c

833 lines
25 KiB
C

/* PIKA - Photo and Image Kooker Application
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
*
* Original copyright, applying to most contents (license remains unchanged):
* 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 3 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, see <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <string.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>
#include "libpikamath/pikamath.h"
#include "paint-types.h"
#include "operations/layer-modes/pika-layer-modes.h"
#include "gegl/pika-gegl-utils.h"
#include "core/pika-palettes.h"
#include "core/pikadrawable.h"
#include "core/pikaimage.h"
#include "core/pikaimage-undo.h"
#include "core/pikapickable.h"
#include "core/pikasymmetry.h"
#include "core/pikatempbuf.h"
#include "pikainkoptions.h"
#include "pikaink.h"
#include "pikaink-blob.h"
#include "pikainkundo.h"
#include "pika-intl.h"
#define SUBSAMPLE 8
/* local function prototypes */
static void pika_ink_finalize (GObject *object);
static void pika_ink_paint (PikaPaintCore *paint_core,
GList *drawables,
PikaPaintOptions *paint_options,
PikaSymmetry *sym,
PikaPaintState paint_state,
guint32 time);
static GeglBuffer * pika_ink_get_paint_buffer (PikaPaintCore *paint_core,
PikaDrawable *drawable,
PikaPaintOptions *paint_options,
PikaLayerMode paint_mode,
const PikaCoords *coords,
gint *paint_buffer_x,
gint *paint_buffer_y,
gint *paint_width,
gint *paint_height);
static PikaUndo * pika_ink_push_undo (PikaPaintCore *core,
PikaImage *image,
const gchar *undo_desc);
static void pika_ink_motion (PikaPaintCore *paint_core,
PikaDrawable *drawable,
PikaPaintOptions *paint_options,
PikaSymmetry *sym,
guint32 time);
static PikaBlob * ink_pen_ellipse (PikaInkOptions *options,
gdouble x_center,
gdouble y_center,
gdouble pressure,
gdouble xtilt,
gdouble ytilt,
gdouble velocity,
const PikaMatrix3 *transform);
static void render_blob (GeglBuffer *buffer,
GeglRectangle *rect,
PikaBlob *blob);
G_DEFINE_TYPE (PikaInk, pika_ink, PIKA_TYPE_PAINT_CORE)
#define parent_class pika_ink_parent_class
void
pika_ink_register (Pika *pika,
PikaPaintRegisterCallback callback)
{
(* callback) (pika,
PIKA_TYPE_INK,
PIKA_TYPE_INK_OPTIONS,
"pika-ink",
_("Ink"),
"pika-tool-ink");
}
static void
pika_ink_class_init (PikaInkClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
PikaPaintCoreClass *paint_core_class = PIKA_PAINT_CORE_CLASS (klass);
object_class->finalize = pika_ink_finalize;
paint_core_class->paint = pika_ink_paint;
paint_core_class->get_paint_buffer = pika_ink_get_paint_buffer;
paint_core_class->push_undo = pika_ink_push_undo;
}
static void
pika_ink_init (PikaInk *ink)
{
}
static void
pika_ink_finalize (GObject *object)
{
PikaInk *ink = PIKA_INK (object);
if (ink->start_blobs)
{
g_list_free_full (ink->start_blobs, g_free);
ink->start_blobs = NULL;
}
if (ink->last_blobs)
{
g_list_free_full (ink->last_blobs, g_free);
ink->last_blobs = NULL;
}
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
pika_ink_paint (PikaPaintCore *paint_core,
GList *drawables,
PikaPaintOptions *paint_options,
PikaSymmetry *sym,
PikaPaintState paint_state,
guint32 time)
{
PikaInk *ink = PIKA_INK (paint_core);
PikaCoords *cur_coords;
PikaCoords last_coords;
g_return_if_fail (g_list_length (drawables) == 1);
pika_paint_core_get_last_coords (paint_core, &last_coords);
cur_coords = pika_symmetry_get_origin (sym);
switch (paint_state)
{
case PIKA_PAINT_STATE_INIT:
{
PikaContext *context = PIKA_CONTEXT (paint_options);
PikaRGB foreground;
pika_symmetry_set_stateful (sym, TRUE);
pika_context_get_foreground (context, &foreground);
pika_palettes_add_color_history (context->pika,
&foreground);
if (cur_coords->x == last_coords.x &&
cur_coords->y == last_coords.y)
{
if (ink->start_blobs)
{
g_list_free_full (ink->start_blobs, g_free);
ink->start_blobs = NULL;
}
if (ink->last_blobs)
{
g_list_free_full (ink->last_blobs, g_free);
ink->last_blobs = NULL;
}
}
else if (ink->last_blobs)
{
PikaBlob *last_blob;
GList *iter;
gint i;
if (ink->start_blobs)
{
g_list_free_full (ink->start_blobs, g_free);
ink->start_blobs = NULL;
}
/* save the start blobs of each stroke for undo otherwise */
for (iter = ink->last_blobs, i = 0; iter; iter = g_list_next (iter), i++)
{
last_blob = g_list_nth_data (ink->last_blobs, i);
ink->start_blobs = g_list_prepend (ink->start_blobs,
pika_blob_duplicate (last_blob));
}
ink->start_blobs = g_list_reverse (ink->start_blobs);
}
}
break;
case PIKA_PAINT_STATE_MOTION:
for (GList *iter = drawables; iter; iter = iter->next)
pika_ink_motion (paint_core, iter->data, paint_options, sym, time);
break;
case PIKA_PAINT_STATE_FINISH:
pika_symmetry_set_stateful (sym, FALSE);
break;
}
}
static GeglBuffer *
pika_ink_get_paint_buffer (PikaPaintCore *paint_core,
PikaDrawable *drawable,
PikaPaintOptions *paint_options,
PikaLayerMode paint_mode,
const PikaCoords *coords,
gint *paint_buffer_x,
gint *paint_buffer_y,
gint *paint_width,
gint *paint_height)
{
PikaInk *ink = PIKA_INK (paint_core);
gint x, y;
gint width, height;
gint dwidth, dheight;
gint x1, y1, x2, y2;
gint offset_change_x, offset_change_y;
PikaCoords new_coords;
GList *iter;
pika_blob_bounds (ink->cur_blob, &x, &y, &width, &height);
x1 = x / SUBSAMPLE - 1;
y1 = y / SUBSAMPLE - 1;
x2 = (x + width) / SUBSAMPLE + 2;
y2 = (y + height) / SUBSAMPLE + 2;
pika_paint_core_expand_drawable (paint_core, drawable, paint_options,
x1, x2, y1, y2,
&offset_change_x, &offset_change_y);
dwidth = pika_item_get_width (PIKA_ITEM (drawable));
dheight = pika_item_get_height (PIKA_ITEM (drawable));
if (offset_change_x || offset_change_y)
{
x += SUBSAMPLE * offset_change_x;
y += SUBSAMPLE * offset_change_y;
new_coords = *coords;
new_coords.x = coords->x + offset_change_x;
new_coords.y = coords->y + offset_change_y;
pika_symmetry_set_origin (paint_core->sym, drawable, &new_coords);
for (iter = ink->blobs_to_render; iter; iter = g_list_next (iter))
pika_blob_move (iter->data,
SUBSAMPLE * offset_change_x,
SUBSAMPLE * offset_change_y);
for (iter = ink->last_blobs; iter; iter = g_list_next (iter))
pika_blob_move (iter->data,
SUBSAMPLE * offset_change_x,
SUBSAMPLE * offset_change_y);
}
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);
if (paint_width)
*paint_width = width / SUBSAMPLE + 3;
if (paint_height)
*paint_height = height / SUBSAMPLE + 3;
/* configure the canvas buffer */
if ((x2 - x1) && (y2 - y1))
{
PikaTempBuf *temp_buf;
const Babl *format;
PikaLayerCompositeMode composite_mode;
composite_mode = pika_layer_mode_get_paint_composite_mode (paint_mode);
format = pika_layer_mode_get_format (paint_mode,
PIKA_LAYER_COLOR_SPACE_AUTO,
PIKA_LAYER_COLOR_SPACE_AUTO,
composite_mode,
pika_drawable_get_format (drawable));
temp_buf = pika_temp_buf_new ((x2 - x1), (y2 - y1),
format);
*paint_buffer_x = x1;
*paint_buffer_y = y1;
if (paint_core->paint_buffer)
g_object_unref (paint_core->paint_buffer);
paint_core->paint_buffer = pika_temp_buf_create_buffer (temp_buf);
pika_temp_buf_unref (temp_buf);
return paint_core->paint_buffer;
}
return NULL;
}
static PikaUndo *
pika_ink_push_undo (PikaPaintCore *core,
PikaImage *image,
const gchar *undo_desc)
{
return pika_image_undo_push (image, PIKA_TYPE_INK_UNDO,
PIKA_UNDO_INK, undo_desc,
0,
"paint-core", core,
NULL);
}
static void
pika_ink_motion (PikaPaintCore *paint_core,
PikaDrawable *drawable,
PikaPaintOptions *paint_options,
PikaSymmetry *sym,
guint32 time)
{
PikaInk *ink = PIKA_INK (paint_core);
PikaInkOptions *options = PIKA_INK_OPTIONS (paint_options);
PikaContext *context = PIKA_CONTEXT (paint_options);
GList *blobs_to_render = NULL;
GeglBuffer *paint_buffer;
gint paint_buffer_x;
gint paint_buffer_y;
PikaLayerMode paint_mode;
PikaRGB foreground;
GeglColor *color;
PikaBlob *last_blob;
PikaCoords coords;
gint off_x, off_y;
gint n_strokes;
gint i;
pika_item_get_offset (PIKA_ITEM (drawable), &off_x, &off_y);
coords = *(pika_symmetry_get_origin (sym));
coords.x -= off_x;
coords.y -= off_y;
pika_symmetry_set_origin (sym, drawable, &coords);
paint_core->sym = sym;
n_strokes = pika_symmetry_get_size (sym);
if (ink->last_blobs &&
g_list_length (ink->last_blobs) != n_strokes)
{
g_list_free_full (ink->last_blobs, g_free);
ink->last_blobs = NULL;
}
if (! ink->last_blobs)
{
if (ink->start_blobs)
{
g_list_free_full (ink->start_blobs, g_free);
ink->start_blobs = NULL;
}
for (i = 0; i < n_strokes; i++)
{
PikaMatrix3 transform;
coords = *(pika_symmetry_get_coords (sym, i));
pika_symmetry_get_matrix (sym, i, &transform);
last_blob = ink_pen_ellipse (options,
coords.x,
coords.y,
coords.pressure,
coords.xtilt,
coords.ytilt,
100,
&transform);
ink->last_blobs = g_list_prepend (ink->last_blobs,
last_blob);
ink->start_blobs = g_list_prepend (ink->start_blobs,
pika_blob_duplicate (last_blob));
blobs_to_render = g_list_prepend (blobs_to_render,
pika_blob_duplicate (last_blob));
}
ink->start_blobs = g_list_reverse (ink->start_blobs);
ink->last_blobs = g_list_reverse (ink->last_blobs);
blobs_to_render = g_list_reverse (blobs_to_render);
}
else
{
for (i = 0; i < n_strokes; i++)
{
PikaBlob *blob;
PikaBlob *blob_union = NULL;
PikaMatrix3 transform;
coords = *(pika_symmetry_get_coords (sym, i));
pika_symmetry_get_matrix (sym, i, &transform);
blob = ink_pen_ellipse (options,
coords.x,
coords.y,
coords.pressure,
coords.xtilt,
coords.ytilt,
coords.velocity * 100,
&transform);
last_blob = g_list_nth_data (ink->last_blobs, i);
blob_union = pika_blob_convex_union (last_blob, blob);
g_free (last_blob);
g_list_nth (ink->last_blobs, i)->data = blob;
blobs_to_render = g_list_prepend (blobs_to_render, blob_union);
}
blobs_to_render = g_list_reverse (blobs_to_render);
}
paint_mode = pika_context_get_paint_mode (context);
pika_context_get_foreground (context, &foreground);
pika_pickable_srgb_to_image_color (PIKA_PICKABLE (drawable),
&foreground, &foreground);
color = pika_gegl_color_new (&foreground, pika_drawable_get_space (drawable));
ink->blobs_to_render = blobs_to_render;
for (i = 0; i < n_strokes; i++)
{
PikaBlob *blob_to_render = g_list_nth_data (blobs_to_render, i);
coords = *(pika_symmetry_get_coords (sym, i));
ink->cur_blob = blob_to_render;
paint_buffer = pika_paint_core_get_paint_buffer (paint_core, drawable,
paint_options,
paint_mode,
&coords,
&paint_buffer_x,
&paint_buffer_y,
NULL, NULL);
ink->cur_blob = NULL;
if (! paint_buffer)
continue;
gegl_buffer_set_color (paint_buffer, NULL, color);
/* draw the blob directly to the canvas_buffer */
render_blob (paint_core->canvas_buffer,
GEGL_RECTANGLE (paint_core->paint_buffer_x,
paint_core->paint_buffer_y,
gegl_buffer_get_width (paint_core->paint_buffer),
gegl_buffer_get_height (paint_core->paint_buffer)),
blob_to_render);
/* draw the paint_area using the just rendered canvas_buffer as mask */
pika_paint_core_paste (paint_core,
NULL,
paint_core->paint_buffer_x,
paint_core->paint_buffer_y,
drawable,
PIKA_OPACITY_OPAQUE,
pika_context_get_opacity (context),
paint_mode,
PIKA_PAINT_CONSTANT);
}
g_object_unref (color);
g_list_free_full (blobs_to_render, g_free);
}
static PikaBlob *
ink_pen_ellipse (PikaInkOptions *options,
gdouble x_center,
gdouble y_center,
gdouble pressure,
gdouble xtilt,
gdouble ytilt,
gdouble velocity,
const PikaMatrix3 *transform)
{
PikaBlobFunc 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 (pika_deg_to_rad (options->tilt_angle));
tscale_s = tscale * sin (pika_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 (SQR (x) + SQR (y));
if (aspect != 0)
{
tcos = x / aspect;
tsin = y / aspect;
}
else
{
tcos = cos (options->blob_angle);
tsin = sin (options->blob_angle);
}
pika_matrix3_transform_point (transform,
tcos, tsin,
&tcos, &tsin);
aspect = CLAMP (aspect, 1.0, 10.0);
radmin = MAX (1.0, SUBSAMPLE * size / aspect);
switch (options->blob_type)
{
case PIKA_INK_BLOB_TYPE_CIRCLE:
blob_function = pika_blob_ellipse;
break;
case PIKA_INK_BLOB_TYPE_SQUARE:
blob_function = pika_blob_square;
break;
case PIKA_INK_BLOB_TYPE_DIAMOND:
blob_function = pika_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);
}
/*********************************/
/* 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;
for (i = 2; i < 2 * n; i += 2)
{
gint tmp1 = data[i];
gint 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 (gfloat *dest,
gfloat alpha,
gint w)
{
if (alpha == 1.0)
{
while (w--)
{
*dest = 1.0;
dest++;
}
}
else
{
while (w--)
{
*dest = MAX (*dest, alpha);
dest++;
}
}
}
static void
render_blob_line (PikaBlob *blob,
gfloat *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, (gfloat) current / SUBSAMPLE, cur_x - last_x);
/* Compute the value for this pixel */
pixel = current * SUBSAMPLE;
while (i<n)
{
gint tmp_x = data[2 * i] / SUBSAMPLE;
if (tmp_x - x != cur_x)
break;
if (data[2 * i + 1] == ROW_START)
{
current++;
pixel += ((tmp_x + 1) * SUBSAMPLE) - data[2 * i];
}
else
{
current--;
pixel -= ((tmp_x + 1) * SUBSAMPLE) - data[2 * i];
}
i++;
}
dest[cur_x] = MAX (dest[cur_x], (gfloat) pixel / (SUBSAMPLE * SUBSAMPLE));
last_x = cur_x + 1;
}
if (current != 0)
fill_run (dest + last_x, (gfloat) current / SUBSAMPLE, width - last_x);
}
static void
render_blob (GeglBuffer *buffer,
GeglRectangle *rect,
PikaBlob *blob)
{
GeglBufferIterator *iter;
GeglRectangle *roi;
iter = gegl_buffer_iterator_new (buffer, rect, 0, babl_format ("Y float"),
GEGL_ACCESS_READWRITE, GEGL_ABYSS_NONE, 1);
roi = &iter->items[0].roi;
while (gegl_buffer_iterator_next (iter))
{
gfloat *d = iter->items[0].data;
gint h = roi->height;
gint y;
for (y = 0; y < h; y++, d += roi->width * 1)
{
render_blob_line (blob, d, roi->x, roi->y + y, roi->width);
}
}
}