circle star square
♪The circle turns into a star, the star into a square…
A shape of 12 points smoothed into curves that endlessly turns from circle to star to square, about 2 s per change with a soft start and finish, in an indigo, violet and pink gradient — a 2D canvas that stays sharp on Retina screens, a loop that stops off screen, a still star when the user reduces motion. An interpolation algorithm, not artificial intelligence.
Updated
This effect is free — the Liquid category contains 31 effects total, including 6 free. Effect.Labs has 840 vanilla effects. Explore the category →
circle star square
♪The circle turns into a star, the star into a square…
23 works, 4 venues, one logo that never holds still.
12-15 March"Shapes" — Cie Pas de Côté
School performance, Thursday 10 am · 45 min
Video cue 3 · loop
The .fx-0411-root frame is 250 px tall and full width; the canvas fits it (display: block, 100% × 100%). The script reads its displayed size, multiplies it by devicePixelRatio for the internal size and draws in CSS pixels through setTransform: measured at density 2, a 1280 × 500 canvas for 640 × 250 displayed, hence sharp edges on Retina. A ResizeObserver redoes this when the frame changes size; the shape stays centered.
Each shape is a list of 12 points in relative coordinates (−0.5 to 0.5): the circle at 0.35 from the center, the star alternating 0.4 and 0.2 (six points), the square 0.6 wide. On each frame, progress moves by 0.008 and each point is placed between its position in the current shape and in the next one, along a 0.5 − 0.5 × cos(progress × π) curve: slow start, fast middle, slow finish. 125 frames per change, about 2 s at 60 frames per second. The outline goes through the middle between two neighboring points and uses each point as the control point of a quadratic curve: it is rounded everywhere and closes without a sharp edge. The size follows the frame's shorter side (Math.min(W, H)) and the linear gradient, laid diagonally over ±32% of that side, keeps all three colors visible at any size.
The circle and the star are described in the same direction, starting at 3 o'clock: that change is direct. The square starts at its top-left corner: between the star and the square, then between the square and the circle, each point crosses to its new side, the shape folds and turns a quarter turn before opening again. That fold is what gives the motion its organic feel; for a direct change, start the square at 3 o'clock like the other two (see the FAQ). The requestAnimationFrame loop stops as soon as an IntersectionObserver sees the canvas leave the screen (measured: 0 frames per second off screen against 120 on screen) and resumes when it comes back; in a hidden tab, the browser suspends it on its own.
prefers-reduced-motion: reduce, no loop starts; the canvas shows a still star, redrawn only if the frame changes size. Measured in Chromium 151, Firefox 153 and WebKit 26: 0% of pixels changed in 500 ms.aria-hidden="true" to the canvas if it is only decoration.progress from elapsed time rather than from the number of frames.2D canvas + 4.6 KB of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: same shape, same gradient, internal size doubled at density 2, stable height after 20 resizes, loop stopped off screen, still star under reduced motion. Measured cost: under 0.1 ms of JavaScript per frame in Chromium, around 1 ms in Firefox. ResizeObserver exists since Chrome 64, Firefox 69 and Safari 13.1; without it, the script listens to window resizing.
Without JavaScript, the frame stays empty on its dark blue-gray background. Without IntersectionObserver (browsers before 2019), the loop also runs off screen; without ResizeObserver, the canvas follows window resizing, not resizing of its own frame alone.
Copy the three blocks into your page. No dependencies.
<div class="fx-0411-root">
<div class="ai-shape-morph-container">
<canvas class="ai-shape-canvas"></canvas>
</div>
</div>
.fx-0411-root,
.fx-0411-root *,
.fx-0411-root *::before,
.fx-0411-root *::after {
box-sizing: border-box
}
.fx-0411-root {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(26, 26, 36);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.ai-shape-morph-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.ai-shape-morph-container .ai-shape-canvas {
display: block;
width: 100%;
height: 100%;
}
(function() {
// ============================================
// AI & Intelligence Effects
// ============================================
// 1. AI Shape Morph
// One instance per .fx-0411-root block: several copies can live on one page
document.querySelectorAll('.fx-0411-root').forEach(function(root) {
const canvas = root.querySelector('.ai-shape-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
// Reduced motion requested: a still star, no loop
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let W = 0,
H = 0;
// Internal size = displayed size × devicePixelRatio (sharp on Retina screens);
// drawing stays in CSS pixels thanks to setTransform.
function resize() {
const dpr = window.devicePixelRatio || 1;
W = canvas.clientWidth;
H = canvas.clientHeight;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
if (reduceMotion) draw();
}
const shapes = [
// Circle points
Array.from({
length: 12
}, (_, i) => {
const a = (i / 12) * Math.PI * 2;
return {
x: Math.cos(a) * 0.35,
y: Math.sin(a) * 0.35
};
}),
// Star points
Array.from({
length: 12
}, (_, i) => {
const a = (i / 12) * Math.PI * 2;
const r = i % 2 === 0 ? 0.4 : 0.2;
return {
x: Math.cos(a) * r,
y: Math.sin(a) * r
};
}),
// Square points
Array.from({
length: 12
}, (_, i) => {
const t = i / 12;
const side = Math.floor(t * 4);
const p = (t * 4) % 1;
const coords = [{
x: -0.3 + p * 0.6,
y: -0.3
},
{
x: 0.3,
y: -0.3 + p * 0.6
},
{
x: 0.3 - p * 0.6,
y: 0.3
},
{
x: -0.3,
y: 0.3 - p * 0.6
}
];
return coords[side];
})
];
let currentShape = reduceMotion ? 1 : 0;
let progress = 0;
let morphPoints = shapes[currentShape].map(p => ({
...p
}));
let raf = 0;
let visible = true;
function draw() {
const nextShape = (currentShape + 1) % shapes.length;
const eased = 0.5 - 0.5 * Math.cos(progress * Math.PI);
for (let i = 0; i < morphPoints.length; i++) {
morphPoints[i].x = shapes[currentShape][i].x + (shapes[nextShape][i].x - shapes[currentShape][i].x) * eased;
morphPoints[i].y = shapes[currentShape][i].y + (shapes[nextShape][i].y - shapes[currentShape][i].y) * eased;
}
ctx.clearRect(0, 0, W, H);
const cx = W / 2;
const cy = H / 2;
const scale = Math.min(W, H);
// Gradient scaled to the shape: ±80 px for a 250 px tall frame
const g = scale * 0.32;
const gradient = ctx.createLinearGradient(cx - g, cy - g, cx + g, cy + g);
gradient.addColorStop(0, '#6366f1');
gradient.addColorStop(0.5, '#a855f7');
gradient.addColorStop(1, '#ec4899');
// Start halfway along the last side: all 12 points act as control points,
// so the shape closes without a straight edge
const n = morphPoints.length;
ctx.beginPath();
ctx.moveTo(cx + (morphPoints[n - 1].x + morphPoints[0].x) / 2 * scale, cy + (morphPoints[n - 1].y + morphPoints[0].y) / 2 * scale);
for (let i = 0; i < n; i++) {
const curr = morphPoints[i];
const next = morphPoints[(i + 1) % n];
const cpx = cx + (curr.x + next.x) / 2 * scale;
const cpy = cy + (curr.y + next.y) / 2 * scale;
ctx.quadraticCurveTo(cx + curr.x * scale, cy + curr.y * scale, cpx, cpy);
}
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fill();
}
function animate() {
progress += 0.008;
if (progress >= 1) {
progress = 0;
currentShape = (currentShape + 1) % shapes.length;
}
draw();
// Off screen, the loop stops; the IntersectionObserver restarts it on return
raf = visible ? requestAnimationFrame(animate) : 0;
}
resize();
if ('ResizeObserver' in window) new ResizeObserver(resize).observe(canvas);
else window.addEventListener('resize', resize);
if (!reduceMotion) {
if ('IntersectionObserver' in window) {
new IntersectionObserver(function(entries) {
visible = entries[0].isIntersecting;
if (visible && !raf) raf = requestAnimationFrame(animate);
}).observe(canvas);
}
raf = requestAnimationFrame(animate);
}
});
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Change duration (JS — progress += 0.008) |
0.008 per frame, about 2 s at 60 frames per second | 0.004 gives about 4 s, 0.016 about 1 s. For a fixed duration in seconds on any screen, compute progress from the timestamp animate receives. |
Shape size (JS — const scale = Math.min(W, H)) |
the frame's shorter side | Math.min(W, H) * 1.3 enlarges the shape by 30% (the gradient follows). Beyond 1.4, the star's points touch the edges of a square frame. |
Colors (JS — the three addColorStop) |
#6366f1, #a855f7, #ec4899 |
Replace them with your colors; the gradient stays diagonal, from top left to bottom right. Two stops are enough for a simple gradient. |
Shapes (JS — the shapes array) |
circle, six-pointed star, square; 12 points each | Add a list of 12 points between −0.5 and 0.5 for a new shape, for instance a hexagon: r = 0.35 for even points, 0.3 for odd ones. All shapes must have the same number of points. |
Frame height (CSS — .fx-0411-root { height: 250px }) |
250 px | The canvas fits the frame and the shape follows the shorter side: 400 px gives a shape 1.6 times larger. |
Background (CSS — background of .fx-0411-root) |
rgb(26, 26, 36) |
The canvas is cleared on every frame: transparent lets the page's backdrop show behind the shape. |
Reduced-motion image (JS — reduceMotion ? 1 : 0) |
1, the star | 0 shows the circle, 2 the square. |
It is the effect's name in the catalog. The code contains no artificial intelligence model: it interpolates 12 points between three hand-written coordinate lists, with an easing curve. It all fits in about thirty readable lines.
The circle and the star number their points from 3 o'clock, the square from its top-left corner: each point crosses to another side, the shape folds and turns a quarter turn. For a direct change, start the square at 3 o'clock: replace its list with points taken on its sides at 0°, 30°, 60°… (r = 0.3 / Math.max(Math.abs(Math.cos(a)), Math.abs(Math.sin(a)))). Its corners will then be slightly rounded, since no point falls exactly on them.
Those two effects change shape on click, from one state to another. AI Shape Morph runs on its own, endlessly, and computes every frame on a canvas: no in-between shape is skipped, and there is nothing to click. Choose it for a living background or logo; the other two to answer a gesture.