Created
May 7, 2026 17:31
-
-
Save motebaya/5f81ea6b8585baaa3dcf47b7658644d4 to your computer and use it in GitHub Desktop.
Generates a YouTube-style subscribe button graphic with a transparent background
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Generates a YouTube-style subscribe button graphic with a transparent background, | |
| decorative dashed accent lines, and an animated pointing-hand cursor overlay. | |
| @github.com/motebaya - 2026-03-29 08:33 PM | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from PIL import Image, ImageDraw, ImageFont, ImageFilter | |
| FONT_PATH = os.path.join( | |
| os.path.expanduser("~"), | |
| "AppData", "Local", "Microsoft", "Windows", "Fonts", "Oswald-Bold.ttf", | |
| ) | |
| # Perimeter of the hand cursor in normalised (unscaled) coordinate space. | |
| # Points are listed clockwise starting at the index-finger left edge. | |
| _CURSOR_PERIMETER: list[tuple[float, float]] = [ | |
| # --- Index Finger (Extended & Rounded) --- | |
| (4, 1), | |
| (4.2, 0.2), | |
| (5, -0.4), # tip peak | |
| (5.8, 0.2), | |
| (6, 1), | |
| (6, 2), # inner corner Index/Middle | |
| # --- Middle Knuckle (Folded & Rounded) --- | |
| (6.2, 1.7), | |
| (7, 1.4), # peak | |
| (7.8, 1.7), | |
| (8, 2.2), | |
| (8, 3), # inner corner Middle/Ring | |
| # --- Ring Knuckle (Folded & Rounded) --- | |
| (8.2, 2.7), | |
| (9, 2.4), # peak | |
| (9.8, 2.7), | |
| (10, 3.2), | |
| (10, 4), # inner corner Ring/Pinky | |
| # --- Pinky Knuckle (Folded & Rounded) --- | |
| (10.2, 3.7), | |
| (11, 3.4), # peak | |
| (11.8, 3.7), | |
| (12, 4.5), | |
| # --- Palm and Wrist --- | |
| (12, 10), | |
| (9, 14), | |
| (9, 18), | |
| (4, 18), | |
| (4, 15), | |
| # --- REVISED: Classic Compact & Rounded Thumb --- | |
| (2.5, 12.5), | |
| (1.0, 10.5), | |
| (0.2, 8.5), | |
| (0.2, 7.2), # tip peak | |
| (0.8, 6.5), | |
| (2.0, 6.5), | |
| (4, 8), # thumb crotch | |
| (4, 1), # close back to index left side | |
| ] | |
| # Unscaled separator lines between folded fingers (start, end). | |
| _FINGER_SEPARATORS: list[tuple[tuple[float, float], tuple[float, float]]] = [ | |
| ((6, 2), (6, 8)), # Index / Middle | |
| ((8, 3), (8, 9)), # Middle / Ring | |
| ((10, 4), (10, 9)), # Ring / Pinky | |
| ] | |
| def draw_broken_line( | |
| draw: ImageDraw.ImageDraw, | |
| x_start: float, | |
| x_end: float, | |
| y: float, | |
| pattern: list[float], | |
| fill: str = "#FF2222", | |
| width: int = 2, | |
| ) -> None: | |
| """Draw a proportional dashed line along a horizontal axis. | |
| :param draw: Active :class:`~PIL.ImageDraw.ImageDraw` context. | |
| :param x_start: Left boundary of the line. | |
| :param x_end: Right boundary of the line. | |
| :param y: Vertical position. | |
| :param pattern: Alternating proportions ``[draw, gap, draw, gap, …]`` | |
| that sum to 1.0. Even-indexed values are drawn segments. | |
| :param fill: Stroke colour (default red). | |
| :param width: Stroke width in pixels. | |
| """ | |
| total = x_end - x_start | |
| current_x = x_start | |
| for i, pct in enumerate(pattern): | |
| segment = total * pct | |
| if i % 2 == 0: # even index → draw segment | |
| draw.line([(current_x, y), (current_x + segment, y)], fill=fill, width=width) | |
| current_x += segment | |
| def draw_dynamic_cursor( | |
| base_img: Image.Image, | |
| offset_x: float, | |
| offset_y: float, | |
| scale: float = 4.5, | |
| ) -> None: | |
| """Composite a vector-style pointing-hand cursor onto *base_img*. | |
| The cursor shape is defined in normalised space via :data:`_CURSOR_PERIMETER` | |
| and scaled/translated at draw-time. A soft drop-shadow is rendered first. | |
| :param base_img: RGBA image to composite onto (modified in-place). | |
| :param offset_x: Horizontal translation applied after scaling. | |
| :param offset_y: Vertical translation applied after scaling. | |
| :param scale: Uniform scale factor (pixels per normalised unit). | |
| """ | |
| scaled = [(x * scale + offset_x, y * scale + offset_y) for x, y in _CURSOR_PERIMETER] | |
| line_width = max(2, int(scale * 0.6)) | |
| # 1. Drop shadow | |
| shadow_layer = Image.new("RGBA", base_img.size, (0, 0, 0, 0)) | |
| shadow_draw = ImageDraw.Draw(shadow_layer) | |
| shadow_offset = 8 | |
| shadow_draw.polygon( | |
| [(x + shadow_offset, y + shadow_offset) for x, y in scaled], | |
| fill=(0, 0, 0, 50), | |
| ) | |
| base_img.alpha_composite(shadow_layer.filter(ImageFilter.GaussianBlur(4))) | |
| # 2. Hand shape | |
| cursor_layer = Image.new("RGBA", base_img.size, (0, 0, 0, 0)) | |
| cursor_draw = ImageDraw.Draw(cursor_layer) | |
| cursor_draw.polygon(scaled, fill="#FFFFFF") | |
| cursor_draw.line(scaled + [scaled[0]], fill="#0F172A", width=line_width, joint="curve") | |
| # 3. Inner finger-separator lines | |
| for (sx_n, sy_n), (ex_n, ey_n) in _FINGER_SEPARATORS: | |
| cursor_draw.line( | |
| [ | |
| (sx_n * scale + offset_x, sy_n * scale + offset_y), | |
| (ex_n * scale + offset_x, ey_n * scale + offset_y), | |
| ], | |
| fill="#0F172A", | |
| width=line_width, | |
| ) | |
| base_img.alpha_composite(cursor_layer) | |
| def create_youtube_button( | |
| text: str = "SUBSCRIBE", | |
| output_filename: str = "custom_button.png", | |
| ) -> None: | |
| """Render a YouTube-style subscribe button and save it as a PNG. | |
| The output uses an RGBA transparent background so it can be placed | |
| directly over any video frame or graphic. | |
| :param text: Label displayed on the button (e.g. ``"@itsmochino"``). | |
| :param output_filename: Destination file path for the rendered PNG. | |
| """ | |
| width, height = 1000, 600 | |
| base_img = Image.new("RGBA", (width, height), (0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(base_img) | |
| # 1. Font | |
| try: | |
| font = ImageFont.truetype(FONT_PATH, 85) | |
| except IOError: | |
| print(f"Error: Could not find {FONT_PATH}. Using default.") | |
| font = ImageFont.load_default() | |
| # 2. Dynamic sizing | |
| bbox = draw.textbbox((0, 0), text, font=font) | |
| text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1] | |
| icon_w, icon_h = 130, 90 | |
| gap = 40 | |
| pad_x, pad_y = 60, 40 | |
| button_w = icon_w + gap + text_w + pad_x * 2 | |
| button_h = max(icon_h, text_h) + pad_y * 2 | |
| btn_x1 = (width - button_w) // 2 | |
| btn_y1 = (height - button_h) // 2 | |
| btn_x2 = btn_x1 + button_w | |
| btn_y2 = btn_y1 + button_h | |
| # 3. Button drop shadow | |
| shadow_layer = Image.new("RGBA", (width, height), (0, 0, 0, 0)) | |
| ImageDraw.Draw(shadow_layer).rounded_rectangle( | |
| [btn_x1, btn_y1 + 15, btn_x2, btn_y2 + 15], | |
| radius=20, | |
| fill=(0, 0, 0, 40), | |
| ) | |
| base_img.alpha_composite(shadow_layer.filter(ImageFilter.GaussianBlur(15))) | |
| # 4. Broken red accent lines | |
| line_start, line_end = btn_x1 + 30, btn_x2 - 30 | |
| offset = 30 | |
| # Proportions: "---- ------------------ - ---------" | |
| draw_broken_line( | |
| draw, | |
| line_start, | |
| line_end, | |
| btn_y1 - offset, | |
| [0.10, 0.10, 0.40, 0.05, 0.05, 0.15, 0.15] | |
| ) | |
| # Proportions: "--------- ------------------- ----" | |
| draw_broken_line( | |
| draw, | |
| line_start, | |
| line_end, | |
| btn_y2 + offset, | |
| [0.20, 0.15, 0.45, 0.15, 0.05] | |
| ) | |
| # 5. White button body | |
| draw.rounded_rectangle([btn_x1, btn_y1, btn_x2, btn_y2], radius=20, fill="#FFFFFF") | |
| # 6. YouTube icon (red pill + white play triangle) | |
| icon_x = btn_x1 + pad_x | |
| icon_y = btn_y1 + (button_h - icon_h) // 2 | |
| draw.rounded_rectangle([ | |
| icon_x, | |
| icon_y, | |
| icon_x + icon_w, | |
| icon_y + icon_h | |
| ], radius=22, fill="#FF0033") | |
| tri_w, tri_h = 35, 40 | |
| tri_x = icon_x + (icon_w - tri_w) // 2 + 5 | |
| tri_y = icon_y + (icon_h - tri_h) // 2 | |
| draw.polygon( | |
| [ | |
| (tri_x, tri_y), | |
| (tri_x, tri_y + tri_h), | |
| (tri_x + tri_w, tri_y + tri_h // 2) | |
| ], | |
| fill="#FFFFFF", | |
| ) | |
| # 7. Label text | |
| text_x = icon_x + icon_w + gap | |
| text_y = btn_y1 + (button_h - text_h) // 2 - 25 | |
| draw.text((text_x, text_y), text, fill="#0F172A", font=font) | |
| # 8. Hand cursor overlaid on the button corner | |
| draw_dynamic_cursor(base_img, btn_x2 - 50, btn_y2 - 30, scale=4.2) | |
| # 9. Save – PNG preserves transparency | |
| base_img.save(output_filename) | |
| print(f"Saved: {output_filename}") | |
| if __name__ == "__main__": | |
| create_youtube_button("@itsmochino", "transparent_subscribe.png") |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
result: