commit 63361cf5d13671557ede052ef9a30c65a29077bd Author: Isaiah Billingsley Date: Thu Feb 19 08:47:37 2026 +0000 Complete rewrite from scratch - Plain JS - BS+ only - Simple song info only - No configuration diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..71f9966 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = tab +max_line_length = 120 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d0da584 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) 2025 iza + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0fa363e --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Beat Saber Overlay + +Simple Beat Saber stream overlay for https://www.twitch.tv/iza_k + +Requires BeatSaberPlus + +image diff --git a/dprint.json b/dprint.json new file mode 100644 index 0000000..b98599f --- /dev/null +++ b/dprint.json @@ -0,0 +1,18 @@ +{ + "lineWidth": 120, + "newLineKind": "lf", + "useTabs": true, + "typescript": { "useBraces": "maintain" }, + "json": {}, + "markdown": {}, + "malva": {}, + "markup": {}, + "excludes": ["**/node_modules", "**/build", "**/dist", "**/*-lock.json", "**/*.svg"], + "plugins": [ + "https://plugins.dprint.dev/typescript-0.95.15.wasm", + "https://plugins.dprint.dev/json-0.21.1.wasm", + "https://plugins.dprint.dev/markdown-0.21.1.wasm", + "https://plugins.dprint.dev/g-plane/malva-v0.15.2.wasm", + "https://plugins.dprint.dev/g-plane/markup_fmt-v0.26.0.wasm" + ] +} diff --git a/fonts/montserrat.woff2 b/fonts/montserrat.woff2 new file mode 100644 index 0000000..ab6fda2 Binary files /dev/null and b/fonts/montserrat.woff2 differ diff --git a/images/characteristic/360Degree.svg b/images/characteristic/360Degree.svg new file mode 100644 index 0000000..6bd7697 --- /dev/null +++ b/images/characteristic/360Degree.svg @@ -0,0 +1,48 @@ + + + + + + + + + +Lorem ipsum + diff --git a/images/characteristic/90Degree.svg b/images/characteristic/90Degree.svg new file mode 100644 index 0000000..a2a7e8e --- /dev/null +++ b/images/characteristic/90Degree.svg @@ -0,0 +1,22 @@ + + + + + + + diff --git a/images/characteristic/Lawless.svg b/images/characteristic/Lawless.svg new file mode 100644 index 0000000..350c2b9 --- /dev/null +++ b/images/characteristic/Lawless.svg @@ -0,0 +1,27 @@ + + + + + diff --git a/images/characteristic/Legacy.svg b/images/characteristic/Legacy.svg new file mode 100644 index 0000000..7c91767 --- /dev/null +++ b/images/characteristic/Legacy.svg @@ -0,0 +1,29 @@ + + + + + + diff --git a/images/characteristic/Lightshow.svg b/images/characteristic/Lightshow.svg new file mode 100644 index 0000000..d643caf --- /dev/null +++ b/images/characteristic/Lightshow.svg @@ -0,0 +1,25 @@ + + + + + diff --git a/images/characteristic/NoArrows.svg b/images/characteristic/NoArrows.svg new file mode 100644 index 0000000..c0ffed5 --- /dev/null +++ b/images/characteristic/NoArrows.svg @@ -0,0 +1,21 @@ + + + + + diff --git a/images/characteristic/OneSaber.svg b/images/characteristic/OneSaber.svg new file mode 100644 index 0000000..be4bdbc --- /dev/null +++ b/images/characteristic/OneSaber.svg @@ -0,0 +1,14 @@ + + + + diff --git a/images/characteristic/Standard.svg b/images/characteristic/Standard.svg new file mode 100644 index 0000000..4389dab --- /dev/null +++ b/images/characteristic/Standard.svg @@ -0,0 +1,14 @@ + + + + diff --git a/images/unknown.svg b/images/unknown.svg new file mode 100644 index 0000000..39d3596 --- /dev/null +++ b/images/unknown.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..423c5c3 --- /dev/null +++ b/index.html @@ -0,0 +1,31 @@ + + + + + BS Overlay + + + +
+
+
+ Title + Subtitle +
+ +
+ Artist + Mapper +
+ +
+ Easy + + Diff Label + 25f + WIP +
+
+ + + diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..0a7a1a2 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "checkJs": true, + "target": "es2021" + } +} diff --git a/main.js b/main.js new file mode 100644 index 0000000..5f03de3 --- /dev/null +++ b/main.js @@ -0,0 +1,81 @@ +"use strict"; +// https://github.com/hardcpp/BeatSaberPlus/wiki/%5BEN%5D-Song-Overlay + +const bspUrl = "ws://localhost:2947/socket"; +const retryMs = 5000; +let retries = 0; + +function connect() { + console.log(`Connecting to ${bspUrl} (attempt ${retries++})`); + const ws = new WebSocket(bspUrl); + ws.onopen = () => console.log("Connection open."); + ws.onmessage = (e) => { + const data = JSON.parse(e.data); + switch (data._type) { + case "event": + switch (data._event) { + case "gameState": + document.body.className = data.gameStateChanged; + break; + + case "mapInfo": + updateMapInfo(data.mapInfoChanged); + break; + } + break; + + default: + console.log("message", e.data); + break; + } + }; + ws.onclose = (e) => { + console.log(`Connection closed. code: ${e.code}, reason: ${e.reason}, clean: ${e.wasClean}`); + setTimeout(connect, retryMs); + }; +} + +const cover = document.getElementById("cover"); +const title = document.getElementById("title"); +const subTitle = document.getElementById("subTitle"); +const artist = document.getElementById("artist"); +const mapper = document.getElementById("mapper"); +const difficulty = document.getElementById("difficulty"); +const characteristicIcon = document.getElementById("characteristicIcon"); +const difficultyLabel = document.getElementById("difficultyLabel"); +const bsrKey = document.getElementById("bsrKey"); +const type = document.getElementById("type"); + +/** @param {MapInfoChanged} data */ +function updateMapInfo(data) { + const custom = data.level_id.startsWith("custom_level_"); + cover.style.backgroundImage = data.coverRaw ? `url("data:image/jpeg;base64,${data.coverRaw}")` : ""; + title.textContent = data.name || ""; + subTitle.textContent = data.sub_name || ""; + artist.textContent = data.artist || ""; + mapper.textContent = data.mapper || ""; + difficulty.textContent = data.difficulty.replace("Plus", " +") || ""; + characteristicIcon.setAttribute("src", `images/characteristic/${data.characteristic}.svg`); + difficultyLabel.textContent = ""; + bsrKey.textContent = data.BSRKey || ""; + type.textContent = !custom ? "OST" : data.level_id.endsWith(" WIP") ? "WIP" : ""; + + if (custom) { + fetch(`https://api.beatsaver.com/maps/hash/${data.level_id.substring(13, 53)}`) + .then(response => response.json()) + .then(map => { + if (!map.id) return; + bsrKey.textContent = map.id; + mapper.textContent = map.metadata.levelAuthorName; // Replace mapper name with full authors list + // Find difficulty label + const diff = map.versions[0].diffs.find(d => + d.characteristic === data.characteristic && d.difficulty === data.difficulty + ); + if (diff.label) difficultyLabel.textContent = diff.label; + }); + } +} + +connect(); + +document.documentElement.onclick = () => document.body.classList.toggle("Playing"); diff --git a/style.css b/style.css new file mode 100644 index 0000000..34a40a1 --- /dev/null +++ b/style.css @@ -0,0 +1,113 @@ +@font-face { + font-family: "Montserrat"; + font-display: swap; + font-weight: 600 700; + src: url("fonts/montserrat.woff2") format("woff2"); +} + +:root { + font-size: 10px; +} + +body { + display: flex; + margin: 0; + padding: 1.4rem 1.6rem; + font-family: "Montserrat", sans-serif; + white-space: nowrap; + background: #0000; + color: white; + filter: drop-shadow(0 0 0.1rem black) drop-shadow(0 0 0.1rem black); + transition: opacity 300ms ease-out; + transition-delay: 200ms; +} + +body:not(.Playing) { + opacity: 0; +} + +#cover { + width: 6.8rem; + height: 6.8rem; + border-radius: 0.6rem; + background-image: url("images/unknown.svg"); + background-repeat: no-repeat; + background-size: contain; + flex-shrink: 0; +} + +#songData { + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: space-between; + margin: -0.4rem 1.6rem -0.2rem; + overflow: hidden; + line-height: 1.2; + font-size: 1.5rem; + font-weight: 600; +} + +#songData * { + overflow: hidden; + text-overflow: ellipsis; +} + +#songData .row { + display: flex; + align-items: baseline; + column-gap: 0.6em; +} + +#songData span:empty { + display: none; +} + +#title { + font-size: 2.4rem; + font-weight: 700; +} + +#subTitle, +#artist, +#mapper { + font-size: 1.6rem; +} + +#mapper:before, +#difficultyLabel:before { + content: "‹"; +} + +#mapper:after, +#difficultyLabel:after { + content: "›"; +} + +#difficultyLabel, +#subTitle { + flex-shrink: 99999; +} + +#characteristicIcon { + width: 1.1em; + height: 1.1em; + flex-shrink: 0; + align-self: center; +} + +#type, +#bsrKey { + font-size: 1.6rem; + font-weight: 700; + flex-shrink: 0; +} + +#bsrKey { + letter-spacing: 0.2rem; +} + +#bsrKey:before { + content: "!bsr "; + letter-spacing: normal; +} diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..95da8af --- /dev/null +++ b/types.d.ts @@ -0,0 +1,20 @@ +interface Document { + getElementById(elementId: string): HTMLElement; +} + +interface MapInfoChanged { + "level_id": string; + "name": string; + "sub_name": string; + "artist": string; + "mapper": string; + "characteristic": string; + "difficulty": string; + "duration": number; + "BPM": number; + "PP": number; + "BSRKey": string; + "coverRaw": string; + "time": number; + "timeMultiplier": number; +}