阶段11开发
This commit is contained in:
1 parent
8e9b83f95b
commit
8802bbe25e
21 files changed
+5285
-81
No files matched your search
+204
-20
@@ -56,6 +56,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="help-btn" :class="{ on: showHelp }" title="查看使用说明" @click="openHelp">使用说明</button>
|
||||
<span class="zoom-pct">{{ Math.round(store.editor.viewScale * 100) }}%</span>
|
||||
|
||||
</div>
|
||||
@@ -79,6 +80,14 @@
|
||||
<div class="sp-section-title">画布辅助显示</div>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="store.settings.showEmitter" />发射点</label>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="store.settings.showBones" />骨骼连线</label>
|
||||
<template v-if="store.settings.showBones">
|
||||
<label class="sp-row"><span>骨骼点绘制大小</span>
|
||||
<input class="sp-num" type="number" min="0.1" max="10" step="0.1" v-model.number="store.settings.bonePointDrawSize" aria-label="骨骼点绘制大小" @change="normalizeBoneDebugSizes" />
|
||||
</label>
|
||||
<label class="sp-row"><span>坐标轴绘制大小</span>
|
||||
<input class="sp-num" type="number" min="0.1" max="10" step="0.1" v-model.number="store.settings.boneAxisDrawSize" aria-label="坐标轴绘制大小" @change="normalizeBoneDebugSizes" />
|
||||
</label>
|
||||
</template>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="store.settings.showSkinMesh" />蒙皮网格</label>
|
||||
<div class="sp-divider"></div>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="store.settings.tickEnabled" />启用刻度</label>
|
||||
@@ -99,6 +108,64 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="showHelp" class="help-overlay" role="presentation" @click.self="showHelp = false">
|
||||
<section class="help-dialog" role="dialog" aria-modal="true" aria-labelledby="help-title">
|
||||
<header class="help-head">
|
||||
<div>
|
||||
<h2 id="help-title">使用说明</h2>
|
||||
<p>从创建粒子到保存、导出的快速入门</p>
|
||||
</div>
|
||||
<button class="help-close" aria-label="关闭使用说明" title="关闭 (Esc)" @click="showHelp = false">✕</button>
|
||||
</header>
|
||||
|
||||
<div class="help-body">
|
||||
<section class="help-section help-start">
|
||||
<h3>快速开始</h3>
|
||||
<ol>
|
||||
<li><b>创建对象:</b>在左侧“场景对象”中添加粒子、碰撞、路径或 Spine。</li>
|
||||
<li><b>编辑粒子:</b>选中粒子系统,依次调整发射、形状、属性、外观资源和修改器。</li>
|
||||
<li><b>查看效果:</b>使用底部时间轴播放、暂停或拖动到指定帧;修改参数后会自动重新计算。</li>
|
||||
<li><b>保存成果:</b>配置文件用于继续编辑;“导出 Spine Json”用于导入 Spine 4.2。</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<div class="help-grid">
|
||||
<section class="help-section">
|
||||
<h3>常用功能</h3>
|
||||
<dl class="help-list">
|
||||
<div><dt>小眼睛</dt><dd>单独显示或隐藏场景对象;隐藏的粒子系统不会导出。</dd></div>
|
||||
<div><dt>+Copy</dt><dd>完整复制当前选中对象,方便对比不同参数效果。</dd></div>
|
||||
<div><dt>背景图</dt><dd>上传参考图并调整位置和透明度,只用于画布预览。</dd></div>
|
||||
<div><dt>系统设置</dt><dd>管理撤回步数、发射点、骨骼、蒙皮网格和刻度显示。</dd></div>
|
||||
<div><dt>配置管理</dt><dd>保存当前编辑数据、加载配置,或直接使用预设文件。</dd></div>
|
||||
<div><dt>Spine 骨骼</dt><dd>在层级树搜索并选择骨骼,可用于粒子发射器跟随。</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="help-section">
|
||||
<h3>快捷键</h3>
|
||||
<div class="shortcut-list">
|
||||
<div><kbd>Ctrl/⌘</kbd><span>+</span><kbd>Z</kbd><em>撤回上一步</em></div>
|
||||
<div><kbd>Ctrl/⌘</kbd><span>+</span><kbd>S</kbd><em>保存配置</em></div>
|
||||
<div><kbd>Q</kbd><em>拖动画布</em></div>
|
||||
<div><kbd>W</kbd><em>位移工具</em></div>
|
||||
<div><kbd>E</kbd><em>旋转工具</em></div>
|
||||
<div><kbd>R</kbd><em>缩放工具</em></div>
|
||||
<div><kbd>D</kbd><em>播放 / 暂停</em></div>
|
||||
<div><kbd>S</kbd><em>回到第一帧</em></div>
|
||||
</div>
|
||||
<p class="help-note">Q / W / E / R 需要先点击画布。光标位于输入框时,单键快捷键不会触发。</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="help-section help-export">
|
||||
<h3>配置与导出有什么区别?</h3>
|
||||
<p><b>保存配置</b>会保存编辑器中的全部对象和参数,之后可以继续修改;<b>导出 Spine Json</b>只导出显示中的粒子系统,并把跟随、碰撞和拖尾结果烘焙为独立动画数据。</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="hud" v-if="info">
|
||||
系统: <b class="hud-number hud-system-count">{{ info.systems }}</b> · 粒子(骨骼): <b class="hud-number hud-particle-count">{{ info.particles }}</b> · 根骨骼: <b>root</b>
|
||||
<span class="hud-tip">滚轮缩放 · 左键拖动画布</span>
|
||||
@@ -122,6 +189,7 @@ const holder = ref<HTMLElement | null>(null)
|
||||
const info = ref<{ systems: number; particles: number } | null>(null)
|
||||
const showSettings = ref(false)
|
||||
const showBackgroundMenu = ref(false)
|
||||
const showHelp = ref(false)
|
||||
const backgroundFileInput = ref<HTMLInputElement | null>(null)
|
||||
const backgroundFileName = computed(() => store.settings.backgroundImageName)
|
||||
// 根变换 gizmo 工具:'translate' | 'rotate' | 'scale' | ''(无)
|
||||
@@ -137,9 +205,16 @@ if (store.settings.backgroundImageName == null) store.settings.backgroundImageNa
|
||||
if (store.settings.backgroundImageData == null) store.settings.backgroundImageData = ''
|
||||
if (store.settings.showEmitter == null) store.settings.showEmitter = true
|
||||
if (store.settings.showBones == null) store.settings.showBones = false
|
||||
if (!(store.settings.bonePointDrawSize > 0)) store.settings.bonePointDrawSize = 1
|
||||
if (!(store.settings.boneAxisDrawSize > 0)) store.settings.boneAxisDrawSize = 2
|
||||
if (store.settings.showSkinMesh == null) store.settings.showSkinMesh = false
|
||||
if (store.settings.showGrid == null) store.settings.showGrid = true
|
||||
|
||||
function normalizeBoneDebugSizes() {
|
||||
store.settings.bonePointDrawSize = Math.min(10, Math.max(0.1, Number(store.settings.bonePointDrawSize) || 1))
|
||||
store.settings.boneAxisDrawSize = Math.min(10, Math.max(0.1, Number(store.settings.boneAxisDrawSize) || 2))
|
||||
}
|
||||
|
||||
useEditorShortcuts({
|
||||
canvasElement: holder,
|
||||
canvasFocused,
|
||||
@@ -186,8 +261,8 @@ watch(() => [store.activeObjectType, store.activeObjectId], () => selectedPathNo
|
||||
async function loadTexture() {
|
||||
if (dotTex && trailTex) return dotTex
|
||||
const [particleTexture, trailTexture] = await Promise.all([
|
||||
Texture.fromURL('/particles/star.png'),
|
||||
Texture.fromURL('/trails/trail.png'),
|
||||
Texture.fromURL('/images/star.png'),
|
||||
Texture.fromURL('/images/trail.png'),
|
||||
])
|
||||
dotTex = particleTexture
|
||||
trailTex = trailTexture
|
||||
@@ -232,6 +307,16 @@ function toggleBackgroundMenu() {
|
||||
if (showBackgroundMenu.value) showSettings.value = false
|
||||
}
|
||||
|
||||
function openHelp() {
|
||||
showHelp.value = true
|
||||
showSettings.value = false
|
||||
showBackgroundMenu.value = false
|
||||
}
|
||||
|
||||
function onHelpKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && showHelp.value) showHelp.value = false
|
||||
}
|
||||
|
||||
function toggleSettings() {
|
||||
showSettings.value = !showSettings.value
|
||||
if (showSettings.value) showBackgroundMenu.value = false
|
||||
@@ -804,7 +889,7 @@ function syncEmitters() {
|
||||
const cfg = ensureEmitterConfig(sys.config as any)
|
||||
if (trailTex) {
|
||||
for (const resource of cfg.trailResources) {
|
||||
if (!resource.texture && resource.previewUrl === '/trails/trail.png') resource.texture = trailTex
|
||||
if (!resource.texture && (resource.previewUrl === '/images/trail.png' || resource.previewUrl === '/trails/trail.png')) resource.texture = trailTex
|
||||
}
|
||||
}
|
||||
if (dotTex) {
|
||||
@@ -1062,14 +1147,15 @@ function drawGizmo() {
|
||||
}
|
||||
|
||||
// 粒子→骨骼映射:每个活动粒子 = 一根挂在 root 下的骨骼
|
||||
type BonePreview = { boneName: string; x: number; y: number; parentX?: number; parentY?: number; trail?: boolean }
|
||||
type BonePreview = { boneName: string; x: number; y: number; rotation: number; parentX?: number; parentY?: number; trail?: boolean }
|
||||
|
||||
function appendStateBones(target: BonePreview[], state: ParticleState) {
|
||||
target.push({ boneName: state.boneName, x: state.x, y: state.y })
|
||||
// 粒子 rotation 使用数学角度,而 Pixi 画布 Y 轴向下,因此显示轴取反。
|
||||
target.push({ boneName: state.boneName, x: state.x, y: state.y, rotation: -state.rotation })
|
||||
if (!state.trailState?.bones.length) return
|
||||
state.trailState.bones.forEach((bone, index, bones) => {
|
||||
const parent = index > 0 ? bones[index - 1] : { x: state.x, y: state.y }
|
||||
target.push({ boneName: bone.boneName, x: bone.x, y: bone.y, parentX: parent.x, parentY: parent.y, trail: true })
|
||||
state.trailState.bones.forEach((bone) => {
|
||||
// 导出结构中拖尾骨骼全部直接挂在粒子系统骨骼下,画布连线也保持同级语义。
|
||||
target.push({ boneName: bone.boneName, x: bone.x, y: bone.y, rotation: bone.rotation, parentX: 0, parentY: 0, trail: true })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1169,6 +1255,8 @@ function bakeLoopFrames(system: ParticleSystem, emitter: ParticleEmitter, fps: n
|
||||
const finalFrame = cloneParticleFrame(frames[frames.length - 1] || [])
|
||||
if (frames.length > totalFrames) frames.length = totalFrames
|
||||
while (frames.length < totalFrames) frames.push(cloneParticleFrame(finalFrame))
|
||||
// 循环持续拥有自己的衔接帧规则:纯循环和结束段允许第 0 帧保留骨骼;
|
||||
// 启动段则已在上方按定义生成空首帧。
|
||||
system.frames = frames
|
||||
emitter.apply(frames[0] || [])
|
||||
}
|
||||
@@ -1179,15 +1267,36 @@ function drawBones(bones: BonePreview[]) {
|
||||
g.clear()
|
||||
if (!store.settings.showBones) return
|
||||
const cx = 0, cy = 0
|
||||
const viewScale = Math.max(0.25, Math.abs(store.editor.viewScale) || 1)
|
||||
const pointSize = Math.min(10, Math.max(0.1, Number(store.settings.bonePointDrawSize) || 1))
|
||||
const axisSize = Math.min(10, Math.max(0.1, Number(store.settings.boneAxisDrawSize) || 2))
|
||||
// 所有尺寸先定义为屏幕像素,再除以画布缩放,保证缩放画布时视觉大小不变。
|
||||
const pointRadius = 2.5 * pointSize / viewScale
|
||||
const forwardLength = 4.5 * axisSize / viewScale
|
||||
const sideLength = 2.5 * axisSize / viewScale
|
||||
const forwardWidth = Math.max(0.65, axisSize * 0.625) / viewScale
|
||||
const sideWidth = Math.max(0.55, axisSize * 0.5) / viewScale
|
||||
// root → 每颗粒子骨骼 的连线
|
||||
for (const b of bones) {
|
||||
const color = b.trail ? 0xff4d8d : 0xff8c5a
|
||||
g.lineStyle(1, color, b.trail ? 0.65 : 0.35)
|
||||
g.moveTo(b.parentX ?? cx, b.parentY ?? cy)
|
||||
g.lineTo(b.x, b.y)
|
||||
// 小型局部方向轴:青色为骨骼正方向,黄色为垂直方向。
|
||||
const angle = b.rotation * Math.PI / 180
|
||||
const cos = Math.cos(angle), sin = Math.sin(angle)
|
||||
g.lineStyle(forwardWidth, 0x38d9ff, 0.95)
|
||||
g.moveTo(b.x, b.y)
|
||||
g.lineTo(b.x + cos * forwardLength, b.y + sin * forwardLength)
|
||||
g.beginFill(0x38d9ff, 0.95)
|
||||
g.drawCircle(b.x + cos * forwardLength, b.y + sin * forwardLength, Math.max(0.8, axisSize * 0.625) / viewScale)
|
||||
g.endFill()
|
||||
g.lineStyle(sideWidth, 0xffd166, 0.85)
|
||||
g.moveTo(b.x, b.y)
|
||||
g.lineTo(b.x - sin * sideLength, b.y + cos * sideLength)
|
||||
// 骨骼点
|
||||
g.beginFill(color, 0.9)
|
||||
g.drawCircle(b.x, b.y, 2.5)
|
||||
g.drawCircle(b.x, b.y, pointRadius)
|
||||
g.endFill()
|
||||
}
|
||||
}
|
||||
@@ -1289,14 +1398,28 @@ function drawPaths() {
|
||||
}
|
||||
}
|
||||
|
||||
function followCurveValue(points: Array<{ x: number; y: number }>, t: number) {
|
||||
type FollowCurvePoint = { x: number; y: number; inX?: number; inY?: number; outX?: number; outY?: number }
|
||||
|
||||
function followCurveValue(points: FollowCurvePoint[], t: number, mode: 'linear' | 'bezier' = 'linear') {
|
||||
if (!points.length) return t
|
||||
const sorted = [...points].sort((a, b) => a.x - b.x)
|
||||
if (t <= sorted[0].x) return sorted[0].y
|
||||
for (let index = 1; index < sorted.length; index++) {
|
||||
const a = sorted[index - 1], b = sorted[index]
|
||||
if (t <= b.x) {
|
||||
const amount = (t - a.x) / Math.max(0.0001, b.x - a.x)
|
||||
let amount = (t - a.x) / Math.max(0.0001, b.x - a.x)
|
||||
if (mode === 'bezier') {
|
||||
const x1 = a.x + (a.outX ?? (b.x - a.x) / 3)
|
||||
const x2 = b.x + (b.inX ?? -(b.x - a.x) / 3)
|
||||
let low = 0, high = 1
|
||||
for (let step = 0; step < 18; step++) {
|
||||
const middle = (low + high) / 2
|
||||
if (cubicPoint(a.x, x1, x2, b.x, middle) < t) low = middle
|
||||
else high = middle
|
||||
}
|
||||
amount = (low + high) / 2
|
||||
return cubicPoint(a.y, a.y + (a.outY ?? (b.y - a.y) / 3), b.y + (b.inY ?? -(b.y - a.y) / 3), b.y, amount)
|
||||
}
|
||||
return a.y + (b.y - a.y) * amount
|
||||
}
|
||||
}
|
||||
@@ -1352,9 +1475,10 @@ function buildPreviewColliders(timeSeconds: number) {
|
||||
const duration = Math.max(0.05, Number(collider.followDuration) || 2)
|
||||
const normalized = Math.min(1, Math.max(0, timeSeconds / duration))
|
||||
const curve = Array.isArray(collider.followCurve) ? collider.followCurve : [{ x: 0, y: 0 }, { x: 1, y: 1 }]
|
||||
const progress = collider.followCurveMode === '线性'
|
||||
? normalized
|
||||
: Math.min(1, Math.max(0, followCurveValue(curve, normalized)))
|
||||
const curveMode = collider.followCurveMode === '跟随导出'
|
||||
? (store.settings.exportSettings.keyframeCurve === 'bezier' ? 'bezier' : 'linear')
|
||||
: collider.followCurveMode === '贝塞尔' ? 'bezier' : 'linear'
|
||||
const progress = Math.min(1, Math.max(0, followCurveValue(curve, normalized, curveMode)))
|
||||
const sample = sampleScenePath(path, progress)
|
||||
return sample
|
||||
? {
|
||||
@@ -1408,6 +1532,7 @@ function loop() {
|
||||
for (const sys of store.systems) {
|
||||
const em = emitterMap.get(sys.id)
|
||||
if (!em) continue
|
||||
em.setFollowExportCurveMode(store.settings.exportSettings.keyframeCurve === 'bezier' ? '贝塞尔' : '线性')
|
||||
const visible = sys.visible !== false
|
||||
em.visible = visible
|
||||
em.renderable = visible
|
||||
@@ -1520,11 +1645,14 @@ function loop() {
|
||||
}
|
||||
for (const [systemId, em] of emitterMap) {
|
||||
const system = store.systems.find((item) => item.id === systemId)
|
||||
const loopFrame = system?.config.mode === 'stream' && system.config.streamBehavior === 'loop'
|
||||
const isLoopStream = system?.config.mode === 'stream' && system.config.streamBehavior === 'loop'
|
||||
const loopFrame = isLoopStream
|
||||
? system.frames?.[tl.frame]
|
||||
: undefined
|
||||
const states = loopFrame || em.update(frameDuration)
|
||||
if (loopFrame) em.apply(loopFrame)
|
||||
const forceEmptyInitialFrame = tl.frame === 0 && !isLoopStream
|
||||
const states = forceEmptyInitialFrame ? [] : (loopFrame || em.update(frameDuration))
|
||||
if (forceEmptyInitialFrame) em.apply([])
|
||||
else if (loopFrame) em.apply(loopFrame)
|
||||
const systemVisible = system?.visible !== false
|
||||
if (systemVisible) for (const state of states) if (state.active) appendStateBones(collected, state)
|
||||
total += states.length
|
||||
@@ -1576,7 +1704,9 @@ function loop() {
|
||||
total++
|
||||
}
|
||||
} else if (emitter) {
|
||||
const states = emitter.update(0)
|
||||
// 尚未录制时,第 0 帧也必须保持绝对空白,不能由 update(0) 提前触发发射。
|
||||
const states = tl.frame === 0 ? [] : emitter.update(0)
|
||||
if (tl.frame === 0) emitter.apply([])
|
||||
if (sys.visible !== false) for (const state of states) if (state.active) appendStateBones(collected, state)
|
||||
total += emitter.activeCount
|
||||
}
|
||||
@@ -1599,6 +1729,7 @@ function loop() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', onHelpKeydown)
|
||||
const el = holder.value!
|
||||
app = new Application({ width: el.clientWidth || 800, height: el.clientHeight || 600, backgroundAlpha: 0, antialias: true })
|
||||
el.appendChild(app.view as unknown as HTMLCanvasElement)
|
||||
@@ -1611,7 +1742,7 @@ onMounted(async () => {
|
||||
const cfg = ensureEmitterConfig(sys.config as any)
|
||||
if (trailTex) {
|
||||
for (const resource of cfg.trailResources) {
|
||||
if (!resource.texture && resource.previewUrl === '/trails/trail.png') resource.texture = trailTex
|
||||
if (!resource.texture && (resource.previewUrl === '/images/trail.png' || resource.previewUrl === '/trails/trail.png')) resource.texture = trailTex
|
||||
}
|
||||
}
|
||||
for (const resource of cfg.imageResources) {
|
||||
@@ -1652,6 +1783,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', onHelpKeydown)
|
||||
cancelAnimationFrame(raf)
|
||||
spineRuntime?.destroy()
|
||||
spineRuntime = null
|
||||
@@ -1723,7 +1855,59 @@ onUnmounted(() => {
|
||||
.background-opacity-row { display: grid; grid-template-columns: 46px 1fr 34px; align-items: center; gap: 7px; margin-top: 10px; font-size: 11px; }
|
||||
.background-opacity-row input { min-width: 0; accent-color: #675de0; }
|
||||
.background-opacity-row b { color: #aeb8cc; font-size: 10px; text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.zoom-pct { margin-left: auto; font-variant-numeric: tabular-nums; color: #9aa; min-width: 44px; text-align: center; }
|
||||
.help-btn {
|
||||
height: 28px; margin-left: auto; padding: 0 11px; flex: 0 0 auto; border: 1px solid #3a4963; border-radius: 6px;
|
||||
background: #202a3b; color: #cbd7ed; font-size: 12px; font-weight: 700; cursor: pointer;
|
||||
}
|
||||
.help-btn:hover, .help-btn.on { border-color: #7774ff; background: #343b63; color: #fff; }
|
||||
.zoom-pct { margin-left: 0; font-variant-numeric: tabular-nums; color: #9aa; min-width: 44px; text-align: center; }
|
||||
.help-overlay {
|
||||
position: absolute; inset: 0; z-index: 40; display: flex; align-items: center; justify-content: center; padding: 72px 24px 28px;
|
||||
box-sizing: border-box; background: rgba(5, 8, 15, .72); backdrop-filter: blur(3px);
|
||||
}
|
||||
.help-dialog {
|
||||
width: min(820px, 100%); max-height: 100%; display: flex; flex-direction: column; overflow: hidden;
|
||||
border: 1px solid #3a4864; border-radius: 14px; background: #141a27; color: #b8c3d8;
|
||||
box-shadow: 0 22px 70px rgba(0, 0, 0, .62);
|
||||
}
|
||||
.help-head {
|
||||
display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; padding: 18px 20px 15px;
|
||||
border-bottom: 1px solid #2b354a; background: linear-gradient(135deg, #202942, #181e2d);
|
||||
}
|
||||
.help-head h2 { margin: 0; color: #eef2ff; font-size: 19px; }
|
||||
.help-head p { margin: 5px 0 0; color: #7786a3; font-size: 11px; }
|
||||
.help-close {
|
||||
width: 28px; height: 28px; flex: 0 0 28px; border: 1px solid #43516b; border-radius: 6px;
|
||||
background: #273249; color: #ccd7ed; cursor: pointer;
|
||||
}
|
||||
.help-close:hover { border-color: #7774ff; background: #3b4670; color: #fff; }
|
||||
.help-body { min-height: 0; overflow: auto; padding: 18px 20px 21px; }
|
||||
.help-section { padding: 14px 15px; border: 1px solid #2c3850; border-radius: 10px; background: #181f2e; }
|
||||
.help-section h3 { margin: 0 0 11px; color: #9fb2ff; font-size: 13px; }
|
||||
.help-start ol { margin: 0; padding-left: 21px; color: #aab6cc; font-size: 12px; line-height: 1.75; }
|
||||
.help-start li::marker { color: #7774ff; font-weight: 700; }
|
||||
.help-section b { color: #dce4f7; }
|
||||
.help-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(250px, .85fr); gap: 12px; margin-top: 12px; }
|
||||
.help-list { display: grid; gap: 0; margin: 0; }
|
||||
.help-list > div { display: grid; grid-template-columns: 78px 1fr; gap: 9px; padding: 7px 0; border-bottom: 1px solid #263146; }
|
||||
.help-list > div:last-child { border-bottom: 0; }
|
||||
.help-list dt { color: #d3dcf0; font-size: 11px; font-weight: 700; }
|
||||
.help-list dd { margin: 0; color: #8795ae; font-size: 11px; line-height: 1.5; }
|
||||
.shortcut-list { display: grid; gap: 7px; }
|
||||
.shortcut-list > div { display: flex; align-items: center; min-height: 25px; gap: 5px; }
|
||||
.shortcut-list kbd {
|
||||
min-width: 24px; box-sizing: border-box; padding: 3px 6px; border: 1px solid #465470; border-bottom-width: 2px;
|
||||
border-radius: 5px; background: #101623; color: #e0e7f6; font: 10px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; text-align: center;
|
||||
}
|
||||
.shortcut-list span { color: #65728b; font-size: 10px; }
|
||||
.shortcut-list em { margin-left: auto; color: #9ca9c1; font-size: 11px; font-style: normal; }
|
||||
.help-note { margin: 11px 0 0; padding-top: 9px; border-top: 1px solid #2a3549; color: #697791; font-size: 10px; line-height: 1.55; }
|
||||
.help-export { margin-top: 12px; }
|
||||
.help-export p { margin: 0; color: #8997b0; font-size: 11px; line-height: 1.65; }
|
||||
@media (max-width: 760px) {
|
||||
.help-overlay { padding: 66px 12px 14px; }
|
||||
.help-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
.hud { position: absolute; bottom: 12px; left: 12px; padding: 8px 12px; background: rgba(20,20,32,0.75); border-radius: 8px; font-size: 12px; color: #a0a8cc; }
|
||||
.hud-number { display: inline-block; box-sizing: border-box; text-align: right; font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; }
|
||||
.hud-system-count { width: 3ch; }
|
||||
|
||||
Reference in new issue
Block a user