JEPA4Japan · チュートリアル

第11章:描かれた文字は編集できる文字ではない

5,559文字 16分で読めます #Canvas#Frontend Engineering#Infinite Canvas#ELI5

Canvasはプレビュー、DOMは編集を担当し、Font、Wrap、Grapheme、Bidi、IME、Caret、Selectionを正しく扱います。

コース進捗 コース目次 18レッスン中 18件を公開中

第I部:描く前に描画面を選ぶ——プロダクト、ピクセル、座標

  1. 01 第1章:まだ描かない——Canvasはプロダクト設計ではない 公開中
  2. 02 第2章:すぐに記憶を失うピクセルの紙 公開中
  3. 03 第3章:お絵描きを再現可能なレシピにする 公開中
  4. 04 第4章:4枚の地図と1台のカメラ 公開中

第II部:ピクセル世界に頭脳を与える——モデル、スケジューリング、入力、ツール

  1. 05 第5章:ピクセル世界に台帳を作る 公開中
  2. 06 第6章:ランプが点いたときだけ描き直す——Render SchedulerとReactの境界 公開中
  3. 07 第7章:マウス、指、ペンに同じ言葉を話してもらう 公開中
  4. 08 第8章:細い縁を調べる前に大きな箱を探す 公開中
  5. 09 第9章:ツールは信号機であり、Booleanの袋ではない 公開中

第III部:「ドラッグできる」から「信頼できる」へ——操作、文字、Asset、復旧

  1. 10 第10章:触って気持ちよいエディターにする 公開中
  2. 11 第11章:描かれた文字は編集できる文字ではない 現在のレッスン
  3. 12 第12章:借りた画像を勝手に箱へ詰めてはいけない 公開中
  4. 13 第13章:タイムマシンと古い箱 公開中
  5. 14 第14章:正しく見えることと、本当に正しいことは違う 公開中

第IV部:マスターの判断——Performance、Worker、GPU、SDK、共同編集、AI

  1. 15 第15章:1万人を一人ずつ探さない 公開中
  2. 16 第16章:受付を厨房へ入れない——WorkerとGPUへの更新 公開中
  3. 17 第17章:車を自作するか、実績あるシャーシを買うか 公開中
  4. 18 第18章:人とAIが同じ台帳を編集する 公開中

まずは5歳児にもわかるゲームから

この章で覚える真実は一つだけです。Text Rendering と Text Editing はまったく別のシステムです。

紙に「こんにちは 👨‍👩‍👧‍👦」と書いて写真を撮り、元の紙と写真の両方を子どもへ渡します。どちらにも文字が見えます。子どもに「ん」と「に」の間へキャレットを置く、「に」を選択する、日本語入力で読みを漢字へ変換する、読み上げ機能で文章を読ませる、という操作を頼んでみましょう。

先に予想してみてください。写真でできるのはどれでしょうか。プログラムが keydown のたびに文字列の末尾へ1文字足したら、中国語のピンイン変換中には何が起きるでしょうか。Backspace を1回押して家族の Emoji を削除するとき、一人だけ消すべきでしょうか、それとも家族の図柄全体でしょうか。

  1. 普段は写真を見る高速で、絵と一緒に動く
  2. 直すときは元の紙に替えるキャレット、選択、言語入力
  3. 元の紙を正確に重ねる位置、拡縮、回転
  4. 書き終えたら新しい写真を撮るcommit または cancel
まず答えてください。文字が見えることは、ブラウザが編集可能な位置をすべて知っていることと同じでしょうか。同じではありません。ピクセルには Caret、Selection、言語入力プロトコルがありません。

Canvas の fillText() は文字を写真へ焼き付けるようなものです。textarea や contenteditable はブラウザが理解する元の紙です。編集中でない Text Shape は Renderer が描き、Editing State に入ったら DOM Overlay が引き受けます。ユーザーが Commit すると Document を更新して Renderer が再描画し、Cancel すれば Overlay を捨てて元の値を保ちます。

おもちゃを Canvas に置き換える

