Skip to content

Commit 28c758d

Browse files
AzgaarCopilot
andcommitted
refactor: enhance coastline settings with new presets and update fractalization logic
Co-authored-by: Copilot <copilot@github.com>
1 parent 937d5c1 commit 28c758d

3 files changed

Lines changed: 224 additions & 54 deletions

File tree

src/controllers/coastline-editor.ts

Lines changed: 187 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Alea from "alea";
22
import {
33
buildCoastlinePath,
44
type CoastlineSettings,
5-
coastSettings,
5+
defaultCoastSettings,
66
fractalize,
77
makeRoughnessProfile,
88
PROFILE_SIZE
@@ -16,7 +16,7 @@ interface SliderDef {
1616
min: number;
1717
max: number;
1818
step: number;
19-
key: keyof CoastlineSettings;
19+
key: keyof Omit<CoastlineSettings, "enabled">;
2020
}
2121

2222
const SLIDER_DEFS: SliderDef[] = [
@@ -73,10 +73,74 @@ const SLIDER_DEFS: SliderDef[] = [
7373
max: 10,
7474
step: 0.1,
7575
key: "roughnessContrast"
76+
},
77+
{
78+
id: "coastProfileHarmonics",
79+
label: "Roughness zones",
80+
tip: "Number of cosine harmonics shaping the roughness envelope. 1 = one large concentrated patch; 8 = many small scattered zones.",
81+
min: 1,
82+
max: 8,
83+
step: 1,
84+
key: "profileHarmonics"
85+
},
86+
{
87+
id: "coastLakeSmoothThreshMult",
88+
label: "Lake smooth multiplier",
89+
tip: "Smooth-threshold multiplier for lake shores. 1 = same roughness as ocean.",
90+
min: 0.1,
91+
max: 5,
92+
step: 0.1,
93+
key: "lakeSmoothThreshMult"
7694
}
7795
];
7896

79-
const PREVIEW_SEED = "preview_coastline_42";
97+
const COAST_PRESETS: Record<string, Omit<CoastlineSettings, "enabled">> = {
98+
Default: {
99+
...defaultCoastSettings,
100+
},
101+
Smooth: {
102+
maxDepth: 3,
103+
baseAmplitude: 1,
104+
amplitudeDecay: 0.6,
105+
minEdge: 1,
106+
smoothThreshold: 0.3,
107+
roughnessContrast: 2.0,
108+
profileHarmonics: 1,
109+
lakeSmoothThreshMult: 3.0,
110+
},
111+
Rocky: {
112+
maxDepth: 4,
113+
baseAmplitude: 3.0,
114+
amplitudeDecay: 0.7,
115+
minEdge: 0.5,
116+
smoothThreshold: 0.05,
117+
roughnessContrast: 0.8,
118+
profileHarmonics: 7,
119+
lakeSmoothThreshMult: 1.2,
120+
},
121+
Fjords: {
122+
maxDepth: 4,
123+
baseAmplitude: 2.8,
124+
amplitudeDecay: 0.92,
125+
minEdge: 0.3,
126+
smoothThreshold: 0.25,
127+
roughnessContrast: 5.0,
128+
profileHarmonics: 2,
129+
lakeSmoothThreshMult: 2.5,
130+
},
131+
Archipelago: {
132+
maxDepth: 4,
133+
baseAmplitude: 1.8,
134+
amplitudeDecay: 0.88,
135+
minEdge: 0.5,
136+
smoothThreshold: 0.18,
137+
roughnessContrast: 1.0,
138+
profileHarmonics: 8,
139+
lakeSmoothThreshMult: 1.5,
140+
},
141+
};
142+
143+
const PREVIEW_SEED = "preview_coastline";
80144

81145
export function open(): void {
82146
if (!byId("coastlineSettingsDialog")) {
@@ -90,50 +154,102 @@ export function open(): void {
90154

91155
if (!slider || !output || !resetBtn) continue;
92156

93-
const defaultVal = coastSettings[key] as number;
157+
const defaultVal = defaultCoastSettings[key] as number;
94158

95159
slider.on("input", () => {
96160
const value = slider.valueAsNumber;
97-
coastSettings[key] = value;
161+
defaultCoastSettings[key] = value;
98162
output.textContent = String(value);
99163
updatePreviews();
100164
drawFeatures();
101165
});
102166

103167
resetBtn.on("click", () => {
104-
(coastSettings[key] as number) = defaultVal;
168+
(defaultCoastSettings[key] as number) = defaultVal;
105169
slider.value = String(defaultVal);
106170
output.textContent = String(defaultVal);
107171
updatePreviews();
108172
drawFeatures();
109173
});
110174
}
111175

176+
const enabledCb = byId("coastEnabled") as HTMLInputElement | null;
177+
const slidersDiv = byId("coastSliders") as HTMLElement | null;
178+
const track = byId("coastEnabledTrack") as HTMLElement | null;
179+
const thumb = byId("coastEnabledThumb") as HTMLElement | null;
180+
if (!enabledCb || !slidersDiv || !track || !thumb) return;
181+
182+
enabledCb.checked = defaultCoastSettings.enabled;
183+
const syncToggle = () => {
184+
track.style.background = defaultCoastSettings.enabled ? "#33bb88" : "#bbb";
185+
thumb.style.left = defaultCoastSettings.enabled ? "18px" : "2px";
186+
slidersDiv.style.opacity = defaultCoastSettings.enabled ? "" : "0.4";
187+
slidersDiv.style.pointerEvents = defaultCoastSettings.enabled ? "" : "none";
188+
Object.keys(COAST_PRESETS).forEach(name => {
189+
const btn = byId(`coastPreset_${name}`) as HTMLButtonElement | null;
190+
if (btn) btn.disabled = !defaultCoastSettings.enabled;
191+
});
192+
};
193+
194+
syncToggle();
195+
enabledCb.on("change", () => {
196+
defaultCoastSettings.enabled = enabledCb.checked;
197+
syncToggle();
198+
updatePreviews();
199+
drawFeatures();
200+
});
201+
202+
// Preset buttons
203+
for (const name of Object.keys(COAST_PRESETS)) {
204+
const btn = byId(`coastPreset_${name}`) as HTMLButtonElement | null;
205+
if (!btn) continue;
206+
btn.on("click", () => {
207+
const preset = COAST_PRESETS[name];
208+
for (const {id, key} of SLIDER_DEFS) {
209+
if (!(key in preset)) continue;
210+
const val = preset[key as keyof typeof preset];
211+
defaultCoastSettings[key] = val;
212+
const slider = byId(id) as HTMLInputElement | null;
213+
const output = byId(`${id}Out`) as HTMLElement | null;
214+
if (slider) slider.value = String(val);
215+
if (output) output.textContent = String(val);
216+
}
217+
updatePreviews();
218+
drawFeatures();
219+
});
220+
}
221+
112222
updatePreviews();
113223
closeDialogs("#culturesEditor, .stable");
114224

115225
$("#coastlineSettingsDialog").dialog({
116226
title: "Coastline Settings Editor",
117227
resizable: false,
118-
width: "auto",
228+
width: 'auto',
119229
position: {my: "right top", at: "right-10 top+10", of: "svg"}
120230
});
121231
}
122232

123233
function buildDialogHTML(): string {
234+
const presetButtons = Object.keys(COAST_PRESETS)
235+
.map(
236+
name => `<button id="coastPreset_${name}" style="font-size:.78em;padding:2px 8px">${name}</button>`
237+
)
238+
.join("");
239+
124240
const rows = SLIDER_DEFS.map(({id, label, tip, min, max, step, key}) => {
125-
const value = coastSettings[key];
241+
const value = defaultCoastSettings[key];
126242
return /* html */ `
127243
<tr data-tip="${tip}">
128-
<td style="padding:4px 8px;white-space:nowrap">${label}</td>
129-
<td style="padding:4px 4px">
244+
<td style="padding:2px 0;white-space:nowrap">${label}</td>
245+
<td style="padding:2px 4px">
130246
<input id="${id}" type="range" min="${min}" max="${max}" step="${step}" value="${value}"
131247
style="width:160px;vertical-align:middle"/>
132248
</td>
133-
<td style="padding:4px 6px;min-width:2.8em;text-align:right">
249+
<td style="padding:2px 6px;min-width:2em;text-align:right">
134250
<span id="${id}Out" style="font-family:monospace;font-size:.85em">${value}</span>
135251
</td>
136-
<td style="padding:4px 4px">
252+
<td style="padding:2px 0">
137253
<button id="${id}Reset" title="Reset to default"
138254
style="font-size:.75em;padding:1px 5px;cursor:pointer">↺</button>
139255
</td>
@@ -142,19 +258,32 @@ function buildDialogHTML(): string {
142258

143259
return /* html */ `
144260
<div id="coastlineSettingsDialog" style="display:none">
145-
<table style="border-collapse:collapse;width:100%">
146-
<tbody>${rows}</tbody>
147-
</table>
261+
<div style="display:flex;justify-content:space-between;gap:10px;margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid #ddd">
262+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none" data-tip="Enable or disable coastline fractalization. When disabled, coastlines are simple arcs between feature vertices. Enabling adds naturalistic roughness but can increase rendering time, especially at high detail levels.">
263+
<input id="coastEnabled" type="checkbox" ${defaultCoastSettings.enabled ? "checked" : ""}
264+
style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"/>
265+
<span id="coastEnabledTrack" style="position:relative;display:inline-block;width:36px;height:20px;border-radius:10px;background:${defaultCoastSettings.enabled ? "#33bb88" : "#bbb"};cursor:pointer;flex-shrink:0">
266+
<span id="coastEnabledThumb" style="position:absolute;top:2px;left:${defaultCoastSettings.enabled ? "18px" : "2px"};width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.3)"></span>
267+
</span>
268+
</label>
269+
<div style="display:flex;align-items:center;gap:4px">
270+
<span style="color:#999;font-size:.85em">Preset</span>
271+
${presetButtons}
272+
</div>
273+
</div>
274+
<div id="coastSliders">
275+
<table style="border-collapse:collapse;width:100%">
276+
<tbody>${rows}</tbody>
277+
</table>
278+
</div>
148279
<div style="display:flex;gap:6px;margin-top:10px;align-items:flex-start">
149280
<div style="flex:1;min-width:0">
150-
<div style="font-size:.72em;color:#999;margin-bottom:3px">Roughness profile</div>
151-
<canvas id="coastRoughnessGraph" width="266" height="100"
152-
style="border:1px solid #ccc;border-radius:2px;display:block"></canvas>
281+
<div style="color:#999;font-size:.85em;margin-bottom:3px">Roughness profile</div>
282+
<canvas id="coastRoughnessGraph" width="auto" height="100" style="display:block"></canvas>
153283
</div>
154284
<div>
155-
<div style="font-size:.72em;color:#999;margin-bottom:3px">Shape preview</div>
156-
<canvas id="coastShapePreview" width="100" height="100"
157-
style="border:1px solid #ccc;border-radius:2px;display:block"></canvas>
285+
<div style="color:#999;font-size:.85em;margin-bottom:3px">Shape preview</div>
286+
<canvas id="coastShapePreview" width="100" height="100" style="display:block"></canvas>
158287
</div>
159288
</div>
160289
</div>`;
@@ -174,24 +303,18 @@ function drawRoughnessGraph(canvas: HTMLCanvasElement): void {
174303
ctx.clearRect(0, 0, W, H);
175304

176305
const rand = Alea(PREVIEW_SEED);
177-
const profile = makeRoughnessProfile(rand, coastSettings.roughnessContrast);
178-
179-
const pl = 2,
180-
pr = 2,
181-
pt = 6,
182-
pb = 6;
183-
const gW = W - pl - pr;
184-
const gH = H - pt - pb;
185-
const thresh = Math.min(Math.max(coastSettings.smoothThreshold, 0), 1);
186-
const threshY = pt + gH * (1 - thresh);
187-
const baseY = pt + gH;
306+
const profile = makeRoughnessProfile(rand, defaultCoastSettings.roughnessContrast, defaultCoastSettings.profileHarmonics);
307+
308+
const thresh = Math.min(Math.max(defaultCoastSettings.smoothThreshold, 0), 1);
309+
const threshY = H * (1 - thresh);
310+
const baseY = H;
188311

189312
// Pre-compute curve points
190313
const xs: number[] = [];
191314
const ys: number[] = [];
192315
for (let i = 0; i <= PROFILE_SIZE; i++) {
193-
xs.push(pl + (i / PROFILE_SIZE) * gW);
194-
ys.push(pt + gH * (1 - profile[i % PROFILE_SIZE]));
316+
xs.push((i / PROFILE_SIZE) * W);
317+
ys.push(H * (1 - profile[i % PROFILE_SIZE]));
195318
}
196319

197320
// Helper: fill area under curve clipped to a horizontal band
@@ -200,7 +323,7 @@ function drawRoughnessGraph(canvas: HTMLCanvasElement): void {
200323
if (h <= 0) return;
201324
ctx.save();
202325
ctx.beginPath();
203-
ctx.rect(pl, clipTop, gW, h);
326+
ctx.rect(0, clipTop, W, h);
204327
ctx.clip();
205328
ctx.beginPath();
206329
ctx.moveTo(xs[0], ys[0]);
@@ -219,7 +342,7 @@ function drawRoughnessGraph(canvas: HTMLCanvasElement): void {
219342
if (h <= 0) return;
220343
ctx.save();
221344
ctx.beginPath();
222-
ctx.rect(pl, clipTop, gW, h);
345+
ctx.rect(0, clipTop, W, h);
223346
ctx.clip();
224347
ctx.beginPath();
225348
ctx.moveTo(xs[0], ys[0]);
@@ -231,8 +354,8 @@ function drawRoughnessGraph(canvas: HTMLCanvasElement): void {
231354
};
232355

233356
// Rough zone (above threshold): warm orange
234-
fillBand(pt, threshY, "rgba(210,90,30,0.20)");
235-
strokeBand(pt, threshY, "#c85520");
357+
fillBand(0, threshY, "rgba(210,90,30,0.20)");
358+
strokeBand(0, threshY, "#c85520");
236359

237360
// Smooth zone (below threshold): cool teal
238361
fillBand(threshY, baseY, "rgba(30,165,135,0.20)");
@@ -242,8 +365,8 @@ function drawRoughnessGraph(canvas: HTMLCanvasElement): void {
242365
ctx.save();
243366
ctx.beginPath();
244367
ctx.setLineDash([4, 3]);
245-
ctx.moveTo(pl, threshY);
246-
ctx.lineTo(W - pr, threshY);
368+
ctx.moveTo(0, threshY);
369+
ctx.lineTo(W, threshY);
247370
ctx.strokeStyle = "rgba(30,140,100,0.75)";
248371
ctx.lineWidth = 1;
249372
ctx.stroke();
@@ -253,13 +376,19 @@ function drawRoughnessGraph(canvas: HTMLCanvasElement): void {
253376
// Zone labels
254377
ctx.font = "bold 8px sans-serif";
255378
ctx.textAlign = "left";
256-
if (threshY > pt + 12) {
379+
if (threshY > 12) {
257380
ctx.fillStyle = "#c85520";
258-
ctx.fillText("ROUGH", pl + 3, pt + 9);
381+
ctx.fillText("ROUGH", 12, 11);
259382
}
260383
if (baseY - threshY > 10) {
261384
ctx.fillStyle = "#18a888";
262-
ctx.fillText("CALM", pl + 3, baseY - 2);
385+
ctx.fillText("CALM", 12, baseY - 4);
386+
}
387+
388+
if (!defaultCoastSettings.enabled) {
389+
ctx.fillStyle = "rgba(0,0,0,0.38)";
390+
ctx.fillRect(0, 0, W, H);
391+
ctx.fillStyle = "#fff";
263392
}
264393
}
265394

@@ -281,7 +410,9 @@ function drawShapePreview(canvas: HTMLCanvasElement): void {
281410
[cx - r, cy] // left
282411
];
283412

284-
const shape = fractalize(basePts, Alea(PREVIEW_SEED), coastSettings);
413+
const shape = defaultCoastSettings.enabled
414+
? fractalize(basePts, Alea(PREVIEW_SEED), defaultCoastSettings)
415+
: {points: basePts, origIndices: [0, 1, 2, 3]};
285416
const path = new Path2D(`${buildCoastlinePath(shape)}Z`);
286417

287418
// Ocean background — radial gradient, lighter at centre
@@ -335,6 +466,18 @@ function drawShapePreview(canvas: HTMLCanvasElement): void {
335466
ctx.lineWidth = 0.8;
336467
ctx.stroke();
337468
}
469+
470+
if (!defaultCoastSettings.enabled) {
471+
ctx.fillStyle = "rgba(0,0,0,0.38)";
472+
ctx.fillRect(0, 0, W, H);
473+
ctx.fillStyle = "#fff";
474+
ctx.font = "bold 11px sans-serif";
475+
ctx.textAlign = "center";
476+
ctx.textBaseline = "middle";
477+
ctx.fillText("OFF", cx, cy);
478+
ctx.textBaseline = "alphabetic";
479+
ctx.textAlign = "left";
480+
}
338481
}
339482

340483
declare global {

0 commit comments

Comments
 (0)