棱镜计划_棱镜效果滑块与画布

本文介绍了如何使用HTML5 canvas元素和JavaScript创建一个具有棱镜效果的滑块。该滑块通过在图像前放置一个形状,并应用全局组合操作,使图像呈现出被该形状反射的效果,从而创造出类似棱镜的视觉效果。文章详细讲解了实现这一效果的技术细节,包括HTML结构、CSS样式、JavaScript代码,以及如何处理图像加载和动画效果。
摘要由CSDN通过智能技术生成
棱镜计划

棱镜计划

PrismEffectSlider

Today we’d like to show you how to build a simple slider with an interesting “prism” effect. The idea is to place a shape in front of the slider and “reflect” the images of each slide, in order to create the illusion of a prism. We’ll be using the HTML5 canvas element and plain JavaScript.

今天,我们想向您展示如何构建一个具有有趣的“棱镜”效果的简单滑块。 这个想法是在滑块的前面放置一个形状,并“反射”每个幻灯片的图像,以产生棱镜的错觉。 我们将使用HTML5 canvas元素和纯JavaScript

The demo is supported in all major browsers, including Internet Explorer 9.

所有主要浏览器(包括Internet Explorer 9)都支持该演示。

技术 (The Technique)

The technique used to create the effect is actually pretty simple: first we’ll load and render the mask, which could be either an SVG or a PNG image (the important thing is that it has to have transparency), then we will render the slide’s image and apply the globalCompositeOperation.

用于创建效果的技术实际上非常简单:首先,我们将加载并渲染遮罩,该遮罩可以是SVG图像或PNG图像(重要的是它必须具有透明度),然后我们将渲染该遮罩。幻灯片的图像并应用globalCompositeOperation

The ‘globalCompositeOperation’ canvas property lets you define how an image should be drawn over another image. By default, when we draw an image over existing pixels, the new image just replaces those pixels.

“ globalCompositeOperation”画布属性可让您定义如何在另一个图像上绘制图像。 默认情况下,当我们在现有像素上绘制图像时,新图像将替换这些像素。

By using globalCompositeOperation we will define how the image (source) is drawn onto the mask (destination). There are twelve composite operations and the one that suits our case is source-atop which will display the image on top of the mask and will not show anything outside the area defined by the mask.

通过使用globalCompositeOperation,我们将定义如何将图像(源)绘制到蒙版(目标)上。 有十二种复合操作,适合我们情况的一种操作是在源头上进行操作,它将在蒙版顶部显示图像,而不会在蒙版定义的区域之外显示任何内容。

The key to the effect is to draw the mask before the image, otherwise, since all pixels are empty initially, the ‘source-atop’ operation will not take any effect.

效果的关键是在图像之前绘制遮罩,否则,由于所有像素最初都是空的,因此“ source-atop”操作将不起作用。

In order to create the full effect we’ll need a layer for each part of our prism that we want to reflect the image in different ways. To do this we’ll use a layering technique, which means that we’ll have a separate canvas element for each layer. Then we’ll simply position them absolutely and place the canvases on top of each other.

为了创建完整的效果,我们需要为棱镜的每个部分都需要一个层,以便以不同的方式反射图像。 为此,我们将使用分层技术,这意味着我们将为每个图层使用单独的canvas元素。 然后,我们将它们完全定位,并将画布彼此叠放。

The reason why we would need to layer multiple canvases is because by W3C definition there is only one CanvasRenderingContext2D object per canvas, so in order to apply different effects on distinct parts (masks) of the prism at the same time we’ll need multiple contexts.

我们需要对多个画布进行分层的原因是,根据W3C的定义,每个画布只有一个CanvasRenderingContext2D对象,因此,为了同时对棱镜的不同部分(蒙版)应用不同的效果,我们需要多个上下文。

We’ll have a look at this in more detail later. Let’s start with the HTML structure and some styles.

我们稍后将对此进行更详细的介绍。 让我们从HTML结构和一些样式开始。

PrismEffectSlider01

HTML和CSS (The HTML and CSS)

