实现字体子系统:注册表枚举中间件、前端 FontFace 挂载与文字面板
This commit is contained in:
@@ -1,21 +1,255 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Shadow, Textbox } from "fabric";
|
||||
import { useEditor } from "@/editor/useEditor";
|
||||
import { ensureFontLoaded, fonts, listFonts, loadingFonts } from "@/fonts/useFonts";
|
||||
|
||||
/**
|
||||
* 【T2 待实现 — 文字面板】
|
||||
* 【T2 — 文字面板】选中 Textbox 时显示。
|
||||
*
|
||||
* 选中 Textbox 时显示。要有:字体下拉(来自 useFonts)、字号、字重/颜色、
|
||||
* 对齐、行高、描边与阴影(现有封面靠这两样保证白字在图上读得清)。
|
||||
* 字体下拉列出 `useFonts` 里的本机字体,选中即挂载并应用到当前文本的 `fontFamily`;
|
||||
* 字号、颜色、对齐、行高、描边(stroke/strokeWidth)与阴影(shadow)。
|
||||
* 描边和阴影是刚需:现有封面全靠它们保证白字压在图上还读得清。
|
||||
*
|
||||
* 拿当前选中对象用 `useEditor().activeObject`,改完记得 `canvas.requestRenderAll()`。
|
||||
* 画布选中对象是 Fabric 实例,不是响应式的,所以这里用一批本地 ref 做 UI 状态:
|
||||
* 选中对象变化时从对象同步进来,用户改动时写回对象并 `requestRenderAll()`。
|
||||
*/
|
||||
const { activeObject, getCanvas } = useEditor();
|
||||
|
||||
const textObj = computed<Textbox | null>(() =>
|
||||
activeObject.value instanceof Textbox ? activeObject.value : null,
|
||||
);
|
||||
|
||||
// 本地 UI 状态。
|
||||
const fontSize = ref(40);
|
||||
const lineHeight = ref(1.16);
|
||||
const fill = ref("#ffffff");
|
||||
const textAlign = ref("center");
|
||||
const stroke = ref("");
|
||||
const strokeWidth = ref(0);
|
||||
const shadowEnabled = ref(false);
|
||||
const shadowColor = ref("#000000");
|
||||
const shadowBlur = ref(0);
|
||||
const shadowOffsetX = ref(0);
|
||||
const shadowOffsetY = ref(0);
|
||||
|
||||
/** 选中对象变化 → 把对象上的值同步进 UI。 */
|
||||
function syncFromObject() {
|
||||
const obj = textObj.value;
|
||||
if (!obj) return;
|
||||
fontSize.value = typeof obj.fontSize === "number" ? obj.fontSize : 40;
|
||||
lineHeight.value = typeof obj.lineHeight === "number" ? obj.lineHeight : 1.16;
|
||||
fill.value = typeof obj.fill === "string" ? obj.fill : "#ffffff";
|
||||
textAlign.value = obj.textAlign ?? "left";
|
||||
stroke.value = typeof obj.stroke === "string" ? obj.stroke : "";
|
||||
strokeWidth.value = typeof obj.strokeWidth === "number" ? obj.strokeWidth : 0;
|
||||
const s = obj.shadow;
|
||||
shadowEnabled.value = !!s;
|
||||
shadowColor.value = typeof s?.color === "string" ? s.color : "#000000";
|
||||
shadowBlur.value = typeof s?.blur === "number" ? s.blur : 0;
|
||||
shadowOffsetX.value = typeof s?.offsetX === "number" ? s.offsetX : 0;
|
||||
shadowOffsetY.value = typeof s?.offsetY === "number" ? s.offsetY : 0;
|
||||
}
|
||||
|
||||
watch(textObj, syncFromObject, { immediate: true });
|
||||
|
||||
/** 用户改动 → 写回对象并重绘。 */
|
||||
function applyToObject() {
|
||||
const obj = textObj.value;
|
||||
if (!obj) return;
|
||||
|
||||
const patch: Record<string, unknown> = {
|
||||
fill: fill.value,
|
||||
textAlign: textAlign.value,
|
||||
stroke: stroke.value || null,
|
||||
};
|
||||
|
||||
const fs = Number(fontSize.value);
|
||||
if (Number.isFinite(fs) && fs > 0) patch.fontSize = fs;
|
||||
const lh = Number(lineHeight.value);
|
||||
if (Number.isFinite(lh) && lh > 0) patch.lineHeight = lh;
|
||||
const sw = Number(strokeWidth.value);
|
||||
if (Number.isFinite(sw) && sw >= 0) patch.strokeWidth = sw;
|
||||
|
||||
let shadow: Shadow | null = null;
|
||||
if (shadowEnabled.value) {
|
||||
const blur = Number(shadowBlur.value);
|
||||
const ox = Number(shadowOffsetX.value);
|
||||
const oy = Number(shadowOffsetY.value);
|
||||
shadow = new Shadow({
|
||||
color: shadowColor.value || "#000000",
|
||||
blur: Number.isFinite(blur) && blur >= 0 ? blur : 0,
|
||||
offsetX: Number.isFinite(ox) ? ox : 0,
|
||||
offsetY: Number.isFinite(oy) ? oy : 0,
|
||||
});
|
||||
}
|
||||
patch.shadow = shadow;
|
||||
|
||||
obj.set(patch);
|
||||
getCanvas().requestRenderAll();
|
||||
}
|
||||
|
||||
watch(
|
||||
[
|
||||
fontSize,
|
||||
lineHeight,
|
||||
fill,
|
||||
textAlign,
|
||||
stroke,
|
||||
strokeWidth,
|
||||
shadowEnabled,
|
||||
shadowColor,
|
||||
shadowBlur,
|
||||
shadowOffsetX,
|
||||
shadowOffsetY,
|
||||
],
|
||||
applyToObject,
|
||||
);
|
||||
|
||||
/** 当前文本的 fontFamily 对应到列表里的 id;找不到(如默认 Times New Roman)就留空。 */
|
||||
const selectedFontId = computed(() => {
|
||||
const fam = textObj.value?.fontFamily;
|
||||
if (!fam) return "";
|
||||
return fonts.value.find((f) => f.family === fam)?.id ?? "";
|
||||
});
|
||||
|
||||
async function pickFont(id: string) {
|
||||
const entry = fonts.value.find((f) => f.id === id);
|
||||
const obj = textObj.value;
|
||||
if (!entry || !obj) return;
|
||||
obj.set("fontFamily", entry.family);
|
||||
getCanvas().requestRenderAll();
|
||||
await ensureFontLoaded(entry);
|
||||
}
|
||||
|
||||
listFonts();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p class="todo">T2 待实现:字体选择与文字样式</p>
|
||||
<div class="text-panel">
|
||||
<div class="field">
|
||||
<label>字体</label>
|
||||
<select :value="selectedFontId" @change="pickFont(($event.target as HTMLSelectElement).value)">
|
||||
<option value="" disabled>— 选择本机字体 —</option>
|
||||
<option v-for="f in fonts" :key="f.id" :value="f.id" :disabled="f.needsExtraction">
|
||||
{{ f.family }}{{ f.needsExtraction ? "(.ttc 合集,暂不支持)" : "" }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="loadingFonts" class="meta">正在读取本机字体…</p>
|
||||
<p v-else-if="fonts.length === 0" class="meta">未找到可用字体(本机需装 .ttf / .otf 字体)。</p>
|
||||
</div>
|
||||
|
||||
<div class="field row">
|
||||
<div class="grow">
|
||||
<label>字号</label>
|
||||
<input type="number" v-model.number="fontSize" min="1" step="1" />
|
||||
</div>
|
||||
<div class="grow">
|
||||
<label>行高</label>
|
||||
<input type="number" v-model.number="lineHeight" min="0.1" step="0.1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field row">
|
||||
<div class="grow">
|
||||
<label>颜色</label>
|
||||
<input type="color" v-model="fill" />
|
||||
</div>
|
||||
<div class="grow">
|
||||
<label>对齐</label>
|
||||
<select v-model="textAlign">
|
||||
<option value="left">左对齐</option>
|
||||
<option value="center">居中</option>
|
||||
<option value="right">右对齐</option>
|
||||
<option value="justify">两端对齐</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset class="group">
|
||||
<legend>描边</legend>
|
||||
<div class="field row">
|
||||
<div class="grow">
|
||||
<label>颜色</label>
|
||||
<input type="color" v-model="stroke" />
|
||||
</div>
|
||||
<div class="grow">
|
||||
<label>宽度</label>
|
||||
<input type="number" v-model.number="strokeWidth" min="0" step="1" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="group">
|
||||
<legend>阴影</legend>
|
||||
<label class="check">
|
||||
<input type="checkbox" v-model="shadowEnabled" />
|
||||
启用阴影
|
||||
</label>
|
||||
<template v-if="shadowEnabled">
|
||||
<div class="field">
|
||||
<label>颜色</label>
|
||||
<input type="color" v-model="shadowColor" />
|
||||
</div>
|
||||
<div class="field row">
|
||||
<div class="grow">
|
||||
<label>模糊</label>
|
||||
<input type="number" v-model.number="shadowBlur" min="0" step="1" />
|
||||
</div>
|
||||
<div class="grow">
|
||||
<label>水平偏移</label>
|
||||
<input type="number" v-model.number="shadowOffsetX" step="1" />
|
||||
</div>
|
||||
<div class="grow">
|
||||
<label>垂直偏移</label>
|
||||
<input type="number" v-model.number="shadowOffsetY" step="1" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</fieldset>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.todo {
|
||||
.text-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: var(--fg-dim);
|
||||
margin: 0;
|
||||
margin: 6px 0 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.group {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 8px 10px 4px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.group legend {
|
||||
padding: 0 4px;
|
||||
color: var(--fg-dim);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.check input {
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user