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,
|
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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue