ネイティブjsは画像のトリミングをゼロから実現します

エフェクト画像

                        

前回の記事では、画像圧縮の実装方法を紹介しました。この記事では、主に、これに基づいて個別に実装される画像トリミング機能について説明します。

写真をアップロードするファイルをクリックして選択します。[切り抜き]をクリックすると、切り抜きボックスが表示されます。切り抜きボックスを移動または拡大すると、下に切り抜き画像が生成されます。ドロップダウンボックスをクリックして、切り抜きの圧縮率を選択します。最後に画像をローカルにダウンロードした後、[生成]をクリックしてトリミングを圧縮します。

githupの完全なコードアドレス

 

HTML構造

                                            

対応するページマップ

                         

 

トリミング機能

 

                           

思考分析

ユーザーがクリックしてファイルを選択すると、画像がbase64エンコードに変換され、ページでプレビューされます。トリミング機能の中核は、中央に破線のトリミングボックスを実装することです。

  • トリミングフレームの最初の基本機能は、ユーザーがフレーム上でマウスを押すとフレームをドラッグでき、ボタンを離した後はドラッグできなくなることです。
  • トリミングボックスの右下隅にある小さな黄色の正方形を伸ばして、ボックスをクリックしてドラッグすることでボックスのサイズを変更できます。

上記の2つの機能は、トリミングを実現するための重要なポイントです。最終的な目標は、タスクが完了した場合でも、トリミングによってスケールデータを取得することです。つまり、左側の境界からトリミングボックスの左側、上部の境界から上部です。ミッションは、左、上、幅、高さの4つのデータを取得することです。次に、他の方法を使用して、これら4つのデータからトリミングされた画像を生成します。

 

クロップボックスを移動する

まず、クロップボックスの最初のコア機能を実装しましょう。フレーム上でマウスを押した後はフレームをドラッグでき、ボタンを離した後はドラッグできません。

 

 

最初にコンストラクタcropImgを定義し、それが生成するオブジェクトを使用してトリミングを実現します。現在、コンストラクタ内のthis.targetに注意する必要があるのは、div要素であり、画像と同じサイズであり、画像全体です。 divを背景画像の形式で塗りつぶします。上の画像から伸びる破線のボックスです。This.targetは元の画像と同等です。説明の便宜上、this.targetを画像ボックスと呼びます。

function cropImg(options) {
  this.target = document.getElementById(options.target) || document;
  this.width = 0;
  this.height = 0;
  this.container_width = 0;
  this.container_height = 0;
  this.mouse_index = 0;
  this.callback = options.callback || function () {};
  this.fun_list = []; //所有绑定的事件存储起来,插件销毁时解绑
  this.init();
}

this.init()関数では、クロップボックスをレンダリングする次のロジックが実行されます。

移動機能を実行する前に、ページ上にトリミングボックスをレンダリングする必要があります。そうしないと、ボックスなしでトリミングボックスを移動する方法について説明できません。トリミングボックスもdivであり、画像ボックス内にレンダリングされます。

  • 事前に額縁のアスペクト比と一致するようにトリミングフレームのアスペクト比を設定し、トリミングフレームが額縁に対して絶対的に配置されるようにします。
  • 初期レンダリングトリミングフレームの幅と高さは、額縁の3分の1に設定されています(スケールデータは自由に設定できます)
  • クロップボックスの最初のレンダリングは、画像ボックスの中央にあります

次のコードは、上記の3つのことを実行し、クロップフレームオブジェクトとしてdiv要素コンテナを作成し、額縁の幅と高さを計算してクロップフレームの幅と高さを取得し、その左右を次のように設定します。真ん中の額縁にあります。

cropImg.prototype.renderBox = function () {
  const width = (this.container_width = parseInt( //算出图片框的宽
    getComputedStyle(this.target).width
  ));
  const height = (this.container_height = parseInt( //算出图片框的高
    getComputedStyle(this.target).height
  ));
  this.radio = width / height; //算出宽高比例
  this.width = parseInt(width / 3); //得到裁剪框的宽
  this.height = parseInt(height / 3); //得到裁剪框的高
  const container = document.createElement('DIV'); //创建裁剪框
  container.style.width = `${this.width}px`;
  container.style.height = `${this.height}px`;
  container.style.left = `${this.container_width/2 - this.width/2}px`; //初次加载裁剪框放到正中间的位置 
  container.style.top = `${this.container_height/2 - this.height/2}px`;
};

