Javascript abstract art! Click on the canvas for another masterpiece.
The current implementation uses a recursive function to split the canvas up into mini boxes. This is then ajusted to add a random factor in splitting and corrected to ensure that the boxes are of a minimum size. Start with the render() function and then move forwards/backwards.
// globals
const isDebug = false,
minWidth = 20,
minHeight = 20,
aspect = 3;
// react specific
// setup a reference to the canvas element
// calculate width, height and dynamically update if required
const ref = useRef<HTMLCanvasElement>(null);
const [width, setWidth] = useState(20);
const [height, setHeight] = useState(20);
const updateWidth = useCallback(() => {
const rect = ref.current?.parentElement?.getBoundingClientRect();
if (rect) {
const { width, height } = rect;
console.log([width, height]);
let targetWidth = width;
targetWidth = (targetWidth / 10) * 10;
let targetHeight = Math.round(targetWidth / aspect);
targetHeight = (targetHeight / 10) * 10;
targetHeight = 300; // override
setWidth(targetWidth);
setHeight(targetHeight);
}
}, []);
useEffect(() => {
updateWidth();
window.addEventListener("resize", updateWidth);
return () => {
window.removeEventListener("resize", updateWidth);
};
}, [updateWidth]);
// generic function, renders the art
const render = useCallback((width: number, height: number) => {
const canvas = document.getElementById("artist-canvas") as HTMLCanvasElement | null;
if (!canvas?.getContext) {
return;
}
const depth = 4;
console.log([width, height, depth]);
const ctx = canvas.getContext("2d");
if (!ctx) {
return;
}
ctx.clearRect(0, 0, width, height);
try {
splitGrid(ctx, depth, 0, 0, width, height);
} catch (err) {
console.error(err);
}
}, []);
useEffect(() => {
render(width, height);
}, [width, height, render]);
/**
* Split a given bounding box width-wise randomly and
* for each width split, create 2 vertical bounding boxes.
*
* Depending on the depth, recursively call to split the grid further
* or simply fill the bounding box using a similar algorithm
*/
const splitGrid = (
ctx: CanvasRenderingContext2D,
depth: number,
gx: number,
gy: number,
cWidth: number,
cHeight: number
) => {
if (depth % 2 !== 0 || depth < 2) {
throw new Error("depth needs to be a multiple of 2 (min 2)");
}
var x = 0,
y = 0,
width = 0,
height = 0;
//
// start at 0 and then loop until the width has been reached, splitting on each loop
//
// note that while the algorithm works on x, y, width and height as a bounding box
// the actual values that need to be passed along need to be corrected via gx and gy
// so that the final drawing on the context knows where to go.
//
while (x < cWidth) {
// reset y to 0, we split our box vertically only once
y = 0;
// get the minimum width (relative to the depth)
// then correct for overflow to keep it smooth
// also need to check for shady widths
if (cWidth <= minWidth * depth) {
width = cWidth;
} else {
width = randomIntFromInterval(minWidth * depth, cWidth);
width = width - (width % (minWidth * depth));
}
// correct for the last box in the series
if (x + width > cWidth) {
width = cWidth - x;
}
// similar concept with height except no last box correction required as we only split it once
if (cHeight <= minHeight * depth) {
height = cHeight;
} else {
height = randomIntFromInterval(minHeight * depth, cHeight);
height = height - (height % (minHeight * depth));
}
if (depth <= 2) {
fillGrid(ctx, x + gx, y + gy, width, height);
} else {
splitGrid(ctx, depth - 2, x + gx, y + gy, width, height);
}
// simple split twice
y = height;
height = cHeight - height;
if (height !== 0) {
if (depth <= 2) {
fillGrid(ctx, x + gx, y + gy, width, height);
} else {
splitGrid(ctx, depth - 2, x + gx, y + gy, width, height);
}
}
x += width;
}
};
/**
* Use a similar algorithm as fill grid to randomly split the bounding box width-wise
* and draw rectangles (2 per width split again)
*/
const fillGrid = (ctx: CanvasRenderingContext2D, gx: number, gy: number, cWidth: number, cHeight: number) => {
if (cWidth <= 0 || cHeight <= 0) {
return;
}
var x = 0,
y = 0,
width = 0,
height = 0;
while (x < cWidth) {
y = 0;
// because we have a dynamically sized canvas, it is possible to end up with shady widths and heights
if (cWidth <= minWidth) {
width = cWidth;
} else {
width = randomIntFromInterval(minWidth, cWidth);
width = width - (width % minWidth);
}
if (x + width > cWidth) {
width = cWidth - x;
}
if (cHeight <= minHeight) {
height = cHeight;
} else {
height = randomIntFromInterval(minHeight, cHeight);
height = height - (height % minHeight);
}
ctx.fillStyle = getColor();
ctx.fillRect(x + gx, y + gy, width, height);
y = height;
height = cHeight - height;
ctx.fillStyle = getColor();
ctx.fillRect(x + gx, y + gy, width, height);
x += width;
}
};
var getColor = () => {
var r = randomIntFromInterval(1, 255),
g = randomIntFromInterval(1, 255),
b = randomIntFromInterval(1, 255);
return `rgb(${r},${g},${b})`;
};
function randomIntFromInterval(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}See also JavaScript Art: Triangles for a simpler concept.
Feel free to send me an email with a screenshot of your favorite canvas :)