@struq/skill 0.3.0 → 0.4.1
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 +22 -36
- package/package.json +4 -2
- package/skills/struq/SKILL.md +139 -307
- package/skills/struq/blocks/CATALOG.md +426 -0
- package/skills/struq/blocks/backgrounds/background-aurora.tsx +818 -0
- package/skills/struq/blocks/backgrounds/background-facets.tsx +1283 -0
- package/skills/struq/blocks/backgrounds/background-silk.tsx +906 -0
- package/skills/struq/blocks/catalog.json +1302 -0
- package/skills/struq/blocks/footers/footer-contact.tsx +789 -0
- package/skills/struq/blocks/footers/footer-reveal.tsx +313 -0
- package/skills/struq/blocks/headers/header-bar.tsx +652 -0
- package/skills/struq/blocks/headers/menu-curtain.tsx +790 -0
- package/skills/struq/blocks/heroes/hero-framed.tsx +627 -0
- package/skills/struq/blocks/heroes/hero-index.tsx +855 -0
- package/skills/struq/blocks/heroes/hero-poster.tsx +1086 -0
- package/skills/struq/blocks/heroes/hero-split.tsx +685 -0
- package/skills/struq/blocks/heroes/hero-still.tsx +862 -0
- package/skills/struq/{recipes/components/struq → blocks/kit}/Cursor.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/HeroCanvas.tsx +4 -4
- package/skills/struq/{recipes/components/struq → blocks/kit}/HorizontalGallery.tsx +2 -2
- package/skills/struq/{recipes/components/struq → blocks/kit}/MagneticButton.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/MediaFrame.tsx +2 -2
- package/skills/struq/{recipes/components/struq → blocks/kit}/PinnedZoom.tsx +14 -4
- package/skills/struq/{recipes/components/struq → blocks/kit}/Reveal.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/SmoothScroll.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/SplitHeadline.tsx +1 -1
- package/skills/struq/blocks/kit/blocks.css +2102 -0
- package/skills/struq/blocks/kit/images.ts +17 -0
- package/skills/struq/blocks/kit/photo-read.ts +102 -0
- package/skills/struq/blocks/page-transitions/page-transition-gate.tsx +706 -0
- package/skills/struq/blocks/page-transitions/page-transition-slices.tsx +469 -0
- package/skills/struq/blocks/page-transitions/page-transition-stack.tsx +583 -0
- package/skills/struq/blocks/preloaders/preloader-gate.tsx +358 -0
- package/skills/struq/blocks/preloaders/preloader-slices-uncovered.ts +50 -0
- package/skills/struq/blocks/preloaders/preloader-slices.tsx +289 -0
- package/skills/struq/blocks/preloaders/preloader-stack.tsx +367 -0
- package/skills/struq/blocks/preloaders/preloader-wordmark.tsx +568 -0
- package/skills/struq/blocks/sections/about-studio.tsx +436 -0
- package/skills/struq/blocks/sections/approach.tsx +661 -0
- package/skills/struq/blocks/sections/contact-form.tsx +664 -0
- package/skills/struq/blocks/sections/cta-closing.tsx +763 -0
- package/skills/struq/blocks/sections/fanned-cards.tsx +547 -0
- package/skills/struq/blocks/sections/film-strip.tsx +385 -0
- package/skills/struq/blocks/sections/grid-assembly.tsx +616 -0
- package/skills/struq/blocks/sections/hover-list.tsx +467 -0
- package/skills/struq/blocks/sections/image-trail.tsx +432 -0
- package/skills/struq/blocks/sections/manifesto.tsx +691 -0
- package/skills/struq/blocks/sections/panel-accordion.tsx +380 -0
- package/skills/struq/blocks/sections/reveals.tsx +416 -0
- package/skills/struq/blocks/sections/split-pair.tsx +670 -0
- package/skills/struq/blocks/sections/sticky-chapters.tsx +256 -0
- package/skills/struq/blocks/sections/velocity-type.tsx +269 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-fill-rows.tsx → blocks/showpieces/fill-rows.tsx} +1 -1
- package/skills/struq/{cinematic/showpieces/toonstuk-hero-opener.tsx → blocks/showpieces/hero-opener.tsx} +5 -5
- package/skills/struq/{cinematic/showpieces/toonstuk-image-to-fullbleed.tsx → blocks/showpieces/image-to-fullbleed.tsx} +16 -5
- package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +48 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-panel-wipe.tsx → blocks/showpieces/panel-wipe.tsx} +2 -2
- package/skills/struq/{cinematic/showpieces/toonstuk-pixel-resolve.tsx → blocks/showpieces/pixel-resolve.tsx} +1 -1
- package/skills/struq/{cinematic/showpieces/toonstuk-project-stack.tsx → blocks/showpieces/project-stack.tsx} +2 -2
- package/skills/struq/blocks/showpieces/scrub-connector.tsx +307 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-wordmark-finale.tsx → blocks/showpieces/wordmark-finale.tsx} +1 -1
- package/skills/struq/blocks/transitions/zoom-through.tsx +613 -0
- package/skills/struq/canon/canon.json +627 -0
- package/skills/struq/cinematic/README.md +7 -18
- package/skills/struq/cinematic/examples/code/nextjs/app/components/LenisRoot.tsx +1 -1
- package/skills/struq/cinematic/examples/code/shared/styles.css +1 -1
- package/skills/struq/cinematic/examples/code/vite/src/LenisRoot.tsx +1 -1
- package/skills/struq/cinematic/references/01-core-mental-model.md +1 -1
- package/skills/struq/cinematic/references/02-vocabulary.md +1 -1
- package/skills/struq/cinematic/references/07-stack-integration.md +2 -2
- package/skills/struq/recipes/app/fonts.ts +12 -22
- package/skills/struq/recipes/app/globals.css +34 -177
- package/skills/struq/recipes/app/layout.tsx +13 -11
- package/skills/struq/references/craft.md +77 -0
- package/skills/struq/references/iconography.md +28 -230
- package/skills/struq/references/stack-targets.md +23 -74
- package/skills/struq/references/tells.md +52 -0
- package/skills/struq/references/triage.md +6 -7
- package/skills/struq/scripts/blocks.mjs +156 -0
- package/skills/struq/scripts/canon.mjs +106 -281
- package/skills/struq/scripts/check.mjs +92 -126
- package/skills/struq/scripts/images.mjs +2 -2
- package/skills/struq/scripts/log.mjs +6 -23
- package/skills/struq/scripts/tokens.mjs +222 -0
- package/skills/struq/cinematic/showpieces/README.md +0 -18
- package/skills/struq/cinematic/showpieces/toonstuk-manifesto-scrub.tsx +0 -81
- package/skills/struq/cinematic/showpieces/toonstuk-scrub-connector.tsx +0 -78
- package/skills/struq/references/anti-patterns.md +0 -330
- package/skills/struq/references/baseline-prompt.md +0 -122
- package/skills/struq/references/brand.md +0 -143
- package/skills/struq/references/brief-router.md +0 -218
- package/skills/struq/references/composition.md +0 -190
- package/skills/struq/references/depth-parallax.md +0 -235
- package/skills/struq/references/direction.md +0 -188
- package/skills/struq/references/feel-layer.md +0 -107
- package/skills/struq/references/gsap-api.md +0 -293
- package/skills/struq/references/hero-scenes.md +0 -107
- package/skills/struq/references/imagery.md +0 -125
- package/skills/struq/references/loaders.md +0 -287
- package/skills/struq/references/micro-interactions.md +0 -270
- package/skills/struq/references/mobile-composition.md +0 -70
- package/skills/struq/references/nav-header.md +0 -296
- package/skills/struq/references/niche-packets.md +0 -338
- package/skills/struq/references/page-architecture.md +0 -134
- package/skills/struq/references/page-entrances.md +0 -198
- package/skills/struq/references/performance.md +0 -235
- package/skills/struq/references/pinning-scrub.md +0 -340
- package/skills/struq/references/pitfalls.md +0 -290
- package/skills/struq/references/route-transitions.md +0 -281
- package/skills/struq/references/scaling.md +0 -151
- package/skills/struq/references/scroll-reveals.md +0 -279
- package/skills/struq/references/sections.md +0 -141
- package/skills/struq/references/seo.md +0 -219
- package/skills/struq/references/setup.md +0 -397
- package/skills/struq/references/showcase.md +0 -87
- package/skills/struq/references/struq-imagery.md +0 -107
- package/skills/struq/references/svg-motion.md +0 -390
- package/skills/struq/references/transitions.md +0 -160
- package/skills/struq/references/typography.md +0 -720
- package/skills/struq/references/vibe-presets.md +0 -287
- package/skills/struq/references/webgl-r3f.md +0 -274
- package/skills/struq/references/webgl-scenes.md +0 -593
- package/skills/struq/scripts/font-names.mjs +0 -40
- package/skills/struq/scripts/recipes.mjs +0 -135
- /package/skills/struq/{recipes/lib → blocks/kit}/gsap.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/contours.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/dots.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/ripple.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/types.ts +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The image a block receives. In a client project the skill's images.mjs writes the list of these from a
|
|
2
|
+
// beeldbank pack; the lab passes a few fixed beeldbank photos so every block is judged on real photography.
|
|
3
|
+
|
|
4
|
+
export type StruqImage = {
|
|
5
|
+
src: string;
|
|
6
|
+
alt: string;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
/** "16:9", "21:9", "3:2", "4:5" or "1:1" */
|
|
10
|
+
ratio: string;
|
|
11
|
+
/** What the photo shows, in more words than the alt: read it to choose the right slot. */
|
|
12
|
+
description: string;
|
|
13
|
+
/** The photo's calm side ("left", "right", "top", "bottom", "center"): where copy reads over it. */
|
|
14
|
+
negativeSpace?: string;
|
|
15
|
+
/** Set on fallback plates (SVG), which next/image must not optimise. */
|
|
16
|
+
unoptimized?: boolean;
|
|
17
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// photo-read (Struq kit)
|
|
2
|
+
// Reading what a photograph holds under a box on the screen, for the blocks that choose an ink or a veil from the
|
|
3
|
+
// photo itself. The whole image is drawn once to a small offscreen canvas and kept as a grid of luminances; a box is
|
|
4
|
+
// then mapped onto that grid through the element's object-fit: cover and its object-position.
|
|
5
|
+
//
|
|
6
|
+
// Why the whole image: with a srcset, img.naturalWidth is density-corrected (a 1920w file shown at 1440 reports
|
|
7
|
+
// 1440), while drawImage's source rectangle counts the file's real pixels. A sub-rectangle mapped from naturalWidth
|
|
8
|
+
// therefore lands up and to the left of where it should. Drawing the whole image to fill the grid only uses the ratio
|
|
9
|
+
// of naturalWidth to naturalHeight, which is right either way.
|
|
10
|
+
//
|
|
11
|
+
// const ys = photoLuminances(img, frame, box); // sorted ascending, or null when it cannot be read
|
|
12
|
+
// const median = ys && ys[Math.floor(ys.length * 0.5)];
|
|
13
|
+
//
|
|
14
|
+
// Returns null, so the caller keeps its safe treatment, when the photo is not loaded, comes from another origin
|
|
15
|
+
// (reading it would taint the canvas) or the canvas cannot be read.
|
|
16
|
+
|
|
17
|
+
export type PhotoBox = { x: number; y: number; w: number; h: number };
|
|
18
|
+
|
|
19
|
+
const GRID_W = 320;
|
|
20
|
+
|
|
21
|
+
const linear = (c: number) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
22
|
+
|
|
23
|
+
type Grid = { w: number; h: number; y: Float32Array };
|
|
24
|
+
const grids = new WeakMap<HTMLImageElement, { src: string; grid: Grid | null }>();
|
|
25
|
+
|
|
26
|
+
/** The photo's luminance on a small grid of the whole image, read once per source and kept. */
|
|
27
|
+
function gridOf(img: HTMLImageElement): Grid | null {
|
|
28
|
+
const src = img.currentSrc || img.src;
|
|
29
|
+
const iw = img.naturalWidth;
|
|
30
|
+
const ih = img.naturalHeight;
|
|
31
|
+
if (!src || !img.complete || !iw || !ih) return null;
|
|
32
|
+
const kept = grids.get(img);
|
|
33
|
+
if (kept && kept.src === src) return kept.grid;
|
|
34
|
+
let grid: Grid | null = null;
|
|
35
|
+
try {
|
|
36
|
+
if (new URL(src, location.href).origin !== location.origin) throw new Error("other origin");
|
|
37
|
+
const w = GRID_W;
|
|
38
|
+
const h = Math.max(1, Math.round((w * ih) / iw));
|
|
39
|
+
const cv = document.createElement("canvas");
|
|
40
|
+
cv.width = w;
|
|
41
|
+
cv.height = h;
|
|
42
|
+
const ctx = cv.getContext("2d", { willReadFrequently: true });
|
|
43
|
+
if (ctx) {
|
|
44
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
45
|
+
const d = ctx.getImageData(0, 0, w, h).data;
|
|
46
|
+
const y = new Float32Array(w * h);
|
|
47
|
+
for (let k = 0; k < w * h; k += 1) {
|
|
48
|
+
y[k] = 0.2126 * linear(d[k * 4] / 255) + 0.7152 * linear(d[k * 4 + 1] / 255) + 0.0722 * linear(d[k * 4 + 2] / 255);
|
|
49
|
+
}
|
|
50
|
+
grid = { w, h, y };
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
grid = null;
|
|
54
|
+
}
|
|
55
|
+
grids.set(img, { src, grid });
|
|
56
|
+
return grid;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The element's object-position as two shares (0 to 1); percentages and keywords, anything else is centred. */
|
|
60
|
+
function positionOf(img: HTMLImageElement): [number, number] {
|
|
61
|
+
const parts = (getComputedStyle(img).objectPosition || "50% 50%").trim().split(/\s+/);
|
|
62
|
+
const share = (v: string | undefined) => {
|
|
63
|
+
if (!v) return 0.5;
|
|
64
|
+
if (v.endsWith("%")) {
|
|
65
|
+
const n = parseFloat(v) / 100;
|
|
66
|
+
return Number.isFinite(n) ? n : 0.5;
|
|
67
|
+
}
|
|
68
|
+
if (v === "left" || v === "top") return 0;
|
|
69
|
+
if (v === "right" || v === "bottom") return 1;
|
|
70
|
+
if (v === "center") return 0.5;
|
|
71
|
+
return 0.5;
|
|
72
|
+
};
|
|
73
|
+
return [share(parts[0]), share(parts[1] ?? parts[0])];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The luminances of the photo under `at`, sorted ascending. `frame` is the box the image is drawn into (object-fit:
|
|
78
|
+
* cover at its object-position), in the same coordinates as `at`; pass `position` to override the element's own.
|
|
79
|
+
*/
|
|
80
|
+
export function photoLuminances(
|
|
81
|
+
img: HTMLImageElement,
|
|
82
|
+
frame: PhotoBox,
|
|
83
|
+
at: PhotoBox,
|
|
84
|
+
position?: [number, number],
|
|
85
|
+
): number[] | null {
|
|
86
|
+
if (frame.w <= 0 || frame.h <= 0 || at.w <= 0 || at.h <= 0) return null;
|
|
87
|
+
const g = gridOf(img);
|
|
88
|
+
if (!g) return null;
|
|
89
|
+
const [px, py] = position ?? positionOf(img);
|
|
90
|
+
// Frame pixels per grid cell, and where the covered image starts in the frame.
|
|
91
|
+
const c = Math.max(frame.w / g.w, frame.h / g.h);
|
|
92
|
+
const ox = frame.x + (frame.w - g.w * c) * px;
|
|
93
|
+
const oy = frame.y + (frame.h - g.h * c) * py;
|
|
94
|
+
const x0 = Math.max(0, Math.floor((at.x - ox) / c));
|
|
95
|
+
const x1 = Math.min(g.w, Math.max(x0 + 1, Math.ceil((at.x + at.w - ox) / c)));
|
|
96
|
+
const y0 = Math.max(0, Math.floor((at.y - oy) / c));
|
|
97
|
+
const y1 = Math.min(g.h, Math.max(y0 + 1, Math.ceil((at.y + at.h - oy) / c)));
|
|
98
|
+
if (x0 >= g.w || y0 >= g.h || x1 <= 0 || y1 <= 0) return null;
|
|
99
|
+
const ys: number[] = [];
|
|
100
|
+
for (let y = y0; y < y1; y += 1) for (let x = x0; x < x1; x += 1) ys.push(g.y[y * g.w + x]);
|
|
101
|
+
return ys.length ? ys.sort((a, b) => a - b) : null;
|
|
102
|
+
}
|