Qualitaetssprung: ebene Belaege, echte Bauteile, Sichtschutz
Deploy App / deploy (push) Successful in 49s

Nach der Plankontrolle ueberarbeitet:

- Belaege sind eben. Je Belag wird aus den Hoehenkoten eine Ebene bestimmt
  (Gefaelle <= 5 %, am Kotenschwerpunkt aufgehaengt, nur Koten derselben
  Gelaendezone). Das Raster wird darauf gezwungen und dort nicht geglaettet.
  Jeder Belag bekommt eine senkrechte Sichtkante zum Gelaende.
- Mauern sind Bauteile: Travertin-Schichtmauerwerk mit versetzten Stossfugen
  und Abdeckplatte, einzelne Winkelelemente mit Fussplatte, Ortbeton in
  Schaltafelfeldern.
- Sichtschutz: die Eibenreihen des Plans werden zu geschnittenen Hecken
  verkettet, ebenso die Ligusterhecke. Lamellenwand im Planrhythmus.
- Der Plan zeichnet die Travertinplatten 40x60 einzeln im Dekorkies - jetzt
  30 erhabene Platten an den Planpositionen statt geschlossenem Belag.
- Normalmaps aus dem Fugenbild fuer Platten, Pflaster, Holzschalung und Laub.
- Nachbarhaus Zollet richtig platziert (stand auf dem Nachbargarten) und
  wie Haus 9a ausgebildet.

