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, slanted, arch, semicircle, oval, pill, triangle, arrow, fan, diamond, clamshell, pentagon, star, gem, sunny, very_sunny, cookie_4, cookie_8, cookie_12, leaf_clover_4, leaf_clover_8, boom, puffy_diamond, flower, ghost_ish, pixel_circle, pixel_triangle, bun, heart, organic_blob, shield, 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
Metadata
Release files for expressive-shapes 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| expressive_shapes-0.2.0.tar.gz | 40.4 kB | With provenance | Details for expressive_shapes-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| expressive_shapes-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for expressive_shapes-0.2.0-py3-none-any.whl |
Total release size: 84.2 kB
Release files / expressive_shapes-0.2.0.tar.gz
| Download URL | expressive_shapes-0.2.0.tar.gz |
|---|---|
| Size | 40.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8c463bba3b1c00fdb4bc6a18b4cdf08f39017bfaf2cb30b74776fe708c38d286
|
|
BLAKE2b-256 checksum How to use checksums |
a67589f2fe6a978888b1c900d8b4a48d0d226f4f170c3645bfacd6c70f1b1f9a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 14, 2026.
Transparency logRelease files / expressive_shapes-0.2.0-py3-none-any.whl
| Download URL | expressive_shapes-0.2.0-py3-none-any.whl |
|---|---|
| Size | 43.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f944ea7127a443bd8ceaa4194c50ad1199ec38c7f69daeb30d6b3d6c096cf4af
|
|
BLAKE2b-256 checksum How to use checksums |
1a85db4829b3484d49b2cefcbe079f4805070c411572a245c2565b75c58bb872
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 14, 2026.
Transparency logSecurity for expressive-shapes 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for expressive_shapes-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 14, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.