golang 仿知乎 中文验证码 倒立 源码

原创,转载请注明出处!

最开始用图形来模仿文字进行各种角度的倒立和排列,后来切换为文字后,有很多问题。总结如下:

1、程序在画图形和画文字方面不一样,图形的是从原点开始(0,0),而文字则从文字的基线开始(0,baseline)

2、在增加角度偏移时,文字或图形的高宽会产生变化(偏∠45度时达到最大),这时候为了让它们顶点对齐,需要计算偏移量(用三角函数)

3、在绘图时,会先旋转“画布”(描述可能不准确),再绘制文字。此时要往回旋转,否则下一个图形会顺着这个角度继续画。

4、为了让图形保持固定宽度,对于有偏角的文字,需要平均缩小左右间距(否则不同的角度,固定的文字个数,会让图形宽度不同)

效果图:

 

源码:(代码还可以再整理和优化,但限于计划时间,懒得弄了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
package main
 
import (
    "math"
    "reflect"
    "time"
    "math/rand"
    "github.com/golang/freetype/truetype"
    "github.com/fogleman/gg" // 需要安装这个包
    "flag"
    "fmt"
    "io/ioutil"
    "log"
)
 
var wORDSLIB = []interface{}{"赵","钱","孙","李","周","吴","郑","冯","陈","褚","卫","蒋","沈","韩","杨","朱","秦","尤","许","何","吕","施","张","孔","曹","严","华","金","魏","陶","姜","戚","谢","邹","喻","柏","水","窦","章","云","苏","潘","葛","奚","范","彭","郎","鲁","韦","昌","马","苗","凤","花","方","俞","任","袁","柳","酆","鲍","史","唐","费","廉","岑","薛","雷","贺","倪","汤","滕","殷","罗","毕","郝","邬","安","常","乐","于","时","傅","皮","卞","齐","康","伍","余","元","卜","顾","孟","平","黄","和","穆","萧","尹","姚","邵","湛","汪","祁","毛","禹","狄","米","贝","明","臧","计","伏","成","戴","谈","宋","茅","庞","熊","纪","舒","屈","项","祝","董","梁","杜","阮","蓝","闵","席","季","麻","强","贾","路","娄","危","江","童","颜","郭","梅","盛","林","***","钟","徐","邱","骆","高","夏","蔡","樊","胡","凌","霍","虞","万","支","柯","昝","管","卢","莫","经","房","裘","缪","干","解","应","宗","丁","宣","贲","邓","郁","单","杭","洪","包","诸","左","石","崔","吉","钮","龚","程","嵇","邢","滑","裴","陆","荣","翁","荀","羊","於","惠","甄","曲","家","封","芮","羿","储","靳","汲","邴","糜","松","井","段","富","巫","乌","焦","巴","弓","牧","隗","山","谷","车","侯","宓","蓬","全","郗","班","仰","秋","仲","伊","宫","宁","仇","栾","暴","甘","钭","厉","戎","祖","武","符","刘","景","詹","束","龙","叶","幸","司","韶","郜","黎","蓟","薄","印","宿","白","怀","蒲","邰","从","鄂","索","咸","籍","赖","卓","蔺","屠","蒙","池","乔","阴","鬱","胥","能","苍","双","闻","莘","党","翟","谭","贡","劳","逄","姬","扶","堵","冉","宰","郦","雍","卻","璩","桑","桂","濮","牛","寿","通","边","扈","燕","冀","郏","浦","尚","农","温","别","庄","晏","柴","瞿","阎","充","慕","连","茹","习","宦","艾","鱼","容","向","古","易","慎","戈","廖","庾","终","暨","居","衡","步","都","耿","满","弘","匡","国","文","寇","广","禄","阙","东","欧","殳","沃","利","蔚","越","夔","隆","师","巩","厍","聂","晁","勾","敖","融","冷","訾","辛","阚","那","简","饶","空","曾","毋","沙","乜","养","鞠","须","丰","巢","关","蒯","相","查","后","荆","红","游","竺","权","逯","盖","益","桓","公","俟","上","官","阳","人","赫","皇","甫","尉","迟","澹","台","冶","政","淳","太","叔","申","轩","辕","令","狐","离","宇","长","鲜","闾","丘","徒","仉","督","子","颛","端","木","西","漆","雕","正","壤","驷","良","拓","跋","夹","父","晋","楚","闫","法","汝","鄢","涂","钦","百","里","南","门","呼","延","归","海","舌","微","生","岳","帅","缑","亢","况","郈","有","琴","商","牟","佘","佴","伯","赏","墨","哈","谯","笪","年","爱","佟","第","五","言","福","姓"}
 
var (
    dpi      = flag.Float64("dpi", 72, "screen resolution in Dots Per Inch")
    fontfile = flag.String("fontfile", "./SIMYOU.TTF", "filename of the ttf font")
    size     = flag.Float64("size", 40, "font size in points")
)
 
func main() {
    x := "abc"
    fmt.Println(x[0:3])
    drawImageBygg()
}
 
func drawImageBygg(){
    dc := gg.NewContext(320, 56)  // 56 => w*sin(45) + h*sin(45)  45度时,字体达到最大高度
    dc.SetRGBA(1, 1, 1,0) // 设置背景色:末尾为透明度 1-0(1-不透明 0-透明)
    dc.Clear()
    dc.SetRGBA(0, 0, 0,1) // 设置字体色
 
    fontBytes, err := ioutil.ReadFile(*fontfile)
    if err != nil {
        log.Println(err)
        return
    }
    font, err := truetype.Parse(fontBytes)
    if err != nil {
        log.Println(err)
        return
    }
    face := truetype.NewFace(font, &truetype.Options{
        Size: *size,
        DPI:*dpi,
    })
    dc.SetFontFace(face)
 
    // 初始化用于计算坐标的变量
    fm := face.Metrics()
    ascent := float64(fm.Ascent.Round()) // 字体的基线到顶部距离
    decent := float64(fm.Descent.Round()) // 字体的基线到底部的距离
    w := float64(fm.Height.Round()) // 方块字,大多数应为等宽字,即和高度一样
    h := float64(fm.Height.Round())
    totalWidth := 0.0 // 目前已累积的图片宽度(需要用来计算字体位置)
 
    // 随机取汉字,定位倒立的字
    words := getRandomMembersFromMemberLibary(wORDSLIB,8) // 取8个字
    reverseWordsIndex := getRandomMembersFromMemberLibary([]interface{}{0,1,2,3,4,5,6,7},2) // 随机2个倒立字
 
    for i,word := range words{
        degree := If(Contain(i,reverseWordsIndex),float64(RandInt64(150,210)),float64(RandInt64(-30,30))) // 随机角度,正向角度 -30~30,倒立角度 150~210
        x,y,leftCutSize,rightCS := getCoordByQuadrantAndDegree(w,h,ascent,decent,degree,totalWidth)
        dc.RotateAbout(gg.Radians(degree),0,0)
        dc.DrawStringAnchored(word.(string), x,y, 0,0)
        dc.RotateAbout(-1*gg.Radians(degree),0,0)
        totalWidth = totalWidth + leftCutSize + rightCS
        fmt.Println("x:",x,"y:",y,"total:",totalWidth,"degree:",degree)
    }
 
    dc.Stroke()
    dc.SavePNG("out.png")
    fmt.Println("Wrote out.png OK.")
}
 
func getCoordByQuadrantAndDegree(w,h,ascent,descent,degree,beforTotalWidth float64)(x,y,leftCutSize,rightCutSize float64){
    var totalWidth float64
    switch{
        case degree<=0 && degree >= -40:  // 第一象限:逆时针 -30度 ~ 0  <=>  330 ~ 360 (目前参数要传入负数) 
            rd := -1 * degree // 转为正整数,便于计算
            leftCutSize = w*getDegreeSin(90-rd)
            rightCutSize = h*getDegreeSin(rd)
             
            offset := (leftCutSize + rightCutSize - w) / 2 // 横向偏移量(角度倾斜越厉害,占宽越多,通过偏移量分摊给它的左右边距来收窄)
            leftCutSize,rightCutSize = leftCutSize - offset,rightCutSize - offset
 
            totalWidth = beforTotalWidth + leftCutSize
            x = getDegreeSin(90 - rd)*totalWidth - w
            y = ascent + getDegreeSin(rd)*totalWidth 
        case degree >=0 && degree <= 40:  // 第四象限:顺时针 0 ~ 30度
            leftCutSize = h*getDegreeSin(degree)
            rightCutSize = w*getDegreeSin(90-degree)
 
            offset := (leftCutSize + rightCutSize - w) / 2
            leftCutSize,rightCutSize = leftCutSize - offset,rightCutSize - offset
 
            totalWidth = beforTotalWidth + leftCutSize // 现在totalwidth = 前面的宽 + 自己的左切边
            x = getDegreeSin(90-degree)*totalWidth
            y = ascent - getDegreeSin(degree)*totalWidth
        case degree >= 180 && degree <= 220:  // 第二象限:顺时针 180 ~ 210度
            rd := degree - 180
            leftCutSize = h*getDegreeSin(rd)
            rightCutSize = w*getDegreeSin(90-rd)
 
            offset := (leftCutSize + rightCutSize - w) / 2
            leftCutSize,rightCutSize = leftCutSize - offset,rightCutSize - offset
 
            totalWidth = beforTotalWidth + leftCutSize
            x = -1 * (getDegreeSin(90-rd)*totalWidth + w)
            y = getDegreeSin(rd)*totalWidth - descent
        case degree >= 140 && degree <= 180:  // 第三象限:顺时针 150 ~ 180度
            rd := 180-degree
            leftCutSize = w*getDegreeSin(90-rd)
            rightCutSize = h*getDegreeSin(rd)
 
            offset := (leftCutSize + rightCutSize - w) / 2
            leftCutSize,rightCutSize = leftCutSize - offset,rightCutSize - offset
             
            totalWidth = beforTotalWidth + leftCutSize
            x = -1 * (getDegreeSin(90-rd) * totalWidth)
            y = -1 * (getDegreeSin(rd) * totalWidth + descent)
        default: panic(fmt.Sprintf("非法的参数:%f",degree))
    }
    return
}
 
func getDegreeSin(degree float64) float64{
    return math.Sin(degree*math.Pi/180)
}
 
//RandInt64 ...
func RandInt64(min, max int64) int64 {
    if min >= max || min == 0 || max == 0 {
        return max
    }
    rand.Seed(time.Now().UnixNano())
    return rand.Int63n(max-min) + min
}
 
func getRandomMembersFromMemberLibary(lib []interface{},size int)[]interface{}{
    source,result := make([]interface{},0),make([]interface{},0)
    if size <= 0 || len(lib) == 0{
        return result
    }
    for _,v := range lib{
        source = append(source,v)
    }
    if size >= len(lib){
        return source
    }
    for i:=0;i<size;i++{
        rand.Seed(time.Now().UnixNano())
        pos := rand.Intn(len(source))
        result = append(result,source[pos])
        source = append(source[:pos], source[pos+1:]...)
    }
    return result
}
 
//Contain ...
func Contain(obj interface{}, target interface{}) bool {
    targetValue := reflect.ValueOf(target)
    switch reflect.TypeOf(target).Kind() {
    case reflect.Slice, reflect.Array:
        for i := 0; i < targetValue.Len(); i++ {
            if targetValue.Index(i).Interface() == obj {
                return true
            }
        }
    case reflect.Map:
        if targetValue.MapIndex(reflect.ValueOf(obj)).IsValid() {
            return true
        }
    }
 
    return false
}
 
//If ...
func If(expr bool,trueVal float64,falseVal float64) float64{
    if expr {
        return trueVal
    }
    return falseVal
}

  

 需要一个字体文件,这里使用的幼圆(幼圆免费,其他免费字体懒得找)。

注意:字体不同,宽度和高度可能会需要调整。

 

posted @   流失的痕迹  阅读(860)  评论(0编辑  收藏  举报
编辑推荐:
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
阅读排行:
· Blazor Hybrid适配到HarmonyOS系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库
点击右上角即可分享
微信分享提示