これで、クロップボックスコンテナを画像ボックスthis.targetに接続するだけで、ページに表示されます。その前に、クロップボックスのモバイル機能の準備が整いました。モバイル機能の開発方法は何でもありません。クロップボックス以上のもの。divはイベントバインディングを行います。

  • マウスダウンイベントをクロップボックスにバインドします(mousedown)。このイベントでは、マウスが押されたかどうかを示す状態を定義する必要があります。クロップボックスはマウスが押されたときにのみドラッグできるためです。イベント関数eventは、現在のマウス押下の開始位置を記録しますstart_x、start_y
  • マウス移動イベントをクロップボックス(マウスモーブ)にバインドします。イベントがトリガーされるたびに、マウスがまだ押された状態にあるかどうかを確認する必要があります。そうでない場合は、移動を拒否します。イベントがトリガーされると、現在のマウス位置pageXは、イベントオブジェクトイベントを取得することで取得できます。pageY。pageX-start_xでマウスの水平方向の移動距離を取得し、pageY-start_yでマウスの垂直方向の距離を取得します。マウスの水平方向と垂直方向の移動距離を割り当てます。クロップボックスの左右に移動して、マウスの移動量を確認します。クロップボックスは、対応する方向に移動する距離にも追従します。移動するたびに、start_xとstart_yを現在のマウス位置pageXに更新する必要があります。次の移動イベントの準備をするpageY。
  • 習慣に応じて、マウスダウンイベントもクロップボックスにバインドする必要があります。その機能は、マウスアップ状態に変更することです。マウスがアップしている限り、もう一度押さない限り、移動イベントを再度トリガーすることはできません。マウスの動きしかし、実際の開発では、現象が見つかりました。mouseupイベントがdivにバインドされている場合、mouseupは、マウスがdiv内で解放されたときにのみトリガーされます。マウスがdivの外側に移動されて解放されると、 mouseupイベントは完全に解放されます。無効です。クロップボックスをよりスムーズに移動させるために、最終的にmouseupイベントをグローバルドキュメントにバインドすることにしました。

移動機能を実現するための鍵は、上記の3つのマウスイベントmousedown、mousemove、mouseupを記述することです。現在のプロジェクトでは、切り抜きボックスをこれら3つのイベントにバインドするだけでなく、右下隅の小さな正方形をバインドする必要があります。トリミングボックスのもマウスイベントにバインドする必要があります。以下に示すように、トリミングフレームを引き伸ばすには:

                    

ストレッチと移動の両方でマウスイベントをバインドする必要があるため、他の関数が呼び出すためにイベントをバインドするロジックを個別に抽出できます。次に、mousedown、mousemove、mouseupイベントをクロップボックスにバインドする必要があります。どのように呼び出すのですか?

 this.bindMouseEvent({
    mousedown: {
      element: container,//container是裁剪框的dom对象
    },
    mousemove: {
      element: container,
      callback(e, start_x, start_y) {
          console.log("每次触发移动事件后的回调函数");   
      },
    },
  });

bindMouseEvent関数はパラメーターオブジェクトを渡す必要があり、キーはバインドするイベントの名前、要素はイベントにバインドする必要のあるdom要素、コールバックは各イベントがトリガーされた後のコールバック関数です。

上記のように、クロップボックスはmousedownとmousemoveの2つのイベントをバインドする準備ができており、mousemoveはコールバック関数のコールバックも定義しています。bindMouseEventのロジックはどのように記述されていますか?

次のコードはクリッピング関数のコアです。MouseUpHandlerは後でそれを調べ、forループに実装されているロジックに注意を払います。Paramsは{mousedown:{element:container}、mousemove:{の上に渡されるパラメーターオブジェクトです。 element:container、callback()()))。paramsでfor inループを直接実行して、各オブジェクトの要素とコールバックを取得します。目的は、要素でaddEventListener(key、fn)を呼び出すことです。キーはイベントです。タイプ、取得が簡単です。そしてfnバインドする必要がある関数であり、fnはthis.strategyEvent関数を呼び出すことによって取得されます。

/**
 * 对dom元素绑定鼠标点击弹起和移动事件
 */
