图片添加水印,可实现水印旋转,拖拽,缩放,导出

1、效果展示

在这里插入图片描述

演示地址:https://little-littleprogrammer.github.io/npm-components/dist/#/waterMark

(地址里还有其他小组件,有兴趣的可以看下)

2、实现的功能

  • 水印样式可自定义 – 斜向平铺以及自定义
  • 自定义样式可自定义水印的个数,颜色,大小,可以实现水印旋转,拖拽
  • 可导出为png格式的图片,文件名为第一个水印的名称
  • 可添加图片水印

3、具体实现

说明:
1、本文通过循序渐进的方式讲解,所以重复代码可能有点多。
2、本文尽可能针对难点性问题讲解,最终所有代码会呈现在底部github链接中

选择图片部分

<template>
   <input type="file" accept="image/*" @change="get_file_data" />
   <div class="result-container" ref="ref-result-container">
		<div class="img-container" ref="ref-img-container">
   		<img :src="imgUrl" alt />
	</div>
</div>
</template>
 <script>
get_file_data(data) {
    const fileReader = new FileReader();
    const $resultContainer = this.$refs['ref-result-container'];
    const $imgContainer = this.$refs['ref-img-container'];
    fileReader.onload = (e) => {
        this.imgUrl = e.target.result;
        const _image = new Image();
        _image.src = this.imgUrl;
        _image.onload = (e) => {
            if (e.path[0].width > document.body.clientWidth) {
                $resultContainer.style.width =
                    document.body.clientWidth + 'px';
                $imgContainer.style.width =
                    document.body.clientWidth - 40 + 'px';
            } else {
                $resultContainer.style.width = e.path[0].width + 'px';
                $imgContainer.style.width = e.path[0].width - 40 + 'px';
            }
        };
    };
    // readAsDataURL
    fileReader.readAsDataURL(data.target.files[0]);
    fileReader.onerror = () => {
        new Error('blobToBase64 error');
    };
},
</script>

1、选择文件后,通过get_file_data方法,将图片转化为base64
2、根据图片的大小,去控制result-container和img-container容器的大小,当超过浏览器宽度时,限制图片为浏览器宽度
根据图片自定义容器

水印样式部分

layout控制样式,1是斜向平铺,2是自定义样式(自定义样式可添加文字水印和图片水印)

1、斜向平铺
最终效果:
在这里插入图片描述

<div v-else class="qm-watermark">
    <p v-for="item in 16" :key="'line'+item">
        <span v-for="i in 60" :key="'name'+i">{{markList[0].username}}</span>
    </p>
</div>
.qm-watermark {
    // 水印
    position: absolute;
    z-index: 99999;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    pointer-events: none;
    font-family: Cursive, serif;
    overflow: hidden;
    $e: 17;
    @for $i from 0 to $e {
        p:nth-child(#{$i}) {
            white-space: nowrap;
            transform-origin: (18% * ($i - 1)) 1%;
            transform: rotate(-20deg);
            text-indent: (-10px * ($i - 1));
        }
    }
    span {
        margin-right: 30px;
    }
}

斜向平铺样式主要为css控制,我这里使用的笨办法,设置很多行,一行很多个,对不同的行设置不同的旋转点,旋转。最后溢出的部分overflow:hidden

2、自定义水印部分

<div v-for="item in markList" :key="item.index">
    <li>
        <div>
            <span>姓名:</span>
            <input v-model="item.username" type="text" />
        </div>
    </li>
    <li>
        <button
            v-if="item.index===markList.length-1 && item.index !==0"
            :disabled="form.layout!=='2'"
            :class="{'disabled':form.layout!=='2'}"
            @click="del_water_mark"
        >删除水印</button>
        <button
            v-if="form.layout=='2'&&item.index===markList.length-1"
            :class="{'disabled':form.layout!=='2'}"
            @click="add_water_mark"
        >添加水印</button>
    </li>
</div>
<script>
data() {
	return {
		markList: [
		    {
		        index: 0,
		        username: '',
		        color: '#000000',
		        size: 14
		    }
		],
	}
}

</script>
methods:{
	add_water_mark() {
       this.keyIndex++;
        this.markList.push({
            index: this.keyIndex,
            username: '',
            color: '#000000',
            size: 14
        });
    },
    del_water_mark() {
        this.keyIndex--;
        this.markList.pop();
    }
}

自定义样式的难点主要在于水印字体的旋转移动缩放

旋转部分

效果
在这里插入图片描述
旋转部分逻辑:

  • 右下角旋转图标为svg图标(生成图片是不会存在,在变成canvas的时候,进行了过滤)
  • 旋转图标添加mousedown事件,点击后,为document添加mousemovemouseup事件,鼠标弹起时移除
  • 当有多个水印时,如何判断点击的是那个水印,在通过rotate_name(e)方法中通过$event的属性,取到父组件与祖父组件,然后通过get_origin_transform($dom)方法获取旋转中心点(因为移动后会改变旋转中心点,所以要每次点击都要获取),然后再通过get_rotate_deg(e)方法,获取到鼠标的点,旋转中心点,通过一下算法,计算出旋转值,通过$dom.style.transform设置旋转

代码


  rotate_name(e) {
      this._parentNode =
          e.target.tagName.toLowerCase() === 'svg'
              ? e.target.parentNode
              : e.target.parentNode.parentNode;
      this._grentParentNode =
          e.target.tagName.toLowerCase() === 'svg'
              ? e.target.parentNode.parentNode
              : e.target.parentNode.parentNode.parentNode;
      document.addEventListener('mousemove', this.get_rotate_deg);
      document.addEventListener('mouseup', () => {
          document.removeEventListener('mousemove', this.get_rotate_deg);
      });
  },
  get_rotate_deg(e) {
      const $dom = this._parentNode;
      const $domParent = this._grentParentNode;
      const transformOption = this.get_origin_transform($domParent);
      // 中心点的计算是根据浏览器视口的计算,offset是根据页面元素的位置,所以要减去卷动值
      const positonByHtml = {
          centerX: Methods.offset($dom).left + $dom.clientWidth / 2 - document.documentElement.scrollLeft,
          centerY: Methods.offset($dom).top + $dom.clientHeight / 2 - document.documentElement.scrollTop
      };
      let x = e.clientX;
      let y = e.clientY;
      const origin = {
          x:
              +positonByHtml.centerX +
              parseInt(transformOption.translateX || 0),
          y:
              +positonByHtml.centerY +
              parseInt(transformOption.translateY || 0)
      }; // 先手动指定当前中心点,也可以根据当前元素的left+width/2 的到x  top+height/2 得到y值
      // 计算出当前鼠标相对于元素中心点的坐标
      x = x - origin.x; // 因为x大于origin.x 是在y轴右边,直接减就行了
      y = origin.y - y; // 但是y如果要在x轴上方,它是比origin.y要小的,所以这里就需要反过来

      // 然后计算就可以了
      const deg = (Math.atan2(y, x) / Math.PI) * 180;
      const option = {
          moveX: 0,
          moveY: 0,
          deg: -deg || 0
      };
      this.set_transform($dom, option);
  },
  get_origin_transform($dom) {
      let old = $dom.style.transform;
      const transformOption = {};
      if (old) {
          old = old.split(' ');
          old.forEach((item) => {
              transformOption[item.replace(/\((.*)\)/, '')] =
                  item.replace(/[^0-9-]/g, '');
          });
      }
      return transformOption;
  },
  set_transform(dom, option) {
      Methods.css(dom, {
          transform: `rotate(${option.deg}deg) translateX(${option.moveX}px) translateY(${option.moveY}px)`
      });
  },
  const positonByHtml = {
      centerX: Methods.offset($dom).left + $dom.clientWidth / 2 - document.documentElement.scrollLeft,
      centerY: Methods.offset($dom).top + $dom.clientHeight / 2 - document.documentElement.scrollTop
  };
  let x = e.clientX;
  let y = e.clientY;
  const origin = {
      x:
          +positonByHtml.centerX +
          parseInt(transformOption.translateX || 0),
      y:
          +positonByHtml.centerY +
          parseInt(transformOption.translateY || 0)
  }; // 先手动指定当前中心点,也可以根据当前元素的left+width/2 的到x  top+height/2 得到y值
  // 计算出当前鼠标相对于元素中心点的坐标
  x = x - origin.x; // 因为x大于origin.x 是在y轴右边,直接减就行了
  y = origin.y - y; // 但是y如果要在x轴上方,它是比origin.y要小的,所以这里就需要反过来

  // 然后计算就可以了
  const deg = (Math.atan2(y, x) / Math.PI) * 180;

说明,旋转点的计算是根据浏览器的可视高度计算的,所以用offsetTop+$dom.clientHeight(水印的一半)计算的值要在减去滚轮的scrollY才计算的精准

移动部分

移动部分代码逻辑

  • 通过javascript存在的drag属性去控制
  • dragstart的时候记录鼠标起始点,记录后开始监听drag时间,通过鼠标移动的距离-减去起始点+之前移动的距离来计算计算
  • transformOption为记录之前移动的距离和旋转,防止第二次拖动或旋转的时候,水印复位
  • 因为css3的旋转和移动都记录在一个属性上,没办法拆分,移动和旋转之前,一定一定要把之前的状态带上

    drag_handle(e) {
      for (let i = 0; i < this.markList.length; i++) {
          const $dom = this.$refs['ref-name'][i];
          const transformOption = this.get_origin_transform($dom);
          const startX = e.clientX;
          const startY = e.clientY;
          $dom.ondrag = (e) => {
              e.preventDefault();
              const option = {
                  moveX:
                      e.clientX -
                      startX +
                      parseInt(transformOption.translateX || 0),
                  moveY:
                      e.clientY -
                      startY +
                      parseInt(transformOption.translateY || 0),
                  deg: transformOption.rotate || 0
              };
              this.set_transform($dom, option);
          };
          $dom.ondragover = (e) => {
              e.preventDefault();
          };
      }
  },
  get_origin_transform($dom) {
      let old = $dom.style.transform;
      const transformOption = {};
      if (old) {
          old = old.split(' ');
          old.forEach((item) => {
              transformOption[item.replace(/\((.*)\)/, '')] =
                  item.replace(/[^0-9-]/g, '');
          });
      }
      return transformOption;
  },
  set_transform(dom, option) {
      Methods.css(dom, {
          transform: `rotate(${option.deg}deg) translateX(${option.moveX}px) translateY(${option.moveY}px)`
      });
  },
  moveX:
   e.clientX -
      startX +
      parseInt(transformOption.translateX || 0),
  moveY:
      e.clientY -
      startY +
      parseInt(transformOption.translateY || 0),
  deg: transformOption.rotate || 0
缩放部分

效果图:
在这里插入图片描述
实现的功能:

  • 点击左上角的缩放图标,可进行拉伸
  • 可以自定义个数

缩放部分代码逻辑

  • 首先获取图标的第一个兄弟元素(img),通过e.target.previousSibling获取,以及图标的父元素(rotate-name)
  • 获取父元素的宽高,以及刚开始的鼠标坐标
  • 算法:开始时的鼠标坐标 - 移动的鼠标坐标 + 父元素的宽高

限制了图片最小缩放为30*30

具体代码

 scale_img(e) {
     // 缩放图片
     this._parentNode =
         e.target.tagName.toLowerCase() === 'svg'
             ? e.target.previousSibling
             : e.target.parentNode.previousSibling;
     const _grentParentNode =
         e.target.tagName.toLowerCase() === 'svg'
             ? e.target.parentNode
             : e.target.parentNode.parentNode;
     this._pos = {
         w: _grentParentNode.offsetWidth,
         h: _grentParentNode.offsetHeight,
         x: e.clientX,
         y: e.clientY
     };
     document.addEventListener('mousemove', this.get_scale);
     document.addEventListener('mouseup', () => {
         document.removeEventListener('mousemove', this.get_scale);
     });
 },
 get_scale(e) {
     // 缩放图片的mousemove事件,给图片设置宽高
     e.preventDefault();
     const $dom = this._parentNode;
     // 设置图片的最小缩放为30*30
     const w = Math.max(30, this._pos.x - e.clientX + this._pos.w - 20);
     const h = Math.max(30, this._pos.y - e.clientY + this._pos.h);
     $dom.style.width = w + 'px';
     $dom.style.height = h + 'px';
 },

颜色选择部分

效果图
颜色选择部分
实现的功能:

  • 此功能单独抽了一个vue文件,可以在其他有需要的地方使用
  • 可以通过内外两个部分,共同去决定颜色
  • 颜色为双向绑定,视图可通过vm改变模型,模型也可通过vm改变视图

具体的逻辑可以查看源码,实现起来挺复杂的,感谢万能的百度

接下来是核心部分

生成图片的部分

效果图
在这里插入图片描述
点击生成图片,便生成了canvas

使用方式
  • 方法被单独抽出,可以实现自定义的代码转化成canvas
  • 方法有两个参数,第一个参数为传入的dom元素,第二个参数为设置到canvas上的css属性
  • 调用后,通过.then方法可以获取到生成图片的base64
具体的逻辑
  • 大体逻辑为:将html代码读取,转化为svg,最后在转成canvas(也可以使用html2canvas)
  • 定义一个str缓存,用于存放生成的dom。
  • 根据传入的dom,遍历里面的所有元素。并且遍历所有元素的css属性,最后拼接起来
  • 通过'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(data)))将svg字符串变成svg格式的base64
  • 最后通过ctx.drawImage(img, 0, 0);绘制到canvas上

