This script adds looping snow effect

Hi all,

I have created a GIMP python script that creates a looped snow animation on an existing animation:

snow

Lots of things can be customized: the number of frames, the number of flakes, the min to max speed, the min to max wind, the min to max rotation, the min to max flake size, the area of fall and the shape, color and opacity of the flakes.

You can render any type of particles like snowflakes, ash, sparks. If you set a negative fall, the particles will raise. You can apply the effect several times to create different types of particles but you have to save, quit GIMP, restart it and reopen your project.

You first have to create all the frames with the background. Otherwise, it will prompt you to do so.

Here is the Python code:

#!/usr/bin/env python

from gimpfu import *
import random
import gtk

class ModeDialog(gtk.Window):
    def __init__ (self):
        self.w, self.h = 0, 0
        ret =  gtk.Window.__init__(self)
        vbox = gtk.VBox(False, 0)
        self.add(vbox)

        btn = gtk.Button("You are supposed to create all the layers for the animation before. Only one layer found. How many frames you want?")
        btn.connect("clicked", self.disappear)
        vbox.pack_start(btn, False, False, 0)
        btn.show()

        adjustment = gtk.Adjustment(50, 0, 10000, 1, 5, 0)
        self.spinbutton = gtk.SpinButton(adjustment)
        vbox.pack_start(self.spinbutton, False, False, 0)
        self.spinbutton.show()

        closeButton = gtk.Button("close", gtk.STOCK_CLOSE)
        closeButton.connect("clicked", self.disappear)
        vbox.pack_start(closeButton, False, False, 0)
        closeButton.show()

        vbox.show()
        self.show()
        return ret

    def disappear(self, widget) :
        gtk.main_quit()
        return False

def snowflake(image, drawable, flake_number, brush_name, color, opacity, flake_size_1, flake_size_2, flake_fall_1, flake_fall_2, relation, wind_speed_1, wind_speed_2, wind_change_1, wind_change_2, angle_1, angle_2, position_1, position_2):
    # Get the list of layers in the image
    layers = image.layers

    if len(layers) == 1:
        r = ModeDialog()
        gtk.main()
        # Get the top-level layer
        top_layer = image.layers[0]

        # Duplicate the layer x times
        pdb.gimp_item_set_visible(top_layer, True)
        for i in range(r.spinbutton.get_value_as_int() - 1):
            duplicate = pdb.gimp_layer_new_from_drawable(top_layer, image)
            pdb.gimp_image_insert_layer(image, duplicate, None, (i + 1) * 2)

    layers = image.layers

    pdb.gimp_progress_init("Start adding snow...", None)
    pdb.gimp_context_push()

    # Translate parameters
    flake_size_min = min(flake_size_1, flake_size_2)
    flake_size_max = max(flake_size_1, flake_size_2)

    flake_fall_min = min(flake_fall_1, flake_fall_2)
    flake_fall_max = max(flake_fall_1, flake_fall_2)

    wind_speed_min = min(wind_speed_1, wind_speed_2)
    wind_speed_max = max(wind_speed_1, wind_speed_2)

    wind_change_min = min(wind_change_1, wind_change_2)
    wind_change_max = max(wind_change_1, wind_change_2)

    angle_min = min(angle_1, angle_2)
    angle_min = max(angle_min, -180)
    angle_max = max(angle_1, angle_2)
    angle_max = min(angle_max, 180)

    position_1 = min(position_1, image.width)
    position_1 = max(position_1, -image.width)
    position_1 = position_1 % image.width
    position_2 = min(position_2, image.width)
    position_2 = max(position_2, -image.width)
    position_2 = position_2 % image.width

    position_min = min(position_1, position_2)
    position_max = max(position_1, position_2)

    if (position_min == 0) and (position_max == 0):
        position_min = 0
        position_max = image.width

    # Group actions as a whole
    pdb.gimp_image_undo_group_start(image)

    # Select the paint tool
    pdb.gimp_context_set_brush(str(brush_name))
    pdb.gimp_context_set_foreground(color)
    pdb.gimp_context_set_opacity(opacity)

    # Loop through each layer and paint the point
    for i in range(flake_number):
        pdb.gimp_progress_update((i * 1.0) / flake_number)
        pdb.gimp_progress_set_text("Rendering flake #" + str(i + 1) + " over " + str(flake_number) + "...")
        flake_size = max(random.uniform(flake_size_min, flake_size_max), 1)
        flake_fall = random.uniform(flake_fall_min, flake_fall_max)

        # Relate to flake size
        flake_fall = ((flake_fall - flake_fall_min) * (1 - relation) + ((flake_size - flake_size_min) * relation * (flake_fall_max - flake_fall_min) / (flake_size_max - flake_size_min))) + flake_fall_min
        if flake_fall == 0:
            flake_fall = 1

        wind_speed = random.uniform(wind_speed_min, wind_speed_max)
        wind_change = -1
        angle = random.uniform(angle_min, angle_max)

        # A flake can fall from any frame
        j = random.randint(0, len(layers) - 1)

        # Flake coodinates
        x = position_min + (flake_size / 2 + (((i + 0.5) * ((position_max - position_min) + flake_size)) / flake_number))
        if 0 < flake_fall:
            y = 0 - flake_size / 2
        else:
            y = image.height + flake_size / 2

        # Set the brush size
        pdb.gimp_context_set_brush_size(flake_size)
        pdb.gimp_context_set_brush_angle(angle)

        # We paint a flake till the flake is out of scope
        while ((0 < flake_fall) and (y < image.height + flake_size / 2)) or ((flake_fall <= 0) and (0 - flake_size / 2 < y)):
            if wind_change < 0:
                wind_change = random.randint(wind_change_min, wind_change_max)
                new_wind_speed = random.uniform(wind_speed_min, wind_speed_max)

                # The wind should not change too quickly
                new_wind_speed = min(new_wind_speed, wind_speed + 1)
                new_wind_speed = max(new_wind_speed, wind_speed - 1)

                wind_speed = new_wind_speed
            else:
                wind_change -= 1
            layer = layers[j]

            # Make the layer active
            pdb.gimp_image_set_active_layer(image, layer)

            # Paint the point
            pdb.gimp_paintbrush_default(layer, 2, [x, y])

            new_angle = pdb.gimp_context_get_brush_angle() + angle
            # More than 360 degrees is useless (and forbidden)
            new_angle = ((new_angle + 180) % 360) - 180
            pdb.gimp_context_set_brush_angle(new_angle)

            # Flake new coodinates
            x += wind_speed
            if x < 0 - flake_size / 2:
                x = image.width + flake_size / 2
            if image.width + flake_size / 2 < x:
                x = 0 - flake_size / 2

            y += flake_fall

            j -= 1
            if j < 0:
                j = len(layers) - 1

    pdb.gimp_context_pop()

    pdb.gimp_image_undo_group_end(image)

    # Update the image display
    gimp.displays_flush()
    pdb.gimp_progress_end()
    pdb.plug_in_animationplay(image, drawable)

