兔头生成工具,快来捏个兔头吧!

lxf2023-03-13 07:40:01

我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛

马上就到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个参数分别为:

  1. relx: 右耳左曲线起始点x轴坐标
  2. rely: 右耳左曲线起始点y轴坐标
  3. reex: 右耳两条曲线的终点x轴坐标
  4. reey: 右耳两条虚线终点的y轴坐标
  5. relcx: 右耳左曲线控制点x轴坐标
  6. relcy: 右耳左曲线控制点y轴坐标
  7. rerx: 右耳右曲线起始点x轴坐标
  8. rery: 右耳右曲线起始点y轴坐标
  9. rercx: 右耳右曲线控制点x轴坐标
  10. 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();
    }

看看随机的效果吧:

兔头生成工具,快来捏个兔头吧! 兔头生成工具,快来捏个兔头吧!

本文到这里就结束了,如果你觉得有趣,不妨点个赞。