おもちゃの世界Text システム担当
文字の写真Canvas Preview高速表示と Scene に合わせた描画
書き込める元の紙textarea / contenteditableCaret、Selection、Clipboard、IME、Spellcheck
活字の箱FontFace / document.fontsFont の読み込みと準備完了通知
文字の定規TextMetrics / DOM Measurement幅、baseline、実 bounds、折り返し
一つの「見える文字」Grapheme ClusterEmoji と結合文字に対するユーザー操作単位
右から左へ読む本Bidi / RTLテキストアルゴリズムが視覚順と論理順を処理
入力候補カードComposition Events変換中の値は最終 Document 値ではない
紙を重ねる透明テープDOM Overlay TransformShape/Camera に合わせ Zoom を補償
書き終える/破棄するCommit / CancelHistory Transaction が一つ、または書き込みゼロ

この比喩には限界があります。DOM Overlay と Canvas は同じ組版エンジンではありません。同一の font string でも、折り返し、fallback、hinting、baseline に数ピクセルの差が出ます。Grapheme はユーザーが認識する単位ですが、言語学上の文字や単語とは限りません。contenteditable は豊富なブラウザ機能を提供しますが、安全な Rich Text データモデルではありません。貼り付けた HTML には引き続き sanitize と normalize が必要です。

まず誤った直感を捨てる

  • 「Canvas で文字を描けるなら、ついでにキャレットも描けばよい。」 Selection、IME、Bidi、Clipboard、仮想キーボード、支援技術、プラットフォーム慣習も再実装することになり、コストはキャレットの比ではありません。
  • 「keydown の event.key を文字列へ足すのが一番制御しやすい。」 Composition は中間キーを発行し、Emoji は複数 code point からなり、Selection が末尾にあるとも限りません。
  • 「text.length が文字数だ。」 UTF-16 code unit、Unicode code point、Grapheme Cluster は異なります。家族 Emoji の length は 1 よりはるかに大きくなります。
  • 「Font URL の読み込みが終われば測定できる。」 Font が decode 前かもしれず、fallback がすでに layout に参加しているかもしれません。FontFaceSet を待ち、font 変更後に invalidate します。
  • 「Canvas の measureText が自動で折り返す。」 渡された inline text を測るだけです。段落 layout または DOM measurement が折り返しを担当します。
  • 「textarea を Shape の x/y に置けばそろう。」 Parent、World、Camera の各行列、rotation、transform origin、padding、line-height、Zoom 補償がまだ必要です。
  • 「Rich Text は innerHTML を保存すればよい。」 信頼できない HTML、ブラウザ私有マークアップ、業務モデルが混ざります。

Production backpack

前提となる契約

第4章は Shape Local→World→Screen 行列を提供します。第6章には対称な cleanup を持つ DOMOverlayLayer Portal があります。第9章には editing State と Commit/Cancel があります。第10章の Selection と Resize は Text Editing 中に入力を奪いません。Document の Text Shape は text、width、style token、direction を保存し、DOM node は保存しません。

正式な知識

Font Loading はファイルを一つダウンロードするだけではありません。FontFace は読み込み可能な font を表し、document.fonts は FontFaceSet です。layout や測定の前に await document.fonts.load('16px "Canvas Sans"', sample) を呼べます。loadingdone 後は invalidate します。失敗時は明示的な Font Fallback chain を使い、失敗を記録しても編集は止めません。fallback が対象 font に置き換わると、Text Metrics、折り返し、Shape height、Connector anchor がすべて変わり得ます。Document 内容を密かに変えず、派生 layout を再計算します。

Canvas の measureText() は TextMetrics を返し、width のほか一部ブラウザでは actual bounding box、font bounding box、baseline 関連値を提供します。textBaseline は描画の基準線であって CSS line box ではありません。Primitive Text Rendering は font、direction、textAlign、baseline を明示し、layout 済みの各行へ fillText() を呼びます。Canvas に段落折り返し API はありません。単純な Text Shape なら決定的な幅で改行できます。本番 Rich Text は hidden DOM の authoritative measurement または成熟した layout engine が適し、measurement version を cache key に含めます。

Line Breaking は空白で split することではありません。CJK には空白がなく、禁則処理、soft hyphen、長い単語にも規則が必要です。Wrapping は hard break \n と soft wrap を区別します。Intl.Segmenter(...,{granularity:'grapheme'}) は多くの Grapheme Cluster の途中分割を防ぎます。対象 Browser Matrix にない場合は Unicode fixture で検証した segmentation library を同梱します。Array.from(string) は code point へ分けるだけで Grapheme fallback にはなりません。完全な Unicode line breaking には、対応言語向けの改行 library または DOM 組版も必要です。Emoji、結合アクセント、肌色 modifier、ZWJ sequence をユーザーが認識するまとまりとしてテストします。

