PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛
先睹为快
如下图,代码在自己一行一行写程序,逐渐画出一个喜气灯笼的模样(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)
})
},
}
}
结尾
马上就要新年啦!愿大家新年快乐,“码”到成功。