让JS与PHP的绘图执行效果一致
最近摸索前端canvas画布和后端图像加工相关的一些功能。写了个练手程序,为了让前端(js控制css)预览的体验与后端php图像处理一致。总结了几点经验。
先上成品,表情包生成器:
https://gen8.orz.com.cn/mymeme
*此文并非系统且科普的教学,讲述的内容基于我个人备忘比较跳跃和零散。
目标:
- 用户上传模板图
- 在图上画出一个录入文字的涂鸦区域(定义尺寸、位置、文字大小、角度),并可以预览效果
- 保存成方案,之后可以打开与分享这个方案。把输入的文字(段子)合成到图片上
- 根据涂鸦区域的大小与文字的多与少,一定范围内自动调节文字的大小(未做更换字体、颜色)
这几个目标遇到问题较多的是预览前端的涂鸦区预览 与 后端生成图片的一致性。
问题一:旋转
前端(CSS)默认以对象的水平与垂直中心旋转,PHP在图像处理中 本案例只涉及imagettftext()方法,要考虑的因素相对比较简单。对于中文来说,是以文字块的左下角作为参考点的(如果是英文,则基准点在大写字母的左下角,要额外考虑小写字母会不会向下越界的问题)
因此当我们前端写css+js预览的时候,就需要修正旋转的中心点
CSS的
transform-origin: left 16px;
/* 第二参数,从左上角顶点向下16px 是 字体的高度,定位到第一行字的第一个字符的左下角 */
用jq写则是这样
$("#pvtext").css({
"font-size":newFontSize+"px",
"line-height":(newFontSize+2)+"px",
"transform-origin":"left "+newFontSize+"px"
});
此外,发现在 CSS中 transform: rotate(45deg);
是顺时针旋转45°,而在 PHP 的 imagettftext() 中 第3参数 float angle
则是相反的逆时针旋转。这个也比较好解决,在 CSS 或 PHP选一方做成负数就可以了
我的方法是 JS 里设成负值, 前端的体验就和后端一致是逆时针旋转了
$("#pvtext").css({"transform":"rotate(-"+ deg +"deg)"});
问题二:换行 断句
对DOM来说多行文字DIV是一个整体,而PHP 的imagettftext()方法不能处理换行,需要自己计算长和宽把文本切割成多行,分多次写入到图像上。
此处发现 同样的 ttf 字库下,在CSS定义的27px 比PHP imagettftext() 中定义的 27px 要小很多,要手动把 php中的字号修正大约 -6 才接近浏览器的观感,甚至担心是不是不同的浏览器解析的字体大小也会有明显的差别,只测试过firefox和chrome px定义还是比较接近的,其他则未验证。
在CSS中,对涂鸦区域定义为强制换行,可以解决英文单词越界的问题
word-break: break-all;
在PHP中,用了 imagettfbbox() 评估字符串的宽度,进行断句。把一整段文字顺序分割成不超出涂鸦框宽度的多行
//还有一种情况是字数较少填不满1行,就不用做这个切割动作的,这里就略过不写
//一般来说字体宽度小于等于fontsize
$min_line_chrs = floor($width/$fontsize); //从这个字数开始尝试 $width是涂鸦区宽度
unset($lines); //多行array
while($msg){ //$msg 是完整的内容
$add_chrs = 0; //尝试添加字数,验证宽度有没有超过涂鸦区域
$line_w = 0; //行的宽度
while($msg && $line_w < $width ){
$newline = mb_substr($msg,0,$min_line_chrs+$add_chrs,"utf-8");
if($newline==$msg){ //所有内容取完,不用继续循环了
$lines[] = $newline;
break 2;
}
//此处 $fsr 值为 经过修正大小的 $fontsize值,收缩约6px 获得与CSS一致观感
//为方便测量宽度不定义角度,水平放置用x值求差。
list($line_x1,$line_y1,$line_x2) = imagettfbbox( $fsr, 0, $ttf, $newline);
$line_w = $line_x2 - $line_x1; //文字块的矩形的宽度
$add_chrs++;
}
//跳出时肯定属于已经超
if($newline == $msg){ //没有触发换行,一行扫描到尾了
$lines[] = $newline; //通常发生在最后一行
break; //不用继续循环了
}else{ //发生换行
$lines[] = mb_substr($newline,0,-1,"utf-8"); //最后一个字超出边界了,不要
//截短原字符串,继续循环断句
$msg = mb_substr($msg,$min_line_chrs+$add_chrs-2,mb_strlen($msg,"utf-8"),"utf-8");
}
}
问题三:参考点
经过上面把整段的文字切割成多行 $lines[]
之后,依次用 imagettftext 写入图片上。此时发现还有一个问题,如果 涂鸦块是带有一定角度的,还得计算第二行开始的第N行的起始位置。只简单同一个X值,Y值依次按字符尺寸递增的话,出来的文字块是带锯齿的
捡起了几乎要忘掉的三角函数,最后推导出来的偏移值是:
//把角度转换成弧度,除以2是为了勾股定理计算参考上图上半部
$hd = deg2rad($deg/2);
//弦 的长度cd,指的是deg = 0 时已知边长矩形块以左上角为顶点逆时针旋转 deg 度°
//之后左下角的点与原来位置的直线距离 ,也是参考上图
$cd = sin($hd)*2*($fs+2);
//推导出内角 b = 0.5 * deg , 进而推算出 x , y 的偏移量
$offset_y = $fs - $cd * sin($hd);
$offset_x = $cd * cos($hd); //同上
//最后逐行添加这个偏移量,叠出来的字块就像一个整体一般整齐
if($lines){
foreach($lines as $li){
//计算换行偏移,要用三角函数公式
//先实现第一行
imagettftext($im,$fsr,$deg,$l,$t+$fs,$c_blk,$ttf,$li);
$l += $offset_x;
$t += $offset_y;
}
}
最后来看效果
CSS+JS预览:
合成图还是比较一致的
