您當前位置>首頁 » 新聞資(zī)訊 » 小程序相關(guān) >
小程序canvas寫一個(gè)簡單的圖片應用
發表時間:2021-1-5
發布人:葵宇科技
浏覽次數:50
應用展示
截圖
需求
既然是小應用,那就希望最終成品是有 适用的場景 且是 有價值 的
需求來源
這個(gè)應用需求的靈感
在以前工作生活中(zhōng),經常會無意中(zhōng)獲得同事的 美照
這時我們想要把這張照片做成表情包
一般給圖片添加幾個(gè)說明文(wén)字
一個(gè)有意思的溝通(tōng)工具(表情包)就完成了
需求分析
基于以上需求的拆解
可(kě)以将要應用功能實現整理一下(xià)
- 用戶需要上傳一張圖片
- 可(kě)以添加文(wén)字
- 文(wén)字可(kě)以作 樣式調整 和(hé) 旋轉縮放
- 另外我們希望還可(kě)以插入一些貼圖
- 貼圖可(kě)以做 旋轉縮放
- 用戶導出圖片到相冊
實現
這個(gè)應用是用小程序開發的
- 使用框架:mpx
- 使用技術(shù):小程序canvas
狀态管理
import { createStore } from '@mpxjs/core'
const store = createStore({
state: {
cavas: null, // cnavas實例
ctx: null, // canvas上下(xià)文(wén)實例
elements: [], // canvas元素
activeIndex: null, // 當前編輯中(zhōng)的元素索引
mode: 'background', // 當前編輯模式:background, text, sticker
fontStyle: { // 文(wén)字默認樣式
opacity: 1,
fillStyle: '#000000',
strokeStyle: '#000000'
}
},
mutations: {
setCanvas (state, data) {
state.canvas = data
},
setCtx (state, data) {
state.ctx = data
},
setElements (state, data) {
state.elements = data
},
setMode (state, data) {
state.mode = data
},
setActiveIndex (state, data) {
state.activeIndex = data
},
setFontStyle (state, { key, data }) {
state.fontStyle[key] = data
},
// 添加文(wén)字
addText (state) {
const size = 50
const string = '請輸入文(wén)字'
const text = {
type: 'text',
data: string,
scale: 1,
size,
left: 100,
top: 100,
rotate: 0,
opacity: state.fontStyle.opacity,
fillStyle: state.fontStyle.fillStyle,
strokeStyle: state.fontStyle.strokeStyle
}
state.elements.push(text)
state.activeIndex = state.elements.length - 1
},
// 添加貼圖
addSticker (state, data) {
state.elements.push(data)
state.activeIndex = state.elements.length - 1
},
// 删除當前選中(zhōng)
deleteActiveELement (state) {
state.elements.splice(state.activeIndex, 1)
state.activeIndex = null
},
// 清空畫布
clear (state) {
state.elements = []
state.activeIndex = null
}
}
})
export default store
畫布初始化
// 初始化畫布
async initCanvas() {
const query = this.createSelectorQuery()
query
.select('#canvas')
.fields({ node: true, size: true })
.exec(async res => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
store.commit('setCanvas', canvas)
store.commit('setCtx', ctx)
await this.loadImage('/images/icon-rotate.png').then(res => {
this.image.rotate = res
})
canvas.width = res[0].width * this.dpr
canvas.height = res[0].height * this.dpr
ctx.scale(this.dpr, this.dpr)
this.drawGrid()
})
}
繪制圖片
/**
* 繪制圖片
* @param { Object } ele canvas元素
*/
drawImage(ele) {
this.ctx.save()
const width = ele.width
const height = ele.height
const centerX = ele.left + ele.width / 2
const centerY = ele.top + ele.height / 2
this.ctx.translate(centerX, centerY)
this.ctx.rotate(ele.rotate)
this.ctx.drawImage(ele.data, ele.left - centerX, ele.top - centerY, width, height)
this.ctx.restore()
}
繪制文(wén)字
/**
* 繪制文(wén)字
* @param { Object } ele canvas元素
*/
drawText(ele) {
this.ctx.save()
const width = ele.size * ele.data.length
const height = ele.size
const centerX = ele.left + width / 2
const centerY = ele.top + height / 2
this.ctx.translate(centerX, centerY)
this.ctx.rotate(ele.rotate)
this.ctx.font = `${ele.size}px bold sans-serif`
this.ctx.globalAlpha = ele.opacity
this.ctx.fillStyle = ele.fillStyle
this.ctx.strokeStyle = ele.strokeStyle
// this.ctx.lineWidth = 2
this.ctx.textBaseline = 'top'
console.log('draw-text', ele)
this.ctx.fillText(ele.data, ele.left - centerX, ele.top - centerY)
this.ctx.strokeText(ele.data, ele.left - centerX, ele.top - centerY)
this.ctx.restore()
}
繪制控制元件
initController(ele) {
const cs = this.convert2ControllerSize(ele)
this.ctx.save()
this.ctx.strokeStyle = '#eee'
this.ctx.translate(cs.centerX, cs.centerY)
this.ctx.rotate(cs.rotate)
// 繪制虛線邊框
this.ctx.setLineDash([10, 5], 5)
this.ctx.strokeRect(cs.left - cs.centerX, cs.top - cs.centerY, cs.width, cs.height)
// 繪制控制點-旋轉
this.ctx.drawImage(this.image.rotate, cs.left + cs.width - 10 - cs.centerX, cs.top + cs.height - 10 - cs.centerY, 20, 20)
this.ctx.restore()
}
畫布渲染函數
// 畫布渲染函數
renderCanvas() {
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
this.drawGrid()
console.log('draw-background', this.background)
if (this.background) this.drawImage(this.background)
for (let i = 0; i < this.elements.length; i++) {
const ele = this.elements[i]
// 渲染背景
if (ele.type === 'background') {
this.drawImage(ele)
}
if (ele.type === 'sticker') {
this.drawImage(ele)
}
// 渲染文(wén)字
if (ele.type === 'text') {
this.drawText(ele)
}
// 選中(zhōng)元素添加控制元件
if (this.activeIndex === i) {
this.initController(ele)
}
}
}
事件監聽
移動(dòng)
// 移動(dòng)事件綁定函數
handleMove(e) {
console.log('mouse-move', e)
if (e.touches.length > 1) return
const x = e.touches[0].x
const y = e.touches[0].y
const dx = this.startTouches[0].x - x
const dy = this.startTouches[0].y - y
const elements = this.elements.slice()
elements[this.activeIndex || 0].left = this.startSelected.left - dx
elements[this.activeIndex || 0].top = this.startSelected.top - dy
store.commit('setElements', elements)
}
旋轉
// 旋轉綁定函數
handleRotate(e) {
console.log('handleRotate')
const start = this.startTouches[0]
const end = e.touches[0]
const center = {
x: this.startSelected.centerX,
y: this.startSelected.centerY
}
const startLength = Math.sqrt((center.x - start.x) ** 2 + (center.y - start.y) ** 2)
const endLength = Math.sqrt((center.x - end.x) ** 2 + (center.y - end.y) ** 2)
const radian = this.convert2Radian(start, end, center)
const scale = endLength / startLength
const elements = this.elements.slice()
const selected = elements[this.activeIndex]
// 旋轉
selected.rotate = this.startSelected.rotate - radian
// 縮放
if (selected.type === 'text') {
selected.left = this.startSelected.centerX - this.startSelected.size * this.startSelected.data.length * scale / 2
selected.top = this.startSelected.centerY - this.startSelected.size * scale / 2
selected.size = this.startSelected.size * scale
}
if (selected.type === 'sticker') {
selected.left = this.startSelected.centerX - this.startSelected.width * scale / 2
selected.top = this.startSelected.centerY - this.startSelected.height * scale / 2
selected.width = this.startSelected.width * scale
selected.height = this.startSelected.height * scale
}
store.commit('setElements', elements)
}
縮放
// 縮放事件綁定函數
handleScale(e) {
if (e.touches.length !== 2 || this.mode !== 'background') return
const startLength = Math.sqrt(
(this.startTouches[0].x - this.startTouches[1].x) ** 2 +
(this.startTouches[0].y - this.startTouches[1].y) ** 2
)
const endLength = Math.sqrt(
(e.touches[0].x - e.touches[1].x) ** 2 + (e.touches[0].y - e.touches[1].y) ** 2
)
const scale = endLength / startLength
const elements = this.elements.slice()
const selected = elements[this.activeIndex || 0]
selected.left = this.startSelected.centerX - this.startSelected.width * scale / 2
selected.top = this.startSelected.centerY - this.startSelected.height * scale / 2
selected.width = this.startSelected.width * scale
selected.height = this.startSelected.height * scale
// elements[this.activeIndex || 0].scale = this.startSelected.scale * scale
store.commit('setElements', elements)
}
導出圖片
export() {
if (!store.state.elements.length) {
wx.showToast({
title: '加點東西再導出吧',
icon: 'none'
})
return
}
wx.showModal({
title: '提示',
content: '圖片将保存到手機相冊',
success(res) {
if (res.confirm) {
console.log('export-canvas', store.state.ctx)
const canvas = store.state.canvas
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: canvas.width,
height: canvas.height,
canvas,
complete(res) {
if (res.errMsg === 'canvasToTempFilePath:ok') {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success(res) {
wx.showToast({
title: '圖片保存成功',
icon: 'none'
})
}
})
}
}
})
}
}
})
}