3552 lines
118 KiB
JavaScript
Executable File
3552 lines
118 KiB
JavaScript
Executable File
// 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: `
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>本设备必须由</span>
|
||
<span class="hint-bold-text">经过培训的专业人员</span>
|
||
<span>操作使用,非专业人员请勿擅自操作。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">孕妇、心血管疾病患者、局部皮肤破损者</span>
|
||
<span>等禁止使用。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">体内有金属者、佩戴金属饰品、严重骨质疏松患者</span>
|
||
<span>及其他身体不适者在理疗前告知理疗师。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">生理期与哺乳期</span>
|
||
<span>不建议进行理疗,建议根据身体状况选择理疗时间。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">空腹、醉酒</span>
|
||
<span>等状态下不宜进行理疗。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>冬季理疗时请避免</span>
|
||
<span class="hint-bold-text">穿过厚、过宽松的衣物</span>
|
||
<span>,以确保最佳效果。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item" style="margin-top: 20px; font-weight: 600;">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>请确认安装的按摩头为</span>
|
||
<span class="hint-bold-text" style="color: #FF5722;">${selectedHeadText}</span>
|
||
<span>按摩头</span>
|
||
</div>
|
||
</div>
|
||
`,
|
||
en: `
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>This device must be operated by </span>
|
||
<span class="hint-bold-text">certified professionals only</span>
|
||
<span>. Unauthorized use is prohibited.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">Pregnant women, cardiovascular patients, individuals with skin lesions</span>
|
||
<span> are strictly prohibited from use.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">Metal implants, jewelry wearers, osteoporosis patients</span>
|
||
<span> must inform therapist prior to treatment.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">Menstruation and lactation periods</span>
|
||
<span> require medical consultation before therapy.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">Fasting or intoxication</span>
|
||
<span> are contraindicated for therapy.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>In winter, avoid </span>
|
||
<span class="hint-bold-text">bulky clothing</span>
|
||
<span> to ensure therapeutic efficacy.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item" style="margin-top: 20px; font-weight: 600;">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>Confirm installed head: </span>
|
||
<span class="hint-bold-text" style="color: #FF5722;">${selectedHeadText}</span>
|
||
</div>
|
||
</div>`,
|
||
jp: `
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>本機器は</span>
|
||
<span class="hint-bold-text">訓練を受けた専門家のみ</span>
|
||
<span>が操作可能です。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">妊婦・心臓病患者・皮膚損傷者</span>
|
||
<span>は使用禁止です。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">体内金属・装身具・骨粗鬆症患者</span>
|
||
<span>は施術前に申告してください。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">生理中・授乳中</span>
|
||
<span>の方は医師にご相談ください。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">空腹時・飲酒時</span>
|
||
<span>の施術は避けてください。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>冬季は</span>
|
||
<span class="hint-bold-text">厚着を避け</span>
|
||
<span>効果を確保してください。</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item" style="margin-top: 20px; font-weight: 600;">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>装着ヘッド確認: </span>
|
||
<span class="hint-bold-text" style="color: #FF5722;">${selectedHeadText}</span>
|
||
</div>
|
||
</div>`,
|
||
ko: `
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>본 장비는 </span>
|
||
<span class="hint-bold-text">훈련된 전문가만</span>
|
||
<span> 조작 가능합니다.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">임산부, 심혈관 질환자, 피부 손상자</span>
|
||
<span> 사용 금지.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">체내 금속, 장신구, 골다공증 환자</span>
|
||
<span>는 시술 전 알려주세요.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">생리 중, 수유 중</span>
|
||
<span>에는 의사 상담이 필요합니다.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span class="hint-bold-text">공복, 음주 상태</span>
|
||
<span>에서는 사용을 자제하세요.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>겨울철 </span>
|
||
<span class="hint-bold-text">두꺼운 옷 착용을 피하고</span>
|
||
<span> 효과를 극대화하세요.</span>
|
||
</div>
|
||
</div>
|
||
<div class="hint-item" style="margin-top: 20px; font-weight: 600;">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>장착 헤드 확인: </span>
|
||
<span class="hint-bold-text" style="color: #FF5722;">${selectedHeadText}</span>
|
||
</div>
|
||
</div>`
|
||
}
|
||
|
||
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 += `
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>${level}</span>
|
||
<span class="hint-bold-text">${(34 + i * 5) + '-' + (35 + i * 5)}℃</span>
|
||
</div>
|
||
</div>`
|
||
}
|
||
if(levelLen === 3) {
|
||
msg += `
|
||
<div class="hint-item">
|
||
<img class="hint-icon" src="static/images/hint/hint.png" alt="">
|
||
<div>
|
||
<span>${level}</span>
|
||
<span class="hint-bold-text">${(33 + i * 3) + '-' + (35 + i * 3)}℃</span>
|
||
</div>
|
||
</div>`
|
||
}
|
||
}
|
||
|
||
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;
|
||
// }
|
||
// });
|