基礎單色
畫筆的基礎實現,除了點與點之間的連接,還需要注意兩點
- 首先是在鼠標移動時計算當前移動的速度,然後根據速度計算線寬,這個是為了實現鼠標移動快,線寬就變窄,移動慢,線寬就恢復正常這個效果
- 為了避免直線連接點效果不好,我會採用貝塞爾曲線進行連接
/**
* 鼠標移動時添加新的座標
* @param position
*/
addPosition(position: MousePosition) {
this.positions.push(position)
// 處理當前線寬
if (this.positions.length > 1) {
// 計算移動速度
const mouseSpeed = this._computedSpeed(
this.positions[this.positions.length - 2],
this.positions[this.positions.length - 1]
)
// 計算線寬
const lineWidth = this._computedLineWidth(mouseSpeed)
this.lineWidths.push(lineWidth)
}
}
/**
* 計算移動速度
* @param start 起點
* @param end 終點
*/
_computedSpeed(start: MousePosition, end: MousePosition) {
// 獲取距離
const moveDistance = getDistance(start, end)
const curTime = Date.now()
// 獲取移動間隔時間 lastMoveTime:最後鼠標移動時間
const moveTime = curTime - this.lastMoveTime
// 計算速度
const mouseSpeed = moveDistance / moveTime
// 更新最後移動時間
this.lastMoveTime = curTime
return mouseSpeed
}
/**
* 計算畫筆寬度
* @param speed 鼠標移動速度
*/
_computedLineWidth(speed: number) {
let lineWidth = 0
const minWidth = this.minWidth
const maxWidth = this.maxWidth
if (speed >= this.maxSpeed) {
lineWidth = minWidth
} else if (speed <= this.minSpeed) {
lineWidth = maxWidth
} else {
lineWidth = maxWidth - (speed / this.maxSpeed) * maxWidth
}
lineWidth = lineWidth * (1 / 3) + this.lastLineWidth * (2 / 3)
this.lastLineWidth = lineWidth
return lineWidth
}
渲染時就遍歷所有座標
/**
* 自由畫筆渲染
* @param context canvas二維渲染上下文
* @param instance FreeDraw
*/
function freeDrawRender(
context: CanvasRenderingContext2D,
instance: FreeLine
) {
context.save()
context.lineCap = 'round'
context.lineJoin = 'round'
switch (instance.style) {
// 現在是隻有基礎畫筆,後續會增加不同的case
case FreeDrawStyle.Basic:
context.strokeStyle = instance.colors[0]
break
default:
break
}
for (let i = 1; i < instance.positions.length; i++) {
switch (instance.style) {
case FreeDrawStyle.Basic:
_drawBasic(instance, i, context)
break
default:
break
}
}
context.restore()
}
/**
* 繪製基礎線條
* @param instance FreeDraw 實例
* @param i 下標
* @param context canvas二維渲染上下文
* @param cb 一些繪製前的處理,修改一些樣式
*
* 畫筆軌跡是借鑑了網上的一些方案,分兩種情況
* 1. 如果是前兩個座標,就通過lineTo連接即可
* 2. 如果是前兩個座標之後的座標,就採用貝塞爾曲線進行連接,
* 比如現在有a, b, c 三個點,到c點時,把ab座標的中間點作為起點
* bc座標的中間點作為終點,b點作為控制點進行連接
*/
function _drawBasic(
instance: FreeLine,
i: number,
context: CanvasRenderingContext2D
cb?: (
instance: FreeDraw,
i: number,
context: CanvasRenderingContext2D
) => void
) {
const { positions, lineWidths } = instance
const { x: centerX, y: centerY } = positions[i - 1]
const { x: endX, y: endY } = positions[i]
context.beginPath()
if (i == 1) {
context.moveTo(centerX, centerY)
context.lineTo(endX, endY)
} else {
const { x: startX, y: startY } = positions[i - 2]
const lastX = (startX + centerX) / 2
const lastY = (startY + centerY) / 2
const x = (centerX + endX) / 2
const y = (centerY + endY) / 2
context.moveTo(lastX, lastY)
context.quadraticCurveTo(centerX, centerY, x, y)
}
context.lineWidth = lineWidths[i]
cb?.(instance, i, context)
context.stroke()
}
熒光
function freeDrawRender(
context: CanvasRenderingContext2D,
instance: FreeLine
) {
context.save()
context.lineCap = 'round'
context.lineJoin = 'round'
switch (instance.style) {
// ...
// 熒光 增加陰影效果
case FreeDrawStyle.Shadow:
context.shadowColor = instance.colors[0]
context.strokeStyle = instance.colors[0]
break
default:
break
}
for (let i = 1; i < instance.positions.length; i++) {
switch (instance.style) {
// ...
// 熒光
case FreeDrawStyle.Shadow:
_drawBasic(instance, i, context, (instance, i, context) => {
context.shadowBlur = instance.lineWidths[i]
})
break
default:
break
}
}
context.restore()
}
多色畫筆
多色畫筆需要使用context.createPattern,這個api是可以通過canvas創建一個指定的模版,然後可以讓這個模版在指定的方向上重複元圖像,具體使用可以看MDN
/**
* 自由畫筆渲染
* @param context canvas二維渲染上下文
* @param instance FreeDraw
* @param material 畫筆素材
*/
export const freeDrawRender = (
context: CanvasRenderingContext2D,
instance: FreeDraw,
material: Material
) => {
context.save()
context.lineCap = 'round'
context.lineJoin = 'round'
switch (instance.style) {
// ...
// 多色畫筆
case FreeDrawStyle.MultiColor:
context.strokeStyle = getMultiColorPattern(instance.colors)
break
default:
break
}
for (let i = 1; i < instance.positions.length; i++) {
switch (instance.style) {
// ...
// 多色畫筆
case FreeDrawStyle.MultiColor:
_drawBasic(instance, i, context)
break
default:
break
}
}
context.restore()
}
/**
* 獲取多色模版
* @param colors 多色數組
*/
const getMultiColorPattern = (colors: string[]) => {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d') as CanvasRenderingContext2D
const COLOR_WIDTH = 5 // 每個顏色的寬度
canvas.width = COLOR_WIDTH * colors.length
canvas.height = 20
colors.forEach((color, i) => {
context.fillStyle = color
context.fillRect(COLOR_WIDTH * i, 0, COLOR_WIDTH, 20)
})
return context.createPattern(canvas, 'repeat') as CanvasPattern
}
噴霧
噴霧是一種類似雪花的效果,在鼠標移動路徑上隨機繪製,但是最初我在寫的時候發現,如果對每個點都進行隨機雪花點記錄然後緩存下來,內存佔用過多,我就嘗試了提前生成5套不同的數據,按順序展示,也能達到隨機的效果
export const freeDrawRender = (
context: CanvasRenderingContext2D,
instance: FreeDraw,
material: Material
) => {
context.save()
context.lineCap = 'round'
context.lineJoin = 'round'
switch (instance.style) {
// ...
// 噴霧
case FreeDrawStyle.Spray:
context.fillStyle = instance.colors[0]
break
default:
break
}
for (let i = 1; i < instance.positions.length; i++) {
switch (instance.style) {
// ...
// 噴霧
case FreeDrawStyle.Spray:
_drawSpray(instance, i, context)
break
default:
break
}
}
context.restore()
}
/**
* 繪製噴霧
* @param instance FreeDraw 實例
* @param i 下標
* @param context canvas二維渲染上下文
*/
const _drawSpray = (
instance: FreeDraw,
i: number,
context: CanvasRenderingContext2D
) => {
const { x, y } = instance.positions[i]
for (let j = 0; j < 50; j++) {
/**
* sprayPoint 是我提前生成的5套隨機噴霧數據,按順序展示
* {
* angle 弧度
* radius 半徑
* alpha 透明度
* }
*/
const { angle, radius, alpha } = sprayPoint[i % 5][j]
context.globalAlpha = alpha
const distanceX = radius * Math.cos(angle)
const distanceY = radius * Math.sin(angle)
// 根據寬度限制噴霧寬度,因為噴霧太細了不好看,我就統一放大一倍
if (
distanceX < instance.lineWidths[i] * 2 &&
distanceY < instance.lineWidths[i] * 2 &&
distanceX > -instance.lineWidths[i] * 2 &&
distanceY > -instance.lineWidths[i] * 2
) {
context.fillRect(x + distanceX, y + distanceY, 2, 2)
}
}
}
蠟筆
蠟筆效果也是使用了context.createPattern,首先我是以當前畫筆顏色為底色,然後通過在網上找的一張蠟筆材質的透明圖覆蓋在上面,就可以實現蠟筆的效果
/**
* 自由畫筆渲染
* @param context canvas二維渲染上下文
* @param instance FreeDraw
* @param material 畫筆素材
*/
export const freeDrawRender = (
context: CanvasRenderingContext2D,
instance: FreeDraw,
material: Material
) => {
context.save()
context.lineCap = 'round'
context.lineJoin = 'round'
switch (instance.style) {
// ...
// 蠟筆
case FreeDrawStyle.Crayon:
context.strokeStyle = getCrayonPattern(
instance.colors[0],
material.crayon
)
break
default:
break
}
for (let i = 1; i < instance.positions.length; i++) {
switch (instance.style) {
// ...
// 蠟筆
case FreeDrawStyle.Crayon:
_drawBasic(instance, i, context)
break
default:
break
}
}
context.restore()
}
/**
* 獲取蠟筆模版
* @param color 蠟筆底色
* @param crayon 蠟筆素材
*/
const getCrayonPattern = (color: string, crayon: Material['crayon']) => {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d') as CanvasRenderingContext2D
canvas.width = 100
canvas.height = 100
context.fillStyle = color
context.fillRect(0, 0, 100, 100)
if (crayon) {
context.drawImage(crayon, 0, 0, 100, 100)
}
return context.createPattern(canvas, 'repeat') as CanvasPattern
}