目次
フロントエンド開発において、写真やアバターをアップロードする機能に遭遇するとき、サイズと解像度の制限がある場合、写真のトリミング機能を使用する必要があります。Canvasを利用することで画像のトリミング機能をWeb側で直接実現することが可能になります。
この記事では、フロントエンド技術を利用して画像の切り抜き機能を実現します。具体的なコードについては、画像切り抜きのソースコードをご覧ください。
実装プロセス
1. 画像ファイルのアップロードと読み込み
ファイル アップロード コントロールを使用して、画像のアップロードを実現します。画像ファイル (File オブジェクト) を取得した後、FileReader またはURL.createObjectURL という 2 つの API を介してファイル データの変換を完了できます。フロントエンド バイナリについては、前の記事で詳しく説明されています: Blob、File、FileReader、ArrayBuffer、TypeArray、DataView。
- FileReader: 通常、readAsDataURL メソッドを使用して、File を画像ファイルの Base64 データとして読み取り、画像データとして直接読み込むことができます。Base64 の知識については、Base64 文字列エンコーディングの知識について詳しく理解するために、前の記事を参照してください。
- URL.createObjectURL: 擬似プロトコルの Blob-Url リンクを生成します。これは、ここでは画像リソースを読み込むための画像 URL リンクとして使用されます。
次のように、ファイル アップロード イベントに応答して、Base64 文字列データを含むイメージをロードします。
// 上传控件事件响应,加载图片文件
document.getElementById('input-file').onchange = (e) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = async (event) => {
initImageCut(event.target.result)
}
reader.readAsDataURL(file)
}
この方法で読み取られたデータは画像の Base64 文字列データであり、画像リソースとして Image オブジェクトによってロードできます。
2. 画像表示とマスク処理
画像ファイルのデータを取得した後、画像をロードしてピクセルの幅と高さを取得します。
const img = new Image()
img.src = dataUrl
img.onload = function () {
resolve(img)
}
通常、 Imageオブジェクトを通じて img インスタンスが生成され 、画像データがロードされます (img インスタンスには画像の幅と高さが含まれます)。
画像の幅と高さは、画像表示領域のズーム率を計算するなど重要なデータであり、その後のドラッグ アンド ドロップやクロップ ボックスのズームも必要になります。
次のコードは、ズーム率 (ズーム) を計算します。
zoom = Math.min(WIDTH / img.width, HEIGHT / img.height)
zoom = zoom > 1 ? 1 : zoom
このうち、WIDTH とHEIGHT は写真やトリミング枠を表示する固定領域を設定するもので、値の大きさは任意に設定でき、ディスプレイの可視領域内で最適です。
ズームの役割により、後で画像の相対的なサイズを取得することが容易になります。
次に、ページ上に画像を表示し、マスキング処理を設定します。
画像の表示には、ここでは HTML の<img> タグを直接使用します。
<img class="image" id="bgMaskImg"/>
<img class="image" id="cutBoxImg"/>
ここでは2 つの<img>タグ要素が使用されており 、2 つの要素は同じ画像リソースをロードします。違いは次のとおりです。
+ bgMaskImg が ベース マップとして使用される場合、マスク効果をシミュレートするために透明度 (0.5 など) を設定します。
+ CutBoxImg は クロップボックス領域の画像、つまりマスクレイヤーのない鮮明な画像として表示されます。
以下に、達成する必要がある効果を示す図を示します。
上の表示では、最初の img タグにマスク効果があるように見えます。
中央領域の透明な画像ブロックは 2 番目の img タグの効果であり、 CSS のクリップパス属性を使用して 実現されます。
次に、2 つの img タグ要素の画像をロードし、各要素のスタイルを設定する必要があります。
bgMaskImgElm.src = cutBoxImgElm.src = imgUrl
setStyle()
CSS クリップパス
Clip-path は、 要素の一部のみを表示し、他の領域を非表示にする CSS プロパティです。
この機能はトリミング機能として使用でき、画像上でマスク レイヤーを使用することでマスク レイヤーのトリミング効果をシミュレートできます。
以前はCSSにclip属性がありましたが廃止され、一部のブラウザではまだサポートされていますが、clip-pathの使用を推奨します。
Clip-path 属性には多くの値がありますが、その多角形値のPolygonを使用して、正方形のクリッピング エリアをシミュレートします。
img {
clip-path: polygon(0 0, 100px 0, 100px 100px, 0 100px);
}
上記のコードは、ピクセル値を使用して正方形の 4 つの頂点 (左上、右上、右下、左下) の座標を特定し、画像の正方形のクリッピング領域を表示します。このとき、他の領域は表示されず、上の画像表示内に明確な領域が形成されます。
このトリミング領域は、トリミング枠の移動やズームに応じて変化するため、JS を通じて変更する必要があります。
const clipPath = 'polygon(...)'
cutBoxImgElm.style.clipPath = clipPath
移動またはドラッグ後、クリッピング領域の座標点を再計算し、クリップパス属性を更新します。
3. クロップ枠の表示
以上で画像の読み込みと表示、マスクレイヤーの処理とトリミング領域の実現が完了したら、次はトリミング枠の実現です。
クロップ ボックスは div の方法で使用でき、クロップ ボックスを定義します。
<div id="cutBox" class="cut-box" style="display: none;">
...
</div>
ここで注意する必要があるのは、JS を介してトリミングされた div の位置とサイズを変更し、上記のクリップパス属性も同期的に変更することです 。
cutBoxElm.style.width = cutBoxWidth * zoom - 2 + 'px'
cutBoxElm.style.height = cutBoxHeight * zoom - 2 + 'px'
cutBoxElm.style.left = cutBoxLeft + 'px'
cutBoxElm.style.top = cutBoxTop + 'px'
クロップボックスのスケールポイント
クロップ フレームの表示には、次の図に示すように、通常 8 つのズーム ポイントが設計されています。
なお、図上にはズームポイントのおおよその名称が記されており、以下では対応するポイントのイベント処理が関与するため、どの操作であるかが明確になる。
この種のコードは div で実装する方が適切です。小さなアイコンまたは CSS を使用して太い線を描き、それをCutBox のカットボックス divの下に置き、動きを追跡します。
<div class="box-corner topleft" style="cursor:nw-resize;"></div>
<div class="box-corner topright" style="cursor:ne-resize;"></div>
<div class="box-corner bottomright" style="cursor:se-resize;"></div>
<div class="box-corner bottomleft" style="cursor:sw-resize;"></div>
<div class="box-middle topmiddle" style="cursor:n-resize;"></div>
<div class="box-middle bottommiddle" style="cursor:s-resize;"></div>
<div class="box-middle leftmiddle" style="cursor:w-resize;"></div>
<div class="box-middle rightmiddle" style="cursor:e-resize;"></div>
上記の HTML コードは 8 つのポイントが定義されており、対応する CSS スタイルを使用すると、目的の効果を実現できます。
マウス スタイルが変更されると、ここでのズーム ポイント上にマウスを置いたときに、別のマウス スタイルが表示される必要があることに注意してください。
カーソルマウスのスタイル
カーソル属性は主にカーソルの種類を設定するもので、ブラウザ上でマウスを使用すると、マウスのさまざまなスタイルのアイコンが表示されます。
ポインター フローティングフィンガースタイル、グラブ グリッパースタイルなど。
クロップ ボックスでは、8 つのスケール関連のマウス ポインター スタイルが使用されます。
価値 | 説明 |
nw-resize、se-resize | 二重左矢印 |
ne-resize、sw-resize | 二重右矢印 |
nリサイズ、sリサイズ | 垂直二重矢印 |
wリサイズ、eリサイズ | 水平二重矢印 |
4番目、クロップ枠移動イベント
画像の構造と表示、マスキング効果、クロップフレームを処理した後、以下にいくつかの操作イベントを追加する必要があります。
最初に処理するのはトリミング フレームの移動イベントです。これにより、基本的なマウス イベントを使用して、トリミング フレームを画像領域内で任意に移動できます。
isMoveDown = false
cutBoxElm.addEventListener('mousedown', (event) => {
isMoveDown = true
const { offsetLeft, offsetTop } = cutBoxElm
const disX = event.clientX - offsetLeft
const disY = event.clientY - offsetTop
document.onmousemove = (docEvent) => {
const left = docEvent.clientX - disX
const top = docEvent.clientY - disY
if (isMoveDown) {
//...
}
docEvent.preventDefault()
}
document.onmouseup = () => {
isMoveDown = false
}
})
クロップ枠の移動にはズームは伴いませんが、マウスと同期して位置情報が更新されるため、マウスイベントとクロップ枠のオフセット位置データの計算が中心となります。
位置決めデータを計算した後、もう 1 つ行う必要があります。つまり、クロップ枠を画像領域から分離することはできません。つまり、クロップ枠を画像の外に移動することはできません。これは無効です。
// 裁剪框 left 数据
cutBoxLeft = Math.max(0, Math.min(left, curImageWidth - curCutBoxWidth))
// 裁剪框 top 数据
cutBoxTop = Math.max(0, Math.min(top, curImageHeight - curCutBoxHeight))
上記コードは画像の幅と高さ、クロップ枠の幅と高さを処理してクロップ枠位置の限界点を求めています。
5. クロップボックスの拡大縮小操作
クロップ フレームのズーム イベントもバインドする必要があります。この記事の例では、8 つのズーム ポイントのそれぞれのイベントをバインドすることで、クロップ フレームを移動するのと同じマウス イベントになります。
// 是左上角的缩放点
document.querySelector('.topleft').addEventListener('mousedown', (event) => {
reSizeDown('topleft', event)
})
// ...
// 其他点各自绑定
Mousemove イベントは引き続きreSizeDown関数で処理されます。
let isResizeDown = false
function reSizeDown (type, event) {
isResizeDown = true
document.onmousemove = (docEvent) => {
const disX = docEvent.clientX - event.clientX
const disY = docEvent.clientY - event.clientY
if (isResizeDown) {
let cutW = currentCutBoxWidth
let cutH = currentCutBoxHeight
switch (type) {
case 'topleft':
cutBoxLeft = Math.min(currentBoxLeft + (currentCutBoxWidth * zoom ) - 16, Math.max(0, currentBoxLeft + disX))
cutBoxTop = Math.min(currentBoxTop + (currentCutBoxHeight * zoom ) - 16, Math.max(0, currentBoxTop + disY))
const nwWidth = currentCutBoxWidth - (disX / zoom)
const nwHeight = currentCutBoxHeight - (disY / zoom)
cutW = +(cutBoxLeft > 0 ? nwWidth : (currentCutBoxWidth + currentBoxLeft / zoom)).toFixed(0)
cutH = +(cutBoxTop > 0 ? nwHeight : (currentCutBoxHeight + currentBoxTop / zoom)).toFixed(0)
break
// case 'topright':
// ...
// 对每个缩放点进行处理
}
// ...
}
}
document.onmouseup = () => {
isResizeDown = false
}
}
上記のコードでは、左上隅の拡大縮小を例にとりますが、左上隅をドラッグしてズームすると、クロップ枠の位置と幅と高さが変化するため、この4つの値(左、上、幅、高さ)を計算する必要があります。
クロップ枠の四隅の位置も同様に処理する必要がありますが、他の 4 つの直線方向の拡大縮小は比較的簡単です。
case 'leftmiddle':
cutBoxLeft = Math.min(currentBoxLeft + (currentCutBoxWidth * zoom ) - 16, Math.max(0, currentBoxLeft + disX))
const wWidth = currentCutBoxWidth - (disX / zoom)
cutW = +(cutBoxLeft > 0 ? wWidth : (currentCutBoxWidth + currentBoxLeft / zoom)).toFixed(0)
break
上記のコードでは、クロップ ボックスの左 オフセットと幅を計算するだけで済みます。
位置と幅と高さのデータを取得した後、クロップ枠のスタイル属性とマスク画像のクリップパス属性をリアルタイムに変更し、同期して変更することで、クロップ枠のイベント処理は基本的に完了します。
第六に、切断機能を完了します
イベントがバインドされると、クロップ フレームの基本機能が完了し、残りは最終的な画像のクロップ操作を実行することになります。
クロップ フレームを移動または拡大縮小した後、現在のクロッピング フレームの位置、幅、高さのデータを取得する必要があります。
位置、幅、高さに応じて、キャンバスを使用して画像を切り抜くことができます。
const left = cutBoxLeft / zoom
const top = cutBoxTop / zoom
const myCanvas = document.createElement('canvas')
const ctx = myCanvas.getContext('2d')
myCanvas.width = cutBoxWidth
myCanvas.height = cutBoxHeight
ctx.drawImage(imgObj, left, top, cutBoxWidth, cutBoxHeight, 0, 0, cutBoxWidth, cutBoxHeight)
上記のコードのように、
位置情報は相対位置として以前に計算されており、元の画像に戻すには拡大縮小で戻す必要がありますが、クロップ枠の幅と高さは以前に戻されているので、ここで直接値を取得できます。
キャンバス myCanvas の 切り抜きと描画が成功すると、必要な画像が取得されるので、キャンバスをページ上に直接表示することも、キャンバスを画像 Base64 データまたは Blob-Url としてエクスポートして読み込むこともできます。
myCanvas.toBlob((blob) => {
const url = URL.createObjectURL(blob)
}, 'image/jpeg')
// 或
myCanvas.toDataURL()
描画イメージ
drawImage は Canvas の API であり、さまざまな画像操作を処理するために使用されます。これには 3 つの構文があり、ここでは切り抜きに使用します。
context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
の、
- パラメータ sx、sy、sWidth、sHeight は、位置、幅、高さの寸法に従って元の画像を切り取るものとして理解でき、切り取られた新しい画像を取得できます。
- パラメータdx、dy、dWidth、dHeightは、上で切り取った新しい絵を、位置、幅、高さに従ってキャンバス上に描画するためのもので、このとき、切り取った後のキャンバス上に新しい絵が表示されます。
次の図は、この例の完全なトリミング機能を備えたインターフェイスの表示効果です。
上図は、疑似要素::file-selector-buttonを使用して、入力タグの スタイルを変更し、アップロード コントロール ボタンを美しくする様子を示しています。詳細については、CSS 疑似要素の詳細な説明と、疑似要素と疑似クラスの違いを参照してください。
追記
上記の手順を経て、フロントエンド技術に基づく基本的な画像トリミング機能が完成します。
画像の切り抜きもさまざまな方法で処理できますが、わかりやすくするために独自のタグとAPIを使用した例を示します。
その後のさまざまなコンポーネントのカプセル化も、vue、react、web コンポーネントなどに関係なく実行でき、カプセル化のためにすぐに導入できます。