|
@@ -0,0 +1,380 @@
|
|
|
+import { reactive, ref, defineComponent, onMounted, onBeforeUnmount, openBlock, createElementBlock, normalizeStyle, createElementVNode, normalizeClass, createCommentVNode, toDisplayString } from "vue";
|
|
|
+const PI = Math.PI;
|
|
|
+function sum(x, y) {
|
|
|
+ return x + y;
|
|
|
+}
|
|
|
+function square(x) {
|
|
|
+ return x * x;
|
|
|
+}
|
|
|
+function draw(ctx, x, y, l, r, operation) {
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x, y);
|
|
|
+ ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI);
|
|
|
+ ctx.lineTo(x + l, y);
|
|
|
+ ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI);
|
|
|
+ ctx.lineTo(x + l, y + l);
|
|
|
+ ctx.lineTo(x, y + l);
|
|
|
+ ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true);
|
|
|
+ ctx.lineTo(x, y);
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.fillStyle = "rgba(255, 255, 255, 0.7)";
|
|
|
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
|
|
|
+ ctx.stroke();
|
|
|
+ ctx[operation]();
|
|
|
+ ctx.globalCompositeOperation = "destination-over";
|
|
|
+}
|
|
|
+function createImg(imgs, onload) {
|
|
|
+ const img = document.createElement("img");
|
|
|
+ img.crossOrigin = "Anonymous";
|
|
|
+ img.onload = onload;
|
|
|
+ img.onerror = () => {
|
|
|
+ img.src = getRandomImg(imgs);
|
|
|
+ };
|
|
|
+ img.src = getRandomImg(imgs);
|
|
|
+ return img;
|
|
|
+}
|
|
|
+function getRandomNumberByRange(start, end) {
|
|
|
+ return Math.round(Math.random() * (end - start) + start);
|
|
|
+}
|
|
|
+function getRandomImg(imgs) {
|
|
|
+ const len = imgs.length;
|
|
|
+ return len > 0 ? imgs[getRandomNumberByRange(0, len - 1)] : "https://picsum.photos/300/150?image=" + getRandomNumberByRange(0, 1084);
|
|
|
+}
|
|
|
+function throttle(fn, interval, options = { leading: true, trailing: true }) {
|
|
|
+ const { leading, trailing, resultCallback } = options;
|
|
|
+ let lastTime = 0;
|
|
|
+ let timer = null;
|
|
|
+ const _throttle = function(...args) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const nowTime = new Date().getTime();
|
|
|
+ if (!lastTime && !leading)
|
|
|
+ lastTime = nowTime;
|
|
|
+ const remainTime = interval - (nowTime - lastTime);
|
|
|
+ if (remainTime <= 0) {
|
|
|
+ if (timer) {
|
|
|
+ clearTimeout(timer);
|
|
|
+ timer = null;
|
|
|
+ }
|
|
|
+ const result = fn.apply(this, args);
|
|
|
+ if (resultCallback)
|
|
|
+ resultCallback(result);
|
|
|
+ resolve(result);
|
|
|
+ lastTime = nowTime;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (trailing && !timer) {
|
|
|
+ timer = setTimeout(() => {
|
|
|
+ timer = null;
|
|
|
+ lastTime = !leading ? 0 : new Date().getTime();
|
|
|
+ const result = fn.apply(this, args);
|
|
|
+ if (resultCallback)
|
|
|
+ resultCallback(result);
|
|
|
+ resolve(result);
|
|
|
+ }, remainTime);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+ _throttle.cancel = function() {
|
|
|
+ if (timer)
|
|
|
+ clearTimeout(timer);
|
|
|
+ timer = null;
|
|
|
+ lastTime = 0;
|
|
|
+ };
|
|
|
+ return _throttle;
|
|
|
+}
|
|
|
+function useSlideAction() {
|
|
|
+ const origin = reactive({
|
|
|
+ x: 0,
|
|
|
+ y: 0
|
|
|
+ });
|
|
|
+ const success = ref(false);
|
|
|
+ const isMouseDown = ref(false);
|
|
|
+ const timestamp = ref(0);
|
|
|
+ const trail = ref([]);
|
|
|
+ const start = (e) => {
|
|
|
+ if (success.value)
|
|
|
+ return;
|
|
|
+ if (e instanceof MouseEvent) {
|
|
|
+ origin.x = e.clientX;
|
|
|
+ origin.y = e.clientY;
|
|
|
+ } else {
|
|
|
+ origin.x = e.changedTouches[0].pageX;
|
|
|
+ origin.y = e.changedTouches[0].pageY;
|
|
|
+ }
|
|
|
+ isMouseDown.value = true;
|
|
|
+ timestamp.value = Date.now();
|
|
|
+ };
|
|
|
+ const move = (w, e, cb) => {
|
|
|
+ if (!isMouseDown.value)
|
|
|
+ return false;
|
|
|
+ let moveX = 0;
|
|
|
+ let moveY = 0;
|
|
|
+ if (e instanceof MouseEvent) {
|
|
|
+ moveX = e.clientX - origin.x;
|
|
|
+ moveY = e.clientY - origin.y;
|
|
|
+ } else {
|
|
|
+ moveX = e.changedTouches[0].pageX - origin.x;
|
|
|
+ moveY = e.changedTouches[0].pageY - origin.y;
|
|
|
+ }
|
|
|
+ if (moveX < 0 || moveX + 38 >= w)
|
|
|
+ return false;
|
|
|
+ cb(moveX);
|
|
|
+ trail.value.push(moveY);
|
|
|
+ };
|
|
|
+ const verify = (left, blockX, accuracy) => {
|
|
|
+ const arr = trail.value;
|
|
|
+ const average = arr.reduce(sum) / arr.length;
|
|
|
+ const deviations = arr.map((x) => x - average);
|
|
|
+ const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length);
|
|
|
+ const leftNum = parseInt(left);
|
|
|
+ accuracy = accuracy <= 1 ? 1 : accuracy > 10 ? 10 : accuracy;
|
|
|
+ return {
|
|
|
+ spliced: Math.abs(leftNum - blockX) <= accuracy,
|
|
|
+ TuringTest: average !== stddev
|
|
|
+ };
|
|
|
+ };
|
|
|
+ const end = (e, cb) => {
|
|
|
+ if (!isMouseDown.value)
|
|
|
+ return false;
|
|
|
+ isMouseDown.value = false;
|
|
|
+ const moveX = e instanceof MouseEvent ? e.clientX : e.changedTouches[0].pageX;
|
|
|
+ if (moveX === origin.x)
|
|
|
+ return false;
|
|
|
+ timestamp.value = Date.now() - timestamp.value;
|
|
|
+ cb(timestamp.value);
|
|
|
+ };
|
|
|
+ return { origin, success, isMouseDown, timestamp, trail, start, move, end, verify };
|
|
|
+}
|
|
|
+var slideVerify_vue_vue_type_style_index_0_scoped_true_lang = "";
|
|
|
+var _export_sfc = (sfc, props) => {
|
|
|
+ const target = sfc.__vccOpts || sfc;
|
|
|
+ for (const [key, val] of props) {
|
|
|
+ target[key] = val;
|
|
|
+ }
|
|
|
+ return target;
|
|
|
+};
|
|
|
+const _sfc_main = defineComponent({
|
|
|
+ name: "SlideVerify",
|
|
|
+ props: {
|
|
|
+ l: {
|
|
|
+ type: Number,
|
|
|
+ default: 42
|
|
|
+ },
|
|
|
+ r: {
|
|
|
+ type: Number,
|
|
|
+ default: 10
|
|
|
+ },
|
|
|
+ w: {
|
|
|
+ type: Number,
|
|
|
+ default: 310
|
|
|
+ },
|
|
|
+ h: {
|
|
|
+ type: Number,
|
|
|
+ default: 155
|
|
|
+ },
|
|
|
+ sliderText: {
|
|
|
+ type: String,
|
|
|
+ default: "Slide filled right"
|
|
|
+ },
|
|
|
+ accuracy: {
|
|
|
+ type: Number,
|
|
|
+ default: 5
|
|
|
+ },
|
|
|
+ show: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ imgs: {
|
|
|
+ type: Array,
|
|
|
+ default: () => []
|
|
|
+ },
|
|
|
+ interval: {
|
|
|
+ type: Number,
|
|
|
+ default: 50
|
|
|
+ }
|
|
|
+ },
|
|
|
+ emits: ["success", "again", "fail", "refresh", "imageLoad"],
|
|
|
+ setup(props, { emit }) {
|
|
|
+ const { imgs, l, r, w, h, accuracy, interval } = props;
|
|
|
+ const loadBlock = ref(true);
|
|
|
+ const blockX = ref(0);
|
|
|
+ const blockY = ref(0);
|
|
|
+ const containerCls = reactive({
|
|
|
+ containerActive: false,
|
|
|
+ containerSuccess: false,
|
|
|
+ containerFail: false
|
|
|
+ });
|
|
|
+ const sliderBox = reactive({
|
|
|
+ iconCls: "arrow-right",
|
|
|
+ width: "0",
|
|
|
+ left: "0"
|
|
|
+ });
|
|
|
+ const block = ref();
|
|
|
+ const blockCtx = ref();
|
|
|
+ const canvas = ref();
|
|
|
+ const canvasCtx = ref();
|
|
|
+ let img;
|
|
|
+ const { success, start, move, end, verify } = useSlideAction();
|
|
|
+ const reset = () => {
|
|
|
+ var _a, _b;
|
|
|
+ success.value = false;
|
|
|
+ containerCls.containerActive = false;
|
|
|
+ containerCls.containerSuccess = false;
|
|
|
+ containerCls.containerFail = false;
|
|
|
+ sliderBox.iconCls = "arrow-right";
|
|
|
+ sliderBox.left = "0";
|
|
|
+ sliderBox.width = "0";
|
|
|
+ block.value.style.left = "0";
|
|
|
+ (_a = canvasCtx.value) == null ? void 0 : _a.clearRect(0, 0, w, h);
|
|
|
+ (_b = blockCtx.value) == null ? void 0 : _b.clearRect(0, 0, w, h);
|
|
|
+ block.value.width = w;
|
|
|
+ img.src = getRandomImg(imgs);
|
|
|
+ };
|
|
|
+ const refresh = () => {
|
|
|
+ reset();
|
|
|
+ emit("refresh");
|
|
|
+ };
|
|
|
+ function moveCb(moveX) {
|
|
|
+ sliderBox.left = moveX + "px";
|
|
|
+ let blockLeft = (w - 40 - 20) / (w - 40) * moveX;
|
|
|
+ block.value.style.left = blockLeft + "px";
|
|
|
+ containerCls.containerActive = true;
|
|
|
+ sliderBox.width = moveX + "px";
|
|
|
+ }
|
|
|
+ function endCb(timestamp) {
|
|
|
+ const { spliced, TuringTest } = verify(block.value.style.left, blockX.value, accuracy);
|
|
|
+ if (spliced) {
|
|
|
+ if (accuracy === -1) {
|
|
|
+ containerCls.containerSuccess = true;
|
|
|
+ sliderBox.iconCls = "success";
|
|
|
+ success.value = true;
|
|
|
+ emit("success", timestamp);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (TuringTest) {
|
|
|
+ containerCls.containerSuccess = true;
|
|
|
+ sliderBox.iconCls = "success";
|
|
|
+ success.value = true;
|
|
|
+ emit("success", timestamp);
|
|
|
+ } else {
|
|
|
+ containerCls.containerFail = true;
|
|
|
+ sliderBox.iconCls = "fail";
|
|
|
+ emit("again");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ containerCls.containerFail = true;
|
|
|
+ sliderBox.iconCls = "fail";
|
|
|
+ emit("fail");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const touchMoveEvent = throttle((e) => {
|
|
|
+ move(w, e, moveCb);
|
|
|
+ }, interval);
|
|
|
+ const touchEndEvent = (e) => {
|
|
|
+ end(e, endCb);
|
|
|
+ };
|
|
|
+ onMounted(() => {
|
|
|
+ var _a, _b;
|
|
|
+ const _canvasCtx = (_a = canvas.value) == null ? void 0 : _a.getContext("2d");
|
|
|
+ const _blockCtx = (_b = block.value) == null ? void 0 : _b.getContext("2d", { willReadFrequently: true });
|
|
|
+ canvasCtx.value = _canvasCtx;
|
|
|
+ blockCtx.value = _blockCtx;
|
|
|
+ img = createImg(imgs, () => {
|
|
|
+ loadBlock.value = false;
|
|
|
+ const L = l + r * 2 + 3;
|
|
|
+ blockX.value = getRandomNumberByRange(L + 10, w - (L + 10));
|
|
|
+ blockY.value = getRandomNumberByRange(10 + r * 2, h - (L + 10));
|
|
|
+ if (_canvasCtx && _blockCtx) {
|
|
|
+ draw(_canvasCtx, blockX.value, blockY.value, l, r, "fill");
|
|
|
+ draw(_blockCtx, blockX.value, blockY.value, l, r, "clip");
|
|
|
+ _canvasCtx.drawImage(img, 0, 0, w, h);
|
|
|
+ _blockCtx.drawImage(img, 0, 0, w, h);
|
|
|
+ const _y = blockY.value - r * 2 - 1;
|
|
|
+ const imgData = _blockCtx.getImageData(blockX.value, _y, L, L);
|
|
|
+ block.value.width = L;
|
|
|
+ _blockCtx.putImageData(imgData, 0, _y);
|
|
|
+ }
|
|
|
+ emit('imageLoad')
|
|
|
+ });
|
|
|
+ document.addEventListener("mousemove", touchMoveEvent);
|
|
|
+ document.addEventListener("mouseup", touchEndEvent);
|
|
|
+ });
|
|
|
+ onBeforeUnmount(() => {
|
|
|
+ document.removeEventListener("mousemove", touchMoveEvent);
|
|
|
+ document.removeEventListener("mouseup", touchEndEvent);
|
|
|
+ });
|
|
|
+ return {
|
|
|
+ block,
|
|
|
+ canvas,
|
|
|
+ loadBlock,
|
|
|
+ containerCls,
|
|
|
+ sliderBox,
|
|
|
+ refresh,
|
|
|
+ sliderDown: start,
|
|
|
+ touchStartEvent: start,
|
|
|
+ touchMoveEvent,
|
|
|
+ touchEndEvent
|
|
|
+ };
|
|
|
+ }
|
|
|
+});
|
|
|
+const _hoisted_1 = ["width", "height"];
|
|
|
+const _hoisted_2 = ["width", "height"];
|
|
|
+const _hoisted_3 = { class: "slide-verify-slider-text" };
|
|
|
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
+ return openBlock(), createElementBlock("div", {
|
|
|
+ id: "slideVerify",
|
|
|
+ class: "slide-verify",
|
|
|
+ style: normalizeStyle({ width: _ctx.w + "px" }),
|
|
|
+ onselectstart: "return false;"
|
|
|
+ }, [
|
|
|
+ createElementVNode("div", {
|
|
|
+ class: normalizeClass({ "slider-verify-loading": _ctx.loadBlock })
|
|
|
+ }, null, 2),
|
|
|
+ createElementVNode("canvas", {
|
|
|
+ ref: "canvas",
|
|
|
+ width: _ctx.w,
|
|
|
+ height: _ctx.h
|
|
|
+ }, null, 8, _hoisted_1),
|
|
|
+ _ctx.show ? (openBlock(), createElementBlock("div", {
|
|
|
+ key: 0,
|
|
|
+ class: "slide-verify-refresh-icon",
|
|
|
+ onClick: _cache[0] || (_cache[0] = (...args) => _ctx.refresh && _ctx.refresh(...args))
|
|
|
+ }, _cache[5] || (_cache[5] = [
|
|
|
+ createElementVNode("i", { class: "iconfont icon-refresh" }, null, -1)
|
|
|
+ ]))) : createCommentVNode("", true),
|
|
|
+ createElementVNode("canvas", {
|
|
|
+ ref: "block",
|
|
|
+ width: _ctx.w,
|
|
|
+ height: _ctx.h,
|
|
|
+ class: "slide-verify-block"
|
|
|
+ }, null, 8, _hoisted_2),
|
|
|
+ createElementVNode("div", {
|
|
|
+ class: normalizeClass(["slide-verify-slider", {
|
|
|
+ "container-active": _ctx.containerCls.containerActive,
|
|
|
+ "container-success": _ctx.containerCls.containerSuccess,
|
|
|
+ "container-fail": _ctx.containerCls.containerFail
|
|
|
+ }])
|
|
|
+ }, [
|
|
|
+ createElementVNode("div", {
|
|
|
+ class: "slide-verify-slider-mask",
|
|
|
+ style: normalizeStyle({ width: _ctx.sliderBox.width })
|
|
|
+ }, [
|
|
|
+ createElementVNode("div", {
|
|
|
+ class: "slide-verify-slider-mask-item",
|
|
|
+ style: normalizeStyle({ left: _ctx.sliderBox.left }),
|
|
|
+ onMousedown: _cache[1] || (_cache[1] = (...args) => _ctx.sliderDown && _ctx.sliderDown(...args)),
|
|
|
+ onTouchstart: _cache[2] || (_cache[2] = (...args) => _ctx.touchStartEvent && _ctx.touchStartEvent(...args)),
|
|
|
+ onTouchmove: _cache[3] || (_cache[3] = (...args) => _ctx.touchMoveEvent && _ctx.touchMoveEvent(...args)),
|
|
|
+ onTouchend: _cache[4] || (_cache[4] = (...args) => _ctx.touchEndEvent && _ctx.touchEndEvent(...args))
|
|
|
+ }, [
|
|
|
+ createElementVNode("i", {
|
|
|
+ class: normalizeClass(["slide-verify-slider-mask-item-icon", "iconfont", `icon-${_ctx.sliderBox.iconCls}`])
|
|
|
+ }, null, 2)
|
|
|
+ ], 36)
|
|
|
+ ], 4),
|
|
|
+ createElementVNode("span", _hoisted_3, toDisplayString(_ctx.sliderText), 1)
|
|
|
+ ], 2)
|
|
|
+ ], 4);
|
|
|
+}
|
|
|
+var SlideVerify = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-3f647794"]]);
|
|
|
+export { SlideVerify as default };
|