@funnycaptcha/embed 0.3.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 +1301 -120
- package/dist/embed.global.js.map +1 -1
- package/dist/embed.js +4 -0
- package/dist/embed.js.map +1 -1
- package/package.json +19 -15
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) {
|
|
@@ -190,47 +310,95 @@ var FunnyCaptcha = (() => {
|
|
|
190
310
|
return c.code.toLowerCase() === answer.trim().toLowerCase();
|
|
191
311
|
}
|
|
192
312
|
var STR2 = {
|
|
193
|
-
zh: { placeholder: "\
|
|
194
|
-
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" }
|
|
195
315
|
};
|
|
196
|
-
|
|
316
|
+
var PERIOD = 180;
|
|
317
|
+
function drawFrame(canvas, code, frame, surface, text, border, accentSoft) {
|
|
197
318
|
const ctx = canvas.getContext("2d");
|
|
198
319
|
if (!ctx) return;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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);
|
|
205
344
|
}
|
|
206
345
|
const chars = code.split("");
|
|
207
|
-
const step =
|
|
346
|
+
const step = W / (chars.length + 1);
|
|
347
|
+
const focusIndex = phase === "strong" ? frame % chars.length : -1;
|
|
208
348
|
chars.forEach((ch, i) => {
|
|
209
349
|
ctx.save();
|
|
210
|
-
const
|
|
211
|
-
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;
|
|
212
355
|
ctx.translate(x, y);
|
|
213
|
-
ctx.rotate((Math.random() - 0.5) * 0.
|
|
214
|
-
|
|
215
|
-
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`;
|
|
216
359
|
ctx.textAlign = "center";
|
|
217
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;
|
|
218
382
|
ctx.fillText(ch, 0, 0);
|
|
219
383
|
ctx.restore();
|
|
220
384
|
});
|
|
221
|
-
|
|
385
|
+
const lineCount = Math.floor(2 + intensity * 6);
|
|
386
|
+
for (let i = 0; i < lineCount; i++) {
|
|
387
|
+
ctx.save();
|
|
222
388
|
ctx.strokeStyle = border;
|
|
223
389
|
ctx.lineWidth = 1 + Math.random() * 1.5;
|
|
390
|
+
ctx.globalAlpha = 0.4 + intensity * 0.4;
|
|
224
391
|
ctx.beginPath();
|
|
225
|
-
const x1 = Math.random() *
|
|
226
|
-
const y1 = Math.random() *
|
|
227
|
-
const x2 = Math.random() *
|
|
228
|
-
const y2 = Math.random() *
|
|
229
|
-
const cpx = Math.random() *
|
|
230
|
-
const cpy = Math.random() *
|
|
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;
|
|
231
398
|
ctx.moveTo(x1, y1);
|
|
232
399
|
ctx.quadraticCurveTo(cpx, cpy, x2, y2);
|
|
233
400
|
ctx.stroke();
|
|
401
|
+
ctx.restore();
|
|
234
402
|
}
|
|
235
403
|
}
|
|
236
404
|
function readVar(root, name, fallback) {
|
|
@@ -243,16 +411,26 @@ var FunnyCaptcha = (() => {
|
|
|
243
411
|
let current;
|
|
244
412
|
let listeners = [];
|
|
245
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;
|
|
246
419
|
function render2() {
|
|
247
420
|
current = generateChallenge2();
|
|
248
421
|
startTime = Date.now();
|
|
422
|
+
cancelAnimationFrame(rafId);
|
|
423
|
+
frame = 0;
|
|
424
|
+
paused = false;
|
|
425
|
+
cssCache = null;
|
|
426
|
+
cssCacheFrame = -100;
|
|
249
427
|
container.innerHTML = `
|
|
250
428
|
<style>
|
|
251
429
|
.fc-text{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
252
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}
|
|
253
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}
|
|
254
432
|
.fc-text{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
255
|
-
.fc-text-canvas{display:block;width:100%;max-width:
|
|
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)}
|
|
256
434
|
.fc-text-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
257
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}
|
|
258
436
|
.fc-text-input:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
|
|
@@ -260,25 +438,59 @@ var FunnyCaptcha = (() => {
|
|
|
260
438
|
.fc-text-btn:hover{opacity:.9}
|
|
261
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}
|
|
262
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)}
|
|
263
443
|
.fc-text-msg{font-size:13px;min-height:18px;margin-top:10px;color:var(--fc-danger)}
|
|
264
444
|
</style>
|
|
265
445
|
<div class="fc-text" data-theme="${theme}">
|
|
266
|
-
<canvas class="fc-text-canvas" width="
|
|
446
|
+
<canvas class="fc-text-canvas" width="280" height="60"></canvas>
|
|
267
447
|
<div class="fc-text-row">
|
|
268
448
|
<input class="fc-text-input" placeholder="${t.placeholder}" />
|
|
269
449
|
<button class="fc-text-btn">${t.submit}</button>
|
|
270
450
|
<button class="fc-text-refresh">${t.refresh}</button>
|
|
451
|
+
<button class="fc-text-pause">${t.pause}</button>
|
|
271
452
|
</div>
|
|
272
453
|
<div class="fc-text-msg"></div>
|
|
273
454
|
</div>
|
|
274
455
|
`;
|
|
275
456
|
const root = container.querySelector(".fc-text");
|
|
276
457
|
const canvas = container.querySelector(".fc-text-canvas");
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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);
|
|
281
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
|
+
});
|
|
282
494
|
const btn = container.querySelector(".fc-text-btn");
|
|
283
495
|
const input = container.querySelector(".fc-text-input");
|
|
284
496
|
const msg = container.querySelector(".fc-text-msg");
|
|
@@ -305,6 +517,7 @@ var FunnyCaptcha = (() => {
|
|
|
305
517
|
mount: () => render2(),
|
|
306
518
|
reset: () => render2(),
|
|
307
519
|
destroy: () => {
|
|
520
|
+
cancelAnimationFrame(rafId);
|
|
308
521
|
container.innerHTML = "";
|
|
309
522
|
listeners = [];
|
|
310
523
|
},
|
|
@@ -332,20 +545,56 @@ var FunnyCaptcha = (() => {
|
|
|
332
545
|
return `${c.token}:completed`;
|
|
333
546
|
}
|
|
334
547
|
var STR3 = {
|
|
335
|
-
zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5230\
|
|
336
|
-
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" }
|
|
337
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
|
+
}
|
|
338
581
|
function createSliderInstance(container, config) {
|
|
339
582
|
const t = STR3[config.locale];
|
|
340
583
|
const theme = config.theme ?? "light";
|
|
584
|
+
const recorder = new TrackRecorder();
|
|
341
585
|
let current;
|
|
342
586
|
let listeners = [];
|
|
343
587
|
let startTime = Date.now();
|
|
344
588
|
let done = false;
|
|
589
|
+
let locked = false;
|
|
345
590
|
function render2() {
|
|
346
591
|
current = generateChallenge3();
|
|
347
592
|
startTime = Date.now();
|
|
348
593
|
done = false;
|
|
594
|
+
locked = false;
|
|
595
|
+
recorder.clear();
|
|
596
|
+
const pts = spiralPoints();
|
|
597
|
+
const pathD = spiralPath(pts);
|
|
349
598
|
container.innerHTML = `
|
|
350
599
|
<style>
|
|
351
600
|
.fc-slider{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
@@ -353,24 +602,31 @@ var FunnyCaptcha = (() => {
|
|
|
353
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}
|
|
354
603
|
.fc-slider{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
355
604
|
.fc-slider-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
|
|
356
|
-
.fc-slider-
|
|
357
|
-
.fc-slider-
|
|
358
|
-
.fc-slider-
|
|
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}
|
|
359
611
|
.fc-slider-handle:active{cursor:grabbing}
|
|
360
|
-
.fc-slider-handle::after{content:'
|
|
361
|
-
.fc-slider-tip{position:absolute;left:50%;top:
|
|
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}
|
|
362
614
|
.fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
|
|
363
615
|
.fc-slider-row{display:flex;justify-content:center;margin-top:10px}
|
|
364
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}
|
|
365
617
|
.fc-slider-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
366
618
|
.fc-slider-done .fc-slider-handle{border-color:var(--fc-success)}
|
|
367
|
-
.fc-slider-done .fc-slider-handle::after{
|
|
368
|
-
.fc-slider-done .fc-slider-
|
|
619
|
+
.fc-slider-done .fc-slider-handle::after{background:var(--fc-success)}
|
|
620
|
+
.fc-slider-done .fc-slider-path-fg{stroke:var(--fc-success)}
|
|
369
621
|
</style>
|
|
370
622
|
<div class="fc-slider" data-theme="${theme}">
|
|
371
623
|
<div class="fc-slider-title">${t.title}</div>
|
|
372
|
-
<div class="fc-slider-
|
|
373
|
-
<
|
|
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>
|
|
374
630
|
<div class="fc-slider-handle" role="slider" tabindex="0"></div>
|
|
375
631
|
<span class="fc-slider-tip">${t.tip}</span>
|
|
376
632
|
</div>
|
|
@@ -381,27 +637,44 @@ var FunnyCaptcha = (() => {
|
|
|
381
637
|
</div>
|
|
382
638
|
`;
|
|
383
639
|
const root = container.querySelector(".fc-slider");
|
|
384
|
-
const
|
|
640
|
+
const stage = container.querySelector(".fc-slider-stage");
|
|
385
641
|
const handle = container.querySelector(".fc-slider-handle");
|
|
386
|
-
const progress = container.querySelector(".fc-slider-progress");
|
|
387
642
|
const tip = container.querySelector(".fc-slider-tip");
|
|
388
643
|
const msg = container.querySelector(".fc-slider-msg");
|
|
389
644
|
const refreshBtn = container.querySelector(".fc-slider-refresh");
|
|
645
|
+
const fgPath = container.querySelector(".fc-slider-path-fg");
|
|
390
646
|
refreshBtn.addEventListener("click", render2);
|
|
647
|
+
const totalLen = fgPath.getTotalLength();
|
|
648
|
+
fgPath.style.strokeDasharray = `${totalLen}`;
|
|
649
|
+
fgPath.style.strokeDashoffset = `${totalLen}`;
|
|
391
650
|
let dragging = false;
|
|
392
|
-
let
|
|
393
|
-
let
|
|
394
|
-
let
|
|
395
|
-
function
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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";
|
|
399
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);
|
|
400
672
|
handle.addEventListener("pointerdown", (e) => {
|
|
401
|
-
if (done) return;
|
|
673
|
+
if (done || locked) return;
|
|
402
674
|
dragging = true;
|
|
403
|
-
|
|
404
|
-
|
|
675
|
+
startPointer = { x: e.clientX, y: e.clientY };
|
|
676
|
+
startProgress = currentProgress;
|
|
677
|
+
recorder.start();
|
|
405
678
|
try {
|
|
406
679
|
handle.setPointerCapture(e.pointerId);
|
|
407
680
|
} catch {
|
|
@@ -410,40 +683,55 @@ var FunnyCaptcha = (() => {
|
|
|
410
683
|
});
|
|
411
684
|
handle.addEventListener("pointermove", (e) => {
|
|
412
685
|
if (!dragging) return;
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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);
|
|
418
700
|
});
|
|
419
701
|
const finish = async () => {
|
|
420
702
|
if (!dragging) return;
|
|
421
703
|
dragging = false;
|
|
422
|
-
const
|
|
423
|
-
if (
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
root.classList.add("fc-slider-done");
|
|
427
|
-
const result = {
|
|
428
|
-
success: true,
|
|
429
|
-
proof: await hashProof(proofInput(current)),
|
|
430
|
-
duration: Date.now() - startTime
|
|
431
|
-
};
|
|
432
|
-
msg.textContent = t.success;
|
|
433
|
-
config.onVerify?.(result);
|
|
434
|
-
listeners.forEach((cb) => cb(result));
|
|
435
|
-
} else {
|
|
436
|
-
currentOffset = 0;
|
|
437
|
-
handle.style.transition = "left .3s";
|
|
438
|
-
progress.style.transition = "width .3s";
|
|
439
|
-
applyPos(0);
|
|
704
|
+
const trackPoints = recorder.stop();
|
|
705
|
+
if (currentProgress < 0.85) {
|
|
706
|
+
bounceBack();
|
|
707
|
+
msg.style.color = "var(--fc-danger)";
|
|
440
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();
|
|
441
717
|
setTimeout(() => {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}, 320);
|
|
718
|
+
if (!done) render2();
|
|
719
|
+
}, 2e3);
|
|
720
|
+
return;
|
|
446
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));
|
|
447
735
|
};
|
|
448
736
|
handle.addEventListener("pointerup", finish);
|
|
449
737
|
handle.addEventListener("pointercancel", finish);
|
|
@@ -489,8 +777,8 @@ var FunnyCaptcha = (() => {
|
|
|
489
777
|
return c.order.join(",");
|
|
490
778
|
}
|
|
491
779
|
var STR4 = {
|
|
492
|
-
zh: { title: "\u6309 1
|
|
493
|
-
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" }
|
|
494
782
|
};
|
|
495
783
|
var AREA_W = 280;
|
|
496
784
|
var AREA_H = 180;
|
|
@@ -528,16 +816,18 @@ var FunnyCaptcha = (() => {
|
|
|
528
816
|
let listeners = [];
|
|
529
817
|
let startTime = Date.now();
|
|
530
818
|
let clicked = [];
|
|
819
|
+
let doneSet = /* @__PURE__ */ new Set();
|
|
531
820
|
let finished = false;
|
|
532
821
|
function render2() {
|
|
533
822
|
current = generateChallenge4();
|
|
534
823
|
clicked = [];
|
|
824
|
+
doneSet = /* @__PURE__ */ new Set();
|
|
535
825
|
finished = false;
|
|
536
826
|
startTime = Date.now();
|
|
537
827
|
const slots = generateSlots(current.targets.length);
|
|
538
828
|
const boxes = current.targets.map((num, i) => {
|
|
539
829
|
const slot = slots[i];
|
|
540
|
-
return `<div class="fc-click-box" data-num="${num}" style="left:${Math.round(slot.x)}px;top:${Math.round(slot.y)}px;">${num}</div>`;
|
|
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>`;
|
|
541
831
|
}).join("");
|
|
542
832
|
container.innerHTML = `
|
|
543
833
|
<style>
|
|
@@ -547,7 +837,7 @@ var FunnyCaptcha = (() => {
|
|
|
547
837
|
.fc-click{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
548
838
|
.fc-click-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
|
|
549
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}
|
|
550
|
-
.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:transform .15s,background .15s,color .15s,border-color .15s}
|
|
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}
|
|
551
841
|
.fc-click-box:hover{transform:scale(1.05)}
|
|
552
842
|
.fc-click-box-active{background:var(--fc-accent);color:var(--fc-bg)}
|
|
553
843
|
.fc-click-box-done{background:var(--fc-success);border-color:var(--fc-success);color:#fff;cursor:default}
|
|
@@ -570,6 +860,17 @@ var FunnyCaptcha = (() => {
|
|
|
570
860
|
const msg = container.querySelector(".fc-click-msg");
|
|
571
861
|
const refreshBtn = container.querySelector(".fc-click-refresh");
|
|
572
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
|
+
}
|
|
573
874
|
area.querySelectorAll(".fc-click-box").forEach((el) => {
|
|
574
875
|
el.addEventListener("click", async () => {
|
|
575
876
|
if (finished) return;
|
|
@@ -578,7 +879,9 @@ var FunnyCaptcha = (() => {
|
|
|
578
879
|
const num = Number(box.dataset.num);
|
|
579
880
|
const expected = current.order[clicked.length];
|
|
580
881
|
if (num === expected) {
|
|
882
|
+
const idx = Number(box.dataset.idx);
|
|
581
883
|
clicked.push(num);
|
|
884
|
+
doneSet.add(idx);
|
|
582
885
|
box.classList.add("fc-click-box-done");
|
|
583
886
|
if (clicked.length === current.order.length) {
|
|
584
887
|
finished = true;
|
|
@@ -590,6 +893,8 @@ var FunnyCaptcha = (() => {
|
|
|
590
893
|
msg.textContent = t.success;
|
|
591
894
|
config.onVerify?.(result);
|
|
592
895
|
listeners.forEach((cb) => cb(result));
|
|
896
|
+
} else {
|
|
897
|
+
relocateBoxes(idx);
|
|
593
898
|
}
|
|
594
899
|
} else {
|
|
595
900
|
finished = true;
|
|
@@ -977,8 +1282,8 @@ var FunnyCaptcha = (() => {
|
|
|
977
1282
|
el.addEventListener("click", async () => {
|
|
978
1283
|
if (finished) return;
|
|
979
1284
|
const btn = el;
|
|
980
|
-
const
|
|
981
|
-
if (verifyAnswer3(current,
|
|
1285
|
+
const pick6 = btn.dataset.emoji;
|
|
1286
|
+
if (verifyAnswer3(current, pick6)) {
|
|
982
1287
|
finished = true;
|
|
983
1288
|
btn.classList.add("fc-emoji-match-opt-correct");
|
|
984
1289
|
const result = {
|
|
@@ -1109,8 +1414,8 @@ var FunnyCaptcha = (() => {
|
|
|
1109
1414
|
el.addEventListener("click", async () => {
|
|
1110
1415
|
if (finished) return;
|
|
1111
1416
|
const btn = el;
|
|
1112
|
-
const
|
|
1113
|
-
if (verifyAnswer4(current,
|
|
1417
|
+
const pick6 = btn.textContent ?? "";
|
|
1418
|
+
if (verifyAnswer4(current, pick6)) {
|
|
1114
1419
|
finished = true;
|
|
1115
1420
|
btn.classList.add("fc-meme-quiz-opt-correct");
|
|
1116
1421
|
const result = {
|
|
@@ -1585,25 +1890,56 @@ var FunnyCaptcha = (() => {
|
|
|
1585
1890
|
return `${c.chars.join("")}:completed`;
|
|
1586
1891
|
}
|
|
1587
1892
|
var STR11 = {
|
|
1588
|
-
zh: { title: "\u8BF7\u6309\u987A\u5E8F\u70B9\u51FB", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5", refresh: "\u5237\u65B0" },
|
|
1589
|
-
en: { title: "Click in order", success: "Verified", fail: "Wrong order, try again", refresh: "Refresh" }
|
|
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" }
|
|
1590
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
|
+
}
|
|
1591
1925
|
function createClickTextInstance(container, config) {
|
|
1592
1926
|
const t = STR11[config.locale];
|
|
1593
1927
|
let current;
|
|
1594
1928
|
let listeners = [];
|
|
1595
1929
|
let startTime = Date.now();
|
|
1596
1930
|
let clicked = [];
|
|
1931
|
+
let doneSet = /* @__PURE__ */ new Set();
|
|
1597
1932
|
let finished = false;
|
|
1598
1933
|
function render2() {
|
|
1599
1934
|
current = generateChallenge11();
|
|
1600
1935
|
clicked = [];
|
|
1936
|
+
doneSet = /* @__PURE__ */ new Set();
|
|
1601
1937
|
finished = false;
|
|
1602
1938
|
startTime = Date.now();
|
|
1603
1939
|
const prompt = current.chars.join("\u3001");
|
|
1604
1940
|
const chars = current.chars.map((ch, i) => {
|
|
1605
1941
|
const p = current.positions[i];
|
|
1606
|
-
return `<div class="fc-click-text-char" data-ch="${ch}" style="left:${p.x}px;top:${p.y}px;">${ch}</div>`;
|
|
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>`;
|
|
1607
1943
|
}).join("");
|
|
1608
1944
|
container.innerHTML = `
|
|
1609
1945
|
<style>
|
|
@@ -1613,7 +1949,7 @@ var FunnyCaptcha = (() => {
|
|
|
1613
1949
|
.fc-click-text-title{font-size:14px;color:var(--fc-text);margin-bottom:4px;text-align:center}
|
|
1614
1950
|
.fc-click-text-prompt{font-size:15px;color:var(--fc-accent);margin-bottom:12px;text-align:center;font-weight:600}
|
|
1615
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}
|
|
1616
|
-
.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:transform .15s,background .15s,color .15s,border-color .15s}
|
|
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}
|
|
1617
1953
|
.fc-click-text-char:hover{transform:scale(1.08);border-color:var(--fc-accent)}
|
|
1618
1954
|
.fc-click-text-char-done{background:var(--fc-success);border-color:var(--fc-success);color:#fff;cursor:default}
|
|
1619
1955
|
.fc-click-text-char-done:hover{transform:none}
|
|
@@ -1631,9 +1967,21 @@ var FunnyCaptcha = (() => {
|
|
|
1631
1967
|
<button class="fc-click-text-refresh">${t.refresh}</button>
|
|
1632
1968
|
</div>
|
|
1633
1969
|
`;
|
|
1970
|
+
const area = container.querySelector(".fc-click-text-area");
|
|
1634
1971
|
const msg = container.querySelector(".fc-click-text-msg");
|
|
1635
1972
|
const refreshBtn = container.querySelector(".fc-click-text-refresh");
|
|
1636
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
|
+
}
|
|
1637
1985
|
container.querySelectorAll(".fc-click-text-char").forEach((el) => {
|
|
1638
1986
|
el.addEventListener("click", async () => {
|
|
1639
1987
|
if (finished) return;
|
|
@@ -1642,7 +1990,9 @@ var FunnyCaptcha = (() => {
|
|
|
1642
1990
|
const ch = box.dataset.ch;
|
|
1643
1991
|
const expected = current.chars[clicked.length];
|
|
1644
1992
|
if (ch === expected) {
|
|
1993
|
+
const idx = Number(box.dataset.idx);
|
|
1645
1994
|
clicked.push(ch);
|
|
1995
|
+
doneSet.add(idx);
|
|
1646
1996
|
box.classList.add("fc-click-text-char-done");
|
|
1647
1997
|
if (clicked.length === current.chars.length) {
|
|
1648
1998
|
finished = true;
|
|
@@ -1654,6 +2004,8 @@ var FunnyCaptcha = (() => {
|
|
|
1654
2004
|
msg.textContent = t.success;
|
|
1655
2005
|
config.onVerify?.(result);
|
|
1656
2006
|
listeners.forEach((cb) => cb(result));
|
|
2007
|
+
} else {
|
|
2008
|
+
relocateChars(idx);
|
|
1657
2009
|
}
|
|
1658
2010
|
} else {
|
|
1659
2011
|
finished = true;
|
|
@@ -1781,8 +2133,8 @@ var FunnyCaptcha = (() => {
|
|
|
1781
2133
|
el.addEventListener("click", async () => {
|
|
1782
2134
|
if (finished) return;
|
|
1783
2135
|
const btn = el;
|
|
1784
|
-
const
|
|
1785
|
-
if (verifyAnswer5(current,
|
|
2136
|
+
const pick6 = btn.dataset.hex;
|
|
2137
|
+
if (verifyAnswer5(current, pick6)) {
|
|
1786
2138
|
finished = true;
|
|
1787
2139
|
btn.classList.add("fc-color-pick-opt-correct");
|
|
1788
2140
|
msg.classList.add("fc-color-pick-msg-success");
|
|
@@ -1826,12 +2178,12 @@ var FunnyCaptcha = (() => {
|
|
|
1826
2178
|
defineCaptcha(colorPickPlugin);
|
|
1827
2179
|
|
|
1828
2180
|
// ../captchas/puzzle/dist/index.js
|
|
1829
|
-
var TOLERANCE =
|
|
2181
|
+
var TOLERANCE = 8;
|
|
1830
2182
|
function randInt8(min, max) {
|
|
1831
2183
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
1832
2184
|
}
|
|
1833
2185
|
function generateChallenge13() {
|
|
1834
|
-
return { gapPosition: randInt8(
|
|
2186
|
+
return { gapPosition: randInt8(25, 58) };
|
|
1835
2187
|
}
|
|
1836
2188
|
function proofInput8(c) {
|
|
1837
2189
|
return `${c.gapPosition}:completed`;
|
|
@@ -1840,71 +2192,232 @@ var FunnyCaptcha = (() => {
|
|
|
1840
2192
|
return Math.abs(userPos - c.gapPosition) <= TOLERANCE;
|
|
1841
2193
|
}
|
|
1842
2194
|
var STR13 = {
|
|
1843
|
-
zh: { title: "\u62D6\u52A8\u6ED1\u5757\u628A\u62FC\u56FE\u5757\u79FB\u5230\u7F3A\u53E3", success: "\u9A8C\u8BC1\u6210\u529F", refresh: "\u5237\u65B0" },
|
|
1844
|
-
en: { title: "Drag the slider to fit the puzzle", success: "Verified", refresh: "Refresh" }
|
|
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" }
|
|
1845
2197
|
};
|
|
1846
2198
|
var STAGE_W = 300;
|
|
2199
|
+
var STAGE_H = 200;
|
|
1847
2200
|
var PIECE_W = 50;
|
|
1848
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
|
+
}
|
|
1849
2252
|
function createPuzzleInstance(container, config) {
|
|
1850
2253
|
const t = STR13[config.locale];
|
|
2254
|
+
const theme = config.theme ?? "light";
|
|
2255
|
+
const recorder = new TrackRecorder();
|
|
1851
2256
|
let current;
|
|
1852
2257
|
let listeners = [];
|
|
1853
2258
|
let startTime = Date.now();
|
|
1854
2259
|
let done = false;
|
|
2260
|
+
let locked = false;
|
|
1855
2261
|
function render2() {
|
|
1856
2262
|
current = generateChallenge13();
|
|
1857
2263
|
startTime = Date.now();
|
|
1858
2264
|
done = false;
|
|
2265
|
+
locked = false;
|
|
2266
|
+
recorder.clear();
|
|
1859
2267
|
const gapLeft = current.gapPosition / 100 * MAX_OFFSET;
|
|
2268
|
+
const pts = zigzagPoints();
|
|
2269
|
+
const pathD = zigzagPath(pts);
|
|
1860
2270
|
container.innerHTML = `
|
|
1861
2271
|
<style>
|
|
1862
2272
|
.fc-puzzle{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
1863
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}
|
|
1864
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)}
|
|
1865
2276
|
.fc-puzzle-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
|
|
1866
|
-
.fc-puzzle-stage{position:relative;width:${STAGE_W}px;height:
|
|
1867
|
-
.fc-puzzle-gap{position:absolute;top
|
|
1868
|
-
.fc-puzzle-piece{position:absolute;
|
|
1869
|
-
.fc-puzzle-
|
|
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}
|
|
1870
2287
|
.fc-puzzle-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
|
|
1871
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}
|
|
1872
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)}
|
|
1873
2294
|
</style>
|
|
1874
|
-
<div class="fc-puzzle" data-theme="${
|
|
2295
|
+
<div class="fc-puzzle" data-theme="${theme}">
|
|
1875
2296
|
<div class="fc-puzzle-title">${t.title}</div>
|
|
1876
2297
|
<div class="fc-puzzle-stage">
|
|
1877
2298
|
<div class="fc-puzzle-gap" style="left:${gapLeft}px"></div>
|
|
1878
|
-
<div class="fc-puzzle-piece" style="left:0"></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>
|
|
1879
2307
|
</div>
|
|
1880
|
-
<input class="fc-puzzle-range" type="range" min="0" max="100" step="1" value="0" />
|
|
1881
2308
|
<div class="fc-puzzle-msg"></div>
|
|
1882
2309
|
<button class="fc-puzzle-refresh">${t.refresh}</button>
|
|
1883
2310
|
</div>
|
|
1884
2311
|
`;
|
|
2312
|
+
const root = container.querySelector(".fc-puzzle");
|
|
1885
2313
|
const piece = container.querySelector(".fc-puzzle-piece");
|
|
1886
|
-
const
|
|
2314
|
+
const track = container.querySelector(".fc-puzzle-track");
|
|
2315
|
+
const handle = container.querySelector(".fc-puzzle-handle");
|
|
1887
2316
|
const msg = container.querySelector(".fc-puzzle-msg");
|
|
1888
2317
|
const refreshBtn = container.querySelector(".fc-puzzle-refresh");
|
|
2318
|
+
const fgPath = container.querySelector(".fc-puzzle-path-fg");
|
|
1889
2319
|
refreshBtn.addEventListener("click", () => render2());
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
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
|
+
}
|
|
1906
2379
|
}
|
|
2380
|
+
applyProgress(bestLen / totalLen);
|
|
2381
|
+
recorder.record(e.clientX, e.clientY);
|
|
1907
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);
|
|
1908
2421
|
}
|
|
1909
2422
|
return {
|
|
1910
2423
|
mount: () => render2(),
|
|
@@ -1928,6 +2441,674 @@ var FunnyCaptcha = (() => {
|
|
|
1928
2441
|
};
|
|
1929
2442
|
defineCaptcha(puzzlePlugin);
|
|
1930
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
|
+
|
|
1931
3112
|
// src/index.ts
|
|
1932
3113
|
var instances = /* @__PURE__ */ new Map();
|
|
1933
3114
|
function render(root = document) {
|