From 485c2b985454548e270f637921a168291f2407ca Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 6 Aug 2023 01:23:01 +0100 Subject: [PATCH 01/34] Add files via upload --- extensions/Lily/Video.js | 209 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 extensions/Lily/Video.js diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js new file mode 100644 index 0000000000..3bff3c97b2 --- /dev/null +++ b/extensions/Lily/Video.js @@ -0,0 +1,209 @@ +(function (Scratch) { + 'use strict'; + + const vm = Scratch.vm; + const runtime = vm.runtime; + const canvas = vm.renderer.canvas; + const Cast = Scratch.Cast; + + class Video { + constructor() { + this.video = document.createElement('video'); + + this.video.style.position = 'absolute'; + this.video.style.visibility = 'hidden'; + this.video.style.transformOrigin = 'center center'; + canvas.parentElement.prepend(this.video); + + const adjustSize = (width, height) => { + this.video.style.width = `${(width / runtime.stageWidth) * 100}%`; + this.video.style.height = `${(height / runtime.stageHeight) * 100}%`; + } + + runtime.on('STAGE_SIZE_CHANGED', () => adjustSize(runtime.stageWidth, runtime.stageHeight)); + adjustSize(runtime.stageWidth, runtime.stageHeight); + } + getInfo() { + return { + id: 'lmsVideo', + color1: '#557882', + name: 'Video', + blocks: [ + { + opcode: 'setVideoURL', + blockType: Scratch.BlockType.COMMAND, + text: 'set video URL to [URL]', + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: '' + } + } + }, + { + opcode: 'getVideoURL', + blockType: Scratch.BlockType.REPORTER, + text: 'current video URL' + }, + '---', + { + opcode: 'startVideo', + blockType: Scratch.BlockType.COMMAND, + text: 'start video at [DURATION] seconds', + arguments: { + DURATION: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0 + } + } + }, + { + opcode: 'getCurrentTime', + blockType: Scratch.BlockType.REPORTER, + text: 'current time' + }, + '---', + { + opcode: 'pauseVideo', + blockType: Scratch.BlockType.COMMAND, + text: 'pause video' + }, + { + opcode: 'resumeVideo', + blockType: Scratch.BlockType.COMMAND, + text: 'resume video' + }, + { + opcode: 'showVideo', + blockType: Scratch.BlockType.COMMAND, + text: 'show video' + }, + { + opcode: 'hideVideo', + blockType: Scratch.BlockType.COMMAND, + text: 'hide video' + }, + { + opcode: 'getState', + blockType: Scratch.BlockType.BOOLEAN, + text: 'video is [STATE]?', + arguments: { + STATE: { + type: Scratch.ArgumentType.STRING, + menu: 'state' + } + } + }, + '---', + { + opcode: 'setOptionOnVideo', + blockType: Scratch.BlockType.COMMAND, + text: 'set [OPTION] on video to [TOGGLE]', + arguments: { + OPTION: { + type: Scratch.ArgumentType.STRING, + menu: 'options' + }, + TOGGLE: { + type: Scratch.ArgumentType.STRING, + menu: 'toggle' + } + } + }, + { + opcode: 'getOptionIsEnabled', + blockType: Scratch.BlockType.BOOLEAN, + text: '[OPTION] is enabled?', + arguments: { + OPTION: { + type: Scratch.ArgumentType.STRING, + menu: 'options' + } + } + } + ], + menus: { + options: { + acceptReporters: true, + items: ['controls', 'loop'] + }, + state: { + acceptReporters: true, + items: ['playing', 'paused', 'visible', 'hidden'] + }, + toggle: { + acceptReporters: true, + items: ['enabled', 'disabled'] + } + } + }; + } + + setVideoURL(args) { + this.video.src = Cast.toString(args.URL); + } + + getVideoURL() { + return Cast.toString(this.video.src); + } + + startVideo(args) { + const time = Cast.toNumber(args.DURATION); + this.video.currentTime = time; + this.video.play(); + this.video.style.visibility = 'visible'; + } + + getCurrentTime() { + return Math.round(Cast.toNumber(this.video.currentTime) * 1000) / 1000; + } + + getState(args) { + switch(args.STATE) { + case('paused'): return this.video.paused; + case('playing'): return !this.video.paused; + case('visible'): return this.video.style.visibility === 'visible'; + case('hidden'): return this.video.style.visibility === 'hidden'; + } + } + + pauseVideo() { + this.video.pause(); + } + + resumeVideo() { + if (this.video.paused) this.video.play(); + } + + getIsPaused() { + return this.video.paused; + } + + showVideo() { + this.video.style.visibility = 'visible'; + } + + hideVideo() { + this.video.style.visibility = 'hidden'; + } + + setOptionOnVideo(args) { + const option = Cast.toString(args.OPTION); + const toggle = Cast.toString(args.TOGGLE); + + switch(option) { + case('controls'): this.video.controls = !!(toggle == 'enabled'); + case('loop'): this.video.loop = !!(toggle == 'enabled'); + } + } + + getOptionIsEnabled(args) { + const option = Cast.toString(args.OPTION); + switch(option) { + case('controls'): return this.video.controls; + case('loop'): return this.video.loop; + } + } + } + Scratch.extensions.register(new Video()); +})(Scratch); \ No newline at end of file From 88987693cc30b80c61a5da0726c81c084d12b2be Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 6 Aug 2023 01:23:28 +0100 Subject: [PATCH 02/34] Add files via upload --- images/Lily/Video.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 images/Lily/Video.svg diff --git a/images/Lily/Video.svg b/images/Lily/Video.svg new file mode 100644 index 0000000000..fcc3cbfab7 --- /dev/null +++ b/images/Lily/Video.svg @@ -0,0 +1 @@ + \ No newline at end of file From a006c8c199238f535de0e02517762e912551a3c5 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 6 Aug 2023 01:25:10 +0100 Subject: [PATCH 03/34] Update index.ejs --- website/index.ejs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/index.ejs b/website/index.ejs index da2953cdf6..d15e470b3b 100644 --- a/website/index.ejs +++ b/website/index.ejs @@ -470,6 +470,12 @@

Play sounds from URLs.

+
+ <%- banner('Lily/Video') %> +

Video

+

Play videos from URLs.

+
+
<%- banner('Xeltalliv/clippingblending') %>

Clipping & Blending

