首页 > 其他分享 >js+canvas图片裁剪

js+canvas图片裁剪

时间:2024-04-27 22:57:16浏览次数:28  
标签:canvas -- clipInfo 裁剪 mask height width originImg js


canvas 裁剪图片功能实现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0" />
    <title>裁剪图片</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      .app {
        display: grid;
        place-content: center;
        row-gap: 20px;
      }
      .avatar-layout {
        display: flex;
        place-content: center;
        column-gap: 20px;
        margin-top: 20px;
      }
      .preview {
        overflow: hidden;
        display: grid;
        place-content: center;
        width: 240px;
        height: 240px;
        border-radius: 6px;
        user-select: none;
      }
      .origin-preview {
        outline: 1px solid #999;

        .img-wrap {
          position: relative;
          display: flex;
        }

        .mask {
          --mask-x: 0px;
          --mask-y: 0px;
          --mask-width: 50px;
          --mask-height: 50px;
          --mask-show: none;

          display: var(--mask-show);
          position: absolute;
          inset: 0;
          translate: var(--mask-x) var(--mask-y);
          width: var(--mask-width);
          height: var(--mask-height);
          background-color: rgba(0, 0, 0, 0.5);
          /* border: 1px solid pink; */
          z-index: 2;
          will-change: translate;

          .drag-point {
            --size: 20px;

            position: absolute;
            width: var(--size);
            height: var(--size);
            border: inherit;
            background-color: inherit;
            z-index: 3;
          }

          .point-lt {
            top: 0;
            left: 0;
            translate: calc(var(--size) * -1) calc(var(--size) * -1);
            cursor: nw-resize;
          }

          .point-lb {
            bottom: 0;
            left: 0;
            translate: calc(var(--size) * -1) calc(var(--size));
            cursor: sw-resize;
          }

          .point-rt {
            top: 0;
            right: 0;
            translate: calc(var(--size)) calc(var(--size) * -1);
            cursor: ne-resize;
          }
          .point-rb {
            bottom: 0;
            right: 0;
            translate: var(--size) var(--size);
            cursor: se-resize;
          }
        }
      }
      .clip-preview {
        row-gap: 20px;
        text-align: center;
        outline: 1px solid #999;

        .clip-wrap {
          /* overflow: hidden; */
          display: grid;
          place-content: center;
          width: 120px;
          height: 120px;
          /* border-radius: 50%; */
          background-color: #dedede;
          outline: inherit;

          .clip {
            width: inherit;
            height: inherit;
          }
        }
      }
      img {
        width: 100%;
        height: 100%;
        object-fit: contain;
      }
      .btn {
        margin-inline: auto;
      }
    </style>
  </head>
  <body>
    <div class="app">
      <div class="avatar-layout">
        <div class="preview origin-preview">
          <div class="img-wrap">
            <img class="origin" alt="" draggable="false" />
            <input type="file" id="file" hidden />
            <div class="mask">
              <span data-dir="lefttop" class="drag-point point-lt"></span>
              <span data-dir="leftbottom" class="drag-point point-lb"></span>
              <span data-dir="righttop" class="drag-point point-rt"></span>
              <span data-dir="rightbottom" class="drag-point point-rb"></span>
            </div>
          </div>
        </div>
        <hr />
        <div class="preview clip-preview">
          <span>预览头像</span>
          <div class="clip-wrap">
            <canvas class="clip"></canvas>
          </div>
        </div>
      </div>
      <div class="btn">
        <button class="btn-upload">重新选择</button>
        <button class="btn-crop-preview">预览裁剪图片</button>
      </div>
    </div>

    <script>
      /*
        canvas 裁剪图片功能实现
        功能:
        1. [x] 选择图片文件,预览原图
        2. [x] 裁剪图片
        3. [x] 显示裁剪后的图片
        4. [x] 调整裁剪区域大小
        5. [x] 获取裁剪后的图片
        6. [] 图片裁剪功能优化
      */
      const originImg = document.querySelector('.origin');
      /** @type {HTMLCanvasElement} */
      const clipCanvas = document.querySelector('.clip');
      const mask = document.querySelector('.mask');
      const dragPoint = Array.from(mask.querySelectorAll('.drag-point'));

      const clipInfo = {
        width: mask.offsetWidth,
        height: mask.offsetHeight,
        x: originImg.offsetLeft,
        y: originImg.offsetTop,
        container: null,
        // 图片缩放比例
        scaleX: originImg.naturalWidth / originImg.offsetWidth,
        scaleY: originImg.naturalHeight / originImg.offsetHeight,
        // 限制最小的 mask 大小
        minMaskSize: 30,
      };

      // mask.style.setProperty('--mask-x', clipInfo.x + 'px');
      // mask.style.setProperty('--mask-y', clipInfo.y + 'px');

      /** @type {CanvasRenderingContext2D} */
      const clipCtx = clipCanvas.getContext('2d');

      let pointX = 0,
        pointY = 0;
      init();
      function init() {
        /** @type {HTMLInputElement} */
        const file = document.getElementById('file');
        const updloadBtn = document.querySelector('.btn-upload');
        const imgWrap = document.querySelector('.img-wrap');
        const cropPreviewBtn = document.querySelector('.btn-crop-preview');

        // 裁切canvas的宽高
        clipCanvas.width = clipCanvas.offsetWidth * devicePixelRatio;
        clipCanvas.height = clipCanvas.offsetHeight * devicePixelRatio;

        clipInfo.container = imgWrap;

        // 预览裁剪图片
        cropPreviewBtn.addEventListener('click', () => {
          clipCanvas.toBlob((blob) => {
            const file = new File([blob], 'clip.png', { type: 'image/png' });

            const url = URL.createObjectURL(file);
            open(url);
          });
        });

        // 监听文件选择
        file.addEventListener('change', handleFile);
        // 点击按钮触发文件选择
        updloadBtn.addEventListener('click', () => file.click());
        // 监听鼠标按下
        mask.addEventListener('pointerdown', (e) => {
          // const isPoint = dragPoint.includes(e.target);
          // if (isPoint) return;

          const {
            container: { offsetLeft, offsetTop },
          } = clipInfo;

          pointX = e.offsetX + offsetLeft;
          pointY = e.offsetY + offsetTop;

          mask.style.cursor = 'grabbing';

          // 监听鼠标移动
          window.addEventListener('pointermove', handleMaskMove);
          // 监听鼠标抬起
          window.addEventListener('pointerup', handleMaskUp);
        });

        dragPoint.forEach((point) => {
          point.addEventListener('pointerdown', (e) => {
            e.stopPropagation();

            const sx = e.clientX;
            const sy = e.clientY;

            const { minMaskSize, container } = clipInfo;
            const {
              left: appLeft,
              top: appTop,
              width: appWidth,
              height: appHeight,
            } = container.getBoundingClientRect();
            let { left, top, width, height, right, bottom } = mask.getBoundingClientRect();
            const { dir } = e.target.dataset;

            window.addEventListener('pointermove', handleDragPointMove);
            window.addEventListener('pointerup', handleDragPointUp);

            function handleDragPointMove(e) {
              const { clientX, clientY } = e;

              const dx = clientX - sx,
                dy = clientY - sy;

              const handlers = {
                lefttop: (box) => updateSizeAndPosition(box, -dx, -dy, true, true),
                leftbottom: (box) => updateSizeAndPosition(box, -dx, dy, true, false),
                righttop: (box) => updateSizeAndPosition(box, dx, -dy, false, true),
                rightbottom: (box) => updateSizeAndPosition(box, dx, dy, false, false),
              };

              handlers[dir]?.(mask);

              function updateSizeAndPosition(box, dx, dy, x, y) {
                let _width = Math.round(width + dx),
                  _height = Math.round(height + dy),
                  _x = Math.round(left + -1 * dx - appLeft),
                  _y = Math.round(top + -1 * dy - appTop);

                // 限制范围
                _x = Math.max(0, _x);
                _x = Math.min(right - minMaskSize - appLeft, _x);
                _y = Math.max(0, _y);
                _y = Math.min(bottom - minMaskSize - appTop, _y);

                _width = Math.max(minMaskSize, _width);
                _width = Math.min(appWidth - (left - appLeft), _width);
                _height = Math.max(minMaskSize, _height);
                _height = Math.min(appHeight - (top - appTop), _height);

                box.style.setProperty('--mask-width', `${_width}px`);
                box.style.setProperty('--mask-height', `${_height}px`);
                if (x) box.style.setProperty('--mask-x', `${_x}px`);
                if (y) box.style.setProperty('--mask-y', `${_y}px`);

                clipInfo.width = _width;
                clipInfo.height = _height;
                clipInfo.x = _x;
                clipInfo.y = _y;

                drawClipImg();
              }
            }
            function handleDragPointUp(e) {
              dragPoint.forEach((point) => {
                window.removeEventListener('pointermove', handleDragPointMove);
                window.removeEventListener('pointerup', handleDragPointUp);
              });
            }
          });
        });
      }

      function handleMaskMove(e) {
        // 遮罩 mask 在容器内的位置
        let x = Math.round(e.clientX - pointX);
        let y = Math.round(e.clientY - pointY);

        // (边界处理) 限制 mask 移动范围
        x = Math.max(x, 0, originImg.offsetLeft);
        x = Math.min(x, originImg.offsetLeft + originImg.offsetWidth - clipInfo.width);
        y = Math.max(y, 0, originImg.offsetTop);
        y = Math.min(y, originImg.offsetTop + originImg.offsetHeight - clipInfo.height);

        clipInfo.x = x;
        clipInfo.y = y;

        mask.style.setProperty('--mask-x', clipInfo.x + 'px');
        mask.style.setProperty('--mask-y', clipInfo.y + 'px');

        drawClipImg();
      }

      function handleMaskUp() {
        this.removeEventListener('pointermove', handleMaskMove);
        this.removeEventListener('pointerup', handleMaskUp);

        pointX = pointY = 0;
        mask.style.cursor = 'grab';
      }

      function handleFile(e) {
        /** @type {File} */
        const file = e.target.files[0];
        if (!file) return;

        // 读取文件内容
        const reader = new FileReader();
        reader.onload = function () {
          originImg.onload = () => {
            // 图片加载完成后,设置图片大小
            if (originImg.width < originImg.height) {
              const previewWrap = document.querySelector('.origin-preview');
              clipInfo.container.style.height = previewWrap.offsetHeight + 'px';
              clipInfo.container.style.width = originImg.width * previewWrap.offsetHeight / originImg.height + 'px';
            }

            // 图片加载完成后,添加蒙层
            addMask();
            // 绘制裁剪图片
            drawClipImg();
          };
          // 赋值原图图片 base64地址
          originImg.src = reader.result;
        };
        reader.readAsDataURL(file);
      }

      // 绘制裁剪图片
      function drawClipImg() {
        const { x, y, width, height, scaleX, scaleY } = clipInfo;
        clipCtx.clearRect(0, 0, clipCanvas.width, clipCanvas.height);
        clipCtx.drawImage(
          originImg,
          /* 裁剪区域 */
          (x - originImg.offsetLeft) * scaleX,
          (y - originImg.offsetTop) * scaleY,
          clipInfo.width * scaleX,
          clipInfo.height * scaleY,
          /* 绘制区域 */
          0,
          0,
          clipCanvas.width,
          clipCanvas.height
        );
      }

      /**
       * 添加蒙层
       */
      function addMask() {
        mask.style.setProperty('--mask-show', 'block');

        clipInfo.width = mask.offsetWidth;
        clipInfo.height = mask.offsetHeight;
        clipInfo.x = originImg.offsetLeft;
        clipInfo.y = originImg.offsetTop;

        clipInfo.scaleX = originImg.naturalWidth / originImg.offsetWidth;
        clipInfo.scaleY = originImg.naturalHeight / originImg.offsetHeight;

        mask.style.setProperty('--mask-x', clipInfo.x + 'px');
        mask.style.setProperty('--mask-y', clipInfo.y + 'px');
      }
    </script>
  </body>
</html>

标签:canvas,--,clipInfo,裁剪,mask,height,width,originImg,js
From: https://www.cnblogs.com/chlai/p/18162704

相关文章

  • js逆向实战之中国男子篮球职业联赛官方网站返回数据解密
    url:https://www.cbaleague.com/data/#/teamMain?teamId=29124分析过程看流量包,返回数据全是加密的字符串,要做的就是解密回显数据。由于这里的网址都比较特殊,里面都带有id号,所以通过url关键字去搜索不是一个很好的办法。看initiators,里面有很多异步传输。异步传输......
  • threejs 浏览器窗口resize变化 自适应 html 全屏
    全屏:画布全屏和body页面全屏;//导入threejsimport*asTHREEfrom"three";import{OrbitControls}from"three/examples/jsm/controls/OrbitControls.js";//创建场景sceneconstscene=newTHREE.Scene();//console.log(scene,'scene');//......
  • js设计模式(上)
     引用:(23条消息)《Javascript设计模式与开发实践》关于设计模式典型代码的整理(上):单例模式、策略模式、代理模式、迭代器模式、发布-订阅模式、命令模式、组合模式_QQsilhonette的博客-CSDN博客1、单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。使用闭包封装......
  • js调整div顺序
    js调整div顺序并保留div原有事件等<divclass="my_tabs"><divclass="el-tabs__nav-scroll"><divclass="el-tabs__nav"><divclass="el-tabs__itemis-active">AAAA</div><d......
  • vue,js直接导出excel,xlsx的方法,XLSX_STYLE 行高设置失效的问题解决
    1、先安装依赖:xlsx、xlsx-style、file-saver三个包npminstallxlsxxlsx-stylefile-saver2、引入:<script>import*asXLSXfrom'xlsx/xlsx.mjs'importXLSX_STYLEfrom'xlsx-style';import{saveAs}from'file-saver';exportdefau......
  • JS相关技巧
    随意修改网页js代码document.body.contentEditable="true"document.designMode="on"或javascript:document.body.contentEditable='true';document.designMode='on';void0(浏览器输入框执行,chrome需要粘贴后,需要在前面手打javascript:因为粘贴的会自动过滤)复......
  • AssemblyResolve巧解未能加载文件或程序集“Newtonsoft.Json, Version=6.0.0.0的问题
    问题:未能加载文件或程序集“Newtonsoft.Json,Version=6.0.0.0,Culture=neutral,PublicKeyToken=30ad4fe6b2a6aeed”或它的某一个...问题分析:原因是因为引用的Microsoft.AspNet.SignalR.Client库的依赖项是6.0版本的Newtonsoft.Json,而且是动态加载进去的(用Assembly.LoadFrom),......
  • 使用 Docker 部署 Nuxt.js 应用程序
     来源:https://medium.com/@jkpeyi/deploying-a-nuxt-js-application-with-docker-69bf822c066d  WhendevelopingaNuxt.jsapplication,it’sessentialtobeabletodeployiteasilyandreproducibly.Inthisarticle,wewillexplorehowtouseDockertod......
  • NodeJS命令行注入:示例及预防
    在本文中,我们将学习如何在NodeJS中使用命令行函数进行注入漏洞攻击。现代网站可以是一个复杂的软件,它由许多分布在不同环境中的部分组成。如果你的应用程序没有得到有效的保护,那么分布在这些环境中的每一个组成部分都有可能受到命令行注入漏洞的攻击。本文将介绍如......
  • vue箭头函数、js-for循环、事件修饰符、摁键事件和修饰符、表单控制、完整购物车版本
    【箭头函数】1<!DOCTYPEhtml>2<htmllang="en">3<head>4<metacharset="UTF-8">5<title>Title</title>6<scriptsrc="https://cdn.jsdelivr.net/npm/vue/dist/vue.js">&l......