ncaptcha.css
.ncaptcha-container {
width: 300px;
height: 80px;
position: relative;
border: 1px solid #d3d3d3;
border-radius: 3px;
box-shadow: 0 0 4px 1px rgba(0, 0, 0, .08);
background: #f9f9f9;
margin-top: 6px;
margin-bottom: 6px;
padding-left: 12px;
display: flex;
align-items: center;
flex-direction: row;
}
.ncaptcha-error {
position: absolute;
color: red;
left: 0;
top: 0;
font-family: Roboto, arial, sans-serif;
}
.ncaptcha-checkbox {
width: 24px;
height: 24px;
border: 2px solid #c1c1c1;
border-radius: 2px;
background-color: #fff;
}
.ncaptcha-spinner {
width: 24px;
height: 24px;
border: 5px solid #4d90fe;
border-radius: 24px;
border-bottom-color: transparent;
border-left-color: transparent;
animation: spinner-spin linear 2.5s infinite;
}
.ncaptcha-checkmark>img {
width: 38px;
height: 38px;
margin-right: -8px;
}
.ncaptcha-text {
margin-left: 12px;
font-size: 16px;
font-family: Roboto, arial, sans-serif;
}
.ncaptcha-info {
margin-left: auto;
margin-right: 12px;
display: flex;
flex-direction: column;
align-items: center;
font-family: Roboto, arial, sans-serif;
font-size: 10px;
color: #555;
}
.ncaptcha-info>div {
font-weight: 400;
margin-top: 2px;
}
@keyframes spinner-spin {
0% {
transform: rotateZ(0deg)
}
10% {
transform: rotateZ(135deg)
}
25% {
transform: rotateZ(245deg)
}
60% {
transform: rotateZ(700deg)
}
75% {
transform: rotateZ(810deg)
}
to {
transform: rotateZ(1080deg)
}
}
/* Popup window */
.ncaptcha-dialog::backdrop {
background: rgb(255, 255, 255);
opacity: 0.5;
}
.ncaptcha-dialog {
border: 0px;
padding: 0px;
box-shadow: 0 0 4px 1px rgba(0, 0, 0, 0.08);
}
.ncaptcha-dialog[open] {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.ncaptcha-question {
width: fit-content;
border: 1px solid rgb(204, 204, 204);
padding: 7px;
}
.ncaptcha-actions {
border: 1px solid rgb(204, 204, 204);
border-top: 0px;
padding: 7px;
display: flex;
}
.ncaptcha-title {
width: 352px;
height: 100px;
background-color: #1a73e8;
font-family: Roboto, arial, sans-serif;
color: white;
display: flex;
flex-direction: column;
padding-left: 24px;
padding-right: 24px;
padding-top: 24px;
}
.ncaptcha-title>strong {
font-size: 28px;
}
.ncaptcha-images {
margin-top: 7px;
display: flex;
gap: 4px;
flex-wrap: wrap;
width: 100%;
}
.ncaptcha-image {
flex: auto;
min-width: 30%;
aspect-ratio: 1 / 1;
background-size: 100% 100%;
transition: all 0.1s ease;
}
.ncaptcha-verify-btn {
cursor: pointer;
height: 42px;
min-width: 100px;
margin-left: auto;
border: 0;
background: #1a73e8;
color: white;
font-family: Roboto, arial, sans-serif;
font-size: 14px;
border-radius: 2px;
padding-right: 10px;
padding-left: 10px;
}
.ncaptcha-verify-btn:disabled {
background-color: rgba(73, 143, 225, 0.50);
cursor: default;
}
.ncaptcha-select {
width: 32px;
height: 32px;
background-color: white;
border-radius: 32px;
position: absolute;
top: -16px;
left: -16px;
}
ncaptcha.js
(function () {
if (!window.OffscreenCanvas) {
alert("nCAPTCHA: Your browser does not support OffscreenCanvas. Please update your browser.")
}
if (typeof HTMLDialogElement !== 'function') {
alert("nCAPTCHA: Your browser does not support HTML dialog. Please update your browser.")
}
let API = "https://ncaptcha.1000005.xyz";
let html = `
<div class="ncaptcha-base">
<input name="ncaptcha-response" class="ncaptcha-response" style="display: none;">
<div class="ncaptcha-container">
<span class="ncaptcha-error"></span>
<div class="ncaptcha-checkbox"></div>
<div class="ncaptcha-spinner" style="display: none;"></div>
<div class="ncaptcha-checkmark" style="display: none;">
<img src="./checkmark.svg">
</div>
<div class="ncaptcha-text">I'm not a human</div>
<div class="ncaptcha-info">
<img src="./icon.svg" width="32px" height="32px" alt="ncaptcha-icon">
<div>nCAPTCHA</div>
<div>Source code</div>
</div>
</div>
<dialog class="ncaptcha-dialog">
<div class="ncaptcha-question">
<div class="ncaptcha-title">
Select all images with
<strong></strong>
Click verify once there are none left.
</div>
<div class="ncaptcha-images">
<div class="ncaptcha-image"></div>
<div class="ncaptcha-image"></div>
<div class="ncaptcha-image"></div>
<div class="ncaptcha-image"></div>
<div class="ncaptcha-image"></div>
<div class="ncaptcha-image"></div>
<div class="ncaptcha-image"></div>
<div class="ncaptcha-image"></div>
<div class="ncaptcha-image"></div>
</div>
</div>
<div class="ncaptcha-actions">
<button class="ncaptcha-verify-btn" type="button">VERIFY</button>
</div>
</dialog>
</div>
`;
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".ncaptcha").forEach((each) => {
each.innerHTML = html;
let checkbox = each.querySelector(".ncaptcha-checkbox");
let spinner = each.querySelector(".ncaptcha-spinner");
let checkmark = each.querySelector(".ncaptcha-checkmark");
let siwtchState = (state) => {
checkbox.style.display = "none";
spinner.style.display = "none";
checkmark.style.display = "none";
switch (state) {
case "checkbox":
checkbox.style.display = "";
break;
case "spinner":
spinner.style.display = "";
break;
case "checkmark":
checkmark.style.display = "";
break;
}
}
// draw a base64 encoded PNG image to context
let drawImage = (ctx, img, x, y) => {
return new Promise((resolve) => {
let i = new Image();
i.onload = () => {
ctx.drawImage(i, x, y, 200, 200, 0, 0, 200, 200);
resolve()
}
i.src = "data:image/png;base64," + img;
})
}
let dialog = each.querySelector(".ncaptcha-dialog");
checkbox.addEventListener("click", async () => {
siwtchState("spinner");
let challengeId = "";
let answers = [];
// clear error message
each.querySelector(".ncaptcha-error").innerHTML = "";
// clear all event listners
let clone = dialog.cloneNode(true);
dialog.parentElement.replaceChild(clone, dialog);
dialog = clone;
// enable the button
dialog.querySelector(".ncaptcha-verify-btn").removeAttribute("disabled");
// close the dialog when clicking outside
dialog.addEventListener("click", (e) => {
if (e.target.className === "ncaptcha-dialog")
dialog.close();
})
// obtain challenge
let resp = await (await fetch(API + "/challenge", {
mode: "no-cors"
})).json();
challengeId = resp["id"];
dialog.querySelector(".ncaptcha-title>strong").textContent = resp["select"];
// split the image and draw to tiles
let canvas = new OffscreenCanvas(200,
200);
let context = canvas.getContext("2d");
let coordinates = [
[0, 0], [200, 0], [400, 0],
[0, 200], [200, 200], [400, 200],
[0, 400], [200, 400], [400, 400]
];
let tiles = dialog.querySelectorAll(".ncaptcha-image");
for (let i = 0; i < tiles.length; i++) {
await drawImage(context, resp["challenge"], coordinates[i][0], coordinates[i][1])
let blob = await canvas.convertToBlob();
tiles.item(i).style.background = "url(" + URL.createObjectURL(blob) + ")";
tiles.item(i).style["background-size"] = "cover";
// reset selected tiles
tiles.item(i).innerHTML = "";
tiles.item(i).style.transform = "";
}
// click to select a tile
for (let i = 0; i < tiles.length; i++) {
tiles.item(i).addEventListener("click", (e) => {
if (answers.includes(i)) {
// unselect
e.target.style.transform = "";
e.target.innerHTML = "";
answers = answers.filter((n) => n !== i);
} else {
// select
e.target.style.transform = "scale(0.8)";
setTimeout(() => {
e.target.innerHTML = `<img class="ncaptcha-select" src="./checkmark-circle.svg">`;
}, 100);
answers.push(i);
}
});
}
// submit answer
dialog.querySelector(".ncaptcha-verify-btn").addEventListener("click",
async () => {
dialog.querySelector(".ncaptcha-verify-btn").setAttribute("disabled", true);
answers.sort((a, b) => a - b);
let body = new FormData();
body.set("challenge", challengeId);
body.set("ans", answers.join(","));
let resp = await (await fetch(API + "/answer", {
method: "POST",
body: body
})).text();
if (resp.startsWith("TOKEN_")) {
each.querySelector(".ncaptcha-response").value = resp.substring(6);
siwtchState("checkmark");
} else {
each.querySelector(".ncaptcha-error").textContent = resp;
}
dialog.close();
});
dialog.showModal();
siwtchState("checkbox");
})
});
});
})()