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