smartdown-gallery
Version:
Example Smartdown documents and associated resources that demonstrate various Smartdown features and serve as raw material for other Smartdown demos.
305 lines (230 loc) • 11.6 kB
Markdown
## Visualizing and Analyzing Astronomy Data
*WIP - Work in Progress - This page is not yet complete. Come back later for a complete version.*
Smartdown can be used to discuss and visualize a variety of scientific data, and the recent availability of data from the [James Webb Space Telescope - JWST](https://webb.nasa.gov) has enabled at least one Smartdown author (see [Credits](##credits) below) to create some compelling explorables. This Gallery example demonstrates a few possible ways to load, manipulate, and visualize digital telescope data.
- [Using HTML Canvas in Smartdown](##basic-canvas-usage)
- [Loading FITS Data](##loading-fits-data)
### Basic Canvas Usage
The playable below will create an HTML `<canvas>` element and will ensure its width is 75% of the screen width, and its height is the same as the width, but no more than 50% of the screen height. The playable will be responsive to `resize` events and will adjust accordingly. The content of the canvas will be a simple pattern where each pixel is assigned a color based upon its x/y coordinate.
```javascript /autoplay/playable
const canvasId = 'basicCanvas'; // Ensure each canvas on this page has distinct id
this.div.innerHTML = `
<canvas id="${canvasId}" style="margin:auto; display:block;"></canvas>
`;
let canvas = document.getElementById(canvasId);
let context = canvas.getContext('2d');
function sizeCanvas() {
canvas.width = Math.floor(window.innerWidth / 2);
canvas.height = Math.min(canvas.width, Math.floor(window.innerHeight * 0.5));
}
sizeCanvas();
window.addEventListener('resize', function(event){
sizeCanvas();
draw();
});
function draw() {
const imagedata = context.createImageData(canvas.width, canvas.height);
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
let pixelindex = (y * canvas.width + x) * 4;
const xcolor = Math.floor(x / canvas.width * 255);
const ycolor = Math.floor(y / canvas.height * 255);
imagedata.data[pixelindex + 0] = xcolor;
imagedata.data[pixelindex + 1] = ycolor;
imagedata.data[pixelindex + 2] = Math.floor((xcolor + ycolor) / 2);
imagedata.data[pixelindex + 3] = 255;
}
}
context.putImageData(imagedata, 0, 0);
}
draw();
```
### Rendering image data
For this next example, we'll create a synthetic 2D data matrix corresponding to an image that might be obtained from a space telescope. Typically, this image data is distributed in [FITS](https://en.wikipedia.org/wiki/FITS) file format, which contains a 2D matrix of data, as well as a metadata header. The metadata header describes the dimensions of the data; the scaling of the measurements; and a wealth of information about the image's location in the sky, the time it was obtained, and the astronomical entity being observed. Each element of the 2D image matrix will be a floating point value corresponding to a measured pixel obtained via a telescope.
#### Generating a synthetic image
We are going to generate a simple `9 x 9` grid containing a radial gradient image of a circle and have its pixel measurements (intensity) be such that the intensity is highest at the center, and fades to black at the perimeter.
For the example below, we'll create a simplistic model where the value `0.0` indicates a recorded pixel of complete darkness, and a value of `1.0` indicates a pixel of maximum brightness.
```javascript /playable/autoplay
//
// The code below inspired by a StackOverflow post:
// How do I make a 'radial gradient' of values in a 2D array?
// https://stackoverflow.com/a/53262255/5667222
//
const gridSize = 9;
window.gridSize = gridSize; // For use in subsequent playables.
const centrePoint = {
x: Math.floor(gridSize / 2),
y: Math.floor(gridSize / 2)
};
const euclideanDistance = (point1, point2) => {
return Math.sqrt(
Math.abs(Math.pow(point1.x - point2.x, 2)) +
Math.abs(Math.pow(point1.y - point2.y, 2))
)
}
const furthestDistanceFromCentre = euclideanDistance(
{
x: 0,
y: 0
},
centrePoint
)
const grid = [];
for (let x = 0; x < gridSize; x++) {
grid[x] = [];
for (let y = 0; y < gridSize; y++) {
grid[x][y] = Math.floor(
furthestDistanceFromCentre - euclideanDistance(
{x, y},
centrePoint
)
);
}
}
function stringifyGrid(grid) {
return grid.map((row) => {
return row.map((col) => col.toFixed(2)).join(', ');
});
}
const euclideanRadialImageData = grid;
const euclideanRadialImageDataStr = stringifyGrid(euclideanRadialImageData);
const radialImageData = euclideanRadialImageData.map((row) => {
return row.map((col) => col / Math.floor(furthestDistanceFromCentre));
});
const radialImageDataStr = stringifyGrid(radialImageData);
smartdown.set('radialImageData', radialImageData);
smartdown.set('radialImageDataStr', radialImageDataStr);
smartdown.set('euclideanRadialImageDataStr', euclideanRadialImageDataStr);
```
And here is the image data:
> [](:!radialImageDataStr)
#### Mapping image data pixel values to Canvas pixel colors
When using actual FITS image data from a space telescope, the range of values recorded in the 2D matrix of pixels is based upon the telescope's characteristics and which (if any) *filters* are being applied. In order to visualize this data using a `<canvas>`, we will need to *scale* the pixel measurement (range `0.0 ... 1.0`) to fully use the canvas elements color scale of `0 ... 255`). This *mapping* function can be a simple linear function, which we'll use below; but it can be a more complex non-linear function that can be used to emphasize or contrast different parts of the image. See [Stretch Functions](##stretch-functions) later in this document.
In the playable below, we will use a canvas element, but will dimension it as a `9x9` pixel canvas, even though the actual visible width and height will be the same as the [Basic Canvas Usage](##Basic Canvas Usage) element above, scaling with the window width. By default, canvas elements will *smooth* adjacent pixels, but we can disable this behavior so that the `9x9` canvas pixels are rendered without smoothing. This is done by setting the canvas's CSS property [image-rendering](https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering) to `crisp-edges`.
```javascript /autoplay/playable
const canvasId = 'syntheticCanvas'; // Ensure each canvas on this page has distinct id
this.div.innerHTML = `
<canvas id="${canvasId}" style="image-rendering: crisp-edges; margin:auto; display:block;"></canvas>
`;
let canvas = document.getElementById(canvasId);
let context = canvas.getContext('2d');
function sizeCanvas() {
const domWidth = Math.floor(window.innerWidth / 2);
const domHeight = Math.min(domWidth, Math.floor(window.innerHeight * 0.5));
canvas.style.width = `${domWidth}px`;
canvas.style.height = `${domHeight}px`;
canvas.width = window.gridSize;
canvas.height = window.gridSize;
}
sizeCanvas();
window.addEventListener('resize', function(event){
sizeCanvas();
draw();
});
function draw() {
const imagedata = context.createImageData(canvas.width, canvas.height);
const sourceData = env.radialImageData;
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
let pixelindex = (y * canvas.width + x) * 4;
const xcolor = Math.floor(x / canvas.width * 255);
const ycolor = Math.floor(y / canvas.height * 255);
imagedata.data[pixelindex + 0] = Math.floor(255 * sourceData[y][x]);
imagedata.data[pixelindex + 1] = Math.floor(255 * sourceData[y][x]);
imagedata.data[pixelindex + 2] = Math.floor(255 * sourceData[y][x]);
imagedata.data[pixelindex + 3] = 255;
}
}
context.putImageData(imagedata, 0, 0);
}
draw();
```
#### Adding Interactivity
In the example above, we used a simple grayscale color mapping. Let's make things more fun by adding a simple control panel that lets us adjust the background color.
[Adjust Background Color :gear:](:=backgroundColorSettings=true)
```javascript /autoplay/playable
const canvasId = 'backgroundColorCanvas'; // Ensure each canvas on this page has distinct id
this.div.innerHTML = `
<canvas id="${canvasId}" style="image-rendering: crisp-edges; margin:auto; display:block;"></canvas>
`;
let canvas = document.getElementById(canvasId);
let context = canvas.getContext('2d');
function sizeCanvas() {
const domWidth = Math.floor(window.innerWidth / 2);
const domHeight = Math.min(domWidth, Math.floor(window.innerHeight * 0.5));
canvas.style.width = `${domWidth}px`;
canvas.style.height = `${domHeight}px`;
canvas.width = window.gridSize;
canvas.height = window.gridSize;
}
sizeCanvas();
window.addEventListener('resize', function(event){
sizeCanvas();
draw();
});
function draw() {
const imagedata = context.createImageData(canvas.width, canvas.height);
const sourceData = env.radialImageData;
const red = env.backgroundColorSettingsColorRed;
const green = env.backgroundColorSettingsColorGreen;
const blue = env.backgroundColorSettingsColorBlue;
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
let pixelindex = (y * canvas.width + x) * 4;
const xcolor = Math.floor(x / canvas.width * 255);
const ycolor = Math.floor(y / canvas.height * 255);
imagedata.data[pixelindex + 0] = Math.floor(red * sourceData[y][x]);
imagedata.data[pixelindex + 1] = Math.floor(green * sourceData[y][x]);
imagedata.data[pixelindex + 2] = Math.floor(blue * sourceData[y][x]);
imagedata.data[pixelindex + 3] = 255;
}
}
context.putImageData(imagedata, 0, 0);
}
smartdown.set({
backgroundColorSettings: false,
backgroundColorSettingsColorRed: 127,
backgroundColorSettingsColorGreen: 127,
backgroundColorSettingsColorBlue: 127
});
function buildColor(r, g, b) {
const rPadded = r.toString(16).padStart(2, '0');
const gPadded = g.toString(16).padStart(2, '0');
const bPadded = b.toString(16).padStart(2, '0');
return `#${rPadded}${gPadded}${bPadded}`;
}
const colorComponentChanged = () => {
const colorHex = buildColor(
env.backgroundColorSettingsColorRed,
env.backgroundColorSettingsColorGreen,
env.backgroundColorSettingsColorBlue);
const colorSwatchMarkdown = ``;
smartdown.set('backgroundColorSettingsSwatch', colorSwatchMarkdown);
draw();
};
this.dependOn.backgroundColorSettingsColorRed = colorComponentChanged;
this.dependOn.backgroundColorSettingsColorGreen = colorComponentChanged;
this.dependOn.backgroundColorSettingsColorBlue = colorComponentChanged;
this.dependOn.backgroundColorSettings = () => {
if (env.backgroundColorSettings) {
smartdown.showDisclosure('backgroundColorSettings', '', 'upperright,closeable,draggable');
}
};
draw();
```
# :::: backgroundColorSettings
# --outlinebox p
|||
|:---|:---|
|Red|[](:-backgroundColorSettingsColorRed/0/255/1)|
|Green|[](:-backgroundColorSettingsColorGreen/0/255/1)|
|Blue|[](:-backgroundColorSettingsColorBlue/0/255/1)|
[](:!backgroundColorSettingsSwatch|markdown)
# --outlinebox
# ::::
#### Credits
This Smartdown Gallery example is inspired by the work of [Isidore Mones](https://izzymones.github.io/blog-kit/), who utilizes publicly available [FITS]() files from the JWST to create interactive images of various cosmic objects:
- [Phantom Galaxy](https://izzymones.github.io/blog-kit/posts/phantomgalaxy/)
- [Southern Ring Nebula](https://izzymones.github.io/blog-kit/posts/southernringnebula/)
- [Cosmic Cliffs](https://izzymones.github.io/blog-kit/posts/cosmic_cliffs/)
---
[Back to Home](:)