XXHolic

XXHolic 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 github.com/XXHolic 编辑
编辑

个人动态

XXHolic 发布了文章 · 12月2日

前端异常示例

引子

前端异常类型及捕获方式 之后,尝试了自己去封装一下,然后去看了 sentry 的源码,发现之前的那篇只是一个概括,当采集的时候,需要更加细致的解析,接下来看看各种异常具体是什么样的。

基础知识点

前端异常一定会接触到 Error 对象,先来简单的了解一下。

错误的类型:

  • Error : 基类型,其它错误类型都是继承自该类型。
  • EvalError : 使用 eval() 函数而发生异常时抛出。
  • RangeError : 数值超出相应有效范围时的异常。
  • ReferenceError : 访问无效的引用的异常。
  • SyntaxError : 语法错误的异常。
  • TypeError : 变量或参数不是一个有效类型的异常。
  • URIError : encodeURI()decodeURI() 传入无效参数的异常。
  • AggregateError : 一个操作导致多个异常需要报告,例如 Promise.any()
  • InternalError : JavaScript 引擎内部抛出的错误,这个还没有标准化。

实例共有的标准属性:

  • message : 异常信息。
  • name : 异常名称。

实例共有的非标准属性:

  • description : 微软的非标准属性,类似 message 。
  • number : 微软的非标准属性,异常数字。
  • fileName : Mozilla 的非标准属性,异常产生所在文件的路径。
  • lineNumber : Mozilla 的非标准属性,异常产生所在文件的行数。
  • columnNumber : Mozilla 的非标准属性,异常产生所在文件的列数。
  • stack : Mozilla 的非标准属性,堆栈跟踪。

更多异常相关的信息见 ecma-262 ErrorWebIDL Exceptions

下面看看每种类型的示例。(暂没有考虑框架)

以下示例环境:

  • Chrome :86.0.4240.198(正式版本) (x86_64)。
  • 使用 nginx 启动了一个本地服务,原生 js 。
  • 为了方便查看更多信息,使用了 try-catch、onerror、onunhandledrejection 。

EvalError

EvalError 已不再被 JavaScript 抛出,现在该对象为了保持兼容性存在。用下面的方式可以看到这种异常:

try {
  throw new EvalError('Hello, EvalError');
} catch (e) {
  console.log(e instanceof EvalError); // true
  console.log(e.message);              // "Hello, EvalError"
  console.log(e.name);                 // "EvalError"
}

查一些资料说下面方式就会抛出该类型异常,但试了一下抛出的异常是 TypeError

new eval();

RangeError

const arr = new Array(-10)

67-range-error

ReferenceError

let a = undefinedVariable

67-reference-error

SyntaxError

eval('hello syntax')

67-syntax-error

有些语法错误是无法捕获的,因为可能导致整个程序无法正常运行,不过这类型大多在编写阶段就会很容易发现。

const a++;

TypeError

const a = 'hell';
a.fun();

67-type-error

URIError

decodeURIComponent('%')

67-uri-error

AggregateError

Promise.any([
  Promise.reject(new Error("some error")),
]).catch(e => {
  throw e; // 这里抛出,让 onunhandledrejection 接收会有详细的异常信息
});

67-aggregate-error

DOMException

const node = document.querySelector('#demo'); // 这个要存在
const refnode = node.nextSibling;
const newnode = document.createTextNode('异常');
node.insertBefore(newnode, refnode);

67-dom-exception

DOMError

已经不推荐使用了,但一些浏览器还是兼容了这个。

const err = new DOMError('DOMError');
throw err;

67-dom-error

ErrorEvent

const err = new ErrorEvent('ErrorEvent');
throw err; // onerror 会捕获到

67-error-event

资源加载异常

常见的 linkscriptimg 标签加载异常都是这样类似的信息:

67-static-error

接口请求异常

基础 XMLHttpRequest 示例:

const xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:6677/index");
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    if (xhr.status == 200) {
      console.info('success')
    } else {
      console.info('xhr error:',xhr)
    }
  }
};
xhr.send();

67-xhr-error

基础 fetch 示例:

fetch("http://localhost:6677/index").then((res) => {
  if (!res.ok) {
    throw res;
  }
}).catch((e) => {
  console.info('fetch error:',e);
})

67-fetch-error

Script error.

这个本地(浏览器直接文件路径访问)很容易出现,例如点击按钮的时候,抛出了一个异常,onerror 也捕获到了,但什么信息也没有:

67-script-error

换成 try-catch 捕获,有了一些信息:

67-try-script-error

这个应该是一个对象,用对象的形式打印一下:

67-error-object

这样就可以拿到相关的信息进行解析了,可参考 TraceKit

区分

看到上面的示例,有好几种形式的异常,进行处理的时候,区分判断的依据是什么?

思路 1

比较简单的做法,就是不用区分,既然都是异常,整个全都上报,在后台人工查看一样可以达到异常分析排查的目的。

思路 2

通过检查特定字段,或者字段的组合来判断区分。针对特定的异常可以做到,所有的就不太清楚了。

思路 3

在看 sentry 源码的时候,发现了另外一种思路:根据异常的类型来区分

使用的主要方法是:

function getType(value) {
  return Object.prototype.toString.call(value);
}

按照这个思路,异常类型有:

  • [object Error] : 属于这种类型的异常有 RangeErrorReferenceErrorSyntaxErrorTypeErrorURIError
  • [object Exception] : 这个没找到,但 sentry 里面有写。
  • [object DOMException] : 属于这种类型的异常有 DOMException
  • [object DOMError] : 属于这种类型的异常有 DOMError
  • [object ErrorEvent] : 属于这种类型的异常有 ErrorEvent
  • [object PromiseRejectionEvent] : 这个有些特殊,当上面的 AggregateError 在 catch 里面检查类型时属于 [object Error] ,如果 throw 时,在 onunhandledrejection 里面类型变成了 [object PromiseRejectionEvent]

这里大概的写一下 sentry 的区分逻辑:

const exception = 'some value';
let format;
if (isErrorEvent(exception) && exception.error) {
  format = doSomething1(exception);
  return format;
}

if (isDOMError(exception) || isDOMException(exception)) {
  format = doSomething2(exception);
  return format;
}

// isError 包含的类型有 [object Error] [object Exception] [object DOMException] ,以及继承自 Error 对象的自定义对象。
if (isError(exception)) {
  format = doSomething3(exception);
  return format;
}

// isPlainObject 检查类型是 [object Object],isEvent检查的是 wat instanceof Event
if (isPlainObject(exception) || isEvent(exception)) {
  format = doSomething4(exception);
  return format;
}

format = doSomething5(exception);

return format;

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 11月23日

本地包调试 :npm link

引子

再次碰到调试 npm 本地包的情况,一时想不起来,看了下文档,实际操作了一下,发现了一些文档上没明写的东西。

介绍

根据文档介绍,包的链接分为两步。

第一步

在一个包文件夹内执行 npm link 将在全局文件 {prefix}/lib/node_modules/<package> 内,创建一个符号链接(symlink),这个链接指向 npm link 命令执行的地方。

第二步

到其它目录下,执行 npm link packageName 命令,将会创建一个从全局安装的 packageName 到当前文件内的 node_modules 下的符号链接。

需要注意的的是, packageName 是取自包的 package.jsonname 字段,不是文件夹名称。

包的名称可能有作用域前缀,如果有, packageName 也要对应加上。

实践

基于文档,结合实际的操作,对照看下产生的效果。

运行环境

  • 项目是基于 webpack 简单配置,本地运行的 server 。
  • node 使用 nvm 管理的。
  • 项目已引用了包,需在基础上进行修改调试。
  • 系统是 macOS 。

操作

首先在包根目录下面执行 npm link 命令,出现下面的提示:

79-link

到对应的目录下,发现生成了提示中所说的文件,就是文档中所说符号链接(symlink):

79-result

试着改了一下本地的源文件,发现全局包里面对应的文件内容也跟着变化。

然后到项目中执行 npm link packageName 指令,出现了下面的提示:

79-link-package

到 node_modules 下发现对应的依赖包已经发生了变化:

79-link-module

这里的包跟全局那个生成的包是一样的,包更新了,本地看没什么效果,原因是本地的 sever 有缓存,需要重新启动一下。注意这个文件夹图标多了一个箭头的标记,未 link 之前没有这个。

重启服务后,到源库修改源码,发现项目 node_modules 下同步了修改的内容,webpack 也检测到变化,自动刷新。

修改好后,想要恢复到原本的包,解除 link 该怎么做?很奇怪,在 npm 官方文档上没找到说明。

解除 link

到项目下执行下面的命令:

npm unlink --no-save package && npm install

npm uninstall 文档中可以发现,unlink 其实是 uninstall 的别名,实质上也是删除了包。

包不需要的 link 的时候,建议也解除,到包目录下执行下面的命令:

npm unlink

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 11月16日

npm 发布包遇到的问题

引子

虽然有发布过包的经历,但没有发布过自己的包,于是就参照 npm developer guide 尝试了一下,有了下面的收获。

403 Forbidden

按照文档中的步骤,注册登录后,准备好了库,然后执行 npm publish,报了下面的错误:

Error: 403 Forbidden - PUT https://registry.npmjs.org/xx - You do not have permission to publish "xx". Are you logged in as the correct user?

查询资料后,发现了这个 issue,原来是因为公开的库里面,已经有了同名的库。由于是免费的账号,所以想在 npm 上发包,要么换个名称,要么花钱创建私有包。还有一种方式就是在内网搭建自己的服务。

Error: 402 Payment Required

换了个名字,想起了自己见过的库,就模仿加了个 @ 前缀的包,再次发布的时候,报了下面的错误:

Error: 402 Payment Required - PUT https://registry.npmjs.org/@x... - You must sign up for private packages

查看文档,发现 @npm/package-name 这种形式的包名,是有作用域的包名形式,执行 npm publish 的时候默认是发布私有的包。因此,第一种方式是花钱买私有包的服务,另外一种方式就是指定参数,表示公开:

npm publish --access public

需要注意的是这种形式的包名跟 npm 账户有对应关系,不能随便填写。

npm init --scope=@my-org

这种形式表示是一个组织,my-org 对应是 npm 中的组织名。

npm init --scope=@my-username

这种形式表示是个人,my-username 对应是 npm 中的用户名。

对旧包的处理

在尝试的过程中,发布了一些包,想着只是测试的包,想要移除掉,但 npm 官方很不推荐这种做法,推荐用 npm deprecate 指令,告知安装者相关信息,例如“我不再维护这个库了,请不要使用”。

npm deprecate package-name "This is test package, do not use it!"

其中 package-name 是指发布的包的名称,可能跟库的名称不一样。

发布成功了,但在 npm 上找不到包

在一次发布中,看到发布成功的提示,但到 npm 账户发现没有相关的包。

反复尝试了几次,发现原来是因为自己本地 registry 指向了内网的一个地址,并没有指向 npm 官方的地址 https://registry.npmjs.org/

所以发包的时候,先确定一下发布的指向:

npm config get registry
或
npm config list

如果发现指向不对,有两种修改的方式:
方式 1:使用 npm config set registry 改成对应的指向,但这个是全局修改,之后如果需要发布其它指向的包,又要修改回来。

方式 2:执行指令时添加 --registry 参数
package.json 中添加参数:

"publishConfig": { "registry": "https://npm.pkg.github.com/" }

登录的时候,添加对应的参数:

npm login --registry=https://registry.npmjs.org/

在使用 npm deprecate 指令的时候,如果本地 registry 指向不对,也要添加 --registry 参数才会有效。

如何判断 npm 账户是否已登录

由于发布的包有不同的 registry 指向,在发布包的时候,登录要指向不同的源,所以发布之前想要看看是否已登录。

查看了文档,没有找到查看登录状态的特定命令,但可以使用能触发登录检查的命令,从侧面看是否已登录。

很容易想到的就是 npm login 命令,但登录过一次后,再执行时还是提示输入账号密码。

后来发现了一个简单的指令 npm whoami ,这个指令只能在登录状态下执行,没有登录就会提示:

npm ERR! code ENEEDAUTH

npm ERR! need auth This command requires you to be logged in.

npm ERR! need auth You need to authorize this machine using npm adduser

需要注意的是,这指令不带 registry 参数时,会指向 npm 默认的 registry ,如果这个不匹配,也是没有效果的。

当有多个 registry 时,一定要注意,不少的指令都需要一致的 registry 才有效。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 11月9日

【译】npm developer guide

引子

npm Getting started 之后,发现了 npm-developers 这篇文档,介绍从开始、本地测试、打包到发布的一个相对完整的流程。感觉是 Getting started 的一个补充,于是就把篇文档翻译成中文,加深下印象。

原文:npm-developers

简述

所以,你已经打算使用 npm 来开发(并且可能发布或部署)你的项目了。

太棒了!

在你的用户安装程序的简单步骤之上,您需要做一些事情。

关于这些文档

这些是手册页。如果你安装了 npm,你应该可以在 npm 手册中找到特定主题的文档,或者通过 npm 帮助来获取相同的信息。

一个 package 是什么

一个 package 是:

1. 一个包含了描述程序的 package.json 文件的文件夹。
2. 包含有一个 gzip 压缩的压缩包(在 1 的基础上)。
3. 一个解析为(2)中包的 url。
4. 一个已经在登记处发布的 <name>@<version>(在 3 的基础上)。
5. 一个指向的(4)的 <name>@<tag>。
6. 一个满意的有 “latest” 标签的 <name>(在 5 的基础上)。
7. 一个克隆的 git 仓库链接,指向(1)中的文件夹。

即使你从来没有发布过你的包,如果你只是想写一个 node 项目,或者在打包成压缩包后,你想在其它地方能很容易的安装,你仍然可以从使用 npm 中受益匪浅。

Git 的链接可是下面的一些形式:

git://github.com/user/project.git#commit-ish
git+ssh://user@hostname:project.git#commit-ish
git+http://user@hostname/project/blah.git#commit-ish
git+https://user@hostname/project/blah.git#commit-ish

commit-ish 可以是任何的 tag、sha 或者能够提供给 git checkout 作为参数的分支名。默认值是 master

package.json 文件

你需要在项目根目录下有个 package.json 文件,这样才能用 npm 做很多事情。这基本上就是整个交接口。

详细的文件信息,可见 package.json。你至少需要知道的有:

  • name:这个应该是标识你项目的字符串。请不要使用 name 来指明这个项目是基于 node 或者使用了 JavaScript。你可以用 “engines” 字段来表明你项目,需要什么版本的 node(或者其它什么),它几乎假设是 JavaScript。
    也不需要匹配你的 github 库的名称。
    因此,node-foobar-js 是不好的命名。foobar 的命名会更好。
  • version:semver 兼容版本。
  • engines:指明你的项目基于那个版本 node(或其它什么) 运行。node API 改变很频繁,你依赖的版本,可能有一些 bug 或新的功能。明确的指出来。
  • author:作者。
  • scripts:如果你有特定的编译或安装脚本,那么你应该把它放到 scripts 对象中。你应该至少有一个基本的 smoke test 命令作为 “scripts.test” 字段。见 npm-scripts
  • main:如果你有一个单独的模块作为你程序的入口(就像 “foo” 包通过 requre("foo") 获取),那么你需要在 “main” 字段指明。
  • directories:这是一个映射名称到文件夹的对象。最好包括 “lib” 和 “doc”,但是如果你使用 “man” 指定一个满是手册页的文件夹,它们将会像安装这些手册页一样进行安装。

你可以在你项目的根目录下,使用 npm init 初始化基本的 package.json 文件。更多信息见 npm-init

过滤文件

使用 .npmignore 文件排除包中的一些东西。如果没有 .npmignore 文件,但有 .gitignore 文件,那么 npm 将忽略该文件中匹配到的东西。如果你想包含你 .gitignore 文件中排除的文件,你可以创建一个空的 .npmignore 文件覆盖它。与 git 一样,npm 在包的所有子目录中查找 .npmignore.gitignore 文件,而不仅仅是根目录。

.npmignore.gitignore 遵循相同的规则模式:

  • # 开始的行会被忽略。
  • 标准的全局模式有效。
  • 你可以使用正斜杠结束模式 / 指定目录。
  • 你可以通过使用感叹号 !来否定一个模式。

默认的,下面的路径和文件会被忽略,因此不需要额外将这些添加到 .npmignore 中:

  • .*.swp
  • ._*
  • .DS_Store
  • .git
  • .hg
  • .npmrc
  • .lock-wscript
  • .svn
  • .wafpickle-*
  • config.gypi
  • CVS
  • npm-debug.log

此外,node_module 中所有的东西会被忽略,除了绑定的依赖。npm 会自动为你处理这个。所以不必将 node_module 加入到 .npmignore 中。

下面的路径和文件绝不会被忽略,所以把这些加入到 .npmignore 中没有意义:

  • package.json
  • README (and its variants)
  • CHANGELOG (and its variants)
  • LICENSE / LICENCE

如果考虑到项目的结构,你发现 .npmignore 是一个维护难题,那么可以尝试填充 package.json 文件的 files 属性,该属性是一个数组,里面是应该包含在包中的文件或目录名。有时候白名单比黑名单更容易管理。

测试你的 .npmignore 或文件配置是否有效

如果你想要再次确认,当发布时,你想要的都包含在包中,你可以本地运行 npm pack 命令,这个命令将在工作目录生成一个压缩包,发布包也是使用这种方式。

Link 包

npm link 旨在安装一个开发包并实时查看更改,而不用再次重新安装。(当然,你需要重新 link 或 npm rebuidl -g 来更新编译的包。)

实际操作见本地包调试 :npm link ,文档信息见 npm-link

发布之前:确保你的包可正常安装运行

这很重要。

如果你的包不能本地安装,你试图发布它,将会有问题。或者更糟糕的是,你能发布,但发布了一个破损或者无意义的包。所以不要这样做。

在你包的根目录下,执行下面命令:

npm install . -g

这将会告诉你是否可行。如果你更喜欢创建一个 link 的包,指向你的工作目录,那么这么做:

npm link

使用 npm ls -g 查看是否存在。

测试本地安装,到另外一个文件夹下,执行:

cd ../some-other-folder
npm install ../my-package

在其它地方,本地安装到 node_module 文件夹下。

然后进入 node-repl,尝试使用 require(“my-thing”)引入你模块的主模块。

创建一个用户账户

通过 adducer 命令创建一个用户。像这样运行:

npm adduser

之后就根据提示进行。详细见 npm-adduser

发布你的包

这部分很简单。到你文件夹根目录下,执行这个:

npm publish

你可以给一个压缩包,一个压缩包的文件名,或者一个文件夹的路径发布一个 url。

请注意,默认情况下,文件夹里面几乎所有的内容将会被公开。所以,如果你在里面有秘密的东西,使用一个 .npmignore 文件列出要全局忽略的东西,或者从一个新的 checkout 发布。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 11月2日

npm Getting started

引子

最近工作上要接触相关的东西,开始熟悉文档,这里主要是 Getting started 部分中,感觉有帮助的内容记录。

组成

npm 包含三个不同的部分:

  • 网站
  • 命令行界面(CLI)
  • 登记处

通过使用网站,可以查找发现包、设置简介、管理你的其它方面 npm 经历。比如,你可以建立 Orgs ,用来管理共有或私有的包。

命令行界面通过终端运行,这个是大多数开发者与 npm 交互的方式。

登记处是一个包含大量 JavaScript 软件和元信息的公开信息库。

注册

按照提示注册后,用 npm 测试登录。

npm login

根据提示输入帐号、密码、邮箱。输入后检测是否登录成功

npm whoami

如果显示出你的用户名,表示登录成功。

配置本地环境

关于 npm CLI 版本

npm 命令行界面按照一定的节奏正常发布。推荐安装的版本:

  • latest release:最新的稳定版本。
  • next release:最新的未发布的版本,即将成为最新版本。

安装 Node.js 的时候,npm 会自动安装。然而 npm 的版本更新比 Node.js 频繁的多,所以安装最新稳定版本的 npm 指令如下:

npm install npm@latest -g

安装 next release 版本指令为:

npm install npm@next -g

需要注意的是,next release 版本包含的功能,在最新的稳定版本中不一定会有。

安装

强烈建议用 Node 版本管理工具来安装 Node.js 和 npm。不要使用 Node 安装工具,因为 Node 安装进程安装 npm 需要本地文件的允许,当运行全局 npm 包的时候可能会导致权限错误。
检查npm Node的版本

node -v
npm -v

使用 Node 版本管理工具来安装 Node.js 和 npm。

Node 版本管理允许你在你自己环境上,安装和切换不同版本的 Node.js 和 npm。这样就能够测试不同的版本,确保它们在不同用户版本上正常运作。

OSX、Linux Node 版本管理工具

Window Node 版本管理工具

如果无法使用版本管理工具,则使用 Node 安装工具,这是下载页面

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 10月24日

JavaScript 新旧替换六:键值对数据

引子

“键值对”的数据结构,之前基本上都是使用 {} ,新的规范中有更合适的选择了。

上一篇 JavaScript 新旧替换五:函数嵌套

ES5 方式

基本用法

使用 {} 存储键值对,键的类型只支持两种:StringSymbol

const obj1 = {1:'1'};
const obj2 = {'name':'Tom'};
const mark = Symbol('age');
const obj3 = {[mark]:19};
console.log(obj1);
console.log(obj2);
console.log(obj3);

结果

78-object-base

上面的 obj1 的键名虽然是一个数字,但会被转换为字符串。

操作

添加/修改

添加和修改使用 .[] 的方式:

const obj = {};
obj.name = 'Tom';
obj['name'] = 'Jim';

对于 Symbol 类型的键值,必须要用 [] 的方式才行。

读取

跟添加类似使用 .[] 的方式:

const mark = Symbol('age');
const obj = {name:'Tom',};
obj[mark] = 19;
console.log(obj.name);
console.log(obj[mark]);

删除

删除使用 delete 操作符:

const mark = Symbol('age');
const obj = {name:'Tom',};
obj[mark] = 19;
delete obj.name;
delete obj[mark];
console.log(obj);

遍历

遍历的常用方法有:for-infor-of

let obj = {name:'Tom',1:'1'};
const mark = Symbol('age');
obj[mark] = 19;
for (const ele of Object.keys(obj)) {
  console.log(ele);
}

// 1
// name

这种数据结构的键是无序的,此外,键如果是 Symbol 类型,无法被遍历。

ES2015+ 方式

为了解决上述键类型的问题,ES2015+ 中提供了 Map 数据结构。Map 结构提供了“值-值”的对应,更加适合存储“键值对”。

基本用法

const m = new Map([
  [1,'1'],
  ['name','Tom'],
  [Symbol('age'),19],
  [{other:'play'},'basketball'],
]);
console.log(m);
console.log(m.size);

结果

78-map-base

Map 数据结构基本属性 size 是成员的总数。

操作

添加/修改

set(key, value) 方法设置键 key 对应的值为 value ,并返回整个 Map 结构,因此可以链式调用。如果 key 已经存在,则键值会被更新。

const m = new Map();
m.set(1,'1').set('name','Tom');
const mark = Symbol('age');
m.set(mark,19);
const obj = {other:'play'};
m.set(obj,'basketball');
console.log(m);

读取

get(key) 方法读取 key 对应的值,如果找不到 key ,返回 undefined

const m = new Map([
  ['name','Tom'],
]);
const mark = Symbol('age');
m.set(mark,19);

console.log(m.get('name'));
console.log(m.get(mark));

删除

delete(key) 方法删除键 key ,删除成功则返回 true ,否则返回 false

const m = new Map([
  ['name','Tom'],
]);

const result = m.delete('name');
console.log(result) // true
console.log(m.delete('age')) // false

其它

  • has(key) : 判断是否有键 key
  • clear() : 清除所有成员。

遍历

遍历的常用方法有: forEachfor-of

const m = new Map([
  ['name','Tom'],
  [1,'1'],
  [Symbol('age'),19],
  [{other:'play'},'basketball'],
]);
for (const ele of m.keys()) {
  console.log(ele);
}

// name
// 1
// Symbol(age)
// {other: "play"}

键名是有序的,Symbol 类型的键也可以遍历到。

区别

维度MapObject
意外的键默认情况不包含任何键,只包含显式插入的键。原型链上的键名有可能与设置的键名产生冲突。
键的类型键可以是任意值。键必须是一个 String 或 Symbol 。
键的顺序有序无序
键的统计通过 size 属性获取要手动计算
迭代Map 可以直接被迭代。以某种方式(例如 Object.keys(obj))处理后才能迭代。
性能在频繁增删键值对的场景下表现更好。在频繁增删除键值对的场景下未作优化。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 10月12日

碰撞检测 :Separating Axis Theorem

引子

Collision Detection :Transformation 中介绍了动态的碰撞检测,至此 CollisionDetection 项目的主要内容差不多都涉及了。在查询资料的时候,还接触到一些其它的检测方法,现在来看一下另外一种检测方法: Separating Axis Theorem 。

相关知识点

矢量和标量

简单的来说:

  • 矢量(vector)也称向量,有大小和方向的量,例如加速度、力。
  • 标量(scalar)只有大小(magnitude)的量,例如时间、温度。

在几何中,矢量用有向线段表示,表示如下:

64-vector

矢量 V 计算方法:

  • V = C2 - C1
  • V = (7-3,7-2)
  • V = (4,5)

法向量:向量的垂直向量,交换 xy 分量,然后将坐标 x 分量取反。上面 V 的法向量为 (-5,4) 。

64-perpendicular-vector

点积和投影

点积

两个矢量,可以用点积(Dot Product)的方式进行相乘,结果是一个标量。表示形式为: A · B 。

点积有两种计算方式:

方式一

A · B = Ax * Bx + Ay * By

方式二

