628 lines
19 KiB
JavaScript
628 lines
19 KiB
JavaScript
// 引入形状和颜色数据
|
|
import { shapes, colors } from './shapes';
|
|
|
|
Page({
|
|
data: {
|
|
// 可选择的形状列表
|
|
availableShapes: shapes,
|
|
// 可选择的颜色列表
|
|
availableColors: colors,
|
|
// 已选择的形状列表
|
|
selectedShapes: [],
|
|
// 当前选中的形状类型
|
|
currentShapeType: 'circle',
|
|
// 当前选中的颜色
|
|
currentColor: '#FF4444',
|
|
// 生成的图片
|
|
generatedImage: null,
|
|
// 是否正在生成
|
|
isGenerating: false,
|
|
// 背景选项
|
|
backgrounds: [
|
|
{ name: '纯白', value: '#FFFFFF' },
|
|
{ name: '米黄', value: '#FAF5E8' },
|
|
{ name: '淡灰', value: '#F5F5F5' },
|
|
],
|
|
bgColor: '#FFFFFF',
|
|
},
|
|
|
|
onLoad() {
|
|
console.log('形状绘制页面加载完成');
|
|
},
|
|
|
|
// 选择形状类型
|
|
selectShape(e) {
|
|
const shapeId = e.currentTarget.dataset.shapeId;
|
|
this.setData({
|
|
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: [],
|
|
});
|
|
},
|
|
|
|
// 设置背景颜色
|
|
setBackground(e) {
|
|
this.setData({
|
|
bgColor: e.detail.value,
|
|
});
|
|
},
|
|
|
|
// 创建形状的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 query = wx.createSelectorQuery().in(this);
|
|
|
|
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 canvas = res[0].node;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// A4纸张尺寸 (595x842 点,按照72DPI)
|
|
const dpr = wx.getSystemInfoSync().pixelRatio;
|
|
const a4Width = 595;
|
|
const a4Height = 842;
|
|
|
|
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) => {
|
|
this.setData({
|
|
generatedImage: res.tempFilePath,
|
|
isGenerating: false,
|
|
});
|
|
wx.showToast({
|
|
title: '图片生成成功',
|
|
icon: 'success',
|
|
});
|
|
},
|
|
fail: (err) => {
|
|
console.error('导出图片失败:', err);
|
|
this.setData({ isGenerating: false });
|
|
wx.showToast({
|
|
title: '图片生成失败',
|
|
icon: 'none',
|
|
});
|
|
},
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error('生成图片出错:', error);
|
|
this.setData({ isGenerating: false });
|
|
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) {
|
|
wx.showToast({
|
|
title: '请先生成图片',
|
|
icon: 'none',
|
|
});
|
|
return;
|
|
}
|
|
|
|
wx.saveImageToPhotosAlbum({
|
|
filePath: this.data.generatedImage,
|
|
success: () => {
|
|
wx.showToast({
|
|
title: '保存成功',
|
|
icon: 'success',
|
|
});
|
|
},
|
|
fail: (err) => {
|
|
if (err.errMsg.includes('auth deny')) {
|
|
this.showAuthGuide();
|
|
} else {
|
|
wx.showToast({
|
|
title: '保存失败',
|
|
icon: 'none',
|
|
});
|
|
}
|
|
},
|
|
});
|
|
},
|
|
|
|
// 显示授权引导
|
|
showAuthGuide() {
|
|
wx.showModal({
|
|
title: '权限申请',
|
|
content: '需要相册访问权限才能保存图片,请在设置中开启权限',
|
|
success: (res) => {
|
|
if (res.confirm) {
|
|
wx.openSetting();
|
|
}
|
|
},
|
|
});
|
|
},
|
|
|
|
// 预览生成的图片
|
|
previewImage() {
|
|
if (!this.data.generatedImage) return;
|
|
|
|
wx.previewImage({
|
|
current: this.data.generatedImage,
|
|
urls: [this.data.generatedImage],
|
|
});
|
|
},
|
|
});
|