A new approach…
This commit is contained in:
parent
821e47a019
commit
7a4748a69c
16 changed files with 366 additions and 2292 deletions
737
.github/copilot-instructions.md
vendored
737
.github/copilot-instructions.md
vendored
|
|
@ -1,8 +1,6 @@
|
|||
# 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.
|
||||
|
||||
**Tech Stack**: Eleventy 3.x (ESM), Tailwind CSS (token-based), CUBE CSS methodology, WebC components, Hotwired Turbo for navigation, PostCSS, esbuild
|
||||
A vision quest, of sorts.
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
@ -17,20 +15,13 @@ npm run test:a11y # Run accessibility tests
|
|||
**Key Files to Know:**
|
||||
- `eleventy.config.js` - Main Eleventy config (imports from `src/_config/`)
|
||||
- `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/pages/projects/mixes/` - Music mix tracks (markdown files)
|
||||
- `src/assets/css/global/` - Global CSS (CUBE methodology)
|
||||
- `src/pages/` - Contains written content (markdown files) that is processed as JavaScript Nunjuks template
|
||||
- `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
|
||||
**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)
|
||||
- `filters.js` - Template filters (dates, markdown, slugify)
|
||||
- `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.
|
||||
|
||||
|
||||
|
||||
### 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`. It’s all rigged up to the design tokens Custom Property block. It’s 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, it’s 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. It’s 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
|
||||
- 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)
|
||||
- `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`)
|
||||
- Custom properties generated via Tailwind plugin (e.g., `--color-pink`, `--space-m`)
|
||||
- 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-cyan`, `--space-m`)
|
||||
- **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`
|
||||
|
||||
### 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
|
||||
**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):
|
||||
- `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**: `` → 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
|
||||
Use the `animateText` shortcode or markdown-it-attrs classes:
|
||||
```jinja2
|
||||
```ninjuks
|
||||
{% 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.
|
||||
|
||||
## 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)
|
||||
- **`tagList`**: Unique tags across all content (excludes 'posts', 'docs', 'all', 'mix', 'project')
|
||||
- **`goPages`**: Pages with `go:` frontmatter for URL redirects (output to `_redirects`)
|
||||
- **`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
|
||||
- Enabled for track-to-track navigation (no full page reload)
|
||||
- `<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 %}`
|
||||
|
||||
### 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, you’d 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 project’s design tokens are in, into Tailwind friendly configuration objects.
|
||||
|
||||
## module.export
|
||||
|
||||
We don’t 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 there’s 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. That’s 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. It’s 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. It’s all rigged up to the design tokens Custom Property block. It’s 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, it’s 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
|
||||

