captcha: show a random image, preload backgrounds into memory
|
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/captcha/bg/A Trail of Footprints In The Sand.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/captcha/bg/Ashim DSilva.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/captcha/bg/Canazei Granite Ridges.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
src/assets/captcha/bg/Martin Adams.jpg
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
src/assets/captcha/bg/Morskie Oko.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/assets/captcha/bg/Mr. Lee.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/captcha/bg/Nattu Adnan.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/captcha/bg/Photo by SpaceX.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/captcha/bg/Photo of Valley.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/assets/captcha/bg/Snow-Capped Mountain.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/captcha/bg/Sunset by the Pier.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/captcha/bg/Tj Holowaychuk.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/assets/captcha/bg/Viktor Forgacs.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
22
src/assets/captcha/bg/copyright.txt
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
Unsplash photos published before June 8, 2017 are CC0 (public domain):
|
||||||
|
|
||||||
|
Ashim D'Silva <https://unsplash.com/photos/WeYamle9fDM>
|
||||||
|
Canazei Granite Ridges <https://unsplash.com/photos/yrwpJwDNSHE>
|
||||||
|
Mr. Lee <https://unsplash.com/photos/v7r8kZStqFw>
|
||||||
|
Photo by SpaceX <https://unsplash.com/photos/VBNb52J8Trk>
|
||||||
|
Sunset by the Pier <https://unsplash.com/photos/ces8_Bo7bhQ>
|
||||||
|
|
||||||
|
Unsplash photos published on or after June 8, 2017 are free to use, modify, and redistribute subject to the Unsplash license <https://unsplash.com/license>:
|
||||||
|
|
||||||
|
Martin Adams <https://unsplash.com/photos/MpTdvXlAsVE>
|
||||||
|
Morskie Oko <https://unsplash.com/photos/_1UF_3TlKcQ>
|
||||||
|
Nattu Adnan <https://unsplash.com/photos/Ai2TRdvI6gM>
|
||||||
|
Tj Holowaychuk <https://unsplash.com/photos/iGrsa9rL11o>
|
||||||
|
Viktor Forgacs <https://unsplash.com/photos/q8XSCZYh6D8>
|
||||||
|
“A Large Body of Water Surrounded By Mountains” by Peter Thomas <https://unsplash.com/photos/Dxod5pdRtsk>
|
||||||
|
“A Trail of Footprints In The Sand” by David Emrich <https://unsplash.com/photos/A9mr3TPoj0k>
|
||||||
|
“Photo of Valley” by Aniket Doele <https://unsplash.com/photos/M6XC789HLe8>
|
||||||
|
|
||||||
|
Pexels photos are free to use, modify, and redistribute subject to the Pexels license <https://www.pexels.com/license/>:
|
||||||
|
|
||||||
|
Snow-Capped Mountain <https://www.pexels.com/photo/photo-of-snow-capped-mountain-during-evening-2440024/>
|
||||||
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 997 B |
|
|
@ -1,4 +1,4 @@
|
||||||
import { createCanvas, loadImage } from '@gfx/canvas-wasm';
|
import { createCanvas, Image, loadImage } from '@gfx/canvas-wasm';
|
||||||
import TTLCache from '@isaacs/ttlcache';
|
import TTLCache from '@isaacs/ttlcache';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
|
@ -17,16 +17,15 @@ interface Dimensions {
|
||||||
}
|
}
|
||||||
|
|
||||||
const captchas = new TTLCache<string, Point>();
|
const captchas = new TTLCache<string, Point>();
|
||||||
|
const imagesAsync = getImages();
|
||||||
|
|
||||||
const BG_SIZE = { w: 370, h: 400 };
|
const BG_SIZE = { w: 370, h: 400 };
|
||||||
const PUZZLE_SIZE = { w: 65, h: 65 };
|
const PUZZLE_SIZE = { w: 65, h: 65 };
|
||||||
|
|
||||||
/** Puzzle captcha controller. */
|
/** Puzzle captcha controller. */
|
||||||
export const captchaController: AppController = async (c) => {
|
export const captchaController: AppController = async (c) => {
|
||||||
const { bg, puzzle, solution } = await generateCaptcha(
|
const { bg, puzzle, solution } = generateCaptcha(
|
||||||
await Deno.readFile(new URL('../../assets/captcha/bg/tj-holowaychuk.jpg', import.meta.url)),
|
await imagesAsync,
|
||||||
await Deno.readFile(new URL('../../assets/captcha/puzzle-mask.png', import.meta.url)),
|
|
||||||
await Deno.readFile(new URL('../../assets/captcha/puzzle-hole.png', import.meta.url)),
|
|
||||||
BG_SIZE,
|
BG_SIZE,
|
||||||
PUZZLE_SIZE,
|
PUZZLE_SIZE,
|
||||||
);
|
);
|
||||||
|
|
@ -47,11 +46,44 @@ export const captchaController: AppController = async (c) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface CaptchaImages {
|
||||||
|
bgImages: Image[];
|
||||||
|
puzzleMask: Image;
|
||||||
|
puzzleHole: Image;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getImages(): Promise<CaptchaImages> {
|
||||||
|
const bgImages = await getBackgroundImages();
|
||||||
|
|
||||||
|
const puzzleMask = await loadImage(
|
||||||
|
await Deno.readFile(new URL('../../assets/captcha/puzzle-mask.png', import.meta.url)),
|
||||||
|
);
|
||||||
|
const puzzleHole = await loadImage(
|
||||||
|
await Deno.readFile(new URL('../../assets/captcha/puzzle-hole.png', import.meta.url)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { bgImages, puzzleMask, puzzleHole };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBackgroundImages(): Promise<Image[]> {
|
||||||
|
const path = new URL('../../assets/captcha/bg/', import.meta.url);
|
||||||
|
|
||||||
|
const images: Image[] = [];
|
||||||
|
|
||||||
|
for await (const dirEntry of Deno.readDir(path)) {
|
||||||
|
if (dirEntry.isFile && dirEntry.name.endsWith('.jpg')) {
|
||||||
|
const file = await Deno.readFile(new URL(dirEntry.name, path));
|
||||||
|
const image = await loadImage(file);
|
||||||
|
images.push(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return images;
|
||||||
|
}
|
||||||
|
|
||||||
/** Generate a puzzle captcha, returning canvases for the board and piece. */
|
/** Generate a puzzle captcha, returning canvases for the board and piece. */
|
||||||
async function generateCaptcha(
|
function generateCaptcha(
|
||||||
from: Uint8Array,
|
{ bgImages, puzzleMask, puzzleHole }: CaptchaImages,
|
||||||
mask: Uint8Array,
|
|
||||||
hole: Uint8Array,
|
|
||||||
bgSize: Dimensions,
|
bgSize: Dimensions,
|
||||||
puzzleSize: Dimensions,
|
puzzleSize: Dimensions,
|
||||||
) {
|
) {
|
||||||
|
|
@ -61,19 +93,16 @@ async function generateCaptcha(
|
||||||
const ctx = bg.getContext('2d');
|
const ctx = bg.getContext('2d');
|
||||||
const pctx = puzzle.getContext('2d');
|
const pctx = puzzle.getContext('2d');
|
||||||
|
|
||||||
const bgImage = await loadImage(from);
|
|
||||||
const maskImage = await loadImage(mask);
|
|
||||||
const holeImage = await loadImage(hole);
|
|
||||||
|
|
||||||
const solution = generateSolution(bgSize, puzzleSize);
|
const solution = generateSolution(bgSize, puzzleSize);
|
||||||
|
const bgImage = bgImages[Math.floor(Math.random() * bgImages.length)];
|
||||||
|
|
||||||
// Draw the background image.
|
// Draw the background image.
|
||||||
ctx.drawImage(bgImage, 0, 0, bg.width, bg.height);
|
ctx.drawImage(bgImage, 0, 0, bg.width, bg.height);
|
||||||
ctx.globalCompositeOperation = 'source-atop';
|
ctx.globalCompositeOperation = 'source-atop';
|
||||||
ctx.drawImage(holeImage, solution.x, solution.y, puzzle.width, puzzle.height);
|
ctx.drawImage(puzzleHole, solution.x, solution.y, puzzle.width, puzzle.height);
|
||||||
|
|
||||||
// Draw the puzzle piece.
|
// Draw the puzzle piece.
|
||||||
pctx.drawImage(maskImage, 0, 0, puzzle.width, puzzle.height);
|
pctx.drawImage(puzzleMask, 0, 0, puzzle.width, puzzle.height);
|
||||||
pctx.globalCompositeOperation = 'source-in';
|
pctx.globalCompositeOperation = 'source-in';
|
||||||
pctx.drawImage(bgImage, solution.x, solution.y, puzzle.width, puzzle.height, 0, 0, puzzle.width, puzzle.height);
|
pctx.drawImage(bgImage, solution.x, solution.y, puzzle.width, puzzle.height, 0, 0, puzzle.width, puzzle.height);
|
||||||
|
|
||||||
|
|
|
||||||