%%{init: {'theme': 'default', 'themeVariables': { 'fontSize': '32px'}}}%%
flowchart LR
A[Dec]-->B[date]-->C[time]-->D[snap]-->E[span]
click A "/dec"
click B "/dec/date"
click C "/dec/time"
click D "/dec/snap"
click E "/dec/span"
Dec
Introducing the Dec measurement system, which uses turns instead of months, weeks, hours, minutes, seconds, and degrees.
2025+329
Dec measurement system
This part of my website focuses on Dec, a measurement system that I created. All Dec measurements are based on turns. When measuring angles📐, a turn represents a full⭕️circle and equals 2\(\pi\) (\(\underline\tau\)) radians (rad) or 360 degrees (°). Geographic coordinates and compass🧭directions are angles📐and thus can, and should😄, be measured in turns instead of rad or °.
Longitude latitude course
Dec measures longitude in parallels (p), latitude in meridians (m), and compass🧭directions in roses (r). To measure certain kinds of angles📐, Dec uses specific types of turns with distinct names like p, m, or r. All turn types can be combined with metric prefixes, like deci, centi, or milli, to create turn submultiples, such as deciturns (dt), centiturns (ct), or milliturns (mt).
The table below provides the current longitude in milliparallels (mp) and latitude in millimeridians (mm) of Points 0 and 1 on the map🗺️beneath the table. By default, Point 0 is at 800 mp and 0 mm, near the Galápagos🏝️archipelago of Ecuador🇪🇨, and Point 1 is at 800 mp and 100 mm, near the bottom of the Missouri bootheel in the United States🇺🇸.
To move the points, click the map🗺️or edit their coordinates in the table. The toggle✅inputs above the table add layers to the map🗺️: country borders, a rainbow🌈colored🎨grid of Dec graticules, a choropleth of UTC time zones, and solar terminator shading with a yellow🟡dot denoting the point where the Sun☀️is directly overhead: mp and mm.
Alongside the geographic coordinates of a point, each row of the table contains the course in milliroses (mr) we would need to maintain to travel🧳the shortest distance to the other point. The shortest distance is shown as orange🟠dots on the map🗺️. The default courses in mr are 0 (North) from Point 0 to 1 and 500 (South) from Point 1 to 0.
Distance speed duration
Dec measures distance in taurs (c), speed in omegars (v), and time in years (y) and days (d). Each of these four turn types approximates (\(\approx\)) a physical property of the Earth🌍: c = \(\tau r\) \(\approx\) its circumference, y \(\approx\) the duration of its orbit around the Sun☀️, d \(\approx\) the duration of its rotation on its axis, and \(\text c\over\text d\) = v = \(\omega r\) \(\approx\) the speed of its rotation at the Equator.
At a speed of 0.5 v or 500 milliomegars (mv), we could travel🧳the 0.1 c or 100 millitaurs (mc) between the default positions📍of Points 0 and 1 in 0.2 d or 200 millidays (md). The time required to travel🧳between two points is the distance divided by the speed: mc ÷ v = md = c ÷ v = mc ÷ mv = d.
Interactive world map
viewof bordertoggle = labelToggle(Inputs.toggle, "Border", false, "bordertoggle")
viewof gridtoggle = labelToggle(Inputs.toggle, "Grid", false, "gridtoggle")
viewof utctoggle = labelToggle(Inputs.toggle, "UTC", false, "utctoggle")
viewof suntoggle = labelToggle(Inputs.toggle, "Sun", false, "suntoggle")
rstbtn.node();table = createTable([
{ Point: 0, Milliparallel: 800, Millimeridian: 0, Milliwindrose: 0 },
{ Point: 1, Milliparallel: 800, Millimeridian: 100, Milliwindrose: 500 },
], { headerEditable: false, appendRows: false })
// {Point: 0, Milliparallel: `${Math.floor(long2turn(Place_A[0], 3))}`, Millimeridian: `${Math.floor(lati2turn(Place_A[1], 3))}`, Milliwindrose: `${Math.floor(lati2turn(coor2bear(Place_A, Place_B)))}`},
// {Point: 1, Milliparallel: `${Math.floor(long2turn(Place_B[0], 3))}`, Millimeridian: `${Math.floor(lati2turn(Place_B[1], 3))}`, Milliwindrose: `${Math.floor(lati2turn(coor2bear(Place_B, Place_A)))}`},
// ], {headerEditable: false, appendRows: false})// https://observablehq.com/@d3/solar-terminator
// https://observablehq.com/@mbostock/time-zones
viewof coordinates = worldMapCoordinates([[turn2long(table.rows[1].cells[1].childNodes[0].innerText), turn2degr(table.rows[1].cells[2].childNodes[0].innerText % 250)], [turn2long(table.rows[2].cells[1].childNodes[0].innerText), turn2degr(table.rows[2].cells[2].childNodes[0].innerText % 250)], projection], [width, height * mapsize / 100])
//viewof coordinates = worldMapCoordinates([
// [turn2long(table.rows[1].cells[1].childNodes[0].innerText), turn2degr(table.rows[1].cells[2].childNodes[0].innerText % 250)],
// [turn2long(table.rows[2].cells[1].childNodes[0].innerText), turn2degr(table.rows[2].cells[2].childNodes[0].innerText % 250)],
// projection], [width, height])Color wheel compass
// https://observablehq.com/@maddievision/enneagram
quickRender(326, 326, context => {
const center = 163
const ringRadius = 140
const ringLineWidth = 4
// Ring
context.beginPath();
context.lineWidth = ringLineWidth
context.strokeStyle = "#ddd"
context.arc(center, center, ringRadius, 0, 2 * Math.PI);
context.stroke();
context.font = "Bold 16px Arial"
context.textAlign = 'center'
let octPoints = []
for (let i = 0; i < 8; i++) {
const xPhase = Math.sin(i / 8 * 2 * Math.PI)
const yPhase = Math.cos(i / 8 * 2 * Math.PI)
const x = center + ringRadius * xPhase
const y = center - ringRadius * yPhase
octPoints.push([x, y])
}
// Lines
octConnections.forEach(([a, b], i ) => {
const [x1, y1] = octPoints[a]
const [x2, y2] = octPoints[b]
const lineAngle = Math.atan2(y2 - y1, x2 - x1)
// Draw just short of the label circumference
const x2a = x2 - 28 * Math.cos(lineAngle)
const y2a = y2 - 28 * Math.sin(lineAngle)
const x1a = x1 + 28 * Math.cos(lineAngle)
const y1a = y1 + 28 * Math.sin(lineAngle)
context.lineWidth = ringLineWidth
context.strokeStyle = "#ddd"
context.beginPath();
context.moveTo(x2a, y2a);
context.lineTo(x1a, y1a);
context.stroke();
})
// Arrow Heads
octConnections.forEach(([a, b], i ) => {
const [x1, y1] = octPoints[a]
const [x2, y2] = octPoints[b]
const lineAngle = Math.atan2(y2 - y1, x2 - x1)
const xl = x2 - 88 * Math.cos(lineAngle - (15 / 360) * 2 * Math.PI)
const yl = y2 - 88 * Math.sin(lineAngle - (15 / 360) * 2 * Math.PI)
const xr = x2 - 88 * Math.cos(lineAngle + (15 / 360) * 2 * Math.PI)
const yr = y2 - 88 * Math.sin(lineAngle + (15 / 360) * 2 * Math.PI)
const x2a = x2 - 22 * Math.cos(lineAngle)
const y2a = y2 - 22 * Math.sin(lineAngle)
const x = x2 - 69 * Math.cos(lineAngle)
const y = y2 - 69 * Math.sin(lineAngle)
context.fillStyle = hsl8[i]
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.lineWidth = 1
context.beginPath();
context.moveTo(x2a, y2a);
context.lineTo(xl, yl);
context.lineTo(xr, yr);
context.lineTo(x2a, y2a);
context.fill();
context.stroke();
context.fillStyle = yiq(hsl8[i]) > 0.51 ? "#000" : "white"
context.fillText(["N", "NE", "E", "SE", "S", "SW", "W", "NW"][i], x, y + 6)
})
// Labels
octPoints.forEach(([x, y], i) => {
context.lineWidth = 1
context.fillStyle = hsl8[i]
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.beginPath();
context.arc(x, y, 22, 0, 2 * Math.PI);
context.fill();
context.stroke();
context.fillStyle = yiq(hsl8[i]) > 0.51 ? "#000" : "white";
context.fillText(["N", "NE", "E", "SE", "S", "SW", "W", "NW"][i], x, y + 6)
})
})// https://observablehq.com/@maddievision/enneagram
quickRender(326, 326, context => {
const center = 163
const ringRadius = 140
const ringLineWidth = 4
// Ring
context.beginPath();
context.lineWidth = ringLineWidth
context.strokeStyle = "#ddd"
context.arc(center, center, ringRadius, 0, 2 * Math.PI);
context.stroke();
context.font = "Bold 24px Arial"
context.textAlign = 'center'
let decPoints = []
for (let i = 0; i < 10; i++) {
const xPhase = Math.sin(i / 10 * 2 * Math.PI)
const yPhase = Math.cos(i / 10 * 2 * Math.PI)
const x = center + ringRadius * xPhase
const y = center - ringRadius * yPhase
decPoints.push([x, y])
}
// Lines
decConnections.forEach(([a, b], i ) => {
const [x1, y1] = decPoints[a]
const [x2, y2] = decPoints[b]
const lineAngle = Math.atan2(y2 - y1, x2 - x1)
// Draw just short of the label circumference
const x2a = x2 - 28 * Math.cos(lineAngle)
const y2a = y2 - 28 * Math.sin(lineAngle)
const x1a = x1 + 28 * Math.cos(lineAngle)
const y1a = y1 + 28 * Math.sin(lineAngle)
context.lineWidth = ringLineWidth
context.strokeStyle = "#ddd"
context.beginPath();
context.moveTo(x2a, y2a);
context.lineTo(x1a, y1a);
context.stroke();
})
// Arrow Heads
decConnections.forEach(([a, b], i ) => {
const [x1, y1] = decPoints[a]
const [x2, y2] = decPoints[b]
const lineAngle = Math.atan2(y2 - y1, x2 - x1)
const xl = x2 - 79 * Math.cos(lineAngle - (15 / 360) * 2 * Math.PI)
const yl = y2 - 79 * Math.sin(lineAngle - (15 / 360) * 2 * Math.PI)
const xr = x2 - 79 * Math.cos(lineAngle + (15 / 360) * 2 * Math.PI)
const yr = y2 - 79 * Math.sin(lineAngle + (15 / 360) * 2 * Math.PI)
const x2a = x2 - 22 * Math.cos(lineAngle)
const y2a = y2 - 22 * Math.sin(lineAngle)
const x = x2 - 60 * Math.cos(lineAngle)
const y = y2 - 60 * Math.sin(lineAngle)
context.fillStyle = hsl10[i]
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.lineWidth = 1
context.beginPath();
context.moveTo(x2a, y2a);
context.lineTo(xl, yl);
context.lineTo(xr, yr);
context.lineTo(x2a, y2a);
context.fill();
context.stroke();
context.fillStyle = yiq(hsl10[i]) > 0.51 ? "#000" : "white"
context.fillText(i, x, y + 8)
})
// Labels
decPoints.forEach(([x, y], i) => {
context.lineWidth = 1
context.fillStyle = hsl10[i]
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.beginPath();
context.arc(x, y, 22, 0, 2 * Math.PI);
context.fill();
context.stroke();
context.fillStyle = yiq(hsl10[i]) > 0.51 ? "#000" : "white";
context.fillText(i, x, y + 8)
})
})// https://observablehq.com/@pjedwards/compass-rose-as-legend-with-colors
svg`<svg width="${size}" height="${size}" viewBox="${-size/2} ${-size/2} ${size} ${size}">
<g transform='rotate(${Math.round(-colorD * .36)})'>
${repeat(tick(radius, 5, '#434343'), 5 * 4 * 10)}
${repeat(tick(radius, 8), 10 * 4)}
${repeat(`<path d="M 0,-${radius+12} l 3,10 l -6,0 z" fill="black" stroke="black" stroke-width="1"/>`, 4, 0)}
${repeat(`<path d="M 0,-${radius+12} l 3,10 l -6,0 z" fill="white" stroke="black" stroke-width="1"/>`, 4, 45)}
<circle r="${radius}" fill="#d3d3d3" stroke="#434343" stroke-width="3" />
${repeat(directionMarker(radius+14, 24), 4, 0)}
${repeat(directionMarker(radius+12, 24), 4, 45)}
${repeat(turnMarker(radius+14, 32), 4, 0)}
${repeat(turnMarker(radius+12, 32), 4, 45)}
${repeat(pie(radius-margin/2, 2 * Math.PI * (radius-margin/2) / deccolors.length / 2, 1, deccolors), deccolors.length, 360/deccolors.length)}
</svg>
`Hue saturation lightness (hsl)
// https://observablehq.com/@paavanb/progressive-color-picker
{ const input = Inputs.range([0, 1000], { label: "Hue", value: 0, step: 1 })
input.value = initialHSL[0]
input.oninput = (evt) => onUpdateHSL(dec2hue(evt.currentTarget.value / 1000), colorS / 1000, colorL / 1000)
return Inputs.bind(input, viewof colorD)
}Course color table
| mr🧭 | c°🧭 | h°🎨 | hex🎨 | |
|---|---|---|---|---|
| NE | 125 | 45 | 44 | fb0 |
| E | 250 | 90 | 68 | df0 |
| SE | 375 | 135 | 96 | 6f0 |
| S | 500 | 180 | 180 | 0ff |
| SW | 625 | 225 | 216 | 06f |
| W | 750 | 270 | 264 | 60f |
| NW | 875 | 315 | 292 | d0f |
| N | 0 | 0 | 0 | f00 |
The color🎨wheel compass🧭above indicates both a hue in mt and a course in mr. We can convert the hue to HSL and HSV degrees (h°) and the course to compass🧭degrees (c°): 25 mr = 9 c°. To rotate🔄the color🎨wheel compass🧭, use the “Hue” range🎚️and hue bar inputs beneath it or change the course from Point 0 to 1 on the map🗺️.
Red green blue (rgb)
The table beneath the hue bar compares the current Point 0 to 1 course in its top row with the cardinal and intercardinal directions. Together, the range🎚️inputs underneath the hue bar form a “Hue Saturation Lightness” (HSL) triplet. Like “Red Green Blue” (RGB) or hexadecimal (hex) triplets, HSL triplets specify a full-fledged color🎨instead of just a hue.
Color🎨can provide a general idea of angular📐measure, regardless of the metric prefixes or units we use. Therefore, we can reuse♻️colors🎨across many different contexts. Most often, red designates starting points, like North (0 mr) and Longitude 0 (0 mp), and cyan denotes midpoints, such as South (500 mr) and Longitude 5 (500 mp).
The Equator (0 mm) is the major latitude midway between the South (-250 mm) and North (250 mm) Poles. Unlike the Equator, the Tropics of Cancer♋(65 mm) and Capricorn♑️(-65 mm) and the Arctic (250 mm – 65 mm = 185 mm) and Antarctic (65 mm – 250 mm = -185 mm) Circles are defined by the axial tilt of the Earth🌏(65 mt).
Dec time zones
Enable the “Grid” toggle✅input to see Latitudes -2 (-200 mm), -1 (-100 mm), 0 (0 mm), 1 (100 mm), and 2 (200 mm) on the map🗺️above along with the ten major longitudes that divide the Earth🌎into the ten Dec time zones. Notably, Longitude 0 is the major longitude that functions as both the Prime Meridian and International Date Line in Dec.
Like the ten major longitudes that separate them, Dec time zones are numbered 0 to 9. Based on its current deciparallel (dp) longitude, , Point 0 on the map🗺️above is in Zone . The number assigned to each time zone is its offset from Zone 0 in decidays (dd). To obtain the dd offset at a location, we floor its dp longitude: ⌊⌋ = .
Each Dec time zone is 1 dp wide and 0.5 m long. While 1 m is always ~1 c long, the length of a p varies by latitude. At the Equator, 1 p is ~1 c long. At the North or South Pole, the length of a p is zero. The approximate c length of a p is the cosine of its latitude in m, rad, or °, depending on the input requirement of our cosine function: cos() = .
Dates and times
Dec dates consist of a “year of era” (yoe) and a “day of year” (doy), whereas Dec times are composed of a “time of day” (tod) and a “time zone offset” (tzo). In Zone 0, the current date is + and the current time is -0. Color🎨labels🏷️can help us to visually parse the date and time that make up a Dec snap🫰: +-0.
Millenium Year Day
Yoe color🎨labels🏷️are based on millimillennia (mk). Every millennium starts with Year 0 (0 mk) and has Year 500 (500 mk) as its midpoint. Doy color🎨labels🏷️are derived from milliyears (my). Every year starts on Day 0 (0 my). The midyear point (500 my) is noon (500 md) on Day 182 in common years and midnight (0 md) on Day 183 in leap years.
Day of dek (dod)
Each doy also has two components. The first two digits of a three-digit doy represent a group of ten days called a decaday (dek). The last digit of a doy is the “day of dek” (dod). In Dec, deks are used instead of months and weeks. Likewise, Dec uses dods in lieu of days of month (doms) and days of week (dows). In Zone 0, it is currently Dek and Dod .
Zone equatorial meter (zem)
Apart from c, Dec also measures distance using a unit called the zone equatorial meter (zem). The width of a Dec time zone at the Equator is approximately ten million (~107) zems. Similarly, the distance from the Equator to one of the Poles is ~107 meters. In other words, a decimeridian (dm) is ~107 zems long and a quarter meridian is ~107 meters long.
Length area volume
You can approximate a zem (z) using your hands🤲. With your palms flat on a table in front of you and the tips of your thumbs👍touching, the maximum distance between the tips of your pinkies is ~1 z. When you spread out the fingers on one hand✋or do the “call me”, “drink”, or “shaka”🤙gesture, your thumb👍and pinky tips are ~0.5 z apart.
To visualize a square zem (z2), imagine four people standing in a circle, facing inward, each with their right hand✋placed on top of the elbow of the person to their right. Alternatively, two people can stand in front of each other and raise their arms💪, placing one hand✋on the elbow of the other person and the other hand✋on their own elbow.
You can approximate a z2 yourself by sitting in a chair🪑or standing🧍with your knees and feet🦶1 z, 4 decimeters (dm), or 16 inches apart, which is probably about the width of your hips or shoulders. The z2 will be between your shins, its top will be below your knees, and its bottom will be either above your ankles or feet🦶, depending on your height.
Typical seat height
According to dimensions.com, 115 centizems (cz) is the typical seat height for both men and women age 25 to 45. A box📦that is the size of a cubic zem (z3) would likely fit under a typical chair🪑 or in between the shins of two people sitting in front of each other with their knees and feet🦶1 z apart and their legs🦵bent at 25 centiturn (ct) angles📐.
Perpetually setting sun
In Slovak🇸🇰, zem means Earth🌍. This is fitting because all Dec units are based on physical attributes of the Earth🌏. At the Equator, the Earth🌎rotates on its axis at a speed of ~1.00224 v. If we could indefinitely maintain this speed while flying West in an airplane✈️towards the setting sun☀️, we would be able to perpetually fly into the sunset🌅.
Speed of sound
To travel fast enough for a perpetual sunset🌅, the airplane✈️would need to surpass the speed of sound🔊, which at 15 ° Celsius and 1 standard atmosphere is ~0.735048 v or Mach 1. Mach numbers are relative to the speed of sound🔊, which varies greatly by air temperature and pressure. The cruising speed of a Boeing 747 is ~0.54 v or Mach ~0.85.
The highway🛣️speed of a car🚗is roughly tenfold slower than the cruising speed of an airplane✈️. If we are driving on a highway🛣️at a speed of 50 mv and our exit is 1000 z away, we will have 20 centimillidays until we have to exit the highway🛣️. To ensure we do not miss our exit, we can periodically check a countdown of the remaining z: .
Centimilliday (cmd)
Dec refers to a centimilliday (cmd) as a beat (b) because it is similar in duration to a heart❤️beat or musical beat. A d is 100 centiday (cd), 105 b, or 106 microdays (µd). One mc is 100 kilozems (kz), 105 z, 106 decizems (dz), or 106 nanotaurs (nc). Therefore, mv = \(\text{mc}\over\text d\) = \(\text {kz}\over\text {cd}\) = \(\text z\over\text b\) = \(\text {dz}\over\text{µd}\) = \(\text {nc}\over\text{µd}\). A cd is 96% of a quarter hour and a b is 86.4% of a second.
Beats per milliday (bpm)
A normal resting heart❤️rate is between 100 and 166.6 b per md (bpm). The unofficial anthem of the Dec measurement system, “Turn the beat around”, has a tempo of 188.64 bpm, which corresponds to the allegro tempo marking. A Dec clock⏰ticks at a rate of 100 bpm, \(\text b^{-1}\), \(1\over\text b\), or 1 inverse beat (ib), which is 1.15740 times more frequent than a Hertz.
Frequency period wavelength
Dec uses ib, b, and z, often with metric prefixes, to measure the frequency, period, and wavelength, respectively, of a sound or light wave. The equations below show how frequency, period, and wavelength are related to each other and to speed. The speed of light is 647.551657 megaomegars (Mv), which is about 881 times faster than the speed of sound.
\[\text{frequency} = \text{speed} \div \text{wavelength} = 1 \div \text{period}\]
\[\text{period} = \text{wavelength} \div \text{speed} = 1 \div \text{frequency}\]
\[\text{wavelength} = \text{speed} \times \text{period} = \text{speed} \div \text{frequency}\]
The frequency range of the visible spectrum of light is ~345.6 to ~914.4 teraib (Tib). The range of sound frequencies which can be audible for humans is ~10 to ~24000 ib. The period and wavelength that correspond to the frequency chosen by the range input below are 1000 ÷ ib = millibeats (mb) and 735.048 mv ÷ ib = z.
In addition to ib, the limits of human hearing can be expressed in musical steps (s). We can use the equations below to convert between ib and s. Frequencies less than 12.5 ib have a negative s value and are unlikely to be audible outside of carefully controlled laboratory experiments. Similarly, frequencies above 109 s or 24320 ib represent the upper limit of the audible range.
Like the ten Dec colors, there are ten frequencies that Dec chooses from the audible range to serve as a set of sound labels. These ten frequencies the Dechromatic scale.
Dec uses nine spectral colors and a tenth color which cannot be defined by a single wavelength, period, or frequency because it is an equal mix of Red and Blue.
To make the audible frequency range more intuitive, Humans can hear The typical range for humans extends from Tone 03 to Tone 104.a sound wave include its frequency, pitch in ib, period, musical in steps, . From the value selected by the “Frequency” range🎚️input below, we can calculate a pitch: , a : The table The positive (+) and negative (–) indexes, hex triplets, and h° in the table below are used by Dec to label🏷️groups of ten, like dods, “top of the dd” tods, and time zones. In addition to colors🎨, Dec also labels🏷️groups of ten with the musical notes that constitute the Dec chromatic (Dechromatic) scale of the Ten equal temperament (Tenet) musical system.
Ten equal temperament (Xet)
Tenet (Xet) identifies each Dechromatic scale note with a single-digit integer and expresses all other possible sound🔊frequencies as decimal numbers. In contrast, the notes of the 12 equal temperament (12ET) musical system have names that consist of a letter from A to G and often a symbol such as sharp (♯), half sharp (𝄲), flat (♭), and half flat (𝄳).
The sound🔊frequencies of two consecutive Xet Dechromatic scale notes always differ by one step (s), but the differences between consecutive 12ET chromatic scale notes vary from the ~599 millisteps (ms) between A♯ and B to the ~1067 ms between G♯ and A. A typical person can reliably distinguish sounds🔊that differ by at least 200 ms.
The rightmost column of the table below shows the 12ET notes that are closest to the Xet Dechromatic scale notes. 12ET considers B𝄲, D𝄲, and E𝄳 to be microtones. The sound🔊frequency differences between the nearest Xet and 12ET notes in the table range from the 8 ms between Notes 9 and A to the ~87 ms between Notes 5 and F.
Color sound table
| + | – | hex🎨 | h°🎨 | 12ET🎶 |
|---|---|---|---|---|
| 0 | -10 | f00 | 0 | A♯ |
| 1 | -9 | f90 | 36 | B𝄲 |
| 2 | -8 | ff0 | 60 | C♯ |
| 3 | -7 | af0 | 80 | D𝄲 |
| 4 | -6 | 0f0 | 120 | E𝄳 |
| 5 | -5 | 0ff | 180 | F |
| 6 | -4 | 08f | 208 | F♯ |
| 7 | -3 | 00f | 240 | G |
| 8 | -2 | 90f | 276 | G♯ |
| 9 | -1 | f0f | 300 | A |
Octave + note = tone
The image above applies Dec color🎨labels🏷️to one octave of piano🎹keys. In Xet, an octave is 10 s or 104 ms. From top to bottom, the text below the image provides the scientific pitch name, integer i sound🔊frequency, and integer mz wavelength of the corresponding white key. As octave indexes and frequencies increase, wavelengths decrease.
From the perspective of Xet, all of the labeled🏷️keys in the image above are in Octave 4. Octave indexes in Xet and 12ET match except for Xet notes with indexes below 2 and 12ET notes from A♯ to B𝄲. An octave index is the Xet analog of a clef in staff notation, because both are responsible for setting the reference frame that allows us to interpret the relative pitches of notes as the absolute pitches of tones.
Clefs in staff notation and octave indexes in Xet set the reference frame that allows us to
When we append a positive note index that is less than ten to an octave index which is a positive integer, we obtain a Xet musical tone index.
The tone indexes of the labeled🏷️keys in the image above range from 40.069 to 49.008. Tone 49.008 is A4, the A note widely used to tune musical instruments. Tone 41.302 is C4, the “Middle C” in between the bass and treble🎼clefs of a grand staff.
The mermaid chart below visualizes the structure of the Xet music notation example above it, which is composed of the Xet equivalents of an ascending C♯ major arpeggio and a C♯ major chord. This arpeggio and chord are also shown beneath the mermaid chart in staff notation. Whereas staff notation is graphical, Xet notation is text-based.
8 |
42 5 8 5 |
2 |
%%{init: {'theme': 'default', 'themeVariables': { 'fontSize': '20px'}}}%%
flowchart LR
subgraph arpeggio[C# Arpeggio]
A[42<br>C#4]
B[5<br>F4]
C[8<br>G#4]
end
subgraph chord[C# Chord]
X[8<br>G#4]
Y[5<br>F4]
Z[2<br>C#4]
end
A-->B-->C
C-->X
C-->Y
C-->Z
click A "#cst"
click B "#cst"
click C "#cst"
click X "#cst"
click Y "#cst"
click Z "#cst"
function abc(tune, midi = false, notation = true) {
function colorRange(range, color) {
if (range && range.elements) {
range.elements.forEach(function(set) {
set.forEach(function(item) {
item.setAttribute("fill", color);
});
});
}
}
const result = html `<div/>`;
if (notation) {
const notation = result.appendChild(html `<div/>`);
var abcElem = (abcjs.renderAbc(notation, tune));
}
if (midi) {
result.appendChild(html `<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css"/><link rel="stylesheet" type="text/css" href="https://unpkg.com/abcjs@5.1.2/abcjs-midi.css"/>`);
const midi = result.appendChild(html `<div/>`);
abcjs.renderMidi(midi, tune, {
midiListener: function(a, b, c) {},
animate: {
listener: function(a, b, c) {
colorRange(a, "#000000");
colorRange(b, "#3D9AFC");
},
target: abcElem[0],
qpm: abcElem[0].getBpm()
}
});
// abcjs.midi.startPlaying(document.querySelector(".abcjs-inline-midi"),true)
}
return result;
}Xet vertically aligns chords into columns. A chord can contain either notes that represent tones or the tones themselves. Mixing notes and tones in the same chord is not allowed. The chord in the example above can be written with notes instead of tones because Tone 42 sets Octave 4 as the default. Each line can have its own default octave.
In the example below, Tone 46 sets the default on the middle line to Octave 4 but then Tone 50 changes it Octave 5. Based on the default, we know the middle note in the chord represents Tone 50. We can then infer the tones represented by the top and bottom notes because chords are always ordered from highest to lowest tone.
2 |
46 50 2 0 |
6 |
%%{init: {'theme': 'default', 'themeVariables': { 'fontSize': '20px'}}}%%
flowchart LR
subgraph arpeggio[F# Arpeggio]
A[46<br>F#4]
B[50<br>A#4]
C[2<br>C#5]
end
subgraph chord[F# Chord]
X[2<br>C#5]
Y[0<br>A#4]
Z[6<br>F#4]
end
A-->B-->C
C-->X
C-->Y
C-->Z
click A "#cst"
click B "#cst"
click C "#cst"
click X "#cst"
click Y "#cst"
click Z "#cst"
Column widths are measured in characters. A column of notes has a width of one character.
The three notes vertically aligned in the column below are the Xet equivalent of a F♯ major chord. Tone 50 is able to set the reference frame for all three of these notes, even though they do not all belong to the same octave. Notes that span more than two octaves can be part of the same column but not part of the same reference frame.
2 | 2 |
46 50 2 0 | 2 0 x 0 |
6 | 6 6 |
8 8 |
5 x 5 |
42 x 2 |
2 2 |2 x 2 |
0 x 0 | 0 x 0 |
46 x 6 | 6 6 |
52 2 x | 2 2 |
50 x 0 x | 0 x 0 |
46 x 6 |6 x 6 |
52 2 | 2 ∅ 2 |
50 ∅∅ 0 | 0 ∅ 0 |
46 ∅∅ 6 | 6 6 |
Thanks to this convention, we can The first example below uses Xet tones to emulate the F♯ major chord. because aligned three notes in column below consists of notes because the previous in the example below below the example below, notes should be played If a chord spans multiple octaves, it has to be specified with tones. In the example below, Tone 46 sets the octave and then Tone 502) changes it. indicate that these notes are in Octave 4 The whereas the chord (F♯A♯C♯ = 602) spans two octaves. When played as an arpeggio starting immediately after kkkkk shown below can be written in Xet as one line of text that consists of a time signature, tones, notes, spaces, and bar (|) characters.
4442 5 8 6 |50 2 2 0 |46 8 5 2
4442 5 8 6 |50 2 2 0 |46 8 5 2
at the end of every measure:
4442 5 8 . time signatures,
444| The value of a space depends on the time signatures, which in Xet consist of a top number that defines the number of musical beats per measure and a bottom number that determines the number of spaces per musical beat. The number of spaces per measure is the product of the top and bottom numbers.
If a time signature is never specified, we assume there are four beats per measure, four spaces per beat, and thus sixteen spaces per measure. Whenever
The color labels of the examples below, one musical beat is indicated by four spaces ( ). Therefore, a single space ( ) is worth a quarter of a musical beat, which is the equivalent of an eighth note in common time. Similarly
Each tone in the arpeggio shown below that consists of the three C♯ major chord notes immediately above Middle C. Each note in the arpeggio lasts a If we specify that one space ( ) is equivalent to a quarter of a musical beat, this arpeggio could be written in Xet as a series of tones: 42 45 48 . To avoid repeating the octave index, we can specify it at the beginning of the line and place a bar character (|) between it and the three notes of the arpeggio: 4|2 5 8 .
Xet indicates note duration with whitespace indicated with one or more spaces following a note or a rest indicates its duration. The single space ( ) that follows each note in the arpeggio example above indicates that each note is held for the shortest duration. The table below shows the note and rest names, symbols, and number of spaces that are equivalent if a quarter note is one beat and there are eight spaces per beat.
a single space ( ) is equivalent to an eighth note in. If a piece of music contains sixteenth notes, we will need at least eight spaces per musical is often an eighth note.
If these three notes are to be played simultaneously instead of sequentially, they need to be split across three separate lines:
4|2
4|5
4|8
If the three notes are all quarter C♯, F, and G♯,. Rather than annotate music as a series of tones, Xet first identifies an octave at the beginning of a line before the notes that should be played in that octave. the notes that are played simultaneously instead of sequentially are written on separate lines and aligned vertically using whitespace. The example below shows an below Middle C. Notes written on the same line are in the same octave and are played sequentially. Notes can be grouped into a chord such as the F♯ major chord (A♯C♯F♯): 026. All notes, rests, and chords are followed by a Xet separator which indicates their respective durations. A separator that follows a chord applies to all notes in that chord: 026 . As shown in the table below, Xet separators can be color coded.
| Note | Rest | Separator | US name | UK name |
|---|---|---|---|---|
| 𝅜 | 𝄺 | double | breve | |
| 𝅝 | 𝄻 | whole | semibreve | |
| 𝅗𝅥 | 𝄼 | half | minim | |
| 𝅘𝅥 | 𝄽 | quarter | crotchet | |
| 𝅘𝅥𝅮 | 𝄾 | 𝄖 | eighth | quaver |
| 𝅘𝅥𝅯 | 𝄿 | 𝄗 | sixteenth | semiquaver |
| 𝅘𝅥𝅰 | 𝅀 | 𝄘 | thirty-second | demisemiquaver |
| 𝅘𝅥𝅱 | 𝅁 | 𝄙 | sixty-fourth | hemidemisemiquaver |
| 𝅘𝅥𝅲 | 𝅂 | 𝄚 | hundred twenty-eighth | semihemidemisemiquaver |
| 𝄛 | two hundred fifty-sixth | demisemihemidemisemiquaver |
In Xet, a rest is denoted by a null sign (∅). Notes, rests, chords, and their separators are grouped into measures that have a bar (|) on both sides. Measures are grouped into lines that start with an octave index and end with a newline character. Lines separated by empty lines are played sequentially, whereas groups of lines are played simultaneously.
Since each line that represent Octaves 4 and 5, span Notes 40 to 59, and overlap with the majority of the range of 12ET treble clef tones: 41.302 to 61.302. Similarly, two lines representing Octaves 2 and 3 are To cover most of the tone range of the bass clef: 21.302 to 41.302, we need two Xet lines labeled as Octaves 2 and 3, Notes 20 to 39. Of these four lines, the top two cover but the treble clef and the bottom two closely match the bass clef.
The Xet analog of the 12ET treble clef is two lines that represent Octaves 4 and 5, to be played separately are separated by spaces and grouped into measures that end with a bar (|). Measures are grouped into lines that start with an octave and time signature and end with a newline character.
Groups of lines are played together and are separated by empty lines. With four lines, we can cover Octaves 2, 3, 4, and 5 or equivalently Notes 20 to 59, which is close to the range of the 12ET bass and treble clefs: 21.302 to 61.302. Of these four lines, the top two are analogous to the treble clef and the bottom two closely match the bass clef.
All rests except whole rests are followed by a fraction: ½ for a half rest, ¼ for a quarter rest, ⅛ for an eighth rest, and so on. The same goes for notes. The first two measures of the chorus from “Turn the beat around” are shown below, first using Xet music notation and then a 12ET treble clef.
444 | 94 98 88 88 84 68 | 634 ∅4 |
444 | 94 98 88 88 84 68 | 634 ∅4 |
444 | 9¼ 9⅛ 8⅛ 8⅛ 8¼ 6⅛ | 6¾ ∅¼ |
US customary units
The unit conversion table below shows the United States🇺🇸(US) customary units that Dec redefines. The values in the first column are approximate fold changes from original to redefined units. A fold change of 1 means 0 change. Identical fold changes indicate US customary units derived from the same Dec and International System of Units (SI) units.
Unit conversion table
| US | Dec | SI |
|---|---|---|
| 0.9688 square inches | \(1\over256\) z2 | 625 mm2 |
| 0.9688 square feets | \(9\over16\) z2 | 9 dm2 |
| 0.9688 square yards | \(81\over16\) z2 | 81 dm2 |
| 0.9843 inches | \(1\over16\) z | 25 mm |
| 0.9843 feet | \(3\over4\) z | 3 dm |
| 0.9843 yards | \(9\over4\) z | 9 dm |
| 0.9877 grains | 1 grain | 64 mg |
| 0.9877 pounds | 7 kilograins | 448 g |
| 0.9884 acres | \(1\over40\) kz2 | \(1\over250\) km2 |
| 0.9884 square miles | 16 kz2 | \(64\over25\) km2 |
| 0.9942 miles | 4 kz | \(8\over5\) km |
| 1.0356 miles per hour | 1 mv | \(5\over3\) \(\text{km}\over\text{hour}\) |
| 1.0567 cups | \(1\over256\) z3 | 250 mL |
| 1.0567 pints | \(1\over128\) z3 | 500 mL |
| 1.0567 quarts | \(1\over64\) z3 | 1 L |
| 1.0567 gallons | \(1\over16\) z3 | 4 L |
| 1.0567 kegs | 1 z3 | 64 L |
| 1.0567 barrels | 2 z3 | 128 L |
| 1.0821 tablespoons | \(1\over4\) dz3 | 16 mL |
| 1.0821 ounces | \(1\over2\) dz3 | 32 mL |
| 1.0821 wineglasses | 1 dz3 | 64 mL |
| 1.2549 drops | 1 cz3 | 64 µL |
Miles per hour (mph)
Unlike Dec and SI, the US customary measurement system does not use metric prefixes to scale units by powers of ten. Redefined US customary units serve as convenient reference points. US customary volume units have intuitive names and scale by powers of two.
Drop wineglass keg
A square kilozem (kz2) is 1 hexakilare, 16 hectares, 1600 ares, 40 Dec acres, 0.16 square kilometers (km2), 0.0625 Dec square miles, or 106 z2. A z2 is a hexamilliare (x), 16 square decimeters (dm2), 1.7 Dec square feet🦶, or 256 Dec square inches. A square decazem (Dz2) is 1 hexadeciare, 16 square meters (m²), ~19.75 Dec square yards, or 100 x.
A cubic decizem (dz3) is 1 cubic nanotaur (nc3), 2 Dec ounces, 64 milliliters (mL), 1000 Dec drops, or 1000 cubic centizems (cz3). A dz3 of water🌊weighs \(1\over7\) Dec pounds, 64 grams (g), or 1000 Dec grains. A Dec ounce of water weighs \(1\over14\) Dec pounds, 500 Dec grains, or 32 g. To avoid confusion between liquid and solid ounces, Dec does not measure weight in ounces.
A z3 of water🌊weighs 64 kilograms (kg), 128 Dec pounds, or a 1000 Dec kilograins. If Leonardo da Vinci’s Vitruvian Man were 4 z tall, we could measure 1 z from his knees to his feet🦶or from his elbows💪to his fingertips. If he also weighed 1000 Dec kilograins, his Body Mass Index (BMI) would be 62.5 kilograins per x (\(\text {kg}\over\text x\)) or 25 kilograms per square meter (\(\text {kg}\over\text m^2\)).
Body mass index (bmi)
A normal BMI ranges from 46.25 to 62.5 \(\text {kg}\over\text x\) or 18.5 to 25 \(\text {kg}\over\text m^2\). A person with a BMI above 75 \(\text {kg}\over\text x\) or 30 \(\text {kg}\over\text m^2\) can be classified as obese. A BMI of kilograins ÷ x = \(\text {kg}\over\text x\) = kilograms ÷ m² = \(\text {kg}\over\text m^2\) is considered .
Centizem centimeter inch
The longest length depicted in the image of a ruler📏above is 1 dz, 1 nc, 4 centimeters (cm), or \(8\over5\) Dec inches, and the shortest length is \(1\over2\) mz, \(1\over5\) millimeters (mm), \(1\over125\) Dec inches, or \(1\over127\) US customary inches. A US customary inch is \(127\over2\) mz, \(127\over5\) mm, or \(127\over125\) Dec inches. A Dec inch is \(5\over2\) cm. A cm is \(5\over2\) cz. A z is 4 dm. A dm is 4 Dec inches.
Summary
This article introduces the Dec measurement system and describes how Dec uses metric prefixes and the properties of the planet Earth🌍to define units based on turns for geographic coordinates, compass🧭directions, dates, times, speeds, distances, areas, volumes, and weights. Each unit has a unique name, such as p, m, r, y, d, b, v, c, z, or x.
Dec attempts to bridge the gap, improve interoperability, and faciliate conversion between the US customary and SI measurement systems by redefining US customary units. Redefinition of US customary units makes inches ~1.58% shorter, miles ~0.58% shorter, pints ~5.67% larger, ounces ~8.21% larger, and pounds ~1.23% lighter.
Dec color🎨labels🏷can convey an impression of a value at a glance. Xet sound🔊labels🏷allow us to estimate a value without even having to look at it. Both types of labels can help avoid confusion when decimal separators appear, disappear, or move due to a measurement unit change such as the addition, removal, or replacement of a metric prefix.
Next
Now that you have had a taste of Dec, I hope that you are hungry for more! If so, dive🤿deeper into Dec dates and times before tackling Dec snaps🫰and spans🌈. My filter, include, and script articles discuss the Quarto publishing system and how I display Dec dates in the navigation bar, title block, and citation section of each article on my website.
%%{init: {'theme': 'default', 'themeVariables': { 'fontSize': '32px'}}}%%
flowchart LR
A[Dec]-->B[date]-->C[time]-->D[snap]-->E[span]
Z[ ]:::empty~~~F[Quarto]-->G[filter]-->H[include]-->I[script]
classDef empty width:0px;
click A "/dec"
click B "/dec/date"
click C "/dec/time"
click D "/dec/snap"
click E "/dec/span"
click F "/quarto"
click G "/quarto/filter"
click H "/quarto/include"
click I "/quarto/script"
Cite
Please support Dec by citing it as shown at the bottom of this article. Listed below are citations for the Observable notebooks that I adapted into the map🗺️, color🎨wheel compass🧭, and other visualizations above. The list also includes citations for three works that predate the French Revolution and three more recent articles published on websites.
In his 1704 book entitled Optiks, Isaac Newton presented the first color🎨wheel and linked its colors🎨to musical notes. On 2025+080, I read The Color of Sound by Clint Goss, which presents a method of connecting musical notes to colors🎨via their frequencies. The note and color🎨pairs in that article are similar to those of the Dechromatic scale.
In 1754, Jean le Rond d’Alembert lauded the benefits of decimalisation. In 1788, Claude Boniface Collignon proposed measuring length in dz or nc and tracking time in deks, dd, md, µd, and nanodays (nd). On 2025+039, I saw the definition of a zem, 1 z = 10-8 c = 40 cm, in a table of ten possible length units from a 2004 arxiv article.
The fundamental properties of Dec dates are defined by algorithms developed by Howard Hinnant and described in his 2021 article entitled chrono-Compatible Low-Level Date Algorithms. On 2024+285, I found a 2014 article which proposed a decimal time system with twenty time zones, each five cd wide, based on Longitude 05 (50 mp).
- Agnoli, Paolo & D’Agostini, Giulio. 2004+330. “Why does the meter beat the second?” . https://arxiv.org/abs/physics/0412078.
- Armstrong, Zan 2023+057. “Text color annotations in markdown.” . https://observablehq.com/@observablehq/text-color-annotations-in-markdown.
- Bostock, Mike 2020+335. “Time Zones.” . https://observablehq.com/@mbostock/time-zones.
- Bostock, Mike 2022+037. “Solar Terminator.” . https://observablehq.com/@d3/solar-terminator.
- Bostock, Mike 2023+314. “Input: Table.” . https://observablehq.com/@observablehq/input-table.
- Clements, John. 2014+091, “Decimal Time Zones.” . https://www.brinckerhoff.org/blog/2014/05/31/decimal-time-zones.
- Clint Goss. 2022+098. “Color of Sound.” . https://www.flutopedia.com/sound_color.htm.
- Collignon, Claude Boniface. 1788. “Découverte d’étalons justes, naturels, invariables et universels.” . https://archive.org/details/dcouvertedtalon00collgoog/page/n68/mode/2up.
- Edwards, Paul. 2022+171. “Compass Rose as legend with colors.” . https://observablehq.com/@pjedwards/compass-rose-as-legend-with-colors.
- Freedman, Dylan. 2017+345. “Sounds.” . https://observablehq.com/@freedmand/sounds.
- Gordon, Marcus A.. 2018+288. “Wavelengths and Spectral Colours.” . https://observablehq.com/@magfoto/wavelengths-and-spectral-colours.
- Harmath, Dénes. 2018+104. “ABC.” . https://observablehq.com/@thsoft/abc.
- Hinnant, Howard. 2021+184. “
chrono-Compatible Low-Level Date Algorithms.” . https://howardhinnant.github.io/date_algorithms.html. - Johnson, Ian 2021+121. “Draggable World Map Coordinates Input.” . https://observablehq.com/@enjalot/draggable-world-map-coordinates-input.
- Lim, Maddie 2018+330. “Enneagram.” . https://observablehq.com/@maddievision/enneagram.
- Newton, Issac. 1704. “Opticks.” . https://doi.org/10.5479/sil.302475.39088000644674.
- Paavanb. 2024+006. “Progressive Color Picker.” . https://observablehq.com/@paavanb/progressive-color-picker.
- Patel, Amit. 2021+290. “Compass Rose.” . https://observablehq.com/@paavanb/progressive-color-picker.
- Pettiross, Jeff 2024+150. “Categorical color scheme test tool.” . https://observablehq.com/@observablehq/categorical-palette-tool
- Rieder, Lukas 2023+032. “Editable table.” . https://observablehq.com/@parlant/editable-table.
- Rivière, Philippe 2022+259. “Add a class to an observable input.” . https://observablehq.com/@recifs/add-a-class-to-an-observable-input--support.
- Rivière, Philippe 2023+330. “D3 Projections.” . https://observablehq.com/@fil/d3-projections.
- Yamahata, Christophe 2021+119. “Great circle: shortest distance between two locations on Earth 🌏.” . https://observablehq.com/@christophe-yamahata/great-circle-shortest-distance-between-two-locations-on-ea.
- d’Alembert, Jean le Rond. 1754. “Decimal.” Encyclopédie, 4, 670. . https://artflsrv04.uchicago.edu/philologic4.7/encyclopedie0922/navigate/4/3458.
tableify = import("https://cdn.skypack.dev/tableify@1.1.1?min")
xss = import("https://cdn.skypack.dev/xss@1.0.14?min")
function createCellDiv(value, max) {
return `<div style="
width: ${Math.abs(value) / max}%;
float: left;
padding: 0px 0px 0px 2px;
text-indent: 2px;
box-sizing: border-box;
overflow: visible;
white-space: nowrap;
display: flex;
justify-content: start;">${Math.round(value)}</div>`
}
liveTable = observeTable(table)
function makeTableEditable(table, options) {
const defaults = {headerEditable: false, appendRows: true};
options = options === undefined ? {} : options;
for (let key in defaults) {
options[key] = options[key] === undefined ? defaults[key] : options[key];
}
return Generators.observe((_notify) => {
const navigate = (event) => {
const cell = event.target;
const row = cell.closest('tr');
const table = row.closest('table');
const isBody = row.parentNode.tagName === 'TBODY';
const isHeader = row.parentNode.tagName === 'THEAD';
const colIndex = cell.cellIndex;
const colCount = row.cells.length;
const rowIndex = row.rowIndex;
const rowCount = table.rows.length;
const headStop = options.headerEditable ? 0 : 1;
let direction = null;
let x = colIndex;
let y = rowIndex;
if (![
// https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes#heading-a-full-list-of-key-event-values
8, 9, 13, 16, 17, 18, 27, 33, 34, 35, 36, 37, 38, 39, 40, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 109, 189
].includes(event.which)) {
event.preventDefault();
}
else {
switch(event.code) {
// Tab cycles through the table, adding new rows as needed.
case 'Tab':
event.preventDefault();
if (event.altKey || event.shiftKey) {
direction = -1;
if (x - 1 < 0) {
if (y - 1 < headStop) break;
x = colCount - 1;
y = y - 1;
} else {
x = x - 1;
}
} else {
direction = 1;
if (x + 1 === colCount) {
x = 0;
y = y + 1;
} else {
x = x + 1;
}
}
break;
// Plain Enter navigates downwards.
// Shift + Enter or Alt + Enter goes up to the cell above.
case 'Enter':
event.preventDefault();
if (event.altKey || event.shiftKey) {
direction = -1;
x = x;
y = y - 1;
}
else {
direction = 1;
x = x;
y = y + 1;
}
break;
// The arrow keys allow you to navigate through cells.
// No new rows are added.
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
case 'Enter':
if (!event.altKey) break;
event.preventDefault();
switch(event.code) {
case 'ArrowUp':
direction = -1;
y = Math.max(y - 1, headStop);
break;
case 'ArrowDown':
direction = 1;
y = Math.min(y + 1, rowCount - 1);
break;
case 'ArrowLeft':
direction = -1;
x = Math.max(x - 1, 0);
break;
case 'ArrowRight':
direction = 1;
x = Math.min(x + 1, colCount - 1);
break;
}
break;
}
if (direction !== null) {
let nextRow;
if (y === rowCount) {
nextRow = options.appendRows ? addRowRelativeTo(row, direction) : row;
} else {
nextRow = table.rows[y];
}
let nextCell = nextRow.cells[x];
focusCell(nextCell);
}
};
}
table.addEventListener("keydown", navigate, false);
if (table.rows.length > 0) {
for (let row of table.rows) {
if (!options.headerEditable && row.rowIndex === 0) continue;
for (let cell of row.cells) {
if (cell.cellIndex === 0) continue;
let cellValue = cell.innerText
cell.innerHTML = `<div style="
width: ${Math.abs(cellValue) / (cell.cellIndex === 2 ? 2.5 : 10)}%;
float: left;
padding: 0px 0px 0px 2px;
text-indent: 2px;
box-sizing: border-box;
overflow: visible;
white-space: nowrap;
display: flex;
justify-content: start;">${cellValue}</div>`
if (cell.cellIndex === 3) continue;
cell.contentEditable = true;
}
}
}
return () => table.removeEventListener("keydown", navigate);
});
}
function observeTable(table) {
return Generators.observe((notify) => {
const keyinput = (event) => notify(parseTableData(table));
table.addEventListener("input", keyinput, false);
notify(parseTableData(table));
return () => window.removeEventListener("input", keyinput);
});
}
function parseTableData(table) {
const header = [];
const data = [];
for (let row of table.rows) {
const rowIndex = row.rowIndex;
const isHeader = row.parentNode.tagName === 'THEAD' && rowIndex === 0;
let obj = {};
for (let cell of row.cells) {
const head = header[cell.cellIndex];
if (isHeader) {
header.push(cell.innerText);
} else {
obj[head] = cell.innerText;
}
}
if (!isHeader) data.push(obj);
}
return JSON.parse(JSON.stringify(data));
}
function focusCell(td) {
const s = window.getSelection();
const r = document.createRange();
let textNode = td.childNodes[0];
const i = td.innerText.length;
td.focus();
if (textNode) {
r.setStart(textNode, i);
r.setEnd(textNode, i);
} else {
r.selectNode(td);
}
s.removeAllRanges();
s.addRange(r);
}
function addRowRelativeTo(tr, direction) {
const newTr = document.createElement('tr');
const insertPosition = direction == 1 ? 'afterend' : 'beforebegin';
tr.insertAdjacentElement(insertPosition, newTr);
for (let _td of Array.from(tr.children)) {
const newTd = document.createElement('td');
newTd.appendChild(document.createTextNode(''));
newTd.contentEditable = true;
newTr.appendChild(newTd);
}
return newTr;
}
// https://observablehq.com/@observablehq/text-color-annotations-in-markdown
rstbtn = d3.create('button').html('Reset').attr("id", "rstbtn").attr("class", "btn btn-quarto");
// https://observablehq.com/@recifs/add-a-class-to-an-observable-input--support
function labelToggle(inputType, inputLabel, inputValue, inputId) {
const input = inputType({label: inputLabel, value: inputValue});
input.setAttribute("id", inputId);
return input;
}
// https://observablehq.com/@observablehq/synchronized-inputs
function set(input, value) {
input.value = value;
input.dispatchEvent(new Event("input", {bubbles: true}));
}
// https://observablehq.com/@observablehq/input-table
// https://stackoverflow.com/a/52079217
// Converts from degrees to radians.
function toRadians(degrees) { return degrees * Math.PI / 180; };
// Converts from radians to degrees.
function toDegrees(radians) { return radians * 180 / Math.PI; }
function coor2bear(strt, dest) {
const [strtLng, strtLat] = strt.map(toRadians);
const [destLng, destLat] = dest.map(toRadians);
return (toDegrees(Math.atan2(
Math.sin(destLng - strtLng) * Math.cos(destLat),
Math.cos(strtLat) * Math.sin(destLat) - Math.sin(strtLat) * Math.cos(destLat) * Math.cos(destLng - strtLng)
)) + 360) % 360;
}
function yiq(color) {
const {r, g, b} = d3.rgb(color);
return (r * 299 + g * 587 + b * 114) / 1000 / 255; // returns values between 0 and 1
}
function textcolor(content, style = {}) {
const {
background,
color = yiq(background) > 0.51 ? "#000" : "white",
padding = "0 2px",
borderRadius = "4px",
fontWeight = 400,
fontFamily = "monospace",
...rest
} = typeof style === "string" ? {background: style} : style;
return htl.html`<span style=${{
background,
color,
padding,
borderRadius,
fontWeight,
fontFamily,
...rest
}}>${content}</span>`;
}
function turn2comp(turn) {
return ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][Math.round(turn / 125) % 8]
}
function dec2rgb(d) {
const color = d3.color(piecewiseColor(d % 1))
return [color.r, color.g, color.b]
}
function dec2hue(d) {
return rgbToHsl(...dec2rgb(d))[0] * 1000
}
piecewiseColor = d3.piecewise(d3.interpolateRgb, [
"#f00", // 0 0 red
"#f50", // 0.25 20 yr
"#f60", // 0.5 24 yr orangered
"#f70", // 0.75 28 yr
"#f90", // 1 36 yr orange
"#fb0", // 1.25 44 yr
"#fc0", // 1.5 48 yr yelloworange
"#fd0", // 1.75 52 yr
"#ff0", // 2 60 yellow
"#ef0", // 2.25 64 gy
"#df0", // 2.5 68 gy limeyellow
"#cf0", // 2.75 72 gy
"#af0", // 3 80 gy lime
"#8f0", // 3.25 88 gy
"#7f0", // 3.5 92 gy greenlime
"#6f0", // 3.75 96 gy
"#0f0", // 4 120 green
"#0f7", // 4.25 148 cg
"#0f9", // 4.5 156 cg cyangreen
"#0fb", // 4.75 164 cg
"#0ff", // 5 180 cyan
"#0cf", // 5.25 192 bc
"#0bf", // 5.5 196 bc azurecyan
"#0af", // 5.75 200 bc
"#08f", // 6 208 bc azure
"#06f", // 6.25 216 bc
"#05f", // 6.5 220 bc blueazure
"#04f", // 6.75 224 bc
"#00f", // 7 240 blue
"#50f", // 7.25 260 mb
"#60f", // 7.5 264 mb purpleblue
"#70f", // 7.75 268 mb
"#90f", // 8 276 mb purple
"#b0f", // 8.25 284 mb
"#c0f", // 8.5 288 mb violetpurple
"#d0f", // 8.75 292 mb
"#f0f", // 9 300 magenta
"#f0a", // 9.25 320 rm
"#f08", // 9.5 328 rm
"#f06", // 9.75 336 rm
"#f00", // 0 0 red
])
hueMtr = Math.round(colorD)
hueDeg = dec2hue(colorD / 1000) * .36
hStr = `hsl(${hueDeg}`
slStr = `, ${colorS / 10}%, ${colorL / 10}%)`
hslStr = hStr + slStr
bkgH = ({background: hStr + ", 100%, 50%)"})
bkgHsl = ({background: hslStr})
rainbowMtr = textcolor(hueMtr, bkgHsl)
rainbowDir = textcolor(turn2comp(hueMtr), bkgHsl)
rainbowDegC = textcolor(Math.round(colorD *.36), bkgHsl)
rainbowDegH = textcolor(Math.round(hueDeg), bkgHsl)
rainbowHex = textcolor(shortenHex(d3.color(hslStr).formatHex()).slice(1), bkgHsl)
rainbowN5zn = textcolor('-5', d3.color(`hsl(180${slStr}`).formatHex())
rainbowP583 = textcolor('5.83̅', d3.color(`hsl(129.88235294117646${slStr}`).formatHex())
// Show preview swatches of color
preview = () => {
const container = DOM.element('div')
d3.select(container).attr('style', 'display: flex;')
d3.select(container)
.append('div')
.text('Selected')
.style('font-weight', 'bold')
.append('div')
.classed('swatch', true)
.style('background-color', `hsl(${dec2hue(colorD / 1000) * .36}, ${colorS / 10}%, ${colorL / 10}%`);
d3.select(container)
.append('div')
.text('Preview')
.style('font-style', 'italic')
.append('div')
.classed('swatch', true)
.style('background-color', `rgb(${hoverRGB[0]}, ${hoverRGB[1]}, ${hoverRGB[2]}`)
d3.select(container).selectAll('div.swatch')
.style('width', '100px')
.style('height', '100px')
.style('margin-right', '8px')
.style('padding', '4px')
return container
}
// The currently hovered color
mutable hoverRGB = [255, 0, 0]
/**
* Draw an interactive color bar
* @param colorFn (t: number) => [number, number, number] Given a position on the bar (between 0 and 1), return its RGB
* @param onSelect (t: number) => void Callback for when a position is selected on the bar
*/
function colorbar({colorFn, onSelect}) {
const WIDTH = 360
const HEIGHT = 32
const container = DOM.element('div')
function handleSelect(coords) {
const t = coords[0] / WIDTH
onSelect(t)
}
let isDragging = false
const canvas = d3.select(container).append('canvas')
.attr('width', WIDTH)
.attr('height', HEIGHT)
.attr('style', 'cursor: crosshair; border: 1px solid black; border-radius: 2px;')
.on('mousedown', function() {
isDragging = true
handleSelect(d3.mouse(this))
})
.on('mouseup', () => { isDragging = false; })
.on('mousemove', function() {
const coords = d3.mouse(this)
if (isDragging) {
handleSelect(coords)
}
mutable hoverRGB = colorFn(coords[0] / WIDTH)
})
const ctx = canvas.node().getContext('2d')
const imgData = ctx.getImageData(0, 0, WIDTH, HEIGHT)
// Possible optimization: cache d3.range so we're not recalculating it a million times
d3.range(WIDTH).forEach(colIdx => {
const t = colIdx / WIDTH
const rgb = colorFn(t)
d3.range(HEIGHT).forEach(rowIdx => {
const screenIdx = rowIdx * WIDTH + colIdx
const imgDataIdx = 4 * screenIdx
imgData.data[imgDataIdx] = rgb[0]
imgData.data[imgDataIdx + 1] = rgb[1]
imgData.data[imgDataIdx + 2] = rgb[2]
imgData.data[imgDataIdx + 3] = 255
})
});
ctx.putImageData(imgData, 0, 0)
return container;
}
initialRGB = [255, 0, 0]
initialHSL = rgbToHsl(...initialRGB)
viewof colorR = Inputs.input(initialRGB[0])
viewof colorG = Inputs.input(initialRGB[1])
viewof colorB = Inputs.input(initialRGB[2])
viewof colorD = Inputs.input(dec2hue(initialHSL[0]))
viewof colorS = Inputs.input(1000)
viewof colorL = Inputs.input(500)
viewof colorA = Inputs.input(1000)
/**
* Update all color values based on current HSL
*/
onUpdateHSL = function(h, s, l) {
const rgb = hslToRgb(h / 1000, s / 1000, l / 1000)
console.log(h)
set(viewof colorR, rgb[0])
set(viewof colorG, rgb[1])
set(viewof colorB, rgb[2])
}
/**
* Credit to github.com/mjackson Source: https://gist.github.com/mjackson/5311256
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSL representation
*/
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [ h, s, l ];
}
/**
* Credit to github.com/mjackson Source: https://gist.github.com/mjackson/5311256
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param {number} h The hue
* @param {number} s The saturation
* @param {number} l The lightness
* @return {Array} The RGB representation
*/
function hslToRgb(h, s, l){
let r, g, b;
if(s == 0){
r = g = b = l; // achromatic
} else {
let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
let p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
/**
* Credit github.com/mjackson. Source: https://gist.github.com/mjackson/5311256
*/
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
// https://observablehq.com/@maddievision/simple-canvas
pixelRatio = window.devicePixelRatio;
createCanvas = (width, height) => {
const canvas = document.createElement('canvas');
canvas.width = width * pixelRatio;
canvas.height = height * pixelRatio;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
return canvas
}
renderWithScale = (context, renderFunction) => {
context.save();
context.scale(pixelRatio, pixelRatio);
renderFunction()
context.restore();
}
quickRender = (width, height, renderer) => {
const canvas = createCanvas(width, height)
const context = canvas.getContext('2d')
renderWithScale(context, () => {
renderer(context)
})
return canvas
}
// http://howardhinnant.github.io/date_algorithms.html#civil_from_days
function unix2dote(unix, zone, offset = 719468) {
return [(unix ?? Date.now()) / 86400000 + (
zone = zone ?? -Math.round(
(new Date).getTimezoneOffset() / 144)
) / 10 + offset, zone]
}
function unix2dote1(unix, zone, offset = 719468) {
return [(unix ?? Date.now()) / 86400000 + (
zone = zone ?? (-Math.round(
(new Date).getTimezoneOffset() / 144) + 10) % 10
) / 10 + offset, zone]
}
octConnections = [
[0, 4],
[1, 5],
[2, 6],
[3, 7],
[4, 0],
[5, 1],
[6, 2],
[7, 3],
]
decConnections = [
[0, 5],
[1, 6],
[2, 7],
[3, 8],
[4, 9],
[5, 0],
[6, 1],
[7, 2],
[8, 3],
[9, 4]
]
hsl8 = [
`hsl(0, ${colorS / 10}%, ${colorL / 10}%)`, // 0
`hsl(44, ${colorS / 10}%, ${colorL / 10}%)`, // 875
`hsl(68, ${colorS / 10}%, ${colorL / 10}%)`, // 750
`hsl(96, ${colorS / 10}%, ${colorL / 10}%)`, // 625
`hsl(180, ${colorS / 10}%, ${colorL / 10}%)`, // 500
`hsl(216, ${colorS / 10}%, ${colorL / 10}%)`, // 375
`hsl(264, ${colorS / 10}%, ${colorL / 10}%)`, // 250
`hsl(292, ${colorS / 10}%, ${colorL / 10}%)`, // 125
]
hsl10 = [
`hsl(0, ${colorS / 10}%, ${colorL / 10}%)`, // red
`hsl(36, ${colorS / 10}%, ${colorL / 10}%)`, // orange
`hsl(60, ${colorS / 10}%, ${colorL / 10}%)`, // yellow
`hsl(80, ${colorS / 10}%, ${colorL / 10}%)`, // lime
`hsl(120, ${colorS / 10}%, ${colorL / 10}%)`, // green
`hsl(180, ${colorS / 10}%, ${colorL / 10}%)`, // cyan
`hsl(208, ${colorS / 10}%, ${colorL / 10}%)`, // azure
`hsl(240, ${colorS / 10}%, ${colorL / 10}%)`, // blue
`hsl(276, ${colorS / 10}%, ${colorL / 10}%)`, // violet
`hsl(300, ${colorS / 10}%, ${colorL / 10}%)`, // magenta
`hsl(0, ${colorS / 10}%, ${colorL / 10}%)`, // red
]
hsla10 = [
`hsla(0, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // red
`hsla(36, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // orange
`hsla(60, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // yellow
`hsla(80, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // lime
`hsla(120, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // green
`hsla(180, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // cyan
`hsla(208, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // azure
`hsla(240, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // blue
`hsla(276, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // violet
`hsla(300, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // magenta
`hsla(0, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // red
]
function dote2date(dote, zone = 0) {
const cote = Math.floor((
dote >= 0 ? dote
: dote - 146096
) / 146097),
dotc = dote - cote * 146097,
yotc = Math.floor((dotc
- Math.floor(dotc / 1460)
+ Math.floor(dotc / 36524)
- Math.floor(dotc / 146096)
) / 365);
return [
yotc + cote * 400,
dotc - (yotc * 365
+ Math.floor(yotc / 4)
- Math.floor(yotc / 100)
), zone]}
sunLonHsl = textcolor(sunLon, `hsl(${d3.hsl(piecewiseColor(sunLon % 1000 / 1000)).h}` + slStr)
sunLatHsl = textcolor(sunLat, `hsl(${d3.hsl(piecewiseColor((sunLat + 1000) % 1000 / 1000)).h}` + slStr)
dz = unix2dote(now)
ydz = dote2date(...dz)
decZone = ydz[2]
decZonePos = (decZone + 10) % 10
decSign = decZone < 0 ? "+" : "–"
ydzP0 = dote2date(...unix2dote(now, 0))
decYearP0 = ydzP0[0]
decYdaP0 = ydzP0[1]
decDateP0 = Math.floor(decYdaP0)
decTimeP0 = ydzP0[1] % 1
decDekP0 = Math.floor(decDateP0 / 10)
decDodP0 = decDateP0 % 10
decYearP0hsl0 = textcolor(decYearP0, `hsl(${d3.hsl(piecewiseColor(decYearP0 % 1000 / 1000)).h}` + slStr)
decYearP0hsl1 = textcolor(decYearP0, `hsl(${d3.hsl(piecewiseColor(decYearP0 % 1000 / 1000)).h}` + slStr)
decDateP0hsl0 = textcolor(decDateP0.toString().padStart(3, "0"), `hsl(${d3.hsl(piecewiseColor(decDateP0 / (365 + isLeapP0))).h}` + slStr)
decDateP0hsl1 = textcolor(decDateP0.toString().padStart(3, "0"), `hsl(${d3.hsl(piecewiseColor(decDateP0 / (365 + isLeapP0))).h}` + slStr)
decYdaP0hsl = textcolor(decYdaP0.toFixed(5).padStart(9, "0"), `hsl(${d3.hsl(piecewiseColor(decYdaP0 / (365 + isLeapP0))).h}` + slStr)
decTimeP0hsl0 = textcolor((decTimeP0 * 10).toFixed(4), `hsl(${d3.hsl(piecewiseColor(decTimeP0)).h}` + slStr)
decTimeP0hsl1 = textcolor((decTimeP0 * 10).toFixed(4), `hsl(${d3.hsl(piecewiseColor(decTimeP0)).h}` + slStr)
decDekP0hsl = textcolor(decDekP0, `hsl(${d3.hsl(piecewiseColor(decDekP0 / 37)).h}` + slStr)
decDodP0hsl = textcolor(decDodP0, `hsl(${d3.hsl(piecewiseColor(decDodP0 / 10)).h}` + slStr)
decLon = longitude % 10
decLonHsl = textcolor(parseFloat(decLon.toFixed(2)), `hsl(${d3.hsl(piecewiseColor(decLon / 10)).h}` + slStr)
decZon = Math.floor(decLon)
decZonHsl = textcolor(decZon, `hsl(${d3.hsl(piecewiseColor(decZon / 10)).h}` + slStr)
parLat = textcolor(parseFloat(latitude.toFixed(3)), `hsl(${d3.hsl(piecewiseColor((latitude + 1) % 1)).h}` + slStr)
parCos = Math.cos(latitude * 2 * Math.PI)
parLen = textcolor(parseFloat(parCos.toFixed(3)), `hsl(${d3.hsl(piecewiseColor(parCos)).h}` + slStr)
conversionFactor = costype === "turns" ? "" : costype === "radians" ? tex`\,\tau\!` : tex`\times360`
zemsLeft = 1000 - 50 * Math.floor(now / 86400000 % 1 * 1000 % 1 * 100 % 21)
zLeft = textcolor(zemsLeft, `hsl(${d3.hsl(piecewiseColor(zemsLeft / 1000)).h}` + slStr)
point0long = long2turn(Place_A[0], 1)
point0zone = Math.floor(point0long)
point0lHsl = textcolor(parseFloat(point0long.toFixed(2)), `hsl(${d3.hsl(piecewiseColor(point0long / 10)).h}` + slStr)
point0zHsl = textcolor(point0zone, `hsl(${d3.hsl(piecewiseColor(point0zone / 10)).h}` + slStr)
isLeapP0 = decYearP0 % 4 == 0 && decYearP0 % 100 != 0 || decYearP0 % 400 == 0;
timezones = FileAttachment("../asset/timezones.json").json()
zones = topojson.feature(timezones, timezones.objects.timezones).features
mesh = topojson.mesh(timezones, timezones.objects.timezones)
color = d3.scaleSequential(d3.interpolateRdBu).domain([-12, 14])
coor = [[[-18, -89.98], [-18, 89.98], [18, 89.98], [18, -89.98], [-18, -89.98], ]]
deczones = [...Array(10).keys()].map(
i => ({
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [coor[0].map(t => [t[0]+36*i, t[1]])]
},
"properties": []
})
)
// https://observablehq.com/@enjalot/draggable-world-map-coordinates-input
// https://observablehq.com/@christophe-yamahata/great-circle-shortest-distance-between-two-locations-on-ea
function worldMapCoordinates(config = {}, dimensions) {
var n_point;
var lonA, lonB, latA, latB;
const {
value = [], title, description, width = dimensions[0]
} = Array.isArray(config) ? {value: config} : config;
const height = dimensions[1];
[lonA, latA] = value[0];
[lonB, latB] = value[1];
lonA = lonA != null ? lonA : 90;
latA = latA != null ? latA : 0.025;
lonB = lonB != null ? lonB : -90;
latB = latB != null ? latB : 36;
const formEl = html`<form style="width: ${width}px;"></form>`;
const context = DOM.context2d(width, height);
const canvas = context.canvas;
const projection = config[2]
.precision(0.1)
.fitSize([width, height], { type: "Sphere" });
const path = d3.geoPath(projection, context).pointRadius(2.5);
formEl.append(canvas);
function fillMesh(f) {
context.beginPath();
path(f);
context.fillStyle = color(f.properties.zone);
context.fill();
context.innerHTML = `<title>${f.properties.places} ${f.properties.time_zone}</title>`;
}
function draw(lon0, lat0, lon1, lat1) {
if (!utctoggle) {
context.beginPath(); path({type: "Sphere"});
context.fillStyle = window.darkmode ? "#007FFF" : mapcolors.ocean;
context.fill();
if (gridtoggle) {
deczones.map((f, i) => {
context.beginPath();
path(f);
context.fillStyle = hsla10[i];
context.fill();
})
}
}
if (utctoggle) {
zones.map(f => fillMesh(f))
}
context.beginPath();
path(land);
if (!utctoggle) {
context.fillStyle = window.darkmode ? "#0808" : mapcolors.land;
context.fill();
}
context.strokeStyle = `#000`;
context.stroke();
if (bordertoggle) {
context.beginPath();
path(borders);
context.lineWidth = 1.25;
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.stroke();
}
if (utctoggle) {
context.beginPath();
path(mesh);
context.lineWidth = 1.25;
context.strokeStyle = `#999`;
context.stroke();
}
if (gridtoggle) {
context.beginPath();
path(graticule);
context.lineWidth = 1.25;
context.strokeStyle = utctoggle || !window.darkmode ? "#000" : "#fff";
context.stroke();
context.fillStyle = "#000";
context.font = width < 760 ? "14px serif" : width < 990 ? "17px serif" : "23px serif";
d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, 54.7])));
d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, -59.7])));
// context.font = width < 760 ? "12px serif" : "21px serif";
// context.fillStyle = `#000`;
// d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, 27.5])));
// d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, -48])));
// d3.range(-18, 336 + 1, 36).map(x => context.fillText(formatLongitude(x), ...projection([x, 90])));
// d3.range(-18, 336 + 1, 36).map(x => context.fillText(formatLongitude(x), ...projection([x, -90])));
}
if (suntoggle) {
context.beginPath();
path(night);
context.fillStyle = "rgba(0,0,255,0.3)";
context.fill();
context.beginPath();
path.pointRadius(width / 84 + 5);
path({type: "Point", coordinates: sun});
context.strokeStyle = "#0009";
context.fillStyle = "#ff0b";
context.lineWidth = 1;
context.stroke();
context.fill();
}
if (lon0 != null && lat0 != null) {
const pointPath = { type: "MultiPoint", coordinates: [[lon0, lat0]], id: "point0test"};
context.beginPath();
path.pointRadius(point_radius_2);
path(pointPath);
context.fillStyle = window.darkmode ? "#A24" : "#FDF";
context.fill();
context.strokeStyle = window.darkmode ? "white" : "black";
context.stroke();
}
if (lon1 != null && lat1 != null) {
const pointPath = { type: "MultiPoint", coordinates: [[lon1, lat1]] };
context.beginPath();
path.pointRadius(point_radius_2);
path(pointPath);
context.fillStyle = window.darkmode ? "#24B" : "#BFF";
context.fill();
context.strokeStyle = window.darkmode ? "white" : "black";
context.stroke();
}
// We draw the path between 2 points
var interpolation = d3.geoInterpolate([lon0,lat0],[lon1,lat1]);
var nb_points = d3.geoDistance([lon0,lat0],[lon1,lat1])*20;
for(let i = 1; i<nb_points; i++) {
const pointPath = { type: "MultiPoint", coordinates: [interpolation(i/nb_points)] };
path.pointRadius(point_radius);
context.beginPath(),
context.fillStyle = window.darkmode ? "#FF420E" : "orange",
path(pointPath),
context.strokeStyle = window.darkmode ? "white" : "black";
context.fill(),
context.stroke();
}
}
draw(lonA, latA, lonB, latB);
canvas.onclick = function(ev) {
const { offsetX, offsetY } = ev;
var coords = projection.invert([offsetX, offsetY]);
if(n_point==0){
lonA = +coords[0].toFixed(2);
latA = +coords[1].toFixed(2);
n_point = 1;
}else{
lonB = +coords[0].toFixed(2);
latB = +coords[1].toFixed(2);
n_point = 0;
}
const point0bear = Math.round(lati2turn(coor2bear([lonA, latA], [lonB, latB])))
set(viewof colorD, point0bear)
table.rows[1].cells[1].innerHTML = createCellDiv(long2turn(lonA), 10)
table.rows[2].cells[1].innerHTML = createCellDiv(long2turn(lonB), 10)
table.rows[1].cells[2].innerHTML = createCellDiv(lati2turn(latA), 2.5)
table.rows[2].cells[2].innerHTML = createCellDiv(lati2turn(latB), 2.5)
table.rows[1].cells[3].innerHTML = createCellDiv(point0bear, 10)
table.rows[2].cells[3].innerHTML = createCellDiv(lati2turn(coor2bear([lonB, latB], [lonA, latA])), 10)
draw(lonA, latA, lonB, latB);
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
};
function resetlatlon() {
lonA = -90;
latA = 0;
lonB = -90;
latB = 36;
set(viewof bordertoggle, false);
set(viewof gridtoggle, false);
set(viewof suntoggle, false);
set(viewof utctoggle, false);
set(viewof yaw, 500);
set(viewof pitch, 0);
set(viewof roll, 0);
set(viewof select, projections.find(t => t.name === "Equirectangular (plate carrée)"));
set(viewof colorD, 0)
set(viewof colorS, 1000)
set(viewof colorL, 500)
table.rows[1].cells[1].innerHTML = createCellDiv(800, 10)
table.rows[2].cells[1].innerHTML = createCellDiv(800, 10)
table.rows[1].cells[2].innerHTML = createCellDiv(0, 2.5)
table.rows[2].cells[2].innerHTML = createCellDiv(100, 2.5)
table.rows[1].cells[3].innerHTML = createCellDiv(0, 10)
table.rows[2].cells[3].innerHTML = createCellDiv(500, 10)
draw(lonA, latA, lonB, latB);
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}
table.onkeyup = function(ev) {
if ([
// https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes#heading-a-full-list-of-key-event-values
9, 13, 27
].includes(ev.which)) {
lonA = turn2long(liveTable[0].Milliparallel);
latA = turn2degr(liveTable[0].Millimeridian);
lonB = turn2long(liveTable[1].Milliparallel);
latB = turn2degr(liveTable[1].Millimeridian);
const point0bear = Math.round(lati2turn(coor2bear([lonA, latA], [lonB, latB])))
set(viewof colorD, point0bear)
table.rows[1].cells[1].innerHTML = createCellDiv(long2turn(lonA), 10)
table.rows[2].cells[1].innerHTML = createCellDiv(long2turn(lonB), 10)
table.rows[1].cells[2].innerHTML = createCellDiv(lati2turn(latA), 2.5)
table.rows[2].cells[2].innerHTML = createCellDiv(lati2turn(latB), 2.5)
table.rows[1].cells[3].innerHTML = createCellDiv(point0bear, 10)
table.rows[2].cells[3].innerHTML = createCellDiv(lati2turn(coor2bear([lonB, latB], [lonA, latA])), 10)
draw(lonA, latA, lonB, latB);
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
} else if ([
// https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes#heading-a-full-list-of-key-event-values
8, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 109, 189
].includes(ev.which)) {
lonA = turn2long(liveTable[0].Milliparallel);
latA = turn2degr(liveTable[0].Millimeridian);
lonB = turn2long(liveTable[1].Milliparallel);
latB = turn2degr(liveTable[1].Millimeridian);
const point0bear = Math.round(lati2turn(coor2bear([lonA, latA], [lonB, latB])))
set(viewof colorD, point0bear)
table.rows[1].cells[3].innerHTML = createCellDiv(point0bear, 10)
table.rows[2].cells[3].innerHTML = createCellDiv(lati2turn(coor2bear([lonB, latB], [lonA, latA])), 10)
draw(lonA, latA, lonB, latB);
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}
}
rstbtn.on('click', resetlatlon);
document.getElementsByClassName("quarto-color-scheme-toggle")[0].onclick = function (e) {
window.quartoToggleColorScheme();
window.darkmode = document.getElementsByTagName("body")[0].className.match(/quarto-dark/) ? true : false;
draw(lonA, latA, lonB, latB);
return false;
};
const form = input({
type: "worldMapCoordinates",
title,
description,
display: v => "",
// html`<div style="width: ${width}px; white-space: nowrap; color: #444; text-align: center; font: 13px sans-serif; margin-bottom: 5px;">
// <span style="color: ${color_A}">Longitude: ${lonA != null ? lonA.toFixed(2) : ""}</span>
//
// <span style="color: ${color_A}">Latitude: ${latA != null ? latA.toFixed(2) : ""} </span>
// </div>
// <div style="width: ${width}px; white-space: nowrap; color: #444; text-align: center; font: 13px sans-serif; margin-bottom: 5px;">
// <span style="color: ${color_B}">Longitude: ${lonB != null ? lonB.toFixed(2) : ""}</span>
//
// <span style="color: ${color_B}">Latitude: ${latB != null ? latB.toFixed(2) : ""}</span>
// </div>`,
getValue: () => [[lonA != null ? lonA : null, latA != null ? latA : null], [lonB != null ? lonB : null, latB != null ? latB : null]],
form: formEl
});
return form;
}
point_radius = width / 900 * mapsize / 100 + 3
point_radius_2 = width / 150 * mapsize / 100 + 3
Place_A = coordinates[0]
Place_B = coordinates[1]
distance_km = (d3.geoDistance(Place_A, Place_B)* 6371).toFixed(0)
distance_mc = distance_km / 40
distance_mcHsl0 = textcolor(parseFloat(distance_mc.toFixed(0)), `hsl(${d3.hsl(piecewiseColor(distance_mc % 1000 / 1000)).h}` + slStr)
distance_mcHsl1 = textcolor(parseFloat(distance_mc.toFixed(0)), `hsl(${d3.hsl(piecewiseColor(distance_mc % 1000 / 1000)).h}` + slStr)
distance_c = distance_mc / 1000
distance_cHsl = textcolor(parseFloat(distance_c.toFixed(3)), `hsl(${d3.hsl(piecewiseColor(distance_c % 1)).h}` + slStr)
velocity_v = travelspeed / 1000
velocity_vHsl0 = textcolor(parseFloat(velocity_v.toFixed(3)), `hsl(${d3.hsl(piecewiseColor(velocity_v)).h}` + slStr)
velocity_vHsl1 = textcolor(parseFloat(velocity_v.toFixed(3)), `hsl(${d3.hsl(piecewiseColor(velocity_v)).h}` + slStr)
velocity_mvHsl = textcolor(parseFloat(travelspeed.toFixed(0)), `hsl(${d3.hsl(piecewiseColor(travelspeed / 1000)).h}` + slStr)
traveltime = Math.round(distance_mc) / Math.round(travelspeed)
traveltimeHsl0 = Number.isFinite(traveltime) ? textcolor(parseFloat(Math.round(traveltime * 1000).toFixed(3)), `hsl(${d3.hsl(piecewiseColor(traveltime % 1)).h}` + slStr) : traveltime
traveltimeHsl1 = Number.isFinite(traveltime) ? textcolor(parseFloat(traveltime.toFixed(3)), `hsl(${d3.hsl(piecewiseColor(traveltime % 1)).h}` + slStr) : traveltime
nb_points = Math.round(distance_km/150)
d3format = require("d3-format@1")
function input(config) {
let {
form,
type = "text",
attributes = {},
action,
getValue,
title,
description,
format,
display,
submit,
options
} = config;
const wrapper = html`<div></div>`;
if (!form)
form = html`<form>
<input name=input type=${type} />
</form>`;
Object.keys(attributes).forEach(key => {
const val = attributes[key];
if (val != null) form.input.setAttribute(key, val);
});
if (submit)
form.append(
html`<input name=submit type=submit style="margin: 0 0.75em" value="${
typeof submit == "string" ? submit : "Submit"
}" />`
);
form.append(
html`<output name=output style="font: 14px Menlo, Consolas, monospace; margin-left: 0.5em;"></output>`
);
if (title)
form.prepend(
html`<div style="font: 700 0.9rem sans-serif; margin-bottom: 3px;">${title}</div>`
);
if (description)
form.append(
html`<div style="font-size: 0.85rem; font-style: italic; margin-top: 3px;">${description}</div>`
);
if (format)
format = typeof format === "function" ? format : d3format.format(format);
if (action) {
action(form);
} else {
const verb = submit
? "onsubmit"
: type == "button"
? "onclick"
: type == "checkbox" || type == "radio"
? "onchange"
: "oninput";
form[verb] = e => {
e && e.preventDefault();
const value = getValue ? getValue(form.input) : form.input.value;
if (form.output) {
const out = display ? display(value) : format ? format(value) : value;
if (out instanceof window.Element) {
while (form.output.hasChildNodes()) {
form.output.removeChild(form.output.lastChild);
}
form.output.append(out);
} else {
form.output.value = out;
}
}
form.value = value;
if (verb !== "oninput")
form.dispatchEvent(new CustomEvent("input", { bubbles: true }));
};
if (verb !== "oninput")
wrapper.oninput = e => e && e.stopPropagation() && e.preventDefault();
if (verb !== "onsubmit") form.onsubmit = e => e && e.preventDefault();
form[verb]();
}
while (form.childNodes.length) {
wrapper.appendChild(form.childNodes[0]);
}
form.append(wrapper);
return form;
}
// https://observablehq.com/@fil/d3-projections
projections = [
{ name: "Airocean", value: d3.geoAirocean },
{ name: "Airy’s minimum error", value: d3.geoAiry },
{ name: "Aitoff", value: d3.geoAitoff },
{ name: "American polyconic", value: d3.geoPolyconic },
{ name: "Armadillo", value: d3.geoArmadillo, options: { clip: { type: "Sphere" } } },
{ name: "August", value: d3.geoAugust },
{ name: "azimuthal equal-area", value: d3.geoAzimuthalEqualArea },
{ name: "azimuthal equidistant", value: d3.geoAzimuthalEquidistant },
{ name: "Baker dinomic", value: d3.geoBaker },
{ name: "Berghaus’ star", value: d3.geoBerghaus, options: { clip: { type: "Sphere" } } },
{ name: "Bertin’s 1953", value: d3.geoBertin1953 },
{ name: "Boggs’ eumorphic", value: d3.geoBoggs },
{ name: "Boggs’ eumorphic (interrupted)", value: d3.geoInterruptedBoggs, options: { clip: { type: "Sphere" } } },
{ name: "Bonne", value: d3.geoBonne },
{ name: "Bottomley", value: d3.geoBottomley },
{ name: "Bromley", value: d3.geoBromley },
{ name: "Butterfly (gnomonic)", value: d3.geoPolyhedralButterfly },
{ name: "Butterfly (Collignon)", value: d3.geoPolyhedralCollignon },
{ name: "Butterfly (Waterman)", value: d3.geoPolyhedralWaterman },
{ name: "Cahill-Keyes", value: d3.geoCahillKeyes },
{ name: "Collignon", value: d3.geoCollignon },
{ name: "conic equal-area", value: d3.geoConicEqualArea },
{ name: "conic equidistant", value: d3.geoConicEquidistant },
{ name: "Craig retroazimuthal", value: d3.geoCraig },
{ name: "Craster parabolic", value: d3.geoCraster },
{ name: "Cox", value: d3.geoCox },
{ name: "cubic", value: d3.geoCubic },
{ name: "cylindrical equal-area", value: d3.geoCylindricalEqualArea },
{ name: "cylindrical stereographic", value: d3.geoCylindricalStereographic },
{ name: "dodecahedral", value: d3.geoDodecahedral },
{ name: "Eckert I", value: d3.geoEckert1 },
{ name: "Eckert II", value: d3.geoEckert2 },
{ name: "Eckert III", value: d3.geoEckert3 },
{ name: "Eckert IV", value: d3.geoEckert4 },
{ name: "Eckert V", value: d3.geoEckert5 },
{ name: "Eckert VI", value: d3.geoEckert6 },
{ name: "Eisenlohr conformal", value: d3.geoEisenlohr },
{ name: "Equal Earth", value: d3.geoEqualEarth },
{ name: "Equirectangular (plate carrée)", value: d3.geoEquirectangular },
{ name: "Fahey pseudocylindrical", value: d3.geoFahey },
{ name: "flat-polar parabolic", value: d3.geoMtFlatPolarParabolic },
{ name: "flat-polar quartic", value: d3.geoMtFlatPolarQuartic },
{ name: "flat-polar sinusoidal", value: d3.geoMtFlatPolarSinusoidal },
{ name: "Foucaut’s stereographic equivalent", value: d3.geoFoucaut },
{ name: "Foucaut’s sinusoidal", value: d3.geoFoucautSinusoidal },
{ name: "general perspective", value: d3.geoSatellite },
{ name: "Gingery", value: d3.geoGingery, options: { clip: { type: "Sphere" } } },
{ name: "Ginzburg V", value: d3.geoGinzburg5 },
{ name: "Ginzburg VI", value: d3.geoGinzburg6 },
{ name: "Ginzburg VIII", value: d3.geoGinzburg8 },
{ name: "Ginzburg IX", value: d3.geoGinzburg9 },
{ name: "Goode’s homolosine", value: d3.geoHomolosine},
{ name: "Goode’s homolosine (interrupted)", value: d3.geoInterruptedHomolosine, options: { clip: { type: "Sphere" } } },
{ name: "gnomonic", value: d3.geoGnomonic },
{ name: "Gringorten square", value: d3.geoGringorten },
{ name: "Gringorten quincuncial", value: d3.geoGringortenQuincuncial },
{ name: "Guyou square", value: d3.geoGuyou },
{ name: "Hammer", value: d3.geoHammer },
{ name: "Hammer retroazimuthal", value: d3.geoHammerRetroazimuthal, options: { clip: { type: "Sphere" } } },
{ name: "HEALPix", value: d3.geoHealpix, options: { clip: { type: "Sphere" } } },
{ name: "Hill eucyclic", value: d3.geoHill },
{ name: "Hufnagel pseudocylindrical", value: d3.geoHufnagel },
{ name: "icosahedral", value: d3.geoIcosahedral },
{ name: "Imago", value: d3.geoImago },
{ name: "Kavrayskiy VII", value: d3.geoKavrayskiy7 },
{ name: "Lagrange conformal", value: d3.geoLagrange },
{ name: "Larrivée", value: d3.geoLarrivee },
{ name: "Laskowski tri-optimal", value: d3.geoLaskowski },
{ name: "Loximuthal", value: d3.geoLoximuthal },
{ name: "Mercator", value: d3.geoMercator },
{ name: "Miller cylindrical", value: d3.geoMiller },
{ name: "Mollweide", value: d3.geoMollweide },
{ name: "Mollweide (Goode’s interrupted)", value: d3.geoInterruptedMollweide, options: { clip: { type: "Sphere" } } },
{ name: "Mollweide (interrupted hemispheres)", value: d3.geoInterruptedMollweideHemispheres, options: { clip: { type: "Sphere" } } },
{ name: "Natural Earth", value: d3.geoNaturalEarth1 },
{ name: "Natural Earth II", value: d3.geoNaturalEarth2 },
{ name: "Nell–Hammer", value: d3.geoNellHammer },
{ name: "Nicolosi globular", value: d3.geoNicolosi },
{ name: "orthographic", value: d3.geoOrthographic },
{ name: "Patterson cylindrical", value: d3.geoPatterson },
{ name: "Peirce quincuncial", value: d3.geoPeirceQuincuncial },
{ name: "rectangular polyconic", value: d3.geoRectangularPolyconic },
{ name: "Robinson", value: d3.geoRobinson },
{ name: "sinusoidal", value: d3.geoSinusoidal },
{ name: "sinusoidal (interrupted)", value: d3.geoInterruptedSinusoidal, options: { clip: { type: "Sphere" } } },
{ name: "sinu-Mollweide", value: d3.geoSinuMollweide },
{ name: "sinu-Mollweide (interrupted)", value: d3.geoInterruptedSinuMollweide, options: { clip: { type: "Sphere" } } },
{ name: "stereographic", value: d3.geoStereographic },
{ name: "Lee’s tetrahedal", value: d3.geoTetrahedralLee },
{ name: "Times", value: d3.geoTimes },
{ name: "Tobler hyperelliptical", value: d3.geoHyperelliptical },
{ name: "transverse Mercator", value: d3.geoTransverseMercator },
{ name: "Van der Grinten", value: d3.geoVanDerGrinten },
{ name: "Van der Grinten II", value: d3.geoVanDerGrinten2 },
{ name: "Van der Grinten III", value: d3.geoVanDerGrinten3 },
{ name: "Van der Grinten IV", value: d3.geoVanDerGrinten4 },
{ name: "Wagner IV", value: d3.geoWagner4 },
{ name: "Wagner VI", value: d3.geoWagner6 },
{ name: "Wagner VII", value: d3.geoWagner7 },
{ name: "Werner", value: d3.geoBonne ? () => d3.geoBonne().parallel(90) : null },
{ name: "Wiechel", value: d3.geoWiechel },
{ name: "Winkel tripel", value: d3.geoWinkel3 }
]
mapcolors = ({
night: "#719fb6",
day: "#ffe438",
grid: "#4b6a79",
ocean: "#adeeff",
land: "#90ff7888",
sun: "#ffb438"
})
function long2turn(degrees = -180, e = 3) {
// turns: e=0, deciturns: e=1, etc.
return (((degrees %= 360) < 0 ? degrees + 360 : degrees) + 18) / (360 / 10**e) % 10**e;
}
function turn2degr(turns = -500, e = 3) {
// turns: e=0, deciturns: e=1, etc.
return turns % 10**e * (360 / 10**e)
}
function turn2long(turns = -500, e = 3) {
// turns: e=0, deciturns: e=1, etc.
return turns % 10**e * (360 / 10**e) - 18
}
function long2zone(degrees = -180) { return Math.floor(long2turn(degrees, 1)); }
function lati2turn(degrees = -180, e = 3) {
// turns: e=0, deciturns: e=1, etc.
return (degrees %= 360) / (360 / 10**e) % 10**e;
}
selectedProjection = select ? select.value() : d3.geoEquirectangular()
projection = {
let proj = selectedProjection;
if (proj.rotate) proj.rotate([-turn2long(yaw), -turn2degr(pitch), turn2degr(roll)]);
return proj;
}
sun = {
const now = new Date;
const day = new Date(+now).setUTCHours(0, 0, 0, 0);
const t = solar.century(now);
const longitude = (day - now) / 864e5 * 360 - 180;
return [longitude - solar.equationOfTime(t) / 4, solar.declination(t)];
}
sunLon = Math.round(long2turn(sun[0]))
sunLat = Math.round(lati2turn(sun[1]))
night = d3.geoCircle().radius(90).center(antipode(sun))()
antipode = ([longitude, latitude]) => [longitude + 180, -latitude]
height = {
const [[x0, y0], [x1, y1]] = d3.geoPath(projection.fitWidth(width, sphere)).bounds(sphere);
const dy = Math.ceil(y1 - y0), l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale(projection.scale() * (l - 1) / l).precision(0.2);
return dy;
}
d3 = require("d3@5", "d3-array@3", "d3-geo@3", "d3-geo-projection@4", "d3-geo-polygon@1.8")
sphere = ({type: "Sphere"})
graticule = d3.geoGraticule().stepMinor([36, 36]).stepMajor([36, 36])()
graticule.coordinates = graticule.coordinates.map(
i => i.map(j => j.map((k, index, arr) => i.length === 3 && index === 0 ? k - 18 : k))
)land = topojson.feature(world, world.objects.land)
world = fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/land-50m.json").then(response => response.json())
topojson = require("topojson-client@3")
solar = require("solar-calculator@0.3/dist/solar-calculator.min.js")
borders = topojson.mesh(countries, countries.objects.countries, (a, b) => a !== b)
countries = fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json").then(response => response.json())
deccolors = [
`hsl(20${slStr}`,
`hsl(24${slStr}`,
`hsl(28${slStr}`,
`hsl(36${slStr}`,
`hsl(44${slStr}`,
`hsl(48${slStr}`,
`hsl(52${slStr}`,
`hsl(60${slStr}`,
`hsl(64${slStr}`,
`hsl(68${slStr}`,
`hsl(72${slStr}`,
`hsl(80${slStr}`,
`hsl(88${slStr}`,
`hsl(92${slStr}`,
`hsl(96${slStr}`,
`hsl(120${slStr}`,
`hsl(148${slStr}`,
`hsl(156${slStr}`,
`hsl(164${slStr}`,
`hsl(180${slStr}`,
`hsl(192${slStr}`,
`hsl(196${slStr}`,
`hsl(200${slStr}`,
`hsl(208${slStr}`,
`hsl(216${slStr}`,
`hsl(220${slStr}`,
`hsl(224${slStr}`,
`hsl(240${slStr}`,
`hsl(260${slStr}`,
`hsl(264${slStr}`,
`hsl(268${slStr}`,
`hsl(276${slStr}`,
`hsl(284${slStr}`,
`hsl(288${slStr}`,
`hsl(292${slStr}`,
`hsl(300${slStr}`,
`hsl(320${slStr}`,
`hsl(328${slStr}`,
`hsl(336${slStr}`,
`hsl(0${slStr}`,
]
viewof size = Inputs.range([50, 700], {
value: 300,
step: 20,
label: 'size'
})
viewof numMajorTicks = Inputs.range([0, 45], {
value: 6,
step: 2,
label: "Major ticks"
})
viewof numMinorTicks = Inputs.range([0, 10], {
value: 2,
step: 1,
label: "Minor ticks"
})
function repeat(component, N, initialAngle=0) {
// NOTE: if component is a function, it will be called with (angle, i)
if (N <= 0) return "";
let result = [];
for (let i = 0; i < N; i++) {
let angle = (360 / N) * i + initialAngle;
let el = typeof component === 'function'? component(angle, i) : component;
result.push(`<g transform="rotate(${angle})">${el}</g>`);
}
return result.join("");
}
function tick(radius, length, color='black') {
return `<path d="M 0,${-radius} l 0,${-length}" fill="none" stroke="${color}" stroke-width="1" />`;
}
function directionMarker(radius, fontSize) { return (angle, _) => {
let label = {0: 'N', 45: 'NE', 90: 'E', 135: 'SE', 180: 'S', 225: 'SW', 270: 'W', 315: 'NW'}[angle] ?? '??';
return `<text y="${-radius-(margin/2)}" font-size="${fontSize}" text-anchor="middle" dy=".36em">${label}</text>`;
};
}
function turnMarker(radius, fontSize) { return (angle, _) => {
let label = {0: '0', 45: '125', 90: '250', 135: '375', 180: '500', 225: '625', 270: '750', 315: '875'}[angle] ?? '??';
return `<text y="${-radius-(margin/2)}" font-size="${fontSize}" text-anchor="middle" dy="-0.36em">${label}</text>`;
};
}
function pie(radius, width, narrowness=1.0, piecolors) {
return (_, i) => `<path id="piepath" d="M 0,0 L ${-width},${-radius} A ${width} ${width/2} 0 0 1 ${width} ${-radius} z" fill="${piecolors[i]}" stroke="black" stroke-width="0.5"/>`;
}
margin = size / 14
padding = 42
radius = size / 2 - margin - padding
window.darkmode = document.getElementsByTagName("body")[0].className.match(/quarto-dark/) ? true : false;function displayPalette(palette, { darkMode = false } = {}) {
return htl.html`
<div style="display: flex; flex-direction: column;">
<div style="margin-bottom:8px;">${cielabScatter(palette, { darkMode: darkMode })}</div>
<div style="display: flex; flex-direction: row; align-items: center; justify-content: space-evenly;">
<div style="">${lightnessSpectrum(palette, { darkMode: darkMode })}</div>
<div>${swatches(palette)}</div>
</div></div>
</div>`
}
paletteDots = function(palette, { darkMode = false } = {}) {
return Plot.plot( {
marks: [
Plot.dot(palette, {
x: (d,i) => i * 36,
r: 16,
fill: { value: (d) => d, label: "Color" },
stroke: darkMode ? "white" : "#202020" ,
tip: {
format: { x: false },
fill: darkMode ? "#202020" : "white"
}
}),
Plot.text(palette.map((d,i) => i), {
x: (d) => d * 36,
dx: 18,
dy: 16,
opacity: 0.8
}),
],
x: { domain: [ 0, 320 ], ticks: 0 },
height: 48,
width: (palette.length) * 40,
marginTop: 16,
style: { fontSize: 18, overflow: "visible" }
})
}
function cielabScatter(palette, { darkMode = false } = {}) {
let labPalette = palette.map((c,i)=>({...d3.lab(c), i, color: c, ...({})}));
return Plot.plot({
width: colorsize,
height: colorsize,
style: { fontSize: 12 },
marks: [
Plot.frame({rx: size / 2, ry: size / 2, opacity: 0.2}),
Plot.gridX({ticks: 3, opacity: 0.2}),
Plot.gridY({ticks: 3, opacity: 0.2}),
Plot.ruleX([0], {opacity: 0.25}),
Plot.ruleY([0], {opacity: 0.25}),
Plot.dot(labPalette, {
x: "a", y: "b", r: 5,
fill: { value: (d) => d.color, label: "Color" },
channels: {
L: { value: "l", label: "L*" }
},
tip: {
format: { fill: (d,i) => `${i}` },
fill: "#202020",
}
}),
],
x: {
domain: [-80, 80],
ticks: 3,
tickSize: 0,
labelArrow: null,
labelAnchor: "center",
label: "a*"
},
y: {
domain: [-80, 80],
ticks: 3,
tickRotate: 0,
tickSize: 0,
labelArrow: null,
labelAnchor: "center",
label: "b*"
}
});
}
function lightnessSpectrum(palette, { darkMode = false } = {}) {
let labPalette = palette.map((c,i)=>({...d3.lab(c), i, color: c, ...({})}));
return Plot.plot({
height: colorsize,
width: 70,
style: { fontSize: 12, overflow: "visible" }, // let the tip overflow the rect of the plot
marks: [
Plot.tickY( labPalette, {
y: (d) => d.l,
stroke: { value: (d) => d.color, label: "Color" },
strokeWidth: 3,
tip: {
anchor: "right",
frameAnchor: "left",
format: {
fontSize: 12,
stroke: (d,i) => `${i}`,
a: true, b: true
},
fill: "#202020"
},
channels: {
a: { value: "a", label: "a*" },
b: { value: "b", label: "b*" }
},
})
],
y: {
domain: [30, 100],
grid: true,
tickSize: 0,
labelAnchor: "center",
label: "L*",
labelArrow: false
},
})
}
function swatches(palette) {
return Plot.plot({
height: colorsize,
width: 50,
x: {ticks: 0},
margin: 0,
marks: [
Plot.barX(
palette, {
y: (d,i) => i,
fill: (d) => d,
inset: -1
}
)
]
})}
colorsize = 210
kilograms = kilograins * 0.064
kgrains = parseFloat(kilograins.toFixed(2))
kgrams = parseFloat(kilograms.toFixed(2))
zem2 = parseFloat((zems**2).toFixed(2))
meter2 = parseFloat(((zems*.4)**2).toFixed(2))
bmi = parseFloat((kilograins / zems**2).toFixed(2))
bmim2 = parseFloat((kilograms / meter2).toFixed(2))
bmiStr = bmi < 46.25 ? "underweight" : bmi < 62.5 ? "normal" : bmi < 75 ? "overweight" : "obese"
// https://observablehq.com/@magfoto/wavelengths-and-spectral-colours
// takes wavelength in nm and returns an rgba value
function wavelengthToColor(wavelength) {
let R,
G,
B,
alpha,
colorSpace,
wl = wavelength,
gamma = 1;
if (wl >= 380 && wl < 440) {
R = -1 * (wl - 440) / (440 - 380);
G = 0;
B = 1;
} else if (wl >= 440 && wl < 490) {
R = 0;
G = (wl - 440) / (490 - 440);
B = 1;
} else if (wl >= 490 && wl < 510) {
R = 0;
G = 1;
B = -1 * (wl - 510) / (510 - 490);
} else if (wl >= 510 && wl < 580) {
R = (wl - 510) / (580 - 510);
G = 1;
B = 0;
} else if (wl >= 580 && wl < 645) {
R = 1;
G = -1 * (wl - 645) / (645 - 580);
B = 0.0;
} else if (wl >= 645 && wl <= 780) {
R = 1;
G = 0;
B = 0;
} else {
R = 0;
G = 0;
B = 0;
}
// intensty is lower at the edges of the visible spectrum.
if (wl > 780 || wl < 380) {
alpha = 0;
} else if (wl > 700) {
alpha = (780 - wl) / (780 - 700);
} else if (wl < 420) {
alpha = (wl - 380) / (420 - 380);
} else {
alpha = 1;
}
colorSpace = ["rgba(" + (R * 100) + "%," + (G * 100) + "%," + (B * 100) + "%, " + alpha + ")", R, G, B, alpha]
// colorSpace is an array with 5 elements.
// The first element is the complete code as a string.
// Use colorSpace[0] as is to display the desired color.
// Use the last four elements alone or together to access each of the individual r, g, and b channels.
return colorSpace;
}
// https://observablehq.com/@freedmand/sounds
function piano(stlibWidth) {
const width = 960;
const keyHeight = 450;
const height = 575;
const whiteKeys = 11;
const blackKeys = [1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1];
const whiteOffsets = blackKeys.reduce((x, y) => x.concat([y + x[x.length - 1] + 1]), [0]);
const svg = html`<svg width="100%" height="auto" viewBox="0 0 ${width} ${height}"
xmlns="http://www.w3.org/2000/svg"></svg>`;
function wrap(elem, note) {
const freq = 440 * Math.pow(2, note / 12);
// Play a note when clicked.
const oscillator = ctx.createOscillator();
const gain = ctx.createGain();
gain.gain.value = 0;
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(freq, ctx.currentTime);
oscillator.connect(gain);
gain.connect(ctx.destination);
oscillator.start();
elem.style.cursor = 'pointer';
elem.onclick = () => {
gain.gain.cancelScheduledValues(ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.1, ctx.currentTime + 0.05);
gain.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.3);
};
return elem;
}
// Draw the white keys.
for (let i = 0; i <= whiteKeys - 1; i++) {
svg.appendChild(wrap(html`<svg><rect x="${width * i / whiteKeys}" y="0" width="${width / whiteKeys}" height="${keyHeight}" fill=${whiteKeyColors[i]} stroke="black" stroke-width="2"/></svg>`, whiteOffsets[i] - 4));
svg.appendChild(html`<svg><text style="user-select: none;" x="${width * (i + 0.5) / whiteKeys}" y="${keyHeight + 42}" font-family="monospace" id="pianotext" font-size="36" text-anchor="middle">${String.fromCharCode('A'.charCodeAt(0) + (i + 5) % 7) + (i < 4 ? "3" : "4")}</text></svg>`);
svg.appendChild(html`<svg><text style="user-select: none;" x="${width * (i + 0.5) / whiteKeys}" y="${keyHeight + 82}" font-family="monospace" id="pianotext" font-size="36" text-anchor="middle">${Math.round(220 * Math.pow(2, (whiteOffsets[i] - 4) / 12) * .864)}</text></svg>`);
svg.appendChild(html`<svg><text style="user-select: none;" x="${width * (i + 0.5) / whiteKeys}" y="${keyHeight + 122}" font-family="monospace" id="pianotext" font-size="36" text-anchor="middle">${Math.round(73504.8 / (220 * Math.pow(2, (whiteOffsets[i] - 4) / 12) * .864))}</text></svg>`);
}
// Draw the black keys.
for (let i = 0; i <= whiteKeys - 2; i++) {
if (blackKeys[i] == 1) {
svg.appendChild(wrap(html`<svg><rect x="${width * ((i + 0.65) / whiteKeys)}" y="0" width="${width / whiteKeys * 0.7}" height="${keyHeight * 0.55}" fill=${blackKeyColors[i]} stroke="black" stroke-width="2"/></svg>`, whiteOffsets[i] - 4 + blackKeys[i]));
}
}
return svg;
}
whiteKeyColors = [
"#fff",
"#fff",
"#fff",
xetHex[1],
xetHex[2],
xetHex[4],
xetHex[6],
xetHex[7],
xetHex[9],
xetHex[11],
"#fff",
"#fff",
"#fff",
]
blackKeyColors = [
"#000",
"#000",
xetHex[0],
"",
xetHex[3],
xetHex[5],
"",
xetHex[8],
xetHex[10],
"#000",
"#000",
"#000",
]
// https://observablehq.com/@freedmand/sounds
ctx = new (window.AudioContext || window.webkitAudioContext)()
function Play(genFn, duration, frequency) {
return new SoundBuffer(genFn, duration, frequency).gui();
}
class SoundBuffer {
constructor(genFn, duration, frequency) {
this.duration = duration;
this.frequency = frequency;
// Create an audio buffer.
this.audioBuffer = ctx.createBuffer(1, ctx.sampleRate * this.duration, ctx.sampleRate);
this.buffer = this.audioBuffer.getChannelData(0);
let max = 0;
for (let i = 0; i < this.audioBuffer.length; i++) {
const value = genFn(i / ctx.sampleRate);
this.buffer[i] = value;
if (Math.abs(value) > max) max = Math.abs(value);
}
for (let i = 0; i < this.audioBuffer.length; i++) {
this.buffer[i] = this.buffer[i] / max;
}
}
play(maxVol = 0.3) {
this.stop();
this.source = ctx.createBufferSource();
this.source.buffer = this.audioBuffer;
const gain = ctx.createGain();
gain.gain.value = maxVol;
this.source.connect(gain);
gain.connect(ctx.destination);
this.source.start();
}
stop() {
if (this.source) this.source.stop();
}
draw(height = 50, width = width, color = 'blue') {
const drawingCtx = DOM.context2d(width, height);
// Draw the middle line.
drawingCtx.strokeStyle = 'gainsboro';
drawingCtx.beginPath();
drawingCtx.moveTo(0, height / 2);
drawingCtx.lineTo(width, height / 2);
drawingCtx.stroke();
// Draw the waveform.
drawingCtx.strokeStyle = color;
drawingCtx.beginPath();
for (let i = 0; i < width; i++) {
const value = this.buffer[Math.floor(i / width * this.audioBuffer.length)];
const y = height - Math.floor((value / 2 + 0.5) * height * 0.9 + height * 0.05);
if (i == 0) {
drawingCtx.moveTo(i, y);
} else {
drawingCtx.lineTo(i, y);
}
}
drawingCtx.stroke();
return drawingCtx.canvas;
}
gui() {
const ui = html`<style>
.sound-player {
border: solid 1px gainsboro;
background: #f5f5f5;
font-family: sans-serif;
color: #6f6f6f;
font-size: 0.8em;
}
.sound-pane {
height: 50px;
background: white;
margin: 8px;
border: solid 1px gainsboro;
position: relative;
}
.icons {
margin: 0 8px 8px 8px;
}
.icons .button {
cursor: pointer;
border: solid 1px transparent;
}
.icons .button:hover {
border: solid 1px gainsboro;
}
.cursor {
background: red;
width: 2px;
height: 100%;
position: absolute;
}
</style>
<div class="sound-player">
<div class="sound-pane">
<span class="cursor" style="display: none;"></span>
</div>
<div class="icons">
<span class="button play-button" style="font-size:18px;">▶</span>
<span class="button stop-button" style="font-size:18px;">◼</span>
<span class="duration">Frequency: ${this.frequency} ib, Duration: ${Math.round(this.duration / .864)} b</span>
</div>
</div>`;
const cursor = ui.querySelector('.cursor');
let interval = null;
const resetInterval = () => {
if (interval != null) {
clearInterval(interval);
interval = null;
}
};
const soundPlayer = ui.querySelector('.sound-player');
ui.querySelector('.sound-pane').appendChild(this.draw(46, width - 20));
ui.querySelector('.play-button').onclick = () => {
cursor.style.left = '0';
this.play();
cursor.style.display = 'block';
const playTime = Date.now();
resetInterval();
interval = setInterval(() => {
if (!document.contains(soundPlayer)) {
resetInterval();
this.stop();
}
let progress = (Date.now() - playTime) / this.duration / 1000;
if (progress < 0) progress = 0;
if (progress > 1) {
progress = 1;
resetInterval();
this.stop();
cursor.style.display = 'none';
}
cursor.style.left = `${Math.floor(progress * (width - 20))}px`;
}, 20);
};
ui.querySelector('.stop-button').onclick = () => {
resetInterval();
this.stop();
cursor.style.display = 'none';
};
return ui;
}
}
function shortenHex(hex) {
if (!/^#([0-9a-f]{3}){1,2}$/i.test(hex)) {
return hex;
}
hex = hex.replace("#", "");
if (hex.length === 6 && hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
return "#" + hex[0] + hex[2] + hex[4];
}
return "#" + hex;
}
piecewiseIob = d3.piecewise(d3.interpolateRound, [
301.734720,
319.675162,
338.684026,
358.823261,
380.160000,
402.765523,
426.715171,
452.088950,
478.971619,
507.452688,
537.627456,
569.596406,
603.466416,
639.351360,
])
piecewiseLen = d3.piecewise(d3.interpolateRound, [
2436.073648,
2299.359125,
2170.306080,
2048.495960,
1933.522727,
1825.002285,
1722.572924,
1625.892425,
1534.637900,
1448.505481,
1367.206961,
1290.471625,
1218.042928,
1149.677698,
])
class minMax {
constructor(limits) {
this.min = Math.min(...limits)
this.max = Math.max(...limits)
}
scale(val) {
return (val - this.min) / (this.max - this.min)
}
}
octave4scaler = new minMax([200, 400])
freqs = [
201.38,
213.36,
226.04,
239.49,
253.73,
268.81,
284.80,
301.73,
319.68,
338.68,
358.82,
380.16,
]
notes = [
"A♯",
"B",
"C",
"C♯",
"D",
"D♯",
"E",
"F",
"F#",
"G",
"G♯",
"A",
]
xet = freqs.map(x => octave4scaler.scale(x))
xetFix = xet.map(x => parseFloat((x * 10).toFixed(2)))
xetCol = xet.map(piecewiseColor)
xetHex = xetCol.map(x => d3.color(x).formatHex())
xetHue = xetCol.map(x => Math.round(d3.hsl(x).h))
xetIob = xet.map(piecewiseIob)
xetLen = xet.map(piecewiseLen)
hues = Object.fromEntries([
0.002,
0.00390625,
0.004,
0.0078125,
0.008,
0.014,
0.015625,
0.0158,
0.016,
0.021,
0.022,
0.024,
0.030,
0.03125,
0.040,
0.039,
0.0625,
0.065,
0.067,
0.130,
0.185,
1/3,
0.40069,
0.41302,
0.413,
0.4132,
0.420,
0.450,
0.460,
0.480,
0.490,
0.49008,
0.599,
0.704,
0.754,
0.788,
0.815,
0.864,
0.935,
0.960,
].map(i => [i, d3.hsl(piecewiseColor(i)).h])
);
h26div300 = d3.hsl(piecewiseColor(26 / 300)).h
hD039 = d3.hsl(piecewiseColor(39 / 365)).h
hD080 = d3.hsl(piecewiseColor(80 / 365)).h
hD285 = d3.hsl(piecewiseColor(285 / 365)).h
fMile = 1.6 / 1.609344
fInch = 25 / 25.4
fMiPH = 5 / 3 / 1.609344
fDrop = 64 / 51
hIob = d3.hsl(piecewiseColor(1 / .864 % 1)).h
hMile = d3.hsl(piecewiseColor(fMile % 1)).h
hMiPH = d3.hsl(piecewiseColor(fMiPH % 1)).h
hDrop = d3.hsl(piecewiseColor(fDrop % 1)).h
hInch = d3.hsl(piecewiseColor(fInch % 1)).h
hMass = d3.hsl(piecewiseColor(448 / 453.59237 % 1)).h
hPint = d3.hsl(piecewiseColor(4 / 3.785411784 % 1)).h
hFlOz = d3.hsl(piecewiseColor(32 / 29.5735296875 % 1)).h
hAcre = d3.hsl(piecewiseColor(247.1053814672 / 250 % 1)).h
hSqMi = d3.hsl(piecewiseColor(fMile**2 % 1)).h
hSqIn = d3.hsl(piecewiseColor(fInch**2 % 1)).h
bcHue = (xetHue[1] + xetHue[2]) / 2
ddsHue = (xetHue[4] + xetHue[5]) / 2
dseHue = (xetHue[5] + xetHue[6]) / 2html`
<style>
.colorAs {
background: hsl(${xetHue[0]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[0]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorAs3 {
background: hsl(${hues[0.40069]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.40069]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0158 {
background: hsl(${hues[0.0158]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.0158]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorC4 {
background: hsl(${hues[0.41302]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.41302]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorA4 {
background: hsl(${hues[0.49008]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.49008]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorB {
background: hsl(${xetHue[1]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[1]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorBc {
background: hsl(${bcHue} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${bcHue}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorC {
background: hsl(${xetHue[2]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[2]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorCs {
background: hsl(${xetHue[3]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[3]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD {
background: hsl(${xetHue[4]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[4]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorDs {
background: hsl(${xetHue[5]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[5]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorDds {
background: hsl(${ddsHue} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${ddsHue}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorDsE {
background: hsl(${dseHue} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${dseHue}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorE {
background: hsl(${xetHue[6]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[6]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorF {
background: hsl(${xetHue[7]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[7]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorFs {
background: hsl(${xetHue[8]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[8]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorG {
background: hsl(${xetHue[9]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[9]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorGs {
background: hsl(${xetHue[10]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[10]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorA {
background: hsl(${xetHue[11]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[11]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0 {
background: hsl(0 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(0, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color002 {
background: hsl(${hues[.002]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.002]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color004 {
background: hsl(${hues[.004]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.004]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color00390625 {
background: hsl(${hues[.00390625]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.00390625]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0078125 {
background: hsl(${hues[.0078125]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.0078125]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color008 {
background: hsl(${hues[.008]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.008]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color014 {
background: hsl(${hues[.014]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.014]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color015625 {
background: hsl(${hues[0.015625]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.015625]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color016 {
background: hsl(${hues[0.016]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.016]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color021 {
background: hsl(${hues[0.021]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.021]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color022 {
background: hsl(${hues[0.022]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.022]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color024 {
background: hsl(${hues[0.024]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.024]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color025 {
background: hsl(20 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(20, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color030 {
background: hsl(${hues[0.030]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.030]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color03125 {
background: hsl(${hues[0.03125]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.03125]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color039 {
background: hsl(${hues[0.039]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.039]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color040 {
background: hsl(${hues[0.040]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.040]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color050 {
background: hsl(24 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(24, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0625 {
background: hsl(${hues[0.0625]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.0625]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color065 {
background: hsl(${hues[0.065]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.065]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color067 {
background: hsl(${hues[0.067]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.067]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color26div300 {
background: hsl(${h26div300} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${h26div300}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color1 {
background: hsl(36 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(36, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color125 {
background: hsl(44 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(44, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color130 {
background: hsl(${hues[0.130]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.130]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color185 {
background: hsl(${hues[0.185]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.185]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color2 {
background: hsl(60 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(60, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color250 {
background: hsl(68 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(68, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color3 {
background: hsl(80 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(80, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorThird {
background: hsl(${hues[1/3]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[1/3]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color375 {
background: hsl(96 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(96, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color4 {
background: hsl(120 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(120, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color413 {
background: hsl(${hues[0.413]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.413]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color4132 {
background: hsl(${hues[0.4132]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.4132]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color420 {
background: hsl(${hues[0.420]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.420]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color450 {
background: hsl(${hues[0.450]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.450]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color460 {
background: hsl(${hues[0.460]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.460]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color480 {
background: hsl(${hues[0.480]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.480]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color490 {
background: hsl(${hues[0.490]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.490]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color5 {
background: hsl(180 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(180, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color599 {
background: hsl(${hues[0.599]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.599]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color6 {
background: hsl(208 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(208, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color625 {
background: hsl(216 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(216, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color7 {
background: hsl(240 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(240, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color704 {
background: hsl(${hues[0.704]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.704]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color750 {
background: hsl(264 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(264, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color754 {
background: hsl(${hues[0.754]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.754]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color788 {
background: hsl(${hues[0.788]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.788]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color8 {
background: hsl(276 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(276, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color815 {
background: hsl(${hues[0.815]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.815]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color864 {
background: hsl(${hues[0.864]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.864]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color875 {
background: hsl(292 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(292, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color9 {
background: hsl(300 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(300, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color935 {
background: hsl(${hues[0.935]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.935]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color950 {
background: hsl(328 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(328, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color960 {
background: hsl(${hues[0.960]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.960]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD039 {
background: hsl(${hD039} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hD039}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD080 {
background: hsl(${hD080} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hD080}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD285 {
background: hsl(${hD285} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hD285}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorff6300 {
background: #ff6300;
color: black;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorffec00 {
background: #ffec00;
color: black;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color99ff00 {
background: #99ff00;
color: black;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color28ff00 {
background: #28ff00;
color: black;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color00ffe8 {
background: #00ffe8;
color: black;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color007cff {
background: #007cff;
color: white;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0800ff {
background: #0800ff;
color: white;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color5e00d6 {
background: #5e00d6;
color: white;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorIob {
background: hsl(${hIob} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hIob}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorMile {
background: hsl(${hMile} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hMile}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorInch {
background: hsl(${hInch} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hInch}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorDrop {
background: hsl(${hDrop} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hDrop}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorMass {
background: hsl(${hMass} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hMass}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorMiPH {
background: hsl(${hMiPH} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hMiPH}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorPint {
background: hsl(${hPint} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hPint}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorFlOz {
background: hsl(${hFlOz} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hFlOz}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorAcre {
background: hsl(${hAcre} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hAcre}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorSqMi {
background: hsl(${hSqMi} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hSqMi}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorSqIn {
background: hsl(${hSqIn} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hSqIn}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
</style>
`Reuse
Citation
@online{laptev2026,
author = {Laptev, Martin},
title = {Dec},
date = {2026},
urldate = {2026},
url = {https://maptv.github.io/dec},
langid = {en}
}