说明
1、最后绘制上去后,要remove掉传入的元素,并把canvas上树,替代原来的元素
2、在绘制svg的时候,对img以及svg,path标签进行了特殊处理,svg,path直接忽视跳过,img除了读取css属性,还要读取src属性,并且读取的src属性必须转化为base64格式

详细的使用方式和原理请看 html2canvas的简单实现

具体代码
/**
     * 将html代码转化为图片
     * @param {*} dom dom元素
     * @param {*} options 配置  宽高:width, height, canvas样式:style
     * @description
     */
methods.htmlTocanvas = (dom, options) => {
    options = Object.assign({ width: 100, height: 100, style: {}}, options); // 默认样式
    const $canvas = document.createElement('canvas');
    $canvas.id = 'canvas';
    $canvas.width = options.width;
    $canvas.height = options.height;
    if (JSON.stringify(options.style) !== '{}') { // 配置canvas的样式
        for (const key in options.style) {
            methods.css($canvas, `${key}`, `${options.style[key]}`);
        }
    }
    const ctx = $canvas.getContext('2d');


    async function init_main() { // 主方法
        const data = await get_svg_dom_string(dom);
        // const DOMURL = window.URL || window.webkitURL || window;
        const img = new Image();
        img.src = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(data)));
        // const svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
        // const url = DOMURL.createObjectURL(svg);
        // img.setAttribute('crossOrigin', 'anonymous');
        // img.src = url;
        return new Promise(resolve => {
            img.onload = function() { // 最终生成的canvas
                ctx.drawImage(img, 0, 0);
                const parentNode = dom.parentNode;
                parentNode.insertBefore($canvas, dom); // 将canvas插入原来的位置
                parentNode.removeChild(dom); // 最终移除页面中被转换的代码
                resolve($canvas.toDataURL('image/png'));
            };
        });
    }

    async function get_svg_dom_string(element) { // 将html代码嵌入svg
        const $dom = await render_dom(element, true);
        return `
                <svg xmlns="http://www.w3.org/2000/svg" width = "${options.width}" height = "${options.height}">
                    <foreignObject width="100%" height="100%">
                         ${$dom}
                    </foreignObject>\n
                </svg>
            `;
    }

    async function render_dom(element, isTop) { // 递归调用获取子标签
        const tag = element.tagName.toLowerCase();
        let str = `<${tag} `;
        let flag = true;
        // 最外层的节点要加xmlns命名空间
        isTop && (str += `xmlns="http://www.w3.org/1999/xhtml" `);
        if (str === '<img ') { // img标签特殊处理
            flag = false;
            let base64Img = '';
            if (element.src.length > 30000) { // 判断src属性是不是base64, 是的话不用处理,不是的话,转换base64
                base64Img = element.src;
            } else {
                base64Img = await getBase64Image(element.src);
            }
            str += `src="${base64Img}" style="${get_element_styles(element)}" />\n`;
        } else if (str.includes('svg') || str.includes('path')) {
            flag = false;
            str = '';
        } else {
            str += `style="${get_element_styles(element)}">\n`;
        }
        if (element.children.length) {
            for (const el of element.children) {
                str += await render_dom(el);
            }
        } else {
            str += element.innerHTML;
        }
        if (flag) {
            str += `</${tag}>\n`;
        }
        return str;
    }

    function get_element_styles(element) { // 获取标签的所有样式
        const css = window.getComputedStyle(element);
        let style = '';
        for (const key of css) {
            style += `${key}:${css[key]};`;
        }
        return style;
    }

    function getBase64Image(img) { // 获取图片的base64
        const image = new Image();
        image.src = img;
        return new Promise(resolve => {
            image.onload = function(){
                const canvas = document.createElement('canvas');
                canvas.id = 'image';
                canvas.width = image.width;
                canvas.height = image.height;
                const ctxImg = canvas.getContext('2d');
                ctxImg.drawImage(image, 0, 0, image.width, image.height);
                const ext = image.src.substring(image.src.lastIndexOf('.') + 1).toLowerCase();
                const dataURL = canvas.toDataURL('image/' + ext);
                resolve(dataURL);
            };
        });
    }

    return init_main();
};

导出图片

通过 <a :href="url" :download="markList[0].username+'.png'">导出</a>导出,href为图片的base64地址,下载名称为,第一个水印的名称

