跳到主要内容

JavaScript 交互

SVG 与 JavaScript 的结合可以创建丰富的交互式图形应用。通过 JavaScript 可以动态创建、修改 SVG 元素,响应用户事件,实现复杂的数据可视化。

获取 SVG 元素

内联 SVG

内联 SVG 可以直接使用 DOM API 获取和操作:

<svg id="mySvg" width="200" height="100" xmlns="http://www.w3.org/2000/svg">
<circle id="myCircle" cx="100" cy="50" r="30" fill="#3498db"/>
</svg>
<script>
const circle = document.getElementById('myCircle');
console.log(circle.getAttribute('cx')); // 输出: 100
</script>

使用 querySelector

const svg = document.querySelector('#mySvg');
const circle = svg.querySelector('circle');
const allCircles = svg.querySelectorAll('circle');

修改 SVG 属性

使用 setAttribute

const circle = document.getElementById('myCircle');
circle.setAttribute('cx', '150');
circle.setAttribute('fill', '#e74c3c');
circle.setAttribute('r', '40');

使用 DOM 属性

部分属性可以直接通过 DOM 属性访问:

const circle = document.getElementById('myCircle');
circle.cx.baseVal.value = 150;
circle.r.baseVal.value = 40;

修改样式

const circle = document.getElementById('myCircle');
circle.style.fill = '#2ecc71';
circle.style.stroke = '#27ae60';
circle.style.strokeWidth = '3px';

创建 SVG 元素

使用 createElementNS

创建 SVG 元素必须使用 createElementNS,指定 SVG 命名空间:

const svg = document.getElementById('mySvg');

const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', '50');
rect.setAttribute('y', '25');
rect.setAttribute('width', '100');
rect.setAttribute('height', '50');
rect.setAttribute('fill', '#9b59b6');

svg.appendChild(rect);

封装创建函数

function createSvgElement(tag, attributes) {
const element = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (const [key, value] of Object.entries(attributes)) {
element.setAttribute(key, value);
}
return element;
}

const circle = createSvgElement('circle', {
cx: 100,
cy: 50,
r: 30,
fill: '#e74c3c'
});
svg.appendChild(circle);

事件处理

添加事件监听器

<svg width="200" height="100" xmlns="http://www.w3.org/2000/svg">
<rect id="clickRect" x="50" y="25" width="100" height="50" fill="#3498db" cursor="pointer"/>
</svg>
<script>
const rect = document.getElementById('clickRect');

rect.addEventListener('click', function(e) {
console.log('矩形被点击');
this.setAttribute('fill', '#e74c3c');
});

rect.addEventListener('mouseenter', function() {
this.setAttribute('fill', '#2980b9');
});

rect.addEventListener('mouseleave', function() {
this.setAttribute('fill', '#3498db');
});
</script>

常用事件

事件类型事件名
鼠标事件click, dblclick, mousedown, mouseup, mouseover, mouseout, mousemove
触摸事件touchstart, touchend, touchmove
键盘事件keydown, keyup, keypress(需要元素可聚焦)

获取鼠标位置

<svg id="positionSvg" width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#ecf0f1"/>
<circle id="followCircle" cx="150" cy="100" r="10" fill="#e74c3c"/>
</svg>
<script>
const svg = document.getElementById('positionSvg');
const circle = document.getElementById('followCircle');

svg.addEventListener('mousemove', function(e) {
const rect = svg.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
});
</script>

使用 SVG 坐标转换

对于有 viewBox 的 SVG,需要转换坐标:

function getSvgPoint(svg, clientX, clientY) {
const pt = svg.createSVGPoint();
pt.x = clientX;
pt.y = clientY;
return pt.matrixTransform(svg.getScreenCTM().inverse());
}

svg.addEventListener('click', function(e) {
const point = getSvgPoint(this, e.clientX, e.clientY);
console.log(`SVG 坐标: (${point.x}, ${point.y})`);
});

拖拽功能

简单拖拽

