原文地址

先睹为快

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

原理探究

这个效果就好像一个打字员在不断地录入文字,页面呈现动态效果。又好像一个早已经录制好影片,而我们只是坐在放映机前观看。
原理本身也非常简单,只要你会一点点前端知识,就可以马上亲手做一个出来

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的编辑区域,下面的灯笼的展示区域
2022/01/19 几个骚操作,让代码自动学会画画,太好玩啦! - 图2
PC端布局
左右结构布局,左边是html、css的编辑区域,右边是灯笼的展示区域
2022/01/19 几个骚操作,让代码自动学会画画,太好玩啦! - 图3

模板

  1. <template>
  2. <div :class="containerClasses">
  3. <div class="edit">
  4. <div class="html-edit" ref="htmlEditRef">
  5. <!-- 这是html代码编辑区域 -->
  6. <pre v-html="htmlEditPre" ref="htmlEditPreRef"></pre>
  7. </div>
  8. <div class="css-edit" ref="cssEditRef">
  9. <!-- 这是css代码编辑区域 -->
  10. <pre v-html="styleEditPre"></pre>
  11. </div>
  12. </div>
  13. <div class="preview">
  14. <!-- 这是预览区域,灯笼最终会被画到这里噢 -->
  15. <div class="preview-html" v-html="previewHtmls"></div>
  16. <!-- 这里是样式真正起作用的地方,密码就隐藏... -->
  17. <div v-html="previewStyles"></div>
  18. </div>
  19. </div>
  20. </template>

端控制
简单的做一下移动端和PC端的适配,然后通过样式去控制布局即可

  1. computed: {
  2. containerClasses () {
  3. // 做一个简单的适配
  4. return [
  5. 'container',
  6. isMobile() ? 'container-mobile' : ''
  7. ]
  8. }
  9. }

2.代码高亮

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

  1. // 核心代码,只有一行
  2. this.styleEditPre = Prism.highlight(previewStylesSource, Prism.languages.css)

通过preview-html``承载html片段,通过previewStyles承载由style标签包裹的css样式

  1. // 容器
  2. <div class="preview">
  3. <!-- 这是预览区域,灯笼最终会被画到这里噢 -->
  4. <div class="preview-html" v-html="previewHtmls"></div>
  5. <!-- 这里是样式真正起作用的地方 -->
  6. <div v-html="previewStyles"></div>
  7. </div>

逻辑代码

  1. // 样式控制核心代码
  2. this.previewStyles = `
  3. <style>
  4. ${previewStylesSource}
  5. </style>
  6. `
  7. // html控制核心代码
  8. this.previewHtmls = previewHtmls

4. 代码配置预览

我们通过一个个步骤将代码按阶段去执行,而代码本身是通过两个文件进行配置的,一个是控制html的文件,一个是控制css的文件。每一个步骤都是数组的一项

4.1 html配置

注意下面的代码格式是故意弄成这种格式的,并非是没有对齐

  1. export default [
  2. // 开头寒暄
  3. `
  4. <!--
  5. XDM好,我是前端胖头鱼~~~
  6. 听说掘金又在搞活动了,奖品还很丰厚...
  7. 我能要那个美腻的小姐姐吗?
  8. -->
  9. `,
  10. // 说明主旨
  11. `
  12. <!--
  13. 以前都是用“手”写代码,今天想尝试一下
  14. “代码写代码”,自动画一个喜庆的灯笼
  15. -->
  16. `,
  17. // 创建编辑器
  18. `
  19. <!--
  20. 第①步,先创建一个编辑器
  21. -->
  22. `,
  23. // 创建编辑器html结构
  24. `
  25. <div class="container">
  26. <div class="edit">
  27. <div class="html-edit">
  28. <!-- 这是html代码编辑区域 -->
  29. <pre v-html="htmlEditPre">
  30. <!-- htmlStep0 -->
  31. </pre>
  32. </div>
  33. <div class="css-edit">
  34. <!-- 这是css代码编辑区域 -->
  35. <pre v-html="cssEditPre"></pre>
  36. </div>
  37. </div>
  38. <div class="preview">
  39. <!-- 这是预览区域,灯笼最终会被画到这里噢 -->
  40. <div class="preview-html"></div>
  41. <!-- 这里是样式真正起作用的地方,密码就隐藏... -->
  42. <div v-html="cssEditPre"></div>
  43. </div>
  44. </div>
  45. `,
  46. // 开始画样式
  47. `
  48. <!--
  49. 第②步,给编辑器来点样式,我要开始画了喔~~
  50. -->
  51. `,
  52. // 画灯笼的大肚子
  53. `
  54. <!-- 第③步,先画灯笼的大肚子结构 -->
  55. <div class="lantern-container">
  56. <!-- htmlStep1 -->
  57. <!-- 大红灯笼区域 -->
  58. <div class="lantern-light">
  59. <!-- htmlStep2 -->
  60. </div>
  61. </div>
  62. `,
  63. // 提着灯笼的线
  64. `
  65. <!-- 第④步,灯笼顶部是有根线的 -->
  66. <div class="lantern-top-line"></div>
  67. `,
  68. `
  69. <!-- 第⑤步,给灯笼加两个盖子 -->
  70. <div class="lantern-hat-top"></div>
  71. <div class="lantern-hat-bottom"></div>
  72. <!-- htmlStep3 -->
  73. `,
  74. `
  75. <!-- 第⑥步,感觉灯笼快要成了,再给他加上四根线吧 -->
  76. <div class="lantern-line-out">
  77. <div class="lantern-line-innner">
  78. <!-- htmlStep5 -->
  79. </div>
  80. </div>
  81. <!-- htmlStep4 -->
  82. `,
  83. `
  84. <!-- 第⑦步,灯笼是不是还有底部的小尾巴呀 -->
  85. <div class="lantern-rope-top">
  86. <div class="lantern-rope-middle"></div>
  87. <div class="lantern-rope-bottom"></div>
  88. </div>
  89. `,
  90. `
  91. <!-- 第⑧步,最后当然少不了送给大家的福啦 -->
  92. <div class="lantern-fu">福</div>
  93. `
  94. ]

4.2 css配置

  1. export default [
  2. // 0. 添加基本样式
  3. `
  4. /* 首先给所有元素加上过渡效果 */
  5. * {
  6. transition: all .3s;
  7. -webkit-transition: all .3s;
  8. }
  9. /* 白色背景太单调了,我们来点背景 */
  10. html {
  11. color: rgb(222,222,222);
  12. background: rgb(0,43,54);
  13. }
  14. /* 代码高亮 */
  15. .token.selector{
  16. color: rgb(133,153,0);
  17. }
  18. .token.property{
  19. color: rgb(187,137,0);
  20. }
  21. .token.punctuation{
  22. color: yellow;
  23. }
  24. .token.function{
  25. color: rgb(42,161,152);
  26. }
  27. `,
  28. // 1.创建编辑器本身的样式
  29. `
  30. /* 我们需要做一个铺满全屏的容器 */
  31. .container{
  32. width: 100%;
  33. height: 100vh;
  34. display: flex;
  35. justify-content: space-between;
  36. align-items: center;
  37. }
  38. /* 代码编辑区域50%宽度,留一些空间给预览区域 */
  39. .edit{
  40. width: 50%;
  41. height: 100%;
  42. background-color: #1d1f20;
  43. display: flex;
  44. flex-direction: column;
  45. justify-content: space-between;
  46. }
  47. .html-edit, .css-edit{
  48. flex: 1;
  49. overflow: scroll;
  50. padding: 10px;
  51. }
  52. .html-edit{
  53. border-bottom: 5px solid #2b2e2f;
  54. }
  55. /* 预览区域有50%的空间 */
  56. .preview{
  57. flex: 1;
  58. height: 100%;
  59. background-color: #2f1f47;
  60. }
  61. .preview-html{
  62. display: flex;
  63. align-items: center;
  64. justify-content: center;
  65. height: 100%;
  66. }
  67. /* 好啦~ 你应该看到一个编辑器的基本感觉了,我们要开始画灯笼咯 */
  68. `,
  69. // 2
  70. `
  71. /* 给灯笼的大肚子整样式 */
  72. .lantern-container {
  73. position: relative;
  74. }
  75. .lantern-light {
  76. position: relative;
  77. width: 120px;
  78. height: 90px;
  79. background-color: #ff0844;
  80. border-radius: 50%;
  81. box-shadow: -5px 5px 100px 4px #fa6c00;
  82. animation: wobble 2.5s infinite ease-in-out;
  83. transform-style: preserve-3d;
  84. }
  85. /* 让他动起来吧 */
  86. @keyframes wobble {
  87. 0% {
  88. transform: rotate(-6deg);
  89. }
  90. 50% {
  91. transform: rotate(6deg);
  92. }
  93. 100% {
  94. transform: rotate(-6deg);
  95. }
  96. }
  97. `,
  98. // 3
  99. `
  100. /* 顶部的灯笼线 */
  101. .lantern-top-line {
  102. width: 4px;
  103. height: 50px;
  104. background-color: #d1bb73;
  105. position: absolute;
  106. left: 50%;
  107. transform: translateX(-50%);
  108. top: -20px;
  109. border-radius: 2px 2px 0 0;
  110. }
  111. `,
  112. // 4
  113. `
  114. /* 灯笼顶部、底部盖子样式 */
  115. .lantern-hat-top,
  116. .lantern-hat-bottom {
  117. content: "";
  118. position: absolute;
  119. width: 60px;
  120. height: 12px;
  121. background-color: #ffa500;
  122. left: 50%;
  123. transform: translateX(-50%);
  124. }
  125. /* 顶部位置 */
  126. .lantern-hat-top {
  127. top: -8px;
  128. border-radius: 6px 6px 0 0;
  129. }
  130. /* 底部位置 */
  131. .lantern-hat-bottom {
  132. bottom: -8px;
  133. border-radius: 0 0 6px 6px;
  134. }
  135. `,
  136. // 5
  137. `
  138. /* 灯笼中间的线条 */
  139. .lantern-line-out,
  140. .lantern-line-innner {
  141. height: 90px;
  142. border-radius: 50%;
  143. border: 2px solid #ffa500;
  144. background-color: rgba(216, 0, 15, 0.1);
  145. }
  146. /* 线条外层 */
  147. .lantern-line-out {
  148. width: 100px;
  149. margin: 12px 8px 8px 10px;
  150. }
  151. /* 线条内层 */
  152. .lantern-line-innner {
  153. margin: -2px 8px 8px 26px;
  154. width: 45px;
  155. display: flex;
  156. align-items: center;
  157. justify-content: center;
  158. }
  159. `,
  160. // 6
  161. `
  162. /* 灯笼底部线条 */
  163. .lantern-rope-top {
  164. width: 6px;
  165. height: 18px;
  166. background-color: #ffa500;
  167. border-radius: 0 0 5px 5px;
  168. position: relative;
  169. margin: -5px 0 0 60px;
  170. /* 让灯穗也有一个动画效果 */
  171. animation: wobble 2.5s infinite ease-in-out;
  172. }
  173. .lantern-rope-middle,
  174. .lantern-rope-bottom {
  175. position: absolute;
  176. width: 10px;
  177. left: -2px;
  178. }
  179. .lantern-rope-middle {
  180. border-radius: 50%;
  181. top: 14px;
  182. height: 10px;
  183. background-color: #dc8f03;
  184. z-index: 2;
  185. }
  186. .lantern-rope-bottom {
  187. background-color: #ffa500;
  188. border-bottom-left-radius: 5px;
  189. height: 35px;
  190. top: 18px;
  191. z-index: 1;
  192. }
  193. `,
  194. // 7
  195. `
  196. /* 福样式 */
  197. .lantern-fu {
  198. font-size: 30px;
  199. font-weight: bold;
  200. color: #ffa500;
  201. }
  202. `
  203. ]

整体流程

实现原理和整个过程所需的知识点,通过简要解析相信你已经明白了,接下来我们要做的事情就是把这些知识点组合在一起,完成自动画画。

  1. import Prism from 'prismjs'
  2. import htmls from './config/htmls'
  3. import styles from './config/styles'
  4. import { isMobile, delay } from '../../common/utils'
  5. export default {
  6. name: 'newYear2022',
  7. data () {
  8. return {
  9. // html代码展示片段
  10. htmlEditPre: '',
  11. htmlEditPreSource: '',
  12. // css代码展示片段
  13. styleEditPre: '',
  14. // 实际起作用的css
  15. previewStylesSource: '',
  16. previewStyles: '',
  17. // 预览的html
  18. previewHtmls: '',
  19. }
  20. },
  21. computed: {
  22. containerClasses () {
  23. // 做一个简单的适配
  24. return [
  25. 'container',
  26. isMobile() ? 'container-mobile' : ''
  27. ]
  28. }
  29. },
  30. async mounted () {
  31. // 1. 打招呼
  32. await this.doHtmlStep(0)
  33. // 2. 说明主旨
  34. await this.doHtmlStep(1)
  35. await delay(500)
  36. // 3. 第一步声明
  37. await this.doHtmlStep(2)
  38. await delay(500)
  39. // 4. 创建写代码的编辑器
  40. await this.doHtmlStep(3)
  41. await delay(500)
  42. // 5. 准备写编辑器的样式
  43. await this.doHtmlStep(4)
  44. await delay(500)
  45. // 6. 基本样式
  46. await this.doStyleStep(0)
  47. await delay(500)
  48. // 7. 编辑器的样式
  49. await this.doStyleStep(1)
  50. await delay(500)
  51. // 8. 画灯笼的大肚子html
  52. await Promise.all([
  53. this.doHtmlStep(5, 0),
  54. this.doEffectHtmlsStep(5, 0),
  55. ])
  56. await delay(500)
  57. // 8. 画灯笼的大肚子css
  58. await this.doStyleStep(2)
  59. await delay(500)
  60. // 9. 提着灯笼的线html
  61. await Promise.all([
  62. this.doHtmlStep(6, 1),
  63. this.doEffectHtmlsStep(6, 1),
  64. ])
  65. await delay(500)
  66. // 10. 提着灯笼的线css
  67. await this.doStyleStep(3)
  68. await delay(500)
  69. // 11. 给灯笼加两个盖子html
  70. await Promise.all([
  71. this.doHtmlStep(7, 2),
  72. this.doEffectHtmlsStep(7, 2),
  73. ])
  74. await delay(500)
  75. // 12. 给灯笼加两个盖子css
  76. await this.doStyleStep(4)
  77. await delay(500)
  78. // 13. 感觉灯笼快要成了,再给他加上四根线吧html
  79. await Promise.all([
  80. this.doHtmlStep(8, 3),
  81. this.doEffectHtmlsStep(8, 3),
  82. ])
  83. await delay(500)
  84. // 14. 感觉灯笼快要成了,再给他加上四根线吧css
  85. await this.doStyleStep(5)
  86. await delay(500)
  87. // 15. 灯笼是不是还有底部的小尾巴呀html
  88. await Promise.all([
  89. this.doHtmlStep(9, 4),
  90. this.doEffectHtmlsStep(9, 4),
  91. ])
  92. await delay(500)
  93. // 16. 灯笼是不是还有底部的小尾巴呀css
  94. await this.doStyleStep(6)
  95. await delay(500)
  96. // 17. 最后当然少不了送给大家的福啦html
  97. await Promise.all([
  98. this.doHtmlStep(10, 5),
  99. this.doEffectHtmlsStep(10, 5),
  100. ])
  101. await delay(500)
  102. // 18. 最后当然少不了送给大家的福啦css
  103. await this.doStyleStep(7)
  104. await delay(500)
  105. },
  106. methods: {
  107. // 渲染css
  108. doStyleStep (step) {
  109. const cssEditRef = this.$refs.cssEditRef
  110. return new Promise((resolve) => {
  111. // 从css配置文件中取出第n步的样式
  112. const styleStepConfig = styles[ step ]
  113. if (!styleStepConfig) {
  114. return
  115. }
  116. let previewStylesSource = this.previewStylesSource
  117. let start = 0
  118. let timter = setInterval(() => {
  119. // 挨个累加
  120. let char = styleStepConfig.substring(start, start + 1)
  121. previewStylesSource += char
  122. if (start >= styleStepConfig.length) {
  123. console.log('css结束')
  124. clearInterval(timter)
  125. resolve(start)
  126. } else {
  127. this.previewStylesSource = previewStylesSource
  128. // 左边编辑器展示给用户看的
  129. this.styleEditPre = Prism.highlight(previewStylesSource, Prism.languages.css)
  130. // 右边预览区域实际起作用的css
  131. this.previewStyles = `
  132. <style>
  133. ${previewStylesSource}
  134. </style>
  135. `
  136. start += 1
  137. // 因为要不断滚动到底部,简单粗暴处理一下
  138. document.documentElement.scrollTo({
  139. top: 10000,
  140. left: 0,
  141. })
  142. // 因为要不断滚动到底部,简单粗暴处理一下
  143. cssEditRef && cssEditRef.scrollTo({
  144. top: 100000,
  145. left: 0,
  146. })
  147. }
  148. }, 0)
  149. })
  150. },
  151. // 渲染html
  152. doEffectHtmlsStep (step, insertStepIndex = -1) {
  153. // 注意html部分和css部分最大的不同在于后面的步骤是有可能插入到之前的代码中间的,并不是一味地添加到尾部
  154. // 所以需要先找到标识,然后插入
  155. const insertStep = insertStepIndex !== -1 ? `<!-- htmlStep${insertStepIndex} -->` : -1
  156. return new Promise((resolve) => {
  157. const htmlStepConfig = htmls[ step ]
  158. let previewHtmls = this.previewHtmls
  159. const index = previewHtmls.indexOf(insertStep)
  160. const stepInHtmls = index !== -1
  161. let frontHtml = stepInHtmls ? previewHtmls.slice(0, index + insertStep.length) : previewHtmls
  162. let endHtml = stepInHtmls ? previewHtmls.slice(index + insertStep.length) : ''
  163. let start = 0
  164. let chars = ''
  165. let timter = setInterval(() => {
  166. let char = htmlStepConfig.substring(start, start + 1)
  167. // 累加字段
  168. chars += char
  169. previewHtmls = frontHtml + chars + endHtml
  170. if (start >= htmlStepConfig.length) {
  171. console.log('html结束')
  172. clearInterval(timter)
  173. resolve(start)
  174. } else {
  175. // 赋值html片段
  176. this.previewHtmls = previewHtmls
  177. start += 1
  178. }
  179. }, 0)
  180. })
  181. },
  182. // 编辑区域html高亮代码
  183. doHtmlStep (step, insertStepIndex = -1) {
  184. const htmlEditRef = this.$refs.htmlEditRef
  185. const htmlEditPreRef = this.$refs.htmlEditPreRef
  186. // 同上需要找到插入标志
  187. const insertStep = insertStepIndex !== -1 ? `<!-- htmlStep${insertStepIndex} -->` : -1
  188. return new Promise((resolve) => {
  189. const htmlStepConfig = htmls[ step ]
  190. let htmlEditPreSource = this.htmlEditPreSource
  191. const index = htmlEditPreSource.indexOf(insertStep)
  192. const stepInHtmls = index !== -1
  193. // 按照条件拼接代码
  194. let frontHtml = stepInHtmls ? htmlEditPreSource.slice(0, index + insertStep.length) : htmlEditPreSource
  195. let endHtml = stepInHtmls ? htmlEditPreSource.slice(index + insertStep.length) : ''
  196. let start = 0
  197. let chars = ''
  198. let timter = setInterval(() => {
  199. let char = htmlStepConfig.substring(start, start + 1)
  200. chars += char
  201. htmlEditPreSource = frontHtml + chars + endHtml
  202. if (start >= htmlStepConfig.length) {
  203. console.log('html结束')
  204. clearInterval(timter)
  205. resolve(start)
  206. } else {
  207. this.htmlEditPreSource = htmlEditPreSource
  208. // 代码高亮处理
  209. this.htmlEditPre = Prism.highlight(htmlEditPreSource, Prism.languages.html)
  210. start += 1
  211. if (insertStep !== -1) {
  212. // 当要插入到中间时,滚动条滚动到中间,方便看代码
  213. htmlEditRef && htmlEditRef.scrollTo({
  214. top: (htmlEditPreRef.offsetHeight - htmlEditRef.offsetHeight) / 2,
  215. left: 1000,
  216. })
  217. } else {
  218. // 否则直接滚动到底部
  219. htmlEditRef && htmlEditRef.scrollTo({
  220. top: 100000,
  221. left: 0,
  222. })
  223. }
  224. }
  225. }, 0)
  226. })
  227. },
  228. }
  229. }

结尾

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

参考

  1. 过年了~我用CSS画了个灯笼,看着真喜庆
  2. 用原生 js 写一个 “多动症” 的简历