Bidi アルゴリズムは LTR/RTL 混在文の視覚配置を決めます。Document は論理文字列を保存し、画面順に文字を並べ替えません。Shape は direction: auto|ltr|rtl を持ち、Overlay は dir と CSS direction、Canvas は context direction を設定します。アラビア語を手動反転してはいけません。Font Fallback は Grapheme ごとに起きるため、測定では描画と同じ CSS font shorthand を使います。

IME は compositionstart/update/end と beforeinput/input を協調させます。変換中に DOM 値は変わりますが、中間段階ごとに History を作ってはいけません。compositionend だけが Commit のタイミングとも限りません。編集 transaction 終了時に Overlay の現在値を Commit します。keydown は Escape や明示的 shortcut など制御意図だけを扱い、変換中は event.isComposing を確認します。Caret、Selection、ローカル undo stack、Clipboard は native control に任せます。Text Shape の Undo と control 内 Undo の境界も必要です。たとえば Editing 中の Ctrl/Cmd+Z は control へ渡し、終了後に Document History へ渡します。

textarea はプレーンテキスト向けで、安定し Selection、IME、モバイルキーボードを標準対応します。contenteditable は Rich Text 向けですが、正規化モデル、Paste Sanitization、Selection mapping、ブラウザマトリックステストが必要です。spellcheck、autocorrect、autocomplete はプロダクトとプライバシー要件で選び、ユーザー文字列を独自 telemetry へ送ってはいけません。Screen Reader には accessible name、編集状態、フォーカスが必要です。通常時にも DOM Inspector または Fallback Semantics を提供し、「Canvas」とだけ読ませないようにします。

Textarea Overlay を開くときは元値を固定し、DOM を作り、value/dir/lang/spellcheck を設定し、完全な screen matrix を適用し、フォーカスして Selection を復元します。Camera または Parent Transform が変わるたび matrix を更新し、構築時だけ合わせてはいけません。Zoom-Compensated Editor は screen scale に応じて DOM font size を増やす方法も、外側へ matrix transform を適用する方法も使えます。どちらでも DOM wrap width と Canvas layout のワールド幅を一致させます。Rotation は位置だけでなく CSS matrix で適用します。Commit は control 値を読み、最大長と Rich Text schema を検証し、一つの UpdateText Command を発行します。Document Schema が明記しユーザーへ説明しない限り、ここで NFC/NFKC normalization を密かに行ってはいけません。正規化は実 code point 列も変えるためです。Cancel は Command を発行しません。Exit は listener と node を除去し、フォーカスを Canvas/Inspector へ戻します。

根拠と互換性(確認日:2026-08-29)

この章で積み上げる実装

開始地点: Text Label は Canvas fillText() だけを使い、Caret、IME、Clipboard がありません。完了地点: 非編集中は Canvas Preview、編集中は DOM Overlay が Text Shape を正確に覆います。Commit は Document を一度更新し、Cancel は書き込みゼロです。

次のファイルを追加します。

  • src/engine/text/TextLayout.ts:font cache、Grapheme-safe wrapping、baseline。
  • src/engine/text/FontManager.ts:FontFaceSet readiness と invalidation。
  • src/ui/overlays/TextEditorOverlay.ts:textarea ライフサイクルと行列。
  • src/engine/shapes/TextShapeRenderer.ts:layout lines から Preview を描画。
  • src/engine/text/__tests__/layout.test.ts:CJK/Emoji/RTL/fallback。
  • tests/browser/text-ime.spec.ts:composition、selection、clipboard、rotation、Zoom。

まず、テスト可能な primitive layout を完全な純粋関数で作ります。hard break、Grapheme、最大幅を尊重し、split('') で Emoji を壊しません。

export type Measure = (text: string) => number;
export type TextLine = Readonly<{ text: string; width: number }>;

