首頁 > 軟體

JS圖形編輯器實現標尺功能範例詳解

2023-01-18 14:01:53

正文

專案地址:

github.com/F-star/suik…

線上體驗:

blog.fstars.wang/app/suika/

標尺指的是畫布上邊和左邊的兩個有刻度的尺子,作用讓使用者知道他正在編輯的視口所在位置範圍。

我們的需求是:間隔特定的長度,繪製一個刻度,並顯示這個刻度在 X 軸或 Y 軸上的位置。

先看最終實現效果:

標尺功能演示

可以看到,視口移動後,標尺上的刻度能正確地改變。此外縮放畫布,標尺的步長會發生改變,保持一個比較適合的密度。

實現思路

總體實現思路:

  • 確定刻度尺的步長(step)。步長是和畫布縮放比(zoom)相關的,zoom 越大,step 就越小;
  • 計算出需要繪製的所有刻度。分別為從視口從左側到右側,從上邊到下邊的範圍;
  • 繪製。繪製上也是有考量的,先繪製背景,然後繪製刻度,最後繪製分界線。

步長選擇

步長會根據 zoom 進行設定,目的是讓視口中的標尺能繪製適宜密度的刻度。

假設我們的步長固定為 50,不跟隨 zoom 改變,在 100% 看起來效果不錯:

但當你縮小時,會變成下面這樣:

密度過大,導致數位重疊。同樣,放大時則過於稀疏,刻度很難才見到一個,沒能發揮標尺的效用。

步長怎麼計算呢?

理論上步長可以是 50,那麼 51 好像也行,3 也行。但更建議使用 5 的倍數、2 的倍數、25 的倍數這些作為步長。

因為沒有什麼理論參考,所以我還是選擇參考市面上的設計工具的步長變化設計。

比如 figma,zoom 落在 [100%, 200%) 的步長為 50,[200%, 500%) 則是 10 等等。

我的實現為:

const getStepByZoom = (zoom: number) => {
  // 可用的步長列表
  const steps = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000];
  // 看著 figma 的 step 變化想出的一個奇怪的規律
  // 然後找出可選步長列表最近的並大於它的 step 作為最終步長
  const step = 50 / zoom;
  for (let i = 0, len = steps.length; i < len; i++) {
    if (steps[i] >= step) return steps[i];
  }
  return steps[0];
};
const step = getStepByZoom(zoom);

計算範圍

這裡我講解水平(x 軸)方向的情況。垂直方向同理,就不贅敘了。

首先計算出視口最左側和最右側的 x 座標值。

需要視口座標轉場景座標的知識,如果你不懂,看我這篇文章:

圖形編輯器:場景座標、視口座標以及它們之間的轉換

let startXInScene = viewport.x + startXInViewport / zoom; // 視口座標轉場景
let endXInScene = viewport.width + startYInViewport / zoom; // 視口座標轉場景

然後找離它們最近的落在刻度上的值。

對此,我實現了一個 getClosestVal 方法。

/**
 * 找出離 value 最近的 segment 的倍數值
 */
const getClosestVal = (value: number, segment: number) => {
  const n = Math.floor(value / segment);
  const left = segment * n;
  const right = segment * (n + 1);
  return value - left <= right - value ? left : right;
};
startXInScene = getClosestVal(startXInScene, step);
endXInScene = getClosestVal(endXInScene, step);

得到起點和終點,我們可以開始迴圈了,從 startXInScene 開始,每次迴圈加一個 step,直至達到末尾為止。

ctx.textAlign = 'center'; // 文字水平居中對齊
while (startXInScene <= endXInScene) {
  ctx.strokeStyle = setting.rulerMarkStroke;
  ctx.fillStyle = setting.rulerMarkStroke;
  // 場景轉回視口再繪製。刻度線不能直接在場景中繪製,因為縮放變換會導致線的粗細變化
  const x = (startXInScene - viewport.x) * zoom;
  // 繪製刻度
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(x, y + setting.rulerMarkSize);
  ctx.stroke();
  ctx.closePath();
  // 刻度值則用場景座標的值
  ctx.fillText(String(startXInScene), x, y - 4);
  // +step,指標移動
  startXInScene += step;
}

垂直方向的標尺同理,只是稍微特殊的是刻度值文字需要多做一個 -90 度的旋轉。

export const rotateInCanvas = (
  ctx: CanvasRenderingContext2D,
  angle: number,
  cx: number,
  cy: number
) => {
  ctx.translate(cx, cy);
  ctx.rotate(angle);
  ctx.translate(-cx, -cy);
};
rotateInCanvas(ctx, -HALF_PI, x, y);

繪製順序

繪製順序需要注意一下,先後順序為:

  • 繪製兩個標尺的背景色;
  • 繪製刻度值;
  • 用一個和背景色同色的矩形蓋掉左上角那個方形,那個地方不能有刻度值,不如兩個標尺的刻度會重疊。你也可以在繪製刻度值時,用裁切(ctx.clip)不讓繪製到那個方形區域上;
  • 繪製兩條分割線;

以上就是JS圖形編輯器實現標尺功能範例詳解的詳細內容,更多關於JS圖形編輯器實現標尺的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com