【canvas】曲線圖繪制使用貝塞爾曲線

GIF 2023-10-30 18-46-11.gif
<div>
    <canvas id="c1" width="500" height="255"></canvas>
    <button onclick="download()">download</button>
    <img id="img" src="" />
</div>
<style>
    * {
        margin: 0;
        padding: 0;
    }
    div {
        padding: 20px;
    }
    body {
        background-color: #333;
    }
    canvas {
        border-radius: 6px;
        background-color: #fff;
    }
</style>
<script src="./my-canvas.js"></script>
<script>
function download() {
    line.download()
}
</script>

class Line {
    /** 邊距 */
    padding = {
        left: 0,
        top: 0,
        bottom: 0,
        right: 0
    }
    /** Y軸標(biāo)尺 */
    axias = { max: 100, show: true, number: 5 }
    /** 間隔 */
    gap = 0
    margin = 12
    shadowX = 7
    shadowWidth = 4
    shadowOpacity = 0.1
    constructor(dom, data, options) {
        let element = document.querySelector(dom)
        this.element = element
        this.ctx = this.element.getContext('2d')
        this.data = JSON.parse(JSON.stringify(data))

        let { padding, axias, labels } = options
        padding = { ...this.padding, ...padding }
        this.padding = padding
        let { width, height } = element
        this.width = width
        this.height = height
        this.contentHight = height - padding.top - padding.bottom
        this.contentWidth = width - padding.left - padding.right
        // this.drawLines()
        this.setGap()
        this.axias = { ...this.axias, ...axias }
        this.AniPoints = this.setPoints()
        this.AniShadows = this.setPoints(true)
        this.points = []
        this.shadows = []
        // console.log(this.points)


        this.labels = labels
        // this.drawXLine()
        // this.drawxLabels()
        // this.drawYLabels()
        this.animation()
        // this.drawSmoothLines()
        // this.drawSmoothLines(true)
    }
    animation() {
        let { AniPoints, AniShadows, ctx, width, height } = this
        this.points.push(AniPoints.shift())
        this.shadows.push(AniShadows.shift())
        ctx.clearRect(0, 0, width, height)
        this.drawXLine()
        this.drawxLabels()
        this.drawYLabels()
        this.drawSmoothLines()
        this.drawSmoothLines(true)

        window.requestAnimationFrame(() => {
            if (AniPoints.length) {
                this.animation()
            }
        })
    }
    /** 橫向底部信息 */
    drawxLabels() {
        let { labels, padding: { left, bottom }, gap, ctx, height, margin } = this
        labels.forEach((item, index) => {
            ctx.fillStyle = '#555'
            ctx.font = '10px Arial'
            let x = gap * index + left - 10 + margin
            let y = height - bottom + 16
            ctx.fillText(item, x, y)
        })
    }
    /** 橫軸線 */
    drawXLine() {
        let { axias: { max, show, number }, padding: { left, right, top }, width, contentHight, ctx } = this
        if (!show) { return }
        let g = max / (number - 1) * (contentHight / max)
        this.yGap = g
        ctx.save()

        for (let i = 0; i < number; i++) {
            let endPoint = Number((i * g + top).toFixed(0)) + 0.5
            ctx.beginPath()
            ctx.moveTo(left, endPoint)
            ctx.lineTo(width - right, endPoint)
            ctx.strokeStyle = '#ddd'
            ctx.lineWidth = 1
            ctx.stroke()
            ctx.closePath()
        }

        ctx.restore()
    }
    /** 豎軸線 */
    drawYLabels() {
        let { axias: { max, show, number }, padding: { left, top }, ctx } = this
        if (!show) { return }
        let g = max / (number - 1)
        let toLeft = {
            1: 0,
            2: 8,
            3: 16
        }
        ctx.save()
        for (let i = 0; i < number; i++) {
            let text = (max - i * g).toFixed(0)
            ctx.fillStyle = '#555'
            ctx.font = '14px Arial'
            // console.log(top + i * this.yGap)
            ctx.textAlign = 'end'
            ctx.textBaseline = 'middle'
            ctx.fillText(text, left - 8, top + i * this.yGap + 0.5)
        }
        ctx.restore()
    }
    /** 橫向間隔 */
    setGap() {
        let { contentWidth, data: { length }, margin } = this
        this.gap = (contentWidth - margin * 2) / (length - 1)
    }
    /** 結(jié)合Y軸標(biāo)尺衡未,轉(zhuǎn)化為px數(shù)值 */
    setPoints(isShadow) {
        let { gap, padding: { left, top }, axias: { max }, data, contentHight, margin, shadowX } = this
        return data.map((item, index) => {
            return [index * gap + left + margin, contentHight - (item * contentHight / max) + top + (isShadow ? shadowX : 0)]
        })
    }
    /** 畫線 */
    drawLines(isShadow) {
        let { ctx, points, shadows } = this
        points = isShadow ? shadows : points
        ctx.save()
        ctx.beginPath()
        ctx.moveTo(points[0][0], points[0][1])
        if (points[1]) {
            ctx.lineTo(points[1][0], points[1][1])
        }

        ctx.stroke()
        ctx.closePath()
        ctx.restore()
    }
    /** 畫平滑線 */
    drawSmoothLines(isShadow) {
        let { ctx, points, drawLines, width, height, shadows, shadowWidth } = this
        points = isShadow ? shadows : points
        // ctx.clearRect(0, 0, 400, 205)
        if (points.length < 3) {
            return drawLines.call(this)
        }

        let f = 0.3
        ctx.save()
        ctx.beginPath()
        ctx.moveTo(points[0][0], points[0][1])
        let dx1 = 0
        let dy1 = 0
        let dx2 = 0
        let dy2 = 0
        let prevPoint = points[0]
        let nextPoint = null
        for (let i = 1; i < points.length; i++) {
            let currtPoint = points[i]
            nextPoint = points[i + 1]
            if (nextPoint) {
                dx2 = -(nextPoint[0] - currtPoint[0]) * f
            } else {
                dx2 = 0
                dy2 = 0
            }
            ctx.bezierCurveTo(prevPoint[0] - dx1, prevPoint[1] - dy1, currtPoint[0] + dx2, currtPoint[1] + dy2, currtPoint[0], currtPoint[1])
            dx1 = dx2
            dy1 = dy2
            prevPoint = currtPoint
        }
        let grd = ctx.createLinearGradient(0, height / 2, width, height / 2)
        grd.addColorStop(0, `rgba(14, 185, 160, ${isShadow ? 0.1 : 1})`)
        grd.addColorStop(1, `rgba(160, 209, 118, ${isShadow ? 0.1 : 1})`)
        // 將漸變賦值給線的樣式
        ctx.strokeStyle = grd
        ctx.lineWidth = isShadow ? shadowWidth : 2
        ctx.stroke()
        ctx.closePath()
        ctx.restore()
    }
    download() {
        this.element.toBlob(blob => {
            console.log(blob)
            let img = new Image()
            let url = URL.createObjectURL(blob)
            document.querySelector('#img').src = url
            console.log(url)
            img.onload = function () {
                URL.revokeObjectURL(url)
            }
            img.src = url
            console.log(img)
        }, "image/png", 1)
    }
}
let data = [70, 45, 120, 100, 110, 80, 70, 102, 120, 80, 80, 90]
let line = new Line('#c1', data, {
    padding: {
        left: 40,
        right: 30,
        top: 28,
        bottom: 38
    },
    axias: { max: 200, show: true, number: 6 },
    labels: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
})
class Bg {
    /** Y軸標(biāo)尺 */
    axias = { max: 100, show: true }
    constructor(dom, options) {
        let element = document.querySelector(dom)
        this.element = element
        this.ctx = this.element.getContext('2d')
        let { axias } = options
        this.axias = { ...this.axias, ...axias }
        let { width, height } = element
        this.width = width
        this.height = height
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末雕什,一起剝皮案震驚了整個濱河市蚤吹,隨后出現(xiàn)的幾起案子铛绰,更是在濱河造成了極大的恐慌,老刑警劉巖硝烂,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件企量,死亡現(xiàn)場離奇詭異俄周,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)疚顷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門旱易,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人腿堤,你說我怎么就攤上這事阀坏。” “怎么了笆檀?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵忌堂,是天一觀的道長。 經(jīng)常有香客問我酗洒,道長士修,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任樱衷,我火速辦了婚禮棋嘲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘矩桂。我一直安慰自己沸移,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布侄榴。 她就那樣靜靜地躺著雹锣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪癞蚕。 梳的紋絲不亂的頭發(fā)上蕊爵,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機(jī)與錄音涣达,去河邊找鬼在辆。 笑死,一個胖子當(dāng)著我的面吹牛度苔,可吹牛的內(nèi)容都是我干的匆篓。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼寇窑,長吁一口氣:“原來是場噩夢啊……” “哼鸦概!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤窗市,失蹤者是張志新(化名)和其女友劉穎先慷,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咨察,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡论熙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了摄狱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脓诡。...
    茶點(diǎn)故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖媒役,靈堂內(nèi)的尸體忽然破棺而出祝谚,到底是詐尸還是另有隱情,我是刑警寧澤酣衷,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布交惯,位于F島的核電站,受9級特大地震影響穿仪,放射性物質(zhì)發(fā)生泄漏席爽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一牡借、第九天 我趴在偏房一處隱蔽的房頂上張望拳昌。 院中可真熱鬧,春花似錦钠龙、人聲如沸炬藤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沈矿。三九已至,卻和暖如春咬腋,著一層夾襖步出監(jiān)牢的瞬間羹膳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工根竿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留陵像,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓寇壳,卻偏偏與公主長得像醒颖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子壳炎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容