<template>
|
<view>
|
<view class="animation_pixel">
|
<view class="back_btn" @click="popupAnimationClose">
|
<uv-icon name="arrow-leftward" color="#333" size="30"></uv-icon>
|
</view>
|
<!-- 动画按钮 -->
|
<view class="controls">
|
<uv-button type="primary" :plain="true" text="分辨率" @click="changeRatio()"></uv-button>
|
<uv-button type="primary" :plain="true" text="颜色" @click="changeColor()"></uv-button>
|
<uv-button type="primary" :plain="true" text="添加文字" @click="addText()"></uv-button>
|
<uv-button type="primary" :plain="true" text="清除" @click="clear()"></uv-button>
|
<uv-button type="primary" :plain="true" text="保存" @click="clickSaveData"></uv-button>
|
</view>
|
|
<!-- 文字生成图形 -->
|
<canvas canvas-id="myCanvas" id="myCanvas" class="canvas"
|
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas>
|
|
<view class="pixel_grid" @touchstart.prevent="handleTouchStart" @touchmove.prevent="handleTouchMove"
|
@touchend.prevent="handleTouchEnd" @touchcancel.prevent="handleTouchEnd" :style="{
|
gridTemplateRows: `repeat(${height}, 1fr)`,
|
gridTemplateColumns: `repeat(${width}, 1fr)`
|
}">
|
<view class="pixel_cell" v-for="(cell, cellIndex) in pixelData" :key="cellIndex">
|
<view class="pixel_cell_item" :style="{ backgroundColor: cell }"></view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 分辨率 -->
|
<uv-picker ref="pickerRatio" :columns="columnsRatio" @confirm="confirmRatio"></uv-picker>
|
|
<!-- 颜色 -->
|
<uv-popup ref="pickerColor" mode="bottom" round="10" :closeable="true">
|
<view class="color_list">
|
<view class="color_item" v-for="(item, index) in colorList" :class="{ hover: color === item }"
|
:key="index" :style="{ backgroundColor: item }" @click="clickColor(item)"></view>
|
</view>
|
</uv-popup>
|
|
</view>
|
</template>
|
|
<script>
|
let ctx = null
|
export default {
|
data() {
|
return {
|
tempName: '',
|
pic: '',
|
specList: [{
|
name: '12*24'
|
}, {
|
name: '16*24'
|
}, {
|
name: '18*24'
|
}],
|
specVal: '12*24',
|
specWidth: 24,
|
specHeight: 12,
|
modeList: [{
|
name: '单个动画帧',
|
val: 0
|
},
|
{
|
name: '动画帧切换',
|
val: 1
|
}
|
],
|
modeVal: 0,
|
columnsTotal: [
|
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
|
28, 29, 30
|
]
|
],
|
total: 1, //动画总帧数
|
columnsSpeed: [
|
[10, 20, 30, 40, 50, 60, 70, 80, 90]
|
],
|
speed: 10,
|
cycle: 1,
|
fpsData: [],
|
fpsIndex: 1, //第几帧
|
fpsIds: [],
|
|
baseURL: '',
|
fileList1: [],
|
animationData: [],
|
fpsTotal: 1,
|
|
|
|
// 模版动画
|
pixelData: [],
|
canvasData: [],
|
canvasWidth: 240,
|
canvasHeight: 120,
|
width: 24,
|
height: 12,
|
color: '#FF0000',
|
gridRect: null, // 存储像素网格的位置信息
|
lastCell: null, // 防止重复绘制
|
columnsRatio: [
|
['12*24', '16*32', '24*48', '32*64', '40*80']
|
],
|
// columnsRatio: [
|
// ['12*24']
|
// ],
|
colorList: [
|
'#FF0000',
|
'#00FF00',
|
'#0000FF',
|
'#FFFF00',
|
'#FFA500',
|
'#800080',
|
'#00FFFF',
|
'#FFC0CB'
|
],
|
fpsIndex: 0,
|
fpsList: [],
|
tId: '',
|
tempInfo: {},
|
titleName: '新增模版',
|
|
|
}
|
},
|
onLoad(opt) {
|
this.baseURL = uni.$uv.http.config.baseURL
|
if (opt.index) {
|
this.fpsIndex = opt.index
|
console.log('fpsIndex--', this.fpsIndex)
|
}
|
},
|
onShow() {
|
this.pixelData = new Array(this.height * this.width).fill('')
|
},
|
onHide() {
|
|
},
|
onUnload() {
|
|
},
|
methods: {
|
// 保存动画
|
clickSaveData() {
|
this.fpsList[this.fpsIndex] = this.pixelData;
|
this.$refs.popupAnimation.close();
|
},
|
changeColor() {
|
this.$refs.pickerColor.open();
|
},
|
clickColor(item) {
|
console.log('confirmColor---', item);
|
this.color = item;
|
this.$refs.pickerColor.close();
|
},
|
changeRatio() {
|
this.$refs.pickerRatio.open();
|
},
|
// 分辨率
|
confirmRatio(e) {
|
console.log('confirmRatio---', e.value[0]);
|
const ratio = e.value[0].split('*');
|
console.log('ratio---', ratio[0]);
|
this.height = Number(ratio[0]);
|
this.width = Number(ratio[1]);
|
this.canvasWidth = this.width * 10;
|
this.canvasHeight = this.height * 10;
|
// 重新初始化一维数组
|
this.pixelData = new Array(this.height * this.width).fill('')
|
console.log('pixelData---', this.pixelData);
|
},
|
// 预先获取像素网格的位置信息
|
getGridRect(cb) {
|
if (this.gridRect) {
|
cb(this.gridRect)
|
} else {
|
uni.createSelectorQuery().in(this).select('.pixel_grid').boundingClientRect(rect => {
|
this.gridRect = rect
|
cb(rect)
|
}).exec()
|
}
|
},
|
// 计算触摸点对应的格子
|
getCellByTouch(touch, cb) {
|
this.getGridRect(rect => {
|
if (!rect) return
|
const x = touch.clientX - rect.left
|
const y = touch.clientY - rect.top
|
const cellWidth = rect.width / this.width
|
const cellHeight = rect.height / this.height
|
const colIndex = Math.floor(x / cellWidth)
|
const rowIndex = Math.floor(y / cellHeight)
|
if (
|
colIndex >= 0 && colIndex < this.width &&
|
rowIndex >= 0 && rowIndex < this.height
|
) {
|
cb({
|
rowIndex,
|
colIndex
|
})
|
}
|
})
|
},
|
handleTouchStart(e) {
|
// 阻止默认行为
|
e.preventDefault && e.preventDefault();
|
|
if (!e.touches.length) return
|
this.getCellByTouch(e.touches[0], cell => {
|
this.lastCell = cell
|
this.handleClick(cell.rowIndex, cell.colIndex)
|
})
|
},
|
handleTouchMove(e) {
|
// 阻止默认行为
|
e.preventDefault && e.preventDefault();
|
|
if (!e.touches.length) return
|
this.getCellByTouch(e.touches[0], cell => {
|
// 只在格子变化时才触发
|
if (!this.lastCell || this.lastCell.rowIndex !== cell.rowIndex || this.lastCell.colIndex !==
|
cell.colIndex) {
|
this.lastCell = cell
|
this.handleClick(cell.rowIndex, cell.colIndex)
|
}
|
})
|
},
|
handleTouchEnd() {
|
this.lastCell = null
|
},
|
handleClick(rowIndex, colIndex) {
|
console.log(rowIndex, colIndex)
|
// 计算一维数组的索引
|
const index = rowIndex * this.width + colIndex
|
// 如果当前像素有颜色,则清除;如果没有颜色,则添加当前选择的颜色
|
const currentColor = this.pixelData[index]
|
const newColor = currentColor ? '' : this.color
|
this.$set(this.pixelData, index, newColor)
|
},
|
clear() {
|
ctx && ctx.clearRect(0, 0, 1000, 1000)
|
// 将所有像素点设置为空字符串
|
this.pixelData = new Array(this.height * this.width).fill('')
|
},
|
// 模版动画弹出框
|
changeAnimation(e) {
|
//console.log('打开关闭模版动画弹窗触发---',e)
|
// ios设备不支持弹出框横屏切换
|
// if(e.show){
|
// if (typeof plus !== 'undefined') {
|
// plus.screen.lockOrientation('landscape-primary')
|
// }
|
// }
|
// else{
|
// if (typeof plus !== 'undefined') {
|
// plus.screen.lockOrientation('portrait')
|
// }
|
// }
|
},
|
// 打开模版动画
|
popupAnimationOpen(index) {
|
this.fpsIndex = index
|
if (this.fpsList[this.fpsIndex] == null) {
|
console.log('第一次编辑-----null')
|
this.pixelData = new Array(this.height * this.width).fill('')
|
} else {
|
console.log('已编辑过-----')
|
this.pixelData = this.fpsList[this.fpsIndex]
|
}
|
},
|
// 关闭模版动画
|
popupAnimationClose() {
|
this.$refs.popupAnimation.close();
|
this.pixelData = new Array(this.height * this.width).fill('')
|
},
|
// 屏幕规格
|
changeSpec(e) {
|
const ratio = e.split('*');
|
this.specHeight = Number(ratio[0]);
|
this.specWidth = Number(ratio[1]);
|
},
|
// 偶数行数据反转
|
toSnakeData(pixelData, cols = 24) {
|
const rows = []
|
for (let i = 0; i < pixelData.length; i += cols) {
|
let row = pixelData.slice(i, i + cols)
|
if ((rows.length + 1) % 2 === 0) {
|
row.reverse() // 偶数行反转
|
}
|
rows.push(row)
|
}
|
return rows
|
},
|
// 二维数组转字符串
|
mergeRowsToString(rows) {
|
const rowStrings = rows.map(r => r.join('')) // 每行合并
|
return rowStrings.join('') // 所有行合并
|
},
|
// 生成一帧动画其他帧数据
|
generateFrames(data, frameCount = 12) {
|
const cols = this.specWidth; // 每行 24 个
|
const rows = this.specHeight; // 共 12 行
|
const frames = [];
|
// 拆分成 12 行
|
let grid = [];
|
for (let r = 0; r < rows; r++) {
|
grid.push(data.slice(r * cols, (r + 1) * cols));
|
}
|
// 生成帧
|
for (let f = 0; f < frameCount; f++) {
|
let frame = [];
|
// 每行左移 f 次
|
for (let r = 0; r < rows; r++) {
|
let row = grid[r].slice(); // 拷贝一行
|
let shifted = row.slice(f % cols).concat(row.slice(0, f % cols));
|
frame = frame.concat(shifted);
|
}
|
frames.push(frame);
|
}
|
return frames;
|
},
|
|
clickSpeed() {
|
this.$refs.pickerSpeed.open();
|
},
|
confirmSpeed(e) {
|
console.log('confirmSpeed', e.value[0]);
|
this.speed = e.value[0]
|
},
|
clickTotal() {
|
if (this.total != 1) {
|
this.$refs.pickerTotal.open();
|
}
|
},
|
confirmTotal(e) {
|
console.log('confirmTotal', e.value[0]);
|
this.total = e.value[0]
|
this.adjustFpsData()
|
},
|
changeMode(e) {
|
this.fpsList = []
|
this.fpsData = []
|
if (e == 0) {
|
this.total = 1
|
} else {
|
this.total = 2
|
}
|
this.adjustFpsData()
|
},
|
adjustFpsData() {
|
// 如果 fpsData 太长 → 截取
|
if (this.fpsList.length > this.total) {
|
this.fpsList = this.fpsList.slice(0, this.total);
|
}
|
// 如果 fpsData 太短 → 补齐 null
|
else if (this.fpsList.length < this.total) {
|
const missing = this.total - this.fpsList.length;
|
this.fpsList = this.fpsList.concat(new Array(missing).fill(null));
|
}
|
|
// 如果 fpsList 太长 → 截取
|
if (this.fpsData.length > this.total) {
|
this.fpsData = this.fpsData.slice(0, this.total);
|
}
|
// 如果 fpsList 太短 → 补齐 null
|
else if (this.fpsData.length < this.total) {
|
const missing = this.total - this.fpsData.length;
|
this.fpsData = this.fpsData.concat(new Array(missing).fill(null));
|
}
|
},
|
// 删除图片
|
deletePic(event) {
|
this[`fileList${event.name}`].splice(event.index, 1)
|
},
|
// 新增图片
|
async afterRead(event) {
|
console.log('event---', event)
|
// 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
|
let lists = [].concat(event.file)
|
let fileListLen = this[`fileList${event.name}`].length
|
lists.map((item) => {
|
this[`fileList${event.name}`].push({
|
...item,
|
status: 'uploading',
|
message: '上传中'
|
})
|
})
|
for (let i = 0; i < lists.length; i++) {
|
const result = await this.uploadFilePromise(lists[i].url)
|
this.avatar = result
|
let item = this[`fileList${event.name}`][fileListLen]
|
this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
|
status: 'success',
|
message: '',
|
url: result
|
}))
|
fileListLen++
|
}
|
},
|
uploadFilePromise(url) {
|
return new Promise((resolve, reject) => {
|
let a = uni.uploadFile({
|
url: this.baseURL + '/common/uploadFileByAnny', // 仅为示例,非真实的接口地址
|
filePath: url,
|
name: 'file',
|
success: (res) => {
|
console.log('图片-----', JSON.parse(res.data))
|
setTimeout(() => {
|
resolve(JSON.parse(res.data).fileName)
|
}, 1000)
|
}
|
});
|
})
|
},
|
// 生成点阵数据
|
generateDotMatrix(char) {
|
return new Promise((resolve, reject) => {
|
|
// 使用 uni-app 的 canvas context
|
ctx = uni.createCanvasContext('myCanvas', this);
|
|
// 设置字体样式
|
ctx.setFontSize(this.canvasHeight * 0.9);
|
ctx.setFillStyle('#000')
|
ctx.setTextAlign('center')
|
ctx.setTextBaseline('middle')
|
// 绘制文字
|
// 计算文字位置
|
// 水平居中
|
const x = this.canvasWidth / 2;
|
// 垂直居中,稍微向上偏移一点
|
const y = this.canvasHeight / 2;
|
|
// 绘制文字
|
ctx.fillText(char, x, y);
|
|
// 绘制到画布
|
ctx.draw(false, () => {
|
// 获取画布数据
|
uni.canvasGetImageData({
|
canvasId: 'myCanvas',
|
x: 0,
|
y: 0,
|
width: this.canvasWidth,
|
height: this.canvasHeight,
|
success: (res) => {
|
console.log(res)
|
const imageData = res.data;
|
// 生成一维数组
|
const pixelArray = new Array(this.height * this.width).fill('')
|
|
// 计算每个格子的像素大小
|
const cellWidth = this.canvasWidth / this.width;
|
const cellHeight = this.canvasHeight / this.height;
|
|
// 生成一维数组
|
for (let y = 0; y < this.height; y++) {
|
for (let x = 0; x < this.width; x++) {
|
let blackPixels = 0;
|
let totalPixels = 0;
|
|
// 对每个格子进行采样
|
for (let py = 0; py < cellHeight; py++) {
|
for (let px = 0; px < cellWidth; px++) {
|
const pixelY = Math.floor(y * cellHeight + py);
|
const pixelX = Math.floor(x * cellWidth + px);
|
|
const idx = (pixelY * this.canvasWidth +
|
pixelX) * 4;
|
const r = imageData[idx];
|
const g = imageData[idx + 1];
|
const b = imageData[idx + 2];
|
const a = imageData[idx + 3];
|
|
// 计算亮度
|
const brightness = 0.3 * r + 0.59 * g + 0.11 *
|
b;
|
|
// 如果亮度小于阈值且透明度大于阈值,则认为是黑色
|
if (brightness < 128 && a > 128) {
|
blackPixels++;
|
}
|
totalPixels++;
|
}
|
}
|
|
// 如果黑色像素占比超过阈值,则设置为当前选择的颜色
|
const isBlack = blackPixels / totalPixels > 0.3;
|
const arrayIndex = y * this.width + x;
|
pixelArray[arrayIndex] = isBlack ? this.color :
|
''; // 改为颜色值或空字符串
|
}
|
}
|
|
resolve(pixelArray);
|
},
|
fail: (err) => {
|
reject(err);
|
}
|
});
|
});
|
});
|
},
|
|
// 修改添加文字方法
|
async addText() {
|
uni.showModal({
|
title: '添加文字',
|
content: '',
|
editable: true,
|
placeholderText: '请输入文字',
|
success: async (res) => {
|
if (res.confirm) {
|
const text = res.content;
|
if (text.length > 0) {
|
try {
|
// 生成点阵数据
|
const dotMatrix = await this.generateDotMatrix(text);
|
console.log('点阵数据:', dotMatrix);
|
this.pixelData = dotMatrix
|
} catch (error) {
|
console.error('生成点阵失败:', error);
|
uni.showToast({
|
title: '生成点阵失败',
|
icon: 'none'
|
});
|
}
|
} else {
|
uni.showToast({
|
title: '请输入文字',
|
icon: 'none'
|
});
|
}
|
}
|
}
|
});
|
},
|
}
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
.color_list {
|
width: 100%;
|
display: flex;
|
justify-content: center;
|
flex-wrap: wrap;
|
gap: 30rpx;
|
box-sizing: border-box;
|
padding: 50rpx;
|
|
.color_item {
|
width: 30rpx;
|
height: 30rpx;
|
border-radius: 5rpx;
|
}
|
}
|
|
.controls {
|
position: absolute;
|
bottom: 10px;
|
left: 50px;
|
display: flex;
|
gap: 20px;
|
z-index: 9;
|
}
|
|
.animation_pixel {
|
width: 100vw;
|
height: 100vh;
|
position: relative;
|
|
.back_btn {
|
position: absolute;
|
left: 30px;
|
top: 20px;
|
z-index: 9999;
|
}
|
|
.canvas {
|
margin: 100rpx auto 0;
|
background: #ccc;
|
position: absolute;
|
z-index: 1;
|
left: -9999px;
|
top: -9999px;
|
}
|
|
.pixel_grid {
|
position: fixed;
|
left: 0;
|
top: var(--status-bar-height);
|
right: 0;
|
bottom: 0;
|
display: grid;
|
gap: 1px;
|
box-sizing: border-box;
|
background: #eee;
|
touch-action: none;
|
-webkit-touch-callout: none;
|
-webkit-user-select: none;
|
user-select: none;
|
|
.pixel_cell {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
box-sizing: border-box;
|
background: #fff;
|
|
.pixel_cell_item {
|
width: 50%;
|
height: 50%;
|
aspect-ratio: 1/1;
|
font-size: 10rpx;
|
}
|
}
|
}
|
|
}
|
</style>
|