0

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

 5 months ago
source link: https://segmentfault.com/a/1190000041305481
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

如下图,代码在自己一行一行写程序,逐渐画出一个喜气灯笼的模样(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的编辑区域,右边是灯笼的展示区域

模板

<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)
      })
    },
  }
}

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK