From 359de9e2f151b00cd700b6b77ab010ed0e344d47 Mon Sep 17 00:00:00 2001
From: Haydn Ewers <27211+haydn@users.noreply.github.com>
Date: Mon, 28 Jul 2025 14:07:50 +1000
Subject: [PATCH 1/5] add tartan shader
---
docs/registry.json | 13 ++
docs/registry/tartan-example.tsx | 5 +
docs/src/app/tartan/layout.tsx | 9 ++
docs/src/app/tartan/page.tsx | 149 ++++++++++++++++++
docs/src/home-shaders.ts | 8 +
packages/shaders-react/src/index.ts | 4 +
packages/shaders-react/src/shaders/tartan.tsx | 65 ++++++++
packages/shaders/src/index.ts | 3 +
packages/shaders/src/shaders/tartan.ts | 86 ++++++++++
9 files changed, 342 insertions(+)
create mode 100644 docs/registry/tartan-example.tsx
create mode 100644 docs/src/app/tartan/layout.tsx
create mode 100644 docs/src/app/tartan/page.tsx
create mode 100644 packages/shaders-react/src/shaders/tartan.tsx
create mode 100644 packages/shaders/src/shaders/tartan.ts
diff --git a/docs/registry.json b/docs/registry.json
index f984641d3..bc9743222 100644
--- a/docs/registry.json
+++ b/docs/registry.json
@@ -275,6 +275,19 @@
"type": "registry:component"
}
]
+ },
+ {
+ "name": "tartan",
+ "type": "registry:component",
+ "title": "Tartan Example",
+ "description": "Tartan shader example.",
+ "dependencies": ["@paper-design/shaders-react"],
+ "files": [
+ {
+ "path": "registry/tartan-example.tsx",
+ "type": "registry:component"
+ }
+ ]
}
]
}
diff --git a/docs/registry/tartan-example.tsx b/docs/registry/tartan-example.tsx
new file mode 100644
index 000000000..583d4c36a
--- /dev/null
+++ b/docs/registry/tartan-example.tsx
@@ -0,0 +1,5 @@
+import { Tartan, type TartanProps } from '@paper-design/shaders-react';
+
+export function TartanExample(props: TartanProps) {
+ return ;
+}
diff --git a/docs/src/app/tartan/layout.tsx b/docs/src/app/tartan/layout.tsx
new file mode 100644
index 000000000..400d7b968
--- /dev/null
+++ b/docs/src/app/tartan/layout.tsx
@@ -0,0 +1,9 @@
+import { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ title: 'Tartan Shader | Paper',
+};
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return <>{children}>;
+}
diff --git a/docs/src/app/tartan/page.tsx b/docs/src/app/tartan/page.tsx
new file mode 100644
index 000000000..a11481976
--- /dev/null
+++ b/docs/src/app/tartan/page.tsx
@@ -0,0 +1,149 @@
+'use client';
+
+import { BackButton } from '@/components/back-button';
+import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params';
+import { toHsla } from '@/helpers/to-hsla';
+import { usePresetHighlight } from '@/helpers/use-preset-highlight';
+import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params';
+import { type ShaderFit, ShaderFitOptions, tartanMeta } from '@paper-design/shaders';
+import { Tartan, tartanPresets } from '@paper-design/shaders-react';
+import { button, folder, useControls } from 'leva';
+import Link from 'next/link';
+
+/**
+ * You can copy/paste this example to use Tartan in your app
+ */
+const TartanExample = () => {
+ return ;
+};
+
+/**
+ * This example has controls added so you can play with settings in the example app
+ */
+
+const defaults = tartanPresets[0].params;
+
+const TartanWithControls = () => {
+ const [{ count: stripeCount }, setStripeCount] = useControls(() => ({
+ Stripes: folder(
+ {
+ count: {
+ value: defaults.stripeColors.length,
+ min: 2,
+ max: tartanMeta.maxStripeCount,
+ step: 1,
+ order: 0,
+ },
+ },
+ { order: 1 }
+ ),
+ }));
+
+ const [colors, setColors] = useControls(() => {
+ const stripe: Record = {};
+
+ for (let i = 0; i < stripeCount; i++) {
+ stripe[`color${i + 1}`] = {
+ value: defaults.stripeColors[i] ? toHsla(defaults.stripeColors[i]) : `hsla(${(40 * i) % 360}, 60%, 50%, 1)`,
+ order: 1 + i * 2,
+ };
+ }
+
+ return {
+ Stripes: folder(stripe),
+ };
+ }, [stripeCount]);
+
+ const [widths, setWidths] = useControls(() => {
+ const stripe: Record = {};
+
+ for (let i = 0; i < stripeCount; i++) {
+ stripe[`width${i + 1}`] = {
+ value: defaults.stripeWidths[i],
+ min: 1,
+ max: 400,
+ step: 1,
+ order: 1 + i * 2 + 1,
+ };
+ }
+
+ return {
+ Stripes: folder(stripe),
+ };
+ }, [stripeCount]);
+
+ const [params, setParams] = useControls(() => {
+ return {
+ Transform: folder(
+ {
+ scale: { value: defaults.scale, min: 0.01, max: 4, order: 400 },
+ rotation: { value: defaults.rotation, min: 0, max: 360, order: 401 },
+ offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 402 },
+ offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 403 },
+ },
+ {
+ order: 2,
+ collapsed: false,
+ }
+ ),
+ Fit: folder(
+ {
+ fit: { value: defaults.fit, options: Object.keys(ShaderFitOptions) as ShaderFit[], order: 404 },
+ worldWidth: { value: 1000, min: 0, max: 5120, order: 405 },
+ worldHeight: { value: 500, min: 0, max: 5120, order: 406 },
+ originX: { value: defaults.originX, min: 0, max: 1, order: 407 },
+ originY: { value: defaults.originY, min: 0, max: 1, order: 408 },
+ },
+ {
+ order: 3,
+ collapsed: true,
+ }
+ ),
+ };
+ });
+
+ useControls(() => {
+ const presets = Object.fromEntries(
+ tartanPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [
+ name,
+ button(() => {
+ const { stripeColors, stripeWidths, ...presetParams } = preset;
+ setStripeCount({ count: stripeColors.length });
+ setColors(
+ Object.fromEntries(stripeColors.map((value, i) => [`color${i + 1}`, toHsla(value)])) as unknown as Record<
+ string,
+ { value: string; [key: string]: unknown }
+ >
+ );
+ setWidths(Object.fromEntries(stripeWidths.map((value, i) => [`width${i + 1}`, value])));
+ setParamsSafe(params, setParams, presetParams);
+ }),
+ ])
+ );
+ return {
+ Presets: folder(presets, { order: -1 }),
+ };
+ });
+
+ // Reset to defaults on mount, so that Leva doesn't show values from other
+ // shaders when navigating (if two shaders have a color1 param for example)
+ useResetLevaParams(params, setParams, defaults);
+ usePresetHighlight(tartanPresets, params);
+ cleanUpLevaParams(params);
+
+ return (
+ <>
+
+
+
+ }
+ stripeWidths={[...Object.values(widths), ...Array(9 - stripeCount).fill(0)]}
+ className="fixed size-full"
+ />
+ >
+ );
+};
+
+export default TartanWithControls;
diff --git a/docs/src/home-shaders.ts b/docs/src/home-shaders.ts
index 895a1ab1c..9eefbffb9 100644
--- a/docs/src/home-shaders.ts
+++ b/docs/src/home-shaders.ts
@@ -63,6 +63,8 @@ import {
staticMeshGradientPresets,
StaticRadialGradient,
staticRadialGradientPresets,
+ Tartan,
+ tartanPresets,
} from '@paper-design/shaders-react';
import { StaticImageData } from 'next/image';
import TextureTest from './app/texture-test/page';
@@ -230,4 +232,10 @@ export const homeShaders = [
image: godRaysImg,
shaderConfig: { ...godRaysPresets[0].params, speed: 2, scale: 0.5, offsetY: -0.5 },
},
+ {
+ name: 'tartan',
+ url: '/tartan',
+ ShaderComponent: Tartan,
+ shaderConfig: { ...tartanPresets[0].params },
+ },
] satisfies HomeShaderConfig[];
diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts
index 9b4d71306..d0e270fd2 100644
--- a/packages/shaders-react/src/index.ts
+++ b/packages/shaders-react/src/index.ts
@@ -87,6 +87,10 @@ export { StaticRadialGradient, staticRadialGradientPresets } from './shaders/sta
export type { StaticRadialGradientProps } from './shaders/static-radial-gradient.js';
export type { StaticRadialGradientUniforms, StaticRadialGradientParams } from '@paper-design/shaders';
+export { Tartan, tartanPresets } from './shaders/tartan.js';
+export type { TartanProps } from './shaders/tartan.js';
+export type { TartanUniforms, TartanParams } from '@paper-design/shaders';
+
export { isPaperShaderElement, getShaderColorFromString } from '@paper-design/shaders';
export type { PaperShaderElement, ShaderFit, ShaderSizingParams, ShaderSizingUniforms } from '@paper-design/shaders';
diff --git a/packages/shaders-react/src/shaders/tartan.tsx b/packages/shaders-react/src/shaders/tartan.tsx
new file mode 100644
index 000000000..7acdfcbed
--- /dev/null
+++ b/packages/shaders-react/src/shaders/tartan.tsx
@@ -0,0 +1,65 @@
+import { memo } from 'react';
+import {
+ defaultPatternSizing,
+ getShaderColorFromString,
+ ShaderFitOptions,
+ type ShaderPreset,
+ tartanFragmentShader,
+ type TartanParams,
+ type TartanUniforms,
+} from '@paper-design/shaders';
+import { colorPropsAreEqual } from '../color-props-are-equal.js';
+import { type ShaderComponentProps, ShaderMount } from '../shader-mount.js';
+
+export interface TartanProps extends ShaderComponentProps, TartanParams {}
+
+type TartanPreset = ShaderPreset;
+
+export const defaultPreset: TartanPreset = {
+ name: 'Default',
+ params: {
+ ...defaultPatternSizing,
+ stripeColors: ['#19600b', '#aa0909', '#19600b', '#041a07', '#c3a855', '#041a07'],
+ stripeWidths: [30, 4, 40, 30, 1, 30],
+ },
+};
+
+export const tartanPresets: TartanPreset[] = [defaultPreset];
+
+export const Tartan: React.FC = memo(function TartanImpl({
+ // Own props
+ stripeColors = defaultPreset.params.stripeColors,
+ stripeWidths = defaultPreset.params.stripeWidths,
+
+ // Sizing props
+ fit = defaultPreset.params.fit,
+ scale = defaultPreset.params.scale,
+ rotation = defaultPreset.params.rotation,
+ originX = defaultPreset.params.originX,
+ originY = defaultPreset.params.originY,
+ offsetX = defaultPreset.params.offsetX,
+ offsetY = defaultPreset.params.offsetY,
+ worldWidth = defaultPreset.params.worldWidth,
+ worldHeight = defaultPreset.params.worldHeight,
+ ...props
+}: TartanProps) {
+ const uniforms = {
+ // Own uniforms
+ u_stripeColors: stripeColors.map(getShaderColorFromString),
+ u_stripeWidths: stripeWidths,
+ u_stripeCount: stripeColors.length,
+
+ // Sizing uniforms
+ u_fit: ShaderFitOptions[fit],
+ u_scale: scale,
+ u_rotation: rotation,
+ u_offsetX: offsetX,
+ u_offsetY: offsetY,
+ u_originX: originX,
+ u_originY: originY,
+ u_worldWidth: worldWidth,
+ u_worldHeight: worldHeight,
+ } satisfies TartanUniforms;
+
+ return ;
+}, colorPropsAreEqual);
diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts
index baa8fa376..a325ec6cb 100644
--- a/packages/shaders/src/index.ts
+++ b/packages/shaders/src/index.ts
@@ -171,6 +171,9 @@ export {
type StaticRadialGradientUniforms,
} from './shaders/static-radial-gradient.js';
+// ----- Tartan ----- //
+export { tartanMeta, tartanFragmentShader, type TartanParams, type TartanUniforms } from './shaders/tartan.js';
+
// ----- Utils ----- //
export { getShaderColorFromString } from './get-shader-color-from-string.js';
export { getShaderNoiseTexture } from './get-shader-noise-texture.js';
diff --git a/packages/shaders/src/shaders/tartan.ts b/packages/shaders/src/shaders/tartan.ts
new file mode 100644
index 000000000..7b34d14ce
--- /dev/null
+++ b/packages/shaders/src/shaders/tartan.ts
@@ -0,0 +1,86 @@
+import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js';
+import type { vec4 } from '../types.js';
+
+export const tartanMeta = {
+ maxStripeCount: 9,
+} as const;
+
+/**
+ * Tartan patterns
+ *
+ * Uniforms:
+ * - u_stripeCount: number of stripes in the pattern (float used as integer)
+ * - u_stripeColors (vec4[])
+ * - u_stripeWidths (mat3)
+ *
+ */
+
+// language=GLSL
+export const tartanFragmentShader: string = `#version 300 es
+precision mediump float;
+
+uniform float u_stripeCount;
+uniform vec4[${tartanMeta.maxStripeCount}] u_stripeColors;
+uniform mat3 u_stripeWidths;
+
+${sizingVariablesDeclaration}
+
+out vec4 fragColor;
+
+void main() {
+ vec2 uv = v_patternUV * 100.0;
+
+ float[${tartanMeta.maxStripeCount}] cumulativeWidths;
+
+ float totalWidth = 0.0;
+
+ for (int i = 0; i < ${tartanMeta.maxStripeCount}; i++) {
+ if (i >= int(u_stripeCount)) break;
+ float width = float(u_stripeWidths[int(i / 3)][int(i % 3)]);
+ cumulativeWidths[i] = (i == 0 ? 0.0 : cumulativeWidths[i - 1]) + width;
+ totalWidth += width;
+ }
+
+ vec2 cell = mod(
+ uv.xy,
+ totalWidth * 2.0
+ ) - totalWidth;
+
+ // Color of vertical stripe.
+ vec4 verticalColor;
+ for (int i = 0; i < ${tartanMeta.maxStripeCount}; i++) {
+ if (i >= int(u_stripeCount)) break;
+ verticalColor = u_stripeColors[i];
+ if (abs(cell.x) < cumulativeWidths[i]) {
+ break;
+ }
+ }
+
+ // Color of horizontal stripe.
+ vec4 horizontalColor;
+ for (int i = 0; i < ${tartanMeta.maxStripeCount}; i++) {
+ if (i >= int(u_stripeCount)) break;
+ horizontalColor = u_stripeColors[i];
+ if (abs(cell.y) < cumulativeWidths[i]) {
+ break;
+ }
+ }
+
+ // Weave pattern.
+ // See: https://en.wikipedia.org/wiki/Tartan#Weaving_construction
+ float a = mod(uv.x + mod(floor(uv.y), 4.0), 4.0) / 4.0;
+
+ fragColor = a < 0.5 ? verticalColor : horizontalColor;
+}
+`;
+
+export interface TartanUniforms extends ShaderSizingUniforms {
+ u_stripeColors: vec4[];
+ u_stripeWidths: number[];
+ u_stripeCount: number;
+}
+
+export interface TartanParams extends ShaderSizingParams {
+ stripeColors?: string[];
+ stripeWidths?: number[];
+}
From 599c963a526e20f85fe15d17f42d9c73838fb10c Mon Sep 17 00:00:00 2001
From: Haydn Ewers <27211+haydn@users.noreply.github.com>
Date: Fri, 1 Aug 2025 16:55:10 +1000
Subject: [PATCH 2/5] add weave texture to tartan shader
---
docs/src/app/tartan/page.tsx | 22 +++++++
packages/shaders-react/src/shaders/tartan.tsx | 12 +++-
packages/shaders/src/shaders/tartan.ts | 66 ++++++++++++++-----
3 files changed, 81 insertions(+), 19 deletions(-)
diff --git a/docs/src/app/tartan/page.tsx b/docs/src/app/tartan/page.tsx
index a11481976..47fdcae47 100644
--- a/docs/src/app/tartan/page.tsx
+++ b/docs/src/app/tartan/page.tsx
@@ -74,6 +74,28 @@ const TartanWithControls = () => {
const [params, setParams] = useControls(() => {
return {
+ Parameters: folder(
+ {
+ weaveSize: {
+ value: defaults.weaveSize,
+ min: 1.0,
+ max: 10.0,
+ step: 0.25,
+ order: 0,
+ },
+ weaveStrength: {
+ value: defaults.weaveStrength,
+ min: 0.0,
+ max: 1.0,
+ step: 0.05,
+ order: 1,
+ },
+ },
+ {
+ order: 0,
+ collapsed: false,
+ }
+ ),
Transform: folder(
{
scale: { value: defaults.scale, min: 0.01, max: 4, order: 400 },
diff --git a/packages/shaders-react/src/shaders/tartan.tsx b/packages/shaders-react/src/shaders/tartan.tsx
index 7acdfcbed..1031b0bf0 100644
--- a/packages/shaders-react/src/shaders/tartan.tsx
+++ b/packages/shaders-react/src/shaders/tartan.tsx
@@ -1,13 +1,13 @@
-import { memo } from 'react';
import {
defaultPatternSizing,
getShaderColorFromString,
ShaderFitOptions,
type ShaderPreset,
- tartanFragmentShader,
type TartanParams,
type TartanUniforms,
+ tartanFragmentShader,
} from '@paper-design/shaders';
+import { memo } from 'react';
import { colorPropsAreEqual } from '../color-props-are-equal.js';
import { type ShaderComponentProps, ShaderMount } from '../shader-mount.js';
@@ -19,8 +19,10 @@ export const defaultPreset: TartanPreset = {
name: 'Default',
params: {
...defaultPatternSizing,
- stripeColors: ['#19600b', '#aa0909', '#19600b', '#041a07', '#c3a855', '#041a07'],
+ stripeColors: ['#19600b', '#aa0909', '#19600b', '#083a0f', '#c3a855', '#083a0f'],
stripeWidths: [30, 4, 40, 30, 1, 30],
+ weaveSize: 3.0,
+ weaveStrength: 0.25,
},
};
@@ -30,6 +32,8 @@ export const Tartan: React.FC = memo(function TartanImpl({
// Own props
stripeColors = defaultPreset.params.stripeColors,
stripeWidths = defaultPreset.params.stripeWidths,
+ weaveSize = defaultPreset.params.weaveSize,
+ weaveStrength = defaultPreset.params.weaveStrength,
// Sizing props
fit = defaultPreset.params.fit,
@@ -48,6 +52,8 @@ export const Tartan: React.FC = memo(function TartanImpl({
u_stripeColors: stripeColors.map(getShaderColorFromString),
u_stripeWidths: stripeWidths,
u_stripeCount: stripeColors.length,
+ u_weaveSize: weaveSize,
+ u_weaveStrength: weaveStrength,
// Sizing uniforms
u_fit: ShaderFitOptions[fit],
diff --git a/packages/shaders/src/shaders/tartan.ts b/packages/shaders/src/shaders/tartan.ts
index 7b34d14ce..48f40e04c 100644
--- a/packages/shaders/src/shaders/tartan.ts
+++ b/packages/shaders/src/shaders/tartan.ts
@@ -10,8 +10,10 @@ export const tartanMeta = {
*
* Uniforms:
* - u_stripeCount: number of stripes in the pattern (float used as integer)
- * - u_stripeColors (vec4[])
- * - u_stripeWidths (mat3)
+ * - u_stripeColors: array of stripe colors (vec4[])
+ * - u_stripeWidths: array of stripe widths (mat3 used as an array)
+ * - u_weaveSize: width of thread used in the weave texture (float)
+ * - u_weaveStrength: strength of weave texture (float)
*
*/
@@ -22,13 +24,27 @@ precision mediump float;
uniform float u_stripeCount;
uniform vec4[${tartanMeta.maxStripeCount}] u_stripeColors;
uniform mat3 u_stripeWidths;
+uniform float u_weaveSize;
+uniform float u_weaveStrength;
${sizingVariablesDeclaration}
out vec4 fragColor;
void main() {
- vec2 uv = v_patternUV * 100.0;
+ vec2 uv = (v_patternUV * 100.0) / u_weaveSize;
+
+ vec2 weave = mod(
+ vec2(
+ uv.x + floor(mod(uv.y, 4.0)),
+ uv.y + floor(mod(uv.x, 4.0)) - 2.0
+ ),
+ 4.0
+ );
+
+ // Color
+
+ vec4 verticalColor, horizontalColor;
float[${tartanMeta.maxStripeCount}] cumulativeWidths;
@@ -41,36 +57,50 @@ void main() {
totalWidth += width;
}
- vec2 cell = mod(
- uv.xy,
- totalWidth * 2.0
+ vec2 stripe = mod(
+ uv,
+ totalWidth * 2.0
) - totalWidth;
- // Color of vertical stripe.
- vec4 verticalColor;
for (int i = 0; i < ${tartanMeta.maxStripeCount}; i++) {
if (i >= int(u_stripeCount)) break;
verticalColor = u_stripeColors[i];
- if (abs(cell.x) < cumulativeWidths[i]) {
+ if (abs(stripe.x) < cumulativeWidths[i]) {
break;
}
}
- // Color of horizontal stripe.
- vec4 horizontalColor;
for (int i = 0; i < ${tartanMeta.maxStripeCount}; i++) {
if (i >= int(u_stripeCount)) break;
horizontalColor = u_stripeColors[i];
- if (abs(cell.y) < cumulativeWidths[i]) {
+ if (abs(stripe.y) < cumulativeWidths[i]) {
break;
}
}
- // Weave pattern.
- // See: https://en.wikipedia.org/wiki/Tartan#Weaving_construction
- float a = mod(uv.x + mod(floor(uv.y), 4.0), 4.0) / 4.0;
+ fragColor = mix(
+ verticalColor,
+ horizontalColor,
+ 1.0 - step(2.0, weave.x)
+ );
+
+ // Texture
+
+ vec2 brightness = vec2(0.0);
+
+ brightness += smoothstep(0.0, 0.5, weave);
+ brightness -= smoothstep(1.5, 2.0, weave);
+
+ brightness += smoothstep(2.0, 2.25, weave);
+ brightness -= smoothstep(2.75, 3.0, weave);
+
+ brightness += smoothstep(3.0, 3.25, weave);
+ brightness -= smoothstep(3.75, 4.0, weave);
+
+ brightness *= u_weaveStrength;
+ brightness += 1.0 - u_weaveStrength;
- fragColor = a < 0.5 ? verticalColor : horizontalColor;
+ fragColor = mix(vec4(0.0, 0.0, 0.0, 1.0), fragColor, brightness.x * brightness.y);
}
`;
@@ -78,9 +108,13 @@ export interface TartanUniforms extends ShaderSizingUniforms {
u_stripeColors: vec4[];
u_stripeWidths: number[];
u_stripeCount: number;
+ u_weaveSize: number;
+ u_weaveStrength: number;
}
export interface TartanParams extends ShaderSizingParams {
stripeColors?: string[];
stripeWidths?: number[];
+ weaveSize?: number;
+ weaveStrength?: number;
}
From 06878d4f7a0b421b12dbeaae267360f60ce2fc52 Mon Sep 17 00:00:00 2001
From: Haydn Ewers <27211+haydn@users.noreply.github.com>
Date: Mon, 4 Aug 2025 18:23:12 +1000
Subject: [PATCH 3/5] add stripeCount param to tartan shader
---
packages/shaders-react/src/shaders/tartan.tsx | 6 ++++--
packages/shaders/src/shaders/tartan.ts | 1 +
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/packages/shaders-react/src/shaders/tartan.tsx b/packages/shaders-react/src/shaders/tartan.tsx
index 1031b0bf0..ac705fdf4 100644
--- a/packages/shaders-react/src/shaders/tartan.tsx
+++ b/packages/shaders-react/src/shaders/tartan.tsx
@@ -19,8 +19,9 @@ export const defaultPreset: TartanPreset = {
name: 'Default',
params: {
...defaultPatternSizing,
+ stripeCount: 6,
stripeColors: ['#19600b', '#aa0909', '#19600b', '#083a0f', '#c3a855', '#083a0f'],
- stripeWidths: [30, 4, 40, 30, 1, 30],
+ stripeWidths: [15, 2, 20, 15, 1, 15],
weaveSize: 3.0,
weaveStrength: 0.25,
},
@@ -30,6 +31,7 @@ export const tartanPresets: TartanPreset[] = [defaultPreset];
export const Tartan: React.FC = memo(function TartanImpl({
// Own props
+ stripeCount = defaultPreset.params.stripeCount,
stripeColors = defaultPreset.params.stripeColors,
stripeWidths = defaultPreset.params.stripeWidths,
weaveSize = defaultPreset.params.weaveSize,
@@ -49,9 +51,9 @@ export const Tartan: React.FC = memo(function TartanImpl({
}: TartanProps) {
const uniforms = {
// Own uniforms
+ u_stripeCount: stripeCount,
u_stripeColors: stripeColors.map(getShaderColorFromString),
u_stripeWidths: stripeWidths,
- u_stripeCount: stripeColors.length,
u_weaveSize: weaveSize,
u_weaveStrength: weaveStrength,
diff --git a/packages/shaders/src/shaders/tartan.ts b/packages/shaders/src/shaders/tartan.ts
index 48f40e04c..52cc4fc9e 100644
--- a/packages/shaders/src/shaders/tartan.ts
+++ b/packages/shaders/src/shaders/tartan.ts
@@ -113,6 +113,7 @@ export interface TartanUniforms extends ShaderSizingUniforms {
}
export interface TartanParams extends ShaderSizingParams {
+ stripeCount?: number;
stripeColors?: string[];
stripeWidths?: number[];
weaveSize?: number;
From c24d652d23a0a4746731e756861ff694acbdedcc Mon Sep 17 00:00:00 2001
From: Haydn Ewers <27211+haydn@users.noreply.github.com>
Date: Mon, 4 Aug 2025 18:24:43 +1000
Subject: [PATCH 4/5] fix issues with leva controls on tartan example page
---
docs/src/app/tartan/page.tsx | 265 ++++++++++---------
docs/src/helpers/create-numbered-object.ts | 36 +++
docs/src/helpers/get-values-sorted-by-key.ts | 12 +
3 files changed, 183 insertions(+), 130 deletions(-)
create mode 100644 docs/src/helpers/create-numbered-object.ts
create mode 100644 docs/src/helpers/get-values-sorted-by-key.ts
diff --git a/docs/src/app/tartan/page.tsx b/docs/src/app/tartan/page.tsx
index 47fdcae47..e8d9b2825 100644
--- a/docs/src/app/tartan/page.tsx
+++ b/docs/src/app/tartan/page.tsx
@@ -1,157 +1,162 @@
'use client';
import { BackButton } from '@/components/back-button';
-import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params';
-import { toHsla } from '@/helpers/to-hsla';
-import { usePresetHighlight } from '@/helpers/use-preset-highlight';
-import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params';
+import { createNumberedObject } from '@/helpers/create-numbered-object';
+import { getValuesSortedByKey } from '@/helpers/get-values-sorted-by-key';
import { type ShaderFit, ShaderFitOptions, tartanMeta } from '@paper-design/shaders';
import { Tartan, tartanPresets } from '@paper-design/shaders-react';
-import { button, folder, useControls } from 'leva';
+import { button, folder, levaStore, useControls } from 'leva';
+import type { Schema } from 'leva/dist/declarations/src/types';
import Link from 'next/link';
+import { useEffect } from 'react';
-/**
- * You can copy/paste this example to use Tartan in your app
- */
-const TartanExample = () => {
- return ;
-};
+const defaults = tartanPresets[0].params;
/**
* This example has controls added so you can play with settings in the example app
*/
-
-const defaults = tartanPresets[0].params;
-
const TartanWithControls = () => {
- const [{ count: stripeCount }, setStripeCount] = useControls(() => ({
+ // Presets
+ useControls({
+ Presets: folder(
+ Object.fromEntries(
+ tartanPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [
+ name,
+ button(() => {
+ const { stripeColors, stripeWidths, ...presetParams } = preset;
+ setParams(presetParams);
+ setColors(
+ createNumberedObject('color', tartanMeta.maxStripeCount, (i) => stripeColors[i % stripeColors.length])
+ );
+ setWidths(
+ createNumberedObject('width', tartanMeta.maxStripeCount, (i) => stripeWidths[i % stripeWidths.length])
+ );
+ }),
+ ])
+ ),
+ {
+ order: -1,
+ collapsed: false,
+ }
+ ),
+ });
+
+ // Scalar parameters
+ const [params, setParams] = useControls(() => ({
+ Parameters: folder(
+ {
+ weaveSize: {
+ value: defaults.weaveSize,
+ min: 1.0,
+ max: 10.0,
+ step: 0.25,
+ order: 0,
+ },
+ weaveStrength: {
+ value: defaults.weaveStrength,
+ min: 0.0,
+ max: 1.0,
+ step: 0.05,
+ order: 1,
+ },
+ },
+ {
+ order: 0,
+ collapsed: false,
+ }
+ ),
Stripes: folder(
{
- count: {
- value: defaults.stripeColors.length,
+ stripeCount: {
+ value: defaults.stripeCount,
min: 2,
max: tartanMeta.maxStripeCount,
step: 1,
order: 0,
+ label: 'count',
},
},
- { order: 1 }
+ {
+ order: 1,
+ collapsed: false,
+ }
+ ),
+ Transform: folder(
+ {
+ scale: { value: defaults.scale, min: 0.01, max: 4, order: 400 },
+ rotation: { value: defaults.rotation, min: 0, max: 360, order: 401 },
+ offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 402 },
+ offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 403 },
+ },
+ {
+ order: 2,
+ collapsed: false,
+ }
+ ),
+ Fit: folder(
+ {
+ fit: { value: defaults.fit, options: Object.keys(ShaderFitOptions) as ShaderFit[], order: 404 },
+ worldWidth: { value: 1000, min: 0, max: 5120, order: 405 },
+ worldHeight: { value: 500, min: 0, max: 5120, order: 406 },
+ originX: { value: defaults.originX, min: 0, max: 1, order: 407 },
+ originY: { value: defaults.originY, min: 0, max: 1, order: 408 },
+ },
+ {
+ order: 3,
+ collapsed: true,
+ }
),
}));
- const [colors, setColors] = useControls(() => {
- const stripe: Record = {};
-
- for (let i = 0; i < stripeCount; i++) {
- stripe[`color${i + 1}`] = {
- value: defaults.stripeColors[i] ? toHsla(defaults.stripeColors[i]) : `hsla(${(40 * i) % 360}, 60%, 50%, 1)`,
- order: 1 + i * 2,
- };
- }
-
- return {
- Stripes: folder(stripe),
- };
- }, [stripeCount]);
-
- const [widths, setWidths] = useControls(() => {
- const stripe: Record = {};
-
- for (let i = 0; i < stripeCount; i++) {
- stripe[`width${i + 1}`] = {
- value: defaults.stripeWidths[i],
- min: 1,
- max: 400,
- step: 1,
- order: 1 + i * 2 + 1,
- };
- }
-
- return {
- Stripes: folder(stripe),
- };
- }, [stripeCount]);
+ // Stripe colors
+ const [colors, setColors] = useControls(
+ () => ({
+ Stripes: folder({
+ ...createNumberedObject(
+ 'color',
+ tartanMeta.maxStripeCount,
+ (i) =>
+ ({
+ label: `color${i + 1}`,
+ order: i * 2 + 1,
+ render: () => params.stripeCount > i,
+ value: defaults.stripeColors[i % defaults.stripeColors.length],
+ }) satisfies Schema[string]
+ ),
+ }),
+ }),
+ [params.stripeCount]
+ );
- const [params, setParams] = useControls(() => {
- return {
- Parameters: folder(
- {
- weaveSize: {
- value: defaults.weaveSize,
- min: 1.0,
- max: 10.0,
- step: 0.25,
- order: 0,
- },
- weaveStrength: {
- value: defaults.weaveStrength,
- min: 0.0,
- max: 1.0,
- step: 0.05,
- order: 1,
- },
- },
- {
- order: 0,
- collapsed: false,
- }
- ),
- Transform: folder(
- {
- scale: { value: defaults.scale, min: 0.01, max: 4, order: 400 },
- rotation: { value: defaults.rotation, min: 0, max: 360, order: 401 },
- offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 402 },
- offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 403 },
- },
- {
- order: 2,
- collapsed: false,
- }
- ),
- Fit: folder(
- {
- fit: { value: defaults.fit, options: Object.keys(ShaderFitOptions) as ShaderFit[], order: 404 },
- worldWidth: { value: 1000, min: 0, max: 5120, order: 405 },
- worldHeight: { value: 500, min: 0, max: 5120, order: 406 },
- originX: { value: defaults.originX, min: 0, max: 1, order: 407 },
- originY: { value: defaults.originY, min: 0, max: 1, order: 408 },
- },
- {
- order: 3,
- collapsed: true,
- }
- ),
- };
- });
+ // Stripe widths
+ const [widths, setWidths] = useControls(
+ () => ({
+ Stripes: folder({
+ ...createNumberedObject(
+ 'width',
+ tartanMeta.maxStripeCount,
+ (i) =>
+ ({
+ label: `width${i + 1}`,
+ max: 100,
+ min: 1,
+ order: i * 2 + 2,
+ render: () => params.stripeCount > i,
+ step: 1,
+ value: defaults.stripeWidths[i % defaults.stripeWidths.length],
+ }) satisfies Schema[string]
+ ),
+ }),
+ }),
+ [params.stripeCount]
+ );
- useControls(() => {
- const presets = Object.fromEntries(
- tartanPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [
- name,
- button(() => {
- const { stripeColors, stripeWidths, ...presetParams } = preset;
- setStripeCount({ count: stripeColors.length });
- setColors(
- Object.fromEntries(stripeColors.map((value, i) => [`color${i + 1}`, toHsla(value)])) as unknown as Record<
- string,
- { value: string; [key: string]: unknown }
- >
- );
- setWidths(Object.fromEntries(stripeWidths.map((value, i) => [`width${i + 1}`, value])));
- setParamsSafe(params, setParams, presetParams);
- }),
- ])
- );
- return {
- Presets: folder(presets, { order: -1 }),
+ // Clear the Leva store when the component unmounts.
+ useEffect(() => {
+ return () => {
+ levaStore.dispose();
};
- });
-
- // Reset to defaults on mount, so that Leva doesn't show values from other
- // shaders when navigating (if two shaders have a color1 param for example)
- useResetLevaParams(params, setParams, defaults);
- usePresetHighlight(tartanPresets, params);
- cleanUpLevaParams(params);
+ }, []);
return (
<>
@@ -160,8 +165,8 @@ const TartanWithControls = () => {
}
- stripeWidths={[...Object.values(widths), ...Array(9 - stripeCount).fill(0)]}
+ stripeColors={getValuesSortedByKey(colors)}
+ stripeWidths={getValuesSortedByKey(widths)}
className="fixed size-full"
/>
>
diff --git a/docs/src/helpers/create-numbered-object.ts b/docs/src/helpers/create-numbered-object.ts
new file mode 100644
index 000000000..fa858698e
--- /dev/null
+++ b/docs/src/helpers/create-numbered-object.ts
@@ -0,0 +1,36 @@
+/**
+ * Creates an object with up to 9 properties, each named using a prefix and a number.
+ *
+ * @example
+ * const result = createNumberedObject('foo', 3, i => `bar${i + 1}`);
+ * console.log(result); // { foo1: 'bar1', foo2: 'bar2', foo3: 'bar3' }
+ */
+export const createNumberedObject = (
+ prefix: Prefix,
+ count: Count,
+ mapFn: (i: number) => Value
+) => {
+ const result = new Map();
+ for (let i = 0; i < count; i++) result.set(`${prefix}${i + 1}`, mapFn(i));
+ return Object.fromEntries(result) as Record<`${Prefix}${Range}`, Value>;
+};
+
+type Range = Count extends 1
+ ? 1
+ : Count extends 2
+ ? 1 | 2
+ : Count extends 3
+ ? 1 | 2 | 3
+ : Count extends 4
+ ? 1 | 2 | 3 | 4
+ : Count extends 5
+ ? 1 | 2 | 3 | 4 | 5
+ : Count extends 6
+ ? 1 | 2 | 3 | 4 | 5 | 6
+ : Count extends 7
+ ? 1 | 2 | 3 | 4 | 5 | 6 | 7
+ : Count extends 8
+ ? 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
+ : Count extends 9
+ ? 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
+ : never;
diff --git a/docs/src/helpers/get-values-sorted-by-key.ts b/docs/src/helpers/get-values-sorted-by-key.ts
new file mode 100644
index 000000000..5665fc117
--- /dev/null
+++ b/docs/src/helpers/get-values-sorted-by-key.ts
@@ -0,0 +1,12 @@
+/**
+ * Returns an array of values from an object ordered by their keys.
+ *
+ * @example
+ * const obj = { foo2: 'dog', foo1: 'pig', foo3: 'cat' };
+ * const results = getValuesFromNumberedObject(obj);
+ * console.log(results); // ['pig', 'dog', 'cat']
+ */
+export const getValuesSortedByKey = (obj: T): Array =>
+ Object.entries(obj)
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([, value]) => value);
From 7e95f0ae1440c504916b02502bfd2f139e419bf5 Mon Sep 17 00:00:00 2001
From: Haydn Ewers <27211+haydn@users.noreply.github.com>
Date: Mon, 4 Aug 2025 18:25:05 +1000
Subject: [PATCH 5/5] add 'Colorful' preset to tartan shader
---
packages/shaders-react/src/shaders/tartan.tsx | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/packages/shaders-react/src/shaders/tartan.tsx b/packages/shaders-react/src/shaders/tartan.tsx
index ac705fdf4..b01ddca4b 100644
--- a/packages/shaders-react/src/shaders/tartan.tsx
+++ b/packages/shaders-react/src/shaders/tartan.tsx
@@ -27,7 +27,19 @@ export const defaultPreset: TartanPreset = {
},
};
-export const tartanPresets: TartanPreset[] = [defaultPreset];
+export const colorfulPreset: TartanPreset = {
+ name: 'Colorful',
+ params: {
+ ...defaultPatternSizing,
+ stripeCount: 9,
+ stripeColors: ['#cc3333', '#cc9933', '#99cc33', '#33cc33', '#33cc99', '#3399cc', '#3333cc', '#9933cc', '#cc3399'],
+ stripeWidths: [1, 2, 2, 2, 2, 2, 2, 2, 1],
+ weaveSize: 6.0,
+ weaveStrength: 0.25,
+ },
+};
+
+export const tartanPresets: TartanPreset[] = [defaultPreset, colorfulPreset];
export const Tartan: React.FC = memo(function TartanImpl({
// Own props