944 lines
42 KiB
JavaScript
944 lines
42 KiB
JavaScript
|
||
(function(){
|
||
|
||
const canvas = document.getElementById('unifiedCanvas');
|
||
const ctx = canvas.getContext('2d');
|
||
// 获取当前选中的屏幕尺寸
|
||
const screenSelect = document.getElementById('epdwh');
|
||
let W = 800, H = 480;
|
||
|
||
function updateCanvasSize() {
|
||
const [newW, newH] = screenSelect.value.split('x').map(Number);
|
||
W = newW;
|
||
H = newH;
|
||
canvas.width = W;
|
||
canvas.height = H;
|
||
}
|
||
|
||
// 初始化尺寸
|
||
updateCanvasSize();
|
||
|
||
let currentTemplate = 'schedule';
|
||
let globalTitle = "◉ 输入内容·页眉";
|
||
|
||
|
||
let presetLinesData = [];
|
||
let hitAreas = [];
|
||
|
||
|
||
let courseData = {
|
||
days: ["周一", "周二", "周三", "周四", "周五"],
|
||
periods: ["第1节", "第2节", "第3节", "第4节"],
|
||
cells: {}
|
||
};
|
||
function initDefaultCourse() {
|
||
courseData.cells = {};
|
||
const defaults = {
|
||
"0,0": "语文", "0,1": "数学", "0,2": "英语", "0,3": "体育",
|
||
"1,0": "数学", "1,1": "语文", "1,2": "物理", "1,3": "历史",
|
||
"2,0": "英语", "2,1": "化学", "2,2": "语文", "2,3": "自习",
|
||
"3,0": "物理", "3,1": "英语", "3,2": "数学", "3,3": "政治",
|
||
"4,0": "历史", "4,1": "体育", "4,2": "化学", "4,3": "班会"
|
||
};
|
||
for(let k in defaults) courseData.cells[k] = defaults[k];
|
||
}
|
||
initDefaultCourse();
|
||
let courseHitAreas = [];
|
||
|
||
|
||
function applyCourseCustomSettings() {
|
||
const dayCount = parseInt(document.getElementById('courseDaysCount').value) || 5;
|
||
const periodCount = parseInt(document.getElementById('coursePeriodsCount').value) || 4;
|
||
|
||
const dayNames = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
||
courseData.days = dayNames.slice(0, dayCount);
|
||
|
||
courseData.periods = [];
|
||
for(let i=1; i<=periodCount; i++) {
|
||
courseData.periods.push(`第${i}节`);
|
||
}
|
||
|
||
courseData.cells = {};
|
||
render();
|
||
}
|
||
|
||
|
||
let qrText = "https://mspppclock.tech/";
|
||
let qrSize = 150;
|
||
let qrCodeImage = null;
|
||
let qrCaptionLines = [];
|
||
let qrCaptionStyle = { fontSize: 14, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "center" };
|
||
|
||
function syncCaptionFromTextarea() {
|
||
const raw = document.getElementById('qrCaptionText').value;
|
||
const lines = raw.split(/\r?\n/).filter(l => l.trim() !== "" || l === "");
|
||
qrCaptionLines = lines.length ? lines : ["(点击添加文字)"];
|
||
}
|
||
function updateCaptionLinesToUI() {
|
||
document.getElementById('qrCaptionText').value = qrCaptionLines.join("\n");
|
||
}
|
||
|
||
async function generateQRCodeImage(text, size=90, colorDark="#000000", colorLight="#FFFFFF") {
|
||
return new Promise((resolve) => {
|
||
const tempDiv = document.createElement('div');
|
||
tempDiv.style.position = 'absolute';
|
||
tempDiv.style.left = '-9999px';
|
||
document.body.appendChild(tempDiv);
|
||
try {
|
||
const qrcode = new QRCode(tempDiv, {
|
||
text: text,
|
||
width: size,
|
||
height: size,
|
||
colorDark: colorDark,
|
||
colorLight: colorLight,
|
||
correctLevel: QRCode.CorrectLevel.M
|
||
});
|
||
setTimeout(() => {
|
||
const canvasQR = tempDiv.querySelector('canvas');
|
||
if (canvasQR) {
|
||
const imgData = canvasQR.toDataURL();
|
||
resolve(imgData);
|
||
} else resolve(null);
|
||
document.body.removeChild(tempDiv);
|
||
}, 60);
|
||
} catch(e) { resolve(null); document.body.removeChild(tempDiv); }
|
||
});
|
||
}
|
||
|
||
async function refreshQRImage() {
|
||
qrSize = parseInt(document.getElementById('qrSizeInput').value) || 84;
|
||
const img = await generateQRCodeImage(qrText, qrSize, "#000", "#fff");
|
||
qrCodeImage = img;
|
||
if(currentTemplate === 'qrcode') render();
|
||
}
|
||
|
||
|
||
let customRawText = "小雨在放学路上捡到一枚【闪闪发亮的硬币】。\n她买了两颗【橘子味的糖果】,一颗塞进嘴里,\n甜得她眯起了眼睛;另一颗放在路边的长椅上。\n一只【唱歌的小鸟】飞过来,歪着脑袋看了看\n,叼起糖果飞走了。小雨有点失落,\n但还是继续往家走。第二天早上,她推开窗户,\n发现窗台上放着一朵【金黄色的小野花】,花瓣上还挂着露珠。小雨捧着花笑了:\n“原来快乐会【越分越多】呀。”\n从那天起,她每天都往长椅上放一颗糖。";
|
||
let customStyle = {
|
||
fontSize: 17, fontFamily: "宋体", baseColor: "#000000", invertMode: "none", alignment: "center"
|
||
};
|
||
|
||
const chineseFonts = ["宋体","微软雅黑","黑体","楷体","思源黑体","苹方"];
|
||
const invertModes = [
|
||
{ value: "none", label: "无" }, { value: "red-black", label: "红字黑底" }, { value: "red-white", label: "红字白底" },
|
||
{ value: "black-red", label: "黑字红底" }, { value: "black-white", label: "黑字白底" },
|
||
{ value: "white-red", label: "白字红底" }, { value: "white-black", label: "白字黑底" }
|
||
];
|
||
|
||
function applyInvertStyle(baseColor, invertMode) {
|
||
if (invertMode === "none") return { bgColor: null, textColor: baseColor };
|
||
if (invertMode === "red-black") return { bgColor: "#000000", textColor: "#FF0000" };
|
||
if (invertMode === "red-white") return { bgColor: "#FFFFFF", textColor: "#FF0000" };
|
||
if (invertMode === "black-red") return { bgColor: "#FF0000", textColor: "#000000" };
|
||
if (invertMode === "black-white") return { bgColor: "#FFFFFF", textColor: "#000000" };
|
||
if (invertMode === "white-red") return { bgColor: "#FF0000", textColor: "#FFFFFF" };
|
||
if (invertMode === "white-black") return { bgColor: "#000000", textColor: "#FFFFFF" };
|
||
return { bgColor: null, textColor: baseColor };
|
||
}
|
||
|
||
const templatePresets = {
|
||
schedule: [ { text: "✅ 今日核心议程", fontSize: 24, fontFamily: "微软雅黑", baseColor: "#FF0000", invertMode: "none", alignment: "center" },
|
||
{ text: "【09:30】 敏捷站会 · 【需求对齐】", fontSize: 18,fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "left" },
|
||
{ text: "11:00 前端架构评审会议", fontSize: 18, fontFamily: "黑体", baseColor: "#000000", invertMode: "black-red", alignment: "left" },
|
||
{ text: "14:30 跨部门协作同步会", fontSize: 18, fontFamily: "微软雅黑", baseColor: "#FF0000", invertMode: "red-white", alignment: "left" },
|
||
{ text: "16:45 代码审查 + 项目复盘", fontSize: 18, fontFamily: "楷体", baseColor: "#000000", invertMode: "none", alignment: "left" }
|
||
],
|
||
businesscard: [ { text: "✦ 名 片 精 选 ✦", fontSize: 24, fontFamily: "微软雅黑", baseColor: "#FF0000", invertMode: "none", alignment: "center" },
|
||
{ text: "欧阳 静澜", fontSize: 26, fontFamily: "黑体", baseColor: "#000000", invertMode: "black-white", alignment: "center" },
|
||
{ text: "创意设计总监 / 合伙人", fontSize: 18, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "center" },
|
||
{ text: "jinglan@art.cn", fontSize: 17, fontFamily: "宋体", baseColor: "#FF0000", invertMode: "red-black", alignment: "center" },
|
||
{ text: "三色哲学 · 红黑白", fontSize: 16, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "center" }
|
||
],
|
||
memo: [ { text: "📋 紧 急 备 忘 录", fontSize: 28, fontFamily: "微软雅黑", baseColor: "#FF0000", invertMode: "none", alignment: "center" },
|
||
{ text: "• 季度总结报告 (周五截止)", fontSize: 24, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "black-red", alignment: "left" },
|
||
{ text: "• 设计评审会议 14:00", fontSize: 24, fontFamily: "宋体", baseColor: "#000000", invertMode: "none", alignment: "left" },
|
||
{ text: "• 前端组件库升级计划", fontSize: 24, fontFamily: "黑体", baseColor: "#FF0000", invertMode: "red-white", alignment: "left" },
|
||
{ text: "• 团队建设活动策划", fontSize: 23, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "left" }
|
||
]
|
||
};
|
||
function loadPresetData(templateId) {
|
||
if(templatePresets[templateId]) presetLinesData = JSON.parse(JSON.stringify(templatePresets[templateId]));
|
||
else presetLinesData = [{ text: "示例行", fontSize: 16, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "left" }];
|
||
}
|
||
|
||
|
||
function renderCourseTable() {
|
||
ctx.fillStyle = "#FFFFFF";
|
||
ctx.fillRect(0, 0, W, H);
|
||
|
||
const isSmallScreen = (W === 296 && H === 128);
|
||
const isLargeScreen = (W === 800 && H === 480);
|
||
|
||
if (!isSmallScreen) {
|
||
// 显示页眉
|
||
const headerFontSize = isLargeScreen ? 36 : 18;
|
||
ctx.strokeStyle = "#000000";
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(4, 4, W-8, H-8);
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillRect(8, 8, 26, 4);
|
||
ctx.fillStyle = "#000000";
|
||
ctx.fillRect(W-54, 12, 42, 4);
|
||
|
||
let title = globalTitle.trim() || "输入学校班级学期等信息";
|
||
|
||
// 页眉标题
|
||
ctx.font = `bold ${headerFontSize}px '微软雅黑'`;
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillText(title, 20, headerFontSize + 12);
|
||
|
||
ctx.beginPath();
|
||
ctx.strokeStyle = "#FF0000";
|
||
ctx.moveTo(12, headerFontSize + 22);
|
||
ctx.lineTo(W-12, headerFontSize + 22);
|
||
ctx.stroke();
|
||
|
||
var startY = headerFontSize + 30;
|
||
} else {
|
||
ctx.strokeStyle = "#FF0000";
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(4, 4, W-8, H-8);
|
||
var startY = 8;
|
||
}
|
||
|
||
const dayCount = courseData.days.length;
|
||
const periodCount = courseData.periods.length;
|
||
const startX = 12;
|
||
const cellW = (W - 24) / (dayCount + 1);
|
||
const cellH = (H - startY - 8) / (periodCount + 1);
|
||
|
||
const headerTextFontSize = isLargeScreen ? 30 : 15;
|
||
const cellTextFontSize = isLargeScreen ? 28 : (isSmallScreen ? 11 : 14);
|
||
|
||
// 绘制节次表头
|
||
ctx.font = `bold ${headerTextFontSize}px '微软雅黑'`;
|
||
ctx.fillStyle = "#000000";
|
||
ctx.fillRect(startX, startY, cellW, cellH);
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.textAlign = "center";
|
||
ctx.textBaseline = "middle";
|
||
ctx.fillText("节次", startX + cellW/2, startY + cellH/2);
|
||
|
||
// 绘制星期表头
|
||
for(let d=0; d<dayCount; d++) {
|
||
ctx.fillStyle = d%2===0 ? "#000000" : "#FF0000";
|
||
ctx.fillRect(startX + (d+1)*cellW, startY, cellW, cellH);
|
||
ctx.fillStyle = "#FFFFFF";
|
||
ctx.font = `bold ${headerTextFontSize}px '微软雅黑'`;
|
||
ctx.fillText(courseData.days[d], startX + (d+1)*cellW + cellW/2, startY + cellH/2);
|
||
}
|
||
|
||
courseHitAreas = [];
|
||
|
||
// 绘制课程内容
|
||
for(let p=0; p<periodCount; p++) {
|
||
const yPos = startY + (p+1)*cellH;
|
||
|
||
// 节次列
|
||
ctx.fillStyle = "#000000";
|
||
ctx.fillRect(startX, yPos, cellW, cellH);
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.font = `bold ${cellTextFontSize}px '微软雅黑'`;
|
||
ctx.fillText(courseData.periods[p], startX + cellW/2, yPos + cellH/2);
|
||
|
||
for(let d=0; d<dayCount; d++) {
|
||
const x = startX + (d+1)*cellW;
|
||
const y = yPos;
|
||
const cellKey = `${d},${p}`;
|
||
let courseName = courseData.cells[cellKey] || "点击";
|
||
|
||
ctx.fillStyle = "#F0F0F0";
|
||
ctx.fillRect(x, y, cellW, cellH);
|
||
|
||
// 获取单元格样式(如果没有则使用默认值)
|
||
const cellStyle = courseData.cellStyles?.[cellKey] || {
|
||
fontSize: cellTextFontSize,
|
||
fontFamily: "微软雅黑",
|
||
color: "#000000"
|
||
};
|
||
|
||
// 限制字体大小不超过单元格高度的60%
|
||
const finalFontSize = Math.min(cellStyle.fontSize, cellH * 0.6);
|
||
ctx.font = `${finalFontSize}px "${cellStyle.fontFamily}"`;
|
||
ctx.fillStyle = cellStyle.color;
|
||
ctx.textAlign = "center";
|
||
ctx.textBaseline = "middle";
|
||
|
||
// 文字截断处理
|
||
let maxChars = isLargeScreen ? 6 : (isSmallScreen ? 2 : 4);
|
||
let displayName = courseName.length > maxChars ? courseName.slice(0, maxChars-1) + "…" : courseName;
|
||
|
||
// 如果文字仍然太宽,进一步缩小字体
|
||
let tempFontSize = finalFontSize;
|
||
let textWidth = ctx.measureText(displayName).width;
|
||
while (textWidth > cellW - 8 && tempFontSize > 10) {
|
||
tempFontSize--;
|
||
ctx.font = `${tempFontSize}px "${cellStyle.fontFamily}"`;
|
||
textWidth = ctx.measureText(displayName).width;
|
||
}
|
||
|
||
ctx.fillText(displayName, x + cellW/2, y + cellH/2);
|
||
|
||
courseHitAreas.push({ row: p, col: d, x, y, w: cellW, h: cellH, cellKey, currentName: courseName });
|
||
}
|
||
}
|
||
|
||
if (!isSmallScreen) {
|
||
// 底部装饰线
|
||
ctx.fillStyle = "#000000";
|
||
ctx.fillRect(16, H-11, 42, 2);
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillRect(66, H-13, 26, 2);
|
||
}
|
||
|
||
|
||
ctx.textAlign = "left";
|
||
ctx.textBaseline = "alphabetic";
|
||
|
||
|
||
ctx.beginPath();
|
||
ctx.strokeStyle = "#ff0000";
|
||
ctx.lineWidth = 1;
|
||
ctx.setLineDash([]);
|
||
|
||
for(let d = 1; d <= dayCount; d++){
|
||
ctx.beginPath();
|
||
ctx.moveTo(startX + d * cellW, startY);
|
||
ctx.lineTo(startX + d * cellW, startY + (periodCount + 1) * cellH);
|
||
ctx.stroke();
|
||
}
|
||
|
||
for(let p = 1; p <= periodCount; p++){
|
||
ctx.beginPath();
|
||
ctx.moveTo(startX, startY + p * cellH);
|
||
ctx.lineTo(startX + (dayCount + 1) * cellW, startY + p * cellH);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
function editCourseCell(row, col, currentName) {
|
||
|
||
const cellKey = `${col},${row}`;
|
||
const existingStyle = courseData.cellStyles?.[cellKey] || {
|
||
fontSize: 16,
|
||
fontFamily: "微软雅黑",
|
||
color: "#000000"
|
||
};
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-mask';
|
||
modal.innerHTML = `
|
||
<div class="modal-card" style="width: 320px;">
|
||
<h3>编辑课程</h3>
|
||
<div class="modal-field"><label>课程名称</label><input id="courseNameInput" value="${escapeHtml(currentName)}" style="width:100%;"></div>
|
||
<div class="modal-field"><label>字体大小</label><input id="fontSizeInput" type="number" value="${existingStyle.fontSize}" min="10" max="48" step="1" style="width:100%;"></div>
|
||
<div class="modal-field"><label>字体</label><select id="fontFamilyInput" style="width:100%;">${chineseFonts.map(f => `<option value="${f}" ${existingStyle.fontFamily === f ? 'selected' : ''}>${f}</option>`).join('')}</select></div>
|
||
<div class="modal-field"><label>文字颜色</label><select id="colorInput" style="width:100%;"><option value="#000000" ${existingStyle.color === '#000000' ? 'selected' : ''}>黑色</option><option value="#FF0000" ${existingStyle.color === '#FF0000' ? 'selected' : ''}>红色</option></select></div>
|
||
<div class="modal-buttons"><button class="cancel" id="cancelBtn">取消</button><button id="confirmBtn">确认</button></div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
|
||
modal.querySelector('#confirmBtn').onclick = () => {
|
||
const newName = modal.querySelector('#courseNameInput').value.trim() || "自习";
|
||
const newFontSize = parseInt(modal.querySelector('#fontSizeInput').value) || 16;
|
||
const newFontFamily = modal.querySelector('#fontFamilyInput').value;
|
||
const newColor = modal.querySelector('#colorInput').value;
|
||
|
||
|
||
courseData.cells[cellKey] = newName;
|
||
|
||
|
||
if (!courseData.cellStyles) courseData.cellStyles = {};
|
||
courseData.cellStyles[cellKey] = {
|
||
fontSize: Math.min(48, Math.max(10, newFontSize)),
|
||
fontFamily: newFontFamily,
|
||
color: newColor
|
||
};
|
||
|
||
modal.remove();
|
||
render();
|
||
};
|
||
|
||
modal.querySelector('.cancel').onclick = () => modal.remove();
|
||
modal.onclick = e => e.target === modal && modal.remove();
|
||
}
|
||
|
||
|
||
|
||
function renderPresetTemplate() {
|
||
ctx.fillStyle = "#ffffffff";
|
||
ctx.fillRect(0, 0, W, H);
|
||
|
||
|
||
const isSmallScreen = (W === 296 && H === 128);
|
||
const isLargeScreen = (W === 800 && H === 480);
|
||
|
||
if (!isSmallScreen) {
|
||
|
||
ctx.strokeStyle = "#FF0000";
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(4, 4, W-8, H-8);
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillRect(8, 8, 26, 4);
|
||
ctx.fillStyle = "#000000";
|
||
ctx.fillRect(W-54, 12, 42, 4);
|
||
|
||
let titleText = globalTitle.trim() || "输入内容·页眉";
|
||
|
||
|
||
let headerFontSize = isLargeScreen ? 52 : 26;
|
||
ctx.font = `bold ${headerFontSize}px '微软雅黑'`;
|
||
|
||
let titleWidth = ctx.measureText(titleText.slice(0,24)).width;
|
||
let bgX = (W - (titleWidth+40))/2;
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillRect(bgX, 7, titleWidth+40, headerFontSize + 10);
|
||
ctx.fillStyle = "#FFF";
|
||
ctx.fillText(titleText.slice(0,24), (W-titleWidth)/2, headerFontSize + 5);
|
||
ctx.beginPath();
|
||
ctx.strokeStyle = "#FF0000";
|
||
ctx.moveTo(12, headerFontSize + 20);
|
||
ctx.lineTo(W-12, headerFontSize + 20);
|
||
ctx.stroke();
|
||
|
||
var startY = headerFontSize + 50;
|
||
} else {
|
||
ctx.strokeStyle = "#FF0000";
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(4, 4, W-8, H-8);
|
||
var startY = 10;
|
||
}
|
||
const lineHeightBase = 26;
|
||
let currentY = startY;
|
||
hitAreas = [];
|
||
|
||
for(let i=0; i<presetLinesData.length; i++) {
|
||
const line = presetLinesData[i];
|
||
|
||
|
||
const segments = parseColoredText(line.text);
|
||
|
||
let { bgColor, textColor } = applyInvertStyle(line.baseColor, line.invertMode);
|
||
ctx.font = `${line.fontSize}px "${line.fontFamily}"`;
|
||
|
||
|
||
let totalWidth = 0;
|
||
segments.forEach(seg => {
|
||
ctx.font = `${line.fontSize}px "${line.fontFamily}"`;
|
||
totalWidth += ctx.measureText(seg.text).width;
|
||
});
|
||
|
||
let startX = line.alignment === 'center' ? (W - totalWidth) / 2 :
|
||
(line.alignment === 'right' ? W - totalWidth - 18 : 22);
|
||
|
||
let currentX = startX;
|
||
|
||
|
||
segments.forEach(seg => {
|
||
ctx.font = `${line.fontSize}px "${line.fontFamily}"`;
|
||
let textWidth = ctx.measureText(seg.text).width;
|
||
|
||
if(bgColor) {
|
||
ctx.fillStyle = bgColor;
|
||
ctx.fillRect(currentX, currentY-line.fontSize, textWidth+2, line.fontSize+4);
|
||
}
|
||
|
||
|
||
if(seg.isRed) {
|
||
ctx.fillStyle = "#FF0000";
|
||
} else {
|
||
ctx.fillStyle = textColor;
|
||
}
|
||
|
||
ctx.fillText(seg.text, currentX, currentY);
|
||
currentX += textWidth;
|
||
});
|
||
|
||
hitAreas.push({ index:i, yTop: currentY-20, yBottom: currentY+10 });
|
||
currentY += line.fontSize + 8;
|
||
if(currentY > H-20) break;
|
||
}
|
||
|
||
ctx.fillStyle = "#000";
|
||
ctx.fillRect(16, H-11, 42, 2);
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillRect(66, H-13, 26, 2);
|
||
}
|
||
|
||
|
||
function parseColoredText(text) {
|
||
const segments = [];
|
||
let lastIndex = 0;
|
||
const regex = /【(.*?)】/g;
|
||
let match;
|
||
|
||
while ((match = regex.exec(text)) !== null) {
|
||
if (match.index > lastIndex) {
|
||
segments.push({
|
||
text: text.substring(lastIndex, match.index),
|
||
isRed: false
|
||
});
|
||
}
|
||
segments.push({
|
||
text: match[1],
|
||
isRed: true
|
||
});
|
||
lastIndex = match.index + match[0].length;
|
||
}
|
||
|
||
if (lastIndex < text.length) {
|
||
segments.push({
|
||
text: text.substring(lastIndex),
|
||
isRed: false
|
||
});
|
||
}
|
||
|
||
return segments.length ? segments : [{ text: text, isRed: false }];
|
||
}
|
||
|
||
|
||
async function renderQRCodeMode() {
|
||
ctx.fillStyle = "#ffffffff";
|
||
ctx.fillRect(0, 0, W, H);
|
||
|
||
const isSmallScreen = (W === 296 && H === 128);
|
||
const isLargeScreen = (W === 800 && H === 480);
|
||
|
||
if (!isSmallScreen) {
|
||
|
||
ctx.strokeStyle = "#FF0000";
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(4, 4, W-8, H-8);
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillRect(8, 8, 26, 4);
|
||
ctx.fillStyle = "#000";
|
||
ctx.fillRect(W-54, 12, 42, 4);
|
||
|
||
const headerFontSize = isLargeScreen ? 36 : 18;
|
||
ctx.font = `bold ${headerFontSize}px '微软雅黑'`;
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillText(globalTitle.slice(0, 20), 33, headerFontSize + 10);
|
||
|
||
ctx.beginPath();
|
||
ctx.moveTo(12, headerFontSize + 20);
|
||
ctx.lineTo(W-12, headerFontSize + 20);
|
||
ctx.stroke();
|
||
|
||
var qrY = headerFontSize + 24;
|
||
var captionStartY = headerFontSize + qrSize + 30;
|
||
} else {
|
||
ctx.strokeStyle = "#FF0000";
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(4, 4, W-8, H-8);
|
||
|
||
var qrY = 8;
|
||
var captionStartY = qrSize + 12;
|
||
}
|
||
|
||
if(qrCodeImage) {
|
||
const img = new Image();
|
||
img.src = qrCodeImage;
|
||
await new Promise(r => { img.onload = r; img.onerror = r; });
|
||
|
||
|
||
let drawSize = qrSize;
|
||
if (isSmallScreen && drawSize > 90) {
|
||
drawSize = 80;
|
||
}
|
||
ctx.drawImage(img, (W-drawSize)/2, qrY, drawSize, drawSize);
|
||
}
|
||
|
||
let y = captionStartY;
|
||
const captionFontSize = isLargeScreen ? 32 : (isSmallScreen ? 12 : 18);
|
||
|
||
for(let line of qrCaptionLines) {
|
||
if(!line.trim()) { y += captionFontSize + 4; continue; }
|
||
ctx.font = `${captionFontSize}px '微软雅黑'`;
|
||
let tw = ctx.measureText(line).width;
|
||
ctx.fillStyle = "#f00";
|
||
ctx.fillText(line, (W-tw)/2, y+(H/20));
|
||
y += captionFontSize + 6;
|
||
}
|
||
|
||
if (!isSmallScreen) {
|
||
ctx.fillStyle = "#000";
|
||
ctx.fillRect(16, H-11, 42, 2);
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillRect(66, H-13, 26, 2);
|
||
}
|
||
}
|
||
|
||
function renderCustomMode() {
|
||
ctx.fillStyle = "#ffffffff";
|
||
ctx.fillRect(0, 0, W, H);
|
||
|
||
const isSmallScreen = (W === 296 && H === 128);
|
||
const isLargeScreen = (W === 800 && H === 480);
|
||
|
||
if (!isSmallScreen) {
|
||
|
||
ctx.strokeStyle = "#FF0000";
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(4, 4, W-8, H-8);
|
||
|
||
const headerFontSize = isLargeScreen ? 40 : 20;
|
||
const headerText = globalTitle.slice(0, 24);
|
||
ctx.font = `bold ${headerFontSize}px '微软雅黑'`;
|
||
const textWidth = ctx.measureText(headerText).width;
|
||
|
||
const boxX = 14;
|
||
const boxY = 8;
|
||
const boxWidth = Math.max(textWidth + 30, 150);
|
||
const boxHeight = headerFontSize + 10;
|
||
|
||
ctx.fillStyle = "#000";
|
||
ctx.fillRect(boxX, boxY, boxWidth, boxHeight);
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillText(headerText, boxX + 15, boxY + headerFontSize + 3);
|
||
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillRect(8, 8, 26, 4);
|
||
ctx.fillStyle = "#000";
|
||
ctx.fillRect(W-54, 12, 42, 4);
|
||
|
||
ctx.beginPath();
|
||
ctx.moveTo(12, headerFontSize + 30);
|
||
ctx.lineTo(W-12, headerFontSize + 30);
|
||
ctx.stroke();
|
||
|
||
var startY = headerFontSize + 48;
|
||
} else {
|
||
ctx.strokeStyle = "#FF0000";
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(4, 4, W-8, H-8);
|
||
|
||
var startY = 8;
|
||
}
|
||
|
||
const { fontSize, fontFamily, baseColor, invertMode, alignment } = customStyle;
|
||
const { bgColor, textColor } = applyInvertStyle(baseColor, invertMode);
|
||
ctx.font = `${fontSize}px "${fontFamily}"`;
|
||
|
||
let rawLines = customRawText.split('\n');
|
||
let allLines = [];
|
||
|
||
for(let line of rawLines) {
|
||
let segments = parseColoredText(line);
|
||
let currentLineSegments = [];
|
||
let currentLineWidth = 0;
|
||
|
||
for(let seg of segments) {
|
||
ctx.font = `${fontSize}px "${fontFamily}"`;
|
||
let segWidth = ctx.measureText(seg.text).width;
|
||
|
||
if(currentLineWidth + segWidth > W - 44 && currentLineSegments.length > 0) {
|
||
allLines.push([...currentLineSegments]);
|
||
currentLineSegments = [];
|
||
currentLineWidth = 0;
|
||
}
|
||
currentLineSegments.push(seg);
|
||
currentLineWidth += segWidth;
|
||
}
|
||
if(currentLineSegments.length > 0) {
|
||
allLines.push(currentLineSegments);
|
||
}
|
||
}
|
||
|
||
let y = startY;
|
||
for(let lineSegments of allLines) {
|
||
if(!lineSegments.length) { y += fontSize + 6; continue; }
|
||
|
||
let totalWidth = 0;
|
||
for(let seg of lineSegments) {
|
||
ctx.font = `${fontSize}px "${fontFamily}"`;
|
||
totalWidth += ctx.measureText(seg.text).width;
|
||
}
|
||
|
||
let startX = alignment === 'center' ? (W - totalWidth) / 2 :
|
||
(alignment === 'right' ? W - totalWidth - 18 : 22);
|
||
|
||
let currentX = startX;
|
||
|
||
for(let seg of lineSegments) {
|
||
ctx.font = `${fontSize}px "${fontFamily}"`;
|
||
let textWidth = ctx.measureText(seg.text).width;
|
||
const textHeight = fontSize;
|
||
|
||
if(bgColor) {
|
||
ctx.fillStyle = bgColor;
|
||
ctx.fillRect(currentX-2, y + fontSize/2 - textHeight+1, textWidth+4, textHeight + 4);
|
||
}
|
||
|
||
if(seg.isRed) {
|
||
ctx.fillStyle = "#FF0000";
|
||
} else {
|
||
ctx.fillStyle = textColor;
|
||
}
|
||
|
||
ctx.fillText(seg.text, currentX, y + fontSize/2);
|
||
currentX += textWidth;
|
||
}
|
||
|
||
y += fontSize + 6;
|
||
if(y > H-30) break;
|
||
}
|
||
|
||
if (!isSmallScreen) {
|
||
ctx.fillStyle = "#000";
|
||
ctx.fillRect(16, H-11, 42, 2);
|
||
ctx.fillStyle = "#FF0000";
|
||
ctx.fillRect(66, H-13, 26, 2);
|
||
}
|
||
}
|
||
|
||
function render() {
|
||
if(currentTemplate === 'course') renderCourseTable();
|
||
else if(currentTemplate === 'qrcode') renderQRCodeMode();
|
||
else if(currentTemplate === 'customText') renderCustomMode();
|
||
else renderPresetTemplate();
|
||
const canvas = document.getElementById('unifiedCanvas');
|
||
applyImageAdjustmentsmb(canvas)
|
||
}
|
||
function applyImageAdjustmentsmb(canvas) {
|
||
const brightness = 0;
|
||
const contrast = 100;
|
||
const saturation = 100;
|
||
const diffusion = 0;
|
||
const ctx = canvas.getContext('2d');
|
||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||
const data = imageData.data;
|
||
if (brightness !== 0) {
|
||
const brightnessFactor = brightness / 100;
|
||
for (let i = 0; i < data.length; i += 4) {
|
||
data[i] = clamp(data[i] + 255 * brightnessFactor, 0, 255);
|
||
data[i + 1] = clamp(data[i + 1] + 255 * brightnessFactor, 0, 255);
|
||
data[i + 2] = clamp(data[i + 2] + 255 * brightnessFactor, 0, 255);
|
||
}
|
||
}
|
||
if (contrast !== 0) {
|
||
const contrastFactor = (contrast + 100) / 100;
|
||
const contrastAdjust = (1 - contrastFactor) * 128;
|
||
for (let i = 0; i < data.length; i += 4) {
|
||
data[i] = clamp(data[i] * contrastFactor + contrastAdjust, 0, 255);
|
||
data[i + 1] = clamp(data[i + 1] * contrastFactor + contrastAdjust, 0, 255);
|
||
data[i + 2] = clamp(data[i + 2] * contrastFactor + contrastAdjust, 0, 255);
|
||
}
|
||
}
|
||
if (saturation !== 100) {
|
||
const saturationFactor = saturation / 100;
|
||
for (let i = 0; i < data.length; i += 4) {
|
||
const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
||
data[i] = clamp(gray + (data[i] - gray) * saturationFactor, 0, 255);
|
||
data[i + 1] = clamp(gray + (data[i + 1] - gray) * saturationFactor, 0, 255);
|
||
data[i + 2] = clamp(gray + (data[i + 2] - gray) * saturationFactor, 0, 255);
|
||
}
|
||
}
|
||
ctx.putImageData(imageData, 0, 0);
|
||
return diffusion;
|
||
}
|
||
|
||
function handleCanvasClick(e) {
|
||
if(currentTemplate === 'course') {
|
||
const r = canvas.getBoundingClientRect();
|
||
const x = (e.clientX-r.left)*(W/r.width);
|
||
const y = (e.clientY-r.top)*(H/r.height);
|
||
for(let a of courseHitAreas) {
|
||
if(x>=a.x&&x<=a.x+a.w&&y>=a.y&&y<=a.y+a.h) {
|
||
editCourseCell(a.row,a.col,a.currentName); return;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
if(currentTemplate === 'qrcode') {
|
||
const r = canvas.getBoundingClientRect();
|
||
const y = (e.clientY-r.top)*(H/r.height);
|
||
if(y>130) {
|
||
const m = document.createElement('div');
|
||
m.className='modal-mask';
|
||
m.innerHTML=`<div class="modal-card"><h3>编辑文字</h3><textarea rows=4 style="width:100%;background:#000;color:#fff;border:1px solid red;border-radius:16px;padding:10px;">${qrCaptionLines.join('\n')}</textarea><div class="modal-buttons"><button class=cancel>取消</button><button class=ok>保存</button></div></div>`;
|
||
document.body.appendChild(m);
|
||
m.querySelector('.ok').onclick=()=>{
|
||
qrCaptionLines = m.querySelector('textarea').value.split('\n');
|
||
updateCaptionLinesToUI();
|
||
render(); m.remove();
|
||
};
|
||
m.querySelector('.cancel').onclick=()=>m.remove();
|
||
}
|
||
return;
|
||
}
|
||
|
||
if(currentTemplate !== 'customText') {
|
||
const r = canvas.getBoundingClientRect();
|
||
const y = (e.clientY-r.top)*(H/r.height);
|
||
for(let a of hitAreas) {
|
||
if(y>=a.yTop&&y<=a.yBottom) {
|
||
editPresetModal(a.index); return;
|
||
}
|
||
}
|
||
|
||
addPresetNewRow();
|
||
}
|
||
}
|
||
|
||
function addPresetNewRow() {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-mask';
|
||
modal.innerHTML = `
|
||
<div class="modal-card">
|
||
<h3>➕ 添加新行</h3>
|
||
<div class="modal-field"><label>文字内容</label><input id="modalText" value="新行内容"></div>
|
||
<div class="modal-field"><label>大小</label><input id="modalFontSize" type="number" value="14" min="10" max="52"></div>
|
||
<div class="modal-field"><label>字体</label><select id="modalFontFamily">${chineseFonts.map(f => `<option value="${f}">${f}</option>`).join('')}</select></div>
|
||
<div class="modal-field"><label>基色</label><select id="modalBaseColor"><option value="#000000">黑色</option><option value="#FF0000">红色</option></select></div>
|
||
<div class="modal-field"><label>反色</label><select id="modalInvert">${invertModes.map(m => `<option value="${m.value}">${m.label}</option>`).join('')}</select></div>
|
||
<div class="modal-field"><label>对齐</label><select id="modalAlign"><option value="left">左</option><option value="center">中</option><option value="right">右</option></select></div>
|
||
<div class="modal-buttons"><button class="cancel" id="cancelBtn">取消</button><button id="confirmBtn">确认</button></div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
const confirm = () => {
|
||
const newRow = {
|
||
text: modal.querySelector('#modalText').value,
|
||
fontSize: Math.min(52, Math.max(10, parseInt(modal.querySelector('#modalFontSize').value) || 14)),
|
||
fontFamily: modal.querySelector('#modalFontFamily').value,
|
||
baseColor: modal.querySelector('#modalBaseColor').value,
|
||
invertMode: modal.querySelector('#modalInvert').value,
|
||
alignment: modal.querySelector('#modalAlign').value
|
||
};
|
||
presetLinesData.push(newRow);
|
||
renderPresetTemplate();
|
||
modal.remove();
|
||
};
|
||
modal.querySelector('#confirmBtn').onclick = confirm;
|
||
modal.querySelector('#cancelBtn').onclick = () => modal.remove();
|
||
modal.onclick = (e) => { if(e.target === modal) modal.remove(); };
|
||
}
|
||
|
||
function editPresetModal(idx) {
|
||
const line = presetLinesData[idx];
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-mask';
|
||
modal.innerHTML = `
|
||
<div class="modal-card">
|
||
<h3>编辑第 ${idx+1} 行</h3>
|
||
<div class="modal-field"><label>文字内容</label><input id="modalText" value="${escapeHtml(line.text)}"></div>
|
||
<div class="modal-field"><label>大小</label><input id="modalFontSize" type="number" value="${line.fontSize}" min="10" max="52"></div>
|
||
<div class="modal-field"><label>字体</label><select id="modalFontFamily">${chineseFonts.map(f => `<option value="${f}" ${line.fontFamily===f?'selected':''}>${f}</option>`).join('')}</select></div>
|
||
<div class="modal-field"><label>基色</label><select id="modalBaseColor"><option value="#000000" ${line.baseColor==='#000000'?'selected':''}>黑色</option><option value="#FF0000" ${line.baseColor==='#FF0000'?'selected':''}>红色</option></select></div>
|
||
<div class="modal-field"><label>反色</label><select id="modalInvert">${invertModes.map(m => `<option value="${m.value}" ${line.invertMode===m.value?'selected':''}>${m.label}</option>`).join('')}</select></div>
|
||
<div class="modal-field"><label>对齐</label><select id="modalAlign"><option value="left" ${line.alignment==='left'?'selected':''}>左</option><option value="center" ${line.alignment==='center'?'selected':''}>中</option><option value="right" ${line.alignment==='right'?'selected':''}>右</option></select></div>
|
||
<div class="modal-buttons"><button class="cancel" id="cancelBtn">取消</button><button id="confirmBtn">确认</button></div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
const confirm = () => {
|
||
presetLinesData[idx] = {
|
||
text: modal.querySelector('#modalText').value,
|
||
fontSize: Math.min(52, Math.max(10, parseInt(modal.querySelector('#modalFontSize').value) || 14)),
|
||
fontFamily: modal.querySelector('#modalFontFamily').value,
|
||
baseColor: modal.querySelector('#modalBaseColor').value,
|
||
invertMode: modal.querySelector('#modalInvert').value,
|
||
alignment: modal.querySelector('#modalAlign').value
|
||
};
|
||
renderPresetTemplate();
|
||
modal.remove();
|
||
};
|
||
modal.querySelector('#confirmBtn').onclick = confirm;
|
||
modal.querySelector('#cancelBtn').onclick = () => modal.remove();
|
||
modal.onclick = (e) => { if(e.target === modal) modal.remove(); };
|
||
}
|
||
|
||
function switchTemplate(tpl) {
|
||
currentTemplate = tpl;
|
||
document.getElementById('customBlock').style.display = tpl==='customText'?'block':'none';
|
||
document.getElementById('qrcodeBlock').style.display = tpl==='qrcode'?'block':'none';
|
||
document.getElementById('courseBlock').style.display = tpl==='course'?'block':'none';
|
||
document.getElementById('presetHintBlock').style.display = (tpl!=='customText'&&tpl!=='qrcode')?'block':'none';
|
||
|
||
if(tpl === 'course') { loadPresetData('schedule'); render(); }
|
||
else if(tpl === 'qrcode') { syncCaptionFromTextarea(); refreshQRImage(); }
|
||
else if(tpl === 'customText') { render(); }
|
||
else { loadPresetData(tpl); render(); }
|
||
}
|
||
|
||
function resetCurrent() {
|
||
if(currentTemplate === 'customText') {
|
||
customRawText = "小雨在放学路上捡到一枚【闪闪发亮的硬币】。\n她买了两颗【橘子味的糖果】,一颗塞进嘴里,\n甜得她眯起了眼睛;另一颗放在路边的长椅上。\n一只【唱歌的小鸟】飞过来,歪着脑袋看了看\n,叼起糖果飞走了。小雨有点失落,\n但还是继续往家走。第二天早上,她推开窗户,\n发现窗台上放着一朵【金黄色的小野花】,花瓣上还挂着露珠。小雨捧着花笑了:\n“原来快乐会【越分越多】呀。”\n从那天起,她每天都往长椅上放一颗糖。";
|
||
document.getElementById('freeTextArea').value = customRawText;
|
||
} else if(currentTemplate === 'qrcode') {
|
||
qrText = "https://mspppclock.tech/";
|
||
document.getElementById('qrContentInput').value = qrText;
|
||
qrCaptionLines = ["扫码访问官网"];
|
||
updateCaptionLinesToUI();
|
||
refreshQRImage();
|
||
} else if(currentTemplate === 'course') {
|
||
initDefaultCourse();
|
||
} else {
|
||
loadPresetData(currentTemplate);
|
||
}
|
||
render();
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
function init() {
|
||
|
||
const fsel = document.getElementById('customFontFamily');
|
||
chineseFonts.forEach(f=>{ let o=document.createElement('option');o.value=f;o.textContent=f;fsel.appendChild(o); });
|
||
const isel = document.getElementById('customInvertMode');
|
||
invertModes.forEach(m=>{ let o=document.createElement('option');o.value=m.value;o.textContent=m.label;isel.appendChild(o); });
|
||
|
||
document.getElementById('freeTextArea').value = customRawText;
|
||
document.getElementById('freeTextArea').oninput = e=>{ customRawText=e.target.value; render(); };
|
||
document.querySelectorAll('#customFontSize,#customFontFamily,#customBaseColor,#customInvertMode,#customAlign').forEach(i=>{
|
||
i.onchange=()=>{
|
||
customStyle.fontSize = +document.getElementById('customFontSize').value;
|
||
customStyle.fontFamily = document.getElementById('customFontFamily').value;
|
||
customStyle.baseColor = document.getElementById('customBaseColor').value;
|
||
customStyle.invertMode = document.getElementById('customInvertMode').value;
|
||
customStyle.alignment = document.getElementById('customAlign').value;
|
||
render();
|
||
};
|
||
});
|
||
|
||
document.getElementById('globalTitleInput').oninput = e=>{ globalTitle=e.target.value; render(); };
|
||
document.getElementById('forceRefreshBtn').onclick = render;
|
||
document.getElementById('resetCurrentBtn').onclick = resetCurrent;
|
||
document.getElementById('generateQRBtn').onclick = ()=>{ qrText=document.getElementById('qrContentInput').value; refreshQRImage(); };
|
||
document.getElementById('qrCaptionText').oninput = ()=>{ syncCaptionFromTextarea(); render(); };
|
||
document.getElementById('applyCourseSettingBtn').onclick = applyCourseCustomSettings;
|
||
canvas.onclick = handleCanvasClick;
|
||
|
||
document.querySelectorAll('.temp-btn').forEach(b=>{
|
||
b.onclick=()=>{
|
||
document.querySelectorAll('.temp-btn').forEach(x=>x.classList.remove('active'));
|
||
b.classList.add('active');
|
||
switchTemplate(b.dataset.template);
|
||
};
|
||
});
|
||
|
||
document.getElementById('epdwh').addEventListener('change', function() {
|
||
const [newW, newH] = this.value.split('x').map(Number);
|
||
canvas.width = newW;
|
||
canvas.height = newH;
|
||
render();
|
||
});
|
||
// 监听屏幕尺寸变化
|
||
document.getElementById('epdwh').addEventListener('change', function() {
|
||
updateCanvasSize();
|
||
if (currentTemplate === 'qrcode') {
|
||
refreshQRImage(); // 二维码模式需要重新生成
|
||
} else {
|
||
render();
|
||
}
|
||
});
|
||
switchTemplate('schedule');
|
||
}
|
||
|
||
init();
|
||
})();
|