個人でWebサービス作る人の雑記ブログ

Webサイトを個人で作ってる人が人の役に立ちそうなことを書いていく雑記ブログです

【JQuery】画像トリミングプラグイン「Croppic.js」が富豪コーディングだったので軽量化しなければいけなくなった話【重すぎ】

事件の経緯

運営しているサイトでWeb上で画像をトリミングさせる機能(Twitterのプロフィールアイコン設定のときのアレみたいなやつ)を実装しているのですが、

ある日サイトをご利用いただいているユーザ様から

 

「トリミングしようと思って画像をタッチして移動させようにも動かせないです。 in Android

 

というご連絡をいただきました。

はて、手持ちのスマホiPhone)で検証してみるも問題なく動かせる・・・何なのか。。

 

使ってるトリミングプラグイン

お洒落なサイトが印象的ですね。

www.croppic.net

 

自宅のテスト用Android実機で試してみる

「確かに動かせない。」

 

フロント側のメモリ不足あたりを疑いつつ、

なぜだろうなぜだろうと色々試行錯誤の上調べていった結果(プラグインごと乗り換えようともしたので丸3日かかりましたorz)、1件の記事にたどり着きました。

qiita.com

 

まさかねー!

だってほら、すごくお洒落なサイトだしブランド感も漂ってるし、新卒社会人みたいなコーディングになんてなってないよねー!(かく言う私も上の記事を見るまで意識してできてなかったですがw)

 

 

まさかでした。

重厚なmousemoveリスナ内のロジックがそこには広がってました、はい。

そこそこ潤沢なメモリを搭載した媒体なら耐えられそうですが、Android機とかそこらになると固まりそうなメモリ食いしん坊がそこにいました。

 

修正案

上記のサイトを参考にしつつ、

レンダリング用にフレームレートを設ける

・スクロール系リスナ内ではイベントの登録のみにする

ように修正してみました。(なお、修正したのはドラッグ系ロジックのみです。多分ここだけの修正で大丈夫だとは思っているのですが・・・お気づきの点があれば教えてくださいm(__)m)

 

修正箇所 #454あたり

initDrag: function() {
	var that = this;
	
	that.img.on("mousedown touchstart", function(e) {
				
		var pageX;
        var pageY;
        var userAgent = window.navigator.userAgent;
        if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
            pageX = e.originalEvent.touches[0].pageX;
            pageY = e.originalEvent.touches[0].pageY;
        } else {
            pageX = e.pageX;
            pageY = e.pageY;
        }
		
		var z_idx = that.img.css('z-index'),
        drg_h = that.img.outerHeight(),
        drg_w = that.img.outerWidth(),
        pos_y = that.img.offset().top + drg_h - pageY,
        pos_x = that.img.offset().left + drg_w - pageX;
		
		that.img.css('z-index', 1000).on("mousemove touchmove", function(e) {
			
			var imgTop;
			var imgLeft;
			
			if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
                imgTop = e.originalEvent.touches[0].pageY + pos_y - drg_h;
                imgLeft = e.originalEvent.touches[0].pageX + pos_x - drg_w;
            } else {
                imgTop = e.pageY + pos_y - drg_h;
                imgLeft = e.pageX + pos_x - drg_w;
            }
			
			that.img.offset({
				top:imgTop,
				left:imgLeft
			}).on("mouseup", function() {
				$(this).removeClass('draggable').css('z-index', z_idx);
			});
			
			if(that.options.imgEyecandy){ that.imgEyecandy.offset({ top:imgTop, left:imgLeft }); }
								
			if (that.objH < that.imgH) {
				if (parseInt(that.img.css('top')) > 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0);}}
				var maxTop = -( that.imgH - that.objH); if (parseInt(that.img.css('top')) < maxTop) { that.img.css('top', maxTop); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', maxTop); }}
			}else{
				if (parseInt(that.img.css('top')) < 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0); }}
				var maxTop =  that.objH - that.imgH; if (parseInt(that.img.css('top')) > maxTop) { that.img.css('top', maxTop);if (that.options.imgEyecandy) {that.imgEyecandy.css('top', maxTop); }}
			}

			if (that.objW < that.imgW) {
				if( parseInt( that.img.css('left')) > 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
				var maxLeft = -( that.imgW-that.objW); if( parseInt( that.img.css('left')) < maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
			}else{
				if( parseInt( that.img.css('left')) < 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
				var maxLeft = ( that.objW - that.imgW); if( parseInt( that.img.css('left')) > maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
			}
			if (that.options.onImgDrag) that.options.onImgDrag.call(that);
			
		});

	}).on("mouseup", function() {
		that.img.off("mousemove");
	}).on("mouseout", function() {
		that.img.off("mousemove");
	}).on("touchend", function() {
		that.img.off("touchmove");
	});
	
}

 