export function wrapGraphemes(
  value: string,
  maxWidth: number,
  measure: Measure,
  locale = 'und',
): readonly TextLine[] {
  if (!(maxWidth > 0)) throw new Error('TEXT_WIDTH_MUST_BE_POSITIVE');
  const segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' });
  const output: TextLine[] = [];
  for (const paragraph of value.split('\n')) {
    const graphemes = Array.from(segmenter.segment(paragraph), (part) => part.segment);
    if (graphemes.length === 0) {
      output.push({ text: '', width: 0 });
      continue;
    }
    let line = '',
      width = 0;
    for (const grapheme of graphemes) {
      const candidate = line + grapheme;
      const candidateWidth = measure(candidate);
      if (line && candidateWidth > maxWidth) {
        output.push({ text: line, width });
        line = grapheme;
        width = measure(grapheme);
      } else {
        line = candidate;
        width = candidateWidth;
      }
    }
    output.push({ text: line, width });
  }
  return output;
}

export async function layoutCanvasText(
  context: CanvasRenderingContext2D,
  value: string,
  cssFont: string,
  width: number,
  locale: string,
  onFontFailure: (reason: unknown) => void = () => undefined,
): Promise<readonly TextLine[]> {
  try {
    const faces = await document.fonts.load(cssFont, value || 'M');
    if (faces.length === 0) onFontFailure(new Error(`no face matched ${cssFont}`));
  } catch (cause) {
    onFontFailure(cause);
  }
  context.save();
  try {
    context.font = cssFont;
    return wrapGraphemes(value, width, (text) => context.measureText(text).width, locale);
  } finally {
    context.restore();
  }
}

以下は完全なプレーンテキスト Overlay です。通常文字の keydown を監視せず、composition 中は Document を書かず、Shape→Screen matrix をそのまま CSS へ渡します。

export type TextShape = Readonly<{
  id: string;
  text: string;
  width: number;
  minHeight: number;
  font: string;
  fontSize: number;
  lineHeight: number;
  color: string;
  direction: 'auto' | 'ltr' | 'rtl';
  language: string;
}>;
export type Matrix2D = Readonly<{
  a: number;
  b: number;
  c: number;
  d: number;
  e: number;
  f: number;
}>;
export interface TextEditPort {
  commit(id: string, before: string, after: string): void;
  focusCanvas(): void;
}

export class TextEditorOverlay {
  private readonly textarea: HTMLTextAreaElement;
  private composing = false;
  private blurPending = false;
  private closed = false;

  constructor(
    host: HTMLElement,
    private readonly shape: TextShape,
    screenMatrix: Matrix2D,
    private readonly port: TextEditPort,
  ) {
    const area = document.createElement('textarea');
    this.textarea = area;
    area.value = shape.text;
    area.dir = shape.direction;
    area.lang = shape.language;
    area.spellcheck = true;
    area.setAttribute('aria-label', 'Canvas のテキストを編集');
    area.style.position = 'absolute';
    area.style.left = '0';
    area.style.top = '0';
    area.style.width = `${shape.width}px`;
    area.style.minHeight = `${shape.minHeight}px`;
    area.style.margin = '0';
    area.style.padding = '0';
    area.style.border = '1px solid currentColor';
    area.style.background = 'Canvas';
    area.style.color = shape.color;
    area.style.font = shape.font;
    area.style.lineHeight = String(shape.lineHeight);
    area.style.resize = 'none';
    area.style.transformOrigin = '0 0';
    this.updateScreenMatrix(screenMatrix);
    area.addEventListener('compositionstart', this.onCompositionStart);
    area.addEventListener('compositionend', this.onCompositionEnd);
    area.addEventListener('keydown', this.onKeyDown);
    area.addEventListener('blur', this.onBlur);
    host.append(area);
    area.focus({ preventScroll: true });
    area.setSelectionRange(area.value.length, area.value.length);
  }

