幾個騷操作,讓程式碼自動學會畫畫,太好玩啦!

前端胖頭魚發表於2022-01-19

先睹為快

如下圖,程式碼在自己一行一行寫程式,逐漸畫出一個喜氣燈籠的模樣(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)
      })
    },
  }
}

結尾

馬上就要新年啦!願大家新年快樂,“碼”到成功。

參考

  1. 過年了~我用CSS畫了個燈籠,看著真喜慶
  2. 用原生 js 寫一個 "多動症" 的簡歷

相關文章