A · B = |A| * |B| * cos(θ)
  • |A| 是矢量 A 的量值
  • |B| 是矢量 A 的量值
  • θ 是矢量 A 和 B 之间的角度

还需要了解的一个概念就是单位向量,单位向量计算方法:向量除以向量自身的量值。

A / |A|

更多信息见这里

投影

关于投影(Projection),先看下图:

64-projection

想象用一个发出平行光线的光源,照射到一个物体上,将在一个面上产生阴影。这个阴影是三维物体的二维投影。

类似的,二维物体的投影就是一维的“阴影”。

64-projection2

点积和投影的关系

利用点积可以得出一个矢量在另外一个矢量上的投影。通过简单的推导就可以明白。

64-dot

如上图所示,将 V 在 W 上的投影标量记为 Pw(V),可以得知:

Pw(V) = |V| * cos(θ)

根据点积计算方法得知:

V · W = |V| * |W| * cos(θ)
V * (W / |W|) = |V| * cos(θ)

因此可以得出:

Pw(V) = |V| * cos(θ) = V * (W / |W|)

多边形

凸多边形

一条直线穿过一个多边形时,如果该线与多边形相交不超过(包含)两次,则该多边形为凸多边形(​Convex Polygon)。

64-convex

凹多边形

一条直线穿过一个多边形时,如果该线与多边形相交超过两次,则该多边形为凹多边形(Concave Polygon)。

64-concave

Separating Axis Theorem

分轴理论(Separating Axis Theorem)由 Hermann Minkowski 提出,可用于解决凸多边形碰撞问题,该理论表明:

如果存在一条轴线,两个凸面物体在该轴上的投影没有重叠,那么这两个凸面物体就没有重叠。

这个轴线称为分轴。接下来进一步讨论一下。在下文中分轴理论简称 SAT 。

没有重叠

64-concave-shadow

在上图中,可以看到投影没有重叠,根据 SAT ,这个两个形状没有重叠。

SAT 在检测的时候,可能需要检测很多轴线,但只要检测到有一个轴线上投影没有重叠,就可以停止继续检测。由于这种特点,SAT 对于有很多物体但碰撞很少的应用(游戏、模拟等等)是理想的选择。

重叠

如果在所有分轴上,形状的投影都重叠,那么我们可以确定这些形状产生了重叠。示例如下:

64-concave-overlap

算法实现

有了上面的原理,接下来转换成算法需要考虑的问题有:

  1. 如何获取到所有潜在的分轴?
  2. 投影重叠判断依据是什么?

问题 1

通过查找资料,第一个问题的答案是:在 2D 中,所有潜在的分轴是形状每条边的法线。

法线简单来说就是没有方向的法向量。在前面的知识点中有介绍。下面是一个大概逻辑实现:

const vertices = [] // 顶点的坐标集合,假设已有值
const axes = [] // 存放分轴
const verticesLen = vertices.length;

for (let i = 0; i < verticesLen; i++) {
  const p1 = vertices[i];
  const p2 = vertices[i + 1 == vertices.length ? 0 : i + 1];
  // 获取每条边的矢量代数表示,subtract 方法功能主要功能是 p2 的坐标与 p1 坐标分量相减
  const edge = subtract(p1,p2);
  // 获取法向量,normalAxes 方法主要功能: (x, y) => (-y, x) or (y, -x)
  const normal = normalAxes(edge);
  axes.push(normal);
}

问题 2

在上面的关于 SAT 的介绍中,在图示中可以比较明显观察到,在算法实现中,需要遍历形状所有的顶点与分轴执行点积,比较获得最小值和最大值。然后在一条轴线上大概标注出最小值和最大值,看是否有重叠的区间。

下面是一个大概逻辑实现:

假设有多边形 A 和多边形 B 。

const verticesA = []; // A 形状所有顶点坐标集合
const verticesB = []; // B 形状所有顶点坐标集合
const axes = [] // 存储获取的所有分轴
const axesLen = axes.length;

for (let i = 0; i < axesLen; i++) {
  const separateAxes = axes[i];
  // getProject 方法获取投影的最大和最小值
  const projectA = getProject(separateAxes,verticesA);
  const aMin = projectA.min;
  const aMax = projectA.max;
  const projectB = getProject(separateAxes,verticesB);
  const bMin = projectB.min;
  const bMax = projectB.max;
  // 符合该条件,表示投影重叠了。
  if ( (aMin <= bMax && aMin >= bMin) || (bMin <= aMax && bMin >= aMin) ) {
    continue;
  } else {
    return false;
  }
}

验证

根据上面的思路,以网页左上角作为坐标原点,水平向左作为 X 轴,垂直向下作为 Y 轴。根据 CSS 的单位描述坐标点。

这个是测试页面,移动端如下:

64-example

在上面测试页面中,以未重叠的投影数据为例,检测的数据投影到一条轴线上:

64-project

可以看出没有重叠。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 10月5日

碰撞检测 : Transformation

引子

Collision Detection :Triangle 中对三角形的碰撞检测从另外一种思路进行思考,到目前为止介绍的都是静态的检测,接着来看一下动态的碰撞检测。

以下示例未做兼容性检查,建议在最新的 Chrome 浏览器中查看。

Transformation

这是示例页面

基于 canvas 的 translaterotatescale 三种转换形成的动画,看看如何进行动态的碰撞检测。

基于 canvas 的动画原理是每隔一段时间进行重绘,所以在检测的时候,实际上是在特定的时刻,进行静态的碰撞检测,所以之前介绍的方法同样适用,这里统一使用 Polygon/Polygon 中的方法。 检测方法有了,接着就是获取在屏幕中相关点动态变化的坐标。下面分情况进行说明。

translate

在 canvas 上进行绘制时,都是基于坐标系进行定位,画布左上角为坐标系原点,水平向右为 X 轴正方向,垂直向下为 Y 轴正方向。绘制一个矩形 rect(20, 20, 40, 40) ,在坐标系上是这样的:

63-origin

如果想要水平向右移动 60 像素,垂直向下移动 80 像素,可以直接进行坐标相加:rect(20 + 60, 20 + 80, 40, 40)

63-new-coord

但还有另外一种更有趣的方式:移动整个坐标轴。如果把整个坐标轴水平向右移动 60 像素,垂直向下移动 80 像素,在视觉上是完全一样的。 translate 方法就是使用这种方式。

63-moved-grid

从上图可以发现,这种方式不用考虑矩形的坐标变化,在处理比较复杂的图形时,会方便很多。

需要注意的是,在进行了 translate 后,需要重置坐标轴,因为可能还有其它图形存在,而且还是以原来的坐标轴作为参考。重置坐标轴使用 setTransform 方法:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

ctx.translate(50, 50);
ctx.fillRect(0,0,100,100);

// 重置
ctx.setTransform(1, 0, 0, 1, 0, 0);

// 其它处理

对于变化后的坐标,直接对平移的像素进行加减。

/**
 * 假设点 A(x,y),经过 translate(x1,y1) 后达到 B(m,n)
 */
 const m = x + x1;
 const n = y + y1;

rotate

rotate 方法与 translate 方法类似,通过旋转坐标轴实现。

63-rotate-grid

对于变化后的坐标,需要进行一些计算。

63-rotate-calculate

/**
 *
 * 圆心坐标 O(0,0),假设点 A(x,y) ,与 X 轴形成的角度为 α
 * 顺时针旋转角度 β 后达到点 B(m,n),下面来推导一下 B 点坐标
 *
 * A 到圆心的距离: dist1 = |OA| = y/sin(α)=x/cos(α)
 * B 到圆心的距离: dist2 = |OB| = n/sin(α-β)=m/cos(α-β)
 *
 * 只是旋转 所以 dist1 = dist2,建设旋转的半径为 r :
 * r = y/sin(α)=x/cos(α)=n/sin(α-β)=m/cons(α-β)
 * y = r * sin(α)  x = r * cos(α)
 *
 * 根据三角函数公式:
 * sin(α+β)=sin(α)cos(β)+cos(α)sin(β)
 * sin(α-β)=sin(α)cos(β)-cos(α)sin(β)
 * cos(α+β)=cos(α)cos(β)-sin(α)sin(β)
 * cos(α-β)=cos(α)cos(β)+sin(α)sin(β)
 *
 * 代入下面公式:
 * m = r*cos(α-β) = r * cos(α)cos(β) + r * sin(α)sin(β) =  x * cos(β) + y * sin(β)
 * n = r*sin(α-β) = r * sin(α)cos(β) - r * cos(α)sin(β) =  y * cos(β) - x * sin(β)
 *
 * 逆时针则相反:
 * m =  x * cos(β) - y * sin(β)
 * n =  y * cos(β) + x * sin(β)
 *
 */

scale

scale 方法 translate 方法类似,通过缩放坐标轴实现。

对于变化后的坐标,直接乘以对应缩放的倍数。

