UNPKG

threeasy

Version:

A wrapper and some sensible defaults to make the early stages of learning ThreeJS easier

456 lines (427 loc) 11 kB
/** * ThreeasyAnimator class * @class ThreeasyAnimator */ class ThreeasyAnimator { /** * Takes an instance of Threeasy * @param {Threeasy} app */ constructor(app) { this.app = app; this.tasks = []; } /** * Adds a function to the animation loop * @example * app.animator.add(()=>{ * // do something * }) * @param {function} fn */ add(fn) { this.tasks.push(fn); } animate() { requestAnimationFrame(this.animate.bind(this)); this.tasks.forEach((task) => task()); this.app.render(); } } class ThreeasyLoader { constructor(app, settings) { this.app = app; this.THREE = app.THREE; this.settings = { load: () => { this.app.init(); }, gltfExtensions: [".gltf", ".glb"], objExtensions: [".obj"], textureExtensions: [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tga"], ...settings, }; this.manager = new this.THREE.LoadingManager( () => this.settings.load(), (itemURL, itemsLoaded, itemsTotal) => this.progress(itemURL, itemsLoaded, itemsTotal) ); this.setUpLoaders(); } /** * Use to add progress to a loading indicator * @param {string} itemURL - name of asset being loaded * @param {number} itemsLoaded - amount of assets loaded * @param {number} itemsTotal - total assets to load * @example * app.loader.progress = (itemURL, itemsLoaded, itemsTotal) =>{ * console.log(`${(itemsLoaded / itemsTotal)*100}%`); * } */ progress(itemURL, itemsLoaded, itemsTotal) {} setUpLoaders() { this.TextureLoader = false; this.GLTFLoader = false; this.OBJLoader = false; if (this.app.settings.GLTFLoader) { this.GLTFLoader = new this.app.settings.GLTFLoader(this.manager); } if (this.app.settings.OBJLoader) { this.OBJLoader = new this.app.settings.OBJLoader(this.manager); } this.TextureLoader = new this.THREE.TextureLoader(this.manager); } load() { this.handleNesting(this.app.settings.preload); } handleNesting(assets, root = this.app) { for (let key in assets) { if (assets.hasOwnProperty(key)) { const asset = assets[key]; if (typeof asset == "string") { this.handleAsset(root, key, asset); } else { this.handleNesting(asset, (root[key] = {})); } } } } handleAsset(root, variable, path) { // gltf if (this.endsWith(path, this.settings.gltfExtensions)) { this.handleGLTF(root, variable, path); } // obj if (this.endsWith(path, this.settings.objExtensions)) { this.handleOBJ(root, variable, path); } // texture if (this.endsWith(path, this.settings.textureExtensions)) { this.handleTexture(root, variable, path); } } handleGLTF(root, variable, path) { if (this.GLTFLoader) { this.GLTFLoader.load(path, (gltf) => { root[variable] = gltf.scene; }); } else { console.warn( `ThreeasyLoader: GLTFLoader is not defined trying to load: ${path}` ); } } handleOBJ(root, variable, path) { if (this.OBJLoader) { this.OBJLoader.load(path, (obj) => { root[variable] = obj; }); } else { console.warn( `ThreeasyLoader: OBJLoader is not defined trying to load: ${path}` ); } } handleTexture(root, variable, path) { this.TextureLoader.load(path, (texture) => { root[variable] = texture; this.setUpTexture(root[variable]); }); } endsWith(path, arr) { return arr.some((extension) => path.endsWith(extension)); } setUpTexture(texture) { texture.colorSpace = this.THREE.SRGBColorSpace; texture.wrapT = this.THREE.RepeatWrapping; texture.wrapS = this.THREE.RepeatWrapping; } } class ThreeasyPostLoader { constructor(app) { this.app = app; this.tasks = []; } /** * Saves an array of functions to run when all assets are loaded * @example * app.postLoader(()=>{ * app.loadingAnimation.end(); * }) * @param {function} fn - a function to run after all assets have loaded */ add(fn) { this.tasks.push(fn); } load() { this.tasks.forEach((task) => task()); } } /** * ThreeasyInteractions class * @class ThreeasyInteractions */ class ThreeasyInteractions { constructor(app) { this.app = app; this.clicks = []; this.hovers = []; if (this.app.settings.interactions) { this.raycaster = new app.THREE.Raycaster(); this.pointer = new app.THREE.Vector2(); this.app.mouse = this.pointer; this.interactions(); } } noInteractions = () => { return this.clicks.length == 0 && this.hovers.length == 0; }; interactions() { this.app.animator.add(() => { if (this.noInteractions()) return; this.raycaster.setFromCamera(this.pointer, this.app.camera); this.intersects = []; this.intersects = this.raycaster.intersectObjects( this.app.scene.children ); }); this.app.renderer.domElement.addEventListener("mousemove", (event) => { if (this.noInteractions()) return; this.calculatePosition(event); this.calculateIntersects("mouseMove", event); }); this.app.renderer.domElement.addEventListener("click", (event) => { if (this.noInteractions()) return; this.calculatePosition(event); this.calculateIntersects("click", event); }); this.app.renderer.domElement.addEventListener("mouseup", (event) => { if (this.noInteractions()) return; this.calculatePosition(event); this.calculateIntersects("mouseup", event); }); } calculatePosition() { this.pointer.x = (event.clientX / window.innerWidth) * 2 - 1; this.pointer.y = -((event.clientY / window.innerHeight) * 2 - 1); } calculateIntersects(interactionType, event) { let intersectUUIDs = []; for (let i = 0; i < this.intersects.length; i++) { const el = this.intersects[i]; intersectUUIDs.push(el.object.uuid); // CLICK if (interactionType == "click") { const item = this.clicks.find((item) => el.object.uuid == item.el.uuid); if (item) { item.fn(event, el); } } } if (interactionType == "mouseMove") { // HOVERS this.hovers.forEach((hoverItem) => { const isCurrentlyHovered = intersectUUIDs.indexOf(hoverItem.el.uuid) > -1; const wasPreviouslyHovered = hoverItem.el.hovered; if (isCurrentlyHovered && !wasPreviouslyHovered) { hoverItem.el.hovered = true; if (hoverItem.fns.enter) { hoverItem.fns.enter(event, hoverItem.el); } } if (!isCurrentlyHovered && wasPreviouslyHovered) { hoverItem.el.hovered = false; if (hoverItem.fns.leave) { hoverItem.fns.leave(event, hoverItem.el); } } }); } } /** * Pass an object and function to fire when it's clicked * @example * app.interactions.onClick(mesh, * (event, element) => { * // do something * } * ); * @param {Object3D} el * @param {function} fn */ onClick(el, fn) { this.clicks.push({ el, fn }); } /** * Pass an object and function to fire when it's clicked * @example * app.interactions.onHover(mesh, { * enter: (event, element) => { * // do something * }, * leave: (event, element) => { * // do something * }, * }); * @param {Object3D} el * @param {object} fns - an object with an enter and leave function */ onHover(el, fns) { el.hovered = false; this.hovers.push({ el, fns }); } } /** * Threeasy class * @class Threeasy */ class Threeasy { /** * * @param {THREE} THREE * @param {object} settings - settings for your app * @example * { * light: false, * preload: { * models: { * car: 'path/to/car.glb' * }, * textures: { * crate: 'path/to/crate.jpg', * barrel: 'path/to/barrel.jpg', * } * }, * GLTFLoader * @param {boolean} settings.light - Whether to add a light to the scene. * @param {object} settings.preload - An object defining texture, .glb or .GLTF files to preload. Can be any shape of object containing other objects * @param {GLTFLoader} settings.GLTFLoader - A ThreeJS GLTFLoader, if loading .glb or .gltf. * @param {OBJLoader} settings.OBJLoader - A ThreeJS OBJLoader, if loading .obj. */ constructor(THREE, settings) { this.settings = { light: true, alpha: false, interactions: false, domElement: document.body, ...settings, }; if (!this.settings.domElement) { throw new Error("Threeasy: settings.domElement not found."); } this.THREE = THREE; this.setSize(); // ANIMATOR this.animator = new ThreeasyAnimator(this); // SCENE this.scene = new THREE.Scene(); // CAMERA this.camera = new THREE.PerspectiveCamera( 75, this.sizes.w / this.sizes.h, 1, 200 ); this.camera.position.x = 0; this.camera.position.y = 0; this.camera.position.z = 2; this.scene.add(this.camera); // RENDERER this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: this.settings.alpha, }); this.renderer.setSize(this.sizes.w, this.sizes.h); this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio)); this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // INTERACTIONS this.mouse = { x: null, y: null, }; this.interactions = new ThreeasyInteractions(this); // LOADER this.models = {}; this.textures = {}; this.loader = new ThreeasyLoader(this); // POSTLOADER this.postLoader = new ThreeasyPostLoader(this); // LIGHT if (this.settings.light) { this.light = new THREE.AmbientLight(0xffffff, 1); this.scene.add(this.light); } // CLOCK this.clock = new THREE.Clock(); this.clock.start(); // RESIZE this.settings.domElement.appendChild(this.renderer.domElement); this.renderer.domElement.style.maxWidth = "100%"; window.addEventListener("resize", () => this.resize()); this.resizeObserver = new ResizeObserver((entries) => this.resize()); this.resizeObserver.observe(this.settings.domElement); // PRELOAD this.preload(); } setSize() { if (this.settings.domElement === document.body) { this.sizes = { w: window.innerWidth, h: window.innerHeight, }; } else { this.sizes = { w: this.settings.domElement.clientWidth, h: this.settings.domElement.clientHeight, }; } } preload() { if (this.settings.preload) { this.loader.load(); } else { this.init(); } } /** * * @param {function} fn * @returns {void} */ postload(fn) { this.postLoader.add(fn); } /** * @type {function} - A function to replace the default ThreeJS render function. EG for post processing * @returns {void} */ render() { this.renderer.render(this.scene, this.camera); } /** * * @param {function} fn * @returns {void} */ animate(fn) { this.animator.add(fn); } init() { this.postLoader.load(); this.animator.animate(); } /** * @type {function} resize - A function to replace the default Threeasy resize function. * @returns {void} */ resize() { this.setSize(); this.camera.aspect = this.sizes.w / this.sizes.h; this.camera.updateProjectionMatrix(); this.renderer.setSize(this.sizes.w, this.sizes.h); this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio)); } } export { Threeasy as default };