@labcat2020/animation-lab 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @labcat2020/animation-lab
2
+
3
+ Shared Astro layouts, components, styles, and Vite helpers for **Animation Lab** (formerly `animations.labcat.nz`) series sites — e.g. `triangles`, `donuts`.
4
+
5
+ > Renamed from `@labcat2020/astro-animations` → `@labcat2020/animation-lab` (Sep 2026). Now published to npm (was GitHub Packages).
6
+
7
+ ## Install
8
+ ```bash
9
+ pnpm add @labcat2020/animation-lab
10
+ # peer deps required in host Astro app:
11
+ pnpm add astro sass-embedded
12
+ ```
13
+
14
+ ## Exports
15
+ ```json
16
+ {
17
+ "./layouts/*": "./src/layouts/*",
18
+ "./components/*": "./src/components/*",
19
+ "./styles/*": "./src/styles/*",
20
+ "./vite": "./src/vite.mjs"
21
+ }
22
+ ```
23
+
24
+ ## Usage (Astro)
25
+
26
+ `astro.config.mjs`:
27
+ ```js
28
+ import { defineConfig } from 'astro/config';
29
+ import { astroAnimationsViteConfig, astroAnimationsViteAliases, astroAnimationsScssPrepend } from '@labcat2020/animation-lab/vite';
30
+
31
+ export default defineConfig({
32
+ vite: {
33
+ ...astroAnimationsViteConfig(),
34
+ resolve: { alias: { ...astroAnimationsViteAliases(), '@': '/src' } },
35
+ css: { preprocessorOptions: { scss: { additionalData: astroAnimationsScssPrepend() } } }
36
+ }
37
+ });
38
+ ```
39
+ - `astroAnimationsViteConfig()` — injects `p5.sound` preamble (binds global `p5` before `p5.sound` IIFE) + marks audio-react side-effects as non-tree-shakable.
40
+ - `astroAnimationsViteAliases()` — `@lib` → `@labcat2020/p5.audioreactive` (legacy series import path).
41
+ - `astroAnimationsScssPrepend()` — prepends `styles/mixins/breakpoints.scss`.
42
+
43
+ Layouts:
44
+ ```astro
45
+ import BaseLayout from '@labcat2020/animation-lab/layouts/BaseLayout.astro';
46
+ import SketchLayout from '@labcat2020/animation-lab/layouts/SketchLayout.astro';
47
+ ```
48
+
49
+ ## Dev helper
50
+
51
+ Series repos linking local hub (sibling `../animations.labcat.nz`):
52
+
53
+ ```bash
54
+ pnpm run postinstall # labcat-link-packages (symlinks packages/*)
55
+ # or
56
+ npx labcat-link-packages
57
+ ```
58
+
59
+ ## License
60
+
61
+ MIT — LABCAT
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@labcat2020/animation-lab",
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "description": "Shared Astro layouts, components, and styles for Animation Lab series sites",
6
+ "exports": {
7
+ "./layouts/*": "./src/layouts/*",
8
+ "./components/*": "./src/components/*",
9
+ "./styles/*": "./src/styles/*",
10
+ "./vite": "./src/vite.mjs"
11
+ },
12
+ "bin": {
13
+ "labcat-link": "./scripts/link-packages.mjs",
14
+ "labcat-link-packages": "./scripts/link-packages.mjs"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "scripts",
19
+ "templates"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "dependencies": {
25
+ "normalize.css": "^8.0.1"
26
+ },
27
+ "peerDependencies": {
28
+ "astro": "^6.0.8",
29
+ "sass-embedded": "^1.98.0"
30
+ },
31
+ "license": "MIT",
32
+ "author": "LABCAT (labcat.nz)",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/LABCAT/animations.labcat.nz.git",
36
+ "directory": "packages/animation-lab"
37
+ },
38
+ "keywords": [
39
+ "astro",
40
+ "animation",
41
+ "labcat",
42
+ "p5",
43
+ "generative"
44
+ ]
45
+ }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { access, mkdir, readdir, rm, symlink } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const seriesRoot = process.cwd();
6
+ const labcatPackages =
7
+ process.env.LABCAT_PACKAGES ??
8
+ path.resolve(seriesRoot, '../animations.labcat.nz/packages');
9
+ const linkDir =
10
+ process.env.LABCAT_LINK_DIR ?? path.join(seriesRoot, 'packages');
11
+
12
+ try {
13
+ await access(labcatPackages);
14
+ } catch {
15
+ console.warn(`[link] @labcat packages not found at ${labcatPackages}`);
16
+ process.exit(1);
17
+ }
18
+
19
+ const entries = await readdir(labcatPackages, { withFileTypes: true });
20
+ const packageNames = entries.filter((e) => e.isDirectory()).map((e) => e.name);
21
+
22
+ if (packageNames.length === 0) {
23
+ console.warn('[link] No @labcat packages found.');
24
+ process.exit(1);
25
+ }
26
+
27
+ await mkdir(linkDir, { recursive: true });
28
+
29
+ for (const name of packageNames) {
30
+ const target = path.join(labcatPackages, name);
31
+ const link = path.join(linkDir, name);
32
+ await rm(link, { recursive: true, force: true });
33
+ await symlink(target, link, process.platform === 'win32' ? 'junction' : 'dir');
34
+ }
35
+
36
+ console.log(`[link] packages/ → ${labcatPackages} (${packageNames.join(', ')})`);
@@ -0,0 +1,99 @@
1
+ ---
2
+ // InstagramCTA component with BEM structure
3
+ ---
4
+
5
+ <div class="instagram-cta">
6
+ <a
7
+ href="https://www.instagram.com/labcat2020/"
8
+ class="instagram-cta__link"
9
+ target="_blank"
10
+ rel="noopener noreferrer"
11
+ >
12
+ <svg
13
+ class="instagram-cta__icon"
14
+ viewBox="0 0 169.06 169.06"
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ >
17
+ <path
18
+ d="M122.406,0H46.654C20.929,0,0,20.93,0,46.655v75.752c0,25.726,20.929,46.655,46.654,46.655h75.752 c25.727,0,46.656-20.93,46.656-46.655V46.655C169.063,20.93,148.133,0,122.406,0z M154.063,122.407 c0,17.455-14.201,31.655-31.656,31.655H46.654C29.2,154.063,15,139.862,15,122.407V46.655C15,29.201,29.2,15,46.654,15h75.752 c17.455,0,31.656,14.201,31.656,31.655V122.407z"
19
+ ></path>
20
+ <path
21
+ d="m84.531 40.97c-24.021 0-43.563 19.542-43.563 43.563 0 24.02 19.542 43.561 43.563 43.561s43.563-19.541 43.563-43.561c0-24.021-19.542-43.563-43.563-43.563zm0 72.123c-15.749 0-28.563-12.812-28.563-28.561 0-15.75 12.813-28.563 28.563-28.563s28.563 12.813 28.563 28.563c0 15.749-12.814 28.561-28.563 28.561z"
22
+ ></path>
23
+ <path
24
+ d="m129.92 28.251c-2.89 0-5.729 1.17-7.77 3.22-2.051 2.04-3.23 4.88-3.23 7.78 0 2.891 1.18 5.73 3.23 7.78 2.04 2.04 4.88 3.22 7.77 3.22 2.9 0 5.73-1.18 7.78-3.22 2.05-2.05 3.22-4.89 3.22-7.78 0-2.9-1.17-5.74-3.22-7.78-2.04-2.05-4.88-3.22-7.78-3.22z"
25
+ ></path>
26
+ </svg>
27
+ <span class="instagram-cta__text">
28
+ <span class="instagram-cta__action">Follow</span>
29
+ @labcat2020
30
+ </span>
31
+ </a>
32
+ </div>
33
+
34
+ <style lang="scss">
35
+ @import '../styles/mixins/breakpoints.scss';
36
+
37
+ .instagram-cta {
38
+ --shadow-colour: var(--cyan);
39
+ position: fixed;
40
+ bottom: 10px;
41
+ left: 10px;
42
+ z-index: 1000;
43
+ background: var(--black);
44
+ border-radius: 8px;
45
+
46
+ &__link {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 8px;
50
+ padding: 8px 12px;
51
+ border-radius: 8px;
52
+ backdrop-filter: blur(10px);
53
+ transition: all 0.5s;
54
+ color: var(--white);
55
+ font-weight: 700;
56
+ text-decoration: none;
57
+ box-shadow:
58
+ 0 0 24px var(--shadow-colour),
59
+ 0 0 12px var(--shadow-colour),
60
+ 0 0 6px var(--shadow-colour),
61
+ 0 0 3px var(--shadow-colour);
62
+
63
+ &:hover {
64
+ --shadow-colour: var(--pink);
65
+ }
66
+ }
67
+
68
+ &__icon {
69
+ --size: 22px;
70
+ width: var(--size);
71
+ height: var(--size);
72
+ border-radius: 8px;
73
+ fill: currentColor;
74
+ transition: all 0.5s;
75
+
76
+ @include media-breakpoint-up(md) {
77
+ --size: 24px;
78
+ }
79
+ }
80
+
81
+ &__text {
82
+ --size: 16px;
83
+ font-size: var(--size);
84
+ -webkit-text-stroke: 1px var(--black);
85
+
86
+ @include media-breakpoint-up(md) {
87
+ --size: 18px;
88
+ }
89
+ }
90
+
91
+ &__action {
92
+ display: none;
93
+
94
+ @include media-breakpoint-up(md) {
95
+ display: inline;
96
+ }
97
+ }
98
+ }
99
+ </style>
@@ -0,0 +1,25 @@
1
+ ---
2
+ import '../styles/global.scss';
3
+
4
+ const { title = 'Sketchbook', description = '' } = Astro.props;
5
+ ---
6
+
7
+ <!doctype html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="utf-8" />
11
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
12
+ <title>{title}</title>
13
+ {description && <meta name="description" content={description} />}
14
+ <link rel="manifest" href="manifest.json"/>
15
+ <link rel="icon" href="https://labcat.nz/favicon.ico" sizes="any"/>
16
+ <link rel="icon" type="image/svg+xml" href="https://labcat.nz/icon.svg"/>
17
+ <link rel="apple-touch-icon" href="https://labcat.nz/apple-touch-icon.png"/>
18
+ <link rel="dns-prefetch" href="//fonts.googleapis.com"/>
19
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="true"/>
20
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;600;800;900&amp;family=Teko:wght@700&amp;display=swap" id="fonts">
21
+ </head>
22
+ <body>
23
+ <slot />
24
+ </body>
25
+ </html>
@@ -0,0 +1,49 @@
1
+ ---
2
+ import BaseLayout from './BaseLayout.astro';
3
+ import InstagramCTA from '../components/InstagramCTA.astro';
4
+
5
+ const { title = 'Sketch', description = '' } = Astro.props;
6
+ ---
7
+
8
+ <BaseLayout title={title} description={description}>
9
+ <slot />
10
+ <div id="p5_loading"></div>
11
+ <div id="loader" class="loading">
12
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
13
+ <path
14
+ fill="none"
15
+ stroke-width="8"
16
+ stroke-dasharray="42.76482137044271 42.76482137044271"
17
+ d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z"
18
+ stroke-linecap="round"
19
+ style="transform:scale(0.8);transform-origin:50px 50px"
20
+ >
21
+ <animate
22
+ attributeName="stroke-dashoffset"
23
+ repeatCount="indefinite"
24
+ dur="1s"
25
+ keyTimes="0;1"
26
+ values="0;256.58892822265625"></animate>
27
+ </path>
28
+ </svg>
29
+ </div>
30
+ <main id="sketch-canvas"></main>
31
+ <!-- Play Icon -->
32
+ <svg
33
+ id="play-icon"
34
+ tabindex="0"
35
+ role="button"
36
+ xmlns="http://www.w3.org/2000/svg"
37
+ height="120"
38
+ width="120"
39
+ viewBox="0 0 24 24"
40
+ class="play-icon"
41
+ >
42
+ <path d="M0 0h24v24H0z" fill="none"></path>
43
+ <path
44
+ d="M10 16.5l6-4.5-6-4.5v9zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"
45
+ ></path>
46
+ </svg>
47
+
48
+ <!-- <InstagramCTA /> -->
49
+ </BaseLayout>
@@ -0,0 +1,79 @@
1
+ // Import normalize.css for consistent base styling
2
+ @import "./variables";
3
+ @import "./mixins/breakpoints";
4
+ @import "normalize.css";
5
+
6
+ $cursor-color: white;
7
+
8
+ body {
9
+ overflow: hidden;
10
+ font-family: Orbitron,sans-serif;
11
+ }
12
+
13
+ .loading {
14
+ display: flex;
15
+ align-items: center;
16
+ position: fixed;
17
+ inset: 0;
18
+ z-index: 1;
19
+ background: var(--gradient-bg);
20
+ background-blend-mode: var(--gradient-blend-mode);
21
+ transform: rotate(-180deg);
22
+ transition: opacity 3s;
23
+ opacity: 1;
24
+
25
+ &--complete {
26
+ opacity: 0;
27
+ }
28
+
29
+ svg {
30
+ margin: auto;
31
+ width: 10rem;
32
+ height: 10rem;
33
+ background: transparent;
34
+ shape-rendering: auto;
35
+ }
36
+
37
+ path {
38
+ fill: none;
39
+ stroke: #ff0cb8;
40
+ transform: scale(0.8);
41
+ transform-origin: 50px 50px;
42
+ }
43
+ }
44
+
45
+ .p5Canvas,
46
+ #defaultCanvas0 {
47
+ position: relative;
48
+ z-index: 1;
49
+ background: var(--canvas-complex-bg);
50
+ background-blend-mode: var(--canvas-complex-blend-mode);
51
+ }
52
+
53
+ .p5Canvas--cursor-play {
54
+ cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><polygon points='8,6 26,16 8,26' fill='#{$cursor-color}'/></svg>") 8 8, pointer;
55
+ }
56
+
57
+ .p5Canvas--cursor-pause {
58
+ cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><rect x='8' y='6' width='4' height='20' fill='#{$cursor-color}'/><rect x='20' y='6' width='4' height='20' fill='#{$cursor-color}'/></svg>") 8 8, pointer;
59
+ }
60
+
61
+ #play-icon {
62
+ position: fixed;
63
+ top: 20px;
64
+ right: 20px;
65
+ width: 120px;
66
+ height: 120px;
67
+ fill: var(--play-icon-color);
68
+ visibility: hidden;
69
+ z-index: 1;
70
+ opacity: 0;
71
+ transition: opacity 1s;
72
+ cursor: pointer;
73
+
74
+ &.fade-in {
75
+ opacity: 1;
76
+ visibility: visible;
77
+ z-index: 2;
78
+ }
79
+ }
@@ -0,0 +1,65 @@
1
+ // Grid breakpoints
2
+ $grid-breakpoints: (
3
+ xs: 0,
4
+ sm: 576px,
5
+ md: 768px,
6
+ lg: 1024px,
7
+ xl: 1280px,
8
+ xxl: 1440px,
9
+ );
10
+
11
+ // Helper function to get minimum breakpoint value
12
+ @function breakpoint-min($name, $breakpoints: $grid-breakpoints) {
13
+ $min: map-get($breakpoints, $name);
14
+ @if $min != 0 {
15
+ @return $min;
16
+ }
17
+ @return null;
18
+ }
19
+
20
+ // Media of at least the minimum breakpoint width. No query for the smallest breakpoint.
21
+ // Makes the @content apply to the given breakpoint and wider.
22
+ @mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {
23
+ $min: breakpoint-min($name, $breakpoints);
24
+ @if $min {
25
+ @media (min-width: $min) {
26
+ @content;
27
+ }
28
+ } @else {
29
+ @content;
30
+ }
31
+ }
32
+
33
+ // Media of at most the maximum breakpoint width. No query for the largest breakpoint.
34
+ // Makes the @content apply to the given breakpoint and narrower.
35
+ @mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {
36
+ $max: map-get($breakpoints, $name) - 0.02;
37
+ @if $max {
38
+ @media (max-width: $max) {
39
+ @content;
40
+ }
41
+ } @else {
42
+ @content;
43
+ }
44
+ }
45
+
46
+ // Media that spans multiple breakpoint widths.
47
+ // Makes the @content apply between the min and max breakpoints
48
+ @mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {
49
+ $min: breakpoint-min($lower, $breakpoints);
50
+ $max: map-get($breakpoints, $upper) - 0.02;
51
+
52
+ @if $min != null and $max != null {
53
+ @media (min-width: $min) and (max-width: $max) {
54
+ @content;
55
+ }
56
+ } @else if $max == null {
57
+ @include media-breakpoint-up($lower, $breakpoints) {
58
+ @content;
59
+ }
60
+ } @else if $min == null {
61
+ @include media-breakpoint-down($upper, $breakpoints) {
62
+ @content;
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,20 @@
1
+ // Brand Colors
2
+ :root {
3
+ --cyan: #0cdbf5;
4
+ --pink: #ff0cb8;
5
+ --white: #fff;
6
+ --black: #000;
7
+ --gradient-bg: linear-gradient(0deg,
8
+ rgba(0, 0, 0, 1) -8.35%,
9
+ rgba(29, 22, 87, 1) -5.1%,
10
+ rgba(51, 65, 174, 1) 28.53%,
11
+ rgba(59, 82, 208, 1) 43.72%,
12
+ rgba(53, 100, 213, 1) 51.31%,
13
+ rgba(37, 146, 225, 1) 67.59%,
14
+ rgba(12, 219, 245, 1) 89.28%,
15
+ rgba(0, 255, 255, 1) 100.13%);
16
+ --play-icon-color: white;
17
+ --canvas-bg: transparent;
18
+ --canvas-complex-bg: none;
19
+ --canvas-complex-blend-mode: normal;
20
+ }
package/src/vite.mjs ADDED
@@ -0,0 +1,64 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import path from 'node:path';
3
+
4
+ const packageRoot = fileURLToPath(new URL('.', import.meta.url));
5
+
6
+ /** Bind p5 before p5.sound's IIFE — bare global `p5` is tree-shaken in production otherwise. */
7
+ function p5SoundPreamblePlugin() {
8
+ return {
9
+ name: 'labcat-p5-sound-preamble',
10
+ enforce: 'pre',
11
+ transform(code, id) {
12
+ const normalized = id.replace(/\\/g, '/');
13
+ if (!normalized.includes('p5.sound/dist/p5.sound.js')) return null;
14
+ if (code.includes('labcat-p5-sound-preamble')) return null;
15
+ return {
16
+ code: [
17
+ "import __labcatP5 from 'p5';",
18
+ 'const p5 = globalThis.p5 ?? __labcatP5;',
19
+ 'globalThis.p5 = p5;',
20
+ '/* labcat-p5-sound-preamble */',
21
+ code,
22
+ ].join('\n'),
23
+ moduleSideEffects: true,
24
+ };
25
+ },
26
+ };
27
+ }
28
+
29
+ /** Vite/Astro SCSS hook for series repos consuming @labcat2020/animation-lab. */
30
+ export function astroAnimationsScssPrepend() {
31
+ const breakpoints = path.join(packageRoot, 'styles/mixins/breakpoints.scss');
32
+ return `@import "${breakpoints.replace(/\\/g, '/')}";`;
33
+ }
34
+
35
+ /** Shared Vite config — keeps p5.sound + p5.audioreactive prototype patches in production builds. */
36
+ export function astroAnimationsViteConfig() {
37
+ return {
38
+ plugins: [p5SoundPreamblePlugin()],
39
+ build: {
40
+ rollupOptions: {
41
+ treeshake: {
42
+ moduleSideEffects(id) {
43
+ if (id.includes('p5.sound')) return true;
44
+ // Only the three side-effect entry files from p5.audioreactive; other helpers are tree-shakable
45
+ if (
46
+ id.includes('p5.audioreactive/src/p5.audioReact.js') ||
47
+ id.includes('p5.audioreactive/src/p5.soundBoot.js') ||
48
+ id.includes('p5.audioreactive/src/p5.setGlobalP5.js')
49
+ )
50
+ return true;
51
+ return undefined;
52
+ },
53
+ },
54
+ },
55
+ },
56
+ };
57
+ }
58
+
59
+ /** Vite aliases — `@lib/*` resolves to `@labcat2020/p5.audioreactive/*` (legacy series import paths). */
60
+ export function astroAnimationsViteAliases() {
61
+ return {
62
+ '@lib': '@labcat2020/p5.audioreactive',
63
+ };
64
+ }
@@ -0,0 +1,10 @@
1
+ packages:
2
+ - .
3
+ - ../animations.labcat.nz/packages/*
4
+
5
+ linkWorkspacePackages: true
6
+
7
+ allowBuilds:
8
+ '@parcel/watcher': true
9
+ esbuild: true
10
+ sharp: true