fix pattern fill lods (#3801)

Camera options broke pattern lods. This PR adapts the more flexible take
of pattern LODs i did for my version of camera controls to the new
version.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `bugfix` — Bug fix
This commit is contained in:
alex 2024-05-22 14:24:14 +01:00 committed by GitHub
parent db32f0e8e6
commit abc8521a71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 83 additions and 66 deletions

View file

@ -9,7 +9,7 @@ import {
useValue, useValue,
} from '@tldraw/editor' } from '@tldraw/editor'
import React from 'react' import React from 'react'
import { HASH_PATTERN_ZOOM_NAMES } from './defaultStyleDefs' import { getHashPatternZoomName } from './defaultStyleDefs'
export interface ShapeFillProps { export interface ShapeFillProps {
d: string d: string
@ -45,7 +45,6 @@ export function PatternFill({ d, color, theme }: ShapeFillProps) {
const svgExport = useSvgExportContext() const svgExport = useSvgExportContext()
const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor]) const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor])
const intZoom = Math.ceil(zoomLevel)
const teenyTiny = editor.getZoomLevel() <= 0.18 const teenyTiny = editor.getZoomLevel() <= 0.18
return ( return (
@ -54,10 +53,10 @@ export function PatternFill({ d, color, theme }: ShapeFillProps) {
<path <path
fill={ fill={
svgExport svgExport
? `url(#${HASH_PATTERN_ZOOM_NAMES[`1_${theme.id}`]})` ? `url(#${getHashPatternZoomName(1, theme.id)})`
: teenyTiny : teenyTiny
? theme[color].semi ? theme[color].semi
: `url(#${HASH_PATTERN_ZOOM_NAMES[`${intZoom}_${theme.id}`]})` : `url(#${getHashPatternZoomName(zoomLevel, theme.id)})`
} }
d={d} d={d}
/> />

View file

@ -4,25 +4,18 @@ import {
DefaultFontStyle, DefaultFontStyle,
FileHelpers, FileHelpers,
SvgExportDef, SvgExportDef,
TLDefaultColorTheme,
TLDefaultFillStyle, TLDefaultFillStyle,
TLDefaultFontStyle, TLDefaultFontStyle,
TLShapeUtilCanvasSvgDef, TLShapeUtilCanvasSvgDef,
debugFlags, debugFlags,
last,
useEditor, useEditor,
useValue,
} from '@tldraw/editor' } from '@tldraw/editor'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useDefaultColorTheme } from './ShapeFill' import { useDefaultColorTheme } from './ShapeFill'
/** @internal */
export const HASH_PATTERN_ZOOM_NAMES: Record<string, string> = {}
const HASH_PATTERN_COUNT = 6
for (let zoom = 1; zoom <= HASH_PATTERN_COUNT; zoom++) {
HASH_PATTERN_ZOOM_NAMES[zoom + '_dark'] = `hash_pattern_zoom_${zoom}_dark`
HASH_PATTERN_ZOOM_NAMES[zoom + '_light'] = `hash_pattern_zoom_${zoom}_light`
}
/** @public */ /** @public */
export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef { export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef {
return { return {
@ -80,7 +73,7 @@ function HashPatternForExport() {
</g> </g>
</mask> </mask>
<pattern <pattern
id={HASH_PATTERN_ZOOM_NAMES[`1_${theme.id}`]} id={getHashPatternZoomName(1, theme.id)}
width="8" width="8"
height="8" height="8"
patternUnits="userSpaceOnUse" patternUnits="userSpaceOnUse"
@ -154,39 +147,64 @@ const canvasBlob = (size: [number, number], fn: (ctx: CanvasRenderingContext2D)
fn(ctx) fn(ctx)
return canvas.toDataURL() return canvas.toDataURL()
} }
type PatternDef = { zoom: number; url: string; darkMode: boolean } type PatternDef = { zoom: number; url: string; theme: 'light' | 'dark' }
const getDefaultPatterns = () => { let defaultPixels: { white: string; black: string } | null = null
const defaultPatterns: PatternDef[] = [] function getDefaultPixels() {
for (let i = 1; i <= HASH_PATTERN_COUNT; i++) { if (!defaultPixels) {
const whitePixelBlob = canvasBlob([1, 1], (ctx) => { defaultPixels = {
ctx.fillStyle = DefaultColorThemePalette.lightMode.black.semi white: canvasBlob([1, 1], (ctx) => {
ctx.fillRect(0, 0, 1, 1) ctx.fillStyle = '#f8f9fa'
}) ctx.fillRect(0, 0, 1, 1)
const blackPixelBlob = canvasBlob([1, 1], (ctx) => { }),
ctx.fillStyle = DefaultColorThemePalette.darkMode.black.semi black: canvasBlob([1, 1], (ctx) => {
ctx.fillRect(0, 0, 1, 1) ctx.fillStyle = '#212529'
}) ctx.fillRect(0, 0, 1, 1)
defaultPatterns.push({ }),
zoom: i, }
url: whitePixelBlob,
darkMode: false,
})
defaultPatterns.push({
zoom: i,
url: blackPixelBlob,
darkMode: true,
})
} }
return defaultPatterns return defaultPixels
}
function getPatternLodForZoomLevel(zoom: number) {
return Math.ceil(Math.log2(Math.max(1, zoom)))
}
export function getHashPatternZoomName(zoom: number, theme: TLDefaultColorTheme['id']) {
const lod = getPatternLodForZoomLevel(zoom)
return `tldraw_hash_pattern_${theme}_${lod}`
}
function getPatternLodsToGenerate(maxZoom: number) {
const levels = []
const minLod = 0
const maxLod = getPatternLodForZoomLevel(maxZoom)
for (let i = minLod; i <= maxLod; i++) {
levels.push(Math.pow(2, i))
}
return levels
}
function getDefaultPatterns(maxZoom: number): PatternDef[] {
const defaultPixels = getDefaultPixels()
return getPatternLodsToGenerate(maxZoom).flatMap((zoom) => [
{ zoom, url: defaultPixels.white, theme: 'light' },
{ zoom, url: defaultPixels.black, theme: 'dark' },
])
} }
function usePattern() { function usePattern() {
const editor = useEditor() const editor = useEditor()
const dpr = editor.getInstanceState().devicePixelRatio const dpr = useValue('devicePixelRatio', () => editor.getInstanceState().devicePixelRatio, [
editor,
])
const maxZoom = useValue('maxZoom', () => Math.ceil(last(editor.getCameraOptions().zoomSteps)!), [
editor,
])
const [isReady, setIsReady] = useState(false) const [isReady, setIsReady] = useState(false)
const defaultPatterns = useMemo(() => getDefaultPatterns(), []) const [backgroundUrls, setBackgroundUrls] = useState<PatternDef[]>(() =>
const [backgroundUrls, setBackgroundUrls] = useState<PatternDef[]>(defaultPatterns) getDefaultPatterns(maxZoom)
)
useEffect(() => { useEffect(() => {
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
@ -194,46 +212,46 @@ function usePattern() {
return return
} }
const promises: Promise<{ zoom: number; url: string; darkMode: boolean }>[] = [] const promise = Promise.all(
getPatternLodsToGenerate(maxZoom).flatMap<Promise<PatternDef>>((zoom) => [
for (let i = 1; i <= HASH_PATTERN_COUNT; i++) { generateImage(dpr, zoom, false).then((blob) => ({
promises.push( zoom,
generateImage(dpr, i, false).then((blob) => ({ theme: 'light',
zoom: i,
url: URL.createObjectURL(blob), url: URL.createObjectURL(blob),
darkMode: false, })),
})) generateImage(dpr, zoom, true).then((blob) => ({
) zoom,
promises.push( theme: 'dark',
generateImage(dpr, i, true).then((blob) => ({
zoom: i,
url: URL.createObjectURL(blob), url: URL.createObjectURL(blob),
darkMode: true, })),
})) ])
) )
}
let isCancelled = false let isCancelled = false
Promise.all(promises).then((urls) => { promise.then((urls) => {
if (isCancelled) return if (isCancelled) return
setBackgroundUrls(urls) setBackgroundUrls(urls)
setIsReady(true) setIsReady(true)
}) })
return () => { return () => {
isCancelled = true isCancelled = true
setIsReady(false) setIsReady(false)
promise.then((patterns) => {
for (const { url } of patterns) {
URL.revokeObjectURL(url)
}
})
} }
}, [dpr]) }, [dpr, maxZoom])
const defs = ( const defs = (
<> <>
{backgroundUrls.map((item) => { {backgroundUrls.map((item) => {
const key = item.zoom + (item.darkMode ? '_dark' : '_light') const id = getHashPatternZoomName(item.zoom, item.theme)
return ( return (
<pattern <pattern
key={key} key={id}
id={HASH_PATTERN_ZOOM_NAMES[key]} id={id}
width={TILE_PATTERN_SIZE} width={TILE_PATTERN_SIZE}
height={TILE_PATTERN_SIZE} height={TILE_PATTERN_SIZE}
patternUnits="userSpaceOnUse" patternUnits="userSpaceOnUse"

View file

@ -49,7 +49,7 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
</mask> </mask>
<pattern <pattern
height="8" height="8"
id="hash_pattern_zoom_1_light" id="tldraw_hash_pattern_light_0"
patternUnits="userSpaceOnUse" patternUnits="userSpaceOnUse"
width="8" width="8"
> >
@ -133,7 +133,7 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
/> />
<path <path
d="M0, 0L100, 0,100, 100,0, 100Z" d="M0, 0L100, 0,100, 100,0, 100Z"
fill="url(#hash_pattern_zoom_1_light)" fill="url(#tldraw_hash_pattern_light_0)"
/> />
<path <path
d="M0, 0L100, 0,100, 100,0, 100Z" d="M0, 0L100, 0,100, 100,0, 100Z"