Created
December 15, 2023 13:41
-
-
Save petrblahos/aff7e60f4e9d95f0d9d2d6ad0ccdbdec to your computer and use it in GitHub Desktop.
A bubble effect under a writing.
This file contains 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
from collections import defaultdict | |
import io | |
import random | |
import pygame | |
from fontTools.pens.svgPathPen import (SVGPathPen, ) | |
from fontTools.ttLib import TTFont | |
class Particle(pygame.sprite.Sprite): | |
def __init__(self, x, y): | |
super().__init__() | |
self.start = (x, y) | |
self.x = x | |
self.y = y | |
self.r = 0 | |
self.speed = random.randint(1, 5) | |
self.image = pygame.Surface((self.r * 2, self.r * 2), pygame.SRCALPHA) | |
self.rect = self.image.get_rect(center=(self.x, self.y)) | |
def update(self, dt): | |
self.r += self.speed * dt / 400 | |
self.image = pygame.Surface((self.r * 2, self.r * 2), pygame.SRCALPHA) | |
pygame.draw.circle(self.image, (192, 255, 255), (self.r, self.r), self.r, width=0) | |
pygame.draw.circle(self.image, (0, 128, 128), (self.r, self.r), self.r, width=2) | |
self.rect = self.image.get_rect(center=(self.x, self.y)) | |
class PGMFont: | |
WIDTH = 1280 | |
HEIGHT = 600 | |
def __init__(self, font): | |
self.font = font | |
self.units_per_em = self.font['head'].unitsPerEm | |
self.ascent = self.font["hhea"].ascent | |
self.descent = self.font["hhea"].descent | |
self.cmap = self.font['cmap'].getBestCmap() | |
self.glyph_set = self.font.getGlyphSet() | |
self.kerning_pairs = self.prepare_kerning(self.font) | |
def prepare_kerning(self, font: TTFont) -> dict: | |
pairs = defaultdict(int) | |
if not "kern" in self.font: | |
return pairs | |
for kern_table in self.font['kern'].kernTables: | |
assert kern_table.version == 0, "kern table version other than 0 not supported" | |
pairs.update(kern_table.kernTable) | |
return pairs | |
def prepare_window(self): | |
self.screen = pygame.display.set_mode((self.WIDTH, self.HEIGHT)) | |
self.screen.fill((0, 0, 255)) | |
def create_svg_pair(self, text): | |
""" | |
Create 2 SVG files, one for stroke, one for fill. | |
""" | |
cmds_stroke = [] | |
cmds_fill = [] | |
pen = SVGPathPen(self.font.getGlyphSet()) | |
x0 = 150 | |
last_glyph = None | |
for unicode in text: | |
glyph_name = self.cmap.get(ord(unicode)) | |
if not glyph_name: | |
glyph_name = ".notdef" | |
glyph = self.glyph_set[glyph_name] | |
pen = SVGPathPen(self.glyph_set) | |
glyph.draw(pen) | |
commands = pen.getCommands() | |
x0 += self.kerning_pairs[(last_glyph, glyph_name)] | |
s = '<g transform="translate(%d %d) scale(1 -1)"><path d="%s" %%s /></g>\n' % ( | |
x0, | |
self.ascent, | |
commands, | |
) | |
x0 += glyph.width | |
cmds_stroke.append(s % 'stroke="#FFF" fill="none" stroke-width="4"') | |
cmds_fill.append(s % 'stroke="none" fill="#000"') | |
last_glyph = glyph_name | |
svg_stroke = "\n".join([ | |
"""<?xml version="1.0" encoding="UTF-8"?>""", | |
"""<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">""" % (x0, self.ascent - self.descent), | |
"\n".join(cmds_stroke) + "</svg>", | |
]) | |
svg_fill = "\n".join([ | |
"""<?xml version="1.0" encoding="UTF-8"?>""", | |
"""<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">""" % (x0, self.ascent - self.descent), | |
"\n".join(cmds_fill) + "</svg>", | |
]) | |
return (svg_stroke, svg_fill) | |
def draw_text(self, surface, text): | |
(svg_stroke, svg_fill) = self.create_svg_pair(text) | |
img = pygame.image.load(io.BytesIO(svg_stroke.encode("utf-8"))) | |
surface.blit(img, (0, 0)) | |
img = pygame.image.load(io.BytesIO(svg_fill.encode("utf-8"))) | |
surface.blit(img, (0, 0)) | |
def prepare_imgs(self, text="Fire!"): | |
(svg_stroke, svg_fill) = self.create_svg_pair(text) | |
self.bg_img = pygame.image.load(io.BytesIO(svg_stroke.encode("utf-8"))) | |
self.fg_img = pygame.image.load(io.BytesIO(svg_fill.encode("utf-8"))) | |
scale = min((self.HEIGHT / self.bg_img.get_height(), | |
self.WIDTH / self.bg_img.get_width())) | |
self.bg_img = pygame.transform.smoothscale( | |
self.bg_img, | |
(int(self.bg_img.get_width() * scale), int(self.bg_img.get_height() * scale)), | |
) | |
self.fg_img = pygame.transform.smoothscale( | |
self.fg_img, | |
(int(self.fg_img.get_width() * scale), int(self.fg_img.get_height() * scale)), | |
) | |
self.points = self.extract_points(self.bg_img, 20) | |
random.shuffle(self.points) | |
self.particles = pygame.sprite.Group() | |
for i in self.points: | |
self.particles.add(Particle(*i)) | |
def extract_points(self, image: pygame.Surface, modulo: int = 2) -> list[tuple[int, int]]: | |
points = [] | |
for x in range(0, image.get_width() // modulo * modulo, modulo): | |
for y in range(0, image.get_height() // modulo * modulo, modulo): | |
v = 0 | |
out = False | |
for i in range(modulo): | |
for j in range(modulo): | |
if sum(image.get_at((x + i, y + j))[0:3]): | |
points.append((x + i, y + j)) | |
out = True | |
break | |
if out: | |
break | |
return points | |
def run(self): | |
dt = 1 | |
clock = pygame.time.Clock() | |
do_run = True | |
self.prepare_imgs() | |
cnt = -50 | |
while do_run: | |
for event in pygame.event.get(): | |
if event.type == pygame.QUIT: | |
do_run = False | |
break | |
self.screen.fill((0, 0, 0)) | |
self.particles.update(dt=dt) | |
self.particles.draw(self.screen) | |
self.screen.blit(self.fg_img, (0, 0)) | |
pygame.display.flip() | |
dt = clock.tick(60) | |
if "__main__" == __name__: | |
f = TTFont("/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf") | |
pygame.init() | |
pgm = PGMFont(f) | |
pgm.prepare_window() | |
pgm.run() | |
pygame.quit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment