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
}
}
}