Below is one called paint_based_on_paths.py.
#!/usr/bin/env python
# coding=utf-8
# -*- encoding: utf-8 -*-
# paint_based_on_paths.py Rel 7
# Created by Tin Tran
# Comments directed to http://gimplearn.net
# Creates new image, paints random strokes that has the same angle as closest points on paths
# defined.
#
# License: GPLv3
# 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.
#
# To view a copy of the GNU General Public License
# visit: http://www.gnu.org/licenses/gpl.html
#
#
# ------------
#| Change Log |
# ------------
# Rel 1: Initial release.
# Rel 2: Just paintbrush instead of trying to do fill selection should be faster.
# Rel 3: Slightly Faster calculations
# Rel 4: Changed to work with GIMP 2.10.6
# Rel 5: Try and continue on error.
# Rel 6: Give brush-strokes random 30 degrees of freedom to look more natural.
# Rel 7: Added random angle option for blotches painting (when it doesn't matter what direction the brush is).
# Rel 8: Changed colors to be slightly randomly off to look like paint rather than exact color
# Rel 9: Changed brush incremental size at a slower rate to not be out of bounds for Van Gogh brush.
# Rel 10: Instead of changing random colors, we change shade/tints of colors
# ======================== AUTOCOMPLETE HELP STARTS =============================
# ========================= while in atom editor can safely be removed
# try:
# #Try to set a variable to pdb, if it works then we're inside GIMP execution
# test = pdb
# except:
# #If it fails we're on our own so we'll use this for autocomplete
# try:
# import mypdb as pdb #import this file so that we bet pdb.* autocomplete
# from myenums import * #import this file so that we have ENUMS autocomplete
# except:
# pass
# ======================= AUTOCOMPLETE HELP ENDS ================================
import random
from gimpfu import *
import math
#Copied this from ofn-text-along-path.py returns angle in radians
def computeOrientedSlope(dx,dy,slope): #This is a better version than below computeThetaWithSlope function.
if abs(slope) > 100000: # very vertical
return math.atan2(dy,dx) # no perfect, but properly oriented
# keep dx/dy signs but give then the same ratio as in slope
return math.atan2(math.copysign(slope,dy),math.copysign(1,dx))
#just returns distance square as we don't care to get distance but just degree to compare them
def distance(p0, p1):
return (p0[0] - p1[0])*(p0[0] - p1[0]) + (p0[1] - p1[1])*(p0[1] - p1[1])
#reads points and angles at each point into points array
def read_points_angles(image,layer,spacing,offset_angle):
points = []
if len(image.vectors) < 1:
pdb.gimp_message("You must define a path/vector")
raise Exception('You must define a path/vector')
vectors = image.vectors[0]
strokes = vectors.strokes
for stroke in strokes: #loop through all strokes
stroke_length = stroke.get_length(0.01) - 1.1 #subtract 1.1 so we can get the point at +1 and its' still valid
sections = int(stroke_length/spacing)
for i in range(0,sections+1): #loop from 0 to sections
dist1 = i * spacing
dist2 = dist1 + 1 #1
x1,y1,slope1,valid1 = stroke.get_point_at_dist(dist1,0.01)
x2,y2,slope2,valid2 = stroke.get_point_at_dist(dist2,0.01)
rad_angle = computeOrientedSlope(x2-x1, y2-y1, slope1)
angle = math.degrees(rad_angle)
angle = (angle + offset_angle + random.randrange(0,30) - 15) % 360
if (angle > 180): #make it fit between -180 and 180
angle = -180 + (angle-180)
points.append([[x1,y1],angle])
#pdb.gimp_message(str(points[len(points)-1]))
return points
#main method
def python_paint_based_on_paths_tt(image, layer, brush, brush_size, brush_spacing, offset_angle, random_angle):
draw_angle = 0.0;
#pdb.gimp_image_undo_group_start(image)
pdb.gimp_context_push()
if random_angle == 0:
points = read_points_angles(image,layer,brush_spacing,offset_angle)
else:
points = []
draw_points = []
y_sects = int(layer.height/brush_spacing)
x_sects = int(layer.width/brush_spacing)
for iy in range(0,y_sects+1):
for ix in range(0,x_sects+1):
x = ix * brush_spacing
y = iy * brush_spacing
draw_points.append([x,y])
#sort it random order so that it's shuffled good
draw_points.sort(key=lambda x:random.randrange(0,layer.width*layer.height))
pdb.gimp_message("Randomization of draw locations completed.")
#pdb.gimp_message(str(draw_points))
new_image = pdb.gimp_image_new(layer.width, layer.height, RGB)
pdb.gimp_display_new(new_image)
source_layer = pdb.gimp_layer_new_from_drawable(layer,new_image)
pdb.gimp_image_insert_layer(new_image, source_layer, None, 0)
pdb.gimp_layer_set_name(source_layer, "Source")
paint_layer = pdb.gimp_layer_new(new_image, layer.width, layer.height, RGBA_IMAGE, "Paint", 100, NORMAL_MODE)
pdb.gimp_image_insert_layer(new_image, paint_layer, None, 0)
#work_layer = pdb.gimp_layer_new(new_image, layer.width, layer.height, RGBA_IMAGE, "Work", 100, LAYER_MODE_NORMAL)
#pdb.gimp_image_insert_layer(new_image, work_layer, None, 0)
pdb.gimp_context_set_brush(brush)
pdb.gimp_context_set_brush_size(brush_size)
for dp in range(0,len(draw_points)):
draw_point = draw_points[dp]
shortestdist = 10000000
if random_angle == 0:
for p in range(0,len(points)):
point = points[p]
dist = distance(draw_point, point[0])
if dist < shortestdist:
shortestdist = dist
draw_angle = point[1] #grab angle
else:
draw_angle = random.randrange(0,360)-180;
if (shortestdist <= brush_size):
#if it's closer to brushsize, use brushsize
pdb.gimp_context_set_brush_size(brush_size)
else:
#else use shortestdist as brushsize or 3 times brushsize(max)
pdb.gimp_context_set_brush_size(min(brush_size*3,brush_size+(shortestdist-brush_size)/3))
pdb.gimp_context_set_brush_angle(draw_angle)
#pdb.gimp_drawable_edit_clear(work_layer)
#pdb.gimp_paintbrush(work_layer, 0, 2, draw_point, PAINT_CONSTANT, 0)
#pdb.gimp_image_select_item(new_image, CHANNEL_OP_REPLACE, work_layer)
#pdb.gimp_drawable_edit_clear(work_layer)
#non_empty,x1,y2,x2,y2 = pdb.gimp_selection_bounds(new_image)
#if non_empty==TRUE: #if there is a selection
# r, _, _, _, _, _ = pdb.gimp_histogram(source_layer,HISTOGRAM_RED,0,255)
# g, _, _, _, _, _ = pdb.gimp_histogram(source_layer,HISTOGRAM_GREEN,0,255)
# b, _, _, _, _, _ = pdb.gimp_histogram(source_layer,HISTOGRAM_BLUE,0,255)
# pdb.gimp_context_set_foreground((int(r),int(g),int(b)))
if (draw_point[0]+1) >= layer.width:
offsetx = -1
else:
offsetx = 1
if (draw_point[1]+1) >= layer.height:
offsety = -1
else:
offsety = 1
draw_point = [int(draw_point[0]),int(draw_point[1])]
#if 1==1:
try:
pixel1 = source_layer.get_pixel(draw_point[0], draw_point[1])
#pixel2 = source_layer.get_pixel(draw_point[0]+offsetx, draw_point[1])
#pixel3 = source_layer.get_pixel(draw_point[0]+offsetx, draw_point[1]+offsety)
#pixel4 = source_layer.get_pixel(draw_point[0], draw_point[1]+offsety)
#introduce color error here
# color_error = 20
# value_change = random.randrange(0,int(color_error)) - color_error/2.0 #value change
# cG = value_change
# cR = value_change/0.72*0.21 #change according to gray scale percei
# cB = value_change/0.72*0.07
# R = int(pixel1[0] + cR)
# G = int(pixel1[1] + cG)
# B = int(pixel1[2] + cB)
# R = max(0,min(255,R)); G = max(0,min(255,G)); B = max(0,min(255,B));
# pdb.gimp_context_set_foreground((R,G,B))
#=======================================
#gray = 0.21*R + 0.72*G + 0.07*B
# R = int(pixel1[0]*1.0 + random.randrange(0,color_error) - color_error/2.0)
# G = int(pixel1[1]*1.0 + random.randrange(0,color_error) - color_error/2.0)
# B = int(pixel1[2]*1.0 + random.randrange(0,color_error) - color_error/2.0)
# R = max(0,min(255,R)); G = max(0,min(255,G)); B = max(0,min(255,B));
# pdb.gimp_context_set_foreground((R,G,B))
#=======================================
# R = int(pixel1[0])
# G = int(pixel1[1])
# B = int(pixel1[2])
# color_error = 30
# value_change = random.randrange(0,int(color_error)) - color_error/2.0 #value change
# if value_change > 0: #if it's brighter we check how bright we can go
# brightest = max(R,G,B)
# # actual_value_change = min(value_change,255-brightest)
# actual_value_change = value_change;
# R = int(R*1.0 + (actual_value_change/255.0*R))
# G = int(G*1.0 + (actual_value_change/255.0*G))
# B = int(B*1.0 + (actual_value_change/255.0*B))
# elif value_change < 0:
# darkest = min(R,G,B)
# # actual_value_change = max(value_change,-darkest)
# actual_value_change = value_change;
# R = int(R*1.0 + (actual_value_change/255.0*R))
# G = int(G*1.0 + (actual_value_change/255.0*G))
# B = int(B*1.0 + (actual_value_change/255.0*B))
# R = max(0,min(255,R)); G = max(0,min(255,G)); B = max(0,min(255,B));
# pdb.gimp_context_set_foreground((R,G,B))
# ================ Try different tints/shades
R = int(pixel1[0])
G = int(pixel1[1])
B = int(pixel1[2])
color_error = 12 #percent
value_change = random.randrange(0,int(color_error)*10)/10.0 - color_error/2.0 #value change
if value_change < 0: #darker shade
R = int(R*(1.0+value_change/100.0)) #-10 will make it multiplied by .90 or 90%
G = int(G*(1.0+value_change/100.0))
B = int(B*(1.0+value_change/100.0))
elif value_change > 0:
R = int(R+(255.0-R)*(value_change/100.0)); #10 will make it 10 percent of distance to 255
G = int(G+(255.0-G)*(value_change/100.0)); #10 will make it 10 percent of distance to 255
B = int(B+(255.0-B)*(value_change/100.0)); #10 will make it 10 percent of distance to 255
pdb.gimp_context_set_foreground((R,G,B))
#pdb.gimp_context_set_foreground(((pixel1[0]+pixel2[0]+pixel3[0]+pixel4[0])/4,(pixel1[1]+pixel2[1]+pixel3[1]+pixel4[1])/4,(pixel1[2]+pixel2[2]+pixel3[2]+pixel4[2])/4))
#pdb.gimp_paintbrush(paint_layer, 0, 2, draw_point, PAINT_CONSTANT, 0)
pdb.gimp_paintbrush_default(paint_layer, 2, draw_point)
#pdb.gimp_message(str(draw_point))
except Exception, e:
pdb.gimp_message("Errored:" + str(e) + "...continuing")
pass
#pdb.gimp_edit_fill(paint_layer,FOREGROUND_FILL)
#pdb.gimp_selection_none(new_image) #select none before we draw.
#pdb.gimp_displays_flush()
if dp % 100 == 0:
pdb.gimp_displays_flush()
pdb.gimp_context_pop()
#pdb.gimp_image_undo_group_end(image)
pdb.gimp_displays_flush()
pdb.gimp_message("Complete!")
#return
register(
"python_fu_paint_based_on_paths_tt",
"Creates new image, paints random strokes with angle same as closest path points",
"Creates new image, paints random strokes with angle same as closest path points",
"Tin Tran",
"Tin Tran",
"September 2018",
"<Image>/Python-Fu/Paint Based on Paths...", #Menu path
"RGB*, GRAY*",
[
(PF_BRUSH, "brush", "Brush:", 0),
(PF_SPINNER, "brush_size", "Brush-size:", 50, (1, 1000, 1)),
(PF_SPINNER, "brush_spacing", "Brush-spacing:", 50, (1, 1000, 1)),
(PF_SPINNER, "offset_angle", "Offset angle (angle to offset from path's angles):", 0, (0,360, 1)),
(PF_TOGGLE, "random_angle", "Use random angle (when doing generally round blotches):", 0),
#(PF_SPINNER, "px_height", "Height of each tile image:", 100, (1, 1000, 50)),
],
[],
python_paint_based_on_paths_tt)
main()