Area-quantile thresholds: In IslandsUmbrales por cuantil de área: En Islands

The heights of the view, sorted. Drag the point: with a percentage it moves along the share, with a raw threshold along the height.Las alturas del encuadre, ordenadas. Arrastra el punto: con porcentaje se mueve por la parte, con umbral crudo por la altura.

The next seeds, each cut on its own view (tap one to open it)Las semillas siguientes, cada una cortada en su encuadre (toca una para abrirla)

The idea in plain words

A noise map comes without a sea: someone has to decide where the coast is. The first idea anyone has is a fixed height: everything below 0 is water. The trouble is that the noise does not come out the same for every seed. One comes out low and almost all of it drowns; the next comes out high and is a continent.

The other way is to ask for a share: "I want 40 % sea". For that, all the heights are put in a row, from lowest to highest, and you look at which one sits at the 40 % mark. That height is the coast. That value, the one that leaves a given share below it, is called a quantile.

It is like splitting a class by height: "the ten shortest to the front row" works in any class; "everyone under 1.40 m" leaves the row empty in one class and full in another. The analogy has a limit: a class has thirty people to count; here each map has 65,536 cells, and counting carefully has a cost.

Look at the strip of seeds under the map and press Raw threshold: each world comes out with a different amount of water. Go back to By percentage: they all give the same. ✦ IA

TODO: Claudio: why this matters to you (the game's worlds, Islands' hills).

The traps, measured

The seed. With the threshold at 0 and frequency 2, the water goes from 26 % to 76 % depending on the seed (200 seeds). With frequency 1, from a map with no sea to one with no land. With frequency 4 it calms down (40–60 %): with more cycles per view, each map averages more. ✦ IA

The octaves. Each octave adds detail, but fBm divides by the sum of the amplitudes, so the values bunch up towards 0. At frequency 2, the typical range of a view drops from 1.44 with one octave to 1.13 with four. The water around 0 barely changes; the high ground does: a fixed snow line at 0.5 covers 11 % of the map with one octave and 1 % with four (mean of 100 seeds). That is why NoiseLab also paints the land bands by share: snow is the top 8 % of the land, whatever the octaves. The view. If the cut is measured on what is on screen, the coast moves when you drag: moving the map by a quarter, between 5 % of the cells (median) and 17 % switch from land to water or back, with no change to the terrain. With Cuts: world the cut comes from a fixed region of 16 × 16 views and the coast stays put. The price: the view no longer holds exactly 40 % water, because the 40 % belongs to the world. ✦ IA

Beyond the sea: hills and trees in Islands

Islands uses the same idea inside the land. Hills are "the top 20 % of the land" and peaks "the top 5 %", computed on every map. Peaks can never be more than hills, and a tie rule says that every cell at exactly the cut's height counts: so the share can come out slightly above, never below. With heights rounded to 1/64 we measured 22.4 % hills instead of 20 %. Vegetation went through the same. According to the notes in Islands' own code, the vegetation noise spanned roughly 0.29 to 0.76, so a fixed threshold above that range let not a single tree grow. Now the cut is a quantile over the cells that can hold plants. ✦ IA

Is there a theorem?

Several, and short. A quantile does not change under an increasing transformation. If every height goes through the same function that never goes down (adding something, multiplying by a positive number, a rising curve), the order of the cells does not change, so the same cells stay under the 40 %. That is why, by percentage, raising the whole terrain does not move the coast (you saw it in Reshaping height). Finding one position costs less than sorting. Sorting N values takes on the order of N log N comparisons. Hoare (1961) published a method that splits the row around a pivot and carries on only on the side that holds the wanted position: linear cost on average. Blum, Floyd, Pratt, Rivest and Tarjan (1973) showed that linear can be guaranteed even in the worst case. A sample is wrong by little, and by how much is known. The Dvoretzky–Kiefer–Wolfowitz inequality (1956), with the constant Massart proved (1990), bounds the gap between the distribution of a random sample of m values and the true one: with 95 % probability, less than √(ln(40) / 2m) at every cut at once. With 4,096 cells that is 2.1 points. What we measured is well below: 0.19 median and 0.61 in the worst of 50 seeds. The bound is for independent random samples, and this sample is a regular grid that covers the view evenly, leaving no area unseen. ✦ IA

