Quantcast
Channel: CSSタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8644

画像プレビューをCSSのtransformで実装しようとしたけど諦めた話

$
0
0

書きながらいろいろ情報が整理され、ただ変な方向に向かっていって苦しんでいただけだったことがわかっただけのポエムになってしまった気がする。

つくろうと思ったもの

  • Vue.jsで実装
  • よくある、画像プレビュー
  • 拡大、縮小、回転、ドラッグができる
  • ドラッグしたとき、プレビュー画像が見切れないように制御する

これを、transform: scale(1) rotate(0deg) translate(0,0);みたいな感じで制御しようとした。

なぜ諦めたか

rotateすると、translateの方向も回転する

transform: scale(1) rotate(0deg) translate(0,0);だけで完結したかったが、、
rotateで回転している時に、translateで移動させようとすると、思った方向に移動しなかった。
例えば、180度回転させた状態でドラッグすると、思った方向と真逆に移動する・・・
これは、厳密に言うと、解決できないことはない。と思う。。
ただ、解決方法は、ドラッグの移動先を回転に合わせて全て調整するということ。
今回つくろうとしていた回転の仕様は90度刻みの4パターンだったので、まだ可能な範囲だったが、コードが増えるだけだと思い、泣く泣く違う方法(position: absolute;で制御)で対応することにした。

また、ドラッグがない場合は特に問題なくtransformで完結できる。

最終的につくったもの

  • scaleとrotateはtransformで制御
  • ドラッグはpositionで制御
