Search code examples
javascripthtmlcsscanvas

Add contents after the canvas


I am trying to build a webpage that display images related to the scroll. I am able to display content on top of the canvas and the animation worked fine.

What I am not able to do is add content AFTER the canvas. Once the canvas ends, and so does the window, I want to display <h2>. A scroll bar should appear when the page is loaded and I should be able to scroll a bit more so that I see the <h2>.

Any ideas?

const html = document.documentElement;
const canvas = document.querySelector('.ultragear-scrolling');
const context = canvas.getContext('2d');
const overlay = document.querySelector('.overlay');
const message = document.querySelector('.message');
const content = document.querySelector('.content');

const currentFrame = index => (
  `https://gi.esmplus.com/BLADER01/Hyuk/oled-moon/${index.toString().padStart(0, '1')}.jpg`
);

// frameCount= Number of image
const frameCount = 294;

// set canvas dimensions
canvas.height = 770;
canvas.width = 1158;

const images = [];

// Preload images
const preloadImages = () => {
  for (let i = 1; i <= frameCount; i++) {
    const img = new Image();
    img.src = currentFrame(i);
    images.push(img);
  }
};

preloadImages();

// Create, load, and draw the image
const img = new Image();
img.src = currentFrame(1);
img.onload = function() {
  drawCentered(img);
}

const updateImage = index => {
  // Clear the previous image
  context.clearRect(0, 0, canvas.width, canvas.height);

  // Draw the new image
  drawCentered(images[index]);

};

function drawCentered(img) {
  const offsetX = (canvas.width - img.width) / 2;
  const offsetY = (canvas.height - img.height) / 2;
  context.drawImage(img, offsetX, offsetY);
}

let animationFrameId;

window.addEventListener('scroll', () => {
  cancelAnimationFrame(animationFrameId);

  const scrollTop = html.scrollTop;
  const maxScrollTop = html.scrollHeight - window.innerHeight;
  const scrollFraction = scrollTop / maxScrollTop;
  const frameIndex = Math.min(
    frameCount - 1,
    Math.floor(scrollFraction * frameCount)
  );

  animationFrameId = requestAnimationFrame(() => updateImage(frameIndex));

  if (frameIndex === frameCount - 1) {
    canvas.style.transition = 'opacity 1s ease-in-out';
    canvas.style.opacity = '0';
    overlay.classList.add('visible');
    message.classList.add('visible');

    // Delay showing the content to ensure the message is fully visible
    setTimeout(() => {
      content.classList.add('visible');
    }, 1000); // Adjust delay as needed
  } else {
    canvas.style.opacity = '1';
    overlay.classList.remove('visible');
    message.classList.remove('visible');
    content.classList.remove('visible');
  }
});
html {
  height: 2000vh;
}

body {
  background: #000;
  height: 2000vh;
  margin: 0;
}

canvas {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  max-height: 100vh;
  max-width: 100vw;
}

.overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  opacity: 0;
  transition: opacity 1s ease-in-out;
  pointer-events: none;
  background: #000;
}

.message {
  color: #fff;
  font-size: 2rem;
  transform: scale(0);
  transition: transform 1s ease-in-out;
}

.overlay.visible {
  opacity: 1;
}

.message.visible {
  transform: scale(1);
}

content {
  position: absolute;
  background: #000;
  color: #fff;
  padding-top: 20px;
  text-align: center;
  margin-top: 100vh;
  opacity: 0;
  transition: opacity 1s ease-in-out;
}

.content.visible {
  opacity: 1;
}
<canvas width="300" height="200" class="ultragear-scrolling"></canvas>
<div class="overlay">
  <h1 class="message">Ultra Gear </h1>
</div>
<div class="content">
  <h2>Additional Content</h2>
  <img src="https://gi.esmplus.com/BLADER01/Hyuk/27gs95qe/27gs95qe_01.png" alt="Additional Image" />
</div>


