Skip to content
GitHub repository
Drawing

Shapes

Rects, squircles with per-corner radii, pills, circles, ellipses, hearts, stars, polygons and rounded triangles, each an exact anti-aliased distance field.

Updated pixi-silk 0.1.0

Every shape method adds a primitive to the active path. Nothing is drawn until you call fill() or stroke(), which paint everything on the path. Pixi’s Graphics works the same way.

Source src/demos/shapes.demo.ts
import { SilkGraphics } from 'pixi-silk';
import { defineDemo } from './runtime';

// Every shape is centred on (0, 0) and moved into its cell with the transform stack.
const SHAPES: { draw: (g: SilkGraphics) => SilkGraphics; color: number; open?: boolean }[] = [
    { draw: (g) => g.rect(-30, -22, 60, 44), color: 0x0a84ff },
    { draw: (g) => g.roundRect(-30, -22, 60, 44, 14, 0.6), color: 0x5e5ce6 },
    { draw: (g) => g.pill(-34, -14, 68, 28), color: 0xbf5af2 },
    { draw: (g) => g.circle(0, 0, 26), color: 0xff375f },
    { draw: (g) => g.ellipse(0, 0, 34, 20), color: 0xff9f0a },
    { draw: (g) => g.arc(0, 0, 26, -2.5, 0.8), color: 0xffd60a, open: true },
    { draw: (g) => g.sector(0, 0, 30, -2.2, 1.6, 14, 4), color: 0x30d158 },
    { draw: (g) => g.heart(0, 2, 56, 0.12), color: 0xff453a },
    { draw: (g) => g.star(0, 0, 5, 30, undefined, 0, 3), color: 0xffd60a },
    { draw: (g) => g.regularPoly(0, 0, 28, 6, 0, 5), color: 0x64d2ff },
    { draw: (g) => g.triangle(-30, 22, 30, 22, 0, -28, 6), color: 0xac8e68 },
    { draw: (g) => g.polyline([-34, 14, -12, -16, 10, 10, 34, -18]), color: 0xffffff, open: true },
];

export default defineDemo({
    size: [520, 250],
    controls: {
        outline: { type: 'toggle', label: 'outline', value: false },
        spin: { type: 'toggle', label: 'rotate', value: false },
    },
    setup({ stage, params, tick }) {
        const g = new SilkGraphics();

        stage.addChild(g);
        tick((t) => {
            g.clear();
            SHAPES.forEach((shape, i) => {
                g.save()
                    .translateTransform(50 + (i % 6) * 84, 65 + Math.floor(i / 6) * 120)
                    .rotateTransform(params.spin ? t * 0.6 : 0);
                const path = shape.draw(g);

                if (shape.open) path.stroke({ width: 5, cap: 'round', color: shape.color });
                else if (params.outline) path.stroke({ width: 2, color: shape.color });
                else path.fill(shape.color);
                g.restore();
            });
        });
    },
});

Rectangles and squircles#

g.rect(x, y, width, height);
g.roundRect(x, y, width, height, radius);                 // circular corners
g.roundRect(x, y, width, height, radius, 0.6);            // squircle: continuous curvature
g.roundRect(x, y, width, height, [24, 24, 8, 8]);         // per corner: tl, tr, br, bl
g.pill(x, y, width, height);                               // fully round ends

The smoothing argument (0 to 1) turns corners into superellipse arcs whose curvature grows gradually. App icons and cards on iOS have these corners. iOS uses about 0.6. A circular corner meets the straight side with a jump in curvature, which the eye reads as a slight kink. At the 45° point, a smoothed corner is exactly as deep as a circular corner of the same radius. So switching is a drop-in change.

Source src/demos/squircle.demo.ts
import { SilkGraphics } from 'pixi-silk';
import { defineDemo } from './runtime';

// A squircle keeps curvature continuous where the side meets the corner (iOS icons, cards).
export default defineDemo({
    size: [420, 220],
    controls: {
        radius: { type: 'range', label: 'radius', min: 0, max: 80, step: 1, value: 48, unit: 'px' },
        smoothing: { type: 'range', label: 'smoothing', min: 0, max: 1, step: 0.01, value: 0.6 },
        compare: { type: 'toggle', label: 'circular overlay', value: true },
    },
    setup({ stage, params, tick }) {
        const g = new SilkGraphics();

        stage.addChild(g);
        tick(() => {
            g.clear();
            g.roundRect(110, 20, 200, 180, params.radius, params.smoothing)
                .fill(0x2c2c2e)
                .stroke({ width: 2, color: 0x64d2ff });
            if (params.compare)
                g.roundRect(110, 20, 200, 180, params.radius).stroke({ width: 1, color: 0xff375f, alpha: 0.8 });
        });
    },
});
Pink is a circular corner of the same radius. Blue is the squircle.

Circles, ellipses and hearts#

g.circle(cx, cy, radius);
g.ellipse(cx, cy, radiusX, radiusY);          // exact ellipse distance (Newton solve, no approximation)
g.heart(cx, cy, width, rounding);             // rounding 0..0.3 puffs it up

Polygons#

g.regularPoly(cx, cy, radius, sides, rotation, cornerRadius);   // first vertex points up
g.star(cx, cy, points, radius, innerRadius, rotation, cornerRadius);
g.triangle(x0, y0, x1, y1, x2, y2, cornerRadius);

Rounded corners on polygons, stars and triangles are exact, not approximations.

Rotated shapes#

Shapes drawn inside a rotated transform stay exact, because the rotation is part of the primitive. See Transforms.

g.save().translateTransform(100, 100).rotateTransform(Math.PI / 6);
g.roundRect(-40, -20, 80, 40, 12, 0.6).fill(0x5e5ce6);
g.restore();

Fill and stroke together#

g.roundRect(0, 0, 200, 80, 20, 0.6).fill(0x1c1c1e).stroke({ width: 2, color: 0x3a3a3c });

Calling stroke() right after fill() merges both into one primitive. The shader splits each pixel’s coverage exactly between the fill and the stroke band. So there is never a seam or a halo between them. See Fills & strokes.

API reference: SilkGraphics