@tileflow/maps 0.1.0-alpha.16 → 0.1.0-alpha.18

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.
Files changed (99) hide show
  1. package/README.md +70 -63
  2. package/THIRD_PARTY_NOTICES.md +7 -0
  3. package/assets/streets/build-trees.mjs +167 -273
  4. package/assets/streets/climate/NOTICE.txt +9 -0
  5. package/assets/streets/climate/build.py +47 -0
  6. package/assets/streets/climate/koppen-geiger-1991-2020.tif +0 -0
  7. package/assets/streets/climate/recipe.json +13 -0
  8. package/assets/streets/icons/streets-tree-columnar-dark.pattern.svg +200 -105
  9. package/assets/streets/icons/streets-tree-columnar.pattern.svg +200 -105
  10. package/assets/streets/icons/streets-tree-conifer-dark.pattern.svg +260 -200
  11. package/assets/streets/icons/streets-tree-conifer.pattern.svg +260 -200
  12. package/assets/streets/icons/streets-tree-open-dark.pattern.svg +170 -200
  13. package/assets/streets/icons/streets-tree-open.pattern.svg +170 -200
  14. package/assets/streets/icons/streets-tree-round-dark.pattern.svg +160 -158
  15. package/assets/streets/icons/streets-tree-round.pattern.svg +160 -158
  16. package/assets/streets/icons/streets-tree-tall-dark.pattern.svg +166 -156
  17. package/assets/streets/icons/streets-tree-tall.pattern.svg +166 -156
  18. package/assets/streets/icons/unified-day-aerialway.svg +11 -0
  19. package/assets/streets/icons/unified-day-airport.svg +11 -0
  20. package/assets/streets/icons/unified-day-bank.svg +11 -0
  21. package/assets/streets/icons/unified-day-bar.svg +11 -0
  22. package/assets/streets/icons/unified-day-beach.svg +11 -0
  23. package/assets/streets/icons/unified-day-beauty.svg +11 -0
  24. package/assets/streets/icons/unified-day-bookstore.svg +11 -0
  25. package/assets/streets/icons/unified-day-buddhist.svg +11 -0
  26. package/assets/streets/icons/unified-day-bus.svg +11 -0
  27. package/assets/streets/icons/unified-day-campsite.svg +11 -0
  28. package/assets/streets/icons/unified-day-car-repair.svg +11 -0
  29. package/assets/streets/icons/unified-day-car.svg +11 -0
  30. package/assets/streets/icons/unified-day-charging-station.svg +11 -0
  31. package/assets/streets/icons/unified-day-christian.svg +11 -0
  32. package/assets/streets/icons/unified-day-clothing.svg +11 -0
  33. package/assets/streets/icons/unified-day-coffee.svg +11 -0
  34. package/assets/streets/icons/unified-day-college.svg +11 -0
  35. package/assets/streets/icons/unified-day-community.svg +11 -0
  36. package/assets/streets/icons/unified-day-convenience.svg +11 -0
  37. package/assets/streets/icons/unified-day-dentist.svg +11 -0
  38. package/assets/streets/icons/unified-day-doctor.svg +11 -0
  39. package/assets/streets/icons/unified-day-dog-park.svg +11 -0
  40. package/assets/streets/icons/unified-day-drinking-water.svg +11 -0
  41. package/assets/streets/icons/unified-day-education.svg +11 -0
  42. package/assets/streets/icons/unified-day-electronics.svg +11 -0
  43. package/assets/streets/icons/unified-day-embassy.svg +11 -0
  44. package/assets/streets/icons/unified-day-fast-food.svg +11 -0
  45. package/assets/streets/icons/unified-day-ferry.svg +11 -0
  46. package/assets/streets/icons/unified-day-fire-station.svg +11 -0
  47. package/assets/streets/icons/unified-day-fitness.svg +11 -0
  48. package/assets/streets/icons/unified-day-florist.svg +11 -0
  49. package/assets/streets/icons/unified-day-food.svg +11 -0
  50. package/assets/streets/icons/unified-day-fuel.svg +11 -0
  51. package/assets/streets/icons/unified-day-garden.svg +11 -0
  52. package/assets/streets/icons/unified-day-golf.svg +11 -0
  53. package/assets/streets/icons/unified-day-grocery.svg +11 -0
  54. package/assets/streets/icons/unified-day-harbor.svg +11 -0
  55. package/assets/streets/icons/unified-day-hardware.svg +11 -0
  56. package/assets/streets/icons/unified-day-health.svg +11 -0
  57. package/assets/streets/icons/unified-day-jewish.svg +11 -0
  58. package/assets/streets/icons/unified-day-laundry.svg +11 -0
  59. package/assets/streets/icons/unified-day-library.svg +11 -0
  60. package/assets/streets/icons/unified-day-lodging.svg +11 -0
  61. package/assets/streets/icons/unified-day-major-transit.svg +11 -0
  62. package/assets/streets/icons/unified-day-mountain.svg +11 -0
  63. package/assets/streets/icons/unified-day-muslim.svg +11 -0
  64. package/assets/streets/icons/unified-day-natural.svg +11 -0
  65. package/assets/streets/icons/unified-day-nature.svg +11 -0
  66. package/assets/streets/icons/unified-day-parking-garage.svg +11 -0
  67. package/assets/streets/icons/unified-day-parking.svg +11 -0
  68. package/assets/streets/icons/unified-day-pharmacy.svg +11 -0
  69. package/assets/streets/icons/unified-day-playground.svg +11 -0
  70. package/assets/streets/icons/unified-day-plaza.svg +11 -0
  71. package/assets/streets/icons/unified-day-police.svg +11 -0
  72. package/assets/streets/icons/unified-day-post.svg +11 -0
  73. package/assets/streets/icons/unified-day-prison.svg +11 -0
  74. package/assets/streets/icons/unified-day-rail-light.svg +11 -0
  75. package/assets/streets/icons/unified-day-rail.svg +11 -0
  76. package/assets/streets/icons/unified-day-services.svg +11 -0
  77. package/assets/streets/icons/unified-day-shopping.svg +11 -0
  78. package/assets/streets/icons/unified-day-sport.svg +11 -0
  79. package/assets/streets/icons/unified-day-stadium.svg +11 -0
  80. package/assets/streets/icons/unified-day-swimming.svg +11 -0
  81. package/assets/streets/icons/unified-day-toilet.svg +11 -0
  82. package/assets/streets/icons/unified-day-town-hall.svg +11 -0
  83. package/assets/streets/icons/unified-day-waterfall.svg +11 -0
  84. package/assets/streets/icons/unified-day-worship.svg +11 -0
  85. package/assets/streets/models/streets-tree-columnar.far.glb +0 -0
  86. package/assets/streets/models/streets-tree-columnar.glb +0 -0
  87. package/assets/streets/models/streets-tree-conifer.far.glb +0 -0
  88. package/assets/streets/models/streets-tree-conifer.glb +0 -0
  89. package/assets/streets/models/streets-tree-open.far.glb +0 -0
  90. package/assets/streets/models/streets-tree-open.glb +0 -0
  91. package/assets/streets/models/streets-tree-round.far.glb +0 -0
  92. package/assets/streets/models/streets-tree-round.glb +0 -0
  93. package/assets/streets/models/streets-tree-tall.far.glb +0 -0
  94. package/assets/streets/models/streets-tree-tall.glb +0 -0
  95. package/dist/index.d.ts +829 -132
  96. package/dist/index.js +1397 -539
  97. package/package.json +3 -4
  98. package/docs/design-and-assets.md +0 -247
  99. package/docs/map-1777.md +0 -91
@@ -33,144 +33,6 @@ function face(m, a, b, c, outward) {
33
33
 
34
34
  // Small irregular lobes, with a continuous normal field and a faceted silhouette. The
35
35
  // variation is in the authored mesh, so it never shimmers or changes while the camera moves.
36
- function crown(m, centre, radii, seed, far, farSides = 6) {
37
- const sides = far ? farSides : 7;
38
- const rings = far ? 2 : 4;
39
- const top = vertex(m, add(centre, [0, radii[1], 0]), [0, 1, 0], 1.07);
40
- const rows = [];
41
- for (let r = 1; r < rings; r++) {
42
- const latitude = (r / rings) * Math.PI;
43
- const row = [];
44
- for (let s = 0; s < sides; s++) {
45
- const angle = (s / sides) * tau + seed * 0.7 + (r % 2) * 0.1;
46
- const ripple = 1 + 0.065 * Math.sin(angle * 3 + seed) + 0.035 * Math.cos(angle * 5 - seed);
47
- const n = [
48
- Math.sin(latitude) * Math.cos(angle),
49
- Math.cos(latitude),
50
- Math.sin(latitude) * Math.sin(angle),
51
- ];
52
- row.push(
53
- vertex(
54
- m,
55
- add(
56
- centre,
57
- n.map((v, i) => v * radii[i] * (i === 1 ? 1 : ripple)),
58
- ),
59
- n.map((v, i) => v / radii[i]),
60
- 0.94 + 0.07 * n[1] + 0.025 * Math.sin(s * 2 + seed),
61
- ),
62
- );
63
- }
64
- rows.push(row);
65
- }
66
- const bottom = vertex(m, add(centre, [0, -radii[1], 0]), [0, -1, 0], 0.86);
67
- for (let s = 0; s < sides; s++) {
68
- const n = (s + 1) % sides;
69
- face(m, top, rows[0][s], rows[0][n], [0, 1, 0]);
70
- for (let r = 0; r + 1 < rows.length; r++) {
71
- const out = sub(m.positions.slice(rows[r][s] * 3, rows[r][s] * 3 + 3), centre);
72
- face(m, rows[r][s], rows[r + 1][s], rows[r + 1][n], out);
73
- face(m, rows[r][s], rows[r + 1][n], rows[r][n], out);
74
- }
75
- face(m, bottom, rows.at(-1)[n], rows.at(-1)[s], [0, -1, 0]);
76
- }
77
- }
78
-
79
- // Open foliage is a curved, two-sided blade with a raised midrib and drooping
80
- // flanks. An ovate outline tapers to its tips, without a vertical, coin-like bevel.
81
- function leafBlade(m, centre, radii, seed, far, tilt, shade) {
82
- const yaw = Math.atan2(centre[0], centre[2]) + Math.sin(seed * golden) * 0.22;
83
- const turn = ([x, y, z]) => {
84
- const yy = y * Math.cos(tilt) - z * Math.sin(tilt);
85
- const zz = y * Math.sin(tilt) + z * Math.cos(tilt);
86
- return [x * Math.cos(yaw) + zz * Math.sin(yaw), yy, -x * Math.sin(yaw) + zz * Math.cos(yaw)];
87
- };
88
- const stations = far ? [-1, 0, 1] : [-1, -0.8, -0.45, 0, 0.45, 0.8, 1];
89
- const width = radii[2],
90
- length = radii[0];
91
- const rows = stations.map((t) => {
92
- const reach = Math.sqrt(1 - t * t) * (1 - t * 0.18);
93
- return (Math.abs(t) === 1 ? [0] : [-1, 0, 1]).map((s) => {
94
- const x = width * (s * reach + 0.08 * (1 - t * t));
95
- const z = length * t;
96
- const y =
97
- radii[1] * (1 - t * t) -
98
- length * (0.28 * t * t - 0.08 * t) -
99
- width * (0.28 * s * s * reach - 0.1 * s * t);
100
- return vertex(
101
- m,
102
- add(centre, turn([x, y, z])),
103
- turn([0.56 * s - 0.1 * t, 1, 0.65 * t - 0.08 - 0.1 * s]),
104
- shade * (1.02 - Math.abs(s) * 0.025),
105
- );
106
- });
107
- });
108
- const outward = turn([0, 1, 0]);
109
- for (let row = 0; row + 1 < rows.length; row++) {
110
- const a = rows[row],
111
- b = rows[row + 1];
112
- for (let flank = 0; flank < 2; flank++) {
113
- if (a.length === 1) face(m, a[0], b[flank], b[flank + 1], outward);
114
- else if (b.length === 1) face(m, a[flank], b[0], a[flank + 1], outward);
115
- else {
116
- face(m, a[flank], b[flank], b[flank + 1], outward);
117
- face(m, a[flank], b[flank + 1], a[flank + 1], outward);
118
- }
119
- }
120
- }
121
- }
122
-
123
- // Rounded ovoid crowns have smooth normals and an irregular low-poly silhouette.
124
- function foliage(m, centre, radii, seed, far, {thin = false, tilt = 0, shade = 1} = {}) {
125
- if (thin) return leafBlade(m, centre, radii, seed, far, tilt, shade);
126
- const sides = far ? 8 : 10;
127
- const rings = far ? 2 : 6;
128
- const yaw = seed * golden;
129
- const turn = ([x, y, z]) => {
130
- const yy = y * Math.cos(tilt) - z * Math.sin(tilt);
131
- const zz = y * Math.sin(tilt) + z * Math.cos(tilt);
132
- return [x * Math.cos(yaw) + zz * Math.sin(yaw), yy, -x * Math.sin(yaw) + zz * Math.cos(yaw)];
133
- };
134
- const point = (n) => add(centre, turn(n.map((v, axis) => v * radii[axis])));
135
- const top = vertex(m, point([0.08, 1, 0]), turn([0, 1, 0]), shade * 1.04);
136
- const bottom = vertex(m, point([-0.06, -1, 0.04]), turn([0, -1, 0]), shade * 0.94);
137
- const rows = [];
138
- for (let row = 1; row < rings; row++) {
139
- const latitude = (row * Math.PI) / rings;
140
- const y = Math.cos(latitude);
141
- const reach = Math.pow(Math.sin(latitude), 0.78);
142
- rows.push(
143
- Array.from({length: sides}, (_, side) => {
144
- const angle = (side * tau) / sides + seed * 0.32;
145
- const ripple = 1 + 0.07 * Math.sin(angle * 3 + seed) + 0.035 * Math.cos(angle * 5 - seed);
146
- const width = reach * ripple * (1 - y * 0.08);
147
- const x = Math.cos(angle) * width + y * 0.08;
148
- const z = Math.sin(angle) * width;
149
- const n = [
150
- (Math.cos(angle) * reach) / radii[0],
151
- y / radii[1],
152
- (Math.sin(angle) * reach) / radii[2],
153
- ];
154
- return vertex(m, point([x, y, z]), turn(n), shade * (0.98 + y * 0.04));
155
- }),
156
- );
157
- }
158
- for (let side = 0; side < sides; side++) {
159
- const next = (side + 1) % sides;
160
- face(m, top, rows[0][side], rows[0][next], turn([0, 1, 0]));
161
- for (let row = 0; row + 1 < rows.length; row++) {
162
- const outward = turn([
163
- Math.cos((side * tau) / sides + seed * 0.32),
164
- 0,
165
- Math.sin((side * tau) / sides + seed * 0.32),
166
- ]);
167
- face(m, rows[row][side], rows[row + 1][side], rows[row + 1][next], outward);
168
- face(m, rows[row][side], rows[row + 1][next], rows[row][next], outward);
169
- }
170
- face(m, bottom, rows.at(-1)[next], rows.at(-1)[side], turn([0, -1, 0]));
171
- }
172
- }
173
-
174
36
  function branch(m, from, to, base, tip, sides) {
175
37
  const axis = unit(sub(to, from));
176
38
  const u = unit(cross(axis, Math.abs(axis[1]) > 0.9 ? [1, 0, 0] : [0, 1, 0]));
@@ -196,135 +58,6 @@ function branch(m, from, to, base, tip, sides) {
196
58
 
197
59
  // Compact, upright and open trees have different branching architectures, not the
198
60
  // same set of spheres stretched along an axis. Lobes carry a stable material shade.
199
- const broadleaf = {
200
- round: [
201
- [-1.5, 6.9, 0.45, 2.25, 2.5, 2.0, -0.22, 1.05],
202
- [1.8, 6.15, 0.45, 2.05, 2.15, 1.8, 0.25, 0.82],
203
- [0.1, 7.7, -1.0, 2.0, 2.55, 1.85, 0.12, 1.13],
204
- ],
205
- tall: [
206
- [-0.55, 9.15, -0.25, 2.15, 3.3, 2.0, -0.1, 1.14],
207
- [1.8, 6.05, 0.5, 1.6, 1.75, 1.45, 0.32, 0.78],
208
- [-1.7, 5.45, 0.8, 1.45, 1.65, 1.4, -0.32, 0.91],
209
- ],
210
- open: [
211
- [-0.7, 8.1, -0.4, 1.3, 0.23, 1.05, 0.12, 1.13],
212
- [0.95, 7.9, -0.65, 1.45, 0.22, 1.15, 0.25, 1.08],
213
- [-2.0, 7.0, 0.65, 1.5, 0.22, 1.1, 0.55, 0.95],
214
- [0.1, 7.7, 1.15, 1.5, 0.24, 1.2, 0.38, 1.18],
215
- [2.0, 7.1, 0.3, 1.45, 0.2, 1.05, 0.6, 1.04],
216
- [-0.35, 7.3, -1.65, 1.35, 0.23, 1.15, 0.42, 1.0],
217
- [-1.85, 6.0, -0.65, 1.25, 0.24, 1.0, 0.85, 0.79],
218
- [1.65, 6.1, 1.0, 1.35, 0.22, 1.1, 0.9, 0.86],
219
- [-0.4, 6.6, 1.8, 1.2, 0.2, 1.0, 0.75, 0.93],
220
- [0.6, 6.2, -1.5, 1.3, 0.24, 1.0, 0.8, 0.75],
221
- ],
222
- conifer: [
223
- [-0.25, 10.1, 0.05, 1.05, 0.4, 0.95, 0.2, 1.14],
224
- [1.1, 8.8, -0.4, 1.5, 0.35, 1.15, -0.35, 1.06],
225
- [-1.35, 8.4, 0.25, 1.55, 0.35, 1.2, 0.45, 1.1],
226
- [0.1, 7.75, 1.4, 1.5, 0.3, 1.25, -0.3, 1.0],
227
- [-0.55, 7.35, -1.4, 1.55, 0.35, 1.25, 0.35, 0.98],
228
- [1.75, 6.4, 0.25, 1.65, 0.35, 1.25, -0.5, 0.88],
229
- [-1.85, 5.9, -0.25, 1.65, 0.35, 1.3, 0.45, 0.91],
230
- [0.35, 5.2, 1.65, 1.75, 0.3, 1.3, -0.6, 0.82],
231
- [-0.25, 4.8, -1.4, 1.65, 0.35, 1.2, 0.5, 0.8],
232
- [0, 6.7, 0.05, 1.25, 0.35, 1.1, -0.2, 0.95],
233
- ],
234
- };
235
-
236
- function deciduous(form, far) {
237
- const bark = mesh('bark');
238
- const thin = form === 'open' || form === 'conifer';
239
- const leaves = mesh(thin ? 'fronds' : 'leaves');
240
- const upright = form === 'tall' || form === 'conifer';
241
- const foot = [0, 0, 0];
242
- const waist = [0.08, 0.9, -0.02];
243
- const fork = [upright ? -0.18 : 0.16, upright ? 3.5 : 3.2, -0.08];
244
- const trunkSides = far ? 3 : 6;
245
- branch(bark, foot, waist, upright ? 0.47 : 0.55, 0.33, trunkSides);
246
- branch(bark, waist, fork, 0.33, 0.26, trunkSides);
247
- const limbs = thin
248
- ? [
249
- [-1.0, 7.3, 0.15],
250
- [1.1, 6.65, -0.25],
251
- [0.25, 5.0, 0.7],
252
- ]
253
- : broadleaf[form].map((lobe) => lobe.slice(0, 3));
254
- for (const [i, end] of limbs.entries()) {
255
- const elbow = add(fork, mul(sub(end, fork), 0.52));
256
- elbow[0] += i === 1 ? 0.18 : -0.15;
257
- if (far) branch(bark, fork, end, 0.21, 0.055, 3);
258
- else {
259
- branch(bark, fork, elbow, 0.24 - i * 0.035, 0.14, 4);
260
- branch(bark, elbow, end, 0.14, 0.045, 4);
261
- }
262
- }
263
- for (const [i, lobe] of broadleaf[form].entries()) {
264
- const centre = lobe.slice(0, 3);
265
- if (thin && !far) {
266
- const limb = limbs[i % limbs.length];
267
- const start = add(
268
- fork,
269
- mul(sub(limb, fork), Math.min(1, (centre[1] - fork[1]) / (limb[1] - fork[1]))),
270
- );
271
- branch(bark, start, centre, 0.1, 0.025, 3);
272
- }
273
- foliage(leaves, centre, lobe.slice(3, 6), i + form.length, far, {
274
- thin,
275
- tilt: lobe[6],
276
- shade: lobe[7],
277
- });
278
- }
279
- return [bark, leaves];
280
- }
281
-
282
- function evergreen(form, far) {
283
- const bark = mesh('bark');
284
- const leaves = mesh('leaves');
285
- const column = form === 'columnar';
286
- branch(bark, [0, 0, 0], [0, 8, 0], column ? 0.17 : 0.23, 0.06, far ? 4 : 6);
287
- const tiers = far ? 3 : column ? 5 : 6;
288
- for (let i = 0; i < tiers; i++) {
289
- const t = i / (tiers - 1);
290
- const radius = column ? 1.35 - t * 0.65 : 3.2 - t * 2.4;
291
- const y = (column ? 3.5 : 3) + t * (column ? 7.4 : 7);
292
- if (column)
293
- crown(
294
- leaves,
295
- [0.12 * Math.sin(i * 2), y, 0.1 * Math.cos(i)],
296
- [radius, 2.1 - t * 0.7, radius],
297
- i + 2,
298
- far,
299
- );
300
- else {
301
- const sides = far ? 6 : 9;
302
- const top = vertex(leaves, [0, y + 2.6, 0], [0, 1, 0], 1.06);
303
- const bottom = vertex(leaves, [0, y - 0.25, 0], [0, -1, 0], 0.86);
304
- const ring = Array.from({length: sides}, (_, s) => {
305
- const a = (s * tau) / sides + i * golden;
306
- const r = radius * (1 + 0.07 * Math.sin(s * 3 + i));
307
- return vertex(
308
- leaves,
309
- [Math.cos(a) * r, y + 0.12 * Math.sin(s * 2), Math.sin(a) * r],
310
- [Math.cos(a), 0.45, Math.sin(a)],
311
- 0.94 + 0.03 * Math.sin(s),
312
- );
313
- });
314
- for (let s = 0; s < sides; s++) {
315
- const n = (s + 1) % sides;
316
- face(leaves, top, ring[s], ring[n], [
317
- Math.cos((s * tau) / sides + i * golden),
318
- 1,
319
- Math.sin((s * tau) / sides + i * golden),
320
- ]);
321
- face(leaves, bottom, ring[n], ring[s], [0, -1, 0]);
322
- }
323
- }
324
- }
325
- return [bark, leaves];
326
- }
327
-
328
61
  function palm(form, far) {
329
62
  const fan = form === 'palm-fan';
330
63
  const stem = mesh('stem');
@@ -506,16 +239,177 @@ function topView(parts, dark) {
506
239
  return `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">\n<!-- Generated by build-trees.mjs. Original Tileflow crown artwork. -->\n${triangles.map((t) => t.path).join('\n')}\n</svg>\n`;
507
240
  }
508
241
 
242
+ const profiles = {
243
+ round: {
244
+ r: [3.5, 3.55, 3.2],
245
+ y: 6.55,
246
+ power: 0.83,
247
+ lobes: 0.1,
248
+ taper: 0.08,
249
+ lean: 0.16,
250
+ phase: 0.4,
251
+ },
252
+ open: {
253
+ r: [4.2, 2.65, 3.65],
254
+ y: 6.35,
255
+ power: 0.78,
256
+ lobes: 0.13,
257
+ taper: 0.09,
258
+ lean: -0.18,
259
+ phase: 1.2,
260
+ },
261
+ tall: {
262
+ r: [2.6, 4.4, 2.35],
263
+ y: 7.2,
264
+ power: 0.94,
265
+ lobes: 0.075,
266
+ taper: 0.16,
267
+ lean: 0.28,
268
+ phase: 2.3,
269
+ },
270
+ conifer: {
271
+ r: [3.0, 4.75, 2.8],
272
+ y: 6.35,
273
+ power: 0.74,
274
+ lobes: 0.035,
275
+ taper: 0.6,
276
+ lean: -0.1,
277
+ phase: 0.8,
278
+ },
279
+ columnar: {
280
+ r: [1.35, 5.5, 1.2],
281
+ y: 6.2,
282
+ power: 1.04,
283
+ lobes: 0.035,
284
+ taper: 0.22,
285
+ lean: 0.13,
286
+ phase: 1.6,
287
+ },
288
+ };
289
+ const crownVolumes = {
290
+ round: [
291
+ {c: [-1.3, -0.25, 0.15], r: [2.3, 2.5, 2.2]},
292
+ {c: [1.4, -0.4, 0], r: [2.2, 2.35, 2.1]},
293
+ {c: [0.1, 1.5, -0.3], r: [2.4, 2.7, 2.3]},
294
+ ],
295
+ open: [
296
+ {c: [-1.75, -0.25, 0.2], r: [2.65, 2.1, 2.45]},
297
+ {c: [1.65, -0.2, 0.1], r: [2.6, 2.3, 2.4]},
298
+ {c: [-0.1, 0.7, -0.45], r: [2.9, 2.5, 2.6]},
299
+ ],
300
+ tall: [
301
+ {c: [-0.15, 1.25, -0.15], r: [2.25, 3.35, 2.05]},
302
+ {c: [-1.0, -1.4, 0.2], r: [1.8, 2.1, 1.7]},
303
+ {c: [1.1, -0.85, 0.1], r: [1.8, 2.35, 1.75]},
304
+ ],
305
+ };
306
+ // Restore depth to the compact crowns; their original lobes were laid out
307
+ // mainly side to side, leaving a flat, elongated footprint from above.
308
+ const crownDepth = {round: 1.45, open: 1.52, tall: 1.3};
309
+ function blendedRadius(direction, volumes) {
310
+ const hits = volumes.map(({c, r}) => {
311
+ let a = 0,
312
+ b = 0,
313
+ d = -1;
314
+ for (let i = 0; i < 3; i++) {
315
+ a += (direction[i] / r[i]) ** 2;
316
+ b -= (2 * direction[i] * c[i]) / r[i] ** 2;
317
+ d += (c[i] / r[i]) ** 2;
318
+ }
319
+ return (-b + Math.sqrt(Math.max(0, b * b - 4 * a * d))) / (2 * a);
320
+ });
321
+ // A smooth union keeps the lobes readable in one continuous canopy skin.
322
+ return hits.reduce((a, b) => Math.max(a, b) + 0.2 * Math.log1p(Math.exp(-Math.abs(a - b) / 0.2)));
323
+ }
324
+
325
+ function compactCrown(m, p, form, far) {
326
+ const sides = far ? 12 : 20,
327
+ rings = far ? 5 : 10;
328
+ const point = (latitude, angle) => {
329
+ const up = Math.cos(latitude),
330
+ reach = Math.max(0, Math.sin(latitude));
331
+ if (crownVolumes[form]) {
332
+ const direction = [reach * Math.cos(angle), up, reach * Math.sin(angle)];
333
+ const radius = blendedRadius(direction, crownVolumes[form]);
334
+ return [
335
+ direction[0] * radius,
336
+ p.y + direction[1] * radius,
337
+ direction[2] * radius * crownDepth[form],
338
+ ];
339
+ }
340
+ const lobes =
341
+ 1 +
342
+ p.lobes * Math.cos(3 * angle + p.phase) * reach * reach +
343
+ 0.025 * Math.sin(2 * angle + p.phase) * reach;
344
+ const tier = form === 'conifer' ? 0.88 * (1 + 0.055 * Math.cos(latitude * 6)) : 1;
345
+ const radius = Math.pow(reach, p.power) * (1 - p.taper * up) * lobes * tier;
346
+ return [
347
+ p.r[0] * radius * Math.cos(angle) + p.lean * up,
348
+ p.y + p.r[1] * up,
349
+ p.r[2] * radius * Math.sin(angle) + 0.08 * up,
350
+ ];
351
+ };
352
+ const normal = (latitude, angle) => {
353
+ const e = 0.0001;
354
+ const around = sub(point(latitude, angle + e), point(latitude, angle - e));
355
+ const down = sub(point(latitude + e, angle), point(latitude - e, angle));
356
+ return unit(cross(around, down));
357
+ };
358
+ const top = vertex(m, point(0, 0), [0, 1, 0], 1.015);
359
+ const bottom = vertex(m, point(Math.PI, 0), [0, -1, 0], 0.985);
360
+ const rows = [];
361
+ for (let row = 1; row < rings; row++) {
362
+ const latitude = (row / rings) * Math.PI;
363
+ rows.push(
364
+ Array.from({length: sides}, (_, side) => {
365
+ const angle = (side / sides) * tau;
366
+ return vertex(
367
+ m,
368
+ point(latitude, angle),
369
+ normal(latitude, angle),
370
+ 1 + 0.015 * Math.cos(latitude),
371
+ );
372
+ }),
373
+ );
374
+ }
375
+ for (let side = 0; side < sides; side++) {
376
+ const next = (side + 1) % sides;
377
+ face(m, top, rows[0][side], rows[0][next], [0, 1, 0]);
378
+ for (let row = 0; row < rows.length - 1; row++) {
379
+ const outward = normal(((row + 1.5) / rings) * Math.PI, ((side + 0.5) / sides) * tau);
380
+ face(m, rows[row][side], rows[row + 1][side], rows[row + 1][next], outward);
381
+ face(m, rows[row][side], rows[row + 1][next], rows[row][next], outward);
382
+ }
383
+ face(m, bottom, rows.at(-1)[next], rows.at(-1)[side], [0, -1, 0]);
384
+ }
385
+ }
386
+
387
+ function compactTree(form, far) {
388
+ const p = profiles[form],
389
+ bark = mesh('bark'),
390
+ leaves = mesh('leaves');
391
+ const slim = form === 'columnar' || form === 'conifer';
392
+ const crownBottom = p.y - p.r[1],
393
+ fork = Math.max(1.5, crownBottom + (slim ? 0.35 : -0.65));
394
+ const sides = far ? 3 : 8;
395
+ branch(bark, [0, 0, 0], [0, fork, 0], slim ? 0.28 : 0.48, slim ? 0.16 : 0.3, sides);
396
+ if (slim) branch(bark, [0, fork, 0], [0, p.y, 0], 0.16, 0.06, sides);
397
+ else
398
+ for (const [sign, z] of [
399
+ [-1, -0.18],
400
+ [1, 0.15],
401
+ ]) {
402
+ branch(bark, [0, fork, 0], [sign * p.r[0] * 0.46, crownBottom + 1.25, z], 0.24, 0.1, sides);
403
+ }
404
+ compactCrown(leaves, p, form, far);
405
+ return [bark, leaves];
406
+ }
407
+
509
408
  export function buildStreetsTrees() {
510
409
  const files = new Map();
511
410
  const catalogue = [];
512
411
  for (const form of ['round', 'open', 'tall', 'conifer', 'columnar', 'palm-feather', 'palm-fan']) {
513
- const build = (far) =>
514
- form.startsWith('palm')
515
- ? palm(form, far)
516
- : form in broadleaf
517
- ? deciduous(form, far)
518
- : evergreen(form, far);
412
+ const build = (far) => (form.startsWith('palm') ? palm(form, far) : compactTree(form, far));
519
413
  const near = build(false),
520
414
  far = build(true);
521
415
  const id = `streets-tree-${form}`;
@@ -0,0 +1,9 @@
1
+ Historical Köppen–Geiger climate classification, 1991–2020.
2
+ Beck et al. (2023), https://doi.org/10.1038/s41597-023-02549-6
3
+ Source: https://www.gloh2o.org/koppen/
4
+ Archive: https://ndownloader.figshare.com/files/61012822
5
+ Archive MD5: 7fc2f5a15d4f5fe0ce59c9a9b502aa09
6
+ License: Creative Commons Attribution 4.0 International.
7
+ https://creativecommons.org/licenses/by/4.0/
8
+ Adaptation: historical classes mapped to a crop-colour proxy, spatially smoothed,
9
+ reprojected to Web Mercator and tiled. No real-time vegetation is inferred.
@@ -0,0 +1,47 @@
1
+ """Rebuild the embedded, historical climate colour image (requires Pillow and NumPy)."""
2
+ from pathlib import Path
3
+ from PIL import Image, ImageFilter
4
+ import hashlib, json, base64, io
5
+ import numpy as np
6
+ ROOT = Path(__file__).resolve().parent
7
+ SOURCE = ROOT / 'koppen-geiger-1991-2020.tif'
8
+ DRY = np.array([239, 241, 230], dtype=float) # Existing global crop colour.
9
+ WET = np.array([202, 235, 215], dtype=float)
10
+ # 0=no-data; 1..30 are the published Beck et al. climate classes.
11
+ # Summer-dry and arid climates retain the current neutral crop palette.
12
+ WEIGHTS = [0, 1, 1, .45, 0, 0, 0, 0, 0, .06, .15,
13
+ .65, .7, .5, 1, 1, .9, .1, .15, .2, .15,
14
+ .65, .85, .65, .35, 1, 1, .8, .5, .15, 0]
15
+ recipe = dict(sourceSha256=hashlib.sha256(SOURCE.read_bytes()).hexdigest(),
16
+ dry=DRY.tolist(), wet=WET.tolist(), weights=WEIGHTS,
17
+ blurDegrees=.25, maxzoom=4, tileSize=256, period='1991–2020')
18
+ digest = hashlib.sha256(json.dumps(recipe, sort_keys=True).encode()).hexdigest()[:24]
19
+ classes = np.asarray(Image.open(SOURCE))
20
+ assert classes.shape == (1800, 3600) and classes.max() <= 30
21
+ valid = classes > 0
22
+ signal = np.take(WEIGHTS, classes)
23
+ # Normalize the blur by valid land so coastlines do not artificially dry out.
24
+ blur = lambda a: np.asarray(Image.fromarray(np.uint8(np.round(a * 255))).filter(
25
+ ImageFilter.GaussianBlur(2.5)), dtype=float) / 255
26
+ smoothed = np.divide(blur(signal), blur(valid), out=np.zeros_like(signal),
27
+ where=blur(valid) > .01).clip(0, 1)
28
+ colour = np.uint8(np.round(DRY + smoothed[..., None] * (WET - DRY)))
29
+ # Project the global 0.1 degree grid to Web Mercator before making XYZ tiles.
30
+ size = 4096
31
+ latitude = np.degrees(np.arctan(np.sinh(np.pi * (1 - 2 * (np.arange(size) + .5) / size))))
32
+ row = (90 - latitude) * 10 - .5
33
+ low = np.floor(row).astype(int).clip(0, 1798)
34
+ fraction = (row - low).clip(0, 1)
35
+ projected = np.uint8(np.round(colour[low] * (1 - fraction[:, None, None]) +
36
+ colour[low + 1] * fraction[:, None, None]))
37
+ world = Image.fromarray(projected).resize((size, size), Image.Resampling.BILINEAR)
38
+ buffer = io.BytesIO()
39
+ world.save(buffer, format='PNG', optimize=True)
40
+ uri = 'data:image/png;base64,' + base64.b64encode(buffer.getvalue()).decode()
41
+ output = ROOT / '../../../src/official/streets-climate-image.ts'
42
+ output.write_text('// Generated by assets/streets/climate/build.py. See its NOTICE.txt.\nexport const streetsClimateImage = ' + json.dumps(uri) + ';\n')
43
+ (ROOT / 'recipe.json').write_text(json.dumps(recipe, indent=2)+'\n')
44
+ # Keep generated sources reproducible under the package formatter.
45
+ import subprocess
46
+ subprocess.run(['pnpm', 'exec', 'prettier', '--write', str(output.resolve()), str(ROOT / 'recipe.json')], cwd=ROOT.parents[4], check=True)
47
+ print(json.dumps({'imageBytes':len(buffer.getvalue()), 'sourceSha256':recipe['sourceSha256']}))
@@ -0,0 +1,13 @@
1
+ {
2
+ "sourceSha256": "59b749fe6711e72c720ff903044eb0efb4373524ee434740ed7384a0284c155b",
3
+ "dry": [239.0, 241.0, 230.0],
4
+ "wet": [202.0, 235.0, 215.0],
5
+ "weights": [
6
+ 0, 1, 1, 0.45, 0, 0, 0, 0, 0, 0.06, 0.15, 0.65, 0.7, 0.5, 1, 1, 0.9, 0.1, 0.15, 0.2, 0.15, 0.65,
7
+ 0.85, 0.65, 0.35, 1, 1, 0.8, 0.5, 0.15, 0
8
+ ],
9
+ "blurDegrees": 0.25,
10
+ "maxzoom": 4,
11
+ "tileSize": 256,
12
+ "period": "1991\u20132020"
13
+ }