ManimCommunity / manim

A community-maintained Python framework for creating mathematical animations.
https://www.manim.community
MIT License
21.09k stars 1.53k forks source link

TracedPath stroke_opacity acting weird, with working version for comparison #3854

Open ev-watson opened 2 months ago

ev-watson commented 2 months ago

Description of bug / unexpected behavior

TracedPath stroke_opacity gradient has weird bug where the opacity gradient direction suddenly flips mid animation

Expected behavior

Fading trail behind a moving Dot3D in a ThreeDScene, this trail should have a opacity gradient with a color gradient and disappear over time.

How to reproduce the issue

from manim import *
from manim import config
import pandas as pd
import numpy as np
from tqdm import tqdm

SCALE_FACTOR = 4                        # Manifold scaling factor
ORBITS = 1.2                            # Number of revolutions, can be float
TRAIL_LENGTH = config.frame_rate * 1.2  # Length of trail behind mercury
DESIRED_TOTAL_TIME = 12                 # Desired length of animation, may not be this length

def prepare_data(file_name):
    """
    Prepare Data Method

    Scales data to fit on screen

    :param file_name: The name of the CSV file containing the data.
    :return: The processed dataframe.

    """
    orbit_points = int(90023 / (5 / 0.2408467) * ORBITS)  # 90023 points = 5 yrs -> 0.2408467 yrs = 1 rev, * # of revs

    df = pd.read_csv(file_name, nrows=orbit_points, usecols=[1, 2, 3, 8], dtype=np.float64)
    df.reset_index(drop=True, inplace=True)
    max_value = df['RG'].values.max()

    for col in ['X', 'Y', 'Z']:
        df[col] = df[col] / max_value * SCALE_FACTOR
    return df

class Orbit(ThreeDScene):
    """Class for visualizing the orbit of Mercury.

    This class inherits from the ThreeDScene class
    """
    def construct(self):
        axes = ThreeDAxes(num_axis_pieces=20,
                          color=GREY,
                          axis_config={
                              'stroke_opacity': 0.8,
                              'stroke_width': 0.5,
                              'include_tip': False
                          })

        self.camera.background_color = BLACK  # Dark grey

        df = prepare_data('horizons_half_hour.csv')  # This processes Mercury's orbit data

        # list of points, with each point of the form [x, y, z]
        points = df[['X', 'Y', 'Z']].values

        # Mercury representation
        mercury = Dot3D(radius=SCALE_FACTOR * 0.01, color=GREY)
        mercury.move_to(points[0])
        index_tracker = ValueTracker(0)  # Tracker for locus of mercury
        mercury.add_updater(lambda m: m.move_to(
            points[min(int(index_tracker.get_value()), len(points) - 1)]
        ))

        # Sun representation (radius close to 4.375% of orbit)
        sun = Dot3D(radius=.6 * SCALE_FACTOR * 0.04375, color=ORANGE)

        traced_path = TracedPath(mercury.get_center,
                                 stroke_color=[PURE_BLUE, PINK],
                                 stroke_opacity=[0, 1],
                                 dissipating_time=1,
                                 stroke_width=SCALE_FACTOR / 1.5)

        fading_path = VMobject(stroke_color=[DARK_BLUE, LIGHT_PINK], stroke_width=4)
        fading_path.start_new_path(mercury.get_center())

        def update_path(path_group):
            new_line = Line(path_group[-1].get_end(), mercury.get_center())
            start_color = PURE_BLUE
            end_color = PINK

            # calculate linear gradient function for opacity
            max_opacity = 1
            min_opacity = 0
            opacity_step = (max_opacity - min_opacity) / len(path_group)

            new_line.set_opacity(max_opacity)
            new_line.set_color(end_color)
            path_group.add(new_line)

            if len(path_group) > TRAIL_LENGTH:
                for line in path_group:
                    # If the opacity of a line is lower than threshold, remove it from path_group
                    if line.get_stroke_opacity() < 0.05:
                        path_group.remove(line)

            for i, line in enumerate(path_group):
                # update opacity for each line in the path group
                new_opacity = min_opacity + i * opacity_step
                line.set_stroke(opacity=new_opacity)

                # update color of each line in the path group
                ratio = i / len(path_group)
                new_color = interpolate_color(start_color, end_color, ratio)
                line.set_color(new_color)

        fading_path.add_updater(update_path)

        self.add(axes, mercury, sun, traced_path)
        self.set_camera_orientation(phi=65 * DEGREES, theta=-120 * DEGREES)

        # initialize trackers for camera orientation
        phi: ValueTracker = self.camera.phi_tracker
        theta: ValueTracker = self.camera.theta_tracker

        step = 100
        total_steps = (len(points) - 2) // step
        run_time = DESIRED_TOTAL_TIME / total_steps

        for _ in tqdm(range(0, int(total_steps))):
            # animate orientation and increment
            self.play(phi.animate.increment_value(-0.004),
                      theta.animate.increment_value(0.006),
                      ApplyMethod(index_tracker.increment_value, step),
                      rate_func=linear,
                      run_time=run_time
                      )

        mercury.clear_updaters()
        self.remove(phi, theta)
        self.wait()

