> For the complete documentation index, see [llms.txt](https://408550179s-organization.gitbook.io/blog/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://408550179s-organization.gitbook.io/blog/fu-wen-ben-miao-bian-zai-hua-bu-suo-fang-shi-bian-xi-huo-diu-shi-de-chu-li.md).

# 富文本描边在画布缩放时变细或丢失的处理

画布编辑器里做富文本描边时，最容易出现的问题是：看起来只是给文字加了 `-webkit-text-stroke`，但在缩放、选区加粗、局部样式切换之后，描边会变细、重复缩放，甚至直接丢失。

这个问题本质上不是 CSS 写错了，而是“全局描边”和“行内描边”混在了一起。

## 问题来源

比如一个文本节点有全局描边：

```css
#textInnerEditor {
  -webkit-text-stroke: 2px #000;
  paint-order: stroke fill;
}
```

当用户只选中几个字做局部加粗、斜体、颜色或者字号时，Quill 可能会在内部生成 `span`、`strong`、`em` 等标签。

如果这些标签错误继承或复制了全局描边，后续再做 zoom 缩放时，就会出现：

* 全局描边被当作局部描边保存。
* zoom 后的 px 值被再次作为原始值参与计算。
* `strong`、`em` 这类排版标签携带独立描边，导致视觉变细。

## 处理原则

我最后采用的是三个规则：

1. 全局描边只写在编辑器宿主节点。
2. `.ql-editor` 本身不能保留描边，避免 Quill 误认为它是 inline format。
3. 真正的行内描边需要记录原始值，缩放时只临时换算，结束后恢复。

## 全局描边只写宿主节点

```ts
export const applyGlobalTextStrokeToEditorHost = (
  host: HTMLElement,
  textStroke: string | undefined,
  zoomScale: number,
): void => {
  const strokeStyle = host.style as CSSStyleDeclaration & {
    webkitTextStroke?: string;
  };

  if (textStroke && textStroke !== "unset") {
    host.style.paintOrder = "stroke fill";
    strokeStyle.webkitTextStroke = scaleCssPxValues(textStroke, zoomScale);
  } else {
    host.style.paintOrder = "unset";
    strokeStyle.webkitTextStroke = "unset";
  }
};
```

这里有一个关键点：全局描边可以按 zoom 临时换算，但不要把换算后的值当成原始值保存。

## 清理 Quill 编辑器上的描边

```ts
export const clearQuillEditorTextStroke = (qlEditor: HTMLElement | null): void => {
  if (!qlEditor) return;
  qlEditor.style.removeProperty("-webkit-text-stroke");
  qlEditor.style.removeProperty("paint-order");
};
```

`.ql-editor` 只是富文本输入容器，不应该承担业务样式保存。如果它携带描边，Quill 在局部格式化时很容易把这个样式复制到内部节点。

## 全局描边场景下清理 typography 标签

```ts
const TYPOGRAPHY_STROKE_TAGS = new Set(["EM", "STRONG", "U", "S"]);

export const sanitizeTypographyTextStrokesForGlobal = (
  root: ParentNode | null,
  globalStroke: string | undefined,
  zoomScale: number,
): void => {
  if (!root || !globalStroke || globalStroke === "unset") return;

  root.querySelectorAll<HTMLElement>("em, strong, u, s").forEach((el) => {
    el.style.removeProperty("-webkit-text-stroke");
    el.style.removeProperty("paint-order");
    el.removeAttribute("data-htmltext-inline-text-stroke");
  });

  root.querySelectorAll<HTMLElement>("span[style]").forEach((el) => {
    const stroke = (el.style as CSSStyleDeclaration & {
      webkitTextStroke?: string;
    }).webkitTextStroke;

    if (!stroke || stroke === "unset" || stroke === "0px") return;

    const scaledGlobal = scaleCssPxValues(globalStroke, zoomScale);
    if (stroke === globalStroke || stroke === scaledGlobal) {
      el.style.removeProperty("-webkit-text-stroke");
      el.style.removeProperty("paint-order");
      el.removeAttribute("data-htmltext-inline-text-stroke");
    }
  });
};
```

这段逻辑的目的不是删除所有描边，而是只清理“误复制的全局描边”。真正用户选区设置的行内描边仍然保留。

## 行内描边缩放前记录原始值

```ts
export const scaleEditorInlineTextStrokes = (
  root: ParentNode,
  zoomScale: number,
): void => {
  if (!Number.isFinite(zoomScale) || zoomScale <= 0) return;

  root.querySelectorAll<HTMLElement>("[style]").forEach((el) => {
    const style = el.style as CSSStyleDeclaration & {
      webkitTextStroke?: string;
    };

    const originalStroke =
      el.getAttribute("data-htmltext-inline-text-stroke") ||
      style.webkitTextStroke;

    if (!originalStroke || originalStroke === "unset" || originalStroke === "0px") return;

    el.setAttribute("data-htmltext-inline-text-stroke", originalStroke);
    style.webkitTextStroke = scaleCssPxValues(originalStroke, zoomScale);
    style.paintOrder = "stroke fill";
  });
};
```

恢复时再把原始值拿回来：

```ts
export const restoreEditorInlineTextStrokes = (root: ParentNode | null): void => {
  if (!root) return;

  root.querySelectorAll<HTMLElement>("[data-htmltext-inline-text-stroke]").forEach((el) => {
    const originalStroke = el.getAttribute("data-htmltext-inline-text-stroke");
    const style = el.style as CSSStyleDeclaration & {
      webkitTextStroke?: string;
    };

    if (originalStroke) style.webkitTextStroke = originalStroke;
    el.removeAttribute("data-htmltext-inline-text-stroke");
  });
};
```

## 总结

富文本描边的问题，关键不在于 `-webkit-text-stroke` 怎么写，而在于样式归属：

* 全局描边属于整个文本节点。
* 行内描边属于选区。
* zoom 缩放值只能作为临时显示值。
* Quill 的内部容器不要承担业务样式。

把这几层分开之后，描边、加粗、斜体、缩放、撤销重做才能稳定共存。
