Perf: Improve text outline performance (#3429)

We use text shadows to create "outlines" around text shapes. These
shadows are rendered on the GPU. In Chrome (and on computers with a
capable GPU) text shadows work pretty well, however on Safari—and in
particular on iOS—they cause massive frame drops.


https://github.com/tldraw/tldraw/assets/23072548/b65cbcaa-6cc3-46f3-b54d-1f9cc07fc499

This PR:
- adds an LOD to text shadows, removing them at < 35% zoom
- removes text shadows entirely on Safari

If we had a "high performance" or "low-end device" mode, then shadows /
text shadows would be the first to go.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features

### Test Plan

1. Use text shapes on iOS.
2. Use text shapes on Safari.
3. Use text shapes on Chrome.

### Release Notes

- Improves performance of text shapes on iOS / Safari.
This commit is contained in:
Steve Ruiz 2024-04-10 11:20:16 +01:00 committed by GitHub
parent 6305e83830
commit 2bbab1a790
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 33 additions and 2 deletions

View file

@ -3,9 +3,10 @@ import { TLHandle, TLShapeId } from '@tldraw/tlschema'
import { dedupe, modulate, objectMapValues } from '@tldraw/utils' import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
import classNames from 'classnames' import classNames from 'classnames'
import { Fragment, JSX, useEffect, useRef, useState } from 'react' import { Fragment, JSX, useEffect, useRef, useState } from 'react'
import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS } from '../../constants' import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS, TEXT_SHADOW_LOD } from '../../constants'
import { useCanvasEvents } from '../../hooks/useCanvasEvents' import { useCanvasEvents } from '../../hooks/useCanvasEvents'
import { useCoarsePointer } from '../../hooks/useCoarsePointer' import { useCoarsePointer } from '../../hooks/useCoarsePointer'
import { useContainer } from '../../hooks/useContainer'
import { useDocumentEvents } from '../../hooks/useDocumentEvents' import { useDocumentEvents } from '../../hooks/useDocumentEvents'
import { useEditor } from '../../hooks/useEditor' import { useEditor } from '../../hooks/useEditor'
import { useEditorComponents } from '../../hooks/useEditorComponents' import { useEditorComponents } from '../../hooks/useEditorComponents'
@ -37,6 +38,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
const rCanvas = useRef<HTMLDivElement>(null) const rCanvas = useRef<HTMLDivElement>(null)
const rHtmlLayer = useRef<HTMLDivElement>(null) const rHtmlLayer = useRef<HTMLDivElement>(null)
const rHtmlLayer2 = useRef<HTMLDivElement>(null) const rHtmlLayer2 = useRef<HTMLDivElement>(null)
const container = useContainer()
useScreenBounds(rCanvas) useScreenBounds(rCanvas)
useDocumentEvents() useDocumentEvents()
@ -45,11 +47,37 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
useGestureEvents(rCanvas) useGestureEvents(rCanvas)
useFixSafariDoubleTapZoomPencilEvents(rCanvas) useFixSafariDoubleTapZoomPencilEvents(rCanvas)
const rMemoizedStuff = useRef({ lodDisableTextOutline: false, allowTextOutline: true })
useQuickReactor( useQuickReactor(
'position layers', 'position layers',
function positionLayersWhenCameraMoves() { function positionLayersWhenCameraMoves() {
const { x, y, z } = editor.getCamera() const { x, y, z } = editor.getCamera()
// This should only run once on first load
if (rMemoizedStuff.current.allowTextOutline && editor.environment.isSafari) {
container.style.setProperty('--tl-text-outline', 'none')
rMemoizedStuff.current.allowTextOutline = false
}
// And this should only run if we're not in Safari;
// If we're below the lod distance for text shadows, turn them off
if (
rMemoizedStuff.current.allowTextOutline &&
z < TEXT_SHADOW_LOD !== rMemoizedStuff.current.lodDisableTextOutline
) {
const lodDisableTextOutline = z < TEXT_SHADOW_LOD
container.style.setProperty(
'--tl-text-outline',
lodDisableTextOutline
? 'none'
: `0 var(--b) 0 var(--color-background), 0 var(--a) 0 var(--color-background),
var(--b) var(--b) 0 var(--color-background), var(--a) var(--b) 0 var(--color-background),
var(--a) var(--a) 0 var(--color-background), var(--b) var(--a) 0 var(--color-background)`
)
rMemoizedStuff.current.lodDisableTextOutline = lodDisableTextOutline
}
// Because the html container has a width/height of 1px, we // Because the html container has a width/height of 1px, we
// need to create a small offset when zoomed to ensure that // need to create a small offset when zoomed to ensure that
// the html container and svg container are lined up exactly. // the html container and svg container are lined up exactly.
@ -62,7 +90,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
setStyleProperty(rHtmlLayer.current, 'transform', transform) setStyleProperty(rHtmlLayer.current, 'transform', transform)
setStyleProperty(rHtmlLayer2.current, 'transform', transform) setStyleProperty(rHtmlLayer2.current, 'transform', transform)
}, },
[editor] [editor, container]
) )
const events = useCanvasEvents() const events = useCanvasEvents()

View file

@ -107,3 +107,6 @@ export const HANDLE_RADIUS = 12
/** @internal */ /** @internal */
export const LONG_PRESS_DURATION = 500 export const LONG_PRESS_DURATION = 500
/** @internal */
export const TEXT_SHADOW_LOD = 0.35