@tileflow/maps 0.1.0-alpha.15 → 0.1.0-alpha.17
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.
- package/README.md +68 -67
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/assets/streets/LICENSE-CC-BY-SA-3.0.txt +359 -0
- package/assets/streets/LICENSE-CC-BY-SA-4.0.txt +428 -0
- package/assets/streets/build-trees.mjs +167 -273
- package/assets/streets/climate/NOTICE.txt +9 -0
- package/assets/streets/climate/build.py +47 -0
- package/assets/streets/climate/koppen-geiger-1991-2020.tif +0 -0
- package/assets/streets/climate/recipe.json +13 -0
- package/assets/streets/icons/oneway.svg +1 -8
- package/assets/streets/icons/streets-tree-columnar-dark.pattern.svg +200 -105
- package/assets/streets/icons/streets-tree-columnar.pattern.svg +200 -105
- package/assets/streets/icons/streets-tree-conifer-dark.pattern.svg +260 -200
- package/assets/streets/icons/streets-tree-conifer.pattern.svg +260 -200
- package/assets/streets/icons/streets-tree-open-dark.pattern.svg +170 -200
- package/assets/streets/icons/streets-tree-open.pattern.svg +170 -200
- package/assets/streets/icons/streets-tree-round-dark.pattern.svg +160 -158
- package/assets/streets/icons/streets-tree-round.pattern.svg +160 -158
- package/assets/streets/icons/streets-tree-tall-dark.pattern.svg +166 -156
- package/assets/streets/icons/streets-tree-tall.pattern.svg +166 -156
- package/assets/streets/icons/transit-madrid-cercanias.svg +8 -0
- package/assets/streets/icons/transit-madrid-emt.svg +16 -0
- package/assets/streets/icons/transit-madrid-metro.svg +11 -0
- package/assets/streets/icons/unified-day-aerialway.svg +11 -0
- package/assets/streets/icons/unified-day-airport.svg +11 -0
- package/assets/streets/icons/unified-day-bank.svg +11 -0
- package/assets/streets/icons/unified-day-bar.svg +11 -0
- package/assets/streets/icons/unified-day-beach.svg +11 -0
- package/assets/streets/icons/unified-day-beauty.svg +11 -0
- package/assets/streets/icons/unified-day-bookstore.svg +11 -0
- package/assets/streets/icons/unified-day-buddhist.svg +11 -0
- package/assets/streets/icons/unified-day-bus.svg +11 -0
- package/assets/streets/icons/unified-day-campsite.svg +11 -0
- package/assets/streets/icons/unified-day-car-repair.svg +11 -0
- package/assets/streets/icons/unified-day-car.svg +11 -0
- package/assets/streets/icons/unified-day-charging-station.svg +11 -0
- package/assets/streets/icons/unified-day-christian.svg +11 -0
- package/assets/streets/icons/unified-day-clothing.svg +11 -0
- package/assets/streets/icons/unified-day-coffee.svg +11 -0
- package/assets/streets/icons/unified-day-college.svg +11 -0
- package/assets/streets/icons/unified-day-community.svg +11 -0
- package/assets/streets/icons/unified-day-convenience.svg +11 -0
- package/assets/streets/icons/unified-day-dentist.svg +11 -0
- package/assets/streets/icons/unified-day-doctor.svg +11 -0
- package/assets/streets/icons/unified-day-dog-park.svg +11 -0
- package/assets/streets/icons/unified-day-drinking-water.svg +11 -0
- package/assets/streets/icons/unified-day-education.svg +11 -0
- package/assets/streets/icons/unified-day-electronics.svg +11 -0
- package/assets/streets/icons/unified-day-embassy.svg +11 -0
- package/assets/streets/icons/unified-day-fast-food.svg +11 -0
- package/assets/streets/icons/unified-day-ferry.svg +11 -0
- package/assets/streets/icons/unified-day-fire-station.svg +11 -0
- package/assets/streets/icons/unified-day-fitness.svg +11 -0
- package/assets/streets/icons/unified-day-florist.svg +11 -0
- package/assets/streets/icons/unified-day-food.svg +11 -0
- package/assets/streets/icons/unified-day-fuel.svg +11 -0
- package/assets/streets/icons/unified-day-garden.svg +11 -0
- package/assets/streets/icons/unified-day-golf.svg +11 -0
- package/assets/streets/icons/unified-day-grocery.svg +11 -0
- package/assets/streets/icons/unified-day-harbor.svg +11 -0
- package/assets/streets/icons/unified-day-hardware.svg +11 -0
- package/assets/streets/icons/unified-day-health.svg +11 -0
- package/assets/streets/icons/unified-day-jewish.svg +11 -0
- package/assets/streets/icons/unified-day-laundry.svg +11 -0
- package/assets/streets/icons/unified-day-library.svg +11 -0
- package/assets/streets/icons/unified-day-lodging.svg +11 -0
- package/assets/streets/icons/unified-day-major-transit.svg +11 -0
- package/assets/streets/icons/unified-day-mountain.svg +11 -0
- package/assets/streets/icons/unified-day-muslim.svg +11 -0
- package/assets/streets/icons/unified-day-natural.svg +11 -0
- package/assets/streets/icons/unified-day-nature.svg +11 -0
- package/assets/streets/icons/unified-day-parking-garage.svg +11 -0
- package/assets/streets/icons/unified-day-parking.svg +11 -0
- package/assets/streets/icons/unified-day-pharmacy.svg +11 -0
- package/assets/streets/icons/unified-day-playground.svg +11 -0
- package/assets/streets/icons/unified-day-plaza.svg +11 -0
- package/assets/streets/icons/unified-day-police.svg +11 -0
- package/assets/streets/icons/unified-day-post.svg +11 -0
- package/assets/streets/icons/unified-day-prison.svg +11 -0
- package/assets/streets/icons/unified-day-rail-light.svg +11 -0
- package/assets/streets/icons/unified-day-rail.svg +11 -0
- package/assets/streets/icons/unified-day-services.svg +11 -0
- package/assets/streets/icons/unified-day-shopping.svg +11 -0
- package/assets/streets/icons/unified-day-sport.svg +11 -0
- package/assets/streets/icons/unified-day-stadium.svg +11 -0
- package/assets/streets/icons/unified-day-swimming.svg +11 -0
- package/assets/streets/icons/unified-day-toilet.svg +11 -0
- package/assets/streets/icons/unified-day-town-hall.svg +11 -0
- package/assets/streets/icons/unified-day-waterfall.svg +11 -0
- package/assets/streets/icons/unified-day-worship.svg +11 -0
- package/assets/streets/models/streets-tree-columnar.far.glb +0 -0
- package/assets/streets/models/streets-tree-columnar.glb +0 -0
- package/assets/streets/models/streets-tree-conifer.far.glb +0 -0
- package/assets/streets/models/streets-tree-conifer.glb +0 -0
- package/assets/streets/models/streets-tree-open.far.glb +0 -0
- package/assets/streets/models/streets-tree-open.glb +0 -0
- package/assets/streets/models/streets-tree-round.far.glb +0 -0
- package/assets/streets/models/streets-tree-round.glb +0 -0
- package/assets/streets/models/streets-tree-tall.far.glb +0 -0
- package/assets/streets/models/streets-tree-tall.glb +0 -0
- package/dist/index.d.ts +829 -132
- package/dist/index.js +1459 -504
- package/package.json +3 -4
- package/docs/design-and-assets.md +0 -224
- 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']}))
|
|
Binary file
|
|
@@ -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
|
+
}
|
|
@@ -1,10 +1,3 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 18">
|
|
2
|
-
<path
|
|
3
|
-
d="M3 9h17m-6-6 6 6-6 6"
|
|
4
|
-
fill="none"
|
|
5
|
-
stroke="#FFFFFF"
|
|
6
|
-
stroke-linecap="round"
|
|
7
|
-
stroke-linejoin="round"
|
|
8
|
-
stroke-width="3.2"
|
|
9
|
-
/>
|
|
2
|
+
<path d="M3 7.8h10V3l8 6-8 6v-4.8H3Z" fill="#7C8D99" />
|
|
10
3
|
</svg>
|