@expofp/renderer 3.44.8 → 3.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Renderer.js +7 -1
- package/dist/controls/hooks.d.ts +2 -1
- package/dist/core/sprites/BatchedSprites.js +2 -34
- package/dist/core/text/MemberTexture.js +49 -8
- package/dist/events/LayoutDriver.d.ts +5 -0
- package/dist/events/LayoutDriver.js +136 -29
- package/dist/events/layout-targets.d.ts +45 -3
- package/dist/events/layout-targets.js +129 -9
- package/dist/events/placement/box.d.ts +59 -0
- package/dist/events/placement/box.js +96 -0
- package/dist/events/placement/grid.d.ts +161 -0
- package/dist/events/placement/grid.js +235 -0
- package/dist/events/placement/index.d.ts +2 -0
- package/dist/events/placement/index.js +1 -0
- package/dist/events/placement/pass.d.ts +70 -0
- package/dist/events/placement/pass.js +617 -0
- package/dist/objects/images/ImageInstance.js +1 -0
- package/dist/objects/images/Images.js +2 -2
- package/dist/objects/images/image-container.d.ts +12 -0
- package/dist/objects/images/image-container.js +20 -4
- package/dist/objects/materials/TextureMaterial.d.ts +11 -8
- package/dist/objects/materials/TextureMaterial.js +24 -17
- package/dist/objects/text/text-container.d.ts +16 -1
- package/dist/objects/text/text-container.js +23 -1
- package/dist/space/layout-measure.d.ts +12 -10
- package/dist/space/layout-measure.js +18 -16
- package/dist/types/defs.d.ts +157 -147
- package/dist/types/events.d.ts +5 -1
- package/dist/types/guards.d.ts +7 -0
- package/dist/types/guards.js +10 -0
- package/dist/utils/math.d.ts +18 -1
- package/dist/utils/math.js +35 -0
- package/package.json +2 -2
|
@@ -10,6 +10,12 @@ import { DIM_SCHEMA } from '../materials/effects/color-dim';
|
|
|
10
10
|
* `uv * zw + xy` maps the quad's unit uv onto the region.
|
|
11
11
|
*/
|
|
12
12
|
export const UV_OFFSET_UNIFORM = 'uvOffset';
|
|
13
|
+
/**
|
|
14
|
+
* The per-instance uniform each atlas mesh reads its opacity from. The container owns the name and
|
|
15
|
+
* hands it to the mesh's `TextureMaterial` as the `opacityUniform` prop, so the material patches its shader
|
|
16
|
+
* without knowing the name itself.
|
|
17
|
+
*/
|
|
18
|
+
export const OPACITY_UNIFORM = 'instanceOpacity';
|
|
13
19
|
/**
|
|
14
20
|
* One atlas's mesh bookkeeping.
|
|
15
21
|
*
|
|
@@ -51,6 +57,15 @@ export class ImageContainer extends SlotContainer {
|
|
|
51
57
|
get facesCamera() {
|
|
52
58
|
return isBatchedSprites(this.mesh);
|
|
53
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Writes a def's opacity into its slot, clamped, treating an absent value as fully opaque.
|
|
62
|
+
* @param slot - The instance's slot.
|
|
63
|
+
* @param opacity - The def's opacity, or `undefined` for the default.
|
|
64
|
+
*/
|
|
65
|
+
setOpacityAt(slot, opacity) {
|
|
66
|
+
const value = opacity === undefined ? 1 : Math.min(Math.max(opacity, 0), 1);
|
|
67
|
+
this.mesh.setUniformAt(slot, OPACITY_UNIFORM, value);
|
|
68
|
+
}
|
|
54
69
|
addInstance(packed) {
|
|
55
70
|
// `addInstances` grows the buffers itself if the atlas ever overflows its sized capacity, so
|
|
56
71
|
// there is nothing to pre-scan; the id is captured from the creation callback.
|
|
@@ -70,14 +85,15 @@ export class ImageContainer extends SlotContainer {
|
|
|
70
85
|
}
|
|
71
86
|
}
|
|
72
87
|
/**
|
|
73
|
-
* The schema the mesh declares (via `addUniforms`) so it can hold a {@link UV_OFFSET_UNIFORM}
|
|
74
|
-
* dim per slot.
|
|
88
|
+
* The schema the mesh declares (via `addUniforms`) so it can hold a {@link UV_OFFSET_UNIFORM}, a
|
|
89
|
+
* {@link OPACITY_UNIFORM} and a dim per slot. The opacity float keeps the schema at two texels: a flat
|
|
90
|
+
* mesh is `vec4` + `float` + `float` = 6 floats, and a sprite mesh's own `anchor` brings it to 8.
|
|
75
91
|
*
|
|
76
|
-
*
|
|
92
|
+
* All in one call although they come from different places: `InstanceUniforms.add` merges and
|
|
77
93
|
* rebuilds the backing texture, so declaring them separately would build it twice per bind.
|
|
78
94
|
*/
|
|
79
95
|
const IMAGE_SCHEMA = {
|
|
80
|
-
vertex: { [UV_OFFSET_UNIFORM]: 'vec4', ...DIM_SCHEMA.vertex },
|
|
96
|
+
vertex: { [UV_OFFSET_UNIFORM]: 'vec4', [OPACITY_UNIFORM]: 'float', ...DIM_SCHEMA.vertex },
|
|
81
97
|
};
|
|
82
98
|
// Scratch for the one uv-region upload a claim performs, filled and read within a single synchronous
|
|
83
99
|
// statement.
|
|
@@ -7,22 +7,25 @@ export interface TextureMaterialProps {
|
|
|
7
7
|
*/
|
|
8
8
|
map: Texture;
|
|
9
9
|
/**
|
|
10
|
-
* The name of a per-instance `vec4` uniform to source the texture region from
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* context, not only an instanced one.
|
|
10
|
+
* The name of a per-instance `vec4` uniform to source the texture region from, for an atlas on an
|
|
11
|
+
* instanced/batched mesh that declares and writes it. The mesh owns the name; the material only
|
|
12
|
+
* patches its shader to sample `map` through it. Omit it to draw the whole `map` unpatched, in any
|
|
13
|
+
* context.
|
|
15
14
|
*/
|
|
16
15
|
uvOffset?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The name of a per-instance `float` uniform holding the instance's opacity, for a mesh that
|
|
18
|
+
* declares and writes one. Omit it to leave the fragment shader unpatched.
|
|
19
|
+
*/
|
|
20
|
+
opacityUniform?: string;
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* The material used to render a texture, optionally through a per-instance uv-offset uniform so an
|
|
20
24
|
* instanced/batched mesh can pack many regions into one atlas.
|
|
21
25
|
*
|
|
22
26
|
* When {@link TextureMaterialProps.uvOffset} is set, the patch overwrites `vMapUv` after
|
|
23
|
-
* `<uv_vertex
|
|
24
|
-
*
|
|
25
|
-
* fetch at the top of `main()`, so the uniform is in scope by the time this line runs.
|
|
27
|
+
* `<uv_vertex>`, replacing the stock chunk's value. Instanced meshes wrap this `onBeforeCompile`
|
|
28
|
+
* and inject their uniform fetch at the top of `main()`, so the uniform is in scope here.
|
|
26
29
|
* @param props - The material's texture and optional uv-offset uniform name.
|
|
27
30
|
* @returns The material element.
|
|
28
31
|
*/
|
|
@@ -8,34 +8,41 @@ import { overlayParams, sharedParams } from './shared';
|
|
|
8
8
|
* instanced/batched mesh can pack many regions into one atlas.
|
|
9
9
|
*
|
|
10
10
|
* When {@link TextureMaterialProps.uvOffset} is set, the patch overwrites `vMapUv` after
|
|
11
|
-
* `<uv_vertex
|
|
12
|
-
*
|
|
13
|
-
* fetch at the top of `main()`, so the uniform is in scope by the time this line runs.
|
|
11
|
+
* `<uv_vertex>`, replacing the stock chunk's value. Instanced meshes wrap this `onBeforeCompile`
|
|
12
|
+
* and inject their uniform fetch at the top of `main()`, so the uniform is in scope here.
|
|
14
13
|
* @param props - The material's texture and optional uv-offset uniform name.
|
|
15
14
|
* @returns The material element.
|
|
16
15
|
*/
|
|
17
16
|
export function TextureMaterial(props) {
|
|
18
|
-
const { map, uvOffset } = props;
|
|
17
|
+
const { map, uvOffset, opacityUniform } = props;
|
|
19
18
|
const applyDim = useDimPatch();
|
|
20
19
|
const overlay = useLayerOverlay();
|
|
21
20
|
function onBeforeCompile(parameters) {
|
|
22
21
|
applyDim(parameters);
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
/*glsl*/ `
|
|
22
|
+
if (uvOffset) {
|
|
23
|
+
parameters.vertexShader = parameters.vertexShader.replace('#include <uv_vertex>',
|
|
24
|
+
/*glsl*/ `
|
|
27
25
|
#include <uv_vertex>
|
|
28
26
|
vMapUv = uv * ${uvOffset}.zw + ${uvOffset}.xy;
|
|
29
27
|
`.trim());
|
|
28
|
+
}
|
|
29
|
+
if (opacityUniform) {
|
|
30
|
+
// All four channels: the blend is premultiplied, so rgb already carries the texel's alpha and
|
|
31
|
+
// must carry the opacity too. Scaling `a` alone leaves a bright halo instead of a faded pin.
|
|
32
|
+
//
|
|
33
|
+
// After `colorspace_fragment`, so rgb is already sRGB-encoded and blends with the same weight
|
|
34
|
+
// as alpha. Before the encode, `0.5` would read about 73% opaque. Dim patches an earlier chunk
|
|
35
|
+
// (`tonemapping_fragment`), so the two never share an anchor.
|
|
36
|
+
parameters.fragmentShader = parameters.fragmentShader.replace('#include <colorspace_fragment>',
|
|
37
|
+
/*glsl*/ `
|
|
38
|
+
#include <colorspace_fragment>
|
|
39
|
+
gl_FragColor *= ${opacityUniform};
|
|
40
|
+
`.trim());
|
|
41
|
+
}
|
|
30
42
|
}
|
|
31
|
-
// An atlas texture is premultiplied (see `makeAtlasTexture`
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// same either way. `blendDst` stays `OneMinusSrcAlphaFactor` from `sharedParams`, since that half
|
|
36
|
-
// does not depend on how the source is stored.
|
|
37
|
-
//
|
|
38
|
-
// The override lives here, not in `sharedParams`, because `ColorMaterial` and `LineMaterial`
|
|
39
|
-
// share that object and their colors are never premultiplied.
|
|
43
|
+
// An atlas texture is premultiplied (see `makeAtlasTexture`), so this needs `blendSrc: OneFactor`
|
|
44
|
+
// rather than `sharedParams`'s straight-alpha source term; `blendDst` is unaffected. The override
|
|
45
|
+
// lives here because `ColorMaterial` and `LineMaterial` share `sharedParams` and are never
|
|
46
|
+
// premultiplied.
|
|
40
47
|
return (_jsx("meshBasicMaterial", { ...sharedParams, ...(overlay ? overlayParams : null), blendSrc: OneFactor, map: map, onBeforeCompile: onBeforeCompile }));
|
|
41
48
|
}
|
|
@@ -48,6 +48,20 @@ export declare class TextContainer extends SlotContainer<number, BatchedTextImpl
|
|
|
48
48
|
* the lifetime of a canvas, so capturing it here is safe.
|
|
49
49
|
*/
|
|
50
50
|
constructor(invalidate: () => void);
|
|
51
|
+
/**
|
|
52
|
+
* Binds the batch, and asks for a frame each time it finishes a sync.
|
|
53
|
+
*
|
|
54
|
+
* {@link scheduleSync}'s callback closes the loop only for a sync it starts. A write made inside a
|
|
55
|
+
* frame callback — a layout rule's, or a placement report's drained before the render — flags the
|
|
56
|
+
* batch before that frame renders, so the batch's own render hook starts the sync, with no
|
|
57
|
+
* callback, and the coalesced one that follows finds nothing left to do. Its glyphs then land
|
|
58
|
+
* after the last frame anyone asked for, and the old text stays on screen until something else
|
|
59
|
+
* happens to draw. A repack that runs inside the render hook is a frame late for the same reason:
|
|
60
|
+
* three has uploaded the batch's attributes by then. The batch's own completion is the one signal
|
|
61
|
+
* both paths share.
|
|
62
|
+
* @param mesh - The mounted batch, or the ref-cleanup `null`.
|
|
63
|
+
*/
|
|
64
|
+
bind(mesh: BatchedTextImpl | null): void;
|
|
51
65
|
/**
|
|
52
66
|
* The members backing one def.
|
|
53
67
|
* @param slot - The def's group.
|
|
@@ -61,7 +75,8 @@ export declare class TextContainer extends SlotContainer<number, BatchedTextImpl
|
|
|
61
75
|
* and parses the font — and under on-demand rendering a sync started during a frame has nothing to
|
|
62
76
|
* request the *next* frame when it completes. The callback carries R3F's `invalidate`, which is
|
|
63
77
|
* what closes that loop; without it new text appears only when something else happens to
|
|
64
|
-
* invalidate.
|
|
78
|
+
* invalidate. A sync this did not start is covered by the batch's own completion, which
|
|
79
|
+
* {@link bind} listens for.
|
|
65
80
|
*/
|
|
66
81
|
scheduleSync(): void;
|
|
67
82
|
claim(def: RenderableDef, lineCount: number): number;
|
|
@@ -45,6 +45,27 @@ export class TextContainer extends SlotContainer {
|
|
|
45
45
|
super();
|
|
46
46
|
this.invalidate = invalidate;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Binds the batch, and asks for a frame each time it finishes a sync.
|
|
50
|
+
*
|
|
51
|
+
* {@link scheduleSync}'s callback closes the loop only for a sync it starts. A write made inside a
|
|
52
|
+
* frame callback — a layout rule's, or a placement report's drained before the render — flags the
|
|
53
|
+
* batch before that frame renders, so the batch's own render hook starts the sync, with no
|
|
54
|
+
* callback, and the coalesced one that follows finds nothing left to do. Its glyphs then land
|
|
55
|
+
* after the last frame anyone asked for, and the old text stays on screen until something else
|
|
56
|
+
* happens to draw. A repack that runs inside the render hook is a frame late for the same reason:
|
|
57
|
+
* three has uploaded the batch's attributes by then. The batch's own completion is the one signal
|
|
58
|
+
* both paths share.
|
|
59
|
+
* @param mesh - The mounted batch, or the ref-cleanup `null`.
|
|
60
|
+
*/
|
|
61
|
+
bind(mesh) {
|
|
62
|
+
// `mesh` reads the bound batch, which is undefined until the first bind; the base ignores a
|
|
63
|
+
// re-bind, so the listener is added once too.
|
|
64
|
+
const first = mesh !== null && this.mesh === undefined;
|
|
65
|
+
super.bind(mesh);
|
|
66
|
+
if (first)
|
|
67
|
+
mesh.addEventListener('synccomplete', () => this.invalidate());
|
|
68
|
+
}
|
|
48
69
|
/**
|
|
49
70
|
* The members backing one def.
|
|
50
71
|
* @param slot - The def's group.
|
|
@@ -60,7 +81,8 @@ export class TextContainer extends SlotContainer {
|
|
|
60
81
|
* and parses the font — and under on-demand rendering a sync started during a frame has nothing to
|
|
61
82
|
* request the *next* frame when it completes. The callback carries R3F's `invalidate`, which is
|
|
62
83
|
* what closes that loop; without it new text appears only when something else happens to
|
|
63
|
-
* invalidate.
|
|
84
|
+
* invalidate. A sync this did not start is covered by the batch's own completion, which
|
|
85
|
+
* {@link bind} listens for.
|
|
64
86
|
*/
|
|
65
87
|
scheduleSync() {
|
|
66
88
|
if (this.syncScheduled)
|
|
@@ -2,7 +2,7 @@ import { type Camera } from 'three';
|
|
|
2
2
|
import { type CameraControlsImpl } from '../controls';
|
|
3
3
|
import { type Space } from './Space';
|
|
4
4
|
/**
|
|
5
|
-
* How much of the plan one
|
|
5
|
+
* How much of the plan one CSS pixel spans, in plan units — what everything sized in pixels is
|
|
6
6
|
* authored against, and what the imperative renderer published as `ptScale`.
|
|
7
7
|
*
|
|
8
8
|
* **One number for a view that has many.** Perspective foreshortens a pitched plan, so no single
|
|
@@ -21,16 +21,18 @@ import { type Space } from './Space';
|
|
|
21
21
|
* Neither path knows a `visibleRect` exists: its framing rides in `projectionMatrix`, which one
|
|
22
22
|
* reads `m00` out of and the other projects through.
|
|
23
23
|
*
|
|
24
|
-
* **
|
|
25
|
-
*
|
|
24
|
+
* **CSS pixels, not device pixels** — a plan reports the same pixel size at every device ratio. The
|
|
25
|
+
* renderer's public surface is CSS pixels, because a caller authors in CSS pixels and the ratio of
|
|
26
|
+
* the drawing buffer is the renderer's own business; the caller hands over the canvas's width in
|
|
27
|
+
* CSS pixels and the ratio never enters here.
|
|
26
28
|
* @param camera - The camera in use.
|
|
27
29
|
* @param space - The scene's space.
|
|
28
|
-
* @param
|
|
30
|
+
* @param cssWidth - The canvas's width in CSS pixels.
|
|
29
31
|
* @param controls - The camera controls, when mounted; they own the distance to the target.
|
|
30
|
-
* @returns Plan units per
|
|
31
|
-
*
|
|
32
|
+
* @returns Plan units per CSS pixel, or `undefined` while the camera misses the plan or the canvas
|
|
33
|
+
* has no width.
|
|
32
34
|
*/
|
|
33
|
-
export declare function measurePixelSize(camera: Camera, space: Space,
|
|
35
|
+
export declare function measurePixelSize(camera: Camera, space: Space, cssWidth: number, controls?: CameraControlsImpl): number | undefined;
|
|
34
36
|
/**
|
|
35
37
|
* Which way the plan is turned, in radians clockwise from north — {@link measurePixelSize}'s
|
|
36
38
|
* counterpart on the roll axis, split the same way.
|
|
@@ -46,12 +48,12 @@ export declare function measurePixelSize(camera: Camera, space: Space, bufferWid
|
|
|
46
48
|
* imperative renderer used, and with it the question of how far apart to place it.
|
|
47
49
|
* @param camera - The camera in use.
|
|
48
50
|
* @param space - The scene's space.
|
|
49
|
-
* @param
|
|
50
|
-
* @param
|
|
51
|
+
* @param cssWidth - The canvas's width in CSS pixels.
|
|
52
|
+
* @param cssHeight - Its height in CSS pixels, which weights the two NDC axes back into pixels.
|
|
51
53
|
* @param controls - The camera controls, when mounted; they hold the angle outright.
|
|
52
54
|
* @returns The roll in `[0, 2π)`, or `undefined` while the camera misses the plan.
|
|
53
55
|
*/
|
|
54
|
-
export declare function measureRoll(camera: Camera, space: Space,
|
|
56
|
+
export declare function measureRoll(camera: Camera, space: Space, cssWidth: number, cssHeight: number, controls?: CameraControlsImpl): number | undefined;
|
|
55
57
|
/**
|
|
56
58
|
* Whether a new pixel size differs enough from the last one to re-lay out on.
|
|
57
59
|
*
|
|
@@ -4,7 +4,7 @@ import { bearing } from '../controls';
|
|
|
4
4
|
import { euclideanModulo } from '../utils/math';
|
|
5
5
|
import { isOrthographic, isPerspective } from './camera-kind';
|
|
6
6
|
/**
|
|
7
|
-
* How much of the plan one
|
|
7
|
+
* How much of the plan one CSS pixel spans, in plan units — what everything sized in pixels is
|
|
8
8
|
* authored against, and what the imperative renderer published as `ptScale`.
|
|
9
9
|
*
|
|
10
10
|
* **One number for a view that has many.** Perspective foreshortens a pitched plan, so no single
|
|
@@ -23,24 +23,26 @@ import { isOrthographic, isPerspective } from './camera-kind';
|
|
|
23
23
|
* Neither path knows a `visibleRect` exists: its framing rides in `projectionMatrix`, which one
|
|
24
24
|
* reads `m00` out of and the other projects through.
|
|
25
25
|
*
|
|
26
|
-
* **
|
|
27
|
-
*
|
|
26
|
+
* **CSS pixels, not device pixels** — a plan reports the same pixel size at every device ratio. The
|
|
27
|
+
* renderer's public surface is CSS pixels, because a caller authors in CSS pixels and the ratio of
|
|
28
|
+
* the drawing buffer is the renderer's own business; the caller hands over the canvas's width in
|
|
29
|
+
* CSS pixels and the ratio never enters here.
|
|
28
30
|
* @param camera - The camera in use.
|
|
29
31
|
* @param space - The scene's space.
|
|
30
|
-
* @param
|
|
32
|
+
* @param cssWidth - The canvas's width in CSS pixels.
|
|
31
33
|
* @param controls - The camera controls, when mounted; they own the distance to the target.
|
|
32
|
-
* @returns Plan units per
|
|
33
|
-
*
|
|
34
|
+
* @returns Plan units per CSS pixel, or `undefined` while the camera misses the plan or the canvas
|
|
35
|
+
* has no width.
|
|
34
36
|
*/
|
|
35
|
-
export function measurePixelSize(camera, space,
|
|
36
|
-
if (!(
|
|
37
|
+
export function measurePixelSize(camera, space, cssWidth, controls) {
|
|
38
|
+
if (!(cssWidth > 0) || !(space.scale > 0))
|
|
37
39
|
return undefined;
|
|
38
40
|
if (controls) {
|
|
39
41
|
const worldWidth = visibleWorldWidth(camera, controls.distance);
|
|
40
42
|
if (!(worldWidth > 0))
|
|
41
43
|
return undefined;
|
|
42
44
|
// The camera measures the world, and only the fit says how much plan that world is.
|
|
43
|
-
return worldWidth /
|
|
45
|
+
return worldWidth / cssWidth / space.scale;
|
|
44
46
|
}
|
|
45
47
|
const aim = aimedAt(camera, space);
|
|
46
48
|
if (!aim)
|
|
@@ -56,7 +58,7 @@ export function measurePixelSize(camera, space, bufferWidth, controls) {
|
|
|
56
58
|
const planLength = Math.hypot(right.x - left.x, right.y - left.y);
|
|
57
59
|
if (!(planLength > 0))
|
|
58
60
|
return undefined;
|
|
59
|
-
return planLength /
|
|
61
|
+
return planLength / cssWidth;
|
|
60
62
|
}
|
|
61
63
|
/**
|
|
62
64
|
* Which way the plan is turned, in radians clockwise from north — {@link measurePixelSize}'s
|
|
@@ -73,12 +75,12 @@ export function measurePixelSize(camera, space, bufferWidth, controls) {
|
|
|
73
75
|
* imperative renderer used, and with it the question of how far apart to place it.
|
|
74
76
|
* @param camera - The camera in use.
|
|
75
77
|
* @param space - The scene's space.
|
|
76
|
-
* @param
|
|
77
|
-
* @param
|
|
78
|
+
* @param cssWidth - The canvas's width in CSS pixels.
|
|
79
|
+
* @param cssHeight - Its height in CSS pixels, which weights the two NDC axes back into pixels.
|
|
78
80
|
* @param controls - The camera controls, when mounted; they hold the angle outright.
|
|
79
81
|
* @returns The roll in `[0, 2π)`, or `undefined` while the camera misses the plan.
|
|
80
82
|
*/
|
|
81
|
-
export function measureRoll(camera, space,
|
|
83
|
+
export function measureRoll(camera, space, cssWidth, cssHeight, controls) {
|
|
82
84
|
if (controls)
|
|
83
85
|
return bearing(controls);
|
|
84
86
|
const aim = aimedAt(camera, space);
|
|
@@ -93,8 +95,8 @@ export function measureRoll(camera, space, bufferWidth, bufferHeight, controls)
|
|
|
93
95
|
// d(ndc)/dx by the quotient rule, less its `1/w²` — positive, so it moves no direction. NDC is
|
|
94
96
|
// aspect-distorted, so each axis is weighted back into pixels; canvas y grows downward where NDC
|
|
95
97
|
// y grows up.
|
|
96
|
-
const dx = (column[0] * clip.w - clip.x * column[3]) *
|
|
97
|
-
const dy = -(column[1] * clip.w - clip.y * column[3]) *
|
|
98
|
+
const dx = (column[0] * clip.w - clip.x * column[3]) * cssWidth;
|
|
99
|
+
const dy = -(column[1] * clip.w - clip.y * column[3]) * cssHeight;
|
|
98
100
|
if (dx === 0 && dy === 0)
|
|
99
101
|
return undefined;
|
|
100
102
|
// The plan turned clockwise by θ shows its east axis at screen angle -θ.
|
|
@@ -137,7 +139,7 @@ function aimedAt(camera, space) {
|
|
|
137
139
|
* installs, which leaves `fov` untouched.
|
|
138
140
|
*
|
|
139
141
|
* Width where the imperative renderer took height, so both of {@link measurePixelSize}'s paths
|
|
140
|
-
* divide by the one
|
|
142
|
+
* divide by the one canvas dimension they are given; for a projection whose aspect is the buffer's
|
|
141
143
|
* the two agree exactly.
|
|
142
144
|
* @param camera - The camera in use.
|
|
143
145
|
* @param distance - Distance from the camera to the point it is aimed at, in world units.
|