dopairb 0.2.0 → 0.4.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,774 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dopairb
4
+ module Gallery
5
+ # Shared shape helpers for the pieces in this file.
6
+ module ModernAncient
7
+ module_function
8
+
9
+ # Distance from (u, v) to the segment (x1, y1)-(x2, y2); sx scales the u axis.
10
+ def seg(u, v, x1, y1, x2, y2, sx = 1.0)
11
+ px = (u - x1) * sx
12
+ py = v - y1
13
+ dx = (x2 - x1) * sx
14
+ dy = y2 - y1
15
+ t = ((px * dx + py * dy) / (dx * dx + dy * dy + 1e-12)).clamp(0.0, 1.0)
16
+ Math.hypot(px - dx * t, py - dy * t)
17
+ end
18
+
19
+ # Position along an Archimedean spiral arm, 0..1 (lines where it is small).
20
+ def spiral(dx, dy, pitch, dir = 1)
21
+ t = Math.hypot(dx, dy) / pitch - dir * Math.atan2(dy, dx) / (2 * Math::PI)
22
+ t - t.floor
23
+ end
24
+
25
+ # Triangle wave 0..1..0 with period 1.
26
+ def tri(x) = 1 - (x - x.floor - 0.5).abs * 2
27
+
28
+ # A cave-painting horse facing left; s scales it, sx is the aspect.
29
+ # => :body, :mane (dark parts) or nil
30
+ def horse(u, v, x, y, s, sx)
31
+ return :body if Paint.ellipse(u * sx, v, x * sx, y, 0.13 * s, 0.065 * s) < 1
32
+ return :mane if seg(u, v, x - 0.045 * s, y - 0.03 * s, x - 0.075 * s, y - 0.085 * s, sx) < 0.028 * s
33
+ return :mane if Paint.ellipse(u * sx, v, (x - 0.085 * s) * sx, y - 0.085 * s, 0.045 * s, 0.025 * s) < 1
34
+ [[-0.05, -0.07], [-0.02, -0.04], [0.04, 0.02], [0.06, 0.08]].each do |a, b|
35
+ return :mane if seg(u, v, x + a * s, y + 0.03 * s, x + b * s, y + 0.13 * s, sx) < 0.012 * s
36
+ end
37
+ return :mane if seg(u, v, x + 0.07 * s, y - 0.02 * s, x + 0.10 * s, y + 0.07 * s, sx) < 0.012 * s
38
+ nil
39
+ end
40
+
41
+ # A tree of the Nebamun garden: trunk at the base, crown pointing away.
42
+ def nebamun_tree(u, v, along, across, palm)
43
+ return nil if along < -0.01 || along > 0.25 || across.abs > 0.08
44
+ return [110, 70, 40] if along < 0.09 && across.abs < 0.008
45
+ if palm
46
+ return [180, 50, 40] if (across / 0.02)**2 + ((along - 0.10) / 0.02)**2 < 1
47
+ fx = along - 0.09
48
+ return nil if fx < 0 || fx > 0.13
49
+ a = Math.atan2(across, fx)
50
+ return [40, 90, 50] if a.abs < 1.1 && Math.sin(a * 9).abs > 0.4
51
+ return nil
52
+ end
53
+ ce = (across / 0.055)**2 + ((along - 0.15) / 0.07)**2
54
+ return nil if ce >= 1
55
+ return [200, 60, 40] if Paint.noise((u * 60).floor, (v * 60).floor) > 0.82
56
+ ce > 0.75 ? [30, 70, 40] : [60, 115, 60]
57
+ end
58
+
59
+ # A Bayeux figure at x; kind :seated, :oath or :stand.
60
+ def bayeux_figure(u, v, x, c, kind, sx)
61
+ olive = [120, 125, 65]
62
+ y0 = kind == :seated ? 0.40 : 0.36
63
+ return [205, 170, 140] if Paint.dist(u, v, x, y0, sx) < 0.045
64
+ return c if seg(u, v, x, y0 + 0.06, x, y0 + 0.28, sx) < 0.05 && v < y0 + 0.32
65
+ case kind
66
+ when :seated
67
+ return olive if (u - x).abs < 0.05 && v.between?(0.55, 0.72) && (u > x + 0.02 || v > 0.66)
68
+ return c if seg(u, v, x, 0.66, x + 0.04, 0.78, sx) < 0.012
69
+ return [80, 70, 60] if seg(u, v, x + 0.02, 0.52, x + 0.06, 0.34, sx) < 0.006 # sword
70
+ when :oath
71
+ return c if seg(u, v, x, y0 + 0.10, x - 0.06, y0 + 0.16, sx) < 0.013
72
+ return c if seg(u, v, x, y0 + 0.10, x + 0.06, y0 + 0.16, sx) < 0.013
73
+ else
74
+ return c if seg(u, v, x, y0 + 0.10, x - 0.04, y0 + 0.04, sx) < 0.012
75
+ end
76
+ return c if v > y0 + 0.25 && ((u - x).abs - 0.008).abs < 0.006 && v < 0.79
77
+ nil
78
+ end
79
+ end
80
+
81
+ # Klimt, The Kiss
82
+ piece :the_kiss, "The Kiss", "Gustav Klimt", "1908", aspect: 1.0 do |u, v|
83
+ # her face, tilted on his shoulder, eyes closed
84
+ if ellipse(u, v, 0.575, 0.31, 0.055, 0.042) < 1
85
+ next [120, 80, 60] if (v - 0.302 - (u - 0.56) * 0.3).abs < 0.006 && (u - 0.555).abs < 0.016
86
+ next [200, 90, 80] if ellipse(u, v, 0.605, 0.325, 0.01, 0.006) < 1
87
+ next mix([248, 228, 205], [225, 190, 160], u - 0.55)
88
+ end
89
+ # her hair with flowers
90
+ if ellipse(u, v, 0.635, 0.285, 0.045, 0.075) < 1
91
+ next noise((u * 90).floor, (v * 90).floor) > 0.7 ? [235, 120, 90] : [120, 60, 30]
92
+ end
93
+ # his cheek and dark hair crowned with ivy
94
+ next [210, 160, 120] if ellipse(u, v, 0.515, 0.22, 0.04, 0.05) < 1
95
+ if ellipse(u, v, 0.47, 0.15, 0.07, 0.06) < 1
96
+ next noise((u * 70).floor, (v * 70).floor) > 0.75 ? [90, 130, 60] : [25, 22, 20]
97
+ end
98
+ # her hand on his neck, her feet over the edge
99
+ next [240, 210, 185] if ellipse(u, v, 0.505, 0.27, 0.022, 0.03) < 1
100
+ next [235, 200, 170] if ellipse(u, v, 0.585, 0.935, 0.035, 0.022) < 1
101
+ robe = [[0.38, 0.10], [0.58, 0.10], [0.72, 0.26], [0.77, 0.50], [0.72, 0.74], [0.63, 0.92],
102
+ [0.50, 0.90], [0.33, 0.74], [0.26, 0.48], [0.29, 0.22]]
103
+ if inside?(u, v, robe)
104
+ if u < 0.55 - (v - 0.3) * 0.15
105
+ # his robe: black, white and silver rectangles on gold
106
+ cx = (u * 18).floor
107
+ cy = (v * 9).floor
108
+ lx = u * 18 - cx
109
+ ly = v * 9 - cy
110
+ n = noise(cx, cy)
111
+ if lx.between?(0.15, 0.85) && ly.between?(0.08, 0.92) && n > 0.35
112
+ next n > 0.75 ? [235, 230, 215] : (n > 0.55 ? [20, 18, 18] : [160, 160, 150])
113
+ end
114
+ next jitter([220, 175, 60], u, v, 20, 40)
115
+ end
116
+ # her dress: colourful circles
117
+ cx = (u * 18).floor
118
+ cy = (v * 18).floor
119
+ n = noise(cx + 7, cy)
120
+ if Math.hypot(u * 18 - cx - 0.5, v * 18 - cy - 0.5) < 0.38
121
+ next [[200, 50, 50], [70, 90, 170], [150, 70, 140], [240, 140, 60], [235, 235, 220]][(n * 5).floor]
122
+ end
123
+ next jitter([210, 165, 60], u, v, 20, 40)
124
+ end
125
+ # flower meadow at the bottom left, dropping to the right
126
+ meadow = [[0.0, 0.66], [0.25, 0.68], [0.50, 0.80], [0.64, 0.90], [0.70, 1.0], [0.0, 1.0]]
127
+ if inside?(u, v, meadow)
128
+ f = noise((u * 50).floor, (v * 50).floor)
129
+ next [[230, 90, 80], [240, 200, 80], [150, 110, 190], [240, 240, 230]][(f * 40).floor % 4] if f > 0.72
130
+ next jitter([60, 100, 55], u, v, 18, 25)
131
+ end
132
+ next [215, 175, 80] if noise((u * 64).floor, (v * 64).floor) > 0.88
133
+ mix([140, 115, 50], [95, 80, 40], vnoise(u, v, 6))
134
+ end
135
+
136
+ # Klimt, The Tree of Life (Stoclet Frieze)
137
+ piece :tree_of_life, "The Tree of Life", "Gustav Klimt", "1909", aspect: 1.9 do |u, v|
138
+ x = u * 1.9
139
+ brown = [140, 90, 35]
140
+ gold = [205, 155, 50]
141
+ # the black bird on top of the tree
142
+ next [15, 15, 15] if ellipse(x, v, 0.95, 0.20, 0.035, 0.05) < 1 || ModernAncient.seg(x, v, 0.98, 0.16, 1.02, 0.15) < 0.01
143
+ # trunk with eye motifs
144
+ tw = 0.035 + (v - 0.4) * 0.05
145
+ if v > 0.26 && (x - 0.95).abs < tw
146
+ next [235, 225, 200] if ellipse(x, v, 0.95, ((v * 10).floor + 0.5) / 10, 0.015, 0.025) < 1 && v > 0.5
147
+ next mix(brown, gold, Math.sin(v * 60) * 0.5 + 0.5)
148
+ end
149
+ # spiralling branches: scrolls hanging off two curving boughs
150
+ scroll = nil
151
+ [[0.20, 0.30, 1], [0.45, 0.22, -1], [0.70, 0.36, 1], [0.30, 0.62, -1], [0.58, 0.55, 1],
152
+ [1.25, 0.30, -1], [1.48, 0.22, 1], [1.72, 0.36, -1], [1.62, 0.62, 1], [1.35, 0.55, -1],
153
+ [0.10, 0.52, 1], [1.82, 0.55, -1], [0.82, 0.62, -1], [1.10, 0.66, 1]].each do |cx, cy, dir|
154
+ dx = x - cx
155
+ dy = v - cy
156
+ r = Math.hypot(dx, dy)
157
+ next if r > 0.14
158
+ if ModernAncient.spiral(dx, dy, 0.05, dir) < 0.5
159
+ scroll = mix(gold, [100, 60, 25], r * 7)
160
+ break
161
+ end
162
+ end
163
+ next scroll if scroll
164
+ ax = (x - 0.95).abs
165
+ bough = 0.42 + 0.10 * Math.sin(ax * 7)
166
+ next brown if (v - bough).abs < 0.022 && ax < 0.9
167
+ bough2 = 0.30 + 0.12 * Math.sin(ax * 5 + 1)
168
+ next brown if (v - bough2).abs < 0.018 && ax.between?(0.05, 0.85)
169
+ # flowers on the ground
170
+ if v > 0.86
171
+ n = noise((u * 70).floor, (v * 40).floor)
172
+ next [[200, 70, 60], [90, 120, 170], [230, 200, 90]][(n * 30).floor % 3] if n > 0.7
173
+ next jitter([200, 175, 110], u, v, 12, 30)
174
+ end
175
+ jitter(mix([238, 225, 185], [225, 200, 140], v), u, v, 10, 15)
176
+ end
177
+
178
+ # Klimt, Portrait of Adele Bloch-Bauer I
179
+ piece :portrait_of_adele_bloch_bauer, "Portrait of Adele Bloch-Bauer I", "Gustav Klimt", "1907", aspect: 1.0 do |u, v|
180
+ skin = [238, 222, 210]
181
+ # face
182
+ fe = ellipse(u, v, 0.50, 0.25, 0.055, 0.075)
183
+ if fe < 1
184
+ next [40, 30, 30] if (v - 0.235).abs < 0.008 && ((u - 0.50).abs - 0.022).abs < 0.012
185
+ next [190, 70, 70] if ellipse(u, v, 0.50, 0.295, 0.015, 0.007) < 1
186
+ next mix(skin, [210, 185, 175], fe)
187
+ end
188
+ # black hair
189
+ next [25, 20, 22] if ellipse(u, v, 0.50, 0.19, 0.08, 0.08) < 1 && v < 0.26
190
+ # neck with golden choker
191
+ if (u - 0.50).abs < 0.028 && v.between?(0.30, 0.37)
192
+ next v.between?(0.32, 0.345) ? [230, 190, 80] : skin
193
+ end
194
+ # pale shoulders and clasped hands
195
+ next skin if ellipse(u, v, 0.50, 0.40, 0.13, 0.04) < 1
196
+ if ellipse(u, v, 0.47, 0.50, 0.07, 0.035) < 1
197
+ next ((u * 60).floor % 3).zero? ? [200, 175, 165] : skin
198
+ end
199
+ # green patch at the lower left
200
+ if u < 0.26 + 0.05 * Math.sin(v * 10) && v > 0.74
201
+ next noise((u * 50).floor, (v * 50).floor) > 0.8 ? [210, 180, 80] : jitter([70, 120, 75], u, v, 15, 20)
202
+ end
203
+ dress = [[0.36, 0.38], [0.64, 0.38], [0.70, 0.70], [0.80, 1.0], [0.22, 1.0], [0.30, 0.70]]
204
+ if inside?(u, v, dress)
205
+ # eye ornaments
206
+ gy = v * 9
207
+ row = gy.floor
208
+ gx = u * 9 + (row.odd? ? 0.5 : 0.0)
209
+ lx = gx - gx.floor - 0.5
210
+ ly = gy - row - 0.5
211
+ e = (lx / 0.38)**2 + (ly / 0.2)**2
212
+ if e < 1
213
+ next [20, 20, 20] if e < 0.18
214
+ next e < 0.55 ? [240, 235, 215] : [120, 80, 30]
215
+ end
216
+ next jitter([225, 180, 60], u, v, 20, 40)
217
+ end
218
+ # background: gold with small squares and spirals
219
+ cx = (u * 16).floor
220
+ cy = (v * 16).floor
221
+ lx = u * 16 - cx
222
+ ly = v * 16 - cy
223
+ n = noise(cx, cy)
224
+ if n > 0.7 && lx.between?(0.2, 0.8) && ly.between?(0.2, 0.8)
225
+ next n > 0.85 ? [150, 110, 40] : [210, 205, 180]
226
+ end
227
+ next [170, 120, 40] if u < 0.3 && v < 0.6 && ModernAncient.spiral(u - 0.15, v - 0.35, 0.04) < 0.2
228
+ jitter([200, 160, 55], u, v, 22, 50)
229
+ end
230
+
231
+ # Henri Rousseau, The Sleeping Gypsy
232
+ piece :the_sleeping_gypsy, "The Sleeping Gypsy", "Henri Rousseau", "1897", aspect: 1.55 do |u, v|
233
+ sx = 1.55
234
+ # the lion, sniffing from behind
235
+ if ellipse(u, v, 0.53, 0.50, 0.035, 0.065) < 1
236
+ next [30, 25, 20] if ellipse(u, v, 0.525, 0.475, 0.008, 0.012) < 1
237
+ next [210, 170, 110]
238
+ end
239
+ next [125, 80, 45] if ellipse(u, v, 0.56, 0.47, 0.065, 0.14) < 1 && u < 0.595 # mane
240
+ lion = [195, 155, 95]
241
+ next lion if ellipse(u, v, 0.71, 0.50, 0.15, 0.085) < 1
242
+ next lion if v.between?(0.52, 0.70) && [0.60, 0.645, 0.78, 0.825].any? { |lx| (u - lx).abs < 0.014 }
243
+ next lion if ModernAncient.seg(u, v, 0.85, 0.48, 0.90, 0.33, sx) < 0.012
244
+ next [110, 70, 40] if dist(u, v, 0.90, 0.32, sx) < 0.02
245
+ # the sleeping woman in a striped robe
246
+ if ellipse(u, v, 0.17, 0.75, 0.04, 0.05) < 1
247
+ next u < 0.17 ? [25, 20, 20] : [90, 55, 40]
248
+ end
249
+ if ellipse(u, v, 0.44, 0.785, 0.25, 0.06) < 1
250
+ s = (u * 30 + v * 20).floor % 5
251
+ next [[200, 60, 50], [235, 150, 60], [230, 210, 120], [70, 100, 160], [235, 230, 210]][s]
252
+ end
253
+ next [80, 50, 35] if ellipse(u, v, 0.70, 0.81, 0.03, 0.015) < 1 # feet
254
+ # mandolin and jug
255
+ next [60, 35, 20] if ellipse(u, v, 0.48, 0.91, 0.022, 0.02) < 1
256
+ next [190, 120, 50] if ellipse(u, v, 0.48, 0.91, 0.065, 0.045) < 1
257
+ next [120, 70, 30] if u.between?(0.34, 0.42) && (v - 0.91).abs < 0.01
258
+ next [175, 95, 55] if ellipse(u, v, 0.84, 0.82, 0.025, 0.05) < 1 || ellipse(u, v, 0.84, 0.765, 0.012, 0.02) < 1
259
+ # moon
260
+ next [235, 232, 205] if dist(u, v, 0.86, 0.13, sx) < 0.055
261
+ # sand
262
+ next jitter(mix([205, 180, 125], [175, 150, 100], (v - 0.6) * 2.5), u, v, 10, 25) if v > 0.585
263
+ # river and far bank hills
264
+ next mix([200, 205, 195], [160, 175, 175], Math.sin(u * 40) * 0.5 + 0.5) if v > 0.555
265
+ hill = 0.47 + 0.04 * Math.sin(u * 7 + 1) + 0.02 * Math.sin(u * 17)
266
+ next mix([170, 165, 150], [130, 130, 125], (v - hill) * 10) if v > hill
267
+ mix([25, 50, 65], [90, 130, 130], smooth(0.0, 0.5, v))
268
+ end
269
+
270
+ # Henri Rousseau, Tiger in a Tropical Storm (Surprised!)
271
+ piece :surprised, "Tiger in a Tropical Storm (Surprised!)", "Henri Rousseau", "1891", aspect: 1.25 do |u, v|
272
+ sx = 1.25
273
+ col = nil
274
+ # the tiger crouching, facing left
275
+ head = ellipse(u, v, 0.36, 0.58, 0.065, 0.075)
276
+ body = ellipse(u, v, 0.52, 0.66, 0.17, 0.075)
277
+ leg = [[0.37, 0.64, 0.33, 0.78], [0.42, 0.66, 0.40, 0.78], [0.62, 0.66, 0.62, 0.77], [0.66, 0.66, 0.69, 0.76]]
278
+ .any? { |a, b, c, d| ModernAncient.seg(u, v, a, b, c, d, sx) < 0.022 }
279
+ tail = ModernAncient.seg(u, v, 0.68, 0.64, 0.80, 0.56, sx) < 0.013 || ModernAncient.seg(u, v, 0.80, 0.56, 0.84, 0.47, sx) < 0.012
280
+ if head < 1
281
+ col = if ellipse(u, v, 0.34, 0.555, 0.012, 0.01) < 1 || ellipse(u, v, 0.385, 0.555, 0.012, 0.01) < 1
282
+ [240, 230, 160]
283
+ elsif ellipse(u, v, 0.34, 0.625, 0.03, 0.02) < 1
284
+ [200, 60, 60]
285
+ elsif ellipse(u, v, 0.33, 0.61, 0.045, 0.035) < 1
286
+ [245, 235, 215]
287
+ else
288
+ Math.sin(v * 120) > 0.6 ? [30, 20, 15] : [230, 135, 45]
289
+ end
290
+ elsif body < 1 || leg || tail
291
+ col = Math.sin(u * 90 + Math.sin(v * 25) * 2) > 0.45 ? [30, 20, 15] : mix([235, 145, 50], [245, 220, 170], (v - 0.64) * 8)
292
+ end
293
+ unless col
294
+ lx = 0.80 + 0.06 * ModernAncient.tri(v * 6)
295
+ if v < 0.4 && (u - lx).abs < 0.008
296
+ col = [250, 250, 220] # lightning
297
+ else
298
+ # layered leaves: the topmost leaf of the 3x3 neighbouring cells wins
299
+ gx = u * 8
300
+ gy = v * 7
301
+ best = -1.0
302
+ (-1..1).each do |di|
303
+ (-1..1).each do |dj|
304
+ cx = gx.floor + di
305
+ cy = gy.floor + dj
306
+ n = noise(cx, cy)
307
+ next if n < best
308
+ ox = cx + 0.5 + (noise(cx + 9, cy) - 0.5) * 0.6
309
+ oy = cy + 0.5 + (noise(cx, cy + 9) - 0.5) * 0.6
310
+ a = noise(cx + 3, cy + 5) * Math::PI
311
+ px = gx - ox
312
+ py = gy - oy
313
+ lx2 = px * Math.cos(a) + py * Math.sin(a)
314
+ ly2 = -px * Math.sin(a) + py * Math.cos(a)
315
+ e = (lx2 / 0.95)**2 + (ly2 / 0.32)**2
316
+ next if e >= 1
317
+ best = n
318
+ greens = [[35, 85, 40], [80, 140, 55], [25, 60, 35], [120, 160, 60], [55, 110, 75], [150, 135, 55], [95, 125, 45]]
319
+ g = greens[(noise(cx + 1, cy + 2) * greens.size).floor]
320
+ col = ly2.abs < 0.04 || e > 0.75 ? Color.scale(g, 0.7) : g
321
+ end
322
+ end
323
+ col ||= mix([140, 150, 130], [20, 40, 25], v * 2.5)
324
+ end
325
+ end
326
+ # slanting rain
327
+ r = u * 22 + v * 9
328
+ (r - r.floor) < 0.07 ? mix(col, [210, 220, 220], 0.45) : col
329
+ end
330
+
331
+ # Malevich, Black Square
332
+ piece :black_square, "Black Square", "Kazimir Malevich", "1915", aspect: 1.0 do |u, v|
333
+ if u.between?(0.17, 0.83) && v.between?(0.17 + (u - 0.17) * 0.01, 0.83)
334
+ next [120, 105, 80] if (vnoise(u, v, 9) - 0.5).abs < 0.018
335
+ next [80, 70, 55] if (vnoise(u + 3, v, 17) - 0.5).abs < 0.02
336
+ next jitter([22, 20, 18], u, v, 6, 20)
337
+ end
338
+ jitter(mix([238, 232, 215], [220, 210, 185], vnoise(u, v, 4)), u, v, 6, 30)
339
+ end
340
+
341
+ # Kandinsky, Composition VII
342
+ piece :composition_vii, "Composition VII", "Wassily Kandinsky", "1913", aspect: 1.5 do |u, v|
343
+ sx = 1.5
344
+ ink = [20, 15, 20]
345
+ # black lines whipping across
346
+ waves = [[0.30, 0.12, 9, 0.0], [0.62, 0.08, 7, 1.5], [0.80, 0.10, 11, 3.0], [0.20, 0.06, 15, 2.0],
347
+ [0.45, 0.15, 5, 4.0], [0.92, 0.04, 19, 1.0]]
348
+ next ink if waves.any? { |a, b, f, ph| (v - (a + b * Math.sin(u * f + ph))).abs < 0.007 }
349
+ next ink if ModernAncient.seg(u, v, 0.55, 0.15, 0.80, 0.90, sx) < 0.006
350
+ next ink if ModernAncient.seg(u, v, 0.10, 0.85, 0.50, 0.05, sx) < 0.006
351
+ # the dark vortex left of center
352
+ d = dist(u, v, 0.40, 0.48, sx)
353
+ if d < 0.17
354
+ next [10, 10, 15] if d < 0.04
355
+ a = Math.atan2(v - 0.48, (u - 0.40) * sx)
356
+ next Math.sin(d * 70 - a * 3) > 0.2 ? [25, 20, 30] : mix([70, 40, 110], [180, 60, 50], vnoise(u, v, 12))
357
+ end
358
+ # colour blobs
359
+ wob = (vnoise(u, v, 10) - 0.5) * 0.8 - 0.25
360
+ blob = [[0.66, 0.28, 0.12, 0.16, [40, 70, 175]], [0.22, 0.72, 0.10, 0.14, [205, 40, 40]],
361
+ [0.58, 0.70, 0.13, 0.10, [245, 205, 50]], [0.84, 0.66, 0.08, 0.13, [55, 150, 90]],
362
+ [0.18, 0.24, 0.08, 0.11, [130, 60, 145]], [0.88, 0.22, 0.06, 0.09, [235, 125, 40]],
363
+ [0.45, 0.16, 0.06, 0.07, [235, 150, 175]], [0.06, 0.50, 0.05, 0.12, [60, 120, 190]],
364
+ [0.72, 0.90, 0.10, 0.06, [110, 60, 40]]].find { |cx, cy, rx, ry, _| ellipse(u, v, cx, cy, rx, ry) + wob < 1 }
365
+ next blob[4] if blob
366
+ ramp(vnoise(u, v, 4) * 0.6 + vnoise(u + 2, v, 11) * 0.4, [240, 220, 170], [150, 190, 215], [235, 170, 140], [180, 215, 150], [225, 200, 215])
367
+ end
368
+
369
+ # Kandinsky, Several Circles
370
+ piece :several_circles, "Several Circles", "Wassily Kandinsky", "1926", aspect: 1.0 do |u, v|
371
+ col = mix([35, 40, 70], [8, 8, 15], dist(u, v, 0.55, 0.55) * 1.8)
372
+ halo = (dist(u, v, 0.36, 0.36) - 0.25).abs
373
+ col = mix(col, [70, 110, 210], 0.5 * (1 - halo / 0.05)) if halo < 0.05
374
+ [[0.36, 0.36, 0.22, [25, 30, 90], 0.95], [0.62, 0.60, 0.13, [205, 60, 60], 0.75],
375
+ [0.54, 0.46, 0.08, [245, 205, 60], 0.75], [0.72, 0.30, 0.065, [90, 185, 140], 0.75],
376
+ [0.30, 0.72, 0.09, [175, 80, 180], 0.7], [0.50, 0.78, 0.05, [240, 140, 60], 0.8],
377
+ [0.78, 0.76, 0.075, [60, 150, 225], 0.7], [0.46, 0.28, 0.04, [235, 235, 220], 0.8],
378
+ [0.18, 0.52, 0.035, [225, 90, 50], 0.8], [0.86, 0.50, 0.04, [205, 225, 90], 0.75],
379
+ [0.64, 0.18, 0.03, [235, 100, 150], 0.8], [0.40, 0.56, 0.05, [60, 175, 190], 0.6],
380
+ [0.70, 0.62, 0.03, [250, 250, 240], 0.8], [0.25, 0.25, 0.06, [60, 60, 150], 0.5]].each do |cx, cy, r, c, a|
381
+ d = dist(u, v, cx, cy)
382
+ col = mix(col, mix(c, Color.scale(c, 1.25), d / r), a) if d < r
383
+ end
384
+ col
385
+ end
386
+
387
+ # Matisse, Dance
388
+ piece :the_dance, "Dance", "Henri Matisse", "1910", aspect: 1.5 do |u, v|
389
+ sx = 1.5
390
+ # five dancers in a ring: [x, y (hip), lean, size]
391
+ figs = [[0.13, 0.48, -0.10, 1.15], [0.36, 0.30, 0.05, 0.95], [0.62, 0.30, -0.04, 0.9],
392
+ [0.86, 0.45, 0.08, 1.05], [0.48, 0.68, -0.12, 1.1]]
393
+ shoulders = figs.map { |x, y, l, s| [x + l * 0.6, y - 0.22 * s] }
394
+ hit = figs.each_with_index.any? do |(x, y, l, s), i|
395
+ hx, hy = shoulders[i]
396
+ dist(u, v, hx + l * 0.25, hy - 0.07 * s, sx) < 0.055 * s ||
397
+ ModernAncient.seg(u, v, x, y, hx, hy + 0.03, sx) < 0.06 * s ||
398
+ ModernAncient.seg(u, v, x, y, x - 0.07 * s, y + 0.27 * s, sx) < 0.032 * s ||
399
+ ModernAncient.seg(u, v, x, y, x + 0.08 * s, y + 0.24 * s, sx) < 0.032 * s
400
+ end
401
+ # linked arms around the ring, except the famous gap between the front pair
402
+ hit ||= [[0, 1], [1, 2], [2, 3], [3, 4]].any? do |a, b|
403
+ ax, ay = shoulders[a]
404
+ bx, by = shoulders[b]
405
+ mx = (ax + bx) / 2
406
+ my = (ay + by) / 2 - 0.05
407
+ ModernAncient.seg(u, v, ax, ay, mx, my, sx) < 0.024 || ModernAncient.seg(u, v, mx, my, bx, by, sx) < 0.024
408
+ end
409
+ fx, fy = shoulders[4]
410
+ gx, gy = shoulders[0]
411
+ hit ||= ModernAncient.seg(u, v, fx, fy, fx - 0.13, fy + 0.04, sx) < 0.024 || ModernAncient.seg(u, v, gx, gy, gx + 0.08, gy + 0.20, sx) < 0.024
412
+ next jitter([215, 85, 50], u, v, 10, 20) if hit
413
+ hill = 0.70 - 0.13 * Math.sin(Math::PI * (u * 0.9 + 0.08))
414
+ next jitter([55, 145, 75], u, v, 10, 12) if v > hill
415
+ jitter([35, 65, 155], u, v, 10, 12)
416
+ end
417
+
418
+ # Munch, Madonna
419
+ piece :madonna_munch, "Madonna", "Edvard Munch", "1894", aspect: 0.76 do |u, v|
420
+ skin = [236, 214, 190]
421
+ hair = [18, 14, 16]
422
+ # face, tilted back, eyes closed
423
+ fe = ellipse(u, v, 0.50, 0.31, 0.12, 0.10)
424
+ if fe < 1
425
+ next [60, 40, 40] if ((u - 0.50).abs - 0.05).abs < 0.025 && (v - 0.30).abs < 0.008
426
+ next [170, 60, 60] if ellipse(u, v, 0.50, 0.37, 0.02, 0.008) < 1
427
+ next mix(skin, [200, 170, 150], fe * 0.8)
428
+ end
429
+ # red beret / halo
430
+ next [205, 40, 40] if ellipse(u, v, 0.50, 0.215, 0.18, 0.05) < 1 && v < 0.235
431
+ # neck and shoulders, cut off by the swirl below
432
+ if v < 0.64 + 0.03 * Math.sin(u * 14)
433
+ # long black hair flowing down both sides
434
+ next hair if (u - (0.32 + 0.04 * Math.sin(v * 15))).abs < 0.07 && v > 0.24
435
+ next hair if (u - (0.68 + 0.04 * Math.sin(v * 15 + 2))).abs < 0.06 && v > 0.24
436
+ next hair if ellipse(u, v, 0.50, 0.28, 0.17, 0.10) < 1 && v < 0.3
437
+ # raised arm behind the head
438
+ next mix(skin, [205, 180, 160], 0.4) if ModernAncient.seg(u, v, 0.22, 0.60, 0.20, 0.30, 0.76) < 0.045
439
+ next skin if (u - 0.50).abs < 0.06 && v.between?(0.38, 0.48)
440
+ next mix(skin, [210, 185, 165], (v - 0.45) * 3) if ellipse(u, v, 0.50, 0.62, 0.28, 0.17) < 1
441
+ end
442
+ # swirling dark background
443
+ d = Math.sqrt(ellipse(u, v, 0.50, 0.42, 0.45, 0.55)) + vnoise(u, v, 5) * 0.35
444
+ s = Math.sin(d * 16 + Math.sin(Math.atan2(v - 0.45, u - 0.5) * 3) * 0.8)
445
+ ramp(s * 0.5 + 0.5, [30, 25, 35], [25, 12, 18], [75, 18, 22], [130, 30, 30])
446
+ end
447
+
448
+ # Modigliani, Jeanne Hébuterne
449
+ piece :portrait_of_jeanne_hebuterne, "Jeanne Hébuterne", "Amedeo Modigliani", "1918", aspect: 0.65 do |u, v|
450
+ sx = 0.65
451
+ th = 0.16
452
+ du = (u - 0.52) * sx
453
+ dv = v - 0.27
454
+ x = du * Math.cos(th) + dv * Math.sin(th)
455
+ y = -du * Math.sin(th) + dv * Math.cos(th)
456
+ skin = [236, 196, 160]
457
+ hair = [135, 60, 35]
458
+ if (x / 0.095)**2 + (y / 0.15)**2 < 1 && y > -0.07
459
+ next [85, 115, 125] if ((x.abs - 0.038) / 0.026)**2 + ((y + 0.01) / 0.011)**2 < 1 # almond eyes
460
+ next [195, 70, 60] if (x / 0.022)**2 + ((y - 0.095) / 0.008)**2 < 1 # mouth
461
+ next mix(skin, [200, 150, 115], 0.6) if x.between?(-0.004, 0.01) && y.between?(0.0, 0.06) # nose
462
+ next mix(skin, [215, 160, 125], smooth(0.0, 0.1, -x) * 0.5)
463
+ end
464
+ # reddish-brown hair
465
+ next jitter(hair, u, v, 15, 25) if (x / 0.12)**2 + ((y + 0.02) / 0.17)**2 < 1 && y < 0.08
466
+ next jitter(hair, u, v, 15, 25) if ((x - 0.085) / 0.04)**2 + ((y - 0.12) / 0.10)**2 < 1
467
+ # long neck, leaning with the head
468
+ nc = 0.50 - (v - 0.40) * 0.12
469
+ if (u - nc).abs < 0.065 && v.between?(0.38, 0.66)
470
+ next mix(skin, [210, 160, 125], ((u - nc) / 0.065 + 1) * 0.4)
471
+ end
472
+ # dark dress with sloping shoulders
473
+ shoulder = 0.60 + 0.5 * (u - 0.49).abs**1.3
474
+ if v > shoulder
475
+ next [225, 220, 205] if v < shoulder + 0.03 && (u - 0.49).abs < 0.2 && v < 0.72
476
+ next jitter([40, 40, 55], u, v, 10, 15)
477
+ end
478
+ # warm ochre background, darker door at the left
479
+ next jitter([140, 85, 50], u, v, 12, 15) if u < 0.18
480
+ jitter(mix([215, 150, 75], [190, 120, 60], v), u, v, 14, 12)
481
+ end
482
+
483
+ # Paul Klee, Senecio
484
+ piece :senecio, "Senecio", "Paul Klee", "1922", aspect: 0.94 do |u, v|
485
+ sx = 0.94
486
+ d = dist(u, v, 0.50, 0.46, sx)
487
+ if d < 0.40
488
+ # eyes: a red square and a red dot
489
+ next [210, 40, 40] if (u - 0.37).abs < 0.03 && (v - 0.42).abs < 0.03
490
+ next [240, 230, 210] if (u - 0.37).abs < 0.06 && (v - 0.42).abs < 0.055
491
+ next [200, 40, 40] if dist(u, v, 0.63, 0.43, sx) < 0.03
492
+ next [245, 235, 215] if dist(u, v, 0.63, 0.43, sx) < 0.055
493
+ # raised eyebrow arc and the nose / mouth lines
494
+ next [140, 70, 40] if (dist(u, v, 0.43, 0.42, sx) - 0.13).abs < 0.01 && v < 0.38
495
+ next [150, 80, 50] if (u - 0.50).abs < 0.008 && v.between?(0.36, 0.58)
496
+ next [150, 80, 50] if (v - 0.62).abs < 0.008 && (u - 0.50).abs < 0.06
497
+ next [190, 100, 60] if (d - 0.39).abs < 0.012
498
+ # flat geometric blocks
499
+ cx = ((u - 0.10) / 0.2).floor
500
+ cy = ((v - 0.06) / 0.16).floor
501
+ pal = [[240, 170, 80], [245, 200, 120], [235, 140, 90], [245, 210, 170], [230, 120, 60], [240, 185, 150]]
502
+ c = pal[(noise(cx + 4, cy + 2) * pal.size).floor]
503
+ c = mix(c, [250, 235, 210], 0.35) if u < 0.5 && v < 0.55
504
+ next jitter(c, u, v, 8, 20)
505
+ end
506
+ # neck and shoulders at the bottom
507
+ next [235, 175, 120] if v > 0.84 && (u - 0.5).abs < 0.12
508
+ next [215, 110, 60] if v > 0.92
509
+ jitter(mix([215, 120, 55], [195, 95, 45], v), u, v, 10, 20)
510
+ end
511
+
512
+ # Mondrian, Composition with Red, Blue and Yellow
513
+ piece :composition_with_red_blue_yellow, "Composition with Red, Blue and Yellow", "Piet Mondrian", "1930", aspect: 1.0 do |u, v|
514
+ w = 0.022
515
+ line = (u - 0.30).abs < w || (v - 0.70).abs < w ||
516
+ (u < 0.30 && (v - 0.36).abs < w) ||
517
+ (v > 0.70 && (u - 0.92).abs < w * 0.8) ||
518
+ (u > 0.92 && (v - 0.86).abs < w * 0.8)
519
+ next [20, 20, 25] if line
520
+ next [220, 35, 30] if u > 0.30 && v < 0.70
521
+ next [30, 50, 150] if u < 0.30 && v > 0.70
522
+ next [245, 215, 50] if u > 0.92 && v > 0.86
523
+ [240, 238, 230]
524
+ end
525
+
526
+ # Hilma af Klint, The Ten Largest, No. 7, Adulthood
527
+ piece :the_ten_largest_no7, "The Ten Largest, No. 7, Adulthood", "Hilma af Klint", "1907", aspect: 0.75 do |u, v|
528
+ sx = 0.75
529
+ # big yellow spiral
530
+ dx = (u - 0.40) * sx
531
+ dy = v - 0.30
532
+ next [250, 225, 110] if Math.hypot(dx, dy) < 0.20 && ModernAncient.spiral(dx, dy, 0.045) < 0.35
533
+ # rosettes: [cx, cy, radius, petals, colour, centre colour]
534
+ petal = nil
535
+ [[0.68, 0.58, 0.16, 8, [165, 200, 230], [250, 230, 140]],
536
+ [0.28, 0.74, 0.13, 6, [250, 230, 140], [230, 110, 120]],
537
+ [0.78, 0.18, 0.09, 5, [240, 200, 215], [120, 150, 200]],
538
+ [0.22, 0.12, 0.06, 4, [165, 200, 230], [250, 250, 240]],
539
+ [0.60, 0.88, 0.07, 6, [240, 200, 215], [165, 200, 230]]].each do |cx, cy, rad, n, c, cc|
540
+ ex = (u - cx) * sx
541
+ ey = v - cy
542
+ rr = Math.hypot(ex, ey)
543
+ next if rr > rad
544
+ if rr < rad * 0.3
545
+ petal = cc
546
+ elsif rr < rad * (0.55 + 0.45 * Math.cos(Math.atan2(ey, ex) * n / 2.0).abs)
547
+ petal = c
548
+ end
549
+ break if petal
550
+ end
551
+ next petal if petal
552
+ # lettering-like curls
553
+ next [90, 60, 80] if u < 0.45 && (v - 0.47 - 0.015 * Math.sin(u * 70)).abs < 0.005
554
+ next [90, 60, 80] if v > 0.92 && u > 0.1 && ModernAncient.spiral((u * 30 % 1 - 0.5) * 0.04, v - 0.955, 0.008) < 0.25
555
+ # soft pale blue forms
556
+ next [190, 215, 230] if ellipse(u, v, 0.15, 0.50, 0.10, 0.07) + (vnoise(u, v, 12) - 0.5) * 0.5 < 1
557
+ jitter(mix([238, 150, 110], [230, 135, 100], v), u, v, 10, 15)
558
+ end
559
+
560
+ # Lascaux cave, the Hall of the Bulls
561
+ piece :lascaux_horses, "Lascaux Cave Paintings (Horses and Aurochs)", "unknown artists", "c. 17,000 BC", aspect: 1.8 do |u, v|
562
+ sx = 1.8
563
+ rock = ramp(vnoise(u, v, 5) * 0.7 + vnoise(u, v, 17) * 0.3, [140, 100, 65], [200, 165, 115], [230, 205, 160])
564
+ rock = Color.scale(rock, 0.65 + 0.35 * smooth(0.0, 0.35, v) - 0.25 * smooth(0.8, 1.0, v))
565
+ black = [30, 22, 18]
566
+ # the great aurochs
567
+ be = ellipse(u * sx, v, 0.55 * sx, 0.38, 0.30, 0.13)
568
+ next black if ellipse(u * sx, v, 0.33 * sx, 0.34, 0.06, 0.065) < 1
569
+ next black if be.between?(0.72, 1.0)
570
+ next black if be < 1 && noise((u * 50).floor, (v * 30).floor) > 0.9
571
+ next black if ModernAncient.seg(u, v, 0.32, 0.29, 0.29, 0.15, sx) < 0.012 || ModernAncient.seg(u, v, 0.35, 0.29, 0.40, 0.16, sx) < 0.012
572
+ legs = [[0.42, 0.48, 0.41, 0.66], [0.47, 0.50, 0.48, 0.66], [0.64, 0.48, 0.66, 0.64], [0.68, 0.46, 0.72, 0.62]]
573
+ next black if legs.any? { |a, b, c, d| ModernAncient.seg(u, v, a, b, c, d, sx) < 0.016 }
574
+ next black if ModernAncient.seg(u, v, 0.71, 0.36, 0.75, 0.55, sx) < 0.008
575
+ next mix(rock, [235, 215, 175], 0.3) if be < 1
576
+ # horses
577
+ horse = nil
578
+ [[0.15, 0.68, 1.0, [150, 60, 35]], [0.84, 0.72, 0.9, [90, 55, 30]], [0.55, 0.82, 0.6, [175, 80, 40]],
579
+ [0.90, 0.30, 0.6, [120, 70, 40]]].each do |x, y, s, c|
580
+ part = ModernAncient.horse(u, v, x, y, s, sx)
581
+ next unless part
582
+ horse = part == :body ? mix(c, [205, 150, 90], smooth(y, y + 0.06 * s, v)) : black
583
+ break
584
+ end
585
+ next horse if horse
586
+ # dots
587
+ dots = [[0.62, 0.15], [0.65, 0.15], [0.68, 0.15], [0.71, 0.15], [0.10, 0.30], [0.12, 0.33], [0.36, 0.86], [0.39, 0.86]]
588
+ next [60, 30, 20] if dots.any? { |x, y| dist(u, v, x, y, sx) < 0.018 }
589
+ # cracks
590
+ next Color.scale(rock, 0.7) if (vnoise(u + 5, v, 6) - 0.5).abs < 0.012
591
+ rock
592
+ end
593
+
594
+ # Tomb of Nebamun, Pool in a Garden
595
+ piece :garden_of_nebamun, "Pool in a Garden (Tomb of Nebamun)", "unknown Egyptian artist", "c. 1350 BC", aspect: 0.86 do |u, v|
596
+ sx = 0.86
597
+ # the pool, seen from above
598
+ if u.between?(0.27, 0.73) && v.between?(0.30, 0.70)
599
+ next [60, 45, 35] if u < 0.285 || u > 0.715 || v < 0.315 || v > 0.685
600
+ # ducks and fish
601
+ if [[0.40, 0.42], [0.58, 0.58]].any? { |x, y| ellipse(u, v, x, y, 0.045, 0.025) < 1 }
602
+ next [235, 230, 215]
603
+ end
604
+ next [120, 80, 50] if [[0.40, 0.42], [0.58, 0.58]].any? { |x, y| ellipse(u, v, x - 0.05, y - 0.02, 0.015, 0.013) < 1 }
605
+ next [190, 150, 130] if [[0.58, 0.40], [0.40, 0.60], [0.50, 0.50]].any? { |x, y| ellipse(u, v, x, y, 0.04, 0.015) < 1 }
606
+ # lotus flowers along the edge
607
+ if (u < 0.33 || u > 0.67 || v < 0.36 || v > 0.64) && noise((u * 40).floor, (v * 40).floor) > 0.6
608
+ next noise((u * 40).floor + 1, (v * 40).floor) > 0.5 ? [240, 240, 230] : [70, 130, 120]
609
+ end
610
+ w = v * 22 + ModernAncient.tri(u * 25) * 0.4
611
+ next (w - w.floor) < 0.18 ? [30, 45, 80] : [70, 115, 170]
612
+ end
613
+ # rows of trees laid flat around the pool, pointing away from it
614
+ trees = []
615
+ 5.times { |i| trees << [0.25 + i * 0.125, 0.27, 0.0, -1.0, i.odd?] }
616
+ 5.times { |i| trees << [0.25 + i * 0.125, 0.73, 0.0, 1.0, i.even?] }
617
+ 3.times { |i| trees << [0.25, 0.38 + i * 0.12, -1.0, 0.0, i.odd?] }
618
+ 3.times { |i| trees << [0.75, 0.38 + i * 0.12, 1.0, 0.0, i.even?] }
619
+ tree = nil
620
+ trees.each do |bx, by, ddx, ddy, palm|
621
+ px = (u - bx) * sx
622
+ py = v - by
623
+ tree = ModernAncient.nebamun_tree(u, v, px * ddx + py * ddy, px * ddy - py * ddx, palm)
624
+ break if tree
625
+ end
626
+ next tree if tree
627
+ jitter([226, 205, 165], u, v, 10, 20)
628
+ end
629
+
630
+ # The Bayeux Tapestry, Harold's oath to William
631
+ piece :bayeux_tapestry, "Bayeux Tapestry (Harold's Oath)", "unknown embroiderers", "c. 1070", aspect: 2.6 do |u, v|
632
+ sx = 2.6
633
+ terra = [175, 90, 60]
634
+ olive = [120, 125, 65]
635
+ blue = [85, 105, 130]
636
+ linen = jitter([236, 224, 192], u, v, 8, 40)
637
+ # border lines and the borders with small beasts
638
+ next [120, 90, 70] if (v - 0.17).abs < 0.008 || (v - 0.83).abs < 0.008
639
+ if v < 0.17 || v > 0.83
640
+ bv = v < 0.17 ? v / 0.17 : (v - 0.83) / 0.17
641
+ k = u * 30 - bv * 0.6
642
+ cell = k.floor
643
+ lx = k - cell
644
+ next [terra, olive, blue][cell % 3] if lx < 0.12
645
+ next [olive, terra][(cell / 2) % 2] if cell.even? && ellipse(lx, bv, 0.55, 0.5, 0.22, 0.17) < 1
646
+ next linen
647
+ end
648
+ # Latin inscription along the top
649
+ if v.between?(0.21, 0.29) && u.between?(0.08, 0.92)
650
+ lx = u * 70
651
+ li = lx.floor
652
+ f = lx - li
653
+ n = noise(li, 3)
654
+ next [55, 65, 95] if n > 0.15 && (f < 0.3 || (n > 0.6 && (v - 0.25).abs < 0.012 && f < 0.7))
655
+ next linen
656
+ end
657
+ # William on his throne, Harold swearing, two witnesses
658
+ fig = nil
659
+ [[0.14, blue, :seated], [0.21, olive, :stand], [0.44, terra, :oath], [0.60, olive, :stand],
660
+ [0.655, blue, :stand], [0.71, terra, :stand]].each do |x, c, kind|
661
+ fig = ModernAncient.bayeux_figure(u, v, x, c, kind, sx)
662
+ break if fig
663
+ end
664
+ next fig if fig
665
+ # two reliquaries Harold swears on
666
+ [0.375, 0.505].each do |x|
667
+ fig = [200, 160, 80] if (u - x).abs < 0.02 && v.between?(0.53, 0.62)
668
+ fig = blue if (u - x).abs < 0.005 && v.between?(0.62, 0.79)
669
+ end
670
+ next fig if fig
671
+ # a building at the left and a horse at the right
672
+ next [terra, olive][(u * 80).floor % 2] if u.between?(0.02, 0.06) && v.between?(0.32, 0.80)
673
+ next terra if u < 0.08 && v.between?(0.30, 0.34)
674
+ next olive if ellipse(u, v, 0.86, 0.55, 0.06, 0.09) < 1
675
+ next olive if ModernAncient.seg(u, v, 0.81, 0.52, 0.78, 0.40, sx) < 0.02 || dist(u, v, 0.77, 0.40, sx) < 0.03
676
+ next olive if v.between?(0.60, 0.79) && [0.82, 0.84, 0.88, 0.90].any? { |x| (u - x).abs < 0.004 }
677
+ # a stylized tree between scenes
678
+ next olive if ModernAncient.seg(u, v, 0.28, 0.79, 0.28, 0.45, sx) < 0.006
679
+ next [terra, olive][(v * 40).floor % 2] if ellipse(u, v, 0.28, 0.42, 0.025, 0.08) < 1
680
+ linen
681
+ end
682
+
683
+ # Pompeii, the Alexander Mosaic
684
+ piece :alexander_mosaic, "Alexander Mosaic", "unknown, Pompeii", "c. 100 BC", aspect: 1.86 do |u, v|
685
+ sx = 1.86
686
+ # lost areas show bare plaster
687
+ lost = (u < 0.42 && vnoise(u, v, 5) + (0.42 - u) * 0.6 > 0.82) || (v > 0.86 && u < 0.3 && vnoise(u, v, 9) > 0.45)
688
+ next jitter([195, 182, 158], u, v, 6, 30) if lost && dist(u, v, 0.22, 0.34, sx) >= 0.08
689
+ # tesserae: quantize to tiles
690
+ nx = 80
691
+ ny = 43
692
+ iu = (u * nx).floor
693
+ iv = (v * ny).floor
694
+ shade = 0.88 + noise(iu, iv) * 0.2
695
+ shade *= 0.85 if u * nx - iu < 0.12 || v * ny - iv < 0.12
696
+ tu = (iu + 0.5) / nx
697
+ tv = (iv + 0.5) / ny
698
+ wheel = dist(tu, tv, 0.71, 0.66, sx)
699
+ col =
700
+ if dist(tu, tv, 0.22, 0.30, sx) < 0.045 then [205, 160, 120] # Alexander's head
701
+ elsif ellipse(tu, tv, 0.23, 0.42, 0.04, 0.09) < 1 then [130, 125, 115] # armour
702
+ elsif ellipse(tu, tv, 0.20, 0.60, 0.10, 0.11) < 1 || ModernAncient.seg(tu, tv, 0.26, 0.55, 0.31, 0.42, sx) < 0.04
703
+ [95, 60, 35] # Bucephalus
704
+ elsif dist(tu, tv, 0.70, 0.27, sx) < 0.04 then [195, 150, 110] # Darius
705
+ elsif ModernAncient.seg(tu, tv, 0.68, 0.33, 0.60, 0.28, sx) < 0.015 then [195, 150, 110] # his outstretched arm
706
+ elsif ellipse(tu, tv, 0.71, 0.38, 0.04, 0.07) < 1 then [120, 50, 70]
707
+ elsif dist(tu, tv, 0.80, 0.28, sx) < 0.035 then [190, 145, 110] # charioteer
708
+ elsif ellipse(tu, tv, 0.80, 0.38, 0.035, 0.07) < 1 then [130, 90, 60]
709
+ elsif (wheel - 0.09).abs < 0.015 || (wheel < 0.09 && Math.sin(Math.atan2(tv - 0.66, tu - 0.71) * 6).abs < 0.15)
710
+ [50, 35, 25] # chariot wheel
711
+ elsif tu.between?(0.62, 0.84) && tv.between?(0.44, 0.58) then [110, 70, 40] # chariot box
712
+ elsif ellipse(tu, tv, 0.90, 0.62, 0.09, 0.10) < 1 || ModernAncient.seg(tu, tv, 0.94, 0.58, 0.99, 0.45, sx) < 0.03
713
+ [35, 30, 28] # black horses
714
+ elsif ellipse(tu, tv, 0.48, 0.70, 0.09, 0.12) < 1 then [140, 90, 50] # Persian horse from behind
715
+ elsif ellipse(tu, tv, 0.52, 0.52, 0.03, 0.08) < 1 then [170, 130, 90]
716
+ elsif (tu - (0.33 + (0.6 - tv) * 0.05)).abs < 0.008 && tv.between?(0.08, 0.5) then [80, 70, 55] # bare tree
717
+ elsif ModernAncient.seg(tu, tv, 0.33, 0.20, 0.27, 0.08, sx) < 0.008 then [80, 70, 55]
718
+ elsif tv.between?(0.02, 0.55) && tu.between?(0.40, 0.90) && (((tu + (0.55 - tv) * 0.35) * 20) % 1) < 0.25
719
+ [45, 35, 28] # forest of spears
720
+ elsif tv > 0.80 && noise((tu * 20).floor, (tv * 12).floor) > 0.7 then [160, 40, 35] # fallen shields
721
+ elsif tv > 0.56 then [150, 110, 70] # ground
722
+ else [215, 190, 150]
723
+ end
724
+ Color.scale(col, shade)
725
+ end
726
+
727
+ # Ravenna, San Vitale: Emperor Justinian and His Retinue
728
+ piece :justinian_mosaic, "Emperor Justinian and His Retinue", "unknown, Ravenna", "c. 547", aspect: 1.7 do |u, v|
729
+ sx = 1.7
730
+ nx = 85
731
+ ny = 50
732
+ iu = (u * nx).floor
733
+ iv = (v * ny).floor
734
+ shade = 0.85 + noise(iu, iv) * 0.25
735
+ shade *= 0.88 if u * nx - iu < 0.12 || v * ny - iv < 0.12
736
+ # jewelled border at the top
737
+ next Color.scale((u * 30).floor.even? ? [40, 90, 80] : [120, 40, 60], shade) if v < 0.06
738
+ col = nil
739
+ # green shield with a chi-rho among the guards at the left
740
+ if ellipse(u, v, 0.13, 0.62, 0.05, 0.14) < 1
741
+ col = (u - 0.13).abs < 0.006 || (v - 0.62).abs < 0.008 ? [230, 200, 90] : [40, 110, 70]
742
+ end
743
+ 9.times do |i|
744
+ break if col
745
+ x = 0.10 + i * 0.10
746
+ emperor = i == 4
747
+ hd = dist(u, v, x, 0.22, sx)
748
+ if emperor && (hd - 0.11).abs < 0.01
749
+ col = [120, 80, 30]
750
+ elsif emperor && hd < 0.11 && hd >= 0.065
751
+ col = [250, 215, 100]
752
+ elsif hd < 0.065
753
+ col = if v >= 0.195 then [225, 180, 150]
754
+ elsif emperor then [230, 200, 80]
755
+ else [60, 40, 30]
756
+ end
757
+ elsif (u - x).abs < 0.036 + (v - 0.30) * 0.04 && v.between?(0.30, 0.86)
758
+ cloak = [3, 5].include?(i) && u < x
759
+ col = if emperor
760
+ v > 0.36 && v < 0.5 && (u - x + 0.015).abs < 0.015 ? [230, 190, 80] : [110, 40, 95]
761
+ elsif cloak then [175, 130, 80]
762
+ elsif ((u - x).abs - 0.018).abs < 0.003 then [150, 110, 110]
763
+ elsif i > 6 && v < 0.5 then [210, 190, 110]
764
+ else [235, 230, 215]
765
+ end
766
+ elsif v.between?(0.86, 0.89) && ((u - x).abs - 0.015).abs < 0.01
767
+ col = [30, 25, 25]
768
+ end
769
+ end
770
+ col ||= v > 0.86 ? [60, 115, 60] : (noise(iu + 3, iv) > 0.8 ? [250, 220, 120] : [215, 170, 60])
771
+ Color.scale(col, shade)
772
+ end
773
+ end
774
+ end