Necessary Information

The real 'horizons_half_hour.csv' is 36 million rows long, so I have provided a shortened version shortened_data.csv

By running the code, as is right now, you will notice the traced path has weird behavior, the opacity gradient suddenly flips at random moments in the orbit.

By modifying the code to replace traced_path with fading_path in the self.add() line, you will see what the desired behavior is

By debugging you will find that it is not due to the color gradient, or dissipation time, but the stroke_opacity gradient passed by the list, as when this is removed everything behaves normally.

This functionality is not specifically mentioned in the documentation, however it is used in the example on the TracedPath documentation page so I am unsure of many details regarding this feature.

Screenshots:

BUG:

Screenshot 2024-07-12 at 5 46 05 PM

WORKING:

Screenshot 2024-07-12 at 5 46 20 PM

System specifications

System Details - OS (with version, e.g., Windows 10 v2004 or macOS 10.15 (Catalina)): macOS Sonoma 14.3 - RAM: 16GB - Python version (`python/py/python3 --version`): 3.9.6 - Installed modules (provide output from `pip list`): ``` (.venv) (base) usr@mac project % pip list Package Version ---------------------------- ------------------ absl-py 2.1.0 anyio 4.4.0 appnope 0.1.4 argon2-cffi 23.1.0 argon2-cffi-bindings 21.2.0 arrow 1.3.0 astropy 6.0.1 astropy-iers-data 0.2024.7.8.0.31.19 asttokens 2.4.1 astunparse 1.6.3 async-lru 2.0.4 attrs 23.2.0 Babel 2.15.0 beautifulsoup4 4.12.3 bleach 6.1.0 certifi 2024.6.2 cffi 1.16.0 charset-normalizer 3.3.2 click 8.1.7 cloup 3.0.5 colour 0.1.5 comm 0.2.2 contourpy 1.2.1 cycler 0.12.1 Cython 3.0.10 debugpy 1.8.2 decorator 5.1.1 defusedxml 0.7.1 exceptiongroup 1.2.1 executing 2.0.1 fastjsonschema 2.20.0 flatbuffers 24.3.25 fonttools 4.53.0 fqdn 1.5.1 gast 0.6.0 glcontext 2.5.0 google-pasta 0.2.0 grpcio 1.64.1 h11 0.14.0 h5py 3.11.0 httpcore 1.0.5 httpx 0.27.0 idna 3.7 importlib_metadata 8.0.0 importlib_resources 6.4.0 ipykernel 6.29.5 ipython 8.18.1 ipywidgets 8.1.3 isoduration 20.11.0 isosurfaces 0.1.2 jedi 0.19.1 Jinja2 3.1.4 json5 0.9.25 jsonpointer 3.0.0 jsonschema 4.22.0 jsonschema-specifications 2023.12.1 jupyter 1.0.0 jupyter_client 8.6.2 jupyter-console 6.6.3 jupyter_core 5.7.2 jupyter-events 0.10.0 jupyter-lsp 2.2.5 jupyter_server 2.14.1 jupyter_server_terminals 0.5.3 jupyterlab 4.2.3 jupyterlab_pygments 0.3.0 jupyterlab_server 2.27.2 jupyterlab_widgets 3.0.11 keras 3.4.1 kiwisolver 1.4.5 libclang 18.1.1 manim 0.18.1 manimgl 1.6.1 ManimPango 0.5.0 mapbox-earcut 1.0.1 Markdown 3.6 markdown-it-py 3.0.0 MarkupSafe 2.1.5 matplotlib 3.9.0 matplotlib-inline 0.1.7 mdurl 0.1.2 mistune 3.0.2 ml-dtypes 0.3.2 moderngl 5.10.0 moderngl-window 2.4.6 mpmath 1.3.0 multipledispatch 1.0.0 namex 0.0.8 nbclient 0.10.0 nbconvert 7.16.4 nbformat 5.10.4 nest-asyncio 1.6.0 networkx 3.2.1 nodejs 0.1.1 notebook 7.2.1 notebook_shim 0.2.4 npm 0.1.1 numpy 1.26.4 opt-einsum 3.3.0 optional-django 0.1.0 optree 0.11.0 overrides 7.7.0 packaging 24.1 pandas 2.2.2 pandocfilters 1.5.1 parso 0.8.4 pexpect 4.9.0 pillow 10.4.0 pip 24.1.1 platformdirs 4.2.2 prometheus_client 0.20.0 prompt_toolkit 3.0.47 protobuf 4.25.3 psutil 6.0.0 ptyprocess 0.7.0 pure-eval 0.2.2 pycairo 1.26.1 pycparser 2.22 pydub 0.25.1 pyerfa 2.0.1.4 pyglet 2.0.15 Pygments 2.18.0 pyobjc-core 10.3.1 pyobjc-framework-Cocoa 10.3.1 PyOpenGL 3.1.7 pyparsing 3.1.2 pyrr 0.10.3 python-dateutil 2.9.0.post0 python-json-logger 2.0.7 pytz 2024.1 PyYAML 6.0.1 pyzmq 26.0.3 qtconsole 5.5.2 QtPy 2.4.1 referencing 0.35.1 requests 2.32.3 rfc3339-validator 0.1.4 rfc3986-validator 0.1.1 rich 13.7.1 rpds-py 0.18.1 scipy 1.13.1 screeninfo 0.8.1 seaborn 0.13.2 Send2Trash 1.8.3 setuptools 68.2.0 six 1.16.0 skia-pathops 0.8.0.post1 sniffio 1.3.1 soupsieve 2.5 srt 3.5.3 stack-data 0.6.3 svgelements 1.9.6 sympy 1.12.1 tensorboard 2.16.2 tensorboard-data-server 0.7.2 tensorflow 2.16.2 tensorflow-io-gcs-filesystem 0.37.1 termcolor 2.4.0 terminado 0.18.1 tinycss2 1.3.0 tomli 2.0.1 tornado 6.4.1 tqdm 4.66.4 traitlets 5.14.3 types-python-dateutil 2.9.0.20240316 typing_extensions 4.12.2 tzdata 2024.1 uri-template 1.3.0 urllib3 2.2.2 validators 0.29.0 vg 2.0.0 watchdog 4.0.1 wcwidth 0.2.13 webcolors 24.6.0 webencodings 0.5.1 websocket-client 1.8.0 Werkzeug 3.0.3 wheel 0.41.2 widgetsnbextension 4.0.11 wrapt 1.16.0 zipp 3.19.2 ```
LaTeX details + LaTeX distribution (e.g. TeX Live 2020): TeX Live 2024 + Installed LaTeX packages: A lot and I don't think this is important so I save the space, if needed let me know