The equations

level = h(k),  k = round(p · N)   (h(0) ≤ h(1) ≤ … ≤ h(N−1), the heights sorted)

water = #{ i : hi < level } / N

hills = h(n−k) of the land,  k = round(f · n);  hill if h ≥ hills

P( supx |Fm(x) − F(x)| > ε ) ≤ 2 e−2mε²

Read it as: sort the heights and take the one at position p · N; everything below it is water. The last line is the sample's bound: Fm is the share below x in the sample, F the true one, and the chance of being off by more than ε falls very fast as m grows. ✦ IA

What the picture says and what it doesn't ✦ IA

  • By percentage with world cuts, the 40 % belongs to the region of 16 × 16 views, not to the view: the legend says how much water is in what you see.
  • The times are your browser's and change from run to run. On a PC with Node, at 256²: sort 11 ms, quickselect 1.1 ms, histogram 2.3 ms, a 1/16 sample 0.6 ms.
  • Sorting looks expensive, but it gives every cut at once: NoiseLab needs eight (three water bands and six land bands), and with the sorted list they come free. Quickselect pays off when only one is needed.
  • There are nine definitions of a sample quantile in use (Hyndman and Fan, 1996), depending on how they round and whether they interpolate. This page uses NoiseLab's: the k-th smallest, with k = round(p · N).
  • The thumbnails in the strip are computed at 64², and each one is cut on its own view.

How it works ✦ IA

This part walks through the code of the cut, step by step. It covers:

  • NoiseLab's sea level.
  • Sorting and reading a position.
  • Quickselect.
  • A histogram.
  • A sample.
  • Islands' hills.

Plain JavaScript, no libraries. The methods (src/scripts/islands/quantiles.js) do not touch the DOM and use no randomness, so they port line by line to C#. The excerpts are cut from the scripts when the site is built.

1. NoiseLab's sea level

With a raw threshold, the level is given. By percentage, it is read from the list already sorted:

// Raw mode: the threshold is given, the area is measured. Percentage mode: the area is
// given, and the threshold is read from the sorted values (the quantile).
export function waterLevel(source, water) {
  const count = source.length;
  if (water.mode !== "percent") return water.raw;
  const k = Math.round((water.percent / 100) * count);
  return k <= 0 ? source[0] : k >= count ? source[count - 1] + 1e-6 : source[k];
}

2. Sorting

The naive, exact version: copy, sort, read position k.

// Index of the cell the level is read from: k = round(p · N), kept inside the grid.
export const rankOf = (p, count) => Math.min(count - 1, Math.max(0, Math.round(p * count)));

// 1. Sort everything, read position k. O(N log N); the sorted copy also gives every other
// cut (bands, histograms) for free, which is why NoiseLab keeps it.
export function quantileBySort(values, p) {
  const sorted = Float64Array.from(values).sort();
  return sorted[rankOf(p, sorted.length)];
}

3. Quickselect

Split around a pivot and carry on only on the side that holds k. The pivot is the median of three values (the first, the middle and the last), not a random one: so the result and the time are the same on every run, as Islands requires. Pick Quickselect in the Method tab: it gives the same level as sorting, about ten times faster.

// 2. Quickselect (Hoare, 1961): partition around a pivot and keep only the side that holds
// position k. Linear on average. The pivot is the median of the first, middle and last
// values, not a random one, so the result and the time are the same on every run.
// `scratch` is overwritten (pass a copy if the values matter).
export function quickselect(scratch, k) {
  let lo = 0, hi = scratch.length - 1;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    const a = scratch[lo], b = scratch[mid], c = scratch[hi];
    const pivot = a < b ? (b < c ? b : a < c ? c : a) : (a < c ? a : b < c ? c : b);
    let i = lo, j = hi;
    while (i <= j) {
      while (scratch[i] < pivot) i++;
      while (scratch[j] > pivot) j--;
      if (i <= j) { const t = scratch[i]; scratch[i] = scratch[j]; scratch[j] = t; i++; j--; }
    }
    if (k <= j) hi = j;
    else if (k >= i) lo = i;
    else break;                       // k sits between j and i: all equal to the pivot
  }
  return scratch[k];
}

4. A histogram