<svg id="dragSvg" width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f5f5f5"/>
<circle id="draggable" cx="150" cy="100" r="30" fill="#3498db" cursor="move"/>
</svg>
<script>
const circle = document.getElementById('draggable');
const svg = document.getElementById('dragSvg');
let isDragging = false;
let offset = { x: 0, y: 0 };

circle.addEventListener('mousedown', function(e) {
isDragging = true;
const rect = svg.getBoundingClientRect();
offset.x = e.clientX - rect.left - parseFloat(this.getAttribute('cx'));
offset.y = e.clientY - rect.top - parseFloat(this.getAttribute('cy'));
});

svg.addEventListener('mousemove', function(e) {
if (!isDragging) return;
const rect = svg.getBoundingClientRect();
const x = e.clientX - rect.left - offset.x;
const y = e.clientY - rect.top - offset.y;
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
});

svg.addEventListener('mouseup', function() {
isDragging = false;
});

svg.addEventListener('mouseleave', function() {
isDragging = false;
});
</script>

操作 defs 元素

动态创建渐变

function createGradient(svg, id, colors) {
const defs = svg.querySelector('defs') || document.createElementNS('http://www.w3.org/2000/svg', 'defs');

const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gradient.setAttribute('id', id);

colors.forEach((color, index) => {
const stop = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop.setAttribute('offset', `${(index / (colors.length - 1)) * 100}%`);
stop.setAttribute('stop-color', color);
gradient.appendChild(stop);
});

defs.appendChild(gradient);
if (!svg.contains(defs)) {
svg.appendChild(defs);
}

return `url(#${id})`;
}

const gradientFill = createGradient(svg, 'myGradient', ['#e74c3c', '#f39c12', '#2ecc71']);
circle.setAttribute('fill', gradientFill);

使用 SVG DOM API

SVGSVGElement 接口

SVG 根元素提供了一些实用方法:

const svg = document.getElementById('mySvg');

// 创建 SVGPoint
const point = svg.createSVGPoint();
point.x = 100;
point.y = 50;

// 创建 SVGRect
const rect = svg.createSVGRect();
rect.x = 0;
rect.y = 0;
rect.width = 100;
rect.height = 100;

// 获取元素边界框
const bbox = circle.getBBox();
console.log(`宽度: ${bbox.width}, 高度: ${bbox.height}`);

// 获取变换矩阵
const ctm = circle.getCTM();
const screenCtm = circle.getScreenCTM();

坐标转换

// 将屏幕坐标转换为 SVG 坐标
function screenToSvg(svg, x, y) {
const pt = svg.createSVGPoint();
pt.x = x;
pt.y = y;
return pt.matrixTransform(svg.getScreenCTM().inverse());
}

// 将 SVG 坐标转换为屏幕坐标
function svgToScreen(svg, x, y) {
const pt = svg.createSVGPoint();
pt.x = x;
pt.y = y;
return pt.matrixTransform(svg.getScreenCTM());
}

批量操作

使用 group 元素

const svg = document.getElementById('mySvg');
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');

for (let i = 0; i < 10; i++) {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', 20 + i * 20);
circle.setAttribute('cy', 50);
circle.setAttribute('r', 8);
circle.setAttribute('fill', '#3498db');
g.appendChild(circle);
}

svg.appendChild(g);

// 批量修改
g.setAttribute('transform', 'translate(0, 50)');

使用 DocumentFragment

const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', Math.random() * 300);
rect.setAttribute('y', Math.random() * 200);
rect.setAttribute('width', 10);
rect.setAttribute('height', 10);
rect.setAttribute('fill', '#e74c3c');
fragment.appendChild(rect);
}

svg.appendChild(fragment);

数据绑定

简单数据可视化

<svg id="chartSvg" width="400" height="200" xmlns="http://www.w3.org/2000/svg"></svg>
<script>
const data = [
{ label: 'A', value: 80 },
{ label: 'B', value: 120 },
{ label: 'C', value: 60 },
{ label: 'D', value: 150 },
{ label: 'E', value: 90 }
];

