javascripthtmlcss

I can't find why nth iterations of an image are not loading


Hi I am trying to create a timeline for my fantasy world, and it works for the most part. You can see it in action here. https://www.skazkaworld.com/timeline.html

As you can see, the background rune image from the 8th entry and beyond does not display correctly. I cannot for the life of me figure out what the issue is. I'm still learning so my code is not very elegant.

Relevant HTML:

<body>
      <div id="navbar"></div><!-- Navbar will be loaded here -->
      <div class="timeline" id="timeline"></div>
    <script src="main.js"></script>
    <script src="timeline.js"></script>
    <script>
        // Load navbar.html dynamically        
        fetch("navbar.html")
            .then(response => response.text())
            .then(data => {
                document.getElementById("navbar").innerHTML = data;

                // Now that navbar is loaded, attach hamburger event
                const hamburger = document.querySelector(".hamburger");
                const navLinks = document.querySelector(".nav-links");

                if (hamburger && navLinks) {
                    hamburger.addEventListener("click", () => {
                        navLinks.classList.toggle("active");
                        hamburger.classList.toggle("open");
                    });
                }
            });
    </script> 
  </body>

CSS:

/* Timeline */ .timeline { 
  display: block; 
  margin: 100px auto 20px; 
  width: 800px; 
  padding: 14px; 
  background: rgba(20, 20, 20, 0.85); 
  height: 80vh; 
  overflow: auto;
  background-image: url("assets/timeline.jpg"); /* replace with your image */ 
  background-repeat: repeat-y; /* repeats vertically */ 
  background-position: center top; /* centers it horizontally */ 
  background-size:cover; /* adjust if needed, e.g., "contain" or specific width */ 
}

.event { 
  margin: 18px 0; 
  cursor: pointer; 

} .event-header { 
  display: flex; 
  align-items: center; 
}

.rune { 
  flex-shrink: 0; 
  width: 28px; 
  height: 28px; 
  margin-right: 8px; 
  background: url("assets/rune-marker.png") center/contain no-repeat; 
  filter: grayscale(100%) brightness(0.9); 
  transition: filter 0.3s ease, transform 0.35s ease; 
}

.rune.active { filter: grayscale(0%) brightness(1.1); 
  transform: scale(1.12); }

.event-details { 
  max-height: 0; 
  overflow: hidden; 
  opacity: 0; 
  transition: max-height 0.4s ease, opacity 0.4s ease; 
  padding-left: 36px; 
  font-size: 0.9em; 
  color: #ccc; 
}

.event.open .event-details { 
  max-height: 200px; 
  opacity: 1; 
  margin-top: 4px; 
}

Finally the JS:

// timeline.js
document.addEventListener("DOMContentLoaded", () => {
  const timeline = document.getElementById("timeline");

  // Timeline data array
  const timelineEvents = [
    {
      year: "1200 AE",
      title: "Founding of the First Ascent",
      details: "The First Ascent arises atop the frozen peaks of Skazka, where mortals defied the giants."
    },
    {
      year: "1345 AE",
      title: "The Rift of Molach",
      details: "A tear in the weave of reality as Molach's fall tore open the land and spirits slipped through."
    },
    {
      year: "1502 AE",
      title: "The Night of Whispering",
      details: "When every shadow spoke and the forests sang curses in voices not their own."
    },
    {
      year: "1502 AE",
      title: "The Night of Whispering",
      details: "When every shadow spoke and the forests sang curses in voices not their own."
    },
    {
      year: "1502 AE",
      title: "The Night of Whispering",
      details: "When every shadow spoke and the forests sang curses in voices not their own."
    },
    {
      year: "1502 AE",
      title: "The Night of Whispering",
      details: "When every shadow spoke and the forests sang curses in voices not their own."
    },
    {
      year: "1502 AE",
      title: "The Night of Whispering",
      details: "When every shadow spoke and the forests sang curses in voices not their own."
    },
    {
      year: "1502 AE",
      title: "The Night of Whispering",
      details: "When every shadow spoke and the forests sang curses in voices not their own."
    },
    {
      year: "1502 AE",
      title: "The Night of Whispering",
      details: "When every shadow spoke and the forests sang curses in voices not their own."
    },
    {
      year: "1502 AE",
      title: "The Night of Whispering",
      details: "When every shadow spoke and the forests sang curses in voices not their own."
    },
    // Add more events here...
  ];

  // Generate events dynamically
  timelineEvents.forEach((ev, i) => {
    const eventDiv = document.createElement("div");
    eventDiv.className = "event";
    eventDiv.dataset.id = i;

    eventDiv.innerHTML = `
      <div class="event-header">
        <span class="rune"></span>
        <div>
          <strong>${ev.year}</strong><br>
          ${ev.title}
        </div>
      </div>
      <div class="event-details">
        ${ev.details}
      </div>
    `;

    timeline.appendChild(eventDiv);
  });

  // Scroll rune activation - FIXED to use timeline container scroll
  const events = document.querySelectorAll(".event");
  
  function onTimelineScroll() {
    const timelineRect = timeline.getBoundingClientRect();
    const scrollTop = timeline.scrollTop;
    const viewportMiddle = timelineRect.height / 2;
    
    events.forEach(evt => {
      const eventRect = evt.getBoundingClientRect();
      const timelineTop = timelineRect.top;
      const eventTop = eventRect.top - timelineTop + scrollTop;
      const rune = evt.querySelector(".rune");
      
      // Activate rune if event is in upper half of timeline viewport
      if (eventTop < scrollTop + viewportMiddle) {
        rune.classList.add("active");
      } else {
        rune.classList.remove("active");
      }
    });
  }

  // Toggle event details on click
  events.forEach(evt => {
    evt.addEventListener("click", () => {
      evt.classList.toggle("open");
    });
  });

  // Listen to timeline scroll instead of window scroll
  timeline.addEventListener("scroll", onTimelineScroll);
  onTimelineScroll(); // Initial call
});

Solution

  • If what you mean by display incorrectly is that the markers are gray, that's what your code is doing by design:

    // Activate rune if event is in upper half of timeline viewport
    if (eventTop < scrollTop + viewportMiddle) {
        rune.classList.add("active");
    } else {
        rune.classList.remove("active");
    }
    

    You adding active for the elements in the upper half but removing it for the lower half. According to your stylesheet only with active will the marker be blue