JS实现酷酷的霓虹灯效果

首发于:知乎

INTRODUCTION

这个是以前写下的一个vue插件,抽取里面的霓虹灯生成部分来说说怎么能代码生成一个霓虹灯字效果。先看看最后的效果图:

neon

ANALYSIS

核心部分用SVG或者CANVAS都是可以的,不过感觉SVG更方便携带,所以我这里用了SVG。主要步骤如下:

  1. 确立一个霓虹灯效果的SVG模板
  2. 将字体转换成SVG路径并将字体路径嵌入到SVG模板中去
  3. 加点CSS动画效果
  4. 完工

开撸

1. SVG模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" :width="width" :height="height" :style="svgStyle">
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="4" result="coloredBlur"></feGaussianBlur>
<feMerge>
<feMergeNode in="coloredBlur"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
<!-- Logo Container -->
<g class="fade">
<!-- Outside -->
<g :style="{fill:color}" v-html="inside">
</g>
</g>
</svg>

这个就是我代码里面的SVG模板,用了vue来进行数据的绑定。

svg里面使用了高斯模糊的filter

1
<feGaussianBlur stdDeviation="4" result="coloredBlur"></feGaussianBlur>

但是因为霓虹灯字体里面的是灯管,应该是清晰的,所以原字体的轮廓不能模糊掉,使用feMerge来融合层

1
2
3
4
<feMerge>
<feMergeNode in="coloredBlur"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>

再来看下面就是内容部分,我在这里加了一个带有fade类的容器,是用来最后加仿真动画效果的,后面再说,然后一个里容器就是用来放置字体的路径。

2. 将字体转换成SVG路径

这个原理其实是读取.ttf , .woff 这些字体文件来获取字体的描点来构成路径。

知道这个原理之后,我们就要找个可以支持浏览器的字体文件读取库 (当然有空读字体文件格式规范的朋友们也能自己写一个库)。

这里可以看下opentype.js 和基于它的 text-to-svg(两个包都没有umd打包)。我这里使用的是text-to-svg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import TextToSVG from 'text-to-svg' 
// this.fontFile => .ttf/.woff字体文件
// this.words => 全部字
// this.width => 全部字加起来的总宽度
// this.height => 全部字占用的单行高度
// this.flash => 是否使用flash效果
// this.inside => 绑定到svg模板的路径数据

TextToSVG.load(this.fontFile, (err,textToSVG) => {
// Divide to single letter
this.words.split('').forEach(word => {
// Setting
const options = {x: this.width + 10, y: 0, anchor: 'left top'}
let svg = textToSVG.getSVG(word,options)
// 获取当前字体的宽高
let matchs = svg.match(/(?<=").*?(?=")/g)
this.width += parseInt(matchs[4])
this.height = parseInt(matchs[6])
// 获取当前字体的路径
let path = svg.match(/\<path\s*(.*?)\/\>/g)[0]
// 如果闪烁效果打开
if (this.flash){
path = path.replace('<path ',`<path ${this.$options._scopeId} class="random${Math.round(Math.random()*10)+1}"`)
}
this.inside += path
this.width += 5
})
})

首先看到TextToSVG.load(this.fontFile,(err,textToSVG) => {}) 这个函数是读取字体文件并且返回一个字体解析对象。再来通过这个返回的字体解析对象textToSVG我们就可以通过 textToSVG.getSVG(word) 用来转换字体,返回的是类似以下的一个SVG对象:

1
2
3
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="72" height="72">
<path d="M323.34 40.01L323.34 7.66L385.99 7.66L385.99 36L385.99 36Q385.71 36.70 385.15 37.76L385.15 37.76L385.15 37.76Q384.59 39.09 384.02 40.01L384.02 40.01L388.10 40.01L388.10 51.89L382.48 51.89L382.69 52.66L383.74 55.13L383.74 55.13Q384.38 56.46 384.80 57.30L384.80 57.30L387.82 57.30L387.82 69.82L374.53 69.82L374.53 69.82Q372 69.19 370.31 66.38L370.31 66.38L370.31 66.38Q368.77 63.77 365.81 55.76L365.81 55.76L365.60 54.77L365.60 51.89L363.21 51.89L363.21 69.82L345.91 69.82L345.91 54.56L345.21 55.13L342.75 63.35L342.75 63.35Q340.85 68.70 338.32 69.82L338.32 69.82L321.87 69.82L321.87 57.38L325.88 57.38L325.88 57.38Q326.16 56.81 327.21 54.70L327.21 54.70L327.21 54.70Q328.13 52.95 328.48 51.89L328.48 51.89L321.59 51.89L321.59 40.01L323.34 40.01ZM384.45 9.21L324.96 9.21L324.96 39.80L347.46 39.80L347.46 41.55L322.99 41.55L322.99 50.34L347.46 50.34L347.46 68.34L361.66 68.34L361.66 50.34L386.55 50.34L386.55 41.55L361.66 41.55L361.66 39.80L378.12 39.80L378.12 39.80Q380.23 39.38 381.98 37.69L381.98 37.69L381.98 37.69Q383.81 36.07 384.45 34.03L384.45 34.03L384.45 9.21ZM338.04 20.46L338.04 17.37L347.46 17.37L347.46 20.46L338.04 20.46ZM361.66 20.46L361.66 17.37L362.09 17.37L372.49 17.37L372.49 20.46L361.66 20.46ZM338.04 31.01L338.04 27.98L347.46 27.98L347.46 31.01L338.04 31.01ZM361.66 31.01L361.66 27.98L362.09 27.98L372.49 27.98L372.49 31.01L361.66 31.01ZM343.80 52.45L330.94 52.45L330.94 52.45Q330.09 53.79 329.18 55.62L329.18 55.62L327.63 58.85L323.20 58.85L323.20 68.34L336.56 68.34L336.56 68.34Q339.02 67.01 340.99 61.52L340.99 61.52L343.59 53.23L343.80 52.45ZM380.02 52.45L367.01 52.45L367.01 52.45Q370.73 62.79 372.14 65.25L372.14 65.25L372.14 65.25Q373.55 67.71 376.29 68.34L376.29 68.34L386.27 68.34L386.27 58.71L383.11 58.71L382.55 57.94L382.55 57.94Q381.91 56.74 381.21 55.20L381.21 55.20L381.21 55.20Q380.72 54.14 380.02 52.45L380.02 52.45Z"/>
</svg>

再下来就可以通过读取这个svg上面的属性来计算总的长度和高度,然后再正则获取到<path /> 里面的内容,对SVG模板上的部分进行绑定,就可以得到当前效果:

neon-1

3. 加点CSS动画效果

光是这样子的话感觉太单调了,跟大家平时去得红灯区感觉有点不一样,所以这里加点小动画。

1) 电压不稳定时候的忽明忽暗

这个效果其实就是一个fade,前面加到容器里的类fade

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.fade {
animation: fade 3s infinite;
}

@keyframes fade {
0%, 100% {
opacity: 0.7;
filter: url(#glow);
}
50% {
opacity: 1;
filter: url(#glow);
}
}
2)濒临坏掉的闪烁

@keyframes random {
0%, 9%, 11% {
opacity: 1;
}
10% {
opacity: 0;
}
}

为了真实性可以多准备几个random的类

1
2
3
4
5
6
7
8
9
10
11
.random1 {
animation: random 8s infinite;
}
.random2 {
animation: random 7s infinite;
animation-delay: 2s;
}
.random3 {
animation: random 6s infinite;
animation-delay: 4s;
}

然后在第二步path绑定到模板的时候,让path随机增加一个random类。

4. 完工

来看看最后效果

Demo

总结

常常总结所得是有益处的,例如我写文章的时候发现了一些组件上写得不好的地方,也想到了能改进的方法,抽空的话把组件也更新一次。