Expressive shape morphing and animations
Project description
expressive-shapes
A pure-Python library for creating, rounding, and morphing polygons using cubic Bezier curves. Inspired by Android's Material Design shape system, brought to Python.
"I like the dots and shapes, and I think they like me too"
Features
- Rounded polygons -- Create polygons with per-vertex corner rounding and smoothing, matching Material Design 3 specifications
- Shape morphing -- Smoothly interpolate between any two shapes using feature-aware Bezier matching
- 30+ built-in presets -- Circle, star, heart, clover, ghost, pixel shapes, and more
- Renderer-agnostic -- Outputs cubic Bezier curves; render with Cairo, SVG, Canvas, or any path-based renderer
- Zero dependencies -- Pure Python, no external packages required
Installation
pip install expressive-shapes
Quick Start
Create a rounded polygon
Vertices are defined as a flat list of [x, y, x, y, ...]. The built-in presets use unit coordinates (0.0--1.0), which keeps shapes resolution-independent -- you scale at render time. But you're free to use whatever coordinate space fits your setup (pixel coords, world units, etc.).
from expressive_shapes import RoundedPolygon, CornerRounding
# unit-coordinate triangle (0.0-1.0)
poly = RoundedPolygon.create(
vertices=[0.5, 0.1, 0.9, 0.9, 0.1, 0.9],
rounding=CornerRounding(radius=0.08, smoothing=0.6),
)
# get cubic Bezier curves to render with any graphics library
for curve in poly.get_all_curves():
print(curve.p0, curve.p1, curve.p2, curve.p3)
Morph between two shapes
from expressive_shapes import RoundedPolygon, CornerRounding
from expressive_shapes.morph.bezier_morph import Morph
poly_a = RoundedPolygon.create(
vertices=[0.5, 0.1, 0.9, 0.9, 0.1, 0.9], # triangle
rounding=CornerRounding(radius=0.07, smoothing=0.0),
)
poly_b = RoundedPolygon.create(
vertices=[0.2, 0.2, 0.8, 0.2, 0.8, 0.8, 0.2, 0.8], # square
rounding=CornerRounding(radius=0.07, smoothing=0.0),
)
# match features between the two shapes (done once)
matched = Morph.match(poly_a, poly_b)
# interpolate at any progress value between 0.0 and 1.0
curves = Morph.as_cubics(matched, progress=0.5) # halfway morph
Use built-in shape presets
Presets are defined in unit coordinates. Scale them to your target size when creating the polygon, or let your renderer handle it via a transform.
from expressive_shapes import RoundedPolygon
from expressive_shapes.shapes.shape_presets import star, heart, circle
def preset_to_polygon(preset):
# convert a unit-coordinate preset to a RoundedPolygon
verts = []
per_vertex = []
for (ux, uy), rounding in preset:
verts.extend([ux, uy])
per_vertex.append(rounding)
return RoundedPolygon.create(vertices=verts, per_vertex_rounding=per_vertex)
star_poly = preset_to_polygon(star)
heart_poly = preset_to_polygon(heart)
Render with Cairo
Since shapes are in unit coordinates, use ctx.scale() to map them to your output size. This is the typical approach for Cairo/SVG/Canvas-style renderers, but you can just as easily pre-multiply your vertices if your pipeline expects pixel coordinates.
import cairo
from expressive_shapes.morph.bezier_morph import Morph
# create poly_a and poly_b in unit coords as above
matched = Morph.match(poly_a, poly_b)
curves = Morph.as_cubics(matched, progress=0.35)
size = 500
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size)
ctx = cairo.Context(surface)
# scale unit coords (0.0-1.0) up to the output size
ctx.scale(size, size)
ctx.move_to(curves[0].p0.x, curves[0].p0.y)
for c in curves:
ctx.curve_to(c.p1.x, c.p1.y, c.p2.x, c.p2.y, c.p3.x, c.p3.y)
ctx.close_path()
ctx.set_source_rgb(0.24, 0.52, 0.93)
ctx.fill()
surface.write_to_png("morph.png")
API Overview
| Class | Description |
|---|---|
RoundedPolygon |
A polygon with per-vertex rounding, represented as cubic Bezier features |
CornerRounding |
Controls corner radius and smoothing (0.0 = circular arc, 1.0 = fully smoothed) |
Morph |
Feature-aware shape matching and interpolation |
Point |
2D point with arithmetic operations and interpolation |
Cubic |
A cubic Bezier segment (p0, p1, p2, p3) |
Feature |
A polygon segment -- either a "corner" or an "edge", containing one or more Cubic curves |
Shape Presets
The shapes.shape_presets module includes 30+ ready-to-use shapes defined in unit coordinates (0.0-1.0):
circle, square, triangle, diamond, pentagon, star, heart, arrow, shield, pill, arch, semicircle, oval, fan, gem, clamshell, cookie_8, cookie_12, four_leaf_clover, boom, ghost_ish, bun, pixel_circle, pixel_triangle, puffy_square, puffy_diamond, concave_rectangle, slanted, and more.
Each preset is a list of ((x, y), CornerRounding) tuples that can be scaled to any size.
Debugging
Debug output is available behind a flag:
import expressive_shapes
expressive_shapes.DEBUG = True
This enables detailed output for shape matching, feature alignment, and the morph walking algorithm.
Requirements
- Python >= 3.9
- No runtime dependencies
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file expressive_shapes-0.1.0.tar.gz.
File metadata
- Download URL: expressive_shapes-0.1.0.tar.gz
- Upload date:
- Size: 39.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff550e7e884e16b31a7be32adabf30277ec56accbbe6287ca9ceb1f7d3db65f8
|
|
| MD5 |
900454e3b27cc39582947cc92ad479f1
|
|
| BLAKE2b-256 |
9f56b18cc5dd636b9c940a4d723a37dcb9c183bb032bbe8f87a4bf4f5e129a7c
|
Provenance
The following attestation bundles were made for expressive_shapes-0.1.0.tar.gz:
Publisher:
publish.yml on amansxcalibur/expressive-shapes
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
expressive_shapes-0.1.0.tar.gz -
Subject digest:
ff550e7e884e16b31a7be32adabf30277ec56accbbe6287ca9ceb1f7d3db65f8 - Sigstore transparency entry: 1085281225
- Sigstore integration time:
-
Permalink:
amansxcalibur/expressive-shapes@6aac4454c8f93be7badaf02feb972599c2def2fe -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/amansxcalibur
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6aac4454c8f93be7badaf02feb972599c2def2fe -
Trigger Event:
release
-
Statement type:
File details
Details for the file expressive_shapes-0.1.0-py3-none-any.whl.
File metadata
- Download URL: expressive_shapes-0.1.0-py3-none-any.whl
- Upload date:
- Size: 42.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9762724ea9333ae7f584ee157de49fed5dda45c90193c49f2f9e334a92434ff7
|
|
| MD5 |
5fb5e6c9dd5a2f1a687f9de922da0f47
|
|
| BLAKE2b-256 |
293d0a1bbc1c2f324029cfac29f4ac9132999547fe6cfe7c993f696fa939435e
|
Provenance
The following attestation bundles were made for expressive_shapes-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on amansxcalibur/expressive-shapes
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
expressive_shapes-0.1.0-py3-none-any.whl -
Subject digest:
9762724ea9333ae7f584ee157de49fed5dda45c90193c49f2f9e334a92434ff7 - Sigstore transparency entry: 1085281307
- Sigstore integration time:
-
Permalink:
amansxcalibur/expressive-shapes@6aac4454c8f93be7badaf02feb972599c2def2fe -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/amansxcalibur
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6aac4454c8f93be7badaf02feb972599c2def2fe -
Trigger Event:
release
-
Statement type: