release: WRNexusJS 0.4.0

This commit is contained in:
2026-07-27 12:42:18 +05:30
parent 8b728a3e5d
commit 30e5721e84
250 changed files with 10065 additions and 3923 deletions
@@ -51,7 +51,13 @@ export class CalculationCaptchaGenerator implements CaptchaChallengeGenerator {
answerKind: "text",
image: renderTextChallenge(expression, context),
inputMode: "numeric",
audioSequence: ["what", "is", ...numberTokens(left), ...(OPERATOR_WORDS[operator] ?? []), ...numberTokens(right)],
audioSequence: [
"what",
"is",
...numberTokens(left),
...(OPERATOR_WORDS[operator] ?? []),
...numberTokens(right),
],
metadata: { operator, imageStyle: context.imageStyle },
};
}
+45 -11
View File
@@ -4,7 +4,15 @@ import type {
CaptchaImageItem,
GeneratedCaptchaChallenge,
} from "../types.ts";
import { createImage, drawLine, fillCircle, fillPolygon, fillRect, pngDataUri, setPixel } from "./png.ts";
import {
createImage,
drawLine,
fillCircle,
fillPolygon,
fillRect,
pngDataUri,
setPixel,
} from "./png.ts";
const SHAPES = ["circle", "square", "triangle", "diamond", "star"] as const;
type Shape = (typeof SHAPES)[number];
@@ -39,9 +47,26 @@ function shapeImage(shape: Shape, context: CaptchaGeneratorContext): string {
if (shape === "circle") fillCircle(image, cx, cy, size, color);
else if (shape === "square") fillRect(image, cx - size, cy - size, size * 2, size * 2, color);
else if (shape === "triangle") {
fillPolygon(image, [[cx, cy - size], [cx - size, cy + size], [cx + size, cy + size]], color);
fillPolygon(
image,
[
[cx, cy - size],
[cx - size, cy + size],
[cx + size, cy + size],
],
color,
);
} else if (shape === "diamond") {
fillPolygon(image, [[cx, cy - size], [cx - size, cy], [cx, cy + size], [cx + size, cy]], color);
fillPolygon(
image,
[
[cx, cy - size],
[cx - size, cy],
[cx, cy + size],
[cx + size, cy],
],
color,
);
} else {
fillPolygon(image, starPoints(cx, cy, size, size * 0.45), color);
}
@@ -49,12 +74,12 @@ function shapeImage(shape: Shape, context: CaptchaGeneratorContext): string {
const ratio = disturbanceRatio(context);
const dots = Math.round(35 + ratio * 150);
for (let index = 0; index < dots; index++) {
setPixel(
image,
context.randomInt(0, 95),
context.randomInt(0, 71),
[context.randomInt(105, 225), context.randomInt(105, 225), context.randomInt(105, 225), Math.round(55 + ratio * 65)],
);
setPixel(image, context.randomInt(0, 95), context.randomInt(0, 71), [
context.randomInt(105, 225),
context.randomInt(105, 225),
context.randomInt(105, 225),
Math.round(55 + ratio * 65),
]);
}
const lines = Math.round(1 + ratio * 4);
@@ -65,7 +90,12 @@ function shapeImage(shape: Shape, context: CaptchaGeneratorContext): string {
context.randomInt(0, 71),
context.randomInt(0, 95),
context.randomInt(0, 71),
[context.randomInt(100, 210), context.randomInt(100, 210), context.randomInt(100, 210), Math.round(45 + ratio * 55)],
[
context.randomInt(100, 210),
context.randomInt(100, 210),
context.randomInt(100, 210),
Math.round(45 + ratio * 55),
],
ratio > 0.75 ? 2 : 1,
);
}
@@ -105,7 +135,11 @@ export class ImageCaptchaGenerator implements CaptchaChallengeGenerator {
}
shuffle(entries, context);
const answer = entries.filter((entry) => entry.shape === target).map((entry) => entry.id).sort().join(",");
const answer = entries
.filter((entry) => entry.shape === target)
.map((entry) => entry.id)
.sort()
.join(",");
return {
type: "image",
presentation: "visual",
+12 -3
View File
@@ -1,8 +1,16 @@
import type { CaptchaChallengeGenerator } from "../types.ts";
import { alphaCaptchaGenerator, alphanumericCaptchaGenerator, numberCaptchaGenerator } from "./text.ts";
import {
alphaCaptchaGenerator,
alphanumericCaptchaGenerator,
numberCaptchaGenerator,
} from "./text.ts";
import { calculationCaptchaGenerator } from "./calculation.ts";
import { imageCaptchaGenerator } from "./image.ts";
import { honeypotCaptchaGenerator, notRobotCaptchaGenerator, timingCaptchaGenerator } from "./invisible.ts";
import {
honeypotCaptchaGenerator,
notRobotCaptchaGenerator,
timingCaptchaGenerator,
} from "./invisible.ts";
export * from "./text.ts";
export * from "./calculation.ts";
@@ -13,7 +21,8 @@ export * from "./styles.ts";
export function defineCaptchaGenerator<T extends CaptchaChallengeGenerator>(generator: T): T {
if (!generator.type) throw new TypeError("CAPTCHA generator requires a stable type");
if (typeof generator.generate !== "function") throw new TypeError("CAPTCHA generator requires generate()");
if (typeof generator.generate !== "function")
throw new TypeError("CAPTCHA generator requires generate()");
return generator;
}
+2 -1
View File
@@ -19,7 +19,8 @@ export class InvisibleCaptchaGenerator implements CaptchaChallengeGenerator {
return {
type: this.type,
presentation: "invisible",
prompt: this.type === "not-robot" ? "Confirm that you are not a robot" : "Automated abuse check",
prompt:
this.type === "not-robot" ? "Confirm that you are not a robot" : "Automated abuse check",
answer: JSON.stringify({ honeypot: "", timingToken }),
answerKind: "invisible",
inputMode: "none",
+72 -12
View File
@@ -8,7 +8,11 @@ export interface RgbaImage {
export type Rgba = readonly [number, number, number, number?];
export function createImage(width: number, height: number, background: Rgba = [255, 255, 255, 255]): RgbaImage {
export function createImage(
width: number,
height: number,
background: Rgba = [255, 255, 255, 255],
): RgbaImage {
const data = new Uint8Array(width * height * 4);
const alpha = background[3] ?? 255;
for (let index = 0; index < data.length; index += 4) {
@@ -33,13 +37,28 @@ export function setPixel(image: RgbaImage, x: number, y: number, color: Rgba): v
image.data[index + 3] = 255;
}
export function fillRect(image: RgbaImage, x: number, y: number, width: number, height: number, color: Rgba): void {
export function fillRect(
image: RgbaImage,
x: number,
y: number,
width: number,
height: number,
color: Rgba,
): void {
for (let py = Math.floor(y); py < Math.ceil(y + height); py++) {
for (let px = Math.floor(x); px < Math.ceil(x + width); px++) setPixel(image, px, py, color);
}
}
export function drawLine(image: RgbaImage, x0: number, y0: number, x1: number, y1: number, color: Rgba, thickness = 1): void {
export function drawLine(
image: RgbaImage,
x0: number,
y0: number,
x1: number,
y1: number,
color: Rgba,
thickness = 1,
): void {
let x = Math.round(x0);
let y = Math.round(y0);
const targetX = Math.round(x1);
@@ -50,7 +69,14 @@ export function drawLine(image: RgbaImage, x0: number, y0: number, x1: number, y
const sy = y < targetY ? 1 : -1;
let error = dx + dy;
while (true) {
fillRect(image, x - Math.floor(thickness / 2), y - Math.floor(thickness / 2), thickness, thickness, color);
fillRect(
image,
x - Math.floor(thickness / 2),
y - Math.floor(thickness / 2),
thickness,
thickness,
color,
);
if (x === targetX && y === targetY) break;
const twice = 2 * error;
if (twice >= dy) {
@@ -64,7 +90,13 @@ export function drawLine(image: RgbaImage, x0: number, y0: number, x1: number, y
}
}
export function fillCircle(image: RgbaImage, centerX: number, centerY: number, radius: number, color: Rgba): void {
export function fillCircle(
image: RgbaImage,
centerX: number,
centerY: number,
radius: number,
color: Rgba,
): void {
const r2 = radius * radius;
for (let y = Math.floor(centerY - radius); y <= Math.ceil(centerY + radius); y++) {
for (let x = Math.floor(centerX - radius); x <= Math.ceil(centerX + radius); x++) {
@@ -75,7 +107,11 @@ export function fillCircle(image: RgbaImage, centerX: number, centerY: number, r
}
}
function pointInPolygon(x: number, y: number, points: readonly (readonly [number, number])[]): boolean {
function pointInPolygon(
x: number,
y: number,
points: readonly (readonly [number, number])[],
): boolean {
let inside = false;
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
const xi = points[i]![0];
@@ -88,13 +124,18 @@ function pointInPolygon(x: number, y: number, points: readonly (readonly [number
return inside;
}
export function fillPolygon(image: RgbaImage, points: readonly (readonly [number, number])[], color: Rgba): void {
export function fillPolygon(
image: RgbaImage,
points: readonly (readonly [number, number])[],
color: Rgba,
): void {
const minX = Math.floor(Math.min(...points.map((point) => point[0])));
const maxX = Math.ceil(Math.max(...points.map((point) => point[0])));
const minY = Math.floor(Math.min(...points.map((point) => point[1])));
const maxY = Math.ceil(Math.max(...points.map((point) => point[1])));
for (let y = minY; y <= maxY; y++) {
for (let x = minX; x <= maxX; x++) if (pointInPolygon(x + 0.5, y + 0.5, points)) setPixel(image, x, y, color);
for (let x = minX; x <= maxX; x++)
if (pointInPolygon(x + 0.5, y + 0.5, points)) setPixel(image, x, y, color);
}
}
@@ -134,7 +175,15 @@ export function drawText(
let cursor = options.x;
for (let index = 0; index < text.length; index++) {
const jitter = options.jitter?.(index) ?? { x: 0, y: 0, shear: 0 };
drawGlyph(image, text[index]!, cursor + jitter.x, options.y + jitter.y, options.scale, options.color, jitter.shear);
drawGlyph(
image,
text[index]!,
cursor + jitter.x,
options.y + jitter.y,
options.scale,
options.color,
jitter.shear,
);
cursor += options.scale * 5 + spacing;
}
}
@@ -170,7 +219,12 @@ function adler32(bytes: Uint8Array): number {
}
function u32(value: number): Uint8Array {
return Uint8Array.of((value >>> 24) & 255, (value >>> 16) & 255, (value >>> 8) & 255, value & 255);
return Uint8Array.of(
(value >>> 24) & 255,
(value >>> 16) & 255,
(value >>> 8) & 255,
value & 255,
);
}
function concat(parts: readonly Uint8Array[]): Uint8Array {
@@ -195,9 +249,15 @@ function deflateStored(data: Uint8Array): Uint8Array {
const size = Math.min(65535, data.length - offset);
const final = offset + size >= data.length;
const length = size;
const inverse = (~length) & 0xffff;
const inverse = ~length & 0xffff;
blocks.push(
Uint8Array.of(final ? 1 : 0, length & 255, (length >>> 8) & 255, inverse & 255, (inverse >>> 8) & 255),
Uint8Array.of(
final ? 1 : 0,
length & 255,
(length >>> 8) & 255,
inverse & 255,
(inverse >>> 8) & 255,
),
data.slice(offset, offset + size),
);
}
+6 -20
View File
@@ -1,7 +1,4 @@
import type {
CaptchaConcreteImageStyle,
CaptchaImageStyle,
} from "../types.ts";
import type { CaptchaConcreteImageStyle, CaptchaImageStyle } from "../types.ts";
export const CAPTCHA_CONCRETE_IMAGE_STYLES = [
"classic",
@@ -52,9 +49,7 @@ export function normalizeCaptchaImageStyle(
if (value === undefined || value === null || value === "") return fallback;
const normalized = String(value).trim().toLowerCase();
if (!STYLE_SET.has(normalized)) {
throw new RangeError(
`imageStyle must be one of: ${CAPTCHA_IMAGE_STYLES.join(", ")}`,
);
throw new RangeError(`imageStyle must be one of: ${CAPTCHA_IMAGE_STYLES.join(", ")}`);
}
return normalized as CaptchaImageStyle;
}
@@ -67,9 +62,7 @@ export function normalizeCaptchaImageStyleList(
for (const entry of styleValues(value)) {
if (entry === "random") continue;
if (!CONCRETE_STYLE_SET.has(entry)) {
throw new RangeError(
`${name} contains an unknown image style: ${entry}`,
);
throw new RangeError(`${name} contains an unknown image style: ${entry}`);
}
const style = entry as CaptchaConcreteImageStyle;
if (!styles.includes(style)) styles.push(style);
@@ -95,17 +88,12 @@ export function resolveCaptchaImageStyle(
options: ResolveCaptchaImageStyleOptions,
): ResolvedCaptchaImageStyle {
const requested = normalizeCaptchaImageStyle(options.imageStyle, "random");
const allowed = normalizeCaptchaImageStyleList(
options.allowedStyles,
"allowedStyles",
);
const allowed = normalizeCaptchaImageStyleList(options.allowedStyles, "allowedStyles");
const excluded = new Set(
normalizeCaptchaImageStyleList(options.excludedStyles, "excludedStyles"),
);
const source = allowed.length
? allowed
: [...CAPTCHA_CONCRETE_IMAGE_STYLES];
const source = allowed.length ? allowed : [...CAPTCHA_CONCRETE_IMAGE_STYLES];
const pool = source.filter((style) => !excluded.has(style));
if (!pool.length) {
@@ -116,9 +104,7 @@ export function resolveCaptchaImageStyle(
if (!options.randomizeStyle && requested !== "random") {
if (!pool.includes(requested)) {
throw new RangeError(
`imageStyle ${requested} is not available in the configured style pool`,
);
throw new RangeError(`imageStyle ${requested} is not available in the configured style pool`);
}
return { requested, resolved: requested, pool };
}
+9 -3
View File
@@ -11,7 +11,9 @@ const ALPHA = "ABCDEFGHJKMNPQRSTUVWXYZ";
const ALPHANUMERIC = `${ALPHA}${NUMBERS}`;
function defaultLength(context: CaptchaGeneratorContext): number {
return context.length ?? (context.difficulty === "easy" ? 4 : context.difficulty === "hard" ? 7 : 6);
return (
context.length ?? (context.difficulty === "easy" ? 4 : context.difficulty === "hard" ? 7 : 6)
);
}
function charset(type: CaptchaChallengeType): string {
@@ -20,11 +22,15 @@ function charset(type: CaptchaChallengeType): string {
return ALPHANUMERIC;
}
function generateText(type: "number" | "alpha" | "alphanumeric", context: CaptchaGeneratorContext): GeneratedCaptchaChallenge {
function generateText(
type: "number" | "alpha" | "alphanumeric",
context: CaptchaGeneratorContext,
): GeneratedCaptchaChallenge {
const source = charset(type);
const length = Math.max(3, Math.min(10, defaultLength(context)));
let answer = "";
for (let index = 0; index < length; index++) answer += source[context.randomInt(0, source.length - 1)];
for (let index = 0; index < length; index++)
answer += source[context.randomInt(0, source.length - 1)];
return {
type,
presentation: "visual",
+139 -42
View File
@@ -1,7 +1,4 @@
import type {
CaptchaConcreteImageStyle,
CaptchaGeneratorContext,
} from "../types.ts";
import type { CaptchaConcreteImageStyle, CaptchaGeneratorContext } from "../types.ts";
import {
createImage,
drawGlyph,
@@ -56,7 +53,11 @@ function randomColor(
];
}
function rawPixel(image: RgbaImage, x: number, y: number): readonly [number, number, number, number] {
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;
@@ -98,11 +99,12 @@ function layoutFor(text: string, style: CaptchaConcreteImageStyle): TextLayout {
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 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 {
@@ -218,9 +220,8 @@ function drawCharacters(
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;
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);
@@ -228,7 +229,15 @@ function drawCharacters(
} 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);
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);
@@ -283,13 +292,25 @@ function shiftColumns(
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);
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);
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++) {
@@ -306,7 +327,15 @@ function applyCollision(image: RgbaImage, context: CaptchaGeneratorContext): voi
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);
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);
@@ -327,7 +356,14 @@ function applyCorrosion(
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]);
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);
@@ -341,12 +377,32 @@ function applySpiderweb(image: RgbaImage, context: CaptchaGeneratorContext): voi
}));
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 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));
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(
@@ -365,8 +421,24 @@ function applySpiderweb(image: RgbaImage, context: CaptchaGeneratorContext): voi
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);
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);
}
@@ -399,11 +471,7 @@ function applySplit2(image: RgbaImage, context: CaptchaGeneratorContext): void {
addCrossingLines(image, context, Math.round(2 + ratio * 4), 55, 1);
}
function applyCut(
image: RgbaImage,
context: CaptchaGeneratorContext,
layout: TextLayout,
): void {
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++) {
@@ -459,12 +527,32 @@ function applyStitch(image: RgbaImage, context: CaptchaGeneratorContext): void {
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);
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);
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);
@@ -479,7 +567,15 @@ function applyStriped(image: RgbaImage, context: CaptchaGeneratorContext): void
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);
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);
@@ -595,16 +691,20 @@ function applyBrokenLines(
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);
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 {
function applyStyle(image: RgbaImage, context: CaptchaGeneratorContext, layout: TextLayout): void {
switch (context.imageStyle) {
case "collision":
applyCollision(image, context);
@@ -667,10 +767,7 @@ function backgroundFor(style: CaptchaConcreteImageStyle): Rgba {
return LIGHT_BACKGROUND;
}
export function renderTextChallenge(
text: string,
context: CaptchaGeneratorContext,
): string {
export function renderTextChallenge(text: string, context: CaptchaGeneratorContext): string {
const layout = layoutFor(text, context.imageStyle);
const image = createImage(300, 104, backgroundFor(context.imageStyle));