重写选中便签的黑边为干净的外描边

This commit is contained in:
2026-09-19 14:13:25 +08:00
parent b94abe32fa
commit 86ee03f4a6
2 changed files with 50 additions and 51 deletions
+40 -38
View File
@@ -53,8 +53,9 @@ function sphericalAreaKm2(polys) {
}
// 生成便签 canvas:黄底圆角 + 左侧箭头 + 黑色加粗文字(整张图,箭头尖端指向坐标)
// 选中态:先描粗边再填充,得到一条干净的 2px 外描边(四周预留 B 的 padding 防止裁切)
function makeStickyNote(text, fontSize, selected = false) {
const padX = 8, padY = 5, radius = 7, arrowW = 10, arrowH = 14, S = 2; // S: 2x 超采样
const padX = 8, padY = 5, radius = 7, arrowW = 10, arrowH = 14, S = 2, B = 2; // S: 2x 超采样,B: 边框预留
const font = `bold ${fontSize}px "Noto Serif SC", "Songti SC", "STSong", "SimSun", Georgia, serif`;
const m = document.createElement('canvas');
const mctx = m.getContext('2d');
@@ -63,55 +64,54 @@ function makeStickyNote(text, fontSize, selected = false) {
const bodyW = Math.max(1, Math.ceil(textW + padX * 2));
const w = arrowW + bodyW;
const h = Math.ceil(fontSize + padY * 2);
const cw = w + B * 2;
const ch = h + B * 2;
const c = document.createElement('canvas');
c.width = w * S;
c.height = h * S;
c.width = cw * S;
c.height = ch * S;
const ctx = c.getContext('2d');
ctx.scale(S, S);
// 阴影
ctx.shadowColor = 'rgba(0, 0, 0, 0.20)';
ctx.shadowBlur = 5;
ctx.shadowOffsetY = 2;
const ox = B, oy = B; // 形状整体内缩 B,给黑边留空间
// 箭头 + 圆角矩形一体化路径(箭头尖端在左侧 x=0,指向坐标)
// 箭头 + 圆角矩形一体化路径(箭头尖端在 (ox, oy + h/2),指向坐标)
ctx.beginPath();
ctx.moveTo(0, h / 2); // 箭头尖端
ctx.lineTo(arrowW, h / 2 - arrowH / 2); // 箭头上沿
ctx.lineTo(arrowW, radius); // 左边缘上部
ctx.arcTo(arrowW, 0, arrowW + radius, 0, radius); // 左上圆角
ctx.lineTo(w - radius, 0); // 上边
ctx.arcTo(w, 0, w, radius, radius); // 右上圆角
ctx.lineTo(w, h - radius); // 右边
ctx.arcTo(w, h, w - radius, h, radius); // 右下圆角
ctx.lineTo(arrowW + radius, h); // 底边
ctx.arcTo(arrowW, h, arrowW, h - radius, radius); // 左下圆角
ctx.lineTo(arrowW, h / 2 + arrowH / 2); // 左边缘下部
ctx.closePath(); // 闭合回箭头尖端
ctx.moveTo(ox, oy + h / 2);
ctx.lineTo(ox + arrowW, oy + h / 2 - arrowH / 2);
ctx.lineTo(ox + arrowW, oy + radius);
ctx.arcTo(ox + arrowW, oy, ox + arrowW + radius, oy, radius);
ctx.lineTo(ox + w - radius, oy);
ctx.arcTo(ox + w, oy, ox + w, oy + radius, radius);
ctx.lineTo(ox + w, oy + h - radius);
ctx.arcTo(ox + w, oy + h, ox + w - radius, oy + h, radius);
ctx.lineTo(ox + arrowW + radius, oy + h);
ctx.arcTo(ox + arrowW, oy + h, ox + arrowW, oy + h - radius, radius);
ctx.lineTo(ox + arrowW, oy + h / 2 + arrowH / 2);
ctx.closePath();
if (selected) {
// 先描粗边,再填充盖住内侧一半 → 外露 2px 干净黑边
ctx.strokeStyle = '#14100d';
ctx.lineWidth = B * 2; // 4px,外露 2px
ctx.lineJoin = 'round';
ctx.stroke();
} else {
// 非选中:柔和的投影
ctx.shadowColor = 'rgba(0, 0, 0, 0.20)';
ctx.shadowBlur = 5;
ctx.shadowOffsetY = 2;
}
ctx.fillStyle = '#e0c46a';
ctx.fill();
// 选中时加黑边
if (selected) {
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
ctx.strokeStyle = '#14100d';
ctx.lineWidth = 3;
ctx.stroke();
}
// 黑色加粗文字(无阴影)
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
// 黑色加粗文字
ctx.fillStyle = '#14100d';
ctx.font = font;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(text, arrowW + bodyW / 2, h / 2 + 1);
ctx.fillText(text, ox + arrowW + bodyW / 2, oy + h / 2 + 1);
return { canvas: c, scale: 1 / S, width: w, height: h };
return { canvas: c, scale: 1 / S, width: cw, height: ch, tipX: B, tipY: B + h / 2 };
}
export class CesiumGlobe {
@@ -300,6 +300,8 @@ export class CesiumGlobe {
el.style.height = `${m.noteNormal.height}px`;
el.dataset.index = m.index;
m.el = el;
m.tipX = m.noteNormal.tipX; // 箭头尖相对元素左上角的偏移
m.tipY = m.noteNormal.tipY;
m.normalUrl = m.noteNormal.canvas.toDataURL();
m.selectedUrl = m.noteSelected.canvas.toDataURL();
el.style.backgroundImage = `url("${m.normalUrl}")`;
@@ -335,8 +337,8 @@ export class CesiumGlobe {
el.style.display = 'block';
el.style.opacity = String(opacity);
el.style.left = `${win.x}px`;
el.style.top = `${win.y}px`;
el.style.left = `${win.x - m.tipX}px`;
el.style.top = `${win.y - m.tipY}px`;
}
}
+10 -13
View File
@@ -47,7 +47,6 @@ html, body {
pointer-events: auto;
background-repeat: no-repeat;
background-size: 100% 100%;
transform: translateY(-50%);
cursor: pointer;
}
@@ -66,27 +65,25 @@ html, body {
background: #0c0906; opacity: 0; transition: opacity 0.18s ease;
}
/* 加载界面 */
/* 加载界面:OS1 3D 加载环 */
#loading {
position: fixed; inset: 0; z-index: 1000;
display: flex; align-items: center; justify-content: center;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 26px;
background: #14100d;
transition: opacity 0.6s ease;
}
#loading.hidden { opacity: 0; pointer-events: none; }
.loading-box { display: flex; flex-direction: column; align-items: center; gap: 18px; }
.spinner {
width: 54px; height: 54px;
border: 3px solid rgba(201, 162, 39, 0.22);
border-top-color: #c9a227;
border-radius: 50%;
animation: spin 0.9s linear infinite;
#loading-canvas {
width: 500px; height: 500px;
max-width: 88vw; max-height: 88vw;
}
@keyframes spin { to { transform: rotate(360deg); } }
#loading-canvas canvas { width: 100%; height: 100%; display: block; }
.loading-text {
font-family: var(--serif);
font-size: 14px; letter-spacing: 0.3em;
color: rgba(230, 216, 182, 0.7);
font-size: 13px;
letter-spacing: 0.2em;
color: rgba(224, 196, 106, 0.8);
}
/* 顶部标题 */