@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.
@@ -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
- <div class="fc-math">
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
- <input class="fc-math-input" type="text" inputmode="numeric" placeholder="${t.placeholder}" />
110
- <button class="fc-math-btn">${t.submit}</button>
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: "\u8F93\u5165\u56FE\u4E2D\u5B57\u7B26", submit: "\u9A8C\u8BC1", fail: "\u770B\u4E0D\u6E05\uFF1F\u6362\u4E00\u5F20", refresh: "\u5237\u65B0" },
173
- en: { placeholder: "Type the text", submit: "Verify", fail: "Wrong? Try another", refresh: "Refresh" }
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
- function drawDistorted(canvas, code) {
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
- ctx.clearRect(0, 0, canvas.width, canvas.height);
179
- ctx.fillStyle = "#f0f0f0";
180
- ctx.fillRect(0, 0, canvas.width, canvas.height);
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 = canvas.width / (chars.length + 1);
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 x = step * (i + 1);
186
- const y = canvas.height / 2 + (Math.random() - 0.5) * 14;
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.5);
189
- ctx.font = `${24 + Math.floor(Math.random() * 8)}px sans-serif`;
190
- ctx.fillStyle = `hsl(${Math.random() * 360}, 70%, 40%)`;
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
- for (let i = 0; i < 4; i++) {
197
- ctx.strokeStyle = `hsla(${Math.random() * 360}, 70%, 50%, 0.4)`;
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
- ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
200
- ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
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
- <div class="fc-text">
214
- <canvas class="fc-text-canvas" width="180" height="60"></canvas>
215
- <button class="fc-text-refresh">${t.refresh}</button>
216
- <input class="fc-text-input" placeholder="${t.placeholder}" />
217
- <button class="fc-text-btn">${t.submit}</button>
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
- drawDistorted(canvas, current.code);
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\u6700\u53F3\u7AEF", tip: "\u5411\u53F3\u62D6\u52A8", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u8BF7\u62D6\u5230\u5C3D\u5934" },
278
- en: { title: "Drag the slider to the right end", tip: "Drag right", success: "Verified", fail: "Drag to the end" }
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:300px;padding:16px;box-sizing:border-box}
293
- .fc-slider-title{font-size:14px;color:#333;margin-bottom:12px;text-align:center}
294
- .fc-slider-track{position:relative;height:40px;background:#e8e8e8;border-radius:20px}
295
- .fc-slider-progress{position:absolute;left:0;top:0;height:100%;width:0;background:#4a90d9;border-radius:20px;transition:width .05s}
296
- .fc-slider-handle{position:absolute;left:0;top:50%;transform:translateY(-50%);width:50px;height:50px;background:#fff;border:2px solid #4a90d9;border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.15);user-select:none;touch-action:none}
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:'\\2192';color:#4a90d9;font-size:18px;font-weight:bold}
299
- .fc-slider-tip{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:13px;color:#999;pointer-events:none;transition:opacity .2s}
300
- .fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:#2e7d32}
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-track">
305
- <div class="fc-slider-progress"></div>
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 track = container.querySelector(".fc-slider-track");
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 startX = 0;
319
- let startOffset = 0;
320
- let currentOffset = 0;
321
- function applyPos(px) {
322
- handle.style.left = `${px}px`;
323
- progress.style.width = `${px}px`;
324
- tip.style.opacity = px > 8 ? "0" : "1";
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
- startX = e.clientX;
330
- startOffset = currentOffset;
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 maxX = track.clientWidth - handle.offsetWidth;
340
- let px = startOffset + (e.clientX - startX);
341
- px = Math.max(0, Math.min(maxX, px));
342
- currentOffset = px;
343
- applyPos(px);
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 maxX = Math.max(1, track.clientWidth - handle.offsetWidth);
349
- if (currentOffset / maxX >= 0.95) {
350
- done = true;
351
- applyPos(maxX);
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
- handle.style.transition = "";
368
- progress.style.transition = "";
369
- if (!done) msg.textContent = "";
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 \u2192 2 \u2192 3 \u2192 4 \u987A\u5E8F\u70B9\u51FB", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5" },
418
- en: { title: "Click in order 1 \u2192 2 \u2192 3 \u2192 4", success: "Verified", fail: "Wrong order, try again" }
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 SLOTS = [
421
- { x: 20, y: 20 },
422
- { x: 160, y: 20 },
423
- { x: 20, y: 100 },
424
- { x: 160, y: 100 }
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 = SLOTS[i];
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:320px;padding:16px;box-sizing:border-box}
445
- .fc-click-title{font-size:14px;color:#333;margin-bottom:12px;text-align:center}
446
- .fc-click-area{position:relative;width:280px;height:180px;background:#f5f5f5;border-radius:8px;border:1px solid #e0e0e0;overflow:hidden}
447
- .fc-click-box{position:absolute;width:100px;height:60px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:bold;color:#4a90d9;background:#fff;border:2px solid #4a90d9;border-radius:8px;cursor:pointer;user-select:none;transition:transform .15s,background .15s,color .15s}
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:#4a90d9;color:#fff}
450
- .fc-click-box-done{background:#2e7d32;border-color:#2e7d32;color:#fff;cursor:default}
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-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:#2e7d32}
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:300px;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:12px}
546
- .fc-rotate-title{font-size:14px;color:#333;text-align:center}
547
- .fc-rotate-stage{position:relative;width:140px;height:140px;display:flex;align-items:center;justify-content:center;border:2px dashed #ccc;border-radius:50%}
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 #e74c3c}
550
- .fc-rotate-hint{font-size:12px;color:#999}
551
- .fc-rotate-range{width:240px;accent-color:#4a90d9}
552
- .fc-rotate-msg{font-size:13px;min-height:18px;text-align:center;color:#2e7d32}
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"><polygon points="50,12 90,85 10,85" fill="#4a90d9"/></svg>
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-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}
675
- .fc-spot-diff-hint{font-size:12px;color:#888;margin-bottom:12px;text-align:center}
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:#f5f5f5;padding:8px;border-radius:8px;border:1px solid #e0e0e0}
678
- .fc-spot-diff-cell{display:flex;align-items:center;justify-content:center;font-size:26px;background:#fff;border-radius:6px;user-select:none}
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 #4a90d9 inset;background:#eaf3ff}
682
- .fc-spot-diff-wrong{box-shadow:0 0 0 3px #e53935 inset;background:#fdecea}
683
- .fc-spot-diff-status{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#2e7d32}
684
- .fc-spot-diff-status-fail{color:#e53935}
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:320px;padding:16px;box-sizing:border-box}
811
- .fc-emoji-match-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}
812
- .fc-emoji-match-desc{font-size:15px;color:#4a90d9;margin-bottom:14px;text-align:center;font-weight:600}
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 #e0e0e0;background:#fff;border-radius:10px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;display:flex;align-items:center;justify-content:center}
815
- .fc-emoji-match-opt:hover{transform:scale(1.06);border-color:#4a90d9}
816
- .fc-emoji-match-opt-wrong{border-color:#e53935;background:#fdecea}
817
- .fc-emoji-match-opt-correct{border-color:#2e7d32;background:#e8f5e9}
818
- .fc-emoji-match-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#e53935}
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 pick2 = btn.dataset.emoji;
833
- if (verifyAnswer3(current, pick2)) {
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.style.color = "#2e7d32";
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:320px;padding:16px;box-sizing:border-box}
928
- .fc-meme-quiz-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}
929
- .fc-meme-quiz-hint{font-size:12px;color:#888;margin-bottom:10px;text-align:center}
930
- .fc-meme-quiz-q{font-size:24px;color:#4a90d9;margin-bottom:14px;text-align:center;letter-spacing:2px}
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 #e0e0e0;background:#fff;border-radius:8px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;color:#333}
933
- .fc-meme-quiz-opt:hover{transform:scale(1.03);border-color:#4a90d9}
934
- .fc-meme-quiz-opt-wrong{border-color:#e53935;background:#fdecea}
935
- .fc-meme-quiz-opt-correct{border-color:#2e7d32;background:#e8f5e9}
936
- .fc-meme-quiz-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#e53935}
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 pick2 = btn.textContent ?? "";
952
- if (verifyAnswer4(current, pick2)) {
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.style.color = "#2e7d32";
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
- clearInterval(moleTimer);
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:340px;padding:16px;box-sizing:border-box}
1133
- .fc-mini-game-title{font-size:15px;color:#333;margin-bottom:4px;text-align:center;font-weight:600}
1134
- .fc-mini-game-rule{font-size:12px;color:#888;margin-bottom:10px;text-align:center}
1135
- .fc-mini-game-hud{display:flex;justify-content:space-between;font-size:13px;color:#555;margin-bottom:10px}
1136
- .fc-mini-game-hud b{color:#4a90d9}
1137
- .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:#e8f5e9;border:1px solid #c8e6c9;border-radius:10px;padding:8px;box-sizing:border-box}
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:#2e7d32}
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:320px;padding:20px;box-sizing:border-box;border:1px solid #e0e0e0;border-radius:10px;background:#fafafa}
1274
- .fc-anti-bot-title{font-size:18px;color:#333;text-align:center;font-weight:600;margin-bottom:6px}
1275
- .fc-anti-bot-subtitle{font-size:12px;color:#888;text-align:center;margin-bottom:16px}
1276
- .fc-anti-bot-btn{display:block;width:100%;padding:12px;font-size:15px;border:1px solid #ccc;background:#fff;border-radius:6px;cursor:pointer;color:#333;transition:background .15s}
1277
- .fc-anti-bot-btn:hover:not(:disabled){background:#f0f7ff}
1278
- .fc-anti-bot-btn:disabled{cursor:default;color:#2e7d32;border-color:#2e7d32;background:#e8f5e9}
1279
- .fc-anti-bot-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#2e7d32}
1280
- .fc-anti-bot-hint{font-size:11px;color:#bbb;margin-top:14px;text-align:center;line-height:1.5;font-style:italic}
1281
- .fc-anti-bot-footer{font-size:10px;color:#ccc;margin-top:8px;text-align:center}
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) {