feat:形状打印页测试

This commit is contained in:
2025-07-22 11:27:10 +08:00
parent 3ce3d3cac4
commit bec8fa978e
6 changed files with 1285 additions and 263 deletions
+550 -131
View File
@@ -1,58 +1,92 @@
// 引入形状数据
// 引入形状和颜色数据
import { shapes, colors } from './shapes';
Page({
data: {
shapeCards: [],
columns: 3,
bgColor: '#FFFFFF',
// 可选择的形状列表
availableShapes: shapes,
// 可选择的颜色列表
availableColors: colors,
// 已选择的形状列表
selectedShapes: [],
// 当前选中的形状类型
currentShapeType: 'circle',
// 当前选中的颜色
currentColor: '#FF4444',
// 生成的图片
generatedImage: null,
// 是否正在生成
isGenerating: false,
// 背景选项
backgrounds: [
{ name: '纯白', value: '#FFFFFF', color: '#f2f2f2' },
{ name: '米黄', value: '#FAF5E8', color: '#d4c5a0' },
{ name: '淡灰', value: '#F5F5F5', color: '#cccccc' },
{ name: '无背景', value: 'transparent', color: '#000000' },
{ name: '纯白', value: '#FFFFFF' },
{ name: '米黄', value: '#FAF5E8' },
{ name: '淡灰', value: '#F5F5F5' },
],
bgColor: '#FFFFFF',
},
onLoad() {
// 初始化示例卡片
const initialCards = [
this.createShapeCard('square', '#FF5252'),
this.createShapeCard('diamond', '#69F0AE'),
this.createShapeCard('cylinder', '#448AFF'),
this.createShapeCard('cube', '#FFD740'),
this.createShapeCard('circle', '#E040FB'),
this.createShapeCard('sphere', '#FAF5E8'),
// this.createShapeCard('triangle', '#FF9800'),
];
this.setData({ shapeCards: initialCards });
console.log('形状绘制页面加载完成');
},
createShapeCard(shapeId, fillColor) {
const shape = shapes.find((s) => s.id === shapeId);
return {
shape: shapeId,
fillColor,
svgData: this.generateSvgData(shape.svg, fillColor),
name: shape.name,
};
},
// SVG Base64生成器
generateSvgData(svgContent, fillColor) {
const styledSvg = svgContent.replace(/var\(--fill-color\)/g, fillColor);
const base64 = wx.arrayBufferToBase64(
new TextEncoder().encode(styledSvg),
);
return `data:image/svg+xml;base64,${base64}`;
},
// 设置列数
setColumns(e) {
// 选择形状类型
selectShape(e) {
const shapeId = e.currentTarget.dataset.shapeId;
this.setData({
columns: e.detail.value,
currentShapeType: shapeId,
});
},
// 选择颜色
selectColor(e) {
const color = e.currentTarget.dataset.color;
this.setData({
currentColor: color,
});
},
// 添加形状到选中列表
addShape() {
const { currentShapeType, currentColor, selectedShapes } = this.data;
const shape = shapes.find((s) => s.id === currentShapeType);
if (shape) {
const newShape = {
id: Date.now(),
type: shape.type,
name: shape.name,
color: currentColor,
shapeId: currentShapeType,
drawFunction: shape.drawFunction,
};
this.setData({
selectedShapes: [...selectedShapes, newShape],
});
wx.showToast({
title: `添加了${shape.name}`,
icon: 'success',
duration: 1000,
});
}
},
// 删除选中的形状
removeShape(e) {
const index = e.currentTarget.dataset.index;
const selectedShapes = [...this.data.selectedShapes];
selectedShapes.splice(index, 1);
this.setData({
selectedShapes,
});
},
// 清空所有形状
clearShapes() {
this.setData({
selectedShapes: [],
});
},
@@ -63,121 +97,506 @@ Page({
});
},
// 生成拼接图片
// 创建形状的SVG预览
createShapePreview(shape) {
const svgContent = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="60" height="60">
${shape.svg}
</svg>
`;
const base64 = wx.arrayBufferToBase64(
new TextEncoder().encode(svgContent),
);
return `data:image/svg+xml;base64,${base64}`;
},
// 绘制各种形状的函数
drawCircle(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(x, y, size * 0.4, 0, Math.PI * 2);
if (filled) ctx.fill();
ctx.stroke();
ctx.restore();
},
drawSquare(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
const squareSize = size * 0.8;
const startX = x - squareSize / 2;
const startY = y - squareSize / 2;
if (filled) ctx.fillRect(startX, startY, squareSize, squareSize);
ctx.strokeRect(startX, startY, squareSize, squareSize);
ctx.restore();
},
drawRectangle(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
const rectWidth = size * 0.9;
const rectHeight = size * 0.6;
const startX = x - rectWidth / 2;
const startY = y - rectHeight / 2;
if (filled) ctx.fillRect(startX, startY, rectWidth, rectHeight);
ctx.strokeRect(startX, startY, rectWidth, rectHeight);
ctx.restore();
},
drawTriangle(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
const triangleSize = size * 0.8;
ctx.beginPath();
ctx.moveTo(x, y - triangleSize / 2);
ctx.lineTo(x - triangleSize / 2, y + triangleSize / 2);
ctx.lineTo(x + triangleSize / 2, y + triangleSize / 2);
ctx.closePath();
if (filled) ctx.fill();
ctx.stroke();
ctx.restore();
},
drawOval(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.ellipse(x, y, size * 0.45, size * 0.3, 0, 0, Math.PI * 2);
if (filled) ctx.fill();
ctx.stroke();
ctx.restore();
},
drawDiamond(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
const diamondSize = size * 0.4;
ctx.beginPath();
ctx.moveTo(x, y - diamondSize);
ctx.lineTo(x + diamondSize, y);
ctx.lineTo(x, y + diamondSize);
ctx.lineTo(x - diamondSize, y);
ctx.closePath();
if (filled) ctx.fill();
ctx.stroke();
ctx.restore();
},
drawStar(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
const outerRadius = size * 0.4;
const innerRadius = size * 0.2;
ctx.beginPath();
for (let i = 0; i < 10; i++) {
const angle = (i * Math.PI) / 5;
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const pointX = x + Math.cos(angle - Math.PI / 2) * radius;
const pointY = y + Math.sin(angle - Math.PI / 2) * radius;
if (i === 0) ctx.moveTo(pointX, pointY);
else ctx.lineTo(pointX, pointY);
}
ctx.closePath();
if (filled) ctx.fill();
ctx.stroke();
ctx.restore();
},
drawHeart(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
const heartSize = size * 0.4;
ctx.beginPath();
ctx.moveTo(x, y + heartSize * 0.3);
ctx.bezierCurveTo(
x - heartSize * 0.5,
y - heartSize * 0.3,
x - heartSize,
y + heartSize * 0.1,
x,
y + heartSize * 0.8,
);
ctx.bezierCurveTo(
x + heartSize,
y + heartSize * 0.1,
x + heartSize * 0.5,
y - heartSize * 0.3,
x,
y + heartSize * 0.3,
);
if (filled) ctx.fill();
ctx.stroke();
ctx.restore();
},
drawHexagon(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
const radius = size * 0.4;
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (i * Math.PI) / 3;
const pointX = x + Math.cos(angle) * radius;
const pointY = y + Math.sin(angle) * radius;
if (i === 0) ctx.moveTo(pointX, pointY);
else ctx.lineTo(pointX, pointY);
}
ctx.closePath();
if (filled) ctx.fill();
ctx.stroke();
ctx.restore();
},
drawPentagon(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
const radius = size * 0.4;
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * 2 * Math.PI) / 5 - Math.PI / 2;
const pointX = x + Math.cos(angle) * radius;
const pointY = y + Math.sin(angle) * radius;
if (i === 0) ctx.moveTo(pointX, pointY);
else ctx.lineTo(pointX, pointY);
}
ctx.closePath();
if (filled) ctx.fill();
ctx.stroke();
ctx.restore();
},
drawSemicircle(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
const radius = size * 0.4;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI);
ctx.closePath();
if (filled) ctx.fill();
ctx.stroke();
ctx.restore();
},
drawTrapezoid(ctx, x, y, size, color, filled = true) {
ctx.save();
if (filled) {
ctx.fillStyle = color;
}
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
const trapezoidSize = size * 0.4;
ctx.beginPath();
ctx.moveTo(x - trapezoidSize * 0.6, y - trapezoidSize * 0.5);
ctx.lineTo(x + trapezoidSize * 0.6, y - trapezoidSize * 0.5);
ctx.lineTo(x + trapezoidSize, y + trapezoidSize * 0.5);
ctx.lineTo(x - trapezoidSize, y + trapezoidSize * 0.5);
ctx.closePath();
if (filled) ctx.fill();
ctx.stroke();
ctx.restore();
},
// 绘制形状的通用方法
drawShape(ctx, shape, x, y, size, filled = true) {
const color = filled ? shape.color : 'transparent';
switch (shape.drawFunction) {
case 'drawCircle':
this.drawCircle(ctx, x, y, size, color, filled);
break;
case 'drawSquare':
this.drawSquare(ctx, x, y, size, color, filled);
break;
case 'drawRectangle':
this.drawRectangle(ctx, x, y, size, color, filled);
break;
case 'drawTriangle':
this.drawTriangle(ctx, x, y, size, color, filled);
break;
case 'drawOval':
this.drawOval(ctx, x, y, size, color, filled);
break;
case 'drawDiamond':
this.drawDiamond(ctx, x, y, size, color, filled);
break;
case 'drawStar':
this.drawStar(ctx, x, y, size, color, filled);
break;
case 'drawHeart':
this.drawHeart(ctx, x, y, size, color, filled);
break;
case 'drawHexagon':
this.drawHexagon(ctx, x, y, size, color, filled);
break;
case 'drawPentagon':
this.drawPentagon(ctx, x, y, size, color, filled);
break;
case 'drawSemicircle':
this.drawSemicircle(ctx, x, y, size, color, filled);
break;
case 'drawTrapezoid':
this.drawTrapezoid(ctx, x, y, size, color, filled);
break;
}
},
// 生成A4纸张大小的图片
async generateImage() {
if (this.data.selectedShapes.length === 0) {
wx.showToast({
title: '请先添加形状',
icon: 'none',
});
return;
}
this.setData({ isGenerating: true });
try {
// 计算布局和画布尺寸
const cardCount = this.data.shapeCards.length;
const cardsPerRow = this.data.columns;
const canvasWidth = 750; // 以rpx为单位的宽度设计
const cardSize = canvasWidth / cardsPerRow;
const rowCount = Math.ceil(cardCount / cardsPerRow);
const canvasHeight = cardSize * rowCount;
const query = wx.createSelectorQuery().in(this);
// 创建离屏Canvas
const offscreenCanvas = wx.createOffscreenCanvas({
type: '2d',
width: canvasWidth,
height: canvasHeight,
});
query
.select('#mainCanvas')
.fields({ node: true, size: true })
.exec((res) => {
if (!res[0] || !res[0].node) {
this.setData({ isGenerating: false });
wx.showToast({
title: 'Canvas 获取失败',
icon: 'none',
});
return;
}
const ctx = offscreenCanvas.getContext('2d');
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
// 填充背景
if (this.data.bgColor !== 'transparent') {
ctx.fillStyle = this.data.bgColor;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
}
// A4纸张尺寸 (595x842 点,按照72DPI)
const dpr = wx.getSystemInfoSync().pixelRatio;
const a4Width = 595;
const a4Height = 842;
// 批量加载图片的Promise数组
const loadPromises = this.data.shapeCards.map((card) => {
return new Promise((resolve) => {
wx.getImageInfo({
src: card.svgData,
canvas.width = a4Width * dpr;
canvas.height = a4Height * dpr;
ctx.scale(dpr, dpr);
// 清空画布并设置背景
ctx.clearRect(0, 0, a4Width, a4Height);
ctx.fillStyle = this.data.bgColor;
ctx.fillRect(0, 0, a4Width, a4Height);
// 绘制标题
this.drawTitle(ctx, a4Width);
// 绘制上半部分:带颜色的形状示例
this.drawUpperSection(ctx, a4Width, a4Height);
// 绘制分割线
this.drawDividerLine(ctx, a4Width, a4Height * 0.45);
// 绘制下半部分:无颜色的练习区域
this.drawLowerSection(ctx, a4Width, a4Height);
// 导出图片
wx.canvasToTempFilePath({
canvas: canvas,
success: (res) => {
resolve({
path: res.path,
card,
this.setData({
generatedImage: res.tempFilePath,
isGenerating: false,
});
wx.showToast({
title: '图片生成成功',
icon: 'success',
});
},
fail: () => {
resolve(null);
fail: (err) => {
console.error('导出图片失败:', err);
this.setData({ isGenerating: false });
wx.showToast({
title: '图片生成失败',
icon: 'none',
});
},
});
});
});
// 等待所有图片加载完成
const images = await Promise.all(loadPromises);
// 绘制所有卡片
images.forEach((imgInfo, index) => {
if (!imgInfo) return;
const row = Math.floor(index / cardsPerRow);
const col = index % cardsPerRow;
const x = col * cardSize + cardSize / 2;
const y = row * cardSize + cardSize / 2;
// 保存当前绘制状态
ctx.save();
// 添加卡片投影效果
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
ctx.shadowBlur = 8;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 3;
// 绘制卡片SVG
ctx.drawImage(
imgInfo.path,
x - (cardSize * 0.8) / 2,
y - (cardSize * 0.8) / 2,
cardSize * 0.8,
cardSize * 0.8,
);
// 恢复绘制状态
ctx.restore();
});
// 将Canvas转换为临时图片路径
wx.canvasToTempFilePath({
canvas: offscreenCanvas,
success: (res) => {
this.setData({
generatedImage: res.tempFilePath,
isGenerating: false,
});
},
fail: () => {
this.setData({ isGenerating: false });
wx.showToast({ title: '图片生成失败', icon: 'none' });
},
});
} catch (e) {
} catch (error) {
console.error('生成图片出错:', error);
this.setData({ isGenerating: false });
console.error(e);
wx.showToast({ title: '生成出错,请重试', icon: 'none' });
wx.showToast({
title: '生成出错,请重试',
icon: 'none',
});
}
},
// 绘制标题
drawTitle(ctx, canvasWidth) {
ctx.save();
ctx.fillStyle = '#333';
ctx.font = 'bold 32px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText('形状涂色练习', canvasWidth / 2, 30);
ctx.font = '20px Arial';
ctx.fillText('给相同的形状涂上相同的颜色', canvasWidth / 2, 75);
ctx.restore();
},
// 绘制上半部分:带颜色的形状示例
drawUpperSection(ctx, canvasWidth, canvasHeight) {
const { selectedShapes } = this.data;
const startY = 120;
const sectionHeight = canvasHeight * 0.32;
const shapeSize = 60;
const cols = Math.min(selectedShapes.length, 6);
const rows = Math.ceil(selectedShapes.length / cols);
const cellWidth = (canvasWidth - 120) / cols;
const cellHeight = sectionHeight / Math.max(rows, 1);
selectedShapes.forEach((shape, index) => {
const row = Math.floor(index / cols);
const col = index % cols;
const x = 60 + col * cellWidth + cellWidth / 2;
const y = startY + row * cellHeight + cellHeight / 2;
this.drawShape(ctx, shape, x, y, shapeSize, true);
});
},
// 绘制分割线
drawDividerLine(ctx, canvasWidth, y) {
ctx.save();
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.setLineDash([10, 5]);
ctx.beginPath();
ctx.moveTo(60, y);
ctx.lineTo(canvasWidth - 60, y);
ctx.stroke();
ctx.restore();
},
// 绘制下半部分:无颜色的练习区域
drawLowerSection(ctx, canvasWidth, canvasHeight) {
const { selectedShapes } = this.data;
const startY = canvasHeight * 0.5;
const sectionHeight = canvasHeight * 0.45;
const shapeSize = 50;
// 计算网格布局
const cols = 8;
const rows = 6;
const cellWidth = (canvasWidth - 120) / cols;
const cellHeight = sectionHeight / rows;
// 生成随机形状排列
const practiceShapes = [];
const totalShapes = cols * rows;
for (let i = 0; i < totalShapes; i++) {
const randomShape = selectedShapes[i % selectedShapes.length];
practiceShapes.push({
...randomShape,
color: 'transparent', // 练习区域不填充颜色
});
}
// 打乱顺序
for (let i = practiceShapes.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[practiceShapes[i], practiceShapes[j]] = [
practiceShapes[j],
practiceShapes[i],
];
}
practiceShapes.forEach((shape, index) => {
const row = Math.floor(index / cols);
const col = index % cols;
const x = 60 + col * cellWidth + cellWidth / 2;
const y = startY + row * cellHeight + cellHeight / 2;
this.drawShape(ctx, shape, x, y, shapeSize, false);
});
},
// 保存到相册
saveToAlbum() {
if (!this.data.generatedImage) return;
if (!this.data.generatedImage) {
wx.showToast({
title: '请先生成图片',
icon: 'none',
});
return;
}
wx.saveImageToPhotosAlbum({
filePath: this.data.generatedImage,
success: () => {
wx.showToast({ title: '保存成功', icon: 'success' });
wx.showToast({
title: '保存成功',
icon: 'success',
});
},
fail: (err) => {
if (err.errMsg === 'saveImageToPhotosAlbum:fail auth deny') {
if (err.errMsg.includes('auth deny')) {
this.showAuthGuide();
} else {
wx.showToast({ title: '保存失败', icon: 'none' });
wx.showToast({
title: '保存失败',
icon: 'none',
});
}
},
});
@@ -187,7 +606,7 @@ Page({
showAuthGuide() {
wx.showModal({
title: '权限申请',
content: '需要相册访问权限才能保存图片,请开启权限',
content: '需要相册访问权限才能保存图片,请在设置中开启权限',
success: (res) => {
if (res.confirm) {
wx.openSetting();
@@ -196,8 +615,8 @@ Page({
});
},
// 预览大图
previewFullImage() {
// 预览生成的图片
previewImage() {
if (!this.data.generatedImage) return;
wx.previewImage({