Skip to content

Commit 97608c6

Browse files
committed
fix issues with leva controls on tartan example page
1 parent 0b735a9 commit 97608c6

File tree

3 files changed

+183
-130
lines changed

3 files changed

+183
-130
lines changed

docs/src/app/tartan/page.tsx

Lines changed: 135 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,157 +1,162 @@
11
'use client';
22

33
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';
4+
import { createNumberedObject } from '@/helpers/create-numbered-object';
5+
import { getValuesSortedByKey } from '@/helpers/get-values-sorted-by-key';
86
import { type ShaderFit, ShaderFitOptions, tartanMeta } from '@paper-design/shaders';
97
import { Tartan, tartanPresets } from '@paper-design/shaders-react';
10-
import { button, folder, useControls } from 'leva';
8+
import { button, folder, levaStore, useControls } from 'leva';
9+
import type { Schema } from 'leva/dist/declarations/src/types';
1110
import Link from 'next/link';
11+
import { useEffect } from 'react';
1212

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-
};
13+
const defaults = tartanPresets[0].params;
1914

2015
/**
2116
* This example has controls added so you can play with settings in the example app
2217
*/
23-
24-
const defaults = tartanPresets[0].params;
25-
2618
const TartanWithControls = () => {
27-
const [{ count: stripeCount }, setStripeCount] = useControls(() => ({
19+
// Presets
20+
useControls({
21+
Presets: folder(
22+
Object.fromEntries(
23+
tartanPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [
24+
name,
25+
button(() => {
26+
const { stripeColors, stripeWidths, ...presetParams } = preset;
27+
setParams(presetParams);
28+
setColors(
29+
createNumberedObject('color', tartanMeta.maxStripeCount, (i) => stripeColors[i % stripeColors.length])
30+
);
31+
setWidths(
32+
createNumberedObject('width', tartanMeta.maxStripeCount, (i) => stripeWidths[i % stripeWidths.length])
33+
);
34+
}),
35+
])
36+
),
37+
{
38+
order: -1,
39+
collapsed: false,
40+
}
41+
),
42+
});
43+
44+
// Scalar parameters
45+
const [params, setParams] = useControls(() => ({
46+
Parameters: folder(
47+
{
48+
weaveSize: {
49+
value: defaults.weaveSize,
50+
min: 1.0,
51+
max: 10.0,
52+
step: 0.25,
53+
order: 0,
54+
},
55+
weaveStrength: {
56+
value: defaults.weaveStrength,
57+
min: 0.0,
58+
max: 1.0,
59+
step: 0.05,
60+
order: 1,
61+
},
62+
},
63+
{
64+
order: 0,
65+
collapsed: false,
66+
}
67+
),
2868
Stripes: folder(
2969
{
30-
count: {
31-
value: defaults.stripeColors.length,
70+
stripeCount: {
71+
value: defaults.stripeCount,
3272
min: 2,
3373
max: tartanMeta.maxStripeCount,
3474
step: 1,
3575
order: 0,
76+
label: 'count',
3677
},
3778
},
38-
{ order: 1 }
79+
{
80+
order: 1,
81+
collapsed: false,
82+
}
83+
),
84+
Transform: folder(
85+
{
86+
scale: { value: defaults.scale, min: 0.01, max: 4, order: 400 },
87+
rotation: { value: defaults.rotation, min: 0, max: 360, order: 401 },
88+
offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 402 },
89+
offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 403 },
90+
},
91+
{
92+
order: 2,
93+
collapsed: false,
94+
}
95+
),
96+
Fit: folder(
97+
{
98+
fit: { value: defaults.fit, options: Object.keys(ShaderFitOptions) as ShaderFit[], order: 404 },
99+
worldWidth: { value: 1000, min: 0, max: 5120, order: 405 },
100+
worldHeight: { value: 500, min: 0, max: 5120, order: 406 },
101+
originX: { value: defaults.originX, min: 0, max: 1, order: 407 },
102+
originY: { value: defaults.originY, min: 0, max: 1, order: 408 },
103+
},
104+
{
105+
order: 3,
106+
collapsed: true,
107+
}
39108
),
40109
}));
41110

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]);
111+
// Stripe colors
112+
const [colors, setColors] = useControls(
113+
() => ({
114+
Stripes: folder({
115+
...createNumberedObject(
116+
'color',
117+
tartanMeta.maxStripeCount,
118+
(i) =>
119+
({
120+
label: `color${i + 1}`,
121+
order: i * 2 + 1,
122+
render: () => params.stripeCount > i,
123+
value: defaults.stripeColors[i % defaults.stripeColors.length],
124+
}) satisfies Schema[string]
125+
),
126+
}),
127+
}),
128+
[params.stripeCount]
129+
);
74130

75-
const [params, setParams] = useControls(() => {
76-
return {
77-
Parameters: folder(
78-
{
79-
weaveSize: {
80-
value: defaults.weaveSize,
81-
min: 1.0,
82-
max: 10.0,
83-
step: 0.25,
84-
order: 0,
85-
},
86-
weaveStrength: {
87-
value: defaults.weaveStrength,
88-
min: 0.0,
89-
max: 1.0,
90-
step: 0.05,
91-
order: 1,
92-
},
93-
},
94-
{
95-
order: 0,
96-
collapsed: false,
97-
}
98-
),
99-
Transform: folder(
100-
{
101-
scale: { value: defaults.scale, min: 0.01, max: 4, order: 400 },
102-
rotation: { value: defaults.rotation, min: 0, max: 360, order: 401 },
103-
offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 402 },
104-
offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 403 },
105-
},
106-
{
107-
order: 2,
108-
collapsed: false,
109-
}
110-
),
111-
Fit: folder(
112-
{
113-
fit: { value: defaults.fit, options: Object.keys(ShaderFitOptions) as ShaderFit[], order: 404 },
114-
worldWidth: { value: 1000, min: 0, max: 5120, order: 405 },
115-
worldHeight: { value: 500, min: 0, max: 5120, order: 406 },
116-
originX: { value: defaults.originX, min: 0, max: 1, order: 407 },
117-
originY: { value: defaults.originY, min: 0, max: 1, order: 408 },
118-
},
119-
{
120-
order: 3,
121-
collapsed: true,
122-
}
123-
),
124-
};
125-
});
131+
// Stripe widths
132+
const [widths, setWidths] = useControls(
133+
() => ({
134+
Stripes: folder({
135+
...createNumberedObject(
136+
'width',
137+
tartanMeta.maxStripeCount,
138+
(i) =>
139+
({
140+
label: `width${i + 1}`,
141+
max: 100,
142+
min: 1,
143+
order: i * 2 + 2,
144+
render: () => params.stripeCount > i,
145+
step: 1,
146+
value: defaults.stripeWidths[i % defaults.stripeWidths.length],
147+
}) satisfies Schema[string]
148+
),
149+
}),
150+
}),
151+
[params.stripeCount]
152+
);
126153

127-
useControls(() => {
128-
const presets = Object.fromEntries(
129-
tartanPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [
130-
name,
131-
button(() => {
132-
const { stripeColors, stripeWidths, ...presetParams } = preset;
133-
setStripeCount({ count: stripeColors.length });
134-
setColors(
135-
Object.fromEntries(stripeColors.map((value, i) => [`color${i + 1}`, toHsla(value)])) as unknown as Record<
136-
string,
137-
{ value: string; [key: string]: unknown }
138-
>
139-
);
140-
setWidths(Object.fromEntries(stripeWidths.map((value, i) => [`width${i + 1}`, value])));
141-
setParamsSafe(params, setParams, presetParams);
142-
}),
143-
])
144-
);
145-
return {
146-
Presets: folder(presets, { order: -1 }),
154+
// Clear the Leva store when the component unmounts.
155+
useEffect(() => {
156+
return () => {
157+
levaStore.dispose();
147158
};
148-
});
149-
150-
// Reset to defaults on mount, so that Leva doesn't show values from other
151-
// shaders when navigating (if two shaders have a color1 param for example)
152-
useResetLevaParams(params, setParams, defaults);
153-
usePresetHighlight(tartanPresets, params);
154-
cleanUpLevaParams(params);
159+
}, []);
155160

156161
return (
157162
<>
@@ -160,8 +165,8 @@ const TartanWithControls = () => {
160165
</Link>
161166
<Tartan
162167
{...params}
163-
stripeColors={Object.values(colors) as unknown as Array<string>}
164-
stripeWidths={[...Object.values(widths), ...Array(9 - stripeCount).fill(0)]}
168+
stripeColors={getValuesSortedByKey(colors)}
169+
stripeWidths={getValuesSortedByKey(widths)}
165170
className="fixed size-full"
166171
/>
167172
</>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Creates an object with up to 9 properties, each named using a prefix and a number.
3+
*
4+
* @example
5+
* const result = createNumberedObject('foo', 3, i => `bar${i + 1}`);
6+
* console.log(result); // { foo1: 'bar1', foo2: 'bar2', foo3: 'bar3' }
7+
*/
8+
export const createNumberedObject = <Prefix extends string, Count extends 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9, Value>(
9+
prefix: Prefix,
10+
count: Count,
11+
mapFn: (i: number) => Value
12+
) => {
13+
const result = new Map<string, Value>();
14+
for (let i = 0; i < count; i++) result.set(`${prefix}${i + 1}`, mapFn(i));
15+
return Object.fromEntries(result) as Record<`${Prefix}${Range<Count>}`, Value>;
16+
};
17+
18+
type Range<Count extends 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9> = Count extends 1
19+
? 1
20+
: Count extends 2
21+
? 1 | 2
22+
: Count extends 3
23+
? 1 | 2 | 3
24+
: Count extends 4
25+
? 1 | 2 | 3 | 4
26+
: Count extends 5
27+
? 1 | 2 | 3 | 4 | 5
28+
: Count extends 6
29+
? 1 | 2 | 3 | 4 | 5 | 6
30+
: Count extends 7
31+
? 1 | 2 | 3 | 4 | 5 | 6 | 7
32+
: Count extends 8
33+
? 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
34+
: Count extends 9
35+
? 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
36+
: never;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Returns an array of values from an object ordered by their keys.
3+
*
4+
* @example
5+
* const obj = { foo2: 'dog', foo1: 'pig', foo3: 'cat' };
6+
* const results = getValuesFromNumberedObject(obj);
7+
* console.log(results); // ['pig', 'dog', 'cat']
8+
*/
9+
export const getValuesSortedByKey = <T extends object>(obj: T): Array<T[keyof T]> =>
10+
Object.entries(obj)
11+
.sort(([a], [b]) => a.localeCompare(b))
12+
.map(([, value]) => value);

0 commit comments

Comments
 (0)