dopairb 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,693 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dopairb
4
+ module Gallery
5
+ # Distance from (px, py) to the segment (ax, ay)-(bx, by); pass aspect-scaled x.
6
+ JP_SEG = lambda do |px, py, ax, ay, bx, by|
7
+ dx = bx - ax
8
+ dy = by - ay
9
+ t = (((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy + 1e-12)).clamp(0.0, 1.0)
10
+ Math.hypot(px - ax - t * dx, py - ay - t * dy)
11
+ end
12
+
13
+ # The color a feature loop produced with `break color`, or nil when it ran out.
14
+ JP_HIT = ->(r) { r if r.is_a?(Array) && r.size == 3 && r[0].is_a?(Integer) }
15
+
16
+ # Triangle wave 0..1..0 with period 1.
17
+ JP_TRI = ->(x) { 1.0 - (x - x.floor - 0.5).abs * 2 }
18
+
19
+ # Approximate signed distance to a rotated ellipse, all in aspect-scaled units.
20
+ JP_RING = lambda do |x, y, cx, cy, rx, ry, ang = 0.0|
21
+ dx = x - cx
22
+ dy = y - cy
23
+ c = Math.cos(ang)
24
+ s = Math.sin(ang)
25
+ ex = dx * c + dy * s
26
+ ey = -dx * s + dy * c
27
+ (Math.sqrt((ex / rx)**2 + (ey / ry)**2) - 1.0) * [rx, ry].min
28
+ end
29
+
30
+ # Tohaku's pine groves: [x, top, ink density, lean, [[dy, dx, rx] needle blots]]
31
+ JP_PINES = [
32
+ [0.07, 0.14, 0.55, 0.03, [[0.02, 0.01, 0.04], [0.14, -0.02, 0.05], [0.27, 0.02, 0.045]]],
33
+ [0.13, 0.30, 0.30, -0.02, [[0.0, 0.0, 0.035], [0.12, 0.015, 0.045]]],
34
+ [0.29, 0.04, 1.00, 0.04, [[0.0, 0.0, 0.045], [0.10, -0.03, 0.06], [0.22, 0.025, 0.07], [0.36, -0.02, 0.06]]],
35
+ [0.36, 0.18, 0.80, -0.03, [[0.0, 0.01, 0.04], [0.12, 0.03, 0.06], [0.25, -0.02, 0.05]]],
36
+ [0.44, 0.32, 0.45, 0.02, [[0.0, 0.0, 0.04], [0.11, -0.02, 0.05]]],
37
+ [0.62, 0.12, 0.40, -0.02, [[0.0, 0.0, 0.04], [0.13, 0.02, 0.055], [0.27, -0.02, 0.05]]],
38
+ [0.68, 0.28, 0.25, 0.03, [[0.0, 0.0, 0.035], [0.12, 0.02, 0.045]]],
39
+ [0.86, 0.10, 0.18, 0.02, [[0.0, 0.0, 0.04], [0.14, -0.02, 0.05], [0.28, 0.02, 0.045]]],
40
+ [0.94, 0.26, 0.10, -0.02, [[0.0, 0.0, 0.035], [0.12, 0.01, 0.04]]]
41
+ ].freeze
42
+
43
+ # Hokusai, Thunderstorm Beneath the Summit
44
+ piece :thunderstorm_beneath_the_summit, "Thunderstorm Beneath the Summit", "Katsushika Hokusai", "c. 1831", aspect: 1.45 do |u, v|
45
+ pu = 0.62
46
+ pv = 0.13
47
+ d = u - pu
48
+ slope = pv + d.abs * (d < 0 ? 0.95 : 0.80) - d * d * 0.25
49
+ if v >= slope
50
+ base_line = 0.56 + 0.04 * Math.sin(u * 11) + 0.02 * Math.sin(u * 37)
51
+ if v > base_line
52
+ # the lightning bolt crackling over the dark foot
53
+ lx = 0.70 + (v - 0.62) * 0.55 + 0.05 * (JP_TRI.((v - 0.6) * 7) - 0.5)
54
+ next [240, 70, 35] if v > 0.62 && v < 0.97 && (u - lx).abs < 0.014
55
+ next jitter([32, 26, 24], u, v, 8, 20)
56
+ end
57
+ # snow streaks running down from the summit
58
+ reach = pv + 0.07 + 0.10 * vnoise(u, 0.3, 30)
59
+ next mix([250, 248, 240], [215, 215, 225], (v - pv) * 5) if v < reach && Math.sin(u * 95) > -0.2
60
+ next [250, 248, 240] if v < pv + 0.03
61
+ base = mix([150, 55, 30], [95, 35, 25], smooth(0.2, 0.55, v))
62
+ streak = Math.sin(u * 120 + v * 15) > 0.7 ? 0.8 : 1.0
63
+ next Color.scale(jitter(base, u, v, 10), streak)
64
+ end
65
+ sky = mix([30, 75, 160], [195, 215, 230], smooth(0.0, 0.75, v))
66
+ # billowing clouds low on the left and right
67
+ cl = vnoise(u, v, 9) + smooth(0.25, 0.65, v) * 0.6 - 0.45
68
+ if cl > 0.35 && (u < 0.42 || u > 0.85)
69
+ next mix([255, 255, 250], [205, 210, 220], (cl - 0.35) * 0.5 + (v - 0.3))
70
+ end
71
+ sky
72
+ end
73
+
74
+ # Hokusai, Ejiri in Suruga Province
75
+ piece :ejiri_in_suruga, "Ejiri in Suruga Province", "Katsushika Hokusai", "c. 1831", aspect: 1.45 do |u, v|
76
+ x = u * 1.45
77
+ # sheets of paper whirling across the sky
78
+ papers = [[0.08, 0.55], [0.20, 0.42], [0.30, 0.30], [0.45, 0.36], [0.52, 0.22], [0.66, 0.30], [0.74, 0.16],
79
+ [0.88, 0.24], [0.98, 0.12], [0.60, 0.46], [0.38, 0.18], [1.10, 0.20]]
80
+ hit = JP_HIT.(papers.each_with_index do |(px, py), i|
81
+ a = i * 1.3
82
+ dx = x - px
83
+ dy = v - py
84
+ rx = dx * Math.cos(a) + dy * Math.sin(a)
85
+ ry = -dx * Math.sin(a) + dy * Math.cos(a)
86
+ next unless rx.abs < 0.022 && ry.abs < 0.016
87
+ break rx.abs > 0.016 || ry.abs > 0.011 ? [150, 140, 120] : [252, 250, 240]
88
+ end)
89
+ next hit if hit
90
+ # a hat tumbling up high
91
+ next [175, 135, 70] if ellipse(u, v, 0.56, 0.12, 0.022, 0.018) < 1
92
+ # bent trees on the right
93
+ hit = JP_HIT.([[0.78, 0.0], [0.86, 0.03], [0.70, 0.05]].each do |tx, off|
94
+ cx = tx - 0.22 * (0.92 - v)**2 + off
95
+ break [70, 55, 40] if v > 0.18 + off * 2 && v < 0.92 && (u - cx).abs < 0.008 + (v - 0.2) * 0.008
96
+ if v > 0.12 && v < 0.5 && ellipse(u, v, cx - 0.05, 0.22 + off * 3, 0.07, 0.05) < 1 && vnoise(u, v, 30) > 0.5
97
+ break [70, 95, 60]
98
+ end
99
+ end)
100
+ next hit if hit
101
+ # travelers bending into the wind along the path
102
+ hit = JP_HIT.([[0.18, 0.80], [0.30, 0.82], [0.42, 0.79], [0.55, 0.83], [0.64, 0.80]].each do |fx, fy|
103
+ next unless (u - fx).abs < 0.03 && v > fy - 0.10 && v < fy + 0.04
104
+ break [180, 140, 70] if ellipse(u, v, fx + 0.012, fy - 0.075, 0.022, 0.02) < 1 # straw hat
105
+ lean = fx + (fy - v) * 0.3
106
+ break [45, 55, 85] if (u - lean).abs < 0.012 && v > fy - 0.065
107
+ end)
108
+ next hit if hit
109
+ ground = 0.66 + 0.02 * Math.sin(u * 6)
110
+ if v > ground
111
+ next mix([165, 145, 95], [130, 105, 70], (v - 0.85) * 5) if v > 0.84
112
+ next Math.sin(u * 160 + v * 30) > 0.6 ? [140, 145, 95] : [185, 180, 125]
113
+ end
114
+ # faint outline of Fuji on the left
115
+ fd = (u - 0.25).abs
116
+ fslope = 0.40 + fd * 0.55
117
+ if fd < 0.25 && v > fslope - 0.012
118
+ next v < fslope + 0.012 ? [120, 125, 130] : [238, 236, 225]
119
+ end
120
+ mix([175, 195, 205], [235, 230, 205], smooth(0.0, 0.65, v))
121
+ end
122
+
123
+ # Hokusai, Kajikazawa in Kai Province
124
+ piece :kajikazawa, "Kajikazawa in Kai Province", "Katsushika Hokusai", "c. 1831", aspect: 1.45 do |u, v|
125
+ x = u * 1.45
126
+ # fishing lines from the angler's hands into the surf
127
+ lines = [[0.74, 0.33, 1.00, 0.62], [0.74, 0.33, 1.10, 0.56], [0.74, 0.33, 1.18, 0.66]]
128
+ next [20, 20, 30] if lines.any? { |ax, ay, bx, by| JP_SEG.(x, v, ax, ay, bx, by) < 0.005 }
129
+ # the fisherman and the boy on the rock tip
130
+ next [25, 30, 45] if ellipse(u, v, 0.49, 0.36, 0.022, 0.07) < 1
131
+ next [205, 170, 95] if ellipse(u, v, 0.49, 0.285, 0.04, 0.02) < 1
132
+ next [60, 75, 110] if ellipse(u, v, 0.43, 0.40, 0.014, 0.04) < 1
133
+ rock = [[0.0, 0.62], [0.15, 0.55], [0.30, 0.48], [0.45, 0.43], [0.56, 0.42], [0.55, 0.50], [0.45, 0.62], [0.36, 0.78], [0.30, 1.0], [0.0, 1.0]]
134
+ if inside?(u, v, rock)
135
+ next [70, 105, 150] if Math.sin((u * 1.2 - v) * 40) > 0.8
136
+ next mix([28, 45, 80], [10, 18, 40], (v - 0.45) * 2)
137
+ end
138
+ sea_top = 0.50
139
+ if v > sea_top
140
+ # claws of surf around the rock and along the bottom
141
+ foam = Math.sin(u * 40 + Math.sin(v * 25) * 3) * 0.5 + 0.5
142
+ near = Math.exp(-((u - (0.55 - (v - 0.5) * 0.6))**2) * 300)
143
+ next [245, 248, 250] if foam * near > 0.5 || (v > 0.82 && Math.sin(u * 55 + v * 40) > 0.75)
144
+ next mix([95, 150, 205], [40, 90, 165], (v - sea_top) * 2.5)
145
+ end
146
+ # pale Fuji above the mist
147
+ fd = (u - 0.70).abs
148
+ if v > 0.12 + fd * 0.75 && v < 0.40
149
+ next v < 0.17 + 0.02 * Math.sin(u * 80) ? [250, 250, 248] : [140, 165, 195]
150
+ end
151
+ next [245, 240, 225] if v > 0.33 # mist
152
+ mix([90, 120, 170], [235, 232, 215], smooth(0.0, 0.3, v))
153
+ end
154
+
155
+ # Hokusai, Kirifuri Waterfall at Kurokami Mountain
156
+ piece :kirifuri_waterfall, "Kirifuri Waterfall at Kurokami Mountain", "Katsushika Hokusai", "c. 1832", aspect: 0.68 do |u, v|
157
+ # tiny travelers at the bottom left
158
+ hit = JP_HIT.([[0.08, 0.86], [0.14, 0.87], [0.20, 0.88]].each do |fx, fy|
159
+ break [200, 160, 90] if ellipse(u, v, fx, fy - 0.035, 0.022, 0.009) < 1
160
+ break [40, 50, 80] if ellipse(u, v, fx, fy, 0.012, 0.03) < 1
161
+ end)
162
+ next hit if hit
163
+ next mix([120, 115, 90], [90, 85, 70], v) if v > 0.89 && u < 0.32 # ledge
164
+ # the falls: one stream at the top splitting into fingers
165
+ cx = 0.52 + 0.03 * Math.sin(v * 5)
166
+ half = v < 0.12 ? 0.07 : 0.07 + (v - 0.12) * 0.25
167
+ if (u - cx).abs < half && v < 0.92
168
+ f = (u - cx) / half
169
+ fingers = v < 0.2 ? 1.0 : Math.cos(f * Math::PI * 2.5 + v * 2)
170
+ if fingers > -0.35
171
+ streak = Math.sin(u * 160 + Math.sin(v * 12) * 2) > 0.3
172
+ next streak ? [235, 245, 250] : mix([60, 120, 190], [30, 80, 160], v)
173
+ end
174
+ end
175
+ # spray and pool
176
+ if v > 0.86
177
+ next [240, 245, 250] if vnoise(u, v, 22) > 0.55
178
+ next [70, 120, 180]
179
+ end
180
+ # dark rocks and cliffs
181
+ if v > 0.07 + 0.04 * Math.sin(u * 9)
182
+ tone = vnoise(u, v, 10)
183
+ base = tone > 0.5 ? [40, 75, 70] : [70, 60, 45]
184
+ next Math.sin(u * 50 + v * 30 + tone * 6) > 0.6 ? Color.scale(base, 0.6) : base
185
+ end
186
+ # trees on top and sky
187
+ next [40, 80, 50] if vnoise(u, v, 30) > 0.55
188
+ [215, 210, 185]
189
+ end
190
+
191
+ # Hiroshige, Sudden Shower over Shin-Ohashi Bridge and Atake
192
+ piece :sudden_shower_atake, "Sudden Shower over Shin-Ōhashi Bridge and Atake", "Utagawa Hiroshige", "1857", aspect: 0.68 do |u, v|
193
+ deck = 0.84 - u * 0.30
194
+ col =
195
+ if v < 0.14
196
+ mix([20, 20, 25], [90, 95, 95], smooth(0.0, 0.14, v) * (0.6 + 0.4 * vnoise(u, v, 12)))
197
+ elsif v > 0.34 && v < 0.41 + 0.01 * Math.sin(u * 30)
198
+ Math.sin(u * 60) > 0.3 ? [70, 95, 70] : [45, 70, 55] # far shore of Atake
199
+ elsif ellipse(u, v, 0.30, 0.47, 0.14, 0.012) < 1
200
+ [110, 90, 60] # a raft
201
+ elsif v >= 0.38
202
+ mix([120, 150, 160], [60, 95, 125], (v - 0.4) * 1.5)
203
+ else
204
+ mix([150, 155, 150], [190, 190, 180], (v - 0.14) * 3)
205
+ end
206
+ if v > deck - 0.03 && v < deck + 0.03
207
+ col = v < deck ? [185, 150, 95] : [120, 90, 55] # planks and their shadowed edge
208
+ elsif v > deck && v < 1.0 && ((u * 6 + 0.3) % 1.0) < 0.14
209
+ col = [80, 60, 40] # pilings
210
+ end
211
+ # people hurrying across under umbrellas and straw capes
212
+ cell = (u * 7).floor
213
+ fu = (u * 7) % 1.0
214
+ pv = deck - 0.03
215
+ if noise(cell, 3) > 0.25 && fu > 0.25 && fu < 0.75 && v < pv && v > pv - 0.10
216
+ if v < pv - 0.065
217
+ col = noise(cell, 5) > 0.5 ? [45, 50, 60] : [200, 175, 115] if ellipse(fu, v, 0.5, pv - 0.065, 0.35, 0.035) < 1 && v < pv - 0.065
218
+ else
219
+ col = noise(cell, 6) > 0.5 ? [55, 65, 95] : [150, 120, 70] if (fu - 0.5).abs < 0.18
220
+ end
221
+ end
222
+ rain = ((u * 28 + v * 4) % 1.0) < 0.2 && noise((u * 28 + v * 4).floor, (v * 6).floor) > 0.25
223
+ rain ||= ((u * 19 - v * 2) % 1.0) < 0.14 && noise((u * 19 - v * 2).floor + 40, (v * 5).floor) > 0.4
224
+ rain ? mix(col, [45, 50, 60], 0.4) : col
225
+ end
226
+
227
+ # Hiroshige, Plum Park in Kameido
228
+ piece :plum_garden_kameido, "Plum Park in Kameido", "Utagawa Hiroshige", "1857", aspect: 0.68 do |u, v|
229
+ x = u * 0.68
230
+ # blossoms along the branches
231
+ blossoms = [[0.12, 0.30], [0.20, 0.22], [0.30, 0.26], [0.40, 0.20], [0.50, 0.30], [0.58, 0.24], [0.64, 0.32],
232
+ [0.08, 0.45], [0.16, 0.12], [0.36, 0.36], [0.46, 0.14], [0.26, 0.42], [0.55, 0.40]]
233
+ hit = JP_HIT.(blossoms.each do |bx, by|
234
+ dd = Math.hypot(x - bx, v - by)
235
+ break [210, 60, 70] if dd < 0.008
236
+ break [252, 250, 245] if dd < 0.026
237
+ end)
238
+ next hit if hit
239
+ # the huge foreground branch: trunk rising on the left, limb sweeping right
240
+ trunk = 0.17 + (v - 0.95) * -0.08 + 0.02 * Math.sin(v * 20)
241
+ if v > 0.30 && (x - trunk).abs < 0.075 + 0.02 * Math.sin(v * 13)
242
+ next Math.sin(v * 60 + u * 20) > 0.4 ? [55, 35, 30] : [30, 20, 20]
243
+ end
244
+ limb_v = 0.30 - (x - 0.17) * 0.18 + 0.03 * Math.sin(x * 25)
245
+ next [35, 22, 20] if x > 0.12 && (v - limb_v).abs < 0.045 - (x - 0.17) * 0.05
246
+ twigs = [[0.30, 0.27, 0.42, 0.13], [0.45, 0.25, 0.62, 0.35], [0.20, 0.40, 0.06, 0.48], [0.18, 0.30, 0.14, 0.10]]
247
+ next [35, 22, 20] if twigs.any? { |ax, ay, bx, by| JP_SEG.(x, v, ax, ay, bx, by) < 0.008 }
248
+ # background trees and visitors
249
+ if v > 0.62
250
+ next [60, 40, 35] if ((u * 6) % 1.0) < 0.06 && v < 0.75
251
+ next [60, 60, 80] if v > 0.70 && v < 0.75 && noise((u * 40).floor, 2) > 0.8
252
+ next mix([150, 185, 110], [90, 140, 80], (v - 0.62) * 3)
253
+ end
254
+ if v > 0.52 && vnoise(u, v, 18) > 0.5
255
+ next noise((u * 40).floor, (v * 40).floor) > 0.8 ? [250, 245, 240] : [110, 80, 70]
256
+ end
257
+ ramp(smooth(0.0, 0.55, v), [175, 30, 40], [225, 110, 90], [250, 240, 225], [250, 248, 240])
258
+ end
259
+
260
+ # Hiroshige, Fireworks at Ryogoku
261
+ piece :fireworks_ryogoku, "Fireworks at Ryōgoku", "Utagawa Hiroshige", "1858", aspect: 0.68 do |u, v|
262
+ x = u * 0.68
263
+ # burst of sparks
264
+ dx = x - 0.40
265
+ dy = v - 0.30
266
+ r = Math.hypot(dx, dy)
267
+ a = Math.atan2(dy, dx)
268
+ if r < 0.16
269
+ ray = Math.cos(a * 11 + r * 8)
270
+ spark = noise((a * 18).floor, (r * 70).floor)
271
+ next [255, 245, 200] if r < 0.012
272
+ next mix([255, 230, 150], [230, 150, 70], r / 0.16) if ray > 0.85 && spark > 0.3
273
+ next [240, 200, 120] if spark > 0.93
274
+ end
275
+ # trailing rocket
276
+ next [220, 180, 110] if (x - 0.42).abs < 0.006 && v > 0.47 && v < 0.62 && noise(0, (v * 50).floor) > 0.35
277
+ # the bridge across the lower part
278
+ deck = 0.70 + (u - 0.5) * 0.05
279
+ if v > deck - 0.02 && v < deck + 0.015
280
+ next noise((u * 60).floor, 1) > 0.6 && v < deck - 0.005 ? [30, 30, 45] : [20, 20, 30]
281
+ end
282
+ next [18, 18, 28] if v > deck && v < 0.88 && ((u * 10) % 1.0) < 0.1
283
+ if v > 0.62
284
+ # boats on the river
285
+ hit = JP_HIT.([[0.18, 0.92], [0.55, 0.90], [0.82, 0.95], [0.38, 0.97]].each do |px, py|
286
+ break [25, 25, 30] if ellipse(u, v, px, py, 0.09, 0.015) < 1
287
+ break [210, 160, 80] if ellipse(u, v, px, py - 0.025, 0.035, 0.012) < 1
288
+ end)
289
+ next hit if hit
290
+ next Math.sin(v * 120 + u * 6) > 0.8 ? [50, 70, 110] : mix([25, 40, 80], [15, 25, 55], (v - 0.62) * 2)
291
+ end
292
+ # far shore
293
+ next [15, 20, 35] if v > 0.60 - 0.01 * Math.sin(u * 20)
294
+ mix([10, 15, 45], [40, 55, 105], smooth(0.0, 0.65, v))
295
+ end
296
+
297
+ # Hiroshige, Night Snow at Kambara
298
+ piece :evening_snow_kanbara, "Night Snow at Kambara", "Utagawa Hiroshige", "1833", aspect: 1.5 do |u, v|
299
+ snow = [238, 238, 240]
300
+ col =
301
+ if v >= 0.80
302
+ # the snowy road with three hunched walkers
303
+ walk = [[0.32, 0.88], [0.44, 0.89], [0.60, 0.87]].find { |fx, fy| ellipse(u, v, fx, fy, 0.022, 0.07) < 1 }
304
+ if walk
305
+ v < walk[1] - 0.035 ? [175, 165, 130] : [55, 55, 70]
306
+ else
307
+ mix(snow, [205, 205, 215], (v - 0.8) * 4)
308
+ end
309
+ elsif v > 0.58
310
+ # village: snow-laden roofs over dark walls
311
+ cell = (u * 7).floor
312
+ fu = (u * 7) % 1.0
313
+ roof = 0.60 + noise(cell, 2) * 0.05
314
+ if fu > 0.08 && fu < 0.92 && v > roof
315
+ if v < roof + 0.05 + 0.02 * (1 - (fu - 0.5).abs * 2)
316
+ snow
317
+ elsif v < 0.76
318
+ noise(cell, 4) > 0.6 && fu > 0.4 && fu < 0.6 && v > 0.68 ? [150, 120, 70] : [50, 48, 52]
319
+ else
320
+ snow
321
+ end
322
+ else
323
+ snow
324
+ end
325
+ elsif v > 0.10 + u * 0.55 + 0.03 * Math.sin(u * 17)
326
+ # the steep snowy hillside, dotted with dark trees
327
+ tree = vnoise(u, v, 10) > 0.66 && Math.sin(u * 120) > 0.3
328
+ tree ? [60, 60, 65] : snow
329
+ elsif u > 0.5 && v > 0.30 + 0.06 * Math.sin(u * 9)
330
+ [70, 70, 76] # far dark hills
331
+ else
332
+ mix([25, 25, 30], [110, 110, 115], smooth(0.0, 0.6, v))
333
+ end
334
+ noise((u * 60).floor, (v * 40).floor) > 0.965 ? [250, 250, 250] : col
335
+ end
336
+
337
+ # Hiroshige, Moon Pine, Ueno
338
+ piece :moon_pine_ueno, "Moon Pine, Ueno", "Utagawa Hiroshige", "1857", aspect: 0.68 do |u, v|
339
+ x = u * 0.68
340
+ # the pine branch twisted into a ring
341
+ cx = 0.34
342
+ cy = 0.42
343
+ r = Math.hypot(x - cx, v - cy)
344
+ if (r - 0.20).abs < 0.022 + 0.006 * Math.sin(Math.atan2(v - cy, x - cx) * 5)
345
+ next Math.sin(r * 300) > 0 ? [80, 55, 35] : [55, 35, 25]
346
+ end
347
+ # the trunk coming in from the top left, with needle clumps
348
+ next [60, 40, 28] if JP_SEG.(x, v, 0.0, 0.08, 0.20, 0.25) < 0.025
349
+ clumps = [[0.08, 0.06], [0.22, 0.10], [0.50, 0.20], [0.56, 0.36], [0.05, 0.22], [0.40, 0.60]]
350
+ if clumps.any? { |nx, ny| ellipse(x, v, nx, ny, 0.08, 0.04) < 1 }
351
+ next Math.sin(x * 200 + v * 80) > 0 ? [40, 90, 50] : [25, 60, 40]
352
+ end
353
+ # foreground bank
354
+ next mix([130, 110, 70], [90, 75, 50], (v - 0.8) * 4) if v > 0.80 + 0.02 * Math.sin(u * 10)
355
+ # pond and far shore with town
356
+ if v > 0.58
357
+ next [45, 80, 55] if v < 0.62 && noise((u * 30).floor, 7) > 0.3
358
+ next Math.sin(v * 150) > 0.7 ? [150, 180, 205] : [80, 130, 175]
359
+ end
360
+ if v > 0.53
361
+ next noise((u * 40).floor, 3) > 0.5 ? [80, 70, 65] : [40, 70, 55]
362
+ end
363
+ ramp(v / 0.53, [70, 120, 185], [165, 195, 220], [240, 220, 200], [225, 110, 80])
364
+ end
365
+
366
+ # Sharaku, Otani Oniji III as Yakko Edobei
367
+ piece :otani_oniji, "Ōtani Oniji III as Yakko Edobei", "Tōshūsai Sharaku", "1794", aspect: 0.68 do |u, v|
368
+ x = u * 0.68
369
+ skin = [242, 232, 220]
370
+ ink = [20, 18, 22]
371
+ # hands with claw-like fingers
372
+ hands = [[0.13, 0.74, 1], [0.54, 0.70, -1]]
373
+ claw = hands.any? do |hx, hy, dir|
374
+ Math.hypot(x - hx, v - hy) < 0.045 ||
375
+ 5.times.any? do |k|
376
+ a = -2.7 + k * 0.38
377
+ JP_SEG.(x, v, hx, hy, hx + Math.cos(a) * 0.10 * dir, hy + Math.sin(a) * 0.10) < 0.014
378
+ end
379
+ end
380
+ next skin if claw
381
+ # face
382
+ fx = 0.34
383
+ fy = 0.32
384
+ face = ellipse(x, v, fx, fy, 0.13, 0.15)
385
+ if face < 1 && v > 0.22
386
+ dx = x - fx
387
+ next ink if (v - (0.29 - dx.abs * 0.35)).abs < 0.012 && dx.abs > 0.025 && dx.abs < 0.10 # brows
388
+ next ink if (v - 0.325).abs < 0.008 && (dx.abs - 0.055).abs < 0.022 # eyes
389
+ next [180, 120, 110] if dx.abs < 0.012 && v > 0.33 && v < 0.39 # nose
390
+ next [150, 30, 40] if (v - (0.41 + dx.abs * 0.25)).abs < 0.01 && dx.abs < 0.06 # grimace
391
+ next mix(skin, [215, 195, 185], face)
392
+ end
393
+ # hair: shaved blue-grey pate, black sides and topknot
394
+ next [120, 130, 150] if ellipse(x, v, fx, 0.20, 0.09, 0.07) < 1
395
+ next ink if ellipse(x, v, fx, 0.24, 0.16, 0.12) < 1 || ellipse(x, v, fx, 0.11, 0.035, 0.03) < 1
396
+ # dark kimono with white under-collar
397
+ shoulder = 0.48 + (x - fx).abs * 0.35
398
+ if v > shoulder - 0.02 && (x - fx).abs < 0.30
399
+ cdx = (x - fx).abs
400
+ next [235, 235, 230] if v > 0.45 && (cdx - (v - 0.45) * 0.5).abs < 0.014
401
+ next skin if v > 0.45 && cdx < (v - 0.45) * 0.5 && v < 0.58
402
+ next Math.sin(x * 90 + v * 30) > 0.85 ? [60, 55, 70] : [25, 25, 35]
403
+ end
404
+ # dark glittering mica ground
405
+ noise((u * 50).floor, (v * 70).floor) > 0.9 ? [120, 115, 125] : [55, 50, 58]
406
+ end
407
+
408
+ # Utamaro, Three Beauties of the Present Day
409
+ piece :three_beauties, "Three Beauties of the Present Day", "Kitagawa Utamaro", "c. 1793", aspect: 0.68 do |u, v|
410
+ x = u * 0.68
411
+ skin = [248, 240, 230]
412
+ hair = [20, 18, 22]
413
+ # [face x, face y, kimono color]; the top beauty sits behind the other two
414
+ beauties = [[0.17, 0.58, [70, 60, 80]], [0.51, 0.58, [200, 110, 60]], [0.34, 0.30, [40, 40, 45]]]
415
+ hit = JP_HIT.(beauties.each do |fx, fy, kc|
416
+ e = ellipse(x, v, fx, fy, 0.075, 0.095)
417
+ if e < 1
418
+ dx = x - fx
419
+ break hair if (v - (fy - 0.015)).abs < 0.006 && (dx.abs - 0.03).abs < 0.015
420
+ break [190, 40, 50] if (v - (fy + 0.055)).abs < 0.007 && dx.abs < 0.012
421
+ break skin
422
+ end
423
+ break hair if ellipse(x, v, fx, fy - 0.10, 0.10, 0.07) < 1 || ellipse(x, v, fx, fy - 0.03, 0.10, 0.05) < 1
424
+ break [220, 190, 110] if (v - (fy - 0.13)).abs < 0.008 && (x - fx).abs < 0.12 # comb
425
+ if v > fy + 0.09 && (x - fx).abs < 0.06 + (v - fy - 0.09) * 0.9
426
+ break [245, 240, 235] if ((x - fx).abs - (v - fy - 0.09) * 0.4).abs < 0.012 # collar
427
+ break skin if (x - fx).abs < (v - fy - 0.09) * 0.4 && v < fy + 0.16
428
+ break Math.sin(x * 120 + v * 60) > 0.7 ? Color.scale(kc, 1.3) : kc
429
+ end
430
+ end)
431
+ next hit if hit
432
+ jitter([225, 185, 95], u, v, 10, 20)
433
+ end
434
+
435
+ # Sotatsu, Wind God and Thunder God
436
+ piece :fujin_raijin, "Wind God and Thunder God", "Tawaraya Sōtatsu", "17th century", aspect: 2.2 do |u, v|
437
+ x = u * 2.2
438
+ dark = [35, 30, 30]
439
+ # [center x, skin, facing]: Fujin crouching on the left, Raijin on the right
440
+ gods = [[0.40, [80, 155, 105], 1], [1.80, [240, 238, 228], -1]]
441
+ hit = JP_HIT.(gods.each do |cx, skin, dir|
442
+ next if (x - cx).abs > 0.45
443
+ hx = cx + 0.05 * dir
444
+ if dir.positive?
445
+ # the long white wind bag, looped over his head from hand to hand
446
+ r = Math.hypot(x - cx, (v - 0.40) * 1.1)
447
+ break(Math.sin(r * 90) > 0.6 ? [190, 195, 185] : [240, 240, 232]) if (r - 0.25).abs < 0.04 && v < 0.44
448
+ else
449
+ # the ring of drums around the thunder god
450
+ r = Math.hypot(x - cx, v - 0.38)
451
+ ang = Math.atan2(v - 0.38, x - cx)
452
+ bead = (ang / (Math::PI / 4)).round * (Math::PI / 4)
453
+ bd = Math.hypot(x - (cx + Math.cos(bead) * 0.28), v - (0.38 + Math.sin(bead) * 0.28))
454
+ break dark if bd < 0.055 && bd > 0.04
455
+ break [200, 80, 45] if bd < 0.04
456
+ break dark if (r - 0.28).abs < 0.008 && v < 0.6
457
+ end
458
+ # wild hair: dark strokes flaring from the head
459
+ hair = 5.times.any? do |k|
460
+ a = -2.6 + k * 0.4
461
+ JP_SEG.(x, v, hx, 0.24, hx + Math.cos(a) * 0.12, 0.24 + Math.sin(a) * 0.12) < 0.016
462
+ end
463
+ break dark if hair
464
+ # head, crouched body and bent limbs, outlined in ink
465
+ head = ellipse(x, v, hx, 0.28, 0.065, 0.065)
466
+ body = ellipse(x, v, cx, 0.45, 0.10, 0.12)
467
+ limbs = [[cx - 0.06, 0.38, cx - 0.22, 0.40], [cx + 0.06, 0.38, cx + 0.22, 0.40],
468
+ [cx - 0.05, 0.52, cx - 0.16, 0.48], [cx - 0.16, 0.48, cx - 0.14, 0.62],
469
+ [cx + 0.05, 0.52, cx + 0.16, 0.50], [cx + 0.16, 0.50, cx + 0.18, 0.62]]
470
+ ld = limbs.map { |ax, ay, bx, by| JP_SEG.(x, v, ax, ay, bx, by) }.min
471
+ if head < 1
472
+ break dark if (v - 0.27).abs < 0.01 && ((x - hx) * dir - 0.025).abs < 0.012 # glaring eye
473
+ break [180, 40, 35] if (v - 0.31).abs < 0.008 && ((x - hx) * dir - 0.02).abs < 0.025 # open mouth
474
+ break head > 0.6 ? Color.scale(skin, 0.75) : skin
475
+ end
476
+ break(body > 0.65 ? Color.scale(skin, 0.75) : skin) if body < 1
477
+ break [160, 45, 35] if ellipse(x, v, cx, 0.55, 0.12, 0.035) < 1 # loincloth
478
+ break(ld > 0.022 ? Color.scale(skin, 0.6) : skin) if ld < 0.03
479
+ # the dark swirling cloud beneath
480
+ cr = ellipse(x, v, cx, 0.68, 0.36, 0.13) - (vnoise(u, v, 12) - 0.5) * 0.9
481
+ if cr < 1
482
+ sw = Math.sin(Math.hypot(x - cx, (v - 0.68) * 2.5) * 45 - Math.atan2(v - 0.68, x - cx) * 2)
483
+ break sw > 0.6 ? [130, 130, 135] : [55, 55, 60]
484
+ end
485
+ end)
486
+ next hit if hit
487
+ # gold leaf with faint square seams
488
+ gold = jitter([215, 170, 70], u, v, 10, 12)
489
+ ((x * 6) % 1.0) < 0.04 || ((v * 4) % 1.0) < 0.04 ? Color.scale(gold, 0.92) : gold
490
+ end
491
+
492
+ # Hasegawa Tohaku, Pine Trees
493
+ piece :pine_trees_tohaku, "Pine Trees", "Hasegawa Tōhaku", "c. 1595", aspect: 2.5 do |u, v|
494
+ paper = jitter([238, 234, 222], u, v, 4, 10)
495
+ # mist: bottom and middle fade to paper, with drifting soft banks
496
+ mist = (1.0 - smooth(0.40, 0.95, v) * 0.97) * (0.7 + 0.3 * smooth(0.3, 0.7, vnoise(u * 0.7, v, 4)))
497
+ next paper if mist < 0.02
498
+ ink = 0.0
499
+ JP_PINES.each do |tx, top, dens, lean, blots|
500
+ next if v < top - 0.08 || (u - tx).abs > 0.16
501
+ cx = tx + (v - top) * lean
502
+ a = (u - cx).abs < 0.006 && v > top ? 0.7 : 0.0 # thin trunk
503
+ blots.each do |dy, dx, rx|
504
+ e = ellipse(u, v, cx + dx * 1.3, top + dy, rx * 1.6, 0.055) + (vnoise(u, v, 22) - 0.5) * 0.6
505
+ next unless e < 1
506
+ a = [a, (1 - e) * 2.2 * (0.45 + 0.55 * vnoise(u * 2, v, 50))].max
507
+ end
508
+ ink = [ink, a.clamp(0.0, 1.0) * dens].max
509
+ end
510
+ mix(paper, [28, 28, 28], ink * mist)
511
+ end
512
+
513
+ # Ogata Korin, Irises
514
+ piece :irises_korin, "Irises", "Ogata Kōrin", "c. 1705", aspect: 2.4 do |u, v|
515
+ x = u * 2.4
516
+ clumps = [[0.10, 0.20], [0.35, 0.32], [0.62, 0.15], [0.90, 0.40], [1.15, 0.22], [1.42, 0.30], [1.70, 0.12], [1.98, 0.35], [2.25, 0.20]]
517
+ hit = JP_HIT.(clumps.each_with_index do |(cx, top), i|
518
+ dx = x - cx
519
+ next if dx.abs > 0.14
520
+ # blossoms at the top of the clump
521
+ petal = 5.times.map do |k|
522
+ ellipse(x, v, cx + (k - 2) * 0.045 + (noise(i, k) - 0.5) * 0.03, top + 0.06 + noise(k, i) * 0.12, 0.03, 0.07)
523
+ end.min
524
+ break(petal < 0.35 ? [120, 110, 210] : [45, 40, 140]) if petal < 1
525
+ # sword leaves
526
+ base = top + 0.15
527
+ if v > base && dx.abs < 0.09 + (1 - v) * 0.02
528
+ blade = Math.sin(dx * 110 + (v - base) * 6 * (dx < 0 ? 1 : -1))
529
+ break(blade > 0.6 ? [70, 140, 60] : [35, 100, 45]) if blade > -0.1
530
+ end
531
+ end)
532
+ next hit if hit
533
+ jitter([220, 175, 70], u, v, 10, 14)
534
+ end
535
+
536
+ # Ogata Korin, Red and White Plum Blossoms
537
+ piece :red_white_plum, "Red and White Plum Blossoms", "Ogata Kōrin", "c. 1710s", aspect: 2.2 do |u, v|
538
+ x = u * 2.2
539
+ # the stylized stream, widening downward
540
+ sc = 1.1 + 0.08 * Math.sin(v * 4)
541
+ half = 0.08 + v * v * 0.45
542
+ if (x - sc).abs < half
543
+ swirl = Math.sin((x - sc) * 30 + Math.sin(v * 14 + x * 6) * 3 + v * 10)
544
+ next swirl > 0.75 ? [150, 150, 155] : [35, 35, 45]
545
+ end
546
+ # white plum on the left: trunk from the left edge, branch dipping and rising
547
+ white = [[0.30, 0.15], [0.42, 0.11], [0.18, 0.32], [0.40, 0.58], [0.48, 0.62], [0.12, 0.70], [0.24, 0.46]]
548
+ next [252, 252, 248] if white.any? { |bx, by| Math.hypot(x - bx, v - by) < 0.035 }
549
+ wb = JP_SEG.(x, v, 0.0, 0.85, 0.25, 0.55) < 0.04 || JP_SEG.(x, v, 0.25, 0.55, 0.20, 0.25) < 0.025 ||
550
+ JP_SEG.(x, v, 0.20, 0.25, 0.45, 0.10) < 0.018 || JP_SEG.(x, v, 0.25, 0.55, 0.50, 0.60) < 0.018
551
+ next [50, 55, 40] if wb
552
+ # red plum on the right: a thick trunk leaning in from the right
553
+ red = [[1.70, 0.12], [1.62, 0.40], [1.78, 0.18], [1.70, 0.43], [2.05, 0.20], [1.98, 0.40], [1.90, 0.10]]
554
+ next [215, 50, 60] if red.any? { |bx, by| Math.hypot(x - bx, v - by) < 0.035 }
555
+ rb = JP_SEG.(x, v, 2.05, 1.0, 1.85, 0.55) < 0.07 || JP_SEG.(x, v, 1.85, 0.55, 1.95, 0.25) < 0.05 ||
556
+ JP_SEG.(x, v, 1.95, 0.25, 1.65, 0.12) < 0.025 || JP_SEG.(x, v, 1.88, 0.45, 1.60, 0.40) < 0.02
557
+ next Math.sin(v * 50) > 0.6 ? [70, 80, 50] : [45, 45, 35] if rb
558
+ next [90, 130, 70] if v > 0.9 && (x < 0.6 || x > 1.6) # grassy bank
559
+ jitter([220, 180, 80], u, v, 10, 12)
560
+ end
561
+
562
+ # Toba Sojo (attr.), Choju-giga
563
+ piece :choju_giga, "Chōjū-giga (Frolicking Animals)", "attributed to Toba Sōjō", "12th century", aspect: 2.5 do |u, v|
564
+ x = u * 2.5
565
+ ink = [40, 35, 30]
566
+ line = 0.024
567
+ # [cx, cy, rx, ry, angle, fill]: the rabbit thrown off balance and the frog pushing it
568
+ parts = [
569
+ [0.82, 0.60, 0.15, 0.27, 0.45, [250, 246, 236]], # rabbit body
570
+ [0.62, 0.32, 0.12, 0.10, 0.30, [250, 246, 236]], # rabbit head
571
+ [0.50, 0.14, 0.045, 0.15, 0.6, [250, 246, 236]], # long ears
572
+ [0.62, 0.11, 0.045, 0.15, 0.3, [250, 246, 236]],
573
+ [1.42, 0.62, 0.24, 0.20, 0.0, [205, 200, 175]], # frog body
574
+ [1.22, 0.40, 0.06, 0.06, 0.0, [205, 200, 175]], # frog eyes
575
+ [1.40, 0.38, 0.06, 0.06, 0.0, [205, 200, 175]],
576
+ [1.70, 0.86, 0.12, 0.05, 0.4, [205, 200, 175]], # frog legs
577
+ [1.18, 0.88, 0.12, 0.05, -0.4, [205, 200, 175]]
578
+ ]
579
+ dists = parts.map { |cx, cy, rx, ry, ang| JP_RING.(x, v, cx, cy, rx, ry, ang) }
580
+ next ink if dists.any? { |dd| dd.abs < line }
581
+ next ink if Math.hypot(x - 0.58, v - 0.30) < 0.024 || Math.hypot(x - 1.22, v - 0.40) < 0.025 || Math.hypot(x - 1.40, v - 0.38) < 0.025
582
+ # arms locked in the struggle, and the frog's wide mouth
583
+ next ink if JP_SEG.(x, v, 0.95, 0.42, 1.22, 0.55) < 0.02 || JP_SEG.(x, v, 1.20, 0.65, 0.95, 0.66) < 0.02
584
+ next ink if JP_SEG.(x, v, 1.20, 0.52, 1.42, 0.50) < 0.015
585
+ idx = dists.index(&:negative?)
586
+ next parts[idx][5] if idx
587
+ # grass tufts
588
+ tufts = [[0.25, 0.95], [1.95, 0.94], [2.30, 0.92], [0.05, 0.93]]
589
+ next ink if tufts.any? { |gx, gy| 3.times.any? { |k| JP_SEG.(x, v, gx, gy, gx + (k - 1) * 0.07, gy - 0.16 - k * 0.03) < 0.014 } }
590
+ jitter([238, 226, 196], u, v, 6, 10)
591
+ end
592
+
593
+ # Hishikawa Moronobu, Beauty Looking Back
594
+ piece :mikaeri_bijin, "Beauty Looking Back", "Hishikawa Moronobu", "late 17th century", aspect: 0.5 do |u, v|
595
+ # head turned back over the shoulder
596
+ next [245, 232, 215] if ellipse(u, v, 0.60, 0.15, 0.06, 0.035) < 1
597
+ next [20, 18, 20] if ellipse(u, v, 0.48, 0.13, 0.11, 0.04) < 1 || ellipse(u, v, 0.43, 0.17, 0.05, 0.05) < 1
598
+ next [200, 160, 60] if (v - 0.10).abs < 0.006 && (u - 0.50).abs < 0.06 # comb
599
+ body = [[0.40, 0.20], [0.62, 0.19], [0.70, 0.32], [0.66, 0.50], [0.60, 0.70], [0.66, 0.88], [0.62, 0.95],
600
+ [0.22, 0.97], [0.16, 0.93], [0.34, 0.80], [0.40, 0.55], [0.36, 0.35]]
601
+ if inside?(u, v, body)
602
+ # obi
603
+ next(Math.sin(u * 60) > 0.5 ? [90, 110, 90] : [50, 70, 60]) if v > 0.38 && v < 0.45
604
+ # roundels of flowers on the red kimono
605
+ cu = (u * 7).floor
606
+ cv = (v * 10).floor
607
+ rx = (u * 7) % 1.0 - 0.5
608
+ ry = (v * 10) % 1.0 - 0.5
609
+ if Math.hypot(rx * 1.4, ry) < 0.28 && (cu + cv).even?
610
+ next noise(cu, cv) > 0.5 ? [240, 220, 150] : [120, 160, 120]
611
+ end
612
+ next mix([200, 45, 40], [160, 30, 35], v)
613
+ end
614
+ # a hand peeking from the sleeve
615
+ next [245, 232, 215] if ellipse(u, v, 0.68, 0.42, 0.03, 0.02) < 1
616
+ jitter([232, 220, 190], u, v, 6, 10)
617
+ end
618
+
619
+ # Zhang Zeduan, Along the River During the Qingming Festival
620
+ piece :qingming_scroll, "Along the River During the Qingming Festival", "Zhang Zeduan", "12th century", aspect: 2.5 do |u, v|
621
+ x = u * 2.5
622
+ silk = jitter([210, 185, 140], u, v, 8, 14)
623
+ # the rainbow bridge arching over the river
624
+ bc = 1.25
625
+ bw = 0.50
626
+ if (x - bc).abs < bw
627
+ t = (x - bc) / bw
628
+ top = 0.40 - 0.22 * (1 - t * t)
629
+ if v > top - 0.08 && v < top
630
+ next noise((x * 50).floor, (v * 40).floor) > 0.4 ? [70, 50, 40] : [180, 90, 60] if noise((x * 50).floor + 7, (v * 40).floor) > 0.35 # crowd
631
+ next silk
632
+ end
633
+ next [95, 65, 40] if v >= top && v < top + 0.06
634
+ next [80, 55, 35] if v >= top + 0.06 && v < top + 0.14 && Math.sin((x - bc) * 60 + v * 30).abs < 0.4
635
+ end
636
+ # river running across, slightly falling to the right
637
+ r1 = 0.44 + u * 0.10
638
+ r2 = 0.80 + u * 0.06
639
+ if v > r1 && v < r2
640
+ # boats with cabins, one lowering its mast under the bridge
641
+ hit = JP_HIT.([[0.45, 0.66, 0.24], [2.05, 0.74, 0.24], [1.62, 0.62, 0.10]].each do |bx, by, bl|
642
+ break [90, 60, 40] if ellipse(x, v, bx, by, bl, 0.05) < 1
643
+ break [150, 110, 70] if ellipse(x, v, bx, by - 0.07, bl * 0.6, 0.045) < 1
644
+ end)
645
+ next hit if hit
646
+ next [70, 50, 35] if JP_SEG.(x, v, 0.50, 0.60, 0.72, 0.18) < 0.014 # mast
647
+ next Math.sin(v * 60 + Math.sin(x * 8) * 2) > 0.9 ? [160, 165, 140] : [115, 125, 105]
648
+ end
649
+ # banks lined with tiled roofs and willows
650
+ cell = (x * 4).floor
651
+ cxf = (x * 4) % 1.0
652
+ far = v < r1
653
+ hv = far ? r1 - 0.17 : r2 + 0.04
654
+ if noise(cell, far ? 1 : 3) > 0.3 && cxf > 0.15 && cxf < 0.75 && v > hv && (v < r1 || !far)
655
+ next [80, 75, 70] if v < hv + 0.06
656
+ next [190, 165, 120] if v < hv + 0.15
657
+ end
658
+ if ellipse(cxf, v, 0.92, far ? hv - 0.02 : hv + 0.06, 0.28, 0.14) < 1 && noise(cell, far ? 2 : 4) > 0.35
659
+ next vnoise(u, v, 40) > 0.5 ? [100, 115, 70] : [70, 85, 55] # willows
660
+ end
661
+ next [60, 45, 35] if noise((x * 50).floor, (v * 30).floor) > 0.94 && v > 0.2 # people
662
+ silk
663
+ end
664
+
665
+ # Fan Kuan, Travelers among Mountains and Streams
666
+ piece :travelers_mountains_streams, "Travelers among Mountains and Streams", "Fan Kuan", "c. 1000", aspect: 0.5 do |u, v|
667
+ silk = jitter([180, 155, 115], u, v, 8, 12)
668
+ # the monumental central peak, filling the top
669
+ peak = [[0.03, 0.70], [0.05, 0.32], [0.12, 0.15], [0.26, 0.05], [0.50, 0.025], [0.72, 0.05], [0.87, 0.14],
670
+ [0.95, 0.32], [0.97, 0.70]]
671
+ if inside?(u, v, peak)
672
+ next [230, 220, 195] if (u - (0.78 + (v - 0.3) * 0.04)).abs < 0.012 && v > 0.26 && v < 0.56 # waterfall
673
+ next [45, 50, 35] if v < 0.09 + 0.02 * Math.sin(u * 20) && vnoise(u, v, 40) > 0.45 # scrub on the summit
674
+ tex = Math.sin(u * 140 + vnoise(u, v, 10) * 8) * 0.5 + 0.5
675
+ dot = noise((u * 80).floor, (v * 140).floor) > 0.65
676
+ base = mix([100, 80, 55], [45, 35, 25], tex * 0.6 + (dot ? 0.3 : 0) + (0.5 - (u - 0.5).abs) * 0.4)
677
+ next mix(base, [215, 200, 165], smooth(0.48, 0.66, v))
678
+ end
679
+ next mix(silk, [215, 200, 165], 0.6) if v > 0.60 && v < 0.70 # mist band
680
+ if v > 0.66
681
+ # a little caravan of travelers on the path at the lower right
682
+ next [35, 30, 25] if u > 0.62 && u < 0.84 && (v - 0.86).abs < 0.012 && noise((u * 60).floor, 9) > 0.35
683
+ # foreground boulders and groves
684
+ rock = ellipse(u, v, 0.30, 0.95, 0.34, 0.12) < 1 || ellipse(u, v, 0.92, 0.95, 0.20, 0.06) < 1
685
+ next mix([85, 68, 48], [40, 32, 22], vnoise(u, v, 20)) if rock
686
+ grove = ellipse(u, v, 0.78, 0.74, 0.20, 0.07) < 1 || ellipse(u, v, 0.18, 0.76, 0.18, 0.08) < 1
687
+ next(vnoise(u, v, 40) > 0.45 ? [40, 45, 30] : [75, 70, 45]) if grove
688
+ next mix(silk, [150, 125, 90], 0.4)
689
+ end
690
+ mix(silk, [215, 200, 165], 0.4) # pale sky
691
+ end
692
+ end
693
+ end