基于 Canvas 的简单画图组件,让你用类似于 DOM 的方式,在 Canvas 上画图。
- 🎨 简单易用 - 类似 DOM 的 API 设计,学习成本低
- 🚀 高性能 - 基于 Canvas 原生渲染,支持大量图形
- 📱 跨平台 - 支持浏览器、Node.js 和微信小程序
- 🎯 丰富图形 - 内置矩形、圆形、线条、箭头、贝塞尔曲线等常用图形
- 🎭 事件系统 - 完整的鼠标和触摸事件支持
- 🔧 可扩展 - 支持自定义图形控件
- 🌈 样式丰富 - 支持渐变、阴影、透明度、滤镜、虚线、混合模式等样式
- 🖼️ 边框系统 - 完整的 border 支持(宽度/样式/颜色),四角独立圆角
- ✂️ 裁剪遮罩 - 支持 clipPath 裁剪路径和 mask 遮罩效果
- 📐 图层管理 - 支持多图层操作,包括创建、切换、删除图层
- 🔍 缩放平移 - 支持画布缩放和平移操作
- 📤 导出功能 - 支持导出为 PNG、JPEG 和 SVG 格式
- 📝 文本换行 - 支持文本自动换行显示
npm install jmgraphyarn add jmgraph直接下载 dist/jmgraph.min.js 并在 HTML 中引用:
<script type="text/javascript" src="../dist/jmgraph.min.js"></script>注意:
g.createShape()创建图形后会自动添加到当前活动图层,无需手动调用g.children.add()。若设置了autoRefresh: true(默认值),画布会自动刷新,也无需手动调用g.redraw()。
<script type="module">
import jmGraph from "jmgraph";
const container = document.getElementById('mycanvas_container');
const g = jmGraph(container, {
width: 800,
height: 600,
autoRefresh: true,
style: {
fill: '#000'
}
});
</script>const jmGraph = require('jmgraph');
const g = jmGraph.create('mycanvas_container', {
width: 800,
height: 600,
style: {
fill: '#000'
}
});const style = {
stroke: '#46BF86',
lineWidth: 2,
shadow: '0,0,10,#fff'
};
const rect = g.createShape('rect', {
style: style,
position: {x: 100, y: 100},
width: 100,
height: 100
});
// createShape 会自动将图形添加到当前活动图层,无需手动 g.children.add()
// 如果 autoRefresh 为 true(默认),也不需要手动调用 g.redraw()jmGraph 支持简化的样式名称和原生 Canvas 样式:
| 简化名称 | 原生名称 | 说明 |
|---|---|---|
| fill | fillStyle | 填充颜色、渐变或模式 |
| stroke | strokeStyle | 描边颜色、渐变或模式 |
| shadow | - | 阴影,格式:'0,0,10,#fff' |
| shadow.blur | shadowBlur | 阴影模糊级别 |
| shadow.x | shadowOffsetX | 阴影水平偏移 |
| shadow.y | shadowOffsetY | 阴影垂直偏移 |
| shadow.color | shadowColor | 阴影颜色 |
| lineWidth | lineWidth | 线条宽度 |
| miterLimit | miterLimit | 最大斜接长度 |
| font | font | 字体 |
| fontSize | font | 字体大小 |
| fontFamily | font | 字体名称 |
| opacity | globalAlpha | 透明度 |
| textAlign | textAlign | 文本水平对齐 |
| textBaseline | textBaseline | 文本垂直对齐 |
| lineJoin | lineJoin | 线条连接样式 |
| lineCap | lineCap | 线条端点样式 |
| maxWidth | maxWidth | 文本最大宽度(用于自动换行) |
| lineDash | - | 自定义虚线模式,数组或字符串格式,如 [10, 5] 或 '10,5' |
| lineDashOffset | lineDashOffset | 虚线偏移量 |
| filter | filter | CSS 滤镜效果,如 'blur(3px) grayscale(50%)' 或对象 { blur: 3, brightness: 1.2 } |
| globalCompositeOperation | globalCompositeOperation | 混合模式,如 multiply、screen、overlay |
| border | - | 边框系统,对象 { width, style, color } 或字符串 '2px solid #ff0000' |
| clipPath | - | 裁剪路径,传入图形控件实例 |
| mask | - | 遮罩效果,传入图形控件实例 |
jmGraph 支持完整的 CSS 渐变语法,通过 jmGradient 类实现。支持线性渐变和径向渐变。
// 1. 角度格式(deg/rad/grad/turn)
'linear-gradient(180deg, #8b5cf6 0%, #6366f1 50%, #4f46e5 100%)'
'linear-gradient(0.5turn, #10b981, #3b82f6)'
'linear-gradient(3.14159rad, #f59e0b, #ef4444)'
// 2. 方向关键词
'linear-gradient(to top, #e94560, #00d4ff)'
'linear-gradient(to right, #ffd93d, #e94560)'
'linear-gradient(to top right, #8b5cf6, #f59e0b)'
// 3. 坐标格式(x1 y1 x2 y2)—— 注意:坐标之间用空格分隔,不要用逗号
'linear-gradient(50% 0 50% 100%, rgba(36,159,218,0) 1, rgba(36,159,218,0.8) 0)'
// 4. 径向渐变
'radial-gradient(circle, #e94560, #8b5cf6)'
'radial-gradient(ellipse at top, #06b6d4, #8b5cf6)'
'radial-gradient(50% 50% 100% 50% 50% 0%, #ffd93d 0%, #f59e0b 100%)'// 方式一:直接使用字符串(最简单)
const rect = g.createShape('rect', {
position: {x: 100, y: 100}, width: 200, height: 100,
style: { fill: 'linear-gradient(180deg, #e94560 0%, #00d4ff 100%)' }
});
// 方式二:使用 jmGradient 对象
import { jmGradient } from 'jmgraph';
const gradient = new jmGradient({
type: 'linear',
x1: '50%', y1: '0%',
x2: '50%', y2: '100%',
stops: [
{ offset: 0, color: 'rgba(36,159,218,0)' },
{ offset: 1, color: 'rgba(36,159,218,0.8)' }
]
});
const rect2 = g.createShape('rect', {
position: {x: 100, y: 220}, width: 200, height: 100,
style: { fill: gradient }
});
// 方式三:使用 createLinearGradient / createRadialGradient 便捷方法
const linearGradient = g.createLinearGradient(0, 0, 0, 100);
linearGradient.addStop(0, '#e94560');
linearGradient.addStop(1, '#00d4ff');
const radialGradient = g.createRadialGradient(100, 100, 0, 100, 100, 50);
radialGradient.addStop(0, '#ffd93d');
radialGradient.addStop(1, '#f59e0b');偏移量(offset)表示颜色在渐变中的位置,支持三种写法:
// 小数(0~1)—— 0 表示渐变起点,1 表示渐变终点
'linear-gradient(180deg, #e94560 0, #00d4ff 0.5, #8b5cf6 1)'
// 百分比(0%~100%)
'linear-gradient(180deg, #e94560 0%, #00d4ff 50%, #8b5cf6 100%)'
// 省略偏移量(首尾自动为 0 和 1,中间均匀分布)
'linear-gradient(180deg, #e94560, #00d4ff, #8b5cf6)'
// 等价于 'linear-gradient(180deg, #e94560 0, #00d4ff 0.5, #8b5cf6 1)'注意:偏移量
1和100%是等价的,都表示渐变终点。偏移量会自动归一化到 0~1 范围。
支持多种颜色格式:
// hex
'linear-gradient(180deg, #e94560 0%, #00d4ff 100%)'
// rgba —— 逗号后有无空格均可
'linear-gradient(180deg, rgba(233,69,96,0.8) 0%, rgba(0,212,255,0.8) 100%)'
'linear-gradient(180deg, rgba(233, 69, 96, 0.8) 0%, rgba(0, 212, 255, 0.8) 100%)'
// hsl/hsla
'linear-gradient(180deg, hsl(345, 82%, 62%) 0%, hsl(191, 100%, 50%) 100%)'
// 命名颜色
'linear-gradient(to top, red, blue, green)'
// transparent
'linear-gradient(180deg, transparent, rgba(233,69,96,0.7))'坐标参数支持多种形式:
// 百分比(推荐)—— 相对于控件边界尺寸计算
x1: '50%' // 控件宽度的一半
// 小数(0~1)—— 自动乘以控件尺寸,效果等同于百分比
x1: 0.5 // 同 '50%'
// 绝对像素值
x1: 100 // 固定 100 像素位置点击展开常见问题
- 坐标分隔符错误:坐标格式中坐标之间用空格分隔,不要用逗号
// 正确
'linear-gradient(50% 0 50% 100%, rgba(36,159,218,0) 1, rgba(36,159,218,0.8) 0)'
// 错误 - 坐标之间不能有逗号
'linear-gradient(50%, 0, 50%, 100%, rgba(36,159,218,0) 1, ...)'- 颜色停止点之间必须有逗号:颜色停止点用逗号分隔
// 正确
'linear-gradient(180deg, #e94560 0%, #00d4ff 100%)'
// 错误 - 颜色停止点之间缺少逗号
'linear-gradient(180deg #e94560 0% #00d4ff 100%)'- rgba 透明度为 0 时必须写完整:不能只写
rgba(r,g,b, 0)而不写颜色值
// 正确
'linear-gradient(50% 0 50% 100%, rgba(36,159,218,0) 1, rgba(36,159,218,0.8) 0)'
// rgba 中透明度参数是最后一个值,逗号后可以有空格
'rgba(36,159,218, 0)' // 正确
'rgba(36,159,218,0)' // 正确
'rgba(36, 159, 218, 0)' // 正确- 渐变至少需要 2 个颜色停止点
// 正确 - 至少 2 个颜色
'linear-gradient(180deg, #e94560 0%, #00d4ff 100%)'
// 错误 - 只有 1 个颜色,无法产生渐变效果
'linear-gradient(180deg, #e94560)'- 多行渐变字符串:支持换行符
// 正确 - 多行字符串也能正常解析
`linear-gradient(50% 0 50% 100%,
rgba(36,159,218,0) 1,
rgba(36,159,218,0.8) 0)`- addStop 偏移量范围:使用
addStop()方法时,偏移量必须在 0~1 之间
// 正确
gradient.addStop(0, '#e94560');
gradient.addStop(0.5, '#00d4ff');
gradient.addStop(1, '#8b5cf6');
// 错误 - 偏移量超出 0~1 范围会被自动裁剪并输出警告
gradient.addStop(1.5, '#e94560'); // 会被调整为 1
gradient.addStop(-0.5, '#e94560'); // 会被调整为 0支持 CSS 标准滤镜,可用值包括:
| 滤镜 | 说明 | 示例 |
|---|---|---|
| blur | 模糊 | 'blur(3px)' |
| grayscale | 灰度 (0-1) | 'grayscale(100%)' |
| sepia | 怀旧 (0-1) | 'sepia(80%)' |
| brightness | 亮度 (数值) | 'brightness(1.5)' |
| contrast | 对比度 (数值) | 'contrast(2)' |
| saturate | 饱和度 (数值) | 'saturate(1.5)' |
| hue-rotate | 色相旋转 (deg) | 'hue-rotate(90deg)' |
| invert | 反转 (0-1) | 'invert(100%)' |
| opacity | 不透明度 (0-1) | 'opacity(0.5)' |
支持字符串格式、对象格式或 jmFilter 实例:
// 字符串格式(多个滤镜组合)
style: { fill: '#e94560', filter: 'blur(1px) brightness(1.2) saturate(1.5)' }
// 对象格式
style: { fill: '#00d4ff', filter: { blur: 3, grayscale: 0.5 } }
// 使用 jmFilter 类
import { jmFilter } from 'jmgraph';
const f = new jmFilter({ blur: 2, brightness: 1.3 });
style: { fill: '#ffd93d', filter: f }通过 lineDash 定义自定义虚线模式,替代原有的 lineType: 'dotted':
// 等间距虚线
style: { stroke: '#00d4ff', lineWidth: 2, lineDash: [10, 5] }
// 字符串格式
style: { stroke: '#ff6b6b', lineWidth: 2, lineDash: '10, 5, 2, 5' }
// 带偏移量
style: { stroke: '#ffd93d', lineWidth: 2, lineDash: [10, 10], lineDashOffset: 5 }radius 属性支持数字(四角相同)和对象格式(四角独立):
// 统一圆角(向后兼容)
g.createShape('rect', { position: {x: 20, y: 20}, width: 200, height: 80, radius: 20,
style: { fill: '#e94560' }
});
// 四角独立圆角
g.createShape('rect', { position: {x: 20, y: 130}, width: 200, height: 80,
radius: { topLeft: 30, topRight: 5, bottomRight: 30, bottomLeft: 5 },
style: { fill: '#00d4ff' }
});
// 通过 style.borderRadius 设置
g.createShape('rect', { position: {x: 20, y: 240}, width: 200, height: 80,
style: { fill: '#00ff88', borderRadius: { topLeft: 40, topRight: 0, bottomRight: 0, bottomLeft: 40 } }
});支持 Canvas 标准混合模式:
// multiply 混合
g.createShape('circle', { center: {x: 120, y: 120}, radius: 60, style: { fill: '#e94560' } });
g.createShape('circle', { center: {x: 170, y: 120}, radius: 60,
style: { fill: '#00d4ff', globalCompositeOperation: 'multiply' }
});传入一个图形控件实例作为裁剪区域:
// 创建裁剪区域(圆形)
const clipCircle = g.createShape('circle', {
center: {x: 300, y: 200}, radius: 80,
style: { close: true }
});
clipCircle.initPoints();
// 被裁剪的矩形,只在圆形区域内可见
g.createShape('rect', {
position: {x: 180, y: 120}, width: 240, height: 160, radius: 12,
style: {
fill: 'linear-gradient(0 0 240 160, #e94560 0, #00d4ff 1)',
clipPath: clipCircle
}
});const rect = g.createShape('rect', {
style: style,
position: {x: 100, y: 100},
width: 100,
height: 100
});const arc = g.createShape('arc', {
style: style,
center: {x: 100, y: 150},
width: 120,
height: 80
});const line = g.createLine(
{x: 10, y: 200},
{x: 80, y: 120},
style
);const arrow = g.createShape('arrow', {
style: style,
start: {x: 150, y: 120},
end: {x: 160, y: 150}
});const bezier = g.createShape('bezier', {
style: style,
points: [p0, p1, p2, p3, p4]
});const img = g.createShape('image', {
style: {src: 'image.png'},
position: {x: 100, y: 100}
});
img.canMove(true);const label = g.createShape('label', {
style: {
stroke: '#effaaa',
fill: '#fff',
textAlign: 'center',
textBaseline: 'middle',
fontSize: 24,
fontFamily: 'Arial',
maxWidth: 200 // 文本最大宽度,超过会自动换行
},
position: {x: 200, y: 150},
text: '这是一段测试文本,展示文本换行功能',
width: 200,
height: 100
});const ellipse = g.createShape('ellipse', {
style: style,
center: {x: 100, y: 150},
width: 120,
height: 80
});const polygon = g.createShape('polygon', {
style: style,
center: {x: 100, y: 150},
sides: 6, // 边数
radius: 50 // 半径
});const star = g.createShape('star', {
style: style,
center: {x: 100, y: 150},
points: 5, // 顶点数
radius: 50, // 外半径
innerRadius: 25 // 内半径
});const shape = g.createShape('rect', {...});
shape.bind('mouseover', function(evt) {
this.style.stroke = 'rgba(39,72,188,0.5)';
this.cursor('pointer');
this.needUpdate = true;
});| 事件名称 | 说明 | 回调参数 |
|---|---|---|
| mousedown | 鼠标按下 | - |
| mousemove | 鼠标移动 | {target, position} |
| mouseover | 鼠标移入 | {target} |
| mouseleave | 鼠标移出 | {target} |
| mouseup | 鼠标松开 | - |
| click | 鼠标点击 | - |
| dblclick | 鼠标双击 | - |
| touchstart | 触摸开始 | {position} |
| touchmove | 触摸移动 | {position} |
| touchend | 触摸结束 | {position} |
大多数控件继承 jmPath 即可,通过实现 initPoints 方法来绘制自定义图形:
import {jmPath} from "jmgraph";
class CustomShape extends jmPath {
constructor(params) {
super(params);
this.center = params.center || {x: 0, y: 0};
this.radius = params.radius || 0;
}
initPoints() {
const location = this.getLocation();
const cx = location.center.x;
const cy = location.center.y;
this.points = [];
this.points.push({x: cx - this.radius, y: cy - this.radius});
this.points.push({x: cx + this.radius, y: cy + this.radius});
return this.points;
}
}jmGraph 支持微信小程序,详情请参考 mini-jmchart。
const jmGraph = require('../../utils/jmgraph');
const g = jmGraph.create('mycanvas', {
style: {fill: '#000'},
width: 400,
height: 600
});
this.canvastouchstart = function (...arg) {
return g.eventHandler.touchStart(...arg);
}
this.canvastouchmove = function (...arg) {
return g.eventHandler.touchMove(...arg);
}
this.canvastouchend = function (...arg) {
return g.eventHandler.touchEnd(...arg);
}// 设置缩放因子,以指定点为中心
// 缩放因子,1为原始大小
// x, y 缩放中心坐标
g.setZoom(1.5, 400, 300);// 平移画布
// dx, dy 平移距离
g.pan(100, 50);// 重置缩放和平移
g.resetTransform();// 创建新图层
// name 图层名称
// options 图层选项
const layer = g.createLayer('My Layer', {
visible: true,
locked: false
});// 切换到指定图层
// layer 图层名称或图层对象
g.setActiveLayer('My Layer');// 获取所有图层
const layers = g.getLayers();
// 获取指定名称的图层
const layer = g.getLayer('My Layer');
// 获取当前活动图层
const activeLayer = g.getActiveLayer();// 移除指定图层
// layer 图层名称或图层对象
const success = g.removeLayer('My Layer');// 将形状添加到指定图层
// shape 形状对象
// layer 图层名称或图层对象,默认为当前活动图层
g.addShapeToLayer(shape, 'My Layer');
// 从图层中移除形状
// shape 形状对象
g.removeShapeFromLayer(shape);// 导出为 PNG 图片
// fileName 文件名
// format 图片格式,默认为 image/png
// quality 图片质量,0-1之间
g.exportToPNG('my-graph', 'image/png', 0.9);// 导出为 JPEG 图片
// fileName 文件名
// quality 图片质量,0-1之间
g.exportToJPEG('my-graph', 0.8);// 导出为 SVG
// fileName 文件名
g.exportToSVG('my-graph');当不再需要 jmGraph 实例时,调用 destroy() 释放资源(如事件监听、动画帧等):
const g = jmGraph('mycanvas', { ... });
// 使用完毕后销毁
g.destroy();
// 销毁后可通过 destroyed 标志判断状态
if (g.destroyed) {
console.log('实例已销毁');
}
destroy()会内部调用eventHandler.destroy()清除所有事件绑定,并设置destroyed = true标记。调用后不应再使用该实例。
当文本长度超过 maxWidth 时,会自动换行显示:
const label = g.createShape('label', {
style: {
fill: '#333',
fontSize: 14,
fontFamily: 'Arial',
textAlign: 'center',
maxWidth: 200 // 文本最大宽度,超过会自动换行
},
position: {x: 200, y: 150},
text: '这是一段测试文本,当文本长度超过最大宽度时,会自动换行显示。',
width: 200,
height: 100
});npm run buildnpm run dev欢迎贡献代码!请遵循以下步骤:
- Fork 本仓库
- 创建特性分支 (
git checkout -b feature/AmazingFeature) - 提交更改 (
git commit -m 'Add some AmazingFeature') - 推送到分支 (
git push origin feature/AmazingFeature) - 开启 Pull Request
本项目采用 MIT 许可证。
- GitHub Issues - 报告 Bug 和功能请求
- GitHub Discussions - 问题和讨论
感谢所有为本项目做出贡献的开发者!
- jmChart - 基于 jmGraph 的图表库
- mini-jmchart - 微信小程序图表库
- 作者: jiamao
- 邮箱: haofefe@163.com
- 主页: https://fefeding.github.io/jmgraph/
如果这个项目对你有帮助,请给个 ⭐️ Star!