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