文字连续光影变化
<div class="box">
<span>H</span>
<span>E</span>
<span>L</span>
<span>L</span>
<span>O</span>
</div>
.box {
background: #1e1f25;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
span {
color: #fff;
font-size: 120px;
}
设置完成后如图,然后创建一个动画应用给span元素
@keyframes change {
to {
color: lightblue;
text-shadow: 2px 0 10px lightblue;
}
}
添加alternate
是为了让文字熄灭的时候可以交替切换颜色,而不是瞬间回到初始值开始变化
span {
....
animation: change 1s ease-in-out infinite alternate;
}
这个时候只需要设置每一个span元素动画的延迟时间即可
span:nth-child(n+1) {
animation-delay: 0;
}
span:nth-child(n+2) {
animation-delay: 0.2s;
}
span:nth-child(n+3) {
animation-delay: 0.4s;
}
span:nth-child(n+4) {
animation-delay: 0.6s;
}
span:nth-child(n+5) {
animation-delay: 0.8s;
}
GSAP
GSAP是一套JavaScript动画库,其核心是补间动画。
有四种类型的补间:
gsap.to()
- 这是最常见的补间类型。补间将从元素的当前状态开始,并“到”补间中定义的值进行动画处理。.to()gsap.from()
- 就像向后一样,它从补间中定义的值“从”动画化,并在元素的当前状态结束。.to()gsap.fromTo()
- 您可以定义起始值和结束值。gsap.set()
- 立即设置属性(无动画)。它本质上是一个可以还原的零持续时间补间。.to()
在页面中使用CDN引入:<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
下面是一个简单的实例,让容器移动
<div class="box"></div>
// gsap.to(运动的容器,可以直接写选择器,{配置参数})
gsap.to('.box', { x: 100, y: 100, duration: 2 })
利用gsap实现图片动画效果案例1
搭建如图所示容器
重复八个container容器
<div class="box">
<div class="container">
<div class="front">
<img src="../3.gif" alt="">
</div>
<div class="back">
我是背面文字效果
</div>
</div>
.....
</div>
首先先为每一个容器添加卡片翻转效果
.box {
width: 1600px;
height: 600px;
border: 1px solid #333;
margin: 100px auto;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-evenly;
}
.container {
width: 330px;
height: 230px;
border-radius: 15px;
overflow: hidden;
perspective: 500px;
position: relative;
}
.front,
.back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
transition: 1.5s;
}
.front {
z-index: 1;
}
.back {
background-color: lightblue;
display: flex;
align-items: center;
justify-content: center;
z-index: -1;
font-size: 30px;
transform: rotateY(-180deg);
}
img {
width: 100%;
height: 100%;
}
.container:hover .front {
transform: rotateY(180deg);
}
.container:hover .back {
transform: rotateY(0);
}
然后利用gsap给每一个容器添加,让每个容器在不同的时间点显示,同时还具有各种特效。然后为每一个容器都绑定点击事件,添加离开的gsap效果
gsap.from('.container', {
scale: 0.5, //缩放
y: -50, //移动y轴位置
stagger: {
amount: 0.3,
from: 'random'
}, //设置交错时间,类似动画的设置不同的延迟时间,但是可以设置更复杂,如从哪里开始等
ease: 'back.in'
})
document.querySelectorAll('.container').forEach(item => {
item.addEventListener('click', () => {
gsap.to('.container', {
duation: 0.5, //动画延续时间
opacity: 0,
y: -100,
stagger: {
amount: 0.3,
from: 'random'
},
ease: 'back.out'
})
})
})
时间线
创建易于调整,有弹性的动画序列的关键。将补间添加到时间线时,默认情况,补间将按照顺序依次部分。但是通过设置时间线,可以调整多个动画编排动作,从而控制整个序列。
基本语法:let tl = gsap.timeline()
搭建如下基本模版
引入并注册时间线,然后给每一个元素设置。默认情况下,下一个动画都是等待上一个动画执行完毕后才会执行
let t = gsap.timeline()
t.to('.circle:nth-child(1)', { x: 300, duration: 2 })
t.to('.circle:nth-child(2)', { x: 100, duration: 1 })
t.to('.circle:nth-child(3)', { y: 300, duration: 2 })
但是我们可以传入第三个配置项去修改默认行为
t.to('.circle:nth-child(1)', { x: 300, duration: 2 }, 2)
代表等待两秒中后触发动画,作用和delay一致t.to('.circle:nth-child(2)', { x: 100, duration: 1 }, '<')
代表上一个动画开始后,该动画立即执行,默认为>
,代表依次触发t.to('.circle:nth-child(3)', { y: 300, duration: 2 }, '+=1')
代表上一个动画结束后,该动画延迟一秒钟执行,但是可以设置-=1
代表上一个动画快播放完的前一秒,该动画执行
vivo官网动画
使用timeline
时间线,控制每一个图片的动画
<div class="screen">
<div class="box">
<img src="./images/1.webp" class="pic1">
<img src="./images/2.webp" class="pic2">
<img src="./images/3.webp" class="pic3">
<img src="./images/4.webp" class="pic4">
<img src="./images/5.webp" class="pic5">
<img src="./images/6.webp" class="pic6">
<img src="./images/7.webp" class="pic7">
</div>
</div>
html {
font-size: 0.78125vw !important;
overflow: hidden;
}
.box {
width: 127em;
}
.box img {
position: absolute;
top: 15em;
}
.pic1,
.pic7 {
width: 11.266666666em;
}
.pic2,
.pic6 {
width: 13.066666666em;
}
.pic3,
.pic5 {
width: 14.866666666em;
}
.pic4 {
width: 18.75em;
left: 55.125em;
}
.pic1,
.pic2,
.pic3 {
left: 55.125em;
z-index: -1;
}
.pic5,
.pic6,
.pic7 {
right: 55.125em;
z-index: -1;
}
let t = gsap.timeline()
t.fromTo('.pic4', { width: '18.75em' }, { width: '16.75em' }) //配置动画起始两个参数
t.fromTo('.pic1', { left: '55.125em' }, { left: '14.3em' }, '<')
t.fromTo('.pic7', { right: '55.125em' }, { right: '14.3em' }, '<')
t.fromTo('.pic2', { left: '55.125em' }, { left: '26.1em' }, '<')
t.fromTo('.pic6', { right: '55.125em' }, { right: '26.1em' }, '<')
t.fromTo('.pic3', { left: '55.125em' }, { left: '39.8em' }, '<')
t.fromTo('.pic5', { right: '55.125em' }, { right: '39.8em' }, '<')
ScrollTrigger滚动触发器
ScrollTrigger滚动触发器可以让动画随着滚动来触发,首先引入使用
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
基本使用格式:ScrollTrigger.create({})
修改之前的vivo案例,这样子动画就不是自己播放了,而是根据滚动条的位置来触发动画,同时上下滚动的时候会触发不同往返动画效果,这是由scrub
属性生效的。这是单屏效果。
ScrollTrigger.create({
trigger: '.box', //目标元素
start: 'top top', //顶部开始
end: '+=800',
scrub: true, //开启动画和滚动条协调
animation: gsap.timeline()
.fromTo('.pic4', { width: '18.75em' }, { width: '16.75em' }) //配置动画起始两个参数
.fromTo('.pic1', { left: '55.125em' }, { left: '14.3em' }, '<')
.fromTo('.pic7', { right: '55.125em' }, { right: '14.3em' }, '<')
.fromTo('.pic2', { left: '55.125em' }, { left: '26.1em' }, '<')
.fromTo('.pic6', { right: '55.125em' }, { right: '26.1em' }, '<')
.fromTo('.pic3', { left: '55.125em' }, { left: '39.8em' }, '<')
.fromTo('.pic5', { right: '55.125em' }, { right: '39.8em' }, '<')
})
接下来是一个多屏效果,需要布置三个容器,在第二个容器中放置动画效果。每一个容器都撑开一个屏幕的大小。需要让动画在第二个屏幕中触发
<div class="screen"></div>
<div class="screen2">...</div>
<div class="screen3"></div>
修改后的代码,只需要添加几个属性即可
ScrollTrigger.create({
trigger: '.screen2', //目标元素
start: 'top top', //顶部开始
end: '+=900',
markers: true, //开启刻度显示,start与end的位置
scrub: true, //开启动画和滚动条协调
pin: true, //当trigger的start条件满足的时候,是否固定animation中容器的位置
animation: gsap.timeline()
.fromTo('.pic4', { width: '18.75em' }, { width: '16.75em' }) //配置动画起始两个参数
.fromTo('.pic1', { left: '55.125em' }, { left: '14.3em' }, '<')
.fromTo('.pic7', { right: '55.125em' }, { right: '14.3em' }, '<')
.fromTo('.pic2', { left: '55.125em' }, { left: '26.1em' }, '<')
.fromTo('.pic6', { right: '55.125em' }, { right: '26.1em' }, '<')
.fromTo('.pic3', { left: '55.125em' }, { left: '39.8em' }, '<')
.fromTo('.pic5', { right: '55.125em' }, { right: '39.8em' }, '<')
})
ScrollTrigger滚动条控制台视频播放
静态结构如下
<div class="screen screen2">
<div class="summar-content">
<video src="./images/summary.mp4" class="summary"></video>
</div>
</div>
基本的js代码部分如下,这样子滑动的时候,屏幕就会固定不动了
ScrollTrigger.create({
trigger: '.screen2',
start: 'top top',
end: '+=2000',
markers: true,
scrub: true,
pin: true,
})
然后在ScrollTrigger.create()
函数中,可以传入函数作为配置项,onUpdate
函数:每次动画更新时调用(在动画处于活动状态时的每一帧上),即滚动条改变的时候。在该函数中可以获取一个参数,该参数就是滚动条对象。e.progress
代表当前滚动条的进度,范围是0~1
onUpdate(e) {
console.log(e);
},
在一个视频元素中,currentTime
代表当前播放的时候,duration
代表视频播放的总时长
onUpdate(e) { //每次动画更新时调用(在动画处于活动状态时的每一帧上)
let video = document.querySelector('.screen2 .summary');
video.currentTime = e.progress * video.duration;//相当于整个视频的百分比
},
这样子整个效果就完成了
我们做一个首屏效果配合这个视频图
修该结构添加内容
<div class="screen screen0">
<div class="kv-content">
<img src="./images/homeimg.webp" alt="">
</div>
</div>
<div class="screen screen1">
<div class="summar-content">
<video src="./images/summary.mp4" class="summary"></video>
</div>
</div>
.kv-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100vh;
}
.kv-content img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
添加完成效果如下,当滚动的时候,图片缩小,同时下一个图片出现缩放变大出现。
首屏中的js代码如下,需要在这里处理两个动画之间的播放
ScrollTrigger.create({
trigger: '.screen0',
start: 'top top',
end: '+=1000',
markers: true,
scrub: true,
animation: gsap.timeline()
.fromTo('.kv-content', { scale: 1 }, { scale: 0.5 })
.fromTo('.summar-content', { scale: 0.5 }, { scale: 1 }, '<')
})
效果图,首屏逐渐变小,下一张紧跟着放大出现
之后给视频内部添加文字,让文字随视频播放而切换
<div class="screen screen1">
<div class="summar-content">
<video src="./images/summary.mp4" class="summary"></video>
<div class="text1">
流动山海纹¹ <br>
光影层叠,山海流淌。
</div>
<div class="text2">
以星为墨 描山映海<br>
行业首创「粒子水墨」工艺²,<br>
1500万繁星粒子²,<br>
共赴山海之约。<br>
</div>
<div class="text3">
东方灵韵 山海青
</div>
</div>
</div>
.text1 {
position: absolute;
top: 60%;
left: 20%;
opacity: 0;
font-size: 2.8rem;
color: #fff
}
.text2 {
position: absolute;
top: 60%;
left: 20%;
font-size: 2.8rem;
color: #fff;
opacity: 0;
}
.text3 {
position: absolute;
top: 60%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0;
font-size: 4rem;
color: #fff
}
之后给视频播放的容器添加动画效果,主体思想是让文字从下面开始移动,移动到正中间,然后在往上移动的时候逐渐消失
ScrollTrigger.create({
trigger: '.screen1',
.....
animation: gsap.timeline()
.to('.text1', { top: '50%', opacity: 1 })
.to('.text1', { top: '0', opacity: 0 }) //让文字在移消失
.to('.text2', { top: '50%', opacity: 1 })
.to('.text2', { top: '0', opacity: 0 })
.to('.text3', { top: '50%', opacity: 1 })
.to('.text3', { top: '0', opacity: 0 })
})
奥运五环
<div class="box">
<div class="circle c1"></div>
<div class="circle c2"></div>
<div class="circle c3"></div>
<div class="circle c4"></div>
<div class="circle c5"></div>
</div>
.box {
width: 600px;
height: 600px;
border: 1px solid #333;
margin: auto;
position: relative;
}
.circle {
width: 100px;
height: 100px;
border-radius: 50%;
position: absolute;
top: 0;
}
.c1 {
border: 5px solid skyblue;
}
.c2 {
left: 120px;
border: 5px solid black;
}
.c3 {
left: 240px;
border: 5px solid red;
}
.c4 {
top: 70px;
left: 60px;
border: 5px solid yellow;
}
.c5 {
top: 70px;
left: 180px;
border: 5px solid greenyellow;
}
布局如图所示的奥运五环效果,但是发现细节处和真实的略微不一样
这里可以利用3D转换效果,利用视觉偏差造成这种效果
.box {transform-style: preserve-3d;}
.c4 {
transform-origin: center 25px; //修改选择中心
transform: rotateX(1deg);
}
.c2 {
/* 让黑色离视觉方向远,让黄色盖住 */
transform: translateZ(-3px) rotateX(4deg);
}
.c5 {
transform: translateZ(3px) rotateX(6deg);
}
.c3 {
transform: translateZ(-5px) rotateX(7deg);
}
时间函数
在利用动画效果的时候,很多时候可能会用到时间函数设置动画的快慢等。
距离:是一个值变化到另一个值的变化范围。
距离范围是0-1,但是可以超过目标值然后在恢复,但是时间轴不行。如图,代表距离可以是先负数方向移动。如果是一个圆起始时候向顺时针选择,那么按照时间曲线,它一开始会先逆时针旋转,然后在顺时针旋转。当顺时针旋转快结束的时候,会先超过目标值1,然后在接近目标值,也是顺时针旋转后逆时针旋转结束。
为什么设置成这种曲线,而非直线。是因为设置为直线的情况下,动画之间的过渡不符合常理,非常生硬。而设计这种曲线可以很好的满足我们的效果,因此它也叫贝塞尔曲线。
在下面所示图的顶部,是设置完成后的曲线运动效果示例图,其中圆颜色越浅,代表当前速度越快。左侧三个示例图是常见的贝塞尔曲线值。主体中两个圆,是自己设置的贝塞尔曲线的坐标点,通过设置这两个点的位置来控制曲线的运动过程。
边框跟随鼠标移动
大致效果如图。图中的四个边框无论是用一个元素解决,还是四个元素处理最终合并为一个元素处理。本质都是对最终合并的元素进行移动位置处理。并且移动的位置范围是在该容器中。所以可以使用定位处理。
在这里图片的大小会随着视口变化而变化,因此还需要获取图片的大小,并且这里图片的宽高做成正方形,因此只需要设置一个变量接收即可。
<div class="box">
<div class="item"><img src="./images/images/1.jpg" alt=""></div>
<div class="item"><img src="./images/images/2.jpg" alt=""></div>
<div class="item"><img src="./images/images/3.jpg" alt=""></div>
<div class="item"><img src="./images/images/4.jpg" alt=""></div>
<div class="item"><img src="./images/images/5.jpg" alt=""></div>
<div class="item"><img src="./images/images/6.jpg" alt=""></div>
<div class="pointer"></div>
</div>
利用gird
布局
html,
body {
height: 100vh;
background-color: lightblue;
}
.box {
display: grid;
width: 90%;
margin: 100px auto;
/* 定义列的大小,利用repeat函数快速实现一行3列效果。单独1fr实现与父元素100%效果*/
grid-template-columns: repeat(3, 1fr);
/* 设置行与列之间的间隙 */
gap: 50px;
position: relative;
}
.item img {
width: 100%;
;
height: 100%;
object-fit: cover;
cursor: pointer;
display: block;
}
.pointer {
position: absolute;
top: 0;
left: 0;
/* 边框粗细 */
--t: 3px;
/* 边框长度 */
--l: 30px;
/* 边框与图片的距离 */
--g: 15px;
/* 图片大小,后期js控制覆盖该变量的值,通过图片的大小间接性的设置移动框的大小*/
--s: 306px;
/* 图片的位置 */
--x: 0;
--y: 0;
width: calc(var(--s) + 2*var(--g));
height: calc(var(--s) + 2*var(--g));
border: var(--t) solid #fff;
/* 移动边框的位置 */
margin-left: calc(var(--g) * -1);
margin-top: calc(var(--g) * -1)
transform: translate(var(--x), var(--y));
transition: .3s;
}
基本效果如图,最终就是通过translate
属性不断的修改边框的位置即可。期间不断变化的属性是图片的大小,和x,y,通过js设置这三个属性的值来控制边框的移动距离。
添加如下js,这样子一个简易的跟随鼠标移动的边框就实现了。
let imgArr = Array.from(document.querySelectorAll('.item img'))
let pointer = document.querySelector('.pointer')
imgArr.forEach(img => {
// 循环给每一张图片绑定鼠标移入事件
img.addEventListener('mouseenter', () => {
// 获取当前图片的大小,还有上,下距离
let s = img.offsetHeight
let x = img.offsetLeft
let y = img.offsetTop
pointer.style.setProperty('--s', s + 'px')
pointer.style.setProperty('--x', x + 'px')
pointer.style.setProperty('--y', y + 'px')
})
})
这样子设置的--s,--x.--y
就会是行内样式,会层叠原来变量的值
之后就是设置边框中四个角的样式;可以在pointer中设置四个元素充当四个边框,这里采样一个背景色的知识点解决。
给pointer
元素添加conic-gradient()
创建锥形渐变,以下是几种使用方式
background: conic-gradient(red, blue)
设置锥形渐变的起始圆心
background: conic-gradient(at 50px 50px, red, blue)
设置每一个颜色的起始位置
background: conic-gradient(at 50px 50px, red 75%, blue 75% 100%)
还可以修改背景颜色的大小
// 0 0设置背景图的偏移量
background: conic-gradient(at 50px 50px, red 75%, blue 75% 100%) 0 0 / calc(100% - 50px) calc(100% - 50px) no-repeat;
这是不添加no-repeat
属性的效果,可以利用该效果实现四个边框样式
将起始颜色从红色修改为透明色:transparent 75%, blue 75% 100%
这里可以发现蓝色正方形实则就是最终的白色边框,因此修改蓝色边框的起始坐标,即设置边框大小(at 50px 50px)
设则设置正方形的大小
background: conic-gradient(at var(--l) var(--l), transparent 75%, blue 75% 100%) 0 0 / calc(100% - var(--l)) calc(100% - var(--l));
将原background
属性修改为-webkit-mask
效果就出现了
该代码的意思是:让下面的背景图,覆盖该属性设置的背景效果。透明的地方,让img背景图显示出来,不透明的地方,只有四个角的位置是不透明,不透明的地方,相应的元素会显示出来。
-webkit-mask: conic-gradient(at var(--l) var(--l), transparent 75%, blue 75% 100%) 0 0 / calc(100% - var(--l)) calc(100% - var(--l));
-webkit-mask
是一个 CSS 属性,用于为元素创建遮罩效果。它是 WebKit 浏览器引擎(如 Chrome 和 Safari)的私有属性,用于实现一些高级的图像遮罩效果。
-webkit-mask
属性接受一个值,可以是图像、线性渐变或径向渐变。这个值定义了遮罩的形状和透明度。
渐变边框
<div class="card">
<div class="container">
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Impedit explicabo consequatur maxime voluptatum natus
possimus dignissimos pariatur deserunt repudiandae? Eos ex culpa tempora quidem sunt asperiores iusto sequi
aspernatur similique!
</div>
</div>
.card {
width: 200px;
height: 300px;
margin: 100px auto;
background: repeating-linear-gradient(#f40,
#f40 10px,
#fff 10px,
#fff 20px,
lightblue, 20px,
lightblue, 30px,
#fff 30px,
#fff 40px);
}
.container {
padding: 15px;
text-align: center;
}
样式完成后如图,重复显示这样子的横条颜色。
这个时候需要让线条斜着摆放,在大多数类似repeating-linear-gradient
设置减半颜色的函数中,都提供了设置起始位置或角度的功能。
repeating-linear-gradient(-45deg, ...);
给内部文字容器添加一个白色背景色,遮住中间有内容的部分,这个时候效果如图,这是因为内部容器和外部容器几乎重叠了,即大小一样,因此可以给外部容器设置一个padding,让外部容器的样式显示出来。
.container {
....
background-color: #fff;
}
给card元素设置padding效果
padding: 15px;
这样子大致效果就完成了。然后就是设置鼠标经过的时候尝试让边框移动。可以移动背景板来控制边框的移动。设置如下代码,这样子整个边框实际是向上移动了一点。然后在鼠标经过的时候恢复0 0 坐标即可
background: repeating-linear-gradient(...) -20px -20px / 120% 120%;
transition: 0.3s;
.card:hover {
background-position: 0 0;
}
如图,后边是鼠标经过后的样式
Element.scrollIntoView()
让元素在任意一个滚动窗口内滑动到指定位置
<div class="box"></div>
<div class="content">
Lorem ipsum dolor sit amet consectetur <br>
adipisicing elit. Explicabo in impedit velit<br>
laboriosam maiores ullam
architecto fuga, pariatur labore vitae!
<br> Delectus magni voluptatum amet animi modi culpa tempore nemo officia!
</div>
let content = document.querySelector(".content")
在浏览器控制台操作,默认情况下是瞬间跳转到指定元素出现的位置
content.scrollIntoView()
可以添加配置项,制定平滑移动,同时也可以设置对齐方向
content.scrollIntoView({behavior :'smooth'})
该API的作用很广泛,如一个长页面的表单校验,点击登录开始校验的的时候,如果某一个项错误,就可以使用该API平滑定位到出错位置
中文拼音触发input事件
<input type="text">
<script>
let input = document.querySelector('input')
function search() {
console.log('搜索', input.value);
}
input.addEventListener('input', search)
</script>
正常英语输入的时候没有问题
但是针对中文输入的时候就会出现一点问题,通常来说,搜索事件输入一个字符就触发一次,但这是针对输入英语情况,如果是针对输入的汉字,那么需要等该次输入完全结束的时候才触发input事件。
compositionstart
和compositionend
,第一个事件代表合成的开始,第二个事件代表合成的结束,在输入英文字符的时候,因为没有合成事件,因此不会触发这两个事件。 但是输入汉字的时候就会触发该事件。
添加代码如下
input.addEventListener('compositionstart', () => {
console.log('start');
})
input.addEventListener('compositionend', () => {
console.log('end');
})
当合成事件开始的时候start只会执行一次,结束的时候end最后处理。input
事件在compositionend
事件之前处理完毕
<script>
let input = document.querySelector('input')
let isComposition = false // 设置标志位
function search() {
if (!isComposition) {
console.log('搜索', input.value);
}
}
input.addEventListener('input', search)
input.addEventListener('compositionstart', () => {
isComposition = true
console.log('start');
})
input.addEventListener('compositionend', () => {
isComposition = false
console.log('end');
search()
})
</script>
背景模糊
backdrop-filter
可以让你为一个元素后面区域添加图形效果(如模糊或颜色偏移)。因为它适用于元素背后的所有元素,为了看到效果,必须使元素或其背景至少部分透明。区别于filter
属性,filter
是让当前元素内部的内容模糊化。
<div class="mask"></div>
html,
body {
height: 100vh;
background: url(./images/images/8.jpg);
}
.mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
backdrop-filter: blur(10px); // 如果设置为filter则没有该效果
}
文字描边
<h1>文字描边</h1>
body {
height: 100vh;
background-color: lightblue;
}
h1 {
text-align: center;
font-size: 100px;
text-shadow:
0 2px #fff,
0 -2px #fff,
-2px 0 #fff,
2px 0 #fff,
-2px -2px #fff,
2px -2px #fff,
-2px 2px #fff,
2px 2px #fff
}
如果是使用text-shadow
处理文字描边的话,是可以处理的,兼容性好,但是细节无法处理到位。文字放大后如图
使用text-shadow
最大的缺点是无法给文字设置透明色。如下添加透明色后,文字内部是被白色阴影填充的
color: transparent
解决方法:使用-webkit-text-stroke
处理,该属性是一个复合属性,需要设置一个边框宽度和颜色。并且支持文字透明色,文字边框处理更加柔和平顺,缺点是兼容不好。
-webkit-text-stroke: 2px #fff;
优化代码分支
在一段代码中可能会出现多个if/else分支判断。如果在一个条件判断次数很少的情况下可以使用。但是一旦规模大了起来,那么代码的可维护性就会变得很差。
假设如下这段代码是通过分支来判断的,有没有什么方法可以替换。采用映射
function fn(name) {
if (name == '牛牛') {
console.log('牛牛来了')
} else if (name == '猫猫') {
console.log('猫猫来了');
} else if (name == '狗狗') {
console.log('狗狗来了');
}
}
fn('牛牛')
将代码优化为以下结构,这么做的好处:方便扩展,以后需要添加什么内容直接在map中添加即可,不需要在写一次if判断,同时还可以处理不同的逻辑业务,可以将原本的输出牛牛来了等全部转换为函数,在不同的函数中做不同的事情
let map = {
'牛牛': '牛牛来了',
'猫猫': '猫猫来了',
'狗狗': '狗狗来了',
}
if (map[name]) {
console.log(map[name]);
} else {
console.log('没有这个人');
}
let map = {
'牛牛': () => console.log('发送请求'),
'猫猫': () => console.log('读取文件'),
'狗狗': () => console.log('计算数量'),
}
if (map[name]) {
map[name]()
} else {
console.log('没有这个人');
}
除了像上面这样子写一个简单的对象映射,还可以写一段元组映射复杂的逻辑关系
function fn(name) {
let map = [
// 每一项是一个元组
[
() => name.includes('牛'),
() => console.log('牛牛来了')
],
[
() => name.endsWith('猫'),
() => console.log('猫来了')
],
[
() => name.endsWith('狗'),
() => console.log('狗来了')
]
]
let target = map.find(item => item[0]())
if (target) {
target[1]()
}
}
fn('牛牛')
flex-grow
<div class="box">
<div class="part" style="background-color: lightblue;"></div>
<div class="part" style="background-color:lightcoral ;"></div>
<div class="part" style="background-color:lightgoldenrodyellow ;"></div>
</div>
.box {
width: 100%;
height: 100vh;
display: flex;
}
.part {
flex-grow: 1;
}
上面的代码就是将一个页面均匀的分三份使用。使用了弹性盒下的flex-grow
属性。该属性是子元素均匀的占用父元素剩余的空间使用。
但是一旦给某一个元素添加上内容,就会出现问题,因为这里没有设置宽度,所以由内容撑开。内容占用一部分区域,剩下的区域依旧是由子元素均匀划分。这就形成了下面的情况。造成了第一个元素过大,其余的过小。
解决方法就是再添加一个属性:flex-basis: 0;
。该属性的意思是设置基准,让他们都从0开始,设置初始大小。默认是auto,自适应内容宽度。
当然可以简写为如下格式:flex: 1 0 0
其中每一项分别代表flex-grow,flex-shrink,flex-basis
。因为这里没有使用压缩就设置为0
下拉列表展开过度
假设有一个文本框和一个下拉列表,每次文本框聚集的时候,下拉列表就会以过度的方式出现逐渐出现,但是这样子会存在一个问题,就是不知道下拉列表的高度,无法设置其过度结束时候的高度,如果设置为auto则无过度效果,如果高度设置具体值,那么又该设置多少也是一个问题。
<div class="box">
<input type="text" />
<div class="select">
<p>Lorem ipsum dolor sit,</p>
<p>amet consectetur adipisicing elit</p>
<p>. Assuuid? Sed iure ex, acea</p>
<p>amet consectetur adipisicing elit</p>
<p>. Assuuid? Sed iure ex, acea</p>
</div>
</div>
.box {
width: 500px;
}
input,
.select {
width: 100%;
}
.select {
border: 1px solid #ccc;
transition: 3s;
height: 0;
overflow: hidden;
}
input:focus ~ .select {
height: auto;
}
现在效果是没有过度展开下拉列表的效果的
因此采用js去处理。
css变化如下
.box {
width: 500px;
}
input,
.select {
width: 100%;
}
.select {
border: 1px solid #ccc;
transition: 3s;
/* 初始化的时候不显示内容 */
height: 0;
overflow: hidden;
}
let select = document.querySelector(".select");
let inp = document.querySelector("input");
inp.addEventListener("focus", () => {
select.style.transition = "none";
select.style.height = "auto";
let height = select.offsetHeight; //获取当前元素的高度
select.style.height = "0";
select.offsetHeight; //强制刷新渲染队列
select.style.transition = "1s";
select.style.height = height + "px";
});
inp.addEventListener("blur", () => {
select.style.transition = "1s";
select.style.height = "0";
});
但是这种js写法会不断的触发重绘和回流,因此可以使用如下css代替,借助scale
属性缩放。css如下,无js代码,纯css解决。只不过样式效果会有一点区别。但是性能比之前的好。会自动自适应高度。
.box {
width: 500px;
}
input,
.select {
width: 100%;
}
.select {
border: 1px solid #ccc;
transition: 1s;
/* 初始化的时候不显示内容 */
transform: scaleY(0);
transform-origin: top center;
}
input:focus ~ .select {
transform: scaleY(1);
}
函数形参数量规则
一个函数身上具有length
属性,那么其规则是如何定义使用?
函数形参定义中规定了,其期望接受的形参属性的个数决定了lengh
属性的大小。
function f1(a, b) {} //期望接受两个
function f2(a, b = 1) {} //期望接受一个
function f3(a, ...args) {} //期望接受一个
console.dir(f1.length); //2
console.dir(f2.length); //1
console.dir(f3.length); //1
拖拽效果
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
</div>
.box {
margin: 100px auto;
width: 500px;
}
.item {
height: 30px;
background-color: orange;
margin-bottom: 10px;
border-radius: 5px;
padding-left: 15px;
}
效果如下
实现简单的拖拽效果,可以给元素添加一个属性 draggable="true"
就可以显示一个简单的拖拽
修改一些拖拽时候的样式,选中激活的样式
.item.active {
background: transparent;
color: transparent;
border: 1px dashed #ccc;
}
// 事件委托
let box = document.querySelector(".box");
let sourceNode; //保存当前拖拽元素
box.addEventListener("dragstart", (e) => {
// 确保处理完成拖拽事件后再添加类名
setTimeout(() => {
e.target.classList.add("active");
}, 0);
sourceNode = e.target;
e.dataTransfer.effectAllowed = "move"; //修改拖拽默认复制效果改为移动
});
box.addEventListener("dragover", (e) => {
e.preventDefault();
});
box.addEventListener("dragenter", (e) => {
e.preventDefault(); //阻止默认行为
// 排除自身和父容器
if (e.target === box || e.target === sourceNode) return;
let children = Array.from(box.children); //获取所有子节点
let sourceIndex = children.indexOf(sourceNode); //获取当前元素的下标
let targetIndex = children.indexOf(e.target); //获取目标元素的下标
// 拖拽分为向上和向下;
if (sourceIndex < targetIndex) {
// 插入到下一个元素的后面
box.insertBefore(sourceNode, e.target.nextElementSibling);
} else {
box.insertBefore(sourceNode, e.target);
}
});
box.addEventListener("dragend", (e) => {
// 拖动结束恢复样式
e.target.classList.remove("active");
});
代理属性
其中arr[2]
是动态的,后面还可以紧跟arr[2][3][5]
等等,甚至尾部相加一个数字,这种情况就适合使用代理解决这种动态属性
let arr = new Proxy(
{},
{
get(target, value) {
console.log(target, value); // {} 2
},
}
);
arr[2];
arr[2][3][10]
如果想实现这种链式调用,那么就一定返回arr本身。在get
函数中返回
get(target, value, receiver) {
// target代理对象,receiver为代理对象arr
console.log(target, value); // {} 2
console.log(receiver === arr); //true
return receiver;
},
对于输出的target和value如下
arr[2][3][10] + 100
如果后面还需要加上一个数字,那么就需要将前面每次链式调用的结果保存,在代理对象中设置一个属性保存
let arr = new Proxy(
{
num: 0,
},
{
get(target, value, receiver) {
target.num += +value; //value接受的string类型
// target代理对象,receiver为代理对象arr
console.log(target, value); // {} 2
return receiver;
},
}
);
将一个对象和一个数字相加的时候,可以对其进行转换,但是直接向上诉代码执行后会报错,因为存在Symbol类型相加,其中Symbol.toPrimitive
代表知名符号,其值是一个函数:代表将一个对象转换为原始类型的时候就会调用这个知名符号属性。
let arr = new Proxy(
{
num: 0,
},
{
get(target, value, receiver) {
// 如果是知名符号,可以理解为对象已经操作结束了开始准备进行相加,需要转换原始类型,也就是直接返回保存的累加结果
if (value === Symbol.toPrimitive) {
return () => {
return target.num;
};
}
target.num += +value;
return receiver;
},
}
);
console.log(arr[2][3][10] + 100);
网页复制元素问题
在网页中复制元素,会遇见点击复制的时候弹窗,这是如何监听到用户操作?
编写随机代码文字
使用copy
监听事件,可以监听用户是否选择了复制。在下面的代码中,禁用了默认行为,即复制。经过测试,每次复制页面中的文字都无法复制成功
document.addEventListener("copy", (e) => {
e.preventDefault();
console.log("复制了");
});
打包可视化插件分析
基于webpack的vue使用插件:webpack-bundle-analyzer
。基于vite的vue使用插件:rollup-plugin-visualizer
。然后需要配置打包的相关信息,只有在打包的环境下即生产环境才需要引入该插件使用即可。
前端实现按钮点击文件或多文件上传
hmtl结构部分如图所示,根据不同的前端属性即可实现文件的上传:multiple,webkitdirectory,mozdirectory,odirectory
,其中后面的三个属性分别用于兼容不同浏览器中处理文件夹中文件上传。如果是ie直接仅支持多文件上传即可。然后都是基于文件的change
获取对应元素的files
属性即可
多文件
<input id="fileID" ref="fileDOM" type="file" style="display: none" multiple />
文件夹
<input
id="fileDirID"
ref="fileDirDOM"
type="file"
style="display: none"
webkitdirectory
mozdirectory
odirectory
/>
前端实现拖拽点击文件或多文件上传
首先需要搭建如图所示的拖拽区域。然后绑定事件监听
document.querySelector('.drag')?.addEventListener('dragover', dragoverfun)
document.querySelector('.drag')?.addEventListener('drop', drogfun)
在e.dataTransfer
属性中存放files
或items
属性,如果是一个多级文件夹目录,那么文件都是存放在items
中保存
以下是基于drop
事件内部方法实现
e.preventDefault()
let arr = []
for (const item of e.dataTransfer.items) {
const entry = item.webkitGetAsEntry()
if (entry.isDirectory) {
//文件夹的情况,不断遍历
const reader = entry.createReader()
//异步读取
let res = await new Promise((resolve) => {
reader.readEntries((entries: any) => {
resolve(entries)
})
})
arr = res
} else {
let res = await new Promise((resolve) => {
// 非文件夹情况,异步读取文件信息
entry.file((f: any) => {
//f是单个文件,但是后面的操作都是针对多文件,因此需要转换为数组
resolve(f)
})
})
arr.push(res)
}
}
粒子时钟
<canvas></canvas>
<script src="./index.js"></script>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
background: radial-gradient(#fff, #ebd0d0);
}
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d", {
willReadFrequently: true,
});
function initCanvasSize() {
//devicePixelRatio保持情绪度
canvas.width = window.innerWidth * devicePixelRatio;
canvas.height = window.innerHeight * devicePixelRatio;
}
initCanvasSize();
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
//粒子类主体
class Particle {
constructor() {
const cx = canvas.width / 2; //大圆的相关信息
const cy = canvas.height / 2;
const r = Math.min(canvas.width, canvas.height) / 2; //取画布宽高中的最小值作为半径
const rad = (getRandom(0, 360) * Math.PI) / 180; //弧度信息,处理小圆位于大圆上位置
//绘制小圆的信息,小圆依附于大院边上排列
//初始情况并没有使用canvas绘制圆的坐标信息,因此需要加cx和cy
this.x = cx + r * Math.cos(rad); //小圆位置等
this.y = cy + r * Math.sin(rad);
this.size = getRandom(2 * devicePixelRatio, 7 * devicePixelRatio); //小圆大小
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = "#544544d";
ctx.fill();
}
moveTo(tx, ty) {
const duration = 500; //移动时长
const sx = this.x,
sy = this.y; //当前坐标
const xSpeed = (tx - sx) / duration,
ySpeed = (ty - sy) / duration; //移动速度
const startTime = Date.now();
const _move = () => {
let t = Date.now() - startTime; //时间差
this.x = xSpeed * t + sx;
this.y = ySpeed * t + sy;
if (t >= duration) {
(this.x = tx), (this.y = ty);
return;
}
requestAnimationFrame(_move);
};
_move();
}
}
let particles = []; //粒子数组
function clear() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
//全局移动方法
function draw() {
clear();
update();
for (let p of particles) {
p.draw();
}
requestAnimationFrame(draw);
}
function getText() {
return new Date().toTimeString().substring(0, 8);
}
let text = null;
function update() {
//函数核心作用绘制粒子,粒子的数量通过需要绘制文件的像素点完成
let curText = getText();
if (curText === text) return; //防止频繁触发绘制
clear(); //测试代码,查看绘制的文字
text = curText;
ctx.fillStyle = "#000";
ctx.textBaseline = "middle"; //设置基线
ctx.font = `${140 * devicePixelRatio}px DS-Digital`; //设置样式
ctx.textAlign = "center";
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
let points = getPoints(); //points为最终需要绘制的像素点数组,即Partical
clear(); //清空画布,通过粒子组成字体
for (let i = 0; i < points.length; i++) {
const [x, y] = points[i]; //移动的像素点目标位置
let p = particles[i]; //从当前粒子数组中寻找对应的粒子(粒子和位置是一一对应)
//这部分属于像素点比数组中粒子多的情况
if (!p) {
//粒子数组中可能会不存
p = new Particle();
particles.push(p);
}
p.moveTo(x, y);
}
if (points.length < particles.length) {
//像素点数组小于粒子数量,直接将多余的粒子数量删除
particles.splice(points.length);
}
}
function getPoints() {
let points = [];
//获取文字的像素点位置信息,数据均保存在data中,四个一组
let { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
let gap = 4; //设置步长
for (let i = 0; i < canvas.width; i += gap) {
for (let j = 0; j < canvas.height; j += gap) {
let index = (i + j * canvas.width) * 4; //每组的起始位置下标
let r = data[index],
g = data[index + 1],
b = data[index + 2],
a = data[index + 3];
//黑色像素点信息,255是不透明度
if (r === 0 && g === 0 && b === 0 && a === 255) {
points.push([i, j]);
}
}
}
return points;
}
draw();