几个骚操作,让代码自动学会画画,太好玩啦!

先睹为快

如下图,代码在自己一行一行写程序,逐渐画出一个喜气灯笼的模样( PC移动端都支持噢),想不想知道是它怎么实现的呢?和胖头鱼一起来探究一番吧O(∩_∩)O~
你也可以直接点击 用程序自动画了一个灯笼 体验一番,胖头鱼的掘金活动仓库查看源码

原理探究
这个效果就好像一个打字员在不断地录入文字,页面呈现动态效果。又好像一个早已经录制好影片,而我们只是坐在放映机前观看。
原理本身也非常简单,只要你会一点点前端知识,就可以马上亲手做一个出来。
1. 滚动的代码
定时器字符累加: 相信聪明的你早已经猜到屏幕中滚动的 htmlcss代码就是通过启动一个定时器,然后将预先准备好的字符,不断累加到一个 pre标签中。
2. 灯笼的布局
动态添加html片段css片段:,一张静态网页由 htmlcss组成,灯笼能不断地发生变化,背后自然是组成灯笼的 htmlcss不断变化的结果。
3. 例子解释 想象一下你要往一张网页每间隔0.1秒增加一个字,是不是开个定时器,间断地往body里面塞,就可以啊!没错,做到这一步就完成了原理的第一部分
再想象一下,在往页面里面塞的时候,我还想改变啊字的字体颜色以及网页背景颜色,那应该怎么做呢,是不是执行下面的代码就可以呢?
.xxx{ color: blue; background: red; }

没错,只不过更改字体和背景色不是突然改变的,而是开个定时器,间断地往style标签中塞入以下代码,这样就完成了原理的第二步,是不是好简单 , 接下来让我们一步步完成它。
简要解析 1.编辑器布局
工欲善其事,必先利其器。在实现代码自己画画的前提是有个类似编辑器地方给他 show,所以会有编辑 htmlcss和预览三个区域。
移动端布局
上下结构布局,上面是 htmlcss的编辑区域,下面的灯笼的展示区域
几个骚操作,让代码自动学会画画,太好玩啦!
文章图片

PC端布局
左右结构布局,左边是 htmlcss的编辑区域,右边是灯笼的展示区域
几个骚操作,让代码自动学会画画,太好玩啦!
文章图片

【几个骚操作,让代码自动学会画画,太好玩啦!】模板

端控制
简单的做一下移动端和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`样式
// 容器

逻辑代码
// 样式控制核心代码 this.previewStyles = `${previewStylesSource}` // html控制核心代码 this.previewHtmls = previewHtmls

4. 代码配置预览
我们通过一个个步骤将代码按阶段去执行,而代码本身是通过两个文件进行配置的,一个是控制 html的文件,一个是控制 css的文件。每一个步骤都是数组的一项
4.1 html配置
注意下面的代码格式是故意弄成这种格式的,并非是没有对齐
export default [ // 开头寒暄 ``, // 说明主旨 ``, // 创建编辑器 ``, // 创建编辑器html结构 `


`, // 开始画样式 ``, // 画灯笼的大肚子 ``, // 提着灯笼的线 ``, ``, ``, ``, `福 `]

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.cssEditRefreturn 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 += charif (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 = `${previewStylesSource}` 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 ? `` : -1 return new Promise((resolve) => { const htmlStepConfig = htmls[ step ] let previewHtmls = this.previewHtmls const index = previewHtmls.indexOf(insertStep) const stepInHtmls = index !== -1let 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 += charpreviewHtmls = frontHtml + chars + endHtmlif (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 ? `` : -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 += charhtmlEditPreSource = frontHtml + chars + endHtmlif (start >= htmlStepConfig.length) { console.log('html结束') clearInterval(timter) resolve(start) } else { this.htmlEditPreSource = htmlEditPreSource // 代码高亮处理 this.htmlEditPre = Prism.highlight(htmlEditPreSource, Prism.languages.html) start += 1if (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 写一个 "多动症" 的简历

    推荐阅读