const svg = document.getElementById('chartSvg');
const barWidth = 50;
const gap = 20;
const maxValue = Math.max(...data.map(d => d.value));

data.forEach((item, index) => {
const barHeight = (item.value / maxValue) * 150;
const x = 30 + index * (barWidth + gap);
const y = 180 - barHeight;

const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', x);
rect.setAttribute('y', y);
rect.setAttribute('width', barWidth);
rect.setAttribute('height', barHeight);
rect.setAttribute('fill', '#3498db');
svg.appendChild(rect);

const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', x + barWidth / 2);
text.setAttribute('y', 195);
text.setAttribute('text-anchor', 'middle');
text.setAttribute('font-size', '12');
text.textContent = item.label;
svg.appendChild(text);
});
</script>

性能优化

减少 DOM 操作

// 不好:频繁修改 DOM
for (let i = 0; i < 1000; i++) {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
svg.appendChild(circle);
}

// 好:使用 DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
fragment.appendChild(circle);
}
svg.appendChild(fragment);

使用 requestAnimationFrame

let animationId;
let x = 0;

function animate() {
x += 1;
circle.setAttribute('cx', x % 300);
animationId = requestAnimationFrame(animate);
}

animate();

// 停止动画
function stopAnimation() {
cancelAnimationFrame(animationId);
}

使用 CSS 类切换

// 不好:直接修改样式
element.style.fill = '#e74c3c';
element.style.stroke = '#c0392b';
element.style.strokeWidth = '2px';

// 好:使用 CSS 类
// CSS: .active { fill: #e74c3c; stroke: #c0392b; stroke-width: 2px; }
element.classList.add('active');

实用示例

可缩放 SVG

<svg id="zoomSvg" width="400" height="300" xmlns="http://www.w3.org/2000/svg" style="border: 1px solid #ccc;">
<g id="zoomContent">
<circle cx="200" cy="150" r="50" fill="#3498db"/>
<rect x="100" y="100" width="80" height="60" fill="#e74c3c"/>
</g>
</svg>
<script>
const svg = document.getElementById('zoomSvg');
const content = document.getElementById('zoomContent');
let scale = 1;
let translateX = 0;
let translateY = 0;

svg.addEventListener('wheel', function(e) {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
scale *= delta;
scale = Math.max(0.1, Math.min(5, scale));
updateTransform();
});

function updateTransform() {
content.setAttribute('transform', `translate(${translateX}, ${translateY}) scale(${scale})`);
}
</script>

SVG 绘图板

<svg id="drawSvg" width="400" height="300" xmlns="http://www.w3.org/2000/svg" style="border: 1px solid #ccc; cursor: crosshair;">
</svg>
<script>
const svg = document.getElementById('drawSvg');
let isDrawing = false;
let currentPath = null;
let pathData = '';

svg.addEventListener('mousedown', function(e) {
isDrawing = true;
const rect = svg.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

pathData = `M ${x} ${y}`;
currentPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
currentPath.setAttribute('d', pathData);
currentPath.setAttribute('stroke', '#2c3e50');
currentPath.setAttribute('stroke-width', '2');
currentPath.setAttribute('fill', 'none');
svg.appendChild(currentPath);
});

svg.addEventListener('mousemove', function(e) {
if (!isDrawing || !currentPath) return;
const rect = svg.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

pathData += ` L ${x} ${y}`;
currentPath.setAttribute('d', pathData);
});

svg.addEventListener('mouseup', function() {
isDrawing = false;
currentPath = null;
});
</script>

小结

JavaScript 与 SVG 的结合可以创建丰富的交互式图形应用。通过 DOM API 可以获取、创建和修改 SVG 元素。事件处理让 SVG 具备交互能力。使用 SVG DOM API 可以进行坐标转换和获取元素信息。性能优化需要注意减少 DOM 操作和使用 requestAnimationFrame。