From d94d67c5c7135107fabb0b4d7bd2de7d86ff9f42 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 6 Aug 2023 01:25:40 +0100 Subject: [PATCH 04/34] Update README.md --- images/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/images/README.md b/images/README.md index 7495122b82..b98c23dc6c 100644 --- a/images/README.md +++ b/images/README.md @@ -247,3 +247,6 @@ All images in this folder are licensed under the [GNU General Public License ver ## veggiecan/LongmanDictionary.svg - Created by [veggiecan](https://github.com/veggiecan0419) - The ship is based on [this](https://www.ldoceonline.com/external/images/logo_home_smartphone.svg?version=1.2.61) logo from the [ldoceonline](https://www.ldoceonline.com/) website + +## Lily/Video.svg + - Created by [@LilyMakesThings](https://github.com/LilyMakesThings) in https://github.com/TurboWarp/extensions/pull/656 From a5c131e7fc58c51589042beca0b4b5c89fc61ebb Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 6 Aug 2023 01:31:37 +0100 Subject: [PATCH 05/34] shut up, eslint --- extensions/Lily/Video.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 3bff3c97b2..b1adb82605 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -18,7 +18,7 @@ const adjustSize = (width, height) => { this.video.style.width = `${(width / runtime.stageWidth) * 100}%`; this.video.style.height = `${(height / runtime.stageHeight) * 100}%`; - } + }; runtime.on('STAGE_SIZE_CHANGED', () => adjustSize(runtime.stageWidth, runtime.stageHeight)); adjustSize(runtime.stageWidth, runtime.stageHeight); @@ -88,7 +88,7 @@ blockType: Scratch.BlockType.BOOLEAN, text: 'video is [STATE]?', arguments: { - STATE: { + STATE: { type: Scratch.ArgumentType.STRING, menu: 'state' } @@ -159,11 +159,11 @@ } getState(args) { - switch(args.STATE) { - case('paused'): return this.video.paused; - case('playing'): return !this.video.paused; - case('visible'): return this.video.style.visibility === 'visible'; - case('hidden'): return this.video.style.visibility === 'hidden'; + switch (args.STATE) { + case ('paused'): return this.video.paused; + case ('playing'): return !this.video.paused; + case ('visible'): return this.video.style.visibility === 'visible'; + case ('hidden'): return this.video.style.visibility === 'hidden'; } } @@ -191,19 +191,19 @@ const option = Cast.toString(args.OPTION); const toggle = Cast.toString(args.TOGGLE); - switch(option) { - case('controls'): this.video.controls = !!(toggle == 'enabled'); - case('loop'): this.video.loop = !!(toggle == 'enabled'); + switch (option) { + case ('controls'): return this.video.controls = !!(toggle == 'enabled'); + case ('loop'): return this.video.loop = !!(toggle == 'enabled'); } } getOptionIsEnabled(args) { const option = Cast.toString(args.OPTION); - switch(option) { - case('controls'): return this.video.controls; - case('loop'): return this.video.loop; + switch (option) { + case ('controls'): return this.video.controls; + case ('loop'): return this.video.loop; } } } Scratch.extensions.register(new Video()); -})(Scratch); \ No newline at end of file +})(Scratch); From 2c8c2c3f52b3f1d7517e66889b7ae9140b8649b4 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 13 Aug 2023 06:09:36 +0100 Subject: [PATCH 06/34] Update Video.js --- extensions/Lily/Video.js | 87 ++++++++++------------------------------ 1 file changed, 21 insertions(+), 66 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index b1adb82605..3f177cb98f 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -3,25 +3,26 @@ const vm = Scratch.vm; const runtime = vm.runtime; - const canvas = vm.renderer.canvas; + const canvas = runtime.renderer.canvas; const Cast = Scratch.Cast; class Video { constructor() { this.video = document.createElement('video'); - - this.video.style.position = 'absolute'; - this.video.style.visibility = 'hidden'; - this.video.style.transformOrigin = 'center center'; - canvas.parentElement.prepend(this.video); - - const adjustSize = (width, height) => { - this.video.style.width = `${(width / runtime.stageWidth) * 100}%`; - this.video.style.height = `${(height / runtime.stageHeight) * 100}%`; - }; - - runtime.on('STAGE_SIZE_CHANGED', () => adjustSize(runtime.stageWidth, runtime.stageHeight)); - adjustSize(runtime.stageWidth, runtime.stageHeight); + this.video.width = 480; + this.video.height = 360; + this.video.crossOrigin = 'anonymous'; + + runtime.on('BEFORE_EXECUTE', () => { + /* Scratch Addons volume slider moment */ + this.video.volume = runtime.audioEngine.inputNode.gain.value; + + const target = runtime.targets[0]; + const drawableID = target.drawableID; + const skinId = runtime.renderer._allDrawables[drawableID].skin._id; + + vm.renderer.updateBitmapSkin(skinId, this.video, 2); + }); } getInfo() { return { @@ -73,16 +74,6 @@ blockType: Scratch.BlockType.COMMAND, text: 'resume video' }, - { - opcode: 'showVideo', - blockType: Scratch.BlockType.COMMAND, - text: 'show video' - }, - { - opcode: 'hideVideo', - blockType: Scratch.BlockType.COMMAND, - text: 'hide video' - }, { opcode: 'getState', blockType: Scratch.BlockType.BOOLEAN, @@ -96,30 +87,9 @@ }, '---', { - opcode: 'setOptionOnVideo', + opcode: 'testBlock', blockType: Scratch.BlockType.COMMAND, - text: 'set [OPTION] on video to [TOGGLE]', - arguments: { - OPTION: { - type: Scratch.ArgumentType.STRING, - menu: 'options' - }, - TOGGLE: { - type: Scratch.ArgumentType.STRING, - menu: 'toggle' - } - } - }, - { - opcode: 'getOptionIsEnabled', - blockType: Scratch.BlockType.BOOLEAN, - text: '[OPTION] is enabled?', - arguments: { - OPTION: { - type: Scratch.ArgumentType.STRING, - menu: 'options' - } - } + text: 'test' } ], menus: { @@ -141,6 +111,7 @@ setVideoURL(args) { this.video.src = Cast.toString(args.URL); + this.video.currentTime = 0; } getVideoURL() { @@ -151,7 +122,6 @@ const time = Cast.toNumber(args.DURATION); this.video.currentTime = time; this.video.play(); - this.video.style.visibility = 'visible'; } getCurrentTime() { @@ -179,24 +149,6 @@ return this.video.paused; } - showVideo() { - this.video.style.visibility = 'visible'; - } - - hideVideo() { - this.video.style.visibility = 'hidden'; - } - - setOptionOnVideo(args) { - const option = Cast.toString(args.OPTION); - const toggle = Cast.toString(args.TOGGLE); - - switch (option) { - case ('controls'): return this.video.controls = !!(toggle == 'enabled'); - case ('loop'): return this.video.loop = !!(toggle == 'enabled'); - } - } - getOptionIsEnabled(args) { const option = Cast.toString(args.OPTION); switch (option) { @@ -204,6 +156,9 @@ case ('loop'): return this.video.loop; } } + + testBlock(args, util) { + } } Scratch.extensions.register(new Video()); })(Scratch); From 32853282e4e606a3dc45139f197fe8fe6ebd1da3 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 13 Aug 2023 19:25:57 +0100 Subject: [PATCH 07/34] Update Video.js --- extensions/Lily/Video.js | 274 +++++++++++++++++++++++++++++---------- 1 file changed, 209 insertions(+), 65 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 3f177cb98f..b21f0ff5f1 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -3,25 +3,28 @@ const vm = Scratch.vm; const runtime = vm.runtime; - const canvas = runtime.renderer.canvas; const Cast = Scratch.Cast; class Video { constructor() { - this.video = document.createElement('video'); - this.video.width = 480; - this.video.height = 360; - this.video.crossOrigin = 'anonymous'; + this.videos = []; + this.targets = []; runtime.on('BEFORE_EXECUTE', () => { - /* Scratch Addons volume slider moment */ - this.video.volume = runtime.audioEngine.inputNode.gain.value; - - const target = runtime.targets[0]; - const drawableID = target.drawableID; - const skinId = runtime.renderer._allDrawables[drawableID].skin._id; - - vm.renderer.updateBitmapSkin(skinId, this.video, 2); + for (const name of Object.keys(this.videos)) { + const video = this.videos[name]; + video.volume = runtime.audioEngine.inputNode.gain.value; + } + + for (const id of Object.keys(this.targets)) { + const target = this.targets[id].target; + const drawableID = target.drawableID; + + const skinId = runtime.renderer._allDrawables[drawableID].skin._id; + const video = this.videos[this.targets[id].videoName]; + + vm.renderer.updateBitmapSkin(skinId, video, 1); + } }); } getInfo() { @@ -31,27 +34,83 @@ name: 'Video', blocks: [ { - opcode: 'setVideoURL', + opcode: 'loadVideoURL', blockType: Scratch.BlockType.COMMAND, - text: 'set video URL to [URL]', + text: 'load video from URL [URL] as [NAME]', arguments: { URL: { type: Scratch.ArgumentType.STRING, defaultValue: '' + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my video' + } + } + }, + { + opcode: 'deleteVideoURL', + blockType: Scratch.BlockType.COMMAND, + text: 'delete video [NAME]', + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my video' } } }, + '---', { - opcode: 'getVideoURL', + opcode: 'showVideo', + blockType: Scratch.BlockType.COMMAND, + text: 'show video [NAME] on [TARGET]', + arguments: { + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: 'targets' + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my video' + }, + } + }, + { + opcode: 'stopShowingVideo', + blockType: Scratch.BlockType.COMMAND, + text: 'stop showing video on [TARGET]', + arguments: { + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: 'targets' + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my video' + }, + } + }, + { + opcode: 'getCurrentVideo', blockType: Scratch.BlockType.REPORTER, - text: 'current video URL' + text: 'current video on [TARGET]', + arguments: { + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: 'targets' + } + } }, '---', { opcode: 'startVideo', blockType: Scratch.BlockType.COMMAND, - text: 'start video at [DURATION] seconds', + text: 'start video [NAME] at [DURATION] seconds', arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my video' + }, DURATION: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 @@ -61,104 +120,189 @@ { opcode: 'getCurrentTime', blockType: Scratch.BlockType.REPORTER, - text: 'current time' + text: 'current time of video [NAME]', + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my video' + } + } }, '---', { opcode: 'pauseVideo', blockType: Scratch.BlockType.COMMAND, - text: 'pause video' + text: 'pause video [NAME]', + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my video' + } + } }, { opcode: 'resumeVideo', blockType: Scratch.BlockType.COMMAND, - text: 'resume video' + text: 'resume video [NAME]', + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my video' + } + } }, { opcode: 'getState', blockType: Scratch.BlockType.BOOLEAN, - text: 'video is [STATE]?', + text: 'video [NAME] is [STATE]?', arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my video' + }, STATE: { type: Scratch.ArgumentType.STRING, menu: 'state' } } - }, - '---', - { - opcode: 'testBlock', - blockType: Scratch.BlockType.COMMAND, - text: 'test' } ], menus: { - options: { + targets: { acceptReporters: true, - items: ['controls', 'loop'] + items: '_getTargets' }, state: { acceptReporters: true, - items: ['playing', 'paused', 'visible', 'hidden'] - }, - toggle: { - acceptReporters: true, - items: ['enabled', 'disabled'] + items: ['playing', 'paused'] } } }; } - setVideoURL(args) { - this.video.src = Cast.toString(args.URL); - this.video.currentTime = 0; + loadVideoURL(args) { + const videoName = Cast.toString(args.NAME); + const url = Cast.toString(args.URL); + + this.videos[videoName] = document.createElement('video'); + this.videos[videoName].width = 480; + this.videos[videoName].height = 360; + this.videos[videoName].crossOrigin = 'anonymous'; + + // To-do : Some urls can't be loaded by the renderer, how can we detect that? + + this.videos[videoName].src = url; + this.videos[videoName].currentTime = 0; } - getVideoURL() { - return Cast.toString(this.video.src); + deleteVideoURL(args) { + const name = Cast.toString(args.NAME); + Reflect.deleteProperty(this.videos, name); + + // To-do : reset the targets with the video + } + + showVideo(args, util) { + const targetName = Cast.toString(args.TARGET); + const videoName = Cast.toString(args.NAME); + const target = this._getTargetFromMenu(targetName, util); + if (!target) return; + + const targetId = target.id; + this.targets[targetId] = { + target: target, + videoName: videoName + }; + } + + stopShowingVideo(args, util) { + const targetName = Cast.toString(args.TARGET); + const target = this._getTargetFromMenu(targetName, util); + if (!target) return; + + const targetId = target.id; + Reflect.deleteProperty(this.targets, targetId); + + // Why does this not work, what + target.updateAllDrawableProperties(); + } + + getCurrentVideo(args, util) { + const targetName = Cast.toString(args.TARGET); + const target = this._getTargetFromMenu(targetName, util); + if (!target) return; + + const targetId = target.id; + return (this.targets[targetId]) ? this.targets[targetId].videoName : ''; } startVideo(args) { - const time = Cast.toNumber(args.DURATION); - this.video.currentTime = time; - this.video.play(); + const videoName = Cast.toString(args.NAME); + const video = this.videos[videoName]; + if (!video) return; + + video.play(); } - getCurrentTime() { - return Math.round(Cast.toNumber(this.video.currentTime) * 1000) / 1000; + getCurrentTime(args) { + const videoName = Cast.toString(args.NAME); + const video = this.videos[videoName]; + if (!video) return 0; + + return video.currentTime; + } - getState(args) { - switch (args.STATE) { - case ('paused'): return this.video.paused; - case ('playing'): return !this.video.paused; - case ('visible'): return this.video.style.visibility === 'visible'; - case ('hidden'): return this.video.style.visibility === 'hidden'; - } + pauseVideo(args) { + const videoName = Cast.toString(args.NAME); + const video = this.videos[videoName]; + if (!video) return; + + video.pause(); } - pauseVideo() { - this.video.pause(); + resumeVideo(args) { + const videoName = Cast.toString(args.NAME); + const video = this.videos[videoName]; + if (!video) return; + + video.play(); } - resumeVideo() { - if (this.video.paused) this.video.play(); + getState(args) { + const videoName = Cast.toString(args.NAME); + const video = this.videos[videoName]; + if (!video) return (args.STATE == 'paused'); + + return (args.STATE == 'playing') ? !video.paused : video.paused; } - getIsPaused() { - return this.video.paused; + _getTargetFromMenu (targetName, util) { + let target = Scratch.vm.runtime.getSpriteTargetByName(targetName); + if (targetName === '_myself_') target = util.target; + if (targetName === '_stage_') target = runtime.getTargetForStage(); + return target; } - getOptionIsEnabled(args) { - const option = Cast.toString(args.OPTION); - switch (option) { - case ('controls'): return this.video.controls; - case ('loop'): return this.video.loop; + _getTargets() { + const spriteNames = [ + {text: 'myself', value: '_myself_'}, + {text: 'Stage', value: '_stage_'} + ]; + const targets = Scratch.vm.runtime.targets; + for (let index = 1; index < targets.length; index++) { + const target = targets[index]; + if (target.isOriginal) { + const targetName = target.getName(); + spriteNames.push({ + text: targetName, + value: targetName + }); + } } + return spriteNames; } - testBlock(args, util) { - } } Scratch.extensions.register(new Video()); })(Scratch); From 41287debc0a7ccec9640bcfca55e92921bca4cdb Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 13 Aug 2023 20:19:20 +0100 Subject: [PATCH 08/34] Update Video.js - Fixed bug with targets not getting reset --- extensions/Lily/Video.js | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index b21f0ff5f1..068f39fd58 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -10,6 +10,26 @@ this.videos = []; this.targets = []; + runtime.on('PROJECT_RUN_STOP', () => { + for (const name of Object.keys(this.videos)) { + const video = this.videos[name]; + video.pause(); + video.currentTime = 0; + } + + this.targets = []; + }); + + runtime.on('PROJECT_START', () => { + for (const name of Object.keys(this.videos)) { + const video = this.videos[name]; + video.pause(); + video.currentTime = 0; + } + + this.targets = []; + }); + runtime.on('BEFORE_EXECUTE', () => { for (const name of Object.keys(this.videos)) { const video = this.videos[name]; @@ -20,7 +40,11 @@ const target = this.targets[id].target; const drawableID = target.drawableID; - const skinId = runtime.renderer._allDrawables[drawableID].skin._id; + const drawable = runtime.renderer._allDrawables[drawableID]; + // This was only a problem when targets weren't reset, I don't + // expect it to happen much now but just in case.. + if (!drawable) return; + const skinId = drawable.skin._id; const video = this.videos[this.targets[id].videoName]; vm.renderer.updateBitmapSkin(skinId, video, 1); @@ -196,8 +220,8 @@ } deleteVideoURL(args) { - const name = Cast.toString(args.NAME); - Reflect.deleteProperty(this.videos, name); + const videoName = Cast.toString(args.NAME); + Reflect.deleteProperty(this.videos, videoName); // To-do : reset the targets with the video } @@ -238,10 +262,12 @@ startVideo(args) { const videoName = Cast.toString(args.NAME); + const duration = Cast.toNumber(args.DURATION); const video = this.videos[videoName]; if (!video) return; video.play(); + video.currentTime = duration; } getCurrentTime(args) { @@ -250,7 +276,6 @@ if (!video) return 0; return video.currentTime; - } pauseVideo(args) { From 0b80d4bf7f79dd7383f91b74bd514e3099d95557 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 13 Aug 2023 21:04:17 +0100 Subject: [PATCH 09/34] Update Video.js --- extensions/Lily/Video.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 068f39fd58..611c995ab9 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -10,7 +10,7 @@ this.videos = []; this.targets = []; - runtime.on('PROJECT_RUN_STOP', () => { + runtime.on('PROJECT_STOP_ALL', () => { for (const name of Object.keys(this.videos)) { const video = this.videos[name]; video.pause(); @@ -266,6 +266,8 @@ const video = this.videos[videoName]; if (!video) return; + console.log(video); + video.play(); video.currentTime = duration; } From 046b1e27d11b3da1d8fcce2920de930b61cf1647 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 13 Aug 2023 22:14:22 +0100 Subject: [PATCH 10/34] Update Video.js - Added volume - More fixes --- extensions/Lily/Video.js | 103 ++++++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 22 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 611c995ab9..4af26b577e 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -22,7 +22,7 @@ runtime.on('PROJECT_START', () => { for (const name of Object.keys(this.videos)) { - const video = this.videos[name]; + const video = this.videos[name].video; video.pause(); video.currentTime = 0; } @@ -32,8 +32,10 @@ runtime.on('BEFORE_EXECUTE', () => { for (const name of Object.keys(this.videos)) { - const video = this.videos[name]; - video.volume = runtime.audioEngine.inputNode.gain.value; + const video = this.videos[name].video; + const videoVolume = this.videos[name].volume; + const projectVolume = runtime.audioEngine.inputNode.gain.value; + video.volume = videoVolume * projectVolume; } for (const id of Object.keys(this.targets)) { @@ -45,7 +47,7 @@ // expect it to happen much now but just in case.. if (!drawable) return; const skinId = drawable.skin._id; - const video = this.videos[this.targets[id].videoName]; + const video = this.videos[this.targets[id].videoName].video; vm.renderer.updateBitmapSkin(skinId, video, 1); } @@ -83,6 +85,11 @@ } } }, + { + opcode: 'getLoadedVideos', + blockType: Scratch.BlockType.REPORTER, + text: 'loaded videos' + }, '---', { opcode: 'showVideo', @@ -154,7 +161,7 @@ }, '---', { - opcode: 'pauseVideo', + opcode: 'pause', blockType: Scratch.BlockType.COMMAND, text: 'pause video [NAME]', arguments: { @@ -165,7 +172,7 @@ } }, { - opcode: 'resumeVideo', + opcode: 'resume', blockType: Scratch.BlockType.COMMAND, text: 'resume video [NAME]', arguments: { @@ -189,6 +196,33 @@ menu: 'state' } } + }, + '---', + { + opcode: 'setVolume', + blockType: Scratch.BlockType.COMMAND, + text: 'set volume of video [NAME] to [VALUE]', + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my video' + }, + VALUE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + { + opcode: 'getVolume', + blockType: Scratch.BlockType.REPORTER, + text: 'volume of video [NAME]', + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my video' + } + } } ], menus: { @@ -204,19 +238,25 @@ }; } - loadVideoURL(args) { + async loadVideoURL(args) { const videoName = Cast.toString(args.NAME); const url = Cast.toString(args.URL); + if (!await Scratch.canFetch(url)) return; + + this.videos[videoName] = { + video: document.createElement('video'), + volume: 1 + } - this.videos[videoName] = document.createElement('video'); - this.videos[videoName].width = 480; - this.videos[videoName].height = 360; - this.videos[videoName].crossOrigin = 'anonymous'; + const video = this.videos[videoName].video; + video.width = 480; + video.height = 360; + video.crossOrigin = 'anonymous'; // To-do : Some urls can't be loaded by the renderer, how can we detect that? - this.videos[videoName].src = url; - this.videos[videoName].currentTime = 0; + video.src = url; + video.currentTime = 0; } deleteVideoURL(args) { @@ -226,6 +266,10 @@ // To-do : reset the targets with the video } + getLoadedVideos() { + return JSON.stringify(Object.keys(this.videos)); + } + showVideo(args, util) { const targetName = Cast.toString(args.TARGET); const videoName = Cast.toString(args.NAME); @@ -263,34 +307,32 @@ startVideo(args) { const videoName = Cast.toString(args.NAME); const duration = Cast.toNumber(args.DURATION); - const video = this.videos[videoName]; + const video = this.videos[videoName].video; if (!video) return; - console.log(video); - video.play(); video.currentTime = duration; } getCurrentTime(args) { const videoName = Cast.toString(args.NAME); - const video = this.videos[videoName]; + const video = this.videos[videoName].video; if (!video) return 0; return video.currentTime; } - pauseVideo(args) { + pause(args) { const videoName = Cast.toString(args.NAME); - const video = this.videos[videoName]; + const video = this.videos[videoName].video; if (!video) return; video.pause(); } - resumeVideo(args) { + resume(args) { const videoName = Cast.toString(args.NAME); - const video = this.videos[videoName]; + const video = this.videos[videoName].video; if (!video) return; video.play(); @@ -298,12 +340,29 @@ getState(args) { const videoName = Cast.toString(args.NAME); - const video = this.videos[videoName]; + const video = this.videos[videoName].video; if (!video) return (args.STATE == 'paused'); return (args.STATE == 'playing') ? !video.paused : video.paused; } + setVolume(args) { + const videoName = Cast.toString(args.NAME); + const value = Cast.toNumber(args.VALUE); + const videoObject = this.videos[videoName]; + if (!videoObject) return; + + videoObject.volume = value / 100; + } + + getVolume(args) { + const videoName = Cast.toString(args.NAME); + const videoObject = this.videos[videoName]; + if (!videoObject) return 0; + + return videoObject.volume * 100; + } + _getTargetFromMenu (targetName, util) { let target = Scratch.vm.runtime.getSpriteTargetByName(targetName); if (targetName === '_myself_') target = util.target; From b648b32704898afe968b54f4f2197b56aba3ce5d Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 13 Aug 2023 22:14:33 +0100 Subject: [PATCH 11/34] Delete Video.svg --- images/Lily/Video.svg | 1 - 1 file changed, 1 deletion(-) delete mode 100644 images/Lily/Video.svg diff --git a/images/Lily/Video.svg b/images/Lily/Video.svg deleted file mode 100644 index fcc3cbfab7..0000000000 --- a/images/Lily/Video.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 169c5c42dad550b8fefcee989a07bdfcec93c174 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 13 Aug 2023 22:15:09 +0100 Subject: [PATCH 12/34] Add files via upload --- images/Lily/Video.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 images/Lily/Video.svg diff --git a/images/Lily/Video.svg b/images/Lily/Video.svg new file mode 100644 index 0000000000..bd9a2f932b --- /dev/null +++ b/images/Lily/Video.svg @@ -0,0 +1 @@ + \ No newline at end of file From f5806e79d84021cf1ab423456cee781c3abe8c07 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 13 Aug 2023 22:16:07 +0100 Subject: [PATCH 13/34] Update Video.js --- extensions/Lily/Video.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 4af26b577e..85d08fa728 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -246,7 +246,7 @@ this.videos[videoName] = { video: document.createElement('video'), volume: 1 - } + }; const video = this.videos[videoName].video; video.width = 480; From 81cc02ec34b7f7442f4a49331d307f8ae900219c Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 14 Aug 2023 06:13:51 +0100 Subject: [PATCH 14/34] Update Video.js --- extensions/Lily/Video.js | 115 ++++++++++++++++++++++++++------------- 1 file changed, 78 insertions(+), 37 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 85d08fa728..fd6d832e89 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -12,11 +12,16 @@ runtime.on('PROJECT_STOP_ALL', () => { for (const name of Object.keys(this.videos)) { - const video = this.videos[name]; + const video = this.videos[name].video; video.pause(); video.currentTime = 0; } + for (const id of Object.keys(this.targets)) { + const target = this.targets[id].target; + target.updateAllDrawableProperties(); + } + this.targets = []; }); @@ -27,15 +32,26 @@ video.currentTime = 0; } + for (const id of Object.keys(this.targets)) { + const target = this.targets[id].target; + target.updateAllDrawableProperties(); + } + this.targets = []; }); runtime.on('BEFORE_EXECUTE', () => { for (const name of Object.keys(this.videos)) { - const video = this.videos[name].video; - const videoVolume = this.videos[name].volume; + const videoObject = this.videos[name]; + + const video = videoObject.video; + const videoVolume = videoObject.volume; + const skin = videoObject.skin; + const projectVolume = runtime.audioEngine.inputNode.gain.value; video.volume = videoVolume * projectVolume; + + vm.renderer.updateBitmapSkin(skin, video, 1); } for (const id of Object.keys(this.targets)) { @@ -46,10 +62,8 @@ // This was only a problem when targets weren't reset, I don't // expect it to happen much now but just in case.. if (!drawable) return; - const skinId = drawable.skin._id; - const video = this.videos[this.targets[id].videoName].video; - - vm.renderer.updateBitmapSkin(skinId, video, 1); + const skin = this.videos[this.targets[id].videoName].skin; + drawable.skin = vm.renderer._allSkins[skin]; } }); } @@ -149,10 +163,14 @@ } }, { - opcode: 'getCurrentTime', + opcode: 'getAttribute', blockType: Scratch.BlockType.REPORTER, - text: 'current time of video [NAME]', + text: '[ATTRIBUTE] of video [NAME]', arguments: { + ATTRIBUTE: { + type: Scratch.ArgumentType.STRING, + menu: 'attribute' + }, NAME: { type: Scratch.ArgumentType.STRING, defaultValue: 'my video' @@ -233,6 +251,10 @@ state: { acceptReporters: true, items: ['playing', 'paused'] + }, + attribute: { + acceptReporters: false, + items: ['current time', 'duration', 'width', 'height'] } } }; @@ -245,25 +267,36 @@ this.videos[videoName] = { video: document.createElement('video'), - volume: 1 - }; + volume: 1, + skin: 0 + } - const video = this.videos[videoName].video; - video.width = 480; - video.height = 360; - video.crossOrigin = 'anonymous'; + const videoObject = this.videos[videoName]; + const video = videoObject.video; - // To-do : Some urls can't be loaded by the renderer, how can we detect that? + video.width = 1; + video.height = 1; + video.crossOrigin = 'anonymous'; video.src = url; video.currentTime = 0; + + videoObject.skin = vm.renderer.createBitmapSkin(video); } deleteVideoURL(args) { const videoName = Cast.toString(args.NAME); - Reflect.deleteProperty(this.videos, videoName); + if (!this.videos[videoName]) return; + + for (const id of Object.keys(this.targets)) { + if (this.targets[id].videoName === videoName) { + this.targets[id].target.updateAllDrawableProperties(); + Reflect.deleteProperty(this.targets, id); + } + } - // To-do : reset the targets with the video + this.videos[videoName].video.pause(); + Reflect.deleteProperty(this.videos, videoName); } getLoadedVideos() { @@ -274,7 +307,7 @@ const targetName = Cast.toString(args.TARGET); const videoName = Cast.toString(args.NAME); const target = this._getTargetFromMenu(targetName, util); - if (!target) return; + if (!target || !this.videos[videoName]) return; const targetId = target.id; this.targets[targetId] = { @@ -291,7 +324,6 @@ const targetId = target.id; Reflect.deleteProperty(this.targets, targetId); - // Why does this not work, what target.updateAllDrawableProperties(); } @@ -307,43 +339,52 @@ startVideo(args) { const videoName = Cast.toString(args.NAME); const duration = Cast.toNumber(args.DURATION); - const video = this.videos[videoName].video; - if (!video) return; + const videoObject = this.videos[videoName]; + if (!videoObject) return; - video.play(); - video.currentTime = duration; + videoObject.video.play(); + videoObject.video.currentTime = duration; } - getCurrentTime(args) { + getAttribute(args) { const videoName = Cast.toString(args.NAME); - const video = this.videos[videoName].video; - if (!video) return 0; + const videoObject = this.videos[videoName]; + if (!videoObject) return 0; + + const skinId = videoObject.skin; + const skin = vm.renderer._allSkins[skinId]; - return video.currentTime; + switch(args.ATTRIBUTE) { + case('current time'): return videoObject.video.currentTime; + case('duration'): return videoObject.video.duration; + case('width'): return skin._textureSize[0]; + case('height'): return skin._textureSize[1]; + default: return 0; + } } pause(args) { const videoName = Cast.toString(args.NAME); - const video = this.videos[videoName].video; - if (!video) return; + const videoObject = this.videos[videoName]; + if (!videoObject) return; - video.pause(); + videoObject.video.pause(); } resume(args) { const videoName = Cast.toString(args.NAME); - const video = this.videos[videoName].video; - if (!video) return; + const videoObject = this.videos[videoName]; + if (!videoObject) return; - video.play(); + videoObject.video.play(); } getState(args) { const videoName = Cast.toString(args.NAME); - const video = this.videos[videoName].video; - if (!video) return (args.STATE == 'paused'); + const videoObject = this.videos[videoName]; + if (!videoObject) return (args.STATE === 'paused'); - return (args.STATE == 'playing') ? !video.paused : video.paused; + return (args.STATE == 'playing') ? !videoObject.video.paused : videoObject.video.paused; } setVolume(args) { From 178af5e34bb199b6b693b25ac04dc74af6729fbf Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 14 Aug 2023 06:16:06 +0100 Subject: [PATCH 15/34] Update Video.js --- extensions/Lily/Video.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index fd6d832e89..2e23cfa1d5 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -269,7 +269,7 @@ video: document.createElement('video'), volume: 1, skin: 0 - } + }; const videoObject = this.videos[videoName]; const video = videoObject.video; @@ -287,7 +287,7 @@ deleteVideoURL(args) { const videoName = Cast.toString(args.NAME); if (!this.videos[videoName]) return; - + for (const id of Object.keys(this.targets)) { if (this.targets[id].videoName === videoName) { this.targets[id].target.updateAllDrawableProperties(); @@ -354,11 +354,11 @@ const skinId = videoObject.skin; const skin = vm.renderer._allSkins[skinId]; - switch(args.ATTRIBUTE) { - case('current time'): return videoObject.video.currentTime; - case('duration'): return videoObject.video.duration; - case('width'): return skin._textureSize[0]; - case('height'): return skin._textureSize[1]; + switch (args.ATTRIBUTE) { + case ('current time'): return videoObject.video.currentTime; + case ('duration'): return videoObject.video.duration; + case ('width'): return skin._textureSize[0]; + case ('height'): return skin._textureSize[1]; default: return 0; } } From 92b22d6c5e35cbd93ddfe15b2af1e43362f5ca4c Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 14 Aug 2023 06:47:00 +0100 Subject: [PATCH 16/34] Add files via upload --- website/dango.mp4 | Bin 0 -> 107326 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/dango.mp4 diff --git a/website/dango.mp4 b/website/dango.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..f5dbfd4738f45e2e1b3a4d9cbe0e91efb3991760 GIT binary patch literal 107326 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 z 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%0DLQyG5(Jo5Xpz^_D2GG`>MqF@A^kT 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 KQ2`kP3H5(3t(AuW literal 0 HcmV?d00001 From 599f0f1ded035b787289b9e23f24c4cbd0e52bff Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 14 Aug 2023 06:47:42 +0100 Subject: [PATCH 17/34] Update Video.js --- extensions/Lily/Video.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 2e23cfa1d5..08bccbd0b2 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -80,7 +80,7 @@ arguments: { URL: { type: Scratch.ArgumentType.STRING, - defaultValue: '' + defaultValue: 'https://extensions.turbowarp.org/dango.mp4' }, NAME: { type: Scratch.ArgumentType.STRING, From 8762452361c99f5df51e85601eebc5289f2a1223 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 21 Aug 2023 01:12:29 +0100 Subject: [PATCH 18/34] Delete index.ejs --- website/index.ejs | 810 ---------------------------------------------- 1 file changed, 810 deletions(-) delete mode 100644 website/index.ejs diff --git a/website/index.ejs b/website/index.ejs deleted file mode 100644 index ae6d799454..0000000000 --- a/website/index.ejs +++ /dev/null @@ -1,810 +0,0 @@ - - - - - - TurboWarp Extension Gallery - - - - - - - - - <% - const getLinkToRun = (extensionPath) => `https://turbowarp.org/editor?extension=${host}${extensionPath}`; - %> - - <% if (mode === "development") { %> -
-
-

Development Server Tools

-

- Most recently modified extensions: - <% for (const extension of mostRecentExtensions) { %> - <%= extension %> - <% } %> -

-
-
- <% } %> - - - - <% if (mode === 'desktop') { %> - - <% } %> - - <% - const banner = (extensionFile, options = {}) => { - if (extensionFile.endsWith('.js')) { - return `Do not add .js when calling banner(): ${extensionFile}`; - } - const imageSource = extensionImages[extensionFile] ? `images/${extensionImages[extensionFile]}` : 'images/unknown.svg'; - const imageClasses = ["extension-image"]; - if (options.invertDark) imageClasses.push("invert-dark"); - return ` -
- -
- - ${mode === "desktop" ? (` - - `) : (` - Open Extension - `)} -
-
` - }; - - const img = (extensionFile) => { - if (extensionImages[extensionFile]) return `images/${extensionImages[extensionFile]}`; - return 'images/unknown.svg'; - }; - %> - -
-
-
- <%- banner('lab/text') %> -

