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 } |
需要一个字体文件,这里使用的幼圆(幼圆免费,其他免费字体懒得找)。
注意:字体不同,宽度和高度可能会需要调整。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· Blazor Hybrid适配到HarmonyOS系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库