На работе более-менее встречается потребность в делительной активности. Деятельность деления, обмен плакатами также является неотъемлемой частью. Поэтому необходимо понимать метод генерации плакатов.
Фоновое изображение+заголовок+контент+эксклюзивная аватарка, никнейм+слив QR кодЭто основной элемент, из которого состоит дизайн плаката. Эти основные элементы являются функциями, которые нам нужно реализовать в коде. Это может быть просто понято как,Элементы чертежа (блоки, картинки), однострочный, многострочный текств Canvas и создайте изображение плаката.
После анализа реализуемых функций в голове всплыло три способа реализации:
-
Решите человека, который просил об этом.
Глядя на большой кулак продукта, я лучше временно отпущу его (это точно не то, что я боюсь, что он меня решит!).
-
«Брат на серверной части, помоги создать плакат на стороне сервера. После того, как ты закончишь писать, я упакую весь белый рис на этой неделе!»
Бэкэнд с братом:
Кажется, что неделя белого риса недостаточно заманчива, и мы полностью закончили.
-
Реализуйте весь функционал самостоятельно, обеспечив полный контроль над кодом.
Почему бы не использовать плагин? Как говорится, лучше научить человека ловить рыбу, чем дать ему рыбу... Ну, в самом деле, поскольку я никогда не соприкасался с холстом, я хочу этому научиться.
Talk is cheap, Show me the demo.
Нарисовать дуговой элемент прямоугольника (блока, изображения)
Реализация небольших программ сильно отличается от работы с CSS в Интернете. Основными проблемами/методами являются следующие три:
нарисовать дугу прямоугольный путь
Canvas не позволяет рисовать прямоугольники со скругленными углами, поэтому нам нужно реализовать его другим способом. В основе метода лежит метод, называемыйCanvasContext.arcTo
метод, давайте посмотрим на его использование:
CanvasRenderingContext2D.arcTo()
Является ли Canvas 2D API отрисовкой пути дуги на основе контрольной точки и радиуса, используя текущую точку рисования (предыдущую конечную точку функций, таких как moveTo или lineTo). По линии, соединяющей текущую точку рисования с заданной контрольной точкой 1, и по линии, соединяющей контрольную точку 1 и контрольную точку 2, в виде окружности с заданным радиусомТангенс, рисует дугу между двумя касательными.
Можно представить, что окружность ⚪ отчаянно сжимается в сторону мертвого угла ∠, и когда она сжимается до предела, это и есть та дуга, которую мы хотим.Прямая линия, соединяющая контрольную точку 1 и контрольную точку 2 в результате создания окружности с заданным радиусомТангенс, поэтому эта линия также будет расширена по беспроводной сети.Тысяча слов стоит картинки:
Тогда закругленные углы прямоугольника выглядят как само собой разумеющееся:
// 绘制弧矩形路径
canvasToDrawArcRectPath(ctx, x, y, w, h, r = 0) {
const [
topLeftRadius,
topRightRadius,
bottomRightRadius,
BottomLeftRadius
] = Array.isArray(r) ? r : [r, r, r, r]
/**
* 1. 移动到圆弧起点
*
* 2. 绘制上直线
* 3. 绘制右上角圆弧
*
* 4. 绘制右直线
* 5. 绘制右下圆弧
*
* 6. 绘制下直线
* 7. 绘制左下圆弧
*
* 8. 绘制左直线
* 9. 绘制左上圆弧
*/
ctx.beginPath()
ctx.moveTo(x + topLeftRadius, y)
// 右上
ctx.lineTo(x + w - topRightRadius, y)
ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius)
// 右下
ctx.lineTo(x + w, y + h - bottomRightRadius)
ctx.arcTo(
x + w,
y + h,
x + w - bottomRightRadius,
y + h,
bottomRightRadius
)
// 左下
ctx.lineTo(x + BottomLeftRadius, y + h)
ctx.arcTo(x, y + h, x, y + h - BottomLeftRadius, BottomLeftRadius)
// 左上
ctx.lineTo(x, y + topLeftRadius)
ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius)
ctx.closePath()
}
Обрезать изображение
При рисовании картинки нам часто нужно вырезать исходную картинку, чтобы получить нужный нам стиль. когда мы звонимCanvasContext.clip()
При отсечении последующие рисунки будут ограничены отсеченной областью (нет доступа к другим областям на холсте). Итак, используяclip
метод с использованиемsave
Метод сохраняет текущую область холста и передает ее после обрезки изображения.restore
метод его восстановления.
ctx.save() // 保存画布区域
this.canvasToDrawArcRectPath(ctx, x, y, width, height, radius) // 绘制弧矩形路径
ctx.clip() // 剪切成弧矩形路径
const { path: tempImageUrl } = await this.uniGetImageInfoSync(url)
ctx.drawImage(tempImageUrl, x, y, width, height) // 在剪切成弧矩形路径后绘制图片
ctx.restore() // 恢复画布区域
Элементы рисования с фоном, границами и закругленными углами
Когда на картинке есть и фон, и окантовка, и закругленные углы, нужно сделать это хитрым способом: «укладка архата».
Потому что «слой» холста следует за первым пришедшимприходи позжеВ принципе, то, что будет нарисовано позже, будет наложено на «слой», нарисованный первым. Итак, идем по порядку:
- нарисовать границу и заполнить
- рисуем фон и заливаем
- Блоки рисования, элементы изображения
CanvasContext.strokeRect
Он может лучше выполнять функцию рисования границ, но не может задавать закругленные углы, поэтому не используется.
Нарисованный эффект выглядит следующим образом:
// 绘制块元素
canvasToDrawBlock(ctx, params) {
return new Promise(async (resolve) => {
const {
x,
y,
url,
width,
height,
radius,
border,
borderColor,
backgroundColor
} = params
if (border) {
ctx.setFillStyle(borderColor ?? '#fff')
this.canvasToDrawArcRectPath(
ctx,
x - border,
y - border,
width + border * 2,
height + border * 2,
radius
)
ctx.fill()
}
if (backgroundColor) {
ctx.setFillStyle(backgroundColor)
this.canvasToDrawArcRectPath(ctx, x, y, width, height, radius)
ctx.fill()
}
if (url) {
ctx.save()
this.canvasToDrawArcRectPath(ctx, x, y, width, height, radius)
ctx.clip()
const { path: tempImageUrl } = await this.uniGetImageInfoSync(url)
ctx.drawImage(tempImageUrl, x, y, width, height)
}
ctx.restore()
resolve()
})
}
Рисование однострочного и многострочного текста
Как правило, плакаты состоят из нескольких строк текста. Но поддержка canvas для набора текста очень слабая, поэтому мы не можем использовать canvas для набора текста так же хорошо, как для набора CSS. Когда холст рисует текст, он будет продолжать рисовать только одну строку без автоматического переноса в соответствии с шириной контейнера.
К счастью, он представлен на холсте.CanvasContext.measureText(string text)返回文本宽度
Интерфейс. Поэтому нам нужно только рассчитать ширину текста и нарисовать его один за другим.Основные шаги таковы:
- Вычислить ширину текста текущего текста плюс следующий текст
- Если ширина текста не превышает ширину контейнера, продолжайте добавлять ширину текста следующего текста.
- Когда ширина текста больше максимальной ширины, нарисуйте заполненный текст на холсте.
- После того, как каждая линия нарисована, в соответствии с набором
lineHeight
Обновите ось Y текстового рисунка, сбросьте текущий текст и продолжайте повторять шаги 1, 2 и 3.
Следует отметить, что у холста есть свои собственные правила отрисовки текста, и эталонные тесты различаются на разных системных устройствах, что приводит к разным положениям текста по оси Y на разных устройствах. Толькоmiddel
Стиль исполнения един на каждой платформе, поэтому мы можем задатьctx.textBaseline = 'middle'
, а затем добавьте к оси Y текстаfontSize / 2
Можно гарантировать, что высота текста будет соответствовать эскизу дизайна на каждой платформе.Этот метод обработки происходит от2dunn как использовать холст для рисования текстовых абзацев.
// 绘制文字
canvasToDrawText(ctx, canvasParam) {
const {
x,
y,
text,
fontWeight = 'normal',
fontSize = 40,
lineHeight,
maxWidth,
textAlign = 'left',
color = '#323233'
} = canvasParam
if (typeof text !== 'string') {
return
}
ctx.font = `normal ${fontWeight} ${fontSize}px sans-serif`
ctx.setFillStyle(color)
ctx.textBaseline = 'middle'
ctx.setTextAlign(textAlign)
function drawLineText(lineText, __y) {
let __lineText = lineText
if (__lineText[0] === ' ') {
__lineText = __lineText.substr(1)
}
ctx.fillText(__lineText, x, __y + fontSize / 2)
}
if (maxWidth) {
const arrayText = text.split('')
let lineText = ''
let __y = y
for (let index = 0; index < arrayText.length; index++) {
const aryTextItem = arrayText[index]
lineText += aryTextItem
/**
* 1. 计算当前文字加下一个文字的文本宽度
* 2. 当文本宽度大于最大宽度时, 在画布上绘制被填充的文本
* 3. __y + fontSize / 2 的问题
* 4. 设置下一行文本的 y轴位置, 重置当前文本信息
*/
const { width: textMetrics } = ctx.measureText(
lineText + (arrayText[index + 1] ?? '')
)
if (textMetrics > maxWidth) {
// 绘制一行文字, 如果第一个文字是空格,则删除
drawLineText(lineText, __y)
__y += lineHeight ?? fontSize
lineText = ''
}
}
// 绘制最后一行文字, 如果第一个文字是空格,则删除
drawLineText(lineText, __y)
return
}
ctx.fillText(text, x, y + fontSize / 2)
}
Нарисуйте плакат и сгенерируйте адрес временного файла изображения
В процессе отрисовки плаката и генерации адреса временного файла картинки происходитПолучить информацию об изображении асинхроннопроцесс. Следовательно, для того, чтобы убедиться, что все необходимые элементы, нам нужно позволитьПроцесс рисования выполняется синхронно. Убедитесь, что все элементы нарисованы перед вызовомCanvasContext.draw
метод.
CanvasContext.draw
После рисования звонимuni.canvasToTempFilePath
Экспортировать содержимое указанной области текущего холста на временный адрес изображения постера. Следует отметить, что под пользовательским компонентом необходимоТретий параметр привязан к текущему экземпляру компонента., чтобы управлять компонентом холста внутри компонента
// 绘制 canvas
canvasToDraw() {
return new Promise(async (resolve) => {
const [ctx, canvasId] = this.createCanvasContext()
const { width, height, backgroundImageUrl, backgroundColor } =
this.posterParams
if (backgroundColor) {
this.canvasToDrawBlock(ctx, {
x: 0,
y: 0,
width,
height,
backgroundColor
})
}
// 绘制背景图
if (backgroundImageUrl) {
const { path: tempBackgroundImageUrl } =
await this.uniGetImageInfoSync(backgroundImageUrl)
ctx.drawImage(tempBackgroundImageUrl, 0, 0, width, height)
}
// 绘制其他元素
for (const canvasParam of this.posterParams.list) {
const { type } = canvasParam
if (type === 'text') {
this.canvasToDrawText(ctx, canvasParam)
}
if (type === 'block') {
await this.canvasToDrawBlock(ctx, canvasParam)
}
}
ctx.draw(false, async () => {
const { tempFilePath } = await this.canvasToTempFilePath(canvasId, {})
resolve([canvasId, tempFilePath])
})
})
}
// canvas 导出图片临时地址
canvasToTempFilePath(canvasId, params) {
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath(
{
canvasId,
fileType: 'jpg',
...params,
success: resolve,
fail: reject
},
this
)
})
}
Сохранение временных файлов в локальный и кэш
Наши плакаты могут не меняться месяцами, но теперь они перерисовываются с каждым кликом. Если содержание плаката богаче, модели с более низкой производительностью будут иметь явные заикания. Хотя в 2019 году команда WeChat провела серию улучшений производительности рендеринга компонента Canvas апплета. Но мы не можем лениться, и нам также необходимо оптимизировать избранные сцены, чтобы улучшить взаимодействие с пользователем.
Поскольку сгенерированные плакаты могут не заменяться в течение нескольких месяцев, мы можемПосле того, как постер отрисовывается в первый раз, файл сохраняется в локальном кеше пользователя.Просто используйте его напрямую в следующий раз. Конкретные шаги заключаются в следующем:
- нарисовать холст иПолучить временный адрес постера.
- Временный адрес изображения постера через
uni.getFileSystemManager().saveFile
Метод хранится в локальной области пользователя и получаетсяПуть к файлу после сохранения (локальный путь). - Передайте сохраненный путь к файлу (локальный путь) через
uni.setStorageSync
По сохраненному в кеше удобно судить о том, был ли сгенерирован постер.
Шаги кажутся идеальными, но им все еще не хватает внимания. После того, как плакат сгенерирован один раз,Изображения плакатов никогда не обновляются, если пользователь вручную не очистит кеш., Если настройки параметров плаката неверны, вы можете только попросить пользователя вручную очистить кеш, в противном случае он не может быть обновлен, вызывая ненужные проблемы. Поэтому мы надеемся, что при сохранении изображения постера в кеше должен бытьСрок действия,МогуРешите, обновлять ли постер напрямую через интерфейс.
Срок действияОчень хорошее решение, при сохранении в кеш ставитьКомбинация текущей метки времени + время истечения срока действияТакже депонировано в. В следующий раз, когда вы возьмете его, судитеЯвляется ли сохраненная метка времени больше, чем текущая метка временичтобы узнать, истек ли срок его действия.
const storage = {
get(key) {
const { value, expires } = uni.getStorageSync(key)
if (expires && expires < Date.parse(new Date())) {
uni.removeStorageSync(key)
return undefined
}
return value
},
set(key, value, expires) {
// expires 秒
uni.setStorageSync(key, {
value,
expires: expires ? Date.parse(new Date()) + expires * 1000 : undefined
})
}
}
export { storage }
Решите, обновлять ли постер напрямую через интерфейсЭто лучшее решение, компоненту нужно только получитьdisableCacheЗначение , и это значение хорошо использовать, чтобы определить, требуется ли принудительное обновление. Возьмите каштан:
<poster
:xxx="xxx"
:disable-cache="true"
/>
props: {
// 海报存入缓存时的 key
cacheKey: {
type: String,
default: 'cache-poster'
},
// 是否禁用缓存(是否需要强制刷新)
disableCache: {
type: Boolean,
default: false
}
...
}
methods: {
pageInit() {
const posterImage = storage.get(this.cacheKey)
// 缓存中存在图片且不禁用缓存(不需要强制刷新)时,不再绘制Canvas
if (posterImage && !this.disableCache) {
this.posterImage = posterImage
return
}
// ...绘制海报
}
}
Наконец, мы сохраняем файл в локальной папке пользователя через файловый менеджер, предоставленный WeChat.wx.getFileSystemManager()
и передать адрес обратного вызова через свой собственныйstorage.set
Его можно сохранить в кэше.
const fs = wx.getFileSystemManager()
fs.saveFile({
tempFilePath: tempCanvasFilePaths, // 传入一个本地临时文件路径
success: (res) => {
storage.set(this.cacheKey, res.savedFilePath, 86400000)
this.posterImage = res.savedFilePath
}
})
Итак, наши полные шаги таковы:
- Когда в кеше есть изображение и кеш не отключен (принудительное обновление не требуется):
- Используйте кэшированный адрес изображения постера напрямую.
- Компоненты холста больше не генерируются.
- Не выполняйте оставшиеся шаги.
- Если изображение отсутствует в кэше или кэш отключен (требуется принудительное обновление):
- нарисовать холст иПолучить временный адрес постера.
- Временный адрес изображения постера через
uni.getFileSystemManager().saveFile
Метод хранится в локальной области пользователя и получаетсяПуть к файлу после сохранения (локальный путь). - Сохраните сохраненный путь к файлу (локальный путь) в свой собственный
storage.set
Сохраните его в кеше и установите время истечения срока действия, что удобно для того, чтобы судить, был ли сгенерирован постер или его нужно обновить.
Сохранить изображение постера в альбом
Здесь мы можем напрямую вызвать API, предоставленный WeChat, чтобы сохранить изображение постера в альбом. Но предпосылка здесь в том, что пользователь авторизовалСохранить картинку в системный альбомразрешение.
Когда пользователь запускает всплывающую авторизацию, чтобы сохранить изображение во всплывающем окне системного альбома в первый раз, и щелкает, чтобы отклонить авторизацию, в следующий раз, когда пользователь щелкает авторизацию, он напрямую вводит обратный вызов ошибки и всплывающее окно авторизации отображаться не будет. Хотя я не нашел описания официального документа WeChat, но также может быть известно, что,Авторизация для сохранения изображений в системный альбом вызовет напоминание только один раз. Поэтому после отказа пользователя от авторизации нам необходимоВручную вызовите интерфейс настройки клиентского апплета uni.openSetting в обратном вызове ошибки.
Следует отметить, что нельзя напрямую вызывать uni.openSetting в обратном вызове с ошибкой, потому что WeChat требует:Уведомление:2.3.0После запуска версии пользователи могут перейти на страницу настроек и управлять информацией для авторизации только после того, как пользователь щелкнет.Подробности. Для запуска нам понадобитсяСначала откройте модальное диалоговое окно uni.showModal, чтобы вызвать поведение клика пользователя, а затем вызовите uni.openSetting, чтобы открыть интерфейс настроек.. Хотя это более хлопотно, это также соответствует логике взаимодействия, что разумно.
// 保存图片到相册
saveImageToPhotosAlbum() {
uni.saveImageToPhotosAlbum({
filePath: this.posterImage,
success: () => {
this.$emit('close-overlay')
uni.showToast({
title: '保存图片成功',
duration: 2000
})
},
fail(err) {
const { errMsg } = err
if (errMsg === 'saveImageToPhotosAlbum:fail auth deny') {
uni.showModal({
title: '保存失败',
content: '请授权保存图片到“相册”的权限',
success: (result) => {
const { confirm } = result
if (confirm) {
uni.openSetting({})
}
}
})
}
}
})
}
напиши в конце
Все дороги ведут в Рим, если вышеописанный способ кажется вам слишком хлопотным. Затем вы можете использовать компоненты расширения, официально рекомендованные WeChat.wxml-to-canvasНарисуйте холст с помощью статических шаблонов и стилей в апплете и экспортируйте изображения, которые можно использовать для создания сцен, таких как обмен изображениями.
Конечно, вы также можете перенести способ создания плакатов вweb-viewВкл, тогда можешь делать что хочешь (funny.jpg). Но недостатки тоже очевидны:Контейнер веб-представления автоматически покрывает всю страницу апплета,Мини-программы персонального типа в настоящее время не поддерживаются.
Это функции на данный момент.Если у вас есть какие-либо функциональные требования, которые вы считаете важными, вы можете указать их в задаче. В будущем также может быть добавлена страница визуализации параметров плаката операции. когда? Обязательно в следующий раз!
Blog Адрес блога
Мини-программа Sun Code (только для демо)