採用Canvas繪製一個可配置的刻度(尺)組件

前言

本次分享一個本身採用Canvas繪製一個可配置的刻度(尺)組件。 組件應用:主要經常使用於移動端數值、金額等的滑動選擇,加強用戶交互體驗。javascript

文章涉及開發思路、代碼拆解、問題解決、刻度組件庫封裝等多方面的知識。有須要的童鞋能夠適當參考,開發出更絢麗、更完善的組件。css

歡迎到github地址,多多start:github.com/now1then/ca…html

也歡迎訪問 演示地址體驗功能:rnlvwyx.cn:3333/#/demovue

功能分析說明

刻度組件 demo,效果圖: java

demo2.gif
從效果圖上分析,有處於中間的不移動的中心線;間隔的刻度長短效果及刻度尺每10間隔的刻度值顯示,且刻度尺可左右移動選取值;移動到左側最小值及右側最大值後,刻度不能繼續移動;支持根據傳入的值,響應刻度變化,以及簽名等...諸多優化項。

組件開發將完成如下功能:git

  • 採用canvas繪製組件,解決移動端繪製模糊問題,
  • 支持刻度尺基本參數配置傳入,
  • 監聽滑動事件,滑動時實時輸出刻度值,同時支持根據外部值動態設置刻度,
  • 支持平滑/緩動滑動、實時繪製刻度,
  • 兼容移動端/pc端滑動,

組件使用

引入scale.js文件:
import scale from './scale.js'; // 引入scale.js文件
// or
npm install canvas-scale
import scale from 'canvas-scale';
複製代碼

scale模塊對外暴露一個init()初始化方法; scale.init()函數 :github

  • 第一個參數爲可經過document.querySelector()獲取到的HTML節點;
  • 第二個參數爲須要重置的配置項;
  • 第三個參數傳入刻度變動時的回調函數,可經過該回調函數獲取最新刻度值;
  • 返回一個實例對象,對外暴露一些操做方法。
/** * scale 刻度函數 * @param {String} el html節點 * @param {Object} options 配置信息 * @param {Function} callBack 刻度變動回調函數 * @returns { Object} */
// 繪製刻度尺
const myScale = scale.init('#myScale', {height: 50, start: 10000, end: 2000},callBack);
function callBack(value) {
  console.log(value);
}
複製代碼
目前返回的實例對象暴露的方法有:
  • update(value):傳入最新的刻度值,更新畫布顯示。value:最新刻度值
  • clear():清除當前畫布。
  • resize(option):重置畫布,可傳入最新須要重置的配置信息。option:刻度配置
myScale.update(1000); // 更新刻度值
myScale.clear();  // 清除畫布
myScale.resize(); // 重置刻度畫布
複製代碼

開發

不熟悉canvas api的童鞋,能夠訪問 Canvas API中文網 學習。 我本身也總結概括了一遍關於Canvas開發及常見問題的文章:vue-cli

這裏講解一下開發思路,主要拆分爲如下幾個步驟:npm

  1. 配置項及功能:
  2. canvas繪製:
  3. 繪製中心線
  4. 繪製整個刻度尺
  5. 根據實際刻度值剪切刻度尺
  6. 繪製簽名及背景
  7. 交互:
  8. 增長監聽滑動事件,實時繪製刻度畫布
  9. 數值與刻度聯動變動
  10. 各類邊界異常狀況處理

配置項

組件支持的配置項,目前爲如下配置:canvas

// 默認配置
const default_conf = {
  // width: '', // 不支持設置,取容器寬度
  height: 50, // 畫布高度
  start: 1000, // 刻度開始值
  end: 10000, // 刻度結束值
  // def: 100, // 中心線停留位置 刻度值
  unit: 10, // 刻度間隔 'px'
  capacity: 100, // 刻度容量值
  background: '#fff', // 設置顏色則背景爲對應顏色虛幻效果,不設置默認爲全白。
  lineColor: '#087af7', // 中心線顏色
  openUnitChange: true, // 是否開啓間隔刻度變動
  sign: '@nowThen', // 簽名,傳入空不顯示簽名
  fontColor: '#68ca68', // 刻度數值顏色, 刻度線顏色暫未提供設置
  fontSize: '16px SimSun, Songti SC', // 刻度數值 字體樣式
};
複製代碼

經過scale.init()的第二個參數傳入配置項,不然將使用以上默認配置項。

canvas繪製

根據傳入的容器繪製canvas:

// 根據傳入的容器繪製canvas
  const container = document.querySelector(el);
  container.appendChild(canvas);
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
複製代碼

繪製中心線

中間線一直停留在畫布中間,標識當前刻度值。

// 繪製中心線
function drawMidLine() {
  const mid_x = Math.floor(config.width / 2);
  ctx.beginPath();
  ctx.fillStyle = config.lineColor;
  ctx.fillRect(mid_x - 1, 0, 2, config.height);
  ctx.stroke();
  ctx.moveTo(mid_x, 8);
  ctx.lineTo(mid_x - 5, 2);
  ctx.lineTo(mid_x - 5, 0);
  ctx.lineTo(mid_x + 5, 0);
  ctx.lineTo(mid_x + 5, 2);
  ctx.fill();
  ctx.moveTo(mid_x, config.height - 8);
  ctx.lineTo(mid_x - 5, config.height - 2);
  ctx.lineTo(mid_x - 5, config.height);
  ctx.lineTo(mid_x + 5, config.height);
  ctx.lineTo(mid_x + 5, config.height - 2);
  ctx.fill();
  ctx.closePath();
}
複製代碼

設置簽名及背景色

背景色config.background: 背景爲傳入的顏色值,且額外爲畫布 營造一些畫布兩邊刻度線虛幻的效果。 傳入空,則整個畫布爲透明色,且無兩端虛化效果。

簽名config.sign顯示在畫布右上角,傳入空值就不顯示簽名。

// 設置簽名及背景
  function drawSign() {
    // 背景 虛化效果、、
    if (config.background) {
      ctx.beginPath();
      var gradient1 = ctx.createLinearGradient(0, 0, config.width, 0);
      gradient1.addColorStop(0, 'rgba(255, 255, 255, 0.95)');
      gradient1.addColorStop(0.45, 'rgba(255, 255, 255, 0)');
      gradient1.addColorStop(0.55, 'rgba(255, 255, 255, 0)');
      gradient1.addColorStop(1, 'rgba(255, 255, 255, 0.95)');
      ctx.fillStyle = gradient1;
      ctx.fillRect(0, 0, config.width, config.height);
      ctx.closePath();
    }
          
    // 簽名
    if (config.sign) {
      ctx.beginPath();
      ctx.font = '10px Arial';
      var gradient = ctx.createLinearGradient(config.width, 0, config.width - 50, 0);
      gradient.addColorStop(0, 'rgba(255, 0, 0, 0.3)');
      gradient.addColorStop(1, 'rgba(0, 128, 0, 0.3)');
      ctx.fillStyle = gradient;
      ctx.textAlign = 'right';
      ctx.fillText(config.sign, config.width - 10, 10);
      ctx.closePath();
      ctx.fillStyle = 'transparent';
    }
  }

// 在繪製刻度尺時,設置背景值
ctx_bg.fillStyle = _config.background || 'transparent'; // 背景色
複製代碼

繪製刻度尺

這裏想到了兩個可行思路:

  1. 提早繪製好整個刻度尺畫布,在滑動時,根據參數截取刻度尺畫布的一部分區域繪製到可視區域中。
  2. 根據當前刻度值、滑動距離等參數,實時繪製畫布可視區域的刻度分佈。

在實際開發時,這兩種方案都嘗試並實現了。也暴露了一些問題:

第一種方案:

步驟分爲如下:

  • 根據傳入的配置,設置樣式;計算刻度尺畫布的寬高;
  • 繪製整個刻度尺底線;
  • 計算刻度個數,並依次繪製每一個刻度,刻度寬度1px,普通刻度1/5 * height高,每隔5刻度1/3 * height高,每隔10刻度 1/2 * height高並繪製刻度值;
  • 計算截取刻度尺畫布的開始位置;
  • 而後經過context.getImageData(sx, sy, sWidth, sHeight);截取刻度尺圖像區域,經過context.putImageData(imageData, 0, 0);將上面截取的ImageData對象的數據繪製到主畫布上。

使用context.drawImage()也能達到相同效果。

在滑動時,根據滑動的位置,截取對應的刻度尺區域圖形,繪製在主畫布上。

// 建立新的刻度畫布 做爲底層圖片
  const canvas_bg = document.createElement('canvas');
  const ctx_bg = canvas_bg.getContext('2d');

  // 繪製刻度尺
  function drawScale() {
    const mid = config.end - config.start + 1; // 取值範圍

    const scale_len = Math.ceil(mid / config.capacity); // 刻度條數
    const space = Math.floor(config.width / 2); //左右兩邊間隙,根據該值計算整數倍刻度值畫線
    const beginNum = Math.ceil(config.start / config.capacity) * config.capacity;
    const st = (Math.ceil(config.start / config.capacity) - config.start / config.capacity) * config.unit;

    // 設置canvas_bg寬高
    canvas_bg.width = (config.unit * (scale_len - 1) + config.width) * dpr;
    canvas_bg.height = config.height * dpr;
    ctx_bg.scale(dpr, dpr);

    ctx_bg.beginPath();
    ctx_bg.fillStyle = config.background || 'transparent'; // 背景色
    ctx_bg.fillRect(0, 0, canvas_bg.width, config.height);
    ctx_bg.closePath();
    // 底線
    ctx_bg.beginPath();
    ctx_bg.moveTo(0, config.height);
    ctx_bg.lineTo(canvas_bg.width, config.height);
    ctx_bg.strokeStyle = config.scaleLineColor || '#9E9E9E';
    ctx_bg.lineWidth = 1;
    ctx_bg.stroke();
    ctx_bg.closePath();

    // 繪製刻度線
    for (let i = 0; i < scale_len; i++) {
      ctx_bg.beginPath();
      ctx_bg.strokeStyle = config.scaleLineColor || "#9E9E9E";
      ctx_bg.font = config.fontSize;
      ctx_bg.fillStyle = config.fontColor;
      ctx_bg.textAlign = 'center';
      ctx_bg.shadowBlur = 0;

      const curPoint = i * config.unit + space + st;
      const curNum = i * config.capacity + beginNum;
      if (curNum % (config.capacity * 10) === 0) {
        ctx_bg.moveTo(curPoint, (config.height * 1) / 2);
        ctx_bg.strokeStyle = config.scaleLineColor || "#666";
        ctx_bg.shadowColor = '#9e9e9e';
        ctx_bg.shadowBlur = 1;
        ctx_bg.fillText(
          curNum,
          curPoint,
          (config.height * 1) / 3
        );
      } else if (curNum % (config.capacity * 5) === 0) {
        ctx_bg.moveTo(curPoint, (config.height * 2) / 3);
        ctx_bg.strokeStyle = config.scaleLineColor || "#888";
        if (scale_len <= 10) {
          ctx_bg.font = '12px Helvetica, Tahoma, Arial';
          ctx_bg.fillText(
            curNum,
            curPoint,
            (config.height * 1) / 2
          );
        }
      } else {
        ctx_bg.moveTo(curPoint, (config.height * 4) / 5);
        if (i === 0 || i === scale_len - 1) {
          ctx_bg.font = '12px Helvetica, Tahoma, Arial';
          ctx_bg.fillText(
            curNum,
            curPoint,
            (config.height * 2) / 3
          );
        }
      }
      ctx_bg.lineTo(curPoint, config.height);
      ctx_bg.stroke();
      ctx_bg.closePath();
    }

    point_x = (config.def - config.start) / config.capacity * config.unit; //初始化開始位置
    const imageData = ctx_bg.getImageData(point_x * dpr, 0, config.width * dpr, config.height * dpr)
    ctx.putImageData(imageData, 0, 0);
  }
複製代碼
第二種方案:

步驟分爲如下:

  • 繪製整個刻度尺底線;
  • 以中心刻度爲基準,計算最左側的刻度值;
  • 計算畫布區域可繪製的刻度線個數及第一個刻度線的位置及刻度數值;
  • 依次繪製每一個刻度,刻度寬度1px,普通刻度1/5 * height高,每隔5刻度1/3 * height高,每隔10刻度 1/2 * height高並繪製刻度值;
  • 而後context.drawImage()繪製圖像到主畫布區域;

在滑動時,實時計算當前中間刻度值,並依據上面的繪製步驟,重繪整個畫布。

// 建立新的刻度畫布 做爲底層圖片
  const canvas_bg = document.createElement('canvas');
  const ctx_bg = canvas_bg.getContext('2d');

  // 繪製刻度尺
  function drawScale() {
    // 設置canvas_bg寬高
    canvas_bg.width = (config.unit * (scale_len - 1) + config.width) * dpr;
    canvas_bg.height = config.height * dpr;
    ctx_bg.scale(dpr, dpr);

    // 以中點刻度爲基準,獲取最左側刻度值
    let begin_num = current_def - (config.width / 2) * (config.capacity / config.unit);
    let cur_x = 0;
    let cur_num = 0;
    const scale_len = Math.ceil((config.width + 1) / config.unit); // 刻度條數
    const real_len = Math.ceil((config.end - config.start + 1) / config.capacity); // 實際可繪製的刻度條數

    ctx_bg.fillStyle = config.background || 'transparent'; // 背景色
    ctx_bg.fillRect(0, 0, config.width, config.height);
    ctx_bg.closePath();
    // 底線
    ctx_bg.beginPath();
    ctx_bg.moveTo(0, config.height);
    ctx_bg.lineTo(config.width, config.height);
    ctx_bg.strokeStyle = config.scaleLineColor || '#9E9E9E';
    ctx_bg.lineWidth = 1;
    ctx_bg.stroke();
    ctx_bg.closePath();

    let space_num = Math.ceil(begin_num / config.capacity) * config.capacity - begin_num;
    let space_x = space_num * (config.unit / config.capacity);

    // 繪製刻度線
    for (let i = 0; i < scale_len; i++) {
      cur_num = (Math.ceil(begin_num / config.capacity) + i) * config.capacity;
      if (cur_num < config.start) {
        continue;
      } else if (cur_num > config.end) {
        break;
      }

      ctx_bg.beginPath();
      ctx_bg.strokeStyle = config.scaleLineColor || "#9E9E9E";
      ctx_bg.font = config.fontSize;
      ctx_bg.fillStyle = config.fontColor;
      ctx_bg.textAlign = 'center';
      ctx_bg.shadowBlur = 0;
      cur_x = space_x + i * config.unit;

      if (cur_num % (config.capacity * 10) === 0) {
        ctx_bg.moveTo(cur_x, (config.height * 1) / 2);
        ctx_bg.strokeStyle = config.scaleLineColor || "#666";
        ctx_bg.shadowColor = '#9e9e9e';
        ctx_bg.shadowBlur = 1;
        ctx_bg.fillText(
          cur_num,
          cur_x,
          (config.height * 1) / 3
        );
      } else if (cur_num % (config.capacity * 5) === 0) {
        ctx_bg.moveTo(cur_x, (config.height * 2) / 3);
        ctx_bg.strokeStyle = config.scaleLineColor || "#888";
        if (real_len <= 10) {
          ctx_bg.font = '12px Helvetica, Tahoma, Arial';
          ctx_bg.fillText(
            cur_num,
            cur_x,
            (config.height * 1) / 2
          );
        }
      } else {
        ctx_bg.moveTo(cur_x, (config.height * 4) / 5);
      }
      ctx_bg.lineTo(cur_x, config.height);
      ctx_bg.stroke();
      ctx_bg.closePath();
    }
    ctx.drawImage(canvas_bg, 0, 0, config.width * dpr, config.height * dpr, 0, 0, config.width, config.height);  
  }
複製代碼
總結

第一種方案: 在實現難度上更簡單;這個刻度尺畫布只需繪製一次,滑動時無需重繪刻度尺畫布;也更能直觀體現刻度移動, 但在繪製刻度區間跨度大時,性能很差,且canvas畫布尺寸過大,會出現繪製空白的問題。 第二種方案: 比較難定位可視區域刻度尺的初始值、結束值,且一滑動,整個畫布都從新計算每一個繪製點。 咋一看實現更麻煩,滑動時整個畫布都得實時繪製,但相比於第一種方案的致命缺陷,效果、性能及兼容性更佳。

這樣,初始的刻度尺樣式就生成了。

image.png
接下來就是添加滑動交互效果。

開發過程當中遇到的問題(在文章後面有較詳細的說明):

  • 移動端canvas繪製模糊問題,canvas插入圖像模糊問題;
  • context.drawImage()參數問題;
  • 當canvas繪製尺寸或插入圖像尺寸大於某個閾值時,可能會出現繪製空白問題。

滑動事件監聽,兼容PC端

監聽左右滑動事件,獲取每次手指滑動的距離,計算刻度須要移動的距離,並從新繪製canvas畫布。

  1. 註冊事件兼容移動端和PC端;移動端監聽touch事件,PC端監聽mouse事件,這裏要注意獲取當前觸碰點的差別處理。

移動端獲取當前觸摸點X座標:e.touches[0].pageX;PC端獲取鼠標X座標:e.pageX

  1. 在move事件中,實時繪製,實時更新刻度值,並調用傳入的回調函數傳回刻度數值;
  2. 處理移動到左右兩側邊界的狀況,達到設置的最大最小值時,沒法繼續移動;
  3. 利用 window.requestAnimationFrame()優化渲染頻率。
  4. 從新繪製前,須要利用ctx.clearRect()先清空畫布再繪製,不然有繪製重疊。
  5. 根據配置項config.openUnitChange決定是否只能間隔刻度移動,好比100、200、300...變動。傳入false時,則按實際移動距離變動。

效果以下圖:

按刻度移動.gif
非刻度移動.gif
主要代碼:

// 事件交互 (第一種方案)
  function addEvent() {
    let begin_x = 0; // 手指x座標
    let ifMove = false; // 是否開始交互
    let moveDistance = 0;

    // 註冊事件,移動端和PC端
    const hasTouch = 'ontouchstart' in window;
    const startEvent = hasTouch ? 'touchstart' : 'mousedown';
    const moveEvent = hasTouch ? 'touchmove' : 'mousemove';
    const endEvent = hasTouch ? 'touchend' : 'mouseup';
    canvas.addEventListener(startEvent, start);
    canvas.addEventListener(moveEvent, move);
    canvas.addEventListener(endEvent, end);

    function start(e) {
      e.stopPropagation();
      e.preventDefault();
      ifMove = true;
      if (!e.touches) {
        begin_x = e.pageX;
      } else {
        begin_x = e.touches[0].pageX;
      }
    }

    function move(e) {
      e.stopPropagation();
      e.preventDefault();
      const current_x = e.touches ? e.touches[0].pageX : e.pageX;
      if (ifMove) {
        moveDistance = current_x - begin_x;
        begin_x = current_x;
        point_x = point_x - moveDistance; //刻度偏移量
        const space = Math.floor(config.width / 2);
        // 邊界值處理
        if (point_x <= 0) {
          point_x = 0;
        } else if (point_x >= canvas_bg.width / dpr - config.width) {
          point_x = canvas_bg.width / dpr - config.width;
        }

        window.requestAnimationFrame(moveDraw)
      }
    }

    function end(e) {
      ifMove = false;
    }
  }

  function moveDraw() {
    let now_x = point_x;
    // 是否刻度移動
    if (config.openUnitChange) {
      const st = ( config.start / config.capacity - Math.floor(config.start / config.capacity)) * config.unit;
      now_x = Math.round(this.point_x / config.unit) * config.unit - st;
    }
    ctx.clearRect(0, 0, config.width, config.height);
    // ctx.drawImage(canvas_bg, now * dpr, 0, config.width * dpr, config.height * dpr, 0, 0, config.width, config.height);
    var imageData = ctx_bg.getImageData(now_x * dpr, 0, config.width * dpr, config.height * dpr)
    ctx.putImageData(imageData, 0, 0)
    drawMidLine();
    drawSign();
    const value = now_x * config.capacity / config.unit + config.start;
    if (typeof callBack === 'function') {
      callBack(Math.round(value));
    } else {
      throw new Error('scale函數的第二個參數,必須爲正確的回調函數!')
    }
  }
複製代碼

平滑移動,緩動函數

上面的事件交互是第一種方案的代碼。未作平滑緩動處理。 在此基礎上改爲第二種方案的代碼,且增長平滑移動,緩動函數採用easeOut,先快後慢。

// easeOut 緩動函數
const slowActionfn = function (t, b, c, d) {
  return c * ((t = t / d - 1) * t * t + 1) + b;
};  

// 事件交互
function addEvent() {
  let begin_x = 0; // 手指x座標
  let last_x = 0; //上一次x座標
  let ifMove = false; // 是否開始交互
  let from_def = 0;
  let lastMoveTime = 0;
  let lastMove_x = 0;

  // 註冊事件,移動端和PC端
  const hasTouch = 'ontouchstart' in window;
  const startEvent = hasTouch ? 'touchstart' : 'mousedown';
  const moveEvent = hasTouch ? 'touchmove' : 'mousemove';
  const endEvent = hasTouch ? 'touchend' : 'mouseup';
  canvas.addEventListener(startEvent, start);
  canvas.addEventListener(moveEvent, move);
  canvas.addEventListener(endEvent, end);

  function start(e) {
    e.stopPropagation();
    e.preventDefault();
    ifMove = true;
    if (!e.touches) {
      last_x = begin_x = e.pageX;
    } else {
      last_x = begin_x = e.touches[0].pageX;
    }
    lastMove_x = last_x;
    lastMoveTime = e.timeStamp || Date.now();
  }

  function move(e) {
    e.stopPropagation();
    e.preventDefault();
    const current_x = e.touches ? e.touches[0].pageX : e.pageX;
    if (ifMove) {
      move_x = current_x - last_x;
      current_def = current_def - move_x * (config.capacity / config.unit);
      window.requestAnimationFrame(moveDraw);
      last_x = current_x;

      const nowTime = e.timeStamp || Date.now();
      if (nowTime - lastMoveTime > 300) {
        lastMoveTime = nowTime;
        lastMove_x = last_x;
      }
    }
  }


  function end(e) {
    const current_x = e.changedTouches ? e.changedTouches[0].pageX : e.pageX;
    const nowTime = e.timeStamp || Date.now();
    const v = -(current_x - lastMove_x) / (nowTime - lastMoveTime); //最後一段時間手指划動速度

    ifMove = false;
    let t = 0, d = 15;
    if (Math.abs(v) >= 0.3) {
      from_def = current_def;
      step();
    } else {
      if (current_def < config.start) {
        current_def = config.start;
      } else if (current_def > config.end) {
        current_def = config.end;
      }
      if (config.openUnitChange) {
        current_def = Math.round(current_def / config.capacity) * config.capacity;
      }
      moveDraw();
    }

    function step() {
      current_def = slowActionfn(t, from_def, (config.capacity) * v * 50, d);
      if (current_def < config.start) {
        current_def = config.start;
      } else if (current_def > config.end) {
        current_def = config.end;
      }
      if (config.openUnitChange) {
        current_def = Math.round(current_def / config.capacity) * config.capacity;
      }
      moveDraw()
      t++;
      if (t <= d) {
        // 繼續運動
        window.requestAnimationFrame(step);
      } else {
        // 結束
      }
    }
  }
}

function moveDraw() {
  ctx.clearRect(0, 0, config.width, config.height);

  drawScale();
  drawMidLine();
  drawSign();

  if (typeof callBack === 'function') {
    callBack(Math.round(current_def));
  } else {
    throw new Error('scale函數的第二個參數,必須爲正確的回調函數!')
  }
}
複製代碼

邊界處理

在繪製過程當中,還須要處理各類邊界狀況:

  1. 對傳入的配置項做簡單驗證提示,好比id是不是有效的dom節點;
  2. 計算並設置默認的中心線位置;
  3. 滑動時,滑到最左側、最右側時的臨界值處理;
  4. 傳入的刻度值不在刻度區間內時的處理;

這些邊界狀況在組件庫代碼中都作了簡單處理。

常見問題

詳情歡迎查看本人另外一篇文章-「記錄canvas使用及常見問題

移動端繪製canvas模糊問題

現象

以下圖:

image.png
上圖中,在未作兼容移動端處理時,繪製的canvas刻度組件在iphone 6s機型上,canvas圖形和文字模糊失真。兼容處理後圖形質量得以保證。 以上刻度組件參考個人另外一篇文章,地址: www.yuque.com/nowthen/lon…

緣由

關於移動端高清屏DPR、圖片模糊、移動端適配等問題,不清楚的童鞋能夠參考「關於移動端適配,你必需要知道的」這篇文章,講的比較詳細。這裏再也不贅述,本文章只處理移動端Canvas模糊問題。 在移動端高清屏幕上,常常會遇到Canvas圖形模糊的問題。本質上跟移動端圖片模糊問題是同樣的。canvas繪製成的圖像跟也是位圖,在dpr > 1的屏幕上,位圖的一個像素可能由多個物理像素來渲染,然而這些物理像素點並不能被準確的分配上對應位圖像素的顏色,只能取近似值,因此在dpr > 1的屏幕上就會模糊。 在PC端繪製canvas圖形,咱們都直接把1個canvas像素直接等於1px的css像素處理,這沒有問題,應該目前PC端屏幕dpr都是1。而在dpr > 1的移動端屏幕上就不能直接這樣處理。

解決

解決方案固然仍是從dpr入手。

  1. 經過window.devicePixelRatio獲取當前設備屏幕的dpr;
  2. 首先獲取或設置Canvas容器的寬高;
  3. 根據dpr,設置canvas元素的寬高屬性;在dpr = 2時至關於擴大畫布2倍;
  4. 經過context.scale(dpr, dpr)縮放Canvas畫布的座標系。在dpr = 2時至關於把canvas座標系也擴大了兩倍,這樣繪製比例放大了2倍,以後canvas的實際繪製像素就能夠按原先的像素值處理。

在渲染到屏幕時,擴大的畫布圖形又等比例縮放渲染到canvas容器中。從而保證canvas圖形的質量。

// 獲取dpr
const dpr = window.devicePixelRatio; 
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 獲取Canvas容器的寬高
const { width: cssWidth, height: cssHeight } = canvas.getBoundingClientRect();
// 根據dpr,設置Canvas的寬高,使1個canvas像素和1個物理像素相等
canvas.width = dpr * cssWidth;
canvas.height = dpr * cssHeight;
// 根據dpr,設置canvas元素的寬高屬性
ctx.scale(dpr,dpr);
複製代碼

canvas drawImage()參數問題,移動端圖片模糊問題

canvas的drawImage() 函數有個特別容易混淆搞錯的地方。它的5參數和9參數用法的參數位置是不一樣的。實際開發中沒注意到這一點,會讓本身特別困惑問題出在哪!汗!

drawImage()方法有一個很是怪異的地方,你們必定要注意,那就是5參數和9參數用法的參數位置是不同的,這個和通常的API有所不一樣。通常API可選參數是放在後面。可是,這裏的drawImage()使用9個參數時候,可選參數sx,sy,sWidth和sHeight是在前面的。若是不注意這一點,有些表現會讓你沒法理解。

且drawImage()函數插入的圖形在移動端dpr >1屏幕一樣會有圖片模糊的問題。 在移動端經過drawImage()載入另外一個已繪製的Canvas元素時,也要注意對另外一個canvas元素作兼容處理,還須要注意二者座標系的不一樣。

// 設置canvas_bg寬高
canvas_bg.width = (config.unit * (scale_len - 1) + config.width) * dpr;
canvas_bg.height = config.height * dpr;
ctx_bg.scale(dpr, dpr);

...

// 初始化開始位置
point_x = (config.def - config.start) / config.capacity * config.unit;
//在主畫布ctx上,經過drawImage()插入另外一個canvas_bg畫布;
ctx.drawImage(canvas_bg, point_x * dpr, 0, config.width * dpr, config.height * dpr, 0, 0, config.width, config.height);

複製代碼

上面的代碼中, canvas_bg畫布一樣須要處理上面提到的canvas模糊問題;在主畫布ctx上,經過drawImage()插入另外一個canvas_bg畫布圖形時,須要注意此時二者座標系比例的不一樣,此時canvas_bg的座標系是根據dpr縮放後的。

當canvas繪製尺寸或drawImage插入圖像、getImageDate獲取圖形資源等尺寸大於某個閾值時,可能會出現繪製空白問題。

在實際開發中遇到,canvas繪製尺寸或drawImage插入圖像、getImageDate獲取圖形資源等尺寸大於某個閾值時,渲染出來的圖片整個都是空白。這個具體的閾值不肯定,跟運行環境有關。但這應該也是drawImage繪製的一個不知什麼時候爆發的隱患。 好比下圖,繪製的刻度尺畫布尺寸過大,截取後渲染到主畫布上,整個刻度空白,但不影響交互。

image.png

組件庫封裝

以上是開發思路及主要代碼。接下來要封裝成開源的組件庫文件,能夠直接引入到項目中使用。 首先對代碼進行適當改造,封裝成類,操做實例的方式構建代碼。以提升代碼複用及避免屢次引入使用的相互影響。具體代碼請移步到項目中查看。

爲了簡便,直接使用vue-cli3提供的的構建庫模式構建組件庫。構建的庫會輸出 CommonJS 包、UMD包等版本。

項目連接:

github源碼地址:github.com/now1then/ca…;

文章-語雀:www.yuque.com/nowthen/lon…;

組件demo體驗地址:rnlvwyx.cn:3333/#/demo;

有問題歡迎探討...

相關文章
相關標籤/搜索