Animated Text

-

An easy way to display and animate text. Compatible with Scratch Lab's Animated Text experiment.

-
- -
- <%- banner('stretch') %> -

Stretch

-

Stretch sprites horizontally or vertically.

-
- -
- <%- banner('gamepad') %> -

Gamepad

-

Directly access gamepads instead of just mapping buttons to keys.

-
- -
- <%- banner('box2d') %> -

Box2D Physics

-

Two dimensional physics. Originally created by griffpatch.

-
- -
- <%- banner('files') %> -

Files

-

Read and download files.

-
- -
- <%- banner('pointerlock') %> -

Pointerlock

-

Adds blocks for mouse locking. Mouse x & y blocks will report the change since the previous frame while the pointer is locked. Replaces the pointerlock experiment.

-
- -
- <%- banner('cursor') %> -

Mouse Cursor

-

Use custom cursors or hide the cursor. Also allows replacing the cursor with any costume image.

-
- -
- <%- banner('runtime-options') %> -

Runtime Options

-

Get and modify turbo mode, framerate, interpolation, clone limit, stage size, and more.

-
- -
- <%- banner('fetch') %> -

Fetch

-

Make requests to the broader internet.

-
- -
- <%- banner('text') %> -

Text

-

Manipulate characters and text. Originally created by CST1229.

