Skip to content

Commit b2552f6

Browse files
committed
feat: add "dithering" shape
1 parent 665a72d commit b2552f6

6 files changed

Lines changed: 77 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
No changes yet.
1111

12+
## [0.1.5] - 2026-03-05
13+
14+
- New shape available : `dithering` as a 4x4 Bayer matrix.
15+
1216
## [0.1.1] - 2026-03-02
1317

1418
### Fixed

README.md

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,21 @@ Creates a pattern that can be used in SVG or Canvas.
116116

117117
#### Options
118118

119-
| Option | Type | Default | Description |
120-
| -------------- | -------- | --------------- | --------------------------------------------------------------------------------------------------- |
121-
| `type` | string | `"line"` | Pattern type: "line", "plaid", "circle", "plus", "cross", "triangle", "square", "diamond", "custom" |
122-
| `scale` | number | `1` | Scale of the tile (multiplier) |
123-
| `size` | number | `20` | Shape size as a percentage of tile area (0-100) |
124-
| `angle` | number | `0` | Rotation angle in degrees (0-360) |
125-
| `fill` | string | `"black"` | Fill color (CSS color) |
126-
| `stroke` | string | `"transparent"` | Stroke color (CSS color) |
127-
| `strokeWidth` | number | `0` | Stroke width in pixels |
128-
| `dash` | number[] | `[]` | Dash pattern for stroke (e.g., `[5, 5]`) |
129-
| `background` | string | `"white"` | Background color of the pattern tile |
130-
| `border` | number | `0` | Border width around tile edges |
131-
| `patchSize` | boolean | `false` | Intelligently swap foreground/background for better appearance at high sizes |
132-
| `custom.shape` | function | - | Custom shape function (for type: "custom") |
133-
| `custom.patch` | number | `50` | Patch size for custom shapes |
119+
| Option | Type | Default | Description |
120+
| -------------- | -------- | --------------- | ---------------------------------------------------------------------------------------------------------------- |
121+
| `type` | string | `"line"` | Pattern type: "line", "plaid", "circle", "plus", "cross", "triangle", "square", "diamond", "dithering", "custom" |
122+
| `scale` | number | `1` | Scale of the tile (multiplier) |
123+
| `size` | number | `20` | Shape size as a percentage of tile area (0-100) |
124+
| `angle` | number | `0` | Rotation angle in degrees (0-360) |
125+
| `fill` | string | `"black"` | Fill color (CSS color) |
126+
| `stroke` | string | `"transparent"` | Stroke color (CSS color) |
127+
| `strokeWidth` | number | `0` | Stroke width in pixels |
128+
| `dash` | number[] | `[]` | Dash pattern for stroke (e.g., `[5, 5]`) |
129+
| `background` | string | `"white"` | Background color of the pattern tile |
130+
| `border` | number | `0` | Border width around tile edges |
131+
| `patchSize` | boolean | `false` | Intelligently swap foreground/background for better appearance at high sizes |
132+
| `custom.shape` | function | - | Custom shape function (for type: "custom") |
133+
| `custom.patch` | number | `50` | Patch size for custom shapes |
134134

135135
#### Return Value
136136

