From 89936159534d5bb3e7b661c08c183651d3e9287e Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 23 Apr 2025 12:32:44 +0100 Subject: [PATCH] nearly sorted with the rendering --- assets/instruments/instruments.json | 60 +++++-- assets/instruments/orchestral-bass/bass.svg | 2 +- assets/instruments/snare/snare.svg | 2 +- assets/instruments/surdo-napa/surdo-napa.svg | 2 +- assets/instruments/surdo/surdo.svg | 2 +- .../timpani-large/timpani-large.svg | 2 +- .../timpani-small/timpani-small.svg | 2 +- assets/instruments/toms/toms.svg | 2 +- assets/svg/stave.svg | 2 +- index.html | 58 +------ notes.txt | 13 ++ planeNotes.txt | 11 ++ src/conductor.js | 44 ------ src/instruments.js | 74 ++++----- src/render.js | 146 +++++------------- src/script.js | 11 +- src/socket.js | 41 +++-- styles.css | 52 +++---- 18 files changed, 204 insertions(+), 322 deletions(-) create mode 100644 planeNotes.txt diff --git a/assets/instruments/instruments.json b/assets/instruments/instruments.json index 4dfbf0b..4ad71d1 100644 --- a/assets/instruments/instruments.json +++ b/assets/instruments/instruments.json @@ -8,7 +8,12 @@ "bdrumnew-hit-v7-rr1-sum-(1).mp3" ], "midi-channel-name": "conductorOrchestralBass", - "image": { "filename": "bass.svg", "yPos": "13%", "width": "100", "height": "100" } + "image": { + "filename": "bass.svg", + "yPos": "13%", + "width": "100", + "height": "100" + } }, "Snare": { "directory": "/assets/instruments/snare", @@ -17,20 +22,35 @@ "ropesnare-low-tsn-main-vl4-rr1.mp3" ], "midi-channel-name": "conductorSnare", - "image": { "filename": "snare.svg", "yPos": "26%", "width": "100", "height": "100" } + "image": { + "filename": "snare.svg", + "yPos": "26%", + "width": "100", + "height": "100" + } }, "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%" , "width": "100", "height": "100"} + "midi-channel-name": "conductorSnare", + "image": { + "filename": "surdo.svg", + "yPos": "39%", + "width": "100", + "height": "100" + } }, "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%" , "width": "100", "height": "100" -} }, + "image": { + "filename": "surdo-napa.svg", + "yPos": "42%", + "width": "100", + "height": "100" + } + }, "Timpani Large": { "directory": "/assets/instruments/timpani-large", "filenames": [ @@ -39,17 +59,28 @@ "timpani7b-hit-v5-rr2-main.mp3", "timpani7d-hit-v2-rr1-main.mp3" ], - "midi-channel-name":"conductorTimpaniLarge", - "image": { "filename": "timpani-large.svg", "yPos": "55%" , "width": "100", "height": "100" -} }, - "Timpani Small": { "directory": "/assets/instruments/timpani-small", + "midi-channel-name": "conductorTimpaniLarge", + "image": { + "filename": "timpani-large.svg", + "yPos": "55%", + "width": "100", + "height": "100" + } + }, + "Timpani Small": { + "directory": "/assets/instruments/timpani-small", "filenames": [ "timpani1a-hit-v5-rr1-main.mp3", "timpani2-hit-v3-rr1-sum.mp3", "timpani4-hit-v2-rr1-sum.mp3" ], "midi-channel-name": "conductorTimpaniSmall", - "image": { "filename": "timpani-small.svg", "yPos": "68%" , "width": "100", "height": "100"} + "image": { + "filename": "timpani-small.svg", + "yPos": "68%", + "width": "100", + "height": "100" + } }, "Toms": { "directory": "/assets/instruments/toms", @@ -59,6 +90,11 @@ "toml-rollm-v2-rr1-mid.mp3" ], "midi-channel-name": "conductorToms", - "image": { "filename": "toms.svg", "yPos": "81%" , "width": "100", "height": "100"} + "image": { + "filename": "toms.svg", + "yPos": "81%", + "width": "100", + "height": "100" + } } } diff --git a/assets/instruments/orchestral-bass/bass.svg b/assets/instruments/orchestral-bass/bass.svg index 30e63e3..334fb75 100644 --- a/assets/instruments/orchestral-bass/bass.svg +++ b/assets/instruments/orchestral-bass/bass.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/instruments/snare/snare.svg b/assets/instruments/snare/snare.svg index f8cba7e..651ad5c 100644 --- a/assets/instruments/snare/snare.svg +++ b/assets/instruments/snare/snare.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/instruments/surdo-napa/surdo-napa.svg b/assets/instruments/surdo-napa/surdo-napa.svg index d90e032..7c3f7fe 100644 --- a/assets/instruments/surdo-napa/surdo-napa.svg +++ b/assets/instruments/surdo-napa/surdo-napa.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/instruments/surdo/surdo.svg b/assets/instruments/surdo/surdo.svg index bccb271..a9707fa 100644 --- a/assets/instruments/surdo/surdo.svg +++ b/assets/instruments/surdo/surdo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/instruments/timpani-large/timpani-large.svg b/assets/instruments/timpani-large/timpani-large.svg index 17c3f6c..d26bcda 100644 --- a/assets/instruments/timpani-large/timpani-large.svg +++ b/assets/instruments/timpani-large/timpani-large.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/instruments/timpani-small/timpani-small.svg b/assets/instruments/timpani-small/timpani-small.svg index e12a085..d43b15c 100644 --- a/assets/instruments/timpani-small/timpani-small.svg +++ b/assets/instruments/timpani-small/timpani-small.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/instruments/toms/toms.svg b/assets/instruments/toms/toms.svg index 8bec501..5a6f04b 100644 --- a/assets/instruments/toms/toms.svg +++ b/assets/instruments/toms/toms.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/svg/stave.svg b/assets/svg/stave.svg index 277660e..a7f40fc 100644 --- a/assets/svg/stave.svg +++ b/assets/svg/stave.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/index.html b/index.html index 5229146..681e5bb 100644 --- a/index.html +++ b/index.html @@ -15,11 +15,6 @@
-
- - -
-
@@ -34,59 +29,8 @@
- -
-
-
- -
- -
-
-
-
-
-

