Skip to content

Commit

Permalink
feat(TresCanvas): add dpr prop (#768)
Browse files Browse the repository at this point in the history
* feat(is): add is.num and tests

* feat(TresCanvas): add dpr prop

* docs: add dpr playground demo

---------

Co-authored-by: Alvaro Saburido <[email protected]>
  • Loading branch information
andretchen0 and alvarosabu committed Jul 12, 2024
1 parent 9a53e60 commit 8943cc3
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 6 deletions.
62 changes: 62 additions & 0 deletions playground/src/pages/advanced/devicePixelRatio/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script setup lang="ts">
import { shallowRef } from 'vue'
import { TresCanvas } from '@tresjs/core'
import type { WebGLRenderer } from 'three'
const rendererRef = shallowRef<WebGLRenderer | null>(null)
const minDpr = 1
const maxDpr = 3
const currDprRef = shallowRef(-1)
const dpr = shallowRef<number | [number, number]>([minDpr, maxDpr])
const onReady = ({ renderer }) => {
rendererRef.value = renderer.value
}
const isRendererDprClamped = (renderer: WebGLRenderer) => {
const dpr = renderer.getPixelRatio()
currDprRef.value = dpr
return (dpr >= minDpr && dpr <= maxDpr)
}
const intervalId = setInterval(() => {
if (rendererRef.value) {
isRendererDprClamped(rendererRef.value)
}
}, 1000)
onUnmounted(() => clearInterval(intervalId))
</script>

<template>
<TresCanvas :dpr="dpr" @ready="onReady">
<TresPerspectiveCamera />

<TresMesh>
<TresSphereGeometry />
<TresMeshNormalMaterial />
</TresMesh>

<TresGridHelper />
</TresCanvas>

<OverlayInfo>
<h1><code>&lt;TresCanvas :dpr="[min, max]" /&gt;</code></h1>
<h2>Setup</h2>
<p>The TresCanvas <code>:dpr</code> prop is set to [{{ minDpr }}, {{ maxDpr }}]</p>
<p>This clamps the possible range for the renderer's DPR setting. (TresCanvas also accepts a numerical value for <code>:dpr</code>. That is not tested in this page.)</p>
<h2>Try it</h2>
<p>Zooming in and out in the browser <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio">triggers a change to the window's <code>devicePixelRatio</code>.</a> In turn, this should set the renderer's DPR. It should remain within the range specified by the <code>:dpr</code> prop.</p>
<h2>Test</h2>
<p>Renderer DPR: <span>{{ currDprRef }}</span></p>
<p
v-if="(!rendererRef || isRendererDprClamped(rendererRef))"
:style="{ color: 'green' }"
>
✅ DPR is clamped.
</p>
<p v-else :style="{ color: 'red' }">
❌ DPR is not properly clamped.
</p>
</OverlayInfo>
</template>
5 changes: 5 additions & 0 deletions playground/src/router/routes/advanced.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export const advancedRoutes = [
name: 'Material array',
component: () => import('../../pages/advanced/materialArray/index.vue'),
},
{
path: '/advanced/device-pixel-ratio',
name: 'Device Pixel Ratio',
component: () => import('../../pages/advanced/devicePixelRatio/index.vue'),
},
{
path: '/advanced/disposal',
name: 'Disposal',
Expand Down
1 change: 1 addition & 0 deletions src/components/TresCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface TresCanvasProps
outputColorSpace?: ColorSpace
toneMappingExposure?: number
renderMode?: 'always' | 'on-demand' | 'manual'
dpr?: number | [number, number]
// required by useTresContextProvider
camera?: TresCamera
Expand Down
13 changes: 8 additions & 5 deletions src/composables/useRenderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { EmitEventFn, TresColor } from '../../types'
import { normalizeColor } from '../../utils/normalize'

import type { TresContext } from '../useTresContextProvider'
import { get, merge, set } from '../../utils'
import { get, merge, set, setPixelRatio } from '../../utils'

// Solution taken from Thretle that actually support different versions https://github.com/threlte/threlte/blob/5fa541179460f0dadc7dc17ae5e6854d1689379e/packages/core/src/lib/lib/useRenderer.ts
import { revision } from '../../core/revision'
Expand Down Expand Up @@ -92,6 +92,11 @@ export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRen
windowSize?: MaybeRefOrGetter<boolean | string>
preset?: MaybeRefOrGetter<RendererPresetsType>
renderMode?: MaybeRefOrGetter<'always' | 'on-demand' | 'manual'>
/**
* A `number` sets the renderer's device pixel ratio.
* `[number, number]` clamp's the renderer's device pixel ratio.
*/
dpr?: MaybeRefOrGetter<number | [number, number]>
}

export function useRenderer(
Expand Down Expand Up @@ -151,10 +156,6 @@ export function useRenderer(

const { pixelRatio } = useDevicePixelRatio()

watch(pixelRatio, () => {
renderer.value.setPixelRatio(pixelRatio.value)
})

const { logError } = useLogger()

const getThreeRendererDefaults = () => {
Expand Down Expand Up @@ -199,6 +200,8 @@ export function useRenderer(
merge(renderer.value, rendererPresets[rendererPreset])
}

setPixelRatio(renderer.value, pixelRatio.value, toValue(options.dpr))

// Render mode

if (renderMode === 'always') {
Expand Down
129 changes: 129 additions & 0 deletions src/utils/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,132 @@ describe('resolve', () => {
expect(utils.resolve(instance, 'ab-cd-xx-yy-zz').key).toBe('xxYyZz')
})
})

describe('setPixelRatio', () => {
const INITIAL_DPR = 1
let dpr = INITIAL_DPR
const mockRenderer = {
setPixelRatio: (n: number) => { dpr = n },
getPixelRatio: () => dpr,
}
const setPixelRatioSpy = vi.spyOn(mockRenderer, 'setPixelRatio')

beforeEach(() => {
dpr = 1
setPixelRatioSpy.mockClear()
})

describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number)', () => {
it('calls the renderer\'s setPixelRatio method with systemDpr', () => {
expect(setPixelRatioSpy).not.toBeCalled()
utils.setPixelRatio(mockRenderer, 2)
expect(setPixelRatioSpy).toBeCalledWith(2)

utils.setPixelRatio(mockRenderer, 2.1)
expect(setPixelRatioSpy).toBeCalledWith(2.1)

utils.setPixelRatio(mockRenderer, 1.44444)
expect(setPixelRatioSpy).toBeCalledWith(1.44444)
})
it('does not set the renderer\'s pixelRatio if systemDpr === pixelRatio', () => {
utils.setPixelRatio(mockRenderer, 1)
expect(setPixelRatioSpy).not.toBeCalled()

utils.setPixelRatio(mockRenderer, 2)
expect(setPixelRatioSpy).toBeCalledTimes(1)

utils.setPixelRatio(mockRenderer, 2)
expect(setPixelRatioSpy).toBeCalledTimes(1)

utils.setPixelRatio(mockRenderer, 1)
expect(setPixelRatioSpy).toBeCalledTimes(2)

utils.setPixelRatio(mockRenderer, 1)
expect(setPixelRatioSpy).toBeCalledTimes(2)
})
it('does not throw if passed a "renderer" without a `setPixelRatio` method', () => {
const mockSVGRenderer = {}
expect(() => utils.setPixelRatio(mockSVGRenderer, 2)).not.toThrow()
})
it('calls `setPixelRatio` even if passed a "renderer" without a `getPixelRatio` method', () => {
const mockSVGRenderer = { setPixelRatio: () => {} }
const setPixelRatioSpy = vi.spyOn(mockSVGRenderer, 'setPixelRatio')
expect(() => utils.setPixelRatio(mockSVGRenderer, 2)).not.toThrow()
expect(setPixelRatioSpy).toBeCalledWith(2)
expect(setPixelRatioSpy).toBeCalledTimes(1)

utils.setPixelRatio(mockSVGRenderer, 1.99)
expect(setPixelRatioSpy).toBeCalledWith(1.99)
expect(setPixelRatioSpy).toBeCalledTimes(2)

utils.setPixelRatio(mockSVGRenderer, 2.1)
expect(setPixelRatioSpy).toBeCalledWith(2.1)
expect(setPixelRatioSpy).toBeCalledTimes(3)
})
})

describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number, userDpr: number)', () => {
it('calls the renderer\'s setPixelRatio method with userDpr', () => {
expect(setPixelRatioSpy).not.toBeCalled()
utils.setPixelRatio(mockRenderer, 2, 100)
expect(setPixelRatioSpy).toBeCalledWith(100)
})
it('does not call the renderer\'s setPixelRatio method if current dpr === new dpr', () => {
expect(setPixelRatioSpy).not.toBeCalled()
utils.setPixelRatio(mockRenderer, 2, 1)
expect(setPixelRatioSpy).not.toBeCalledWith()

utils.setPixelRatio(mockRenderer, 3, 1.4)
expect(setPixelRatioSpy).toBeCalledTimes(1)
expect(setPixelRatioSpy).toBeCalledWith(1.4)

utils.setPixelRatio(mockRenderer, 3, 1.4)
expect(setPixelRatioSpy).toBeCalledTimes(1)
expect(setPixelRatioSpy).toBeCalledWith(1.4)

utils.setPixelRatio(mockRenderer, 2, 1.4)
expect(setPixelRatioSpy).toBeCalledTimes(1)
expect(setPixelRatioSpy).toBeCalledWith(1.4)

utils.setPixelRatio(mockRenderer, 42, 0.1)
expect(setPixelRatioSpy).toBeCalledTimes(2)
expect(setPixelRatioSpy).toBeCalledWith(0.1)

utils.setPixelRatio(mockRenderer, 4, 0.1)
expect(setPixelRatioSpy).toBeCalledTimes(2)
expect(setPixelRatioSpy).toBeCalledWith(0.1)
})
})

