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:
parent
db32f0e8e6
commit
abc8521a71
3 changed files with 83 additions and 66 deletions
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
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)
|
||||
})
|
||||
const blackPixelBlob = canvasBlob([1, 1], (ctx) => {
|
||||
ctx.fillStyle = DefaultColorThemePalette.darkMode.black.semi
|
||||
}),
|
||||
black: canvasBlob([1, 1], (ctx) => {
|
||||
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() {
|
||||
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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue