@funnycaptcha/slider 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +126 -51
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { defineCaptcha } from "@funnycaptcha/core";
|
|
3
3
|
|
|
4
4
|
// src/render.ts
|
|
5
|
-
import { hashProof as hashProof2 } from "@funnycaptcha/core";
|
|
5
|
+
import { hashProof as hashProof2, TrackRecorder, analyzeTrack } from "@funnycaptcha/core";
|
|
6
6
|
|
|
7
7
|
// src/challenge.ts
|
|
8
8
|
import { hashProof } from "@funnycaptcha/core";
|
|
@@ -20,20 +20,56 @@ async function verifyCompleted(c, proof) {
|
|
|
20
20
|
|
|
21
21
|
// src/render.ts
|
|
22
22
|
var STR = {
|
|
23
|
-
zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5230\
|
|
24
|
-
en: { title: "Drag the
|
|
23
|
+
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" },
|
|
24
|
+
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" }
|
|
25
25
|
};
|
|
26
|
+
var CANVAS_W = 320;
|
|
27
|
+
var CANVAS_H = 200;
|
|
28
|
+
var HANDLE_W = 36;
|
|
29
|
+
var HANDLE_H = 36;
|
|
30
|
+
var SPIRAL_THETA_MAX = 6 * Math.PI;
|
|
31
|
+
var SPIRAL_A = 0;
|
|
32
|
+
var SPIRAL_B = 5;
|
|
33
|
+
var CX = CANVAS_W / 2;
|
|
34
|
+
var CY = CANVAS_H / 2;
|
|
35
|
+
var SPIRAL_R_MAX = SPIRAL_B * SPIRAL_THETA_MAX;
|
|
36
|
+
function spiralPoints() {
|
|
37
|
+
const steps = 200;
|
|
38
|
+
const pts = [];
|
|
39
|
+
for (let i = 0; i <= steps; i++) {
|
|
40
|
+
const t = i / steps;
|
|
41
|
+
const theta = SPIRAL_THETA_MAX * (1 - t);
|
|
42
|
+
const r = SPIRAL_A + SPIRAL_B * theta;
|
|
43
|
+
const x = CX + r * Math.cos(theta);
|
|
44
|
+
const y = CY + r * Math.sin(theta);
|
|
45
|
+
pts.push({ x, y, theta });
|
|
46
|
+
}
|
|
47
|
+
return pts;
|
|
48
|
+
}
|
|
49
|
+
function spiralPath(pts) {
|
|
50
|
+
return pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(" ");
|
|
51
|
+
}
|
|
52
|
+
function pointAtProgress(pts, progress) {
|
|
53
|
+
const idx = Math.min(pts.length - 1, Math.max(0, Math.round(progress * (pts.length - 1))));
|
|
54
|
+
return pts[idx];
|
|
55
|
+
}
|
|
26
56
|
function createSliderInstance(container, config) {
|
|
27
57
|
const t = STR[config.locale];
|
|
28
58
|
const theme = config.theme ?? "light";
|
|
59
|
+
const recorder = new TrackRecorder();
|
|
29
60
|
let current;
|
|
30
61
|
let listeners = [];
|
|
31
62
|
let startTime = Date.now();
|
|
32
63
|
let done = false;
|
|
64
|
+
let locked = false;
|
|
33
65
|
function render() {
|
|
34
66
|
current = generateChallenge();
|
|
35
67
|
startTime = Date.now();
|
|
36
68
|
done = false;
|
|
69
|
+
locked = false;
|
|
70
|
+
recorder.clear();
|
|
71
|
+
const pts = spiralPoints();
|
|
72
|
+
const pathD = spiralPath(pts);
|
|
37
73
|
container.innerHTML = `
|
|
38
74
|
<style>
|
|
39
75
|
.fc-slider{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
@@ -41,24 +77,31 @@ function createSliderInstance(container, config) {
|
|
|
41
77
|
.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}
|
|
42
78
|
.fc-slider{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
43
79
|
.fc-slider-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
|
|
44
|
-
.fc-slider-
|
|
45
|
-
.fc-slider-
|
|
46
|
-
.fc-slider-
|
|
80
|
+
.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}
|
|
81
|
+
.fc-slider-svg{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
|
|
82
|
+
.fc-slider-path-bg{fill:none;stroke:var(--fc-border);stroke-width:3;stroke-linecap:round}
|
|
83
|
+
.fc-slider-path-fg{fill:none;stroke:var(--fc-accent);stroke-width:4;stroke-linecap:round;transition:stroke .15s}
|
|
84
|
+
.fc-slider-target{fill:none;stroke:var(--fc-success);stroke-width:2;stroke-dasharray:4 3;opacity:.7}
|
|
85
|
+
.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}
|
|
47
86
|
.fc-slider-handle:active{cursor:grabbing}
|
|
48
|
-
.fc-slider-handle::after{content:'
|
|
49
|
-
.fc-slider-tip{position:absolute;left:50%;top:
|
|
87
|
+
.fc-slider-handle::after{content:'';width:10px;height:10px;background:var(--fc-accent);border-radius:50%;transition:background .15s}
|
|
88
|
+
.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}
|
|
50
89
|
.fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
|
|
51
90
|
.fc-slider-row{display:flex;justify-content:center;margin-top:10px}
|
|
52
91
|
.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}
|
|
53
92
|
.fc-slider-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
54
93
|
.fc-slider-done .fc-slider-handle{border-color:var(--fc-success)}
|
|
55
|
-
.fc-slider-done .fc-slider-handle::after{
|
|
56
|
-
.fc-slider-done .fc-slider-
|
|
94
|
+
.fc-slider-done .fc-slider-handle::after{background:var(--fc-success)}
|
|
95
|
+
.fc-slider-done .fc-slider-path-fg{stroke:var(--fc-success)}
|
|
57
96
|
</style>
|
|
58
97
|
<div class="fc-slider" data-theme="${theme}">
|
|
59
98
|
<div class="fc-slider-title">${t.title}</div>
|
|
60
|
-
<div class="fc-slider-
|
|
61
|
-
<
|
|
99
|
+
<div class="fc-slider-stage">
|
|
100
|
+
<svg class="fc-slider-svg" viewBox="0 0 ${CANVAS_W} ${CANVAS_H}">
|
|
101
|
+
<path class="fc-slider-path-bg" d="${pathD}"></path>
|
|
102
|
+
<path class="fc-slider-path-fg" d="${pathD}"></path>
|
|
103
|
+
<circle class="fc-slider-target" cx="${CX}" cy="${CY}" r="14"></circle>
|
|
104
|
+
</svg>
|
|
62
105
|
<div class="fc-slider-handle" role="slider" tabindex="0"></div>
|
|
63
106
|
<span class="fc-slider-tip">${t.tip}</span>
|
|
64
107
|
</div>
|
|
@@ -69,27 +112,44 @@ function createSliderInstance(container, config) {
|
|
|
69
112
|
</div>
|
|
70
113
|
`;
|
|
71
114
|
const root = container.querySelector(".fc-slider");
|
|
72
|
-
const
|
|
115
|
+
const stage = container.querySelector(".fc-slider-stage");
|
|
73
116
|
const handle = container.querySelector(".fc-slider-handle");
|
|
74
|
-
const progress = container.querySelector(".fc-slider-progress");
|
|
75
117
|
const tip = container.querySelector(".fc-slider-tip");
|
|
76
118
|
const msg = container.querySelector(".fc-slider-msg");
|
|
77
119
|
const refreshBtn = container.querySelector(".fc-slider-refresh");
|
|
120
|
+
const fgPath = container.querySelector(".fc-slider-path-fg");
|
|
78
121
|
refreshBtn.addEventListener("click", render);
|
|
122
|
+
const totalLen = fgPath.getTotalLength();
|
|
123
|
+
fgPath.style.strokeDasharray = `${totalLen}`;
|
|
124
|
+
fgPath.style.strokeDashoffset = `${totalLen}`;
|
|
79
125
|
let dragging = false;
|
|
80
|
-
let
|
|
81
|
-
let
|
|
82
|
-
let
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
126
|
+
let startPointer = { x: 0, y: 0 };
|
|
127
|
+
let startProgress = 0;
|
|
128
|
+
let currentProgress = 0;
|
|
129
|
+
function applyProgress(p) {
|
|
130
|
+
currentProgress = Math.max(0, Math.min(1, p));
|
|
131
|
+
const pt = pointAtProgress(pts, currentProgress);
|
|
132
|
+
handle.style.left = `${pt.x / CANVAS_W * 100}%`;
|
|
133
|
+
handle.style.top = `${pt.y / CANVAS_H * 100}%`;
|
|
134
|
+
fgPath.style.strokeDashoffset = `${totalLen * (1 - currentProgress)}`;
|
|
135
|
+
tip.style.opacity = currentProgress > 0.02 ? "0" : "1";
|
|
136
|
+
}
|
|
137
|
+
function bounceBack() {
|
|
138
|
+
handle.style.transition = "left .35s ease, top .35s ease, border-color .15s";
|
|
139
|
+
fgPath.style.transition = "stroke-dashoffset .35s ease, stroke .15s";
|
|
140
|
+
applyProgress(0);
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
handle.style.transition = "border-color .15s";
|
|
143
|
+
fgPath.style.transition = "stroke .15s";
|
|
144
|
+
}, 360);
|
|
87
145
|
}
|
|
146
|
+
applyProgress(0);
|
|
88
147
|
handle.addEventListener("pointerdown", (e) => {
|
|
89
|
-
if (done) return;
|
|
148
|
+
if (done || locked) return;
|
|
90
149
|
dragging = true;
|
|
91
|
-
|
|
92
|
-
|
|
150
|
+
startPointer = { x: e.clientX, y: e.clientY };
|
|
151
|
+
startProgress = currentProgress;
|
|
152
|
+
recorder.start();
|
|
93
153
|
try {
|
|
94
154
|
handle.setPointerCapture(e.pointerId);
|
|
95
155
|
} catch {
|
|
@@ -98,40 +158,55 @@ function createSliderInstance(container, config) {
|
|
|
98
158
|
});
|
|
99
159
|
handle.addEventListener("pointermove", (e) => {
|
|
100
160
|
if (!dragging) return;
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
161
|
+
const rect = stage.getBoundingClientRect();
|
|
162
|
+
const sx = rect.width / CANVAS_W;
|
|
163
|
+
const sy = rect.height / CANVAS_H;
|
|
164
|
+
const svgX = (e.clientX - rect.left) / sx;
|
|
165
|
+
const svgY = (e.clientY - rect.top) / sy;
|
|
166
|
+
const dx = svgX - CX;
|
|
167
|
+
const dy = svgY - CY;
|
|
168
|
+
const r = Math.sqrt(dx * dx + dy * dy);
|
|
169
|
+
const progress = 1 - r / SPIRAL_R_MAX;
|
|
170
|
+
const step = progress - currentProgress;
|
|
171
|
+
const maxStep = 0.08;
|
|
172
|
+
const clampedStep = Math.max(-maxStep, Math.min(maxStep, step));
|
|
173
|
+
applyProgress(currentProgress + clampedStep);
|
|
174
|
+
recorder.record(e.clientX, e.clientY);
|
|
106
175
|
});
|
|
107
176
|
const finish = async () => {
|
|
108
177
|
if (!dragging) return;
|
|
109
178
|
dragging = false;
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
root.classList.add("fc-slider-done");
|
|
115
|
-
const result = {
|
|
116
|
-
success: true,
|
|
117
|
-
proof: await hashProof2(proofInput(current)),
|
|
118
|
-
duration: Date.now() - startTime
|
|
119
|
-
};
|
|
120
|
-
msg.textContent = t.success;
|
|
121
|
-
config.onVerify?.(result);
|
|
122
|
-
listeners.forEach((cb) => cb(result));
|
|
123
|
-
} else {
|
|
124
|
-
currentOffset = 0;
|
|
125
|
-
handle.style.transition = "left .3s";
|
|
126
|
-
progress.style.transition = "width .3s";
|
|
127
|
-
applyPos(0);
|
|
179
|
+
const trackPoints = recorder.stop();
|
|
180
|
+
if (currentProgress < 0.85) {
|
|
181
|
+
bounceBack();
|
|
182
|
+
msg.style.color = "var(--fc-danger)";
|
|
128
183
|
msg.textContent = t.fail;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const analysis = analyzeTrack(trackPoints);
|
|
187
|
+
if (analysis.isBot) {
|
|
188
|
+
locked = true;
|
|
189
|
+
msg.style.color = "var(--fc-danger)";
|
|
190
|
+
msg.textContent = config.locale === "zh" ? `\u7591\u4F3C\u673A\u5668\u4EBA\u64CD\u4F5C\uFF08${analysis.reasons.join("\u3001")}\uFF09` : `${t.botFail} detected`;
|
|
191
|
+
bounceBack();
|
|
129
192
|
setTimeout(() => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}, 320);
|
|
193
|
+
if (!done) render();
|
|
194
|
+
}, 2e3);
|
|
195
|
+
return;
|
|
134
196
|
}
|
|
197
|
+
done = true;
|
|
198
|
+
applyProgress(1);
|
|
199
|
+
root.classList.add("fc-slider-done");
|
|
200
|
+
const result = {
|
|
201
|
+
success: true,
|
|
202
|
+
proof: await hashProof2(proofInput(current)),
|
|
203
|
+
duration: Date.now() - startTime,
|
|
204
|
+
metadata: { humanScore: analysis.humanScore, reasons: analysis.reasons }
|
|
205
|
+
};
|
|
206
|
+
msg.style.color = "var(--fc-success)";
|
|
207
|
+
msg.textContent = t.success;
|
|
208
|
+
config.onVerify?.(result);
|
|
209
|
+
listeners.forEach((cb) => cb(result));
|
|
135
210
|
};
|
|
136
211
|
handle.addEventListener("pointerup", finish);
|
|
137
212
|
handle.addEventListener("pointercancel", finish);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/render.ts","../src/challenge.ts","../src/verifier.ts"],"sourcesContent":["import { defineCaptcha, type CaptchaConfig, type CaptchaPlugin } from '@funnycaptcha/core';\nimport { createSliderInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const sliderPlugin: CaptchaPlugin = {\n id: 'slider',\n category: 'interactive',\n create: createSliderInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '滑动拼图' : 'Slider',\n description: locale === 'zh'\n ? '拖动滑块到终点,简单直接的交互验证'\n : 'Drag the slider to the end. Simple and direct.',\n tags: ['slider', 'drag', 'interactive'],\n }),\n};\n\ndefineCaptcha(sliderPlugin);\n\nexport function mountSlider(container: HTMLElement, config: CaptchaConfig) {\n return createSliderInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, type SliderChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '拖动滑块到最右端', tip: '向右拖动', success: '验证成功', fail: '请拖到尽头', refresh: '刷新' },\n en: { title: 'Drag the slider to the right end', tip: 'Drag right', success: 'Verified', fail: 'Drag to the end', refresh: 'Refresh' },\n};\n\nexport function createSliderInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n const theme = config.theme ?? 'light';\n let current: SliderChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let done = false;\n\n function render() {\n current = generateChallenge();\n startTime = Date.now();\n done = false;\n container.innerHTML = `\n <style>\n .fc-slider{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}\n .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}\n .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}\n .fc-slider{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}\n .fc-slider-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}\n .fc-slider-track{position:relative;height:40px;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:20px}\n .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}\n .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}\n .fc-slider-handle:active{cursor:grabbing}\n .fc-slider-handle::after{content:'\\\\2192';color:var(--fc-accent);font-size:18px;font-weight:bold;transition:color .15s}\n .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}\n .fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}\n .fc-slider-row{display:flex;justify-content:center;margin-top:10px}\n .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}\n .fc-slider-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}\n .fc-slider-done .fc-slider-handle{border-color:var(--fc-success)}\n .fc-slider-done .fc-slider-handle::after{color:var(--fc-success)}\n .fc-slider-done .fc-slider-progress{background:var(--fc-success)}\n </style>\n <div class=\"fc-slider\" data-theme=\"${theme}\">\n <div class=\"fc-slider-title\">${t.title}</div>\n <div class=\"fc-slider-track\">\n <div class=\"fc-slider-progress\"></div>\n <div class=\"fc-slider-handle\" role=\"slider\" tabindex=\"0\"></div>\n <span class=\"fc-slider-tip\">${t.tip}</span>\n </div>\n <div class=\"fc-slider-msg\"></div>\n <div class=\"fc-slider-row\">\n <button class=\"fc-slider-refresh\">${t.refresh}</button>\n </div>\n </div>\n `;\n const root = container.querySelector('.fc-slider') as HTMLDivElement;\n const track = container.querySelector('.fc-slider-track') as HTMLDivElement;\n const handle = container.querySelector('.fc-slider-handle') as HTMLDivElement;\n const progress = container.querySelector('.fc-slider-progress') as HTMLDivElement;\n const tip = container.querySelector('.fc-slider-tip') as HTMLSpanElement;\n const msg = container.querySelector('.fc-slider-msg') as HTMLDivElement;\n const refreshBtn = container.querySelector('.fc-slider-refresh') as HTMLButtonElement;\n refreshBtn.addEventListener('click', render);\n\n let dragging = false;\n let startX = 0;\n let startOffset = 0;\n let currentOffset = 0;\n\n function applyPos(px: number) {\n handle.style.left = `${px}px`;\n progress.style.width = `${px}px`;\n tip.style.opacity = px > 8 ? '0' : '1';\n }\n\n handle.addEventListener('pointerdown', (e) => {\n if (done) return;\n dragging = true;\n startX = e.clientX;\n startOffset = currentOffset;\n try { handle.setPointerCapture(e.pointerId); } catch { /* ignore */ }\n e.preventDefault();\n });\n\n handle.addEventListener('pointermove', (e) => {\n if (!dragging) return;\n const maxX = track.clientWidth - handle.offsetWidth;\n let px = startOffset + (e.clientX - startX);\n px = Math.max(0, Math.min(maxX, px));\n currentOffset = px;\n applyPos(px);\n });\n\n const finish = async () => {\n if (!dragging) return;\n dragging = false;\n const maxX = Math.max(1, track.clientWidth - handle.offsetWidth);\n if (currentOffset / maxX >= 0.95) {\n done = true;\n applyPos(maxX);\n root.classList.add('fc-slider-done');\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(proofInput(current)),\n duration: Date.now() - startTime,\n };\n msg.textContent = t.success;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n } else {\n // 未到终点,弹回\n currentOffset = 0;\n handle.style.transition = 'left .3s';\n progress.style.transition = 'width .3s';\n applyPos(0);\n msg.textContent = t.fail;\n setTimeout(() => {\n handle.style.transition = '';\n progress.style.transition = '';\n if (!done) msg.textContent = '';\n }, 320);\n }\n };\n\n handle.addEventListener('pointerup', finish);\n handle.addEventListener('pointercancel', finish);\n }\n\n return {\n mount: () => render(),\n reset: () => render(),\n destroy: () => { container.innerHTML = ''; listeners = []; },\n onResult: cb => listeners.push(cb),\n };\n}\n","import { hashProof } from '@funnycaptcha/core';\n\n// 滑动拼图题目:包含一个随机 token,用户拖到终点后用 token 构造 proof\nexport interface SliderChallenge {\n token: string;\n}\n\n// 生成随机 token(用于构造 proof)\nexport function generateChallenge(): SliderChallenge {\n const token = Math.random().toString(36).slice(2) + Date.now().toString(36);\n return { token };\n}\n\n// 构造 proof 输入串:token + ':completed'\nexport function proofInput(c: SliderChallenge): string {\n return `${c.token}:completed`;\n}\n\n// 校验完成 proof:hashProof(token + ':completed') === proof\nexport async function verifyCompleted(c: SliderChallenge, proof: string): Promise<boolean> {\n const expected = await hashProof(proofInput(c));\n return expected === proof;\n}\n","import { generateChallenge, verifyCompleted, type SliderChallenge } from './challenge.js';\n\nexport function issueSliderChallenge(): { challenge: SliderChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { token: challenge.token } };\n}\n\nexport async function verifySliderProof(\n challenge: SliderChallenge,\n proof: string,\n): Promise<boolean> {\n return verifyCompleted(challenge, proof);\n}\n"],"mappings":"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","names":["hashProof","hashProof"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/render.ts","../src/challenge.ts","../src/verifier.ts"],"sourcesContent":["import { defineCaptcha, type CaptchaConfig, type CaptchaPlugin } from '@funnycaptcha/core';\nimport { createSliderInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const sliderPlugin: CaptchaPlugin = {\n id: 'slider',\n category: 'interactive',\n create: createSliderInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '滑动拼图' : 'Slider',\n description: locale === 'zh'\n ? '拖动滑块到终点,简单直接的交互验证'\n : 'Drag the slider to the end. Simple and direct.',\n tags: ['slider', 'drag', 'interactive'],\n }),\n};\n\ndefineCaptcha(sliderPlugin);\n\nexport function mountSlider(container: HTMLElement, config: CaptchaConfig) {\n return createSliderInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof, TrackRecorder, analyzeTrack } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, type SliderChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '沿螺旋拖动滑块到终点', tip: '沿螺旋路径拖动', success: '验证成功', fail: '请拖到尽头', botFail: '检测到异常操作', refresh: '刷新' },\n 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' },\n};\n\n// 螺旋轨道画布尺寸\nconst CANVAS_W = 320;\nconst CANVAS_H = 200;\nconst HANDLE_W = 36;\nconst HANDLE_H = 36;\n\n// 螺旋参数:阿基米德螺旋 r = a + b*θ\n// 从外向内,θ 从 0 到 6π(3 圈)\n// 半径受画布限制:CY=100,留 8px 边距,r_max ≤ 92\n// r_max = SPIRAL_B * SPIRAL_THETA_MAX = 5 * 6π ≈ 94.2\nconst SPIRAL_THETA_MAX = 6 * Math.PI;\nconst SPIRAL_A = 0; // 起始半径(0 = 终点在正中心)\nconst SPIRAL_B = 5; // 每圈半径增量(调小以适配画布)\n\n// 中心点\nconst CX = CANVAS_W / 2;\nconst CY = CANVAS_H / 2;\n\n// 螺旋外端最大半径(用于极坐标距离映射)\nconst SPIRAL_R_MAX = SPIRAL_B * SPIRAL_THETA_MAX;\n\n// 预生成螺旋路径点(从外向内,用户从外端开始拖到中心)\nfunction spiralPoints(): { x: number; y: number; theta: number }[] {\n const steps = 200;\n const pts: { x: number; y: number; theta: number }[] = [];\n for (let i = 0; i <= steps; i++) {\n // t: 0 -> 1, theta: SPIRAL_THETA_MAX -> 0(外到内)\n const t = i / steps;\n const theta = SPIRAL_THETA_MAX * (1 - t);\n const r = SPIRAL_A + SPIRAL_B * theta;\n const x = CX + r * Math.cos(theta);\n const y = CY + r * Math.sin(theta);\n pts.push({ x, y, theta });\n }\n return pts;\n}\n\n// 生成 SVG path\nfunction spiralPath(pts: { x: number; y: number }[]): string {\n return pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(' ');\n}\n\n// 根据进度 (0-1) 获取螺旋上的点\nfunction pointAtProgress(pts: { x: number; y: number }[], progress: number): { x: number; y: number } {\n const idx = Math.min(pts.length - 1, Math.max(0, Math.round(progress * (pts.length - 1))));\n return pts[idx]!;\n}\n\nexport function createSliderInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n const theme = config.theme ?? 'light';\n const recorder = new TrackRecorder();\n let current: SliderChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let done = false;\n let locked = false;\n\n function render() {\n current = generateChallenge();\n startTime = Date.now();\n done = false;\n locked = false;\n recorder.clear();\n const pts = spiralPoints();\n const pathD = spiralPath(pts);\n container.innerHTML = `\n <style>\n .fc-slider{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}\n .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}\n .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}\n .fc-slider{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}\n .fc-slider-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}\n .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}\n .fc-slider-svg{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}\n .fc-slider-path-bg{fill:none;stroke:var(--fc-border);stroke-width:3;stroke-linecap:round}\n .fc-slider-path-fg{fill:none;stroke:var(--fc-accent);stroke-width:4;stroke-linecap:round;transition:stroke .15s}\n .fc-slider-target{fill:none;stroke:var(--fc-success);stroke-width:2;stroke-dasharray:4 3;opacity:.7}\n .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}\n .fc-slider-handle:active{cursor:grabbing}\n .fc-slider-handle::after{content:'';width:10px;height:10px;background:var(--fc-accent);border-radius:50%;transition:background .15s}\n .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}\n .fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}\n .fc-slider-row{display:flex;justify-content:center;margin-top:10px}\n .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}\n .fc-slider-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}\n .fc-slider-done .fc-slider-handle{border-color:var(--fc-success)}\n .fc-slider-done .fc-slider-handle::after{background:var(--fc-success)}\n .fc-slider-done .fc-slider-path-fg{stroke:var(--fc-success)}\n </style>\n <div class=\"fc-slider\" data-theme=\"${theme}\">\n <div class=\"fc-slider-title\">${t.title}</div>\n <div class=\"fc-slider-stage\">\n <svg class=\"fc-slider-svg\" viewBox=\"0 0 ${CANVAS_W} ${CANVAS_H}\">\n <path class=\"fc-slider-path-bg\" d=\"${pathD}\"></path>\n <path class=\"fc-slider-path-fg\" d=\"${pathD}\"></path>\n <circle class=\"fc-slider-target\" cx=\"${CX}\" cy=\"${CY}\" r=\"14\"></circle>\n </svg>\n <div class=\"fc-slider-handle\" role=\"slider\" tabindex=\"0\"></div>\n <span class=\"fc-slider-tip\">${t.tip}</span>\n </div>\n <div class=\"fc-slider-msg\"></div>\n <div class=\"fc-slider-row\">\n <button class=\"fc-slider-refresh\">${t.refresh}</button>\n </div>\n </div>\n `;\n const root = container.querySelector('.fc-slider') as HTMLDivElement;\n const stage = container.querySelector('.fc-slider-stage') as HTMLDivElement;\n const handle = container.querySelector('.fc-slider-handle') as HTMLDivElement;\n const tip = container.querySelector('.fc-slider-tip') as HTMLSpanElement;\n const msg = container.querySelector('.fc-slider-msg') as HTMLDivElement;\n const refreshBtn = container.querySelector('.fc-slider-refresh') as HTMLButtonElement;\n const fgPath = container.querySelector('.fc-slider-path-fg') as SVGPathElement;\n refreshBtn.addEventListener('click', render);\n\n // 计算路径总长度,用于进度\n const totalLen = fgPath.getTotalLength();\n fgPath.style.strokeDasharray = `${totalLen}`;\n fgPath.style.strokeDashoffset = `${totalLen}`;\n\n let dragging = false;\n let startPointer = { x: 0, y: 0 };\n let startProgress = 0;\n let currentProgress = 0; // 0 = 外端, 1 = 中心\n\n function applyProgress(p: number) {\n currentProgress = Math.max(0, Math.min(1, p));\n const pt = pointAtProgress(pts, currentProgress);\n // handle 用百分比定位,适配 stage 实际尺寸\n handle.style.left = `${(pt.x / CANVAS_W) * 100}%`;\n handle.style.top = `${(pt.y / CANVAS_H) * 100}%`;\n fgPath.style.strokeDashoffset = `${totalLen * (1 - currentProgress)}`;\n tip.style.opacity = currentProgress > 0.02 ? '0' : '1';\n }\n\n function bounceBack() {\n handle.style.transition = 'left .35s ease, top .35s ease, border-color .15s';\n fgPath.style.transition = 'stroke-dashoffset .35s ease, stroke .15s';\n applyProgress(0);\n setTimeout(() => {\n handle.style.transition = 'border-color .15s';\n fgPath.style.transition = 'stroke .15s';\n }, 360);\n }\n\n applyProgress(0);\n\n // 核心:把鼠标水平/垂直位移映射到螺旋进度\n // 由于螺旋是 2D 的,我们用鼠标移动的距离(向中心方向的投影)来推进进度\n handle.addEventListener('pointerdown', (e) => {\n if (done || locked) return;\n dragging = true;\n startPointer = { x: e.clientX, y: e.clientY };\n startProgress = currentProgress;\n recorder.start();\n try { handle.setPointerCapture(e.pointerId); } catch { /* ignore */ }\n e.preventDefault();\n });\n\n handle.addEventListener('pointermove', (e) => {\n if (!dragging) return;\n // 极坐标距离映射:鼠标到中心的距离 r → 螺旋进度\n // 阿基米德螺旋 r = SPIRAL_B * theta,theta 从 theta_max(外) 到 0(中心)\n // progress = 1 - theta/theta_max = 1 - r/r_max\n // 鼠标越接近中心 → r 越小 → progress 越接近 1(终点)\n // 鼠标越远离中心 → r 越大 → progress 越接近 0(起点)\n // 实时获取 stage 坐标(避免页面滚动后 stageRect 过时)\n const rect = stage.getBoundingClientRect();\n const sx = rect.width / CANVAS_W;\n const sy = rect.height / CANVAS_H;\n const svgX = (e.clientX - rect.left) / sx;\n const svgY = (e.clientY - rect.top) / sy;\n const dx = svgX - CX;\n const dy = svgY - CY;\n const r = Math.sqrt(dx * dx + dy * dy);\n const progress = 1 - r / SPIRAL_R_MAX;\n // 平滑推进:限制单步变化幅度,避免抖动跨越圈\n const step = progress - currentProgress;\n const maxStep = 0.08; // 单次最多推进 8%\n const clampedStep = Math.max(-maxStep, Math.min(maxStep, step));\n applyProgress(currentProgress + clampedStep);\n recorder.record(e.clientX, e.clientY);\n });\n\n const finish = async () => {\n if (!dragging) return;\n dragging = false;\n const trackPoints = recorder.stop();\n\n // a. 位置判定:进度 >= 0.92 算到达中心\n if (currentProgress < 0.85) {\n bounceBack();\n msg.style.color = 'var(--fc-danger)';\n msg.textContent = t.fail;\n return;\n }\n\n // c. 行为分析\n const analysis = analyzeTrack(trackPoints);\n\n // d. 机器人检测\n if (analysis.isBot) {\n locked = true;\n msg.style.color = 'var(--fc-danger)';\n msg.textContent = config.locale === 'zh'\n ? `疑似机器人操作(${analysis.reasons.join('、')})`\n : `${t.botFail} detected`;\n bounceBack();\n setTimeout(() => { if (!done) render(); }, 2000);\n return;\n }\n\n // e. 通过\n done = true;\n applyProgress(1);\n root.classList.add('fc-slider-done');\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(proofInput(current)),\n duration: Date.now() - startTime,\n metadata: { humanScore: analysis.humanScore, reasons: analysis.reasons },\n };\n msg.style.color = 'var(--fc-success)';\n msg.textContent = t.success;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n };\n\n handle.addEventListener('pointerup', finish);\n handle.addEventListener('pointercancel', finish);\n }\n\n return {\n mount: () => render(),\n reset: () => render(),\n destroy: () => { container.innerHTML = ''; listeners = []; },\n onResult: cb => listeners.push(cb),\n };\n}\n","import { hashProof } from '@funnycaptcha/core';\n\n// 滑动拼图题目:包含一个随机 token,用户拖到终点后用 token 构造 proof\nexport interface SliderChallenge {\n token: string;\n}\n\n// 生成随机 token(用于构造 proof)\nexport function generateChallenge(): SliderChallenge {\n const token = Math.random().toString(36).slice(2) + Date.now().toString(36);\n return { token };\n}\n\n// 构造 proof 输入串:token + ':completed'\nexport function proofInput(c: SliderChallenge): string {\n return `${c.token}:completed`;\n}\n\n// 校验完成 proof:hashProof(token + ':completed') === proof\nexport async function verifyCompleted(c: SliderChallenge, proof: string): Promise<boolean> {\n const expected = await hashProof(proofInput(c));\n return expected === proof;\n}\n","import { generateChallenge, verifyCompleted, type SliderChallenge } from './challenge.js';\n\nexport function issueSliderChallenge(): { challenge: SliderChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { token: challenge.token } };\n}\n\nexport async function verifySliderProof(\n challenge: SliderChallenge,\n proof: string,\n): Promise<boolean> {\n return verifyCompleted(challenge, proof);\n}\n"],"mappings":"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","names":["hashProof","hashProof"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnycaptcha/slider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Slider captcha — FunnyChapter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@funnycaptcha/core": "0.
|
|
18
|
+
"@funnycaptcha/core": "0.6.0"
|
|
19
19
|
},
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public",
|