Draw scene inside an existing div

I’m trying to display a three.js scene inside a div in my html, but i can’t figure out how. I’ve followed multiple stackoverflow posts but none of the answers i’ve found works… I’m starting THREE.JS, so it might be a stupid mistake but i’ve spent hours trying to fix it without success.

I think the error lies into my class GL{} constructor in the init() phase…

Here is my current code :


const store = {
  ww: window.innerWidth,
  wh: window.innerHeight,
  isDevice:
    navigator.userAgent.match(/Android/i) ||
    navigator.userAgent.match(/webOS/i) ||
    navigator.userAgent.match(/iPhone/i) ||
    navigator.userAgent.match(/iPad/i) ||
    navigator.userAgent.match(/iPod/i) ||
    navigator.userAgent.match(/BlackBerry/i) ||
    navigator.userAgent.match(/Windows Phone/i)
};

class Slider {
  constructor(el, opts = {}) {
    this.bindAll();

    this.el = el;

    this.opts = Object.assign(
      {
        speed: 2,
        threshold: 50,
        ease: 0.075
      },
      opts
    );

    this.ui = {
      items: this.el.querySelectorAll('.p-js-slide'),
      titles: document.querySelectorAll('.p-js-title'),
      lines: document.querySelectorAll('.p-js-progress-line')
    };

    this.state = {
      target: 0,
      current: 0,
      currentRounded: 0,
      y: 0,
      on: {
        x: 0,
        y: 0
      },
      off: 0,
      progress: 0,
      diff: 0,
      max: 0,
      min: 0,
      snap: {
        points: []
      },
      flags: {
        dragging: false
      }
    };

    this.items = [];

    this.events = {
      move: store.isDevice ? 'touchmove' : 'mousemove',
      up: store.isDevice ? 'touchend' : 'mouseup',
      down: store.isDevice ? 'touchstart' : 'mousedown'
    };

    this.init();
  }

  bindAll() {
    ['onDown', 'onMove', 'onUp'].forEach(fn => (this[fn] = this[fn].bind(this)));
  }

  init() {
    return gsap.utils.pipe(this.setup(), this.on());
  }

  destroy() {
    this.off();
    this.state = null;
    this.items = null;
    this.opts = null;
    this.ui = null;
  }

  on() {
    const { move, up, down } = this.events;

    window.addEventListener(down, this.onDown);
    window.addEventListener(move, this.onMove);
    window.addEventListener(up, this.onUp);
  }

  off() {
    const { move, up, down } = this.events;

    window.removeEventListener(down, this.onDown);
    window.removeEventListener(move, this.onMove);
    window.removeEventListener(up, this.onUp);
  }

  setup() {
    const { ww } = store;
    const state = this.state;
    const { items, titles } = this.ui;

    const { width: wrapWidth, left: wrapDiff } = this.el.getBoundingClientRect();

    // Set bounding
    state.max = -(items[items.length - 1].getBoundingClientRect().right - wrapWidth - wrapDiff);
    state.min = 0;

    // Global timeline
    this.tl = gsap
      .timeline({
        paused: true,
        defaults: {
          duration: 1,
          ease: 'linear'
        }
      })
      .fromTo(
        '.p-js-progress-line-2',
        {
          scaleX: 1
        },
        {
          scaleX: 0,
          duration: 0.5,
          ease: 'power3'
        },
        0
      )
      .fromTo(
        '.p-js-titles',
        {
          yPercent: 0
        },
        {
          yPercent: -(100 - 100 / titles.length)
        },
        0
      )
      .fromTo(
        '.p-js-progress-line',
        {
          scaleX: 0
        },
        {
          scaleX: 1
        },
        0
      );

    // Cache stuff
    for (let i = 0; i < items.length; i++) {
      const el = items[i];
      const { left, right, width } = el.getBoundingClientRect();

      // Create webgl plane
      const plane = new Plane();
      plane.init(el);

      // Timeline that plays when visible
      const tl = gsap.timeline({ paused: true }).fromTo(
        plane.mat.uniforms.uScale,
        {
          value: 0.65
        },
        {
          value: 1,
          duration: 1,
          ease: 'linear'
        }
      );

      // Push to cache
      this.items.push({
        el,
        plane,
        left,
        right,
        width,
        min: left < ww ? ww * 0.775 : -(ww * 0.225 - wrapWidth * 0.2),
        max: left > ww ? state.max - ww * 0.775 : state.max + (ww * 0.225 - wrapWidth * 0.2),
        tl,
        out: false
      });
    }
  }

