【数学篇】05 # 如何用向量和坐标系描述点和线段?

简介: 【数学篇】05 # 如何用向量和坐标系描述点和线段?

说明

【跟月影学可视化】学习笔记。



坐标系与坐标映射


   HTML:采用的是窗口坐标系,以参考对象(参考对象通常是最接近图形元素的 position 非 static 的元素)的元素盒子左上角为坐标原点,x 轴向右,y 轴向下,坐标值对应像素值。


   SVG:采用的是视区盒子(viewBox)坐标系。这个坐标系在默认情况下,是以 svg 根元素左上角为坐标原点,x 轴向右,y 轴向下,svg 根元素右下角坐标为它的像素宽高值。如果我们设置了 viewBox 属性,那么 svg 根元素左上角为 viewBox 的前两个值,右下角为 viewBox 的后两个值。


   Canvas:采用的坐标系默认以画布左上角为坐标原点,右下角坐标值为 Canvas 的画布宽高值。


   WebGL:是一个三维坐标系。它默认以画布正中间为坐标原点,x 轴朝右,y 轴朝上,z 轴朝外,x 轴、y 轴在画布中范围是 -1 到 1。


上面4个都属于直角坐标系。


   直角坐标系特性:不管原点和轴的方向怎么变,用同样的方法绘制几何图形,它们的形状和相对位置都不变。


转换坐标系:


   HTML、SVG 和 Canvas 都提供了 transform 的 API 转换坐标系。

   WebGL 本身不提供 tranform 的 API,但可以在 shader 里做矩阵运算来实现坐标转换。



如何用 Canvas 实现坐标系转换?


以一个例子为例:在宽 512 * 高 256 的一个 Canvas 画布上实现如下的视觉效果。其中,山的高度是 100,底边 200,山是等腰三角形,两座山的中心位置到中线的距离都是 80,太阳的圆心高度是 150。可以使用一个 Rough.js:https://github.com/rough-stuff/rough的库,绘制一个手绘风格的图像。

fdaaab7b2d8d42909ca3d43485bcb321.png



方法一:不转换坐标系

首先我们需要计算出来三角形各个顶点的坐标

e66732c6ddfb444dabec4a5cf053301e.png


然后绘制

<!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>不转换坐标系</title>
    <style>
        canvas {
            border: 1px dashed salmon;
        }
    </style>
</head>
<body>
    <canvas width="512" height="256"></canvas>
    <script src="https://lib.baomitu.com/rough.js/3.1.0/rough.umd.js"></script>
    <script>
        const rc = rough.canvas(document.querySelector('canvas'));
        const hillOpts = {
            roughness: 2.8,
            strokeWidth: 2,
            fill: 'cyan'
        };
        rc.path('M76 256 L176 156 L276 256', hillOpts);
        rc.path('M236 256 L336 156 L436 256', hillOpts);
        rc.circle(256, 106, 105, {
            stroke: 'red',
            strokeWidth: 4,
            fill: 'salmon',
            fillStyle: 'solid',
        });
    </script>
</body>
</html>


效果如下:

9142a3c552784e6c8d31d41f01f6a1dc.png



方法二:转换坐标系

以画布底边中点为原点,x 轴向右,y 轴向上的坐标系,相对来说转换之后的坐标系计算的坐标点简单清晰一些:

// 以画布底边中点为原点
ctx.translate(256, 256);
// x 轴向右,y 轴向上的坐标系
ctx.scale(1, -1);


330ad974692245ab9437194defb9b3f4.png

<!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>转换坐标系</title>
    <style>
        canvas {
            border: 1px dashed salmon;
        }
    </style>
</head>
<body>
    <canvas width="512" height="512"></canvas>
    <script src="https://lib.baomitu.com/rough.js/3.1.0/rough.umd.js"></script>
    <script>
        const rc = rough.canvas(document.querySelector('canvas'));
        console.log(rc)
        const ctx = rc.ctx;
        // 以画布底边中点为原点
        ctx.translate(256, 256);
        // x 轴向右,y 轴向上的坐标系
        ctx.scale(1, -1);
        const hillOpts = {
            roughness: 2.8,
            strokeWidth: 2,
            fill: 'gray'
        };
        // //线条宽度
        // ctx.lineWidth = 2;
        // ctx.fillStyle = 'orange';
        // //线条颜色填充
        // ctx.strokeStyle = 'black';
        // //开启绘画路径
        // ctx.beginPath();
        // //画笔初始化点
        // ctx.moveTo(-180, 0);
        // // 画笔目标位置
        // ctx.lineTo(-80, 100);
        // // 连接路径
        // ctx.stroke();
        // ctx.lineTo(20, 0);
        // ctx.stroke();
        // ctx.closePath(); //闭合线路(首尾坐标)
        // ctx.stroke(); //连接首尾
        // ctx.fill();
        rc.path('M-180 0 L-80 100 L20 0', hillOpts);
        rc.path('M-20 0 L80 100 L180 0', hillOpts);
        rc.circle(0, 150, 105, {
            stroke: 'salmon',
            strokeWidth: 4,
            fill: 'gold',
            fillStyle: 'solid',
        });
    </script>