-
- -
- <%- banner('local-storage') %> -

Local Storage

-

Store data persistently. Like cookies, but better.

-
- -
- <%- banner('true-fantom/base') %> -

Base

-

Convert numbers between bases. Created by TrueFantom.

-
- -
- <%- banner('bitwise') %> -

Bitwise

-

Blocks that operate on the binary representation of numbers in computers. Modified by TrueFantom.

-
- -
- <%- banner('Skyhigh173/bigint') %> -

BigInt

-

Math blocks that work on infinitely large integers (no decimals). Created by Skyhigh173.

-
- -
- <%- banner('utilities') %> -

Utilities

-

A bunch of interesting blocks. Originally created by Sheep_maker.

-
- -
- <%- banner('sound') %> -

Sound

-

Play sounds from URLs.

-
- -
- <%- banner('Lily/Video') %> -

Video

-

Play videos from URLs.

-
- -
- <%- banner('Xeltalliv/clippingblending') %> -

Clipping & Blending

-

Clipping outside of a specified rectangular area and additive color blending. Created by Vadik1.

-
- -
- <%- banner('clipboard') %> -

Clipboard

-

Read and write from the system clipboard.

-
- -
- <%- banner('penplus') %> -

Pen Plus

-

Advanced rendering capabilities. Created by ObviousAlexC.