Behobene Fehler: boxGeometry erzeugte Seiten und Deckel mit umgekehrtem
Wickelsinn - saemtliche Boxen waren innen nach aussen gekehrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 07:47:47 +02:00
co-authored by Claude Opus 5
parent f2b7b1b236
commit 8979775895
24 changed files with 1452 additions and 319 deletions
+351 -96
View File
@@ -8,6 +8,7 @@
*/
import {
CanvasTexture, RepeatWrapping, SRGBColorSpace, LinearMipmapLinearFilter,
LinearSRGBColorSpace, Vector2,
} from 'three';
const SIZE = 512;
@@ -102,6 +103,9 @@ function slabs(
export interface Belag {
map: CanvasTexture;
/** Aus dem Fugenbild abgeleitete Normalmap - gibt den Belaegen Tiefe. */
normalMap?: CanvasTexture;
normalScale?: Vector2;
tile: number;
roughness: number;
color?: number;
@@ -119,75 +123,210 @@ function make(key: string, tile: number, roughness: number, draw: (ctx: CanvasRe
return belag;
}
/**
* Rechnet ein Hoehenbild in eine Normalmap um (Sobel, kachelnd).
* Damit bekommen Fugen und Steinkanten echte Tiefe im Streiflicht.
*/
function normalFromHeight(src: HTMLCanvasElement, strength: number): CanvasTexture {
const sctx = src.getContext('2d', { willReadFrequently: true })!;
const h = sctx.getImageData(0, 0, SIZE, SIZE).data;
const [c, ctx] = canvas();
const out = ctx.createImageData(SIZE, SIZE);
const at = (x: number, y: number) =>
h[(((y + SIZE) % SIZE) * SIZE + ((x + SIZE) % SIZE)) * 4] / 255;
for (let y = 0; y < SIZE; y++) {
for (let x = 0; x < SIZE; x++) {
const dx =
at(x - 1, y - 1) + 2 * at(x - 1, y) + at(x - 1, y + 1) -
at(x + 1, y - 1) - 2 * at(x + 1, y) - at(x + 1, y + 1);
const dy =
at(x - 1, y - 1) + 2 * at(x, y - 1) + at(x + 1, y - 1) -
at(x - 1, y + 1) - 2 * at(x, y + 1) - at(x + 1, y + 1);
const nx = dx * strength, ny = dy * strength, nz = 1;
const len = Math.hypot(nx, ny, nz);
const k = (y * SIZE + x) * 4;
out.data[k] = ((nx / len) * 0.5 + 0.5) * 255;
out.data[k + 1] = ((ny / len) * 0.5 + 0.5) * 255;
out.data[k + 2] = ((nz / len) * 0.5 + 0.5) * 255;
out.data[k + 3] = 255;
}
}
ctx.putImageData(out, 0, 0);
const t = new CanvasTexture(c);
t.wrapS = t.wrapT = RepeatWrapping;
t.colorSpace = LinearSRGBColorSpace;
t.anisotropy = 8;
t.minFilter = LinearMipmapLinearFilter;
return t;
}
/**
* Wie `make`, zusaetzlich mit einem Hoehenbild fuer die Normalmap.
* `drawHeight` zeichnet Graustufen: hell = hoch, dunkel = Fuge.
*/
function makeRelief(
key: string, tile: number, roughness: number, normalStrength: number,
draw: (ctx: CanvasRenderingContext2D) => void,
drawHeight: (ctx: CanvasRenderingContext2D) => void,
): Belag {
const hit = cache.get(key);
if (hit) return hit;
const [c, ctx] = canvas();
draw(ctx);
const [hc, hctx] = canvas();
drawHeight(hctx);
const belag: Belag = {
map: texture(c),
normalMap: normalFromHeight(hc, normalStrength),
normalScale: new Vector2(1, 1),
tile,
roughness,
};
cache.set(key, belag);
return belag;
}
/** Zeichnet dasselbe Plattenraster als Hoehenbild: Platte hell, Fuge dunkel. */
function slabHeight(
ctx: CanvasRenderingContext2D, tile: number, slabW: number, slabH: number,
jointM: number, chamfer = 0.012,
) {
const px = SIZE / tile;
ctx.fillStyle = 'rgb(40,40,40)';
ctx.fillRect(0, 0, SIZE, SIZE);
const cols = Math.round(tile / slabW);
const rows = Math.round(tile / slabH);
const j = Math.max(1.5, jointM * px);
const ch = Math.max(1, chamfer * px);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col * slabW * px + j / 2;
const y = row * slabH * px + j / 2;
const w = slabW * px - j, hgt = slabH * px - j;
// Fase aussen, Plattenspiegel innen
ctx.fillStyle = 'rgb(170,170,170)';
ctx.fillRect(x, y, w, hgt);
ctx.fillStyle = 'rgb(235,235,235)';
ctx.fillRect(x + ch, y + ch, w - 2 * ch, hgt - 2 * ch);
}
}
}
// ---------------------------------------------------------------------------
// Belaege
// ---------------------------------------------------------------------------
/** Travertinplatten 80 x 80 x 3 cm, warmes Creme mit feiner Poren-Struktur. */
export const travertin80 = () => make('travertin80', 3.2, 0.72, (ctx) => {
slabs(ctx, 3.2, 0.8, 0.8, 'rgb(209,195,168)', 'rgb(170,157,134)', 0.008, 11, 7);
noiseTile(ctx, 21, 32, 0.05);
grain(ctx, 31, 0.05, 2);
});
/** Travertin-Oberflaeche: warme Wolken und feine Poren. */
function travertinFinish(ctx: CanvasRenderingContext2D, seed: number) {
noiseTile(ctx, seed, 48, 0.035);
noiseTile(ctx, seed + 100, 144, 0.025);
const rnd = mulberry32(seed + 7);
// Poren und Adern, typisch fuer Travertin
for (let i = 0; i < 900; i++) {
ctx.fillStyle = `rgba(120,105,84,${0.05 + rnd() * 0.10})`;
ctx.beginPath();
ctx.ellipse(rnd() * SIZE, rnd() * SIZE, 1 + rnd() * 5, 0.6 + rnd() * 1.6, rnd() * 0.6, 0, Math.PI * 2);
ctx.fill();
}
for (let i = 0; i < 40; i++) {
ctx.strokeStyle = `rgba(146,130,104,${0.06 + rnd() * 0.08})`;
ctx.lineWidth = 1 + rnd() * 2;
ctx.beginPath();
const y = rnd() * SIZE;
ctx.moveTo(0, y);
for (let x = 0; x <= SIZE; x += 32) ctx.lineTo(x, y + Math.sin(x / 40 + i) * 3);
ctx.stroke();
}
}
/** Travertinplatten 40 x 60 x 3 cm, im Plan mit Dekorkies in den Fugen. */
export const travertin4060 = () => make('travertin4060', 2.4, 0.75, (ctx) => {
slabs(ctx, 2.4, 0.4, 0.6, 'rgb(212,199,172)', 'rgb(158,150,133)', 0.02, 12, 8);
noiseTile(ctx, 22, 32, 0.05);
grain(ctx, 32, 0.06, 2);
});
/** Travertinplatten 80 x 80 x 3 cm, Hauptsitzplatz. */
export const travertin80 = () => makeRelief('travertin80', 3.2, 0.70, 3.2,
(ctx) => {
slabs(ctx, 3.2, 0.8, 0.8, 'rgb(209,195,168)', 'rgb(158,145,124)', 0.010, 11, 7);
travertinFinish(ctx, 21);
},
(ctx) => slabHeight(ctx, 3.2, 0.8, 0.8, 0.010, 0.015));
/** Einzelne Travertinplatte 40 x 60 x 3 cm - Textur fuer die losen Platten. */
export const travertinSlab = () => makeRelief('travertinSlab', 0.8, 0.70, 2.2,
(ctx) => {
ctx.fillStyle = 'rgb(213,199,171)';
ctx.fillRect(0, 0, SIZE, SIZE);
travertinFinish(ctx, 26);
},
(ctx) => { ctx.fillStyle = 'rgb(210,210,210)'; ctx.fillRect(0, 0, SIZE, SIZE); noiseTile(ctx, 27, 64, 0.06); });
/** Travertinstellplatten als schmaler Randstreifen. */
export const travertinstell = () => make('travertinstell', 2.4, 0.75, (ctx) => {
slabs(ctx, 2.4, 0.4, 0.4, 'rgb(206,192,166)', 'rgb(164,154,135)', 0.012, 13, 8);
noiseTile(ctx, 23, 32, 0.05);
grain(ctx, 33, 0.05, 2);
});
export const travertinstell = () => makeRelief('travertinstell', 2.4, 0.73, 3.0,
(ctx) => {
slabs(ctx, 2.4, 0.4, 0.4, 'rgb(206,192,166)', 'rgb(155,144,124)', 0.012, 13, 8);
travertinFinish(ctx, 23);
},
(ctx) => slabHeight(ctx, 2.4, 0.4, 0.4, 0.012, 0.012));
/** Keramikplatten 60 x 60 x 2 cm, grau - bauseits vorhanden (siehe Fotos). */
export const keramik = () => make('keramik', 2.4, 0.55, (ctx) => {
slabs(ctx, 2.4, 0.6, 0.6, 'rgb(160,158,154)', 'rgb(128,127,124)', 0.006, 14, 5);
noiseTile(ctx, 24, 48, 0.035);
grain(ctx, 34, 0.04, 1);
});
export const keramik = () => makeRelief('keramik', 2.4, 0.52, 2.6,
(ctx) => {
slabs(ctx, 2.4, 0.6, 0.6, 'rgb(160,158,154)', 'rgb(118,117,114)', 0.006, 14, 5);
noiseTile(ctx, 24, 48, 0.035);
noiseTile(ctx, 124, 160, 0.02);
grain(ctx, 34, 0.04, 1);
},
(ctx) => slabHeight(ctx, 2.4, 0.6, 0.6, 0.006, 0.008));
/** Tegula-Pflastersteine grau, kleinformatig und leicht unregelmaessig. */
export const tegula = () => make('tegula', 1.2, 0.85, (ctx) => {
const px = SIZE / 1.2;
ctx.fillStyle = 'rgb(118,116,114)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(15);
const sw = 0.15, sh = 0.1;
const cols = Math.round(1.2 / sw), rows = Math.round(1.2 / sh);
for (let row = 0; row < rows; row++) {
const off = (row % 2) * 0.5;
for (let col = -1; col <= cols; col++) {
const v = 140 + rnd() * 34;
ctx.fillStyle = `rgb(${v | 0},${(v - 2) | 0},${(v - 5) | 0})`;
const x = (col + off) * sw * px + 1.2;
const y = row * sh * px + 1.2;
ctx.fillRect(x, y, sw * px - 2.4, sh * px - 2.4);
export const tegula = () => makeRelief('tegula', 1.2, 0.84, 3.4,
(ctx) => {
const px = SIZE / 1.2;
ctx.fillStyle = 'rgb(104,102,100)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(15);
const sw = 0.15, sh = 0.1;
const cols = Math.round(1.2 / sw), rows = Math.round(1.2 / sh);
for (let row = 0; row < rows; row++) {
const off = (row % 2) * 0.5;
for (let col = -1; col <= cols; col++) {
const v = 138 + rnd() * 36;
ctx.fillStyle = `rgb(${v | 0},${(v - 2) | 0},${(v - 6) | 0})`;
ctx.fillRect((col + off) * sw * px + 1.6, row * sh * px + 1.6, sw * px - 3.2, sh * px - 3.2);
}
}
}
noiseTile(ctx, 25, 24, 0.06);
grain(ctx, 35, 0.09, 1);
});
noiseTile(ctx, 25, 24, 0.06);
grain(ctx, 35, 0.10, 1);
},
(ctx) => {
const px = SIZE / 1.2;
ctx.fillStyle = 'rgb(40,40,40)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(115);
const sw = 0.15, sh = 0.1;
for (let row = 0; row < Math.round(1.2 / sh); row++) {
const off = (row % 2) * 0.5;
for (let col = -1; col <= Math.round(1.2 / sw); col++) {
const v = 200 + rnd() * 45;
ctx.fillStyle = `rgb(${v | 0},${v | 0},${v | 0})`;
ctx.fillRect((col + off) * sw * px + 2.2, row * sh * px + 2.2, sw * px - 4.4, sh * px - 4.4);
}
}
});
/** Verbundsteine des Nachbarn, etwas dunkler als Tegula. */
export const verbundstein = () => make('verbundstein', 1.2, 0.88, (ctx) => {
const px = SIZE / 1.2;
ctx.fillStyle = 'rgb(110,108,106)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(16);
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const v = 126 + rnd() * 30;
ctx.fillStyle = `rgb(${v | 0},${v | 0},${(v - 4) | 0})`;
ctx.fillRect(col * 0.15 * px + 1.5, row * 0.15 * px + 1.5, 0.15 * px - 3, 0.15 * px - 3);
export const verbundstein = () => makeRelief('verbundstein', 1.2, 0.88, 3.0,
(ctx) => {
const px = SIZE / 1.2;
ctx.fillStyle = 'rgb(100,98,96)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(16);
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const v = 124 + rnd() * 32;
ctx.fillStyle = `rgb(${v | 0},${v | 0},${(v - 4) | 0})`;
ctx.fillRect(col * 0.15 * px + 1.8, row * 0.15 * px + 1.8, 0.15 * px - 3.6, 0.15 * px - 3.6);
}
}
}
grain(ctx, 36, 0.08, 1);
});
grain(ctx, 36, 0.08, 1);
},
(ctx) => slabHeight(ctx, 1.2, 0.15, 0.15, 0.008, 0.006));
/** Rasen: gemaehte Flaeche mit leichten Bahnen. */
export const rasen = () => make('rasen', 2.0, 0.95, (ctx) => {
@@ -234,11 +373,11 @@ export const schotterwiese = () => make('schotterwiese', 2.0, 0.98, (ctx) => {
/** Dekorkies in den Pflanzflaechen und um den Flammengrill. */
export const dekorkies = () => make('dekorkies', 1.0, 0.99, (ctx) => {
ctx.fillStyle = 'rgb(150,144,132)';
ctx.fillStyle = 'rgb(118,111,99)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(46);
for (let i = 0; i < 6500; i++) {
const v = 128 + rnd() * 90;
const v = 104 + rnd() * 78;
ctx.fillStyle = `rgb(${v | 0},${(v - 5) | 0},${(v - 16) | 0})`;
ctx.beginPath();
ctx.ellipse(rnd() * SIZE, rnd() * SIZE, 2 + rnd() * 4, 2 + rnd() * 3, rnd() * 3.14, 0, Math.PI * 2);
@@ -276,26 +415,45 @@ export const spielsand = () => make('spielsand', 1.5, 1.0, (ctx) => {
// Bauteile
// ---------------------------------------------------------------------------
/** Travertin-Mauerwerk in Schichten, wie auf den Referenzfotos des Plans. */
export const travertinMauer = () => make('travertinMauer', 1.6, 0.7, (ctx) => {
const px = SIZE / 1.6;
ctx.fillStyle = 'rgb(168,152,126)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(61);
const h = 0.2;
for (let row = 0; row < Math.round(1.6 / h); row++) {
let x = -((row % 2) * 0.3);
while (x < 1.6) {
const w = 0.4 + rnd() * 0.4;
const v = 202 + rnd() * 26;
ctx.fillStyle = `rgb(${v | 0},${(v - 17) | 0},${(v - 44) | 0})`;
ctx.fillRect(x * px + 1, row * h * px + 1, w * px - 2, h * px - 2);
x += w;
/**
* Travertin als Steinoberflaeche - warm, geschichtet, mit den typischen
* Poren. Die Schichtung der Mauer entsteht in walls.ts aus echten Bloecken,
* die Textur liefert nur das Material.
*/
export const travertinMauer = () => makeRelief('travertinMauer', 0.7, 0.68, 1.9,
(ctx) => {
ctx.fillStyle = 'rgb(203,183,148)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(61);
// liegende Sedimentbaender, bewusst zurueckhaltend - sonst wirkt der Stein
// aus der Naehe fleckig statt geschichtet
for (let i = 0; i < 34; i++) {
const y = rnd() * SIZE;
const hgt = 3 + rnd() * 12;
ctx.fillStyle = `rgba(${(188 + rnd() * 20) | 0},${(168 + rnd() * 18) | 0},${(134 + rnd() * 16) | 0},0.35)`;
ctx.beginPath();
ctx.moveTo(0, y);
for (let x = 0; x <= SIZE; x += 24) ctx.lineTo(x, y + Math.sin(x / 55 + i) * 4);
for (let x = SIZE; x >= 0; x -= 24) ctx.lineTo(x, y + hgt + Math.sin(x / 48 + i * 1.7) * 4);
ctx.closePath();
ctx.fill();
}
}
noiseTile(ctx, 62, 40, 0.045);
grain(ctx, 63, 0.05, 2);
});
travertinFinish(ctx, 62);
grain(ctx, 63, 0.05, 2);
},
(ctx) => {
ctx.fillStyle = 'rgb(190,190,190)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(64);
// Poren als feine Vertiefungen
for (let i = 0; i < 2400; i++) {
ctx.fillStyle = `rgba(60,60,60,${0.18 + rnd() * 0.32})`;
ctx.beginPath();
ctx.ellipse(rnd() * SIZE, rnd() * SIZE, 0.8 + rnd() * 2.2, 0.6 + rnd() * 1.1, rnd() * 0.7, 0, Math.PI * 2);
ctx.fill();
}
noiseTile(ctx, 65, 96, 0.07);
});
/** Sichtbeton der Winkelelemente und der Nachbarmauer. */
export const beton = () => make('beton', 2.0, 0.8, (ctx) => {
@@ -340,28 +498,46 @@ export const holzscheiter = () => make('holzscheiter', 1.0, 0.9, (ctx) => {
});
/** Vertikale Holzschalung der Fassade - graubraun vergraut, wie auf den Fotos. */
export const fassadeHolz = () => make('fassadeHolz', 1.2, 0.78, (ctx) => {
const px = SIZE / 1.2;
ctx.fillStyle = 'rgb(124,114,100)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(71);
const w = 0.06;
for (let col = 0; col < Math.round(1.2 / w); col++) {
const v = 152 + rnd() * 32;
ctx.fillStyle = `rgb(${v | 0},${(v - 8) | 0},${(v - 20) | 0})`;
ctx.fillRect(col * w * px + 1.5, 0, w * px - 3, SIZE);
ctx.strokeStyle = 'rgba(84,76,66,0.45)';
ctx.lineWidth = 1;
for (let i = 0; i < 14; i++) {
const y = rnd() * SIZE;
ctx.beginPath();
ctx.moveTo(col * w * px + 2, y);
ctx.lineTo(col * w * px + w * px - 3, y + (rnd() - 0.5) * 5);
ctx.stroke();
export const fassadeHolz = () => makeRelief('fassadeHolz', 1.2, 0.78, 3.6,
(ctx) => {
const px = SIZE / 1.2;
ctx.fillStyle = 'rgb(96,88,76)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(71);
const w = 0.06;
for (let col = 0; col < Math.round(1.2 / w); col++) {
const v = 150 + rnd() * 34;
ctx.fillStyle = `rgb(${v | 0},${(v - 9) | 0},${(v - 22) | 0})`;
ctx.fillRect(col * w * px + 1.5, 0, w * px - 3, SIZE);
// Maserung in Faserrichtung
ctx.strokeStyle = 'rgba(84,76,66,0.42)';
ctx.lineWidth = 1;
for (let i = 0; i < 16; i++) {
const y = rnd() * SIZE;
ctx.beginPath();
ctx.moveTo(col * w * px + 2, y);
ctx.lineTo(col * w * px + w * px - 3, y + (rnd() - 0.5) * 6);
ctx.stroke();
}
}
}
noiseTile(ctx, 72, 32, 0.03);
});
noiseTile(ctx, 72, 32, 0.035);
},
(ctx) => {
// Hoehenbild: jede Latte gewoelbt, dazwischen die Schattenfuge
const px = SIZE / 1.2;
ctx.fillStyle = 'rgb(30,30,30)';
ctx.fillRect(0, 0, SIZE, SIZE);
const w = 0.06;
for (let col = 0; col < Math.round(1.2 / w); col++) {
const x0 = col * w * px + 2.2, wPx = w * px - 4.4;
const g = ctx.createLinearGradient(x0, 0, x0 + wPx, 0);
g.addColorStop(0, 'rgb(150,150,150)');
g.addColorStop(0.5, 'rgb(240,240,240)');
g.addColorStop(1, 'rgb(150,150,150)');
ctx.fillStyle = g;
ctx.fillRect(x0, 0, wPx, SIZE);
}
});
/** Heller Betonsockel unter der Holzschalung. */
export const sockel = () => make('sockel', 2.0, 0.72, (ctx) => {
@@ -399,3 +575,82 @@ export const spritzschutz = () => make('spritzschutz', 1.0, 0.98, (ctx) => {
ctx.fill();
}
});
/**
* Rohboden mit spaerlichem Bewuchs. Das ist der Grundton des Gelaendes -
* die eigentlichen Flaechen des Plans liegen als eigene Meshes darueber.
*/
export const rohboden = () => make('rohboden', 2.0, 0.99, (ctx) => {
ctx.fillStyle = 'rgb(104,122,68)';
ctx.fillRect(0, 0, SIZE, SIZE);
noiseTile(ctx, 81, 14, 0.11);
noiseTile(ctx, 82, 56, 0.06);
const rnd = mulberry32(83);
// Wiesenhalme, unregelmaessiger und matter als der gemaehte Rasen
for (let i = 0; i < 4200; i++) {
const x = rnd() * SIZE, y = rnd() * SIZE;
const v = rnd();
ctx.strokeStyle = `rgba(${(88 + v * 66) | 0},${(112 + v * 62) | 0},${(52 + v * 44) | 0},0.6)`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + (rnd() - 0.5) * 4, y - 3 - rnd() * 5);
ctx.stroke();
}
// vereinzelt offene Bodenstellen
for (let i = 0; i < 500; i++) {
ctx.fillStyle = `rgba(122,110,84,${0.15 + rnd() * 0.25})`;
ctx.beginPath();
ctx.arc(rnd() * SIZE, rnd() * SIZE, 2 + rnd() * 6, 0, Math.PI * 2);
ctx.fill();
}
grain(ctx, 84, 0.04, 1);
});
/**
* Laub fuer geschnittene Hecken: dichte, kleinblaettrige Oberflaeche mit
* Relief. Ohne das wirken die Heckenkoerper wie lackierte Bloecke.
*/
function laub(key: string, base: string, seed: number, leaf: number): Belag {
return makeRelief(key, 0.55, 0.97, 2.6,
(ctx) => {
ctx.fillStyle = base;
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(seed);
const [r, g, b] = base.match(/\d+/g)!.map(Number);
for (let i = 0; i < 5200; i++) {
const v = (rnd() - 0.45) * 70;
ctx.fillStyle = `rgb(${Math.max(0, Math.min(255, r + v)) | 0},${Math.max(0, Math.min(255, g + v * 1.15)) | 0},${Math.max(0, Math.min(255, b + v * 0.7)) | 0})`;
ctx.save();
ctx.translate(rnd() * SIZE, rnd() * SIZE);
ctx.rotate(rnd() * Math.PI);
ctx.beginPath();
ctx.ellipse(0, 0, leaf * (0.7 + rnd() * 0.6), leaf * (0.4 + rnd() * 0.35), 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
noiseTile(ctx, seed + 5, 12, 0.10);
},
(ctx) => {
ctx.fillStyle = 'rgb(120,120,120)';
ctx.fillRect(0, 0, SIZE, SIZE);
const rnd = mulberry32(seed + 11);
for (let i = 0; i < 3600; i++) {
const v = 90 + rnd() * 150;
ctx.fillStyle = `rgb(${v | 0},${v | 0},${v | 0})`;
ctx.save();
ctx.translate(rnd() * SIZE, rnd() * SIZE);
ctx.rotate(rnd() * Math.PI);
ctx.beginPath();
ctx.ellipse(0, 0, leaf * 0.9, leaf * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
noiseTile(ctx, seed + 13, 10, 0.16);
});
}
/** Eibe: dunkel, feinnadelig. */
export const laubEibe = () => laub('laubEibe', 'rgb(64,92,52)', 201, 3.4);
/** Liguster: mittelgruen, groesseres Blatt. */
export const laubLiguster = () => laub('laubLiguster', 'rgb(85,118,55)', 211, 5.2);