demo 效果javascript
不少場景都須要作各類活動,抽獎最是司空見慣了,跑馬燈的,轉盤的,下面先花幾分鐘擼出一個轉盤的吧,固然網上至少有一打的 demo 可供參考。
真的只須要一點點時間而已。css
實現一個東西,通常都先寫僞代碼,這裏也不例外。
初步想法功能主要有兩點:html
考慮一下,這個類須要什麼方法功能?
根據上面的兩個需求,java
這裏再提供一個 init 方法用做初始化操做,好比設置參數或者還有其餘處理,增長兼容性。
下面是僞代碼css3
class RotatePlate { constructor(options) { this.init(); } /** * 初始化操做 */ init() { this.setOptions(); } /** * 啓動轉動函數 */ rotate() {} /** * 設置配置參數 */ setOptions() {} /** * 動畫主循環 */ _animate() {} }
爲了方便使用和兼容處理,咱們開發者經常使用的作法就是配置默認參數,而後用 調用者的參數去覆蓋默認參數。因此,先給類增長一些默認配置,以下:git
constructor(options) { // 緩存用戶數據參數,稍後會進行默認參數覆蓋,以後再作重複初始化也會很方便 this.customOps = options; // 默認參數配置 this._parameters = { angle: 0, // 元素初始角度設置 animateTo: 0, // 目標角度 step: null, // 旋轉過程當中 回調函數 easing: function(t, b, c, d) { return -c * ((t = t / d - 1) * t * t * t - 1) + b; }, // 緩動曲線,關於動畫 緩動函數不太懂得能夠自行搜索 duration: 3000, // 動畫旋轉時間 callback: () => {}, // 旋轉完成回調 }; this._angle = 0; // 維護一個當前時刻角度的私有變量,下面setOptions就知道如何使用了 } this.init(); // 調用初始化方法
接下來實現 setOptions 方法,而且在 init 方法中進行調用,這個方法實現沒什麼難度,就是對象合併操做github
init() { // 初始化參數 this.setOptions(Object.assign({}, this.customOps)); } /** * 設置配置參數 */ setOptions(parameters) { try { // 獲取容器元素 if (typeof parameters.el === 'string') { this.el = document.querySelector(parameters.el); } else { this.el = parameters.el; } // 獲取初始角度 if (typeof parameters.angle === 'number') this._angle = parameters.angle; // 合併參數 Object.assign(this._parameters, parameters); } catch (err) {} }
上面設置完了參數,咱們還沒辦法驗證參數是否正確。
爲了實現旋轉效果,咱們有兩種方式可供選擇,第一種,利用 css3 的 transform,第二種利用 canvas 繪圖。其實兩種方法都比較簡單,這裏先選擇 css3 實現一版,結尾再附上 canvas 版本的。canvas
// 實現一個css3樣式,咱們須要處理兼容性,肯定瀏覽器類型,選擇對應的屬性 // 這裏添加一個輔助方法 /** * 判斷運行環境支持的css,用做css製做動畫 */ function getSupportCSS() { let supportedCSS = null; const styles = document.getElementsByTagName('head')[0].style; const toCheck = 'transformProperty WebkitTransform OTransform msTransform MozTransform'.split( ' ' ); for (var a = 0; a < toCheck.length; a++) { if (styles[toCheck[a]] !== undefined) { supportedCSS = toCheck[a]; break; } } return supportedCSS; } // 在constructor構造函數裏面增長一個屬性 this.supportedCSS = getSupportCSS();
而後 給類增長一個 設置樣式的方法_rotate瀏覽器
_rotate(angle) { const el = this.el; this._angle = angle; // 更新當前角度 el.style[this.supportedCSS] = `rotate3d(0,0,1,${angle % 360}deg)`; } // 在 init裏面增長 _rotate方法,初始化元素 初始角度 init() { // 初始化參數 this.setOptions(Object.assign({}, this.customOps)); // 設置一次初始角度 this._rotate(this._angle); }
在這裏,就能夠寫一個 demo,進行測試了,固然還麼有動畫,只能測試初始角度 angle 設置
demo 代碼,順便看看咱們的腳本代碼變成了什麼樣子:緩存
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> .ball { width: 100px; height: 100px; background: red; margin: 40px auto; } </style> </head> <body> <div class="ball" id="ball"></div> <script> class RotatePlate { constructor(options) { // 獲取當前運行環境支持的樣式屬性 this.supportedCSS = getSupportCSS(); // 緩存用戶數據 this.customOps = options; // 私有參數 this._parameters = { angle: 0, animateTo: 0, step: null, easing: function(t, b, c, d) { return -c * ((t = t / d - 1) * t * t * t - 1) + b; }, duration: 3000, callback: () => {}, }; this._angle = 0; // 當前時刻角度 this.init(); } /** * 初始化操做 */ init() { // 初始化參數 this.setOptions(Object.assign({}, this.customOps)); // 設置一次初始角度 this._rotate(this._angle); } /** * 啓動轉動函數 */ rotate() {} /** * 設置配置參數 */ setOptions(parameters) { try { // 獲取容器元素 if (typeof parameters.el === 'string') { this.el = document.querySelector(parameters.el); } else { this.el = parameters.el; } // 獲取初始角度 if (typeof parameters.angle === 'number') this._angle = parameters.angle; // 合併參數 Object.assign(this._parameters, parameters); } catch (err) {} } _rotate(angle) { const el = this.el; this._angle = angle; // 更新當前角度 el.style[this.supportedCSS] = `rotate3d(0,0,1,${angle % 360}deg)`; } /** * 動畫主循環 */ _animate() {} } /** * 判斷運行環境支持的css,用做css製做動畫 */ function getSupportCSS() { let supportedCSS = null; const styles = document.getElementsByTagName('head')[0].style; const toCheck = 'transformProperty WebkitTransform OTransform msTransform MozTransform'.split( ' ' ); for (var a = 0; a < toCheck.length; a++) { if (styles[toCheck[a]] !== undefined) { supportedCSS = toCheck[a]; break; } } return supportedCSS; } </script> <script> const r = new RotatePlate({ el: document.querySelector('#ball'), angle: 60, animateTo: 1800, }) </script> </body> </html>
寫到這裏,雖說了一大推話,可是代碼去掉註釋,真的尚未幾行。
可是咱們只差一個定時器循環了,接下里實現這個主循環,不斷更新 angle 值就能夠了。
提及定時器,咱們須要計算動畫時間來 判斷是否應該取消定時器等等,一些附加操做,因此增長一個_animateStart 方法清理和計時, 下面直接上代碼,
_animateStart() { if (this._timer) { clearTimeout(this._timer); } this._animateStartTime = Date.now(); this._animateStartAngle = this._angle; this._animate(); } /** * 動畫主循環 */ _animate() { const actualTime = Date.now(); const checkEnd = actualTime - this._animateStartTime > this._parameters.duration; // 判斷是否應該 結束 if (checkEnd) { clearTimeout(this._timer); } else { if (this.el) { // 調用緩動函數,獲取當前時刻 angle值 var angle = this._parameters.easing( actualTime - this._animateStartTime, this._animateStartAngle, this._parameters.animateTo - this._animateStartAngle, this._parameters.duration ); // 設置 el 元素的樣式 this._rotate(~~(angle * 10) / 10); } if (this._parameters.step) { this._parameters.step.call(this, this._angle); } // 循環調用 this._timer = setTimeout(() => { this._animate(); }, 10); } // 完成回調 if (this._parameters.callback && checkEnd) { this._angle = this._parameters.animateTo; this._rotate(this._angle); this._parameters.callback.call(this); } }
而後再 rotate 方法調用_animateStart 就行了
rotate() { if (this._angle === this._parameters.animateTo) { this._rotate(this._angle); } else { this._animateStart(); } }
至此,一個利用 css3 實現的腳本就完成了,有木有很簡單,下面貼上完整代碼.
/** * 功能: 開發一個旋轉插件,傳入一個元素節點便可控制旋轉 * 轉動角度和時長以及動畫曲線 可 經過參數進行配置 * * 參數列表: * - dom 須要一個容器 必選 * - angle 初始角度 非必選 * - animateTo 結束角度 非必選 * - duration 動畫時長 非必選 * - easing 緩動函數 非必選 * - step 角度每次更新調用 非必選 * - callback 動畫結束回調 非必選 */ class RotatePlate { constructor(options) { // 獲取當前運行環境支持的樣式屬性 this.supportedCSS = getSupportCSS(); // 緩存用戶數據 this.customOps = options; // 私有參數 this._parameters = { angle: 0, animateTo: 0, step: null, easing: function(t, b, c, d) { return -c * ((t = t / d - 1) * t * t * t - 1) + b; }, duration: 3000, callback: () => {}, }; this._angle = 0; // 當前時刻角度 this.init(); } /** * 初始化操做 */ init(newOps = {}) { // 初始化參數 this.setOptions(Object.assign({}, this.customOps, newOps)); // 設置一次初始角度 this._rotate(this._angle); } /** * 啓動轉動函數 */ rotate() { if (this._angle === this._parameters.animateTo) { this._rotate(this._angle); } else { this._animateStart(); } } /** * 設置配置參數 */ setOptions(parameters) { try { // 獲取容器元素 if (typeof parameters.el === 'string') { this.el = document.querySelector(parameters.el); } else { this.el = parameters.el; } // 獲取初始角度 if (typeof parameters.angle === 'number') this._angle = parameters.angle; // 合併參數 Object.assign(this._parameters, parameters); } catch (err) {} } _rotate(angle) { const el = this.el; this._angle = angle; // 更新當前角度 el.style[this.supportedCSS] = `rotate3d(0,0,1,${angle % 360}deg)`; } _animateStart() { if (this._timer) { clearTimeout(this._timer); } this._animateStartTime = Date.now(); this._animateStartAngle = this._angle; this._animate(); } /** * 動畫主循環 */ _animate() { const actualTime = Date.now(); const checkEnd = actualTime - this._animateStartTime > this._parameters.duration; if (checkEnd) { clearTimeout(this._timer); } else { if (this.el) { var angle = this._parameters.easing( actualTime - this._animateStartTime, this._animateStartAngle, this._parameters.animateTo - this._animateStartAngle, this._parameters.duration ); this._rotate(~~(angle * 10) / 10); } if (this._parameters.step) { this._parameters.step.call(this, this._angle); } this._timer = setTimeout(() => { this._animate(); }, 10); } if (this._parameters.callback && checkEnd) { this._angle = this._parameters.animateTo; this._rotate(this._angle); this._parameters.callback.call(this); } } } /** * 判斷運行環境支持的css,用做css製做動畫 */ function getSupportCSS() { let supportedCSS = null; const styles = document.getElementsByTagName('head')[0].style; const toCheck = 'transformProperty WebkitTransform OTransform msTransform MozTransform'.split( ' ' ); for (var a = 0; a < toCheck.length; a++) { if (styles[toCheck[a]] !== undefined) { supportedCSS = toCheck[a]; break; } } return supportedCSS; }
下面再補充一個 canvas 實現的動畫方法:
_rotateCanvas(angle) { // devicePixelRatio 是設備像素比,爲了解決canvas模糊問題設置的 // 原理把 canvas畫布擴大,而後縮小顯示在屏幕 this._angle = angle; const radian = ((angle % 360) * Math.PI) / 180; this._canvas.width = this.WIDTH * this.devicePixelRatio; this._canvas.height = this.HEIGHT * this.devicePixelRatio; // 解決模糊問題 this._cnv.scale(this.devicePixelRatio, this.devicePixelRatio); // 平移canvas原點 this._cnv.translate(this.WIDTH / 2, this.HEIGHT / 2); // 平移後旋轉 this._cnv.rotate(radian); // 移回 原點 this._cnv.translate(-this.WIDTH / 2, -this.HEIGHT / 2); this._cnv.drawImage(this._img, 0, 0, this.WIDTH, this.HEIGHT); }