Skip to content

Commit c441798

Browse files
authored
Merge pull request #936 from StioStudio/Picture-in-Picture
Picture in Picture
2 parents 9704f85 + abef64c commit c441798

File tree

4 files changed

+216
-0
lines changed

4 files changed

+216
-0
lines changed

features/features.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
[
2+
{
3+
"version": 2,
4+
"id": "picture-in-picture",
5+
"versionAdded": "v5.0.0"
6+
},
27
{
38
"version": 2,
49
"id": "mutual-following",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "Picture in Picture",
3+
"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.",
4+
"credits": [
5+
{
6+
"username": "stio_studio",
7+
"url": "https://stio.studio/"
8+
}
9+
],
10+
"type": [
11+
"Website"
12+
],
13+
"tags": [
14+
"New",
15+
"Featured"
16+
],
17+
"scripts": [
18+
{
19+
"file": "picture-in-picture.js",
20+
"runOn": "/projects/*"
21+
}
22+
]
23+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
export default async function ({ feature, console }) {
2+
const ALLOW_INTERACTIVITY = false
3+
4+
await new Promise(async (resolve, reject) => {
5+
(async () => {
6+
const rem = await ScratchTools.waitForElement(".preview .inner .flex-row.action-buttons")
7+
resolve(rem);
8+
})();
9+
(async () => {
10+
const rem = await ScratchTools.waitForElement(".menu-bar_account-info-group_MeJZP")
11+
resolve(rem);
12+
})();
13+
})
14+
15+
const canvas = feature.traps.vm.renderer.canvas;
16+
let openPopup = document.createElement("button");
17+
18+
ScratchTools.waitForElements(".preview .inner .flex-row.action-buttons", async function (row) {
19+
if (row.querySelector(".ste-picture-in-picture")) return;
20+
openPopup = document.createElement("button");
21+
openPopup.className = "button action-button ste-picture-in-picture";
22+
openPopup.textContent = "Picture in Picture";
23+
row.insertAdjacentElement("afterbegin", openPopup);
24+
openPopup.addEventListener('click', () => {
25+
popup()
26+
})
27+
})
28+
ScratchTools.waitForElements(".menu-bar_account-info-group_MeJZP", async function (row) {
29+
if (row.querySelector(".ste-picture-in-picture")) return;
30+
openPopup = document.createElement("div");
31+
openPopup.className = "menu-bar_menu-bar-item_oLDa- menu-bar_hoverable_c6WFB";
32+
let rem = document.createElement("div");
33+
rem.textContent = "Picture in Picture";
34+
openPopup.append(rem);
35+
row.insertAdjacentElement("afterbegin", openPopup);
36+
openPopup.addEventListener('click', () => {
37+
popup()
38+
})
39+
})
40+
41+
let popup;
42+
43+
// Code for allowing interactivity (not yet ready)
44+
if (ALLOW_INTERACTIVITY) {
45+
if (!"documentPictureInPicture" in window) console.error("Picture in Picture not supported")
46+
47+
let pipWindow
48+
49+
let docPopup = document.createElement("div");
50+
docPopup.insertAdjacentHTML("afterbegin", await (await fetch(feature.self.getResource("popup-html"))).text())
51+
docPopup = docPopup.querySelector("div.popup-GUI")
52+
53+
let video = docPopup.querySelector("video");
54+
55+
const greenFlag = document.querySelector(".green-flag_green-flag_1kiAo")
56+
docPopup.querySelector(".popup-greenflag").addEventListener("click", () => {
57+
greenFlag.click()
58+
});
59+
const redFlag = document.querySelector(".stop-all_stop-all_1Y8P9")
60+
docPopup.querySelector(".popup-redflag").addEventListener("click", () => {
61+
redFlag.click()
62+
});
63+
64+
// video.addEventListener("mousedown", (old_event) => {
65+
function translateEvent_pointer(old_event) {
66+
// Calculate the canvas position relative to the viewport
67+
const a_rect = canvas.getBoundingClientRect();
68+
const b_rect = video.getBoundingClientRect();
69+
70+
// console.log(old_event)
71+
// Create a new event with the adjusted coordinates
72+
73+
let new_event = new old_event.constructor(old_event.type, {
74+
bubbles: old_event.bubbles,
75+
cancelable: old_event.cancelable,
76+
clientX: (old_event.clientX - b_rect.left) * (a_rect.width / b_rect.width) + a_rect.left,
77+
clientY: (old_event.clientY - b_rect.top) * (a_rect.height / b_rect.height) + a_rect.top,
78+
// Copy over other necessary properties from the old event
79+
screenX: (old_event.screenX - pipWindow.screenLeft + window.screenLeft - b_rect.left) * (a_rect.width / b_rect.width) + a_rect.left,
80+
screenY: (old_event.screenY - pipWindow.screenTop + window.screenTop - b_rect.top) * (a_rect.height / b_rect.height) + a_rect.top,
81+
layerX: old_event.layerX,
82+
layerY: old_event.layerY,
83+
button: old_event.button,
84+
buttons: old_event.buttons,
85+
relatedTarget: old_event.relatedTarget,
86+
altKey: old_event.altKey,
87+
ctrlKey: old_event.ctrlKey,
88+
shiftKey: old_event.shiftKey,
89+
metaKey: old_event.metaKey,
90+
movementX: old_event.movementX,
91+
movementY: old_event.movementY,
92+
});
93+
94+
// Dispatch the new event
95+
canvas.dispatchEvent(new_event);
96+
}
97+
video.addEventListener("mousedown", translateEvent_pointer)
98+
video.addEventListener("mouseup", translateEvent_pointer)
99+
video.addEventListener("mousemove", translateEvent_pointer)
100+
video.addEventListener("wheel", translateEvent_pointer)
101+
video.addEventListener("touchstart", translateEvent_pointer)
102+
video.addEventListener("touchend", translateEvent_pointer)
103+
video.addEventListener("touchmove", translateEvent_pointer)
104+
105+
function translateEvent_key(old_event) {
106+
let new_event = new KeyboardEvent(old_event.type, old_event)
107+
document.dispatchEvent(new_event);
108+
}
109+
110+
let buttonClickedTimes = 0
111+
popup = async function () {
112+
if (buttonClickedTimes === 0) {
113+
video.srcObject = canvas.captureStream()
114+
buttonClickedTimes++
115+
}
116+
// Open a Picture-in-Picture window.
117+
pipWindow = await window.documentPictureInPicture.requestWindow({
118+
width: canvas.width,
119+
height: canvas.height + 20 + 6 * 2,
120+
});
121+
122+
// Move the player to the Picture-in-Picture window.
123+
pipWindow.document.body.append(docPopup);
124+
125+
pipWindow.document.addEventListener("keydown", translateEvent_key)
126+
pipWindow.document.addEventListener("keypress", translateEvent_key)
127+
pipWindow.document.addEventListener("keyup", translateEvent_key)
128+
}
129+
}
130+
else {
131+
let video = document.createElement("video");
132+
// video.setAttribute("controls", "controls");
133+
video.setAttribute("autoplay", "autoplay");
134+
video.setAttribute("style", "width: 100%; height: 100%");
135+
// document.querySelector(".preview .inner").append(video);
136+
137+
video.srcObject = canvas.captureStream()
138+
139+
popup = function () {
140+
try {
141+
video.requestPictureInPicture()
142+
}
143+
catch {
144+
console.log("Picture in Picture not supported or failed to request")
145+
}
146+
}
147+
}
148+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<div class="popup-GUI">
2+
<style>
3+
.popup-GUI {
4+
width: 100%;
5+
height: 100%;
6+
display: flex;
7+
flex-direction: column;
8+
align-items: center;
9+
justify-content: center;
10+
}
11+
.popup-GUI video {
12+
max-width: 100%;
13+
max-height: 100%;
14+
aspect-ratio: 960/720;
15+
}
16+
.popup-GUI .popup-canvas {
17+
width: 100%;
18+
height: calc(100% - 20px);
19+
display: flex;
20+
justify-content: center;
21+
}
22+
.popup-GUI .popup-buttons {
23+
margin: auto;
24+
width: 100%;
25+
height: calc(20px + 6px * 2);
26+
}
27+
.popup-GUI .popup-buttons img {
28+
height: calc(100% - 6px * 2);
29+
aspect-ratio: 1/1;
30+
padding: 6px;
31+
}
32+
</style>
33+
<div class="popup-buttons">
34+
<img class="popup-greenflag" draggable="false" src="data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNi42MyAxNy41Ij48ZGVmcz48c3R5bGU+LmNscy0xLC5jbHMtMntmaWxsOiM0Y2JmNTY7c3Ryb2tlOiM0NTk5M2Q7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO30uY2xzLTJ7c3Ryb2tlLXdpZHRoOjEuNXB4O308L3N0eWxlPjwvZGVmcz48dGl0bGU+aWNvbi0tZ3JlZW4tZmxhZzwvdGl0bGU+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNLjc1LDJBNi40NCw2LjQ0LDAsMCwxLDguNDQsMmgwYTYuNDQsNi40NCwwLDAsMCw3LjY5LDBWMTIuNGE2LjQ0LDYuNDQsMCwwLDEtNy42OSwwaDBhNi40NCw2LjQ0LDAsMCwwLTcuNjksMCIvPjxsaW5lIGNsYXNzPSJjbHMtMiIgeDE9IjAuNzUiIHkxPSIxNi43NSIgeDI9IjAuNzUiIHkyPSIwLjc1Ii8+PC9zdmc+" title="Go">
35+
<img class="popup-redflag" draggable="false" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNCAxNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTQgMTQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRUM1OTU5O3N0cm9rZTojQjg0ODQ4O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9Cjwvc3R5bGU+Cjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iNC4zLDAuNSA5LjcsMC41IDEzLjUsNC4zIDEzLjUsOS43IDkuNywxMy41IDQuMywxMy41IDAuNSw5LjcgMC41LDQuMyAiLz4KPC9zdmc+Cg==" title="Stop">
36+
</div>
37+
<div class="popup-canvas">
38+
<video autoplay></video>
39+
</div>
40+
</div>

0 commit comments

Comments
 (0)