D3JS 实现画笔效果(svg)

本文详细介绍了如何使用D3.js在SVG画布上创建一个可交互的绘图工具,包括创建SVG容器、添加背景、定义缩放和平移行为,以及通过mousedown、mousemove和mouseup事件实现画笔绘制和移动。
摘要由CSDN通过智能技术生成

目录

功能效果

效果图

实现逻辑

1、创建一个SVG容器:

2、创建一个主分组:

3、添加背景:

4、定义缩放和平移行为:

5、应用缩放和平移行为:

6、定义鼠标事件:

7、事件监听

完整代码


 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容器上,使得图表可以响应鼠标事件。
注:阻止鼠标落下默认事件。

svg.call(zoom).on("mousedown.zoom", null);

6、定义鼠标事件:

参数定义:

let isPanning = false;
let startX = 0, startY = 0;

// 画笔状态
let isBrushDrawing = 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 updateBrushPath(x, y) {
    pathData += `${pathData ? ' L' : 'M'} ${x} ${y}`;
    if (currentPath) {
        currentPath.attr('d', pathData);
    } else {
        currentPath = g.append('path')
            .attr('id', 'path')
            .attr('d', pathData)
            .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 brushMouseDown(event) {
    if (event.button !== 0) return; // 检查左键
    console.log('左键按下');
    isBrushDrawing = true;
    const [x, y] = getTransformedCoordinate(event);
    updateBrushPath(x, y);
}

画笔鼠标移动事件(mousemove):

function brushMouseMove(event) {
    if (isBrushDrawing) {
        // 获取缩放及平移后的坐标
        const [x, y] = getTransformedCoordinate(event);
        updateBrushPath(x, y);
    }
}

画笔鼠标抬起事件(mouseup):

function brushMouseUp(event) {
    if (isBrushDrawing) {
        isBrushDrawing = false;
        pathData = '';
        currentPath = null;
    }
}

7、事件监听

svg.on("mousedown", function (event) {
    panMouseDown(event);
    brushMouseDown(event);

    svg.on("mousemove", function (event) {
        panMouseMove(event);
        brushMouseMove(event);
    });
    svg.on("mouseup", function (event) {
        panMouseUp(event);
        brushMouseUp(event);

        svg.on("mousemove", function (event) {
            panMouseMove(event);
        });
        svg.on("mouseup", function (event) {
            panMouseUp(event);
        });
    });
});

完整代码

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("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 isBrushDrawing = 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 updateBrushPath(x, y) {
    pathData += `${pathData ? ' L' : 'M'} ${x} ${y}`;
    if (currentPath) {
        currentPath.attr('d', pathData);
    } else {
        currentPath = g.append('path')
            .attr('id', 'path')
            .attr('d', pathData)
            .attr('stroke', 'red')
            .attr('fill', 'none')
            .attr('stroke-width', 2);
    }
}

// 画笔 - 监听事件
function brushMouseDown(event) {
    if (event.button !== 0) return; // 检查左键
    console.log('左键按下');
    isBrushDrawing = true;
    const [x, y] = getTransformedCoordinate(event);
    updateBrushPath(x, y);
}

function brushMouseMove(event) {
    if (isBrushDrawing) {
        // 获取缩放及平移后的坐标
        const [x, y] = getTransformedCoordinate(event);
        updateBrushPath(x, y);
    }
}

function brushMouseUp(event) {
    if (isBrushDrawing) {
        isBrushDrawing = false;
        pathData = '';
        currentPath = null;
    }
}

svg.on("mousedown", function (event) {
    panMouseDown(event);
    brushMouseDown(event);

    svg.on("mousemove", function (event) {
        panMouseMove(event);
        brushMouseMove(event);
    });
    svg.on("mouseup", function (event) {
        panMouseUp(event);
        brushMouseUp(event);

        svg.on("mousemove", function (event) {
            panMouseMove(event);
        });
        svg.on("mouseup", function (event) {
            panMouseUp(event);
        });
    });
});

感谢阅读!在您离开之前,不妨留下您的足迹:

👍 点个赞,让我知道您喜欢这篇文章!

📝 有任何想法、建议或问题?欢迎在下方评论区分享您的想法!

🔁 分享给您的朋友们,让更多人受益!

💌 喜欢我的内容?立即订阅我的博客,获取更多精彩内容!

谢谢您的支持与参与,让我们一起创造更好的内容体验!

  • 27
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值