马黑黑 发表于 2024-3-23 11:58

做一个canvas时钟(三)

<style>
.mama { font: normal 18px / 26px sans-serif; }
.mama p { margin: 12px 0; }
.mama mark { padding: 0 6px; background: lightblue; }
.wrap { margin: 20px auto 0; text-align: center; }
#canv, #canv1 { 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="mama">

<p>我们在上一讲讨论如何绘制矩形,我们成功地把矩形画在了画布上,矩形的左边处在画布的正中央、整个身子垂直居中,它指向三点钟方向。若我们需要它指向十二点钟方向,我们是不是要画一个宽10px、高100px的矩形?这好像可行,可是,指向两点钟方向呢?显然,这样的矩形画不出来,而本教程铁了心要用矩形做指针,这怎么办?</p>
<p>办法是有的:我们永远使用像下面那样的方式去画矩形,所画的矩形永远指向三点钟方向。比如画时针,不是用 fillRect 方法就是用 strokeRect 方法或者两种方法同时使用,当然矩形的宽高我们会根据需要调整,起笔的XY坐标数据也可能略有不同,但总体上就从画布的中央区域发起、指向三点钟方向:</p>
<div class='mum'>
<cl-cd data-idx="1"><span class="tGreen">//从 (0,-10) 开始绘制矩形,矩形宽高为 100*20</span></cl-cd>
<cl-cd data-idx="2">ctx.strokeRect(0, -10, 100, 20);</cl-cd>
<cl-cd data-idx="3">ctx.fillRect(0, -10, 100, 20);</cl-cd>
</div>
<p>上一讲,我们从画布的 (150,150) 处起笔,虽然达到了预期目的,但上面从中心区域起笔的画法更科学或会得更省事(这一点在后续画其他元素时更能体会到)。所以这里,先解决第一个问题:<mark>更换画布坐标体系</mark>。canvas画布作为HTML的一个元素,它的坐标体系和其他HTML元素一样,始于左上角(0,0),上面的代码,我们从X方向的 0 开始起笔没问题,但Y方向的 -10 则意味着矩形有一半的厚度将看不到。若我们<span class="tRed">临时</span>更改画布的坐标系,将其变为平面正交坐标系即卡迪尔坐标系(国内翻译成笛卡尔坐标系),就是XY坐标轴相交于元素正中央的那种坐标系,那问题就解决了。方法其实很简单:</p>
<div class='mum'>
<cl-cd data-idx="1"><span class="tGreen">//转换画布坐标系 :画布原点设在 300*300 画布的中心</span></cl-cd>
<cl-cd data-idx="2">ctx.translate(150,150);</cl-cd>
</div>
<p>translate 大家可能不陌生,我们在CSS的 transform 属性中经常看到它的身影,它是一个平移指令,可以令元素朝xy方向移动。不过canvas画布的上述 translate(x,y) 指令不隶属与CSS的transform属性,它是独立存在的,它用来改变canvas画布的坐标系,通过画笔操作变量 ctx(ctx 是我们给的命名)来实现画布坐标系的迁移。当我们用 translate() 令画布的坐标系迁移到画布中心点,那么,画笔在 (0,-10) 处起笔绘制的矩形,效果就如上一讲的最后一个示例展现的那样,矩形处在我们所需要的位置——起笔于元素中心点区域且垂直居中。下一步就是要解决的第二个问题,旋转绘制对象,这用到 rotate 方法,<mark>旋转画布坐标系</mark>——这里注意不是旋转矩形——其道理和移动画布坐标系一样:</p>
<div class='mum'>
<cl-cd data-idx="1"><span class="tGreen">//改变画布坐标系</span></cl-cd>
<cl-cd data-idx="2">ctx.translate(150,150);</cl-cd>
<cl-cd data-idx="3"><span class="tGreen">//旋转画布坐标系</span></cl-cd>
<cl-cd data-idx="4">ctx.rotate(1.5707963267948966);</cl-cd>
<cl-cd data-idx="5"><span class="tGreen">//绘制矩形</span></cl-cd>
<cl-cd data-idx="6">ctx.fillRect(0, -10, 100, 20);</cl-cd>
<cl-cd data-idx="7">ctx.strokeRect(0, -10, 100, 20);</cl-cd>
</div>
<p>如下所示,矩形从指向三点钟方向变成了指向十二点钟方向,这是移动和旋转画布坐标系得到的效果,而不是移动和旋转矩形:</p>
<div class="wrap"><canvas id="canv" width="300" height="300"></canvas></div>
<p>眼尖的小童鞋可能已经心里嘀咕着:rotate 参数为啥是那么长的一串数字呢?嗯,这串数字不是旋转的角度,是弧度。canvas画布的 rotate 方法用到的参数单位是弧度,这和CSS的 transform 的 rotate 方法用的是角度不一样,后面的章节我们会讨论角度转弧度的问题,本讲先把精力放在画布坐标系转换(translate,移动)和画布坐标系旋转(rotate)之上。</p>
<p>如果仅仅是画一个矩形,我们怎么变换画布的坐标系、怎么旋转画布的坐标系都没有问题,问题是,我们要画的东西会很多,单单时钟指针就有三根,它们极少存在同一个旋转角度(弧度)的现象。所以,我们前面说到是<span class="tRed">临时变换坐标</span>,画完需要移动一定距离和旋转一定角度的矩形后应立马还原画布的上一个状态(可能是初始状态),然后继续下一个绘制任务。这就涉及到画布设置状态的保存与还原,并不难,也是通过画笔变量 ctx 来实现:</p>
<div class='mum'>
<cl-cd data-idx="1"><span class="tGreen">//绘制前保存画布状态</span></cl-cd>
<cl-cd data-idx="2">ctx.save();</cl-cd>
<cl-cd data-idx="3"><span class="tGreen">//这里设置画布,比如设置画笔颜色、线条尺寸、变换坐标系、旋转画布等</span></cl-cd>
<cl-cd data-idx="4"><span class="tGreen">//这里绘制图形</span></cl-cd>
<cl-cd data-idx="5"><span class="tGreen">//然后还原画布到 save 的状态</span></cl-cd>
<cl-cd data-idx="6">ctx.restore();</cl-cd>
</div>
<p>save() 是保存,restore() 是还原,一般地,每一次使用 save() 是保存绘制新图形前的画布设置状态,绘制好新图形后,应立马使用 restore() 进行还原,还原的是画新图像前的画布状态,这是保证画布设置状态不会出现逻辑错误从而可能导致画布上出现画面混乱现象的好习惯。特别要注意的是,save() 和 restore() 的对象是指画布的设置状态,像画笔的颜色和线条尺寸、画布坐标体系的移动和旋转等等这些,而不是保存和恢复绘制出来的图形——绘制出来的图形,在同一个时间节点上的绘制都会一同出现,其形态如颜色、因画布坐标系移位和旋转而变更了位置与朝向等等都保持在画布上,直至被擦除。每一次 save() 操作浏览器都会在内存堆栈中保存新绘制操作前的画布状态,而当 ctx 运行了 restore(),系统会将上一次的保存从栈中取出以恢复前面保存的画布状态,所以save() 和 restore() 应当是以前呼后应的方式出现,并且在复杂的canvas创作中 save() 和 restore() 可能还会存在嵌套,有时候 save() 两次 restore() 两次也是根据需要来的,好在我们的时钟创作不算太复杂,加之我们将用函数封装各种图形的画法,可以有效避免保存与还原画布状态的复杂逻辑与嵌套。下面我们就来画三个长短不一、厚度(高度)不同、朝向各异的矩形(指针):</p>
<div class="wrap"><canvas id="canv1" width="300" height="300"></canvas></div>
<p>上述效果的完整代码如下:</p>
<div class='mum'>
<cl-cd data-idx="1">&lt;<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">"300"</span>&gt;&lt;<span class="tDarkRed">/canvas</span>&gt;</cl-cd>
<cl-cd data-idx="2"> </cl-cd>
<cl-cd data-idx="3">&lt;<span class="tDarkRed">script</span>&gt;</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">&nbsp;</cl-cd>
<div class="tGreen"><cl-cd data-idx="6">/* 绘制指针函数</cl-cd>
<cl-cd data-idx="7">   x, y - 矩形 XY坐标</cl-cd>
<cl-cd data-idx="8">   w, h - 矩形宽高</cl-cd>
<cl-cd data-idx="9">   rad - 矩形旋转的弧度</cl-cd>
<cl-cd data-idx="10">   color - 矩形填充色</cl-cd>
<cl-cd data-idx="11">*/</cl-cd></div>
<cl-cd data-idx="12"><span class="tBlue">let</span> draw_rect = (x, y, w, h, rad, color) =&gt; {</cl-cd>
<cl-cd data-idx="13">    ctx.save();</cl-cd>
<cl-cd data-idx="14">    ctx.fillStyle = color;</cl-cd>
<cl-cd data-idx="15">    ctx.translate(150,150);</cl-cd>
<cl-cd data-idx="16">    ctx.rotate(rad);</cl-cd>
<cl-cd data-idx="17">    ctx.fillRect(x,y,w,h);</cl-cd>
<cl-cd data-idx="18">    ctx.restore();</cl-cd>
<cl-cd data-idx="19">};</cl-cd>
<cl-cd data-idx="20">&nbsp;</cl-cd>
<cl-cd data-idx="21">draw_rect(0, -5, 100, 10, 0, <span class="tMagenta">'tan'</span>);</cl-cd>
<cl-cd data-idx="22">draw_rect(0, -3, 120, 6, -1.5707963267948966, <span class="tMagenta">'lightblue'</span>);</cl-cd>
<cl-cd data-idx="23">draw_rect(0, -2, 130, 4, 4.39822971502571, <span class="tMagenta">'lightgreen'</span>);</cl-cd>
<cl-cd data-idx="24">&lt;<span class="tDarkRed">/script</span>&gt;</cl-cd>
</div>
<p>上述代码,我们将绘制矩形封装成一个自定义函数 draw_rect(),所需参数较多但一目了然:x、y、w、h 是画矩形必须要有的数据(XY坐标与宽高),rad 是旋转弧度,color 是填充色。我们的计划是只用 fillRect() 来绘制指针,所以基于 strokeRect() 的参数省掉了(将来若有描边需求再加上也不是个事儿)。封装的好处多多,随便说两点,其一,可重复调用,例如画三个指针,每次以不同的参数调用函数即可,不必每一次绘制指针都需要手动重复函数里的操作流程,方便且可以节省代码量;其二,save() 和 restore() 画布状态不会被遗漏,因为它们放在函数代码的第一行和最末一行,每一次对函数的调用都会执行一次完整的画布状态的保存和还原。最后我们三次调用函数 draw_rect(),画出了三个尺寸、颜色以及走向到不同的指针。</p>
<p>[小结】本讲重点在画布坐标系的位置变换和旋转,内容相当抽象,可能和大家接触过的知识体系全然不同。我们需要明白:canvas画布原始坐标系的起点是在画布元素自身的左上角 (0,0),出于绘图需要,比如 ① 我们希望在画布的中心起笔绘制图形,我们可以<span class="tRed">临时</span>地将坐标系移到画布的中心,使之成为卡迪尔坐标系,② 我们希望总是绘制相同朝向的图形、然后让它们旋转一定角度以达到朝向不同的绘制目的,我们可以让画布坐标系旋转一定弧度。这些,包括画笔颜色、线条粗细等基于画布、画笔的设置层面的操作,可以用 save() 保存前面的设置,绘制完了所需图形后再用 restore() 还原上一次的画布设置状态。所有这一切,看起来操作极其细腻繁琐,但绘画本身就是这么一个细腻而繁琐的创作过程,熟悉并习惯后便可在画布上从容创作。</p>

</div>

<script>
let ctx = canv.getContext('2d');
ctx.fillStyle = 'tan';
ctx.strokeStyle = 'red';
ctx.save();
ctx.translate(150,150);
ctx.rotate(-1.5707963267948966);
ctx.fillRect(0, -10, 100, 20);
ctx.strokeRect(0, -10, 100, 20);
ctx.restore();
let ctx1 = canv1.getContext('2d');
let draw_rect = (x, y, w, h, rad, color) => {
    ctx1.save();
    ctx1.fillStyle = color;
    ctx1.translate(150,150);
    ctx1.rotate(rad);
    ctx1.fillRect(x,y,w,h);
    ctx1.restore();
};
draw_rect(0, -5, 100, 10, 0, 'tan');
draw_rect(0, -3, 120, 6, -1.5707963267948966, 'lightblue');
draw_rect(0, -2, 130, 4, 4.39822971502571, 'lightgreen');
</script>

南无月 发表于 2024-3-23 17:32

都是三点钟方向的矩形,通过改变弧度和颜色及长宽度,形成时分秒指针。。
draw_rect()这个封装,小白能看懂{:4_173:}就很开心。。

南无月 发表于 2024-3-23 17:35

其二,save() 和 restore() 画布状态不会被遗漏,因为它们放在函数代码的第一行和最末一行,每一次对函数的调用都会执行一次完整的画布状态的保存和还原

是不是可以想像为三根针,就有三个画布,像透明幻灯片似的叠起来。。第二个画布转一点,第三个画布多转一 点。。。
不是针在转,是画布在转
可以这么理解么{:4_173:}

马黑黑 发表于 2024-3-23 18:06

南无月 发表于 2024-3-23 17:32
都是三点钟方向的矩形,通过改变弧度和颜色及长宽度,形成时分秒指针。。
draw_rect()这个封装,小白能看 ...

看懂是入门的开始。

马黑黑 发表于 2024-3-23 18:09

南无月 发表于 2024-3-23 17:35
其二,save() 和 restore() 画布状态不会被遗漏,因为它们放在函数代码的第一行和最末一行,每一次对函数的 ...

实际上你应该理解为图层。画布就是一个。

画时钟,用一个图层,这个图层移位和旋转了坐标系;分针、秒针同理。这就有了三个图层,它们叠加在一起成为一个整体。

绿叶清舟 发表于 2024-3-23 20:34

这个看上去很复杂了

南无月 发表于 2024-3-23 21:36

马黑黑 发表于 2024-3-23 18:06
看懂是入门的开始。

{:4_199:}反正感觉能看懂就很开心

南无月 发表于 2024-3-23 21:37

马黑黑 发表于 2024-3-23 18:09
实际上你应该理解为图层。画布就是一个。

画时钟,用一个图层,这个图层移位和旋转了坐标系;分针、秒 ...

透明幻灯片说法就是用来理解PS图层时用的。。
老师这么说看来我理解没错,同样适用于这里。。

马黑黑 发表于 2024-3-23 21:59

南无月 发表于 2024-3-23 21:37
透明幻灯片说法就是用来理解PS图层时用的。。
老师这么说看来我理解没错,同样适用于这里。。

但不是多个画布。画布就一个。这个和ps图层概念是一脉相承的。

马黑黑 发表于 2024-3-23 21:59

南无月 发表于 2024-3-23 21:36
反正感觉能看懂就很开心

那必须的

马黑黑 发表于 2024-3-23 22:11

绿叶清舟 发表于 2024-3-23 20:34
这个看上去很复杂了

没啥的吧

南无月 发表于 2024-3-23 22:14

马黑黑 发表于 2024-3-23 21:59
但不是多个画布。画布就一个。这个和ps图层概念是一脉相承的。

那就更容易理解了。。
三个指针就是三个图层

南无月 发表于 2024-3-23 22:15

马黑黑 发表于 2024-3-23 21:59
那必须的

{:4_187:}老师教得好

马黑黑 发表于 2024-3-23 23:09

南无月 发表于 2024-3-23 22:15
老师教得好

不如小鸟儿起得早

马黑黑 发表于 2024-3-23 23:09

南无月 发表于 2024-3-23 22:14
那就更容易理解了。。
三个指针就是三个图层

就是这么个意思吧

南无月 发表于 2024-3-24 17:37

马黑黑 发表于 2024-3-23 23:09
不如小鸟儿起得早

晚睡早起不是个好习惯{:4_173:}

南无月 发表于 2024-3-24 17:38

马黑黑 发表于 2024-3-23 23:09
就是这么个意思吧

三个图层可以同名同姓{:4_173:}

马黑黑 发表于 2024-3-24 17:57

南无月 发表于 2024-3-24 17:38
三个图层可以同名同姓

也许可以吧

马黑黑 发表于 2024-3-24 17:57

南无月 发表于 2024-3-24 17:37
晚睡早起不是个好习惯

怕被虫子吃?

南无月 发表于 2024-3-24 19:54

马黑黑 发表于 2024-3-24 17:57
也许可以吧

好吧,时分秒就是同姓。。但不同名{:4_173:}
页: [1] 2 3 4 5
查看完整版本: 做一个canvas时钟(三)