feat: Add Learn-Number tool for preschool math practice and refactor HTML components
Build and Push Docker Image / build (push) Successful in 2m55s
Build and Push Docker Image / build (push) Successful in 2m55s
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"toolbox/pkg/learnnumber/data"
|
||||
|
||||
"github.com/signintech/gopdf"
|
||||
)
|
||||
|
||||
type CountingRequest struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
IconTypes int `json:"icon_types"`
|
||||
PaperSize string `json:"paper_size"`
|
||||
}
|
||||
|
||||
var reSVG = regexp.MustCompile(`([MLQCZmlqcz])|(-?\d+\.?\d*)`)
|
||||
|
||||
func GenerateCountingPDF(req CountingRequest) ([]byte, error) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
pdf := &gopdf.GoPdf{}
|
||||
rect := gopdf.Rect{W: 595.28, H: 841.89}
|
||||
if req.PaperSize == "Letter" { rect = gopdf.Rect{W: 612, H: 792} }
|
||||
pdf.Start(gopdf.Config{PageSize: rect})
|
||||
pdf.AddPage()
|
||||
|
||||
drawProblem(pdf, req, 0, rect.W, rect.H/2)
|
||||
drawProblem(pdf, req, rect.H/2, rect.W, rect.H/2)
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := pdf.WriteTo(&buf)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
func drawProblem(pdf *gopdf.GoPdf, req CountingRequest, startY, pW, pH float64) {
|
||||
margin := 40.0
|
||||
boxW := (pW - 2*margin) * 0.7
|
||||
boxH := pH - 60.0
|
||||
x, y := margin, startY + 30.0
|
||||
|
||||
// 1. 绘制方框
|
||||
pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(2.0)
|
||||
pdf.RectFromUpperLeft(x, y, boxW, boxH)
|
||||
|
||||
// 2. 分配图标数量
|
||||
numTypes := req.IconTypes; if numTypes < 1 { numTypes = 1 }
|
||||
if numTypes > len(data.CountingIcons) { numTypes = len(data.CountingIcons) }
|
||||
|
||||
total := req.TotalCount; if total < numTypes { total = numTypes }
|
||||
if total > 30 { total = 30 }
|
||||
|
||||
// 随机选出图标种类
|
||||
allIcons := rand.Perm(len(data.CountingIcons))
|
||||
selectedIcons := []data.Icon{}
|
||||
counts := []int{}
|
||||
|
||||
// 先每种分配1个
|
||||
rem := total - numTypes
|
||||
for i := 0; i < numTypes; i++ {
|
||||
selectedIcons = append(selectedIcons, data.CountingIcons[allIcons[i]])
|
||||
counts = append(counts, 1)
|
||||
}
|
||||
// 随机分配剩下的
|
||||
for i := 0; i < rem; i++ {
|
||||
counts[rand.Intn(numTypes)]++
|
||||
}
|
||||
|
||||
// 3. 布局计划 (使用网格,增加 Padding 避开边框)
|
||||
rows, cols := 5, 6
|
||||
cellW, cellH := boxW/float64(cols), boxH/float64(rows)
|
||||
indices := rand.Perm(rows * cols)
|
||||
|
||||
// 严格图标大小:单元格的 70%,确保不溢出单元格边界
|
||||
iconSize := math.Min(cellW, cellH) * 0.7
|
||||
paddingX, paddingY := (cellW - iconSize)/2, (cellH - iconSize)/2
|
||||
|
||||
iconTypeIdx := 0
|
||||
countInCurrentType := 0
|
||||
|
||||
for i := 0; i < total; i++ {
|
||||
cellIdx := indices[i]
|
||||
rIdx, cIdx := cellIdx/cols, cellIdx%cols
|
||||
|
||||
// 基础位置
|
||||
baseX := x + float64(cIdx)*cellW + paddingX
|
||||
baseY := y + float64(rIdx)*cellH + paddingY
|
||||
|
||||
// 渲染图标
|
||||
drawIcon(pdf, selectedIcons[iconTypeIdx], baseX + iconSize/2, baseY + iconSize/2, iconSize)
|
||||
|
||||
countInCurrentType++
|
||||
if countInCurrentType >= counts[iconTypeIdx] {
|
||||
iconTypeIdx++
|
||||
countInCurrentType = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 绘制右侧对照表
|
||||
legendX := x + boxW + 20.0
|
||||
legendStepY := boxH / float64(numTypes + 1)
|
||||
for i := 0; i < numTypes; i++ {
|
||||
lY := y + float64(i+1)*legendStepY
|
||||
drawIcon(pdf, selectedIcons[i], legendX + 20, lY, 30)
|
||||
pdf.SetStrokeColor(150, 150, 150); pdf.SetLineWidth(0.8)
|
||||
pdf.RectFromUpperLeft(legendX + 50, lY - 15, 30, 30)
|
||||
}
|
||||
}
|
||||
|
||||
func drawIcon(pdf *gopdf.GoPdf, icon data.Icon, cX, cY, size float64) {
|
||||
scale := size / 1024.0
|
||||
pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(1.5) // 加粗线条
|
||||
|
||||
ox, oy := cX - size/2, cY - size/2
|
||||
for _, pStr := range icon.Paths {
|
||||
pts := parseSmoothPath(pStr, scale, ox, oy)
|
||||
if len(pts) > 1 {
|
||||
// 如果是闭合路径
|
||||
if strings.Contains(strings.ToUpper(pStr), "Z") {
|
||||
pdf.Polygon(pts, "D")
|
||||
} else {
|
||||
for i := 0; i < len(pts)-1; i++ {
|
||||
pdf.Line(pts[i].X, pts[i].Y, pts[i+1].X, pts[i+1].Y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 支持贝塞尔曲线采样,确保图标圆润
|
||||
func parseSmoothPath(path string, scale, ox, oy float64) []gopdf.Point {
|
||||
var pts []gopdf.Point
|
||||
matches := reSVG.FindAllStringSubmatch(path, -1)
|
||||
var lx, ly float64
|
||||
for i := 0; i < len(matches); {
|
||||
cmd := matches[i][0]
|
||||
if cmd == "M" || cmd == "L" {
|
||||
x, _ := strconv.ParseFloat(matches[i+1][0], 64); y, _ := strconv.ParseFloat(matches[i+2][0], 64)
|
||||
lx, ly = x, y
|
||||
pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale})
|
||||
i += 3
|
||||
} else if cmd == "C" {
|
||||
x1, _ := strconv.ParseFloat(matches[i+1][0], 64); y1, _ := strconv.ParseFloat(matches[i+2][0], 64)
|
||||
x2, _ := strconv.ParseFloat(matches[i+3][0], 64); y2, _ := strconv.ParseFloat(matches[i+4][0], 64)
|
||||
x, _ := strconv.ParseFloat(matches[i+5][0], 64); y, _ := strconv.ParseFloat(matches[i+6][0], 64)
|
||||
for t := 0.25; t <= 1.0; t += 0.25 {
|
||||
tx := math.Pow(1-t, 3)*lx + 3*math.Pow(1-t, 2)*t*x1 + 3*(1-t)*math.Pow(t, 2)*x2 + math.Pow(t, 3)*x
|
||||
ty := math.Pow(1-t, 3)*ly + 3*math.Pow(1-t, 2)*t*y1 + 3*(1-t)*math.Pow(t, 2)*y2 + math.Pow(t, 3)*y
|
||||
pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale})
|
||||
}
|
||||
lx, ly = x, y; i += 7
|
||||
} else if cmd == "Q" {
|
||||
x1, _ := strconv.ParseFloat(matches[i+1][0], 64); y1, _ := strconv.ParseFloat(matches[i+2][0], 64)
|
||||
x, _ := strconv.ParseFloat(matches[i+3][0], 64); y, _ := strconv.ParseFloat(matches[i+4][0], 64)
|
||||
for t := 0.25; t <= 1.0; t += 0.25 {
|
||||
tx := math.Pow(1-t, 2)*lx + 2*(1-t)*t*x1 + math.Pow(t, 2)*x
|
||||
ty := math.Pow(1-t, 2)*ly + 2*(1-t)*t*y1 + math.Pow(t, 2)*y
|
||||
pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale})
|
||||
}
|
||||
lx, ly = x, y; i += 5
|
||||
} else { i++ }
|
||||
}
|
||||
return pts
|
||||
}
|
||||
Reference in New Issue
Block a user