package util import ( "bytes" "fmt" "image" "image/color" "image/draw" "image/gif" "math" "math/rand" "time" "github.com/openimsdk/tools/errs" ) // GenerateCaptchaImageFromCode 根据给定的验证码生成动态GIF图片 // 返回GIF图片的字节数组 func GenerateCaptchaImageFromCode(code string) (imgBytes []byte, err error) { // 图片尺寸 - 宽度280px,高度50px width := 280 height := 50 // GIF动画参数 frameCount := 12 // 12帧动画,更流畅 delay := 8 // 每帧延迟8(单位:1/100秒) // 创建统一的调色板(所有帧共享) sharedPalette := createSharedPalette() // 创建GIF g := &gif.GIF{ Image: []*image.Paletted{}, Delay: []int{}, } // 使用固定种子保证数字位置基本一致 baseSeed := time.Now().UnixNano() // 预生成每帧的基础数据(干扰线、圆圈等的位置) noiseData := generateNoiseData(baseSeed, frameCount, width, height) // 生成每一帧 for i := 0; i < frameCount; i++ { frame := image.NewRGBA(image.Rect(0, 0, width, height)) // 设置背景色(浅灰色,稍微模糊) bgColor := color.RGBA{235, 235, 235, 255} draw.Draw(frame, frame.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src) // 使用随机数生成器(每帧略有不同) rng := rand.New(rand.NewSource(baseSeed + int64(i*1000))) // 绘制丰富的背景 - 添加渐变背景 drawGradientBackground(frame, rng, width, height) // 绘制移动的背景圆圈(类似bubbles效果) drawAnimatedCircles(frame, noiseData, i, width, height) // 绘制额外的背景形状(矩形、椭圆等) drawAdditionalShapes(frame, rng, width, height) // 添加动态干扰线(位置会变化) drawAnimatedNoiseLines(frame, noiseData, i, width, height) // 决定是否显示数字(每隔几帧就隐藏数字,造成闪烁效果) // 例如:0,1,2显示数字,3,4隐藏,5,6显示,7,8隐藏,9,10显示,11隐藏 showNumbers := (i % 3) != 2 // 每3帧中有2帧显示数字,1帧隐藏 if showNumbers { // 绘制验证码文字(位置会抖动) drawNumbersWithWiggle(frame, code, rng, width, height, i, frameCount) } // 添加动态干扰点(位置会变化) drawAnimatedNoiseDots(frame, noiseData, i, width, height) // 添加模糊噪点 - 增加噪点密度 addBlurNoise(frame, rng) // 添加额外的随机干扰线 drawRandomNoiseLines(frame, rng, width, height) // 转换为调色板图像(GIF需要) // 使用统一的调色板 paletted := convertToPalettedWithPalette(frame, sharedPalette) if paletted == nil { return nil, errs.New(fmt.Sprintf("failed to convert frame %d to paletted", i+1)) } g.Image = append(g.Image, paletted) g.Delay = append(g.Delay, delay) } // 编码为GIF var buf bytes.Buffer err = gif.EncodeAll(&buf, g) if err != nil { return nil, errs.WrapMsg(err, "encode gif failed") } // 验证数据不为空 if buf.Len() == 0 { return nil, errs.New("generated gif data is empty") } return buf.Bytes(), nil } // NoiseData 存储每帧的干扰数据 type NoiseData struct { lines [][]LineData circles [][]CircleData dots [][]DotData } type LineData struct { x1, y1, x2, y2 int color color.RGBA thickness int } type CircleData struct { x, y, radius int color color.RGBA } type DotData struct { x, y int color color.RGBA size int } // generateNoiseData 预生成所有帧的干扰数据 func generateNoiseData(baseSeed int64, frameCount, width, height int) *NoiseData { rng := rand.New(rand.NewSource(baseSeed)) data := &NoiseData{ lines: make([][]LineData, frameCount), circles: make([][]CircleData, frameCount), dots: make([][]DotData, frameCount), } // 生成干扰线数据(每帧位置略有变化)- 增加干扰线数量 lineCount := 20 // 从10增加到20 for frame := 0; frame < frameCount; frame++ { lines := make([]LineData, lineCount) for i := 0; i < lineCount; i++ { // 基础位置 baseX1 := rng.Intn(width) baseY1 := rng.Intn(height) baseX2 := rng.Intn(width) baseY2 := rng.Intn(height) // 每帧添加偏移,形成移动效果 offsetX := int(float64(frame) * 2.5 * float64(rng.Float64()-0.5)) offsetY := int(float64(frame) * 2.5 * float64(rng.Float64()-0.5)) lines[i] = LineData{ x1: clampCoord(baseX1+offsetX, width), y1: clampCoord(baseY1+offsetY, height), x2: clampCoord(baseX2+offsetX, width), y2: clampCoord(baseY2+offsetY, height), color: color.RGBA{ uint8(150 + rng.Intn(80)), // 更宽的颜色范围 uint8(150 + rng.Intn(80)), uint8(150 + rng.Intn(80)), 200 + uint8(rng.Intn(55)), // 半透明 }, thickness: 1 + rng.Intn(3), // 增加线条粗细变化 } } data.lines[frame] = lines } // 生成背景圆圈数据(移动的bubbles)- 增加圆圈数量和大小变化 circleCount := 15 // 从8增加到15 for frame := 0; frame < frameCount; frame++ { circles := make([]CircleData, circleCount) for i := 0; i < circleCount; i++ { // 基础位置 baseX := rng.Intn(width*2) - width/2 baseY := rng.Intn(height*2) - height/2 // 每帧移动 moveX := int(float64(frame) * 1.5 * float64(rng.Float64()-0.5)) moveY := int(float64(frame) * 1.5 * float64(rng.Float64()-0.5)) circles[i] = CircleData{ x: clampCoord(baseX+moveX, width), y: clampCoord(baseY+moveY, height), radius: 8 + rng.Intn(35), // 更大的半径范围(8-43) color: color.RGBA{ uint8(180 + rng.Intn(50)), // 更宽的颜色范围 uint8(180 + rng.Intn(50)), uint8(180 + rng.Intn(50)), 150 + uint8(rng.Intn(105)), // 更宽的透明度范围 }, } } data.circles[frame] = circles } // 生成干扰点数据(闪烁效果)- 增加点数量 dotCount := 200 // 从100增加到200 for frame := 0; frame < frameCount; frame++ { dots := make([]DotData, dotCount) for i := 0; i < dotCount; i++ { x := rng.Intn(width) y := rng.Intn(height) // 某些点会在某些帧消失(闪烁效果) visible := frame%3 != i%3 || rng.Float64() > 0.3 dots[i] = DotData{ x: x, y: y, color: color.RGBA{ uint8(120 + rng.Intn(110)), // 更宽的颜色范围 uint8(120 + rng.Intn(110)), uint8(120 + rng.Intn(110)), func() uint8 { if visible { return 200 + uint8(rng.Intn(55)) } return uint8(80 + rng.Intn(120)) }(), }, size: 1 + rng.Intn(3), // 更大的点尺寸(1-3) } } data.dots[frame] = dots } return data } func clampCoord(v, max int) int { if v < 0 { return 0 } if v >= max { return max - 1 } return v } // createSharedPalette 创建统一的调色板(所有帧共享) func createSharedPalette() color.Palette { palette := make(color.Palette, 256) // 添加灰色渐变(0-239) for i := 0; i < 240; i++ { val := uint8(i * 255 / 240) palette[i] = color.RGBA{val, val, val, 255} } // 添加常用颜色(240-255) colorMap := map[int]color.RGBA{ 240: {30, 30, 30, 255}, 241: {60, 60, 60, 255}, 242: {50, 50, 50, 255}, 243: {40, 40, 80, 255}, 244: {80, 40, 40, 255}, 245: {50, 80, 50, 255}, 246: {100, 30, 30, 255}, 247: {30, 100, 30, 255}, 248: {30, 30, 100, 255}, 249: {140, 140, 140, 255}, 250: {160, 160, 160, 255}, 251: {180, 180, 180, 255}, 252: {200, 200, 200, 255}, 253: {220, 220, 220, 255}, 254: {235, 235, 235, 255}, 255: {255, 255, 255, 255}, } for idx, col := range colorMap { if idx < 256 { palette[idx] = col } } return palette } // convertToPalettedWithPalette 使用指定调色板将RGBA图像转换为Paletted图像 func convertToPalettedWithPalette(img *image.RGBA, palette color.Palette) *image.Paletted { bounds := img.Bounds() // 创建Paletted图像 paletted := image.NewPaletted(bounds, palette) // 转换:找到最接近的调色板颜色 for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { c := img.At(x, y) // 找到调色板中最接近的颜色 idx := findClosestColor(c, palette) paletted.SetColorIndex(x, y, uint8(idx)) } } return paletted } // findClosestColor 在调色板中找到最接近的颜色索引 func findClosestColor(c color.Color, palette color.Palette) int { r, g, b, _ := c.RGBA() minDist := uint32(^uint32(0)) bestIdx := 0 for i := 0; i < len(palette); i++ { cr, cg, cb, _ := palette[i].RGBA() // 计算欧氏距离 dr := (r >> 8) - (cr >> 8) dg := (g >> 8) - (cg >> 8) db := (b >> 8) - (cb >> 8) dist := dr*dr + dg*dg + db*db if dist < minDist { minDist = dist bestIdx = i } } return bestIdx } // drawGradientBackground 绘制渐变背景 func drawGradientBackground(img *image.RGBA, rng *rand.Rand, width, height int) { // 随机选择渐变方向 vertical := rng.Intn(2) == 0 if vertical { // 垂直渐变 startR := uint8(220 + rng.Intn(35)) startG := uint8(220 + rng.Intn(35)) startB := uint8(220 + rng.Intn(35)) endR := uint8(200 + rng.Intn(55)) endG := uint8(200 + rng.Intn(55)) endB := uint8(200 + rng.Intn(55)) for y := 0; y < height; y++ { ratio := float64(y) / float64(height) r := uint8(float64(startR)*(1-ratio) + float64(endR)*ratio) g := uint8(float64(startG)*(1-ratio) + float64(endG)*ratio) b := uint8(float64(startB)*(1-ratio) + float64(endB)*ratio) for x := 0; x < width; x++ { img.Set(x, y, color.RGBA{r, g, b, 255}) } } } else { // 水平渐变 startR := uint8(220 + rng.Intn(35)) startG := uint8(220 + rng.Intn(35)) startB := uint8(220 + rng.Intn(35)) endR := uint8(200 + rng.Intn(55)) endG := uint8(200 + rng.Intn(55)) endB := uint8(200 + rng.Intn(55)) for x := 0; x < width; x++ { ratio := float64(x) / float64(width) r := uint8(float64(startR)*(1-ratio) + float64(endR)*ratio) g := uint8(float64(startG)*(1-ratio) + float64(endG)*ratio) b := uint8(float64(startB)*(1-ratio) + float64(endB)*ratio) for y := 0; y < height; y++ { img.Set(x, y, color.RGBA{r, g, b, 255}) } } } } // drawAdditionalShapes 绘制额外的背景形状 func drawAdditionalShapes(img *image.RGBA, rng *rand.Rand, width, height int) { shapeCount := 5 + rng.Intn(8) // 5-12个形状 for i := 0; i < shapeCount; i++ { shapeType := rng.Intn(3) x := rng.Intn(width) y := rng.Intn(height) // 随机颜色(浅色,半透明) col := color.RGBA{ uint8(190 + rng.Intn(45)), uint8(190 + rng.Intn(45)), uint8(190 + rng.Intn(45)), uint8(100 + rng.Intn(100)), } switch shapeType { case 0: // 矩形 w := 15 + rng.Intn(40) h := 15 + rng.Intn(40) drawRect(img, x, y, w, h, col) case 1: // 椭圆 rx := 10 + rng.Intn(25) ry := 10 + rng.Intn(25) drawEllipse(img, x, y, rx, ry, col) case 2: // 小圆圈 radius := 5 + rng.Intn(15) drawCircle(img, x, y, radius, col) } } } // drawRect 绘制矩形 func drawRect(img *image.RGBA, x, y, w, h int, c color.Color) { for dy := 0; dy < h && y+dy < img.Bounds().Dy(); dy++ { if y+dy < 0 { continue } for dx := 0; dx < w && x+dx < img.Bounds().Dx(); dx++ { if x+dx < 0 { continue } r, g, b, _ := img.At(x+dx, y+dy).RGBA() cr, cg, cb, ca := c.RGBA() alpha := uint32(ca >> 8) invAlpha := 255 - alpha newR := uint8(((uint32(r>>8) * invAlpha) + (uint32(cr>>8) * alpha)) / 255) newG := uint8(((uint32(g>>8) * invAlpha) + (uint32(cg>>8) * alpha)) / 255) newB := uint8(((uint32(b>>8) * invAlpha) + (uint32(cb>>8) * alpha)) / 255) img.Set(x+dx, y+dy, color.RGBA{newR, newG, newB, 255}) } } } // drawEllipse 绘制椭圆 func drawEllipse(img *image.RGBA, cx, cy, rx, ry int, c color.Color) { for y := -ry; y <= ry; y++ { for x := -rx; x <= rx; x++ { // 椭圆方程: (x/rx)^2 + (y/ry)^2 <= 1 if float64(x*x)/(float64(rx*rx))+float64(y*y)/(float64(ry*ry)) <= 1.0 { px := cx + x py := cy + y if px >= 0 && px < img.Bounds().Dx() && py >= 0 && py < img.Bounds().Dy() { r, g, b, _ := img.At(px, py).RGBA() cr, cg, cb, ca := c.RGBA() alpha := uint32(ca >> 8) invAlpha := 255 - alpha newR := uint8(((uint32(r>>8) * invAlpha) + (uint32(cr>>8) * alpha)) / 255) newG := uint8(((uint32(g>>8) * invAlpha) + (uint32(cg>>8) * alpha)) / 255) newB := uint8(((uint32(b>>8) * invAlpha) + (uint32(cb>>8) * alpha)) / 255) img.Set(px, py, color.RGBA{newR, newG, newB, 255}) } } } } } // drawRandomNoiseLines 绘制额外的随机干扰线 func drawRandomNoiseLines(img *image.RGBA, rng *rand.Rand, width, height int) { lineCount := 8 + rng.Intn(12) // 8-19条随机线 for i := 0; i < lineCount; i++ { x1 := rng.Intn(width) y1 := rng.Intn(height) x2 := rng.Intn(width) y2 := rng.Intn(height) col := color.RGBA{ uint8(160 + rng.Intn(70)), uint8(160 + rng.Intn(70)), uint8(160 + rng.Intn(70)), uint8(150 + rng.Intn(105)), } thickness := 1 + rng.Intn(2) drawThickLine(img, x1, y1, x2, y2, col, thickness) } } // addBlurNoise 添加模糊噪点效果 - 增加噪点密度 func addBlurNoise(img *image.RGBA, rng *rand.Rand) { // 增加随机噪点,使图片看起来更模糊(从1/50增加到1/30) for i := 0; i < img.Bounds().Dx()*img.Bounds().Dy()/30; i++ { x := rng.Intn(img.Bounds().Dx()) y := rng.Intn(img.Bounds().Dy()) // 获取当前像素 r, g, b, _ := img.At(x, y).RGBA() rVal := uint8(r >> 8) gVal := uint8(g >> 8) bVal := uint8(b >> 8) // 添加轻微随机变化 noise := int8(rng.Intn(21) - 10) // -10 到 10 newR := clamp(int(rVal) + int(noise)) newG := clamp(int(gVal) + int(noise)) newB := clamp(int(bVal) + int(noise)) img.Set(x, y, color.RGBA{uint8(newR), uint8(newG), uint8(newB), 255}) } } func clamp(v int) int { if v < 0 { return 0 } if v > 255 { return 255 } return v } // drawNumbersWithWiggle 绘制数字,带抖动效果(类似wiggle)- 增加随机分布 func drawNumbersWithWiggle(img *image.RGBA, code string, rng *rand.Rand, width, height int, frameIndex, totalFrames int) { // 根据宽度和高度调整字符大小(适配新的280x50尺寸) charWidth := width / 18 // 约15(280宽度时为15.5) charHeight := height * 4 / 5 // 约40(50高度时为40) charSpacing := width / 25 // 字符间距,稍微减小以适应更小的空间 // 基础起始位置 - 更随机化 baseStartX := width/12 + rng.Intn(width/20) // 随机起始X位置 baseStartY := height/8 + rng.Intn(height/6) // 随机起始Y位置,允许上下移动(适配50px高度) // 数字颜色(深色但稍微模糊,降低对比度) colors := []color.RGBA{ {30, 30, 30, 255}, // 深灰(不是纯黑,更模糊) {60, 60, 60, 255}, // 中灰 {40, 40, 80, 255}, // 深蓝(模糊) {80, 40, 40, 255}, // 深红(模糊) {50, 80, 50, 255}, // 深绿(模糊) {50, 50, 50, 255}, // 灰色 } // 计算抖动偏移(使用正弦波创建平滑的抖动效果) wiggleX := float64(frameIndex) / float64(totalFrames) * 2 * 3.14159 wiggleY := float64(frameIndex) / float64(totalFrames) * 2 * 3.14159 * 1.3 for i, char := range code { // 随机选择颜色(偏深色但不要太黑,更模糊) charColor := colors[rng.Intn(len(colors))] // 基础位置 - 每个字符位置增加随机偏移 charRandomOffsetX := rng.Intn(width/25) - width/50 // ±5像素的随机偏移 charRandomOffsetY := rng.Intn(height/8) - height/16 // ±3像素的随机偏移 baseX := baseStartX + i*(charWidth+charSpacing) + charRandomOffsetX baseY := baseStartY + charRandomOffsetY // 添加抖动效果(每个字符的抖动幅度和相位不同)- 增加抖动幅度 charWiggleX := math.Sin(wiggleX+float64(i)*0.8) * 4.0 // 从2.0增加到4.0 charWiggleY := math.Sin(wiggleY+float64(i)*1.1) * 4.0 // 从2.0增加到4.0 // 额外的随机偏移 - 增加随机范围 xOffset := int(charWiggleX) + rng.Intn(7) - 3 // 从±1增加到±3 yOffset := int(charWiggleY) + rng.Intn(7) - 3 // 从±1增加到±3 x := baseX + xOffset y := baseY + yOffset // 确保不超出边界 if x < 0 { x = 0 } if x+charWidth > width { x = width - charWidth } if y < 0 { y = 0 } if y+charHeight > height { y = height - charHeight } // 绘制数字(使用简化版本:绘制矩形块模拟数字) drawSimpleNumber(img, byte(char), x, y, charWidth, charHeight, charColor) } } // drawSimpleNumber 绘制单个数字(简化版本,根据尺寸缩放) func drawSimpleNumber(img *image.RGBA, digit byte, x, y, width, height int, c color.Color) { digitValue := int(digit - '0') // 缩放比例(相对于原始16x24大小) scaleX := float64(width) / 16.0 scaleY := float64(height) / 24.0 // 根据数字绘制不同的小矩形块来模拟数字形状(原始坐标) basePatterns := [][]struct{ x, y, w, h int }{ // 0 {{2, 2, 14, 2}, {2, 2, 2, 20}, {14, 2, 2, 20}, {2, 20, 14, 2}}, // 1 {{8, 2, 2, 20}}, // 2 {{2, 2, 12, 2}, {12, 2, 2, 10}, {2, 10, 12, 2}, {2, 10, 2, 10}, {2, 20, 12, 2}}, // 3 {{2, 2, 12, 2}, {12, 2, 2, 20}, {2, 10, 10, 2}, {2, 20, 12, 2}}, // 4 {{2, 2, 2, 10}, {2, 10, 10, 2}, {12, 2, 2, 20}}, // 5 {{2, 2, 12, 2}, {2, 2, 2, 10}, {2, 10, 12, 2}, {12, 10, 2, 10}, {2, 20, 12, 2}}, // 6 {{2, 2, 12, 2}, {2, 2, 2, 20}, {12, 10, 2, 10}, {2, 10, 10, 2}, {2, 20, 12, 2}}, // 7 {{2, 2, 12, 2}, {12, 2, 2, 20}}, // 8 {{2, 2, 12, 2}, {2, 2, 2, 10}, {12, 2, 2, 10}, {2, 10, 12, 2}, {2, 10, 2, 10}, {12, 10, 2, 10}, {2, 20, 12, 2}}, // 9 {{2, 2, 12, 2}, {2, 2, 2, 10}, {12, 2, 2, 20}, {2, 10, 10, 2}}, } if digitValue >= 0 && digitValue <= 9 { pattern := basePatterns[digitValue] for _, p := range pattern { // 缩放坐标 px := int(float64(p.x) * scaleX) py := int(float64(p.y) * scaleY) pw := int(float64(p.w) * scaleX) ph := int(float64(p.h) * scaleY) // 确保最小尺寸 if pw < 1 { pw = 1 } if ph < 1 { ph = 1 } rect := image.Rect(x+px, y+py, x+px+pw, y+py+ph) draw.Draw(img, rect, &image.Uniform{c}, image.Point{}, draw.Over) } } } // drawAnimatedCircles 绘制移动的背景圆圈 func drawAnimatedCircles(img *image.RGBA, data *NoiseData, frameIndex int, width, height int) { if frameIndex >= len(data.circles) { return } for _, circle := range data.circles[frameIndex] { drawCircle(img, circle.x, circle.y, circle.radius, circle.color) } } // drawCircle 绘制圆圈 func drawCircle(img *image.RGBA, cx, cy, radius int, c color.Color) { for y := -radius; y <= radius; y++ { for x := -radius; x <= radius; x++ { if x*x+y*y <= radius*radius { px := cx + x py := cy + y if px >= 0 && px < img.Bounds().Dx() && py >= 0 && py < img.Bounds().Dy() { // 获取当前像素并混合颜色 r, g, b, _ := img.At(px, py).RGBA() cr, cg, cb, ca := c.RGBA() // Alpha混合 alpha := uint32(ca >> 8) invAlpha := 255 - alpha newR := uint8(((uint32(r>>8) * invAlpha) + (uint32(cr>>8) * alpha)) / 255) newG := uint8(((uint32(g>>8) * invAlpha) + (uint32(cg>>8) * alpha)) / 255) newB := uint8(((uint32(b>>8) * invAlpha) + (uint32(cb>>8) * alpha)) / 255) img.Set(px, py, color.RGBA{newR, newG, newB, 255}) } } } } } // drawAnimatedNoiseLines 绘制动态干扰线 func drawAnimatedNoiseLines(img *image.RGBA, data *NoiseData, frameIndex int, width, height int) { if frameIndex >= len(data.lines) { return } for _, line := range data.lines[frameIndex] { drawThickLine(img, line.x1, line.y1, line.x2, line.y2, line.color, line.thickness) } } // drawAnimatedNoiseDots 绘制动态干扰点 func drawAnimatedNoiseDots(img *image.RGBA, data *NoiseData, frameIndex int, width, height int) { if frameIndex >= len(data.dots) { return } for _, dot := range data.dots[frameIndex] { if dot.color.A < 50 { continue // 跳过几乎透明的点 } // 绘制点(可能是1x1或2x2) for dx := 0; dx < dot.size && dot.x+dx < width; dx++ { for dy := 0; dy < dot.size && dot.y+dy < height; dy++ { img.Set(dot.x+dx, dot.y+dy, dot.color) } } } } // drawThickLine 绘制粗线 func drawThickLine(img *image.RGBA, x1, y1, x2, y2 int, c color.Color, thickness int) { for i := 0; i < thickness; i++ { offsetX := i - thickness/2 offsetY := i - thickness/2 drawLine(img, x1+offsetX, y1+offsetY, x2+offsetX, y2+offsetY, c) } } // drawLine 绘制直线(Bresenham算法简化版) func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.Color) { dx := abs(x2 - x1) dy := abs(y2 - y1) sx := 1 if x1 > x2 { sx = -1 } sy := 1 if y1 > y2 { sy = -1 } err := dx - dy x, y := x1, y1 for { if x >= 0 && x < img.Bounds().Dx() && y >= 0 && y < img.Bounds().Dy() { img.Set(x, y, c) } if x == x2 && y == y2 { break } e2 := 2 * err if e2 > -dy { err -= dy x += sx } if e2 < dx { err += dx y += sy } } } // addNoiseDots 添加干扰点(更多更密集) func addNoiseDots(img *image.RGBA, rng *rand.Rand, count int) { for i := 0; i < count; i++ { x := rng.Intn(img.Bounds().Dx()) y := rng.Intn(img.Bounds().Dy()) // 随机颜色(浅色,增加模糊效果) brightness := 140 + rng.Intn(90) // 140-230之间 dotColor := color.RGBA{ uint8(brightness + rng.Intn(30) - 15), uint8(brightness + rng.Intn(30) - 15), uint8(brightness + rng.Intn(30) - 15), 255, } // 有时绘制小区域而不是单个点(更密集的干扰) if rng.Intn(3) == 0 { // 绘制2x2的小块 for dx := 0; dx < 2 && x+dx < img.Bounds().Dx(); dx++ { for dy := 0; dy < 2 && y+dy < img.Bounds().Dy(); dy++ { img.Set(x+dx, y+dy, dotColor) } } } else { img.Set(x, y, dotColor) } } } func abs(x int) int { if x < 0 { return -x } return x }