Skip to content

Commit 760f81d

Browse files
committed
add tartan shader
1 parent d1a2688 commit 760f81d

File tree

9 files changed

+342
-0
lines changed

9 files changed

+342
-0
lines changed

docs/registry.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,19 @@
275275
"type": "registry:component"
276276
}
277277
]
278+
},
279+
{
280+
"name": "tartan",
281+
"type": "registry:component",
282+
"title": "Tartan Example",
283+
"description": "Tartan shader example.",
284+
"dependencies": ["@paper-design/shaders-react"],
285+
"files": [
286+
{
287+
"path": "registry/tartan-example.tsx",
288+
"type": "registry:component"
289+
}
290+
]
278291
}
279292
]
280293
}

docs/registry/tartan-example.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Tartan, type TartanProps } from '@paper-design/shaders-react';
2+
3+
export function TartanExample(props: TartanProps) {
4+
return <Tartan style={{ position: 'fixed', width: '100%', height: '100%' }} {...props} />;
5+
}

docs/src/app/tartan/layout.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Metadata } from 'next';
2+
3+
export const metadata: Metadata = {
4+
title: 'Tartan Shader | Paper',
5+
};
6+
7+
export default function Layout({ children }: { children: React.ReactNode }) {
8+
return <>{children}</>;
9+
}

docs/src/app/tartan/page.tsx

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
'use client';
2+
3+
import { BackButton } from '@/components/back-button';
4+
import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params';
5+
import { toHsla } from '@/helpers/to-hsla';
6+
import { usePresetHighlight } from '@/helpers/use-preset-highlight';
7+
import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params';
8+
import { type ShaderFit, ShaderFitOptions, tartanMeta } from '@paper-design/shaders';
9+
import { Tartan, tartanPresets } from '@paper-design/shaders-react';
10+
import { button, folder, useControls } from 'leva';
11+
import Link from 'next/link';
12+
13+
/**
14+
* You can copy/paste this example to use Tartan in your app
15+
*/
16+
const TartanExample = () => {
17+
return <Tartan style={{ position: 'fixed', width: '100%', height: '100%' }} />;
18+
};
19+
20+
/**
21+
* This example has controls added so you can play with settings in the example app
22+
*/
23+
24+
const defaults = tartanPresets[0].params;
25+
26+
const TartanWithControls = () => {
27+
const [{ count: stripeCount }, setStripeCount] = useControls(() => ({
28+
Stripes: folder(
29+
{
30+
count: {
31+
value: defaults.stripeColors.length,
32+
min: 2,
33+
max: tartanMeta.maxStripeCount,
34+
step: 1,
35+
order: 0,
36+
},
37+
},
38+
{ order: 1 }
39+
),
40+
}));
41+
42+
const [colors, setColors] = useControls(() => {
43+
const stripe: Record<string, { value: string; [key: string]: unknown }> = {};
44+
45+
for (let i = 0; i < stripeCount; i++) {
46+
stripe[`color${i + 1}`] = {
47+
value: defaults.stripeColors[i] ? toHsla(defaults.stripeColors[i]) : `hsla(${(40 * i) % 360}, 60%, 50%, 1)`,
48+
order: 1 + i * 2,
49+
};
50+
}
51+
52+
return {
53+
Stripes: folder(stripe),
54+
};
55+
}, [stripeCount]);
56+
57+
const [widths, setWidths] = useControls(() => {
58+
const stripe: Record<string, { value: number; [key: string]: unknown }> = {};
59+
60+
for (let i = 0; i < stripeCount; i++) {
61+
stripe[`width${i + 1}`] = {
62+
value: defaults.stripeWidths[i],
63+
min: 1,
64+
max: 400,
65+
step: 1,
66+
order: 1 + i * 2 + 1,
67+
};
68+
}
69+
70+
return {
71+
Stripes: folder(stripe),
72+
};
73+
}, [stripeCount]);
74+
75+
const [params, setParams] = useControls(() => {
76+
return {
77+
Transform: folder(
78+
{
79+
scale: { value: defaults.scale, min: 0.01, max: 4, order: 400 },
80+
rotation: { value: defaults.rotation, min: 0, max: 360, order: 401 },
81+
offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 402 },
82+
offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 403 },
83+
},
84+
{
85+
order: 2,
86+
collapsed: false,
87+
}
88+
),
89+
Fit: folder(
90+
{
91+
fit: { value: defaults.fit, options: Object.keys(ShaderFitOptions) as ShaderFit[], order: 404 },
92+
worldWidth: { value: 1000, min: 0, max: 5120, order: 405 },
93+
worldHeight: { value: 500, min: 0, max: 5120, order: 406 },
94+
originX: { value: defaults.originX, min: 0, max: 1, order: 407 },
95+
originY: { value: defaults.originY, min: 0, max: 1, order: 408 },
96+
},
97+
{
98+
order: 3,
99+
collapsed: true,
100+
}
101+
),
102+
};
103+
});
104+
105+
useControls(() => {
106+
const presets = Object.fromEntries(
107+
tartanPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [
108+
name,
109+
button(() => {
110+
const { stripeColors, stripeWidths, ...presetParams } = preset;
111+
setStripeCount({ count: stripeColors.length });
112+
setColors(
113+
Object.fromEntries(stripeColors.map((value, i) => [`color${i + 1}`, toHsla(value)])) as unknown as Record<
114+
string,
115+
{ value: string; [key: string]: unknown }
116+
>
117+
);
118+
setWidths(Object.fromEntries(stripeWidths.map((value, i) => [`width${i + 1}`, value])));
119+
setParamsSafe(params, setParams, presetParams);
120+
}),
121+
])
122+
);
123+
return {
124+
Presets: folder(presets, { order: -1 }),
125+
};
126+
});
127+
128+
// Reset to defaults on mount, so that Leva doesn't show values from other
129+
// shaders when navigating (if two shaders have a color1 param for example)
130+
useResetLevaParams(params, setParams, defaults);
131+
usePresetHighlight(tartanPresets, params);
132+
cleanUpLevaParams(params);
133+
134+
return (
135+
<>
136+
<Link href="/">
137+
<BackButton />
138+
</Link>
139+
<Tartan
140+
{...params}
141+
stripeColors={Object.values(colors) as unknown as Array<string>}
142+
stripeWidths={[...Object.values(widths), ...Array(9 - stripeCount).fill(0)]}
143+
className="fixed size-full"
144+
/>
145+
</>
146+
);
147+
};
148+
149+
export default TartanWithControls;

