- 310 registry:skill items, 1528 files, 7 categories (hub tags) - registry.json generated by @kortix/registry buildRegistry - nested SKILL.md in sn-da-* renamed SUBSKILL.md (no phantom skills) - provenance + licenses: SOURCES.md
399 lines
11 KiB
Python
399 lines
11 KiB
Python
"""
|
|
Thumbnail Generation Presets
|
|
Ready-to-use functions for creating video thumbnails and title cards
|
|
"""
|
|
|
|
from manim import *
|
|
import numpy as np
|
|
|
|
|
|
# =============================================================================
|
|
# ASPECT RATIO CONFIGURATIONS
|
|
# =============================================================================
|
|
|
|
ASPECT_RATIOS = {
|
|
"youtube": {"width": 1920, "height": 1080, "name": "16:9"},
|
|
"youtube_short": {"width": 1080, "height": 1920, "name": "9:16"},
|
|
"instagram": {"width": 1080, "height": 1080, "name": "1:1"},
|
|
"twitter": {"width": 1200, "height": 675, "name": "16:9"},
|
|
"linkedin": {"width": 1200, "height": 627, "name": "1.91:1"},
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# THUMBNAIL SCENE BASE
|
|
# =============================================================================
|
|
|
|
class ThumbnailScene(Scene):
|
|
"""Base class for thumbnail generation."""
|
|
|
|
# Override in subclass
|
|
TITLE = "Your Title Here"
|
|
SUBTITLE = ""
|
|
ASPECT_RATIO = "youtube"
|
|
|
|
def construct(self):
|
|
# Set up frame for aspect ratio
|
|
config = ASPECT_RATIOS.get(self.ASPECT_RATIO, ASPECT_RATIOS["youtube"])
|
|
|
|
# Build thumbnail
|
|
self.create_background()
|
|
self.create_main_visual()
|
|
self.create_text_overlay()
|
|
|
|
# Hold frame (for image export)
|
|
self.wait(0.1)
|
|
|
|
def create_background(self):
|
|
"""Override to customize background."""
|
|
# Default gradient background
|
|
bg = Rectangle(
|
|
width=config.frame_width + 1,
|
|
height=config.frame_height + 1,
|
|
fill_opacity=1,
|
|
stroke_width=0
|
|
)
|
|
bg.set_color(color=[BLUE_E, PURPLE_E])
|
|
self.add(bg)
|
|
|
|
def create_main_visual(self):
|
|
"""Override to add main visual element."""
|
|
pass
|
|
|
|
def create_text_overlay(self):
|
|
"""Override to customize text."""
|
|
title = Text(
|
|
self.TITLE,
|
|
font_size=72,
|
|
weight=BOLD,
|
|
color=WHITE
|
|
)
|
|
title.move_to(ORIGIN)
|
|
|
|
if self.SUBTITLE:
|
|
subtitle = Text(
|
|
self.SUBTITLE,
|
|
font_size=36,
|
|
color=GREY_A
|
|
)
|
|
subtitle.next_to(title, DOWN, buff=0.5)
|
|
self.add(subtitle)
|
|
|
|
self.add(title)
|
|
|
|
|
|
# =============================================================================
|
|
# PRESET THUMBNAIL STYLES
|
|
# =============================================================================
|
|
|
|
def create_math_thumbnail(scene, title, equation, colors=None):
|
|
"""
|
|
Create a math-focused thumbnail.
|
|
|
|
Args:
|
|
scene: The Scene instance
|
|
title: Main title text
|
|
equation: LaTeX equation string
|
|
colors: Optional dict of {substring: color}
|
|
"""
|
|
if colors is None:
|
|
colors = {}
|
|
|
|
# Dark gradient background
|
|
bg = Rectangle(
|
|
width=20, height=12,
|
|
fill_opacity=1, stroke_width=0
|
|
)
|
|
bg.set_color(color=[BLUE_E, BLACK])
|
|
scene.add(bg)
|
|
|
|
# Equation (main focus)
|
|
eq = MathTex(equation, font_size=96)
|
|
for substr, color in colors.items():
|
|
eq.set_color_by_tex(substr, color)
|
|
eq.move_to(ORIGIN)
|
|
|
|
# Title above
|
|
title_text = Text(title, font_size=56, weight=BOLD, color=WHITE)
|
|
title_text.to_edge(UP, buff=0.8)
|
|
|
|
# Decorative elements
|
|
line_left = Line(LEFT * 6, LEFT * 2, color=BLUE, stroke_width=4)
|
|
line_right = Line(RIGHT * 2, RIGHT * 6, color=BLUE, stroke_width=4)
|
|
line_left.next_to(title_text, DOWN, buff=0.3)
|
|
line_right.next_to(title_text, DOWN, buff=0.3)
|
|
|
|
scene.add(bg, eq, title_text, line_left, line_right)
|
|
|
|
|
|
def create_concept_thumbnail(scene, title, icon_mobject, subtitle=""):
|
|
"""
|
|
Create a concept-focused thumbnail with icon.
|
|
|
|
Args:
|
|
scene: The Scene instance
|
|
title: Main title text
|
|
icon_mobject: A Mobject to use as the central icon
|
|
subtitle: Optional subtitle
|
|
"""
|
|
# Gradient background
|
|
bg = Rectangle(width=20, height=12, fill_opacity=1, stroke_width=0)
|
|
bg.set_color(color=[PURPLE_E, BLUE_E])
|
|
scene.add(bg)
|
|
|
|
# Icon (scaled and centered)
|
|
icon = icon_mobject.copy()
|
|
icon.scale_to_fit_height(4)
|
|
icon.move_to(ORIGIN)
|
|
|
|
# Add glow effect
|
|
glow = icon.copy()
|
|
glow.set_color(WHITE)
|
|
glow.set_opacity(0.3)
|
|
glow.scale(1.2)
|
|
scene.add(glow)
|
|
scene.add(icon)
|
|
|
|
# Title
|
|
title_text = Text(title, font_size=64, weight=BOLD, color=WHITE)
|
|
title_text.to_edge(UP, buff=0.6)
|
|
scene.add(title_text)
|
|
|
|
# Subtitle
|
|
if subtitle:
|
|
sub = Text(subtitle, font_size=32, color=GREY_A)
|
|
sub.to_edge(DOWN, buff=0.8)
|
|
scene.add(sub)
|
|
|
|
|
|
def create_comparison_thumbnail(scene, left_text, right_text, vs_text="VS"):
|
|
"""
|
|
Create a comparison/versus thumbnail.
|
|
|
|
Args:
|
|
scene: The Scene instance
|
|
left_text: Text for left side
|
|
right_text: Text for right side
|
|
vs_text: Center divider text
|
|
"""
|
|
# Split background
|
|
left_bg = Rectangle(width=8, height=12, fill_opacity=1, stroke_width=0)
|
|
left_bg.set_color(BLUE_E)
|
|
left_bg.shift(LEFT * 4)
|
|
|
|
right_bg = Rectangle(width=8, height=12, fill_opacity=1, stroke_width=0)
|
|
right_bg.set_color(RED_E)
|
|
right_bg.shift(RIGHT * 4)
|
|
|
|
scene.add(left_bg, right_bg)
|
|
|
|
# VS circle
|
|
vs_circle = Circle(radius=1, fill_opacity=1, fill_color=WHITE, stroke_width=0)
|
|
vs_label = Text(vs_text, font_size=48, weight=BOLD, color=BLACK)
|
|
vs_group = VGroup(vs_circle, vs_label)
|
|
scene.add(vs_group)
|
|
|
|
# Left text
|
|
left = Text(left_text, font_size=56, weight=BOLD, color=WHITE)
|
|
left.move_to(LEFT * 4)
|
|
scene.add(left)
|
|
|
|
# Right text
|
|
right = Text(right_text, font_size=56, weight=BOLD, color=WHITE)
|
|
right.move_to(RIGHT * 4)
|
|
scene.add(right)
|
|
|
|
|
|
def create_numbered_thumbnail(scene, number, title, subtitle=""):
|
|
"""
|
|
Create a numbered episode/part thumbnail.
|
|
|
|
Args:
|
|
scene: The Scene instance
|
|
number: Episode/part number
|
|
title: Main title
|
|
subtitle: Optional subtitle
|
|
"""
|
|
# Background
|
|
bg = Rectangle(width=20, height=12, fill_opacity=1, stroke_width=0)
|
|
bg.set_color(color=[GREY_E, BLACK])
|
|
scene.add(bg)
|
|
|
|
# Large number
|
|
num = Text(str(number), font_size=200, weight=BOLD, color=BLUE)
|
|
num.set_opacity(0.3)
|
|
num.move_to(LEFT * 3)
|
|
scene.add(num)
|
|
|
|
# Title
|
|
title_text = Text(title, font_size=64, weight=BOLD, color=WHITE)
|
|
title_text.move_to(RIGHT * 1 + UP * 0.5)
|
|
scene.add(title_text)
|
|
|
|
# Part label
|
|
part_label = Text(f"PART {number}", font_size=28, color=BLUE)
|
|
part_label.next_to(title_text, UP, buff=0.5)
|
|
scene.add(part_label)
|
|
|
|
# Subtitle
|
|
if subtitle:
|
|
sub = Text(subtitle, font_size=32, color=GREY_A)
|
|
sub.next_to(title_text, DOWN, buff=0.5)
|
|
scene.add(sub)
|
|
|
|
|
|
def create_quote_thumbnail(scene, quote, author=""):
|
|
"""
|
|
Create a quote-style thumbnail.
|
|
|
|
Args:
|
|
scene: The Scene instance
|
|
quote: The quote text
|
|
author: Optional author attribution
|
|
"""
|
|
# Dark background
|
|
bg = Rectangle(width=20, height=12, fill_opacity=1, stroke_width=0)
|
|
bg.set_color(BLACK)
|
|
scene.add(bg)
|
|
|
|
# Quote marks
|
|
open_quote = Text('"', font_size=200, color=BLUE)
|
|
open_quote.set_opacity(0.5)
|
|
open_quote.to_corner(UL, buff=0.5)
|
|
scene.add(open_quote)
|
|
|
|
close_quote = Text('"', font_size=200, color=BLUE)
|
|
close_quote.set_opacity(0.5)
|
|
close_quote.to_corner(DR, buff=0.5)
|
|
scene.add(close_quote)
|
|
|
|
# Quote text
|
|
quote_text = Text(
|
|
quote,
|
|
font_size=48,
|
|
color=WHITE,
|
|
line_spacing=1.5
|
|
)
|
|
quote_text.move_to(ORIGIN)
|
|
|
|
# Wrap if too wide
|
|
if quote_text.width > 12:
|
|
quote_text.scale_to_fit_width(12)
|
|
|
|
scene.add(quote_text)
|
|
|
|
# Author
|
|
if author:
|
|
author_text = Text(f"— {author}", font_size=32, color=GREY_A)
|
|
author_text.next_to(quote_text, DOWN, buff=0.8)
|
|
scene.add(author_text)
|
|
|
|
|
|
# =============================================================================
|
|
# TITLE CARD PRESETS
|
|
# =============================================================================
|
|
|
|
class IntroTitleCard(Scene):
|
|
"""Animated intro title card."""
|
|
|
|
TITLE = "Your Title"
|
|
SUBTITLE = "Your Subtitle"
|
|
DURATION = 3
|
|
|
|
def construct(self):
|
|
# Background
|
|
bg = Rectangle(width=20, height=12, fill_opacity=1, stroke_width=0)
|
|
bg.set_color(color=[BLUE_E, BLACK])
|
|
self.add(bg)
|
|
|
|
# Title
|
|
title = Text(self.TITLE, font_size=72, weight=BOLD, color=WHITE)
|
|
title.move_to(UP * 0.5)
|
|
|
|
# Subtitle
|
|
subtitle = Text(self.SUBTITLE, font_size=36, color=GREY_A)
|
|
subtitle.next_to(title, DOWN, buff=0.5)
|
|
|
|
# Underline
|
|
underline = Line(LEFT * 4, RIGHT * 4, color=BLUE, stroke_width=4)
|
|
underline.next_to(subtitle, DOWN, buff=0.5)
|
|
|
|
# Animate
|
|
self.play(Write(title), run_time=1)
|
|
self.play(FadeIn(subtitle, shift=UP * 0.2), run_time=0.5)
|
|
self.play(Create(underline), run_time=0.5)
|
|
self.wait(self.DURATION - 2)
|
|
|
|
|
|
class OutroTitleCard(Scene):
|
|
"""Animated outro/end card."""
|
|
|
|
THANKS_TEXT = "Thanks for watching!"
|
|
CTA_TEXT = "Subscribe for more"
|
|
DURATION = 4
|
|
|
|
def construct(self):
|
|
# Background
|
|
bg = Rectangle(width=20, height=12, fill_opacity=1, stroke_width=0)
|
|
bg.set_color(BLACK)
|
|
self.add(bg)
|
|
|
|
# Thanks
|
|
thanks = Text(self.THANKS_TEXT, font_size=64, weight=BOLD, color=WHITE)
|
|
thanks.move_to(UP * 1)
|
|
|
|
# CTA
|
|
cta = Text(self.CTA_TEXT, font_size=36, color=BLUE)
|
|
cta.move_to(DOWN * 0.5)
|
|
|
|
# Animate
|
|
self.play(FadeIn(thanks, scale=1.2), run_time=1)
|
|
self.wait(0.5)
|
|
self.play(FadeIn(cta, shift=UP * 0.3), run_time=0.5)
|
|
self.wait(self.DURATION - 2)
|
|
self.play(FadeOut(thanks), FadeOut(cta), run_time=0.5)
|
|
|
|
|
|
# =============================================================================
|
|
# UTILITY FUNCTIONS
|
|
# =============================================================================
|
|
|
|
def export_thumbnail(scene_class, output_path, **scene_kwargs):
|
|
"""
|
|
Export a thumbnail scene as an image.
|
|
|
|
Usage:
|
|
export_thumbnail(MyThumbnail, "thumbnail.png", TITLE="Custom Title")
|
|
"""
|
|
# Set scene attributes
|
|
for key, value in scene_kwargs.items():
|
|
setattr(scene_class, key, value)
|
|
|
|
# Configure for image export
|
|
config.frame_rate = 1
|
|
config.pixel_width = 1920
|
|
config.pixel_height = 1080
|
|
|
|
# Render
|
|
scene = scene_class()
|
|
scene.render()
|
|
|
|
# The output will be in media/images/
|
|
|
|
|
|
def batch_export_thumbnails(scenes, output_dir):
|
|
"""
|
|
Export multiple thumbnails.
|
|
|
|
Args:
|
|
scenes: List of (scene_class, filename, kwargs) tuples
|
|
output_dir: Directory for output files
|
|
"""
|
|
import os
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
for scene_class, filename, kwargs in scenes:
|
|
output_path = os.path.join(output_dir, filename)
|
|
export_thumbnail(scene_class, output_path, **kwargs)
|
|
print(f"Exported: {output_path}")
|