- The Conductor translates live lightning strikes from around the world into a graphic score for seven percussion - instruments. - -

- - -

- The project was conceived and developed by the artist Mishka Henner between 2023 and 2024 with a team of - musicians, sound artists and acoustics engineers during an artist's residency at Energy House 2.0 at the - University of Salford. -

-

- The Conductor was first performed at the University of Salford’s Acoustic Laboratories during the Sounds From - the Other City Festival on 5 May 2024. -

-

- Click here - to read more about the project. Download a copy of the artist's graphic score here. -

-
-
-
-
- Project Conception and Design by Mishka Henner. -
- Data Capture, Web Design and Coding by Joe Gibson. -
- Lightning data from Blitzortung.org -
-

- With support from the University of Salford Art Collection in partnership with Open Eye Gallery, Liverpool as - part of the LOOK Photo Biennial, and Castlefield Gallery, Manchester. Generously supported by Friends of - Energy House Labs. -

-
- + id="time-indicator">
diff --git a/notes.txt b/notes.txt index 0c50a07..4d00686 100644 --- a/notes.txt +++ b/notes.txt @@ -16,3 +16,16 @@ sort out the blitzortung connection issue make the text max size smaller about page make title smaller + + + +lOOK AT SPACING BETWEEN THE TITLE AND STAVES AND THEN MAYBE ALSO THE STAVE SPACING + +TRY BOTH THE SVG RENDER AND THE RASTER RENDERING APPROACH... + +SCROLL BAR MUST GO (VERTICAL) + +Remove all passwords and google accounts from laptop. + +Ensure it boots into landscape mode + diff --git a/planeNotes.txt b/planeNotes.txt new file mode 100644 index 0000000..a4ea469 --- /dev/null +++ b/planeNotes.txt @@ -0,0 +1,11 @@ +placeIcon method: + Now img is an element - does it have a img.naturalHeight/Width property? + Check because we are using these values to draw the img. + + What is the sheetWindow.offsetHeight? - This is probably okay but take a look. + +Moving forward: + We need to measure the stavewrapper and place the icons using an offset factor based on the stave number. + + + diff --git a/src/conductor.js b/src/conductor.js index 901a6a5..1bf9843 100644 --- a/src/conductor.js +++ b/src/conductor.js @@ -35,37 +35,7 @@ export class Conductor { showMainContent() { Conductor.setTitle(); - const mute = document.getElementById("mute"); - - mute.addEventListener("click", () => { - const mutedSrc = "/assets/svg/sound-on.svg"; - const unMutedSrc = "/assets/svg/mute.svg"; - interacted = !interacted; - interacted ? unMute() : muter(); - mute.src = interacted ? mutedSrc : unMutedSrc; - - startAudio(); - fadeInVolume(); - }); - this.fadeAndReplaceADiv("main-content", "loading-screen"); - this.fadeAndReplaceADiv("buttons", "buttons"); - this.showingWhichContent = "main-content"; - - document.getElementById("menu-button").onclick = function () { - toggleInfo(); - }; - } - - toggleInfo() { - if (this.showingWhichContent === "main-content") { - this.fadeAndReplaceADiv("about-content", "main-content"); - cleanUpAndRestart(); - this.showingWhichContent = "about-content"; - } else { - this.fadeAndReplaceADiv("main-content", "about-content"); - this.showingWhichContent = "main-content"; - } } fadeAndReplaceADiv(innyDivId, outtyDivId) { @@ -87,20 +57,6 @@ export class Conductor { } async init() { - const instrumentsKey = document.getElementById("instrument-key-div"); this.instruments = await loadInstruments(); - const instNames = Object.keys(this.instruments); - for (const instrument of instNames) { - const keyDiv = document.createElement("div"); - const keySvg = document.createElement("object"); - const keyName = document.createElement("div"); - keyName.innerText = instrument.toUpperCase(); - keySvg.type = "image/svg+xml"; - keySvg.data = this.instruments[instrument].image; - keyDiv.appendChild(keySvg); - keyDiv.appendChild(keyName); - keyDiv.classList.add("instrument-key"); - instrumentsKey.appendChild(keyDiv); - } } } diff --git a/src/instruments.js b/src/instruments.js index 9afed55..20ebfd8 100644 --- a/src/instruments.js +++ b/src/instruments.js @@ -3,64 +3,64 @@ async function loadInstrumentsConfig() { return await response.json(); } -async function fetchAudioData(url) { +async function fetchSvgText(url) { const response = await fetch(url); - const arrayBuffer = await response.arrayBuffer(); - return arrayBuffer; + return await response.text(); } -async function decodeAudioData(context, arrayBuffer) { +function svgTextToImage(svgText, defaultWidth = 100, defaultHeight = 100) { return new Promise((resolve, reject) => { - context.decodeAudioData(arrayBuffer, resolve, reject); - }); -} + // Create a blob from the SVG text + const blob = new Blob([svgText], { type: "image/svg+xml" }); + const url = URL.createObjectURL(blob); + const img = new Image(); -async function loadAudioSamples(context, directory, filenames) { - const audioFiles = filenames; + // Set the onload callback + img.onload = () => { + // Check if naturalWidth and naturalHeight are invalid + if (img.naturalWidth === 0 || img.naturalHeight === 0) { + // Set default dimensions if image is invalid (missing width/height in SVG) + img.width = defaultWidth; + img.height = defaultHeight; + } - const audioBuffers = {}; - for (const file of audioFiles) { - const arrayBuffer = await fetchAudioData(`${directory}/${file}`); - const audioBuffer = await decodeAudioData(context, arrayBuffer); - const fileName = file.split("/").pop(); // Extract filename without path for reference - audioBuffers[fileName] = audioBuffer; - } + // Clean up the object URL to free memory + URL.revokeObjectURL(url); - return audioBuffers; -} + resolve(img); + }; -async function fetchImageData(url) { - const response = await fetch(url); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); - reader.onerror = reject; - reader.readAsDataURL(blob); // Convert to base64 + // Handle image loading errors + img.onerror = (err) => { + URL.revokeObjectURL(url); // Clean up URL + reject(err); + }; + img.src = url; }); } export async function loadInstruments() { const instrumentsConfig = await loadInstrumentsConfig(); - const context = new (window.AudioContext || window.webkitAudioContext)(); const instruments = {}; for (const [instrument, config] of Object.entries(instrumentsConfig)) { - const samples = await loadAudioSamples( - context, - config.directory, - config.filenames - ); - const imageData = await fetchImageData( + const svgText = await fetchSvgText( `${config.directory}/${config.image.filename}` ); + + // Pass the width and height to the svgTextToImage function + const img = await svgTextToImage( + svgText, + config.image.width || 100, + config.image.height || 100 + ); + instruments[instrument] = { - samples: samples, - image: imageData, - yPos: config.image.yPos, // Store base64-encoded image data + image: img, + yPos: config.image.yPos, midiChannelName: config["midi-channel-name"], width: config.image.width, - height: config.image.width, + height: config.image.height, }; } diff --git a/src/render.js b/src/render.js index 06403ab..57bc39a 100644 --- a/src/render.js +++ b/src/render.js @@ -5,27 +5,20 @@ export class Renderer { currStaveNumber; numStaves; sheetWindow; - eventCanvas; - ctx; + canvases = []; canvasWidth; canvasHeight; - iconCache = []; // stores { image, x, y, width, height } - imageCache = new Map(); // memoized + iconCache = []; + iconScale = 0.25; constructor() { this.numStaves = 6; this.currStaveNumber = 1; - this.timeIndicator = document.querySelector("#time-indicator"); + this.timeIndicator = document.getElementById("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(); }); @@ -36,9 +29,15 @@ export class Renderer { } }); - // Render stave SVGs for (let i = 1; i <= this.numStaves; i++) { const staveWrapper = document.createElement("div"); + const canvas = document.createElement("canvas"); + canvas.id = `canvas-${i}`; + canvas.className = "event-canvas"; + + const ctx = canvas.getContext("2d"); + this.canvases.push({ canvas, ctx }); + staveWrapper.classList.add("stave-wrapper"); staveWrapper.setAttribute("id", `stave-wrapper-${i}`); staveWrapper.style.position = "relative"; @@ -48,17 +47,17 @@ export class Renderer { staveObject.data = "assets/svg/stave.svg"; staveObject.classList.add("stave-svg"); + staveWrapper.appendChild(canvas); staveWrapper.appendChild(staveObject); this.sheetWindow.appendChild(staveWrapper); - } - } - resizeCanvas() { - this.canvasWidth = this.sheetWindow.offsetWidth; - this.canvasHeight = this.sheetWindow.offsetHeight; - this.eventCanvas.width = this.canvasWidth; - this.eventCanvas.height = this.canvasHeight; - console.log("Canvas size:", this.canvasWidth, this.canvasHeight); + // Set canvas size after layout is applied + requestAnimationFrame(() => { + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; + }); + } } async placeIcon(instrument) { @@ -68,112 +67,47 @@ export class Renderer { const sheetLeft = this.sheetWindow.getBoundingClientRect().left; const xPosition = rect.left + window.scrollX - sheetLeft; - const yPercent = parseFloat(instrument.yPos); // e.g., "55%" => 55 - const y = (yPercent / 100) * this.canvasHeight; + const canvasEntry = this.canvases[this.currStaveNumber - 1]; + const canvas = canvasEntry.canvas; + const ctx = canvasEntry.ctx; - // Handle image caching - let svgString = instrument.image; - if (svgString.startsWith("data:image/svg+xml;base64,")) { - const base64 = svgString.split(",")[1]; - svgString = atob(base64); - } + const yPercent = parseFloat(instrument.yPos); + const y = (yPercent / 100) * canvas.height; - // Ensure SVG has dimensions - svgString = this.ensureSvgHasDimensions( - svgString, - instrument.width || 50, - instrument.height || 50 - ); + const width = instrument.width * this.iconScale; + const height = instrument.height * this.iconScale; - const img = await this.loadImageFromSVG(svgString); + ctx.drawImage(instrument.image, xPosition, y, width, height); - // Fallback if instrument.width/height not provided - const naturalWidth = img.naturalWidth || 50; - const naturalHeight = img.naturalHeight || 50; - const scale = 0.25; - const width = parseFloat(instrument.width || naturalWidth) * scale; - const height = parseFloat(instrument.height || naturalHeight) * scale; - - this.ctx.drawImage(img, xPosition, y, width, height); - - // Store for redrawing later this.iconCache.push({ - image: img, + image: instrument.image, x: xPosition, y, width, height, - }); - - console.log("Drawing position:", xPosition, y); - console.log("width", width, "height", height); - console.log( - "Canvas size:", - this.eventCanvas.width, - this.eventCanvas.height - ); - console.log( - "Canvas offset size:", - this.eventCanvas.offsetWidth, - this.eventCanvas.offsetHeight - ); - } - ensureSvgHasDimensions(svgString, defaultWidth = 50, defaultHeight = 50) { - const hasWidth = /]*\bwidth=/.test(svgString); - const hasHeight = /]*\bheight=/.test(svgString); - - if (!hasWidth || !hasHeight) { - svgString = svgString.replace( - /]*)>/, - `` - ); - } - - return svgString; - } - - async loadImageFromSVG(svgString) { - if (this.imageCache.has(svgString)) { - return this.imageCache.get(svgString); - } - - return new Promise((resolve, reject) => { - const blob = new Blob([svgString], { type: "image/svg+xml" }); - const url = URL.createObjectURL(blob); - const img = new Image(); - - img.onload = () => { - console.log("Image loaded:", img.width, img.height); // should no longer be 0 - - const scale = 0.25; - const width = instrument.width * scale || img.width * scale; - const height = instrument.height * scale || img.height * scale; - - ctx.drawImage(img, xPosition, y, width, height); - URL.revokeObjectURL(url); - }; - - img.onerror = (err) => { - console.error("Image failed to load from SVG:", err); - reject(err); - }; - - img.src = url; + stave: this.currStaveNumber, }); } redrawIcons() { - this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); - this.iconCache.forEach(({ image, x, y, width, height }) => { - this.ctx.drawImage(image, x, y, width, height); + this.canvases.forEach(({ ctx, canvas }) => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + }); + + this.iconCache.forEach(({ image, x, y, width, height, stave }) => { + const ctx = this.canvases[stave - 1].ctx; + ctx.drawImage(image, x, y, width, height); }); } cleanUpAndRestart(reload = false) { this.iconCache = []; - this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); this.currStaveNumber = 1; + this.canvases.forEach(({ ctx, canvas }) => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + }); + Conductor.setTitle(); if (reload) location.reload(); } diff --git a/src/script.js b/src/script.js index 311b54f..d5860ba 100644 --- a/src/script.js +++ b/src/script.js @@ -1,7 +1,6 @@ import { sendMidiMessage } from "./midi.js"; import { Conductor } from "./conductor.js"; import { DataStream } from "./socket.js"; -import { playSound } from "./audio"; import { Renderer } from "./render.js"; document.addEventListener("DOMContentLoaded", async () => { @@ -21,17 +20,11 @@ document.addEventListener("DOMContentLoaded", async () => { const selectedInstrument = instruments[keys[randomIndex]]; renderer.placeIcon(selectedInstrument); - playSound(selectedInstrument); + + // **** add midi trigger +++++++++ /// }); dataStream.init(); - // let currStaveNumber = 1; - conductor.showMainContent(); - - // let strikeNumber = 0; - - // setupWebSocketListeners(); // Setup the WebSocket listeners - // resetTimeout(); // Start the timeout timer }); diff --git a/src/socket.js b/src/socket.js index 00e8183..85abb53 100644 --- a/src/socket.js +++ b/src/socket.js @@ -7,6 +7,8 @@ export class DataStream extends EventTarget { this.client = null; this.lastReceived = null; this.connectionTimeout = null; + this.test = false; + this.testInterval = null; } async init() { @@ -38,25 +40,32 @@ export class DataStream extends EventTarget { } setupWebSocketListeners() { - this.client.removeAllListeners?.("data"); - this.client.removeAllListeners?.("error"); - this.client.removeAllListeners?.("close"); + if (this.test === false) { + this.client.removeAllListeners?.("data"); + this.client.removeAllListeners?.("error"); + this.client.removeAllListeners?.("close"); - this.client.on("data", (data) => { - this.lastReceived = Date.now(); - this.resetTimeout(); + this.client.on("data", (data) => { + this.lastReceived = Date.now(); + this.resetTimeout(); - this.dispatchEvent(new CustomEvent("strike", { detail: data })); - }); + this.dispatchEvent(new CustomEvent("strike", { detail: data })); + }); - this.client.on("error", (err) => { - console.error("WebSocket error:", err); - this.reconnectWebSocket(); - }); + this.client.on("error", (err) => { + console.error("WebSocket error:", err); + this.reconnectWebSocket(); + }); - this.client.on("close", () => { - console.log("WebSocket closed, attempting to reconnect..."); - this.reconnectWebSocket(); - }); + this.client.on("close", () => { + console.log("WebSocket closed, attempting to reconnect..."); + this.reconnectWebSocket(); + }); + } else { + clearInterval(this.testInterval); + this.testInterval = setInterval(() => { + this.dispatchEvent(new CustomEvent("strike", { detail: "fakeData" })); + }, 1000); + } } } diff --git a/styles.css b/styles.css index d7ea433..2fb0be3 100644 --- a/styles.css +++ b/styles.css @@ -1,4 +1,3 @@ - body { display: flex; flex-direction: column; @@ -8,6 +7,7 @@ body { overflow: hidden; } + object { margin-bottom: 10px; } @@ -74,14 +74,16 @@ html { transition: opacity 1s ease; } -#event-canvas { +.event-canvas { background: rgba(255, 0, 0, 0.2); /* light red overlay */ position: absolute; top: 0; left: 0; - z-index: 1000; - } - + width: 100%; + height:100%; + pointer-events: none; + z-index: 9999; +} .logo { color: #fff; @@ -96,10 +98,6 @@ html { margin: 20px; } -.event-icon { - width: 1vw; - min-width: 10px; -} .instruments-key { position: relative; @@ -174,6 +172,7 @@ html { top: 30px; width: 50%; } + .title-title { font-family: Helvetica; color: #fff; @@ -191,42 +190,28 @@ html { .stave-wrapper { position: relative; background-color: rgb(24,24,24); - border: 1px solid rgb(24,24,24); + border: 1px solid rgb(99,99,99); padding-top: 30px; padding-bottom: 20px; - z-index: 1; + z-index: 100; } -.time-indicator { - top: 0; - left: 0; - position: absolute; - width: 2px; - /* height: 100%; */ - animation: moveRight 10s linear infinite; - z-index: 999; -} .stave-svg { position: relative; - z-index: 1; /* Same here */ - } -#event-canvas { - position: absolute; - top: 0; - left: 0; - pointer-events: none; - z-index: 999; - } + z-index: 1; +} + - #time-indicator { +#time-indicator { position: absolute; top: 0; bottom: 0; width: 2px; background-color: red; - z-index: 3; + z-index: 999; + animation: moveRight 10s linear infinite; will-change: transform; - } +} .fade-in { opacity: 1; @@ -244,7 +229,8 @@ html { to { transform: rotate(360deg); } - } +} + @keyframes moveRight { 0% { left: 0;