  private onCompositionStart = () => {
    this.composing = true;
  };
  private onCompositionEnd = () => {
    this.composing = false;
    if (this.blurPending) this.commit();
  };
  private onKeyDown = (event: KeyboardEvent) => {
    if (event.isComposing || this.composing) return;
    if (event.key === 'Escape') {
      event.preventDefault();
      this.cancel();
    }
    if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
      event.preventDefault();
      this.commit();
    }
  };
  private onBlur = () => {
    if (this.closed) return;
    if (this.composing) {
      this.blurPending = true;
      return;
    }
    this.commit();
  };

  updateScreenMatrix(matrix: Matrix2D) {
    if (this.closed) return;
    this.textarea.style.transform = `matrix(${matrix.a},${matrix.b},${matrix.c},${matrix.d},${matrix.e},${matrix.f})`;
  }

  commit() {
    if (this.closed) return;
    if (this.composing) {
      this.blurPending = true;
      return;
    }
    this.blurPending = false;
    const after = this.textarea.value;
    if (new TextEncoder().encode(after).byteLength > 100_000) {
      this.textarea.setCustomValidity('テキストが長すぎます');
      this.textarea.reportValidity();
      this.textarea.focus({ preventScroll: true });
      return;
    }
    this.textarea.setCustomValidity('');
    this.closed = true;
    this.cleanup();
    if (after !== this.shape.text) this.port.commit(this.shape.id, this.shape.text, after);
    this.port.focusCanvas();
  }
  cancel() {
    if (this.closed) return;
    this.closed = true;
    this.cleanup();
    this.port.focusCanvas();
  }
  private cleanup() {
    this.textarea.removeEventListener('compositionstart', this.onCompositionStart);
    this.textarea.removeEventListener('compositionend', this.onCompositionEnd);
    this.textarea.removeEventListener('keydown', this.onKeyDown);
    this.textarea.removeEventListener('blur', this.onBlur);
    this.textarea.remove();
  }
}

Canvas Preview は同じ font/lineHeight とキャッシュ済み lines を使い、baseline を明示します。Canvas の direction は ltr/rtl/inherit しか受けないため、direction:auto を直接代入できません。authoritative な DOM measurement/layout が段落の resolvedDirection を先に解決し、Preview は同じ結果を使います。fontSize も独立した検証済み数値でなければなりません。"600 16px Canvas Sans" に parseFloat() を使うと、font size 16 ではなく font weight 600 を読み取ってしまいます。

export function renderTextPreview(
  context: CanvasRenderingContext2D,
  shape: TextShape,
  lines: readonly TextLine[],
  resolvedDirection: 'ltr' | 'rtl',
) {
  context.save();
  try {
    context.font = shape.font;
    context.fillStyle = shape.color;
    context.textBaseline = 'alphabetic';
    context.direction = resolvedDirection;
    const fontSize = shape.fontSize;
    if (!(fontSize > 0) || !Number.isFinite(fontSize)) throw new Error('INVALID_FONT_SIZE');
    lines.forEach((line, index) =>
      context.fillText(line.text, 0, fontSize + index * fontSize * shape.lineHeight),
    );
  } finally {
    context.restore();
  }
}

単体テストは Grapheme が ZWJ sequence の途中で切れないことを証明します。IME の最終証拠は実ブラウザテストで取得します。

import { describe, expect, it } from 'vitest';
import { wrapGraphemes } from '../TextLayout';

describe('text layout', () => {
  it('家族 emoji を一つの折り返し単位として扱う', () => {
    const family = '👨‍👩‍👧‍👦';
    const lines = wrapGraphemes(
      `A${family}B`,
      2,
      (text) =>
        Array.from(new Intl.Segmenter('und', { granularity: 'grapheme' }).segment(text)).length,
    );
    expect(lines.map((line) => line.text)).toEqual([`A${family}`, 'B']);
    expect(lines.flatMap((line) => [...line.text]).join('')).toBe(`A${family}B`);
  });

  it('空行を含む hard line break を保持する', () => {
    expect(wrapGraphemes('甲\n\n乙', 10, (text) => text.length).map((line) => line.text)).toEqual([
      '甲',
      '',
      '乙',
    ]);
  });
});

pnpm vitest run src/engine/text を実行し、CJK、Emoji、空行、幅境界、font cache のテストが通ることを確認します。pnpm playwright test tests/browser/text-ime.spec.ts --project=chromium を実行し、composition 中は History revision が変わらず、終了して Commit したあと 1 だけ増えることを確認します。続けて Safari/WebKit と Firefox で Clipboard、Selection、RTL を実行します。モバイルでは仮想キーボードによって Overlay が Viewport 外へ永久に押し出されないことを実機確認します。

写真のゲームへ戻りましょう。Renderer は新しい写真を表示し、DOM control は一時的に重ねる元の紙、FontManager は活字の箱、Composition はまだ選ばれていない候補カードです。子どもが「完了」と言ったときだけ新しい写真を撮り、台帳を書き換えます。Cancel なら元の紙を取り除くだけです。