修正後

initDrag: function() {
	var that = this;

	// 独自に軽量に修正
	var fps = 30; // 30fpsに制限
	var frameTime = 1000 / fps;
	var isAnimated = false;
	var lastTouchInfo = null;
	var userAgent = window.navigator.userAgent;

	var z_idx = null,
	drg_h = null,
    drg_w = null,
    pos_y = null,
    pos_x = null;

	that.img.on("mousedown touchstart", function(e) {
		// 最初のデータを入れてアニメーションを開始
		e.preventDefault();
		if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
	    	lastTouchInfo = e.originalEvent.touches[0];
		}
		else{
			lastTouchInfo = e;
		}
	    isAnimated = true;

	    pageX = lastTouchInfo.pageX;
        pageY = lastTouchInfo.pageY;

        z_idx = that.img.css('z-index');
        drg_h = that.img.outerHeight();
        drg_w = that.img.outerWidth();
        pos_y = that.img.offset().top + drg_h - pageY;
        pos_x = that.img.offset().left + drg_w - pageX;

	    dragAnimation();
	});

	that.img.on("mousemove touchmove", function(e) {
	    if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
	    	lastTouchInfo = e.originalEvent.touches[0];
		}
		else{
			lastTouchInfo = e;
		}
	});

	that.img.on("mouseup touchend touchcancel", function() {
		// アニメーションの終了
	    isAnimated = false;
	    lastTouchInfo = null;
	});

	function dragAnimation(){
		// isAnimatedフラグが立ってなかったら終了
		if (!isAnimated) {
	        return; 
	    }

        var imgTop = lastTouchInfo.pageY + pos_y - drg_h;
        var imgLeft = lastTouchInfo.pageX + pos_x - drg_w;

        that.img.offset({
			top:imgTop,
			left:imgLeft
		}).on("mouseup", function() {
			$(this).removeClass('draggable').css('z-index', z_idx);
		});

		if(that.options.imgEyecandy){ that.imgEyecandy.offset({ top:imgTop, left:imgLeft }); }
							
		if (that.objH < that.imgH) {
			if (parseInt(that.img.css('top')) > 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0);}}
			var maxTop = -( that.imgH - that.objH); if (parseInt(that.img.css('top')) < maxTop) { that.img.css('top', maxTop); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', maxTop); }}
		}else{
			if (parseInt(that.img.css('top')) < 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0); }}
			var maxTop =  that.objH - that.imgH; if (parseInt(that.img.css('top')) > maxTop) { that.img.css('top', maxTop);if (that.options.imgEyecandy) {that.imgEyecandy.css('top', maxTop); }}
		}

		if (that.objW < that.imgW) {
			if( parseInt( that.img.css('left')) > 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
			var maxLeft = -( that.imgW-that.objW); if( parseInt( that.img.css('left')) < maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
		}else{
			if( parseInt( that.img.css('left')) < 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
			var maxLeft = ( that.objW - that.imgW); if( parseInt( that.img.css('left')) > maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
		}
		if (that.options.onImgDrag) that.options.onImgDrag.call(that);

        // 次のanimationを登録
		setTimeout(dragAnimation, frameTime);

	}
}

 

どなたかのお役に立てば光栄ですm(__)m

 

 

【追記:2019/2/28】

Android機でスクロールできない問題の根本原因違いました。。

どうやら

 

that.img.on("mousedown touchstart", function(e) {...
...
that.img.css('z-index', 1000).on("mousemove touchmove", function(e) {...

 

といった「that.img.on(...」というイベント登録のところに原因があるようです。

アプリケーションのつくりにもよると思いますが、レンダリングする画像データが重かったりすると、that.imgに正常にイベントが登録されないようです。

よって、私の場合は一つ上の上位divにイベントを登録するように修正することで、この問題を回避することができました。