摘要:中的拖拽縮放旋轉(zhuǎn)上數(shù)學(xué)知識(shí)準(zhǔn)備。表示整個(gè)區(qū)域,表示中的元素。事實(shí)上,工作上的需求并沒有要求旋轉(zhuǎn),只需要實(shí)現(xiàn)拖拽縮放即可。
寫在前面
本文首發(fā)于公眾號(hào):符合預(yù)期的CoyPan
demo體驗(yàn)地址及代碼在這里:請(qǐng)用手機(jī)或?yàn)g覽器模擬手機(jī)訪問(wèn)
上一篇文章介紹了canvas中的拖拽、縮放、旋轉(zhuǎn)中涉及到的數(shù)學(xué)知識(shí)。可以點(diǎn)擊下面的鏈接查看。
canvas中的拖拽、縮放、旋轉(zhuǎn) (上) —— 數(shù)學(xué)知識(shí)準(zhǔn)備。
代碼準(zhǔn)備 - 如何在canvas中畫出一個(gè)帶旋轉(zhuǎn)角度的元素在canvas中,如果一個(gè)元素帶有一個(gè)旋轉(zhuǎn)角度,可以直接變化canvas的坐標(biāo)軸來(lái)畫出此元素。舉個(gè)例子,
ctx.save(); // 保存舊的坐標(biāo)系狀態(tài) ctx.translate(x0 + w / 2, y0 + h / 2); // 坐標(biāo)原點(diǎn)移動(dòng)到旋轉(zhuǎn)中心 ctx.rotate(angle); // 旋轉(zhuǎn)坐標(biāo)系 ctx.translate(-(x0 + w / 2), -(y0 + h / 2)); // 坐標(biāo)原點(diǎn)還原 ctx.rect(x0, y0, w, h); // 以新坐標(biāo)系為參照,畫出矩形。 ctx.restore(); // 還原之前的坐標(biāo)系狀態(tài)代碼整體思路
整個(gè)demo的實(shí)現(xiàn)思路如下:
用戶開始觸摸(touchstart)時(shí),獲取用戶的觸摸對(duì)象,是Sprite的本體?刪除按鈕?縮放按鈕?旋轉(zhuǎn)按鈕?并且根據(jù)各種情況,對(duì)變化參數(shù)進(jìn)行初始化。
用戶移動(dòng)手指(touchmove)時(shí),根據(jù)手指的坐標(biāo),更新stage中的所有元素的位置、大小,記錄變化參數(shù)。修改對(duì)應(yīng)sprite的屬性值。同時(shí)對(duì)canvas進(jìn)行重繪。
用戶一旦停止觸摸(touchend)時(shí),根據(jù)變化參數(shù),更新sprite的坐標(biāo),同時(shí)對(duì)變化參數(shù)進(jìn)行重置。
需要注意的是,在touchmove的過(guò)程中,并不需要更新sprite的坐標(biāo),只需要記錄變化的參數(shù)即可。在touchend過(guò)程中,再進(jìn)行坐標(biāo)的更新。坐標(biāo)的唯一用處,就是判斷用戶點(diǎn)擊時(shí),落點(diǎn)是否在指定區(qū)域內(nèi)。
代碼細(xì)節(jié)首先,聲明兩個(gè)類:Stage和Sprite。Stage表示整個(gè)canvas區(qū)域,Sprite表示canvas中的元素。我們可以在Stage中添加多個(gè)Sprite,刪除Sprite。這兩個(gè)類的屬性如下。
class Stage { constructor(props) { this.canvas = props.canvas; this.ctx = this.canvas.getContext("2d"); // 用一個(gè)數(shù)組來(lái)保存canvas中的元素。每一個(gè)元素都是一個(gè)Sprite類的實(shí)例。 this.spriteList = []; // 獲取canvas在視窗中的位置,以便計(jì)算用戶touch時(shí),相對(duì)與canvas內(nèi)部的坐標(biāo)。 const pos = this.canvas.getBoundingClientRect(); this.canvasOffsetLeft = pos.left; this.canvasOffsetTop = pos.top; this.dragSpriteTarget = null; // 拖拽的對(duì)象 this.scaleSpriteTarget = null; // 縮放的對(duì)象 this.rotateSpriteTarget = null; // 旋轉(zhuǎn)的對(duì)象 this.dragStartX = undefined; this.dragStartY = undefined; this.scaleStartX = undefined; this.scaleStartY = undefined; this.rotateStartX = undefined; this.rotateStartY = undefined; } } class Sprite { constructor(props) { // 每一個(gè)sprite都有一個(gè)唯一的id this.id = Date.now() + Math.floor(Math.random() * 10); this.pos = props.pos; // 在canvas中的位置 this.size = props.size; // sprite的當(dāng)前大小 this.baseSize = props.size; // sprite的初始化大小 this.minSize = props.minSize; // sprite縮放時(shí)允許的最小size this.maxSize = props.maxSize; // sprite縮放時(shí)允許的最大size // 中心點(diǎn)坐標(biāo) this.center = [ props.pos[0] + props.size[0] / 2, props.pos[1] + props.size[1] / 2 ]; this.delIcon = null; this.scaleIcon = null; this.rotateIcon = null; // 四個(gè)頂點(diǎn)的坐標(biāo),順序?yàn)椋鹤笊希疑希笙拢蚁? this.coordinate = this.setCoordinate(this.pos, this.size); this.rotateAngle = 0; // 累計(jì)旋轉(zhuǎn)的角度 this.rotateAngleDir = 0; // 每次旋轉(zhuǎn)角度 this.scalePercent = 1; // 縮放比例 } }
demo中,點(diǎn)擊canvas下方的紅色方塊時(shí),會(huì)實(shí)例化一個(gè)sprite,調(diào)用stage.append時(shí),會(huì)將實(shí)例化的sprite直接push到Stage的spriteList屬性內(nèi)。
window.onload = function () { const stage = new Stage({ canvas: document.querySelector("canvas") }); document.querySelector(".red-box").addEventListener("click", function () { const randomX = Math.floor(Math.random() * 200); const randomY = Math.floor(Math.random() * 200); const sprite = new Sprite({ pos: [randomX, randomY], size: [120, 60], minSize: [40, 20], maxSize: [240, 120] }); stage.append(sprite); }); }
下面是Stage的方法:
class Stage { constructor(props) {} // 將sprite添加到stage內(nèi) append(sprite) {} // 監(jiān)聽事件 initEvent() {} // 處理touchstart handleTouchStart(e) {} // 處理touchmove handleTouchMove(e) {} // 處理touchend handleTouchEnd() {} // 初始化sprite的拖拽事件 initDragEvent(sprite, { touchX, touchY }) {} // 初始化sprite的縮放事件 initScaleEvent(sprite, { touchX, touchY }) {} // 初始化sprite的旋轉(zhuǎn)事件 initRotateEvent(sprite, { touchX, touchY }) {} // 通過(guò)觸摸的坐標(biāo)重新計(jì)算sprite的坐標(biāo) reCalSpritePos(sprite, touchX, touchY) {} // 通過(guò)觸摸的【橫】坐標(biāo)重新計(jì)算sprite的大小 reCalSpriteSize(sprite, touchX, touchY) {} // 重新計(jì)算sprite的角度 reCalSpriteRotate(sprite, touchX, touchY) {} // 返回當(dāng)前touch的sprite getTouchSpriteTarget({ touchX, touchY }) {} // 判斷是否touch在了sprite中的某一部分上,返回這個(gè)sprite getTouchTargetOfSprite({ touchX, touchY }, part) {} // 返回觸摸點(diǎn)相對(duì)于canvas的坐標(biāo) normalizeTouchEvent(e) {} // 判斷是否在在某個(gè)sprite中移動(dòng)。當(dāng)前默認(rèn)所有的sprite都是長(zhǎng)方形的。 checkIfTouchIn({ touchX, touchY }, sprite) {} // 從場(chǎng)景中刪除 remove(sprite) {} // 畫出stage中的所有sprite drawSprite() {} // 清空畫布 clearStage() {} }
Sprite的方法:
class Sprite { constructor(props) {} // 設(shè)置四個(gè)頂點(diǎn)的初始化坐標(biāo) setCoordinate(pos, size) {} // 根據(jù)旋轉(zhuǎn)角度更新sprite的所有部分的頂點(diǎn)坐標(biāo) updateCoordinateByRotate() {} // 根據(jù)旋轉(zhuǎn)角度更新頂點(diǎn)坐標(biāo) updateItemCoordinateByRotate(target, center, angle){} // 根據(jù)縮放比例更新頂點(diǎn)坐標(biāo) updateItemCoordinateByScale(sprite, center, scale) {} // 根據(jù)按鈕icon的頂點(diǎn)坐標(biāo)獲取icon中心點(diǎn)坐標(biāo) getIconCenter(iconCoordinate) {} // 根據(jù)按鈕icon的中心點(diǎn)坐標(biāo)獲取icon的頂點(diǎn)坐標(biāo) getIconCoordinateByIconCenter(center) {} // 根據(jù)縮放比更新頂點(diǎn)坐標(biāo) updateCoordinateByScale() {} // 畫出該sprite draw(ctx) {} // 畫出該sprite對(duì)應(yīng)的按鈕icon drawIcon(ctx, icon) {} // 對(duì)sprite進(jìn)行初始化 init() {} // 初始化刪除按鈕,左下角 initDelIcon() {} // 初始化縮放按鈕,右上角 initScaleIcon() {} // 初始化旋轉(zhuǎn)按鈕,左上角 initRotateIcon() {} // 重置icon的位置與大小 resetIconPos() {} // 根據(jù)移動(dòng)的距離重置sprite所有部分的位置 resetPos(dirX, dirY) {} // 根據(jù)觸摸點(diǎn)移動(dòng)的距離計(jì)算縮放比,并重置sprite的尺寸 resetSize(dir) {} // 設(shè)置sprite的旋轉(zhuǎn)角度 setRotateAngle(angleDir) {} }
Stage的方法主要是處理和用戶交互的邏輯,得到用戶操作的交互參數(shù),然后根據(jù)交互參數(shù)調(diào)用Sprite的方法來(lái)進(jìn)行變化。
代碼在這里:https://coypan.info/demo/canvas-drag-scale-rotate.html
寫在后面本文介紹了文章開頭給出的demo的詳細(xì)實(shí)現(xiàn)過(guò)程。代碼還有很大的優(yōu)化空間。事實(shí)上,工作上的需求并沒有要求【旋轉(zhuǎn)】,只需要實(shí)現(xiàn)【拖拽】、【縮放】即可。在只實(shí)現(xiàn)【拖拽】和【縮放】的情況下,會(huì)容易很多,不需要用到四個(gè)頂點(diǎn)的坐標(biāo)以及之前的那些復(fù)雜的數(shù)學(xué)知識(shí)。而在自己實(shí)現(xiàn)【旋轉(zhuǎn)】的過(guò)程中,也學(xué)到了很多。符合預(yù)期。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/101699.html
摘要:參考了很多別人寫的代碼,最后終于弄明白了其中的原理,自己也寫了一個(gè)。效果圖如下地址如下拖拽類封裝代碼使用方法引入和對(duì)應(yīng)的。如果沒有為的結(jié)構(gòu),就創(chuàng)建。鼠標(biāo)移動(dòng)時(shí),記錄再次計(jì)算鼠標(biāo)位置距離中心位置的的反正切函數(shù)。 在公司做一個(gè)h5編輯平臺(tái),中間需要對(duì)元素進(jìn)行拖拽、放大縮小、旋轉(zhuǎn)等操作,且需要對(duì)文本、圖片、音樂(lè)組件等不同元素都可以具備這些功能。參考了很多別人寫的代碼,最后終于弄明白了其中的原...
這只是個(gè)開頭 說(shuō)在最前面,本文是一個(gè)系列文章的開頭, 這個(gè)系列里我會(huì)講如何用typescript開發(fā)一款支持pc和手機(jī)端的手勢(shì)庫(kù)any-touch, 以及通過(guò)jest讓你的代碼測(cè)試覆蓋率100%. showImg(https://segmentfault.com/img/bVbp3B0?w=936&h=246); 目錄 用TypeScript開發(fā)手勢(shì)庫(kù) - (2)tsconfig.json & r...
摘要:移動(dòng)的過(guò)程中可以通過(guò)拿到元素的坐標(biāo),記為。向上滾動(dòng)放大,向下滾動(dòng)縮小這里要注意控制最小縮放值。還要注意的是圖片在邊界的縮放,不然圖片可能會(huì)移動(dòng)在屏幕外。代碼實(shí)現(xiàn)控制滾輪縮放計(jì)算縮放后的大小每一次滾輪限制最小不讓由于縮小消失在視野中 cxj-react-image 用法如下: yarn add cxj-react-image // npm i cxj-react-image import...
摘要:隨后會(huì)陸續(xù)發(fā)布及相關(guān)版本的插件。這和圖片查看器的操作方式是相同的。目前的調(diào)整大小存在一點(diǎn),但不影響整體的使用。鍵盤控制和照片查看器的按鍵是一樣的。除了照片查看器,的圖片查看器也非常的高大上。 showImg(https://segmentfault.com/img/remote/1460000012565638?w=750&h=375); 前言 因?yàn)橐恍┨厥獾臉I(yè)務(wù)需求,經(jīng)過(guò)一個(gè)多月的蟄...
閱讀 2696·2023-04-25 21:26
閱讀 1514·2021-11-25 09:43
閱讀 1949·2019-08-30 15:52
閱讀 932·2019-08-30 14:05
閱讀 2614·2019-08-29 16:10
閱讀 414·2019-08-29 13:48
閱讀 1860·2019-08-29 12:47
閱讀 1299·2019-08-23 18:04