I essentially used turbo_warp packager to make an Electron windows application of a Scratch project and I'd like to know how we can press a button to screenshot the application window to make it a separate image so I can easily save it to my hard disk drive.
Basically, the Scratch project is a random image generator and I'd like to screenshot the image whenever I press a button, so the screenshotted image will appear (either below the application or a pop-up window or anything really). I tried to use html2canvas and Electron's desktop capture. How can I get it working?
P.S.: This is the Scratch project: https://scratch.mit.edu/projects/127701037/
File Main.JS
'use strict';
const {app, BrowserWindow, Menu, shell, screen} = require('electron');
const path = require('path');
const isWindows = process.platform === 'win32';
const isMac = process.platform === 'darwin';
const isLinux = process.platform === 'linux';
if (isMac) {
// TODO
Menu.setApplicationMenu(Menu.buildFromTemplate([
{ role: 'appMenu' },
{ role: 'fileMenu' },
{ role: 'editMenu' },
{ role: 'viewMenu' },
{ role: 'windowMenu' },
{ role: 'help' }
]));
} else {
Menu.setApplicationMenu(null);
}
const isSafeOpenExternal = (url) => {
try {
const parsedUrl = new URL(url);
return parsedUrl.protocol === 'https:' || parsedUrl.protocol === 'http:';
} catch (e) {
// ignore
}
return false;
};
const createWindow = () => {
const options = {
width: 250,
height: 225,
useContentSize: true,
minWidth: 50,
minHeight: 50,
icon: path.resolve('icon.png'),
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true
},
show: false,
backgroundColor: "#000000"
};
const activeScreen = screen.getDisplayNearestPoint(screen.getCursorScreenPoint());
const bounds = activeScreen.workArea;
options.x = bounds.x + ((bounds.width - options.width) / 2);
options.y = bounds.y + ((bounds.height - options.height) / 2);
const window = new BrowserWindow(options);
window.once('ready-to-show', () => {
window.show();
});
window.loadFile('../../index.html');
};
const acquiredLock = app.requestSingleInstanceLock();
if (acquiredLock) {
app.enableSandbox();
app.on('web-contents-created', (event, contents) => {
contents.setWindowOpenHandler((details) => {
if (isSafeOpenExternal(details.url)) {
setImmediate(() => {
shell.openExternal(details.url);
});
}
return {action: 'deny'};
});
contents.on('will-navigate', (e, url) => {
e.preventDefault();
if (isSafeOpenExternal(url)) {
shell.openExternal(url);
}
});
});
app.on('window-all-closed', () => {
app.quit();
});
app.on('second-instance', () => {
createWindow();
});
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
} else {
app.quit();
}
File index.html
<!DOCTYPE html>
<!-- Created with https://packager.turbowarp.org/ -->
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<!-- We only include this to explicitly loosen the CSP of various packager environments. It does not provide any security. -->
<meta http-equiv="Content-Security-Policy" content="default-src * 'self' 'unsafe-inline' 'unsafe-eval' data: blob:">
<title>Map Maker Final</title>
<style>
body {
color: #ffffff;
font-family: sans-serif;
overflow: hidden;
margin: 0;
padding: 0;
}
:root, body.is-fullscreen {
background-color: #000000;
}
[hidden] {
display: none !important;
}
h1 {
font-weight: normal;
}
a {
color: inherit;
text-decoration: underline;
cursor: pointer;
}
#app, #loading, #error, #launch {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
cursor: default;
user-select: none;
-webkit-user-select: none;
background-color: #000000;
}
#launch {
background-color: rgba(0, 0, 0, 0.7);
cursor: pointer;
}
.green-flag {
width: 80px;
height: 80px;
padding: 16px;
border-radius: 100%;
background: rgba(255, 255, 255, 0.75);
border: 3px solid hsla(0, 100%, 100%, 1);
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
#loading {
}
.progress-bar-outer {
border: 1px solid currentColor;
height: 10px;
width: 200px;
max-width: 200px;
}
.progress-bar-inner {
height: 100%;
width: 0;
background-color: currentColor;
}
.loading-text {
font-weight: normal;
font-size: 36px;
margin: 0 0 16px;
}
.loading-image {
margin: 0 0 16px;
}
#error-message, #error-stack {
font-family: monospace;
max-width: 600px;
white-space: pre-wrap;
user-select: text;
-webkit-user-select: text;
}
#error-stack {
text-align: left;
max-height: 200px;
overflow: auto;
}
.control-button {
width: 2rem;
height: 2rem;
padding: 0.375rem;
border-radius: 0.25rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
user-select: none;
-webkit-user-select: none;
cursor: pointer;
border: 0;
border-radius: 4px;
}
.control-button:hover {
background: #ff4c4c26;
}
.control-button.active {
background: #ff4c4c59;
}
.fullscreen-button {
background: white !important;
}
.standalone-fullscreen-button {
position: absolute;
top: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 0 0 0 4px;
padding: 4px;
cursor: pointer;
}
.sc-canvas {
cursor: auto;
}
</style>
<meta name="theme-color" content="#000000">
<!-- no favicon -->
</head>
<body>
<noscript>Enable JavaScript</noscript>
<div id="app"></div>
<div id="launch" class="screen" hidden title="Click to start">
<div class="green-flag">
<svg viewBox="0 0 16.63 17.5" width="42" height="44">
<defs><style>.cls-1,.cls-2{fill:#4cbf56;stroke:#45993d;stroke-linecap:round;stroke-linejoin:round;}.cls-2{stroke-width:1.5px;}</style></defs>
<path class="cls-1" d="M.75,2A6.44,6.44,0,0,1,8.44,2h0a6.44,6.44,0,0,0,7.69,0V12.4a6.44,6.44,0,0,1-7.69,0h0a6.44,6.44,0,0,0-7.69,0"/>
<line class="cls-2" x1="0.75" y1="16.75" x2="0.75" y2="0.75"/>
</svg>
</div>
</div>
<div id="loading" class="screen">
<div class="progress-bar-outer"><div class="progress-bar-inner" id="loading-inner"></div></div>
</div>
<div id="error" class="screen" hidden>
<h1>Error</h1>
<details>
<summary id="error-message"></summary>
<p id="error-stack"></p>
</details>
</div>
<script src="script.js"></script>
<script>
const appElement = document.getElementById('app');
const launchScreen = document.getElementById('launch');
const loadingScreen = document.getElementById('loading');
const loadingInner = document.getElementById('loading-inner');
const errorScreen = document.getElementById('error');
const errorScreenMessage = document.getElementById('error-message');
const errorScreenStack = document.getElementById('error-stack');
const handleError = (error) => {
console.error(error);
if (!errorScreen.hidden) return;
errorScreen.hidden = false;
errorScreenMessage.textContent = '' + error;
let debug = error && error.stack || 'no stack';
debug += '\nUser agent: ' + navigator.userAgent;
errorScreenStack.textContent = debug;
};
const setProgress = (progress) => {
if (loadingInner) loadingInner.style.width = progress * 100 + '%';
};
try {
const scaffolding = new Scaffolding.Scaffolding();
scaffolding.width = 250;
scaffolding.height = 225;
scaffolding.resizeToFill = false;
scaffolding.setup();
scaffolding.appendTo(appElement);
// Expose values expected by third-party plugins
window.scaffolding = scaffolding;
window.vm = scaffolding.vm;
const {storage, vm} = scaffolding;
storage.addWebStore(
[storage.AssetType.ImageVector, storage.AssetType.ImageBitmap, storage.AssetType.Sound],
(asset) => new URL('./assets/' + asset.assetId + '.' + asset.dataFormat, location).href
);
storage.onprogress = (total, loaded) => {
setProgress(0.2 + (loaded / total) * 0.8);
};
setProgress(0.1);
scaffolding.setUsername("Smartplus50".replace(/#/g, () => Math.floor(Math.random() * 10)));
scaffolding.setAccentColor("#ff4c4c");
scaffolding.addCloudProvider(new Scaffolding.Cloud.WebSocketProvider("wss://clouddata.turbowarp.org", "586286535"));
if (false) {
const greenFlagButton = document.createElement('img');
greenFlagButton.src = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16.63 17.5"><path d="M.75 2a6.44 6.44 0 017.69 0h0a6.44 6.44 0 007.69 0v10.4a6.44 6.44 0 01-7.69 0h0a6.44 6.44 0 00-7.69 0" fill="#4cbf56" stroke="#45993d" stroke-linecap="round" stroke-linejoin="round"/><path stroke-width="1.5" fill="#4cbf56" stroke="#45993d" stroke-linecap="round" stroke-linejoin="round" d="M.75 16.75v-16"/></svg>');
greenFlagButton.className = 'control-button';
greenFlagButton.addEventListener('click', () => {
scaffolding.greenFlag();
});
scaffolding.addEventListener('PROJECT_RUN_START', () => {
greenFlagButton.classList.add('active');
});
scaffolding.addEventListener('PROJECT_RUN_STOP', () => {
greenFlagButton.classList.remove('active');
});
scaffolding.addControlButton({
element: greenFlagButton,
where: 'top-left'
});
}
if (false) {
const stopAllButton = document.createElement('img');
stopAllButton.src = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><path fill="#ec5959" stroke="#b84848" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M4.3.5h5.4l3.8 3.8v5.4l-3.8 3.8H4.3L.5 9.7V4.3z"/></svg>');
stopAllButton.className = 'control-button';
stopAllButton.addEventListener('click', () => {
scaffolding.stopAll();
});
scaffolding.addControlButton({
element: stopAllButton,
where: 'top-left'
});
}
if (false && (document.fullscreenEnabled || document.webkitFullscreenEnabled)) {
let isFullScreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
const fullscreenButton = document.createElement('img');
fullscreenButton.className = 'control-button fullscreen-button';
fullscreenButton.addEventListener('click', () => {
if (isFullScreen) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
} else {
if (document.body.requestFullscreen) {
document.body.requestFullscreen();
} else if (document.body.webkitRequestFullscreen) {
document.body.webkitRequestFullscreen();
}
}
});
const otherControlsExist = false;
const fillColor = otherControlsExist ? '#575E75' : '#ffffff';
const updateFullScreen = () => {
isFullScreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
document.body.classList.toggle('is-fullscreen', isFullScreen);
if (isFullScreen) {
fullscreenButton.src = 'data:image/svg+xml,' + encodeURIComponent('<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><g fill="' + fillColor + '" fill-rule="evenodd"><path d="M12.662 3.65l.89.891 3.133-2.374a.815.815 0 011.15.165.819.819 0 010 .986L15.467 6.46l.867.871c.25.25.072.664-.269.664L12.388 8A.397.397 0 0112 7.611V3.92c0-.341.418-.514.662-.27M7.338 16.35l-.89-.89-3.133 2.374a.817.817 0 01-1.15-.166.819.819 0 010-.985l2.37-3.143-.87-.871a.387.387 0 01.27-.664L7.612 12a.397.397 0 01.388.389v3.692a.387.387 0 01-.662.27M7.338 3.65l-.89.891-3.133-2.374a.815.815 0 00-1.15.165.819.819 0 000 .986l2.37 3.142-.87.871a.387.387 0 00.27.664L7.612 8A.397.397 0 008 7.611V3.92a.387.387 0 00-.662-.27M12.662 16.35l.89-.89 3.133 2.374a.817.817 0 001.15-.166.819.819 0 000-.985l-2.368-3.143.867-.871a.387.387 0 00-.269-.664L12.388 12a.397.397 0 00-.388.389v3.692c0 .342.418.514.662.27"/></g></svg>');
} else {
fullscreenButton.src = 'data:image/svg+xml,' + encodeURIComponent('<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><g fill="' + fillColor + '" fill-rule="evenodd"><path d="M16.338 7.35l-.89-.891-3.133 2.374a.815.815 0 01-1.15-.165.819.819 0 010-.986l2.368-3.142-.867-.871a.387.387 0 01.269-.664L16.612 3a.397.397 0 01.388.389V7.08a.387.387 0 01-.662.27M3.662 12.65l.89.89 3.133-2.374a.817.817 0 011.15.166.819.819 0 010 .985l-2.37 3.143.87.871c.248.25.071.664-.27.664L3.388 17A.397.397 0 013 16.611V12.92c0-.342.418-.514.662-.27M3.662 7.35l.89-.891 3.133 2.374a.815.815 0 001.15-.165.819.819 0 000-.986L6.465 4.54l.87-.871a.387.387 0 00-.27-.664L3.388 3A.397.397 0 003 3.389V7.08c0 .341.418.514.662.27M16.338 12.65l-.89.89-3.133-2.374a.817.817 0 00-1.15.166.819.819 0 000 .985l2.368 3.143-.867.871a.387.387 0 00.269.664l3.677.005a.397.397 0 00.388-.389V12.92a.387.387 0 00-.662-.27"/></g></svg>');
}
};
updateFullScreen();
document.addEventListener('fullscreenchange', updateFullScreen);
document.addEventListener('webkitfullscreenchange', updateFullScreen);
if (otherControlsExist) {
fullscreenButton.className = 'control-button fullscreen-button';
scaffolding.addControlButton({
element: fullscreenButton,
where: 'top-right'
});
} else {
fullscreenButton.className = 'standalone-fullscreen-button';
document.body.appendChild(fullscreenButton);
}
}
vm.setTurboMode(true);
if (vm.setInterpolation) vm.setInterpolation(true);
if (vm.setFramerate) vm.setFramerate(60);
if (vm.renderer.setUseHighQualityRender) vm.renderer.setUseHighQualityRender(false);
if (vm.setRuntimeOptions) vm.setRuntimeOptions({
fencing: false,
miscLimits: false,
maxClones: 9999999999,
});
if (vm.setCompilerOptions) vm.setCompilerOptions({
enabled: true,
warpTimer: true
});
if (typeof ScaffoldingAddons !== 'undefined') {
ScaffoldingAddons.run(scaffolding, {"gamepad":false,"pointerlock":false,"specialCloudBehaviors":false});
}
for (const extension of []) {
vm.extensionManager.loadExtensionURL(extension);
}
} catch (e) {
handleError(e);
}
// NW.js hook
if (typeof nw !== 'undefined') {
const win = nw.Window.get();
win.on('new-win-policy', (frame, url, policy) => {
policy.ignore();
nw.Shell.openExternal(url);
});
win.on('navigation', (frame, url, policy) => {
policy.ignore();
nw.Shell.openExternal(url);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && document.fullscreenElement) {
document.exitFullscreen();
}
});
}
// Electron hook
if (true) {
document.addEventListener('keydown', (e) => {
if (e.key === 'F11') {
e.preventDefault();
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.body.requestFullscreen();
}
}
});
}
</script>
<script>
const getProjectData = () => new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
resolve(xhr.response);
};
xhr.onerror = () => {
if (location.protocol === 'file:') {
reject(new Error('Zip environment must be used from a website, not from a file URL.'));
} else {
reject(new Error('Request to load project data failed.'));
}
};
xhr.onprogress = (e) => {
if (e.lengthComputable) {
setProgress(0.1 + (e.loaded / e.total) * 0.1);
}
};
xhr.responseType = 'arraybuffer';
xhr.open("GET", "./assets/project.json");
xhr.send();
});
</script>
<script>
const run = async () => {
const projectData = await getProjectData();
await scaffolding.loadProject(projectData);
setProgress(1);
loadingScreen.hidden = true;
if (true) {
scaffolding.start();
} else {
launchScreen.hidden = false;
launchScreen.addEventListener('click', () => {
launchScreen.hidden = true;
scaffolding.start();
});
launchScreen.focus();
}
};
run().catch(handleError);
</script>
</body>
</html>