From c5538ce0441d7484101da5fed9a98b5165d8265f Mon Sep 17 00:00:00 2001 From: joey Date: Tue, 22 Jul 2025 16:36:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AI=20=E4=BF=AE=E6=94=B9=E5=9B=BE?= =?UTF-8?q?=E5=BD=A2=E6=89=93=E5=8D=B0=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/demoPages/shapePrint/index.js | 984 ++++++++++++-------- miniprogram/demoPages/shapePrint/index.json | 13 +- miniprogram/demoPages/shapePrint/index.less | 430 +++++++-- miniprogram/demoPages/shapePrint/index.wxml | 181 ++-- 4 files changed, 1049 insertions(+), 559 deletions(-) diff --git a/miniprogram/demoPages/shapePrint/index.js b/miniprogram/demoPages/shapePrint/index.js index 318f4f8..69af751 100644 --- a/miniprogram/demoPages/shapePrint/index.js +++ b/miniprogram/demoPages/shapePrint/index.js @@ -1,18 +1,17 @@ // 引入形状和颜色数据 import { shapes, colors } from './shapes'; +import { WATER_COLORS } from '../../constants/colors'; Page({ data: { // 可选择的形状列表 availableShapes: shapes, - // 可选择的颜色列表 - availableColors: colors, // 已选择的形状列表 selectedShapes: [], - // 当前选中的形状类型 - currentShapeType: 'circle', - // 当前选中的颜色 - currentColor: '#FF4444', + // 基础12色 + basic12Colors: WATER_COLORS.basic12, + // 已使用的颜色索引(确保不重复) + usedColorIndexes: [], // 生成的图片 generatedImage: null, // 是否正在生成 @@ -24,178 +23,580 @@ Page({ { name: '淡灰', value: '#F5F5F5' }, ], bgColor: '#FFFFFF', + // 弹窗控制 + showShapeSelector: false, + showColorPicker: false, + // 当前编辑的形状索引 + currentEditingIndex: -1, + currentEditingColor: '', + // 临时选择的形状ID列表(弹窗中的选择状态) + tempSelectedShapeIds: [], }, onLoad() { console.log('形状绘制页面加载完成'); + // 初始化临时选择列表 + this.setData({ + tempSelectedShapeIds: this.data.selectedShapes.map( + (shape) => shape.id, + ), + }); }, - // 选择形状类型 - selectShape(e) { + // 打开形状选择器 + openShapeSelector() { + this.setData({ + showShapeSelector: true, + // 将当前已选择的形状ID设置到临时列表中 + tempSelectedShapeIds: this.data.selectedShapes.map( + (shape) => shape.shapeId, + ), + }); + }, + + // 关闭形状选择器 + closeShapeSelector() { + this.setData({ + showShapeSelector: false, + }); + }, + + // 切换形状选择状态 + toggleShape(e) { const shapeId = e.currentTarget.dataset.shapeId; - this.setData({ - currentShapeType: shapeId, + const { tempSelectedShapeIds } = this.data; + + if (tempSelectedShapeIds.includes(shapeId)) { + // 取消选择 + const newTempSelected = tempSelectedShapeIds.filter( + (id) => id !== shapeId, + ); + this.setData({ + tempSelectedShapeIds: newTempSelected, + }); + } else { + // 添加选择(最多6个) + if (tempSelectedShapeIds.length < 6) { + this.setData({ + tempSelectedShapeIds: [...tempSelectedShapeIds, shapeId], + }); + } else { + wx.showToast({ + title: '最多只能选择6个形状', + icon: 'none', + duration: 2000, + }); + } + } + }, + + // 确认形状选择 + confirmShapeSelection() { + const { tempSelectedShapeIds, availableShapes, selectedShapes } = + this.data; + + // 构建新的选择列表 + const newSelectedShapes = []; + const usedColorIndexes = []; + + tempSelectedShapeIds.forEach((shapeId, index) => { + const shape = availableShapes.find((s) => s.id === shapeId); + if (shape) { + // 查找是否已存在该形状 + const existingShape = selectedShapes.find( + (s) => s.shapeId === shapeId, + ); + if (existingShape) { + // 保持原有颜色 + newSelectedShapes.push(existingShape); + const colorIndex = this.data.basic12Colors.findIndex( + (c) => c.hex === existingShape.fillColor, + ); + if (colorIndex !== -1) { + usedColorIndexes.push(colorIndex); + } + } else { + // 新形状,分配随机颜色 + const randomColor = + this.getRandomUnusedColor(usedColorIndexes); + newSelectedShapes.push({ + id: Date.now() + index, + shapeId: shape.id, + name: shape.name, + fillColor: randomColor.hex, + colorName: randomColor.name, + drawFunction: shape.drawFunction, + }); + } + } + }); + + this.setData( + { + selectedShapes: newSelectedShapes, + usedColorIndexes, + showShapeSelector: false, + }, + () => { + // 自动生成新的打印纸 + if (newSelectedShapes.length > 0) { + this.generateImage(); + } + }, + ); + + wx.showToast({ + title: `已选择${newSelectedShapes.length}个形状`, + icon: 'success', + duration: 1500, }); }, - // 选择颜色 - selectColor(e) { - const color = e.currentTarget.dataset.color; - this.setData({ - currentColor: color, + // 获取随机未使用的颜色 + getRandomUnusedColor(usedIndexes) { + const { basic12Colors } = this.data; + const availableIndexes = []; + + for (let i = 0; i < basic12Colors.length; i++) { + if (!usedIndexes.includes(i)) { + availableIndexes.push(i); + } + } + + if (availableIndexes.length === 0) { + // 如果所有颜色都被使用,则重新开始 + const randomIndex = Math.floor( + Math.random() * basic12Colors.length, + ); + usedIndexes.push(randomIndex); + return basic12Colors[randomIndex]; + } + + const randomIndex = + availableIndexes[ + Math.floor(Math.random() * availableIndexes.length) + ]; + usedIndexes.push(randomIndex); + return basic12Colors[randomIndex]; + }, + + // 删除形状 + deleteShape(e) { + const index = parseInt(e.currentTarget.dataset.index); + const selectedShapes = [...this.data.selectedShapes]; + const deletedShape = selectedShapes[index]; + + // 释放颜色 + const colorIndex = this.data.basic12Colors.findIndex( + (c) => c.hex === deletedShape.fillColor, + ); + const usedColorIndexes = this.data.usedColorIndexes.filter( + (i) => i !== colorIndex, + ); + + selectedShapes.splice(index, 1); + + this.setData( + { + selectedShapes, + usedColorIndexes, + }, + () => { + // 如果还有形状,自动重新生成 + if (selectedShapes.length > 0) { + this.generateImage(); + } else { + // 清空生成的图片 + this.setData({ + generatedImage: null, + }); + } + }, + ); + + wx.showToast({ + title: '已删除形状', + icon: 'success', + duration: 1000, }); }, - // 添加形状到选中列表 - addShape() { - const { currentShapeType, currentColor, selectedShapes } = this.data; - const shape = shapes.find((s) => s.id === currentShapeType); + // 打开颜色选择器 + openColorPicker(e) { + const index = parseInt(e.currentTarget.dataset.index); + const shape = this.data.selectedShapes[index]; - if (shape) { - const newShape = { - id: Date.now(), - type: shape.type, - name: shape.name, - color: currentColor, - shapeId: currentShapeType, - drawFunction: shape.drawFunction, + this.setData({ + showColorPicker: true, + currentEditingIndex: index, + currentEditingColor: shape.fillColor, + }); + }, + + // 关闭颜色选择器 + closeColorPicker() { + this.setData({ + showColorPicker: false, + currentEditingIndex: -1, + currentEditingColor: '', + }); + }, + + // 颜色改变事件 + onColorChange(e) { + const { color } = e.detail; + const { currentEditingIndex, selectedShapes, basic12Colors } = + this.data; + + if (currentEditingIndex >= 0) { + const newSelectedShapes = [...selectedShapes]; + const colorInfo = basic12Colors.find((c) => c.hex === color) || + WATER_COLORS.extended24.find((c) => c.hex === color) || { + name: '自定义色', + hex: color, + }; + + newSelectedShapes[currentEditingIndex] = { + ...newSelectedShapes[currentEditingIndex], + fillColor: color, + colorName: colorInfo.name, }; - this.setData({ - selectedShapes: [...selectedShapes, newShape], - }); + this.setData( + { + selectedShapes: newSelectedShapes, + showColorPicker: false, + currentEditingIndex: -1, + }, + () => { + // 自动重新生成 + this.generateImage(); + }, + ); wx.showToast({ - title: `添加了${shape.name}`, + title: '颜色已更新', 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, + this.setData( + { + bgColor: e.detail.value, + }, + () => { + // 如果有形状,自动重新生成 + if (this.data.selectedShapes.length > 0) { + this.generateImage(); + } + }, + ); + }, + + // 生成练习页图片 + async generateImage() { + if (this.data.selectedShapes.length === 0) { + wx.showToast({ + title: '请先选择形状', + icon: 'none', + }); + return; + } + + this.setData({ isGenerating: true }); + + try { + const canvas = await this.getCanvas(); + const ctx = canvas.getContext('2d'); + + // A4纸张尺寸 (595x842 像素,72 DPI) + const width = 595; + const height = 842; + + // 设置画布尺寸 + canvas.width = width; + canvas.height = height; + + // 设置背景 + ctx.fillStyle = this.data.bgColor; + ctx.fillRect(0, 0, width, height); + + // 绘制标题 + this.drawTitle(ctx, width); + + // 绘制有颜色的示例部分(上半部) + this.drawColoredExamples(ctx, width, height); + + // 绘制分隔线 + this.drawDivider(ctx, width, height); + + // 绘制练习区域(下半部,无颜色) + this.drawPracticeArea(ctx, width, height); + + // 导出图片 + const tempFilePath = canvas.toTempFilePathSync({ + fileType: 'png', + quality: 1, + }); + + this.setData({ + generatedImage: tempFilePath, + isGenerating: false, + }); + + wx.showToast({ + title: '生成完成', + icon: 'success', + }); + } catch (error) { + console.error('生成图片失败:', error); + this.setData({ isGenerating: false }); + wx.showToast({ + title: '生成失败,请重试', + icon: 'none', + }); + } + }, + + // 获取Canvas实例 + getCanvas() { + return new Promise((resolve) => { + const query = wx.createSelectorQuery().in(this); + query + .select('#mainCanvas') + .fields({ node: true, size: true }) + .exec((res) => { + const canvas = res[0].node; + resolve(canvas); + }); }); }, - // 创建形状的SVG预览 - createShapePreview(shape) { - const svgContent = ` - - ${shape.svg} - - `; + // 绘制标题 + drawTitle(ctx, width) { + ctx.save(); + ctx.fillStyle = '#333'; + ctx.font = 'bold 24px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('形状涂色练习', width / 2, 40); - const base64 = wx.arrayBufferToBase64( - new TextEncoder().encode(svgContent), - ); - return `data:image/svg+xml;base64,${base64}`; + ctx.font = '16px Arial'; + ctx.fillText('请为下面的形状涂上颜色', width / 2, 70); + ctx.restore(); }, - // 绘制各种形状的函数 - drawCircle(ctx, x, y, size, color, filled = true) { + // 绘制有颜色的示例 + drawColoredExamples(ctx, width, height) { + const { selectedShapes } = this.data; + const startY = 100; + const shapeSize = 80; + const cols = 3; + const spacing = width / (cols + 1); + + selectedShapes.forEach((shape, index) => { + const row = Math.floor(index / cols); + const col = index % cols; + const x = (col + 1) * spacing; + const y = startY + row * (shapeSize + 60); + + // 绘制形状 + this.drawShapeOnCanvas(ctx, shape, x, y, shapeSize, true); + + // 绘制形状名称 + ctx.save(); + ctx.fillStyle = '#333'; + ctx.font = '14px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(shape.name, x, y + shapeSize / 2 + 25); + ctx.restore(); + }); + }, + + // 绘制分隔线 + drawDivider(ctx, width, height) { + const y = height / 2; ctx.save(); - if (filled) { - ctx.fillStyle = color; + ctx.strokeStyle = '#ccc'; + ctx.lineWidth = 2; + ctx.setLineDash([10, 5]); + ctx.beginPath(); + ctx.moveTo(50, y); + ctx.lineTo(width - 50, y); + ctx.stroke(); + + // 分隔线文字 + ctx.fillStyle = '#666'; + ctx.font = '14px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('练习区域(请涂色)', width / 2, y + 20); + ctx.restore(); + }, + + // 绘制练习区域 + drawPracticeArea(ctx, width, height) { + const { selectedShapes } = this.data; + const startY = height / 2 + 60; + const shapeSize = 80; + const cols = 3; + const spacing = width / (cols + 1); + + selectedShapes.forEach((shape, index) => { + const row = Math.floor(index / cols); + const col = index % cols; + const x = (col + 1) * spacing; + const y = startY + row * (shapeSize + 60); + + // 绘制形状(无填充色,仅轮廓) + this.drawShapeOnCanvas(ctx, shape, x, y, shapeSize, false); + + // 绘制形状名称 + ctx.save(); + ctx.fillStyle = '#333'; + ctx.font = '14px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(shape.name, x, y + shapeSize / 2 + 25); + ctx.restore(); + }); + }, + + // 在Canvas上绘制形状 + drawShapeOnCanvas(ctx, shape, x, y, size, filled) { + const color = filled ? shape.fillColor : 'transparent'; + + switch (shape.drawFunction) { + case 'drawCircle': + this.drawCircle(ctx, x, y, size, color, '#333', filled); + break; + case 'drawSquare': + this.drawSquare(ctx, x, y, size, color, '#333', filled); + break; + case 'drawRectangle': + this.drawRectangle(ctx, x, y, size, color, '#333', filled); + break; + case 'drawTriangle': + this.drawTriangle(ctx, x, y, size, color, '#333', filled); + break; + case 'drawOval': + this.drawOval(ctx, x, y, size, color, '#333', filled); + break; + case 'drawDiamond': + this.drawDiamond(ctx, x, y, size, color, '#333', filled); + break; + case 'drawStar': + this.drawStar(ctx, x, y, size, color, '#333', filled); + break; + case 'drawHeart': + this.drawHeart(ctx, x, y, size, color, '#333', filled); + break; + case 'drawHexagon': + this.drawHexagon(ctx, x, y, size, color, '#333', filled); + break; + case 'drawPentagon': + this.drawPentagon(ctx, x, y, size, color, '#333', filled); + break; + case 'drawSemicircle': + this.drawSemicircle(ctx, x, y, size, color, '#333', filled); + break; + case 'drawTrapezoid': + this.drawTrapezoid(ctx, x, y, size, color, '#333', filled); + break; } - ctx.strokeStyle = '#333'; - ctx.lineWidth = 3; + }, + + // 绘制各种形状的函数(更新参数以支持边框色) + drawCircle(ctx, x, y, size, fillColor, borderColor, filled = true) { + ctx.save(); ctx.beginPath(); ctx.arc(x, y, size * 0.4, 0, Math.PI * 2); - if (filled) ctx.fill(); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); }, - drawSquare(ctx, x, y, size, color, filled = true) { + drawSquare(ctx, x, y, size, fillColor, borderColor, 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); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fillRect(startX, startY, squareSize, squareSize); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; ctx.strokeRect(startX, startY, squareSize, squareSize); ctx.restore(); }, - drawRectangle(ctx, x, y, size, color, filled = true) { + drawRectangle(ctx, x, y, size, fillColor, borderColor, 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); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fillRect(startX, startY, rectWidth, rectHeight); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; ctx.strokeRect(startX, startY, rectWidth, rectHeight); ctx.restore(); }, - drawTriangle(ctx, x, y, size, color, filled = true) { + drawTriangle(ctx, x, y, size, fillColor, borderColor, 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(); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); }, - drawOval(ctx, x, y, size, color, filled = true) { + drawOval(ctx, x, y, size, fillColor, borderColor, 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(); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); }, - drawDiamond(ctx, x, y, size, color, filled = true) { + drawDiamond(ctx, x, y, size, fillColor, borderColor, 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); @@ -203,18 +604,20 @@ Page({ ctx.lineTo(x, y + diamondSize); ctx.lineTo(x - diamondSize, y); ctx.closePath(); - if (filled) ctx.fill(); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); }, - drawStar(ctx, x, y, size, color, filled = true) { + drawStar(ctx, x, y, size, fillColor, borderColor, 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(); @@ -227,18 +630,20 @@ Page({ else ctx.lineTo(pointX, pointY); } ctx.closePath(); - if (filled) ctx.fill(); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); }, - drawHeart(ctx, x, y, size, color, filled = true) { + drawHeart(ctx, x, y, size, fillColor, borderColor, 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); @@ -258,18 +663,20 @@ Page({ x, y + heartSize * 0.3, ); - if (filled) ctx.fill(); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); }, - drawHexagon(ctx, x, y, size, color, filled = true) { + drawHexagon(ctx, x, y, size, fillColor, borderColor, 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++) { @@ -280,18 +687,20 @@ Page({ else ctx.lineTo(pointX, pointY); } ctx.closePath(); - if (filled) ctx.fill(); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); }, - drawPentagon(ctx, x, y, size, color, filled = true) { + drawPentagon(ctx, x, y, size, fillColor, borderColor, 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++) { @@ -302,34 +711,38 @@ Page({ else ctx.lineTo(pointX, pointY); } ctx.closePath(); - if (filled) ctx.fill(); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); }, - drawSemicircle(ctx, x, y, size, color, filled = true) { + drawSemicircle(ctx, x, y, size, fillColor, borderColor, 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(); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); }, - drawTrapezoid(ctx, x, y, size, color, filled = true) { + drawTrapezoid(ctx, x, y, size, fillColor, borderColor, 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); @@ -337,245 +750,23 @@ Page({ ctx.lineTo(x + trapezoidSize, y + trapezoidSize * 0.5); ctx.lineTo(x - trapezoidSize, y + trapezoidSize * 0.5); ctx.closePath(); - if (filled) ctx.fill(); + + if (filled && fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + + ctx.strokeStyle = borderColor; + ctx.lineWidth = 3; 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: '请先生成图片', + title: '没有可保存的图片', icon: 'none', }); return; @@ -585,13 +776,23 @@ Page({ filePath: this.data.generatedImage, success: () => { wx.showToast({ - title: '保存成功', + title: '已保存到相册', icon: 'success', }); }, fail: (err) => { - if (err.errMsg.includes('auth deny')) { - this.showAuthGuide(); + console.error('保存失败:', err); + if (err.errMsg.includes('auth')) { + wx.showModal({ + title: '提示', + content: '需要授权访问相册才能保存图片', + confirmText: '去授权', + success: (res) => { + if (res.confirm) { + wx.openSetting(); + } + }, + }); } else { wx.showToast({ title: '保存失败', @@ -602,26 +803,13 @@ Page({ }); }, - // 显示授权引导 - 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], - }); + if (this.data.generatedImage) { + wx.previewImage({ + urls: [this.data.generatedImage], + current: this.data.generatedImage, + }); + } }, }); diff --git a/miniprogram/demoPages/shapePrint/index.json b/miniprogram/demoPages/shapePrint/index.json index b686e95..e425a53 100644 --- a/miniprogram/demoPages/shapePrint/index.json +++ b/miniprogram/demoPages/shapePrint/index.json @@ -1,7 +1,12 @@ { - "navigationBarTitleText": "图形绘制 示例", - "navigationBarBackgroundColor": "#d2e7d8", + "navigationBarTitleText": "形状涂色练习生成器", + "navigationBarBackgroundColor": "#667eea", "homeButton": true, - "backgroundColor": "#e8eddb", - "enablePullDownRefresh": false + "backgroundColor": "#f5f5f5", + "enablePullDownRefresh": false, + "usingComponents": { + "van-popup": "@vant/weapp/popup/index", + "van-icon": "@vant/weapp/icon/index", + "color-picker": "/components/color-picker/color-picker" + } } diff --git a/miniprogram/demoPages/shapePrint/index.less b/miniprogram/demoPages/shapePrint/index.less index 5bc908d..03cb644 100644 --- a/miniprogram/demoPages/shapePrint/index.less +++ b/miniprogram/demoPages/shapePrint/index.less @@ -48,24 +48,171 @@ padding-bottom: 15rpx; } -.clear-btn { - background: linear-gradient(45deg, #ff6b6b, #ee5a24); - color: white; - border: none; - padding: 15rpx 25rpx; +/* 选择形状按钮 */ +.select-shapes-btn { + width: 100%; + height: 90rpx; + font-size: 32rpx; border-radius: 20rpx; - font-size: 24rpx; - box-shadow: 0 4rpx 15rpx rgba(255, 107, 107, 0.4); + 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); + + .btn-text { + margin-left: 10rpx; + font-weight: 500; + } } -/* 形状选择器 */ -.shape-selector { +/* 形状卡片容器 */ +.shape-cards-container { display: grid; - grid-template-columns: repeat(auto-fill, minmax(200rpx, 1fr)); + grid-template-columns: 1fr 1fr; gap: 20rpx; } +/* 形状卡片 */ +.shape-card { + position: relative; + padding: 20rpx; + background: linear-gradient(145deg, #ffffff, #f0f0f0); + 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); + display: flex; + flex-direction: column; + align-items: center; + + &:active { + transform: scale(0.98); + background: linear-gradient(145deg, #f8f9fa, #e9ecef); + } + + .delete-btn { + position: absolute; + top: 10rpx; + right: 10rpx; + width: 40rpx; + height: 40rpx; + background: rgba(255, 255, 255, 0.9); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15); + z-index: 10; + } + + .shape-preview-container { + position: relative; + width: 80rpx; + height: 80rpx; + margin-bottom: 15rpx; + display: flex; + align-items: center; + justify-content: center; + } + + .shape-preview-canvas { + position: absolute; + width: 80rpx; + height: 80rpx; + opacity: 0; + /* 隐藏,只用于未来可能的Canvas预览 */ + } + + .shape-name { + font-size: 26rpx; + color: #333; + font-weight: 500; + margin-bottom: 10rpx; + text-align: center; + } + + .color-info { + display: flex; + align-items: center; + gap: 8rpx; + + .color-dot { + width: 20rpx; + height: 20rpx; + border-radius: 50%; + border: 2rpx solid #ddd; + } + + .color-text { + font-size: 22rpx; + color: #666; + } + } +} + +/* 形状选择弹窗样式 */ +.shape-selector-popup { + .van-popup { + max-height: 80vh; + } +} + +.shape-selector-container { + padding: 30rpx; + max-height: 80vh; + overflow-y: auto; + + .popup-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30rpx; + padding-bottom: 20rpx; + border-bottom: 2rpx solid #f0f0f0; + + .popup-title { + font-size: 32rpx; + font-weight: bold; + color: #333; + } + + .selected-count { + font-size: 24rpx; + color: #667eea; + background: rgba(102, 126, 234, 0.1); + padding: 8rpx 15rpx; + border-radius: 15rpx; + } + } + + .shape-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20rpx; + margin-bottom: 30rpx; + } + + .popup-actions { + padding-top: 20rpx; + border-top: 2rpx solid #f0f0f0; + + .confirm-btn { + width: 100%; + height: 80rpx; + font-size: 30rpx; + border-radius: 20rpx; + background: linear-gradient(45deg, #667eea, #764ba2); + } + } +} + +/* 弹窗中的形状选项 */ .shape-option { + position: relative; height: 140rpx; border: 3rpx solid #e0e0e0; border-radius: 20rpx; @@ -78,7 +225,7 @@ cursor: pointer; box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.1); - &.active { + &.selected { border-color: #667eea; background: linear-gradient(145deg, #e3f2fd, #bbdefb); transform: translateY(-5rpx); @@ -89,6 +236,30 @@ font-weight: bold; } } + + &.disabled { + opacity: 0.5; + background: #f5f5f5; + border-color: #ddd; + + .shape-name { + color: #999; + } + } + + .selected-mark { + position: absolute; + top: 8rpx; + right: 8rpx; + width: 24rpx; + height: 24rpx; + background: #667eea; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + } } .shape-preview-box { @@ -107,19 +278,22 @@ &.shape-icon-circle { border-radius: 50%; - background: transparent; + background: var(--fill-color, transparent); + border-color: var(--border-color, #666); } &.shape-icon-square { border-radius: 5rpx; - background: transparent; + background: var(--fill-color, transparent); + border-color: var(--border-color, #666); } &.shape-icon-rectangle { width: 60rpx; height: 40rpx; border-radius: 5rpx; - background: transparent; + background: var(--fill-color, transparent); + border-color: var(--border-color, #666); } &.shape-icon-triangle { @@ -128,7 +302,7 @@ border: none; border-left: 25rpx solid transparent; border-right: 25rpx solid transparent; - border-bottom: 45rpx solid #666; + border-bottom: 45rpx solid var(--fill-color, red); background: transparent; } @@ -136,7 +310,8 @@ border-radius: 50%; width: 60rpx; height: 40rpx; - background: transparent; + background: var(--fill-color, transparent); + border-color: var(--border-color, #666); } &.shape-icon-diamond { @@ -146,72 +321,142 @@ border-left: 25rpx solid transparent; border-right: 25rpx solid transparent; border-top: 25rpx solid transparent; - border-bottom: 25rpx solid #666; + border-bottom: 25rpx solid var(--fill-color, #666); background: transparent; transform: rotate(45deg); } + /* 修正星形 */ &.shape-icon-star { position: relative; - width: 0; - height: 0; + width: 80rpx; + height: 80rpx; border: none; - border-left: 25rpx solid transparent; - border-right: 25rpx solid transparent; - border-bottom: 18rpx solid #666; background: transparent; - transform: rotate(35deg); + display: flex; + align-items: center; + justify-content: center; + --star-fill-color: var(--fill-color, red); + --star-border-color: var(--border-color, #333); + --star-border-width: 2rpx; + + /* 边框层 */ &::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); + left: 50%; + top: 50%; + width: 66rpx; + height: 66rpx; + transform: translate(-50%, -50%); + background: transparent; + border: var(--star-border-width, 2rpx) solid var(--star-border-color, #333); + clip-path: polygon(50% 0%, + 61% 35%, + 98% 35%, + 68% 57%, + 79% 91%, + 50% 70%, + 21% 91%, + 32% 57%, + 2% 35%, + 39% 35%); + z-index: 1; + box-sizing: border-box; + } + + &::after { + content: ''; + position: absolute; + left: 50%; + top: 50%; + width: 60rpx; + height: 60rpx; + transform: translate(-50%, -50%); + background: var(--star-fill-color, red); + clip-path: polygon(50% 0%, + 61% 35%, + 98% 35%, + 68% 57%, + 79% 91%, + 50% 70%, + 21% 91%, + 32% 57%, + 2% 35%, + 39% 35%); + z-index: 2; + pointer-events: none; } } + /* 爱心形状 */ &.shape-icon-heart { position: relative; - width: 40rpx; - height: 35rpx; - border: none; - background: #666; - transform: rotate(-45deg); + width: 44rpx; + height: 40rpx; + display: flex; + align-items: center; + justify-content: center; + background: transparent; - &::before, + /* 主体部分(三角形) */ &::after { content: ''; position: absolute; - width: 20rpx; - height: 30rpx; - background: #666; - border-radius: 20rpx 20rpx 0 0; - transform: rotate(-45deg); - transform-origin: 0 100%; + left: 50%; + bottom: 0; + transform: translateX(-50%) rotate(-45deg); + width: 32rpx; + height: 32rpx; + background: var(--fill-color, #f00); + border-left: 2rpx solid var(--border-color, #333); + border-bottom: 2rpx solid var(--border-color, #333); + border-bottom-left-radius: 20rpx; + border-bottom-right-radius: 20rpx; + z-index: 1; + clip-path: polygon(0 0, 100% 0, 50% 100%); } + /* 左半圆 */ &::before { - left: 20rpx; + content: ''; + position: absolute; + left: 4rpx; + top: 0; + width: 24rpx; + height: 24rpx; + background: var(--fill-color, #f00); + border: 2rpx solid var(--border-color, #333); + border-bottom: none; + border-right: none; + border-radius: 50% 50% 0 0; + z-index: 2; } - &::after { - top: -15rpx; - transform: rotate(45deg); - transform-origin: 100% 100%; + /* 右半圆 */ + .heart-right { + position: absolute; + right: 4rpx; + top: 0; + width: 24rpx; + height: 24rpx; + background: var(--fill-color, #f00); + border: 2rpx solid var(--border-color, #333); + border-bottom: none; + border-left: none; + border-radius: 50% 50% 0 0; + z-index: 2; + content: ''; + display: block; } } &.shape-icon-hexagon { width: 40rpx; height: 22rpx; - background: #666; - border: none; + background: var(--fill-color, #666); + border: 2rpx solid var(--border-color, #333); position: relative; &::before, @@ -226,63 +471,90 @@ &::before { bottom: 100%; - border-bottom: 11rpx solid #666; + border-bottom: 11rpx solid var(--fill-color, #666); } &::after { top: 100%; - border-top: 11rpx solid #666; + border-top: 11rpx solid var(--fill-color, #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; + width: 40rpx; + height: 38rpx; background: transparent; + border: none; + display: flex; + align-items: center; + justify-content: center; &::before { content: ''; position: absolute; - left: -18rpx; - top: 30rpx; - width: 36rpx; - height: 15rpx; - background: #666; + left: 0; + top: 0; + width: 40rpx; + height: 38rpx; + background: var(--fill-color, #666); + border: 2rpx solid var(--border-color, #333); + clip-path: polygon(50% 0%, + 100% 38%, + 81% 100%, + 19% 100%, + 0% 38%); + box-sizing: border-box; } } + /* 修正半圆 */ &.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; + overflow: hidden; &::before { content: ''; position: absolute; - left: -25rpx; - top: 40rpx; + left: 0; + top: 0; width: 50rpx; - height: 2rpx; - background: #666; + height: 50rpx; + background: var(--fill-color, #666); + border: 2rpx solid var(--border-color, #333); + border-radius: 50%; + box-sizing: border-box; + } + } + + /* 修正梯形 */ + &.shape-icon-trapezoid { + width: 50rpx; + height: 30rpx; + border: none; + background: transparent; + position: relative; + overflow: visible; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 0; + width: 50rpx; + height: 30rpx; + background: var(--fill-color, #666); + border: 2rpx solid var(--border-color, #333); + clip-path: polygon(15% 100%, + 85% 100%, + 100% 0%, + 0% 0%); + box-sizing: border-box; } } } diff --git a/miniprogram/demoPages/shapePrint/index.wxml b/miniprogram/demoPages/shapePrint/index.wxml index f1e2265..e0e25a4 100644 --- a/miniprogram/demoPages/shapePrint/index.wxml +++ b/miniprogram/demoPages/shapePrint/index.wxml @@ -5,88 +5,65 @@ 为小朋友制作涂色练习页 - + - 选择形状 - - - - - - - {{item.name}} - - - - - - - 选择颜色 - - - {{item.name}} - - - - - - - - + - - 已选择的形状 ({{selectedShapes.length}}) - - - + 已选择的形状 ({{selectedShapes.length}}) + + class="shape-card" + bindtap="openColorPicker" + data-index="{{index}}"> + - - {{item.name}} - {{item.color}} + class="delete-btn" + catchtap="deleteShape" + data-index="{{index}}"> + + + + + + + + + + + {{item.name}} + + + + {{item.colorName}} - 长按删除 - + 打印设置 - 背景颜色: @@ -102,17 +79,10 @@ - - - 功能说明: - • 上半部分:带颜色的形状示例 - • 下半部分:无颜色的练习区域 - • 图片大小:A4 纸张,适合打印 - - - + + + + + + + + + + + +