马黑黑 发表于 2024-3-30 12:37

如何在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">&lt;<span class="tDarkRed">style</span>&gt;</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">&lt;<span class="tDarkRed">/style</span>&gt;</cl-cd>
<cl-cd data-idx="7">&nbsp;</cl-cd>
<cl-cd data-idx="8">&lt;<span class="tDarkRed">div</span> class=<span class="tMagenta">"mama"</span>&gt;</cl-cd>
<cl-cd data-idx="9">&nbsp; &nbsp; &lt;<span class="tDarkRed">h</span>2 class=<span class="tMagenta">"tMid"</span>&gt;在canvas上绘制绕圆心旋转的圆&lt;<span class="tDarkRed">/h</span>2&gt;</cl-cd>
<cl-cd data-idx="10">&nbsp; &nbsp; &lt;<span class="tDarkRed">canvas</span> <span class="tRed">id</span>=<span class="tMagenta">"canv"</span>&gt;&lt;<span class="tDarkRed">/canvas</span>&gt;</cl-cd>
<cl-cd data-idx="11">&nbsp; &nbsp; &lt;<span class="tDarkRed">div</span> class=<span class="tMagenta">"wrap"</span>&gt;</cl-cd>
<cl-cd data-idx="12">&nbsp; &nbsp; &nbsp; &nbsp; &lt;<span class="tDarkRed">label</span> <span class="tBlue">for</span>=<span class="tMagenta">"rngCx"</span>&gt;调节圆心X坐标位置 :&lt;<span class="tDarkRed">/label</span>&gt;</cl-cd>
<cl-cd data-idx="13">&nbsp; &nbsp; &nbsp; &nbsp; &lt;<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> /&gt;</cl-cd>
<cl-cd data-idx="14">&nbsp; &nbsp; &nbsp; &nbsp; &lt;<span class="tDarkRed">output</span> <span class="tRed">id</span>=<span class="tMagenta">"cxData"</span>&gt;60&lt;<span class="tDarkRed">/output</span>&gt;</cl-cd>
<cl-cd data-idx="15">&nbsp; &nbsp; &nbsp; &nbsp; &lt;<span class="tDarkRed">br</span>&gt;</cl-cd>
<cl-cd data-idx="16">&nbsp; &nbsp; &nbsp; &nbsp; &lt;<span class="tDarkRed">label</span> <span class="tBlue">for</span>=<span class="tMagenta">"rngCy"</span>&gt;调节圆心Y坐标位置 :&lt;<span class="tDarkRed">/label</span>&gt;</cl-cd>
<cl-cd data-idx="17">&nbsp; &nbsp; &nbsp; &nbsp; &lt;<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> /&gt;</cl-cd>
<cl-cd data-idx="18">&nbsp; &nbsp; &nbsp; &nbsp; &lt;<span class="tDarkRed">output</span> <span class="tRed">id</span>=<span class="tMagenta">"cyData"</span>&gt;60&lt;<span class="tDarkRed">/output</span>&gt;</cl-cd>
<cl-cd data-idx="19">&nbsp; &nbsp; &lt;<span class="tDarkRed">/div</span>&gt;</cl-cd>
<cl-cd data-idx="20">&lt;<span class="tDarkRed">/div</span>&gt;</cl-cd>
<cl-cd data-idx="21">&nbsp;</cl-cd>
<cl-cd data-idx="22">&lt;<span class="tDarkRed">script</span>&gt;</cl-cd>
<cl-cd data-idx="23">&nbsp;</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">&nbsp;</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">&nbsp;</cl-cd>
<cl-cd data-idx="38">ctx.fillStyle = gradient;</cl-cd>
<cl-cd data-idx="39">&nbsp;</cl-cd>
<cl-cd data-idx="40">draw_degCircle();</cl-cd>
<cl-cd data-idx="41">&nbsp;</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">&nbsp; &nbsp; raf = cancelAnimationFrame(raf);</cl-cd>
<cl-cd data-idx="45">&nbsp; &nbsp; cx = cxData.value = rngCx.value;</cl-cd>
<cl-cd data-idx="46">&nbsp; &nbsp; cy = cyData.value = rngCy.value;</cl-cd>
<cl-cd data-idx="47">&nbsp; &nbsp; draw_degCircle();</cl-cd>
<cl-cd data-idx="48">};</cl-cd>
<cl-cd data-idx="49">&nbsp;</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">&nbsp; &nbsp; ctx.clearRect(0,0,400,400);</cl-cd>
<cl-cd data-idx="53">&nbsp; &nbsp; ctx.save();</cl-cd>
<cl-cd data-idx="54">&nbsp; &nbsp; ctx.beginPath();</cl-cd>
<cl-cd data-idx="55">&nbsp; &nbsp; ctx.translate(cx, cy);</cl-cd>
<cl-cd data-idx="56">&nbsp; &nbsp; ctx.rotate(deg * <span class="tRed">Math</span>.PI / 180);</cl-cd>
<cl-cd data-idx="57">&nbsp; &nbsp; ctx.translate(-cx, -cy);</cl-cd>
<cl-cd data-idx="58">&nbsp; &nbsp; ctx.arc(cx, cy, r, 0, 2 * <span class="tRed">Math</span>.PI);</cl-cd>
<cl-cd data-idx="59">&nbsp; &nbsp; ctx.fill();</cl-cd>
<cl-cd data-idx="60">&nbsp; &nbsp; ctx.restore();</cl-cd>
<cl-cd data-idx="61">&nbsp; &nbsp; deg = (deg + 1) % 360;</cl-cd>
<cl-cd data-idx="62">&nbsp; &nbsp; raf = requestAnimationFrame(draw_degCircle);</cl-cd>
<cl-cd data-idx="63">};</cl-cd>
<cl-cd data-idx="64">&nbsp;</cl-cd>
<cl-cd data-idx="65">&lt;<span class="tDarkRed">/script</span>&gt;</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>

