我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛
马上就到2023年农历新年了,新的一年是兔年,在这里做一个捏兔头的小工具,可以对兔子耳朵、眼睛、嘴巴的角度、位置或大小进行调整,也能点击随机按钮,随机画出一个兔头(或许会很有趣),希望能给阅读本文的你带来一点点的欢乐。
祝大家新年快乐。
兔头生成工具
成品
实现细节
1.画兔脸
创建一块canvas画布,并设置大小,我这里设置了300*300的大小。画一个圆形作为兔脸,并设置半径(r=50)。这里使用canvas的arc方法画一个圆形,该函数参数的意义分别是(圆心x轴坐标、圆心y轴坐标、半径、起始角度、结束角度)
ctx.arc(cx, cy, r, 0, Math.PI * 2)
画布的正中心是坐标点(150, 150),画一个完整圆的起始角度是0,结束角度是360度 现在我们得到了一个圆形的兔脸。
代码如下:
let canvas = $('canvas');
let width = 300, height = 300;
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
let ctx = canvas.getContext('2d');
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.fillStyle = "black";
ctx.fillRect(0, 0, width, height);
let cx = 150, cy = 150;
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();
2.画眼睛与嘴巴
笑的时候眼睛是眯起来的,就画一双在笑的眼睛,使用画二次曲线的函数:
quadraticCurveTo(cx, cy, x, y)
这里简单介绍一下画曲线函数的用法,想要画一条二次曲线,需要确定三个点:起点、终点、控制点。
曲线在起点、终点的两条切线相交的那个点,就是控制点,在很多的画图软件中都可以通过拖拽这个控制点来调整曲线的形态。
先画右眼,这里需要计算起始点的x、y坐标(relx, rely)、终点的x、y坐标(rerx, rery)、控制点的x、y坐标(recx, recy)共6个参数。
drawEyes方法传入了参数cx、cy,是脸的中心点坐标, 考虑了左右眼距离、眼睛的宽高、眼睛在脸部的相对位置,依次设置了eyeDistance、eyeWidth、eyeHeight、eyePosition几个参数。
这里的eyePosition的含义是相对于中心点上方的距离,那么眼睛的y坐标就是cy - eyePosition。
计算右眼左端点,就是用中心点x坐标(cx)加上眼距的一半 relx = cx + eyeDistance / 2。
右眼右端点就是用右眼左端点加上眼睛宽度rerx = relx + eyeWidth。
控制点的高度就设置在眼睛的正上方,那么x坐标就在左右端点的中间,y坐标就是左右端的y坐标减去眼睛高度eyeHeight。
recx = (relx + rerx) / 2;
recy = cy - eyePosition - eyeHeight;
这样右眼需要的参数就都计算完了。
画右眼代码:
let eyeDistance = drawParams.eyeDistance;
let eyeWidth = drawParams.eyeWidth;
let eyeHeight = drawParams.eyeHeight;
let eyePosition = drawParams.eyePosition;
let relx = cx + eyeDistance / 2;
let rely = cy - eyePosition;
let rerx = relx + eyeWidth;
let rery = cy - eyePosition;
let recx = (relx + rerx) / 2;
let recy = cy - eyePosition - eyeHeight;
tx.beginPath();
ctx.moveTo(relx, rely);
ctx.quadraticCurveTo(recx, recy, rerx, rery);
画左眼的端点计算可以利用左右眼对称的特性简化一下,端点和控制点的y坐标和右眼是相等的。左眼的左右端点x坐标与右眼的左右端点x坐标是相对于中心点(cx)对称的, 这里写了一个getSymmety函数计算对称点的x轴坐标,入参为某个点的x轴坐标、中心点坐标。将左眼的6个参数值计算出来后,就可以将左眼画好。
画左眼代码:
let lelx = getSymmety(relx, cx);
let lely = rely;
let lerx = getSymmety(rerx, cx);
let lery = rery;
let lecx = getSymmety(recx, cx);
let lecy = recy;
ctx.moveTo(lelx, lely);
ctx.quadraticCurveTo(lecx, lecy, lerx, lery);
ctx.stroke();
嘴巴的画法仍然是计算左右端点与控制点的x、y轴坐标,共六个参数,然后绘制曲线,就不再赘述了。
画嘴巴代码如下:
function drawMouth(ctx, cx, cy) {
let mouthPosition = drawParams.mouthPosition;
let mouthWidth = drawParams.mouthWidth;
let mouthHeight = drawParams.mouthHeight;
let mlx = cx - mouthWidth / 2;
let mly = cy + mouthPosition;
let mrx = getSymmety(mlx, cx);
let mry = mly;
let mcx = cx;
let mcy = mly + mouthHeight;
ctx.beginPath();
ctx.moveTo(mlx, mly);
ctx.quadraticCurveTo(mcx, mcy, mrx, mry);
ctx.stroke();
}
3.画耳朵
画耳朵是最复杂的部分,要计算的节点很多。
先定义几个变量,耳朵相对于中线的角度eAngle,耳宽的角度ewAngle,耳朵的长度earLength。
单只耳朵是由两条曲线拼接成的,那么就要分别计算两条曲线的起点、终点和控制点的横纵坐标,共12个参数。
由于两条曲线的终点是重合的,那么就省略了两个参数的计算,我们仍需要计算10个参数。
这10个参数分别为:
- relx: 右耳左曲线起始点x轴坐标
- rely: 右耳左曲线起始点y轴坐标
- reex: 右耳两条曲线的终点x轴坐标
- reey: 右耳两条虚线终点的y轴坐标
- relcx: 右耳左曲线控制点x轴坐标
- relcy: 右耳左曲线控制点y轴坐标
- rerx: 右耳右曲线起始点x轴坐标
- rery: 右耳右曲线起始点y轴坐标
- rercx: 右耳右曲线控制点x轴坐标
- rercy: 右耳右曲线控制点y轴坐标
计算这些端点坐标时,要根据耳朵的角度,使用三角函数计算。
为了描述清楚这几个变量的含义,请看下图。
画耳朵代码如下:
function drawEars(ctx, cx, cy) {
let eAngle = drawParams.eAngle;
let ewAngle = drawParams.ewAngle;
let earLength = drawParams.earLength;
let relx = cx + Math.sin(Math.PI * (eAngle - ewAngle) / 180) * r;
let rely = cy - Math.cos(Math.PI * (eAngle - ewAngle) / 180) * r;
let reex = cx + earLength * Math.sin(Math.PI * eAngle / 180);
let reey = cy - earLength * Math.cos(Math.PI * eAngle / 180);
let relcx = cx + (earLength / Math.cos(Math.PI * ewAngle / 180) * Math.sin(Math.PI * (eAngle - ewAngle) / 180));
let relcy = cy - (earLength / Math.cos(Math.PI * ewAngle / 180) * Math.cos(Math.PI * (eAngle - ewAngle) / 180));
let rerx = cx + Math.sin(Math.PI * (eAngle + ewAngle) / 180) * r;
let rery = cy - Math.cos(Math.PI * (eAngle + ewAngle) / 180) * r;
let rercx = cx + (earLength / Math.cos(Math.PI * ewAngle / 180) * Math.cos(Math.PI * (90 - (eAngle + ewAngle)) / 180));
let rercy = cy - (earLength / Math.cos(Math.PI * ewAngle / 180) * Math.sin(Math.PI * (90 - (eAngle + ewAngle)) / 180));
ctx.moveTo(relx, rely);
ctx.quadraticCurveTo(relcx, relcy, reex, reey);
ctx.moveTo(rerx, rery);
ctx.quadraticCurveTo(rercx, rercy, reex, reey);
ctx.stroke();
let lelx = getSymmety(rerx, cx);
let lely = rery;
let leex = getSymmety(reex, cx);
let leey = reey;
let lelcx = getSymmety(rercx, cx);
let lelcy = rercy;
let lerx = getSymmety(relx, cx);
let lery = rely;
let lercy = relcy;
let lercx = getSymmety(relcx, cx);
ctx.beginPath();
ctx.moveTo(lerx, lery);
ctx.quadraticCurveTo(lercx, lercy, leex, leey);
ctx.quadraticCurveTo(lelcx, lelcy, lelx, lely);
ctx.stroke();
}
4.添加滚动条控制参数
针对每一项可调整的参数生成一个滚动条,添加到页面中。
对耳朵长度参数做了特殊的限制,最小值是脸半径的2.5倍,为了避耳朵太短(兔子的耳朵就是要长)。
效果如下:
代码如下:
function initInputList() {
for (let i in drawParams) {
let d = document.createElement("input");
d.setAttribute("type", "range");
d.setAttribute("id", i);
d.setAttribute("oninput", "update(this)")
if (i == "earLength") {
d.setAttribute("min", 2.5 * r);
}
$("inputList").appendChild(d);
}
}
5.参数随机生成
为了让程序更有趣,随机生成参数画一个兔头。
调整某些参数的同时,要注意有些关联参数的最大值上限会发生变动,所以每次调整参数时,会调整相关参数的最大值上限,否则会出现类似嘴巴画在脸之外的效果。
另外参数发生变动后要更新滚动条的值,这也会将当前的参数直观的展示出来。
代码如下:
function random() {
for (var i in drawParams) {
if (i == "earLength") {
drawParams[i] = 2.5 * r + getRandomInt(r);
} else {
drawParams[i] = getRandomInt(drawParamsMax[i + "Max"]);
}
updateMax();
}
setIntputVal();
color = genRandomColor();
f();
}
看看随机的效果吧:
本文到这里就结束了,如果你觉得有趣,不妨点个赞。