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
+13
View File
@@ -68,4 +68,17 @@
确定 确定
</toy-button> </toy-button>
</view> </view>
<van-cell-group custom-class="debug-wrapper">
<van-cell
title="测试页面入口"
size="large"
custom-class="centered-title" />
<van-cell
title="形状打印"
clickable
data-name="shapePrint"
is-link
url="/pages/shapePrint/index" />
</van-cell-group>
</view> </view>
+550 -131
View File
@@ -1,58 +1,92 @@
// 引入形状数据 // 引入形状和颜色数据
import { shapes, colors } from './shapes'; import { shapes, colors } from './shapes';
Page({ Page({
data: { data: {
shapeCards: [], // 可选择的形状列表
columns: 3, availableShapes: shapes,
bgColor: '#FFFFFF', // 可选择的颜色列表
availableColors: colors,
// 已选择的形状列表
selectedShapes: [],
// 当前选中的形状类型
currentShapeType: 'circle',
// 当前选中的颜色
currentColor: '#FF4444',
// 生成的图片
generatedImage: null, generatedImage: null,
// 是否正在生成
isGenerating: false, isGenerating: false,
// 背景选项
backgrounds: [ backgrounds: [
{ name: '纯白', value: '#FFFFFF', color: '#f2f2f2' }, { name: '纯白', value: '#FFFFFF' },
{ name: '米黄', value: '#FAF5E8', color: '#d4c5a0' }, { name: '米黄', value: '#FAF5E8' },
{ name: '淡灰', value: '#F5F5F5', color: '#cccccc' }, { name: '淡灰', value: '#F5F5F5' },
{ name: '无背景', value: 'transparent', color: '#000000' },
], ],
bgColor: '#FFFFFF',
}, },
onLoad() { onLoad() {
// 初始化示例卡片 console.log('形状绘制页面加载完成');
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 });
}, },
createShapeCard(shapeId, fillColor) { // 选择形状类型
const shape = shapes.find((s) => s.id === shapeId); selectShape(e) {
return { const shapeId = e.currentTarget.dataset.shapeId;
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) {
this.setData({ 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() { async generateImage() {
if (this.data.selectedShapes.length === 0) {
wx.showToast({
title: '请先添加形状',
icon: 'none',
});
return;
}
this.setData({ isGenerating: true }); this.setData({ isGenerating: true });
try { try {
// 计算布局和画布尺寸 const query = wx.createSelectorQuery().in(this);
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;
// 创建离屏Canvas query
const offscreenCanvas = wx.createOffscreenCanvas({ .select('#mainCanvas')
type: '2d', .fields({ node: true, size: true })
width: canvasWidth, .exec((res) => {
height: canvasHeight, 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');
// 填充背景 // A4纸张尺寸 (595x842 点,按照72DPI)
if (this.data.bgColor !== 'transparent') { const dpr = wx.getSystemInfoSync().pixelRatio;
ctx.fillStyle = this.data.bgColor; const a4Width = 595;
ctx.fillRect(0, 0, canvasWidth, canvasHeight); const a4Height = 842;
}
// 批量加载图片的Promise数组 canvas.width = a4Width * dpr;
const loadPromises = this.data.shapeCards.map((card) => { canvas.height = a4Height * dpr;
return new Promise((resolve) => { ctx.scale(dpr, dpr);
wx.getImageInfo({
src: card.svgData, // 清空画布并设置背景
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) => { success: (res) => {
resolve({ this.setData({
path: res.path, generatedImage: res.tempFilePath,
card, isGenerating: false,
});
wx.showToast({
title: '图片生成成功',
icon: 'success',
}); });
}, },
fail: () => { fail: (err) => {
resolve(null); console.error('导出图片失败:', err);
this.setData({ isGenerating: false });
wx.showToast({
title: '图片生成失败',
icon: 'none',
});
}, },
}); });
}); });
}); } catch (error) {
console.error('生成图片出错:', error);
// 等待所有图片加载完成
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) {
this.setData({ isGenerating: false }); this.setData({ isGenerating: false });
console.error(e); wx.showToast({
wx.showToast({ title: '生成出错,请重试', icon: 'none' }); 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() { saveToAlbum() {
if (!this.data.generatedImage) return; if (!this.data.generatedImage) {
wx.showToast({
title: '请先生成图片',
icon: 'none',
});
return;
}
wx.saveImageToPhotosAlbum({ wx.saveImageToPhotosAlbum({
filePath: this.data.generatedImage, filePath: this.data.generatedImage,
success: () => { success: () => {
wx.showToast({ title: '保存成功', icon: 'success' }); wx.showToast({
title: '保存成功',
icon: 'success',
});
}, },
fail: (err) => { fail: (err) => {
if (err.errMsg === 'saveImageToPhotosAlbum:fail auth deny') { if (err.errMsg.includes('auth deny')) {
this.showAuthGuide(); this.showAuthGuide();
} else { } else {
wx.showToast({ title: '保存失败', icon: 'none' }); wx.showToast({
title: '保存失败',
icon: 'none',
});
} }
}, },
}); });
@@ -187,7 +606,7 @@ Page({
showAuthGuide() { showAuthGuide() {
wx.showModal({ wx.showModal({
title: '权限申请', title: '权限申请',
content: '需要相册访问权限才能保存图片,请开启权限', content: '需要相册访问权限才能保存图片,请在设置中开启权限',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
wx.openSetting(); wx.openSetting();
@@ -196,8 +615,8 @@ Page({
}); });
}, },
// 预览大图 // 预览生成的图片
previewFullImage() { previewImage() {
if (!this.data.generatedImage) return; if (!this.data.generatedImage) return;
wx.previewImage({ wx.previewImage({
+505 -51
View File
@@ -1,84 +1,538 @@
.container { .container {
padding: 20rpx; padding: 20rpx;
background-color: #f5f7fa; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 30rpx;
padding: 30rpx;
background: rgba(255, 255, 255, 0.95);
border-radius: 25rpx;
box-shadow: 0 8rpx 25rpx rgba(0, 0, 0, 0.15);
.title {
font-size: 40rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 10rpx;
}
.subtitle {
font-size: 26rpx;
color: #666;
display: block;
}
}
.section {
background: rgba(255, 255, 255, 0.95);
border-radius: 25rpx;
padding: 30rpx;
margin-bottom: 25rpx;
box-shadow: 0 8rpx 25rpx rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10rpx);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 25rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2rpx solid #f0f0f0;
padding-bottom: 15rpx;
}
.clear-btn {
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
color: white;
border: none;
padding: 15rpx 25rpx;
border-radius: 20rpx;
font-size: 24rpx;
box-shadow: 0 4rpx 15rpx rgba(255, 107, 107, 0.4);
}
/* 形状选择器 */
.shape-selector {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200rpx, 1fr));
gap: 20rpx;
}
.shape-option {
height: 140rpx;
border: 3rpx solid #e0e0e0;
border-radius: 20rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #ffffff, #f0f0f0);
transition: all 0.3s ease;
cursor: pointer;
box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.1);
&.active {
border-color: #667eea;
background: linear-gradient(145deg, #e3f2fd, #bbdefb);
transform: translateY(-5rpx);
box-shadow: 0 8rpx 25rpx rgba(102, 126, 234, 0.3);
.shape-name {
color: #667eea;
font-weight: bold;
}
}
} }
.card-container { .shape-preview-box {
display: flex; width: 60rpx;
flex-wrap: wrap; height: 60rpx;
justify-content: space-around; margin-bottom: 10rpx;
padding: 20rpx;
background: white;
border-radius: 20rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.shape-card {
width: 200rpx;
height: 200rpx;
margin: 15rpx;
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 16rpx;
background: white;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
} }
.shape-image { .shape-icon {
width: 140rpx; width: 50rpx;
height: 140rpx; height: 50rpx;
border: 2rpx solid #666;
&.shape-icon-circle {
border-radius: 50%;
background: transparent;
}
&.shape-icon-square {
border-radius: 5rpx;
background: transparent;
}
&.shape-icon-rectangle {
width: 60rpx;
height: 40rpx;
border-radius: 5rpx;
background: transparent;
}
&.shape-icon-triangle {
width: 0;
height: 0;
border: none;
border-left: 25rpx solid transparent;
border-right: 25rpx solid transparent;
border-bottom: 45rpx solid #666;
background: transparent;
}
&.shape-icon-oval {
border-radius: 50%;
width: 60rpx;
height: 40rpx;
background: transparent;
}
&.shape-icon-diamond {
width: 0;
height: 0;
border: none;
border-left: 25rpx solid transparent;
border-right: 25rpx solid transparent;
border-top: 25rpx solid transparent;
border-bottom: 25rpx solid #666;
background: transparent;
transform: rotate(45deg);
}
&.shape-icon-star {
position: relative;
width: 0;
height: 0;
border: none;
border-left: 25rpx solid transparent;
border-right: 25rpx solid transparent;
border-bottom: 18rpx solid #666;
background: transparent;
transform: rotate(35deg);
&::before {
content: '';
position: absolute;
left: -25rpx;
top: -12rpx;
width: 0;
height: 0;
border-left: 25rpx solid transparent;
border-right: 25rpx solid transparent;
border-bottom: 18rpx solid #666;
transform: rotate(-70deg);
}
}
&.shape-icon-heart {
position: relative;
width: 40rpx;
height: 35rpx;
border: none;
background: #666;
transform: rotate(-45deg);
&::before,
&::after {
content: '';
position: absolute;
width: 20rpx;
height: 30rpx;
background: #666;
border-radius: 20rpx 20rpx 0 0;
transform: rotate(-45deg);
transform-origin: 0 100%;
}
&::before {
left: 20rpx;
}
&::after {
top: -15rpx;
transform: rotate(45deg);
transform-origin: 100% 100%;
}
}
&.shape-icon-hexagon {
width: 40rpx;
height: 22rpx;
background: #666;
border: none;
position: relative;
&::before,
&::after {
content: '';
position: absolute;
width: 0;
height: 0;
border-left: 20rpx solid transparent;
border-right: 20rpx solid transparent;
}
&::before {
bottom: 100%;
border-bottom: 11rpx solid #666;
}
&::after {
top: 100%;
border-top: 11rpx solid #666;
}
}
&.shape-icon-pentagon {
position: relative;
width: 0;
height: 0;
border: none;
border-left: 25rpx solid transparent;
border-right: 25rpx solid transparent;
border-bottom: 30rpx solid #666;
background: transparent;
&::before {
content: '';
position: absolute;
left: -18rpx;
top: 30rpx;
width: 36rpx;
height: 15rpx;
background: #666;
}
}
&.shape-icon-semicircle {
width: 50rpx;
height: 25rpx;
border: 2rpx solid #666;
border-bottom: none;
border-radius: 50rpx 50rpx 0 0;
background: transparent;
}
&.shape-icon-trapezoid {
width: 0;
height: 0;
border: none;
border-left: 15rpx solid transparent;
border-right: 15rpx solid transparent;
border-bottom: 40rpx solid #666;
background: transparent;
position: relative;
&::before {
content: '';
position: absolute;
left: -25rpx;
top: 40rpx;
width: 50rpx;
height: 2rpx;
background: #666;
}
}
} }
.color-box { .shape-name {
position: absolute; font-size: 24rpx;
bottom: 10rpx; color: #666;
right: 10rpx; text-align: center;
width: 30rpx;
height: 30rpx;
border-radius: 6rpx;
border: 1rpx solid #eee;
} }
.control-panel { /* 颜色选择器 */
.color-selector {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120rpx, 1fr));
gap: 20rpx;
}
.color-option {
height: 100rpx;
border-radius: 20rpx;
border: 3rpx solid #e0e0e0;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
position: relative;
box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.1);
&.active {
border-color: #667eea;
transform: scale(1.05);
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.3);
}
.color-name {
color: white;
font-size: 22rpx;
font-weight: bold;
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.5);
}
}
/* 添加按钮 */
.add-btn {
width: 100%;
height: 90rpx;
font-size: 32rpx;
border-radius: 20rpx;
background: linear-gradient(45deg, #667eea, #764ba2);
border: none;
color: white;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 25rpx rgba(102, 126, 234, 0.4);
.add-icon {
font-size: 40rpx;
margin-right: 10rpx;
font-weight: bold;
}
}
/* 已选择的形状 */
.selected-shapes {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20rpx; gap: 15rpx;
padding: 30rpx;
background: white;
border-radius: 20rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
} }
.bg-picker { .selected-shape {
display: flex;
align-items: center;
padding: 20rpx;
background: linear-gradient(145deg, #f8f9fa, #e9ecef);
border-radius: 20rpx;
border: 2rpx solid #e0e0e0;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.1);
&:active {
background: linear-gradient(145deg, #ffebee, #ffcdd2);
border-color: #ff5722;
transform: scale(0.98);
}
}
.shape-preview {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
margin-right: 20rpx;
border: 2rpx solid #333;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
.shape-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 5rpx;
}
.shape-info {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.color-info {
font-size: 22rpx;
color: #666;
font-family: monospace;
}
.remove-hint {
font-size: 20rpx;
color: #999;
font-style: italic;
}
/* 设置项 */
.setting-item {
margin-bottom: 30rpx;
&:last-child {
margin-bottom: 0;
}
}
.setting-label {
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
display: block;
font-weight: 500;
}
.bg-radio-group {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 25rpx;
margin-top: 15rpx;
}
.bg-option {
display: flex;
align-items: center; align-items: center;
gap: 20rpx; gap: 10rpx;
padding: 20rpx; padding: 10rpx 15rpx;
background: white; border-radius: 15rpx;
border-radius: 20rpx; background: rgba(0, 0, 0, 0.05);
margin-bottom: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); text {
font-size: 26rpx;
color: #666;
}
} }
.label { .feature-intro {
font-weight: bold; margin-top: 30rpx;
padding: 25rpx;
background: linear-gradient(145deg, #f0f8ff, #e6f3ff);
border-radius: 15rpx;
border-left: 5rpx solid #667eea;
}
.intro-title {
font-size: 26rpx;
color: #333; color: #333;
font-weight: bold;
display: block;
margin-bottom: 15rpx;
} }
.intro-text {
font-size: 24rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
padding-left: 15rpx;
&:last-child {
margin-bottom: 0;
}
}
/* 操作按钮 */
.generate-btn {
width: 100%;
height: 90rpx;
font-size: 32rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
background: linear-gradient(45deg, #4CAF50, #45a049);
border: none;
color: white;
box-shadow: 0 8rpx 25rpx rgba(76, 175, 80, 0.4);
&:disabled {
background: #ccc;
box-shadow: none;
}
}
.save-btn {
width: 100%;
height: 90rpx;
font-size: 32rpx;
border-radius: 20rpx;
background: linear-gradient(45deg, #ff9800, #f57c00);
border: none;
color: white;
box-shadow: 0 8rpx 25rpx rgba(255, 152, 0, 0.4);
}
/* 隐藏的画布 */
.hidden-canvas {
position: fixed;
top: -9999rpx;
left: -9999rpx;
width: 595px;
height: 842px;
}
/* 预览图片 */
.preview-image { .preview-image {
width: 100%; width: 100%;
height: 600rpx; max-height: 600rpx;
background-color: white;
border-radius: 20rpx; border-radius: 20rpx;
border: 3rpx solid #e0e0e0;
background-color: white;
cursor: pointer;
box-shadow: 0 8rpx 25rpx rgba(0, 0, 0, 0.15);
transition: transform 0.3s ease;
&:active {
transform: scale(0.98);
}
}
.preview-hint {
display: block;
text-align: center;
font-size: 24rpx;
color: #666;
margin-top: 20rpx; margin-top: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1); padding: 15rpx;
border: 1rpx solid #eee; background: rgba(0, 0, 0, 0.05);
border-radius: 15rpx;
} }
+127 -42
View File
@@ -1,62 +1,147 @@
<view class="container"> <view class="container">
<!-- 形状卡片预览区 --> <!-- 标题 -->
<view class="card-container"> <view class="header">
<view wx:for="{{shapeCards}}" wx:key="index" class="shape-card"> <text class="title">形状涂色练习生成器</text>
<image <text class="subtitle">为小朋友制作涂色练习页</text>
src="{{item.svgData}}" </view>
mode="aspectFit"
class="shape-image" /> <!-- 形状选择区域 -->
<view class="section">
<view class="section-title">选择形状</view>
<view class="shape-selector">
<view <view
class="color-box" wx:for="{{availableShapes}}"
style="background-color:{{item.fillColor}}"></view> wx:key="id"
class="shape-option {{currentShapeType === item.id ? 'active' : ''}}"
data-shape-id="{{item.id}}"
bindtap="selectShape">
<!-- 使用简单的CSS形状预览 -->
<view class="shape-preview-box">
<view class="shape-icon shape-icon-{{item.id}}"></view>
</view>
<text class="shape-name">{{item.name}}</text>
</view>
</view> </view>
</view> </view>
<!-- 控制面板 --> <!-- 颜色选择区域 -->
<view class="control-panel"> <view class="section">
<slider <view class="section-title">选择颜色</view>
min="1" <view class="color-selector">
max="5" <view
value="{{columns}}" wx:for="{{availableColors}}"
bindchange="setColumns" wx:key="value"
show-value /> class="color-option {{currentColor === item.value ? 'active' : ''}}"
<text>布局列数: {{columns}}</text> style="background-color: {{item.value}}"
data-color="{{item.value}}"
bindtap="selectColor">
<view class="color-name">{{item.name}}</view>
</view>
</view>
</view>
<!-- 添加按钮 -->
<view class="section">
<button class="add-btn" bindtap="addShape" type="primary">
<text class="add-icon">+</text>
添加 {{currentShapeType === 'circle' ? '圆形' : currentShapeType ===
'square' ? '正方形' : currentShapeType === 'rectangle' ? '长方形' :
currentShapeType === 'triangle' ? '三角形' : currentShapeType ===
'oval' ? '椭圆形' : currentShapeType === 'diamond' ? '菱形' :
currentShapeType === 'star' ? '星形' : currentShapeType === 'heart'
? '心形' : currentShapeType === 'hexagon' ? '六边形' :
currentShapeType === 'pentagon' ? '五边形' : currentShapeType ===
'semicircle' ? '半圆形' : currentShapeType === 'trapezoid' ? '梯形'
: '形状'}}
</button>
</view>
<!-- 已选择的形状列表 -->
<view class="section" wx:if="{{selectedShapes.length > 0}}">
<view class="section-title">
已选择的形状 ({{selectedShapes.length}})
<button class="clear-btn" bindtap="clearShapes" size="mini">
清空
</button>
</view>
<view class="selected-shapes">
<view
wx:for="{{selectedShapes}}"
wx:key="id"
class="selected-shape"
data-index="{{index}}"
bindlongpress="removeShape">
<view
class="shape-preview"
style="background-color: {{item.color}}"></view>
<view class="shape-details">
<text class="shape-info">{{item.name}}</text>
<text class="color-info">{{item.color}}</text>
</view>
<text class="remove-hint">长按删除</text>
</view>
</view>
</view>
<!-- 设置区域 -->
<view class="section">
<view class="section-title">打印设置</view>
<view class="setting-item">
<text class="setting-label">背景颜色:</text>
<radio-group bindchange="setBackground" class="bg-radio-group">
<label
wx:for="{{backgrounds}}"
wx:key="value"
class="bg-option">
<radio
value="{{item.value}}"
checked="{{bgColor === item.value}}"
color="#1976d2" />
<text>{{item.name}}</text>
</label>
</radio-group>
</view>
<view class="feature-intro">
<text class="intro-title">功能说明:</text>
<text class="intro-text">• 上半部分:带颜色的形状示例</text>
<text class="intro-text">• 下半部分:无颜色的练习区域</text>
<text class="intro-text">• 图片大小:A4 纸张,适合打印</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="section">
<button <button
class="generate-btn"
bindtap="generateImage" bindtap="generateImage"
type="primary" type="primary"
loading="{{isGenerating}}"> loading="{{isGenerating}}"
生成图片 disabled="{{selectedShapes.length === 0}}">
{{isGenerating ? '生成中...' : '生成练习页'}}
</button> </button>
<button <button
bindtap="saveToAlbum"
wx:if="{{generatedImage}}" wx:if="{{generatedImage}}"
type="warn" class="save-btn"
plain> bindtap="saveToAlbum"
type="warn">
保存到相册 保存到相册
</button> </button>
</view> </view>
<!-- 预览区域 --> <!-- Canvas 画布 (隐藏) -->
<image <canvas type="2d" id="mainCanvas" class="hidden-canvas"> </canvas>
wx:if="{{generatedImage}}"
src="{{generatedImage}}"
class="preview-image"
bindtap="previewFullImage" />
<!-- 背景选择器 --> <!-- 预览生成的图片 -->
<view class="bg-picker"> <view class="section" wx:if="{{generatedImage}}">
<text class="label">背景颜色:</text> <view class="section-title">生成的练习页</view>
<radio-group bindchange="setBackground"> <image
<radio src="{{generatedImage}}"
wx:for="{{backgrounds}}" class="preview-image"
wx:key="value" mode="aspectFit"
value="{{item.value}}" bindtap="previewImage" />
color="{{item.color}}" <text class="preview-hint">点击图片可放大预览 • A4尺寸 • 适合打印</text>
checked="{{bgColor === item.value}}">
{{item.name}}
</radio>
</radio-group>
</view> </view>
</view> </view>
+81 -37
View File
@@ -1,61 +1,105 @@
// shapes.js // shapes.js - 幼儿园到小学三年级基本几何形状定义
const shapes = [ const shapes = [
{
id: 'circle',
name: '圆形',
type: 'circle',
svg: '<circle cx="50" cy="50" r="40" stroke="#333" stroke-width="2" fill="none"/>',
drawFunction: 'drawCircle',
},
{ {
id: 'square', id: 'square',
name: '正方形', name: '正方形',
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> type: 'rect',
<rect x="5" y="5" width="90" height="90" fill="var(--fill-color)" stroke="black" stroke-width="2"/> svg: '<rect x="10" y="10" width="80" height="80" stroke="#333" stroke-width="2" fill="none"/>',
</svg>`, drawFunction: 'drawSquare',
},
{
id: 'rectangle',
name: '长方形',
type: 'rect',
svg: '<rect x="5" y="25" width="90" height="50" stroke="#333" stroke-width="2" fill="none"/>',
drawFunction: 'drawRectangle',
},
{
id: 'triangle',
name: '三角形',
type: 'triangle',
svg: '<polygon points="50,10 20,80 80,80" stroke="#333" stroke-width="2" fill="none"/>',
drawFunction: 'drawTriangle',
},
{
id: 'oval',
name: '椭圆形',
type: 'oval',
svg: '<ellipse cx="50" cy="50" rx="40" ry="25" stroke="#333" stroke-width="2" fill="none"/>',
drawFunction: 'drawOval',
}, },
{ {
id: 'diamond', id: 'diamond',
name: '菱形', name: '菱形',
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> type: 'diamond',
<polygon points="50,5 95,50 50,95 5,50" fill="var(--fill-color)" stroke="black" stroke-width="2"/> svg: '<polygon points="50,10 80,50 50,90 20,50" stroke="#333" stroke-width="2" fill="none"/>',
</svg>`, drawFunction: 'drawDiamond',
}, },
{ {
id: 'cylinder', id: 'star',
name: '圆柱体', name: '星形',
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> type: 'star',
<rect x="20" y="40" width="60" height="50" rx="5" ry="5" fill="var(--fill-color)" stroke="black" stroke-width="2"/> svg: '<polygon points="50,5 60,35 90,35 70,55 80,85 50,70 20,85 30,55 10,35 40,35" stroke="#333" stroke-width="2" fill="none"/>',
<ellipse cx="50" cy="40" rx="30" ry="10" fill="var(--fill-color)" stroke="black" stroke-width="2"/> drawFunction: 'drawStar',
<ellipse cx="50" cy="90" rx="30" ry="10" fill="var(--fill-color)" stroke="black" stroke-width="2" style="opacity:0.7"/>
</svg>`,
}, },
{ {
id: 'cube', id: 'heart',
name: '立方体', name: '心形',
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> type: 'heart',
<polygon points="20,30 50,10 80,30 50,50" fill="var(--fill-color)" stroke="black" stroke-width="2" opacity="0.9"/> svg: '<path d="M50,85 C30,65 10,45 10,25 C10,15 20,5 30,5 C40,5 50,15 50,25 C50,15 60,5 70,5 C80,5 90,15 90,25 C90,45 70,65 50,85 Z" stroke="#333" stroke-width="2" fill="none"/>',
<polygon points="50,50 80,30 80,70 50,90" fill="var(--fill-color)" stroke="black" stroke-width="2" opacity="0.7"/> drawFunction: 'drawHeart',
<polygon points="20,30 20,70 50,90 50,50" fill="var(--fill-color)" stroke="black" stroke-width="2" opacity="0.8"/>
</svg>`,
}, },
{ {
id: 'circle', id: 'hexagon',
name: '形', name: '六边形',
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> type: 'hexagon',
<circle cx="50" cy="50" r="40" fill="var(--fill-color)" stroke="black" stroke-width="2"/> svg: '<polygon points="25,15 75,15 90,50 75,85 25,85 10,50" stroke="#333" stroke-width="2" fill="none"/>',
</svg>`, drawFunction: 'drawHexagon',
}, },
{ {
id: 'sphere', id: 'pentagon',
name: '球体', name: '五边形',
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> type: 'pentagon',
<circle cx="50" cy="50" r="40" fill="var(--fill-color)" stroke="black" stroke-width="2" style="opacity:0.9"/> svg: '<polygon points="50,10 80,35 65,75 35,75 20,35" stroke="#333" stroke-width="2" fill="none"/>',
<ellipse cx="50" cy="50" rx="30" ry="10" fill="var(--fill-color)" stroke="black" stroke-width="2" style="opacity:0.7"/> drawFunction: 'drawPentagon',
</svg>`, },
{
id: 'semicircle',
name: '半圆形',
type: 'semicircle',
svg: '<path d="M 15 50 A 35 35 0 0 1 85 50 Z" stroke="#333" stroke-width="2" fill="none"/>',
drawFunction: 'drawSemicircle',
},
{
id: 'trapezoid',
name: '梯形',
type: 'trapezoid',
svg: '<polygon points="30,20 70,20 85,80 15,80" stroke="#333" stroke-width="2" fill="none"/>',
drawFunction: 'drawTrapezoid',
}, },
]; ];
// 颜色选项 // 颜色选项
const colors = [ const colors = [
{ name: '红色', value: '#ff5252' }, { name: '红色', value: '#FF4444' },
{ name: '蓝色', value: '#448aff' }, { name: '蓝色', value: '#4A90E2' },
{ name: '绿色', value: '#69f0ae' }, { name: '绿色', value: '#7ED321' },
{ name: '黄色', value: '#ffd740' }, { name: '黄色', value: '#F5A623' },
{ name: '紫色', value: '#e040fb' }, { name: '紫色', value: '#9013FE' },
{ name: '橙色', value: '#FF8F00' },
{ name: '粉色', value: '#E91E63' },
{ name: '青色', value: '#00BCD4' },
{ name: '深绿', value: '#4CAF50' },
{ name: '深蓝', value: '#2196F3' },
{ name: '棕色', value: '#8D6E63' },
{ name: '灰色', value: '#9E9E9E' },
]; ];
export { shapes, colors }; export { shapes, colors };
+9 -2
View File
@@ -24,11 +24,18 @@
"miniprogram": { "miniprogram": {
"list": [ "list": [
{ {
"name": "debug页", "name": "测试图形绘制",
"pathName": "pages/debug/debug", "pathName": "pages/shapePrint/index",
"query": "", "query": "",
"scene": null, "scene": null,
"launchMode": "default" "launchMode": "default"
},
{
"name": "debug页",
"pathName": "pages/debug/debug",
"query": "",
"launchMode": "default",
"scene": null
} }
] ]
} }