PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛

先睹为快

如下图,代码在自己一行一行写程序,逐渐画出一个喜气灯笼的模样(PC移动端都支持噢),想不想知道是它怎么实现的呢?和胖头鱼一起来探究一番吧O(∩_∩)O~

你也可以直接点击 用程序自动画了一个灯笼 体验一番,胖头鱼的掘金活动仓库查看源码

111111.gif

原理探究

这个效果就好像一个打字员在不断地录入文字,页面呈现动态效果。又好像一个早已经录制好影片,而我们只是坐在放映机前观看。

原理本身也非常简单,只要你会一点点前端知识,就可以马上亲手做一个出来。

1. 滚动的代码

定时器字符累加: 相信聪明的你早已经猜到屏幕中滚动的htmlcss代码就是通过启动一个定时器,然后将预先准备好的字符,不断累加到一个pre标签中。

2. 灯笼的布局

动态添加html片段css片段,一张静态网页由htmlcss组成,灯笼能不断地发生变化,背后自然是组成灯笼的htmlcss不断变化的结果。

3. 例子解释

想象一下你要往一张网页每间隔0.1秒增加一个字,是不是开个定时器,间断地往body里面塞,就可以啊!没错,做到这一步就完成了原理的第一部分

再想象一下,在往页面里面塞的时候,我还想改变啊字的字体颜色以及网页背景颜色,那应该怎么做呢,是不是执行下面的代码就可以呢?

.xxx{ color: blue; background: red; }

没错,只不过更改字体和背景色不是突然改变的,而是开个定时器,间断地往style标签中塞入以下代码,这样就完成了原理的第二步,是不是好简单 , 接下来让我们一步步完成它。

简要解析

1.编辑器布局

工欲善其事,必先利其器。在实现代码自己画画的前提是有个类似编辑器地方给他show,所以会有编辑htmlcss和预览三个区域。

移动端布局

上下结构布局,上面是htmlcss的编辑区域,下面的灯笼的展示区域

1111-1.png

PC端布局

左右结构布局,左边是htmlcss的编辑区域,右边是灯笼的展示区域

1111-2.png

模板

<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.代码高亮

示例中的代码高亮是借助prismjspre进行转化处理的,只需要填充你想要高亮的代码,以及选择高亮的语言就可以实现上述效果。

// 核心代码,只有一行 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) }) }, } }

结尾

马上就要新年啦!愿大家新年快乐,“码”到成功。

参考

  1. 过年了~我用CSS画了个灯笼,看着真喜庆

  2. 用原生 js 写一个 “多动症” 的简历