主頁 > 軟體設計 > 【前端開發】Vue + Fabric.js + Element-plus 實作簡易的H5可視化圖片編輯器

【前端開發】Vue + Fabric.js + Element-plus 實作簡易的H5可視化圖片編輯器

2021-08-13 08:02:43 軟體設計

目錄

  • 前言
  • 一 、實戰效果
    • 技術選型
    • 核心功能
    • 代碼實作
  • 二、Fabric.js 簡介
    • 安裝
    • 創建畫布
    • 監聽畫布事件
    • 滑鼠事件監聽
    • 設定畫布背景
    • 設定背景顏色
    • 向畫布添加圖層物件
    • 獲取當前選中的物件
    • 控制圖層層級
    • 將畫布匯出成圖片
    • 下載為圖片
    • 畫布狀態記錄
    • 清除物件/圖層
    • 清除畫布

前言

canves 繪圖的運用是前端必知必會的基礎,但是canves 本身并不好用,而fabric.js是基于canves的一個繪圖框架庫,用起來可比canves友好多了!接下來帶你看看案例和初識fabric.js~

一 、實戰效果

編輯前:
請添加圖片描述
編輯中:
在這里插入圖片描述

技術選型

  • Vue3
  • Element-plus
  • Fabric.js
  • ES6

核心功能

主要實作H5 移動端簡單的圖片的可視化編輯、預覽、下載等功能,

  • 文本編輯:實作畫布上的文本框的添加、字體屬性的設定、樣式風格編輯、背景顏色個性化設定、文本內容的設定等;
  • 圖片上傳:點擊上傳圖片,實作圖片的讀取并添加到畫布編輯區上,自由設定背景圖片;
  • 繪制圖片:實作根據滑鼠坐標自由繪制圖形(目前僅實作矩形,其他類似,可參考fabric 檔案說明自己實作);
  • 洗掉控制元件:編輯區上的控制元件可自由洗掉;
  • 預覽圖片:編輯完成后可預覽自己編輯的圖片效果,并在右側100%顯示;
  • 下載圖片:可下載編輯好的圖片;
  • 畫布縮放:可放大或者縮小畫布,畫布上的內容同步縮放;
  • 一鍵清空:重置實作畫布一鍵清空;

目前僅實作以上的功能,后續在繼續完善更新~~

代碼實作

editor.vue:

<template>
  <el-container >
    <el-header >
      <el-menu
        style="background: #6A60E3"
        mode="horizontal"
      >
        <div style="width: 400px; float: left;height:70px;color: #fff; background: #5951B6;line-height: 70px">
          <h1 style="text-align: center">{{ templatesTitle }}</h1>
        </div>
        <el-form inline="inline" style="float: right; width: 1000px" >
          <el-button
              class="el-icon-plus btn_style" style="width: 140px"
              @click="addTextHandle('textbox', 'add')"
          > 添加文本框
          </el-button>
          <el-button class="el-icon-plus btn_style" @click="imgDraw" >
            <input type="file" accept="image/*"
                style="display:none"
                id="uploadfile"
                @change="uploadFile" />
            上傳圖片
          </el-button>
          <el-button class="el-icon-edit btn_style"
              @click="initD"
          >   繪 制 </el-button>
          <el-button
            class="btn-delete btn_style el-icon-delete"
            @click="deleteText"
          > 洗掉控制元件
          </el-button>
        </el-form>
      </el-menu>
    </el-header>
    <el-container>
      <el-aside :width="isCollapsed ? '30px' : '300px'">
        <div class="text-edit">
          <div
            @click="closeSetting"
            class="title-setting"
            :tipTitle="isCollapsed ? '展開設定' : '收起設定'"
          >
            <span class="text-setting">{{ title }}</span>
            <i :class="isCollapsed ? 'el-icon-s-unfold' : 'el-icon-close'"/>
          </div>
          <el-form ref="form" >
            <el-form-item label="文本內容:">
              <div id="textBox"  style="width: 100% ">
                <el-input type="textarea"
                    @input="changeText"
                    @focus="changeText"
                    id="in" ref="in"
                    v-model="msg" ></el-input>
              </div>
            </el-form-item>
            <el-form-item label="字體:" label-width="82px">
              <el-select  v-model="fontFamilies.value"
                  placeholder="請選擇字體"
                  @change="changeFontFamily">
                <el-option
                  v-for="item in fontFamilies"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value">
                </el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="字體顏色:" >
              <el-select v-model="fontColor.value"
              placeholder="請選擇字體顏色"
                  @change="changeFontColor"
              >
                <el-option
                  v-for="item in fontColor"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value">
                </el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="字體粗細:" >
              <el-select v-model="fontWeight.value"
              placeholder="請選擇字體粗細"
                  @change="changefontWeight"
              >
                <el-option
                  v-for="item in fontWeight"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value">
                </el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="字體風格:" >
              <el-select v-model="fontStyle.value"
              placeholder="請選擇字體風格"
                  @change="changeFontStyle">
                <el-option
                  v-for="item in fontStyle"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value">
                </el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="字體大小:" >
              <el-select v-model="fontSizes.value"
              placeholder="選擇字體大小"
                  @change="changeFontSize">
                <el-option
                  v-for="item in fontSizes"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value">
                </el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="對齊方式:" >
              <el-select v-model="textAlign.value"
                  @change="changeTextAlign"
                  placeholder="選擇對齊方式">
                <el-option
                  v-for="item in textAlign"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                ></el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="背景顏色:" >
              <el-color-picker
                class="color-picker"
                v-model="bgcolor" circle
                size="small"
                show-alpha
                :predefine="predefineColors"
                @change="changeBgColor"
                color-format="hex"
              ></el-color-picker>
            </el-form-item>
            <el-form-item>
<!--              <ul class="template-edit" id="template-edit" @click="chooseTemplate">
                <li><img src="@/assets/logo.png" alt="" id="bg1"  style="width: 100px;height: 50px" /></li>
                <li><img src="@/assets/logo.png" alt="" id="bg2"  style="width: 100px;height: 50px" /></li>
                <li><img src="@/assets/logo.png" alt="" id="bg3"  style="width: 100px;height: 50px" /></li>
              </ul>-->
            </el-form-item>
          </el-form>
        </div>
      </el-aside>
      <el-main style="height: 1040px" >
        <div class="content-show">
          <div class="canvas">
          <canvas ref="canvas" id="editorCanvas"></canvas>
        </div>
        </div>
        <el-form class="handleSave">
          <el-button type="primary" class="btn-save" @click="downLoadImage1">預覽圖片</el-button>
          <el-button type="primary" class="btn-download" @click="downLoad">下載圖片</el-button>
          <el-form-item inline="inline" class="btn-zoom" style="margin: 10px 0px">
            <i class="el-icon-caret-left" @click="zoomIt(0.8)"></i>
            <span> {{ zoomCounter }} % </span>
            <i class="el-icon-caret-right" @click="zoomIt(1.2)"></i>
          </el-form-item>
          <el-button type="danger" class="btn-reset" @click="resetCanvas">重 置</el-button>
        </el-form>
      </el-main>
      <el-aside :width="isCollapsed ? '30px' : '375px'">
        <div
          @click="closeSetting"
          class="title-setting"
          :tipTitle="isCollapsed ? '展開設定' : '收起設定'"
        >
          <span class="text-setting">{{ showTitle }}</span>
          <i :class="isCollapsed ? 'el-icon-s-unfold' : 'el-icon-close'"/>
        </div>
        <img :src="imageBase64" alt="">
      </el-aside>
    </el-container>
  </el-container>
</template>

