HTML5 canvas带棱镜效果的炫酷幻灯片特效

当前位置:主页 > Html5库 > html5 canvas > HTML5 canvas带棱镜效果的炫酷幻灯片特效
HTML5 canvas带棱镜效果的炫酷幻灯片特效
分享:

    插件介绍

    这是一款效果非常炫酷华丽的HTML5 canvas带棱镜效果的幻灯片特效。这个特效在每一个幻灯片的前面放置一个图形,并将图形制作为三棱镜效果,它底下的幻灯片图片会被“折射”到棱镜上面,形成一种棱镜折射效果。

    浏览器兼容性

    浏览器兼容性
    时间:04-01
    阅读:
简要教程

这是一款效果非常炫酷华丽的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));
};