红影 发表于 2024-3-30 13:48

这个代码好玩,那个旋转的圆,在移动横向纵向位置时,颜色也跟着变化呢{:4_199:}

红影 发表于 2024-3-30 13:50

canvas画布绘制旋转的图像,多了点步骤,但也有了更多的组合呢{:4_199:}

红影 发表于 2024-3-30 13:55

这个圆还能自己旋转。在xy等于200的时候,也就是居中时候,颜色也差不多居中,都几乎看不出旋转了呢。
又试了试,好像y200,,192左右,颜色最居中{:4_173:}

红影 发表于 2024-3-30 13:55

这个好玩,黑黑又带来新的知识新的效果,这个太棒了{:4_199:}

马黑黑 发表于 2024-3-30 16:29

红影 发表于 2024-3-30 13:48
这个代码好玩,那个旋转的圆,在移动横向纵向位置时,颜色也跟着变化呢

渐变是整个画布的渐变,在不同区域呈现的图像,只要使用 gradient 填充或描边,色彩都不一样

马黑黑 发表于 2024-3-30 16:29

红影 发表于 2024-3-30 13:55
这个圆还能自己旋转。在xy等于200的时候,也就是居中时候,颜色也差不多居中,都几乎看不出旋转了呢。
又 ...

好玩吧

马黑黑 发表于 2024-3-30 16:30

红影 发表于 2024-3-30 13:50
canvas画布绘制旋转的图像,多了点步骤,但也有了更多的组合呢

对,有无限可能

马黑黑 发表于 2024-3-30 16:31

红影 发表于 2024-3-30 13:55
这个好玩,黑黑又带来新的知识新的效果,这个太棒了

其实在做时钟过程中类似的也有,只是时钟的需求与别的不同,不变展示更多的东东

南无月 发表于 2024-3-30 17:06

感觉我有缺课。。
var gradient = ctx.createRadialGradient(190, 200, 15, 200, 200, 280);
这个径向渐变这些数据是什么意思呢。。。

我看这个圆移到哪里,就显示哪里的渐变。。
感觉红黄变得最自然,其它颜色变化有点硬,想调一下,可惜不懂怎么调{:4_170:}

南无月 发表于 2024-3-30 17:07

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:}

南无月 发表于 2024-3-30 17:12

我把小圆放大到半径300,显示了背景上所有渐变,发现里面不同色彩的圆,中心点不在一个点上啊。。。
还各自旋转,方向并不相同。。
看上去各转各的。。(咦,是不是因为中心点不在一个位置,大家围着自己的中心点转,所以就各转各的了)
这个有意思了。。
{:4_170:}

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

南无月 发表于 2024-3-30 17:12
我把小圆放大到半径300,显示了背景上所有渐变,发现里面不同色彩的圆,中心点不在一个点上啊。。。
还各 ...

画布就 400*400,圆的半径300的画,实际上这个圆有一部分在外面了,你能看到的是里面部分。由于画布设置了径向渐变,圆显示的色彩是画布渐变的色彩。径向渐变是一圈一圈的,所以你看到酱紫的效果

马黑黑 发表于 2024-3-30 18:11

本帖最后由 马黑黑 于 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:12

马黑黑 发表于 2024-3-30 18:09
画布就 400*400,圆的半径300的画,实际上这个圆有一部分在外面了,你能看到的是里面部分。由于画布设置 ...

是的,有一部分在外面。半径200,稍移一下也到外面了。。
其实我是好奇,为了看背景渐变全景才设这么大的。。
有奇效{:4_173:}

南无月 发表于 2024-3-30 18:15

马黑黑 发表于 2024-3-30 18:11
这是配合前面一句设置画布的径向渐变,从 0 到 1 取值,其实就是 0% ~ 100% 这么个意思。这些的前面那句 ...

.15是指宽度?还是指位置?

马黑黑 发表于 2024-3-30 18:16

南无月 发表于 2024-3-30 18:12
是的,有一部分在外面。半径200,稍移一下也到外面了。。
其实我是好奇,为了看背景渐变全景才设这么大 ...

你想看完整 的背景,可以画一个矩形:

ctx.fillStyle = gradient;
ctx.fillRect(0,0,400,400);

把 draw_degCircle(); 暂时注释掉

南无月 发表于 2024-3-30 18:17

马黑黑 发表于 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:20

马黑黑 发表于 2024-3-30 18:16
你想看完整 的背景,可以画一个矩形:

ctx.fillStyle = gradient;

矩形0.0位置旋转和以中心点位置旋转可以理解。。下方代里中这个圆的显示,是不是可以想象成一个以中心点向外径向渐变的方形颜色画布(隐藏的),圆的位置到哪里,就显示那一块的渐变。。还是红黄渐变比较自然。。

这是我在你网站里的回复,这样理解是错的么?{:4_170:}

南无月 发表于 2024-3-30 18:21

马黑黑 发表于 2024-3-30 18:16
你想看完整 的背景,可以画一个矩形:

ctx.fillStyle = gradient;


大哥,我不会画的呀。{:4_170:}
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 如何在canvas画布中旋转图像