@@ -2,7 +2,7 @@ import Alea from "alea";
22import {
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
2222const 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
81145export 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
123233function 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
340483declare global {
0 commit comments