Additional comments

ev-watson commented 2 months ago

Here is object version of a working fading traced path code:

class FadingPath(VMobject):
    """
    Custom VMobject subclass to create a fading path trail.
    Contains an updater method to update each of its points.
    """

    def __init__(self,
                 traced_mobject: Mobject,
                 trail_length: int = TRAIL_LENGTH,
                 stroke_width: int = SCALE_FACTOR,
                 stroke_color: ManimColor = None,
                 **kwargs) -> None:
        super().__init__(**kwargs)
        if stroke_color is None:
            stroke_color = [PURE_BLUE, PINK]
        self.stroke_color = stroke_color
        self.stroke_width = stroke_width
        self.traced_mobject = traced_mobject
        self.trail_length = trail_length
        self.transparent_color = stroke_color[0] if isinstance(stroke_color, list) else stroke_color
        self.opaque_color = stroke_color[1] if isinstance(stroke_color, list) else stroke_color
        self.start_new_path(self.traced_mobject.get_center())
        self.add_updater(self.update_trace)

    def update_trace(self, mobj, dt):
        new_line = Line(self[-1].get_end(), self.traced_mobject.get_center())

        # calculate linear gradient function for opacity
        max_opacity = 1
        min_opacity = 0
        opacity_step = (max_opacity - min_opacity) / len(self)

        new_line.set_opacity(max_opacity)
        new_line.set_color(self.opaque_color)
        self.add(new_line)

        if len(self) > self.trail_length:
            for i, line in enumerate(self):
                if i in [0, 1]:
                    self.remove(line)

        for i, line in enumerate(self):
            # update opacity for each line in the path group
            new_opacity = min_opacity + i * opacity_step
            line.set_stroke(opacity=new_opacity)

            # update color of each line in the path group
            ratio = (i / len(self)) ** 2
            new_color = interpolate_color(self.transparent_color, self.opaque_color, ratio)
            line.set_color(new_color)