Lightning

Header image for Lightning

Draws a customizable bolt of lightning, and leaves the new layers intact so that you can tweak them afterwards.

14 minute reading time of 2938 words (inc code) Code available at github.com

Intro

class LightningPath(object):
    def __init__(self, image, start, end, depth=1):
        self.image = image
        self.start, self.end = start, end
        self.depth = max(1, depth)

        # NOTE length is the as-the-crow-flies distance AND NOT the length of the actual path/beziers
        self.length = int(self.end.distance_from(self.start))
        self.angle = self.end.angle_of(self.start)
        
        # minimum possible starting point for branches
        self.branch_point = self.length - (self.length / (self.depth+1))

        self.path = []

        noise_defaults = [
            (8, 2.5, 0.40, 0.4, 200, self.length/2),
            (8, 2.0, 0.55, 0.2, 200, self.length)
        ]
        pick = min(self.depth-1, 1)
        self.octaves, self.lacunarity, self.gain, self.scale, self.amplitude, self.hgrid = noise_defaults[pick]

        self.brush_size = 0
    pass

    ''' adds two paths together; does NOT check that they are continuous, in the same image, nor of the same depth '''
    def __add__(self, other):
        obj = LightningPath(self.image, self.start, other.end, self.depth)
        obj.length = self.length + other.length
        obj.path = self.path + other.path
        return obj
    pass

    ''' because random.choice(path) only returns the Point object of a path and I want the index too '''
    def random_point_along_path(self):
        index = random.randint(0, len(self.path)-1)
        return index, self.path[index]
    pass

    ''' brush details for this depth '''
    def set_brush(self):
        if self.brush_size == 0:
            self.brush_size = int(self.image.width/150)
        if self.depth > 1:
            self.brush_size = max(1, int(self.brush_size / self.depth))

        pdb.gimp_context_set_paint_method("gimp-paintbrush")	# make sure we have the right tool selected in this context else gimp errors when you have a non-painting tool selected
        pdb.gimp_context_set_brush("1. Pixel")
        pdb.gimp_context_set_brush_size(self.brush_size)
        pdb.gimp_context_set_brush_hardness(0.75)
    pass

    ''' makes a noisy wave along a straight line '''
    def make_path(self):
        pn = PerlinNoise(self.octaves, 0.1, self.scale)
        for i in xrange(self.length):
            n = int(normalize(pn.fractal(i, self.hgrid, self.lacunarity, self.gain)) * self.amplitude) - self.amplitude/2
            self.path.append(Point(i,n))
    pass
            
    ''' rotates that wave to follow our real start->end line '''
    def rotate_path(self):
        for index, item in enumerate(self.path):
            # rotate x,y around the origin - https://en.wikipedia.org/wiki/Rotation_matrix
            x = int(item.x * math.cos(self.angle) - item.y * math.sin(self.angle))
            y = int(item.x * math.sin(self.angle) + item.y * math.cos(self.angle))
            # translate x,y according to the offset
            self.path[index] = Point(x + self.start.x, y + self.start.y)
    pass

    ''' trebles each of the path coords so gimp can draw the curve '''
    def make_beziers(self):
        self.beziers = []
        for i in self.path:
            # add this to our curve
            self.beziers.extend([i.x,i.y, i.x,i.y, i.x,i.y])
    pass

    ''' plot the bezier curve as a gimp path '''
    def draw_beziers_path(self):
        vectors = pdb.gimp_vectors_new(self.image, "Lightning Path")
        pdb.gimp_image_insert_vectors(self.image, vectors, None, 0)
        if len(self.beziers) > 6:
            pdb.gimp_vectors_stroke_new_from_points(vectors, VECTORS_STROKE_TYPE_BEZIER, len(self.beziers), self.beziers, False)	
        return vectors
    pass

    ''' and draw the bezier curve - solid color, no messing around with gradient overlays etc '''
    def draw_beziers(self, group):
        layer = pdb.gimp_layer_new(self.image, self.image.width, self.image.height, RGBA_IMAGE, "Main Bolt", 100.00, LAYER_MODE_NORMAL)
        pdb.gimp_image_insert_layer(self.image, layer, group, 0)
        self.set_brush()
        self.make_beziers()
        vectors = self.draw_beziers_path()
        pdb.gimp_drawable_edit_stroke_item(layer, vectors)
        pdb.gimp_image_remove_vectors(self.image, vectors)
        return layer
    pass

    def draw_beziers_lighting(self, group):
        pdb.gimp_progress_set_text("Drawing main bolt (lighting) ...")
        pdb.gimp_image_undo_freeze(self.image)

        layerc = self.draw_beziers(group)

        layer1 = pdb.gimp_layer_copy(layerc, True)
        layer2 = pdb.gimp_layer_copy(layerc, True)
        layer3 = pdb.gimp_layer_copy(layerc, True)
        layer4 = pdb.gimp_layer_copy(layerc, True)
        pdb.gimp_image_remove_layer(self.image, layerc)

        pdb.gimp_image_insert_layer(self.image, layer4, group, 1)
        pdb.gimp_image_insert_layer(self.image, layer3, group, 1)
        pdb.gimp_image_insert_layer(self.image, layer2, group, 1)
        pdb.gimp_image_insert_layer(self.image, layer1, group, 1)
        
        pdb.plug_in_gauss(self.image, layer1, 10,10, 0)
        pdb.plug_in_gauss(self.image, layer2, 20,20, 0)
        pdb.plug_in_gauss(self.image, layer3, 35,35, 0)
        pdb.plug_in_gauss(self.image, layer4, 70,70, 0)
        
        pdb.gimp_drawable_brightness_contrast(layer2, 0.3, 0.3)
        pdb.gimp_drawable_brightness_contrast(layer3, -0.2, 0)
        pdb.gimp_drawable_brightness_contrast(layer4, -0.3, 0)

        layer1.mode = LAYER_MODE_HARDLIGHT
        layer = pdb.gimp_image_merge_down(self.image, layer1, CLIP_TO_IMAGE)
        layer = pdb.gimp_image_merge_down(self.image, layer, CLIP_TO_IMAGE)
        layer = pdb.gimp_image_merge_down(self.image, layer, CLIP_TO_IMAGE)
        layer.name = "Main Bolt (Underlay)"
        
        # take our now single lighting layer and do some more blurry things to it

        layer2 = pdb.gimp_layer_copy(layer, True)
        layer3 = pdb.gimp_layer_copy(layer, True)

        pdb.gimp_image_insert_layer(self.image, layer2, group, 2)
        pdb.gimp_image_insert_layer(self.image, layer3, group, 3)
        
        pdb.plug_in_mblur(self.image, layer2, 2, 100, 90, self.start.x, self.start.y)
        pdb.plug_in_mblur(self.image, layer3, 2, 250, 90, self.start.x, self.start.y)
        
        pdb.plug_in_whirl_pinch(self.image, layer2, 0.0, -1.0, 1.2)
        pdb.plug_in_whirl_pinch(self.image, layer3, 0.0, -1.0, 1.2)
        
        pdb.gimp_drawable_brightness_contrast(layer2, -0.5, -0.1)
        pdb.gimp_drawable_brightness_contrast(layer2, -0.3, 0)
        pdb.gimp_drawable_brightness_contrast(layer3, -0.5, 0)
        pdb.gimp_drawable_brightness_contrast(layer3, -0.5, 0)
        pdb.gimp_drawable_brightness_contrast(layer3, -0.2, 0) # yes, you need to repeat b/c adjustments if you want to change a layer that much
        
        layer2 = pdb.gimp_image_merge_down(self.image, layer2, CLIP_TO_IMAGE)
        layer2.name = "Main Bolt (Underlay 2)"
        layer2.opacity = 40.0

        pdb.gimp_image_undo_thaw(self.image)

        return layer
    pass


    ''' paint the plain x,y coords as a gradient pixel by pixel - looks better than a solid stroke but takes longer '''
    def draw_path(self, group):
        pdb.gimp_progress_set_text("Drawing child strokes ...")
        pdb.gimp_image_undo_freeze(self.image)

        layer = pdb.gimp_layer_new(self.image, self.image.width, self.image.height, RGBA_IMAGE, "Side Bolt", 100.00, LAYER_MODE_NORMAL)
        pdb.gimp_image_insert_layer(self.image, layer, group, 0)
        self.set_brush()

        for index, item in enumerate(self.path):
            percent = float(index)/float(self.length)
            pdb.gimp_context_set_opacity((1 - percent) * 100)
            pdb.gimp_paintbrush(layer, 0, 2, (item.x,item.y), PAINT_CONSTANT, 0)
            #pdb.gimp_paintbrush(layer, 0, 2, (item.x,item.y), PAINT_CONSTANT, 0)
            #pdb.gimp_paintbrush(layer, 0, 2, (item.x,item.y), PAINT_CONSTANT, 0)
            pdb.gimp_progress_update(percent)

        pdb.gimp_image_undo_thaw(self.image)

        return layer
    pass
