应用场景
我在跟朋友聊天,灵感来了
“我有个很应景的表情包,这时候我要是发出去的话,肯定很有意思”
于是我打开微信表情寻找,划呀划划呀划……过了大概30秒,我突然感觉有点头疼,这种感觉就像我今天下午跟女朋友玩1000张的拼图,里面的天空是一望无际的蓝色一样,让人奔溃。图还没找到,可是我们的话题已经来到了另一个世界。之前想找的那张表情就像我冰箱里龙眼,放了太久,还没拿出来吃就烂了。
有时候我知道表情包上写着什么,可是却没有办法搜索。
我手机相册里有从各种app上收集来的几百张表情图,每次想要发送一张,我就得在几百张图片中来回寻找,可谓众里寻他千某度。
生活已经很累了,就不要再徒增烦恼了。
所以就有了今天这个小程序:抽屉表情;就像名字一样,你想要的表情就在这个抽屉里,打开就能看到。
主要功能
上传表情图,管理你手机里的表情包。
给表情添加标签,通过标签快速搜索。
使用表情模板功能,超简单三步,表情配图并发送。
目标用户
我,以及跟我一样有上面烦恼的同学。
实现思路
为了达到低成本,高效率开发,服务端使用了小程序云函数三件套:云函数,数据库,云存储。
小程序端使用了小程序兼容层框架 Taro,使用前端流行的 react 框架开发。其特点是高效便捷。
使用了typescript增加代码质量与可维护性,使用了mobx作为状态管理工具,实现了更好的页面组件的数据流通信。
架构图
效果截图
功能代码展示
import { observable, action } from 'mobx'
import { TemplateIndexData } from 'src/types'
/**
* 表情制作模块 store
*/
export class MakeStore {
/**
* 首页数据
*/
[@observable](/user/observable) indexData: TemplateIndexData[] = []
/**
* 设置首页的数据
*/
@action.bound
setIndexData(data) {
this.indexData = data
}
/**
* 存储表情模板集合的map
* 用户缓存表情模板集合的数据,避免接口重复加载
*/
[@observable](/user/observable) collectionMap: Map = new Map()
/**
* 存储表情模板的map
*/
[@observable](/user/observable) templateMap: Map = new Map()
/**
* 设置模板集合
*/
@action.bound
setCollection(data) {
this.collectionMap.set(data._id, data)
const { templates = [] } = data
templates.forEach(template => {
this.templateMap.set(template._id, template)
});
}
/**
* 获取单个模板集合
*/
@action.bound
getCollection(id) {
return this.collectionMap.get(id)
}
/**
* 获取单个模板数据
*/
@action.bound
getTemplate(id) {
return this.templateMap.get(id)
}
}
const makeStore = new MakeStore()
export default makeStore
import React, { Component } from 'react'
import { View, Canvas, Textarea, Button, Input } from '[@tarojs](/user/tarojs)/components'
import Taro, { getImageInfo } from '[@tarojs](/user/tarojs)/taro'
import './edit.scss';
import { inject, observer } from 'mobx-react';
import { MemeTemplate, Store } from 'src/types';
type PageProps = {
store: Store
}
type PageState = {
loading: boolean,
canvasStyle: {
width: number,
height: number
},
/** 表情模板 */
template: MemeTemplate,
text?: string,
}
[@inject](/user/inject)('store')
[@observer](/user/observer)
class MakeEdit extends Component {
/** 开发模式,显示文字框与调试框 */
dev = false
ctx: Taro.CanvasContext
imgRatio = 1
image: getImageInfo.SuccessCallbackResult
/** 每1px(非物理像素) 等于多少 rpx */
pxOfRpx = 1
state: PageState = {
loading: false,
canvasStyle: {
width: 690,
height: 460
},
template: null as any
}
/**
* 由于图片显示时有可能进行缩放
* 所以需要计算出缩放后的文本框的位置与大小
*/
get computedTextArea() {
const { x, y, w, h } = this.state.template.textArea
const result = {
x: x * this.imgRatio,
y: y * this.imgRatio,
w: w * this.imgRatio,
h: h * this.imgRatio,
}
return result
}
async loadImg() {
this.setState({ loading: true })
Taro.showLoading({
title: '加载中',
mask: true
})
const res = await Taro.getImageInfo({ src: this.state.template.url })
this.setState({ loading: false })
Taro.hideLoading()
return res
}
async onLoad(option) {
const template = this.props.store.make.getTemplate(option.id)
this.setState({ template })
//由于setState 不会马上执行,需要等待 setState 执行完成
await Promise.resolve()
const res = await this.loadImg()
const { screenWidth } = Taro.getSystemInfoSync()
/** 每1px 有多少 rpx */
this.pxOfRpx = 750 / screenWidth
this.image = res
this.ctx = Taro.createCanvasContext('canvas')
this.initCanvas()
this.drawContent()
}
initCanvas() {
const maxWidth = 690;
const maxHeight = 690;
const width = this.image.width * this.pxOfRpx
const height = this.image.height * this.pxOfRpx
/** 最大宽高与图片宽高的最小比值 */
const ratio = Math.min(maxWidth / width, maxHeight / height)
/** 图片需要缩放的比例,图片宽高没有超过最大宽高的话,不需要缩放 */
this.imgRatio = Math.min(ratio, 1)
this.setState({
canvasStyle: {
width: width * this.imgRatio,
height: height * this.imgRatio,
}
})
}
/**
* dev:绘制文本框
*/
drawRect() {
const ctx = this.ctx
ctx.setStrokeStyle('red')
const { x, y, w, h } = this.computedTextArea
ctx.strokeRect(x, y, w, h)
ctx.draw(true)
}
/**
* 绘制文本
* 根据文本的长度,自动计算出字体大小。简化用户操作
*/
drawText(lines) {
if (!lines.length) return
const textStyle = this.state.template.textStyle
const textColor = textStyle?.color || 'black'
let maxFontSize = textStyle?.maxFontSize || 50
const ctx = this.ctx
ctx.setTextBaseline("top")
ctx.setTextAlign("center")
ctx.setFillStyle(textColor)
const { x, y, w, h } = this.computedTextArea
let fontSize = maxFontSize
ctx.setFontSize(fontSize)
//计算出长度最长那行的值
const longestValue = lines.map(text => ctx.measureText(text).width).sort((a, b) => b - a)[0]
//文字行数 * 行高不能超过文本框高度,计算出最大字体大小
maxFontSize = Math.min(maxFontSize, h / lines.length / 1.2)
//根据比例,计算出最合适的字体大小
fontSize = Math.min(maxFontSize, w / longestValue * fontSize)
ctx.setFontSize(fontSize)
//逐行绘制
const lineHeight = fontSize * 1.2
for (const [index, text] of lines.entries()) {
ctx.fillText(text, x + w / 2, y + lineHeight * index)
}
ctx.draw(true)
}
async drawImg() {
const ctx = this.ctx
const { width, height } = this.state.canvasStyle
ctx.drawImage(this.image.path, 0, 0, width / this.pxOfRpx, height / this.pxOfRpx)
ctx.draw(true)
}
/**
* 绘制所有内容
*/
drawContent() {
this.ctx.draw(false)
const text = this.state.text || ''
const lines = text.split('\n').filter(item => item).map(item => item.trim())
this.drawImg()
this.dev && this.drawRect()
this.drawText(lines)
}
/**
* 文本变化时绘制内容
*/
handleTextChange = e => {
this.setState({
text: e.target.value
})
this.drawContent()
}
/**
* 将canvas上的内容生成临时文件,并且预览
*/
previewImg() {
const ctx = this.ctx
//真机上如果没有操作,会无法draw成功
ctx.setStrokeStyle('red')
ctx.draw(true, () => {
Taro.canvasToTempFilePath({
canvasId: 'canvas',
success: function (res) {
Taro.previewImage({
urls: [res.tempFilePath]
})
},
})
});
}
handleComplete = async () => {
this.previewImg()
}
handleCancel = () => {
Taro.navigateBack()
}
/**
* 开发时,调试文本框与文字样式
*/
handleDevInput = (e) => {
const value: string = e.target.value
const [x, y, w, h, fz, color] = value.split(',')
if (value.split(',').some(item => !item)) {
return
}
this.setState({
template: {
...this.state.template,
textArea: { x: +x, y: +y, w: +w, h: +h },
textStyle: {
maxFontSize: +fz,
color
}
}
})
console.log(`设置文本框`);
console.log(JSON.stringify({
id: this.state.template._id,
...this.state.template
}));
this.drawContent()
}
render() {
return (
{!this.state.loading &&
}
{/* *点击完成后,长按图片分享给朋友哦! */}
{
this.dev && this.state.template?.textArea && (
x,y,w,h,fz,color
)
}
完成
取消
)
}
}
export default MakeEdit
团队简介
为了获得T恤队,口号是:不忘初心