しかし、時には本当に創造的なタスクがあり、通常のコピーアンドペーストでは民主主義は保存されません(正直なところ、ほとんど保存されません)。 また、これらのケースの1つについて、著名な大衆に伝えたいと思います。
背景はそのようなものです。 弊社では、クライアント部分が純粋なJavasciptで記述される製品を開発しました。 ある時点で、製品のすべての機能を実現するために、アニメーション要素(要素の拡大、ディゾルブ、画面上でのスムーズな動きなど)なしではできないことに気付きました。
最も論理的なステップは、同じjQueryでアニメーションがどのように機能するかを確認し、可能であればコード内でソリューションを繰り返すように思えた。 ただし、一見しただけでも、jQueryコードはすべての依存関係を理解するのがそれほど簡単ではないことが明らかになりました。 そして、プロジェクト管理者は、同僚の経験を振り返ることなく、座って決断を下すのに十分な時間を割り当てました。 今、振り返ってみると、クライアントプログラミングで本当に難しいタスクを解決するのに素晴らしい経験だったと思います。
だから、トップレベルで問題を検討してください
次の条件があります。
1)ページ上の要素はすべてアニメーション化され、要素の数は無制限です
2)アニメーション化されたプロパティのリストが設定されます(線形寸法、位置、インデント、透明度)
3)アニメーションはパラメーター(実行時間、実行速度の関数)で制御する必要があります
4)アニメーションが完了すると、任意のコールバックが呼び出されます
5)アニメーションはいつでも中断できます
ほとんどの場合、パラメータのリストで呼び出される1つのグローバル関数がアイテム1〜4を担当し、別の特別な関数がアイテム5を担当します。 わずか2(実際、3つであることが判明しました:))。 次に、特定のブロックが存在する理由とそれがどのように機能するかについての簡単な説明とともに関数を記述するプロセスを説明し、最後に完全なコードへのリンクと例を示します。
始めましょう
ウィンドウに直接入らないように、var $ = {d:window.document、w:window}をすぐに作成します。
メインアニメーション関数を作成します。
$.w.ltAnimate = function (el, props, opts, cb) {});
ここで、elはアニメーション要素、propsはアニメーションの本質、optsは特性、cbはコールバックです。
私たちはそれを埋め始めます。 コンテキストを保存するには、selfを使用して、アニメーションIDを一意に決定します。 必要なチェックをすぐに実行します。
コード1
var self = this, id = new Date().getTime(); // id self._debug = true; // if ((typeof el == "string") && el) el = this.ltElem(el); if ((typeof el != "object") || !el || (typeof el.nodeType != "number") || (el.nodeType > 1)) { doFail(" "); return; } // opts switch (typeof opts) { case "number": opts = {duration: opts}; break; case "function": opts = {cbDone: opts}; break; case "object": if (!opts) opts = {}; break; default: opts = {}; } if (typeof cb == "function") opts.cbDone = cb; // var defaultOptions = { tick : 30, // , duration : 1000, // easing : 'linear', // cbDone : function() { // if (self._debug) $.w.console.log(" [id: " + id + "] "); }, cbFail : function() { // if (self._debug) $.w.console.log(" [id: " + id + "] "); } }
注意 :エラー処理は特別なdoFail関数により便利です。
これで、要素で実行する必要があるアクションが配列に追加されます。
コード2
// , var instructions = []; for (var key in props) { if (!props.hasOwnProperty(key)) continue; instructions.push([key, props[key]]); } // , if (instructions.length === 0) { doFail(" , "); return; }
デフォルトのオプションは、指定されたクライアントによって書き換えられます(もちろん、チェック付き)。
コード3
// var optionsList = [], easing = {linear: 1, swing:1, quad:1, cubic:1}; for (var key in opts) { if (!opts.hasOwnProperty(key)) continue; switch (key) { case "duration": if (typeof opts[key] != "number") { $.w.console.log("ltAnimate(): ! . "); continue; } break; case "easing": if (typeof easing[opts[key]] == "undefined") { $.w.console.log("ltAnimate(): ! easing. "); continue; } break; case "cbDone": case "cbFail": if (typeof opts[key] != "function") { $.w.console.log("ltAnimate(): ! !"); continue; } break; default: $.w.console.log("ltAnimate(): ! !"); continue; } optionsList.push([key, opts[key]]) } // options defaultOptions var options = defaultOptions; if (optionsList.length) { for (var i=0; i < optionsList.length; i++) { if (optionsList[i][0] == 'duration') options.duration = optionsList[i][1]; if (optionsList[i][0] == 'easing') options.easing = optionsList[i][1]; if (optionsList[i][0] == 'cbDone') options.cbDone = optionsList[i][1]; if (optionsList[i][0] == 'cbFail') options.cbFail = optionsList[i][1]; } }
それでは、現実の世界に少し戻りましょう。
実際、アニメーションはどの要素でもいつでも呼び出されます。 したがって、この要素を変更する方法を理解するために、この時点でこの要素のパラメータを受け入れる必要があります。
// , var startParams = {};
ここで、最後に、アニメーション要素を説明する追加の機能が必要になることがわかります(それについてはもう少し詳しく説明します)。 それまでの間、別の重要なチェックを行います。
アニメーションの数とキュー
はい、タスクは難しくなっています。 実際、アニメーションは、前のアニメーションの実行状態にある(およびまだ終了していない)要素で呼び出すことができます。 私たちの機能は普遍的でなければならないので、独立した呼び出しのチェーンを受信して正しく処理できなければなりません。
以下はコードです。 それには行ごとの説明があるので、ブロック全体を引用します:
コード4
// if (el.ltAnimateQueue && el.ltAnimateQueue.length > 0) { // , ( , .. ) var animateEnds = 1, timeNow = new Date().getTime(); for (var i=0; i < el.ltAnimateQueue.length; i++) { if (i == 0) { animateEnds = el.ltAnimateQueue[i][1] - timeNow + el.ltAnimateQueue[i][0]; } else { animateEnds += el.ltAnimateQueue[i][1]; } } // el.ltAnimateQueue.push([timeNow + animateEnds, options.duration]); // , var thisTimeout = $.w.setTimeout(function(){ checkAnimation(); }, animateEnds); // , , ltAnimateStop if (!el.ltAnimateTimeouts) { el.ltAnimateTimeouts = []; } el.ltAnimateTimeouts.push(thisTimeout); // } else { // , el.ltAnimateQueue = [[new Date().getTime(), options.duration]]; startAnimation(); } // , function checkAnimation() { // , if (!el.ltAnimateIsDoing) { startAnimation(); } else { // , function _check() { if (!el.ltAnimateIsDoing) { $.w.clearInterval(_checking); startAnimation(); } } var _checking = $.w.setInterval(_check, 30); } }
アニメーションの実行を開始する準備ができました。その開始はstartAnimation()関数によって実行されます。 ここでは、最初に、プロパティを使用して要素を説明します(アニメーションをキューに入れている間に要素を変更できるため、これを行うことはできません)。
コード5
function startAnimation() { // el.ltAnimateIsDoing = true; // var startStyles = self.ltStyle(el); // startParams.left = parseInt(startStyles.left); startParams.right = parseInt(startStyles.right); startParams.top = parseInt(startStyles.top) + 0.01; startParams.bottom = parseInt(startStyles.bottom) - 0.01; startParams.width = parseInt(startStyles.width); startParams.height = parseInt(startStyles.height); startParams.opacity = parseFloat(startStyles.opacity); startParams.marginTop = parseInt(startStyles.marginTop); startParams.marginBottom = parseInt(startStyles.marginBottom); startParams.marginLeft = parseInt(startStyles.marginLeft); startParams.marginRight = parseInt(startStyles.marginRight); startParams.parentWidth = parseInt(self.ltStyle(el.parentNode).width); startParams.parentHeight = parseInt(self.ltStyle(el.parentNode).height); // Chrome IE for (key in startParams) { if (key == 'left' && !startParams[key]) { startParams.left = startParams.parentWidth - startParams.right - startParams.width || 0; } if (key == 'right' && !startParams[key]) { startParams.right = startParams.parentWidth - startParams.left - startParams.width || 0; } if (key == 'bottom' && !startParams[key]) { startParams.bottom = startParams.parentHeight - startParams.top - startParams.height || 0; } if (key == 'top' && !startParams[key]) { startParams.top = startParams.parentHeight - startParams.bottom - startParams.height || 0; } } // el.currentAnimation = new doAnimation({ element : el, delay : defaultOptions.delay }); }
ご覧のとおり、ここでは、要素のスタイルの説明を提供する特別な関数を使用します。 コードは純粋なJSである必要があり、プロジェクトはIE8ブラウザーで適切に動作する必要があることに留意してください(はい、さまざまな国にお金で販売される実際の商用コードを書くので、「これは流行りません」という議論は受け入れられません! )、すべての意志を拳に絞り、ロバのトラブルを拾いに行きます:
コード6
/** * ( el) ( styleName ). * opts - , computed true. , , false - . * (, div) , , . * IE8 %, auto, thin/medium/thick . * Opacity IE8 ( 0 1) * * @param {DOM} el - * @param {string} style - , * @param {Object} opts - * * @returns {(number|string)} value - */ $.w.ltStyle = function(el, styleName, opts) { if (!opts || typeof opts != 'object' || typeof opts.computed != 'boolean') opts = {computed : true}; if (typeof el == 'string') el = this.ltElem(el); // (NodeList), if (!el || !el.nodeType || (el.nodeType != 1)) return ''; var _style; // IE8 getComputedStyle currentStyle if (!$.w.getComputedStyle) { var __style = el.currentStyle, _style = {}; for (var i in __style) { _style[i] = __style[i]; } // , IE8 : pixelLeft, pixelRight - , var pixel = { left: 1, right: 1, width: 1, height: 1, top: 1, bottom: 1 }; // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 var other = { paddingLeft: 1, paddingRight: 1, paddingTop: 1, paddingBottom: 1, marginLeft: 1, marginRight: 1, marginTop: 1, marginBottom: 1 }; var leftCopy = el.style.left; var runtimeLeftCopy = el.runtimeStyle.left; // if (!styleName) { // IE8 , for (c in _style) { if (!_style.hasOwnProperty(c)) continue; if (c.indexOf("border") !== 0) continue; switch (_style[c]) { case "thin": _style[c] = 2; break; case "medium": _style[c] = 4; break; case "thick": _style[c] = 6; break; default: _style[c] = 0; } } //pixel for (var key in pixel) { _style[key] = el.style["pixel" + key.charAt(0).toUpperCase() + key.replace(key.charAt(0), "")]; } // getComputedStyle for (var key in other) { el.runtimeStyle.left = el.currentStyle.left; el.style.left = _style[key]; _style[key] = el.style.pixelLeft; el.style.left = leftCopy; el.runtimeStyle.left = runtimeLeftCopy; } // } else { if (_style[styleName]) { if (style.indexOf("border") === 0) switch (_style[styleName]) { case "thin": _style[styleName] = 2; break; case "medium": _style[styleName] = 4; break; case "thick": _style[styleName] = 6; break; default: _style[styleName] = 0; } } else { if (pixel[styleName]) { _style[styleName] = el.style["pixel" + key.charAt(0).toUpperCase() + key.replace(key.charAt(0), "")]; } else { el.runtimeStyle.left = el.currentStyle.left; el.style.left = _style[styleName]; _style[styleName] = el.style.pixelLeft; el.style.left = leftCopy; el.runtimeStyle.left = runtimeLeftCopy; } } } // opacity IE8 if (_style.filter.match('alpha')) { _style.opacity = _style.filter.substr(14); _style.opacity = parseInt(_style.opacity.substring(0, _style.opacity.length - 1)) / 100; } else { _style.opacity = 1; } // } else { if (opts.computed) { _style = $.w.getComputedStyle(el, null); } else { _style = el.style.styleName; } } if (!styleName) { return _style || ''; } else { return _style[styleName] || ''; } };
ここでは、ビジネスのように思えます。要素のスタイルを取得してください! しかし、いや、創造性の分野があります。
最後に、要素が完全に記述され、すべてのアニメーションパラメータがチェックされ、規範的な形式になります。 次に、実行コードを記述します。 関数doAnimation(params){}で完全に囲まれています。
以下に示す最初の部分で最も興味深いのは、アニメーション命令のデコードです(「オブジェクトをどうするか?」)。 要素の一部のプロパティ(寸法、位置、インデント)は、ピクセルだけでなくパーセントでも設定できることを誰もが覚えていることを願っています。
コード7
// var val = instructions[i][1].toString(); // , val.match(/\%/) ? percent = true : percent = false; val = parseFloat(val); var x; switch (instructions[i][0]) { case 'top' : x = function(factor, val, percent) { element.style.bottom = ''; element.style.top = startParams.top - (startParams.top - (percent ? startParams.parentHeight * val / 100 : val))*factor + 'px'; }; break; case 'bottom' : x = function(factor, val, percent) { element.style.top = ''; element.style.bottom = startParams.bottom - (startParams.bottom - (percent ? (startParams.parentHeight * val / 100) : val))*factor + 'px'; }; break; case 'left' : x = function(factor, val, percent) { element.style.right = ''; element.style.left = startParams.left - (startParams.left - (percent ? (startParams.parentWidth * val / 100) : val))*factor + 'px'; }; break; case 'right' : x = function(factor, val, percent) { element.style.left = ''; element.style.right = startParams.right - (startParams.right - (percent ? (startParams.parentWidth * val / 100) : val))*factor + 'px'; }; break; case 'width' : x = function(factor, val, percent) { element.style.width = startParams.width - (startParams.width - (percent ? (startParams.width * val / 100) : val))*factor + 'px'; }; break; case 'height' : x = function(factor, val, percent) { element.style.height = startParams.height - (startParams.height - (percent ? (startParams.height * val / 100) : val))*factor + 'px'; }; break; case 'opacity' : x = function(factor, val, percent) { // IE8 if (!$.w.getComputedStyle) { element.style.filter = 'alpha(opacity=' + (startParams.opacity - (startParams.opacity - (percent ? (val / 100) : val))*factor) * 100 + ')'; } else { element.style.opacity = startParams.opacity - (startParams.opacity - (percent ? (val / 100) : val))*factor; } } break; case 'marginTop' : x = function(factor, val, percent) { element.style.marginBottom = 'auto'; element.style.marginTop = startParams.marginTop - (startParams.marginTop - (percent ? (startParams.height * val / 100) : val))*factor + 'px'; }; break; case 'marginBottom' : x = function(factor, val, percent) { element.style.marginTop = 'auto'; element.style.marginBottom = startParams.marginBottom - (startParams.marginBottom - (percent ? (startParams.height * val / 100) : val))*factor + 'px'; }; break; case 'marginLeft' : x = function(factor, val, percent) { element.style.marginRight = 'auto'; element.style.marginLeft = startParams.marginLeft - (startParams.marginLeft - (percent ? (startParams.width * val / 100) : val))*factor + 'px'; }; break; case 'marginRight' : x = function(factor, val, percent) { element.style.marginLeft = 'auto'; element.style.marginRight = startParams.marginRight - (startParams.marginRight - (percent ? (startParams.width * val / 100) : val))*factor + 'px'; } break; // , default : x = function(){}; } // exec.push([x, val, percent]); } var eLength = exec.length;
最後に、メカニズムの核心に到達しました
アニメーションとは何かを考えてみましょう。 現実の世界では、どんな動きも時間内に展開するアイデアです。
剣を振るSa
武士が剣を振らないように
しかし、彼の剣を振っただけ
プログラミングの世界では、あらゆる詩をデジタルに変換する必要があります。 つまり、より重要なことは、理解可能な値を使用して、ある時点で要素を記述することです。
考えてみましょう-「瞬間」が必要ですか? ミリ秒からの時間間隔で要素の動作を制御できます。 実際、当然ながら、要素の記述に必要な間隔は、ブラウザーのパフォーマンスと個々の個別の写真を1つのシーンに入れる人間の脳の能力との間の妥協です。 経験的に、30ミリ秒が適切であることがわかりました。
言い換えると、アニメーションは、一定の間隔で、要素の状態の連続的な変化です。 そして、同じ期間、setIntervalがあります:
el.ltAnimateInterval = $.w.setInterval(function(){ _animating(); }, options.tick);
これがアニメーションエンジンです。
注意 :間隔を要素のプロパティとして設定します。これは、今後アニメーションを停止する(つまり間隔をリセットする)ために外部からアクセスする必要があるためです。
最後に、アニメーションを実行するたびに指定した間隔で実行される要素をレンダリングする機能:
コード8
// jumpToEnd - true/false - , , // function _animating(param, jumpToEnd, callback) { counter++; // 0 1 var progress = counter / animationLength; // stopAnimation if (param == animationLength) { $.w.clearInterval(el.ltAnimateInterval); // if (jumpToEnd) _step(getProgress(1)); // el.ltAnimateQueue.splice(0, 1); // el.ltAnimateIsDoing = false; // , if (!callback) { try { options.cbDone(); } catch(e) { doFail(e); } } else { try { callback(); } catch(e) { doFail(e); } } return false; } // , if (progress > 1) { // , (progress , 0.99...) _step(getProgress(1)); $.w.clearInterval(el.ltAnimateInterval); // el.ltAnimateQueue.splice(0, 1); // el.ltAnimateIsDoing = false; try { options.cbDone(); } catch(e) { doFail(e); } return false; } _step(getProgress(progress)); }
ご覧のとおり、関数コードの最初の2つのブロックはアニメーションをオフにするメカニズムであり(あまり解析する必要はないと思います)、レンダリング自体は_step(getProgress(progress))関数で行われます:
function _step(factor) { for (var i=0; i < eLength; i++) { var s = exec[i][0], val = exec[i][1], percent = exec[i][2]; s(factor, val, percent); } }
ここでは、すべてを可能な限り詳細に分析します。
- 以前に計算したeLength-アニメーションディレクティブのリストの長さ(「要素をどうするか?」)
- sは、要素のパラメーターを変更する関数です(doAnimationのswitch(命令...)を参照)
- val-アニメーションが到達するパラメーターの最終値
- percent-パラメーターはパーセンテージとして設定されるかどうか
次に、この関数が呼び出される要因について説明します。 これは計算されたパラメーターであり、特定の時点で要素のパラメーターを(初期値から最終値に向かって)どれだけ変更するかを示します。これは、0から1の間隔のタイムライン上のポイントとして理解されます。
// 0 1 var progress = counter / animationLength;
等速でセグメントに沿って歩くことも、標準のアニメーション関数のいずれかの動作に従うこともできます。
// , function getProgress(p) { switch (options.easing) { case 'linear' : return p; break; case 'swing' : return 0.5 - Math.cos(p * Math.PI ) / 2; break case 'quad' : return Math.pow(p, 2); break; case 'cubic' : return Math.pow(p, 3); break; default : return p; } }
ロシア語では、これはすべて複雑に聞こえます、はい(または、ロシア語の使用方法が十分にわかりません)。 しかし、図はより明確で、横軸は時間、縦軸は変数パラメーターの値です。
したがって、メカニズムの本質は次のとおりです:定期的にgetProgressにアニメーションのどの段階(開始点から最終点への途中)を要求し、_stepでこの知識を使用して、リストからパラメーターを変更する機能を実行します。
doAnimationで最後に記述するのは、アニメーション停止呼び出しインターフェイスです。
コード9
// el.stopAnimation = function(jumpToEnd, callback) { _animating(animationLength, jumpToEnd, callback); // if (el.ltAnimateTimeouts) { for (var i=0; i < el.ltAnimateTimeouts.length; i++) { $.w.clearTimeout(el.ltAnimateTimeouts[i]) } el.ltAnimateTimeouts = []; } }
アニメーションを停止する呼び出しは簡単です。単純に要素を示し、停止時にアニメーションの終点に移動するかどうかを示し、必要に応じて新しいコールバックを示します。
アニメーションを停止するには、別のグローバル関数があります。
コード10
/* * : , (true/false) (true/false), */ $.w.ltAnimateStop = function(el, jumpToEnd, callback) { // , if (!el.ltAnimateInterval) return false; el.stopAnimation(jumpToEnd, callback); };
さて、最新のものはエラーハンドラです。 これは$ .w.ltAnimateで記述され、必要に応じて呼び出されます。
コード11
// function doFail(text) { if (self._debug._enabled) { if ((typeof text != "string") || !text) text = " [id: " + id + "] - ."; $.w.console.log("ltAnimate(): ! " + text); } if (opts.cbFail) { try { opts.cbFail(); } catch (e) { $.w.console.log("ltAnimate(): ! [id: " + id + ", " + e.name + ": " + e.message + "]"); } } }
→ ここで自分で長方形を動かしてみることができます
そこで、3つの機能すべての完全なソースコードを取得できます。
_____________________________________
追伸 コードは1年半前に書かれたので、そのように行われたいくつかのポイントの詳細は既にメモリから消去され始めています。 ただし、質問する場合は、コードを書いたときに念頭に置いていたものをできる限り正確に記憶しようとします。
もちろん、提示されたコードは理想的ではありません。 建設的なコメントと、エラーと追加の表示に満足しています。
また、別のアプローチを使用して、ブラウザーで要素のアニメーションを開発するタスクにアプローチする方法を議論することも興味深いでしょう。