Skip to content

Latest commit

 

History

History
94 lines (75 loc) · 18.6 KB

AGENTS.md

File metadata and controls

94 lines (75 loc) · 18.6 KB

AGENTS.md

Notes for coding agents working on this repo. Read README.md first for what the demo is.

Setup

  • There is no build step, no package.json and nothing to install. Plain HTML, CSS and ES modules.
  • Serve the root with any static server (npx serve . or python3 -m http.server). ES modules don't load from file://.
  • GSAP (with Flip and ScrollTrigger) and Lenis are vendored in js/ and loaded as classic scripts before the module, so gsap, Flip, ScrollTrigger and Lenis are globals. Don't import them.
  • The analytics and ad scripts are commented out in every page. They're added on the live server only; leave them commented out here.
  • There are no automated tests. Check a change in a browser: scroll through every gallery on both pages at desktop and phone width, resize the window in the middle of a gallery, turn on reduced motion, and look at the console.

Map

Path What it is
index.html Layouts 01, the author's page: frame, intro, nine galleries (#gallery-1…#gallery-9) and the text between them.
index2.html Layouts 02: the same page structure with new layouts, its own pictures and its own copy ("Near Copies", about the variations an image model gives back for one prompt). Its galleries have named ids (#gallery-carousel, #gallery-pyramid, #gallery-sunflower, #gallery-sun, #gallery-pile, #gallery-slices, #gallery-stairs, #gallery-question, #gallery-deal, #gallery-reflow).
js/index.js Shared by both pages: Lenis, the list of every page's galleries and their options, the Flip and ScrollTrigger setup, the rebuild on resize, reduced motion, start-up.
js/utils.js preloadImages: waits for <img> elements and CSS background images to load and decode.
js/gsap.min.js, js/Flip.min.js, js/ScrollTrigger.min.js GSAP 3.15.0.
js/lenis.min.js Lenis 1.3.27 (its UMD build).
css/base.css All styles: frame, loader, Lenis' recommended styles, and each gallery's start and end layout.
img/ The pictures, set as inline background-image on the gallery items. 1.webp…70.webp are page 1's (Midjourney, one Unsplash photo); 71.webp…113.webp are Layouts 02's own (Nano Banana in Paper): 71–76 pile, 77–82 slit-scan (2:3, same framing, so the bands line up), 83–88 curtain (face centred), 89–96 deal (square, 2000px, the first shows full screen), 97 reflow, 98–104 carousel and pyramid, 114–133 sunflower (4:3, from codrops/Staggered3DGridAnimations, Midjourney, MIT, recoloured into the files as a duotone of ink blue #1e2756 and peach #f7cfb0 with ffmpeg; repeated over its 90 cards). 134–149 the sun's: page 1's 16–31, faded into the files (saturation 0.42, contrast 0.86, lifted blacks, a slightly warm cast, with ffmpeg). Gallery 7 on Layouts 02 (the question mark) reuses page 1's pictures as they are.
og-image.jpg The social preview (1200×630), a frame of gallery 6.
og-image2.jpg Layouts 02's social preview (1200×630), the carousel's feature view.

How a gallery works

Every gallery is two layouts in CSS and one Flip animation between them, scrubbed by scroll while the gallery is pinned.

<div class="gallery-wrap">
	<div class="gallery gallery--row" id="gallery-1">
		<div class="gallery__item" style="background-image:url(img/6.webp)"></div>
		…
		<div class="caption">…</div>
	</div>
</div>
  • The start layout is .gallery--<layout> in css/base.css. The end layout is the same gallery with .gallery--switch added (.gallery--switch.gallery--<layout> …). Design both in CSS first; the JS only measures them.
  • triggerFlipOnScroll adds .gallery--switch, records the items and the caption with Flip.getState (props: 'filter, opacity', so those two animate as well), removes the class again, then calls Flip.to with a scrubbed ScrollTrigger that pins the .gallery-wrap. The class never stays on, except with reduced motion (below).
  • Only the items and the caption are animated. A switch rule on anything else (like .gallery--switch.gallery--grid .caption p) only changes what gets measured.
  • scroll() goes through the galleries list, which holds every page's galleries, and skips the ones that aren't on the page.
  • The defaults are pinned from 'center center' for '+=300%', scale: true, simple: true, props: 'filter, opacity', no stagger and a linear ease ('none'). flip.props lists the CSS properties Flip records and animates besides the layout (the slit-scan adds the background's size and position). Each gallery overrides them in the galleries list in scroll(), one group at a time (flip, scrollTrigger, stagger, ease); the other defaults of a group stay. With scale: false, Flip animates width and height instead of scale.
  • A layout that places each item by its number reads --i from the item's inline style, next to its picture (the sunflower, whose end layout is computed in CSS with cos(), sin() and sqrt()). Absolutely placed end layouts set grid-area: auto on grid items, or the grid cell becomes their containing block. A caption whose box would change size between the two layouts gets the same box in both (absolute, inset: 0), or Flip scales its text unevenly.
  • 3D layouts. Flip only records 2D (position, size, 2D rotation and scale), so a gallery with a 3D start gets its own function, set with animate in the galleries list. The carousel uses one for its 3D part and still ends with a Flip switch. triggerUnrollOnScroll (the carousel) leaves the pictures in their CSS layout (the row, or the column on phones), measures where each one lies, and on every update places them on a ring whose bend goes from a closed circle to flat: each picture at sin(angle) / bend along the row and (cos(angle) - 1) / bend in depth, turned by its angle, its --light (read by the CSS filter) dimming towards the back. Its trigger is the wrapper, since the gallery itself starts tilted and scaled. With reduced motion nothing runs and the CSS layout shows as it is.
  • .gallery__item-inner elements (gallery 2) also zoom from 2 to 1. That tween has its own ScrollTrigger with the gallery's start and end: Flip puts the scrollTrigger on a tween inside the timeline it returns, so that timeline isn't scrubbed, and a tween added to it needs its own trigger.
  • Resizing. Flip records both layouts in pixels, so initLayoutAnimations builds every gallery again after the window changes size (only the width counts on touch screens, where the toolbar changes the height). Reverting a Flip.to leaves its end state inline, so it first puts back the inline styles the items and captions started with. Keep the pictures in those inline styles and nothing else.
  • Reduced motion. gsap.matchMedia() sets everything up only for (prefers-reduced-motion: no-preference). With reduce, every gallery gets .gallery--switch and stays still, there's no Lenis and no pinning, and the page scrolls natively. The end layouts are where the captions can be read.
  • Start-up. The page waits for every picture (on .gallery__item and .gallery__item-inner) and for the fonts before anything is measured, then removes loading from the body.
# Classes Start → end Options
1 gallery--row A row of 2:3 pictures in three sizes → the middle one fills the screen, darkened; the caption rises. absoluteOnLeave, scale: false
2 gallery--grid gallery--breakout (in gallery-wrap--large) A 3×3 grid → every cell 110vw × 110vh with no gap, so the middle picture fills the screen; the pictures zoom out inside their cells. (defaults)
3 gallery--grid10 16 pictures scattered over a 10-column grid (2 on phones) → all stacked in the middle cell, one after another. absolute, scale: false, '+=900%', stagger: 0.05
4 gallery--stack gallery--stack-inverse gallery--stack-dark Six in a row → a stack with 1rem offsets, darker towards the back; the caption slides in. (defaults)
5 gallery--stack gallery--stack-glass The same stack, the pictures at 0.7 opacity. (defaults)
6 gallery--stack gallery--stack-inverse gallery--stack-scale gallery--stack-dark The same stack, each picture smaller and darker towards the back. (defaults)
7 gallery--gridtiny 80 grey tiles in a grid three screens wide → the grid fits the screen (its width capped so all eight rows fit the height), in color. (defaults)
8 gallery--bento A bento grid of eight → the cells grow until one picture fills the view. scale: false
9 gallery--one One picture at 400%, hue-rotated → fitted to the screen, darkened; the caption rises. Always the last one. (defaults)

Layouts

Each page is a set of layouts for the same content. The frame's nav calls them layouts, not variations.

# Page Galleries Notes
01 index.html The author's nine, above.
02 index2.html #gallery-carousel (gallery--carousel, in gallery-wrap--3d): the seven pictures rolled up into a 3D carousel seen from above, 1.5× its size, the backs of the far cards dimmed. While it scrolls into view the ring turns (turns), and the turn runs on into the unrolling (overlap) so the two read as one motion; the turn is a slide along the ring (shift), which becomes the row gliding into place as the ring opens. The unrolling starts before the pin (unrollStart) and lays the ring out flat into a row (a column on phones). Then a Flip switch to the feature view (.gallery--switch): the front card, the 4th, becomes one large picture (on top, z-index: 2, so the others pass behind it) and the other six a strip of thumbnails under it; the caption appears beside it (below on phones). The 3D part is triggerUnrollOnScroll, not Flip; the feature view is a Flip.to recorded at the start, while the row has no 3D on it, and added to the same timeline. Every card is placed absolutely in CSS in both layouts (--k in the row, --j in the strip), so no reflow happens and Flip needs no absolute option, which would fight the ring's transforms. #gallery-pyramid (gallery--pyramid, right after the carousel, with the same seven pictures): the whole screen as one collage of seven tiles of different sizes, edge to edge with hairline gaps (each picture cropped to its tile) → stacked from the bottom into a step pyramid of whole cards, four, then two on the middle pair, then one on top, each resting on the ones below (--px, --row; card size from --w, which fits phones too); the caption beside it (below on phones). #gallery-sunflower (gallery--sunflower): gallery 2's pictures on 90 landscape cards in a band of six rows, wider than the screen → a sunflower head, each card 137.508° (the golden angle) round from the one before and further out with the square root of its number, larger and lighter towards the rim; the band is pulled in from its left end and the flower fills in from the middle. #gallery-sun (gallery--sun): gallery 3's sixteen pictures in a four by four contact sheet, placed clockwise from the top (each --n with its grid-area) → they bloom into a sun around the caption, every picture turned to point away from the middle, on the ray nearest to where it was, so they travel outwards; a clockwise sweep. #gallery-pile (gallery--pile): gallery 4's six portraits in a three by two contact sheet → tossed one by one onto a pile in the middle, larger, each at its own angle and offset (--x, --y, --r), the ones underneath darker (--light); the caption appears beside it (below on phones). #gallery-slices (gallery--slices): gallery 5's six portraits hung like a salon wall, six frames of different sizes (left, top, --h per picture) → they stack into one portrait, each frame stretching into a band that shows its own sixth of its picture (background-size: 100% 600%, background-position 0%…100%), so the woman is stitched from six variations; the caption appears beside it (below on phones). #gallery-stairs (gallery--stairs): gallery 6's six portraits as a curtain of six full-height strips edge to edge (--n per picture), each showing the middle of its portrait → the curtain parts, each strip widening into its whole portrait and stepping down into a diagonal staircase, the pictures growing and getting lighter towards the front (--x, --y, --h, --light, scaled by --u on phones); the caption sits in the empty corner above it (below on phones). #gallery-question (gallery--question): gallery 7's eighty paintings as a wall of square tiles, as big as fits the screen with every tile whole (10 by 8, 8 by 10 on phones), grey and dim → they regroup into one big question mark in colour, each tile at its cell of a 12 by 17 pixel glyph (--gr, --gc in its inline style, in reading order, so the tiles from the top of the wall build the top of the glyph); the caption "What is creativity" sits on the line of the dot, left of it (above it on phones), and its own "?" (.caption__mark) is kept for screen readers only. Tiles are square in both layouts, so Flip only scales them. #gallery-deal (gallery--deal): gallery 8's eight close-ups as one deck of square cards filling the whole screen (100vmax), the first card on top, each one under it a little darker (--shade, 1 down to 0.37) → dealt out clockwise into a three by three frame, each card lighting up as it is dealt, the middle cell left empty; the caption sits in that cell's place in both layouts (absolute, sized from --cell), so it only fades in. #gallery-reflow (gallery--reflow, always the last): gallery 9's one picture in nine pieces (--c, --r per piece; background-size: 300% 300%, so each shows its piece at any size), in reading order as one long line (a column on phones) → the line wraps like text into a narrower column (the gallery's own width changes from nine pieces to three), until the pieces make the picture again as a three by three print, not full screen; the caption sits under it like a signature. Complete: one gallery per slot of index.html. Options: carousel animate: triggerUnrollOnScroll; pyramid scale: false, props adding borderRadius, stagger: 0.08; sunflower '+=600%', stagger: 0.01; sun '+=400%', stagger: 0.03; pile stagger: 0.06; slices scale: false, props adding backgroundSize, backgroundPosition, '+=400%', stagger: 0.06; stairs scale: false, props adding borderRadius, stagger: 0.06; question '+=400%', stagger: 0.006; deal '+=400%', stagger: 0.07; reflow '+=400%', stagger: 0.04; both Flip ones ease: 'power2.inOut'.

Adding a layout

  1. Layouts 02 follows index.html's order of galleries and text sections, with its own pictures and copy (galleries 3 and 7 reuse page 1's pictures). A gallery can also be an extra example after a slot, using that slot's pictures, like the pyramid after the carousel.
  2. Give it a named id and a gallery--<name> class, write its start and end layouts in css/base.css, and add { id: '#gallery-<name>', options } to the galleries list in js/index.js, under its page. A layout Flip can't record (3D) gets its own function, passed as animate.
  3. A new layouts page (index3.html…) starts as a copy of index2.html with its own <title>, description, canonical, og:url and JSON-LD url. Add it to the .frame__demos nav on every page; the current page's entry is a <span class="frame__demo frame__demo--current" aria-current="page">, not a link.

Style

  • Tabs in js/index.js and css/base.css, four spaces in js/utils.js. Single quotes and semicolons. Follow what's in the file.
  • Functions are const name = () => {}, and conditions are written if ( condition ).
  • A short comment above almost every step, saying what it does ("// Select elements within the gallery that will be animated").
  • Flat CSS with BEM-style names (gallery__item, gallery--row), the end state as the gallery--switch modifier, sizes mostly in vw and vh.
  • No dependencies and no build tooling unless asked.

Ask before changing

  • The <title>, meta description, keywords and social tags, the frame__title heading, the frame's links and tags.
  • The credits (in the README and the page's Photo credits section), the page's text and the pictures.
  • The closing "More demos in the Creative Hub" button (.cta__link), the same on every page, and the link under it to the other layouts page (.cta__next).
  • The layouts and options of the nine galleries in index.html, which are the author's own.