describe('setPixelRatio(renderer: WebGLRenderer, systemDpr: number, userDpr: [number, number])', () => {
it('clamps systemDpr to userDpr', () => {
utils.setPixelRatio(mockRenderer, 2, [0, 4])
expect(setPixelRatioSpy).toBeCalledTimes(1)
expect(setPixelRatioSpy).toBeCalledWith(2)

utils.setPixelRatio(mockRenderer, 2, [3, 4])
expect(setPixelRatioSpy).toBeCalledTimes(2)
expect(setPixelRatioSpy).toBeCalledWith(3)

utils.setPixelRatio(mockRenderer, 5, [3, 4])
expect(setPixelRatioSpy).toBeCalledTimes(3)
expect(setPixelRatioSpy).toBeCalledWith(4)

utils.setPixelRatio(mockRenderer, 100, [3, 4])
expect(setPixelRatioSpy).toBeCalledTimes(3)
expect(setPixelRatioSpy).toBeCalledWith(4)

utils.setPixelRatio(mockRenderer, 100, [3.5, 4])
expect(setPixelRatioSpy).toBeCalledTimes(3)
expect(setPixelRatioSpy).toBeCalledWith(4)

utils.setPixelRatio(mockRenderer, 100, [3, 6.1])
expect(setPixelRatioSpy).toBeCalledTimes(4)
expect(setPixelRatioSpy).toBeCalledWith(6.1)

utils.setPixelRatio(mockRenderer, 1, [2.99, 6.1])
expect(setPixelRatioSpy).toBeCalledTimes(5)
expect(setPixelRatioSpy).toBeCalledWith(2.99)
})
})
})
22 changes: 21 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Material, Mesh, Object3D, Texture } from 'three'
import { DoubleSide, MeshBasicMaterial, Scene, Vector3 } from 'three'
import { DoubleSide, MathUtils, MeshBasicMaterial, Scene, Vector3 } from 'three'
import type { AttachType, LocalState, TresInstance, TresObject, TresPrimitive } from 'src/types'
import type { nodeOps } from 'src/core/nodeOps'
import { HightlightMesh } from '../devtools/highlight'
Expand Down Expand Up @@ -455,6 +455,26 @@ export function noop(fn: string): any {
fn
}