Two passes and no sorting: count how many values fall in each bin and walk the counts until k is passed. Inside the bin it interpolates. Lower the Bins to 4 and watch the error grow.

// 3. Histogram: count the values in `bins` equal bins between the minimum and the maximum,
// walk the counts until k values are passed, and interpolate inside that bin. Two passes,
// no sorting, a fixed amount of memory; the answer is approximate inside one bin.
export function quantileByHistogram(values, p, bins = 256, counts = new Uint32Array(bins)) {
  let min = Infinity, max = -Infinity;
  for (const v of values) { if (v < min) min = v; if (v > max) max = v; }
  if (!(max > min)) return min;
  counts.fill(0);
  const scale = bins / (max - min);
  for (const v of values) counts[Math.min(bins - 1, ((v - min) * scale) | 0)]++;
  const k = rankOf(p, values.length);
  let below = 0;
  for (let b = 0; b < bins; b++) {
    if (below + counts[b] > k) return min + (b + (k - below + 0.5) / counts[b]) / scale;
    below += counts[b];
  }
  return max;
}

5. A sample

One cell in every few along x and y, sorted. Move Cells used: at 1/16 the error stays in tenths of a point; at 1/256, a few points.

// 4. A sample: every `stride`-th cell in x and in y (one cell in stride²), sorted. The
// sample is a regular grid, not a random one, so it is deterministic. Its error shrinks like
// 1 / √(sample size) (see the DKW inequality in the page text).
export function quantileBySample(values, n, p, stride) {
  const side = Math.ceil(n / stride), sample = new Float64Array(side * side);
  let m = 0;
  for (let y = 0; y < n; y += stride)
    for (let x = 0; x < n; x += stride) sample[m++] = values[y * n + x];
  const sorted = sample.subarray(0, m).sort();
  return sorted[rankOf(p, m)];
}

6. Islands' hills

The same cut from the top, over the land, with the tie rule:

// Islands' area thresholds for hills (HillsThresholdOps2D, F3b′): over the land cells only,
// sorted ascending, k = round(f · n) cells from the top and the threshold is sorted[n − k];
// a cell is a hill when height ≥ threshold, so ties at the cut all count (the share can only
// come out above the target). f2 (peaks) is clamped to f1 (hills), so peaks ⊆ hills.
// Returns [hills, peaks]; +Infinity means an empty band.
export function areaThresholds(landSortedAsc, f1, f2) {
  const n = landSortedAsc.length;
  const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
  f1 = clamp01(f1);
  f2 = Math.min(clamp01(f2), f1);
  const one = (f) => {
    if (n <= 0) return Infinity;
    const k = Math.round(f * n);
    if (k <= 0) return Infinity;
    if (k >= n) return landSortedAsc[0];
    return landSortedAsc[n - k];
  };
  return [one(f1), one(f2)];
}

Going further

Bring quickselect to NoiseLab when one cut is enough (today it always sorts). Use a sample for the world cut on the phone, with the DKW bound to choose its size. And quantile cuts for other layers: moisture or temperature, so each biome covers a fixed share of the map. ✦ IA

La idea en palabras sencillas

Un mapa de ruido no trae mar: hay que decidir dónde está la costa. Lo primero que se le ocurre a cualquiera es una altura fija: todo lo que queda bajo 0 es agua. El problema es que el ruido no sale igual con cada semilla. Con una sale bajo y casi todo queda bajo el agua; con la siguiente sale alto y es un continente.

La otra manera es pedir una parte: «quiero un 40 % de mar». Para eso se ponen todas las alturas en fila, de la más baja a la más alta, y se mira cuál ocupa el puesto 40 %. Esa altura es la costa. Ese valor, el que deja por debajo una parte dada, se llama cuantil.

Es como cortar una clase por estatura: «los diez más bajos, a la primera fila» funciona en cualquier curso; «los que midan menos de 1,40» deja la fila vacía en un curso y llena en otro. La analogía tiene un límite: en una clase se cuentan treinta personas; aquí, 65.536 celdas en cada mapa, y contar con cuidado tiene un costo.

Mira la tira de semillas bajo el mapa y pulsa Umbral crudo: cada mundo sale con otra cantidad de agua. Vuelve a Por porcentaje: todos dan lo mismo. ✦ IA

TODO: Claudio: por qué te importa esto (los mundos del juego, las colinas de Islands).

Las trampas, medidas

La semilla. Con el umbral en 0 y frecuencia 2, el agua va del 26 % al 76 % según la semilla (200 semillas). Con frecuencia 1, de un mapa sin mar a uno sin tierra. Con frecuencia 4 se calma (40–60 %): con más ciclos por encuadre, cada mapa promedia más. ✦ IA

Las octavas. Cada octava suma detalle, pero el fBm divide entre la suma de las amplitudes, así que los valores se juntan hacia el 0. Con frecuencia 2, el rango típico de un encuadre baja de 1,44 con una octava a 1,13 con cuatro. El agua en torno a 0 casi no cambia; lo que cambia es lo alto: una línea de nieve fija en 0,5 cubre el 11 % del mapa con una octava y el 1 % con cuatro (media de 100 semillas). Por eso NoiseLab pinta también las bandas de tierra por porcentaje: la nieve es el 8 % más alto de la tierra, haya las octavas que haya. El encuadre. Si el corte se mide en lo que se ve, la costa se mueve al arrastrar: al desplazar el mapa un cuarto, entre un 5 % de las celdas (mediana) y un 17 % cambian de tierra a agua o al revés, sin que el terreno haya cambiado. Con Cortes: mundo el corte sale de una región fija de 16 × 16 encuadres y la costa se queda quieta. El precio: en el encuadre ya no hay exactamente un 40 % de agua, porque el 40 % es del mundo. ✦ IA

Más allá del mar: colinas y árboles en Islands

Islands usa la misma idea dentro de la tierra. Las colinas son «el 20 % más alto de la tierra» y las cumbres, «el 5 % más alto», calculados en cada mapa. Las cumbres nunca pueden ser más que las colinas, y una regla para los empates dice que todas las celdas con la altura justa del corte cuentan: así la parte puede salir un poco por encima, nunca por debajo. Con alturas redondeadas a 1/64 medimos un 22,4 % de colinas en lugar de un 20 %.

La vegetación pasó por lo mismo. Según las notas del propio código de Islands, el ruido de vegetación ocupaba más o menos de 0,29 a 0,76, así que un umbral fijo por encima de ese rango no dejaba crecer ni un solo árbol. Ahora el corte es un cuantil sobre las celdas que pueden tener plantas. ✦ IA

¿Hay un teorema?

Varios, y cortos. Un cuantil no cambia con una transformación creciente. Si todas las alturas pasan por la misma función que nunca baja (sumar algo, multiplicar por un número positivo, una curva creciente), el orden de las celdas no cambia, así que bajo el 40 % quedan las mismas. Por eso, con porcentaje, subir el terreno entero no mueve la costa (lo viste en Remodelar la altura). Encontrar un solo puesto cuesta menos que ordenar. Ordenar N valores cuesta del orden de N log N comparaciones. Hoare (1961) publicó un método que parte la fila en torno a un pivote y sigue solo por el lado donde está el puesto buscado: en promedio, un costo lineal. Blum, Floyd, Pratt, Rivest y Tarjan (1973) mostraron que se puede garantizar lineal incluso en el peor caso. Una muestra se equivoca poco, y se sabe cuánto. La desigualdad de Dvoretzky, Kiefer y Wolfowitz (1956), con la constante que demostró Massart (1990), acota la diferencia entre la distribución de una muestra al azar de m valores y la verdadera: con un 95 % de probabilidad, menos de √(ln(40) / 2m) en todos los cortes a la vez. Con 4.096 celdas eso da 2,1 puntos. Lo medido es bastante menos: 0,19 de mediana y 0,61 en el peor de 50 semillas. La cota es para muestras independientes y al azar, y esta muestra es una rejilla regular que cubre el encuadre de forma pareja, sin dejar zonas sin mirar. ✦ IA

Las ecuaciones

nivel = h(k),  k = round(p · N)   (h(0) ≤ h(1) ≤ … ≤ h(N−1), las alturas ordenadas)

agua = #{ i : hi < nivel } / N

colinas = h(n−k) de la tierra,  k = round(f · n);  colina si h ≥ colinas

P( supx |Fm(x) − F(x)| > ε ) ≤ 2 e−2mε²

Léase así: se ordenan las alturas y se toma la que ocupa el puesto p · N; todo lo que queda por debajo es agua. La última línea es la cota de la muestra: Fm es la proporción bajo x en la muestra, F la verdadera, y la probabilidad de equivocarse en más de ε cae muy rápido al crecer m. ✦ IA

Lo que la imagen dice y lo que no ✦ IA

  • Con porcentaje y cortes por mundo, el 40 % es de la región de 16 × 16 encuadres, no del encuadre: la leyenda dice cuánta agua hay en lo que ves.
  • Los tiempos son de tu navegador y cambian de una vez a otra. En un PC con Node, a 256²: ordenar 11 ms, quickselect 1,1 ms, histograma 2,3 ms, muestra de 1/16 0,6 ms.
  • Ordenar parece caro, pero da todos los cortes a la vez: NoiseLab necesita ocho (tres bandas de agua y seis de tierra), y con la lista ordenada salen gratis. Quickselect conviene cuando se necesita uno solo.
  • Hay nueve definiciones de cuantil de una muestra en uso (Hyndman y Fan, 1996), según cómo se redondea y si se interpola. Esta página usa la de NoiseLab: la k-ésima menor, con k = round(p · N).
  • Las miniaturas de la tira se calculan a 64² y cada una corta en su propio encuadre.

Cómo funciona ✦ IA

Esta parte recorre el código del corte, paso a paso. Cubre:

  • El nivel del mar de NoiseLab.
  • Ordenar y leer un puesto.
  • Quickselect.
  • Un histograma.
  • Una muestra.
  • Las colinas de Islands.

JavaScript sin librerías. Los métodos (src/scripts/islands/quantiles.js) no tocan el DOM ni usan azar, así que se portean línea a línea a C#. Los fragmentos se cortan de los scripts al construir el sitio. Los comentarios del código quedan en inglés.

1. El nivel del mar de NoiseLab

Con umbral crudo, el nivel viene dado. Con porcentaje, se lee en la lista ya ordenada:

// Raw mode: the threshold is given, the area is measured. Percentage mode: the area is
// given, and the threshold is read from the sorted values (the quantile).
export function waterLevel(source, water) {
  const count = source.length;
  if (water.mode !== "percent") return water.raw;
  const k = Math.round((water.percent / 100) * count);
  return k <= 0 ? source[0] : k >= count ? source[count - 1] + 1e-6 : source[k];
}

2. Ordenar

La versión ingenua y exacta: copiar, ordenar, leer el puesto k.

// Index of the cell the level is read from: k = round(p · N), kept inside the grid.
export const rankOf = (p, count) => Math.min(count - 1, Math.max(0, Math.round(p * count)));

// 1. Sort everything, read position k. O(N log N); the sorted copy also gives every other
// cut (bands, histograms) for free, which is why NoiseLab keeps it.
export function quantileBySort(values, p) {
  const sorted = Float64Array.from(values).sort();
  return sorted[rankOf(p, sorted.length)];
}

3. Quickselect

Partir en torno a un pivote y seguir solo por el lado que contiene k. El pivote es la mediana de tres valores (el primero, el del medio y el último), no uno al azar: así el resultado y el tiempo son los mismos en cada ejecución, como pide Islands. Elige Quickselect en la pestaña Método: da el mismo nivel que ordenar, unas diez veces más rápido.

// 2. Quickselect (Hoare, 1961): partition around a pivot and keep only the side that holds
// position k. Linear on average. The pivot is the median of the first, middle and last
// values, not a random one, so the result and the time are the same on every run.
// `scratch` is overwritten (pass a copy if the values matter).
export function quickselect(scratch, k) {
  let lo = 0, hi = scratch.length - 1;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    const a = scratch[lo], b = scratch[mid], c = scratch[hi];
    const pivot = a < b ? (b < c ? b : a < c ? c : a) : (a < c ? a : b < c ? c : b);
    let i = lo, j = hi;
    while (i <= j) {
      while (scratch[i] < pivot) i++;
      while (scratch[j] > pivot) j--;
      if (i <= j) { const t = scratch[i]; scratch[i] = scratch[j]; scratch[j] = t; i++; j--; }
    }
    if (k <= j) hi = j;
    else if (k >= i) lo = i;
    else break;                       // k sits between j and i: all equal to the pivot
  }
  return scratch[k];
}

