Jump to content
Search Community

GSAP & Three.js : Positioning

Perdixo test
Moderator Tag

Recommended Posts

Hi everybody!

I'm trying to display a three.js scene inside a div in my html, but i can't figure out how. I've posted in the three.js forums, but it seems it's an UI issue and not a three.js one so.. here i am!

 

Here is a jsfiddle trying to replicate the issue JSFIDDLE

 

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

 

If anybody could take a look at this and help me, i'd be super greatful!

 

Here is my current javascript 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);

 

 

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.2, 1, 0.2, 1);

.dom-gl {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    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;
    }
}


.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;
        }
    }
}

 

By the way, i'm also using Locomotive Scroll on my page for creating smooth inertia scrolling.

Link to comment
Share on other sites

Welcome to the forums, @Perdixo

 

Did you have any GSAP-specific questions? This forum is focused on GSAP - we just don't have the resources to help everyone with generic positioning or THREE.js questions but we'd love to help with any GSAP animation questions. Feel free to ask any of those here.

 

Good luck with your project!

  • Like 1
Link to comment
Share on other sites

Hey,

Thanks for the welcome!


So i've posted on the three.js forums and they told me that my issue was a GSAP specific question.. That's why i'm posting it here! I'm a little bit losted since i originally thought my issue was THREE related, it seems that it is not..

Link to comment
Share on other sites

Well what i want is to make my THREE.JS scene only appear in the canvas-projects div. Right now, the images from my three.js scene don't appear.. The only thing that appear is my text animation on drag. In the background, there should be the image slides but right now the background stays black and don't show them.

Link to comment
Share on other sites

1 minute ago, Perdixo said:

The issue is the following : It works when i don't have any other sections than the slider. You can take a look at it here https://jsfiddle.net/j5huqxpd/15/

What i need is to add sections before and after, but as soon as i add sections with a defined height, the images don't show up anymore

That proves that it's an HTML and CSS question :) So you may or may not get aid regarding that question in these forums.

  • Like 1
Link to comment
Share on other sites

Hey @ZachSaucier you were right, that was kind of an HTML / JS issue, now fixed! Thanks a lot. 

 

By the way, here is a fixed version for those interested https://jsfiddle.net/t3ye7ga9/7/

 

Still im having some trouble (GSAP this time) with the slider : there is a delay with the titles when you drag the slider. So if you drag it to do a full round, you can see that the "Project 01" Re-appear right after the "Project 04", and so when you continue dragging it delays the titles (meaning the good titles don't match the images => "Project 01" should be on top of the first image, "Project 02" on the second image and so on). And the first image that appear before you drag is the fourth one..

 

I don't understand what i'm missing here as i'm using my items.length to translate titles..

Link to comment
Share on other sites

Again, not a GSAP issue. It's a logical issue. Essentially what's happening: as soon as it crosses 100%, it jumps to 0. As a result, just add another duplicate of the first project name at the end of the list: https://jsfiddle.net/hmrfzop1/ Note that I only changed your HTML by adding an extra <a class="p-titles__title | js-title" href="#">Project 01</a>

  • Like 1
Link to comment
Share on other sites

23 minutes ago, Perdixo said:

"Project 01" is coming too fast then, as the rest of the titles..

All of the titles are offset. Again, it's a logical issue :) 

 

Unfortunately I don't have the capacity to investigate the issue because there is too much code for me to wade through and it's not an issue with GSAP. Maybe someone else in the forums will be able to help you or you'll be able to figure it out. 

 

The code that you posted in the last post is surely related but I can't say whether or not it's the only thing you'll need to change.

  • Like 1
Link to comment
Share on other sites

  • 1 month later...

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...