A new approach…

This commit is contained in:
Ben Aultowski 2026-01-08 01:35:35 -05:00
parent 821e47a019
commit 7a4748a69c
16 changed files with 366 additions and 2292 deletions

View file

@ -1,8 +1,6 @@
# Hypnagaga - Eleventy Project # Hypnagaga - Eleventy Project
This is an Eleventy (11ty) static site generator project, based on [eleventy-excellent](https://github.com/madrilene/eleventy-excellent.git), customized for hosting music mixes with track listings and annotations. A vision quest, of sorts.
**Tech Stack**: Eleventy 3.x (ESM), Tailwind CSS (token-based), CUBE CSS methodology, WebC components, Hotwired Turbo for navigation, PostCSS, esbuild
## Quick Start ## Quick Start
@ -17,20 +15,13 @@ npm run test:a11y # Run accessibility tests
**Key Files to Know:** **Key Files to Know:**
- `eleventy.config.js` - Main Eleventy config (imports from `src/_config/`) - `eleventy.config.js` - Main Eleventy config (imports from `src/_config/`)
- `src/_config/` - Modular config (collections, filters, events, plugins, shortcodes) - `src/_config/` - Modular config (collections, filters, events, plugins, shortcodes)
- `src/assets/scripts` - scripts are built from here
- `src/assets/css/global` - global CSS is built from here
- `src/assets/css/local` - inline CSS for certain pages is built from here
- `src/_data/designTokens/` - Design tokens (colors, spacing, fonts) - `src/_data/designTokens/` - Design tokens (colors, spacing, fonts)
- `src/pages/projects/mixes/` - Music mix tracks (markdown files) - `src/pages/` - Contains written content (markdown files) that is processed as JavaScript Nunjuks template
- `src/assets/css/global/` - Global CSS (CUBE methodology) - `dist/` - the project builds to here
## Architecture & Key Concepts
### Music Mix System
The project's core feature is a hierarchical music mix/track system:
- **Projects** (e.g., "TomorrowsBacon") contain multiple tracks
- **Tracks** are individual markdown files in `src/pages/projects/mixes/{project-name}/`
- Tracks have frontmatter: `project`, `track_number`, `artist`, `album`, `albumArt`, `start_time`
- Collection `tracksByProject` (defined in `src/_config/collections.js`) groups and sorts tracks by project
- Layouts: `mix.njk` shows track list, `mix-track.njk` shows individual track with prev/next navigation
- Turbo Drive provides SPA-like navigation between tracks without full page reloads
### Configuration Architecture ### Configuration Architecture
**ESM-based modular config** in `src/_config/` - all files use `export` syntax: **ESM-based modular config** in `src/_config/` - all files use `export` syntax:
@ -38,32 +29,78 @@ The project's core feature is a hierarchical music mix/track system:
- `events.js` - Build hooks (CSS/JS compilation, pagefind indexing) - `events.js` - Build hooks (CSS/JS compilation, pagefind indexing)
- `filters.js` - Template filters (dates, markdown, slugify) - `filters.js` - Template filters (dates, markdown, slugify)
- `plugins.js` - Eleventy plugins (image transform, WebC, RSS, syntax highlighting) - `plugins.js` - Eleventy plugins (image transform, WebC, RSS, syntax highlighting)
- `shortcodes.js` - Reusable template functions (image, svg, animateText) - `shortcodes.js` - Reusable functions, used within Nunjuks templates.
Each category imports from subdirectories (e.g., `filters/dates.js`) and exports a consolidated object. Each category imports from subdirectories (e.g., `filters/dates.js`) and exports a consolidated object.
### CSS Architecture
1. Based on [eleventy-excellent](https://github.com/madrilene/eleventy-excellent.git), which uses a [progressive enhancement](https://piccalil.li/blog/how-a-minimum-viable-experience-produces-a-resilient-inclusive-end-product/) philosphy for a responsive design.
2. The global CSS bundle is organized with cascade layers. The local CSS bundle, used for per-page or omponent-specific styles, does not utilize cascade layers. As a result, all CSS blocks included have a higher specificity than global styles, regardless of the selector specificity in the global CSS bundle.
3. Tailwind is used primarily to generate utility classes on demand. It goes through each defined `group` (`groups`) and then grabs the values > generates Custom Property values > sticks them to the `result`. Finally, using `postcssJs` and `postcss`, an object that Tailwind can understand is created. The `addComponents` function sticks that custom properties block on the `@components` layer. Lastly, this function creates custom utilities that I can use in markup, such as `gutter-m` or `flow-space-s`. Its all rigged up to the design tokens Custom Property block. Its damn useful, especially when tweaking layout compositions in context.
4. Everything is built using PostCSS from `src/assets/css/`.
.
├── src
│ ├── css-utils
│ │ ├── clamp-generator.js
│ │ └── tokens-to-tailwind.js
│ ├── css
│ │ ├── blocks
│ │ │ └── prose.css
│ │ ├── compositions
│ │ │ ├── cluster.css
│ │ │ ├── flow.css
│ │ │ ├── grid.css
│ │ │ ├── repel.css
│ │ │ ├── sidebar.css
│ │ │ ├── switcher.css
│ │ │ └── wrapper.css
│ │ ├── global
│ │ │ ├── fonts.css
│ │ │ ├── global-styles.css
│ │ │ ├── reset.css
│ │ │ └── variables.css
│ │ ├── utilities
│ │ │ ├── region.css
│ │ │ └── visually-hidden.css
│ │ └── global.css
│ └── design-tokens
│ ├── colors.json
│ ├── fonts.json
│ ├── spacing.json
│ ├── text-leading.json
│ ├── text-sizes.json
│ ├── text-weights.json
│ └── viewports.json
├── tailwind.config.js
└── postcss.config.js
3. I try to maintain a decent source order for specificity purposes as you can see. The `@import` `tailwindcss/components` is where that block of Custom Properties generated in the Tailwind config gets put. Because everything else that layer does is disabled in config, its nice and clean. The CUBE parts are all imported using the extremely useful `import-glob` PostCSS plugin. This allows new files to be added to directories and imported straight away. Its very Sass-like, but I like that.
4. There are layouts in `src/assets/css/global/compositions`.
### Image Handling
1. ** Primarily accomplished** by extending the `{% imageKeys {...} %}` **shortcode** processed from within content markdown files.
Images are processed to WebP/JPEG, multiple widths, and optimized. Source paths are prepended with `./src` automatically in shortcodes.
### Design Tokens & Tailwind ### Design Tokens & Tailwind
- We only use Tailwind to generate class names. All our CSS lives in `src/assets/css/` and uses an opinionated, responsive design. It is bundled in `dist/bundle/`
- `src/assets/css/local` - Local inline CSS. Overrides inlined for one-off pages, sections of content, and other endevours. `src/assets/css/global` is used for site-wide classes of core styling and animation features.
- Design tokens stored as JSON in `src/_data/designTokens/` (colors, spacing, fonts, viewports) - Design tokens stored as JSON in `src/_data/designTokens/` (colors, spacing, fonts, viewports)
- `tokensToTailwind()` and `clampGenerator()` utilities convert tokens to Tailwind config - `tokensToTailwind()` and `clampGenerator()` utilities convert tokens to Tailwind config
- Tailwind is customized to generate utilities from design tokens (e.g., `mt-xs-s`, `bg-pink`) - Tailwind is customized to generate utilities from design tokens (e.g., `mt-xs-s`, `bg-magenta`)
- Custom properties generated via Tailwind plugin (e.g., `--color-pink`, `--space-m`) - Custom properties generated via Tailwind plugin (e.g., `--color-cyan`, `--space-m`)
- **Run `npm run colors`** after editing `colorsBase.json` to regenerate `colors.json` with shades - **Run `npm run colors`** after editing `colorsBase.json` to regenerate `colors.json` with shades
- Tailwind's preflight/reset is **disabled** - custom reset in `src/assets/css/global/reset.css` - Tailwind's preflight/reset is **disabled** - custom reset in `src/assets/css/global/reset.css`
### CSS Architecture (CUBE CSS)
Layer order in `src/assets/css/global/global.css`:
1. `tailwindcss/base` (mostly unused, preflight disabled)
2. Custom reset, fonts
3. `tailwindcss/components` (where custom properties are injected)
4. Variables, global styles
5. Blocks, compositions, utilities (via `@import-glob`)
6. `tailwindcss/utilities`
**Key patterns:**
- Use compositions (grid, flow, wrapper) over Tailwind's container
- Blocks for component-specific styles
- Utilities for single-purpose helpers (e.g., text animations)
- Custom utilities like `flow-space-*`, `gutter-*`, `region-space-*` control layout spacing
### Build Process ### Build Process
**Before Eleventy runs** (`eleventy.before` event): **Before Eleventy runs** (`eleventy.before` event):
@ -79,639 +116,35 @@ Layer order in `src/assets/css/global/global.css`:
**After Eleventy builds** (`eleventy.after` event): **After Eleventy builds** (`eleventy.after` event):
- `buildPagefind()` generates search index (only in production) - `buildPagefind()` generates search index (only in production)
**Watch targets**: `src/assets/**/*.{css,js,svg,png,jpeg}`, `src/_includes/**/*.webc` **Watch targets**: `src/assets/**/*.{css,js,svg,png,jpeg}`, `src/_includes/**/*.webc`, `dist/`
### Image Handling
Three methods for image optimization (via @11ty/eleventy-img):
1. **HTML Transform**: Automatically processes `<img>` elements in HTML output
2. **Markdown**: `![alt](/path/to/image.jpg)` → transformed to `<picture>` element
3. **Shortcodes**: `{% image '/path/to/image.jpg', 'alt text' %}` or `{% imageKeys {...} %}`
Images are processed to WebP/JPEG, multiple widths, and optimized. Source paths are prepended with `./src` automatically in shortcodes.
## Development Workflows
### Essential Commands
- **`npm start`** - Dev server with live reload (runs `dev:11ty`)
- **`npm run build`** - Full production build (clean → build:11ty → build:search)
- **`npm run colors`** - Regenerate color palettes from `colorsBase.json`
- **`npm run favicons`** - Generate favicons from `src/assets/svg/misc/logo.svg`
- **`npm run test:a11y`** - Run Pa11y accessibility tests
### Adding a New Track
1. Create markdown file in `src/pages/projects/mixes/{project-name}/{track-number}-{slug}.md`
2. Include frontmatter:
```yaml
---
title: "Song Title"
artist: "Artist Name"
album: "Album Name"
project: ProjectName # must match existing project
track_number: 1
albumArt: "/pages/projects/mixes/{project}/image.jpg"
---
```
3. Track automatically appears in `collections.tracksByProject[project]`
4. Navigation (prev/next) auto-generates in `mix-track.njk` layout
### Working with Design Tokens
1. Edit JSON files in `src/_data/designTokens/`
2. For colors: Edit `colorsBase.json` → run `npm run colors` → regenerate `colors.json`
3. Tokens flow to: CSS custom properties, Tailwind utilities, and templates (via `{{ designTokens }}`)
4. Fluid type/space uses Utopia-style clamps (configured in `clamp-generator.js`)
### Text Animations ### Text Animations
Use the `animateText` shortcode or markdown-it-attrs classes: Use the `animateText` shortcode or markdown-it-attrs classes:
```jinja2 ```ninjuks
{% animateText "Hello", "shiver", "1.5" %} {# content, animation, speed #} {% animateText "Hello", "shiver", "1.5" %} {# content, animation, speed #}
``` ```
Available: `shiver`, `wobble`, `jitter`, `bounce`, `bob`, `wave` (defined in `src/assets/css/global/utilities/text-animations.css`) (defined in `src/assets/css/global/utilities/text-animations.css`)
Safelist these classes in `tailwind.config.js` so they're not purged. Safelist these classes in `tailwind.config.js` so they're not purged.
## Project-Specific Patterns ## Pages and Projects
### Collections ### Digital Mixed CD project
The project's core feature is a hierarchical music mix/track system:
- **Projects** (e.g., "TomorrowsBacon") contain multiple tracks
- **Tracks** are individual markdown files in `src/pages/projects/mixes/{project-name}/`
- Tracks have frontmatter: `project`, `track_number`, `artist`, `album`, `albumArt`, `start_time`
- Collection `tracksByProject` (defined in `src/_config/collections.js`) groups and sorts tracks by project
- Layouts: `mix.njk` shows track list, `mix-track.njk` shows individual track with prev/next navigation
- Turbo Drive provides SPA-like navigation between tracks without full page reloads
#### Collections
- **`allPosts`**: All markdown files in `src/posts/**/*.md` (reversed chronological) - **`allPosts`**: All markdown files in `src/posts/**/*.md` (reversed chronological)
- **`tagList`**: Unique tags across all content (excludes 'posts', 'docs', 'all', 'mix', 'project') - **`tagList`**: Unique tags across all content (excludes 'posts', 'docs', 'all', 'mix', 'project')
- **`goPages`**: Pages with `go:` frontmatter for URL redirects (output to `_redirects`) - **`goPages`**: Pages with `go:` frontmatter for URL redirects (output to `_redirects`)
- **`tracksByProject`**: Tracks grouped by project, sorted by track_number - **`tracksByProject`**: Tracks grouped by project, sorted by track_number
### WebC Components
Located in `src/_includes/webc/`:
- `<custom-card>` - Card layout with slots (image, headline, date, tag, content, footer)
- `<custom-masonry>` - Masonry grid (with optional `layout="50-50"`)
- `<custom-details>` - Enhanced `<details>` with expand/collapse all buttons
### Turbo Drive Navigation ### Turbo Drive Navigation
- Enabled for track-to-track navigation (no full page reload)
- `<turbo-frame id="main-content">` in layouts wraps main content - `<turbo-frame id="main-content">` in layouts wraps main content
- Import Turbo in defer bundle: `{% js "defer" %} import * as Turbo from '/assets/components/turbo.js'; {% endjs %}` - Import Turbo in defer bundle: `{% js "defer" %} import * as Turbo from '/assets/components/turbo.js'; {% endjs %}`
### Pagination
Defined in `src/pages/blog.njk` with `pagination.size`. Adjust labels in `src/_data/meta.js` (`blog.paginationLabel`, etc.).
### Go URLs / Redirects
Add `go: /short-url` to page frontmatter. The `goPages` collection generates `_redirects` file for Netlify/Vercel.
## Common Issues & Solutions
### CSS Not Updating
CSS is built in `eleventy.before` event, not by Eleventy's passthrough copy. Changes to `src/assets/css/**/*.css` trigger rebuild via watch targets.
### Images Not Processing
Ensure image paths start with `/` (relative to `src/`). HTML Transform plugin processes existing `<img>` tags in output HTML.
### New Color Not Available
Run `npm run colors` after editing `colorsBase.json`. Update `src/assets/css/global/base/variables.css` if color names change.
### Tailwind Class Not Generated
Check if the utility is enabled in `tailwind.config.js` theme (only `backgroundColor`, `textColor`, `margin`, `padding` enabled by default). Add more if needed.
### Track Navigation Broken
Verify `project` frontmatter matches across tracks. Collection sorts by `track_number` (ensure it's a number, not string).
## File Conventions
### This project uses ESM syntax instead of CommonJS. Configurations are structured into separate modules in `src/_config` and are then imported into the main configuration file.
- **collections.js**: Manages Eleventy collections such as posts and tags: https://www.11ty.dev/docs/collections/
- **events.js**: For code that should run at certain times during the compiling process: https://www.11ty.dev/docs/events/
- **filters.js**: Used within templating syntax to transform data into a more presentable format: https://www.11ty.dev/docs/filters/
- **plugins.js**: Everything I or Eleventy considers to be a plugin: https://www.11ty.dev/docs/plugins/
- **shortcodes.js**: Defines shortcodes for reusable content: https://www.11ty.dev/docs/shortcodes/
Each configuration category (filters, plugins, shortcodes, etc.) is modularized. or example, `dates.js` within the `filters` folder contains date-related filters.
```js
import dayjs from 'dayjs';
export const toISOString = dateString => dayjs(dateString).toISOString();
export const formatDate = (date, format) => dayjs(date).format(format);
```
These individual modules are then imported and consolidated in a central `filters.js` file, which exports all the filters as a single default object.
```js
import {toISOString, formatDate} from './filters/dates.js';
// more imports
export default {
toISOString,
formatDate,
// more exports
};
```
### Integration in Eleventy Config
In the main Eleventy configuration file (`eleventy.config.js`), these modules are imported:
```js
import filters from './src/_config/filters.js';
import shortcodes from './src/_config/shortcodes.js';
```
They are then used to register filters and shortcodes with Eleventy, using this nice concise syntax:
```js
eleventyConfig.addFilter('toIsoString', filters.toISOString);
eleventyConfig.addFilter('formatDate', filters.formatDate);
// More filters...
eleventyConfig.addShortcode('svg', shortcodes.svgShortcode);
```
This method hopefully keeps the Eleventy config clean and focused, only concerning itself with the registration of functionalities, while the logic and definition remain abstracted in their respective modules.
# CSS
We are using Tailwind CSS to generate utility classes on demand, based on our design tokens. Add and delete your globally available custom block stylesheets in `src/assets/css/global/blocks/*.css`.
The methodology used is [CUBE CSS.](https://cube.fyi/)
If you have a look at the tailwind.config.js, you can see how that is done. For example, we are deactivating Tailwinds default reset.
We are hooking into the components layer, to make Tailwind output classes based on our tokens, instead of their default design system.
That is, you are able to use mt-xs-s instead of a class like mt-20 for example. Same goes for colors, depending on the namesin your colors.json, you get custom classes like text-pink. These use the usual Tailwind prefixes (see docs to learn how to generate colors).
You get a custom property mapped to the color name --color-my-custom-color-name and the classes bg-my-custom-color-name as well as text-my-custom-color-name.
Consider that we limit those utilities in the theme section:
```js
backgroundColor: ({theme}) => theme('colors'),
textColor: ({theme}) => theme('colors'),
margin: ({theme}) => ({ auto: 'auto', ...theme('spacing')}),
padding: ({theme}) => theme('spacing')
```
If you want to add the generation for border-color classes for example, youd have to add that right there:
{% raw %}
```js
borderWidth: ({theme}) => theme('borderWidth'),
borderColor: ({theme}) => theme('colors')
```
{% endraw %}
Also. you do have something like md:text-right available because we define the screens (src/_data/designTokens/viewports.json):
{% raw %}
```js
screens: {
ltsm: {max: `${viewportTokens.sm}px`},
sm: `${viewportTokens.sm}px`,
md: `${viewportTokens.md}px`,
navigation: `${viewportTokens.navigation}px`
},
```
{% endraw %}
Additionally, you get custom properties based on the naming of your design token files:
{% raw %}
```js
const groups = [
{key: 'colors', prefix: 'color'},
{key: 'borderRadius', prefix: 'border-radius'},
{key: 'spacing', prefix: 'space'},
{key: 'fontSize', prefix: 'size'},
{key: 'lineHeight', prefix: 'leading'},
{key: 'fontFamily', prefix: 'font'},
{key: 'fontWeight', prefix: 'font'}
];
```
{% endraw %}
The clampGenerator generates Utopia-like CSS clamp values for fluid type and space and the tokensToTailwind function converts whatever format the projects design tokens are in, into Tailwind friendly configuration objects.
## module.export
We dont let Tailwind create all the utilities that it can do at the core. Each of the single keyword properties like fontSize are defined at the top of the file by running through the tokensToTailwind function, so they get referenced straight in the config.
The Tailwind media query function — screen — is very rarely used, but we at least want that rigged up to design tokens.
```js
// Prevents Tailwind's core components
blocklist: ['container'],
```
Container is not needed because theres a wrapper in css/compositions.
```js
experimental: {
optimizeUniversalDefaults: true
},
```
This config value also contributes to getting rid of the massive wall of Custom Properties.
## plugins
{% raw %}
```js
plugin(function ({addComponents, config}) {
let result = '';
const currentConfig = config();
const groups = [
{key: 'colors', prefix: 'color'},
{key: 'spacing', prefix: 'space'},
{key: 'fontSize', prefix: 'size'},
{key: 'fontLeading', prefix: 'leading'},
{key: 'fontFamily', prefix: 'font'},
{key: 'fontWeight', prefix: 'font'}
];
groups.forEach(({key, prefix}) => {
const group = currentConfig.theme[key];
if (!group) {
return;
}
Object.keys(group).forEach(key => {
result += `--${prefix}-${key}: ${group[key]};`;
});
});
addComponents({
':root': postcssJs.objectify(postcss.parse(result))
});
}),
```
{% endraw %}
Right, this is the sort of thing that sold me on Tailwind. They have a whole custom plugin system that you can tap into. One thing I want to always do is generate a nice block of Custom Properties, based on design tokens. Thats exactly what the above code does.
It goes through each defined group (groups) and then grabs the values > generates Custom Property values > sticks them to the result.
Finally, using postcssJs and postcss, an object that Tailwind can understand is created. The addComponents function sticks that custom properties block on the @components layer. Its a bit of a hack, but it does the job.
{% raw %}
```js
plugin(function ({addUtilities, config}) {
const currentConfig = config();
const customUtilities = [
{key: 'spacing', prefix: 'flow-space', property: '--flow-space'},
{key: 'spacing', prefix: 'region-space', property: '--region-space'},
{key: 'spacing', prefix: 'gutter', property: '--gutter'}
];
customUtilities.forEach(({key, prefix, property}) => {
const group = currentConfig.theme[key];
if (!group) {
return;
}
Object.keys(group).forEach(key => {
addUtilities({
[`.${prefix}-${key}`]: postcssJs.objectify(
postcss.parse(`${property}: ${group[key]}`)
)
});
});
});
})
```
{% endraw %}
This function creates custom utilities that can be used in markup, such as gutter-m or flow-space-s. Its all rigged up to the design tokens Custom Property block. Its damn useful, especially when tweaking layout compositions in context.
## PostCSS
{% raw %}
```js
@import 'tailwindcss/base';
@import 'global/reset.css';
@import 'global/fonts.css';
@import 'tailwindcss/components';
@import 'global/variables.css';
@import 'global/global-styles.css';
@import-glob 'blocks/*.css';
@import-glob 'compositions/*.css';
@import-glob 'utilities/*.css';
@import 'tailwindcss/utilities';
```
{% endraw %}
I try to maintain a decent source order for specificity purposes as you can see. The @import 'tailwindcss/components' is where that block of Custom Properties generated in the Tailwind config gets put. Because everything else that layer does is disabled in config, its nice and clean.
The CUBE parts are all imported using the extremely useful import-glob PostCSS plugin. This allows new files to be added to directories and imported straight away.
### Inline CSS and bundles
The main CSS file is now inline in production to improve performance, see `.src/_includes/head/css-inline.njk`.
You can add per-page or component bundles of CSS. Instead of adding your CSS file to the `src/assets/css/global/blocks/` directory, you can place them in `src/assets/css/local/`. All CSS files in there will be stored alongside `global.css` in `.src/_includes/css/`. You can now include them in the "local" bundle only on pages or components where you need them:
{% raw %}
```jinja2
{% css "local" %}
{% include "css/your-stylesheet.css" %}
{% endcss %}
```
{% endraw %}
### Component CSS
All CSS files placed in `src/assets/css/components/` will be sent to the output folder, where components can reference them: `/assets/css/components/*.css`.
### Debugging CSS
In `src/assets/css/global.css` you can decomment `@import-glob 'tests/*.css';` to include CSS for debugging.
It makes visible when your code[ wrapped in `<is-land>` elements](https://github.com/11ty/is-land) is being hydrated, where things might overflow and many other warnings and errors [that Heydon Pickering came up with](https://heydonworks.com/article/testing-html-with-modern-css/).
### Cascade layers
We now use cascade layers! Up until now, I used the `:where()` pseudo-class to create low specificity selectors for the reset and compositions. [Mayank inspired me](https://mayank.co/blog/css-reset-layer/) to change to cascade layers. We have two major bundles of CSS: everything included in "global" In `src/assets/css/global/global.css` is now organized in cascade layers. The "local" bundle is for per-page or component CSS, and does not use cascade layers - it has thus a higher specificity, independent of any selector specificity in the global CSS.
## Design Tokens
Edit all your preferences (colors, fluid text sizes etc.) in `src/_data/designTokens/*.json`.
Additional colors, variants and gradients for custom properties are automatically created in `src/assets/css/global/base/variables.css` based on the colors set in `colors.json`.
In the [style guide](/styleguide/) you can see how everything turns out.
### Special case: colors
As of version 4.0, you can create colors dynamically. Run `npm run colors` after setting your color values in `src/_data/designTokens/colorsBase.json`. This will create / overwrite the required `colors.json` file in the same directory. These colors become custom properties (e.g. `--color-gray-100`) and utility classes similar to the Tailwind CSS syntax (for example `bg-gray-100`, `text-gray-900`).
If you want to adjust how the colors turn out, edit `src/_config/setup/create-colors.js`.
Colors placed under `shades_neutral` or `shades_vibrant` are converted into scalable palettes. `shades_neutral` is better for grayish / monochromatic colors, while `shades_vibrant` is better for colorful palettes. Colors listed under `standalone` and `light_dark` are left as they are, `light_dark` items output a second "subdued" version optimized for dark themes.
```js
// this creates a palette with shades of green, 100 to 900
"shades_vibrant": [
{
"name": "green",
"value": "#008000"
}
],
```
Important: If you change the color names, you must edit `src/assets/css/global/base/variables.css` with your color names. The rest of the CSS files should only reference custom properties set in `variables.css`.
# Javascript
This project uses ESM syntax instead of CommonJS. Configurations are structured into separate modules in `src/_config` and are then imported into the main configuration file.
There are two kinds of bundles for JavaScript in this starter, see `.src/_includes/head/js-inline.njk` and `.src/_includes/head/js-defer.njk`.
By default, I include Eleventy's [is-land](https://github.com/11ty/is-land) framework and the theme toggle inline.
You can include more scripts like so:
{% raw %}
```jinja2
{% js "inline" %}
{% include "scripts/your-inline-script.js" %}
{% endjs %}
```
{% endraw %}
Same goes for scripts that should be defered:
{% raw %}
```jinja2
{% js "defer" %}
{% include "scripts/your-defered-script.js" %}
{% endjs %}
```
{% endraw %}
Scripts stored in `src/assets/scripts/components/` are sent to the output folder, while scripts in `src/assets/scripts/bundle/` are sent to `.src/_includes/scripts/`, from where you can include them in the respective bundle.
# Details
The `<custom-details>` WebC component has a corresponding Nunjucks include.
It uses the `<details>` and `<summary>` elements to create a collapsible section and enhances them aesthetically and functionally.
The JavaScript for the `<custom-details>` component adds functionality to buttons to expand and collapse the sections with one action. When JavaScript is disabled, the sections are still accessible and collapsible, but the extra buttons are hidden.
On page load, it checks if a hash corresponding to a details ID exists in the URL. If such an ID is found, the corresponding details section is programmatically opened, allowing direct navigation to an open section from a shared URL.
The sorting is set by default on "alphabetic", but you can also pass in "shuffle" or "reverse" as a parameter (directly in the `details.njk` partial).
### Usage
{% raw %}
```jinja2
{% set itemList = collections.docs %}
{% set headingLevel = "h2" %} {# optional, defaults to false #}
{% include 'partials/details.njk' %}
```
{% endraw %}
# Images
Using the [Eleventy Image](https://www.11ty.dev/docs/plugins/image/) plugin, there are three ways to handle image optimization: HTML Transform, Markdown syntax, and Nunjucks shortcodes. [See the dedicated blog post to dive (much) deeper.](/blog/post-with-an-image/)
Have a look at the [Attribute Overrides](https://www.11ty.dev/docs/plugins/image/#attribute-overrides) for the HTML Transform methods (1 and 2) for per instance overrides. Adding `eleventy:ignore` to an `<img>` element for example, skips this image.
### 1. HTML Transform
The HTML Transform automatically processes `<img>` and `<picture>` elements in your HTML files as a post-processing step during the build.
```html
<img src="./path/to/image.jpg" alt="alt text">
```
### 2. Markdown Syntax
The Markdown syntax creates the `<img>` element that the _HTML Transform plugin_ is looking for, and then transforms it to the `<picture>` element (if more than one format is set).
```markdown
![alt text](/path/to/image.jpg)
```
### 3. Nunjucks Shortcodes
In Nunjucks templates you can also use shortcodes (`image` and `imageKeys`).
{% raw %}
```jinja2
{% image '/path/to/image.jpg', 'alt text' %}
{% imageKeys {
"alt": "alt text",
"src": "/path/to/image.jpg"
} %}
```
{% endraw %}
# Custom masonry
`<custom-masonry>` is designed to function as a masonry grid by dynamically adjusting item positions based on the available column space and the size of its content. The necessary JavaScript (`custom-masonry.js`) is loaded only once per component usage due to the `data-island="once"` attribute.
Optional: pass in `layout="50-50"` to set a 50% width for each column.
If no JavaScript is available, the grid will fall back to the regular grid layout defined in `src/assets/css/global/compositions/grid.css`.
```js
<custom-masonry> (children) </custom-masonry>
<custom-masonry layout="50-50"> (children) </custom-masonry>
```
# Navigation
Edit your navigation items in `src/_data/navigation.js`.
You have two options for mobile navigation: by default, the navigation on small displays is converted to small pills that wrap. This does not require any additional JavaScript.
### Drawer Menu
You can activate a drawer menu for mobile in `src/_data/meta.js`:
```js
navigation: {
// other settings
drawerNav: true,
},
```
`drawerNav` activates the navigation drawer, [built according to Manuel Matuzović's article on web.dev.](https://web.dev/articles/website-navigation)
Adjust your menu breakpoint in `src/_data/designTokens/viewports.json`
```json
{
// ...
"navigation": 662,
// ...
}
```
### Submenu
You can activate submenus in `src/_data/meta.js`:
```js
navigation: {
// other settings
subMenu: true,
},
```
This includes the JavaScript for the submenu functionality. Add your submenu items to `src/_data/navigation.js` using this structure:
```js
{
text: 'Unlinked parent',
url: '#',
submenu: [
{
text: 'Sub Item',
url: '/sub-item/'
},
... more items
]
},
```
# Pagination
The blog posts use [Eleventy's pagination feature](https://www.11ty.dev/docs/pagination/). The logic for this can be found in tha partial `src/_includes/partials/pagination.njk`, the layout `src/_layouts/blog.njk` includes it, how many entries should be on a page is defined in `src/pages/blog.md`.
If you do not want any pagination at all, it is easiest to set a very high number for the pagination size, for example:
```yaml
pagination:
data: collections.posts
size: 10000
```
In `src/_data_/meta.js` you can set some values for the visible content (previous / next buttons) and the aria labels.
You can also **hide the number fields** between the previous and next buttons by setting `paginationNumbers` to `false`.
```js
blog: {
// other adjustments
paginationLabel: 'Blog',
paginationPage: 'Page',
paginationPrevious: 'Previous',
paginationNext: 'Next',
paginationNumbers: true
}
```
If you want to change the collection that is paginated (by default `collections.posts`), you must do so in two places: the front matter of the template, `src/pages/blog.md`:
```yaml
pagination:
data: collections.posts
```
and where the pagination component is included: `src/_layouts/blog.njk`:
{% raw %}
```jinja2
<!-- set collection to paginate -->
{% set collectionToPaginate = collections.posts %}
<!-- if the number of items in the collection is greater than the number of items shown on one page -->
{% if collectionToPaginate.length > pagination.size %}
<!-- include pagination -->
{% include 'partials/pagination.njk' %}
{% endif %}
```
{% endraw %}
# Cards
WebC, we can use the custom element and opt in to the different slots.
Available slots:
image: has slot="image" on the container (picture or figure) by default)
headline: display the card's main title
date and tag: Grouped within the classes meta and cluster for date and tagging information
content
footer: for links or whatever footer information
I added some variants, avaliable via attribute selectors:
img-square: Enforces a square aspect ratio for images
clickable: Makes the whole card clickable
no-padding: Removes padding and background modifications
Usage
{% raw %}
```html
<custom-card>
{% image "path-to-img", "alt-text" %}
<span slot="date"></span>
<span slot="tag" class="button"></span>
<h2 slot="headline"></h2>
<p slot="content"></p>
<footer slot="footer"></footer>
</custom-card>
```
{% endraw %}

View file

@ -1,59 +1,34 @@
# GSAP Animation System # GSAP Animation System (Punch animations)
This project uses GSAP (GreenSock Animation Platform) for scroll-driven and interactive animations, designed for animated storytelling with low friction. This project uses GSAP for scroll-driven punch animations (zoom in/out effects) via shortcodes. The system is intentionally minimal and focused on high-quality image reveals.
## Architecture Overview ## Architecture Overview
The animation system is split into three main parts: The animation system is split into two main parts:
1. **Shared Effects Library** ([`gsap-effects.js`](../src/assets/scripts/bundle/gsap-effects.js)) 1. **Animations Library** ([`gsap-animations.js`](../src/assets/scripts/bundle/gsap-animations.js))
- Reusable animation effects (fadeIn, shake, zoom, etc.) - Reusable zoom presets: `punchIn`, `punchOut`
- Emotional presets (jumpscare, anticipation, dread, etc.) - Default variables with simple override merging
- Effect composition utilities - `shouldAnimate()` utility respecting `prefers-reduced-motion`
2. **Content Animations** ([`gsap-shortcode-init.js`](../src/assets/scripts/bundle/gsap-shortcode-init.js)) 2. **Content Animations** ([`gsap-shortcode-init.js`](../src/assets/scripts/bundle/gsap-shortcode-init.js))
- Scroll-triggered animations in markdown/blog posts - Scroll-triggered animations in markdown/blog posts
- Low-friction shortcode syntax for content authors - Paired shortcode reads JSON config and passes it through. The shortcode also passes arguments for a Tailwind class name.
- We must remember that this is the only function of Tailwind that we use and that we should consult files in ([`our design tokens`](../src/assets/css/global/global.css)), and be mindful of the fact that these animations will exist as containers of ([`prose.css`](../src/assets/css/global/blocks/prose.css)).
3. **UI Component Animations** ([`mix-nav-animations.js`](../src/assets/scripts/bundle/mix-nav-animations.js))
- Interactive UI animations (hover, click, etc.)
- Component-specific animation logic
--- ---
# For Content Authors # For Content Authors
See [GSAP_USAGE.md](./GSAP_USAGE.md) for the complete usage guide. See [GSAP_USAGE.md](./GSAP_USAGE.md) for shortcode usage.
## Quick Start ## Quick Start
### Emotional Presets (Recommended) ### Punch In Animation
Animate emotions, not numbers:
```markdown ```markdown
{% gsapScrollAnim { "emotion": "jumpscare" } %} {% gsapScrollAnim { "animationType": "punchIn", "focalX": 75, "focalY": 25, "startZoom": 1, "endZoom": 2.5 } %}
[{ "src": "/scary-image.jpg", "alt": "Boo!" }] [{ "src": "/images/detail-photo.jpg", "alt": "Macro photography showing fine detail" }]
{% endgsapScrollAnim %}
```
Available emotions: `jumpscare`, `anticipation`, `dread`, `relief`, `tension`, `excitement`
### Simple Animations
```markdown
{% gsapScrollAnim { "animationType": "fadeIn" } %}
[{ "src": "/image.jpg", "alt": "Description" }]
{% endgsapScrollAnim %}
```
### Effect Composition
Combine multiple effects:
```markdown
{% gsapScrollAnim { "effects": ["fadeIn", "shake", "tremble"] } %}
[{ "src": "/image.jpg", "alt": "Custom combo" }]
{% endgsapScrollAnim %} {% endgsapScrollAnim %}
``` ```
@ -63,33 +38,27 @@ Combine multiple effects:
## Configuration Options ## Configuration Options
All parameters are optional with sensible defaults: All parameters are optional with sensible defaults and fully passed through:
```javascript ```javascript
{ {
"animationType": "fadeIn", // Animation preset (see below) "animationType": "punchIn", // punchIn or punchOut
"scrollStart": "top 80%", // When animation starts "scrollStart": "top 80%", // Timeline start
"scrollEnd": "bottom 20%", // When animation completes "scrollEnd": "bottom 20%", // Timeline end
"scrub": true, // Tied to scroll position (bidirectional) "scrub": true, // Tie progress to scroll
"containerClass": "gsap-container", // CSS class for wrapper "containerClass": "gsap-container",
"pin": false, // Pin element during animation "spillingInto": false, // false | true | 'prose' | 'bleed'
"markers": false // Show debug markers (dev only) "pin": true,
"markers": false,
// Punch-specific overrides
"focalX": 50, // 0-100 (% from left)
"focalY": 50, // 0-100 (% from top)
"startZoom": 1, // 1 for punchIn, 2.5 for punchOut
"endZoom": 2.5, // 2.5 for punchIn, 1 for punchOut
"ease": "power2.inOut"
} }
``` ```
### Animation Presets
- **fadeIn** - Fade in from below (opacity + translate Y)
- **fadeInUp** - Fade in from further below
- **fadeInDown** - Fade in from above
- **scaleIn** - Scale up with fade in (with bounce effect)
- **slideInLeft** - Slide in from left side
- **slideInRight** - Slide in from right side
- **parallax** - Subtle vertical parallax effect
- **stagger** - Sequential animation for multiple items
- **zoomIn** - Start with full image, zoom into focal point
- **zoomOut** - Start zoomed into focal point, pull back to full image
## Image Format ## Image Format
Images are defined as JSON array inside the paired shortcode: Images are defined as JSON array inside the paired shortcode:
@ -108,67 +77,7 @@ Images are defined as JSON array inside the paired shortcode:
Images are automatically optimized via Eleventy Image plugin (WebP/JPEG, responsive sizes). Images are automatically optimized via Eleventy Image plugin (WebP/JPEG, responsive sizes).
## Examples ## Example
### Single Image with Fade In
```markdown
{% gsapScrollAnim {
"animationType": "fadeIn"
} %}
[{
"src": "/images/photo.jpg",
"alt": "Description"
}]
{% endgsapScrollAnim %}
```
### Multiple Images with Stagger
```markdown
{% gsapScrollAnim {
"animationType": "stagger",
"scrollStart": "top 70%"
} %}
[{
"src": "/images/photo1.jpg",
"alt": "First photo",
"caption": "Photo 1"
}, {
"src": "/images/photo2.jpg",
"alt": "Second photo",
"caption": "Photo 2"
}]
{% endgsapScrollAnim %}
```
### Parallax Effect
```markdown
{% gsapScrollAnim {
"animationType": "parallax",
"scrub": true
} %}
[{
"src": "/images/background.jpg",
"alt": "Background scene"
}]
{% endgsapScrollAnim %}
```
### Custom Container Class
```markdown
{% gsapScrollAnim {
"animationType": "scaleIn",
"containerClass": "gsap-container featured-image"
} %}
[{
"src": "/images/hero.jpg",
"alt": "Hero image"
}]
{% endgsapScrollAnim %}
```
### Zoom In to Focal Point ### Zoom In to Focal Point
@ -284,9 +193,9 @@ This shows colored markers in viewport indicating trigger start/end positions.
- ScrollTrigger efficiently batches calculations - ScrollTrigger efficiently batches calculations
- Contexts properly cleaned up on navigation - Contexts properly cleaned up on navigation
## Files Modified ## Files
- **Shortcode**: `src/_config/shortcodes/gsap.js` - Library: `src/assets/scripts/bundle/gsap-animations.js`
- **Initializer**: `src/assets/scripts/bundle/gsap-shortcode-init.js` - Shortcode initializer: `src/assets/scripts/bundle/gsap-shortcode-init.js`
- **CSS**: `src/assets/css/global/utilities/gsap-animations.css` - Shortcode: `src/_config/shortcodes/gsap.js`
- **Config**: Registered in `eleventy.config.js` as paired shortcode - CSS: `src/assets/css/global/utilities/gsap-animations.css`

View file

@ -1,11 +1,11 @@
# GSAP Animation System # GSAP Animation System (Punch animations)
This project uses GSAP for scroll-driven and UI animations, designed for low-friction animated storytelling. This project uses GSAP for scroll-driven punch animations (zoom in/out effects) via shortcodes. The system is intentionally minimal.
## Architecture ## Architecture
### 1. Shared Effects Library (`gsap-effects.js`) ### 1. Animations Library (`gsap-animations.js`)
Contains reusable animation effects and emotional presets. Contains reusable zoom presets and `shouldAnimate()`.
### 2. Content Animations (`gsap-shortcode-init.js`) ### 2. Content Animations (`gsap-shortcode-init.js`)
Handles scroll-triggered animations in markdown/blog posts via shortcodes. Handles scroll-triggered animations in markdown/blog posts via shortcodes.
@ -19,40 +19,29 @@ Handles interactive animations for navigation, buttons, and UI elements.
### Basic Usage ### Basic Usage
Simple fade-in animation: Punch into a focal point on scroll:
```markdown ```markdown
{% gsapScrollAnim { {% gsapScrollAnim {
"animationType": "fadeIn" "animationType": "punchIn",
"focalX": 50,
"focalY": 50,
"startZoom": 1,
"endZoom": 2.5
} %} } %}
[{ [{ "src": "/path/to/high-res-image.jpg", "alt": "Image to punch into" }]
"src": "/path/to/image.jpg",
"alt": "Description"
}]
{% endgsapScrollAnim %} {% endgsapScrollAnim %}
``` ```
### Available Animation Types ### Available Animation Types
- `fadeIn` - Fade in from below - `punchIn` - Punch into image focal point (zoom in)
- `fadeInUp` - Fade in from further below - `punchOut` - Punch out from focal point (zoom out)
- `fadeInDown` - Fade in from above
- `scaleIn` - Scale up from small
- `slideInLeft` - Slide in from left
- `slideInRight` - Slide in from right
- `parallax` - Parallax scroll effect
- `stagger` - Multiple items animate in sequence
- `zoomIn` - Zoom into image focal point
- `zoomOut` - Zoom out from focal point
- `shake` - Shake back and forth
- `tremble` - Subtle continuous trembling
- `wobble` - Wobble rotation
- `pulse` - Continuous pulsing scale
### Zoom Animations with Focal Points ### Punch Animations with Focal Points
```markdown ```markdown
{% gsapScrollAnim { {% gsapScrollAnim {
"animationType": "zoomIn", "animationType": "punchIn",
"focalX": 30, "focalX": 30,
"focalY": 40, "focalY": 40,
"startZoom": 1, "startZoom": 1,
@ -71,88 +60,6 @@ Simple fade-in animation:
--- ---
## Emotional Presets (New!)
Create emotional storytelling without technical details:
### Jumpscare
```markdown
{% gsapScrollAnim {
"emotion": "jumpscare"
} %}
[{ "src": "/scary-image.jpg", "alt": "Boo!" }]
{% endgsapScrollAnim %}
```
Sudden appearance + shake + tremble (like an arrow hitting its mark)
### Anticipation
```markdown
{% gsapScrollAnim {
"emotion": "anticipation",
"scrub": false
} %}
[{ "src": "/windup.jpg", "alt": "Getting ready" }]
{% endgsapScrollAnim %}
```
Pull back, then spring forward (like winding up before a punch)
### Dread
```markdown
{% gsapScrollAnim {
"emotion": "dread"
} %}
[{ "src": "/ominous.jpg", "alt": "Something's coming" }]
{% endgsapScrollAnim %}
```
Slow reveal with unsettling movement
### Relief
```markdown
{% gsapScrollAnim {
"emotion": "relief"
} %}
[{ "src": "/safe-now.jpg", "alt": "Phew" }]
{% endgsapScrollAnim %}
```
Gentle fade in with settling motion
### Tension
```markdown
{% gsapScrollAnim {
"emotion": "tension"
} %}
[{ "src": "/suspense.jpg", "alt": "Building suspense" }]
{% endgsapScrollAnim %}
```
Slow zoom with subtle shake
### Excitement
```markdown
{% gsapScrollAnim {
"emotion": "excitement",
"scrub": false
} %}
[{ "src": "/celebration.jpg", "alt": "Yay!" }]
{% endgsapScrollAnim %}
```
Bouncy entrance with energy
---
## Effect Composition (Advanced)
Combine multiple effects to create custom emotions:
```markdown
{% gsapScrollAnim {
"effects": ["fadeIn", "shake", "tremble"]
} %}
[{ "src": "/custom-combo.jpg", "alt": "Custom animation" }]
{% endgsapScrollAnim %}
```
Effects apply in sequence and can overlap.
--- ---
## Scroll Control Options ## Scroll Control Options
@ -180,88 +87,45 @@ Pin the element in place during animation:
} %} } %}
``` ```
### `markers` (default: `false`) ### `spillingInto` (default: `false`)
Show debug markers (for development): Control layout overflow beyond prose margins:
- `false` - Default: constrained within prose margins
- `true` / `"prose"` - Override margins for responsive bleed
- `"bleed"` - Full-width edge-to-edge breakout
```markdown ```markdown
{% gsapScrollAnim { {% gsapScrollAnim {
"animationType": "fadeIn", "animationType": "punchIn",
"markers": true "spillingInto": "bleed"
} %} } %}
[{ "src": "/path/to/image.jpg", "alt": "Full-width image" }]
{% endgsapScrollAnim %}
``` ```
### `markers` (default: `false`)
Show debug markers (for development).
--- ---
## Multiple Images ## Multiple Images
You can include multiple images; zoom applies to `.gsap-image` elements inside the container.
```markdown
{% gsapScrollAnim {
"animationType": "stagger"
} %}
[{
"src": "/image1.jpg",
"alt": "First",
"caption": "Image 1"
}, {
"src": "/image2.jpg",
"alt": "Second",
"caption": "Image 2"
}, {
"src": "/image3.jpg",
"alt": "Third",
"caption": "Image 3"
}]
{% endgsapScrollAnim %}
```
--- ---
## Tips for Storytelling ## Tips
1. **Use emotions first** - `"emotion": "jumpscare"` is easier than combining effects manually 1. **Scrub for slow reveals** - Set `"scrub": true` for scroll-controlled drama
2. **Scrub for slow reveals** - Set `"scrub": true` for scroll-controlled drama 2. **No scrub for punchy moments** - Set `"scrub": false` for quick actions
3. **No scrub for punchy moments** - Set `"scrub": false` for quick actions 3. **Pin for focus** - Use `"pin": true` to hold attention on an element
4. **Pin for focus** - Use `"pin": true` to hold attention on an element 4. **Zoom needs high-res** - Zoom animations automatically request larger image sizes
5. **Zoom needs high-res** - Zoom animations automatically request larger image sizes
6. **Compose for unique feels** - Combine effects when presets don't fit: `"effects": ["fadeIn", "wobble"]`
--- ---
## For Developers ## For Developers
### Adding New Effects ### Library API
Edit [`gsap-effects.js`](../src/assets/scripts/bundle/gsap-effects.js): Zoom-only presets live in [`gsap-animations.js`](../src/assets/scripts/bundle/gsap-animations.js). Defaults are merged and can be overridden via shortcode config.
```javascript
export const effects = {
myNewEffect: (element, config = {}) => ({
from: {
opacity: 0,
rotationY: 90
},
to: {
opacity: 1,
rotationY: 0,
ease: 'power2.out',
...config
}
})
};
```
### Adding Emotional Presets
```javascript
export const emotions = {
myEmotion: (element, config = {}) => {
const tl = gsap.timeline();
tl.from(element, { /* initial state */ })
.to(element, { /* first animation */ })
.to(element, { /* second animation */ }, '-=0.5'); // overlap
return tl;
}
};
```
### UI Component Animations ### UI Component Animations
@ -269,7 +133,7 @@ Create component-specific files like [`mix-nav-animations.js`](../src/assets/scr
```javascript ```javascript
import gsap from 'gsap'; import gsap from 'gsap';
import { shouldAnimate } from './gsap-effects.js'; import { shouldAnimate } from './gsap-animations.js';
function initMyComponentAnimations() { function initMyComponentAnimations() {
if (!shouldAnimate()) return; if (!shouldAnimate()) return;
@ -298,12 +162,6 @@ All animations respect `prefers-reduced-motion`. Users with this preference will
## Debugging ## Debugging
Enable markers to see scroll trigger points: Enable markers to see scroll trigger points via the shortcode config.
```markdown
{% gsapScrollAnim {
"animationType": "fadeIn",
"markers": true
} %}
```
Check browser console for warnings about missing animation types or configuration errors. Check browser console for warnings about missing animation types or configuration errors.

13
package-lock.json generated
View file

@ -2215,8 +2215,7 @@
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@types/markdown-it": { "node_modules/@types/markdown-it": {
"version": "14.1.2", "version": "14.1.2",
@ -2235,8 +2234,7 @@
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.10.1", "version": "24.10.1",
@ -2746,6 +2744,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.25", "baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754", "caniuse-lite": "^1.0.30001754",
@ -3411,7 +3410,8 @@
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
"dev": true, "dev": true,
"license": "BSD-3-Clause" "license": "BSD-3-Clause",
"peer": true
}, },
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
@ -4858,6 +4858,7 @@
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"argparse": "^2.0.1", "argparse": "^2.0.1",
"entities": "^4.4.0", "entities": "^4.4.0",
@ -5770,6 +5771,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@ -6465,6 +6467,7 @@
"resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz", "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz",
"integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==", "integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"posthtml-parser": "^0.11.0", "posthtml-parser": "^0.11.0",
"posthtml-render": "^3.0.0" "posthtml-render": "^3.0.0"

View file

@ -9,7 +9,10 @@ export const buildJs = async (inputPath, outputPath) => {
entryPoints: [inputPath], entryPoints: [inputPath],
bundle: true, bundle: true,
minify: true, minify: true,
write: false write: false,
define: {
'process.env.ELEVENTY_ENV': JSON.stringify(process.env.ELEVENTY_ENV || 'production')
}
}); });
const output = result.outputFiles[0].text; const output = result.outputFiles[0].text;

View file

@ -59,7 +59,7 @@ export const markdownLib = markdownIt({
const attributes = token.attrs || []; const attributes = token.attrs || [];
const hasEleventyWidths = attributes.some(([key]) => key === 'eleventy:widths'); const hasEleventyWidths = attributes.some(([key]) => key === 'eleventy:widths');
if (!hasEleventyWidths) { if (!hasEleventyWidths) {
attributes.push(['eleventy:widths', '650,960,1400']); attributes.push(['eleventy:widths', '650,960,1400,2400']);
} }
const attributesString = attributes.map(([key, value]) => `${key}="${value}"`).join(' '); const attributesString = attributes.map(([key, value]) => `${key}="${value}"`).join(' ');

View file

@ -1,19 +1,25 @@
import {imageKeysShortcode} from './image.js'; import {imageKeysShortcode} from './image.js';
/** /**
* GSAP Scroll Animation Shortcode * GSAP Scroll Animation Shortcode (prose-only)
* Paired shortcode for creating scroll-controlled GSAP animations with images * Paired shortcode for scroll-triggered punch animations with images.
* Prose-exclusive: used within blog posts and article content.
* *
* Usage: * @param {string} content - JSON array of image objects
* {% gsapScrollAnim { * @param {Object} config - Animation configuration
* animationType: "fadeIn", * @param {string} config.animationType - 'punchIn' | 'punchOut' (default: 'punchIn')
* scrollStart: "top 80%", * @param {string} config.containerClass - Custom wrapper class (default: 'gsap-container')
* scrollEnd: "bottom 20%", * @param {string|boolean} config.spillingInto - Layout overflow: false | true | 'prose' | 'bleed' (default: false)
* scrub: true, * @param {string} config.scrollStart - ScrollTrigger start (default: 'top 80%')
* containerClass: "my-custom-class" * @param {string} config.scrollEnd - ScrollTrigger end (default: 'bottom 20%')
* } %} * @param {boolean} config.scrub - Link to scroll progress (default: true)
* [{ src: "/path/to/image.jpg", alt: "Alt text", caption: "Caption text" }] * @param {boolean} config.pin - Pin during animation (default: true)
* {% endgsapScrollAnim %} * @param {boolean} config.debug - Show markers (default: false)
* @param {number} config.focalX - Punch focal point X: 0-100 (default: 50)
* @param {number} config.focalY - Punch focal point Y: 0-100 (default: 50)
* @param {number} config.startZoom - Start scale (default: 1 for punchIn, 2.5 for punchOut)
* @param {number} config.endZoom - End scale (default: 2.5 for punchIn, 1 for punchOut)
* @param {string} config.ease - Easing function (default: 'power2.inOut')
*/ */
const parseImagesFromContent = content => { const parseImagesFromContent = content => {
@ -44,31 +50,41 @@ export const gsapScrollAnim = async function(content, configString = '{}') {
// Set defaults // Set defaults
const { const {
animationType = 'fadeIn', animationType = 'punchIn',
scrollStart = 'top 80%', scrollStart = 'top 80%',
scrollEnd = 'bottom 20%', scrollEnd = 'bottom 20%',
scrub = true, scrub = true,
containerClass = 'gsap-container', containerClass = 'gsap-container',
pin = false, spillingInto = false,
markers = false // Set to true for debugging pin = true,
debug = false
} = config; } = config;
// Build animation config for data attribute // Build animation config for data attribute
// Pass full config through so defaults can be overridden in the library
const animConfig = { const animConfig = {
...config,
animationType, animationType,
scrollStart, scrollStart,
scrollEnd, scrollEnd,
scrub, scrub,
pin, pin,
markers debug
}; };
// Build container classes with optional spillingInto variant
let finalContainerClass = containerClass;
if (spillingInto === 'bleed') {
finalContainerClass += ' spillingInto spillingInto-bleed';
} else if (spillingInto === true || spillingInto === 'prose') {
finalContainerClass += ' spillingInto';
}
// Parse images from content // Parse images from content
const images = parseImagesFromContent(content); const images = parseImagesFromContent(content);
// Detect if this is a zoom animation (needs high-res images) // Detect if this is a punch animation (needs high-res images)
const isZoomAnimation = animationType === 'zoomIn' || animationType === 'zoomOut' || const isPunchAnimation = animationType === 'punchIn' || animationType === 'punchOut';
animationType.includes('zoom');
// Process images using existing image shortcode // Process images using existing image shortcode
const processedImages = await Promise.all( const processedImages = await Promise.all(
@ -82,8 +98,8 @@ export const gsapScrollAnim = async function(content, configString = '{}') {
loading: index === 0 ? 'eager' : 'lazy', loading: index === 0 ? 'eager' : 'lazy',
imageClass: 'gsap-image', imageClass: 'gsap-image',
containerClass: 'gsap-image-wrapper', containerClass: 'gsap-image-wrapper',
// Inject high-res widths for zoom animations // Inject high-res widths for punch animations
...(isZoomAnimation && { widths: [960, 1400, 2400, 3200] }) ...(isPunchAnimation && { widths: [960, 1400, 2400, 3200] })
}); });
return `<div class="gsap-item" data-image-index="${index}">${imageHtml}</div>`; return `<div class="gsap-item" data-image-index="${index}">${imageHtml}</div>`;
} else if (img.content) { } else if (img.content) {
@ -95,7 +111,7 @@ export const gsapScrollAnim = async function(content, configString = '{}') {
); );
// Build final HTML // Build final HTML
return `<div class="${containerClass}" data-gsap-scroll-anim='${JSON.stringify(animConfig)}'> return `<div class="${finalContainerClass}" data-gsap-scroll-anim='${JSON.stringify(animConfig)}'>
${processedImages.join('\n ')} ${processedImages.join('\n ')}
</div>`; </div>`;
}; };

View file

@ -1,6 +1,6 @@
/** /**
* GSAP Scroll Animation Utilities * GSAP Scroll Animation Utilities
* CSS classes for GSAP scroll-driven animations triggered by shortcodes * Punch animations: default constrained layout, optional spillingInto variant for breakout
*/ */
/* Container for GSAP scroll animations */ /* Container for GSAP scroll animations */
@ -24,13 +24,43 @@
display: block; display: block;
width: 100%; width: 100%;
height: auto; height: auto;
/* GSAP will add will-change when animating */
transform: translateZ(0); transform: translateZ(0);
} }
/* Initial state - GSAP will handle visibility/opacity */ /* Punch animation container: constrained by default */
.gsap-container[data-gsap-scroll-anim] .gsap-item { .gsap-container[data-gsap-scroll-anim*='punch'] {
transform: translateZ(0); overflow: hidden;
position: relative;
}
.gsap-container[data-gsap-scroll-anim*='punch'] .gsap-image-wrapper {
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
}
.gsap-container[data-gsap-scroll-anim*='punch'] .gsap-image {
position: relative;
width: 100%;
height: auto;
display: block;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
/* spillingInto variant: responsive bleed breaking out of prose margins */
.spillingInto {
--wrapper-width: 100%;
}
/* spillingInto-bleed: full-width edge-to-edge */
.spillingInto-bleed {
margin-left: calc(-50vw + 50%);
margin-right: calc(-50vw + 50%);
padding-left: calc(50vw - 50%);
padding-right: calc(50vw - 50%);
width: 100vw;
} }
/* Responsive spacing */ /* Responsive spacing */
@ -38,30 +68,7 @@
margin-top: var(--space-m, 1.5rem); margin-top: var(--space-m, 1.5rem);
} }
/* Zoom animation specific styles */ /* Accessibility: respect prefers-reduced-motion */
.gsap-container[data-gsap-scroll-anim*='zoom'] {
overflow: hidden;
position: relative;
}
.gsap-container[data-gsap-scroll-anim*='zoom'] .gsap-image-wrapper {
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
}
.gsap-container[data-gsap-scroll-anim*='zoom'] .gsap-image {
position: relative;
width: 100%;
height: auto;
display: block;
/* Ensure smooth scaling */
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
/* When animations are disabled (prefers-reduced-motion) */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.gsap-image, .gsap-image,
.gsap-item { .gsap-item {
@ -70,19 +77,3 @@
opacity: 1 !important; opacity: 1 !important;
} }
} }
/* Loading state */
.gsap-container[data-gsap-loading] {
opacity: 0.5;
}
/* Debug mode - shows animation markers */
.gsap-container[data-gsap-debug] {
outline: 2px dashed red;
outline-offset: 4px;
}
.gsap-container[data-gsap-debug] .gsap-item {
outline: 1px dashed blue;
outline-offset: 2px;
}

View file

@ -0,0 +1,73 @@
/**
* GSAP Animations Library (zoom-only)
* Reusable presets with defaults that can be overridden via shortcode config
*/
import gsap from 'gsap';
/**
* Check if user prefers reduced motion
*/
export const shouldAnimate = () =>
!window.matchMedia('(prefers-reduced-motion: reduce)').matches;
/**
* Zoom defaults
*/
const zoomDefaults = {
focalX: 50, // 0-100 (% from left)
focalY: 50, // 0-100 (% from top)
startZoom: 1, // scale
endZoom: 2.5, // scale
ease: 'power2.inOut'
};
/**
* Zoom In: Start with full image visible, zoom into focal point
* config keys: focalX, focalY, startZoom, endZoom, ease
*/
export const punchIn = (element, config = {}) => {
const final = { ...zoomDefaults, ...config };
const { focalX, focalY, startZoom, endZoom, ease } = final;
return {
from: {
scale: startZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0
},
to: {
scale: endZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0,
ease
}
};
};
/**
* Zoom Out: Start zoomed into focal point, pull back to show full image
* config keys: focalX, focalY, startZoom, endZoom, ease
*/
export const punchOut = (element, config = {}) => {
const defaults = { ...zoomDefaults, startZoom: 2.5, endZoom: 1 };
const final = { ...defaults, ...config };
const { focalX, focalY, startZoom, endZoom, ease } = final;
return {
from: {
scale: startZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0
},
to: {
scale: endZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0,
ease
}
};
};
export default { shouldAnimate, punchIn, punchOut };

View file

@ -1,494 +0,0 @@
/**
* GSAP Effects Library
* Shared animation effects for both shortcodes and UI components
*/
import gsap from 'gsap';
/**
* Base animation effects
* Each returns { from, to } objects for GSAP
*/
export const effects = {
fadeIn: (element, config = {}) => ({
from: {
opacity: 0,
y: 50
},
to: {
opacity: 1,
y: 0,
ease: 'power2.out',
...config
}
}),
fadeInUp: (element, config = {}) => ({
from: {
opacity: 0,
y: 100
},
to: {
opacity: 1,
y: 0,
ease: 'power2.out',
...config
}
}),
fadeInDown: (element, config = {}) => ({
from: {
opacity: 0,
y: -100
},
to: {
opacity: 1,
y: 0,
ease: 'power2.out',
...config
}
}),
scaleIn: (element, config = {}) => ({
from: {
opacity: 0,
scale: 0.8
},
to: {
opacity: 1,
scale: 1,
ease: 'back.out(1.7)',
...config
}
}),
slideInLeft: (element, config = {}) => ({
from: {
opacity: 0,
x: -100
},
to: {
opacity: 1,
x: 0,
ease: 'power2.out',
...config
}
}),
slideInRight: (element, config = {}) => ({
from: {
opacity: 0,
x: 100
},
to: {
opacity: 1,
x: 0,
ease: 'power2.out',
...config
}
}),
parallax: (element, config = {}) => ({
from: {
y: -100
},
to: {
y: 100,
ease: 'none',
...config
}
}),
stagger: (element, config = {}) => ({
from: {
opacity: 0,
y: 50
},
to: {
opacity: 1,
y: 0,
ease: 'power2.out',
stagger: 0.2,
...config
}
}),
shake: (element, config = {}) => ({
from: {},
to: {
x: 0,
duration: 0.1,
repeat: 5,
yoyo: true,
ease: 'power1.inOut',
keyframes: {
x: [-5, 5, -4, 4, -3, 3, -2, 2, -1, 0]
},
...config
}
}),
tremble: (element, config = {}) => ({
from: {},
to: {
rotation: 0,
duration: 0.05,
repeat: -1,
yoyo: true,
ease: 'none',
keyframes: {
rotation: [-1, 1, -1, 1]
},
...config
}
}),
pulse: (element, config = {}) => ({
from: {},
to: {
scale: 1,
duration: 0.8,
repeat: -1,
yoyo: true,
ease: 'power1.inOut',
keyframes: {
scale: [1, 1.05, 1]
},
...config
}
}),
wobble: (element, config = {}) => ({
from: {},
to: {
rotation: 0,
duration: 0.3,
repeat: 3,
yoyo: true,
ease: 'power1.inOut',
keyframes: {
rotation: [-5, 5, -3, 3, -1, 0]
},
...config
}
}),
zoomIn: (element, config = {}) => {
const {
focalX = 50,
focalY = 50,
startZoom = 1,
endZoom = 2.5
} = config || {};
return {
from: {
scale: startZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0
},
to: {
scale: endZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0,
ease: 'power2.inOut',
...config
}
};
},
zoomOut: (element, config = {}) => {
const {
focalX = 50,
focalY = 50,
startZoom = 2.5,
endZoom = 1
} = config || {};
return {
from: {
scale: startZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0
},
to: {
scale: endZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0,
ease: 'power2.inOut',
...config
}
};
},
vibrate: (target, config = {}) => {
return {
to: {
x: () => gsap.utils.random(-5, 5),
y: () => gsap.utils.random(-5, 5),
duration: 0.05,
repeat: config.repeat ?? 10,
ease: 'none',
...config
}
};
}
};
/**
* animation combos, express emtions, tell a story
* ****************************************************
* ****************************************************
* ****************************************************
*****************************************************/
export const emotions = {
/**
* jumpscare
* ****************************************************
* ****************************************************
* ****************************************************
*****************************************************/
jumpscare: (element, config = {}) => {
const tl = gsap.timeline();
tl.from(element, {
scale: 0.5,
opacity: 0,
duration: 0.1,
ease: 'power4.out'
})
.to(element, {
x: -15,
y: -15,
rotation: 30,
duration: 1,
repeat: 3,
yoyo: true
})
.to(element, {
rotation: -2,
duration: 0.05,
repeat: -1,
yoyo: true
});
return tl;
},
/**
* anticipation
* ****************************************************
* ****************************************************
* ****************************************************
*****************************************************/
anticipation: (element, config = {}) => {
const tl = gsap.timeline();
tl.to(element, {
scale: 0.95,
duration: 0.3,
ease: 'power2.in'
})
.to(element, {
scale: 1.1,
duration: 0.2,
ease: 'back.out(4)'
})
.to(element, {
scale: 1,
duration: 0.2,
ease: 'power1.out'
});
return tl;
},
/**
* dread, alledgedly
* ****************************************************
* ****************************************************
* ****************************************************
*****************************************************/
dread: (element, config = {}) => {
const tl = gsap.timeline();
tl.from(element, {
opacity: 0,
scale: 1.2,
duration: 2,
ease: 'power1.in'
})
.to(element, {
rotation: -1,
duration: 0.1,
repeat: -1,
yoyo: true,
ease: 'none'
}, '-=1.5');
return tl;
},
/**
* relief, alledgedly
* ****************************************************
* ****************************************************
* ****************************************************
*****************************************************/
relief: (element, config = {}) => {
const tl = gsap.timeline();
tl.from(element, {
opacity: 0,
y: -30,
duration: 1,
ease: 'power2.out'
})
.to(element, {
y: 5,
duration: 0.4,
ease: 'power1.inOut'
})
.to(element, {
y: 0,
duration: 0.3,
ease: 'power1.out'
});
return tl;
},
/**
* tension
* ****************************************************
* ****************************************************
* ****************************************************
*****************************************************/
tension: (element, config = {}) => {
const tl = gsap.timeline();
tl.from(element, {
scale: 1,
duration: 3,
ease: 'none'
})
.to(element, {
scale: 1.15,
duration: 3,
ease: 'power1.in'
}, 0)
.to(element, {
x: -1,
duration: 0.1,
repeat: -1,
yoyo: true,
ease: 'none'
}, 1);
return tl;
},
/**
* excitement
* ****************************************************
* ****************************************************
* ****************************************************
*****************************************************/
excitement: (element, config = {}) => {
const tl = gsap.timeline();
tl.from(element, {
opacity: 0,
scale: 0,
duration: 0.3,
ease: 'back.out(3)'
})
.to(element, {
y: -10,
duration: 0.3,
ease: 'power2.out'
})
.to(element, {
y: 0,
duration: 0.3,
ease: 'bounce.out'
});
return tl;
},
/**
* Image Swap: Swap image source at scroll position and vibrate
*/
imageSwap: (element, config = {}) => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: element,
start: 'center center',
toggleActions: 'play none none none',
once: true,
markers: config.markers || false,
...config.scrollTrigger
}
});
// Get both images from config or data attributes
const img = element.querySelector('img');
const secondSrc = config.secondImage || element.dataset.secondImage;
if (!secondSrc || !img) {
console.warn('imageSwap: No second image specified or img element not found', element);
return tl;
}
// Swap image and vibrate simultaneously
tl.call(() => {
img.src = secondSrc;
// Also update srcset if it exists
if (img.srcset) {
img.srcset = secondSrc;
}
})
.to(element, {
x: () => gsap.utils.random(-5, 5),
y: () => gsap.utils.random(-5, 5),
duration: 0.05,
repeat: config.vibrateRepeats ?? 15,
ease: 'none'
}, 0); // 0 means start immediately
return tl;
}
};
/**
* Check if user prefers reduced motion
*/
export const shouldAnimate = () =>
!window.matchMedia('(prefers-reduced-motion: reduce)').matches;
/**
* Apply multiple effects to an element
* @param {Element} element - Target element
* @param {Array} effectNames - Array of effect names to apply
* @param {Object} config - Configuration for effects
*/
export const composeEffects = (element, effectNames, config = {}) => {
if (!shouldAnimate()) return gsap.timeline();
const tl = gsap.timeline();
effectNames.forEach((effectName, index) => {
const effect = effects[effectName];
if (effect) {
const { from, to } = effect(element, config[effectName] || {});
if (index === 0 && Object.keys(from).length > 0) {
tl.from(element, from);
}
if (Object.keys(to).length > 0) {
// If this is a looping effect (repeat: -1), add it at the start
if (to.repeat === -1) {
tl.to(element, to, 0);
} else {
tl.to(element, to);
}
}
}
});
return tl;
};
export default { effects, emotions, shouldAnimate, composeEffects };

View file

@ -1,178 +1,30 @@
/** /**
* GSAP Scroll Animation Initializer Initialize GSAP on pages with Turbo frames */
* Handles scroll-driven animations triggered by gsapScrollAnim shortcode
* Compatible with Hotwired Turbo navigation
*/
import gsap from 'gsap'; import gsap from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger'; import ScrollTrigger from 'gsap/ScrollTrigger';
import { effects, emotions, shouldAnimate, composeEffects } from './gsap-effects.js'; import { shouldAnimate, punchIn, punchOut } from './gsap-animations.js';
// Register GSAP plugins // Register GSAP plugins
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
// Register GSDevTools in development only (esbuild inlines ELEVENTY_ENV at build time)
if (process.env.ELEVENTY_ENV === 'development') {
try {
// Dynamic import wrapped in try-catch; GSDevTools package must be installed
import('gsap/GSDevTools').then(module => {
gsap.registerPlugin(module.default);
}).catch(() => {
// Silently continue if GSDevTools is not available
});
} catch (e) {
// Fallback for any import errors
}
}
// Store active contexts for cleanup // Store active contexts for cleanup
const activeContexts = new Map(); const activeContexts = new Map();
/**
* Animation presets (use effects from gsap-effects.js, but kept for backward compatibility)
* Each returns GSAP animation properties for the given element(s)
*/
const animations = {
fadeIn: (element) => ({
from: {
opacity: 0,
y: 50
},
to: {
opacity: 1,
y: 0,
ease: 'power2.out'
}
}),
fadeInUp: (element) => ({
from: {
opacity: 0,
y: 100
},
to: {
opacity: 1,
y: 0,
ease: 'power2.out'
}
}),
fadeInDown: (element) => ({
from: {
opacity: 0,
y: -100
},
to: {
opacity: 1,
y: 0,
ease: 'power2.out'
}
}),
scaleIn: (element) => ({
from: {
opacity: 0,
scale: 0.8
},
to: {
opacity: 1,
scale: 1,
ease: 'back.out(1.7)'
}
}),
slideInLeft: (element) => ({
from: {
opacity: 0,
x: -100
},
to: {
opacity: 1,
x: 0,
ease: 'power2.out'
}
}),
slideInRight: (element) => ({
from: {
opacity: 0,
x: 100
},
to: {
opacity: 1,
x: 0,
ease: 'power2.out'
}
}),
parallax: (element) => ({
from: {
y: -100
},
to: {
y: 100,
ease: 'none'
}
}),
stagger: (element) => ({
from: {
opacity: 0,
y: 50
},
to: {
opacity: 1,
y: 0,
ease: 'power2.out',
stagger: 0.2
}
}),
/**
* Zoom In: Start with full image visible, zoom into focal point
* Requires: focalX, focalY (0-100%), startZoom, endZoom
*/
zoomIn: (element, config) => {
const {
focalX = 50,
focalY = 50,
startZoom = 1,
endZoom = 2.5
} = config || {};
return {
from: {
scale: startZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0
},
to: {
scale: endZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0,
ease: 'power2.inOut'
}
};
},
/**
* Zoom Out: Start zoomed into focal point, pull back to show full image
* Requires: focalX, focalY (0-100%), startZoom, endZoom
*/
zoomOut: (element, config) => {
const {
focalX = 50,
focalY = 50,
startZoom = 2.5,
endZoom = 1
} = config || {};
return {
from: {
scale: startZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0
},
to: {
scale: endZoom,
transformOrigin: `${focalX}% ${focalY}%`,
x: 0,
y: 0,
ease: 'power2.inOut'
}
};
}
};
/** /**
* Initialize GSAP animations for all containers * Initialize GSAP animations for all containers
*/ */
@ -195,12 +47,10 @@ function initGsapAnimations() {
// Extract configuration with defaults // Extract configuration with defaults
const { const {
animationType, animationType,
emotion,
effects: effectsList,
scrollStart = 'top 80%', scrollStart = 'top 80%',
scrollEnd = 'bottom 20%', scrollEnd = 'bottom 20%',
scrub = true, scrub = true,
pin = false, pin = true,
markers = false markers = false
} = config; } = config;
@ -212,68 +62,12 @@ function initGsapAnimations() {
const ctx = gsap.context(() => { const ctx = gsap.context(() => {
let timeline; let timeline;
// Handle emotional presets (take priority) if (animationType === 'punchIn' || animationType === 'punchOut') {
if (emotion && emotions[emotion]) { const targetElements = container.querySelectorAll('.gsap-image');
timeline = gsap.timeline({
scrollTrigger: {
trigger: container,
start: scrollStart,
end: scrollEnd,
scrub: scrub ? 1 : false,
markers: markers,
pin: pin,
toggleActions: scrub ? undefined : 'play reverse play reverse',
onEnter: () => container.dataset.gsapActive = 'true',
onLeave: () => container.dataset.gsapActive = 'false',
onEnterBack: () => container.dataset.gsapActive = 'true',
onLeaveBack: () => container.dataset.gsapActive = 'false'
}
});
// Apply emotional preset to each item const anim = animationType === 'punchIn'
items.forEach(item => { ? punchIn(targetElements, config)
const emotionTl = emotions[emotion](item, config); : punchOut(targetElements, config);
timeline.add(emotionTl, 0); // Add at start
});
}
// Handle effect composition (multiple effects)
else if (effectsList && Array.isArray(effectsList)) {
timeline = gsap.timeline({
scrollTrigger: {
trigger: container,
start: scrollStart,
end: scrollEnd,
scrub: scrub ? 1 : false,
markers: markers,
pin: pin,
toggleActions: scrub ? undefined : 'play reverse play reverse',
onEnter: () => container.dataset.gsapActive = 'true',
onLeave: () => container.dataset.gsapActive = 'false',
onEnterBack: () => container.dataset.gsapActive = 'true',
onLeaveBack: () => container.dataset.gsapActive = 'false'
}
});
// Apply each effect in sequence
items.forEach(item => {
const composedTl = composeEffects(item, effectsList, config);
timeline.add(composedTl, 0);
});
}
// Handle single animation type (original behavior)
else if (animationType) {
const animationPreset = animations[animationType];
if (!animationPreset) {
console.warn(`Unknown animation type: ${animationType}`);
return;
}
// For zoom animations, apply to images directly
const targetElements = (animationType === 'zoomIn' || animationType === 'zoomOut')
? container.querySelectorAll('.gsap-image')
: items;
const anim = animationPreset(targetElements, config);
// Create timeline with ScrollTrigger // Create timeline with ScrollTrigger
timeline = gsap.timeline({ timeline = gsap.timeline({
@ -301,7 +95,7 @@ function initGsapAnimations() {
timeline.to(targetElements, anim.to); timeline.to(targetElements, anim.to);
} }
} else { } else {
console.warn('No animation type, emotion, or effects specified', config); console.warn('Unsupported animation type. Only zoomIn/zoomOut are available.', config);
return; return;
} }
}, container); }, container);
@ -368,4 +162,4 @@ window.addEventListener('resize', () => {
}); });
// Export for manual control if needed // Export for manual control if needed
export { initGsapAnimations, cleanupGsapAnimations, refreshScrollTrigger, animations }; export { initGsapAnimations, cleanupGsapAnimations, refreshScrollTrigger };

View file

@ -3,7 +3,7 @@
* GSAP animations for mix track navigation UI elements * GSAP animations for mix track navigation UI elements
*/ */
import gsap from 'gsap'; import gsap from 'gsap';
import { shouldAnimate } from './gsap-effects.js'; import { shouldAnimate } from './gsap-animations.js';
/** /**
* Initialize mix navigation animations * Initialize mix navigation animations

View file

@ -1,352 +0,0 @@
---
title: GSAP Animation Reference
description: Visual guide to all available GSAP animations and how to use them
layout: post
permalink: 'docs/animations/index.html'
---
# GSAP Animation Reference
A visual reference for all available scroll-driven animations. Scroll down to see each effect in action!
## Quick Start
```markdown
{% raw %}{% gsapScrollAnim { "animationType": "fadeIn" } %}
[{ "src": "/path/to/image.jpg", "alt": "My image" }]
{% endgsapScrollAnim %}{% endraw %}
```
---
## Basic Effects
### Fade In
Gentle fade and slide up entrance.
{% gsapScrollAnim { "animationType": "fadeIn", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/gorillaz-cracker-island.jpg", "alt": "Fade In Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim { "animationType": "fadeIn" } %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
### Fade In Up
Strong upward entrance.
{% gsapScrollAnim { "animationType": "fadeInUp", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg", "alt": "Fade In Up Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim { "animationType": "fadeInUp" } %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
### Fade In Down
Drops in from above.
{% gsapScrollAnim { "animationType": "fadeInDown", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/the-clash-london-calling.jpeg", "alt": "Fade In Down Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim { "animationType": "fadeInDown" } %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
### Scale In
Grows from center with bounce.
{% gsapScrollAnim { "animationType": "scaleIn", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": false } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/james-whiplash.jpg", "alt": "Scale In Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim { "animationType": "scaleIn", "scrub": false } %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
### Slide In Left
{% gsapScrollAnim { "animationType": "slideInLeft", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/rtj-rtj4.jpg", "alt": "Slide In Left Demo" }]
{% endgsapScrollAnim %}
### Slide In Right
{% gsapScrollAnim { "animationType": "slideInRight", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/ride-nowhere.jpg", "alt": "Slide In Right Demo" }]
{% endgsapScrollAnim %}
---
## Zoom Effects
### Zoom In
Slow zoom into the image as you scroll.
{% gsapScrollAnim { "animationType": "zoomIn", "scrollStart": "top 80%", "scrollEnd": "middle middle", "scrub": true } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/modest-mouse-we-were-dead.jpg", "alt": "Zoom In Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim {
"animationType": "zoomIn",
"focalX": 50,
"focalY": 50,
"startZoom": 1,
"endZoom": 2.5,
"scrub": true
} %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
### Zoom Out
Reverse zoom effect.
{% gsapScrollAnim { "animationType": "zoomOut", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/morphine-yes.jpg", "alt": "Zoom Out Demo" }]
{% endgsapScrollAnim %}
---
## Emotional Presets
These animations tell stories, not just numbers.
### Jumpscare 💥
Sudden, intense appearance like an arrow hitting its mark.
{% gsapScrollAnim { "emotion": "jumpscare", "scrub": false } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/james-whiplash.jpg", "alt": "Jumpscare Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim { "emotion": "jumpscare", "scrub": false } %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
### Anticipation ⏳
Wind up before the punch - builds tension.
{% gsapScrollAnim { "emotion": "anticipation", "scrub": false } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/gorillaz-cracker-island.jpg", "alt": "Anticipation Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim { "emotion": "anticipation", "scrub": false } %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
### Dread 😰
Something ominous slowly approaches.
{% gsapScrollAnim { "emotion": "dread", "scrub": true } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg", "alt": "Dread Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim { "emotion": "dread", "scrub": true } %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
### Relief 😌
Everything's going to be okay - gentle, calming.
{% gsapScrollAnim { "emotion": "relief", "scrub": false } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/the-clash-london-calling.jpeg", "alt": "Relief Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim { "emotion": "relief", "scrub": false } %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
### Tension 😬
Slow zoom with subtle shake - something's wrong.
{% gsapScrollAnim { "emotion": "tension", "scrub": true } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/bjork-all-is-full-of-love.jpg", "alt": "Tension Demo" }]
{% endgsapScrollAnim %}
### Excitement 🎉
Bouncy, energetic entrance.
{% gsapScrollAnim { "emotion": "excitement", "scrub": false } %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/parquet-courts-wide-awake.png", "alt": "Excitement Demo" }]
{% endgsapScrollAnim %}
---
## Advanced: Image Swap 🔄
### Emotion Preset (Easier)
Swap images when scrolled halfway past, with vibration effect.
{% gsapScrollAnim {
"emotion": "imageSwap",
"secondImage": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg",
"vibrateRepeats": 20
} %}
[{
"src": "/pages/projects/mixes/tomorrowsbacon/gorillaz-cracker-island.jpg",
"alt": "Image Swap Emotion Demo",
"data-second-image": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg"
}]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim {
"emotion": "imageSwap",
"secondImage": "/path/to/second-image.jpg",
"vibrateRepeats": 20
} %}
[{
"src": "/path/to/first-image.jpg",
"alt": "Demo",
"data-second-image": "/path/to/second-image.jpg"
}]
{% endgsapScrollAnim %}{% endraw %}
```
### Effects Composition (More Control)
Combine vibrate with other effects for custom animations.
{% gsapScrollAnim {
"effects": ["fadeIn", "vibrate"],
"scrollStart": "top 80%",
"scrollEnd": "bottom 20%",
"scrub": false
} %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/operation-ivy-energy.jpg", "alt": "Vibrate Effect Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim {
"effects": ["fadeIn", "vibrate"],
"scrub": false
} %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
---
## Effect Composition
Combine multiple effects to create unique animations.
### Fade + Shake
{% gsapScrollAnim {
"effects": ["fadeIn", "shake"],
"scrollStart": "top 80%",
"scrollEnd": "bottom 20%",
"scrub": false
} %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/ramones-mania.jpg", "alt": "Fade + Shake Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim {
"effects": ["fadeIn", "shake"],
"scrub": false
} %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
### Scale + Wobble + Pulse
{% gsapScrollAnim {
"effects": ["scaleIn", "wobble", "pulse"],
"scrollStart": "top 80%",
"scrollEnd": "bottom 20%",
"scrub": false
} %}
[{ "src": "/pages/projects/mixes/tomorrowsbacon/van-halen-van-halen.jpg", "alt": "Triple Combo Demo" }]
{% endgsapScrollAnim %}
```markdown
{% raw %}{% gsapScrollAnim {
"effects": ["scaleIn", "wobble", "pulse"],
"scrub": false
} %}
[{ "src": "/image.jpg", "alt": "Demo" }]
{% endgsapScrollAnim %}{% endraw %}
```
---
## Configuration Options
### Scrub
- `"scrub": true` - Animation progress tied to scroll position (smooth)
- `"scrub": false` - Animation plays once when triggered
### Scroll Triggers
- `"scrollStart": "top 80%"` - When to start (element position + viewport position)
- `"scrollEnd": "bottom 20%"` - When to end
- Common values: `"top center"`, `"center center"`, `"bottom top"`
### Pin
- `"pin": true` - Pin element in place while animating
- `"pin": false` - Element scrolls normally
### Markers
- `"markers": true` - Show debug markers (for development)
- `"markers": false` - Hide markers (for production)
---
## Tips & Best Practices
1. **Use emotions for storytelling** - They're designed to evoke feelings
2. **Scrub for cinematic effects** - Ties animation to scroll for precise control
3. **No scrub for surprise** - Let animations play independently
4. **Compose effects carefully** - Too many can be overwhelming
5. **Test on mobile** - Animations may need adjustment for smaller screens
6. **Respect reduced motion** - All animations respect `prefers-reduced-motion` setting
---
## All Available Effects
**Base Effects:**
- `fadeIn`, `fadeInUp`, `fadeInDown`
- `scaleIn`
- `slideInLeft`, `slideInRight`
- `parallax`
- `stagger`
- `shake`, `tremble`, `pulse`, `wobble`
- `zoomIn`, `zoomOut`
- `vibrate`
**Emotional Presets:**
- `jumpscare` - Sudden impact
- `anticipation` - Building tension
- `dread` - Ominous approach
- `relief` - Calming resolution
- `tension` - Slow building stress
- `excitement` - Bouncy energy
- `imageSwap` - Image replacement with vibration
**Composition:**
- Combine any effects using `"effects": ["effect1", "effect2"]`
- Effects can be layered for unique results
---
<p class="text-step-min-1"><a href="https://git.hypnagaga.com/wires/hypnagaga/src/branch/main/src/pages/gsap-animations.md">View this page's source code</a> to see exactly how each animation is configured.</p>

View file

@ -1,123 +0,0 @@
---
title: 'GSAP Emotional Animation Demo'
description: "Demonstrating the new emotional animation presets"
date: 2026-01-05
tags: ['test', 'gsap']
---
## Emotional Presets Demo
Testing the new emotional animation system that lets you animate feelings, not just numbers.
### Jumpscare
Like an arrow hitting its mark - sudden appearance with impact:
{% gsapScrollAnim {
"emotion": "jumpscare",
"scrub": true
} %}
[{
"src": "/pages/projects/mixes/tomorrowsbacon/james-whiplash.jpg",
"alt": "Sudden impact!"
}]
{% endgsapScrollAnim %}
### Anticipation
Wind up before the punch:
{% gsapScrollAnim {
"emotion": "anticipation",
"scrub": false,
"scrollStart": "top 75%"
} %}
[{
"src": "/pages/projects/mixes/tomorrowsbacon/gorillaz-cracker-island.jpg",
"alt": "Building up..."
}]
{% endgsapScrollAnim %}
### Dread
Something ominous approaches:
{% gsapScrollAnim {
"emotion": "dread",
"scrub": true
} %}
[{
"src": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg",
"alt": "Unsettling feeling"
}]
{% endgsapScrollAnim %}
### Relief
Everything's going to be okay:
{% gsapScrollAnim {
"emotion": "relief",
"scrub": false
} %}
[{
"src": "/pages/projects/mixes/tomorrowsbacon/the-clash-london-calling.jpeg",
"alt": "Phew, safe now"
}]
{% endgsapScrollAnim %}
### Excitement
Bouncy, energetic entrance:
{% gsapScrollAnim {
"emotion": "excitement",
"scrub": false
} %}
[{
"src": "/pages/projects/mixes/tomorrowsbacon/james-whiplash.jpg",
"alt": "So exciting!"
}]
{% endgsapScrollAnim %}
## Effect Composition
Combining multiple effects to create custom emotions:
{% gsapScrollAnim {
"effects": ["fadeIn", "shake"],
"scrub": true
} %}
[{
"src": "/pages/projects/mixes/tomorrowsbacon/gorillaz-cracker-island.jpg",
"alt": "Fade in + shake combo"
}]
{% endgsapScrollAnim %}
### Triple Combo
{% gsapScrollAnim {
"effects": ["scaleIn", "wobble", "pulse"],
"scrub": false
} %}
[{
"src": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg",
"alt": "Scale + wobble + pulse"
}]
{% endgsapScrollAnim %}
---
## Simple Animations Still Work
The original simple syntax still works perfectly:
{% gsapScrollAnim {
"animationType": "fadeIn",
"scrub": true
} %}
[{
"src": "/pages/projects/mixes/tomorrowsbacon/the-clash-london-calling.jpeg",
"alt": "Classic fade in"
}]
{% endgsapScrollAnim %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -1,80 +1,37 @@
--- ---
title: 'Testing' title: 'Testing'
description: "Test animations and such here." description: "Test animations and such here."
date: 2026-01-04 date: 2026-12-12
tags: ['test'] tags: ['test']
--- ---
## GSAP Scroll Animations ## GSAP Proving Ground
Testing the `gsapScrollAnim` shortcode with scroll-driven animations: Putting the `gsapScrollAnim` shortcode through its paces.
{% gsapScrollAnim {
"animationType": "fadeIn",
"scrollStart": "top 80%",
"scrub": true
} %}
[{
"src": "/pages/projects/mixes/tomorrowsbacon/james-whiplash.jpg",
"alt": "Miles Teller as Andrew Neiman from Whiplash",
"caption": "Testing scroll-driven fade in animation"
}]
{% endgsapScrollAnim %}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam a sem ultrices, congue dui vitae, sodales augue. Mauris congue libero vitae nisi ullamcorper, nec laoreet sapien commodo. Vivamus dignissim urna et metus fermentum porta. Vivamus tempor tortor turpis, in pellentesque nisl viverra eget. Phasellus sed ligula quis massa commodo sagittis. Duis eu rhoncus augue. Vestibulum pretium convallis velit eget pharetra. Nam dapibus lacus eu cursus eleifend. Proin condimentum eros et est volutpat, vitae ullamcorper nulla vulputate. Pellentesque facilisis sem id nulla sodales, eu fermentum erat finibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam a sem ultrices, congue dui vitae, sodales augue. Mauris congue libero vitae nisi ullamcorper, nec laoreet sapien commodo. Vivamus dignissim urna et metus fermentum porta. Vivamus tempor tortor turpis, in pellentesque nisl viverra eget. Phasellus sed ligula quis massa commodo sagittis. Duis eu rhoncus augue. Vestibulum pretium convallis velit eget pharetra. Nam dapibus lacus eu cursus eleifend. Proin condimentum eros et est volutpat, vitae ullamcorper nulla vulputate. Pellentesque facilisis sem id nulla sodales, eu fermentum erat finibus.
{% gsapScrollAnim {
"animationType": "slideInRight",
"scrollStart": "top 70%",
"scrub": true,
"containerClass": "gsap-container custom-spacing"
} %}
[{
"src": "/pages/projects/mixes/tomorrowsbacon/gorillaz-cracker-island.jpg",
"alt": "Gorillaz Cracker Island album cover",
"caption": "Image 1 - Stagger animation"
}, {
"src": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg",
"alt": "Radiohead OK Computer album cover",
"caption": "Image 2 - Stagger animation"
}, {
"src": "/pages/projects/mixes/tomorrowsbacon/the-clash-london-calling.jpeg",
"alt": "The Clash London Calling album cover",
"caption": "Image 3 - Stagger animation"
}]
{% endgsapScrollAnim %}
Proin id risus venenatis arcu sollicitudin venenatis. Ut quam nisl, commodo non urna ac, aliquam ullamcorper justo. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean sed dignissim massa, ac pulvinar sem. Etiam elementum justo lectus, nec blandit tortor varius sollicitudin. Nam tincidunt eros non sodales gravida. Donec pellentesque diam ante, sit amet ullamcorper urna efficitur ut. Quisque ut felis id nisi condimentum rhoncus non non eros. Mauris dapibus id magna et hendrerit. Phasellus in imperdiet quam. Vestibulum euismod leo at augue aliquam venenatis. Nullam quis nisi laoreet nisi laoreet ultricies. Morbi ut facilisis libero. Aliquam odio nunc, sagittis vel elit nec, finibus tristique velit. Nunc suscipit venenatis magna ut aliquet. Praesent sodales orci gravida facilisis finibus.
Donec non pretium lectus. Aliquam pulvinar mattis egestas. Phasellus sit amet magna maximus velit dictum convallis. Nam mollis porttitor libero eu ornare. Cras sit amet leo ac mauris lacinia sodales. Maecenas pretium eu sapien sit amet interdum. Mauris sodales accumsan nibh vitae facilisis. Duis leo massa, placerat ut luctus quis, condimentum id dolor. Ut ut velit a ipsum mattis maximus. Praesent porta justo a lectus pretium iaculis.
Nullam congue lectus a convallis dictum. Nunc rhoncus, ante id porta tempor, lorem ex molestie justo, eget egestas risus urna sit amet ipsum. Etiam dapibus eu eros vitae pharetra. Mauris dapibus erat sit amet quam semper laoreet. Suspendisse potenti. Curabitur et dolor quis nulla ultrices varius. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Sed auctor faucibus finibus. Integer porttitor, mauris sit amet interdum imperdiet, sapien tellus fermentum metus, non accumsan augue sapien nec ligula.
Suspendisse eu rutrum dolor. Mauris semper, eros quis dapibus euismod, ante mi tempor libero, vel mollis ipsum dolor vel nulla. Sed cursus aliquam luctus. Suspendisse egestas erat ante, fringilla aliquam urna ullamcorper ac. Aliquam facilisis sed magna et bibendum. Nam sed justo a sapien vulputate dignissim vitae imperdiet enim. Integer mi ex, imperdiet porttitor posuere ut, placerat id magna. Pellentesque imperdiet porta blandit. Mauris eget rutrum sapien, luctus consequat ligula. Nullam sed ipsum in velit commodo rutrum. Aenean eu massa in dolor accumsan commodo. Sed vitae fermentum dolor, non vestibulum mi. Sed ac orci ac metus porta finibus.
## Zoom Animation Test
Testing the `zoomIn` effect to draw attention to album artwork details:
{% gsapScrollAnim { {% gsapScrollAnim {
"animationType": "zoomIn", "animationType": "punchIn",
"focalX": 50, "focalX": 90,
"focalY": 50, "focalY": 60,
"startZoom": 1, "startZoom": 1,
"endZoom": 2.5, "endZoom": 2.5,
"scrollStart": "top top", "scrollStart": "middle middle",
"scrollEnd": "+=100%", "scrollEnd": "+=100%",
"scrub": true, "scrub": true,
"pin": true "pin": true,
"debug": true
} %} } %}
[{ [{
"src": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg", "src": "/posts/2025/testing/swanpoint.jpg",
"alt": "Radiohead OK Computer album cover - zoom to center detail" "alt": "alt text",
"caption": "This is a caption. Have we assigned it a class?"
}] }]
{% endgsapScrollAnim %} {% endgsapScrollAnim %}
Mauris dignissim nisl et sem condimentum, eu molestie enim cursus. Proin varius tincidunt odio, ac varius ex faucibus quis. Cras ultrices dolor in mauris varius, eu vehicula nisi fringilla. Suspendisse eu rutrum dolor. Mauris semper, eros quis dapibus euismod, ante mi tempor libero, vel mollis ipsum dolor vel nulla. Sed cursus aliquam luctus. Suspendisse egestas erat ante, fringilla aliquam urna ullamcorper ac. Aliquam facilisis sed magna et bibendum. Nam sed justo a sapien vulputate dignissim vitae imperdiet enim. Integer mi ex, imperdiet porttitor posuere ut, placerat id magna. Pellentesque imperdiet porta blandit. Mauris eget rutrum sapien, luctus consequat ligula. Nullam sed ipsum in velit commodo rutrum. Aenean eu massa in dolor accumsan commodo. Sed vitae fermentum dolor, non vestibulum mi. Sed ac orci ac metus porta finibus.
{% gsapScrollAnim { {% gsapScrollAnim {
"animationType": "zoomOut", "animationType": "zoomOut",
@ -95,3 +52,9 @@ Mauris dignissim nisl et sem condimentum, eu molestie enim cursus. Proin varius
Nullam quis facilisis mi. Sed dignissim tellus ut nisi tempor, eu ultrices libero consectetur. Integer et augue vitae nunc consequat elementum. Nullam quis facilisis mi. Sed dignissim tellus ut nisi tempor, eu ultrices libero consectetur. Integer et augue vitae nunc consequat elementum.
Proin id risus venenatis arcu sollicitudin venenatis. Ut quam nisl, commodo non urna ac, aliquam ullamcorper justo. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean sed dignissim massa, ac pulvinar sem. Etiam elementum justo lectus, nec blandit tortor varius sollicitudin. Nam tincidunt eros non sodales gravida. Donec pellentesque diam ante, sit amet ullamcorper urna efficitur ut. Quisque ut felis id nisi condimentum rhoncus non non eros. Mauris dapibus id magna et hendrerit. Phasellus in imperdiet quam. Vestibulum euismod leo at augue aliquam venenatis. Nullam quis nisi laoreet nisi laoreet ultricies. Morbi ut facilisis libero. Aliquam odio nunc, sagittis vel elit nec, finibus tristique velit. Nunc suscipit venenatis magna ut aliquet. Praesent sodales orci gravida facilisis finibus.
Donec non pretium lectus. Aliquam pulvinar mattis egestas. Phasellus sit amet magna maximus velit dictum convallis. Nam mollis porttitor libero eu ornare. Cras sit amet leo ac mauris lacinia sodales. Maecenas pretium eu sapien sit amet interdum. Mauris sodales accumsan nibh vitae facilisis. Duis leo massa, placerat ut luctus quis, condimentum id dolor. Ut ut velit a ipsum mattis maximus. Praesent porta justo a lectus pretium iaculis.
Nullam congue lectus a convallis dictum. Nunc rhoncus, ante id porta tempor, lorem ex molestie justo, eget egestas risus urna sit amet ipsum. Etiam dapibus eu eros vitae pharetra. Mauris dapibus erat sit amet quam semper laoreet. Suspendisse potenti. Curabitur et dolor quis nulla ultrices varius. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Sed auctor faucibus finibus. Integer porttitor, mauris sit amet interdum imperdiet, sapien tellus fermentum metus, non accumsan augue sapien nec ligula.