目录
D3.js绘图工具系列文章总提纲:【传送门】
功能效果
通过按下鼠标左键开始绘制,移动鼠标更新路径坐标预览效果,再次点击后则将鼠标当前位置添加进路径集,移动鼠标更新路径坐标预览效果,双击鼠标左键则结束绘制;通过画布的平移(按住滑轮)与缩放(滚动滑轮)不影响画笔的绘制效果。
效果图
实现逻辑
通过修改 mousedown、mousemove与mouseup事件实现。
1、创建一个SVG容器:
使用d3.js创建一个SVG容器(h5代码中须有相关id标签),用于呈现图表和图形;
let svg = reactive({});
// 生成画布的dom节点id为d3Canvas;须在html代码中添加
svg = d3
.select('#d3Canvas')
.append("svg")
.attr("id", "svg")
.attr("width", 500)
.attr("height", 500)
.attr("viewBox", "0 0 500 500");
2、创建一个主分组:
使用d3.js对所创建的SVG容器,添加一个g标签,用于控制整体缩放;不将事件直接绑定于svg上,是因为会出现bug:画面呈现出一闪一闪的,解决方案是将所有元素使用g标签进行包裹。
let g = reactive({});
g = svg.append("g").attr("id", "svg-grid");
3、添加背景:
用于让拖拽缩放效果更明显;
g.insert("rect", ":first-child")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "url(#grid)");
// 使用网格样式填充 SVG 元素背景
var pattern = svg.insert("defs", ":first-child")
.append("pattern")
.attr("id", "grid")
.attr("width", 20)
.attr("height", 20)
.attr("patternUnits", "userSpaceOnUse");
// 添加网格线
pattern.append("path")
.attr("d", "M 20 0 L 0 0 0 20")
.attr("stroke", "lightgray")
.attr("stroke-width", 0.5)
.attr("fill", "none");
4、定义缩放和平移行为:
使用d3.zoom()函数创建一个缩放和平移行为,可以通过设置缩放比例和平移偏移量来控制缩放和平移的效果;
function zoomed(event, svg) {
// 对g进行偏移赋值
g.attr("transform", event.transform);
}
var zoom = d3
.zoom()
.scaleExtent([0.3, 10])
.on("zoom", (event) => zoomed(event));
5、应用缩放和平移行为:
将缩放和平移行为应用到SVG容器上,使得图表可以响应鼠标事件。
注:阻止鼠标落下默认事件与鼠标双击事件。
g.call(zoom).on("dblclick.zoom", null).on("mousedown.zoom", null);
6、定义鼠标事件:
参数定义:
let isPanning = false;
let startX = 0, startY = 0;
// 折线画笔状态
let isPolygonalLineDrawing = false;
// 路径
let pathData = '';
let currentPath = null;
定义坐标转换方法:
function getTransformedCoordinate(event) {
// 获取鼠标当前位置的坐标
const [x, y] = d3.pointer(event);
// 获取画布当前的平移和缩放值
const transform = d3.zoomTransform(g.node());
// 应用平移和缩放转换,得到变换后的坐标
const transformedX = (x - transform.x) / transform.k;
const transformedY = (y - transform.y) / transform.k;
return [transformedX, transformedY];
}
定义获取路劲中最后一组坐标的方法:
function getLastSegment(pathData) {
// 使用正则表达式匹配最后一个'L'后的字符串
const regex = /L\s*([^L]*)$/;
const match = pathData.match(regex);
// 如果找到匹配项,则返回最后一个'L'后的字符串,否则返回空字符串
if (match && match.length > 1) {
return match[1].trim();
} else {
return '';
}
}
定义绘制/更新折现路径方法:
function updatePolygonalLinePath(x, y) {
let currentPolygonalLineData = '';
// 截取最后一个L后的字符
if (isPolygonalLineDrawing) {
// 若点击位置相同,则不绘制
const lastSegment = getLastSegment(pathData);
if (lastSegment == `${x} ${y}`) return;
// 折线路径
pathData += `${pathData ? ' L' : 'M'} ${x} ${y}`;
currentPolygonalLineData = pathData;
} else {
currentPolygonalLineData = pathData + `${pathData ? ' L' : 'M'} ${x} ${y}`;
}
if (currentPath) {
currentPath.attr('d', currentPolygonalLineData);
} else {
currentPath = g.append('path')
.attr('id', 'path')
.attr('d', currentPolygonalLineData)
.attr('stroke', 'red')
.attr('fill', 'none')
.attr('stroke-width', 2);
}
}
平移鼠标落下事件(mousedown):
function panMouseDown(event) {
if (event.button !== 1) return; // 中键按下
console.log('中键按下');
isPanning = true;
[startX, startY] = d3.pointer(event);
}
平移鼠标移动事件(mousemove):
function panMouseMove(event) {
if (isPanning) {
event.preventDefault();
const [x, y] = d3.pointer(event);
const transform = d3.zoomTransform(g.node()); // 获取当前变换
const transformedX = (x - startX) / transform.k;
const transformedY = (y - startY) / transform.k;
zoom.translateBy(g, transformedX, transformedY); // 使用偏移量进行平移
startX = x; // 更新起始位置
startY = y;
}
}
平移鼠标抬起事件(mouseup):
function panMouseUp() {
if (isPanning) {
isPanning = false;
}
}
折线鼠标落下事件(mousedown):
function polygonalLineMouseDown(event) {
if (event.button !== 0) return; // 检查左键
console.log('左键按下');
isPolygonalLineDrawing = true;
const [x, y] = getTransformedCoordinate(event);
updatePolygonalLinePath(x, y);
}
折线鼠标移动事件(mousemove):
function polygonalLineMouseMove(event) {
if (!isPolygonalLineDrawing && currentPath) {
// 获取缩放及平移后的坐标
const [x, y] = getTransformedCoordinate(event);
updatePolygonalLinePath(x, y);
}
}
折线鼠标抬起事件(mouseup):
function polygonalLineMouseUp(event) {
if (isPolygonalLineDrawing) {
isPolygonalLineDrawing = false;
}
}
折线鼠标双击事件(dblclick):
function polygonalLineDbClick(event) {
isPolygonalLineDrawing = false;
pathData = '';
currentPath = null;
console.log('结束绘制');
}
7、事件监听
svg.on("mousedown", function (event) {
panMouseDown(event);
polygonalLineMouseDown(event);
});
svg.on("mousemove", function (event) {
panMouseMove(event);
polygonalLineMouseMove(event);
});
svg.on("mouseup", function (event) {
panMouseUp(event);
polygonalLineMouseUp();
});
svg.on("dblclick", function (event) {
polygonalLineDbClick()
});
完整代码
let svg = reactive({});
// 生成画布的dom节点id为d3Canvas;须在html代码中添加
svg = d3
.select('#d3Canvas')
.append("svg")
.attr("id", "svg")
.attr("width", 500)
.attr("height", 500)
.attr("viewBox", "0 0 500 500");
let g = reactive({});
g = svg.append("g").attr("id", "svg-grid");
g.insert("rect", ":first-child")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "url(#grid)");
// 使用网格样式填充 SVG 元素背景
var pattern = svg.insert("defs", ":first-child")
.append("pattern")
.attr("id", "grid")
.attr("width", 20)
.attr("height", 20)
.attr("patternUnits", "userSpaceOnUse");
// 添加网格线
pattern.append("path")
.attr("d", "M 20 0 L 0 0 0 20")
.attr("stroke", "lightgray")
.attr("stroke-width", 0.5)
.attr("fill", "none");
function zoomed(event, svg) {
// 对g进行偏移赋值
g.attr("transform", event.transform);
}
var zoom = d3
.zoom()
.scaleExtent([0.3, 10])
.on("zoom", (event) => zoomed(event));
g.call(zoom).on("dblclick.zoom", null).on("mousedown.zoom", null);
let isPanning = false;
let startX = 0, startY = 0;
function panMouseDown(event) {
if (event.button !== 1) return; // 中键按下
console.log('中键按下');
// event.preventDefault(); // 阻止默认行为
isPanning = true;
[startX, startY] = d3.pointer(event);
}
function panMouseMove(event) {
if (isPanning) {
event.preventDefault();
const [x, y] = d3.pointer(event);
const transform = d3.zoomTransform(g.node()); // 获取当前变换
const transformedX = (x - startX) / transform.k;
const transformedY = (y - startY) / transform.k;
zoom.translateBy(g, transformedX, transformedY); // 使用偏移量进行平移
startX = x; // 更新起始位置
startY = y;
}
}
function panMouseUp() {
if (isPanning) {
isPanning = false;
}
}
// 折线画笔状态
let isPolygonalLineDrawing = false;
// 路径
let pathData = '';
let currentPath = null;
// 获取转换坐标
function getTransformedCoordinate(event) {
// 获取鼠标当前位置的坐标
const [x, y] = d3.pointer(event);
// 获取画布当前的平移和缩放值
const transform = d3.zoomTransform(g.node());
// 应用平移和缩放转换,得到变换后的坐标
const transformedX = (x - transform.x) / transform.k;
const transformedY = (y - transform.y) / transform.k;
return [transformedX, transformedY];
}
// 获取最后一个'L'后的坐标
function getLastSegment(pathData) {
// 使用正则表达式匹配最后一个'L'后的字符串
const regex = /L\s*([^L]*)$/;
const match = pathData.match(regex);
// 如果找到匹配项,则返回最后一个'L'后的字符串,否则返回空字符串
if (match && match.length > 1) {
return match[1].trim();
} else {
return '';
}
}
// 绘制/更新折线路径
function updatePolygonalLinePath(x, y) {
let currentPolygonalLineData = '';
// 截取最后一个L后的字符
if (isPolygonalLineDrawing) {
// 若点击位置相同,则不绘制
const lastSegment = getLastSegment(pathData);
if (lastSegment == `${x} ${y}`) return;
// 折线路径
pathData += `${pathData ? ' L' : 'M'} ${x} ${y}`;
currentPolygonalLineData = pathData;
} else {
currentPolygonalLineData = pathData + `${pathData ? ' L' : 'M'} ${x} ${y}`;
}
if (currentPath) {
currentPath.attr('d', currentPolygonalLineData);
} else {
currentPath = g.append('path')
.attr('id', 'path')
.attr('d', currentPolygonalLineData)
.attr('stroke', 'red')
.attr('fill', 'none')
.attr('stroke-width', 2);
}
}
// 折线 - 监听事件
function polygonalLineMouseDown(event) {
if (event.button !== 0) return; // 检查左键
console.log('左键按下');
isPolygonalLineDrawing = true;
const [x, y] = getTransformedCoordinate(event);
updatePolygonalLinePath(x, y);
}
function polygonalLineMouseMove(event) {
if (!isPolygonalLineDrawing && currentPath) {
// 获取缩放及平移后的坐标
const [x, y] = getTransformedCoordinate(event);
updatePolygonalLinePath(x, y);
}
}
function polygonalLineMouseUp(event) {
if (isPolygonalLineDrawing) {
isPolygonalLineDrawing = false;
}
}
function polygonalLineDbClick(event) {
isPolygonalLineDrawing = false;
pathData = '';
currentPath = null;
console.log('结束绘制');
}
svg.on("mousedown", function (event) {
panMouseDown(event);
polygonalLineMouseDown(event);
});
svg.on("mousemove", function (event) {
panMouseMove(event);
polygonalLineMouseMove(event);
});
svg.on("mouseup", function (event) {
panMouseUp(event);
polygonalLineMouseUp();
});
svg.on("dblclick", function (event) {
polygonalLineDbClick()
});
感谢阅读!在您离开之前,不妨留下您的足迹:
👍 点个赞,让我知道您喜欢这篇文章!
📝 有任何想法、建议或问题?欢迎在下方评论区分享您的想法!
🔁 分享给您的朋友们,让更多人受益!
💌 喜欢我的内容?立即订阅我的博客,获取更多精彩内容!
谢谢您的支持与参与,让我们一起创造更好的内容体验!