The only bits of HTML that we need for the slideshow initially, are a division where we append the canvases and an unordered list for the navigation bullets:

最初,幻灯片放映所需HTML唯一内容是一个分区,在其中添加画布和导航项目符号的无序列表:

<div class="container">
    <ul class="navigation"></ul>
  </div>

And here it is the required CSS:

这是必需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;
}

We also need to pre-load the external resources for images and masks before the initialization of the slider, otherwise we’ll get an empty slider until the images are loaded. To do this we’ll use a division with the class “cache” which will have have an img element for each image/mask:

我们还需要在初始化滑块之前预先加载图像和遮罩的外部资源,否则我们将获得一个空的滑块,直到图像被加载为止。 为此,我们将对“ cache”类使用一个除法,该除法将为每个图像/蒙版包含一个img元素:

<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>

Then we simply hide it with display: none and initialize the plugin on window.onload.

然后我们只用display: none隐藏它display: none并在window.onload上初始化插件。

JavaScript (The JavaScript)

The JavaScript is split in two modules: slideshow.js which acts as a controller, and PrismSlider.js which is the class responsible for the creation and rendering of each canvas layer.

JavaScript分为两个模块:充当控制器的slideshow.js和负责创建和渲染每个画布层的类PrismSlider.js

Let’s first have a look at the main JavaScript of the Prism Slider and its first method that we call:

首先让我们看一下Prism Slider的主要JavaScript及其调用的第一个方法:

/**
 * 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);
};

Now that we have a canvas element as mentioned earlier we need to add and draw the mask:

现在我们有了前面提到的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);
};

In the snippets above we used the loadImage method, since the browser has already cached all images at this point (because the script started after the document loaded) we can get the SVG mask and proceed without delaying the execution.

在上面的代码段中,我们使用了loadImage方法,因为此时浏览器已经缓存了所有图像(因为脚本在加载文档后启动),所以我们可以获得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;
};

Now that we added and drew the mask let’s add some slides in a similar way:

现在,我们添加并绘制了蒙版,让我们以类似的方式添加一些幻灯片:

/**
 * 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. this.slides[i] = this.loadImage_(path, callback); }, this); };

The rendering callback is a little bit more complex:

渲染回调稍微复杂一点:

  • we get a couple of arguments, the index from the loop addSlides_ and a progress value that we don’t need for now but we make sure that it’s not going to be anything different from a number (like an event derived from image.onload).

    我们得到了几个参数,来自循环addSlides_的索引,以及我们现在不需要的进度值,但我们确保它与数字没有什么不同(例如从image.onload派生的事件) 。

  • Notice how we calculate the X position and remember that i is a number between 0 and the length of the slides that we will use.

    注意我们如何计算X位置,并记住i是介于0和将要使用的幻灯片长度之间的数字。
  • We also apply the composite operation only if we have a mask to render.

    仅当我们要渲染遮罩时,我们才应用复合操作。
  • Finally, we apply some effects right before the drawing.

    最后,我们在图纸之前应用一些效果。

The code:

代码:

/**
 * 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();
};

The applyEffects method will just select one or both effects which will change the context before the drawing and will distinguish the image inside the mask from the main image on the slider.

applyEffects方法将仅选择一个或两个效果,这将在绘制之前更改上下文,并将遮罩内的图像与滑块上的主图像区分开。

/**
 * Apply effects.
 * Check mask object parameters and select effect.
 */
PrismSlider.prototype.applyEffects_ = function() {
  if (this.mask.effects.flip) this.flip_();
  if (this.mask.effects.rotate > 0) this.rotate_();
};

/**
 * Flip Effect.
 */
PrismSlider.prototype.flip_ = function() {
  // Get axes.
  var axes = this.mask.effects.flip;

  if (axes === 'X') {
    // Invert x position.
    this.context.translate(this.canvas.width, 0);
    // Flip context horizontally.
    this.context.scale(-1, 1);
  }

  if (axes === 'Y') {
    // Invert y position.
    this.context.translate(0, this.canvas.height);
    // Flip context vertically.
    this.context.scale(1, -1);
  }
};

