從張鑫旭的demo中,我學到了影像拉伸的原理

凌覽發表於2023-05-01

文章收錄:

產品經理又有新需求啦,其中有一個圖片上傳後使用者拉伸影像寬高的功能,評估後因要卡上線時間來不及砍掉了。保不準下一個版本又會提這個功能,所以還是要去研究研究。

幸虧我有關注張鑫旭大佬的部落格,印象中記得發表過一篇關於影像拉伸的文章,就是它JS之我用單img元素實現了影像resize拉伸效果。剛好滿足產品想要的效果,demo都是現成的。

文章對js邏輯部分並沒有描述,像我這種愛學習,那不得知其所以然。

因此,我讀了讀原始碼200行左右,並且去掉邊界判斷邏輯,只將核心邏輯寫了一遍。

先把效果秀出來:

先搞定影像拉伸樣式

先寫一個img元素,給它的src屬性新增一個線上的影像連結。

<img class="image-size" src="https://pic1.zhimg.com/v2-d58ce10bf4e01f5086c604a9cfed29f3_r.jpg?source=1940ef5c" alt="拉伸">

再給它整點樣式重點是 border-image屬性,大佬文章也是介紹使用border-image屬性做到單img實現拉伸。不贅述,跟我一樣愛學習的人肯定會去瞅一眼大佬文章的。

/* 先預設寬度400px */ 
.image-size {
  width: 400px;
}

img.active {
  cursor: default;
  z-index: 1;
  display: inline-block;
  vertical-align: bottom;
  font-size: 12px;
  border: 3px solid transparent;
  border-image: url("data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='%23914AFF' d='M2.5 2.5h25v25h-25z'/%3E%3Cpath d='M0 0v12h2V2h10V0H0zM0 30V18h2v10h10v2H0zM30 0H18v2h10v10h2V0zM30 30H18v-2h10V18h2v12z' fill='%23914AFF'/%3E%3C/svg%3E") 12 / 12px / 0;
  margin: -1px;
  position: relative;
  -webkit-user-select: none;
  user-select: none;
}

點選圖片後,給它新增一個active類名。若點選的不是該影像,則清除影像的active類名。

const image = document.getElementsByClassName('image-size')[0]
image.onclick = (e) => {
  if (image.classList.contains('active')) return
  image.classList.add('active')
}

document.onclick = (e) => {
   if (e.target === image) return
   image.classList.remove('active')
}

如下GIF錄屏所示:

也可以點選這裡體驗:拉伸樣式demo

再搞定滑鼠游標樣式

滑鼠游標預設箭頭,現在需要當滑鼠移動到影像左上、左下、右上、右下四個角時,滑鼠游標樣式隨之進行改變。

注意看GIF演示中的滑鼠變化:

上圖是大佬的demo。

滑鼠移動至左上角、右下角處,滑鼠游標樣式修改成:

滑鼠移動至左下角、右上角,滑鼠游標樣式修改成:

修改滑鼠游標使用屬性cursor,對該屬性不清楚的童鞋們移步cursor官方文件

給影像新增active類名後,再給document繫結一個滑鼠移動事件onmousemove。當滑鼠移動過程中計算滑鼠位置是否已進行某個區域內。

該區域可以為下圖紅框框起來的區域,影像左上、左下、右上、右下四個角均會有一個這樣的區域。

計算過程要獲取到影像的left、top、right、bottom值,也就是使用Element.getBoundingClientRect(),不清楚該API的童鞋移步getBoundingClinetRect官方文件

 //省略...
 
image.onclick = (e) => {
  if (image.classList.contains('active')) return
  image.classList.add('active')

  document.onmousemove = (e) => {
      const target = e.target
      if (target !== image || !target.classList.contains('active')) return
      const x = e.clientX,
          y = e.clientY
      const { top, left, bottom, right } = image.getBoundingClientRect()
      //左上角或右下角
      if ((bottom - y < 20 && right - x < 20) || (x - left < 20 && y - top < 20)) {
          image.style.cursor = 'nwse-resize'

          //左下角或右上角
      } else if ((y - top < 20 && right - x < 20) || (bottom - y < 20 && x - left < 20)) {
          image.style.cursor = 'nesw-resize'

          //若都不是,滑鼠游標為預設箭頭樣式
      } else {
          image.style.cursor = 'default'
      }
  }
}

//省略...

虛擬碼中20這個數字是我隨意寫的,這個值為紅框框起來區域的寬高。

若取消影像的拉伸狀態,則也把document已繫結的滑鼠移動事件取消。

//省略...

document.onclick = (e) => {
  if (e.target === image) return
  image.classList.remove('active')
  document.onmousemove = null
}

可以點選這裡體驗:影像拉伸滑鼠樣式改變demo

讓影像寬高動起來

先給image元素新增onmousedown事件,並獲取滑鼠左鍵按下時clientXclientY的值作為開始拉伸的位置,預設為0

拉伸邏輯計算發生在document.onmousemove事件內,因此,需要有一個變數isResizeing表示image.onmousedown事件是否被觸發。觸發,滑鼠移動即影像寬高也要進行改變;未觸發,滑鼠移動僅改變滑鼠游標的樣式,不影響影像的寬高。

isResizeing不能一直為true,滑鼠抬起onmouseup重置為false

 //省略...
 
image.onclick = (e) => {
  if (image.classList.contains('active')) return
  image.classList.add('active')
  let startX = 0,
      startY = 0,
      //是否拉伸狀態
      isResizeing = false,
      position = ''
  image.onmousedown = (e) => {
      //滑鼠左鍵落下的X、Y位置
      startX = e.clientX
      startY = e.clientY
      isResizeing = true
  }

  document.onmousemove = (e) => {
     e.preventDefault()
     const target = e.target
     let x = e.clientX,
          y = e.clientY,
          distanceX = x - startX,
          distanceY = y - startY
     if(isResizeing){
       //影像已處於拉伸的狀態
     
    
     }else{
       //改變滑鼠游標樣式
       
       if(if (target !== image || !target.classList.contains('active')) return){
         const { top, left, bottom, right } = image.getBoundingClientRect()
          //左上角或右下角
         if ((bottom - y < 20 && right - x < 20) || (x - left < 20 && y - top < 20)) {
            image.style.cursor = 'nwse-resize'
  
            //左下角或右上角
         } else if ((y - top < 20 && right - x < 20) || (bottom - y < 20 && x - left < 20)) {
            image.style.cursor = 'nesw-resize'
  
            //若都不是,滑鼠游標為預設箭頭樣式
         } else {
            image.style.cursor = 'default'
         }
       }
    }
  
  document.onmouseup = () => {
    //滑鼠抬起時,重置isResizeing變數
    isResizeing = false
  }
}

//省略...

還需要有一個變數 position,用於判斷拉伸的是左上、右下、左下、右上四個角中的哪一個 。

    //省略
    if ((bottom - y < 20 && right - x < 20) || (x - left < 20 && y - top < 20)) {
          //左上角或右下角
          
          image.style.cursor = 'nwse-resize'
          // 右下角
          if (bottom - y < 20) {
              position = 'bottom right'
              return
          }
          position = 'top left'
    
     } else if ((y - top < 20 && right - x < 20) || (bottom - y < 20 && x - left < 20)) {
       //左下角或右上角
       
        image.style.cursor = 'nesw-resize'
        // 右上角
        if (y - top < 20) {
            position = 'top right'
            return
        }

        position = 'bottom left'

        //若都不是,滑鼠游標為預設箭頭樣式
     } else {
        image.style.cursor = 'default'
         position = ''
     }
   //省略 ...

有了position變數,即可在if(isResizeing){...}這個程式碼塊中依據position值進行拉伸計算。

 //省略...
 
image.onclick = (e) => {
  if (image.classList.contains('active')) return
  image.classList.add('active')
  let startX = 0,
      startY = 0,
      //是否拉伸狀態
      isResizeing = false,
      position = '',
      storeWidth = 0,
      storeHeight = 0
  image.onmousedown = (e) => {
      //滑鼠左鍵落下的X、Y位置
      startX = e.clientX
      startY = e.clientY
      isResizeing = true
      storeWidth = image.clientWidth
      storeHeight = image.clientHeight
  }

  document.onmousemove = (e) => {
     e.preventDefault()
     const target = e.target
     let x = e.clientX,
        y = e.clientY,
        distanceX = x - startX,
        distanceY = y - startY,
        width = 0,
        height = 0
     if(isResizeing){
       //影像已處於拉伸的狀態
          if (!position) return
          if (position === 'bottom right') {
              /*
                  右下角  
                  distanceX值為正,distanceY值為正,影像寬高變大;
                  distanceX值為負,distanceY值為負,影像寬高變小
              */
              width = storeWidth + distanceX
              height = storeHeight + distanceY
          } else if (position === 'top left') {
              /*
                  左上角正好與右下角相反
                  distanceX值為正,distanceY值為正,影像寬高變大;
                  distanceX值為負,distanceY值為負,影像寬高變小
              */
              width = storeWidth - distanceX
              height = storeHeight - distanceY
          } else if (position === 'top right') {
              /*
                  右上角
                  distanceX值為正,distanceY值為負,影像寬高變大;
                  distanceX值為負,distanceY值為正,影像寬高變小
              */
              width = storeWidth + distanceX
              height = storeHeight - distanceY
          } else if (position === 'bottom left') {
              /*
                  左下角
                  distanceX值為負,distanceY值為正,影像寬高變大;
                  distanceX值為正,distanceY值為負,影像寬高變小
              */
              width = storeWidth - distanceX
              height = storeHeight + distanceY
          }  
         image.style.width = Math.round(width) + 'px'
         image.style.height = Math.round(height) + 'px'  
     }else{
       //省略
    }
  
  document.onmouseup = () => {
    //滑鼠抬起時,重置isResizeing變數
    isResizeing = false
  }
}

//省略...

該部分程式碼可以最佳化,但這樣寫容易理解,如何最佳化可以看下張鑫旭/單IMG元素的影像拉伸效果

完成這步,影像已經可以位伸寬高了。

如下GIF錄屏所示:
拉伸移動

可以點選這裡體驗:影像拉伸寬高改變

影像是有比例的,如常見比例16:9,3:4等。現在實現出來的拉伸沒有按比例進行拉伸,還需要最佳化。

先計算出影像比例,比例=影像寬度/ 影像高度。比較distanceX、distanceY兩者值誰的移動距離更大。若distanceX值更大,則以影像寬度計算出它的高度;若distanceY值更大,則以影像的高度計算出它的寬度。

 //省略...
 
image.onclick = (e) => {
    //省略
  document.onmousemove = (e) => {
     e.preventDefault()
     const target = e.target
     let x = e.clientX,
        y = e.clientY,
        distanceX = x - startX,
        distanceY = y - startY,
        width = 0,
        height = 0
     if(isResizeing){
       //影像已處於拉伸的狀態
          if (!position) return
          //省略
          
          let imageWidth = 0,
              imageHeight = 0 
          const ratio = storeWidth / storeHeight
          // 選擇移動距離大的方向
          if (Math.abs(distanceX) > Math.abs(distanceY)) {
              // 寬度變化為主
              imageWidth = width;
              imageHeight = width / ratio;
          } else {
              // 高度變化為主
              imageHeight = height;
              imageWidth = height * ratio;
          }
         image.style.width = Math.round(imageWidth) + 'px'
         image.style.height = Math.round(imageWidth) + 'px'  
     }else{
       //省略
    }
   //省略
}

//省略...

OK,到此完畢。

如下GIF錄屏所示:

可以點選這裡體驗:影像拉伸

總結

從讀張鑫旭的文章demo原始碼出發,自己也對影像拉伸的功能實現了一遍,做到腦會手也會。以後再有類似的功能分分鐘搞定它!

關注張鑫旭大佬,好處多多。

如果我的文章對你有幫助,你的?就是對我的最大支援^_^。

相關文章