diff --git a/captcha/puzzle.png b/captcha/puzzle.png new file mode 100644 index 00000000..37d762c9 Binary files /dev/null and b/captcha/puzzle.png differ diff --git a/src/controllers/api/captcha.ts b/src/controllers/api/captcha.ts index 7ca8cc4c..9599c641 100644 --- a/src/controllers/api/captcha.ts +++ b/src/controllers/api/captcha.ts @@ -8,12 +8,13 @@ import { aesEncrypt } from '@/utils/aes.ts'; export const captchaController: AppController = async (c) => { const { puzzle, piece, solution } = await generateCaptcha( await Deno.readFile(new URL('../../../captcha/tj-holowaychuk.jpg', import.meta.url)), + await Deno.readFile(new URL('../../../captcha/puzzle.png', import.meta.url)), { cw: 300, ch: 300, pw: 50, ph: 50, - alpha: 0.6, + alpha: 0.8, }, ); @@ -41,6 +42,7 @@ interface Point { async function generateCaptcha( from: Uint8Array, + mask: Uint8Array, opts: { pw: number; ph: number; @@ -54,12 +56,34 @@ async function generateCaptcha( const ctx = puzzle.getContext('2d'); const image = await loadImage(from); ctx.drawImage(image, 0, 0, image.width(), image.height(), 0, 0, cw, ch); + const piece = createCanvas(pw, ph); const pctx = piece.getContext('2d'); + const solution = getPieceCoords(puzzle.width, puzzle.height, pw, ph); + + // Draw the piece onto the puzzle piece canvas but only where the mask allows + const maskImage = await loadImage(mask); + pctx.globalCompositeOperation = 'source-over'; + pctx.drawImage(maskImage, 0, 0, pw, ph); + pctx.globalCompositeOperation = 'source-in'; pctx.drawImage(puzzle, solution.x, solution.y, pw, ph, 0, 0, pw, ph); - ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`; - ctx.fillRect(solution.x, solution.y, pw, ph); + + // Reset composite operation + pctx.globalCompositeOperation = 'source-over'; + + // Create a temporary canvas to draw the darkened shape + const tempCanvas = createCanvas(pw, ph); + const tempCtx = tempCanvas.getContext('2d'); + + // Draw the darkened shape onto the temporary canvas but only where the mask allows + tempCtx.fillStyle = `rgba(0, 0, 0, ${alpha})`; + tempCtx.fillRect(0, 0, pw, ph); + tempCtx.globalCompositeOperation = 'destination-in'; + tempCtx.drawImage(maskImage, 0, 0, pw, ph); + + // Draw the temporary canvas onto the puzzle at the piece's location + ctx.drawImage(tempCanvas, solution.x, solution.y, pw, ph); return { puzzle,