opsCanvas

import type { OpsCanvas } from "@ops/canvas";

export const runCircleWrapper = (
  canvas: OpsCanvas,
  func: (canvas: OpsCanvas) => void,
) => {
  const patternCanvas = canvas.clone();

  const offset = 10;

  func(patternCanvas);
  canvas.setColor("white");

  patternCanvas.mask({
    fill: (ctx) => {
      ctx.addCircle(
        patternCanvas.getWidth() * 0.5 +
          canvas.seededNoise.getRandomRange(-offset, offset),
        patternCanvas.getHeight() * 0.5 +
          canvas.seededNoise.getRandomRange(-offset, offset),
        patternCanvas.getRel(0.4, "relativeMin"),
      );
    },
  });

  canvas.drawFillPath(
    (ctx) => {
      ctx.addCircle(
        canvas.getWidth() * 0.5 +
          canvas.seededNoise.getRandomRange(-offset, offset),
        canvas.getHeight() * 0.5 +
          canvas.seededNoise.getRandomRange(-offset, offset),
        canvas.getRel(0.4, "relativeMin"),
      );
    },
    {
      color: "yellow",
    },
  );

  canvas.blend(patternCanvas, 0, 0);
};
parts/circleWrapper.ts
import type { OpsCanvas } from "@ops/canvas";
import { Grid } from "@ops-utils/layout";

import { runCircleWrapper } from "./circleWrapper.ts";

export const addCircleGridDots = (_canvas: OpsCanvas) => {
  runCircleWrapper(_canvas, (canvas) => {
    canvas.drawStrokePath(
      (ctx) => {
        Grid.CircleGrid(
          {
            min: [0, 0],
            max: [canvas.getWidth(), canvas.getHeight()],
            radius: canvas.getRel(0.08, "relativeMin"),
            count: 9,
            // count: [9, 9],
            relativePositionInsideStep: [0.5, 0.5],
          },
          (data) => {
            ctx.addCircle(
              data.position[0],
              data.position[1],
              canvas.getRel(0.032, "relativeMin"),
            );
          },
        );
      },
      { lineWeight: canvas.getRel(0.01, "relativeMin"), color: "black" },
    );
  });
};
parts/3_circleGridDots.ts
import type { OpsCanvas } from "@ops/canvas";
import { lerp, V2 } from "@ops-utils/math";

import { runCircleWrapper } from "./circleWrapper.ts";

export const addArrows = (_canvas: OpsCanvas) => {
  runCircleWrapper(_canvas, (canvas) => {
    const width = canvas.getWidth();
    const height = canvas.getHeight();

    const arrowAngle: V2.V2 = [1, 0.1];
    V2.normalize(arrowAngle);

    const arrowExtrude: V2.V2 = [arrowAngle[1], -arrowAngle[0]];

    const numRows = 20;
    const arrowThicknessStart = canvas.getRel(0.017, "relativeMin");
    // const arrowThicknessEnd = canvas.getRel(0.0001, "relativeMin");

    const arrowYSpacing = height / (numRows - 1);
    // let arrowY = arrowYSpacing * 0.3;
    let arrowY = (arrowThicknessStart * 2.0 + arrowYSpacing * 0.3) * 4.8;

    const startX = width * -0.1;
    let endX = width * 0.92;

    for (let i = 0; i < numRows; i++) {
      canvas.drawFillPath(
        (ctx) => {
          ctx.moveTo(startX, arrowY - arrowThicknessStart);
          ctx.lineTo(endX, arrowY);
          // ctx.lineTo(endX, arrowY);
          ctx.lineTo(startX, arrowY + arrowThicknessStart);
          ctx.close();

          // ctx.addCircle(
          //   x,
          //   y,
          //   circleSize,
          // );
          arrowY += arrowYSpacing;
        },
        {
          color: "black",
          transform: [
            {
              type: "rotateAround",
              angle: -20,
              centerX: startX,
              centerY: arrowY,
            },
          ],
        },
      );

      // endX += canvas.getRel(0.001, "relativeMin");
    }
  });
};
parts/6_arrows.ts
import type { OpsCanvas } from "@ops/canvas";
import { Grid } from "@ops-utils/layout";
import { clamp01, V2 } from "@ops-utils/math";

import { runCircleWrapper } from "./circleWrapper.ts";

export const addBoxGridDotsDiagonal = (_canvas: OpsCanvas) => {
  const width = _canvas.getWidth();
  const height = _canvas.getHeight();

  const projectionStart: V2.V2 = [width * -0.2, height * 1.2];
  const projectionEnd: V2.V2 = [width * 0.8, 0];

  const sizeProjectionV = V2.fromToV2(projectionStart, projectionEnd);
  const sizeProjectionVLength = V2.length(sizeProjectionV);
  V2.normalize(sizeProjectionV);

  let dotSize = 0;

  runCircleWrapper(_canvas, (canvas) => {
    canvas.drawFillPath(
      (ctx) => {
        Grid.BoxGrid(
          {
            min: [0, 0],
            max: [width, height],
            count: [8, 8],
            relativePositionInsideStep: [0.5, 0.5],
          },
          (data) => {
            const toDotV = V2.fromTo(
              projectionStart[0],
              projectionStart[1],
              data.position[0],
              data.position[1]
            );

            dotSize =
              (1.0 -
                clamp01(
                  Math.abs(
                    V2.dot(sizeProjectionV, toDotV) / sizeProjectionVLength
                  )
                )) *
              data.step[2];

            ctx.addCircle(data.position[0], data.position[1], dotSize);
          }
        );
      },
      { color: "black" }
    );
  });
};
parts/2_boxGridDotsDiagonal.ts
import type { OpsCanvas } from "@ops/canvas";
import { Grid } from "@ops-utils/layout";
import { inverseLerpClamped, lerp, V2 } from "@ops-utils/math";

import { runCircleWrapper } from "./circleWrapper.ts";

export const addCircleGridDotsCenterScaled = (_canvas: OpsCanvas) => {
  runCircleWrapper(_canvas, (canvas) => {
    const canvasCenter: V2.V2 = [
      canvas.getWidth() * 0.52,
      canvas.getHeight() * 0.53,
    ];

    const minLength = canvas.getRel(0.01, "relativeMin");
    const maxLength = canvas.getRel(0.4, "relativeMin");

    const maxCircleSize = canvas.getRel(0.07, "relativeMin");
    const minCircleSize = canvas.getRel(0.001, "relativeMin");

    canvas.drawFillPath(
      (ctx) => {
        Grid.CircleGrid(
          {
            min: [0, 0],
            max: [canvas.getWidth(), canvas.getHeight()],
            radius: canvas.getRel(0.08, "relativeMin"),
            count: 10,
            relativePositionInsideStep: [0.5, 0.5],
          },
          (data) => {
            const dist = V2.dist(
              canvasCenter,
              [data.position[0], data.position[1]],
            );

            let relDist = 1.0 - inverseLerpClamped(minLength, maxLength, dist);
            relDist = lerp(relDist, relDist * relDist, 0.6);
            relDist = 1.0 - relDist;

            const dotSize = lerp(
              maxCircleSize,
              minCircleSize,
              relDist,
            );

            ctx.addCircle(
              data.position[0],
              data.position[1],
              dotSize,
            );
          },
        );
      },
      { color: "black" },
    );
  });
};
parts/4_circleGridDotsCenterScaled.ts
import type { OpsCanvas } from "@ops/canvas";
import { Grid } from "@ops-utils/layout";

import { runCircleWrapper } from "./circleWrapper.ts";

export const addBoxGridDotsEdge = (_canvas: OpsCanvas) => {
  runCircleWrapper(_canvas, (canvas) => {
    canvas.drawFillPath(
      (ctx) => {
        Grid.BoxGrid(
          {
            min: [0, 0],
            max: [canvas.getWidth(), canvas.getHeight()],
            count: [8, 8],
            relativePositionInsideStep: [0.5, 0.5],
          },
          (data) => {
            const dotSize = Math.max(
              2.0,
              data.step[2] *
                0.5 *
                Math.min(1.0 - data.index[0] / 7, data.index[1] / 7)
            );

            ctx.addCircle(data.position[0], data.position[1], dotSize);
          }
        );
      },
      { color: "black" }
    );
  });
};
parts/1_boxGridDotsEdge.ts
import type { OpsCanvas } from "@ops/canvas";
import { lerp } from "@ops-utils/math";

import { runCircleWrapper } from "./circleWrapper.ts";

export const addMaskedLineSteps = (_canvas: OpsCanvas) => {
  runCircleWrapper(_canvas, (canvas) => {
    const width = canvas.getWidth();
    const height = canvas.getHeight();

    const centerX = width * 0.5;
    const centerY = height * 0.5;

    const numSteps = 7;
    const numLines = 12;
    const lineThicknessStart = canvas.getRel(0.08, "relativeMin");
    const lineThicknessEnd = canvas.getRel(0.008, "relativeMin");

    const lineYSpacing = height / (numLines - 1);
    // let arrowY = arrowYSpacing * 0.3;

    const maskedCanvas = canvas.clone();

    for (let j = 0; j < numSteps; j++) {
      maskedCanvas.set(0, "rgba");

      const relProgress = j / (numSteps - 1);
      let arrowY = lineYSpacing * 0.3;
      maskedCanvas.drawStrokePath(
        (ctx) => {
          for (let i = 0; i < numLines; i++) {
            ctx.moveTo(0, arrowY);
            ctx.lineTo(width, arrowY);

            arrowY += lineYSpacing;
          }
        },
        {
          color: "black",
          lineWeight: lerp(
            lineThicknessStart,
            lineThicknessEnd,
            relProgress,
          ),
          transform: [
            {
              type: "rotateAround",
              angle: -20,
              centerX: centerX,
              centerY: centerY,
            },
          ],
        },
      );

      maskedCanvas.mask({
        fill: (ctx) => {
          ctx.addRect(0, 0, width * relProgress, height);
        },
        transform: [
          {
            type: "rotateAround",
            angle: -50,
            centerX: centerX,
            centerY: centerY,
          },
        ],
      });

      canvas.blend(maskedCanvas, 0, 0);
    }
  });
};
parts/7_maskedLineSteps.ts
import type { OpsCanvas } from "@ops/canvas";
import { lerp } from "@ops-utils/math";

import { runCircleWrapper } from "./circleWrapper.ts";

export const addMaskedLines = (_canvas: OpsCanvas) => {
  runCircleWrapper(_canvas, (canvas) => {
    const width = canvas.getWidth();
    const height = canvas.getHeight();

    const centerX = width * 0.5;
    const centerY = height * 0.5;

    const numSteps = 6;
    const numLines = 12;
    const lineThicknessStart = canvas.getRel(0.085, "relativeMin");
    const lineThicknessEnd = canvas.getRel(0.008, "relativeMin");

    const lineYSpacing = height / (numLines - 1);
    // let arrowY = arrowYSpacing * 0.3;

    const maskedCanvas = canvas.clone();

    for (let j = 0; j < numSteps; j++) {
      maskedCanvas.set(0, "rgba");

      const relProgress = j / (numSteps - 1);
      let arrowY = lineYSpacing * canvas.seededNoise.getRandomRange(-2.0, 2.0);
      maskedCanvas.drawStrokePath(
        (ctx) => {
          for (let i = 0; i < numLines; i++) {
            ctx.moveTo(0, arrowY);
            ctx.lineTo(width, arrowY);

            arrowY += lineYSpacing;
          }
        },
        {
          color: "black",
          lineWeight: lerp(
            lineThicknessStart,
            lineThicknessEnd,
            relProgress,
          ),
          transform: [
            {
              type: "rotateAround",
              angle: -20,
              centerX: centerX,
              centerY: centerY,
            },
          ],
        },
      );

      maskedCanvas.mask({
        fill: (ctx) => {
          ctx.addRect(
            width * (j / numSteps),
            0,
            width / numSteps + 0.5,
            height,
          );
        },
        transform: [
          {
            type: "rotateAround",
            angle: -50,
            centerX: centerX,
            centerY: centerY,
          },
        ],
      });

      canvas.blend(maskedCanvas, 0, 0);
    }
  });
};
parts/8_maskedLines.ts
import type { OpsCanvas } from "@ops/canvas";
import { Grid } from "@ops-utils/layout";

import { runCircleWrapper } from "./circleWrapper.ts";

export const addBoxGridDots = (_canvas: OpsCanvas) => {
  runCircleWrapper(_canvas, (canvas) => {
    canvas.drawFillPath(
      (ctx) => {
        Grid.BoxGrid(
          {
            min: [0, 0],
            max: [canvas.getWidth(), canvas.getHeight()],
            count: [8, 8],
            relativePositionInsideStep: [0.5, 0.5],
          },
          (data) => {
            ctx.addCircle(
              data.position[0],
              data.position[1],
              canvas.getRel(0.01, "relativeMin")
            );
          }
        );
      },
      { color: "black" }
    );
  });
};
parts/0_boxGridDots.ts
import type { OpsCanvas } from "@ops/canvas";
import { lerp, V2 } from "@ops-utils/math";

import { runCircleWrapper } from "./circleWrapper.ts";

export const addCircleStar = (_canvas: OpsCanvas) => {
  runCircleWrapper(_canvas, (canvas) => {
    const canvasCenter: V2.V2 = [
      canvas.getWidth() * 0.5,
      canvas.getHeight() * 0.5,
    ];

    const numRows = 14;
    const circleSizeMax = canvas.getRel(0.06, "relativeMin");
    const circleSizeMin = canvas.getRel(0.0001, "relativeMin");
    const circleSpacing = canvas.getRel(0.016, "relativeMin");

    let circleSize = 0;
    let radius = 0;

    const getCircleSize = (index: number) => {
      if (index <= 0) {
        return circleSizeMax;
      }

      const sizeInterpolator = Math.pow(1 - index / (numRows - 1), 4);

      return lerp(
        circleSizeMin,
        circleSizeMax,
        sizeInterpolator,
      );
    };

    canvas.drawFillPath(
      (ctx) => {
        for (let i = 0; i < numRows; i++) {
          // calcualted with https://www.wolframalpha.com/input?i=linear+fit+%280%2C1%29+%281%2C6%29+%282%2C12%29+%283%2C+18%29+%284%2C+24%29
          const numCircles = Math.max(
            1,
            Math.round(
              5.8 * i + 0.6,
            ),
          );

          const angleSteps = (Math.PI * 2) / numCircles;

          circleSize = getCircleSize(i);

          for (let j = 0; j < numCircles; j++) {
            const angle = angleSteps * j;

            const x = canvasCenter[0] + Math.sin(angle) * radius;
            const y = canvasCenter[1] + -Math.cos(angle) * radius;

            ctx.addCircle(
              x,
              y,
              circleSize,
            );
          }

          radius += getCircleSize(i);
          radius += circleSpacing;
          radius += getCircleSize(i + 1);
        }
      },
      { color: "black" },
    );
  });
};
parts/5_circleStar.ts
import { createOpsCanvas } from "@ops/canvas";
import { Grid } from "@ops-utils/layout";

import { addBoxGridDots } from "./parts/0_boxGridDots.ts";
import { addBoxGridDotsEdge } from "./parts/1_boxGridDotsEdge.ts";
import { addBoxGridDotsDiagonal } from "./parts/2_boxGridDotsDiagonal.ts";
import { addCircleGridDots } from "./parts/3_circleGridDots.ts";
import { addCircleGridDotsCenterScaled } from "./parts/4_circleGridDotsCenterScaled.ts";
import { addCircleStar } from "./parts/5_circleStar.ts";
import { addArrows } from "./parts/6_arrows.ts";
import { addMaskedLineSteps } from "./parts/7_maskedLineSteps.ts";
import { addMaskedLines } from "./parts/8_maskedLines.ts";

const renderFuns = [
  addBoxGridDots,
  addBoxGridDotsEdge,
  addBoxGridDotsDiagonal,
  addCircleGridDots,
  addCircleGridDotsCenterScaled,
  addCircleStar,
  addArrows,
  addMaskedLineSteps,
  addMaskedLines,
];

const size = Math.round(parseFloat(Deno.args[1]));
const canvas = createOpsCanvas(size, size);

const partSize = Math.floor(size / 3);

canvas.setColor("white");

canvas.seededNoise.setSeed(`pattern3`);

Grid.BoxGrid({ count: [3, 3], step: [partSize, partSize] }, (data) => {
  const partCanvas = createOpsCanvas(partSize, partSize);
  partCanvas.seededNoise.setSeed(
    `pattern-${canvas.seededNoise.getRandomRange(0, 100).toFixed()}`,
  );

  const func = renderFuns[data.index[2]];

  if (func) {
    func(partCanvas);
    canvas.blend(partCanvas, data.position[0], data.position[1]);
  }
});

Deno.writeFileSync(Deno.args[0], canvas.toPngBuffer());
mod.ts