@funnycaptcha/embed 0.2.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/embed.global.js +1949 -170
- package/dist/embed.global.js.map +1 -1
- package/dist/embed.js +7 -0
- package/dist/embed.js.map +1 -1
- package/package.json +19 -12
package/dist/embed.global.js
CHANGED
|
@@ -55,6 +55,126 @@ var FunnyCaptcha = (() => {
|
|
|
55
55
|
function getCaptcha(id) {
|
|
56
56
|
return registry.get(id);
|
|
57
57
|
}
|
|
58
|
+
var TrackRecorder = class {
|
|
59
|
+
points = [];
|
|
60
|
+
origin = 0;
|
|
61
|
+
start() {
|
|
62
|
+
this.origin = performance.now();
|
|
63
|
+
this.points = [];
|
|
64
|
+
}
|
|
65
|
+
record(x, y) {
|
|
66
|
+
this.points.push({ x, y, t: performance.now() - this.origin });
|
|
67
|
+
}
|
|
68
|
+
stop() {
|
|
69
|
+
return this.points.slice();
|
|
70
|
+
}
|
|
71
|
+
clear() {
|
|
72
|
+
this.points = [];
|
|
73
|
+
this.origin = 0;
|
|
74
|
+
}
|
|
75
|
+
get count() {
|
|
76
|
+
return this.points.length;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
function mean(arr) {
|
|
80
|
+
if (arr.length === 0) return 0;
|
|
81
|
+
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
82
|
+
}
|
|
83
|
+
function std(arr) {
|
|
84
|
+
if (arr.length === 0) return 0;
|
|
85
|
+
const m = mean(arr);
|
|
86
|
+
const v = arr.reduce((a, b) => a + (b - m) ** 2, 0) / arr.length;
|
|
87
|
+
return Math.sqrt(v);
|
|
88
|
+
}
|
|
89
|
+
function analyzeTrack(track) {
|
|
90
|
+
const reasons = [];
|
|
91
|
+
let score = 100;
|
|
92
|
+
if (track.length < 2) {
|
|
93
|
+
return {
|
|
94
|
+
humanScore: 0,
|
|
95
|
+
isBot: true,
|
|
96
|
+
reasons: ["\u8F68\u8FF9\u70B9\u4E0D\u8DB3"],
|
|
97
|
+
samples: track.length,
|
|
98
|
+
duration: 0
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const first = track[0];
|
|
102
|
+
const last = track[track.length - 1];
|
|
103
|
+
const duration = last.t - first.t;
|
|
104
|
+
if (track.length < 8) {
|
|
105
|
+
reasons.push("\u8F68\u8FF9\u70B9\u592A\u5C11");
|
|
106
|
+
score -= 35;
|
|
107
|
+
}
|
|
108
|
+
if (duration < 280) {
|
|
109
|
+
reasons.push("\u62D6\u52A8\u8FC7\u5FEB");
|
|
110
|
+
score -= 30;
|
|
111
|
+
}
|
|
112
|
+
if (duration > 3e4) {
|
|
113
|
+
reasons.push("\u62D6\u52A8\u5F02\u5E38\u7F13\u6162");
|
|
114
|
+
score -= 15;
|
|
115
|
+
}
|
|
116
|
+
const speeds = [];
|
|
117
|
+
for (let i = 1; i < track.length; i++) {
|
|
118
|
+
const prev = track[i - 1];
|
|
119
|
+
const cur = track[i];
|
|
120
|
+
const dx = cur.x - prev.x;
|
|
121
|
+
const dy = cur.y - prev.y;
|
|
122
|
+
const dt = Math.max(1, cur.t - prev.t);
|
|
123
|
+
speeds.push(Math.sqrt(dx * dx + dy * dy) / dt);
|
|
124
|
+
}
|
|
125
|
+
const meanSpeed = mean(speeds);
|
|
126
|
+
const speedStd = std(speeds);
|
|
127
|
+
const cv = meanSpeed > 0 ? speedStd / meanSpeed : 0;
|
|
128
|
+
if (cv < 0.18 && speeds.length > 5) {
|
|
129
|
+
reasons.push("\u901F\u5EA6\u8FC7\u4E8E\u5747\u5300");
|
|
130
|
+
score -= 25;
|
|
131
|
+
}
|
|
132
|
+
const ys = track.map((p) => p.y);
|
|
133
|
+
const yStd = std(ys);
|
|
134
|
+
if (track.length > 10 && yStd < 1.2) {
|
|
135
|
+
reasons.push("Y\u8F74\u65E0\u81EA\u7136\u6296\u52A8");
|
|
136
|
+
score -= 22;
|
|
137
|
+
}
|
|
138
|
+
if (speeds.length > 6) {
|
|
139
|
+
const maxSpeed = Math.max(...speeds);
|
|
140
|
+
const tailLen = Math.max(1, Math.ceil(speeds.length * 0.2));
|
|
141
|
+
const tail = speeds.slice(-tailLen);
|
|
142
|
+
const tailMean = mean(tail);
|
|
143
|
+
if (maxSpeed > 0 && tailMean > maxSpeed * 0.55) {
|
|
144
|
+
reasons.push("\u672B\u7AEF\u672A\u51CF\u901F");
|
|
145
|
+
score -= 18;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let reversals = 0;
|
|
149
|
+
for (let i = 2; i < track.length; i++) {
|
|
150
|
+
const d1 = track[i - 1].x - track[i - 2].x;
|
|
151
|
+
const d2 = track[i].x - track[i - 1].x;
|
|
152
|
+
if (d1 * d2 < 0 && Math.abs(d1) > 0.5 && Math.abs(d2) > 0.5) reversals++;
|
|
153
|
+
}
|
|
154
|
+
if (track.length > 12 && reversals === 0) {
|
|
155
|
+
reasons.push("\u65E0\u56DE\u9000/\u6821\u5BF9\u52A8\u4F5C");
|
|
156
|
+
score -= 12;
|
|
157
|
+
}
|
|
158
|
+
if (speeds.length > 4) {
|
|
159
|
+
const accels = [];
|
|
160
|
+
for (let i = 1; i < speeds.length; i++) {
|
|
161
|
+
accels.push(speeds[i] - speeds[i - 1]);
|
|
162
|
+
}
|
|
163
|
+
const accelStd = std(accels);
|
|
164
|
+
if (accelStd < 0.05 && meanSpeed > 0.1) {
|
|
165
|
+
reasons.push("\u52A0\u901F\u5EA6\u8FC7\u4E8E\u5E73\u6ED1");
|
|
166
|
+
score -= 15;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
score = Math.max(0, Math.min(100, Math.round(score)));
|
|
170
|
+
return {
|
|
171
|
+
humanScore: score,
|
|
172
|
+
isBot: score < 50,
|
|
173
|
+
reasons,
|
|
174
|
+
samples: track.length,
|
|
175
|
+
duration: Math.round(duration)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
58
178
|
|
|
59
179
|
// src/iframe-host.ts
|
|
60
180
|
function mountInto(container, type, config) {
|
|
@@ -92,11 +212,12 @@ var FunnyCaptcha = (() => {
|
|
|
92
212
|
return c.answer === userAnswer;
|
|
93
213
|
}
|
|
94
214
|
var STR = {
|
|
95
|
-
zh: { title: "\u8BF7\u8BA1\u7B97", placeholder: "\u8F93\u5165\u7B54\u6848", submit: "\u9A8C\u8BC1", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898" },
|
|
96
|
-
en: { title: "Solve", placeholder: "Enter answer", submit: "Verify", fail: "Wrong, try again" }
|
|
215
|
+
zh: { title: "\u8BF7\u8BA1\u7B97", placeholder: "\u8F93\u5165\u7B54\u6848", submit: "\u9A8C\u8BC1", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898", refresh: "\u5237\u65B0" },
|
|
216
|
+
en: { title: "Solve", placeholder: "Enter answer", submit: "Verify", fail: "Wrong, try again", refresh: "Refresh" }
|
|
97
217
|
};
|
|
98
218
|
function createMathInstance(container, config) {
|
|
99
219
|
const t = STR[config.locale];
|
|
220
|
+
const theme = config.theme ?? "light";
|
|
100
221
|
let current;
|
|
101
222
|
let listeners = [];
|
|
102
223
|
let startTime = Date.now();
|
|
@@ -104,16 +225,36 @@ var FunnyCaptcha = (() => {
|
|
|
104
225
|
current = generateChallenge();
|
|
105
226
|
startTime = Date.now();
|
|
106
227
|
container.innerHTML = `
|
|
107
|
-
<
|
|
228
|
+
<style>
|
|
229
|
+
.fc-math{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
230
|
+
.fc-math[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
231
|
+
.fc-math[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
232
|
+
.fc-math{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
233
|
+
.fc-math-q{display:block;font-size:14px;font-weight:600;color:var(--fc-text);margin-bottom:12px}
|
|
234
|
+
.fc-math-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
235
|
+
.fc-math-input{flex:1;min-width:0;padding:8px 12px;font-size:14px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text);border-radius:8px;box-sizing:border-box;outline:none}
|
|
236
|
+
.fc-math-input:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
|
|
237
|
+
.fc-math-btn{padding:8px 16px;font-size:14px;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
|
|
238
|
+
.fc-math-btn:hover{opacity:.9}
|
|
239
|
+
.fc-math-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
240
|
+
.fc-math-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
241
|
+
.fc-math-msg{font-size:13px;min-height:18px;margin-top:10px;color:var(--fc-danger)}
|
|
242
|
+
</style>
|
|
243
|
+
<div class="fc-math" data-theme="${theme}">
|
|
108
244
|
<label class="fc-math-q">${t.title}: ${current.question}</label>
|
|
109
|
-
<
|
|
110
|
-
|
|
245
|
+
<div class="fc-math-row">
|
|
246
|
+
<input class="fc-math-input" type="text" inputmode="numeric" placeholder="${t.placeholder}" />
|
|
247
|
+
<button class="fc-math-btn">${t.submit}</button>
|
|
248
|
+
<button class="fc-math-refresh">${t.refresh}</button>
|
|
249
|
+
</div>
|
|
111
250
|
<div class="fc-math-msg"></div>
|
|
112
251
|
</div>
|
|
113
252
|
`;
|
|
114
253
|
const btn = container.querySelector(".fc-math-btn");
|
|
115
254
|
const input = container.querySelector(".fc-math-input");
|
|
116
255
|
const msg = container.querySelector(".fc-math-msg");
|
|
256
|
+
const refreshBtn = container.querySelector(".fc-math-refresh");
|
|
257
|
+
refreshBtn.addEventListener("click", render2);
|
|
117
258
|
btn.addEventListener("click", async () => {
|
|
118
259
|
const val = Number(input.value);
|
|
119
260
|
const success = verifyAnswer(current, val);
|
|
@@ -169,58 +310,187 @@ var FunnyCaptcha = (() => {
|
|
|
169
310
|
return c.code.toLowerCase() === answer.trim().toLowerCase();
|
|
170
311
|
}
|
|
171
312
|
var STR2 = {
|
|
172
|
-
zh: { placeholder: "\
|
|
173
|
-
en: { placeholder: "
|
|
313
|
+
zh: { placeholder: "\u89C2\u5BDF\u95EA\u70C1\u6587\u5B57\u5E76\u8F93\u5165", submit: "\u9A8C\u8BC1", fail: "\u770B\u4E0D\u6E05\uFF1F\u6362\u4E00\u5F20", refresh: "\u5237\u65B0", pause: "\u6682\u505C", resume: "\u7EE7\u7EED" },
|
|
314
|
+
en: { placeholder: "Watch the flicker and type", submit: "Verify", fail: "Wrong? Try another", refresh: "Refresh", pause: "Pause", resume: "Resume" }
|
|
174
315
|
};
|
|
175
|
-
|
|
316
|
+
var PERIOD = 180;
|
|
317
|
+
function drawFrame(canvas, code, frame, surface, text, border, accentSoft) {
|
|
176
318
|
const ctx = canvas.getContext("2d");
|
|
177
319
|
if (!ctx) return;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
320
|
+
const W = canvas.width;
|
|
321
|
+
const H = canvas.height;
|
|
322
|
+
const f = frame % PERIOD;
|
|
323
|
+
let phase;
|
|
324
|
+
if (f < 90) phase = "strong";
|
|
325
|
+
else if (f < 120) phase = "fade-out";
|
|
326
|
+
else if (f < 150) phase = "clear";
|
|
327
|
+
else phase = "fade-in";
|
|
328
|
+
let intensity;
|
|
329
|
+
if (phase === "strong") intensity = 1;
|
|
330
|
+
else if (phase === "fade-out") intensity = 1 - (f - 90) / 30;
|
|
331
|
+
else if (phase === "clear") intensity = 0;
|
|
332
|
+
else intensity = (f - 150) / 30;
|
|
333
|
+
let bg = surface;
|
|
334
|
+
if (phase === "strong") {
|
|
335
|
+
bg = Math.floor(frame / 5) % 2 === 0 ? surface : accentSoft;
|
|
336
|
+
}
|
|
337
|
+
ctx.clearRect(0, 0, W, H);
|
|
338
|
+
ctx.fillStyle = bg;
|
|
339
|
+
ctx.fillRect(0, 0, W, H);
|
|
340
|
+
const noiseCount = Math.floor(40 + intensity * 80);
|
|
341
|
+
for (let i = 0; i < noiseCount; i++) {
|
|
342
|
+
ctx.fillStyle = `rgba(0,0,0,${Math.random() * 0.12 * (0.3 + intensity * 0.7)})`;
|
|
343
|
+
ctx.fillRect(Math.random() * W, Math.random() * H, 1.5, 1.5);
|
|
344
|
+
}
|
|
181
345
|
const chars = code.split("");
|
|
182
|
-
const step =
|
|
346
|
+
const step = W / (chars.length + 1);
|
|
347
|
+
const focusIndex = phase === "strong" ? frame % chars.length : -1;
|
|
183
348
|
chars.forEach((ch, i) => {
|
|
184
349
|
ctx.save();
|
|
185
|
-
const
|
|
186
|
-
const
|
|
350
|
+
const jitter = 3 + intensity * 5;
|
|
351
|
+
const dx = (Math.random() - 0.5) * jitter * 2;
|
|
352
|
+
const dy = (Math.random() - 0.5) * jitter * 2;
|
|
353
|
+
const x = step * (i + 1) + dx;
|
|
354
|
+
const y = H / 2 + dy;
|
|
187
355
|
ctx.translate(x, y);
|
|
188
|
-
ctx.rotate((Math.random() - 0.5) * 0.
|
|
189
|
-
|
|
190
|
-
ctx.
|
|
356
|
+
ctx.rotate((Math.random() - 0.5) * (0.2 + intensity * 0.3));
|
|
357
|
+
const fontSize = 24 + Math.floor(Math.random() * 8);
|
|
358
|
+
ctx.font = `${fontSize}px sans-serif`;
|
|
191
359
|
ctx.textAlign = "center";
|
|
192
360
|
ctx.textBaseline = "middle";
|
|
361
|
+
let alpha;
|
|
362
|
+
let blur;
|
|
363
|
+
if (phase === "strong") {
|
|
364
|
+
if (i === focusIndex) {
|
|
365
|
+
alpha = 0.85 + Math.random() * 0.15;
|
|
366
|
+
blur = 0;
|
|
367
|
+
} else {
|
|
368
|
+
alpha = 0.1 + Math.random() * 0.3;
|
|
369
|
+
blur = 3;
|
|
370
|
+
}
|
|
371
|
+
} else if (phase === "clear") {
|
|
372
|
+
alpha = 0.85 + Math.random() * 0.15;
|
|
373
|
+
blur = 0;
|
|
374
|
+
} else {
|
|
375
|
+
const t = 1 - intensity;
|
|
376
|
+
alpha = 0.5 + t * 0.2 + (Math.random() - 0.5) * 0.05;
|
|
377
|
+
blur = intensity * 3;
|
|
378
|
+
}
|
|
379
|
+
ctx.globalAlpha = Math.max(0, Math.min(1, alpha));
|
|
380
|
+
if (blur > 0) ctx.filter = `blur(${blur}px)`;
|
|
381
|
+
ctx.fillStyle = text;
|
|
193
382
|
ctx.fillText(ch, 0, 0);
|
|
194
383
|
ctx.restore();
|
|
195
384
|
});
|
|
196
|
-
|
|
197
|
-
|
|
385
|
+
const lineCount = Math.floor(2 + intensity * 6);
|
|
386
|
+
for (let i = 0; i < lineCount; i++) {
|
|
387
|
+
ctx.save();
|
|
388
|
+
ctx.strokeStyle = border;
|
|
389
|
+
ctx.lineWidth = 1 + Math.random() * 1.5;
|
|
390
|
+
ctx.globalAlpha = 0.4 + intensity * 0.4;
|
|
198
391
|
ctx.beginPath();
|
|
199
|
-
|
|
200
|
-
|
|
392
|
+
const x1 = Math.random() * W;
|
|
393
|
+
const y1 = Math.random() * H;
|
|
394
|
+
const x2 = Math.random() * W;
|
|
395
|
+
const y2 = Math.random() * H;
|
|
396
|
+
const cpx = Math.random() * W;
|
|
397
|
+
const cpy = Math.random() * H;
|
|
398
|
+
ctx.moveTo(x1, y1);
|
|
399
|
+
ctx.quadraticCurveTo(cpx, cpy, x2, y2);
|
|
201
400
|
ctx.stroke();
|
|
401
|
+
ctx.restore();
|
|
202
402
|
}
|
|
203
403
|
}
|
|
404
|
+
function readVar(root, name, fallback) {
|
|
405
|
+
const v = getComputedStyle(root).getPropertyValue(name).trim();
|
|
406
|
+
return v || fallback;
|
|
407
|
+
}
|
|
204
408
|
function createTextDistortInstance(container, config) {
|
|
205
409
|
const t = STR2[config.locale];
|
|
410
|
+
const theme = config.theme ?? "light";
|
|
206
411
|
let current;
|
|
207
412
|
let listeners = [];
|
|
208
413
|
let startTime = Date.now();
|
|
414
|
+
let rafId = 0;
|
|
415
|
+
let frame = 0;
|
|
416
|
+
let paused = false;
|
|
417
|
+
let cssCache = null;
|
|
418
|
+
let cssCacheFrame = -100;
|
|
209
419
|
function render2() {
|
|
210
420
|
current = generateChallenge2();
|
|
211
421
|
startTime = Date.now();
|
|
422
|
+
cancelAnimationFrame(rafId);
|
|
423
|
+
frame = 0;
|
|
424
|
+
paused = false;
|
|
425
|
+
cssCache = null;
|
|
426
|
+
cssCacheFrame = -100;
|
|
212
427
|
container.innerHTML = `
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
428
|
+
<style>
|
|
429
|
+
.fc-text{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
430
|
+
.fc-text[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
431
|
+
.fc-text[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
432
|
+
.fc-text{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
433
|
+
.fc-text-canvas{display:block;width:100%;max-width:280px;height:60px;border-radius:8px;border:1px solid var(--fc-border);margin-bottom:10px;box-shadow:0 0 0 1px var(--fc-border)}
|
|
434
|
+
.fc-text-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
435
|
+
.fc-text-input{flex:1;min-width:0;padding:8px 12px;font-size:14px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text);border-radius:8px;box-sizing:border-box;outline:none}
|
|
436
|
+
.fc-text-input:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
|
|
437
|
+
.fc-text-btn{padding:8px 16px;font-size:14px;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
|
|
438
|
+
.fc-text-btn:hover{opacity:.9}
|
|
439
|
+
.fc-text-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
440
|
+
.fc-text-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
441
|
+
.fc-text-pause{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
442
|
+
.fc-text-pause:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
443
|
+
.fc-text-msg{font-size:13px;min-height:18px;margin-top:10px;color:var(--fc-danger)}
|
|
444
|
+
</style>
|
|
445
|
+
<div class="fc-text" data-theme="${theme}">
|
|
446
|
+
<canvas class="fc-text-canvas" width="280" height="60"></canvas>
|
|
447
|
+
<div class="fc-text-row">
|
|
448
|
+
<input class="fc-text-input" placeholder="${t.placeholder}" />
|
|
449
|
+
<button class="fc-text-btn">${t.submit}</button>
|
|
450
|
+
<button class="fc-text-refresh">${t.refresh}</button>
|
|
451
|
+
<button class="fc-text-pause">${t.pause}</button>
|
|
452
|
+
</div>
|
|
218
453
|
<div class="fc-text-msg"></div>
|
|
219
454
|
</div>
|
|
220
455
|
`;
|
|
456
|
+
const root = container.querySelector(".fc-text");
|
|
221
457
|
const canvas = container.querySelector(".fc-text-canvas");
|
|
222
|
-
|
|
458
|
+
const code = current.code;
|
|
459
|
+
function readCssVars() {
|
|
460
|
+
return {
|
|
461
|
+
surface: readVar(root, "--fc-surface", "#f6f7f9"),
|
|
462
|
+
text: readVar(root, "--fc-text", "#0f172a"),
|
|
463
|
+
border: readVar(root, "--fc-border", "#e2e8f0"),
|
|
464
|
+
accentSoft: readVar(root, "--fc-accent-soft", "#eef2ff")
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function tick() {
|
|
468
|
+
if (paused) return;
|
|
469
|
+
if (!cssCache || frame - cssCacheFrame >= 30) {
|
|
470
|
+
cssCache = readCssVars();
|
|
471
|
+
cssCacheFrame = frame;
|
|
472
|
+
}
|
|
473
|
+
const c = cssCache;
|
|
474
|
+
if (c) {
|
|
475
|
+
drawFrame(canvas, code, frame, c.surface, c.text, c.border, c.accentSoft);
|
|
476
|
+
}
|
|
477
|
+
frame++;
|
|
478
|
+
rafId = requestAnimationFrame(tick);
|
|
479
|
+
}
|
|
480
|
+
rafId = requestAnimationFrame(tick);
|
|
223
481
|
container.querySelector(".fc-text-refresh").addEventListener("click", render2);
|
|
482
|
+
const pauseBtn = container.querySelector(".fc-text-pause");
|
|
483
|
+
pauseBtn.addEventListener("click", () => {
|
|
484
|
+
if (paused) {
|
|
485
|
+
paused = false;
|
|
486
|
+
pauseBtn.textContent = t.pause;
|
|
487
|
+
rafId = requestAnimationFrame(tick);
|
|
488
|
+
} else {
|
|
489
|
+
paused = true;
|
|
490
|
+
cancelAnimationFrame(rafId);
|
|
491
|
+
pauseBtn.textContent = t.resume;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
224
494
|
const btn = container.querySelector(".fc-text-btn");
|
|
225
495
|
const input = container.querySelector(".fc-text-input");
|
|
226
496
|
const msg = container.querySelector(".fc-text-msg");
|
|
@@ -247,6 +517,7 @@ var FunnyCaptcha = (() => {
|
|
|
247
517
|
mount: () => render2(),
|
|
248
518
|
reset: () => render2(),
|
|
249
519
|
destroy: () => {
|
|
520
|
+
cancelAnimationFrame(rafId);
|
|
250
521
|
container.innerHTML = "";
|
|
251
522
|
listeners = [];
|
|
252
523
|
},
|
|
@@ -274,60 +545,136 @@ var FunnyCaptcha = (() => {
|
|
|
274
545
|
return `${c.token}:completed`;
|
|
275
546
|
}
|
|
276
547
|
var STR3 = {
|
|
277
|
-
zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5230\
|
|
278
|
-
en: { title: "Drag the
|
|
548
|
+
zh: { title: "\u6CBF\u87BA\u65CB\u62D6\u52A8\u6ED1\u5757\u5230\u7EC8\u70B9", tip: "\u6CBF\u87BA\u65CB\u8DEF\u5F84\u62D6\u52A8", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u8BF7\u62D6\u5230\u5C3D\u5934", botFail: "\u68C0\u6D4B\u5230\u5F02\u5E38\u64CD\u4F5C", refresh: "\u5237\u65B0" },
|
|
549
|
+
en: { title: "Drag along the spiral to the center", tip: "Follow the spiral", success: "Verified", fail: "Drag to the end", botFail: "Bot-like behavior", refresh: "Refresh" }
|
|
279
550
|
};
|
|
551
|
+
var CANVAS_W = 320;
|
|
552
|
+
var CANVAS_H = 200;
|
|
553
|
+
var HANDLE_W = 36;
|
|
554
|
+
var HANDLE_H = 36;
|
|
555
|
+
var SPIRAL_THETA_MAX = 6 * Math.PI;
|
|
556
|
+
var SPIRAL_A = 0;
|
|
557
|
+
var SPIRAL_B = 5;
|
|
558
|
+
var CX = CANVAS_W / 2;
|
|
559
|
+
var CY = CANVAS_H / 2;
|
|
560
|
+
var SPIRAL_R_MAX = SPIRAL_B * SPIRAL_THETA_MAX;
|
|
561
|
+
function spiralPoints() {
|
|
562
|
+
const steps = 200;
|
|
563
|
+
const pts = [];
|
|
564
|
+
for (let i = 0; i <= steps; i++) {
|
|
565
|
+
const t = i / steps;
|
|
566
|
+
const theta = SPIRAL_THETA_MAX * (1 - t);
|
|
567
|
+
const r = SPIRAL_A + SPIRAL_B * theta;
|
|
568
|
+
const x = CX + r * Math.cos(theta);
|
|
569
|
+
const y = CY + r * Math.sin(theta);
|
|
570
|
+
pts.push({ x, y, theta });
|
|
571
|
+
}
|
|
572
|
+
return pts;
|
|
573
|
+
}
|
|
574
|
+
function spiralPath(pts) {
|
|
575
|
+
return pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(" ");
|
|
576
|
+
}
|
|
577
|
+
function pointAtProgress(pts, progress) {
|
|
578
|
+
const idx = Math.min(pts.length - 1, Math.max(0, Math.round(progress * (pts.length - 1))));
|
|
579
|
+
return pts[idx];
|
|
580
|
+
}
|
|
280
581
|
function createSliderInstance(container, config) {
|
|
281
582
|
const t = STR3[config.locale];
|
|
583
|
+
const theme = config.theme ?? "light";
|
|
584
|
+
const recorder = new TrackRecorder();
|
|
282
585
|
let current;
|
|
283
586
|
let listeners = [];
|
|
284
587
|
let startTime = Date.now();
|
|
285
588
|
let done = false;
|
|
589
|
+
let locked = false;
|
|
286
590
|
function render2() {
|
|
287
591
|
current = generateChallenge3();
|
|
288
592
|
startTime = Date.now();
|
|
289
593
|
done = false;
|
|
594
|
+
locked = false;
|
|
595
|
+
recorder.clear();
|
|
596
|
+
const pts = spiralPoints();
|
|
597
|
+
const pathD = spiralPath(pts);
|
|
290
598
|
container.innerHTML = `
|
|
291
599
|
<style>
|
|
292
|
-
.fc-slider{font-family:-apple-system,system-ui,sans-serif;width:
|
|
293
|
-
.fc-slider-
|
|
294
|
-
.fc-slider-
|
|
295
|
-
.fc-slider
|
|
296
|
-
.fc-slider-
|
|
600
|
+
.fc-slider{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
601
|
+
.fc-slider[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
602
|
+
.fc-slider[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
603
|
+
.fc-slider{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
604
|
+
.fc-slider-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
|
|
605
|
+
.fc-slider-stage{position:relative;width:${CANVAS_W}px;height:${CANVAS_H}px;margin:0 auto;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:10px;overflow:hidden}
|
|
606
|
+
.fc-slider-svg{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
|
|
607
|
+
.fc-slider-path-bg{fill:none;stroke:var(--fc-border);stroke-width:3;stroke-linecap:round}
|
|
608
|
+
.fc-slider-path-fg{fill:none;stroke:var(--fc-accent);stroke-width:4;stroke-linecap:round;transition:stroke .15s}
|
|
609
|
+
.fc-slider-target{fill:none;stroke:var(--fc-success);stroke-width:2;stroke-dasharray:4 3;opacity:.7}
|
|
610
|
+
.fc-slider-handle{position:absolute;width:${HANDLE_W}px;height:${HANDLE_H}px;background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.2);user-select:none;touch-action:none;transition:border-color .15s;transform:translate(-50%,-50%);z-index:2}
|
|
297
611
|
.fc-slider-handle:active{cursor:grabbing}
|
|
298
|
-
.fc-slider-handle::after{content:'
|
|
299
|
-
.fc-slider-tip{position:absolute;left:50%;top:
|
|
300
|
-
.fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color
|
|
612
|
+
.fc-slider-handle::after{content:'';width:10px;height:10px;background:var(--fc-accent);border-radius:50%;transition:background .15s}
|
|
613
|
+
.fc-slider-tip{position:absolute;left:50%;top:8px;transform:translateX(-50%);font-size:12px;color:var(--fc-text-soft);pointer-events:none;transition:opacity .2s;background:var(--fc-bg);padding:2px 8px;border-radius:4px}
|
|
614
|
+
.fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
|
|
615
|
+
.fc-slider-row{display:flex;justify-content:center;margin-top:10px}
|
|
616
|
+
.fc-slider-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
617
|
+
.fc-slider-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
618
|
+
.fc-slider-done .fc-slider-handle{border-color:var(--fc-success)}
|
|
619
|
+
.fc-slider-done .fc-slider-handle::after{background:var(--fc-success)}
|
|
620
|
+
.fc-slider-done .fc-slider-path-fg{stroke:var(--fc-success)}
|
|
301
621
|
</style>
|
|
302
|
-
<div class="fc-slider">
|
|
622
|
+
<div class="fc-slider" data-theme="${theme}">
|
|
303
623
|
<div class="fc-slider-title">${t.title}</div>
|
|
304
|
-
<div class="fc-slider-
|
|
305
|
-
<
|
|
624
|
+
<div class="fc-slider-stage">
|
|
625
|
+
<svg class="fc-slider-svg" viewBox="0 0 ${CANVAS_W} ${CANVAS_H}">
|
|
626
|
+
<path class="fc-slider-path-bg" d="${pathD}"></path>
|
|
627
|
+
<path class="fc-slider-path-fg" d="${pathD}"></path>
|
|
628
|
+
<circle class="fc-slider-target" cx="${CX}" cy="${CY}" r="14"></circle>
|
|
629
|
+
</svg>
|
|
306
630
|
<div class="fc-slider-handle" role="slider" tabindex="0"></div>
|
|
307
631
|
<span class="fc-slider-tip">${t.tip}</span>
|
|
308
632
|
</div>
|
|
309
633
|
<div class="fc-slider-msg"></div>
|
|
634
|
+
<div class="fc-slider-row">
|
|
635
|
+
<button class="fc-slider-refresh">${t.refresh}</button>
|
|
636
|
+
</div>
|
|
310
637
|
</div>
|
|
311
638
|
`;
|
|
312
|
-
const
|
|
639
|
+
const root = container.querySelector(".fc-slider");
|
|
640
|
+
const stage = container.querySelector(".fc-slider-stage");
|
|
313
641
|
const handle = container.querySelector(".fc-slider-handle");
|
|
314
|
-
const progress = container.querySelector(".fc-slider-progress");
|
|
315
642
|
const tip = container.querySelector(".fc-slider-tip");
|
|
316
643
|
const msg = container.querySelector(".fc-slider-msg");
|
|
644
|
+
const refreshBtn = container.querySelector(".fc-slider-refresh");
|
|
645
|
+
const fgPath = container.querySelector(".fc-slider-path-fg");
|
|
646
|
+
refreshBtn.addEventListener("click", render2);
|
|
647
|
+
const totalLen = fgPath.getTotalLength();
|
|
648
|
+
fgPath.style.strokeDasharray = `${totalLen}`;
|
|
649
|
+
fgPath.style.strokeDashoffset = `${totalLen}`;
|
|
317
650
|
let dragging = false;
|
|
318
|
-
let
|
|
319
|
-
let
|
|
320
|
-
let
|
|
321
|
-
function
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
651
|
+
let startPointer = { x: 0, y: 0 };
|
|
652
|
+
let startProgress = 0;
|
|
653
|
+
let currentProgress = 0;
|
|
654
|
+
function applyProgress(p) {
|
|
655
|
+
currentProgress = Math.max(0, Math.min(1, p));
|
|
656
|
+
const pt = pointAtProgress(pts, currentProgress);
|
|
657
|
+
handle.style.left = `${pt.x / CANVAS_W * 100}%`;
|
|
658
|
+
handle.style.top = `${pt.y / CANVAS_H * 100}%`;
|
|
659
|
+
fgPath.style.strokeDashoffset = `${totalLen * (1 - currentProgress)}`;
|
|
660
|
+
tip.style.opacity = currentProgress > 0.02 ? "0" : "1";
|
|
325
661
|
}
|
|
662
|
+
function bounceBack() {
|
|
663
|
+
handle.style.transition = "left .35s ease, top .35s ease, border-color .15s";
|
|
664
|
+
fgPath.style.transition = "stroke-dashoffset .35s ease, stroke .15s";
|
|
665
|
+
applyProgress(0);
|
|
666
|
+
setTimeout(() => {
|
|
667
|
+
handle.style.transition = "border-color .15s";
|
|
668
|
+
fgPath.style.transition = "stroke .15s";
|
|
669
|
+
}, 360);
|
|
670
|
+
}
|
|
671
|
+
applyProgress(0);
|
|
326
672
|
handle.addEventListener("pointerdown", (e) => {
|
|
327
|
-
if (done) return;
|
|
673
|
+
if (done || locked) return;
|
|
328
674
|
dragging = true;
|
|
329
|
-
|
|
330
|
-
|
|
675
|
+
startPointer = { x: e.clientX, y: e.clientY };
|
|
676
|
+
startProgress = currentProgress;
|
|
677
|
+
recorder.start();
|
|
331
678
|
try {
|
|
332
679
|
handle.setPointerCapture(e.pointerId);
|
|
333
680
|
} catch {
|
|
@@ -336,39 +683,55 @@ var FunnyCaptcha = (() => {
|
|
|
336
683
|
});
|
|
337
684
|
handle.addEventListener("pointermove", (e) => {
|
|
338
685
|
if (!dragging) return;
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
686
|
+
const rect = stage.getBoundingClientRect();
|
|
687
|
+
const sx = rect.width / CANVAS_W;
|
|
688
|
+
const sy = rect.height / CANVAS_H;
|
|
689
|
+
const svgX = (e.clientX - rect.left) / sx;
|
|
690
|
+
const svgY = (e.clientY - rect.top) / sy;
|
|
691
|
+
const dx = svgX - CX;
|
|
692
|
+
const dy = svgY - CY;
|
|
693
|
+
const r = Math.sqrt(dx * dx + dy * dy);
|
|
694
|
+
const progress = 1 - r / SPIRAL_R_MAX;
|
|
695
|
+
const step = progress - currentProgress;
|
|
696
|
+
const maxStep = 0.08;
|
|
697
|
+
const clampedStep = Math.max(-maxStep, Math.min(maxStep, step));
|
|
698
|
+
applyProgress(currentProgress + clampedStep);
|
|
699
|
+
recorder.record(e.clientX, e.clientY);
|
|
344
700
|
});
|
|
345
701
|
const finish = async () => {
|
|
346
702
|
if (!dragging) return;
|
|
347
703
|
dragging = false;
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const result = {
|
|
353
|
-
success: true,
|
|
354
|
-
proof: await hashProof(proofInput(current)),
|
|
355
|
-
duration: Date.now() - startTime
|
|
356
|
-
};
|
|
357
|
-
msg.textContent = t.success;
|
|
358
|
-
config.onVerify?.(result);
|
|
359
|
-
listeners.forEach((cb) => cb(result));
|
|
360
|
-
} else {
|
|
361
|
-
currentOffset = 0;
|
|
362
|
-
handle.style.transition = "left .3s";
|
|
363
|
-
progress.style.transition = "width .3s";
|
|
364
|
-
applyPos(0);
|
|
704
|
+
const trackPoints = recorder.stop();
|
|
705
|
+
if (currentProgress < 0.85) {
|
|
706
|
+
bounceBack();
|
|
707
|
+
msg.style.color = "var(--fc-danger)";
|
|
365
708
|
msg.textContent = t.fail;
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const analysis = analyzeTrack(trackPoints);
|
|
712
|
+
if (analysis.isBot) {
|
|
713
|
+
locked = true;
|
|
714
|
+
msg.style.color = "var(--fc-danger)";
|
|
715
|
+
msg.textContent = config.locale === "zh" ? `\u7591\u4F3C\u673A\u5668\u4EBA\u64CD\u4F5C\uFF08${analysis.reasons.join("\u3001")}\uFF09` : `${t.botFail} detected`;
|
|
716
|
+
bounceBack();
|
|
366
717
|
setTimeout(() => {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}, 320);
|
|
718
|
+
if (!done) render2();
|
|
719
|
+
}, 2e3);
|
|
720
|
+
return;
|
|
371
721
|
}
|
|
722
|
+
done = true;
|
|
723
|
+
applyProgress(1);
|
|
724
|
+
root.classList.add("fc-slider-done");
|
|
725
|
+
const result = {
|
|
726
|
+
success: true,
|
|
727
|
+
proof: await hashProof(proofInput(current)),
|
|
728
|
+
duration: Date.now() - startTime,
|
|
729
|
+
metadata: { humanScore: analysis.humanScore, reasons: analysis.reasons }
|
|
730
|
+
};
|
|
731
|
+
msg.style.color = "var(--fc-success)";
|
|
732
|
+
msg.textContent = t.success;
|
|
733
|
+
config.onVerify?.(result);
|
|
734
|
+
listeners.forEach((cb) => cb(result));
|
|
372
735
|
};
|
|
373
736
|
handle.addEventListener("pointerup", finish);
|
|
374
737
|
handle.addEventListener("pointercancel", finish);
|
|
@@ -414,51 +777,100 @@ var FunnyCaptcha = (() => {
|
|
|
414
777
|
return c.order.join(",");
|
|
415
778
|
}
|
|
416
779
|
var STR4 = {
|
|
417
|
-
zh: { title: "\u6309 1
|
|
418
|
-
en: { title: "Click in order 1
|
|
780
|
+
zh: { title: "\u6309 1 -> 2 -> 3 -> 4 \u987A\u5E8F\u70B9\u51FB\uFF08\u4F4D\u7F6E\u4F1A\u53D8\uFF09", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5", refresh: "\u5237\u65B0" },
|
|
781
|
+
en: { title: "Click in order 1 -> 2 -> 3 -> 4 (positions shift)", success: "Verified", fail: "Wrong order, try again", refresh: "Refresh" }
|
|
419
782
|
};
|
|
420
|
-
var
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
783
|
+
var AREA_W = 280;
|
|
784
|
+
var AREA_H = 180;
|
|
785
|
+
var BOX_W = 100;
|
|
786
|
+
var BOX_H = 60;
|
|
787
|
+
var MIN_DIST = 110;
|
|
788
|
+
function generateSlots(count) {
|
|
789
|
+
const maxX = AREA_W - BOX_W;
|
|
790
|
+
const maxY = AREA_H - BOX_H;
|
|
791
|
+
const slots = [];
|
|
792
|
+
let attempts = 0;
|
|
793
|
+
while (slots.length < count && attempts < 1e3) {
|
|
794
|
+
attempts++;
|
|
795
|
+
const candidate = { x: Math.random() * maxX, y: Math.random() * maxY };
|
|
796
|
+
let ok = true;
|
|
797
|
+
for (const s of slots) {
|
|
798
|
+
const dx = candidate.x - s.x;
|
|
799
|
+
const dy = candidate.y - s.y;
|
|
800
|
+
if (Math.sqrt(dx * dx + dy * dy) < MIN_DIST) {
|
|
801
|
+
ok = false;
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (ok) slots.push(candidate);
|
|
806
|
+
}
|
|
807
|
+
while (slots.length < count) {
|
|
808
|
+
slots.push({ x: Math.random() * maxX, y: Math.random() * maxY });
|
|
809
|
+
}
|
|
810
|
+
return slots;
|
|
811
|
+
}
|
|
426
812
|
function createClickOrderInstance(container, config) {
|
|
427
813
|
const t = STR4[config.locale];
|
|
814
|
+
const theme = config.theme ?? "light";
|
|
428
815
|
let current;
|
|
429
816
|
let listeners = [];
|
|
430
817
|
let startTime = Date.now();
|
|
431
818
|
let clicked = [];
|
|
819
|
+
let doneSet = /* @__PURE__ */ new Set();
|
|
432
820
|
let finished = false;
|
|
433
821
|
function render2() {
|
|
434
822
|
current = generateChallenge4();
|
|
435
823
|
clicked = [];
|
|
824
|
+
doneSet = /* @__PURE__ */ new Set();
|
|
436
825
|
finished = false;
|
|
437
826
|
startTime = Date.now();
|
|
827
|
+
const slots = generateSlots(current.targets.length);
|
|
438
828
|
const boxes = current.targets.map((num, i) => {
|
|
439
|
-
const slot =
|
|
440
|
-
return `<div class="fc-click-box" data-num="${num}" style="left:${slot.x}px;top:${slot.y}px;">${num}</div>`;
|
|
829
|
+
const slot = slots[i];
|
|
830
|
+
return `<div class="fc-click-box" data-idx="${i}" data-num="${num}" style="left:${Math.round(slot.x)}px;top:${Math.round(slot.y)}px;">${num}</div>`;
|
|
441
831
|
}).join("");
|
|
442
832
|
container.innerHTML = `
|
|
443
833
|
<style>
|
|
444
|
-
.fc-click{font-family:-apple-system,system-ui,sans-serif;width:
|
|
445
|
-
.fc-click-
|
|
446
|
-
.fc-click-
|
|
447
|
-
.fc-click
|
|
834
|
+
.fc-click{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
835
|
+
.fc-click[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
836
|
+
.fc-click[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
837
|
+
.fc-click{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
838
|
+
.fc-click-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
|
|
839
|
+
.fc-click-area{position:relative;width:${AREA_W}px;height:${AREA_H}px;background:var(--fc-surface);border-radius:8px;border:1px solid var(--fc-border);overflow:hidden}
|
|
840
|
+
.fc-click-box{position:absolute;width:${BOX_W}px;height:${BOX_H}px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:bold;color:var(--fc-accent);background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:8px;cursor:pointer;user-select:none;transition:left .4s cubic-bezier(.4,0,.2,1),top .4s cubic-bezier(.4,0,.2,1),transform .15s,background .15s,color .15s,border-color .15s}
|
|
448
841
|
.fc-click-box:hover{transform:scale(1.05)}
|
|
449
|
-
.fc-click-box-active{background
|
|
450
|
-
.fc-click-box-done{background
|
|
842
|
+
.fc-click-box-active{background:var(--fc-accent);color:var(--fc-bg)}
|
|
843
|
+
.fc-click-box-done{background:var(--fc-success);border-color:var(--fc-success);color:#fff;cursor:default}
|
|
451
844
|
.fc-click-box-done:hover{transform:none}
|
|
452
|
-
.fc-click-
|
|
845
|
+
.fc-click-row{display:flex;justify-content:center;margin-top:10px}
|
|
846
|
+
.fc-click-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
847
|
+
.fc-click-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
848
|
+
.fc-click-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
|
|
453
849
|
</style>
|
|
454
|
-
<div class="fc-click">
|
|
850
|
+
<div class="fc-click" data-theme="${theme}">
|
|
455
851
|
<div class="fc-click-title">${t.title}</div>
|
|
456
852
|
<div class="fc-click-area">${boxes}</div>
|
|
457
853
|
<div class="fc-click-msg"></div>
|
|
854
|
+
<div class="fc-click-row">
|
|
855
|
+
<button class="fc-click-refresh">${t.refresh}</button>
|
|
856
|
+
</div>
|
|
458
857
|
</div>
|
|
459
858
|
`;
|
|
460
859
|
const area = container.querySelector(".fc-click-area");
|
|
461
860
|
const msg = container.querySelector(".fc-click-msg");
|
|
861
|
+
const refreshBtn = container.querySelector(".fc-click-refresh");
|
|
862
|
+
refreshBtn.addEventListener("click", render2);
|
|
863
|
+
function relocateBoxes(excludeIndex) {
|
|
864
|
+
const remaining = current.targets.map((_, i) => i).filter((i) => i !== excludeIndex && !doneSet.has(i));
|
|
865
|
+
if (remaining.length === 0) return;
|
|
866
|
+
const newSlots = generateSlots(remaining.length);
|
|
867
|
+
remaining.forEach((idx, j) => {
|
|
868
|
+
const box = area.querySelector(`[data-idx="${idx}"]`);
|
|
869
|
+
const slot = newSlots[j];
|
|
870
|
+
box.style.left = `${Math.round(slot.x)}px`;
|
|
871
|
+
box.style.top = `${Math.round(slot.y)}px`;
|
|
872
|
+
});
|
|
873
|
+
}
|
|
462
874
|
area.querySelectorAll(".fc-click-box").forEach((el) => {
|
|
463
875
|
el.addEventListener("click", async () => {
|
|
464
876
|
if (finished) return;
|
|
@@ -467,7 +879,9 @@ var FunnyCaptcha = (() => {
|
|
|
467
879
|
const num = Number(box.dataset.num);
|
|
468
880
|
const expected = current.order[clicked.length];
|
|
469
881
|
if (num === expected) {
|
|
882
|
+
const idx = Number(box.dataset.idx);
|
|
470
883
|
clicked.push(num);
|
|
884
|
+
doneSet.add(idx);
|
|
471
885
|
box.classList.add("fc-click-box-done");
|
|
472
886
|
if (clicked.length === current.order.length) {
|
|
473
887
|
finished = true;
|
|
@@ -479,6 +893,8 @@ var FunnyCaptcha = (() => {
|
|
|
479
893
|
msg.textContent = t.success;
|
|
480
894
|
config.onVerify?.(result);
|
|
481
895
|
listeners.forEach((cb) => cb(result));
|
|
896
|
+
} else {
|
|
897
|
+
relocateBoxes(idx);
|
|
482
898
|
}
|
|
483
899
|
} else {
|
|
484
900
|
finished = true;
|
|
@@ -527,11 +943,12 @@ var FunnyCaptcha = (() => {
|
|
|
527
943
|
return `${c.angle}:aligned`;
|
|
528
944
|
}
|
|
529
945
|
var STR5 = {
|
|
530
|
-
zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5C06\u56FE\u5F62\u8F6C\u6B63", success: "\u9A8C\u8BC1\u6210\u529F", hint: "\u8BA9\u7BAD\u5934\u6307\u5411\u4E0A\u65B9" },
|
|
531
|
-
en: { title: "Drag to rotate the figure upright", success: "Verified", hint: "Point the arrow up" }
|
|
946
|
+
zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5C06\u56FE\u5F62\u8F6C\u6B63", success: "\u9A8C\u8BC1\u6210\u529F", hint: "\u8BA9\u7BAD\u5934\u6307\u5411\u4E0A\u65B9", refresh: "\u5237\u65B0" },
|
|
947
|
+
en: { title: "Drag to rotate the figure upright", success: "Verified", hint: "Point the arrow up", refresh: "Refresh" }
|
|
532
948
|
};
|
|
533
949
|
function createRotateInstance(container, config) {
|
|
534
950
|
const t = STR5[config.locale];
|
|
951
|
+
const theme = config.theme ?? "light";
|
|
535
952
|
let current;
|
|
536
953
|
let listeners = [];
|
|
537
954
|
let startTime = Date.now();
|
|
@@ -542,31 +959,42 @@ var FunnyCaptcha = (() => {
|
|
|
542
959
|
done = false;
|
|
543
960
|
container.innerHTML = `
|
|
544
961
|
<style>
|
|
545
|
-
.fc-rotate{font-family:-apple-system,system-ui,sans-serif;width:
|
|
546
|
-
.fc-rotate-
|
|
547
|
-
.fc-rotate-
|
|
962
|
+
.fc-rotate{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:12px}
|
|
963
|
+
.fc-rotate[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
964
|
+
.fc-rotate[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
965
|
+
.fc-rotate{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
966
|
+
.fc-rotate-title{font-size:14px;color:var(--fc-text);text-align:center}
|
|
967
|
+
.fc-rotate-stage{position:relative;width:140px;height:140px;display:flex;align-items:center;justify-content:center;border:2px dashed var(--fc-border);border-radius:50%}
|
|
548
968
|
.fc-rotate-figure{width:100px;height:100px;transform-origin:center;transition:transform .05s}
|
|
549
|
-
.fc-rotate-marker{position:absolute;top:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:12px solid
|
|
550
|
-
.fc-rotate-hint{font-size:12px;color
|
|
551
|
-
.fc-rotate-range{width:240px;accent-color
|
|
552
|
-
.fc-rotate-msg{font-size:13px;min-height:18px;text-align:center;color
|
|
969
|
+
.fc-rotate-marker{position:absolute;top:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:12px solid var(--fc-danger)}
|
|
970
|
+
.fc-rotate-hint{font-size:12px;color:var(--fc-text-soft)}
|
|
971
|
+
.fc-rotate-range{width:240px;accent-color:var(--fc-accent)}
|
|
972
|
+
.fc-rotate-msg{font-size:13px;min-height:18px;text-align:center;color:var(--fc-success)}
|
|
973
|
+
.fc-rotate-row{display:flex;justify-content:center}
|
|
974
|
+
.fc-rotate-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
975
|
+
.fc-rotate-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
553
976
|
</style>
|
|
554
|
-
<div class="fc-rotate">
|
|
977
|
+
<div class="fc-rotate" data-theme="${theme}">
|
|
555
978
|
<div class="fc-rotate-title">${t.title}</div>
|
|
556
979
|
<div class="fc-rotate-stage">
|
|
557
980
|
<div class="fc-rotate-figure" style="transform: rotate(${current.angle}deg)">
|
|
558
|
-
<svg viewBox="0 0 100 100" width="100" height="100"><
|
|
981
|
+
<svg viewBox="0 0 100 100" width="100" height="100"><path d="M 50 8 L 82 42 L 62 42 L 62 92 L 38 92 L 38 42 Z" fill="var(--fc-accent)"/></svg>
|
|
559
982
|
</div>
|
|
560
983
|
<div class="fc-rotate-marker"></div>
|
|
561
984
|
</div>
|
|
562
985
|
<div class="fc-rotate-hint">${t.hint}</div>
|
|
563
986
|
<input class="fc-rotate-range" type="range" min="0" max="360" step="1" value="0" />
|
|
564
987
|
<div class="fc-rotate-msg"></div>
|
|
988
|
+
<div class="fc-rotate-row">
|
|
989
|
+
<button class="fc-rotate-refresh">${t.refresh}</button>
|
|
990
|
+
</div>
|
|
565
991
|
</div>
|
|
566
992
|
`;
|
|
567
993
|
const figure = container.querySelector(".fc-rotate-figure");
|
|
568
994
|
const range = container.querySelector(".fc-rotate-range");
|
|
569
995
|
const msg = container.querySelector(".fc-rotate-msg");
|
|
996
|
+
const refreshBtn = container.querySelector(".fc-rotate-refresh");
|
|
997
|
+
refreshBtn.addEventListener("click", render2);
|
|
570
998
|
const update = async () => {
|
|
571
999
|
if (done) return;
|
|
572
1000
|
const v = Number(range.value);
|
|
@@ -649,11 +1077,12 @@ var FunnyCaptcha = (() => {
|
|
|
649
1077
|
return sorted.every((v, i) => v === c.diffs[i]);
|
|
650
1078
|
}
|
|
651
1079
|
var STR6 = {
|
|
652
|
-
zh: { title: "\u627E\u51FA\u4E24\u5904\u4E0D\u540C", hint: "\u70B9\u51FB\u53F3\u4FA7\u7F51\u683C\u4E2D\u4E0D\u540C\u7684\u683C\u5B50", found: "\u5DF2\u627E\u5230", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u627E\u5F97\u4E0D\u5BF9\uFF0C\u518D\u6765\u4E00\u6B21" },
|
|
653
|
-
en: { title: "Spot 2 differences", hint: "Click the differing cells on the right grid", found: "Found", success: "Verified", fail: "Not quite, try again" }
|
|
1080
|
+
zh: { title: "\u627E\u51FA\u4E24\u5904\u4E0D\u540C", hint: "\u70B9\u51FB\u53F3\u4FA7\u7F51\u683C\u4E2D\u4E0D\u540C\u7684\u683C\u5B50", found: "\u5DF2\u627E\u5230", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u627E\u5F97\u4E0D\u5BF9\uFF0C\u518D\u6765\u4E00\u6B21", refresh: "\u5237\u65B0" },
|
|
1081
|
+
en: { title: "Spot 2 differences", hint: "Click the differing cells on the right grid", found: "Found", success: "Verified", fail: "Not quite, try again", refresh: "Refresh" }
|
|
654
1082
|
};
|
|
655
1083
|
function createSpotDiffInstance(container, config) {
|
|
656
1084
|
const t = STR6[config.locale];
|
|
1085
|
+
const theme = config.theme ?? "light";
|
|
657
1086
|
let current;
|
|
658
1087
|
let listeners = [];
|
|
659
1088
|
let startTime = Date.now();
|
|
@@ -670,20 +1099,26 @@ var FunnyCaptcha = (() => {
|
|
|
670
1099
|
startTime = Date.now();
|
|
671
1100
|
container.innerHTML = `
|
|
672
1101
|
<style>
|
|
673
|
-
.fc-spot-diff{font-family:-apple-system,system-ui,sans-serif;width:360px;padding:16px;box-sizing:border-box}
|
|
674
|
-
.fc-spot-diff-
|
|
675
|
-
.fc-spot-diff-
|
|
1102
|
+
.fc-spot-diff{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
1103
|
+
.fc-spot-diff[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
1104
|
+
.fc-spot-diff[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
1105
|
+
.fc-spot-diff{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
1106
|
+
.fc-spot-diff-title{font-size:14px;color:var(--fc-text);margin-bottom:6px;text-align:center;font-weight:600}
|
|
1107
|
+
.fc-spot-diff-hint{font-size:12px;color:var(--fc-text-soft);margin-bottom:12px;text-align:center}
|
|
676
1108
|
.fc-spot-diff-grids{display:flex;gap:16px;justify-content:center}
|
|
677
|
-
.fc-spot-diff-grid{display:grid;grid-template-columns:repeat(3,48px);grid-template-rows:repeat(3,48px);gap:4px;background
|
|
678
|
-
.fc-spot-diff-cell{display:flex;align-items:center;justify-content:center;font-size:26px;background
|
|
1109
|
+
.fc-spot-diff-grid{display:grid;grid-template-columns:repeat(3,48px);grid-template-rows:repeat(3,48px);gap:4px;background:var(--fc-surface);padding:8px;border-radius:8px;border:1px solid var(--fc-border)}
|
|
1110
|
+
.fc-spot-diff-cell{display:flex;align-items:center;justify-content:center;font-size:26px;background:var(--fc-bg);border-radius:6px;user-select:none}
|
|
679
1111
|
.fc-spot-diff-clickable{cursor:pointer;transition:transform .12s,box-shadow .12s}
|
|
680
1112
|
.fc-spot-diff-clickable:hover{transform:scale(1.06)}
|
|
681
|
-
.fc-spot-diff-marked{box-shadow:0 0 0 3px
|
|
682
|
-
.fc-spot-diff-wrong{box-shadow:0 0 0 3px
|
|
683
|
-
.fc-spot-diff-status{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color
|
|
684
|
-
.fc-spot-diff-status-fail{color
|
|
1113
|
+
.fc-spot-diff-marked{box-shadow:0 0 0 3px var(--fc-accent) inset;background:var(--fc-accent-soft)}
|
|
1114
|
+
.fc-spot-diff-wrong{box-shadow:0 0 0 3px var(--fc-danger) inset;background:var(--fc-danger)}
|
|
1115
|
+
.fc-spot-diff-status{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-success)}
|
|
1116
|
+
.fc-spot-diff-status-fail{color:var(--fc-danger)}
|
|
1117
|
+
.fc-spot-diff-row{display:flex;justify-content:center;margin-top:8px}
|
|
1118
|
+
.fc-spot-diff-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
1119
|
+
.fc-spot-diff-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
685
1120
|
</style>
|
|
686
|
-
<div class="fc-spot-diff">
|
|
1121
|
+
<div class="fc-spot-diff" data-theme="${theme}">
|
|
687
1122
|
<div class="fc-spot-diff-title">${t.title}</div>
|
|
688
1123
|
<div class="fc-spot-diff-hint">${t.hint}</div>
|
|
689
1124
|
<div class="fc-spot-diff-grids">
|
|
@@ -691,10 +1126,15 @@ var FunnyCaptcha = (() => {
|
|
|
691
1126
|
<div class="fc-spot-diff-grid fc-spot-diff-grid-b">${renderGrid(current.gridB, true)}</div>
|
|
692
1127
|
</div>
|
|
693
1128
|
<div class="fc-spot-diff-status">${t.found} 0/${current.diffs.length}</div>
|
|
1129
|
+
<div class="fc-spot-diff-row">
|
|
1130
|
+
<button class="fc-spot-diff-refresh">${t.refresh}</button>
|
|
1131
|
+
</div>
|
|
694
1132
|
</div>
|
|
695
1133
|
`;
|
|
696
1134
|
const gridB = container.querySelector(".fc-spot-diff-grid-b");
|
|
697
1135
|
const status = container.querySelector(".fc-spot-diff-status");
|
|
1136
|
+
const refreshBtn = container.querySelector(".fc-spot-diff-refresh");
|
|
1137
|
+
refreshBtn.addEventListener("click", render2);
|
|
698
1138
|
gridB.querySelectorAll(".fc-spot-diff-clickable").forEach((el) => {
|
|
699
1139
|
el.addEventListener("click", async () => {
|
|
700
1140
|
if (finished) return;
|
|
@@ -789,11 +1229,12 @@ var FunnyCaptcha = (() => {
|
|
|
789
1229
|
return c.correct === userPick;
|
|
790
1230
|
}
|
|
791
1231
|
var STR7 = {
|
|
792
|
-
zh: { title: "\u9009\u51FA\u7B26\u5408\u63CF\u8FF0\u7684\u8868\u60C5", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u9009\u9519\u4E86\uFF0C\u518D\u8BD5\u4E00\u6B21" },
|
|
793
|
-
en: { title: "Pick the face matching the description", success: "Verified", fail: "Wrong, try again" }
|
|
1232
|
+
zh: { title: "\u9009\u51FA\u7B26\u5408\u63CF\u8FF0\u7684\u8868\u60C5", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u9009\u9519\u4E86\uFF0C\u518D\u8BD5\u4E00\u6B21", refresh: "\u5237\u65B0" },
|
|
1233
|
+
en: { title: "Pick the face matching the description", success: "Verified", fail: "Wrong, try again", refresh: "Refresh" }
|
|
794
1234
|
};
|
|
795
1235
|
function createEmojiMatchInstance(container, config) {
|
|
796
1236
|
const t = STR7[config.locale];
|
|
1237
|
+
const theme = config.theme ?? "light";
|
|
797
1238
|
let current;
|
|
798
1239
|
let listeners = [];
|
|
799
1240
|
let startTime = Date.now();
|
|
@@ -807,30 +1248,42 @@ var FunnyCaptcha = (() => {
|
|
|
807
1248
|
).join("");
|
|
808
1249
|
container.innerHTML = `
|
|
809
1250
|
<style>
|
|
810
|
-
.fc-emoji-match{font-family:-apple-system,system-ui,sans-serif;width:
|
|
811
|
-
.fc-emoji-match-
|
|
812
|
-
.fc-emoji-match-
|
|
1251
|
+
.fc-emoji-match{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
1252
|
+
.fc-emoji-match[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
1253
|
+
.fc-emoji-match[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
1254
|
+
.fc-emoji-match{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
1255
|
+
.fc-emoji-match-title{font-size:14px;color:var(--fc-text);margin-bottom:6px;text-align:center;font-weight:600}
|
|
1256
|
+
.fc-emoji-match-desc{font-size:15px;color:var(--fc-accent);margin-bottom:14px;text-align:center;font-weight:600}
|
|
813
1257
|
.fc-emoji-match-opts{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
|
|
814
|
-
.fc-emoji-match-opt{font-size:30px;height:64px;border:2px solid
|
|
815
|
-
.fc-emoji-match-opt:hover{transform:scale(1.06);border-color
|
|
816
|
-
.fc-emoji-match-opt-wrong{border-color
|
|
817
|
-
.fc-emoji-match-opt-correct{border-color
|
|
818
|
-
.fc-emoji-match-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color
|
|
1258
|
+
.fc-emoji-match-opt{font-size:30px;height:64px;border:2px solid var(--fc-border);background:var(--fc-bg);border-radius:10px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;display:flex;align-items:center;justify-content:center;color:var(--fc-text)}
|
|
1259
|
+
.fc-emoji-match-opt:hover{transform:scale(1.06);border-color:var(--fc-accent)}
|
|
1260
|
+
.fc-emoji-match-opt-wrong{border-color:var(--fc-danger);background:var(--fc-danger)}
|
|
1261
|
+
.fc-emoji-match-opt-correct{border-color:var(--fc-success);background:var(--fc-accent-soft)}
|
|
1262
|
+
.fc-emoji-match-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-danger)}
|
|
1263
|
+
.fc-emoji-match-msg-ok{color:var(--fc-success)}
|
|
1264
|
+
.fc-emoji-match-row{display:flex;justify-content:center;margin-top:8px}
|
|
1265
|
+
.fc-emoji-match-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
1266
|
+
.fc-emoji-match-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
819
1267
|
</style>
|
|
820
|
-
<div class="fc-emoji-match">
|
|
1268
|
+
<div class="fc-emoji-match" data-theme="${theme}">
|
|
821
1269
|
<div class="fc-emoji-match-title">${t.title}</div>
|
|
822
1270
|
<div class="fc-emoji-match-desc">${current.description}</div>
|
|
823
1271
|
<div class="fc-emoji-match-opts">${buttons}</div>
|
|
824
1272
|
<div class="fc-emoji-match-msg"></div>
|
|
1273
|
+
<div class="fc-emoji-match-row">
|
|
1274
|
+
<button class="fc-emoji-match-refresh">${t.refresh}</button>
|
|
1275
|
+
</div>
|
|
825
1276
|
</div>
|
|
826
1277
|
`;
|
|
827
1278
|
const msg = container.querySelector(".fc-emoji-match-msg");
|
|
1279
|
+
const refreshBtn = container.querySelector(".fc-emoji-match-refresh");
|
|
1280
|
+
refreshBtn.addEventListener("click", render2);
|
|
828
1281
|
container.querySelectorAll(".fc-emoji-match-opt").forEach((el) => {
|
|
829
1282
|
el.addEventListener("click", async () => {
|
|
830
1283
|
if (finished) return;
|
|
831
1284
|
const btn = el;
|
|
832
|
-
const
|
|
833
|
-
if (verifyAnswer3(current,
|
|
1285
|
+
const pick6 = btn.dataset.emoji;
|
|
1286
|
+
if (verifyAnswer3(current, pick6)) {
|
|
834
1287
|
finished = true;
|
|
835
1288
|
btn.classList.add("fc-emoji-match-opt-correct");
|
|
836
1289
|
const result = {
|
|
@@ -838,7 +1291,7 @@ var FunnyCaptcha = (() => {
|
|
|
838
1291
|
proof: await hashProof(current.correct),
|
|
839
1292
|
duration: Date.now() - startTime
|
|
840
1293
|
};
|
|
841
|
-
msg.
|
|
1294
|
+
msg.classList.add("fc-emoji-match-msg-ok");
|
|
842
1295
|
msg.textContent = t.success;
|
|
843
1296
|
config.onVerify?.(result);
|
|
844
1297
|
listeners.forEach((cb) => cb(result));
|
|
@@ -906,11 +1359,12 @@ var FunnyCaptcha = (() => {
|
|
|
906
1359
|
return c.correct === userPick;
|
|
907
1360
|
}
|
|
908
1361
|
var STR8 = {
|
|
909
|
-
zh: { title: "\u6897\u56FE\u95EE\u7B54", hint: "\u9009\u51FA\u6B63\u786E\u7684\u6897\u540D", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898" },
|
|
910
|
-
en: { title: "Meme Quiz", hint: "Pick the correct meme name", success: "Verified", fail: "Wrong, try again" }
|
|
1362
|
+
zh: { title: "\u6897\u56FE\u95EE\u7B54", hint: "\u9009\u51FA\u6B63\u786E\u7684\u6897\u540D", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898", refresh: "\u5237\u65B0" },
|
|
1363
|
+
en: { title: "Meme Quiz", hint: "Pick the correct meme name", success: "Verified", fail: "Wrong, try again", refresh: "Refresh" }
|
|
911
1364
|
};
|
|
912
1365
|
function createMemeQuizInstance(container, config) {
|
|
913
1366
|
const t = STR8[config.locale];
|
|
1367
|
+
const theme = config.theme ?? "light";
|
|
914
1368
|
let current;
|
|
915
1369
|
let listeners = [];
|
|
916
1370
|
let startTime = Date.now();
|
|
@@ -924,32 +1378,44 @@ var FunnyCaptcha = (() => {
|
|
|
924
1378
|
).join("");
|
|
925
1379
|
container.innerHTML = `
|
|
926
1380
|
<style>
|
|
927
|
-
.fc-meme-quiz{font-family:-apple-system,system-ui,sans-serif;width:
|
|
928
|
-
.fc-meme-quiz-
|
|
929
|
-
.fc-meme-quiz-
|
|
930
|
-
.fc-meme-quiz
|
|
1381
|
+
.fc-meme-quiz{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
1382
|
+
.fc-meme-quiz[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
1383
|
+
.fc-meme-quiz[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
1384
|
+
.fc-meme-quiz{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
1385
|
+
.fc-meme-quiz-title{font-size:14px;color:var(--fc-text);margin-bottom:6px;text-align:center;font-weight:600}
|
|
1386
|
+
.fc-meme-quiz-hint{font-size:12px;color:var(--fc-text-soft);margin-bottom:10px;text-align:center}
|
|
1387
|
+
.fc-meme-quiz-q{font-size:24px;color:var(--fc-accent);margin-bottom:14px;text-align:center;letter-spacing:2px}
|
|
931
1388
|
.fc-meme-quiz-opts{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
|
932
|
-
.fc-meme-quiz-opt{font-size:15px;height:48px;border:2px solid
|
|
933
|
-
.fc-meme-quiz-opt:hover{transform:scale(1.03);border-color
|
|
934
|
-
.fc-meme-quiz-opt-wrong{border-color
|
|
935
|
-
.fc-meme-quiz-opt-correct{border-color
|
|
936
|
-
.fc-meme-quiz-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color
|
|
1389
|
+
.fc-meme-quiz-opt{font-size:15px;height:48px;border:2px solid var(--fc-border);background:var(--fc-bg);border-radius:8px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;color:var(--fc-text)}
|
|
1390
|
+
.fc-meme-quiz-opt:hover{transform:scale(1.03);border-color:var(--fc-accent)}
|
|
1391
|
+
.fc-meme-quiz-opt-wrong{border-color:var(--fc-danger);background:var(--fc-danger)}
|
|
1392
|
+
.fc-meme-quiz-opt-correct{border-color:var(--fc-success);background:var(--fc-accent-soft)}
|
|
1393
|
+
.fc-meme-quiz-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-danger)}
|
|
1394
|
+
.fc-meme-quiz-msg-ok{color:var(--fc-success)}
|
|
1395
|
+
.fc-meme-quiz-row{display:flex;justify-content:center;margin-top:8px}
|
|
1396
|
+
.fc-meme-quiz-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
1397
|
+
.fc-meme-quiz-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
937
1398
|
</style>
|
|
938
|
-
<div class="fc-meme-quiz">
|
|
1399
|
+
<div class="fc-meme-quiz" data-theme="${theme}">
|
|
939
1400
|
<div class="fc-meme-quiz-title">${t.title}</div>
|
|
940
1401
|
<div class="fc-meme-quiz-hint">${t.hint}</div>
|
|
941
1402
|
<div class="fc-meme-quiz-q">${current.question}</div>
|
|
942
1403
|
<div class="fc-meme-quiz-opts">${buttons}</div>
|
|
943
1404
|
<div class="fc-meme-quiz-msg"></div>
|
|
1405
|
+
<div class="fc-meme-quiz-row">
|
|
1406
|
+
<button class="fc-meme-quiz-refresh">${t.refresh}</button>
|
|
1407
|
+
</div>
|
|
944
1408
|
</div>
|
|
945
1409
|
`;
|
|
946
1410
|
const msg = container.querySelector(".fc-meme-quiz-msg");
|
|
1411
|
+
const refreshBtn = container.querySelector(".fc-meme-quiz-refresh");
|
|
1412
|
+
refreshBtn.addEventListener("click", render2);
|
|
947
1413
|
container.querySelectorAll(".fc-meme-quiz-opt").forEach((el) => {
|
|
948
1414
|
el.addEventListener("click", async () => {
|
|
949
1415
|
if (finished) return;
|
|
950
1416
|
const btn = el;
|
|
951
|
-
const
|
|
952
|
-
if (verifyAnswer4(current,
|
|
1417
|
+
const pick6 = btn.textContent ?? "";
|
|
1418
|
+
if (verifyAnswer4(current, pick6)) {
|
|
953
1419
|
finished = true;
|
|
954
1420
|
btn.classList.add("fc-meme-quiz-opt-correct");
|
|
955
1421
|
const result = {
|
|
@@ -957,7 +1423,7 @@ var FunnyCaptcha = (() => {
|
|
|
957
1423
|
proof: await hashProof(current.correct),
|
|
958
1424
|
duration: Date.now() - startTime
|
|
959
1425
|
};
|
|
960
|
-
msg.
|
|
1426
|
+
msg.classList.add("fc-meme-quiz-msg-ok");
|
|
961
1427
|
msg.textContent = t.success;
|
|
962
1428
|
config.onVerify?.(result);
|
|
963
1429
|
listeners.forEach((cb) => cb(result));
|
|
@@ -1014,7 +1480,8 @@ var FunnyCaptcha = (() => {
|
|
|
1014
1480
|
time: "\u5269\u4F59",
|
|
1015
1481
|
sec: "\u79D2",
|
|
1016
1482
|
success: "\u9A8C\u8BC1\u6210\u529F\uFF01\u4F60\u679C\u7136\u662F\u4EBA\u7C7B",
|
|
1017
|
-
fail: "\u65F6\u95F4\u5230\uFF0C\u6CA1\u8FBE\u5230\u76EE\u6807\u5206\u6570"
|
|
1483
|
+
fail: "\u65F6\u95F4\u5230\uFF0C\u6CA1\u8FBE\u5230\u76EE\u6807\u5206\u6570",
|
|
1484
|
+
restart: "\u91CD\u65B0\u5F00\u59CB"
|
|
1018
1485
|
},
|
|
1019
1486
|
en: {
|
|
1020
1487
|
title: "Whack-a-Mole",
|
|
@@ -1023,12 +1490,16 @@ var FunnyCaptcha = (() => {
|
|
|
1023
1490
|
time: "Time",
|
|
1024
1491
|
sec: "s",
|
|
1025
1492
|
success: "Verified! You are human.",
|
|
1026
|
-
fail: "Time's up, target not reached"
|
|
1493
|
+
fail: "Time's up, target not reached",
|
|
1494
|
+
restart: "Restart"
|
|
1027
1495
|
}
|
|
1028
1496
|
};
|
|
1029
1497
|
var HOLE_COUNT = 9;
|
|
1498
|
+
var MOLE_MIN_DELAY = 600;
|
|
1499
|
+
var MOLE_MAX_DELAY = 1200;
|
|
1030
1500
|
function createMiniGameInstance(container, config) {
|
|
1031
1501
|
const t = STR9[config.locale];
|
|
1502
|
+
const theme = config.theme ?? "light";
|
|
1032
1503
|
let current;
|
|
1033
1504
|
let listeners = [];
|
|
1034
1505
|
let startTime = Date.now();
|
|
@@ -1045,7 +1516,7 @@ var FunnyCaptcha = (() => {
|
|
|
1045
1516
|
countdownTimer = null;
|
|
1046
1517
|
}
|
|
1047
1518
|
if (moleTimer) {
|
|
1048
|
-
|
|
1519
|
+
clearTimeout(moleTimer);
|
|
1049
1520
|
moleTimer = null;
|
|
1050
1521
|
}
|
|
1051
1522
|
if (hideTimer) {
|
|
@@ -1093,6 +1564,14 @@ var FunnyCaptcha = (() => {
|
|
|
1093
1564
|
if (activeHole === next) activeHole = -1;
|
|
1094
1565
|
}, 700);
|
|
1095
1566
|
}
|
|
1567
|
+
function scheduleMole() {
|
|
1568
|
+
if (ended) return;
|
|
1569
|
+
const delay = MOLE_MIN_DELAY + Math.random() * (MOLE_MAX_DELAY - MOLE_MIN_DELAY);
|
|
1570
|
+
moleTimer = setTimeout(() => {
|
|
1571
|
+
showMole();
|
|
1572
|
+
scheduleMole();
|
|
1573
|
+
}, delay);
|
|
1574
|
+
}
|
|
1096
1575
|
function startGame() {
|
|
1097
1576
|
clearTimers();
|
|
1098
1577
|
score = 0;
|
|
@@ -1114,8 +1593,8 @@ var FunnyCaptcha = (() => {
|
|
|
1114
1593
|
void endGame(verifyScore(current, score));
|
|
1115
1594
|
}
|
|
1116
1595
|
}, 100);
|
|
1117
|
-
moleTimer = setInterval(showMole, 850);
|
|
1118
1596
|
showMole();
|
|
1597
|
+
scheduleMole();
|
|
1119
1598
|
}
|
|
1120
1599
|
function render2() {
|
|
1121
1600
|
current = generateChallenge9();
|
|
@@ -1129,19 +1608,25 @@ var FunnyCaptcha = (() => {
|
|
|
1129
1608
|
).join("");
|
|
1130
1609
|
container.innerHTML = `
|
|
1131
1610
|
<style>
|
|
1132
|
-
.fc-mini-game{font-family:-apple-system,system-ui,sans-serif;width:
|
|
1133
|
-
.fc-mini-game-
|
|
1134
|
-
.fc-mini-game-
|
|
1135
|
-
.fc-mini-game
|
|
1136
|
-
.fc-mini-game-
|
|
1137
|
-
.fc-mini-game-
|
|
1611
|
+
.fc-mini-game{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
1612
|
+
.fc-mini-game[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
1613
|
+
.fc-mini-game[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
1614
|
+
.fc-mini-game{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
1615
|
+
.fc-mini-game-title{font-size:15px;color:var(--fc-text);margin-bottom:4px;text-align:center;font-weight:600}
|
|
1616
|
+
.fc-mini-game-rule{font-size:12px;color:var(--fc-text-soft);margin-bottom:10px;text-align:center}
|
|
1617
|
+
.fc-mini-game-hud{display:flex;justify-content:space-between;font-size:13px;color:var(--fc-text-soft);margin-bottom:10px}
|
|
1618
|
+
.fc-mini-game-hud b{color:var(--fc-accent)}
|
|
1619
|
+
.fc-mini-game-board{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);gap:8px;width:300px;height:300px;margin:0 auto;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:10px;padding:8px;box-sizing:border-box}
|
|
1138
1620
|
.fc-mini-game-hole{position:relative;background:radial-gradient(circle at 50% 60%,#6d4c41,#4e342e);border-radius:50%;display:flex;align-items:flex-end;justify-content:center;overflow:hidden;cursor:pointer;user-select:none}
|
|
1139
1621
|
.fc-mini-game-hole:hover{background:radial-gradient(circle at 50% 60%,#7d5c51,#5e443e)}
|
|
1140
1622
|
.fc-mini-game-mole{font-size:34px;line-height:1;transform:translateY(120%);transition:transform .12s ease-out;pointer-events:none}
|
|
1141
1623
|
.fc-mini-game-mole-active .fc-mini-game-mole{transform:translateY(0)}
|
|
1142
|
-
.fc-mini-game-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color
|
|
1624
|
+
.fc-mini-game-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-success)}
|
|
1625
|
+
.fc-mini-game-row{display:flex;justify-content:center;margin-top:8px}
|
|
1626
|
+
.fc-mini-game-restart{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
1627
|
+
.fc-mini-game-restart:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
1143
1628
|
</style>
|
|
1144
|
-
<div class="fc-mini-game">
|
|
1629
|
+
<div class="fc-mini-game" data-theme="${theme}">
|
|
1145
1630
|
<div class="fc-mini-game-title">${t.title}</div>
|
|
1146
1631
|
<div class="fc-mini-game-rule">${t.rule}</div>
|
|
1147
1632
|
<div class="fc-mini-game-hud">
|
|
@@ -1150,8 +1635,13 @@ var FunnyCaptcha = (() => {
|
|
|
1150
1635
|
</div>
|
|
1151
1636
|
<div class="fc-mini-game-board">${holes}</div>
|
|
1152
1637
|
<div class="fc-mini-game-msg"></div>
|
|
1638
|
+
<div class="fc-mini-game-row">
|
|
1639
|
+
<button class="fc-mini-game-restart">${t.restart}</button>
|
|
1640
|
+
</div>
|
|
1153
1641
|
</div>
|
|
1154
1642
|
`;
|
|
1643
|
+
const restartBtn = container.querySelector(".fc-mini-game-restart");
|
|
1644
|
+
restartBtn.addEventListener("click", startGame);
|
|
1155
1645
|
container.querySelectorAll(".fc-mini-game-hole").forEach((el) => {
|
|
1156
1646
|
el.addEventListener("click", () => {
|
|
1157
1647
|
if (ended) return;
|
|
@@ -1233,6 +1723,7 @@ var FunnyCaptcha = (() => {
|
|
|
1233
1723
|
};
|
|
1234
1724
|
function createAntiBotInstance(container, config) {
|
|
1235
1725
|
const t = STR10[config.locale];
|
|
1726
|
+
const theme = config.theme ?? "light";
|
|
1236
1727
|
let current;
|
|
1237
1728
|
let listeners = [];
|
|
1238
1729
|
let startTime = Date.now();
|
|
@@ -1270,17 +1761,20 @@ var FunnyCaptcha = (() => {
|
|
|
1270
1761
|
}
|
|
1271
1762
|
container.innerHTML = `
|
|
1272
1763
|
<style>
|
|
1273
|
-
.fc-anti-bot{font-family:-apple-system,system-ui,sans-serif;width:
|
|
1274
|
-
.fc-anti-bot-
|
|
1275
|
-
.fc-anti-bot-
|
|
1276
|
-
.fc-anti-bot
|
|
1277
|
-
.fc-anti-bot-
|
|
1278
|
-
.fc-anti-bot-
|
|
1279
|
-
.fc-anti-bot-
|
|
1280
|
-
.fc-anti-bot-
|
|
1281
|
-
.fc-anti-bot-
|
|
1764
|
+
.fc-anti-bot{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:20px;box-sizing:border-box;border:1px solid var(--fc-border);border-radius:10px;background:var(--fc-surface)}
|
|
1765
|
+
.fc-anti-bot[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
1766
|
+
.fc-anti-bot[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
1767
|
+
.fc-anti-bot{color:var(--fc-text)}
|
|
1768
|
+
.fc-anti-bot-title{font-size:18px;color:var(--fc-text);text-align:center;font-weight:600;margin-bottom:6px}
|
|
1769
|
+
.fc-anti-bot-subtitle{font-size:12px;color:var(--fc-text-soft);text-align:center;margin-bottom:16px}
|
|
1770
|
+
.fc-anti-bot-btn{display:block;width:100%;padding:12px;font-size:15px;border:1px solid var(--fc-border);background:var(--fc-bg);border-radius:6px;cursor:pointer;color:var(--fc-text);transition:background .15s,border-color .15s}
|
|
1771
|
+
.fc-anti-bot-btn:hover:not(:disabled){background:var(--fc-accent-soft);border-color:var(--fc-accent)}
|
|
1772
|
+
.fc-anti-bot-btn:disabled{cursor:default;color:var(--fc-success);border-color:var(--fc-success);background:var(--fc-accent-soft)}
|
|
1773
|
+
.fc-anti-bot-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-success)}
|
|
1774
|
+
.fc-anti-bot-hint{font-size:11px;color:var(--fc-text-soft);margin-top:14px;text-align:center;line-height:1.5;font-style:italic}
|
|
1775
|
+
.fc-anti-bot-footer{font-size:10px;color:var(--fc-text-soft);margin-top:8px;text-align:center;opacity:.7}
|
|
1282
1776
|
</style>
|
|
1283
|
-
<div class="fc-anti-bot">
|
|
1777
|
+
<div class="fc-anti-bot" data-theme="${theme}">
|
|
1284
1778
|
<div class="fc-anti-bot-title">${t.title}</div>
|
|
1285
1779
|
<div class="fc-anti-bot-subtitle">${t.subtitle}</div>
|
|
1286
1780
|
<button class="fc-anti-bot-btn">${t.btn}</button>
|
|
@@ -1330,6 +1824,1291 @@ var FunnyCaptcha = (() => {
|
|
|
1330
1824
|
};
|
|
1331
1825
|
defineCaptcha(antiBotPlugin);
|
|
1332
1826
|
|
|
1827
|
+
// ../captchas/click-text/dist/index.js
|
|
1828
|
+
var COMMON_CHARS = [
|
|
1829
|
+
"\u7684",
|
|
1830
|
+
"\u4E86",
|
|
1831
|
+
"\u662F",
|
|
1832
|
+
"\u5728",
|
|
1833
|
+
"\u6709",
|
|
1834
|
+
"\u4EBA",
|
|
1835
|
+
"\u8FD9",
|
|
1836
|
+
"\u4E2D",
|
|
1837
|
+
"\u5927",
|
|
1838
|
+
"\u4E3A",
|
|
1839
|
+
"\u4E0A",
|
|
1840
|
+
"\u4E2A",
|
|
1841
|
+
"\u56FD",
|
|
1842
|
+
"\u6211",
|
|
1843
|
+
"\u4EE5",
|
|
1844
|
+
"\u8981",
|
|
1845
|
+
"\u4ED6",
|
|
1846
|
+
"\u65F6",
|
|
1847
|
+
"\u6765",
|
|
1848
|
+
"\u7528",
|
|
1849
|
+
"\u4EEC",
|
|
1850
|
+
"\u5730",
|
|
1851
|
+
"\u5230",
|
|
1852
|
+
"\u53EF"
|
|
1853
|
+
];
|
|
1854
|
+
var AREA_W2 = 320;
|
|
1855
|
+
var AREA_H2 = 200;
|
|
1856
|
+
var MARGIN = 30;
|
|
1857
|
+
var MIN_DIST2 = 100;
|
|
1858
|
+
function randInt6(min, max) {
|
|
1859
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
1860
|
+
}
|
|
1861
|
+
function shuffle5(arr) {
|
|
1862
|
+
const a = arr.slice();
|
|
1863
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
1864
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1865
|
+
const tmp = a[i];
|
|
1866
|
+
a[i] = a[j];
|
|
1867
|
+
a[j] = tmp;
|
|
1868
|
+
}
|
|
1869
|
+
return a;
|
|
1870
|
+
}
|
|
1871
|
+
function placePositions(count) {
|
|
1872
|
+
const positions = [];
|
|
1873
|
+
let attempts = 0;
|
|
1874
|
+
while (positions.length < count && attempts < 2e3) {
|
|
1875
|
+
attempts++;
|
|
1876
|
+
const x = MARGIN + Math.random() * (AREA_W2 - MARGIN * 2);
|
|
1877
|
+
const y = MARGIN + Math.random() * (AREA_H2 - MARGIN * 2);
|
|
1878
|
+
const ok = positions.every((p) => Math.hypot(p.x - x, p.y - y) >= MIN_DIST2);
|
|
1879
|
+
if (ok) positions.push({ x, y });
|
|
1880
|
+
}
|
|
1881
|
+
return positions;
|
|
1882
|
+
}
|
|
1883
|
+
function generateChallenge11() {
|
|
1884
|
+
const count = randInt6(3, 4);
|
|
1885
|
+
const chars = shuffle5(COMMON_CHARS).slice(0, count);
|
|
1886
|
+
const positions = placePositions(count);
|
|
1887
|
+
return { chars, positions };
|
|
1888
|
+
}
|
|
1889
|
+
function proofInput6(c) {
|
|
1890
|
+
return `${c.chars.join("")}:completed`;
|
|
1891
|
+
}
|
|
1892
|
+
var STR11 = {
|
|
1893
|
+
zh: { title: "\u8BF7\u6309\u987A\u5E8F\u70B9\u51FB\uFF08\u4F4D\u7F6E\u4F1A\u53D8\uFF09", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5", refresh: "\u5237\u65B0" },
|
|
1894
|
+
en: { title: "Click in order (positions shift)", success: "Verified", fail: "Wrong order, try again", refresh: "Refresh" }
|
|
1895
|
+
};
|
|
1896
|
+
var AREA_W22 = 320;
|
|
1897
|
+
var AREA_H22 = 200;
|
|
1898
|
+
var BOX_W2 = 44;
|
|
1899
|
+
var BOX_H2 = 44;
|
|
1900
|
+
var MIN_DIST22 = 80;
|
|
1901
|
+
function generateSlots2(count) {
|
|
1902
|
+
const maxX = AREA_W22 - BOX_W2;
|
|
1903
|
+
const maxY = AREA_H22 - BOX_H2;
|
|
1904
|
+
const slots = [];
|
|
1905
|
+
let attempts = 0;
|
|
1906
|
+
while (slots.length < count && attempts < 1e3) {
|
|
1907
|
+
attempts++;
|
|
1908
|
+
const candidate = { x: Math.random() * maxX, y: Math.random() * maxY };
|
|
1909
|
+
let ok = true;
|
|
1910
|
+
for (const s of slots) {
|
|
1911
|
+
const dx = candidate.x - s.x;
|
|
1912
|
+
const dy = candidate.y - s.y;
|
|
1913
|
+
if (Math.sqrt(dx * dx + dy * dy) < MIN_DIST22) {
|
|
1914
|
+
ok = false;
|
|
1915
|
+
break;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (ok) slots.push(candidate);
|
|
1919
|
+
}
|
|
1920
|
+
while (slots.length < count) {
|
|
1921
|
+
slots.push({ x: Math.random() * maxX, y: Math.random() * maxY });
|
|
1922
|
+
}
|
|
1923
|
+
return slots;
|
|
1924
|
+
}
|
|
1925
|
+
function createClickTextInstance(container, config) {
|
|
1926
|
+
const t = STR11[config.locale];
|
|
1927
|
+
let current;
|
|
1928
|
+
let listeners = [];
|
|
1929
|
+
let startTime = Date.now();
|
|
1930
|
+
let clicked = [];
|
|
1931
|
+
let doneSet = /* @__PURE__ */ new Set();
|
|
1932
|
+
let finished = false;
|
|
1933
|
+
function render2() {
|
|
1934
|
+
current = generateChallenge11();
|
|
1935
|
+
clicked = [];
|
|
1936
|
+
doneSet = /* @__PURE__ */ new Set();
|
|
1937
|
+
finished = false;
|
|
1938
|
+
startTime = Date.now();
|
|
1939
|
+
const prompt = current.chars.join("\u3001");
|
|
1940
|
+
const chars = current.chars.map((ch, i) => {
|
|
1941
|
+
const p = current.positions[i];
|
|
1942
|
+
return `<div class="fc-click-text-char" data-idx="${i}" data-ch="${ch}" style="left:${p.x}px;top:${p.y}px;">${ch}</div>`;
|
|
1943
|
+
}).join("");
|
|
1944
|
+
container.innerHTML = `
|
|
1945
|
+
<style>
|
|
1946
|
+
.fc-click-text{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
1947
|
+
.fc-click-text[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
1948
|
+
.fc-click-text[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
1949
|
+
.fc-click-text-title{font-size:14px;color:var(--fc-text);margin-bottom:4px;text-align:center}
|
|
1950
|
+
.fc-click-text-prompt{font-size:15px;color:var(--fc-accent);margin-bottom:12px;text-align:center;font-weight:600}
|
|
1951
|
+
.fc-click-text-area{position:relative;width:320px;height:200px;background:var(--fc-surface);border-radius:8px;border:1px solid var(--fc-border);overflow:hidden}
|
|
1952
|
+
.fc-click-text-char{position:absolute;width:44px;height:44px;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:600;color:var(--fc-text);background:var(--fc-bg);border:2px solid var(--fc-border);border-radius:8px;cursor:pointer;user-select:none;transition:left .4s cubic-bezier(.4,0,.2,1),top .4s cubic-bezier(.4,0,.2,1),transform .15s,background .15s,color .15s,border-color .15s}
|
|
1953
|
+
.fc-click-text-char:hover{transform:scale(1.08);border-color:var(--fc-accent)}
|
|
1954
|
+
.fc-click-text-char-done{background:var(--fc-success);border-color:var(--fc-success);color:#fff;cursor:default}
|
|
1955
|
+
.fc-click-text-char-done:hover{transform:none}
|
|
1956
|
+
.fc-click-text-char-wrong{background:var(--fc-danger);border-color:var(--fc-danger);color:#fff}
|
|
1957
|
+
.fc-click-text-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
|
|
1958
|
+
.fc-click-text-msg-fail{color:var(--fc-danger)}
|
|
1959
|
+
.fc-click-text-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer;margin-top:8px;display:block;margin-left:auto;margin-right:auto}
|
|
1960
|
+
.fc-click-text-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
1961
|
+
</style>
|
|
1962
|
+
<div class="fc-click-text" data-theme="${config.theme}">
|
|
1963
|
+
<div class="fc-click-text-title">${t.title}</div>
|
|
1964
|
+
<div class="fc-click-text-prompt">${prompt}</div>
|
|
1965
|
+
<div class="fc-click-text-area">${chars}</div>
|
|
1966
|
+
<div class="fc-click-text-msg"></div>
|
|
1967
|
+
<button class="fc-click-text-refresh">${t.refresh}</button>
|
|
1968
|
+
</div>
|
|
1969
|
+
`;
|
|
1970
|
+
const area = container.querySelector(".fc-click-text-area");
|
|
1971
|
+
const msg = container.querySelector(".fc-click-text-msg");
|
|
1972
|
+
const refreshBtn = container.querySelector(".fc-click-text-refresh");
|
|
1973
|
+
refreshBtn.addEventListener("click", () => render2());
|
|
1974
|
+
function relocateChars(excludeIndex) {
|
|
1975
|
+
const remaining = current.chars.map((_, i) => i).filter((i) => i !== excludeIndex && !doneSet.has(i));
|
|
1976
|
+
if (remaining.length === 0) return;
|
|
1977
|
+
const newSlots = generateSlots2(remaining.length);
|
|
1978
|
+
remaining.forEach((idx, j) => {
|
|
1979
|
+
const box = area.querySelector(`[data-idx="${idx}"]`);
|
|
1980
|
+
const slot = newSlots[j];
|
|
1981
|
+
box.style.left = `${Math.round(slot.x)}px`;
|
|
1982
|
+
box.style.top = `${Math.round(slot.y)}px`;
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
container.querySelectorAll(".fc-click-text-char").forEach((el) => {
|
|
1986
|
+
el.addEventListener("click", async () => {
|
|
1987
|
+
if (finished) return;
|
|
1988
|
+
const box = el;
|
|
1989
|
+
if (box.classList.contains("fc-click-text-char-done")) return;
|
|
1990
|
+
const ch = box.dataset.ch;
|
|
1991
|
+
const expected = current.chars[clicked.length];
|
|
1992
|
+
if (ch === expected) {
|
|
1993
|
+
const idx = Number(box.dataset.idx);
|
|
1994
|
+
clicked.push(ch);
|
|
1995
|
+
doneSet.add(idx);
|
|
1996
|
+
box.classList.add("fc-click-text-char-done");
|
|
1997
|
+
if (clicked.length === current.chars.length) {
|
|
1998
|
+
finished = true;
|
|
1999
|
+
const result = {
|
|
2000
|
+
success: true,
|
|
2001
|
+
proof: await hashProof(proofInput6(current)),
|
|
2002
|
+
duration: Date.now() - startTime
|
|
2003
|
+
};
|
|
2004
|
+
msg.textContent = t.success;
|
|
2005
|
+
config.onVerify?.(result);
|
|
2006
|
+
listeners.forEach((cb) => cb(result));
|
|
2007
|
+
} else {
|
|
2008
|
+
relocateChars(idx);
|
|
2009
|
+
}
|
|
2010
|
+
} else {
|
|
2011
|
+
finished = true;
|
|
2012
|
+
box.classList.add("fc-click-text-char-wrong");
|
|
2013
|
+
msg.classList.add("fc-click-text-msg-fail");
|
|
2014
|
+
msg.textContent = t.fail;
|
|
2015
|
+
setTimeout(render2, 700);
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
return {
|
|
2021
|
+
mount: () => render2(),
|
|
2022
|
+
reset: () => render2(),
|
|
2023
|
+
destroy: () => {
|
|
2024
|
+
container.innerHTML = "";
|
|
2025
|
+
listeners = [];
|
|
2026
|
+
},
|
|
2027
|
+
onResult: (cb) => listeners.push(cb)
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
var clickTextPlugin = {
|
|
2031
|
+
id: "click-text",
|
|
2032
|
+
category: "interactive",
|
|
2033
|
+
create: createClickTextInstance,
|
|
2034
|
+
describe: (locale) => ({
|
|
2035
|
+
name: locale === "zh" ? "\u6587\u5B57\u70B9\u9009" : "Click Text",
|
|
2036
|
+
description: locale === "zh" ? "\u6309\u6307\u5B9A\u987A\u5E8F\u70B9\u51FB\u753B\u5E03\u4E0A\u7684\u6C49\u5B57" : "Click characters on the canvas in the specified order.",
|
|
2037
|
+
tags: ["click", "text", "interactive"]
|
|
2038
|
+
})
|
|
2039
|
+
};
|
|
2040
|
+
defineCaptcha(clickTextPlugin);
|
|
2041
|
+
|
|
2042
|
+
// ../captchas/color-pick/dist/index.js
|
|
2043
|
+
var COLORS = [
|
|
2044
|
+
{ name: "red", zh: "\u7EA2\u8272", en: "red", hex: "#ef4444" },
|
|
2045
|
+
{ name: "blue", zh: "\u84DD\u8272", en: "blue", hex: "#3b82f6" },
|
|
2046
|
+
{ name: "green", zh: "\u7EFF\u8272", en: "green", hex: "#22c55e" },
|
|
2047
|
+
{ name: "yellow", zh: "\u9EC4\u8272", en: "yellow", hex: "#eab308" },
|
|
2048
|
+
{ name: "purple", zh: "\u7D2B\u8272", en: "purple", hex: "#a855f7" },
|
|
2049
|
+
{ name: "orange", zh: "\u6A59\u8272", en: "orange", hex: "#f97316" },
|
|
2050
|
+
{ name: "pink", zh: "\u7C89\u8272", en: "pink", hex: "#ec4899" },
|
|
2051
|
+
{ name: "cyan", zh: "\u9752\u8272", en: "cyan", hex: "#06b6d4" }
|
|
2052
|
+
];
|
|
2053
|
+
function randInt7(min, max) {
|
|
2054
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
2055
|
+
}
|
|
2056
|
+
function shuffle6(arr) {
|
|
2057
|
+
const a = arr.slice();
|
|
2058
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
2059
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
2060
|
+
const tmp = a[i];
|
|
2061
|
+
a[i] = a[j];
|
|
2062
|
+
a[j] = tmp;
|
|
2063
|
+
}
|
|
2064
|
+
return a;
|
|
2065
|
+
}
|
|
2066
|
+
function colorLabel(name, locale) {
|
|
2067
|
+
const c = COLORS.find((c2) => c2.name === name);
|
|
2068
|
+
return c ? locale === "zh" ? c.zh : c.en : name;
|
|
2069
|
+
}
|
|
2070
|
+
function generateChallenge12() {
|
|
2071
|
+
const target = COLORS[randInt7(0, COLORS.length - 1)];
|
|
2072
|
+
const distractorCount = randInt7(5, 8);
|
|
2073
|
+
const others = COLORS.filter((c) => c.name !== target.name);
|
|
2074
|
+
const distractors = shuffle6(others).slice(0, Math.min(distractorCount, others.length));
|
|
2075
|
+
const options = shuffle6([
|
|
2076
|
+
{ name: target.name, hex: target.hex },
|
|
2077
|
+
...distractors.map((d) => ({ name: d.name, hex: d.hex }))
|
|
2078
|
+
]);
|
|
2079
|
+
return { targetColor: target.name, targetHex: target.hex, options };
|
|
2080
|
+
}
|
|
2081
|
+
function proofInput7(c) {
|
|
2082
|
+
return `${c.targetColor}:completed`;
|
|
2083
|
+
}
|
|
2084
|
+
function verifyAnswer5(c, userPickHex) {
|
|
2085
|
+
return c.targetHex === userPickHex;
|
|
2086
|
+
}
|
|
2087
|
+
var STR12 = {
|
|
2088
|
+
zh: { success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u9009\u9519\u4E86\uFF0C\u518D\u8BD5\u4E00\u6B21", refresh: "\u5237\u65B0" },
|
|
2089
|
+
en: { success: "Verified", fail: "Wrong, try again", refresh: "Refresh" }
|
|
2090
|
+
};
|
|
2091
|
+
function createColorPickInstance(container, config) {
|
|
2092
|
+
const t = STR12[config.locale];
|
|
2093
|
+
let current;
|
|
2094
|
+
let listeners = [];
|
|
2095
|
+
let startTime = Date.now();
|
|
2096
|
+
let finished = false;
|
|
2097
|
+
function render2() {
|
|
2098
|
+
current = generateChallenge12();
|
|
2099
|
+
finished = false;
|
|
2100
|
+
startTime = Date.now();
|
|
2101
|
+
const label = colorLabel(current.targetColor, config.locale);
|
|
2102
|
+
const prompt = config.locale === "zh" ? `\u8BF7\u70B9\u51FB${label}\u7684\u65B9\u5757` : `Click the ${label} square`;
|
|
2103
|
+
const squares = current.options.map(
|
|
2104
|
+
(o) => `<button class="fc-color-pick-opt" data-hex="${o.hex}" style="background:${o.hex}" aria-label="${o.name}"></button>`
|
|
2105
|
+
).join("");
|
|
2106
|
+
container.innerHTML = `
|
|
2107
|
+
<style>
|
|
2108
|
+
.fc-color-pick{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
2109
|
+
.fc-color-pick[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
2110
|
+
.fc-color-pick[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
2111
|
+
.fc-color-pick-prompt{font-size:15px;color:var(--fc-text);margin-bottom:12px;text-align:center;font-weight:600}
|
|
2112
|
+
.fc-color-pick-opts{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
|
|
2113
|
+
.fc-color-pick-opt{height:56px;border:2px solid var(--fc-border);border-radius:10px;cursor:pointer;transition:transform .12s,border-color .12s;outline:none;padding:0}
|
|
2114
|
+
.fc-color-pick-opt:hover{transform:scale(1.06);border-color:var(--fc-accent)}
|
|
2115
|
+
.fc-color-pick-opt-correct{border-color:var(--fc-success);transform:scale(1.06)}
|
|
2116
|
+
.fc-color-pick-opt-wrong{border-color:var(--fc-danger)}
|
|
2117
|
+
.fc-color-pick-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-danger)}
|
|
2118
|
+
.fc-color-pick-msg-success{color:var(--fc-success)}
|
|
2119
|
+
.fc-color-pick-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer;margin-top:8px;display:block;margin-left:auto;margin-right:auto}
|
|
2120
|
+
.fc-color-pick-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
2121
|
+
</style>
|
|
2122
|
+
<div class="fc-color-pick" data-theme="${config.theme}">
|
|
2123
|
+
<div class="fc-color-pick-prompt">${prompt}</div>
|
|
2124
|
+
<div class="fc-color-pick-opts">${squares}</div>
|
|
2125
|
+
<div class="fc-color-pick-msg"></div>
|
|
2126
|
+
<button class="fc-color-pick-refresh">${t.refresh}</button>
|
|
2127
|
+
</div>
|
|
2128
|
+
`;
|
|
2129
|
+
const msg = container.querySelector(".fc-color-pick-msg");
|
|
2130
|
+
const refreshBtn = container.querySelector(".fc-color-pick-refresh");
|
|
2131
|
+
refreshBtn.addEventListener("click", () => render2());
|
|
2132
|
+
container.querySelectorAll(".fc-color-pick-opt").forEach((el) => {
|
|
2133
|
+
el.addEventListener("click", async () => {
|
|
2134
|
+
if (finished) return;
|
|
2135
|
+
const btn = el;
|
|
2136
|
+
const pick6 = btn.dataset.hex;
|
|
2137
|
+
if (verifyAnswer5(current, pick6)) {
|
|
2138
|
+
finished = true;
|
|
2139
|
+
btn.classList.add("fc-color-pick-opt-correct");
|
|
2140
|
+
msg.classList.add("fc-color-pick-msg-success");
|
|
2141
|
+
const result = {
|
|
2142
|
+
success: true,
|
|
2143
|
+
proof: await hashProof(proofInput7(current)),
|
|
2144
|
+
duration: Date.now() - startTime
|
|
2145
|
+
};
|
|
2146
|
+
msg.textContent = t.success;
|
|
2147
|
+
config.onVerify?.(result);
|
|
2148
|
+
listeners.forEach((cb) => cb(result));
|
|
2149
|
+
} else {
|
|
2150
|
+
finished = true;
|
|
2151
|
+
btn.classList.add("fc-color-pick-opt-wrong");
|
|
2152
|
+
msg.textContent = t.fail;
|
|
2153
|
+
setTimeout(render2, 800);
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
return {
|
|
2159
|
+
mount: () => render2(),
|
|
2160
|
+
reset: () => render2(),
|
|
2161
|
+
destroy: () => {
|
|
2162
|
+
container.innerHTML = "";
|
|
2163
|
+
listeners = [];
|
|
2164
|
+
},
|
|
2165
|
+
onResult: (cb) => listeners.push(cb)
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
var colorPickPlugin = {
|
|
2169
|
+
id: "color-pick",
|
|
2170
|
+
category: "recognize",
|
|
2171
|
+
create: createColorPickInstance,
|
|
2172
|
+
describe: (locale) => ({
|
|
2173
|
+
name: locale === "zh" ? "\u989C\u8272\u9009\u62E9" : "Color Pick",
|
|
2174
|
+
description: locale === "zh" ? "\u4ECE\u591A\u4E2A\u5F69\u8272\u65B9\u5757\u4E2D\u70B9\u51FB\u6307\u5B9A\u989C\u8272" : "Pick the square of the specified color.",
|
|
2175
|
+
tags: ["color", "pick", "recognize"]
|
|
2176
|
+
})
|
|
2177
|
+
};
|
|
2178
|
+
defineCaptcha(colorPickPlugin);
|
|
2179
|
+
|
|
2180
|
+
// ../captchas/puzzle/dist/index.js
|
|
2181
|
+
var TOLERANCE = 8;
|
|
2182
|
+
function randInt8(min, max) {
|
|
2183
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
2184
|
+
}
|
|
2185
|
+
function generateChallenge13() {
|
|
2186
|
+
return { gapPosition: randInt8(25, 58) };
|
|
2187
|
+
}
|
|
2188
|
+
function proofInput8(c) {
|
|
2189
|
+
return `${c.gapPosition}:completed`;
|
|
2190
|
+
}
|
|
2191
|
+
function verifyPosition(c, userPos) {
|
|
2192
|
+
return Math.abs(userPos - c.gapPosition) <= TOLERANCE;
|
|
2193
|
+
}
|
|
2194
|
+
var STR13 = {
|
|
2195
|
+
zh: { title: "\u62D6\u52A8\u6ED1\u5757\u628A\u62FC\u56FE\u5757\u79FB\u5230\u7F3A\u53E3", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u672A\u5BF9\u9F50\u7F3A\u53E3", botFail: "\u68C0\u6D4B\u5230\u5F02\u5E38\u64CD\u4F5C", refresh: "\u5237\u65B0" },
|
|
2196
|
+
en: { title: "Drag the slider to fit the puzzle", success: "Verified", fail: "Not aligned", botFail: "Bot-like behavior", refresh: "Refresh" }
|
|
2197
|
+
};
|
|
2198
|
+
var STAGE_W = 300;
|
|
2199
|
+
var STAGE_H = 200;
|
|
2200
|
+
var PIECE_W = 50;
|
|
2201
|
+
var MAX_OFFSET = STAGE_W - PIECE_W;
|
|
2202
|
+
var TRACK_W = 300;
|
|
2203
|
+
var TRACK_H = 100;
|
|
2204
|
+
var HANDLE_W2 = 40;
|
|
2205
|
+
var HANDLE_H2 = 40;
|
|
2206
|
+
function zigzagPoints() {
|
|
2207
|
+
const pts = [];
|
|
2208
|
+
const margin = HANDLE_W2 / 2;
|
|
2209
|
+
const w = TRACK_W - margin * 2;
|
|
2210
|
+
const h = TRACK_H - margin * 2;
|
|
2211
|
+
const seg1Steps = 30;
|
|
2212
|
+
for (let i = 0; i <= seg1Steps; i++) {
|
|
2213
|
+
pts.push({ x: margin + w / seg1Steps * i, y: margin });
|
|
2214
|
+
}
|
|
2215
|
+
const seg2Steps = 10;
|
|
2216
|
+
const midY = margin + h / 4;
|
|
2217
|
+
for (let i = 1; i <= seg2Steps; i++) {
|
|
2218
|
+
pts.push({ x: TRACK_W - margin, y: margin + (midY - margin) / seg2Steps * i });
|
|
2219
|
+
}
|
|
2220
|
+
for (let i = 1; i <= seg1Steps; i++) {
|
|
2221
|
+
pts.push({ x: TRACK_W - margin - w / seg1Steps * i, y: midY });
|
|
2222
|
+
}
|
|
2223
|
+
const midY2 = margin + h * 3 / 4;
|
|
2224
|
+
for (let i = 1; i <= seg2Steps; i++) {
|
|
2225
|
+
pts.push({ x: margin, y: midY + (midY2 - midY) / seg2Steps * i });
|
|
2226
|
+
}
|
|
2227
|
+
for (let i = 1; i <= seg1Steps; i++) {
|
|
2228
|
+
pts.push({ x: margin + w / seg1Steps * i, y: midY2 });
|
|
2229
|
+
}
|
|
2230
|
+
for (let i = 1; i <= seg2Steps; i++) {
|
|
2231
|
+
pts.push({ x: TRACK_W - margin, y: midY2 + (TRACK_H - margin - midY2) / seg2Steps * i });
|
|
2232
|
+
}
|
|
2233
|
+
return pts;
|
|
2234
|
+
}
|
|
2235
|
+
function zigzagPath(pts) {
|
|
2236
|
+
return pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(" ");
|
|
2237
|
+
}
|
|
2238
|
+
function pointAtProgress2(pts, progress) {
|
|
2239
|
+
const idx = Math.min(pts.length - 1, Math.max(0, Math.round(progress * (pts.length - 1))));
|
|
2240
|
+
return pts[idx];
|
|
2241
|
+
}
|
|
2242
|
+
function pieceCirclePos(progress) {
|
|
2243
|
+
const cx = STAGE_W / 2;
|
|
2244
|
+
const cy = STAGE_H / 2;
|
|
2245
|
+
const radius = Math.min(STAGE_W, STAGE_H) / 2 - PIECE_W / 2 - 8;
|
|
2246
|
+
const angle = -progress * Math.PI * 2;
|
|
2247
|
+
return {
|
|
2248
|
+
x: cx + radius * Math.cos(angle) - PIECE_W / 2,
|
|
2249
|
+
y: cy + radius * Math.sin(angle) - PIECE_W / 2
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
function createPuzzleInstance(container, config) {
|
|
2253
|
+
const t = STR13[config.locale];
|
|
2254
|
+
const theme = config.theme ?? "light";
|
|
2255
|
+
const recorder = new TrackRecorder();
|
|
2256
|
+
let current;
|
|
2257
|
+
let listeners = [];
|
|
2258
|
+
let startTime = Date.now();
|
|
2259
|
+
let done = false;
|
|
2260
|
+
let locked = false;
|
|
2261
|
+
function render2() {
|
|
2262
|
+
current = generateChallenge13();
|
|
2263
|
+
startTime = Date.now();
|
|
2264
|
+
done = false;
|
|
2265
|
+
locked = false;
|
|
2266
|
+
recorder.clear();
|
|
2267
|
+
const gapLeft = current.gapPosition / 100 * MAX_OFFSET;
|
|
2268
|
+
const pts = zigzagPoints();
|
|
2269
|
+
const pathD = zigzagPath(pts);
|
|
2270
|
+
container.innerHTML = `
|
|
2271
|
+
<style>
|
|
2272
|
+
.fc-puzzle{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
2273
|
+
.fc-puzzle[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
2274
|
+
.fc-puzzle[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
2275
|
+
.fc-puzzle{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
2276
|
+
.fc-puzzle-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
|
|
2277
|
+
.fc-puzzle-stage{position:relative;width:${STAGE_W}px;height:${STAGE_H}px;margin:0 auto;border-radius:8px;overflow:hidden;border:1px solid var(--fc-border);background:linear-gradient(135deg,var(--fc-accent-soft),var(--fc-surface) 60%,var(--fc-accent-soft))}
|
|
2278
|
+
.fc-puzzle-gap{position:absolute;top:${(STAGE_H - PIECE_W) / 2}px;width:${PIECE_W}px;height:${PIECE_W}px;background:var(--fc-bg);border:2px dashed var(--fc-text-soft);border-radius:6px;box-shadow:inset 0 0 8px rgba(0,0,0,.15);transition:left .15s}
|
|
2279
|
+
.fc-puzzle-piece{position:absolute;width:${PIECE_W}px;height:${PIECE_W}px;border-radius:6px;background:linear-gradient(135deg,var(--fc-accent),var(--fc-accent-soft));border:2px solid var(--fc-bg);box-shadow:0 2px 6px rgba(0,0,0,.25);transition:background .15s,border-color .15s}
|
|
2280
|
+
.fc-puzzle-track{position:relative;width:${TRACK_W}px;height:${TRACK_H}px;margin:14px auto 0;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:8px;overflow:hidden}
|
|
2281
|
+
.fc-puzzle-svg{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
|
|
2282
|
+
.fc-puzzle-path-bg{fill:none;stroke:var(--fc-border);stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
|
|
2283
|
+
.fc-puzzle-path-fg{fill:none;stroke:var(--fc-accent);stroke-width:4;stroke-linecap:round;stroke-linejoin:round;transition:stroke .15s}
|
|
2284
|
+
.fc-puzzle-handle{position:absolute;width:${HANDLE_W2}px;height:${HANDLE_H2}px;background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.2);user-select:none;touch-action:none;transition:border-color .15s;transform:translate(-50%,-50%);z-index:2}
|
|
2285
|
+
.fc-puzzle-handle:active{cursor:grabbing}
|
|
2286
|
+
.fc-puzzle-handle::after{content:'\\2192';color:var(--fc-accent);font-size:16px;font-weight:bold;transition:color .15s}
|
|
2287
|
+
.fc-puzzle-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
|
|
2288
|
+
.fc-puzzle-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer;margin-top:8px;display:block;margin-left:auto;margin-right:auto}
|
|
2289
|
+
.fc-puzzle-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
2290
|
+
.fc-puzzle-done .fc-puzzle-piece{background:linear-gradient(135deg,var(--fc-success),var(--fc-accent-soft));border-color:var(--fc-success)}
|
|
2291
|
+
.fc-puzzle-done .fc-puzzle-handle{border-color:var(--fc-success)}
|
|
2292
|
+
.fc-puzzle-done .fc-puzzle-handle::after{color:var(--fc-success)}
|
|
2293
|
+
.fc-puzzle-done .fc-puzzle-path-fg{stroke:var(--fc-success)}
|
|
2294
|
+
</style>
|
|
2295
|
+
<div class="fc-puzzle" data-theme="${theme}">
|
|
2296
|
+
<div class="fc-puzzle-title">${t.title}</div>
|
|
2297
|
+
<div class="fc-puzzle-stage">
|
|
2298
|
+
<div class="fc-puzzle-gap" style="left:${gapLeft}px"></div>
|
|
2299
|
+
<div class="fc-puzzle-piece" style="left:0;top:${(STAGE_H - PIECE_W) / 2}px"></div>
|
|
2300
|
+
</div>
|
|
2301
|
+
<div class="fc-puzzle-track">
|
|
2302
|
+
<svg class="fc-puzzle-svg" viewBox="0 0 ${TRACK_W} ${TRACK_H}">
|
|
2303
|
+
<path class="fc-puzzle-path-bg" d="${pathD}"></path>
|
|
2304
|
+
<path class="fc-puzzle-path-fg" d="${pathD}"></path>
|
|
2305
|
+
</svg>
|
|
2306
|
+
<div class="fc-puzzle-handle" role="slider" tabindex="0"></div>
|
|
2307
|
+
</div>
|
|
2308
|
+
<div class="fc-puzzle-msg"></div>
|
|
2309
|
+
<button class="fc-puzzle-refresh">${t.refresh}</button>
|
|
2310
|
+
</div>
|
|
2311
|
+
`;
|
|
2312
|
+
const root = container.querySelector(".fc-puzzle");
|
|
2313
|
+
const piece = container.querySelector(".fc-puzzle-piece");
|
|
2314
|
+
const track = container.querySelector(".fc-puzzle-track");
|
|
2315
|
+
const handle = container.querySelector(".fc-puzzle-handle");
|
|
2316
|
+
const msg = container.querySelector(".fc-puzzle-msg");
|
|
2317
|
+
const refreshBtn = container.querySelector(".fc-puzzle-refresh");
|
|
2318
|
+
const fgPath = container.querySelector(".fc-puzzle-path-fg");
|
|
2319
|
+
refreshBtn.addEventListener("click", () => render2());
|
|
2320
|
+
const totalLen = fgPath.getTotalLength();
|
|
2321
|
+
fgPath.style.strokeDasharray = `${totalLen}`;
|
|
2322
|
+
fgPath.style.strokeDashoffset = `${totalLen}`;
|
|
2323
|
+
let dragging = false;
|
|
2324
|
+
let startPointer = { x: 0, y: 0 };
|
|
2325
|
+
let startProgress = 0;
|
|
2326
|
+
let currentProgress = 0;
|
|
2327
|
+
function applyProgress(p) {
|
|
2328
|
+
currentProgress = Math.max(0, Math.min(1, p));
|
|
2329
|
+
const pt = pointAtProgress2(pts, currentProgress);
|
|
2330
|
+
handle.style.left = `${pt.x / TRACK_W * 100}%`;
|
|
2331
|
+
handle.style.top = `${pt.y / TRACK_H * 100}%`;
|
|
2332
|
+
fgPath.style.strokeDashoffset = `${totalLen * (1 - currentProgress)}`;
|
|
2333
|
+
const piecePos = pieceCirclePos(currentProgress);
|
|
2334
|
+
piece.style.left = `${piecePos.x}px`;
|
|
2335
|
+
piece.style.top = `${piecePos.y}px`;
|
|
2336
|
+
}
|
|
2337
|
+
function bounceBack() {
|
|
2338
|
+
handle.style.transition = "left .35s ease, top .35s ease, border-color .15s";
|
|
2339
|
+
piece.style.transition = "left .35s ease, top .35s ease, background .15s, border-color .15s";
|
|
2340
|
+
fgPath.style.transition = "stroke-dashoffset .35s ease, stroke .15s";
|
|
2341
|
+
applyProgress(0);
|
|
2342
|
+
setTimeout(() => {
|
|
2343
|
+
handle.style.transition = "border-color .15s";
|
|
2344
|
+
piece.style.transition = "background .15s, border-color .15s";
|
|
2345
|
+
fgPath.style.transition = "stroke .15s";
|
|
2346
|
+
}, 360);
|
|
2347
|
+
}
|
|
2348
|
+
applyProgress(0);
|
|
2349
|
+
handle.addEventListener("pointerdown", (e) => {
|
|
2350
|
+
if (done || locked) return;
|
|
2351
|
+
dragging = true;
|
|
2352
|
+
startPointer = { x: e.clientX, y: e.clientY };
|
|
2353
|
+
startProgress = currentProgress;
|
|
2354
|
+
recorder.start();
|
|
2355
|
+
try {
|
|
2356
|
+
handle.setPointerCapture(e.pointerId);
|
|
2357
|
+
} catch {
|
|
2358
|
+
}
|
|
2359
|
+
e.preventDefault();
|
|
2360
|
+
});
|
|
2361
|
+
handle.addEventListener("pointermove", (e) => {
|
|
2362
|
+
if (!dragging) return;
|
|
2363
|
+
const rect = track.getBoundingClientRect();
|
|
2364
|
+
const sx = rect.width / TRACK_W;
|
|
2365
|
+
const sy = rect.height / TRACK_H;
|
|
2366
|
+
const svgX = (e.clientX - rect.left) / sx;
|
|
2367
|
+
const svgY = (e.clientY - rect.top) / sy;
|
|
2368
|
+
let bestLen = 0;
|
|
2369
|
+
let bestDist = Infinity;
|
|
2370
|
+
const steps = 100;
|
|
2371
|
+
for (let i = 0; i <= steps; i++) {
|
|
2372
|
+
const len = i / steps * totalLen;
|
|
2373
|
+
const p = fgPath.getPointAtLength(len);
|
|
2374
|
+
const d = (p.x - svgX) * (p.x - svgX) + (p.y - svgY) * (p.y - svgY);
|
|
2375
|
+
if (d < bestDist) {
|
|
2376
|
+
bestDist = d;
|
|
2377
|
+
bestLen = len;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
applyProgress(bestLen / totalLen);
|
|
2381
|
+
recorder.record(e.clientX, e.clientY);
|
|
2382
|
+
});
|
|
2383
|
+
const finish = async () => {
|
|
2384
|
+
if (!dragging) return;
|
|
2385
|
+
dragging = false;
|
|
2386
|
+
const trackPoints = recorder.stop();
|
|
2387
|
+
const piecePos = pieceCirclePos(currentProgress);
|
|
2388
|
+
const pieceLeftPct = piecePos.x / STAGE_W * 100;
|
|
2389
|
+
if (!verifyPosition(current, pieceLeftPct)) {
|
|
2390
|
+
bounceBack();
|
|
2391
|
+
msg.style.color = "var(--fc-danger)";
|
|
2392
|
+
msg.textContent = t.fail;
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
const analysis = analyzeTrack(trackPoints);
|
|
2396
|
+
if (analysis.isBot) {
|
|
2397
|
+
locked = true;
|
|
2398
|
+
msg.style.color = "var(--fc-danger)";
|
|
2399
|
+
msg.textContent = config.locale === "zh" ? `\u7591\u4F3C\u673A\u5668\u4EBA\u64CD\u4F5C\uFF08${analysis.reasons.join("\u3001")}\uFF09` : `${t.botFail} detected`;
|
|
2400
|
+
bounceBack();
|
|
2401
|
+
setTimeout(() => {
|
|
2402
|
+
if (!done) render2();
|
|
2403
|
+
}, 2e3);
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
done = true;
|
|
2407
|
+
root.classList.add("fc-puzzle-done");
|
|
2408
|
+
const result = {
|
|
2409
|
+
success: true,
|
|
2410
|
+
proof: await hashProof(proofInput8(current)),
|
|
2411
|
+
duration: Date.now() - startTime,
|
|
2412
|
+
metadata: { humanScore: analysis.humanScore, reasons: analysis.reasons }
|
|
2413
|
+
};
|
|
2414
|
+
msg.style.color = "var(--fc-success)";
|
|
2415
|
+
msg.textContent = t.success;
|
|
2416
|
+
config.onVerify?.(result);
|
|
2417
|
+
listeners.forEach((cb) => cb(result));
|
|
2418
|
+
};
|
|
2419
|
+
handle.addEventListener("pointerup", finish);
|
|
2420
|
+
handle.addEventListener("pointercancel", finish);
|
|
2421
|
+
}
|
|
2422
|
+
return {
|
|
2423
|
+
mount: () => render2(),
|
|
2424
|
+
reset: () => render2(),
|
|
2425
|
+
destroy: () => {
|
|
2426
|
+
container.innerHTML = "";
|
|
2427
|
+
listeners = [];
|
|
2428
|
+
},
|
|
2429
|
+
onResult: (cb) => listeners.push(cb)
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
var puzzlePlugin = {
|
|
2433
|
+
id: "puzzle",
|
|
2434
|
+
category: "interactive",
|
|
2435
|
+
create: createPuzzleInstance,
|
|
2436
|
+
describe: (locale) => ({
|
|
2437
|
+
name: locale === "zh" ? "\u62FC\u56FE\u7F3A\u53E3" : "Puzzle Gap",
|
|
2438
|
+
description: locale === "zh" ? "\u62D6\u52A8\u6ED1\u5757\u628A\u62FC\u56FE\u5757\u79FB\u5230\u7F3A\u53E3\u4F4D\u7F6E" : "Drag the slider to fit the puzzle piece into the gap.",
|
|
2439
|
+
tags: ["puzzle", "drag", "interactive"]
|
|
2440
|
+
})
|
|
2441
|
+
};
|
|
2442
|
+
defineCaptcha(puzzlePlugin);
|
|
2443
|
+
|
|
2444
|
+
// ../captchas/advanced-math/dist/index.js
|
|
2445
|
+
function randInt9(min, max) {
|
|
2446
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
2447
|
+
}
|
|
2448
|
+
function pick2(arr) {
|
|
2449
|
+
return arr[randInt9(0, arr.length - 1)];
|
|
2450
|
+
}
|
|
2451
|
+
var SUP_DIGITS = {
|
|
2452
|
+
"0": "\u2070",
|
|
2453
|
+
"1": "\xB9",
|
|
2454
|
+
"2": "\xB2",
|
|
2455
|
+
"3": "\xB3",
|
|
2456
|
+
"4": "\u2074",
|
|
2457
|
+
"5": "\u2075",
|
|
2458
|
+
"6": "\u2076",
|
|
2459
|
+
"7": "\u2077",
|
|
2460
|
+
"8": "\u2078",
|
|
2461
|
+
"9": "\u2079"
|
|
2462
|
+
};
|
|
2463
|
+
var SUP_TO_DIGIT = {
|
|
2464
|
+
"\u2070": "0",
|
|
2465
|
+
"\xB9": "1",
|
|
2466
|
+
"\xB2": "2",
|
|
2467
|
+
"\xB3": "3",
|
|
2468
|
+
"\u2074": "4",
|
|
2469
|
+
"\u2075": "5",
|
|
2470
|
+
"\u2076": "6",
|
|
2471
|
+
"\u2077": "7",
|
|
2472
|
+
"\u2078": "8",
|
|
2473
|
+
"\u2079": "9"
|
|
2474
|
+
};
|
|
2475
|
+
function sup(n) {
|
|
2476
|
+
return String(n).split("").map((d) => SUP_DIGITS[d] ?? d).join("");
|
|
2477
|
+
}
|
|
2478
|
+
function normalizeAnswer(input) {
|
|
2479
|
+
let r = input.trim();
|
|
2480
|
+
r = r.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹]+/g, (m) => "^" + m.split("").map((c) => SUP_TO_DIGIT[c] ?? c).join(""));
|
|
2481
|
+
r = r.replace(/\s+/g, "");
|
|
2482
|
+
r = r.replace(/\+[Cc]$/, "");
|
|
2483
|
+
r = r.replace(/(\d)([a-zA-Z])/g, "$1*$2");
|
|
2484
|
+
return r;
|
|
2485
|
+
}
|
|
2486
|
+
function verifyAnswer6(challenge, userInput) {
|
|
2487
|
+
const u = normalizeAnswer(userInput);
|
|
2488
|
+
return challenge.accept.some((a) => normalizeAnswer(a) === u);
|
|
2489
|
+
}
|
|
2490
|
+
function genDerivative() {
|
|
2491
|
+
const n = pick2([2, 3, 4, 5]);
|
|
2492
|
+
const question = `\u6C42 f(x) = x${sup(n)} \u7684\u5BFC\u6570`;
|
|
2493
|
+
const exp = n - 1;
|
|
2494
|
+
const answer = `${n}x^${exp}`;
|
|
2495
|
+
const accept = [
|
|
2496
|
+
`${n}x^${exp}`,
|
|
2497
|
+
`${n}x${sup(exp)}`,
|
|
2498
|
+
`${n}*x^${exp}`,
|
|
2499
|
+
`${n}*x${sup(exp)}`
|
|
2500
|
+
];
|
|
2501
|
+
return { type: "derivative", question, answer, accept };
|
|
2502
|
+
}
|
|
2503
|
+
function genIntegral() {
|
|
2504
|
+
const n = pick2([1, 2, 3]);
|
|
2505
|
+
const up = n + 1;
|
|
2506
|
+
const question = `\u6C42 \u222B x${sup(n)} dx`;
|
|
2507
|
+
const answer = `x^${up}/${up} + C`;
|
|
2508
|
+
const accept = [
|
|
2509
|
+
`x^${up}/${up} + C`,
|
|
2510
|
+
`x^${up}/${up}+C`,
|
|
2511
|
+
`x^${up}/${up}`,
|
|
2512
|
+
`x${sup(up)}/${up} + C`,
|
|
2513
|
+
`x${sup(up)}/${up}`,
|
|
2514
|
+
`x^${up}/(${up}) + C`,
|
|
2515
|
+
`x^${up}/(${up})`,
|
|
2516
|
+
`x${sup(up)}/(${up}) + C`,
|
|
2517
|
+
`x${sup(up)}/(${up})`
|
|
2518
|
+
];
|
|
2519
|
+
return { type: "integral", question, answer, accept };
|
|
2520
|
+
}
|
|
2521
|
+
function genLimit() {
|
|
2522
|
+
const a = pick2([1, 2, 3]);
|
|
2523
|
+
const question = `\u6C42 lim(x\u2192${a}) (x${sup(2)}-${a}${sup(2)})/(x-${a})`;
|
|
2524
|
+
const answer = `${2 * a}`;
|
|
2525
|
+
const accept = [
|
|
2526
|
+
`${2 * a}`,
|
|
2527
|
+
`2*${a}`
|
|
2528
|
+
];
|
|
2529
|
+
return { type: "limit", question, answer, accept };
|
|
2530
|
+
}
|
|
2531
|
+
function genDefinite() {
|
|
2532
|
+
const n = pick2([1, 2, 3]);
|
|
2533
|
+
const up = n + 1;
|
|
2534
|
+
const question = `\u8BA1\u7B97 \u222B\u2080\xB9 x${sup(n)} dx`;
|
|
2535
|
+
const answer = `1/${up}`;
|
|
2536
|
+
const accept = [
|
|
2537
|
+
`1/${up}`,
|
|
2538
|
+
`1/(${up})`
|
|
2539
|
+
];
|
|
2540
|
+
return { type: "definite", question, answer, accept };
|
|
2541
|
+
}
|
|
2542
|
+
function generateChallenge14() {
|
|
2543
|
+
const gen = pick2([genDerivative, genIntegral, genLimit, genDefinite]);
|
|
2544
|
+
return gen();
|
|
2545
|
+
}
|
|
2546
|
+
var STR14 = {
|
|
2547
|
+
zh: {
|
|
2548
|
+
title: "\u9AD8\u7B49\u6570\u5B66",
|
|
2549
|
+
placeholder: "\u8F93\u5165\u7B54\u6848\uFF08\u652F\u6301 ^ \u548C *\uFF09",
|
|
2550
|
+
submit: "\u9A8C\u8BC1",
|
|
2551
|
+
fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898",
|
|
2552
|
+
refresh: "\u5237\u65B0",
|
|
2553
|
+
hint: "\u63D0\u793A\uFF1Ax\xB2\u53EF\u5199\u4E3Ax^2\uFF0C\u53EF\u7701\u7565*"
|
|
2554
|
+
},
|
|
2555
|
+
en: {
|
|
2556
|
+
title: "Calculus",
|
|
2557
|
+
placeholder: "Enter answer (^ and * supported)",
|
|
2558
|
+
submit: "Verify",
|
|
2559
|
+
fail: "Wrong, try again",
|
|
2560
|
+
refresh: "Refresh",
|
|
2561
|
+
hint: "Tip: x\xB2 = x^2, * optional"
|
|
2562
|
+
}
|
|
2563
|
+
};
|
|
2564
|
+
function createAdvancedMathInstance(container, config) {
|
|
2565
|
+
const t = STR14[config.locale];
|
|
2566
|
+
const theme = config.theme ?? "light";
|
|
2567
|
+
let current;
|
|
2568
|
+
let listeners = [];
|
|
2569
|
+
let startTime = Date.now();
|
|
2570
|
+
function render2() {
|
|
2571
|
+
current = generateChallenge14();
|
|
2572
|
+
startTime = Date.now();
|
|
2573
|
+
container.innerHTML = `
|
|
2574
|
+
<style>
|
|
2575
|
+
.fc-amath{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
2576
|
+
.fc-amath[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
2577
|
+
.fc-amath[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
2578
|
+
.fc-amath{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
2579
|
+
.fc-amath-q{display:block;font-size:14px;font-weight:600;color:var(--fc-text);margin-bottom:8px}
|
|
2580
|
+
.fc-amath-hint{display:block;font-size:11px;color:var(--fc-text-soft);margin-bottom:12px}
|
|
2581
|
+
.fc-amath-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
2582
|
+
.fc-amath-input{flex:1;min-width:0;padding:8px 12px;font-size:14px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text);border-radius:8px;box-sizing:border-box;outline:none}
|
|
2583
|
+
.fc-amath-input:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
|
|
2584
|
+
.fc-amath-btn{padding:8px 16px;font-size:14px;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
|
|
2585
|
+
.fc-amath-btn:hover{opacity:.9}
|
|
2586
|
+
.fc-amath-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
2587
|
+
.fc-amath-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
2588
|
+
.fc-amath-msg{font-size:13px;min-height:18px;margin-top:10px;color:var(--fc-danger)}
|
|
2589
|
+
</style>
|
|
2590
|
+
<div class="fc-amath" data-theme="${theme}">
|
|
2591
|
+
<label class="fc-amath-q">${t.title}: ${current.question}</label>
|
|
2592
|
+
<span class="fc-amath-hint">${t.hint}</span>
|
|
2593
|
+
<div class="fc-amath-row">
|
|
2594
|
+
<input class="fc-amath-input" type="text" autocomplete="off" placeholder="${t.placeholder}" />
|
|
2595
|
+
<button class="fc-amath-btn">${t.submit}</button>
|
|
2596
|
+
<button class="fc-amath-refresh">${t.refresh}</button>
|
|
2597
|
+
</div>
|
|
2598
|
+
<div class="fc-amath-msg"></div>
|
|
2599
|
+
</div>
|
|
2600
|
+
`;
|
|
2601
|
+
const btn = container.querySelector(".fc-amath-btn");
|
|
2602
|
+
const input = container.querySelector(".fc-amath-input");
|
|
2603
|
+
const msg = container.querySelector(".fc-amath-msg");
|
|
2604
|
+
const refreshBtn = container.querySelector(".fc-amath-refresh");
|
|
2605
|
+
refreshBtn.addEventListener("click", render2);
|
|
2606
|
+
btn.addEventListener("click", async () => {
|
|
2607
|
+
const val = input.value;
|
|
2608
|
+
const success = verifyAnswer6(current, val);
|
|
2609
|
+
if (!success) {
|
|
2610
|
+
msg.textContent = t.fail;
|
|
2611
|
+
render2();
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
const result = {
|
|
2615
|
+
success: true,
|
|
2616
|
+
proof: await hashProof(`${current.question}=${current.answer}`),
|
|
2617
|
+
duration: Date.now() - startTime
|
|
2618
|
+
};
|
|
2619
|
+
config.onVerify?.(result);
|
|
2620
|
+
listeners.forEach((cb) => cb(result));
|
|
2621
|
+
});
|
|
2622
|
+
input.addEventListener("keydown", (e) => {
|
|
2623
|
+
if (e.key === "Enter") btn.click();
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
return {
|
|
2627
|
+
mount: () => render2(),
|
|
2628
|
+
reset: () => render2(),
|
|
2629
|
+
destroy: () => {
|
|
2630
|
+
container.innerHTML = "";
|
|
2631
|
+
listeners = [];
|
|
2632
|
+
},
|
|
2633
|
+
onResult: (cb) => listeners.push(cb)
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
defineCaptcha({
|
|
2637
|
+
id: "advanced-math",
|
|
2638
|
+
category: "recognize",
|
|
2639
|
+
create: createAdvancedMathInstance,
|
|
2640
|
+
describe: (locale) => ({
|
|
2641
|
+
name: locale === "zh" ? "\u9AD8\u7B49\u6570\u5B66" : "Calculus",
|
|
2642
|
+
description: locale === "zh" ? "\u6C42\u5BFC\u3001\u79EF\u5206\u3001\u6781\u9650\u7B49\u9AD8\u7B49\u6570\u5B66\u9898\uFF0C\u786C\u6838\u9632\u673A\u5668\u4EBA" : "Derivatives, integrals and limits \u2014 hardcore anti-bot.",
|
|
2643
|
+
tags: ["math", "calculus", "hard", "recognize"]
|
|
2644
|
+
})
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
// ../captchas/olympiad/dist/index.js
|
|
2648
|
+
function randInt10(min, max) {
|
|
2649
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
2650
|
+
}
|
|
2651
|
+
function pick3(arr) {
|
|
2652
|
+
return arr[randInt10(0, arr.length - 1)];
|
|
2653
|
+
}
|
|
2654
|
+
function genChickenRabbit() {
|
|
2655
|
+
const c = randInt10(2, 15);
|
|
2656
|
+
const r = randInt10(2, 10);
|
|
2657
|
+
const heads = c + r;
|
|
2658
|
+
const legs = 2 * c + 4 * r;
|
|
2659
|
+
return {
|
|
2660
|
+
type: "chicken-rabbit",
|
|
2661
|
+
question: `\u9E21\u5154\u540C\u7B3C\uFF0C\u5171${heads}\u4E2A\u5934\uFF0C${legs}\u53EA\u811A\uFF0C\u9E21\u6709\u51E0\u53EA\uFF1F`,
|
|
2662
|
+
answer: c
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
function genTreePlanting() {
|
|
2666
|
+
const gap = randInt10(2, 10);
|
|
2667
|
+
const minK = Math.ceil(20 / gap);
|
|
2668
|
+
const maxK = Math.floor(100 / gap);
|
|
2669
|
+
const k = randInt10(minK, maxK);
|
|
2670
|
+
const len = gap * k;
|
|
2671
|
+
return {
|
|
2672
|
+
type: "tree-planting",
|
|
2673
|
+
question: `\u4E00\u6761\u8DEF\u957F${len}\u7C73\uFF0C\u6BCF\u9694${gap}\u7C73\u79CD\u4E00\u68F5\u6811\uFF0C\u4E24\u7AEF\u90FD\u79CD\uFF0C\u5171\u51E0\u68F5\uFF1F`,
|
|
2674
|
+
answer: len / gap + 1
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
function genAge() {
|
|
2678
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
2679
|
+
const f = randInt10(35, 50);
|
|
2680
|
+
const s = randInt10(8, 15);
|
|
2681
|
+
if (f <= s) continue;
|
|
2682
|
+
const num = 5 * s - f;
|
|
2683
|
+
if (num <= 0 || num % 4 !== 0) continue;
|
|
2684
|
+
const x = num / 4;
|
|
2685
|
+
if (x <= 0 || x >= s) continue;
|
|
2686
|
+
return {
|
|
2687
|
+
type: "age",
|
|
2688
|
+
question: `\u7238\u7238${f}\u5C81\uFF0C\u513F\u5B50${s}\u5C81\uFF0C\u51E0\u5E74\u524D\u7238\u7238\u5E74\u9F84\u662F\u513F\u5B50\u76845\u500D\uFF1F`,
|
|
2689
|
+
answer: x
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
return {
|
|
2693
|
+
type: "age",
|
|
2694
|
+
question: "\u7238\u723842\u5C81\uFF0C\u513F\u5B5010\u5C81\uFF0C\u51E0\u5E74\u524D\u7238\u7238\u5E74\u9F84\u662F\u513F\u5B50\u76845\u500D\uFF1F",
|
|
2695
|
+
answer: 2
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
function genApple() {
|
|
2699
|
+
const p = randInt10(3, 12);
|
|
2700
|
+
const a = randInt10(3, 6);
|
|
2701
|
+
const b = a + 1;
|
|
2702
|
+
const m = randInt10(1, p - 1);
|
|
2703
|
+
const n = p - m;
|
|
2704
|
+
return {
|
|
2705
|
+
type: "apple",
|
|
2706
|
+
question: `\u5206\u82F9\u679C\uFF0C\u6BCF\u4EBA${a}\u4E2A\u591A${m}\u4E2A\uFF0C\u6BCF\u4EBA${b}\u4E2A\u5C11${n}\u4E2A\uFF0C\u51E0\u4EBA\uFF1F`,
|
|
2707
|
+
answer: p
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
function genTravel() {
|
|
2711
|
+
const v1 = randInt10(3, 12);
|
|
2712
|
+
const v2 = randInt10(3, 12);
|
|
2713
|
+
const t = randInt10(2, 8);
|
|
2714
|
+
const d = (v1 + v2) * t;
|
|
2715
|
+
return {
|
|
2716
|
+
type: "travel",
|
|
2717
|
+
question: `\u7532\u4E59\u76F8\u5411\u800C\u884C\uFF0C\u7532\u901F${v1}km/h\uFF0C\u4E59\u901F${v2}km/h\uFF0C\u76F8\u8DDD${d}km\uFF0C\u51E0\u5C0F\u65F6\u540E\u76F8\u9047\uFF1F`,
|
|
2718
|
+
answer: t
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
function generateChallenge15() {
|
|
2722
|
+
const gen = pick3([
|
|
2723
|
+
genChickenRabbit,
|
|
2724
|
+
genTreePlanting,
|
|
2725
|
+
genAge,
|
|
2726
|
+
genApple,
|
|
2727
|
+
genTravel
|
|
2728
|
+
]);
|
|
2729
|
+
return gen();
|
|
2730
|
+
}
|
|
2731
|
+
function verifyAnswer7(c, userAnswer) {
|
|
2732
|
+
return c.answer === userAnswer;
|
|
2733
|
+
}
|
|
2734
|
+
var STR15 = {
|
|
2735
|
+
zh: {
|
|
2736
|
+
title: "\u5C0F\u5B66\u5965\u6570",
|
|
2737
|
+
placeholder: "\u8F93\u5165\u6570\u5B57\u7B54\u6848",
|
|
2738
|
+
submit: "\u9A8C\u8BC1",
|
|
2739
|
+
fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898",
|
|
2740
|
+
refresh: "\u5237\u65B0"
|
|
2741
|
+
},
|
|
2742
|
+
en: {
|
|
2743
|
+
title: "Math Olympiad",
|
|
2744
|
+
placeholder: "Enter number",
|
|
2745
|
+
submit: "Verify",
|
|
2746
|
+
fail: "Wrong, try again",
|
|
2747
|
+
refresh: "Refresh"
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
function createOlympiadInstance(container, config) {
|
|
2751
|
+
const t = STR15[config.locale];
|
|
2752
|
+
const theme = config.theme ?? "light";
|
|
2753
|
+
let current;
|
|
2754
|
+
let listeners = [];
|
|
2755
|
+
let startTime = Date.now();
|
|
2756
|
+
function render2() {
|
|
2757
|
+
current = generateChallenge15();
|
|
2758
|
+
startTime = Date.now();
|
|
2759
|
+
container.innerHTML = `
|
|
2760
|
+
<style>
|
|
2761
|
+
.fc-olym{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
2762
|
+
.fc-olym[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
2763
|
+
.fc-olym[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
2764
|
+
.fc-olym{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
2765
|
+
.fc-olym-q{display:block;font-size:14px;font-weight:600;color:var(--fc-text);margin-bottom:12px;line-height:1.5}
|
|
2766
|
+
.fc-olym-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
2767
|
+
.fc-olym-input{flex:1;min-width:0;padding:8px 12px;font-size:14px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text);border-radius:8px;box-sizing:border-box;outline:none}
|
|
2768
|
+
.fc-olym-input:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
|
|
2769
|
+
.fc-olym-btn{padding:8px 16px;font-size:14px;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
|
|
2770
|
+
.fc-olym-btn:hover{opacity:.9}
|
|
2771
|
+
.fc-olym-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
2772
|
+
.fc-olym-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
2773
|
+
.fc-olym-msg{font-size:13px;min-height:18px;margin-top:10px;color:var(--fc-danger)}
|
|
2774
|
+
</style>
|
|
2775
|
+
<div class="fc-olym" data-theme="${theme}">
|
|
2776
|
+
<label class="fc-olym-q">${t.title}: ${current.question}</label>
|
|
2777
|
+
<div class="fc-olym-row">
|
|
2778
|
+
<input class="fc-olym-input" type="text" inputmode="numeric" autocomplete="off" placeholder="${t.placeholder}" />
|
|
2779
|
+
<button class="fc-olym-btn">${t.submit}</button>
|
|
2780
|
+
<button class="fc-olym-refresh">${t.refresh}</button>
|
|
2781
|
+
</div>
|
|
2782
|
+
<div class="fc-olym-msg"></div>
|
|
2783
|
+
</div>
|
|
2784
|
+
`;
|
|
2785
|
+
const btn = container.querySelector(".fc-olym-btn");
|
|
2786
|
+
const input = container.querySelector(".fc-olym-input");
|
|
2787
|
+
const msg = container.querySelector(".fc-olym-msg");
|
|
2788
|
+
const refreshBtn = container.querySelector(".fc-olym-refresh");
|
|
2789
|
+
refreshBtn.addEventListener("click", render2);
|
|
2790
|
+
btn.addEventListener("click", async () => {
|
|
2791
|
+
const val = Number(input.value);
|
|
2792
|
+
const success = Number.isFinite(val) && verifyAnswer7(current, val);
|
|
2793
|
+
if (!success) {
|
|
2794
|
+
msg.textContent = t.fail;
|
|
2795
|
+
render2();
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
const result = {
|
|
2799
|
+
success: true,
|
|
2800
|
+
proof: await hashProof(`${current.question}=${current.answer}`),
|
|
2801
|
+
duration: Date.now() - startTime
|
|
2802
|
+
};
|
|
2803
|
+
config.onVerify?.(result);
|
|
2804
|
+
listeners.forEach((cb) => cb(result));
|
|
2805
|
+
});
|
|
2806
|
+
input.addEventListener("keydown", (e) => {
|
|
2807
|
+
if (e.key === "Enter") btn.click();
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
return {
|
|
2811
|
+
mount: () => render2(),
|
|
2812
|
+
reset: () => render2(),
|
|
2813
|
+
destroy: () => {
|
|
2814
|
+
container.innerHTML = "";
|
|
2815
|
+
listeners = [];
|
|
2816
|
+
},
|
|
2817
|
+
onResult: (cb) => listeners.push(cb)
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
defineCaptcha({
|
|
2821
|
+
id: "olympiad",
|
|
2822
|
+
category: "recognize",
|
|
2823
|
+
create: createOlympiadInstance,
|
|
2824
|
+
describe: (locale) => ({
|
|
2825
|
+
name: locale === "zh" ? "\u5C0F\u5B66\u5965\u6570" : "Math Olympiad",
|
|
2826
|
+
description: locale === "zh" ? "\u9E21\u5154\u540C\u7B3C\u3001\u690D\u6811\u3001\u76C8\u4E8F\u7B49\u7ECF\u5178\u5965\u6570\u9898\uFF0C\u6321\u4F4F\u673A\u5668\u4E5F\u6321\u4F4F\u5C0F\u5B66\u751F" : "Classic olympiad word problems \u2014 chickens, trees, apples.",
|
|
2827
|
+
tags: ["math", "olympiad", "word-problem", "recognize"]
|
|
2828
|
+
})
|
|
2829
|
+
});
|
|
2830
|
+
|
|
2831
|
+
// ../captchas/equation-balance/dist/index.js
|
|
2832
|
+
var EQUATIONS = [
|
|
2833
|
+
{ reactants: ["H\u2082", "O\u2082"], products: ["H\u2082O"], coefficients: [2, 1, 2] },
|
|
2834
|
+
{ reactants: ["Fe", "O\u2082"], products: ["Fe\u2082O\u2083"], coefficients: [4, 3, 2] },
|
|
2835
|
+
{ reactants: ["CH\u2084", "O\u2082"], products: ["CO\u2082", "H\u2082O"], coefficients: [1, 2, 1, 2] },
|
|
2836
|
+
{ reactants: ["C\u2082H\u2086", "O\u2082"], products: ["CO\u2082", "H\u2082O"], coefficients: [2, 7, 4, 6] },
|
|
2837
|
+
{ reactants: ["Al", "HCl"], products: ["AlCl\u2083", "H\u2082"], coefficients: [2, 6, 2, 3] },
|
|
2838
|
+
{ reactants: ["N\u2082", "H\u2082"], products: ["NH\u2083"], coefficients: [1, 3, 2] }
|
|
2839
|
+
];
|
|
2840
|
+
function pick4(arr) {
|
|
2841
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
2842
|
+
}
|
|
2843
|
+
function generateChallenge16() {
|
|
2844
|
+
const eq = pick4(EQUATIONS);
|
|
2845
|
+
return {
|
|
2846
|
+
reactants: [...eq.reactants],
|
|
2847
|
+
products: [...eq.products],
|
|
2848
|
+
coefficients: [...eq.coefficients]
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
function verifyAnswer8(c, userCoefficients) {
|
|
2852
|
+
if (userCoefficients.length !== c.coefficients.length) return false;
|
|
2853
|
+
return c.coefficients.every((coef, i) => coef === userCoefficients[i]);
|
|
2854
|
+
}
|
|
2855
|
+
var STR16 = {
|
|
2856
|
+
zh: { title: "\u914D\u5E73\u5316\u5B66\u65B9\u7A0B\u5F0F", submit: "\u9A8C\u8BC1", fail: "\u7CFB\u6570\u4E0D\u5BF9\uFF0C\u518D\u8BD5\u8BD5", refresh: "\u6362\u4E00\u9898", hint: "\u7CFB\u6570\u4E3A1\u4E5F\u8981\u586B" },
|
|
2857
|
+
en: { title: "Balance the equation", submit: "Verify", fail: "Wrong coefficients", refresh: "Next", hint: "Enter 1 if needed" }
|
|
2858
|
+
};
|
|
2859
|
+
function createEquationBalanceInstance(container, config) {
|
|
2860
|
+
const t = STR16[config.locale];
|
|
2861
|
+
const theme = config.theme ?? "light";
|
|
2862
|
+
let current;
|
|
2863
|
+
let listeners = [];
|
|
2864
|
+
let startTime = Date.now();
|
|
2865
|
+
function render2() {
|
|
2866
|
+
current = generateChallenge16();
|
|
2867
|
+
startTime = Date.now();
|
|
2868
|
+
const items = [...current.reactants, ...current.products];
|
|
2869
|
+
const R = current.reactants.length;
|
|
2870
|
+
const eqHtml = items.map((formula, i) => {
|
|
2871
|
+
const sep = i === 0 ? "" : i === R ? "\u2192" : "+";
|
|
2872
|
+
const sepHtml = sep ? `<span class="fc-eqbal-sep">${sep}</span>` : "";
|
|
2873
|
+
return `${sepHtml}<span class="fc-eqbal-term"><input class="fc-eqbal-coef" type="text" inputmode="numeric" maxlength="2" aria-label="${formula}" /><span class="fc-eqbal-formula">${formula}</span></span>`;
|
|
2874
|
+
}).join("");
|
|
2875
|
+
container.innerHTML = `
|
|
2876
|
+
<style>
|
|
2877
|
+
.fc-eqbal{font-family:-apple-system,system-ui,sans-serif;max-width:420px;width:100%;padding:16px;box-sizing:border-box}
|
|
2878
|
+
.fc-eqbal[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
2879
|
+
.fc-eqbal[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
2880
|
+
.fc-eqbal{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
2881
|
+
.fc-eqbal-title{display:block;font-size:14px;font-weight:600;color:var(--fc-text);margin-bottom:4px}
|
|
2882
|
+
.fc-eqbal-hint{display:block;font-size:12px;color:var(--fc-text-soft);margin-bottom:12px}
|
|
2883
|
+
.fc-eqbal-eq{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:18px;color:var(--fc-text);margin-bottom:14px;justify-content:center}
|
|
2884
|
+
.fc-eqbal-term{display:inline-flex;align-items:center;gap:4px}
|
|
2885
|
+
.fc-eqbal-sep{color:var(--fc-text-soft);font-weight:600}
|
|
2886
|
+
.fc-eqbal-formula{font-family:'SF Mono',ui-monospace,monospace}
|
|
2887
|
+
.fc-eqbal-coef{width:40px;text-align:center;padding:4px 0;font-size:16px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text);border-radius:6px;outline:none;box-sizing:border-box}
|
|
2888
|
+
.fc-eqbal-coef:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
|
|
2889
|
+
.fc-eqbal-row{display:flex;gap:8px;align-items:center;justify-content:center}
|
|
2890
|
+
.fc-eqbal-btn{padding:8px 16px;font-size:14px;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
|
|
2891
|
+
.fc-eqbal-btn:hover{opacity:.9}
|
|
2892
|
+
.fc-eqbal-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
2893
|
+
.fc-eqbal-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
2894
|
+
.fc-eqbal-msg{font-size:13px;min-height:18px;margin-top:10px;text-align:center;color:var(--fc-danger)}
|
|
2895
|
+
</style>
|
|
2896
|
+
<div class="fc-eqbal" data-theme="${theme}">
|
|
2897
|
+
<label class="fc-eqbal-title">${t.title}</label>
|
|
2898
|
+
<span class="fc-eqbal-hint">${t.hint}</span>
|
|
2899
|
+
<div class="fc-eqbal-eq">${eqHtml}</div>
|
|
2900
|
+
<div class="fc-eqbal-row">
|
|
2901
|
+
<button class="fc-eqbal-btn">${t.submit}</button>
|
|
2902
|
+
<button class="fc-eqbal-refresh">${t.refresh}</button>
|
|
2903
|
+
</div>
|
|
2904
|
+
<div class="fc-eqbal-msg"></div>
|
|
2905
|
+
</div>
|
|
2906
|
+
`;
|
|
2907
|
+
const inputs = Array.from(container.querySelectorAll(".fc-eqbal-coef"));
|
|
2908
|
+
const btn = container.querySelector(".fc-eqbal-btn");
|
|
2909
|
+
const msg = container.querySelector(".fc-eqbal-msg");
|
|
2910
|
+
const refreshBtn = container.querySelector(".fc-eqbal-refresh");
|
|
2911
|
+
refreshBtn.addEventListener("click", render2);
|
|
2912
|
+
btn.addEventListener("click", async () => {
|
|
2913
|
+
const vals = inputs.map((i) => Number(i.value));
|
|
2914
|
+
if (!verifyAnswer8(current, vals)) {
|
|
2915
|
+
msg.textContent = t.fail;
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
const result = {
|
|
2919
|
+
success: true,
|
|
2920
|
+
proof: await hashProof(`equation-balance:${current.reactants.join("+")}>${current.products.join("+")}:${current.coefficients.join(",")}`),
|
|
2921
|
+
duration: Date.now() - startTime
|
|
2922
|
+
};
|
|
2923
|
+
msg.textContent = "";
|
|
2924
|
+
config.onVerify?.(result);
|
|
2925
|
+
listeners.forEach((cb) => cb(result));
|
|
2926
|
+
});
|
|
2927
|
+
inputs.forEach((input, idx) => {
|
|
2928
|
+
input.addEventListener("keydown", (e) => {
|
|
2929
|
+
if (e.key === "Enter") btn.click();
|
|
2930
|
+
});
|
|
2931
|
+
if (idx === 0) input.focus();
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
return {
|
|
2935
|
+
mount: () => render2(),
|
|
2936
|
+
reset: () => render2(),
|
|
2937
|
+
destroy: () => {
|
|
2938
|
+
container.innerHTML = "";
|
|
2939
|
+
listeners = [];
|
|
2940
|
+
},
|
|
2941
|
+
onResult: (cb) => listeners.push(cb)
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
var equationBalancePlugin = {
|
|
2945
|
+
id: "equation-balance",
|
|
2946
|
+
category: "recognize",
|
|
2947
|
+
create: createEquationBalanceInstance,
|
|
2948
|
+
describe: (locale) => ({
|
|
2949
|
+
name: locale === "zh" ? "\u914D\u5E73\u5316\u5B66\u65B9\u7A0B\u5F0F" : "Balance Equation",
|
|
2950
|
+
description: locale === "zh" ? "\u586B\u5165\u6B63\u786E\u7684\u5316\u5B66\u8BA1\u91CF\u6570\uFF0C\u914D\u5E73\u65B9\u7A0B\u5F0F" : "Enter the right stoichiometric coefficients.",
|
|
2951
|
+
tags: ["chemistry", "equation", "balance"]
|
|
2952
|
+
})
|
|
2953
|
+
};
|
|
2954
|
+
defineCaptcha(equationBalancePlugin);
|
|
2955
|
+
|
|
2956
|
+
// ../captchas/random-hunt/dist/index.js
|
|
2957
|
+
function isPrime(n) {
|
|
2958
|
+
if (n < 2) return false;
|
|
2959
|
+
for (let i = 2; i * i <= n; i++) {
|
|
2960
|
+
if (n % i === 0) return false;
|
|
2961
|
+
}
|
|
2962
|
+
return true;
|
|
2963
|
+
}
|
|
2964
|
+
var CONDITIONS = [
|
|
2965
|
+
{ key: "gt50", zh: "\u5927\u4E8E 50", en: "greater than 50", check: (n) => n > 50 },
|
|
2966
|
+
{ key: "lt20", zh: "\u5C0F\u4E8E 20", en: "less than 20", check: (n) => n < 20 },
|
|
2967
|
+
{ key: "even", zh: "\u5076\u6570", en: "even number", check: (n) => n % 2 === 0 },
|
|
2968
|
+
{ key: "odd", zh: "\u5947\u6570", en: "odd number", check: (n) => n % 2 === 1 },
|
|
2969
|
+
{ key: "mult3", zh: "3 \u7684\u500D\u6570", en: "multiple of 3", check: (n) => n % 3 === 0 },
|
|
2970
|
+
{ key: "gt50-even", zh: "\u5927\u4E8E 50 \u7684\u5076\u6570", en: "even and > 50", check: (n) => n > 50 && n % 2 === 0 },
|
|
2971
|
+
{ key: "lt30-odd", zh: "\u5C0F\u4E8E 30 \u7684\u5947\u6570", en: "odd and < 30", check: (n) => n < 30 && n % 2 === 1 },
|
|
2972
|
+
{ key: "prime", zh: "\u8D28\u6570", en: "prime number", check: (n) => isPrime(n) }
|
|
2973
|
+
];
|
|
2974
|
+
function pick5(arr) {
|
|
2975
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
2976
|
+
}
|
|
2977
|
+
function generateChallenge17() {
|
|
2978
|
+
const c = pick5(CONDITIONS);
|
|
2979
|
+
return { condition: c.key, check: c.check };
|
|
2980
|
+
}
|
|
2981
|
+
function conditionLabel(condition, locale) {
|
|
2982
|
+
const def = CONDITIONS.find((c) => c.key === condition);
|
|
2983
|
+
if (!def) return condition;
|
|
2984
|
+
return locale === "zh" ? def.zh : def.en;
|
|
2985
|
+
}
|
|
2986
|
+
var STR17 = {
|
|
2987
|
+
zh: { title: "\u968F\u673A\u6570\u730E\u624B", hunt: "\u6355\u83B7", success: "\u6355\u83B7\u6210\u529F\uFF01", fail: "\u4E0D\u7B26\u5408\u6761\u4EF6\uFF0C\u7EE7\u7EED", refresh: "\u6362\u6761\u4EF6", tip: "\u5F53\u6570\u5B57\u6EE1\u8DB3\u6761\u4EF6\u65F6\u70B9\u51FB\u6355\u83B7" },
|
|
2988
|
+
en: { title: "Random Hunter", hunt: "Catch!", success: "Caught it!", fail: "Not matching, keep going", refresh: "New rule", tip: "Click when the number matches" }
|
|
2989
|
+
};
|
|
2990
|
+
var TICK_MS = 120;
|
|
2991
|
+
var MIN = 0;
|
|
2992
|
+
var MAX = 99;
|
|
2993
|
+
function randInt11(min, max) {
|
|
2994
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
2995
|
+
}
|
|
2996
|
+
function createRandomHuntInstance(container, config) {
|
|
2997
|
+
const t = STR17[config.locale];
|
|
2998
|
+
const locale = config.locale;
|
|
2999
|
+
const theme = config.theme ?? "light";
|
|
3000
|
+
let current;
|
|
3001
|
+
let listeners = [];
|
|
3002
|
+
let startTime = Date.now();
|
|
3003
|
+
let timer = null;
|
|
3004
|
+
let number = 0;
|
|
3005
|
+
let succeeded = false;
|
|
3006
|
+
function clearTimer() {
|
|
3007
|
+
if (timer) {
|
|
3008
|
+
clearInterval(timer);
|
|
3009
|
+
timer = null;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
function render2() {
|
|
3013
|
+
current = generateChallenge17();
|
|
3014
|
+
startTime = Date.now();
|
|
3015
|
+
succeeded = false;
|
|
3016
|
+
clearTimer();
|
|
3017
|
+
number = randInt11(MIN, MAX);
|
|
3018
|
+
container.innerHTML = `
|
|
3019
|
+
<style>
|
|
3020
|
+
.fc-hunt{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
3021
|
+
.fc-hunt[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
|
|
3022
|
+
.fc-hunt[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
|
|
3023
|
+
.fc-hunt{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
3024
|
+
.fc-hunt-title{font-size:15px;font-weight:600;color:var(--fc-text);text-align:center;margin-bottom:4px}
|
|
3025
|
+
.fc-hunt-cond{font-size:13px;color:var(--fc-accent);text-align:center;margin-bottom:4px;font-weight:600}
|
|
3026
|
+
.fc-hunt-tip{font-size:12px;color:var(--fc-text-soft);text-align:center;margin-bottom:12px}
|
|
3027
|
+
.fc-hunt-num{font-family:'SF Mono',ui-monospace,monospace;font-size:48px;text-align:center;color:var(--fc-text);min-height:56px;line-height:56px;transition:opacity .08s ease-out,color .15s ease-out;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:10px;margin-bottom:14px}
|
|
3028
|
+
.fc-hunt-num-success{color:var(--fc-success);border-color:var(--fc-success)}
|
|
3029
|
+
.fc-hunt-row{display:flex;gap:8px;align-items:center;justify-content:center}
|
|
3030
|
+
.fc-hunt-btn{flex:1;padding:12px 16px;font-size:16px;font-weight:600;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
|
|
3031
|
+
.fc-hunt-btn:hover{opacity:.9}
|
|
3032
|
+
.fc-hunt-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
3033
|
+
.fc-hunt-refresh{padding:8px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
|
|
3034
|
+
.fc-hunt-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
3035
|
+
.fc-hunt-msg{font-size:13px;min-height:18px;margin-top:10px;text-align:center;color:var(--fc-danger)}
|
|
3036
|
+
</style>
|
|
3037
|
+
<div class="fc-hunt" data-theme="${theme}">
|
|
3038
|
+
<div class="fc-hunt-title">${t.title}</div>
|
|
3039
|
+
<div class="fc-hunt-cond">${t.hunt}\uFF1A${conditionLabel(current.condition, locale)}</div>
|
|
3040
|
+
<div class="fc-hunt-tip">${t.tip}</div>
|
|
3041
|
+
<div class="fc-hunt-num">${number}</div>
|
|
3042
|
+
<div class="fc-hunt-row">
|
|
3043
|
+
<button class="fc-hunt-btn">${t.hunt}</button>
|
|
3044
|
+
<button class="fc-hunt-refresh">${t.refresh}</button>
|
|
3045
|
+
</div>
|
|
3046
|
+
<div class="fc-hunt-msg"></div>
|
|
3047
|
+
</div>
|
|
3048
|
+
`;
|
|
3049
|
+
const numEl = container.querySelector(".fc-hunt-num");
|
|
3050
|
+
const btn = container.querySelector(".fc-hunt-btn");
|
|
3051
|
+
const msg = container.querySelector(".fc-hunt-msg");
|
|
3052
|
+
const refreshBtn = container.querySelector(".fc-hunt-refresh");
|
|
3053
|
+
refreshBtn.addEventListener("click", render2);
|
|
3054
|
+
timer = setInterval(() => {
|
|
3055
|
+
if (succeeded) return;
|
|
3056
|
+
number = randInt11(MIN, MAX);
|
|
3057
|
+
if (numEl) {
|
|
3058
|
+
numEl.textContent = String(number);
|
|
3059
|
+
numEl.style.opacity = "0.55";
|
|
3060
|
+
requestAnimationFrame(() => {
|
|
3061
|
+
numEl.style.opacity = "1";
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
}, TICK_MS);
|
|
3065
|
+
btn.addEventListener("click", async () => {
|
|
3066
|
+
if (succeeded) return;
|
|
3067
|
+
const ok = current.check(number);
|
|
3068
|
+
if (!ok) {
|
|
3069
|
+
msg.textContent = t.fail;
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
3072
|
+
succeeded = true;
|
|
3073
|
+
clearTimer();
|
|
3074
|
+
if (numEl) numEl.classList.add("fc-hunt-num-success");
|
|
3075
|
+
btn.disabled = true;
|
|
3076
|
+
const duration = Date.now() - startTime;
|
|
3077
|
+
const result = {
|
|
3078
|
+
success: true,
|
|
3079
|
+
proof: await hashProof(`random-hunt:${current.condition}:${duration}`),
|
|
3080
|
+
duration,
|
|
3081
|
+
metadata: { condition: current.condition, number }
|
|
3082
|
+
};
|
|
3083
|
+
msg.textContent = t.success;
|
|
3084
|
+
msg.style.color = "var(--fc-success)";
|
|
3085
|
+
config.onVerify?.(result);
|
|
3086
|
+
listeners.forEach((cb) => cb(result));
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
return {
|
|
3090
|
+
mount: () => render2(),
|
|
3091
|
+
reset: () => render2(),
|
|
3092
|
+
destroy: () => {
|
|
3093
|
+
clearTimer();
|
|
3094
|
+
container.innerHTML = "";
|
|
3095
|
+
listeners = [];
|
|
3096
|
+
},
|
|
3097
|
+
onResult: (cb) => listeners.push(cb)
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
var randomHuntPlugin = {
|
|
3101
|
+
id: "random-hunt",
|
|
3102
|
+
category: "game",
|
|
3103
|
+
create: createRandomHuntInstance,
|
|
3104
|
+
describe: (locale) => ({
|
|
3105
|
+
name: locale === "zh" ? "\u968F\u673A\u6570\u730E\u624B" : "Random Hunter",
|
|
3106
|
+
description: locale === "zh" ? "\u6570\u5B57\u98DE\u901F\u53D8\u5316\uFF0C\u5728\u6EE1\u8DB3\u6761\u4EF6\u7684\u77AC\u95F4\u6309\u4E0B\u6355\u83B7" : "Numbers flash fast \u2014 catch the moment it matches the rule.",
|
|
3107
|
+
tags: ["game", "reaction", "timed"]
|
|
3108
|
+
})
|
|
3109
|
+
};
|
|
3110
|
+
defineCaptcha(randomHuntPlugin);
|
|
3111
|
+
|
|
1333
3112
|
// src/index.ts
|
|
1334
3113
|
var instances = /* @__PURE__ */ new Map();
|
|
1335
3114
|
function render(root = document) {
|