fix: fonts not rendered on init if loadingdone
not fired (#5923)
* fix: fonts not rendered on init if `loadingdone` not fired * remove unnecessary check
This commit is contained in:
parent
52c96a6870
commit
1f117995d9
@ -123,11 +123,7 @@ import {
|
|||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
import {
|
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
|
||||||
deepCopyElement,
|
|
||||||
newFreeDrawElement,
|
|
||||||
refreshTextDimensions,
|
|
||||||
} from "../element/newElement";
|
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
@ -272,6 +268,7 @@ import {
|
|||||||
} from "../element/Hyperlink";
|
} from "../element/Hyperlink";
|
||||||
import { shouldShowBoundingBox } from "../element/transformHandles";
|
import { shouldShowBoundingBox } from "../element/transformHandles";
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
|
import { Fonts } from "../scene/Fonts";
|
||||||
|
|
||||||
export const isMenuOpenAtom = atom(false);
|
export const isMenuOpenAtom = atom(false);
|
||||||
export const isDropdownOpenAtom = atom(false);
|
export const isDropdownOpenAtom = atom(false);
|
||||||
@ -354,6 +351,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public scene: Scene;
|
public scene: Scene;
|
||||||
|
private fonts: Fonts;
|
||||||
private resizeObserver: ResizeObserver | undefined;
|
private resizeObserver: ResizeObserver | undefined;
|
||||||
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
||||||
public library: AppClassProperties["library"];
|
public library: AppClassProperties["library"];
|
||||||
@ -445,6 +443,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.scene = new Scene();
|
this.scene = new Scene();
|
||||||
|
this.fonts = new Fonts({
|
||||||
|
scene: this.scene,
|
||||||
|
onSceneUpdated: this.onSceneUpdated,
|
||||||
|
});
|
||||||
this.history = new History();
|
this.history = new History();
|
||||||
this.actionManager = new ActionManager(
|
this.actionManager = new ActionManager(
|
||||||
this.syncActionResult,
|
this.syncActionResult,
|
||||||
@ -730,23 +732,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onFontLoaded = () => {
|
|
||||||
let didUpdate = false;
|
|
||||||
this.scene.mapElements((element) => {
|
|
||||||
if (isTextElement(element)) {
|
|
||||||
invalidateShapeForElement(element);
|
|
||||||
didUpdate = true;
|
|
||||||
return newElementWith(element, {
|
|
||||||
...refreshTextDimensions(element),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
});
|
|
||||||
if (didUpdate) {
|
|
||||||
this.onSceneUpdated();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private resetHistory = () => {
|
private resetHistory = () => {
|
||||||
this.history.clear();
|
this.history.clear();
|
||||||
};
|
};
|
||||||
@ -846,6 +831,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FontFaceSet loadingdone event we listen on may not always fire
|
||||||
|
// (looking at you Safari), so on init we manually load fonts for current
|
||||||
|
// text elements on canvas, and rerender them once done. This also
|
||||||
|
// seems faster even in browsers that do fire the loadingdone event.
|
||||||
|
this.fonts.loadFontsForElements(scene.elements);
|
||||||
|
|
||||||
this.resetHistory();
|
this.resetHistory();
|
||||||
this.syncActionResult({
|
this.syncActionResult({
|
||||||
...scene,
|
...scene,
|
||||||
@ -1059,7 +1050,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.updateCurrentCursorPosition,
|
this.updateCurrentCursorPosition,
|
||||||
);
|
);
|
||||||
// rerender text elements on font load to fix #637 && #1553
|
// rerender text elements on font load to fix #637 && #1553
|
||||||
document.fonts?.addEventListener?.("loadingdone", this.onFontLoaded);
|
document.fonts?.addEventListener?.("loadingdone", (event) => {
|
||||||
|
const loadedFontFaces = (event as FontFaceSetLoadEvent).fontfaces;
|
||||||
|
this.fonts.onFontsLoaded(loadedFontFaces);
|
||||||
|
});
|
||||||
|
|
||||||
// Safari-only desktop pinch zoom
|
// Safari-only desktop pinch zoom
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
EVENT.GESTURE_START,
|
EVENT.GESTURE_START,
|
||||||
|
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@ -2,6 +2,8 @@
|
|||||||
interface Document {
|
interface Document {
|
||||||
fonts?: {
|
fonts?: {
|
||||||
ready?: Promise<void>;
|
ready?: Promise<void>;
|
||||||
|
check?: (font: string, text?: string) => boolean;
|
||||||
|
load?: (font: string, text?: string) => Promise<FontFace[]>;
|
||||||
addEventListener?(
|
addEventListener?(
|
||||||
type: "loading" | "loadingdone" | "loadingerror",
|
type: "loading" | "loadingdone" | "loadingerror",
|
||||||
listener: (this: Document, ev: Event) => any,
|
listener: (this: Document, ev: Event) => any,
|
||||||
|
93
src/scene/Fonts.ts
Normal file
93
src/scene/Fonts.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { isTextElement, refreshTextDimensions } from "../element";
|
||||||
|
import { newElementWith } from "../element/mutateElement";
|
||||||
|
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||||
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
|
import { getFontString } from "../utils";
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
|
export class Fonts {
|
||||||
|
private scene: Scene;
|
||||||
|
private onSceneUpdated: () => void;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
scene,
|
||||||
|
onSceneUpdated,
|
||||||
|
}: {
|
||||||
|
scene: Scene;
|
||||||
|
onSceneUpdated: () => void;
|
||||||
|
}) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.onSceneUpdated = onSceneUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's ok to track fonts across multiple instances only once, so let's use
|
||||||
|
// a static member to reduce memory footprint
|
||||||
|
private static loadedFontFaces = new Set<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if we load a (new) font, it's likely that text elements using it have
|
||||||
|
* already been rendered using a fallback font. Thus, we want invalidate
|
||||||
|
* their shapes and rerender. See #637.
|
||||||
|
*
|
||||||
|
* Invalidates text elements and rerenders scene, provided that at least one
|
||||||
|
* of the supplied fontFaces has not already been processed.
|
||||||
|
*/
|
||||||
|
public onFontsLoaded = (fontFaces: readonly FontFace[]) => {
|
||||||
|
if (
|
||||||
|
// bail if all fonts with have been processed. We're checking just a
|
||||||
|
// subset of the font properties (though it should be enough), so it
|
||||||
|
// can technically bail on a false positive.
|
||||||
|
fontFaces.every((fontFace) => {
|
||||||
|
const sig = `${fontFace.family}-${fontFace.style}-${fontFace.weight}`;
|
||||||
|
if (Fonts.loadedFontFaces.has(sig)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Fonts.loadedFontFaces.add(sig);
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let didUpdate = false;
|
||||||
|
|
||||||
|
this.scene.mapElements((element) => {
|
||||||
|
if (isTextElement(element)) {
|
||||||
|
invalidateShapeForElement(element);
|
||||||
|
didUpdate = true;
|
||||||
|
return newElementWith(element, {
|
||||||
|
...refreshTextDimensions(element),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (didUpdate) {
|
||||||
|
this.onSceneUpdated();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public loadFontsForElements = async (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
const fontFaces = await Promise.all(
|
||||||
|
[
|
||||||
|
...new Set(
|
||||||
|
elements
|
||||||
|
.filter((element) => isTextElement(element))
|
||||||
|
.map((element) => (element as ExcalidrawTextElement).fontFamily),
|
||||||
|
),
|
||||||
|
].map((fontFamily) => {
|
||||||
|
const fontString = getFontString({
|
||||||
|
fontFamily,
|
||||||
|
fontSize: 16,
|
||||||
|
});
|
||||||
|
if (!document.fonts?.check?.(fontString)) {
|
||||||
|
return document.fonts?.load?.(fontString);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.onFontsLoaded(fontFaces.flat().filter(Boolean) as FontFace[]);
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user