4. Un histograma

Dos pasadas y sin ordenar: contar cuántos valores caen en cada caja y recorrer las cuentas hasta pasar k. Dentro de la caja se interpola. Baja las Cajas a 4 y mira cómo crece el error.

// 3. Histogram: count the values in `bins` equal bins between the minimum and the maximum,
// walk the counts until k values are passed, and interpolate inside that bin. Two passes,
// no sorting, a fixed amount of memory; the answer is approximate inside one bin.
export function quantileByHistogram(values, p, bins = 256, counts = new Uint32Array(bins)) {
  let min = Infinity, max = -Infinity;
  for (const v of values) { if (v < min) min = v; if (v > max) max = v; }
  if (!(max > min)) return min;
  counts.fill(0);
  const scale = bins / (max - min);
  for (const v of values) counts[Math.min(bins - 1, ((v - min) * scale) | 0)]++;
  const k = rankOf(p, values.length);
  let below = 0;
  for (let b = 0; b < bins; b++) {
    if (below + counts[b] > k) return min + (b + (k - below + 0.5) / counts[b]) / scale;
    below += counts[b];
  }
  return max;
}

5. Una muestra

Una celda de cada tantas en x y en y, ordenadas. Mueve Celdas usadas: con 1/16 el error queda en décimas de punto; con 1/256, en unos pocos puntos.

// 4. A sample: every `stride`-th cell in x and in y (one cell in stride²), sorted. The
// sample is a regular grid, not a random one, so it is deterministic. Its error shrinks like
// 1 / √(sample size) (see the DKW inequality in the page text).
export function quantileBySample(values, n, p, stride) {
  const side = Math.ceil(n / stride), sample = new Float64Array(side * side);
  let m = 0;
  for (let y = 0; y < n; y += stride)
    for (let x = 0; x < n; x += stride) sample[m++] = values[y * n + x];
  const sorted = sample.subarray(0, m).sort();
  return sorted[rankOf(p, m)];
}

6. Las colinas de Islands

El mismo corte desde arriba, sobre la tierra, con la regla de los empates:

// Islands' area thresholds for hills (HillsThresholdOps2D, F3b′): over the land cells only,
// sorted ascending, k = round(f · n) cells from the top and the threshold is sorted[n − k];
// a cell is a hill when height ≥ threshold, so ties at the cut all count (the share can only
// come out above the target). f2 (peaks) is clamped to f1 (hills), so peaks ⊆ hills.
// Returns [hills, peaks]; +Infinity means an empty band.
export function areaThresholds(landSortedAsc, f1, f2) {
  const n = landSortedAsc.length;
  const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
  f1 = clamp01(f1);
  f2 = Math.min(clamp01(f2), f1);
  const one = (f) => {
    if (n <= 0) return Infinity;
    const k = Math.round(f * n);
    if (k <= 0) return Infinity;
    if (k >= n) return landSortedAsc[0];
    return landSortedAsc[n - k];
  };
  return [one(f1), one(f2)];
}

Para ir más lejos

Llevar quickselect a NoiseLab cuando basta un corte (hoy ordena siempre). Usar una muestra para el corte por mundo en el móvil, con la cota de DKW para elegir su tamaño. Y cortes por cuantil para otras capas: la humedad o la temperatura, para que cada bioma ocupe una parte fija del mapa. ✦ IA

SourcesFuentes

Part of Islands Lab. The noise is the simplex of part 4 with Islands' PerlinOnSimplex gradient, summed in octaves; the water and the bands follow NoiseLab, the didactic scene in Islands, and the codes open the same map there. Texts marked ✦ IA were written with AI (on a heading: the whole section; ✓: reviewed by Claudio); the code and the measures were checked on this page and with tools/islands/measure-quantiles.mjs.Parte de Islands Lab. El ruido es el simplex de la parte 4 con el gradiente PerlinOnSimplex de Islands, sumado en octavas; el agua y las bandas siguen a NoiseLab, la escena didáctica de Islands, y los códigos abren allí el mismo mapa. Los textos marcados con ✦ IA están escritos con IA (en un título: toda la sección; ✓: revisados por Claudio); el código y las medidas se comprobaron en esta página y con tools/islands/measure-quantiles.mjs.