わざと壊してみる

注入する障害症状証拠修正回帰テスト復旧
中国語ピンイン composition候補が重複し、キーごとに Undocomposition/input/History tracenative control が中間値を保持し、終了時に一度 CommitChromium IME event sequenceOverlay を Cancel、Document 不変
日本語変換Enter で早すぎる編集終了isComposing=true の keydowncomposition 中は commit shortcut を無視WebKit/Chromium fixtureSelection を復元
Family EmojiBackspace が壊れた記号を残す、折り返しが分断grapheme segmentsIntl.Segmenter または DOM layoutZWJ/肌色/国旗 fixturelayout cache 再構築
アラビア語 RTL文字が反転し Caret がずれるlogical value と visual screenshot文字列を反転せず dir/direction を設定RTL 混在数字テスト論理テキストを復元
Font 読み込み遅延テキストが跳ね Connector anchor が誤るfonts.status と前後 metricsloadingdone で invalidatedelayed-font routelayout cache を消す
fallback が対象 font に置換行数が変わるのに Bounds 不変line count/metrics revision派生 height と anchors を再計算font-swap test一度の deterministic redraw
400% Zoom + RotationOverlay と文字が離れるCSS/Shape screen matrix 比較完全 matrix と transform-origin0/37/90° の複数 Zoom screenshotOverlay 再構築
Rich HTML を PasteScript/私有 style がモデルへ入るclipboard MIME/schema ログplain-text 方針または sanitizer + typed rich model悪意ある HTML fixturePaste を原子的に拒否
Mobile Virtual Keyboard入力欄が隠れフォーカス喪失VisualViewport/focus trace可視領域 scroll と focus 方針iOS/Android 実機編集 session を保って再配置

証拠をもって合格する

証明すること自動化された証拠必須の手動・実機証拠
keydown 文字組み立てが IME を壊さないevent replay と一つの Command中国語・日本語の実 IME
Grapheme を分断しないSegmenter fixtureシステム Emoji キーボードで削除
RTL/Bidi が論理値を保つvalue snapshotScreen Reader と Caret の進行方向
Font 変更から復旧遅延読み込みと cache invalidationOS ごとの fallback
Overlay がそろう複数 Zoom/rotation screenshot diff400% Zoom で手動選択
Commit/Cancel 境界revision 1/0 assertionblur、Escape、仮想キーボード
  • FontFace、document.fonts、fallback、loading failure、再測定にプロトコルがある。
  • Text Metrics、Line Breaking/Wrapping、Baseline、Canvas Preview が明示的な layout data を使う。
  • Grapheme、Emoji、Bidi、RTL、IME、Composition、Caret、Selection を正しい層が担当する。
  • textarea/contenteditable の選択に ADR があり、Rich Text、Spellcheck、Paste に安全方針がある。
  • DOM Measurement と Overlay が Zoom、Rotation、Parent Transform 後もそろう。
  • Commit は一つの Command、Cancel は Document 書き込みゼロ。
  • 通常テキストを keydown から手作業で組み立てず、IME、Selection、Clipboard、Composition、Screen Reader を壊さない。

5歳児に説明する

「DOM」「Canvas」「入力メソッド」「書記素」「双方向テキスト」という言葉を使わずに答えてください。

  1. 文字の写真は読めるのに、なぜ2文字の間へ小さな縦線を置けないのでしょうか。
  2. 子どもがまだ候補文字を選んでいる途中に、なぜキーを押すたび共有台帳を変えてはいけないのでしょうか。
  3. 家族の小さな絵は部品が多いのに、なぜ一つのまとまりとして扱うのでしょうか。
  4. 新しい問題です。活字の箱が1分遅れて届き、1行だった文字が2行になりました。何を測り直しますか。
参考解答を見る 写真が覚えているのは色だけで、挿入できる場所や選択領域ではありません。編集にはブラウザが理解する書き込み可能な紙が必要です。まだ選んでいない候補は下書きなので、子どもが確定するか編集を終えたときに一度だけ台帳へ記録します。家族の絵は多数の記号が接着されて目に一つの絵として見えるため、接着部分で切ってはいけません。新しい活字は幅が違うので、各行の文字、紙片の高さ、接続ひもの位置、表示写真を再計算しますが、文字の内容自体は変えません。