src/components/slide.vue
<template> <div class="slide" :class="{ invisible: invisible_slide }" @click.stop="change_slide()"> <div ref="canvas" class="slider_images" /> <div class="slider_ads">Click to change slide</div> <span ref="image_legend" class="slider_legend" /> <div class="informations"> <div class="field"> <span class="legend">Client</span> <span class="value">{{ client }}</span> </div> <div class="field"> <span class="legend">Industry</span> <span class="value">{{ industry }}</span> </div> <div class="field"> <span class="legend">Position</span> <span class="value">{{ position }}</span> </div> <ul ref="summary" class="slider_index"> <li v-for="title in titles" :key="title" :data-title="title"> {{ title }} </li> </ul> </div> <div class="description_mobile"> <span>{{ slide.first_text }}</span> <span>{{ slide.second_text }}</span> </div> <div ref="description" class="description"> <div class="block block_1"> <div class="block_title"> <span>Slide 1</span> </div> <div class="block_information"> <span>{{ slide.first_text }}</span> </div> </div> <div class="block block_2"> <div class="block_title"> <span>Slide 2</span> </div> <div class="block_information"> <span>{{ slide.second_text }}</span> </div> </div> </div> <div class="borders_top_right" /> <div class="borders_bottom_left" /> </div></template><script>import utils from '../helper/utils.js';import * as THREE from 'three'; const FOV = 40;const BOARD_WIDTH = 256;const DEFAULT_POSITION_Z_CAMERA = BOARD_WIDTH + 150;const BOARD_HEIGHT = 128;const BOARD_NAME_CENTER = 'center';const BOARD_NAME_LEFT = 'left';const BOARD_NAME_RIGHT = 'right';const BOARD_NAME_LEFT_WIREFRAME = 'left_wireframe';const BOARD_NAME_RIGHT_WIREFRAME = 'right_wireframe';// Half of the board minus the distance between the inside of the shape to the extremity rightconst SIDE_BOARD_X = BOARD_WIDTH / 2 - 25;const SPEED_MOVEMENT_CENTER_BOARD = 12;const SPEED_MOVEMENT_SIDE_BOARD = 450;const DEFAULT_ROTATION_PERPETUAL_X = 0.001;const DEFAULT_ROTATION_PERPETUAL_Y = 0.002;const DEFAULT_ROTATION_PERPETUAL_X_START = 0;const DEFAULT_ROTATION_PERPETUAL_Y_START = 0;const DEFAULT_ROTATION_PERPETUAL_X_AMPLITUDE = 20;const DEFAULT_ROTATION_PERPETUAL_Y_AMPLITUDE = 15;const DEFAULT_ROTATION_PERPETUAL_X_SPEED = 100;const DEFAULT_ROTATION_PERPETUAL_Y_SPEED = 200;const FRONT_BOARD_MATERIAL = new THREE.MeshBasicMaterial({ color: 0x111116 });const BLACK_MATERIAL = new THREE.MeshBasicMaterial({ color: 0x000000 });const BACK_BOARD_MATERIAL = new THREE.MeshBasicMaterial({ color: 0x111116 });const BOARD_MATERIAL = new THREE.MeshPhongMaterial({ color: 0x1d1d23 });const BLUE_LINE_MATERIAL = new THREE.LineBasicMaterial({ color: 0x61c3ff, linewidth: 1});const EXTRUDE_SETTINGS = { amount: 10, bevelEnabled: true, bevelSegments: 1, steps: 2, bevelSize: 3, bevelThickness: 3};const SHAPE = new THREE.Shape();SHAPE.moveTo(-50, -85);SHAPE.lineTo(-45, -30);SHAPE.lineTo(-35, -20);SHAPE.lineTo(-35, 25);SHAPE.lineTo(-45, 35);SHAPE.lineTo(-50, 85);SHAPE.lineTo(-5, 75);SHAPE.lineTo(0, 64);SHAPE.lineTo(-20, 64);SHAPE.lineTo(-25, 60);SHAPE.lineTo(-25, -55);SHAPE.lineTo(-20, -60);SHAPE.lineTo(0, -64);SHAPE.lineTo(-5, -75);const GEOMETRY_FROM_SHAPE = new THREE.ExtrudeGeometry(SHAPE, EXTRUDE_SETTINGS); export default { props: { invisible_slide: { type: Boolean, required: true }, titles: { type: Array, required: true }, slide: { type: Object, required: true }, client: { type: String, required: true }, industry: { type: String, required: true }, position: { type: String, required: true } }, emits: ['change_slide'], data: () => { return { camera: null, scene: null, renderer: null, new_image: false, board: null, picture_loaded: false, board_animation: false, board_animation_step: 0, board_actual_rotation: 0 }; }, watch: { slide() { this.slide_image(); } }, async mounted() { this.initCamera(); this.animate(); }, methods: { initCamera() { this.camera = new THREE.PerspectiveCamera( FOV, this.$refs.canvas.clientWidth / this.$refs.canvas.clientHeight ); this.update_camera_z(); this.clock = new THREE.Clock(); this.clock.start(); this.scene = new THREE.Scene(); this.scene.add(new THREE.AmbientLight(0xffffff, 1)); // Initialize the board this.board = this.initialize_board(); this.scene.add(this.board); this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setClearColor(0x111116, 1); this.renderer.setSize( this.$refs.canvas.clientWidth, this.$refs.canvas.clientHeight ); this.$refs.canvas.appendChild(this.renderer.domElement); window.addEventListener('resize', this.onWindowResize, false); }, /** * Construct a board piece by piece **/ initialize_board() { const board = new THREE.Group(); board.add(this.create_center_board(BOARD_NAME_CENTER)); board.add(this.createSideBoard(BOARD_NAME_LEFT, -SIDE_BOARD_X)); board.add(this.createSideBoard(BOARD_NAME_RIGHT, SIDE_BOARD_X, true)); board.add( this.createSideWireframe(BOARD_NAME_LEFT_WIREFRAME, -SIDE_BOARD_X) ); board.add( this.createSideWireframe(BOARD_NAME_RIGHT_WIREFRAME, SIDE_BOARD_X, true) ); return board; }, createSideBoard(name, x, symmetry = false) { const mesh = new THREE.Mesh(GEOMETRY_FROM_SHAPE, BOARD_MATERIAL); mesh.position.set(x, 0, 0); if (symmetry) { this.apply_symmetry(mesh); } mesh.name = name; return mesh; }, createSideWireframe(name, x, symmetry = false) { const wireframe = new THREE.LineSegments( new THREE.EdgesGeometry(GEOMETRY_FROM_SHAPE), BLUE_LINE_MATERIAL ); wireframe['wireframe'] = true; wireframe.position.set(x, 0, 0); if (symmetry) { this.apply_symmetry(wireframe); } wireframe.name = name; return wireframe; }, apply_symmetry(mesh) { mesh.position.z = 10; mesh.rotation.y = Math.PI; }, create_center_board(name) { const mesh = new THREE.Mesh( new THREE.BoxBufferGeometry(BOARD_WIDTH, BOARD_HEIGHT, 1), [ BLACK_MATERIAL, BLACK_MATERIAL, BLACK_MATERIAL, BLACK_MATERIAL, FRONT_BOARD_MATERIAL, BACK_BOARD_MATERIAL ] ); mesh.name = name; return mesh; }, get_children_by_name(name) { return this.board.children.find((children) => children.name === name); }, animate() { requestAnimationFrame(this.animate); this.delta = this.clock.getDelta(); this.perpetual(this.board); if (this.board_animation) { this.board_new_image_animation(); } this.renderer.render(this.scene, this.camera); }, change_image(path) { const index = this.board_actual_rotation % 360 === 0 ? 4 : 5; const board_center = this.get_children_by_name(BOARD_NAME_CENTER); const texture = new THREE.TextureLoader().load( utils.absolute_path_from_relative(path), () => { this.picture_loaded = true; } ); if (index === 5) { texture.flipY = false; texture.wrapS = THREE.RepeatWrapping; texture.repeat.x = -1; } const material = new THREE.MeshBasicMaterial({ map: texture }); board_center.material[index] = material; }, perpetual(board) { board.rotation.x = this.radians(DEFAULT_ROTATION_PERPETUAL_X_START) + Math.cos( this.clock.elapsedTime * DEFAULT_ROTATION_PERPETUAL_X_SPEED * DEFAULT_ROTATION_PERPETUAL_X ) * this.radians(DEFAULT_ROTATION_PERPETUAL_X_AMPLITUDE); board.rotation.y = this.radians(DEFAULT_ROTATION_PERPETUAL_Y_START) + Math.cos( this.clock.elapsedTime * DEFAULT_ROTATION_PERPETUAL_Y_SPEED * DEFAULT_ROTATION_PERPETUAL_Y + 300 ) * this.radians(DEFAULT_ROTATION_PERPETUAL_Y_AMPLITUDE); }, initialize_board_rotation() { this.picture_loaded = false; this.board_animation = true; this.board_actual_rotation += 180; this.board_animation_step = 0; }, board_new_image_animation() { // Remove the space between the side and the center if (this.board_animation_step === 0) { const left_step = this.space_negative_side_board( -165, BOARD_NAME_LEFT, BOARD_NAME_LEFT_WIREFRAME ); const right_step = this.space_positive_side_board( 165, BOARD_NAME_RIGHT, BOARD_NAME_RIGHT_WIREFRAME ); this.board_animation_step = left_step && right_step ? 1 : 0; } // rotate the center board if (this.board_animation_step === 1) { this.board_animation_step = this.rotate_center_board() ? 2 : 1; } // Make the sides come back if (this.board_animation_step === 2) { const left_step = this.space_positive_side_board( -SIDE_BOARD_X, BOARD_NAME_LEFT, BOARD_NAME_LEFT_WIREFRAME ); const right_step = this.space_negative_side_board( SIDE_BOARD_X, BOARD_NAME_RIGHT, BOARD_NAME_RIGHT_WIREFRAME ); this.board_animation_step = left_step && right_step ? 3 : 2; } // Animation over if (this.board_animation_step === 3) { this.board_animation = false; } }, space_side_board(limit, board_name, wireframe_name, fc) { const side_board = [ this.get_children_by_name(board_name), this.get_children_by_name(wireframe_name) ]; const is_limit_reached = side_board.map((children) => { children.position.x = fc(limit, children, SPEED_MOVEMENT_SIDE_BOARD); return children.position.x === limit; }); return is_limit_reached[0] && is_limit_reached[1]; }, space_negative_side_board(limit, board_name, wireframe_name) { return this.space_side_board( limit, board_name, wireframe_name, (limit, children, speed) => { return Math.max(limit, children.position.x - this.delta * speed); } ); }, space_positive_side_board(limit, board_name, wireframe_name) { return this.space_side_board( limit, board_name, wireframe_name, (limit, children, speed) => { return Math.min(limit, children.position.x + this.delta * speed); } ); }, rotate_center_board() { const board_center = this.get_children_by_name(BOARD_NAME_CENTER); if (this.picture_loaded) { board_center.rotation.x = Math.min( this.radians(this.board_actual_rotation), board_center.rotation.x + this.delta * SPEED_MOVEMENT_CENTER_BOARD ); } return ( board_center.rotation.x === this.radians(this.board_actual_rotation) ); }, radians(degrees) { return (degrees * Math.PI) / 180; }, change_slide() { this.$emit('change_slide'); }, change_summary(title) { const liPrevious = this.$refs.summary.querySelector('.selected'); const liNext = this.$refs.summary.querySelector( "li[data-title='" + title + "']" ); liNext.classList.add('selected'); if (liPrevious) { liPrevious.classList.remove('selected'); } }, slide_image() { this.$refs.description.classList.add('loading'); if (this.slide.image.path === undefined) { return null; } this.change_summary(this.slide.title); this.$refs.image_legend.innerHTML = this.slide.image.name; this.initialize_board_rotation(); this.change_image(this.slide.image.path); this.$refs.description.classList.remove('loading'); }, update_camera_z() { if (this.$refs.canvas.clientWidth < 650) { this.camera.position.z = 642.927 - 0.434518 * this.$refs.canvas.clientWidth; } else if (this.$refs.canvas.clientWidth > 800) { this.camera.position.z = BOARD_WIDTH + 75; } else { this.camera.position.z = DEFAULT_POSITION_Z_CAMERA; } }, onWindowResize() { this.camera.aspect = this.$refs.canvas.clientWidth / this.$refs.canvas.clientHeight; this.update_camera_z(); this.camera.updateProjectionMatrix(); this.renderer.setSize( this.$refs.canvas.clientWidth, this.$refs.canvas.clientHeight ); } }};</script><style src="../assets/less/slide.less"></style>