docs/src/home-shaders.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ import {
6363
staticMeshGradientPresets,
6464
StaticRadialGradient,
6565
staticRadialGradientPresets,
66+
Tartan,
67+
tartanPresets,
6668
} from '@paper-design/shaders-react';
6769
import { StaticImageData } from 'next/image';
6870
import TextureTest from './app/texture-test/page';
@@ -230,4 +232,10 @@ export const homeShaders = [
230232
image: staticRadialGradientImg,
231233
shaderConfig: { ...staticRadialGradientPresets[0].params },
232234
},
235+
{
236+
name: 'tartan',
237+
url: '/tartan',
238+
ShaderComponent: Tartan,
239+
shaderConfig: { ...tartanPresets[0].params },
240+
},
233241
] satisfies HomeShaderConfig[];

packages/shaders-react/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ export { StaticRadialGradient, staticRadialGradientPresets } from './shaders/sta
8787
export type { StaticRadialGradientProps } from './shaders/static-radial-gradient.js';
8888
export type { StaticRadialGradientUniforms, StaticRadialGradientParams } from '@paper-design/shaders';
8989

90+
export { Tartan, tartanPresets } from './shaders/tartan.js';
91+
export type { TartanProps } from './shaders/tartan.js';
92+
export type { TartanUniforms, TartanParams } from '@paper-design/shaders';
93+
9094
export { isPaperShaderElement, getShaderColorFromString } from '@paper-design/shaders';
9195
export type { PaperShaderElement, ShaderFit, ShaderSizingParams, ShaderSizingUniforms } from '@paper-design/shaders';
9296

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { memo } from 'react';
2+
import {
3+
defaultPatternSizing,
4+
getShaderColorFromString,
5+
ShaderFitOptions,
6+
type ShaderPreset,
7+
tartanFragmentShader,
8+
type TartanParams,
9+
type TartanUniforms,
10+
} from '@paper-design/shaders';
11+
import { colorPropsAreEqual } from '../color-props-are-equal.js';
12+
import { type ShaderComponentProps, ShaderMount } from '../shader-mount.js';
13+
14+
export interface TartanProps extends ShaderComponentProps, TartanParams {}
15+
16+
type TartanPreset = ShaderPreset<TartanParams>;
17+
18+
export const defaultPreset: TartanPreset = {
19+
name: 'Default',
20+
params: {
21+
...defaultPatternSizing,
22+
stripeColors: ['#19600b', '#aa0909', '#19600b', '#041a07', '#c3a855', '#041a07'],
23+
stripeWidths: [30, 4, 40, 30, 1, 30],
24+
},
25+
};
26+
27+
export const tartanPresets: TartanPreset[] = [defaultPreset];
28+
29+
export const Tartan: React.FC<TartanProps> = memo(function TartanImpl({
30+
// Own props
31+
stripeColors = defaultPreset.params.stripeColors,
32+
stripeWidths = defaultPreset.params.stripeWidths,
33+
34+
// Sizing props
35+
fit = defaultPreset.params.fit,
36+
scale = defaultPreset.params.scale,
37+
rotation = defaultPreset.params.rotation,
38+
originX = defaultPreset.params.originX,
39+
originY = defaultPreset.params.originY,
40+
offsetX = defaultPreset.params.offsetX,
41+
offsetY = defaultPreset.params.offsetY,
42+
worldWidth = defaultPreset.params.worldWidth,
43+
worldHeight = defaultPreset.params.worldHeight,
44+
...props
45+
}: TartanProps) {
46+
const uniforms = {
47+
// Own uniforms
48+
u_stripeColors: stripeColors.map(getShaderColorFromString),
49+
u_stripeWidths: stripeWidths,
50+
u_stripeCount: stripeColors.length,
51+
52+
// Sizing uniforms
53+
u_fit: ShaderFitOptions[fit],
54+
u_scale: scale,
55+
u_rotation: rotation,
56+
u_offsetX: offsetX,
57+
u_offsetY: offsetY,
58+
u_originX: originX,
59+
u_originY: originY,
60+
u_worldWidth: worldWidth,
61+
u_worldHeight: worldHeight,
62+
} satisfies TartanUniforms;
63+
64+
return <ShaderMount {...props} fragmentShader={tartanFragmentShader} uniforms={uniforms} />;
65+
}, colorPropsAreEqual);