@@ -211,6 +211,14 @@ motif({ type: "square", size: 25 });
211211
motif({ type: "diamond", size: 25 });
212212
```
213213

214+
### Dithering
215+
216+
Dithering pattern. Due to the 4x4 Bayer matrix, the `size` option is limited to only 16 values/steps (multiples of 4).
217+
218+
```typescript
219+
motif({ type: "dithering", scale: 2, size: 30 });
220+
```
221+
214222
### Custom
215223

216224
Define your own pattern shape

demo/src/components/AttentionPoints.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"triangle",
1616
"square",
1717
"diamond",
18+
"dithering",
1819
];
1920
const patchSizes = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
2021
let patchEnabled = $state(true);

demo/src/components/Generator.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"triangle",
2828
"square",
2929
"diamond",
30+
"dithering",
3031
];
3132
3233
let dash = $derived(

demo/src/lib/presets.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export const presets: Preset[] = [
4242
name: "Plus",
4343
options: { type: "plus", scale: 4, size: 21, angle: 15, fill: "#f08080", stroke: "#000000", strokeWidth: 2 }
4444
},
45+
{
46+
name: "Dithering",
47+
options: { type: "dithering", scale: 2, size: 30 }
48+
},
4549
{
4650
name: "Royal blue",
4751
options: { type: "square", scale: 2.5, size: 5, angle: 45, fill: "#ffd700", background: "#00009D" }

src/motif.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const DEFAULT_DPI_MULTIPLIER = 2;
2424
const CROSS_DIAMOND_ROTATION = 45;
2525
const TILE_BASE_SIZE = 10;
2626

27-
export type PatternType = "line" | "plaid" | "circle" | "plus" | "cross" | "triangle" | "square" | "diamond" | "custom";
27+
export type PatternType = "line" | "plaid" | "circle" | "plus" | "cross" | "triangle" | "square" | "diamond" | "dithering" | "custom";
2828

2929
export type CustomShapeFnSVG = (t: number, shapeArea: number) => string;
3030
export type CustomShapeFnCanvas = (t: number, shapeArea: number, context: Path) => Path;
@@ -134,6 +134,47 @@ function square(t: number, shapeArea: number): string {
134134
Z`;
135135
}
136136

137+
function dithering(t: number, shapeArea: number): string {
138+
// 1. Calcul du ratio de remplissage (entre 0 et 1)
139+
// t * t est l'aire totale de la tuile
140+
const ratio = Math.max(0, Math.min(1, shapeArea / (t * t)));
141+
142+
// 2. Matrice de Bayer 4x4 (valeurs de 0 à 15)
143+
// Elle définit l'ordre dans lequel les pixels s'allument
144+
const bayer = [
145+
0, 8, 2, 10,
146+
12, 4, 14, 6,
147+
3, 11, 1, 9,
148+
15, 7, 13, 5
149+
];
150+
151+
const gridSize = 4; // Grille de 4x4 pixels
152+
const pixelSize = t / gridSize; // Taille d'un "pixel"
153+
let path = '';
154+
155+
// 3. Boucle sur la grille pour construire le chemin SVG
156+
for (let y = 0; y < gridSize; y++) {
157+
for (let x = 0; x < gridSize; x++) {
158+
// On normalise le seuil entre 0 et 1
159+
// (+ 0.5 pour centrer l'activation sur les classes de ok-palette)
160+
const threshold = (bayer[y * gridSize + x] + 0.5) / (gridSize * gridSize);
161+
162+
if (ratio >= threshold) {
163+
// Coordonnées du coin supérieur gauche du pixel
164+
// On part de -t/2 pour respecter le centrage de motif.js
165+
const px = -t / 2 + x * pixelSize;
166+
const py = -t / 2 + y * pixelSize;
167+
168+
// Ajout du petit carré au chemin SVG global
169+
path += `M ${px} ${py} h ${pixelSize} v ${pixelSize} h ${-pixelSize} Z `;
170+
}
171+
}
172+
}
173+
174+
// Si l'aire est très proche de 0, on retourne un chemin vide
175+
return path.trim() || 'M 0 0';
176+
}
177+
137178
export function motif(options: PatternOptions = {}): PatternResult {
138179
let {
139180
type = "line",
@@ -164,6 +205,7 @@ export function motif(options: PatternOptions = {}): PatternResult {
164205
["triangle", triangle],
165206
["square", square],
166207
["diamond", square],
208+
["dithering", dithering],
167209
["custom", custom?.shape as CustomShapeFn]
168210
]);
169211

@@ -177,6 +219,7 @@ export function motif(options: PatternOptions = {}): PatternResult {
177219
["cross", 55],
178220
["square", 50], // arbitrary
179221
["diamond", 50],
222+
["dithering", 100],
180223
["custom", custom?.patch ?? 50]
181224
]);
182225

0 commit comments

Comments
 (0)