// let lang = localStorage.getItem("selectedLanguage");
const partSelections = document.querySelectorAll(".operate-button-item");
const headerButtons = document.querySelectorAll(".header-button-item");
const selectedData = {}; // 用于存储选中的部位信息
const imgContainer = document.getElementById("massage-img-box");
const imgWidth = imgContainer.offsetWidth;
const imgHeight = imgContainer.offsetHeight;
const strengthCard = document.querySelector("#strength-card");
const temperatureCard = document.querySelector("#temperature-card");
const shakeCard = document.querySelector("#shake-card");
const currentCard = document.querySelector("#current-card");
const frequencyCard = document.querySelector("#frequency-card");
const pressCard = document.querySelector("#press-card");
const speedCard = document.querySelector("#speed-card");
const directionCard = document.querySelector("#direction-card");
const highCard = document.querySelector("#high-card");
const radioSwitch = document.querySelector(".radio-switch");
const radioSlideToggle = radioSwitch.querySelector(
".radio-slide-toggle"
);
const manualSwitch = document.querySelector(".manual-switch");
const manualSlideToggle = manualSwitch.querySelector(
".manual-slide-toggle"
);
const commonSwitch = document.querySelector(".common-switch");
const commonSlideToggle = commonSwitch.querySelector(
".common-slide-toggle"
);
const timeDecreaseBtn = document.getElementById('time-decrease-btn');
const timeIncreaseBtn = document.getElementById('time-increase-btn');
const decreaseBtn = document.getElementById('decrease-btn');
const increaseBtn = document.getElementById('increase-btn');
const temperDecreaseBtn = document.getElementById('temper-decrease-btn');
const temperIncreaseBtn = document.getElementById('temper-increase-btn');
const startButton = document.querySelector("#start-btn");
const prev2Btn = document.querySelector("#prev2");
const photoBtn = document.querySelector("#photo");
const reSelectBtn = document.querySelector("#re-select");
const getAcupointBtn = document.getElementById("get-acupoint-btn");
const acupunctureBtn = document.querySelector("#acupuncture-btn")
const acupunctureName = document.querySelector("#acupuncture-name");
const massagePlanText = document.querySelector("#massage-plan-text")
const addMassage = document.querySelector("#add-massage")
let startHint = false; // 控制开始是否显示弹窗
const preHeat = document.querySelector("#pre-heat");
const tempInputBox = document.querySelector("#temp-input-box");
const stoneTime = document.querySelector("#stone-time");
const stoneVal = document.querySelector("#stone-val");
const skipMassage = document.querySelector("#skip-massage")
let nowStep = 1;
setStepBox(nowStep);
// let timer = null;
let timeArr = [
{
txt: "15分钟",
value: 900
},
{
txt: "30分钟",
value: 1800
},
{
txt: "45分钟",
value: 2700
},
{
txt: "1小时",
value: 3600
},
{
txt: "1小时30分钟",
value: 5400
}, ,
{
txt: "2小时",
value: 7200
}
]
// 初始的时间选择下标
let timeIndex = 0;
async function getCommand() {
try {
// 调用 /get_command 接口
const response = await fetch('/get_command', { method: 'GET' });
// 解析 JSON 数据
const data = await response.json();
// 如果成功,返回命令,否则返回失败消息
if (data.status === "success") {
return data.command;
} else {
return "Failed to retrieve command.";
}
} catch (error) {
// 捕获并返回错误
console.error("请求失败:", error);
return "Error fetching command.";
}
}
document.addEventListener("DOMContentLoaded", function () {
// 初始化脖子点为隐藏状态
if (document.querySelector(".neck-point-left")) {
document.querySelector(".neck-point-left").style.display = "none";
}
if (document.querySelector(".neck-point-right")) {
document.querySelector(".neck-point-right").style.display = "none";
}
if (document.querySelector(".neck-point-bottom-left")) {
document.querySelector(".neck-point-bottom-left").style.display = "none";
}
if (document.querySelector(".neck-point-bottom-right")) {
document.querySelector(".neck-point-bottom-right").style.display = "none";
}
if (document.querySelector("#ojo-point-box")) {
document.querySelector("#ojo-point-box").style.display = "none";
}
// 初始化线条拖动功能
// initLineContainer();
socket.on("update_stone_status", (data) => {
if (data.temper_head !== null) {
if(selectedData.selectedHead === 'stone') {
startPreHeat(data.temper_head, data.temperature);
}
} else {
stopPreHeat();
}
})
socket.on("update_massage_status", function (data) {
updateStatusDisplay(data);
});
// 获取图片的宽高
socket.on("change_image", function (data) {
console.log(data, 'change_image')
let path;
// let imageArr = ["static/images/smart_mode/back.png", "static/images/smart_mode/belly.png", "static/images/smart_mode/leg.png"];
if (data.path === "static/images/smart_mode/back.png") {
let partVal = selectedData.selectedPart;
path = (partVal === 'leg' || partVal === 'belly') ? `static/images/smart_mode/${partVal}.png` : `static/images/smart_mode/back.png`;
// if (selectedData.selectedPart && selectedData.selectedPart === "belly") {
// path = imageArr[1];
// } else if (selectedData.selectedPart && selectedData.selectedPart === "leg") {
// path = imageArr[2];
// } else {
// path = imageArr[0]
// }
} else {
path = data.path;
}
let type = data.type ? parseInt(data.type) : null;
const newSrc = `${path.split('?')[0]}?t=${Date.now()}`;
// 使用懒加载模式优化视觉过渡
const tempImg = new Image();
tempImg.src = newSrc;
tempImg.onload = async () => {
// 预加载完成后切换图片
imgContainer.src = newSrc;
// 可选:添加 CSS 过渡效果
// imgContainer.style.opacity = 0;
// setTimeout(() => imgContainer.style.opacity = 1, 50);
if (type === 1) {
const command = await getCommand();
if (command !== null && command !== 'None') {
// transitionTo("step" + nowStep, "step3", 3);
const commandArr = command.split(':');
const head = commandArr[1];
const bodyPart = commandArr[3];
if (selectedData.selectedPart == null) {
selectedData.selectedPart = bodyPart;
}
if (selectedData.selectedHead == null) {
selectedData.selectedHead = head;
// selectedData.selectedHeadText = formatHeadName(head);
}
}
if (nowStep !== 3 && nowStep !== 4) {
transitionTo("step" + nowStep, "step3", 3);
// transitionTo("step" + nowStep, "step5", 5);
}
reSelectBtn.style.opacity = 1;
}
// type 1: 首次拍摄的图片;
// type 2: 重拍的图片;
// type 3: 带穴位的图片;
// type 4: 按摩中的图片;
if (type === 3) {
startButton.style.opacity = 1;
} else {
startButton.style.opacity = 0.4;
}
};
});
partSelections.forEach((item) => {
item.addEventListener("click", function () {
// 清除其他按钮的选中状态
partSelections.forEach((btn) => {
btn.classList.remove("active"); // 移除选中样式
const img = btn.querySelector("img");
// 仅在需要时替换回普通图片
if (!img.src.includes("_normal-btn.png")) {
img.src = img.src.replace("-btn.png", "_normal-btn.png");
}
});
// 添加当前按钮的选中状态
this.classList.add("active");
const img = this.querySelector("img");
// 仅在需要时替换为选中图片
if (img.src.includes("_normal-btn.png")) {
img.src = img.src.replace("_normal-btn.png", "-btn.png");
}
// 保存选中的部位
selectedData.selectedPart = this.getAttribute("data-part");
setHeadData(this.getAttribute("data-part"));
transitionTo("step1", "step2", 2);
// let part = formatPartName(selectedData.selectedPart);
});
});
// 按摩头选择逻辑
headerButtons.forEach((button) => {
button.addEventListener("click", function () {
if(photoBtn.style.opacity === "0.4") {
return;
}
// 移除其他按摩头的选中状态
headerButtons.forEach((btn) => btn.classList.remove("active"));
// 为当前按摩头添加选中状态
this.classList.add("active");
// 保存选中的按摩头信息
const headerText = this.querySelector(".header-text").textContent.trim();
selectedData.selectedHead = this.getAttribute("data-header");
selectedData.selectedHeadText = headerText;
});
localStorage.setItem("manual_command", null);
});
// 切换到 step2 的逻辑
prev2Btn.addEventListener("click", function () {
if (prev2Btn.style.opacity === "0.4") {
return;
}
transitionTo("step2", "step1", 1);
});
photoBtn.addEventListener("click", async function () {
// transitionTo("step2", "step3", 3);
if (photoBtn.style.opacity === "0.4") {
return;
}
checkLicense()
.then(async (data) => {
if (!data.can_use) {
let activatePleaseText = await getPopupText("activatePleaseText");
showPopup(activatePleaseText, {
confirm: true,
cancel: false,
})
return
}
const status = await getStatus();
if (!status.massage_service_started) {
let connectArmText = await getPopupText("connectArmText");
showPopup(connectArmText, { confirm: true, cancel: false });
return;
}
if (selectedData.selectedHead === null || !selectedData.selectedHead) {
let selectHeadText = await getPopupText("selectHeadText");
showPopup(selectHeadText, { confirm: true, cancel: false });
return;
}
function setHintMsg(selectedHeadText) {
let msg = {
zh: `
本设备必须由
经过培训的专业人员
操作使用,非专业人员请勿擅自操作。
孕妇、心血管疾病患者、局部皮肤破损者
等禁止使用。
体内有金属者、佩戴金属饰品、严重骨质疏松患者
及其他身体不适者在理疗前告知理疗师。
生理期与哺乳期
不建议进行理疗,建议根据身体状况选择理疗时间。
空腹、醉酒
等状态下不宜进行理疗。
冬季理疗时请避免
穿过厚、过宽松的衣物
,以确保最佳效果。
请确认安装的按摩头为
${selectedHeadText}
按摩头
`,
en: `
This device must be operated by
certified professionals only
. Unauthorized use is prohibited.
Pregnant women, cardiovascular patients, individuals with skin lesions
are strictly prohibited from use.
Metal implants, jewelry wearers, osteoporosis patients
must inform therapist prior to treatment.
Menstruation and lactation periods
require medical consultation before therapy.
Fasting or intoxication
are contraindicated for therapy.
In winter, avoid
bulky clothing
to ensure therapeutic efficacy.
Confirm installed head:
${selectedHeadText}
`,
jp: `
本機器は
訓練を受けた専門家のみ
が操作可能です。
妊婦・心臓病患者・皮膚損傷者
は使用禁止です。
体内金属・装身具・骨粗鬆症患者
は施術前に申告してください。
生理中・授乳中
の方は医師にご相談ください。
空腹時・飲酒時
の施術は避けてください。
冬季は
厚着を避け
効果を確保してください。
装着ヘッド確認:
${selectedHeadText}
`,
ko: `
본 장비는
훈련된 전문가만
조작 가능합니다.
임산부, 심혈관 질환자, 피부 손상자
사용 금지.
체내 금속, 장신구, 골다공증 환자
는 시술 전 알려주세요.
생리 중, 수유 중
에는 의사 상담이 필요합니다.
공복, 음주 상태
에서는 사용을 자제하세요.
겨울철
두꺼운 옷 착용을 피하고
효과를 극대화하세요.
장착 헤드 확인:
${selectedHeadText}
`
}
return msg[lang];
}
let msg = setHintMsg(selectedData.selectedHeadText)
showHintModal(msg).then((confirm) => {
if (confirm) {
localStorage.setItem("acupunctureName", '默认')
acupunctureName.innerHTML = '默认'
massagePlanText.innerHTML = `默认`
let head = selectedData.selectedHead;
// let headText = selectedData.selectedHeadText;
let bodyPart = selectedData.selectedPart;
const modeReal = calculateModeReal();
socket.emit("send_command", `begin:${head}:1:${bodyPart}:${modeReal}:manual`);
console.log("send_command", `begin:${head}:1:${bodyPart}:${modeReal}:manual`)
prev2Btn.style.opacity = 0.4;
photoBtn.style.opacity = 0.4;
// transitionTo("step2", "step3", 3);
// if (head === 'stone') {
// if (head === 'finger') {
// startPreHeat();
// }
}
})
})
});
reSelectBtn.addEventListener("click", async function () {
if (reSelectBtn.style.opacity === "0.4") {
return;
}
let reSelectText = await getPopupText("reSelectText");
showPopup(reSelectText)
.then(async (confirm) => {
if (confirm) {
socket.emit("send_command", `stop`);
prev2Btn.style.opacity = 0.4;
photoBtn.style.opacity = 0.4;
manualSwitch.style.opacity = 0.4;
getAcupointBtn.style.opacity = 0.4;
acupunctureBtn.style.opacity = 0.4;
}
})
});
document.querySelector("#re-photo").addEventListener("click", async () => {
// 重新拍照
const status = await getStatus();
if (status.is_pause && status.is_pause === true) {
const head = status.current_head.replace(/_head$/, "");
const bodyPart = status.body_part;
const modeReal = calculateModeReal();
reSelectBtn.style.opacity = 0.4;
socket.emit(
"send_command",
`begin:${head}:1:${bodyPart}:${modeReal}:manual`
);
console.log(
"send_command",
`begin:${head}:1:${bodyPart}:${modeReal}:manual`
);
}
socket.emit("send_command", `get_picture`);
});
document.querySelector("#pause").addEventListener("click", debounce(async () => {
let isPausedText = await getPopupText("isPausedText");
showPopup(isPausedText)
.then(async (confirm) => {
if (confirm) {
socket.emit("send_command", `pause`);
}
})
// const pauseIcon = document.querySelector("#pause-icon");
// if (pauseIcon.src.includes('/static/images/select_program/pause.png')) {
// pauseIcon.src = "/static/images/select_program/re-start.png";
// } else {
// pauseIcon.src = "/static/images/select_program/pause.png";
// }
}), 500);
acupunctureBtn.addEventListener("click", debounce(async () => {
if (acupunctureBtn.style.opacity === "0.4") {
return;
}
const status = await getStatus();
if (status && status.is_pause === true && status.is_massaging === false) {
return
}
transitionTo("step3", "step4", 4);
document.querySelector("#acupuncture-iframe").src = `/massage_plans?choose_task=${selectedData.selectedHead}&body_part=${selectedData.selectedPart}`
}), 500);
const timeNum = document.getElementById('time-num');
timeDecreaseBtn.addEventListener('click', async () => {
if (timeDecreaseBtn.style.opacity === "0.4") {
return;
}
if (timeIndex > 0) {
timeIndex--;
timeNum.value = timeArr[timeIndex].txt;
} else {
let timeCannotReduceText = await getPopupText("timeCannotReduceText")
showPopup(timeCannotReduceText)
}
})
timeIncreaseBtn.addEventListener('click', async () => {
if (timeIncreaseBtn.style.opacity === "0.4") {
return;
}
let len = timeArr.length;
if (timeIndex < len - 1) {
timeIndex++;
timeNum.value = timeArr[timeIndex].txt;
} else {
let timeCannotIncreaseText = await getPopupText("timeCannotIncreaseText")
showPopup(timeCannotIncreaseText)
}
})
temperDecreaseBtn.addEventListener('click', async () => {
if(temperDecreaseBtn.style.opacity === '0.4') {
return;
}
if(parseInt(temperNum.value) > 0) {
socket.emit("send_command", 'adjust:temperature:decrease:low');
let val = parseInt(temperNum.value) - 1;
temperNum.value = val;
temperDecreaseBtn.style.opacity = '0.4';
temperIncreaseBtn.style.opacity = '0.4';
} else {
showPopup("已经是最小的档位了", {confirm: true, cancel: false})
}
})
temperIncreaseBtn.addEventListener('click', async () => {
if(temperIncreaseBtn.style.opacity === '0.4') {
return;
}
let head = selectedData.selectedHead;
let maxLevelVal = head.includes("heat") ? 3 : 5;
if(parseInt(temperNum.value) < maxLevelVal) {
socket.emit("send_command", 'adjust:temperature:increase:low');
let val = parseInt(temperNum.value) + 1;
temperNum.value = val;
temperDecreaseBtn.style.opacity = '0.4';
temperIncreaseBtn.style.opacity = '0.4';
} else {
showPopup("已经是最大的档位了", {confirm: true, cancel: false})
}
})
const acupunctureNum = document.getElementById('acupuncture-num');
// 减按钮事件
decreaseBtn.addEventListener('click', async () => {
if (decreaseBtn.style.opacity === "0.4") {
return;
}
let currentValue = parseInt(acupunctureNum.value);
if (currentValue > 1) {
acupunctureNum.value = currentValue - 1;
} else {
let twiceCannotReduceText = await getPopupText("twiceCannotReduceText")
showPopup(twiceCannotReduceText)
}
});
// 加按钮事件
increaseBtn.addEventListener('click', async () => {
if (increaseBtn.style.opacity === "0.4") {
return;
}
let currentValue = parseInt(acupunctureNum.value);
if (currentValue < 5) {
acupunctureNum.value = currentValue + 1;
} else {
let twiceCannotIncreaseText = await getPopupText("twiceCannotIncreaseText")
showPopup(twiceCannotIncreaseText)
}
});
document.querySelector("#massage-setting-btn").addEventListener("click", function () {
transitionTo("step4", "step3", 3);
});
document.querySelector("#stop").addEventListener("click", async function () {
let isEndingText = await getPopupText("isEndingText");
showPopup(isEndingText)
.then(async (confirm) => {
if (confirm) {
socket.emit("send_command", `stop`);
}
})
})
let startFunction = async () => {
initDisplay();
let headSelectIndex = parseInt(getSelectedHeadIndex(selectedData.selectedHead)) || 0;
toggleSettingCard(headSelectIndex);
let manual_command = localStorage.getItem("manual_command");
if (manual_command === null || !manual_command || manual_command === 'null') {
let task_time = radioSlideToggle.style.left === "0px" ? timeArr[timeIndex].value : 0;
let loop_num = radioSlideToggle.style.left === "0px" ? 0 : parseInt(acupunctureNum.value);
socket.emit("send_command", `manual_start:${task_time}:${loop_num}`);
localStorage.setItem("manual_command", `manual_start:${task_time}:${loop_num}`);
} else {
socket.emit("send_command", manual_command);
}
// toggleContainer(false);
changeSetting();
// transitionTo("step3", "step5", 5);
// 更新 step5 中的按摩部位和按摩头显示
updateMassageTypeDisplay();
}
startButton.addEventListener("click", debounce(async () => {
if (startButton.style.opacity === "0.4") {
return;
}
if(startHint) {
let msg = await getPopupText("startHintText");
let hintTitle = await getPopupText("startHintTitleText");
let hintConfirm = await getPopupText("commonConfirmText");
let hintCancel = await getPopupText("startHintCancelText");
// showHintModal
showHintModal(msg, hintTitle, hintConfirm, hintCancel, true, true).then((confirm) => {
if(confirm) {
startFunction();
}
})
} else {
startFunction();
}
}), 500);
radioSwitch.addEventListener("click", (() => {
if (radioSwitch.style.opacity === "0.4") {
return;
}
const timeInputBox = document.querySelector("#time-input-box");
const stepInputBox = document.querySelector("#loop-input-box");
if (radioSlideToggle.style.left === "0px") {
radioSlideToggle.style.left = "40px";
timeInputBox.style.display = "none";
stepInputBox.style.display = "flex";
} else {
radioSlideToggle.style.left = "0px";
timeInputBox.style.display = "flex";
stepInputBox.style.display = "none";
}
}))
manualSwitch.addEventListener("click", debounce(async () => {
if (manualSwitch.style.opacity === "0.4") {
return;
}
const status = await getStatus();
if (status && status.is_pause === true && status.is_massaging === false) {
return
}
if (manualSlideToggle.style.left === "0px") {
manualSlideToggle.style.left = "40px";
toggleContainer(true);
} else {
manualSlideToggle.style.left = "0px";
toggleContainer(false);
}
}), 500);
});
function setHeadData(part) {
// 1 ---> 深部热疗
// 2 ---> 点阵按摩
// 3 ---> 全能滚珠
// 4 ---> 指疗通络
// 5 ---> 滚滚刺疗
// 6 ---> 温砭舒揉
// 7 ---> 离子光灸
// 8 ---> 能量热疗
// 9 ---> 天球滚捏
switch (part) {
case "belly":
setPartHead('none', 2, 4, 5, 7, 9);
break;
case "shoulder":
setPartHead('none', 1, 3, 5, 6, 7, 8);
break;
case "back":
break;
case "waist":
// setPartHead("none",1, 3, 4, 5, 6);
break;
case "leg":
setPartHead('none', 1, 2, 3, 7);
break;
default:
break;
}
}
// 按摩头编号与英文标识映射
const HEAD_MAP = {
1: 'thermotherapy',
2: 'shockwave',
3: 'ball',
4: 'finger',
5: 'roller',
6: 'stone',
7: 'ion',
8: 'heat',
9: 'spheres'
};
// 获取用户启用的按摩头(带缓存)
let enabledHeadsCache = null;
async function getEnabledHeads() {
if (enabledHeadsCache) return enabledHeadsCache;
try {
const response = await fetch('/get_massage_heads');
const { data } = await response.json();
enabledHeadsCache = new Set(
Object.entries(data)
.filter(([_, { display }]) => display)
.map(([key]) => key)
);
return enabledHeadsCache;
} catch (error) {
console.error('获取按摩头设置失败,默认全部启用:', error);
return new Set(Object.values(HEAD_MAP)); // 失败时返回全部
}
}
function setPartHead(display, ...num) {
num.forEach((item) => {
let key = `head${item}`;
let head = document.querySelector(`#${key}`);
head.style.display = display;
})
}
async function setPartHeadDefault() {
const enabledHeads = await getEnabledHeads();
const headsToShow = [];
// 检查哪些按摩头是用户启用的
for (const [num, key] of Object.entries(HEAD_MAP)) {
if (enabledHeads.has(key)) {
headsToShow.push(parseInt(num));
}
}
// 只显示用户启用的按摩头
setPartHead("block", ...headsToShow);
}
setPartHeadDefault();
// 定义切换步骤的动画
function transitionTo(hideId, showId, step = 1) {
if (hideId === showId) {
return;
}
const hideElement = document.getElementById(hideId);
const showElement = document.getElementById(showId);
// 动画隐藏当前 div
anime({
targets: `#${hideId}`,
opacity: 0,
translateX: "150%",
duration: 200,
easing: "easeInOutQuad",
complete: function () {
hideElement.classList.remove("active");
hideElement.style.zIndex = "-2";
// hideElement.style.display = "none";
hideElement.style.visibility = "hidden";
// 动画显示下一个 div
showElement.classList.add("active");
showElement.style.zIndex = "10";
// showElement.style.display = "block";
showElement.style.visibility = "visible";
anime({
targets: `#${showId}`,
opacity: 1,
translateX: "0%",
duration: 200,
easing: "easeInOutQuad",
});
},
});
if (step === 1) {
setTimeout(() => {
setPartHeadDefault();
}, 600);
selectedData.selectedPart = null;
selectedData.selectedHead = null;
selectedData.selectedHeadText = null;
// 切回第一个版块的时候把选中状态置零
partSelections.forEach((btn) => {
btn.classList.remove("active"); // 移除选中样式
const img = btn.querySelector("img");
// 仅在需要时替换回普通图片
if (!img.src.includes("_normal-btn.png")) {
img.src = img.src.replace("-btn.png", "_normal-btn.png");
}
});
prev2Btn.style.opacity = 1;
photoBtn.style.opacity = 1;
} else if (step === 2) {
selectedData.selectedHead = null;
selectedData.selectedHeadText = null;
// 切回第二个版块的时候把选中状态置零
headerButtons.forEach((btn) => btn.classList.remove("active"));
}
if (step !== 3) {
toggleContainer(false);
stopPreHeat();
} else {
manualSlideToggle.style.left = "0px";
}
if (step === 5) {
settings[1].max = getDefaultData("max-temperature"); // temperature
const forceRange = document.querySelector("#force-range");
forceRange.innerText = `[${settings[0].min}~${settings[0].max}]`;
}
setStepBox(step);
nowStep = step;
}
async function switchMode(mode) {
const status = await getStatus();
if (status.is_massaging && status.is_massaging == true) {
let commandStartedText = await getPopupText("commandStartedText");
showPopup(commandStartedText);
return;
} else {
window.location.href = `/switch_mode/${mode}`;
}
}
const container = document.getElementById("resize-container");
const ojoPointBox = document.querySelector("#ojo-point-box");
let angle = 0;
// 获取脖子左侧和右侧的点元素
const neckPointLeft = document.querySelector("#neck-point-left");
const neckPointRight = document.querySelector("#neck-point-right");
const neckTextLeft = document.querySelector("#neck-text-left");
const neckTextRight = document.querySelector("#neck-text-right");
function toggleContainer(flag = true) {
const container = document.getElementById("resize-container");
const lineContainer = document.getElementById("line-container");
// 添加一个标记来记录是否已经初始化
const lineContainerInitialized = lineContainer.getAttribute("data-initialized") === "true";
if (flag) {
// 判断是否为腿部,决定显示哪个容器
if (selectedData.selectedPart === "leg") {
// 显示曲线容器,隐藏矩形容器
container.classList.remove("show");
lineContainer.classList.add("show");
lineContainer.style.display = "block";
// 只有在未初始化或需要重新初始化时才调用
if (!lineContainerInitialized) {
initLineContainer();
// 设置初始化标记
lineContainer.setAttribute("data-initialized", "true");
}
} else {
// 显示矩形容器,隐藏曲线容器
container.classList.add("show");
lineContainer.classList.remove("show");
lineContainer.style.display = "none";
// 根据选择的部位决定是否显示大椎点和脖子点
if (selectedData.selectedPart === "back" || selectedData.selectedPart === "shoulder" || selectedData.selectedPart === "waist") {
// updateOjoPoint();
ojoPointBox.style.display = "block";
document.querySelector(".neck-point-left").style.display = "block";
document.querySelector(".neck-point-right").style.display = "block";
document.querySelector(".neck-point-bottom-left").style.display = "block";
document.querySelector(".neck-point-bottom-right").style.display = "block";
} else {
ojoPointBox.style.display = "none";
document.querySelector(".neck-point-left").style.display = "none";
document.querySelector(".neck-point-right").style.display = "none";
document.querySelector(".neck-point-bottom-left").style.display = "none";
document.querySelector(".neck-point-bottom-right").style.display = "none";
}
}
} else {
// 隐藏所有容器
container.classList.remove("show");
lineContainer.classList.remove("show");
lineContainer.style.display = "none";
ojoPointBox.style.display = "none";
document.querySelector(".neck-point-left").style.display = "none";
document.querySelector(".neck-point-right").style.display = "none";
document.querySelector(".neck-point-bottom-left").style.display = "none";
document.querySelector(".neck-point-bottom-right").style.display = "none";
}
}
// 拖拽功能
interact(container)
.draggable({
inertia: true,
modifiers: [
interact.modifiers.restrictRect({
restriction: "parent",
endOnly: true,
}),
],
listeners: {
move: function (event) {
const target = event.target;
const x =
(parseFloat(target.getAttribute("data-x")) || 0) + event.dx;
const y =
(parseFloat(target.getAttribute("data-y")) || 0) + event.dy;
target.style.transform = `translate(${x}px, ${y}px) rotate(${angle}deg)`;
target.setAttribute("data-x", x);
target.setAttribute("data-y", y);
},
},
});
// 为调整大小的角点添加拖动功能
interact('.resize-handle')
.draggable({
inertia: false, // 关闭惯性,避免抖动
modifiers: [
interact.modifiers.restrictRect({
restriction: "parent",
endOnly: true,
}),
],
listeners: {
start: function (event) {
const handle = event.target;
const container = handle.parentElement;
// 记录矩形框的初始中心坐标(这是绝对坐标,在页面中的位置)
const rect = container.getBoundingClientRect();
const initialCenterX = rect.left + rect.width / 2;
const initialCenterY = rect.top + rect.height / 2;
handle.setAttribute("data-center-x", initialCenterX);
handle.setAttribute("data-center-y", initialCenterY);
// 记录初始尺寸
handle.setAttribute("data-initial-width", rect.width);
handle.setAttribute("data-initial-height", rect.height);
// 记录初始鼠标位置
handle.setAttribute("data-start-x", event.clientX);
handle.setAttribute("data-start-y", event.clientY);
// 记录角点类型
const handleClass = handle.className;
if (handleClass.includes("top-left")) handle.setAttribute("data-corner", "tl");
else if (handleClass.includes("top-right")) handle.setAttribute("data-corner", "tr");
else if (handleClass.includes("bottom-left")) handle.setAttribute("data-corner", "bl");
else if (handleClass.includes("bottom-right")) handle.setAttribute("data-corner", "br");
},
move: function (event) {
const handle = event.target;
const container = handle.parentElement;
// 获取初始中心点坐标
const initialCenterX = parseFloat(handle.getAttribute("data-center-x"));
const initialCenterY = parseFloat(handle.getAttribute("data-center-y"));
// 获取初始尺寸
const initialWidth = parseFloat(handle.getAttribute("data-initial-width"));
const initialHeight = parseFloat(handle.getAttribute("data-initial-height"));
// 获取鼠标移动距离
const startX = parseFloat(handle.getAttribute("data-start-x"));
const startY = parseFloat(handle.getAttribute("data-start-y"));
const dx = event.clientX - startX;
const dy = event.clientY - startY;
// 获取旋转角度
const transform = container.style.transform || '';
const angleMatch = transform.match(/rotate\(([-\d.]+)deg\)/);
angle = angleMatch ? parseFloat(angleMatch[1]) : 0;
const radians = angle * Math.PI / 180;
// 旋转矩阵计算
const cos = Math.cos(radians);
const sin = Math.sin(radians);
// 在旋转坐标系中计算移动距离
const rotatedDx = dx * cos + dy * sin;
const rotatedDy = -dx * sin + dy * cos;
// 根据角点类型和旋转角度计算新尺寸
let newWidth, newHeight;
const corner = handle.getAttribute("data-corner");
// 计算在旋转坐标系中相应的缩放因子
switch (corner) {
case "tl": // 左上角
newWidth = Math.max(60, initialWidth - rotatedDx * 2);
newHeight = Math.max(60, initialHeight - rotatedDy * 2);
break;
case "tr": // 右上角
newWidth = Math.max(60, initialWidth + rotatedDx * 2);
newHeight = Math.max(60, initialHeight - rotatedDy * 2);
break;
case "bl": // 左下角
newWidth = Math.max(60, initialWidth - rotatedDx * 2);
newHeight = Math.max(60, initialHeight + rotatedDy * 2);
break;
case "br": // 右下角
newWidth = Math.max(60, initialWidth + rotatedDx * 2);
newHeight = Math.max(60, initialHeight + rotatedDy * 2);
break;
}
// 限制最大尺寸
newWidth = Math.min(newWidth, 400);
newHeight = Math.min(newHeight, 500);
// 设置新尺寸
container.style.width = `${newWidth}px`;
container.style.height = `${newHeight}px`;
// 重新计算位置,使中心点保持不变
// 获取容器当前尺寸和位置
const currentRect = container.getBoundingClientRect();
const currentCenterX = currentRect.left + currentRect.width / 2;
const currentCenterY = currentRect.top + currentRect.height / 2;
// 计算中心点偏移量
const centerOffsetX = initialCenterX - currentCenterX;
const centerOffsetY = initialCenterY - currentCenterY;
// 获取当前transform值
let matrix = new DOMMatrix(window.getComputedStyle(container).transform);
// 计算新的位置,加上偏移量以保持中心点位置不变
const currentX = matrix.e;
const currentY = matrix.f;
const newX = currentX + centerOffsetX;
const newY = currentY + centerOffsetY;
// 更新位置,保持旋转角度不变
container.style.transform = `translate(${newX}px, ${newY}px) rotate(${angle}deg)`;
// 更新数据属性
container.setAttribute("data-x", newX);
container.setAttribute("data-y", newY);
}
}
});
// 旋转功能
interact(".rotate-handle").draggable({
listeners: {
move: function (event) {
const target = event.target.parentElement;
const rect = target.getBoundingClientRect();
const center = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
const deltaX = event.clientX - center.x;
const deltaY = event.clientY - center.y;
// 计算原始角度(带限制)
let newAngle = (Math.atan2(deltaY, deltaX) * 180 / Math.PI + 90);
// 限制角度在 -30 到 30 度之间
angle = Math.max(-30, Math.min(30, newAngle));
const x = parseFloat(target.getAttribute("data-x")) || 0;
const y = parseFloat(target.getAttribute("data-y")) || 0;
// 应用限制后的角度
target.style.transform = `translate(${x}px, ${y}px) rotate(${angle}deg)`;
// 更新旋转手柄方向(保持与容器旋转方向相反)
const rotateHandle = event.target;
rotateHandle.style.transform = `translateX(-50%) rotate(${-angle}deg)`;
}
},
});
getAcupointBtn.addEventListener("click", async function () {
if (getAcupointBtn.style.opacity === "0.4") {
return;
}
const status = await getStatus();
if (status && status.is_pause === true && status.is_massaging === false) {
return
}
const commandPrefix = "cal_acu";
const container = document.getElementById("resize-container");
const lineContainer = document.getElementById("line-container");
// 发送数据逻辑
if ((selectedData.selectedPart === "leg" && lineContainer.classList.contains("show")) ||
(selectedData.selectedPart !== "leg" && container.classList.contains("show"))) {
// 根据部位类型选择不同的数据获取方式
if (selectedData.selectedPart === "leg") {
// 获取曲线坐标和曲率数据
getLinePointsAndCurvatures().then((data) => {
// 从数据中提取点坐标
const points = data.linePoints;
// 从数据中提取曲率信息
const curvatures = data.curvatures;
// 按顺序构建数据数组:所有点的坐标和四条曲线的曲率
let commonPointData = [];
// 添加6个点的坐标
for (let i = 0; i < points.length; i++) {
commonPointData.push(points[i].originalX);
commonPointData.push(points[i].originalY);
}
// 添加4条曲线的曲率值(使用与后端兼容的曲率值)
for (let i = 0; i < curvatures.length; i++) {
commonPointData.push(curvatures[i].curvature);
}
// 发送命令 - 使用socket.emit
const command = `${commandPrefix}:${commonPointData.join(":")}`;
console.log("腿部曲线数据 - send_command", command);
socket.emit("send_command", command);
}).catch(error => {
console.error("获取曲线数据失败:", error);
});
} else {
// 根据选择的部位使用不同的方法获取点位
const getPointsFunc = (selectedData.selectedPart === "back" || selectedData.selectedPart === "shoulder" || selectedData.selectedPart === "waist")
? getNeckPoints
: getRotatedCorners;
getPointsFunc().then((coordInfo) => {
let commonPointData = [
coordInfo.center[0],
coordInfo.center[1],
coordInfo.topLeft[0],
coordInfo.topLeft[1],
coordInfo.topRight[0],
coordInfo.topRight[1],
coordInfo.bottomRight[0],
coordInfo.bottomRight[1],
coordInfo.bottomLeft[0],
coordInfo.bottomLeft[1],
]
// 发送命令 - 使用socket.emit
const command = `${commandPrefix}:${commonPointData.join(":")}`;
console.log("其他部位数据 - send_command", command);
socket.emit("send_command", command);
});
}
} else {
// 未显示任何选择界面时
const command = `${commandPrefix}:None`;
console.log("无选择 - send_command", command);
socket.emit("send_command", command);
}
})
function getRotatedCorners() {
return new Promise((resolve, reject) => {
try {
const container = document.getElementById("resize-container");
const containerRect = container.getBoundingClientRect();
const imgContainer = document.querySelector(".body-image");
const imgContainerRect = imgContainer.getBoundingClientRect();
// const img = document.querySelector("#massage-img-box");
// 获取旋转角度(从transform矩阵中解析)
const transform = container.style.transform;
const angleMatch = transform.match(/rotate\(([-\d.]+)deg\)/);
const angle = angleMatch ? parseFloat(angleMatch[1]) : 0;
const radians = angle * (Math.PI / 180);
// 计算图片实际渲染参数
// const naturalWidth = img.naturalWidth;
// const naturalHeight = img.naturalHeight;
const naturalWidth = 640;
const naturalHeight = 400;
const scale = Math.max(
imgContainerRect.width / naturalWidth,
imgContainerRect.height / naturalHeight
);
const scaledWidth = naturalWidth * scale;
const scaledHeight = naturalHeight * scale;
const offsetX = (imgContainerRect.width - scaledWidth) / 2;
const offsetY = (imgContainerRect.height - scaledHeight) / 2;
// 容器中心点坐标(相对于图片容器)
const center = {
x: containerRect.left - imgContainerRect.left + containerRect.width / 2,
y: containerRect.top - imgContainerRect.top + containerRect.height / 2
};
// 定义四个角点的相对坐标(未旋转状态)
const corners = [
{ x: -containerRect.width / 2, y: -containerRect.height / 2 }, // top-left
{ x: containerRect.width / 2, y: -containerRect.height / 2 }, // top-right
{ x: -containerRect.width / 2, y: containerRect.height / 2 }, // bottom-left
{ x: containerRect.width / 2, y: containerRect.height / 2 } // bottom-right
];
// 旋转坐标函数
const rotatePoint = (point) => ({
x: point.x * Math.cos(radians) - point.y * Math.sin(radians),
y: point.x * Math.sin(radians) + point.y * Math.cos(radians)
});
// 转换到原始图片坐标的函数
const toOriginalCoords = (x, y) => ({
x: (x - offsetX) / scale,
y: (y - offsetY) / scale
});
// 计算旋转后的四个角点
const rotatedCorners = corners.map(point => {
const rotated = rotatePoint(point);
return {
x: center.x + rotated.x,
y: center.y + rotated.y
};
});
// 转换为原始图片坐标
const finalCoords = rotatedCorners.map(point =>
toOriginalCoords(point.x, point.y)
);
resolve({
topLeft: [Math.round(finalCoords[0].x), Math.round(finalCoords[0].y)],
topRight: [Math.round(finalCoords[1].x), Math.round(finalCoords[1].y)],
bottomLeft: [Math.round(finalCoords[2].x), Math.round(finalCoords[2].y)],
bottomRight: [Math.round(finalCoords[3].x), Math.round(finalCoords[3].y)],
center: [Math.round(toOriginalCoords(center.x, center.y).x),
Math.round(toOriginalCoords(center.x, center.y).y)],
angle: Math.round(angle)
});
} catch (error) {
reject(error);
}
});
}
function getSelectedHeadIndex(selectedHead) {
const headMap = {
"thermotherapy": 0,
"shockwave": 1,
"ball": 2,
"finger": 3,
"roller": 4,
"stone": 5,
"ion": 6,
"heat": 7,
"spheres": 8
};
return headMap[selectedHead];
}
function getDefaultData(type) {
let bodyPart = selectedData.selectedPart;
let headSelectIndex = parseInt(getSelectedHeadIndex(selectedData.selectedHead)) || 0;
let val;
switch (type) {
case "force":
val = headSelectIndex == 1 ? 5 : headSelectIndex == 2 ? 15 : bodyPart == "belly" ? 10 : 20;
break;
case "max-force":
// val = headSelectIndex === 1 ? 12 : 50;
val = 70;
break;
case "temperature":
val = headSelectIndex === 0 ? 3 : 1;
break;
case "max-temperature":
val = headSelectIndex === 7 ? 3 : 5;
break;
default:
break;
}
return val;
}
async function getStatus() {
try {
const response = await fetch("/get_status");
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
console.log(data, 'get_status')
return data; // 直接返回字典数据
} catch (error) {
console.error("There was a problem with the fetch operation:", error);
return null;
}
}
async function changeSetting() {
// 初始化设置的默认值
settings[0].initialValue = getDefaultData("force");
settings[0].max = getDefaultData("max-force"); // force
settings[1].initialValue = getDefaultData("temperature"); // temperature
settings[1].max = getDefaultData("max-temperature"); // temperature
// settings[1].max = 5;
settings[2].initialValue = 0; // shake
settings[3].initialValue = 0; // gear
settings[4].initialValue = 6; // frequency
settings[5].initialValue = 12; // press
settings[6].initialValue = 2; // speed
settings[7].initialValue = 1; // direction
settings[8].initialValue = 6; // high
commonSlideToggle.style.left = "40px";
const forceRange = document.querySelector("#force-range");
forceRange.innerText = `[${settings[0].min}~${settings[0].max}]`;
const temperatureRange = document.querySelector("#temperature-range");
temperatureRange.innerText = `[${settings[1].min}~${settings[1].max}]`;
// 更新显示
settings.forEach((setting, index) => {
if (index != 7) {
updateDisplay(setting);
}
});
}
async function initDisplay() {
const status = await getStatus();
updateStatusDisplay(status);
}
function changeData(name, val) {
let setting = settings.find((setting) => setting.name === name);
setting.initialValue = parseInt(val);
if (name !== "direction") {
updateDisplay(setting);
} else {
// commonSlideToggle.style.left = "40px";
if (val == 1) {
commonSlideToggle.style.left = "40px";
} else {
commonSlideToggle.style.left = 0;
}
}
}
// 更新按摩类型显示
function updateMassageTypeDisplay() {
// 获取选中的部位信息
const selectedPart = document.querySelector(".operate-button-item.active");
if (selectedPart) {
let img = selectedPart.querySelector(".item-icon")
if (img.src.includes("_normal-btn.png")) {
img.src = img.src.replace("_normal-btn.png", "-btn.png");
}
const partIcon = img.src;
const partTitle = selectedPart.querySelector(".item-title").textContent;
// 更新部位显示
const massageInfoPart = document.querySelector(".massage-info-part");
const partImg = massageInfoPart.querySelector(".info-part-icon");
const partText = massageInfoPart.querySelector(".info-part-title");
partImg.src = partIcon;
partText.textContent = partTitle;
}
const massageInfoHead = document.querySelector(".massage-info-head");
const headImgDisplay = massageInfoHead.querySelector(".info-header-img");
const headTextDisplay = massageInfoHead.querySelector(".info-header-text");
headImgDisplay.src = getHeadImg(selectedData.selectedHead);
headTextDisplay.textContent = selectedData.selectedHeadText;
}
function getHeadImg(head) {
let headMap = {
thermotherapy : "深部热疗",
shockwave : "点阵按摩",
ball : "全能滚珠",
finger : "指疗通络",
roller : "滚滚刺疗",
stone : "温砭舒揉",
ion : "离子光灸",
heat : "能量热疗",
spheres : "天球滚捏",
}
return `/static/images/massage_control/head/${headMap[head]}.png`
}
// async function formatPartName(name) {
// let key = name + "Text";
// let partName = await getPopupText(key);
// return partName;
// }
// async function formatHeadName(name) {
// let key = name + "Text";
// console.log(key);
// let headName = await getPopupText(key);
// return headName;
// }
async function setStepBox(step) {
const stepBox = document.querySelector("#step-box");
const stepTitle1 = document.querySelector("#title1");
const stepTitle2 = document.querySelector("#title2");
const stepTitle3 = document.querySelector("#title3");
// const stepTitle4 = document.querySelector("#title4");
// const stepTitle5 = document.querySelector("#title5");
if ([1, 2, 3].includes(step)) {
stepBox.style.display = "flex";
} else {
stepBox.style.display = "none";
}
if (step == 1) {
let selectBodyPartText = await getPopupText("selectBodyPartText");
let selectHeaderText = await getPopupText("selectHeaderText");
let photoGetAcuText = await getPopupText("photoGetAcuText");
stepTitle1.innerText = selectBodyPartText;
stepTitle2.innerText = selectHeaderText;
stepTitle3.innerText = photoGetAcuText;
} else if (step == 2) {
let formatPartName = await getPopupText(selectedData.selectedPart + "Text");
stepTitle1.innerText = formatPartName;
let selectHeaderText = await getPopupText("selectHeaderText");
let photoGetAcuText = await getPopupText("photoGetAcuText");
stepTitle2.innerText = selectHeaderText;
stepTitle3.innerText = photoGetAcuText;
let partVal = selectedData.selectedPart;
imgContainer.src = (partVal === 'belly' || partVal === 'leg') ? `static/images/smart_mode/${partVal}.png` : `static/images/smart_mode/back.png`;
} else if (step == 3) {
let formatPartName = await getPopupText(selectedData.selectedPart + "Text");
stepTitle1.innerText = formatPartName;
let formatHeadName = await getPopupText(selectedData.selectedHead + "Text");
stepTitle2.innerText = formatHeadName;
let photoGetAcuText = await getPopupText("photoGetAcuText");
stepTitle3.innerText = photoGetAcuText;
}
setStepItemClass(parseInt(step - 1));
}
function setStepItemClass(index) {
document.querySelectorAll(".step-item").forEach((item, i) => {
if (i < index) {
item.classList.add("done");
} else if (i == index) {
item.classList.remove("done");
item.classList.add("active");
} else if (i > index) {
item.classList.remove("active");
item.classList.remove("done");
}
});
}
// 公用设置项
let settings = [
{
name: "force",
min: 5,
// max: 50,
max: getDefaultData("max-force"),
initialValue: getDefaultData("force"),
elementSelectors: {
value: ".force-value",
addButton: ".force-add-btn",
downButton: ".force-down-btn",
},
socketCommand: {
increase: "adjust:force:increase:low",
decrease: "adjust:force:decrease:low",
},
maxPopupText: "maxForceText",
minPopupText: "minForceText",
},
{
name: "temperature",
min: 0,
max: getDefaultData("max-temperature"),
initialValue: getDefaultData("temperature"),
elementSelectors: {
value: ".temperature-value",
addButton: ".temperature-add-btn",
downButton: ".temperature-down-btn",
},
socketCommand: {
increase: "adjust:temperature:increase:low",
decrease: "adjust:temperature:decrease:low",
},
maxPopupText: "maxTempText",
minPopupText: "minTempText",
},
{
name: "shake",
min: 0,
max: 5,
initialValue: 1,
elementSelectors: {
value: ".shake-value",
addButton: ".shake-add-btn",
downButton: ".shake-down-btn",
},
socketCommand: {
increase: "adjust:shake:increase:low",
decrease: "adjust:shake:decrease:low",
},
maxPopupText: "maxLevelText",
minPopupText: "minLevelText",
},
{
name: "gear",
min: 0,
max: 5,
initialValue: 1,
elementSelectors: {
value: ".gear-value",
addButton: ".gear-add-btn",
downButton: ".gear-down-btn",
},
socketCommand: {
increase: "adjust:gear:increase:low",
decrease: "adjust:gear:decrease:low",
},
maxPopupText: "maxLevelText",
minPopupText: "minLevelText",
},
{
name: "frequency",
min: 1,
max: 16,
initialValue: 8,
elementSelectors: {
value: ".frequency-value",
addButton: ".frequency-add-btn",
downButton: ".frequency-down-btn",
},
socketCommand: {
increase: "adjust:frequency:increase:low",
decrease: "adjust:frequency:decrease:low",
},
maxPopupText: "maxFrequencyText",
minPopupText: "minFrequencyText",
},
{
name: "press",
min: 1,
max: 27,
initialValue: 15,
elementSelectors: {
value: ".press-value",
addButton: ".press-add-btn",
downButton: ".press-down-btn",
},
socketCommand: {
increase: "adjust:press:increase:low",
decrease: "adjust:press:decrease:low",
},
maxPopupText: "maxPressText",
minPopupText: "minPressText",
},
{
name: "speed",
min: 0,
max: 3,
initialValue: 2,
elementSelectors: {
value: ".speed-value",
addButton: ".speed-add-btn",
downButton: ".speed-down-btn",
},
socketCommand: {
increase: "adjust:speed:increase:low",
decrease: "adjust:speed:decrease:low",
},
maxPopupText: "maxLevelText",
minPopupText: "minLevelText",
},
{
name: "direction",
min: 0,
max: 1,
initialValue: 1,
},
{
name: "high",
min: 4,
max: 15,
initialValue: 6,
elementSelectors: {
value: ".high-value",
addButton: ".high-add-btn",
downButton: ".high-down-btn",
},
socketCommand: {
increase: "adjust:high:increase:low",
decrease: "adjust:high:decrease:low",
},
maxPopupText: "maxLevelText",
minPopupText: "minLevelText",
},
];
// 创建通用函数来处理增减逻辑
async function handleAdjustButtonClick(setting, isIncrease) {
const btnClick = function () {
return new Promise(async (resolve, reject) => {
if (isIncrease) {
// 判断增值操作
if (setting.initialValue == setting.max) {
let maxPopupText = await getPopupText(setting.maxPopupText);
showPopup(maxPopupText); // 超过最大值时弹出提示
reject(); // 直接返回,不执行后续逻辑
} else {
socket.emit(
"send_command",
setting.socketCommand.increase,
() => {
resolve(); // 确保命令执行后再继续
}
);
console.log("send_command", "setting.socketCommand.increase")
}
} else {
// 判断减值操作
if (setting.initialValue == setting.min) {
let minPopupText = await getPopupText(setting.minPopupText);
showPopup(minPopupText); // 超过最小值时弹出提示
reject(); // 直接返回,不执行后续逻辑
} else {
socket.emit(
"send_command",
setting.socketCommand.decrease,
() => {
resolve(); // 确保命令执行后再继续
}
);
console.log("send_command", "setting.socketCommand.decrease")
}
}
});
};
try {
btnClick().then((res) => {
getStatus();
});
} catch (error) {
console.error("操作未执行", error);
}
}
commonSwitch.addEventListener(
"click",
throttle(() => {
if (settings[7].initialValue == 0) {
settings[7].initialValue = 1;
commonSlideToggle.style.left = "40px";
} else {
settings[7].initialValue = 0;
commonSlideToggle.style.left = 0;
}
socket.emit("send_command", "adjust:direction:null:null");
}, 2000)
);
// 更新显示的通用函数
function updateDisplay(setting) {
const valueElement = document.querySelector(
setting.elementSelectors.value
);
valueElement.textContent = String(setting.initialValue).padStart(2, "0");
}
// 创建并绑定事件监听器
function initializeSettings() {
settings.forEach((setting, index) => {
if (index != 7) {
// 更新显示初始值
updateDisplay(setting);
// 获取按钮元素
const addButton = document.querySelector(
setting.elementSelectors.addButton
);
const downButton = document.querySelector(
setting.elementSelectors.downButton
);
// 增加按钮,减少按钮
// 绑定点击事件
addButton.addEventListener(
"click",
throttle(() => handleAdjustButtonClick(setting, true), 1000)
); // 增加
downButton.addEventListener(
"click",
throttle(() => handleAdjustButtonClick(setting, false), 1000)
); // 减少
}
});
}
let progressBar = document.querySelector(".progress-bar");
let progressData = document.querySelector(".progress-num");
let infoAcupointText = document.querySelector("#info-acupoint-text");
let massageTypeText = document.querySelector("#massage-type-text");
// 按摩手法映射
const pathTypeMap = {
line: "循经直推法",
in_spiral: "螺旋内揉法",
out_spiral: "螺旋外散法",
ellipse: "周天环摩法",
lemniscate: "双环疏经法",
cycloid: "摆浪通络法",
point: "定穴点按法",
point_rub: "定点揉摩法"
};
const temperNum = document.querySelector("#temper-num");
function startPreHeat(temper, temperLevel) {
preHeat.style.display = "flex";
tempInputBox.style.display = "flex";
stoneVal.innerText = temper;
temperNum.value = temperLevel;
temperDecreaseBtn.style.opacity = '1';
temperIncreaseBtn.style.opacity = '1';
startHint = true;
}
function stopPreHeat() {
preHeat.style.display = "none";
tempInputBox.style.display = "none";
startHint = false;
}
function getLevelString(level) {
let levelString = "";
switch (lang) {
case 'zh':
levelString = `${level}档温度`;
break;
case 'en':
levelString = `Level ${level} Temp`;
break;
case 'jp':
levelString = `温度レベル${level}`;
break;
case 'ko':
levelString = `온도 단계${level}`;
break;
default:
levelString = `${level}档温度`;
break;
}
return levelString;
}
preHeat.addEventListener("click", async () => {
const head = selectedData.selectedHead;
let levelLen = head.includes("stone") ? 5 : 3;
let msg = '';
for(let i = 1; i <= levelLen; i++) {
let level = await getLevelString(i)
if(levelLen === 5) {
msg += `
${level}
${(34 + i * 5) + '-' + (35 + i * 5)}℃
`
}
if(levelLen === 3) {
msg += `
${level}
${(33 + i * 3) + '-' + (35 + i * 3)}℃
`
}
}
let hintTitle = await getPopupText("preheatHintTitleText");
let hintConfirm = await getPopupText("commonConfirmText");
showHintModal(msg, hintTitle, hintConfirm, null, true, false)
});
// 更新Status信息显示
async function updateStatusDisplay(data) {
if (data.hasOwnProperty("progress")) {
var newValueStr = data.progress; // 接收到的字符串类型的progress
var newValue = Math.round(parseFloat(newValueStr));
if (!isNaN(newValue)) {
// 确保转换成功
// 格式化 progress 值
var formattedValue;
if (newValue === 0 || (newValue !== 0 && newValue < progressData.innerText)) {
if (data.is_massaging) {
formattedValue = parseFloat(progressData.innerText) || 0;
} else {
formattedValue = newValue;
progressBar.style.width = "0%";
progressData.innerText = `0%`;
}
} else if (newValue >= 100) {
if (data.is_massaging) {
formattedValue = 100;
} else {
formattedValue = 0;
}
} else if (newValue < 10) {
formattedValue = Math.round(newValue * 10) / 10; // 保留一位小数
} else {
formattedValue = Math.round(newValue * 10) / 10; // 保留一位小数
}
progressBar.style.width = `${formattedValue}%`;
progressData.innerText = `${formattedValue}%`;
} else {
console.error("Received an invalid progress value:", newValueStr);
}
}
if (data.hasOwnProperty("is_massaging")) {
if (data.is_massaging) {
partSelections.forEach((item) => {
if (item.getAttribute("data-part") === data.body_part) {
item.classList.add("active")
} else {
item.classList.remove("active")
}
});
const head = data.current_head.replace(/_head$/, "");
if (selectedData.selectedPart == null) {
selectedData.selectedPart = data.body_part;
}
if (selectedData.selectedHead == null) {
selectedData.selectedHead = head;
selectedData.selectedHeadText = await getPopupText(head + "Text");
const stepTitle2 = document.querySelector("#title2");
stepTitle2.innerHTML = selectedData.selectedHeadText;
}
updateMassageTypeDisplay();
toggleSettingCard(parseInt(getSelectedHeadIndex(head)));
// 需要切换到 step5 页面
if (data.manual_stage === 1 && nowStep !== 4 && nowStep !== 3) {
transitionTo("step" + nowStep, "step3", 3);
} else if (data.manual_stage === 2 && data.is_pause === false) {
transitionTo("step" + nowStep, "step5", 5);
}
prev2Btn.style.opacity = 0.4;
photoBtn.style.opacity = 0.4;
manualSwitch.style.opacity = 1;
getAcupointBtn.style.opacity = 1;
if (!data.is_pause) {
acupunctureBtn.style.opacity = 1;
}
radioSwitch.style.opacity = 1;
timeDecreaseBtn.style.opacity = 1;
timeIncreaseBtn.style.opacity = 1;
decreaseBtn.style.opacity = 1;
increaseBtn.style.opacity = 1;
} else {
if (data.is_pause) {
const timeInputBox = document.querySelector("#time-input-box");
const stepInputBox = document.querySelector("#loop-input-box");
transitionTo("step" + nowStep, "step3", 3);
let manual_command = localStorage.getItem("manual_command");
if (manual_command) {
let manualArr = manual_command.split(":");
if (parseInt(manualArr[2]) === 0) {
radioSlideToggle.style.left = "0px";
timeInputBox.style.display = "flex";
stepInputBox.style.display = "none";
} else {
radioSlideToggle.style.left = "40px";
timeInputBox.style.display = "none";
stepInputBox.style.display = "flex";
}
}
} else {
if (nowStep > 2) {
transitionTo("step" + nowStep, "step1", 1);
}
progressBar.style.width = "0%";
progressData.innerText = `0%`;
if (localStorage.getItem('manual_command')) {
localStorage.setItem('manual_command', null);
}
}
prev2Btn.style.opacity = 1;
photoBtn.style.opacity = 1;
manualSwitch.style.opacity = 0.4;
getAcupointBtn.style.opacity = 0.4;
acupunctureBtn.style.opacity = 0.4;
radioSwitch.style.opacity = 0.4;
timeDecreaseBtn.style.opacity = 0.4;
timeIncreaseBtn.style.opacity = 0.4;
decreaseBtn.style.opacity = 0.4;
increaseBtn.style.opacity = 0.4;
changeSetting();
}
}
if (data.is_massaging) {
if (data.hasOwnProperty("force")) {
if (data.force) {
changeData("force", data.force);
}
}
if (data.hasOwnProperty("temperature")) {
if (data.temperature) {
changeData("temperature", data.temperature);
}
}
if (data.hasOwnProperty("shake")) {
if (data.shake) {
changeData("shake", data.shake);
}
}
if (data.hasOwnProperty("gear")) {
if (data.gear) {
changeData("gear", data.gear);
}
}
if (data.hasOwnProperty("press")) {
if (data.press) {
changeData("press", data.press);
}
}
if (data.hasOwnProperty("frequency")) {
if (data.frequency) {
changeData("frequency", data.frequency);
}
}
if (data.hasOwnProperty("speed")) {
if (data.speed) {
changeData("speed", data.speed);
}
}
if (data.hasOwnProperty("direction")) {
if (data.direction) {
changeData("direction", data.direction);
}
}
if (data.hasOwnProperty("high")) {
if (data.high) {
changeData("high", data.high);
}
}
if (data.hasOwnProperty("start_pos")) {
if (data.start_pos && data.end_pos && data.start_pos !== '') {
if (data.start_pos === data.end_pos) {
infoAcupointText.innerText = `${formatText(data.start_pos)}`
} else {
infoAcupointText.innerText = `${formatText(data.start_pos)} - ${formatText(data.end_pos)}`
}
}
} else {
infoAcupointText.innerText = "-"
}
if (data.hasOwnProperty("massage_path")) {
if (data.massage_path && data.massage_path !== '') {
let pathType = pathTypeMap[data.massage_path];
if (pathType) {
massageTypeText.innerText = `${pathType}`
} else {
massageTypeText.innerText = `${data.massage_path}`
}
}
} else {
massageTypeText.innerText = "-"
}
} else {
// if (timer) {
// clearInterval(timer);
// timer = null; // 重置定时器ID
// }
infoAcupointText.innerText = "-"
massageTypeText.innerText = "-"
}
}
function formatText(input) {
// 使用正则表达式去除 @ 后面的部分
const regex = /^([^@]+)/;
const match = input.match(regex);
if (match) {
return match[1]; // 返回 @ 前面的部分
}
return input; // 如果不匹配,返回原文本
}
function toggleSettingCard(type) {
/**
* 0:深部热疗
* 1:点阵按摩
* 2:全能滚珠
* 3:指疗通络
* 4:滚滚刺疗
* 5:温砭舒揉 stone
* 6:离子光灸 ion
* 7:能量热疗 heat
* 8: 天球滚捏
*/
const allCards = [
strengthCard, temperatureCard, currentCard, shakeCard,
frequencyCard, pressCard, speedCard, directionCard, highCard
];
allCards.forEach(card => card.style.display = "none");
switch (parseInt(type)) {
case 0:
strengthCard.style.display = "block";
temperatureCard.style.display = "block";
shakeCard.style.display = "block";
currentCard.style.display = "block";
break;
case 1:
strengthCard.style.display = "block";
frequencyCard.style.display = "block";
pressCard.style.display = "block";
break;
case 2:
case 3:
case 4:
case 8:
strengthCard.style.display = "block";
break;
case 5:
strengthCard.style.display = "block";
temperatureCard.style.display = "block";
speedCard.style.display = "block";
directionCard.style.display = "block";
break;
case 6:
temperatureCard.style.display = "block";
highCard.style.display = "block";
break;
case 7:
strengthCard.style.display = "block";
temperatureCard.style.display = "block";
break;
}
}
const calculateModeReal = () => {
let modeReal = parseInt(localStorage.getItem("modeReal")) === 0 ? 0 : 1;
const powerControl = localStorage.getItem("powerControl");
let loadMode = parseInt(localStorage.getItem("loadMode")) === 1 ? 1 : 0;
if (modeReal === 0) {
if (powerControl === "导纳" || !powerControl) {
modeReal = 0;
} else {
modeReal = 2;
}
} else {
if (loadMode === 1) {
modeReal = 3;
} else if (loadMode === 0) {
modeReal = 1;
}
}
return modeReal;
};
addMassage.addEventListener("click", async () => {
if (addMassage.style.opacity === "0.4") {
return;
}
operationQueue('确认这里要多按一会吗?', 'insert_queue');
})
skipMassage.addEventListener("click", async () => {
if (addMassage.style.opacity === "0.4") {
return;
}
operationQueue('确认要跳过当前按摩吗?', 'skip_queue');
})
const operationQueue = async (msg, command) => {
const status = await getStatus();
if (status.start_pos === '' || status.end_pos === '') {
let isWaitingText = await getPopupText("isWaitingText");
showPopup(isWaitingText, { confirm: true, cancel: false })
return;
}
showPopup(msg).then((confirm) => {
if (confirm) {
socket.emit("send_command", command);
let operationButton = command === 'insert_queue' ? addMassage : skipMassage;
// 将按钮透明度设置为0.4
operationButton.style.opacity = 0.4;
// 2秒后将按钮透明度恢复为1
setTimeout(() => {
operationButton.style.opacity = 1;
}, 2000);
}
})
}
initDisplay();
initializeSettings();
changeSetting();
// 监听 storage 事件
window.addEventListener("storage", function (event) {
if (event.key === "acupunctureName") {
if (event.oldValue === event.newValue) {
return;
} else {
acupunctureName.innerHTML = event.newValue
massagePlanText.innerHTML = event.newValue
}
}
});
// 获取脖子四个点的坐标
function getNeckPoints() {
return new Promise((resolve, reject) => {
try {
const container = document.getElementById("resize-container");
const containerRect = container.getBoundingClientRect();
const imgContainer = document.querySelector(".body-image");
const imgContainerRect = imgContainer.getBoundingClientRect();
// 获取旋转角度(从transform矩阵中解析)
const transform = container.style.transform;
const angleMatch = transform.match(/rotate\(([-\d.]+)deg\)/);
const angle = angleMatch ? parseFloat(angleMatch[1]) : 0;
const radians = angle * (Math.PI / 180);
// 计算图片实际渲染参数
const naturalWidth = 640;
const naturalHeight = 400;
const scale = Math.max(
imgContainerRect.width / naturalWidth,
imgContainerRect.height / naturalHeight
);
const scaledWidth = naturalWidth * scale;
const scaledHeight = naturalHeight * scale;
const offsetX = (imgContainerRect.width - scaledWidth) / 2;
const offsetY = (imgContainerRect.height - scaledHeight) / 2;
// 获取四个脖子点元素
const neckLeftEl = document.querySelector(".neck-point-left");
const neckRightEl = document.querySelector(".neck-point-right");
const neckBottomLeftEl = document.querySelector(".neck-point-bottom-left");
const neckBottomRightEl = document.querySelector(".neck-point-bottom-right");
// 获取元素相对于容器的位置
const neckLeftRect = neckLeftEl.getBoundingClientRect();
const neckRightRect = neckRightEl.getBoundingClientRect();
const neckBottomLeftRect = neckBottomLeftEl.getBoundingClientRect();
const neckBottomRightRect = neckBottomRightEl.getBoundingClientRect();
// 计算四个点的中心坐标(相对于图片容器)
const neckLeft = {
x: neckLeftRect.left - imgContainerRect.left + neckLeftRect.width / 2,
y: neckLeftRect.top - imgContainerRect.top + neckLeftRect.height / 2
};
const neckRight = {
x: neckRightRect.left - imgContainerRect.left + neckRightRect.width / 2,
y: neckRightRect.top - imgContainerRect.top + neckRightRect.height / 2
};
const neckBottomLeft = {
x: neckBottomLeftRect.left - imgContainerRect.left + neckBottomLeftRect.width / 2,
y: neckBottomLeftRect.top - imgContainerRect.top + neckBottomLeftRect.height / 2
};
const neckBottomRight = {
x: neckBottomRightRect.left - imgContainerRect.left + neckBottomRightRect.width / 2,
y: neckBottomRightRect.top - imgContainerRect.top + neckBottomRightRect.height / 2
};
// 转换到原始图片坐标的函数
const toOriginalCoords = (x, y) => ({
x: (x - offsetX) / scale,
y: (y - offsetY) / scale
});
// 转换为原始图片坐标
const neckLeftCoords = toOriginalCoords(neckLeft.x, neckLeft.y);
const neckRightCoords = toOriginalCoords(neckRight.x, neckRight.y);
const neckBottomLeftCoords = toOriginalCoords(neckBottomLeft.x, neckBottomLeft.y);
const neckBottomRightCoords = toOriginalCoords(neckBottomRight.x, neckBottomRight.y);
// 容器中心点坐标(相对于图片容器)
const center = {
x: containerRect.left - imgContainerRect.left + containerRect.width / 2,
y: containerRect.top - imgContainerRect.top + containerRect.height / 2
};
resolve({
topLeft: [Math.round(neckLeftCoords.x), Math.round(neckLeftCoords.y)],
topRight: [Math.round(neckRightCoords.x), Math.round(neckRightCoords.y)],
bottomLeft: [Math.round(neckBottomLeftCoords.x), Math.round(neckBottomLeftCoords.y)],
bottomRight: [Math.round(neckBottomRightCoords.x), Math.round(neckBottomRightCoords.y)],
center: [Math.round(toOriginalCoords(center.x, center.y).x),
Math.round(toOriginalCoords(center.x, center.y).y)],
angle: Math.round(angle)
});
} catch (error) {
console.error("获取脖子点坐标失败:", error);
// 如果获取失败,回退到普通的getRotatedCorners函数
getRotatedCorners().then(resolve).catch(reject);
}
});
}
// 添加线条拖动功能相关代码
function initLineContainer() {
// 获取线条和点元素
const lineContainer = document.getElementById('line-container');
const linePoint1 = document.getElementById('line-point-1');
const linePoint2 = document.getElementById('line-point-2');
const linePoint3 = document.getElementById('line-point-3');
const linePoint4 = document.getElementById('line-point-4');
const linePoint5 = document.getElementById('line-point-5');
const linePoint6 = document.getElementById('line-point-6');
// 获取曲率控制点
const curvePoint1 = document.getElementById('curve-point-1');
const curvePoint2 = document.getElementById('curve-point-2');
const curvePoint3 = document.getElementById('curve-point-3');
const curvePoint4 = document.getElementById('curve-point-4');
// 获取曲线路径
const curveSegment1 = document.getElementById('curve-segment-1');
const curveSegment2 = document.getElementById('curve-segment-2');
const curveSegment3 = document.getElementById('curve-segment-3');
const curveSegment4 = document.getElementById('curve-segment-4');
// 创建曲线中点元素
function createCurveMidpoints() {
// 创建四个曲线中点元素
for (let i = 1; i <= 4; i++) {
const midpoint = document.createElement('div');
midpoint.id = `curve-midpoint-${i}`;
midpoint.className = 'curve-midpoint';
midpoint.style.cssText = `
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.7);
transform: translate(-50%, -50%);
z-index: 4;
pointer-events: none;
`;
lineContainer.appendChild(midpoint);
// 创建连接中点和控制点的线
const connectionLine = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
connectionLine.id = `connection-line-${i}`;
connectionLine.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 3;
pointer-events: none;
overflow: visible;
`;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.id = `line-${i}`;
line.setAttribute('stroke', 'rgba(255, 255, 255, 0.5)');
line.setAttribute('stroke-width', '1.5');
line.setAttribute('stroke-dasharray', '3,2');
connectionLine.appendChild(line);
lineContainer.appendChild(connectionLine);
}
}
// 计算二次贝塞尔曲线上的中点 (t=0.5)
function calculateBezierPoint(startX, startY, controlX, controlY, endX, endY, t) {
// 二次贝塞尔曲线公式: P(t) = (1-t)²*P0 + 2(1-t)t*P1 + t²*P2
const mt = 1 - t;
const mt2 = mt * mt;
const t2 = t * t;
return {
x: mt2 * startX + 2 * mt * t * controlX + t2 * endX,
y: mt2 * startY + 2 * mt * t * controlY + t2 * endY
};
}
// 更新曲线中点位置
function updateCurveMidpoints(positions) {
const containerWidth = lineContainer.offsetWidth;
const containerHeight = lineContainer.offsetHeight;
// 计算每条曲线的中点 (t=0.5)
const midpoint1 = calculateBezierPoint(
positions.point1.x, positions.point1.y,
positions.curve1.x, positions.curve1.y,
positions.point2.x, positions.point2.y,
0.5
);
const midpoint2 = calculateBezierPoint(
positions.point2.x, positions.point2.y,
positions.curve2.x, positions.curve2.y,
positions.point3.x, positions.point3.y,
0.5
);
const midpoint3 = calculateBezierPoint(
positions.point4.x, positions.point4.y,
positions.curve3.x, positions.curve3.y,
positions.point5.x, positions.point5.y,
0.5
);
const midpoint4 = calculateBezierPoint(
positions.point5.x, positions.point5.y,
positions.curve4.x, positions.curve4.y,
positions.point6.x, positions.point6.y,
0.5
);
// 更新中点元素位置
updateMidpointPosition('curve-midpoint-1', midpoint1, containerWidth, containerHeight);
updateMidpointPosition('curve-midpoint-2', midpoint2, containerWidth, containerHeight);
updateMidpointPosition('curve-midpoint-3', midpoint3, containerWidth, containerHeight);
updateMidpointPosition('curve-midpoint-4', midpoint4, containerWidth, containerHeight);
}
// 更新单个中点元素位置
function updateMidpointPosition(id, point, containerWidth, containerHeight) {
const midpoint = document.getElementById(id);
if (midpoint) {
// 计算百分比位置
const left = (point.x / containerWidth) * 100;
const top = (point.y / containerHeight) * 100;
// 更新样式
midpoint.style.left = left + '%';
midpoint.style.top = top + '%';
// 更新连接线
const index = id.split('-')[2];
updateConnectionLine(index, point, containerWidth, containerHeight);
}
}
// 更新连接线位置
function updateConnectionLine(index, midpoint, containerWidth, containerHeight) {
const line = document.getElementById(`line-${index}`);
const curvePoint = document.getElementById(`curve-point-${index}`);
if (line && curvePoint) {
const curveRect = curvePoint.getBoundingClientRect();
const containerRect = lineContainer.getBoundingClientRect();
// 获取控制点位置
const curveX = curveRect.left + curveRect.width / 2 - containerRect.left;
const curveY = curveRect.top + curveRect.height / 2 - containerRect.top;
// 设置线条坐标
line.setAttribute('x1', midpoint.x);
line.setAttribute('y1', midpoint.y);
line.setAttribute('x2', curveX);
line.setAttribute('y2', curveY);
}
}
// 初始化点的位置数据,防止首次拖动时跳变
const initPointPosition = (point) => {
const containerWidth = lineContainer.offsetWidth;
const containerHeight = lineContainer.offsetHeight;
const rect = point.getBoundingClientRect();
const containerRect = lineContainer.getBoundingClientRect();
// 计算点相对于容器的位置
const x = rect.left + rect.width / 2 - containerRect.left;
const y = rect.top + rect.height / 2 - containerRect.top;
// 将像素位置转换为相对于容器的偏移量
const offsetX = x - containerWidth / 2;
const offsetY = y - containerHeight / 2;
point.setAttribute('data-x', offsetX);
point.setAttribute('data-y', offsetY);
};
// 初始化所有点的位置数据
initPointPosition(linePoint1);
initPointPosition(linePoint2);
initPointPosition(linePoint3);
initPointPosition(linePoint4);
initPointPosition(linePoint5);
initPointPosition(linePoint6);
initPointPosition(curvePoint1);
initPointPosition(curvePoint2);
initPointPosition(curvePoint3);
initPointPosition(curvePoint4);
// 通过三个点计算贝塞尔曲线路径
function calculateCurvePath(startX, startY, controlX, controlY, endX, endY) {
return `M ${startX} ${startY} Q ${controlX} ${controlY} ${endX} ${endY}`;
}
// 计算线段中点位置
function calculateMidPoint(x1, y1, x2, y2) {
return { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
}
// 计算曲率控制点到线段的投影点位置
function projectPointToLine(lineX1, lineY1, lineX2, lineY2, pointX, pointY) {
// 线段的方向向量
const dx = lineX2 - lineX1;
const dy = lineY2 - lineY1;
// 线段长度的平方
const lineLength2 = dx * dx + dy * dy;
// 防止除以零
if (lineLength2 === 0) return { x: lineX1, y: lineY1 };
// 计算投影比例
const t = ((pointX - lineX1) * dx + (pointY - lineY1) * dy) / lineLength2;
// 限制t在[0,1]范围内,即投影点必须在线段上
const clampedT = Math.max(0, Math.min(1, t));
// 计算投影点坐标
return {
x: lineX1 + clampedT * dx,
y: lineY1 + clampedT * dy,
t: clampedT
};
}
// 约束点在垂直于线段且通过中点的直线上
function constrainToPerpendicularLine(lineX1, lineY1, lineX2, lineY2, pointX, pointY) {
// 计算线段中点
const midPoint = calculateMidPoint(lineX1, lineY1, lineX2, lineY2);
// 计算线段方向向量
const dx = lineX2 - lineX1;
const dy = lineY2 - lineY1;
// 计算垂直于线段的方向向量(顺时针旋转90度)
const perpDx = -dy;
const perpDy = dx;
// 计算垂直向量的长度
const perpLength = Math.sqrt(perpDx * perpDx + perpDy * perpDy);
if (perpLength === 0) return midPoint;
// 归一化垂直向量
const perpUnitDx = perpDx / perpLength;
const perpUnitDy = perpDy / perpLength;
// 计算控制点与中点之间在垂线方向上的投影长度
const vectorToPointX = pointX - midPoint.x;
const vectorToPointY = pointY - midPoint.y;
const projectionLength = vectorToPointX * perpUnitDx + vectorToPointY * perpUnitDy;
// 计算约束后的点坐标(沿垂线方向)
return {
x: midPoint.x + projectionLength * perpUnitDx,
y: midPoint.y + projectionLength * perpUnitDy
};
}
// 更新曲率控制点位置,确保它们在线段附近
function updateCurvePointsPosition() {
const containerRect = lineContainer.getBoundingClientRect();
// 获取点1、2、3的位置
const point1Rect = linePoint1.getBoundingClientRect();
const point2Rect = linePoint2.getBoundingClientRect();
const point3Rect = linePoint3.getBoundingClientRect();
// 计算点在容器内的相对位置
const point1X = point1Rect.left + point1Rect.width / 2 - containerRect.left;
const point1Y = point1Rect.top + point1Rect.height / 2 - containerRect.top;
const point2X = point2Rect.left + point2Rect.width / 2 - containerRect.left;
const point2Y = point2Rect.top + point2Rect.height / 2 - containerRect.top;
const point3X = point3Rect.left + point3Rect.width / 2 - containerRect.left;
const point3Y = point3Rect.top + point3Rect.height / 2 - containerRect.top;
// 获取曲率控制点1的位置
const curvePoint1Rect = curvePoint1.getBoundingClientRect();
const curve1X = curvePoint1Rect.left + curvePoint1Rect.width / 2 - containerRect.left;
const curve1Y = curvePoint1Rect.top + curvePoint1Rect.height / 2 - containerRect.top;
// 投影到线段1-2
const projection1 = projectPointToLine(point1X, point1Y, point2X, point2Y, curve1X, curve1Y);
// 获取曲率控制点2的位置
const curvePoint2Rect = curvePoint2.getBoundingClientRect();
const curve2X = curvePoint2Rect.left + curvePoint2Rect.width / 2 - containerRect.left;
const curve2Y = curvePoint2Rect.top + curvePoint2Rect.height / 2 - containerRect.top;
// 投影到线段2-3
const projection2 = projectPointToLine(point2X, point2Y, point3X, point3Y, curve2X, curve2Y);
// 获取点4、5、6的位置
const point4Rect = linePoint4.getBoundingClientRect();
const point5Rect = linePoint5.getBoundingClientRect();
const point6Rect = linePoint6.getBoundingClientRect();
// 计算点在容器内的相对位置
const point4X = point4Rect.left + point4Rect.width / 2 - containerRect.left;
const point4Y = point4Rect.top + point4Rect.height / 2 - containerRect.top;
const point5X = point5Rect.left + point5Rect.width / 2 - containerRect.left;
const point5Y = point5Rect.top + point5Rect.height / 2 - containerRect.top;
const point6X = point6Rect.left + point6Rect.width / 2 - containerRect.left;
const point6Y = point6Rect.top + point6Rect.height / 2 - containerRect.top;
// 获取曲率控制点3的位置
const curvePoint3Rect = curvePoint3.getBoundingClientRect();
const curve3X = curvePoint3Rect.left + curvePoint3Rect.width / 2 - containerRect.left;
const curve3Y = curvePoint3Rect.top + curvePoint3Rect.height / 2 - containerRect.top;
// 投影到线段4-5
const projection3 = projectPointToLine(point4X, point4Y, point5X, point5Y, curve3X, curve3Y);
// 获取曲率控制点4的位置
const curvePoint4Rect = curvePoint4.getBoundingClientRect();
const curve4X = curvePoint4Rect.left + curvePoint4Rect.width / 2 - containerRect.left;
const curve4Y = curvePoint4Rect.top + curvePoint4Rect.height / 2 - containerRect.top;
// 投影到线段5-6
const projection4 = projectPointToLine(point5X, point5Y, point6X, point6Y, curve4X, curve4Y);
return {
point1: { x: point1X, y: point1Y },
point2: { x: point2X, y: point2Y },
point3: { x: point3X, y: point3Y },
point4: { x: point4X, y: point4Y },
point5: { x: point5X, y: point5Y },
point6: { x: point6X, y: point6Y },
curve1: { x: curve1X, y: curve1Y, projection: projection1 },
curve2: { x: curve2X, y: curve2Y, projection: projection2 },
curve3: { x: curve3X, y: curve3Y, projection: projection3 },
curve4: { x: curve4X, y: curve4Y, projection: projection4 }
};
}
// 更新线条位置的函数
function updateLines() {
// 获取所有点的位置
const positions = updateCurvePointsPosition();
// 绘制曲线1
const curve1Path = calculateCurvePath(
positions.point1.x, positions.point1.y,
positions.curve1.x, positions.curve1.y,
positions.point2.x, positions.point2.y
);
curveSegment1.setAttribute('d', curve1Path);
// 绘制曲线2
const curve2Path = calculateCurvePath(
positions.point2.x, positions.point2.y,
positions.curve2.x, positions.curve2.y,
positions.point3.x, positions.point3.y
);
curveSegment2.setAttribute('d', curve2Path);
// 绘制曲线3
const curve3Path = calculateCurvePath(
positions.point4.x, positions.point4.y,
positions.curve3.x, positions.curve3.y,
positions.point5.x, positions.point5.y
);
curveSegment3.setAttribute('d', curve3Path);
// 绘制曲线4
const curve4Path = calculateCurvePath(
positions.point5.x, positions.point5.y,
positions.curve4.x, positions.curve4.y,
positions.point6.x, positions.point6.y
);
curveSegment4.setAttribute('d', curve4Path);
// 更新曲线中点位置
updateCurveMidpoints(positions);
}
// 初始化线条位置
createCurveMidpoints(); // 创建中点元素
updateLines();
// 更新曲率控制点位置使其保持在垂直线上
function updateCurvePointPosition(lineStartPoint, lineEndPoint, curvePoint, containerWidth, containerHeight) {
// 获取控制点的当前位置数据
const curveX = parseFloat(curvePoint.getAttribute('data-x')) || 0;
const curveY = parseFloat(curvePoint.getAttribute('data-y')) || 0;
// 转换为绝对坐标(相对于容器)
const absoluteCurveX = curveX + containerWidth / 2;
const absoluteCurveY = curveY + containerHeight / 2;
// 约束到垂直线上
const constrained = constrainToPerpendicularLine(
lineStartPoint.x, lineStartPoint.y,
lineEndPoint.x, lineEndPoint.y,
absoluteCurveX, absoluteCurveY
);
// 转回相对坐标
const newPosX = constrained.x - containerWidth / 2;
const newPosY = constrained.y - containerHeight / 2;
// 应用新位置
curvePoint.setAttribute('data-x', newPosX);
curvePoint.setAttribute('data-y', newPosY);
// 计算百分比位置
const left = (newPosX / containerWidth) * 100 + 50;
const top = (newPosY / containerHeight) * 100 + 50;
// 更新样式
curvePoint.style.left = left + '%';
curvePoint.style.top = top + '%';
}
// 端点拖动时,更新相关的曲率控制点位置
function updateRelatedCurvePoints(movedPointId, containerWidth, containerHeight) {
const containerRect = lineContainer.getBoundingClientRect();
// 获取所有点的位置
const points = {
p1: {
el: linePoint1,
rect: linePoint1.getBoundingClientRect(),
get pos() {
return {
x: this.rect.left + this.rect.width / 2 - containerRect.left,
y: this.rect.top + this.rect.height / 2 - containerRect.top
};
}
},
p2: {
el: linePoint2,
rect: linePoint2.getBoundingClientRect(),
get pos() {
return {
x: this.rect.left + this.rect.width / 2 - containerRect.left,
y: this.rect.top + this.rect.height / 2 - containerRect.top
};
}
},
p3: {
el: linePoint3,
rect: linePoint3.getBoundingClientRect(),
get pos() {
return {
x: this.rect.left + this.rect.width / 2 - containerRect.left,
y: this.rect.top + this.rect.height / 2 - containerRect.top
};
}
},
p4: {
el: linePoint4,
rect: linePoint4.getBoundingClientRect(),
get pos() {
return {
x: this.rect.left + this.rect.width / 2 - containerRect.left,
y: this.rect.top + this.rect.height / 2 - containerRect.top
};
}
},
p5: {
el: linePoint5,
rect: linePoint5.getBoundingClientRect(),
get pos() {
return {
x: this.rect.left + this.rect.width / 2 - containerRect.left,
y: this.rect.top + this.rect.height / 2 - containerRect.top
};
}
},
p6: {
el: linePoint6,
rect: linePoint6.getBoundingClientRect(),
get pos() {
return {
x: this.rect.left + this.rect.width / 2 - containerRect.left,
y: this.rect.top + this.rect.height / 2 - containerRect.top
};
}
}
};
// 根据移动的端点,更新相关曲率控制点
switch (movedPointId) {
case 'line-point-1':
updateCurvePointPosition(points.p1.pos, points.p2.pos, curvePoint1, containerWidth, containerHeight);
break;
case 'line-point-2':
updateCurvePointPosition(points.p1.pos, points.p2.pos, curvePoint1, containerWidth, containerHeight);
updateCurvePointPosition(points.p2.pos, points.p3.pos, curvePoint2, containerWidth, containerHeight);
break;
case 'line-point-3':
updateCurvePointPosition(points.p2.pos, points.p3.pos, curvePoint2, containerWidth, containerHeight);
break;
case 'line-point-4':
updateCurvePointPosition(points.p4.pos, points.p5.pos, curvePoint3, containerWidth, containerHeight);
break;
case 'line-point-5':
updateCurvePointPosition(points.p4.pos, points.p5.pos, curvePoint3, containerWidth, containerHeight);
updateCurvePointPosition(points.p5.pos, points.p6.pos, curvePoint4, containerWidth, containerHeight);
break;
case 'line-point-6':
updateCurvePointPosition(points.p5.pos, points.p6.pos, curvePoint4, containerWidth, containerHeight);
break;
}
}
// 获取所有端点坐标和曲率信息的函数
function getLinePointsAndCurvatures() {
return new Promise((resolve, reject) => {
try {
const containerRect = lineContainer.getBoundingClientRect();
const imgContainer = document.querySelector(".body-image");
const imgContainerRect = imgContainer.getBoundingClientRect();
// 获取所有端点元素
const linePoints = [
linePoint1, linePoint2, linePoint3,
linePoint4, linePoint5, linePoint6
];
// 获取所有曲率控制点
const curvePoints = [
curvePoint1, curvePoint2, curvePoint3, curvePoint4
];
// 图片原始尺寸和缩放参数
const naturalWidth = 640;
const naturalHeight = 400;
const scale = Math.max(
imgContainerRect.width / naturalWidth,
imgContainerRect.height / naturalHeight
);
const offsetX = (imgContainerRect.width - naturalWidth * scale) / 2;
const offsetY = (imgContainerRect.height - naturalHeight * scale) / 2;
// 转换到原始图片坐标的函数
const toOriginalCoords = (x, y) => ({
x: (x - offsetX) / scale,
y: (y - offsetY) / scale
});
// 计算所有端点像素坐标
const linePointsCoordinates = linePoints.map(point => {
const rect = point.getBoundingClientRect();
const containerX = rect.left + rect.width / 2 - containerRect.left;
const containerY = rect.top + rect.height / 2 - containerRect.top;
// 转换为原始图片坐标
const originalCoords = toOriginalCoords(
containerX + containerRect.left - imgContainerRect.left,
containerY + containerRect.top - imgContainerRect.top
);
return {
id: point.id,
containerX: Math.round(containerX),
containerY: Math.round(containerY),
x: Math.round(originalCoords.x),
y: Math.round(originalCoords.y)
};
});
// 计算曲线曲率信息
const curvatures = [];
// 计算第一条曲线的曲率
curvatures.push(calculateCurvature(
linePointsCoordinates[0].containerX, linePointsCoordinates[0].containerY,
linePointsCoordinates[1].containerX, linePointsCoordinates[1].containerY,
curvePoints[0], containerRect
));
// 计算第二条曲线的曲率
curvatures.push(calculateCurvature(
linePointsCoordinates[1].containerX, linePointsCoordinates[1].containerY,
linePointsCoordinates[2].containerX, linePointsCoordinates[2].containerY,
curvePoints[1], containerRect
));
// 计算第三条曲线的曲率
curvatures.push(calculateCurvature(
linePointsCoordinates[3].containerX, linePointsCoordinates[3].containerY,
linePointsCoordinates[4].containerX, linePointsCoordinates[4].containerY,
curvePoints[2], containerRect
));
// 计算第四条曲线的曲率
curvatures.push(calculateCurvature(
linePointsCoordinates[4].containerX, linePointsCoordinates[4].containerY,
linePointsCoordinates[5].containerX, linePointsCoordinates[5].containerY,
curvePoints[3], containerRect
));
resolve({
linePoints: linePointsCoordinates,
curvatures: curvatures
});
} catch (error) {
console.error("获取端点坐标和曲率失败:", error);
reject(error);
}
});
}
// 计算曲线曲率
function calculateCurvature(lineX1, lineY1, lineX2, lineY2, curvePoint, containerRect) {
// 获取控制点位置
const rect = curvePoint.getBoundingClientRect();
const curveX = rect.left + rect.width / 2 - containerRect.left;
const curveY = rect.top + rect.height / 2 - containerRect.top;
// 计算线段中点
const midPoint = calculateMidPoint(lineX1, lineY1, lineX2, lineY2);
// 计算线段长度
const lineLength = Math.sqrt(
Math.pow(lineX2 - lineX1, 2) +
Math.pow(lineY2 - lineY1, 2)
);
// 计算线段的法向量
const dx = lineX2 - lineX1;
const dy = lineY2 - lineY1;
const normalX = -dy;
const normalY = dx;
// 法向量的长度
const normalLength = Math.sqrt(normalX * normalX + normalY * normalY);
if (normalLength < 1e-6) {
// 避免除以接近零的值
return {
segmentId: curvePoint.id,
lineLength: lineLength,
distanceToMidPoint: 0,
distanceToLine: 0,
normalizedCurvature: 0,
curvature: 0, // 新增:与后端算法兼容的曲率值
controlPoint: {
x: curveX,
y: curveY
},
midPoint: midPoint,
projectionPoint: midPoint
};
}
// 计算控制点到投影点的矢量
const vectorX = curveX - midPoint.x;
const vectorY = curveY - midPoint.y;
// 计算控制点到线段的投影
const projection = projectPointToLine(
lineX1, lineY1,
lineX2, lineY2,
curveX, curveY
);
// 计算控制点到投影点的距离
const distanceToLine = Math.sqrt(
Math.pow(curveX - projection.x, 2) +
Math.pow(curveY - projection.y, 2)
);
// 计算点积以确定方向(正负号)
const dotProduct = vectorX * (normalX / normalLength) + vectorY * (normalY / normalLength);
const sign = Math.sign(dotProduct);
// 与后端算法兼容的曲率值
// 后端算法使用 curvature 乘以单位法向量来偏移中点,所以这里需要返回带符号的距离
const curvature = sign * distanceToLine;
// 标准化曲率(相对于线段长度)
const normalizedCurvature = distanceToLine / lineLength;
return {
segmentId: curvePoint.id,
lineLength: lineLength,
distanceToMidPoint: Math.sqrt(
Math.pow(curveX - midPoint.x, 2) +
Math.pow(curveY - midPoint.y, 2)
),
distanceToLine: distanceToLine,
normalizedCurvature: normalizedCurvature,
curvature: curvature, // 新增:与后端算法兼容的曲率值
controlPoint: {
x: curveX,
y: curveY
},
midPoint: midPoint,
projectionPoint: {
x: projection.x,
y: projection.y
}
};
}
// 设置主点的交互拖动
[linePoint1, linePoint2, linePoint3, linePoint4, linePoint5, linePoint6].forEach(point => {
interact(point)
.draggable({
inertia: false,
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent',
endOnly: true
})
],
autoScroll: true,
touchAction: 'none', // 添加此行,禁用默认触摸行为
onmove: function (event) {
// 获取当前位置
const target = event.target;
// 获取容器宽高
const containerWidth = lineContainer.offsetWidth;
const containerHeight = lineContainer.offsetHeight;
// 计算新位置(百分比)
const posX = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
const posY = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
// 保存新位置
target.setAttribute('data-x', posX);
target.setAttribute('data-y', posY);
// 计算中心位置
const left = (posX / containerWidth) * 100 + 50; // 加上50是因为初始transform是-50%
const top = (posY / containerHeight) * 100 + 50;
// 应用新位置
target.style.left = left + '%';
target.style.top = top + '%';
// 更新相关的曲率控制点位置
updateRelatedCurvePoints(target.id, containerWidth, containerHeight);
// 更新线条
updateLines();
// // 获取并打印点坐标和曲率信息
// getLinePointsAndCurvatures().then(data => {
// console.log('端点拖动 - 实时数据:', data);
// }).catch(error => {
// console.error('获取数据失败:', error);
// });
}
});
});
// 设置曲率控制点的交互拖动
[curvePoint1, curvePoint2, curvePoint3, curvePoint4].forEach((point, index) => {
interact(point)
.draggable({
inertia: false,
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent',
endOnly: true
})
],
autoScroll: true,
touchAction: 'none', // 添加此行,禁用默认触摸行为
onmove: function (event) {
// 获取当前位置
const target = event.target;
// 获取容器宽高
const containerWidth = lineContainer.offsetWidth;
const containerHeight = lineContainer.offsetHeight;
// 计算新位置(百分比)
let posX = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
let posY = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
// 获取当前控制点ID和对应的线段端点
const pointId = target.id;
const containerRect = lineContainer.getBoundingClientRect();
// 根据控制点ID获取对应的线段端点
let startPoint, endPoint;
if (pointId === 'curve-point-1') {
// 获取线段1-2的端点位置
const point1Rect = linePoint1.getBoundingClientRect();
const point2Rect = linePoint2.getBoundingClientRect();
startPoint = {
x: point1Rect.left + point1Rect.width / 2 - containerRect.left,
y: point1Rect.top + point1Rect.height / 2 - containerRect.top
};
endPoint = {
x: point2Rect.left + point2Rect.width / 2 - containerRect.left,
y: point2Rect.top + point2Rect.height / 2 - containerRect.top
};
} else if (pointId === 'curve-point-2') {
// 获取线段2-3的端点位置
const point2Rect = linePoint2.getBoundingClientRect();
const point3Rect = linePoint3.getBoundingClientRect();
startPoint = {
x: point2Rect.left + point2Rect.width / 2 - containerRect.left,
y: point2Rect.top + point2Rect.height / 2 - containerRect.top
};
endPoint = {
x: point3Rect.left + point3Rect.width / 2 - containerRect.left,
y: point3Rect.top + point3Rect.height / 2 - containerRect.top
};
} else if (pointId === 'curve-point-3') {
// 获取线段4-5的端点位置
const point4Rect = linePoint4.getBoundingClientRect();
const point5Rect = linePoint5.getBoundingClientRect();
startPoint = {
x: point4Rect.left + point4Rect.width / 2 - containerRect.left,
y: point4Rect.top + point4Rect.height / 2 - containerRect.top
};
endPoint = {
x: point5Rect.left + point5Rect.width / 2 - containerRect.left,
y: point5Rect.top + point5Rect.height / 2 - containerRect.top
};
} else if (pointId === 'curve-point-4') {
// 获取线段5-6的端点位置
const point5Rect = linePoint5.getBoundingClientRect();
const point6Rect = linePoint6.getBoundingClientRect();
startPoint = {
x: point5Rect.left + point5Rect.width / 2 - containerRect.left,
y: point5Rect.top + point5Rect.height / 2 - containerRect.top
};
endPoint = {
x: point6Rect.left + point6Rect.width / 2 - containerRect.left,
y: point6Rect.top + point6Rect.height / 2 - containerRect.top
};
}
// 转换当前拖动位置为绝对坐标
const absoluteX = posX + containerWidth / 2;
const absoluteY = posY + containerHeight / 2;
// 约束点在垂直于线段的直线上
if (startPoint && endPoint) {
const constrained = constrainToPerpendicularLine(
startPoint.x, startPoint.y,
endPoint.x, endPoint.y,
absoluteX, absoluteY
);
// 转回相对坐标
posX = constrained.x - containerWidth / 2;
posY = constrained.y - containerHeight / 2;
}
// 保存新位置
target.setAttribute('data-x', posX);
target.setAttribute('data-y', posY);
// 计算中心位置
const left = (posX / containerWidth) * 100 + 50; // 加上50是因为初始transform是-50%
const top = (posY / containerHeight) * 100 + 50;
// 应用新位置
target.style.left = left + '%';
target.style.top = top + '%';
// 更新线条
updateLines();
// 获取并打印点坐标和曲率信息
getLinePointsAndCurvatures().then(data => {
console.log('控制点拖动 - 实时数据:', data);
}).catch(error => {
console.error('获取数据失败:', error);
});
}
});
});
lineContainer.style.display = "none";
}
// 添加全局版本的曲线坐标和曲率获取函数
window.getLinePointsAndCurvatures = function () {
return new Promise((resolve, reject) => {
try {
const lineContainer = document.getElementById('line-container');
const containerRect = lineContainer.getBoundingClientRect();
const imgContainer = document.querySelector(".body-image");
const imgContainerRect = imgContainer.getBoundingClientRect();
// 获取所有端点元素
const linePoints = [
document.getElementById('line-point-1'),
document.getElementById('line-point-2'),
document.getElementById('line-point-3'),
document.getElementById('line-point-4'),
document.getElementById('line-point-5'),
document.getElementById('line-point-6')
];
// 获取所有曲率控制点
const curvePoints = [
document.getElementById('curve-point-1'),
document.getElementById('curve-point-2'),
document.getElementById('curve-point-3'),
document.getElementById('curve-point-4')
];
// 图片原始尺寸和缩放参数
const naturalWidth = 640;
const naturalHeight = 400;
const scale = Math.max(
imgContainerRect.width / naturalWidth,
imgContainerRect.height / naturalHeight
);
const offsetX = (imgContainerRect.width - naturalWidth * scale) / 2;
const offsetY = (imgContainerRect.height - naturalHeight * scale) / 2;
// 转换到原始图片坐标的函数
const toOriginalCoords = (x, y) => ({
x: (x - offsetX) / scale,
y: (y - offsetY) / scale
});
// 计算所有端点像素坐标
const linePointsCoordinates = linePoints.map(point => {
const rect = point.getBoundingClientRect();
const containerX = rect.left + rect.width / 2 - containerRect.left;
const containerY = rect.top + rect.height / 2 - containerRect.top;
// 转换为原始图片坐标
const originalCoords = toOriginalCoords(
containerX + containerRect.left - imgContainerRect.left,
containerY + containerRect.top - imgContainerRect.top
);
return {
id: point.id,
containerX: Math.round(containerX),
containerY: Math.round(containerY),
originalX: Math.round(originalCoords.x),
originalY: Math.round(originalCoords.y)
};
});
// 计算曲线曲率信息
const curvatures = [];
// 计算线段中点
const calculateMidPoint = (x1, y1, x2, y2) => {
return { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
};
// 计算点到线段的投影
const projectPointToLine = (lineX1, lineY1, lineX2, lineY2, pointX, pointY) => {
// 线段的方向向量
const dx = lineX2 - lineX1;
const dy = lineY2 - lineY1;
// 线段长度的平方
const lineLength2 = dx * dx + dy * dy;
// 防止除以零
if (lineLength2 === 0) return { x: lineX1, y: lineY1 };
// 计算投影比例
const t = ((pointX - lineX1) * dx + (pointY - lineY1) * dy) / lineLength2;
// 限制t在[0,1]范围内,即投影点必须在线段上
const clampedT = Math.max(0, Math.min(1, t));
// 计算投影点坐标
return {
x: lineX1 + clampedT * dx,
y: lineY1 + clampedT * dy,
t: clampedT
};
};
// 计算曲率的辅助函数
const calculateCurvature = (lineX1, lineY1, lineX2, lineY2, curvePoint, containerRect) => {
// 获取控制点位置
const rect = curvePoint.getBoundingClientRect();
const curveX = rect.left + rect.width / 2 - containerRect.left;
const curveY = rect.top + rect.height / 2 - containerRect.top;
// 计算线段中点
const midPoint = calculateMidPoint(lineX1, lineY1, lineX2, lineY2);
// 计算线段长度
const lineLength = Math.sqrt(
Math.pow(lineX2 - lineX1, 2) +
Math.pow(lineY2 - lineY1, 2)
);
// 计算线段的法向量
const dx = lineX2 - lineX1;
const dy = lineY2 - lineY1;
const normalX = -dy;
const normalY = dx;
// 法向量的长度
const normalLength = Math.sqrt(normalX * normalX + normalY * normalY);
if (normalLength < 1e-6) {
// 避免除以接近零的值
return {
segmentId: curvePoint.id,
lineLength: lineLength,
distanceToMidPoint: 0,
distanceToLine: 0,
normalizedCurvature: 0,
curvature: 0, // 新增:与后端算法兼容的曲率值
controlPoint: {
x: curveX,
y: curveY
},
midPoint: midPoint,
projectionPoint: midPoint
};
}
// 计算控制点到投影点的矢量
const vectorX = curveX - midPoint.x;
const vectorY = curveY - midPoint.y;
// 计算控制点到线段的投影
const projection = projectPointToLine(
lineX1, lineY1,
lineX2, lineY2,
curveX, curveY
);
// 计算控制点到投影点的距离
const distanceToLine = Math.sqrt(
Math.pow(curveX - projection.x, 2) +
Math.pow(curveY - projection.y, 2)
);
// 计算点积以确定方向(正负号)
const dotProduct = vectorX * (normalX / normalLength) + vectorY * (normalY / normalLength);
const sign = Math.sign(dotProduct);
// 与后端算法兼容的曲率值
// 后端算法使用 curvature 乘以单位法向量来偏移中点,所以这里需要返回带符号的距离
const curvature = sign * distanceToLine;
// 标准化曲率(相对于线段长度)
const normalizedCurvature = distanceToLine / lineLength;
return {
segmentId: curvePoint.id,
lineLength: lineLength,
distanceToMidPoint: Math.sqrt(
Math.pow(curveX - midPoint.x, 2) +
Math.pow(curveY - midPoint.y, 2)
),
distanceToLine: distanceToLine,
normalizedCurvature: normalizedCurvature,
curvature: curvature, // 新增:与后端算法兼容的曲率值
controlPoint: {
x: curveX,
y: curveY
},
midPoint: midPoint,
projectionPoint: {
x: projection.x,
y: projection.y
}
};
};
// 计算所有曲线的曲率
curvatures.push(calculateCurvature(
linePointsCoordinates[0].containerX, linePointsCoordinates[0].containerY,
linePointsCoordinates[1].containerX, linePointsCoordinates[1].containerY,
curvePoints[0], containerRect
));
curvatures.push(calculateCurvature(
linePointsCoordinates[1].containerX, linePointsCoordinates[1].containerY,
linePointsCoordinates[2].containerX, linePointsCoordinates[2].containerY,
curvePoints[1], containerRect
));
curvatures.push(calculateCurvature(
linePointsCoordinates[3].containerX, linePointsCoordinates[3].containerY,
linePointsCoordinates[4].containerX, linePointsCoordinates[4].containerY,
curvePoints[2], containerRect
));
curvatures.push(calculateCurvature(
linePointsCoordinates[4].containerX, linePointsCoordinates[4].containerY,
linePointsCoordinates[5].containerX, linePointsCoordinates[5].containerY,
curvePoints[3], containerRect
));
resolve({
linePoints: linePointsCoordinates,
curvatures: curvatures
});
} catch (error) {
console.error("获取端点坐标和曲率失败:", error);
reject(error);
}
});
};
// // 在页面卸载时清除定时器
// window.addEventListener('beforeunload', function () {
// if (timer) {
// clearInterval(timer);
// timer = null;
// }
// });