今天,小程式正式支援 SVG

當耐特發表於2019-04-18

寫在前面

經過騰訊 Omi 團隊的努力,今天你可以在小程式中使用 Cax 引擎高效能渲染 SVG!

SVG 是可縮放向量圖形(Scalable Vector Graphics),基於可擴充套件標記語言,用於描述二維向量圖形的一種圖形格式。它由全球資訊網聯盟制定,是一個開放標準。SVG 的優勢有很多:

  • SVG 使用 XML 格式定義圖形,可通過文字編輯器來建立和修改
  • SVG 影象可被搜尋、索引、指令碼化或壓縮
  • SVG 是可伸縮的,且放大圖片質量不下降
  • SVG 影象可在任何的解析度下被高質量地列印
  • SVG 可被非常多的工具讀取和修改(比如記事本)
  • SVG 與 JPEG 和 GIF 影象比起來,尺寸更小,且可壓縮性、可程式設計星更強
  • SVG 完全支援 DOM 程式設計,具有互動性和動態性

而支援上面這些優秀特性的前提是 - 需要支援 SVG 標籤。比如在小程式中直接寫:

<svg width="300" height="150">
  <rect
    bindtap="tapHandler" height="100" width="100"
    style="stroke:#ff0000; fill: #0000ff">
  </rect>
</svg>
複製程式碼

上面定義了 SVG 的結構、樣式和點選行為。但是小程式目前不支援 SVG 標籤,僅僅支援載入 SVG 之後 作為 background-image 進行展示,如 background-image: url("data:image/svg+xml.......),或者 base64 後作為 background-image 的 url。

那麼怎麼辦呢?有沒有辦法讓小程式支援 SVG? 答案是有的!需要下面這些東西(站在巨人的肩膀上):

  • JSX,史上最強 UI 表示式,支援書寫 XML-Hyperscript 互轉的 JS 語言
  • 小程式內建 Canvas 渲染器
  • Cax 最新渲染引擎
  • HTM,Hyperscript Tagged Markup,可能是 JSX 的替代品或者另一種選擇,使用ES標準的模板語法實現的 Hyperscript 執行時/編譯時生成,preact 作者(也是google工程師)打造

這裡稍微解釋下 Hyperscript:

比如 JSX 中的

<div>
  Hello {this.props.name}
</div>
複製程式碼

或者 js 中的 htm:

html`<div>
  Hello {this.props.name}
</div>`
複製程式碼

最後都會被編譯成:

h(
  "div",
  null,
  "Hello ",
  this.props.name
);
複製程式碼

巢狀的 div 也會變編譯成 h 巢狀 h,比如

<div>
  <div>abc</div>
</div>
複製程式碼

編譯後:

h(
  "div",
  null,
  h(
    "div",
    null,
    "abc"
  )
)
複製程式碼

而 h 函式的定義也是相當簡潔:

function h(type, props, ...children) {
  return { type, props, children }
}
複製程式碼

通過 h 的執行可以 js 中拿到結構化的資料,也就是所謂的虛擬 dom。需要注意的是 htm 有輕微的執行時開銷,jsx 沒有。

一句話總結:

使用小程式內建的 Canvas 渲染器, 在 Cax 中實現 SVG 標準的子集,使用 JSX 或者 HTM 描述 SVG 結構行為表現

直接看在小程式種使用案例:

import { html, renderSVG } from '../../cax/cax'

Page({
  onLoad: function () {

    renderSVG(html`
<svg width="300" height="220">
  <rect bindtap="tapHandler"
  height="110" width="110"
  style="stroke:#ff0000; fill: #ccccff"
  transform="translate(100 50) rotate(45 50 50)">
  </rect>
</svg>`, 'svg-a', this)

  },

  tapHandler: function () {
    console.log('你點選了 rect')
  }
})
複製程式碼

其中的 svg-a 對應著 wxml 裡 cax-element 的 id:

<view class="container">
  <cax-element id="svg-c"></cax-element>
</view>
複製程式碼

宣告元件依賴

{
  "usingComponents": {
    "cax-element":"../../cax/index"
  }
}
複製程式碼

小程式中顯示效果:

今天,小程式正式支援 SVG

在來一個複雜的例子,用 SVG 繪製 Omi 的 logo:

renderSVG(html`
<svg width="300" height="220">
  <g transform="translate(50,10) scale(0.2 0.2)">
   <circle fill="#07C160" cx="512" cy="512" r="512"/>
   <polygon fill="white" points="159.97,807.8 338.71,532.42 509.9,829.62 519.41,829.62 678.85,536.47 864.03,807.8 739.83,194.38 729.2,194.38 517.73,581.23 293.54,194.38 283.33,194.38 "/>
   <circle fill="white" cx="839.36" cy="242.47" r="50"/>
  </g>
</svg>`, 'svg-a', this)
複製程式碼

小程式種顯示效果:

今天,小程式正式支援 SVG

在 omip 和 mps 當中使用 cax 渲染 svg,你可以不用使用 htm。比如在 omip 中實現上面兩個例子:

    renderSVG(
<svg width="300" height="220">
  <rect bindtap="tapHandler"
  height="110" width="110"
  style="stroke:#ff0000; fill: #ccccff"
  transform="translate(100 50) rotate(45 50 50)">
  </rect>
</svg>, 'svg-a', this.$scope)
複製程式碼
renderSVG(
<svg width="300" height="220">
  <g transform="translate(50,10) scale(0.2 0.2)">
   <circle fill="#07C160" cx="512" cy="512" r="512"/>
   <polygon fill="white" points="159.97,807.8 338.71,532.42 509.9,829.62 519.41,829.62 678.85,536.47 864.03,807.8 739.83,194.38 729.2,194.38 517.73,581.23 293.54,194.38 283.33,194.38 "/>
   <circle fill="white" cx="839.36" cy="242.47" r="50"/>
  </g>
</svg>, 'svg-a', this.$scope)
複製程式碼

需要注意的是在 omip 中傳遞的最後一個引數不是 this,而是 this.$scope

在 mps 中,更加徹底,你可以單獨建立 svg 檔案,通過 import 匯入。

//注意這裡不能寫 test.svg,因為 mps 會把 test.svg 編譯成 test.js 
import testSVG from '../../svg/test'
import { renderSVG } from '../../cax/cax'

Page({
  tapHandler: function(){
    this.pause = !this.pause
  },
  onLoad: function () {
    renderSVG(testSVG, 'svg-a', this)
  }
})
複製程式碼

比如 test.svg :

<svg width="300" height="300">
  <rect bindtap="tapHandler" x="0" y="0" height="110" width="110"
         style="stroke:#ff0000; fill: #0000ff" />
</svg>
複製程式碼

會被 mps 編譯成:

const h = (type, props, ...children) => ({ type, props, children });
export default h(
  "svg",
  { width: "300", height: "300" },
  h("rect", {
    bindtap: "tapHandler",
    x: "0",
    y: "0",
    height: "110",
    width: "110",
    style: "stroke:#ff0000; fill: #0000ff"
  })
);
複製程式碼

所以總結一下:

  • 你可以在 mps 中直接使用 import 的 SVG 檔案的方式使用 SVG
  • 你可以直接在 omip 中使用 JSX 的使用 SVG
  • 你可以直接在原生小程式當中使用 htm 的方式使用 SVG

這就完了?遠沒有,看 cax 在小程式中的這個例子:

今天,小程式正式支援 SVG

詳細程式碼:

renderSVG(html`
<svg width="300" height="200">
  <path d="M 256,213 C 245,181 206,187 234,262 147,181 169,71.2 233,18 220,56 235,81 283,88 285,78.7 286,69.3 288,60 289,61.3 290,62.7 291,64 291,64 297,63 300,63 303,63 309,64 309,64 310,62.7 311,61.3 312,60 314,69.3 315,78.7 317,88 365,82 380,56 367,18 431,71 453,181 366,262 394,187 356,181 344,213 328,185 309,184 300,284 291,184 272,185 256,213 Z" style="stroke:#ff0000; fill: black">
    <animate dur="32s" repeatCount="indefinite" attributeName="d" values="......太長,這裡省略 paths........" />
  </path>
</svg>`, 'svg-c', this)
複製程式碼

再試試著名的 SVG 老虎:

今天,小程式正式支援 SVG

path 太長,就不貼程式碼了,可以點選這裡檢視

就這麼多?未完待續...,後續補充:

pasiton 標籤

import { html, renderSVG } from '../../cax/cax'

Page({
  onLoad: function () {


    const svg = renderSVG(html`
<svg width="200" height="200">
  <pasition duration="200" bindtap=${this.changePath} width="100" height="100" from="M28.228,23.986L47.092,5.122c1.172-1.171,1.172-3.071,0-4.242c-1.172-1.172-3.07-1.172-4.242,0L23.986,19.744L5.121,0.88
		c-1.172-1.172-3.07-1.172-4.242,0c-1.172,1.171-1.172,3.071,0,4.242l18.865,18.864L0.879,42.85c-1.172,1.171-1.172,3.071,0,4.242
		C1.465,47.677,2.233,47.97,3,47.97s1.535-0.293,2.121-0.879l18.865-18.864L42.85,47.091c0.586,0.586,1.354,0.879,2.121,0.879
		s1.535-0.293,2.121-0.879c1.172-1.171,1.172-3.071,0-4.242L28.228,23.986z"
    to="M49.1 23.5H2.1C0.9 23.5 0 24.5 0 25.6s0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1C51.2 24.5 50.3 23.5 49.1 23.5zM49.1 7.8H2.1C0.9 7.8 0 8.8 0 9.9c0 1.1 0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1C51.2 8.8 50.3 7.8 49.1 7.8zM49.1 39.2H2.1C0.9 39.2 0 40.1 0 41.3s0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1S50.3 39.2 49.1 39.2z"
    from-stroke="red" to-stroke="green" from-fill="blue" to-fill="red" stroke-width="2" />
</svg>`, 'svg-c', this)

    this.pasitionElement = svg.children[0]

  },

  changePath: function () {
    this.pasitionElement.toggle()
  }
})
複製程式碼

pasiton 提供了兩個 path 和 顏色 相互切換的能力,最常見的場景比如 menu 按鈕和 close 按鈕點選後 path 的變形。

舉個例子,看顏色和 path 同時變化:

今天,小程式正式支援 SVG

線性運動

這裡舉一個在 mps 中使用 SVG 的案例:

import { renderSVG, To } from '../../cax/cax'

Page({
  tapHandler: function(){
    this.pause = !this.pause
  },

  onLoad: function () {
    const svg = renderSVG(html`
    <svg width="300" height="300">
     <rect bindtap="tapHandler" x="0" y="0" height="110" width="110"
            style="stroke:#ff0000; fill: #0000ff" />
    </svg>`
    , 'svg-a', this)
    const rect = svg.children[0]
    rect.originX = rect.width/2
    rect.originY = rect.height/2
    rect.x = svg.stage.width/2
    rect.y = svg.stage.height/2
    this.pause = false
    this.interval = setInterval(()=>{
      if(!this.pause){
        rect.rotation++
        svg.stage.update()
      }
    },15)
})
複製程式碼

效果如下:

今天,小程式正式支援 SVG

組合運動

import { renderSVG, To } from '../../cax/cax'

Page({
  onLoad: function () {

    const svg = renderSVG(html`
    <svg width="300" height="300">
     <rect bindtap="tapHandler" x="0" y="0" height="110" width="110"
            style="stroke:#ff0000; fill: #0000ff" />
    </svg>`
    ,'svg-a', this)
    const rect = svg.children[0]
    rect.originX = rect.width/2
    rect.originY = rect.height
    rect.x = svg.stage.width/2
    rect.y = svg.stage.height/2

    var sineInOut = To.easing.sinusoidalInOut
    To.get(rect)
        .to().scaleY(0.8, 450, sineInOut).skewX(20, 900, sineInOut)
        .wait(900)
        .cycle().start()
    To.get(rect)
        .wait(450)
        .to().scaleY(1, 450, sineInOut)
        .wait(900)
        .cycle().start()
    To.get(rect)
        .wait(900)
        .to().scaleY(0.8, 450, sineInOut).skewX(-20, 900, sineInOut)
        .cycle()
        .start()
    To.get(rect)
        .wait(1350)
        .to().scaleY(1, 450, sineInOut)
        .cycle()
        .start()

      setInterval(() => {
          rect.stage.update()
      }, 16)
  }
})
複製程式碼

效果如下:

今天,小程式正式支援 SVG

其他

  • vscode 安裝 lit-html 外掛使 htm 的 html內容 高亮
  • 還希望小程式 SVG 提供什麼功能可以開 issues告訴我們,評估後通過,我們去實現!
  • Cax SVG Github
  • 參考文件

相關文章