/**
 * Rotate Effect.
 */
PrismSlider.prototype.rotate_ = function() {
  // Convert degrees to radians.
  var radians = this.mask.effects.rotate * (Math.PI / 180);
  // Move registration point to the center of the canvas.
  this.context.translate(this.canvas.width / 2, this.canvas.height / 2);
  // Apply rotation.
  this.context.rotate(radians);
  // Move registration point back to the top left corner of canvas.
  this.context.translate(-this.canvas.width / 2, -this.canvas.height / 2);
};

Let’s have a look at the slideshow controller.

让我们看一下幻灯片控制器。

At this point we have PrismSlider.js which can be instantiated and it will generate a canvas element, load the images and render both, mask and slide.

至此,我们可以实例化PrismSlider.js,它将生成一个canvas元素,加载图像并渲染蒙版和幻灯片。

To keep things clean we’ll add another script which we’ll use as main controller to give instructions to the PrismSlider.

为了保持整洁,我们将添加另一个脚本,该脚本将用作主控制器来向PrismSlider提供指令。

The code necessary for this is in slideshow.js. Let’s take a look at the configuration variables:

为此所需的代码在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 = {};

Notice the the last “instances” variable: it’s an empty object that we’ll use as a ‘container’ in order to keep a reference to each canvas, or better, to the PrismSlider instance and its methods.

注意最后一个“ instances”变量:这是一个空对象,我们将其用作“容器”,以便保留对每个画布或更佳的PrismSlider实例及其方法的引用。

In the init function steps are the following:

在init函数中,步骤如下:

/**
 * 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_() {
  ...
}

In initSlider we create a new PrismSlider instance with mask set to false in order to have a full background layer.

initSlider中,我们创建一个新的PrismSlider实例,并将其mask设置为false ,以便具有完整的背景层。

In initPrism we loop through the masks array defined above and for each mask we create a new instance and pass the mask parameters.

initPrism中,我们遍历上面定义的masks数组,并为每个mask创建一个新实例并传递mask参数。

Now the only thing left to do is the animation. When a click on a navigation bullet is captured, the function slideAllTo is called:

现在剩下要做的就是动画。 捕获对导航项目符号的单击后,将调用函数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);
    }
  }
}

As described in the comment, slideAllTo will loop through the instances and call the PrismSlider.prototype.slideTo method.

如评论中所述, slideAllTo将遍历实例并调用PrismSlider.prototype.slideTo方法。

So let's add this method to PrismSlider.js together with animate_ and ticker_ which are used to run the sliding:

因此,让我们将此方法与animate_ticker_一起添加到PrismSlider.js中,这些方法用于运行滑动:

/**
 * 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_();
};

In the above method the key steps are when we update the indexes and when we calculate how many slides we need to animate with indexOffset.

在上述方法中,关键步骤是何时更新索引以及何时计算需要使用indexOffset进行动画处理的幻灯片

The lasts two methods are animate which simply calculate the end time by adding the duration to Date.now(), and ticker, which is basically the method that we call with requestAnimationFrame.

的持续两种方法是有生命的,其简单地通过将所述持续时间Date.now(计算结束时间),以及断续器,它基本上是,我们称之为使用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));
};

结论 (Conclusion)

I hope you enjoyed this tutorial and find it useful!

我希望您喜欢本教程并发现它有用!

Please note that in a real life case it would be better to preload the images directly with JavaScript by extending the code presented above in order to accomplish a more dynamic optimization.

请注意,在现实生活中,最好通过扩展上面提供的代码直接用JavaScript预加载图像,以实现更动态的优化。

Download the full source and have a look at the files; everything is well documented and, I hope, pretty straightforward to understand and adapt to your needs.

下载完整的源代码并查看文件; 一切内容都有据可查,并且我希望它非常容易理解和适应您的需求。

翻译自: https://tympanus.net/codrops/2015/03/31/prism-effect-slider-canvas/

棱镜计划

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值