/**
 * 假设点 A(x,y),经过 scale(num1,num2) 后达到 B(m,n)
 */
const m = x * num1;
const n = y * num2;

Transformation Order

当连续进行多次不同变换时,顺序不同,结果可能会不一样。这是示例

这是因为连续进行变换时,都是基于上一次变换后的状态,再次进行变换。在进行计算的时候,需要多方面考虑。基于 transform 中的参数格式,进行计算会比较方便一些,translaterotatescale 的效果都可以转换为 transform 的形式。

/**
 * canvas.transform(sx, ry, rx, sy, tx, ty)
 * sx-水平缩放,ry-垂直倾斜,rx-水平倾斜,sy-垂直缩放,tx-水平移动,ty-垂直移动
 *
 */
function Transform() {
  this.reset();
}

Transform.prototype.reset = function() {
  this.transformData = [1,0,0,1,0,0];
};

Transform.prototype.translate = function(x, y) {
  let [sx,ry,rx,sy,tx,ty] = this.transformData;
  const newTX = sx * x + rx * y;
  const newTY = ry * x + sy * y;
  this.transformData = [sx,ry,rx,sy,newTX,newTY];
};

Transform.prototype.rotate = function(angle) {
  let c = Math.cos(angle);
  let s = Math.sin(angle);
  let [sx,ry,rx,sy,tx,ty] = this.transformData;
  let newSX = sx * c + rx * s;
  let newRY = ry * c + sy * s;
  let newRX = sx * -s + rx * c;
  let newSY = ry * -s + sy * c;
  this.transformData = [newSX,newRY,newRX,newSY,tx,ty];
};

Transform.prototype.scale = function(x, y) {
  let [sx,ry,rx,sy,tx,ty] = this.transformData;
  let newSX = sx * x;
  let newRY = ry * x;
  let newRX = rx * y;
  let newSY = sy * y;
  this.transformData = [newSX,newRY,newRX,newSY,tx,ty];
};

Transform.prototype.getCoordinate = function(x, y) {
  let [sx,ry,rx,sy,tx,ty] = this.transformData;
  const px = x * sx + y*rx + tx;
  const py = x * ry + y*sy + ty;
  return [px,py];
};

参考资料

查看原文

赞 1 收藏 0 评论 0

XXHolic 发布了文章 · 10月2日

碰撞检测 :Triangle

引子

Collision Detection :Polygon 中主要介绍了多边形相关的碰撞检测,接着来看看三角形的情况。三角形同样属于多边形,因此,多边形的方法对三角形都适用。在这里探讨一下另外一种思路。

以下示例未做兼容性检查,建议在最新的 Chrome 浏览器中查看。

Triangle/Point

这是示例页面

三角形与点的碰撞检测,可以从面积的角度进行切入,看下面的一张图:

62-tri-point

如果点在三角形内,那么与三角形顶点相连,切割成了三部分,这三部分的面积之和,如果跟三角形面积相等,那么就可以说明发生了碰撞。

计算三角形的面积可以使用叉积海伦公式:

62-math

/**
 * 叉积 用符号 x 表示
 * V 矢量
 * W 矢量
 * |V| |W| 矢量的模,就是线段实际长度
 * θ V 与 W 之间的角度
 */

V x W = |V| * |W| * sin(θ)

/**
 * 计算面积公式
 * a 底
 * h 高
 */
S = a * h * 1/2

从上图中可以看出 |W| * sin(θ) 的结果就是三角形的高 h ,也就是说:

S = a * h * 1/2 = V x W *1/2

基于上面的理论支持,下面是完整检测逻辑:

/*
 * points 三角形顶点坐标,形式为 [[x1,y1],[x2,y2]]
 * (px,py) 检测点坐标
 */
function checkTrianglePoint({points,px,py}) {
  const [point1,point2,point3] = points;
  const [x1,y1] = point1;
  const [x2,y2] = point2;
  const [x3,y3] = point3;
  // 原始总面积
  const areaOrig = Math.abs( (x2-x1)*(y3-y1) - (x3-x1)*(y2-y1) );

  // 检测点与三角形顶点形成的面积
  const area1 = Math.abs( (x1-px)*(y2-py) - (x2-px)*(y1-py) );
  const area2 = Math.abs( (x2-px)*(y3-py) - (x3-px)*(y2-py) );
  const area3 = Math.abs( (x3-px)*(y1-py) - (x1-px)*(y3-py) );
  const areaTotal = area1 + area2 + area3;

  // 计算误差允许值
  const buffer = 0.1;
  if (areaTotal >= areaOrig-buffer && areaTotal<= areaOrig+buffer) {
    return true;
  }
  return false;
}

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 9月21日

碰撞检测 :Polygon

引子

Collision Detection :Line 中主要介绍了直线相关的碰撞检测,接着来看看更加复杂一些的多边形的情况。

以下示例未做兼容性检查,建议在最新的 Chrome 浏览器中查看。

Polygon/Point

这是示例页面

多边形与点的碰撞检测,需要每一条边与点进行检测,才能确定是否产生了碰撞:


const points = []; // 多边形顶点坐标

const pointsLen = points.length;

let collision = false; // 初始化碰撞状态

for(let index = 0; index < pointsLen; index ++) {

// checkCondition 边与检测点的判断

if (checkCondition()) {

collision = !collision;

}

}

假设一条边的一个端点的坐标为 [cx,cy] ,另一端点坐标为 [nx,ny],检测点的坐标为 [px,py] ,需要检测的条件为:


if ( ((cy > py) != (ny > py)) && (px < (nx-cx) * (py-cy) / (ny-cy) + cx) ) {

collision = !collision;

}

这里的判断条件做了两个检测,首先是检查检测点的 Y 坐标是否在两个端点的 Y 坐标范围内:


(cy >= py && ny < py) || (cy < py && ny >= py)

// 等同于

(cy > py) != (ny > py)

61-polygon-point

然后检查监测点的 X 坐标,依赖的方法是 Jordan Curve Theorem :


px < (nx-cx) * (py-cy) / (ny-cy) + cx)

每次检测后,符合条件都需要将 collision 的布尔值取反。直到检测所有的边后,最后的 collision 值才是最终结果。

下面是完整检测逻辑:


/*

* points 多边形顶点坐标,形式为 [[x1,y1],[x2,y2]]

* (px,py) 检测点坐标

*/

function checkPolygonPoint({points,px,py}) {

let collision = false;

const pointsLen = points.length;

for (let index = 0; index < pointsLen; index++) {

const currentPoint = points[index];

const next = index === pointsLen-1 ? 0:index+1;

const nextPoint = points[next];

const [cx,cy] = currentPoint;

const [nx,ny] = nextPoint;

const judgeX = px < (nx-cx)*(py-cy) / (ny-cy)+cx;

const judgeY = (cy >= py && ny < py) || (cy < py && ny >= py);

if (judgeX && judgeY) {

collision = !collision;

}

}

return collision;

}

Polygon/Circle

这是示例页面

多边形与圆的碰撞检测,可以分解为多边形的边与圆的碰撞检测,只要有一条边产生了碰撞,就可以进行判定。这个时候可以使用之前介绍的关于 Line/Circle 检测的方法。

下面是完整检测逻辑:


/*

* points 多边形顶点坐标,形式为 [[x1,y1],[x2,y2]]

* (cx,cy) 圆心坐标

* radius 圆半径

*/

function checkPolygonCircle({points,cx,cy,radius}) {

const pointsLen = points.length;

for (let index = 0; index < pointsLen; index++) {

const currentPoint = points[index];

const next = index === pointsLen-1 ? 0:index+1;

const nextPoint = points[next];

const [x1,y1] = currentPoint;

const [x2,y2] = nextPoint;

const collision = checkLineCircle({x1,y1,x2,y2,cx,cy,radius});

if (collision) {

return true;

}

}

return false;

}

Polygon/Rectangle

这是示例页面

多边形与矩形的碰撞检测,可以分解为多边形的边与矩形的碰撞检测,只要有一条边产生了碰撞,就可以进行判定。这个时候可以使用之前介绍的关于 Line/Rectangle 检测的方法。

下面是完整检测逻辑:


/*

* points 多边形顶点坐标,形式为 [[x1,y1],[x2,y2]]

* (rx,ry) 矩形左上角顶点坐标

* rw 矩形宽度

* rh 矩形高度

*/

function checkPolygonRectangle({points,rx,ry,rw,rh}) {

const pointsLen = points.length;

for (let index = 0; index < pointsLen; index++) {

const currentPoint = points[index];

const next = index === pointsLen-1 ? 0:index+1;

const nextPoint = points[next];

const [x1,y1] = currentPoint;

const [x2,y2] = nextPoint;

const collision = checkLineRectangle({x1,y1,x2,y2,rx,ry,rw,rh});

if (collision) {

return true;

}

}

return false;

}

