这是一款效果非常炫酷华丽的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 <img> object. * @param {String} path The path of the file. * @param {Function} callback The callback to be executed when loading completed. * @return {Object} The JavaScript <img> 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)); }; |
网盘下载密码:wmiq