packages/shaders/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ export {
171171
type StaticRadialGradientUniforms,
172172
} from './shaders/static-radial-gradient.js';
173173

174+
// ----- Tartan ----- //
175+
export { tartanMeta, tartanFragmentShader, type TartanParams, type TartanUniforms } from './shaders/tartan.js';
176+
174177
// ----- Utils ----- //
175178
export { getShaderColorFromString } from './get-shader-color-from-string.js';
176179
export { getShaderNoiseTexture } from './get-shader-noise-texture.js';
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js';
2+
import type { vec4 } from '../types.js';
3+
4+
export const tartanMeta = {
5+
maxStripeCount: 9,
6+
} as const;
7+
8+
/**
9+
* Tartan patterns
10+
*
11+
* Uniforms:
12+
* - u_stripeCount: number of stripes in the pattern (float used as integer)
13+
* - u_stripeColors (vec4[])
14+
* - u_stripeWidths (mat3)
15+
*
16+
*/
17+
18+
// language=GLSL
19+
export const tartanFragmentShader: string = `#version 300 es
20+
precision mediump float;
21+
22+
uniform float u_stripeCount;
23+
uniform vec4[${tartanMeta.maxStripeCount}] u_stripeColors;
24+
uniform mat3 u_stripeWidths;
25+
26+
${sizingVariablesDeclaration}
27+
28+
out vec4 fragColor;
29+
30+
void main() {
31+
vec2 uv = v_patternUV * 100.0;
32+
33+
float[${tartanMeta.maxStripeCount}] cumulativeWidths;
34+
35+
float totalWidth = 0.0;
36+
37+
for (int i = 0; i < ${tartanMeta.maxStripeCount}; i++) {
38+
if (i >= int(u_stripeCount)) break;
39+
float width = float(u_stripeWidths[int(i / 3)][int(i % 3)]);
40+
cumulativeWidths[i] = (i == 0 ? 0.0 : cumulativeWidths[i - 1]) + width;
41+
totalWidth += width;
42+
}
43+
44+
vec2 cell = mod(
45+
uv.xy,
46+
totalWidth * 2.0
47+
) - totalWidth;
48+
49+
// Color of vertical stripe.
50+
vec4 verticalColor;
51+
for (int i = 0; i < ${tartanMeta.maxStripeCount}; i++) {
52+
if (i >= int(u_stripeCount)) break;
53+
verticalColor = u_stripeColors[i];
54+
if (abs(cell.x) < cumulativeWidths[i]) {
55+
break;
56+
}
57+
}
58+
59+
// Color of horizontal stripe.
60+
vec4 horizontalColor;
61+
for (int i = 0; i < ${tartanMeta.maxStripeCount}; i++) {
62+
if (i >= int(u_stripeCount)) break;
63+
horizontalColor = u_stripeColors[i];
64+
if (abs(cell.y) < cumulativeWidths[i]) {
65+
break;
66+
}
67+
}
68+
69+
// Weave pattern.
70+
// See: https://en.wikipedia.org/wiki/Tartan#Weaving_construction
71+
float a = mod(uv.x + mod(floor(uv.y), 4.0), 4.0) / 4.0;
72+
73+
fragColor = a < 0.5 ? verticalColor : horizontalColor;
74+
}
75+
`;
76+
77+
export interface TartanUniforms extends ShaderSizingUniforms {
78+
u_stripeColors: vec4[];
79+
u_stripeWidths: number[];
80+
u_stripeCount: number;
81+
}
82+
83+
export interface TartanParams extends ShaderSizingParams {
84+
stripeColors?: string[];
85+
stripeWidths?: number[];
86+
}

0 commit comments

Comments
 (0)