diff --git a/ROADMAP.md b/ROADMAP.md
index af8eb54..c5c02fb 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -5,6 +5,7 @@ Open ideas for the playground frontend. Tick a box when done.
## Quick wins
- [ ] **Labyrinth grid size crash**: `labyrinth.component.html` still binds `[gridRows]`; typed sizes above the max crash the page. Same fix as pathfinding (clamp in code, drop the binding).
+- [ ] **Fourier circles slider thumb**: at "Circles: 256" the thumb sits near the left end, like at 30. Check `max`/value binding of the circles `mat-slider` (seen 2026-10-14, not verified).
- [ ] **Lighthouse gate**: add an `assert` block to `lighthouserc.json` (e.g. performance, accessibility, SEO >= 0.9). Today CI runs the audit but never fails.
Note: the `@lhci/cli` → lighthouse 13 override in `package.json` is only safe without `assert`. Check it again before you add one.
- [ ] **robots.txt + sitemap.xml**: static files in `public/`, listing all routes from `app.routes.ts`.
diff --git a/src/app/pages/algorithms/fourier/fourier.component.html b/src/app/pages/algorithms/fourier/fourier.component.html
index 9d573a1..0588f3e 100644
--- a/src/app/pages/algorithms/fourier/fourier.component.html
+++ b/src/app/pages/algorithms/fourier/fourier.component.html
@@ -17,6 +17,9 @@
+
diff --git a/src/app/pages/algorithms/fourier/fourier.models.ts b/src/app/pages/algorithms/fourier/fourier.models.ts
index 6ebb7cf..9d968b3 100644
--- a/src/app/pages/algorithms/fourier/fourier.models.ts
+++ b/src/app/pages/algorithms/fourier/fourier.models.ts
@@ -3,7 +3,7 @@ export interface Point { x: number; y: number; }
/** One rotating circle. frequency is signed (turns per period), phase in radians. */
export interface EpicycleTerm { frequency: number; amplitude: number; phase: number; }
-export enum Preset { HEART = 0, STAR = 1, SQUARE = 2 }
+export enum Preset { HEART = 0, STAR = 1, SQUARE = 2, DOG = 3 }
export const SAMPLE_COUNT = 256;
export const MIN_DRAW_POINTS = 3;
diff --git a/src/app/pages/algorithms/fourier/service/fourier.service.spec.ts b/src/app/pages/algorithms/fourier/service/fourier.service.spec.ts
index 7d78beb..60be5c1 100644
--- a/src/app/pages/algorithms/fourier/service/fourier.service.spec.ts
+++ b/src/app/pages/algorithms/fourier/service/fourier.service.spec.ts
@@ -58,7 +58,7 @@ describe('FourierService', () => {
expect(service.epicyclePositions(terms, 0, 0)).toEqual([{x: 0, y: 0}]);
});
- it.each([Preset.HEART, Preset.STAR, Preset.SQUARE])('keeps preset %s inside the canvas', preset => {
+ it.each([Preset.HEART, Preset.STAR, Preset.SQUARE, Preset.DOG])('keeps preset %s inside the canvas', preset => {
const points = service.presetPoints(preset);
expect(points.length).toBeGreaterThanOrEqual(3);
@@ -68,6 +68,30 @@ describe('FourierService', () => {
});
});
+ it('mirrors the dog preset left-right', () => {
+ const points = service.presetPoints(Preset.DOG);
+
+ points.forEach((p, i) => {
+ expect(points.some(q => Math.abs(q.x + p.x) < 1e-9 && Math.abs(q.y - p.y) < 1e-9)).toBe(true);
+ const next = points[(i + 1) % points.length];
+ expect(Math.hypot(next.x - p.x, next.y - p.y)).toBeGreaterThan(0);
+ });
+ });
+
+ it('cuts the corners of a square with one chaikin pass', () => {
+ const square = service.presetPoints(Preset.SQUARE);
+ const r = square[2].x;
+
+ const cut = service.chaikin(square, 1);
+
+ expect(cut.length).toBe(8);
+ cut.forEach(p => {
+ expect(Math.abs(p.x)).toBeLessThanOrEqual(r);
+ expect(Math.abs(p.y)).toBeLessThanOrEqual(r);
+ expect(Math.abs(p.x) === r && Math.abs(p.y) === r).toBe(false);
+ });
+ });
+
it('samples the heart preset SAMPLE_COUNT times', () => {
expect(service.presetPoints(Preset.HEART).length).toBe(SAMPLE_COUNT);
});
diff --git a/src/app/pages/algorithms/fourier/service/fourier.service.ts b/src/app/pages/algorithms/fourier/service/fourier.service.ts
index ea693e9..05dc707 100644
--- a/src/app/pages/algorithms/fourier/service/fourier.service.ts
+++ b/src/app/pages/algorithms/fourier/service/fourier.service.ts
@@ -1,6 +1,38 @@
import {Injectable} from '@angular/core';
import {EpicycleTerm, Point, PRESET_RADIUS, Preset, SAMPLE_COUNT} from '../fourier.models';
+/** Right half of the dog head in a [-1, 1] box, clockwise from top center to chin center. */
+const DOG_RIGHT_HALF: Point[] = [
+ {x: 0, y: -0.8},
+ {x: 0.14, y: -0.79},
+ {x: 0.26, y: -0.74},
+ {x: 0.34, y: -0.64},
+ {x: 0.4, y: -0.54},
+ {x: 0.52, y: -0.58},
+ {x: 0.68, y: -0.5},
+ {x: 0.8, y: -0.36},
+ {x: 0.88, y: -0.12},
+ {x: 0.9, y: 0.18},
+ {x: 0.86, y: 0.48},
+ {x: 0.78, y: 0.72},
+ {x: 0.66, y: 0.72},
+ {x: 0.58, y: 0.5},
+ {x: 0.55, y: 0.2},
+ {x: 0.5, y: -0.1},
+ {x: 0.46, y: -0.46},
+ {x: 0.39, y: -0.1},
+ {x: 0.4, y: 0.15},
+ {x: 0.38, y: 0.36},
+ {x: 0.31, y: 0.5},
+ {x: 0.23, y: 0.58},
+ {x: 0.21, y: 0.68},
+ {x: 0.15, y: 0.72},
+ {x: 0.13, y: 0.78},
+ {x: 0.09, y: 0.9},
+ {x: 0, y: 0.92},
+];
+const DOG_SMOOTHING_PASSES = 3;
+
@Injectable({providedIn: 'root'})
export class FourierService {
/** Treats points as a closed polygon and returns count points at equal arc-length spacing. */
@@ -72,6 +104,22 @@ export class FourierService {
return positions;
}
+ /** Corner cutting on a closed polygon: each edge P->Q becomes 0.75P+0.25Q and 0.25P+0.75Q. */
+ chaikin(points: Point[], passes: number): Point[] {
+ let result = points;
+ for (let pass = 0; pass < passes; pass++) {
+ const previous = result;
+ result = previous.flatMap((p, i) => {
+ const q = previous[(i + 1) % previous.length];
+ return [
+ {x: 0.75 * p.x + 0.25 * q.x, y: 0.75 * p.y + 0.25 * q.y},
+ {x: 0.25 * p.x + 0.75 * q.x, y: 0.25 * p.y + 0.75 * q.y},
+ ];
+ });
+ }
+ return result;
+ }
+
presetPoints(preset: Preset): Point[] {
switch (preset) {
case Preset.HEART:
@@ -92,6 +140,11 @@ export class FourierService {
const r = PRESET_RADIUS * 0.85;
return [{x: -r, y: -r}, {x: r, y: -r}, {x: r, y: r}, {x: -r, y: r}];
}
+ case Preset.DOG: {
+ const left = DOG_RIGHT_HALF.slice(1, -1).reverse().map(p => ({x: -p.x, y: p.y}));
+ const outline = this.chaikin([...DOG_RIGHT_HALF, ...left], DOG_SMOOTHING_PASSES);
+ return outline.map(p => ({x: p.x * PRESET_RADIUS, y: p.y * PRESET_RADIUS}));
+ }
}
}
}
diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json
index c8c8c96..5da8d4c 100644
--- a/src/assets/i18n/de.json
+++ b/src/assets/i18n/de.json
@@ -679,6 +679,7 @@
"PRESET_HEART": "Herz",
"PRESET_STAR": "Stern",
"PRESET_SQUARE": "Quadrat",
+ "PRESET_DOG": "Hund",
"CIRCLES": "Kreise",
"SPEED": "Geschwindigkeit",
"SHOW_CIRCLES": "Kreise anzeigen",
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index bdb51f6..de4cdc6 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -679,6 +679,7 @@
"PRESET_HEART": "Heart",
"PRESET_STAR": "Star",
"PRESET_SQUARE": "Square",
+ "PRESET_DOG": "Dog",
"CIRCLES": "Circles",
"SPEED": "Speed",
"SHOW_CIRCLES": "Show circles",