这是一款效果非常炫酷华丽的HTML5 canvas带棱镜效果的幻灯片特效。这个特效在每一个幻灯片的前面放置一个图形,并将图形制作为三棱镜效果,它底下的幻灯片图片会被“折射”到棱镜上面,形成一种棱镜折射效果。该效果中使用HTML5 canvas和纯JS来制作棱镜效果。
所有的现代浏览器都支持这个幻灯片特效,包括IE9。
制作方法
这个特效使用的主要技术是:首先调用和渲染一个遮罩层,它可以是SVG或一个PNG图片(重点在于图片是透明的),然后使用globalCompositeOperation来渲染幻灯片的各个图片。
globalCompositeOperation
是canvas的属性,它可以让你定义一幅图片如何在另一幅图片上进行绘制。
默认情况下,当我们在已有的像素上绘制图像的时候,只是会覆盖那些已经存在的像素。而使用globalCompositeOperation
属性,我们可以定义要绘制的图像作为一个遮罩层绘制在目标图像的上面。它有十二个复合操作,这个幻灯片特效中使用的是source-atop
效果,它会以遮罩的形式显示图片,并且遮罩外的部分不会被显示出来。
制作这个效果的关键在于在图像前面绘制遮罩层。另外,由于开始的时候所有的像素都是不存在的,source-atop
操作不会有任何效果。
为了创建效果,需要为棱镜的每一个面都折射出不同的图像,这需要使用分层技术,意思是每一个面都需要一个单独的canvas元素,然后将他们设置为绝对定位并排列成想要的图案。
这个特效需要多个canvas的原因是W3C 定义每一个canvas中只有一个CanvasRenderingContext2D
对象。所以要同时为棱镜的不同部分使用不同的效果只能使用多个层。
HTML结构
这个幻灯片特效需要的HTML结构只是一个<div>
,它里面会被添加canvas,和放置用于制作圆点导航按钮的无序列表。
<div class="container"> <ul class="navigation"></ul> </div>
同时还要在幻灯片初始化之前对需要用到的图片和遮罩进行预加载,否则在图片加载完之前会得到一个空的幻灯片。为了达到这个目的,插件中使用一个div.cache
来包裹一系列需要使用到的图片和遮罩层svg。然后使用display: none
将它隐藏,然后在window.onload时初始化插件。(实际上使用javascript来预加载图片效果会更加好。)
<div class="cache"> <!-- masks --> <img src="img/masks/cube-a.svg"> <img src="img/masks/cube-b.svg"> <img src="img/masks/cube-c.svg"> <!-- photos --> <img src="img/shoreditch-a.jpg"> <img src="img/shoreditch-b.jpg"> <img src="img/shoreditch-c.jpg"> </div>
CSS样式
下面是该幻灯片插件的基本样式。
.prism-slider { width: 1200px; max-width: 100%; height: 0; padding-bottom: 48%; position: relative; } .prism-slider canvas { width: 100%; position: absolute; top: 0; left: 0; } .navigation { width: 100%; position: absolute; bottom: 5%; text-align: center; list-style: none; z-index: 1; } .navigation li { border: 3px solid #eceff1; width: 18px; height: 18px; margin: 0 5px; background: #52525a; border-radius: 50%; display: inline-block; cursor: pointer; } .navigation .active { background: #eceff1; }
JAVASCRIPT
该幻灯片插件使用的JavaScript分为两个模块:slideshow.js
作为控制器,PrismSlider.js
负责插件和渲染每一个canvas层。
先来看看PrismSlider的第一个函数:
/** * Create canvas element, get context, set sizes * and append to main container. */ PrismSlider.prototype.addCanvas_ = function() { this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext('2d'); this.canvas.width = this.settings.container.sizes.w; this.canvas.height = this.settings.container.sizes.h; this.container.appendChild(this.canvas); };
现在我们有了一个canvas元素,这时要添加和绘制遮罩层。
/** * Add Mask. * Call loadImage method with path and callback, * once the loading will be completed we'll replace * the string path (this.mask.source) reference with * the actual <img> object. */ PrismSlider.prototype.addMask_ = function() { var path = this.mask.source; var callback = this.renderMask_.bind(this); // Replace image path with <img> object. this.mask.source = this.loadImage_(path, callback); }; /** * Draw mask. * Calculate center position and draw mask, width and height at 100% of the container sizes. */ PrismSlider.prototype.renderMask_ = function() { var centerX = this.canvas.width / 2 - this.settings.container.sizes.w / 2; var centerY = this.canvas.height / 2 - this.settings.container.sizes.h / 2; var w = this.settings.container.sizes.w; var h = this.settings.container.sizes.h; this.context.drawImage(this.mask.source, centerX, centerY, w, h); };
上面的代码中使用了 loadImage
方法,由于这时浏览器已经缓存了所需要的图片和SVG遮罩,所以可以在这时无延迟的获取到SVG遮罩图像。
/** * Load image source from path and fire given callback, * return loaded object. * @param {String} path The path of the file. * @param {Function} callback The callback to be executed when loading completed. * @return {Object} The JavaScript object. */ PrismSlider.prototype.loadImage_ = function(path, callback) { var image = new Image(); image.onload = callback; // Path always after callback. image.src = path; return image; };
下载已经添加和绘制的遮罩层,接下来要以相同的手法添加幻灯片。
/** * Add Slides. * Call loadImage method for each image path in the slides array, * only when it's the first slide pass render callback, * when loading completed replace image path with object. */ PrismSlider.prototype.addSlides_ = function() { this.slides.forEach(function(path, i) { // Render only first slide. var callback = (i === 0) ? this.renderSlide_.bind(this, i) : null; // Replace image path with object. this.slides[i] = this.loadImage_(path, callback); }, this); };
renderSlide_
渲染回调函数有一些复杂:
- 它有两个参数,一个是
addSlides_
循环的index数,另一个是一个progress值,目前还用不到。 - 要注意是如何计算出X坐标的值的,还要记住
i
是一个0到幻灯片length
之间的值。 - 只有在遮罩层被渲染的时候才使用图像复合操作。
- 最后在绘制图像之前为它们添加一些效果。
代码如下:
/** * Draw Slide. * Calculate frame position, apply composite operation * and effects on the image when there is a mask. * @param {Number} i The index used to get the img to render. * @param {Number} progress The progress value. */ PrismSlider.prototype.renderSlide_ = function(i, progress) { // Set progress to 0 if Not a Number or undefined. progress = (isNaN(progress) || progress === undefined) ? 0 : progress; // Get img object from array. var slide = this.slides[i]; // Calculate X position. var x = this.canvas.width * (i - progress); var y = 0; var w = this.canvas.width; var h = this.canvas.height; // Apply composite operation. if (this.mask) this.context.globalCompositeOperation = 'source-atop'; this.context.save(); if (this.mask) this.applyEffects_(); // Draw slide. this.context.drawImage(slide, x, y, w, h); this.context.restore(); };
下面来看一下slideshow
控制器。
现在,PrismSlider.js
可以被实例化并生成canvas元素,调用图像并渲染它们。为了代码的整洁,插件中使用一个控制器来控制PrismSlider。这个控制器就是slideshow.js。下面来看看它的变量:
/** * Enum navigation classes, attributes and * provide navigation DOM element container. */ var navigation = { selector: '.navigation', element: null, bullet: 'li', attrs: { active: 'active', index: 'data-index' } }; /** * Enum main element, sizes and provide * main DOM element container. * @type {Object} */ var container = { selector: '.container', element: null, sizes: { w: 1200, h: 780 } }; /** * Set of images to be used. * @type {Array} */ var slides = [ 'img/shoreditch-a.jpg', 'img/shoreditch-b.jpg', 'img/shoreditch-c.jpg', 'img/graffiti-a.jpg', 'img/graffiti-b.jpg', 'img/graffiti-c.jpg' ]; /** * Set of masks with related effects. * @type {Array} */ var masks = [ { source: 'img/masks/cube-a.svg', effects: { flip: 'Y', rotate: 167 // degrees } }, { source: 'img/masks/cube-b.svg', effects: { flip: 'X', rotate: 90 // degrees } }, { source: 'img/masks/cube-c.svg', effects: { flip: false, rotate: 13 // degrees } } ]; /** * Set global easing. * @type {Function(currentTime)} */ var easing = Easing.easeInOutQuint; /** * Set global duration. * @type {Number} */ var duration = 2000; /** * Container for PrismSlider instances. * @type {Object} */ var instances = {};
注意最后一个instances
变量,它是一个空的对象,它将被作为“容器”来用于引用每一个canvas。
初始化的方法如下:
/** * Init. */ function init() { getContainer_(); initSlider_(); initPrism_(); addNavigation_(); addEvents_(); } /** * Get main container element, and store in container element. */ function getContainer_() { container.element = document.querySelector(container.selector); } /** * Init Slides. * Create and initialise main background slider (first layer). * Since we'll use this as main slider no mask is given. */ function initSlider_() { instances.slider = new PrismSlider({ container: container, slides: slides, mask: false, duration: duration, easing: easing }); // Initialise instance. instances.slider.init(); } /** * Init Masks. * Loop masks variable and create a new layer for each mask object. */ function initPrism_() { masks.forEach(function(mask, i) { // Generate reference name. var name = 'mask_' + i; instances[name] = new PrismSlider({ container: container, slides: slides, mask: mask, // Here is the mask object. duration: duration, easing: easing }); // Initialise instance. instances[name].init(); }); } /** * Add Navigation. * Create a new bullet for each slide and add it to navigation (ul) * with data-index reference. */ function addNavigation_() { // Store navigation element. navigation.element = document.querySelector(navigation.selector); slides.forEach(function(slide, i) { var bullet = document.createElement(navigation.bullet); bullet.setAttribute(navigation.attrs.index, i); // When it's first bullet set class as active. if (i === 0) bullet.className = navigation.attrs.active; navigation.element.appendChild(bullet); }); } /** * Add Events. * Bind click on bullets. */ function addEvents_() { ... }
在initSlider
方法中创建了一个新的PrismSlider对象,并将mask
设置为false
,这是为了创建一个完整的背景图层。
在initPrism
方法中,通过玄幻遮罩层数组,为每一个遮罩层创建一个新的实例并插入遮罩参数。
下面要做的事情就是这种动画效果。当用户点击圆点导航按钮的时候,slideAllTo
事件就会被触发。
/** * Add Events. * Bind click on bullets. */ function addEvents_() { // Detect click on navigation elment (ul). navigation.element.addEventListener('click', function(e) { // Get clicked element. var bullet = e.target; // Detect if the clicked element is actually a bullet (li). var isBullet = bullet.nodeName === navigation.bullet.toUpperCase(); // Check bullet and prevent action if animation is in progress. if (isBullet && !instances.slider.isAnimated) { // Remove active class from all bullets. for (var i = 0; i < navigation.element.childNodes.length; i++) { navigation.element.childNodes[i].className = ''; } // Add active class to clicked bullet. bullet.className = navigation.attrs.active; // Get index from data attribute and convert string to number. var index = Number(bullet.getAttribute(navigation.attrs.index)); // Call slideAllTo method with index. slideAllTo_(index); } }); } /** * Call slideTo method of each instance. * In order to sync sliding of all layers we'll loop through the * instances object and call the slideTo method for each instance. * @param {Number} index The index of the destination slide. */ function slideAllTo_(index) { // Loop PrismSlider instances. for (var key in instances) { if (instances.hasOwnProperty(key)) { // Call slideTo for current instance. instances[key].slideTo(index); } } }
正如上面的注释所描述的,slideAllTo
方法会循环所有的实例,并调用PrismSlider.prototype.slideTo
方法。
/** * Slide To. * @param {Number} index The destination slide index. */ PrismSlider.prototype.slideTo = function(index) { // Prevent when animation is in progress or if same bullet is clicked. if (this.isAnimated || index === this.slidesIndex) return; // Store current (start) index. this.prevSlidesIndex = this.slidesIndex; // Set destination (end) index. this.slidesIndex = index; // Calculate how many slides between current (start) and destination (end). var indexOffset = (this.prevSlidesIndex - this.slidesIndex) * -1; // Store offset always converted to positive number. this.indexOffset = (indexOffset > 0) ? indexOffset : indexOffset * -1; // Kickstart animation. this.animate_(); };
上面代码的关键点在于更新index和有多少个幻灯片需要进行动画。
最后两个动画的方法是简单的通过Date.now()
与持续时间之和来计算结束时间。ticker
方法是通过调用requestAnimationFrame
方法来完成的。
/** * Animate. */ PrismSlider.prototype.animate_ = function() { // Calculate end time. var end = Date.now() + this.duration; // Mark animation as in progress. this.isAnimated = true; // Kickstart frames ticker. this.ticker_(end); }; /** * Ticker called for each frame of the animation. * @param {Number} end The end time of the animation. */ PrismSlider.prototype.ticker_ = function(end) { // Start time. var now = Date.now(); // Update time left in the animation. var remaining = end - now; // Retrieve easing and multiply for number of slides between stars // and end, in order to jump through N slides in one ease. var easing = this.easing(remaining / this.duration) * this.indexOffset; var i, progress, slide; // Select sliding direction. if (this.slidesIndex > this.prevSlidesIndex) { // Sliding forward. progress = this.slidesIndex - easing; // Loop offset and render slides from start to end. for (i = 0; i <= this.indexOffset; i++) { slide = this.slidesIndex - i; this.renderSlide_(slide, progress); } } else { // Sliding backward. progress = this.slidesIndex + easing; // Loop offset and render slides from start to end. for (i = 0; i >= this.indexOffset; i++) { slide = this.slidesIndex + i; this.renderSlide_(slide, progress); } } // Under 50 milliseconds reset and stop. if (remaining < 50) { // Set default value. this.indexOffset = 1; // Make sure slide is perfectly aligned. this.renderSlide_(this.slidesIndex); // Mark animation as finished. this.isAnimated = false; // Stop. return; } // Kickstart rAF with updated end. window.requestAnimationFrame(this.ticker_.bind(this, end)); };