<script>

  import {
    fontFamilies,
    fontSizes,
    fontColor,
    fontStyle,
    fontWeight,
    backgroundColor,
    textAlign,
  } from "@/utils/fontData";

  import { fabric } from "fabric";
  let editorCanvas = "";

  fabric.Object.prototype.set({
    cornerStrokeColor: "#66b0ef",
    cornerColor: "#60abec",
    cornerStyle: "rectangele",
    cornerSize: 8,
    borderScaleFactor: 2,
    transparentCorners: false,
    borderColor: "#61abe8",
  });

  export default {
    name: "Editor",
    data() {
      return {
        isHide: true,
        checkAll: false,
        isChecked:false,
        isIndeterminate: true,
        fontFamilies,
        fontSizes,
        fontColor,
        fontStyle,
        fontWeight,
        textAlign,
        backgroundColor,
        predefineColors: ["#ffffff", "#FF0000", "#000000","#FFF800","#00FF0A","#FD00FF","#0095FF"],
        checked: true,
        // 模板圖片保存陣列
        templateImgs: [],
        zoomCounter: 100,
        tipTitle: "",
        imageBase64: "",
        isCollapsed: false,
        backColor: "#eec5c5",
        backGroundStatus: false,
        bgcolor: "#ff0000",
        itemWidth: "230px",
        done: false,
        // 文本控制元件屬性
        textStyleData: {
          type: "editText",
          text: "雙擊編輯文字",
          top: 50,
          left: 50,
          width: 100,
          opacity: 1,
          stroke: "#ffffff",
          strokeWidth: 0,
          textAlign: "left",
          lineHeight: 1,
          charSpacing: 1,
          fontFamily: "hyzktjjkt",
          fontSize: 40,
          fontWeight: "normal",
          fontStyle: "normal",
          fill: "#000000",
          textBackgroundColor: "rgba(0,0,0,0)",
          selectable: true,
        },
        isOpen: false,
        isMove: false,
        src: "",
        msg: "",
        canvas: null,
        templatesTitle: "H5 可視化編輯",
        title: "文本設定",
        showTitle:"100% 預覽",
        templateData: {},
        mouseFrom: {},
        mouseTo: {},
        moveCount: 1,
      };
    },
    mounted() {
      this.initeditorCanvas();
    },
    methods: {
      downLoadImage1() {
        this.done = true
        let base64URl = editorCanvas.toDataURL({
          formart: 'png',
          multiplier: 1
        })
        this.imageBase64 = base64URl
        this.done = false
      },

      saveTemplates() {
        console.log("你點擊了模板保存");
        let base64URl = editorCanvas.toDataURL({
          formart: "jpg",
          multiplier: 1,
        });
      },

      addTemplates() {
        console.log("添加模板");
      },

      initD() {
        // 監聽滑鼠按下
        const obj = editorCanvas.getActiveObject();
        editorCanvas.on("mouse:down", (options) => {
          // 記錄當前滑鼠的起點坐標
          if(!obj) {
            this.mouseFrom.x = options.e.clientX - editorCanvas._offset.left;
            this.mouseFrom.y = options.e.clientY - editorCanvas._offset.top;
          }
        });
        // 監聽滑鼠移動
        editorCanvas.on("mouse:move", (options) => {
          // 記錄當前滑鼠移動終點坐標
          if (!obj) {
            this.mouseTo.x = options.e.clientX - editorCanvas._offset.left
            this.mouseTo.y = options.e.clientY - editorCanvas._offset.top
            this.drawRect();
          }
        });
        editorCanvas.on("mouse:up", (options) => {
          if (!obj) {
            this.mouseFrom.x = options.e.clientX - editorCanvas._offset.left;
            this.mouseFrom.y = options.e.clientY - editorCanvas._offset.top;
            this.doDrawing = false;
            this.canvasObject = null;
            this.mouseFrom = {};
            this.mouseTo = {}
          }
        });
        editorCanvas.on("selection:created",(option) => {
          if (option) {
            this.doDrawing = false;
          }
        })
      },
      getTransformedPosX(x) {
        let zoom = Number(editorCanvas.getZoom())
        return (x - editorCanvas.viewportTransform[4]) / zoom;
      },
      getTransformedPosY(y) {
        let zoom = Number(editorCanvas.getZoom())
        return (y - editorCanvas.viewportTransform[5]) / zoom;
      },
      // 繪制矩形
      drawRect() {
        // 計算矩形長寬
        let left = this.getTransformedPosX(this.mouseFrom.x);
        let top = this.getTransformedPosY(this.mouseFrom.y);
        let width = this.mouseTo.x - this.mouseFrom.x;
        let height = this.mouseTo.y - this.mouseFrom.y;
        const canvasObject = new fabric.Rect({
          left: left,
          top: top,
          width: width,
          height: height,
          fill: "#d70202",
          strokeWidth: 2,
        });
        editorCanvas.add(canvasObject);
      },

      // 初始化模板編輯畫布
      initeditorCanvas() {
        editorCanvas = new fabric.Canvas("editorCanvas", {
          devicePixelRatio: true,
          width: "375",
          height: "667",
          originX: "center",
          originY: "center",
          backgroundColor: "#ffffff",
          transparentCorners: false,
        });
        editorCanvas.preserveObjectStacking = true;
      },

      // 收起文本設定
      closeSetting() {
        this.isCollapsed = !this.isCollapsed;
      },

      // 載入圖片
      imgDraw() {
        document.getElementById("uploadfile").click();
      },

      uploadFile(e) {
        editorCanvas.isDrawingMode = false;
        let file = e.target.files[0];
        let reader = new FileReader();
        reader.onload = (e) => {
          let data = e.target.result;
          fabric.Image.fromURL(data, (img) => {
            editorCanvas.add(img).renderAll();
          });
        };
        reader.readAsDataURL(file);
        e.target.value = "";
      },

      // 下載圖片
      downLoad() {
        this.done = true;
        const dataURL = editorCanvas.toDataURL({
          width: editorCanvas.width,
          height: editorCanvas.height,
          left: 0,
          top: 0,
          format: "png",
        });
        const link = document.createElement("a");
        link.download = "圖片.png";
        link.href = dataURL;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
      },

      // 清慷訓布
      resetCanvas() {
        let children = editorCanvas.getObjects();
        if (children.length > 0) {
          editorCanvas.remove(...children);
        }
        editorCanvas.setBackgroundColor("#fff");
      },

      // 縮放
      zoomIt(factor) {
        let zoomCounter = this.zoomCounter;
        let cWidth = editorCanvas.width;
        let cHeight = editorCanvas.height;

        /* 同步縮小 */
        if (factor < 1 && zoomCounter > 0) {
          this.zoomCounter -= 20;
          editorCanvas.setWidth(cWidth * factor);
          editorCanvas.setHeight(cHeight * factor);

          const objects = editorCanvas.getObjects();
          for (let i in objects) {
            let scaleX = objects[i].scaleX;
            let scaleY = objects[i].scaleY;
            let left = objects[i].left;
            let top = objects[i].top;
            let tempScaleX = scaleX * factor;
            let tempScaleY = scaleY * factor;
            let tempLeft = left * factor;
            let tempTop = top * factor;
            objects[i].scaleX = tempScaleX;
            objects[i].scaleY = tempScaleY;
            objects[i].left = tempLeft;
            objects[i].top = tempTop;
            objects[i].setCoords();
            let zoomPoint = new fabric.Point(
              editorCanvas.width / 2,
              editorCanvas.height / 2
            );
            editorCanvas.zoomToPoint(zoomPoint, factor);
            editorCanvas.renderAll();
            editorCanvas.calcOffset();
          }
        }

        /* 同步放大 */
        if (factor > 1 && zoomCounter < 100) {
          this.zoomCounter += 20;
          editorCanvas.setWidth(cWidth * factor);
          editorCanvas.setHeight(cHeight * factor);
          const objects = editorCanvas.getObjects();
          for (let i in objects) {
            let scaleX = objects[i].scaleX;
            let scaleY = objects[i].scaleY;
            let left = objects[i].left;
            let top = objects[i].top;
            let tempScaleX = scaleX * factor;
            let tempScaleY = scaleY * factor;
            let tempLeft = left * factor;
            let tempTop = top * factor;
            objects[i].scaleX = tempScaleX;
            objects[i].scaleY = tempScaleY;
            objects[i].left = tempLeft;
            objects[i].top = tempTop;
            objects[i].setCoords();
          }
          let zoomPoint = new fabric.Point(
            editorCanvas.width / 2,
            editorCanvas.height / 2
          );
          editorCanvas.zoomToPoint(zoomPoint, factor);
          editorCanvas.renderAll();
          editorCanvas.calcOffset();
        } else {
          return;
        }
      },

      // 洗掉當前滑鼠活動控制元件
      deleteText() {
        const obj = editorCanvas.getActiveObject();
        if (obj) {
          editorCanvas.remove(obj);
        }
        editorCanvas.renderAll();
      },

      // 更改字體大小
      changeFontSize(value) {
        let mfontSize = value;
        const obj = editorCanvas.getActiveObject();
        if (obj) {
          obj.set({
            fontSize: mfontSize,
          });
          editorCanvas.renderAll();
        }
        this.templateData.fontSize = mfontSize;
      },

      // 背景顏色
      changeBgColor(value) {
        let mbgColor = value;
        const obj = editorCanvas;
        if (obj) {
          obj.set({
            backgroundColor: mbgColor,
          });
          editorCanvas.renderAll();
        }
        this.templateData.bgColor = mbgColor;
      },

      // 字體顏色
      changeFontColor(value) {
        let mfontColor = value;
        const obj = editorCanvas.getActiveObject();
        if (obj) {
          obj.set({
            fill: mfontColor,
          });
          editorCanvas.renderAll();
        }
        this.templateData.textColor = mfontColor;
      },

      // 對齊方式
      changeTextAlign(value) {
        let mtextAlign = value;
        const obj = editorCanvas.getActiveObject();
        if (obj) {
          obj.set("textAlign", mtextAlign);
          obj.centerH();
          editorCanvas.renderAll();
        }
        this.templateData.horizontalAlign = mtextAlign;
      },

      // 字體粗細
      changefontWeight(value) {
        let mfontWeight = value;
        const obj = editorCanvas.getActiveObject();
        if (obj) {
          obj.set("fontWeight", mfontWeight);
          editorCanvas.renderAll();
        }
        this.templateData.fontWeight = mfontWeight;
      },

      // 字體風格
      changeFontStyle(value) {
        let mfontStyle = value;
        const obj = editorCanvas.getActiveObject();
        if (obj) {
          obj.set("fontStyle", mfontStyle);
          editorCanvas.renderAll();
        }
        this.templateData.fontStyle = mfontStyle;
      },
      
      // 圖片預覽
      downLoadImage() {
        let base64URl = editorCanvas.toDataURL({
          formart: "png",
          multiplier: 1,
        });
        this.imageBase64 = base64URl;
        this.done = false;
      },
    },
    ......
    
    components: {},
    created() {},
  };