# Register the GIMP Python-fu command
register(
    "python_fu_snowflake",
    "Adds a snowflake fall effect on an existing animation",
    "Adds a snowflake fall effect on an existing animation",
    "Fabrice TIERCELIN",
    "Fabrice TIERCELIN",
    "2023",
    "<Image>/Filters/Animation/Snowflake fall on animation",
    "*",
    [
        (PF_INT, "flake_number", "_Number of flakes that appear during the animation", 200),
        (PF_BRUSH, "brush_name", "Flake _brush", None),
        (PF_COLOR, "color", "Flake _color", (255, 255, 255)),
        (PF_SLIDER, "opacity", "Flake _opacity (0=transparent; 100=opaque)", 65, (1, 100, 1)),
        (PF_FLOAT, "flake_size_1", "Flake from this _size (in pixels)", 1.1),
        (PF_FLOAT, "flake_size_2", "...to this si_ze (in pixels)", 10.5),
        (PF_FLOAT, "fall_per_frame_1", "_Fall from this speed (pixels per frame)", 0.5),
        (PF_FLOAT, "fall_per_frame_2", "..._to this speed (pixels per frame)", 10.5),
        (PF_SLIDER, "relation", "_Relation between size and fall (0=not related; 1=analogous)", 0.85, (0, 1, 0.01)),
        (PF_FLOAT, "wind_speed_1", "_Wind from this force (in pixels, negative for left, positive for right)", -2.5),
        (PF_FLOAT, "wind_speed_2", "...to t_his force (in pixels, negative for left, positive for right)", 2.5),
        (PF_INT, "wind_change_1", "Win_d change from this time (frame number)", 1),
        (PF_INT, "wind_change_2", "...to th_is time (frame number)", 10),
        (PF_FLOAT, "angle_1", "Rotation from this _angle (in degrees)", -20.5),
        (PF_FLOAT, "angle_2", "...to this an_gle (in degrees)", 20.5),
        (PF_FLOAT, "position_1", "Falling from this _position (in pixels)", 0),
        (PF_FLOAT, "position_2", "...to this position (in pixels)", 0),
    ],
    [],
    snowflake)

main()

…or download this file:
snowflake.py.txt (8.1 KB)

  1. Rename the file snowflake.py
  2. Put it on your C:\Users\YourName\AppData\Roaming\GIMP\2.10\plug-ins folder
  3. Start GIMP
  4. Open an image
  5. Create as many frames as necessary for your animation
  6. Go on Filter β†’ Animation β†’ Snowflake fall on an animation
  7. Launch the script

The animation is done.

Successfully tested on GIMP 2.10.10 on Windows 10.

1 Like