  calc() {
    const state = this.state;
    state.current += (state.target - state.current) * this.opts.ease;
    state.currentRounded = Math.round(state.current * 100) / 100;
    state.diff = (state.target - state.current) * 0.0005;
    state.progress = gsap.utils.wrap(0, 1, state.currentRounded / state.max);

    this.tl && this.tl.progress(state.progress);
  }

  render() {
    this.calc();
    this.transformItems();
  }

  transformItems() {
    const { flags } = this.state;

    for (let i = 0; i < this.items.length; i++) {
      const item = this.items[i];
      const { translate, isVisible, progress } = this.isVisible(item);

      item.plane.updateX(translate);
      item.plane.mat.uniforms.uVelo.value = this.state.diff;

      if (!item.out && item.tl) {
        item.tl.progress(progress);
      }

      if (isVisible || flags.resize) {
        item.out = false;
      } else if (!item.out) {
        item.out = true;
      }
    }
  }

  isVisible({ left, right, width, min, max }) {
    const { ww } = store;
    const { currentRounded } = this.state;
    const translate = gsap.utils.wrap(min, max, currentRounded);
    // console.log(translate);
    const threshold = this.opts.threshold;
    const start = left + translate;
    const end = right + translate;
    const isVisible = start < threshold + ww && end > -threshold;
    const progress = gsap.utils.clamp(0, 1, 1 - (translate + left + width) / (ww + width));

    return {
      translate,
      isVisible,
      progress
    };
  }

  clampTarget() {
    const state = this.state;

    state.target = gsap.utils.clamp(state.max, 0, state.target);
  }

  getPos({ changedTouches, clientX, clientY, target }) {
    const x = changedTouches ? changedTouches[0].clientX : clientX;
    const y = changedTouches ? changedTouches[0].clientY : clientY;

    return {
      x,
      y,
      target
    };
  }

  onDown(e) {
    const { x, y } = this.getPos(e);
    const { flags, on } = this.state;

    flags.dragging = true;
    on.x = x;
    on.y = y;
  }

  onUp() {
    const state = this.state;

    state.flags.dragging = false;
    state.off = state.target;
  }

  onMove(e) {
    const { x, y } = this.getPos(e);
    const state = this.state;

    if (!state.flags.dragging) return;

    const { off, on } = state;
    const moveX = x - on.x;
    const moveY = y - on.y;

    if (Math.abs(moveX) > Math.abs(moveY) && e.cancelable) {
      e.preventDefault();
      e.stopPropagation();
    }

    state.target = off + moveX * this.opts.speed;
  }
}

/** */
/** * GL STUFF *** */
/** */

const backgroundCoverUv = `
(...)
`;

const vertexShader = `
(...)
`;

const fragmentShader = `
(...)
`;

const loader = new THREE.TextureLoader();
loader.crossOrigin = 'anonymous';

class Gl {
  constructor() {
    this.scene = new THREE.Scene();

    this.camera = new THREE.OrthographicCamera(
      store.ww / -2,
      store.ww / 2,
      store.wh / 2,
      store.wh / -2,
      1,
      10
    );
    this.camera.lookAt(this.scene.position);
    this.camera.position.z = 1;

    this.renderer = new THREE.WebGLRenderer({
      alpha: true,
      antialias: true
    });
    this.renderer.setPixelRatio(1.5);
    this.renderer.setSize(store.ww, store.wh);
    this.renderer.setClearColor(0xffffff, 0);

    this.init();
  }

  render() {
    this.renderer.render(this.scene, this.camera);
  }

  init() {
    const container = document.getElementById('canvas-projects');

    const domEl = this.renderer.domElement;
    domEl.classList.add('dom-gl');
    // document.body.appendChild(domEl);
    container.appendChild(domEl);
  }
}

class GlObject extends THREE.Object3D {
  init(el) {
    this.el = el;

    this.resize();
  }

  resize() {
    this.rect = this.el.getBoundingClientRect();
    const { left, top, width, height } = this.rect;

    this.pos = {
      x: left + width / 2 - store.ww / 2,
      y: top + height / 2 - store.wh / 2
    };

    this.position.y = this.pos.y;
    this.position.x = this.pos.x;

    this.updateX();
  }

  updateX(current) {
    current && (this.position.x = current + this.pos.x);
  }
}

