Skip to content

Instantly share code, notes, and snippets.

@petrblahos
Created December 15, 2023 13:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save petrblahos/aff7e60f4e9d95f0d9d2d6ad0ccdbdec to your computer and use it in GitHub Desktop.
Save petrblahos/aff7e60f4e9d95f0d9d2d6ad0ccdbdec to your computer and use it in GitHub Desktop.
A bubble effect under a writing.
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