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";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import {
|
||||
deepCopyElement,
|
||||
newFreeDrawElement,
|
||||
refreshTextDimensions,
|
||||
} from "../element/newElement";
|
||||
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isBindingElement,
|
||||
@ -272,6 +268,7 @@ import {
|
||||
} from "../element/Hyperlink";
|
||||
import { shouldShowBoundingBox } from "../element/transformHandles";
|
||||
import { atom } from "jotai";
|
||||
import { Fonts } from "../scene/Fonts";
|
||||
|
||||
export const isMenuOpenAtom = atom(false);
|
||||
export const isDropdownOpenAtom = atom(false);
|
||||
@ -354,6 +351,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
};
|
||||
|
||||
public scene: Scene;
|
||||
private fonts: Fonts;
|
||||
private resizeObserver: ResizeObserver | undefined;
|
||||
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
||||
public library: AppClassProperties["library"];
|
||||
@ -445,6 +443,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
};
|
||||
|
||||
this.scene = new Scene();
|
||||
this.fonts = new Fonts({
|
||||
scene: this.scene,
|
||||
onSceneUpdated: this.onSceneUpdated,
|
||||
});
|
||||
this.history = new History();
|
||||
this.actionManager = new ActionManager(
|
||||
this.syncActionResult,
|
||||
@ -730,23 +732,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
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 = () => {
|
||||
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.syncActionResult({
|
||||
...scene,
|
||||
@ -1059,7 +1050,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.updateCurrentCursorPosition,
|
||||
);
|
||||
// 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
|
||||
document.addEventListener(
|
||||
EVENT.GESTURE_START,
|
||||
|
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@ -2,6 +2,8 @@
|
||||
interface Document {
|
||||
fonts?: {
|
||||
ready?: Promise<void>;
|
||||
check?: (font: string, text?: string) => boolean;
|
||||
load?: (font: string, text?: string) => Promise<FontFace[]>;
|
||||
addEventListener?(
|
||||
type: "loading" | "loadingdone" | "loadingerror",
|
||||
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