2023年6月21日发(作者:)
pythongooey_使⽤制作Gooey图像悬停效果gooey’s grandson, WebGL has become more and more popular over the last few years with libraries like , or therecent . Those are very useful for easily creating a blank board where the only boundaries are your imagination. Wesee more and more, often subtle integration of WebGL in an interface for hover, scroll or reveal effects. Examples are thegallery of articles on or the effects seen on .Flash的孙⼦在过去⼏年中越来越流⾏,它具有,或最近的之类的库。 这些对于轻松创建空⽩板⾮常有⽤,其中只有您的想象⼒。 我们看到越来越多的WebGL通常会微妙地集成到界⾯中,以进⾏悬停,滚动或显⽰效果。 例如,的⽂章集或在看到的效果。In this tutorial, we’ll use to create a special gooey texture that we’ll use to reveal another image when hovering one. Headover to the demo to see the effect in action. For the demo itself, I’ve created a more practical example that shows avertical scrollable layout with images, where each one has a variation of the effect. You can click on an image and it willexpand to a larger version while some other content shows up (just a mock-up). We’ll go over the most interesting parts ofthe effect, so that you get an understanding of how it works and how to create your own.在本教程中,我们将使⽤创建特殊的粘稠纹理,将其⽤于在悬停时显⽰另⼀幅图像。 前往演⽰观看效果。 对于演⽰本⾝,我创建了⼀个更实际的⽰例,该⽰例显⽰了带有图像的垂直可滚动布局,其中每个图像都有不同的效果。 您可以单击图像,它将扩展为更⼤的版本,同时显⽰⼀些其他内容(只是⼀个模型)。 我们将介绍该效果中最有趣的部分,以便您了解其效果以及如何创建⾃⼰的效果。I’ll assume that you are comfortable with JavaScript and have some knowledge of and shader logic. If you’re not,have a look at the or , or .我假设您对JavaScript熟悉并且对和着⾊器逻辑有⼀些了解。 如果不是,请查看或 , 或 。Attention: This tutorial covers many parts; if you prefer, you can skip the HTML/CSS/JavaScript part and go directly go tothe 注意:本教程涵盖了许多部分。 如果愿意,可以跳过HTML / CSS / JavaScript部分,直接转到“ . 。Now that we are clear, let’s do this!现在我们很清楚了,让我们开始吧!在DOM中创建场景 (Create the scene in the DOM)Before we start making some magic, we are first going to mark up the images in the HTML. It will be easier to handleresizing our scene after we’ve set up the initial position and dimension in HTML/CSS rather than positioning everything inJavaScript. Moreover, the styling part should be only made with CSS, not JavaScript. For example, if our image has a ratioof 16:9 on desktop but a 4:3 ratio on mobile, we just want to handle this using CSS. JavaScript will only get the new valuesand do its stuff.在开始制作魔术之前,我们⾸先要标记HTML中的图像。 在HTML / CSS中设置了初始位置和尺⼨之后,⽐在JavaScript中放置所有内容更容易处理调整场景⼤⼩。 此外,样式部分应仅使⽤CSS⽽不是JavaScript制成。 例如,如果我们的图⽚在桌⾯上的⽐例为16:9,⽽在移动设备上的⽐例为4:3,我们只想使⽤CSS来处理。 JavaScript将仅获取新值并执⾏其⼯作。11//
8293031//
.container { display: flex; align-items: center; justify-content: center; width: 100%; height: 100vh; z-index: 10;}
.tile { width: 35vw; flex: 0 0 auto;}
.tile__image { width: 100%; height: 100%; object-fit: cover; object-position: center;}
canvas { position: fixed; left: 0; top: 0; width: 100%; height: 100vh; z-index: 9;}As you can see above, we have create a single image that is centered in the middle of our screen. Did you notice the data-src and data-hover attributes on the image? These will be our reference images and we’ll load both of these later in ourscript with lazy loading.如您在上⽅看到的,我们已经创建了⼀个位于屏幕中间居中的图像。 您是否注意到图像上的data-src和data-hover属性? 这些将是我们的参考图像,稍后我们将通过延迟加载在脚本中加载这两个图像。Don’t forget the canvas. We’ll stack it below our main section to draw the images in the exact same place as we haveplaced them before.不要忘记画布。 我们将其堆叠在主要部分的下⽅,以在与之前放置的位置完全相同的位置绘制图像。⽤JavaScript创建场景(Create the scene in JavaScript)Let’s get started with the less-easy-but-ok part! First, we’ll create the scene, the lights, and the renderer.让我们从不那么容易但还可以的部分开始吧! ⾸先,我们将创建场景,灯光和渲染器。1718192//
import * as THREE from 'three'
export default class Scene { constructor() { ner = mentById('stage')
= new () er = new enderer({ canvas: ner, alpha: true, })
e(idth, eight) elRatio(PixelRatio)
ghts() }
initLights() { const ambientlight = new tLight(0xffffff, 2) (ambientlight) }}This is a very basic scene. But we need one more essential thing in our scene: the camera. We have a choice between twotypes of cameras: orthographic or perspective. If we keep our image flat, we can use the first one. But for our rotationeffect, we want some perspective as we move the mouse around.这是⼀个⾮常基本的场景。 但是我们需要场景中的另⼀项重要内容:相机。 我们有两种类型的相机可供选择:正交相机或透视相机。 如果我们保持图像平坦,则可以使⽤第⼀个图像。 但是为了获得旋转效果,我们希望在移动⿏标时有⼀些透视图。In (and other libraries for WebGL) with a perspective camera, 10 unit values on our screen are not 10px. So thetrick here is to use some math to transform 1 unit to 1 pixel and change the perspective to increase or decrease thedistortion effect.在带有透视相机的(和其他⽤于WebGL的库)中,屏幕上的10个单位值不是10px。 因此,这⾥的技巧是使⽤⼀些数学运算将1单位转换为1像素,并更改视⾓以增加或减少失真效果。1234567891//
const perspective = 800
constructor() { // ... mera()}
initCamera() { const fov = (180 * (2 * (eight / 2 / perspective))) /
= new ctiveCamera(fov, idth / eight, 1, 1000) (0, 0, perspective)}We’ll set the perspective to 800 to have a not-so-strong distortion as we rotate the plane. The more we increase theperspective, the less we’ll perceive the distortion, and vice versa.我们将透视图设置为800,以便在旋转平⾯时不会产⽣太⼤的变形。 我们增加的视⾓越多,我们对扭曲的感知就越少,反之亦然。The last thing we need to do is to render our scene in each frame.我们需要做的最后⼀件事是在每个帧中渲染场景。1112//
constructor() { // ... ()}
update() { requestAnimationFrame((this))
(, )}If your screen is not black, you are on the right way!如果您的屏幕不是⿊⾊,则说明⽅法正确!⽤正确的尺⼨建造飞机 (Build the plane with the correct sizes)As we mentioned above, we have to retrieve some additional information from the image in the DOM like its dimension andposition on the page.如上所述,我们必须从DOM中的图像中检索⼀些其他信息,例如图像的尺⼨和在页⾯上的位置。12345678//
import Figure from './Figure'
constructor() { // ... = new Figure()}171819//
export default class Figure { constructor(scene) { this.$image = elector('.tile__image') = scene
= new eLoader()
= (this.$) mage = (this.$) = new 2(0, 0) = new 2(0, 0)
es()
Mesh() }}First, we create another class where we pass the scene as a property. We set two new vectors, dimension and offset, inwhich we’ll store the dimension and position of our DOM image.⾸先,我们创建另⼀个类,将场景作为属性传递给该类。 我们设置了两个新的向量,尺⼨和偏移,将在其中存储DOM图像的尺⼨和位置。Furthermore, we’ll use a TextureLoader to “load” our images and convert them into a texture. We need to do that as wewant to use these pictures in our shaders.此外,我们将使⽤TextureLoader来“加载”图像并将其转换为纹理。 我们需要这样做,因为我们想在着⾊器中使⽤这些图⽚。We need to create a method in our class to handle the loading of our images and wait for a callback. We could achieve thatwith an async function but for this tutorial, let’s keep it simple. Just keep in mind that you’ll probably need to refactorthis a bit for your own purposes.我们需要在类中创建⼀个⽅法来处理图像的加载并等待回调。 我们可以使⽤异步功能来实现这⼀点,但是对于本教程⽽⾔,我们让它保持简单。 请记住,您可能需要出于⾃⾝⽬的对它进⾏⼀些重构。//
// ... getSizes() { const { width, height, top, left } = this.$ndingClientRect()
(width, height) (left - idth / 2 + width / 2, -top + eight / 2 - height / 2) }// ...We get our image information in the getBoundingClientRect object. After that, we’ll pass these to our two variables. Theoffset is here to calculate the distance between the center of the screen and the object on the page.我们在getBoundingClientRect对象中获取图像信息。 之后,我们将它们传递给我们的两个变量。 这⾥的偏移量⽤于计算屏幕中⼼与页⾯上的对象之间的距离。17//
// ... createMesh() { ry = new ufferGeometry(1, 1, 1, 1) al = new sicMaterial({ map: })
= new (ry, al)
(.x, .y, 0) (.x, .y, 1)
() }// ...After that, we’ll set our values on the plane we’re building. As you can notice, we have created a plane of 1 on 1px with 1row and 1 column. As we don’t want to distort the plane, we don’t need a lot of faces or vertices. So let’s keep it simple.之后,我们将在正在构建的飞机上设置值。 如您所见,我们在1px上创建了⼀个平⾯,该平⾯上有1⾏1列。 因为我们不想使飞机变形,所以不需要很多⾯或顶点。 因此,让我们保持简单。But why scale it while we can set the size directly? Glad you asked.但是为什么要缩放它,⽽我们却可以直接设置⼤⼩呢? 很⾼兴你问。Because of the resizing part. If we want to change the size of our mesh afterwards, there is no other proper way than thisone. While it’s easier to change the scale of the mesh, it’s not for the dimension.由于调整⼤⼩的⼀部分。 如果我们之后要更改⽹格的⼤⼩,则没有其他适当的⽅法。 虽然更容易更改⽹格的⽐例,但不适⽤于尺⼨。For the moment, we set a MeshBasicMaterial, just to see if everything is fine.⽬前,我们设置了MeshBasicMaterial,只是看⼀切是否正常。获取⿏标坐标 (Get mouse coordinates)Now that we have built our scene with our mesh, we want to get our mouse coordinates and, to keep things easy, we’llnormalize them. Why normalize? Because of the coordinate system in shaders.现在,我们已经使⽤⽹格构建了场景,我们想要获取⿏标坐标,并且为了使事情变得简单,我们将其标准化。 为什么要归⼀化? 由于着⾊器中的坐标系。As you can see in the figure above, we have normalized the values for both of our shaders. So to keep things simple, we’llprepare our mouse coordinate to match the vertex shader coordinate.如上图所⽰,我们已经将两个着⾊器的值标准化了。 因此,为了简单起见,我们将准备⿏标坐标以匹配顶点着⾊器坐标。If you’re lost at this point, I recommend you to read the and the respective part of . Both have good advice and a lot ofexamples to help understand what’s going on.如果您此时迷失了⽅向,建议您阅读和的相应部分。 两者都有很好的建议,并提供了许多⽰例来帮助您了解正在发⽣的事情。17181920//
// ...
= new 2(0, 0)ntListener('mousemove', (ev) => { eMove(ev) })
// ...
onMouseMove(event) { (, 0.5, { x: (X / idth) * 2 - 1, y: -(Y / eight) * 2 + 1, })
(on, 0.5, { x: -.y * 0.3, y: .x * ( / 6) })}For the tween parts, I’m going to use . This is the best library ever. EVER. And it’s perfect for our purpose. We don’tneed to handle the transition between two states, TweenMax will do it for us. Each time we move our mouse, TweenMax willupdate the position and the rotation smoothly.对于补间部分,我将使⽤ 。 这是有史以来最好的图书馆。 永远这对于我们的⽬标⽽⾔是完美的。 我们不需要处理两个状态之间的转换,TweenMax会为我们完成。 每次移动⿏标,TweenMax都会平滑更新位置和旋转。One last thing before we continue: we’ll update our material from MeshBasicMaterial to ShaderMaterial and pass somevalues (uniforms), the device pixel ratio and shaders.继续之前的最后⼀件事:我们将材质从MeshBasicMaterial更新为ShaderMaterial,并传递⼀些值(均匀值),设备像素⽐率和着⾊器。22324//
// ...
ms = { u_image: { type: 't', value: }, u_imagehover: { type: 't', value: }, u_mouse: { value: }, u_time: { value: 0 }, u_res: { value: new 2(idth, eight) }}
al = new Material({ uniforms: ms, vertexShader: vertexShader, fragmentShader: fragmentShader, defines: { PR: d(1) }})
update() { ms.u_ += 0.01}We passed our two textures, the mouse position, the size of our screen and a variable called u_time which we will incrementeach frame.我们传递了两个纹理,即⿏标位置,屏幕⼤⼩和⼀个名为u_time的变量,该变量将使每帧递增。But keep in mind that it’s not the best way to do that. For example, we only need to increment when we are hovering thefigure, not every frame. I’m not going into details, but performance-wise, it’s better to just update our shader only whenwe need it.但是请记住,这不是最好的⽅法。 例如,当我们将⿏标悬停在图形上时,我们只需要增加,⽽不必在每⼀帧上增加。 我不讨论细节,⽽是考虑性能,最好仅在需要时更新着⾊器。技巧背后的逻辑及如何使⽤噪⾳ (The logic behind the trick & how to use noise)Still here? Nice! Time for some magic tricks.还在? 真好! 是时候使⽤⼀些魔术了。I will not explain what noise is and where it comes from. If you’re interested, be sure to read from The Book of ’s well explained.我不会解释什么是噪声以及噪声的来源。 如果您有兴趣,请务必阅读《 The Shader of Shaders》中的。 很好解释。Long story short, Noise is a function that gives us a value between -1 and 1 based on values we pass through. It will outputa random pattern but more organic.长话短说,Noise是⼀个函数,它根据传递的值为我们提供介于-1和1之间的值。 它将输出随机模式,但更加有机。Thanks to noise, we can generate a lot of different shapes, like maps, random patterns, etc.多亏了噪⾳,我们才能⽣成许多不同的形状,例如地图,随机图案等。Let’s start with a 2D noise result. Just by passing the coordinate of our texture, we’ll have something like a cloud texture.让我们从2D噪声结果开始。 仅通过传递纹理的坐标,我们就可以得到类似云的纹理。But there are several kinds of noise functions. Let’s use a 3D noise by giving one more parameter like … the time? Thenoise pattern will evolve and change over time. By changing the frequency and the amplitude, we can give some movementand increase the contrast.但是,有⼏种噪声函数。 让我们使⽤3D噪声,例如再给⼀个参数,例如...时间? 噪声模式将随着时间的流逝⽽变化。 通过更改频率和幅度,我们可以进⾏⼀些移动并增加对⽐度。It will be our first base.这将是我们的第⼀基地。Second, we’ll create a circle. It’s quite easy to build a simple shape like a circle in the fragment shader. We just take thefunction from : Shapes to create a blurred circle, increase the contrast and voilà!其次,我们将创建⼀个圆。 在⽚段着⾊器中构建像圆形这样的简单形状⾮常容易。 我们只是采⽤了《 :形状的功能来创建⼀个模糊的圆圈,增加对⽐度和外观!Last, we add these two together, play with some variables, cut a “slice” of this and tadaaa:最后,我们将这两个加在⼀起,使⽤⼀些变量,将其与tadaaa进⾏“切⽚”:We finally mix our textures together based on this result and here we are, easy peasy lemon squeezy!最后,根据此结果,我们将纹理混合在⼀起,在这⾥,轻松榨柠檬!Let’s dive into the code.让我们深⼊研究代码。着⾊器 (Shaders)We won’t really need the vertex shader here so this is our code:我们在这⾥实际上并不需要顶点着⾊器,所以这是我们的代码:12345678// rying vec2 v_uv;
void main() { v_uv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);}ShaderMaterial from provides some when you’re a beginner:当您是初学者时,的ShaderMaterial提供了⼀些:position (vec3): the coordinates of each vertex of our mesh位置(vec3):⽹格的每个顶点的坐标uv (vec2): the coordinates of our textureuv (vec2):纹理的坐标normals (vec3): normal of each vertex our mesh have.法线(vec3):⽹格物体每个顶点的法线。Here we’re just passing the UV coordinates from the vertex shader to fragment shader.在这⾥,我们只是将UV坐标从顶点着⾊器传递到⽚段着⾊器。创建圈⼦ (Create the circle)Let’s use the function from to build our circle and add a variable to handle the blurriness of our edges.让我们使⽤的函数来构建圆并添加⼀个变量来处理边缘的模糊性。Moreover, we’ll add the mouse position to the origin of our circle. This way, the circle will be moving as long as we moveour mouse over our image.此外,我们将⿏标位置添加到圆的原点。 这样,只要我们将⿏标移到图像上,圆就会⼀直移动。1718192627// iform vec2 u_mouse;uniform vec2 u_res;
float circle(in vec2 _st, in float _radius, in float blurriness){ vec2 dist = _st; return 1.-smoothstep(_radius-(_radius*blurriness), _radius+(_radius*blurriness), dot(dist,dist)*4.0);}
void main() { // We manage the device ratio by passing PR constant vec2 res = u_res * PR; vec2 st = gl_ / - vec2(0.5); // tip: use the following formula to keep the good ratio of your coordinates st.y *= u_res.y / u_res.x;
// We readjust the mouse coordinates vec2 mouse = u_mouse * -0.5; // tip2: do the same for your mouse mouse.y *= u_res.y / u_res.x; mouse *= -1.;
vec2 circlePos = st + mouse; float c = circle(circlePos, .03, 2.);
gl_FragColor = vec4(vec3(c), 1.);}弄些东西(Make some noooooise)As we saw above, the noise function has several parameters and gives us a smooth cloudy pattern. How could we havethat? Glad you asked.正如我们在上⾯看到的,噪声函数具有多个参数,并为我们提供了平滑的浑浊模式。 我们怎么有呢? 很⾼兴你问。For this part, I’m using and , and two npm packages to include other functions. It keeps our shader a little bit morereadable and avoids having a lot of displayed functions that we will not use after all.对于这⼀部分,我将使⽤和以及两个npm软件包来包含其他功能。 它使我们的着⾊器更具可读性,并避免了很多我们根本不会使⽤的显⽰功能。// #pragma glslify: snoise2 = require('glsl-noise/simplex/2d')
//...
varying vec2 v_uv;
uniform float u_time;
void main() { // ...
float n = snoise2(vec2(v_uv.x, v_uv.y));
gl_FragColor = vec4(vec3(n), 1.);}By changing the amplitude and the frequency of our noise (exactly like the sin/cos functions), we can change the render.通过更改噪声的幅度和频率(完全类似于sin / cos函数),我们可以更改渲染。123456//
float offx = v_uv.x + sin(v_uv.y + u_time * .1);float offy = v_uv.y - u_time * 0.1 - cos(u_time * .001) * .01;
float n = snoise2(vec2(offx, offy) * 5.) * 1.;But it isn’t evolving through time! It is distorted but that’s it. We want more. So we will use noise3d instead and pass a3rd parameter: the time.但这并没有随着时间的推移⽽发展! 它失真了,仅此⽽已。 我们想要更多。 因此,我们将改为使⽤noise3d并传递第三个参数:时间。float n = snoise3(vec3(offx, offy, u_time * .1) * 4.) * .5;As you can see, I changed the amplitude and the frequency to have the render I desire.如您所见,我更改了幅度和频率以得到所需的渲染。Alright, let’s add them together!好吧,让我们将它们加在⼀起!合并两个纹理 (Merging both textures)By just adding these together, we’ll already see an interesting shape changing through time.通过将它们加在⼀起,我们已经可以看到有趣的形状随时间变化。To explain what’s happening, let’s imagine our noise is like a sea floating between -1 and 1. But our screen can’t displaynegative color or pixels more than 1 (pure white) so we are just seeing the values between 0 and 1.为了解释正在发⽣的事情,让我们想象⼀下我们的噪⾳就像是在-1和1之间漂浮的⼤海。但是我们的屏幕⽆法显⽰负⾊或像素⼤于1(纯⽩⾊)的像素,因此我们只能看到0到1之间的值。And our circle is like a flan.我们的圈⼦就像果馅饼。By adding these two shapes together it will give this very approximative result:通过将这两个形状加在⼀起,将得到⾮常近似的结果:Our very white pixels are only pixels outside the visible spectrum.我们⾮常⽩的像素只是可见光谱之外的像素。If we scale down our noise and subtract a small number, it will be completely moving down your waves until it disappearsabove the surface of the ocean of visible colors.如果我们按⽐例缩⼩噪声并减去⼀⼩部分,它将完全沿您的波浪向下移动,直到它消失在⽔⾯之上。 海洋可见的颜⾊。float n = snoise(vec3(offx, offy, u_time * .1) * 4.) - 1.;Our circle is still there but not enough visible to be displayed. If we multiply its value, it will be more contrasted.我们的圈⼦仍然存在,但可见度不⾜以显⽰。 如果我们乘以它的值,它将形成更⼤的对⽐。float c = circle(circlePos, 0.3, 0.3) * 2.5;We are almost there! But as you can see, there are still some details missing. And our edges aren’t sharp at all.我们就快到了! 但是正如您所看到的,仍然缺少⼀些细节。 ⽽且我们的边缘根本不锋利。To avoid that, we’ll use the .为了避免这种情况,我们将使⽤。123float finalMask = smoothstep(0.4, 0.5, n + c);
gl_FragColor = vec4(vec3(finalMask), 1.);Thanks to this function, we’ll cut a slice of our pattern between 0.4 et 0.5, for example. The shorter the space is betweenthese values, the sharper the edges are.例如,借助此功能,我们将在0.4到0.5之间切出⼀部分图案。 这些值之间的间隔越短,边缘越锐利。Finally, we can mix our two textures to use them as a mask.最后,我们可以混合两个纹理以将它们⽤作遮罩。11uniform sampler2D u_image;uniform sampler2D u_imagehover;
// ...
vec4 image = texture2D(u_image, uv);vec4 hover = texture2D(u_imagehover, uv);
vec4 finalImage = mix(image, hover, finalMask);
gl_FragColor = finalImage;We can change a few variables to have a more gooey effect:我们可以更改⼀些变量以产⽣更⼤的粘性:123456789// ...
float c = circle(circlePos, 0.3, 2.) * 2.5;
float n = snoise3(vec3(offx, offy, u_time * .1) * 8.) - 1.;
float finalMask = smoothstep(0.4, 0.5, n + pow(c, 2.));
// ...And voilà!和瞧!Check out the full or take a look at the live demo.查看完整的源代码或观看现场演⽰。麦克风下降 (Mic drop)Congratulations to those who came this far. I haven’t planned to explain this much. This isn’t perfect and I might havemissed some details but I hope you’ve enjoyed this tutorial anyway. Don’t hesitate to play with variables, try other noisefunctions and try to implement other effects using the mouse direction or play with the scroll!恭喜那些来到这⾥的⼈。 我没有计划对此做太多解释。 这并不完美,我可能已经错过了⼀些细节,但是我希望您仍然喜欢本教程。 不要犹豫,使⽤变量,尝试其他噪声函数,并尝试使⽤⿏标⽅向或滚动来实现其他效果!If you have any questions, let me know in the comments section! I also encourage you to download the demo, it’s a little bitmore complex and shows the effects in action with hover and click effects ¯_(?)_/¯如有任何疑问,请在评论部分让我知道! 我也⿎励您下载该演⽰,它稍微复杂⼀点,并通过悬停和单击效果来展⽰实际效果。 _(?)_ /参考和鸣谢 (References and Credits)python gooey
2023年6月21日发(作者:)
pythongooey_使⽤制作Gooey图像悬停效果gooey’s grandson, WebGL has become more and more popular over the last few years with libraries like , or therecent . Those are very useful for easily creating a blank board where the only boundaries are your imagination. Wesee more and more, often subtle integration of WebGL in an interface for hover, scroll or reveal effects. Examples are thegallery of articles on or the effects seen on .Flash的孙⼦在过去⼏年中越来越流⾏,它具有,或最近的之类的库。 这些对于轻松创建空⽩板⾮常有⽤,其中只有您的想象⼒。 我们看到越来越多的WebGL通常会微妙地集成到界⾯中,以进⾏悬停,滚动或显⽰效果。 例如,的⽂章集或在看到的效果。In this tutorial, we’ll use to create a special gooey texture that we’ll use to reveal another image when hovering one. Headover to the demo to see the effect in action. For the demo itself, I’ve created a more practical example that shows avertical scrollable layout with images, where each one has a variation of the effect. You can click on an image and it willexpand to a larger version while some other content shows up (just a mock-up). We’ll go over the most interesting parts ofthe effect, so that you get an understanding of how it works and how to create your own.在本教程中,我们将使⽤创建特殊的粘稠纹理,将其⽤于在悬停时显⽰另⼀幅图像。 前往演⽰观看效果。 对于演⽰本⾝,我创建了⼀个更实际的⽰例,该⽰例显⽰了带有图像的垂直可滚动布局,其中每个图像都有不同的效果。 您可以单击图像,它将扩展为更⼤的版本,同时显⽰⼀些其他内容(只是⼀个模型)。 我们将介绍该效果中最有趣的部分,以便您了解其效果以及如何创建⾃⼰的效果。I’ll assume that you are comfortable with JavaScript and have some knowledge of and shader logic. If you’re not,have a look at the or , or .我假设您对JavaScript熟悉并且对和着⾊器逻辑有⼀些了解。 如果不是,请查看或 , 或 。Attention: This tutorial covers many parts; if you prefer, you can skip the HTML/CSS/JavaScript part and go directly go tothe 注意:本教程涵盖了许多部分。 如果愿意,可以跳过HTML / CSS / JavaScript部分,直接转到“ . 。Now that we are clear, let’s do this!现在我们很清楚了,让我们开始吧!在DOM中创建场景 (Create the scene in the DOM)Before we start making some magic, we are first going to mark up the images in the HTML. It will be easier to handleresizing our scene after we’ve set up the initial position and dimension in HTML/CSS rather than positioning everything inJavaScript. Moreover, the styling part should be only made with CSS, not JavaScript. For example, if our image has a ratioof 16:9 on desktop but a 4:3 ratio on mobile, we just want to handle this using CSS. JavaScript will only get the new valuesand do its stuff.在开始制作魔术之前,我们⾸先要标记HTML中的图像。 在HTML / CSS中设置了初始位置和尺⼨之后,⽐在JavaScript中放置所有内容更容易处理调整场景⼤⼩。 此外,样式部分应仅使⽤CSS⽽不是JavaScript制成。 例如,如果我们的图⽚在桌⾯上的⽐例为16:9,⽽在移动设备上的⽐例为4:3,我们只想使⽤CSS来处理。 JavaScript将仅获取新值并执⾏其⼯作。11//
8293031//
.container { display: flex; align-items: center; justify-content: center; width: 100%; height: 100vh; z-index: 10;}
.tile { width: 35vw; flex: 0 0 auto;}
.tile__image { width: 100%; height: 100%; object-fit: cover; object-position: center;}
canvas { position: fixed; left: 0; top: 0; width: 100%; height: 100vh; z-index: 9;}As you can see above, we have create a single image that is centered in the middle of our screen. Did you notice the data-src and data-hover attributes on the image? These will be our reference images and we’ll load both of these later in ourscript with lazy loading.如您在上⽅看到的,我们已经创建了⼀个位于屏幕中间居中的图像。 您是否注意到图像上的data-src和data-hover属性? 这些将是我们的参考图像,稍后我们将通过延迟加载在脚本中加载这两个图像。Don’t forget the canvas. We’ll stack it below our main section to draw the images in the exact same place as we haveplaced them before.不要忘记画布。 我们将其堆叠在主要部分的下⽅,以在与之前放置的位置完全相同的位置绘制图像。⽤JavaScript创建场景(Create the scene in JavaScript)Let’s get started with the less-easy-but-ok part! First, we’ll create the scene, the lights, and the renderer.让我们从不那么容易但还可以的部分开始吧! ⾸先,我们将创建场景,灯光和渲染器。1718192//
import * as THREE from 'three'
export default class Scene { constructor() { ner = mentById('stage')
= new () er = new enderer({ canvas: ner, alpha: true, })
e(idth, eight) elRatio(PixelRatio)
ghts() }
initLights() { const ambientlight = new tLight(0xffffff, 2) (ambientlight) }}This is a very basic scene. But we need one more essential thing in our scene: the camera. We have a choice between twotypes of cameras: orthographic or perspective. If we keep our image flat, we can use the first one. But for our rotationeffect, we want some perspective as we move the mouse around.这是⼀个⾮常基本的场景。 但是我们需要场景中的另⼀项重要内容:相机。 我们有两种类型的相机可供选择:正交相机或透视相机。 如果我们保持图像平坦,则可以使⽤第⼀个图像。 但是为了获得旋转效果,我们希望在移动⿏标时有⼀些透视图。In (and other libraries for WebGL) with a perspective camera, 10 unit values on our screen are not 10px. So thetrick here is to use some math to transform 1 unit to 1 pixel and change the perspective to increase or decrease thedistortion effect.在带有透视相机的(和其他⽤于WebGL的库)中,屏幕上的10个单位值不是10px。 因此,这⾥的技巧是使⽤⼀些数学运算将1单位转换为1像素,并更改视⾓以增加或减少失真效果。1234567891//
const perspective = 800
constructor() { // ... mera()}
initCamera() { const fov = (180 * (2 * (eight / 2 / perspective))) /
= new ctiveCamera(fov, idth / eight, 1, 1000) (0, 0, perspective)}We’ll set the perspective to 800 to have a not-so-strong distortion as we rotate the plane. The more we increase theperspective, the less we’ll perceive the distortion, and vice versa.我们将透视图设置为800,以便在旋转平⾯时不会产⽣太⼤的变形。 我们增加的视⾓越多,我们对扭曲的感知就越少,反之亦然。The last thing we need to do is to render our scene in each frame.我们需要做的最后⼀件事是在每个帧中渲染场景。1112//
constructor() { // ... ()}
update() { requestAnimationFrame((this))
(, )}If your screen is not black, you are on the right way!如果您的屏幕不是⿊⾊,则说明⽅法正确!⽤正确的尺⼨建造飞机 (Build the plane with the correct sizes)As we mentioned above, we have to retrieve some additional information from the image in the DOM like its dimension andposition on the page.如上所述,我们必须从DOM中的图像中检索⼀些其他信息,例如图像的尺⼨和在页⾯上的位置。12345678//
import Figure from './Figure'
constructor() { // ... = new Figure()}171819//
export default class Figure { constructor(scene) { this.$image = elector('.tile__image') = scene
= new eLoader()
= (this.$) mage = (this.$) = new 2(0, 0) = new 2(0, 0)
es()
Mesh() }}First, we create another class where we pass the scene as a property. We set two new vectors, dimension and offset, inwhich we’ll store the dimension and position of our DOM image.⾸先,我们创建另⼀个类,将场景作为属性传递给该类。 我们设置了两个新的向量,尺⼨和偏移,将在其中存储DOM图像的尺⼨和位置。Furthermore, we’ll use a TextureLoader to “load” our images and convert them into a texture. We need to do that as wewant to use these pictures in our shaders.此外,我们将使⽤TextureLoader来“加载”图像并将其转换为纹理。 我们需要这样做,因为我们想在着⾊器中使⽤这些图⽚。We need to create a method in our class to handle the loading of our images and wait for a callback. We could achieve thatwith an async function but for this tutorial, let’s keep it simple. Just keep in mind that you’ll probably need to refactorthis a bit for your own purposes.我们需要在类中创建⼀个⽅法来处理图像的加载并等待回调。 我们可以使⽤异步功能来实现这⼀点,但是对于本教程⽽⾔,我们让它保持简单。 请记住,您可能需要出于⾃⾝⽬的对它进⾏⼀些重构。//
// ... getSizes() { const { width, height, top, left } = this.$ndingClientRect()
(width, height) (left - idth / 2 + width / 2, -top + eight / 2 - height / 2) }// ...We get our image information in the getBoundingClientRect object. After that, we’ll pass these to our two variables. Theoffset is here to calculate the distance between the center of the screen and the object on the page.我们在getBoundingClientRect对象中获取图像信息。 之后,我们将它们传递给我们的两个变量。 这⾥的偏移量⽤于计算屏幕中⼼与页⾯上的对象之间的距离。17//
// ... createMesh() { ry = new ufferGeometry(1, 1, 1, 1) al = new sicMaterial({ map: })
= new (ry, al)
(.x, .y, 0) (.x, .y, 1)
() }// ...After that, we’ll set our values on the plane we’re building. As you can notice, we have created a plane of 1 on 1px with 1row and 1 column. As we don’t want to distort the plane, we don’t need a lot of faces or vertices. So let’s keep it simple.之后,我们将在正在构建的飞机上设置值。 如您所见,我们在1px上创建了⼀个平⾯,该平⾯上有1⾏1列。 因为我们不想使飞机变形,所以不需要很多⾯或顶点。 因此,让我们保持简单。But why scale it while we can set the size directly? Glad you asked.但是为什么要缩放它,⽽我们却可以直接设置⼤⼩呢? 很⾼兴你问。Because of the resizing part. If we want to change the size of our mesh afterwards, there is no other proper way than thisone. While it’s easier to change the scale of the mesh, it’s not for the dimension.由于调整⼤⼩的⼀部分。 如果我们之后要更改⽹格的⼤⼩,则没有其他适当的⽅法。 虽然更容易更改⽹格的⽐例,但不适⽤于尺⼨。For the moment, we set a MeshBasicMaterial, just to see if everything is fine.⽬前,我们设置了MeshBasicMaterial,只是看⼀切是否正常。获取⿏标坐标 (Get mouse coordinates)Now that we have built our scene with our mesh, we want to get our mouse coordinates and, to keep things easy, we’llnormalize them. Why normalize? Because of the coordinate system in shaders.现在,我们已经使⽤⽹格构建了场景,我们想要获取⿏标坐标,并且为了使事情变得简单,我们将其标准化。 为什么要归⼀化? 由于着⾊器中的坐标系。As you can see in the figure above, we have normalized the values for both of our shaders. So to keep things simple, we’llprepare our mouse coordinate to match the vertex shader coordinate.如上图所⽰,我们已经将两个着⾊器的值标准化了。 因此,为了简单起见,我们将准备⿏标坐标以匹配顶点着⾊器坐标。If you’re lost at this point, I recommend you to read the and the respective part of . Both have good advice and a lot ofexamples to help understand what’s going on.如果您此时迷失了⽅向,建议您阅读和的相应部分。 两者都有很好的建议,并提供了许多⽰例来帮助您了解正在发⽣的事情。17181920//
// ...
= new 2(0, 0)ntListener('mousemove', (ev) => { eMove(ev) })
// ...
onMouseMove(event) { (, 0.5, { x: (X / idth) * 2 - 1, y: -(Y / eight) * 2 + 1, })
(on, 0.5, { x: -.y * 0.3, y: .x * ( / 6) })}For the tween parts, I’m going to use . This is the best library ever. EVER. And it’s perfect for our purpose. We don’tneed to handle the transition between two states, TweenMax will do it for us. Each time we move our mouse, TweenMax willupdate the position and the rotation smoothly.对于补间部分,我将使⽤ 。 这是有史以来最好的图书馆。 永远这对于我们的⽬标⽽⾔是完美的。 我们不需要处理两个状态之间的转换,TweenMax会为我们完成。 每次移动⿏标,TweenMax都会平滑更新位置和旋转。One last thing before we continue: we’ll update our material from MeshBasicMaterial to ShaderMaterial and pass somevalues (uniforms), the device pixel ratio and shaders.继续之前的最后⼀件事:我们将材质从MeshBasicMaterial更新为ShaderMaterial,并传递⼀些值(均匀值),设备像素⽐率和着⾊器。22324//
// ...
ms = { u_image: { type: 't', value: }, u_imagehover: { type: 't', value: }, u_mouse: { value: }, u_time: { value: 0 }, u_res: { value: new 2(idth, eight) }}
al = new Material({ uniforms: ms, vertexShader: vertexShader, fragmentShader: fragmentShader, defines: { PR: d(1) }})
update() { ms.u_ += 0.01}We passed our two textures, the mouse position, the size of our screen and a variable called u_time which we will incrementeach frame.我们传递了两个纹理,即⿏标位置,屏幕⼤⼩和⼀个名为u_time的变量,该变量将使每帧递增。But keep in mind that it’s not the best way to do that. For example, we only need to increment when we are hovering thefigure, not every frame. I’m not going into details, but performance-wise, it’s better to just update our shader only whenwe need it.但是请记住,这不是最好的⽅法。 例如,当我们将⿏标悬停在图形上时,我们只需要增加,⽽不必在每⼀帧上增加。 我不讨论细节,⽽是考虑性能,最好仅在需要时更新着⾊器。技巧背后的逻辑及如何使⽤噪⾳ (The logic behind the trick & how to use noise)Still here? Nice! Time for some magic tricks.还在? 真好! 是时候使⽤⼀些魔术了。I will not explain what noise is and where it comes from. If you’re interested, be sure to read from The Book of ’s well explained.我不会解释什么是噪声以及噪声的来源。 如果您有兴趣,请务必阅读《 The Shader of Shaders》中的。 很好解释。Long story short, Noise is a function that gives us a value between -1 and 1 based on values we pass through. It will outputa random pattern but more organic.长话短说,Noise是⼀个函数,它根据传递的值为我们提供介于-1和1之间的值。 它将输出随机模式,但更加有机。Thanks to noise, we can generate a lot of different shapes, like maps, random patterns, etc.多亏了噪⾳,我们才能⽣成许多不同的形状,例如地图,随机图案等。Let’s start with a 2D noise result. Just by passing the coordinate of our texture, we’ll have something like a cloud texture.让我们从2D噪声结果开始。 仅通过传递纹理的坐标,我们就可以得到类似云的纹理。But there are several kinds of noise functions. Let’s use a 3D noise by giving one more parameter like … the time? Thenoise pattern will evolve and change over time. By changing the frequency and the amplitude, we can give some movementand increase the contrast.但是,有⼏种噪声函数。 让我们使⽤3D噪声,例如再给⼀个参数,例如...时间? 噪声模式将随着时间的流逝⽽变化。 通过更改频率和幅度,我们可以进⾏⼀些移动并增加对⽐度。It will be our first base.这将是我们的第⼀基地。Second, we’ll create a circle. It’s quite easy to build a simple shape like a circle in the fragment shader. We just take thefunction from : Shapes to create a blurred circle, increase the contrast and voilà!其次,我们将创建⼀个圆。 在⽚段着⾊器中构建像圆形这样的简单形状⾮常容易。 我们只是采⽤了《 :形状的功能来创建⼀个模糊的圆圈,增加对⽐度和外观!Last, we add these two together, play with some variables, cut a “slice” of this and tadaaa:最后,我们将这两个加在⼀起,使⽤⼀些变量,将其与tadaaa进⾏“切⽚”:We finally mix our textures together based on this result and here we are, easy peasy lemon squeezy!最后,根据此结果,我们将纹理混合在⼀起,在这⾥,轻松榨柠檬!Let’s dive into the code.让我们深⼊研究代码。着⾊器 (Shaders)We won’t really need the vertex shader here so this is our code:我们在这⾥实际上并不需要顶点着⾊器,所以这是我们的代码:12345678// rying vec2 v_uv;
void main() { v_uv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);}ShaderMaterial from provides some when you’re a beginner:当您是初学者时,的ShaderMaterial提供了⼀些:position (vec3): the coordinates of each vertex of our mesh位置(vec3):⽹格的每个顶点的坐标uv (vec2): the coordinates of our textureuv (vec2):纹理的坐标normals (vec3): normal of each vertex our mesh have.法线(vec3):⽹格物体每个顶点的法线。Here we’re just passing the UV coordinates from the vertex shader to fragment shader.在这⾥,我们只是将UV坐标从顶点着⾊器传递到⽚段着⾊器。创建圈⼦ (Create the circle)Let’s use the function from to build our circle and add a variable to handle the blurriness of our edges.让我们使⽤的函数来构建圆并添加⼀个变量来处理边缘的模糊性。Moreover, we’ll add the mouse position to the origin of our circle. This way, the circle will be moving as long as we moveour mouse over our image.此外,我们将⿏标位置添加到圆的原点。 这样,只要我们将⿏标移到图像上,圆就会⼀直移动。1718192627// iform vec2 u_mouse;uniform vec2 u_res;
float circle(in vec2 _st, in float _radius, in float blurriness){ vec2 dist = _st; return 1.-smoothstep(_radius-(_radius*blurriness), _radius+(_radius*blurriness), dot(dist,dist)*4.0);}
void main() { // We manage the device ratio by passing PR constant vec2 res = u_res * PR; vec2 st = gl_ / - vec2(0.5); // tip: use the following formula to keep the good ratio of your coordinates st.y *= u_res.y / u_res.x;
// We readjust the mouse coordinates vec2 mouse = u_mouse * -0.5; // tip2: do the same for your mouse mouse.y *= u_res.y / u_res.x; mouse *= -1.;
vec2 circlePos = st + mouse; float c = circle(circlePos, .03, 2.);
gl_FragColor = vec4(vec3(c), 1.);}弄些东西(Make some noooooise)As we saw above, the noise function has several parameters and gives us a smooth cloudy pattern. How could we havethat? Glad you asked.正如我们在上⾯看到的,噪声函数具有多个参数,并为我们提供了平滑的浑浊模式。 我们怎么有呢? 很⾼兴你问。For this part, I’m using and , and two npm packages to include other functions. It keeps our shader a little bit morereadable and avoids having a lot of displayed functions that we will not use after all.对于这⼀部分,我将使⽤和以及两个npm软件包来包含其他功能。 它使我们的着⾊器更具可读性,并避免了很多我们根本不会使⽤的显⽰功能。// #pragma glslify: snoise2 = require('glsl-noise/simplex/2d')
//...
varying vec2 v_uv;
uniform float u_time;
void main() { // ...
float n = snoise2(vec2(v_uv.x, v_uv.y));
gl_FragColor = vec4(vec3(n), 1.);}By changing the amplitude and the frequency of our noise (exactly like the sin/cos functions), we can change the render.通过更改噪声的幅度和频率(完全类似于sin / cos函数),我们可以更改渲染。123456//
float offx = v_uv.x + sin(v_uv.y + u_time * .1);float offy = v_uv.y - u_time * 0.1 - cos(u_time * .001) * .01;
float n = snoise2(vec2(offx, offy) * 5.) * 1.;But it isn’t evolving through time! It is distorted but that’s it. We want more. So we will use noise3d instead and pass a3rd parameter: the time.但这并没有随着时间的推移⽽发展! 它失真了,仅此⽽已。 我们想要更多。 因此,我们将改为使⽤noise3d并传递第三个参数:时间。float n = snoise3(vec3(offx, offy, u_time * .1) * 4.) * .5;As you can see, I changed the amplitude and the frequency to have the render I desire.如您所见,我更改了幅度和频率以得到所需的渲染。Alright, let’s add them together!好吧,让我们将它们加在⼀起!合并两个纹理 (Merging both textures)By just adding these together, we’ll already see an interesting shape changing through time.通过将它们加在⼀起,我们已经可以看到有趣的形状随时间变化。To explain what’s happening, let’s imagine our noise is like a sea floating between -1 and 1. But our screen can’t displaynegative color or pixels more than 1 (pure white) so we are just seeing the values between 0 and 1.为了解释正在发⽣的事情,让我们想象⼀下我们的噪⾳就像是在-1和1之间漂浮的⼤海。但是我们的屏幕⽆法显⽰负⾊或像素⼤于1(纯⽩⾊)的像素,因此我们只能看到0到1之间的值。And our circle is like a flan.我们的圈⼦就像果馅饼。By adding these two shapes together it will give this very approximative result:通过将这两个形状加在⼀起,将得到⾮常近似的结果:Our very white pixels are only pixels outside the visible spectrum.我们⾮常⽩的像素只是可见光谱之外的像素。If we scale down our noise and subtract a small number, it will be completely moving down your waves until it disappearsabove the surface of the ocean of visible colors.如果我们按⽐例缩⼩噪声并减去⼀⼩部分,它将完全沿您的波浪向下移动,直到它消失在⽔⾯之上。 海洋可见的颜⾊。float n = snoise(vec3(offx, offy, u_time * .1) * 4.) - 1.;Our circle is still there but not enough visible to be displayed. If we multiply its value, it will be more contrasted.我们的圈⼦仍然存在,但可见度不⾜以显⽰。 如果我们乘以它的值,它将形成更⼤的对⽐。float c = circle(circlePos, 0.3, 0.3) * 2.5;We are almost there! But as you can see, there are still some details missing. And our edges aren’t sharp at all.我们就快到了! 但是正如您所看到的,仍然缺少⼀些细节。 ⽽且我们的边缘根本不锋利。To avoid that, we’ll use the .为了避免这种情况,我们将使⽤。123float finalMask = smoothstep(0.4, 0.5, n + c);
gl_FragColor = vec4(vec3(finalMask), 1.);Thanks to this function, we’ll cut a slice of our pattern between 0.4 et 0.5, for example. The shorter the space is betweenthese values, the sharper the edges are.例如,借助此功能,我们将在0.4到0.5之间切出⼀部分图案。 这些值之间的间隔越短,边缘越锐利。Finally, we can mix our two textures to use them as a mask.最后,我们可以混合两个纹理以将它们⽤作遮罩。11uniform sampler2D u_image;uniform sampler2D u_imagehover;
// ...
vec4 image = texture2D(u_image, uv);vec4 hover = texture2D(u_imagehover, uv);
vec4 finalImage = mix(image, hover, finalMask);
gl_FragColor = finalImage;We can change a few variables to have a more gooey effect:我们可以更改⼀些变量以产⽣更⼤的粘性:123456789// ...
float c = circle(circlePos, 0.3, 2.) * 2.5;
float n = snoise3(vec3(offx, offy, u_time * .1) * 8.) - 1.;
float finalMask = smoothstep(0.4, 0.5, n + pow(c, 2.));
// ...And voilà!和瞧!Check out the full or take a look at the live demo.查看完整的源代码或观看现场演⽰。麦克风下降 (Mic drop)Congratulations to those who came this far. I haven’t planned to explain this much. This isn’t perfect and I might havemissed some details but I hope you’ve enjoyed this tutorial anyway. Don’t hesitate to play with variables, try other noisefunctions and try to implement other effects using the mouse direction or play with the scroll!恭喜那些来到这⾥的⼈。 我没有计划对此做太多解释。 这并不完美,我可能已经错过了⼀些细节,但是我希望您仍然喜欢本教程。 不要犹豫,使⽤变量,尝试其他噪声函数,并尝试使⽤⿏标⽅向或滚动来实现其他效果!If you have any questions, let me know in the comments section! I also encourage you to download the demo, it’s a little bitmore complex and shows the effects in action with hover and click effects ¯_(?)_/¯如有任何疑问,请在评论部分让我知道! 我也⿎励您下载该演⽰,它稍微复杂⼀点,并通过悬停和单击效果来展⽰实际效果。 _(?)_ /参考和鸣谢 (References and Credits)python gooey
发布评论