边读边看图

图片预览:通过双击按键,把图片固定在页面上,边读文字边看图,同时支持缩放、移动功能

目前為 2023-08-10 提交的版本,檢視 最新版本

// ==UserScript==
// @name         边读边看图
// @namespace    http://tampermonkey.net/
// @version      0.2.8
// @description  图片预览:通过双击按键,把图片固定在页面上,边读文字边看图,同时支持缩放、移动功能
// @author       Enjoy
// @icon         https://foruda.gitee.com/avatar/1671100286067517749/4867929_enjoy_li_1671100285.png!avatar60
// @match        *://*/*
// @exclude    *hrwork*
// @exclude    *zhaopinyun*
// @exclude    *localhost*
// @exclude    *127.0.0.1*
// @grant        GM_addElement
// @grant        GM_setClipboard
// @license      GPL License
// ==/UserScript==

// 函数文档 https://www.tampermonkey.net/documentation.php#api:GM_addElement
// @match        *://mp.weixin.qq.com/s/*

(function () {
  let crxName = 'img_preview_style'
  let modifyDate = new Date().toLocaleString()
  GM_addElement('style',{
    textContent: `.pages_skin_pc .rich_media_area_primary_inner{margin-left:initial;}`,
    id: crxName
  })
  run()
  return

  function run() {
    class ImgPreviwer {
      constructor(options = {}) {
        this.state = this.mergeOptions(options)
        this.shadowRoot = this.createShadowRoot()
        this.onPreviwerEvent()
        return this.shadowRoot
      }
      /** @描述 状态 */
      state = null
      shadowRoot = null
      /** @描述 创建 shadowRoot */
      createShadowRoot(selector = '#imgPreview') {
        let dom = document.querySelector(`${selector}`)
        if (!dom) {
          dom = document.createElement('div')
          dom.setAttribute('id',selector.replace(/[.#]/g,''))
          dom.setAttribute('style','width:0;height:0')
          document.documentElement.appendChild(dom)
        }
        if (!dom.shadowRoot) {
          // 创建蒙层容器
          const maskContent = document.createElement('div')
          maskContent.classList.add('modal')
          maskContent.appendChild(this.createStyle(this.state))
          // 添加在body下
          dom.attachShadow({ mode: 'open' })
          dom.shadowRoot.appendChild(maskContent)
        }
        return dom.shadowRoot
      }
      /** @描述 合并选项 */
      mergeOptions(options) {
        let opt = {}
        let defaultOptions = {
          contentSelector: 'body',
          selector: 'img',
          showRootSelector: '#img_preview',
          backgroundColor: "rgba(0,0,0,0)",
          extraStyle:''
        }
        Object.assign(opt,defaultOptions,options)
        return opt
      }
      /** @描述 创建shadowbox中的样式 */
      createStyle({contentSelector,selector,backgroundColor,extraStyle}) {
        const style = document.createElement('style');
        style.innerHTML = `${contentSelector} ${selector} {
                                cursor: zoom-in;
                              }
                                /* 图片预览 */
                                .modal {
                                touch-action: none;
                                position: fixed;
                                z-index: 99;
                                top: 0;
                                left: 0;
                                width: 100vw;
                                height: 100vh;
                                background-color: ${backgroundColor};
                                user-select: none;
                                pointer-events: none;
                                }
                                .modal>*{
                                  pointer-events: auto;
                                }
                                .modal>img {
                                position: absolute;
                                padding: 0;
                                margin: 0;
                                box-shadow: #09818f 0px 0px 15px 0px;
                                border-radius: 10px;
                                /* transition: all var(--delay_time); */
                                transform: translateZ(0);
                                }

                                img.active {
                                  animation: activeImg 0.5s 4 ease-out forwards;
                                  transition: all;
                                }

                                @keyframes activeImg {
                                  0% {
                                    box-shadow: #09818f 0px 0px 15px 0px;
                                  }
                                  50% {
                                    box-shadow: red 0px 0px 100px 0px;
                                  }
                                  100% {
                                    box-shadow: #09818f 0px 0px 15px 0px;
                                  }
                                }
                                ${extraStyle}
                                `
        return style
      }
      /** @描述 预览操作 */
      onPreviwerEvent() {
        let that = this
        let { contentSelector,selector } = that.state

        let eventsProxy = document.querySelector(contentSelector) || window.document.body

        eventsProxy.addEventListener('dblclick',function (e) {
          let src = e.target.src || window.getComputedStyle(e.target).backgroundImage.match(/^url\("([^\s]+)"\)$/i)?.[1]
          if (!src) return;
          e.preventDefault()

          let findOneInPage = [...eventsProxy.querySelectorAll(selector)].find(item => item === e.target)
          if (findOneInPage) {

            let findOneInModal = [...that.shadowRoot.querySelectorAll(selector)].find(item => item.src === src)
            if (findOneInModal) {
              if (!findOneInModal.classList.contains('active')) {
                findOneInModal.classList.add('active')
                return;
              } else {
                findOneInModal.remove()
                findOneInModal = null
              }
            }

            if (!findOneInModal) {
              // originalEl.style.opacity = 0
              new ImgPreviewer(that.shadowRoot,e.target,src)
            }
          }
        })
      }
    }

    class ImgPreviewer {
      constructor(shadowRoot,originalEl,src) {
        this.state = Object.assign({},this.state,this.mergeOptions(shadowRoot,originalEl,src))
        let cloneEl = this.appendImg(src)
        this.state.cloneEl = cloneEl
        this.fixPosition(cloneEl)
        this.addEvents(cloneEl)
        return cloneEl
      }
      state = {
        scale: 1,
        offset: { left: 0,top: 0 },
        origin: 'center',
        initialData: {
          offset: {},
          origin: 'center',
          scale: 1
        },
        startPoint: { x: 0,y: 0 },
        // 记录初始触摸点位
        isTouching: false,
        // 标记是否正在移动
        isMove: false,
        // 正在移动中,与点击做区别
        touches: new Map(),
        // 触摸点数组
        lastDistance: 0,
        lastScale: 1,
        // 记录下最后的缩放值
        scaleOrigin: { x: 0,y: 0,},
      }
      mergeOptions(shadowRoot,originalEl,src) {
        const { innerWidth: winWidth,innerHeight: winHeight } = window
        const { offsetWidth,offsetHeight } = originalEl

        // Element.getBoundingClientRect() 方法返回元素的大小及其相对于【视口】的位置
        const { top,left } = originalEl.getBoundingClientRect()

        return ({
          shadowRoot,
          originalEl,
          src,
          winWidth,
          winHeight,
          offsetWidth,
          offsetHeight,
          top,
          left,
          maskContent: shadowRoot.querySelector('.modal')
        })
      }
      /** @描述 添加图片 */
      appendImg(src) {
        let cloneEl = document.createElement('img')
        cloneEl.src = src
        this.state.maskContent.appendChild(cloneEl)
        return cloneEl
      }
      /** @描述 添加监听事件 */
      addEvents(cloneEl,events = ['dblclick','mousewheel','pointerdown','pointerup','pointermove','pointercancel']) {
        let that = this
        events.forEach(item => {
          if (item === 'mousewheel') {
            cloneEl.addEventListener('mousewheel',that[`on${item}`],{ passive: false })
            return
          }
          cloneEl.addEventListener(item,that[`on${item}`])
        })
      }
      /** @描述 双击事件 */
      ondblclick = (e) => {
        e.preventDefault()
        let that = this
        let state = that.state
        setTimeout(() => {
          if (state.isMove) {
            state.isMove = false
          } else {
            that.changeStyle(state.cloneEl,['transition: all .3s',`left: ${state.left}px`,`top: ${state.top}px`,`transform: translate(0,0)`,`width: ${state.offsetWidth}px`])
            setTimeout(() => {
              state.maskContent.removeChild(state.cloneEl)
              // originalEl.style.opacity = 1
              state.cloneEl.removeEventListener('dblclick',that.ondblclick)
            },300)
          }
        },280)
      }
      /** @描述  指针按下事件*/
      onpointerdown = (e) => {
        e.preventDefault()
        let that = this
        let state = that.state

        state.touches.set(e.pointerId,e)
        // TODO: 点击存入触摸点
        state.isTouching = true

        state.startPoint = {
          x: e.clientX,
          y: e.clientY
        }

        if (state.touches.size === 2) {
          // TODO: 判断双指触摸,并立即记录初始数据
          state.lastDistance = that.getDistance()
          state.lastScale = state.scale
        }
      }
      /** @描述 滚轮缩放 */
      onmousewheel = (e) => {
        e.preventDefault()
        if (!e.deltaY) return;

        let that = this
        let state = that.state

        state.origin = `${e.offsetX}px ${e.offsetY}px`

        // 缩放执行
        if (e.deltaY < 0) {
          // 放大
          state.scale += 0.1
        } else if (e.deltaY > 0) {
          state.scale >= 0.2 && (state.scale -= 0.1)
          // 缩小
        }

        if (state.scale < state.initialData.scale) {
          console.log(`state.scale < state.initialData.scale => %O `,state.scale,state.initialData.scale);

          that.reduction()
        }

        state.offset = that.getOffsetPageCenter(e.offsetX,e.offsetY)

        that.changeStyle(state.cloneEl,['transition: all .15s',`transform-origin: ${state.origin}`,`transform: translate(${state.offset.left + 'px'}, ${state.offset.top + 'px'}) scale(${state.scale})`])

      }

      /** @描述 获取中心改变的偏差 */
      getOffsetPageCenter(x = 0,y = 0) {
        let state = this.state
        const touchArr = Array.from(state.touches)
        if (touchArr.length === 2) {
          const start = touchArr[0][1]
          const end = touchArr[1][1]
          x = (start.offsetX + end.offsetX) / 2
          y = (start.offsetY + end.offsetY) / 2
        }
        state.origin = `${x}px ${y}px`

        const offsetLeft = (state.scale - 1) * (x - state.scaleOrigin.x) + state.offset.left
        const offsetTop = (state.scale - 1) * (y - state.scaleOrigin.y) + state.offset.top

        state.scaleOrigin = { x,y }

        return {
          left: offsetLeft,
          top: offsetTop
        }
      }

      /** @描述  获取距离*/
      getDistance() {
        const touchArr = Array.from(this.state.touches)
        if (touchArr.length < 2) {
          return 0
        }
        const start = touchArr[0][1]
        const end = touchArr[1][1]
        return Math.hypot(end.x - start.x,end.y - start.y)
      }

      /** @描述  修改样式,减少回流重绘*/
      changeStyle(el,arr) {
        const original = el.style.cssText.split(';')
        original.pop()
        el.style.cssText = original.concat(arr).join(';') + ';'
      }

      /** @描述 还原记录,用于边界处理 */
      reduction() {
        let that = this
        let state = that.state
        that.timer && clearTimeout(that.timer)
        that.timer = setTimeout(() => {
          // offset = state.initialData.offset
          // origin = state.initialData.origin
          // scale = state.initialData.scale
          console.log(`state => %O `,state);

          that.changeStyle(state.cloneEl,[`transform: translate(${state.offset.left + 'px'}, ${state.offset.top + 'px'}) scale(${state.scale})`,`transform-origin: ${state.origin}`])
        },300)
      }

      /** @描述 松开指针 事件 */
      onpointerup = (e) => {
        e.preventDefault()
        let that = this
        let state = that.state
        state.touches.delete(e.pointerId)
        // TODO: 抬起移除触摸点
        if (state.touches.size <= 0) {
          state.isTouching = false
        } else {
          const touchArr = Array.from(state.touches)
          // 更新点位
          state.startPoint = {
            x: touchArr[0][1].clientX,
            y: touchArr[0][1].clientY
          }
        }
        setTimeout(() => {
          state.isMove = false
        },300);
      }

      /** @描述 指针移动事件 */
      onpointermove = (e) => {
        e.preventDefault()
        let that = this
        let state = that.state
        if (state.isTouching) {
          state.isMove = true
          if (state.touches.size < 2) {
            // 单指滑动
            state.offset = {
              left: state.offset.left + (e.clientX - state.startPoint.x),
              top: state.offset.top + (e.clientY - state.startPoint.y)
            }

            that.changeStyle(state.cloneEl,['transition: all 0s',`transform: translate(${state.offset.left + 'px'}, ${state.offset.top + 'px'}) scale(${state.scale})`,`transform-origin: ${origin}`])
            // 更新点位
            state.startPoint = {
              x: e.clientX,
              y: e.clientY
            }
          } else {
            // 双指缩放
            state.touches.set(e.pointerId,e)
            const ratio = that.getDistance() / state.lastDistance
            state.scale = ratio * state.lastScale
            state.offset = that.getOffsetPageCenter()
            if (state.scale < state.initialData.scale) {
              that.reduction()
            }
            that.changeStyle(state.cloneEl,['transition: all 0s',`transform: translate(${state.offset.left + 'px'}, ${state.offset.top + 'px'}) scale(${state.scale})`,`transform-origin: ${state.origin}`])
          }
        }
      }
      /** @描述 取消指针事件 */
      onpointercancel = (e) => {
        e.preventDefault()
        this.state.touches.clear()
        // 可能存在特定事件导致中断,真机操作时 pointerup 在某些边界情况下不会生效,所以需要清空
      }
      /** @描述 移动图片到屏幕中心位置 */
      fixPosition(cloneEl) {
        let that = this
        let state = that.state

        /** @描述 原图片 中心点 */
        const originalCenterPoint = {
          x: state.offsetWidth / 2 + state.left,
          y: state.offsetHeight / 2 + state.top
        }

        /** @描述 页面 中心点 */
        const winCenterPoint = {
          x: state.winWidth / 2,
          y: state.winHeight / 2
        }

        /** @描述  新建图片的定位点:通过原图片中心点到页面中心点的 偏移量*/
        const offsetDistance = {
          left: winCenterPoint.x - originalCenterPoint.x + state.left,
          top: winCenterPoint.y - originalCenterPoint.y + state.top
        }

        /** @描述 放大后的 */
        let scaleNum = this.adaptScale()
        const diffs = {
          left: ((scaleNum - 1) * state.offsetWidth) / 2,
          top: ((scaleNum - 1) * state.offsetHeight) / 2
        }
    console.log(`state => %O `,state );

        this.changeStyle(cloneEl,[`left: ${state.left}px`,`top: ${state.top}px`,'transition: all 0.3s',`width: ${state.offsetWidth * scaleNum + 'px'}`,`transform: translate(${offsetDistance.left - state.left - diffs.left}px, ${offsetDistance.top - state.top - diffs.top}px)`])

        /** @描述 消除偏差:让图片相对于window  0 0定位,通过translate设置中心点重合*/
        // setTimeout(() => {
        //   that.changeStyle(cloneEl,['transition: all 0s',`left: 0`,`top: 0`,`transform: translate(${offsetDistance.left - diffs.left}px, ${offsetDistance.top - diffs.top}px)`])
        //   that.state.offset = {
        //     left: offsetDistance.left - diffs.left,
        //     top: offsetDistance.top - diffs.top
        //   }
        //   // 记录值
        //   that.record()
        // },300)
      }
      /** @描述 记录初始化数据 */
      record() {
        let state = this.state
        state.initialData = Object.assign({},{
          offset: state.offset,
          origin: state.origin,
          scale: state.scale
        })
      }
      /** @描述 计算自适应屏幕的缩放 */
      adaptScale() {
        let { winWidth,winHeight,originalEl } = this.state
        const { offsetWidth: w,offsetHeight: h } = originalEl
        let scale = winWidth / w
        if (h * scale > winHeight - 80) {
          scale = (winHeight - 80) / h
        }
        return scale
      }
    }
    let shadowRoot = new ImgPreviwer({ backgroundColor: "rgba(0,0,0,0)" })
  }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址