The Artist

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 :)