@dcg-overseas/cartesian
v0.1.4
Published
Cartesian axes & math↔canvas coordinate transform for react-konva
Readme
@dcg-overseas/cartesian
为 react-konva 提供的笛卡尔坐标系基础组件:坐标轴渲染、数学坐标↔画布像素双向变换,以及一套可选的画板状态机(点 / 线段 / 自由笔触 / 选中 / 橡皮擦 / 撤销)。
包负责绘图的操作逻辑;消费者负责工具栏 UI、调色板、形状选择。颜色和形状作为受控 props 传入。
安装
pnpm add @dcg-overseas/cartesian konva react-konvareact、react-dom、konva、react-konva 是 peer 依赖(>=18)。
两种使用模式
包支持两种模式,按需选择。
模式 A — 完整画板(推荐)
用 <CartesianProvider> + <CartesianBoard> 得到一个可交互的完整坐标平面:坐标轴、绘图、选中、历史撤销,全部内置。消费者只需提供工具栏 UI 和调色板。
模式 B — 仅坐标轴(headless)
用 <CartesianAxes> + useCartesianTransform()。只要坐标轴显示和坐标变换,其他内容你自己在 Stage 里画。
模式 A:完整画板
快速示例
import { useState } from 'react'
import {
CartesianProvider,
CartesianBoard,
useCartesianContext,
} from '@dcg-overseas/cartesian'
import type { ToolMode, PointShape } from '@dcg-overseas/cartesian'
function MyPlane() {
const [tool, setTool] = useState<ToolMode>('select')
const [pointShape, setPointShape] = useState<PointShape>('circle')
const [color, setColor] = useState('#ef4444')
return (
<CartesianProvider
grid="10x10"
tool={tool}
pointShape={pointShape}
color={color}
>
{/* The Board fills its parent — wrap it in a sized container */}
<div style={{ width: 420, height: 420 }}>
<CartesianBoard />
</div>
<MyToolbar onToolChange={setTool} />
</CartesianProvider>
)
}
function MyToolbar({ onToolChange }: { onToolChange: (t: ToolMode) => void }) {
const { canDelete, canUndo, deleteSelected, undo, reset } = useCartesianContext()
return (
<div>
<button onClick={() => onToolChange('point')}>点</button>
<button onClick={() => onToolChange('pen')}>笔</button>
<button onClick={() => onToolChange('line')}>直线</button>
<button onClick={() => onToolChange('eraser')}>橡皮擦</button>
<button onClick={deleteSelected} disabled={!canDelete}>删除</button>
<button onClick={undo} disabled={!canUndo}>撤销</button>
<button onClick={reset}>重置</button>
</div>
)
}工具行为
| 工具 | 行为 |
|---|---|
| select | 点击元素切换选中态(支持多选)。点空白处取消所有选中。 |
| point | 点击落点。光标跟随显示半透明 ghost 预览。使用 pointShape + color。 |
| pen | 按下-拖动-松开画自由笔触(自带平滑)。使用 color。 |
| line | 第一次点击落起点,第二次落终点。期间显示虚线预览。使用 color。 |
| eraser | 悬停元素时高亮,点击删除。 |
切换工具会自动清理进行中的状态(选中、line 锚点、未结束的笔触)。
<CartesianProvider> props
| Prop | 类型 | 必填 | 说明 |
|---|---|---|---|
| grid | GridKey \| GridSpec | ✓ | 坐标范围。见下方 grid。 |
| tool | ToolMode | ✓ | 当前激活工具。消费者受控。 |
| pointShape | PointShape | ✓ | 添加新点时使用的形状。 |
| color | string | ✓ | 新元素的十六进制颜色。 |
| axesTheme | CartesianTheme | | 覆盖坐标轴外观,见 坐标轴主题。 |
| boardTheme | CartesianBoardTheme | | 覆盖画板外观,见 画板主题。 |
| children | ReactNode | ✓ | 任何放在 context 内的内容(board + 工具栏)。 |
Provider 没有
width/heightprops。<CartesianBoard>会自动填满它的父容器,由父容器的 CSS(px / %、flex、grid、vw 等)决定画布尺寸。
<CartesianBoard> props
无。组件从 context 读取所有状态。它会填满最近的父容器,所以你需要给父容器一个明确的尺寸:
<div style={{ width: 420, height: 420 }}>
<CartesianBoard />
</div>
// 或响应式
<div style={{ flex: 1, height: '60vh' }}>
<CartesianBoard />
</div>useCartesianContext() 返回值
<CartesianProvider> 任何后代组件都能读取的只读状态 + 操作函数。
{
// 身份信息(透传 props)
width, height, tool, pointShape, color,
transform: CartesianTransform,
axesTheme?: CartesianTheme,
boardTheme: Required<CartesianBoardTheme>,
// 只读状态
elements: DrawnElement[],
selectedIds: ReadonlySet<string>,
hoveredId: string | null,
lineStart: { mx, my } | null, // line 工具的第一次点击锚点
pointerMath: { mx, my } | null, // 当前光标的数学坐标
activeStroke: number[] | null, // 进行中笔触的画布坐标点列
// 操作(在你的工具栏里调用)
canDelete: boolean,
canUndo: boolean,
deleteSelected: () => void,
undo: () => void,
reset: () => void,
// 内部 — 由 <CartesianBoard> 使用,消费者通常不需要
onStageMouseDown, onStageMouseMove, onStageMouseUp,
}绘图模型
type ToolMode = 'select' | 'point' | 'pen' | 'line' | 'eraser'
type PointShape = 'circle' | 'triangle' | 'square' | 'star'
type DrawnPoint = { id, type: 'point'; mx, my, shape, color }
type DrawnStroke = { id, type: 'stroke'; points: number[]; color } // [mx, my, mx, my, ...]
type DrawnLine = { id, type: 'line'; x1, y1, x2, y2, color }
type DrawnElement = DrawnPoint | DrawnStroke | DrawnLine所有数学坐标(mx, my, x1...)是数学空间值(即网格的 min..max 范围),不是画布像素。包内部通过 transform 自动换算。
模式 B:仅坐标轴
快速示例
import { Stage, Layer } from 'react-konva'
import { CartesianAxes, useCartesianTransform } from '@dcg-overseas/cartesian'
function MyChart() {
const W = 420, H = 420
const t = useCartesianTransform({ width: W, height: H, grid: '10x10' })
return (
<Stage width={W} height={H}>
<Layer listening={false}>
<CartesianAxes width={W} height={H} grid="10x10" />
</Layer>
<Layer>
{/* 你自己的元素,用 t.toCanvasX / t.toCanvasY 定位 */}
</Layer>
</Stage>
)
}<CartesianAxes width height grid theme?>
输出一个 Konva <Group>,包含网格线、刻度数字、O / x / y 轴标记。放在任意 <Layer> 内。
useCartesianTransform({ width, height, grid })
返回 memoized 的 CartesianTransform:
{
min, max, step, width, height,
ticks: number[], // 预计算,浮点安全
toCanvasX: (mx) => number,
toCanvasY: (my) => number,
toMathX: (cx) => number,
toMathY: (cy) => number,
}memo 依赖被拆成原子(width / height / min / max / step),所以即使每次都传字面量 { min, max, step },也不会让 transform 反复重建。
createCartesianTransform({ width, height, grid })
形状一致的纯函数 —— 不带 React,不调 useMemo。width、height、range、step 非正时会 throw。可在 worker、测试、React 树外的地方使用。
grid prop
可以是内置预设 key,也可以是自定义 spec。
// 内置预设(GRID_OPTIONS 列出所有 key)
grid="10x10" // -5..5, step 1
grid="20x20" // -10..10, step 2
grid="30x30" // -15..15, step 5
// 自定义 — 支持小数 step(不会丢末端刻度)
grid={{ min: 0, max: 1, step: 0.1 }}GRID_OPTIONS
导出的预设数组 { key, label, min, max, step }[],用它渲染网格选择器。
resolveGrid(grid)
辅助函数:把 GridKey | GridSpec 解析为纯 { min, max, step }。未知 key 时 throw。
主题定制
坐标轴主题 (CartesianTheme)
<CartesianProvider
axesTheme={{
// 轴颜色 — 优先级见下方
axisColor: '#0ea5e9', // 两条轴的统一 fallback
xAxisColor: '#3b82f6', // 仅 x 轴(水平零线),覆盖 axisColor
yAxisColor: '#ef4444', // 仅 y 轴(垂直零线),覆盖 axisColor
// 其他样式
gridColor: '#e5e7eb',
labelColor: '#374151',
fontSize: 9, // 刻度数字字号
axisLabelFontSize: 11, // O / x / y 标签字号
axisStrokeWidth: 1.5,
gridStrokeWidth: 1,
}}
...
>颜色优先级:xAxisColor || axisColor || 默认值(yAxisColor 同理)。两轴想同色,只设 axisColor;想分别上色,设 xAxisColor / yAxisColor。
画板主题 (CartesianBoardTheme)
<CartesianProvider
boardTheme={{
background: '#fff', // 画布底色
selectionColor: '#f97316', // 选中态描边色
hoverColor: '#fbbf24', // 橡皮擦悬停描边色
pointSize: 8, // 点的画布像素大小
}}
...
>导出清单
| 名称 | 类型 | 用途 |
|---|---|---|
| CartesianProvider | 组件 | 持有绘图状态,注入 context。 |
| CartesianBoard | 组件 | 渲染 Stage + 坐标轴 + 绘制元素 + 预览。 |
| CartesianAxes | 组件 | 仅坐标轴 (Konva Group),headless 模式用。 |
| PointShapeNode | 组件 | 单个 PointShape 的 Konva 节点;消费者可在缩略图/工具栏预览中复用。 |
| useCartesianTransform | Hook | memoized 数学↔画布坐标变换。 |
| useCartesianContext | Hook | 在 <CartesianProvider> 内读取状态和操作。 |
| createCartesianTransform | 函数 | CartesianTransform 的纯函数工厂。 |
| resolveGrid | 函数 | GridKey \| GridSpec → GridSpec。 |
| GRID_OPTIONS | 常量 | 内置预设列表。 |
类型
GridKey、GridSpec、GridConfig、CartesianTransform、CartesianTheme、CartesianAxesProps、CartesianBoardTheme、CartesianProviderProps、CartesianContextValue、ToolMode、PointShape、DrawnPoint、DrawnStroke、DrawnLine、DrawnElement。
关于响应式尺寸
Konva 渲染到 <canvas> 时使用确切的像素尺寸 —— 没有 SVG viewBox 那种等比缩放。
完整画板模式(Provider + Board)
<CartesianBoard> 内部用 ResizeObserver 测自身外层 div,并自动把测得的物理像素同步给 Provider。消费者只需要给 Board 的父容器一个 CSS 尺寸(px / rem / %、flex 子项、grid 单元、vw / vh 等都行),剩下的不用管:
<div style={{ flex: 1, height: '60vh' }}>
<CartesianBoard />
</div>Headless 模式(CartesianAxes)
仅坐标轴的 headless 用法不带自动测量 —— 你自己决定 <Stage> 的 width / height,传给 <CartesianAxes> 就行。
关于 postcss-pxtorem / postcss-px-to-viewport
包不引入任何 CSS,也不输出带 px 的 inline 样式(width: '100%'、height: '100%'、touchAction: 'none' 都不是 px),所以消费者侧的 px 转换插件完全不用关心这个包 —— 它们扫不到也无须转换。
License
MIT
