項(xiàng)目地址https://github.com/zz632893783/canvasShape
預(yù)覽
如何選中繪制在canvas上的圖形
安裝依賴模塊
npm install
運(yùn)行項(xiàng)目
npm run dev
問(wèn)題
繪制在 canvas 上的圖形并不像 html 元素一樣是分開的元素,當(dāng)圖形繪制在canvas上之后这嚣,它們已經(jīng)變成了單純的像素垂券,你在 canvas 上點(diǎn)擊它的時(shí)候宾符,并不會(huì)有任何反應(yīng)厂捞,我們需要其做特殊處理解虱,使它像普通的 html 元素一樣
思路
- 如果是圓形捅儒,描述一個(gè)圓形查刻,我們需要的是圓心的 x,y 坐標(biāo)秧饮,圓的半徑映挂,還有圓的顏色
- 如果是矩形,描述一個(gè)矩形(這里先不考慮矩形旋轉(zhuǎn))浦楣,我們需要的是矩形的起始點(diǎn)(左上角點(diǎn))的 x袖肥,y 坐標(biāo),以及矩形的長(zhǎng)寬振劳,還有矩形的顏色
- 如果是正三角形(這里先不考慮正三角形旋轉(zhuǎn))椎组,我們需要的是三角形中心點(diǎn)的 x,y 坐標(biāo)历恐,三角形的邊長(zhǎng)寸癌,還有三角形的顏色
除此之代专筷,每種圖形還應(yīng)有一個(gè)參數(shù)用以表示上下級(jí)關(guān)系
總而言之描述每種圖形,所需要的參數(shù)都不相同蒸苇,所以可以將每一種圖形都分別聲明一個(gè)類磷蛹,在它們各自的構(gòu)造函數(shù)中,分別設(shè)置描述該種圖形所需要的數(shù)據(jù)
// 圓形
class Circle {
constructor (x, y, radius, fillStyle, zIndex = 0) {
this.type = 'circle'
this.x = x
this.y = y
this.radius = radius
this.fillStyle = fillStyle
this.zIndex = zIndex
}
}
// 矩形
class Rectangle {
constructor (x, y, width, height, fillStyle, zIndex = 0) {
this.type = 'rectangle'
this.x = x
this.y = y
this.width = width
this.height = height
this.fillStyle = fillStyle
this.zIndex = zIndex
}
}
// 三角形
class Triangle {
constructor (x, y, side, fillStyle, zIndex = 0) {
this.type = 'triangle'
this.x = x
this.y = y
this.side = side
this.fillStyle = fillStyle
this.zIndex = zIndex
}
}
描述每種圖形的數(shù)據(jù)都不相同溪烤,它們繪制在canvas上使用的方式也不相同
- 圓形使用的是 canvas 的 arc 方法
- 矩形可以使用 canvas 的 rect 方法味咳,也可以使用 moveTo 和 lineTo 方法
- 三角形使用 canvas 的 moveTo 和 lineTo 方法
三角形使用 moveTo 和 lineTo 方法的話,需要知道三角形三個(gè)頂點(diǎn)的 x檬嘀,y 坐標(biāo)槽驶,我們?cè)谌切蚊看螌?shí)例化的時(shí)候,自動(dòng)調(diào)用方法計(jì) computePoint 算三個(gè)點(diǎn)的位置鸳兽,需要用到中學(xué)時(shí)候?qū)W到的三角函數(shù)知識(shí)
class Triangle {
constructor (x, y, side, fillStyle, zIndex = 0) {
this.type = 'triangle'
this.x = x
this.y = y
this.side = side
this.fillStyle = fillStyle
this.zIndex = zIndex
this.computePoint()
}
computePoint () {
this.pointList = []
this.pointList.push({
x: this.x,
y: this.y - this.side / Math.pow(3, 1 / 2)
})
this.pointList.push({
x: this.x + this.side / 2,
y: this.y + this.side / Math.pow(3, 1 / 2) / 2
})
this.pointList.push({
x: this.x - this.side / 2,
y: this.y + this.side / Math.pow(3, 1 / 2) / 2
})
}
}
如何繪制
我們?cè)诿總€(gè)圖形的原型上聲明一個(gè) draw 方法
如果是圓形的話雳攘,使用 arc 方法
draw (ctx) {
ctx.beginPath()
ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
ctx.closePath()
ctx.fillStyle = this.fillStyle
ctx.fill()
}
如果是矩形的話忽舟,使用 rect 方法
draw (ctx) {
ctx.beginPath()
ctx.rect(this.x, this.y, this.width, this.height)
ctx.closePath()
ctx.fillStyle = this.fillStyle
ctx.fill()
}
如果是三角形的話卧须,使用 moveTo 和 lineTo 方法萄传,三角形實(shí)例化的時(shí)候,已經(jīng)調(diào)用了 computePoint 計(jì)算了三角形三個(gè)頂點(diǎn)的位置衷掷,保存在 this.pointList 中
draw (ctx) {
ctx.beginPath()
ctx.moveTo(this.pointList[0].x, this.pointList[0].y)
ctx.lineTo(this.pointList[1].x, this.pointList[1].y)
ctx.lineTo(this.pointList[2].x, this.pointList[2].y)
ctx.closePath()
ctx.fillStyle = this.fillStyle
ctx.fill()
}
繪制到 canvas 中
聲明三種圖形類之后辱姨,調(diào)用它們并繪制到 canvas 中,首先就是分別實(shí)例化三種圖形棍鳖,保存在 shapeList 中炮叶,window.requestAnimationFrame 不停地刷新碗旅,每次遍歷 shapeList 的時(shí)候渡处,分別調(diào)用每個(gè)實(shí)例的 draw 方法繪制圖形
<template>
<canvas class="canvasShape" ref="canvasShape" v-bind:width="width" v-bind:height="height"></canvas>
</template>
<script>
import Circle from '@/lib/circle'
import Triangle from '@/lib/triangle'
import Rectangle from '@/lib/rectangle'
export default {
data: function () {
return {
ctx: null,
width: 800,
height: 500,
shapeList: [
new Circle(100, 100, 50, 'red', 0),
new Triangle(200, 200, 100, 'green', 1),
new Rectangle(300, 300, 100, 50, 'blue', 2)
]
}
},
methods: {
init: function () {
this.ctx = this.$refs.canvasShape.getContext('2d')
},
draw: function () {
this.ctx.clearRect(0, 0, this.width, this.height)
this.shapeList.forEach(shape => shape.draw(this.ctx))
},
animationFrame: function () {
window.requestAnimationFrame(() => {
this.draw()
this.animationFrame()
})
}
},
mounted: function () {
this.init()
this.animationFrame()
}
}
</script>
<style lang="stylus" scoped>
.canvasShape {
box-sizing: content-box;
border: 1px solid;
}
</style>
如何判斷鼠標(biāo)選中了圖形
和繪制圖形一樣,每種圖形判斷是否選中也不一樣祟辟,所以同樣的医瘫,在每種圖形的原型上聲明一個(gè) isHover 方法,判斷是否選中
- 圓形: 回憶下中學(xué)數(shù)學(xué)旧困,判斷一個(gè)點(diǎn)是否在圓內(nèi)部的方法醇份,點(diǎn)距離圓心的距離 < 圓的半徑,就可認(rèn)為這個(gè)點(diǎn)在圓內(nèi)部
isHover (x, y) {
// 勾股定理
return (Math.pow(x - this.x, 2) + Math.pow(y - this.y, 2)) <= Math.pow(this.radius, 2)
}
- 矩形:矩形非常簡(jiǎn)單吼具,矩形左側(cè)的 x 坐標(biāo) < 鼠標(biāo)點(diǎn)擊的 x 坐標(biāo) < 矩形右側(cè)的 x 坐標(biāo)僚纷,并且 矩形上側(cè)的 y 坐標(biāo) < 鼠標(biāo)點(diǎn)擊的 y 坐標(biāo) < 矩形下側(cè)的 y 坐標(biāo),可認(rèn)為這個(gè)點(diǎn)在矩形內(nèi)部
isHover (x, y) {
return this.x <= x && this.x + this.width >= x && this.y <= y && this.y + this.height >= y
}
-
三角形:這里又得運(yùn)用中學(xué)數(shù)學(xué)的知識(shí)了拗盒,直線方程式是 y = a * x + b怖竭,我們有一個(gè)點(diǎn) (m, n),將 x = m 帶入方程陡蝇,通過(guò)比較 a * m + b 與 n 的大小關(guān)系痊臭,可以判斷 (m, n) 點(diǎn)是在直線的上方還是直線的下方哮肚。類比到我們的代碼中,判斷 D 點(diǎn)是否在三角形內(nèi)部广匙,可以轉(zhuǎn)換為允趟,D 是否在直線AB的下方,并且在直線AC的下方鸦致,并且在直線BC的上方潮剪,而三角形三個(gè)頂點(diǎn)的坐標(biāo)我們?cè)趯?shí)例化三角形的時(shí)候,就已經(jīng)計(jì)算并保存在 pointList 中了
然后又得運(yùn)用到中學(xué)數(shù)學(xué)的知識(shí)了分唾,已知兩個(gè)點(diǎn)鲁纠,如何求這條直線的方程
// 傳入不同的點(diǎn)鳍寂,生成這條直線的函數(shù)
createLineFunctionByPoint (firstPoint, lastPoint) {
return x => (firstPoint.y - lastPoint.y) / (firstPoint.x - lastPoint.x) * x + firstPoint.y - (firstPoint.y - lastPoint.y) / (firstPoint.x - lastPoint.x) * firstPoint.x
}
直線 AB 的方程為
createLineFunctionByPoint(pointA, pointB)
判斷鼠標(biāo)點(diǎn)擊的 (mouseX, mouseY) 是否在直線AB的下方改含,則
// 由于我們的 canvas 坐標(biāo)系的 y 軸和數(shù)學(xué)作坐標(biāo)系相反,所以這里是 <= mouseY
createLineFunctionByPoint(pointA, pointB)(mouseX) <= mouseY
我們代碼中的 pointA 即 shapeList[0]迄汛,pointB 即 shapeList[2]
代碼寫成
isHover (x, y) {
if (this.createLineFunctionByPoint(this.pointList[0], this.pointList[2])(x) <= y) {
console.log('點(diǎn)擊位置在直線AB的下方')
}
}
同理我們帶入不同的點(diǎn)捍壤,用以判斷 D 點(diǎn)是否在 AC 下方,D 點(diǎn)是否在 BC 上方
isHover (x, y) {
let result = false
if (this.createLineFunctionByPoint(this.pointList[0], this.pointList[1])(x) <= y && this.createLineFunctionByPoint(this.pointList[0], this.pointList[2])(x) <= y && this.createLineFunctionByPoint(this.pointList[1], this.pointList[2])(x) >= y) {
result = true
}
return result
}
點(diǎn)擊 canvas
當(dāng)點(diǎn)擊 canvas 的時(shí)候鞍爱,取得鼠標(biāo)點(diǎn)擊的 x, y坐標(biāo)鹃觉,遍歷 shapeList 中的每個(gè)圖形實(shí)例,調(diào)用每個(gè)實(shí)例的 isHover 方法睹逃,判斷是否選中了圖形盗扇,如果點(diǎn)擊到了圖形的重合區(qū)域,則比較實(shí)例化時(shí)候傳入的 zIndex 參數(shù)沉填,zIndex 大的為點(diǎn)擊的圖形
mousedownFunc: function (event) {
let x = event.offsetX
let y = event.offsetY
let hoverList = []
// 首先篩選出點(diǎn)擊的圖形
this.shapeList.forEach(shape => {
shape.isHover(x, y) && (hoverList.push(shape))
})
if (hoverList.length) {
// 對(duì)選中的圖形做排序疗隶,zIndex最大的那個(gè)圖形即當(dāng)前鼠標(biāo)選擇的圖形
hoverList.sort((x, y) => y.zIndex - x.zIndex)
// 將當(dāng)前選中的圖形實(shí)例賦值到 this.activeShape
this.activeShape = hoverList[0]
// 將當(dāng)前選中的圖形的 zIndex 設(shè)置為最大
this.activeShape.zIndex = Math.max(...this.shapeList.map(shape => shape.zIndex)) + 1
// 將當(dāng)前的 shapeList 排序,確保 zIndex 越大的圖形越后繪制
this.shapeList.sort((x, y) => x.zIndex - y.zIndex)
}
}
移動(dòng)圖形
點(diǎn)擊圖形的時(shí)候翼闹,首先記錄一次點(diǎn)擊位置距離圖形標(biāo)記位置的偏移量
然后移動(dòng)鼠標(biāo)的時(shí)候斑鼻,圖形位置x = 鼠標(biāo)當(dāng)前x - x方向的偏移量,將這兩個(gè)函數(shù)寫在每個(gè)圖形的原型中
setOffset (x, y) {
this.offsetX = x - this.x
this.offsetY = y - this.y
}
setPosition (x, y) {
this.x = x - this.offsetX
this.y = y - this.offsetY
}
由于三角形的繪制方法是由 moveTo 和 lineTo 連線構(gòu)成的猎荠,所以移動(dòng)的時(shí)候坚弱,需要重新調(diào)用一次 computePoint 計(jì)算三個(gè)點(diǎn)的位置
setPosition (x, y) {
this.x = x - this.offsetX
this.y = y - this.offsetY
this.computePoint()
}
點(diǎn)擊 canvas 的時(shí)候調(diào)用實(shí)例的 setOffset 方法
mousedownFunc: function (event) {
let x = event.offsetX
let y = event.offsetY
let hoverList = []
// 首先篩選出點(diǎn)擊的圖形
this.shapeList.forEach(shape => {
shape.isHover(x, y) && (hoverList.push(shape))
})
if (hoverList.length) {
// 對(duì)選中的圖形做排序,zIndex最大的那個(gè)圖形即當(dāng)前鼠標(biāo)選擇的圖形
hoverList.sort((x, y) => y.zIndex - x.zIndex)
// 將當(dāng)前選中的圖形實(shí)例賦值到 this.activeShape
this.activeShape = hoverList[0]
// 設(shè)置該圖形的偏移量
this.activeShape.setOffset(x, y)
// 將當(dāng)前選中的圖形的 zIndex 設(shè)置為最大
this.activeShape.zIndex = Math.max(...this.shapeList.map(shape => shape.zIndex)) + 1
// 將當(dāng)前的 shapeList 排序关摇,確保 zIndex 越大的圖形越后繪制
this.shapeList.sort((x, y) => x.zIndex - y.zIndex)
}
}
移動(dòng)鼠標(biāo)的時(shí)候荒叶,重新設(shè)置圖形的位置
mousemoveFunc: function (event) {
// 如果現(xiàn)在已經(jīng)選中了一個(gè)圖形
if (this.activeShape) {
let x = event.offsetX
let y = event.offsetY
this.activeShape.setPosition(x, y)
}
}
其他圖形
例如要繪制一個(gè)五角星,我們的思路還是一樣的输虱,首選確定描述這個(gè)圖形需要的參數(shù)些楣,傳入構(gòu)造方法中
確定一個(gè)五角星,需要位置坐標(biāo) x,y戈毒,尺寸艰猬,顏色,確定圖形在 canvas 中的上下層級(jí)埋市,還需要 zIindex冠桃,并且由于五角星我們也是使用 moveTo 和 lineTo 方法,所以還需要知道五角星各個(gè)頂點(diǎn)的坐標(biāo)
constructor (x, y, side, fillStyle, zIndex = 0) {
this.type = 'triangle'
this.x = x
this.y = y
this.side = side
this.fillStyle = fillStyle
this.zIndex = zIndex
this.offsetX = this.offsetY = 0
this.computePoint()
}
根據(jù)五角星的中心店道宅,計(jì)算各個(gè)頂點(diǎn)的坐標(biāo)食听,同樣需要用到一些中學(xué)數(shù)學(xué)知識(shí)
computePoint () {
this.pointList = []
let temp = (this.side * Math.sin(18 / 180 * Math.PI) + this.side) / Math.cos(18 / 180 * Math.PI)
this.pointList.push({
x: temp * Math.cos((72 * 0 - 18 - 72) / 180 * Math.PI) + this.x,
y: temp * Math.sin((72 * 0 - 18 - 72) / 180 * Math.PI) + this.y
})
this.pointList.push({
x: temp * Math.cos((72 * 1 - 18 - 72) / 180 * Math.PI) + this.x,
y: temp * Math.sin((72 * 1 - 18 - 72) / 180 * Math.PI) + this.y
})
this.pointList.push({
x: temp * Math.cos((72 * 2 - 18 - 72) / 180 * Math.PI) + this.x,
y: temp * Math.sin((72 * 2 - 18 - 72) / 180 * Math.PI) + this.y
})
this.pointList.push({
x: temp * Math.cos((72 * 3 - 18 - 72) / 180 * Math.PI) + this.x,
y: temp * Math.sin((72 * 3 - 18 - 72) / 180 * Math.PI) + this.y
})
this.pointList.push({
x: temp * Math.cos((72 * 4 - 18 - 72) / 180 * Math.PI) + this.x,
y: temp * Math.sin((72 * 4 - 18 - 72) / 180 * Math.PI) + this.y
})
}
這里的 pointList[0],pointList[1]污茵,pointList[2]樱报,pointList[3],pointList[4] 分別對(duì)應(yīng)下圖的 ABCDE 點(diǎn)
接著是繪制圖形泞当,順序 A → C → E → B → D
draw (ctx) {
ctx.beginPath()
ctx.moveTo(this.pointList[0].x, this.pointList[0].y)
ctx.lineTo(this.pointList[2].x, this.pointList[2].y)
ctx.lineTo(this.pointList[4].x, this.pointList[4].y)
ctx.lineTo(this.pointList[1].x, this.pointList[1].y)
ctx.lineTo(this.pointList[3].x, this.pointList[3].y)
ctx.closePath()
ctx.fillStyle = this.fillStyle
ctx.fill()
}
同樣的再接著是判斷是否被選中迹蛤,將五角星拆分為 △AFJ + △BGF + △CHG + △DIH + △EJI + 五邊形FGHIJ
- 是否在 △AFJ 中可以轉(zhuǎn)化為,是否在直線 AD 下方襟士,并且在直線 AC 下方盗飒,并且在直線 EB 上方
A 點(diǎn)對(duì)應(yīng) this.pointList[0]
B 點(diǎn)對(duì)應(yīng) this.pointList[1]
C 點(diǎn)對(duì)應(yīng) this.pointList[2]
D 點(diǎn)對(duì)應(yīng) this.pointList[3]
E 點(diǎn)對(duì)應(yīng) this.pointList[4]
鼠標(biāo)點(diǎn)擊位置(mouseX, mouseY)是否在 AD 下方可轉(zhuǎn)換為
createLineFunctionByPoint(this.pointList[0], this.pointList[3])(mouseX) <= mouseY
同理可知是否在直線 AC 下方為
createLineFunctionByPoint(this.pointList[0], this.pointList[2])(mouseX) <= mouseY
是否在直線 EB 上方為
createLineFunctionByPoint(this.pointList[1], this.pointList[4])(mouseX) >= mouseY
是否在 △AFJ 中表示為
isHover (x, y) {
if (this.createLineFunctionByPoint(this.pointList[0], this.pointList[3])(x) <= y && this.createLineFunctionByPoint(this.pointList[0], this.pointList[2])(x) <= y && this.createLineFunctionByPoint(this.pointList[1], this.pointList[4])(x) >= y) {
console.log('在三角形中')
}
}
同理可得到另外四個(gè)三角形點(diǎn)擊的代碼
中心的五邊形方法也是相同,即是否在直線 IJ 陋桂,直線 JF 逆趣,直線 FG 下方,并且在直線 GH 嗜历,直線 HI 上方宣渗,將點(diǎn) FGHIJ 帶入 createLineFunctionByPoint 即可
最后得到是否點(diǎn)擊了五角星的代碼
isHover (x, y) {
let result = false
if (this.createLineFunctionByPoint(this.pointList[0], this.pointList[3])(x) <= y && this.createLineFunctionByPoint(this.pointList[0], this.pointList[2])(x) <= y && this.createLineFunctionByPoint(this.pointList[1], this.pointList[4])(x) >= y) {
result = true
} else if (this.createLineFunctionByPoint(this.pointList[0], this.pointList[2])(x) >= y && this.createLineFunctionByPoint(this.pointList[1], this.pointList[3])(x) >= y && this.createLineFunctionByPoint(this.pointList[1], this.pointList[4])(x) <= y) {
result = true
} else if (this.createLineFunctionByPoint(this.pointList[0], this.pointList[2])(x) <= y && this.createLineFunctionByPoint(this.pointList[1], this.pointList[3])(x) <= y && this.createLineFunctionByPoint(this.pointList[2], this.pointList[4])(x) >= y) {
result = true
} else if (this.createLineFunctionByPoint(this.pointList[2], this.pointList[4])(x) <= y && this.createLineFunctionByPoint(this.pointList[1], this.pointList[3])(x) >= y && this.createLineFunctionByPoint(this.pointList[0], this.pointList[3])(x) <= y) {
result = true
} else if (this.createLineFunctionByPoint(this.pointList[1], this.pointList[4])(x) <= y && this.createLineFunctionByPoint(this.pointList[0], this.pointList[3])(x) >= y && this.createLineFunctionByPoint(this.pointList[2], this.pointList[4])(x) >= y) {
result = true
} else if (this.createLineFunctionByPoint(this.pointList[0], this.pointList[3])(x) <= y && this.createLineFunctionByPoint(this.pointList[1], this.pointList[4])(x) <= y && this.createLineFunctionByPoint(this.pointList[0], this.pointList[2])(x) <= y && this.createLineFunctionByPoint(this.pointList[1], this.pointList[3])(x) >= y && this.createLineFunctionByPoint(this.pointList[2], this.pointList[4])(x) >= y) {
result = true
}
return result
}
設(shè)置偏移量,移動(dòng)函數(shù)與之前的相同
setOffset (x, y) {
this.offsetX = x - this.x
this.offsetY = y - this.y
}
setPosition (x, y) {
this.x = x - this.offsetX
this.y = y - this.offsetY
this.computePoint()
}
擴(kuò)展到其它圖形
例如繪制幾何圖形組成的海豚
其實(shí)思路都是一樣的梨州,將海豚拆分為若干個(gè)幾何圖形
按照上面的做法痕囱,構(gòu)造方法中定義各個(gè)點(diǎn)的位置信息,判斷是否點(diǎn)擊的時(shí)候?qū)D形拆分為各個(gè)小的幾何圖形就可以了摊唇,具體就不再贅述了咐蝇,請(qǐng)查看 海豚.psd 中的位置信息,與項(xiàng)目代碼即可
再?gòu)?fù)雜的幾何圖形都可用這種方法