79129024

Date: 2024-10-26 16:53:38
Score: 1
Natty:
Report link

To build on top of the user1693593 answer, I've added a method to add missing steps between generated images for thicker outlines or more pointy base image. It works by adding steps between set checkpoint based on set granularity.

  // Notice that I've used 0.75 instead of 0.5 as it generates less pointy border
  let dArr: number[][] = [
    [-0.75, -0.75], // ↖️
    [ 0   , -1   ], // ⬆️
    [ 0.75, -0.75], // ↗️
    [ 1   ,  0   ], // ➡️
    [ 0.75,  0.75], // ↘️
    [ 0   ,  1   ], // ⬇️
    [-0.75,  0.75], // ↙️
    [-1   ,  0   ], // ⬅️
  ];

  // Our breaking point, below 5 pixels it's not worth runing
  if (thickness <= 5) {
    return dArr;
  }

  // 2.5 is the factor deciding the amount of steps in between checkpoints
  // the lower the number => the more steps will be added
  const granularity = Math.floor(thickness / 2.5);
  let newDArr: number[][] = [];
  for(let i=0; i < dArr.length; i++) {
    newDArr.push(dArr[i]);
    // c* is our current directions and d* is a destination 
    const [cX, cY] = dArr[i],
      [dX, dY] = i + 1 === dArr.length ? dArr[0] : dArr[i + 1]
    ;

    // Here we are defining our trends: up or down.
    // As Y and X can have different trend (X can go down where Y up)
    // we have to treat them independly
    const trendX = cX > dX ? -1 : 1,
      trendY = cY > dY ? -1 : 1,
      bX = (Math.abs(cX - dX)/granularity) * trendX,
      bY = (Math.abs(cY - dY)/granularity) * trendY,
      between: number[][] = []
    ;
    let x = cX,
      y = cY
    ;
    while (
      (
        trendX > 0 && x + bX < dX
        || trendX < 0 && x + bX > dX
      )
      && (
        trendY > 0 && y + bY < dY
        || trendY < 0 && y + bY > dY
      )
    ) {
      x += bX;
      y += bY;
      between.push([x, y]);
    }
    newDArr = newDArr.concat(between);
  }

  return newDArr;

Whole function (with user1693593 answer):

const outlineImage = (
  image: HTMLImageElement,
  fill: string,
  thickness: number,
  asWidth: number,
  asHeight: number,
): void => {
  const canvas = document.createElement('canvas'),
    ctx = canvas.getContext('2d')!
  ;
  let dArr = [
    [-0.75, -0.75], // ↖️
    [ 0   , -1   ], // ⬆️
    [ 0.75, -0.75], // ↗️
    [ 1   ,  0   ], // ➡️
    [ 0.75,  0.75], // ↘️
    [ 0   ,  1   ], // ⬇️
    [-0.75,  0.75], // ↙️
    [-1   ,  0   ], // ⬅️
  ];

  if (thickness > 5) {
    const granularity = Math.floor(thickness / 2.5);
    let newDArr: number[][] = [];
    for(let i=0; i < dArr.length; i++) {
      newDArr.push(dArr[i]);
      const [cX, cY] = dArr[i],
        [dX, dY] = i + 1 === dArr.length ? dArr[0] : dArr[i + 1]
      ;

      const trendX = cX > dX ? -1 : 1,
        trendY = cY > dY ? -1 : 1,
        bX = (Math.abs(cX - dX)/granularity) * trendX,
        bY = (Math.abs(cY - dY)/granularity) * trendY,
        between: number[][] = []
      ;
      let x = cX,
        y = cY
      ;
      while (
        (
          trendX > 0 && x + bX < dX
          || trendX < 0 && x + bX > dX
        )
        && (
          trendY > 0 && y + bY < dY
          || trendY < 0 && y + bY > dY
        )
      ) {
        x += bX;
        y += bY;
        between.push([x, y]);
      }
      newDArr = newDArr.concat(between);
    }

    dArr = newDArr;
  }

  canvas.setAttribute('width', String(asWidth + thickness*2));
  canvas.setAttribute('height', String(asHeight + thickness*2));

  for (let i = 0; i < dArr.length; i++) {
    ctx.drawImage(
      image,
      thickness + dArr[i][0] * thickness,
      thickness + dArr[i][1] * thickness,
      asWidth,
      asHeight,
    );

    if (thickness === 0) {
      break;
    }
  }

  ctx.globalCompositeOperation = 'source-in';
  ctx.fillStyle = fill;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.globalCompositeOperation = 'source-over';

  ctx.drawImage(image, thickness, thickness, asWidth, asHeight);
}

Before: Script output before additions After: Script output after additions

Disclaimer:

Based on the amount of additional steps you want to add or thickness of the border, this might become quite slow and loose its edge over the marching ant's solution.

Reasons:
  • Probably link only (1):
  • Long answer (-1):
  • Has code block (-0.5):
  • User mentioned (1): user1693593
  • User mentioned (0): user1693593
  • Low reputation (0.5):
Posted by: Mortimer