diff --git a/README.md b/README.md index 476ca8d..7606d41 100644 --- a/README.md +++ b/README.md @@ -1,265 +1,91 @@ -# util +

@antv/util

-> AntV 底层依赖的工具库,不建议在自己业务中使用。 +
+ +AntV 底层依赖的工具库,包含有所有的 util 纯函数,**不建议在自己业务中使用**,避免因为迭代过程的 Break Change 给您带来维护成本。 [![Build Status](https://github.com/antvis/util/workflows/build/badge.svg)](https://github.com/antvis/util/actions) [![npm Version](https://img.shields.io/npm/v/@antv/util.svg)](https://www.npmjs.com/package/@antv/util) [![npm Download](https://img.shields.io/npm/dm/@antv/util.svg)](https://www.npmjs.com/package/@antv/util) [![npm License](https://img.shields.io/npm/l/@antv/util.svg)](https://www.npmjs.com/package/@antv/util) -## Usage - -```ts -import { gradient } from '@antv/util'; -``` - -## 原则 +
-- util 只有一个 npm 包,按照目录来组织不同类型的方法,避免 monorepo 互相依赖 -- 内容和 AntV 强相关,避免做和 lodash 等相同的工具库 -- 不使用的方法,及时删除,并保持新增方法可以按需引入 -- 旧版本的不维护,如果 AntV 技术栈的旧版本需要迭代,请升级到 v3 -## API +## ✨ 特性 -提供以下 Path 工具方法,包含转换、几何计算等。 +- **轻量级**:按需实现代码逻辑,尽可能的减少包大小,目前约 `12kb`;在 lodash 未优化包大小之前,不要在 AntV 中使用 lodash。 +- **工具丰富**:包含了不同类别的函数上百种。 +- **可视化**:特化工具可视化中使用的颜色、形状、路径、向量、矩阵等方向的功能。 -### path2String -将 PathArray 转换成字符串形式,不会对原始定义中的命令进行修改: +## 📦 安装 -```js -const str: PathArray = [ - ['M', 10, 10], - ['L', 100, 100], - ['l', 10, 10], - ['h', 20], - ['v', 20], -]; -expect(path2String(str)).toEqual('M10 10L100 100l10 10h20v20'); -``` - -### path2Array - -将 PathArray 转换成数组,不会对原始定义中的命令进行修改: - -```js -const str = 'M10 10L100 100l10 10h20v20'; -expect(path2Array(str)).toEqual([ - ['M', 10, 10], - ['L', 100, 100], - ['l', 10, 10], - ['h', 20], - ['v', 20], -]); +```bash +$ npm install @antv/util ``` -### path2Absolute - -将定义中的相对命令转换成绝对命令,例如: +## 🔨 上手 -- l -> L -- h -> H -- v -> V - -完整方法签名如下: - -```js -path2Absolute(pathInput: string | PathArray): AbsoluteArray; -``` +```ts +import { path2String, path2Array } from '@antv/util'; -```js -const str: PathArray = [ +path2String([ ['M', 10, 10], ['L', 100, 100], ['l', 10, 10], ['h', 20], ['v', 20], -]; -const arr = path2Absolute(str); -expect(arr).toEqual([ - ['M', 10, 10], - ['L', 100, 100], - ['L', 110, 110], - ['H', 130], - ['V', 130], -]); -``` - -### path2Curve - -将部分命令转曲,例如 L / A 转成 C 命令,借助 cubic bezier 易于分割的特性用于实现形变动画。 -该方法内部会调用 [path2Absolute](#path2Absolute),因此最终返回的 PathArray 中仅包含 M 和 C 命令。 +]); +// ----> 'M10 10L100 100l10 10h20v20' -完整方法签名如下: - -```js -path2Curve(pathInput: string | PathArray): CurveArray; +path2Array('M10 10L100 100l10 10h20v20'); +/** + * -------> + * [ + * ['M', 10, 10], + * ['L', 100, 100], + * ['l', 10, 10], + * ['h', 20], + * ['v', 20], + * ] + */ ``` -```js -expect( - path2Curve([ - ['M', 0, 0], - ['L', 100, 100], - ]), -).toEqual([ - ['M', 0, 0], - ['C', 44.194173824159215, 44.194173824159215, 68.75, 68.75, 100, 100], -]); -``` -### clonePath +## 📎 API -复制路径: +- [color(颜色)](./docs/api/color.md) - 颜色格式转化、g 渐变转化 css 渐变等 +- [dom(元素)](./docs/api/dom.md) - 基础和 css 添加 +- [math(数学)](./docs/api/math.md) - 基础数学计算,包含判断点是否在两个点的线段上、判断点十分在多变形内等 +- [matrix(矩阵)](./docs/api/matrix.md) - 向量及矩阵计算方法 +- [path(图形)](./docs/api/path.md) - 图形绘画计算方法,包含转换、几何计算等 +- [lodash(通用方法)](./docs/api/lodash.md) - util 内置 lodash 通用方法,并添加了更多针对 AntV 的方法工具。 -```js -const cloned = clonePath(pathInput); -``` - -### reverseCurve - -```js -const pathArray: CurveArray = [ - ['M', 170, 90], - ['C', 150, 90, 155, 10, 130, 10], - ['C', 105, 10, 110, 90, 90, 90], - ['C', 70, 90, 75, 10, 50, 10], - ['C', 25, 10, 30, 90, 10, 90], -]; - -const reversed = reverseCurve(pathArray); -``` -### getPathBBox +## 🚥 原则 -获取几何定义下的包围盒,形如: - -```js -export interface PathBBox { - width: number; - height: number; - x: number; - y: number; - x2: number; - y2: number; - cx: number; - cy: number; - cz: number; -} -``` - -```js -const bbox = getPathBBox([['M', 0, 0], ['L', 100, 0], ['L', 100, 100], ['L', 0, 100], ['Z']]); - -expect(bbox).toEqual({ cx: 50, cy: 50, cz: 150, height: 100, width: 100, x: 0, x2: 100, y: 0, y2: 100 }); -``` +- util 只有一个 npm 包,按照目录来组织不同类型的方法,避免 monorepo 互相依赖。 +- 内容和 AntV 强相关,避免做和 lodash 等相同的工具库。 +- 不使用的方法,及时删除,并保持新增方法可以按需引入。 +- 保持单元测试、文档的完整性。 +- 旧版本不维护,如果 AntV 技术栈的旧版本需要迭代,请升级到 v3。 -### getTotalLength -获取路径总长度。 +## 📮 贡献 -```js -const length = getTotalLength([['M', 0, 0], ['L', 100, 0], ['L', 100, 100], ['L', 0, 100], ['Z']]); - -expect(length).toEqual(400); -``` - -### getPointAtLength - -获取路径上从起点出发,到指定距离的点。 - -```js -const point = getPointAtLength([['M', 0, 0], ['L', 100, 0], ['L', 100, 100], ['L', 0, 100], ['Z']], 0); -expect(point).toEqual({ x: 0, y: 0 }); -``` - -### getPathArea - -计算路径包围的面积。内部实现中首先通过 [path2Curve](#path2Curve) 转曲,再计算 cubic curve 面积,[详见](https://stackoverflow.com/a/15845996)。 - -方法签名如下: - -```js -function getPathArea(path: PathArray): number; -``` - -### isPointInStroke - -判断一个点是否在路径上,仅通过几何定义计算,不考虑其他样式属性例如线宽、lineJoin、miter 等。 - -方法签名如下: - -```js -isPointInStroke(pathInput: string | PathArray, point: Point): boolean; -``` - -```js -const result = isPointInStroke(segments, { x: 10, y: 10 }); -``` - -### distanceSquareRoot - -计算两点之间的距离。 - -方法签名如下: - -```js -distanceSquareRoot(a: [number, number], b: [number, number]): number; -``` - -### equalizeSegments - -将两条路径处理成段数相同,用于形变动画前的分割操作。 - -```js -const [formattedPath1, formattedPath2] = equalizeSegments(path1, path2); -``` - -### isPointInPolygon - -判断一个点是否在多边形内。多边形形如: - -```js -const polygon = [ - [0, 0], - [0, 100], - [30, 100], - [30, 0], -]; - -// [0, 0] 在多边形的边上 -isPointInPolygon(polygon, 0, 0); // true -``` - -### isPolygonsIntersect - -判断两个多边形是否相交: - -```js -isPolygonsIntersect(polygon1, polygon2); -``` +```bash +$ git clone git@github.com:antvis/util.git -## Benchmarks +$ cd util -Build first. +$ npm i -```bash -yarn run benchmarks -``` +$ npm t +```📁 -We can get the following output in the console, it can be seen that the same method from 5.0 is ~3 times faster than 4.0. +写完代码之后,提交 PR 即可。 -```js -// logs -// Path2String#4.0 x 14,795 ops/sec ±3.35% (79 runs sampled) -// Path2String#5.0 x 51,710 ops/sec ±2.05% (85 runs sampled) -// Fastest is Path2String#5.0 - -// Path2Absolute#4.0 x 14,524 ops/sec ±2.55% (80 runs sampled) -// Path2Absolute#5.0 x 35,120 ops/sec ±3.10% (81 runs sampled) -// Fastest is Path2Absolute#5.0 -``` ## License diff --git a/docs/api/color.md b/docs/api/color.md new file mode 100644 index 0000000..c44cc64 --- /dev/null +++ b/docs/api/color.md @@ -0,0 +1,305 @@ +# 颜色 `color` 相关方法 + +> 主要包含颜色语法的转化,渐变色,颜色数据结构额度变换等相关的方法。 + +- [toHex](#tohex) - 将数值从 0-255 转换成 16 进制字符串 +- [toRGB](#torgb) - 将颜色转换为标准的 RGB 格式('#ffffff')。该方法使用了缓存机制,可以提高重复颜色转换的性能。 +- [arr2rgb](#arr2rgb) - 数组转换成 rgb 颜色 +- [rgb2arr](#rgb2arr) - rgb 颜色转换成数组 +- [gradient](#gradient) - 获取渐变函数 +- [toCSSGradient](#tocssgradient) - 将 g 渐变转换为 css 渐变 + + +## toHex + +将数值从 0-255 转换成 16 进制字符串,主要用于颜色值的转换。 + +- 将 0-255 之间的数值转换为两位十六进制字符串 +- 自动补零,确保返回两位字符串 +- 对小数进行四舍五入处理 +- 返回的十六进制字符串为小写形式 + +```ts +import { toHex } from '@antv/util'; + +// 基本数值转换 +toHex(0); // "00" +toHex(16); // "10" +toHex(255); // "ff" + +// 个位数补零 +toHex(1); // "01" +toHex(9); // "09" + +// 小数四舍五入 +toHex(15.4); // "0f" +toHex(15.6); // "10" + +// 常用颜色值转换 +toHex(255); // "ff" (红/绿/蓝最大值) +toHex(128); // "80" (中间值) +toHex(0); // "00" (红/绿/蓝最小值) +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 需要转换的数值 (0-255) | number | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 两位十六进制字符串 | string | - | + + +## toRGB + +将颜色转换为标准的 RGB 格式('#ffffff')。该方法使用了缓存机制,可以提高重复颜色转换的性能。 + +- 将各种格式的颜色值转换为标准的 RGB 十六进制格式(如 '#ff0000') +- 支持颜色名称、RGB、RGBA 等格式的输入 +- 使用 memoize 进行结果缓存,最多缓存 256 个结果 +- 通过创建隐藏的 DOM 元素来进行颜色计算 + + +```ts +import { toRGB } from '@antv/util'; + +// 颜色名称转换 +toRGB('red'); // '#ff0000' +toRGB('green'); // '#008000' +toRGB('blue'); // '#0000ff' + +// RGB/RGBA 格式转换 +toRGB('rgb(255,0,0)'); // '#ff0000' +toRGB('rgba(0,255,0,1)'); // '#00ff00' + +// 十六进制格式 +toRGB('#f00'); // '#ff0000' +toRGB('#ff0000'); // '#ff0000' (保持不变) + +// 其他颜色名称 +toRGB('lightblue'); // '#add8e6' +toRGB('skyblue'); // '#87ceeb' +toRGB('darkred'); // '#8b0000' +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|--------- | +| color | 输入的颜色值 | string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| rgb | 标准的 RGB 十六进制颜色字符串 | string | - | + + +## arr2rgb + +将 RGB 数组转换成十六进制颜色字符串。 + +- 将包含三个数值(红、绿、蓝)的数组转换为十六进制颜色字符串 +- 每个数值范围应在 0-255 之间 +- 返回格式为 "#RRGGBB" 的颜色字符串 +- 使用 toHex 函数处理每个颜色分量 + +```ts +import { arr2rgb } from '@antv/util'; + +// 基本颜色转换 +arr2rgb([255, 0, 0]); // "#ff0000" (红色) +arr2rgb([0, 255, 0]); // "#00ff00" (绿色) +arr2rgb([0, 0, 255]); // "#0000ff" (蓝色) + +// 混合颜色 +arr2rgb([255, 255, 0]); // "#ffff00" (黄色) +arr2rgb([255, 0, 255]); // "#ff00ff" (紫色) +arr2rgb([0, 255, 255]); // "#00ffff" (青色) + +// 灰度值 +arr2rgb([128, 128, 128]); // "#808080" (灰色) +arr2rgb([0, 0, 0]); // "#000000" (黑色) +arr2rgb([255, 255, 255]); // "#ffffff" (白色) + +// 带小数的值(会被四舍五入) +arr2rgb([128.6, 128.4, 128.5]); // "#808080" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| arr | RGB 颜色值数组 | number[] | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 十六进制颜色字符串 | string | - | + +## rgb2arr + +将十六进制颜色字符串转换为 RGB 数组。 + +- 将 "#RRGGBB" 格式的颜色字符串转换为 [R, G, B] 数组 +- 每个返回值都是 0-255 之间的整数 +- 支持解析标准的六位十六进制颜色值 +- 返回一个包含三个数值的数组,分别表示红、绿、蓝 + +```ts +import { rgb2arr } from '@antv/util'; + +// 基本颜色转换 +rgb2arr('#ff0000'); // [255, 0, 0] (红色) +rgb2arr('#00ff00'); // [0, 255, 0] (绿色) +rgb2arr('#0000ff'); // [0, 0, 255] (蓝色) + +// 混合颜色 +rgb2arr('#ffff00'); // [255, 255, 0] (黄色) +rgb2arr('#ff00ff'); // [255, 0, 255] (紫色) +rgb2arr('#00ffff'); // [0, 255, 255] (青色) + +// 灰度值 +rgb2arr('#808080'); // [128, 128, 128] (灰色) +rgb2arr('#000000'); // [0, 0, 0] (黑色) +rgb2arr('#ffffff'); // [255, 255, 255] (白色) + +// 其他值 +rgb2arr('#a1b2c3'); // [161, 178, 195] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| str | 十六进制颜色字符串 | string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| rgbs | RGB 颜色值数组 | number[] | - | + + +## gradient + +获取颜色渐变函数,用于计算多个颜色之间的过渡值。 + +- 接受多个颜色值,创建一个渐变函数 +- 支持字符串(用'-'分隔)或数组形式的颜色输入 +- 支持颜色名称、十六进制等多种颜色格式 +- 返回一个函数,通过百分比获取渐变颜色 + +```ts +import { gradient } from '@antv/util'; + +// 双色渐变 +const redToBlue = gradient(['#ff0000', '#0000ff']); +redToBlue(0); // "#ff0000" (红色) +redToBlue(0.5); // "#800080" (紫色) +redToBlue(1); // "#0000ff" (蓝色) + +// 多色渐变 +const rainbow = gradient(['#ff0000', '#ffff00', '#00ff00', '#0000ff']); +rainbow(0); // "#ff0000" (红色) +rainbow(0.33); // 接近黄色 +rainbow(0.66); // 接近绿色 +rainbow(1); // "#0000ff" (蓝色) + +// 使用颜色名称 +const gradient1 = gradient('red-yellow-green'); +gradient1(0); // "#ff0000" +gradient1(0.5); // 黄色和绿色的中间色 +gradient1(1); // "#008000" + +// 混合使用颜色名称和十六进制 +const gradient2 = gradient(['red', '#00ff00', 'blue']); +gradient2(0.5); // 绿色和蓝色的中间色 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| colors | 颜色值列表 | string \| string[] | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| gradientFunction | 渐变函数 | (percent: number) => string | - | + +- 注意事项 + +1. 输入颜色可以是颜色名称或十六进制格式 +2. 百分比参数范围应在 0-1 之间 +3. 超出范围的百分比会被限制在 0-1 之间 +4. 非数字的百分比会被转换为 0 +5. 渐变计算是线性的 +6. 返回的颜色始终是十六进制格式 + +## toCSSGradient + +将 G 渐变格式转换为标准的 CSS 渐变格式。 + +- 支持线性渐变(l)和径向渐变(r)的转换 +- 自动调整渐变角度(CSS 和 G 的渐变起始角度差 90 度) +- 将色标位置从 0-1 转换为百分比 +- 保持非渐变颜色值不变 + +```ts +import { toCSSGradient } from '@antv/util'; + +// 线性渐变 +const linear1 = 'l(0) 0:#ffffff 1:#000000'; +toCSSGradient(linear1); +// "linear-gradient(90deg, #ffffff 0%, #000000 100%)" + +const linear2 = 'l(90) 0:#ff0000 0.5:#00ff00 1:#0000ff'; +toCSSGradient(linear2); +// "linear-gradient(180deg, #ff0000 0%, #00ff00 50%, #0000ff 100%)" + +// 带空格的格式 +const linear3 = 'l (45) 0: #fff 1: #000'; +toCSSGradient(linear3); +// "linear-gradient(135deg, #fff 0%, #000 100%)" + +// 径向渐变 +const radial1 = 'r(0.5, 0.5, 0.5) 0:#ffffff 1:#000000'; +toCSSGradient(radial1); +// "radial-gradient(#ffffff 0%, #000000 100%)" + +// 多色停止点的径向渐变 +const radial2 = 'r(0.5, 0.5, 0.5) 0:#ff0000 0.5:#00ff00 1:#0000ff'; +toCSSGradient(radial2); +// "radial-gradient(#ff0000 0%, #00ff00 50%, #0000ff 100%)" + +// 非渐变颜色 +toCSSGradient('#ff0000'); // "#ff0000" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| gradientColor | G 渐变格式的颜色字符串 | string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | CSS 渐变字符串 | string | - | + +- 注意事项 + +1. 线性渐变格式:`l(angle) position:color ...` +2. 径向渐变格式:`r(x, y, r) position:color ...` +3. 位置值范围是 0-1,会被转换为百分比 +4. 支持多个色标点 +5. 自动处理空格 +6. 非渐变颜色会原样返回 diff --git a/docs/api/dom.md b/docs/api/dom.md new file mode 100644 index 0000000..f54ecb4 --- /dev/null +++ b/docs/api/dom.md @@ -0,0 +1,137 @@ +# 元素 `dom` 相关函数 + +> 和 DOM 操作相关的函数。 + +- [createDOM](#createdom) - 从 HTML 字符串创建 DOM 元素。 +- [modifyCSS](#modifycss) - 修改 DOM 元素的 CSS 样式。 + +## createDOM + +从 HTML 字符串创建 DOM 元素。 + +```ts +import { createDOM } from '@antv/util'; + +// 创建简单元素 +const div = createDOM('
Hello World
'); +console.log(div.tagName); // "DIV" +console.log(div.innerHTML); // "Hello World" + +// 创建带属性的元素 +const button = createDOM(''); +console.log(button.className); // "btn" +console.log(button.id); // "myBtn" + +// 创建带嵌套结构的元素 +const complex = createDOM(` +
+

Title

+

Paragraph

+
+`); +console.log(complex.children.length); // 2 + +// 创建带样式的元素 +const styledDiv = createDOM(` +
+ Styled Content +
+`); +console.log(styledDiv.style.color); // "red" + +// 实际应用示例 +function createTooltip(content: string) { + return createDOM(` +
+
${content}
+
+
+ `); +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| str | HTML 字符串 | string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| element | DOM 元素 | HTMLElement | - | + + +## modifyCSS + +修改 DOM 元素的 CSS 样式。 + +```ts +import { modifyCSS } from '@antv/util'; + +// 基本样式修改 +const div = document.createElement('div'); +modifyCSS(div, { + width: '100px', + height: '100px', + backgroundColor: 'red' +}); + +// 使用数值(自动添加单位) +modifyCSS(div, { + padding: 10, // "10px" + margin: '20', // "20px" + fontSize: 16 // "16px" +}); + +// 多个样式组合 +modifyCSS(div, { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)' +}); + +// 链式调用 +const element = modifyCSS(div, { + display: 'flex', + justifyContent: 'center' +}).querySelector('.child'); + +// 实际应用示例 +function createTooltip(content: string) { + const tooltip = document.createElement('div'); + return modifyCSS(tooltip, { + position: 'absolute', + padding: '8px', + backgroundColor: 'rgba(0,0,0,0.75)', + color: 'white', + borderRadius: '4px', + fontSize: '12px', + pointerEvents: 'none' + }); +} + +// 条件样式修改 +function updateElementStyle(element: HTMLElement, isActive: boolean) { + modifyCSS(element, { + backgroundColor: isActive ? '#007bff' : '#6c757d', + color: isActive ? 'white' : '#333', + cursor: isActive ? 'pointer' : 'default' + }); +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| dom | DOM 元素 | HTMLElement \| null \| undefined | - | +| css | CSS 样式对象 | { [key: string]: any } | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| element | DOM 元素 | HTMLElement | - | diff --git a/docs/api/lodash.md b/docs/api/lodash.md new file mode 100644 index 0000000..453c7ad --- /dev/null +++ b/docs/api/lodash.md @@ -0,0 +1,4016 @@ +# 通用类函数方法 + +> 类似于 lodash 的一些方法,考虑到当前 lodash 的方法灵活性高,但是包比较大的问题,所以按需做简单的实现。 + +- 基础类型相关 + - [isBoolean](#isboolean) - 判断布尔值 + - [isNumber](#isnumber) - 判断数字 + - [isString](#isstring) - 判断字符串 + - [isNil](#isnil) - 判断 null/undefined + - [isNull](#isnull) - 判断 null + - [isUndefined](#isundefined) - 判断 undefined + - [isArray](#isarray) - 判断数组 + - [isArrayLike](#isarraylike) - 判断类数组 + - [isObject](#isobject) - 判断对象 + - [isObjectLike](#isobjectlike) - 判断类对象 + - [isPlainObject](#isplainobject) - 判断普通对象 + - [isPrototype](#isprototype) - 判断原型对象 + - [isArguments](#isarguments) - 判断 Arguments + - [isDate](#isdate) - 判断日期 + - [isError](#iserror) - 判断错误 + - [isFunction](#isfunction) - 判断函数 + - [isRegExp](#isregexp) - 判断正则 + - [isElement](#iselement) - 判断 DOM 元素 + - [isType](#istype) - 判断具体类型 + - [getType](#gettype) - 获取值类型 +- 数组 `array` 相关 + - [contains/includes](#containsincludes) - 检查数组是否包含指定值 + - [find](#find) - 查找满足条件的第一个元素 + - [findIndex](#findindex) - 查找满足条件元素的索引 + - [indexOf](#indexof) - 查找元素索引 + - [firstValue](#firstvalue) - 获取对象数组中指定属性的第一个非空值 + - [head](#head) - 获取数组或类数组对象的第一个元素 + - [last](#last) - 获取数组或类数组对象的最后一个元素 + - [startsWith](#startswith) - 检查数组是否以指定元素开头 + - [endsWith](#endswith) - 检查数组是否以指定元素结尾 + - [every](#every) - 检查所有元素是否满足条件 + - [some](#some) - 检查是否存在满足条件的元素 + - [flatten](#flatten) - 数组扁平化一层 + - [flattenDeep](#flattendeep) - 数组完全扁平化 + - [map](#map) - 映射数组元素 + - [toArray](#toarray) - 转换为数组 + - [filter](#filter) - 过滤数组元素 + - [pull](#pull) - 移除指定值 + - [pullAt](#pullat) - 按索引移除元素 + - [remove](#remove) - 按条件移除元素 + - [sortBy](#sortby) - 对对象数组排序 + - [group](#group) - 数组分组为数组列表 + - [groupBy](#groupby) - 数组分组为对象 + - [groupToMap](#grouptomap) - 数组分组为映射对象 + - [reduce](#reduce) - 数组归并操作 + - [difference](#difference) - 计算数组差集 + - [union](#union) - 合并数组并去重 + - [uniq](#uniq) - 数组去重 + - [valuesOfKey](#valuesofkey) - 提取指定键的唯一值列表 +- 对象 `object` 相关 + - [get](#get) - 获取深层属性值 + - [set](#set) - 设置深层属性值 + - [has/hasKey](#hashaskey) - 检查属性存在 + - [hasValue](#hasvalue) - 检查值存在 + - [keys](#keys) - 获取所有键名 + - [values](#values) - 获取所有值 + - [mapValues](#mapvalues) - 映射对象值 + - [pick](#pick) - 选取指定属性 + - [omit](#omit) - 忽略指定属性 + - [mix/assiage](#mixassiage) - 混合对象属性 + - [deepMix](#deepmix) - 深度混合对象 + - [clone](#clone) - 深度克隆对象 + - [isMatch](#ismatch) - 检查对象是否匹配 + - [isEqual](#isequal) - 判断两个值是否相等 + - [isEqualWith](#isequalwith) - 自定义相等判断 + - [isEmpty](#isempty) - 判断值是否为空 +- 数值 `number` 相关 + - [isDecimal](#isdecimal) - 判断小数 + - [isEven](#iseven) - 判断偶数 + - [isInteger](#isinteger) - 判断整数 + - [isNegative](#isnegative) - 判断负数 + - [isNumberEqual](#isnumberequal) - 判断数值相等 + - [isOdd](#isodd) - 判断奇数 + - [isPositive](#ispositive) - 判断正数 + - [isFinite](#isfinite) - 判断有限数 + - [clamp](#clamp) - 限制数值范围 + - [fixedBase](#fixedbase) - 格式化数值 + - [max](#max) - 计算最大值 + - [min](#min) - 计算最小值 + - [maxBy](#maxby) - 条件最大值 + - [minBy](#minby) - 条件最小值 + - [mod](#mod) - 模运算 + - [getRange](#getrange) - 计算数值范围 + - [numberToColor](#numbertocolor) - 数字转颜色 + - [parseRadius](#parseradius) - 解析圆角 + - [toDegree](#todegree) - 弧度转角度 + - [toRadian](#toradian) - 角度转弧度 +- 字符串 `string` 相关 + - [lowerCase](#lowercase) - 转小写 + - [upperCase](#uppercase) - 转大写 + - [lowerFirst](#lowerfirst) - 首字母小写 + - [upperFirst](#upperfirst) - 首字母大写 + - [substitute](#substitute) - 模板替换 + - [toString](#tostring) - 转字符串 +- 函数 `function` 相关 + - [debounce](#debounce) - 函数防抖 + - [throttle](#throttle) - 函数节流 + - [memoize](#memoize) - 函数结果缓存 + - [getWrapBehavior](#getwrapbehavior) - 获取包装函数 + - [wrapBehavior](#wrapbehavior) - 创建包装函数 +- 其他工具函数 + - [augment](#augment) - 扩展原型 + - [each](#each) - 遍历集合 + - [extend](#extend) - 类继承 + - [requestAnimationFrame](#requestanimationframe) - 请求动画帧 + - [cancelAnimationFrame](#cancelanimationframe) - 取消动画帧 + - [size](#size) - 获取集合大小 + - [uniqueId](#uniqueid) - 生成唯一 ID + +## contains/includes + +检查数组或类数组对象中是否包含指定值。 + + + +- 示例 + +```ts +import { contains } from '@antv/util'; + +// 基本使用 +const numbers = [1, 2, 3, 4, 5]; +console.log(contains(numbers, 3)); // true +console.log(contains(numbers, 6)); // false + +// 字符串数组 +const fruits = ['apple', 'banana', 'orange']; +console.log(contains(fruits, 'banana')); // true +console.log(contains(fruits, 'grape')); // false + +// 类数组对象 +const arrayLike = { + 0: 'a', + 1: 'b', + 2: 'c', + length: 3 +}; +console.log(contains(arrayLike, 'b')); // true + +// 实际应用示例 +interface User { + id: number; + role: string; +} + +const users: User[] = [ + { id: 1, role: 'admin' }, + { id: 2, role: 'user' } +]; + +// 检查权限 +const hasRole = (roles: string[], userRole: string) => + contains(roles, userRole); + +console.log(hasRole(['admin', 'editor'], 'admin')); // true +console.log(hasRole(['user', 'guest'], 'admin')); // false + +// 检查 ID 是否存在 +const isIdExists = (ids: number[], userId: number) => + contains(ids, userId); + +console.log(isIdExists([1, 2, 3], 2)); // true +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | any[] | - | 要检查的数组 | +| value | 值 | any | - | 要查找的值 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 检查结果 | boolean | - | 是否包含该值 | + +## difference + +计算两个数组的差集,返回第一个数组中不在第二个数组中的元素。 + +- 示例 + +```ts +import { difference } from '@antv/util'; + +// 基本使用 +const arr1 = [1, 2, 3, 4]; +const arr2 = [2, 4]; +console.log(difference(arr1, arr2)); // [1, 3] + +// 字符串数组 +const fruits1 = ['apple', 'banana', 'orange']; +const fruits2 = ['banana', 'grape']; +console.log(difference(fruits1, fruits2)); // ['apple', 'orange'] + +// 空数组处理 +console.log(difference([1, 2, 3])); // [1, 2, 3] +console.log(difference([], [1, 2])); // [] + +// 实际应用示例 +interface Item { + id: number; + name: string; +} + +const oldItems: Item[] = [ + { id: 1, name: 'A' }, + { id: 2, name: 'B' }, + { id: 3, name: 'C' } +]; + +const newItems: Item[] = [ + { id: 2, name: 'B' }, + { id: 3, name: 'C' }, + { id: 4, name: 'D' } +]; + +// 找出删除的项 +const removedItems = difference( + oldItems.map(item => item.id), + newItems.map(item => item.id) +); +console.log(removedItems); // [1] + +// 找出新增的项 +const addedItems = difference( + newItems.map(item => item.id), + oldItems.map(item => item.id) +); +console.log(addedItems); // [4] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 源数组 | T[] | - | 要处理的数组 | +| values | 排除值 | T[] | [] | 要排除的值数组 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 差集结果 | T[] | - | 差集数组 | + +## find + +在数组中查找满足条件的第一个元素。 + +- 示例 + +```ts +import { find } from '@antv/util'; + +// 基本使用 +const numbers = [1, 2, 3, 4, 5]; +console.log(find(numbers, n => n > 3)); // 4 + +// 对象数组查找 +const users = [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' } +]; + +// 使用函数查找 +console.log(find(users, user => user.id === 2)); +// { id: 2, name: 'Jane' } + +// 使用对象匹配 +console.log(find(users, { id: 2 })); +// { id: 2, name: 'Jane' } + +// 找不到时返回 null +console.log(find(users, { id: 3 })); // null + +// 实际应用示例 +interface User { + id: number; + name: string; + role: string; +} + +const userList: User[] = [ + { id: 1, name: 'John', role: 'admin' }, + { id: 2, name: 'Jane', role: 'user' }, + { id: 3, name: 'Bob', role: 'user' } +]; + +// 查找管理员 +const admin = find(userList, { role: 'admin' }); +console.log(admin); // { id: 1, name: 'John', role: 'admin' } + +// 查找指定 ID 的用户 +const findUserById = (id: number) => find(userList, { id }); +console.log(findUserById(2)); // { id: 2, name: 'Jane', role: 'user' } + +// 使用函数进行复杂查找 +const findUserByNamePattern = (pattern: string) => + find(userList, user => user.name.toLowerCase().includes(pattern.toLowerCase())); + +console.log(findUserByNamePattern('ja')); // { id: 2, name: 'Jane', role: 'user' } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | T[] | - | 要搜索的数组 | +| predicate | 判断条件 | Function \| object | - | 查找条件 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 查找结果 | T \| null | - | 匹配的元素或 null | + +## findIndex + +查找数组中满足条件的第一个元素的索引。 + +- 示例 + +```ts +import { findIndex } from '@antv/util'; + +// 基本使用 +const numbers = [1, 2, 3, 4, 5]; +console.log(findIndex(numbers, n => n > 3)); // 3 +console.log(findIndex(numbers, n => n > 5)); // -1 + +// 使用起始索引 +console.log(findIndex(numbers, n => n > 3, 4)); // 4 + +// 带索引的查找 +console.log(findIndex(numbers, (n, i) => i > 2 && n > 3)); // 3 + +// 实际应用示例 +interface User { + id: number; + name: string; + active: boolean; +} + +const users: User[] = [ + { id: 1, name: 'John', active: false }, + { id: 2, name: 'Jane', active: true }, + { id: 3, name: 'Bob', active: true } +]; + +// 查找第一个激活用户 +const activeUserIndex = findIndex(users, user => user.active); +console.log(activeUserIndex); // 1 + +// 查找指定 ID 的用户 +const findUserById = (id: number) => + findIndex(users, user => user.id === id); + +console.log(findUserById(2)); // 1 +console.log(findUserById(4)); // -1 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | T[] | - | 要搜索的数组 | +| predicate | 判断函数 | (item: T, idx?: number) => boolean | - | 条件函数 | +| fromIndex | 起始位置 | number | 0 | 开始搜索的位置 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| index | 索引值 | number | - | 找到的索引或-1 | + +## firstValue + +获取对象数组中指定属性的第一个非空值。 + +- 示例 + +```ts +import { firstValue } from '@antv/util'; + +// 基本使用 +const data = [ + { name: null }, + { name: 'John' }, + { name: 'Jane' } +]; +console.log(firstValue(data, 'name')); // "John" + +// 数组属性 +const items = [ + { tags: [] }, + { tags: ['a', 'b'] }, + { tags: ['c'] } +]; +console.log(firstValue(items, 'tags')); // "a" + +// 实际应用示例 +interface DataItem { + id?: number; + value?: string | number; + items?: string[]; +} + +const dataset: DataItem[] = [ + { id: 1 }, + { id: 2, value: 'test' }, + { id: 3, value: 123 }, + { id: 4, items: ['x', 'y'] } +]; + +// 获取第一个有效值 +console.log(firstValue(dataset, 'value')); // "test" +console.log(firstValue(dataset, 'items')); // "x" + +// 处理不存在的属性 +console.log(firstValue(dataset, 'missing')); // null +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| data | 对象数组 | object[] | - | 要搜索的数组 | +| name | 属性名 | string | - | 要获取的属性名 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| value | 属性值 | any | null | 找到的第一个值或 null | + +## flatten + +将数组扁平化一层。 + +- 示例 + +```ts +import { flatten } from '@antv/util'; + +// 基本使用 +console.log(flatten([1, [2, 3], 4])); // [1, 2, 3, 4] +console.log(flatten([1, [2, [3]], 4])); // [1, 2, [3], 4] + +// 处理空数组 +console.log(flatten([])); // [] +console.log(flatten([[], []])); // [] + +// 实际应用示例 +// 处理嵌套数据 +const nestedData = [ + [1, 2], + [3, 4], + [5, 6] +]; +console.log(flatten(nestedData)); // [1, 2, 3, 4, 5, 6] + +// 处理分组数据 +const groups = [ + ['A', 'B'], + ['C'], + ['D', 'E'] +]; +console.log(flatten(groups)); // ['A', 'B', 'C', 'D', 'E'] + +// 处理对象数组 +interface Item { + id: number; + name: string; +} + +const items: Item[][] = [ + [ + { id: 1, name: 'A' }, + { id: 2, name: 'B' } + ], + [ + { id: 3, name: 'C' } + ] +]; + +console.log(flatten(items)); +// [ +// { id: 1, name: 'A' }, +// { id: 2, name: 'B' }, +// { id: 3, name: 'C' } +// ] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | T[] | - | 要扁平化的数组 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 扁平化数组 | T[] | - | 降维后的数组 | + +## flattenDeep + +将数组完全扁平化,递归处理所有嵌套层级。 + +- 示例 + +```ts +import { flattenDeep } from '@antv/util'; + +// 基本使用 +console.log(flattenDeep([1, [2, [3, [4]], 5]])); // [1, 2, 3, 4, 5] + +// 处理空数组 +console.log(flattenDeep([])); // [] +console.log(flattenDeep([[], [[]]])); // [] + +// 处理混合类型 +const mixed = [1, [2, ['a', ['b']], { x: 1 }]]; +console.log(flattenDeep(mixed)); +// [1, 2, 'a', 'b', { x: 1 }] + +// 处理对象数组 +interface Item { + id: number; + children?: Item[]; +} + +const nested: Item[] = [ + { + id: 1, + children: [ + { id: 2 }, + { + id: 3, + children: [{ id: 4 }] + } + ] + } +]; + +const flattened = flattenDeep(nested); +console.log(flattened); +// [ +// { id: 1, children: [...] }, +// { id: 2 }, +// { id: 3, children: [...] }, +// { id: 4 } +// ] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | any[] | - | 要扁平化的数组 | +| result | 结果数组 | any[] | [] | 存放结果的数组 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 扁平化数组 | any[] | - | 完全扁平化后的数组 | + +## getRange + +获取数值数组的范围(最小值和最大值)。 + +- 示例 + +```ts +import { getRange } from '@antv/util'; + +// 基本使用 +console.log(getRange([1, 2, 3, 4, 5])); +// { min: 1, max: 5 } + +// 处理 NaN +console.log(getRange([1, NaN, 3, 4, NaN])); +// { min: 1, max: 4 } + +// 处理空数组 +console.log(getRange([])); +// { min: 0, max: 0 } + +// 处理嵌套数组 +console.log(getRange([[1, 2], [3, 4], [5]])); +// { min: 1, max: 5 } + +// 实际应用 +const data = [2.5, 3.8, 1.2, 4.9, 2.1]; +const { min, max } = getRange(data); +console.log(`Range: ${min} - ${max}`); +// "Range: 1.2 - 4.9" + +// 处理数据集 +const dataset = [ + [1.1, 2.2, 3.3], + [2.5, 4.5], + [0.5, 5.5] +]; +console.log(getRange(dataset)); +// { min: 0.5, max: 5.5 } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| values | 数值数组 | number[] | - | 要计算范围的数组 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 范围对象 | RangeType | - | 包含最小值和最大值的对象 | + +## pull + +从数组中移除指定的值,会修改原数组。 + +- 示例 + +```ts +import { pull } from '@antv/util'; + +// 基本使用 +const numbers = [1, 2, 3, 2, 4, 2, 5]; +console.log(pull(numbers, 2)); // [1, 3, 4, 5] +console.log(numbers); // [1, 3, 4, 5] + +// 移除多个值 +const letters = ['a', 'b', 'c', 'a', 'b', 'c']; +console.log(pull(letters, 'a', 'c')); // ['b', 'b'] + +// 处理对象数组 +interface Item { + id: number; + value: string; +} + +const items: Item[] = [ + { id: 1, value: 'a' }, + { id: 2, value: 'b' }, + { id: 1, value: 'c' } +]; + +const target = { id: 1, value: 'a' }; +console.log(pull(items, target)); +// [ +// { id: 2, value: 'b' }, +// { id: 1, value: 'c' } +// ] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | T[] | - | 要修改的数组 | +| values | 要移除的值 | ...any[] | - | 要移除的值列表 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 修改后的数组 | T[] | - | 移除指定值后的数组 | + +## pullAt + +根据索引移除数组中的元素,会修改原数组。 + +- 示例 + +```ts +import { pullAt } from '@antv/util'; + +// 基本使用 +const numbers = [1, 2, 3, 4, 5]; +console.log(pullAt(numbers, [1, 3])); // [1, 3, 5] +console.log(numbers); // [1, 3, 5] + +// 处理重复索引 +const letters = ['a', 'b', 'c', 'd']; +console.log(pullAt(letters, [1, 1, 1])); // ['a', 'c', 'd'] + +// 处理对象数组 +interface Item { + id: number; + name: string; +} + +const items: Item[] = [ + { id: 1, name: 'a' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' } +]; + +console.log(pullAt(items, [0, 2])); +// [ +// { id: 2, name: 'b' }, +// { id: 4, name: 'd' } +// ] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | T[] | - | 要修改的数组 | +| indexes | 索引数组 | number[] | - | 要移除的位置索引 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 修改后的数组 | T[] | - | 移除元素后的数组 | + +## reduce + +对数组或对象进行归并操作,类似 Array.reduce。 + +- 示例 + +```ts +import { reduce } from '@antv/util'; + +// 数组归并 +const numbers = [1, 2, 3, 4]; +const sum = reduce(numbers, (acc, curr) => acc + curr, 0); +console.log(sum); // 10 + +// 对象归并 +const obj = { a: 1, b: 2, c: 3 }; +const result = reduce(obj, (acc, value, key) => { + acc[key] = value * 2; + return acc; +}, {}); +console.log(result); // { a: 2, b: 4, c: 6 } + +// 复杂示例 +interface Item { + value: number; + weight: number; +} + +const items: Item[] = [ + { value: 10, weight: 2 }, + { value: 15, weight: 3 }, + { value: 20, weight: 4 } +]; + +// 计算加权平均 +const weightedAvg = reduce(items, + (acc, item) => { + acc.sum += item.value * item.weight; + acc.weights += item.weight; + return acc; + }, + { sum: 0, weights: 0 } +); + +console.log(weightedAvg.sum / weightedAvg.weights); // 16 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组或对象 | G[] \| ObjectType | - | 要归并的数组或对象 | +| fn | 归并函数 | (result: T, data: G, idx: string \| number) => T | - | 处理每个元素的函数 | +| init | 初始值 | T | - | 归并的初始值 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 归并结果 | T | - | 归并操作的结果 | + +## remove + +根据条件移除数组中的元素,返回被移除的元素,会修改原数组。 + +- 示例 + +```ts +import { remove } from '@antv/util'; + +// 基本使用 +const numbers = [1, 2, 3, 4, 5]; +const evens = remove(numbers, n => n % 2 === 0); +console.log(evens); // [2, 4] +console.log(numbers); // [1, 3, 5] + +// 使用索引 +const letters = ['a', 'b', 'c', 'd']; +const removed = remove(letters, (_, i) => i > 1); +console.log(removed); // ['c', 'd'] +console.log(letters); // ['a', 'b'] + +// 对象数组 +interface Item { + id: number; + active: boolean; +} + +const items: Item[] = [ + { id: 1, active: false }, + { id: 2, active: true }, + { id: 3, active: true } +]; + +const inactive = remove(items, item => !item.active); +console.log(inactive); // [{ id: 1, active: false }] +console.log(items); // [{ id: 2, active: true }, { id: 3, active: true }] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | T[] | - | 要修改的数组 | +| predicate | 判断函数 | (value: T, idx: number, arr?: T[]) => boolean | - | 返回 true 表示移除该元素 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 移除的元素 | T[] | - | 被移除的元素数组 | + +## sortBy + +对对象数组进行排序,支持函数、字符串或字符串数组作为排序依据。 + +- 示例 + +```ts +import { sortBy } from '@antv/util'; + +// 使用字符串键排序 +const users = [ + { name: 'Bob', age: 30 }, + { name: 'Alice', age: 25 }, + { name: 'Carol', age: 35 } +]; + +console.log(sortBy(users, 'age')); +// [ +// { name: 'Alice', age: 25 }, +// { name: 'Bob', age: 30 }, +// { name: 'Carol', age: 35 } +// ] + +// 使用多个键排序 +const items = [ + { type: 'A', value: 2 }, + { type: 'B', value: 1 }, + { type: 'A', value: 1 } +]; + +console.log(sortBy(items, ['type', 'value'])); +// [ +// { type: 'A', value: 1 }, +// { type: 'A', value: 2 }, +// { type: 'B', value: 1 } +// ] + +// 使用函数排序 +const numbers = [ + { value: -3 }, + { value: 1 }, + { value: -2 } +]; + +console.log(sortBy(numbers, item => Math.abs(item.value))); +// [ +// { value: 1 }, +// { value: -2 }, +// { value: -3 } +// ] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 对象数组 | ObjectType[] | - | 要排序的数组 | +| key | 排序依据 | Function \| string \| string[] | - | 排序的键或函数 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 排序后的数组 | ObjectType[] | - | 排序后的数组(修改原数组) | + +## union + +合并多个数组并去除重复值。 + +- 示例 + +```ts +import { union } from '@antv/util'; + +// 基本使用 +console.log(union([1, 2], [2, 3], [3, 4])); // [1, 2, 3, 4] + +// 处理重复值 +console.log(union( + [1, 1, 2], + [2, 2, 3], + [3, 3, 4] +)); // [1, 2, 3, 4] + +// 混合类型 +console.log(union( + ['a', 1], + [1, 'b'], + ['b', 2] +)); // ['a', 1, 'b', 2] + +// 对象数组 +const arr1 = [{ id: 1 }, { id: 2 }]; +const arr2 = [{ id: 2 }, { id: 3 }]; +console.log(union(arr1, arr2)); +// [{ id: 1 }, { id: 2 }, { id: 2 }, { id: 3 }] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| sources | 源数组列表 | ...any[][] | - | 要合并的数组列表 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 合并结果 | any[] | - | 去重后的合并数组 | + +## uniq + +数组去重,返回新数组。 + +- 示例 + +```ts +import { uniq } from '@antv/util'; + +// 基本使用 +console.log(uniq([1, 2, 2, 3, 3, 3])); // [1, 2, 3] + +// 混合类型 +console.log(uniq([1, '1', true, 1, true])); // [1, '1', true] + +// 使用缓存 +const cache = new Map(); +console.log(uniq([1, 2, 2, 3], cache)); // [1, 2, 3] +console.log(uniq([2, 3, 3, 4], cache)); // [4] + +// 对象数组 +const objects = [ + { id: 1 }, + { id: 2 }, + { id: 1 } +]; +console.log(uniq(objects)); +// [{ id: 1 }, { id: 2 }, { id: 1 }] // 注意:对象引用不同 + +// 空数组处理 +console.log(uniq([])); // [] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | any[] | - | 要去重的数组 | +| cache | 缓存对象 | Map | new Map() | 用于提升性能的缓存 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 去重数组 | any[] | - | 去重后的新数组 | + +## valuesOfKey + +从对象数组中提取指定键的唯一值列表。 + +- 示例 + +```ts +import { valuesOfKey } from '@antv/util'; + +// 基本使用 +const data = [ + { color: 'red' }, + { color: 'blue' }, + { color: 'red' } +]; +console.log(valuesOfKey(data, 'color')); // ['red', 'blue'] + +// 处理数组值 +const items = [ + { categories: ['A', 'B'] }, + { categories: ['B', 'C'] }, + { categories: ['A', 'C'] } +]; +console.log(valuesOfKey(items, 'categories')); // ['A', 'B', 'C'] + +// 混合类型属性 +const mixed = [ + { type: 1 }, + { type: [2, 3] }, + { type: '4' } +]; +console.log(valuesOfKey(mixed, 'type')); // [1, 2, 3, '4'] + +// 处理空值 +const withNulls = [ + { id: 1 }, + { id: null }, + { id: undefined }, + { id: 2 } +]; +console.log(valuesOfKey(withNulls, 'id')); // [1, 2] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| data | 对象数组 | any[] | - | 要处理的数组 | +| name | 键名 | string | - | 要提取的键名 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 值列表 | any[] | - | 去重后的值数组 | + +## head + +获取数组或类数组对象的第一个元素。 + +- 示例 + +```ts +import { head } from '@antv/util'; + +// 基本使用 +console.log(head([1, 2, 3])); // 1 +console.log(head([])); // undefined + +// 字符串 +console.log(head('abc')); // 'a' +console.log(head('')); // undefined + +// 类数组对象 +const arrayLike = { + 0: 'a', + 1: 'b', + length: 2 +}; +console.log(head(arrayLike)); // 'a' + +// 非数组类型 +console.log(head(null)); // undefined +console.log(head(undefined)); // undefined +console.log(head(42)); // undefined +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| o | 输入值 | unknown | - | 要获取首个元素的数组或类数组对象 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 首个元素 | any | undefined | 第一个元素或 undefined | + +## last + +获取数组或类数组对象的最后一个元素。 + +- 示例 + +```ts +import { last } from '@antv/util'; + +// 基本使用 +console.log(last([1, 2, 3])); // 3 +console.log(last([])); // undefined + +// 字符串 +console.log(last('abc')); // 'c' +console.log(last('')); // undefined + +// 类数组对象 +const arrayLike = { + 0: 'a', + 1: 'b', + length: 2 +}; +console.log(last(arrayLike)); // 'b' + +// 非数组类型 +console.log(last(null)); // undefined +console.log(last(undefined)); // undefined +console.log(last(42)); // undefined +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| o | 输入值 | unknown | - | 要获取最后元素的数组或类数组对象 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 最后元素 | any | undefined | 最后一个元素或 undefined | + +## startsWith + +检查数组或字符串是否以指定元素开头。 + +- 示例 + +```ts +import { startsWith } from '@antv/util'; + +// 数组使用 +console.log(startsWith([1, 2, 3], 1)); // true +console.log(startsWith([1, 2, 3], 2)); // false +console.log(startsWith([], 1)); // false + +// 字符串使用 +console.log(startsWith('abc', 'a')); // true +console.log(startsWith('abc', 'b')); // false +console.log(startsWith('', 'a')); // false + +// 对象数组 +const items = [ + { id: 1 }, + { id: 2 } +]; +console.log(startsWith(items, items[0])); // true + +// 类型安全 +console.log(startsWith(['a', 'b'], 1)); // false +console.log(startsWith([1, 2], '1')); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 输入值 | string \| T[] | - | 要检查的数组或字符串 | +| e | 目标元素 | string \| T | - | 要匹配的元素 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 检查结果 | boolean | - | 是否以指定元素开头 | + +## endsWith + +检查数组或字符串是否以指定元素结尾。 + +- 示例 + +```ts +import { endsWith } from '@antv/util'; + +// 数组使用 +console.log(endsWith([1, 2, 3], 3)); // true +console.log(endsWith([1, 2, 3], 2)); // false +console.log(endsWith([], 1)); // false + +// 字符串使用 +console.log(endsWith('abc', 'c')); // true +console.log(endsWith('abc', 'b')); // false +console.log(endsWith('', 'a')); // false + +// 对象数组 +const items = [ + { id: 1 }, + { id: 2 } +]; +console.log(endsWith(items, items[1])); // true + +// 类型安全 +console.log(endsWith(['a', 'b'], 1)); // false +console.log(endsWith([1, 2], '2')); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 输入值 | string \| T[] | - | 要检查的数组或字符串 | +| e | 目标元素 | string \| T | - | 要匹配的元素 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 检查结果 | boolean | - | 是否以指定元素结尾 | + +## filter + +根据条件函数过滤数组元素。 + +- 示例 + +```ts +import { filter } from '@antv/util'; + +// 基本使用 +const numbers = [1, 2, 3, 4, 5]; +console.log(filter(numbers, n => n % 2 === 0)); // [2, 4] + +// 使用索引 +const letters = ['a', 'b', 'c']; +console.log(filter(letters, (_, i) => i > 0)); // ['b', 'c'] + +// 对象数组 +interface Item { + id: number; + active: boolean; +} + +const items: Item[] = [ + { id: 1, active: false }, + { id: 2, active: true }, + { id: 3, active: true } +]; + +console.log(filter(items, item => item.active)); +// [ +// { id: 2, active: true }, +// { id: 3, active: true } +// ] + +// 类数组对象 +const arrayLike = { + 0: 'a', + 1: 'b', + 2: 'c', + length: 3 +}; +console.log(filter(arrayLike as any, v => v > 'a')); // ['b', 'c'] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | T[] | - | 要过滤的数组 | +| func | 过滤函数 | (v: T, idx: number) => boolean | - | 返回 true 表示保留该元素 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 过滤结果 | T[] | - | 过滤后的新数组 | + +## every + +检查数组中的所有元素是否都满足条件。 + +- 示例 + +```ts +import { every } from '@antv/util'; + +// 基本使用 +const numbers = [2, 4, 6, 8]; +console.log(every(numbers, n => n % 2 === 0)); // true +console.log(every(numbers, n => n > 5)); // false + +// 使用索引 +const letters = ['a', 'b', 'c']; +console.log(every(letters, (_, i) => i < 3)); // true + +// 对象数组 +interface Item { + id: number; + active: boolean; +} + +const items: Item[] = [ + { id: 1, active: true }, + { id: 2, active: true }, + { id: 3, active: false } +]; + +console.log(every(items, item => item.active)); // false + +// 空数组 +console.log(every([], () => false)); // true +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | T[] | - | 要检查的数组 | +| func | 检查函数 | (v: T, idx?: number) => any | - | 返回真值表示元素满足条件 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 检查结果 | boolean | - | 是否所有元素都满足条件 | + +## some + +检查数组中是否存在满足条件的元素。 + +- 示例 + +```ts +import { some } from '@antv/util'; + +// 基本使用 +const numbers = [1, 2, 3, 4]; +console.log(some(numbers, n => n > 3)); // true +console.log(some(numbers, n => n > 5)); // false + +// 使用索引 +const letters = ['a', 'b', 'c']; +console.log(some(letters, (_, i) => i === 1)); // true + +// 对象数组 +interface Item { + id: number; + active: boolean; +} + +const items: Item[] = [ + { id: 1, active: false }, + { id: 2, active: true }, + { id: 3, active: false } +]; + +console.log(some(items, item => item.active)); // true + +// 空数组 +console.log(some([], () => true)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| arr | 数组 | T[] | - | 要检查的数组 | +| func | 检查函数 | (v: T, idx?: number) => any | - | 返回真值表示元素满足条件 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 检查结果 | boolean | - | 是否存在满足条件的元素 | + +## group + +将数组按条件分组,返回分组后的数组列表。 + +- 示例 + +```ts +import { group } from '@antv/util'; + +// 使用函数分组 +const numbers = [1, 2, 3, 4, 5]; +console.log(group(numbers, n => n % 2 === 0 ? 'even' : 'odd')); +// [[1, 3, 5], [2, 4]] + +// 使用属性名分组 +const items = [ + { type: 'A', value: 1 }, + { type: 'B', value: 2 }, + { type: 'A', value: 3 } +]; +console.log(group(items, 'type')); +// [ +// [{ type: 'A', value: 1 }, { type: 'A', value: 3 }], +// [{ type: 'B', value: 2 }] +// ] + +// 使用多个属性分组 +const data = [ + { type: 'A', status: 1 }, + { type: 'A', status: 2 }, + { type: 'B', status: 1 } +]; +console.log(group(data, ['type', 'status'])); +// [ +// [{ type: 'A', status: 1 }], +// [{ type: 'A', status: 2 }], +// [{ type: 'B', status: 1 }] +// ] + +// 无条件分组 +console.log(group([1, 2, 3], null)); // [[1, 2, 3]] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| data | 数组 | T[] | - | 要分组的数组 | +| condition | 分组条件 | ((v: T) => string) \| string \| string[] | - | 分组条件 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 分组结果 | T[][] | - | 分组后的数组列表 | + +## groupBy + +按条件将数组分组,返回分组对象。 + +- 示例 + +```ts +import { groupBy } from '@antv/util'; + +// 使用函数分组 +const numbers = [1, 2, 3, 4, 5]; +console.log(groupBy(numbers, n => n % 2 === 0 ? 'even' : 'odd')); +// { +// odd: [1, 3, 5], +// even: [2, 4] +// } + +// 使用属性名分组 +const items = [ + { type: 'A', value: 1 }, + { type: 'B', value: 2 }, + { type: 'A', value: 3 } +]; +console.log(groupBy(items, 'type')); +// { +// A: [ +// { type: 'A', value: 1 }, +// { type: 'A', value: 3 } +// ], +// B: [ +// { type: 'B', value: 2 } +// ] +// } + +// 处理空值 +console.log(groupBy([], n => n)); // {} +console.log(groupBy(numbers, null)); // {} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| data | 数组 | T[] | - | 要分组的数组 | +| condition | 分组条件 | ((item: T) => string) \| string | - | 分组条件函数或属性名 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 分组结果 | ObjectType | - | 分组后的对象 | + +## groupToMap + +将数组按条件分组成映射对象,支持多字段组合分组。 + +- 示例 + +```ts +import { groupToMap } from '@antv/util'; + +// 使用函数分组 +const numbers = [1, 2, 3, 4]; +console.log(groupToMap(numbers, n => `group${n % 2}`)); +// { +// _group0: [2, 4], +// _group1: [1, 3] +// } + +// 使用单个属性分组 +const items = [ + { type: 'A', value: 1 }, + { type: 'B', value: 2 }, + { type: 'A', value: 3 } +]; +console.log(groupToMap(items, 'type')); +// { +// _A: [ +// { type: 'A', value: 1 }, +// { type: 'A', value: 3 } +// ], +// _B: [ +// { type: 'B', value: 2 } +// ] +// } + +// 使用多个属性分组 +const data = [ + { type: 'A', status: 1 }, + { type: 'A', status: 2 }, + { type: 'B', status: 1 } +]; +console.log(groupToMap(data, ['type', 'status'])); +// { +// _A1: [{ type: 'A', status: 1 }], +// _A2: [{ type: 'A', status: 2 }], +// _B1: [{ type: 'B', status: 1 }] +// } + +// 无条件分组 +console.log(groupToMap([1, 2, 3], null)); +// { 0: [1, 2, 3] } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| data | 数组 | any[] | - | 要分组的数组 | +| condition | 分组条件 | string[] \| string \| ((row: any) => string) | - | 分组条件 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 分组结果 | object | - | 分组后的映射对象 | + +## getWrapBehavior + +获取对象方法的事件包装函数。 + +- 示例 + +```ts +import { getWrapBehavior } from '@antv/util'; + +class Button { + private name: string; + + constructor(name: string) { + this.name = name; + } + + onClick(e: Event) { + console.log(`${this.name} clicked`, e); + } + + unbind(element: HTMLElement) { + const handler = getWrapBehavior(this, 'onClick'); + element.removeEventListener('click', handler); + } +} + +// 基本使用 +const button = new Button('test'); +const handler = getWrapBehavior(button, 'onClick'); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| obj | 对象 | object | - | +| action | 方法名 | string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| handler | 包装函数 | Function | - | + +## wrapBehavior + +创建对象方法的事件包装函数。 + +- 示例 + +```ts +import { wrapBehavior } from '@antv/util'; + +class Button { + private name: string; + + constructor(name: string) { + this.name = name; + } + + onClick(e: Event) { + console.log(`${this.name} clicked`, e); + } + + bind(element: HTMLElement) { + const handler = wrapBehavior(this, 'onClick'); + element.addEventListener('click', handler); + } +} + +// 基本使用 +const button = new Button('test'); +const handler = wrapBehavior(button, 'onClick'); + +// 实际应用 +class Component { + private handlers: { [key: string]: Function } = {}; + + constructor(public element: HTMLElement) {} + + on(eventName: string, handler: Function) { + const wrappedHandler = wrapBehavior({ + handleEvent: handler + }, 'handleEvent'); + + this.handlers[eventName] = wrappedHandler; + this.element.addEventListener(eventName, wrappedHandler); + } +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| obj | 对象 | object | - | +| action | 方法名 | string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| handler | 包装函数 | Function | - | + +## numberToColor + +将数字转换为十六进制颜色值,带缓存功能。 + +- 示例 + +```ts +import { numberToColor } from '@antv/util'; + +// 基本使用 +console.log(numberToColor(0)); // "#000000" +console.log(numberToColor(255)); // "#0000ff" +console.log(numberToColor(16711680)); // "#ff0000" + +// 缓存复用 +const color1 = numberToColor(12345); +const color2 = numberToColor(12345); // 使用缓存值 +console.log(color1 === color2); // true +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| num | 数字 | number | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| color | 颜色值 | string | - | + +## parseRadius + +解析圆角半径值,支持单个数值或数组形式。 + +- 示例 + +```ts +import { parseRadius } from '@antv/util'; + +// 单个数值 +console.log(parseRadius(5)); +// { r1: 5, r2: 5, r3: 5, r4: 5 } + +// 数组 - 1个值 +console.log(parseRadius([10])); +// { r1: 10, r2: 10, r3: 10, r4: 10 } + +// 数组 - 2个值 +console.log(parseRadius([5, 10])); +// { r1: 5, r2: 10, r3: 5, r4: 10 } + +// 数组 - 3个值 +console.log(parseRadius([5, 10, 15])); +// { r1: 5, r2: 10, r3: 15, r4: 10 } + +// 数组 - 4个值 +console.log(parseRadius([5, 10, 15, 20])); +// { r1: 5, r2: 10, r3: 15, r4: 20 } + +// 实际应用 +function drawRoundRect(x: number, y: number, width: number, height: number, radius: number | number[]) { + const { r1, r2, r3, r4 } = parseRadius(radius); + // 使用四个圆角绘制矩形... +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| radius | 圆角半径 | number \| number[] | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 四个角的半径 | RadiusType | - | + +## clamp + +将数值限制在指定范围内。 + +- 示例 + +```ts +import { clamp } from '@antv/util'; + +// 基本使用 +console.log(clamp(5, 0, 10)); // 5 +console.log(clamp(-5, 0, 10)); // 0 +console.log(clamp(15, 0, 10)); // 10 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| a | 要限制的数值 | number | - | +| min | 最小值 | number | - | +| max | 最大值 | number | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 限制后的数值 | number | - | + +## fixedBase + +按指定精度格式化数值。 + +- 示例 + +```ts +import { fixedBase } from '@antv/util'; + +// 基本使用 +console.log(fixedBase(3.1415926, 0)); // 3 +console.log(fixedBase(3.1415926, '0.1')); // 3.1 +console.log(fixedBase(3.1415926, '0.00')); // 3.14 + +// 处理长小数 +console.log(fixedBase(1/3, '0.000')); // 0.333 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| v | 要格式化的数值 | number | - | +| base | 精度基准值 | number \| string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 格式化后的数值 | number | - | + +## isDecimal + +判断是否为小数。 + +- 示例 + +```ts +import { isDecimal } from '@antv/util'; + +// 基本使用 +console.log(isDecimal(3.14)); // true +console.log(isDecimal(3)); // false +console.log(isDecimal('3.14')); // false +console.log(isDecimal(null)); // false + +// 特殊值 +console.log(isDecimal(1.0)); // false +console.log(isDecimal(NaN)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| num | 要检查的值 | unknown | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为小数 | boolean | - | + +## isEven + +判断是否为偶数。 + +- 示例 + +```ts +import { isEven } from '@antv/util'; + +// 基本使用 +console.log(isEven(2)); // true +console.log(isEven(3)); // false +console.log(isEven(0)); // true +console.log(isEven(-2)); // true +console.log(isEven(2.2)); // false +console.log(isEven(NaN)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| num | 要检查的数值 | number | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为偶数 | boolean | - | + +## isInteger + +判断是否为整数。 + +- 示例 + +```ts +import { isInteger } from '@antv/util'; + +// 基本使用 +console.log(isInteger(1)); // true +console.log(isInteger(1.1)); // false +console.log(isInteger(0)); // true +console.log(isInteger(-1)); // true +console.log(isInteger(NaN)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的数值 | number | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为整数 | boolean | - | + +## isNegative + +判断是否为负数。 + +- 示例 + +```ts +import { isNegative } from '@antv/util'; + +// 基本使用 +console.log(isNegative(-1)); // true +console.log(isNegative(0)); // false +console.log(isNegative(1)); // false +console.log(isNegative(-1.1)); // true +console.log(isNegative(NaN)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| num | 要检查的数值 | number | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为负数 | boolean | - | + +## isNumberEqual + +判断两个数是否相等(考虑精度)。 + +- 示例 + +```ts +import { isNumberEqual } from '@antv/util'; + +// 基本使用 +console.log(isNumberEqual(0.1 + 0.2, 0.3)); // true +console.log(isNumberEqual(1, 1.000001, 0.001)); // true +console.log(isNumberEqual(1, 2)); // false + +// 自定义精度 +console.log(isNumberEqual(1, 1.1, 0.2)); // true +console.log(isNumberEqual(1, 1.1, 0.01)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| a | 第一个数 | number | - | +| b | 第二个数 | number | - | +| precision | 精度 | number | 0.00001 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否相等 | boolean | - | + +## isOdd + +判断是否为奇数。 + +- 示例 + +```ts +import { isOdd } from '@antv/util'; + +// 基本使用 +console.log(isOdd(1)); // true +console.log(isOdd(2)); // false +console.log(isOdd(-1)); // true +console.log(isOdd(1.1)); // false +console.log(isOdd(NaN)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| num | 要检查的数值 | number | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为奇数 | boolean | - | + +## isPositive + +判断是否为正数。 + +- 示例 + +```ts +import { isPositive } from '@antv/util'; + +// 基本使用 +console.log(isPositive(1)); // true +console.log(isPositive(0)); // false +console.log(isPositive(-1)); // false +console.log(isPositive(1.1)); // true +console.log(isPositive(NaN)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| num | 要检查的数值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为正数 | boolean | - | + +## max + +计算数组的最大值。 + +- 示例 + +```ts +import { max } from '@antv/util'; + +// 基本使用 +console.log(max([1, 2, 3])); // 3 +console.log(max([-1, -2, -3])); // -1 +console.log(max([])); // -Infinity +console.log(max([5])); // 5 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| arr | 数值数组 | number[] | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 最大值 | number | - | + +## maxBy + +根据指定条件计算数组的最大值。 + +- 示例 + +```ts +import { maxBy } from '@antv/util'; + +// 使用函数 +const objects = [{ n: 1 }, { n: 2 }]; +console.log(maxBy(objects, o => o.n)); // { n: 2 } + +// 使用属性名 +console.log(maxBy(objects, 'n')); // { n: 2 } + +// 复杂对象 +const data = [ + { name: 'A', value: 10 }, + { name: 'B', value: 30 }, + { name: 'C', value: 20 } +]; +console.log(maxBy(data, 'value')); // { name: 'B', value: 30 } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| arr | 数组 | T[] | - | +| fn | 迭代函数或属性名 | ((v: T) => number) \| string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 最大值元素 | T \| undefined | - | + +## min + +计算数组的最小值。 + +- 示例 + +```ts +import { min } from '@antv/util'; + +// 基本使用 +console.log(min([1, 2, 3])); // 1 +console.log(min([-1, -2, -3])); // -3 +console.log(min([])); // undefined +console.log(min([5])); // 5 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| arr | 数值数组 | number[] | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 最小值 | number \| undefined | - | + +## minBy + +根据指定条件计算数组的最小值。 + +- 示例 + +```ts +import { minBy } from '@antv/util'; + +// 使用函数 +const objects = [{ n: 1 }, { n: 2 }]; +console.log(minBy(objects, o => o.n)); // { n: 1 } + +// 使用属性名 +console.log(minBy(objects, 'n')); // { n: 1 } + +// 复杂对象 +const data = [ + { name: 'A', value: 10 }, + { name: 'B', value: 30 }, + { name: 'C', value: 20 } +]; +console.log(minBy(data, 'value')); // { name: 'A', value: 10 } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| arr | 数组 | T[] | - | +| fn | 迭代函数或属性名 | ((v: T) => number) \| string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 最小值元素 | T \| undefined | - | + +## mod + +计算正确的模运算结果(包括负数)。 + +- 示例 + +```ts +import { mod } from '@antv/util'; + +// 基本使用 +console.log(mod(5, 3)); // 2 +console.log(mod(-5, 3)); // 1 +console.log(mod(5, -3)); // -1 +console.log(mod(-5, -3)); // -2 + +// 常见应用 +function getCircularIndex(index: number, length: number) { + return mod(index, length); +} + +console.log(getCircularIndex(7, 5)); // 2 +console.log(getCircularIndex(-1, 5)); // 4 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| n | 被除数 | number | - | +| m | 除数 | number | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 模运算结果 | number | - | + +## toDegree + +将弧度转换为角度。 + +- 示例 + +```ts +import { toDegree } from '@antv/util'; + +// 基本使用 +console.log(toDegree(Math.PI)); // 180 +console.log(toDegree(Math.PI / 2)); // 90 +console.log(toDegree(Math.PI / 4)); // 45 +console.log(toDegree(0)); // 0 + +// 实际应用 +const angle = Math.atan2(1, 1); +console.log(toDegree(angle)); // 45 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| radian | 弧度值 | number | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 角度值 | number | - | + +## toRadian + +将角度转换为弧度。 + +- 示例 + +```ts +import { toRadian } from '@antv/util'; + +// 基本使用 +console.log(toRadian(180)); // 3.141592653589793 +console.log(toRadian(90)); // 1.5707963267948966 +console.log(toRadian(45)); // 0.7853981633974483 +console.log(toRadian(0)); // 0 + +// 实际应用 +function rotatePoint(x: number, y: number, angle: number) { + const rad = toRadian(angle); + return { + x: x * Math.cos(rad) - y * Math.sin(rad), + y: x * Math.sin(rad) + y * Math.cos(rad) + }; +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| degree | 角度值 | number | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 弧度值 | number | - | + +## each + +遍历数组或对象的每个元素。 + +- 示例 + +```ts +import { each } from '@antv/util'; + +// 遍历数组 +const arr = [1, 2, 3]; +each(arr, (item, index) => { + console.log(item, index); +}); +// 1 0 +// 2 1 +// 3 2 + +// 遍历对象 +const obj = { a: 1, b: 2, c: 3 }; +each(obj, (value, key) => { + console.log(key, value); +}); +// a 1 +// b 2 +// c 3 + +// 提前终止遍历 +each(arr, (item) => { + if (item === 2) return false; + console.log(item); +}); +// 1 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| elements | 要遍历的数组或对象 | any[] \| object | - | +| func | 遍历函数 | (v: any, k: any) => any | - | + +## has/hasKey + +检查对象是否具有指定属性。 + +- 示例 + +```ts +import { has } from '@antv/util'; + +const obj = { a: 1, b: 2 }; + +console.log(has(obj, 'a')); // true +console.log(has(obj, 'c')); // false +console.log(has(obj, 'toString')); // false (只检查自有属性) +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| obj | 要检查的对象 | object | - | +| key | 属性名 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否具有该属性 | boolean | - | + +## hasValue + +检查对象是否包含指定值。 + +- 示例 + +```ts +import { hasValue } from '@antv/util'; + +const obj = { a: 1, b: 2, c: 3 }; + +console.log(hasValue(obj, 1)); // true +console.log(hasValue(obj, 4)); // false + +// 复杂对象 +const data = { + user: { name: 'John' }, + age: 30 +}; +console.log(hasValue(data, 30)); // true +console.log(hasValue(data, { name: 'John' })); // true +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| obj | 要检查的对象 | object | - | +| value | 要查找的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否包含该值 | boolean | - | + +## keys + +获取对象的所有自有可枚举属性的键名。 + +- 示例 + +```ts +import { keys } from '@antv/util'; + +// 基本使用 +const obj = { a: 1, b: 2, c: 3 }; +console.log(keys(obj)); // ['a', 'b', 'c'] + +// 函数对象 +function Foo() {} +Foo.prototype.bar = 1; +console.log(keys(Foo)); // [] + +// 数组 +console.log(keys(['a', 'b', 'c'])); // ['0', '1', '2'] + +// 空对象 +console.log(keys({})); // [] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| obj | 要获取键名的对象 | object | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 键名数组 | string[] | - | + +## isMatch + +检查对象是否匹配指定的属性值。 + +- 示例 + +```ts +import { isMatch } from '@antv/util'; + +// 基本使用 +const object = { a: 1, b: 2, c: 3 }; + +console.log(isMatch(object, { a: 1 })); // true +console.log(isMatch(object, { a: 1, b: 2 })); // true +console.log(isMatch(object, { a: 1, d: 4 })); // false + +// 空对象 +console.log(isMatch({}, {})); // true +console.log(isMatch(null, {})); // true +console.log(isMatch(undefined, { a: 1 })); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| obj | 要检查的对象 | any | - | +| attrs | 要匹配的属性对象 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否匹配 | boolean | - | + +## values + +获取对象的所有值。 + +- 示例 + +```ts +import { values } from '@antv/util'; + +// 基本使用 +const obj = { a: 1, b: 2, c: 3 }; +console.log(values(obj)); // [1, 2, 3] + +// 函数对象 +function Foo() {} +Foo.a = 1; +console.log(values(Foo)); // [1] + +// 数组 +console.log(values(['a', 'b', 'c'])); // ['a', 'b', 'c'] + +// 空对象 +console.log(values({})); // [] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| obj | 要获取值的对象 | object | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 值数组 | any[] | - | + +## lowerCase + +转换字符串为小写。 + +- 示例 + +```ts +import { lowerCase } from '@antv/util'; + +console.log(lowerCase('Hello')); // "hello" +console.log(lowerCase('WORLD')); // "world" +console.log(lowerCase('Hello123')); // "hello123" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| str | 要转换的字符串 | string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 转换后的字符串 | string | - | + +## lowerFirst + +将字符串的第一个字符转换为小写。 + +- 示例 + +```ts +import { lowerFirst } from '@antv/util'; + +console.log(lowerFirst('Hello')); // "hello" +console.log(lowerFirst('WORLD')); // "wORLD" +console.log(lowerFirst('ABC')); // "aBC" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要转换的字符串 | string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 转换后的字符串 | string | - | + +## substitute + +字符串模板替换。 + +- 示例 + +```ts +import { substitute } from '@antv/util'; + +// 基本使用 +const template = 'Hello {name}!'; +console.log(substitute(template, { name: 'World' })); +// "Hello World!" + +// 多个替换 +const complex = '{greeting} {user}!'; +console.log(substitute(complex, { + greeting: 'Hi', + user: 'John' +})); +// "Hi John!" + +// 转义 +console.log(substitute('\\{escaped} {normal}', { normal: 'value' })); +// "{escaped} value" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| str | 模板字符串 | string | - | +| o | 替换值对象 | ObjectType | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 替换后的字符串 | string | - | + +## upperCase + +转换字符串为大写。 + +- 示例 + +```ts +import { upperCase } from '@antv/util'; + +console.log(upperCase('hello')); // "HELLO" +console.log(upperCase('World')); // "WORLD" +console.log(upperCase('hello123')); // "HELLO123" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| str | 要转换的字符串 | string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 转换后的字符串 | string | - | + +## upperFirst + +将字符串的第一个字符转换为大写。 + +- 示例 + +```ts +import { upperFirst } from '@antv/util'; + +console.log(upperFirst('hello')); // "Hello" +console.log(upperFirst('world')); // "World" +console.log(upperFirst('abc')); // "Abc" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要转换的字符串 | string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 转换后的字符串 | string | - | + +## getType + +获取值的类型字符串。 + +- 示例 + +```ts +import { getType } from '@antv/util'; + +console.log(getType([])); // "Array" +console.log(getType({})); // "Object" +console.log(getType('')); // "String" +console.log(getType(1)); // "Number" +console.log(getType(true)); // "Boolean" +console.log(getType(null)); // "Null" +console.log(getType(undefined)); // "Undefined" +console.log(getType(new Date())); // "Date" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 类型字符串 | string | - | + +## isArguments + +判断值是否为 Arguments 对象。 + +- 示例 + +```ts +import { isArguments } from '@antv/util'; + +function test() { + console.log(isArguments(arguments)); // true +} +test(); + +console.log(isArguments([])); // false +console.log(isArguments({})); // false +console.log(isArguments(null)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为 Arguments 对象 | boolean | - | + +## isArray + +判断值是否为数组。 + +- 示例 + +```ts +import { isArray } from '@antv/util'; + +console.log(isArray([1, 2, 3])); // true +console.log(isArray([])); // true +console.log(isArray(new Array(3))); // true +console.log(isArray('abc')); // false +console.log(isArray({ length: 3 })); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | unknown | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为数组 | boolean | - | + +## isArrayLike + +判断值是否为类数组对象。 + +- 示例 + +```ts +import { isArrayLike } from '@antv/util'; + +console.log(isArrayLike([1, 2, 3])); // true +console.log(isArrayLike('abc')); // true +console.log(isArrayLike({ length: 3 })); // true +console.log(isArrayLike(null)); // false +console.log(isArrayLike(undefined)); // false +console.log(isArrayLike(() => {})); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为类数组对象 | boolean | - | + +## isBoolean + +判断值是否为布尔值。 + +- 示例 + +```ts +import { isBoolean } from '@antv/util'; + +console.log(isBoolean(true)); // true +console.log(isBoolean(false)); // true +console.log(isBoolean(0)); // false +console.log(isBoolean('true')); // false +console.log(isBoolean(new Boolean(true))); // true +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为布尔值 | boolean | - | + +## isDate + +判断值是否为日期对象。 + +- 示例 + +```ts +import { isDate } from '@antv/util'; + +console.log(isDate(new Date())); // true +console.log(isDate(Date.now())); // false +console.log(isDate('2023-01-01')); // false +console.log(isDate({})); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | unknown | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为日期对象 | boolean | - | + +## isError + +判断值是否为错误对象。 + +- 示例 + +```ts +import { isError } from '@antv/util'; + +console.log(isError(new Error())); // true +console.log(isError(new TypeError())); // true +console.log(isError('error')); // false +console.log(isError({ message: 'Error' })); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为错误对象 | boolean | - | + +## isFunction + +判断值是否为函数。 + +- 示例 + +```ts +import { isFunction } from '@antv/util'; + +console.log(isFunction(() => {})); // true +console.log(isFunction(function(){})); // true +console.log(isFunction(class {})); // true +console.log(isFunction({})); // false +console.log(isFunction(null)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | unknown | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为函数 | boolean | - | + +## isFinite + +判断值是否为有限数。 + +- 示例 + +```ts +import { isFinite } from '@antv/util'; + +console.log(isFinite(123)); // true +console.log(isFinite(-1.23)); // true +console.log(isFinite(Infinity)); // false +console.log(isFinite(-Infinity)); // false +console.log(isFinite(NaN)); // false +console.log(isFinite('123')); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | number | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为有限数 | boolean | - | + +## isNil + +判断值是否为 null 或 undefined。 + +- 示例 + +```ts +import { isNil } from '@antv/util'; + +console.log(isNil(null)); // true +console.log(isNil(undefined)); // true +console.log(isNil(0)); // false +console.log(isNil('')); // false +console.log(isNil(false)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | unknown | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为 null 或 undefined | boolean | - | + +## isNull + +判断值是否为 null。 + +- 示例 + +```ts +import { isNull } from '@antv/util'; + +console.log(isNull(null)); // true +console.log(isNull(undefined)); // false +console.log(isNull(0)); // false +console.log(isNull('')); // false +console.log(isNull({})); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | unknown | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为 null | boolean | - | + +## isNumber + +判断值是否为数字。 + +- 示例 + +```ts +import { isNumber } from '@antv/util'; + +console.log(isNumber(1)); // true +console.log(isNumber(1.5)); // true +console.log(isNumber(NaN)); // true +console.log(isNumber(Infinity)); // true +console.log(isNumber('1')); // false +console.log(isNumber(true)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | unknown | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为数字 | boolean | - | + +## isObject + +判断值是否为对象(包括函数和数组)。 + +- 示例 + +```ts +import { isObject } from '@antv/util'; + +console.log(isObject({})); // true +console.log(isObject([1, 2, 3])); // true +console.log(isObject(() => {})); // true +console.log(isObject(new Date())); // true +console.log(isObject(null)); // false +console.log(isObject(undefined)); // false +console.log(isObject('string')); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为对象 | boolean | - | + +## isObjectLike + +判断值是否为类对象(对象但不包括函数)。 + +- 示例 + +```ts +import { isObjectLike } from '@antv/util'; + +console.log(isObjectLike({})); // true +console.log(isObjectLike([1, 2, 3])); // true +console.log(isObjectLike(new Date())); // true +console.log(isObjectLike(() => {})); // false +console.log(isObjectLike(null)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为类对象 | boolean | - | + +## isPlainObject + +判断值是否为普通对象(由 `{}` 或 `new Object` 创建)。 + +- 示例 + +```ts +import { isPlainObject } from '@antv/util'; + +console.log(isPlainObject({})); // true +console.log(isPlainObject({ x: 0, y: 0 })); // true +console.log(isPlainObject(Object.create(null))); // true +console.log(isPlainObject([1, 2, 3])); // false +console.log(isPlainObject(new Date())); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为普通对象 | boolean | - | + +## isPrototype + +判断值是否为原型对象。 + +- 示例 + +```ts +import { isPrototype } from '@antv/util'; + +function Foo() {} +console.log(isPrototype(Foo.prototype)); // true +console.log(isPrototype(Object.prototype)); // true +console.log(isPrototype({})); // false +console.log(isPrototype(Object.create(null))); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为原型对象 | boolean | - | + +## isRegExp + +判断值是否为正则表达式。 + +- 示例 + +```ts +import { isRegExp } from '@antv/util'; + +console.log(isRegExp(/abc/)); // true +console.log(isRegExp(new RegExp('abc'))); // true +console.log(isRegExp('/abc/')); // false +console.log(isRegExp({})); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| str | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为正则表达式 | boolean | - | + +## isString + +判断值是否为字符串。 + +- 示例 + +```ts +import { isString } from '@antv/util'; + +console.log(isString('abc')); // true +console.log(isString('')); // true +console.log(isString(123)); // false +console.log(isString({})); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | unknown | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为字符串 | boolean | - | + +## isType + +判断值的具体类型。 + +- 示例 + +```ts +import { isType } from '@antv/util'; + +console.log(isType([], 'Array')); // true +console.log(isType({}, 'Object')); // true +console.log(isType('abc', 'String')); // true +console.log(isType(123, 'Number')); // true +console.log(isType(true, 'Boolean')); // true +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | +| type | 类型字符串 | string | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为指定类型 | boolean | - | + +## isUndefined + +判断值是否为 undefined。 + +- 示例 + +```ts +import { isUndefined } from '@antv/util'; + +console.log(isUndefined(undefined)); // true +console.log(isUndefined(void 0)); // true +console.log(isUndefined(null)); // false +console.log(isUndefined('')); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为 undefined | boolean | - | + +## isElement + +判断值是否为 DOM 元素或文档对象。 + +- 示例 + +```ts +import { isElement } from '@antv/util'; + +console.log(isElement(document.body)); // true +console.log(isElement(document)); // true +console.log(isElement(document.createElement('div'))); // true +console.log(isElement('
')); // false +console.log(isElement({})); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | unknown | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为 DOM 元素 | boolean | - | + +## requestAnimationFrame + +跨浏览器的 requestAnimationFrame 实现,用于执行动画。 + +- 示例 + +```ts +import { requestAnimationFrame } from '@antv/util'; + +// 基本使用 +requestAnimationFrame(() => { + console.log('Animation frame'); +}); + +// 动画循环 +function animate() { + // 更新动画 + requestAnimationFrame(animate); +} +animate(); + +// 实际应用示例 +function smoothScroll(element: HTMLElement, target: number, duration: number) { + const start = element.scrollTop; + const distance = target - start; + const startTime = performance.now(); + + function update(currentTime: number) { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + + element.scrollTop = start + distance * progress; + + if (progress < 1) { + requestAnimationFrame(update); + } + } + + requestAnimationFrame(update); +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| fn | 动画回调函数 | FrameRequestCallback | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| handler | 动画句柄 | number | - | + +## cancelAnimationFrame + +取消之前请求的动画帧。 + +- 示例 + +```ts +import { requestAnimationFrame, cancelAnimationFrame } from '@antv/util'; + +// 基本使用 +const handler = requestAnimationFrame(() => { + console.log('Cancelled'); +}); +cancelAnimationFrame(handler); + +// 实际应用示例 +class Animation { + private frameId: number; + + start() { + const animate = () => { + // 执行动画 + this.frameId = requestAnimationFrame(animate); + }; + this.frameId = requestAnimationFrame(animate); + } + + stop() { + if (this.frameId) { + cancelAnimationFrame(this.frameId); + this.frameId = null; + } + } +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| handler | 要取消的动画句柄 | number | - | + +- 返回值 + +无返回值 + +## augment + +扩展类的原型,支持混入多个对象或类的属性和方法。 + +- 示例 + +```ts +import { augment } from '@antv/util'; + +// 基本使用 +class Base {} +const mixin = { sayHi() { console.log('Hi'); } }; +augment(Base, mixin); + +const obj = new Base(); +obj.sayHi(); // "Hi" + +// 混入类 +class Mixin { + greet() { console.log('Hello'); } +} +augment(Base, Mixin); +obj.greet(); // "Hello" +``` + +## clone + +深度克隆对象或数组。 + +- 示例 + +```ts +import { clone } from '@antv/util'; + +// 基本使用 +const obj = { a: 1, b: { c: 2 } }; +const copy = clone(obj); +console.log(copy); // { a b: { c: 2 } } +console.log(copy !== obj); // true +console.log(copy.b !== obj.b); // true + +// 克隆数组 +const arr = [1, [2, 3], { a: 4 }]; +const copyArr = clone(arr); +console.log(copyArr); // [1, [2, 3], { a: 4 }] +``` + +## debounce + +创建一个防抖函数,延迟调用。 + +- 示例 + +```ts +import { debounce } from '@antv/util'; + +// 基本使用 +const handler = debounce(() => { + console.log('Resized'); +}, 200); + +window.addEventListener('resize', handler); + +// 立即执行 +const immediate = debounce(() => { + console.log('Clicked'); +}, 200, true); + +button.addEventListener('click', immediate); +``` + +## memoize + +缓存函数的计算结果。 + +- 示例 + +```ts +import { memoize } from '@antv/util'; + +// 基本使用 +const fibonacci = memoize((n: number): number => { + return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); +}); + +console.log(fibonacci(10)); // 快速计算,使用缓存 + +// 自定义缓存键 +const getUser = memoize( + (id: number, type: string) => fetchUser(id, type), + (id, type) => `${id}-${type}` +); + +// 设置缓存大小 +const cached = memoize(heavyComputation, undefined, 1000); +``` + +- 参数说明 + +| 方法 | 参数 | 类型 | 说明 | +|---------|------|------|---------| +| augment | args | any[] | 目标类和要混入的对象/类 | +| clone | obj | any | 要克隆的对象或数组 | +| debounce | func | Function | 要防抖的函数 | +| debounce | wait | number | 延迟时间(ms) | +| debounce | immediate | boolean | 是否立即执行 | +| memoize | fn | Function | 要缓存的函数 | +| memoize | resolver | Function | 缓存键生成函数 | +| memoize | maxSize | number | 缓存大小限制 | + +- 返回值 + +| 方法 | 返回类型 | 说明 | +|---------|------|------| +| augment | void | 无返回值 | +| clone | any | 克隆后的对象/数组 | +| debounce | Function | 防抖后的函数 | +| memoize | Function | 带缓存的函数 | + +## deepMix + +深度混合多个对象。 + +- 示例 + +```ts +import { deepMix } from '@antv/util'; + +// 基本使用 +const obj1 = { a: 1, b: { c: 2 } }; +const obj2 = { b: { d: 3 }, e: 4 }; +console.log(deepMix({}, obj1, obj2)); +// { a: 1, b: { c: 2, d: 3 }, e: 4 } + +// 数组处理 +const result = deepMix({}, { + arr: [1, 2], + obj: { a: 1 } +}, { + arr: [3], + obj: { b: 2 } +}); +console.log(result); +// { arr: [3], obj: { a: 1, b: 2 } } +``` + +## each + +遍历数组或对象的每个元素。 + +- 示例 + +```ts +import { each } from '@antv/util'; + +// 遍历数组 +const arr = [1, 2, 3]; +each(arr, (item, index) => { + console.log(item, index); +}); + +// 遍历对象 +const obj = { a: 1, b: 2 }; +each(obj, (value, key) => { + console.log(key, value); +}); + +// 提前终止 +each(arr, (item) => { + if (item === 2) return false; + console.log(item); +}); +``` + +## extend + +实现类继承。 + +- 示例 + +```ts +import { extend } from '@antv/util'; + +// 基本继承 +class Parent { + name: string; + constructor(name: string) { + this.name = name; + } +} + +const Child = extend(function(name) { + Child.superclass.constructor.call(this, name); +}, Parent); + +// 带方法继承 +extend(Child, Parent, { + sayHi() { + console.log(`Hi, ${this.name}`); + } +}, { + staticMethod() {} +}); +``` + +## indexOf + +查找元素在数组中的索引。 + +- 示例 + +```ts +import { indexOf } from '@antv/util'; + +// 基本使用 +const arr = [1, 2, 3, 2]; +console.log(indexOf(arr, 2)); // 1 +console.log(indexOf(arr, 4)); // -1 + +// 对象数组 +const items = [{ id: 1 }, { id: 2 }]; +const target = items[0]; +console.log(indexOf(items, target)); // 0 +``` + +- 参数说明 + +| 方法 | 参数 | 类型 | 说明 | +|---------|------|------|---------| +| deepMix | rst, ...args | any, ...any[] | 目标对象和源对象列表 | +| each | elements, func | any[] \| object, Function | 要遍历的集合和回调函数 | +| extend | subclass, superclass, overrides?, staticOverrides? | Function, Function, object?, object? | 子类、父类和扩展方法 | +| indexOf | arr, obj | T[], T | 要搜索的数组和目标元素 | + +- 返回值 + +| 方法 | 返回类型 | 说明 | +|---------|------|------| +| deepMix | any | 混合后的对象 | +| each | void | 无返回值 | +| extend | Function | 扩展后的子类 | +| indexOf | number | 元素索引或-1 | + +## isEmpty + +判断值是否为空。 + +- 示例 + +```ts +import { isEmpty } from '@antv/util'; + +console.log(isEmpty(null)); // true +console.log(isEmpty(undefined)); // true +console.log(isEmpty([])); // true +console.log(isEmpty({})); // true +console.log(isEmpty(new Map())); // true +console.log(isEmpty([1, 2])); // false +console.log(isEmpty({ a: 1 })); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要检查的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 是否为空 | boolean | - | + +## isEqual + +深度比较两个值是否相等。 + +- 示例 + +```ts +import { isEqual } from '@antv/util'; + +// 基本类型 +console.log(isEqual(1, 1)); // true +console.log(isEqual('a', 'a')); // true + +// 数组 +console.log(isEqual([1, 2], [1, 2])); // true +console.log(isEqual([1, 2], [2, 1])); // false + +// 对象 +console.log(isEqual( + { a: 1, b: 2 }, + { a: 1, b: 2 } +)); // true +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要比较的值 | any | - | +| other | 另一个值 | any | - | + +## isEqualWith + +使用自定义函数比较两个值。 + +- 示例 + +```ts +import { isEqualWith } from '@antv/util'; + +const customizer = (v1, v2) => { + if (v1.type === v2.type) return true; + return false; +}; + +console.log(isEqualWith( + { type: 'a', value: 1 }, + { type: 'a', value: 2 }, + customizer +)); // true +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要比较的值 | T | - | +| other | 另一个值 | T | - | +| fn | 比较函数 | (v1: T, v2: T) => boolean | - | + +## map + +映射数组的每个元素。 + +- 示例 + +```ts +import { map } from '@antv/util'; + +// 基本使用 +const numbers = [1, 2, 3]; +console.log(map(numbers, x => x * 2)); // [2, 4, 6] + +// 使用索引 +console.log(map(numbers, (x, i) => x + i)); // [1, 3, 5] + +// 对象数组 +const items = [{ value: 1 }, { value: 2 }]; +console.log(map(items, item => item.value)); // [1, 2] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| arr | 要映射的数组 | T[] | - | +| func | 映射函数 | (v: T, idx: number) => G | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 映射后的数组 | G[] | - | + +## mapValues + +映射对象的所有值。 + +- 示例 + +```ts +import { mapValues } from '@antv/util'; + +// 基本使用 +const obj = { a: 1, b: 2 }; +console.log(mapValues(obj, x => x * 2)); +// { a: 2, b: 4 } + +// 使用键名 +console.log(mapValues(obj, (value, key) => `${key}:${value}`)); +// { a: 'a:1', b: 'b:2' } + +// 默认映射 +console.log(mapValues(obj)); +// { a: 1, b: 2 } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| object | 源对象 | { [key: string]: T } | - | +| func | 映射函数 | (value: T, key: string) => any | identity | + +## mix/assiage + +混合多个对象的属性。 + +- 示例 + +```ts +import { mix } from '@antv/util'; + +// 基本使用 +const obj1 = { a: 1 }; +const obj2 = { b: 2 }; +const obj3 = { c: 3 }; + +console.log(mix({}, obj1, obj2, obj3)); +// { a: 1, b: 2, c: 3 } + +// 忽略 undefined +console.log(mix({}, { a: 1, b: undefined })); +// { a: 1 } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| dist | 目标对象 | Base & A & B & C | - | +| src1 | 源对象 1 | A | - | +| src2 | 源对象 2 | B | - | +| src3 | 源对象 3 | C | - | + +## get + +安全获取对象深层属性值。 + +- 示例 + +```ts +import { get } from '@antv/util'; + +const obj = { + a: { + b: { + c: 1 + } + } +}; + +// 使用点号路径 +console.log(get(obj, 'a.b.c')); // 1 +console.log(get(obj, 'a.b.d', 0)); // 0 + +// 使用数组路径 +console.log(get(obj, ['a', 'b', 'c'])); // 1 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| obj | 源对象 | any | - | +| key | 属性路径 | string \| any[] | - | +| defaultValue | 默认值 | any | - | + +## set + +设置对象深层属性值。 + +- 示例 + +```ts +import { set } from '@antv/util'; + +const obj = {}; + +// 使用点号路径 +set(obj, 'a.b.c', 1); +console.log(obj); // { a: { b: { c: 1 } } } + +// 使用数组路径 +set(obj, ['x', 0, 'y'], 2); +console.log(obj); // { a: { b: { c: 1 } }, x: [{ y: 2 }] } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| obj | 目标对象 | any | - | +| path | 属性路径 | string \| any[] | - | +| value | 要设置的值 | any | - | + +## pick + +创建一个从源对象中选取指定属性的对象。 + +- 示例 + +```ts +import { pick } from '@antv/util'; + +const object = { a: 1, b: '2', c: 3 }; + +console.log(pick(object, ['a', 'c'])); +// { a: 1, c: 3 } + +// 处理不存在的键 +console.log(pick(object, ['a', 'd'])); +// { a: 1 } + +// 处理无效输入 +console.log(pick(null, ['a'])); +// {} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| object | 源对象 | ObjectType | - | +| keys | 要选取的属性数组 | string[] | - | + +## omit + +创建一个忽略指定属性的对象。 + +- 示例 + +```ts +import { omit } from '@antv/util'; + +const object = { a: 1, b: '2', c: 3 }; + +console.log(omit(object, ['b'])); +// { a: 1, c: 3 } + +// 忽略多个属性 +console.log(omit(object, ['a', 'c'])); +// { b: '2' } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| obj | 源对象 | ObjectType | - | +| keys | 要忽略的属性数组 | string[] | - | + +## throttle + +创建一个节流函数。 + +- 示例 + +```ts +import { throttle } from '@antv/util'; + +// 基本使用 +const handler = throttle(() => { + console.log('Throttled'); +}, 200); + +window.addEventListener('resize', handler); + +// 自定义选项 +const scroll = throttle(() => { + console.log('Scrolled'); +}, 300, { + leading: false, // 禁用第一次立即执行 + trailing: true // 启用最后一次延迟执行 +}); + +// 取消节流 +handler.cancel(); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| func | 要节流的函数 | Function | - | +| wait | 等待时间(ms) | number | - | +| options | 配置选项 | OptionsType | {} | + +## toArray + +将类数组对象转换为数组。 + +- 示例 + +```ts +import { toArray } from '@antv/util'; + +// 类数组对象 +function example() { + console.log(toArray(arguments)); +} +example(1, 2, 3); // [1, 2, 3] + +// 字符串 +console.log(toArray('abc')); // ['a', 'b', 'c'] + +// 无效输入 +console.log(toArray(null)); // [] +console.log(toArray(123)); // [] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要转换的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 转换后的数组 | any[] | - | + +## toString + +将值转换为字符串,处理 null 和 undefined。 + +- 示例 + +```ts +import { toString } from '@antv/util'; + +console.log(toString(123)); // "123" +console.log(toString('abc')); // "abc" +console.log(toString(null)); // "" +console.log(toString(undefined)); // "" +console.log(toString({})); // "[object Object]" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| value | 要转换的值 | any | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 转换后的字符串 | string | - | + +## uniqueId + +生成带前缀的唯一 ID。 + +- 示例 + +```ts +import { uniqueId } from '@antv/util'; + +console.log(uniqueId()); // "g1" +console.log(uniqueId()); // "g2" +console.log(uniqueId('user_')); // "user_1" +console.log(uniqueId('user_')); // "user_2" + +// 不同前缀独立计数 +console.log(uniqueId('a_')); // "a_1" +console.log(uniqueId('b_')); // "b_1" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| prefix | ID 前缀 | string | 'g' | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 唯一 ID | string | - | + +## size + +获取数组、类数组或对象的大小。 + +- 示例 + +```ts +import { size } from '@antv/util'; + +// 数组 +console.log(size([1, 2, 3])); // 3 + +// 字符串 +console.log(size('abc')); // 3 + +// 对象 +console.log(size({ a: 1, b: 2 })); // 2 + +// 类数组对象 +console.log(size({ length: 3 })); // 3 + +// 空值 +console.log(size(null)); // 0 +console.log(size(undefined)); // 0 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| o | 要计算大小的集合 | unknown | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 集合的大小 | number | - | diff --git a/docs/api/math.md b/docs/api/math.md new file mode 100644 index 0000000..6f073ea --- /dev/null +++ b/docs/api/math.md @@ -0,0 +1,146 @@ +# 数学 `math` 相关函数 + +> 一些数学计算相关函数,目前主要是图形拾取和碰撞相关。 + +- [isPointInPolygon](#ispointinpolygon) - 判断点是否在多边形内部(射线法) +- [isPolygonsIntersect](#ispolygonsintersect) - 判断两个多边形是否相交 + +## isPointInPolygon + +判断点是否在多边形内部(射线法)。 + +- 使用射线法判断点是否在多边形内部 +- 支持判断点是否在多边形边上 +- 处理特殊情况(如点数少于 3 个的情况) +- 使用容差处理浮点数精度问题 + +```ts +import { isPointInPolygon } from '@antv/util'; + +// 定义一个矩形多边形 +const rectangle = [ + [0, 0], // 左上 + [100, 0], // 右上 + [100, 100], // 右下 + [0, 100] // 左下 +]; + +// 判断点是否在多边形内 +console.log(isPointInPolygon(rectangle, 50, 50)); // true (在内部) +console.log(isPointInPolygon(rectangle, 0, 0)); // true (在顶点上) +console.log(isPointInPolygon(rectangle, 50, 0)); // true (在边上) +console.log(isPointInPolygon(rectangle, 150, 150)); // false (在外部) + +// 定义一个三角形 +const triangle = [ + [0, 0], + [100, 0], + [50, 100] +]; + +// 测试三角形的点 +console.log(isPointInPolygon(triangle, 50, 50)); // true +console.log(isPointInPolygon(triangle, 0, 50)); // false + +// 特殊情况测试 +const invalidPolygon = [[0, 0], [100, 100]]; // 少于3个点 +console.log(isPointInPolygon(invalidPolygon, 50, 50)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| points | 多边形顶点数组 | number[][] | - | 多边形的顶点坐标数组 | +| x | 检测点的 x 坐标 | number | - | 待检测点的 x 坐标 | +| y | 检测点的 y 坐标 | number | - | 待检测点的 y 坐标 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 判断结果 | boolean | - | 点是否在多边形内部或边上 | + +- 注意事项 + +1. 多边形顶点数量必须大于等于 3 个 +2. 支持凸多边形和凹多边形 +3. 边界点被认为是在多边形内部 +4. 使用容差处理浮点数精度问题 +5. 多边形顶点按顺序排列(顺时针或逆时针) + + +## isPolygonsIntersect + +判断两个多边形是否相交。 + +- 判断两个多边形是否存在交集 +- 使用包围盒快速判断是否可能相交 +- 检查点是否在另一个多边形内部 +- 检查线段是否相交 +- 支持凸多边形和凹多边形 + +```ts +import { isPolygonsIntersect } from '@antv/util'; + +// 定义两个矩形 +const rectangle1 = [ + [0, 0], + [100, 0], + [100, 100], + [0, 100] +]; + +const rectangle2 = [ + [50, 50], + [150, 50], + [150, 150], + [50, 150] +]; + +// 相交的情况 +console.log(isPolygonsIntersect(rectangle1, rectangle2)); // true + +// 不相交的情况 +const rectangle3 = [ + [200, 200], + [300, 200], + [300, 300], + [200, 300] +]; +console.log(isPolygonsIntersect(rectangle1, rectangle3)); // false + +// 一个多边形包含另一个的情况 +const smallRect = [ + [25, 25], + [75, 25], + [75, 75], + [25, 75] +]; +console.log(isPolygonsIntersect(rectangle1, smallRect)); // true + +// 特殊情况测试 +const invalidPolygon = [[0, 0]]; // 单点 +console.log(isPolygonsIntersect(rectangle1, invalidPolygon)); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| points1 | 第一个多边形的顶点数组 | number[][] | - | +| points2 | 第二个多边形的顶点数组 | number[][] | - | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | +|---------|------|------|---------| +| result | 两个多边形是否相交 | boolean | - | + +- 注意事项 + +1. 多边形顶点数量必须大于等于 2 个 +2. 支持凸多边形和凹多边形 +3. 包围盒检测可以提高性能 +4. 一个多边形完全包含另一个多边形也被视为相交 +5. 边界接触也被视为相交 diff --git a/docs/api/matrix.md b/docs/api/matrix.md new file mode 100644 index 0000000..605ed80 --- /dev/null +++ b/docs/api/matrix.md @@ -0,0 +1,315 @@ +# 矩阵 `matrix` 相关函数 + +> 和可视化 matrix 相关的函数,向量夹角、距离、变换等等。 + +- [angleTo](#angleto) - 计算两个二维向量之间的夹角 +- [direction](#direction) - 计算两个向量之间夹角的方向(顺时针或逆时针) +- [transform](#transform) - 根据变换操作序列对矩阵进行变换 +- [vertical](#vertical) - 计算二维向量的垂直向量。 + +## angleTo + +计算两个二维向量之间的夹角。 + +- 计算两个二维向量之间的夹角 +- 支持顺时针和逆时针方向的角度计算 +- 使用 gl-matrix 库进行向量计算 +- 返回弧度值(0 到 2π) + +```ts +import { angleTo } from '@antv/util'; + +// 基本向量夹角 +const v1: [number, number] = [1, 0]; +const v2: [number, number] = [0, 1]; +console.log(angleTo(v1, v2)); // π/2 (90度) +console.log(angleTo(v1, v2, true)); // 3π/2 (270度) + +// 相同方向的向量 +const v3: [number, number] = [1, 0]; +const v4: [number, number] = [2, 0]; +console.log(angleTo(v3, v4)); // 0 + +// 相反方向的向量 +const v5: [number, number] = [1, 0]; +const v6: [number, number] = [-1, 0]; +console.log(angleTo(v5, v6)); // π (180度) + +// 实际应用示例 +function rotateVector(vector: [number, number], angle: number): [number, number] { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + return [ + vector[0] * cos - vector[1] * sin, + vector[0] * sin + vector[1] * cos + ]; +} + +// 计算旋转后的向量夹角 +const original: [number, number] = [1, 0]; +const rotated = rotateVector(original, Math.PI / 4); // 旋转45度 +console.log(angleTo(original, rotated)); // π/4 (45度) +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 详细说明 | +|---------|------|------|---------|----------| +| v1 | 第一个向量 | [number, number] | - | 起始向量 | +| v2 | 第二个向量 | [number, number] | - | 目标向量 | +| direct | 是否按逆时针方向计算 | boolean | false | 是否逆时针计算角度 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 详细说明 | +|---------|------|------|---------|----------| +| angle | 夹角弧度值 | number | - | 两个向量之间的夹角(0 到 2π) | + +- 注意事项 + +1. 返回值是弧度制(0 到 2π) +2. 默认顺时针方向计算角度 +3. direct 参数可以改变角度的计算方向 +4. 使用了 gl-matrix 库的 vec2.angle 计算基础角度 +5. 零向量可能导致计算结果不准确 + +## direction + +计算两个向量之间夹角的方向(顺时针或逆时针)。 + +- 判断从向量 v1 到向量 v2 的旋转方向 +- 使用向量叉积的方法计算方向 +- 返回值大于等于 0 表示顺时针方向 +- 返回值小于 0 表示逆时针方向 + +```ts +import { direction } from '@antv/util'; + +// 基本方向判断 +const v1 = [1, 0]; +const v2 = [0, 1]; +console.log(direction(v1, v2)); // -1 (逆时针) + +const v3 = [0, -1]; +console.log(direction(v1, v3)); // 1 (顺时针) + +// 共线向量 +const v4 = [2, 0]; +console.log(direction(v1, v4)); // 0 (共线) + +// 实际应用示例 +function getRotationDirection(startAngle: number, endAngle: number): string { + const start = [Math.cos(startAngle), Math.sin(startAngle)]; + const end = [Math.cos(endAngle), Math.sin(endAngle)]; + + return direction(start, end) >= 0 ? '顺时针' : '逆时针'; +} + +// 测试不同角度的旋转方向 +console.log(getRotationDirection(0, Math.PI / 2)); // '逆时针' +console.log(getRotationDirection(0, -Math.PI / 2)); // '顺时针' + +// 在动画中使用 +function animateRotation(element: HTMLElement, targetAngle: number) { + const currentVector = [1, 0]; + const targetVector = [ + Math.cos(targetAngle), + Math.sin(targetAngle) + ]; + + const isClockwise = direction(currentVector, targetVector) >= 0; + // 根据方向选择最短路径旋转 + element.style.transition = 'transform 1s'; + element.style.transform = `rotate(${isClockwise ? '' : '-'}${targetAngle}rad)`; +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| v1 | 第一个向量 | number[] | - | 起始向量 | +| v2 | 第二个向量 | number[] | - | 目标向量 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 方向判断值 | number | - | >=0 表示顺时针,<0 表示逆时针 | + +- 注意事项 + +1. 向量应该是二维的 +2. 返回值的正负表示旋转方向 +3. 返回值为 0 表示向量共线 +4. 计算基于向量叉积 +5. 坐标系采用数学标准坐标系(y 轴向上为正) + +## transform + +根据变换操作序列对矩阵进行变换。 + +- 支持平移(translate)、缩放(scale)、旋转(rotate)和矩阵乘法(multiply)操作 +- 使用 gl-matrix 库进行矩阵运算 +- 按顺序执行变换操作 +- 支持初始矩阵输入 +- 所有变换都是左乘方式进行 + +```ts +import { transform } from '@antv/util'; + +// 基本变换示例 +const matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // 单位矩阵 + +// 平移变换 +const translated = transform(matrix, [ + ['t', 100, 200] // 向右平移100,向下平移200 +]); + +// 缩放变换 +const scaled = transform(matrix, [ + ['s', 2, 3] // x方向放大2倍,y方向放大3倍 +]); + +// 旋转变换 +const rotated = transform(matrix, [ + ['r', Math.PI / 4] // 旋转45度 +]); + +// 组合变换 +const combined = transform(matrix, [ + ['t', 100, 100], // 先平移 + ['r', Math.PI / 2], // 再旋转90度 + ['s', 2, 2] // 最后放大2倍 +]); + +// 实际应用示例 +function createTransformMatrix(x: number, y: number, rotation: number, scale: number) { + return transform(null, [ + ['t', x, y], + ['r', rotation], + ['s', scale, scale] + ]); +} + +// 创建一个元素的变换矩阵 +const elementTransform = createTransformMatrix(100, 100, Math.PI / 4, 2); +console.log(elementTransform); + +// 应用到DOM元素 +function applyMatrixToElement(element: HTMLElement, matrix: number[]) { + const cssMatrix = `matrix(${matrix[0]}, ${matrix[1]}, ${matrix[3]}, ${matrix[4]}, ${matrix[2]}, ${matrix[5]})`; + element.style.transform = cssMatrix; +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| m | 初始矩阵 | number[] | [1,0,0,0,1,0,0,0,1] | 初始变换矩阵 | +| actions | 变换操作数组 | any[][] | - | 变换操作序列 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| matrix | 变换后的矩阵 | number[] | - | 经过变换后的矩阵 | + +- 变换操作类型 + +| 操作符 | 说明 | 参数格式 | 示例 | +|---------|------|------|---------| +| t | 平移 | ['t', x, y] | ['t', 100, 200] | +| s | 缩放 | ['s', sx, sy] | ['s', 2, 3] | +| r | 旋转 | ['r', angle] | ['r', Math.PI/4] | +| m | 矩阵乘法 | ['m', matrix] | ['m', [1,0,0,0,1,0,0,0,1]] | + +- 注意事项 + +1. 变换操作按数组顺序依次执行 +2. 所有变换都是左乘方式 +3. 旋转角度使用弧度制 +4. 矩阵使用 3x3 的形式存储 +5. 如果不提供初始矩阵,默认使用单位矩阵 + +## vertical + +> 计算二维向量的垂直向量。 + +- 计算给定二维向量的垂直向量 +- 支持顺时针和逆时针方向的垂直向量计算 +- 可以将结果存储在指定的数组中 +- 返回垂直向量的引用 + +```ts +import { vertical } from '@antv/util'; + +// 基本使用 +const v = [1, 0]; // 原始向量 +const out = [0, 0]; // 用于存储结果的数组 + +// 逆时针垂直向量 +vertical(out, v, true); +console.log(out); // [0, -1] + +// 顺时针垂直向量 +vertical(out, v, false); +console.log(out); // [0, 1] + +// 对任意向量求垂直向量 +const v2 = [3, 4]; +const perpendicular = [0, 0]; +vertical(perpendicular, v2, true); +console.log(perpendicular); // [4, -3] + +// 实际应用示例 +function createPerpendicularLine(point: number[], direction: number[], length: number) { + const perpendicular = [0, 0]; + vertical(perpendicular, direction, true); + + // 标准化垂直向量 + const magnitude = Math.sqrt(perpendicular[0]**2 + perpendicular[1]**2); + perpendicular[0] = (perpendicular[0] / magnitude) * length; + perpendicular[1] = (perpendicular[1] / magnitude) * length; + + return { + start: [ + point[0] - perpendicular[0]/2, + point[1] - perpendicular[1]/2 + ], + end: [ + point[0] + perpendicular[0]/2, + point[1] + perpendicular[1]/2 + ] + }; +} + +// 使用示例 +const point = [100, 100]; +const direction = [1, 0]; +const perpendicularLine = createPerpendicularLine(point, direction, 50); +console.log(perpendicularLine); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| out | 结果向量 | number[] | - | 用于存储结果的数组 | +| v | 原始向量 | number[] | - | 需要计算垂直向量的原始向量 | +| flag | 方向标志 | boolean | - | true 表示逆时针,false 表示顺时针 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 垂直向量 | number[] | - | 计算得到的垂直向量(与 out 相同) | + +- 注意事项 + +1. 输入向量必须是二维的 +2. out 数组会被修改 +3. 返回的是 out 数组的引用 +4. flag 为 true 时逆时针旋转 90 度 +5. flag 为 false 时顺时针旋转 90 度 diff --git a/docs/api/path.md b/docs/api/path.md new file mode 100644 index 0000000..b045894 --- /dev/null +++ b/docs/api/path.md @@ -0,0 +1,5968 @@ +# 图形 `path` 相关方法 + +> 和 SVG Path 相关的一些方法,覆盖面很广。 + +- parser + - [isSpace](#isspace) - 判断字符是否为空白字符。 + - [isPathCommand](#ispathcommand) - 判断字符是否为 SVG 路径命令。 + - [isDigitStart](#isdigitstart) - 判断字符是否为数字的起始字符(包括数字、正负号和小数点)。 + - [isDigit](#isdigit) - 判断字符是否为数字字符(0-9)。 + - [isArcCommand](#isarccommand) - 判断字符是否为 SVG 路径中的弧线命令(A/a)。 + - [finalizeSegment](#finalizesegment) - 完成 SVG 路径片段的解析,并将解析结果添加到路径段数组中。 + - [parsePathString](#parsepathstring) - 将 SVG 路径字符串解析为路径片段数组。 + - [scanFlag](#scanflag) - 扫描并验证 SVG 路径中弧线命令(A/a)的标志位参数。 + - [scanParam](#scanparam) - 扫描并验证 SVG 路径中的数值参数。 + - [scanSegment](#scansegment) - 扫描并解析 SVG 路径中的单个路径片段。 + - [skipSpaces](#skipspaces) - 跳过路径字符串中的空白字符。 + +- process + - [arcToCubic](#arctocubic) - 将 SVG 路径中的弧线命令(A)转换为三次贝塞尔曲线命令(C)。 + - [clonePath](#clonepath) - 深度克隆路径数组,创建路径数据的完整副本。 + - [fixArc](#fixarc) - 修复和处理弧线命令转换后的路径数组。 + - [lineToCubic](#linetocubic) - 将直线转换为三次贝塞尔曲线。 + - [normalizePath](#normalizepath) - 将 SVG 路径标准化为统一格式的路径数组。 + - [normalizeSegment](#normalizesegment) - 标准化单个路径段,将特殊命令转换为基本命令(如 H/V -> L, T -> Q)。 + - [quadToCubic](#quadtocubic) - 将二次贝塞尔曲线转换为三次贝塞尔曲线。 + - [reverseCurve](#reversecurve) - 反转基于贝塞尔曲线的路径数组。 + - [roundPath](#roundpath) - 对路径数组中的数值进行精度舍入。 + - [segmentToCubic](#segmenttocubic) - 将各种路径段转换为三次贝塞尔曲线段。 + +- util + - [distanceSquareRoot](#distancesquareroot) - 计算两点之间的欧几里得距离。 + - [equalizeSegments](#equalizesegments) - 平衡两个路径的段数,使它们具有相同数量的贝塞尔曲线段。 + - [getDrawDirection](#getdrawdirection) - 判断路径的绘制方向(顺时针或逆时针)。 + - [getPathArea](#getpatharea) - 计算路径的面积,支持复杂的贝塞尔曲线路径。 + - [getPathBBoxTotalLength](#getpathbboxtotallength) - 计算路径的边界框和总长度信息。 + - [getPathBBox](#getpathbbox) - 获取路径的边界框(Bounding Box)信息。 + - [getPointAtLength](#getpointatlength) - 获取路径上指定距离处的坐标点。 + - [getPropertiesAtLength](#getpropertiesatlength) - 获取路径上指定距离处的段属性信息。 + - [getPropertiesAtPoint](#getpropertiesatpoint) - 获取路径上距离指定点最近的位置及其属性信息。 + - [getRotatedCurve](#getrotatedcurve) - 获取最佳旋转匹配的曲线路径。 + - [getTotalLength](#gettotallength) - 计算路径的总长度。 + - [isAbsoluteArray](#isabsolutearray) - 判断路径数组是否全部由绝对坐标命令组成。 + - [isCurveArray](#iscurvearray) - 判断路径数组是否全部由三次贝塞尔曲线段(C)和移动命令(M)组成。 + - [isNormalizedArray](#isnormalizedarray) - 判断路径数组是否为标准化的绝对坐标路径(不包含简写命令)。 + - [isPathArray](#ispatharray) - 判断是否为有效的路径数组。 + - [isPointInStroke](#ispointinstroke) - 判断点是否在路径的描边上。 + - [midPoint](#midpoint) - 计算两点之间的中间点或按比例插值点。 + - [pathLengthFactory](#pathlengthfactory) - 路径长度计算工厂函数,用于计算路径的长度、指定距离的点位置和边界框。 + - [rotateVector](#rotatevector) - 旋转二维向量,根据给定角度计算向量的新坐标。 + - [segmentArcFactory](#segmentarcfactory) - 计算弧线段的长度、指定距离的点位置和边界框。 + - [segmentLineFactory](#segmentlinefactory) - 计算直线段的长度、指定距离的点位置和边界框。 + - [segmentCubicFactory](#segmentcubicfactory) - 计算三次贝塞尔曲线段的长度、指定距离的点位置和边界框。 + - [segmentQuadFactory](#segmentquadfactory) - 计算二次贝塞尔曲线段的长度、指定距离的点位置和边界框。 + +- convert + - [path2Absolute](#path2absolute) - 将路径数组或路径字符串转换为绝对坐标路径数组。 + - [path2Array](#path2array) - 将 SVG 路径字符串转换为标准的路径数组格式。 + - [path2Curve](#path2curve) - 将 SVG 路径转换为三次贝塞尔曲线路径。 + - [path2String](#path2string) - 将路径数组转换为 SVG 路径字符串,支持数值精度控制。 + + +## isSpace + +判断字符是否为空白字符。 + +- 检测字符是否为空白字符 +- 支持所有标准空白字符 +- 支持特殊 Unicode 空白字符 +- 包含换行符和行终止符 +- 使用字符的 Unicode 编码进行判断 + +```ts +import { isSpace } from '@antv/util'; + +// 基本空白字符测试 +console.log(isSpace(' '.charCodeAt(0))); // true (普通空格 0x20) +console.log(isSpace('\t'.charCodeAt(0))); // true (制表符 0x09) +console.log(isSpace('\n'.charCodeAt(0))); // true (换行符 0x0a) +console.log(isSpace('\r'.charCodeAt(0))); // true (回车符 0x0d) + +// 特殊空白字符测试 +console.log(isSpace(0xa0)); // true (不间断空格 NBSP) +console.log(isSpace(0x2000)); // true (en quad) +console.log(isSpace(0x3000)); // true (表意文字空格) + +// 非空白字符测试 +console.log(isSpace('a'.charCodeAt(0))); // false +console.log(isSpace('1'.charCodeAt(0))); // false + +// 实际应用示例 +function trimString(str: string): string { + let start = 0; + let end = str.length; + + // 去除开头空白 + while (start < end && isSpace(str.charCodeAt(start))) { + start++; + } + + // 去除结尾空白 + while (end > start && isSpace(str.charCodeAt(end - 1))) { + end--; + } + + return str.slice(start, end); +} + +// 使用示例 +console.log(trimString(' hello ')); // "hello" +console.log(trimString('\t\nhello\r\n')); // "hello" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| ch | 字符编码 | number | - | 要检查的字符的 Unicode 编码值 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 判断结果 | boolean | - | 是否为空白字符 | + +- 支持的空白字符 + +| 类型 | Unicode | 说明 | +|---------|------|------| +| 普通空格 | 0x20 | 标准空格 | +| 制表符 | 0x09 | 水平制表符 | +| 换行符 | 0x0a | 换行 | +| 回车符 | 0x0d | 回车 | +| 行终止符 | 0x2028, 0x2029 | 行分隔符,段落分隔符 | +| 特殊空格 | 0x1680-0xfeff | 各种 Unicode 空白字符 | + + +## isPathCommand + +判断字符是否为 SVG 路径命令。 + +```ts +import { isPathCommand } from '@antv/util'; + +// 基本命令测试 +console.log(isPathCommand('M'.charCodeAt(0))); // true (移动到) +console.log(isPathCommand('m'.charCodeAt(0))); // true (相对移动到) +console.log(isPathCommand('L'.charCodeAt(0))); // true (画线到) +console.log(isPathCommand('z'.charCodeAt(0))); // true (闭合路径) + +// 曲线命令测试 +console.log(isPathCommand('C'.charCodeAt(0))); // true (三次贝塞尔曲线) +console.log(isPathCommand('Q'.charCodeAt(0))); // true (二次贝塞尔曲线) +console.log(isPathCommand('A'.charCodeAt(0))); // true (弧线) + +// 非命令字符测试 +console.log(isPathCommand('X'.charCodeAt(0))); // false +console.log(isPathCommand('1'.charCodeAt(0))); // false + +// 实际应用示例 +function parsePath(pathString: string) { + const commands = []; + let current = ''; + + for (let i = 0; i < pathString.length; i++) { + const code = pathString.charCodeAt(i); + if (isPathCommand(code)) { + if (current) { + commands.push(current); + } + current = pathString[i]; + } else { + current += pathString[i]; + } + } + if (current) { + commands.push(current); + } + return commands; +} + +// 使用示例 +console.log(parsePath('M10 10L20 20')); // ['M10 10', 'L20 20'] +console.log(parsePath('m5,5 l10,10')); // ['m5,5', 'l10,10'] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| code | 字符编码 | number | - | 要检查的字符的 Unicode 编码值 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 判断结果 | boolean | - | 是否为路径命令 | + +- 支持的路径命令 + +| 命令 | 说明 | 大写 | 小写 | +|---------|------|------|------| +| M/m | 移动到 | 绝对坐标 | 相对坐标 | +| Z/z | 闭合路径 | 绝对坐标 | 相对坐标 | +| L/l | 画线到 | 绝对坐标 | 相对坐标 | +| H/h | 水平线到 | 绝对坐标 | 相对坐标 | +| V/v | 垂直线到 | 绝对坐标 | 相对坐标 | +| C/c | 三次贝塞尔曲线 | 绝对坐标 | 相对坐标 | +| S/s | 平滑三次贝塞尔曲线 | 绝对坐标 | 相对坐标 | +| Q/q | 二次贝塞尔曲线 | 绝对坐标 | 相对坐标 | +| T/t | 平滑二次贝塞尔曲线 | 绝对坐标 | 相对坐标 | +| A/a | 弧线 | 绝对坐标 | 相对坐标 | + + +## isDigitStart + +判断字符是否为数字的起始字符(包括数字、正负号和小数点)。 + +```ts +import { isDigitStart } from '@antv/util'; + +// 数字测试 +console.log(isDigitStart('0'.charCodeAt(0))); // true +console.log(isDigitStart('9'.charCodeAt(0))); // true +console.log(isDigitStart('5'.charCodeAt(0))); // true + +// 符号测试 +console.log(isDigitStart('+'.charCodeAt(0))); // true +console.log(isDigitStart('-'.charCodeAt(0))); // true +console.log(isDigitStart('.'.charCodeAt(0))); // true + +// 非数字起始字符测试 +console.log(isDigitStart('a'.charCodeAt(0))); // false +console.log(isDigitStart(' '.charCodeAt(0))); // false + +// 实际应用示例 +function parseNumber(str: string): { value: number, length: number } { + if (!isDigitStart(str.charCodeAt(0))) { + return { value: NaN, length: 0 }; + } + + let i = 1; + while (i < str.length && /[\d.]/.test(str[i])) { + i++; + } + + return { + value: parseFloat(str.slice(0, i)), + length: i + }; +} + +// 使用示例 +console.log(parseNumber('123.45abc')); // { value: 123.45, length: 6 } +console.log(parseNumber('-42px')); // { value: -42, length: 3 } +console.log(parseNumber('+0.5em')); // { value: 0.5, length: 4 } +console.log(parseNumber('.75%')); // { value: 0.75, length: 3 } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| code | 字符编码 | number | - | 要检查的字符的 Unicode 编码值 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 判断结果 | boolean | - | 是否为数字起始字符 | + +- 支持的字符 + +| 字符类型 | Unicode | 说明 | +|---------|------|------| +| 数字 | 48-57 | 0-9 | +| 正号 | 0x2b | + | +| 负号 | 0x2d | - | +| 小数点 | 0x2e | . | + + +## isDigit + +判断字符是否为数字字符(0-9)。 + +```ts +import { isDigit } from '@antv/util'; + +// 数字测试 +console.log(isDigit('0'.charCodeAt(0))); // true +console.log(isDigit('9'.charCodeAt(0))); // true +console.log(isDigit('5'.charCodeAt(0))); // true + +// 非数字测试 +console.log(isDigit('.'.charCodeAt(0))); // false +console.log(isDigit('-'.charCodeAt(0))); // false +console.log(isDigit('a'.charCodeAt(0))); // false + +// 实际应用示例 +function extractNumbers(str: string): string { + return str + .split('') + .filter(char => isDigit(char.charCodeAt(0))) + .join(''); +} + +// 使用示例 +console.log(extractNumbers('abc123def456')); // "123456" +console.log(extractNumbers('价格:99.99元')); // "9999" +console.log(extractNumbers('电话:123-456-789')); // "123456789" + +// 数字验证示例 +function isNumericString(str: string): boolean { + return str.split('').every(char => isDigit(char.charCodeAt(0))); +} + +console.log(isNumericString('12345')); // true +console.log(isNumericString('12.34')); // false +console.log(isNumericString('12a34')); // false +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| code | 字符编码 | number | - | 要检查的字符的 Unicode 编码值 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 判断结果 | boolean | - | 是否为数字字符 | + +- 支持的字符范围 + +| 字符 | Unicode | 说明 | +|---------|------|------| +| 0-9 | 48-57 | 阿拉伯数字 | + + +## isArcCommand + +判断字符是否为 SVG 路径中的弧线命令(A/a)。 + +```ts +import { isArcCommand } from '@antv/util'; + +// 基本测试 +console.log(isArcCommand('A'.charCodeAt(0))); // true +console.log(isArcCommand('a'.charCodeAt(0))); // true + +// 非弧线命令测试 +console.log(isArcCommand('L'.charCodeAt(0))); // false +console.log(isArcCommand('M'.charCodeAt(0))); // false + +// 实际应用示例 +function parseArcSegment(pathData: string) { + const segments = []; + let current = ''; + + for (let i = 0; i < pathData.length; i++) { + const code = pathData.charCodeAt(i); + if (isArcCommand(code)) { + if (current) { + segments.push(current); + } + current = pathData[i]; + } else { + current += pathData[i]; + } + } + if (current) { + segments.push(current); + } + return segments.filter(seg => isArcCommand(seg.charCodeAt(0))); +} + +// 使用示例 +const path = 'M10 10 A50 50 0 0 1 60 60 a30 30 0 0 1 30 30'; +console.log(parseArcSegment(path)); +// ['A50 50 0 0 1 60 60', 'a30 30 0 0 1 30 30'] + +// 弧线参数解析示例 +function parseArcParams(arcCommand: string) { + const params = arcCommand.slice(1).trim().split(/[\s,]+/); + return { + command: arcCommand[0], + rx: parseFloat(params[0]), + ry: parseFloat(params[1]), + xAxisRotation: parseFloat(params[2]), + largeArcFlag: parseInt(params[3]), + sweepFlag: parseInt(params[4]), + x: parseFloat(params[5]), + y: parseFloat(params[6]) + }; +} + +// 解析弧线命令 +const arc = 'A50 50 0 0 1 60 60'; +console.log(parseArcParams(arc)); +// { +// command: 'A', +// rx: 50, +// ry: 50, +// xAxisRotation: 0, +// largeArcFlag: 0, +// sweepFlag: 1, +// x: 60, +// y: 60 +// } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| code | 字符编码 | number | - | 要检查的字符的 Unicode 编码值 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 判断结果 | boolean | - | 是否为弧线命令 | + +- 弧线命令格式 + +| 命令 | 参数 | 说明 | +|---------|------|------| +| A/a | rx ry x-axis-rotation large-arc-flag sweep-flag x y | 绘制弧线 | + + +## finalizeSegment + +完成 SVG 路径片段的解析,并将解析结果添加到路径段数组中。 + +- 处理路径命令和对应的参数 +- 特殊处理 moveTo (M/m) 命令 +- 根据命令类型提取正确数量的参数 +- 支持命令的重复使用 +- 将解析结果添加到路径段数组中 + +```ts +import { finalizeSegment } from '@antv/util'; + +// 路径解析器示例 +const pathParser: PathParser = { + pathValue: 'M10 10 L20 20', + segmentStart: 0, + data: [10, 10, 20, 20], + segments: [] +}; + +// 基本使用 +finalizeSegment(pathParser); +console.log(pathParser.segments); +// [['M', 10, 10], ['L', 20, 20]] + +// MoveTo命令的特殊处理 +const moveParser: PathParser = { + pathValue: 'M10 10 20 20 30 30', + segmentStart: 0, + data: [10, 10, 20, 20, 30, 30], + segments: [] +}; + +finalizeSegment(moveParser); +console.log(moveParser.segments); +// [['M', 10, 10], ['l', 20, 20], ['l', 30, 30]] + +// 实际应用示例 +function parsePath(pathString: string) { + const parser: PathParser = { + pathValue: pathString, + segmentStart: 0, + data: [], + segments: [] + }; + + // 模拟数据收集 + const numbers = pathString.match(/[-+]?[0-9]*\.?[0-9]+/g); + if (numbers) { + parser.data = numbers.map(Number); + } + + finalizeSegment(parser); + return parser.segments; +} + +// 使用示例 +console.log(parsePath('M10 10 L20 20 L30 30')); +// [['M', 10, 10], ['L', 20, 20], ['L', 30, 30]] + +console.log(parsePath('M10 10 20 20 30 30')); +// [['M', 10, 10], ['l', 20, 20], ['l', 30, 30]] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径解析器对象 | PathParser | - | 包含路径解析状态的对象 | + +- PathParser 接口 + +| 属性 | 说明 | 类型 | 中文说明 | +|---------|------|------|---------| +| pathValue | 路径字符串 | string | 原始路径字符串 | +| segmentStart | 片段起始位置 | number | 当前片段的起始索引 | +| data | 数值数组 | number[] | 收集的数值参数 | +| segments | 路径片段数组 | any[][] | 解析后的路径片段数组 | + + +## parsePathString + +将 SVG 路径字符串解析为路径片段数组。 + +- 功能说明 + +- 解析 SVG 路径字符串为标准格式的数组 +- 支持所有标准 SVG 路径命令 +- 处理空格和分隔符 +- 错误处理和验证 +- 支持已经是数组格式的输入 + +```ts +import { parsePathString } from '@antv/util'; + +// 基本路径解析 +console.log(parsePathString('M10 10L20 20')); +// [['M', 10, 10], ['L', 20, 20]] + +// 复杂路径解析 +console.log(parsePathString('M10,10 L20,20 H30 V40 Z')); +// [ +// ['M', 10, 10], +// ['L', 20, 20], +// ['H', 30], +// ['V', 40], +// ['Z'] +// ] + +// 曲线命令解析 +console.log(parsePathString('M0,0 C10,10 20,10 30,0')); +// [ +// ['M', 0, 0], +// ['C', 10, 10, 20, 10, 30, 0] +// ] + +// 已经是数组格式的输入 +const pathArray = [['M', 10, 10], ['L', 20, 20]]; +console.log(parsePathString(pathArray)); +// [['M', 10, 10], ['L', 20, 20]] + +// 实际应用示例 +function createPathObject(pathString: string) { + const segments = parsePathString(pathString); + return { + moveTo: segments.filter(seg => seg[0] === 'M'), + lineTo: segments.filter(seg => seg[0] === 'L'), + closePath: segments.filter(seg => seg[0] === 'Z'), + curves: segments.filter(seg => ['C', 'S', 'Q', 'T'].includes(seg[0])) + }; +} + +// 使用示例 +const path = 'M10,10 L20,20 C30,30 40,40 50,50 Z'; +console.log(createPathObject(path)); +// { +// moveTo: [['M', 10, 10]], +// lineTo: [['L', 20, 20]], +// closePath: [['Z']], +// curves: [['C', 30, 30, 40, 40, 50, 50]] +// } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathInput | 路径输入 | PathArray \| string | - | SVG 路径字符串或路径数组 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 解析结果 | PathArray \| string | - | 路径片段数组或错误信息 | + +- 支持的路径命令 + +| 命令 | 说明 | 参数格式 | +|---------|------|------| +| M/m | 移动到 | x, y | +| L/l | 画线到 | x, y | +| H/h | 水平线到 | x | +| V/v | 垂直线到 | y | +| C/c | 三次贝塞尔曲线 | x1, y1, x2, y2, x, y | +| S/s | 平滑三次贝塞尔曲线 | x2, y2, x, y | +| Q/q | 二次贝塞尔曲线 | x1, y1, x, y | +| T/t | 平滑二次贝塞尔曲线 | x, y | +| A/a | 弧线 | rx, ry, angle, large-arc, sweep, x, y | +| Z/z | 闭合路径 | 无参数 | + + +## scanFlag + +扫描并验证 SVG 路径中弧线命令(A/a)的标志位参数。 + +- 功能说明 + +- 验证弧线命令的标志位参数(large-arc-flag 和 sweep-flag) +- 只接受 0 或 1 作为有效值 +- 更新解析器的参数值和索引 +- 提供详细的错误信息 +- 用于 SVG 路径解析过程 + +```ts +import { scanFlag } from '@antv/util'; + +// 基本使用 +const validPath: PathParser = { + pathValue: 'A100 100 0 1 0 200 200', + index: 11, // 指向第一个标志位 + param: 0, + err: '' +}; + +scanFlag(validPath); +console.log(validPath.param); // 1 +console.log(validPath.index); // 12 + +// 错误处理示例 +const invalidPath: PathParser = { + pathValue: 'A100 100 0 2 0 200 200', + index: 11, + param: 0, + err: '' +}; + +scanFlag(invalidPath); +console.log(invalidPath.err); +// "[path-util]: invalid Arc flag "2", expecting 0 or 1 at index 11" + +// 实际应用示例 +function parseArcFlags(arcCommand: string) { + const parser: PathParser = { + pathValue: arcCommand, + index: 0, + param: 0, + err: '' + }; + + // 跳过命令和前三个参数 + const params = arcCommand.split(/[\s,]+/); + parser.index = arcCommand.indexOf(params[4]); + + // 解析large-arc-flag + scanFlag(parser); + const largeArcFlag = parser.param; + + // 跳到sweep-flag + parser.index = arcCommand.indexOf(params[5]); + + // 解析sweep-flag + scanFlag(parser); + const sweepFlag = parser.param; + + return { + largeArcFlag, + sweepFlag, + error: parser.err + }; +} + +// 使用示例 +console.log(parseArcFlags('A100 100 0 1 0 200 200')); +// { largeArcFlag: 1, sweepFlag: 0, error: '' } + +console.log(parseArcFlags('A100 100 0 2 0 200 200')); +// { largeArcFlag: 0, sweepFlag: 0, error: '...' } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径解析器对象 | PathParser | - | 包含路径解析状态的对象 | + +- PathParser 接口 + +| 属性 | 说明 | 类型 | 中文说明 | +|---------|------|------|---------| +| pathValue | 路径字符串 | string | 原始路径字符串 | +| index | 当前索引 | number | 当前解析位置 | +| param | 参数值 | number | 解析得到的参数值 | +| err | 错误信息 | string | 解析过程中的错误信息 | + + +## scanParam + +扫描并验证 SVG 路径中的数值参数。 + +- 功能说明 + +- 验证路径字符串中的数值参数 +- 支持整数和浮点数 +- 支持科学计数法 +- 处理正负号 +- 提供详细的错误信息 + +```ts +import { scanParam } from '@antv/util'; + +// 基本数值解析 +const path1: PathParser = { + pathValue: 'M100.5 200', + index: 1, + param: 0, + max: 10, + err: '' +}; + +scanParam(path1); +console.log(path1.param); // 100.5 +console.log(path1.index); // 5 + +// 科学计数法解析 +const path2: PathParser = { + pathValue: 'L1e2 50', + index: 1, + param: 0, + max: 6, + err: '' +}; + +scanParam(path2); +console.log(path2.param); // 100 + +// 错误处理示例 +const path3: PathParser = { + pathValue: 'M.e1 200', + index: 1, + param: 0, + max: 7, + err: '' +}; + +scanParam(path3); +console.log(path3.err); // 包含错误信息 + +// 实际应用示例 +function parseNumberSequence(str: string) { + const results = []; + const parser: PathParser = { + pathValue: str, + index: 0, + param: 0, + max: str.length, + err: '' + }; + + while (parser.index < parser.max && !parser.err) { + scanParam(parser); + if (!parser.err) { + results.push(parser.param); + } + // 跳过分隔符 + parser.index++; + } + + return { + values: results, + error: parser.err + }; +} + +// 使用示例 +console.log(parseNumberSequence('100.5 -200 1e2')); +// { values: [100.5, -200, 100], error: '' } + +console.log(parseNumberSequence('100 200 abc')); +// { values: [100, 200], error: '...' } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径解析器对象 | PathParser | - | 包含路径解析状态的对象 | + +- PathParser 接口 + +| 属性 | 说明 | 类型 | 中文说明 | +|---------|------|------|---------| +| pathValue | 路径字符串 | string | 原始路径字符串 | +| index | 当前索引 | number | 当前解析位置 | +| param | 参数值 | number | 解析得到的参数值 | +| max | 最大长度 | number | 字符串最大长度 | +| err | 错误信息 | string | 解析过程中的错误信息 | + +- 支持的数值格式 + +1. 整数:`123` +2. 负数:`-123` +3. 浮点数:`123.456` +4. 科学计数法:`1e2`, `1.23e-4` + + +## scanSegment + +扫描并解析 SVG 路径中的单个路径片段。 + +- 功能说明 + +- 解析路径命令和其参数 +- 处理特殊的弧线命令参数 +- 支持参数之间的逗号分隔 +- 跳过空白字符 +- 验证参数数量和格式 + +```ts +import { scanSegment } from '@antv/util'; + +// 基本路径片段解析 +const path1: PathParser = { + pathValue: 'M100 200', + index: 0, + max: 8, + segmentStart: 0, + data: [], + segments: [], + param: 0, + err: '' +}; + +scanSegment(path1); +console.log(path1.segments); // [['M', 100, 200]] + +// 弧线命令解析 +const path2: PathParser = { + pathValue: 'A100,100 0 1,0 200,200', + index: 0, + max: 20, + segmentStart: 0, + data: [], + segments: [], + param: 0, + err: '' +}; + +scanSegment(path2); +console.log(path2.segments); +// [['A', 100, 100, 0, 1, 0, 200, 200]] + +// 实际应用示例 +function parseSVGPath(pathString: string) { + const parser: PathParser = { + pathValue: pathString, + index: 0, + max: pathString.length, + segmentStart: 0, + data: [], + segments: [], + param: 0, + err: '' + }; + + while (parser.index < parser.max && !parser.err) { + scanSegment(parser); + } + + return { + segments: parser.segments, + error: parser.err + }; +} + +// 使用示例 +console.log(parseSVGPath('M10,10 L20,20 A30,30 0 1,0 40,40 Z')); +// { +// segments: [ +// ['M', 10, 10], +// ['L', 20, 20], +// ['A', 30, 30, 0, 1, 0, 40, 40], +// ['Z'] +// ], +// error: '' +// } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径解析器对象 | PathParser | - | 包含路径解析状态的对象 | + +- PathParser 接口 + +| 属性 | 说明 | 类型 | 中文说明 | +|---------|------|------|---------| +| pathValue | 路径字符串 | string | 原始路径字符串 | +| index | 当前索引 | number | 当前解析位置 | +| max | 最大长度 | number | 字符串最大长度 | +| segmentStart | 片段起始位置 | number | 当前片段的起始索引 | +| data | 数值数组 | number[] | 收集的数值参数 | +| segments | 路径片段数组 | any[][] | 解析后的路径片段数组 | +| param | 参数值 | number | 当前解析的参数值 | +| err | 错误信息 | string | 解析过程中的错误信息 | + + +## skipSpaces + +跳过路径字符串中的空白字符。 + +- 功能说明 + +- 跳过所有类型的空白字符 +- 更新解析器的当前位置 +- 支持多种空白字符格式 +- 用于路径解析过程中的空白处理 +- 确保解析器指向有效字符 + +```ts +import { skipSpaces } from '@antv/util'; + +// 基本使用 +const path1: PathParser = { + pathValue: 'M 100 200', + index: 1, + max: 12 +}; + +skipSpaces(path1); +console.log(path1.index); // 4 (跳过空格后指向'1') + +// 混合空白字符 +const path2: PathParser = { + pathValue: 'L\t\n\r100,200', + index: 1, + max: 11 +}; + +skipSpaces(path2); +console.log(path2.index); // 4 (跳过所有空白字符) + +// 实际应用示例 +function parsePathValues(pathString: string) { + const parser: PathParser = { + pathValue: pathString, + index: 0, + max: pathString.length + }; + + const values = []; + let currentValue = ''; + + while (parser.index < parser.max) { + // 跳过开头的空白 + skipSpaces(parser); + + // 收集数字或字符 + while (parser.index < parser.max && + !isSpace(parser.pathValue.charCodeAt(parser.index))) { + currentValue += parser.pathValue[parser.index]; + parser.index++; + } + + if (currentValue) { + values.push(currentValue); + currentValue = ''; + } + } + + return values; +} + +// 使用示例 +console.log(parsePathValues('M 100 200 L 300 400')); +// ['M', '100', '200', 'L', '300', '400'] + +// 格式化路径字符串 +function formatPathString(pathString: string) { + const parser: PathParser = { + pathValue: pathString, + index: 0, + max: pathString.length + }; + + let formatted = ''; + let lastWasCommand = false; + + while (parser.index < parser.max) { + skipSpaces(parser); + + if (parser.index < parser.max) { + const char = parser.pathValue[parser.index]; + if (/[A-Za-z]/.test(char)) { + if (formatted && !lastWasCommand) { + formatted += ' '; + } + lastWasCommand = true; + } else { + if (formatted) { + formatted += ' '; + } + lastWasCommand = false; + } + formatted += char; + parser.index++; + } + } + + return formatted; +} + +console.log(formatPathString('M 100 200 L 300 400')); +// "M 100 200 L 300 400" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径解析器对象 | PathParser | - | 包含路径解析状态的对象 | + +- PathParser 接口 + +| 属性 | 说明 | 类型 | 中文说明 | +|---------|------|------|---------| +| pathValue | 路径字符串 | string | 原始路径字符串 | +| index | 当前索引 | number | 当前解析位置 | +| max | 最大长度 | number | 字符串最大长度 | + + +## arcToCubic + +将 SVG 路径中的弧线命令(A)转换为三次贝塞尔曲线命令(C)。 + +- 功能说明 + +- 将椭圆弧转换为一系列三次贝塞尔曲线 +- 支持旋转角度 +- 处理大弧和小弧标志 +- 处理顺时针和逆时针方向 +- 基于 SVG 规范的实现 + +```ts +import { arcToCubic } from '@antv/util'; + +// 基本弧线转换 +const result1 = arcToCubic( + 0, 0, // 起点 (x1, y1) + 100, 50, // 半径 (rx, ry) + 0, // 旋转角度 + 0, // 大弧标志 + 1, // 顺时针标志 + 100, 0 // 终点 (x2, y2) +); +console.log(result1); // 返回贝塞尔曲线控制点坐标 + +// 实际应用示例 +function createArcPath( + start: [number, number], + radius: [number, number], + end: [number, number], + options = { angle: 0, largeArc: 0, sweep: 1 } +) { + const points = arcToCubic( + start[0], start[1], + radius[0], radius[1], + options.angle, + options.largeArc, + options.sweep, + end[0], end[1], + null + ); + + // 转换为SVG路径 + const path = [ + `M ${start[0]} ${start[1]}`, + `C ${points.slice(0, 6).join(' ')}`, + ]; + + if (points.length > 6) { + for (let i = 6; i < points.length; i += 6) { + path.push(`C ${points.slice(i, i + 6).join(' ')}`); + } + } + + return path.join(' '); +} + +// 使用示例 +const arcPath = createArcPath( + [0, 0], // 起点 + [100, 50], // 半径 + [100, 0], // 终点 + { angle: 45, largeArc: 0, sweep: 1 } +); +console.log(arcPath); + +// 动画插值示例 +function interpolateArc( + start: [number, number], + end: [number, number], + progress: number +) { + const points = arcToCubic( + start[0], start[1], + 50, 50, // 固定半径 + 0, 0, 1, + end[0], end[1], + null + ); + + // 计算当前点位置 + const t = progress; + const p0 = points.slice(0, 2); + const p1 = points.slice(2, 4); + const p2 = points.slice(4, 6); + const p3 = points.slice(6, 8); + + return bezierPoint(p0, p1, p2, p3, t); +} + +// 贝塞尔曲线点计算 +function bezierPoint(p0: number[], p1: number[], p2: number[], p3: number[], t: number) { + const mt = 1 - t; + return [ + mt * mt * mt * p0[0] + 3 * mt * mt * t * p1[0] + 3 * mt * t * t * p2[0] + t * t * t * p3[0], + mt * mt * mt * p0[1] + 3 * mt * mt * t * p1[1] + 3 * mt * t * t * p2[1] + t * t * t * p3[1] + ]; +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| X1, Y1 | 起点坐标 | number | - | 弧线起点的 x,y 坐标 | +| RX, RY | 半径 | number | - | 椭圆的 x,y 半径 | +| angle | 旋转角度 | number | - | 椭圆的旋转角度(度) | +| LAF | 大弧标志 | number | - | 是否选择大弧(0 或 1) | +| SF | 顺时针标志 | number | - | 是否顺时针绘制(0 或 1) | +| X2, Y2 | 终点坐标 | number | - | 弧线终点的 x,y 坐标 | +| recursive | 递归参数 | number[] | - | 用于递归调用的参数数组 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| points | 控制点坐标 | number[] | - | 贝塞尔曲线的控制点坐标数组 | + + +## clonePath + +深度克隆路径数组,创建路径数据的完整副本。 + +- 功能说明 + +- 创建路径数组的深度副本 +- 处理嵌套数组结构 +- 保持原始数据不变 +- 支持单个路径段和完整路径数组 +- 返回新的路径数组实例 + +```ts +import { clonePath } from '@antv/util'; + +// 基本路径克隆 +const path1: PathArray = [ + ['M', 10, 10], + ['L', 20, 20] +]; +const clone1 = clonePath(path1); +console.log(clone1); // [['M', 10, 10], ['L', 20, 20]] +console.log(clone1 !== path1); // true +console.log(clone1[0] !== path1[0]); // true + +// 单个路径段克隆 +const segment: PathSegment = ['C', 10, 20, 30, 40, 50, 60]; +const clonedSegment = clonePath(segment); +console.log(clonedSegment); // [['C', 10, 20, 30, 40, 50, 60]] + +// 实际应用示例 +function transformPath(path: PathArray, dx: number, dy: number) { + const cloned = clonePath(path); + return cloned.map(segment => { + const [command, ...params] = segment; + if (command === 'H') { + return [command, params[0] + dx]; + } + if (command === 'V') { + return [command, params[0] + dy]; + } + return [ + command, + ...params.map((p, i) => p + (i % 2 ? dy : dx)) + ]; + }); +} + +// 使用示例 +const originalPath: PathArray = [ + ['M', 0, 0], + ['L', 50, 50], + ['H', 100], + ['V', 100] +]; + +const transformedPath = transformPath(originalPath, 10, 20); +console.log(transformedPath); +// [ +// ['M', 10, 20], +// ['L', 60, 70], +// ['H', 110], +// ['V', 120] +// ] +console.log(originalPath); // 原始路径保持不变 + +// 路径动画示例 +function createPathAnimator(path: PathArray) { + const pathCopy = clonePath(path); + let currentPath = pathCopy; + + return { + getPath: () => currentPath, + transform: (dx: number, dy: number) => { + currentPath = transformPath(currentPath, dx, dy); + }, + reset: () => { + currentPath = clonePath(pathCopy); + } + }; +} + +// 使用动画器 +const animator = createPathAnimator([ + ['M', 0, 0], + ['L', 100, 100] +]); + +animator.transform(10, 10); +console.log(animator.getPath()); +// [['M', 10, 10], ['L', 110, 110]] + +animator.reset(); +console.log(animator.getPath()); +// [['M', 0, 0], ['L', 100, 100]] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径数据 | PathArray \| PathSegment | - | 要克隆的路径数组或路径段 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 克隆结果 | PathArray | - | 克隆后的新路径数组 | + + +## fixArc + +修复和处理弧线命令转换后的路径数组。 + +- 功能说明 + +- 处理转换后的弧线命令 +- 将长路径段分割成多个三次贝塞尔曲线 +- 维护命令类型记录 +- 确保路径数组格式正确 +- 优化路径数据结构 + +```ts +import { fixArc } from '@antv/util'; + +// 基本使用 +const pathArray: PathArray = [ + ['M', 0, 0], + ['A', 10, 20, 30, 40, 50, 60, 70, 80, 90, 100] // 过长的弧线段 +]; +const commands = ['M', 'A']; + +fixArc(pathArray, commands, 1); +console.log(pathArray); +// [ +// ['M', 0, 0], +// ['C', 10, 20, 30, 40, 50, 60], +// ['C', 70, 80, 90, 100] +// ] +console.log(commands); // ['M', 'A', 'A'] + +// 实际应用示例 +function processPath(path: PathArray) { + const commands: string[] = path.map(segment => segment[0]); + + for (let i = 0; i < path.length; i++) { + if (commands[i] === 'A') { + fixArc(path, commands, i); + // 由于fixArc可能改变数组长度,需要调整索引 + i--; + } + } + + return { + path, + commands + }; +} + +// 使用示例 +const originalPath: PathArray = [ + ['M', 0, 0], + ['A', 50, 50, 0, 1, 1, 100, 100, 150, 150, 200, 200] +]; + +const result = processPath(originalPath); +console.log(result); +// { +// path: [ +// ['M', 0, 0], +// ['C', 50, 50, 0, 100, 100], +// ['C', 150, 150, 200, 200] +// ], +// commands: ['M', 'A', 'A'] +// } + +// 路径优化示例 +function optimizePath(path: PathArray) { + const commands: string[] = []; + const optimizedPath = path.slice(); + + for (let i = 0; i < optimizedPath.length; i++) { + commands[i] = optimizedPath[i][0]; + if (commands[i] === 'A') { + fixArc(optimizedPath, commands, i); + } + } + + return optimizedPath; +} + +// 使用优化函数 +const complexPath: PathArray = [ + ['M', 0, 0], + ['A', 100, 100, 0, 1, 1, 200, 200, 300, 300] +]; + +console.log(optimizePath(complexPath)); +// [ +// ['M', 0, 0], +// ['C', 100, 100, 200, 200], +// ['C', 300, 300] +// ] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathArray | 路径数组 | PathArray | - | 要处理的路径数组 | +| allPathCommands | 命令数组 | string[] | - | 记录所有路径命令的数组 | +| i | 当前索引 | number | - | 当前处理的路径段索引 | + + +## lineToCubic + +将直线转换为三次贝塞尔曲线。 + +- 功能说明 + +- 将直线段转换为等效的三次贝塞尔曲线 +- 使用中点作为控制点 +- 保持视觉效果不变 +- 统一路径段表示方式 +- 简化路径处理 + +```ts +import { lineToCubic } from '@antv/util'; + +// 基本使用 +const result = lineToCubic(0, 0, 100, 100); +console.log(result); // [50, 50, 100, 100, 100, 100] + +// 水平线转换 +const horizontal = lineToCubic(0, 0, 100, 0); +console.log(horizontal); // [50, 0, 100, 0, 100, 0] + +// 垂直线转换 +const vertical = lineToCubic(0, 0, 0, 100); +console.log(vertical); // [0, 50, 0, 100, 0, 100] + +// 实际应用示例 +function convertPathToCubic(path: [number, number][]) { + const result = []; + + for (let i = 0; i < path.length - 1; i++) { + const [x1, y1] = path[i]; + const [x2, y2] = path[i + 1]; + + if (i === 0) { + result.push(['M', x1, y1]); + } + + const cubic = lineToCubic(x1, y1, x2, y2); + result.push(['C', ...cubic]); + } + + return result; +} + +// 使用示例 +const points = [ + [0, 0], + [100, 100], + [200, 0] +]; + +console.log(convertPathToCubic(points)); +// [ +// ['M', 0, 0], +// ['C', 50, 50, 100, 100, 100, 100], +// ['C', 150, 50, 200, 0, 200, 0] +// ] + +// 动画插值示例 +function createLineAnimator(x1: number, y1: number, x2: number, y2: number) { + const cubic = lineToCubic(x1, y1, x2, y2); + + return (t: number) => { + // 三次贝塞尔曲线插值 + const mt = 1 - t; + const mt2 = mt * mt; + const mt3 = mt2 * mt; + const t2 = t * t; + const t3 = t2 * t; + + return { + x: mt3 * x1 + 3 * mt2 * t * cubic[0] + 3 * mt * t2 * cubic[2] + t3 * cubic[4], + y: mt3 * y1 + 3 * mt2 * t * cubic[1] + 3 * mt * t2 * cubic[3] + t3 * cubic[5] + }; + }; +} + +// 使用动画器 +const animator = createLineAnimator(0, 0, 100, 100); +console.log(animator(0)); // { x: 0, y: 0 } +console.log(animator(0.5)); // { x: 50, y: 50 } +console.log(animator(1)); // { x: 100, y: 100 } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| x1 | 起点 x 坐标 | number | - | 直线起点的 x 坐标 | +| y1 | 起点 y 坐标 | number | - | 直线起点的 y 坐标 | +| x2 | 终点 x 坐标 | number | - | 直线终点的 x 坐标 | +| y2 | 终点 y 坐标 | number | - | 直线终点的 y 坐标 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| points | 控制点坐标 | number[] | - | 贝塞尔曲线的控制点坐标数组 [cx1,cy1,cx2,cy2,x,y] | + +- 注意事项 + +1. 返回 6 个数值(3 个控制点) +2. 使用中点作为控制点 +3. 保持直线的视觉效果 +4. 便于统一处理 +5. 支持任意方向的直线 + + +## normalizePath + +将 SVG 路径标准化为统一格式的路径数组。 + +- 功能说明 + +- 将路径转换为绝对坐标 +- 标准化所有路径命令 +- 转换特殊命令(如 H、V)为通用命令(L) +- 保持路径参数的连续性 +- 维护控制点信息 + +```ts +import { normalizePath } from '@antv/util'; + +// 基本路径标准化 +console.log(normalizePath('M0 0 H50')); +// [['M', 0, 0], ['L', 50, 0]] + +// 相对命令标准化 +console.log(normalizePath('M10 10 l20 20')); +// [['M', 10, 10], ['L', 30, 30]] + +// 复杂路径标准化 +const complexPath = 'M0,0 h50 v50 H0 V0 z'; +console.log(normalizePath(complexPath)); +// [ +// ['M', 0, 0], +// ['L', 50, 0], +// ['L', 50, 50], +// ['L', 0, 50], +// ['L', 0, 0], +// ['Z'] +// ] + +// 实际应用示例 +function createPathAnalyzer(pathString: string) { + const normalized = normalizePath(pathString); + + return { + // 获取所有点 + getPoints() { + return normalized.map(segment => { + const [cmd, ...params] = segment; + return cmd === 'Z' ? null : [params[params.length - 2], params[params.length - 1]]; + }).filter(Boolean); + }, + + // 获取路径长度(简单估算) + getLength() { + let length = 0; + let prevX, prevY; + + normalized.forEach(segment => { + const [cmd, ...params] = segment; + if (cmd === 'M') { + [prevX, prevY] = params; + } else if (cmd === 'L') { + const [x, y] = params; + length += Math.sqrt( + Math.pow(x - prevX, 2) + Math.pow(y - prevY, 2) + ); + [prevX, prevY] = [x, y]; + } + }); + + return length; + }, + + // 获取边界框 + getBBox() { + const points = this.getPoints(); + const xs = points.map(p => p[0]); + const ys = points.map(p => p[1]); + + return { + x: Math.min(...xs), + y: Math.min(...ys), + width: Math.max(...xs) - Math.min(...xs), + height: Math.max(...ys) - Math.min(...ys) + }; + } + }; +} + +// 使用示例 +const analyzer = createPathAnalyzer('M0,0 h100 v100 h-100 z'); +console.log(analyzer.getPoints()); +// [[0,0], [100,0], [100,100], [0,100]] + +console.log(analyzer.getLength()); +// 400 + +console.log(analyzer.getBBox()); +// { x: 0, y: 0, width: 100, height: 100 } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathInput | 路径输入 | string \| PathArray | - | SVG 路径字符串或路径数组 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 标准化路径 | NormalArray | - | 标准化后的路径数组 | + + +## normalizeSegment + +标准化单个路径段,将特殊命令转换为基本命令(如 H/V -> L, T -> Q)。 + +- 功能说明 + +- 将水平线段(H)转换为直线段(L) +- 将垂直线段(V)转换为直线段(L) +- 将平滑曲线(S)转换为贝塞尔曲线(C) +- 将平滑二次曲线(T)转换为二次贝塞尔曲线(Q) +- 维护控制点信息 + +```ts +import { normalizeSegment } from '@antv/util'; + +// 基本参数对象 +const params = { + x1: 0, y1: 0, // 当前点 + x2: 0, y2: 0, // 前一个控制点 + qx: null, qy: null // 二次贝塞尔曲线控制点 +}; + +// 水平线段转换 +console.log(normalizeSegment(['H', 100], params)); +// ['L', 100, 0] + +// 垂直线段转换 +console.log(normalizeSegment(['V', 100], params)); +// ['L', 0, 100] + +// 平滑曲线转换 +params.x1 = 50; +params.y1 = 50; +params.x2 = 25; +params.y2 = 25; +console.log(normalizeSegment(['S', 75, 75, 100, 100], params)); +// ['C', 75, 75, 75, 75, 100, 100] + +// 实际应用示例 +function normalizePath(segments: PathSegment[]) { + const params = { + x1: 0, y1: 0, + x2: 0, y2: 0, + qx: null, qy: null + }; + + return segments.map(segment => { + const normalized = normalizeSegment(segment, params); + + // 更新当前点 + const [cmd, ...values] = normalized; + if (values.length >= 2) { + params.x1 = values[values.length - 2]; + params.y1 = values[values.length - 1]; + } + + return normalized; + }); +} + +// 使用示例 +const path = [ + ['M', 0, 0], + ['H', 100], + ['V', 100], + ['S', 150, 150, 200, 200] +]; + +console.log(normalizePath(path)); +// [ +// ['M', 0, 0], +// ['L', 100, 0], +// ['L', 100, 100], +// ['C', 100, 100, 150, 150, 200, 200] +// ] + +// 处理二次贝塞尔曲线 +function normalizeQuadratic(segments: PathSegment[]) { + const params = { + x1: 0, y1: 0, + x2: 0, y2: 0, + qx: null, qy: null + }; + + return segments.map(segment => { + if (segment[0] === 'Q') { + params.qx = segment[1]; + params.qy = segment[2]; + } + return normalizeSegment(segment, params); + }); +} + +// 使用示例 +const quadraticPath = [ + ['M', 0, 0], + ['Q', 50, 50, 100, 0], + ['T', 200, 0] +]; + +console.log(normalizeQuadratic(quadraticPath)); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| segment | 路径段 | PathSegment | - | 要标准化的路径段 | +| params | 参数对象 | object | - | 包含控制点信息的参数对象 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 标准化路径段 | NormalSegment | - | 标准化后的路径段 | + +- 参数对象属性 + +| 属性 | 说明 | 类型 | 中文说明 | +|---------|------|------|---------| +| x1, y1 | 当前点坐标 | number | 当前绘制点的坐标 | +| x2, y2 | 前一控制点 | number | 前一个控制点的坐标 | +| qx, qy | 二次曲线控制点 | number | 二次贝塞尔曲线的控制点 | + + +## quadToCubic + +将二次贝塞尔曲线转换为三次贝塞尔曲线。 + +- 功能说明 + +- 将二次贝塞尔曲线(Q)转换为三次贝塞尔曲线(C) +- 保持曲线的视觉效果不变 +- 使用 1/3 和 2/3 比例计算控制点 +- 统一曲线表示方式 +- 返回完整的控制点坐标 + +```ts +import { quadToCubic } from '@antv/util'; + +// 基本使用 +const result = quadToCubic(0, 0, 50, 50, 100, 0); +console.log(result); +// [33.33, 33.33, 66.67, 33.33, 100, 0] + +// 实际应用示例 +function convertQuadraticPath(path: [number, number][]) { + const result = []; + + for (let i = 0; i < path.length - 2; i += 2) { + const [x1, y1] = path[i]; // 起点 + const [qx, qy] = path[i + 1]; // 控制点 + const [x2, y2] = path[i + 2]; // 终点 + + if (i === 0) { + result.push(['M', x1, y1]); + } + + const cubic = quadToCubic(x1, y1, qx, qy, x2, y2); + result.push(['C', ...cubic]); + } + + return result; +} + +// 使用示例 +const quadraticPath = [ + [0, 0], // 起点 + [50, 50], // 控制点1 + [100, 0], // 终点1/起点2 + [150, 50], // 控制点2 + [200, 0] // 终点2 +]; + +console.log(convertQuadraticPath(quadraticPath)); +// [ +// ['M', 0, 0], +// ['C', 33.33, 33.33, 66.67, 33.33, 100, 0], +// ['C', 133.33, 33.33, 166.67, 33.33, 200, 0] +// ] + +// 动画插值示例 +function createCurveAnimator(x1: number, y1: number, qx: number, qy: number, x2: number, y2: number) { + const cubic = quadToCubic(x1, y1, qx, qy, x2, y2); + + return (t: number) => { + // 三次贝塞尔曲线插值 + const mt = 1 - t; + const mt2 = mt * mt; + const mt3 = mt2 * mt; + const t2 = t * t; + const t3 = t2 * t; + + return { + x: mt3 * x1 + + 3 * mt2 * t * cubic[0] + + 3 * mt * t2 * cubic[2] + + t3 * x2, + y: mt3 * y1 + + 3 * mt2 * t * cubic[1] + + 3 * mt * t2 * cubic[3] + + t3 * y2 + }; + }; +} + +// 使用动画器 +const animator = createCurveAnimator(0, 0, 50, 50, 100, 0); +console.log(animator(0)); // { x: 0, y: 0 } +console.log(animator(0.5)); // { x: 50, y: 25 } +console.log(animator(1)); // { x: 100, y: 0 } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| x1 | 起点 x 坐标 | number | - | 曲线起点的 x 坐标 | +| y1 | 起点 y 坐标 | number | - | 曲线起点的 y 坐标 | +| qx | 控制点 x 坐标 | number | - | 二次贝塞尔曲线控制点的 x 坐标 | +| qy | 控制点 y 坐标 | number | - | 二次贝塞尔曲线控制点的 y 坐标 | +| x2 | 终点 x 坐标 | number | - | 曲线终点的 x 坐标 | +| y2 | 终点 y 坐标 | number | - | 曲线终点的 y 坐标 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| points | 控制点坐标 | number[] | - | 三次贝塞尔曲线的控制点坐标数组 [cpx1,cpy1,cpx2,cpy2,x,y] | + +- 注意事项 + +1. 保持曲线形状不变 +2. 使用固定比例计算 +3. 返回 6 个坐标值 +4. 适用于路径转换 +5. 便于统一处理 + + +## reverseCurve + +反转基于贝塞尔曲线的路径数组。 + +- 功能说明 + +- 反转曲线路径的方向 +- 保持曲线的形状不变 +- 重新计算控制点位置 +- 仅处理贝塞尔曲线路径 +- 维护路径的连续性 + +```ts +import { reverseCurve } from '@antv/util'; + +// 基本使用 +const curve: CurveArray = [ + ['M', 0, 0], + ['C', 20, 20, 40, 20, 60, 0] +]; + +console.log(reverseCurve(curve)); +// [ +// ['M', 60, 0], +// ['C', 40, 20, 20, 20, 0, 0] +// ] + +// 多段曲线反转 +const multiCurve: CurveArray = [ + ['M', 0, 0], + ['C', 20, 20, 40, 20, 60, 0], + ['C', 80, -20, 100, -20, 120, 0] +]; + +console.log(reverseCurve(multiCurve)); +// [ +// ['M', 120, 0], +// ['C', 100, -20, 80, -20, 60, 0], +// ['C', 40, 20, 20, 20, 0, 0] +// ] + +// 实际应用示例 +function createReversiblePath(points: [number, number][]) { + // 创建曲线路径 + const createCurve = (points: [number, number][]): CurveArray => { + const result: CurveArray = [['M', points[0][0], points[0][1]]]; + + for (let i = 1; i < points.length; i++) { + const prev = points[i - 1]; + const curr = points[i]; + const cp1 = [(prev[0] + curr[0]) / 2, prev[1]]; + const cp2 = [(prev[0] + curr[0]) / 2, curr[1]]; + result.push(['C', ...cp1, ...cp2, ...curr]); + } + + return result; + }; + + const forward = createCurve(points); + const backward = reverseCurve(forward); + + return { + forward, + backward, + getPath: (reverse = false) => reverse ? backward : forward + }; +} + +// 使用示例 +const points: [number, number][] = [ + [0, 0], + [50, 50], + [100, 0] +]; + +const path = createReversiblePath(points); +console.log('Forward:', path.getPath()); +console.log('Backward:', path.getPath(true)); + +// 动画路径示例 +function createPathAnimator(curve: CurveArray) { + return { + forward: (t: number) => interpolatePath(curve, t), + backward: (t: number) => interpolatePath(reverseCurve(curve), t) + }; +} + +function interpolatePath(curve: CurveArray, t: number) { + // 简单的线性插值示例 + if (t <= 0) return curve[0].slice(-2); + if (t >= 1) return curve[curve.length - 1].slice(-2); + + const segmentIndex = Math.floor(t * (curve.length - 1)); + const segment = curve[segmentIndex + 1]; + const localT = (t * (curve.length - 1)) % 1; + + // 贝塞尔曲线插值计算 + return bezierPoint(segment.slice(1), localT); +} + +// 使用动画器 +const animator = createPathAnimator([ + ['M', 0, 0], + ['C', 20, 20, 40, 20, 60, 0] +]); + +console.log(animator.forward(0.5)); // 正向动画中点 +console.log(animator.backward(0.5)); // 反向动画中点 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathArray | 曲线路径数组 | CurveArray | - | 要反转的贝塞尔曲线路径数组 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 反转后的路径 | CurveArray | - | 反转后的贝塞尔曲线路径数组 | + +- 注意事项 + +1. 仅支持贝塞尔曲线路径 +2. 保持曲线形状不变 +3. 重新计算控制点 +4. 维护路径连续性 +5. 起点变为终点 + + +## roundPath + +对路径数组中的数值进行精度舍入。 + +- 功能说明 + +- 控制路径坐标的精度 +- 支持自定义小数位数 +- 可以关闭舍入功能 +- 保持命令类型不变 +- 创建新的路径数组 + +```ts +import { roundPath } from '@antv/util'; + +// 基本使用 +const path: PathArray = [ + ['M', 10.123, 20.456], + ['L', 30.789, 40.987] +]; + +console.log(roundPath(path, 2)); +// [ +// ['M', 10.12, 20.46], +// ['L', 30.79, 40.99] +// ] + +// 关闭舍入 +console.log(roundPath(path, 'off')); +// 返回原始值的副本 + +// 整数舍入 +console.log(roundPath(path, 0)); +// [ +// ['M', 10, 20], +// ['L', 31, 41] +// ] + +// 实际应用示例 +function createPathOptimizer(precision: number | 'off' = 2) { + return { + optimize(path: PathArray) { + return roundPath(path, precision); + }, + + // 计算路径长度(简单估算) + getLength(path: PathArray) { + const optimized = this.optimize(path); + let length = 0; + let prevX, prevY; + + optimized.forEach(segment => { + const [cmd, ...params] = segment; + if (cmd === 'M') { + [prevX, prevY] = params; + } else if (cmd === 'L') { + const [x, y] = params; + length += Math.sqrt( + Math.pow(x - prevX, 2) + Math.pow(y - prevY, 2) + ); + [prevX, prevY] = [x, y]; + } + }); + + return length; + }, + + // 创建SVG路径字符串 + toPathString(path: PathArray) { + return this.optimize(path) + .map(segment => segment.join(' ')) + .join(' '); + } + }; +} + +// 使用示例 +const optimizer = createPathOptimizer(2); + +const complexPath: PathArray = [ + ['M', 0.123, 0.456], + ['L', 50.789, 50.987], + ['L', 100.234, 0.567] +]; + +console.log(optimizer.optimize(complexPath)); +// [ +// ['M', 0.12, 0.46], +// ['L', 50.79, 50.99], +// ['L', 100.23, 0.57] +// ] + +console.log(optimizer.getLength(complexPath)); +// 计算优化后的路径长度 + +console.log(optimizer.toPathString(complexPath)); +// "M 0.12 0.46 L 50.79 50.99 L 100.23 0.57" + +// 动态精度调整 +function adjustPathPrecision(path: PathArray, viewportWidth: number) { + // 根据视口宽度动态调整精度 + const precision = viewportWidth < 500 ? 1 : + viewportWidth < 1000 ? 2 : 3; + + return roundPath(path, precision); +} + +// 使用示例 +const responsivePath = adjustPathPrecision(complexPath, 800); +console.log(responsivePath); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径数组 | PathArray | - | 要处理的路径数组 | +| round | 精度控制 | number \| 'off' | - | 小数位数或关闭舍入 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 处理后的路径 | PathArray | - | 精度调整后的路径数组 | + +- 注意事项 + +1. 仅处理数值部分 +2. 保持命令字符不变 +3. 创建新的数组 +4. 精度值必须为整数 +5. 'off'表示不进行舍入 + + +## segmentToCubic + +将各种路径段转换为三次贝塞尔曲线段。 + +- 功能说明 + +- 将不同类型的路径段统一转换为三次贝塞尔曲线 +- 支持弧线(A)、二次贝塞尔曲线(Q)、直线(L)和闭合路径(Z)的转换 +- 保持移动命令(M)不变 +- 维护控制点信息 +- 处理特殊情况 + +```ts +import { segmentToCubic } from '@antv/util'; + +// 基本参数对象 +const params = { + x1: 0, y1: 0, // 当前点 + x: 0, y: 0, // 起始点 + qx: null, qy: null // 二次贝塞尔曲线控制点 +}; + +// 直线转换 +console.log(segmentToCubic(['L', 100, 100], params)); +// ['C', 50, 50, 100, 100, 100, 100] + +// 二次贝塞尔曲线转换 +console.log(segmentToCubic(['Q', 50, 50, 100, 0], params)); +// ['C', 33.33, 33.33, 66.67, 33.33, 100, 0] + +// 实际应用示例 +function convertPathToCubic(pathSegments: PathSegment[]) { + const params = { + x1: 0, y1: 0, + x: 0, y: 0, + qx: null, qy: null + }; + + return pathSegments.map(segment => { + const converted = segmentToCubic(segment, params); + + // 更新当前点 + const [cmd, ...values] = converted; + if (values.length >= 2) { + params.x1 = values[values.length - 2]; + params.y1 = values[values.length - 1]; + } + if (cmd === 'M') { + params.x = values[0]; + params.y = values[1]; + } + + return converted; + }); +} + +// 使用示例 +const path = [ + ['M', 0, 0], + ['L', 100, 0], + ['Q', 150, 50, 200, 0], + ['A', 50, 50, 0, 0, 1, 250, 50], + ['Z'] +]; + +console.log(convertPathToCubic(path)); + +// 动画插值示例 +function createPathAnimator(segments: PathSegment[]) { + const cubicPath = convertPathToCubic(segments); + + return (t: number) => { + if (t <= 0) return cubicPath[0].slice(-2); + if (t >= 1) return cubicPath[cubicPath.length - 1].slice(-2); + + const segmentIndex = Math.floor(t * (cubicPath.length - 1)); + const segment = cubicPath[segmentIndex]; + const localT = (t * (cubicPath.length - 1)) % 1; + + if (segment[0] === 'C') { + return bezierInterpolate( + [segment[1], segment[2]], + [segment[3], segment[4]], + [segment[5], segment[6]], + localT + ); + } + + return segment.slice(-2); + }; +} + +// 贝塞尔曲线插值 +function bezierInterpolate(p1: number[], p2: number[], p3: number[], t: number) { + const mt = 1 - t; + return [ + mt * mt * p1[0] + 2 * mt * t * p2[0] + t * t * p3[0], + mt * mt * p1[1] + 2 * mt * t * p2[1] + t * t * p3[1] + ]; +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| segment | 路径段 | PathSegment | - | 要转换的路径段 | +| params | 参数对象 | ParserParams | - | 包含控制点信息的参数对象 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 转换结果 | CSegment \| MSegment | - | 转换后的三次贝塞尔曲线段或移动命令 | + + +## distanceSquareRoot + +计算两点之间的欧几里得距离。 + +- 功能说明 + +- 计算二维平面上两点之间的直线距离 +- 使用平方根公式计算 +- 接受坐标点数组作为参数 +- 返回非负数值 +- 基于欧几里得距离公式 + +```ts +import { distanceSquareRoot } from '@antv/util'; + +// 基本使用 +const point1: [number, number] = [0, 0]; +const point2: [number, number] = [3, 4]; +console.log(distanceSquareRoot(point1, point2)); // 5 + +// 水平距离 +console.log(distanceSquareRoot([0, 0], [5, 0])); // 5 + +// 垂直距离 +console.log(distanceSquareRoot([0, 0], [0, 5])); // 5 + +// 实际应用示例 +function createDistanceCalculator() { + return { + // 计算点到点集合中最近的距离 + getNearestDistance(point: [number, number], points: [number, number][]) { + return Math.min(...points.map(p => distanceSquareRoot(point, p))); + }, + + // 计算路径长度 + getPathLength(points: [number, number][]) { + let length = 0; + for (let i = 1; i < points.length; i++) { + length += distanceSquareRoot(points[i-1], points[i]); + } + return length; + }, + + // 判断点是否在圆内 + isPointInCircle( + point: [number, number], + center: [number, number], + radius: number + ) { + return distanceSquareRoot(point, center) <= radius; + } + }; +} + +// 使用示例 +const calculator = createDistanceCalculator(); + +// 计算最近距离 +const points = [ + [0, 0], + [3, 4], + [6, 8] +] as [number, number][]; + +const targetPoint: [number, number] = [2, 2]; +console.log(calculator.getNearestDistance(targetPoint, points)); + +// 计算路径长度 +console.log(calculator.getPathLength(points)); + +// 点在圆内判断 +console.log(calculator.isPointInCircle([1, 1], [0, 0], 2)); // true + +// 碰撞检测示例 +function createCollisionDetector(radius: number) { + const points: [number, number][] = []; + + return { + addPoint(point: [number, number]) { + if (!this.hasCollision(point)) { + points.push(point); + return true; + } + return false; + }, + + hasCollision(point: [number, number]) { + return points.some(p => + distanceSquareRoot(point, p) < radius * 2 + ); + }, + + getPoints() { + return [...points]; + } + }; +} + +// 使用碰撞检测器 +const detector = createCollisionDetector(5); +console.log(detector.addPoint([0, 0])); // true +console.log(detector.addPoint([4, 4])); // true +console.log(detector.addPoint([1, 1])); // false (太近) +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| a | 第一个点 | [number, number] | - | 第一个点的坐标 | +| b | 第二个点 | [number, number] | - | 第二个点的坐标 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| distance | 距离 | number | - | 两点之间的距离 | + +- 注意事项 + +1. 返回非负数值 +2. 使用欧几里得距离公式 +3. 参数必须是二维坐标 +4. 计算结果是精确值 +5. 适用于平面坐标系 + + +## equalizeSegments + +平衡两个路径的段数,使它们具有相同数量的贝塞尔曲线段。 + +- 功能说明 + +- 调整两个路径使其具有相同数量的段 +- 通过分割曲线段来实现平衡 +- 保持路径的视觉效果 +- 支持递归处理 +- 考虑曲线长度进行优化分割 + +```ts +import { equalizeSegments } from '@antv/util'; + +// 基本使用 +const path1: PathArray = [ + ['M', 0, 0], + ['C', 50, 0, 100, 50, 100, 100] +]; + +const path2: PathArray = [ + ['M', 0, 0], + ['C', 25, 25, 75, 75, 100, 100], + ['C', 125, 125, 150, 150, 200, 200] +]; + +const [equalPath1, equalPath2] = equalizeSegments(path1, path2); +console.log(equalPath1.length === equalPath2.length); // true + +// 实际应用示例 +function createPathMorpher(startPath: PathArray, endPath: PathArray) { + const [equalizedStart, equalizedEnd] = equalizeSegments(startPath, endPath); + + return { + // 获取某个时间点的插值路径 + getPath(t: number): PathArray { + return equalizedStart.map((segment, i) => { + if (segment[0] === 'M') { + return [ + 'M', + interpolate(segment[1], equalizedEnd[i][1], t), + interpolate(segment[2], equalizedEnd[i][2], t) + ]; + } else if (segment[0] === 'C') { + return [ + 'C', + interpolate(segment[1], equalizedEnd[i][1], t), + interpolate(segment[2], equalizedEnd[i][2], t), + interpolate(segment[3], equalizedEnd[i][3], t), + interpolate(segment[4], equalizedEnd[i][4], t), + interpolate(segment[5], equalizedEnd[i][5], t), + interpolate(segment[6], equalizedEnd[i][6], t) + ]; + } + return segment; + }) as PathArray; + }, + + // 获取动画帧 + getFrames(frames: number): PathArray[] { + return Array.from({ length: frames }, (_, i) => + this.getPath(i / (frames - 1)) + ); + } + }; +} + +// 线性插值函数 +function interpolate(start: number, end: number, t: number): number { + return start + (end - start) * t; +} + +// 使用示例 +const morpher = createPathMorpher( + [['M', 0, 0], ['C', 50, 0, 100, 50, 100, 100]], + [['M', 100, 0], ['C', 150, 0, 200, 50, 200, 100]] +); + +// 获取中间状态 +console.log(morpher.getPath(0.5)); + +// 获取动画帧 +const frames = morpher.getFrames(5); +console.log(frames); + +// 路径动画示例 +function animatePath(element: SVGPathElement, startPath: PathArray, endPath: PathArray, duration: number) { + const morpher = createPathMorpher(startPath, endPath); + const startTime = performance.now(); + + function update(currentTime: number) { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + + const currentPath = morpher.getPath(progress); + element.setAttribute('d', pathToString(currentPath)); + + if (progress < 1) { + requestAnimationFrame(update); + } + } + + requestAnimationFrame(update); +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path1 | 第一个路径 | PathArray | - | 要平衡的第一个路径 | +| path2 | 第二个路径 | PathArray | - | 要平衡的第二个路径 | +| TL | 目标段数 | number | - | 可选的目标段数 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 平衡后的路径 | CurveArray[] | - | 包含两个平衡后路径的数组 | + +- 注意事项 + +1. 保持路径视觉效果 +2. 考虑曲线长度 +3. 递归处理直到平衡 +4. 仅处理贝塞尔曲线 +5. 维护路径连续性 + + +## getDrawDirection + +判断路径的绘制方向(顺时针或逆时针)。 + +- 功能说明 + +- 通过计算路径面积判断绘制方向 +- 正面积表示顺时针方向 +- 负面积表示逆时针方向 +- 适用于闭合路径 +- 基于路径面积计算 + +```ts +import { getDrawDirection } from '@antv/util'; + +// 基本使用 +const clockwise: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100], + ['L', 0, 100], + ['Z'] +]; +console.log(getDrawDirection(clockwise)); // true (顺时针) + +const counterClockwise: PathArray = [ + ['M', 0, 0], + ['L', 0, 100], + ['L', 100, 100], + ['L', 100, 0], + ['Z'] +]; +console.log(getDrawDirection(counterClockwise)); // false (逆时针) + +// 实际应用示例 +function createPathAnalyzer() { + return { + // 判断绘制方向 + isClockwise(path: PathArray) { + return getDrawDirection(path); + }, + + // 确保路径为顺时针方向 + ensureClockwise(path: PathArray): PathArray { + return this.isClockwise(path) ? path : reversePath(path); + }, + + // 确保路径为逆时针方向 + ensureCounterClockwise(path: PathArray): PathArray { + return this.isClockwise(path) ? reversePath(path) : path; + }, + + // 反转路径方向 + reversePath(path: PathArray): PathArray { + const result = [path[0]]; // 保持起点 + for (let i = path.length - 1; i > 0; i--) { + result.push(path[i]); + } + return result; + } + }; +} + +// 使用示例 +const analyzer = createPathAnalyzer(); + +const path: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100], + ['L', 0, 100], + ['Z'] +]; + +console.log(analyzer.isClockwise(path)); // true +console.log(analyzer.ensureCounterClockwise(path)); // 返回反转后的路径 + +// 复合路径处理示例 +function processCompoundPath(paths: PathArray[]) { + const analyzer = createPathAnalyzer(); + + return paths.map((path, index) => + // 外层路径顺时针,内层路径逆时针 + index === 0 ? + analyzer.ensureClockwise(path) : + analyzer.ensureCounterClockwise(path) + ); +} + +// 使用示例 +const compound = [ + // 外层矩形 + [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100], + ['L', 0, 100], + ['Z'] + ], + // 内层矩形 + [ + ['M', 25, 25], + ['L', 75, 25], + ['L', 75, 75], + ['L', 25, 75], + ['Z'] + ] +]; + +console.log(processCompoundPath(compound)); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathArray | 路径数组 | PathArray | - | 要判断方向的路径数组 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 方向判断结果 | boolean | - | true 表示顺时针,false 表示逆时针 | + +- 注意事项 + +1. 适用于闭合路径 +2. 基于路径面积计算 +3. 路径应该是完整的 +4. 考虑路径的起始点 +5. 支持复杂路径 + + +## getPathArea + +计算路径的面积,支持复杂的贝塞尔曲线路径。 + +- 功能说明 + +- 计算路径围成的面积 +- 支持三次贝塞尔曲线 +- 自动转换路径为曲线形式 +- 考虑路径方向(正负面积) +- 基于数学公式精确计算 + +```ts +import { getPathArea } from '@antv/util'; + +// 基本矩形路径 +const rectangle: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100], + ['L', 0, 100], + ['Z'] +]; +console.log(getPathArea(rectangle)); // 10000 (100 * 100) + +// 曲线路径 +const curve: PathArray = [ + ['M', 0, 0], + ['C', 50, 0, 100, 50, 100, 100], + ['C', 100, 150, 50, 200, 0, 200], + ['Z'] +]; +console.log(getPathArea(curve)); // 返回曲线围成的面积 + +// 实际应用示例 +function createShapeAnalyzer() { + return { + // 计算面积 + getArea(path: PathArray) { + return Math.abs(getPathArea(path)); + }, + + // 判断点是否在路径内 + isPointInPath(path: PathArray, point: [number, number]) { + // 通过射线法判断点是否在路径内 + const testPath: PathArray = [ + ['M', point[0], point[1]], + ['L', point[0] + 10000, point[1]] // 水平射线 + ]; + + // 计算交点数量 + const intersections = getPathIntersections(path, testPath); + return intersections.length % 2 === 1; + }, + + // 计算路径的重心 + getCentroid(path: PathArray) { + const area = getPathArea(path); + let cx = 0; + let cy = 0; + + // 遍历路径段计算重心 + let x = 0, y = 0; + path2Curve(path).forEach(seg => { + if (seg[0] === 'M') { + [, x, y] = seg; + } else { + const [c1x, c1y, c2x, c2y, x2, y2] = seg.slice(1); + // 计算每段的贡献 + const segArea = getCubicSegArea(x, y, c1x, c1y, c2x, c2y, x2, y2); + cx += ((x + c1x + c2x + x2) / 4) * segArea; + cy += ((y + c1y + c2y + y2) / 4) * segArea; + [x, y] = [x2, y2]; + } + }); + + return { + x: cx / area, + y: cy / area + }; + } + }; +} + +// 使用示例 +const analyzer = createShapeAnalyzer(); + +const shape: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100], + ['L', 0, 100], + ['Z'] +]; + +console.log(analyzer.getArea(shape)); // 10000 +console.log(analyzer.isPointInPath(shape, [50, 50])); // true +console.log(analyzer.getCentroid(shape)); // { x: 50, y: 50 } +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径数组 | PathArray | - | 要计算面积的路径数组 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| area | 面积值 | number | - | 路径围成的面积(可能为负) | + +- 注意事项 + +1. 路径应该是闭合的 +2. 返回值可能为负(取决于路径方向) +3. 自动转换为贝塞尔曲线 +4. 基于精确的数学公式 +5. 支持复杂路径计算 + + +## getPathBBoxTotalLength + +计算路径的边界框和总长度信息。 + +- 功能说明 + +- 计算路径的边界框(BBox) +- 计算路径的总长度 +- 计算中心点坐标 +- 提供估算的深度值 +- 处理空路径的情况 + +```ts +import { getPathBBoxTotalLength } from '@antv/util'; + +// 基本使用 +const path: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100], + ['L', 0, 100], + ['Z'] +]; + +console.log(getPathBBoxTotalLength(path)); +// { +// length: 400, // 路径总长度 +// width: 100, // 宽度 +// height: 100, // 高度 +// x: 0, // 左上角x坐标 +// y: 0, // 左上角y坐标 +// x2: 100, // 右下角x坐标 +// y2: 100, // 右下角y坐标 +// cx: 50, // 中心x坐标 +// cy: 50, // 中心y坐标 +// cz: 150 // 估算深度 +// } + +// 实际应用示例 +function createPathAnalyzer() { + return { + // 获取路径信息 + getPathInfo(path: PathArray) { + const info = getPathBBoxTotalLength(path); + return { + ...info, + // 计算额外信息 + aspectRatio: info.width / info.height, + area: info.width * info.height, + perimeter: info.length, + diagonal: Math.sqrt(info.width ** 2 + info.height ** 2) + }; + }, + + // 创建居中变换 + createCenterTransform(path: PathArray, targetWidth: number, targetHeight: number) { + const { width, height, cx, cy } = getPathBBoxTotalLength(path); + + const scale = Math.min( + targetWidth / width, + targetHeight / height + ); + + const tx = targetWidth / 2 - cx * scale; + const ty = targetHeight / 2 - cy * scale; + + return { + scale, + translate: [tx, ty], + transform: `translate(${tx},${ty}) scale(${scale})` + }; + }, + + // 检查碰撞 + checkCollision(path1: PathArray, path2: PathArray) { + const box1 = getPathBBoxTotalLength(path1); + const box2 = getPathBBoxTotalLength(path2); + + return !( + box2.x > box1.x2 || + box2.x2 < box1.x || + box2.y > box1.y2 || + box2.y2 < box1.y + ); + } + }; +} + +// 使用示例 +const analyzer = createPathAnalyzer(); + +// 获取路径信息 +const pathInfo = analyzer.getPathInfo(path); +console.log(pathInfo); + +// 创建居中变换 +const transform = analyzer.createCenterTransform(path, 500, 500); +console.log(transform); + +// 碰撞检测 +const path2: PathArray = [ + ['M', 50, 50], + ['L', 150, 50], + ['L', 150, 150], + ['L', 50, 150], + ['Z'] +]; + +console.log(analyzer.checkCollision(path, path2)); // true +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径数组 | PathArray | - | 要分析的路径数组 | +| options | 配置选项 | Partial | - | 可选的配置参数 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 路径信息 | PathBBoxTotalLength | - | 包含边界框和长度信息的对象 | + +- 返回对象属性 + +| 属性 | 说明 | 类型 | 中文说明 | +|---------|------|------|---------| +| length | 路径长度 | number | 路径的总长度 | +| width | 宽度 | number | 边界框的宽度 | +| height | 高度 | number | 边界框的高度 | +| x | 左上角 x | number | 边界框左上角 x 坐标 | +| y | 左上角 y | number | 边界框左上角 y 坐标 | +| x2 | 右下角 x | number | 边界框右下角 x 坐标 | +| y2 | 右下角 y | number | 边界框右下角 y 坐标 | +| cx | 中心 x | number | 边界框中心 x 坐标 | +| cy | 中心 y | number | 边界框中心 y 坐标 | +| cz | 深度估计 | number | 估算的深度值 | + + +## getPathBBox + +获取路径的边界框(Bounding Box)信息。 + +- 功能说明 + +- 计算路径的边界框 +- 支持字符串或数组格式的路径 +- 计算中心点坐标 +- 提供估算的深度值 +- 处理空路径的情况 + +```ts +import { getPathBBox } from '@antv/util'; + +// 基本使用 +const path: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100], + ['L', 0, 100], + ['Z'] +]; + +console.log(getPathBBox(path)); +// { +// width: 100, // 宽度 +// height: 100, // 高度 +// x: 0, // 左上角x坐标 +// y: 0, // 左上角y坐标 +// x2: 100, // 右下角x坐标 +// y2: 100, // 右下角y坐标 +// cx: 50, // 中心x坐标 +// cy: 50, // 中心y坐标 +// cz: 150 // 估算深度 +// } + +// 字符串路径 +console.log(getPathBBox('M0,0 L100,0 L100,100 L0,100 Z')); + +// 实际应用示例 +function createShapeUtil() { + return { + // 计算缩放比例以适应目标大小 + calculateScale(path: PathArray, targetWidth: number, targetHeight: number) { + const bbox = getPathBBox(path); + return { + scaleX: targetWidth / bbox.width, + scaleY: targetHeight / bbox.height, + uniform: Math.min(targetWidth / bbox.width, targetHeight / bbox.height) + }; + }, + + // 创建居中变换矩阵 + createCenterTransform(path: PathArray, containerWidth: number, containerHeight: number) { + const bbox = getPathBBox(path); + + return { + translateX: (containerWidth - bbox.width) / 2 - bbox.x, + translateY: (containerHeight - bbox.height) / 2 - bbox.y, + toString() { + return `translate(${this.translateX},${this.translateY})`; + } + }; + }, + + // 检查是否包含点 + containsPoint(path: PathArray, x: number, y: number) { + const bbox = getPathBBox(path); + return ( + x >= bbox.x && + x <= bbox.x2 && + y >= bbox.y && + y <= bbox.y2 + ); + } + }; +} + +// 使用示例 +const shapeUtil = createShapeUtil(); + +// 计算缩放 +const scale = shapeUtil.calculateScale(path, 500, 300); +console.log(scale); + +// 居中变换 +const transform = shapeUtil.createCenterTransform(path, 800, 600); +console.log(transform.toString()); + +// 点击检测 +console.log(shapeUtil.containsPoint(path, 50, 50)); // true + +// SVG元素定位示例 +function positionElement(element: SVGElement, path: PathArray, padding = 10) { + const bbox = getPathBBox(path); + + element.style.position = 'absolute'; + element.style.left = `${bbox.x - padding}px`; + element.style.top = `${bbox.y - padding}px`; + element.style.width = `${bbox.width + padding * 2}px`; + element.style.height = `${bbox.height + padding * 2}px`; +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径 | string \| PathArray | - | 要分析的路径 | +| options | 配置选项 | Partial | - | 可选的配置参数 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 边界框信息 | PathBBox | - | 包含边界框信息的对象 | + +- 返回对象属性 + +| 属性 | 说明 | 类型 | 中文说明 | +|---------|------|------|---------| +| width | 宽度 | number | 边界框的宽度 | +| height | 高度 | number | 边界框的高度 | +| x | 左上角 x | number | 边界框左上角 x 坐标 | +| y | 左上角 y | number | 边界框左上角 y 坐标 | +| x2 | 右下角 x | number | 边界框右下角 x 坐标 | +| y2 | 右下角 y | number | 边界框右下角 y 坐标 | +| cx | 中心 x | number | 边界框中心 x 坐标 | +| cy | 中心 y | number | 边界框中心 y 坐标 | +| cz | 深度估计 | number | 估算的深度值 | + + +## getPointAtLength + +获取路径上指定距离处的坐标点。 + +- 功能说明 + +- 计算路径上特定距离的点坐标 +- 支持字符串或数组格式的路径 +- 基于路径总长度的比例计算 +- 支持自定义配置选项 +- 精确定位路径上的点 + +```ts +import { getPointAtLength } from '@antv/util'; + +// 基本使用 +const path: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100] +]; + +console.log(getPointAtLength(path, 50)); // [50, 0] +console.log(getPointAtLength(path, 150)); // [100, 50] + +// 字符串路径 +console.log(getPointAtLength('M0,0 L100,0 L100,100', 100)); + +// 实际应用示例 +function createPathAnimator() { + return { + // 创建路径动画 + animate(path: PathArray, duration: number = 1000) { + const startTime = performance.now(); + const totalLength = pathLengthFactory(path, undefined, { length: true }).length; + + return { + // 获取当前动画帧的点 + getCurrentPoint(currentTime: number) { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + return getPointAtLength(path, totalLength * progress); + }, + + // 创建动画函数 + start(callback: (point: [number, number]) => void) { + const animate = (time: number) => { + const point = this.getCurrentPoint(time); + callback(point); + + if (time - startTime < duration) { + requestAnimationFrame(animate); + } + }; + + requestAnimationFrame(animate); + } + }; + }, + + // 创建路径采样器 + createSampler(path: PathArray, samples: number = 100) { + const totalLength = pathLengthFactory(path, undefined, { length: true }).length; + const step = totalLength / (samples - 1); + + return Array.from({ length: samples }, (_, i) => + getPointAtLength(path, i * step) + ); + }, + + // 创建路径跟随器 + createFollower(path: PathArray) { + const totalLength = pathLengthFactory(path, undefined, { length: true }).length; + + return { + getPointAtDistance(distance: number) { + return getPointAtLength(path, distance); + }, + + getPointAtPercent(percent: number) { + return getPointAtLength(path, totalLength * Math.min(Math.max(percent, 0), 1)); + } + }; + } + }; +} + +// 使用示例 +const animator = createPathAnimator(); + +// 路径动画 +const path = [ + ['M', 0, 0], + ['C', 50, 0, 100, 50, 100, 100] +]; + +animator.animate(path).start(point => { + console.log('Current position:', point); +}); + +// 路径采样 +const samples = animator.createSampler(path, 10); +console.log('Path samples:', samples); + +// 路径跟随 +const follower = animator.createFollower(path); +console.log(follower.getPointAtPercent(0.5)); // 路径中点 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathInput | 路径 | string \| PathArray | - | 要分析的路径 | +| distance | 距离 | number | - | 要获取点的距离 | +| options | 配置选项 | Partial | - | 可选的配置参数 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| point | 坐标点 | [number, number] | - | 指定距离处的坐标点 | + + +## getPropertiesAtLength + +获取路径上指定距离处的段属性信息。 + +- 功能说明 + +- 获取指定距离处的路径段信息 +- 计算到该段的累计长度 +- 返回段的索引和属性 +- 支持字符串或数组格式的路径 +- 处理边界情况 + +```ts +import { getPropertiesAtLength } from '@antv/util'; + +// 基本使用 +const path: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100], + ['L', 0, 100] +]; + +console.log(getPropertiesAtLength(path, 150)); +// { +// segment: ['L', 100, 100], // 当前段 +// index: 2, // 段索引 +// length: 100, // 段长度 +// lengthAtSegment: 100, // 到该段起点的累计长度 +// point: { x: 100, y: 100 } // 段终点坐标 +// } + +// 实际应用示例 +function createPathAnalyzer() { + return { + // 获取路径段信息 + getSegmentInfo(path: PathArray, distance: number) { + const props = getPropertiesAtLength(path, distance); + return { + ...props, + // 计算在当前段的进度 + segmentProgress: props.length ? + (distance - props.lengthAtSegment) / props.length : 0, + // 计算总体进度 + totalProgress: distance / getTotalLength(path) + }; + }, + + // 创建路径动画控制器 + createAnimationController(path: PathArray) { + const totalLength = getTotalLength(path); + + return { + // 获取指定进度的位置信息 + getPositionAt(progress: number) { + const distance = totalLength * Math.min(Math.max(progress, 0), 1); + return this.getSegmentInfo(path, distance); + }, + + // 创建动画生成器 + *createAnimator(duration: number, fps = 60) { + const frames = duration / 1000 * fps; + const step = 1 / frames; + + for (let progress = 0; progress <= 1; progress += step) { + yield this.getPositionAt(progress); + } + } + }; + }, + + // 路径分段器 + splitPath(path: PathArray, segments: number) { + const totalLength = getTotalLength(path); + const segmentLength = totalLength / segments; + + return Array.from({ length: segments + 1 }, (_, i) => + getPropertiesAtLength(path, i * segmentLength) + ); + } + }; +} + +// 使用示例 +const analyzer = createPathAnalyzer(); + +// 获取特定位置信息 +const segmentInfo = analyzer.getSegmentInfo(path, 150); +console.log(segmentInfo); + +// 创建动画控制器 +const controller = analyzer.createAnimationController(path); +const animator = controller.createAnimator(1000); + +// 运行动画 +function runAnimation() { + const frame = animator.next(); + if (!frame.done) { + console.log('Current position:', frame.value); + requestAnimationFrame(runAnimation); + } +} +runAnimation(); + +// 路径分段 +const segments = analyzer.splitPath(path, 4); +console.log('Path segments:', segments); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathInput | 路径 | string \| PathArray | - | 要分析的路径 | +| distance | 距离 | number | - | 指定的距离 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 段属性 | SegmentProperties | - | 包含段信息的对象 | + +- 返回对象属性 + +| 属性 | 说明 | 类型 | 中文说明 | +|---------|------|------|---------| +| segment | 路径段 | PathSegment | 当前路径段 | +| index | 索引 | number | 段在路径中的索引 | +| length | 长度 | number | 当前段的长度 | +| lengthAtSegment | 累计长度 | number | 到该段的累计长度 | +| point | 坐标点 | {x: number, y: number} | 段终点坐标 | + + +## getPropertiesAtPoint + +获取路径上距离指定点最近的位置及其属性信息。 + +- 功能说明 + +- 查找路径上最接近给定点的位置 +- 计算点到路径的最短距离 +- 返回最近点的段属性信息 +- 使用二分查找优化精度 +- 支持复杂路径结构 + +```ts +import { getPropertiesAtPoint } from '@antv/util'; + +// 基本使用 +const path: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100] +]; + +const point = { x: 50, y: 50 }; +console.log(getPropertiesAtPoint(path, point)); +// { +// closest: { x: 100, y: 50 }, // 最近点 +// distance: 50, // 最短距离 +// segment: { // 段属性 +// segment: ['L', 100, 100], +// index: 2, +// length: 100, +// lengthAtSegment: 100 +// } +// } + +// 实际应用示例 +function createPathInteractor() { + return { + // 创建路径跟随器 + createPathFollower(path: PathArray) { + return { + // 获取最近点信息 + getNearestPoint(point: Point) { + return getPropertiesAtPoint(path, point); + }, + + // 检查点是否在路径附近 + isNearPath(point: Point, threshold = 5) { + const { distance } = getPropertiesAtPoint(path, point); + return distance <= threshold; + }, + + // 获取路径上的投影点 + getProjection(point: Point) { + const { closest } = getPropertiesAtPoint(path, point); + return closest; + } + }; + }, + + // 创建磁吸效果 + createSnapEffect(path: PathArray, threshold = 10) { + return (point: Point) => { + const { closest, distance } = getPropertiesAtPoint(path, point); + if (distance <= threshold) { + return closest; + } + return point; + }; + }, + + // 创建路径距离监听器 + createDistanceMonitor(path: PathArray) { + return { + // 监听点到路径的距离变化 + watch(point: Point, callback: (info: PointProperties) => void) { + let lastDistance = Infinity; + + return (newPoint: Point) => { + const props = getPropertiesAtPoint(path, newPoint); + if (props.distance !== lastDistance) { + lastDistance = props.distance; + callback(props); + } + }; + } + }; + } + }; +} + +// 使用示例 +const interactor = createPathInteractor(); + +// 创建路径跟随器 +const follower = interactor.createPathFollower(path); +console.log(follower.isNearPath({ x: 50, y: 10 })); // true + +// 创建磁吸效果 +const snap = interactor.createSnapEffect(path); +console.log(snap({ x: 98, y: 48 })); // { x: 100, y: 50 } + +// 创建距离监听器 +const monitor = interactor.createDistanceMonitor(path); +const unwatch = monitor.watch({ x: 0, y: 0 }, (info) => { + console.log('Distance changed:', info.distance); +}); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathInput | 路径 | string \| PathArray | - | 要分析的路径 | +| point | 目标点 | Point | - | 要检测的点坐标 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 点属性 | PointProperties | - | 包含最近点信息的对象 | + +- 返回对象属性 + +| 属性 | 说明 | 类型 | 中文说明 | +|---------|------|------|---------| +| closest | 最近点 | Point | 路径上最近的点 | +| distance | 距离 | number | 到路径的最短距离 | +| segment | 段属性 | SegmentProperties | 最近点所在段的属性 | + + +## getRotatedCurve + +获取最佳旋转匹配的曲线路径。 + +- 功能说明 + +- 计算两个曲线之间的最佳旋转匹配 +- 通过比较点之间的距离来评估匹配度 +- 支持循环旋转比较 +- 返回最佳匹配的旋转路径 +- 优化路径变形效果 + +```ts +import { getRotatedCurve } from '@antv/util'; + +// 基本使用 +const curve1: CurveArray = [ + ['M', 0, 0], + ['C', 20, 20, 40, 20, 60, 0], + ['C', 80, -20, 100, -20, 120, 0] +]; + +const curve2: CurveArray = [ + ['M', 120, 0], + ['C', 100, -20, 80, -20, 60, 0], + ['C', 40, 20, 20, 20, 0, 0] +]; + +console.log(getRotatedCurve(curve1, curve2)); +// 返回最佳匹配的旋转路径 + +// 实际应用示例 +function createShapeMorpher() { + return { + // 创建形状变形器 + createMorph(from: CurveArray, to: CurveArray) { + const rotated = getRotatedCurve(from, to); + + return { + // 获取中间状态 + getIntermediate(progress: number) { + return rotated.map((segment, i) => { + if (segment[0] === 'M') { + return [ + 'M', + interpolate(segment[1], to[i][1], progress), + interpolate(segment[2], to[i][2], progress) + ]; + } + return [ + 'C', + ...segment.slice(1).map((value, j) => + interpolate(value, to[i][j + 1], progress) + ) + ]; + }); + }, + + // 创建动画帧 + createFrames(frames: number) { + return Array.from({ length: frames }, (_, i) => + this.getIntermediate(i / (frames - 1)) + ); + } + }; + }, + + // 创建形状匹配器 + createMatcher(shapes: CurveArray[]) { + return { + // 找到最佳匹配 + findBestMatch(target: CurveArray) { + return shapes.map(shape => ({ + shape, + rotated: getRotatedCurve(shape, target), + distance: calculateDistance(shape, target) + })).sort((a, b) => a.distance - b.distance)[0]; + } + }; + } + }; +} + +// 辅助函数:插值计算 +function interpolate(start: number, end: number, progress: number) { + return start + (end - start) * progress; +} + +// 辅助函数:计算两个形状间的距离 +function calculateDistance(shape1: CurveArray, shape2: CurveArray) { + return shape1.reduce((sum, segment, i) => { + if (segment[0] === 'M') return sum; + return sum + distanceSquareRoot( + segment.slice(-2) as [number, number], + shape2[i].slice(-2) as [number, number] + ); + }, 0); +} + +// 使用示例 +const morpher = createShapeMorpher(); + +// 创建形状变形 +const morphing = morpher.createMorph(curve1, curve2); +console.log(morphing.getIntermediate(0.5)); // 中间状态 +console.log(morphing.createFrames(10)); // 动画帧 + +// 形状匹配 +const matcher = morpher.createMatcher([curve1, curve2]); +const bestMatch = matcher.findBestMatch(curve1); +console.log(bestMatch); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| a | 源曲线 | CurveArray | - | 源曲线路径 | +| b | 目标曲线 | CurveArray | - | 目标曲线路径 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 旋转后的曲线 | CurveArray | - | 最佳匹配的旋转曲线 | + + +## getTotalLength + +计算路径的总长度。 + +- 功能说明 + +- 计算路径的总长度 +- 支持字符串或数组格式的路径 +- 提供配置选项 +- 高效准确的长度计算 +- 等同于 SVG 的 getTotalLength() 方法 + +```ts +import { getTotalLength } from '@antv/util'; + +// 基本使用 +const path: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100] +]; + +console.log(getTotalLength(path)); // 200 + +// 字符串路径 +console.log(getTotalLength('M0,0 L100,0 L100,100')); // 200 + +// 实际应用示例 +function createPathUtil() { + return { + // 计算路径进度 + calculateProgress(path: PathArray, distance: number) { + const totalLength = getTotalLength(path); + return Math.min(Math.max(distance / totalLength, 0), 1); + }, + + // 创建路径采样器 + createSampler(path: PathArray, samples: number = 100) { + const totalLength = getTotalLength(path); + const step = totalLength / (samples - 1); + + return { + // 获取采样点 + getPoints() { + return Array.from({ length: samples }, (_, i) => + getPointAtLength(path, i * step) + ); + }, + + // 获取均匀分布的长度 + getLengths() { + return Array.from({ length: samples }, (_, i) => i * step); + } + }; + }, + + // 创建路径动画控制器 + createAnimationController(path: PathArray) { + const totalLength = getTotalLength(path); + + return { + // 获取指定时间的位置 + getPositionAtTime(time: number, duration: number) { + const progress = Math.min(time / duration, 1); + return getPointAtLength(path, totalLength * progress); + }, + + // 创建动画生成器 + *createFrames(duration: number, fps = 60) { + const frameCount = duration / 1000 * fps; + const step = totalLength / frameCount; + + for (let distance = 0; distance <= totalLength; distance += step) { + yield getPointAtLength(path, distance); + } + } + }; + } + }; +} + +// 使用示例 +const pathUtil = createPathUtil(); + +// 计算进度 +console.log(pathUtil.calculateProgress(path, 100)); // 0.5 + +// 路径采样 +const sampler = pathUtil.createSampler(path, 5); +console.log(sampler.getPoints()); +console.log(sampler.getLengths()); + +// 动画控制 +const animator = pathUtil.createAnimationController(path); + +// 创建动画 +function animate(duration: number) { + const startTime = performance.now(); + + function update(currentTime: number) { + const elapsed = currentTime - startTime; + const position = animator.getPositionAtTime(elapsed, duration); + + // 更新位置... + console.log(position); + + if (elapsed < duration) { + requestAnimationFrame(update); + } + } + + requestAnimationFrame(update); +} + +animate(1000); // 1秒动画 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathInput | 路径 | string \| PathArray | - | 要计算长度的路径 | +| options | 配置选项 | Partial | - | 可选的配置参数 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| length | 总长度 | number | - | 路径的总长度 | + + +## isAbsoluteArray + +判断路径数组是否全部由绝对坐标命令组成。 + +- 功能说明 + +- 检查路径数组是否为绝对坐标 +- 验证所有命令是否为大写 +- 类型保护功能 +- 支持路径数组格式 +- 用于路径验证 + +```ts +import { isAbsoluteArray } from '@antv/util'; + +// 基本使用 +const absolutePath: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100] +]; +console.log(isAbsoluteArray(absolutePath)); // true + +const relativePath: PathArray = [ + ['M', 0, 0], + ['l', 100, 0], + ['l', 0, 100] +]; +console.log(isAbsoluteArray(relativePath)); // false + +// 实际应用示例 +function createPathValidator() { + return { + // 验证路径格式 + validatePath(path: PathArray) { + return { + isValid: isPathArray(path), + isAbsolute: isAbsoluteArray(path), + hasRelative: path.some(([cmd]) => cmd !== cmd.toUpperCase()), + commands: path.map(([cmd]) => cmd) + }; + }, + + // 确保路径为绝对坐标 + ensureAbsolute(path: PathArray): AbsoluteArray { + if (isAbsoluteArray(path)) { + return path; + } + return convertToAbsolute(path); + }, + + // 创建路径检查器 + createPathChecker() { + return { + // 检查路径是否符合要求 + check(path: PathArray) { + if (!isAbsoluteArray(path)) { + throw new Error('Path must use absolute coordinates'); + } + return true; + }, + + // 验证并修复路径 + validate(path: PathArray) { + return this.check(path) ? path : this.ensureAbsolute(path); + } + }; + } + }; +} + +// 使用示例 +const validator = createPathValidator(); + +// 验证路径 +const pathInfo = validator.validatePath(absolutePath); +console.log(pathInfo); +// { +// isValid: true, +// isAbsolute: true, +// hasRelative: false, +// commands: ['M', 'L', 'L'] +// } + +// 路径检查器 +const checker = validator.createPathChecker(); + +try { + checker.check(relativePath); +} catch (error) { + console.log(error.message); // "Path must use absolute coordinates" +} + +// 辅助函数:转换为绝对坐标 +function convertToAbsolute(path: PathArray): AbsoluteArray { + let currentX = 0; + let currentY = 0; + + return path.map(segment => { + const [cmd, ...params] = segment; + if (cmd === cmd.toLowerCase()) { + // 转换相对坐标为绝对坐标 + const absolute = [cmd.toUpperCase()]; + for (let i = 0; i < params.length; i += 2) { + currentX += params[i] || 0; + currentY += params[i + 1] || 0; + absolute.push(currentX, currentY); + } + return absolute; + } + // 更新当前位置 + if (params.length >= 2) { + [currentX, currentY] = params.slice(-2); + } + return segment; + }) as AbsoluteArray; +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径 | string \| PathArray | - | 要检查的路径 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 判断结果 | boolean | - | 是否为绝对坐标路径 | + + +## isCurveArray + +判断路径数组是否全部由三次贝塞尔曲线段(C)和移动命令(M)组成。 + +- 功能说明 + +- 检查路径是否为标准化的曲线数组 +- 验证所有段是否为 M 或 C 命令 +- 提供类型保护功能 +- 用于曲线路径验证 +- 确保路径格式一致性 + +```ts +import { isCurveArray } from '@antv/util'; + +// 基本使用 +const curvePath: PathArray = [ + ['M', 0, 0], + ['C', 20, 20, 40, 20, 60, 0] +]; +console.log(isCurveArray(curvePath)); // true + +const mixedPath: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['Q', 150, 50, 200, 0] +]; +console.log(isCurveArray(mixedPath)); // false + +// 实际应用示例 +function createCurveUtil() { + return { + // 验证曲线路径 + validateCurve(path: PathArray) { + return { + isCurve: isCurveArray(path), + segments: path.map(([cmd]) => cmd), + isValid: path.every(([cmd]) => 'MC'.includes(cmd)) + }; + }, + + // 确保路径为曲线格式 + ensureCurve(path: PathArray) { + if (isCurveArray(path)) { + return path; + } + return convertToCurve(path); + }, + + // 创建曲线处理器 + createCurveProcessor() { + return { + // 处理路径 + process(path: PathArray) { + const curves = this.ensureCurve(path); + return { + // 获取控制点 + getControlPoints() { + return curves.reduce((points, segment) => { + if (segment[0] === 'C') { + points.push( + [segment[1], segment[2]], + [segment[3], segment[4]] + ); + } + return points; + }, [] as [number, number][]); + }, + + // 简化曲线 + simplify(tolerance = 1) { + return simplifyBezier(curves, tolerance); + } + }; + } + }; + } + }; +} + +// 使用示例 +const curveUtil = createCurveUtil(); + +// 验证曲线 +const curveInfo = curveUtil.validateCurve(curvePath); +console.log(curveInfo); +// { +// isCurve: true, +// segments: ['M', 'C'], +// isValid: true +// } + +// 曲线处理 +const processor = curveUtil.createCurveProcessor(); +const processed = processor.process(curvePath); +console.log(processed.getControlPoints()); + +// 辅助函数:转换为曲线 +function convertToCurve(path: PathArray): PathArray { + return path.map((segment, i) => { + const [cmd, ...params] = segment; + if (cmd === 'M') return segment; + if (cmd === 'L') { + const prev = path[i - 1]; + const [x1, y1] = prev.slice(-2); + const [x2, y2] = params; + // 将直线转换为曲线 + return [ + 'C', + x1 + (x2 - x1) / 3, + y1 + (y2 - y1) / 3, + x1 + 2 * (x2 - x1) / 3, + y1 + 2 * (y2 - y1) / 3, + x2, + y2 + ]; + } + // 其他命令的转换... + return segment; + }); +} + +// 简化贝塞尔曲线 +function simplifyBezier(curves: PathArray, tolerance: number): PathArray { + // 实现曲线简化算法... + return curves; +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径 | string \| PathArray | - | 要检查的路径 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 判断结果 | boolean | - | 是否为曲线数组 | + + +## isNormalizedArray + +判断路径数组是否为标准化的绝对坐标路径(不包含简写命令)。 + +- 功能说明 + +- 检查路径是否使用绝对坐标 +- 验证是否只包含标准命令(A、C、L、M、Q、Z) +- 不允许使用简写命令(H、V、S、T) +- 提供类型保护功能 +- 确保路径格式标准化 + +```ts +import { isNormalizedArray } from '@antv/util'; + +// 基本使用 +const normalizedPath: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['C', 150, 0, 200, 50, 200, 100] +]; +console.log(isNormalizedArray(normalizedPath)); // true + +const shorthandPath: PathArray = [ + ['M', 0, 0], + ['H', 100], // 简写命令 + ['V', 100] // 简写命令 +]; +console.log(isNormalizedArray(shorthandPath)); // false + +// 实际应用示例 +function createPathNormalizer() { + return { + // 验证路径格式 + validatePath(path: PathArray) { + return { + isNormalized: isNormalizedArray(path), + hasShorthand: path.some(([cmd]) => 'HSTVT'.includes(cmd)), + commands: path.map(([cmd]) => cmd), + isAbsolute: path.every(([cmd]) => cmd === cmd.toUpperCase()) + }; + }, + + // 标准化路径 + normalizePath(path: PathArray) { + if (isNormalizedArray(path)) { + return path; + } + return convertToNormalized(path); + }, + + // 创建路径处理器 + createPathProcessor() { + return { + // 处理并验证路径 + process(path: PathArray) { + const normalized = this.normalizePath(path); + + return { + path: normalized, + // 获取所有点 + getPoints() { + return normalized.map(segment => + segment.slice(-2) as [number, number] + ); + }, + // 获取命令统计 + getCommandStats() { + return normalized.reduce((stats, [cmd]) => { + stats[cmd] = (stats[cmd] || 0) + 1; + return stats; + }, {} as Record); + } + }; + } + }; + } + }; +} + +// 使用示例 +const normalizer = createPathNormalizer(); + +// 验证路径 +const pathInfo = normalizer.validatePath(normalizedPath); +console.log(pathInfo); +// { +// isNormalized: true, +// hasShorthand: false, +// commands: ['M', 'L', 'C'], +// isAbsolute: true +// } + +// 路径处理 +const processor = normalizer.createPathProcessor(); +const processed = processor.process(shorthandPath); +console.log(processed.getCommandStats()); + +// 辅助函数:转换为标准化格式 +function convertToNormalized(path: PathArray): PathArray { + let currentX = 0; + let currentY = 0; + + return path.map(segment => { + const [cmd, ...params] = segment; + switch (cmd) { + case 'H': + currentX = params[0]; + return ['L', currentX, currentY]; + case 'V': + currentY = params[0]; + return ['L', currentX, currentY]; + case 'S': + // 转换平滑曲线命令为完整的贝塞尔曲线 + // 实现转换逻辑... + break; + case 'T': + // 转换平滑二次贝塞尔曲线为标准格式 + // 实现转换逻辑... + break; + default: + if (params.length >= 2) { + [currentX, currentY] = params.slice(-2); + } + return segment; + } + }); +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径 | string \| PathArray | - | 要检查的路径 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 判断结果 | boolean | - | 是否为标准化路径 | + +- 标准命令列表 + +| 命令 | 说明 | 参数格式 | +|---------|------|------| +| A | 弧线 | rx,ry,angle,large-arc,sweep,x,y | +| C | 三次贝塞尔曲线 | x1,y1,x2,y2,x,y | +| L | 直线 | x,y | +| M | 移动 | x,y | +| Q | 二次贝塞尔曲线 | x1,y1,x,y | +| Z | 闭合路径 | 无参数 | + + +## isPathArray + +判断是否为有效的路径数组。 + +- 功能说明 + +- 验证数组是否为有效的路径数组 +- 检查每个段的命令和参数数量 +- 支持所有标准 SVG 路径命令 +- 提供类型保护功能 +- 验证路径格式的完整性 + +```ts +import { isPathArray } from '@antv/util'; + +// 基本使用 +const validPath: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['C', 150, 0, 200, 50, 200, 100] +]; +console.log(isPathArray(validPath)); // true + +const invalidPath = [ + ['M', 0], // 参数不足 + ['L', 100, 0], + ['X', 200, 0] // 无效命令 +]; +console.log(isPathArray(invalidPath)); // false + +// 实际应用示例 +function createPathValidator() { + return { + // 验证路径 + validatePath(path: any) { + if (!isPathArray(path)) { + return { + isValid: false, + errors: this.getValidationErrors(path) + }; + } + + return { + isValid: true, + commands: path.map(([cmd]) => cmd) + }; + }, + + // 获取验证错误 + getValidationErrors(path: any[]) { + const errors = []; + + if (!Array.isArray(path)) { + errors.push('Input must be an array'); + return errors; + } + + path.forEach((segment, index) => { + if (!Array.isArray(segment)) { + errors.push(`Segment ${index} must be an array`); + return; + } + + const [cmd] = segment; + const lowerCmd = cmd.toLowerCase(); + + if (!'achlmqstvz'.includes(lowerCmd)) { + errors.push(`Invalid command "${cmd}" at segment ${index}`); + } + + const expectedParams = paramsCount[lowerCmd]; + const actualParams = segment.length - 1; + + if (expectedParams !== actualParams) { + errors.push( + `Wrong number of parameters for "${cmd}" command at segment ${index}. ` + + `Expected ${expectedParams}, got ${actualParams}` + ); + } + }); + + return errors; + }, + + // 创建路径构建器 + createPathBuilder() { + const segments: PathArray = []; + + return { + moveTo(x: number, y: number) { + segments.push(['M', x, y]); + return this; + }, + + lineTo(x: number, y: number) { + segments.push(['L', x, y]); + return this; + }, + + curveTo(x1: number, y1: number, x2: number, y2: number, x: number, y: number) { + segments.push(['C', x1, y1, x2, y2, x, y]); + return this; + }, + + close() { + segments.push(['Z']); + return this; + }, + + build() { + if (!isPathArray(segments)) { + throw new Error('Invalid path construction'); + } + return segments; + } + }; + } + }; +} + +// 使用示例 +const validator = createPathValidator(); + +// 验证路径 +console.log(validator.validatePath(validPath)); +console.log(validator.validatePath(invalidPath)); + +// 使用路径构建器 +const builder = validator.createPathBuilder(); +const newPath = builder + .moveTo(0, 0) + .lineTo(100, 0) + .curveTo(150, 0, 200, 50, 200, 100) + .close() + .build(); + +console.log(isPathArray(newPath)); // true +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径 | string \| PathArray | - | 要验证的路径 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 判断结果 | boolean | - | 是否为有效路径数组 | + +- 支持的命令 + +| 命令 | 说明 | 参数数量 | 中文说明 | +|---------|------|------|---------| +| A/a | 弧线 | 7 | 绘制弧线 | +| C/c | 三次贝塞尔曲线 | 6 | 绘制三次贝塞尔曲线 | +| H/h | 水平线 | 1 | 绘制水平线 | +| L/l | 直线 | 2 | 绘制直线 | +| M/m | 移动 | 2 | 移动到点 | +| Q/q | 二次贝塞尔曲线 | 4 | 绘制二次贝塞尔曲线 | +| S/s | 平滑三次贝塞尔曲线 | 4 | 绘制平滑三次贝塞尔曲线 | +| T/t | 平滑二次贝塞尔曲线 | 2 | 绘制平滑二次贝塞尔曲线 | +| V/v | 垂直线 | 1 | 绘制垂直线 | +| Z/z | 闭合路径 | 0 | 闭合路径 | + + +## isPointInStroke + +判断点是否在路径的描边上。 + +- 功能说明 + +- 检测点是否位于路径的描边上 +- 支持字符串或数组格式的路径 +- 使用距离阈值进行判断 +- 高精度检测(默认阈值 0.001) +- 基于最近点距离计算 + +```ts +import { isPointInStroke } from '@antv/util'; + +// 基本使用 +const path: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100] +]; + +console.log(isPointInStroke(path, { x: 50, y: 0 })); // true +console.log(isPointInStroke(path, { x: 50, y: 1 })); // false + +// 实际应用示例 +function createPathInteractor() { + return { + // 创建路径检测器 + createDetector(path: PathArray, tolerance = 0.001) { + return { + // 检测点是否在路径上 + isOnPath(point: Point) { + return isPointInStroke(path, point); + }, + + // 获取路径上的点 + getPointsOnPath(points: Point[]) { + return points.filter(point => + isPointInStroke(path, point) + ); + }, + + // 创建点击区域检测 + createClickDetector(threshold = 5) { + return (event: MouseEvent) => { + const point = { + x: event.clientX, + y: event.clientY + }; + return this.isOnPath(point); + }; + } + }; + }, + + // 创建路径编辑器 + createPathEditor(path: PathArray) { + const detector = this.createDetector(path); + let selectedPoint: Point | null = null; + + return { + // 处理鼠标移动 + handleMouseMove(point: Point) { + if (detector.isOnPath(point)) { + // 高亮路径... + return true; + } + return false; + }, + + // 处理点击 + handleClick(point: Point) { + if (detector.isOnPath(point)) { + selectedPoint = point; + return true; + } + selectedPoint = null; + return false; + }, + + // 获取选中点 + getSelectedPoint() { + return selectedPoint; + } + }; + }, + + // 创建路径分析器 + createPathAnalyzer(path: PathArray, sampleCount = 100) { + return { + // 获取路径上的采样点 + getSamplePoints() { + const points: Point[] = []; + const length = getTotalLength(path); + const step = length / (sampleCount - 1); + + for (let i = 0; i <= length; i += step) { + const point = getPointAtLength(path, i); + points.push(point); + } + + return points; + }, + + // 检查点序列是否在路径上 + checkPointSequence(points: Point[]) { + return points.every(point => + isPointInStroke(path, point) + ); + } + }; + } + }; +} + +// 使用示例 +const interactor = createPathInteractor(); + +// 创建检测器 +const detector = interactor.createDetector(path); +console.log(detector.isOnPath({ x: 50, y: 0 })); + +// 创建编辑器 +const editor = interactor.createPathEditor(path); +editor.handleMouseMove({ x: 50, y: 0 }); + +// 创建分析器 +const analyzer = interactor.createPathAnalyzer(path); +const samplePoints = analyzer.getSamplePoints(); +console.log(analyzer.checkPointSequence(samplePoints)); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathInput | 路径 | string \| PathArray | - | 要检测的路径 | +| point | 检测点 | Point | - | 要检测的点坐标 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 判断结果 | boolean | - | 点是否在路径描边上 | + + +## midPoint + +计算两点之间的中间点或按比例插值点。 + +- 功能说明 + +- 计算两点之间的插值点 +- 支持任意比例的插值 +- 线性插值计算 +- 返回插值点坐标 +- 用于路径和动画计算 + +```ts +import { midPoint } from '@antv/util'; + +// 基本使用 +const point1 = [0, 0]; +const point2 = [100, 100]; + +console.log(midPoint(point1, point2, 0.5)); // [50, 50] 中点 +console.log(midPoint(point1, point2, 0.25)); // [25, 25] 四分之一点 +console.log(midPoint(point1, point2, 0.75)); // [75, 75] 四分之三点 + +// 实际应用示例 +function createInterpolator() { + return { + // 创建点插值器 + createPointInterpolator(start: number[], end: number[]) { + return { + // 获取插值点 + getPoint(t: number) { + return midPoint(start, end, Math.max(0, Math.min(1, t))); + }, + + // 获取多个插值点 + getPoints(count: number) { + return Array.from({ length: count }, (_, i) => + this.getPoint(i / (count - 1)) + ); + } + }; + }, + + // 创建路径插值器 + createPathInterpolator(points: number[][]) { + return { + // 获取路径上的点 + getPointAtPercent(percent: number) { + const t = Math.max(0, Math.min(1, percent)); + const totalSegments = points.length - 1; + const segment = Math.floor(t * totalSegments); + const segmentT = (t * totalSegments) % 1; + + return midPoint( + points[segment], + points[Math.min(segment + 1, points.length - 1)], + segmentT + ); + }, + + // 采样路径点 + samplePoints(count: number) { + return Array.from({ length: count }, (_, i) => + this.getPointAtPercent(i / (count - 1)) + ); + } + }; + }, + + // 创建动画生成器 + createAnimator(start: number[], end: number[], duration: number) { + const startTime = performance.now(); + + return { + // 获取当前动画帧 + getCurrentPoint() { + const elapsed = performance.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + return midPoint(start, end, progress); + }, + + // 启动动画 + animate(callback: (point: number[]) => void) { + const update = () => { + const point = this.getCurrentPoint(); + callback(point); + + if (performance.now() - startTime < duration) { + requestAnimationFrame(update); + } + }; + + requestAnimationFrame(update); + } + }; + } + }; +} + +// 使用示例 +const interpolator = createInterpolator(); + +// 点插值 +const pointInterp = interpolator.createPointInterpolator([0, 0], [100, 100]); +console.log(pointInterp.getPoints(5)); + +// 路径插值 +const pathPoints = [[0, 0], [50, 50], [100, 0], [150, 50]]; +const pathInterp = interpolator.createPathInterpolator(pathPoints); +console.log(pathInterp.samplePoints(10)); + +// 动画示例 +const animator = interpolator.createAnimator([0, 0], [100, 100], 1000); +animator.animate(point => { + console.log('Current position:', point); +}); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| a | 起始点 | number[] | - | 起始点坐标 | +| b | 终点 | number[] | - | 终点坐标 | +| t | 插值比例 | number | - | 0-1 之间的插值比例 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| point | 插值点 | number[] | - | 计算得到的插值点坐标 | + + +## pathLengthFactory + +路径长度计算工厂函数,用于计算路径的长度、指定距离的点位置和边界框。 + +- 功能说明 + +- 计算路径总长度 +- 获取指定距离处的点坐标 +- 计算路径的边界框 +- 支持所有标准路径命令 +- 处理复杂路径计算 + +```ts +import { pathLengthFactory } from '@antv/util'; + +// 基本使用 +const path: PathArray = [ + ['M', 0, 0], + ['L', 100, 0], + ['L', 100, 100] +]; + +console.log(pathLengthFactory(path)); +// { +// length: 200, // 总长度 +// point: { x: 0, y: 0 }, // 起点 +// min: { x: 0, y: 0 }, // 最小坐标 +// max: { x: 100, y: 100 } // 最大坐标 +// } + +// 获取特定距离的点 +console.log(pathLengthFactory(path, 150)); +// 返回距离起点150单位的点坐标 + +// 实际应用示例 +function createPathAnalyzer() { + return { + // 创建路径分析器 + analyzePath(path: PathArray) { + const info = pathLengthFactory(path); + + return { + // 获取路径信息 + getInfo() { + return info; + }, + + // 获取路径上的点 + getPointAt(distance: number) { + return pathLengthFactory(path, distance).point; + }, + + // 获取路径采样点 + getSamplePoints(count: number) { + const { length } = info; + const step = length / (count - 1); + + return Array.from({ length: count }, (_, i) => + pathLengthFactory(path, i * step).point + ); + }, + + // 创建动画控制器 + createAnimationController(duration: number) { + const { length } = info; + + return { + // 获取当前位置 + getPosition(time: number) { + const progress = Math.min(time / duration, 1); + return pathLengthFactory(path, length * progress).point; + }, + + // 创建动画 + animate(callback: (point: { x: number, y: number }) => void) { + const startTime = performance.now(); + + const update = (currentTime: number) => { + const elapsed = currentTime - startTime; + const point = this.getPosition(elapsed); + callback(point); + + if (elapsed < duration) { + requestAnimationFrame(update); + } + }; + + requestAnimationFrame(update); + } + }; + } + }; + }, + + // 创建路径变换器 + createPathTransformer(path: PathArray) { + const { min, max } = pathLengthFactory(path); + + return { + // 缩放到指定大小 + scaleToSize(width: number, height: number) { + const scaleX = width / (max.x - min.x); + const scaleY = height / (max.y - min.y); + + return this.transformPath(path, { + scale: Math.min(scaleX, scaleY), + translate: [-min.x, -min.y] + }); + } + }; + } + }; +} + +// 使用示例 +const analyzer = createPathAnalyzer(); +const pathAnalysis = analyzer.analyzePath(path); + +// 获取路径信息 +console.log(pathAnalysis.getInfo()); + +// 获取采样点 +console.log(pathAnalysis.getSamplePoints(10)); + +// 创建动画 +const animation = pathAnalysis.createAnimationController(1000); +animation.animate(point => { + console.log('Current position:', point); +}); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathInput | 路径 | string \| PathArray | - | 要分析的路径 | +| distance | 距离 | number | - | 可选的指定距离 | +| options | 配置选项 | Partial | - | 可选的配置参数 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 计算结果 | LengthFactory | - | 包含长度和边界信息的对象 | + +- 返回对象属性 + +| 属性 | 说明 | 类型 | 中文说明 | +|---------|------|------|---------| +| length | 总长度 | number | 路径的总长度 | +| point | 指定点 | {x: number, y: number} | 指定距离处的点 | +| min | 最小坐标 | {x: number, y: number} | 边界框最小坐标 | +| max | 最大坐标 | {x: number, y: number} | 边界框最大坐标 | + + +## rotateVector + +旋转二维向量,根据给定角度计算向量的新坐标。 + +- 功能说明 + +- 将二维向量绕原点旋转 +- 使用弧度表示旋转角度 +- 基于旋转矩阵计算 +- 返回新的坐标点 +- 保持向量长度不变 + +```ts +import { rotateVector } from '@antv/util'; + +// 基本使用 +console.log(rotateVector(1, 0, Math.PI / 2)); // { x: 0, y: 1 } // 90度 +console.log(rotateVector(1, 0, Math.PI)); // { x: -1, y: 0 } // 180度 +console.log(rotateVector(0, 1, -Math.PI / 2)); // { x: 1, y: 0 } // -90度 + +// 实际应用示例 +function createVectorUtil() { + return { + // 创建向量旋转器 + createRotator(x: number, y: number) { + return { + // 旋转指定角度 + rotate(angle: number) { + return rotateVector(x, y, angle); + }, + + // 获取多个角度的点 + getPoints(angles: number[]) { + return angles.map(angle => this.rotate(angle)); + }, + + // 创建均匀分布的点 + createEvenPoints(count: number) { + const step = (Math.PI * 2) / count; + return Array.from({ length: count }, (_, i) => + this.rotate(i * step) + ); + } + }; + }, + + // 创建动画旋转器 + createAnimatedRotator(x: number, y: number) { + return { + // 创建旋转动画 + animate(duration: number, rounds = 1) { + const startTime = performance.now(); + const totalAngle = Math.PI * 2 * rounds; + + return { + // 获取当前位置 + getCurrentPosition(currentTime: number) { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + return rotateVector(x, y, totalAngle * progress); + }, + + // 启动动画 + start(callback: (point: { x: number, y: number }) => void) { + const update = (time: number) => { + const position = this.getCurrentPosition(time); + callback(position); + + if (time - startTime < duration) { + requestAnimationFrame(update); + } + }; + + requestAnimationFrame(update); + } + }; + } + }; + }, + + // 创建形状生成器 + createShapeGenerator() { + return { + // 生成正多边形的点 + createRegularPolygon(radius: number, sides: number) { + const rotator = this.createRotator(radius, 0); + return rotator.createEvenPoints(sides); + }, + + // 生成星形的点 + createStar(outerRadius: number, innerRadius: number, points: number) { + const angleStep = Math.PI / points; + const allPoints = []; + + for (let i = 0; i < points * 2; i++) { + const radius = i % 2 === 0 ? outerRadius : innerRadius; + const angle = i * angleStep; + allPoints.push(rotateVector(radius, 0, angle)); + } + + return allPoints; + } + }; + } + }; +} + +// 使用示例 +const vectorUtil = createVectorUtil(); + +// 基本旋转 +const rotator = vectorUtil.createRotator(100, 0); +console.log(rotator.rotate(Math.PI / 4)); // 45度旋转 + +// 创建均匀分布的点 +console.log(rotator.createEvenPoints(6)); // 六边形的顶点 + +// 动画旋转 +const animator = vectorUtil.createAnimatedRotator(100, 0); +const animation = animator.animate(1000, 2); // 1秒转2圈 +animation.start(point => { + console.log('Current position:', point); +}); + +// 生成形状 +const shapeGenerator = vectorUtil.createShapeGenerator(); +console.log(shapeGenerator.createRegularPolygon(100, 5)); // 正五边形 +console.log(shapeGenerator.createStar(100, 50, 5)); // 五角星 +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| x | x 坐标 | number | - | 向量的 x 坐标 | +| y | y 坐标 | number | - | 向量的 y 坐标 | +| rad | 旋转角度 | number | - | 旋转的弧度值 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 旋转结果 | {x: number, y: number} | - | 旋转后的坐标点 | + + +## segmentArcFactory + +计算弧线段的长度、指定距离的点位置和边界框。 + +- 功能说明 + +- 计算弧线段的总长度 +- 获取弧线上指定距离的点 +- 计算弧线的边界框 +- 支持椭圆弧参数 +- 提供采样点精度控制 + +```ts +import { segmentArcFactory } from '@antv/util'; + +// 基本使用 +const arcInfo = segmentArcFactory( + 0, 0, // 起点 + 100, 50, // 半径 + 45, // 旋转角度 + 0, // 大弧标志 + 1, // 顺时针标志 + 100, 100, // 终点 + 0, // 距离 + { sampleSize: 30 } // 配置选项 +); + +console.log(arcInfo); +// { +// length: 数值, // 弧线长度 +// point: {x, y}, // 指定距离的点 +// min: {x, y}, // 最小坐标 +// max: {x, y} // 最大坐标 +// } + +// 实际应用示例 +function createArcUtil() { + return { + // 创建弧线分析器 + createArcAnalyzer( + start: [number, number], + radii: [number, number], + angle: number, + flags: [number, number], + end: [number, number] + ) { + return { + // 获取弧线信息 + getInfo(options = {}) { + return segmentArcFactory( + ...start, + ...radii, + angle, + ...flags, + ...end, + 0, + options + ); + }, + + // 获取采样点 + getSamplePoints(count: number) { + const { length } = this.getInfo(); + const step = length / (count - 1); + + return Array.from({ length: count }, (_, i) => + segmentArcFactory( + ...start, + ...radii, + angle, + ...flags, + ...end, + i * step, + { sampleSize: count } + ).point + ); + }, + + // 创建动画控制器 + createAnimationController(duration: number) { + const { length } = this.getInfo(); + + return { + // 获取当前位置 + getPosition(time: number) { + const progress = Math.min(time / duration, 1); + return segmentArcFactory( + ...start, + ...radii, + angle, + ...flags, + ...end, + length * progress, + { sampleSize: 60 } + ).point; + }, + + // 启动动画 + animate(callback: (point: { x: number, y: number }) => void) { + const startTime = performance.now(); + + const update = (currentTime: number) => { + const elapsed = currentTime - startTime; + const point = this.getPosition(elapsed); + callback(point); + + if (elapsed < duration) { + requestAnimationFrame(update); + } + }; + + requestAnimationFrame(update); + } + }; + } + }; + } + }; +} + +// 使用示例 +const arcUtil = createArcUtil(); + +// 创建弧线分析器 +const analyzer = arcUtil.createArcAnalyzer( + [0, 0], // 起点 + [100, 50], // 半径 + 45, // 角度 + [0, 1], // 标志 + [100, 100] // 终点 +); + +// 获取弧线信息 +console.log(analyzer.getInfo()); + +// 获取采样点 +console.log(analyzer.getSamplePoints(10)); + +// 创建动画 +const animation = analyzer.createAnimationController(1000); +animation.animate(point => { + console.log('Current position:', point); +}); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| X1, Y1 | 起点坐标 | number | - | 弧线起点 | +| RX, RY | 半径 | number | - | 椭圆半径 | +| angle | 旋转角度 | number | - | 椭圆旋转角度 | +| LAF | 大弧标志 | number | - | 是否选择大弧 | +| SF | 方向标志 | number | - | 是否顺时针 | +| X2, Y2 | 终点坐标 | number | - | 弧线终点 | +| distance | 距离 | number | - | 指定距离 | +| options | 配置选项 | Partial | - | 配置参数 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 计算结果 | LengthFactory | - | 包含长度和边界信息的对象 | + +- 配置选项 + +| 选项 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| bbox | 是否计算边界框 | boolean | true | 是否计算边界框 | +| length | 是否计算长度 | boolean | true | 是否计算长度 | +| sampleSize | 采样点数量 | number | 30 | 采样精度 | + + +## segmentCubicFactory + +计算三次贝塞尔曲线段的长度、指定距离的点位置和边界框。 + +- 功能说明 + +- 计算贝塞尔曲线段的总长度 +- 获取曲线上指定距离的点 +- 计算曲线的边界框 +- 支持采样点精度控制 +- 基于贝塞尔曲线公式计算 + +```ts +import { segmentCubicFactory } from '@antv/util'; + +// 基本使用 +const curveInfo = segmentCubicFactory( + 0, 0, // 起点 + 50, 0, // 控制点1 + 50, 100, // 控制点2 + 100, 100, // 终点 + 0, // 距离 + { sampleSize: 10 } // 配置选项 +); + +console.log(curveInfo); +// { +// length: 数值, // 曲线长度 +// point: {x, y}, // 指定距离的点 +// min: {x, y}, // 最小坐标 +// max: {x, y} // 最大坐标 +// } + +// 实际应用示例 +function createBezierUtil() { + return { + // 创建贝塞尔曲线分析器 + createCurveAnalyzer( + start: [number, number], + cp1: [number, number], + cp2: [number, number], + end: [number, number] + ) { + return { + // 获取曲线信息 + getInfo(options = {}) { + return segmentCubicFactory( + ...start, + ...cp1, + ...cp2, + ...end, + 0, + options + ); + }, + + // 获取采样点 + getSamplePoints(count: number) { + const { length } = this.getInfo(); + const step = length / (count - 1); + + return Array.from({ length: count }, (_, i) => + segmentCubicFactory( + ...start, + ...cp1, + ...cp2, + ...end, + i * step, + { sampleSize: count } + ).point + ); + }, + + // 创建动画控制器 + createAnimationController(duration: number) { + const { length } = this.getInfo(); + + return { + // 获取当前位置 + getPosition(time: number) { + const progress = Math.min(time / duration, 1); + return segmentCubicFactory( + ...start, + ...cp1, + ...cp2, + ...end, + length * progress, + { sampleSize: 30 } + ).point; + }, + + // 创建动画 + animate(callback: (point: { x: number, y: number }) => void) { + const startTime = performance.now(); + + const update = (currentTime: number) => { + const elapsed = currentTime - startTime; + const point = this.getPosition(elapsed); + callback(point); + + if (elapsed < duration) { + requestAnimationFrame(update); + } + }; + + requestAnimationFrame(update); + } + }; + } + }; + }, + + // 创建路径生成器 + createPathGenerator() { + return { + // 生成平滑曲线路径 + createSmoothPath(points: [number, number][], tension = 0.5) { + const result = []; + + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[Math.max(0, i - 1)]; + const p1 = points[i]; + const p2 = points[i + 1]; + const p3 = points[Math.min(points.length - 1, i + 2)]; + + const cp1 = [ + p1[0] + (p2[0] - p0[0]) * tension, + p1[1] + (p2[1] - p0[1]) * tension + ]; + + const cp2 = [ + p2[0] - (p3[0] - p1[0]) * tension, + p2[1] - (p3[1] - p1[1]) * tension + ]; + + result.push([p1, cp1, cp2, p2]); + } + + return result; + } + }; + } + }; +} + +// 使用示例 +const bezierUtil = createBezierUtil(); + +// 创建曲线分析器 +const analyzer = bezierUtil.createCurveAnalyzer( + [0, 0], // 起点 + [50, 0], // 控制点1 + [50, 100], // 控制点2 + [100, 100] // 终点 +); + +// 获取曲线信息 +console.log(analyzer.getInfo()); + +// 获取采样点 +console.log(analyzer.getSamplePoints(10)); + +// 创建动画 +const animation = analyzer.createAnimationController(1000); +animation.animate(point => { + console.log('Current position:', point); +}); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| x1, y1 | 起点坐标 | number | - | 曲线起点 | +| c1x, c1y | 控制点 1 | number | - | 第一控制点 | +| c2x, c2y | 控制点 2 | number | - | 第二控制点 | +| x2, y2 | 终点坐标 | number | - | 曲线终点 | +| distance | 距离 | number | - | 指定距离 | +| options | 配置选项 | Partial | - | 配置参数 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 计算结果 | LengthFactory | - | 包含长度和边界信息的对象 | + +- 配置选项 + +| 选项 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| bbox | 是否计算边界框 | boolean | true | 是否计算边界框 | +| length | 是否计算长度 | boolean | true | 是否计算长度 | +| sampleSize | 采样点数量 | number | 10 | 采样精度 | + + +## segmentLineFactory + +计算直线段的长度、指定距离的点位置和边界框。 + +- 功能说明 + +- 计算直线段的总长度 +- 获取线段上指定距离的点 +- 计算线段的边界框 +- 支持垂直线和水平线 +- 处理闭合路径(Z 命令) + +```ts +import { segmentLineFactory } from '@antv/util'; + +// 基本使用 +const lineInfo = segmentLineFactory( + 0, 0, // 起点 + 100, 100, // 终点 + 50 // 距离 +); + +console.log(lineInfo); +// { +// length: 141.42, // 线段长度 +// point: { x: 50, y: 50 }, // 指定距离的点 +// min: { x: 0, y: 0 }, // 最小坐标 +// max: { x: 100, y: 100 } // 最大坐标 +// } + +// 实际应用示例 +function createLineUtil() { + return { + // 创建线段分析器 + createLineAnalyzer(start: [number, number], end: [number, number]) { + return { + // 获取线段信息 + getInfo(distance?: number) { + return segmentLineFactory( + ...start, + ...end, + distance || 0 + ); + }, + + // 获取等分点 + getDivisionPoints(divisions: number) { + const { length } = this.getInfo(); + const step = length / divisions; + + return Array.from({ length: divisions + 1 }, (_, i) => + segmentLineFactory(...start, ...end, i * step).point + ); + }, + + // 检查点是否在线段上 + isPointOnLine(point: [number, number], tolerance = 0.1) { + const { length } = this.getInfo(); + const d1 = distanceSquareRoot(start, point); + const d2 = distanceSquareRoot(point, end); + + return Math.abs(d1 + d2 - length) <= tolerance; + } + }; + }, + + // 创建动画控制器 + createAnimationController( + start: [number, number], + end: [number, number], + duration: number + ) { + const { length } = segmentLineFactory(...start, ...end, 0); + + return { + // 获取当前位置 + getPosition(time: number) { + const progress = Math.min(time / duration, 1); + return segmentLineFactory( + ...start, + ...end, + length * progress + ).point; + }, + + // 创建动画 + animate(callback: (point: { x: number, y: number }) => void) { + const startTime = performance.now(); + + const update = (currentTime: number) => { + const elapsed = currentTime - startTime; + const point = this.getPosition(elapsed); + callback(point); + + if (elapsed < duration) { + requestAnimationFrame(update); + } + }; + + requestAnimationFrame(update); + } + }; + }, + + // 创建网格生成器 + createGridGenerator() { + return { + // 生成网格线 + generateGrid(width: number, height: number, cols: number, rows: number) { + const lines = []; + const dx = width / cols; + const dy = height / rows; + + // 垂直线 + for (let i = 0; i <= cols; i++) { + const x = i * dx; + lines.push(segmentLineFactory(x, 0, x, height, 0)); + } + + // 水平线 + for (let i = 0; i <= rows; i++) { + const y = i * dy; + lines.push(segmentLineFactory(0, y, width, y, 0)); + } + + return lines; + } + }; + } + }; +} + +// 使用示例 +const lineUtil = createLineUtil(); + +// 创建线段分析器 +const analyzer = lineUtil.createLineAnalyzer([0, 0], [100, 100]); +console.log(analyzer.getInfo()); +console.log(analyzer.getDivisionPoints(4)); +console.log(analyzer.isPointOnLine([50, 50])); + +// 创建动画 +const animation = lineUtil.createAnimationController( + [0, 0], + [100, 100], + 1000 +); +animation.animate(point => { + console.log('Current position:', point); +}); + +// 生成网格 +const gridGenerator = lineUtil.createGridGenerator(); +const grid = gridGenerator.generateGrid(200, 200, 4, 4); +console.log(grid); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| x1, y1 | 起点坐标 | number | - | 线段起点 | +| x2, y2 | 终点坐标 | number | - | 线段终点 | +| distance | 距离 | number | - | 指定距离 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 计算结果 | LengthFactory | - | 包含长度和边界信息的对象 | + + +## segmentQuadFactory + +计算二次贝塞尔曲线段的长度、指定距离的点位置和边界框。 + +- 功能说明 + +- 计算二次贝塞尔曲线的总长度 +- 获取曲线上指定距离的点 +- 计算曲线的边界框 +- 支持采样点精度控制 +- 基于二次贝塞尔曲线公式计算 + +```ts +import { segmentQuadFactory } from '@antv/util'; + +// 基本使用 +const curveInfo = segmentQuadFactory( + 0, 0, // 起点 + 50, 50, // 控制点 + 100, 0, // 终点 + 0, // 距离 + { sampleSize: 10 } // 配置选项 +); + +console.log(curveInfo); +// { +// length: 数值, // 曲线长度 +// point: {x, y}, // 指定距离的点 +// min: {x, y}, // 最小坐标 +// max: {x, y} // 最大坐标 +// } + +// 实际应用示例 +function createQuadBezierUtil() { + return { + // 创建曲线分析器 + createAnalyzer( + start: [number, number], + control: [number, number], + end: [number, number] + ) { + return { + // 获取曲线信息 + getInfo(options = {}) { + return segmentQuadFactory( + ...start, + ...control, + ...end, + 0, + options + ); + }, + + // 获取采样点 + getSamplePoints(count: number) { + const { length } = this.getInfo(); + const step = length / (count - 1); + + return Array.from({ length: count }, (_, i) => + segmentQuadFactory( + ...start, + ...control, + ...end, + i * step, + { sampleSize: count } + ).point + ); + }, + + // 创建动画控制器 + createAnimationController(duration: number) { + const { length } = this.getInfo(); + + return { + // 获取当前位置 + getPosition(time: number) { + const progress = Math.min(time / duration, 1); + return segmentQuadFactory( + ...start, + ...control, + ...end, + length * progress, + { sampleSize: 30 } + ).point; + }, + + // 创建动画 + animate(callback: (point: { x: number, y: number }) => void) { + const startTime = performance.now(); + + const update = (currentTime: number) => { + const elapsed = currentTime - startTime; + const point = this.getPosition(elapsed); + callback(point); + + if (elapsed < duration) { + requestAnimationFrame(update); + } + }; + + requestAnimationFrame(update); + } + }; + } + }; + }, + + // 创建路径生成器 + createPathGenerator() { + return { + // 生成平滑曲线 + createSmoothCurve(points: [number, number][], tension = 0.5) { + const result = []; + + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[i]; + const p1 = points[i + 1]; + const cp = [ + p0[0] + (p1[0] - p0[0]) * tension, + (p0[1] + p1[1]) / 2 + ]; + + result.push([p0, cp, p1]); + } + + return result; + } + }; + } + }; +} + +// 使用示例 +const bezierUtil = createQuadBezierUtil(); + +// 创建分析器 +const analyzer = bezierUtil.createAnalyzer( + [0, 0], // 起点 + [50, 50], // 控制点 + [100, 0] // 终点 +); + +// 获取曲线信息 +console.log(analyzer.getInfo()); + +// 获取采样点 +console.log(analyzer.getSamplePoints(10)); + +// 创建动画 +const animation = analyzer.createAnimationController(1000); +animation.animate(point => { + console.log('Current position:', point); +}); +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| x1, y1 | 起点坐标 | number | - | 曲线起点 | +| qx, qy | 控制点 | number | - | 控制点坐标 | +| x2, y2 | 终点坐标 | number | - | 曲线终点 | +| distance | 距离 | number | - | 指定距离 | +| options | 配置选项 | Partial | - | 配置参数 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 计算结果 | LengthFactory | - | 包含长度和边界信息的对象 | + +- 配置选项 + +| 选项 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| bbox | 是否计算边界框 | boolean | true | 是否计算边界框 | +| length | 是否计算长度 | boolean | true | 是否计算长度 | +| sampleSize | 采样点数量 | number | 10 | 采样精度 | + + +## path2Absolute + +将路径数组或路径字符串转换为绝对坐标路径数组。 + +- 功能说明 + +- 将相对路径命令转换为绝对路径命令 +- 支持所有 SVG 路径命令(M, L, H, V, C, S, Q, T, A, Z) +- 保持路径的形状不变 +- 跟踪当前绘制点的位置 +- 处理闭合路径命令(Z) + +```ts +import { path2Absolute } from '@antv/util'; + +// 基本路径转换 +const relativePath = 'm10,20 l30,40'; +console.log(path2Absolute(relativePath)); +// [['M', 10, 20], ['L', 40, 60]] + +// 复杂路径转换 +const complexPath = 'M10 10 h 80 v 80 h -80 Z'; +console.log(path2Absolute(complexPath)); +// [ +// ['M', 10, 10], +// ['H', 90], +// ['V', 90], +// ['H', 10], +// ['Z'] +// ] + +// 曲线路径转换 +const curvePath = 'm10,10 c10,20 30,40 50,60'; +console.log(path2Absolute(curvePath)); +// [ +// ['M', 10, 10], +// ['C', 20, 30, 40, 50, 60, 70] +// ] + +// 实际应用示例 +function normalizePath(pathString: string) { + const absolutePath = path2Absolute(pathString); + return absolutePath.map(segment => { + const [command, ...params] = segment; + return `${command}${params.join(',')}`; + }).join(' '); +} + +// 使用示例 +const path = 'm10,20 l30,40 h50 z'; +console.log(normalizePath(path)); +// "M10,20 L40,60 H90 Z" +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathInput | 路径输入 | string \| PathArray | - | SVG 路径字符串或路径数组 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 绝对路径数组 | AbsoluteArray | - | 转换后的绝对坐标路径数组 | + +- 支持的路径命令 + +| 命令 | 说明 | 相对命令 | 绝对命令 | +|---------|------|------|---------| +| M/m | 移动到 | m | M | +| L/l | 画线到 | l | L | +| H/h | 水平线到 | h | H | +| V/v | 垂直线到 | v | V | +| C/c | 三次贝塞尔曲线 | c | C | +| S/s | 平滑三次贝塞尔曲线 | s | S | +| Q/q | 二次贝塞尔曲线 | q | Q | +| T/t | 平滑二次贝塞尔曲线 | t | T | +| A/a | 弧线 | a | A | +| Z/z | 闭合路径 | z | Z | + +- 注意事项 + +1. 输入可以是字符串或路径数组 +2. 所有数值会被转换为数字类型 +3. 保持原始路径的形状不变 +4. 跟踪当前点位置以计算绝对坐标 +5. 处理路径闭合时返回到起始点 + + +## path2Array + +将 SVG 路径字符串转换为标准的路径数组格式。 + +- 功能说明 + +- 将 SVG 路径字符串解析为数组格式 +- 使用 parsePathString 进行解析 +- 返回标准化的路径数组 +- 便于路径数据的处理和操作 +- 类型安全的转换 + +```ts +import { path2Array } from '@antv/util'; + +// 基本路径转换 +console.log(path2Array('M10 10L20 20')); +// [['M', 10, 10], ['L', 20, 20]] + +// 复杂路径转换 +console.log(path2Array('M10,10 L20,20 H30 V40 Z')); +// [ +// ['M', 10, 10], +// ['L', 20, 20], +// ['H', 30], +// ['V', 40], +// ['Z'] +// ] + +// 曲线命令转换 +console.log(path2Array('M0,0 C10,10 20,10 30,0')); +// [ +// ['M', 0, 0], +// ['C', 10, 10, 20, 10, 30, 0] +// ] + +// 实际应用示例 +function analyzePath(pathString: string) { + const segments = path2Array(pathString); + + return { + startPoint: segments.find(seg => seg[0] === 'M')?.slice(1), + lines: segments.filter(seg => seg[0] === 'L'), + curves: segments.filter(seg => ['C', 'S', 'Q', 'T'].includes(seg[0])), + closed: segments.some(seg => seg[0] === 'Z') + }; +} + +// 使用示例 +const pathAnalysis = analyzePath('M10,10 L20,20 C30,30 40,40 50,50 Z'); +console.log(pathAnalysis); +// { +// startPoint: [10, 10], +// lines: [['L', 20, 20]], +// curves: [['C', 30, 30, 40, 40, 50, 50]], +// closed: true +// } + +// 路径变换示例 +function translatePath(pathString: string, dx: number, dy: number) { + return path2Array(pathString).map(segment => { + const [command, ...params] = segment; + if (command === 'H') { + return [command, params[0] + dx]; + } + if (command === 'V') { + return [command, params[0] + dy]; + } + return [ + command, + ...params.map((p, i) => p + (i % 2 ? dy : dx)) + ]; + }); +} + +// 平移路径 +console.log(translatePath('M10,10 L20,20', 5, 10)); +// [['M', 15, 20], ['L', 25, 30]] +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathInput | SVG 路径字符串 | string | - | 需要转换的 SVG 路径字符串 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 路径数组 | PathArray | - | 转换后的标准路径数组 | + +- 路径数组格式 + +```typescript +type PathArray = [string, ...number[]][]; +``` + +每个数组元素的格式: +- 第一个元素是命令字符串 +- 后续元素是该命令的参数值 + +- 注意事项 + +1. 输入必须是有效的 SVG 路径字符串 +2. 返回类型安全的路径数组 +3. 保持原始路径的完整性 +4. 支持所有标准 SVG 路径命令 +5. 便于后续处理和转换 + + +## path2Curve + +将 SVG 路径转换为三次贝塞尔曲线路径。 + +- 功能说明 + +- 将各种路径命令转换为三次贝塞尔曲线(C 命令) +- 保持路径的形状不变 +- 支持记录闭合路径(Z 命令)的位置 +- 处理弧线命令的特殊情况 +- 维护曲线的连续性 + +```ts +import { path2Curve } from '@antv/util'; + +// 基本路径转换 +console.log(path2Curve('M10 10L20 20')); +// [ +// ['M', 10, 10], +// ['C', 10, 10, 20, 20, 20, 20] +// ] + +// 复杂路径转换 +const arcPath = 'M0,0 A50,50 0 0,1 50,50'; +console.log(path2Curve(arcPath)); +// [ +// ['M', 0, 0], +// ['C', 27.614, 0, 50, 22.386, 50, 50] +// ] + +// 带闭合路径的转换 +const result = path2Curve('M0,0 L50,0 L50,50 Z', true); +if (Array.isArray(result)) { + const [curves, zIndexes] = result; + console.log(curves); + // [ + // ['M', 0, 0], + // ['C', 0, 0, 50, 0, 50, 0], + // ['C', 50, 0, 50, 50, 50, 50], + // ['Z'] + // ] + console.log(zIndexes); // [3] +} + +// 实际应用示例 +function createAnimatedPath(pathString: string) { + const curves = path2Curve(pathString) as CurveArray; + + return { + curves, + getPointAt(t: number) { + // 简单的点插值示例 + if (t <= 0) return curves[0].slice(-2); + if (t >= 1) return curves[curves.length - 1].slice(-2); + + const segmentIndex = Math.floor(t * (curves.length - 1)); + const segment = curves[segmentIndex]; + const localT = (t * (curves.length - 1)) % 1; + + // 贝塞尔曲线插值计算 + return getBezierPoint(segment.slice(1), localT); + } + }; +} + +// 使用示例 +const animator = createAnimatedPath('M0,0 L100,0 L100,100'); +console.log(animator.getPointAt(0)); // [0, 0] +console.log(animator.getPointAt(0.5)); // [100, 0] +console.log(animator.getPointAt(1)); // [100, 100] + +// 辅助函数:计算贝塞尔曲线上的点 +function getBezierPoint(points: number[], t: number) { + const [x1, y1, x2, y2, x3, y3, x4, y4] = points; + const t1 = 1 - t; + + return [ + t1 * t1 * t1 * x1 + 3 * t1 * t1 * t * x2 + 3 * t1 * t * t * x3 + t * t * t * x4, + t1 * t1 * t1 * y1 + 3 * t1 * t1 * t * y2 + 3 * t1 * t * t * y3 + t * t * t * y4 + ]; +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| pathInput | 路径输入 | string \| PathArray | - | SVG 路径字符串或路径数组 | +| needZCommandIndexes | 是否需要 Z 命令索引 | boolean | false | 是否返回闭合路径命令的位置 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 转换结果 | CurveArray \| [CurveArray, number[]] | - | 曲线数组或带 Z 命令索引的元组 | + +- 注意事项 + +1. 所有路径段都转换为 C 命令 +2. 保持路径的视觉效果不变 +3. 维护曲线的连续性 +4. 可选择保留 Z 命令位置 +5. 支持弧线命令的转换 + + +## path2String + +将路径数组转换为 SVG 路径字符串,支持数值精度控制。 + +- 功能说明 + +- 将路径数组转换为 SVG 路径字符串 +- 支持数值精度的四舍五入 +- 生成标准的 SVG path 'd' 属性值 +- 连接所有路径段 +- 优化输出格式 + +```ts +import { path2String } from '@antv/util'; + +// 基本路径转换 +const path1 = [ + ['M', 10, 10], + ['L', 20, 20], + ['Z'] +]; +console.log(path2String(path1)); // "M10 10L20 20Z" + +// 带精度控制的转换 +const path2 = [ + ['M', 10.123, 10.456], + ['L', 20.789, 20.987] +]; +console.log(path2String(path2, 2)); // "M10.12 10.46L20.79 20.99" + +// 曲线路径转换 +const path3 = [ + ['M', 0, 0], + ['C', 10, 10, 20, 20, 30, 30] +]; +console.log(path2String(path3)); // "M0 0C10 10 20 20 30 30" + +// 实际应用示例 +function createSVGPath(points: number[][], closed = false, precision = 2) { + const pathArray = [ + ['M', points[0][0], points[0][1]], + ...points.slice(1).map(point => ['L', point[0], point[1]]) + ]; + + if (closed) { + pathArray.push(['Z']); + } + + return path2String(pathArray, precision); +} + +// 使用示例 +const points = [ + [10.123, 20.456], + [30.789, 40.987], + [50.432, 60.765] +]; + +console.log(createSVGPath(points)); +// "M10.12 20.46L30.79 40.99L50.43 60.77" + +console.log(createSVGPath(points, true)); +// "M10.12 20.46L30.79 40.99L50.43 60.77Z" + +// SVG元素创建示例 +function createSVGElement(pathData: PathArray, precision = 2) { + const svgNS = "http://www.w3.org/2000/svg"; + const path = document.createElementNS(svgNS, "path"); + path.setAttribute("d", path2String(pathData, precision)); + return path; +} + +// 动态路径更新示例 +function animatePath(pathElement: SVGPathElement, newPath: PathArray) { + const currentPath = pathElement.getAttribute("d"); + pathElement.setAttribute("d", path2String(newPath)); +} +``` + +- 参数说明 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| path | 路径数组 | PathArray | - | 要转换的路径数组 | +| round | 精度控制 | number \| 'off' | 'off' | 数值精度(小数位数)或关闭四舍五入 | + +- 返回值 + +| 参数 | 说明 | 类型 | 默认值 | 中文说明 | +|---------|------|------|---------|----------| +| result | 路径字符串 | string | - | SVG 路径字符串(d 属性值) | + +- 注意事项 + +1. 输入必须是有效的路径数组 +2. 精度控制影响所有数值 +3. 'off' 表示不进行四舍五入 +4. 生成的字符串符合 SVG 标准 +5. 命令和参数之间使用空格分隔 + + +## 常量 + +```ts +// SVG 路径命令的参数数量常量对象 +const paramsCount = { + a: 7, + c: 6, + h: 1, + l: 2, + m: 2, + r: 4, + q: 4, + s: 4, + t: 2, + v: 1, + z: 0, +}; + +// 路径参数解析器的初始状态对象 +const paramsParser = { + x1: 0, + y1: 0, + x2: 0, + y2: 0, + x: 0, + y: 0, + qx: null, + qy: null, +} +``` \ No newline at end of file diff --git a/package.json b/package.json index 3c600ad..eddeec6 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "clean": "rimraf lib esm dist", "lint-staged": "lint-staged", "size": "limit-size", - "lint": "eslint ./src ./__tests__ && prettier ./src ./__tests__ --check ", - "fix": "eslint ./src ./__tests__ --fix && prettier ./src ./__tests__ --write ", + "lint": "eslint ./src ./__tests__ && prettier ./src ./__tests__ --check && lint-md docs/**/* README.md", + "fix": "eslint ./src ./__tests__ --fix && prettier ./src ./__tests__ --write && lint-md docs/**/* README.md --fix", "test": "jest", "build:umd": "rimraf ./dist && rollup -c && npm run size", "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib", @@ -36,6 +36,7 @@ "@antv/path-util": "^2.0.15", "@commitlint/cli": "^18.6.1", "@commitlint/config-conventional": "^18.6.3", + "@lint-md/cli": "^2.0.0", "@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", diff --git a/src/color/tocssgradient.ts b/src/color/tocssgradient.ts index 3cd5f6f..80e2e2c 100644 --- a/src/color/tocssgradient.ts +++ b/src/color/tocssgradient.ts @@ -6,6 +6,9 @@ function isGradientColor(val) { return /^[r,R,L,l]{1}[\s]*\(/.test(val); } +/** + * 将 g 渐变转换为 css 渐变 + */ export function toCSSGradient(gradientColor) { if (isGradientColor(gradientColor)) { let cssColor; diff --git a/src/path/convert/path-2-absolute.ts b/src/path/convert/path-2-absolute.ts index 1981990..9699bd1 100644 --- a/src/path/convert/path-2-absolute.ts +++ b/src/path/convert/path-2-absolute.ts @@ -2,6 +2,9 @@ import { isAbsoluteArray } from '../util/is-absolute-array'; import { parsePathString } from '../parser/parse-path-string'; import type { PathArray, AbsoluteArray, AbsoluteSegment } from '../types'; +/** + * Converts a `PathArray` to an `AbsoluteArray`. + */ export function path2Absolute(pathInput: string | PathArray): AbsoluteArray { if (isAbsoluteArray(pathInput)) { return [].concat(pathInput) as AbsoluteArray; diff --git a/tsconfig.json b/tsconfig.json index 513580c..dc7bbd7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,5 @@ "baseUrl": "src" }, "include": ["src/**/*"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "docs"] }