用canvas画布绘制一棵静态树
本帖最后由 马黑黑 于 2024-4-19 23:43 编辑 <br /><br /><style>.papa { font: normal 18px/24px sans-serif; }
.papa p { margin: 12px 0; }
.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>递归指的是一个函数在执行过程中调用自己、直至执行条件满足时终止函数的自执行。我们接触得最多的应该就是请求关键帧动画,我们把动画的执行机制做成一个函数,在函数内部使用 requestAnimationFrame() 反复调用该函数:</p>
<div class='mum'>
<cl-cd data-idx="1"><span class="tBlue">let</span> raf = null; <span class="tGreen">//操作动画的开关</span></cl-cd>
<cl-cd data-idx="2"> </cl-cd>
<cl-cd data-idx="3"><span class="tBlue">let</span> <span class="tRed">render</span> = () => {</cl-cd>
<cl-cd data-idx="4"> <span class="tGreen">/* ... 这里是运行动画的代码 */</span></cl-cd>
<cl-cd data-idx="5"> raf = requestAnimationFrame(<span class="tRed">render</span>); <span class="tGreen">//通过请求关键帧动画调用函数自身</span></cl-cd>
<cl-cd data-idx="6">};</cl-cd>
</div>
<p>requestAnimationFrame() 方法是JS封装好的以显示刷新率为运行频率的API,它在函数 render() 的内部调用了函数 render() ;自身,使得函数要执行的任务得以不间断地执行。上例没有在函数内部提供任务终结的直接出口,而是通过请求关键帧动画标识 raf 用做开关,以便可以在函数的外部对动画的运行、暂停进行有效控制。不过地道的递归函数则应在函数内部提供出口条件,换言之,函数自身应该具备终止执行任务的条件,为此,我们在设计递归函数时,除了可以像上例那样可以在外部开、关函数的运行外,还应有能力在函数内部设立开、关逻辑清晰的运行机制,让递归函数适时终止自执行。以下例子,函数需要一个整数型参数,函数的任务是1加到该参数,就是等差数列求和,数学家高斯小时候做的1加到100的那个问题:</p>
<div class='mum'>
<cl-cd data-idx="1"><span class="tBlue">let</span> <span class="tRed">add</span> = (num) => {</cl-cd>
<cl-cd data-idx="2"> <span class="tBlue">if</span>(num <= 0) <span class="tBlue">return</span> 0;</cl-cd>
<cl-cd data-idx="3"> console.log(`${num} + add(${num} - 1)`);<span class="tGreen">//本行测试 :打印运算式子</span></cl-cd>
<cl-cd data-idx="4"> <span class="tBlue">return</span> num + <span class="tRed">add(num - 1)</span>;</cl-cd>
<cl-cd data-idx="5">};</cl-cd>
<cl-cd data-idx="6">console.log(add(100));</cl-cd>
</div>
<p>add(num) 函数首先设立一个条件,即代码第2行,如果参数 num ≤ 0 将返回 0,这个条件一旦满足,函数就会退出运行。第 4 行代码,整体意思是返回 num + add(num -1),这里信息量很庞大:return 是返回结果;而结果,首先是 num 自身,其次是它依次通过修改参数为 num - 1 来反复调用函数自身、直至参数 num ≤ 1 时把第二行的返回值也加上就退出累加工作。代码第 6 行,运行函数 add(num),传参是 100,并打印出最终累加结果。</p>
<p>需要特别注意,递归函数必须设计出口,以逻辑清晰的条件为临界,令任务执行完毕跳出任务的执行。递归递归,递是传递,将传递出去的指令一一落实,归就是回归,指令执行到满足条件了就要回头,不要无休止地干下去。</p>
<p>利用递归函数,我们可以在canvas画布上画一棵树,思路是这样子:</p>
<blockquote>
① 设计一个 draw 函数,需要 5 个参数:树干起始点XY坐标 sx 和 sy、树干单节长度 len、树干宽度 width、树干折向角度 deg;<br>
② 接力绘制树干,每一次都将画布坐标系位移到(sx,sy)处、旋转画布坐标系 deg 个弧度。首次在外部调用函数时使用实数参数,内部的递归调用则依据情况按一定系数调整各个参数;<br>
③ 以 len 参数即一节树干(后来变为树枝)的长度为出口参数,内部递归调用乘上一个小于 1 的参数,这样它不断变短——实际上从第二次开始它们变成了分岔的树枝——,当它短到一定的程度,退出递归调用,退出时还有一个重要的任务,绘制果实;<br>
④ 函数内部要两次递归调用函数自身,实现树枝左右折向,同时也令绘制情景更为复杂化;<br>
⑤ 我们需要树枝的折向角度不那么一致,以避免树的成品过于对称。<br>
</blockquote>
<p>这样绘制出来的树不带叶子,适合在沙漠中生长,其果实可以加工成黑石浓缩饮料,价值连城。下面是完整代码:</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">"800"</span> height=<span class="tMagenta">"500"</span> style=<span class="tMagenta">"<span class="tBlue">border:</span> 1px solid gray"</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>
<cl-cd data-idx="6"> </cl-cd>
<cl-cd data-idx="7"><span class="tBlue">let</span> rad = () => <span class="tRed">Math</span>.random() * 10 + 10; <span class="tGreen">//获取10~20的弧度</span></cl-cd>
<cl-cd data-idx="8"> </cl-cd>
<div class="tGreen"><cl-cd data-idx="9">/* 绘制函数 以画树干为主体绘制任务,通过递归调用函数自身,变树干为树枝</cl-cd>
<cl-cd data-idx="10"> 当树干长度低于 15 改画随机颜色的果实并退出绘制任务</cl-cd>
<cl-cd data-idx="11">*/</cl-cd></div>
<cl-cd data-idx="12"><span class="tBlue">let</span> draw = (sx,sy,len,width,deg) => {</cl-cd>
<cl-cd data-idx="13"> ctx.strokeStyle = <span class="tMagenta">'olivedrab'</span>;</cl-cd>
<cl-cd data-idx="14"> ctx.lineWidth = width;</cl-cd>
<cl-cd data-idx="15"> ctx.lineCap = <span class="tMagenta">'square'</span>;</cl-cd>
<cl-cd data-idx="16"> ctx.lineJoin = <span class="tMagenta">'miter'</span>;</cl-cd>
<cl-cd data-idx="17"> ctx.beginPath();</cl-cd>
<cl-cd data-idx="18"> ctx.save();</cl-cd>
<cl-cd data-idx="19"> ctx.translate(sx,sy);</cl-cd>
<cl-cd data-idx="20"> ctx.rotate(deg * <span class="tRed">Math</span>.PI / 180);</cl-cd>
<cl-cd data-idx="21"> ctx.moveTo(0, 0);</cl-cd>
<cl-cd data-idx="22"> ctx.lineTo(0, -len);</cl-cd>
<cl-cd data-idx="23"> ctx.stroke();</cl-cd>
<cl-cd data-idx="24"> </cl-cd>
<cl-cd data-idx="25"> <span class="tBlue">if</span>(len < 15) {</cl-cd>
<cl-cd data-idx="26"> ctx.beginPath();</cl-cd>
<cl-cd data-idx="27"> ctx.arc(0, -len, 6, 0, deg * <span class="tRed">Math</span>.PI * 2, false);</cl-cd>
<cl-cd data-idx="28"> ctx.fillStyle = `#${<span class="tRed">Math</span>.random().toString(16).substr(-6)}`;</cl-cd>
<cl-cd data-idx="29"> ctx.fill();</cl-cd>
<cl-cd data-idx="30"> ctx.restore();</cl-cd>
<cl-cd data-idx="31"> <span class="tBlue">return</span>; </cl-cd>
<cl-cd data-idx="32"> }</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(0, -len, len * 0.8, width * 0.8, <span class="tRed">deg + rad()</span>);</cl-cd>
<cl-cd data-idx="36"> draw(0, -len, len * 0.8, width * 0.8, <span class="tRed">deg - rad()</span>);</cl-cd>
<cl-cd data-idx="37"> </cl-cd>
<cl-cd data-idx="38"> ctx.restore();</cl-cd>
<cl-cd data-idx="39">};</cl-cd>
<cl-cd data-idx="40"> </cl-cd>
<cl-cd data-idx="41"><span class="tGreen">//从函数外部调用绘制函数</span></cl-cd>
<cl-cd data-idx="42">draw(ww / 2, hh, hh / 4.5, 10, 0);</cl-cd>
<cl-cd data-idx="43"> </cl-cd>
<cl-cd data-idx="44"><<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> 来自大漠的成吉思汗贴子里知道了回调函数,
这个贴子依然是大漠风,用递归函数绘制的黑石沙棘林。。{:4_170:}
函数的样式好多,功能好强,老师运用的好灵活。。{:4_199:}
思路和代码对应,模块很清晰。。
如看看高手画画,三两下就画出惊世之作,鼓掌赞叹。。 {:4_170:}黑石沙棘原浆预定一箱。。 小辣椒就看看,以后看看黑黑的作品效果 南无月 发表于 2024-4-19 20:52
来自大漠的成吉思汗贴子里知道了回调函数,
这个贴子依然是大漠风,用递归函数绘制的黑石沙棘林。。{:4_17 ...
{:4_189:} 小辣椒 发表于 2024-4-19 21:26
小辣椒就看看,以后看看黑黑的作品效果
成品在代码里 南无月 发表于 2024-4-19 20:57
黑石沙棘原浆预定一箱。。
首次下单3万一箱 马黑黑 发表于 2024-4-19 21:32
首次下单3万一箱
这么贵,杀新啊。。{:4_170:} 南无月 发表于 2024-4-19 21:33
这么贵,杀新啊。。
正常价是5W 马黑黑 发表于 2024-4-19 21:31
回调函数,递归函数,这么形象的名字是原本就有的还是你取的。 马黑黑 发表于 2024-4-19 21:34
正常价是5W
居然打了骨折。{:4_170:} 南无月 发表于 2024-4-19 21:35
居然打了骨折。
就是就是,知足吧 南无月 发表于 2024-4-19 21:34
回调函数,递归函数,这么形象的名字是原本就有的还是你取的。
这是原有的,就这么定着 马黑黑 发表于 2024-4-19 21:35
就是就是,知足吧
知足者常乐。。。喝一瓶才乐。{:4_170:} 南无月 发表于 2024-4-19 21:37
知足者常乐。。。喝一瓶才乐。
浓缩了五万倍,买一箱,喝一辈子还有遗产{:4_170:} 这棵树太美了,这样的运算喝设计好强大{:4_199:} 马黑黑 发表于 2024-4-19 21:36
这是原有的,就这么定着
好形象的名字。。功能也强大。。一下子觉得挺可爱的这些函数 马黑黑 发表于 2024-4-19 21:38
浓缩了五万倍,买一箱,喝一辈子还有遗产
太厉害了。这可是比黄金还保值。。谁也不要告诉,再来一箱。。{:4_170:} 南无月 发表于 2024-4-19 22:20
太厉害了。这可是比黄金还保值。。谁也不要告诉,再来一箱。。
俺这就安排