-
- -
- <%- banner('Lily/Skins') %> -

Skins

-

Have your sprites render as other images or costumes. Created by LilyMakesThings.

-
- -
- <%- banner('obviousAlexC/SensingPlus') %> -

Sensing Plus

-

An extension to the sensing category. Created by ObviousAlexC.

-
- -
- <%- banner('Lily/ClonesPlus') %> -

Clones Plus

-

Expansion of Scratch's clone features. Created by LilyMakesThings.

-
- -
- <%- banner('Lily/LooksPlus') %> -

Looks Plus

-

Expands upon the looks category, allowing you to show/hide, get costume data and edit SVG skins on sprites. Created by LilyMakesThings.

-
- -
- <%- banner('NexusKitten/moremotion') %> -

More Motion

-

More motion-related blocks. Created by NamelessCat.

-
- -
- <%- banner('navigator') %> -

Navigator

-

Details about the user's browser and operating system.

-
- -
- <%- banner('battery') %> -

Battery

-

Access information about the battery of phones or laptops. May not work on all devices and browsers.

-
- -
- <%- banner('TheShovel/CustomStyles') %> -

Custom Styles

-

Customize the appearance of variable monitors and prompts in your project. Created by TheShovel.

-
- -
- <%- banner('mdwalters/notifications') %> -

Notifications

-

Display notifications.

-
- -
- <%- banner('ar') %> -

Augmented Reality

-

Shows image from camera and performs motion tracking, allowing 3D projects to correctly overlay virtual objects on real world. Created by Vadik1.

-
- -
- <%- banner('encoding') %> -

Encoding

-

Encode and decode strings into their unicode numbers, base 64, or URLs. Created by -SIPC-.

-
- -
- <%- banner('Lily/TempVariables2') %> -

Temporary Variables

-

Create disposable runtime or thread variables. Created by LilyMakesThings.

-
- -
- <%- banner('Lily/MoreTimers') %> -

More Timers

-

Control several timers at once. Created by LilyMakesThings.

-
- -
- <%- banner('clouddata-ping') %> -

Ping Cloud Data

-

Determine whether a cloud variable server is probably up. Originally created by TheShovel.

-
- -
- <%- banner('cloudlink') %> -

Cloudlink

-

Powerful WebSocket extension for Scratch 3. Created by MikeDEV.

-
- -
- <%- banner('true-fantom/network') %> -

Network

-

Various blocks for interacting with the network. Created by TrueFantom.

-
- -
- <%- banner('true-fantom/math') %> -

Math

-

A lot of operators blocks, from exponentiation to trigonometric functions. Created by TrueFantom.

-
- -
- <%- banner('true-fantom/regexp') %> -

RegExp

-

Full interface for working with Regular Expressions. Created by TrueFantom.

-
- -
- <%- banner('true-fantom/couplers') %> -

Couplers

-

A few adapter blocks. Created by TrueFantom.

-
- -
- <%- banner('Lily/AllMenus') %> -

All Menus

-

Special category with every menu from every Scratch category and extensions. Created by LilyMakesThings

-
- -
- <%- banner('Lily/Cast') %> -

Cast

-

Convert values between types. Created by LilyMakesThings

-
- -
- <%- banner('-SIPC-/time') %> -

Time

-

Blocks for interacting with unix timestamps and other date strings. Created by -SIPC-.

-
- -
- <%- banner('-SIPC-/consoles') %> -

Consoles

-

Blocks that interact the JavaScript console built in to your browser's developer tools. Created by -SIPC-.

-
- -
- <%- banner('ZXMushroom63/searchApi') %> -

Search Params

-

Interact with URL search parameters: the part of the URL after a question mark. Created by ZXMushroom63.

-
- -
- <%- banner('TheShovel/ShovelUtils') %> -

ShovelUtils

-

A bunch of miscellaneous blocks. Created by TheShovel.

-
- -
- <%- banner('Skyhigh173/json') %> -

JSON

-

Handle JSON strings and arrays. Created by Skyhigh173.

-
- -
- <%- banner('cs2627883/numericalencoding') %> -

Numerical Encoding

-

Encode strings as numbers for cloud variables. Created by cs2627883.

-
- -
- <%- banner('DT/cameracontrols') %> -

Camera Controls

-

Move the visible part of the stage. Created by DT.

-
- -
- <%- banner('TheShovel/CanvasEffects') %> -

Canvas Effects

-

Apply visual effects to the entire stage. Created by TheShovel.

-
- -
- <%- banner('Longboost/color_channels') %> -

RGB Channels

-

Only render or stamp certain RGB channels.

-
- -
- <%- banner('CST1229/zip') %> -

Zip

-

Create and edit .zip format files, including .sb3 files. Created by CST1229.

-
- -
- <%- banner('TheShovel/LZ-String') %> -

LZ Compress

-

Compress and decompress text using lz-string.

-
- -
- <%- banner('0832/rxFS2') %> -

rxFS

-

Blocks for interacting with a virtual in-memory filesystem. Created by 0832.

-
- -
- <%- banner('NexusKitten/sgrab') %> -

S-Grab

-

Get information about Scratch projects and Scratch users. Created by NamelessCat.

-
- -
- <%- banner('NOname-awa/graphics2d') %> -

Graphics 2D

-

Blocks to compute lengths, angles, and areas in two dimensions. Created by NOname-awa.

-
- -
- <%- banner('NOname-awa/more-comparisons') %> -

More Comparisons

-

More comparison blocks. Created by NOname-awa.

-
- -
- <%- banner('JeremyGamer13/tween') %> -

Tween

-

Easing methods for smooth animations. Created by JeremyGamer13

-
- -
- <%- banner('rixxyx') %> -

RixxyX

-

Various utility blocks. Created by RixTheTyrunt.

-
- -
- <%- banner('qxsck/data-analysis') %> -

Data Analysis

-

Blocks to compute means, medians, maximums, minimums, variances, and modes. Created by qxsck.

-
- -
- <%- banner('qxsck/var-and-list') %> -

Variable and list

-

More blocks related to variables and lists. Created by qxsck.

-
- -
- <%- banner('vercte/dictionaries') %> -

Dictionaries

-

Use the power of dictionaries in your project. Created by Vercte.

-
- -
- <%- banner('godslayerakp/http') %> -

HTTP

-

Comprehensive extension for interacting with external websites. Created by RedMan13.

-
- -
- <%- banner('Lily/CommentBlocks') %> -

Comment Blocks

-

Annotate your scripts. Created by LilyMakesThings.

-
- -
- <%- banner('veggiecan/LongmanDictionary') %> -

Longman Dictionary

-

Get the definitions of words from the Longman Dictionary in your projects. Created by veggiecan0419

-
- -
- <%- banner('CubesterYT/TurboHook') %> -

TurboHook

-

Allows you to use webhooks. Created by CubesterYT.

-
- -
- <%- banner('Alestore/nfcwarp') %> -

NFCWarp

-

Allows reading data from NFC (NDEF) devices. Only works in Chrome on Android. Created by Alestore Games.

-
- -
- <%- banner('itchio') %> -

itch.io

-

Blocks that interact with the itch.io website. Unofficial. Created by softed.

-
- -
- <%- banner('gamejolt') %> -

GameJolt

-

Blocks that allow games to interact with the GameJolt API. Unofficial. Created by softed.

-
- -
- <%- banner('obviousAlexC/newgroundsIO') %> -

Newgrounds

-

Blocks that allow games to interact with the Newgrounds API. Unofficial. Created by ObviousAlexC.

-
- -
- <%- banner('Lily/McUtils') %> -

McUtils

-

Helpful utilities for any fast food employee. Created by LilyMakesThings.

