// ==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)" })
}
})();