image: follow-up fixes for LOD (#3934)
couple fixes and improvements for the LOD work. - add `format=auto` for Cloudflare to send back more modern image formats - fix the broken asset logic that regressed (should not have looked at `url`) - fix stray parenthesis, omg - rm the `useValueDebounced` function in lieu of just debouncing the resolver. the problem was that the initial load in a multiplayer room has a zoom of 1 but then the real zoom comes in (via the url) and so we would double load all images 😬. this switches the debouncing to the resolving stage, not making it tied to the zoom specifically. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know
This commit is contained in:
parent
fba82ed924
commit
73c2b1088a
11 changed files with 30 additions and 69 deletions
|
@ -128,7 +128,7 @@ describe('resolveAsset', () => {
|
||||||
networkEffectiveType: null,
|
networkEffectiveType: null,
|
||||||
})
|
})
|
||||||
).toBe(
|
).toBe(
|
||||||
'https://localhost:8788/cdn-cgi/image/width=50,dpr=2,fit=scale-down,quality=92/http://example.com/image.jpg'
|
'https://localhost:8788/cdn-cgi/image/format=auto,width=50,dpr=2,fit=scale-down,quality=92/http://example.com/image.jpg'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ describe('resolveAsset', () => {
|
||||||
networkEffectiveType: '3g',
|
networkEffectiveType: '3g',
|
||||||
})
|
})
|
||||||
).toBe(
|
).toBe(
|
||||||
'https://localhost:8788/cdn-cgi/image/width=25,dpr=2,fit=scale-down,quality=92/http://example.com/image.jpg'
|
'https://localhost:8788/cdn-cgi/image/format=auto,width=25,dpr=2,fit=scale-down,quality=92/http://example.com/image.jpg'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -162,7 +162,7 @@ describe('resolveAsset', () => {
|
||||||
networkEffectiveType: '4g',
|
networkEffectiveType: '4g',
|
||||||
})
|
})
|
||||||
).toBe(
|
).toBe(
|
||||||
'https://localhost:8788/cdn-cgi/image/width=400,dpr=1,fit=scale-down,quality=92/https://example.com/image.jpg'
|
'https://localhost:8788/cdn-cgi/image/format=auto,width=400,dpr=1,fit=scale-down,quality=92/https://example.com/image.jpg'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -179,7 +179,7 @@ describe('resolveAsset', () => {
|
||||||
networkEffectiveType: '2g',
|
networkEffectiveType: '2g',
|
||||||
})
|
})
|
||||||
).toBe(
|
).toBe(
|
||||||
'https://localhost:8788/cdn-cgi/image/width=25,dpr=1,fit=scale-down,quality=92/https://example.com/image.jpg'
|
'https://localhost:8788/cdn-cgi/image/format=auto,width=25,dpr=1,fit=scale-down,quality=92/https://example.com/image.jpg'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -196,7 +196,7 @@ describe('resolveAsset', () => {
|
||||||
networkEffectiveType: '4g',
|
networkEffectiveType: '4g',
|
||||||
})
|
})
|
||||||
).toBe(
|
).toBe(
|
||||||
'https://localhost:8788/cdn-cgi/image/width=25,dpr=1,fit=scale-down,quality=92/https://example.com/image.jpg'
|
'https://localhost:8788/cdn-cgi/image/format=auto,width=25,dpr=1,fit=scale-down,quality=92/https://example.com/image.jpg'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -36,5 +36,5 @@ export async function resolveAsset(asset: TLAsset | null | undefined, context: A
|
||||||
// On preview, builds the origin for the asset won't be the right one for the Cloudflare transform.
|
// On preview, builds the origin for the asset won't be the right one for the Cloudflare transform.
|
||||||
const src = asset.props.src.replace(ASSET_UPLOADER_URL, ASSET_BUCKET_ORIGIN)
|
const src = asset.props.src.replace(ASSET_UPLOADER_URL, ASSET_BUCKET_ORIGIN)
|
||||||
|
|
||||||
return `${ASSET_BUCKET_ORIGIN}/cdn-cgi/image/width=${width},dpr=${context.dpr},fit=scale-down,quality=92/${src}`
|
return `${ASSET_BUCKET_ORIGIN}/cdn-cgi/image/format=auto,width=${width},dpr=${context.dpr},fit=scale-down,quality=92/${src}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,6 @@ import { useComputed } from '@tldraw/state';
|
||||||
import { useQuickReactor } from '@tldraw/state';
|
import { useQuickReactor } from '@tldraw/state';
|
||||||
import { useReactor } from '@tldraw/state';
|
import { useReactor } from '@tldraw/state';
|
||||||
import { useValue } from '@tldraw/state';
|
import { useValue } from '@tldraw/state';
|
||||||
import { useValueDebounced } from '@tldraw/state';
|
|
||||||
import { VecModel } from '@tldraw/tlschema';
|
import { VecModel } from '@tldraw/tlschema';
|
||||||
import { whyAmIRunning } from '@tldraw/state';
|
import { whyAmIRunning } from '@tldraw/state';
|
||||||
|
|
||||||
|
@ -3480,8 +3479,6 @@ export function useTransform(ref: React.RefObject<HTMLElement | SVGElement>, x?:
|
||||||
|
|
||||||
export { useValue }
|
export { useValue }
|
||||||
|
|
||||||
export { useValueDebounced }
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export class Vec {
|
export class Vec {
|
||||||
constructor(x?: number, y?: number, z?: number);
|
constructor(x?: number, y?: number, z?: number);
|
||||||
|
|
|
@ -28,7 +28,6 @@ export {
|
||||||
useQuickReactor,
|
useQuickReactor,
|
||||||
useReactor,
|
useReactor,
|
||||||
useValue,
|
useValue,
|
||||||
useValueDebounced,
|
|
||||||
whyAmIRunning,
|
whyAmIRunning,
|
||||||
type Atom,
|
type Atom,
|
||||||
type Signal,
|
type Signal,
|
||||||
|
|
|
@ -207,9 +207,6 @@ export function useValue<Value>(value: Signal<Value>): Value;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function useValue<Value>(name: string, fn: () => Value, deps: unknown[]): Value;
|
export function useValue<Value>(name: string, fn: () => Value, deps: unknown[]): Value;
|
||||||
|
|
||||||
// @public
|
|
||||||
export function useValueDebounced<Value>(name: string, fn: () => Value, deps: unknown[], ms: number): Value;
|
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function whyAmIRunning(): void;
|
export function whyAmIRunning(): void;
|
||||||
|
|
||||||
|
|
|
@ -5,4 +5,3 @@ export { useQuickReactor } from './useQuickReactor'
|
||||||
export { useReactor } from './useReactor'
|
export { useReactor } from './useReactor'
|
||||||
export { useStateTracking } from './useStateTracking'
|
export { useStateTracking } from './useStateTracking'
|
||||||
export { useValue } from './useValue'
|
export { useValue } from './useValue'
|
||||||
export { useValueDebounced } from './useValueDebounced'
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
/* eslint-disable prefer-rest-params */
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useValue } from './useValue'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the value from a signal and subscribes to it, debouncing the value by the given number of milliseconds.
|
|
||||||
*
|
|
||||||
* @see [[useValue]] for more information.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export function useValueDebounced<Value>(
|
|
||||||
name: string,
|
|
||||||
fn: () => Value,
|
|
||||||
deps: unknown[],
|
|
||||||
ms: number
|
|
||||||
): Value
|
|
||||||
/** @public */
|
|
||||||
export function useValueDebounced<Value>(): Value {
|
|
||||||
const args = [...arguments].slice(0, -1) as Parameters<typeof useValue>
|
|
||||||
const ms = arguments[arguments.length - 1] as number
|
|
||||||
const value = useValue(...args) as Value
|
|
||||||
const [debouncedValue, setDebouncedValue] = useState<Value>(value)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setDebouncedValue(value)
|
|
||||||
}, ms)
|
|
||||||
return () => clearTimeout(timer)
|
|
||||||
}, [value, ms])
|
|
||||||
|
|
||||||
return debouncedValue
|
|
||||||
}
|
|
|
@ -963,7 +963,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
canCrop: () => boolean;
|
canCrop: () => boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLImageShape): JSX_2.Element;
|
component(shape: TLImageShape): JSX_2.Element | null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getDefaultProps(): TLImageShape['props'];
|
getDefaultProps(): TLImageShape['props'];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
|
|
@ -73,7 +73,6 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
// cause visual flickering when the image is loaded.
|
// cause visual flickering when the image is loaded.
|
||||||
if (url && !this.isAnimated(shape)) {
|
if (url && !this.isAnimated(shape)) {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
if (!url) return
|
|
||||||
|
|
||||||
const image = Image()
|
const image = Image()
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
|
@ -91,7 +90,6 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (url && this.isAnimated(shape)) {
|
if (url && this.isAnimated(shape)) {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
if (!url) return
|
|
||||||
|
|
||||||
const image = Image()
|
const image = Image()
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
|
@ -129,7 +127,8 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
|
|
||||||
const containerStyle = getCroppedContainerStyle(shape)
|
const containerStyle = getCroppedContainerStyle(shape)
|
||||||
|
|
||||||
if (!url) {
|
// This is specifically `asset?.props.src` and not `url` because we're looking for broken assets.
|
||||||
|
if (!asset?.props.src) {
|
||||||
return (
|
return (
|
||||||
<HTMLContainer
|
<HTMLContainer
|
||||||
id={shape.id}
|
id={shape.id}
|
||||||
|
@ -145,7 +144,6 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
<div className="tl-image-container" style={containerStyle}>
|
<div className="tl-image-container" style={containerStyle}>
|
||||||
{asset ? null : <BrokenAssetIcon />}
|
{asset ? null : <BrokenAssetIcon />}
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
{'url' in shape.props && shape.props.url && (
|
{'url' in shape.props && shape.props.url && (
|
||||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.getZoomLevel()} />
|
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.getZoomLevel()} />
|
||||||
)}
|
)}
|
||||||
|
@ -153,6 +151,8 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!loadedSrc) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showCropPreview && (
|
{showCropPreview && (
|
||||||
|
@ -189,7 +189,6 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
<div className="tl-image__tg">GIF</div>
|
<div className="tl-image__tg">GIF</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
{shape.props.url && (
|
{shape.props.url && (
|
||||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.getZoomLevel()} />
|
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.getZoomLevel()} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TLAssetId, useEditor, useValueDebounced } from '@tldraw/editor'
|
import { TLAssetId, useEditor, useValue } from '@tldraw/editor'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -10,22 +10,25 @@ export function useAsset(assetId: TLAssetId | null, width: number) {
|
||||||
const shapeScale = asset && 'w' in asset.props ? width / asset.props.w : 1
|
const shapeScale = asset && 'w' in asset.props ? width / asset.props.w : 1
|
||||||
// We debounce the zoom level to reduce the number of times we fetch a new image and,
|
// We debounce the zoom level to reduce the number of times we fetch a new image and,
|
||||||
// more importantly, to not cause zooming in and out to feel janky.
|
// more importantly, to not cause zooming in and out to feel janky.
|
||||||
const debouncedScreenScale = useValueDebounced(
|
const screenScale = useValue('zoom level', () => editor.getZoomLevel() * shapeScale, [
|
||||||
'zoom level',
|
editor,
|
||||||
() => editor.getZoomLevel() * shapeScale,
|
shapeScale,
|
||||||
[editor, shapeScale],
|
])
|
||||||
500
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function resolve() {
|
let isCancelled = false
|
||||||
|
const timer = editor.timers.setTimeout(async () => {
|
||||||
const resolvedUrl = await editor.resolveAssetUrl(assetId, {
|
const resolvedUrl = await editor.resolveAssetUrl(assetId, {
|
||||||
screenScale: debouncedScreenScale,
|
screenScale,
|
||||||
})
|
})
|
||||||
setUrl(resolvedUrl)
|
if (!isCancelled) setUrl(resolvedUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
isCancelled = true
|
||||||
}
|
}
|
||||||
resolve()
|
}, [assetId, screenScale, editor])
|
||||||
}, [assetId, debouncedScreenScale, editor])
|
|
||||||
|
|
||||||
return { asset, url }
|
return { asset, url }
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,7 +158,9 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
|
||||||
>
|
>
|
||||||
<div className="tl-counter-scaled">
|
<div className="tl-counter-scaled">
|
||||||
<div className="tl-video-container">
|
<div className="tl-video-container">
|
||||||
{url ? (
|
{!asset?.props.src ? (
|
||||||
|
<BrokenAssetIcon />
|
||||||
|
) : url ? (
|
||||||
<video
|
<video
|
||||||
ref={rVideo}
|
ref={rVideo}
|
||||||
style={isEditing ? { pointerEvents: 'all' } : undefined}
|
style={isEditing ? { pointerEvents: 'all' } : undefined}
|
||||||
|
@ -181,9 +183,7 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
|
||||||
>
|
>
|
||||||
<source src={url} />
|
<source src={url} />
|
||||||
</video>
|
</video>
|
||||||
) : (
|
) : null}
|
||||||
<BrokenAssetIcon />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HTMLContainer>
|
</HTMLContainer>
|
||||||
|
|
Loading…
Reference in a new issue