</script>

<style scoped>
  * {
    margin: 0;
    padding: 0;
  }
  body {
    font-family: 'PingFang SC', "Helvetica Neue", Helvetica, "microsoft yahei", arial, STHeiTi, sans-serif;
  }

  a {
    text-decoration: none
  }

  .el-aside {
    background-color: #F3F3F3;
    color: #333;
    text-align: center;
  }

  .el-main {
    background-color: #EEEEEE;
    color: #333;
    text-align: center;
    line-height: 160px;
  }

  .el-main .content-show {
    display: flex;
    flex-direction: column;
  }

  .text-edit h6 {
    height: 40px;
    color: black;
  }

  .text-edit .el-form {
    margin: 15px;
  }


  .btn-delete {
    min-height: 36px;
  }

  .template-content {
    padding: 0px;
    margin: 0px;
  }

  ton-box:hover {
    color: white;
    opacity: 1;
  }

  .templateContent {
    width: 100%;
    padding: 0px;
    background-color: #fff;
  }
  
  .template-upload {
    width: 256px;
    height: 130px;
    border: 2px solid #adadad;
    margin: 10px auto;
    line-height: 130px;
    text-align: center;
    cursor: pointer;
  }
  
  .template-img {
    width: 300px;
    height: 130px;
    margin: 0px auto;
    line-height: 130px;
    text-align: center;
    cursor: pointer;
  }

  #editorCanvas {
    box-shadow: 0 0 25px #cac6c6;
    width: 100%;
    display: block;
    margin: 15px auto;
    height: 100%;
  }

  .text-setting {
    text-align: center;
    width: 80px;
    font-size: 18px;
    font-weight: 500;
    margin-right:170px;
  }

  .text-edit h4,
  .side-right h4,
  .show h1 {
    text-align: center;
    font-size: 16px;
    color: #585858;
    font-weight: normal;
    margin: 15px auto;
  }

  .show h1 {
    text-align: center;
    color: #585858;
    padding: 15px;
    font-weight: bold;
    font-size: 24px;
    margin: 15px auto;
  }
  
  .btn-zoom {
    display: inline-block;
    line-height: 1;
    height: 40px;
    width: 104px;
    white-space: nowrap;
    -webkit-appearance: none;
    text-align: center;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    outline: 0;
    margin: 0;
    -webkit-transition: 0.1s;
    transition: 0.1s;
    font-weight: 500;
    font-size: 14px;
    border-radius: 4px;
    color: #fff;
    background-color: #6A60E3;
    border-color: #6A60E3;
  }

  .btn-download,
  .btn-save,
  .btn-load,
  .btn-reset {
    display: inline-block;
    line-height: 1;
    height: 40px;
    width: 104px;
    white-space: nowrap;
    -webkit-appearance: none;
    text-align: center;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    outline: 0;
    margin: 0;
    -webkit-transition: 0.1s;
    transition: 0.1s;
    font-weight: 500;
    padding: 12px 20px;
    font-size: 14px;
    border-radius: 4px;
    color: #fff;
    background-color: #6A60E3;
    margin: 10px 0px 0px 10px;
    border-color: #6A60E3;
  }

  .btn-reset {
    height: 40px;
    color: #fff;
    width: 98px;
    background-color: #6A60E3;
    margin: 10px 0px 0px 10px;
    border-color:  #6A60E3;
  }

  .el-form-item {
    margin: 15px auto;
  }

  .el-form {
    height: 70px;
    line-height: 70px;
  }

  .el-header {
    height: 60px;
  }

  .el-form--inline .el-form-item__content {
    margin: 0;
    padding: 0;
    height: 60px;
  }

  .btn_style {
    display: inline-block;
    height: 40px;
    margin-left: 15px;
    width: 120px;
    line-height: 40px;
    white-space: nowrap;
    -webkit-appearance: none;
    text-align: center;
    font-weight: 500;
    background: none;
    padding: 0px 20px 20px;
    font-size: 14px;
    border-radius: 4px;
    color: #fff;
  }

</style>


文本屬性資料:fontData.js

export const fontFamilies = [
    { label: '宋體', value: "'sans-serif, cursive"  },
    { label: '黑體', value: "'SimHei', cursive" },
    { label: '微軟雅黑', value: "'Microsoft Yahei', cursive" },
    { label: '微軟正黑體', value: "'Microsoft JhengHei', cursive" },
    { label: '楷體', value: "'KaiTi', cursive" },
    { label: '仿宋', value: "'FangSong', cursive" },
    { label: '蘋方', value: "'PingFang SC', cursive" },
    { label: '華文黑體', value: "'STHeiti', cursive" },
    { label: '隸書', value: "'LiSu', cursive" },
    { label: 'Yatra One', value: "'Yatra One', cursive" }
];

export const fontSizes = [
    { label: '9', value: 9 },
    { label: '10', value: 10 },
    { label: '12', value: 12 },
    { label: '14', value: 14 },
    { label: '16', value: 16 },
    { label: '18', value: 18 },
    { label: '20', value: 20 },
    { label: '24', value: 24 },
    { label: '28', value: 28 },
    { label: '32', value: 32 },
    { label: '36', value: 36 },
    { label: '40', value: 40 },
    { label: '42', value: 42 },
    { label: '50', value: 50 },
    { label: '60', value: 64 },
    { label: '72', value: 72 },
    { label: '82', value: 82 },
    { label: '90', value: 90 },
    { label: '96', value: 96 }
];

export const fontStyle = [
        { value: 'normal', label: '正常' },
        { value: 'italic', label: '斜體' }
];

export const  fontColor = [
    {  label: '黑色', value: 'block' },
    {  label: '白色', value: 'white' },
    {  label: '紅色', value: 'red' },
    {  label: '綠色', value: 'green' },
    {  label: '藍色', value: 'blue' },
    {  label: '紫色', value: 'purple' }
];

export const fontWeight = [
    { value: 'normal', label: '正常' },
    { value: 'bold',   label: '粗體' },
    { value: 'lighter', label: '細體' },
    { value: '100', label: '100' },
    { value: '200', label: '200' },
    { value: '300', label: '300' },
    { value: '400', label: '400' },
    { value: '500', label: '500' },
    { value: '600', label: '600' },
    { value: '700', label: '700' },
    { value: '800', label: '800' },
    { value: '900', label: '900' }
];

export const backgroundColor = [
    {  label: '黑色', value: 'block' },
    {  label: '白色', value: 'white' },
    {  label: '紅色', value: 'red' },
    {  label: '綠色', value: 'green' },
    {  label: '藍色', value: 'blue' },
    {  label: '紫色', value: 'purple' }
];

export const textAlign = [
    { label: '居中', value: 'center' },
    { label: '兩端對齊', value: 'justify' }
/*    { label: '左對齊', value: 'left' },
    { label: '右對齊', value: 'right' },*/
];

vue專案中main.js:

import { createApp } from 'vue'
import ElementPlus from 'element-plus';
import 'element-plus/lib/theme-chalk/index.css';
import App from './App.vue';

const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')

代碼有點長,沒有全部放上去,需要原始碼的下面留言!

二、Fabric.js 簡介

Fabric.js是一個可以簡化Canvas程式撰寫的canvas繪圖庫,
fabric.js官網:http://fabricjs.com/

Fabric.js可以做很多事情:

  • 在Canvas上創建、填充圖形(包括圖片、文字、規則圖形和復雜路徑組成圖形);
  • 給圖形填充漸變顏色;
  • 組合圖形(包括組合圖形、圖形文字、圖片等);
  • 設定圖形影片集用戶互動;
    等等

安裝

vue專案里,通過npm 安裝:

npm install fabric --save

html檔案的可以通過通過cdn參考:

<script src="http://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.6/fabric.min.js"></script>

創建畫布

html 結構里,創建一個canves,并設定id

<canvas ref="canvas" id="editorCanvas"></canvas>

初始化fabric的canvas物件:

      // 初始化模板編輯畫布
      initeditorCanvas() {
       const  editorCanvas = new fabric.Canvas("editorCanvas", {
          devicePixelRatio: true,
          width: "375",
          height: "667",
          originX: "center",
          originY: "center",
          backgroundColor: "#ffffff",
          transparentCorners: false,
        });
        editorCanvas.preserveObjectStacking = true;
      },

以上canves 畫布就創建好了,可以在畫布上操作添加內容了,

監聽畫布事件

object:added 添加圖層
object:modified 編輯圖層
object:removed 移除圖層
selection:created 初次選中圖層
selection:updated 圖層選擇變化
selection:cleared 清空圖層選中

滑鼠事件監聽

        // 監聽滑鼠按下
        const obj = editorCanvas.getActiveObject();
        editorCanvas.on("mouse:down", (options) => {
          // 記錄當前滑鼠的起點坐標
          if(!obj) {
            this.mouseFrom.x = options.e.clientX - editorCanvas._offset.left;
            this.mouseFrom.y = options.e.clientY - editorCanvas._offset.top;
          }
        });
        // 監聽滑鼠移動
        editorCanvas.on("mouse:move", (options) => {
          // 記錄當前滑鼠移動終點坐標
          if (!obj) {
            this.mouseTo.x = options.e.clientX - editorCanvas._offset.left
            this.mouseTo.y = options.e.clientY - editorCanvas._offset.top
            this.drawRect();
          }
        });
        // 滑鼠抬起
        editorCanvas.on("mouse:up", (options) => {
          if (!obj) {
            this.mouseFrom.x = options.e.clientX - editorCanvas._offset.left;
            this.mouseFrom.y = options.e.clientY - editorCanvas._offset.top;
            this.doDrawing = false;
            this.canvasObject = null;
            this.mouseFrom = {};
            this.mouseTo = {}
          }
        });
        editorCanvas.on("selection:created",(option) => {
          if (option) {
            this.doDrawing = false;
          }
        })

設定畫布背景

  1. 通過背景圖片的方式設定:
// 讀取圖片地址,設定畫布背景
let imgScanle = 50;
fabric.Image.fromURL('xx/xx/bg.jpg', (img) => {
  img.set({
   // 通過scale來設定圖片大小,這里設定和畫布一樣大
    scaleX: 400,
    scaleY: 400,
    originX: 'left',
    originY: 'top'
  });
  // 設定背景
  editorCanvas.setBackgroundImage(img,editorCanvas.renderAll.bind(editorCanvas));
  editorCanvas.renderAll(); //重新渲染畫布
});
  1. fabric圖片物件的方式:
let img = new Image()
img.src = '../img/pic' //圖片路徑
img.onload = function () {
  let _img = new fabric.Image(img)
  canves.setBackgroundImage(_img, canves.renderAll.bind(canves), {
    left: 0,
    top: 0,
    scaleX: imgScale,
    scaleY: imgScale,
    originX: 'left',
    originY: 'top'
  })
}

設定背景顏色

通過backgroundColor設定背景顏色:

editorCanvas.set({
   backgroundColor: '#000',
 });
editorCanvas.renderAll();

向畫布添加圖層物件

Fabric.js 里有 Image,Textbox,Group,Rect,Circle,Line,Ellipse,Polygon,Polyline等物件,可以實作物件內容的添加:

// 添加Textbox文本框
const textbox = new fabric.Textbox('我是TextBox', {
    left: 50,
    top: 50,
    width: 150,
    textAlign: "left", // 對齊方式
    lineHeight: 1,  // 字體行高
    charSpacing: 1,  // 字體間距
    fontFamily: "hyzktjjkt",  // 字體
    fontSize: 40, // 字體大小
    fontWeight: 'blod', // 字體粗細
    fill: 'red', // 字體顏色
    fontStyle: 'italic',  // 斜體
    fontFamily: 'Delicious', // 設定字體
    stroke: 'green', // 描邊顏色
    strokeWidth: 3, // 描邊寬度
    hasControls: false,
    borderColor: 'red',  // 字體描邊顏色
    type: "editText",  // 型別
});
editorCanvas.add(textbox);  // 通過add方法把物件添加到畫布上

獲取當前選中的物件

// 獲取當前選中的物件
this.selectedObj = editorCanves.getActiveObject();
 
// 獲取選中的圖層
editorCanves.on('selection:created', (e) => {
    this.selectedObj = e.target
})

控制圖層層級

向畫布添加圖層,默認是依次往上疊加,但是當你選中一個圖層進入active狀態時,該圖層會默認置于頂層:

// 上移圖層
this.selectedObj.bringForward();
 
// 下移圖層
this.selectedObj.sendBackwards();
 
// 通過canvas物件的moveTo方法,移至圖層到指定位置
editorCanves.moveTo(object, index);

禁止選中圖層時指定:

// 在畫布初始化后設定
editorCanves.preserveObjectStacking = true // 禁止選中圖層時自定置于頂部

將畫布匯出成圖片

      // 圖片預覽
      downLoadImage() {
        let base64URl = editorCanvas.toDataURL({
          formart: "png",
          multiplier: 1,
        });
        this.imageBase64 = base64URl;
        this.done = false;
      },

下載為圖片

      // 下載圖片
      downLoad() {
        this.done = true;
        const dataURL = editorCanvas.toDataURL({
          width: editorCanvas.width,
          height: editorCanvas.height,
          left: 0,
          top: 0,
          format: "png",
        });
        const link = document.createElement("a");
        link.download = "圖片.png";
        link.href = dataURL;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
      },

畫布狀態記錄

框架提供了如 toJSON 和 loadFromJSON 方法,作用分別為匯出當前畫布的json資訊,加載json畫布資訊來還原畫布狀態,

// 匯出當前畫布資訊
const currState = editorCanves.toJSON(); 

清除物件/圖層

// 移除某個選中的物件
editorCanves.remove(this.selectedObj) 
editorCanves.renderAll() // 重新渲染

// 清除畫布上的所有物件內容
editorCanves.clear();
editoeCanves.renderAll();

清除畫布

      // 清慷訓布
      resetCanvas() {
        let children = editorCanvas.getObjects();
        if (children.length > 0) {
          editorCanvas.remove(...children);
        }
        editorCanvas.setBackgroundColor("#fff");
      },

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/293399.html

標籤:其他

上一篇:牛客網刷題小結---C語言---有序序列(詳解)

下一篇:讓汽車軟體進入iPhone時代!

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more