-
Notifications
You must be signed in to change notification settings - Fork 1
/
main.js
265 lines (235 loc) · 7.74 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
const { app, BrowserWindow, ipcMain, screen } = require('electron');
const path = require('node:path');
const fs = require('node:fs');
/**
* Default content browsing window dimensions
*/
const defaultViewportWidth = 800;
const defaultViewportHeight = 600;
/**
* Current mode
*/
let mode = "classic";
/**
* Scale factor of the primary display.
*/
let scaleFactor;
/**
* Handle to the immersive browser window
*/
let immersiveWindow;
/**
* Handles to the offscreen content windows.
*
* First content window in the list contains the main content being rendered.
* Further content windows contain side content (images or companion page).
*/
let contentWindows = [];
/**
* Number of companion windows (on top of the main one)
*/
const companionContentWindowsNumber = 4;
/**
* Handle to the last painted image in the main offscreen content window.
*
* This last painted image is used to extract rectangles aimed at 3D effects.
*/
let lastPaintedImage;
/**
* Sorted list of links to use to fill side content planes.
* The list is sent by the main content window when the page is done loading.
* It is only useful in "navigation" mode
*/
let sortedLinks;
/**
* Create the immersive browser window
*
* The window renders the a-frame 3D scene that represents the immersive
* experience. The scene typically contains planes used to render web
* content (actually rendered from offscreen content windows).
*/
const createImmersiveWindow = () => {
immersiveWindow = new BrowserWindow({
width: 1024,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
},
titleBarStyle: 'hidden'
});
immersiveWindow.on('closed', _ => {
contentWindows.forEach(win => {
win.destroy();
});
contentWindows = null;
immersiveWindow = null;
});
immersiveWindow.loadFile('browser.html');
};
/**
* Create offscreen content window at the given position in the list
*
* The content window is used to render an actual web page in 2D in the
* background. Whenever the content is painted, a copy of the rendered
* content is sent to the immersive browser window, so that the corresponding
* content plane in the a-frame scene can be updated.
*
* The main content window (position 0) can also apply a 3D effect to a set of
* features (identified through `data-xr-z` attributes in the HTML, and
* effectively identified as rectangles in the painted image. When needed, the
* function sends these rectangles to the immersive browser window so that it
* may convert them to planes in the 3D scene.
*/
const createContentWindow = (position = 0) => {
const options = {
webPreferences: {
offscreen: true
},
show: false,
width: defaultViewportWidth,
height: defaultViewportHeight
};
if (position === 0) {
options.transparent = true;
options.frame = false;
options.webPreferences.preload = path.join(__dirname, 'preload-content.js')
}
const contentWindow = new BrowserWindow(options);
contentWindow.webContents.on('paint', (event, dirty, image) => {
if (!immersiveWindow) {
return;
}
if (position > 0 && mode === "page") {
return;
}
let resized = image;
if (scaleFactor !== 1) {
// Immersive browser window uses the image as texture. The dimensions of
// the texture must be the dimensions of the content window (800x600).
resized = image.resize({
width: Math.floor(image.getSize().width / scaleFactor),
height: Math.floor(image.getSize().height / scaleFactor)
});
}
immersiveWindow.webContents.send('paint', "content" + position, resized);
lastPaintedImage = resized;
});
contentWindow.webContents.setFrameRate(10);
contentWindows[position] = contentWindow;
};
const showMode = newMode => {
mode = newMode;
switch (mode) {
case "classic":
for (let i = 0 ; i < companionContentWindowsNumber; i++) {
contentWindows[i+1].loadURL('about:blank');
}
break;
case "3d":
contentWindows[0].webContents.send('toggle3d');
break;
case "page":
contentWindows[1].loadURL('about:blank');
contentWindows[0].webContents.send('illustrate');
break;
case "navigation":
for (let i = 0 ; i < Math.min(sortedLinks.length, companionContentWindowsNumber); i++) {
contentWindows[i+1].loadURL(sortedLinks[i]);
}
break;
}
};
app.whenReady().then(() => {
// Content window is rendered offscreen, but is still attached to a display.
// This affects the Dots per Inch (DPI) scale used to render the content.
// Let's assume that the window is attached to the primary display.
const primaryDisplay = screen.getPrimaryDisplay();
scaleFactor = primaryDisplay.scaleFactor;
let mode = "classic";
createImmersiveWindow();
for (let i = 0 ; i < 5 ; i++) {
createContentWindow(i);
}
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createImmersiveWindow();
}
});
showMode(mode);
// Proxy console messages sent by browser windows
ipcMain.handle('console', function(e, ...args) {
console.log(...args);
});
// Send viewport geometry of main content plane to immersive browser
// once it's ready to process the message, and load default page.
ipcMain.handle('browserIsLoaded', function(e) {
immersiveWindow.webContents.send('viewportGeometry', {
width: defaultViewportWidth*scaleFactor,
height: defaultViewportHeight*scaleFactor,
scaleFactor
});
if (process.argv[2]) {
contentWindows[0].webContents.send('addUrl', process.argv[2], process.argv[3] ?? 'Requested URL');
}
contentWindows[0].loadFile("home.html");
});
// Proxy click and scroll events between the main content plane in the
// immersive browser window and the related offscreen content window.
ipcMain.handle('sendClick', function(e, x, y) {
contentWindows[0].webContents.send('click', x, y);
});
ipcMain.handle('scroll', (event, delta) => {
contentWindows[0].webContents.send('scroll', delta);
});
// An image needs to be loaded on the side of the main content plane
ipcMain.handle('loadImage', (event, src) => {
immersiveWindow.webContents.send('paint', "content1", src);
});
// Switch to another browsing mode
ipcMain.handle('setMode', (event, mode) => {
console.log('setMode', mode);
showMode(mode);
});
// Received a list of links from the main content window
ipcMain.handle('sortedLinks', (event, links) => {
sortedLinks = Object.keys(links).sort((l1, l2) => links[l1] - links[l2]);
});
// Received a list of features that should be highlighed from the main
// offscreen content window
ipcMain.handle('features', (event, features) => {
for (const feature of features) {
if (feature.src) {
immersiveWindow.webContents.send('paint3d', feature, feature.src);
}
else {
const crop = lastPaintedImage.crop(feature.rect);
immersiveWindow.webContents.send('paint3d', feature, crop.toDataURL());
}
}
});
// Relay the instruction from immersive window to toggle feature's visibility
// in content window.
ipcMain.handle('toggleFeatureInContent', (event, name) => {
contentWindows[0].webContents.send('toggleFeatureInContent', name);
});
// Reset 3D effects whenever the loaded content changes.
ipcMain.handle('beforeunload', _ => {
immersiveWindow.webContents.send('reset3d');
});
// Proxy user actions on buttons between immersive browser window
// and the main offscreen content window
ipcMain.handle('toggle3d', async () => {
showMode("3d");
});
ipcMain.handle('toggle-illustrate', async () => {
showMode("page");
});
ipcMain.handle('toggle-navigate', async () => {
showMode("navigation");
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});