nx_gradient_blend.nx source
↩ module page · 267 lines · 12366 B
1// nx_gradient_blend.nx -- smooth zone-to-zone transition primitive.
2//
3// Sixth demonstration of the kind-specific-generator cardinal
4// `feedback-kind-specific-generators-not-broad-noise`. Solves the
5// "biome edges are sharp cliffs" problem: when two zones (biomes,
6// elevation bands, climate regions, kind-generators) meet, they
7// SHOULDN'T abruptly snap from one to the other. Real-world biome
8// transitions are gradients over kilometres of varying ecological
9// pressure. This primitive ships the interpolation kernels.
10//
11// Caller pattern:
12// blended = nx_gradient_blend_q14(value_a, value_b, progress, curve)
13// where progress in [0, Q] (0 = pure A, Q = pure B).
14//
15// Sealed-enum easing curves give different aesthetic options:
16// LINEAR -- straight line, equal-weight blend
17// SMOOTHSTEP -- cubic Hermite, smooth at both ends (Perlin's
18// classic fade curve)
19// EASE_IN_OUT -- slow start + slow end (s-curve)
20// SHARP -- nearly-step but with thin transition band
21// CRENULATE -- alternating pattern (used for forest/grassland
22// transitions that look "fingery" rather than a
23// smooth line)
24//
25// V2 follow-ups (queued):
26// - 2D progress (perpendicular + along the transition line)
27// - Noise-perturbed transition for organic edges (composes
28// nx_perlin or nx_worley_noise to roughen the transition line)
29// - Catmull-Rom and bezier easing for N-way blends
30// - Multi-zone blending (3+ neighbours)
31// - "Fingery" forest edge via L-system extension
32//
33// Loss audit: Q14 integer arithmetic; cubic and s-curve evaluation
34// uses 64-bit intermediates to avoid overflow. Clamping at boundary
35// values to keep output bounded. CRENULATE relies on hash for
36// determinism.
37//
38// genealogy_id: perlin_2002_improving_noise + ken_perlin_smoothstep +
39// whittaker_1975_community_ecology
40// lineage_id: nx_gradient_blend_5curve_q14_v1
41
42// nx_safety_envelope:
43// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
44// sil_target: SIL1
45// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
46// verdict: NOT_YET_EVALUATED
47
48import "nx_syscalls.nx"
49import "nx_tier.nx"
50const NX_MAGIC_2654435761: i64 = 2654435761
51
52// ===== Q14 ==========================================================
53const NX_GB_Q: nx_int = 16384
54
55// Park-Miller LCG (for CRENULATE).
56const NX_GB_LCG_A: nx_int = 48271
57const NX_GB_LCG_M: nx_int = 2147483647
58
59// ===== Curve sealed enum ============================================
60const NX_GB_CURVE_LINEAR: nx_int = 0
61const NX_GB_CURVE_SMOOTHSTEP: nx_int = 1
62const NX_GB_CURVE_EASE_IN_OUT: nx_int = 2
63const NX_GB_CURVE_SHARP: nx_int = 3
64const NX_GB_CURVE_CRENULATE: nx_int = 4
65
66const NX_GB_CURVE_COUNT: nx_int = 5
67
68// ===== Validity predicate ===========================================
69func nx_gb_curve_is_valid(c: nx_int) -> nx_int {
70 if c == NX_GB_CURVE_LINEAR { return 1 }
71 if c == NX_GB_CURVE_SMOOTHSTEP { return 1 }
72 if c == NX_GB_CURVE_EASE_IN_OUT { return 1 }
73 if c == NX_GB_CURVE_SHARP { return 1 }
74 if c == NX_GB_CURVE_CRENULATE { return 1 }
75 return 0
76}
77
78// ===== Curve transforms ==============================================
79// Each function takes Q14 progress in [0, Q] and returns Q14 weight
80// in [0, Q]. The blend then = a + (b - a) * weight / Q.
81
82// LINEAR: identity.
83func _gb_curve_linear(t: nx_int) -> nx_int { return t }
84
85// SMOOTHSTEP: classic 3t^2 - 2t^3. In Q14:
86// t_q = t in [0, Q]
87// t_norm_sq = t_q * t_q / Q (Q14)
88// t_norm_cube = t_norm_sq * t_q / Q
89// result = 3 * t_norm_sq - 2 * t_norm_cube
90func _gb_curve_smoothstep(t: nx_int) -> nx_int {
91 let q: nx_int = NX_GB_Q
92 if t <= 0 { return 0 }
93 if t >= q { return q }
94 let t_sq: nx_int = t * t / q
95 let t_cube: nx_int = t_sq * t / q
96 return 3 * t_sq - 2 * t_cube
97}
98
99// EASE_IN_OUT: stronger s-curve via 6t^5 - 15t^4 + 10t^3 (Perlin
100// improved fade).
101func _gb_curve_ease_in_out(t: nx_int) -> nx_int {
102 let q: nx_int = NX_GB_Q
103 if t <= 0 { return 0 }
104 if t >= q { return q }
105 let t2: nx_int = t * t / q
106 let t3: nx_int = t2 * t / q
107 let t4: nx_int = t3 * t / q
108 let t5: nx_int = t4 * t / q
109 return 6 * t5 - 15 * t4 + 10 * t3
110}
111
112// SHARP: thin transition band; almost-step. Implemented as a clamped
113// remap centred on t=0.5, width 0.1 (10% of full range).
114func _gb_curve_sharp(t: nx_int) -> nx_int {
115 let q: nx_int = NX_GB_Q
116 // Map t in [0.45Q, 0.55Q] to [0, Q]; below = 0; above = Q.
117 let lo: nx_int = q * 45 / 100
118 let hi: nx_int = q * 55 / 100
119 if t <= lo { return 0 }
120 if t >= hi { return q }
121 return (t - lo) * q / (hi - lo)
122}
123
124// CRENULATE: hash-perturbed transition for "fingery" edges. Caller
125// must supply a SECOND parameter (perpendicular_q14) which is used
126// to hash-jitter the effective progress so the transition isn't a
127// straight line. V1 simplification: uses a sin-like wobble of
128// progress proportional to the perpendicular coordinate.
129//
130// This curve has a SEPARATE entry below (nx_gradient_crenulate_q14)
131// because the standard nx_gradient_blend_q14 doesn't carry the
132// perpendicular param.
133func _gb_curve_crenulate_fallback(t: nx_int) -> nx_int {
134 // Without perpendicular context, fall back to SMOOTHSTEP.
135 return _gb_curve_smoothstep(t)
136}
137
138// ===== Public: standard blend ======================================
139// Input: a, b (any Q14 value), progress (Q14 [0, Q]), curve enum.
140// Output: a + (b - a) * curve(progress) / Q. Clamped if progress out
141// of band.
142func nx_gradient_blend_q14(
143 a: nx_int,
144 b: nx_int,
145 progress_q14: nx_int,
146 curve: nx_int
147) -> nx_int {
148 if nx_gb_curve_is_valid(curve) == 0 { return a }
149 // Clamp progress.
150 var t: nx_int = progress_q14
151 if t < 0 { t = 0 }
152 if t > NX_GB_Q { t = NX_GB_Q }
153
154 var weight: nx_int = t
155 if curve == NX_GB_CURVE_LINEAR { weight = _gb_curve_linear(t) }
156 if curve == NX_GB_CURVE_SMOOTHSTEP { weight = _gb_curve_smoothstep(t) }
157 if curve == NX_GB_CURVE_EASE_IN_OUT { weight = _gb_curve_ease_in_out(t) }
158 if curve == NX_GB_CURVE_SHARP { weight = _gb_curve_sharp(t) }
159 if curve == NX_GB_CURVE_CRENULATE { weight = _gb_curve_crenulate_fallback(t) }
160
161 return a + (b - a) * weight / NX_GB_Q
162}
163
164// ===== Crenulate blend with perpendicular jitter ===================
165// Hash-perturbs the progress by a fraction of perpendicular_q14 so
166// the transition between A and B forms organic "fingers" rather than
167// a clean line. Useful for forest/grassland edges.
168func nx_gradient_blend_crenulate_q14(
169 a: nx_int,
170 b: nx_int,
171 progress_q14: nx_int,
172 perpendicular_q14: nx_int,
173 finger_amplitude_q14: nx_int,
174 seed: nx_int
175) -> nx_int {
176 // Hash the perpendicular coordinate -> signed jitter in [-Q, Q].
177 var h: nx_int = seed
178 h = (h * NX_GB_LCG_A + perpendicular_q14 * NX_MAGIC_2654435761) % NX_GB_LCG_M
179 if h < 0 { h = h + NX_GB_LCG_M }
180 let jitter: nx_int = (h % (2 * NX_GB_Q)) - NX_GB_Q
181
182 // Modulate progress by finger amplitude.
183 let progress_adj: nx_int = progress_q14 + jitter * finger_amplitude_q14 / NX_GB_Q
184
185 return nx_gradient_blend_q14(a, b, progress_adj, NX_GB_CURVE_SMOOTHSTEP)
186}
187
188// ===== Self-test ====================================================
189func main() -> i64 {
190 let q: nx_int = NX_GB_Q
191
192 // T1: Validity predicate.
193 if nx_gb_curve_is_valid(NX_GB_CURVE_LINEAR) != 1 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
194 if nx_gb_curve_is_valid(NX_GB_CURVE_SMOOTHSTEP) != 1 { return __syscall(93, 2, 0, 0, 0, 0, 0) }
195 if nx_gb_curve_is_valid(NX_GB_CURVE_EASE_IN_OUT) != 1 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
196 if nx_gb_curve_is_valid(NX_GB_CURVE_SHARP) != 1 { return __syscall(93, 4, 0, 0, 0, 0, 0) }
197 if nx_gb_curve_is_valid(NX_GB_CURVE_CRENULATE) != 1 { return __syscall(93, 5, 0, 0, 0, 0, 0) }
198 if nx_gb_curve_is_valid(99) != 0 { return __syscall(93, 6, 0, 0, 0, 0, 0) }
199
200 // T2: LINEAR -- exact identity weight.
201 if nx_gradient_blend_q14(100, 200, 0, NX_GB_CURVE_LINEAR) != 100 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
202 if nx_gradient_blend_q14(100, 200, q, NX_GB_CURVE_LINEAR) != 200 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
203 if nx_gradient_blend_q14(100, 200, q / 2, NX_GB_CURVE_LINEAR) != 150 { return __syscall(93, 12, 0, 0, 0, 0, 0) }
204
205 // T3: SMOOTHSTEP -- weight at t=0.5 is 0.5 (3*0.25 - 2*0.125 = 0.5).
206 let s_half: nx_int = nx_gradient_blend_q14(0, q, q / 2, NX_GB_CURVE_SMOOTHSTEP)
207 // Expected 0.5 * Q = 8192; allow +/- 5 for integer rounding.
208 if s_half < q / 2 - 5 { return __syscall(93, 20, 0, 0, 0, 0, 0) }
209 if s_half > q / 2 + 5 { return __syscall(93, 21, 0, 0, 0, 0, 0) }
210 // SMOOTHSTEP at t=0.25: 3*0.0625 - 2*0.015625 = 0.15625. Less
211 // than linear's 0.25.
212 let s_quarter: nx_int = nx_gradient_blend_q14(0, q, q / 4, NX_GB_CURVE_SMOOTHSTEP)
213 let l_quarter: nx_int = nx_gradient_blend_q14(0, q, q / 4, NX_GB_CURVE_LINEAR)
214 if s_quarter >= l_quarter { return __syscall(93, 22, 0, 0, 0, 0, 0) }
215 // Endpoints exact.
216 if nx_gradient_blend_q14(0, q, 0, NX_GB_CURVE_SMOOTHSTEP) != 0 { return __syscall(93, 23, 0, 0, 0, 0, 0) }
217 if nx_gradient_blend_q14(0, q, q, NX_GB_CURVE_SMOOTHSTEP) != q { return __syscall(93, 24, 0, 0, 0, 0, 0) }
218
219 // T4: EASE_IN_OUT -- at t=0.5, weight = 6*0.03125 - 15*0.0625 +
220 // 10*0.125 = 0.1875 - 0.9375 + 1.25 = 0.5.
221 let e_half: nx_int = nx_gradient_blend_q14(0, q, q / 2, NX_GB_CURVE_EASE_IN_OUT)
222 if e_half < q / 2 - 5 { return __syscall(93, 30, 0, 0, 0, 0, 0) }
223 if e_half > q / 2 + 5 { return __syscall(93, 31, 0, 0, 0, 0, 0) }
224 // Steeper than SMOOTHSTEP near the middle -- at t=0.6, EASE_IN_OUT
225 // is closer to 1 than SMOOTHSTEP is.
226 let e_06: nx_int = nx_gradient_blend_q14(0, q, q * 6 / 10, NX_GB_CURVE_EASE_IN_OUT)
227 let s_06: nx_int = nx_gradient_blend_q14(0, q, q * 6 / 10, NX_GB_CURVE_SMOOTHSTEP)
228 if e_06 <= s_06 { return __syscall(93, 32, 0, 0, 0, 0, 0) }
229
230 // T5: SHARP -- near-step at t=0.5.
231 // Below 0.45: 0. Above 0.55: Q. At 0.5: 0.5*Q.
232 if nx_gradient_blend_q14(0, q, q * 40 / 100, NX_GB_CURVE_SHARP) != 0 { return __syscall(93, 40, 0, 0, 0, 0, 0) }
233 if nx_gradient_blend_q14(0, q, q * 60 / 100, NX_GB_CURVE_SHARP) != q { return __syscall(93, 41, 0, 0, 0, 0, 0) }
234 let sh_half: nx_int = nx_gradient_blend_q14(0, q, q / 2, NX_GB_CURVE_SHARP)
235 if sh_half < q / 2 - 100 { return __syscall(93, 42, 0, 0, 0, 0, 0) }
236 if sh_half > q / 2 + 100 { return __syscall(93, 43, 0, 0, 0, 0, 0) }
237
238 // T6: Clamping out-of-band progress.
239 if nx_gradient_blend_q14(100, 200, 0 - 5 * q, NX_GB_CURVE_LINEAR) != 100 { return __syscall(93, 50, 0, 0, 0, 0, 0) }
240 if nx_gradient_blend_q14(100, 200, 99 * q, NX_GB_CURVE_LINEAR) != 200 { return __syscall(93, 51, 0, 0, 0, 0, 0) }
241
242 // T7: Invalid curve -> returns a (refusal).
243 if nx_gradient_blend_q14(777, 888, q / 2, 99) != 777 { return __syscall(93, 60, 0, 0, 0, 0, 0) }
244
245 // T8: Crenulate -- deterministic (same seed + perp + progress
246 // -> same value).
247 let c_a: nx_int = nx_gradient_blend_crenulate_q14(0, q, q / 2, 100, q / 4, 42)
248 let c_b: nx_int = nx_gradient_blend_crenulate_q14(0, q, q / 2, 100, q / 4, 42)
249 if c_a != c_b { return __syscall(93, 70, 0, 0, 0, 0, 0) }
250 // Different perpendicular -> different blend (the finger effect).
251 let c_c: nx_int = nx_gradient_blend_crenulate_q14(0, q, q / 2, 200, q / 4, 42)
252 if c_a == c_c { return __syscall(93, 71, 0, 0, 0, 0, 0) }
253 // Zero finger amplitude -> same as smoothstep at progress.
254 let c_zero: nx_int = nx_gradient_blend_crenulate_q14(0, q, q / 2, 100, 0, 42)
255 let s_for_comp: nx_int = nx_gradient_blend_q14(0, q, q / 2, NX_GB_CURVE_SMOOTHSTEP)
256 if c_zero != s_for_comp { return __syscall(93, 72, 0, 0, 0, 0, 0) }
257
258 // T9: Composition example -- blending two biome heights.
259 // forest_height = 200; desert_height = 50. Transition smoothly
260 // via SMOOTHSTEP across a region. At t=0.5, the blend should be
261 // 125 (50 + 75 = 125).
262 let blend_125: nx_int = nx_gradient_blend_q14(50, 200, q / 2, NX_GB_CURVE_SMOOTHSTEP)
263 if blend_125 < 120 { return __syscall(93, 80, 0, 0, 0, 0, 0) }
264 if blend_125 > 130 { return __syscall(93, 81, 0, 0, 0, 0, 0) }
265
266 return 0
267}