如何在canvas画布中旋转图像
<style>.mama { margin: auto; font: normal 18px/26px sans-serif; }
.mama p { margin: 12px 0; }
.mama canvas { display: block; margin: 12px auto; border: 1px solid gray; }
.btnwrap { margin: auto; width: 400px; }
.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>canvas画布要绘制旋转的图像,它不是旋转图像对象,它没有这个能力,它要做的是临时旋转绘画坐标系,再绘制图像。我们来看看在 400*400 的画布上绘制于画布中心的小矩形如何旋转:</p>
<canvas id="canv1" width="400" height="400"></canvas>
<div class="btnwrap">
<label for="range1">调节矩形旋转角度:</label>
<input id="range1" type="range" min="-45" max="45" value="0" />
<output id="data1">0</output>
</div>
<p>尽管我们说调节矩形旋转角度——这确实也是我们直观观察到的效果——,在画布上旋转 100*40 的小矩形,实现机制却是要旋转画布坐标系。我们可以通过核心代码来理解这个问题:</p>
<div class='mum'>
<cl-cd data-idx="1">ctx.clearRect(0, 0, 400, 400); <span class="tGreen">//把黑板擦干净</span></cl-cd>
<cl-cd data-idx="2">ctx.save(); <span class="tGreen">//保存画笔状态</span></cl-cd>
<cl-cd data-idx="3">ctx.rotate(deg * <span class="tRed">Math</span>.PI / 180); <span class="tGreen">//旋转 deg 度</span></cl-cd>
<cl-cd data-idx="4">ctx.fillRect(x,y,w,h); <span class="tGreen">//绘制矩形 :(x,y)为矩形左上角坐标点,(w,h)为矩形宽高尺寸</span></cl-cd>
<cl-cd data-idx="5">ctx.restore(); <span class="tGreen">//还原画笔状态</span></cl-cd>
</div>
<p>核心代码是第三行,用 rotate() 方法旋转画笔的绘图坐标系,这个坐标系也可以理解为是画布的坐标系,我们在本文的讨论过程中这两种提法都有。rotate() 需要一个参数,旋转的弧度,因此参数里是一个式子,它将获取到的 deg 角度值换算为弧度值。</p>
<p>演示实例的效果表明,矩形不是在自己的原始位置上旋转,它所旋转的 0 以外的任意角度,都会伴随着位置移动。这是由于画笔旋转坐标系是有一个固定的原点,在画布的左上角,坐标值是(0,0),上例中矩形的每一次旋转都围绕那个点进行,因旋转而发生的移动轨迹是一个弧形路线。也许大家会觉得奇怪,旋转为什么会产生移动?回顾一下旋转的实现机制:是画布在虚拟地旋转,即,画布旋转它的画笔坐标系,这个旋转带动矩形跟着旋转并移位。画布虚拟旋转并在其上绘制图像,可以视为是一个动态的图层,因此旋转的过程我们看不到实质性的画布旋转,而是矩形的旋转与移位。</p>
<p>上面的图像旋转方式应该不是我们所需要的,往往,我们对旋转运动的需求是图像在原地旋转,绕图像自己的中心点进行。画布可以做到这一点,思路与流程严格按照下面的描述进行:</p>
<blockquote class="tBlue">① 用 translate(cx,cy) 方法临时迁移画布坐标系到图像的中心点;<br>② 旋转画布坐标系 deg 个角度:rotate(deg * Math.PI / 180);<br>③ 反向将画布坐标系移回初始坐标点 translate(-cx,-cy);<br>④ 绘制图像。</blockquote>
<p>步骤一需要获知画布坐标系原点移动到哪里,它应当与矩形的中心重合才能达到矩形原地旋转的目的,因此,(cx,cy) 既是新坐标系的原点,也是矩形的中心点。新坐标系原点、矩形中心点共同的坐标 (cx,cy) 和矩形自身绘制时的左上角坐标(x,y)以及矩形的宽高(w,h)存在这样的数学关系:</p>
<blockquote class="tRed">cx = x + w / 2<br>cy = y + h / 2</blockquote>
<p>即,矩形中心点坐标和画布新坐标系原点坐标 cx 值等于矩形左上角 x 坐标值加上矩形宽度的一半、cy 值等于矩形左上角坐标 y 坐标值加上矩形高度的一半。有了这个数据,步骤三画布坐标系原路返回就不是个问题 。</p>
<p>而步骤四,即绘制矩形,为什么要放在画布坐标系复原之后而不出之前呢?这是画布的思维异于常人的表现之一,它先把规划制定好,每一个规划细节都不遗漏地记录下来,规划做完了才一鼓作气把东西fill或stroke上去,而不管不顾常人的正常先后次序逻辑。切记:画布坐标系反向复原之后才能绘制矩形(或其它图像)。</p>
<p>这里扩展一下:如果旋转的是用 arc() 或 arcTo() 绘制成的圆,由于 arc 和 arcTo 使用圆心而不是左上角xy坐标值定位,圆的圆心即为画布坐标系移动的坐标值,省却了一些计算。</p>
<p>最后看效果:</p>
<canvas id="canv2" width="400" height="400"></canvas>
<div class="btnwrap">
<label for="range2">调节矩形旋转角度:</label>
<input id="range2" type="range" min="-360" max="360" value="0" />
<output id="data2">0</output>
</div>
<p>附:canvas画布绘制原地旋转圆源码</p>
<div class='mum'>
<cl-cd data-idx="1"><<span class="tDarkRed">style</span>></cl-cd>
<cl-cd data-idx="2"> .mama { <span class="tBlue">font:</span> normal 18px/26px sans-serif; }</cl-cd>
<cl-cd data-idx="3"> #canv { <span class="tBlue">display:</span> block; <span class="tBlue">margin:</span> auto; <span class="tBlue">border:</span> 1px solid gray; }</cl-cd>
<cl-cd data-idx="4"> .wrap { <span class="tBlue">margin:</span> 20px auto; <span class="tBlue">width:</span> 360px; }</cl-cd>
<cl-cd data-idx="5"> .tMid { <span class="tBlue">text-align:</span> center; }</cl-cd>
<cl-cd data-idx="6"><<span class="tDarkRed">/style</span>></cl-cd>
<cl-cd data-idx="7"> </cl-cd>
<cl-cd data-idx="8"><<span class="tDarkRed">div</span> class=<span class="tMagenta">"mama"</span>></cl-cd>
<cl-cd data-idx="9"> <<span class="tDarkRed">h</span>2 class=<span class="tMagenta">"tMid"</span>>在canvas上绘制绕圆心旋转的圆<<span class="tDarkRed">/h</span>2></cl-cd>
<cl-cd data-idx="10"> <<span class="tDarkRed">canvas</span> <span class="tRed">id</span>=<span class="tMagenta">"canv"</span>><<span class="tDarkRed">/canvas</span>></cl-cd>
<cl-cd data-idx="11"> <<span class="tDarkRed">div</span> class=<span class="tMagenta">"wrap"</span>></cl-cd>
<cl-cd data-idx="12"> <<span class="tDarkRed">label</span> <span class="tBlue">for</span>=<span class="tMagenta">"rngCx"</span>>调节圆心X坐标位置 :<<span class="tDarkRed">/label</span>></cl-cd>
<cl-cd data-idx="13"> <<span class="tDarkRed">input</span> <span class="tRed">id</span>=<span class="tMagenta">"rngCx"</span> type=<span class="tMagenta">"range"</span> min=<span class="tMagenta">"0"</span> max=<span class="tMagenta">"400"</span> value=<span class="tMagenta">"60"</span> /></cl-cd>
<cl-cd data-idx="14"> <<span class="tDarkRed">output</span> <span class="tRed">id</span>=<span class="tMagenta">"cxData"</span>>60<<span class="tDarkRed">/output</span>></cl-cd>
<cl-cd data-idx="15"> <<span class="tDarkRed">br</span>></cl-cd>
<cl-cd data-idx="16"> <<span class="tDarkRed">label</span> <span class="tBlue">for</span>=<span class="tMagenta">"rngCy"</span>>调节圆心Y坐标位置 :<<span class="tDarkRed">/label</span>></cl-cd>
<cl-cd data-idx="17"> <<span class="tDarkRed">input</span> <span class="tRed">id</span>=<span class="tMagenta">"rngCy"</span> type=<span class="tMagenta">"range"</span> min=<span class="tMagenta">"0"</span> max=<span class="tMagenta">"400"</span> value=<span class="tMagenta">"60"</span> /></cl-cd>
<cl-cd data-idx="18"> <<span class="tDarkRed">output</span> <span class="tRed">id</span>=<span class="tMagenta">"cyData"</span>>60<<span class="tDarkRed">/output</span>></cl-cd>
<cl-cd data-idx="19"> <<span class="tDarkRed">/div</span>></cl-cd>
<cl-cd data-idx="20"><<span class="tDarkRed">/div</span>></cl-cd>
<cl-cd data-idx="21"> </cl-cd>
<cl-cd data-idx="22"><<span class="tDarkRed">script</span>></cl-cd>
<cl-cd data-idx="23"> </cl-cd>
<cl-cd data-idx="24"><span class="tBlue">var</span> ctx = canv.getContext(<span class="tMagenta">'2d'</span>);</cl-cd>
<cl-cd data-idx="25"><span class="tBlue">var</span> w = canv.width = 400, h = canv.height = 400;</cl-cd>
<cl-cd data-idx="26"><span class="tBlue">var</span> deg = 0, cx = 60, cy = 60, r = 50, raf = null;</cl-cd>
<cl-cd data-idx="27"> </cl-cd>
<cl-cd data-idx="28"><span class="tGreen">/* 径向渐变 */</span></cl-cd>
<cl-cd data-idx="29"><span class="tBlue">var</span> gradient = ctx.createRadialGradient(190, 200, 15, 200, 200, 280);</cl-cd>
<cl-cd data-idx="30">gradient.addColorStop(0, <span class="tMagenta">'red'</span>);</cl-cd>
<cl-cd data-idx="31">gradient.addColorStop(.15, <span class="tMagenta">'orange'</span>);</cl-cd>
<cl-cd data-idx="32">gradient.addColorStop(.3, <span class="tMagenta">'yellow'</span>);</cl-cd>
<cl-cd data-idx="33">gradient.addColorStop(.45, <span class="tMagenta">'green'</span>);</cl-cd>
<cl-cd data-idx="34">gradient.addColorStop(.51,<span class="tMagenta">'cyan'</span>);</cl-cd>
<cl-cd data-idx="35">gradient.addColorStop(.85,<span class="tMagenta">'blue'</span>);</cl-cd>
<cl-cd data-idx="36">gradient.addColorStop(1,<span class="tMagenta">'purple'</span>);</cl-cd>
<cl-cd data-idx="37"> </cl-cd>
<cl-cd data-idx="38">ctx.fillStyle = gradient;</cl-cd>
<cl-cd data-idx="39"> </cl-cd>
<cl-cd data-idx="40">draw_degCircle();</cl-cd>
<cl-cd data-idx="41"> </cl-cd>
<cl-cd data-idx="42"><span class="tGreen">/* 滑杆输入事件 :改变圆心 */</span></cl-cd>
<cl-cd data-idx="43">rngCx.oninput = rngCy.oninput = <span class="tBlue">function</span>(e) {</cl-cd>
<cl-cd data-idx="44"> raf = cancelAnimationFrame(raf);</cl-cd>
<cl-cd data-idx="45"> cx = cxData.value = rngCx.value;</cl-cd>
<cl-cd data-idx="46"> cy = cyData.value = rngCy.value;</cl-cd>
<cl-cd data-idx="47"> draw_degCircle();</cl-cd>
<cl-cd data-idx="48">};</cl-cd>
<cl-cd data-idx="49"> </cl-cd>
<cl-cd data-idx="50"><span class="tGreen">/* 函数 :画绕圆心旋转的圆 */</span></cl-cd>
<cl-cd data-idx="51"><span class="tBlue">function</span> draw_degCircle() {</cl-cd>
<cl-cd data-idx="52"> ctx.clearRect(0,0,400,400);</cl-cd>
<cl-cd data-idx="53"> ctx.save();</cl-cd>
<cl-cd data-idx="54"> ctx.beginPath();</cl-cd>
<cl-cd data-idx="55"> ctx.translate(cx, cy);</cl-cd>
<cl-cd data-idx="56"> ctx.rotate(deg * <span class="tRed">Math</span>.PI / 180);</cl-cd>
<cl-cd data-idx="57"> ctx.translate(-cx, -cy);</cl-cd>
<cl-cd data-idx="58"> ctx.arc(cx, cy, r, 0, 2 * <span class="tRed">Math</span>.PI);</cl-cd>
<cl-cd data-idx="59"> ctx.fill();</cl-cd>
<cl-cd data-idx="60"> ctx.restore();</cl-cd>
<cl-cd data-idx="61"> deg = (deg + 1) % 360;</cl-cd>
<cl-cd data-idx="62"> raf = requestAnimationFrame(draw_degCircle);</cl-cd>
<cl-cd data-idx="63">};</cl-cd>
<cl-cd data-idx="64"> </cl-cd>
<cl-cd data-idx="65"><<span class="tDarkRed">/script</span>></cl-cd>
</div>
<p>代码可以复制到 <a href="http://mhh.52qingyin.cn/api/pcode/" target="_blank">pencil code</a> 运行以查看效果,也可以将代码存为本地HTML文档后运行。</p>
</div>
<script>
(function() {
let ctx1 = canv1.getContext('2d'), ctx2 = canv2.getContext('2d');
ctx1.fillStyle = ctx2.fillStyle = 'purple';
draw_rot_rect(ctx1, 150, 180, 100, 40, 0, false);
draw_rot_rect(ctx2, 60, 60, 100, 40, 0, true);
range1.oninput = function() {
let deg = data1.value = parseInt(this.value);
draw_rot_rect(ctx1, 150, 180, 100, 40, deg, false);
};
range2.oninput = function() {
let deg = data2.value = parseInt(this.value);
draw_rot_rect(ctx2, 50, 50, 100, 40, deg, true);
};
function draw_rot_rect(ctx,x,y,w,h,deg=0,moveNot=false) {
let cx = x + 50, cy = y + 20;
ctx.clearRect(0, 0, 400, 400);
draw_cross_line(ctx);
ctx.save();
if(moveNot) ctx.translate(cx, cy);
ctx.rotate(deg * Math.PI / 180);
if(moveNot) ctx.translate(-cx, -cy);
ctx.fillRect(x, y ,w, h);
ctx.restore();
};
function draw_cross_line(ctx) {
ctx.clearRect(0, 0, 400, 400);
ctx.save();
ctx.strokeStyle = 'gray';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(199.5, 0);
ctx.lineTo(199.5, 399.5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, 199.5);
ctx.lineTo(399.5, 199.5)
ctx.stroke();
ctx.restore();
};
})();
</script>
这个代码好玩,那个旋转的圆,在移动横向纵向位置时,颜色也跟着变化呢{:4_199:} canvas画布绘制旋转的图像,多了点步骤,但也有了更多的组合呢{:4_199:} 这个圆还能自己旋转。在xy等于200的时候,也就是居中时候,颜色也差不多居中,都几乎看不出旋转了呢。
又试了试,好像y200,,192左右,颜色最居中{:4_173:} 这个好玩,黑黑又带来新的知识新的效果,这个太棒了{:4_199:} 红影 发表于 2024-3-30 13:48
这个代码好玩,那个旋转的圆,在移动横向纵向位置时,颜色也跟着变化呢
渐变是整个画布的渐变,在不同区域呈现的图像,只要使用 gradient 填充或描边,色彩都不一样 红影 发表于 2024-3-30 13:55
这个圆还能自己旋转。在xy等于200的时候,也就是居中时候,颜色也差不多居中,都几乎看不出旋转了呢。
又 ...
好玩吧 红影 发表于 2024-3-30 13:50
canvas画布绘制旋转的图像,多了点步骤,但也有了更多的组合呢
对,有无限可能 红影 发表于 2024-3-30 13:55
这个好玩,黑黑又带来新的知识新的效果,这个太棒了
其实在做时钟过程中类似的也有,只是时钟的需求与别的不同,不变展示更多的东东 感觉我有缺课。。
var gradient = ctx.createRadialGradient(190, 200, 15, 200, 200, 280);
这个径向渐变这些数据是什么意思呢。。。
我看这个圆移到哪里,就显示哪里的渐变。。
感觉红黄变得最自然,其它颜色变化有点硬,想调一下,可惜不懂怎么调{:4_170:} gradient.addColorStop(0, 'red');
gradient.addColorStop(.15, 'orange');
gradient.addColorStop(.3, 'yellow');
gradient.addColorStop(.45, 'green');
gradient.addColorStop(.51,'cyan');
gradient.addColorStop(.85,'blue');
gradient.addColorStop(1,'purple');
还有就是这些颜色前面的数字,有的是整数,有的不是。。。也不知道是控制啥的。。{:4_173:} 我把小圆放大到半径300,显示了背景上所有渐变,发现里面不同色彩的圆,中心点不在一个点上啊。。。
还各自旋转,方向并不相同。。
看上去各转各的。。(咦,是不是因为中心点不在一个位置,大家围着自己的中心点转,所以就各转各的了)
这个有意思了。。
{:4_170:} 南无月 发表于 2024-3-30 17:12
我把小圆放大到半径300,显示了背景上所有渐变,发现里面不同色彩的圆,中心点不在一个点上啊。。。
还各 ...
画布就 400*400,圆的半径300的画,实际上这个圆有一部分在外面了,你能看到的是里面部分。由于画布设置了径向渐变,圆显示的色彩是画布渐变的色彩。径向渐变是一圈一圈的,所以你看到酱紫的效果 本帖最后由 马黑黑 于 2024-3-30 18:24 编辑
南无月 发表于 2024-3-30 17:07
gradient.addColorStop(0, 'red');
gradient.addColorStop(.15, 'orange');
gradient.addColorStop(.3, ' ...
这是配合前面一句设置画布的径向渐变,从 0 到 1 取值,其实就是 0% ~ 100% 这么个意思。这些的前面那句,是设置径向渐变的范围(具体看后面的回复) 马黑黑 发表于 2024-3-30 18:09
画布就 400*400,圆的半径300的画,实际上这个圆有一部分在外面了,你能看到的是里面部分。由于画布设置 ...
是的,有一部分在外面。半径200,稍移一下也到外面了。。
其实我是好奇,为了看背景渐变全景才设这么大的。。
有奇效{:4_173:} 马黑黑 发表于 2024-3-30 18:11
这是配合前面一句设置画布的径向渐变,从 0 到 1 取值,其实就是 0% ~ 100% 这么个意思。这些的前面那句 ...
.15是指宽度?还是指位置?
南无月 发表于 2024-3-30 18:12
是的,有一部分在外面。半径200,稍移一下也到外面了。。
其实我是好奇,为了看背景渐变全景才设这么大 ...
你想看完整 的背景,可以画一个矩形:
ctx.fillStyle = gradient;
ctx.fillRect(0,0,400,400);
把 draw_degCircle(); 暂时注释掉 马黑黑 发表于 2024-3-30 18:11
这是配合前面一句设置画布的径向渐变,从 0 到 1 取值,其实就是 0% ~ 100% 这么个意思。这些的前面那句 ...
var gradient = ctx.createRadialGradient(190, 200, 15, 200, 200, 280);
这六个数字是范围?{:4_173:}
之前有没有相关的贴子啊,我搜一下。。
缺课严重 马黑黑 发表于 2024-3-30 18:16
你想看完整 的背景,可以画一个矩形:
ctx.fillStyle = gradient;
矩形0.0位置旋转和以中心点位置旋转可以理解。。下方代里中这个圆的显示,是不是可以想象成一个以中心点向外径向渐变的方形颜色画布(隐藏的),圆的位置到哪里,就显示那一块的渐变。。还是红黄渐变比较自然。。
这是我在你网站里的回复,这样理解是错的么?{:4_170:} 马黑黑 发表于 2024-3-30 18:16
你想看完整 的背景,可以画一个矩形:
ctx.fillStyle = gradient;
大哥,我不会画的呀。{:4_170:}