
The curtain menu
Menus, Physics, WebGL, Transitions
NewFree
A sweet factory's menu wrapped like a candy in ruby foil, with its two ends twisted: pressed, the ends untwist turning opposite ways, the wrapper unrolls and lies flat keeping its crinkles, and the entries appear printed on it in cream. Under the pointer the foil ripples and glints; once an entry is chosen, the candy wraps itself up again with a bounce.
A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The candy wrapper menu</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full-italic.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the menu of a sweet shop, wrapped like a sweet: at rest it is a candy in ruby foil with its two ends twisted.
Pressed, the ends untwist the opposite ways, the wrapper unrolls and lies flat with its creases, and the menu
is printed on it. The foil is a WebGL mesh drawn by the script; the links below are the real menu, laid over
the flat wrapper -->
<div class="candy" data-candy>
<span class="candy__shadow" data-shadow aria-hidden="true"></span>
<canvas class="candy__gl" data-canvas aria-hidden="true"></canvas>
<button class="candy__toggle" type="button" aria-expanded="false" aria-controls="sweets-menu" data-toggle data-autoclic>
<span class="candy__word" data-name>Open the menu</span>
<svg class="candy__cross" viewBox="0 0 24 24" aria-hidden="true" data-cross><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></svg>
</button>
<nav class="candy__nav" id="sweets-menu" aria-label="Main" data-nav inert>
<ul class="candy__list">
<li><a class="sweet" href="#" data-item><span class="sweet__name">Bonbons</span><span class="sweet__note">Hand-wrapped, sold by the jar</span></a></li>
<li><a class="sweet" href="#" data-item><span class="sweet__name">Chocolate</span><span class="sweet__note">Bars, pralines and truffles</span></a></li>
<li><a class="sweet" href="#" data-item><span class="sweet__name">Gift tins</span><span class="sweet__note">Packed and posted for you</span></a></li>
<li><a class="sweet" href="#" data-item><span class="sweet__name">The factory</span><span class="sweet__note">Tours on Saturday mornings</span></a></li>
<li><a class="sweet" href="#" data-item><span class="sweet__name">Visit us</span><span class="sweet__note">The shop on Mill Lane</span></a></li>
</ul>
</nav>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.
27 values to change
With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:
Take “The candy wrapper menu” from Animorio and fit it into my siteThe paper is a grid of 140 by 88 points the script works out again at every frame, in a WebGL buffer. Each column of the grid is a strip going round the candy: its place along the candy goes from the wrapped one (body and ends) to the flat one, while the strip is bent along a circle.
Each point has its distance along the paper, s, and lies on the arc of a circle of curvature k: y = sin(ks)/k, z = (cos(ks) − 1)/k. With k = 2π over the length the strip is a closed circle; as k falls to zero it straightens keeping its length, like real paper. With back.out the curvature dips a little below zero and the paper bends the other way before it settles.
Everything else is a push along the arc’s normal: towards the axis where the candy narrows to the neck, in and out with a triangle wave for the gathered folds (PLEATS), a little for the wrinkles of the flat sheet. The twist slides s by an angle times the radius, growing along each end and opposite on the two sides: as the ends untwist the number goes to zero and the ends turn opposite ways.
The normals come from the grid (differences between neighbours). In the fragment shader a cellular noise gives every facet of the foil its own tilt, blended with its neighbours so the crinkles are crisp but not cut glass, and longer on the ends; the reflection of a small photo studio (a big soft light, a strip, ceiling and floor) colours the metal. The inside is silver paper, the closed folds darken, and the cream print is matt.
The frame, the shop’s name and the word Menu are on a texture drawn once on a canvas as big as the flat sheet; Menu is drawn squeezed the way the wrapping squeezes it, so on the candy it reads right, and it fades as the paper opens. The entries are real links over the flat sheet: they appear with a GSAP timeline at the end of the opening and, under the pointer, hand the shader the point the ripple starts from and the box of the glint.
| Name | File | Default | What it changes |
|---|---|---|---|
OPEN | script.js | 1.7 | The seconds of the whole unwrapping (wrapping up again is a little quicker). |
TWIST | script.js | 2.6 | How far each end is twisted, in radians. |
PLEATS | script.js | 11 | How many folds the paper makes, gathered round the ends. |
CRINKLE | script.js | 0.34 | How strong the crinkles of the foil are, the little facets that glint. |
WRINKLE | script.js | 1.8 | How high the wrinkles left on the flat sheet are, in pixels. |
RIPPLE | script.js | 2 | How high the ripple under the entry pointed at is, in pixels. |
SHINE | script.js | 1 | How bright the glint running over the foil is. |
TILT | script.js | [24, -12] | The degrees the paper leans back and turns while it unrolls. |
SETTLE | script.js | elastic.out(1, 0.42) | How the ends settle when they twist again: the bounce. |
For the site of a patisserie, a confectioner, a chocolate factory or a gift shop: wherever the menu can be a little sweet to unwrap. With four to six short entries; for a long menu the sheet is not enough.
The box is at most the screen minus 32 pixels wide and the sheet is worked out again at every change of width. A tap opens and closes; without a pointer the candy does not roll while waiting, and the entry tapped ripples and glints before the paper wraps up.
No twisting or unrolling: the candy and the flat sheet swap in a fade, the entries appear without moving, the foil does not ripple (the glint, being only light, stays).
GSAP 3.15.0, Fraunces 5.3.0.
Compressed with gzip, the three demo files weigh 13 KB; the libraries: GSAP 28 KB.
Tested on 9 October 2026
Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.