cropImg.prototype.bindMouseEvent = function (params) {
  this.mouseUpHandler(); //处理mouseup事件

  this.mouse_index++; //每当需要绑定一次鼠标事件,mouse_index自增1,作为唯一的id标识

  for (let key in params) {
    const value = params[key]; // 得到key和value
    let { element, callback } = value;
    const defaultFn = this.strategyEvent(key, this.mouse_index); //获取默认运行函数
    if (!defaultFn) {
      //如果发现params的参数配置里面的key没有和在策略函数里定义的默认函数匹配上,那么判定当前对应的key-value是无效的
      continue;
    }
    element = Array.isArray(element) ? element : [element]; //不是数组也转化成数组
    element.forEach((ele) => {
      const fn = (e) => {
        //开始绑定事件
        defaultFn.call(this, e, callback); //某些默认策略函数需要callback参数,所以params.callback也作为参数传入
      };
      ele.addEventListener(key, fn);
    });
  }
};

this.strategyEvent関数を介してイベントバインディング関数fnを取得する必要があるのはなぜですか?これを行わなくても問題ありません。次に示すように、ifelseコードをたくさん記述する必要があります。このように記述されたコードはエレガントではなく、上記で呼び出します。StrategyEventは、キーに従ってバインドされる関数を直接取得できますdefaultFn.this.strategyEventは関数ファクトリと同等であり、キーを渡すと処理関数が返されます。ここでは戦略モードは、ifelse構造を最適化するために使用されます。

if(key === "mouseup"){
  
 ele.addEventListener(key, function(){
   
 });

}else if(key === "mousemove"){
 
 ele.addEventListener(key, function(){
   
 }); 

}else if(key === "mousedown"){

 ele.addEventListener(key, function(){
   
 });

}

bindMouseEventが行うことは非常に純粋です。これは、addEventListenerを呼び出して、さまざまなイベント関数をさまざまな要素要素にバインドすることです。イベント関数の特定のコンテンツはどこにありますか?This.strategyEventは、さまざまなイベント関数のロジックを定義します。

以下のstrategyEvent関数のキーは、「mousedown」や「mousemove」など、渡されるイベントのタイプです。idxはグローバルに一意のID識別子であり、bindMouseEvent関数が呼び出されるたびに増分します。1。現在の状況によると上記のようにフレームをクリッピングしてプルするストレッチブロックはマウスイベントにバインドする必要があり、クロップボックスがイベントにバインドされている場合の対応するidxは1に等しく、ストレッチブロックの対応するidxは2になります。

  • マウスダウンによって実行されるイベントは2つだけです。1つはマウスが押されたか上かを反映する状態を定義することです。2つ目はマウスが押されたときの位置を記録することです。
  • mousemoveイベントでは、if(!this [`mouse _ $ {idx}`])の値を使用して、マウスが押されたかどうかを検出し、バウンスされた場合は何の操作も行いません。次に、を定義します。タイマーからスロットルへ(セクションフローは主にパフォーマンスを考慮したものであり、関数の実装には影響しません)、コールバック関数はタイマーで呼び出されます。このコールバック関数は、次の図に示すように、呼び出し時に渡されるパラメーターです。 、開始位置start_xは、callback、start_y、およびtimeオブジェクトを呼び出すときに渡されます。e。コールバックの実行後に、start_xとstart_yの位置を更新します。
  •  

                                      

/**
 * 定义一些策略函数
 */
cropImg.prototype.strategyEvent = function (key, idx) {
  function mousedown(e) {
    //鼠标按下时的默认操作
    e.stopPropagation();
    this[`mouse_${idx}`] = true; //检测鼠标是否处于按下的状态
    this[`start_x${idx}`] = e.pageX;
    this[`start_y${idx}`] = e.pageY;
  }

  function mousemove(e, callback) {
    //鼠标移动时的默认操作
    e.stopPropagation();
    e.preventDefault();
    if (!this[`mouse_${idx}`]) {
      return false;
    }
    if (this[`timer${idx}`]) {
      return false;
    }
    this[`timer${idx}`] = setTimeout(() => {
      callback.call(this, e, this[`start_x${idx}`], this[`start_y${idx}`]);
      this[`start_x${idx}`] = e.pageX;
      this[`start_y${idx}`] = e.pageY;
      clearTimeout(this[`timer${idx}`]);
      this[`timer${idx}`] = null;
    }, 20);
  }

  const funList = { mousedown, mousemove };

  return funList[key];
};

コールバック関数は、mousemove関数がトリガーされるたびに呼び出されます。calback関数は、イベントオブジェクトeと初期座標位置start_xおよびstart_yを受け取ります。イベントオブジェクトeは、現在のマウス位置e.pageXおよびe.pageYを取得できます。 .pageX- start_xは、マウスの水平方向の動きの距離を取得できます。e.pageY-start_yは、マウスの垂直方向の動きの距離を取得できます。これで、マウスの動きの距離を取得できるようになったので、コールバックでクロップボックスを移動させることができます。 。

呼び出すと、コールバックのロジックが完了します。クロップボックスのmousemoveイベントがトリガーされるたびに、次のコールバック関数が呼び出されます。xはマウスの横方向の動きの距離、yはマウスの縦方向の距離です。動き。xとyに加えてトリミングボックスの左側と上部がトリミングボックスに再割り当てされるため、トリミングボックスの位置はマウスの動きに合わせてスライドします。

 this.bindMouseEvent({
    mousedown: {
      element: container,
    },
    mousemove: {
      element: container,
      callback(e, start_x, start_y) {
        const x = e.pageX - start_x;
        const y = e.pageY - start_y;
        let top = parseInt(getComputedStyle(container).top);
        let left = parseInt(getComputedStyle(container).left);
        top += y;
        left += x;
        container.style.top = `${top}px`;
        container.style.left = `${left}px`;
      },
    },
  });

上記では、mousemoveイベントとmousedownイベントのみが記述されており、mouseupイベントは個別に処理されます。ロジックは非常に単純です。マウスのバウンスをグローバルに監視し、トリガーされるとマウスの状態を変更します。


/**
 * mouseup处理函数
 */
cropImg.prototype.mouseUpHandler= function(){
 if (this.mouse_index > 0) {  //已经绑定过mouseup事件了,mouseup事件绑定一次即可
    return false;
  }
  document.addEventListener('mouseup', ()=>{
     Array.from(Array(this.mouse_index)).forEach((value, idx) => {
       this[`mouse_${idx + 1}`] = false;
     });
  });  
}

 

クロップボックスを伸ばす

 

                  

上の図の赤い線でマークされた小さな正方形は、トリミングフレームのサイズを変更するために引き伸ばすことができます。その実現は、最初に小さな正方形をレンダリングしてから、mousedownイベントを小さな正方形にバインドしますが、バインドしないことです。小さな正方形の面積が小さすぎて、マウスが簡単にスライドしてストレッチ効果に影響を与える可能性があるため、mousemoveイベントを小さな正方形にバインドします。したがって、mousemoveイベントをクロップフレームと同時に画像フレーム、および機能がトリガーされたときにトリミングフレームが比例して引き伸ばされます。

コードは次のとおりです。小さな正方形のdomオブジェクトとしてdiv要素シンボルを作成し、マウスイベントをdomオブジェクトにバインドします。

  • mousedownイベントを小さな正方形にバインドします。this.maskはクロップボックスのdomオブジェクトです。
  • mousemoveイベントを額縁とトリミングフレームにバインドし、各イベントがトリガーされた後にcallback.calback関数を呼び出して、e.pageX-start_xを介したマウスの横方向の動きの距離xを計算します。トリミングフレームの幅+ xは等しいストレッチ後のクロップフレームの幅に対して幅は同じ比率でストレッチする必要があるため、このラジオを使用してクロップフレームの幅とアスペクト比を計算できます。ストレッチ後の高さを計算できます。トリミングフレームに幅と高さを割り当てると、dom要素のサイズが変更されます。
  • クロップボックスthis.maskの要素に小さな正方形の記号を入れて、ページにレンダリングします。
/**
 * 渲染右下角的拉升框
 */
cropImg.prototype.renderSymbol = function () {
  const symbol = document.createElement('DIV');
  symbol.setAttribute('class', 'symbol');
  this.bindMouseEvent({
    mousedown: {
      element: symbol,
    },
    mousemove: {
      element: [this.target, this.mask],
      callback(e, start_x) {
        const x = e.pageX - start_x;
        const width = parseInt(getComputedStyle(this.mask).width) + x;
        const height = parseInt((width * 1) / this.radio);
        this.mask.style.width = `${width}px`;
        this.mask.style.height = `${height}px`;
        this.width = width;
        this.height = height;
      },
    },
  });
  this.symbol = symbol;
  this.mask.appendChild(symbol);
};

 

外部コールクロッププラグイン

ここで、外部で開発されたばかりのcropImgプラグインを呼び出します。「box」は、プラグインで定義されたthis.targetである額縁のIDです。コールバック関数が現在の額縁のスケールデータを返すことを期待しています。 、つまり、クロップフレームの幅左の境界線から左と上、上の境界線から上。Container_heightとcontainer_widthは、額縁の幅と高さです。

new cropImg({
      target: 'box',
      callback({left,top,width,height,container_height,container_width,}) {
       
      },
    });

this.callbackプロパティを外部呼び出しのコンストラクターに追加します

function cropImg(options) {
  this.target = document.getElementById(options.target) || document;
  this.width = 0;
  this.height = 0;
  this.container_width = 0;
  this.container_height = 0;
  this.mouse_index = 0;
  this.callback = options.callback || function () {};
  this.init();
}

現在、プラグインにはスケールデータを返す機能がないため、this.callbackをmouseupに入れて実行することができます。マウスがポップアップするたびにthis.callback関数が実行され、関連するスケールデータが返されます。

/**
 * mouseup处理函数
 */
cropImg.prototype.mouseUpHandler= function(){

 if (this.mouse_index > 0) {  //已经绑定过mouseup事件了
    return false;
  }

  document.addEventListener('mouseup', ()=>{

     Array.from(Array(this.mouse_index)).forEach((value, idx) => {
       this[`mouse_${idx + 1}`] = false;
     });
     
      const { top, left } = this.mask.style; //this.mask是裁剪框的dom对象

      this.callback({ //将相关比例数据返回
       width: this.width, //裁剪框的宽
       height: this.height, //裁剪框的高
       top: parseInt(top),
       left: parseInt(left),
       container_height: this.container_height,//图片框的高
       container_width: this.container_width, //图片框的宽
    });   

  });  
}

これで、マウスをドラッグまたはストレッチするたびに、コールバック関数が実行され、この時点でクロップボックスのスケールデータが提供されます。次に、これらのスケールデータに基づいてクロップされた画像を生成する必要があります。

  • メモリー内にcanvas_bakとして新しいコピーキャンバスを作成します。imgはアップロードおよびプレビューされた元の画像であり、額縁の幅と高さに応じてコピーキャンバスにimgをレンダリングします。
  • 次に、新しいキャンバスを再作成して、その幅と高さがコールバックによって返される幅と高さ(つまり、トリミングフレームの幅と高さ)と等しくなるようにします。次に、最も重要なステップコードctx.drawImage(canvas_bak、left 、top、width、height、0,0、width、height)を使用して、トリミングを実行します。
  • ctx.drawImageは9つのパラメーターを渡します。その意味は、コピーキャンバスの開始点として左からの開始点xの値を取得し、開始点yとして左上からの値を取得することです。x、yを取得します。ピースをインターセプトするための開始点座標として幅が幅、高さが高さの長方形。これは最初の5つのパラメーターの関数です。インターセプトされた長方形は新しいキャンバスに描画され、新しいキャンバスは座標で完全に塗りつぶされます。開始点として0,0の。新しいキャンバスは傍受後に表示されます。
  • 最後に、新しいキャンバス圧縮がbase64エンコーディングに変換されてからページに表示されます。これは、トリミングされて圧縮された画像です。
new cropImg({
      target: 'box',
      callback({left,top,width,height,container_height,container_width,}) {

        const canvas_bak = document.createElement('CANVAS');
        const ctx_bak = canvas_bak.getContext('2d');
        canvas_bak.width = container_width;
        canvas_bak.height = container_height;
        ctx_bak.drawImage(img, 0, 0, container_width, container_height);

        const canvas = document.createElement('CANVAS');
        canvas.width = width;
        canvas.height = height;
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = '#fff';
        ctx.fillRect(0, 0, width, height);
        ctx.drawImage(canvas_bak,left,top,width,height,
        0,0,width,height
        );

        const value = Number(document.getElementById('sel').value);
        const code = canvas.toDataURL('image/jpeg', value);
        const image = new Image();
        image.src = code;

        image.onload = () => {
          const des = document.getElementById('production');
          des.innerHTML = '';
          des.appendChild(image);
          compress_img = image;
        };
      },
    });

 

画像を生成する

 Compress_imgは、トリミングと圧縮後の画像オブジェクトです。ユーザーがクリックして生成すると、生成関数がトリガーされます。最初にAタグを作成し、圧縮された画像のbase64エンコーディングをhref属性に割り当て、値をに追加します。ダウンロード属性は、ダウンロード後の画像の名前です。最後に、Aタグをクリックして、画像をローカルにダウンロードします。

  /**
   * 下载图片
   * @param {*}
   */
  function generate() {
    if (!compress_img) {
      return false;
    }
    const a = document.createElement('A');
    a.href = compress_img.src;
    a.download = 'download';
    a.click();
  }

 

おすすめ

転載: blog.csdn.net/brokenkay/article/details/107945227