Polygon/Line

这是示例页面

多边形与直线的碰撞检测,可以分解为多边形的边与直线的碰撞检测,只要有一条边产生了碰撞,就可以进行判定。这个时候可以使用之前介绍的关于 Line/Line 检测的方法。

下面是完整检测逻辑:


/*

* points 多边形顶点坐标,形式为 [[x1,y1],[x2,y2]]

* (x1,y1) 直线线端点坐标

* (x2,y2) 直线另一个端点坐标

*/

function checkPolygonLine({points,x1,y1,x2,y2}) {

const pointsLen = points.length;

for (let index = 0; index < pointsLen; index++) {

const currentPoint = points[index];

const next = index === pointsLen-1 ? 0:index+1;

const nextPoint = points[next];

const [x3,y3] = currentPoint;

const [x4,y4] = nextPoint;

const collision = checkLineLine({x1,y1,x2,y2,x3,y3,x4,y4});

if (collision) {

return true;

}

}

return false;

}

Polygon/Polygon

这是示例页面,刷新页面,会生成随机的多边形。

多边形与多边形的碰撞检测,思路是检测一个多边形任意边是否与另外一个多边形的任意边产生碰撞。这个时候可以使用前面介绍的关于 Polygon/Line 检测的方法。

下面是完整检测逻辑:


/*

* points1 多边形1顶点坐标,形式为 [[x1,y1],[x2,y2]]

* points2 多边形2顶点坐标,形式为 [[x1,y1],[x2,y2]]

*/

function checkPolygonPolygon({points1,points2}) {

const pointsLen = points1.length;

for (let index = 0; index < pointsLen; index++) {

const currentPoint = points1[index];

const next = index === pointsLen-1 ? 0:index+1;

const nextPoint = points1[next];

const [x1,y1] = currentPoint;

const [x2,y2] = nextPoint;

const collision = checkPolygonLine({points:points2,x1,y1,x2,y2});

if (collision) {

return true;

}

}

return false;

}

参考资料

查看原文

赞 1 收藏 0 评论 5

XXHolic 发布了文章 · 9月14日

碰撞检测 :Line

引子

Collision Detection :Rectangle 中主要介绍了矩形相关的碰撞检测,接着来看看直线的情况。

以下示例未做兼容性检查,建议在最新的 Chrome 浏览器中查看。

Line/Point

这是示例页面

线与点的碰撞检测,观察下面一张图:

60-line-point

从图中可以发现,当点在线上时,到两个端点的距离之和与线的长度相同。两点之间的距离,同样使用之前用到过的勾股定理。考虑到计算的精度误差,可以设置一个误差允许范围值,这样会感觉更加自然一些。


/*

* (x1,y1) 线的一个端点

* (x2,y2) 线的另一个端点

* (px,py) 检测点的坐标

*/

function checkLinePoint({x1,y1,x2,y2,px,py}) {

const d1 = getLen([px,py],[x2,y2]);

const d2 = getLen([px,py],[x2,y2]);

const lineLen = getLen([x1,y1],[x2,y2]);

const buffer = 0.1; // 误差允许范围

if (d1+d2 >= lineLen-buffer && d1+d2 <= lineLen+buffer) {

return true; // 发生碰撞

} else {

return false; // 没有碰撞

}

}

  

/*

* 勾股定理计算两点间直线距离

* point1 线的一个端点

* point2 线的另一个端点

*/

function getLen(point1,point2) {

const [x1,y2] = point1;

const [x2,y2] = point1;

const minusX = x2-x1;

const minusY = y2-y1;

const len = Math.sqrt(minusX*minusX + minusY*minusY);

return len;

}

Line/Circle

这是示例页面

直线和圆的碰撞检测,首先需要考虑直线是否位于圆内,因为有可能出现直线的长度小于圆的直径。为了检测这个,可以使用之前 Point/Circle 的检测方法,如果任意一端在内部,就直接返回 true 跳过剩下的检测。


const isInside1 = checkPointCircle({px:x1,py:y1,cx,cy,radius});

const isInside2 = checkPointCircle({px:x2,py:y2,cx,cy,radius});

if (isInside1 || isInside2) {

return true

}

接下来需要找到直线上离圆心最近的一个点,这个时候使用矢量的点积可以计算出最近点的坐标。下面是一个简单的数学推导过程。


/**

*

* a 代表线的向量

* t 系数

* p1 直线上任意一点

* p0 非直线上的一点

* pt 直线上离 p0 最近的一点

*

* pt = p1 + t*a // p1 和 pt 都在直线上,存在这样成立的关系系数 t

*

* (a.x,a.y)*(pt.x-p0.x,pt.y-p0.y) = 0 // 垂直的向量,点积为 0

*

* (a.x,a.y)*( (p1+t*a).x-p0.x,(p1+t*a).y-p0.y) = 0 // 带入 pt

*

* a.x *(p1.x + t*a.x - p0.x) + a.y *(p1.y + t*a.y - p0.y) = 0

* t*(a.x*a.x + a.y*a.y) = a.x*(p0.x-p1.x)+a.y*(p0.y-p1.y)

* t = (a.x*(p0.x-p1.x)+a.y*(p0.y-p1.y)) / ((a.x*a.x + a.y*a.y))

*

* 得出系数 t 的值后,代入到一开始的公式中,就可以得出 pt 的坐标

*/

然而得出的这个点可能存在这条线延伸的方向上,所以需要判断该点是否在所提供的线段上。这个时候可以使用前面介绍的关于 Line/Point 检测的方法。


const isOnSegment = checkLinePoint({x1,y1,x2,y2, px:closestX,py:closestY});

if (!isOnSegment) return false;

最后计算圆心到直线上最近点的距离,与圆的半径进行比较,判断是否碰撞。下面是主要逻辑:


/*

* (x1,y1) 线的一个端点

* (x2,y2) 线的另一个端点

* (px,py) 圆心的坐标

* radius 圆的半径

*/

function checkLineCircle({x1,y1,x2,y2,cx,cy,radius}) {

const isInside1 = checkPointCircle({px:x1,py:y1,cx,cy,radius});

const isInside2 = checkPointCircle({px:x2,py:y2,cx,cy,radius});

if (isInside1 || isInside2) {

return true

}

  

const pointVectorX = x1 - x2;

const pointVectorY = y1 - y2;

const t = (pointVectorX*(cx - x1) + pointVectorY*(cy-y1))/(pointVectorX*pointVectorX+pointVectorY*pointVectorY);

const closestX = x1 + t*pointVectorX;

const closestY = y1 + t*pointVectorY;

  

const isOnSegment = checkLinePoint({x1,y1,x2,y2, px:closestX,py:closestY});

if (!isOnSegment) return false;

  

const distX = closestX - cx;

const distY = closestY - cy;

const distance = Math.sqrt( (distX*distX) + (distY*distY) );

  

if (distance <= radius) {

return true; // 发生碰撞

} else {

return false; // 没有碰撞

}

}

  

Line/Line

这是示例页面

直线与直线的碰撞检测,需要借助数学的推导:


/**

*

* P1 P2 直线 1 上的两个点

* A1 代表直线 1 的向量

* t1 直线 1 的系数

*

* P3 P4 直线 2 上的两个点

* A2 代表直线 2 的向量

* t2 直线 2 的系数

*

* Pa = P1 + t1*A1

* Pb = P3 + t2*A2

*

* 相交时,Pa = Pb

* x1 + t1*(x2-x1) = x3 + t2*(x4-x3)

* y1 + t1*(y2-y1) =y3 + t2*(y4-y3)

*

* 剩下就是二元一次方程求解

* t1 = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3))/((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1))

* t2 = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1))

*

*/

计算出两条线的系数后,如果两条线相交,就要符合条件:


if (t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1) {

return true;

}

return false;

下面是完整的判断方法:


/*

* (x1,y1) 线1的一个端点

* (x2,y2) 线1的另一个端点

* (x3,y3) 线2的一个端点

* (x4,y4) 线2的另一个端点

*/

function checkLineLine({x1,y1,x2,y2,x3,y3,x4,y4}) {

const t1 = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));

const t2 = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));

  

if (t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1) {

return true; // 发生碰撞

} else {

return false; // 没有碰撞

}

}

Line/Rectangle

这是示例页面

直线与矩形的碰撞检测,可以转换为直线与矩形四条边的碰撞检测,使用前面介绍的关于 Line/Line 检测的方法即可。

60-line-rect


/*

* (x1,y1) 线的一个端点

* (x2,y2) 线的另一个端点

* (rx,ry) 矩形顶点坐标

* rw 矩形宽度

* rh 矩形高度

*/

