Amazing Main Menu Animations Tutorial

A place for Ren'Py tutorials and reusable Ren'Py code.
Forum rules
Do not post questions here!

This forum is for example code you want to show other people. Ren'Py questions should be asked in the Ren'Py Questions and Announcements forum.
Post Reply
Message
Author
User avatar
jeffster
Eileen-Class Veteran
Posts: 1225
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Amazing Main Menu Animations Tutorial

#1 Post by jeffster »

If you are wondering what kinds of cool animated effects you can use in your Main Menu (and in any Ren'Py screen, actually), here's a few examples with step-by-step explanations.

1. Animate brightness, contrast, color, transparency
2. Images moving, zooming and rotation
3. Sprite Manager and Movie Clips
4. Animated Shaders & Conclusion (with downloadable complete game)
5. (bonus) BlockDrop ("crumbling") effect

1. Animate brightness, contrast, color, transparency

(I won't talk much about transparency, as changing it is as simple as

Code: Select all

transform alpha_half:
    alpha 0.5
See an example:
https://renpy.org/doc/html/gui.html#scr ... o-continue
)

When you want an element to smoothly change brightness, contrast or color, `matrixcolor` transforms will do that for you:
https://renpy.org/doc/html/matrixcolor.html

(In particular, there are several matrices for color, e.g.:

Hue
Invert
Saturation
Sepia
Tint
)

Here's an example:

"Shimmering" Effect

Code: Select all

transform shimmer:
    matrixcolor SaturationMatrix(.25) * BrightnessMatrix(-.2)
    block:
        easein 1.5 matrixcolor SaturationMatrix(1.) * BrightnessMatrix(0.)
        0.5
        easeout 1.5 matrixcolor SaturationMatrix(0.25) * BrightnessMatrix(-.2)
        0.5
        repeat
Here we set the initial parameters:

SaturationMatrix(.25) means that the image will have only 25% of its original color.

BrightnessMatrix(-.2) means it will have 20% less brightness.

Note that different matrices have different behavior:

BrightnessMatrix gets the parameter between -1.0 and 1.0 (with normal brightness at 0).
SaturationMatrix: between 0.0 and 1.0 (1.0 is normal, full original saturation).
ContrastMatrix: from 0.0 and below 1.0 it decreases contrast, and above 1.0 it increases contrast.

Here we change saturation and brightness in a loop, using warpers
https://renpy.org/doc/html/transforms.html#warpers

"easein" and "easeout", each for 1.5 seconds, with a pause 0.5 sec.


In a screen, apply this transform to a picture

Code: Select all

screen some_screen():
    add "some_picture":
        at shimmer
and voila:
shimmer.gif
shimmer.gif (117.45 KiB) Viewed 15031 times
This effect can be used to draw attention to a pulsating element (like an urgent notification, or a rare edible bonus in a minigame, etc.), to depict heartbeat, or siren flashes, etc. You can vary all the parameters: timing, changes in brightness & saturation etc.

For now, this might not seem like very much, but we only just started. Look at another, more impressive example, where we apply brightness and saturation effects to the whole scene -

"Lights Out" Effect
lights_out.gif
lights_out.gif (1.96 MiB) Viewed 15031 times
Now I'll explain step by step how to use this effect in Main Menu.

Usually we have a background image for Main Menu. In this example, I'll use a file I created and put in "gui" folder, named "main_menu.webp". So it will be available as
gui/main_menu.webp

To make it darker, we can use for example this transform:

Code: Select all

transform night_fall:
    matrixcolor SaturationMatrix(1.) * BrightnessMatrix(0.)
    ease .67 matrixcolor SaturationMatrix(0.4) * BrightnessMatrix(-.2)
which means originally we show it at full saturation & brightness, then during 0.67 sec we reduce saturation to 40% and decrease brightness by 20%.

When the picture becomes both darker and less saturated, it really looks like the night is coming.

Apart from that, we can make the bottom part of the picture even darker (because it's very often that the top of the room gets a bit more light - through the window etc.).

In that case, we can supplement our transform with "gradient" shader, to transition smoothly from the lighter top to the darker bottom. I'll just take the shader from the Ren'Py documentation:
https://renpy.org/doc/html/model.html#c ... tom-shader

and change it to be "top to bottom" rather than "left to right":

Code: Select all

init -1 python:

    renpy.register_shader("gradient.vertical", variables="""
        uniform vec4 u_gradient_top;
        uniform vec4 u_gradient_bottom;
        uniform vec2 u_model_size;
        varying float v_gradient_done;
        attribute vec4 a_position;
    """, vertex_300="""
        v_gradient_done = a_position.y / u_model_size.y;
    """, fragment_300="""
        float gradient_done = v_gradient_done;
        gl_FragColor *= mix(u_gradient_top, u_gradient_bottom, gradient_done);
    """)
Let's combine our transform "night_fall" with this shader, "gradient.vertical". I will call the new transform "dark_bottom":

Code: Select all

transform dark_bottom:
    matrixcolor SaturationMatrix(1.)
    shader "gradient.vertical"
    u_gradient_top (1.0, 1.0, 1.0, 1.0)
    u_gradient_bottom (1.0, 1.0, 1.0, 1.0)
    ease .67:
        matrixcolor SaturationMatrix(0.4)
        u_gradient_top (0.8, 0.8, 0.8, 1.0)
        u_gradient_bottom (0.3, 0.3, 0.3, 1.0)
Explanation:

Here we start with the normal, unmodified picture: saturation 1. and "gradient" that has the white color all the way from top to bottom, hence not modifying the picture at all.

(Four color channels: Red, Green, Blue and Alpha all have values 1.0 - meaning it's fully opaque white color:
u_gradient_top (1.0, 1.0, 1.0, 1.0)
u_gradient_bottom (1.0, 1.0, 1.0, 1.0)
)

Then we have "ease" transition during 0.67 sec, that changes saturation to 40%, top lighting to 80% and bottom lighting to 30%:

Code: Select all

    ease .67:
        matrixcolor SaturationMatrix(0.4)
        u_gradient_top (0.8, 0.8, 0.8, 1.0)
        u_gradient_bottom (0.3, 0.3, 0.3, 1.0)
(Alpha channel remains at 1.0, because we don't need to add transparency),

OK... that's good enough, but to make the effect really standing out, we can also make the window look brighter.

Yes, you might have noticed in the picture above that it's not just an optical illusion: the window is actually getting brighter, when the room gets darker.

How to do that?.. We use 2 pictures there. One is the window, another is the rest of the room. We can add the window "on top" of the room picture, or we can cut out a piece of the room picture (creating a transparent area there) and show the window image behind that transparent area. This technique can be used also for showing a movie through the transparent part, etc. As the window frame can have a complicated shape, with semi-transparent pixels at the edges etc., maybe plants at the window sill etc., I prefer to put the window picture behind the room picture, through a transparent area (but YMMV).

Now, for the window picture we can use a transform like this:

Code: Select all

transform brighter:
    matrixcolor ContrastMatrix(1.) * BrightnessMatrix(0.)
    ease .67 matrixcolor ContrastMatrix(1.2) * BrightnessMatrix(0.2)
meaning we smoothly increase the image contrast to 120% and increase brightness by 20%.

Now, how do we actually apply those transforms?

As you can see from the picture above, the effect is applied when we hover the button "Quit". How to change screen elements by hovering a button?

Here's the code I used:

Code: Select all

screen main_menu():
    tag menu
    style_prefix "main_menu"

    default hover_quit = False

    if hover_quit:
        add "gui/window.webp" pos (514, 219) at brighter
        add "gui/main_menu.webp" pos (0, 0) at dark_bottom
    else:
        add "gui/window.webp" pos (514, 219)
        add "gui/main_menu.webp" pos (0, 0)

    if renpy.variant("pc"):
        textbutton "Quit":
            pos (1890, 840)
            xanchor 1.
            action Quit(confirm=False)
            hovered SetScreenVariable("hover_quit", True)
            unhovered SetScreenVariable("hover_quit", False)
Meaning: we define variable "hover_quit" and originally assign it False value. So the pictures will be shown on screen without any transforms.

Then we add to our button properties "hovered" and "unhovered":

Code: Select all

            hovered SetScreenVariable("hover_quit", True)
            unhovered SetScreenVariable("hover_quit", False)
On hover, we set "hover_quit" to True. On unhover, we return it to False.

Now to change an element we just check if "hover_quit" is True, and if so, we show the pictures with the transforms applied.


Here's how you can test this in a freshly created Ren'Py 8 project:

1. Put files (see the attachment) "main_menu.webp" & "window.webp" in "game/gui" folder.

2. Open file "screens.rpy" and replace lines

Code: Select all

screen main_menu():

    ## This ensures that any other menu screen is replaced.
    tag menu

    add gui.main_menu_background
with these lines:

Code: Select all

init -1 python:

    renpy.register_shader("gradient.vertical", variables="""
        uniform vec4 u_gradient_top;
        uniform vec4 u_gradient_bottom;
        uniform vec2 u_model_size;
        varying float v_gradient_done;
        attribute vec4 a_position;
    """, vertex_300="""
        v_gradient_done = a_position.y / u_model_size.y;
    """, fragment_300="""
        float gradient_done = v_gradient_done;
        gl_FragColor *= mix(u_gradient_top, u_gradient_bottom, gradient_done);
    """)

transform dark_bottom:
    matrixcolor SaturationMatrix(1.)
    shader "gradient.vertical"
    u_gradient_top (1.0, 1.0, 1.0, 1.0)
    u_gradient_bottom (1.0, 1.0, 1.0, 1.0)
    ease .67:
        matrixcolor SaturationMatrix(0.4)
        u_gradient_top (0.8, 0.8, 0.8, 1.0)
        u_gradient_bottom (0.3, 0.3, 0.3, 1.0)

transform brighter:
    matrixcolor ContrastMatrix(1.) * BrightnessMatrix(0.)
    ease .67 matrixcolor ContrastMatrix(1.2) * BrightnessMatrix(0.2)

screen main_menu():
    tag menu
    style_prefix "main_menu"

    default hover_quit = False

    if hover_quit:
        add "gui/window.webp" pos (514, 219) at brighter
        add "gui/main_menu.webp" pos (0, 0) at dark_bottom
    else:
        add "gui/window.webp" pos (514, 219)
        add "gui/main_menu.webp" pos (0, 0)
2. In the same file (in "screen navigation") find the "Quit" button. It's this line:

Code: Select all

            textbutton _("Quit") action Quit(confirm=not main_menu)
Add to that a colon and our "hovered" & "unhovered" properties, so that it looks like:

Code: Select all

            textbutton _("Quit") action Quit(confirm=not main_menu):
                hovered SetScreenVariable("hover_quit", True)
                unhovered SetScreenVariable("hover_quit", False)
3. Run the project, hover "Quit" button and enjoy this really cool effect!

You are welcome to use this stuff for any project, and be inspired to create something even more amazing.

The image files that I used:
Attachments
AMMAT_gui.zip
(528.96 KiB) Downloaded 1794 times
Last edited by jeffster on Sun Apr 20, 2025 2:05 am, edited 5 times in total.

User avatar
jeffster
Eileen-Class Veteran
Posts: 1225
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Amazing Main Menu Animations Tutorial

#2 Post by jeffster »

2. Images moving, zooming and rotation

In this part of the tutorial we'll explore:

"Endless Scrolling"
"Pendulum Effect"
"Re-focusing" (move & zoom)

Moving and zooming displayables we can create dynamic backgrounds, intro animations, slideshow cut scenes... It's a vast ground for creativity and imagination. From the technical point of view, it can be just an "image with ATL block", containing names of image files, timed effects and various transitions.

To position and move images in Ren'Py, we can use many properties: pos, align, anchor, offset, pan, center etc. - and camera movement in 3D scenes, creating parallax effect.
https://renpy.org/doc/html/style_properties.html
https://renpy.org/doc/html/transform_properties.html

A. Panning images (aka "moving your viewport") is similar to shifting them, except that panning beyond an image edge will enter the same picture from the opposite side (try `xpan 180` and/or `ypan 180` to check it out). With panning you can scroll e.g. panoramic images for 180, 360 degrees and more, as their sides join seamlessly. And maybe it's just me, but it seems that panning looks better than just shifting when you turn your field of view against a large background image.

B. align is the easiest way of positioning images in the whole screen or its part. Example:

xalign 0. aligns the image with the left edge of the containing area (regardless of their px sizes).
xalign 1. aligns the image with the right edge.

Moving from xalign 0. to xalign 1. or back means:
  • For small images - they are moved from one edge of the area to another.
  • For images larger than the viewport - they are scrolled from their one edge to another one.
C. anchor is similar to `align`, except that the (x, y) "official coordinate" of the element does not move when you change `anchor`; only anchoring of the element to that coordinate changes.


Let's use this knowledge to get a nice animation effect:

Endless Scrolling
scrolling.gif
scrolling.gif (937.48 KiB) Viewed 14729 times

The code:

Code: Select all

transform scrolling_terminal:
    yanchor 0
    linear 4. yanchor 400
    repeat

# screen ...:

    default hover_about = False

    if hover_about:
        add "gui/monitor.webp":
            pos (802, 444)
            at scrolling_terminal
    else:
        add "gui/monitor_idle.webp":
            pos (787, 435)

# ...

    textbutton "About":
        pos (30, 525)
        action ShowMenu("about")
        hovered SetScreenVariable("hover_about", True)
        unhovered SetScreenVariable("hover_about", False)
Like in the first part of the tutorial, we hover a button to change a variable, and that variable "commands" the screen to display some different content.

Hovering button "About" we set screen variable "hover_about" to True. So instead of the "monitor_idle" picture (which is a blank surface) we show a picture of some text scrolling on a monitor:

Code: Select all

    if hover_about:
        add "gui/monitor.webp":
            at scrolling_terminal
    else:
        add "gui/monitor_idle.webp"
The transform "scrolling_terminal" creates that endless scroll effect. It starts with showing the picture at its top edge
(yanchor 0)

and then shifts it during 4 sec with constant speed -
linear 4. -

by 400 pixels upwards
(because the picture "anchoring" goes 400 px lower in its "body":
yanchor 400).

And then - with "repeat" - we jump back to showing the pic at its top edge.

Here's the picture for scrolling ("monitor.webp"):
monitor.jpg
monitor.jpg (17.66 KiB) Viewed 14729 times

It had 400 px of height of original image, and then I added a piece from the beginning, at 400 px downwards, so that jumping from that position back to 0 would create a seamless illusion of endless scrolling.

(Of course instead of shifting `anchor`, we can shift `pos` or `offset`. This is 100% equivalent:

Code: Select all

transform scrolling_terminal:
    yoffset 0
    linear 4. yoffset -400
    repeat
)

As in the first part of the tutorial, we put the transforming picture behind the transparent hole in the main picture. That way it's simple to show scrolling content in a non-rectangular area.

Endless scrolling is a simple but widely used effect for moving scenery, raining, snowing, flying birds etc.

BTW, to show a large number of the same images, Ren'Py has "sprite manager":
https://renpy.org/doc/html/sprites.html
but we'll explore that in the next part of the Amazing Main Menu Animations Tutorial for Ren'Py.


Pendulum Effect

is another animation, very simple one:
pendulum.gif
pendulum.gif (3.43 MiB) Viewed 14729 times
Code:

Code: Select all

transform rotating_cobweb:
    zoom .5
    yoffset 30
    anchor (.5, .5)
    block:
        ease .33 zoom 1 yoffset 0 rotate 360
        ease .33 rotate 30
        ease .33 rotate 330
        ease .33 rotate 60
        ease .33 rotate 300
        ease .33 rotate 90
        ease .33 rotate 270
        ease .33 rotate 120
        ease .33 rotate 240
        ease .33 rotate 150
        ease .33 rotate 210
        ease .33 rotate 180
        ease .33 rotate 0
        repeat

#...screen ...

    default hover_options = False

    if hover_options:
        add "gui/cobweb.webp":
            pos (1786, 811)
            at rotating_cobweb

#...

    textbutton "Options":
        pos (1905, 525)
        xanchor 1.
        action ShowMenu("preferences")
        hovered SetScreenVariable("hover_options", True)
        unhovered SetScreenVariable("hover_options", False)
We switch it the same way as the previous animations. The only difference is that the transform uses rotations.

(Note that these 2 lines at the start of the transform
zoom .5
yoffset 30
and then
zoom 1 yoffset 0
are a little moving and zooming, having nothing to do with the pendulum animation itself. It's just something I do with that particular picture).


Re-focusing

or amazing move & zoom :wink: . Let's suppose we have Main Menu like this:
mm_idle.jpg
mm_idle.jpg (118.44 KiB) Viewed 14729 times
Below the "Start" button you can see a plasmoid (or whatever that is). And when I hover "Start" button, I want to enlarge that plasmoid and shift this part of the picture to the center of the screen.

It should look like this:
mm_hover.gif
mm_hover.gif (4.69 MiB) Viewed 14729 times
(As you see, the plasmoid itself becomes animated too, but we'll discuss this animation in the next part of the tutorial, "Sprite Manager and Movie Clips").

The principle is simple, just like before: on button hover, setting a variable we substitute some content (a static image) with some other content (a movie displayable and a sprite manager).

The two images that I use here are:
(1) gui/plasmoid_static.webp - a picture of a static plasmoid;
(2) a "movie displayable" is defined as

Code: Select all

image plasmoid = Movie(size=(340, 340), play="video/plasmoid.webm",
                image="gui/plasmoid_static.webp", side_mask=True,
                start_image="gui/plasmoid_static.webp")

When zooming or rotating elements, it's easier to keep them in place if we set anchor to their center:
anchor (0.5, 0.5)
instead of default top-left corner.

That also makes it trivial to calculate image positions when we want to zoom and shift the whole scene. (You could have noticed: when we zoom the main menu background, we need to zoom also the images of the "window" and "monitor" that we see through the holes in the background image. To do that, we calculate start and end positions of those pictures and move & zoom them with the main image).

Code: Select all

    elif hover_start:
        # Window
        add "gui/window.webp" pos (650, 399) at move_zoom1(-790, -41)
        # Not-scrolling monitor
        add "gui/monitor_idle.webp" pos (842, 505) at move_zoom1(-598, 65)
        # Main background
        add "gui/main_menu.webp" pos (960, 540) at move_zoom1(-480, 100)
        # Plasmoid movie
        add "plasmoid" pos (1200, 490) at move_zoom1(-240, 50)
        # 
        add sparks pos (1200, 490) at move_zoom1(-240, 50)
        if gui.show_name:
            frame:
                xysize (1400, 400)
                background None
                align (.5, 1.)
                text "[config.name]":
                    style "main_menu_title"

    elif unhover_start:
        add "gui/window.webp" at move_unzoom(650, 399, -790, -41)
        add "gui/monitor_idle.webp" at move_unzoom(842, 505, -598, 65)
        add "gui/main_menu.webp" at move_unzoom(960, 540, -480, 100)
        add "plasmoid" at move_unzoom(1200, 490, -240, 50)
        add sparks at move_unzoom(1200, 490, -240, 50)
        if gui.show_name:
            frame:
                xysize (1400, 400)
                background None
                align (.5, 1.)
                text "[config.name]":
                    style "main_menu_title"
        timer .7:
            action SetScreenVariable("unhover_start", False)
    else:
        add "gui/window.webp" pos (514, 219)

        if hover_about:
            add "gui/monitor.webp":
                pos (802, 444)
                at scrolling_terminal
        else:
            add "gui/monitor_idle.webp":
                pos (787, 435)

        add "gui/main_menu.webp" pos (0, 0)

        add "gui/plasmoid_static.webp" pos (1030, 320)

        if gui.show_name:
            frame:
                xsize 1400
                background None
                align (.5, 1.)
                text "[config.name]":
                    style "main_menu_title"
                    if hover_help:
                        at waving
                        color "#006A81"
Don't be afraid of this rather large code snippet. We just check if variables `hover_start` and `unhover_start` are set, and show contents depending on them.

The transforms that we are using:

Code: Select all

transform move_zoom1(dx, dy):
    subpixel True
    zoom 1.
    anchor (0.5, 0.5)
    offset (0, 0)
    ease .67:
        zoom 2.
        offset (dx, dy)

transform move_unzoom(endx, endy, dx, dy):
    subpixel True
    zoom 2.
    anchor (0.5, 0.5)
    pos (endx, endy)
    offset (dx, dy)
    ease .67:
        zoom 1.
        offset (0, 0)
Setting the variables like this:

Code: Select all

    textbutton _("Start"):
        pos (1030, 190)
        action Start()
        hovered SetScreenVariable("hover_start", True)
        unhovered [SetScreenVariable("hover_start", False),
                    SetScreenVariable("unhover_start", True)]
        if hover_quit or hover_all:
            at very_forsaken
        elif sum((hover_about, hover_help, hover_load,
                            hover_options, hover_quit)):
            at forsaken
The important part here is that we set animation effects both on hover and on unhover, hence we use two variables: `hover_start` and `unhover_start`.

Other transforms there (`forsaken` and `very_forsaken`) are there for other effects (dim buttons when hovering "Quit" button and the like).

That's it for today, folks!

Enjoy endless scrolling, pendulums and "re-focusing" in your games!
Last edited by jeffster on Thu Apr 03, 2025 7:56 am, edited 4 times in total.

User avatar
jeffster
Eileen-Class Veteran
Posts: 1225
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Amazing Main Menu Animations Tutorial

#3 Post by jeffster »

3. Sprite Manager and Movie Clips


To make the animated "plasmoid" Movie Clip,
globe.gif
globe.gif (543.16 KiB) Viewed 14504 times

I found this "Plasma Globe"
https://www.shadertoy.com/view/XsjXRm
(CC-BY-NC-SA 3.0)

and decided it's easier to record it on video than translating the shader to Ren'Py.

I used free open-source software (OBS Studio)
https://obsproject.com
and recorded a video clip for a few dozen seconds.

Then cropped or scaled it with ffmpeg
https://ffmpeg.org/ffmpeg-filters.html

or possibly with Avidemux.
https://avidemux.org

(I feel more comfortable using these two tools than large "video studio" software, because these are very simple in usage and interface).

(PS) To make the video loop seamless, I created a reversed version of the clip
https://ffmpeg.org/ffmpeg-filters.html#reverse
and concatenated it with the original.
https://ffmpeg.org/ffmpeg-formats.html#concat

To use Movie() displayable with `side_mask` in Ren'Py, I didn't need to extract alpha channel, because the video already had black background. (The closer the color is to black, the more transparency it gives in "side mask").
https://renpy.org/doc/html/movie.html

I used "vp9" though, instead of "vp8" as the codec, because it's more modern and probably better (?). (Don't ask me video-mastering questions, I'm not Andrew Lau!)

As "original video" I set an almost-white picture (and the plasmoid video as the side mask).
https://renpy.org/doc/html/movie.html#Movie

The first frame of the resulting video was used as the static plasmoid image. (How to switch them? See the previous part of this Amazing Main Menu Animations Tutorial for Ren'Py).


To make the plasmoid fancier, I decided to add random sparks generation, employing

Sprite Manager

With the code below, 40 sparks will appear in radius 20 px around the source point, in random positions excluding (0, 0).

They will have random speed, equal to their original position vector.

Their movement will be affected by a little gravity and a little drag, so that slow sparks will be visibly "falling down" a bit.

Sparks will disappear at randomized distances from the source point, between half of the area radius and the area radius.

All these details will make this all kinda vivid.

A spark that "disappeared" will receive random position at the source again, as if a new spark was created.

Code: Select all

define spark = renpy.displayable("gui/spark.png")

define SPARKS_SOURCE_X = config.screen_width / 2    # 960
define SPARKS_SOURCE_Y = config.screen_height / 2   # 540
define SPARKS_SOURCE_RADIUS = 20
define SPARKS_AREA_RADIUS = 100
define SPARKS_AMOUNT = 40
define SPARKS_GRAVITY = 0.03
define SPARKS_DRAG = 0.02

define sparks_list = [None] * SPARKS_AMOUNT
define sparks_speeds = [None] * SPARKS_AMOUNT

init python:
    import math

    def set_pos_and_speed(i):
        """
        Set random position & speed for the sprite number `i`.

        """

        # Set random dx, dy in SPARKS_SOURCE_RADIUS excluding 0

        dx = renpy.random.randint(1, 2 * SPARKS_SOURCE_RADIUS)
        if dx > SPARKS_SOURCE_RADIUS:
           dx = SPARKS_SOURCE_RADIUS - dx

        dy = renpy.random.randint(1, 2 * SPARKS_SOURCE_RADIUS)
        if dy > SPARKS_SOURCE_RADIUS:
           dy = SPARKS_SOURCE_RADIUS - dy

        sparks_list[i].x = SPARKS_SOURCE_X + dx
        sparks_list[i].y = SPARKS_SOURCE_Y + dy

        sparks_speeds[i] = [dx/2, dy/2]


    def sparks_update(st):

        for i in range(0, SPARKS_AMOUNT):

            # The vector between the sprite and the source
            vx = sparks_list[i].x - SPARKS_SOURCE_X
            vy = sparks_list[i].y - SPARKS_SOURCE_Y

            # Get the vector length
            vl = math.hypot(vx, vy)
            if vl > SPARKS_AREA_RADIUS - renpy.random.randint(1, int(
                                                SPARKS_AREA_RADIUS/2)):

                # When a spark left the area, it's reborn from source
                set_pos_and_speed(i)
                continue

            # Move
            sparks_list[i].x = sparks_list[i].x + sparks_speeds[i][0]
            sparks_list[i].y = sparks_list[i].y + sparks_speeds[i][1]
            sparks_speeds[i][0] = sparks_speeds[i][0] * (1 - SPARKS_DRAG)
            sparks_speeds[i][1] = sparks_speeds[i][1] * (1 - SPARKS_DRAG) + SPARKS_GRAVITY

        return .016


label before_main_menu:
    python:
        sparks = SpriteManager(update=sparks_update)

        for i in range(SPARKS_AMOUNT):
            sparks_list[i] = sparks.create(spark)
            set_pos_and_speed(i)
    return
We use SpriteManager
https://renpy.org/doc/html/sprites.html

We initialize it in "label before_main_menu", because we'll use it in Main Menu screen.

We set the original data of a spark in `set_pos_and_speed()` function, and every frame we'll update all sparks' positions and speeds with `sparks_update()` function.

All this is pretty simple and straightforward:
sparks.gif
sparks.gif (126.06 KiB) Viewed 14504 times

Have a good day and be kind to the world!


PS. Oh, to use it in the in-game script, you can do basically the same thing:

Code: Select all

label start:
    show black

    python:
        sparks = SpriteManager(update=sparks_update)

        for i in range(SPARKS_AMOUNT):
            sparks_list[i] = sparks.create(spark)
            set_pos_and_speed(i)

    show expression sparks at truecenter
Last edited by jeffster on Fri Apr 04, 2025 5:22 pm, edited 3 times in total.

User avatar
Donmai
Eileen-Class Veteran
Posts: 1974
Joined: Sun Jun 10, 2012 1:45 am
Completed: Toire No Hanako, Li'l Red [NaNoRenO 2013], The One in LOVE [NaNoRenO 2014], Running Blade [NaNoRenO 2016], The Other Question, To The Girl With Sunflowers
Projects: Slumberland
Location: Brazil
Contact:

Re: Amazing Main Menu Animations Tutorial

#4 Post by Donmai »

Awesome! Bookmarked.
Image
No, sorry! You must be mistaking me for someone else.
TOIRE NO HANAKO (A Story About Fear)

User avatar
jeffster
Eileen-Class Veteran
Posts: 1225
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Amazing Main Menu Animations Tutorial

#5 Post by jeffster »

Enjoy the second part,
Images moving, zooming and rotation

I didn't put all the files as an attachment this time, because I'm planning to do that when I finish all this tutorial.

Edit: The third part is out:
Sprite Manager and Movie Clips

I thought to do another part on animated shaders, but realized that they deserve their own tutorial. See
Simple Shaders Tutorial

Happy coding!

User avatar
jeffster
Eileen-Class Veteran
Posts: 1225
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Amazing Main Menu Animations Tutorial

#6 Post by jeffster »

4. Animated Shaders & Conclusion

I used shader animations for hovering effects on two buttons: "Help" and "Load".

Read about such animations in "Simple Shaders Tutorial".

1. "Help" button hovering applies "wave" effect to the game title, using a picture of "Voronoi noise" as the control texture.

2. "Load" button hovering applies rainbow effect with some complicated space distortion. (Don't ask me why that formula; I've no idea how I did it. But it looks cool!)
load.gif
load.gif (5.86 MiB) Viewed 13773 times

At first I used more aggressive color radiation, like from a real magical portal... But then I thought, who knows, can that be conductive to seizures?.. and decided to replace it with a modest rainbow spiral, to stay on the safe side.

To conclude, here are (some) AMMATEURs -
Amazing Main Menu Animations from Tutorial Enhancing Universally-loved Ren'Py:

"Help" - shimmer + title wave effects.
"About" - endless scrolling effect.
"Load" - rainbow spiral + space distortions.
"Start" - move & zoom + video clip + sprite manager.
"Options" - pendulum effect.
"Quit" - shimmer + darkened room effect.

If you hover the version number in the top-right corner, several effects will be played simultaneously.
combo.gif
combo.gif (2.24 MiB) Viewed 13773 times

Quite a bacchanalia of magical spirits, eh?

The game files are in the attachment. Unpack them (overwriting) into a freshly created Ren'Py project, using the last stable Ren'Py version (like 8.3.7).

If you want to share other amazing ideas for Main Menu animations, or anything like that, or have questions, welcome to post them in the comments!

Be healthy and happy, and live a responsible life to make it beautiful!
Attachments
AMMATEUR.zip
(6.57 MiB) Downloaded 1769 times

User avatar
jeffster
Eileen-Class Veteran
Posts: 1225
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Amazing Main Menu Animations Tutorial

#7 Post by jeffster »

Remember the BlockDrop effect?

It can be applied not only as transitions between screens, but also as a transform for separate images.
bonus.gif
bonus.gif (940.28 KiB) Viewed 4945 times
We do that by using AlphaMask displayable, with

- the target image as child
- and BlockDropControl image as mask

(you can get the BlockDropControl code from the link at the top of this post).

As an example, here's how we apply the "crumbling" effect to the game title in the picture above:

Code: Select all

        frame:
            xysize (1400, 400)
            background None
            align (.5, 1.)
            if gui.show_name:

                #text "[config.name]":
                #    style "main_menu_title"
                #    at night_fall_lift
                #    color "#006A81"

                add AlphaMask(

                    Text("[config.name]",
                            style="main_menu_title", color="#006A81"),

                    BlockDropControl(3., time_warp=_warper.easeout,
                                    w=1400, h=400, cols=70, rows=40),

                    invert=True
                    ):
                        at night_fall_lift
                        xalign 0.5

Post Reply