Published
Edited
Mar 14, 2021
Importers
3 stars
Insert cell
Insert cell
Curve.hcurve(5)
.setFillColor(d3.interpolateInferno)
.setStrokeColor(() => "#8884")
.skewHorizontal(2, 0)
.skewVertical(1, 0)
.cclockwise()
.draw(1, 256)
Insert cell
// Added a Hilbert curve too this time
Curve.hilbert(5)
.skewHorizontal(4, 3)
.skewHorizontal(-4, 1)
.translate(1, 0)
.draw(1, 256)
Insert cell
Curve.hcurve(4)
.skewHorizontal(-2, 16)
.translate(8, 0)
.draw(2, 32)
Insert cell
// Turns out that repeated skewing by 45 degrees is equivalent to a rotation
Curve.hcurve(4)
.skewHorizontal(1,1)
.skewVertical(1,1)
.skewHorizontal(1,0)
.draw(2,32)
Insert cell
Curve.hcurve(4)
.clockwise()
.draw(2,32)
Insert cell
// This breaks because geometry - the wrap-around doesn't align properly
Curve.hicurve(4)
.skewHorizontal(2, 0)
.skewVertical(2, 0)
.draw(2, 256)
Insert cell
// Small-scale version. This probably can be fixed somehow, but it
// needs someone a bit smarter than me to figure out.
Curve.hicurve(0)
.skewHorizontal(2, 0)
.skewVertical(2,0)
.draw(5, 1)
Insert cell
// The drawing code assumes everything tiles perfectly
{
const {T} = directions
return new Curve([T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T])
.skewHorizontal(1, 0)
.skewVertical(2, 1)
.skewVertical(4, 3)
.draw(4, 1)
}
Insert cell
// The line drawing code does not, btw.
{
const {T} = directions
return new Curve([T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T])
.skewHorizontal(1, 0)
.skewVertical(2, 1)
.skewVertical(4, 3)
.line(4, 1)
}
Insert cell
Insert cell
Insert cell
Insert cell
hilbertindices = function(x, y, size) {
let d = 0;
for (let s = size >> 1; s > 0; s >>= 1) {
var rx = +((x & s) > 0);
var ry = +((y & s) > 0);
d += s * s * ((3 * rx) ^ ry);

if (ry === 0) {
if (rx === 1) {
x = s - 1 - x;
y = s - 1 - y;
}
var t = x;
x = y;
y = t;
}
}
return d;
}
Insert cell
{
let grid = [];
const size = 4
for(let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
grid.push(hilbertindices(x, y, size*size));
}
}
return grid;
}
Insert cell
class Curve {

constructor(grid, width, height){
this.grid = grid;
if (width === undefined || height === undefined) {
const size = Math.sqrt(grid.length);
width = size;
height = size;
}
this.width = width;
this.height = height;
this.fill = d3.interpolateSpectral;
this.stroke = (i) => d3.interpolateSpectral(1-i);
}

static hcurve(k){
return new this(hcurve(k));
}

static hicurve(k) {
return new this(hicurve(k));
}
static hilbert(k) {
const side = 4 * 2 ** k;
const size = side*side;
const indices = [], grid = [];
for (let x = 0; x < side; x++) {
for(let y = 0; y < side; y++) {
grid.push(hilbertindices(x, y, size));
indices.push(0);
}
}
for(let i = 0; i < grid.length; i++) {
const idx = grid[i];
indices[idx] = i;
grid[i] = 0;
}
const {T, R, B, L} = directions;
for(let i = 0, idx = 0, nidx = 0, x = 0, y = 0; i < grid.length-1; i++) {
nidx = indices[i+1];
switch(nidx - idx) {
case 1:
grid[idx] = R;
break;
case -1:
grid[idx] = L;
break;
case side:
grid[idx] = B;
break
case -side:
grid[idx] = T;
break;
}
idx = nidx;
}
if (grid[side-1] === 0) grid[side-1] = R;
else grid[grid.length-side] = B;
return new this(grid);
}

setFillColor(lerpColor){
this.fill = lerpColor;
return this;
}

setStrokeColor(lerpColor){
this.stroke = lerpColor;
return this;
}

skewHorizontal(step, offset) {
const {grid, width, height} = this;
const {T, TR, R, RB, B, BL, L, LT} = directions;
const newGrid = grid.slice();
if (step > 0) {
for (let y = 0; y < height; y++) {
const yoffset = ((y+offset+height)%height) / step | 0;
const nyoffset = ((y+1+offset+height)%height) / step | 0;
const pyoffset = ((y-1+offset+height)%height) / step | 0;
const line = y*width;
for (let x = 0; x < width; x++) {
let v = grid[x + y*width];
if (yoffset !== nyoffset){
if(v === B) { v = BL; } else if (v === RB) { v = B; }
}
if (yoffset !== pyoffset) {
if (v === T) { v = TR; } else if (v === LT) { v = T; }
}
newGrid[((x - yoffset + width)%width) + line] = v;
}
}
} else {
step = -step;
for (let y = 0; y < height; y++) {
const yoffset = ((y+offset+height)%height) / step | 0;
const nyoffset = ((y+1+offset+height)%height) / step | 0;
const pyoffset = ((y-1+offset+height)%height) / step | 0;
const line = y*width;
for (let x = 0; x < width; x++) {
let v = grid[x + y*width];
if (yoffset !== nyoffset) {
if(v === B) { v = RB } else if (v === BL) { v = B }
}
if (yoffset !== pyoffset) {
if(v === T) { v = LT } else if (v === TR) { v = T; }
}
newGrid[((x + yoffset + width)%width) + line] = v;
}
}
}
this.grid = newGrid;

return this;
}

skewVertical(step, offset) {
const {grid, width, height} = this;
const {T, TR, R, RB, B, BL, L, LT} = directions;
const newGrid = grid.slice();
if (step > 0) {
for (let x = 0; x < width; x++) {
const xoffset = ((x+width+offset)%width) / step | 0;
const nxoffset = ((x+1+width+offset)%width) / step | 0;
const pxoffset = ((x-1+width+offset)%width) / step | 0;
for (let y = 0; y < height; y++) {
let v = grid[x + y*width];
if (xoffset !== nxoffset){
if (v === R) { v = RB; } else if (v === TR) { v = R; }
}
if (xoffset !== pxoffset) {
if (v === L) { v = LT; } else if (v === BL) { v = L; }
}
newGrid[x + ((y+xoffset)%height)*width] = v;
}
}
} else if (step < 0) {
step = -step;
for (let x = 0; x < width; x++) {
const xoffset = ((x+width+offset)%width) / step | 0;
const nxoffset = ((x+1+width+offset)%width) / step | 0;
const pxoffset = ((x-1+width+offset)%width) / step | 0;
for (let y = 0; y < height; y++) {
let v = grid[x + y*width];
if (xoffset !== nxoffset) {
if (v === RB) { v = R; } else if (v === R) { v = TR; }
}
if (xoffset !== pxoffset) {
if (v === LT) { v = L; } else if (v === L) { v = BL; }
}
newGrid[x + ((y-xoffset+width)%height)*width] = v;
}
}
}
this.grid = newGrid;
return this;
}

translate(dx, dy){
const {grid, width, height} = this;
const newGrid = grid.slice();
while(dy < 0) dy += height;
dy = dy % height;
while (dx < 0) dx += width;
dx = dx % width;
for (let y = 0; y < height; y++) {
const line = ((y + dy + height)%height)*width;
for (let x = 0; x < width; x++) {
let v = grid[x + y*width];
newGrid[((x + dx + width)%width) + line] = v;
}
}
this.grid = newGrid;
return this;
}

clockwise() {
const {grid, width, height} = this;
const newGrid = grid.slice();
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const x1 = height - 1 - y;
const y1 = x * width;
let v = grid[x + y*width];
newGrid[x1 + y1] = (v >>> 6) + (v << 2) & 0xFF;
}
}
this.grid = newGrid;
this.width = height;
this.height = width;
return this;
}

cclockwise() {
const {grid, width, height} = this;
const newGrid = grid.slice();
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const x1 = height - 1 - y;
const y1 = x * width;
let v = grid[x1 + y1];
newGrid[x + y*width] = (v << 6) + (v >>> 2) & 0xFF;
}
}
this.grid = newGrid;
this.width = height;
this.height = width;
return this;
}

toIndices(){
const {grid, width, height} = this;
const gridToLine = [];
const lineToGrid = [];
// initiate a numerical array without holes.
for(let i = 0; i < grid.length; i++) {
gridToLine.push(0);
lineToGrid.push(0);
}
const {T, TR, R, RB, B, BL, L, LT} = directions;
for(let i = 0, x = 0, y = 0, idx = 0; i < grid.length; i++) {
gridToLine[idx] = i;
lineToGrid[i] = idx;
const direction = grid[idx];
switch (direction) {
case T:
y -= 1;
break;
case TR:
y -= 1;
x += 1;
break;
case R:
x += 1;
break
case RB:
x += 1;
y += 1;
break;
case B:
y += 1;
break;
case BL:
y += 1;
x -= 1;
break;
case L:
x -= 1;
break;
case LT:
x -= 1;
y -= 1;
break;
}
x = (x + width)%width;
y = (y + height)%height;
idx = x + y*width;
}
return {gridToLine, lineToGrid};
}

*draw(scale, steps){
const {grid, width, height} = this;
const {T, TR, R, RB, B, BL, L, LT} = directions;
const step = scale * 5;
const w = step * width;
const h = step * height;
const strokeCtx = DOM.context2d(w, h, 1);
const gradientCtx = DOM.context2d(w, h, 1);
const outCtx = DOM.context2d(w, h, 1);

gradientCtx.fillStyle = "white";
gradientCtx.fillRect(0, 0, w, h);

strokeCtx.lineWidth = Math.floor(scale * 2);
strokeCtx.lineCap = "round"
let x = 0, y = 0, px = 0, py = 0;;
for(let i = 0, idx = 0; i < grid.length; i++) {
strokeCtx.strokeStyle = this.stroke(i / grid.length);
if (Math.abs(px-x) > step) px = x + (px < x ? step : -step);
if (Math.abs(py-y) > step) py = y + (py < y ? step : -step)
strokeCtx.beginPath();
strokeCtx.moveTo(px + step/2, py + step/2);
strokeCtx.lineTo(x + step/2, y + step/2);
strokeCtx.moveTo(px + step/2 + w, py + step/2);
strokeCtx.lineTo(x + step/2 + w, y + step/2);
strokeCtx.moveTo(px + step/2 - w, py + step/2);
strokeCtx.lineTo(x + step/2 - w, y + step/2);
strokeCtx.moveTo(px + step/2, py + step/2 + h);
strokeCtx.lineTo(x + step/2, y + step/2 + h);
strokeCtx.moveTo(px + step/2, py + step/2 - h);
strokeCtx.lineTo(x + step/2, y + step/2 - h);
strokeCtx.stroke();
px = x = (x + w)%w;
py = y = (y + h)%h;
gradientCtx.fillStyle = this.fill(i / grid.length);
gradientCtx.fillRect(x, y, step, step);
const direction = grid[idx];
switch (direction) {
case T:
y -= step;
break;
case TR:
y -= step;
x += step;
break;
case R:
x += step;
break
case RB:
x += step;
y += step;
break;
case B:
y += step;
break;
case BL:
y += step;
x -= step;
break;
case L:
x -= step;
break;
case LT:
x -= step;
y -= step;
break;
}
x = (x + w)%w;
y = (y + h)%h;
idx = (x/step) + (y/step)*width;
if (i%steps === 0) {
outCtx.drawImage(gradientCtx.canvas, 0, 0);
outCtx.drawImage(strokeCtx.canvas, 0, 0);
yield outCtx.canvas
}
}
outCtx.drawImage(gradientCtx.canvas, 0, 0);
outCtx.drawImage(strokeCtx.canvas, 0, 0);
return outCtx.canvas;
}

