最近摸索前端canvas画布和后端图像加工相关的一些功能。写了个练手程序,为了让前端(js控制css)预览的体验与后端php图像处理一致。总结了几点经验。

先上成品,表情包生成器:
https://gen8.orz.com.cn/mymeme
QQ截图20191227142153.png

img11.jpg

*此文并非系统且科普的教学,讲述的内容基于我个人备忘比较跳跃和零散。

目标:

  1. 用户上传模板图
  2. 在图上画出一个录入文字的涂鸦区域(定义尺寸、位置、文字大小、角度),并可以预览效果
  3. 保存成方案,之后可以打开与分享这个方案。把输入的文字(段子)合成到图片上
  4. 根据涂鸦区域的大小与文字的多与少,一定范围内自动调节文字的大小(未做更换字体、颜色)

这几个目标遇到问题较多的是预览前端的涂鸦区预览 与 后端生成图片的一致性。

问题一:旋转

前端(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值依次按字符尺寸递增的话,出来的文字块是带锯齿的

微信图片编辑_20191227142109.jpg

捡起了几乎要忘掉的三角函数,最后推导出来的偏移值是:

//把角度转换成弧度,除以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预览:
QQ截图20191227141814.png

合成图还是比较一致的
img11.jpg

标签: php, js, css

添加新评论