</body>
</html>


实现的效果如下,这里我有个疑问就是为什么y轴左边的这个三角形没有填充到颜色,我试了一下canvas原生的代码是可以填充的,有点搞不懂,知道的大佬还请指导一下,在此先感谢。

31c50f7d66454eff88e4178128e31716.png


如何用向量来描述点和线段?

可以用二维向量来表示这个平面上的点和线段。二维向量其实就是一个包含了两个数值的数组,一个是 x 坐标值,一个是 y 坐标值。


baee467accce4474a78b1e6aac14d996.png


  • 向量和标量一样可以进行数学运算。
  • 一个向量包含有长度和方向信息。



向量运算的意义

向量运算的意义并不仅仅只是用来算点的位置和构造线段,可视化呈现依赖于计算机图形学,而向量运算是整个计算机图形学的数学基础。



实战演练:用向量绘制一棵树

需要实现的效果如下:


6ccaac7389d94e4384b786c8c3bb59dc.png



二维旋转矩阵与向量旋转基本思想:处于某二维空间中的任意向量,可以通过标准正交基来表示。通俗来讲,就是用坐标系来表示。不过表示这个向量的不是x轴和y轴坐标,而是二维的基向量。我们可以联想一下物理中的静止参考系和动参考系。动静参考系在这里对应于动静坐标系。向量旋转的同时,动坐标系是相对于这个向量不动的,相对于静止坐标系则旋转同样的角度。只要知道旋转后动坐标系中的标准正交基在静止坐标系中的表达,就能知道旋转后的向量在静止坐标系中的表达。


新建文件 vector2d.js 实现 Vector2D

export class Vector2D extends Array {
    constructor(x = 1, y = 0) {
        super(x, y);
    }
    set x(v) {
        this[0] = v;
    }
    set y(v) {
        this[1] = v;
    }
    get x() {
        return this[0];
    }
    get y() {
        return this[1];
    }
    get length() {
        return Math.hypot(this.x, this.y);
    }
    get dir() {
        return Math.atan2(this.y, this.x);
    }
    copy() {
        return new Vector2D(this.x, this.y);
    }
    add(v) {
        this.x += v.x;
        this.y += v.y;
        return this;
    }
    sub(v) {
        this.x -= v.x;
        this.y -= v.y;
        return this;
    }
    scale(a) {
        this.x *= a;
        this.y *= a;
        return this;
    }
    cross(v) {
        return this.x * v.y - v.x * this.y;
    }
    dot(v) {
        return this.x * v.x + v.y * this.y;
    }
    normalize() {
        return this.scale(1 / this.length);
    }
    rotate(rad) {
        const c = Math.cos(rad),
            s = Math.sin(rad);
        const [x, y] = this;
        this.x = x * c + y * -s;
        this.y = x * s + y * c;
        return this;
    }
}


代码实现如下:

<!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>用向量绘制一棵树</title>
    <style>
        canvas {
            border: 1px dashed salmon;
        }
    </style>
</head>
<body>
    <canvas width="640" height="512"></canvas>
    <script type="module">
        import {Vector2D} from './common/lib/vector2d.js';
        const canvas = document.querySelector('canvas');
        const ctx = canvas.getContext('2d');
        // 以画布左下角为原点
        ctx.translate(0, canvas.height);
        // x 轴向右,y 轴向上的坐标系
        ctx.scale(1, -1);
        ctx.lineCap = 'round';
        /**
         * 画树枝的函数
         * context 是 Canvas2D 上下文
         *   v0 是起始向量
         *   length 是当前树枝的长度
         *   thickness 是当前树枝的粗细
         *   dir 是当前树枝的方向,用与 x 轴的夹角表示,单位是弧度。
         *   bias 是一个随机偏向因子,用来让树枝的朝向有一定的随机性
         * */ 
        function drawBranch(context, v0, length, thickness, dir, bias) {
            // 计算出树枝的终点坐标;创建一个单位向量 (1, 0),它是一个朝向 x 轴,长度为 1 的向量。然后旋转 dir 弧度,再乘以树枝长度 length。
            const v = new Vector2D().rotate(dir).scale(length);
            const v1 = v0.copy().add(v);
            // 绘制一个固定方向的树枝为根部
            context.lineWidth = thickness;
            context.beginPath();
            context.moveTo(...v0);
            context.lineTo(...v1);
            context.stroke();
            // 从一个起始角度开始递归地旋转树枝,每次将树枝分叉成左右两个分枝        
            if(thickness > 2) {
                const left = Math.PI / 4 + 0.5 * (dir + 0.2) + bias * (Math.random() - 0.5);
                drawBranch(context, v1, length * 0.9, thickness * 0.8, left, bias * 0.9);
                const right = Math.PI / 4 + 0.5 * (dir - 0.2) + bias * (Math.random() - 0.5);
                drawBranch(context, v1, length * 0.9, thickness * 0.8, right, bias * 0.9);
            }
            // 随机绘制花瓣
            if(thickness < 5 && Math.random() < 0.3) {
                context.save();
                context.strokeStyle = '#c72c35';
                const th = Math.random() * 6 + 3;
                context.lineWidth = th;
                context.beginPath();
                context.moveTo(...v1);
                context.lineTo(v1.x, v1.y - 2);
                context.stroke();
                context.restore();
            }
        }
        // 在(256, 0)位置绘制
        const v0 = new Vector2D(256, 0);
        drawBranch(ctx, v0, 50, 10, 1, 3);
    </script>
</body>
</html>


024e2440ce52404d875331dddf41e49f.png

目录
相关文章
【UI】 element -ui select下拉框label显示多个值
【UI】 element -ui select下拉框label显示多个值
483 1
|
前端开发 JavaScript Java
java常用数据判空、比较和类型转换
本文介绍了Java开发中常见的数据处理技巧,包括数据判空、数据比较和类型转换。详细讲解了字符串、Integer、对象、List、Map、Set及数组的判空方法,推荐使用工具类如StringUtils、Objects等。同时,讨论了基本数据类型与引用数据类型的比较方法,以及自动类型转换和强制类型转换的规则。最后,提供了数值类型与字符串互相转换的具体示例。
658 3
|
9月前
|
存储 人工智能 监控
Mahilo:多智能体实时协作框架开源!人类与AI无缝交互,复杂任务一键协同
Mahilo 是一个灵活的多智能体框架,支持创建与人类互动的多智能体系统,适用于从客户服务到紧急响应等多种场景。
685 2
Mahilo:多智能体实时协作框架开源!人类与AI无缝交互,复杂任务一键协同
|
9月前
|
存储 JavaScript
(ERP系统查看DWG)MxCAD APP调用内部弹框的方法
MxCAD APP 二次开发提供了调用项目内部弹框的接口,以保持样式统一。用户需创建 `test_dialog` 文件夹并依次创建 `dialog.ts`、`dialog.vue` 和 `index.ts` 文件来注册、构建和渲染弹框。通过 `useDialogIsShow` 钩子函数控制弹框显示,并可在方法中直接调用 `dialog.showDialog()` 来控制弹框显隐。此外,还支持监听确认或取消事件获取数据,以及通过配置 `vite.config.ts` 解决样式冲突问题。最终在 `src/index.ts` 中引入相关文件即可实现弹框功能。
|
C#
c#中switch case语句的用法
C#中的 `switch case`语句提供了一种简洁而高效的方式来处理多个条件分支。通过了解其基本语法、注意事项和高级用法,可以在实际开发中灵活运用 `switch case`,提高代码的可读性和维护性。希望本文能帮助你更好地理解和使用C#中的 `switch case`语句。
912 0
|
机器人
ROS2教程 04 话题Topic
本文是关于ROS2(机器人操作系统2)中话题(Topic)机制的教程,详细介绍了ROS2中话题的命令使用,包括列出、回显、发布、信息查询、类型查询等功能,并通过示例代码展示了如何创建发布者(Publisher)和订阅者(Subscriber)节点,以及如何测试发布-话题-订阅通信。
1908 1
ROS2教程 04 话题Topic
|
监控 程序员 持续交付
`pylint`是一个高度可配置的Python代码分析工具,它可以帮助程序员查找代码中的错误、样式问题、可能的bug以及不符合编码标准的部分。
`pylint`是一个高度可配置的Python代码分析工具,它可以帮助程序员查找代码中的错误、样式问题、可能的bug以及不符合编码标准的部分。
|
缓存 网络协议 Shell
ADB各种操作指令详解大汇总
这篇文章提供了ADB(Android Debug Bridge)的详细操作指令汇总,包括设备管理、应用操作、日志查看、文件操作、屏幕截取与录制、Shell命令使用等。
2339 2
|
开发工具 git
成功解决:Svnion not found. installat
这篇文章分享了作者在使用VSCode进行SVN版本控制时遇到的一个问题,即SVN插件提示找不到`svn.exe`的问题。原因是在安装SVN时没有选择客户端工具,导致没有`svn.exe`文件。文章提供了解决方案,包括重新安装SVN时选择客户端工具,并在VSCode的`setting.json`文件中配置SVN的路径。
成功解决:Svnion not found. installat
|
前端开发 JavaScript
Vue 中 Promise 的then方法异步使用及async/await 异步使用总结
Vue 中 Promise 的then方法异步使用及async/await 异步使用总结
523 1