function checkLineRectangle({x1,y1,x2,y2,rx,ry,rw,rh}) {

const isLeftCollision = checkLineLine(x1,y1,x2,y2, x3:rx,y3:ry,x4:rx, y4:ry+rh);

const isRightCollision = checkLineLine(x1,y1,x2,y2, x3:rx+rw,y3:ry, x4:rx+rw,y4:ry+rh);

const isTopCollision = checkLineLine(x1,y1,x2,y2, x3:rx,y3:ry, x4:rx+rw,y4:ry);

const isBottomCollision = checkLineLine(x1,y1,x2,y2, x3:rx,y3:ry+rh, x4:rx+rw,y4:ry+rh);

  

if (isLeftCollision || isRightCollision || isTopCollision || isBottomCollision ) {

return true; // 发生碰撞

} else {

return false; // 没有碰撞

}

}

参考资料

查看原文

赞 1 收藏 1 评论 0

XXHolic 发布了文章 · 9月7日

碰撞检测 :Rectangle

引子

Collision Detection :Point 中主要介绍了点的碰撞检测,接着来看看矩形的情况。

以下示例未做兼容性检查,建议在最新的 Chrome 浏览器中查看。

Rectangle/Point

这是示例页面

绘制矩形可以通过四个点的坐标进行绘制,也可以通过一个顶点的坐标,结合矩形的宽高进行绘制:

59-rect-bounding-box

矩形与点的碰撞检测,只要点的坐标在矩形的坐标范围之内即可:

/*
 * (px,py) 点的坐标
 * (rx,ry) 矩形顶点的坐标
 * rw 矩形的宽度
 * rh 矩形的高度
 */
function checkRectanglePoint({px,py,rx,ry,rw,rh}) {
  const isTrue = px >= rx && // 左边界
                 px <= rx + rw && // 右边界
                 py >= ry && // 上边界
                 py <= ry + rh; // 下边界
  if (isTrue) {
    return true; // 发生碰撞
  } else {
    return false; // 没有碰撞
  }
}

Rectangle/Rectangle

这是示例页面

矩形与矩形的碰撞检测,看下面一张图:

59-rect-rect

通过观察可以得知,两个矩形发生碰撞,需要符合的条件是:

/*
 * (r1x,r1y) 矩形1顶点的坐标
 * (r2x,r2y) 矩形2顶点的坐标
 * r1w r1h 矩形1的宽度和高度
 * r2w r2h 矩形2的宽度和高度
 */
function checkRectangleRectangle({r1x,r1y,,r1w,r1h,r2x,r2y,r2w,r2h}) {
  const isTrue = r1x + r1w >= r2x && // 矩形 2 左边界
                 r1x <= r2x + r2w && // 矩形 2 右边界
                 r1y + r1h >= r2y && // 矩形 2 上边界
                 r1y <= r2y + r2h; // 矩形 2 下边界
  if (isTrue) {
    return true; // 发生碰撞
  } else {
    return false; // 没有碰撞
  }
}

Rectangle/Circle

这是示例页面

矩形与圆的碰撞检测,思路是:

  1. 首先要确定圆处于矩形那个边界;
  2. 然后在边界上确定与圆心距离最短的点;
  3. 最后使用勾股定理计算出距离,与圆心半径进行比较。
/*
 * (cx,cy) 圆心的坐标
 * radius 圆的半径
 * (rx,ry) 矩形顶点的坐标
 * rw 矩形的宽度
 * rh 矩形的高度
 */
function checkRectangleCircle({cx,cy,radius,rx,ry,rw,rh}) {
  let nearestX = cx,nearestY = cy; // 初始化边界上离圆心最近的点坐标
  if (cx < rx) {
    nearestX = rx;
  } else if (cx > rx + rw) {
    nearestX = rx + rw;
  }
  if (cy < ry) {
    nearestY = ry;
  } else if (cy > ry + rh) {
    nearestY = ry + rh;
  }
  const distX = cx-nearestX;
  const distY = cy-nearestY;
  const distance = Math.sqrt( (distX*distX) + (distY*distY) );

  if (distance <= radius) {
    return true; // 发生碰撞
  } else {
    return false; // 没有碰撞
  }
}

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 9月1日

碰撞检测:Point

引子

Collision Detection 中对碰撞检测作了简单的介绍。在查询资料时,在 Github 上发现了 CollisionDetection 这个项目。查看之后,发现关于碰撞检测的讨论,由浅入深,很适合学习。在其基础上,做了 JavaScript 版本的实现。

以下示例未做兼容性检查,建议在最新的 Chrome 浏览器中查看。

Point/Point

这是示例页面

最早的碰撞检测是检测两个点的碰撞。为了检测它们是否接触,我们只需要检查它们的 X 和 Y 坐标是否一样。

/*
 * (x1,x2) 点的坐标
 * (y1,y2) 点的坐标
 */
function checkPointPoint(x1,x2,y1,y2) {
  if (x1 == x2 && y1 == y2) {
    return true; // 发生碰撞
  } else {
    return false; // 没有碰撞
  }
}

Point/Circle

这是示例页面

点与圆的碰撞检测,只需要比较点和圆心之间的距离 distance 是否小于或等于圆的半径 r

function checkPointCircle() {
  if (distance <= r) {
    return true; // 发生碰撞
  } else {
    return false; // 没有碰撞
  }
}

如下图所示,计算两个点之间的距离,在几何上进行转换,使用勾股定理(Pythagorean Theorem)可以得出:

a*a + b*b = c*c; // 勾股定理
a = px - cx; // 点 x 坐标与 圆心 x 坐标
b = py - cy; // 点 y 坐标与 圆心 y 坐标
c = Math.sqrt(a*a + b*b);

58-point-circle

带入到上面检测碰撞的逻辑中:

/*
 * (px,py) 点的坐标
 * (cx,cy) 圆心的坐标
 * radius 圆的半径
 */
function checkPointCircle({px,py,cx,cy,radius}) {
  const minusX = px - cx;
  const minusY = py - cy;
  const distance = Math.sqrt(minusX*minusX + minusY*minusY);
  if (distance <= radius) {
    return true; // 发生碰撞
  } else {
    return false; // 没有碰撞
  }
}

Circle/Circle

这是示例页面

圆与圆的碰撞检测,只需要比较两个圆心之间的距离 distance 是否小于或等于两个圆的半径之和 r1+r2

同样的使用到了勾股定理。

/*
 * (c1x,c1y) 圆心的坐标
 * (c2x,c2y) 圆心的坐标
 * r1,r2 圆的半径
 */
function checkCircleCircle({c1x,c1y,c2x,c2y,r1,r2}) {
  const minusX = c1x - c2x;
  const minusY = c1y - c2y;
  const distance = Math.sqrt(minusX*minusX + minusY*minusY);
  if (distance <= r1+r2) {
    return true; // 发生碰撞
  } else {
    return false; // 没有碰撞
  }
}

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 8月28日

Canvas 绘制 1 px 直线模糊(非高清屏)的问题

引子

Canvas 显示模糊问题 中解决了在高清显示屏上模糊的问题,最近碰到了绘制直线的情况,使用了同样的方案,一开始觉得影响不大,但时间长了,发现非高清屏幕上,直线模糊的感觉越来越明显,就去找了下资料,进行了处理。

问题

这是问题重现页面

在网上同样可以找同样的问题,从各种回答中,在 LET’S CALL IT A DRAW(ING SURFACE) 中找到较有说服力的解释:

把每一个像素想象为一个正方形。整数坐标(0,1,2…)是正方形的边。如果你在整数坐标之间绘制 1 像素的线,它将与相邻像素块的边重叠,生成的线将绘制两个像素宽度。

例如,如果你尝试绘制从点(1,0)到(1,3)的 1 像素线,浏览器将会在 x=1 坐标点的两边绘制 0.5 屏幕像素。由于屏幕不能显示半个像素,它将线扩展到包含 2 个像素。

75-pixels-1

处理方法

按照上面举例的解释,对应的处理方式是:

如果你尝试从点(1.5,0)到(1.5,3)绘制 1 像素的线,浏览器将会在 x=1.5 坐标点的两边绘制 0.5 屏幕像素,结果就是 1 像素的线了。

75-pixels-2

也就是说为了绘制只有 1 像素的线,你需要将坐标垂直于线的方向偏移 0.5 。这是按照这种方式处理后的页面

下面在非高清屏上展示对比:

75-compare

除了上面有些效果的方法外,尝试过但无效的方法有:

  • 使用 context.translate(0.5, 0.5)
  • 使用 scale

参考资料

查看原文

赞 1 收藏 0 评论 0

XXHolic 发布了文章 · 8月28日

Canvas 图像灰度处理

引子

