Обесцвечивание картинки под курсором
Область под курсором переводит в оттенки серого цветное изображение благодаря шейдерам
HTML
<div id="page-wrap">
<!-- will hold our WebGL context -->
<div id="canvas"></div>
<div id="fullwidth-image">
<img data-sampler="displacedImage" src="https://www.martin-laxenaire.fr/libs/curtainsjs/examples/ajax-navigation/images/plane-texture-1.jpg" />
</div>
<!-- shaders and lib -->
<script id="mouse-displacement-vs" type="x-shader/x-vertex">
#ifdef GL_ES
precision mediump float;
#endif
// default mandatory variables
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
// custom variables
varying vec3 vVertexPosition;
varying vec2 vTextureCoord;
varying vec2 vDistortionEffect;
// custom uniforms
uniform vec2 uMousePosition;
void main() {
vec3 vertexPosition = aVertexPosition;
gl_Position = uPMatrix * uMVMatrix * vec4(vertexPosition, 1.0);
// varyings
vTextureCoord = aTextureCoord;
vVertexPosition = vertexPosition;
vDistortionEffect = uMousePosition - vec2(vertexPosition.x, vertexPosition.y);
}
</script>
<script id="mouse-displacement-fs" type="x-shader/x-fragment">
#ifdef GL_ES
precision mediump float;
#endif
varying vec2 vTextureCoord;
varying vec2 vDistortionEffect;
// custom uniforms
uniform float uDisplacementStrength;
// our textures samplers
uniform sampler2D displacedImage;
uniform sampler2D canvasTexture;
void main( void ) {
// our texture coords
vec2 textureCoords = vTextureCoord;
// get our canvas texture
vec4 mouseEffect = texture2D(canvasTexture, textureCoords);
// apply displacement to the texture coords based on our canvas texture RGB
textureCoords = textureCoords + mouseEffect .r * vDistortionEffect * (uDisplacementStrength / 10.0);
// get our image texture
vec4 finalColor = texture2D(displacedImage, textureCoords);
// get a B&W version of our image texture
vec4 finalBW = vec4(1.0);
finalBW.rgb = vec3(finalColor.r * 0.3 + finalColor.g * 0.59 + finalColor.b * 0.11);
// mix both texture based on our canvas texture
finalColor = mix(finalColor, finalBW, mouseEffect.r);
// that's all folks!
gl_FragColor = finalColor;
}
</script>
CSS
@media screen {
html, body {
min-height: 100%;
}
body {
margin: 0;
font-size: 18px;
font-family: Verdana, sans-serif;
background: #ffffff;
line-height: 1.4;
}
#page-wrap {
width: 100%;
min-height: 100vh;
position: relative;
overflow: hidden;
}
/*** canvas ***/
#canvas {
position: absolute;
top: 0;
right: 0;
height: 100vh;
left: 0;
}
#fullwidth-image {
position: absolute;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
}
#fullwidth-image img, #fullwidth-image canvas {
display: none;
}
/* handling WebGL context errors */
.no-webgl #fullwidth-image {
display: flex;
justify-content: center;
align-items: center;
}
.no-webgl #fullwidth-image img {
display: block;
object-fit: cover;
min-width: 100%;
min-height: 100%;
}
}
JS
var params = {
webGLCanvasID: "canvas",
planeElementID: "fullwidth-image",
// TWEAK THOSE VALUES TO CHANGE OVERALL EFFECT
// size of the effect (0: no effect, 1: full window)
pointerSize: 0.25,
// how much to increase/decrease opacity on each frame
opacitySpeed: 0.0125,
// strength of the velocity of the mouse effect
velocityStrength: 0.25,
// the bigger the more displacement
displacementStrength: 0.25,
// does not change anything visually, but the smaller the scale the better the performance
canvasScale: 0.125,
};
// look at window.onload on line 315
// Constructor function
function MouseEffect(params) {
// Init the effect
this.init(params);
return this;
}
/*
* Init everything
*/
MouseEffect.prototype.init = function(params) {
this.curtains = new Curtains(params.webGLCanvasID);
this.plane = null;
// get our plane element
this.planeElement = document.getElementById(params.planeElementID);
this.pixelRatio = this.curtains.pixelRatio || 1;
// mouse positions history
this.mouse = {
position: {
x: 0,
y: 0,
},
attributes: [],
};
// params
this.params = {
pointerSize: params.pointerSize || 0.25,
opacitySpeed: params.opacitySpeed || 0.0125,
velocityStrength: params.velocityStrength || 0.25,
displacementStrength: params.displacementStrength || 0.25,
canvasScale: params.canvasScale || 0.125,
};
this.canvas = null;
this.canvasContext = null;
if(
!document.getElementById(params.webGLCanvasID) ||
!document.getElementById(params.planeElementID)
) {
console.warn("You must specify a valid ID for the WebGL canvas and the plane element");
return false;
}
}
/*
* Resize the mouse canvas
*/
MouseEffect.prototype.resize = function() {
if(this.canvas && this.canvasContext) {
this.canvas.width = this.planeElement.clientWidth * this.pixelRatio * this.params.canvasScale;
this.canvas.height = this.planeElement.clientHeight * this.pixelRatio * this.params.canvasScale;
this.canvasContext.width = this.planeElement.clientWidth * this.pixelRatio * this.params.canvasScale;
this.canvasContext.height = this.planeElement.clientHeight * this.pixelRatio * this.params.canvasScale;
this.canvasContext.scale(this.pixelRatio * 1 / this.params.canvasScale, this.pixelRatio * 1 / this.params.canvasScale);
//this.mouse.canvasContext.imageSmoothingEnabled = true;
}
}
/*
* Handle mouse/touch moves and push the positions into an array
*/
MouseEffect.prototype.handleMovement = function(e) {
this.mouse.position.x = e.clientX;
this.mouse.position.y = e.clientY;
// touch event
if(e.targetTouches) {
this.mouse.position.x = e.targetTouches[0].clientX;
this.mouse.position.y = e.targetTouches[0].clientY;
}
// always check that the plane is still here
if(this.planeElement && this.plane) {
var mouseAttributes = {
x: this.mouse.position.x * Math.pow(this.params.canvasScale, 2),
y: this.mouse.position.y * Math.pow(this.params.canvasScale, 2),
scale: 0.05,
opacity: 1,
velocity: {
x: 0,
y: 0,
},
}
// keep tracks of the initial position of the mouse to calculate velocity
mouseAttributes.initialPosition = {
x: mouseAttributes.x,
y: mouseAttributes.y
}
// handle velocity based on past values
if(this.mouse.attributes.length > 0) {
mouseAttributes.velocity = {
x: Math.max(-this.params.canvasScale * 1.25, Math.min(this.params.canvasScale * 1.25, mouseAttributes.initialPosition.x - this.mouse.attributes[this.mouse.attributes.length - 1].initialPosition.x)),
y: Math.max(-this.params.canvasScale * 1.25, Math.min(this.params.canvasScale * 1.25, mouseAttributes.initialPosition.y - this.mouse.attributes[this.mouse.attributes.length - 1].initialPosition.y)),
};
}
// if this is our first mouse move, start drawing again
if(this.mouse.attributes.length == 0) {
this.curtains.enableDrawing();
}
// push our coords to our mouse coords array
this.mouse.attributes.push(mouseAttributes);
// convert our mouse/touch position to coordinates relative to the vertices of the plane
var mouseCoords = this.plane.mouseToPlaneCoords(this.mouse.position.x, this.mouse.position.y);
// update our mouse position uniform
this.plane.uniforms.mousePosition.value = [mouseCoords.x, mouseCoords.y];
}
}
/*
* This draws a gradient circle based on mouse attributes positions
*/
MouseEffect.prototype.drawGradientCircle = function(pointerSize, circleAttributes) {
this.canvasContext.beginPath();
var gradient = this.canvasContext.createRadialGradient(
circleAttributes.x, circleAttributes.y, 0,
circleAttributes.x, circleAttributes.y, pointerSize * circleAttributes.scale * this.params.canvasScale
);
// our gradient could go from opaque white to transparent white or from opaque white to transparent black
// it changes the effect a bit
gradient.addColorStop(0, 'rgba(255, 255, 255, ' + circleAttributes.opacity + ')');
// use another gradient stop if we want to add more transparency
//gradient.addColorStop(0.85, 'rgba(255, 255, 255, 0.05)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
this.canvasContext.fillStyle = gradient;
this.canvasContext.arc(
circleAttributes.x, circleAttributes.y, pointerSize * circleAttributes.scale * this.params.canvasScale,
0, 2 * Math.PI, false
);
this.canvasContext.fill();
this.canvasContext.closePath();
}
/*
* Drawing onto our canvas
*/
MouseEffect.prototype.animateCanvas = function() {
// here we will handle our canvas texture animation
var pointerSize = window.innerWidth > window.innerHeight ?
Math.floor(this.canvas.height * this.params.pointerSize) :
Math.floor(this.canvas.width * this.params.pointerSize);
// clear scene
this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
// draw a background black rectangle
this.canvasContext.beginPath();
this.canvasContext.fillStyle = "black";
this.canvasContext.rect(0, 0, this.canvas.width, this.canvas.height);
this.canvasContext.fill();
this.canvasContext.closePath();
// draw all our mouse coords
for(var i = 0; i < this.mouse.attributes.length; i++) {
this.drawGradientCircle(pointerSize, this.mouse.attributes[i]);
}
}
/*
* Once the plane is ready we set the event listeners and handle the render loop
*/
MouseEffect.prototype.handlePlane = function() {
var self = this;
self.plane.onReady(function() {
// on resize, update the resolution uniform
window.addEventListener("resize", self.resize.bind(self), false);
document.body.addEventListener("mousemove", self.handleMovement.bind(self), false);
document.body.addEventListener("touchmove", self.handleMovement.bind(self), {
passive: true
});
// for performance purpose, disable the drawing for now
self.curtains.disableDrawing();
// render the first frame only to display the picture
self.curtains.needRender();
}).onRender(function() {
for(var i = 0; i < self.mouse.attributes.length; i++) {
// decrease opacity
self.mouse.attributes[i].opacity -= self.params.opacitySpeed;
// apply velocity
self.mouse.attributes[i].x += self.mouse.attributes[i].velocity.x * self.params.velocityStrength;
self.mouse.attributes[i].y += self.mouse.attributes[i].velocity.y * self.params.velocityStrength;
// change scale
if(self.mouse.attributes[i].opacity >= 0.5) {
self.mouse.attributes[i].scale += (self.params.opacitySpeed * 2);
}
else {
self.mouse.attributes[i].scale -= self.params.opacitySpeed;
}
if(self.mouse.attributes[i].opacity <= 0) {
// if element is fully transparent, remove it
self.mouse.attributes.splice(i, 1);
// if this was our last mouse move, disable drawing again
if(self.mouse.attributes.length == 0) {
self.curtains.disableDrawing();
}
}
}
// draw our mouse coords arrays
self.animateCanvas();
});
}
/*
* If you want to remove the plane cleanly (like if you're navigating away of this page)
*/
MouseEffect.prototype.removePlane = function() {
var self = this;
// remove all events
window.removeEventListener("resize", self.resize);
document.body.removeEventListener("mousemove", self.handleMovement);
document.body.removeEventListener("touchmove", self.handleMovement);
// remove the plane
self.curtains.removePlane(self.plane);
self.plane = null;
self.canvas = null;
self.canvasContext = null;
}
/*
* Adds the plane and starts the effect
*/
MouseEffect.prototype.addPlane = function() {
// parameters to apply to our WebGL plane
this.planeParams = {
vertexShaderID: "mouse-displacement-vs",
fragmentShaderID: "mouse-displacement-fs",
imageCover: true,
uniforms: {
mousePosition: {
name: "uMousePosition",
type: "2f",
value: [this.mouse.position.x, this.mouse.position.y],
},
mouseEffect: {
name: "uDisplacementStrength",
type: "1f",
value: this.params.displacementStrength,
},
},
};
// create our plane
this.plane = this.curtains.addPlane(this.planeElement, this.planeParams);
// if the plane was created successfully we can go on
if(this.plane) {
this.canvas = document.createElement("canvas");
this.canvas.setAttribute("data-sampler", "canvasTexture");
this.canvasContext = this.canvas.getContext("2d", { alpha: false });
// load our canvas texture
this.plane.loadCanvases([this.canvas]);
// first we resize our mouse canvas
this.resize();
// then we handle the plane
this.handlePlane();
}
}
window.onload = function() {
// init everything
var mouseEffect = new MouseEffect(params);
// if there's an error during the WebGL context creation
mouseEffect.curtains.onerror(function() {
document.body.classList.add("no-webgl");
});
// add the plane to start the effect
mouseEffect.addPlane();
}