Fabric.js Document Engine

搜索文档

查找页面或章节

EN

阿拉伯文和从右到左文字

查看 Markdown

让 Fabric.js 从右到左文字的光标、选区和点击都对准相连的字母。

Fabric.js 会单独测量每个字母,再把宽度加起来。但它绘制时会一次画出一串字母,字体会把它们连在一起:阿拉伯文字母会变成相连的形态,“ffi”也可能变成一个连字。于是在 Fabric.js 从右到左文字中,实际画出的行比测量的短,光标、选区和点击都会偏离字母(fabric.js #4815)。40px 的阿拉伯文,光标可能偏出两个字母。

试一试

正在加载在线示例,代码在下方。

shaped-text-demo.tsx
"use client";
 
import { IText } from "fabric";
import { ShapedIText } from "fabricjs-document-engine/text";
import { TextCursorInputIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
 
import { DemoCanvas, DemoFrame, Status, Toolbar, ToolButton } from "./demo-ui";
import { PAGE, useFabricCanvas } from "./use-fabric-canvas";
 
const ARABIC = "مرحبا بالعالم العربي الجميل";
const FIRST_SPACE = [...ARABIC].indexOf(" ");
 
export default function ShapedTextDemo() {
  const t = useTranslations("demos");
  const { elementRef, canvas } = useFabricCanvas();
  const textsRef = useRef<{ plain: IText; shaped: IText } | null>(null);
  const [active, setActive] = useState<"plain" | "shaped" | null>(null);
 
  useEffect(() => {
    if (!canvas) return;
    const common = {
      fontSize: 34,
      fontFamily: "sans-serif",
      direction: "rtl",
      textAlign: "right",
      originX: "right",
      originY: "top",
      left: PAGE.width - 24,
      cursorColor: "#e11d48",
      cursorWidth: 3,
    } as const;
    const plain = new IText(ARABIC, { ...common, top: 50, fill: "#71717a" });
    const shaped = new ShapedIText(ARABIC, {
      ...common,
      top: 170,
      fill: "#0d0d0d",
    });
    textsRef.current = { plain, shaped };
    canvas.add(plain, shaped);
    canvas.requestRenderAll();
    return () => {
      canvas.remove(plain, shaped);
    };
  }, [canvas]);
 
  function placeCursor(which: "plain" | "shaped") {
    const texts = textsRef.current;
    if (!canvas || !texts) return;
    texts.plain.exitEditing();
    texts.shaped.exitEditing();
    const text = texts[which];
    canvas.setActiveObject(text);
    text.enterEditing();
    text.setSelectionStart(FIRST_SPACE);
    text.setSelectionEnd(FIRST_SPACE);
    canvas.requestRenderAll();
    setActive(which);
  }
 
  return (
    <DemoFrame>
      <DemoCanvas elementRef={elementRef} label={t("canvas")} />
 
      <Toolbar>
        <ToolButton
          icon={TextCursorInputIcon}
          label={t("shaped.plain")}
          primary={active === "plain"}
          disabled={!canvas}
          onClick={() => placeCursor("plain")}
        />
        <ToolButton
          icon={TextCursorInputIcon}
          label={t("shaped.shaped")}
          primary={active === "shaped"}
          disabled={!canvas}
          onClick={() => placeCursor("shaped")}
        />
      </Toolbar>
 
      <Status tone={active === "plain" ? "error" : active ? "ok" : "idle"}>
        {active ? t(`shaped.${active}Result`) : t("shaped.hint")}
      </Status>
    </DemoFrame>
  );
}

两行是同样的阿拉伯文。按钮会把光标放在第一个单词之后。

使用按字形测量的类

import {
  ShapedIText,
  ShapedTextbox,
  textObjects,
} from "fabricjs-document-engine/text";
 
const engine = createDocumentEngine({ canvas, customObjects: textObjects });
 
canvas.add(
  new ShapedIText("مرحبا بالعالم", {
    direction: "rtl",
    textAlign: "right",
    fontSize: 36,
  })
);

ShapedIText 和 ShapedTextbox 按浏览器实际排版的文字逐词测量每个位置,并缓存结果。输入、选择、点击、复制和粘贴和 IText、Textbox 一样。

  • Fabric.js 从右到左文字仍然使用 direction: "rtl"。
  • 拉丁字体中的连字,例如 “fi” 和 “ffi”,也会得到同样的修正。
  • 有字间距(charSpacing)、justify 或沿路径排列的文字,Fabric 本来就逐个字母绘制,测量已经准确,所以保持不变。

在 BoundedTextbox 中

new BoundedTextbox(arabicText, { width: 260, direction: "rtl", shaping: true });

shaping: true 还会让换行跟随相连的字母。见文本框宽度。

测量函数列在文字 API里,可以用在你自己的 Fabric.js 从右到左文字类中。