Skip to main content

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.

Shape morphing demo

"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

[!NOTE] See /examples for more implementation demos.

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

GPL-3.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

expressive_shapes-0.2.0.tar.gz (40.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

expressive_shapes-0.2.0-py3-none-any.whl (43.8 kB view details)

Uploaded Python 3

File details

Details for the file expressive_shapes-0.2.0.tar.gz.

File metadata

  • Download URL: expressive_shapes-0.2.0.tar.gz
  • Upload date:
  • Size: 40.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for expressive_shapes-0.2.0.tar.gz
Algorithm Hash digest
SHA256 8c463bba3b1c00fdb4bc6a18b4cdf08f39017bfaf2cb30b74776fe708c38d286
MD5 bb6826df9c9f298fca2e0f3ed57eceff
BLAKE2b-256 a67589f2fe6a978888b1c900d8b4a48d0d226f4f170c3645bfacd6c70f1b1f9a

See more details on using hashes here.

Provenance

The following attestation bundles were made for expressive_shapes-0.2.0.tar.gz:

Publisher: publish.yml on amansxcalibur/expressive-shapes

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file expressive_shapes-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for expressive_shapes-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f944ea7127a443bd8ceaa4194c50ad1199ec38c7f69daeb30d6b3d6c096cf4af
MD5 bbd5327924d29fff9fd3a301e1da9247
BLAKE2b-256 1a85db4829b3484d49b2cefcbe079f4805070c411572a245c2565b75c58bb872

See more details on using hashes here.

Provenance

The following attestation bundles were made for expressive_shapes-0.2.0-py3-none-any.whl:

Publisher: publish.yml on amansxcalibur/expressive-shapes

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page