Language tree: room to zoom out further

The tapestry is laid out at half its former size in the map's world square,
so from the opening view the reader can zoom out two levels instead of
one. Every zoom-dependent stop (dot sizes, label entry zooms, framing
limits) moves up one level with it, so the opening view looks the same.
This commit is contained in:
Doxa Maps 2026-09-27 05:27:34 +00:00
parent 414ed9664f
commit 705a815190
15 changed files with 38 additions and 31 deletions

File diff suppressed because one or more lines are too long

View file

@ -140,7 +140,10 @@ for (const root of [...roots].sort((a, b) => b._R - a._R || a.name.localeCompare
// ── to lng/lat through inverse Web Mercator, uniformly scaled, so the drawing is not distorted ─────
let ext = 1
for (const p of placed) ext = Math.max(ext, Math.abs(p.x) + p.R, Math.abs(p.y) + p.R)
const K = 0.34 / ext // the tapestry spans ~68% of the Mercator square
// The tapestry spans ~34% of the Mercator square. The engines stop at zoom 0 (the whole square), so the
// smaller the drawing, the further a reader can zoom OUT from its opening view: at 68% there was about
// one level of room (Driver, 2026-09-27: "I need to be able to zoom out a bit more"); now there are two.
const K = 0.17 / ext
const toLngLat = (u, v) => {
const X = 0.5 + u * K, Y = 0.5 - v * K
const lng = (X - 0.5) * 360

View file

@ -96,9 +96,9 @@ nodes.forEach((n, i) => {
})
const ext = roots.reduce((m, r) => Math.max(m, circ.get(r.id).R), 0)
if (minD === Infinity) console.log('NOT MEASURED: no two nodes within 1 px at z0; the spacing measure found nothing to compare')
console.log(`info closest two nodes: ${minD.toFixed(4)} px at z0 = ${(minD * 64).toFixed(1)} px at z6 (${pair ? nodes[pair[0]].n + ' / ' + nodes[pair[1]].n : ''}); largest family radius ${ext.toFixed(1)} px at z0`)
// Dialect dots first show at z6; two dots closer than 4 px there print as one.
ok(minD * 2 ** 6 >= 4, 'no two nodes print on one another at zoom 6 (>= 4 px apart)')
console.log(`info closest two nodes: ${minD.toFixed(4)} px at z0 = ${(minD * 128).toFixed(1)} px at z7 (${pair ? nodes[pair[0]].n + ' / ' + nodes[pair[1]].n : ''}); largest family radius ${ext.toFixed(1)} px at z0`)
// Dialect labels first show at z5.6 (dots at every zoom); two dots closer than 4 px by z7 print as one.
ok(minD * 2 ** 7 >= 4, 'no two nodes print on one another at zoom 7 (>= 4 px apart)')
if (PNG) {
// a tiny RGBA rasteriser: links, then dots, then family rings. No dependency.

View file

@ -79,14 +79,18 @@ function linkFC() {
// level: a top-level exponential interpolate with a data-driven stop value is valid on both engines.
const dishRadius = ['interpolate', ['exponential', 2], ['zoom'], 0, ['*', ['get', 'R'], 512], 22, ['*', ['get', 'R'], 512 * 2 ** 22]]
const colorExpr = ['match', ['get', 'col'], 'g', TREE_COLOR.g, 'y', TREE_COLOR.y, 'r', TREE_COLOR.r, '#9aa2ad']
// Zoom stops are in the tree's own scale (build-language-tree.mjs K): the drawing is half the size it
// was, so every threshold sits one level higher than before and the opening view looks the same.
const nodeRadius = ['interpolate', ['linear'], ['zoom'],
0, ['match', ['get', 'lv'], 'f', 4, 'l', 1.6, 'b', 0.9, 0.7],
4, ['match', ['get', 'lv'], 'f', 9, 'l', 4.5, 'b', 2.4, 1.8],
8, ['match', ['get', 'lv'], 'f', 14, 'l', 8, 'b', 4, 3.5]]
0, ['match', ['get', 'lv'], 'f', 3, 'l', 1, 'b', 0.6, 0.5],
1, ['match', ['get', 'lv'], 'f', 4, 'l', 1.6, 'b', 0.9, 0.7],
5, ['match', ['get', 'lv'], 'f', 9, 'l', 4.5, 'b', 2.4, 1.8],
9, ['match', ['get', 'lv'], 'f', 14, 'l', 8, 'b', 4, 3.5]]
const nodeRadiusPlus = (k) => ['interpolate', ['linear'], ['zoom'],
0, ['+', k, ['match', ['get', 'lv'], 'f', 4, 'l', 1.6, 'b', 0.9, 0.7]],
4, ['+', k, ['match', ['get', 'lv'], 'f', 9, 'l', 4.5, 'b', 2.4, 1.8]],
8, ['+', k, ['match', ['get', 'lv'], 'f', 14, 'l', 8, 'b', 4, 3.5]]]
0, ['+', k, ['match', ['get', 'lv'], 'f', 3, 'l', 1, 'b', 0.6, 0.5]],
1, ['+', k, ['match', ['get', 'lv'], 'f', 4, 'l', 1.6, 'b', 0.9, 0.7]],
5, ['+', k, ['match', ['get', 'lv'], 'f', 9, 'l', 4.5, 'b', 2.4, 1.8]],
9, ['+', k, ['match', ['get', 'lv'], 'f', 14, 'l', 8, 'b', 4, 3.5]]]
function addLayers(map) {
map.addSource('gt-nodes', { type: 'geojson', data: nodeFC(), promoteId: 'id' })
@ -97,7 +101,7 @@ function addLayers(map) {
'circle-stroke-color': '#d1d4cd', 'circle-stroke-width': 1, 'circle-pitch-alignment': 'map',
} })
map.addLayer({ id: 'gt-links', type: 'line', source: 'gt-links', paint: {
'line-color': '#8d968e', 'line-width': ['interpolate', ['linear'], ['zoom'], 0, 0.3, 6, 1],
'line-color': '#8d968e', 'line-width': ['interpolate', ['linear'], ['zoom'], 1, 0.3, 7, 1],
'line-opacity': ['case', ['==', ['get', 'm'], 1], ['case', ['==', ['get', 'd'], 1], 0.35, 0.6], 0.12],
} })
map.addLayer({ id: 'gt-nodes', type: 'circle', source: 'gt-nodes', paint: {
@ -115,10 +119,10 @@ function addLayers(map) {
} })
// labels, one layer per level so each level has its own entry zoom; placement never overlaps labels
const LABELS = [
{ lv: ['f'], min: 0, size: [0, 11, 5, 15], weight: 'bold', offset: 1.1 },
{ lv: ['l'], min: 2.6, size: [2.6, 10, 7, 13], weight: 'regular', offset: 0.8 },
{ lv: ['b'], min: 3.4, size: [3.4, 9, 7, 11], weight: 'regular', offset: 0.7, muted: true },
{ lv: ['d'], min: 4.6, size: [4.6, 9, 8, 11], weight: 'regular', offset: 0.7, muted: true },
{ lv: ['f'], min: 0, size: [0, 10, 1, 11, 6, 15], weight: 'bold', offset: 1.1 },
{ lv: ['l'], min: 3.6, size: [3.6, 10, 8, 13], weight: 'regular', offset: 0.8 },
{ lv: ['b'], min: 4.4, size: [4.4, 9, 8, 11], weight: 'regular', offset: 0.7, muted: true },
{ lv: ['d'], min: 5.6, size: [5.6, 9, 9, 11], weight: 'regular', offset: 0.7, muted: true },
]
for (const L of LABELS) {
map.addLayer({ id: 'gt-lab-' + L.lv[0], type: 'symbol', source: 'gt-nodes', minzoom: L.min,
@ -199,9 +203,9 @@ function frame(coords) {
const duration = reducedMotion() ? 0 : 900
try {
if (e - w < 1e-4 && n - s < 1e-4) {
const z = Math.max(map.getZoom(), 5)
const z = Math.max(map.getZoom(), 6)
duration ? map.easeTo({ center: [w, s], zoom: z, duration, essential: true }) : map.jumpTo({ center: [w, s], zoom: z })
} else map.fitBounds([[w, s], [e, n]], { padding: 60, maxZoom: 7, duration, essential: true })
} else map.fitBounds([[w, s], [e, n]], { padding: 60, maxZoom: 8, duration, essential: true })
} catch (_) { /* a degenerate frame never breaks the board */ }
}

File diff suppressed because one or more lines are too long

View file

@ -578,7 +578,7 @@
"bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js",
"integrity": "sha384-7OLwBf2bK1Szc8ISMHusFv6dZkvimSiZdBtnCIsRCMZtVnMbBe1KaZ96OvXbu37e",
"integrity": "sha384-deOFSn4WjpXO/pQwb+/3TK1qR6tEsxDfl4Fi66Myv/KmPiuWd1rV8YG62fEctMEF",
"elements": [
"gospel-internet-board"
],
@ -594,7 +594,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": {
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-7OLwBf2bK1Szc8ISMHusFv6dZkvimSiZdBtnCIsRCMZtVnMbBe1KaZ96OvXbu37e\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-deOFSn4WjpXO/pQwb+/3TK1qR6tEsxDfl4Fi66Myv/KmPiuWd1rV8YG62fEctMEF\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
},
"instances": [
{

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-7OLwBf2bK1Szc8ISMHusFv6dZkvimSiZdBtnCIsRCMZtVnMbBe1KaZ96OvXbu37e" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-deOFSn4WjpXO/pQwb+/3TK1qR6tEsxDfl4Fi66Myv/KmPiuWd1rV8YG62fEctMEF" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../gospel-internet.js" integrity="sha384-7OLwBf2bK1Szc8ISMHusFv6dZkvimSiZdBtnCIsRCMZtVnMbBe1KaZ96OvXbu37e" crossorigin="anonymous"></script>
<script src="../gospel-internet.js" integrity="sha384-deOFSn4WjpXO/pQwb+/3TK1qR6tEsxDfl4Fi66Myv/KmPiuWd1rV8YG62fEctMEF" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-7OLwBf2bK1Szc8ISMHusFv6dZkvimSiZdBtnCIsRCMZtVnMbBe1KaZ96OvXbu37e" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-deOFSn4WjpXO/pQwb+/3TK1qR6tEsxDfl4Fi66Myv/KmPiuWd1rV8YG62fEctMEF" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-7OLwBf2bK1Szc8ISMHusFv6dZkvimSiZdBtnCIsRCMZtVnMbBe1KaZ96OvXbu37e" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-deOFSn4WjpXO/pQwb+/3TK1qR6tEsxDfl4Fi66Myv/KmPiuWd1rV8YG62fEctMEF" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-7OLwBf2bK1Szc8ISMHusFv6dZkvimSiZdBtnCIsRCMZtVnMbBe1KaZ96OvXbu37e" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-deOFSn4WjpXO/pQwb+/3TK1qR6tEsxDfl4Fi66Myv/KmPiuWd1rV8YG62fEctMEF" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-7OLwBf2bK1Szc8ISMHusFv6dZkvimSiZdBtnCIsRCMZtVnMbBe1KaZ96OvXbu37e" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-deOFSn4WjpXO/pQwb+/3TK1qR6tEsxDfl4Fi66Myv/KmPiuWd1rV8YG62fEctMEF" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
<script src="../../gospel-internet.js" integrity="sha384-7OLwBf2bK1Szc8ISMHusFv6dZkvimSiZdBtnCIsRCMZtVnMbBe1KaZ96OvXbu37e" crossorigin="anonymous"></script>
<script src="../../gospel-internet.js" integrity="sha384-deOFSn4WjpXO/pQwb+/3TK1qR6tEsxDfl4Fi66Myv/KmPiuWd1rV8YG62fEctMEF" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -578,7 +578,7 @@
"bundle": "gospel-internet",
"scriptUrl": "./gospel-internet/gospel-internet.js",
"output": "dx-maps/gospel-internet/gospel-internet.js",
"integrity": "sha384-7OLwBf2bK1Szc8ISMHusFv6dZkvimSiZdBtnCIsRCMZtVnMbBe1KaZ96OvXbu37e",
"integrity": "sha384-deOFSn4WjpXO/pQwb+/3TK1qR6tEsxDfl4Fi66Myv/KmPiuWd1rV8YG62fEctMEF",
"elements": [
"gospel-internet-board"
],
@ -594,7 +594,7 @@
"scriptUrl": "./gospel-internet/gospel-internet.js",
"embed": {
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-7OLwBf2bK1Szc8ISMHusFv6dZkvimSiZdBtnCIsRCMZtVnMbBe1KaZ96OvXbu37e\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-deOFSn4WjpXO/pQwb+/3TK1qR6tEsxDfl4Fi66Myv/KmPiuWd1rV8YG62fEctMEF\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
},
"instances": [
{