diff --git a/assets/instruments/instruments.json b/assets/instruments/instruments.json index 1137269..3d3512c 100644 --- a/assets/instruments/instruments.json +++ b/assets/instruments/instruments.json @@ -8,7 +8,7 @@ "bdrumnew-hit-v7-rr1-sum-(1).mp3" ], "midi-channel-name": "conductorOrchestralBass", - "image": { "filename": "bass.svg", "yPos": "13%" } + "image": { "filename": "bass.svg", "yPos": "13%", "width": "15vw", "height": "15vw" } }, "Snare": { "directory": "/assets/instruments/snare", @@ -17,20 +17,20 @@ "ropesnare-low-tsn-main-vl4-rr1.mp3" ], "midi-channel-name": "conductorSnare", - "image": { "filename": "snare.svg", "yPos": "26%" } + "image": { "filename": "snare.svg", "yPos": "26%", "width": "15vw", "height": "15vw" } }, "Surdo": { "directory": "/assets/instruments/surdo", "filenames": ["surdo-5.mp3", "surdo-6.mp3", "surdo-7.mp3"], "midi-channel-name":"conductorSnare", - "image": { "filename": "surdo.svg", "yPos": "39%" } + "image": { "filename": "surdo.svg", "yPos": "39%" , "width": "15vw", "height": "15vw"} }, "Surdo Napa": { "directory": "/assets/instruments/surdo-napa", "filenames": ["surdo-1.mp3", "surdo-3.mp3", "surdo-4.mp3"], "midi-channel-name": "conductorSurdoNapa", - "image": { "filename": "surdo-napa.svg", "yPos": "42%" } - }, + "image": { "filename": "surdo-napa.svg", "yPos": "42%" , "width": "15vw", "height": "15vw" +} }, "Timpani Large": { "directory": "/assets/instruments/timpani-large", "filenames": [ @@ -40,8 +40,8 @@ "timpani7d-hit-v2-rr1-main.mp3" ], "midi-channel-name":"conductorTimpaniLarge", - "image": { "filename": "timpani-large.svg", "yPos": "55%" } - }, + "image": { "filename": "timpani-large.svg", "yPos": "55%" , "width": "15vw", "height": "15vw" +} }, "Timpani Small": { "directory": "/assets/instruments/timpani-small", "filenames": [ "timpani1a-hit-v5-rr1-main.mp3", @@ -49,7 +49,7 @@ "timpani4-hit-v2-rr1-sum.mp3" ], "midi-channel-name": "conductorTimpaniSmall", - "image": { "filename": "timpani-small.svg", "yPos": "68%" } + "image": { "filename": "timpani-small.svg", "yPos": "68%" , "width": "15vw", "height": "15vw"} }, "Toms": { "directory": "/assets/instruments/toms", @@ -59,6 +59,6 @@ "toml-rollm-v2-rr1-mid.mp3" ], "midi-channel-name": "conductorToms", - "image": { "filename": "toms.svg", "yPos": "81%" } + "image": { "filename": "toms.svg", "yPos": "81%" , "width": "15vw", "height": "15vw"} } } diff --git a/index.html b/index.html index c3b93c1..5229146 100644 --- a/index.html +++ b/index.html @@ -34,6 +34,7 @@
+
diff --git a/src/instruments.js b/src/instruments.js index 36bbaf2..9afed55 100644 --- a/src/instruments.js +++ b/src/instruments.js @@ -59,6 +59,8 @@ export async function loadInstruments() { image: imageData, yPos: config.image.yPos, // Store base64-encoded image data midiChannelName: config["midi-channel-name"], + width: config.image.width, + height: config.image.width, }; } diff --git a/src/render.js b/src/render.js index 9b6244c..4c4be3d 100644 --- a/src/render.js +++ b/src/render.js @@ -5,11 +5,29 @@ export class Renderer { currStaveNumber; numStaves; sheetWindow; + eventCanvas; + ctx; + canvasWidth; + canvasHeight; + iconCache = []; // stores drawn icons + imageCache = new Map(); // for memoizing loaded images constructor() { this.numStaves = 6; this.currStaveNumber = 1; + this.timeIndicator = document.querySelector("#time-indicator"); + this.sheetWindow = document.getElementById("music-window"); + + // Setup canvas + this.eventCanvas = document.getElementById("event-canvas"); + this.ctx = this.eventCanvas.getContext("2d"); + + this.resizeCanvas(); + window.addEventListener("resize", () => { + this.resizeCanvas(); + this.redrawIcons(); + }); this.timeIndicator.addEventListener("animationiteration", () => { this.currStaveNumber++; @@ -18,8 +36,7 @@ export class Renderer { } }); - this.sheetWindow = document.getElementById("music-window"); - + // Render stave SVGs for (let i = 1; i <= this.numStaves; i++) { const staveWrapper = document.createElement("div"); staveWrapper.classList.add("stave-wrapper"); @@ -36,13 +53,19 @@ export class Renderer { } } - placeIcon = (instrument) => { + resizeCanvas() { + this.canvasWidth = this.sheetWindow.offsetWidth; + this.canvasHeight = this.sheetWindow.offsetHeight; + this.eventCanvas.width = this.canvasWidth; + this.eventCanvas.height = this.canvasHeight; + } + + async placeIcon(instrument) { if (this.currStaveNumber > this.numStaves) this.currStaveNumber = 1; const targetWrapper = document.getElementById( `stave-wrapper-${this.currStaveNumber}` ); - const rect = this.timeIndicator.getBoundingClientRect(); const sheetLeft = this.sheetWindow.getBoundingClientRect().left; const xPosition = rect.left + window.scrollX - sheetLeft; @@ -50,34 +73,71 @@ export class Renderer { const newObject = document.createElement("div"); newObject.classList.add("event-icon"); newObject.style.position = "absolute"; - newObject.style.left = `${ - xPosition - document.documentElement.clientWidth / 200 - }px`; - newObject.style.top = `${instrument.yPos}`; + newObject.style.left = `${xPosition}px`; + newObject.style.top = instrument.yPos; + newObject.style.width = instrument.width; + newObject.style.height = instrument.height; - const eventIcon = document.createElement("object"); - eventIcon.type = "image/svg+xml"; - eventIcon.data = instrument.image; + // ✅ Detect and decode base64 if needed + let svgString = instrument.image; + if (svgString.startsWith("data:image/svg+xml;base64,")) { + const base64 = svgString.split(",")[1]; + svgString = atob(base64); + } - newObject.appendChild(eventIcon); + const parser = new DOMParser(); + const svgDoc = parser.parseFromString(svgString, "image/svg+xml"); + const svgElement = svgDoc.documentElement; + + // ✅ Handle parsing error + if (svgElement.nodeName === "parsererror") { + console.error("Failed to parse SVG:", svgString); + return; + } + + svgElement.style.width = "100%"; + svgElement.style.height = "100%"; + + newObject.appendChild(svgElement); targetWrapper.appendChild(newObject); - }; + } + + async loadImageFromSVG(svgString) { + if (this.imageCache.has(svgString)) { + return this.imageCache.get(svgString); + } + + return new Promise((resolve) => { + const blob = new Blob([svgString], { type: "image/svg+xml" }); + const url = URL.createObjectURL(blob); + const img = new Image(); + + img.onload = () => { + URL.revokeObjectURL(url); // clean up after load + this.imageCache.set(svgString, img); + resolve(img); + }; + + img.src = url; + }); + } + + redrawIcons() { + this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); + this.iconCache.forEach(({ image, x, y }) => { + this.ctx.drawImage(image, x, y, 24, 24); // Adjust size as needed + }); + } cleanUpAndRestart(reload = false) { - const icons = document.querySelectorAll(".event-icon"); - this.timeIndicator = document.getElementById("time-indicator"); - - icons.forEach((icon) => { - icon.classList.add("fade-out"); - icon.addEventListener("transitionend", () => { - icon.remove(); - }); - }); - + this.iconCache = []; + this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); this.currStaveNumber = 1; Conductor.setTitle(); - if (reload) location.reload(); } } + +// This will need to be worked on. There is no need for the loadImageFromSVG function here and the placeIcon function will need to be changed to reflect it. +// image size and z-index isn't correct and the canvas will need to be enlarged to allow icons to overhang the staves. diff --git a/src/script.js b/src/script.js index 5264e24..311b54f 100644 --- a/src/script.js +++ b/src/script.js @@ -2,7 +2,7 @@ import { sendMidiMessage } from "./midi.js"; import { Conductor } from "./conductor.js"; import { DataStream } from "./socket.js"; import { playSound } from "./audio"; -import { Renderer, placeIcon } from "./render.js"; +import { Renderer } from "./render.js"; document.addEventListener("DOMContentLoaded", async () => { sendMidiMessage(); diff --git a/styles.css b/styles.css index f9feceb..61e7175 100644 --- a/styles.css +++ b/styles.css @@ -74,7 +74,10 @@ html { transition: opacity 1s ease; } - +#event-canvas { + background-color: rgba(255, 255, 255, 0.02); /* lightly visible canvas for debugging */ + } + .logo { color: #fff; width: 20%; @@ -195,10 +198,27 @@ html { width: 2px; /* height: 100%; */ animation: moveRight 10s linear infinite; - z-index: 2; + z-index: 3; } - +#event-canvas { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + z-index: 2; + } + + #time-indicator { + position: absolute; + top: 0; + bottom: 0; + width: 2px; + background-color: red; + z-index: 3; + will-change: transform; + } + .fade-in { opacity: 1; }