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,
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 arrowYSpacing = height / (numRows - 1);
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(startX, arrowY + arrowThicknessStart);
ctx.close();
arrowY += arrowYSpacing;
},
{
color: "black",
transform: [
{
type: "rotateAround",
angle: -20,
centerX: startX,
centerY: arrowY,
},
],
},
);
}
});
};
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);
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);
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++) {
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