diff --git a/miniprogram/app.json b/miniprogram/app.json index 9ec599c..374ee6a 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -10,7 +10,11 @@ { "root": "mathPages", "name": "mathPages", - "pages": ["numberFind/numberFind", "countMatch/countMatch"], + "pages": [ + "numberFind/numberFind", + "countMatch/countMatch", + "addition/addition" + ], "independent": false } ], diff --git a/miniprogram/config/config.ts b/miniprogram/config/config.ts index 14cca35..34c8487 100644 --- a/miniprogram/config/config.ts +++ b/miniprogram/config/config.ts @@ -1,6 +1,7 @@ export default { printHeader: { - header: 'wechat', - appName: '涂鸦丫' - } -} \ No newline at end of file + // header: 'wechat', + header: 'LogoImage', + appName: '涂鸦丫小程序', + }, +}; diff --git a/miniprogram/constants/mathFunctions.ts b/miniprogram/constants/mathFunctions.ts index f53d453..c6927b4 100644 --- a/miniprogram/constants/mathFunctions.ts +++ b/miniprogram/constants/mathFunctions.ts @@ -35,6 +35,34 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [ desc: '通过连线配对数字和对应的数量图形', icon: '🔗', }, + { + id: 'addition-5', + page: 'addition', + title: '5以内加法', + desc: '练习5以内的加法运算,图形化展示', + icon: '➕', + }, + { + id: 'addition-10', + page: 'addition', + title: '10以内加法', + desc: '练习10以内的加法运算,图形化展示', + icon: '➕', + }, + { + id: 'subtraction-10', + page: 'addition', + title: '10以内减法', + desc: '练习10以内的减法运算,图形化展示', + icon: '➖', + }, + { + id: 'addition-subtraction-10', + page: 'addition', + title: '10以内加减法', + desc: '练习10以内的加法和减法混合运算', + icon: '±', + }, { id: 'missing-number', title: '填上缺少的数字', @@ -59,22 +87,4 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [ desc: '数出物品数量,填写对应的数字', icon: '✏️', }, - { - id: 'addition-5', - title: '5以内加法', - desc: '练习5以内的加法运算,图形化展示', - icon: '➕', - }, - { - id: 'addition-10', - title: '10以内加法', - desc: '练习10以内的加法运算,图形化展示', - icon: '➕', - }, - { - id: 'addition-subtraction-10', - title: '10以内加减法', - desc: '练习10以内的加法和减法混合运算', - icon: '±', - }, ]; diff --git a/miniprogram/mathPages/addition/addition.json b/miniprogram/mathPages/addition/addition.json new file mode 100644 index 0000000..a78fa9d --- /dev/null +++ b/miniprogram/mathPages/addition/addition.json @@ -0,0 +1,12 @@ +{ + "navigationBarTitleText": "加减法计算", + "navigationBarBackgroundColor": "#FFD719", + "homeButton": true, + "backgroundColor": "#F6F6F6", + "enablePullDownRefresh": false, + "usingComponents": { + "toy-button": "../../ui/button/button", + "share-guide-popup": "../../components/share-guide-popup/share-guide-popup", + "van-action-sheet": "../../miniprogram_npm/@vant/weapp/action-sheet/index" + } +} diff --git a/miniprogram/mathPages/addition/addition.less b/miniprogram/mathPages/addition/addition.less new file mode 100644 index 0000000..b628a25 --- /dev/null +++ b/miniprogram/mathPages/addition/addition.less @@ -0,0 +1,101 @@ +page { + background-color: #f6f6f6; +} + +.page-container { + background-color: #f6f6f6; + padding: 0 24rpx; + box-sizing: border-box; + padding-bottom: 160rpx; // 为底部按钮预留空间 + + .empty { + height: 50rpx; + } +} + +.wrapper { + background-color: #ffffff; + border-radius: 20rpx; + padding: 36rpx 24rpx; + box-shadow: 0 6rpx 8rpx rgba(0, 0, 0, 0.15); + margin: 30rpx 0; + display: flex; + flex-direction: column; + + .wrapper-title { + font-size: 32rpx; + color: #141414; + margin-bottom: 36rpx; + font-weight: bold; + text-align: center; + } + + .canvas-wrapper { + display: flex; + justify-content: center; + align-items: center; + min-height: 400rpx; + background: #f8f9fa; + border-radius: 12rpx; + border: 2rpx dashed #dee2e6; + } + + .canvas-content { + max-width: 100%; + border-radius: 8rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); + overflow: hidden; + } + + .random-button-area { + margin-top: 40rpx; + display: flex; + align-items: center; + gap: 20rpx; + + .type-selector { + flex: 1; // 占据剩余空间 + height: 80rpx; + background-color: #fff; + border: 2rpx solid #e5e5e5; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24rpx; + box-sizing: border-box; + + .type-selector-text { + font-size: 28rpx; + color: #333; + } + + .type-selector-arrow { + font-size: 20rpx; + color: #999; + } + } + + .random-button { + flex: 2; + } + } +} + +/* 底部按钮区域 */ +.bottom-btn-box { + position: fixed; + left: 0; + bottom: 0; + width: 100%; + height: 180rpx; + display: flex; + justify-content: space-between; + background-color: #fff; + box-shadow: 0px -3.9rpx 5.2rpx rgba(0, 0, 0, 0.15); + padding: 24rpx; + padding-bottom: env(safe-area-inset-bottom); + box-sizing: border-box; + border-radius: 16rpx 16rpx 0 0; + z-index: 2; +} \ No newline at end of file diff --git a/miniprogram/mathPages/addition/addition.ts b/miniprogram/mathPages/addition/addition.ts new file mode 100644 index 0000000..3aa4a58 --- /dev/null +++ b/miniprogram/mathPages/addition/addition.ts @@ -0,0 +1,298 @@ +import { PAPER_SIZE } from '../../constants/colors'; +import { checkAndSaveImage } from '../../utils/saveImage'; +import { shouldShowShareGuide } from '../../utils/shareGuide'; +import AdditionDraw from '../service/additionDraw'; +import { + MATH_FUNCTION_TYPES, + MathFunctionType, +} from '../../constants/mathFunctions'; + +Page({ + canvas: null as Canvas | null, + ctx: null as RenderingContext | null, + boxHeight: 0, + boxWidth: 0, + drawService: null as AdditionDraw | null, + calculationData: null as { + problems: Array<{ + type: 'addition' | 'subtraction'; + left: number; + right: number; + result: number; + }>; + } | null, + + data: { + pageTitle: '加减法计算', + functionId: '', + hasContent: false, + showShareDialog: false, + showTypeSelector: false, + currentType: 'addition-5', // 'addition-5', 'addition-10', 'subtraction-10', 'addition-subtraction-10' + currentTypeName: '5以内加法', + typeActions: [ + { name: '5以内加法', value: 'addition-5' }, + { name: '10以内加法', value: 'addition-10' }, + { name: '10以内减法', value: 'subtraction-10' }, + { name: '10以内加减法', value: 'addition-subtraction-10' }, + ], + imageType: 'twelve-animals', // 图片类型:twelve-animals 或 fruits + }, + + onLoad(options: { id?: string }) { + const functionId = options.id || 'addition-5'; + this.setData({ functionId }); + + const functionItem = MATH_FUNCTION_TYPES.find( + (item: MathFunctionType) => item.id === functionId, + ); + + const pageTitle = functionItem?.title || '加减法计算'; + this.setData({ pageTitle }); + wx.setNavigationBarTitle({ title: pageTitle }); + + // 根据 functionId 设置默认类型 + if (functionId === 'addition-5') { + this.setData({ + currentType: 'addition-5', + currentTypeName: '5以内加法', + }); + } else if (functionId === 'addition-10') { + this.setData({ + currentType: 'addition-10', + currentTypeName: '10以内加法', + }); + } else if (functionId === 'subtraction-10') { + this.setData({ + currentType: 'subtraction-10', + currentTypeName: '10以内减法', + }); + } else if (functionId === 'addition-subtraction-10') { + this.setData({ + currentType: 'addition-subtraction-10', + currentTypeName: '10以内加减法', + }); + } + }, + + onReady() { + this.initCanvas(); + }, + + /** + * 初始化Canvas + */ + initCanvas() { + const query = wx.createSelectorQuery(); + query + .select('#canvasWrapper') + .boundingClientRect((rect) => { + if (rect) { + const { width, height } = PAPER_SIZE['A4']; + const boxWidth = rect.width; + const boxHeight = boxWidth / (width / height); + + this.boxHeight = boxHeight; + this.boxWidth = boxWidth; + + this.setData({ boxWidth, boxHeight }); + + const canvas = wx + .createSelectorQuery() + .select('#canvasContent'); + canvas.fields({ node: true, size: true }).exec((res) => { + if (res[0]) { + const canvasNode = res[0].node; + const ctx = canvasNode.getContext('2d'); + const dpr = wx.getSystemInfoSync().pixelRatio; + + canvasNode.width = boxWidth * dpr; + canvasNode.height = boxHeight * dpr; + ctx.scale(dpr, dpr); + + this.canvas = canvasNode; + this.ctx = ctx; + + this.drawService = new AdditionDraw( + canvasNode, + ctx, + { + title: this.data.currentTypeName, + subTitle: '通过图形化方式学习加减法运算', + }, + ); + + // 初始随机生成 + this.onRandom(); + } + }); + } + }) + .exec(); + }, + + /** + * 绘制Canvas内容 + */ + async drawCanvas() { + if (!this.ctx || !this.drawService || !this.calculationData) { + return; + } + + try { + // 更新 Header 的 Title + if (this.drawService) { + this.drawService.options.title = this.data.currentTypeName; + } + + await this.drawService.draw( + this.calculationData, + this.data.currentType, + ); + this.setData({ hasContent: true }); + } catch (error) { + console.error('绘制失败:', error); + this.setData({ hasContent: false }); + } + }, + + /** + * 随机生成 + */ + onRandom() { + const problems: Array<{ + type: 'addition' | 'subtraction'; + left: number; + right: number; + result: number; + }> = []; + + const type = this.data.currentType; + + // 生成6道题目 + for (let i = 0; i < 5; i++) { + if (type === 'addition-5') { + // 5以内加法:和 ≤ 5 + // left >= 1, right >= 1, left + right <= 5 + const maxSum = 5; + const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 4 + const maxRight = maxSum - left; // 确保 left + right <= 5 + const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight + const result = left + right; + problems.push({ type: 'addition', left, right, result }); + } else if (type === 'addition-10') { + // 10以内加法:和 ≤ 10 + // left >= 1, right >= 1, left + right <= 10 + const maxSum = 10; + const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9 + const maxRight = maxSum - left; // 确保 left + right <= 10 + const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight + const result = left + right; + problems.push({ type: 'addition', left, right, result }); + } else if (type === 'subtraction-10') { + // 10以内减法:被减数 ≤ 10 + // left <= 10, left - right = result, result >= 1 + const maxLeft = 10; + const left = Math.floor(Math.random() * maxLeft) + 1; // 1 到 10 + const maxRight = left - 1; // 确保 result >= 1 + const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight + const result = left - right; + problems.push({ type: 'subtraction', left, right, result }); + } else if (type === 'addition-subtraction-10') { + // 加减法混合 + if (Math.random() < 0.5) { + // 加法:和 ≤ 10 + const maxSum = 10; + const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9 + const maxRight = maxSum - left; // 确保 left + right <= 10 + const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight + const result = left + right; + problems.push({ type: 'addition', left, right, result }); + } else { + // 减法:被减数 ≤ 10 + const maxLeft = 10; + const left = Math.floor(Math.random() * maxLeft) + 1; // 1 到 10 + const maxRight = left - 1; // 确保 result >= 1 + const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight + const result = left - right; + problems.push({ type: 'subtraction', left, right, result }); + } + } + } + + this.calculationData = { problems }; + this.drawCanvas(); + }, + + /** + * 导出打印 + */ + exportToPrint() { + if (!this.canvas || !this.data.hasContent) { + return; + } + + if (shouldShowShareGuide()) { + this.setData({ showShareDialog: true }); + return; + } + + checkAndSaveImage(this.canvas); + }, + + /** + * 分享小程序 + */ + onShareAppMessage() { + return { + title: '涂鸦丫-数学学习涂鸦卡', + path: `/mathPages/addition/addition?id=${this.data.functionId}`, + imageUrl: + 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png', + }; + }, + + onShareTimeline() { + return { + title: '涂鸦丫-数学学习涂鸦卡', + query: `id=${this.data.functionId}`, + imageUrl: + 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png', + }; + }, + + /** 关闭分享引导弹窗 */ + onCloseShareDialog() { + this.setData({ showShareDialog: false }); + }, + + /** 分享成功回调 */ + onShareSuccess() { + this.setData({ showShareDialog: false }); + if (this.canvas) { + checkAndSaveImage(this.canvas); + } + }, + + /** 显示类型选择器 */ + onShowTypeSelector() { + this.setData({ showTypeSelector: true }); + }, + + /** 关闭类型选择器 */ + onCloseTypeSelector() { + this.setData({ showTypeSelector: false }); + }, + + /** 选择类型 */ + onSelectType(event: any) { + const { name, value } = event.detail; + this.setData({ + currentType: value, + currentTypeName: name, + showTypeSelector: false, + }); + // 重新生成数据 + this.onRandom(); + }, +}); diff --git a/miniprogram/mathPages/addition/addition.wxml b/miniprogram/mathPages/addition/addition.wxml new file mode 100644 index 0000000..604a209 --- /dev/null +++ b/miniprogram/mathPages/addition/addition.wxml @@ -0,0 +1,69 @@ + + + 预览打印效果 + + + + + + + + + + {{currentTypeName}} + + + + 随机生成 + + + + + + + + + 分享 + + + 下载打印 + + + diff --git a/miniprogram/mathPages/assets/fruits/1.png b/miniprogram/mathPages/assets/fruits/1.png index 4e08518..fdd46cc 100644 Binary files a/miniprogram/mathPages/assets/fruits/1.png and b/miniprogram/mathPages/assets/fruits/1.png differ diff --git a/miniprogram/mathPages/assets/fruits/10.png b/miniprogram/mathPages/assets/fruits/10.png index 2f58b1f..c899e0f 100644 Binary files a/miniprogram/mathPages/assets/fruits/10.png and b/miniprogram/mathPages/assets/fruits/10.png differ diff --git a/miniprogram/mathPages/assets/fruits/11.png b/miniprogram/mathPages/assets/fruits/11.png index f8fe214..5fab469 100644 Binary files a/miniprogram/mathPages/assets/fruits/11.png and b/miniprogram/mathPages/assets/fruits/11.png differ diff --git a/miniprogram/mathPages/assets/fruits/12.png b/miniprogram/mathPages/assets/fruits/12.png index 5d99cfe..40e7828 100644 Binary files a/miniprogram/mathPages/assets/fruits/12.png and b/miniprogram/mathPages/assets/fruits/12.png differ diff --git a/miniprogram/mathPages/assets/fruits/13.png b/miniprogram/mathPages/assets/fruits/13.png index 1a31aed..b107bf0 100644 Binary files a/miniprogram/mathPages/assets/fruits/13.png and b/miniprogram/mathPages/assets/fruits/13.png differ diff --git a/miniprogram/mathPages/assets/fruits/14.png b/miniprogram/mathPages/assets/fruits/14.png index 873e624..a248eca 100644 Binary files a/miniprogram/mathPages/assets/fruits/14.png and b/miniprogram/mathPages/assets/fruits/14.png differ diff --git a/miniprogram/mathPages/assets/fruits/15.png b/miniprogram/mathPages/assets/fruits/15.png index 34fc0c3..7453066 100644 Binary files a/miniprogram/mathPages/assets/fruits/15.png and b/miniprogram/mathPages/assets/fruits/15.png differ diff --git a/miniprogram/mathPages/assets/fruits/16.png b/miniprogram/mathPages/assets/fruits/16.png index d48f3d9..3dd0a10 100644 Binary files a/miniprogram/mathPages/assets/fruits/16.png and b/miniprogram/mathPages/assets/fruits/16.png differ diff --git a/miniprogram/mathPages/assets/fruits/17.png b/miniprogram/mathPages/assets/fruits/17.png index dfb8798..6a9ea31 100644 Binary files a/miniprogram/mathPages/assets/fruits/17.png and b/miniprogram/mathPages/assets/fruits/17.png differ diff --git a/miniprogram/mathPages/assets/fruits/18.png b/miniprogram/mathPages/assets/fruits/18.png index c05ca6d..8266b0a 100644 Binary files a/miniprogram/mathPages/assets/fruits/18.png and b/miniprogram/mathPages/assets/fruits/18.png differ diff --git a/miniprogram/mathPages/assets/fruits/19.png b/miniprogram/mathPages/assets/fruits/19.png index a69e258..760c1c0 100644 Binary files a/miniprogram/mathPages/assets/fruits/19.png and b/miniprogram/mathPages/assets/fruits/19.png differ diff --git a/miniprogram/mathPages/assets/fruits/2.png b/miniprogram/mathPages/assets/fruits/2.png index 445cff5..d9ff943 100644 Binary files a/miniprogram/mathPages/assets/fruits/2.png and b/miniprogram/mathPages/assets/fruits/2.png differ diff --git a/miniprogram/mathPages/assets/fruits/20.png b/miniprogram/mathPages/assets/fruits/20.png index 492b3ac..23ea1b7 100644 Binary files a/miniprogram/mathPages/assets/fruits/20.png and b/miniprogram/mathPages/assets/fruits/20.png differ diff --git a/miniprogram/mathPages/assets/fruits/21.png b/miniprogram/mathPages/assets/fruits/21.png index fcca685..c0d80df 100644 Binary files a/miniprogram/mathPages/assets/fruits/21.png and b/miniprogram/mathPages/assets/fruits/21.png differ diff --git a/miniprogram/mathPages/assets/fruits/22.png b/miniprogram/mathPages/assets/fruits/22.png index 917ceb8..5e5e5a5 100644 Binary files a/miniprogram/mathPages/assets/fruits/22.png and b/miniprogram/mathPages/assets/fruits/22.png differ diff --git a/miniprogram/mathPages/assets/fruits/3.png b/miniprogram/mathPages/assets/fruits/3.png index 141d3cd..d45404a 100644 Binary files a/miniprogram/mathPages/assets/fruits/3.png and b/miniprogram/mathPages/assets/fruits/3.png differ diff --git a/miniprogram/mathPages/assets/fruits/4.png b/miniprogram/mathPages/assets/fruits/4.png index 8ced475..dc19393 100644 Binary files a/miniprogram/mathPages/assets/fruits/4.png and b/miniprogram/mathPages/assets/fruits/4.png differ diff --git a/miniprogram/mathPages/assets/fruits/5.png b/miniprogram/mathPages/assets/fruits/5.png index 885011b..e6f2301 100644 Binary files a/miniprogram/mathPages/assets/fruits/5.png and b/miniprogram/mathPages/assets/fruits/5.png differ diff --git a/miniprogram/mathPages/assets/fruits/6.png b/miniprogram/mathPages/assets/fruits/6.png index 761c03d..bb5d18e 100644 Binary files a/miniprogram/mathPages/assets/fruits/6.png and b/miniprogram/mathPages/assets/fruits/6.png differ diff --git a/miniprogram/mathPages/assets/fruits/7.png b/miniprogram/mathPages/assets/fruits/7.png index cba3cab..d4a6cbd 100644 Binary files a/miniprogram/mathPages/assets/fruits/7.png and b/miniprogram/mathPages/assets/fruits/7.png differ diff --git a/miniprogram/mathPages/assets/fruits/8.png b/miniprogram/mathPages/assets/fruits/8.png index 051d4cb..e5c8272 100644 Binary files a/miniprogram/mathPages/assets/fruits/8.png and b/miniprogram/mathPages/assets/fruits/8.png differ diff --git a/miniprogram/mathPages/assets/fruits/9.png b/miniprogram/mathPages/assets/fruits/9.png index 9328d40..ec65c31 100644 Binary files a/miniprogram/mathPages/assets/fruits/9.png and b/miniprogram/mathPages/assets/fruits/9.png differ diff --git a/miniprogram/mathPages/service/additionContentDraw.ts b/miniprogram/mathPages/service/additionContentDraw.ts new file mode 100644 index 0000000..a8464ef --- /dev/null +++ b/miniprogram/mathPages/service/additionContentDraw.ts @@ -0,0 +1,576 @@ +import { getImage } from '../../utils/index'; + +interface DrawAdditionContentParams { + canvas: WechatMiniprogram.Canvas; + ctx: RenderingContext; + problems: Array<{ + type: 'addition' | 'subtraction'; + left: number; + right: number; + result: number; + }>; + canvasWidth: number; + startY: number; + imageType?: string; // 'twelve-animals' 或 'fruits' +} + +/** + * 绘制圆角矩形(虚线或实线边框) + */ +function drawRoundedRect( + ctx: RenderingContext, + x: number, + y: number, + width: number, + height: number, + radius: number, + isDashed: boolean = true, +) { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + + if (isDashed) { + ctx.setLineDash([2, 2]); // 虚线 + } else { + ctx.setLineDash([]); // 实线 + } + ctx.stroke(); + ctx.setLineDash([]); // 重置为实线 +} + +/** + * 绘制实线圆角方框(用于填写答案) + */ +function drawAnswerBox( + ctx: RenderingContext, + x: number, + y: number, + width: number, + height: number, +) { + const radius = 6; // 圆角大小 + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1; + ctx.setLineDash([]); // 实线 + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + ctx.stroke(); +} + +/** + * 绘制右侧答案框 + */ +function drawAnswerBoxArea( + ctx: RenderingContext, + params: { + rightBoxX: number; + boxY: number; + rightBoxWidth: number; + borderRadius: number; + boxHeight: number; + padding: number; + problemType: 'addition' | 'subtraction'; + }, +) { + const { + rightBoxX, + boxY, + rightBoxWidth, + borderRadius, + boxHeight, + padding, + problemType, + } = params; + + const plusSignSize = 18; // 加号大小(缩小) + const minusSignSize = 18; // 减号大小(缩小) + const equalsSignSize = 18; // 等号大小(缩小) + const spacing = 8; // 加号左右间距(方便修改和调试) + const answerBoxWidth = 34; // 答案框宽度 + const answerBoxHeight = 34; // 答案框高度 + + // 绘制右侧答案框边框 + ctx.strokeStyle = '#999'; + ctx.lineWidth = 1; + drawRoundedRect( + ctx, + rightBoxX, + boxY, + rightBoxWidth, + boxHeight, + borderRadius, + true, + ); + + const rightBoxCenterY = boxY + boxHeight / 2; + const rightBoxAvailableWidth = rightBoxWidth - padding * 2; + const answerStartY = rightBoxCenterY - answerBoxHeight / 2; + + // 根据问题类型确定运算符号大小 + const operatorSignSize = + problemType === 'addition' ? plusSignSize : minusSignSize; + + // 计算总宽度并水平居中(加法和减法逻辑相同) + const totalAnswerWidth = + answerBoxWidth + + spacing + + operatorSignSize + + spacing + + answerBoxWidth + + spacing + + equalsSignSize + + spacing + + answerBoxWidth; + const answerStartX = + rightBoxX + padding + (rightBoxAvailableWidth - totalAnswerWidth) / 2; + + // 计算各个元素的位置 + const box1X = answerStartX; + const operatorX = box1X + answerBoxWidth + spacing; + const box2X = operatorX + plusSignSize + spacing; + const equalsX = box2X + answerBoxWidth + spacing; + const box3X = equalsX + equalsSignSize + spacing; + + // 绘制第一个方框(圆角) + drawAnswerBox(ctx, box1X, answerStartY, answerBoxWidth, answerBoxHeight); + + // 绘制运算符号(加号或减号) + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.beginPath(); + if (problemType === 'addition') { + // 加号:横线和竖线 + ctx.moveTo(operatorX, rightBoxCenterY); + ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY); + ctx.moveTo( + operatorX + operatorSignSize / 2, + rightBoxCenterY - operatorSignSize / 2, + ); + ctx.lineTo( + operatorX + operatorSignSize / 2, + rightBoxCenterY + operatorSignSize / 2, + ); + } else { + // 减号:只有横线 + ctx.moveTo(operatorX, rightBoxCenterY); + ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY); + } + ctx.stroke(); + + // 绘制第二个方框(圆角) + drawAnswerBox(ctx, box2X, answerStartY, answerBoxWidth, answerBoxHeight); + + // 绘制等号(缩短) + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(equalsX, rightBoxCenterY - 3); + ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY - 3); + ctx.moveTo(equalsX, rightBoxCenterY + 3); + ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY + 3); + ctx.stroke(); + + // 绘制第三个方框(答案,圆角) + drawAnswerBox(ctx, box3X, answerStartY, answerBoxWidth, answerBoxHeight); +} + +// 根据图片类型设置图片大小 +function getImageSize(imageType: 'twelve-animals' | 'fruits'): number { + return imageType === 'twelve-animals' ? 40 : 36; +} + +/** + * 绘制加减法计算内容区域 + * 左侧框:展示图片(加法:加号左右展示图片;减法:总数图片,后几个加划线) + * 右侧框:填写运算方式(实线方框 + 实线方框 = 实线方框 或 实线方框 - 实线方框 = 实线方框) + */ +export async function drawAdditionContent({ + canvas, + ctx, + problems, + canvasWidth, + startY, + imageType: _imageType = '', // 不再使用,每个运算会随机选择图片类型 +}: DrawAdditionContentParams): Promise { + // 根据图片类型确定图片目录和最大索引 + const imageConfig = { + 'twelve-animals': { + folder: 'twelve-animals', + maxIndex: 12, + }, + fruits: { + folder: 'fruits', + maxIndex: 22, + }, + }; + + const leftMargin = 24; + const rightMargin = 24; + const itemSpacing = 140; + // 调整左右框宽度比例:6:4 (约420:280) + const leftBoxWidth = 334; // 左侧图片框固定宽度 + const rightBoxWidth = 196; // 右侧答案框固定宽度 + const boxHeight = 110; + const borderRadius = 12; + + const imageSpacing = 0; // 图片之间的间距(水平方向) + const rowSpacing = 4; // 第一行和第二行之间的间距(垂直方向) + const padding = 15; // 框内边距 + const startYPos = startY + 20; + const plusSignSize = 20; // 加号大小(缩小) + const plusSignSpacing = 15; // 加号左右间距(方便修改和调试) + + // 计算左侧和右侧的起始X位置 + const leftBoxX = leftMargin; + const rightBoxX = canvasWidth - rightMargin - rightBoxWidth; + + // 为每个题目随机选择图片类型和图片索引 + const problemImageConfigs: Array<{ + type: 'twelve-animals' | 'fruits'; + imageIndex: number; + }> = []; + + for (let i = 0; i < problems.length; i++) { + // 随机选择图片类型 + const imageTypes: ('twelve-animals' | 'fruits')[] = [ + 'twelve-animals', + 'fruits', + ]; + const randomType = + imageTypes[Math.floor(Math.random() * imageTypes.length)]; + const config = imageConfig[randomType]; + + // 为当前题目生成唯一的图片索引 + const availableImageIndices = Array.from( + { length: config.maxIndex }, + (_, i) => i + 1, + ); + const randomIndex = Math.floor( + Math.random() * availableImageIndices.length, + ); + const imageIndex = availableImageIndices[randomIndex]; + + problemImageConfigs.push({ + type: randomType, + imageIndex, + }); + } + + // 绘制每一行的内容 + for (let i = 0; i < problems.length; i++) { + const problem = problems[i]; + const boxY = startYPos + i * itemSpacing; + const imageConfigItem = problemImageConfigs[i]; + const config = imageConfig[imageConfigItem.type]; + + // 根据图片类型获取图片大小 + const imageSize = getImageSize(imageConfigItem.type); + + // 加载图片 + let image: any = null; + try { + const imagePath = `/mathPages/assets/${config.folder}/${imageConfigItem.imageIndex}.png`; + image = await getImage(canvas, imagePath); + } catch (error) { + console.error('图片加载失败:', error); + } + + // ========== 绘制左侧图片框 ========== + ctx.strokeStyle = '#999'; + ctx.lineWidth = 1; + drawRoundedRect( + ctx, + leftBoxX, + boxY, + leftBoxWidth, + boxHeight, + borderRadius, + true, + ); + + const leftBoxCenterY = boxY + boxHeight / 2; + const leftBoxContentX = leftBoxX + padding; + + if (problem.type === 'addition') { + // 加法:左侧图片 + 加号 + 右侧图片 + const leftImageCount = problem.left; + const rightImageCount = problem.right; + + /** + * 根据数量计算每行最多展示的图片数 + */ + const getMaxImagesPerRow = (count: number): number => { + if (count <= 6) return 3; + if (count <= 8) return 4; + return 5; // 8-10 + }; + + const leftMaxPerRow = getMaxImagesPerRow(leftImageCount); + const rightMaxPerRow = getMaxImagesPerRow(rightImageCount); + const leftRows = Math.min( + 2, + Math.ceil(leftImageCount / leftMaxPerRow), + ); + const rightRows = Math.min( + 2, + Math.ceil(rightImageCount / rightMaxPerRow), + ); + + // 计算左侧图片区域的宽度和高度 + // 需要根据实际行数计算每行的最大宽度 + const leftFirstRowCount = Math.min(leftImageCount, leftMaxPerRow); + const leftSecondRowCount = Math.max( + 0, + Math.min(leftMaxPerRow, leftImageCount - leftMaxPerRow), + ); + const leftImagesWidth = + Math.max( + leftFirstRowCount * imageSize + + (leftFirstRowCount - 1) * imageSpacing, + leftSecondRowCount * imageSize + + (leftSecondRowCount - 1) * imageSpacing, + ) || imageSize; // 至少为 imageSize + + // 计算右侧图片区域的宽度和高度 + const rightFirstRowCount = Math.min( + rightImageCount, + rightMaxPerRow, + ); + const rightSecondRowCount = Math.max( + 0, + Math.min(rightMaxPerRow, rightImageCount - rightMaxPerRow), + ); + const rightImagesWidth = + Math.max( + rightFirstRowCount * imageSize + + (rightFirstRowCount - 1) * imageSpacing, + rightSecondRowCount * imageSize + + (rightSecondRowCount - 1) * imageSpacing, + ) || imageSize; // 至少为 imageSize + + // 获取图片实际高度(用于垂直居中计算) + let imageActualHeight = imageSize; + if (image) { + // @ts-ignore + imageActualHeight = (image.height / image.width) * imageSize; + } + + // 计算左侧图片区域的实际高度(考虑图片的实际高度和行间距) + const leftImagesHeight = + leftRows * imageActualHeight + (leftRows - 1) * rowSpacing; + // 计算右侧图片区域的实际高度 + const rightImagesHeight = + rightRows * imageActualHeight + (rightRows - 1) * rowSpacing; + + // 计算加号位置(居中,包含左右间距) + const totalContentWidth = + leftImagesWidth + + plusSignSpacing + + plusSignSize + + plusSignSpacing + + rightImagesWidth; + const availableWidth = leftBoxWidth - padding * 2; + const contentStartX = + leftBoxContentX + (availableWidth - totalContentWidth) / 2; + + const leftImagesStartX = contentStartX; + // 垂直居中:使用图片实际高度计算 + const leftImagesStartY = leftBoxCenterY - leftImagesHeight / 2; + + // 绘制左侧图片(最多两行) + for (let j = 0; j < Math.min(leftImageCount, 10); j++) { + const row = Math.floor(j / leftMaxPerRow); + const col = j % leftMaxPerRow; + + // 如果超过两行,停止绘制 + if (row >= 2) break; + + const imagesInThisRow = Math.min( + leftMaxPerRow, + leftImageCount - row * leftMaxPerRow, + ); + const rowWidth = + imagesInThisRow * imageSize + + (imagesInThisRow - 1) * imageSpacing; + const rowStartX = + leftImagesStartX + (leftImagesWidth - rowWidth) / 2; + + const imageX = rowStartX + col * (imageSize + imageSpacing); + // 使用图片实际高度和行间距计算垂直位置 + const imageY = + leftImagesStartY + row * (imageActualHeight + rowSpacing); + + if (image) { + // @ts-ignore + const scaledHeight = + (image.height / image.width) * imageSize; + ctx.drawImage( + image, + imageX, + imageY, + imageSize, + scaledHeight, + ); + } + } + + // 绘制加号(左右有间距) + const plusX = + leftImagesStartX + + leftImagesWidth + + plusSignSpacing + + plusSignSize / 2; + const plusY = leftBoxCenterY; + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.beginPath(); + // 横线 + ctx.moveTo(plusX - plusSignSize / 2, plusY); + ctx.lineTo(plusX + plusSignSize / 2, plusY); + // 竖线 + ctx.moveTo(plusX, plusY - plusSignSize / 2); + ctx.lineTo(plusX, plusY + plusSignSize / 2); + ctx.stroke(); + + const rightImagesStartX = + plusX + plusSignSize / 2 + plusSignSpacing; + // 垂直居中:使用图片实际高度计算 + const rightImagesStartY = leftBoxCenterY - rightImagesHeight / 2; + + // 绘制右侧图片(最多两行) + for (let j = 0; j < Math.min(rightImageCount, 10); j++) { + const row = Math.floor(j / rightMaxPerRow); + const col = j % rightMaxPerRow; + + // 如果超过两行,停止绘制 + if (row >= 2) break; + + const imagesInThisRow = Math.min( + rightMaxPerRow, + rightImageCount - row * rightMaxPerRow, + ); + const rowWidth = + imagesInThisRow * imageSize + + (imagesInThisRow - 1) * imageSpacing; + const rowStartX = + rightImagesStartX + (rightImagesWidth - rowWidth) / 2; + + const imageX = rowStartX + col * (imageSize + imageSpacing); + // 使用图片实际高度和行间距计算垂直位置 + const imageY = + rightImagesStartY + row * (imageActualHeight + rowSpacing); + + if (image) { + // @ts-ignore + const scaledHeight = + (image.height / image.width) * imageSize; + ctx.drawImage( + image, + imageX, + imageY, + imageSize, + scaledHeight, + ); + } + } + } else { + // 减法:展示总数量的图片,后几个加划线 + const totalCount = problem.left; // 总数 + const subtractCount = problem.right; // 要减去的数量 + + // 获取图片实际高度(用于垂直居中计算) + let imageActualHeight = imageSize; + if (image) { + // @ts-ignore + imageActualHeight = (image.height / image.width) * imageSize; + } + + // 计算图片布局 + const imagesPerRow = 5; + const totalRows = Math.ceil(totalCount / imagesPerRow); + // 使用图片实际高度和行间距计算总高度 + const totalHeight = + totalRows * imageActualHeight + (totalRows - 1) * rowSpacing; + const imagesStartY = leftBoxCenterY - totalHeight / 2; + + // 绘制所有图片 + for (let j = 0; j < totalCount; j++) { + const row = Math.floor(j / imagesPerRow); + const col = j % imagesPerRow; + const imagesInThisRow = Math.min( + imagesPerRow, + totalCount - row * imagesPerRow, + ); + const rowWidth = + imagesInThisRow * imageSize + + (imagesInThisRow - 1) * imageSpacing; + const rowStartX = + leftBoxContentX + + (leftBoxWidth - padding * 2 - rowWidth) / 2; + + const imageX = rowStartX + col * (imageSize + imageSpacing); + // 使用图片实际高度和行间距计算垂直位置 + const imageY = + imagesStartY + row * (imageActualHeight + rowSpacing); + + if (image) { + // @ts-ignore + const scaledHeight = + (image.height / image.width) * imageSize; + ctx.drawImage( + image, + imageX, + imageY, + imageSize, + scaledHeight, + ); + + // 如果是后几个图片,绘制虚线删除线(表示减去) + if (j >= totalCount - subtractCount) { + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.setLineDash([4, 3]); // 设置虚线样式 + ctx.beginPath(); + // 绘制第一条虚线对角线(从左上到右下) + ctx.moveTo(imageX, imageY); + ctx.lineTo(imageX + imageSize, imageY + scaledHeight); + // // 绘制第二条虚线对角线(从右上到左下) + // ctx.moveTo(imageX + imageSize, imageY); + // ctx.lineTo(imageX, imageY + scaledHeight); + ctx.stroke(); + ctx.setLineDash([]); // 重置为实线,防止影响后续绘制 + } + } + } + } + + // ========== 绘制右侧答案框 ========== + drawAnswerBoxArea(ctx, { + rightBoxX, + boxY, + rightBoxWidth, + borderRadius, + boxHeight, + padding, + problemType: problem.type, + }); + } +} diff --git a/miniprogram/mathPages/service/additionDraw.ts b/miniprogram/mathPages/service/additionDraw.ts new file mode 100644 index 0000000..a362274 --- /dev/null +++ b/miniprogram/mathPages/service/additionDraw.ts @@ -0,0 +1,67 @@ +import { BaseMathDrawService } from './baseMathDraw'; +import { drawAdditionContent } from './additionContentDraw'; + +/** + * 加减法计算绘制服务 + * 组合使用基础绘制服务和内容区域绘制服务 + */ +class AdditionDraw extends BaseMathDrawService { + calculationData: { + problems: Array<{ + type: 'addition' | 'subtraction'; + left: number; + right: number; + result: number; + }>; + } | null; + + constructor( + canvas: Canvas, + ctx: RenderingContext, + options?: Record, + ) { + super(canvas, ctx, options); + this.calculationData = null; + } + + async draw( + calculationData: { + problems: Array<{ + type: 'addition' | 'subtraction'; + left: number; + right: number; + result: number; + }>; + }, + _calculationType: string = 'addition-5', // 保留参数以保持接口兼容性 + _imageType?: string, // 可选,不再使用,每个运算会随机选择图片类型 + ) { + if (!calculationData || !calculationData.problems) { + return; + } + + this.setPrintConfig(); + this.calculationData = calculationData; + this.clear(); + this.setPaper(); + + // 绘制Header + if (this.headerType !== 'minimal') { + await this.drawHeader(); + } else { + this.drawMiniHeader(); + } + + // 绘制内容区域(加减法计算) + this.drawDivider(); + await drawAdditionContent({ + canvas: this.canvas, + ctx: this.ctx, + problems: this.calculationData.problems, + canvasWidth: this.canvasWidth, + startY: this.currentY, + }); + } +} + +export default AdditionDraw; diff --git a/project.private.config.json b/project.private.config.json index c6f27d4..3378cfc 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -23,12 +23,19 @@ "condition": { "miniprogram": { "list": [ + { + "name": "mathPages/addition/addition", + "pathName": "mathPages/addition/addition", + "query": "id=addition-5", + "scene": null, + "launchMode": "default" + }, { "name": "mathPages/countMatch/countMatch", "pathName": "mathPages/countMatch/countMatch", "query": "id=number-coloring", - "scene": null, - "launchMode": "default" + "launchMode": "default", + "scene": null }, { "name": "pages/mathIndex/mathIndex",