Files
chat-deploy/internal/api/util/captcha.go
kim.dev.6789 b7f8db7d08 复制项目
2026-01-14 22:35:45 +08:00

784 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 // 约15280宽度时为15.5
charHeight := height * 4 / 5 // 约4050高度时为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
}