|
||||
```
|
||||
|
||||
### 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 %}
|
||||
- Import Turbo in defer bundle: `{% js "defer" %} import * as Turbo from '/assets/components/turbo.js'; {% endjs %}`
|
||||
|
|
@ -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
|
||||
|
||||
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))
|
||||
- Reusable animation effects (fadeIn, shake, zoom, etc.)
|
||||
- Emotional presets (jumpscare, anticipation, dread, etc.)
|
||||
- Effect composition utilities
|
||||
1. **Animations Library** ([`gsap-animations.js`](../src/assets/scripts/bundle/gsap-animations.js))
|
||||
- Reusable zoom presets: `punchIn`, `punchOut`
|
||||
- Default variables with simple override merging
|
||||
- `shouldAnimate()` utility respecting `prefers-reduced-motion`
|
||||
|
||||
2. **Content Animations** ([`gsap-shortcode-init.js`](../src/assets/scripts/bundle/gsap-shortcode-init.js))
|
||||
- Scroll-triggered animations in markdown/blog posts
|
||||
- Low-friction shortcode syntax for content authors
|
||||
|
||||
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
|
||||
- Scroll-triggered animations in markdown/blog posts
|
||||
- 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)).
|
||||
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
### Emotional Presets (Recommended)
|
||||
|
||||
Animate emotions, not numbers:
|
||||
### Punch In Animation
|
||||
|
||||
```markdown
|
||||
{% gsapScrollAnim { "emotion": "jumpscare" } %}
|
||||
[{ "src": "/scary-image.jpg", "alt": "Boo!" }]
|
||||
{% 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" }]
|
||||
{% gsapScrollAnim { "animationType": "punchIn", "focalX": 75, "focalY": 25, "startZoom": 1, "endZoom": 2.5 } %}
|
||||
[{ "src": "/images/detail-photo.jpg", "alt": "Macro photography showing fine detail" }]
|
||||
{% endgsapScrollAnim %}
|
||||
```
|
||||
|
||||
|
|
@ -63,33 +38,27 @@ Combine multiple effects:
|
|||
|
||||
## Configuration Options
|
||||
|
||||
All parameters are optional with sensible defaults:
|
||||
All parameters are optional with sensible defaults and fully passed through:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"animationType": "fadeIn", // Animation preset (see below)
|
||||
"scrollStart": "top 80%", // When animation starts
|
||||
"scrollEnd": "bottom 20%", // When animation completes
|
||||
"scrub": true, // Tied to scroll position (bidirectional)
|
||||
"containerClass": "gsap-container", // CSS class for wrapper
|
||||
"pin": false, // Pin element during animation
|
||||
"markers": false // Show debug markers (dev only)
|
||||
"animationType": "punchIn", // punchIn or punchOut
|
||||
"scrollStart": "top 80%", // Timeline start
|
||||
"scrollEnd": "bottom 20%", // Timeline end
|
||||
"scrub": true, // Tie progress to scroll
|
||||
"containerClass": "gsap-container",
|
||||
"spillingInto": false, // false | true | 'prose' | 'bleed'
|
||||
"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
|
||||
|
||||
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).
|
||||
|
||||
## Examples
|
||||
|
||||
### 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 %}
|
||||
```
|
||||
## Example
|
||||
|
||||
### Zoom In to Focal Point
|
||||
|
||||
|
|
@ -284,9 +193,9 @@ This shows colored markers in viewport indicating trigger start/end positions.
|
|||
- ScrollTrigger efficiently batches calculations
|
||||
- Contexts properly cleaned up on navigation
|
||||
|
||||
## Files Modified
|
||||
## Files
|
||||
|
||||
- **Shortcode**: `src/_config/shortcodes/gsap.js`
|
||||
- **Initializer**: `src/assets/scripts/bundle/gsap-shortcode-init.js`
|
||||
- **CSS**: `src/assets/css/global/utilities/gsap-animations.css`
|
||||
- **Config**: Registered in `eleventy.config.js` as paired shortcode
|
||||
- Library: `src/assets/scripts/bundle/gsap-animations.js`
|
||||
- Shortcode initializer: `src/assets/scripts/bundle/gsap-shortcode-init.js`
|
||||
- Shortcode: `src/_config/shortcodes/gsap.js`
|
||||
- CSS: `src/assets/css/global/utilities/gsap-animations.css`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
### 1. Shared Effects Library (`gsap-effects.js`)
|
||||
Contains reusable animation effects and emotional presets.
|
||||
### 1. Animations Library (`gsap-animations.js`)
|
||||
Contains reusable zoom presets and `shouldAnimate()`.
|
||||
|
||||
### 2. Content Animations (`gsap-shortcode-init.js`)
|
||||
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
|
||||
|
||||
Simple fade-in animation:
|
||||
Punch into a focal point on scroll:
|
||||
```markdown
|
||||
{% gsapScrollAnim {
|
||||
"animationType": "fadeIn"
|
||||
"animationType": "punchIn",
|
||||
"focalX": 50,
|
||||
"focalY": 50,
|
||||
"startZoom": 1,
|
||||
"endZoom": 2.5
|
||||
} %}
|
||||
[{
|
||||
"src": "/path/to/image.jpg",
|
||||
"alt": "Description"
|
||||
}]
|
||||
[{ "src": "/path/to/high-res-image.jpg", "alt": "Image to punch into" }]
|
||||
{% endgsapScrollAnim %}
|
||||
```
|
||||
|
||||
### Available Animation Types
|
||||
|
||||
- `fadeIn` - Fade in from below
|
||||
- `fadeInUp` - Fade in from further below
|
||||
- `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
|
||||
- `punchIn` - Punch into image focal point (zoom in)
|
||||
- `punchOut` - Punch out from focal point (zoom out)
|
||||
|
||||
### Zoom Animations with Focal Points
|
||||
### Punch Animations with Focal Points
|
||||
|
||||
```markdown
|
||||
{% gsapScrollAnim {
|
||||
"animationType": "zoomIn",
|
||||
"animationType": "punchIn",
|
||||
"focalX": 30,
|
||||
"focalY": 40,
|
||||
"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
|
||||
|
|
@ -180,88 +87,45 @@ Pin the element in place during animation:
|
|||
} %}
|
||||
```
|
||||
|
||||
### `markers` (default: `false`)
|
||||
Show debug markers (for development):
|
||||
### `spillingInto` (default: `false`)
|
||||
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
|
||||
{% gsapScrollAnim {
|
||||
"animationType": "fadeIn",
|
||||
"markers": true
|
||||
"animationType": "punchIn",
|
||||
"spillingInto": "bleed"
|
||||
} %}
|
||||
[{ "src": "/path/to/image.jpg", "alt": "Full-width image" }]
|
||||
{% endgsapScrollAnim %}
|
||||
```
|
||||
|
||||
### `markers` (default: `false`)
|
||||
Show debug markers (for development).
|
||||
|
||||
---
|
||||
|
||||
## Multiple Images
|
||||
|
||||
```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 %}
|
||||
```
|
||||
You can include multiple images; zoom applies to `.gsap-image` elements inside the container.
|
||||
|
||||
---
|
||||
|
||||
## Tips for Storytelling
|
||||
## Tips
|
||||
|
||||
1. **Use emotions first** - `"emotion": "jumpscare"` is easier than combining effects manually
|
||||
2. **Scrub for slow reveals** - Set `"scrub": true` for scroll-controlled drama
|
||||
3. **No scrub for punchy moments** - Set `"scrub": false` for quick actions
|
||||
4. **Pin for focus** - Use `"pin": true` to hold attention on an element
|
||||
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"]`
|
||||
1. **Scrub for slow reveals** - Set `"scrub": true` for scroll-controlled drama
|
||||
2. **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. **Zoom needs high-res** - Zoom animations automatically request larger image sizes
|
||||
|
||||
---
|
||||
|
||||
## For Developers
|
||||
|
||||
### Adding New Effects
|
||||
### Library API
|
||||
|
||||
Edit [`gsap-effects.js`](../src/assets/scripts/bundle/gsap-effects.js):
|
||||
|
||||
```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;
|
||||
}
|
||||
};
|
||||
```
|
||||
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.
|
||||
|
||||
### UI Component Animations
|
||||
|
||||
|
|
@ -269,7 +133,7 @@ Create component-specific files like [`mix-nav-animations.js`](../src/assets/scr
|
|||
|
||||
```javascript
|
||||
import gsap from 'gsap';
|
||||
import { shouldAnimate } from './gsap-effects.js';
|
||||
import { shouldAnimate } from './gsap-animations.js';
|
||||
|
||||
function initMyComponentAnimations() {
|
||||
if (!shouldAnimate()) return;
|
||||
|
|
@ -298,12 +162,6 @@ All animations respect `prefers-reduced-motion`. Users with this preference will
|
|||
|
||||
## Debugging
|
||||
|
||||
Enable markers to see scroll trigger points:
|
||||
```markdown
|
||||
{% gsapScrollAnim {
|
||||
"animationType": "fadeIn",
|
||||
"markers": true
|
||||
} %}
|
||||
```
|
||||
Enable markers to see scroll trigger points via the shortcode config.
|
||||
|
||||
Check browser console for warnings about missing animation types or configuration errors.
|
||||
|
|
|
|||
13
package-lock.json
generated
13
package-lock.json
generated
|
|
@ -2215,8 +2215,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
|
|
@ -2235,8 +2234,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.10.1",
|
||||
|
|
@ -2746,6 +2744,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
|
|
@ -3411,7 +3410,8 @@
|
|||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
|
||||
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
|
|
@ -4858,6 +4858,7 @@
|
|||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
|
||||
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
|
|
@ -5770,6 +5771,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
|
|
@ -6465,6 +6467,7 @@
|
|||
"resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz",
|
||||
"integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"posthtml-parser": "^0.11.0",
|
||||
"posthtml-render": "^3.0.0"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ export const buildJs = async (inputPath, outputPath) => {
|
|||
entryPoints: [inputPath],
|
||||
bundle: 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;
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export const markdownLib = markdownIt({
|
|||
const attributes = token.attrs || [];
|
||||
const hasEleventyWidths = attributes.some(([key]) => key === 'eleventy:widths');
|
||||
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(' ');
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
import {imageKeysShortcode} from './image.js';
|
||||
|
||||
/**
|
||||
* GSAP Scroll Animation Shortcode
|
||||
* Paired shortcode for creating scroll-controlled GSAP animations with images
|
||||
* GSAP Scroll Animation Shortcode (prose-only)
|
||||
* Paired shortcode for scroll-triggered punch animations with images.
|
||||
* Prose-exclusive: used within blog posts and article content.
|
||||
*
|
||||
* Usage:
|
||||
* {% gsapScrollAnim {
|
||||
* animationType: "fadeIn",
|
||||
* scrollStart: "top 80%",
|
||||
* scrollEnd: "bottom 20%",
|
||||
* scrub: true,
|
||||
* containerClass: "my-custom-class"
|
||||
* } %}
|
||||
* [{ src: "/path/to/image.jpg", alt: "Alt text", caption: "Caption text" }]
|
||||
* {% endgsapScrollAnim %}
|
||||
* @param {string} content - JSON array of image objects
|
||||
* @param {Object} config - Animation configuration
|
||||
* @param {string} config.animationType - 'punchIn' | 'punchOut' (default: 'punchIn')
|
||||
* @param {string} config.containerClass - Custom wrapper class (default: 'gsap-container')
|
||||
* @param {string|boolean} config.spillingInto - Layout overflow: false | true | 'prose' | 'bleed' (default: false)
|
||||
* @param {string} config.scrollStart - ScrollTrigger start (default: 'top 80%')
|
||||
* @param {string} config.scrollEnd - ScrollTrigger end (default: 'bottom 20%')
|
||||
* @param {boolean} config.scrub - Link to scroll progress (default: true)
|
||||
* @param {boolean} config.pin - Pin during animation (default: true)
|
||||
* @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 => {
|
||||
|
|
@ -44,31 +50,41 @@ export const gsapScrollAnim = async function(content, configString = '{}') {
|
|||
|
||||
// Set defaults
|
||||
const {
|
||||
animationType = 'fadeIn',
|
||||
animationType = 'punchIn',
|
||||
scrollStart = 'top 80%',
|
||||
scrollEnd = 'bottom 20%',
|
||||
scrub = true,
|
||||
containerClass = 'gsap-container',
|
||||
pin = false,
|
||||
markers = false // Set to true for debugging
|
||||
spillingInto = false,
|
||||
pin = true,
|
||||
debug = false
|
||||
} = config;
|
||||
|
||||
// Build animation config for data attribute
|
||||
// Pass full config through so defaults can be overridden in the library
|
||||
const animConfig = {
|
||||
...config,
|
||||
animationType,
|
||||
scrollStart,
|
||||
scrollEnd,
|
||||
scrub,
|
||||
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
|
||||
const images = parseImagesFromContent(content);
|
||||
|
||||
// Detect if this is a zoom animation (needs high-res images)
|
||||
const isZoomAnimation = animationType === 'zoomIn' || animationType === 'zoomOut' ||
|
||||
animationType.includes('zoom');
|
||||
// Detect if this is a punch animation (needs high-res images)
|
||||
const isPunchAnimation = animationType === 'punchIn' || animationType === 'punchOut';
|
||||
|
||||
// Process images using existing image shortcode
|
||||
const processedImages = await Promise.all(
|
||||
|
|
@ -82,8 +98,8 @@ export const gsapScrollAnim = async function(content, configString = '{}') {
|
|||
loading: index === 0 ? 'eager' : 'lazy',
|
||||
imageClass: 'gsap-image',
|
||||
containerClass: 'gsap-image-wrapper',
|
||||
// Inject high-res widths for zoom animations
|
||||
...(isZoomAnimation && { widths: [960, 1400, 2400, 3200] })
|
||||
// Inject high-res widths for punch animations
|
||||
...(isPunchAnimation && { widths: [960, 1400, 2400, 3200] })
|
||||
});
|
||||
return `<div class="gsap-item" data-image-index="${index}">${imageHtml}</div>`;
|
||||
} else if (img.content) {
|
||||
|
|
@ -95,7 +111,7 @@ export const gsapScrollAnim = async function(content, configString = '{}') {
|
|||
);
|
||||
|
||||
// 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 ')}
|
||||
</div>`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* 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 */
|
||||
|
|
@ -24,13 +24,43 @@
|
|||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
/* GSAP will add will-change when animating */
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Initial state - GSAP will handle visibility/opacity */
|
||||
.gsap-container[data-gsap-scroll-anim] .gsap-item {
|
||||
transform: translateZ(0);
|
||||
/* Punch animation container: constrained by default */
|
||||
.gsap-container[data-gsap-scroll-anim*='punch'] {
|
||||
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 */
|
||||
|
|
@ -38,30 +68,7 @@
|
|||
margin-top: var(--space-m, 1.5rem);
|
||||
}
|
||||
|
||||
/* Zoom animation specific styles */
|
||||
.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) */
|
||||
/* Accessibility: respect prefers-reduced-motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.gsap-image,
|
||||
.gsap-item {
|
||||
|
|
@ -70,19 +77,3 @@
|
|||
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;
|
||||
}
|
||||
73
src/assets/scripts/bundle/gsap-animations.js
Normal file
73
src/assets/scripts/bundle/gsap-animations.js
Normal 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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -1,178 +1,30 @@
|
|||
/**
|
||||
* GSAP Scroll Animation Initializer
|
||||
* Handles scroll-driven animations triggered by gsapScrollAnim shortcode
|
||||
* Compatible with Hotwired Turbo navigation
|
||||
*/
|
||||
Initialize GSAP on pages with Turbo frames */
|
||||
|
||||
import gsap from 'gsap';
|
||||
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
|
||||
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
|
||||
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
|
||||
*/
|
||||
|
|
@ -195,12 +47,10 @@ function initGsapAnimations() {
|
|||
// Extract configuration with defaults
|
||||
const {
|
||||
animationType,
|
||||
emotion,
|
||||
effects: effectsList,
|
||||
scrollStart = 'top 80%',
|
||||
scrollEnd = 'bottom 20%',
|
||||
scrub = true,
|
||||
pin = false,
|
||||
pin = true,
|
||||
markers = false
|
||||
} = config;
|
||||
|
||||
|
|
@ -211,70 +61,14 @@ function initGsapAnimations() {
|
|||
// Create GSAP context for this container
|
||||
const ctx = gsap.context(() => {
|
||||
let timeline;
|
||||
|
||||
// Handle emotional presets (take priority)
|
||||
if (emotion && emotions[emotion]) {
|
||||
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
|
||||
items.forEach(item => {
|
||||
const emotionTl = emotions[emotion](item, 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);
|
||||
|
||||
|
||||
if (animationType === 'punchIn' || animationType === 'punchOut') {
|
||||
const targetElements = container.querySelectorAll('.gsap-image');
|
||||
|
||||
const anim = animationType === 'punchIn'
|
||||
? punchIn(targetElements, config)
|
||||
: punchOut(targetElements, config);
|
||||
|
||||
// Create timeline with ScrollTrigger
|
||||
timeline = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
|
|
@ -291,7 +85,7 @@ function initGsapAnimations() {
|
|||
onLeaveBack: () => container.dataset.gsapActive = 'false'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Apply animation
|
||||
if (anim.from && anim.to) {
|
||||
timeline.fromTo(targetElements, anim.from, anim.to);
|
||||
|
|
@ -301,7 +95,7 @@ function initGsapAnimations() {
|
|||
timeline.to(targetElements, anim.to);
|
||||
}
|
||||
} else {
|
||||
console.warn('No animation type, emotion, or effects specified', config);
|
||||
console.warn('Unsupported animation type. Only zoomIn/zoomOut are available.', config);
|
||||
return;
|
||||
}
|
||||
}, container);
|
||||
|
|
@ -368,4 +162,4 @@ window.addEventListener('resize', () => {
|
|||
});
|
||||
|
||||
// Export for manual control if needed
|
||||
export { initGsapAnimations, cleanupGsapAnimations, refreshScrollTrigger, animations };
|
||||
export { initGsapAnimations, cleanupGsapAnimations, refreshScrollTrigger };
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* GSAP animations for mix track navigation UI elements
|
||||
*/
|
||||
import gsap from 'gsap';
|
||||
import { shouldAnimate } from './gsap-effects.js';
|
||||
import { shouldAnimate } from './gsap-animations.js';
|
||||
|
||||
/**
|
||||
* Initialize mix navigation animations
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 %}
|
||||
BIN
src/posts/2025/testing/swanpoint.jpg
Normal file
BIN
src/posts/2025/testing/swanpoint.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
|
|
@ -1,80 +1,37 @@
|
|||
---
|
||||
title: 'Testing'
|
||||
description: "Test animations and such here."
|
||||
date: 2026-01-04
|
||||
date: 2026-12-12
|
||||
tags: ['test']
|
||||
---
|
||||
|
||||
## GSAP Scroll Animations
|
||||
## GSAP Proving Ground
|
||||
|
||||
Testing the `gsapScrollAnim` shortcode with scroll-driven animations:
|
||||
|
||||
{% 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 %}
|
||||
Putting the `gsapScrollAnim` shortcode through its paces.
|
||||
|
||||
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 {
|
||||
"animationType": "zoomIn",
|
||||
"focalX": 50,
|
||||
"focalY": 50,
|
||||
"animationType": "punchIn",
|
||||
"focalX": 90,
|
||||
"focalY": 60,
|
||||
"startZoom": 1,
|
||||
"endZoom": 2.5,
|
||||
"scrollStart": "top top",
|
||||
"scrollStart": "middle middle",
|
||||
"scrollEnd": "+=100%",
|
||||
"scrub": true,
|
||||
"pin": true
|
||||
"pin": true,
|
||||
"debug": true
|
||||
} %}
|
||||
[{
|
||||
"src": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg",
|
||||
"alt": "Radiohead OK Computer album cover - zoom to center detail"
|
||||
"src": "/posts/2025/testing/swanpoint.jpg",
|
||||
"alt": "alt text",
|
||||
"caption": "This is a caption. Have we assigned it a class?"
|
||||
}]
|
||||
{% 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 {
|
||||
"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.
|
||||
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue