mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
feat(studio): linen and paper grain, two quiet textures among the patterns
This commit is contained in:
@@ -46,6 +46,11 @@ interface PatternGeometry {
|
||||
const r2 = (v: number) => Math.round(v * 100) / 100 || 0;
|
||||
const f = (v: number) => String(r2(v));
|
||||
|
||||
/** Threads per cell each way in linen: fine enough to read as cloth, few enough for a whole page under the cap. */
|
||||
const LINEN_THREADS = 6;
|
||||
/** Specks per cell in grain. */
|
||||
const GRAIN_SPECKS = 3;
|
||||
|
||||
/**
|
||||
* How many primitives a repeat needs over a region: `area` primitives per
|
||||
* cell squared, `lin` per cell along the width and the height. Read before
|
||||
@@ -66,6 +71,8 @@ const DENSITY: Record<BookPattern, { area: number; lin: number }> = {
|
||||
scales: { area: 2, lin: 0 },
|
||||
sunburst: { area: 0, lin: 0 },
|
||||
confetti: { area: 1.2, lin: 0 },
|
||||
linen: { area: 0, lin: 2 * LINEN_THREADS },
|
||||
grain: { area: GRAIN_SPECKS, lin: 0 },
|
||||
};
|
||||
|
||||
/** A primitive count estimate for a repeat over a region at a cell size. */
|
||||
@@ -269,6 +276,24 @@ const DRAW: Record<Exclude<BookPattern, 'sunburst'>, Draw> = {
|
||||
]);
|
||||
}
|
||||
},
|
||||
// Threads of uneven weight both ways, on two half-clear layers: where they cross the cloth reads denser.
|
||||
linen: (pen, g, s, alt) => {
|
||||
const rand = mulberry32(Math.round(s * 1000) + 31);
|
||||
const pitch = s / LINEN_THREADS;
|
||||
for (const y of steps(g.y0, g.y1, pitch)) pen.rect(g.x0, y, g.x1 - g.x0, pitch * (0.2 + rand() * 0.35));
|
||||
for (const x of steps(g.x0, g.x1, pitch)) alt.rect(x, g.y0, pitch * (0.2 + rand() * 0.35), g.y1 - g.y0);
|
||||
},
|
||||
// Specks of a few sizes, the larger ones fainter, scattered the same way on every render.
|
||||
grain: (pen, g, s, alt) => {
|
||||
const rand = mulberry32(Math.round(s * 1000) + 43);
|
||||
const n = Math.round((((g.x1 - g.x0) * (g.y1 - g.y0)) / (s * s)) * GRAIN_SPECKS);
|
||||
for (let i = 0; i < n && !pen.full && !alt.full; i++) {
|
||||
const x = g.x0 + rand() * (g.x1 - g.x0);
|
||||
const y = g.y0 + rand() * (g.y1 - g.y0);
|
||||
if (rand() < 0.6) pen.circle(x, y, s * (0.012 + rand() * 0.012));
|
||||
else alt.circle(x, y, s * (0.025 + rand() * 0.02));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/** A seeded generator, so a confetti page draws the same scatter on every render and in print. */
|
||||
@@ -307,6 +332,13 @@ function sunburst(pen: Pen, box: PatternBox, cell: number, angle: number) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The repeats drawn on two layers, and how clear each layer is: textures stay quiet over their ground. */
|
||||
const LAYER_OPACITY: Partial<Record<BookPattern, readonly [number, number]>> = {
|
||||
gingham: [0.5, 0.5],
|
||||
linen: [0.32, 0.32],
|
||||
grain: [0.5, 0.28],
|
||||
};
|
||||
|
||||
/**
|
||||
* A repeat over a box. The geometry covers the box however it is turned: the
|
||||
* region drawn is the square around the box's circumscribed circle, and the
|
||||
@@ -338,17 +370,18 @@ export function patternGeometry(
|
||||
return { layers: [{ d: pen.d, opacity: 1 }], count: pen.count, cell };
|
||||
}
|
||||
|
||||
// The second layer only ever holds gingham's cross bands; the cell fitted above keeps both under the cap together.
|
||||
// The second layer holds gingham's and linen's cross bands and grain's larger specks;
|
||||
// the cell fitted above keeps both under the cap together.
|
||||
const alt = new Pen(cap);
|
||||
DRAW[paint.pattern](pen, region, cell, alt);
|
||||
const count = pen.count + alt.count;
|
||||
const layers =
|
||||
paint.pattern === 'gingham'
|
||||
? [
|
||||
{ d: pen.d, opacity: 0.5 },
|
||||
{ d: alt.d, opacity: 0.5 },
|
||||
]
|
||||
: [{ d: pen.d, opacity: 1 }];
|
||||
const two = LAYER_OPACITY[paint.pattern];
|
||||
const layers = two
|
||||
? [
|
||||
{ d: pen.d, opacity: two[0] },
|
||||
{ d: alt.d, opacity: two[1] },
|
||||
]
|
||||
: [{ d: pen.d, opacity: 1 }];
|
||||
return {
|
||||
layers: layers.filter((l) => l.d),
|
||||
count,
|
||||
|
||||
@@ -42,6 +42,8 @@ export const BOOK_PATTERNS = [
|
||||
'scales',
|
||||
'sunburst',
|
||||
'confetti',
|
||||
'linen',
|
||||
'grain',
|
||||
] as const;
|
||||
export type BookPattern = (typeof BOOK_PATTERNS)[number];
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('the paint contract', () => {
|
||||
for (const p of [{ type: 'solid', color: '#abcdef', alpha: 0.4 }, linear, radial, stripes]) {
|
||||
expect(bookPaintSchema.parse(p)).toEqual(p);
|
||||
}
|
||||
expect(BOOK_PATTERNS).toHaveLength(12);
|
||||
expect(BOOK_PATTERNS).toHaveLength(14);
|
||||
for (const pattern of BOOK_PATTERNS) expect(bookPaintSchema.safeParse({ ...stripes, pattern }).success).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -138,4 +138,6 @@ export const BOOK_PATTERN_LINES: Readonly<Record<BookPattern, string>> = {
|
||||
scales: 'Overlapping half circles, fish scales or roof tiles.',
|
||||
sunburst: 'Rays from a centre, a sunburst.',
|
||||
confetti: 'Scattered confetti.',
|
||||
linen: 'Fine woven threads, the texture of a cloth-bound book, best in a colour close to its ground.',
|
||||
grain: 'Fine scattered specks, the grain of paper or kraft card, best in a colour close to its ground.',
|
||||
};
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'حراشف',
|
||||
'journey.studio.pattern.sunburst': 'أشعة الشمس',
|
||||
'journey.studio.pattern.confetti': 'قصاصات ملونة',
|
||||
'journey.studio.pattern.linen': 'كتان',
|
||||
'journey.studio.pattern.grain': 'حبيبات الورق',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Pulcuqlar',
|
||||
'journey.studio.pattern.sunburst': 'Şüalar',
|
||||
'journey.studio.pattern.confetti': 'Konfeti',
|
||||
'journey.studio.pattern.linen': 'Kətan',
|
||||
'journey.studio.pattern.grain': 'Kağız dənəsi',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Escamas',
|
||||
'journey.studio.pattern.sunburst': 'Raios de sol',
|
||||
'journey.studio.pattern.confetti': 'Confete',
|
||||
'journey.studio.pattern.linen': 'Linho',
|
||||
'journey.studio.pattern.grain': 'Textura de papel',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Escates',
|
||||
'journey.studio.pattern.sunburst': 'Raigs',
|
||||
'journey.studio.pattern.confetti': 'Confeti',
|
||||
'journey.studio.pattern.linen': 'Lli',
|
||||
'journey.studio.pattern.grain': 'Gra de paper',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Šupiny',
|
||||
'journey.studio.pattern.sunburst': 'Paprsky',
|
||||
'journey.studio.pattern.confetti': 'Konfety',
|
||||
'journey.studio.pattern.linen': 'Plátno',
|
||||
'journey.studio.pattern.grain': 'Zrno papíru',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Schuppen',
|
||||
'journey.studio.pattern.sunburst': 'Strahlen',
|
||||
'journey.studio.pattern.confetti': 'Konfetti',
|
||||
'journey.studio.pattern.linen': 'Leinen',
|
||||
'journey.studio.pattern.grain': 'Papierkorn',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Scales',
|
||||
'journey.studio.pattern.sunburst': 'Sunburst',
|
||||
'journey.studio.pattern.confetti': 'Confetti',
|
||||
'journey.studio.pattern.linen': 'Linen',
|
||||
'journey.studio.pattern.grain': 'Paper grain',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Escamas',
|
||||
'journey.studio.pattern.sunburst': 'Rayos',
|
||||
'journey.studio.pattern.confetti': 'Confeti',
|
||||
'journey.studio.pattern.linen': 'Lino',
|
||||
'journey.studio.pattern.grain': 'Grano de papel',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Soomused',
|
||||
'journey.studio.pattern.sunburst': 'Päikesekiired',
|
||||
'journey.studio.pattern.confetti': 'Konfetid',
|
||||
'journey.studio.pattern.linen': 'Linane',
|
||||
'journey.studio.pattern.grain': 'Paberi tekstuur',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Écailles',
|
||||
'journey.studio.pattern.sunburst': 'Rayons',
|
||||
'journey.studio.pattern.confetti': 'Confettis',
|
||||
'journey.studio.pattern.linen': 'Lin',
|
||||
'journey.studio.pattern.grain': 'Grain de papier',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Λέπια',
|
||||
'journey.studio.pattern.sunburst': 'Ηλιαχτίδες',
|
||||
'journey.studio.pattern.confetti': 'Κομφετί',
|
||||
'journey.studio.pattern.linen': 'Λινό',
|
||||
'journey.studio.pattern.grain': 'Υφή χαρτιού',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Pikkelyek',
|
||||
'journey.studio.pattern.sunburst': 'Sugarak',
|
||||
'journey.studio.pattern.confetti': 'Konfetti',
|
||||
'journey.studio.pattern.linen': 'Vászon',
|
||||
'journey.studio.pattern.grain': 'Papírszemcse',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Sisik',
|
||||
'journey.studio.pattern.sunburst': 'Sinar matahari',
|
||||
'journey.studio.pattern.confetti': 'Konfeti',
|
||||
'journey.studio.pattern.linen': 'Linen', // same-as-en
|
||||
'journey.studio.pattern.grain': 'Tekstur kertas',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Squame',
|
||||
'journey.studio.pattern.sunburst': 'Raggiera',
|
||||
'journey.studio.pattern.confetti': 'Coriandoli',
|
||||
'journey.studio.pattern.linen': 'Lino',
|
||||
'journey.studio.pattern.grain': 'Grana della carta',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'うろこ',
|
||||
'journey.studio.pattern.sunburst': 'サンバースト',
|
||||
'journey.studio.pattern.confetti': '紙吹雪',
|
||||
'journey.studio.pattern.linen': 'リネン',
|
||||
'journey.studio.pattern.grain': '紙の質感',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': '비늘',
|
||||
'journey.studio.pattern.sunburst': '햇살',
|
||||
'journey.studio.pattern.confetti': '색종이 조각',
|
||||
'journey.studio.pattern.linen': '리넨',
|
||||
'journey.studio.pattern.grain': '종이 질감',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Schubben',
|
||||
'journey.studio.pattern.sunburst': 'Zonnestralen',
|
||||
'journey.studio.pattern.confetti': 'Confetti', // same-as-en
|
||||
'journey.studio.pattern.linen': 'Linnen',
|
||||
'journey.studio.pattern.grain': 'Papierkorrel',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Łuski',
|
||||
'journey.studio.pattern.sunburst': 'Promienie',
|
||||
'journey.studio.pattern.confetti': 'Konfetti',
|
||||
'journey.studio.pattern.linen': 'Len',
|
||||
'journey.studio.pattern.grain': 'Faktura papieru',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Чешуя',
|
||||
'journey.studio.pattern.sunburst': 'Солнечные лучи',
|
||||
'journey.studio.pattern.confetti': 'Конфетти',
|
||||
'journey.studio.pattern.linen': 'Лён',
|
||||
'journey.studio.pattern.grain': 'Зерно бумаги',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Šupiny',
|
||||
'journey.studio.pattern.sunburst': 'Lúče',
|
||||
'journey.studio.pattern.confetti': 'Konfety',
|
||||
'journey.studio.pattern.linen': 'Plátno',
|
||||
'journey.studio.pattern.grain': 'Zrno papiera',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Fjäll',
|
||||
'journey.studio.pattern.sunburst': 'Solstrålar',
|
||||
'journey.studio.pattern.confetti': 'Konfetti',
|
||||
'journey.studio.pattern.linen': 'Linne',
|
||||
'journey.studio.pattern.grain': 'Pappersstruktur',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'เกล็ด',
|
||||
'journey.studio.pattern.sunburst': 'รัศมีแสง',
|
||||
'journey.studio.pattern.confetti': 'กระดาษสี',
|
||||
'journey.studio.pattern.linen': 'ผ้าลินิน',
|
||||
'journey.studio.pattern.grain': 'เนื้อกระดาษ',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Pullar',
|
||||
'journey.studio.pattern.sunburst': 'Güneş ışınları',
|
||||
'journey.studio.pattern.confetti': 'Konfeti',
|
||||
'journey.studio.pattern.linen': 'Keten',
|
||||
'journey.studio.pattern.grain': 'Kâğıt dokusu',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Луска',
|
||||
'journey.studio.pattern.sunburst': 'Сонячні промені',
|
||||
'journey.studio.pattern.confetti': 'Конфеті',
|
||||
'journey.studio.pattern.linen': 'Льон',
|
||||
'journey.studio.pattern.grain': 'Зерно паперу',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -45,5 +45,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': 'Vảy',
|
||||
'journey.studio.pattern.sunburst': 'Tia nắng',
|
||||
'journey.studio.pattern.confetti': 'Hoa giấy',
|
||||
'journey.studio.pattern.linen': 'Vải lanh',
|
||||
'journey.studio.pattern.grain': 'Vân giấy',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -44,5 +44,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': '鱗片',
|
||||
'journey.studio.pattern.sunburst': '放射光芒',
|
||||
'journey.studio.pattern.confetti': '彩色紙花',
|
||||
'journey.studio.pattern.linen': '亞麻',
|
||||
'journey.studio.pattern.grain': '紙張紋理',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
@@ -44,5 +44,7 @@ const studioPaint: TranslationStrings = {
|
||||
'journey.studio.pattern.scales': '鳞片',
|
||||
'journey.studio.pattern.sunburst': '放射光',
|
||||
'journey.studio.pattern.confetti': '彩纸屑',
|
||||
'journey.studio.pattern.linen': '亚麻',
|
||||
'journey.studio.pattern.grain': '纸张纹理',
|
||||
};
|
||||
export default studioPaint;
|
||||
|
||||
Reference in New Issue
Block a user