MassageRobot_Dobot/UI_next/templates/massage_plan_visualization.html
2025-05-27 15:46:31 +08:00

1508 lines
69 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>按摩计划可视化</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
height: 100vh;
background-color: transparent;
font-family: "Microsoft YaHei", sans-serif;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
overflow: hidden;
}
.container {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.image-container {
position: relative;
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
img {
height: 100%;
width: auto;
object-fit: contain;
-webkit-user-select: none;
user-select: none;
}
.overlay {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
.point {
fill: rgba(147, 112, 219, 0.2);
stroke: rgba(147, 112, 219, 0.8);
stroke-width: 1.5;
cursor: pointer;
pointer-events: all;
transition: all 0.3s ease;
r: 6;
transform-origin: center;
}
.point-group {
transition: transform 0.3s ease;
transform-origin: center;
}
.point-group.active .point {
fill: rgba(236, 226, 255, 0.76);
r: 8;
}
.label-line {
stroke: rgba(147, 112, 219, 0.6);
stroke-width: 2;
stroke-dasharray: 4 4;
opacity: 0;
transition: all 0.3s ease;
}
.point-group.active .label-line {
opacity: 1;
}
.label-bg {
fill: rgba(255, 255, 255, 0.95);
rx: 8;
ry: 8;
opacity: 0;
transition: all 0.3s ease;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
.point-group.active .label-bg {
opacity: 1;
}
.label {
font-size: 14px;
fill: #4a148c;
text-anchor: middle;
dominant-baseline: middle;
pointer-events: none;
opacity: 0;
transition: all 0.3s ease;
font-weight: 500;
}
.point-group.active .label {
opacity: 1;
}
.point-pulse {
fill: none;
stroke: rgba(172, 244, 255, 1);
stroke-width: 4;
opacity: 0;
r: 6;
transform-origin: center;
transform-box: fill-box;
}
.point-group.active .point-pulse {
opacity: 1;
animation: pulse 1.5s infinite;
}
.touch-target {
fill: transparent;
stroke: none;
r: 15;
cursor: pointer;
pointer-events: all;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 0.4;
}
100% {
transform: scale(1.5);
opacity: 0;
}
}
</style>
</head>
<body>
<div class="container">
<div class="image-container">
<img src="{{ url_for('static', filename='images/smart_mode/' + body_part + '.png') }}" alt="按摩计划示意图" id="baseImage">
<svg class="overlay" id="pointsOverlay" preserveAspectRatio="none">
<defs>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<marker id="arrowhead" markerWidth="8" markerHeight="6"
refX="4" refY="3" orient="auto">
<path d="M 0 0 L 8 3 L 0 6"
fill="none"
stroke="rgba(255, 64, 129, 0.8)"
stroke-width="1.5"/>
</marker>
</defs>
</svg>
</div>
</div>
<script>
// 添加全局变量声明
let activePoints = new Set(); // 用于跟踪活动点的全局集合
// 定义中线位置为242用于计算左右对称的穴位
// 左侧穴位x坐标与右侧穴位x坐标应满足关系左x + 右x = 2 * 中线
// 左右穴位的y坐标应相同或非常接近
const backPoints = {
'肩中左俞': [206.77748316653222, 238.1262087946534],
'肩外左俞': [188.0139028442666, 254.39847136789558],
'秉风左': [141.60937708743066, 276.64148284250496],
'天宗左': [142.6497671020791, 320.9231295710206],
'膈俞左': [213.20643824465722, 381.92746306711433],
'魂门左': [187.32457172687234, 425.9175424702871],
'三焦左俞': [212.03067219073952, 496.8722238667715],
'京门左': [166.6565724592942, 509.5702219136465],
'关元左俞': [210.6317224094994, 584.1379643980357],
'膀胱左俞': [208.7101220677025, 615.1861821714732],
'白环左俞': [208.03470702864, 637.6799565855357],
'秩边左': [187.6327752659447, 637.8695928160045],
'肩中右俞': [277.22251683346778, 238.1262087946534],
'肩外右俞': [295.9860971557334, 254.39847136789558],
'秉风右': [342.39062291256934, 276.64148284250496],
'天宗右': [341.3502328979209, 320.9231295710206],
'膈俞右': [270.79356175534278, 381.92746306711433],
'魂门右': [296.67542827312766, 425.9175424702871],
'三焦右俞': [271.96932780926048, 496.8722238667715],
'京门右': [317.3434275407058, 509.5702219136465],
'关元右俞': [273.3682775905006, 584.1379643980357],
'膀胱右俞': [275.2898779322975, 615.1861821714732],
'白环右俞': [275.96529297136, 637.6799565855357],
'秩边右': [296.3672247340553, 637.8695928160045],
'督俞左': [213.69436724519898, 360.8133435814997],
'督俞右': [270.30563275480102, 360.8133435814997],
'心俞左': [214.1432619256974, 341.3883536547342],
'心俞右': [269.8567380743026, 341.3883536547342],
'厥阴左俞': [214.57263944617415, 322.8079285073933],
'厥阴右俞': [269.42736055382585, 322.8079285073933],
'肺俞左': [215.08008560673758, 300.8492442423541],
'肺俞右': [268.91991439326242, 300.8492442423541],
'风门左': [215.43139448712765, 285.6470782127115],
'风门右': [268.56860551287235, 285.6470782127115],
'大杼左': [215.74366904747438, 272.13404174191817],
'大杼右': [268.25633095252562, 272.13404174191817],
'膈关左': [188.30042972795584, 383.6893034990578],
'膈关右': [295.69957027204416, 383.6893034990578],
'譩譆左': [188.76884156847595, 363.4197487928677],
'譩譆右': [295.23115843152405, 363.4197487928677],
'神堂左': [189.21773624897438, 343.9947588661023],
'神堂右': [294.78226375102562, 343.9947588661023],
'膏肓左': [189.64711376945112, 325.4143337187613],
'膏肓右': [294.35288623054888, 325.4143337187613],
'魄户左': [190.15455993001456, 303.4556494537221],
'魄户右': [293.84544006998544, 303.4556494537221],
'附分左': [190.46683449036126, 289.9426129829287],
'附分右': [293.53316550963874, 289.9426129829287],
'曲垣左': [190.74007473066467, 278.11870607098456],
'曲垣右': [293.25992526933533, 278.11870607098456],
'胃俞左': [212.06159658565437, 478.39869418514525],
'胃俞右': [271.93840341434563, 478.39869418514525],
'脾俞左': [212.08908493668977, 461.97777891258863],
'脾俞右': [271.91091506331023, 461.97777891258863],
'胆俞左': [212.12000933160462, 443.50424923096244],
'胆俞右': [271.87999066839538, 443.50424923096244],
'肝俞左': [212.15436977039892, 422.9781051402666],
'肝俞右': [271.84563022960108, 422.9781051402666],
'小肠左俞': [209.80817940587215, 597.4443434437947],
'中膂左俞': [208.32417061680965, 628.0397675509375],
'小肠右俞': [274.19182059412785, 597.4443434437947],
'中膂右俞': [275.67582938319035, 628.0397675509375],
'大肠左俞': [210.7413501827378, 561.812951302039],
'大肠右俞': [273.2586498172622, 561.812951302039],
'气海左俞': [210.85097795597622, 539.4879382060423],
'气海右俞': [273.14902204402378, 539.4879382060423],
'肾俞左': [210.96060572921462, 517.1629251100456],
'肾俞右': [273.03939427078538, 517.1629251100456],
'胞肓左': [187.66626251761986, 608.9671279900574],
'胞肓右': [296.33373748238014, 608.9671279900574],
'志室左': [187.77788668987037, 512.6255785702336],
'志室右': [296.22211331012963, 512.6255785702336],
'肓门左': [187.8024440077655, 491.4304376978725],
'肓门右': [296.1975559922345, 491.4304376978725],
'胃仓左': [187.82030387532558, 476.01578979070064],
'胃仓右': [296.17969612467442, 476.01578979070064],
'意舍左': [187.83704750116317, 461.56455737772717],
'意舍右': [296.16295249883683, 461.56455737772717],
'阳纲左': [187.85713985216825, 444.2230784821588],
'阳纲右': [296.14286014783175, 444.2230784821588],
'崇骨': [242.0782644165322, 222.50720549875496],
'大椎': [241.88662928469626, 239.28152800852058],
'陶道': [242.18446494591186, 272.39031310422286],
'身柱': [241.52088150517505, 301.10551560465876],
'神道': [240.58405782413487, 341.6446250170389],
'灵台': [240.13516314363648, 361.06961494380437],
'至阳': [239.6472341430947, 382.183734429419],
'筋缩': [239.16981166493014, 423.4233108287432],
'中枢': [239.13545122613584, 443.949454919439],
'脊中': [239.104526831221, 462.4229846010652],
'悬枢': [239.04611408527074, 497.31742955524805],
'命门': [237.82681666671465, 517.603202209655],
'腰阳关': [237.6075611202378, 562.2532284016484]
};
const bellyPoints = {
'神阙': [248, 490],
'气海': [248, 510],
'石门': [248, 525],
'关元': [248, 545],
'水分': [249, 465],
'天枢右': [285, 491],
'天枢左': [215, 490],
'外陵右': [286, 507],
'外陵左': [214, 506],
'滑肉右': [285, 456],
'滑肉左': [215, 455],
'大横左': [184, 490],
'大横右': [316, 491]
};
const legPoints = {
'承扶左': [193, 30],
'委中左': [195, 270],
'昆仑左': [203, 516],
'承扶右': [302, 30],
'委中右': [290, 270],
'昆仑右': [270, 516],
'殷门左': [193, 118],
'殷门右': [302, 118],
'上委中左': [195, 215],
'上委中右': [295, 215],
'承山左': [200, 425],
'承筋左': [196, 360],
'合阳左': [197, 310],
'承山右': [280, 425],
'承筋右': [290, 360],
'合阳右': [290, 310]
};
// 初始化当前身体部位
let currentBodyPart = '{{ body_part }}';
let points = currentBodyPart === 'belly' ? bellyPoints :
currentBodyPart === 'leg' ? legPoints : backPoints;
function calculateLabelPosition(x, y, index, totalPoints) {
const isLeftSide = x < 240;
const labelDistance = window.innerWidth <= 768 ? 50 : 40; // 平板上更大的间距
const labelSpacing = window.innerWidth <= 768 ? 20 : 10; // 平板上更大的垂直间距
const labelX = isLeftSide ? x - labelDistance : x + labelDistance;
const verticalOffset = (index % 3 - 1) * labelSpacing;
const labelY = y + verticalOffset;
return { labelX, labelY };
}
function initializePoints() {
const img = document.getElementById('baseImage');
const svg = document.getElementById('pointsOverlay');
let currentMode = 'none';
let currentShape = null;
// 创建广播通道
const channel = new BroadcastChannel('massage_plan_channel');
function updateSVGSize() {
const rect = img.getBoundingClientRect();
// 获取图片的实际显示尺寸
const imgDisplayWidth = img.clientWidth;
const imgDisplayHeight = img.clientHeight;
// 设置SVG的尺寸与图片完全一致
svg.setAttribute('width', imgDisplayWidth);
svg.setAttribute('height', imgDisplayHeight);
svg.setAttribute('viewBox', `0 0 ${img.naturalWidth} ${img.naturalHeight}`);
// 计算SVG的位置使其与图片完全重叠
const imgRect = img.getBoundingClientRect();
const containerRect = img.parentElement.getBoundingClientRect();
// 计算图片相对于容器的位置
const left = imgRect.left - containerRect.left;
const top = imgRect.top - containerRect.top;
// 设置SVG的位置
svg.style.position = 'absolute';
svg.style.left = `${left}px`;
svg.style.top = `${top}px`;
}
// 切换身体部位并重新渲染点位
function switchBodyPart(bodyPart) {
// 腰部(waist)、肩膀(shoulder)和背部(back)都使用相同的图片和点位集
if (bodyPart === 'waist' || bodyPart === 'shoulder') {
bodyPart = 'back';
}
if (bodyPart === currentBodyPart) return; // 如果相同,不需要切换
// 更新当前身体部位和点位集合
currentBodyPart = bodyPart;
points = currentBodyPart === 'belly' ? bellyPoints :
currentBodyPart === 'leg' ? legPoints : backPoints;
// 更新图片源
img.src = `/static/images/smart_mode/${bodyPart}.png`;
// 清除当前活动点和图形
activePoints.forEach(point => point.classList.remove('active'));
activePoints.clear();
clearShape();
// 清除现有点位
while (svg.lastChild) {
if (svg.lastChild.tagName === 'defs') break; // 保留defs元素
svg.removeChild(svg.lastChild);
}
// 重新添加点位
Object.entries(points).forEach(([name, [x, y]], index) => {
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.classList.add('point-group');
group.setAttribute('data-name', name);
const touchTarget = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
touchTarget.classList.add('touch-target');
touchTarget.setAttribute('cx', x);
touchTarget.setAttribute('cy', y);
const pulse = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
pulse.classList.add('point-pulse');
pulse.setAttribute('cx', x);
pulse.setAttribute('cy', y);
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.classList.add('point');
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('filter', 'url(#glow)');
const { labelX, labelY } = calculateLabelPosition(x, y, index, Object.keys(points).length);
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.classList.add('label-line');
line.setAttribute('x1', x);
line.setAttribute('y1', y);
line.setAttribute('x2', labelX);
line.setAttribute('y2', labelY);
const textWidth = name.length * (window.innerWidth <= 768 ? 16 : 12) + 10;
const textHeight = window.innerWidth <= 768 ? 24 : 20;
const labelBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
labelBg.classList.add('label-bg');
// 根据点的位置决定标签背景的位置
const isLeftSide = x < 240;
const labelBgX = isLeftSide ? labelX - textWidth : labelX;
labelBg.setAttribute('x', labelBgX);
labelBg.setAttribute('y', labelY - textHeight/2);
labelBg.setAttribute('width', textWidth);
labelBg.setAttribute('height', textHeight);
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.classList.add('label');
// 标签文本居中
text.setAttribute('x', labelBgX + textWidth/2);
text.setAttribute('y', labelY);
text.textContent = name;
touchTarget.addEventListener('click', () => handlePointClick(group, name, x, y));
group.appendChild(pulse);
group.appendChild(line);
group.appendChild(labelBg);
group.appendChild(circle);
group.appendChild(text);
group.appendChild(touchTarget);
svg.appendChild(group);
});
// 更新SVG尺寸以适应新图片
img.onload = function() {
updateSVGSize();
// 延迟执行以确保图片已完全渲染
setTimeout(updateSVGSize, 100);
};
}
function handlePointClick(group, name, x, y) {
if (currentMode === 'none') {
// 普通模式下,点击高亮/取消高亮
if (activePoints.has(group)) {
group.classList.remove('active');
activePoints.delete(group);
} else {
group.classList.add('active');
activePoints.add(group);
}
} else if (currentMode === 'point') {
// 点按模式下,点击发送消息
channel.postMessage({
type: 'point_selected',
data: { name, x, y }
});
} else {
// 其他绘制模式下允许两个点同时激活
if (activePoints.size >= 2) {
// 如果已有两个点,清除所有点和形状,重新开始
activePoints.forEach(point => point.classList.remove('active'));
activePoints.clear();
clearShape();
}
// 添加新点
group.classList.add('active');
activePoints.add(group);
// 发送选中点的信息
channel.postMessage({
type: 'point_selected',
data: { name, x, y }
});
}
}
// 清除当前绘制的形状
function clearShape() {
// 移除当前形状
if (currentShape) {
currentShape.remove();
currentShape = null;
}
// 移除所有临时点
const tempPoints = document.querySelectorAll('.point-group[data-temp="true"]');
tempPoints.forEach(point => {
svg.removeChild(point);
});
// 移除高亮状态
activePoints.forEach(point => {
point.classList.remove('active');
});
activePoints.clear();
}
// 绘制直线
function drawLine(point1, point2) {
clearShape();
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.classList.add('path-shape'); // 添加class方便清理
// 计算线段中点
const midX = (point1.x + point2.x) / 2;
const midY = (point1.y + point2.y) / 2;
// 计算方向向量
const dx = point2.x - point1.x;
const dy = point2.y - point1.y;
const len = Math.sqrt(dx * dx + dy * dy);
const nx = dx / len;
const ny = dy / len;
// 绘制主线条
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', point1.x);
line.setAttribute('y1', point1.y);
line.setAttribute('x2', point2.x);
line.setAttribute('y2', point2.y);
line.setAttribute('stroke', 'rgba(255, 64, 129, 0.8)');
line.setAttribute('stroke-width', '3');
line.setAttribute('filter', 'url(#glow)');
// 在中点添加箭头
const arrowSize = 8;
const arrowWidth = arrowSize / 1.85;
const tipX = midX + arrowSize * nx;
const tipY = midY + arrowSize * ny;
const backX = midX - arrowSize * nx;
const backY = midY - arrowSize * ny;
const leftX = backX - arrowWidth * ny;
const leftY = backY + arrowWidth * nx;
const rightX = backX + arrowWidth * ny;
const rightY = backY - arrowWidth * nx;
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const arrowPathData = `M ${leftX} ${leftY} L ${tipX} ${tipY} L ${rightX} ${rightY}`;
arrow.setAttribute("d", arrowPathData);
arrow.setAttribute('fill', 'none');
arrow.setAttribute('stroke', 'rgba(255, 64, 129, 0.8)');
arrow.setAttribute('stroke-width', '3');
arrow.setAttribute('filter', 'url(#glow)');
group.appendChild(line);
group.appendChild(arrow);
svg.appendChild(group);
currentShape = group;
}
// 绘制椭圆
function drawEllipse(point1, point2, params) {
clearShape();
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.classList.add('path-shape'); // 添加class方便清理
// 计算椭圆参数
const centerX = (point1.x + point2.x) / 2;
const centerY = (point1.y + point2.y) / 2;
const a = Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2)) / 2;
const b = params.width; // 直接使用像素值
const angle = Math.atan2(point2.y - point1.y, point2.x - point1.x);
const points = Math.round((params.cycles + 1) * 40);
// 创建主路径
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
let pathData = '';
// 存储箭头位置的点
const arrowPoints = [];
const numArrows = params.cycles * 2 + 1; // 每圈2个箭头
// 生成椭圆路径
for (let i = 0; i <= points; i++) {
const t = (i / points) * (2 * Math.PI * (params.cycles + 1));
const direction = params.direction === 'CCW' ? -1 : 1;
const x = a * Math.cos(direction * t);
const y = b * Math.sin(direction * t);
const rotatedX = centerX + x * Math.cos(angle) - y * Math.sin(angle);
const rotatedY = centerY + x * Math.sin(angle) + y * Math.cos(angle);
if (i === 0) {
pathData += `M ${rotatedX} ${rotatedY}`;
} else {
pathData += ` L ${rotatedX} ${rotatedY}`;
}
// 在特定间隔添加箭头位置
if (i % Math.floor(points / numArrows) === 0 && arrowPoints.length < numArrows) {
// 计算切线方向,考虑椭圆宽度
const dx = -a * Math.sin(direction * t);
const dy = b * Math.cos(direction * t);
// 旋转切线,并根据方向调整
const rotatedDx = (dx * Math.cos(angle) - dy * Math.sin(angle)) * direction;
const rotatedDy = (dx * Math.sin(angle) + dy * Math.cos(angle)) * direction;
// 归一化方向向量
const len = Math.sqrt(rotatedDx * rotatedDx + rotatedDy * rotatedDy);
arrowPoints.push({
x: rotatedX,
y: rotatedY,
dx: rotatedDx / len,
dy: rotatedDy / len
});
}
}
path.setAttribute('d', pathData);
path.setAttribute('fill', 'none');
path.setAttribute('stroke', 'rgba(255, 64, 129, 0.8)');
path.setAttribute('stroke-width', '3');
path.setAttribute('filter', 'url(#glow)');
group.appendChild(path);
// 添加箭头
arrowPoints.forEach((point, index) => {
if (index === 0) return; // 跳过起点
const arrowSize = 8;
const arrowWidth = arrowSize / 1.85;
// 计算箭头中心点(在曲线上)
const arrowX = point.x;
const arrowY = point.y;
// 归一化方向向量
const len = Math.sqrt(point.dx * point.dx + point.dy * point.dy);
const nx = point.dx / len;
const ny = point.dy / len;
// 计算箭头三个点
const tipX = arrowX + arrowSize * nx;
const tipY = arrowY + arrowSize * ny;
const backX = arrowX - arrowSize * nx;
const backY = arrowY - arrowSize * ny;
const leftX = backX - arrowWidth * ny;
const leftY = backY + arrowWidth * nx;
const rightX = backX + arrowWidth * ny;
const rightY = backY - arrowWidth * nx;
// 创建箭头
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const arrowPathData = `M ${leftX} ${leftY} L ${tipX} ${tipY} L ${rightX} ${rightY}`;
arrow.setAttribute("d", arrowPathData);
arrow.setAttribute('fill', 'none');
arrow.setAttribute('stroke', 'rgba(255, 64, 129, 0.8)');
arrow.setAttribute('stroke-width', '3');
arrow.setAttribute('filter', 'url(#glow)');
group.appendChild(arrow);
});
svg.appendChild(group);
currentShape = group;
}
// 绘制8字形
function drawLemniscate(point1, point2, params) {
clearShape();
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.classList.add('path-shape'); // 添加class方便清理
// 计算8字形参数
const centerX = (point1.x + point2.x) / 2;
const centerY = (point1.y + point2.y) / 2;
const a = Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2)) / 2;
const b = params.width / 2; // 使用宽度的一半作为8字形的高度
const angle = Math.atan2(point2.y - point1.y, point2.x - point1.x);
const points = Math.round((params.cycles + 1) * 40);
// 创建主路径
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
let pathData = '';
// 存储箭头位置的点
const arrowPoints = [];
const numArrows = params.cycles * 2 + 1; // 每圈2个箭头
// 生成8字形路径 (根据Gerono卵形线)
for (let i = 0; i <= points; i++) {
const t = (i / points) * (2 * Math.PI * (params.cycles + 1)) + Math.PI / 2;
const direction = params.direction === 'CCW' ? -1 : 1;
// 使用Gerono卵形线公式
const x = a * Math.sin(t);
const y = direction * b * Math.sin(2 * t);
// 旋转和平移
const rotatedX = centerX + x * Math.cos(angle) - y * Math.sin(angle);
const rotatedY = centerY + x * Math.sin(angle) + y * Math.cos(angle);
if (i === 0) {
pathData += `M ${rotatedX} ${rotatedY}`;
} else {
pathData += ` L ${rotatedX} ${rotatedY}`;
}
// 在特定间隔添加箭头位置
if (i % Math.floor(points / numArrows) === 0 && arrowPoints.length < numArrows) {
// 计算切线方向
const dx = a * Math.cos(t);
const dy = direction * 2 * b * Math.cos(2 * t);
// 旋转切线
const rotatedDx = dx * Math.cos(angle) - dy * Math.sin(angle);
const rotatedDy = dx * Math.sin(angle) + dy * Math.cos(angle);
// 归一化方向向量
const len = Math.sqrt(rotatedDx * rotatedDx + rotatedDy * rotatedDy);
arrowPoints.push({
x: rotatedX,
y: rotatedY,
dx: rotatedDx / len,
dy: rotatedDy / len
});
}
}
path.setAttribute('d', pathData);
path.setAttribute('fill', 'none');
path.setAttribute('stroke', 'rgba(255, 64, 129, 0.8)');
path.setAttribute('stroke-width', '3');
path.setAttribute('filter', 'url(#glow)');
group.appendChild(path);
// 添加箭头,与椭圆相同
arrowPoints.forEach((point, index) => {
if (index === 0) return; // 跳过起点
const arrowSize = 8;
const arrowWidth = arrowSize / 1.85;
// 计算箭头中心点(在曲线上)
const arrowX = point.x;
const arrowY = point.y;
// 方向向量已经归一化
const nx = point.dx;
const ny = point.dy;
// 计算箭头三个点
const tipX = arrowX + arrowSize * nx;
const tipY = arrowY + arrowSize * ny;
const backX = arrowX - arrowSize * nx;
const backY = arrowY - arrowSize * ny;
const leftX = backX - arrowWidth * ny;
const leftY = backY + arrowWidth * nx;
const rightX = backX + arrowWidth * ny;
const rightY = backY - arrowWidth * nx;
// 创建箭头
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const arrowPathData = `M ${leftX} ${leftY} L ${tipX} ${tipY} L ${rightX} ${rightY}`;
arrow.setAttribute("d", arrowPathData);
arrow.setAttribute('fill', 'none');
arrow.setAttribute('stroke', 'rgba(255, 64, 129, 0.8)');
arrow.setAttribute('stroke-width', '3');
arrow.setAttribute('filter', 'url(#glow)');
group.appendChild(arrow);
});
svg.appendChild(group);
currentShape = group;
}
// 绘制摆线
function drawCycloid(point1, point2, params) {
clearShape();
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.classList.add('path-shape'); // 添加class方便清理
// 计算点之间的距离和角度
const dx = point2.x - point1.x;
const dy = point2.y - point1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
// 参数设置匹配Python代码
const a_prolate = 4.5; // 延长系数
const radius = 30.0; // 基础半径
const r_cycloid = radius / (2 * a_prolate); // 摆线半径
const step_degree = Math.PI / 12; // 步长
// 生成摆线路径
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
// 生成摆线点
const cycles = params.cycles;
// 修改起始和结束角度确保起点和终点为选定点
const startTheta = 0.5 * Math.PI;
const endTheta = startTheta + cycles * 2 * Math.PI + Math.PI;
const totalRange = endTheta - startTheta;
const totalSteps = Math.round(totalRange / step_degree);
// 计算缩放系数确保曲线刚好从起点到终点
const temp_k = distance / (r_cycloid * (Math.PI + 2 * a_prolate + 2 * Math.PI * cycles));
let d = '';
let firstX = null, firstY = null;
// 存储箭头位置的点
const arrowPoints = [];
const numArrows = 2; // 只需要2个箭头起点和终点
for (let i = 0; i <= totalSteps; i++) {
// 计算当前角度
const progress = i / totalSteps;
const t = startTheta + progress * totalRange;
const theta = params.direction === 'CW' ? -t : t;
// 延长摆线参数方程
let x = r_cycloid * (theta - a_prolate * Math.sin(theta)) * temp_k;
let y = r_cycloid * (1 - a_prolate * Math.cos(theta));
// 根据宽度缩放y值
y = (y / 30) * params.width;
if (params.direction === 'CW') {
x = -x;
y = -y;
}
// 旋转
const rotatedX = x * Math.cos(angle) - y * Math.sin(angle);
const rotatedY = x * Math.sin(angle) + y * Math.cos(angle);
if (i === 0) {
// 记录第一个点的偏移量
firstX = rotatedX;
firstY = rotatedY;
}
// 平移,确保起点对齐
const finalX = point1.x + (rotatedX - firstX);
const finalY = point1.y + (rotatedY - firstY);
if (i === 0) {
d = `M ${finalX},${finalY}`;
} else {
d += ` L${finalX},${finalY}`;
}
// 只在起点附近10%位置和终点附近90%位置)添加箭头
if ((i === Math.floor(totalSteps * 0.075) && arrowPoints.length === 0) ||
(i === Math.floor(totalSteps * 0.975) && arrowPoints.length === 1)) {
// 计算切线方向
const direction = params.direction === 'CW' ? -1 : 1;
// 使用参数方程的导数作为切线方向
const dx = direction * r_cycloid * (1 - a_prolate * Math.cos(theta)) * temp_k;
const dy = direction * r_cycloid * a_prolate * Math.sin(theta);
// 旋转切线
const rotatedDx = dx * Math.cos(angle) - dy * Math.sin(angle);
const rotatedDy = dx * Math.sin(angle) + dy * Math.cos(angle);
// 归一化方向向量
const len = Math.sqrt(rotatedDx * rotatedDx + rotatedDy * rotatedDy);
arrowPoints.push({
x: finalX,
y: finalY,
dx: rotatedDx / len,
dy: rotatedDy / len
});
}
}
path.setAttribute("d", d);
path.setAttribute("fill", "none");
path.setAttribute("stroke", "rgba(255, 64, 129, 0.8)");
path.setAttribute("stroke-width", "3");
path.setAttribute("filter", "url(#glow)");
group.appendChild(path);
// 添加箭头
arrowPoints.forEach(point => {
const arrowSize = 8;
const arrowWidth = arrowSize / 1.85;
// 计算箭头三个点
const tipX = point.x + arrowSize * point.dx;
const tipY = point.y + arrowSize * point.dy;
const backX = point.x - arrowSize * point.dx;
const backY = point.y - arrowSize * point.dy;
const leftX = backX - arrowWidth * point.dy;
const leftY = backY + arrowWidth * point.dx;
const rightX = backX + arrowWidth * point.dy;
const rightY = backY - arrowWidth * point.dx;
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const arrowPathData = `M ${leftX} ${leftY} L ${tipX} ${tipY} L ${rightX} ${rightY}`;
arrow.setAttribute('d', arrowPathData);
arrow.setAttribute('fill', 'none');
arrow.setAttribute('stroke', 'rgba(255, 64, 129, 0.8)');
arrow.setAttribute('stroke-width', '3');
arrow.setAttribute('filter', 'url(#glow)');
group.appendChild(arrow);
});
svg.appendChild(group);
currentShape = group;
return group;
}
// 绘制内螺旋
function drawInSpiral(point1, point2, params) {
clearShape();
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.classList.add('path-shape'); // 添加class方便清理
// 计算点之间的距离和角度
const dx = point2.x - point1.x;
const dy = point2.y - point1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const cycles = params.cycles;
const points = Math.round(cycles * 2 * Math.PI / (Math.PI / 9));
// 生成螺旋线路径
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
let d = '';
// 存储箭头位置的点
const arrowPoints = [];
const numArrows = Math.min(params.cycles * 2, 8); // 限制最大箭头数量为8个
// 计算缩放系数
const tempK = Math.abs(params.width / (distance * ((2 * cycles - 0.75) / cycles)));
// 生成螺旋线点
for (let i = 0; i <= points; i++) {
const t = (i / points) * (2 * Math.PI * cycles);
const r = (i / points) * distance;
// 计算螺旋线的x, y坐标
let x, y;
if (params.direction === 'CW') {
x = r * Math.cos(-t);
y = r * Math.sin(-t) * tempK;
} else {
x = r * Math.cos(t);
y = r * Math.sin(t) * tempK;
}
// 旋转和平移
const rotatedX = x * Math.cos(angle) - y * Math.sin(angle);
const rotatedY = x * Math.sin(angle) + y * Math.cos(angle);
const finalX = rotatedX + point1.x;
const finalY = rotatedY + point1.y;
if (i === 0) {
d = `M ${finalX},${finalY}`;
} else {
d += ` L${finalX},${finalY}`;
}
// 在特定间隔添加箭头位置
if (i % Math.floor(points / numArrows) === 0 && arrowPoints.length < numArrows && i > points * 0.1) {
// 计算切线方向
const direction = params.direction === 'CCW' ? -1 : 1;
const dx = direction * (-r * Math.sin(t));
const dy = direction * (r * Math.cos(t)) * tempK;
// 旋转切线
const rotatedDx = dx * Math.cos(angle) - dy * Math.sin(angle);
const rotatedDy = dx * Math.sin(angle) + dy * Math.cos(angle);
// 归一化方向向量
const len = Math.sqrt(rotatedDx * rotatedDx + rotatedDy * rotatedDy);
arrowPoints.push({
x: finalX,
y: finalY,
dx: rotatedDx / len,
dy: rotatedDy / len
});
}
}
path.setAttribute('d', d);
path.setAttribute('fill', 'none');
path.setAttribute('stroke', 'rgba(255, 64, 129, 0.8)');
path.setAttribute('stroke-width', '3');
path.setAttribute('filter', 'url(#glow)');
group.appendChild(path);
// // 添加箭头
// arrowPoints.forEach(point => {
// const arrowSize = 8;
// const arrowWidth = arrowSize / 1.85;
// // 计算箭头三个点
// const tipX = point.x + arrowSize * point.dx;
// const tipY = point.y + arrowSize * point.dy;
// const backX = point.x - arrowSize * point.dx;
// const backY = point.y - arrowSize * point.dy;
// const leftX = backX - arrowWidth * point.dy;
// const leftY = backY + arrowWidth * point.dx;
// const rightX = backX + arrowWidth * point.dy;
// const rightY = backY - arrowWidth * point.dx;
// const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
// const arrowPathData = `M ${leftX} ${leftY} L ${tipX} ${tipY} L ${rightX} ${rightY}`;
// arrow.setAttribute('d', arrowPathData);
// arrow.setAttribute('fill', 'none');
// arrow.setAttribute('stroke', 'rgba(255, 64, 129, 0.8)');
// arrow.setAttribute('stroke-width', '3');
// arrow.setAttribute('filter', 'url(#glow)');
// group.appendChild(arrow);
// });
svg.appendChild(group);
currentShape = group;
}
// 绘制外螺旋
function drawOutSpiral(point1, point2, params) {
clearShape();
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.classList.add('path-shape'); // 添加class方便清理
// 计算点之间的距离和角度
const dx = point2.x - point1.x;
const dy = point2.y - point1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const cycles = params.cycles;
const points = Math.round(cycles * 2 * Math.PI / (Math.PI / 36));
// 生成螺旋线路径
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
let d = '';
// 存储箭头位置的点
const arrowPoints = [];
const numArrows = Math.min(params.cycles * 2, 8); // 限制最大箭头数量为8个
// 计算缩放系数
const tempK = Math.abs(params.width / (distance * ((2 * cycles - 0.75) / cycles)));
// 生成螺旋线点
for (let i = 0; i <= points; i++) {
const progress = i / points;
const t = progress * (2 * Math.PI * cycles + Math.PI);
const r = distance * (1 - progress);
// 计算螺旋线的x, y坐标
let x, y;
if (params.direction === 'CW') {
x = r * Math.cos(-t);
y = r * Math.sin(-t) * tempK;
} else {
x = r * Math.cos(t);
y = r * Math.sin(t) * tempK;
}
// 旋转和平移
const rotatedX = x * Math.cos(angle) - y * Math.sin(angle);
const rotatedY = x * Math.sin(angle) + y * Math.cos(angle);
const finalX = rotatedX + point1.x;
const finalY = rotatedY + point1.y;
if (i === 0) {
d = `M ${finalX},${finalY}`;
} else {
d += ` L${finalX},${finalY}`;
}
// 在特定间隔添加箭头位置
if (i % Math.floor(points / numArrows) === 0 && arrowPoints.length < numArrows && i < points * 0.9) {
// 计算切线方向
const direction = params.direction === 'CCW' ? -1 : 1;
// 对于外螺旋,使用简化的切线计算方法
// 由于外螺旋从外向内,切线方向与内螺旋相反
// 使用简单的参数导数近似切线方向
let dx, dy;
// 计算下一个点的位置来确定方向
const nextT = t + (2 * Math.PI * cycles) / points;
const nextR = distance * (1 - (i + 1) / points);
let nextX, nextY;
if (params.direction === 'CCW') {
nextX = nextR * Math.cos(-nextT);
nextY = nextR * Math.sin(-nextT) * tempK;
} else {
nextX = nextR * Math.cos(nextT);
nextY = nextR * Math.sin(nextT) * tempK;
}
// 计算旋转后的下一个点坐标
const nextRotatedX = nextX * Math.cos(angle) - nextY * Math.sin(angle);
const nextRotatedY = nextX * Math.sin(angle) + nextY * Math.cos(angle);
const nextFinalX = nextRotatedX + point1.x;
const nextFinalY = nextRotatedY + point1.y;
// 计算切线方向为当前点指向下一个点的向量
dx = nextFinalX - finalX;
dy = nextFinalY - finalY;
// 归一化方向向量
const len = Math.sqrt(dx * dx + dy * dy);
arrowPoints.push({
x: finalX,
y: finalY,
dx: dx / len,
dy: dy / len
});
}
}
path.setAttribute('d', d);
path.setAttribute('fill', 'none');
path.setAttribute('stroke', 'rgba(255, 64, 129, 0.8)');
path.setAttribute('stroke-width', '3');
path.setAttribute('filter', 'url(#glow)');
group.appendChild(path);
// // 添加箭头
// arrowPoints.forEach(point => {
// const arrowSize = 8;
// const arrowWidth = arrowSize / 1.85;
// // 计算箭头三个点
// const tipX = point.x + arrowSize * point.dx;
// const tipY = point.y + arrowSize * point.dy;
// const backX = point.x - arrowSize * point.dx;
// const backY = point.y - arrowSize * point.dy;
// const leftX = backX - arrowWidth * point.dy;
// const leftY = backY + arrowWidth * point.dx;
// const rightX = backX + arrowWidth * point.dy;
// const rightY = backY - arrowWidth * point.dx;
// const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
// const arrowPathData = `M ${leftX} ${leftY} L ${tipX} ${tipY} L ${rightX} ${rightY}`;
// arrow.setAttribute('d', arrowPathData);
// arrow.setAttribute('fill', 'none');
// arrow.setAttribute('stroke', 'rgba(255, 64, 129, 0.8)');
// arrow.setAttribute('stroke-width', '3');
// arrow.setAttribute('filter', 'url(#glow)');
// group.appendChild(arrow);
// });
svg.appendChild(group);
currentShape = group;
}
// 修改发光滤镜效果
const defs = svg.querySelector('defs');
const glowFilter = defs.querySelector('#glow');
glowFilter.innerHTML = `
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
`;
// 监听来自控制页面的消息
channel.onmessage = (event) => {
const { type, mode, points: pointsData, params } = event.data;
// 添加处理body_part变更的逻辑
if (type === 'change_body_part') {
switchBodyPart(event.data.body_part);
return;
}
if (type === 'mode_change') {
currentMode = mode;
activePoints.forEach(point => point.classList.remove('active'));
activePoints.clear();
clearShape();
}
else if (type === 'clear_points') {
activePoints.forEach(point => point.classList.remove('active'));
activePoints.clear();
clearShape();
}
else if (type === 'draw_shape') {
// 先清除现有的形状
if (mode !== 'point') {
clearShape();
}
if (mode === 'line') {
drawLine(pointsData[0], pointsData[1]);
}
else if (mode === 'ellipse') {
drawEllipse(pointsData[0], pointsData[1], params);
}
else if (mode === 'lemniscate') {
drawLemniscate(pointsData[0], pointsData[1], params);
}
else if (mode === 'cycloid') {
drawCycloid(pointsData[0], pointsData[1], params);
}
else if (mode === 'in_spiral') {
// drawInSpiral(pointsData[0], pointsData[1], params);
drawInSpiral(pointsData[1], pointsData[0], params);
}
else if (mode === 'out_spiral') {
drawOutSpiral(pointsData[0], pointsData[1], params);
}
// 对于point模式确保选中的点被高亮
if (pointsData && pointsData.length > 0) {
// 遍历所有点,找到名称匹配的并高亮
pointsData.forEach(point => {
if (point && point.name) {
const pointName = point.name;
// 检查是否为复合点(包含&符号)
if (pointName.includes('&')) {
// 为复合点创建临时可视化元素
createTempPointVisualization(point);
} else {
// 常规穴位的处理
const pointGroups = Array.from(document.querySelectorAll('.point-group'));
const targetGroup = pointGroups.find(group =>
group.getAttribute('data-name') === pointName
);
if (targetGroup) {
targetGroup.classList.add('active');
activePoints.add(targetGroup);
}
}
}
});
}
}
else if (type === 'get_points_coordinates') {
// 处理获取穴位坐标的请求
const requestedPoints = event.data.points;
const coordinates = {};
requestedPoints.forEach(pointName => {
// 检查是否为复合点(包含&符号的都视为复合点)
if (pointName && pointName.includes('&')) {
try {
let point1, point2, ratio = 0.5; // 默认比例为0.5(中点)
// 处理带@的复合点
if (pointName.includes('@')) {
// 解析复合点格式
const [pointsPart, ratioPart] = pointName.split('@');
[point1, point2] = pointsPart.split('&');
// 正确解析比例:处理分数形式如"1/4"
if (ratioPart.includes('/')) {
const [numerator, denominator] = ratioPart.split('/');
ratio = parseFloat(numerator) / parseFloat(denominator);
} else {
ratio = parseFloat(ratioPart);
}
} else {
// 不带@的复合点直接拆分两个穴位名使用默认比例0.5
[point1, point2] = pointName.split('&');
}
// 检查两个基础穴位是否存在
if (points[point1] && points[point2]) {
// 获取两个穴位的坐标
const p1 = points[point1];
const p2 = points[point2];
// 计算复合点坐标(线性插值)
const x = p1[0] + (p2[0] - p1[0]) * ratio;
const y = p1[1] + (p2[1] - p1[1]) * ratio;
// 保存计算的坐标
coordinates[pointName] = [x, y];
// 调试输出
console.log(`复合点计算: ${pointName}`);
console.log(`- 点1(${point1}): ${p1[0]}, ${p1[1]}`);
console.log(`- 点2(${point2}): ${p2[0]}, ${p2[1]}`);
console.log(`- 比例: ${ratio}`);
console.log(`- 结果: ${x}, ${y}`);
} else {
console.error(`找不到基础穴位: ${point1}${point2}`);
}
} catch (e) {
console.error(`无法解析复合点: ${pointName}`, e);
}
} else if (points[pointName]) {
// 处理常规穴位
coordinates[pointName] = points[pointName];
} else {
console.warn(`找不到穴位: ${pointName}`);
}
});
// 发送坐标数据回去
channel.postMessage({
type: 'points_coordinates',
data: coordinates
});
}
};
img.onload = function() {
updateSVGSize();
// 添加图片加载完成后的处理
setTimeout(updateSVGSize, 100); // 延迟执行以确保图片已完全渲染
};
// 监听窗口大小变化
window.addEventListener('resize', function() {
updateSVGSize();
});
// 监听图片尺寸变化
const resizeObserver = new ResizeObserver(function() {
updateSVGSize();
});
resizeObserver.observe(img);
// 添加穴位点
Object.entries(points).forEach(([name, [x, y]], index) => {
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.classList.add('point-group');
// 添加data-name属性便于后续通过名称查找
group.setAttribute('data-name', name);
// 添加更大的触摸区域
const touchTarget = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
touchTarget.classList.add('touch-target');
touchTarget.setAttribute('cx', x);
touchTarget.setAttribute('cy', y);
// 添加脉冲动画圆圈
const pulse = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
pulse.classList.add('point-pulse');
pulse.setAttribute('cx', x);
pulse.setAttribute('cy', y);
// 添加主要点
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.classList.add('point');
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('filter', 'url(#glow)');
const { labelX, labelY } = calculateLabelPosition(x, y, index, Object.keys(points).length);
// 添加连接线
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.classList.add('label-line');
line.setAttribute('x1', x);
line.setAttribute('y1', y);
line.setAttribute('x2', labelX);
line.setAttribute('y2', labelY);
// 添加标签背景
const textWidth = name.length * (window.innerWidth <= 768 ? 16 : 12) + 20;
const textHeight = window.innerWidth <= 768 ? 24 : 20;
const labelBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
labelBg.classList.add('label-bg');
// 根据点的位置决定标签背景的位置
const isLeftSide = x < 240;
const labelBgX = isLeftSide ? labelX - textWidth : labelX;
labelBg.setAttribute('x', labelBgX);
labelBg.setAttribute('y', labelY - textHeight/2);
labelBg.setAttribute('width', textWidth);
labelBg.setAttribute('height', textHeight);
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.classList.add('label');
// 标签文本居中
text.setAttribute('x', labelBgX + textWidth/2);
text.setAttribute('y', labelY);
text.textContent = name;
// 修改点击事件处理
touchTarget.addEventListener('click', () => handlePointClick(group, name, x, y));
group.appendChild(pulse);
group.appendChild(line);
group.appendChild(labelBg);
group.appendChild(circle);
group.appendChild(text);
group.appendChild(touchTarget);
svg.appendChild(group);
});
// 点击背景时的处理
svg.addEventListener('click', (event) => {
if (event.target === svg) {
if (activePoints.size > 0) {
activePoints.forEach(point => point.classList.remove('active'));
activePoints.clear();
}
}
});
updateSVGSize();
}
// 创建临时点可视化元素函数
function createTempPointVisualization(point) {
// 获取svg元素
const svg = document.querySelector('svg');
if (!svg) {
console.error('找不到SVG元素');
return null;
}
// 移除之前可能存在的相同名称的临时元素
removeTempPoint(point.name);
// 创建临时点元素组
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.classList.add('point-group', 'active');
group.setAttribute('data-name', point.name);
group.setAttribute('data-temp', 'true');
// 添加更大的触摸区域
const touchTarget = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
touchTarget.classList.add('touch-target');
touchTarget.setAttribute('cx', point.x);
touchTarget.setAttribute('cy', point.y);
touchTarget.setAttribute('r', '15');
// 添加脉冲动画圆圈
const pulse = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
pulse.classList.add('point-pulse');
pulse.setAttribute('cx', point.x);
pulse.setAttribute('cy', point.y);
// 添加主要点
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.classList.add('point');
circle.setAttribute('cx', point.x);
circle.setAttribute('cy', point.y);
circle.setAttribute('filter', 'url(#glow)');
// 将所有元素添加到组
group.appendChild(pulse);
group.appendChild(circle);
group.appendChild(touchTarget);
// 添加到SVG
const lastChild = svg.lastElementChild;
if (lastChild) {
svg.insertBefore(group, lastChild);
} else {
svg.appendChild(group);
}
// 添加到活动点集合
activePoints.add(group);
return group;
}
// 移除临时点函数
function removeTempPoint(pointName) {
const svg = document.querySelector('svg');
if (!svg) {
console.error('找不到SVG元素');
return;
}
const existingTemp = document.querySelector(`.point-group[data-name="${pointName}"][data-temp="true"]`);
if (existingTemp) {
svg.removeChild(existingTemp);
// 如果在activePoints中也需要移除
activePoints.forEach(point => {
if (point.getAttribute('data-name') === pointName &&
point.getAttribute('data-temp') === 'true') {
activePoints.delete(point);
}
});
}
}
document.addEventListener('DOMContentLoaded', initializePoints);
</script>
</body>
</html>