@jtakeit/astro 0.1.0
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/LICENSE +21 -0
- package/README.md +59 -0
- package/bin/jtk.mjs +41 -0
- package/docs/booking.md +164 -0
- package/docs/catalogue.md +459 -0
- package/docs/collections.md +249 -0
- package/docs/css.md +86 -0
- package/docs/gallery.md +127 -0
- package/docs/hero-motion.md +189 -0
- package/docs/kit.md +454 -0
- package/docs/languages.md +182 -0
- package/docs/lead-form.md +109 -0
- package/docs/pages.md +193 -0
- package/docs/photos.md +314 -0
- package/docs/scaffold.md +75 -0
- package/docs/shapes.md +140 -0
- package/docs/surface.md +187 -0
- package/lib/catalogue.mjs +1678 -0
- package/lib/codes.mjs +171 -0
- package/lib/create.mjs +282 -0
- package/package.json +16 -0
- package/template/astro.config.mjs +84 -0
- package/template/figures.mjs +122 -0
- package/template/gitignore +16 -0
- package/template/jtakeit-meta.mjs +112 -0
- package/template/jtk/content/index.json +38 -0
- package/template/jtk/design.json +24 -0
- package/template/markdown.mjs +36 -0
- package/template/package-lock.json +5320 -0
- package/template/package.json +26 -0
- package/template/specimens.mjs +46 -0
- package/template/src/components/Blocks.astro +151 -0
- package/template/src/components/BookingForm.astro +506 -0
- package/template/src/components/Clip.astro +155 -0
- package/template/src/components/Hero.astro +66 -0
- package/template/src/components/LeadForm.astro +347 -0
- package/template/src/components/OpeningHours.astro +69 -0
- package/template/src/components/Pile.astro +185 -0
- package/template/src/components/Shot.astro +472 -0
- package/template/src/components/gallery/Gallery.astro +381 -0
- package/template/src/components/gallery/galleries.ts +139 -0
- package/template/src/components/motion/HeroField.astro +520 -0
- package/template/src/components/motion/fields.ts +430 -0
- package/template/src/components/surface/Pattern.astro +278 -0
- package/template/src/components/surface/patterns.ts +187 -0
- package/template/src/content/blocks.ts +758 -0
- package/template/src/content.config.ts +19 -0
- package/template/src/copy/LOCALE.ts +324 -0
- package/template/src/data/site.ts +137 -0
- package/template/src/layouts/Layout.astro +282 -0
- package/template/src/lib/alive.ts +49 -0
- package/template/src/lib/entries.ts +106 -0
- package/template/src/lib/entryLoader.ts +315 -0
- package/template/src/lib/noise.ts +26 -0
- package/template/src/lib/page.ts +287 -0
- package/template/src/lib/photos.ts +168 -0
- package/template/src/lib/under.ts +32 -0
- package/template/src/lib/uploads.ts +85 -0
- package/template/src/pages/[...entry].astro +207 -0
- package/template/src/pages/[...feed].xml.ts +64 -0
- package/template/src/pages/index.astro +90 -0
- package/template/src/pages/llms.txt.ts +50 -0
- package/template/src/pages/privacy.astro +59 -0
- package/template/src/pages/robots.txt.ts +21 -0
- package/template/src/pages/sitemap.xml.ts +50 -0
- package/template/src/styles/global.css +411 -0
- package/template/src/styles/surface.css +375 -0
- package/template/tsconfig.json +5 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hero fields, as GLSL.
|
|
3
|
+
*
|
|
4
|
+
* Data only — no DOM, no WebGL calls. `HeroField.astro` owns the canvas, the
|
|
5
|
+
* palette, the sizing budget and every path back to the still CSS ground; this
|
|
6
|
+
* file is what each field actually looks like, which is the part worth reading
|
|
7
|
+
* on its own.
|
|
8
|
+
*
|
|
9
|
+
* Adding one is a fragment source plus a row in FRAGMENT. What it has to earn
|
|
10
|
+
* is a *different job* — another variation on soft coloured blobs is the same
|
|
11
|
+
* field with different constants.
|
|
12
|
+
*
|
|
13
|
+
* Every fragment shader is handed the same uniforms:
|
|
14
|
+
*
|
|
15
|
+
* u_resolution canvas pixels
|
|
16
|
+
* u_time seconds, already multiplied by the component's `speed`
|
|
17
|
+
* u_grain 0–1
|
|
18
|
+
* u_colors[4] the variant's tokens, in the order they were passed
|
|
19
|
+
* u_bg the ground colour
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type Field =
|
|
23
|
+
| 'drift'
|
|
24
|
+
| 'aurora'
|
|
25
|
+
| 'plasma'
|
|
26
|
+
| 'glow'
|
|
27
|
+
| 'flow'
|
|
28
|
+
| 'silk'
|
|
29
|
+
| 'smoke'
|
|
30
|
+
| 'riso'
|
|
31
|
+
| 'veil'
|
|
32
|
+
| 'ribbons';
|
|
33
|
+
|
|
34
|
+
/** The fields drawn by a fragment shader. `ribbons` is Canvas 2D and is not one. */
|
|
35
|
+
export type ShaderField = Exclude<Field, 'ribbons'>;
|
|
36
|
+
|
|
37
|
+
export const VERTEX = `attribute vec2 a_position;
|
|
38
|
+
void main() { gl_Position = vec4(a_position, 0.0, 1.0); }`;
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* highp where it exists, mediump where it does not. A phone GPU without highp
|
|
42
|
+
* in the fragment stage renders the noise as banding rather than failing, so
|
|
43
|
+
* the guard is worth the four lines.
|
|
44
|
+
*/
|
|
45
|
+
const HEAD = `#ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
46
|
+
precision highp float;
|
|
47
|
+
#else
|
|
48
|
+
precision mediump float;
|
|
49
|
+
#endif
|
|
50
|
+
|
|
51
|
+
uniform vec2 u_resolution;
|
|
52
|
+
uniform float u_time;
|
|
53
|
+
uniform float u_grain;
|
|
54
|
+
uniform vec3 u_colors[4];
|
|
55
|
+
uniform vec3 u_bg;
|
|
56
|
+
|
|
57
|
+
// Even, unstructured noise for the grain. A multiply hash shows a faint
|
|
58
|
+
// axis-aligned mesh at integer fragment coordinates, which reads as a net.
|
|
59
|
+
float grainHash(vec2 p) {
|
|
60
|
+
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
|
61
|
+
p3 += dot(p3, p3.yzx + 33.33);
|
|
62
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
/** Value noise: cheap, slightly blocky, right for large soft shapes. */
|
|
67
|
+
const VALUE_NOISE = `
|
|
68
|
+
float hash21(vec2 p) {
|
|
69
|
+
p = fract(p * vec2(234.34, 435.345));
|
|
70
|
+
p += dot(p, p + 34.23);
|
|
71
|
+
return fract(p.x * p.y);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
float noise(vec2 p) {
|
|
75
|
+
vec2 i = floor(p);
|
|
76
|
+
vec2 f = fract(p);
|
|
77
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
78
|
+
return mix(
|
|
79
|
+
mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),
|
|
80
|
+
mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),
|
|
81
|
+
u.y);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
float fbm(vec2 p) {
|
|
85
|
+
float v = 0.0;
|
|
86
|
+
float a = 0.5;
|
|
87
|
+
for (int i = 0; i < 4; i++) {
|
|
88
|
+
v += a * noise(p);
|
|
89
|
+
p = p * 2.03 + vec2(17.0, 9.2);
|
|
90
|
+
a *= 0.5;
|
|
91
|
+
}
|
|
92
|
+
return v;
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
/** Simplex noise: smoother and directionless, right for anything flowing. */
|
|
97
|
+
const SIMPLEX = `
|
|
98
|
+
vec3 permute(vec3 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); }
|
|
99
|
+
|
|
100
|
+
float snoise(vec2 v) {
|
|
101
|
+
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
|
|
102
|
+
-0.577350269189626, 0.024390243902439);
|
|
103
|
+
vec2 i = floor(v + dot(v, C.yy));
|
|
104
|
+
vec2 x0 = v - i + dot(i, C.xx);
|
|
105
|
+
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
|
106
|
+
vec4 x12 = x0.xyxy + C.xxzz;
|
|
107
|
+
x12.xy -= i1;
|
|
108
|
+
i = mod(i, 289.0);
|
|
109
|
+
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
|
|
110
|
+
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
|
|
111
|
+
m = m * m;
|
|
112
|
+
m = m * m;
|
|
113
|
+
vec3 x = 2.0 * fract(p * C.www) - 1.0;
|
|
114
|
+
vec3 h = abs(x) - 0.5;
|
|
115
|
+
vec3 ox = floor(x + 0.5);
|
|
116
|
+
vec3 a0 = x - ox;
|
|
117
|
+
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
|
|
118
|
+
vec3 g;
|
|
119
|
+
g.x = a0.x * x0.x + h.x * x0.y;
|
|
120
|
+
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
|
|
121
|
+
return 130.0 * dot(m, g);
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A ramp through the four colours. WebGL1 forbids indexing a uniform array by
|
|
127
|
+
* a computed value in the fragment stage, so the steps are written out.
|
|
128
|
+
*/
|
|
129
|
+
const PALETTE = `
|
|
130
|
+
vec3 palette(float x) {
|
|
131
|
+
float f = clamp(x, 0.0, 1.0) * 3.0;
|
|
132
|
+
vec3 col = u_colors[0];
|
|
133
|
+
col = mix(col, u_colors[1], smoothstep(0.0, 1.0, clamp(f, 0.0, 1.0)));
|
|
134
|
+
col = mix(col, u_colors[2], smoothstep(0.0, 1.0, clamp(f - 1.0, 0.0, 1.0)));
|
|
135
|
+
col = mix(col, u_colors[3], smoothstep(0.0, 1.0, clamp(f - 2.0, 0.0, 1.0)));
|
|
136
|
+
return col;
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
/** Four lamps of colour on slow orbits through a warped field. */
|
|
141
|
+
const DRIFT = `
|
|
142
|
+
void main() {
|
|
143
|
+
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
|
144
|
+
float m = min(u_resolution.x, u_resolution.y);
|
|
145
|
+
// Aspect-corrected, so the field does not stretch with the window.
|
|
146
|
+
vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / m * 1.3;
|
|
147
|
+
float t = u_time;
|
|
148
|
+
|
|
149
|
+
p += 0.15 * vec2(sin(t * 0.31), cos(t * 0.23));
|
|
150
|
+
p += 0.19 * (vec2(fbm(p * 2.0), fbm(p * 2.0 + vec2(5.2, 1.3))) - 0.5);
|
|
151
|
+
|
|
152
|
+
vec3 acc = u_bg * 0.25;
|
|
153
|
+
float total = 0.25;
|
|
154
|
+
for (int i = 0; i < 4; i++) {
|
|
155
|
+
float fi = float(i);
|
|
156
|
+
vec2 c = vec2(
|
|
157
|
+
sin(t * (0.21 + fi * 0.071) + fi * 2.4),
|
|
158
|
+
cos(t * (0.17 + fi * 0.093) + fi * 1.7)) * 0.75;
|
|
159
|
+
float w = exp(-dot(p - c, p - c) * 3.4);
|
|
160
|
+
acc += u_colors[i] * w;
|
|
161
|
+
total += w;
|
|
162
|
+
}
|
|
163
|
+
vec3 col = acc / total;
|
|
164
|
+
|
|
165
|
+
float vd = length(uv - 0.5) * 1.41421356;
|
|
166
|
+
col *= 1.0 - 0.15 * smoothstep(0.35, 1.0, vd);
|
|
167
|
+
col += (grainHash(gl_FragCoord.xy + vec2(t * 17.0)) - 0.5) * u_grain * 0.09;
|
|
168
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
169
|
+
}
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
/** Three layers of simplex noise riding each other. Weather. */
|
|
173
|
+
const AURORA = `
|
|
174
|
+
void main() {
|
|
175
|
+
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
|
176
|
+
vec2 p = uv - 0.5;
|
|
177
|
+
p.x *= u_resolution.x / u_resolution.y;
|
|
178
|
+
float t = u_time * 0.1;
|
|
179
|
+
|
|
180
|
+
// The second and third layers take the one above as an offset, which is what
|
|
181
|
+
// turns noise into weather.
|
|
182
|
+
float n1 = snoise(p * 0.4 + vec2(t * 0.2, -t * 0.3));
|
|
183
|
+
float n2 = snoise(p * 0.55 + vec2(-t * 0.15, t * 0.25) + n1 * 0.25);
|
|
184
|
+
float n3 = snoise(p * 0.75 + vec2(t * 0.1, -t * 0.2) + n2 * 0.2);
|
|
185
|
+
|
|
186
|
+
float dist = length(p) * 1.5;
|
|
187
|
+
float vignette = 1.0 - smoothstep(0.3, 1.2, dist);
|
|
188
|
+
|
|
189
|
+
vec3 col = u_bg;
|
|
190
|
+
col = mix(col, u_colors[0], smoothstep(-0.2, 0.5, n1) * 0.85);
|
|
191
|
+
col = mix(col, u_colors[1], smoothstep(-0.1, 0.6, n2) * 0.70);
|
|
192
|
+
col = mix(col, u_colors[2], smoothstep(-0.3, 0.4, n3) * 0.60);
|
|
193
|
+
col = mix(col, u_colors[3], smoothstep(0.0, 0.7, n1 * n2) * 0.50);
|
|
194
|
+
col += u_colors[1] * smoothstep(0.8, 0.0, dist) * 0.3;
|
|
195
|
+
col = mix(u_bg, col, 0.2 + 0.8 * vignette);
|
|
196
|
+
|
|
197
|
+
col += (grainHash(gl_FragCoord.xy + vec2(u_time * 17.0)) - 0.5) * u_grain * 0.1;
|
|
198
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
199
|
+
}
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Interference. Four sine fields summed and read through the palette, which
|
|
204
|
+
* produces bands and rings rather than blobs — the one field in the set with
|
|
205
|
+
* visible structure to it.
|
|
206
|
+
*/
|
|
207
|
+
const PLASMA = `
|
|
208
|
+
void main() {
|
|
209
|
+
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
|
210
|
+
float m = min(u_resolution.x, u_resolution.y);
|
|
211
|
+
vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / m * 1.5;
|
|
212
|
+
float t = u_time;
|
|
213
|
+
|
|
214
|
+
p += 0.32 * vec2(sin(t * 0.31), cos(t * 0.23));
|
|
215
|
+
|
|
216
|
+
// Higher k is more rings in the same frame. Past ~9 a phone shows moiré.
|
|
217
|
+
float k = 7.0;
|
|
218
|
+
float v = sin(p.x * k + t)
|
|
219
|
+
+ sin(p.y * k * 0.8 - t * 0.7)
|
|
220
|
+
+ sin((p.x + p.y) * k * 0.6 + t * 0.5)
|
|
221
|
+
+ sin(length(p) * k * 1.2 - t);
|
|
222
|
+
vec3 col = palette(0.5 + 0.5 * sin(v));
|
|
223
|
+
|
|
224
|
+
float vd = length(uv - 0.5) * 1.41421356;
|
|
225
|
+
col = mix(col, u_bg, 0.61 * smoothstep(0.35, 1.0, vd));
|
|
226
|
+
col += (grainHash(gl_FragCoord.xy + vec2(t * 17.0)) - 0.5) * u_grain * 0.12;
|
|
227
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
228
|
+
}
|
|
229
|
+
`;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* One bright filament of light in a dark room. Almost all of the frame stays
|
|
233
|
+
* near the ground colour, which is what makes the lit part read as light rather
|
|
234
|
+
* than as paint — and what makes this the only field in the set that needs a
|
|
235
|
+
* dark ground to work at all.
|
|
236
|
+
*/
|
|
237
|
+
const GLOW = `
|
|
238
|
+
void main() {
|
|
239
|
+
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
|
240
|
+
vec2 p = uv * vec2(u_resolution.x / u_resolution.y, 1.0);
|
|
241
|
+
float t = u_time * 0.2;
|
|
242
|
+
|
|
243
|
+
float n1 = snoise(p * 0.5 + t);
|
|
244
|
+
float n2 = snoise(p * 0.9 - t * 0.5 + n1);
|
|
245
|
+
float n3 = snoise(p * 1.6 + t * 0.35 + n2 * 0.5);
|
|
246
|
+
// The power is what thins the lit band into a filament: everything but the
|
|
247
|
+
// ridge of the noise falls away to nothing.
|
|
248
|
+
float light = pow(abs(n2), 2.5) * 0.5;
|
|
249
|
+
|
|
250
|
+
vec3 col = u_bg;
|
|
251
|
+
col += u_colors[0] * smoothstep(0.1, 1.0, n1) * 0.5;
|
|
252
|
+
col += u_colors[1] * light;
|
|
253
|
+
col += u_colors[2] * pow(abs(n3), 4.0) * 0.35;
|
|
254
|
+
|
|
255
|
+
col += (grainHash(gl_FragCoord.xy + vec2(u_time * 17.0)) - 0.5) * u_grain * 0.16;
|
|
256
|
+
// Toward the ground rather than toward black, so a light palette is possible
|
|
257
|
+
// even though this field does not really want one.
|
|
258
|
+
col = mix(u_bg, col, smoothstep(1.2, 0.2, length(uv - 0.5)));
|
|
259
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
260
|
+
}
|
|
261
|
+
`;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* A flow field. An fbm sample decides a direction at every point, and a second
|
|
265
|
+
* one is read along it — which is what turns noise into something that appears
|
|
266
|
+
* to be *moving somewhere*, like ink pulled through water or marbled paper.
|
|
267
|
+
*/
|
|
268
|
+
const FLOW = `
|
|
269
|
+
void main() {
|
|
270
|
+
float m = min(u_resolution.x, u_resolution.y);
|
|
271
|
+
vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / m * 1.5;
|
|
272
|
+
float t = u_time;
|
|
273
|
+
|
|
274
|
+
float angle = fbm(p * 2.0 + 1.7) * 6.2831;
|
|
275
|
+
vec2 dir = vec2(cos(angle), sin(angle));
|
|
276
|
+
vec3 col = palette(fbm(p * 3.0 + dir * 1.1 + t * 0.10));
|
|
277
|
+
|
|
278
|
+
col += (grainHash(gl_FragCoord.xy + vec2(t * 17.0)) - 0.5) * u_grain * 0.06;
|
|
279
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
280
|
+
}
|
|
281
|
+
`;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Silk. Four rounds of feeding the coordinate back through a cosine of itself
|
|
285
|
+
* fold the plane into smooth creases — the softest field in the set, and the
|
|
286
|
+
* one that survives being put behind a lot of text.
|
|
287
|
+
*/
|
|
288
|
+
const SILK = `
|
|
289
|
+
void main() {
|
|
290
|
+
float m = min(u_resolution.x, u_resolution.y);
|
|
291
|
+
vec2 q = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / m * 1.5 * 1.6;
|
|
292
|
+
float t = u_time * 0.35;
|
|
293
|
+
|
|
294
|
+
float amp = 0.72;
|
|
295
|
+
for (float i = 1.0; i < 5.0; i += 1.0) {
|
|
296
|
+
q.x += amp / i * cos(i * 2.4 * q.y + t * 0.8 + 1.0);
|
|
297
|
+
q.y += amp / i * cos(i * 1.7 * q.x + t * 0.6);
|
|
298
|
+
}
|
|
299
|
+
vec3 col = palette(0.5 + 0.5 * sin(q.x + q.y));
|
|
300
|
+
|
|
301
|
+
col += (grainHash(gl_FragCoord.xy + vec2(u_time * 17.0)) - 0.5) * u_grain * 0.06;
|
|
302
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
303
|
+
}
|
|
304
|
+
`;
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Smoke. Two rounds of domain warping — an fbm pair displaces the plane, and a
|
|
309
|
+
* second pair displaces it again — which is what produces volume: plumes that
|
|
310
|
+
* fold over each other and read as depth rather than as a pattern on a wall.
|
|
311
|
+
*
|
|
312
|
+
* The behaviour is the classic Iñigo Quilez warp, by way of the 21st.dev shader
|
|
313
|
+
* builder's "Smoke". What arrived with it was a fixed blue palette, and that is
|
|
314
|
+
* the half that had to go: the ramp is `u_colors` here, so it is their smoke.
|
|
315
|
+
*
|
|
316
|
+
* It is not `flow`. That one is a single flow field — one direction per point,
|
|
317
|
+
* one sample read along it — and it reads as marbling, flat and streaked. This
|
|
318
|
+
* has an inside.
|
|
319
|
+
*/
|
|
320
|
+
const SMOKE = `
|
|
321
|
+
void main() {
|
|
322
|
+
float m = min(u_resolution.x, u_resolution.y);
|
|
323
|
+
// Deliberately wider than the flow field. At the same scale the two are the
|
|
324
|
+
// same picture: what separates them is that this has a handful of large
|
|
325
|
+
// plumes crossing the frame, where flow is even everywhere.
|
|
326
|
+
vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / m * 1.1;
|
|
327
|
+
float t = u_time;
|
|
328
|
+
|
|
329
|
+
// How far the second warp pushes. Past ~6 the plumes stop reading as one
|
|
330
|
+
// volume and start reading as noise.
|
|
331
|
+
const float warp = 5.0;
|
|
332
|
+
vec2 q = vec2(fbm(p + t * 0.08), fbm(p + vec2(5.2, 1.3) - t * 0.06));
|
|
333
|
+
vec2 r = vec2(fbm(p + warp * q + vec2(1.7, 9.2)),
|
|
334
|
+
fbm(p + warp * q + vec2(8.3, 2.8)));
|
|
335
|
+
// The contrast goes on the ramp coordinate, not on the finished colour.
|
|
336
|
+
// Two rounds of warping pull fbm hard toward its average, so the field needs
|
|
337
|
+
// spreading — but spreading the *colour* blows the light end of the palette
|
|
338
|
+
// to white, and the centre below 0.5 is what keeps the dark end in the frame.
|
|
339
|
+
vec3 col = palette(clamp((fbm(p + 3.0 * r) - 0.5) * 1.4 + 0.4, 0.0, 1.0));
|
|
340
|
+
|
|
341
|
+
col += (grainHash(gl_FragCoord.xy + vec2(t * 17.0)) - 0.5) * u_grain * 0.07;
|
|
342
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
343
|
+
}
|
|
344
|
+
`;
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Riso. A slow wave through the palette, read through a coarse *quantised*
|
|
348
|
+
* noise — the ramp is dithered into cells of flat colour before it is looked
|
|
349
|
+
* up, which is the risograph and newsprint fault, not a texture laid on top.
|
|
350
|
+
*
|
|
351
|
+
* That distinction is the whole field. `u_grain` adds noise to the finished
|
|
352
|
+
* pixel and every field here has it; this adds noise to the *position in the
|
|
353
|
+
* ramp*, so the speckle lands as neighbouring bands of solid colour with a
|
|
354
|
+
* ragged border between them. It is the one field in the set that does not
|
|
355
|
+
* produce a smooth gradient, and the one that survives being printed.
|
|
356
|
+
*
|
|
357
|
+
* The cell is in device pixels and does not move. A dither that drifts shimmers,
|
|
358
|
+
* which is a screen effect; a dither that sits still is ink.
|
|
359
|
+
*/
|
|
360
|
+
const RISO = `
|
|
361
|
+
void main() {
|
|
362
|
+
float m = min(u_resolution.x, u_resolution.y);
|
|
363
|
+
// Two or three bands across the frame, not six. Wide bands leave flat areas
|
|
364
|
+
// for the dither to *not* be in, and a speckle that is everywhere is a
|
|
365
|
+
// texture rather than a print.
|
|
366
|
+
vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / m * 1.4;
|
|
367
|
+
float t = u_time;
|
|
368
|
+
|
|
369
|
+
vec2 q = p;
|
|
370
|
+
q.x += sin(q.y * 2.1 + t * 0.2) * 0.33;
|
|
371
|
+
q.y += cos(q.x * 2.7 - t * 0.17) * 0.26;
|
|
372
|
+
float wave = 0.5 + 0.5 * sin(q.x * 2.4 + q.y * 1.6 + fbm(q * 2.0) * 3.0);
|
|
373
|
+
|
|
374
|
+
// Three device pixels: coarse enough to see as ink, fine enough that it is
|
|
375
|
+
// not a mosaic. The amplitude decides how far into the neighbouring band a
|
|
376
|
+
// speckle can reach — past ~0.3 the ramp stops being a gradient at all.
|
|
377
|
+
float cell = hash21(floor(gl_FragCoord.xy / 3.0));
|
|
378
|
+
vec3 col = palette(clamp(wave + (cell - 0.5) * 0.22, 0.0, 1.0));
|
|
379
|
+
|
|
380
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
381
|
+
}
|
|
382
|
+
`;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Veil. A curtain: one fbm decides where the drapery hangs, a second is read
|
|
386
|
+
* *along* it, and the whole thing fades out toward the top and bottom edges of
|
|
387
|
+
* the frame.
|
|
388
|
+
*
|
|
389
|
+
* It is the field anchored to an edge. `aurora` is centred — a glow in the
|
|
390
|
+
* middle with a vignette around it — and everything in it points inward; this
|
|
391
|
+
* hangs from the horizontal and thins as it rises, so the hero's copy sits in
|
|
392
|
+
* the quiet part rather than on top of the bright part. Two fields of weather
|
|
393
|
+
* with two different compositions.
|
|
394
|
+
*
|
|
395
|
+
* Where the curtain is absent the ground shows through, which is why it wants a
|
|
396
|
+
* ground worth showing.
|
|
397
|
+
*/
|
|
398
|
+
const VEIL = `
|
|
399
|
+
void main() {
|
|
400
|
+
float m = min(u_resolution.x, u_resolution.y);
|
|
401
|
+
vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / m * 1.88;
|
|
402
|
+
float t = u_time;
|
|
403
|
+
|
|
404
|
+
float curtain = fbm(vec2(p.x * 2.0 + t * 0.15, p.y * 0.6 - t * 0.05));
|
|
405
|
+
// The second sample is indexed by the first rather than offset by it, which
|
|
406
|
+
// is what makes the bands hang vertically instead of drifting sideways.
|
|
407
|
+
float band = fbm(vec2(p.x * 2.2 - t * 0.1, curtain * 2.4));
|
|
408
|
+
// The upper bound sits past 1: the curtain never quite reaches the brightest
|
|
409
|
+
// colour, which is the difference between a veil and a floodlight.
|
|
410
|
+
float glow = smoothstep(0.18, 1.05, band) * (1.0 - abs(p.y) * 0.55);
|
|
411
|
+
|
|
412
|
+
vec3 col = mix(u_bg, palette(clamp(glow, 0.0, 1.0)),
|
|
413
|
+
0.12 + 0.88 * smoothstep(0.0, 0.35, glow));
|
|
414
|
+
|
|
415
|
+
col += (grainHash(gl_FragCoord.xy + vec2(t * 17.0)) - 0.5) * u_grain * 0.08;
|
|
416
|
+
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
|
417
|
+
}
|
|
418
|
+
`;
|
|
419
|
+
|
|
420
|
+
export const FRAGMENT: Record<ShaderField, string> = {
|
|
421
|
+
drift: HEAD + VALUE_NOISE + DRIFT,
|
|
422
|
+
aurora: HEAD + SIMPLEX + AURORA,
|
|
423
|
+
plasma: HEAD + PALETTE + PLASMA,
|
|
424
|
+
glow: HEAD + SIMPLEX + GLOW,
|
|
425
|
+
flow: HEAD + VALUE_NOISE + PALETTE + FLOW,
|
|
426
|
+
silk: HEAD + PALETTE + SILK,
|
|
427
|
+
smoke: HEAD + VALUE_NOISE + PALETTE + SMOKE,
|
|
428
|
+
riso: HEAD + VALUE_NOISE + PALETTE + RISO,
|
|
429
|
+
veil: HEAD + VALUE_NOISE + PALETTE + VEIL,
|
|
430
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The page's ground.
|
|
4
|
+
*
|
|
5
|
+
* One decorative layer behind the content, printed in the variant's own tokens.
|
|
6
|
+
* It is the opposite trade from `HeroField`: no canvas, no JavaScript, nothing
|
|
7
|
+
* that can lose a context, and it is meant to be *quiet*. The hero is where a
|
|
8
|
+
* page spends its one loud moment; this is the paper the rest of it is printed
|
|
9
|
+
* on, and a ground somebody notices is a ground turned up too far.
|
|
10
|
+
*
|
|
11
|
+
* <Pattern pattern="rules" scope="page" />
|
|
12
|
+
* <Pattern pattern="construction" opacity={0.05} draw />
|
|
13
|
+
* <Pattern pattern="motif"><path d="…" /></Pattern>
|
|
14
|
+
*
|
|
15
|
+
* Six grounds ship and they are not six skins of one look — pick from the
|
|
16
|
+
* design, not by taste. What each one is for is in the table in
|
|
17
|
+
* `docs/surface.md` of @jtakeit/astro; the figures themselves are in
|
|
18
|
+
* ./patterns.ts. The seventh choice is **none**, and it is a real one: on a
|
|
19
|
+
* page carrying strong photographs it is usually the right answer. It is made
|
|
20
|
+
* by not rendering this component — `jtk/design.json` leaves
|
|
21
|
+
* `surface.pattern` empty and nothing appears.
|
|
22
|
+
*
|
|
23
|
+
* ── it fails to nothing ─────────────────────────────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* No JavaScript, no mask support, a printer, reduced motion: every one of them
|
|
26
|
+
* ends at the plain token ground, which is a design somebody chose rather than
|
|
27
|
+
* a hole in the page. Nothing readable is ever inside this layer, nothing is
|
|
28
|
+
* ever positioned against it, and it is never the LCP element.
|
|
29
|
+
*/
|
|
30
|
+
import { STENCILS, DRAWINGS, DEFAULTS, isStencil, nextMotifId, type Pattern } from './patterns';
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
/**
|
|
34
|
+
* grain · rules · hatch · contour · construction · motif.
|
|
35
|
+
* Required: there is no sensible default ground, and a default is exactly how
|
|
36
|
+
* fifty sites end up sharing one. Choosing *none* means not rendering this.
|
|
37
|
+
*/
|
|
38
|
+
pattern: Pattern;
|
|
39
|
+
/**
|
|
40
|
+
* `page` pins one layer behind the whole document; `block` fills the section
|
|
41
|
+
* it sits in — that section needs `class="has-pattern"`, which is the two
|
|
42
|
+
* properties the layer has to sit under content rather than over it.
|
|
43
|
+
*
|
|
44
|
+
* A page-scope ground is only visible through sections that let it through.
|
|
45
|
+
* On a stack of full-bleed bands with their own backgrounds, half of them
|
|
46
|
+
* cover it — use `block` there, on the sections that should carry it.
|
|
47
|
+
*/
|
|
48
|
+
scope?: 'page' | 'block';
|
|
49
|
+
/** The figure's pitch: line spacing, hatch gap, motif tile. Any CSS length. */
|
|
50
|
+
size?: string;
|
|
51
|
+
/** Stroke weight. Any CSS length. */
|
|
52
|
+
weight?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The volume knob, and the property most likely to be set too high. A ground
|
|
55
|
+
* that competes with body text makes the page harder to read for everyone who
|
|
56
|
+
* did not choose it, so `fl-check` measures this one rather than leaving it
|
|
57
|
+
* to taste.
|
|
58
|
+
*/
|
|
59
|
+
opacity?: number;
|
|
60
|
+
/** What the ground is printed in. A token, not a colour. */
|
|
61
|
+
ink?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Let a drawn ground set itself out once, in order, and then stop. Ignored by
|
|
64
|
+
* the stencils — a texture has no beginning. Reduced motion gets the finished
|
|
65
|
+
* drawing, never a missing one.
|
|
66
|
+
*/
|
|
67
|
+
draw?: boolean;
|
|
68
|
+
class?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
pattern,
|
|
73
|
+
scope = 'block',
|
|
74
|
+
size,
|
|
75
|
+
weight,
|
|
76
|
+
opacity,
|
|
77
|
+
ink,
|
|
78
|
+
draw = false,
|
|
79
|
+
class: className,
|
|
80
|
+
} = Astro.props;
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* `motif` is the ground that carries the client's own mark — their tile, their
|
|
84
|
+
* fabric, their stamp, the shape off their sign — and it is the one entry in
|
|
85
|
+
* the catalogue that cannot be filled in from a catalogue. Empty, it would
|
|
86
|
+
* render an invisible layer that looks like it works, so it fails the build
|
|
87
|
+
* instead, the way `HeroField` fails without a palette.
|
|
88
|
+
*/
|
|
89
|
+
if (pattern === 'motif' && !Astro.slots.has('default')) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
'<Pattern pattern="motif"> needs the mark itself as its content — one SVG figure ' +
|
|
92
|
+
"taken from this business's own material. See references/surface.md; the other " +
|
|
93
|
+
'five grounds are chosen from the catalogue, this one is not.',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const base = DEFAULTS[pattern];
|
|
98
|
+
const stencil = isStencil(pattern) ? STENCILS[pattern] : null;
|
|
99
|
+
const drawing = pattern === 'motif' ? null : DRAWINGS[pattern as keyof typeof DRAWINGS];
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* The geometry lives on the layer rather than on :root, because the same token
|
|
103
|
+
* name means a 180px noise tile to one ground and a 14px hatch gap to another —
|
|
104
|
+
* one shared value would put at least one of them in the wrong place. A variant
|
|
105
|
+
* retunes a ground per breakpoint by targeting `.pattern`, which is also where
|
|
106
|
+
* it can drop the pitch on a phone.
|
|
107
|
+
*/
|
|
108
|
+
const vars = [
|
|
109
|
+
`--pattern-size:${size ?? base.size}`,
|
|
110
|
+
`--pattern-weight:${weight ?? base.weight}`,
|
|
111
|
+
`--pattern-opacity:${opacity ?? base.opacity}`,
|
|
112
|
+
ink ? `--pattern-ink:${ink}` : '',
|
|
113
|
+
stencil ? `--pattern-stencil:${stencil}` : '',
|
|
114
|
+
]
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.join(';');
|
|
117
|
+
|
|
118
|
+
/* Its own tile, in user units: an SVG <pattern> takes a number, not a token. */
|
|
119
|
+
const tile = pattern === 'motif' ? Math.max(1, parseFloat(size ?? base.size) || 96) : 0;
|
|
120
|
+
const motifId = pattern === 'motif' ? nextMotifId() : '';
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
<div
|
|
124
|
+
class:list={['pattern', `pattern--${scope}`, className]}
|
|
125
|
+
data-pattern={pattern}
|
|
126
|
+
data-draw={draw && !stencil ? '' : undefined}
|
|
127
|
+
style={vars}
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
>
|
|
130
|
+
{
|
|
131
|
+
stencil ? (
|
|
132
|
+
<div class="pattern__stencil" />
|
|
133
|
+
) : pattern === 'motif' ? (
|
|
134
|
+
<svg class="pattern__drawing" aria-hidden="true" focusable="false">
|
|
135
|
+
<defs>
|
|
136
|
+
<pattern id={motifId} width={tile} height={tile} patternUnits="userSpaceOnUse">
|
|
137
|
+
<slot />
|
|
138
|
+
</pattern>
|
|
139
|
+
</defs>
|
|
140
|
+
<rect width="100%" height="100%" fill={`url(#${motifId})`} />
|
|
141
|
+
</svg>
|
|
142
|
+
) : (
|
|
143
|
+
<svg
|
|
144
|
+
class="pattern__drawing"
|
|
145
|
+
viewBox={drawing?.viewBox}
|
|
146
|
+
preserveAspectRatio="xMidYMid slice"
|
|
147
|
+
fill="none"
|
|
148
|
+
stroke="currentColor"
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
focusable="false"
|
|
151
|
+
set:html={drawing?.markup}
|
|
152
|
+
/>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<style>
|
|
158
|
+
.pattern {
|
|
159
|
+
pointer-events: none;
|
|
160
|
+
color: var(--pattern-ink, var(--ink, #000));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/*
|
|
164
|
+
* Behind the document. A fixed layer costs one composited surface and does
|
|
165
|
+
* not repaint on scroll, which is why the page ground is fixed rather than a
|
|
166
|
+
* tall repeating image: the same picture for a tenth of the work.
|
|
167
|
+
*/
|
|
168
|
+
.pattern--page {
|
|
169
|
+
position: fixed;
|
|
170
|
+
inset: 0;
|
|
171
|
+
z-index: -1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/*
|
|
175
|
+
* Inside its section. `z-index: -1` puts it above that section's background
|
|
176
|
+
* and below its content — but only where the section is a stacking context,
|
|
177
|
+
* which is what `.has-pattern` is for. Without it the layer disappears behind
|
|
178
|
+
* the band's own colour.
|
|
179
|
+
*/
|
|
180
|
+
.pattern--block {
|
|
181
|
+
position: absolute;
|
|
182
|
+
inset: 0;
|
|
183
|
+
z-index: -1;
|
|
184
|
+
overflow: hidden;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.pattern__stencil,
|
|
188
|
+
.pattern__drawing {
|
|
189
|
+
position: absolute;
|
|
190
|
+
inset: 0;
|
|
191
|
+
inline-size: 100%;
|
|
192
|
+
block-size: 100%;
|
|
193
|
+
opacity: var(--pattern-opacity, 0.06);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
* The ink, stencilled. Painting a token through a mask rather than shipping a
|
|
198
|
+
* coloured image is what keeps the ground in the variant's palette — and it
|
|
199
|
+
* follows the studio bar's live accent for the same reason `HeroField` does.
|
|
200
|
+
*
|
|
201
|
+
* Where masks are unsupported the layer would be a solid block of ink over
|
|
202
|
+
* the page, so it is not drawn at all: no ground is a design, a rectangle of
|
|
203
|
+
* ink is a bug.
|
|
204
|
+
*/
|
|
205
|
+
.pattern__stencil {
|
|
206
|
+
display: none;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@supports (mask-image: linear-gradient(#000, #000)) or
|
|
210
|
+
(-webkit-mask-image: linear-gradient(#000, #000)) {
|
|
211
|
+
.pattern__stencil {
|
|
212
|
+
display: block;
|
|
213
|
+
background-color: currentColor;
|
|
214
|
+
-webkit-mask-image: var(--pattern-stencil);
|
|
215
|
+
mask-image: var(--pattern-stencil);
|
|
216
|
+
-webkit-mask-repeat: repeat;
|
|
217
|
+
mask-repeat: repeat;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/*
|
|
222
|
+
* Only a tile has a size. The ruled and hatched grounds are gradients, which
|
|
223
|
+
* measure their pitch themselves out of --pattern-size and repeat inside one
|
|
224
|
+
* mask box; sizing that box would squash the figure instead of tiling it.
|
|
225
|
+
*/
|
|
226
|
+
.pattern[data-pattern='grain'] .pattern__stencil {
|
|
227
|
+
-webkit-mask-size: var(--pattern-size) var(--pattern-size);
|
|
228
|
+
mask-size: var(--pattern-size) var(--pattern-size);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.pattern__drawing {
|
|
232
|
+
stroke-width: var(--pattern-weight, 1px);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* The figures arrive as markup from patterns.ts, so the scoped selector does
|
|
236
|
+
not reach them. */
|
|
237
|
+
.pattern__drawing :global(*) {
|
|
238
|
+
vector-effect: non-scaling-stroke;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/*
|
|
242
|
+
* Setting out, once. The still state is the default, so a visitor with no
|
|
243
|
+
* JavaScript sees the finished drawing — `html.js` is set before first paint
|
|
244
|
+
* in the layout, and only then is anything hidden.
|
|
245
|
+
*
|
|
246
|
+
* `pathLength="1"` on every figure is what makes this one rule work for a
|
|
247
|
+
* circle, a line and a 56-point contour alike: the browser measures the path,
|
|
248
|
+
* nobody types a length into the markup, and a figure edited later still
|
|
249
|
+
* draws correctly.
|
|
250
|
+
*/
|
|
251
|
+
html.js .pattern[data-draw] .pattern__drawing :global(*) {
|
|
252
|
+
stroke-dasharray: 1;
|
|
253
|
+
stroke-dashoffset: 1;
|
|
254
|
+
animation: pattern-draw 1.4s var(--ease, ease) forwards;
|
|
255
|
+
animation-delay: calc(var(--i, 0) * 180ms);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@keyframes pattern-draw {
|
|
259
|
+
to {
|
|
260
|
+
stroke-dashoffset: 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Reduced motion means the finished picture, not a missing one. */
|
|
265
|
+
@media (prefers-reduced-motion: reduce) {
|
|
266
|
+
html.js .pattern[data-draw] .pattern__drawing :global(*) {
|
|
267
|
+
stroke-dashoffset: 0;
|
|
268
|
+
animation: none;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* Ink is expensive and a texture is not the content. */
|
|
273
|
+
@media print {
|
|
274
|
+
.pattern {
|
|
275
|
+
display: none;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
</style>
|