diff --git a/views/archive/ruffle/demo/index.css b/views/archive/ruffle/demo/index.css
new file mode 100644
index 00000000..b05c67c1
--- /dev/null
+++ b/views/archive/ruffle/demo/index.css
@@ -0,0 +1,131 @@
+body {
+ position: absolute;
+ top: 0;
+ left: 0;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ font-family: "Lato", sans-serif;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background-color: black;
+}
+
+#main {
+ width: 100%;
+ height: 100%;
+ align-self: center;
+ max-height: calc(100vh - 70px);
+}
+
+#container {
+ height: calc(100vh - 70px);
+ width: 100vw;
+}
+
+#player {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+#nav {
+ width: 100%;
+ background: #37528c;
+ border: 0;
+ border-color: darkgray;
+ border-bottom: 2px #37528c;
+ border-style: solid;
+ box-shadow: 0 5px 5px #37528c;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-around;
+ align-items: center;
+ align-self: flex-start;
+ color: white;
+ min-height: 45px;
+ margin-bottom: 15px;
+}
+
+#title {
+ transition-duration: 0.5s;
+}
+
+#title:hover {
+ opacity: 0.5;
+ cursor: pointer;
+}
+
+#title img {
+ display: inline-block;
+ height: 32px;
+ margin-top: 5px;
+}
+
+#file-picker {
+ margin-top: 7px;
+}
+
+#file-picker select,
+#file-picker input {
+ margin: 0 5px;
+}
+
+#local-file-container {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+#sample-swfs-container {
+ display: none;
+ vertical-align: middle;
+}
+
+#author-container {
+ display: none;
+ font-size: 13px;
+ text-align: left;
+}
+
+#author {
+ padding-left: 5px;
+ color: #ffad33;
+}
+
+@media only screen and (max-width: 800px) {
+ #local-file-container,
+ #sample-swfs-container {
+ display: block;
+ }
+
+ #nav {
+ min-height: 65px;
+ }
+
+ #player {
+ height: calc(100vh - 90px) !important;
+ }
+}
+
+@media only screen and (max-width: 600px) {
+ #local-file-container {
+ display: none;
+ }
+
+ #sample-swfs-label {
+ display: none;
+ }
+
+ #author-container {
+ font-size: 12px;
+ text-align: center;
+ }
+
+ #nav {
+ min-height: 85px;
+ flex-direction: column;
+ padding-bottom: 2px;
+ }
+}
diff --git a/views/archive/ruffle/demo/index.html b/views/archive/ruffle/demo/index.html
new file mode 100644
index 00000000..40d4fb09
--- /dev/null
+++ b/views/archive/ruffle/demo/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+ Ruffle Web Demo
+
+
+
+
+
+
+
+
+ Local SWF:
+
+
+
+
Sample SWF:
+
+
+
+
+
+
+
+
+
diff --git a/views/archive/ruffle/demo/index.js b/views/archive/ruffle/demo/index.js
new file mode 100644
index 00000000..a5dfd6cf
--- /dev/null
+++ b/views/archive/ruffle/demo/index.js
@@ -0,0 +1,113 @@
+import "./index.css";
+
+const { SourceAPI, PublicAPI } = require("ruffle-core");
+
+window.RufflePlayer = PublicAPI.negotiate(
+ window.RufflePlayer,
+ "local",
+ new SourceAPI("local")
+);
+
+let ruffle;
+let player;
+
+const container = document.getElementById("main");
+const authorContainer = document.getElementById("author-container");
+const author = document.getElementById("author");
+const sampleFileInputContainer = document.getElementById(
+ "sample-swfs-container"
+);
+const sampleFileInput = document.getElementById("sample-swfs");
+const localFileInput = document.getElementById("local-file");
+const animOptGroup = document.getElementById("anim-optgroup");
+const gamesOptGroup = document.getElementById("games-optgroup");
+
+// Default config used by the player.
+const config = {
+ letterbox: "on",
+ logLevel: "warn",
+};
+
+window.addEventListener("DOMContentLoaded", async () => {
+ ruffle = window.RufflePlayer.newest();
+ player = ruffle.createPlayer();
+ player.id = "player";
+ container.append(player);
+
+ const response = await fetch("swfs.json");
+ if (!response.ok) {
+ sampleFileInputContainer.style.display = "none";
+ return;
+ }
+
+ const data = await response.json();
+ for (const swfData of data.swfs) {
+ const option = document.createElement("option");
+ option.textContent = swfData.title;
+ option.value = swfData.location;
+ option.swfData = swfData;
+ switch (swfData.type) {
+ case "Animation":
+ animOptGroup.append(option);
+ break;
+ case "Game":
+ gamesOptGroup.append(option);
+ break;
+ }
+ }
+ sampleFileInputContainer.style.display = "inline-block";
+
+ const initialFile = new URLSearchParams(window.location.search).get("file");
+ if (initialFile) {
+ const options = Array.from(sampleFileInput.options);
+ sampleFileInput.selectedIndex = Math.max(
+ options.findIndex((swfData) => swfData.value.endsWith(initialFile)),
+ 0
+ );
+ } else {
+ // Load a random file.
+ sampleFileInput.selectedIndex =
+ Math.floor(Math.random() * data.swfs.length) + 1;
+ }
+ sampleFileSelected();
+});
+
+sampleFileInput.addEventListener("change", sampleFileSelected);
+localFileInput.addEventListener("change", localFileSelected);
+
+function sampleFileSelected() {
+ const swfData = sampleFileInput[sampleFileInput.selectedIndex].swfData;
+ if (swfData) {
+ authorContainer.style.display = "block";
+ author.textContent = swfData.author;
+ author.href = swfData.authorLink;
+ localFileInput.value = null;
+ player.load({ url: swfData.location, ...config });
+ } else {
+ document.getElementById("main").children[0].remove();
+ player = ruffle.create_player();
+ player.id = "player";
+ container.append(player);
+ authorContainer.style.display = "none";
+ author.textContent = "";
+ author.href = "";
+ }
+}
+
+function localFileSelected() {
+ sampleFileInput.selectedIndex = 0;
+ authorContainer.style.display = "none";
+ author.textContent = "";
+ author.href = "";
+
+ const file = localFileInput.files[0];
+ if (!file) {
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = () => {
+ player.load({ data: reader.result, ...config });
+ };
+ reader.readAsArrayBuffer(file);
+}