继折腾Magento项目有这么一个需求就是客户定制产品,网上的商业插件都比较呵呵惨不忍睹。而且需求也决定了必须手动开发。这里记录一下具体思路和踩过的坑。
需求如下:
客户在购买时需要实时反馈效果,加入购物车之后就要反馈出客户定制的实际效果图片,并把重要步骤的图片(如:客户上传的原图,处理后的图、合成图、定制文本等等)一并体现到订单数据里面。
开工前准备:
前端打算用到的技术:Vue、Jquery、Canvas、SVG;
后端用到的技术:PHP GD库
大致实现思路:
数据库设计:
在magento里面扩展一个新的产品类型,建立一个定制产品表存放定制属性,该表通过产品ID与产品主表关联。具体属性需要有主画布(宽、高、比例、使用的字体、使用的字号等等)画布里面的层(宽、高、坐标、默认字体、默认图片等等)
1、客户上传图片与我们的产品模板图叠加合成;这个需求好满足用GD库后台合成。这里需要处理好一个画布上有多个图片上传点的处理,只要是层级问题避免图片合成的时候结果不对;
2、客户定制文本,这个比较麻烦,坑点也很多下面会不太详细的展开说;
首先单行水平文本或单行垂直问题还相对比较简单,圆形、弧线文本绕图则很麻烦,主要是误差和支持度问题。
SVG:没有生成图片的接口,对文本的支持程度很好,可以直接在页面上用SVG代码表示出来,只需要通过VUE的双向绑定即可达到目的;
SVG只需要初始化的时候计算出路径即可。
SVG弧线文本实现:
<svg width="<?php echo $_item['width'];?>" height="<?php echo $_item['height'];?>" ref="<?php echo $_item['code'];?>_svg" style="<?php echo $roate;?>">
<text stroke-width="0" stroke="<?php echo $_item['color']?>" style="<?php echo $svgFt?>">
<textPath xlink:href="#<?php echo $_item['code'];?>_svg_path" v-text="<?php echo $_item['code'];?>" startOffset="50%" text-anchor="middle" alignment-baseline="<?php echo $ali;?>" >
</textPath>
</text>
</svg>
function createSVGPath(startX, startY, R, theta, r, pathid) {
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
var dir = 1;
var lenghty = theta > 180 ? 1 : 0;
var dArr = '';
switch (R) {
case 1:
case 180:
//圆
dir = parseFloat(r) > 0 ? 1 : 0;
var w = startX,
r = startY,
dArr = ['M', r, r, 'm',-r , w-2*r, 'a', r, r, 0, lenghty, dir, 2*r, 0, 'a', r, r, 0, lenghty, dir, -2*r, 0];//圆
break;
case 360:
//弧
dir = parseFloat(r) > 0 ? 0 : 1;
var realR = R;
var startY = dir ? startY : startY * 0.92;
dArr = ["M" + startX, startY, "A" + realR, realR/3, 0, lenghty, dir];
var cx = startX, cy = startY + R;
var theta2 = theta%360;
// 避免360度与0度一样的情况
theta = theta > 0 && theta2 == 0 ? 359.9 : theta2;
var alpha = (theta + 90)/180 * Math.PI;
var dx = realR * Math.cos(alpha) - startY - startX/10 - 30;
var dy = realR * Math.sin(alpha)+startX/10;
var x = cx + dx, y = cy - dy;
dArr.push(x.toFixed(2));
dArr.push(y.toFixed(2));
break;
}
var d = dArr.join(" ");
path.setAttribute('d', d);
//path.setAttribute('stroke', 'red');
//path.setAttribute('stroke-width', 1);
path.setAttribute('fill', 'none');
path.setAttribute('id', pathid);
return path;
}
CANVAS:有生成图片的接口,对文本的支持程度让人抓狂各种实现困难 ,只能通过js渲染到页面上,每次改变需要重新执行方法,画布复杂了开销就上去了;并且全部在客户端生成合成图有一个大坑就是数据确实都可以生成出来,但是数据流太大了,前台页面会很卡,服务器也因为超出了post字节范围,反馈很慢或者干脆放弃了。
canvas无法实现文字翻转,无法精确实现文字按弧度居中。且每次改变都需要执行重绘方法,付代码:
canvas弧线文本实现:
function drawTextAlongArc(canvasId, str, centerX, centerY, radius, angle, spacing, maxLen) {
var canvas = document.getElementById(canvasId),
context = canvas.getContext('2d');
var len = maxLen||str.length, s;
context.save();
context.translate(centerX, centerY);
context.font = '30px Calibri';
context.textAlign = 'center';
context.fillStyle = 'blue';
context.strokeStyle = 'blue';
context.lineWidth = 4;
//context.rotate(-1 * angle);
//角度归零
//context.rotate(-1 * (angle / str.length) / 2);
//正
context.rotate(-1 * ( angle+ spacing)/ len*str.length / 2);
if(angle<0){
context.scale(-1, -1);
str = reversetext(str);
console.log(str);
}
//context.rotate(-1*( angle+ spacing)/ len * str.length);
// context.rotate((angle / maxLen)*str.length / 2);
// console.log((angle / maxLen)*str.length / 2);
//居中
//context.rotate(0);
for(var n = 0; n < len; n++) {
if(str.length>=n+1){
s = str[n];
console.log(s);
if(s===' '){
t=-2
}else{t=0}
context.rotate((angle + t +spacing) / len);
context.save();
context.translate(0, -1 * radius);
context.fillText(s, 0, 0);
context.restore();
}else{
continue;
}
}
context.restore();
}
PHP GD:后端图片库,对文本支持不太好跟canvas有得一拼,且需要在服务器上放置字体文件才能显示合成字,中文字体不认识英文字,英文字体不认识中文字。预览合成都调用服务器,不是个好办法。而且复杂文本应用上跟前台的误差非常大。
请仔细看字母“i”e",PHP的文字宽度每个是不一样的,这就造成了误差很大,即便是单独处理这些玩意也让人蛋疼,总之看来想砸电脑。付相关代码:
PHP 弧线文本实现:
private function drawString(){
//编码处理
$charset = mb_detect_encoding($this->sealString);
if($charset != 'UTF-8'){
$this->sealString = mb_convert_encoding($this->sealString, 'UTF-8', 'GBK');
}
//相关计量
$this->charRadius = $this->sealRadius - $this->fontSize; //字符串半径
$leng = mb_strlen($this->sealString,'utf8'); //字符串长度
if($leng > $this->strMaxLeng) $leng = $this->strMaxLeng;
$avgAngle = $this->circleDir ? 180 / ($this->strMaxLeng) : 265 / ($this->strMaxLeng); //平均字符倾斜度
//拆分并写入字符串
$words = array(); //字符数组
$fw = 0;
if($this->circleDir){
$textBox = imagettfbbox($this->fontSize, 0, $this->font, $this->sealString);
$textWidth = $textBox[2] - $textBox[0];
$textHeight = $textBox[1] - $textBox[7];
// if($textWidth < 70) {
// $textWidth = 215;
// }
$centerWorld = $textWidth / 2;
if( $this->arc < $centerWorld ){
$this->arc = $centerWorld;
}
$angle = 2 * asin( $textWidth / ( 2 * $this->arc ) );
$dtArc = $this->arc * $angle;
$iteratorX = 0;
$nowFw = 0;
}
for($i=0;$i<$leng;$i++){
$words[] = mb_substr($this->sealString,$i,1,'utf8');
if($this->circleDir){
// $r = ($aStart * ($leng - 1 - $i) + $aEnd * $i) / ($leng - 1);
// $fontBox = imagettfbbox($this->fontSize, 0, $this->font, $words[$i]);
// $fw = $fontBox[2] - $fontBox[0];
// $fh = $fontBox[1] - $fontBox[7];
// $R = (int)$r + 90 * ($bCCW ? 1 : -1);//字符角度
// $x = $this->sealRadius + $this->sealRadius * cos(deg2rad($r)) - $this->fontSize;
// $y = $this->sealRadius - $this->sealRadius * sin(deg2rad($r)) - $this->fontSize;
$fontBox = imagettfbbox($this->fontSize, 0, $this->font, $words[$i]);
$fw = $fontBox[2] - $fontBox[0];
$letterCenter = $textWidth - $fw / 2;
$dtArcLetter = ( $fw / $textWidth ) * $dtArc;
$beta = $dtArcLetter / $this->arc;
$h = $this->arc * cos( $beta / 2 );
$alpha = acos(( $textWidth / 2 - $iteratorX ) / $this->arc );
$theta = $alpha + $beta / 2;
$x1 = cos( $theta ) * $h;
$y1 = sin( $theta ) * $h;
$xpos = $iteratorX + abs( $textWidth / 2 - $x1 - $iteratorX );
$x = $xpos - $letterCenter +$this->centerDot['x'] + $nowFw/2 + $textWidth/4;
$y = $this->reverse ? $textHeight/1.5 + $this->height/10+$this->centerDot['y'] - ($this->arc - $y1) : ($this->arc - $y1)+$this->centerDot['y']-$this->height/10+$textHeight;//;
$R = 0;
//var_dump($this->circleDir - $y1);
$iteratorX = 2 * $xpos - $iteratorX;
$nowFw = $nowFw + $fw;
}else{
$e = $i > $leng/2 ? -1 : 1;
$fontBox = imagettfbbox($this->fontSize, 0, $this->font, $words[$i]);
$fw = $fontBox[2] - $fontBox[0];
//$avg = ($fw < 6) ? $avgAngle*($i - $leng/2) - $fw : $avgAngle*($i - $leng/2);
$avgx = ($fw < $this->fontSize/3) ? $e*1.5 : 0;
$avgy = ($fw < $this->fontSize/3) ? $e*7 + $i*0.1 : 0;
$r = $this->reverse>0 ? 635 - $this->charAngle + $avgAngle*($i - $leng/2) + $this->spacing*($i-1) : 625 - $this->charAngle - $avgAngle*($i - $leng/2) - $this->spacing*($i+1);//625 + $this->charAngle + $avgAngle*($i - $leng/2) + $this->spacing*($i-1);//坐标角度
$R = $this->reverse>0 ? 710 - $this->charAngle + $avgAngle*($leng-2*$i)/2 + $this->spacing*(1-$i) : 725 - $this->charAngle - $avgAngle*($leng-2*$i)/2;//720 - $this->charAngle + $avgAngle*($leng-2*$i)/2 + $this->spacing*(1-$i);字符角度
$x = $this->reverse>0 ? $this->centerDot['x'] + $this->charRadius * cos(deg2rad($r)) - $avgx : $this->centerDot['x'] - ($this->charRadius+$this->fontSize/2) * cos(deg2rad($r)) + $avgx;//$this->centerDot['x'] - $this->charRadius * cos(deg2rad($r));// //字符的x坐标
$y = $this->reverse>0 ? $this->centerDot['y'] + $this->charRadius * sin(deg2rad($r)) - $avgy : $this->centerDot['y'] - ($this->charRadius+$this->fontSize/2) * sin(deg2rad($r)) + $avgy; //$this->centerDot['y'] + $this->charRadius * sin(deg2rad($r))字符的y坐标
}
imagettftext($this->img, $this->fontSize, $R, $x, $y, $this->backGround, $this->font, $words[$i]);
}
}
综上:我采取的办法是前端购买页面点击加入购物车之前全部操作通过vue实时在界面上反馈, 点击加入购物车之后
1、复杂文本如环形文本、扇形文本、曲线文本、采用SVG+vue然后分层转成canvas生成分层图片数据到服务器与模板图合成得到最终图
2、简单文本如垂直文本、水平文本直接到服务器合成,这里你会问为什么不用svg,因为svg对垂直文本支持不好。
具体可以参见这个文章写的很详细了:https://blog.csdn.net/huanhuanq1209/article/details/71438629
3、图片定制服务器合成
然后在加入购物车逻辑里面加入定制数据,这样在订单和购物车里面就可以获取到定制的相关内容。
最后你肯定还有个疑问,为什么大部分非得要在服务器合成?因为模版图是在服务器上的这个图片会比较大,产品不一样这样的图可能还不只一张,前端只拿需要合成的定制图,这样提交速度也能够保障,当然并不是完全没有误差,只是折中取舍了。
最后是把SVG转换成图片:
function drawInlineSVG(svgElement, callback){
var canvas = document.createElement('canvas');
canvas.setAttribute('width', $(svgElement).width());
canvas.setAttribute('height', $(svgElement).height());
var ctx = canvas.getContext('2d');
var svgURL = new XMLSerializer().serializeToString(svgElement);
var img = new Image();
img.onload = function(){
//console.log(this);
ctx.drawImage(this, 0,0);
callback(canvas);
}
img.src = 'data:image/svg+xml; charset=utf8, '+encodeURIComponent(svgURL);
}
这里也有几个大坑,
1、SVG转换成canvas图片是没办法把fontfamily带到图片中去的,网上的办法都不行。但是调用系统的字体则没问题就是web安全字体是OK的。 这里我做了很多尝试,最后网上有种说法是在转canvas图片的时候浏览器出于安全考虑是不允许访问外部字体的,所以,这里如果要用第三方字体的唯一解决办法就是把字体文件转成base64的dataurl。这里简要说下核心坑点的填法:把ttf格式的文件转成base64,把要转图片的svg通过js克隆一下,把转换的base64字符串包裹在defs标签内加上style标签动态塞进svg里面(String 格式如下图)再转成xml编码再传到canvas画布里面转成图片;
2、drawImage必须放在onload或者window onload里面否则不会执行。
3、还有个问题就是网站运营人员是不懂什么SVG的,所以还需要额外开发一个SVG路径生成的工具否则这个东西没法玩。
4、空格问题,如果在文本中连续输入空格会被浏览器解析成一个空格, 我是用vue 输出的适合把空格转换成“nbsp;”这样做在界面展示的时候确实是没问题的,但是在转canvas的适合程序就卡住了转换出来的图片是一个损坏的文件,也没有报错,具体原因暂时不明,后来是用一个特殊符号空格解决的。