LiquidFree

AI Shape Morph

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.

CanvasMorphingAnimation

Updated

Hover or click the scene to interact

This effect is free — the Liquid category contains 31 effects total, including 6 free. Effect.Labs has 840 vanilla effects. Explore the category →

Usage examples

The shapes song

circle star square

The circle turns into a star, the star into a square…

① The shapes song — an early-learning app for ages 2 to 4

WhenWhile the voice sings "circle… star… square", the shape changes in the middle of the screen and the word shows underneath, in large letters.
WhyThe shape never jumps from one state to the next: the child sees the circle become a star, which is the whole game, without having to touch the screen.
SettingsCream to pale pink background, transparent root; shape enlarged by 15%; the word under the shape follows the displayed shape (the scenes' copy exposes its number in data-shape, the caption changes halfway through).
Ondes Claires ProgrammeVenuesTickets
Digital arts festival · 6th edition

Ondes Claires

23 works, 4 venues, one logo that never holds still.

12-15 March
Thu 12 · InstallationsOld grain hall, 6 pm
Fri 13 · Live A/VThéâtre du Port, 9 pm
Sat 14 · WorkshopsMedia library, from 10 am

② A variable identity — the program header of a digital arts festival

WhenHeader of the online program: the logo is a shape that never holds still, next to the festival's name and the dates "12-15 March".
WhyA logo that keeps changing its outline while keeping the same material (the same gradient) is the very idea of a variable identity; the canvas computes it live, with no video to load.
Settings220 px square frame with 18 px corners on a violet glow, shape enlarged by 30% to fill the square; dates in a pill using the shape's gradient; three program events in the footer.

"Shapes" — Cie Pas de Côté
School performance, Thursday 10 am · 45 min

Video cue 3 · loop

③ A backdrop projection — a contemporary dance show for schools

WhenLarge screen behind the dancer of the show "Shapes"; the shape moves from one state to the next, echoing the body's figures, during the school performance.
WhyContinuous motion, with a soft start and finish, accompanies a choreography instead of punctuating it; the fold between star and square reads like a gesture.
SettingsScreen 82% wide and 60% tall on a black stage, violet glow around it; shape enlarged by 25%; wings, floor and a silhouette in shadow in front of the screen.

How it works

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.

Accessibility

  • Reduced motion: under 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.
  • Decorative role: the canvas has no text and no role. If it illustrates content (the word of a nursery song, a festival's name), write that content as text next to it, as in the examples, and add aria-hidden="true" to the canvas if it is only decoration.
  • Speed tied to the screen: progress is counted per frame. On a 120 Hz screen, a change lasts about 1 s instead of 2. If timing matters (syncing with a voice or music), compute progress from elapsed time rather than from the number of frames.
  • No interaction: nothing happens on click, keyboard or touch; no information is reserved for the mouse.

Browser compatibility

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.

Chrome 74+✓ Full
Firefox 69+✓ Full
Safari 13.1+✓ Full
Edge 79+✓ Full
Mobile iOS 13.4+✓ Full
Android Chrome 74+✓ Full

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.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="fx-0411-root">
  <div class="ai-shape-morph-container">
    <canvas class="ai-shape-canvas"></canvas>
  </div>
</div>
CSS
.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%;
}
JavaScript (fx-0411)
(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);
    }
  });
})();

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
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.

FAQ

Why "AI" in the name?

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.

Why does the shape fold between the star and the square?

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.

How does it differ from SVG Path Morph (fx-0407) and Shape Shifter (fx-0403)?

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.