利用canvas陰影功能與雙線技巧繪製軌道交通大屏專案效果

netcy發表於2020-12-30

利用canvas陰影功能與雙線技巧繪製軌道交通大屏專案效果

前言

近日公司接到一個軌道系統的需求,需要將地鐵線路及列車實時位置展示在大屏上。既然是大屏專案,那視覺效果當然是第一重點,我們們可以先來看看專案完成後的效果圖。
line.gif
可以看到中間線路里軌道的效果是非常炫酷的,那麼本文的主要內容就是講解如何在canvas上繪製出這種效果。

分析設計稿

先看看設計稿中的軌道效果
123.jpg
程式設計師解決問題時經常喜歡用到的方法是把一個大問題拆解為若干個小問題然後逐一處理,也就是分而治之,所以我在思考這個軌道效果的實現時,也是先考慮到將它拆解。
根據設計稿我們可以看到這個線路實際上是由 外層的空心線+發光效果+內層的斑馬線+倒影 組成的,所以我們要做的就是如何處理這幾個小問題。

實現效果

繪製空心線與發光效果

繪製空心線時我們需要利用到[CanvasRenderingContext2D.globalCompositeOperation](https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation)這個屬性,詳細原理可以檢視canvas 繪製雙線技巧,本文不再做贅述。
瞭解實現原理之後動手就很容易了,簡述思路就是:
通過ctx.globalCompositeOperation = "destination-out"繪製空心線,再利用canvas的陰影配置來模擬發光的效果。
直接上程式碼:

//  獲取頁面裡的畫布元素和其上下文物件
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
//  由於ctx.globalCompositeOperation = "destination-out"會影響到畫布上已有的影像
//  所以需要先建立一個離屏canvas,把空心線繪製到離屏canvas上,再將離屏canvas繪製到頁面的畫布中
var tempCanvas = document.createElement("canvas");
tempCanvas.width = 800;
tempCanvas.height = 800;
var tempCtx = tempCanvas.getContext("2d");
//  建立座標點用來連線
var points = [createPoint(50, 50), createPoint(500, 50), createPoint(500, 500)];
//  配置引數
var options = {
  color: "#03a4fe", //  軌道顏色
  lineWidth: 26,    //  總寬度
  borderWidth: 8,   //  邊框寬度
  shadowBlur: 20,   //  陰影模糊半徑
};
paint(ctx, points, options);
//  繪製
function paint(ctx, points, options) {
  paintHollow(tempCtx, points, options);
  //    將離屏canvas繪製到頁面上
  ctx.drawImage(tempCanvas, 0, 0);
}
/**
 * 繪製空心線
 * @param {*} ctx 畫布上下文
 * @param {*} points 座標點的集合
 * @param {*} options 配置 
 */
function paintHollow(
  ctx,
  points,
  { color, lineWidth, borderWidth, shadowBlur }
) {
    //  連線
  paintLine(ctx, points);
  //    新增配置引數
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = color;
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
  //    利用陰影
  ctx.shadowColor = color;
  ctx.shadowOffsetX = 0;
  ctx.shadowOffsetY = 0;
  ctx.shadowBlur = shadowBlur;
  ctx.stroke();
  ctx.globalCompositeOperation = "destination-out";
  ctx.lineWidth -= borderWidth;
  ctx.strokeStyle = color;
  ctx.stroke();
  ctx.globalCompositeOperation = "source-over";
}
/**
 * 根據點位繪製連線
 * @param {*} ctx 畫布上下文
 * @param {Array} points 座標點的集合
 */
function paintLine(ctx, points) {
  var pointIndex = 0,
    p0,
    value,
    pointCount = points.length;
  p0 = points[0];
  ctx.beginPath();
  ctx.moveTo(p0.x, p0.y);
  for (pointIndex = 1; pointIndex < pointCount; pointIndex++) {
    value = points[pointIndex];
    ctx.lineTo(value.x, value.y);
  }
}

效果圖
image.png

繪製倒影

可以看到設計稿裡的倒影效果就是在軌道下方再次繪製了一條透明度較低的空心線,所以這裡實現起來就比較簡單了,稍微改造一下paintHollow方法就可以。

/**
 * 繪製空心線
 * @param {*} ctx 畫布上下文
 * @param {*} points 座標點的集合
 * @param {*} options 配置
 * @param {*} isReflect 當前繪製的是否是倒影效果
 */
function paintHollow(
  ctx,
  points,
  { color, lineWidth, borderWidth, shadowBlur, reflectOffset },
  isReflect = false
) {
  if (!isReflect) {
      //    繪製倒影的時候透明度降低
    ctx.globalAlpha = 0.5;
    //  通過自調繪製一個倒影效果出來
    paintHollow(
      ctx,
      points.map(({ x, y }) => {
        return { x, y: y + reflectOffset };
      }),
      { color, lineWidth, borderWidth, shadowBlur: 0 },
      true
    );
    ctx.globalAlpha = 1;
  }
  //  連線
  paintLine(ctx, points);
  //    新增配置引數
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = color;
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
  //    利用陰影
  ctx.shadowColor = color;
  ctx.shadowOffsetX = 0;
  ctx.shadowOffsetY = 0;
  ctx.shadowBlur = shadowBlur;
  ctx.stroke();
  ctx.globalCompositeOperation = "destination-out";
  ctx.lineWidth -= borderWidth;
  ctx.strokeStyle = color;
  ctx.stroke();
  ctx.globalCompositeOperation = "source-over";
}

效果圖
image.png

繪製軌道中間的斑馬線效果

中間的斑馬線效果我們又可以再拆分為兩個部分,先繪製一條底色的連線,然後再通過lineDash屬性繪製一條虛線,就可以達到設計稿上的效果了。


/**
 * 繪製軌道中間部分
 * @param {*} ctx 
 * @param {*} points 
 * @param {*} param2 
 */
function paintInner(
  ctx,
  points,
  { color, innerWidth, borderWidth, innerColor, shadowBlur }
) {
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
  paintLine(ctx, points);
  ctx.lineWidth = innerWidth;
  ctx.shadowOffsetX = 0;
  ctx.shadowOffsetY = 0;
  ctx.shadowBlur = shadowBlur;
  ctx.strokeStyle = innerColor;
  ctx.shadowColor = color;
  //  先根據中間部分的顏色繪製一條線出來
  ctx.stroke();
  ctx.lineCap = "butt";
  ctx.setLineDash([5, 15]);
  ctx.lineDashOffset = 0;
  const { r, g: green, b } = getRgba(color);
  //  再根據軌道的主色調繪製一條透明度較低的虛線
  ctx.strokeStyle = `rgba(${r},${green},${b},0.4)`;
  ctx.stroke();
}
/**
 * 獲取一個顏色值的r,g,b,a
 * @param {*} color 
 */
function getRgba(color) {
  if (!canvas1 || !ctx1) {
    canvas1 = document.createElement("canvas");
    canvas1.width = 1;
    canvas1.height = 1;
    ctx1 = canvas1.getContext("2d");
  }
  canvas1.width = 1;
  ctx1.fillStyle = color;
  ctx1.fillRect(0, 0, 1, 1);
  const colorData = ctx1.getImageData(0, 0, 1, 1).data;
  return {
    r: colorData[0],
    g: colorData[1],
    b: colorData[2],
    a: colorData[3],
  };
}

效果圖
image.png
至此我們就還原了設計稿上的軌道效果了!

結語

至此文章已經到達尾聲,我們可以總結一下繪製這條軌道線路效果所用到的技術點

  1. CanvasRenderingContext2D.globalCompositeOperation
  2. CanvasRenderingContext2D.shadowBlur
  3. CanvasRenderingContext2D.setLineDash()
  4. 離屏canvas技巧

可以看到想要達到好的效果還是不容易的,需要我們靈活配合使用多種繪製技巧,希望這篇文章能對大家有所幫助!

如果對視覺化感興趣,可以和我交流,微信541002349. 另外關注公眾號“ITMan彪叔” 可以及時收到更多有價值的文章。

相關文章