
The loader that becomes a logo
Transitions, SVG, Text
NewFree
A period door in a corridor at night, with a brass lock and the key hanging on its hook. On click the key goes into the lock and turns with two clicks, the light of the room comes through the hole and the view dives into it: the keyhole opens until it fills the screen and shows the room behind. The next click comes back out through the keyhole of another door.
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, viewport-fit=cover">
<title>Through the keyhole</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/instrument-sans@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage" data-stage>
<!-- the corridor: the wall, the floor and a period door with its brass lock and the key on its hook.
The three doors of the walk share this drawing: data-door picks the colours and the panels -->
<div class="hall" data-hall>
<svg class="door" viewBox="0 0 2400 800" preserveAspectRatio="xMidYMid slice" data-door="0" data-door-svg aria-hidden="true">
<defs>
<pattern id="paper" width="60" height="60" patternUnits="userSpaceOnUse">
<rect class="wp-base" width="60" height="60"/>
<rect class="wp-line" x="0" width="2" height="60"/>
<path class="wp-motif" d="M30 22l6 8-6 8-6-8z"/>
</pattern>
<linearGradient id="brass" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#f3d38a"/>
<stop offset="0.45" stop-color="#c79443"/>
<stop offset="1" stop-color="#7e5622"/>
</linearGradient>
<radialGradient id="knob" cx="0.38" cy="0.34" r="0.7">
<stop offset="0" stop-color="#fff0c2"/>
<stop offset="0.35" stop-color="#d9a957"/>
<stop offset="1" stop-color="#6e4a1c"/>
</radialGradient>
<radialGradient id="warm" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" class="warm-a"/>
<stop offset="1" class="warm-b"/>
</radialGradient>
<radialGradient id="vignette" cx="0.5" cy="0.52" r="0.62">
<stop offset="0.45" stop-color="#000" stop-opacity="0"/>
<stop offset="1" stop-color="#000" stop-opacity="0.62"/>
</radialGradient>
<clipPath id="plaqueClip">
<rect x="1142" y="173" width="116" height="25" rx="3"/>
</clipPath>
<linearGradient id="floorShade" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#000" stop-opacity="0.45"/>
<stop offset="1" stop-color="#000" stop-opacity="0"/>
</linearGradient>
</defs>
<rect class="wall" width="2400" height="700" fill="url(#paper)"/>
<!-- the two wall lights of the corridor -->
<g class="sconce">
<ellipse cx="880" cy="300" rx="190" ry="190" fill="url(#warm)" class="sconce__glow"/>
<ellipse cx="1520" cy="300" rx="190" ry="190" fill="url(#warm)" class="sconce__glow"/>
<path class="sconce__arm" d="M880 352v-30M868 352h24M1520 352v-30M1508 352h24"/>
<path class="sconce__shade" d="M858 322l8-46h28l8 46zM1498 322l8-46h28l8 46z"/>
</g>
<rect class="skirting" y="664" width="2400" height="36"/>
<rect class="floor" y="700" width="2400" height="100"/>
<rect y="700" width="2400" height="60" fill="url(#floorShade)"/>
<path class="plank" d="M0 724H2400M0 752H2400M0 784H2400"/>
<!-- the light of the room, under the door -->
<ellipse cx="1200" cy="712" rx="190" ry="22" fill="url(#warm)" class="underglow"/>
<!-- the frame round the door, and the crowns of the second and third doors -->
<g class="v1">
<path class="frame" d="M1036 140C1036 50 1364 50 1364 140z"/>
<path class="fan" d="M1064 140C1064 76 1336 76 1336 140z"/>
<path class="fan__bars" d="M1200 140V82M1200 140L1118 92M1200 140L1282 92M1200 140L1080 116M1200 140L1320 116"/>
</g>
<g class="v2">
<path class="frame" d="M1012 136L1200 70 1388 136z"/>
<path class="frame-lo" d="M1050 130L1200 84 1350 130z"/>
</g>
<rect class="door-shadow" x="1028" y="132" width="344" height="574"/>
<rect class="frame" x="1036" y="136" width="328" height="564"/>
<path class="frame-lo" d="M1048 700V148H1352V700"/>
<!-- the door leaf -->
<g data-leaf class="leaf-hit">
<rect class="leaf" x="1070" y="170" width="260" height="530"/>
<path class="leaf-edge" d="M1070 700V170H1330"/>
<!-- four panels: the hotel -->
<g class="v0">
<rect class="panel" x="1106" y="204" width="80" height="200"/>
<rect class="panel" x="1214" y="204" width="80" height="200"/>
<rect class="panel" x="1106" y="476" width="80" height="190"/>
<rect class="panel" x="1214" y="476" width="80" height="190"/>
<rect class="panel-in" x="1118" y="216" width="56" height="176"/>
<rect class="panel-in" x="1226" y="216" width="56" height="176"/>
<rect class="panel-in" x="1118" y="488" width="56" height="166"/>
<rect class="panel-in" x="1226" y="488" width="56" height="166"/>
</g>
<!-- two long panels with arched heads: the gallery -->
<g class="v1">
<path class="panel" d="M1106 404V262C1106 210 1294 210 1294 262V404z"/>
<path class="panel-in" d="M1120 390V266C1120 226 1280 226 1280 266V390z"/>
<rect class="panel" x="1106" y="476" width="188" height="190"/>
<rect class="panel-in" x="1120" y="490" width="160" height="162"/>
</g>
<!-- six panels: the winter garden -->
<g class="v2">
<rect class="panel" x="1106" y="204" width="80" height="64"/>
<rect class="panel" x="1214" y="204" width="80" height="64"/>
<rect class="panel" x="1106" y="284" width="80" height="120"/>
<rect class="panel" x="1214" y="284" width="80" height="120"/>
<rect class="panel" x="1106" y="476" width="80" height="190"/>
<rect class="panel" x="1214" y="476" width="80" height="190"/>
<rect class="panel-in" x="1116" y="214" width="60" height="44"/>
<rect class="panel-in" x="1224" y="214" width="60" height="44"/>
<rect class="panel-in" x="1116" y="294" width="60" height="100"/>
<rect class="panel-in" x="1224" y="294" width="60" height="100"/>
<rect class="panel-in" x="1116" y="486" width="60" height="170"/>
<rect class="panel-in" x="1224" y="486" width="60" height="170"/>
</g>
<!-- the brass plate with the room's name -->
<g class="plaque" data-plaque>
<rect x="1142" y="173" width="116" height="25" rx="3" fill="url(#brass)"/>
<rect class="plaque__line" x="1145.5" y="176.5" width="109" height="18" rx="1.5"/>
<text class="plaque__text" x="1200" y="189.6" text-anchor="middle" data-plaque-text>Blue Hour Suite</text>
<g clip-path="url(#plaqueClip)"><path class="plaque__glint" d="M1130 173h10l-8 25h-10z" data-glint/></g>
</g>
<!-- the light of the room on the door, round the keyhole, once the lock gives -->
<ellipse cx="1310" cy="470" rx="60" ry="60" fill="url(#warm)" class="spill" data-spill/>
<!-- the little hook the key hangs from -->
<g class="hook">
<circle cx="1240" cy="440" r="4.6" fill="url(#knob)"/>
<path d="M1240 441v8a5.2 5.2 0 0 0 10.4 0v-3"/>
<circle cx="1245.2" cy="454" r="0.1" data-hook/>
</g>
<!-- the lock: the long backplate, the knob and the keyhole -->
<g class="lock" data-lock>
<rect x="1296" y="374" width="28" height="134" rx="14" fill="url(#brass)"/>
<rect class="lock__line" x="1299.5" y="377.5" width="21" height="127" rx="10.5"/>
<circle cx="1310" cy="396" r="14" fill="url(#knob)"/>
<ellipse class="lock__shine" cx="1305" cy="391" rx="4.5" ry="3"/>
<path class="keyhole" d="M1307.8 474.9A5.5 5.5 0 1 1 1312.2 474.9L1313.8 485H1306.2z"/>
<circle cx="1310" cy="470" r="0.1" data-keyhole/>
<path class="ticks" d="M1290 461l-9-4M1289 470h-10M1290 479l-9 4M1330 461l9-4M1331 470h10M1330 479l9 4" pathLength="1" data-ticks/>
</g>
</g>
<rect width="2400" height="800" fill="url(#vignette)" pointer-events="none"/>
</svg>
<!-- the name of the place, top left: it changes with the door -->
<p class="where" data-where>
<span class="where__place" data-where-place>Hotel Marlowe Vane</span>
<span class="where__floor" data-where-floor>Third floor, east corridor</span>
</p>
</div>
<!-- the dark inside the lock, while the view goes through it -->
<div class="veil" data-veil></div>
<!-- the rooms behind the doors: only the keyhole's shape of them shows, until it opens to the whole screen -->
<div class="room" data-room>
<div class="room__inner" data-room-inner>
<!-- the hotel: a suite at the blue hour -->
<section class="scene" data-scene="0" aria-label="The Blue Hour Suite, Hotel Marlowe Vane">
<svg class="scene__art" viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<defs>
<linearGradient id="r0sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#18234a"/>
<stop offset="0.55" stop-color="#3a4d8c"/>
<stop offset="1" stop-color="#e79a5a"/>
</linearGradient>
<radialGradient id="r0lamp" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#ffd58c" stop-opacity="0.75"/>
<stop offset="1" stop-color="#ffd58c" stop-opacity="0"/>
</radialGradient>
<pattern id="r0paper" width="44" height="44" patternUnits="userSpaceOnUse">
<rect width="44" height="44" fill="#1e2b50"/>
<rect width="3" height="44" fill="#24345e"/>
</pattern>
</defs>
<rect width="1600" height="1000" fill="url(#r0paper)"/>
<!-- the window on the harbour -->
<path d="M610 520V280a190 190 0 0 1 380 0v240z" fill="url(#r0sky)"/>
<g class="r-drift">
<circle cx="872" cy="262" r="40" fill="#f8e8c2"/>
<circle cx="886" cy="252" r="9" fill="#ead6a6"/>
<circle cx="858" cy="278" r="6" fill="#ead6a6"/>
</g>
<path class="r-pop" d="M700 214l4 12 12 4-12 4-4 12-4-12-12-4 12-4zM760 300l3 8 8 3-8 3-3 8-3-8-8-3 8-3zM668 330l2.5 7 7 2.5-7 2.5-2.5 7-2.5-7-7-2.5 7-2.5z" fill="#f8e8c2"/>
<path d="M610 520V452h40v-22h26v22h34v-40h18v-16h10v16h22v28h50v-34h30v-18h12v18h20v40h40v-26h34v18h44v62z" fill="#121a36"/>
<path d="M686 470h8v10h-8zM760 456h8v10h-8zM846 466h8v10h-8zM922 472h8v10h-8z" fill="#f3c46b" class="r-glow"/>
<path d="M610 520V280a190 190 0 0 1 380 0v240zM800 92v428M610 380h380" fill="none" stroke="#e8dcc2" stroke-width="12"/>
<rect x="588" y="518" width="424" height="22" fill="#d9caa9"/>
<!-- the curtains -->
<path class="r-sway" data-o="560 110" d="M520 110h120c-8 120 18 250 4 360-10 80 10 120 30 150H520z" fill="#8a2e3b"/>
<path class="r-sway" data-o="560 110" d="M560 110c4 140-10 300 0 510" fill="none" stroke="#6c2230" stroke-width="5"/>
<path class="r-sway" data-o="1040 110" d="M1080 110H960c8 120-18 250-4 360 10 80-10 120-30 150h154z" fill="#8a2e3b"/>
<path class="r-sway" data-o="1040 110" d="M1040 110c-4 140 10 300 0 510" fill="none" stroke="#6c2230" stroke-width="5"/>
<rect x="500" y="96" width="600" height="16" rx="8" fill="#c9a24e"/>
<!-- a little picture of a boat -->
<g class="r-swing" data-o="1230 250">
<path d="M1230 250l-52 46M1230 250l52 46" stroke="#c9a24e" stroke-width="2" fill="none"/>
<rect x="1166" y="294" width="128" height="104" fill="#c9a24e"/>
<rect x="1176" y="304" width="108" height="84" fill="#f0e3c6"/>
<path d="M1196 362h68l-10 14h-48zM1230 314v46l26-4z" fill="#3a4d8c"/>
</g>
<!-- the floor and the rug -->
<rect y="860" width="1600" height="140" fill="#1d1628"/>
<rect y="846" width="1600" height="16" fill="#2c2340"/>
<ellipse cx="800" cy="940" rx="420" ry="54" fill="#5b2c42"/>
<ellipse cx="800" cy="940" rx="384" ry="40" fill="none" stroke="#c9a24e" stroke-width="3" stroke-dasharray="14 10"/>
<!-- the armchair -->
<path d="M404 860V690c0-40 30-60 76-60s76 20 76 60v170z" fill="#7b3342"/>
<rect x="388" y="740" width="184" height="80" rx="26" fill="#93404f"/>
<path d="M412 820v40M548 820v40" stroke="#2a1a14" stroke-width="10"/>
<!-- the bed -->
<path d="M640 720V600c0-50 320-50 320 0v120z" fill="#4b2a46"/>
<path d="M680 604v100M740 590v114M800 586v118M860 590v114M920 604v100" stroke="#3c2038" stroke-width="3"/>
<rect x="620" y="690" width="360" height="70" rx="12" fill="#efe6d6"/>
<rect class="r-pop" x="650" y="650" width="130" height="54" rx="20" fill="#f7f1e6"/>
<rect class="r-pop" x="820" y="650" width="130" height="54" rx="20" fill="#f7f1e6"/>
<path d="M606 740h388l14 120H592z" fill="#c4964f"/>
<path d="M606 740h388l4 30H602z" fill="#d8ad66"/>
<path d="M640 800h320M630 830h340" stroke="#a87c3c" stroke-width="3"/>
<!-- the bedside table and the lamp -->
<rect x="1010" y="740" width="110" height="120" rx="6" fill="#3a2618"/>
<rect x="1022" y="770" width="86" height="6" rx="3" fill="#c9a24e"/>
<ellipse class="r-glow" cx="1065" cy="650" rx="250" ry="250" fill="url(#r0lamp)"/>
<path d="M1050 740h30l-6-14h-18zM1063 726v-70" stroke="#c9a24e" stroke-width="5" fill="#c9a24e"/>
<path class="r-glow" d="M1028 668l16-58h42l16 58z" fill="#f6cd84"/>
</svg>
<div class="scene__text">
<p class="scene__place">Hotel Marlowe Vane</p>
<h2 class="scene__title" data-split>The Blue Hour Suite</h2>
<p class="scene__line">Third floor, a window on the old harbour</p>
</div>
</section>
<!-- the exhibition: weathers under glass -->
<section class="scene" data-scene="1" aria-label="The Cabinet of Small Weathers, The Ferrow Gallery">
<svg class="scene__art" viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<defs>
<linearGradient id="r1cone" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fffaf0" stop-opacity="0.9"/>
<stop offset="1" stop-color="#fffaf0" stop-opacity="0"/>
</linearGradient>
<pattern id="r1floor" width="80" height="40" patternUnits="userSpaceOnUse">
<rect width="80" height="40" fill="#b27d4d"/>
<path d="M0 40L40 0M40 40L80 0" stroke="#9c6a3e" stroke-width="3"/>
</pattern>
</defs>
<rect width="1600" height="1000" fill="#e9e2d4"/>
<rect width="1600" height="84" fill="#d8cfbd"/>
<path d="M0 140H1600" stroke="#d3c9b5" stroke-width="4"/>
<!-- the spotlights -->
<path class="r-glow" d="M790 84h20l110 480H680zM1170 84h20l130 560H1040z" fill="url(#r1cone)"/>
<path d="M792 84h16v18h-16zM1172 84h16v18h-16z" fill="#2c2924"/>
<!-- the two framed weathers on the walls: a sun and a wind -->
<g class="r-swing" data-o="420 290">
<path d="M420 290l-90 38M420 290l90 38" stroke="#8b8172" stroke-width="2" fill="none"/>
<rect x="300" y="326" width="240" height="300" fill="#2b2723"/>
<rect x="316" y="342" width="208" height="268" fill="#f7f3ea"/>
<circle cx="420" cy="460" r="46" fill="#e2a03f"/>
<path class="r-draw" pathLength="1" d="M420 392v-26M420 528v26M352 460h-26M488 460h26M372 412l-18-18M468 508l18 18M468 412l18-18M372 508l-18 18" stroke="#e2a03f" stroke-width="6" stroke-linecap="round" fill="none"/>
</g>
<g class="r-swing" data-o="1180 140">
<path d="M1180 140l-90 108M1180 140l90 108" stroke="#8b8172" stroke-width="2" fill="none"/>
<rect x="1060" y="246" width="240" height="300" fill="#2b2723"/>
<rect x="1076" y="262" width="208" height="268" fill="#f7f3ea"/>
<path class="r-draw" pathLength="1" d="M1110 350c40-30 90 30 140 0M1110 400c50-30 100 30 150 0M1110 450c40-30 80 30 120 0" stroke="#4b6f8c" stroke-width="7" stroke-linecap="round" fill="none"/>
</g>
<path d="M370 652h100M370 666h70M1130 572h100M1130 586h70" stroke="#a79d8c" stroke-width="5"/>
<!-- the floor -->
<rect y="800" width="1600" height="22" fill="#d5cbb6"/>
<rect y="822" width="1600" height="178" fill="url(#r1floor)"/>
<!-- the plinth and the bell jar with its little cloud -->
<ellipse cx="800" cy="826" rx="150" ry="16" fill="#000" opacity="0.18"/>
<rect x="700" y="566" width="200" height="260" fill="#f9f7f2"/>
<rect x="862" y="566" width="38" height="260" fill="#e4dfd3"/>
<rect x="690" y="556" width="220" height="14" fill="#ffffff"/>
<ellipse cx="800" cy="552" rx="96" ry="13" fill="#6b4a33"/>
<g class="r-bob">
<path class="r-draw" pathLength="1" d="M760 470l-8 30M790 476l-8 30M820 470l-8 30M846 478l-8 30" stroke="#5d8bb0" stroke-width="5" stroke-linecap="round" fill="none"/>
<path class="r-pop" d="M742 458c-22 0-30-28-10-38-2-24 26-36 44-22 10-22 46-22 54 4 22-6 38 18 26 34 12 14 0 30-16 30z" fill="#ffffff" stroke="#dfe4ea" stroke-width="4"/>
</g>
<path d="M716 552V398a84 84 0 0 1 168 0v154" fill="#d9ecf2" fill-opacity="0.3" stroke="#ffffff" stroke-width="4"/>
<path d="M738 520V404c0-30 12-52 34-64" fill="none" stroke="#ffffff" stroke-width="8" stroke-linecap="round" opacity="0.7"/>
<circle cx="800" cy="308" r="9" fill="#6b4a33"/>
<!-- the velvet rope -->
<path d="M560 700v150M1040 700v150" stroke="#b88a3c" stroke-width="12"/>
<ellipse cx="560" cy="852" rx="26" ry="8" fill="#b88a3c"/>
<ellipse cx="1040" cy="852" rx="26" ry="8" fill="#b88a3c"/>
<circle cx="560" cy="696" r="12" fill="#d8a94f"/>
<circle cx="1040" cy="696" r="12" fill="#d8a94f"/>
<path class="r-draw" pathLength="1" d="M560 706C700 800 900 800 1040 706" stroke="#8e1f2b" stroke-width="10" stroke-linecap="round" fill="none"/>
</svg>
<div class="scene__text">
<p class="scene__place">The Ferrow Gallery</p>
<h2 class="scene__title" data-split>The Cabinet of Small Weathers</h2>
<p class="scene__line">Tiny climates under glass, until 14 March</p>
</div>
</section>
<!-- the hotel's winter garden -->
<section class="scene" data-scene="2" aria-label="The Winter Garden, Hotel Ostrander">
<svg class="scene__art" viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<defs>
<linearGradient id="r2glass" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#cfe2dd"/>
<stop offset="1" stop-color="#f3efdb"/>
</linearGradient>
<pattern id="r2tiles" width="80" height="80" patternUnits="userSpaceOnUse">
<rect width="80" height="80" fill="#ece3cd"/>
<path d="M0 0h40v40H0zM40 40h40v40H40z" fill="#2f3d37"/>
</pattern>
</defs>
<rect width="1600" height="800" fill="url(#r2glass)"/>
<!-- the snowy hills beyond the glass -->
<path d="M0 690c160-60 280-50 400-20 180-80 340-70 520-10 160-70 340-80 480-10 100-40 160-40 200-30v180H0z" fill="#ffffff" opacity="0.85"/>
<path d="M0 740c200-50 400-40 600 0 200-40 420-50 600 0 160-30 300-30 400-10v70H0z" fill="#e6efec"/>
<!-- the iron of the glasshouse -->
<path d="M0 800V200M400 800V200M800 800V200M1200 800V200M1600 800V200M0 200a200 150 0 0 1 400 0M400 200a200 150 0 0 1 400 0M800 200a200 150 0 0 1 400 0M1200 200a200 150 0 0 1 400 0M0 440H1600M200 800V50M600 800V50M1000 800V50M1400 800V50" stroke="#2f3d37" stroke-width="10" fill="none"/>
<rect y="800" width="1600" height="200" fill="url(#r2tiles)"/>
<rect y="796" width="1600" height="10" fill="#2f3d37"/>
<!-- a fern hanging from the roof -->
<g class="r-swing" data-o="560 0">
<path d="M560 0v210" stroke="#2f3d37" stroke-width="3"/>
<path d="M520 210h80l-12 50h-56z" fill="#b5562f"/>
<path d="M530 214c-40 30-60 80-62 130M546 214c-14 50-12 90 0 140M574 214c14 50 12 90 0 140M590 214c40 30 60 80 62 130" stroke="#3f8a5c" stroke-width="10" stroke-linecap="round" fill="none"/>
</g>
<!-- the palm in its pot -->
<path d="M330 860h120l-14 90h-92z" fill="#b5562f"/>
<rect x="322" y="848" width="136" height="22" rx="6" fill="#c76a40"/>
<g class="r-swing" data-o="390 850">
<path d="M390 850c-40-120-150-180-250-170 70 20 150 70 250 170z" fill="#2f6b4a"/>
<path d="M390 850c-10-150 40-260 120-300-30 80-70 170-120 300z" fill="#3f8a5c"/>
<path d="M390 850c40-110 140-160 220-150-70 30-140 80-220 150z" fill="#2f6b4a"/>
<path d="M390 850c-60-90-80-200-50-280 10 90 30 180 50 280z" fill="#4c9a69"/>
</g>
<!-- the orange tree -->
<path d="M1150 856h110l-12 94h-86z" fill="#b5562f"/>
<path d="M1205 856V660" stroke="#5b4030" stroke-width="12"/>
<g class="r-swing" data-o="1205 760">
<circle cx="1205" cy="600" r="110" fill="#2f6b4a"/>
<circle cx="1130" cy="650" r="70" fill="#3f8a5c"/>
<circle cx="1280" cy="650" r="70" fill="#3f8a5c"/>
<circle class="r-pop" cx="1160" cy="580" r="16" fill="#f08a24"/>
<circle class="r-pop" cx="1240" cy="560" r="16" fill="#f08a24"/>
<circle class="r-pop" cx="1210" cy="640" r="16" fill="#f08a24"/>
<circle class="r-pop" cx="1120" cy="670" r="14" fill="#f08a24"/>
<circle class="r-pop" cx="1290" cy="660" r="14" fill="#f08a24"/>
</g>
<!-- the table for tea, its chairs, the teapot and the cups -->
<path d="M640 860V720c0-50 70-50 70 0v140M600 860l40-80M960 860V720c0-50-70-50-70 0v140M1000 860l-40-80" stroke="#2f3d37" stroke-width="6" fill="none"/>
<path d="M800 732v130M760 870h80" stroke="#2f3d37" stroke-width="10"/>
<ellipse cx="800" cy="728" rx="150" ry="26" fill="#f6f1e4"/>
<path d="M650 728c0 30 300 30 300 0" fill="#e9e1cf"/>
<g class="r-pop">
<ellipse cx="780" cy="678" rx="46" ry="40" fill="#3b7a8c"/>
<path d="M820 670c30-6 36-30 46-40" stroke="#3b7a8c" stroke-width="10" stroke-linecap="round" fill="none"/>
<path d="M738 660c-30-6-30 40 0 36" stroke="#3b7a8c" stroke-width="8" fill="none"/>
<path d="M760 642h40l-6-12h-28z" fill="#2c5f6d"/>
<rect x="772" y="620" width="16" height="12" rx="6" fill="#2c5f6d"/>
</g>
<path class="r-draw" pathLength="1" d="M868 622c-12-20 12-30 0-52s12-30 0-50M884 626c-10-18 10-26 0-44" stroke="#ffffff" stroke-width="5" stroke-linecap="round" fill="none"/>
<path class="r-pop" d="M886 700h36l-4 22h-28z" fill="#ffffff"/>
<path class="r-pop" d="M696 702h32l-4 20h-24z" fill="#ffffff"/>
</svg>
<div class="scene__text">
<p class="scene__place">Hotel Ostrander</p>
<h2 class="scene__title" data-split>The Winter Garden</h2>
<p class="scene__line">Tea under glass, from four in the afternoon</p>
</div>
</section>
<!-- the light that comes through the keyhole, before the room is near enough to be seen -->
<div class="room__light" data-room-light></div>
</div>
</div>
<!-- the soft rim of light round the keyhole while it opens -->
<svg class="rim" aria-hidden="true" data-rim>
<path class="rim__wide" data-rim-wide/>
<path class="rim__thin" data-rim-thin/>
</svg>
<!-- the key: hanging on its hook, then in the lock -->
<div class="key" data-key aria-hidden="true">
<div class="key__turn" data-key-turn>
<svg class="key__art" viewBox="0 0 40 100">
<defs>
<linearGradient id="keyBrass" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#8a5f25"/>
<stop offset="0.35" stop-color="#f0cf85"/>
<stop offset="0.6" stop-color="#c99847"/>
<stop offset="1" stop-color="#7a521d"/>
</linearGradient>
<path id="keyShape" fill-rule="evenodd" d="M20 1.5a8 8 0 0 1 7.6 10.6 8 8 0 0 1 1.4 15.4H23v2.5h3v4h-3.6V76h9.2v4h-3v4h3v6h-9.2v4.5h-4.8V34h-3.6v-4h3v-2.5H11a8 8 0 0 1 1.4-15.4A8 8 0 0 1 20 1.5zM20 6a3.6 3.6 0 1 0 0 7.2A3.6 3.6 0 0 0 20 6zM13 16.5a3.6 3.6 0 1 0 0 7.2 3.6 3.6 0 0 0 0-7.2zM27 16.5a3.6 3.6 0 1 0 0 7.2 3.6 3.6 0 0 0 0-7.2z"/>
<clipPath id="keyCut" clipPathUnits="userSpaceOnUse">
<rect x="-10" y="-10" width="60" height="120" data-key-cut/>
</clipPath>
</defs>
<!-- the part of the key already inside the lock is cut away from below (the script sets the height) -->
<g clip-path="url(#keyCut)">
<use href="#keyShape" class="key__shadow" x="1.4" y="2.2"/>
<use href="#keyShape" fill="url(#keyBrass)"/>
<path d="M18.2 36v52" stroke="#fff3cf" stroke-width="0.9" opacity="0.7"/>
</g>
</svg>
</div>
</div>
<!-- the one command: turn the key, then step back out -->
<div class="dock" data-dock>
<button class="act" type="button" data-act data-autoclic>
<svg class="act__icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M8.5 4.5a4 4 0 1 1 0 8 4 4 0 0 1 0-8zM12 9.8l8.5-.1M17 9.7v3.2M19.6 9.7v2.2"/>
</svg>
<span class="act__label"><span data-act-label>Turn the key</span></span>
</button>
</div>
<p class="sr-only" aria-live="polite" data-live></p>
</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.
52 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 keyhole transition” from Animorio and fit it into my siteThe whole dive depends on st.p, from 0 to 1, which a GSAP timeline runs in ZOOM seconds; render() works out the rest from it. The zoom is exponential, z = zFull to the power p, so the speed looks constant from the first instant to the last (doubling is always the same step), and zFull is the zoom at which the circle of the hole covers the corners of the screen too: half the diagonal over the hole's radius in pixels, plus a margin.
The door, an SVG filling the screen with preserveAspectRatio slice, grows by z with its transform-origin on the keyhole (its place on the screen comes from getScreenCTM) and meanwhile slides to the middle, all within z = 6. Between z = 2.2 and z = 8 a black veil rises from 0 to 1: the dark inside the lock; then the door is hidden, because a hundred times larger it is of no use to anyone.
The room sits above the door, cut with a clip-path path() in the shape of the hole: a circle arc wider than half a turn (A with large-arc 1) and the trapezoid slot under it. render() rewrites it every frame at the same place and the same scale as the hole drawn on the door, so they match; at the end the clip-path becomes none. Round it, the same outline in a separate SVG makes the rim of light, which fades as the hole opens.
The key is a div with an SVG inside, above the room. It flies from the hook with two sets of keyframes (up and down, tilted and straight), then sinks into the lock: an SVG clipPath, a rectangle getting shorter, cuts from below the part already inside, in step with the descent. The clicks turn the key on itself by TURN_ANGLE degrees with back.out: the width follows the cosine of the angle and a light skewY gives the depth; at each click the plate moves by a pixel and three short lines draw themselves with stroke-dashoffset.
In the room, the inner scene starts DEPTH times larger and settles to 1 while the hole opens, and in front of it a warm light fades out between z = 3 and z = 26: from far away the hole shows only light, then the room. Halfway through the dive the room's entrance starts: the title split into letters rises with back.out, pictures and plants swing from their hanging point with svgOrigin and elastic, lights come on with a keyframed flicker, lines draw themselves with pathLength 1.
| Name | File | Default | What it changes |
|---|---|---|---|
TURNS | script.js | 2 | The clicks of the key in the lock before the door gives. |
TURN_ANGLE | script.js | 70 | How many degrees the key turns at each click. |
ZOOM | script.js | 1.5 | The seconds of the dive through the keyhole (the way out lasts 85 per cent of it). |
DEPTH | script.js | 1.4 | How far back the room is at the start of the dive (1 = no depth). |
SWING | script.js | 1 | How much the key swings on its hook (0 = it hangs still). |
LIGHT | script.js | 0.85 | How strong the light of the room through the keyhole is. |
KEY_SIZE | script.js | 1.15 | The size of the key against the door. |
For a hotel site that opens its rooms one at a time, for an exhibition that lets you peek at its rooms before going in, for an advent calendar, for the secret page of an event or a launch, for the step from the cover to the content of a story.
The door is drawn in a very wide SVG with preserveAspectRatio slice, so on a phone held upright it stays whole in the middle and only the sides of the corridor are lost; the dive is the same, with the zoom worked out from the screen's diagonal. The rooms keep the important things in the middle. The key swaying under the pointer is there only with a mouse.
Nothing moves: the key fades from the hook and reappears in the lock already turned, the light of the keyhole comes on, and the room, already all in place, fades in; on the way out the room fades onto the next door, with the key reappearing on its hook.
GSAP 3.15.0, Fraunces (font variabile) 5.3.0, Instrument Sans (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 16 KB; the libraries: GSAP 28 KB.
Tested on 11 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.