jsgui3-server 0.0.133 → 0.0.134
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/.vscode/settings.json +8 -1
- package/examples/controls/14) window, canvas/client.js +238 -0
- package/examples/controls/14) window, canvas/server.js +21 -0
- package/examples/controls/14b) window, canvas (improved renderer)/client.js +391 -0
- package/examples/controls/14b) window, canvas (improved renderer)/server.js +21 -0
- package/examples/controls/14d) window, canvas globe/EarthGlobeRenderer.js +878 -0
- package/examples/controls/14d) window, canvas globe/client.js +95 -0
- package/examples/controls/14d) window, canvas globe/math.js +76 -0
- package/examples/controls/14d) window, canvas globe/server.js +21 -0
- package/examples/controls/14e) window, canvas multithreaded/client.js +948 -0
- package/examples/controls/14e) window, canvas multithreaded/server.js +21 -0
- package/examples/controls/14f) window, canvas polyglobe/client.js +569 -0
- package/examples/controls/14f) window, canvas polyglobe/math.js +137 -0
- package/examples/controls/14f) window, canvas polyglobe/server.js +21 -0
- package/package.json +5 -5
package/.vscode/settings.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"workbench.colorCustomizations": {
|
|
3
3
|
"minimap.background": "#00000000",
|
|
4
|
-
"scrollbar.shadow": "#00000000"
|
|
4
|
+
"scrollbar.shadow": "#00000000",
|
|
5
|
+
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
|
|
6
|
+
"editor.formatOnType": false, // required
|
|
7
|
+
"editor.formatOnPaste": true, // optional
|
|
8
|
+
"editor.formatOnSave": true, // optional
|
|
9
|
+
"editor.formatOnSaveMode": "file", // required to format on save
|
|
10
|
+
"files.autoSave": "onFocusChange", // optional but recommended
|
|
11
|
+
"vs-code-prettier-eslint.prettierLast": false // set as "true" to run 'prettier' last not first
|
|
5
12
|
}
|
|
6
13
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const jsgui = require('jsgui3-client');
|
|
2
|
+
const {controls, Control, mixins} = jsgui;
|
|
3
|
+
const {dragable} = mixins;
|
|
4
|
+
const {Checkbox, Date_Picker, Text_Input, Text_Field, Dropdown_Menu} = controls;
|
|
5
|
+
const Active_HTML_Document = require('../../../controls/Active_HTML_Document');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BlueSphereRenderer {
|
|
10
|
+
constructor(canvas, opts = {}) {
|
|
11
|
+
this.canvas =
|
|
12
|
+
typeof canvas === "string" ? document.getElementById(canvas) : canvas;
|
|
13
|
+
if (!(this.canvas instanceof HTMLCanvasElement)) {
|
|
14
|
+
throw new Error("Pass a canvas element or its id");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.ctx = this.canvas.getContext("2d");
|
|
18
|
+
this.opts = {
|
|
19
|
+
padding: opts.padding ?? 8,
|
|
20
|
+
color: opts.color ?? "#2d7fff", // base blue
|
|
21
|
+
background: opts.background ?? null, // e.g. "#0a0f1e" or null for transparent
|
|
22
|
+
dpr: window.devicePixelRatio || 1
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
this.resize();
|
|
26
|
+
this.render();
|
|
27
|
+
|
|
28
|
+
// Re-render automatically on viewport resizes.
|
|
29
|
+
this._onResize = () => { this.resize(); this.render(); };
|
|
30
|
+
window.addEventListener("resize", this._onResize, { passive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
destroy() {
|
|
34
|
+
window.removeEventListener("resize", this._onResize);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setColor(hex) {
|
|
38
|
+
this.opts.color = hex;
|
|
39
|
+
this.render();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resize() {
|
|
43
|
+
const dpr = (this.opts.dpr = window.devicePixelRatio || 1);
|
|
44
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
45
|
+
|
|
46
|
+
// If CSS controls the canvas size, use that; otherwise fall back to attributes.
|
|
47
|
+
const displayWidth = rect.width || this.canvas.width;
|
|
48
|
+
const displayHeight = rect.height || this.canvas.height;
|
|
49
|
+
|
|
50
|
+
const w = Math.max(1, Math.round(displayWidth * dpr));
|
|
51
|
+
const h = Math.max(1, Math.round(displayHeight * dpr));
|
|
52
|
+
|
|
53
|
+
if (this.canvas.width !== w || this.canvas.height !== h) {
|
|
54
|
+
this.canvas.width = w;
|
|
55
|
+
this.canvas.height = h;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Scale the drawing coordinates back to CSS pixels.
|
|
59
|
+
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
60
|
+
this.ctx.scale(dpr, dpr);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
render() {
|
|
64
|
+
const ctx = this.ctx;
|
|
65
|
+
|
|
66
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
67
|
+
const width = rect.width || this.canvas.width / this.opts.dpr;
|
|
68
|
+
const height = rect.height || this.canvas.height / this.opts.dpr;
|
|
69
|
+
|
|
70
|
+
// Clear / background
|
|
71
|
+
ctx.clearRect(0, 0, width, height);
|
|
72
|
+
if (this.opts.background) {
|
|
73
|
+
ctx.fillStyle = this.opts.background;
|
|
74
|
+
ctx.fillRect(0, 0, width, height);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const pad = this.opts.padding;
|
|
78
|
+
const r = Math.max(1, Math.min(width, height) / 2 - pad);
|
|
79
|
+
const cx = width / 2;
|
|
80
|
+
const cy = height / 2;
|
|
81
|
+
|
|
82
|
+
// Clip to a circle so the gradient stays perfectly round.
|
|
83
|
+
ctx.save();
|
|
84
|
+
ctx.beginPath();
|
|
85
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
86
|
+
ctx.clip();
|
|
87
|
+
|
|
88
|
+
// Light from top-left: bright highlight near that side, darker on the rim.
|
|
89
|
+
const hx = cx - r * 0.4;
|
|
90
|
+
const hy = cy - r * 0.45;
|
|
91
|
+
const grad = ctx.createRadialGradient(hx, hy, r * 0.15, cx, cy, r);
|
|
92
|
+
|
|
93
|
+
const base = this.opts.color;
|
|
94
|
+
grad.addColorStop(0.0, this._tint(base, 0.35)); // bright highlight
|
|
95
|
+
grad.addColorStop(0.5, base); // mid
|
|
96
|
+
grad.addColorStop(1.0, this._shade(base, 0.45)); // dark rim
|
|
97
|
+
|
|
98
|
+
ctx.fillStyle = grad;
|
|
99
|
+
ctx.fillRect(cx - r, cy - r, r * 2, r * 2);
|
|
100
|
+
ctx.restore();
|
|
101
|
+
|
|
102
|
+
// Soft rim outline
|
|
103
|
+
ctx.beginPath();
|
|
104
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
105
|
+
ctx.strokeStyle = "rgba(0,0,0,0.15)";
|
|
106
|
+
ctx.lineWidth = 1.5;
|
|
107
|
+
ctx.stroke();
|
|
108
|
+
|
|
109
|
+
// Subtle glossy specular highlight
|
|
110
|
+
ctx.save();
|
|
111
|
+
ctx.globalAlpha = 0.18;
|
|
112
|
+
ctx.beginPath();
|
|
113
|
+
ctx.ellipse(cx - r * 0.35, cy - r * 0.43, r * 0.25, r * 0.18, -0.35, 0, Math.PI * 2);
|
|
114
|
+
const gloss = ctx.createRadialGradient(
|
|
115
|
+
cx - r * 0.35, cy - r * 0.43, 0,
|
|
116
|
+
cx - r * 0.35, cy - r * 0.43, r * 0.25
|
|
117
|
+
);
|
|
118
|
+
gloss.addColorStop(0, "rgba(255,255,255,0.9)");
|
|
119
|
+
gloss.addColorStop(1, "rgba(255,255,255,0)");
|
|
120
|
+
ctx.fillStyle = gloss;
|
|
121
|
+
ctx.fill();
|
|
122
|
+
ctx.restore();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- helpers: simple HSL lighten/darken from a hex ----
|
|
126
|
+
|
|
127
|
+
_tint(hex, amount) { // amount in [0..1]
|
|
128
|
+
const { h, s, l } = this._hexToHSL(hex);
|
|
129
|
+
return this._hslToCSS(h, s, Math.min(100, l + amount * 100));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_shade(hex, amount) {
|
|
133
|
+
const { h, s, l } = this._hexToHSL(hex);
|
|
134
|
+
return this._hslToCSS(h, s, Math.max(0, l - amount * 100));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_hexToHSL(hex) {
|
|
138
|
+
let c = hex.replace("#", "");
|
|
139
|
+
if (c.length === 3) c = [...c].map(x => x + x).join("");
|
|
140
|
+
|
|
141
|
+
const r = parseInt(c.slice(0, 2), 16) / 255;
|
|
142
|
+
const g = parseInt(c.slice(2, 4), 16) / 255;
|
|
143
|
+
const b = parseInt(c.slice(4, 6), 16) / 255;
|
|
144
|
+
|
|
145
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
146
|
+
let h, s, l = (max + min) / 2;
|
|
147
|
+
|
|
148
|
+
if (max === min) {
|
|
149
|
+
h = 0; s = 0;
|
|
150
|
+
} else {
|
|
151
|
+
const d = max - min;
|
|
152
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
153
|
+
switch (max) {
|
|
154
|
+
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
|
155
|
+
case g: h = (b - r) / d + 2; break;
|
|
156
|
+
case b: h = (r - g) / d + 4; break;
|
|
157
|
+
}
|
|
158
|
+
h /= 6;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { h: h * 360, s: s * 100, l: l * 100 };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_hslToCSS(h, s, l) {
|
|
165
|
+
return `hsl(${h.toFixed(1)} ${s.toFixed(1)}% ${l.toFixed(1)}%)`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
class Demo_UI extends Active_HTML_Document {
|
|
170
|
+
constructor(spec = {}) {
|
|
171
|
+
spec.__type_name = spec.__type_name || 'demo_ui';
|
|
172
|
+
super(spec);
|
|
173
|
+
const {context} = this;
|
|
174
|
+
if (typeof this.body.add_class === 'function') {
|
|
175
|
+
this.body.add_class('demo-ui');
|
|
176
|
+
}
|
|
177
|
+
const compose = () => {
|
|
178
|
+
const window = new controls.Window({
|
|
179
|
+
context: context,
|
|
180
|
+
title: 'jsgui3-html Dropdown_Menu',
|
|
181
|
+
pos: [5, 5]
|
|
182
|
+
});
|
|
183
|
+
window.size = [480, 400];
|
|
184
|
+
const canvas = new controls.canvas({
|
|
185
|
+
context
|
|
186
|
+
});
|
|
187
|
+
canvas.dom.attributes.id = 'globeCanvas'
|
|
188
|
+
canvas.size = [300, 300];
|
|
189
|
+
window.inner.add(canvas);
|
|
190
|
+
this.body.add(window);
|
|
191
|
+
this._ctrl_fields = this._ctrl_fields || {};
|
|
192
|
+
this._ctrl_fields.canvas = this.canvas = canvas;
|
|
193
|
+
}
|
|
194
|
+
if (!spec.el) {
|
|
195
|
+
compose();
|
|
196
|
+
//this.add_change_listeners();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/*
|
|
200
|
+
add_change_listeners() {
|
|
201
|
+
const {select_options} = this;
|
|
202
|
+
select_options.data.model.on('change', e => {
|
|
203
|
+
console.log('select_options.data.model change e', e);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
*/
|
|
207
|
+
activate() {
|
|
208
|
+
if (!this.__active) {
|
|
209
|
+
super.activate();
|
|
210
|
+
const {context, ti1, ti2} = this;
|
|
211
|
+
//this.add_change_listeners();
|
|
212
|
+
console.log('activate Demo_UI');
|
|
213
|
+
context.on('window-resize', e_resize => {
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const globe = new BlueSphereRenderer("globeCanvas", {
|
|
217
|
+
color: "#1e88e5", // try different blues
|
|
218
|
+
background: null, // or a color like "#0b1020"
|
|
219
|
+
padding: 10
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
Demo_UI.css = `
|
|
225
|
+
* {
|
|
226
|
+
margin: 0;
|
|
227
|
+
padding: 0;
|
|
228
|
+
}
|
|
229
|
+
body {
|
|
230
|
+
overflow-x: hidden;
|
|
231
|
+
overflow-y: hidden;
|
|
232
|
+
background-color: #E0E0E0;
|
|
233
|
+
}
|
|
234
|
+
.demo-ui {
|
|
235
|
+
}
|
|
236
|
+
`;
|
|
237
|
+
controls.Demo_UI = Demo_UI;
|
|
238
|
+
module.exports = jsgui;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const jsgui = require('./client');
|
|
2
|
+
const {Demo_UI} = jsgui.controls;
|
|
3
|
+
const Server = require('../../../server');
|
|
4
|
+
if (require.main === module) {
|
|
5
|
+
const server = new Server({
|
|
6
|
+
Ctrl: Demo_UI,
|
|
7
|
+
debug: true,
|
|
8
|
+
'src_path_client_js': require.resolve('./client.js'),
|
|
9
|
+
});
|
|
10
|
+
console.log('waiting for server ready event');
|
|
11
|
+
server.one('ready', () => {
|
|
12
|
+
console.log('server ready');
|
|
13
|
+
server.start(52000, function (err, cb_start) {
|
|
14
|
+
if (err) {
|
|
15
|
+
throw err;
|
|
16
|
+
} else {
|
|
17
|
+
console.log('server started');
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
})
|
|
21
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const jsgui = require('jsgui3-client');
|
|
5
|
+
const {controls, Control, mixins} = jsgui;
|
|
6
|
+
const {dragable} = mixins;
|
|
7
|
+
const {Checkbox, Date_Picker, Text_Input, Text_Field, Dropdown_Menu} = controls;
|
|
8
|
+
const Active_HTML_Document = require('../../../controls/Active_HTML_Document');
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
// EarthLikeSphereRenderer.js
|
|
12
|
+
// Renders a blue "Earth-like" sphere with sunlit shading and ocean glint.
|
|
13
|
+
// - Proper diffuse + specular lighting from a 3D sun direction vector
|
|
14
|
+
// - Soft day/night terminator
|
|
15
|
+
// - Atmospheric rim, including backlit twilight when the sun is behind Earth
|
|
16
|
+
// - High-DPI aware, resizes with the canvas element
|
|
17
|
+
|
|
18
|
+
class EarthLikeSphereRenderer {
|
|
19
|
+
constructor(canvas, opts = {}) {
|
|
20
|
+
this.canvas =
|
|
21
|
+
typeof canvas === "string" ? document.getElementById(canvas) : canvas;
|
|
22
|
+
if (!(this.canvas instanceof HTMLCanvasElement)) {
|
|
23
|
+
throw new Error("Pass a canvas element or its id");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.ctx = this.canvas.getContext("2d");
|
|
27
|
+
|
|
28
|
+
// Lighting / material defaults tuned for "ocean Earth" look
|
|
29
|
+
this.opts = {
|
|
30
|
+
padding: opts.padding ?? 8,
|
|
31
|
+
background: opts.background ?? null, // e.g. "#071018" or null
|
|
32
|
+
baseColor: opts.baseColor ?? [0.13, 0.42, 0.86], // ocean-ish RGB (linear-ish 0..1)
|
|
33
|
+
ambient: opts.ambient ?? 0.08, // base floor light
|
|
34
|
+
diffuse: opts.diffuse ?? 1.0, // lambertian strength
|
|
35
|
+
specular: opts.specular ?? 0.75, // specular strength (ocean glint)
|
|
36
|
+
shininess: opts.shininess ?? 120.0, // specular tightness (higher = tighter)
|
|
37
|
+
terminatorSoftness: opts.terminatorSoftness ?? 0.08, // soften day/night edge
|
|
38
|
+
atmosphere: opts.atmosphere ?? 0.45, // day-side rim
|
|
39
|
+
backlight: opts.backlight ?? 0.22, // night-side backscatter (sun behind)
|
|
40
|
+
dpr: window.devicePixelRatio || 1,
|
|
41
|
+
quality: Math.min(2, Math.max(0.5, opts.quality ?? 1.0)), // internal sampling scale
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Default sun direction (unit; toward viewer is +Z). This is "above-left-front".
|
|
45
|
+
this.sun = this._normalize([ -0.45, 0.55, 0.65 ]);
|
|
46
|
+
|
|
47
|
+
this.resize();
|
|
48
|
+
this.render();
|
|
49
|
+
|
|
50
|
+
this._onResize = () => { this.resize(); this.render(); };
|
|
51
|
+
window.addEventListener("resize", this._onResize, { passive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
destroy() {
|
|
55
|
+
window.removeEventListener("resize", this._onResize);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---- Public controls ------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set sun direction as a unit vector [x, y, z] in camera coordinates.
|
|
62
|
+
* Axes: +X right, +Y up, +Z toward camera (viewer).
|
|
63
|
+
* Example: front-left-up is [-0.5, 0.5, 0.7]
|
|
64
|
+
*/
|
|
65
|
+
setSunDirection(vec3) {
|
|
66
|
+
this.sun = this._normalize(vec3);
|
|
67
|
+
this.render();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Convenience: set sun from spherical angles (degrees).
|
|
72
|
+
* lonDeg: 0° is toward camera (+Z), +90° is to the right (+X), ±180° is away (-Z)
|
|
73
|
+
* latDeg: +90° straight up (+Y), -90° straight down (-Y)
|
|
74
|
+
*/
|
|
75
|
+
setSunFromSpherical(lonDeg, latDeg) {
|
|
76
|
+
const toRad = Math.PI / 180;
|
|
77
|
+
const lon = lonDeg * toRad;
|
|
78
|
+
const lat = latDeg * toRad;
|
|
79
|
+
const cx = Math.cos(lat), sx = Math.sin(lat);
|
|
80
|
+
const sz = Math.sin(lon), cz = Math.cos(lon);
|
|
81
|
+
this.setSunDirection([ cx * sz, sx, cx * cz ]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Convenience: set sun from azimuth/elevation (degrees) in camera frame.
|
|
86
|
+
* azimuthDeg: 0° = straight toward camera (+Z), 90° = right (+X), 180° = away (-Z)
|
|
87
|
+
* elevationDeg: 0° = horizon plane, +90° = straight up (+Y)
|
|
88
|
+
*/
|
|
89
|
+
setSunFromAzEl(azimuthDeg, elevationDeg) {
|
|
90
|
+
this.setSunFromSpherical(azimuthDeg, elevationDeg);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Optional helper: very rough astronomical position -> direction in an
|
|
95
|
+
* inertial, equatorial-like frame (J2000-ish). For realism you’ll typically
|
|
96
|
+
* rotate this into your camera frame. Good enough as a starting point.
|
|
97
|
+
*/
|
|
98
|
+
setSunApproxFromDateUTC(dateUtc /* Date */) {
|
|
99
|
+
const vEci = this._approxSunEciUnit(dateUtc);
|
|
100
|
+
// Default assumption: camera frame aligned with ECI, viewer on +Z.
|
|
101
|
+
// If you have your own camera orientation, rotate vEci accordingly before setSunDirection.
|
|
102
|
+
this.setSunDirection(vEci);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- Canvas sizing --------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
resize() {
|
|
108
|
+
const dpr = (this.opts.dpr = window.devicePixelRatio || 1);
|
|
109
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
110
|
+
const displayWidth = rect.width || this.canvas.width;
|
|
111
|
+
const displayHeight = rect.height || this.canvas.height;
|
|
112
|
+
|
|
113
|
+
const w = Math.max(1, Math.round(displayWidth * dpr));
|
|
114
|
+
const h = Math.max(1, Math.round(displayHeight * dpr));
|
|
115
|
+
|
|
116
|
+
if (this.canvas.width !== w || this.canvas.height !== h) {
|
|
117
|
+
this.canvas.width = w;
|
|
118
|
+
this.canvas.height = h;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Scale so drawing uses CSS pixels.
|
|
122
|
+
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
123
|
+
this.ctx.scale(dpr, dpr);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- Rendering ------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
render() {
|
|
129
|
+
const ctx = this.ctx;
|
|
130
|
+
|
|
131
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
132
|
+
const width = rect.width || this.canvas.width / this.opts.dpr;
|
|
133
|
+
const height = rect.height || this.canvas.height / this.opts.dpr;
|
|
134
|
+
|
|
135
|
+
// Background
|
|
136
|
+
ctx.clearRect(0, 0, width, height);
|
|
137
|
+
if (this.opts.background) {
|
|
138
|
+
ctx.fillStyle = this.opts.background;
|
|
139
|
+
ctx.fillRect(0, 0, width, height);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Sphere geometry
|
|
143
|
+
const pad = this.opts.padding;
|
|
144
|
+
const r = Math.max(1, Math.min(width, height) / 2 - pad);
|
|
145
|
+
const cx = width / 2;
|
|
146
|
+
const cy = height / 2;
|
|
147
|
+
|
|
148
|
+
// Offscreen buffer in CSS pixels for clean putImageData/drawImage
|
|
149
|
+
// (quality allows oversampling for smoother edges/specular)
|
|
150
|
+
const q = this.opts.quality;
|
|
151
|
+
const d = Math.max(2, Math.floor(2 * r * q));
|
|
152
|
+
const off = this._getOffscreen(d, d);
|
|
153
|
+
const id = off.ctx.createImageData(d, d);
|
|
154
|
+
const data = id.data;
|
|
155
|
+
|
|
156
|
+
// Precompute lighting constants
|
|
157
|
+
const L = this.sun; // [x,y,z], unit
|
|
158
|
+
const V = [0, 0, 1]; // viewer along +Z
|
|
159
|
+
const H = this._normalize([ L[0] + V[0], L[1] + V[1], L[2] + V[2] ]); // Blinn-Phong half-vector
|
|
160
|
+
|
|
161
|
+
const base = this.opts.baseColor; // base ocean color (linear-ish 0..1)
|
|
162
|
+
const amb = this.opts.ambient;
|
|
163
|
+
const kd = this.opts.diffuse;
|
|
164
|
+
const ks = this.opts.specular;
|
|
165
|
+
const shin = this.opts.shininess;
|
|
166
|
+
const termSoft = this.opts.terminatorSoftness;
|
|
167
|
+
const atm = this.opts.atmosphere;
|
|
168
|
+
const back = this.opts.backlight;
|
|
169
|
+
|
|
170
|
+
// Helpers
|
|
171
|
+
const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
|
172
|
+
const smoothstep = (e0, e1, x) => {
|
|
173
|
+
const t = clamp01((x - e0) / (e1 - e0));
|
|
174
|
+
return t * t * (3 - 2 * t);
|
|
175
|
+
};
|
|
176
|
+
const pow = Math.pow;
|
|
177
|
+
|
|
178
|
+
// Loop over the square and shade only pixels within the circle
|
|
179
|
+
const rad = d * 0.5;
|
|
180
|
+
const invR = 1 / rad;
|
|
181
|
+
|
|
182
|
+
let p = 0;
|
|
183
|
+
for (let y = 0; y < d; y++) {
|
|
184
|
+
const vy_screen = (y + 0.5 - rad) * invR; // +down in screen
|
|
185
|
+
for (let x = 0; x < d; x++) {
|
|
186
|
+
const vx = (x + 0.5 - rad) * invR; // +right
|
|
187
|
+
const vy = -vy_screen; // convert to +up for math
|
|
188
|
+
|
|
189
|
+
const rr = vx * vx + vy * vy;
|
|
190
|
+
if (rr > 1.0005) {
|
|
191
|
+
// outside sphere — transparent
|
|
192
|
+
data[p++] = 0; data[p++] = 0; data[p++] = 0; data[p++] = 0;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Anti-aliased edge alpha: blend over ~1 pixel at the rim
|
|
197
|
+
const dist = Math.sqrt(rr);
|
|
198
|
+
const aa = clamp01((1 - dist) * rad); // ~1 pixel smoothing
|
|
199
|
+
const alpha = Math.round(255 * aa);
|
|
200
|
+
|
|
201
|
+
// Sphere normal
|
|
202
|
+
const vz = Math.sqrt(Math.max(0, 1 - rr));
|
|
203
|
+
const N = [vx, vy, vz];
|
|
204
|
+
|
|
205
|
+
// Lighting terms
|
|
206
|
+
const NL = N[0] * L[0] + N[1] * L[1] + N[2] * L[2]; // cosine to light
|
|
207
|
+
const NV = N[2]; // dot(N, V) since V = [0,0,1]
|
|
208
|
+
const NH = N[0] * H[0] + N[1] * H[1] + N[2] * H[2];
|
|
209
|
+
|
|
210
|
+
// Softened terminator so night/day edge isn’t razor-sharp
|
|
211
|
+
const dayMask = smoothstep(-termSoft, termSoft, NL) * clamp01(NL);
|
|
212
|
+
|
|
213
|
+
// Diffuse (Lambert)
|
|
214
|
+
const diff = kd * dayMask;
|
|
215
|
+
|
|
216
|
+
// Specular (Blinn-Phong) — only on lit side
|
|
217
|
+
const spec = NL > 0 ? ks * pow(clamp01(NH), shin) : 0;
|
|
218
|
+
|
|
219
|
+
// Atmospheric rim (day side): stronger near limb (low NV)
|
|
220
|
+
let rimDay = atm * pow(1 - clamp01(NV), 2.4) * clamp01(NL + 0.15);
|
|
221
|
+
|
|
222
|
+
// Backlit rim (sun behind Earth): glow near limb where N faces away from sun
|
|
223
|
+
let rimBack = 0;
|
|
224
|
+
if (L[2] < 0) { // sun somewhere behind viewer
|
|
225
|
+
const away = clamp01(-NL); // how much the normal faces away from light
|
|
226
|
+
rimBack = back * pow(1 - clamp01(NV), 3.2) * away;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Combine lighting
|
|
230
|
+
let rLin = base[0] * (amb + diff) + spec + 0.40 * rimDay + 0.25 * rimBack;
|
|
231
|
+
let gLin = base[1] * (amb + diff) + spec + 0.65 * rimDay + 0.40 * rimBack;
|
|
232
|
+
let bLin = base[2] * (amb + diff) + spec + 1.00 * rimDay + 0.80 * rimBack;
|
|
233
|
+
|
|
234
|
+
// Simple tone & gamma to keep it punchy but not clipped
|
|
235
|
+
const tone = (c) => 1 - Math.exp(-2.6 * c); // filmic-ish
|
|
236
|
+
rLin = tone(rLin); gLin = tone(gLin); bLin = tone(bLin);
|
|
237
|
+
|
|
238
|
+
const r8 = Math.round(clamp01(pow(rLin, 1 / 2.2)) * 255);
|
|
239
|
+
const g8 = Math.round(clamp01(pow(gLin, 1 / 2.2)) * 255);
|
|
240
|
+
const b8 = Math.round(clamp01(pow(bLin, 1 / 2.2)) * 255);
|
|
241
|
+
|
|
242
|
+
data[p++] = r8; data[p++] = g8; data[p++] = b8; data[p++] = alpha;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Blit with masking onto main canvas
|
|
247
|
+
off.ctx.putImageData(id, 0, 0);
|
|
248
|
+
|
|
249
|
+
// Draw at the correct size and position; offscreen is in CSS px already
|
|
250
|
+
const drawSize = (d / q);
|
|
251
|
+
const drawX = cx - drawSize * 0.5;
|
|
252
|
+
const drawY = cy - drawSize * 0.5;
|
|
253
|
+
|
|
254
|
+
// Ensure we stay within a circular footprint on the main canvas
|
|
255
|
+
ctx.save();
|
|
256
|
+
ctx.beginPath();
|
|
257
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
258
|
+
ctx.clip();
|
|
259
|
+
ctx.drawImage(off.canvas, drawX, drawY, drawSize, drawSize);
|
|
260
|
+
ctx.restore();
|
|
261
|
+
|
|
262
|
+
// Optional subtle outline for crispness
|
|
263
|
+
ctx.beginPath();
|
|
264
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
265
|
+
ctx.strokeStyle = "rgba(0,0,0,0.14)";
|
|
266
|
+
ctx.lineWidth = 1.2;
|
|
267
|
+
ctx.stroke();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---- Internals ------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
_getOffscreen(w, h) {
|
|
273
|
+
if (!this._off || this._off.canvas.width !== w || this._off.canvas.height !== h) {
|
|
274
|
+
const c = document.createElement("canvas");
|
|
275
|
+
c.width = w; c.height = h;
|
|
276
|
+
this._off = { canvas: c, ctx: c.getContext("2d") };
|
|
277
|
+
}
|
|
278
|
+
return this._off;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_normalize(v) {
|
|
282
|
+
const n = Math.hypot(v[0], v[1], v[2]) || 1;
|
|
283
|
+
return [ v[0] / n, v[1] / n, v[2] / n ];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Very rough solar position in an Earth-centered, equatorial-like frame.
|
|
287
|
+
// Enough to get a plausible sun direction that varies with date.
|
|
288
|
+
_approxSunEciUnit(dateUtc) {
|
|
289
|
+
// Julian centuries since J2000
|
|
290
|
+
const JD = (dateUtc.getTime() / 86400000) + 2440587.5;
|
|
291
|
+
const n = JD - 2451545.0;
|
|
292
|
+
|
|
293
|
+
// mean anomaly (deg) & mean longitude (deg)
|
|
294
|
+
const g = this._deg2rad((357.529 + 0.98560028 * n) % 360);
|
|
295
|
+
const L = this._deg2rad((280.459 + 0.98564736 * n) % 360);
|
|
296
|
+
|
|
297
|
+
// ecliptic longitude (deg)
|
|
298
|
+
const lambda = L + this._deg2rad(1.915) * Math.sin(g) + this._deg2rad(0.020) * Math.sin(2 * g);
|
|
299
|
+
|
|
300
|
+
// obliquity of the ecliptic (deg)
|
|
301
|
+
const eps = this._deg2rad(23.439 - 0.00000036 * n);
|
|
302
|
+
|
|
303
|
+
// convert to equatorial (RA/Dec) then to Cartesian unit vector
|
|
304
|
+
const x = Math.cos(lambda);
|
|
305
|
+
const y = Math.cos(eps) * Math.sin(lambda);
|
|
306
|
+
const z = Math.sin(eps) * Math.sin(lambda);
|
|
307
|
+
|
|
308
|
+
// This vector points from Earth to Sun. Align camera so +Z ≈ viewer.
|
|
309
|
+
// You may rotate this to match your camera.
|
|
310
|
+
return this._normalize([ x, z, y ]); // a simple axis shuffle to fit our (+X right, +Y up, +Z toward viewer) convention
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_deg2rad(d) { return d * Math.PI / 180; }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class Demo_UI extends Active_HTML_Document {
|
|
318
|
+
constructor(spec = {}) {
|
|
319
|
+
spec.__type_name = spec.__type_name || 'demo_ui';
|
|
320
|
+
super(spec);
|
|
321
|
+
const {context} = this;
|
|
322
|
+
if (typeof this.body.add_class === 'function') {
|
|
323
|
+
this.body.add_class('demo-ui');
|
|
324
|
+
}
|
|
325
|
+
const compose = () => {
|
|
326
|
+
const window = new controls.Window({
|
|
327
|
+
context: context,
|
|
328
|
+
title: 'jsgui3-html Dropdown_Menu',
|
|
329
|
+
pos: [5, 5]
|
|
330
|
+
});
|
|
331
|
+
window.size = [480, 400];
|
|
332
|
+
const canvas = new controls.canvas({
|
|
333
|
+
context
|
|
334
|
+
});
|
|
335
|
+
canvas.dom.attributes.id = 'globeCanvas'
|
|
336
|
+
canvas.size = [300, 300];
|
|
337
|
+
window.inner.add(canvas);
|
|
338
|
+
this.body.add(window);
|
|
339
|
+
this._ctrl_fields = this._ctrl_fields || {};
|
|
340
|
+
this._ctrl_fields.canvas = this.canvas = canvas;
|
|
341
|
+
}
|
|
342
|
+
if (!spec.el) {
|
|
343
|
+
compose();
|
|
344
|
+
//this.add_change_listeners();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/*
|
|
348
|
+
add_change_listeners() {
|
|
349
|
+
const {select_options} = this;
|
|
350
|
+
select_options.data.model.on('change', e => {
|
|
351
|
+
console.log('select_options.data.model change e', e);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
*/
|
|
355
|
+
activate() {
|
|
356
|
+
if (!this.__active) {
|
|
357
|
+
super.activate();
|
|
358
|
+
const {context, ti1, ti2} = this;
|
|
359
|
+
//this.add_change_listeners();
|
|
360
|
+
console.log('activate Demo_UI');
|
|
361
|
+
context.on('window-resize', e_resize => {
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const earth = new EarthLikeSphereRenderer("globeCanvas", {
|
|
365
|
+
background: "#081019",
|
|
366
|
+
quality: 1.25, // 1.0..2.0 (higher = smoother, slower)
|
|
367
|
+
shininess: 140, // tighten/loosen the ocean glint
|
|
368
|
+
atmosphere: 0.5, // rim intensity (day side)
|
|
369
|
+
backlight: 0.25 // twilight rim when sun is behind Earth
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Place sun front-left-up:
|
|
373
|
+
earth.setSunFromSpherical(-35, 25);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
Demo_UI.css = `
|
|
378
|
+
* {
|
|
379
|
+
margin: 0;
|
|
380
|
+
padding: 0;
|
|
381
|
+
}
|
|
382
|
+
body {
|
|
383
|
+
overflow-x: hidden;
|
|
384
|
+
overflow-y: hidden;
|
|
385
|
+
background-color: #E0E0E0;
|
|
386
|
+
}
|
|
387
|
+
.demo-ui {
|
|
388
|
+
}
|
|
389
|
+
`;
|
|
390
|
+
controls.Demo_UI = Demo_UI;
|
|
391
|
+
module.exports = jsgui;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const jsgui = require('./client');
|
|
2
|
+
const {Demo_UI} = jsgui.controls;
|
|
3
|
+
const Server = require('../../../server');
|
|
4
|
+
if (require.main === module) {
|
|
5
|
+
const server = new Server({
|
|
6
|
+
Ctrl: Demo_UI,
|
|
7
|
+
debug: true,
|
|
8
|
+
'src_path_client_js': require.resolve('./client.js'),
|
|
9
|
+
});
|
|
10
|
+
console.log('waiting for server ready event');
|
|
11
|
+
server.one('ready', () => {
|
|
12
|
+
console.log('server ready');
|
|
13
|
+
server.start(52000, function (err, cb_start) {
|
|
14
|
+
if (err) {
|
|
15
|
+
throw err;
|
|
16
|
+
} else {
|
|
17
|
+
console.log('server started');
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
})
|
|
21
|
+
}
|