export function setPixelRatio(renderer: { setPixelRatio?: (dpr: number) => void, getPixelRatio?: () => number }, systemDpr: number, userDpr?: number | [number, number]) {
// NOTE: Optional `setPixelRatio` allows this function to accept
// THREE renderers like SVGRenderer.
if (!is.fun(renderer.setPixelRatio)) { return }

let newDpr = 0

if (is.arr(userDpr) && userDpr.length >= 2) {
const [min, max] = userDpr
newDpr = MathUtils.clamp(systemDpr, min, max)
}
else if (is.num(userDpr)) { newDpr = userDpr }
else { newDpr = systemDpr }

// NOTE: Don't call `setPixelRatio` unless both:
// - the dpr value has changed
// - the renderer has `setPixelRatio`; this check allows us to pass any THREE renderer
if (newDpr !== renderer.getPixelRatio?.()) { renderer.setPixelRatio(newDpr) }
}

export function setPrimitiveObject(
newObject: TresObject,
primitive: TresPrimitive,
Expand Down
35 changes: 35 additions & 0 deletions src/utils/is.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,41 @@ import { BufferGeometry, Fog, MeshBasicMaterial, MeshNormalMaterial, Object3D, P
import * as is from './is'

describe('is', () => {
describe('is.num(a: any)', () => {
describe('true', () => {
it('number', () => {
assert(is.num(0))
assert(is.num(-1))
assert(is.num(Math.PI))
assert(is.num(Number.POSITIVE_INFINITY))
assert(is.num(Number.NEGATIVE_INFINITY))
assert(is.num(42))
assert(is.num(0b1111))
assert(is.num(0o17))
assert(is.num(0xF))
})
})
describe('false', () => {
it('null', () => {
assert(!is.num(null))
})
it('undefined', () => {
assert(!is.num(undefined))
})
it('string', () => {
assert(!is.num(''))
assert(!is.num('1'))
})
it('function', () => {
assert(!is.num(() => {}))
assert(!is.num(() => 1))
})
it('array', () => {
assert(!is.num([]))
assert(!is.num([1]))
})
})
})
describe('is.und(a: any)', () => {
describe('true', () => {
it('undefined', () => {
Expand Down
4 changes: 4 additions & 0 deletions src/utils/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export function arr(u: unknown) {
return Array.isArray(u)
}

export function num(u: unknown): u is number {
return typeof u === 'number'
}

export function str(u: unknown): u is string {
return typeof u === 'string'
}
Expand Down

0 comments on commit 8943cc3

Please sign in to comment.