class MatrixGrid {
constructor(rows, columns, {
cellSize = millimeter(10),
topLeftPos = glm.vec2.fromValues(millimeter(20), millimeter(-20)),
wallWidth = 2,
}) {
this.numRows = rows;
this.numColumns = columns;
this.cellSize = cellSize;
this.topLeftPos = topLeftPos;
this.wallWidth = wallWidth;
this.topLeftCellCenter = glm.vec2.set(glm.vec2.create(), this.topLeftPos[0] + this.cellSize / 2, this.topLeftPos[1] - this.cellSize / 2);
this.oneCellDownward = glm.vec2.fromValues(0, -this.cellSize);
this.oneCellRightward = glm.vec2.fromValues(this.cellSize, 0);
const properRowLineLength = this.cellSize * this.numColumns;
const properColLineLength = this.cellSize * this.numRows;
this.lengthOfRowLine = [...Array(rows + 1)].map(() => properRowLineLength + mathjs.random(0, this.cellSize));
this.lengthOfColLine = [...Array(columns + 1)].map(() => properColLineLength + mathjs.random(0, this.cellSize));
this.bezierCurvePointsOfRowLine = this.lengthOfRowLine.map((length, row) => {
const rowLineOffset = length - (this.cellSize * this.numColumns);
let lineStart = glm.vec2.add(glm.vec2.create(), this.topLeftPos, glm.vec2.scale(glm.vec2.create(), this.oneCellDownward, row));
lineStart = glm.vec2.set(lineStart, lineStart[0] - rowLineOffset / 2, lineStart[1]);
let lineEnd = glm.vec2.add(glm.vec2.create(), lineStart, glm.vec2.scale(glm.vec2.create(), this.oneCellRightward, this.numColumns));
lineEnd = glm.vec2.set(lineEnd, lineEnd[0] + rowLineOffset, lineEnd[1]);
const w1 = mathjs.random(0.1, 0.7);
const w2 = mathjs.random(w1, 0.9);
const h1 = mathjs.random(-this.cellSize / 5, this.cellSize / 5);
const h2 = mathjs.random(-this.cellSize / 5, this.cellSize / 5);
const points = bezierCurvePointsFromWeights(lineStart, lineEnd, [w1, w2], [h1, h2]);
return points;
});
this.bezierCurvePointsOfColLine = this.lengthOfColLine.map((length, col) => {
const colLineOffset = length - (this.cellSize * this.numRows);
let lineStart = glm.vec2.add(glm.vec2.create(), this.topLeftPos, glm.vec2.scale(glm.vec2.create(), this.oneCellRightward, col));
lineStart = glm.vec2.set(lineStart, lineStart[0], lineStart[1] + colLineOffset / 2);
let lineEnd = glm.vec2.add(glm.vec2.create(), lineStart, glm.vec2.scale(glm.vec2.create(), this.oneCellDownward, this.numRows));
lineEnd = glm.vec2.set(lineEnd, lineEnd[0], lineEnd[1] - colLineOffset);
const w1 = mathjs.random(0.1, 0.7);
const w2 = mathjs.random(w1, 0.9);
const h1 = mathjs.random(-this.cellSize / 5, this.cellSize / 5);
const h2 = mathjs.random(-this.cellSize / 5, this.cellSize / 5);
const points = bezierCurvePointsFromWeights(lineStart, lineEnd, [w1, w2], [h1, h2]);
return points;
});
this.lineCurvePoints = [...this.bezierCurvePointsOfRowLine, ...this.bezierCurvePointsOfColLine];
// this.cells = [];
// for (let i = 0; i < rows; i++) {
// const row = [];
// for (let j = 0; j < columns; j++) {
// row.push(new MatrixCell());
// }
// this.cells.push(row);
// }
this.pendingCommands = new Queue();
this.drawables = [];
this.updateables = [];
this.drawables = [];
this.worldStartTimestamp = 0;
this.justCompletedUpdateables = new Queue();
this.poppedFirstCommand = false;
}
getCellCenter(row, col) {
let c = glm.vec2.add(glm.vec2.create(), this.topLeftCellCenter, glm.vec2.scale(glm.vec2.create(), this.oneCellRightward, col))
c = glm.vec2.add(c, c, glm.vec2.scale(glm.vec2.create(), this.oneCellDownward, row));
return c;
}
// offset is added as the vector (offset, -offset)
getCellTopLeft(row, col, offset = 0) {
let topLeftCorner = glm.vec2.clone(this.topLeftPos);
topLeftCorner = glm.vec2.add(topLeftCorner, topLeftCorner, glm.vec2.scale(glm.vec2.create(), this.oneCellRightward, col));
topLeftCorner = glm.vec2.add(topLeftCorner, topLeftCorner, glm.vec2.scale(glm.vec2.create(), this.oneCellDownward, row));
glm.vec2.set(topLeftCorner, topLeftCorner[0] + offset, topLeftCorner[1] - offset);
return topLeftCorner;
}
draw() {
saveCanvasStyle(() => {
wp.canvas.lineWidth = this.wallWidth;
let rowStart = glm.vec2.clone(this.topLeftPos);
let rowEnd = glm.vec2.add(glm.vec2.create(), rowStart, glm.vec2.fromValues(this.cellSize * this.numColumns, 0));
wp.canvas.beginPath();
wp.canvas.strokeStyle = "#020202";
for (let curvePoints of this.lineCurvePoints) {
wp.canvas.beginPath();
wp.bezierCurveTo(curvePoints);
wp.canvas.stroke();
}
});
}
pushCommand(command) {
this.pendingCommands.push(command);
// console.log('push:', command);
}
popCommand(timestampOfPop, msg, throwOnNoCommand = false) {
// console.log('pop command called from', msg);
const cmd = this.pendingCommands.pop();
if (!cmd) {
if (throwOnNoCommand) {
throw new Error('no command to pop!');
}
return false;
}
// console.log('pop result:', cmd, 'from:', msg);
// For the arrows
const perCellForwardDuration = cmd.duration * 0.8;
const perCellPauseDuration = 0;
const perCellBackwardDuration = cmd.duration - perCellForwardDuration;
const durationPerCell = perCellForwardDuration + perCellPauseDuration + perCellBackwardDuration;
const timestampForwardStart = timestampOfPop;
const timestampForwardEnd = timestampForwardStart + perCellForwardDuration;
const timestampBackwardStart = timestampForwardEnd + perCellPauseDuration;
const timestampBackwardEnd = timestampBackwardStart + perCellBackwardDuration;
const padding = this.cellSize / 20;
const targetCellTopLeft = this.getCellTopLeft(cmd.targetCellIndex[0], cmd.targetCellIndex[1], padding);
// console.log('enqueued interpolator starts at', timestampForwardStart);
// console.log('enqueued interpolator ends at', timestampForwardEnd);
if (cmd.type === CellDataflowCommandEnum) {
// console.log('processing new popped command:', cmd);
// Create the cell fill
const cellFillInterpolator = new LinearInterpolator(timestampForwardStart, timestampBackwardEnd);
const rectFiller = new InterpolatedRectFiller(
cellFillInterpolator,
targetCellTopLeft,
this.cellSize - 2 * padding,
this.cellSize - 2 * padding,
glm.vec3.fromValues(0.8, 0.8, 0.8),
glm.vec3.fromValues(0.3, 0.3, 0.3));
rectFiller.setOnComplete(() => {
console.log('oncomplete rect filler for', cmd.targetCellIndex);
this.justCompletedUpdateables.push({
updateable: rectFiller,
timestampEnd: rectFiller.interpolator.timestampEnd, action: (ts) => {
console.log('popping new command which starts at', Math.max(rectFiller.interpolator.timestampEnd, ts));
this.popCommand(Math.max(rectFiller.interpolator.timestampEnd, ts), `from rectFiller at ${timestampOfPop}`);
}
});
});
// console.log('enqueueing rect filler at', timestampForwardStart);
this.updateables.push(rectFiller);
this.drawables.push(rectFiller);
const targetPoint = this.getCellCenter(cmd.targetCellIndex[0], cmd.targetCellIndex[1]);
// Create the arrows
for (let sourceCellIndex of cmd.sourceCellIndices) {
let sourcePoint = this.getCellCenter(sourceCellIndex[0], sourceCellIndex[1]);
if (glm.vec2.equals(sourceCellIndex, cmd.targetCellIndex)) {
sourcePoint = glm.vec2.set(sourcePoint, sourcePoint[0], sourcePoint[1] + this.cellSize / 10);
}
const curvePoints = parabolicBezierCurvePoints(sourcePoint, targetPoint, this.cellSize / 2);
const arrowInterpolator = new LinearRoundtripInterpolator(timestampForwardStart, timestampForwardEnd, timestampBackwardStart, timestampBackwardEnd, false);
const arrow = new InterpolatedBezierCurve(arrowInterpolator, curvePoints);
arrow.setOnComplete(() => {
console.log("oncomplete for arrow from", sourceCellIndex, cmd.targetCellIndex);
this.justCompletedUpdateables.push({
updateable: arrow, timestampEnd: arrow.interpolator.timestampBackwardEnd
});
});
arrow.setShouldDrawArrows(true);
arrow.arrowSideLength = this.cellSize / 4;
arrow.cacheArrowHeads();
// console.log("enqueueing arrow at", arrow, timestampForwardStart);
this.updateables.push(arrow);
this.drawables.push(arrow);
}
}
return true;
}
step(ts) {
if (!this.poppedFirstCommand) {
const didPop = this.popCommand(ts, `from step at ${ts}`);
if (didPop) {
this.poppedFirstCommand = true;
}
} else {
// Pop from the just-completed updateable. We pop a command if we find one.
const completedInfo = this.justCompletedUpdateables.pop();
if (completedInfo && completedInfo.action) {
completedInfo.action(ts);
}
}
// Iterate through all updateables and update them
for (let upd of this.updateables) {
if (!upd.completed) {
// console.log("updating:", upd);
upd.update(ts);
}
}
const drawableOrder = ["rect_fill", "bezier_curve"];
for (let curDrawable of drawableOrder) {
for (let drawable of this.drawables) {
if (drawable.drawableType !== curDrawable) {
continue;
}
switch (drawable.drawableType) {
case "bezier_curve":
this.drawBezierCurve(drawable);
break;
case "rect_fill":
this.drawRectFill(drawable);
break;
default:
throw new Error('Unknown drawable', drawable);
}
}
}
this.draw();
}
drawBezierCurve(drawable) {
saveCanvasStyle(() => {
wp.canvas.lineWidth = 4;
wp.canvas.strokeStyle = "#2a2a2a";
wp.canvas.beginPath();
wp.bezierCurveTo(drawable.curTruncatedPoints);
wp.canvas.stroke();
wp.canvas.beginPath();
// console.log('arrowHeadLeft =', drawable.arrowHeadLeft);
// console.log('arrowHeadRight =', drawable.arrowHeadRight);
wp.moveTo(drawable.curTruncatedPoints[3]);
wp.lineTo(drawable.arrowHeadLeft);
wp.lineTo(drawable.arrowHeadRight);
wp.lineTo(drawable.curTruncatedPoints[3]);
wp.canvas.fillStyle = "#2a2a2a";
wp.canvas.fill();
});
}
drawRectFill(drawable) {
saveCanvasStyle(() => {
wp.canvas.beginPath();
wp.canvas.fillStyle = rgbCssFromVec3(drawable.curColor);
wp.rect(drawable.topLeft, drawable.width, drawable.height);
wp.canvas.fill();
wp.canvas.closePath();
});
}
}