@funnycaptcha/embed 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,6 +55,126 @@ var FunnyCaptcha = (() => {
55
55
  function getCaptcha(id) {
56
56
  return registry.get(id);
57
57
  }
58
+ var TrackRecorder = class {
59
+ points = [];
60
+ origin = 0;
61
+ start() {
62
+ this.origin = performance.now();
63
+ this.points = [];
64
+ }
65
+ record(x, y) {
66
+ this.points.push({ x, y, t: performance.now() - this.origin });
67
+ }
68
+ stop() {
69
+ return this.points.slice();
70
+ }
71
+ clear() {
72
+ this.points = [];
73
+ this.origin = 0;
74
+ }
75
+ get count() {
76
+ return this.points.length;
77
+ }
78
+ };
79
+ function mean(arr) {
80
+ if (arr.length === 0) return 0;
81
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
82
+ }
83
+ function std(arr) {
84
+ if (arr.length === 0) return 0;
85
+ const m = mean(arr);
86
+ const v = arr.reduce((a, b) => a + (b - m) ** 2, 0) / arr.length;
87
+ return Math.sqrt(v);
88
+ }
89
+ function analyzeTrack(track) {
90
+ const reasons = [];
91
+ let score = 100;
92
+ if (track.length < 2) {
93
+ return {
94
+ humanScore: 0,
95
+ isBot: true,
96
+ reasons: ["\u8F68\u8FF9\u70B9\u4E0D\u8DB3"],
97
+ samples: track.length,
98
+ duration: 0
99
+ };
100
+ }
101
+ const first = track[0];
102
+ const last = track[track.length - 1];
103
+ const duration = last.t - first.t;
104
+ if (track.length < 8) {
105
+ reasons.push("\u8F68\u8FF9\u70B9\u592A\u5C11");
106
+ score -= 35;
107
+ }
108
+ if (duration < 280) {
109
+ reasons.push("\u62D6\u52A8\u8FC7\u5FEB");
110
+ score -= 30;
111
+ }
112
+ if (duration > 3e4) {
113
+ reasons.push("\u62D6\u52A8\u5F02\u5E38\u7F13\u6162");
114
+ score -= 15;
115
+ }
116
+ const speeds = [];
117
+ for (let i = 1; i < track.length; i++) {
118
+ const prev = track[i - 1];
119
+ const cur = track[i];
120
+ const dx = cur.x - prev.x;
121
+ const dy = cur.y - prev.y;
122
+ const dt = Math.max(1, cur.t - prev.t);
123
+ speeds.push(Math.sqrt(dx * dx + dy * dy) / dt);
124
+ }
125
+ const meanSpeed = mean(speeds);
126
+ const speedStd = std(speeds);
127
+ const cv = meanSpeed > 0 ? speedStd / meanSpeed : 0;
128
+ if (cv < 0.18 && speeds.length > 5) {
129
+ reasons.push("\u901F\u5EA6\u8FC7\u4E8E\u5747\u5300");
130
+ score -= 25;
131
+ }
132
+ const ys = track.map((p) => p.y);
133
+ const yStd = std(ys);
134
+ if (track.length > 10 && yStd < 1.2) {
135
+ reasons.push("Y\u8F74\u65E0\u81EA\u7136\u6296\u52A8");
136
+ score -= 22;
137
+ }
138
+ if (speeds.length > 6) {
139
+ const maxSpeed = Math.max(...speeds);
140
+ const tailLen = Math.max(1, Math.ceil(speeds.length * 0.2));
141
+ const tail = speeds.slice(-tailLen);
142
+ const tailMean = mean(tail);
143
+ if (maxSpeed > 0 && tailMean > maxSpeed * 0.55) {
144
+ reasons.push("\u672B\u7AEF\u672A\u51CF\u901F");
145
+ score -= 18;
146
+ }
147
+ }
148
+ let reversals = 0;
149
+ for (let i = 2; i < track.length; i++) {
150
+ const d1 = track[i - 1].x - track[i - 2].x;
151
+ const d2 = track[i].x - track[i - 1].x;
152
+ if (d1 * d2 < 0 && Math.abs(d1) > 0.5 && Math.abs(d2) > 0.5) reversals++;
153
+ }
154
+ if (track.length > 12 && reversals === 0) {
155
+ reasons.push("\u65E0\u56DE\u9000/\u6821\u5BF9\u52A8\u4F5C");
156
+ score -= 12;
157
+ }
158
+ if (speeds.length > 4) {
159
+ const accels = [];
160
+ for (let i = 1; i < speeds.length; i++) {
161
+ accels.push(speeds[i] - speeds[i - 1]);
162
+ }
163
+ const accelStd = std(accels);
164
+ if (accelStd < 0.05 && meanSpeed > 0.1) {
165
+ reasons.push("\u52A0\u901F\u5EA6\u8FC7\u4E8E\u5E73\u6ED1");
166
+ score -= 15;
167
+ }
168
+ }
169
+ score = Math.max(0, Math.min(100, Math.round(score)));
170
+ return {
171
+ humanScore: score,
172
+ isBot: score < 50,
173
+ reasons,
174
+ samples: track.length,
175
+ duration: Math.round(duration)
176
+ };
177
+ }
58
178
 
59
179
  // src/iframe-host.ts
60
180
  function mountInto(container, type, config) {
@@ -190,47 +310,95 @@ var FunnyCaptcha = (() => {
190
310
  return c.code.toLowerCase() === answer.trim().toLowerCase();
191
311
  }
192
312
  var STR2 = {
193
- zh: { placeholder: "\u8F93\u5165\u56FE\u4E2D\u5B57\u7B26", submit: "\u9A8C\u8BC1", fail: "\u770B\u4E0D\u6E05\uFF1F\u6362\u4E00\u5F20", refresh: "\u5237\u65B0" },
194
- 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" }
195
315
  };
196
- function drawDistorted(canvas, code, surface, text, border) {
316
+ var PERIOD = 180;
317
+ function drawFrame(canvas, code, frame, surface, text, border, accentSoft) {
197
318
  const ctx = canvas.getContext("2d");
198
319
  if (!ctx) return;
199
- ctx.clearRect(0, 0, canvas.width, canvas.height);
200
- ctx.fillStyle = surface;
201
- ctx.fillRect(0, 0, canvas.width, canvas.height);
202
- for (let i = 0; i < 80; i++) {
203
- ctx.fillStyle = `rgba(0,0,0,${Math.random() * 0.12})`;
204
- ctx.fillRect(Math.random() * canvas.width, Math.random() * canvas.height, 1.5, 1.5);
320
+ const W = canvas.width;
321
+ const H = canvas.height;
322
+ const f = frame % PERIOD;
323
+ let phase;
324
+ if (f < 90) phase = "strong";
325
+ else if (f < 120) phase = "fade-out";
326
+ else if (f < 150) phase = "clear";
327
+ else phase = "fade-in";
328
+ let intensity;
329
+ if (phase === "strong") intensity = 1;
330
+ else if (phase === "fade-out") intensity = 1 - (f - 90) / 30;
331
+ else if (phase === "clear") intensity = 0;
332
+ else intensity = (f - 150) / 30;
333
+ let bg = surface;
334
+ if (phase === "strong") {
335
+ bg = Math.floor(frame / 5) % 2 === 0 ? surface : accentSoft;
336
+ }
337
+ ctx.clearRect(0, 0, W, H);
338
+ ctx.fillStyle = bg;
339
+ ctx.fillRect(0, 0, W, H);
340
+ const noiseCount = Math.floor(40 + intensity * 80);
341
+ for (let i = 0; i < noiseCount; i++) {
342
+ ctx.fillStyle = `rgba(0,0,0,${Math.random() * 0.12 * (0.3 + intensity * 0.7)})`;
343
+ ctx.fillRect(Math.random() * W, Math.random() * H, 1.5, 1.5);
205
344
  }
206
345
  const chars = code.split("");
207
- const step = canvas.width / (chars.length + 1);
346
+ const step = W / (chars.length + 1);
347
+ const focusIndex = phase === "strong" ? frame % chars.length : -1;
208
348
  chars.forEach((ch, i) => {
209
349
  ctx.save();
210
- const x = step * (i + 1);
211
- 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;
212
355
  ctx.translate(x, y);
213
- ctx.rotate((Math.random() - 0.5) * 0.5);
214
- ctx.font = `${24 + Math.floor(Math.random() * 8)}px sans-serif`;
215
- ctx.fillStyle = text;
356
+ ctx.rotate((Math.random() - 0.5) * (0.2 + intensity * 0.3));
357
+ const fontSize = 24 + Math.floor(Math.random() * 8);
358
+ ctx.font = `${fontSize}px sans-serif`;
216
359
  ctx.textAlign = "center";
217
360
  ctx.textBaseline = "middle";
361
+ let alpha;
362
+ let blur;
363
+ if (phase === "strong") {
364
+ if (i === focusIndex) {
365
+ alpha = 0.85 + Math.random() * 0.15;
366
+ blur = 0;
367
+ } else {
368
+ alpha = 0.1 + Math.random() * 0.3;
369
+ blur = 3;
370
+ }
371
+ } else if (phase === "clear") {
372
+ alpha = 0.85 + Math.random() * 0.15;
373
+ blur = 0;
374
+ } else {
375
+ const t = 1 - intensity;
376
+ alpha = 0.5 + t * 0.2 + (Math.random() - 0.5) * 0.05;
377
+ blur = intensity * 3;
378
+ }
379
+ ctx.globalAlpha = Math.max(0, Math.min(1, alpha));
380
+ if (blur > 0) ctx.filter = `blur(${blur}px)`;
381
+ ctx.fillStyle = text;
218
382
  ctx.fillText(ch, 0, 0);
219
383
  ctx.restore();
220
384
  });
221
- for (let i = 0; i < 6; i++) {
385
+ const lineCount = Math.floor(2 + intensity * 6);
386
+ for (let i = 0; i < lineCount; i++) {
387
+ ctx.save();
222
388
  ctx.strokeStyle = border;
223
389
  ctx.lineWidth = 1 + Math.random() * 1.5;
390
+ ctx.globalAlpha = 0.4 + intensity * 0.4;
224
391
  ctx.beginPath();
225
- const x1 = Math.random() * canvas.width;
226
- const y1 = Math.random() * canvas.height;
227
- const x2 = Math.random() * canvas.width;
228
- const y2 = Math.random() * canvas.height;
229
- const cpx = Math.random() * canvas.width;
230
- const cpy = 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;
231
398
  ctx.moveTo(x1, y1);
232
399
  ctx.quadraticCurveTo(cpx, cpy, x2, y2);
233
400
  ctx.stroke();
401
+ ctx.restore();
234
402
  }
235
403
  }
236
404
  function readVar(root, name, fallback) {
@@ -243,16 +411,26 @@ var FunnyCaptcha = (() => {
243
411
  let current;
244
412
  let listeners = [];
245
413
  let startTime = Date.now();
414
+ let rafId = 0;
415
+ let frame = 0;
416
+ let paused = false;
417
+ let cssCache = null;
418
+ let cssCacheFrame = -100;
246
419
  function render2() {
247
420
  current = generateChallenge2();
248
421
  startTime = Date.now();
422
+ cancelAnimationFrame(rafId);
423
+ frame = 0;
424
+ paused = false;
425
+ cssCache = null;
426
+ cssCacheFrame = -100;
249
427
  container.innerHTML = `
250
428
  <style>
251
429
  .fc-text{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
252
430
  .fc-text[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
253
431
  .fc-text[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
254
432
  .fc-text{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
255
- .fc-text-canvas{display:block;width:100%;max-width:240px;height:60px;border-radius:8px;border:1px solid var(--fc-border);margin-bottom:10px}
433
+ .fc-text-canvas{display:block;width:100%;max-width:280px;height:60px;border-radius:8px;border:1px solid var(--fc-border);margin-bottom:10px;box-shadow:0 0 0 1px var(--fc-border)}
256
434
  .fc-text-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
257
435
  .fc-text-input{flex:1;min-width:0;padding:8px 12px;font-size:14px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text);border-radius:8px;box-sizing:border-box;outline:none}
258
436
  .fc-text-input:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
@@ -260,25 +438,59 @@ var FunnyCaptcha = (() => {
260
438
  .fc-text-btn:hover{opacity:.9}
261
439
  .fc-text-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
262
440
  .fc-text-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
441
+ .fc-text-pause{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
442
+ .fc-text-pause:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
263
443
  .fc-text-msg{font-size:13px;min-height:18px;margin-top:10px;color:var(--fc-danger)}
264
444
  </style>
265
445
  <div class="fc-text" data-theme="${theme}">
266
- <canvas class="fc-text-canvas" width="240" height="60"></canvas>
446
+ <canvas class="fc-text-canvas" width="280" height="60"></canvas>
267
447
  <div class="fc-text-row">
268
448
  <input class="fc-text-input" placeholder="${t.placeholder}" />
269
449
  <button class="fc-text-btn">${t.submit}</button>
270
450
  <button class="fc-text-refresh">${t.refresh}</button>
451
+ <button class="fc-text-pause">${t.pause}</button>
271
452
  </div>
272
453
  <div class="fc-text-msg"></div>
273
454
  </div>
274
455
  `;
275
456
  const root = container.querySelector(".fc-text");
276
457
  const canvas = container.querySelector(".fc-text-canvas");
277
- const surface = readVar(root, "--fc-surface", "#f6f7f9");
278
- const text = readVar(root, "--fc-text", "#0f172a");
279
- const border = readVar(root, "--fc-border", "#e2e8f0");
280
- drawDistorted(canvas, current.code, surface, text, border);
458
+ const code = current.code;
459
+ function readCssVars() {
460
+ return {
461
+ surface: readVar(root, "--fc-surface", "#f6f7f9"),
462
+ text: readVar(root, "--fc-text", "#0f172a"),
463
+ border: readVar(root, "--fc-border", "#e2e8f0"),
464
+ accentSoft: readVar(root, "--fc-accent-soft", "#eef2ff")
465
+ };
466
+ }
467
+ function tick() {
468
+ if (paused) return;
469
+ if (!cssCache || frame - cssCacheFrame >= 30) {
470
+ cssCache = readCssVars();
471
+ cssCacheFrame = frame;
472
+ }
473
+ const c = cssCache;
474
+ if (c) {
475
+ drawFrame(canvas, code, frame, c.surface, c.text, c.border, c.accentSoft);
476
+ }
477
+ frame++;
478
+ rafId = requestAnimationFrame(tick);
479
+ }
480
+ rafId = requestAnimationFrame(tick);
281
481
  container.querySelector(".fc-text-refresh").addEventListener("click", render2);
482
+ const pauseBtn = container.querySelector(".fc-text-pause");
483
+ pauseBtn.addEventListener("click", () => {
484
+ if (paused) {
485
+ paused = false;
486
+ pauseBtn.textContent = t.pause;
487
+ rafId = requestAnimationFrame(tick);
488
+ } else {
489
+ paused = true;
490
+ cancelAnimationFrame(rafId);
491
+ pauseBtn.textContent = t.resume;
492
+ }
493
+ });
282
494
  const btn = container.querySelector(".fc-text-btn");
283
495
  const input = container.querySelector(".fc-text-input");
284
496
  const msg = container.querySelector(".fc-text-msg");
@@ -305,6 +517,7 @@ var FunnyCaptcha = (() => {
305
517
  mount: () => render2(),
306
518
  reset: () => render2(),
307
519
  destroy: () => {
520
+ cancelAnimationFrame(rafId);
308
521
  container.innerHTML = "";
309
522
  listeners = [];
310
523
  },
@@ -332,20 +545,56 @@ var FunnyCaptcha = (() => {
332
545
  return `${c.token}:completed`;
333
546
  }
334
547
  var STR3 = {
335
- zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5230\u6700\u53F3\u7AEF", tip: "\u5411\u53F3\u62D6\u52A8", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u8BF7\u62D6\u5230\u5C3D\u5934", refresh: "\u5237\u65B0" },
336
- en: { title: "Drag the slider to the right end", tip: "Drag right", success: "Verified", fail: "Drag to the end", refresh: "Refresh" }
548
+ zh: { title: "\u6CBF\u87BA\u65CB\u62D6\u52A8\u6ED1\u5757\u5230\u7EC8\u70B9", tip: "\u6CBF\u87BA\u65CB\u8DEF\u5F84\u62D6\u52A8", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u8BF7\u62D6\u5230\u5C3D\u5934", botFail: "\u68C0\u6D4B\u5230\u5F02\u5E38\u64CD\u4F5C", refresh: "\u5237\u65B0" },
549
+ en: { title: "Drag along the spiral to the center", tip: "Follow the spiral", success: "Verified", fail: "Drag to the end", botFail: "Bot-like behavior", refresh: "Refresh" }
337
550
  };
551
+ var CANVAS_W = 320;
552
+ var CANVAS_H = 200;
553
+ var HANDLE_W = 36;
554
+ var HANDLE_H = 36;
555
+ var SPIRAL_THETA_MAX = 6 * Math.PI;
556
+ var SPIRAL_A = 0;
557
+ var SPIRAL_B = 5;
558
+ var CX = CANVAS_W / 2;
559
+ var CY = CANVAS_H / 2;
560
+ var SPIRAL_R_MAX = SPIRAL_B * SPIRAL_THETA_MAX;
561
+ function spiralPoints() {
562
+ const steps = 200;
563
+ const pts = [];
564
+ for (let i = 0; i <= steps; i++) {
565
+ const t = i / steps;
566
+ const theta = SPIRAL_THETA_MAX * (1 - t);
567
+ const r = SPIRAL_A + SPIRAL_B * theta;
568
+ const x = CX + r * Math.cos(theta);
569
+ const y = CY + r * Math.sin(theta);
570
+ pts.push({ x, y, theta });
571
+ }
572
+ return pts;
573
+ }
574
+ function spiralPath(pts) {
575
+ return pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(" ");
576
+ }
577
+ function pointAtProgress(pts, progress) {
578
+ const idx = Math.min(pts.length - 1, Math.max(0, Math.round(progress * (pts.length - 1))));
579
+ return pts[idx];
580
+ }
338
581
  function createSliderInstance(container, config) {
339
582
  const t = STR3[config.locale];
340
583
  const theme = config.theme ?? "light";
584
+ const recorder = new TrackRecorder();
341
585
  let current;
342
586
  let listeners = [];
343
587
  let startTime = Date.now();
344
588
  let done = false;
589
+ let locked = false;
345
590
  function render2() {
346
591
  current = generateChallenge3();
347
592
  startTime = Date.now();
348
593
  done = false;
594
+ locked = false;
595
+ recorder.clear();
596
+ const pts = spiralPoints();
597
+ const pathD = spiralPath(pts);
349
598
  container.innerHTML = `
350
599
  <style>
351
600
  .fc-slider{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
@@ -353,24 +602,31 @@ var FunnyCaptcha = (() => {
353
602
  .fc-slider[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
354
603
  .fc-slider{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
355
604
  .fc-slider-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
356
- .fc-slider-track{position:relative;height:40px;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:20px}
357
- .fc-slider-progress{position:absolute;left:0;top:0;height:100%;width:0;background:var(--fc-accent);border-radius:20px;transition:width .05s,background .15s}
358
- .fc-slider-handle{position:absolute;left:0;top:50%;transform:translateY(-50%);width:50px;height:50px;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,.15);user-select:none;touch-action:none;transition:border-color .15s,color .15s}
605
+ .fc-slider-stage{position:relative;width:${CANVAS_W}px;height:${CANVAS_H}px;margin:0 auto;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:10px;overflow:hidden}
606
+ .fc-slider-svg{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
607
+ .fc-slider-path-bg{fill:none;stroke:var(--fc-border);stroke-width:3;stroke-linecap:round}
608
+ .fc-slider-path-fg{fill:none;stroke:var(--fc-accent);stroke-width:4;stroke-linecap:round;transition:stroke .15s}
609
+ .fc-slider-target{fill:none;stroke:var(--fc-success);stroke-width:2;stroke-dasharray:4 3;opacity:.7}
610
+ .fc-slider-handle{position:absolute;width:${HANDLE_W}px;height:${HANDLE_H}px;background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.2);user-select:none;touch-action:none;transition:border-color .15s;transform:translate(-50%,-50%);z-index:2}
359
611
  .fc-slider-handle:active{cursor:grabbing}
360
- .fc-slider-handle::after{content:'\\2192';color:var(--fc-accent);font-size:18px;font-weight:bold;transition:color .15s}
361
- .fc-slider-tip{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:13px;color:var(--fc-text-soft);pointer-events:none;transition:opacity .2s}
612
+ .fc-slider-handle::after{content:'';width:10px;height:10px;background:var(--fc-accent);border-radius:50%;transition:background .15s}
613
+ .fc-slider-tip{position:absolute;left:50%;top:8px;transform:translateX(-50%);font-size:12px;color:var(--fc-text-soft);pointer-events:none;transition:opacity .2s;background:var(--fc-bg);padding:2px 8px;border-radius:4px}
362
614
  .fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
363
615
  .fc-slider-row{display:flex;justify-content:center;margin-top:10px}
364
616
  .fc-slider-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
365
617
  .fc-slider-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
366
618
  .fc-slider-done .fc-slider-handle{border-color:var(--fc-success)}
367
- .fc-slider-done .fc-slider-handle::after{color:var(--fc-success)}
368
- .fc-slider-done .fc-slider-progress{background: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)}
369
621
  </style>
370
622
  <div class="fc-slider" data-theme="${theme}">
371
623
  <div class="fc-slider-title">${t.title}</div>
372
- <div class="fc-slider-track">
373
- <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>
374
630
  <div class="fc-slider-handle" role="slider" tabindex="0"></div>
375
631
  <span class="fc-slider-tip">${t.tip}</span>
376
632
  </div>
@@ -381,27 +637,44 @@ var FunnyCaptcha = (() => {
381
637
  </div>
382
638
  `;
383
639
  const root = container.querySelector(".fc-slider");
384
- const track = container.querySelector(".fc-slider-track");
640
+ const stage = container.querySelector(".fc-slider-stage");
385
641
  const handle = container.querySelector(".fc-slider-handle");
386
- const progress = container.querySelector(".fc-slider-progress");
387
642
  const tip = container.querySelector(".fc-slider-tip");
388
643
  const msg = container.querySelector(".fc-slider-msg");
389
644
  const refreshBtn = container.querySelector(".fc-slider-refresh");
645
+ const fgPath = container.querySelector(".fc-slider-path-fg");
390
646
  refreshBtn.addEventListener("click", render2);
647
+ const totalLen = fgPath.getTotalLength();
648
+ fgPath.style.strokeDasharray = `${totalLen}`;
649
+ fgPath.style.strokeDashoffset = `${totalLen}`;
391
650
  let dragging = false;
392
- let startX = 0;
393
- let startOffset = 0;
394
- let currentOffset = 0;
395
- function applyPos(px) {
396
- handle.style.left = `${px}px`;
397
- progress.style.width = `${px}px`;
398
- 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";
399
661
  }
662
+ function bounceBack() {
663
+ handle.style.transition = "left .35s ease, top .35s ease, border-color .15s";
664
+ fgPath.style.transition = "stroke-dashoffset .35s ease, stroke .15s";
665
+ applyProgress(0);
666
+ setTimeout(() => {
667
+ handle.style.transition = "border-color .15s";
668
+ fgPath.style.transition = "stroke .15s";
669
+ }, 360);
670
+ }
671
+ applyProgress(0);
400
672
  handle.addEventListener("pointerdown", (e) => {
401
- if (done) return;
673
+ if (done || locked) return;
402
674
  dragging = true;
403
- startX = e.clientX;
404
- startOffset = currentOffset;
675
+ startPointer = { x: e.clientX, y: e.clientY };
676
+ startProgress = currentProgress;
677
+ recorder.start();
405
678
  try {
406
679
  handle.setPointerCapture(e.pointerId);
407
680
  } catch {
@@ -410,40 +683,55 @@ var FunnyCaptcha = (() => {
410
683
  });
411
684
  handle.addEventListener("pointermove", (e) => {
412
685
  if (!dragging) return;
413
- const maxX = track.clientWidth - handle.offsetWidth;
414
- let px = startOffset + (e.clientX - startX);
415
- px = Math.max(0, Math.min(maxX, px));
416
- currentOffset = px;
417
- 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);
418
700
  });
419
701
  const finish = async () => {
420
702
  if (!dragging) return;
421
703
  dragging = false;
422
- const maxX = Math.max(1, track.clientWidth - handle.offsetWidth);
423
- if (currentOffset / maxX >= 0.95) {
424
- done = true;
425
- applyPos(maxX);
426
- root.classList.add("fc-slider-done");
427
- const result = {
428
- success: true,
429
- proof: await hashProof(proofInput(current)),
430
- duration: Date.now() - startTime
431
- };
432
- msg.textContent = t.success;
433
- config.onVerify?.(result);
434
- listeners.forEach((cb) => cb(result));
435
- } else {
436
- currentOffset = 0;
437
- handle.style.transition = "left .3s";
438
- progress.style.transition = "width .3s";
439
- applyPos(0);
704
+ const trackPoints = recorder.stop();
705
+ if (currentProgress < 0.85) {
706
+ bounceBack();
707
+ msg.style.color = "var(--fc-danger)";
440
708
  msg.textContent = t.fail;
709
+ return;
710
+ }
711
+ const analysis = analyzeTrack(trackPoints);
712
+ if (analysis.isBot) {
713
+ locked = true;
714
+ msg.style.color = "var(--fc-danger)";
715
+ msg.textContent = config.locale === "zh" ? `\u7591\u4F3C\u673A\u5668\u4EBA\u64CD\u4F5C\uFF08${analysis.reasons.join("\u3001")}\uFF09` : `${t.botFail} detected`;
716
+ bounceBack();
441
717
  setTimeout(() => {
442
- handle.style.transition = "";
443
- progress.style.transition = "";
444
- if (!done) msg.textContent = "";
445
- }, 320);
718
+ if (!done) render2();
719
+ }, 2e3);
720
+ return;
446
721
  }
722
+ done = true;
723
+ applyProgress(1);
724
+ root.classList.add("fc-slider-done");
725
+ const result = {
726
+ success: true,
727
+ proof: await hashProof(proofInput(current)),
728
+ duration: Date.now() - startTime,
729
+ metadata: { humanScore: analysis.humanScore, reasons: analysis.reasons }
730
+ };
731
+ msg.style.color = "var(--fc-success)";
732
+ msg.textContent = t.success;
733
+ config.onVerify?.(result);
734
+ listeners.forEach((cb) => cb(result));
447
735
  };
448
736
  handle.addEventListener("pointerup", finish);
449
737
  handle.addEventListener("pointercancel", finish);
@@ -489,8 +777,8 @@ var FunnyCaptcha = (() => {
489
777
  return c.order.join(",");
490
778
  }
491
779
  var STR4 = {
492
- zh: { title: "\u6309 1 \u2192 2 \u2192 3 \u2192 4 \u987A\u5E8F\u70B9\u51FB", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5", refresh: "\u5237\u65B0" },
493
- en: { title: "Click in order 1 \u2192 2 \u2192 3 \u2192 4", success: "Verified", fail: "Wrong order, try again", refresh: "Refresh" }
780
+ zh: { title: "\u6309 1 -> 2 -> 3 -> 4 \u987A\u5E8F\u70B9\u51FB\uFF08\u4F4D\u7F6E\u4F1A\u53D8\uFF09", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5", refresh: "\u5237\u65B0" },
781
+ en: { title: "Click in order 1 -> 2 -> 3 -> 4 (positions shift)", success: "Verified", fail: "Wrong order, try again", refresh: "Refresh" }
494
782
  };
495
783
  var AREA_W = 280;
496
784
  var AREA_H = 180;
@@ -528,16 +816,18 @@ var FunnyCaptcha = (() => {
528
816
  let listeners = [];
529
817
  let startTime = Date.now();
530
818
  let clicked = [];
819
+ let doneSet = /* @__PURE__ */ new Set();
531
820
  let finished = false;
532
821
  function render2() {
533
822
  current = generateChallenge4();
534
823
  clicked = [];
824
+ doneSet = /* @__PURE__ */ new Set();
535
825
  finished = false;
536
826
  startTime = Date.now();
537
827
  const slots = generateSlots(current.targets.length);
538
828
  const boxes = current.targets.map((num, i) => {
539
829
  const slot = slots[i];
540
- return `<div class="fc-click-box" data-num="${num}" style="left:${Math.round(slot.x)}px;top:${Math.round(slot.y)}px;">${num}</div>`;
830
+ return `<div class="fc-click-box" data-idx="${i}" data-num="${num}" style="left:${Math.round(slot.x)}px;top:${Math.round(slot.y)}px;">${num}</div>`;
541
831
  }).join("");
542
832
  container.innerHTML = `
543
833
  <style>
@@ -547,7 +837,7 @@ var FunnyCaptcha = (() => {
547
837
  .fc-click{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
548
838
  .fc-click-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
549
839
  .fc-click-area{position:relative;width:${AREA_W}px;height:${AREA_H}px;background:var(--fc-surface);border-radius:8px;border:1px solid var(--fc-border);overflow:hidden}
550
- .fc-click-box{position:absolute;width:${BOX_W}px;height:${BOX_H}px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:bold;color:var(--fc-accent);background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:8px;cursor:pointer;user-select:none;transition:transform .15s,background .15s,color .15s,border-color .15s}
840
+ .fc-click-box{position:absolute;width:${BOX_W}px;height:${BOX_H}px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:bold;color:var(--fc-accent);background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:8px;cursor:pointer;user-select:none;transition:left .4s cubic-bezier(.4,0,.2,1),top .4s cubic-bezier(.4,0,.2,1),transform .15s,background .15s,color .15s,border-color .15s}
551
841
  .fc-click-box:hover{transform:scale(1.05)}
552
842
  .fc-click-box-active{background:var(--fc-accent);color:var(--fc-bg)}
553
843
  .fc-click-box-done{background:var(--fc-success);border-color:var(--fc-success);color:#fff;cursor:default}
@@ -570,6 +860,17 @@ var FunnyCaptcha = (() => {
570
860
  const msg = container.querySelector(".fc-click-msg");
571
861
  const refreshBtn = container.querySelector(".fc-click-refresh");
572
862
  refreshBtn.addEventListener("click", render2);
863
+ function relocateBoxes(excludeIndex) {
864
+ const remaining = current.targets.map((_, i) => i).filter((i) => i !== excludeIndex && !doneSet.has(i));
865
+ if (remaining.length === 0) return;
866
+ const newSlots = generateSlots(remaining.length);
867
+ remaining.forEach((idx, j) => {
868
+ const box = area.querySelector(`[data-idx="${idx}"]`);
869
+ const slot = newSlots[j];
870
+ box.style.left = `${Math.round(slot.x)}px`;
871
+ box.style.top = `${Math.round(slot.y)}px`;
872
+ });
873
+ }
573
874
  area.querySelectorAll(".fc-click-box").forEach((el) => {
574
875
  el.addEventListener("click", async () => {
575
876
  if (finished) return;
@@ -578,7 +879,9 @@ var FunnyCaptcha = (() => {
578
879
  const num = Number(box.dataset.num);
579
880
  const expected = current.order[clicked.length];
580
881
  if (num === expected) {
882
+ const idx = Number(box.dataset.idx);
581
883
  clicked.push(num);
884
+ doneSet.add(idx);
582
885
  box.classList.add("fc-click-box-done");
583
886
  if (clicked.length === current.order.length) {
584
887
  finished = true;
@@ -590,6 +893,8 @@ var FunnyCaptcha = (() => {
590
893
  msg.textContent = t.success;
591
894
  config.onVerify?.(result);
592
895
  listeners.forEach((cb) => cb(result));
896
+ } else {
897
+ relocateBoxes(idx);
593
898
  }
594
899
  } else {
595
900
  finished = true;
@@ -977,8 +1282,8 @@ var FunnyCaptcha = (() => {
977
1282
  el.addEventListener("click", async () => {
978
1283
  if (finished) return;
979
1284
  const btn = el;
980
- const pick2 = btn.dataset.emoji;
981
- if (verifyAnswer3(current, pick2)) {
1285
+ const pick6 = btn.dataset.emoji;
1286
+ if (verifyAnswer3(current, pick6)) {
982
1287
  finished = true;
983
1288
  btn.classList.add("fc-emoji-match-opt-correct");
984
1289
  const result = {
@@ -1109,8 +1414,8 @@ var FunnyCaptcha = (() => {
1109
1414
  el.addEventListener("click", async () => {
1110
1415
  if (finished) return;
1111
1416
  const btn = el;
1112
- const pick2 = btn.textContent ?? "";
1113
- if (verifyAnswer4(current, pick2)) {
1417
+ const pick6 = btn.textContent ?? "";
1418
+ if (verifyAnswer4(current, pick6)) {
1114
1419
  finished = true;
1115
1420
  btn.classList.add("fc-meme-quiz-opt-correct");
1116
1421
  const result = {
@@ -1585,25 +1890,56 @@ var FunnyCaptcha = (() => {
1585
1890
  return `${c.chars.join("")}:completed`;
1586
1891
  }
1587
1892
  var STR11 = {
1588
- zh: { title: "\u8BF7\u6309\u987A\u5E8F\u70B9\u51FB", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5", refresh: "\u5237\u65B0" },
1589
- en: { title: "Click in order", success: "Verified", fail: "Wrong order, try again", refresh: "Refresh" }
1893
+ zh: { title: "\u8BF7\u6309\u987A\u5E8F\u70B9\u51FB\uFF08\u4F4D\u7F6E\u4F1A\u53D8\uFF09", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5", refresh: "\u5237\u65B0" },
1894
+ en: { title: "Click in order (positions shift)", success: "Verified", fail: "Wrong order, try again", refresh: "Refresh" }
1590
1895
  };
1896
+ var AREA_W22 = 320;
1897
+ var AREA_H22 = 200;
1898
+ var BOX_W2 = 44;
1899
+ var BOX_H2 = 44;
1900
+ var MIN_DIST22 = 80;
1901
+ function generateSlots2(count) {
1902
+ const maxX = AREA_W22 - BOX_W2;
1903
+ const maxY = AREA_H22 - BOX_H2;
1904
+ const slots = [];
1905
+ let attempts = 0;
1906
+ while (slots.length < count && attempts < 1e3) {
1907
+ attempts++;
1908
+ const candidate = { x: Math.random() * maxX, y: Math.random() * maxY };
1909
+ let ok = true;
1910
+ for (const s of slots) {
1911
+ const dx = candidate.x - s.x;
1912
+ const dy = candidate.y - s.y;
1913
+ if (Math.sqrt(dx * dx + dy * dy) < MIN_DIST22) {
1914
+ ok = false;
1915
+ break;
1916
+ }
1917
+ }
1918
+ if (ok) slots.push(candidate);
1919
+ }
1920
+ while (slots.length < count) {
1921
+ slots.push({ x: Math.random() * maxX, y: Math.random() * maxY });
1922
+ }
1923
+ return slots;
1924
+ }
1591
1925
  function createClickTextInstance(container, config) {
1592
1926
  const t = STR11[config.locale];
1593
1927
  let current;
1594
1928
  let listeners = [];
1595
1929
  let startTime = Date.now();
1596
1930
  let clicked = [];
1931
+ let doneSet = /* @__PURE__ */ new Set();
1597
1932
  let finished = false;
1598
1933
  function render2() {
1599
1934
  current = generateChallenge11();
1600
1935
  clicked = [];
1936
+ doneSet = /* @__PURE__ */ new Set();
1601
1937
  finished = false;
1602
1938
  startTime = Date.now();
1603
1939
  const prompt = current.chars.join("\u3001");
1604
1940
  const chars = current.chars.map((ch, i) => {
1605
1941
  const p = current.positions[i];
1606
- return `<div class="fc-click-text-char" data-ch="${ch}" style="left:${p.x}px;top:${p.y}px;">${ch}</div>`;
1942
+ return `<div class="fc-click-text-char" data-idx="${i}" data-ch="${ch}" style="left:${p.x}px;top:${p.y}px;">${ch}</div>`;
1607
1943
  }).join("");
1608
1944
  container.innerHTML = `
1609
1945
  <style>
@@ -1613,7 +1949,7 @@ var FunnyCaptcha = (() => {
1613
1949
  .fc-click-text-title{font-size:14px;color:var(--fc-text);margin-bottom:4px;text-align:center}
1614
1950
  .fc-click-text-prompt{font-size:15px;color:var(--fc-accent);margin-bottom:12px;text-align:center;font-weight:600}
1615
1951
  .fc-click-text-area{position:relative;width:320px;height:200px;background:var(--fc-surface);border-radius:8px;border:1px solid var(--fc-border);overflow:hidden}
1616
- .fc-click-text-char{position:absolute;width:44px;height:44px;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:600;color:var(--fc-text);background:var(--fc-bg);border:2px solid var(--fc-border);border-radius:8px;cursor:pointer;user-select:none;transition:transform .15s,background .15s,color .15s,border-color .15s}
1952
+ .fc-click-text-char{position:absolute;width:44px;height:44px;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:600;color:var(--fc-text);background:var(--fc-bg);border:2px solid var(--fc-border);border-radius:8px;cursor:pointer;user-select:none;transition:left .4s cubic-bezier(.4,0,.2,1),top .4s cubic-bezier(.4,0,.2,1),transform .15s,background .15s,color .15s,border-color .15s}
1617
1953
  .fc-click-text-char:hover{transform:scale(1.08);border-color:var(--fc-accent)}
1618
1954
  .fc-click-text-char-done{background:var(--fc-success);border-color:var(--fc-success);color:#fff;cursor:default}
1619
1955
  .fc-click-text-char-done:hover{transform:none}
@@ -1631,9 +1967,21 @@ var FunnyCaptcha = (() => {
1631
1967
  <button class="fc-click-text-refresh">${t.refresh}</button>
1632
1968
  </div>
1633
1969
  `;
1970
+ const area = container.querySelector(".fc-click-text-area");
1634
1971
  const msg = container.querySelector(".fc-click-text-msg");
1635
1972
  const refreshBtn = container.querySelector(".fc-click-text-refresh");
1636
1973
  refreshBtn.addEventListener("click", () => render2());
1974
+ function relocateChars(excludeIndex) {
1975
+ const remaining = current.chars.map((_, i) => i).filter((i) => i !== excludeIndex && !doneSet.has(i));
1976
+ if (remaining.length === 0) return;
1977
+ const newSlots = generateSlots2(remaining.length);
1978
+ remaining.forEach((idx, j) => {
1979
+ const box = area.querySelector(`[data-idx="${idx}"]`);
1980
+ const slot = newSlots[j];
1981
+ box.style.left = `${Math.round(slot.x)}px`;
1982
+ box.style.top = `${Math.round(slot.y)}px`;
1983
+ });
1984
+ }
1637
1985
  container.querySelectorAll(".fc-click-text-char").forEach((el) => {
1638
1986
  el.addEventListener("click", async () => {
1639
1987
  if (finished) return;
@@ -1642,7 +1990,9 @@ var FunnyCaptcha = (() => {
1642
1990
  const ch = box.dataset.ch;
1643
1991
  const expected = current.chars[clicked.length];
1644
1992
  if (ch === expected) {
1993
+ const idx = Number(box.dataset.idx);
1645
1994
  clicked.push(ch);
1995
+ doneSet.add(idx);
1646
1996
  box.classList.add("fc-click-text-char-done");
1647
1997
  if (clicked.length === current.chars.length) {
1648
1998
  finished = true;
@@ -1654,6 +2004,8 @@ var FunnyCaptcha = (() => {
1654
2004
  msg.textContent = t.success;
1655
2005
  config.onVerify?.(result);
1656
2006
  listeners.forEach((cb) => cb(result));
2007
+ } else {
2008
+ relocateChars(idx);
1657
2009
  }
1658
2010
  } else {
1659
2011
  finished = true;
@@ -1781,8 +2133,8 @@ var FunnyCaptcha = (() => {
1781
2133
  el.addEventListener("click", async () => {
1782
2134
  if (finished) return;
1783
2135
  const btn = el;
1784
- const pick2 = btn.dataset.hex;
1785
- if (verifyAnswer5(current, pick2)) {
2136
+ const pick6 = btn.dataset.hex;
2137
+ if (verifyAnswer5(current, pick6)) {
1786
2138
  finished = true;
1787
2139
  btn.classList.add("fc-color-pick-opt-correct");
1788
2140
  msg.classList.add("fc-color-pick-msg-success");
@@ -1826,12 +2178,12 @@ var FunnyCaptcha = (() => {
1826
2178
  defineCaptcha(colorPickPlugin);
1827
2179
 
1828
2180
  // ../captchas/puzzle/dist/index.js
1829
- var TOLERANCE = 5;
2181
+ var TOLERANCE = 8;
1830
2182
  function randInt8(min, max) {
1831
2183
  return Math.floor(Math.random() * (max - min + 1)) + min;
1832
2184
  }
1833
2185
  function generateChallenge13() {
1834
- return { gapPosition: randInt8(10, 90) };
2186
+ return { gapPosition: randInt8(25, 58) };
1835
2187
  }
1836
2188
  function proofInput8(c) {
1837
2189
  return `${c.gapPosition}:completed`;
@@ -1840,71 +2192,232 @@ var FunnyCaptcha = (() => {
1840
2192
  return Math.abs(userPos - c.gapPosition) <= TOLERANCE;
1841
2193
  }
1842
2194
  var STR13 = {
1843
- zh: { title: "\u62D6\u52A8\u6ED1\u5757\u628A\u62FC\u56FE\u5757\u79FB\u5230\u7F3A\u53E3", success: "\u9A8C\u8BC1\u6210\u529F", refresh: "\u5237\u65B0" },
1844
- en: { title: "Drag the slider to fit the puzzle", success: "Verified", refresh: "Refresh" }
2195
+ zh: { title: "\u62D6\u52A8\u6ED1\u5757\u628A\u62FC\u56FE\u5757\u79FB\u5230\u7F3A\u53E3", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u672A\u5BF9\u9F50\u7F3A\u53E3", botFail: "\u68C0\u6D4B\u5230\u5F02\u5E38\u64CD\u4F5C", refresh: "\u5237\u65B0" },
2196
+ en: { title: "Drag the slider to fit the puzzle", success: "Verified", fail: "Not aligned", botFail: "Bot-like behavior", refresh: "Refresh" }
1845
2197
  };
1846
2198
  var STAGE_W = 300;
2199
+ var STAGE_H = 200;
1847
2200
  var PIECE_W = 50;
1848
2201
  var MAX_OFFSET = STAGE_W - PIECE_W;
2202
+ var TRACK_W = 300;
2203
+ var TRACK_H = 100;
2204
+ var HANDLE_W2 = 40;
2205
+ var HANDLE_H2 = 40;
2206
+ function zigzagPoints() {
2207
+ const pts = [];
2208
+ const margin = HANDLE_W2 / 2;
2209
+ const w = TRACK_W - margin * 2;
2210
+ const h = TRACK_H - margin * 2;
2211
+ const seg1Steps = 30;
2212
+ for (let i = 0; i <= seg1Steps; i++) {
2213
+ pts.push({ x: margin + w / seg1Steps * i, y: margin });
2214
+ }
2215
+ const seg2Steps = 10;
2216
+ const midY = margin + h / 4;
2217
+ for (let i = 1; i <= seg2Steps; i++) {
2218
+ pts.push({ x: TRACK_W - margin, y: margin + (midY - margin) / seg2Steps * i });
2219
+ }
2220
+ for (let i = 1; i <= seg1Steps; i++) {
2221
+ pts.push({ x: TRACK_W - margin - w / seg1Steps * i, y: midY });
2222
+ }
2223
+ const midY2 = margin + h * 3 / 4;
2224
+ for (let i = 1; i <= seg2Steps; i++) {
2225
+ pts.push({ x: margin, y: midY + (midY2 - midY) / seg2Steps * i });
2226
+ }
2227
+ for (let i = 1; i <= seg1Steps; i++) {
2228
+ pts.push({ x: margin + w / seg1Steps * i, y: midY2 });
2229
+ }
2230
+ for (let i = 1; i <= seg2Steps; i++) {
2231
+ pts.push({ x: TRACK_W - margin, y: midY2 + (TRACK_H - margin - midY2) / seg2Steps * i });
2232
+ }
2233
+ return pts;
2234
+ }
2235
+ function zigzagPath(pts) {
2236
+ return pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(" ");
2237
+ }
2238
+ function pointAtProgress2(pts, progress) {
2239
+ const idx = Math.min(pts.length - 1, Math.max(0, Math.round(progress * (pts.length - 1))));
2240
+ return pts[idx];
2241
+ }
2242
+ function pieceCirclePos(progress) {
2243
+ const cx = STAGE_W / 2;
2244
+ const cy = STAGE_H / 2;
2245
+ const radius = Math.min(STAGE_W, STAGE_H) / 2 - PIECE_W / 2 - 8;
2246
+ const angle = -progress * Math.PI * 2;
2247
+ return {
2248
+ x: cx + radius * Math.cos(angle) - PIECE_W / 2,
2249
+ y: cy + radius * Math.sin(angle) - PIECE_W / 2
2250
+ };
2251
+ }
1849
2252
  function createPuzzleInstance(container, config) {
1850
2253
  const t = STR13[config.locale];
2254
+ const theme = config.theme ?? "light";
2255
+ const recorder = new TrackRecorder();
1851
2256
  let current;
1852
2257
  let listeners = [];
1853
2258
  let startTime = Date.now();
1854
2259
  let done = false;
2260
+ let locked = false;
1855
2261
  function render2() {
1856
2262
  current = generateChallenge13();
1857
2263
  startTime = Date.now();
1858
2264
  done = false;
2265
+ locked = false;
2266
+ recorder.clear();
1859
2267
  const gapLeft = current.gapPosition / 100 * MAX_OFFSET;
2268
+ const pts = zigzagPoints();
2269
+ const pathD = zigzagPath(pts);
1860
2270
  container.innerHTML = `
1861
2271
  <style>
1862
2272
  .fc-puzzle{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
1863
2273
  .fc-puzzle[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
1864
2274
  .fc-puzzle[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
2275
+ .fc-puzzle{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
1865
2276
  .fc-puzzle-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
1866
- .fc-puzzle-stage{position:relative;width:${STAGE_W}px;height:160px;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))}
1867
- .fc-puzzle-gap{position:absolute;top:55px;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)}
1868
- .fc-puzzle-piece{position:absolute;top:55px;left:0;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)}
1869
- .fc-puzzle-range{width:${STAGE_W}px;margin-top:14px;accent-color:var(--fc-accent)}
2277
+ .fc-puzzle-stage{position:relative;width:${STAGE_W}px;height:${STAGE_H}px;margin:0 auto;border-radius:8px;overflow:hidden;border:1px solid var(--fc-border);background:linear-gradient(135deg,var(--fc-accent-soft),var(--fc-surface) 60%,var(--fc-accent-soft))}
2278
+ .fc-puzzle-gap{position:absolute;top:${(STAGE_H - PIECE_W) / 2}px;width:${PIECE_W}px;height:${PIECE_W}px;background:var(--fc-bg);border:2px dashed var(--fc-text-soft);border-radius:6px;box-shadow:inset 0 0 8px rgba(0,0,0,.15);transition:left .15s}
2279
+ .fc-puzzle-piece{position:absolute;width:${PIECE_W}px;height:${PIECE_W}px;border-radius:6px;background:linear-gradient(135deg,var(--fc-accent),var(--fc-accent-soft));border:2px solid var(--fc-bg);box-shadow:0 2px 6px rgba(0,0,0,.25);transition:background .15s,border-color .15s}
2280
+ .fc-puzzle-track{position:relative;width:${TRACK_W}px;height:${TRACK_H}px;margin:14px auto 0;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:8px;overflow:hidden}
2281
+ .fc-puzzle-svg{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
2282
+ .fc-puzzle-path-bg{fill:none;stroke:var(--fc-border);stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
2283
+ .fc-puzzle-path-fg{fill:none;stroke:var(--fc-accent);stroke-width:4;stroke-linecap:round;stroke-linejoin:round;transition:stroke .15s}
2284
+ .fc-puzzle-handle{position:absolute;width:${HANDLE_W2}px;height:${HANDLE_H2}px;background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.2);user-select:none;touch-action:none;transition:border-color .15s;transform:translate(-50%,-50%);z-index:2}
2285
+ .fc-puzzle-handle:active{cursor:grabbing}
2286
+ .fc-puzzle-handle::after{content:'\\2192';color:var(--fc-accent);font-size:16px;font-weight:bold;transition:color .15s}
1870
2287
  .fc-puzzle-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
1871
2288
  .fc-puzzle-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer;margin-top:8px;display:block;margin-left:auto;margin-right:auto}
1872
2289
  .fc-puzzle-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
2290
+ .fc-puzzle-done .fc-puzzle-piece{background:linear-gradient(135deg,var(--fc-success),var(--fc-accent-soft));border-color:var(--fc-success)}
2291
+ .fc-puzzle-done .fc-puzzle-handle{border-color:var(--fc-success)}
2292
+ .fc-puzzle-done .fc-puzzle-handle::after{color:var(--fc-success)}
2293
+ .fc-puzzle-done .fc-puzzle-path-fg{stroke:var(--fc-success)}
1873
2294
  </style>
1874
- <div class="fc-puzzle" data-theme="${config.theme}">
2295
+ <div class="fc-puzzle" data-theme="${theme}">
1875
2296
  <div class="fc-puzzle-title">${t.title}</div>
1876
2297
  <div class="fc-puzzle-stage">
1877
2298
  <div class="fc-puzzle-gap" style="left:${gapLeft}px"></div>
1878
- <div class="fc-puzzle-piece" style="left:0"></div>
2299
+ <div class="fc-puzzle-piece" style="left:0;top:${(STAGE_H - PIECE_W) / 2}px"></div>
2300
+ </div>
2301
+ <div class="fc-puzzle-track">
2302
+ <svg class="fc-puzzle-svg" viewBox="0 0 ${TRACK_W} ${TRACK_H}">
2303
+ <path class="fc-puzzle-path-bg" d="${pathD}"></path>
2304
+ <path class="fc-puzzle-path-fg" d="${pathD}"></path>
2305
+ </svg>
2306
+ <div class="fc-puzzle-handle" role="slider" tabindex="0"></div>
1879
2307
  </div>
1880
- <input class="fc-puzzle-range" type="range" min="0" max="100" step="1" value="0" />
1881
2308
  <div class="fc-puzzle-msg"></div>
1882
2309
  <button class="fc-puzzle-refresh">${t.refresh}</button>
1883
2310
  </div>
1884
2311
  `;
2312
+ const root = container.querySelector(".fc-puzzle");
1885
2313
  const piece = container.querySelector(".fc-puzzle-piece");
1886
- const range = container.querySelector(".fc-puzzle-range");
2314
+ const track = container.querySelector(".fc-puzzle-track");
2315
+ const handle = container.querySelector(".fc-puzzle-handle");
1887
2316
  const msg = container.querySelector(".fc-puzzle-msg");
1888
2317
  const refreshBtn = container.querySelector(".fc-puzzle-refresh");
2318
+ const fgPath = container.querySelector(".fc-puzzle-path-fg");
1889
2319
  refreshBtn.addEventListener("click", () => render2());
1890
- range.addEventListener("input", async () => {
1891
- if (done) return;
1892
- const v = Number(range.value);
1893
- piece.style.left = `${v / 100 * MAX_OFFSET}px`;
1894
- if (verifyPosition(current, v)) {
1895
- done = true;
1896
- range.disabled = true;
1897
- piece.style.left = `${gapLeft}px`;
1898
- const result = {
1899
- success: true,
1900
- proof: await hashProof(proofInput8(current)),
1901
- duration: Date.now() - startTime
1902
- };
1903
- msg.textContent = t.success;
1904
- config.onVerify?.(result);
1905
- listeners.forEach((cb) => cb(result));
2320
+ const totalLen = fgPath.getTotalLength();
2321
+ fgPath.style.strokeDasharray = `${totalLen}`;
2322
+ fgPath.style.strokeDashoffset = `${totalLen}`;
2323
+ let dragging = false;
2324
+ let startPointer = { x: 0, y: 0 };
2325
+ let startProgress = 0;
2326
+ let currentProgress = 0;
2327
+ function applyProgress(p) {
2328
+ currentProgress = Math.max(0, Math.min(1, p));
2329
+ const pt = pointAtProgress2(pts, currentProgress);
2330
+ handle.style.left = `${pt.x / TRACK_W * 100}%`;
2331
+ handle.style.top = `${pt.y / TRACK_H * 100}%`;
2332
+ fgPath.style.strokeDashoffset = `${totalLen * (1 - currentProgress)}`;
2333
+ const piecePos = pieceCirclePos(currentProgress);
2334
+ piece.style.left = `${piecePos.x}px`;
2335
+ piece.style.top = `${piecePos.y}px`;
2336
+ }
2337
+ function bounceBack() {
2338
+ handle.style.transition = "left .35s ease, top .35s ease, border-color .15s";
2339
+ piece.style.transition = "left .35s ease, top .35s ease, background .15s, border-color .15s";
2340
+ fgPath.style.transition = "stroke-dashoffset .35s ease, stroke .15s";
2341
+ applyProgress(0);
2342
+ setTimeout(() => {
2343
+ handle.style.transition = "border-color .15s";
2344
+ piece.style.transition = "background .15s, border-color .15s";
2345
+ fgPath.style.transition = "stroke .15s";
2346
+ }, 360);
2347
+ }
2348
+ applyProgress(0);
2349
+ handle.addEventListener("pointerdown", (e) => {
2350
+ if (done || locked) return;
2351
+ dragging = true;
2352
+ startPointer = { x: e.clientX, y: e.clientY };
2353
+ startProgress = currentProgress;
2354
+ recorder.start();
2355
+ try {
2356
+ handle.setPointerCapture(e.pointerId);
2357
+ } catch {
2358
+ }
2359
+ e.preventDefault();
2360
+ });
2361
+ handle.addEventListener("pointermove", (e) => {
2362
+ if (!dragging) return;
2363
+ const rect = track.getBoundingClientRect();
2364
+ const sx = rect.width / TRACK_W;
2365
+ const sy = rect.height / TRACK_H;
2366
+ const svgX = (e.clientX - rect.left) / sx;
2367
+ const svgY = (e.clientY - rect.top) / sy;
2368
+ let bestLen = 0;
2369
+ let bestDist = Infinity;
2370
+ const steps = 100;
2371
+ for (let i = 0; i <= steps; i++) {
2372
+ const len = i / steps * totalLen;
2373
+ const p = fgPath.getPointAtLength(len);
2374
+ const d = (p.x - svgX) * (p.x - svgX) + (p.y - svgY) * (p.y - svgY);
2375
+ if (d < bestDist) {
2376
+ bestDist = d;
2377
+ bestLen = len;
2378
+ }
1906
2379
  }
2380
+ applyProgress(bestLen / totalLen);
2381
+ recorder.record(e.clientX, e.clientY);
1907
2382
  });
2383
+ const finish = async () => {
2384
+ if (!dragging) return;
2385
+ dragging = false;
2386
+ const trackPoints = recorder.stop();
2387
+ const piecePos = pieceCirclePos(currentProgress);
2388
+ const pieceLeftPct = piecePos.x / STAGE_W * 100;
2389
+ if (!verifyPosition(current, pieceLeftPct)) {
2390
+ bounceBack();
2391
+ msg.style.color = "var(--fc-danger)";
2392
+ msg.textContent = t.fail;
2393
+ return;
2394
+ }
2395
+ const analysis = analyzeTrack(trackPoints);
2396
+ if (analysis.isBot) {
2397
+ locked = true;
2398
+ msg.style.color = "var(--fc-danger)";
2399
+ msg.textContent = config.locale === "zh" ? `\u7591\u4F3C\u673A\u5668\u4EBA\u64CD\u4F5C\uFF08${analysis.reasons.join("\u3001")}\uFF09` : `${t.botFail} detected`;
2400
+ bounceBack();
2401
+ setTimeout(() => {
2402
+ if (!done) render2();
2403
+ }, 2e3);
2404
+ return;
2405
+ }
2406
+ done = true;
2407
+ root.classList.add("fc-puzzle-done");
2408
+ const result = {
2409
+ success: true,
2410
+ proof: await hashProof(proofInput8(current)),
2411
+ duration: Date.now() - startTime,
2412
+ metadata: { humanScore: analysis.humanScore, reasons: analysis.reasons }
2413
+ };
2414
+ msg.style.color = "var(--fc-success)";
2415
+ msg.textContent = t.success;
2416
+ config.onVerify?.(result);
2417
+ listeners.forEach((cb) => cb(result));
2418
+ };
2419
+ handle.addEventListener("pointerup", finish);
2420
+ handle.addEventListener("pointercancel", finish);
1908
2421
  }
1909
2422
  return {
1910
2423
  mount: () => render2(),
@@ -1928,6 +2441,674 @@ var FunnyCaptcha = (() => {
1928
2441
  };
1929
2442
  defineCaptcha(puzzlePlugin);
1930
2443
 
2444
+ // ../captchas/advanced-math/dist/index.js
2445
+ function randInt9(min, max) {
2446
+ return Math.floor(Math.random() * (max - min + 1)) + min;
2447
+ }
2448
+ function pick2(arr) {
2449
+ return arr[randInt9(0, arr.length - 1)];
2450
+ }
2451
+ var SUP_DIGITS = {
2452
+ "0": "\u2070",
2453
+ "1": "\xB9",
2454
+ "2": "\xB2",
2455
+ "3": "\xB3",
2456
+ "4": "\u2074",
2457
+ "5": "\u2075",
2458
+ "6": "\u2076",
2459
+ "7": "\u2077",
2460
+ "8": "\u2078",
2461
+ "9": "\u2079"
2462
+ };
2463
+ var SUP_TO_DIGIT = {
2464
+ "\u2070": "0",
2465
+ "\xB9": "1",
2466
+ "\xB2": "2",
2467
+ "\xB3": "3",
2468
+ "\u2074": "4",
2469
+ "\u2075": "5",
2470
+ "\u2076": "6",
2471
+ "\u2077": "7",
2472
+ "\u2078": "8",
2473
+ "\u2079": "9"
2474
+ };
2475
+ function sup(n) {
2476
+ return String(n).split("").map((d) => SUP_DIGITS[d] ?? d).join("");
2477
+ }
2478
+ function normalizeAnswer(input) {
2479
+ let r = input.trim();
2480
+ r = r.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹]+/g, (m) => "^" + m.split("").map((c) => SUP_TO_DIGIT[c] ?? c).join(""));
2481
+ r = r.replace(/\s+/g, "");
2482
+ r = r.replace(/\+[Cc]$/, "");
2483
+ r = r.replace(/(\d)([a-zA-Z])/g, "$1*$2");
2484
+ return r;
2485
+ }
2486
+ function verifyAnswer6(challenge, userInput) {
2487
+ const u = normalizeAnswer(userInput);
2488
+ return challenge.accept.some((a) => normalizeAnswer(a) === u);
2489
+ }
2490
+ function genDerivative() {
2491
+ const n = pick2([2, 3, 4, 5]);
2492
+ const question = `\u6C42 f(x) = x${sup(n)} \u7684\u5BFC\u6570`;
2493
+ const exp = n - 1;
2494
+ const answer = `${n}x^${exp}`;
2495
+ const accept = [
2496
+ `${n}x^${exp}`,
2497
+ `${n}x${sup(exp)}`,
2498
+ `${n}*x^${exp}`,
2499
+ `${n}*x${sup(exp)}`
2500
+ ];
2501
+ return { type: "derivative", question, answer, accept };
2502
+ }
2503
+ function genIntegral() {
2504
+ const n = pick2([1, 2, 3]);
2505
+ const up = n + 1;
2506
+ const question = `\u6C42 \u222B x${sup(n)} dx`;
2507
+ const answer = `x^${up}/${up} + C`;
2508
+ const accept = [
2509
+ `x^${up}/${up} + C`,
2510
+ `x^${up}/${up}+C`,
2511
+ `x^${up}/${up}`,
2512
+ `x${sup(up)}/${up} + C`,
2513
+ `x${sup(up)}/${up}`,
2514
+ `x^${up}/(${up}) + C`,
2515
+ `x^${up}/(${up})`,
2516
+ `x${sup(up)}/(${up}) + C`,
2517
+ `x${sup(up)}/(${up})`
2518
+ ];
2519
+ return { type: "integral", question, answer, accept };
2520
+ }
2521
+ function genLimit() {
2522
+ const a = pick2([1, 2, 3]);
2523
+ const question = `\u6C42 lim(x\u2192${a}) (x${sup(2)}-${a}${sup(2)})/(x-${a})`;
2524
+ const answer = `${2 * a}`;
2525
+ const accept = [
2526
+ `${2 * a}`,
2527
+ `2*${a}`
2528
+ ];
2529
+ return { type: "limit", question, answer, accept };
2530
+ }
2531
+ function genDefinite() {
2532
+ const n = pick2([1, 2, 3]);
2533
+ const up = n + 1;
2534
+ const question = `\u8BA1\u7B97 \u222B\u2080\xB9 x${sup(n)} dx`;
2535
+ const answer = `1/${up}`;
2536
+ const accept = [
2537
+ `1/${up}`,
2538
+ `1/(${up})`
2539
+ ];
2540
+ return { type: "definite", question, answer, accept };
2541
+ }
2542
+ function generateChallenge14() {
2543
+ const gen = pick2([genDerivative, genIntegral, genLimit, genDefinite]);
2544
+ return gen();
2545
+ }
2546
+ var STR14 = {
2547
+ zh: {
2548
+ title: "\u9AD8\u7B49\u6570\u5B66",
2549
+ placeholder: "\u8F93\u5165\u7B54\u6848\uFF08\u652F\u6301 ^ \u548C *\uFF09",
2550
+ submit: "\u9A8C\u8BC1",
2551
+ fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898",
2552
+ refresh: "\u5237\u65B0",
2553
+ hint: "\u63D0\u793A\uFF1Ax\xB2\u53EF\u5199\u4E3Ax^2\uFF0C\u53EF\u7701\u7565*"
2554
+ },
2555
+ en: {
2556
+ title: "Calculus",
2557
+ placeholder: "Enter answer (^ and * supported)",
2558
+ submit: "Verify",
2559
+ fail: "Wrong, try again",
2560
+ refresh: "Refresh",
2561
+ hint: "Tip: x\xB2 = x^2, * optional"
2562
+ }
2563
+ };
2564
+ function createAdvancedMathInstance(container, config) {
2565
+ const t = STR14[config.locale];
2566
+ const theme = config.theme ?? "light";
2567
+ let current;
2568
+ let listeners = [];
2569
+ let startTime = Date.now();
2570
+ function render2() {
2571
+ current = generateChallenge14();
2572
+ startTime = Date.now();
2573
+ container.innerHTML = `
2574
+ <style>
2575
+ .fc-amath{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
2576
+ .fc-amath[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
2577
+ .fc-amath[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
2578
+ .fc-amath{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
2579
+ .fc-amath-q{display:block;font-size:14px;font-weight:600;color:var(--fc-text);margin-bottom:8px}
2580
+ .fc-amath-hint{display:block;font-size:11px;color:var(--fc-text-soft);margin-bottom:12px}
2581
+ .fc-amath-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
2582
+ .fc-amath-input{flex:1;min-width:0;padding:8px 12px;font-size:14px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text);border-radius:8px;box-sizing:border-box;outline:none}
2583
+ .fc-amath-input:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
2584
+ .fc-amath-btn{padding:8px 16px;font-size:14px;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
2585
+ .fc-amath-btn:hover{opacity:.9}
2586
+ .fc-amath-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
2587
+ .fc-amath-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
2588
+ .fc-amath-msg{font-size:13px;min-height:18px;margin-top:10px;color:var(--fc-danger)}
2589
+ </style>
2590
+ <div class="fc-amath" data-theme="${theme}">
2591
+ <label class="fc-amath-q">${t.title}: ${current.question}</label>
2592
+ <span class="fc-amath-hint">${t.hint}</span>
2593
+ <div class="fc-amath-row">
2594
+ <input class="fc-amath-input" type="text" autocomplete="off" placeholder="${t.placeholder}" />
2595
+ <button class="fc-amath-btn">${t.submit}</button>
2596
+ <button class="fc-amath-refresh">${t.refresh}</button>
2597
+ </div>
2598
+ <div class="fc-amath-msg"></div>
2599
+ </div>
2600
+ `;
2601
+ const btn = container.querySelector(".fc-amath-btn");
2602
+ const input = container.querySelector(".fc-amath-input");
2603
+ const msg = container.querySelector(".fc-amath-msg");
2604
+ const refreshBtn = container.querySelector(".fc-amath-refresh");
2605
+ refreshBtn.addEventListener("click", render2);
2606
+ btn.addEventListener("click", async () => {
2607
+ const val = input.value;
2608
+ const success = verifyAnswer6(current, val);
2609
+ if (!success) {
2610
+ msg.textContent = t.fail;
2611
+ render2();
2612
+ return;
2613
+ }
2614
+ const result = {
2615
+ success: true,
2616
+ proof: await hashProof(`${current.question}=${current.answer}`),
2617
+ duration: Date.now() - startTime
2618
+ };
2619
+ config.onVerify?.(result);
2620
+ listeners.forEach((cb) => cb(result));
2621
+ });
2622
+ input.addEventListener("keydown", (e) => {
2623
+ if (e.key === "Enter") btn.click();
2624
+ });
2625
+ }
2626
+ return {
2627
+ mount: () => render2(),
2628
+ reset: () => render2(),
2629
+ destroy: () => {
2630
+ container.innerHTML = "";
2631
+ listeners = [];
2632
+ },
2633
+ onResult: (cb) => listeners.push(cb)
2634
+ };
2635
+ }
2636
+ defineCaptcha({
2637
+ id: "advanced-math",
2638
+ category: "recognize",
2639
+ create: createAdvancedMathInstance,
2640
+ describe: (locale) => ({
2641
+ name: locale === "zh" ? "\u9AD8\u7B49\u6570\u5B66" : "Calculus",
2642
+ description: locale === "zh" ? "\u6C42\u5BFC\u3001\u79EF\u5206\u3001\u6781\u9650\u7B49\u9AD8\u7B49\u6570\u5B66\u9898\uFF0C\u786C\u6838\u9632\u673A\u5668\u4EBA" : "Derivatives, integrals and limits \u2014 hardcore anti-bot.",
2643
+ tags: ["math", "calculus", "hard", "recognize"]
2644
+ })
2645
+ });
2646
+
2647
+ // ../captchas/olympiad/dist/index.js
2648
+ function randInt10(min, max) {
2649
+ return Math.floor(Math.random() * (max - min + 1)) + min;
2650
+ }
2651
+ function pick3(arr) {
2652
+ return arr[randInt10(0, arr.length - 1)];
2653
+ }
2654
+ function genChickenRabbit() {
2655
+ const c = randInt10(2, 15);
2656
+ const r = randInt10(2, 10);
2657
+ const heads = c + r;
2658
+ const legs = 2 * c + 4 * r;
2659
+ return {
2660
+ type: "chicken-rabbit",
2661
+ question: `\u9E21\u5154\u540C\u7B3C\uFF0C\u5171${heads}\u4E2A\u5934\uFF0C${legs}\u53EA\u811A\uFF0C\u9E21\u6709\u51E0\u53EA\uFF1F`,
2662
+ answer: c
2663
+ };
2664
+ }
2665
+ function genTreePlanting() {
2666
+ const gap = randInt10(2, 10);
2667
+ const minK = Math.ceil(20 / gap);
2668
+ const maxK = Math.floor(100 / gap);
2669
+ const k = randInt10(minK, maxK);
2670
+ const len = gap * k;
2671
+ return {
2672
+ type: "tree-planting",
2673
+ question: `\u4E00\u6761\u8DEF\u957F${len}\u7C73\uFF0C\u6BCF\u9694${gap}\u7C73\u79CD\u4E00\u68F5\u6811\uFF0C\u4E24\u7AEF\u90FD\u79CD\uFF0C\u5171\u51E0\u68F5\uFF1F`,
2674
+ answer: len / gap + 1
2675
+ };
2676
+ }
2677
+ function genAge() {
2678
+ for (let attempt = 0; attempt < 200; attempt++) {
2679
+ const f = randInt10(35, 50);
2680
+ const s = randInt10(8, 15);
2681
+ if (f <= s) continue;
2682
+ const num = 5 * s - f;
2683
+ if (num <= 0 || num % 4 !== 0) continue;
2684
+ const x = num / 4;
2685
+ if (x <= 0 || x >= s) continue;
2686
+ return {
2687
+ type: "age",
2688
+ question: `\u7238\u7238${f}\u5C81\uFF0C\u513F\u5B50${s}\u5C81\uFF0C\u51E0\u5E74\u524D\u7238\u7238\u5E74\u9F84\u662F\u513F\u5B50\u76845\u500D\uFF1F`,
2689
+ answer: x
2690
+ };
2691
+ }
2692
+ return {
2693
+ type: "age",
2694
+ question: "\u7238\u723842\u5C81\uFF0C\u513F\u5B5010\u5C81\uFF0C\u51E0\u5E74\u524D\u7238\u7238\u5E74\u9F84\u662F\u513F\u5B50\u76845\u500D\uFF1F",
2695
+ answer: 2
2696
+ };
2697
+ }
2698
+ function genApple() {
2699
+ const p = randInt10(3, 12);
2700
+ const a = randInt10(3, 6);
2701
+ const b = a + 1;
2702
+ const m = randInt10(1, p - 1);
2703
+ const n = p - m;
2704
+ return {
2705
+ type: "apple",
2706
+ question: `\u5206\u82F9\u679C\uFF0C\u6BCF\u4EBA${a}\u4E2A\u591A${m}\u4E2A\uFF0C\u6BCF\u4EBA${b}\u4E2A\u5C11${n}\u4E2A\uFF0C\u51E0\u4EBA\uFF1F`,
2707
+ answer: p
2708
+ };
2709
+ }
2710
+ function genTravel() {
2711
+ const v1 = randInt10(3, 12);
2712
+ const v2 = randInt10(3, 12);
2713
+ const t = randInt10(2, 8);
2714
+ const d = (v1 + v2) * t;
2715
+ return {
2716
+ type: "travel",
2717
+ question: `\u7532\u4E59\u76F8\u5411\u800C\u884C\uFF0C\u7532\u901F${v1}km/h\uFF0C\u4E59\u901F${v2}km/h\uFF0C\u76F8\u8DDD${d}km\uFF0C\u51E0\u5C0F\u65F6\u540E\u76F8\u9047\uFF1F`,
2718
+ answer: t
2719
+ };
2720
+ }
2721
+ function generateChallenge15() {
2722
+ const gen = pick3([
2723
+ genChickenRabbit,
2724
+ genTreePlanting,
2725
+ genAge,
2726
+ genApple,
2727
+ genTravel
2728
+ ]);
2729
+ return gen();
2730
+ }
2731
+ function verifyAnswer7(c, userAnswer) {
2732
+ return c.answer === userAnswer;
2733
+ }
2734
+ var STR15 = {
2735
+ zh: {
2736
+ title: "\u5C0F\u5B66\u5965\u6570",
2737
+ placeholder: "\u8F93\u5165\u6570\u5B57\u7B54\u6848",
2738
+ submit: "\u9A8C\u8BC1",
2739
+ fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898",
2740
+ refresh: "\u5237\u65B0"
2741
+ },
2742
+ en: {
2743
+ title: "Math Olympiad",
2744
+ placeholder: "Enter number",
2745
+ submit: "Verify",
2746
+ fail: "Wrong, try again",
2747
+ refresh: "Refresh"
2748
+ }
2749
+ };
2750
+ function createOlympiadInstance(container, config) {
2751
+ const t = STR15[config.locale];
2752
+ const theme = config.theme ?? "light";
2753
+ let current;
2754
+ let listeners = [];
2755
+ let startTime = Date.now();
2756
+ function render2() {
2757
+ current = generateChallenge15();
2758
+ startTime = Date.now();
2759
+ container.innerHTML = `
2760
+ <style>
2761
+ .fc-olym{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
2762
+ .fc-olym[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
2763
+ .fc-olym[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
2764
+ .fc-olym{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
2765
+ .fc-olym-q{display:block;font-size:14px;font-weight:600;color:var(--fc-text);margin-bottom:12px;line-height:1.5}
2766
+ .fc-olym-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
2767
+ .fc-olym-input{flex:1;min-width:0;padding:8px 12px;font-size:14px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text);border-radius:8px;box-sizing:border-box;outline:none}
2768
+ .fc-olym-input:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
2769
+ .fc-olym-btn{padding:8px 16px;font-size:14px;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
2770
+ .fc-olym-btn:hover{opacity:.9}
2771
+ .fc-olym-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
2772
+ .fc-olym-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
2773
+ .fc-olym-msg{font-size:13px;min-height:18px;margin-top:10px;color:var(--fc-danger)}
2774
+ </style>
2775
+ <div class="fc-olym" data-theme="${theme}">
2776
+ <label class="fc-olym-q">${t.title}: ${current.question}</label>
2777
+ <div class="fc-olym-row">
2778
+ <input class="fc-olym-input" type="text" inputmode="numeric" autocomplete="off" placeholder="${t.placeholder}" />
2779
+ <button class="fc-olym-btn">${t.submit}</button>
2780
+ <button class="fc-olym-refresh">${t.refresh}</button>
2781
+ </div>
2782
+ <div class="fc-olym-msg"></div>
2783
+ </div>
2784
+ `;
2785
+ const btn = container.querySelector(".fc-olym-btn");
2786
+ const input = container.querySelector(".fc-olym-input");
2787
+ const msg = container.querySelector(".fc-olym-msg");
2788
+ const refreshBtn = container.querySelector(".fc-olym-refresh");
2789
+ refreshBtn.addEventListener("click", render2);
2790
+ btn.addEventListener("click", async () => {
2791
+ const val = Number(input.value);
2792
+ const success = Number.isFinite(val) && verifyAnswer7(current, val);
2793
+ if (!success) {
2794
+ msg.textContent = t.fail;
2795
+ render2();
2796
+ return;
2797
+ }
2798
+ const result = {
2799
+ success: true,
2800
+ proof: await hashProof(`${current.question}=${current.answer}`),
2801
+ duration: Date.now() - startTime
2802
+ };
2803
+ config.onVerify?.(result);
2804
+ listeners.forEach((cb) => cb(result));
2805
+ });
2806
+ input.addEventListener("keydown", (e) => {
2807
+ if (e.key === "Enter") btn.click();
2808
+ });
2809
+ }
2810
+ return {
2811
+ mount: () => render2(),
2812
+ reset: () => render2(),
2813
+ destroy: () => {
2814
+ container.innerHTML = "";
2815
+ listeners = [];
2816
+ },
2817
+ onResult: (cb) => listeners.push(cb)
2818
+ };
2819
+ }
2820
+ defineCaptcha({
2821
+ id: "olympiad",
2822
+ category: "recognize",
2823
+ create: createOlympiadInstance,
2824
+ describe: (locale) => ({
2825
+ name: locale === "zh" ? "\u5C0F\u5B66\u5965\u6570" : "Math Olympiad",
2826
+ description: locale === "zh" ? "\u9E21\u5154\u540C\u7B3C\u3001\u690D\u6811\u3001\u76C8\u4E8F\u7B49\u7ECF\u5178\u5965\u6570\u9898\uFF0C\u6321\u4F4F\u673A\u5668\u4E5F\u6321\u4F4F\u5C0F\u5B66\u751F" : "Classic olympiad word problems \u2014 chickens, trees, apples.",
2827
+ tags: ["math", "olympiad", "word-problem", "recognize"]
2828
+ })
2829
+ });
2830
+
2831
+ // ../captchas/equation-balance/dist/index.js
2832
+ var EQUATIONS = [
2833
+ { reactants: ["H\u2082", "O\u2082"], products: ["H\u2082O"], coefficients: [2, 1, 2] },
2834
+ { reactants: ["Fe", "O\u2082"], products: ["Fe\u2082O\u2083"], coefficients: [4, 3, 2] },
2835
+ { reactants: ["CH\u2084", "O\u2082"], products: ["CO\u2082", "H\u2082O"], coefficients: [1, 2, 1, 2] },
2836
+ { reactants: ["C\u2082H\u2086", "O\u2082"], products: ["CO\u2082", "H\u2082O"], coefficients: [2, 7, 4, 6] },
2837
+ { reactants: ["Al", "HCl"], products: ["AlCl\u2083", "H\u2082"], coefficients: [2, 6, 2, 3] },
2838
+ { reactants: ["N\u2082", "H\u2082"], products: ["NH\u2083"], coefficients: [1, 3, 2] }
2839
+ ];
2840
+ function pick4(arr) {
2841
+ return arr[Math.floor(Math.random() * arr.length)];
2842
+ }
2843
+ function generateChallenge16() {
2844
+ const eq = pick4(EQUATIONS);
2845
+ return {
2846
+ reactants: [...eq.reactants],
2847
+ products: [...eq.products],
2848
+ coefficients: [...eq.coefficients]
2849
+ };
2850
+ }
2851
+ function verifyAnswer8(c, userCoefficients) {
2852
+ if (userCoefficients.length !== c.coefficients.length) return false;
2853
+ return c.coefficients.every((coef, i) => coef === userCoefficients[i]);
2854
+ }
2855
+ var STR16 = {
2856
+ zh: { title: "\u914D\u5E73\u5316\u5B66\u65B9\u7A0B\u5F0F", submit: "\u9A8C\u8BC1", fail: "\u7CFB\u6570\u4E0D\u5BF9\uFF0C\u518D\u8BD5\u8BD5", refresh: "\u6362\u4E00\u9898", hint: "\u7CFB\u6570\u4E3A1\u4E5F\u8981\u586B" },
2857
+ en: { title: "Balance the equation", submit: "Verify", fail: "Wrong coefficients", refresh: "Next", hint: "Enter 1 if needed" }
2858
+ };
2859
+ function createEquationBalanceInstance(container, config) {
2860
+ const t = STR16[config.locale];
2861
+ const theme = config.theme ?? "light";
2862
+ let current;
2863
+ let listeners = [];
2864
+ let startTime = Date.now();
2865
+ function render2() {
2866
+ current = generateChallenge16();
2867
+ startTime = Date.now();
2868
+ const items = [...current.reactants, ...current.products];
2869
+ const R = current.reactants.length;
2870
+ const eqHtml = items.map((formula, i) => {
2871
+ const sep = i === 0 ? "" : i === R ? "\u2192" : "+";
2872
+ const sepHtml = sep ? `<span class="fc-eqbal-sep">${sep}</span>` : "";
2873
+ return `${sepHtml}<span class="fc-eqbal-term"><input class="fc-eqbal-coef" type="text" inputmode="numeric" maxlength="2" aria-label="${formula}" /><span class="fc-eqbal-formula">${formula}</span></span>`;
2874
+ }).join("");
2875
+ container.innerHTML = `
2876
+ <style>
2877
+ .fc-eqbal{font-family:-apple-system,system-ui,sans-serif;max-width:420px;width:100%;padding:16px;box-sizing:border-box}
2878
+ .fc-eqbal[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
2879
+ .fc-eqbal[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
2880
+ .fc-eqbal{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
2881
+ .fc-eqbal-title{display:block;font-size:14px;font-weight:600;color:var(--fc-text);margin-bottom:4px}
2882
+ .fc-eqbal-hint{display:block;font-size:12px;color:var(--fc-text-soft);margin-bottom:12px}
2883
+ .fc-eqbal-eq{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:18px;color:var(--fc-text);margin-bottom:14px;justify-content:center}
2884
+ .fc-eqbal-term{display:inline-flex;align-items:center;gap:4px}
2885
+ .fc-eqbal-sep{color:var(--fc-text-soft);font-weight:600}
2886
+ .fc-eqbal-formula{font-family:'SF Mono',ui-monospace,monospace}
2887
+ .fc-eqbal-coef{width:40px;text-align:center;padding:4px 0;font-size:16px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text);border-radius:6px;outline:none;box-sizing:border-box}
2888
+ .fc-eqbal-coef:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
2889
+ .fc-eqbal-row{display:flex;gap:8px;align-items:center;justify-content:center}
2890
+ .fc-eqbal-btn{padding:8px 16px;font-size:14px;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
2891
+ .fc-eqbal-btn:hover{opacity:.9}
2892
+ .fc-eqbal-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
2893
+ .fc-eqbal-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
2894
+ .fc-eqbal-msg{font-size:13px;min-height:18px;margin-top:10px;text-align:center;color:var(--fc-danger)}
2895
+ </style>
2896
+ <div class="fc-eqbal" data-theme="${theme}">
2897
+ <label class="fc-eqbal-title">${t.title}</label>
2898
+ <span class="fc-eqbal-hint">${t.hint}</span>
2899
+ <div class="fc-eqbal-eq">${eqHtml}</div>
2900
+ <div class="fc-eqbal-row">
2901
+ <button class="fc-eqbal-btn">${t.submit}</button>
2902
+ <button class="fc-eqbal-refresh">${t.refresh}</button>
2903
+ </div>
2904
+ <div class="fc-eqbal-msg"></div>
2905
+ </div>
2906
+ `;
2907
+ const inputs = Array.from(container.querySelectorAll(".fc-eqbal-coef"));
2908
+ const btn = container.querySelector(".fc-eqbal-btn");
2909
+ const msg = container.querySelector(".fc-eqbal-msg");
2910
+ const refreshBtn = container.querySelector(".fc-eqbal-refresh");
2911
+ refreshBtn.addEventListener("click", render2);
2912
+ btn.addEventListener("click", async () => {
2913
+ const vals = inputs.map((i) => Number(i.value));
2914
+ if (!verifyAnswer8(current, vals)) {
2915
+ msg.textContent = t.fail;
2916
+ return;
2917
+ }
2918
+ const result = {
2919
+ success: true,
2920
+ proof: await hashProof(`equation-balance:${current.reactants.join("+")}>${current.products.join("+")}:${current.coefficients.join(",")}`),
2921
+ duration: Date.now() - startTime
2922
+ };
2923
+ msg.textContent = "";
2924
+ config.onVerify?.(result);
2925
+ listeners.forEach((cb) => cb(result));
2926
+ });
2927
+ inputs.forEach((input, idx) => {
2928
+ input.addEventListener("keydown", (e) => {
2929
+ if (e.key === "Enter") btn.click();
2930
+ });
2931
+ if (idx === 0) input.focus();
2932
+ });
2933
+ }
2934
+ return {
2935
+ mount: () => render2(),
2936
+ reset: () => render2(),
2937
+ destroy: () => {
2938
+ container.innerHTML = "";
2939
+ listeners = [];
2940
+ },
2941
+ onResult: (cb) => listeners.push(cb)
2942
+ };
2943
+ }
2944
+ var equationBalancePlugin = {
2945
+ id: "equation-balance",
2946
+ category: "recognize",
2947
+ create: createEquationBalanceInstance,
2948
+ describe: (locale) => ({
2949
+ name: locale === "zh" ? "\u914D\u5E73\u5316\u5B66\u65B9\u7A0B\u5F0F" : "Balance Equation",
2950
+ description: locale === "zh" ? "\u586B\u5165\u6B63\u786E\u7684\u5316\u5B66\u8BA1\u91CF\u6570\uFF0C\u914D\u5E73\u65B9\u7A0B\u5F0F" : "Enter the right stoichiometric coefficients.",
2951
+ tags: ["chemistry", "equation", "balance"]
2952
+ })
2953
+ };
2954
+ defineCaptcha(equationBalancePlugin);
2955
+
2956
+ // ../captchas/random-hunt/dist/index.js
2957
+ function isPrime(n) {
2958
+ if (n < 2) return false;
2959
+ for (let i = 2; i * i <= n; i++) {
2960
+ if (n % i === 0) return false;
2961
+ }
2962
+ return true;
2963
+ }
2964
+ var CONDITIONS = [
2965
+ { key: "gt50", zh: "\u5927\u4E8E 50", en: "greater than 50", check: (n) => n > 50 },
2966
+ { key: "lt20", zh: "\u5C0F\u4E8E 20", en: "less than 20", check: (n) => n < 20 },
2967
+ { key: "even", zh: "\u5076\u6570", en: "even number", check: (n) => n % 2 === 0 },
2968
+ { key: "odd", zh: "\u5947\u6570", en: "odd number", check: (n) => n % 2 === 1 },
2969
+ { key: "mult3", zh: "3 \u7684\u500D\u6570", en: "multiple of 3", check: (n) => n % 3 === 0 },
2970
+ { key: "gt50-even", zh: "\u5927\u4E8E 50 \u7684\u5076\u6570", en: "even and > 50", check: (n) => n > 50 && n % 2 === 0 },
2971
+ { key: "lt30-odd", zh: "\u5C0F\u4E8E 30 \u7684\u5947\u6570", en: "odd and < 30", check: (n) => n < 30 && n % 2 === 1 },
2972
+ { key: "prime", zh: "\u8D28\u6570", en: "prime number", check: (n) => isPrime(n) }
2973
+ ];
2974
+ function pick5(arr) {
2975
+ return arr[Math.floor(Math.random() * arr.length)];
2976
+ }
2977
+ function generateChallenge17() {
2978
+ const c = pick5(CONDITIONS);
2979
+ return { condition: c.key, check: c.check };
2980
+ }
2981
+ function conditionLabel(condition, locale) {
2982
+ const def = CONDITIONS.find((c) => c.key === condition);
2983
+ if (!def) return condition;
2984
+ return locale === "zh" ? def.zh : def.en;
2985
+ }
2986
+ var STR17 = {
2987
+ zh: { title: "\u968F\u673A\u6570\u730E\u624B", hunt: "\u6355\u83B7", success: "\u6355\u83B7\u6210\u529F\uFF01", fail: "\u4E0D\u7B26\u5408\u6761\u4EF6\uFF0C\u7EE7\u7EED", refresh: "\u6362\u6761\u4EF6", tip: "\u5F53\u6570\u5B57\u6EE1\u8DB3\u6761\u4EF6\u65F6\u70B9\u51FB\u6355\u83B7" },
2988
+ en: { title: "Random Hunter", hunt: "Catch!", success: "Caught it!", fail: "Not matching, keep going", refresh: "New rule", tip: "Click when the number matches" }
2989
+ };
2990
+ var TICK_MS = 120;
2991
+ var MIN = 0;
2992
+ var MAX = 99;
2993
+ function randInt11(min, max) {
2994
+ return Math.floor(Math.random() * (max - min + 1)) + min;
2995
+ }
2996
+ function createRandomHuntInstance(container, config) {
2997
+ const t = STR17[config.locale];
2998
+ const locale = config.locale;
2999
+ const theme = config.theme ?? "light";
3000
+ let current;
3001
+ let listeners = [];
3002
+ let startTime = Date.now();
3003
+ let timer = null;
3004
+ let number = 0;
3005
+ let succeeded = false;
3006
+ function clearTimer() {
3007
+ if (timer) {
3008
+ clearInterval(timer);
3009
+ timer = null;
3010
+ }
3011
+ }
3012
+ function render2() {
3013
+ current = generateChallenge17();
3014
+ startTime = Date.now();
3015
+ succeeded = false;
3016
+ clearTimer();
3017
+ number = randInt11(MIN, MAX);
3018
+ container.innerHTML = `
3019
+ <style>
3020
+ .fc-hunt{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
3021
+ .fc-hunt[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
3022
+ .fc-hunt[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
3023
+ .fc-hunt{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
3024
+ .fc-hunt-title{font-size:15px;font-weight:600;color:var(--fc-text);text-align:center;margin-bottom:4px}
3025
+ .fc-hunt-cond{font-size:13px;color:var(--fc-accent);text-align:center;margin-bottom:4px;font-weight:600}
3026
+ .fc-hunt-tip{font-size:12px;color:var(--fc-text-soft);text-align:center;margin-bottom:12px}
3027
+ .fc-hunt-num{font-family:'SF Mono',ui-monospace,monospace;font-size:48px;text-align:center;color:var(--fc-text);min-height:56px;line-height:56px;transition:opacity .08s ease-out,color .15s ease-out;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:10px;margin-bottom:14px}
3028
+ .fc-hunt-num-success{color:var(--fc-success);border-color:var(--fc-success)}
3029
+ .fc-hunt-row{display:flex;gap:8px;align-items:center;justify-content:center}
3030
+ .fc-hunt-btn{flex:1;padding:12px 16px;font-size:16px;font-weight:600;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
3031
+ .fc-hunt-btn:hover{opacity:.9}
3032
+ .fc-hunt-btn:disabled{opacity:.5;cursor:not-allowed}
3033
+ .fc-hunt-refresh{padding:8px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
3034
+ .fc-hunt-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
3035
+ .fc-hunt-msg{font-size:13px;min-height:18px;margin-top:10px;text-align:center;color:var(--fc-danger)}
3036
+ </style>
3037
+ <div class="fc-hunt" data-theme="${theme}">
3038
+ <div class="fc-hunt-title">${t.title}</div>
3039
+ <div class="fc-hunt-cond">${t.hunt}\uFF1A${conditionLabel(current.condition, locale)}</div>
3040
+ <div class="fc-hunt-tip">${t.tip}</div>
3041
+ <div class="fc-hunt-num">${number}</div>
3042
+ <div class="fc-hunt-row">
3043
+ <button class="fc-hunt-btn">${t.hunt}</button>
3044
+ <button class="fc-hunt-refresh">${t.refresh}</button>
3045
+ </div>
3046
+ <div class="fc-hunt-msg"></div>
3047
+ </div>
3048
+ `;
3049
+ const numEl = container.querySelector(".fc-hunt-num");
3050
+ const btn = container.querySelector(".fc-hunt-btn");
3051
+ const msg = container.querySelector(".fc-hunt-msg");
3052
+ const refreshBtn = container.querySelector(".fc-hunt-refresh");
3053
+ refreshBtn.addEventListener("click", render2);
3054
+ timer = setInterval(() => {
3055
+ if (succeeded) return;
3056
+ number = randInt11(MIN, MAX);
3057
+ if (numEl) {
3058
+ numEl.textContent = String(number);
3059
+ numEl.style.opacity = "0.55";
3060
+ requestAnimationFrame(() => {
3061
+ numEl.style.opacity = "1";
3062
+ });
3063
+ }
3064
+ }, TICK_MS);
3065
+ btn.addEventListener("click", async () => {
3066
+ if (succeeded) return;
3067
+ const ok = current.check(number);
3068
+ if (!ok) {
3069
+ msg.textContent = t.fail;
3070
+ return;
3071
+ }
3072
+ succeeded = true;
3073
+ clearTimer();
3074
+ if (numEl) numEl.classList.add("fc-hunt-num-success");
3075
+ btn.disabled = true;
3076
+ const duration = Date.now() - startTime;
3077
+ const result = {
3078
+ success: true,
3079
+ proof: await hashProof(`random-hunt:${current.condition}:${duration}`),
3080
+ duration,
3081
+ metadata: { condition: current.condition, number }
3082
+ };
3083
+ msg.textContent = t.success;
3084
+ msg.style.color = "var(--fc-success)";
3085
+ config.onVerify?.(result);
3086
+ listeners.forEach((cb) => cb(result));
3087
+ });
3088
+ }
3089
+ return {
3090
+ mount: () => render2(),
3091
+ reset: () => render2(),
3092
+ destroy: () => {
3093
+ clearTimer();
3094
+ container.innerHTML = "";
3095
+ listeners = [];
3096
+ },
3097
+ onResult: (cb) => listeners.push(cb)
3098
+ };
3099
+ }
3100
+ var randomHuntPlugin = {
3101
+ id: "random-hunt",
3102
+ category: "game",
3103
+ create: createRandomHuntInstance,
3104
+ describe: (locale) => ({
3105
+ name: locale === "zh" ? "\u968F\u673A\u6570\u730E\u624B" : "Random Hunter",
3106
+ description: locale === "zh" ? "\u6570\u5B57\u98DE\u901F\u53D8\u5316\uFF0C\u5728\u6EE1\u8DB3\u6761\u4EF6\u7684\u77AC\u95F4\u6309\u4E0B\u6355\u83B7" : "Numbers flash fast \u2014 catch the moment it matches the rule.",
3107
+ tags: ["game", "reaction", "timed"]
3108
+ })
3109
+ };
3110
+ defineCaptcha(randomHuntPlugin);
3111
+
1931
3112
  // src/index.ts
1932
3113
  var instances = /* @__PURE__ */ new Map();
1933
3114
  function render(root = document) {