canvas 多次画图效果_canvas练习之终极的奔跑小人
這次做一個終極的練習,先看一下最后的效果。
一個不停奔跑的小人,點擊鼠標后會讓他跑到目的地,并且呈現不同的角度。下面來看一下如何一步步來實現它的。
準備
網上下載了一張圖片,其中包含了小人面向不同角度奔跑的各個分解動作。
新建一個html文件,放入常規的canvas元素以及script標簽。
第0版
這一版的目的是在canvas上畫出素材里的其中一個小人。其中,事先經過了測量,每一個小人橫向占70像素,縱向占92像素。
這里主要用到了CanvasRenderingContext2D的drawImage方法,特別是這個方法的九參數版本。
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); // image 繪制到上下文的元素 // sx 需要繪制到目標上下文中的,image的矩形(裁剪)選擇框的左上角 X 軸坐標。 // sy 需要繪制到目標上下文中的,image的矩形(裁剪)選擇框的左上角 Y 軸坐標。 // sWidth 需要繪制到目標上下文中的,image的矩形(裁剪)選擇框的寬度。如果不說明,整個矩形(裁剪)從坐標的sx和sy開始,到image的右下角結束。 // sHeight 需要繪制到目標上下文中的,image的矩形(裁剪)選擇框的高度。 // dx image的左上角在目標canvas上 X 軸坐標。 // dy image的左上角在目標canvas上 Y 軸坐標。 // dWidth image在目標canvas上繪制的寬度。 允許對繪制的image進行縮放。 如果不說明, 在繪制時image寬度不會縮放。 // dHeight image在目標canvas上繪制的高度。 允許對繪制的image進行縮放。 如果不說明, 在繪制時image高度不會縮放完成后效果如下圖,目前代碼很簡單,節約篇幅,就不具體貼了。
第1版
這一版的目的是要讓小人奔跑起來,但位置可以不變。也就是利用人的視覺停留,在canvas上不停地重繪小人,給人以奔跑的感覺。
這里以及后面的代碼都做了一個假設就是requestAnimationFrame是被系統穩定調用的,沒有阻塞的情況發生,這樣子就簡化了代碼,不需要根據兩次requestAnimationFrame調用的時差來進行一些計算。
const image = new Image() image.src = 'walking.jpeg' let index = 0 //計數,每次渲染時+1 const HSTEP = 70 //橫向每幀間隔距離 const VSTEP = 92 //縱向每幀間隔距離 let lastTime = 0 //記錄上次時間戳 image.onload = () => {window.requestAnimationFrame(walking) //啟動動畫 } function walking(timestamp) {if (timestamp - lastTime > 80) { //滿時差后渲染lastTime = timestampcontext.drawImage(image, HSTEP * (index++ % 8), VSTEP * 2, HSTEP, VSTEP, 0, 0, HSTEP, VSTEP)}window.requestAnimationFrame(walking) }里面有個魔鬼數字VSTEP * 2這個2是從上向下的第三組圖片,也即是小人面向正右方的分解動作。
第2版
這次在上一版的基礎上增加了小人的坐標移動。所要多做的僅僅是在drawImage方法參數中改變畫圖位置的坐標。
const image = new Image() image.src = 'walking.jpeg' let index = 0 //計數,每次渲染時+1 const HSTEP = 70 //橫向每幀間隔距離 const VSTEP = 92 //縱向每幀間隔距離 const MOVESTEP = 10 //每幀移動距離 let lastTime = 0 //記錄上次時間戳 let xPos = 0 // x坐標位置 image.onload = () => {window.requestAnimationFrame(walking) //啟動動畫 } function walking(timestamp) {if (timestamp - lastTime > 80) { //滿時差后渲染lastTime = timestampif (xPos < canvas.width - HSTEP) {xPos = index * MOVESTEP}context.clearRect(0, 0, canvas.width, canvas.height)context.drawImage(image, HSTEP * (index++ % 8), VSTEP * 2, HSTEP, VSTEP, xPos, 0, HSTEP, VSTEP)}window.requestAnimationFrame(walking) }第3版
這次也是小步迭代,增加了小人縱向移動,換了另一個角度的動作。
const image = new Image() image.src = 'walking.jpeg' let index = 0 //計數,每次渲染時+1 const HSTEP = 70 //橫向每幀間隔距離 const VSTEP = 92 //縱向每幀間隔距離 const MOVESTEP = 10 //每幀移動距離 let lastTime = 0 //記錄上次時間戳 let xPos = 0 // x坐標位置 let yPos = 0 // y坐標位置 image.onload = () => {window.requestAnimationFrame(walking) //啟動動畫 } function walking(timestamp) {if (timestamp - lastTime > 80) { //滿時差后渲染lastTime = timestampif (xPos < canvas.width - HSTEP) {xPos = index * MOVESTEP}if(yPos < canvas.height - VSTEP) {yPos = index * MOVESTEP}context.clearRect(0, 0, canvas.width, canvas.height)context.drawImage(image, HSTEP * (index++ % 8), VSTEP * 5, HSTEP, VSTEP, xPos, yPos, HSTEP, VSTEP)}window.requestAnimationFrame(walking) }魔鬼數字換成了5哈。
最終版
先定義個class用來存放x、y軸的坐標,這個類的兩個實例分別對應了當前的坐標和目的地的坐標。
class Position {constructor(x, y) {this.x = xthis.y = y} }let currentPos = new Position(0, 0) //當前位置 let targetPos = new Position(0, 0) //目標位置移動時的相關新建變量有
let angle = 0 let movingAngleType = 2 //角度類型一共8種 let xMoveStep //x軸每次移動距離 let yMoveStep //y軸每次移動距離其中angle在鼠標點擊時,即可通過當前位置和鼠標點擊位置計算得出。movingAngleType就對應了八種奔跑角度的分解動作。xMoveStep和yMoveStep是每次渲染時x軸和y軸移動的距離,跟anglue一樣,也只要在點擊的時候計算出來就行。
每次渲染時,計算目標點和當前點的距離差,如果大于步進,就加上步進,如果小于,就將目標點位置賦給當前點。
剩下的事情就是計算了,這一次好好的把三角函數復習了一遍。這個例子中的角度是在-90° ~ 270°之間。Math.asin計算結果是在-π ~ π 之間的,而canvas的坐標軸原點位于左上角,并且根據位置不同要如下計算
if (targetPos.x < currentPos.x) {angle = Math.PI - angle }上面這段代碼就把360°的坐標軸分在了-90° ~ 270°之間,而不是0 ~ 360°,影響到了移動角度類型,也即是從上至下的八個動作的選取。-75° ~ -15°是右下角度,-15° ~ 15°是右角度,依次類推。
完整的代碼如下
class Position {constructor(x, y) {this.x = xthis.y = y} } const canvas = document.getElementById('canvas') const context = canvas.getContext('2d') const image = new Image() image.src = 'walking.jpeg' let index = 0 //計數,每次渲染時+1 const HSTEP = 70 //橫向每幀間隔距離 const VSTEP = 92 //縱向每幀間隔距離 const MOVESTEP = 10 //每幀移動距離 const TIMESTAMP_THRESHOLD = 80 // 時差閾值 const FRAMES_LENGTH = 8 let lastTime = 0 //記錄上次時間戳 let currentPos = new Position(0, 0) //當前位置 let targetPos = new Position(0, 0) //目標位置 let angle = 0 let movingAngleType = 2 //角度類型一共8種 let xMoveStep //x軸每次移動距離 let yMoveStep //y軸每次移動距離 image.onload = () => {window.requestAnimationFrame(walking) //啟動動畫 } function windowToCanvas(canvas, x, y) {var bbox = canvas.getBoundingClientRect();var style = window.getComputedStyle(canvas);return {x: (x - bbox.left - parseInt(style.paddingLeft) - parseInt(style.borderLeft))* (canvas.width / parseInt(style.width)),y: (y - bbox.top - parseInt(style.paddingTop) - parseInt(style.borderTop))* (canvas.height / parseInt(style.height))}; } function walking(timestamp) { //抽象成:在什么位置,畫什么圖,完全由狀態數據決定 UI = f(state)if (timestamp - lastTime > TIMESTAMP_THRESHOLD) { //滿時差后渲染lastTime = timestamp//計算步進,MOVESTEP要投射到x軸和y軸上// 這里xMoveStep一開始沒取絕對值,調試了好久if (Math.abs(targetPos.x - currentPos.x) > Math.abs(xMoveStep)) { currentPos.x += xMoveStep} else {currentPos.x = targetPos.x}if (Math.abs(targetPos.y - currentPos.y) > Math.abs(yMoveStep)) {currentPos.y += yMoveStep} else {currentPos.y = targetPos.y}context.clearRect(0, 0, canvas.width, canvas.height)context.drawImage(image, HSTEP * (index++ % FRAMES_LENGTH), VSTEP * movingAngleType,HSTEP, VSTEP, currentPos.x, currentPos.y, HSTEP, VSTEP)}window.requestAnimationFrame(walking) }canvas.addEventListener('click', e => {const position = windowToCanvas(canvas, e.clientX, e.clientY)targetPos.x = position.x - HSTEP / 2 //鼠標點擊點為人物的中心,所以減去一半寬度targetPos.y = position.y - VSTEP / 2 //鼠標點擊點為人物的中心,所以減去一半高度//計算angleangle = Math.asin((targetPos.y - currentPos.y) / Math.sqrt(Math.pow(targetPos.y - currentPos.y, 2)+ Math.pow(targetPos.x - currentPos.x, 2)))if (targetPos.x < currentPos.x) {angle = Math.PI - angle}xMoveStep = MOVESTEP * Math.cos(angle) //只用計算一次yMoveStep = MOVESTEP * Math.sin(angle) //判斷哪種角度的奔跑if(angle >-5 * Math.PI /12 && angle < -Math.PI /12) {movingAngleType = 7 //右上} else if(angle >= -Math.PI / 12 && angle <= Math.PI / 12 ) {movingAngleType = 2 //右} else if(angle > Math.PI /12 && angle < 5 * Math.PI / 12) {movingAngleType = 5 //右下} else if(angle >= 5 * Math.PI /12 && angle <= 7 * Math.PI /12) {movingAngleType = 0 //下} else if(angle > 7 * Math.PI /12 && angle < 11 * Math.PI /12) {movingAngleType = 4 //左下} else if(angle >= 11 * Math.PI /12 && angle <= 13 * Math.PI /12) {movingAngleType = 1 //左} else if(angle > 13 * Math.PI /12 && angle < 17 * Math.PI /12) {movingAngleType = 6 //左上} else {movingAngleType = 3 //上} })代碼貼到了github上
swordrain/running?github.com補充一下,其實還有一種思路是不用計算小人的坐標,而是移動canvas坐標軸,每次都只在(0, 0)處繪制小人即可。總結
以上是生活随笔為你收集整理的canvas 多次画图效果_canvas练习之终极的奔跑小人的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: cdr软件百度百科_什么是CDR软件?
- 下一篇: GB-T2260-2020 <中华人民共