GitHub地址:https://github.com/Little-LittleProgrammer/npm-components/tree/master

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
主要功能: 1.多用户注册各自使用,互不干予。 2.在自己的空间中创建多个相册,并上传多照片进行在线查看管理。 3.相册共享权限可设好友、所有人、指定人,并可指定共享类别、查看密码。 4.强大的相册及照片搜索功能,可按各种属性进行复合查找。 5.照片打印、设为封面、设为用户头像等多种操作。 6.在线大头贴拍照功能,并有数十种相框可供选择。 7.数码照片40多种属性EXIF信息显示。 8.照片可批量打包成ZIP文件下载。 9.对相册及照片添加文字简介功能。 10.强大的在线图片编辑功能(缩放旋转水印,裁剪)。 11.类似Windows中的图片缩略图预览模式,方便查看图片。 12.注册用户有二级域名空间可以让访客随时访问自己的空间进行查看及留言。 13.可自由设定相册中的照片排序方式或手动拖拽进行照片排序。 14.上传时用户可自己指定是否需要缩小大图到网页标准尺寸,以节省空间。 15.相册及照片标签输入方式,可按各类标签查看及搜索照片。 16.他人共享照片收藏功能,实时查看、评论、及其它操作。 17.对共享照片投票功能,可按投票数进行排序。 18.图片地址点击复制和短地址功能。 19.好友功能,可将他人加为好友,并查看对方的指定好友相册及发送短消息。 20.公共相册显示区,显示每个用户共享给所有人的相册和照片并可进行显示及评论。 21.用户可按等级权限自由绑定顶级域名代替外部空间地址,空间地址个性化。 22.界面样式自定义并可由用户在前台选择喜爱的空间风格。 23.用户空间个性化定义公告及标题显示,和对访客留言的查看及管理。 24.站内短信功能,提供收件箱、发件箱、已发送、垃圾箱功能。 25.网银、支付宝、财付通、快钱,PayNow(台湾)在线支付,空间自动续费升级,别人代充,充值卡使用管理。 26.用户积分功能, 可积分兑换金币并可升级空间。 27.前台违法举报,实时对上传内容进行管理。 28.网站留言功能,用户可实时向管理员反馈信息,管理员后台回复。 29.自动过滤内容和禁传非法文件,防止不正当使用。 30.可针对不同等级,不同分组的用户设置进入时公告。 31.批量用户管理操作,群发短信/邮件,可对列表中的所有用户统一一次操作。 32.后台实时查看及管理用户上传的所有照片和建立的相册。 33.强大的用户查找,根据有效期、最后登录时间,审核及锁定,等级查找。 34.用户等级制,可设每个等级的空间、上传大小、相册和照片个数及其它条数限制。 35.为每个等级设置开启外链、外链地址、开启二级域名及等级费用。 36.注册审核、邮件验证、防重复IP注册、时长注册功能。 37.来访IP限制设定和管理员可登录的IP设定。 38.后台管理员可进行管理权限划分并记录操作日志。 39.可直观设定相册、照片、用户、公告、留言的外部调用参数,支持模板调用。 40.可设置相册及照片的推荐,模板调用。 41.管理员在后台可统一设置用户上传照片后添加水印。 42.可设置照片需审核后才能显示已共享的照片。 43.导入用户功能,支持ACCESS或Excel中导出的TXT格式信息,分项目一次导入。 44.导出用户功能,可选择条件过滤导出,指定导出项目,导出成txt或Excel格式。 45.后台可指定原图被缩减的最大尺寸,以防巨大图片。 46.页面广告分区添加及管理。 47.随时在线整理硬盘和数据中的数据,保持数据最优化。 48.前台模板标签调用,可组建自己的照片站页面。 49.上传照片路径可按日期建立目录,确保同一目录下图片不会过多。 50.完善的等级防盗链功能,支持链接排除、链接包含、个人独立设置可以链接的地址。 51.Flash上传模式支持一次选择多图片、进度显示上传。上传完毕自动生成等比例缩略图。 52.可以同时下载多张网络图片到空间,并可设置水印等功能。 53.上传时可自由设置是否在照片上添加文字水印图片水印或不添加水印。 54.用户分组设置,可分多组域名及多台服务器协同管理。 55.仅需为程序目录及用户存放目录设置写入及修改权限,系统更安全。 56.完美兼容firefox等其它非IE内核浏览器。 57.三层架构模式开发,扩充及调用更方便。 58.内含ajax文件操作技术,更加提高用户体验,提高系统运行效率。 59.分简体版、繁体版、英文版三种版本。 60.可以和《桃源网络硬盘.Net》用户完全整合。 61.全面的整合接口,支持注册、登录、修改资料及密码、添加、删除、审核、锁定用户,支持不同域名整合。 62.多种数据支持:ACCESS、MSSQL、MySQL、Oracle。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值