def WeatherLightningWrapper(args):
    
    image, layer, args = args


    pdb.gimp_progress_init("Drawing lighting ...", None)

    #random.seed(seed)
    width = image.width
    height = image.height

    start = Point(width * args['start_x']/100, height * args['start_y']/100)
    mid = Point(width * args['mid_x']/100, height * args['mid_y']/100)
    end = Point(width * args['end_x']/100, height * args['end_y']/100)

    n_main = max(1, min(10, int(args['n_main']))) # between 1-10 main bolts
    n_side = max(0, min(20, int(args['n_side']))) # and 0-20 side bolts


    side_points = []

    group = gimp.GroupLayer(image)
    group.name = "Lightning"
    pdb.gimp_image_insert_layer(image, group, None, 0)

    for i in range(n_main):
        pdb.gimp_progress_set_text("Drawing main bolt(s) ...")
        path1 = LightningPath(image, start, mid)
        path1.make_path()
        path1.rotate_path()
        
        if args['move_end_point_a_bit']:
            end.move_point(width/20, height/20) # 5% wiggle room

        path2 = LightningPath(image, mid, end)
        path2.make_path()
        path2.rotate_path()
        
        main_path = path1 + path2

        size_mod = random.choice([120,150,180,200])
        main_path.brush_size = int(image.width/size_mod)

        pdb.gimp_context_set_foreground(args['color_bolt'])
        layer1 = main_path.draw_beziers(group)

        pdb.gimp_context_set_foreground(args['color_lighting'])
        layer2 = main_path.draw_beziers_lighting(group)

        # need to pick points/angles here from each bolt for the side ones
        for i in range(n_side):
            j, side_start = main_path.random_point_along_path()
            side_next = main_path.path[j+1]

            angle = side_next.angle_of(side_start)
            distance = random.randint(int(main_path.length/15), int(main_path.length/3))

            #debug.write("{}: {} {} {}\n".format(i, main_path.angle, angle, distance))

            side_end = side_start.make_point(distance, angle)
            side_end.check_bounds(width, height)

            #debug.write("{}: {} to {} @ {} {}\n".format(i, side_start, side_end, distance, angle))
            side_points.append((side_start, side_end))



    pdb.gimp_context_set_foreground(args['color_bolt']) # reset after doing the main bolt lighting

    random.shuffle(side_points)

    for i in range(0, n_side): # still only picking n_side items from a list n_main*n_side long 

        start, end = side_points[i]
        
        side_path = LightningPath(image, start, end, 2)
        side_path.make_path()
        side_path.rotate_path()

        layer1 = side_path.draw_path(group)
        
        layer2 = pdb.gimp_layer_copy(layer1, True)
        layer2.mode = LAYER_MODE_HARDLIGHT
        pdb.gimp_image_insert_layer(image, layer2, None, 1)
        pdb.plug_in_gauss(image, layer2, 10,10, 0)
        pdb.gimp_brightness_contrast(layer2, 0, 123)

Reply via email