<template><divref="drag_area"class="drag_area"@mousedown="dragStart($event)"@mousemove="dragMove($event)"@mouseup="dragEnd($event)"@mouseleave="dragEnd($event)"><imgref="preview_image"class="preview_image"src="image.png":style="{transform: 'scale('+scale+') rotate('+rotate+'deg)',top: new_pos.y+'px',left: new_pos.x+'px'}">
    <divclass="control_btn"><button@click="zoomIn"></button><button@click="zoomOut"></button><button@click="rotateLeft"></button><button@click="rotateRight"></button><button@click="resetStyle"></button></div></div></template><script>exportdefault{data(){return{scale:0.8,rotate:0,new_pos:{x:0,y:0,},prev_pos:{x:0,y:0,},isDrag:false,}},mounted(){this.resetStyle();},methods:{zoomIn(){this.scale*=1.2;if(this.scale>10){this.scale=10;}},zoomOut(){this.scale/=1.2;if(this.scale<0.1){this.scale=0.1;}},rotateLeft(){this.rotate-=90;},rotateRight(){this.rotate+=90;},resetStyle(){this.scale=0.8;this.rotate=0;this.new_pos.x=(this.$refs.drag_area.offsetWidth-this.$refs.preview_image.offsetWidth)/2;this.new_pos.y=(this.$refs.drag_area.offsetHeight-this.$refs.preview_image.offsetHeight)/2;this.prev_pos={x:0,y:0,};},dragStart(e){e.preventDefault();this.isDrag=true;this.prev_pos.x=e.pageX;this.prev_pos.y=e.pageY;},dragMove(e){if(this.isDrag){// マウス座標の差分letmoved_x=e.pageX-this.prev_pos.x;letmoved_y=e.pageY-this.prev_pos.y;// ドラッグで移動できる範囲を制御leta_w=this.$refs.drag_area.offsetWidth;leta_h=this.$refs.drag_area.offsetHeight;letb_w=this.$refs.preview_image.offsetWidth;letb_h=this.$refs.preview_image.offsetHeight;letc_w=b_w*this.scale;letc_h=b_h*this.scale;letdiff_w=(c_w-c_h)/2;letdiff_h=(c_h-c_w)/2;letmax_x=a_w-(b_w-c_w)/2-40;letmin_x=-(b_w-(b_w-c_w)/2-40);letmax_y=a_h-(b_h-c_h)/2-40;letmin_y=-(b_h-(b_h-c_h)/2-40);// 回転したときの差分if(this.rotate/90%2!==0){max_x-=diff_w;min_x+=diff_w;max_y-=diff_h;min_y+=diff_h;}// 移動距離を反映letnew_pos_x=this.new_pos.x+moved_x;letnew_pos_y=this.new_pos.y+moved_y;if(new_pos_x>max_x){this.new_pos.x=max_x;}elseif(new_pos_x<min_x){this.new_pos.x=min_x;}else{this.new_pos.x=new_pos_x;}if(new_pos_y>max_y){this.new_pos.y=max_y;}elseif(new_pos_y<min_y){this.new_pos.y=min_y;}else{this.new_pos.y=new_pos_y;}// マウス座標を更新this.prev_pos.x=e.pageX;this.prev_pos.y=e.pageY;}},dragEnd(e){this.isDrag=false;},}}</script><stylelang="scss"scoped>// ※デザインの部分は省略してます。.drag_area{cursor:move;position:relative;overflow:hidden;.preview_image{position:absolute;top:0;left:0;width:auto;height:auto;max-width:100%;max-height:100%;transform:scale(.8)rotate(0deg);}}</style>

少し解説

インラインスタイルをバインディング

  • transform: scale(.8) rotate(0deg);top: 0;left: 0;の値を更新する。
<imgref="preview_image"class="preview_image"src="images.png":style="{transform: 'scale('+scale+') rotate('+rotate+'deg)',top: new_pos.y+'px',left: new_pos.x+'px'}">

@mousedown="dragStart($event)"でドラッグ開始

  • 今回、画像プレビューだったので、drag_areaにドラッグイベントをセットしているが、対象のみにイベントをつける場合はpreview_imageにしても大丈夫はず。
  • isDragがtrueのときだけマウスの動きに合わせてプレビュー画像を移動する。
  • offsetX,offsetYを使ったら、誤作動したので、pageX,pageYを使用して、ドラッグを開始した位置を保存。
dragStart(e){e.preventDefault();this.isDrag=true;this.prev_pos.x=e.pageX;this.prev_pos.y=e.pageY;},

@mousemove="dragMove($event)"でプレビュー画像を移動する

最低限の部分
  • dragStart()で保存した開始位置と今のマウス座標の差分(=ドラッグした距離)を今のtopとleftの値に加算して移動させる。
dragMove(e){if(this.isDrag){// マウス座標の差分letmoved_x=e.pageX-this.prev_pos.x;letmoved_y=e.pageY-this.prev_pos.y;// 移動距離を反映this.new_pos.x+=moved_x;this.new_pos.y+=moved_y;// マウス座標を更新this.prev_pos.x=e.pageX;this.prev_pos.y=e.pageY;}},
「ドラッグしたとき、プレビュー画像が見切れないように制御する」の部分

いろいろな要因から複雑に考えてしまっていたが、結構シンプルな計算だった。
一応画像で説明します。
また、-40は見切れないように残す大きさなので、説明では無視します。

A:ドラッグできるエリア(drag_area)
B:プレビュー画像のデフォルトサイズ(preview_imageのscale(1))
C:プレビュー画像の表示サイズ(preview_imageのscale(0.8))
とします。
preview_1.png

ここでの問題は、支点が、scaleしても変わらないということ
具体的に言うと、Cの支点もBの左上の位置ということです。

Cの右に移動できる範囲(max_x)は、
scaleが1だったら、
Aの横幅

scaleが1ではなかったら、
Aの横幅 - BとCの横幅の差分の半分
preview_2.png

Cの左に移動できる範囲(min_x)は、
Bの横幅 - BとCの横幅の差分の半分
preview_3.png
となります。

// ドラッグで移動できる範囲を制御leta_w=this.$refs.drag_area.offsetWidth;leta_h=this.$refs.drag_area.offsetHeight;letb_w=this.$refs.preview_image.offsetWidth;letb_h=this.$refs.preview_image.offsetHeight;letc_w=b_w*this.scale;letc_h=b_h*this.scale;letmax_x=a_w-(b_w-c_w)/2;letmin_x=-(b_w-(b_w-c_w)/2);letmax_y=a_h-(b_h-c_h)/2;letmin_y=-(b_h-(b_h-c_h)/2);

あとは、このmax,minの値でドラッグの移動距離を制御する。

// 移動距離を反映letnew_pos_x=this.new_pos.x+moved_x;letnew_pos_y=this.new_pos.y+moved_y;if(new_pos_x>max_x){this.new_pos.x=max_x;}elseif(new_pos_x<min_x){this.new_pos.x=min_x;}else{this.new_pos.x=new_pos_x;}if(new_pos_y>max_y){this.new_pos.y=max_y;}elseif(new_pos_y<min_y){this.new_pos.y=min_y;}else{this.new_pos.y=new_pos_y;}
回転したときの調整

これも、諦めかけた問題、、
rotateしても、支点の位置は変わらない

どういうことかというと、
例えば、横長の画像を90度回転させて、縦長にしたとする。
しかし、支点は、回転する前の位置なので、それを考慮してドラッグ可能範囲を設定しなければならない。
preview_4.png
しかしこれは、冷静に考えれば、今回の90度刻みの仕様なら対応できると気づき、
縦向きになったとき(rotateを90で割り、奇数かどうかで判断)に
Cの縦横の差分の半分を足すか引くかで調整できた。

letdiff_w=(c_w-c_h)/2;letdiff_h=(c_h-c_w)/2;// 回転したときの差分if(this.rotate/90%2!=0){max_x-=diff_w;min_x+=diff_w;max_y-=diff_h;min_y+=diff_h;}

@mouseup="dragEnd($event)"@mouseleave="dragEnd($event)"でドラッグ終了

dragEnd(e){this.isDrag=false;},

上記の理由で結局使わないことになったが、rotateが絡まなければ解決した、scaleとtranslateの兼ね合い

  • 私をややこしくした原因は、scaleの支点をセンターで行いたい関係で、translateの支点も左上ではなくセンターだったことである。
  • ドラッグ可能範囲の計算をとても複雑に考えてしまった。。
  • scaleの値はtranslateにも影響があり、なかなかうまく調整できなかったが、scaleの値を割って計算することで、辻褄を合わせることができた。
  • transform: scale(1) translate(0,0);なら、問題なく調整できた。

差分のみ書きます。

  • transform: scale(.8) translate(0,0);をインラインスタイルにバインディング
<imgref="preview_file"class="preview_file":src="images[activeIndex].data":style="{transform: 'scale('+scale+') translate('+new_pos.x+'px,'+new_pos.y+'px)'}">

ドラッグ可能範囲の計算

↓なぞに考えてしまった内容
支点がセンターなので、
Cの右に移動できる範囲(max_x)は、
scaleが1かつ、AとBの横幅が同じだったら、
Cの横幅

scaleが0.8で、AとBの横幅が同じだったら、
(Cの横幅 + BとCの横幅の差分の半分) ÷ 0.8

scaleが0.8で、AとBの横幅が違ったら、
(Cの横幅 + BとCの横幅の差分の半分 + AとBの横幅の差分) ÷ 0.8

このように、scaleの値を割ることによって、扱うべき値をもとめることができる。

と、なぞに複雑に考えていたが、(本当はさらにからみ合ってこれと同じ値を導き出していた)
パズルをはめ直してみると、
(Aの横幅 - BとCの横幅の差分の半分) ÷ 0.8
で問題なかった。。

ようは、scale分割って戻せばよかったのだった。。。

letmax_x=(a_w-(b_w-c_w)/2)/this.scale;letmin_x=-(b_w-(b_w-c_w)/2)/this.scale;letmax_y=(a_h-(b_h-c_h)/2)/this.scale;letmin_y=-(b_h-(b_h-c_h)/2)/this.scale;

お疲れ様でした。

参考サイト

https://fuwafuwac.com/?p=748
https://qiita.com/yukiB/items/cc533fbbf3bb8372a924


Viewing all articles
Browse latest Browse all 8644

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>