@johnmorrisdotca/uchusen-rockets 0.2.0 → 0.2.1
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/CHANGELOG.md +12 -0
- package/dist/element/scene.d.ts +2 -1
- package/dist/element/scene.js +33 -13
- package/dist/element.d.ts +9 -3
- package/dist/element.js +73 -19
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pixels.d.ts +8 -0
- package/dist/pixels.js +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.1]
|
|
6
|
+
|
|
7
|
+
- Fixed: the demo could hang the screen while turning. When the 3D view turned by itself, or glided to a stop after a drag, the controls' change event asked for the next frame while the view's own loop did too, so each frame started two and the number of draws doubled every frame: thousands of draws a second, enough to starve the graphics card (measured in a real browser on a real graphics card: 12,308 draws in 3 seconds, against 181 now at 60 frames a second). One frame is asked for, once.
|
|
8
|
+
- Fixed: every change of variant, look or parts left the old shapes in the graphics card (about 60 buffers each), and a painted model's pictures with them. They are given back now, and the glow's too.
|
|
9
|
+
- Fixed: the canvas had no size limit but a ratio of 2. It is held to 4096 pixels a side and 6 million pixels in all, so a view as wide as a 5K screen is no longer drawn at 15 million.
|
|
10
|
+
- The drawing, with no WebGL, is zoomed and moved by its `viewBox` instead of a CSS scale, so it never grows past its box; it is made again at most 20 times a second, never while out of sight (a hidden tab, scrolled away), and is not rebuilt when only the zoom or the move changes.
|
|
11
|
+
- The "Tilt to turn" listener is let go when the 3D view is, and does nothing out of sight; it was added again after each return to the screen.
|
|
12
|
+
- `drive()` with the same choice again does nothing.
|
|
13
|
+
- The demo's auto-turn stops after a minute and when the view leaves the screen; Play starts it again.
|
|
14
|
+
- `pixelRatioFor(width, height, deviceRatio)`, with `MAX_PIXELS`, `MAX_SIDE` and `MAX_RATIO`.
|
|
15
|
+
- New browser tests: draws per frame, nothing drawing when still or out of sight, no growth of contexts, buffers, vertex arrays or textures over changes, a lost context, and the canvas within its budget from 1x to 5x on a 5K screen.
|
|
16
|
+
|
|
5
17
|
## [0.2.0]
|
|
6
18
|
|
|
7
19
|
- `element.drive({ yaw, tilt, zoom, panX, panY })`: a page can hand the turn, tip, zoom and move to the family's `<kazoku-view-controls>` ([`@johnmorrisdotca/kazoku-ui`](https://www.npmjs.com/package/@johnmorrisdotca/kazoku-ui)). The element's own drag, wheel, keys and spin stand down, and the 3D view and the drawing both follow, so it works with no WebGL as well. `drive(null)` takes them back. kazoku-ui is not imported by the package: the demo uses it, as a dev dependency.
|
package/dist/element/scene.d.ts
CHANGED
|
@@ -102,7 +102,8 @@ export declare class RocketViewer {
|
|
|
102
102
|
private lens;
|
|
103
103
|
setTilt(azimuthDeg: number, polarDeg: number): void;
|
|
104
104
|
dispose(): void;
|
|
105
|
-
|
|
105
|
+
/** Lets the graphics card have back what the parts took: geometries, the thick lines' materials and a licensed model's own materials and pictures. */
|
|
106
|
+
private disposeParts;
|
|
106
107
|
private addGlow;
|
|
107
108
|
/** The pool of light is as wide as what stands in it. */
|
|
108
109
|
private fitGlow;
|
package/dist/element/scene.js
CHANGED
|
@@ -9,6 +9,7 @@ import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry.js
|
|
|
9
9
|
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
10
10
|
import { directionOf, FOV_DEG, frameFor, PICTURE_H, turnedBy } from "../framing.js";
|
|
11
11
|
import { LOOKS } from "../looks.js";
|
|
12
|
+
import { pixelRatioFor } from "../pixels.js";
|
|
12
13
|
import { MATERIALS } from "../model/materials.js";
|
|
13
14
|
import { paintGlow, paintSky, paintSteel, paintTiles } from "./paint.js";
|
|
14
15
|
const IDLE_STOP_MS = 90_000;
|
|
@@ -72,6 +73,8 @@ void main() {
|
|
|
72
73
|
/** The materials of an engine's bell, in this package's own meshes. */
|
|
73
74
|
const NOZZLE_MATERIALS = new Set(["bell", "bell-inside", "engine-metal"]);
|
|
74
75
|
const asVec = (v) => [v.x, v.y, v.z];
|
|
76
|
+
/** The pictures a licensed model's material may hold (the shared sky it reflects is not one of them). */
|
|
77
|
+
const MODEL_PICTURES = ["map", "normalMap", "roughnessMap", "metalnessMap", "aoMap", "emissiveMap"];
|
|
75
78
|
export class RocketViewer {
|
|
76
79
|
canvas;
|
|
77
80
|
frames = 0;
|
|
@@ -110,7 +113,6 @@ export class RocketViewer {
|
|
|
110
113
|
this.holder = holder;
|
|
111
114
|
this.options = options;
|
|
112
115
|
this.renderer = new WebGLRenderer({ antialias: true, alpha: true, powerPreference: "low-power" });
|
|
113
|
-
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
114
116
|
this.renderer.outputColorSpace = SRGBColorSpace;
|
|
115
117
|
this.renderer.toneMapping = ACESFilmicToneMapping;
|
|
116
118
|
this.renderer.toneMappingExposure = 1;
|
|
@@ -188,9 +190,7 @@ export class RocketViewer {
|
|
|
188
190
|
const gltf = await new GLTFLoader().parseAsync(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), "");
|
|
189
191
|
if (this.disposed || generation !== this.generation)
|
|
190
192
|
return;
|
|
191
|
-
|
|
192
|
-
part.holder.removeFromParent();
|
|
193
|
-
this.disposeTree(this.root);
|
|
193
|
+
this.disposeParts();
|
|
194
194
|
this.root.clear();
|
|
195
195
|
this.parts = [];
|
|
196
196
|
this.built = built;
|
|
@@ -609,6 +609,9 @@ export class RocketViewer {
|
|
|
609
609
|
}
|
|
610
610
|
/** Hands the turn, tip, zoom and pan to the page, as the family's view controls report them: this view's own hands (drag, wheel, keys, spin) stand down, and the camera is the chosen view's angle turned and tipped by them. `null` takes them back. */
|
|
611
611
|
drive(choice) {
|
|
612
|
+
const same = choice !== null && this.driven !== null && Object.keys(choice).every((key) => choice[key] === this.driven?.[key]);
|
|
613
|
+
if (same)
|
|
614
|
+
return;
|
|
612
615
|
this.driven = choice;
|
|
613
616
|
this.controls.enabled = choice === null;
|
|
614
617
|
if (choice)
|
|
@@ -670,7 +673,9 @@ export class RocketViewer {
|
|
|
670
673
|
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
671
674
|
this.resize.disconnect();
|
|
672
675
|
this.controls.dispose();
|
|
673
|
-
this.
|
|
676
|
+
this.disposeParts();
|
|
677
|
+
this.glow?.geometry.dispose();
|
|
678
|
+
this.glow?.material?.dispose();
|
|
674
679
|
for (const material of this.materialCache.values())
|
|
675
680
|
material.dispose();
|
|
676
681
|
this.outlineMaterial.dispose();
|
|
@@ -681,13 +686,25 @@ export class RocketViewer {
|
|
|
681
686
|
this.renderer.forceContextLoss();
|
|
682
687
|
this.canvas.remove();
|
|
683
688
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
node
|
|
690
|
-
|
|
689
|
+
/** Lets the graphics card have back what the parts took: geometries, the thick lines' materials and a licensed model's own materials and pictures. */
|
|
690
|
+
disposeParts() {
|
|
691
|
+
for (const part of this.parts) {
|
|
692
|
+
part.holder.traverse((node) => {
|
|
693
|
+
node.geometry?.dispose();
|
|
694
|
+
if (node instanceof LineSegments2)
|
|
695
|
+
node.material.dispose();
|
|
696
|
+
});
|
|
697
|
+
part.model?.traverse((node) => {
|
|
698
|
+
if (!(node instanceof Mesh))
|
|
699
|
+
return;
|
|
700
|
+
for (const material of Array.isArray(node.material) ? node.material : [node.material]) {
|
|
701
|
+
for (const slot of MODEL_PICTURES)
|
|
702
|
+
material[slot]?.dispose();
|
|
703
|
+
material.dispose();
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
part.holder.removeFromParent();
|
|
707
|
+
}
|
|
691
708
|
}
|
|
692
709
|
addGlow() {
|
|
693
710
|
if (!this.built || this.glow)
|
|
@@ -805,7 +822,8 @@ export class RocketViewer {
|
|
|
805
822
|
if (this.glow)
|
|
806
823
|
this.glow.visible = this.look !== "painted" && this.camera.position.y > 0;
|
|
807
824
|
}
|
|
808
|
-
|
|
825
|
+
// the controls' own change event may already have asked for the next frame (wake): one frame is asked for, never two, or each frame would double the next
|
|
826
|
+
if (!this.frame && ((ambient && this.controls.autoRotate) || changed || this.pose || this.needsDraw || Math.abs(dA) > 1e-5))
|
|
809
827
|
this.frame = requestAnimationFrame(this.tick);
|
|
810
828
|
};
|
|
811
829
|
syncSizes() {
|
|
@@ -815,7 +833,9 @@ export class RocketViewer {
|
|
|
815
833
|
fit() {
|
|
816
834
|
const w = Math.max(1, this.holder.clientWidth);
|
|
817
835
|
const h = Math.max(1, this.holder.clientHeight);
|
|
836
|
+
this.renderer.setPixelRatio(pixelRatioFor(w, h, window.devicePixelRatio));
|
|
818
837
|
this.renderer.setSize(w, h, false);
|
|
838
|
+
this.outlineMaterial.uniforms.width.value = LOOKS[this.look].outlinePx * this.renderer.getPixelRatio();
|
|
819
839
|
this.camera.aspect = w / h;
|
|
820
840
|
this.lens();
|
|
821
841
|
this.syncSizes();
|
package/dist/element.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ export declare class UchusenRocket extends HTMLBase {
|
|
|
13
13
|
private drawing;
|
|
14
14
|
private viewer;
|
|
15
15
|
private watch;
|
|
16
|
-
private cleanups;
|
|
17
16
|
private releaseTimer;
|
|
18
17
|
private revealTimer;
|
|
19
18
|
private generation;
|
|
@@ -25,6 +24,11 @@ export declare class UchusenRocket extends HTMLBase {
|
|
|
25
24
|
private key;
|
|
26
25
|
private driven;
|
|
27
26
|
private placeFrame;
|
|
27
|
+
private placedAt;
|
|
28
|
+
private stale;
|
|
29
|
+
private onScreen;
|
|
30
|
+
private drawn;
|
|
31
|
+
private tiltCleanups;
|
|
28
32
|
/** What is showing now. */
|
|
29
33
|
get state(): RocketState;
|
|
30
34
|
/** What the fit check said about what is showing: its label, parts and credits. */
|
|
@@ -36,8 +40,9 @@ export declare class UchusenRocket extends HTMLBase {
|
|
|
36
40
|
get angle(): Angle | null;
|
|
37
41
|
/** Lets a page choose the turn, tip, zoom and pan, as the family's `<kazoku-view-controls>` reports them in `kazoku-viewchange` (`element.drive(event.detail)`): the element's own drag, wheel, keys and spin stand down, and the 3D view and the drawing both follow. `null` takes them back. */
|
|
38
42
|
drive(choice: Partial<ViewChoice> | null): void;
|
|
39
|
-
/** The drawing, turned and moved to the chosen view. The 3D view moves itself, so only the drawing needs it, and only until the 3D view shows. */
|
|
43
|
+
/** The drawing, turned and moved to the chosen view. The 3D view moves itself, so only the drawing needs it, and only until the 3D view shows. It is drawn at most about 20 times a second and never while out of sight: it is made again when it comes back. */
|
|
40
44
|
private place;
|
|
45
|
+
private seen;
|
|
41
46
|
connectedCallback(): void;
|
|
42
47
|
disconnectedCallback(): void;
|
|
43
48
|
attributeChangedCallback(name: string, before: string | null, after: string | null): void;
|
|
@@ -51,13 +56,14 @@ export declare class UchusenRocket extends HTMLBase {
|
|
|
51
56
|
private settings;
|
|
52
57
|
private update;
|
|
53
58
|
private renderShell;
|
|
54
|
-
/** The drawing for what the attributes say now. It is made from code, so it needs no file. */
|
|
59
|
+
/** The drawing for what the attributes say now. It is made from code, so it needs no file. A zoom or a move only changes which part of it the box shows (its viewBox), so the drawing is as sharp as the box and never grows past it. */
|
|
55
60
|
private redraw;
|
|
56
61
|
private mark;
|
|
57
62
|
private applyRocket;
|
|
58
63
|
private lastView;
|
|
59
64
|
private applySettings;
|
|
60
65
|
private startWatching;
|
|
66
|
+
private stopTilt;
|
|
61
67
|
private fallBack;
|
|
62
68
|
private start;
|
|
63
69
|
private startTilt;
|
package/dist/element.js
CHANGED
|
@@ -14,6 +14,8 @@ const HTMLBase = typeof HTMLElement === "undefined" ? class {
|
|
|
14
14
|
/** A view out of sight keeps its WebGL canvas this long before letting it go (a browser allows only a few at a time). */
|
|
15
15
|
const RELEASE_MS = 30_000;
|
|
16
16
|
const DEFAULT_SPIN = { seconds: 180, way: "cw" };
|
|
17
|
+
/** The drawing, followed along a turn, is made again at most this often. */
|
|
18
|
+
const PLACE_MS = 50;
|
|
17
19
|
let assetBase = null;
|
|
18
20
|
/** Where the licensed models are, for every element with no `assets` attribute of its own. Left unset: beside this module, in ./assets/. */
|
|
19
21
|
export function setAssetBase(url) {
|
|
@@ -88,7 +90,6 @@ export class UchusenRocket extends HTMLBase {
|
|
|
88
90
|
drawing = null;
|
|
89
91
|
viewer = null;
|
|
90
92
|
watch = null;
|
|
91
|
-
cleanups = [];
|
|
92
93
|
releaseTimer = 0;
|
|
93
94
|
revealTimer = 0;
|
|
94
95
|
generation = 0;
|
|
@@ -100,6 +101,11 @@ export class UchusenRocket extends HTMLBase {
|
|
|
100
101
|
key = "";
|
|
101
102
|
driven = null;
|
|
102
103
|
placeFrame = 0;
|
|
104
|
+
placedAt = 0;
|
|
105
|
+
stale = false;
|
|
106
|
+
onScreen = true;
|
|
107
|
+
drawn = { key: "", box: [0, 0, 1, 1] };
|
|
108
|
+
tiltCleanups = [];
|
|
103
109
|
/** What is showing now. */
|
|
104
110
|
get state() {
|
|
105
111
|
return this.stateNow;
|
|
@@ -122,6 +128,7 @@ export class UchusenRocket extends HTMLBase {
|
|
|
122
128
|
}
|
|
123
129
|
/** Lets a page choose the turn, tip, zoom and pan, as the family's `<kazoku-view-controls>` reports them in `kazoku-viewchange` (`element.drive(event.detail)`): the element's own drag, wheel, keys and spin stand down, and the 3D view and the drawing both follow. `null` takes them back. */
|
|
124
130
|
drive(choice) {
|
|
131
|
+
const before = this.driven;
|
|
125
132
|
if (choice === null) {
|
|
126
133
|
this.driven = null;
|
|
127
134
|
delete this.dataset.driven;
|
|
@@ -137,22 +144,44 @@ export class UchusenRocket extends HTMLBase {
|
|
|
137
144
|
};
|
|
138
145
|
this.dataset.driven = "true";
|
|
139
146
|
}
|
|
147
|
+
const now = this.driven;
|
|
148
|
+
if (before && now && Object.keys(now).every((key) => before[key] === now[key]))
|
|
149
|
+
return;
|
|
140
150
|
this.viewer?.drive(this.driven);
|
|
141
151
|
this.place();
|
|
142
152
|
}
|
|
143
|
-
/** The drawing, turned and moved to the chosen view. The 3D view moves itself, so only the drawing needs it, and only until the 3D view shows. */
|
|
153
|
+
/** The drawing, turned and moved to the chosen view. The 3D view moves itself, so only the drawing needs it, and only until the 3D view shows. It is drawn at most about 20 times a second and never while out of sight: it is made again when it comes back. */
|
|
144
154
|
place() {
|
|
145
155
|
if (this.stateNow === "3d" && this.viewer)
|
|
146
156
|
return;
|
|
157
|
+
if (!this.onScreen || document.hidden) {
|
|
158
|
+
this.stale = true;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
147
161
|
window.cancelAnimationFrame(this.placeFrame);
|
|
148
|
-
|
|
162
|
+
const frame = (time) => {
|
|
163
|
+
if (time - this.placedAt < PLACE_MS) {
|
|
164
|
+
this.placeFrame = window.requestAnimationFrame(frame);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.placedAt = time;
|
|
168
|
+
this.stale = false;
|
|
169
|
+
this.redraw();
|
|
170
|
+
};
|
|
171
|
+
this.placeFrame = window.requestAnimationFrame(frame);
|
|
149
172
|
}
|
|
173
|
+
seen = () => {
|
|
174
|
+
if (!document.hidden && this.stale)
|
|
175
|
+
this.place();
|
|
176
|
+
};
|
|
150
177
|
connectedCallback() {
|
|
151
178
|
if (!this.shadowRoot)
|
|
152
179
|
adopt(this.attachShadow({ mode: "open" }), CSS, rocketSheet);
|
|
180
|
+
document.addEventListener("visibilitychange", this.seen);
|
|
153
181
|
this.queue();
|
|
154
182
|
}
|
|
155
183
|
disconnectedCallback() {
|
|
184
|
+
document.removeEventListener("visibilitychange", this.seen);
|
|
156
185
|
this.teardown();
|
|
157
186
|
this.key = "";
|
|
158
187
|
}
|
|
@@ -204,8 +233,7 @@ export class UchusenRocket extends HTMLBase {
|
|
|
204
233
|
window.clearTimeout(this.releaseTimer);
|
|
205
234
|
window.clearTimeout(this.revealTimer);
|
|
206
235
|
window.cancelAnimationFrame(this.placeFrame);
|
|
207
|
-
|
|
208
|
-
clean();
|
|
236
|
+
this.stopTilt();
|
|
209
237
|
this.viewer?.dispose();
|
|
210
238
|
this.viewer = null;
|
|
211
239
|
}
|
|
@@ -274,23 +302,35 @@ export class UchusenRocket extends HTMLBase {
|
|
|
274
302
|
this.holder = holder;
|
|
275
303
|
this.startWatching();
|
|
276
304
|
}
|
|
277
|
-
/** The drawing for what the attributes say now. It is made from code, so it needs no file. */
|
|
305
|
+
/** The drawing for what the attributes say now. It is made from code, so it needs no file. A zoom or a move only changes which part of it the box shows (its viewBox), so the drawing is as sharp as the box and never grows past it. */
|
|
278
306
|
redraw() {
|
|
279
307
|
if (!this.builtNow || !this.drawing)
|
|
280
308
|
return;
|
|
281
309
|
const { look, show, angle } = this.settings(this.builtNow);
|
|
282
310
|
// when a 3D view is showing, a drawing of the same angle is ready behind it
|
|
283
311
|
const from = this.driven ? turnedBy(angle, this.driven.yaw, this.driven.tilt) : (this.viewer?.currentAngle() ?? angle);
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
312
|
+
const label = this.getAttribute("label") ?? this.builtNow.report.label;
|
|
313
|
+
const key = `${this.key}|${look}|${show}|${from.az.toFixed(2)}|${from.el.toFixed(2)}|${label}`;
|
|
314
|
+
let node = this.drawing.firstElementChild;
|
|
315
|
+
if (key !== this.drawn.key || !node) {
|
|
316
|
+
const template = document.createElement("template");
|
|
317
|
+
template.innerHTML = this.builtNow.svg({ look, show, view: from });
|
|
318
|
+
node = template.content.firstElementChild;
|
|
319
|
+
if (!node)
|
|
320
|
+
return;
|
|
321
|
+
node.setAttribute("aria-label", label);
|
|
322
|
+
this.drawing.replaceChildren(node);
|
|
323
|
+
const [x = 0, y = 0, w = 1, h = 1] = (node.getAttribute("viewBox") ?? "").split(/[\s,]+/).map(Number);
|
|
324
|
+
this.drawn = { key, box: [x, y, w, h] };
|
|
325
|
+
}
|
|
292
326
|
const { zoom = 1, panX = 0, panY = 0 } = this.driven ?? {};
|
|
293
|
-
|
|
327
|
+
const [x, y, w, h] = this.drawn.box;
|
|
328
|
+
// the picture fits the box (meet), so a user unit is `unit` pixels; the zoom is about the box's middle and the move is in pixels
|
|
329
|
+
const unit = Math.min(this.drawing.clientWidth / w, this.drawing.clientHeight / h) || 1;
|
|
330
|
+
const view = zoom === 1 && panX === 0 && panY === 0 ? [x, y, w, h] : [x + w / 2 - w / (2 * zoom) - panX / (unit * zoom), y + h / 2 - h / (2 * zoom) - panY / (unit * zoom), w / zoom, h / zoom];
|
|
331
|
+
node.setAttribute("viewBox", view.map((n) => +n.toFixed(3)).join(" "));
|
|
332
|
+
this.drawing.dataset.zoom = zoom.toFixed(2);
|
|
333
|
+
this.drawing.dataset.pan = `${Math.round(panX)},${Math.round(panY)}`;
|
|
294
334
|
}
|
|
295
335
|
mark() {
|
|
296
336
|
if (!this.builtNow)
|
|
@@ -340,10 +380,15 @@ export class UchusenRocket extends HTMLBase {
|
|
|
340
380
|
this.setState(reason === "off" ? "picture" : "fallback", reason);
|
|
341
381
|
if (reason !== "off")
|
|
342
382
|
this.emit("uchusen-fallback", { reason });
|
|
343
|
-
return;
|
|
344
383
|
}
|
|
345
|
-
|
|
384
|
+
else
|
|
385
|
+
this.setState("picture");
|
|
346
386
|
this.watch = new IntersectionObserver(([seen]) => {
|
|
387
|
+
this.onScreen = seen.isIntersecting;
|
|
388
|
+
if (seen.isIntersecting && this.stale)
|
|
389
|
+
this.place();
|
|
390
|
+
if (reason)
|
|
391
|
+
return;
|
|
347
392
|
if (seen.isIntersecting) {
|
|
348
393
|
this.shownAt ||= performance.now();
|
|
349
394
|
window.clearTimeout(this.releaseTimer);
|
|
@@ -358,6 +403,7 @@ export class UchusenRocket extends HTMLBase {
|
|
|
358
403
|
this.releaseTimer = window.setTimeout(() => {
|
|
359
404
|
this.viewer?.dispose();
|
|
360
405
|
this.viewer = null;
|
|
406
|
+
this.stopTilt();
|
|
361
407
|
if (generation === this.generation) {
|
|
362
408
|
this.setState("picture");
|
|
363
409
|
if (this.driven)
|
|
@@ -368,7 +414,12 @@ export class UchusenRocket extends HTMLBase {
|
|
|
368
414
|
}, { rootMargin: "200px 0px" });
|
|
369
415
|
this.watch.observe(this);
|
|
370
416
|
}
|
|
417
|
+
stopTilt() {
|
|
418
|
+
for (const clean of this.tiltCleanups.splice(0))
|
|
419
|
+
clean();
|
|
420
|
+
}
|
|
371
421
|
fallBack(why) {
|
|
422
|
+
this.stopTilt();
|
|
372
423
|
this.viewer?.dispose();
|
|
373
424
|
this.viewer = null;
|
|
374
425
|
this.setState("fallback", why);
|
|
@@ -428,6 +479,7 @@ export class UchusenRocket extends HTMLBase {
|
|
|
428
479
|
}
|
|
429
480
|
}
|
|
430
481
|
startTilt() {
|
|
482
|
+
this.stopTilt();
|
|
431
483
|
const Orientation = (typeof DeviceOrientationEvent === "undefined" ? undefined : DeviceOrientationEvent);
|
|
432
484
|
const mode = tiltMode({
|
|
433
485
|
reducedMotion: media("(prefers-reduced-motion: reduce)"),
|
|
@@ -438,6 +490,8 @@ export class UchusenRocket extends HTMLBase {
|
|
|
438
490
|
const listen = () => {
|
|
439
491
|
let rest = null;
|
|
440
492
|
const onTilt = (event) => {
|
|
493
|
+
if (!this.onScreen || document.hidden)
|
|
494
|
+
return;
|
|
441
495
|
const reading = { beta: event.beta, gamma: event.gamma };
|
|
442
496
|
rest ??= reading;
|
|
443
497
|
const turn = tiltTurn(reading, rest, window.screen.orientation?.angle ?? 0);
|
|
@@ -445,7 +499,7 @@ export class UchusenRocket extends HTMLBase {
|
|
|
445
499
|
this.viewer?.setTilt(turn.azimuth, turn.polar);
|
|
446
500
|
};
|
|
447
501
|
window.addEventListener("deviceorientation", onTilt);
|
|
448
|
-
this.
|
|
502
|
+
this.tiltCleanups.push(() => window.removeEventListener("deviceorientation", onTilt));
|
|
449
503
|
};
|
|
450
504
|
if (mode === "on")
|
|
451
505
|
listen();
|
|
@@ -466,7 +520,7 @@ export class UchusenRocket extends HTMLBase {
|
|
|
466
520
|
}
|
|
467
521
|
});
|
|
468
522
|
this.picture.appendChild(button);
|
|
469
|
-
this.
|
|
523
|
+
this.tiltCleanups.push(() => button.remove());
|
|
470
524
|
}
|
|
471
525
|
}
|
|
472
526
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type { Look, LookName } from "./looks.ts";
|
|
|
17
17
|
export { cleanShow, placesFor, showsOf } from "./layout.ts";
|
|
18
18
|
export type { Place, ShowName } from "./layout.ts";
|
|
19
19
|
export { CROSSFADE_MS, DRAWING_HOLD_MS, REDUCED_FADE_MS, revealDelay } from "./crossfade.ts";
|
|
20
|
+
export { MAX_PIXELS, MAX_RATIO, MAX_SIDE, pixelRatioFor } from "./pixels.ts";
|
|
20
21
|
export { tiltMode, tiltTurn } from "./tilt.ts";
|
|
21
22
|
export type { Reading, TiltMode } from "./tilt.ts";
|
|
22
23
|
export { MATERIALS } from "./model/materials.ts";
|
package/dist/index.js
CHANGED
|
@@ -9,5 +9,6 @@ export { axesOf, cleanView, DEFAULT_VIEW, directionOf, FOV_DEG, frameFor, PICTUR
|
|
|
9
9
|
export { cleanLook, DEFAULT_LOOK, LOOK_NAMES, LOOKS } from "./looks.js";
|
|
10
10
|
export { cleanShow, placesFor, showsOf } from "./layout.js";
|
|
11
11
|
export { CROSSFADE_MS, DRAWING_HOLD_MS, REDUCED_FADE_MS, revealDelay } from "./crossfade.js";
|
|
12
|
+
export { MAX_PIXELS, MAX_RATIO, MAX_SIDE, pixelRatioFor } from "./pixels.js";
|
|
12
13
|
export { tiltMode, tiltTurn } from "./tilt.js";
|
|
13
14
|
export { MATERIALS } from "./model/materials.js";
|
package/dist/pixels.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The most the backing store may hold, in pixels (a 620 x 827 view at a ratio of 2 is 2 million). */
|
|
2
|
+
export declare const MAX_PIXELS = 6000000;
|
|
3
|
+
/** The longest side the backing store may have. */
|
|
4
|
+
export declare const MAX_SIDE = 4096;
|
|
5
|
+
/** The sharpest ratio ever used, however dense the screen. */
|
|
6
|
+
export declare const MAX_RATIO = 2;
|
|
7
|
+
/** The device pixel ratio to draw a `width` x `height` view at: the screen's own, held to 2, to 4096 pixels a side and to 6 million in all. At least 0.25, and 1 for a nonsense value. */
|
|
8
|
+
export declare function pixelRatioFor(width: number, height: number, deviceRatio: number): number;
|
package/dist/pixels.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// How sharp the 3D canvas may be. The backing store is width x height x ratio squared, and a view as big as a 5K screen at a ratio of 2 would ask the graphics card for 15 million pixels, every frame. Pure.
|
|
2
|
+
/** The most the backing store may hold, in pixels (a 620 x 827 view at a ratio of 2 is 2 million). */
|
|
3
|
+
export const MAX_PIXELS = 6_000_000;
|
|
4
|
+
/** The longest side the backing store may have. */
|
|
5
|
+
export const MAX_SIDE = 4096;
|
|
6
|
+
/** The sharpest ratio ever used, however dense the screen. */
|
|
7
|
+
export const MAX_RATIO = 2;
|
|
8
|
+
/** The device pixel ratio to draw a `width` x `height` view at: the screen's own, held to 2, to 4096 pixels a side and to 6 million in all. At least 0.25, and 1 for a nonsense value. */
|
|
9
|
+
export function pixelRatioFor(width, height, deviceRatio) {
|
|
10
|
+
const w = Math.max(1, width);
|
|
11
|
+
const h = Math.max(1, height);
|
|
12
|
+
const wanted = Number.isFinite(deviceRatio) && deviceRatio > 0 ? Math.min(deviceRatio, MAX_RATIO) : 1;
|
|
13
|
+
return Math.max(0.25, Math.min(wanted, MAX_SIDE / Math.max(w, h), Math.sqrt(MAX_PIXELS / (w * h))));
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/uchusen-rockets",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Uchusen Rockets 宇宙船: line drawings and 3D views of rockets, as a web component. Every variant with its own sourced sizes, a typed way to assemble a rocket from parts, stack and separate. No logos. three.js is a peer dependency.",
|
|
5
5
|
"keywords": ["rocket", "rockets", "spaceship", "starship", "super-heavy", "falcon-9", "dragon", "spaceflight", "line-drawing", "svg", "3d", "3d-models", "glb", "gltf", "three", "threejs", "webgl", "web-component", "custom-element", "react", "typescript", "data"],
|
|
6
6
|
"homepage": "https://johnmorrisdotca.github.io/uchusen-rockets/",
|