Files
WRNexusJS/packages/captcha/src/challenges/visual.ts
T
2026-07-25 13:38:18 +05:30

691 lines
24 KiB
TypeScript

import type {
CaptchaConcreteImageStyle,
CaptchaGeneratorContext,
} from "../types.ts";
import {
createImage,
drawGlyph,
drawLine,
fillCircle,
fillRect,
pngDataUri,
setPixel,
type Rgba,
type RgbaImage,
} from "./png.ts";
const LIGHT_BACKGROUND: Rgba = [248, 250, 252, 255];
const ALT_BACKGROUND: Rgba = [241, 245, 249, 255];
const INK: Rgba = [15, 23, 42, 255];
const ACCENTS: readonly Rgba[] = [
[30, 64, 175, 235],
[126, 34, 206, 235],
[190, 24, 93, 230],
[15, 118, 110, 230],
[194, 65, 12, 230],
];
interface TextLayout {
scale: number;
spacing: number;
startX: number;
startY: number;
textWidth: number;
textHeight: number;
}
function clamp(value: number, minimum: number, maximum: number): number {
return Math.max(minimum, Math.min(maximum, value));
}
function disturbanceRatio(context: CaptchaGeneratorContext): number {
return clamp((context.disturbance - 25) / 50, 0, 1);
}
function randomColor(
context: CaptchaGeneratorContext,
minimum = 70,
maximum = 205,
alpha = 100,
): Rgba {
return [
context.randomInt(minimum, maximum),
context.randomInt(minimum, maximum),
context.randomInt(minimum, maximum),
alpha,
];
}
function rawPixel(image: RgbaImage, x: number, y: number): readonly [number, number, number, number] {
const px = clamp(Math.round(x), 0, image.width - 1);
const py = clamp(Math.round(y), 0, image.height - 1);
const index = (py * image.width + px) * 4;
return [
image.data[index]!,
image.data[index + 1]!,
image.data[index + 2]!,
image.data[index + 3]!,
];
}
function putRawPixel(
image: RgbaImage,
x: number,
y: number,
color: readonly [number, number, number, number],
): void {
const px = Math.round(x);
const py = Math.round(y);
if (px < 0 || py < 0 || px >= image.width || py >= image.height) return;
const index = (py * image.width + px) * 4;
image.data[index] = color[0];
image.data[index + 1] = color[1];
image.data[index + 2] = color[2];
image.data[index + 3] = color[3];
}
function cloneImage(image: RgbaImage): RgbaImage {
return { width: image.width, height: image.height, data: image.data.slice() };
}
function replaceImage(target: RgbaImage, source: RgbaImage): void {
target.data.set(source.data);
}
function layoutFor(text: string, style: CaptchaConcreteImageStyle): TextLayout {
const width = 300;
const height = 104;
const scale = text.length > 9 ? 3 : text.length > 7 ? 4 : 5;
const glyphWidth = scale * 5;
const normalSpacing = scale + 3;
const spacing = style === "collision"
? Math.max(-Math.floor(scale * 0.45), -2)
: style === "cross-shadow"
? scale
: normalSpacing;
const textWidth = text.length * glyphWidth + Math.max(0, text.length - 1) * spacing;
const textHeight = scale * 7;
return {
scale,
spacing,
startX: Math.max(12, Math.floor((width - textWidth) / 2)),
startY: Math.floor((height - textHeight) / 2),
textWidth,
textHeight,
};
}
function drawCircleOutline(
image: RgbaImage,
centerX: number,
centerY: number,
radius: number,
color: Rgba,
thickness = 1,
): void {
const steps = Math.max(24, Math.round(radius * 8));
for (let index = 0; index < steps; index++) {
const angle = (index / steps) * Math.PI * 2;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
fillCircle(image, x, y, Math.max(0.75, thickness / 2), color);
}
}
function drawDashedLine(
image: RgbaImage,
x0: number,
y0: number,
x1: number,
y1: number,
color: Rgba,
dash = 5,
gap = 4,
thickness = 1,
): void {
const distance = Math.hypot(x1 - x0, y1 - y0);
if (distance <= 0) return;
const dx = (x1 - x0) / distance;
const dy = (y1 - y0) / distance;
for (let offset = 0; offset < distance; offset += dash + gap) {
const end = Math.min(distance, offset + dash);
drawLine(
image,
x0 + dx * offset,
y0 + dy * offset,
x0 + dx * end,
y0 + dy * end,
color,
thickness,
);
}
}
function addDots(
image: RgbaImage,
context: CaptchaGeneratorContext,
count: number,
alpha: number,
minimum = 75,
maximum = 220,
radius = 0,
): void {
for (let index = 0; index < count; index++) {
const x = context.randomInt(0, image.width - 1);
const y = context.randomInt(0, image.height - 1);
const color = randomColor(context, minimum, maximum, alpha);
if (radius > 0) fillCircle(image, x, y, context.randomInt(1, radius), color);
else setPixel(image, x, y, color);
}
}
function addCrossingLines(
image: RgbaImage,
context: CaptchaGeneratorContext,
count: number,
alpha: number,
thickness = 1,
): void {
for (let index = 0; index < count; index++) {
drawLine(
image,
context.randomInt(0, image.width - 1),
context.randomInt(0, image.height - 1),
context.randomInt(0, image.width - 1),
context.randomInt(0, image.height - 1),
randomColor(context, 65, 195, alpha),
thickness,
);
}
}
function drawCharacters(
image: RgbaImage,
text: string,
context: CaptchaGeneratorContext,
layout: TextLayout,
style: CaptchaConcreteImageStyle,
): void {
const ratio = disturbanceRatio(context);
const jitterX = Math.round(1 + ratio * 3);
const jitterY = Math.round(2 + ratio * 4);
const shear = 0.12 + ratio * 0.42;
let cursor = layout.startX;
for (let index = 0; index < text.length; index++) {
const character = text[index]!;
const x = cursor + context.randomInt(-jitterX, jitterX);
const y = layout.startY + context.randomInt(-jitterY, jitterY);
const glyphShear = (context.randomFloat() - 0.5) * shear;
const accent = ACCENTS[context.randomInt(0, ACCENTS.length - 1)]!;
const color = style === "collision" || style === "cross-shadow" || style === "pixel"
? accent
: INK;
if (style === "cross-shadow") {
drawGlyph(image, character, x - 3, y + 3, layout.scale, [37, 99, 235, 95], glyphShear);
drawGlyph(image, character, x + 3, y - 2, layout.scale, [220, 38, 38, 85], glyphShear);
} else if (style === "collision") {
drawGlyph(image, character, x - 2, y + 2, layout.scale, [15, 23, 42, 75], glyphShear);
if (index > 0 && context.randomFloat() < 0.65) {
drawGlyph(image, character, x + context.randomInt(-4, 1), y, layout.scale, [2, 6, 23, 60], -glyphShear);
}
} else if (context.randomFloat() < 0.28 + ratio * 0.35) {
drawGlyph(image, character, x + 1, y + 1, layout.scale, [15, 23, 42, 70], glyphShear);
}
drawGlyph(image, character, x, y, layout.scale, color, glyphShear);
if (style === "pixel" && context.randomFloat() < 0.45) {
drawGlyph(image, character, x + 1, y, layout.scale, [15, 23, 42, 90], glyphShear);
}
cursor += layout.scale * 5 + layout.spacing;
}
}
function shiftRows(
image: RgbaImage,
shiftForY: (y: number) => number,
background: Rgba = LIGHT_BACKGROUND,
): void {
const source = cloneImage(image);
const output = createImage(image.width, image.height, background);
for (let y = 0; y < image.height; y++) {
const shift = Math.round(shiftForY(y));
for (let x = 0; x < image.width; x++) {
const sourceX = x - shift;
if (sourceX >= 0 && sourceX < image.width) {
putRawPixel(output, x, y, rawPixel(source, sourceX, y));
}
}
}
replaceImage(image, output);
}
function shiftColumns(
image: RgbaImage,
shiftForX: (x: number) => number,
background: Rgba = LIGHT_BACKGROUND,
): void {
const source = cloneImage(image);
const output = createImage(image.width, image.height, background);
for (let x = 0; x < image.width; x++) {
const shift = Math.round(shiftForX(x));
for (let y = 0; y < image.height; y++) {
const sourceY = y - shift;
if (sourceY >= 0 && sourceY < image.height) {
putRawPixel(output, x, y, rawPixel(source, x, sourceY));
}
}
}
replaceImage(image, output);
}
function applyClassic(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
addCrossingLines(image, context, Math.round(3 + ratio * 5), Math.round(55 + ratio * 55), ratio > 0.7 ? 2 : 1);
addDots(image, context, Math.round(140 + ratio * 470), Math.round(45 + ratio * 50));
}
function applyCollision(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
addCrossingLines(image, context, Math.round(5 + ratio * 7), Math.round(60 + ratio * 60), ratio > 0.55 ? 2 : 1);
addDots(image, context, Math.round(120 + ratio * 320), 70);
const bars = Math.round(2 + ratio * 5);
for (let index = 0; index < bars; index++) {
fillRect(
image,
context.randomInt(0, image.width - 35),
context.randomInt(18, image.height - 20),
context.randomInt(18, 50),
context.randomInt(1, ratio > 0.65 ? 3 : 2),
randomColor(context, 35, 160, 75),
);
}
}
function applySnow(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
addDots(image, context, Math.round(420 + ratio * 950), Math.round(85 + ratio * 75), 120, 245, ratio > 0.5 ? 2 : 1);
for (let index = 0; index < Math.round(35 + ratio * 90); index++) {
const x = context.randomInt(0, image.width - 1);
const y = context.randomInt(0, image.height - 1);
drawLine(image, x - 2, y, x + 2, y, [255, 255, 255, 185], 1);
drawLine(image, x, y - 2, x, y + 2, [255, 255, 255, 185], 1);
}
addCrossingLines(image, context, Math.round(2 + ratio * 4), 55, 1);
}
function applyCorrosion(
image: RgbaImage,
context: CaptchaGeneratorContext,
layout: TextLayout,
): void {
const ratio = disturbanceRatio(context);
const holes = Math.round(70 + ratio * 230);
for (let index = 0; index < holes; index++) {
const x = context.randomInt(layout.startX - 5, layout.startX + layout.textWidth + 5);
const y = context.randomInt(layout.startY - 5, layout.startY + layout.textHeight + 5);
const size = context.randomInt(1, ratio > 0.65 ? 4 : 3);
fillRect(image, x, y, size, size, context.randomFloat() < 0.6 ? LIGHT_BACKGROUND : [203, 213, 225, 210]);
}
addDots(image, context, Math.round(170 + ratio * 430), 85, 90, 190, 2);
addCrossingLines(image, context, Math.round(2 + ratio * 4), 60, 1);
}
function applySpiderweb(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const nodes = Array.from({ length: Math.round(8 + ratio * 12) }, () => ({
x: context.randomInt(0, image.width - 1),
y: context.randomInt(0, image.height - 1),
}));
for (let index = 0; index < nodes.length; index++) {
const current = nodes[index]!;
const next = nodes[(index + context.randomInt(1, Math.max(1, nodes.length - 1))) % nodes.length]!;
drawLine(image, current.x, current.y, next.x, next.y, [71, 85, 105, Math.round(65 + ratio * 65)], 1);
if (index % 2 === 0) drawCircleOutline(image, current.x, current.y, context.randomInt(2, 5), [100, 116, 139, 70], 1);
}
const anchorX = context.randomInt(Math.floor(image.width * 0.25), Math.floor(image.width * 0.75));
const anchorY = context.randomInt(Math.floor(image.height * 0.25), Math.floor(image.height * 0.75));
for (let index = 0; index < Math.round(7 + ratio * 7); index++) {
const angle = (index / Math.round(7 + ratio * 7)) * Math.PI * 2;
drawLine(
image,
anchorX,
anchorY,
anchorX + Math.cos(angle) * image.width,
anchorY + Math.sin(angle) * image.height,
[71, 85, 105, 65],
1,
);
}
addDots(image, context, Math.round(80 + ratio * 200), 60);
}
function applyCrossShadow(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const centerY = Math.floor(image.height / 2);
drawLine(image, 0, centerY - 6, image.width - 1, centerY + 6, [37, 99, 235, 75], ratio > 0.55 ? 2 : 1);
drawLine(image, 0, centerY + 7, image.width - 1, centerY - 8, [220, 38, 38, 65], ratio > 0.55 ? 2 : 1);
addCrossingLines(image, context, Math.round(2 + ratio * 5), 55, 1);
addDots(image, context, Math.round(120 + ratio * 300), 60);
}
function applySplit(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const bandHeight = context.randomInt(7, 13);
const amplitude = Math.round(4 + ratio * 13);
shiftRows(image, (y) => {
const band = Math.floor(y / bandHeight);
return band % 2 === 0 ? amplitude : -amplitude;
});
for (let y = bandHeight; y < image.height; y += bandHeight) {
drawLine(image, 0, y, image.width - 1, y, [100, 116, 139, 75], 1);
}
addDots(image, context, Math.round(90 + ratio * 250), 65);
}
function applySplit2(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const stripWidth = context.randomInt(10, 18);
const amplitude = Math.round(3 + ratio * 10);
shiftColumns(image, (x) => {
const strip = Math.floor(x / stripWidth);
return strip % 2 === 0 ? amplitude : -amplitude;
});
for (let x = stripWidth; x < image.width; x += stripWidth) {
drawLine(image, x, 0, x, image.height - 1, [100, 116, 139, 65], 1);
}
addCrossingLines(image, context, Math.round(2 + ratio * 4), 55, 1);
}
function applyCut(
image: RgbaImage,
context: CaptchaGeneratorContext,
layout: TextLayout,
): void {
const ratio = disturbanceRatio(context);
const cuts = Math.round(4 + ratio * 8);
for (let index = 0; index < cuts; index++) {
const x0 = context.randomInt(layout.startX - 10, layout.startX + layout.textWidth);
const y0 = context.randomInt(layout.startY - 5, layout.startY + layout.textHeight + 5);
const x1 = x0 + context.randomInt(24, 72);
const y1 = y0 + context.randomInt(-18, 18);
drawLine(image, x0, y0, x1, y1, [248, 250, 252, 245], ratio > 0.6 ? 3 : 2);
drawLine(image, x0, y0 + 2, x1, y1 + 2, randomColor(context, 75, 170, 80), 1);
}
addDots(image, context, Math.round(90 + ratio * 260), 70);
}
function applyDarts(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const targets = Math.round(2 + ratio * 3);
for (let index = 0; index < targets; index++) {
const x = context.randomInt(25, image.width - 25);
const y = context.randomInt(16, image.height - 16);
const maxRadius = context.randomInt(9, Math.round(14 + ratio * 11));
for (let radius = maxRadius; radius >= 4; radius -= 5) {
drawCircleOutline(image, x, y, radius, randomColor(context, 55, 175, 70), 1);
}
for (let ray = 0; ray < Math.round(3 + ratio * 4); ray++) {
const angle = context.randomFloat() * Math.PI * 2;
drawLine(
image,
x,
y,
x + Math.cos(angle) * context.randomInt(25, 70),
y + Math.sin(angle) * context.randomInt(18, 55),
randomColor(context, 55, 175, 70),
1,
);
}
}
addDots(image, context, Math.round(100 + ratio * 240), 60);
}
function applyDistortion(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const amplitudeX = 3 + ratio * 9;
const amplitudeY = 2 + ratio * 5;
const phase = context.randomFloat() * Math.PI * 2;
shiftRows(image, (y) => Math.sin(y / (7 + ratio * 4) + phase) * amplitudeX);
shiftColumns(image, (x) => Math.sin(x / (20 - ratio * 6) + phase) * amplitudeY);
addCrossingLines(image, context, Math.round(3 + ratio * 5), 60, ratio > 0.7 ? 2 : 1);
addDots(image, context, Math.round(100 + ratio * 280), 60);
}
function applyStitch(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const rows = Math.round(4 + ratio * 5);
for (let index = 0; index < rows; index++) {
const y = Math.round(((index + 1) / (rows + 1)) * image.height);
drawDashedLine(image, 0, y, image.width - 1, y + context.randomInt(-3, 3), [71, 85, 105, 75], 4, 4, 1);
}
const columns = Math.round(2 + ratio * 4);
for (let index = 0; index < columns; index++) {
const x = context.randomInt(10, image.width - 10);
drawDashedLine(image, x, 0, x + context.randomInt(-5, 5), image.height - 1, [100, 116, 139, 65], 3, 5, 1);
}
for (let index = 0; index < Math.round(18 + ratio * 35); index++) {
const x = context.randomInt(0, image.width - 1);
const y = context.randomInt(0, image.height - 1);
drawLine(image, x - 2, y - 2, x + 2, y + 2, [100, 116, 139, 65], 1);
drawLine(image, x + 2, y - 2, x - 2, y + 2, [100, 116, 139, 65], 1);
}
}
function applyStriped(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const spacing = Math.round(10 - ratio * 4);
const offset = context.randomInt(-image.height, image.width);
for (let x = offset; x < image.width + image.height; x += spacing) {
drawLine(image, x, 0, x - image.height, image.height - 1, [71, 85, 105, Math.round(45 + ratio * 45)], ratio > 0.7 ? 2 : 1);
}
for (let y = context.randomInt(3, 9); y < image.height; y += context.randomInt(8, 14)) {
drawLine(image, 0, y, image.width - 1, y, [148, 163, 184, 45], 1);
}
addDots(image, context, Math.round(70 + ratio * 180), 55);
}
function applyWave(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const phase = context.randomFloat() * Math.PI * 2;
const amplitude = 5 + ratio * 13;
shiftRows(image, (y) => Math.sin(y / (5.5 + ratio * 3) + phase) * amplitude);
shiftColumns(image, (x) => Math.cos(x / (24 - ratio * 7) + phase) * (2 + ratio * 6));
for (let index = 0; index < Math.round(2 + ratio * 3); index++) {
const baseY = context.randomInt(10, image.height - 10);
let previousX = 0;
let previousY = baseY;
for (let x = 8; x < image.width; x += 8) {
const y = baseY + Math.sin(x / 17 + phase + index) * (5 + ratio * 5);
drawLine(image, previousX, previousY, x, y, randomColor(context, 75, 180, 55), 1);
previousX = x;
previousY = y;
}
}
}
function applyGridNoise(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const cell = Math.round(16 - ratio * 6);
for (let x = 0; x < image.width; x += cell) {
drawLine(image, x, 0, x, image.height - 1, [100, 116, 139, 55], 1);
}
for (let y = 0; y < image.height; y += cell) {
drawLine(image, 0, y, image.width - 1, y, [100, 116, 139, 55], 1);
}
for (let index = 0; index < Math.round(10 + ratio * 26); index++) {
const x = context.randomInt(0, Math.floor(image.width / cell)) * cell;
const y = context.randomInt(0, Math.floor(image.height / cell)) * cell;
fillRect(image, x, y, cell, cell, randomColor(context, 100, 220, Math.round(25 + ratio * 35)));
}
}
function applyScribble(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const scribbles = Math.round(3 + ratio * 6);
for (let index = 0; index < scribbles; index++) {
let x = context.randomInt(0, image.width - 1);
let y = context.randomInt(0, image.height - 1);
const segments = context.randomInt(7, Math.round(12 + ratio * 13));
const color = randomColor(context, 45, 180, Math.round(55 + ratio * 45));
for (let segment = 0; segment < segments; segment++) {
const nextX = clamp(x + context.randomInt(-28, 28), 0, image.width - 1);
const nextY = clamp(y + context.randomInt(-18, 18), 0, image.height - 1);
drawLine(image, x, y, nextX, nextY, color, ratio > 0.7 && segment % 3 === 0 ? 2 : 1);
x = nextX;
y = nextY;
}
}
addDots(image, context, Math.round(80 + ratio * 220), 55);
}
function applyPixel(image: RgbaImage, context: CaptchaGeneratorContext): void {
const ratio = disturbanceRatio(context);
const blocks = Math.round(28 + ratio * 75);
for (let index = 0; index < blocks; index++) {
const size = context.randomInt(2, Math.round(4 + ratio * 5));
fillRect(
image,
context.randomInt(0, image.width - size),
context.randomInt(0, image.height - size),
size,
size,
randomColor(context, 65, 220, Math.round(45 + ratio * 45)),
);
}
const source = cloneImage(image);
for (let index = 0; index < Math.round(4 + ratio * 8); index++) {
const blockWidth = context.randomInt(12, 28);
const blockHeight = context.randomInt(6, 16);
const x = context.randomInt(0, image.width - blockWidth);
const y = context.randomInt(0, image.height - blockHeight);
const shift = context.randomInt(-8, 8);
for (let py = 0; py < blockHeight; py++) {
for (let px = 0; px < blockWidth; px++) {
const sourceX = clamp(x + px, 0, image.width - 1);
const targetX = x + px + shift;
putRawPixel(image, targetX, y + py, rawPixel(source, sourceX, y + py));
}
}
}
}
function applyBrokenLines(
image: RgbaImage,
context: CaptchaGeneratorContext,
layout: TextLayout,
): void {
const ratio = disturbanceRatio(context);
const gaps = Math.round(12 + ratio * 35);
for (let index = 0; index < gaps; index++) {
const horizontal = context.randomFloat() < 0.7;
const width = horizontal ? context.randomInt(6, 24) : context.randomInt(1, 4);
const height = horizontal ? context.randomInt(1, 3) : context.randomInt(7, 18);
fillRect(
image,
context.randomInt(layout.startX - 3, layout.startX + layout.textWidth),
context.randomInt(layout.startY - 3, layout.startY + layout.textHeight),
width,
height,
LIGHT_BACKGROUND,
);
}
for (let index = 0; index < Math.round(12 + ratio * 28); index++) {
const x = context.randomInt(0, image.width - 12);
const y = context.randomInt(0, image.height - 1);
drawLine(image, x, y, x + context.randomInt(4, 18), y + context.randomInt(-2, 2), randomColor(context, 45, 175, 75), 1);
}
addDots(image, context, Math.round(70 + ratio * 180), 60);
}
function applyStyle(
image: RgbaImage,
context: CaptchaGeneratorContext,
layout: TextLayout,
): void {
switch (context.imageStyle) {
case "collision":
applyCollision(image, context);
break;
case "snow":
applySnow(image, context);
break;
case "corrosion":
applyCorrosion(image, context, layout);
break;
case "spiderweb":
applySpiderweb(image, context);
break;
case "cross-shadow":
applyCrossShadow(image, context);
break;
case "split":
applySplit(image, context);
break;
case "split2":
applySplit2(image, context);
break;
case "cut":
applyCut(image, context, layout);
break;
case "darts":
applyDarts(image, context);
break;
case "distortion":
applyDistortion(image, context);
break;
case "stitch":
applyStitch(image, context);
break;
case "striped":
applyStriped(image, context);
break;
case "wave":
applyWave(image, context);
break;
case "grid-noise":
applyGridNoise(image, context);
break;
case "scribble":
applyScribble(image, context);
break;
case "pixel":
applyPixel(image, context);
break;
case "broken-lines":
applyBrokenLines(image, context, layout);
break;
default:
applyClassic(image, context);
}
}
function backgroundFor(style: CaptchaConcreteImageStyle): Rgba {
if (style === "snow" || style === "grid-noise" || style === "stitch") return ALT_BACKGROUND;
return LIGHT_BACKGROUND;
}
export function renderTextChallenge(
text: string,
context: CaptchaGeneratorContext,
): string {
const layout = layoutFor(text, context.imageStyle);
const image = createImage(300, 104, backgroundFor(context.imageStyle));
if (context.imageStyle === "grid-noise") applyGridNoise(image, context);
else if (context.imageStyle === "snow") addDots(image, context, 130, 45, 160, 235, 1);
else addDots(image, context, Math.round(35 + disturbanceRatio(context) * 80), 30);
drawCharacters(image, text, context, layout, context.imageStyle);
applyStyle(image, context, layout);
if (context.imageStyle !== "snow" && context.imageStyle !== "grid-noise") {
const ratio = disturbanceRatio(context);
addDots(image, context, Math.round(25 + ratio * 100), 35, 100, 220);
}
return pngDataUri(image);
}