-
-
-
- - - - From 3260ea10bc7eecc0b8e066896cf4263135133555 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 21 Aug 2023 01:13:13 +0100 Subject: [PATCH 19/34] Update Video.js --- extensions/Lily/Video.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 08bccbd0b2..a83830f398 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -1,3 +1,7 @@ +// Name: Video +// ID: lmsVideo +// Description: Play videos from URLs. + (function (Scratch) { 'use strict'; From 35317ffd18e04c14ea20a42327ec1dd137608275 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 21 Aug 2023 01:14:08 +0100 Subject: [PATCH 20/34] Update extensions.json --- extensions/extensions.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/extensions.json b/extensions/extensions.json index 7ec0ffa91d..230b35ea6a 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -15,6 +15,7 @@ "Skyhigh173/bigint", "utilities", "sound", + "Lily/Video", "Xeltalliv/clippingblending", "clipboard", "penplus", @@ -69,4 +70,4 @@ "gamejolt", "obviousAlexC/newgroundsIO", "Lily/McUtils" -] \ No newline at end of file +] From cd2012cc7a36770421466e747ca8c124de79a820 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 23 Aug 2023 05:24:39 +0100 Subject: [PATCH 21/34] Update Video.js --- extensions/Lily/Video.js | 229 +++++++++++++++++++-------------------- 1 file changed, 114 insertions(+), 115 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index a83830f398..91278468bb 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -3,7 +3,7 @@ // Description: Play videos from URLs. (function (Scratch) { - 'use strict'; + "use strict"; const vm = Scratch.vm; const runtime = vm.runtime; @@ -14,7 +14,7 @@ this.videos = []; this.targets = []; - runtime.on('PROJECT_STOP_ALL', () => { + runtime.on("PROJECT_STOP_ALL", () => { for (const name of Object.keys(this.videos)) { const video = this.videos[name].video; video.pause(); @@ -29,7 +29,7 @@ this.targets = []; }); - runtime.on('PROJECT_START', () => { + runtime.on("PROJECT_START", () => { for (const name of Object.keys(this.videos)) { const video = this.videos[name].video; video.pause(); @@ -44,7 +44,7 @@ this.targets = []; }); - runtime.on('BEFORE_EXECUTE', () => { + runtime.on("BEFORE_EXECUTE", () => { for (const name of Object.keys(this.videos)) { const videoObject = this.videos[name]; @@ -73,206 +73,206 @@ } getInfo() { return { - id: 'lmsVideo', - color1: '#557882', - name: 'Video', + id: "lmsVideo", + color1: "#557882", + name: "Video", blocks: [ { - opcode: 'loadVideoURL', + opcode: "loadVideoURL", blockType: Scratch.BlockType.COMMAND, - text: 'load video from URL [URL] as [NAME]', + text: "load video from URL [URL] as [NAME]", arguments: { URL: { type: Scratch.ArgumentType.STRING, - defaultValue: 'https://extensions.turbowarp.org/dango.mp4' + defaultValue: "https://extensions.turbowarp.org/dango.mp4", }, NAME: { type: Scratch.ArgumentType.STRING, - defaultValue: 'my video' - } - } + defaultValue: "my video", + }, + }, }, { - opcode: 'deleteVideoURL', + opcode: "deleteVideoURL", blockType: Scratch.BlockType.COMMAND, - text: 'delete video [NAME]', + text: "delete video [NAME]", arguments: { NAME: { type: Scratch.ArgumentType.STRING, - defaultValue: 'my video' - } - } + defaultValue: "my video", + }, + }, }, { - opcode: 'getLoadedVideos', + opcode: "getLoadedVideos", blockType: Scratch.BlockType.REPORTER, - text: 'loaded videos' + text: "loaded videos", }, - '---', + "---", { - opcode: 'showVideo', + opcode: "showVideo", blockType: Scratch.BlockType.COMMAND, - text: 'show video [NAME] on [TARGET]', + text: "show video [NAME] on [TARGET]", arguments: { TARGET: { type: Scratch.ArgumentType.STRING, - menu: 'targets' + menu: "targets", }, NAME: { type: Scratch.ArgumentType.STRING, - defaultValue: 'my video' + defaultValue: "my video", }, - } + }, }, { - opcode: 'stopShowingVideo', + opcode: "stopShowingVideo", blockType: Scratch.BlockType.COMMAND, - text: 'stop showing video on [TARGET]', + text: "stop showing video on [TARGET]", arguments: { TARGET: { type: Scratch.ArgumentType.STRING, - menu: 'targets' + menu: "targets", }, NAME: { type: Scratch.ArgumentType.STRING, - defaultValue: 'my video' + defaultValue: "my video", }, - } + }, }, { - opcode: 'getCurrentVideo', + opcode: "getCurrentVideo", blockType: Scratch.BlockType.REPORTER, - text: 'current video on [TARGET]', + text: "current video on [TARGET]", arguments: { TARGET: { type: Scratch.ArgumentType.STRING, - menu: 'targets' - } - } + menu: "targets", + }, + }, }, - '---', + "---", { - opcode: 'startVideo', + opcode: "startVideo", blockType: Scratch.BlockType.COMMAND, - text: 'start video [NAME] at [DURATION] seconds', + text: "start video [NAME] at [DURATION] seconds", arguments: { NAME: { type: Scratch.ArgumentType.STRING, - defaultValue: 'my video' + defaultValue: "my video", }, DURATION: { type: Scratch.ArgumentType.NUMBER, - defaultValue: 0 - } - } + defaultValue: 0, + }, + }, }, { - opcode: 'getAttribute', + opcode: "getAttribute", blockType: Scratch.BlockType.REPORTER, - text: '[ATTRIBUTE] of video [NAME]', + text: "[ATTRIBUTE] of video [NAME]", arguments: { ATTRIBUTE: { type: Scratch.ArgumentType.STRING, - menu: 'attribute' + menu: "attribute", }, NAME: { type: Scratch.ArgumentType.STRING, - defaultValue: 'my video' - } - } + defaultValue: "my video", + }, + }, }, - '---', + "---", { - opcode: 'pause', + opcode: "pause", blockType: Scratch.BlockType.COMMAND, - text: 'pause video [NAME]', + text: "pause video [NAME]", arguments: { NAME: { type: Scratch.ArgumentType.STRING, - defaultValue: 'my video' - } - } + defaultValue: "my video", + }, + }, }, { - opcode: 'resume', + opcode: "resume", blockType: Scratch.BlockType.COMMAND, - text: 'resume video [NAME]', + text: "resume video [NAME]", arguments: { NAME: { type: Scratch.ArgumentType.STRING, - defaultValue: 'my video' - } - } + defaultValue: "my video", + }, + }, }, { - opcode: 'getState', + opcode: "getState", blockType: Scratch.BlockType.BOOLEAN, - text: 'video [NAME] is [STATE]?', + text: "video [NAME] is [STATE]?", arguments: { NAME: { type: Scratch.ArgumentType.STRING, - defaultValue: 'my video' + defaultValue: "my video", }, STATE: { type: Scratch.ArgumentType.STRING, - menu: 'state' - } - } + menu: "state", + }, + }, }, - '---', + "---", { - opcode: 'setVolume', + opcode: "setVolume", blockType: Scratch.BlockType.COMMAND, - text: 'set volume of video [NAME] to [VALUE]', + text: "set volume of video [NAME] to [VALUE]", arguments: { NAME: { type: Scratch.ArgumentType.STRING, - defaultValue: 'my video' + defaultValue: "my video", }, VALUE: { type: Scratch.ArgumentType.NUMBER, - defaultValue: 100 - } - } + defaultValue: 100, + }, + }, }, { - opcode: 'getVolume', + opcode: "getVolume", blockType: Scratch.BlockType.REPORTER, - text: 'volume of video [NAME]', + text: "volume of video [NAME]", arguments: { NAME: { type: Scratch.ArgumentType.STRING, - defaultValue: 'my video' - } - } - } + defaultValue: "my video", + }, + }, + }, ], menus: { targets: { acceptReporters: true, - items: '_getTargets' + items: "_getTargets", }, state: { acceptReporters: true, - items: ['playing', 'paused'] + items: ["playing", "paused"], }, attribute: { acceptReporters: false, - items: ['current time', 'duration', 'width', 'height'] - } - } + items: ["current time", "duration", "width", "height"], + }, + }, }; } async loadVideoURL(args) { const videoName = Cast.toString(args.NAME); const url = Cast.toString(args.URL); - if (!await Scratch.canFetch(url)) return; + if (!(await Scratch.canFetch(url))) return; this.videos[videoName] = { - video: document.createElement('video'), + video: document.createElement("video"), volume: 1, - skin: 0 + skin: 0, }; const videoObject = this.videos[videoName]; @@ -280,7 +280,7 @@ video.width = 1; video.height = 1; - video.crossOrigin = 'anonymous'; + video.crossOrigin = "anonymous"; video.src = url; video.currentTime = 0; @@ -316,7 +316,7 @@ const targetId = target.id; this.targets[targetId] = { target: target, - videoName: videoName + videoName: videoName, }; } @@ -337,7 +337,7 @@ if (!target) return; const targetId = target.id; - return (this.targets[targetId]) ? this.targets[targetId].videoName : ''; + return this.targets[targetId] ? this.targets[targetId].videoName : ""; } startVideo(args) { @@ -359,11 +359,16 @@ const skin = vm.renderer._allSkins[skinId]; switch (args.ATTRIBUTE) { - case ('current time'): return videoObject.video.currentTime; - case ('duration'): return videoObject.video.duration; - case ('width'): return skin._textureSize[0]; - case ('height'): return skin._textureSize[1]; - default: return 0; + case "current time": + return videoObject.video.currentTime; + case "duration": + return videoObject.video.duration; + case "width": + return skin._textureSize[0]; + case "height": + return skin._textureSize[1]; + default: + return 0; } } @@ -386,9 +391,11 @@ getState(args) { const videoName = Cast.toString(args.NAME); const videoObject = this.videos[videoName]; - if (!videoObject) return (args.STATE === 'paused'); + if (!videoObject) return args.STATE === "paused"; - return (args.STATE == 'playing') ? !videoObject.video.paused : videoObject.video.paused; + return args.STATE == "playing" + ? !videoObject.video.paused + : videoObject.video.paused; } setVolume(args) { @@ -408,32 +415,24 @@ return videoObject.volume * 100; } - _getTargetFromMenu (targetName, util) { + _getTargetFromMenu(targetName, util) { let target = Scratch.vm.runtime.getSpriteTargetByName(targetName); - if (targetName === '_myself_') target = util.target; - if (targetName === '_stage_') target = runtime.getTargetForStage(); + if (targetName === "_myself_") target = util.target; + if (targetName === "_stage_") target = runtime.getTargetForStage(); return target; } _getTargets() { - const spriteNames = [ - {text: 'myself', value: '_myself_'}, - {text: 'Stage', value: '_stage_'} + let spriteNames = [ + { text: "myself", value: "_myself_" }, + { text: "Stage", value: "_stage_" }, ]; - const targets = Scratch.vm.runtime.targets; - for (let index = 1; index < targets.length; index++) { - const target = targets[index]; - if (target.isOriginal) { - const targetName = target.getName(); - spriteNames.push({ - text: targetName, - value: targetName - }); - } - } + const targets = Scratch.vm.runtime.targets + .filter((target) => target.isOriginal && !target.isStage) + .map((target) => target.getName()); + spriteNames = spriteNames.concat(targets); return spriteNames; } - } Scratch.extensions.register(new Video()); })(Scratch); From 783e67a879bd699f96d06c96614f632a84070f37 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 23 Aug 2023 19:46:48 +0100 Subject: [PATCH 22/34] Update Video.js --- extensions/Lily/Video.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 91278468bb..02922174b9 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -1,6 +1,9 @@ // Name: Video // ID: lmsVideo // Description: Play videos from URLs. +// By: LilyMakesThings + +// Attribution is not required, but greatly appreciated. (function (Scratch) { "use strict"; From 7c022d545b8f67576af59250b577a52cfbcdccf1 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 27 Aug 2023 05:00:07 +0100 Subject: [PATCH 23/34] Update Video.js --- extensions/Lily/Video.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 02922174b9..e5362a3829 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -68,7 +68,7 @@ const drawable = runtime.renderer._allDrawables[drawableID]; // This was only a problem when targets weren't reset, I don't // expect it to happen much now but just in case.. - if (!drawable) return; + if (!drawable) continue; const skin = this.videos[this.targets[id].videoName].skin; drawable.skin = vm.renderer._allSkins[skin]; } From 2a490f410f8cc15d467860b2a3b65f15eb6e02cf Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 27 Aug 2023 05:03:15 +0100 Subject: [PATCH 24/34] video paused check before updating bitmap Adds a check for if the video is paused before updating the bitmap skin of various targets. --- extensions/Lily/Video.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index e5362a3829..f37a97db72 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -58,7 +58,9 @@ const projectVolume = runtime.audioEngine.inputNode.gain.value; video.volume = videoVolume * projectVolume; - vm.renderer.updateBitmapSkin(skin, video, 1); + if (!video.paused) { + vm.renderer.updateBitmapSkin(skin, video, 1); + } } for (const id of Object.keys(this.targets)) { From ee7233213c9d26d840c6a18790f291ac5e3da243 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 27 Aug 2023 05:14:43 +0100 Subject: [PATCH 25/34] Add check for if any drawable has the video applied --- extensions/Lily/Video.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index f37a97db72..a7fd3cb10d 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -58,7 +58,19 @@ const projectVolume = runtime.audioEngine.inputNode.gain.value; video.volume = videoVolume * projectVolume; - if (!video.paused) { + const drawables = vm.renderer._allDrawables; + let targetsWithVideo = []; + + for (const target of runtime.targets) { + const drawableID = target.drawableID; + const targetSkin = drawables[drawableID].skin.id; + + if (targetSkin === skin.id) { + targetsWithVideo.push(target); + } + } + + if (!video.paused || targetsWithVideo.length > 0) { vm.renderer.updateBitmapSkin(skin, video, 1); } } From f18fe2ee1ba5640bda608b2834600cbd15b7e769 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 27 Aug 2023 05:21:13 +0100 Subject: [PATCH 26/34] Update Video.js --- extensions/Lily/Video.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index a7fd3cb10d..62a4c32633 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -60,11 +60,11 @@ const drawables = vm.renderer._allDrawables; let targetsWithVideo = []; - + for (const target of runtime.targets) { const drawableID = target.drawableID; const targetSkin = drawables[drawableID].skin.id; - + if (targetSkin === skin.id) { targetsWithVideo.push(target); } From 03da0b2714f27d37821ff019f7c85f55aa96b927 Mon Sep 17 00:00:00 2001 From: Muffin Date: Mon, 28 Aug 2023 13:04:06 -0500 Subject: [PATCH 27/34] Make it work more like animated text --- extensions/Lily/Video.js | 268 ++++++++++++++++++--------------------- 1 file changed, 122 insertions(+), 146 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 62a4c32633..4fd3cfcee0 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -10,84 +10,76 @@ const vm = Scratch.vm; const runtime = vm.runtime; + const renderer = vm.renderer; const Cast = Scratch.Cast; - class Video { - constructor() { - this.videos = []; - this.targets = []; - - runtime.on("PROJECT_STOP_ALL", () => { - for (const name of Object.keys(this.videos)) { - const video = this.videos[name].video; - video.pause(); - video.currentTime = 0; - } + const BitmapSkin = runtime.renderer.exports.BitmapSkin; + class VideoSkin extends BitmapSkin { + constructor (id, renderer, videoName, videoSrc) { + super(id, renderer); - for (const id of Object.keys(this.targets)) { - const target = this.targets[id].target; - target.updateAllDrawableProperties(); - } + /** @type {string} */ + this.videoName = videoName; - this.targets = []; - }); + /** @type {string} */ + this.videoSrc = videoSrc; - runtime.on("PROJECT_START", () => { - for (const name of Object.keys(this.videos)) { - const video = this.videos[name].video; - video.pause(); - video.currentTime = 0; - } + this.videoElement = document.createElement('video'); + // Need to set non-zero dimensions, otherwise scratch-render thinks this is an empty image + this.videoElement.width = 1; + this.videoElement.height = 1; + this.videoElement.crossOrigin = "anonymous"; + this.videoElement.onloadeddata = () => this.markVideoDirty(); + this.videoElement.src = videoSrc; + this.videoElement.currentTime = 0; - for (const id of Object.keys(this.targets)) { - const target = this.targets[id].target; - target.updateAllDrawableProperties(); - } + this.videoDirty = true; - this.targets = []; - }); + this.reuploadVideo(); + } - runtime.on("BEFORE_EXECUTE", () => { - for (const name of Object.keys(this.videos)) { - const videoObject = this.videos[name]; + reuploadVideo () { + console.log('reupload'); + this.videoDirty = false; + this.setBitmap(this.videoElement); + } - const video = videoObject.video; - const videoVolume = videoObject.volume; - const skin = videoObject.skin; + markVideoDirty () { + console.log('dirty'); + this.videoDirty = true; + this.emitWasAltered(); + } - const projectVolume = runtime.audioEngine.inputNode.gain.value; - video.volume = videoVolume * projectVolume; + getTexture (scale) { + if (this.videoDirty) { + this.reuploadVideo(); + } + return super.getTexture(scale); + } - const drawables = vm.renderer._allDrawables; - let targetsWithVideo = []; + dispose () { + super.dispose(); + this.videoElement.pause(); + } + } - for (const target of runtime.targets) { - const drawableID = target.drawableID; - const targetSkin = drawables[drawableID].skin.id; + class Video { + constructor() { + /** @type {Record} */ + this.videos = Object.create(null); - if (targetSkin === skin.id) { - targetsWithVideo.push(target); - } - } + runtime.on("PROJECT_STOP_ALL", () => this.resetEverything()); + runtime.on("PROJECT_START", () => this.resetEverything()); - if (!video.paused || targetsWithVideo.length > 0) { - vm.renderer.updateBitmapSkin(skin, video, 1); + runtime.on("BEFORE_EXECUTE", () => { + for (const skin of renderer._allSkins) { + if (skin instanceof VideoSkin && !skin.videoElement.paused) { + skin.markVideoDirty(); } } - - for (const id of Object.keys(this.targets)) { - const target = this.targets[id].target; - const drawableID = target.drawableID; - - const drawable = runtime.renderer._allDrawables[drawableID]; - // This was only a problem when targets weren't reset, I don't - // expect it to happen much now but just in case.. - if (!drawable) continue; - const skin = this.videos[this.targets[id].videoName].skin; - drawable.skin = vm.renderer._allSkins[skin]; - } }); } + getInfo() { return { id: "lmsVideo", @@ -251,18 +243,7 @@ defaultValue: 100, }, }, - }, - { - opcode: "getVolume", - blockType: Scratch.BlockType.REPORTER, - text: "volume of video [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "my video", - }, - }, - }, + } ], menus: { targets: { @@ -275,48 +256,53 @@ }, attribute: { acceptReporters: false, - items: ["current time", "duration", "width", "height"], + items: ["current time", "duration", "volume", "width", "height"], }, }, }; } + resetEverything () { + for (const {videoElement} of Object.values(this.videos)) { + videoElement.pause(); + videoElement.currentTime = 0; + } + + for (const target of runtime.targets) { + const drawable = renderer._allDrawables[target.drawableID]; + if (drawable instanceof VideoSkin) { + target.setCostume(target.currentCostume); + } + } + } + async loadVideoURL(args) { + // Always delete the old video with the same name, if it exists. + this.deleteVideoURL(args); + const videoName = Cast.toString(args.NAME); const url = Cast.toString(args.URL); if (!(await Scratch.canFetch(url))) return; - this.videos[videoName] = { - video: document.createElement("video"), - volume: 1, - skin: 0, - }; - - const videoObject = this.videos[videoName]; - const video = videoObject.video; - - video.width = 1; - video.height = 1; - video.crossOrigin = "anonymous"; - - video.src = url; - video.currentTime = 0; - - videoObject.skin = vm.renderer.createBitmapSkin(video); + const skinId = renderer._nextSkinId++; + const skin = new VideoSkin(skinId, renderer, videoName, url); + renderer._allSkins[skinId] = skin; + this.videos[videoName] = skin; } deleteVideoURL(args) { const videoName = Cast.toString(args.NAME); - if (!this.videos[videoName]) return; + const videoSkin = this.videos[videoName]; + if (!videoSkin) return; - for (const id of Object.keys(this.targets)) { - if (this.targets[id].videoName === videoName) { - this.targets[id].target.updateAllDrawableProperties(); - Reflect.deleteProperty(this.targets, id); + for (const target of runtime.targets) { + const drawable = renderer._allDrawables[target.drawableID]; + if (drawable && drawable.skin === videoSkin) { + target.setCostume(target.currentCostume); } } - this.videos[videoName].video.pause(); + renderer.destroySkin(videoSkin.id); Reflect.deleteProperty(this.videos, videoName); } @@ -328,13 +314,10 @@ const targetName = Cast.toString(args.TARGET); const videoName = Cast.toString(args.NAME); const target = this._getTargetFromMenu(targetName, util); - if (!target || !this.videos[videoName]) return; + const videoSkin = this.videos[videoName]; + if (!target || !videoSkin) return; - const targetId = target.id; - this.targets[targetId] = { - target: target, - videoName: videoName, - }; + vm.renderer.updateDrawableSkinId(target.drawableID, videoSkin._id); } stopShowingVideo(args, util) { @@ -342,10 +325,7 @@ const target = this._getTargetFromMenu(targetName, util); if (!target) return; - const targetId = target.id; - Reflect.deleteProperty(this.targets, targetId); - - target.updateAllDrawableProperties(); + target.setCostume(target.currentCostume); } getCurrentVideo(args, util) { @@ -353,37 +333,38 @@ const target = this._getTargetFromMenu(targetName, util); if (!target) return; - const targetId = target.id; - return this.targets[targetId] ? this.targets[targetId].videoName : ""; + const drawable = renderer._allDrawables[target.drawableID]; + const skin = drawable && drawable.skin; + return skin instanceof VideoSkin ? skin.videoName : ''; } startVideo(args) { const videoName = Cast.toString(args.NAME); const duration = Cast.toNumber(args.DURATION); - const videoObject = this.videos[videoName]; - if (!videoObject) return; + const videoSkin = this.videos[videoName]; + if (!videoSkin) return; - videoObject.video.play(); - videoObject.video.currentTime = duration; + videoSkin.videoElement.play(); + videoSkin.videoElement.currentTime = duration; + videoSkin.markVideoDirty(); } getAttribute(args) { const videoName = Cast.toString(args.NAME); - const videoObject = this.videos[videoName]; - if (!videoObject) return 0; - - const skinId = videoObject.skin; - const skin = vm.renderer._allSkins[skinId]; + const videoSkin = this.videos[videoName]; + if (!videoSkin) return 0; switch (args.ATTRIBUTE) { case "current time": - return videoObject.video.currentTime; + return videoSkin.videoElement.currentTime; case "duration": - return videoObject.video.duration; + return videoSkin.videoElement.duration; + case "volume": + return videoSkin.videoElement.volume * 100; case "width": - return skin._textureSize[0]; + return videoSkin._textureSize[0]; case "height": - return skin._textureSize[1]; + return videoSkin._textureSize[1]; default: return 0; } @@ -391,52 +372,46 @@ pause(args) { const videoName = Cast.toString(args.NAME); - const videoObject = this.videos[videoName]; - if (!videoObject) return; + const videoSkin = this.videos[videoName]; + if (!videoSkin) return; - videoObject.video.pause(); + videoSkin.videoElement.pause(); + videoSkin.markVideoDirty(); } resume(args) { const videoName = Cast.toString(args.NAME); - const videoObject = this.videos[videoName]; - if (!videoObject) return; + const videoSkin = this.videos[videoName]; + if (!videoSkin) return; - videoObject.video.play(); + videoSkin.videoElement.play(); + videoSkin.markVideoDirty(); } getState(args) { const videoName = Cast.toString(args.NAME); - const videoObject = this.videos[videoName]; - if (!videoObject) return args.STATE === "paused"; + const videoSkin = this.videos[videoName]; + if (!videoSkin) return args.STATE === "paused"; return args.STATE == "playing" - ? !videoObject.video.paused - : videoObject.video.paused; + ? !videoSkin.videoElement.paused + : videoSkin.videoElement.paused; } setVolume(args) { const videoName = Cast.toString(args.NAME); const value = Cast.toNumber(args.VALUE); - const videoObject = this.videos[videoName]; - if (!videoObject) return; - - videoObject.volume = value / 100; - } + const videoSkin = this.videos[videoName]; + if (!videoSkin) return; - getVolume(args) { - const videoName = Cast.toString(args.NAME); - const videoObject = this.videos[videoName]; - if (!videoObject) return 0; - - return videoObject.volume * 100; + videoSkin.videoElement.volume = value / 100; } + /** @returns {VM.Target|undefined} */ _getTargetFromMenu(targetName, util) { - let target = Scratch.vm.runtime.getSpriteTargetByName(targetName); - if (targetName === "_myself_") target = util.target; - if (targetName === "_stage_") target = runtime.getTargetForStage(); - return target; + if (targetName === "_myself_") return util.target; + if (targetName === "_stage_") return runtime.getTargetForStage(); + return Scratch.vm.runtime.getSpriteTargetByName(targetName); } _getTargets() { @@ -451,5 +426,6 @@ return spriteNames; } } + Scratch.extensions.register(new Video()); })(Scratch); From 476a048c3206b42ed974f05cbbb5dd0123739db6 Mon Sep 17 00:00:00 2001 From: Muffin Date: Mon, 28 Aug 2023 13:16:27 -0500 Subject: [PATCH 28/34] Show a classic ? image when the video fails to load --- extensions/Lily/Video.js | 59 ++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 4fd3cfcee0..ff61444321 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -15,7 +15,7 @@ const BitmapSkin = runtime.renderer.exports.BitmapSkin; class VideoSkin extends BitmapSkin { - constructor (id, renderer, videoName, videoSrc) { + constructor(id, renderer, videoName, videoSrc) { super(id, renderer); /** @type {string} */ @@ -24,12 +24,21 @@ /** @type {string} */ this.videoSrc = videoSrc; - this.videoElement = document.createElement('video'); + this.videoError = false; + + this.videoElement = document.createElement("video"); // Need to set non-zero dimensions, otherwise scratch-render thinks this is an empty image this.videoElement.width = 1; this.videoElement.height = 1; this.videoElement.crossOrigin = "anonymous"; - this.videoElement.onloadeddata = () => this.markVideoDirty(); + this.videoElement.onloadeddata = () => { + // First frame loaded + this.markVideoDirty(); + }; + this.videoElement.onerror = () => { + this.videoError = true; + this.markVideoDirty(); + }; this.videoElement.src = videoSrc; this.videoElement.currentTime = 0; @@ -38,26 +47,48 @@ this.reuploadVideo(); } - reuploadVideo () { - console.log('reupload'); + reuploadVideo() { this.videoDirty = false; - this.setBitmap(this.videoElement); + if (this.videoError) { + // Draw an image that looks similar to Scratch's normal costume loading errors + const canvas = document.createElement("canvas"); + canvas.width = this.videoElement.videoWidth || 128; + canvas.height = this.videoElement.videoHeight || 128; + const ctx = canvas.getContext("2d"); + + if (ctx) { + ctx.fillStyle = "#cccccc"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const fontSize = Math.min(canvas.width, canvas.height); + ctx.fillStyle = "#000000"; + ctx.font = `${fontSize}px serif`; + ctx.textBaseline = "middle"; + ctx.textAlign = "center"; + ctx.fillText("?", canvas.width / 2, canvas.height / 2); + } else { + // guess we can't draw the error then + } + + this.setBitmap(canvas); + } else { + this.setBitmap(this.videoElement); + } } - markVideoDirty () { - console.log('dirty'); + markVideoDirty() { this.videoDirty = true; this.emitWasAltered(); } - getTexture (scale) { + getTexture(scale) { if (this.videoDirty) { this.reuploadVideo(); } return super.getTexture(scale); } - dispose () { + dispose() { super.dispose(); this.videoElement.pause(); } @@ -243,7 +274,7 @@ defaultValue: 100, }, }, - } + }, ], menus: { targets: { @@ -262,8 +293,8 @@ }; } - resetEverything () { - for (const {videoElement} of Object.values(this.videos)) { + resetEverything() { + for (const { videoElement } of Object.values(this.videos)) { videoElement.pause(); videoElement.currentTime = 0; } @@ -335,7 +366,7 @@ const drawable = renderer._allDrawables[target.drawableID]; const skin = drawable && drawable.skin; - return skin instanceof VideoSkin ? skin.videoName : ''; + return skin instanceof VideoSkin ? skin.videoName : ""; } startVideo(args) { From fc62b5c647cbc5244d340cd4f5126e636f679368 Mon Sep 17 00:00:00 2001 From: Muffin Date: Mon, 28 Aug 2023 13:19:09 -0500 Subject: [PATCH 29/34] Yell at people when they try to use YouTube links, as many will --- extensions/Lily/Video.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index ff61444321..84d958c320 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -313,6 +313,15 @@ const videoName = Cast.toString(args.NAME); const url = Cast.toString(args.URL); + + if (url.startsWith('https://www.youtube.com/') || url.startsWith('https://youtube.com/')) { + alert([ + 'The video extension does not support YouTube links.', + 'You can use the Iframe extension instead.' + ].join('\n\n')); + return; + } + if (!(await Scratch.canFetch(url))) return; const skinId = renderer._nextSkinId++; From 10cc535af3f3328f0c42c0265a0845d1aec26aaa Mon Sep 17 00:00:00 2001 From: Muffin Date: Mon, 28 Aug 2023 16:04:10 -0500 Subject: [PATCH 30/34] prettier --- extensions/Lily/Video.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 84d958c320..3356364d12 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -314,11 +314,16 @@ const videoName = Cast.toString(args.NAME); const url = Cast.toString(args.URL); - if (url.startsWith('https://www.youtube.com/') || url.startsWith('https://youtube.com/')) { - alert([ - 'The video extension does not support YouTube links.', - 'You can use the Iframe extension instead.' - ].join('\n\n')); + if ( + url.startsWith("https://www.youtube.com/") || + url.startsWith("https://youtube.com/") + ) { + alert( + [ + "The video extension does not support YouTube links.", + "You can use the Iframe extension instead.", + ].join("\n\n") + ); return; } From 9e499ca8686041b76cd4d02c90fd402cc01b9993 Mon Sep 17 00:00:00 2001 From: Muffin Date: Tue, 29 Aug 2023 15:48:32 -0500 Subject: [PATCH 31/34] Fix skins not resetting when project stopped --- extensions/Lily/Video.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 3356364d12..4000fdef0a 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -301,7 +301,7 @@ for (const target of runtime.targets) { const drawable = renderer._allDrawables[target.drawableID]; - if (drawable instanceof VideoSkin) { + if (drawable.skin instanceof VideoSkin) { target.setCostume(target.currentCostume); } } From ac721083d37e85d2ca4100712f37ebe620ab1057 Mon Sep 17 00:00:00 2001 From: Muffin Date: Tue, 29 Aug 2023 15:53:29 -0500 Subject: [PATCH 32/34] Use a more public API for video width/height --- extensions/Lily/Video.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 4000fdef0a..3217e7dcd3 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -81,6 +81,13 @@ this.emitWasAltered(); } + get size () { + if (this.videoDirty) { + this.reuploadVideo(); + } + return super.size; + } + getTexture(scale) { if (this.videoDirty) { this.reuploadVideo(); @@ -407,9 +414,9 @@ case "volume": return videoSkin.videoElement.volume * 100; case "width": - return videoSkin._textureSize[0]; + return videoSkin.size[0]; case "height": - return videoSkin._textureSize[1]; + return videoSkin.size[1]; default: return 0; } From 89b137b0db3175e14aef0270807f6b80b02fed42 Mon Sep 17 00:00:00 2001 From: Muffin Date: Tue, 29 Aug 2023 20:23:05 -0500 Subject: [PATCH 33/34] Wait until first frame is loaded or an error before resolving load --- extensions/Lily/Video.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 3217e7dcd3..960bf4130e 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -26,6 +26,10 @@ this.videoError = false; + this.readyPromise = new Promise((resolve) => { + this.readyCallback = resolve; + }); + this.videoElement = document.createElement("video"); // Need to set non-zero dimensions, otherwise scratch-render thinks this is an empty image this.videoElement.width = 1; @@ -33,10 +37,12 @@ this.videoElement.crossOrigin = "anonymous"; this.videoElement.onloadeddata = () => { // First frame loaded + this.readyCallback(); this.markVideoDirty(); }; this.videoElement.onerror = () => { this.videoError = true; + this.readyCallback(); this.markVideoDirty(); }; this.videoElement.src = videoSrc; @@ -340,6 +346,8 @@ const skin = new VideoSkin(skinId, renderer, videoName, url); renderer._allSkins[skinId] = skin; this.videos[videoName] = skin; + + return skin.readyPromise; } deleteVideoURL(args) { From 1fd11152b15e4e17dd528a1038a136a290843708 Mon Sep 17 00:00:00 2001 From: Muffin Date: Tue, 29 Aug 2023 20:24:31 -0500 Subject: [PATCH 34/34] prettier --- extensions/Lily/Video.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 960bf4130e..307e89f2ec 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -87,7 +87,7 @@ this.emitWasAltered(); } - get size () { + get size() { if (this.videoDirty) { this.reuploadVideo(); }
-

- -
TurboWarp Extension Gallery
-

- -

Unlike custom extensions on other websites, these aren't limited by the extension sandbox, so they are a lot more powerful. All extensions are reviewed for safety.

-

- <% if (mode === 'desktop') { %> - To use these extensions in TurboWarp Desktop, hover over the extension and press the add button. - <% } else { %> - To use multiple of these extensions in TurboWarp, hover over the extension and press the button to copy its URL. Then go to the editor, open the extension chooser, then choose the "Custom Extension" option at the bottom, and enter the URL. - <% } %> -

- -
- -
- <% if (mode === 'desktop') { %> -
Some extensions may be outdated or missing.
- <% - const now = new Date(); - const date = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`; - %> - For compatibility, security, and offline support, TurboWarp Desktop includes an offline copy of extensions.turbowarp.org from this update's release date (<%= date %>). - Compared to the live website, some extensions may be missing or outdated. - <% } else { %> -
Some extensions may not work in TurboWarp Desktop.
- For compatibility, security, and offline support, each TurboWarp Desktop update contains an offline copy of these extensions from its release date, so some extensions may be outdated or missing. - Use the latest update for best results. - <% } %> -
-