462 lines
13 KiB
Vue
462 lines
13 KiB
Vue
<script setup lang="js">
|
||
import {onMounted} from "vue"
|
||
import {createWorker} from "tesseract.js";
|
||
import useMobile from "@/hooks/useMobile.ts";
|
||
|
||
const isMobile = useMobile()
|
||
|
||
onMounted(async () => {
|
||
Array.prototype.clone = function () {
|
||
return [].concat(this);
|
||
//或者 return this.concat();
|
||
}
|
||
|
||
class Point {
|
||
constructor(x, y, time) {
|
||
this.x = x;
|
||
this.y = y;
|
||
this.isControl = false;
|
||
this.time = Date.now();
|
||
this.lineWidth = 0;
|
||
this.isAdd = false;
|
||
}
|
||
}
|
||
|
||
class Line {
|
||
constructor() {
|
||
this.points = new Array();
|
||
this.changeWidthCount = 0;
|
||
this.lineWidth = 10;
|
||
}
|
||
}
|
||
|
||
class HandwritingSelf {
|
||
|
||
constructor(canvas) {
|
||
this.canvas = canvas;
|
||
this.ctx = canvas.getContext("2d")
|
||
let canvasRect = canvas.getBoundingClientRect()
|
||
let {width, height} = canvasRect
|
||
let dpr = window.devicePixelRatio
|
||
if (dpr) {
|
||
canvas.style.width = width + "px"
|
||
canvas.style.height = height + "px"
|
||
canvas.height = height * dpr
|
||
canvas.width = width * dpr
|
||
this.ctx.scale(dpr, dpr)
|
||
}
|
||
// this.points = new Array();
|
||
this.line = new Line();
|
||
this.pointLines = new Array();//Line数组
|
||
this.k = 0.5;
|
||
this.begin = null;
|
||
this.middle = null;
|
||
this.end = null;
|
||
this.preTime = null;
|
||
this.lineWidth = 8;
|
||
this.isDown = false;
|
||
}
|
||
|
||
down(x, y) {
|
||
// console.log("down:", x, y)
|
||
this.isDown = true;
|
||
this.line = new Line();
|
||
this.line.lineWidth = this.lineWidth;
|
||
let currentPoint = new Point(x, y, Date.now());
|
||
this.addPoint(currentPoint);
|
||
|
||
this.preTime = Date.now();
|
||
}
|
||
|
||
move(x, y) {
|
||
// console.log("move:",x,y,this.isDown)
|
||
if (this.isDown) {
|
||
let currentPoint = new Point(x, y, Date.now())
|
||
this.addPoint(currentPoint);
|
||
this.draw();
|
||
}
|
||
}
|
||
|
||
up(x, y) {
|
||
// if (e.touches.length > 0) {
|
||
let currentPoint = new Point(x, y, Date.now())
|
||
this.addPoint(currentPoint);
|
||
// }
|
||
this.draw(true);
|
||
|
||
this.pointLines.push(this.line);
|
||
|
||
this.begin = null;
|
||
this.middle = null;
|
||
this.end = null;
|
||
this.isDown = false;
|
||
}
|
||
|
||
draw(isUp = false) {
|
||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||
this.ctx.strokeStyle = "rgba(255,20,87,1)";
|
||
|
||
//绘制不包含this.line的线条
|
||
this.pointLines.forEach((line, index) => {
|
||
let points = line.points;
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(points[0].x - 1.5, points[0].y, 6, 3, Math.PI / 4, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(points[0].x, points[0].y);
|
||
let lastW = line.lineWidth;
|
||
this.ctx.lineWidth = line.lineWidth;
|
||
this.ctx.lineJoin = "round";
|
||
this.ctx.lineCap = "round";
|
||
let minLineW = line.lineWidth / 4;
|
||
let isChangeW = false;
|
||
|
||
let changeWidthCount = line.changeWidthCount;
|
||
for (let i = 1; i <= points.length; i++) {
|
||
if (i == points.length) {
|
||
this.ctx.stroke();
|
||
break;
|
||
}
|
||
if (i > points.length - changeWidthCount) {
|
||
if (!isChangeW) {
|
||
this.ctx.stroke();//将之前的线条不变的path绘制完
|
||
isChangeW = true;
|
||
if (i > 1 && points[i - 1].isControl)
|
||
continue;
|
||
}
|
||
let w = (lastW - minLineW) / changeWidthCount * (points.length - i) + minLineW;
|
||
points[i - 1].lineWidth = w;
|
||
this.ctx.beginPath();//为了开启新的路径 否则每次stroke 都会把之前的路径在描一遍
|
||
// this.ctx.strokeStyle = "rgba("+Math.random()*255+","+Math.random()*255+","+Math.random()*255+",1)";
|
||
this.ctx.lineWidth = w;
|
||
this.ctx.moveTo(points[i - 1].x, points[i - 1].y);//移动到之前的点
|
||
this.ctx.lineTo(points[i].x, points[i].y);
|
||
this.ctx.stroke();//将之前的线条不变的path绘制完
|
||
} else {
|
||
if (points[i].isControl && points[i + 1]) {
|
||
this.ctx.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y);
|
||
} else if (i >= 1 && points[i - 1].isControl) {//上一个是控制点 当前点已经被绘制
|
||
} else
|
||
this.ctx.lineTo(points[i].x, points[i].y);
|
||
}
|
||
}
|
||
})
|
||
|
||
//绘制this.line线条
|
||
let points;
|
||
if (isUp)
|
||
points = this.line.points;
|
||
else
|
||
points = this.line.points.clone();
|
||
//当前绘制的线条最后几个补点 贝塞尔方式增加点
|
||
let count = 0;
|
||
let insertCount = 0;
|
||
let i = points.length - 1;
|
||
let endPoint = points[i];
|
||
let controlPoint;
|
||
let startPoint;
|
||
while (i >= 0) {
|
||
if (points[i].isControl == true) {
|
||
controlPoint = points[i];
|
||
count++;
|
||
} else {
|
||
startPoint = points[i];
|
||
}
|
||
if (startPoint && controlPoint && endPoint) {//使用贝塞尔计算补点
|
||
let dis = this.z_distance(startPoint, controlPoint) + this.z_distance(controlPoint, endPoint);
|
||
let insertPoints = this.BezierCalculate([startPoint, controlPoint, endPoint], Math.floor(dis / 6) + 1);
|
||
insertCount += insertPoints.length;
|
||
var index = i;//插入位置
|
||
// 把insertPoints 变成一个适合splice的数组(包含splice前2个参数的数组)
|
||
insertPoints.unshift(index, 1);
|
||
Array.prototype.splice.apply(points, insertPoints);
|
||
|
||
//补完点后
|
||
endPoint = startPoint;
|
||
startPoint = null;
|
||
}
|
||
if (count >= 6)
|
||
break;
|
||
i--;
|
||
}
|
||
//确定最后线宽变化的点数
|
||
let changeWidthCount = count + insertCount;
|
||
if (isUp)
|
||
this.line.changeWidthCount = changeWidthCount;
|
||
|
||
//制造椭圆头
|
||
this.ctx.fillStyle = "rgba(255,20,87,1)"
|
||
this.ctx.beginPath();
|
||
this.ctx.ellipse(points[0].x - 1.5, points[0].y, 6, 3, Math.PI / 4, 0, Math.PI * 2);
|
||
this.ctx.fill();
|
||
|
||
// console.log('points', points)
|
||
|
||
this.ctx.beginPath();
|
||
this.ctx.moveTo(points[0].x, points[0].y);
|
||
let lastW = this.line.lineWidth;
|
||
this.ctx.lineWidth = this.line.lineWidth;
|
||
this.ctx.lineJoin = "round";
|
||
this.ctx.lineCap = "round";
|
||
let minLineW = this.line.lineWidth / 4;
|
||
let isChangeW = false;
|
||
for (let i = 1; i <= points.length; i++) {
|
||
if (i == points.length) {
|
||
this.ctx.stroke();
|
||
break;
|
||
}
|
||
//最后的一些点线宽变细
|
||
if (i > points.length - changeWidthCount) {
|
||
if (!isChangeW) {
|
||
this.ctx.stroke();//将之前的线条不变的path绘制完
|
||
isChangeW = true;
|
||
if (i > 1 && points[i - 1].isControl)
|
||
continue;
|
||
}
|
||
|
||
//计算线宽
|
||
let w = (lastW - minLineW) / changeWidthCount * (points.length - i) + minLineW;
|
||
points[i - 1].lineWidth = w;
|
||
this.ctx.beginPath();//为了开启新的路径 否则每次stroke 都会把之前的路径在描一遍
|
||
// this.ctx.strokeStyle = "rgba(" + Math.random() * 255 + "," + Math.random() * 255 + "," + Math.random() * 255 + ",0.5)";
|
||
this.ctx.lineWidth = w;
|
||
this.ctx.moveTo(points[i - 1].x, points[i - 1].y);//移动到之前的点
|
||
this.ctx.lineTo(points[i].x, points[i].y);
|
||
this.ctx.stroke();//将之前的线条不变的path绘制完
|
||
} else {
|
||
if (points[i].isControl && points[i + 1]) {
|
||
this.ctx.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y);
|
||
} else if (i >= 1 && points[i - 1].isControl) {//上一个是控制点 当前点已经被绘制
|
||
} else
|
||
this.ctx.lineTo(points[i].x, points[i].y);
|
||
}
|
||
}
|
||
}
|
||
|
||
addPoint(p) {
|
||
if (this.line.points.length >= 1) {
|
||
let last_point = this.line.points[this.line.points.length - 1]
|
||
let distance = this.z_distance(p, last_point);
|
||
if (distance < 10) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (this.line.points.length === 0) {
|
||
this.begin = p;
|
||
p.isControl = true;
|
||
this.pushPoint(p);
|
||
} else {
|
||
this.middle = p;
|
||
let controlPs = this.computeControlPoints(this.k, this.begin, this.middle, null);
|
||
this.pushPoint(controlPs.first);
|
||
this.pushPoint(p);
|
||
p.isControl = true;
|
||
|
||
this.begin = this.middle;
|
||
}
|
||
}
|
||
|
||
pushPoint(p) {
|
||
//排除重复点
|
||
if (this.line.points.length >= 1) {
|
||
let last = this.line.points[this.line.points.length - 1]
|
||
if (last.x === p.x && last.y === p.y) return;
|
||
}
|
||
this.line.points.push(p);
|
||
}
|
||
|
||
computeControlPoints(k, begin, middle, end) {
|
||
if (k > 0.5 || k <= 0)
|
||
return;
|
||
|
||
let diff1 = new Point(middle.x - begin.x, middle.y - begin.y)
|
||
let diff2 = null;
|
||
if (end)
|
||
diff2 = new Point(end.x - middle.x, end.y - middle.y)
|
||
|
||
// let l1 = (diff1.x ** 2 + diff1.y ** 2) ** (1 / 2)
|
||
// let l2 = (diff2.x ** 2 + diff2.y ** 2) ** (1 / 2)
|
||
|
||
let first = new Point(middle.x - (k * diff1.x), middle.y - (k * diff1.y))
|
||
let second = null;
|
||
if (diff2)
|
||
second = new Point(middle.x + (k * diff2.x), middle.y + (k * diff2.y))
|
||
return {first: first, second: second}
|
||
}
|
||
|
||
z_distance(b, e) {
|
||
return Math.sqrt(Math.pow(e.x - b.x, 2) + Math.pow(e.y - b.y, 2));
|
||
}
|
||
|
||
BezierCalculate(poss, precision) {
|
||
|
||
//维度,坐标轴数(二维坐标,三维坐标...)
|
||
let dimersion = 2;
|
||
|
||
//贝塞尔曲线控制点数(阶数)
|
||
let number = poss.length;
|
||
|
||
//控制点数不小于 2 ,至少为二维坐标系
|
||
if (number < 2 || dimersion < 2)
|
||
return null;
|
||
|
||
let result = new Array();
|
||
|
||
//计算杨辉三角
|
||
let mi = new Array();
|
||
mi[0] = mi[1] = 1;
|
||
for (let i = 3; i <= number; i++) {
|
||
|
||
let t = new Array();
|
||
for (let j = 0; j < i - 1; j++) {
|
||
t[j] = mi[j];
|
||
}
|
||
|
||
mi[0] = mi[i - 1] = 1;
|
||
for (let j = 0; j < i - 2; j++) {
|
||
mi[j + 1] = t[j] + t[j + 1];
|
||
}
|
||
}
|
||
|
||
//计算坐标点
|
||
for (let i = 0; i < precision; i++) {
|
||
let t = i / precision;
|
||
let p = new Point(0, 0);
|
||
p.isAdd = true;
|
||
result.push(p);
|
||
for (let j = 0; j < dimersion; j++) {
|
||
let temp = 0.0;
|
||
for (let k = 0; k < number; k++) {
|
||
temp += Math.pow(1 - t, number - k - 1) * (j == 0 ? poss[k].x : poss[k].y) * Math.pow(t, k) * mi[k];
|
||
}
|
||
j == 0 ? p.x = temp : p.y = temp;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
clear() {
|
||
this.line = new Line();
|
||
this.pointLines = new Array();//Line数组
|
||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||
}
|
||
}
|
||
|
||
const worker = await createWorker({
|
||
// logger: m => console.log(m)
|
||
});
|
||
await worker.loadLanguage('eng');
|
||
await worker.initialize('eng');
|
||
await worker.setParameters({
|
||
tessedit_char_whitelist: 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'
|
||
});
|
||
|
||
// alert('好了')
|
||
let lastTime = Date.now()
|
||
let timer = -1
|
||
let checkTime = 400
|
||
|
||
let eventMap = {
|
||
down: '',
|
||
move: '',
|
||
up: '',
|
||
}
|
||
if (isMobile) {
|
||
eventMap = {
|
||
down: 'touchstart',
|
||
move: 'touchmove',
|
||
up: 'touchend',
|
||
}
|
||
} else {
|
||
eventMap = {
|
||
down: 'mousedown',
|
||
move: 'mousemove',
|
||
up: 'mouseup',
|
||
}
|
||
}
|
||
let handwriting = new HandwritingSelf(document.getElementById("canvasId"))
|
||
|
||
window.addEventListener(eventMap.down, (e) => {
|
||
if (Date.now() - lastTime > checkTime) {
|
||
handwriting.clear()
|
||
} else {
|
||
clearTimeout(timer)
|
||
}
|
||
if (e.type === "touchstart")
|
||
handwriting.down(e.touches[0].pageX, e.touches[0].pageY);
|
||
else
|
||
handwriting.down(e.x, e.y);
|
||
})
|
||
|
||
window.addEventListener(eventMap.move, (e) => {
|
||
if (e.type === "touchmove")
|
||
handwriting.move(e.touches[0].pageX, e.touches[0].pageY);
|
||
else
|
||
handwriting.move(e.x, e.y);
|
||
})
|
||
|
||
window.addEventListener(eventMap.up, (e) => {
|
||
if (e.type === "touchend")
|
||
handwriting.up(e.touches[0].pageX, e.touches[0].pageY);
|
||
else
|
||
handwriting.up(e.x, e.y);
|
||
|
||
clearTimeout(timer)
|
||
timer = setTimeout(() => {
|
||
// console.log('识别');
|
||
// handwriting.canvas.toDataURL()
|
||
|
||
// var MIME_TYPE = "image/png";
|
||
// var imgURL = handwriting.canvas.toDataURL(MIME_TYPE);
|
||
// var dlLink = document.createElement('a');
|
||
// dlLink.download = 'fileName.png';
|
||
// dlLink.href = imgURL;
|
||
// dlLink.dataset.downloadurl = [MIME_TYPE, dlLink.download, dlLink.href].join(':');
|
||
//
|
||
// document.body.appendChild(dlLink);
|
||
// dlLink.click();
|
||
// document.body.removeChild(dlLink);
|
||
|
||
(async () => {
|
||
const {data: {text}} = await worker.recognize(handwriting.canvas);
|
||
console.log(text);
|
||
if (isMobile){
|
||
alert(text)
|
||
}
|
||
})();
|
||
}, checkTime)
|
||
|
||
lastTime = Date.now()
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="mobile">
|
||
<canvas id="canvasId"></canvas>
|
||
</div>
|
||
</template>
|
||
<style>
|
||
html, body {
|
||
overflow: hidden;
|
||
}
|
||
</style>
|
||
<style scoped lang="scss">
|
||
|
||
.mobile {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: var(--color-background);
|
||
overflow: hidden;
|
||
|
||
canvas {
|
||
width: 100%;
|
||
height: 100%;
|
||
//border: 1px solid gray;
|
||
}
|
||
}
|
||
|
||
</style>
|