共计 10535 个字符,预计需要花费 27 分钟才能阅读完成。
先睹为快
如下图,代码在本人一行一行写程序,逐步画出一个喜气灯笼的模样(PC 挪动端都反对噢),想不想晓得是它怎么实现的呢?和胖头鱼一起来探索一番吧 O(∩_∩)O~
你也能够间接点击 用程序主动画了一个灯笼 体验一番,胖头鱼的掘金流动仓库查看源码
原理探索
这个成果就如同一个打字员在一直地录入文字,页面出现动态效果。又如同一个早曾经录制好影片,而咱们只是坐在放映机前观看。
原理自身也非常简单,只有你会一点点前端常识,就能够马上亲手做一个进去。
1. 滚动的代码
定时器字符累加: 置信聪慧的你早曾经猜到屏幕中滚动的
html
、css
代码就是通过启动一个定时器,而后将事后筹备好的字符,一直累加到一个pre
标签中。
2. 灯笼的布局
动静增加
html 片段
和css 片段
:,一张动态网页由html
和css
组成,灯笼能一直地发生变化,背地天然是组成灯笼的html
和css
一直变动的后果。
3. 例子解释
设想一下你要往一张网页每距离 0.1 秒减少一个 啊
字,是不是开个定时器,间断地往 body 外面塞 啊
,就能够啊!没错,做到这一步就实现了原理的第一局部
再设想一下,在往页面外面塞 啊
的时候,我还想扭转啊字的字体色彩以及网页背景色彩,那应该怎么做呢,是不是执行上面的代码就能够呢?
.xxx{
color: blue;
background: red;
}
没错,只不过更改字体和背景色不是忽然扭转的,而是开个定时器,间断地往 style
标签中塞入以下代码,这样就实现了原理的第二步,是不是好简略,接下来让咱们一步步实现它。
简要解析
1. 编辑器布局
工欲善其事,必先利其器。在实现代码本人画画的前提是有个相似编辑器中央给他
show
,所以会有编辑html
、css
和预览三个区域。
挪动端布局
高低构造布局,下面是
html
、css
的编辑区域,上面的灯笼的展现区域
PC 端布局
左右构造布局,右边是
html
、css
的编辑区域,左边是灯笼的展现区域
模板
<template>
<div :class="containerClasses">
<div class="edit">
<div class="html-edit" ref="htmlEditRef">
<!-- 这是 html 代码编辑区域 -->
<pre v-html="htmlEditPre" ref="htmlEditPreRef"></pre>
</div>
<div class="css-edit" ref="cssEditRef">
<!-- 这是 css 代码编辑区域 -->
<pre v-html="styleEditPre"></pre>
</div>
</div>
<div class="preview">
<!-- 这是预览区域,灯笼最终会被画到这里噢 -->
<div class="preview-html" v-html="previewHtmls"></div>
<!-- 这里是款式真正起作用的中央,明码就暗藏... -->
<div v-html="previewStyles"></div>
</div>
</div>
</template>
端管制
简略的做一下挪动端和 PC 端的适配,而后通过款式去管制布局即可
computed: {containerClasses () {
// 做一个简略的适配
return [
'container',
isMobile() ? 'container-mobile' : '']
}
}
2. 代码高亮
示例中的代码高亮是借助
prismjs
和pre
进行转化解决的,只须要填充你想要高亮的代码,以及抉择高亮的语言就能够实现上述成果。
// 外围代码, 只有一行
this.styleEditPre = Prism.highlight(previewStylesSource, Prism.languages.css)
3. 灯笼布局实现
要实现灯笼一直变动的布局,须要两个货色,一个是灯笼自身的
html
元素还有就是管制html
款式的css
通过 preview-html
` 承载 html 片段,通过
previewStyles 承载由
style 标签包裹的
css` 款式
// 容器
<div class="preview">
<!-- 这是预览区域,灯笼最终会被画到这里噢 -->
<div class="preview-html" v-html="previewHtmls"></div>
<!-- 这里是款式真正起作用的中央 -->
<div v-html="previewStyles"></div>
</div>
逻辑代码
// 款式管制外围代码
this.previewStyles = `
<style>
${previewStylesSource}
</style>
`
// html 管制外围代码
this.previewHtmls = previewHtmls
4. 代码配置预览
咱们通过一个个步骤将代码按阶段去执行,而代码自身是通过两个文件进行配置的,一个是管制
html
的文件,一个是管制css
的文件。每一个步骤都是数组的一项
4.1 html 配置
留神上面的代码格局是成心弄成这种格局的,并非是没有对齐
export default [
// 结尾寒暄
`
<!--
XDM 好,我是前端胖头鱼~~~
据说掘金又在搞流动了,奖品还很丰富...
我能要那个美腻的小姐姐吗?-->
`,
// 阐明宗旨
`
<!--
以前都是用“手”写代码,明天想尝试一下“代码写代码”, 主动画一个喜庆的灯笼
-->
`,
// 创立编辑器
`
<!--
第①步,先创立一个编辑器
-->
`,
// 创立编辑器 html 构造
`
<div class="container">
<div class="edit">
<div class="html-edit">
<!-- 这是 html 代码编辑区域 -->
<pre v-html="htmlEditPre">
<!-- htmlStep0 -->
</pre>
</div>
<div class="css-edit">
<!-- 这是 css 代码编辑区域 -->
<pre v-html="cssEditPre"></pre>
</div>
</div>
<div class="preview">
<!-- 这是预览区域,灯笼最终会被画到这里噢 -->
<div class="preview-html"></div>
<!-- 这里是款式真正起作用的中央,明码就暗藏... -->
<div v-html="cssEditPre"></div>
</div>
</div>
`,
// 开始画款式
`
<!--
第②步,给编辑器来点款式,我要开始画了喔~~
-->
`,
// 画灯笼的大肚子
`
<!-- 第③步,先画灯笼的大肚子构造 -->
<div class="lantern-container">
<!-- htmlStep1 -->
<!-- 大红灯笼区域 -->
<div class="lantern-light">
<!-- htmlStep2 -->
</div>
</div>
`,
// 提着灯笼的线
`
<!-- 第④步,灯笼顶部是有根线的 -->
<div class="lantern-top-line"></div>
`,
`
<!-- 第⑤步,给灯笼加两个盖子 -->
<div class="lantern-hat-top"></div>
<div class="lantern-hat-bottom"></div>
<!-- htmlStep3 -->
`,
`
<!-- 第⑥步,感觉灯笼快要成了,再给他加上四根线吧 -->
<div class="lantern-line-out">
<div class="lantern-line-innner">
<!-- htmlStep5 -->
</div>
</div>
<!-- htmlStep4 -->
`,
`
<!-- 第⑦步,灯笼是不是还有底部的小尾巴呀 -->
<div class="lantern-rope-top">
<div class="lantern-rope-middle"></div>
<div class="lantern-rope-bottom"></div>
</div>
`,
`
<!-- 第⑧步,最初当然少不了送给大家的福啦 -->
<div class="lantern-fu"> 福 </div>
`
]
4.2 css 配置
export default [
// 0. 增加根本款式
`
/* 首先给所有元素加上过渡成果 */
* {
transition: all .3s;
-webkit-transition: all .3s;
}
/* 红色背景太枯燥了,咱们来点背景 */
html {color: rgb(222,222,222);
background: rgb(0,43,54);
}
/* 代码高亮 */
.token.selector{color: rgb(133,153,0);
}
.token.property{color: rgb(187,137,0);
}
.token.punctuation{color: yellow;}
.token.function{color: rgb(42,161,152);
}
`,
// 1. 创立编辑器自身的款式
`
/* 咱们须要做一个铺满全屏的容器 */
.container{
width: 100%;
height: 100vh;
display: flex;
justify-content: space-between;
align-items: center;
}
/* 代码编辑区域 50% 宽度,留一些空间给预览区域 */
.edit{
width: 50%;
height: 100%;
background-color: #1d1f20;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.html-edit, .css-edit{
flex: 1;
overflow: scroll;
padding: 10px;
}
.html-edit{border-bottom: 5px solid #2b2e2f;}
/* 预览区域有 50% 的空间 */
.preview{
flex: 1;
height: 100%;
background-color: #2f1f47;
}
.preview-html{
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
/* 好啦~ 你应该看到一个编辑器的根本感觉了,咱们要开始画灯笼咯 */
`,
// 2
`
/* 给灯笼的大肚子整款式 */
.lantern-container {position: relative;}
.lantern-light {
position: relative;
width: 120px;
height: 90px;
background-color: #ff0844;
border-radius: 50%;
box-shadow: -5px 5px 100px 4px #fa6c00;
animation: wobble 2.5s infinite ease-in-out;
transform-style: preserve-3d;
}
/* 让他动起来吧 */
@keyframes wobble {
0% {transform: rotate(-6deg);
}
50% {transform: rotate(6deg);
}
100% {transform: rotate(-6deg);
}
}
`,
// 3
`
/* 顶部的灯笼线 */
.lantern-top-line {
width: 4px;
height: 50px;
background-color: #d1bb73;
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -20px;
border-radius: 2px 2px 0 0;
}
`,
// 4
`
/* 灯笼顶部、底部盖子款式 */
.lantern-hat-top,
.lantern-hat-bottom {
content: "";
position: absolute;
width: 60px;
height: 12px;
background-color: #ffa500;
left: 50%;
transform: translateX(-50%);
}
/* 顶部地位 */
.lantern-hat-top {
top: -8px;
border-radius: 6px 6px 0 0;
}
/* 底部地位 */
.lantern-hat-bottom {
bottom: -8px;
border-radius: 0 0 6px 6px;
}
`,
// 5
`
/* 灯笼两头的线条 */
.lantern-line-out,
.lantern-line-innner {
height: 90px;
border-radius: 50%;
border: 2px solid #ffa500;
background-color: rgba(216, 0, 15, 0.1);
}
/* 线条外层 */
.lantern-line-out {
width: 100px;
margin: 12px 8px 8px 10px;
}
/* 线条内层 */
.lantern-line-innner {
margin: -2px 8px 8px 26px;
width: 45px;
display: flex;
align-items: center;
justify-content: center;
}
`,
// 6
`
/* 灯笼底部线条 */
.lantern-rope-top {
width: 6px;
height: 18px;
background-color: #ffa500;
border-radius: 0 0 5px 5px;
position: relative;
margin: -5px 0 0 60px;
/* 让灯穗也有一个动画成果 */
animation: wobble 2.5s infinite ease-in-out;
}
.lantern-rope-middle,
.lantern-rope-bottom {
position: absolute;
width: 10px;
left: -2px;
}
.lantern-rope-middle {
border-radius: 50%;
top: 14px;
height: 10px;
background-color: #dc8f03;
z-index: 2;
}
.lantern-rope-bottom {
background-color: #ffa500;
border-bottom-left-radius: 5px;
height: 35px;
top: 18px;
z-index: 1;
}
`,
// 7
`
/* 福款式 */
.lantern-fu {
font-size: 30px;
font-weight: bold;
color: #ffa500;
}
`
]
整体流程
实现原理和整个过程所需的知识点,通过简要解析置信你曾经明确了,接下来咱们要做的事件就是把这些知识点组合在一起,实现主动画画。
import Prism from 'prismjs'
import htmls from './config/htmls'
import styles from './config/styles'
import {isMobile, delay} from '../../common/utils'
export default {
name: 'newYear2022',
data () {
return {
// html 代码展现片段
htmlEditPre: '',
htmlEditPreSource: '',
// css 代码展现片段
styleEditPre: '',
// 理论起作用的 css
previewStylesSource: '',
previewStyles: '',
// 预览的 html
previewHtmls: '',
}
},
computed: {containerClasses () {
// 做一个简略的适配
return [
'container',
isMobile() ? 'container-mobile' : '']
}
},
async mounted () {
// 1. 打招呼
await this.doHtmlStep(0)
// 2. 阐明宗旨
await this.doHtmlStep(1)
await delay(500)
// 3. 第一步申明
await this.doHtmlStep(2)
await delay(500)
// 4. 创立写代码的编辑器
await this.doHtmlStep(3)
await delay(500)
// 5. 筹备写编辑器的款式
await this.doHtmlStep(4)
await delay(500)
// 6. 根本款式
await this.doStyleStep(0)
await delay(500)
// 7. 编辑器的款式
await this.doStyleStep(1)
await delay(500)
// 8. 画灯笼的大肚子 html
await Promise.all([this.doHtmlStep(5, 0),
this.doEffectHtmlsStep(5, 0),
])
await delay(500)
// 8. 画灯笼的大肚子 css
await this.doStyleStep(2)
await delay(500)
// 9. 提着灯笼的线 html
await Promise.all([this.doHtmlStep(6, 1),
this.doEffectHtmlsStep(6, 1),
])
await delay(500)
// 10. 提着灯笼的线 css
await this.doStyleStep(3)
await delay(500)
// 11. 给灯笼加两个盖子 html
await Promise.all([this.doHtmlStep(7, 2),
this.doEffectHtmlsStep(7, 2),
])
await delay(500)
// 12. 给灯笼加两个盖子 css
await this.doStyleStep(4)
await delay(500)
// 13. 感觉灯笼快要成了,再给他加上四根线吧 html
await Promise.all([this.doHtmlStep(8, 3),
this.doEffectHtmlsStep(8, 3),
])
await delay(500)
// 14. 感觉灯笼快要成了,再给他加上四根线吧 css
await this.doStyleStep(5)
await delay(500)
// 15. 灯笼是不是还有底部的小尾巴呀 html
await Promise.all([this.doHtmlStep(9, 4),
this.doEffectHtmlsStep(9, 4),
])
await delay(500)
// 16. 灯笼是不是还有底部的小尾巴呀 css
await this.doStyleStep(6)
await delay(500)
// 17. 最初当然少不了送给大家的福啦 html
await Promise.all([this.doHtmlStep(10, 5),
this.doEffectHtmlsStep(10, 5),
])
await delay(500)
// 18. 最初当然少不了送给大家的福啦 css
await this.doStyleStep(7)
await delay(500)
},
methods: {
// 渲染 css
doStyleStep (step) {
const cssEditRef = this.$refs.cssEditRef
return new Promise((resolve) => {
// 从 css 配置文件中取出第 n 步的款式
const styleStepConfig = styles[step]
if (!styleStepConfig) {return}
let previewStylesSource = this.previewStylesSource
let start = 0
let timter = setInterval(() => {
// 挨个累加
let char = styleStepConfig.substring(start, start + 1)
previewStylesSource += char
if (start >= styleStepConfig.length) {console.log('css 完结')
clearInterval(timter)
resolve(start)
} else {
this.previewStylesSource = previewStylesSource
// 右边编辑器展现给用户看的
this.styleEditPre = Prism.highlight(previewStylesSource, Prism.languages.css)
// 左边预览区域理论起作用的 css
this.previewStyles = `
<style>
${previewStylesSource}
</style>
`
start += 1
// 因为要一直滚动到底部,简略粗犷解决一下
document.documentElement.scrollTo({
top: 10000,
left: 0,
})
// 因为要一直滚动到底部,简略粗犷解决一下
cssEditRef && cssEditRef.scrollTo({
top: 100000,
left: 0,
})
}
}, 0)
})
},
// 渲染 html
doEffectHtmlsStep (step, insertStepIndex = -1) {
// 留神 html 局部和 css 局部最大的不同在于前面的步骤是有可能插入到之前的代码两头的,并不是一味地增加到尾部
// 所以须要先找到标识,而后插入
const insertStep = insertStepIndex !== -1 ? `<!-- htmlStep${insertStepIndex} -->` : -1
return new Promise((resolve) => {const htmlStepConfig = htmls[ step]
let previewHtmls = this.previewHtmls
const index = previewHtmls.indexOf(insertStep)
const stepInHtmls = index !== -1
let frontHtml = stepInHtmls ? previewHtmls.slice(0, index + insertStep.length) : previewHtmls
let endHtml = stepInHtmls ? previewHtmls.slice(index + insertStep.length) : ''
let start = 0
let chars = ''
let timter = setInterval(() => {let char = htmlStepConfig.substring(start, start + 1)
// 累加字段
chars += char
previewHtmls = frontHtml + chars + endHtml
if (start >= htmlStepConfig.length) {console.log('html 完结')
clearInterval(timter)
resolve(start)
} else {
// 赋值 html 片段
this.previewHtmls = previewHtmls
start += 1
}
}, 0)
})
},
// 编辑区域 html 高亮代码
doHtmlStep (step, insertStepIndex = -1) {
const htmlEditRef = this.$refs.htmlEditRef
const htmlEditPreRef = this.$refs.htmlEditPreRef
// 同上须要找到插入标记
const insertStep = insertStepIndex !== -1 ? `<!-- htmlStep${insertStepIndex} -->` : -1
return new Promise((resolve) => {const htmlStepConfig = htmls[ step]
let htmlEditPreSource = this.htmlEditPreSource
const index = htmlEditPreSource.indexOf(insertStep)
const stepInHtmls = index !== -1
// 依照条件拼接代码
let frontHtml = stepInHtmls ? htmlEditPreSource.slice(0, index + insertStep.length) : htmlEditPreSource
let endHtml = stepInHtmls ? htmlEditPreSource.slice(index + insertStep.length) : ''
let start = 0
let chars = ''
let timter = setInterval(() => {let char = htmlStepConfig.substring(start, start + 1)
chars += char
htmlEditPreSource = frontHtml + chars + endHtml
if (start >= htmlStepConfig.length) {console.log('html 完结')
clearInterval(timter)
resolve(start)
} else {
this.htmlEditPreSource = htmlEditPreSource
// 代码高亮解决
this.htmlEditPre = Prism.highlight(htmlEditPreSource, Prism.languages.html)
start += 1
if (insertStep !== -1) {
// 当要插入到两头时,滚动条滚动到两头,不便看代码
htmlEditRef && htmlEditRef.scrollTo({top: (htmlEditPreRef.offsetHeight - htmlEditRef.offsetHeight) / 2,
left: 1000,
})
} else {
// 否则间接滚动到底部
htmlEditRef && htmlEditRef.scrollTo({
top: 100000,
left: 0,
})
}
}
}, 0)
})
},
}
}
结尾
马上就要新年啦!愿大家新年快乐,“码”到胜利。
参考
- 过年了~ 我用 CSS 画了个灯笼,看着真喜庆
- 用原生 js 写一个 “ 多动症 ” 的简历