美文网首页
canvas画布中控制点形变元素,包含旋转

canvas画布中控制点形变元素,包含旋转

作者: littlesunn | 来源:发表于2023-08-03 19:41 被阅读0次
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <canvas id="myCanvas" width="1400" height="700"></canvas>
</body>

</html>
<script>

    function vct(start, end) {
        let x = end[0] - start[0];
        let y = end[1] - start[1];
        return [x, y];
    };

    function getVctLen([x, y]) {
        return Math.sqrt(x * x + y * y);
    };

    /**
* 根据点在向量上的比例计算点坐标, [xO, yO]为起点,[xVct, yVct]为向量,k 为该点在向量方向上的长度
* 获取
*/
    function getPntInVct([xO, yO], [xVct, yVct], k) {
        let lenVct = getVctLen([xVct, yVct]);  // 获取向量长度
        let stdVct;
        if (lenVct === 0) {
            stdVct = [0, 0];
        } else {
            stdVct = [xVct / lenVct, yVct / lenVct];   // 单位向量
        }
        return [xO + k * stdVct[0], yO + k * stdVct[1]];
    };

    getRotateAng = function ([x1, y1], [x2, y2]) {
        let EPSILON = 1.0e-8;
        let dist, dot, cross, degree, angle;

        dist = Math.sqrt(x1 * x1 + y1 * y1);
        x1 /= dist;
        y1 /= dist;
        dist = Math.sqrt(x2 * x2 + y2 * y2);
        x2 /= dist;
        y2 /= dist;

        dot = x1 * x2 + y1 * y2;
        if (Math.abs(dot - 1.0) <= EPSILON) {
            angle = 0;
        } else if (Math.abs(dot + 1.0) <= EPSILON) {
            angle = Math.PI;
        } else {
            angle = Math.acos(dot);
            cross = x1 * y2 - x2 * y1;
            if (cross < 0) {
                angle = 2 * Math.PI - angle;
            }
        }
        degree = angle * 180 / Math.PI;
        return degree;
    };


    function getLenOfTwoPnts(p1, p2) {
        return Math.sqrt(Math.pow((p1[0] - p2[0]), 2) + Math.pow((p1[1] - p2[1]), 2));
    };

    /**
     * 获取O点到直线PQ的距离
     */
    getLenOfPntToLine = function (O, P, Q) {
        if ((O[0] == P[0] && O[1] == P[1]) || (P[0] == Q[0] && P[1] == Q[1])) {
            return 0;
        }
        let rotateAng = getRotateAng([O[0] - P[0], O[1] - P[1]], [Q[0] - P[0], Q[1] - P[1]]);
        let len = getLenOfTwoPnts(O, P) * Math.sin(rotateAng * Math.PI / 180);
        len = len < 0 ? -len : len;
        return len;
    };

    /**
     * 向量逆时针旋转angle角度后得到新的向量
     */
    getRotateVct = function (vct, angle) {
        let rad = angle * Math.PI / 180;
        let x1 = Math.cos(rad) * vct[0] - Math.sin(rad) * vct[1];
        let y1 = Math.sin(rad) * vct[0] + Math.cos(rad) * vct[1];
        return [x1, y1];  // 返回的是向量
    };


    // 工具类
    function getRotateVct(vct, angle) {
        let rad = angle * Math.PI / 180;
        let x1 = Math.cos(rad) * vct[0] - Math.sin(rad) * vct[1];
        let y1 = Math.sin(rad) * vct[0] + Math.cos(rad) * vct[1];
        return [x1, y1];  // 返回的是向量
    };

    function getMousePos(myCanvas, e) {
        let downX;
        let downY;
        if (e.x && e.y) {
            downX = e.x;
            downY = e.y;
        } else {
            downX = e.clientX;
            downY = e.clientY;
        }

        let { left, top } = myCanvas.getBoundingClientRect();

        let xDist = downX - left;
        let yDist = downY - top;

        return {
            x: xDist,
            y: yDist,
        };
    }
    // -------------------------------------

    // 常量
    const SOLIDCOLOR = '#CCCCCC70'; // 实线颜色
    const DASHEDCOLOR = '#CCCCCC25'; // 虚线颜色
    const ZEROCOLOR = '#358bf3'; // 0 点坐标系颜色
    const GRIDSIZE = 5;  // 一个正方形网格的宽高大小, 一个GRIDSIZE视为一个单位
    // -------------------------------------

    // 矩形点状元素
    class MyRect {

        constructor(x, y, width, height, angle = 0) {   // 相对坐标
            this.pointList = []
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
            this.angle = angle;
            this.isPointIn = false;
            this.fillStyle = "#69b1ff";
            this.ctrlPnts = []
        }

        addCtrlPnt(pnt) {
            pnt.parentNode = this;
            pnt.angle = this.angle;
            this.ctrlPnts.push(pnt);
        }

        getPointsWithPosAndAngle(x, y, width, height) {
            let O = [x, y];
            let point1 = [x - width / 2, y - height / 2];
            let point2 = [x + width / 2, y - height / 2];
            let point3 = [x + width / 2, y + height / 2];
            let point4 = [x - width / 2, y + height / 2];

            let vctp1 = [point1[0] - O[0], point1[1] - O[1]];
            let vctp2 = [point2[0] - O[0], point2[1] - O[1]];
            let vctp3 = [point3[0] - O[0], point3[1] - O[1]];
            let vctp4 = [point4[0] - O[0], point4[1] - O[1]];

            let rvctp1 = getRotateVct(vctp1, this.angle);
            let rvctp2 = getRotateVct(vctp2, this.angle);
            let rvctp3 = getRotateVct(vctp3, this.angle);
            let rvctp4 = getRotateVct(vctp4, this.angle);

            let pnt1 = [rvctp1[0] + x, rvctp1[1] + y];
            let pnt2 = [rvctp2[0] + x, rvctp2[1] + y];
            let pnt3 = [rvctp3[0] + x, rvctp3[1] + y];
            let pnt4 = [rvctp4[0] + x, rvctp4[1] + y];

            return [
                pnt1,
                pnt2,
                pnt3,
                pnt4,
            ];
        }

        draw(gls, pnts) {  // 真正绘制用像素坐标
            gls.ctx.save();
            gls.ctx.beginPath();
            pnts.forEach((p, i) => {
                let { x: x1, y: y1 } = gls.getRelativePos(p);
                this.ctrlPnts[i].x = x1;
                this.ctrlPnts[i].y = y1;
                if (i == 0) {
                    gls.ctx.moveTo(p.x, p.y);
                } else {
                    gls.ctx.lineTo(p.x, p.y);
                }
            })

            let vct1 = vct([this.x, this.y + 10], [this.x, this.y]);
            let vct11 = getRotateVct(vct1, this.angle)
            let pnt = getPntInVct([this.x, this.y], vct11, this.height / 2 + 20);
            let { x: x2, y: y2 } = { x: pnt[0], y: pnt[1] };
            this.ctrlPnts[4].x = x2;
            this.ctrlPnts[4].y = y2;

            gls.ctx.closePath();
            gls.ctx.fillStyle = this.fillStyle;
            gls.ctx.fill();
            this.setPointIn(gls.ctx);
            gls.ctx.restore();
        }

        setPointIn(ctx) {
            this.isPointIn = ctx.isPointInPath(GridSystem.currentMousePos.x, GridSystem.currentMousePos.y);
            if (!this.isPointIn) {
                this.fillStyle = "#69b1ff";
            } else {
                this.fillStyle = "#003eb3";
            }
        }

        transform() {

        }
    }

    class MyCtrlPnt extends MyRect {

        constructor(name = 'size', x, y, width, height, angle = 0) {
            super(x, y, width, height, angle);
            this.width = 5;
            this.height = 5;
            this.name = name;
            this.ctrlPnts = null;
            this.parentNode = null;
        }

        draw(gls, pnts) {  // 真正绘制用像素坐标
            gls.ctx.save();
            gls.ctx.beginPath();
            // let pnts = this.getPointsWithPosAndAngle(x, y, width, height);
            pnts.forEach((p, i) => {
                if (i == 0) {
                    gls.ctx.moveTo(p.x, p.y);
                } else {
                    gls.ctx.lineTo(p.x, p.y);
                }
            })
            gls.ctx.closePath();
            gls.ctx.fillStyle = this.fillStyle;
            gls.ctx.fill();
            this.setPointIn(gls.ctx);
            gls.ctx.restore();
        }

        setPointIn(ctx) {
            this.isPointIn = ctx.isPointInPath(GridSystem.currentMousePos.x, GridSystem.currentMousePos.y);
            if (!this.isPointIn) {
                this.fillStyle = "#ff4d4f";
            } else {
                this.fillStyle = "#a8071a";
            }
        }
    }

    // 绘制类,主类
    class GridSystem {
        static currentMousePos = {
            x: 0,
            y: 0
        }

        constructor(canvasDom) {
            // 当前 canvas 的 0 0 坐标,我们设置 canvas 左上角顶点为 0 0,向右👉和向下👇是 X Y 轴正方向,0,0 为 pageSlicePos 初始值
            this.pageSlicePos = {
                x: 0,
                y: 0,
            };
            this.scale = 10; // 缩放比例
            this.myCanvas = canvasDom;
            this.ctx = this.myCanvas.getContext('2d');
            this.canvasWidth = this.ctx.canvas.width;
            this.canvasHeight = this.ctx.canvas.height;

            this.hoverNode = null;

            let rect = new MyRect(50, 50, 50, 50, 30);
            rect.addCtrlPnt(new MyCtrlPnt());
            rect.addCtrlPnt(new MyCtrlPnt());
            rect.addCtrlPnt(new MyCtrlPnt());
            rect.addCtrlPnt(new MyCtrlPnt());
            rect.addCtrlPnt(new MyCtrlPnt('angle'));
            this.features = [
                rect,
            ];

            this.firstPageSlicePos = Object.freeze({
                x: this.pageSlicePos.x,
                y: this.pageSlicePos.y
            });  // 记录一开始的的中心点坐标,不会改变的
            this.extent = [750, 800, 750, 800]  // 限制画布拖拽范围: 上右下左,顺时针
            this.initEventListener();

        }


        initEventListener() {
            this.myCanvas.addEventListener("mousemove", this.mouseMove.bind(this));
            this.myCanvas.addEventListener("mousedown", this.mouseDown.bind(this));
            this.myCanvas.addEventListener("mousewheel", this.mouseWheel.bind(this));
        }

        /**
 * 绘制网格
 */
        drawLineGrid = () => {
            this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
            /*获取绘图工具*/
            // 设置网格大小
            var girdSize = this.getPixelSize(GRIDSIZE);
            // 获取Canvas的width、height
            var CanvasWidth = this.ctx.canvas.width;
            var CanvasHeight = this.ctx.canvas.height;

            // 在 pageSlicePos 的 x,y 点位画一个 10 * 10 的红色标记用来表示当前页面的 0 0 坐标
            this.ctx.fillRect(this.pageSlicePos.x, this.pageSlicePos.y, 10, 10); // 效果图红色小方块
            this.ctx.fillStyle = 'red';

            const canvasXHeight = CanvasHeight - this.pageSlicePos.y;
            const canvasYWidth = CanvasWidth - this.pageSlicePos.x;
            // 从 pageSlicePos.y 处开始往 Y 轴正方向画 X 轴网格
            const xPageSliceTotal = Math.ceil(canvasXHeight / girdSize);
            for (let i = 0; i < xPageSliceTotal; i++) {
                this.ctx.beginPath(); // 开启路径,设置不同的样式
                this.ctx.moveTo(0, this.pageSlicePos.y + girdSize * i);
                this.ctx.lineTo(CanvasWidth, this.pageSlicePos.y + girdSize * i);
                this.ctx.strokeStyle = i === 0 ? ZEROCOLOR : (i % 5 === 0 ? SOLIDCOLOR : DASHEDCOLOR); // 如果为 0 则用蓝色标记,取余 5 为实线,其余为比较淡的线
                this.ctx.stroke();
            }

            // 从 pageSlicePos.y 处开始往 Y 轴负方向画 X 轴网格
            const xRemaining = this.pageSlicePos.y;
            const xRemainingTotal = Math.ceil(xRemaining / girdSize);
            for (let i = 0; i < xRemainingTotal; i++) {
                if (i === 0) continue;
                this.ctx.beginPath(); // 开启路径,设置不同的样式
                this.ctx.moveTo(0, this.pageSlicePos.y - girdSize * i); // -0.5是为了解决像素模糊问题
                this.ctx.lineTo(CanvasWidth, this.pageSlicePos.y - girdSize * i);
                this.ctx.strokeStyle = i === 0 ? ZEROCOLOR : (i % 5 === 0 ? SOLIDCOLOR : DASHEDCOLOR);// 如果为 0 则用蓝色标记,取余 5 为实线,其余为比较淡的线
                this.ctx.stroke();
            }

            // 从 pageSlicePos.x 处开始往 X 轴正方向画 Y 轴网格
            const yPageSliceTotal = Math.ceil(canvasYWidth / girdSize); // 计算需要绘画y轴的条数
            for (let j = 0; j < yPageSliceTotal; j++) {
                this.ctx.beginPath(); // 开启路径,设置不同的样式
                this.ctx.moveTo(this.pageSlicePos.x + girdSize * j, 0);
                this.ctx.lineTo(this.pageSlicePos.x + girdSize * j, CanvasHeight);
                this.ctx.strokeStyle = j === 0 ? ZEROCOLOR : (j % 5 === 0 ? SOLIDCOLOR : DASHEDCOLOR);// 如果为 0 则用蓝色标记,取余 5 为实线,其余为比较淡的线
                this.ctx.stroke();
            }

            // 从 pageSlicePos.x 处开始往 X 轴负方向画 Y 轴网格
            const yRemaining = this.pageSlicePos.x;
            const yRemainingTotal = Math.ceil(yRemaining / girdSize);
            for (let j = 0; j < yRemainingTotal; j++) {
                if (j === 0) continue;
                this.ctx.beginPath(); // 开启路径,设置不同的样式
                this.ctx.moveTo(this.pageSlicePos.x - girdSize * j, 0);
                this.ctx.lineTo(this.pageSlicePos.x - girdSize * j, CanvasHeight);
                this.ctx.strokeStyle = j === 0 ? ZEROCOLOR : (j % 5 === 0 ? SOLIDCOLOR : DASHEDCOLOR);// 如果为 0 则用蓝色标记,取余 5 为实线,其余为比较淡的线
                this.ctx.stroke();
            }

            if (this.constructor.name == "GridSystem") {  // 如果是主类
                document.dispatchEvent(new CustomEvent("draw", { detail: this }));
            }
            this.drawFeatures(this.features);
        };


        /**
         * 滚轮缩放倍数
         */
        mouseWheel(e) {
            console.log(e.wheelDelta, " e");
            if (e.wheelDelta > 0) {
                if (this.scale < 10) {
                    this.scale++;
                }
            } else {
                if (this.scale > 1) {
                    this.scale--;
                }
            }
            this.drawLineGrid();
        }

        mouseMove(e) {
            GridSystem.currentMousePos.x = e.clientX;
            GridSystem.currentMousePos.y = e.clientY;
        }

        findHoverNode(features) {
            for (let index = 0; index < features.length; index++) {
                const f = features[index];
                if (f.isPointIn) {
                    return f;
                } else if (f.ctrlPnts) {
                    return this.findHoverNode(f.ctrlPnts)
                }
            }
        }

        /**
         * 拖动 canvas 动态渲染,拖动时,动态设置 pageSlicePos 的值
         * @param e Event
         */
        mouseDown(e) {
            const downX = e.clientX;
            const downY = e.clientY;
            const { x, y } = this.pageSlicePos;
            this.hoverNode = this.findHoverNode(this.features);
            if (this.hoverNode) {
                let fw, fh, fx, fy = 0;
                if (this.hoverNode.constructor.name === "MyCtrlPnt") {
                    fw = this.hoverNode.parentNode.width;
                    fh = this.hoverNode.parentNode.height;
                } else {
                    fx = this.hoverNode.x;
                    fy = this.hoverNode.y;
                }
                this.myCanvas.onmousemove = (ev) => {
                    const moveX = ev.clientX;
                    const moveY = ev.clientY;
                    let { x: x1, y: y1 } = this.getRelativePos({ x: downX, y: downY });
                    let { x: x2, y: y2 } = this.getRelativePos({ x: moveX, y: moveY });
                    if (this.hoverNode.constructor.name === "MyCtrlPnt") {  // 控制点
                        if (this.hoverNode.name == 'size') {
                            // 垂直方向
                            let vct1 = vct([x2, y2 + 100], [x2, y2]);
                            let vct11 = getRotateVct(vct1, this.hoverNode.parentNode.angle)
                            let pnt1 = getPntInVct([x2, y2], vct11, 100);
                            let len1 = getLenOfPntToLine([this.hoverNode.parentNode.x, this.hoverNode.parentNode.y], [x2, y2], pnt1);
                            this.hoverNode.parentNode.width = len1 * 2;
                            // 水平方向
                            let vct2 = vct([x2 + 100, y2], [x2, y2]);
                            let vct22 = getRotateVct(vct2, this.hoverNode.parentNode.angle)
                            let pnt2 = getPntInVct([x2, y2], vct22, 100);
                            let len2 = getLenOfPntToLine([this.hoverNode.parentNode.x, this.hoverNode.parentNode.y], [x2, y2], pnt2);
                            this.hoverNode.parentNode.height = len2 * 2;
                        }
                        if (this.hoverNode.name == 'angle') {
                            let vct1 = [0,-100];
                            let vct2 = vct([this.hoverNode.parentNode.x, this.hoverNode.parentNode.y], [x2, y2]);
                            let angle = getRotateAng(vct1, vct2);
                            this.hoverNode.parentNode.angle = angle;
                            this.hoverNode.parentNode.ctrlPnts.forEach(pnt=>{
                                pnt.angle = angle;
                            })
                        }
                    } else {   // 元素拖拽
                        this.hoverNode.x = fx + (x2 - x1);
                        this.hoverNode.y = fy + (y2 - y1);
                    }

                    this.myCanvas.onmouseup = (en) => {
                        this.myCanvas.onmousemove = null;
                        this.myCanvas.onmouseup = null;
                    };
                }
            } else {
                this.myCanvas.onmousemove = (ev) => {
                    const moveX = ev.clientX;
                    const moveY = ev.clientY;
                    this.pageSlicePos.x = x + (moveX - downX);
                    this.pageSlicePos.y = y + (moveY - downY);
                    this.setPageSliceByExtent(this.extent);
                    this.myCanvas.onmouseup = (en) => {
                        this.myCanvas.onmousemove = null;
                        this.myCanvas.onmouseup = null;
                    };
                }
            }
        }

        setPageSliceByExtent(extent = []) {
            if (extent?.length > 0) { // 限制拖拽范围
                let topExtent = extent[0];
                let rightExtent = extent[1];
                let bottomExtent = extent[2];
                let leftExtent = extent[3];

                if (this.pageSlicePos.x > this.firstPageSlicePos.x + leftExtent) {
                    this.pageSlicePos.x = this.firstPageSlicePos.x + leftExtent;
                }
                if (this.pageSlicePos.x < this.firstPageSlicePos.x - rightExtent) {
                    this.pageSlicePos.x = this.firstPageSlicePos.x - rightExtent;
                }
                if (this.pageSlicePos.y > this.firstPageSlicePos.y + topExtent) {
                    this.pageSlicePos.y = this.firstPageSlicePos.y + topExtent;
                }
                if (this.pageSlicePos.y < this.firstPageSlicePos.y - bottomExtent) {
                    this.pageSlicePos.y = this.firstPageSlicePos.y - bottomExtent;
                }
            }
        }

        getPixelPos(point, block) {
            return {
                x: this.pageSlicePos.x + (point.x / GRIDSIZE) * this.scale,
                y: this.pageSlicePos.y + (point.y / GRIDSIZE) * this.scale,
            };
        }

        getRelativePos(point, block) {
            return {
                x: ((point.x - this.pageSlicePos.x) / this.scale) * GRIDSIZE,
                y: ((point.y - this.pageSlicePos.y) / this.scale) * GRIDSIZE,
            };
        }

        getPixelSize(size) {
            return size * this.scale;
        }

        getRelativeSize(size) {
            return size / this.scale;
        }

        getPixelPosAndWH(block) {  // 元素的像素坐标和大小
            let { x, y } = this.getPixelPos(block, block);
            var width = this.getPixelSize(block.width, block);
            var height = this.getPixelSize(block.height, block);
            return { x, y, width, height, }
        }

        getRelativePosAndWH(x1, y1, width1, height1, block) {  // 元素的绝对坐标和大小
            let { x, y } = this.getRelativePos(
                { x: x1, y: y1 }, block
            );
            let width = this.getRelativeSize(width1, block)
            let height = this.getRelativeSize(height1, block)
            return { x, y, width, height }
        }

        drawFeatures(features) {
            features.forEach(f => {
                let pnts = f.getPointsWithPosAndAngle(f.x, f.y, f.width, f.height);
                let newPnts = []
                pnts.forEach(p => {
                    newPnts.push(this.getPixelPos({ x: p[0], y: p[1] }));
                });
                f.draw(this, newPnts);
                if (f.ctrlPnts) {
                    this.drawFeatures(f.ctrlPnts)
                }
            })
        }
    }

    let gls = new GridSystem(document.querySelector('#myCanvas'));

    (function main() {
        gls.drawLineGrid();
        requestAnimationFrame(main);
    })()

</script>
<style>
    body {
        margin: 0;
    }
</style>

相关文章

  • Canvas2D画线和面

    我们知道canvas是画布,今天我们就来画布上面画线和面。 1.Html中的画布 canvas是HTML5中的元素...

  • HTML5 Canvas 完整测试 - canvas 标签

    在 html5 文档内创建 canvas 画布: “画布”(canvas) 是 html5 中独有的元素,通过它可...

  • Unity UGUI系列一 Canvas 和 Canvas Gr

    Canvas(画布)是承载所有UI元素的区域。所有的UI元素都必须是Canvas的子对象。如果场景中没有画布,那么...

  • 初识canvas(画布)

    canvas(画布) 1、 HTML5提供的新元素2、 Canvas在HTML页面提供画布的功能,可以在页面中绘制...

  • 使用 canvas 绘图的几种方法

    canvas 标签 要使用 canvas 绘图,需要现在HTML中定义一个画布。 canvas 元素本身没有绘图能...

  • Unity-UGUI

    UGUI与NGUI区别 画布Canvas 画布是摆放容纳所有的UI元素的区域,所有的UI元素需要在Canvas上组...

  • HTML5新特性1(画布,媒体,地理位置,Web worker)

    HTML画布 canvas canvas 元素用于在网页上绘制图形。canvas 元素本身是没有绘图能力的。所有的...

  • HTML5 Canvas

    一、添加一个 Canvas 1.布置画布:通过添加标签,添加canvas元素 Canvas在HTM...

  • 教你使用Canvas处理图片

    Canvas,中文译为“画布”,HTML5中新增了 元素,可以结合JavaScript动态地在画布中绘制图形。 今...

  • canvas绘图详解(一)

    一、canvas绘图环境 该浏览器不支持canvas元素 在 中添加这段代码相当于创建了一块画布,要使用 元素,...

网友评论

      本文标题:canvas画布中控制点形变元素,包含旋转

      本文链接:https://www.haomeiwen.com/subject/vanmpdtx.html