*line(scale, steps = 1){
const {grid, width, height} = this;
const {T, TR, R, RB, B, BL, L, LT} = directions;
const step = scale * 5;
const w = step * width;
const h = step * height;
const strokeCtx = DOM.context2d(w, h, 1);
const gradientCtx = DOM.context2d(w, h, 1);
const outCtx = DOM.context2d(w, h, 1);
const drawSegment = (x0, y0, dx, dy) => {
outCtx.beginPath();
outCtx.moveTo(x0, y0);
outCtx.lineTo(x0 + dx, y0 + dy);

outCtx.moveTo(x0 + w, y0);
outCtx.lineTo(x0 + w + dx, y0 + dy);

outCtx.moveTo(x0 - w, y0);
outCtx.lineTo(x0 - w + dx, y0 + dy);

outCtx.moveTo(x0, y0 + h);
outCtx.lineTo(x0 + dx, y0 + dy + h);

outCtx.moveTo(x0, y0 - h);
outCtx.lineTo(x0 + dx, y0 + dy - h);

outCtx.stroke();
};
outCtx.lineWidth = Math.floor(scale * 1.5);
outCtx.lineCap = "round";
outCtx.fillStyle = "#EEE";
outCtx.fillRect(0, 0, w, h);
outCtx.strokeStyle = "#FFF";
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const x0 = x*step + step/2, y0 = y*step + step/2;
drawSegment(x0, y0, 0, 0);
}
}
outCtx.strokeStyle = "black";
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const x0 = x*step + step/2, y0 = y*step + step/2;
const idx = x + y*width;
const direction = grid[idx];
if (direction & T) drawSegment(x0, y0, 0, -step);
if (direction & TR) drawSegment(x0, y0, step, -step);
if (direction & R) drawSegment(x0, y0, step, 0);
if (direction & RB) drawSegment(x0, y0, step, step);
if (direction & B) drawSegment(x0, y0, 0, step);
if (direction & BL) drawSegment(x0, y0, -step, step);
if (direction & L) drawSegment(x0, y0, -step, 0);
if (direction & LT) drawSegment(x0, y0, -step, -step);
}
yield outCtx.canvas
}
}
}
Insert cell
Insert cell
testGrid1 = {
const {T, TR, R, RB, B, BL, L, LT} = directions;
return [
R, R, RB, B,
TR, R, B, B,
T, T, L, BL,
T, LT, L, L,
];
}
Insert cell
new Curve(testGrid1)
.draw(5, 1)
Insert cell
new Curve(testGrid1)
.line(5, 1)
Insert cell
new Curve(testGrid1)
.translate(0,2)
.draw(5, 1)
Insert cell
new Curve(testGrid1)
.translate(0,2)
.line(5, 1)
Insert cell
new Curve(testGrid1)
.translate(2,0)
.draw(5, 1)
Insert cell
new Curve(testGrid1)
.translate(2,0)
.line(5, 1)
Insert cell
new Curve(testGrid1 )
.translate(2,2)
.draw(5, 1)
Insert cell
new Curve(testGrid1)
.translate(2,2)
.line(5, 1)
Insert cell
Insert cell
new Curve(testGrid2)
.line(5, 1)
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
new Curve(testGrid2)
.skewHorizontal(1,3)
.draw(5, 1)
Insert cell
new Curve(testGrid2)
.skewHorizontal(1,3)
.line(5, 1)
Insert cell
new Curve(testGrid2)
.skewVertical(2,0)
.draw(5, 1)
Insert cell
new Curve(testGrid2)
.skewVertical(2,0)
.line(5, 1)
Insert cell
new Curve(testGrid2)
.skewHorizontal(1,3)
.skewVertical(2,0)
.draw(5, 1)
Insert cell
new Curve(testGrid2)
.skewVertical(2,0)
.skewHorizontal(1,3)
.line(5, 1)
Insert cell
// Still bugged
Curve.hicurve(1)
.skewHorizontal(2, 1)
.skewVertical(2, 1)
.draw(3, 6)
Insert cell
d3 = require('d3-scale-chromatic', 'd3-color')
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more