diff --git a/features/features.json b/features/features.json index 779defe0..32e0549a 100644 --- a/features/features.json +++ b/features/features.json @@ -1,4 +1,9 @@ [ + { + "version": 2, + "id": "pip", + "versionAdded": "v4.0.0" + }, { "version": 2, "id": "remove-confirmation", diff --git a/features/pip/data.json b/features/pip/data.json new file mode 100644 index 00000000..0a0496c7 --- /dev/null +++ b/features/pip/data.json @@ -0,0 +1,24 @@ +{ + "title": "Picture in picture", + "description": "Picture in picture aka PiP, allows you to view your project in a separate small window.", + "credits": [ + { + "username": "stio_studio", + "url": "https://stio.studio" + } + ], + "scripts": [ + { + "file": "feature.js", + "runOn": "/projects/*" + } + ], + "tags": [ + "New" + ], + "type": [ + "Website" + ], + "dynamic": true, + "resources": [{ "name": "pip-css", "path": "/style.css" }] +} \ No newline at end of file diff --git a/features/pip/feature.js b/features/pip/feature.js new file mode 100644 index 00000000..fca013c6 --- /dev/null +++ b/features/pip/feature.js @@ -0,0 +1,121 @@ +export default async function ({ feature, console }) { + ScratchTools.waitForElements( + ".preview .inner .flex-row.action-buttons", + async function (row) { + if (row.querySelector(".ste-pip")) return; + let button = document.createElement("button"); + button.className = "button action-button ste-pip"; + button.textContent = "PiP"; + row.appendChild(button); + feature.self.hideOnDisable(button); + + let pipButton = button + + pipButton.addEventListener("click", async () => { + let fake_canvas = createFakeCanvas() + let doc = await createPipDoc(fake_canvas) + let pipWindow = await togglePictureInPicture(doc) + await addTranslatedEvents(fake_canvas, pipWindow, doc) + }) + } + ) + + function createFakeCanvas() { + const fake_canvas = document.createElement("video"); + fake_canvas.width = feature.traps.vm.renderer.canvas.width; + fake_canvas.height = feature.traps.vm.renderer.canvas.height; + fake_canvas.srcObject = feature.traps.vm.renderer.canvas.captureStream() + fake_canvas.play() + return fake_canvas; + } + + async function createPipDoc(fake_canvas) { + // Main doc + let doc = document.createElement("div"); doc.classList.add("popup-GUI") + + // Video container + let video_container = document.createElement("div"); video_container.classList.add("video-container") + doc.appendChild(video_container) + // Video + video_container.appendChild(fake_canvas) + + // CSS + let pip_css = await (await fetch(feature.self.getResource("pip-css"))).text() + let pip_css_elm = document.createElement("style") + pip_css_elm.textContent = pip_css + doc.appendChild(pip_css_elm) + + return doc + } + + async function togglePictureInPicture(doc) { + // Early return if there's already a Picture-in-Picture window open + if (window.documentPictureInPicture.window) { + return; + } + + // Open a Picture-in-Picture window. + const pipWindow = await window.documentPictureInPicture.requestWindow({ + width: feature.traps.vm.renderer.canvas.width / 2, + height: feature.traps.vm.renderer.canvas.height / 2, + }); + + // ... + + // Move the player to the Picture-in-Picture window. + pipWindow.document.body.append(doc); + return pipWindow + } + + function addTranslatedEvents(fake_canvas, pipWindow, doc) { + { + function translateEvent_pointer(old_event) { + // Calculate the canvas position relative to the viewport + let a_rect = feature.traps.vm.renderer.canvas.getBoundingClientRect(); + let b_rect = fake_canvas.getBoundingClientRect(); + + // console.log(old_event) + // Create a new event with the adjusted coordinates + + let new_event = new old_event.constructor(old_event.type, { + bubbles: old_event.bubbles, + cancelable: old_event.cancelable, + clientX: (old_event.clientX - b_rect.left) * (a_rect.width / b_rect.width) + a_rect.left, + clientY: (old_event.clientY - b_rect.top) * (a_rect.height / b_rect.height) + a_rect.top, + // Copy over other necessary properties from the old event + screenX: (old_event.screenX - pipWindow.screenLeft + window.screenLeft - b_rect.left) * (a_rect.width / b_rect.width) + a_rect.left, + screenY: (old_event.screenY - pipWindow.screenTop + window.screenTop - b_rect.top) * (a_rect.height / b_rect.height) + a_rect.top, + layerX: old_event.layerX, + layerY: old_event.layerY, + button: old_event.button, + buttons: old_event.buttons, + relatedTarget: old_event.relatedTarget, + altKey: old_event.altKey, + ctrlKey: old_event.ctrlKey, + shiftKey: old_event.shiftKey, + metaKey: old_event.metaKey, + movementX: old_event.movementX, + movementY: old_event.movementY, + }); + // Dispatch the new event + feature.traps.vm.renderer.canvas.dispatchEvent(new_event); + } + fake_canvas.addEventListener("mousedown", translateEvent_pointer) + fake_canvas.addEventListener("mouseup", translateEvent_pointer) + fake_canvas.addEventListener("mousemove", translateEvent_pointer) + fake_canvas.addEventListener("wheel", translateEvent_pointer) + fake_canvas.addEventListener("touchstart", translateEvent_pointer) + fake_canvas.addEventListener("touchend", translateEvent_pointer) + fake_canvas.addEventListener("touchmove", translateEvent_pointer) + + function translateEvent_key(old_event) { + let new_event = new KeyboardEvent(old_event.type, old_event) + document.dispatchEvent(new_event); + } + fake_canvas.addEventListener("keydown", translateEvent_key) + fake_canvas.addEventListener("keypress", translateEvent_key) + fake_canvas.addEventListener("keyup", translateEvent_key) + } + } + +} \ No newline at end of file diff --git a/features/pip/style.css b/features/pip/style.css new file mode 100644 index 00000000..18fea0bf --- /dev/null +++ b/features/pip/style.css @@ -0,0 +1,29 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: #f0f0f0; +} + +.video-container { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +video { + width: 100%; + height: auto; + max-width: calc(100vh * (4 / 3)); /* Maintain 480/360 ratio */ + max-height: 100vh; +}