const planeGeo = new THREE.PlaneBufferGeometry(1, 1, 32, 32);
const planeMat = new THREE.ShaderMaterial({
  transparent: true,
  fragmentShader,
  vertexShader
});

class Plane extends GlObject {
  init(el) {
    super.init(el);

    this.geo = planeGeo;
    this.mat = planeMat.clone();

    this.mat.uniforms = {
      uTime: { value: 0 },
      uTexture: { value: 0 },
      uMeshSize: { value: new THREE.Vector2(this.rect.width, this.rect.height) },
      uImageSize: { value: new THREE.Vector2(0, 0) },
      uScale: { value: 0.75 },
      uVelo: { value: 0 }
    };

    this.img = this.el.querySelector('img');
    this.texture = loader.load(this.img.src, texture => {
      texture.minFilter = THREE.LinearFilter;
      texture.generateMipmaps = false;

      this.mat.uniforms.uTexture.value = texture;
      this.mat.uniforms.uImageSize.value = [this.img.naturalWidth, this.img.naturalHeight];
    });

    this.mesh = new THREE.Mesh(this.geo, this.mat);
    this.mesh.scale.set(this.rect.width, this.rect.height, 1);
    this.add(this.mesh);
    gl.scene.add(this);
  }
}

/** */
/** * INIT STUFF *** */
/** */

const gl = new Gl();
const slider = new Slider(document.querySelector('.p-js-slider'));

const tick = () => {
  gl.render();
  slider.render();
};

gsap.ticker.add(tick);


So i want my animation to appear in my <div id="canvas-projects"></div> div, but my images don’t appear. If i put my div as my first html element, it works. But it’s supposed to be in the bottom of my page. So my guess i that my positioning isn’t good. But i can’t figure out how to make it work!

Here is my html


(... bunch of other html sections)

<section class="section section-larger section-more-projects" data-scroll-section>

            <div id="canvas-projects"></div>

            <div class="p-slider | p-js-drag-area">
              (...)
            </div>

            <div class="p-titles">
              (...)
            </div>

            <div class="p-progress">
              (...)
            </div>

</section>

(... bunch of other html sections)

And here is my css


$easeOutExpo: cubic-bezier(0.190, 1.000, 0.220, 1.000);

.dom-gl {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    //pointer-events: none;
    z-index: 1;
}

.p-slider {
    position: relative;
    padding: 0 22.5vw;
    display: flex;
    align-items: center;
    height: 100%;
    user-select: none;
    cursor: grab;
    z-index: 2;

    &__inner {
        display: flex;
        position: relative;
    }
}

.p-slide {
    overflow: hidden;

    &:first-child {
        position: relative;
    }

    &:not(:first-child) {
        position: absolute;
        top: 0;
        height: 100%;
    }

    &__inner {
        position: relative;
        overflow: hidden;
        width: 55vw;
        padding-top: 56.5%;
    }

    img {
        display: none;

        /*
    height: 100%;
    width: 140%;
    position: absolute;
    top: 0;
    left: -20%;
    object-fit: cover;
    will-change: transform;
    */
    }
}


.p-titles {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    overflow: hidden;
    pointer-events: auto;
    z-index: 9999999999;

    &__list {
        position: absolute;
        top: 0;
        left: 0;
    }

    &__title {
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 6vw;
        font-weight: bold;
        letter-spacing: -0.1vw;
        color: #fff;

        &--proxy {
            visibility: hidden;
        }
    }
}

.p-progress {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 0.25rem;
    overflow: hidden;
    pointer-events: none;

    &__line {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        transform: scaleX(0);
        transform-origin: left;
        background-color: #fff;

        &:nth-child(2) {
            transform-origin: right;
        }
    }
}

Can you please try to demonstrate the issue with a small live example? https://jsfiddle.net/f2Lommf5/

Right now, it’s really hard to examine all your posted code. Providing a debugging option will make it much easier to analyze your issue and provide help.

/cc

Here is a fiddle reproducing the issue : https://jsfiddle.net/qLc8e3fu/47/

I’m afraid this issue is not three.js but a pure UI related issue. You might get better feedback when posting this in the GSAP community. What 3D engine renders onto the canvas does not matter here.

I also suggest you remove the three.js tag in your stackoverflow post. You are really targeting the wrong community. You need UI devs for this issue, no 3D devs.

Allright @Mugen87, i’ll take a shot on the GSAP forums, thanks for the answer.