diff --git a/src/playground/playground.sb3 b/src/playground/playground.sb3 new file mode 100644 index 00000000000..66bee4e30ce Binary files /dev/null and b/src/playground/playground.sb3 differ diff --git a/src/playground/standalone-player.html b/src/playground/standalone-player.html new file mode 100644 index 00000000000..30e6c167d86 --- /dev/null +++ b/src/playground/standalone-player.html @@ -0,0 +1,69 @@ + + + + + + + + +
+ + +
+ + + + diff --git a/src/playground/standalone-player.js b/src/playground/standalone-player.js new file mode 100644 index 00000000000..637e12a5c39 --- /dev/null +++ b/src/playground/standalone-player.js @@ -0,0 +1,157 @@ +const AudioEngine = require('scratch-audio'); +const ScratchStorage = require('scratch-storage'); +const ScratchRender = require('scratch-render'); +const ScratchSVGRenderer = require('scratch-svg-renderer'); +const VirtualMachine = require('..'); + +// This file is an example of how to create a standalone, full screen +// minimal scratch player without the editor view. +// This file does not presentally include monitors, which are drawn by the GUI. + +/** + * @param {Asset} projectAsset - calculate a URL for this asset. + * @returns {string} a URL to download a project file. + */ +const projectGetConfig = function (projectAsset) { + return `https://projects.scratch.mit.edu/${projectAsset.assetId}`; +}; + +/** + * @param {Asset} asset - calculate a URL for this asset. + * @returns {string} a URL to download a project asset (PNG, WAV, etc.) + */ +const assetGetConfig = function (asset) { + return `https://assets.scratch.mit.edu/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/`; +}; + +window.onload = function () { + + // Get the project id from the hash, or use the default project. + let projectId; + if (window.location.hash) { + projectId = window.location.hash.substring(1); + } + + // Instantiate the VM. + const vm = new VirtualMachine(); + vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter()); + vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer()); + + // Initialize storage + const storage = new ScratchStorage(); + const AssetType = storage.AssetType; + storage.addWebStore([AssetType.Project], projectGetConfig); + storage.addWebStore([AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], assetGetConfig); + + vm.attachStorage(storage); + + // Compatibility mode will set the frame rate to 30 TPS, + // which is the standard for the scratch player. + vm.setCompatibilityMode(true); + + if (projectId) { + vm.downloadProjectId(projectId); + } else { + // If no project ID is supplied, load a local project + fetch('./playground.sb3').then(response => response.arrayBuffer()) + .then(arrayBuffer => { + vm.loadProject(arrayBuffer); + }); + } + + vm.on('workspaceUpdate', () => { + document.getElementById('overlay').classList.remove('hidden'); + }); + + // Instantiate the renderer and connect it to the VM. + const canvas = document.getElementById('scratch-stage'); + const renderer = new ScratchRender(canvas); + vm.attachRenderer(renderer); + const audioEngine = new AudioEngine(); + vm.attachAudioEngine(audioEngine); + + // Resets size of canvas directly for proper image calcuations + // when the window is resized + const resize = () => { + renderer.resize(canvas.clientWidth, canvas.clientHeight); + }; + window.addEventListener('resize', resize); + + resize(); + + // Start project after green flag clicked and attempt to go + // fullscreen + let attemptFullscreen = Boolean(document.body.requestFullscreen); + document.getElementById('green-flag').addEventListener('click', () => { + document.getElementById('overlay').classList.add('hidden'); + vm.greenFlag(); + if (attemptFullscreen) { + document.body.requestFullscreen(); + attemptFullscreen = false; + } + }); + + // Feed mouse events as VM I/O events. + document.body.addEventListener('mousemove', e => { + const rect = canvas.getBoundingClientRect(); + const coordinates = { + x: e.clientX - rect.left, + y: e.clientY - rect.top, + canvasWidth: rect.width, + canvasHeight: rect.height + }; + vm.postIOData('mouse', coordinates); + }); + canvas.addEventListener('mousedown', e => { + const rect = canvas.getBoundingClientRect(); + const data = { + isDown: true, + x: e.clientX - rect.left, + y: e.clientY - rect.top, + canvasWidth: rect.width, + canvasHeight: rect.height + }; + vm.postIOData('mouse', data); + e.preventDefault(); + }); + canvas.addEventListener('mouseup', e => { + const rect = canvas.getBoundingClientRect(); + const data = { + isDown: false, + x: e.clientX - rect.left, + y: e.clientY - rect.top, + canvasWidth: rect.width, + canvasHeight: rect.height + }; + vm.postIOData('mouse', data); + e.preventDefault(); + }); + + // Feed keyboard events as VM I/O events. + document.body.addEventListener('keydown', e => { + // Don't capture keys intended for Blockly inputs. + if (e.target !== document && e.target !== document.body) { + return; + } + vm.postIOData('keyboard', { + key: e.code, + isDown: true + }); + e.preventDefault(); + }); + document.body.addEventListener('keyup', e => { + // Always capture up events, + // even those that have switched to other targets. + vm.postIOData('keyboard', { + key: e.code, + isDown: false + }); + // E.g., prevent scroll. + if (e.target !== document && e.target !== document.body) { + e.preventDefault(); + } + }); + + // Run threads + vm.start(); +}; diff --git a/webpack.config.js b/webpack.config.js index 9b38815bd61..449c9ffe326 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -88,6 +88,7 @@ module.exports = [ target: 'web', entry: { 'benchmark': './src/playground/benchmark', + 'standalone-player': './src/playground/standalone-player', 'video-sensing-extension-debug': './src/extensions/scratch3_video_sensing/debug' }, output: {