Solution

  • I've introduced 2 functions to be used on the canvas: makeFixed and makeAbsolute which attempt to change position while maintaining visual position.

    Also introduced a .below-canvas element which accordingly is located below the canvas.

    Now the trick is when to make the switch between aboslute and fixed and another trick is how calculate the maxScrollTop etc. The result is not perfect but it's a start.

    const html = document.documentElement;
    const canvas = document.querySelector('.ultragear-scrolling');
    const context = canvas.getContext('2d');
    const overlay = document.querySelector('.overlay');
    const message = document.querySelector('.message');
    const content = document.querySelector('.content');
    const below = document.querySelector('.below-canvas');
    
    const currentFrame = index => (
      `https://gi.esmplus.com/BLADER01/Hyuk/oled-moon/${index.toString().padStart(0, '1')}.jpg`
    );
    
    // frameCount= Number of image
    const frameCount = 294;
    
    // set canvas dimensions
    canvas.height = 770;
    canvas.width = 1158;
    
    const images = [];
    
    // Preload images
    const preloadImages = () => {
      for (let i = 1; i <= frameCount; i++) {
        const img = new Image();
        img.src = currentFrame(i);
        images.push(img);
      }
    };
    
    preloadImages();
    
    // Create, load, and draw the image
    const img = new Image();
    img.src = currentFrame(1);
    img.onload = function() {
      drawCentered(img);
    }
    
    const updateImage = index => {
      // Clear the previous image
      context.clearRect(0, 0, canvas.width, canvas.height);
    
      // Draw the new image
      drawCentered(images[index]);
    
    };
    
    function drawCentered(img) {
      const offsetX = (canvas.width - img.width) / 2;
      const offsetY = (canvas.height - img.height) / 2;
      context.drawImage(img, offsetX, offsetY);
    }
    
    let animationFrameId;
    
    addEventListener('scroll', () => {
      cancelAnimationFrame(animationFrameId);
    
      const scrollTop = html.scrollTop;
      const maxScrollTop = html.scrollHeight - window.innerHeight - 50;
      const scrollFraction = scrollTop / maxScrollTop;
      const frameIndex = Math.min(
        frameCount - 1,
        Math.floor(scrollFraction * frameCount)
      );
    
      animationFrameId = requestAnimationFrame(() => updateImage(frameIndex));
    
      if (frameIndex >= frameCount - 1) {
        canvas.style.transition = 'opacity 1s ease-in-out';
        canvas.style.opacity = '0.5';
        overlay.classList.add('visible');
        message.classList.add('visible');
    
        // Delay showing the content to ensure the message is fully visible
        setTimeout(() => {
          content.classList.add('visible');
          makeAbsolute(canvas)
          makeAbsolute(overlay)
        }, 1000); // Adjust delay as needed
      } else {
        canvas.style.opacity = '0.5';
        overlay.classList.remove('visible');
        message.classList.remove('visible');
        content.classList.remove('visible');
    
        makeFixed(canvas)
        makeFixed(overlay)
      }
    });
    
    
    function makeAbsolute(fixedElement) {
      const rect = fixedElement.getBoundingClientRect();
      fixedElement.style.position = 'absolute';
      fixedElement.style.transform = 'translate(0,0)';
      const newTop = rect.top;
      const newLeft = rect.left;
      fixedElement.style.top = `${window.pageYOffset + rect.top}px`;
      fixedElement.style.left = `${window.pageXOffset + rect.left}px`;
      below.style.position = 'absolute';
      below.style.top = `${window.pageYOffset + rect.top + rect.height}px`;
    }
    
    
    function makeFixed(abosluteElement) {
      abosluteElement.style.position = 'fixed';
      abosluteElement.style.transform = 'translate(-50%,-50%)';
      abosluteElement.style.top = `50%`;
      abosluteElement.style.left = `50%`;
      below.style.position = 'fixed';
      below.style.top = `100%`;
    }
    html {
      height: 2000px;
      margin: 0;
      padding: 0;
    }
    
    body {
      background: #000;
      height: 2000px;
      margin: 0;
      padding: 0;
    }
    
    canvas {
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      max-height: 100vh;
      max-width: 100vw;
    }
    
    .overlay {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      opacity: 0;
      transition: opacity 1s ease-in-out;
      pointer-events: none;
      background: #000;
    }
    
    .message {
      color: #fff;
      font-size: 2rem;
      transform: scale(0);
      transition: transform 1s ease-in-out;
    }
    
    .overlay.visible {
      opacity: 1;
    }
    
    .message.visible {
      transform: scale(1);
    }
    
    content {
      position: absolute;
      background: #000;
      color: #fff;
      padding-top: 20px;
      text-align: center;
      margin-top: 100vh;
      opacity: 0;
      transition: opacity 1s ease-in-out;
    }
    
    .content.visible {
      opacity: 1;
    }
    
    .below-canvas {
      position: fixed;
      top: 100%;
    }
    <canvas width="300" height="200" class="ultragear-scrolling"></canvas>
    
    <div class="overlay">
      <h1 class="message">Ultra Gear </h1>
    </div>
    
    <div class="content">
      <h2>Additional Content</h2>
      <img src="https://gi.esmplus.com/BLADER01/Hyuk/27gs95qe/27gs95qe_01.png" alt="Additional Image" />
    </div>
    
    <div class="below-canvas">
      <h2>hello hellloo</h2>
    </div>