Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
]
}
5 changes: 5 additions & 0 deletions docs/registry/tartan-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Tartan, type TartanProps } from '@paper-design/shaders-react';

export function TartanExample(props: TartanProps) {
return <Tartan style={{ position: 'fixed', width: '100%', height: '100%' }} {...props} />;
}
9 changes: 9 additions & 0 deletions docs/src/app/tartan/layout.tsx
Original file line number Diff line number Diff line change
@@ -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}</>;
}
176 changes: 176 additions & 0 deletions docs/src/app/tartan/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
'use client';

import { BackButton } from '@/components/back-button';
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, levaStore, useControls } from 'leva';
import type { Schema } from 'leva/dist/declarations/src/types';
import Link from 'next/link';
import { useEffect } from 'react';

const defaults = tartanPresets[0].params;

/**
* This example has controls added so you can play with settings in the example app
*/
const TartanWithControls = () => {
// 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(
{
stripeCount: {
value: defaults.stripeCount,
min: 2,
max: tartanMeta.maxStripeCount,
step: 1,
order: 0,
label: 'count',
},
},
{
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,
}
),
}));

// 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]
);

// 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]
);

// Clear the Leva store when the component unmounts.
useEffect(() => {
return () => {
levaStore.dispose();
};
}, []);

return (
<>
<Link href="/">
<BackButton />
</Link>
<Tartan
{...params}
stripeColors={getValuesSortedByKey(colors)}
stripeWidths={getValuesSortedByKey(widths)}
className="fixed size-full"
/>
</>
);
};

export default TartanWithControls;
36 changes: 36 additions & 0 deletions docs/src/helpers/create-numbered-object.ts
Original file line number Diff line number Diff line change
@@ -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 extends string, Count extends 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9, Value>(
prefix: Prefix,
count: Count,
mapFn: (i: number) => Value
) => {
const result = new Map<string, Value>();
for (let i = 0; i < count; i++) result.set(`${prefix}${i + 1}`, mapFn(i));
return Object.fromEntries(result) as Record<`${Prefix}${Range<Count>}`, Value>;
};

type Range<Count extends 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9> = 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;
12 changes: 12 additions & 0 deletions docs/src/helpers/get-values-sorted-by-key.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends object>(obj: T): Array<T[keyof T]> =>
Object.entries(obj)
.sort(([a], [b]) => a.localeCompare(b))
.map(([, value]) => value);
8 changes: 8 additions & 0 deletions docs/src/home-shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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[];
4 changes: 4 additions & 0 deletions packages/shaders-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
85 changes: 85 additions & 0 deletions packages/shaders-react/src/shaders/tartan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
defaultPatternSizing,
getShaderColorFromString,
ShaderFitOptions,
type ShaderPreset,
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';

export interface TartanProps extends ShaderComponentProps, TartanParams {}

type TartanPreset = ShaderPreset<TartanParams>;

export const defaultPreset: TartanPreset = {
name: 'Default',
params: {
...defaultPatternSizing,
stripeCount: 6,
stripeColors: ['#19600b', '#aa0909', '#19600b', '#083a0f', '#c3a855', '#083a0f'],
stripeWidths: [15, 2, 20, 15, 1, 15],
weaveSize: 3.0,
weaveStrength: 0.25,
},
};

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<TartanProps> = memo(function TartanImpl({
// Own props
stripeCount = defaultPreset.params.stripeCount,
stripeColors = defaultPreset.params.stripeColors,
stripeWidths = defaultPreset.params.stripeWidths,
weaveSize = defaultPreset.params.weaveSize,
weaveStrength = defaultPreset.params.weaveStrength,

// 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_stripeCount: stripeCount,
u_stripeColors: stripeColors.map(getShaderColorFromString),
u_stripeWidths: stripeWidths,
u_weaveSize: weaveSize,
u_weaveStrength: weaveStrength,

// 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 <ShaderMount {...props} fragmentShader={tartanFragmentShader} uniforms={uniforms} />;
}, colorPropsAreEqual);
3 changes: 3 additions & 0 deletions packages/shaders/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading