|
|
@@ -142,6 +142,98 @@ function pieceMontee(): Canvas {
|
|
|
return c;
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Le Roulé: a slice of Swiss roll lying on the plate, a continuous spiral of jam (jelly) through
|
|
|
+ * the sponge. Cut the sponge strip and everything inside that turn of the spiral comes loose.
|
|
|
+ */
|
|
|
+function roule(): Canvas {
|
|
|
+ const c = new Canvas();
|
|
|
+ c.plate();
|
|
|
+ const cx = 1024;
|
|
|
+ const R = 330;
|
|
|
+ const cy = 960 - R - 14;
|
|
|
+ // Biscuit stand under the roll so it does not just touch the plate at one point.
|
|
|
+ c.rect(cx - 180, 900, cx + 181, 960, BISCUIT);
|
|
|
+ const SPACING = 64; // distance between two turns of the spiral
|
|
|
+ const JAM = 12;
|
|
|
+ for (let y = cy - R; y <= cy + R; y++) {
|
|
|
+ for (let x = cx - R; x <= cx + R; x++) {
|
|
|
+ const dx = x - cx;
|
|
|
+ const dy = y - cy;
|
|
|
+ const r = Math.hypot(dx, dy);
|
|
|
+ if (r > R) continue;
|
|
|
+ if (r > R - 10) {
|
|
|
+ c.put(x, y, CREAM); // thin whipped cream coat
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // Archimedean spiral r = SPACING * (θ / 2π): distance to the nearest turn.
|
|
|
+ const theta = (Math.atan2(dy, dx) + Math.PI * 2) % (Math.PI * 2);
|
|
|
+ const phase = (r - (SPACING * theta) / (Math.PI * 2) + SPACING * 100) % SPACING;
|
|
|
+ c.put(x, y, phase < JAM && r > 24 ? JELLY : SPONGE);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return c;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * La Tarte aux Fruits: a brittle shortcrust shell (biscuit) holding custard cream, topped with
|
|
|
+ * jelly fruit halves. The base cracks at random (modifier).
|
|
|
+ */
|
|
|
+function tarteFruits(): Canvas {
|
|
|
+ const c = new Canvas();
|
|
|
+ c.plate();
|
|
|
+ const x0 = 300;
|
|
|
+ const x1 = 1748;
|
|
|
+ const bottom = 960;
|
|
|
+ // Shell: base and fluted rims.
|
|
|
+ c.rect(x0, bottom - 70, x1, bottom, BISCUIT);
|
|
|
+ for (const [rx0, rx1] of [
|
|
|
+ [x0, x0 + 46],
|
|
|
+ [x1 - 46, x1],
|
|
|
+ ]) {
|
|
|
+ for (let y = 640; y < bottom; y++) {
|
|
|
+ const flute = Math.round(4 * Math.sin(y / 9));
|
|
|
+ c.rect(rx0 + (rx0 === x0 ? flute : 0), y, rx1 - (rx0 !== x0 ? flute : 0), y + 1, BISCUIT);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Pastry cream.
|
|
|
+ c.rect(x0 + 46, 740, x1 - 46, bottom - 70, SPONGE);
|
|
|
+ // Fruit halves: rows of jelly domes on the cream.
|
|
|
+ for (let i = 0; i < 18; i++) {
|
|
|
+ const fx = x0 + 100 + i * 75;
|
|
|
+ const r = 30 + (i % 3) * 6;
|
|
|
+ for (let y = 740 - r; y < 742; y++) for (let x = fx - r; x <= fx + r; x++) if ((x - fx) ** 2 + (y - 742) ** 2 <= r * r) c.put(x, y, JELLY);
|
|
|
+ }
|
|
|
+ // Glaze specks of whipped cream on the rims.
|
|
|
+ for (const x of [x0 + 23, x1 - 23]) c.disc(x, 632, 16, CREAM);
|
|
|
+ return c;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * La Pile de Crêpes: dozens of thin crêpes with spreads in between (chocolate, jam, caramel),
|
|
|
+ * stacked into a pile that leans. Side wind (modifier).
|
|
|
+ */
|
|
|
+function crepes(): Canvas {
|
|
|
+ const c = new Canvas();
|
|
|
+ c.plate();
|
|
|
+ const SPREADS = [CHOCOLATE, JELLY, CARAMEL];
|
|
|
+ const CREPE = 9;
|
|
|
+ const SPREAD = 3;
|
|
|
+ const lean = 0.22; // horizontal shift per pixel of height
|
|
|
+ let layer = 0;
|
|
|
+ for (let top = 960; top > 340; top -= CREPE + SPREAD, layer++) {
|
|
|
+ const shift = Math.round((960 - top) * lean);
|
|
|
+ const halfWidth = 330 - Math.round(layer * 1.5) + Math.round(6 * Math.sin(layer * 1.7));
|
|
|
+ const left = 860 - halfWidth + shift;
|
|
|
+ const right = 860 + halfWidth + shift;
|
|
|
+ c.rect(left, top - CREPE, right, top, SPONGE);
|
|
|
+ if (top - CREPE - SPREAD > 340) c.rect(left + 12, top - CREPE - SPREAD, right - 12, top - CREPE, SPREADS[layer % 3]);
|
|
|
+ }
|
|
|
+ // Whipped cream on top.
|
|
|
+ c.disc(860 + Math.round(620 * lean), 345, 40, CREAM);
|
|
|
+ return c;
|
|
|
+}
|
|
|
+
|
|
|
function writePng(dir: string, name: string, w: number, h: number, pixel: (x: number, y: number) => number): void {
|
|
|
const png = new PNG({ width: w, height: h });
|
|
|
for (let y = 0; y < h; y++) {
|
|
|
@@ -182,6 +274,9 @@ const THEMES: Record<string, Theme> = {
|
|
|
'gateau-etages': { stripe: 0xffdbe3, cloth: [0xe85d75, 0xf7a8b8, 0xfff3f5], flags: [0xff6f91, 0xffe14d, 0x4dc4ff, 0x9be15d, 0xb18cff] },
|
|
|
'mille-feuille': { stripe: 0xd9ecff, cloth: [0x4d8fd9, 0xa8cdf7, 0xf3f8ff], flags: [0x4dc4ff, 0xffffff, 0xffe14d, 0x9be15d, 0x4dc4ff] },
|
|
|
'piece-montee': { stripe: 0xfff0c2, cloth: [0xd9a01e, 0xf7d98a, 0xfffaf0], flags: [0xffe14d, 0xff9a2e, 0xffffff, 0xff6f91, 0xffd27a] },
|
|
|
+ roule: { stripe: 0xe3f5d9, cloth: [0x5fae4a, 0xb8e0a8, 0xf6fff2], flags: [0x9be15d, 0xff6f91, 0xffffff, 0xffe14d, 0x9be15d] },
|
|
|
+ 'tarte-fruits': { stripe: 0xefe3ff, cloth: [0x8a5fc9, 0xcdb8f0, 0xfaf6ff], flags: [0xb18cff, 0xff6f91, 0x9be15d, 0xffffff, 0xffe14d] },
|
|
|
+ crepes: { stripe: 0xffe8d0, cloth: [0xe0862e, 0xf5c79a, 0xfff8f0], flags: [0xff9a2e, 0x5a3a22, 0xffe14d, 0xffffff, 0xff6f91] },
|
|
|
};
|
|
|
|
|
|
function paintBackground(theme: Theme): Uint32Array {
|
|
|
@@ -298,6 +393,9 @@ const MAPS: Record<string, () => Canvas> = {
|
|
|
'gateau-etages': gateauEtages,
|
|
|
'mille-feuille': milleFeuille,
|
|
|
'piece-montee': pieceMontee,
|
|
|
+ roule,
|
|
|
+ 'tarte-fruits': tarteFruits,
|
|
|
+ crepes,
|
|
|
};
|
|
|
|
|
|
const TW = 256;
|