Skip to content

Instantly share code, notes, and snippets.

@sudosilico
Created April 17, 2024 00:22
Show Gist options
  • Save sudosilico/e56f5d4494550724f339c267d59de86a to your computer and use it in GitHub Desktop.
Save sudosilico/e56f5d4494550724f339c267d59de86a to your computer and use it in GitHub Desktop.

Code Transformations via diff-match-patch

Demo.mp4

Inspired by Shiki Magic Move

This code uses Google's diff-match-patch library to animate code transformations in Manim.

from manim import *
import diff_match_patch as dmp_module


config.background_color = "#191919"


def find_matching_code_spans(before: Code, after: Code):
    in_str = before.code_string
    out_str = after.code_string

    dmp = dmp_module.diff_match_patch()
    diffs = dmp.diff_main(in_str, out_str)
    dmp.diff_cleanupSemantic(diffs)

    in_pos = out_pos = 0

    matches = []
    deletions = []
    additions = []

    for op, diff in diffs:
        length = len(diff)
        if op == 0:  # Match
            in_span = (in_pos, length)
            out_span = (out_pos, length)
            matches.append((in_span, out_span))
            in_pos += length
            out_pos += length
        elif op == -1:  # Delete
            deletions.append((in_pos, length))
            in_pos += length
        elif op == 1:  # Add
            additions.append((out_pos, length))
            out_pos += length

    return matches, deletions, additions


def select_characters(code: Code, span):
    start, length = span

    if length == 0:
        return []

    end = start + length
    selected_chars = []

    current_index = 0
    for line_number, line in enumerate(code.code):
        pos_of_line_start = current_index
        pos_of_line_end = current_index + len(line) + 1

        span_start = max(start, pos_of_line_start)
        span_end = min(end, pos_of_line_end)

        if span_start < span_end:
            line_relative_start = span_start - pos_of_line_start
            line_relative_end = span_end - pos_of_line_start

            selected_chars.append(line[line_relative_start:line_relative_end])

        current_index = pos_of_line_end

        if current_index > end:
            break

    return selected_chars


class CodeTransform(AnimationGroup):
    def __init__(
        self,
        before: Code,
        after: Code,
        **kwargs,
    ):
        matching_pairs, deletions, additions = find_matching_code_spans(before, after)

        transform_pairs = [
            (select_characters(before, in_match), select_characters(after, out_match))
            for in_match, out_match in matching_pairs
        ]

        delete_chars = [select_characters(before, deletion) for deletion in deletions]
        add_chars = [select_characters(after, addition) for addition in additions]

        super().__init__(
            LaggedStart(
                FadeOut(*[char for chars in delete_chars for char in chars]),
                LaggedStart(*[
                    Transform(before_match, after_match)
                    for before_chars, after_chars in transform_pairs
                    for before_match, after_match in zip(before_chars, after_chars)
                ]),
                FadeIn(*[char for chars in add_chars for char in chars])
            ),
            group=None,
            run_time=None,
            rate_func=linear,
            lag_ratio=0.0,
            **kwargs,
        )


def load_code(filename=None, code=None):
    code = Code(
        file_name=filename,
        language="rust",
        tab_width=4,
        line_spacing=0.75,
        background_stroke_width=0.1,
        background_stroke_color=WHITE,
        insert_line_no=False,
        style="monokai",
        background="rectangle",
        generate_html_file=True,
    ).scale(0.8)

    code.remove(code.background_mobject)

    return code


class Demo(Scene):
    def construct(self):
        before = load_code(filename="before.rs")
        after = load_code(filename="after.rs")

        self.add(before)

        self.wait()
        self.play(CodeTransform(before, after))
        self.wait()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment