欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JS前端使用canvas動態(tài)繪制函數(shù)曲線示例詳解

 更新時間:2022年08月02日 11:44:14   作者:尤水就下  
這篇文章主要為大家介紹了JS前端使用canvas畫函數(shù)曲線的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前言

不說廢話,我們直入主題。先來看看讀了這篇文章你將得到什么,就是下面這個東西啦??(是不是很清晰很順滑):

那具體要做什么呢,我們來簡單拆解一下步驟:

  • 繪制坐標(biāo)系
  • 繪制多條函數(shù)曲線
  • 繪制輔助線和坐標(biāo)點
  • 支持平移、縮放
  • 支持動態(tài)繪制曲線
  • 使之高清 下面就來一一攻克它們,如果你在電腦前可以順便瞅瞅示例代碼(傳送門),不依賴任何庫哦。

第一步:繪制坐標(biāo)系

既然我們要畫函數(shù),那肯定要先搞一個坐標(biāo)系啦,也就是網(wǎng)格。大家可能都知道怎么畫網(wǎng)格,就是橫的畫幾條、豎的畫幾條。思路是沒錯,但是具體畫起來要考慮的問題還是有很多的。畫一個不動的、沒有要求的網(wǎng)格很簡單,但加了刻度后就不那么容易了。這里我就直接說下會遇到的一些問題吧!

1、如何確定 x 軸和 y 軸的邊界值

這個東西很重要,是接下來所有一切操作的開端,因為我們畫的是函數(shù),一定要有個 x 軸的邊界(當(dāng)做參數(shù)傳進(jìn)來),那 y 軸呢,一般我們的網(wǎng)格都是正方形,所以這里讓 y 軸的取值范圍跟 x 軸一致即可。如果你不傳 x 軸的兩個邊界值(lefxX、rightX),那我們會給個默認(rèn)值[-canvas寬度的一半/100, canvas寬度的一半/100],因為一般畫布的寬高都是幾百,所以這里就簡單的除以 100,你要除以 10 也是 ok 的。再次強調(diào)一下邊界值這個東西很重要,因為接下來的操作都是基于邊界值來繪制的。不信你可以嘗試一下沒有邊界值的情況,那可能會無從下手??。

2、不是傳入多少網(wǎng)格數(shù)就是多少網(wǎng)格

因為網(wǎng)格刻度是需要騷微取整的,比如 10、5、1、0.5、0.2、0.1 這樣。什么意思呢,舉個具體的例子??,比如 x 軸的取值范圍是 (-5,5),網(wǎng)格的數(shù)量是10,那剛好就可以有 10 個網(wǎng)格,這樣刻度就會挺整的(-5,-4,-3這樣);如果網(wǎng)格數(shù)是9,那么我們也會有10個網(wǎng)格,刻度也是(-5,-4,-3這樣),而不是(-5,-3.9,-2.8),不知道大家 get 到木有。所以網(wǎng)格大小是要稍微計算一下的,算是個小小的算法,下面是其中的一種解法????:

/**
 * 計算每一個網(wǎng)格的寬高大小,注意并不是 gridCount 是多少就會有多少網(wǎng)格
 * 因為我們的坐標(biāo)刻度通常都是 10、5、1、0.5、0.1 這個樣子,大部分都是以 2、5、10 的倍數(shù)
 * @param len x 或 y 軸代表的長度
 * @param gridCount 網(wǎng)格個數(shù)
 */
calcGridSize(len: number, gridCount: number): number[] {
    let gridWidth = 1;
    // 應(yīng)該保留幾位小數(shù),避免浮點數(shù)
    let fixedCount = 0;
    // 事實上,要是圖方便的話,你也可以直接用 unitX 來當(dāng)做網(wǎng)格大小,不過記得要取整
    let unitX = len / gridCount;
    // 而這里呢,我們需要找到離 unitX 最近的(稍微偏整數(shù)的)值
    // 首先要找到離 unitX 最近的 10 的整數(shù)倍,如 0.01、0.1、1、10、100
    while (gridWidth < unitX) {
        gridWidth *= 10
    }
    while (gridWidth / 10 > unitX) {
        gridWidth /= 10
        fixedCount++;
    }
    // 看看能不能再劃分一次,如(/5 得到 0.02、0.2、20)、(/2 得到 0.05、5、50)
    if (gridWidth / 5 > unitX) {
        gridWidth /= 5;
        fixedCount++;
    } else if (gridWidth / 2 > unitX) {
        gridWidth /= 2;
        fixedCount++;
    }
    // 因為 x 軸長度和 y 軸的長度是一樣的,所以可以這樣賦值
    return [gridWidth, gridWidth, fixedCount];
}

3、如何讓坐標(biāo)原點位于畫布中心

這個比較抽象,所以我們看圖。我們不要這樣(注意看四周的網(wǎng)格):

我們要這樣(注意看四周的網(wǎng)格,再和上圖對比一下):

不知道大家 get 到木有,其實你可以從中間開始往上下、往左右畫網(wǎng)格,不過這里我們還是從左到右畫就行(做點簡單的數(shù)學(xué)運算),直接看代碼應(yīng)該會清晰點,而且我還配了圖??:

drawGrid() {
    const { width, height, leftX, rightX, leftY, rightY, xLen, yLen, gridCount, ctx2d } = this;
    ctx2d?.save();
    // 注意這里我們是將網(wǎng)格數(shù)作為配置項,而不是網(wǎng)格的寬高大小
    const [gridWidth, gridHeight, fixedCount] = this.calcGridSize(xLen, gridCount);
    // 由于計算會產(chǎn)生浮點數(shù)偏差,所以要控制下小數(shù)點后面的數(shù)字個數(shù)
    this.fixedCount = fixedCount;
    // 從左到右繪制豎線
    for (let i = Math.floor(leftX / gridWidth); i * gridWidth < rightX; i++) {
        // 繪制像素點 / 整個畫布寬度 = 實際 x 值 / 實際表示的 x 軸長度
        const x = (i * gridWidth - leftX) / xLen * width;
        // i = 0 就說明是 y 軸,顏色加深
        const color = i ? '#ddd' : '#000';
        this.drawLine(x, 0, x, height, color)
        this.fillText(String(this.formatNum(i * gridWidth, this.fixedCount)), x, height, this.fontSize, TextAlign.Center);
    }
    // 繪制橫線也是和上面一樣的方法,就是要注意畫布的 y 軸向下,需要用 height 減一下,或者用 scale(1, -1);
    for (let j = Math.floor(leftY / gridHeight); j * gridHeight < rightY; j++) {
        let y = (j * gridWidth - leftY) / yLen * height;
        y = height - y;
        const color = j ? '#ddd' : '#000';
        this.drawLine(0, y, width, y, color);
        this.fillText(String(this.formatNum(j * gridHeight, this.fixedCount)), 0, y, this.fontSize, TextAlign.Middle);
    }
    ctx2d?.restore();
}
// 保留 fixedCount 位小數(shù),整數(shù)不補零
formatNum(num: number, fixedCount: number): number {
    return parseFloat(num.toFixed(fixedCount));
}

另外簡單說下為什么刻度要畫在邊上呢,不畫在軸上,因為只要你騷微移動或者放大下畫布,坐標(biāo)軸就不在視線范圍內(nèi)了,刻度也就看不見了,所以不如一開始就放邊上。

4、刻度總是會有浮點數(shù)

刻度和坐標(biāo)點這種東西總是要做些加減乘除的運算,自然而然就會遇到我們前端知名的浮點數(shù)問題,所以這個是需要處理一下的,比如保留幾位小數(shù)。當(dāng)然關(guān)于浮點數(shù)的運算是有庫可以用的,大致的思路分為兩種:

  • 變成整數(shù)在計算(先把數(shù)字都乘以10的n次方,最后除以10的n次方)
  • 變成字符串來計算(類似于小學(xué)數(shù)學(xué)的計算方式,比如乘法,與個位、十位、百位...分別相乘再相加)

第二步:畫函數(shù)曲線

畫函數(shù)曲線的思路很直白,就是以直代曲,將多個點用線連起來就行了,我們會先通過 y = fn(x) (fn 大概長這樣 sin(x)、1 / x 這樣)來算出函數(shù)的坐標(biāo)點,然后將函數(shù)的坐標(biāo)點換算成畫布上的點,最后繪制出來。下面是一些要小小注意的點:

  • 點越多畫出來的曲線就越平滑,這里我們可以通過參數(shù) steps 來控制,默認(rèn)值為 1。
  • 計算出來的點可以不需要保存下來,因為你一移動、縮放,值就全變了。
  • 記得在繪制每條函數(shù)開始前 beginPath 一下,否則會影響到下一條曲線的繪制(比如顏色、線寬)。
  • 對于畫布外的點我們是不需要繪制的,直接用 moveTo 即可(但不是用 continue 跳過)。
/** 繪制函數(shù)曲線,就是用一段段直線連起來 */
drawFn() {
    const { width, height, leftX, leftY, xLen, yLen, steps, ctx2d } = this;
    if (!ctx2d) return;
    ctx2d.save();
    this.fnList.forEach(fn => {
        ctx2d.strokeStyle = (fn as any).color;
        ctx2d.beginPath();
        for (let i = 0; i < width; i += steps) {
            // 小小的公式推導(dǎo):像素點 / 畫布寬 = x / 實際表示的 x 軸長度
            const x = i / width * xLen + leftX;
            let y = fn(x);
            if (isNaN(y)) continue;
            // 換算到具體繪制點
            y = height - (y - leftY) / yLen * height;
            // 在畫布之外是不用繪制的所以用 moveTo 即可
            if (i === 0 || y > height || y < 0) {
                ctx2d.moveTo(i, y);
            } else {
                ctx2d.lineTo(i, y);
            }
        }
        ctx2d.stroke();
    });
    ctx2d.restore();
}

這里想強調(diào)一點的就是在繪制前后時刻記得要 save 和 restore,并且這兩個方法要配對使用。我們都應(yīng)該聽過 canvas 是基于狀態(tài)管理的,如果你想要畫一條紅色的線,我們需要把畫筆設(shè)置成紅色,不使用 save 和 restore 的話,這個紅色會影響到接下來的所有操作,所以一般我們要養(yǎng)成下面這樣的習(xí)慣(尤其是平移、旋轉(zhuǎn)和縮放等操作,不然一臉懵逼不是夢??):

// 初始狀態(tài)
ctx2d.save();
// 在這里設(shè)置各種狀態(tài)1
    ctx2d.save();
    // 在這里設(shè)置各種狀態(tài)2
    ctx2d.restore();
// 這里是狀態(tài)1
ctx2d.restore();
// 這里是初始狀態(tài)

之所以可以嵌套使用是因為 canvas 使用了棧(只能 push 和 pop 的數(shù)組)的方式來管理狀態(tài),就像這樣 [初始狀態(tài)的集合、狀態(tài)1的集合、狀態(tài)2的集合],save 就是將當(dāng)前狀態(tài)往數(shù)組末尾追加,restore 就是取出數(shù)組最后一項當(dāng)做當(dāng)前狀態(tài)。而集合你可以理解成是一個大對象,里面有各種屬性,形如 {strokeStyle, fillStyle, lineWidth} 這樣。

第三步:繪制輔助線和交點坐標(biāo)

這個很簡單,首先我們能夠知道鼠標(biāo)相對于畫布的坐標(biāo),然后換算成曲線的 x 值,代入每個函數(shù)中計算出一個 y 值,如果 y 是個有效值,那么就在點 [x, y] 處畫個圓即可,記得數(shù)字都格式化一下的。

drawSubLine(canvasPos: Point) {
    const ctx2d: CanvasRenderingContext2D | null = this.ctx2d;
    if (!ctx2d) return;
    const { width, height } = this;
    const { x, y } = canvasPos;
    // 先重新繪制網(wǎng)格和函數(shù)曲線
    this.draw();
    // 繪制輔助線
    ctx2d.save();
    this.drawLine(x, 0, x, height, '#999',true);
    this.drawLine(0, y, width, y, '#999', true);
    ctx2d.restore();
    // 繪制鼠標(biāo)點
    const centerRectLen: number = 8;
    this.strokeRect(x - centerRectLen / 2, y - centerRectLen / 2, centerRectLen, centerRectLen);
    const actualPos = this.canvasPosToFnVal(canvasPos);
    // 繪制曲線和輔助線的交點坐標(biāo)
    this.handleCrosspoint(actualPos.x);
}
handleCrosspoint(x: number) {
    const pointList: Point[] = this.checkCrosspoint(x);
    pointList.forEach(point => {
        const { x, y } = this.fnValToCanvasPos(point);
        this.fillCircle(x, y, 4, 'red');
        this.fillText(`[${this.formatNum(point.x, this.fixedCount + 1)}, ${this.formatNum(point.y, this.fixedCount + 1)}]`, x, y, this.fontSize);
    });
}

繪制輔助線的時候我們總是會先重新繪制網(wǎng)格和曲線,這個是可以優(yōu)化的。我們可以進(jìn)行分層處理,因為鼠標(biāo)移動的時候,就只會改變輔助線和坐標(biāo)點,所以我們可以將網(wǎng)格和函數(shù)曲線放在下層,輔助線和坐標(biāo)點放在上層,這樣每次繪制的時候只要重新繪制上層就可以了。

第四步:平移

首先肯定是要加個事件監(jiān)聽的:

this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this), false);
// 也可以把 mouseup 和 mousemove 這兩個監(jiān)聽寫在 mousedown 的回調(diào)中
document.addEventListener('mouseup', this.handleMouseUp.bind(this), false);
document.addEventListener('mousemove', this.handleMouseMove.bind(this), false);

平移應(yīng)該是眾多操作中最簡單的一個,平移的結(jié)果就是改變了 x 軸和 y 軸的邊界值,所以只需要重新計算出邊界值,重新調(diào)用繪制函數(shù)即可。我們用 startPos 和 endPos 來計算平移的距離,就像下面這樣:

// 返回距離畫布左上角的點
viewportToCanvasPosition(e: MouseEvent): Point {
    const { clientX, clientY } = e;
    const { top, left } = this.canvas.getBoundingClientRect();
    const x = clientX - top;
    const y = clientY - left;
    return new Point(x, y);
}
// 記錄鼠標(biāo)按下的點
handleMouseDown(e: MouseEvent) {
    const canvasPos: Point = this.viewportToCanvasPosition(e);
    this.state.startPos = canvasPos;
}
// 計算平移距離,更新 leftX 和 rightX 的值,然后重新繪制
handleMouseMove(e: MouseEvent) {
    const canvasPos: Point = this.viewportToCanvasPosition(e);
    if (!this.state.startPos) return;
    document.body.style.cursor = 'move';
    this.state.endPos = canvasPos;
    const { width, height, xLen, yLen, state: { startPos, endPos } } = this;
    // 算出平移距離
    const dx = (endPos.x - startPos.x) / width * xLen;
    const dy = (endPos.y - startPos.y) / height * yLen;
    // 更新邊界值
    this.leftX -= dx;
    this.rightX -= dx;
    this.leftY += dy;
    this.rightY += dy;
    this.xLen = this.rightX - this.leftX;
    this.yLen = this.rightY - this.leftY;
    this.draw();
    this.state.startPos = canvasPos;
}
// 還原拖拽前的狀態(tài)
handleMouseUp(e: MouseEvent) {
    this.state.startPos = null;
    this.state.endPos = null;
    document.body.style.cursor = 'auto';
}

要注意的是如果我們往右平移,上面代碼中的 dx 應(yīng)該是正值,但刻度是減小的,所以計算的時候應(yīng)該用減號,其它方向同理。

第五步:縮放

首先就是要監(jiān)聽滾輪事件:

this.canvas.addEventListener('mousewheel', this.handleMouseWheel.bind(this), false);

同樣的,縮放的核心也是改變邊界值,但是這個推導(dǎo)會比平移騷微麻煩一些,自己動手畫個圖會比較清晰點。另外和平移不一樣的是,縮放應(yīng)該是以當(dāng)前點為中心進(jìn)行縮放,并且是一邊加一邊減。啥意思呢?就是比如你在畫布中心放大的時候,leftX 的值應(yīng)該是增加的,rightX 應(yīng)該是減少的。還有就是縮放應(yīng)該要有個限制,無限大或者無限小都沒啥太大意義。下面是代碼和圖的示例:

handleMouseWheel(e: Event) {
    e.preventDefault();
    const event: WheelEvent = e as WheelEvent;
    const canvasPos: Point = this.viewportToCanvasPosition(event);
    const { deltaY } = event;
    const { leftX, rightX, leftY, rightY } = this;
    const scale: number = deltaY > 0 ? 1.1 : 0.9;
    if (this.isInvalidVal(scale)) return;
    const { x, y } = this.canvasPosToFnVal(canvasPos);
    // 注意縮放和平移不一樣,軸的左右兩邊一邊是加一邊是減
    this.leftX = x - (x - leftX) * scale;
    this.rightX = x + (rightX - x) * scale;
    this.leftY = y - (y - leftY) * scale;
    this.rightY = y + (rightY - y) * scale;
    this.xLen = this.rightX - this.leftX;
    this.yLen = this.rightY - this.leftY;
    this.draw();
}
/** 縮放過大過小都沒啥意義,所以設(shè)置下邊界值 */
isInvalidVal(ratio: number): boolean {
    const { xLen, yLen, MIN_DELTA, MAX_DELTA } = this;
    if (ratio > 1 && (xLen > MAX_DELTA || yLen > MAX_DELTA)) return true;
    if (ratio < 1 && (xLen < MIN_DELTA || yLen < MIN_DELTA)) return true;
    // 上面的判斷為什么不直接 (xLen > MAX_DELTA || yLen > MAX_DELTA || xLen < MIN_DELTA || yLen < MIN_DELTA)這樣判斷呢?
    // 因為如果這樣判斷你會發(fā)現(xiàn)縮放到最大和最小的時候,再繼續(xù)操作都是無效的。
    return false;
}
canvasPosToFnVal(canvasPos: Point): Point {
    const { width, height, leftX, leftY, xLen, yLen } = this;
    const x = leftX + canvasPos.x / width * xLen;
    const y = leftY + canvasPos.y / height * yLen;
    return new Point(x, y);
}

第六步:動態(tài)繪制曲線

既然你已經(jīng)會畫函數(shù)曲線了,那動態(tài)繪制也不在話下。就是用 requestAnimationFrame、setTimeout 或者 setInterval 一次畫一段,繪制的快慢可以通過調(diào)整時間參數(shù)或者每次畫多段來調(diào)整,然后有以下幾個注意點:

  • 動畫的時候整個畫布應(yīng)該是不可操作的狀態(tài)。
  • 我們的曲線是一條繪制完再繪制另一條的,而不是從左到右同時繪制每條曲線,當(dāng)然怎么做都可以。
  • 有的曲線左邊開始部分沒有圖像,就會出現(xiàn)等待一段時間后才開始繪制的情況,所以我們應(yīng)該跳過不用繪制的點。

第七步:模糊到高清

其實寫到最后你會發(fā)現(xiàn)曲線好像有點模糊,事實上整個畫布都會有點模糊,這在 canvas 中是個很常見的問題,以上就是JS前端使用canvas動態(tài)繪制函數(shù)曲線示例詳解的詳細(xì)內(nèi)容,更多關(guān)于JS canvas繪制函數(shù)曲線的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 文字幻燈片

    文字幻燈片

    文字幻燈片...
    2006-06-06
  • JS異步觀察目標(biāo)元素方式完成分頁加載

    JS異步觀察目標(biāo)元素方式完成分頁加載

    這篇文章主要為大家介紹了異步觀察目標(biāo)元素方式完成分頁加載示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-07-07
  • Hooks封裝與使用示例詳解

    Hooks封裝與使用示例詳解

    這篇文章主要為大家介紹了Hooks封裝與使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-01-01
  • Performance 內(nèi)存監(jiān)控使用技巧詳解

    Performance 內(nèi)存監(jiān)控使用技巧詳解

    這篇文章主要為大家介紹了Performance 內(nèi)存監(jiān)控使用技巧詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-10-10
  • 淺談TypeScript 索引簽名的理解

    淺談TypeScript 索引簽名的理解

    這篇文章主要給大家分享的是TypeScript 索引簽名的理解,索引簽名由方括號中的索引名稱及其類型組成,后面是冒號和值類型:{ [indexName: KeyType]: ValueType }, KeyType 可以是一個 string、number 或 symbol,而ValueType 可以是任何類型,下面就倆簡單了解一下吧
    2021-10-10
  • javascript 判斷是否是微信瀏覽器的方法

    javascript 判斷是否是微信瀏覽器的方法

    這篇文章主要介紹了javascript 判斷是否是微信瀏覽器的方法的相關(guān)資料,需要的朋友可以參考下
    2016-10-10
  • JS前端面試數(shù)組扁平化手寫flat函數(shù)示例

    JS前端面試數(shù)組扁平化手寫flat函數(shù)示例

    這篇文章主要為大家介紹了JS前端面試數(shù)組扁平化手寫flat函數(shù)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-07-07
  • 微信小程序 條件渲染詳解

    微信小程序 條件渲染詳解

    這篇文章主要介紹了微信小程序 條件渲染詳解的相關(guān)資料,需要的朋友可以參考下
    2016-10-10
  • 最新評論