feat: shape masks
diff --git a/public/circle.png b/public/circle.png
new file mode 100644
index 0000000..c553805
--- /dev/null
+++ b/public/circle.png
Binary files differ
diff --git a/public/cloud.png b/public/cloud.png
new file mode 100644
index 0000000..c396d7e
--- /dev/null
+++ b/public/cloud.png
Binary files differ
diff --git a/public/drop.png b/public/drop.png
new file mode 100644
index 0000000..71ff363
--- /dev/null
+++ b/public/drop.png
Binary files differ
diff --git a/public/echarts-0.png b/public/echarts-0.png
new file mode 100644
index 0000000..bd95647
--- /dev/null
+++ b/public/echarts-0.png
Binary files differ
diff --git a/public/echarts-1.png b/public/echarts-1.png
new file mode 100644
index 0000000..0238878
--- /dev/null
+++ b/public/echarts-1.png
Binary files differ
diff --git a/public/flash.png b/public/flash.png
new file mode 100644
index 0000000..4eb9af5
--- /dev/null
+++ b/public/flash.png
Binary files differ
diff --git a/public/flower.png b/public/flower.png
new file mode 100644
index 0000000..07ca318
--- /dev/null
+++ b/public/flower.png
Binary files differ
diff --git a/public/heart.png b/public/heart.png
new file mode 100644
index 0000000..7f66ee2
--- /dev/null
+++ b/public/heart.png
Binary files differ
diff --git a/public/hexagonal.png b/public/hexagonal.png
new file mode 100644
index 0000000..6b89415
--- /dev/null
+++ b/public/hexagonal.png
Binary files differ
diff --git a/public/maps-and-flags.png b/public/maps-and-flags.png
new file mode 100644
index 0000000..1779512
--- /dev/null
+++ b/public/maps-and-flags.png
Binary files differ
diff --git a/public/pentagon.png b/public/pentagon.png
new file mode 100644
index 0000000..3bf2a1e
--- /dev/null
+++ b/public/pentagon.png
Binary files differ
diff --git a/public/question-mark.png b/public/question-mark.png
new file mode 100644
index 0000000..09834c9
--- /dev/null
+++ b/public/question-mark.png
Binary files differ
diff --git a/public/rect.png b/public/rect.png
new file mode 100644
index 0000000..1cd8ded
--- /dev/null
+++ b/public/rect.png
Binary files differ
diff --git a/public/rhombus.png b/public/rhombus.png
new file mode 100644
index 0000000..b7459e5
--- /dev/null
+++ b/public/rhombus.png
Binary files differ
diff --git a/public/rounded-rectangle.png b/public/rounded-rectangle.png
new file mode 100644
index 0000000..e5814fe
--- /dev/null
+++ b/public/rounded-rectangle.png
Binary files differ
diff --git a/public/splash.png b/public/splash.png
new file mode 100644
index 0000000..be7877e
--- /dev/null
+++ b/public/splash.png
Binary files differ
diff --git a/public/star.png b/public/star.png
new file mode 100644
index 0000000..3caf69e
--- /dev/null
+++ b/public/star.png
Binary files differ
diff --git a/public/triangle.png b/public/triangle.png
new file mode 100644
index 0000000..dde4164
--- /dev/null
+++ b/public/triangle.png
Binary files differ
diff --git a/public/wow.png b/public/wow.png
new file mode 100644
index 0000000..dbcdb78
--- /dev/null
+++ b/public/wow.png
Binary files differ
diff --git a/src/components/WChart.vue b/src/components/WChart.vue
index f62e6b3..4d5cce5 100644
--- a/src/components/WChart.vue
+++ b/src/components/WChart.vue
@@ -34,6 +34,7 @@
   width: number;
   height: number;
   shape: string;
+  shapeRatio: boolean;
 };
 
 function setLoading(isLoading: boolean) {
@@ -64,37 +65,55 @@
     const range = max - min || 1;
     return Math.random() * range + min;
   }
+  console.log(config?.shapeRatio)
 
-  chart.value!.setOption({
-    backgroundColor: config!.bgColor,
-    series: [
-      {
-        type: 'wordCloud',
-        data: data || [],
-        // gridSize: 0,
-        sizeRange: config?.fontSize,
-        rotationRange: config?.rotate,
-        shape: config?.shape,
-        width: config?.width + '%',
-        height: config?.height + '%',
-        layoutAnimation: true,
-        keepAspect: true,
-        textStyle: {
-          color: (param: any) => {
-            const value = param.value;
-            const h = getHue();
-            const s = getRandom(config?.saturation);
-            const l = getRandom(config?.lightness);
-            const color = Color(`hsl(${h}, ${s}%, ${l}%)`);
-            return color.toString();
+  function render(maskImage?: HTMLImageElement) {
+    chart.value!.setOption({
+      backgroundColor: config!.bgColor,
+      series: [
+        {
+          type: 'wordCloud',
+          data: data || [],
+          gridSize: 4,
+          sizeRange: config?.fontSize,
+          rotationRange: config?.rotate,
+          maskImage,
+          width: config?.width + '%',
+          height: config?.height + '%',
+          layoutAnimation: true,
+          keepAspect: config?.shapeRatio,
+          textStyle: {
+            color: (param: any) => {
+              const value = param.value;
+              const h = getHue();
+              const s = getRandom(config?.saturation);
+              const l = getRandom(config?.lightness);
+              const color = Color(`hsl(${h}, ${s}%, ${l}%)`);
+              return color.toString();
+            }
           }
         }
+      ],
+      textStyle: {
+        fontFamily: config?.fontFamily
       }
-    ],
-    textStyle: {
-      fontFamily: config?.fontFamily
-    }
-  });
+    });
+  }
+
+  let maskImage: HTMLImageElement;
+  if (config) {
+    maskImage = new Image();
+    maskImage.src = config.shape + '.png';
+    maskImage.onload = () => {
+      render(maskImage);
+    };
+    maskImage.onerror = () => {
+      render();
+    };
+  }
+  else {
+    render();
+  }
 }
 
 onMounted(() => {
diff --git a/src/components/WConfig.vue b/src/components/WConfig.vue
index 33d6506..6eb62d4 100644
--- a/src/components/WConfig.vue
+++ b/src/components/WConfig.vue
@@ -6,12 +6,14 @@
         <div
           class="color-palette"
           v-for="palette in colorPalettes"
+          v-bind:key="palette.bgColor"
           :style="{ background: palette.bgColor }"
           @click="useColorPalette(palette)"
         >
           <div
             class="color"
             v-for="color in palette.themeColors"
+            v-bind:key="color"
             :style="{ background: color }"
           ></div>
         </div>
@@ -195,22 +197,29 @@
         <el-col :span="12">
           <h5>遮罩形状</h5>
         </el-col>
-        <el-col :span="12">
-          <el-select
-            v-model="selectedMask"
-            placeholder="请选择遮罩图形"
-            @change="changeMask"
+        <el-col :span="12" class="header-right">
+          <el-checkbox
+            v-model="shapeRatio"
+            @change="change"
           >
-            <el-option
-              v-for="item in masks"
-              :key="item.value"
-              :label="item.name"
-              :value="item.value"
-            >
-            </el-option>
-          </el-select>
+            保持长宽比
+          </el-checkbox>
         </el-col>
       </el-row>
+      <div class="img-select"
+        v-for="item in masks"
+        :key="item.value"
+        :value="item.value"
+        v-bind:class="{ selected: item.value === selectedMask }"
+        v-bind:title="item.name"
+        @click="changeMask(item.value)"
+      >
+        <img v-bind:src="item.value + '.png'" />
+        <div class="mark" v-if="item.isFromFreepik">*</div>
+      </div>
+      <div class="hint">
+        带 * 的形状来自 <a href="https://www.freepik.com" title="Freepik" _target="_blank">Freepik</a>,查看<a href="https://media.flaticon.com/license/license.pdf" _target="_blank">版权</a>
+      </div>
     </el-collapse-item>
   </el-collapse>
 </template>
@@ -249,7 +258,8 @@
 const rotate = ref([-90, 90]);
 const width = ref(90);
 const height = ref(90);
-const selectedMask = ref('circle');
+const selectedMask = ref('heart');
+const shapeRatio = ref(true);
 
 const normalizeFont = (font: string | { name: string; value: string }) => {
   if (typeof font === 'string') {
@@ -312,35 +322,94 @@
 
 const masks = [
   {
-    name: '椭圆',
+    name: 'ECharts 0',
+    value: 'echarts-0'
+  },
+  {
+    name: 'ECharts 1',
+    value: 'echarts-1'
+  },
+  {
+    name: '圆形',
     value: 'circle'
   },
   {
     name: '方形',
-    value: 'square'
+    value: 'rect'
+  },
+  {
+    name: '云',
+    value: 'cloud',
+    isFromFreepik: true
   },
   {
     name: '菱形',
-    value: 'diamond'
+    value: 'rhombus',
+    isFromFreepik: true
   },
   {
     name: '三角形',
-    value: 'triangle'
+    value: 'triangle',
+    isFromFreepik: true
   },
   {
     name: '五边形',
-    value: 'pentagon'
+    value: 'pentagon',
+    isFromFreepik: true
   },
   {
     name: '五角星',
-    value: 'star'
+    value: 'star',
+    isFromFreepik: true
+  },
+  {
+    name: '圆角矩形',
+    value: 'rounded-rectangle',
+    isFromFreepik: true
   },
   {
     name: '爱心',
-    value: 'cardioid'
-    // }, {
-    //     name: '自定义图片',
-    //     value: 'image'
+    value: 'heart',
+    isFromFreepik: true
+  },
+  {
+    name: '六边形',
+    value: 'hexagonal',
+    isFromFreepik: true
+  },
+  {
+    name: '闪电',
+    value: 'flash',
+    isFromFreepik: true
+  },
+  {
+    name: '水滴',
+    value: 'drop',
+    isFromFreepik: true
+  },
+  {
+    name: '六瓣花',
+    value: 'flower',
+    isFromFreepik: true
+  },
+  {
+    name: '喷溅',
+    value: 'splash',
+    isFromFreepik: true
+  },
+  {
+    name: '地标',
+    value: 'maps-and-flags',
+    isFromFreepik: true
+  },
+  {
+    name: '问号',
+    value: 'question-mark',
+    isFromFreepik: true
+  },
+  {
+    name: 'WOW',
+    value: 'wow'
   }
 ];
 
@@ -373,7 +442,8 @@
   change();
 }
 
-function changeMask() {
+function changeMask(value: string) {
+  selectedMask.value = value;
   change();
 }
 
@@ -443,12 +513,16 @@
     rotate: rotate.value,
     width: width.value,
     height: height.value,
-    shape: selectedMask.value === 'image' ? null : selectedMask.value
+    shape: selectedMask.value,
+    shapeRatio: shapeRatio.value,
   };
 }
 </script>
 
 <style lang="scss">
+$brand-color: #409eff;
+$border-color: #e6e6e6;
+
 .title-right {
   position: absolute;
   top: 10px;
@@ -459,6 +533,10 @@
   }
 }
 
+.header-right {
+  text-align: right;
+}
+
 .el-color-picker {
   margin-right: 5px;
 }
@@ -472,9 +550,9 @@
   padding: 3px 6px;
   box-sizing: border-box;
   vertical-align: top;
-  border: 1px solid #e6e6e6;
+  border: 1px solid $border-color;
   border-radius: 4px;
-  color: #409eff;
+  color: $brand-color;
 }
 
 .color-palette {
@@ -498,6 +576,45 @@
   }
 }
 
+.hint {
+  margin: 5px 0;
+  color: #888;
+
+  a {
+    color: $brand-color;
+  }
+}
+
+.img-select {
+  position: relative;
+  display: inline-block;
+  width: 57px;
+  height: 57px;
+  margin-right: 5px;
+  border: 1px solid $border-color;
+  padding: 5px;
+  border-radius: 4px;
+  opacity: 0.5;
+  cursor: pointer;
+
+  &.selected {
+    border-color: $brand-color;
+    opacity: 1;
+  }
+
+  img {
+    max-width: 100%;
+    max-height: 100%;
+  }
+
+  .mark {
+    position: absolute;
+    top: -2px;
+    right: 4px;
+    color: #000;
+  }
+}
+
 .el-checkbox {
   --el-checkbox-font-color: #888;
   margin-top: 7px;
@@ -509,7 +626,7 @@
 
 .el-dropdown-link {
   cursor: pointer;
-  color: #409eff;
+  color: $brand-color;
 }
 
 .el-icon-arrow-down {