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

View file

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

View file

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