在canvas画布中绘制二次贝塞尔曲线
本帖最后由 马黑黑 于 2024-4-14 14:28 编辑 <br /><br /><style>.papa { font: normal 18px/24px sans-serif; }
.papa p { margin: 10px 0; }
.papa canvas { border: 1px solid gray; }
.mum { position: relative; margin: 0; padding: 10px; font: normal 16px/20px Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; color: black; background: rgba(240, 240, 240,.95); box-shadow: 2px 2px 4px gray; border: thick groove lightblue; border-radius: 6px; }
.mum ::selection { background-color: rgba(0,100,100,.35); }
.mum div { margin: 0; padding: 0; }
.mum cl-cd { display: block; position: relative; margin: 0 0 0 50px; padding: 0 0 0 10px; white-space: pre-wrap; overflow-wrap: break-word; border-left: 1px solid silver; }
.mum cl-cd::before { position: absolute; content: attr(data-idx); width: 50px; color: gray; text-align: right; transform: translate(-70px); }
.tRed { color: red; }
.tBlue { color: blue; }
.tGreen { color: green; }
.tDarkRed { color: darkred; }
.tMagenta { color: magenta; }
</style>
<div class="papa">
<p>二次贝塞尔曲线需要三个点的坐标数据:曲线起始点坐标(x1,y1)、曲线控制点坐标(cpx,cpy)和曲线终点坐标(x2,y2)。在 canvas 画布中,起始点坐标默认使用上一次绘制图形的路径终点坐标,没有路径终点坐标时起始点坐标为(0,0),也可以使用 moveTo(x,y) 指令重新定义。canvas 绘制二次贝塞尔曲线有专门的指令,<span class="tRed">quadraticCurseTo(cpx, cpy, endX, endY)</span>, 其中,cpx 和 cpy 为曲线控制点坐标,endX 和 endY 为曲线终点坐标,曲线起点坐标取上一回路径终点坐标或通过 moveTo(x,y) 指令定义。假设画笔标识为 ctx,我们来绘制一条二次贝塞尔曲线:</p>
<blockquote>ctx.moveTo(50, 100);<br>ctx.quadraticCurveTo(120, 0, 250, 100);</blockquote>
<p>这表示,曲线从(50,100)出发,到(250,100)停车,曲线的控制点是(120, 10),效果如下示例所示(小圆点用于标识三个点的位置):</p>
<canvas id="canv"></canvas>
<p>影响二次贝塞尔曲线的外观主要是控制点,上例中的小绿点就是控制点。控制点可以在曲线上,若此,绘制出来的将不是曲线而是一条直线;控制点可以为正负数、可以超出画布的范围,不同的数值将直接影响曲线的曲率和最终外观。</p>
<p>单纯使用二次贝塞尔曲线,我们就可以绘制出奇妙的动态图案。以下代码,我们仅来回改变控制点(cpx,cpy)中的cpy值,作出的动态图案十分令人惊艳,它还有无限可能:</p>
<div class='mum'>
<cl-cd data-idx="1"><<span class="tDarkRed">canvas</span> <span class="tRed">id</span>=<span class="tMagenta">"canv"</span> width=<span class="tMagenta">"300"</span> height=<span class="tMagenta">"200"</span>><<span class="tDarkRed">/canvas</span>></cl-cd>
<cl-cd data-idx="2"> </cl-cd>
<cl-cd data-idx="3"><<span class="tDarkRed">script</span>></cl-cd>
<cl-cd data-idx="4"><span class="tBlue">let</span> ctx = canv.getContext(<span class="tMagenta">'2d'</span>);</cl-cd>
<cl-cd data-idx="5"><span class="tBlue">let</span> ww = canv.width, hh = canv.height;</cl-cd>
<div class="tGreen"><cl-cd data-idx="6">/* 全局变量</cl-cd>
<cl-cd data-idx="7"> ctrY :控制点Y坐标</cl-cd>
<cl-cd data-idx="8"> step :控制点Y坐标变化幅度</cl-cd>
<cl-cd data-idx="9"> total :曲线总数</cl-cd>
<cl-cd data-idx="10"> raf :关键帧动画标识</cl-cd>
<cl-cd data-idx="11">*/</cl-cd></div>
<cl-cd data-idx="12"><span class="tBlue">let</span> ctrY = hh / 2, step = 0.25, total = 20, raf = null;</cl-cd>
<cl-cd data-idx="13"> </cl-cd>
<cl-cd data-idx="14"><span class="tGreen">//函数 :生成随机rgb颜色</span></cl-cd>
<cl-cd data-idx="15"><span class="tBlue">let</span> mkRgba = (opacity=0.5) => {</cl-cd>
<cl-cd data-idx="16"> <span class="tBlue">let</span> ar = .map((item) => <span class="tRed">Math</span>.floor(<span class="tRed">Math</span>.random() * item));</cl-cd>
<cl-cd data-idx="17"> ar.push( opacity || (.5 + <span class="tRed">Math</span>.random() * .5).toFixed(1));</cl-cd>
<cl-cd data-idx="18"> <span class="tBlue">return</span> <span class="tMagenta">'rgba('</span> + ar.join(<span class="tMagenta">','</span>) + <span class="tMagenta">')'</span>;</cl-cd>
<cl-cd data-idx="19">}</cl-cd>
<cl-cd data-idx="20"> </cl-cd>
<cl-cd data-idx="21"><span class="tGreen">//创建一个曲线类</span></cl-cd>
<cl-cd data-idx="22"><span class="tBlue">class </span>curveLine {</cl-cd>
<cl-cd data-idx="23"> <span class="tGreen">//类构造</span></cl-cd>
<cl-cd data-idx="24"> constructor(x1, y1, cpx, cpy, x2, y2) {</cl-cd>
<cl-cd data-idx="25"> <span class="tBlue">this</span>.x1 = x1;</cl-cd>
<cl-cd data-idx="26"> <span class="tBlue">this</span>.y1 = y1;</cl-cd>
<cl-cd data-idx="27"> <span class="tBlue">this</span>.cpx = cpx;</cl-cd>
<cl-cd data-idx="28"> <span class="tBlue">this</span>.cpy = cpy;</cl-cd>
<cl-cd data-idx="29"> <span class="tBlue">this</span>.x2 = x2;</cl-cd>
<cl-cd data-idx="30"> <span class="tBlue">this</span>.y2 = y2;</cl-cd>
<cl-cd data-idx="31"> <span class="tBlue">this</span>.color = <span class="tMagenta">'green'</span>;</cl-cd>
<cl-cd data-idx="32"> <span class="tBlue">this</span>.lineWidth = 4;</cl-cd>
<cl-cd data-idx="33"> };</cl-cd>
<cl-cd data-idx="34"> <span class="tGreen">//类的绘制行为</span></cl-cd>
<cl-cd data-idx="35"> draw(ctx) {</cl-cd>
<cl-cd data-idx="36"> ctx.save();</cl-cd>
<cl-cd data-idx="37"> ctx.strokeStyle = <span class="tBlue">this</span>.color;</cl-cd>
<cl-cd data-idx="38"> ctx.lineCap = <span class="tMagenta">'round'</span>;</cl-cd>
<cl-cd data-idx="39"> ctx.lineWidth = <span class="tBlue">this</span>.lineWidth;</cl-cd>
<cl-cd data-idx="40"> ctx.beginPath();</cl-cd>
<cl-cd data-idx="41"> ctx.moveTo(<span class="tBlue">this</span>.x1, <span class="tBlue">this</span>.y1);</cl-cd>
<cl-cd data-idx="42"> ctx.quadraticCurveTo(<span class="tBlue">this</span>.cpx, <span class="tBlue">this</span>.cpy, <span class="tBlue">this</span>.x2, <span class="tBlue">this</span>.y2);</cl-cd>
<cl-cd data-idx="43"> ctx.stroke();</cl-cd>
<cl-cd data-idx="44"> ctx.closePath();</cl-cd>
<cl-cd data-idx="45"> ctx.restore();</cl-cd>
<cl-cd data-idx="46"> };</cl-cd>
<cl-cd data-idx="47">};</cl-cd>
<cl-cd data-idx="48"> </cl-cd>
<cl-cd data-idx="49"><span class="tGreen">//批量绘制曲线</span></cl-cd>
<cl-cd data-idx="50"><span class="tBlue">let</span> draw = () => {</cl-cd>
<cl-cd data-idx="51"> ctx.clearRect(0, 0, ww, hh);</cl-cd>
<cl-cd data-idx="52"> <span class="tBlue">let</span> add = ww - 20;</cl-cd>
<cl-cd data-idx="53"> <span class="tBlue">for</span>(<span class="tBlue">let</span> i = 0; i < total; i ++) {</cl-cd>
<cl-cd data-idx="54"> <span class="tBlue">let</span> cl = <span class="tBlue">new</span> curveLine(); <span class="tGreen">//创建曲线类实例</span></cl-cd>
<cl-cd data-idx="55"> cl.x1 = ww / 2;</cl-cd>
<cl-cd data-idx="56"> cl.y1 = hh - 5;</cl-cd>
<cl-cd data-idx="57"> cl.cpx = (ww + add) / total * i - add / 2 + 5;</cl-cd>
<cl-cd data-idx="58"> cl.cpy = ctrY;</cl-cd>
<cl-cd data-idx="59"> cl.x2 = ww / 2;</cl-cd>
<cl-cd data-idx="60"> cl.y2 = 5;</cl-cd>
<cl-cd data-idx="61"> cl.color = mkRgba(0.7);</cl-cd>
<cl-cd data-idx="62"> cl.lineWidth = 4;</cl-cd>
<cl-cd data-idx="63"> cl.draw(ctx); </cl-cd>
<cl-cd data-idx="64"> }</cl-cd>
<cl-cd data-idx="65">};</cl-cd>
<cl-cd data-idx="66"> </cl-cd>
<cl-cd data-idx="67"><span class="tGreen">//渲染效果</span></cl-cd>
<cl-cd data-idx="68"><span class="tBlue">let</span> render = () => {</cl-cd>
<cl-cd data-idx="69"> ctrY += step;</cl-cd>
<cl-cd data-idx="70"> <span class="tBlue">if</span>(ctrY > hh || ctrY < 0) step = -step;</cl-cd>
<cl-cd data-idx="71"> draw();</cl-cd>
<cl-cd data-idx="72"> raf = requestAnimationFrame(render); <span class="tGreen">//调用关键帧动画接口</span></cl-cd>
<cl-cd data-idx="73">};</cl-cd>
<cl-cd data-idx="74"> </cl-cd>
<cl-cd data-idx="75">render();</cl-cd>
<cl-cd data-idx="76"> </cl-cd>
<cl-cd data-idx="77"><span class="tGreen">//画布单击事件 :暂停或启用动画</span></cl-cd>
<cl-cd data-idx="78">canv.onclick = () => {</cl-cd>
<cl-cd data-idx="79"> raf = raf ? cancelAnimationFrame(raf) : requestAnimationFrame(render);</cl-cd>
<cl-cd data-idx="80">};</cl-cd>
<cl-cd data-idx="81"><<span class="tDarkRed">/script</span>></cl-cd>
</div>
<p>以上代码,可以保存为本地 .html 文档或将代码拿到 <a href="http://mhh.52qingyin.cn/api/pcode/" target="_blank">pencil code</a> 运行以查看效果,提醒一下,动画是可以暂停和继续运行的,方法是单击画布。</p>
</div>
<script>
let ctx = canv.getContext('2d');
let drawCurveLine = (x1,y1,cpx,cpy,x2,y2) => {
ctx.save();
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.quadraticCurveTo(cpx,cpy,x2,y2);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
let drawCircle = (x,y,r,color) => {
ctx.save();
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(x,y,r,0,2*Math.PI);
ctx.fill();
ctx.restore();
};
drawCurveLine(50, 100, 120, 10, 250, 100);
drawCircle(50, 100, 3, 'red');
drawCircle(120, 10, 3, 'green');
drawCircle(250, 100, 3, 'red');
</script>
黑黑辛苦,教程讲的很仔细{:4_199:} 预览了效果好像灯笼一样的在旋转。。。。 绘制了这么多二次贝塞尔曲线啊,组合在一起而且改变cpx的值,就能变成动画呢。
看着像大蒜头{:4_173:} cl.color = mkRgba(0.7);
这个不太懂呢。 红影 发表于 2024-4-14 16:02
cl.color = mkRgba(0.7);
这个不太懂呢。
mkRgba(opacity) 是调用前面写的函数,函数是用来生成随机rgba颜色的,唯一参数是透明度参数,可选。 小辣椒 发表于 2024-4-14 15:34
黑黑辛苦,教程讲的很仔细
{:4_190:} 红影 发表于 2024-4-14 15:57
绘制了这么多二次贝塞尔曲线啊,组合在一起而且改变cpx的值,就能变成动画呢。
看着像大蒜头
{:4_196:} 小辣椒 发表于 2024-4-14 15:34
预览了效果好像灯笼一样的在旋转。。。。
{:4_173:} 以下代码,我们仅来回改变控制点(cpx,cpy)中的cpy值,作出的动态图案十分令人惊艳,它还有无限可能
画布能画出各种漂亮的图案。。真让人惊叹。。 南无月 发表于 2024-4-14 18:18
以下代码,我们仅来回改变控制点(cpx,cpy)中的cpy值,作出的动态图案十分令人惊艳,它还有无限可能
画 ...
{:4_191:} 马黑黑 发表于 2024-4-14 17:49
mkRgba(opacity) 是调用前面写的函数,函数是用来生成随机rgba颜色的,唯一参数是透明度参数,可选。
原来是调用的,还以为咋一句就能控制颜色了呢{:4_173:}
谢谢黑黑的解答{:4_187:} 马黑黑 发表于 2024-4-14 17:50
不好意思,我这个比喻不太美{:4_173:} 红影 发表于 2024-4-14 19:57
不好意思,我这个比喻不太美
很美很美 红影 发表于 2024-4-14 19:56
原来是调用的,还以为咋一句就能控制颜色了呢
谢谢黑黑的解答
看代码要看前前后后 马黑黑 发表于 2024-4-14 20:43
很美很美
大蒜头也美啊{:4_170:} 马黑黑 发表于 2024-4-14 20:43
看代码要看前前后后
是的,通过这个记住教训了,不能只看一句呢{:4_173:} 红影 发表于 2024-4-14 21:45
是的,通过这个记住教训了,不能只看一句呢
其实也简单:你看不懂,选择 mkRgba ,就按一下 Ctrl + F,你就会看到了 红影 发表于 2024-4-14 21:44
大蒜头也美啊
你不说美,山东人跟你急 马黑黑 发表于 2024-4-14 19:51
晚上喝啤的