Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions docs/performance/motion-freezes-membership.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ held differently while coasting:
2026-10-04). A stack's opacity in the mix stops at 0.999: Safari painted every slice under a stack again each time its
opacity left or reached 1 (54 ms for 88 slices with the camera still, nothing for 0.99 to 0.98).
A galaxy's or nebula's picture layers follow the same two rules (`image-layers/prepared-image-layer-runtime.ts`,
`universe/universe-catalog-banks.ts`): a bank of sheets stops its own opacity at 0.999 too. A bank of patches draws
at 1, since a group under 1 lost a wedge of the picture at the Homunculus's nearest view, and pays the repaint each
time it crosses 1 (180 to 208 ms for Cassiopeia A's 1,397 patches). A stack the camera turns to, or a bank shown
again, stays hidden until its
`universe/universe-catalog-banks.ts`): the bank's own opacity stops at 0.999 too, for sheets and for patches
(Cassiopeia A's 1,397 patches repainted in one frame of 291 ms each time the bank crossed 1 on a zoom out, 51 ms
under the ceiling). A stack the camera turns to, or a bank shown again, stays hidden until its
images are decoded off the page's thread, then shows whole: decoded inside the paint, NGC 2392's 57 layers made a
frame of 235 ms on a turn and 270 ms on a dataset switch, against 34 and 31 ms decoded first (iPad, 2026-10-05).
Joining such a stack a share a frame was measured and rejected: each joining frame painted the layers already shown.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,8 @@ test('a stack mounts a camera for each of its scenes, and a stack without leaves
assert.ok(first!.style.transform === camera && second!.style.transform === camera && cameras.every(each => each.style.perspective === '600px'));
// Seen edge-on the stack is still the only one that draws: it keeps the whole weight.
publish([Math.SQRT1_2, 0, 0, Math.SQRT1_2]);
// A stack of patches is drawn at 1, not under the ceiling a stack of sheets is held to.
assert.equal(named('css-volume-projection')[0]!.style.opacity, '1');
// A stack of patches stays under the ceiling too, as a stack of sheets does.
assert.equal(named('css-volume-projection')[0]!.style.opacity, '0.999');
assert.equal(named('css-volume-projection')[0]!.style.visibility, 'visible');
assert.deepEqual(imageLayerAxisWeights([Math.SQRT1_2, 0, 0, Math.SQRT1_2], views.filter(view => view.axis === 'z')), { z: 1 });
});
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,6 @@ export function mountPreparedCssImageLayers({ host, before, payload, resolveReso
let destroyed = false;
const quotable = (address: string) => address.replace(/["\\\n\r]/g, char => `\\${char}`);
return Object.freeze({ root,
/** The opacity the bank's root stops at, as its stacks do (`publish`): 1 for a bank of patches, under 1
* (STACK_OPACITY_CEILING) for any other. */
ceiling: banks.every(bank => bank.patches) ? 1 : STACK_OPACITY_CEILING,
/** The bank root is shown again: each stack it draws waits for its images' decode, as one the camera turns to does. */
resume() { for (const bank of banks) { bank.drawn = false; bank.turn++; } },
/** `around`: the bank is drawn around a body that stands inside it, at that place (reference metres). The sheets the
Expand Down Expand Up @@ -196,12 +193,14 @@ export function mountPreparedCssImageLayers({ host, before, payload, resolveReso
const shown = body && texture.sheet && (withinReach(texture.sheet, local.positionUnits, bank.reach) || withinReach(texture.sheet, body, bank.reach)) ? '0' : '';
if (texture.shown !== shown) { texture.shown = shown; texture.element.style.opacity = shown; }
}
// Never 1 for a stack of sheets (STACK_OPACITY_CEILING): a drag across M31 had its longest frame at 108 to 111 ms
// with a bank's opacity reaching 1, and 67 to 72 ms under the ceiling (iPad, 2026-10-04). A stack of patches is
// the only stack of its bank and keeps the whole weight, so it is drawn at 1: under the ceiling the browser
// composites the stack as one group, and at the Homunculus's nearest view a wedge of that group, a third of the
// picture, was black for one step of a drag, at the same camera places on every pass (headless Chrome, 2026-10-05).
set(bank.projection, 'opacity', String(Math.min(bank.patches ? 1 : STACK_OPACITY_CEILING, weight)));
// Never 1 (STACK_OPACITY_CEILING): a drag across M31 had its longest frame at 108 to 111 ms with a bank's opacity
// reaching 1, and 67 to 72 ms under the ceiling (iPad, 2026-10-04). A stack of patches was drawn at 1 for a day:
// under the ceiling a wedge of the Homunculus had gone black for a step of a drag at its nearest view, in headless
// Chrome. That was looked for again and not found. At the nearest view, on the nebula's page and around its star,
// the picture under the ceiling matched the picture at 1 at every step: 580 steps in Chrome on a GPU, 90 in the
// software-rendered shell, 11 on an iPad. And at 1 the bank crossed 1 on every zoom out: Cassiopeia A's 1,397
// patches repainted in one frame of 291 ms on the iPad, 51 ms under the ceiling (2026-10-05).
set(bank.projection, 'opacity', String(Math.min(STACK_OPACITY_CEILING, weight)));
set(bank.projection, 'visibility', wanted && bank.ready ? 'visible' : 'hidden');
// A zero-weight axis contributes nothing; its 3D leaves leave compositing.
set(bank.projection, 'display', wanted ? '' : 'none');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mock.module('../image-layers/prepared-image-layer-runtime.js', { namedExports: {
mountPreparedCssImageLayers: ({ host, before }: { host: HTMLElement; before: Element }) => {
const root = host.ownerDocument.createElement('div');
host.insertBefore(root, before);
return { root, ceiling: 0.999, publish(_publication: unknown, around: readonly number[] | false = false) { drawnAround.push(around !== false); }, resume() {}, destroy() { root.remove(); } };
return { root, publish(_publication: unknown, around: readonly number[] | false = false) { drawnAround.push(around !== false); }, resume() {}, destroy() { root.remove(); } };
},
} });
// The modules under test import the mocked ones, so they load after the mocks.
Expand Down
10 changes: 5 additions & 5 deletions packages/renderer/src/universe/universe-catalog-banks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createImageFocusBank, createPointFocusBank } from './prepared-focus-bank.js';
import { STACK_OPACITY_CEILING } from '../volume/prepared-volume-runtime.js';
import type { SceneLifetime } from '@cssearth/engine';
import { type PreparedCatalogObject, type DensityVolumeFrame } from '@cssearth/objects';
import { type WorldCameraPose } from '@cssearth/engine';
Expand Down Expand Up @@ -225,11 +226,10 @@ export function createUniverseCatalogBanks({ root, end, stage, lifetime, declara
bank.dotsShown = dotOpacity > 0;
}
if (opacity !== bank.publishedOpacity) {
// The root stops where its stacks do (the bank's `ceiling`). Under 1 for a bank of sheets: Safari paints every leaf
// under the root again when its opacity leaves or reaches 1. At 1 for a bank of patches, which a group under 1
// draws with a wedge missing at its nearest view (prepared-image-layer-runtime.ts); such a bank pays that repaint,
// 180 to 208 ms each way for Cassiopeia A's 1,397 patches with the camera still on an iPad (2026-10-05).
bank.mounted.root.style.opacity = String(Math.min(bank.mounted.ceiling, opacity));
// Never 1 (STACK_OPACITY_CEILING), as a stack's own opacity: Safari paints every leaf under the root again when its
// opacity leaves or reaches 1. Cassiopeia A's 1,397 patches made a frame of 180 to 208 ms each way with the camera
// still on an iPad, and none between 0.999 and 0.99 (2026-10-05): the first frames of every zoom out of a nebula.
bank.mounted.root.style.opacity = String(Math.min(STACK_OPACITY_CEILING, opacity));
if (opacity > 0 && bank.mounted.root.style.display === 'none') bank.mounted.resume();
bank.mounted.root.style.display = opacity > 0 ? '' : 'none';
bank.publishedOpacity = opacity;
Expand Down
Loading