在玩游戏的时候,碰到一个交互效果:背景一张看起来黑白的图,然后用擦除的交互,让图像变的有颜色。也想试试做这个效果,首先想到的是那个黑白的图是怎么形成的,于是就查资料,找到了用 Canvas 转换的方法。

思路

看起来是黑白的图像,其实是灰度图像,进一步说明见图像。这种图像的特点是像素的颜色分量取值都是一样的,而 Canvas 的方法 getImageData 可以获取到画布上的像素值数据,改变数据后,使用方法 putImageData 将数据绘制到画布上。这样就可以达到灰度图像的效果。

实现

这是示例页面,移动端访问如下:

65-gray

主要实现如下:

  /**
   * 图像灰度处理
   * @param {object} context canvas 上下文
   * @param {number} sx 提取图像数据矩形区域的左上角 x 坐标。
   * @param {number} sy 提取图像数据矩形区域的左上角 y 坐标。
   * @param {number} sw 提取图像数据矩形区域的宽度。这要注意一下,canvas 标签上 width 属性值,不是渲染后实际宽度值,否则在高清手机屏幕下且做了高清处理,只能获取到部分图像宽度。
   * @param {number} sh 提取图像数据矩形区域的高度。这要注意一下,canvas 标签上 height 属性值,不是渲染后实际高度值,否则在高清手机屏幕下且做了高清处理,只能获取到部分图像高度。
   */
  function toGray(context,sx, sy, sw, sh) {
    var imageData = context.getImageData(sx, sy, sw, sh);
    var colorDataArr = imageData.data;
    var colorDataArrLen = colorDataArr.length;
    for(var i = 0; i < colorDataArrLen; i+=4) {
      // 计算方式之一
      var gray=(colorDataArr[i]+colorDataArr[i+1]+colorDataArr[i+2])/3;
      colorDataArr[i] = gray;
      colorDataArr[i+1] = gray;
      colorDataArr[i+2] = gray;
    }
    context.putImageData(imageData,0,0);
  }

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 8月28日

canvas 文本坐标(0,0)显示问题

引子

在测试 canvas 文字显示的时候,发现坐标设为(0,0),文字显示会有问题。

文本坐标(0,0)显示问题

刚开始本以为使用 canvas 的方法不对,尝试改变坐标后,发现又可以显示。这是问题示例,扫描访问二维码如下。

27-qrcode-problem

查询资料,发现了类似的问题,原因是 canvas 中的文本坐标位置,是按照属性 textBaseline 设置的基线作为参考,默认值是 alphabetic。效果如下图。

27-img-textbaseline

当位置坐标为(0,0)时,文本基线以上的就不在 canvas 显示区域内了,详细文档见 MDN textBaseline。将 textBaseline 设置为 top 就可以正常显示,这是正常示例,扫描访问二维码如下。

27-qrcode-normal

在测试的过程中,发现英文可以正常显示,但中文,字体大小不同,顶部显示可能有稍微的截断。目前想到的解决方法有:

  • 调整到适当的字体大小。
  • 将文本显示的位置稍微的下移。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 8月3日

canvas 图片跨域处理

引子

近期的工作中处理图片合并时,碰到图片来源跨域的情况,在此记录。

图片跨域处理

在用 canvas 合成图片时,放在画布里面的图片,有些图片源是另外一个域名,由于同源策略,首先需要在服务配置中添加对应的 Access-Control-Allow-Origin,允许对应域名的请求。在这次处理过程中,还发现如果有用 CDN 进行加速,那么对应的 CDN 的配置也要添加这个请求头。

此外在合成图片的时候,要给对应的图片添加 crossOrigin 属性,否则会被认为污染了画布,无法继续合成。详细可见文档说明。

var img = new Image();
img.crossOrigin = "Anonymous";
img.src = '***';
img.onload = function() {}

最终合成图片的处理,要在图片加载完成的事件处理程序中才行,不然对应图片不会出现在合成的图片中。

参考资料

查看原文

赞 1 收藏 1 评论 2

XXHolic 发布了文章 · 7月20日

canvas 文字换行

引子

近期的工作中,遇到的功能需求,需要控制文字显示行数,超过就省略号显示。

文字换行

一般文字行数控制用 css 就可以实现,但在 canvas 中不行。在网站查询资料,就可以发现需要程序控制文字换行,主要使用到的方法是 measureText(),这个方法会返回一个度量文本的相关信息的对象,例如文本的宽度。

这里会有一个边界问题:如果文字在 canvas 边界出现换行,那么就可能出现文字显示不全的问题。

主要处理方法如下:

// 文本换行处理,并返回实际文字所占据的高度
function textEllipsis (context, text, x, y, maxWidth, lineHeight, row) {
  if (typeof text != 'string' || typeof x != 'number' || typeof y != 'number') {
    return;
  }
  var canvas = context.canvas;

  if (typeof maxWidth == 'undefined') {
    maxWidth = canvas && canvas.width || 300;
  }

  if (typeof lineHeight == 'undefined') {
    // 有些情况取值结果是字符串,比如 normal。所以要判断一下
    var getLineHeight = window.getComputedStyle(canvas).lineHeight;
    var reg=/^[0-9]+.?[0-9]*$/;
    lineHeight = reg.test(getLineHeight)? getLineHeight:20;
  }

  // 字符分隔为数组
  var arrText = text.split('');
  // 文字最终占据的高度,放置在文字下面的内容排版,可能会根据这个来确定位置
  var textHeight = 0;
  // 每行显示的文字
  var showText = '';
  // 控制行数
  var limitRow = row;
  var rowCount = 0;

  for (var n = 0; n < arrText.length; n++) {
    var singleText = arrText[n];
    var connectShowText = showText + singleText;
    // 没有传控制的行数,那就一直换行
    var isLimitRow = limitRow ? rowCount === (limitRow - 1) : false;
    var measureText = isLimitRow ? (connectShowText+'……') : connectShowText;
    var metrics = context.measureText(measureText);
    var textWidth = metrics.width;

    if (textWidth > maxWidth && n > 0 && rowCount !== limitRow) {
      var canvasShowText = isLimitRow ? measureText:showText;
      context.fillText(canvasShowText, x, y);
      showText = singleText;
      y += lineHeight;
      textHeight += lineHeight;
      rowCount++;
      if (isLimitRow) {
        break;
      }
    } else {
      showText = connectShowText;
    }
  }
  if (rowCount !== limitRow) {
    context.fillText(showText, x, y);
  }

  var textHeightValue = rowCount < limitRow ? (textHeight + lineHeight): textHeight;
  return textHeightValue;
}

这是示例,扫描访问二维码如下。

19-canvas-canvas-text

参考资料

查看原文

赞 1 收藏 1 评论 0

XXHolic 发布了文章 · 7月13日

canvas 图片圆角问题

引子

近期的工作中,是继 canvas 设置边框问题 之后碰到的第 4 个问题。

图片圆角问题

如果只是想要显示圆角的效果,设置 border-radius 就可以了,但如果要让 canvas 合成的图片显示为圆角,这种 css 方式不行。这是示例,扫描访问二维码如下。

19-canvas-border-radius

在网上查询资料,发现同样的问题,解决的方式是用 canvas 的裁剪功能。

解决方法

先画布上画一个有圆角的矩形,然后使用裁剪的方式 clip()

// 生成有圆角的矩形
function drawRoundedRect(cxt, x, y, width, height, radius) {
  cxt.beginPath();
  cxt.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 3 / 2);
  cxt.lineTo(width - radius + x, y);
  cxt.arc(width - radius + x, radius + y, radius, Math.PI * 3 / 2, Math.PI * 2);
  cxt.lineTo(width + x, height + y - radius);
  cxt.arc(width - radius + x, height - radius + y, radius, 0, Math.PI * 1 / 2);
  cxt.lineTo(radius + x, height + y);
  cxt.arc(radius + x, height - radius + y, radius, Math.PI * 1 / 2, Math.PI);
  cxt.closePath();
}

这是示例,扫描访问二维码如下。

19-canvas-radius-clip

参考资料

查看原文

赞 1 收藏 0 评论 0

XXHolic 发布了文章 · 7月6日

canvas 设置边框问题

引子

近期的工作中,是继 canvas 显示模糊问题 之后碰到的第 3 个问题。

设置边框问题

这个是示例,扫描访问二维码如下。

19-canvas-border

在手机上可以看到,设置边框后,图片就模糊了。如果 border 不占用 canvas 的高宽度,就没有那个问题,在画布上画个边框也可以。

原因应该跟 canvas 显示模糊问题中差不多,但疑问的是这种情况并没有少像素,而是展示的空间少了,画布上的像素多了,为什么也会模糊?难道是像素挤到一起重叠了?查了下资料,是有像素重叠的情况。真正原因是什么就不太确定了。

参考资料

查看原文

赞 0 收藏 0 评论 0