Skip to content
5 changes: 5 additions & 0 deletions features/features.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
[
{
"version": 2,
"id": "picture-in-picture",
"versionAdded": "v5.0.0"
},
{
"version": 2,
"id": "mutual-following",
Expand Down
23 changes: 23 additions & 0 deletions features/picture-in-picture/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"title": "Picture in Picture",
"description": "Adds a button to the project page that allows you to open the stage up and continue to view it while using other tabs or apps.",
"credits": [
{
"username": "stio_studio",
"url": "https://stio.studio/"
}
],
"type": [
"Website"
],
"tags": [
"New",
"Featured"
],
"scripts": [
{
"file": "picture-in-picture.js",
"runOn": "/projects/*"
}
]
}
148 changes: 148 additions & 0 deletions features/picture-in-picture/picture-in-picture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
export default async function ({ feature, console }) {
const ALLOW_INTERACTIVITY = false

await new Promise(async (resolve, reject) => {
(async () => {
const rem = await ScratchTools.waitForElement(".preview .inner .flex-row.action-buttons")
resolve(rem);
})();
(async () => {
const rem = await ScratchTools.waitForElement(".menu-bar_account-info-group_MeJZP")
resolve(rem);
})();
})

const canvas = feature.traps.vm.renderer.canvas;
let openPopup = document.createElement("button");

ScratchTools.waitForElements(".preview .inner .flex-row.action-buttons", async function (row) {
if (row.querySelector(".ste-picture-in-picture")) return;
openPopup = document.createElement("button");
openPopup.className = "button action-button ste-picture-in-picture";
openPopup.textContent = "Picture in Picture";
row.insertAdjacentElement("afterbegin", openPopup);
openPopup.addEventListener('click', () => {
popup()
})
})
ScratchTools.waitForElements(".menu-bar_account-info-group_MeJZP", async function (row) {
if (row.querySelector(".ste-picture-in-picture")) return;
openPopup = document.createElement("div");
openPopup.className = "menu-bar_menu-bar-item_oLDa- menu-bar_hoverable_c6WFB";
let rem = document.createElement("div");
rem.textContent = "Picture in Picture";
openPopup.append(rem);
row.insertAdjacentElement("afterbegin", openPopup);
openPopup.addEventListener('click', () => {
popup()
})
})

let popup;

// Code for allowing interactivity (not yet ready)
if (ALLOW_INTERACTIVITY) {
if (!"documentPictureInPicture" in window) console.error("Picture in Picture not supported")

let pipWindow

let docPopup = document.createElement("div");
docPopup.insertAdjacentHTML("afterbegin", await (await fetch(feature.self.getResource("popup-html"))).text())
docPopup = docPopup.querySelector("div.popup-GUI")

let video = docPopup.querySelector("video");

const greenFlag = document.querySelector(".green-flag_green-flag_1kiAo")
docPopup.querySelector(".popup-greenflag").addEventListener("click", () => {
greenFlag.click()
});
const redFlag = document.querySelector(".stop-all_stop-all_1Y8P9")
docPopup.querySelector(".popup-redflag").addEventListener("click", () => {
redFlag.click()
});

// video.addEventListener("mousedown", (old_event) => {
function translateEvent_pointer(old_event) {
// Calculate the canvas position relative to the viewport
const a_rect = canvas.getBoundingClientRect();
const b_rect = video.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
canvas.dispatchEvent(new_event);
}
video.addEventListener("mousedown", translateEvent_pointer)
video.addEventListener("mouseup", translateEvent_pointer)
video.addEventListener("mousemove", translateEvent_pointer)
video.addEventListener("wheel", translateEvent_pointer)
video.addEventListener("touchstart", translateEvent_pointer)
video.addEventListener("touchend", translateEvent_pointer)
video.addEventListener("touchmove", translateEvent_pointer)

function translateEvent_key(old_event) {
let new_event = new KeyboardEvent(old_event.type, old_event)
document.dispatchEvent(new_event);
}

let buttonClickedTimes = 0
popup = async function () {
if (buttonClickedTimes === 0) {
video.srcObject = canvas.captureStream()
buttonClickedTimes++
}
// Open a Picture-in-Picture window.
pipWindow = await window.documentPictureInPicture.requestWindow({
width: canvas.width,
height: canvas.height + 20 + 6 * 2,
});

// Move the player to the Picture-in-Picture window.
pipWindow.document.body.append(docPopup);

pipWindow.document.addEventListener("keydown", translateEvent_key)
pipWindow.document.addEventListener("keypress", translateEvent_key)
pipWindow.document.addEventListener("keyup", translateEvent_key)
}
}
else {
let video = document.createElement("video");
// video.setAttribute("controls", "controls");
video.setAttribute("autoplay", "autoplay");
video.setAttribute("style", "width: 100%; height: 100%");
// document.querySelector(".preview .inner").append(video);

video.srcObject = canvas.captureStream()

popup = function () {
try {
video.requestPictureInPicture()
}
catch {
console.log("Picture in Picture not supported or failed to request")
}
}
}
}
40 changes: 40 additions & 0 deletions features/picture-in-picture/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<div class="popup-GUI">
<style>
.popup-GUI {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.popup-GUI video {
max-width: 100%;
max-height: 100%;
aspect-ratio: 960/720;
}
.popup-GUI .popup-canvas {
width: 100%;
height: calc(100% - 20px);
display: flex;
justify-content: center;
}
.popup-GUI .popup-buttons {
margin: auto;
width: 100%;
height: calc(20px + 6px * 2);
}
.popup-GUI .popup-buttons img {
height: calc(100% - 6px * 2);
aspect-ratio: 1/1;
padding: 6px;
}
</style>
<div class="popup-buttons">
<img class="popup-greenflag" draggable="false" src="data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNi42MyAxNy41Ij48ZGVmcz48c3R5bGU+LmNscy0xLC5jbHMtMntmaWxsOiM0Y2JmNTY7c3Ryb2tlOiM0NTk5M2Q7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO30uY2xzLTJ7c3Ryb2tlLXdpZHRoOjEuNXB4O308L3N0eWxlPjwvZGVmcz48dGl0bGU+aWNvbi0tZ3JlZW4tZmxhZzwvdGl0bGU+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNLjc1LDJBNi40NCw2LjQ0LDAsMCwxLDguNDQsMmgwYTYuNDQsNi40NCwwLDAsMCw3LjY5LDBWMTIuNGE2LjQ0LDYuNDQsMCwwLDEtNy42OSwwaDBhNi40NCw2LjQ0LDAsMCwwLTcuNjksMCIvPjxsaW5lIGNsYXNzPSJjbHMtMiIgeDE9IjAuNzUiIHkxPSIxNi43NSIgeDI9IjAuNzUiIHkyPSIwLjc1Ii8+PC9zdmc+" title="Go">
<img class="popup-redflag" draggable="false" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNCAxNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTQgMTQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRUM1OTU5O3N0cm9rZTojQjg0ODQ4O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9Cjwvc3R5bGU+Cjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iNC4zLDAuNSA5LjcsMC41IDEzLjUsNC4zIDEzLjUsOS43IDkuNywxMy41IDQuMywxMy41IDAuNSw5LjcgMC41LDQuMyAiLz4KPC9zdmc+Cg==" title="Stop">
</div>
<div class="popup-canvas">
<video autoplay></video>
</div>
</div>