1508 lines
69 KiB
HTML
1508 lines
69 KiB
HTML
<!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> |