|
import PySide6.QtSvg |
|
import PySide6.QtGui |
|
import PySide6.QtCore |
|
import moviepy.video.VideoClip |
|
import moviepy.editor |
|
import numpy as np |
|
from moviepy.editor import * |
|
from vectortween.PointAnimation import PointAnimation |
|
from vectortween.SequentialAnimation import SequentialAnimation |
|
|
|
# set up some properties of the clip we are going to generate |
|
fps = 25 |
|
duration = 10 |
|
W = 1000 |
|
H = 500 |
|
|
|
# some helper code to convert SVG to png |
|
def toNumpy(incomingImage): |
|
''' Converts a QImage into numpy format ''' |
|
|
|
incomingImage = incomingImage.convertToFormat(PySide6.QtGui.QImage.Format.Format_RGB32) |
|
|
|
width = incomingImage.width() |
|
height = incomingImage.height() |
|
|
|
ptr = incomingImage.bits() |
|
arr = np.array(ptr).reshape(height, width, 4) # Copies the data |
|
return arr[:, :, :3] # remove alpha channel |
|
|
|
# need to create dummy application to access font database |
|
dummyapp = PySide6.QtGui.QGuiApplication() |
|
|
|
# dynamically build a function that generates frames based on animation parameters and an svg text template |
|
def make_frame_with_animation_and_text(xyanimation, anim_fraction, total_duration, text): |
|
""" |
|
xyanimation: a PointAnimation to vary the x, y position of the text. (0, 0) is centered in the middle of the screen. |
|
anim_fraction: how long should the animation take to complete relative to the total_duration of the clip (e.g. 0.9 is 90%) |
|
total_duration: total duration of the clip in seconds |
|
text: svg text template (see example code below) |
|
""" |
|
def make_frame(t): |
|
df = total_duration * fps |
|
tf = t * fps |
|
x, y = xyanimation.make_frame(tf, 0, 0, anim_fraction * df, df, None) |
|
svg = f"""<?xml version="1.0" encoding="UTF-8"?> |
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" |
|
width="{W}" height="{H}" viewBox="{-W / 2} {-H / 2} {W} {H}"> |
|
{text.format(x=x, y=y)} |
|
</svg> |
|
""" |
|
r = PySide6.QtSvg.QSvgRenderer(bytes(svg, "utf-8")) |
|
i = PySide6.QtGui.QImage(PySide6.QtCore.QSize(W, H), PySide6.QtGui.QImage.Format_RGB32) |
|
i.fill(PySide6.QtGui.QColor(0, 0, 0)) |
|
p = PySide6.QtGui.QPainter(i) |
|
r.render(p) |
|
p.end() |
|
return toNumpy(i.rgbSwapped()) |
|
|
|
return make_frame |
|
|
|
# define some fancy styles we can use while defining texts |
|
my_styles = { |
|
"normal": """fill="black" stroke="white" stroke-width="2px" font-size="55" font-family="sans-serif" text-anchor="middle" """, |
|
"h1": """fill="blue" stroke="gold" stroke-width="4px" font-size="80" font-family="serif" text-anchor="middle" """, |
|
"h2": """fill="gold" stroke="maroon" stroke-width="2px" font-size="90" font-family="serif" font-style="italic" text-anchor="middle" """, |
|
"h3": """fill="green" stroke="white" stroke-width="6px" font-size="200" font-family="serif" text-anchor="middle" """, |
|
"small": """fill="red" stroke="white" stroke-width="5px" font-size="30" font-weight="bold" font-family="serif" text-anchor="middle" """ |
|
} |
|
|
|
# define some fancy animations we can use to move our text around the screen |
|
anim = { |
|
"top_to_bottom": PointAnimation((-W, -H / 2), (0, H / 3), tween=['easeOutBounce'], ytween=['easeOutBounce']), |
|
"topright_to_left": PointAnimation((W + 10, -H), (0, 0), tween=['easeOutQuad'], ytween=['easeOutCirc']), |
|
"multi": SequentialAnimation([ |
|
PointAnimation((-W / 5, -H / 3), (W / 3, H / 2), tween=['easeOutBounce'], ytween=['easeOutQuad']), |
|
PointAnimation((W / 3, H / 2), (-W / 5, H / 5), tween=['easeOutQuad'], ytween=['easeOutBounce']), |
|
PointAnimation((-W / 5, H / 5), (W / 5, -H / 3), tween=['easeOutQuad'], ytween=['easeOutQuad']), |
|
PointAnimation((W / 5, -H / 3), (-W / 5, -H / 3), tween=['easeOutQuad'], ytween=['easeOutBounce']), |
|
], timeweight=[1, 0.5, 0.5, 1], repeats=3) |
|
} |
|
|
|
# define some texts |
|
# each line of text must be defined separately, and can be styled and animated separately |
|
|
|
# text svg template must take include x="{{x}}" y="{{y}}" properties for an animation to have effect |
|
my_text = """<text {normal} x="{{x}}" y="{{y}}">Outline text <tspan {h1}>with</tspan> <tspan {h2}>highlights</tspan>!</text>""".format( |
|
**my_styles) |
|
# render clip with text line |
|
txt_clip = moviepy.video.VideoClip.VideoClip( |
|
make_frame=make_frame_with_animation_and_text(anim['top_to_bottom'], 0.25, duration, my_text), \ |
|
duration=duration / 2) |
|
# make black (= background) transparent so we can overlay text lines later on |
|
txt_clip = moviepy.editor.vfx.mask_color(txt_clip, color=[0, 0, 0]) |
|
|
|
my_2nd_text = """<text {normal} x="{{x}}" y="{{y}}">Follow-up <tspan {h3}>to</tspan> <tspan {h1}>first part?!</tspan></text>""".format( |
|
**my_styles) |
|
_2nd_txt_clip = moviepy.video.VideoClip.VideoClip( |
|
make_frame=make_frame_with_animation_and_text(anim['topright_to_left'], 0.1, duration, my_2nd_text), \ |
|
duration=duration / 2) |
|
_2nd_txt_clip = moviepy.editor.vfx.mask_color(_2nd_txt_clip, |
|
color=[0, 0, 0]) # make black transparent so we can overlay clips |
|
|
|
my_3rd_text = """<text {small} x="{{x}}" y="{{y}}">MOVIEPY ROCKS!</text>""".format(**my_styles) |
|
_3rd_txt_clip = moviepy.video.VideoClip.VideoClip( |
|
make_frame=make_frame_with_animation_and_text(anim['multi'], 1.0, duration, my_3rd_text), \ |
|
duration=duration |
|
) |
|
_3rd_txt_clip = moviepy.editor.vfx.mask_color(_3rd_txt_clip, |
|
color=[0, 0, 0]) # make black transparent so we can overlay clips |
|
|
|
# Overlay all text clips for some fancy animation |
|
video = CompositeVideoClip([txt_clip, _2nd_txt_clip, _3rd_txt_clip]) |
|
|
|
# Write video file with the result |
|
video.write_gif("test_with_svg.gif", fps=fps) |
see: Example of what this produces.