one-screen-scale 1.1.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ /**
2
+ * 组件级反缩放补偿(与业务 WinRateChart 手写公式一致):
3
+ * transform: scale(inverse); width/height: 100% * pageScale; origin: top left
4
+ *
5
+ * 只抵消页面 v-fit-screen 的 scale;不处理浏览器 Ctrl+/-。
6
+ */
7
+
8
+ import { SCALE_CHANGE_EVENT } from './core.js'
9
+
10
+ /** 子树 canvas 需要重绘时抛出(挂在每个 <canvas> 上) */
11
+ export const CANVAS_RESIZE_EVENT = 'one-screen-scale-canvas-resize'
12
+
13
+ /** @type {null | { getInstanceByDom?: (el: Element) => { resize: Function } | undefined }} */
14
+ let registeredEcharts = null
15
+
16
+ /** 业务可注入 echarts 模块(避免插件硬依赖);供 resizeCanvas 探测 */
17
+ export function setCompensateEcharts(echarts) {
18
+ registeredEcharts = echarts || null
19
+ }
20
+
21
+ function readCssVarScale(el) {
22
+ if (!el || typeof getComputedStyle === 'undefined') return NaN
23
+ const raw = getComputedStyle(el).getPropertyValue('--one-screen-scale').trim()
24
+ if (!raw) return NaN
25
+ const v = parseFloat(raw)
26
+ return v > 0 ? v : NaN
27
+ }
28
+
29
+ function readTransformScale(el) {
30
+ if (!el || typeof getComputedStyle === 'undefined') return NaN
31
+ const t = getComputedStyle(el).transform
32
+ if (!t || t === 'none') return NaN
33
+
34
+ const m2 = t.match(/matrix\(([^,]+),\s*[^,]+,\s*[^,]+,\s*([^,]+)/)
35
+ if (m2) return (parseFloat(m2[1]) + parseFloat(m2[2])) / 2
36
+
37
+ const m3 = t.match(
38
+ /matrix3d\(([^,]+),[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,([^,]+)\)/
39
+ )
40
+ if (m3) return (parseFloat(m3[1]) + parseFloat(m3[2])) / 2
41
+
42
+ const sm = t.match(/scale\(([^)]+)\)/)
43
+ if (sm) {
44
+ const vals = sm[1].split(',').map((v) => parseFloat(v.trim()))
45
+ return vals.length >= 2 ? (vals[0] + vals[1]) / 2 : vals[0]
46
+ }
47
+ return NaN
48
+ }
49
+
50
+ /** 从 fromEl 向上解析页面 scale(跳过已挂补偿的节点自身 transform) */
51
+ export function resolvePageScale(fromEl) {
52
+ let node = fromEl
53
+ while (node && node.nodeType === 1) {
54
+ const fromVar = readCssVarScale(node)
55
+ if (fromVar > 0) return fromVar
56
+ node = node.parentElement
57
+ }
58
+
59
+ node = fromEl
60
+ while (node && node.nodeType === 1) {
61
+ if (!node.__oneScreenCompensate) {
62
+ const fromTf = readTransformScale(node)
63
+ if (fromTf > 0 && fromTf !== 1) return fromTf
64
+ }
65
+ node = node.parentElement
66
+ }
67
+ return 1
68
+ }
69
+
70
+ /**
71
+ * 给槽位内层写入反缩放行内样式。父槽位需有明确宽高。
72
+ * @returns {{ scale: number, inverseScale: number }}
73
+ */
74
+ export function applyCompensateTransform(el) {
75
+ if (!el) return { scale: 1, inverseScale: 1 }
76
+ const scale = resolvePageScale(el)
77
+ const inverseScale = scale > 0 ? 1 / scale : 1
78
+ el.style.transform = `scale(${inverseScale})`
79
+ el.style.transformOrigin = 'top left'
80
+ el.style.width = `${100 * scale}%`
81
+ el.style.height = `${100 * scale}%`
82
+ el.style.overflow = 'visible'
83
+ el.__oneScreenCompensate = true
84
+ return { scale, inverseScale }
85
+ }
86
+
87
+ export function clearCompensateTransform(el) {
88
+ if (!el) return
89
+ el.style.transform = ''
90
+ el.style.transformOrigin = ''
91
+ el.style.width = ''
92
+ el.style.height = ''
93
+ el.style.overflow = ''
94
+ delete el.__oneScreenCompensate
95
+ }
96
+
97
+ function resolveEchartsLib(bindingEcharts) {
98
+ return (
99
+ bindingEcharts ||
100
+ registeredEcharts ||
101
+ (typeof globalThis !== 'undefined' ? globalThis.echarts : null) ||
102
+ null
103
+ )
104
+ }
105
+
106
+ function boxSize(node) {
107
+ const w = Math.max(1, Math.round(node?.offsetWidth || 0))
108
+ const h = Math.max(1, Math.round(node?.offsetHeight || 0))
109
+ return { width: w, height: h }
110
+ }
111
+
112
+ function tryEchartsResize(node, getInst) {
113
+ if (!node || typeof getInst !== 'function') return false
114
+ let inst
115
+ try {
116
+ inst = getInst(node)
117
+ } catch {
118
+ return false
119
+ }
120
+ if (!inst || typeof inst.resize !== 'function') return false
121
+ const { width, height } = boxSize(node)
122
+ try {
123
+ inst.resize({ width, height })
124
+ return true
125
+ } catch {
126
+ return false
127
+ }
128
+ }
129
+
130
+ function tryChartJsResize(canvas) {
131
+ const Chart = typeof globalThis !== 'undefined' ? globalThis.Chart : null
132
+ const getChart = Chart?.getChart
133
+ if (typeof getChart !== 'function') return false
134
+ let chart
135
+ try {
136
+ chart = getChart(canvas)
137
+ } catch {
138
+ return false
139
+ }
140
+ if (!chart || typeof chart.resize !== 'function') return false
141
+ try {
142
+ chart.resize()
143
+ return true
144
+ } catch {
145
+ return false
146
+ }
147
+ }
148
+
149
+ /**
150
+ * 补偿后通知子树 canvas 重绘:
151
+ * - 探测 ECharts / Chart.js(若已加载)
152
+ * - 每个 <canvas> 派发 CANVAS_RESIZE_EVENT,业务可自行监听重绘
153
+ * 量尺寸用 offsetWidth(勿用 getBoundingClientRect)
154
+ *
155
+ * @returns {number} 成功调用已知图表库 resize 的次数
156
+ */
157
+ export function resizeCanvasUnder(el, echartsLib) {
158
+ if (!el) return 0
159
+
160
+ const echarts = resolveEchartsLib(echartsLib)
161
+ const getInst = echarts?.getInstanceByDom
162
+ let count = 0
163
+
164
+ const canvases = el.querySelectorAll ? el.querySelectorAll('canvas') : []
165
+ for (const canvas of canvases) {
166
+ const host = canvas.parentElement || canvas
167
+ const { width, height } = boxSize(host)
168
+
169
+ if (tryEchartsResize(canvas, getInst)) count += 1
170
+ else if (tryEchartsResize(host, getInst)) count += 1
171
+
172
+ if (tryChartJsResize(canvas)) count += 1
173
+
174
+ try {
175
+ canvas.dispatchEvent(
176
+ new CustomEvent(CANVAS_RESIZE_EVENT, {
177
+ bubbles: true,
178
+ detail: { width, height, canvas, el },
179
+ })
180
+ )
181
+ } catch {
182
+ /* ignore */
183
+ }
184
+ }
185
+
186
+ // ECharts 常挂在 div 上(不一定已有 canvas)
187
+ if (typeof getInst === 'function') {
188
+ const nodes = [el, ...el.querySelectorAll('div')]
189
+ for (const node of nodes) {
190
+ if (tryEchartsResize(node, getInst)) count += 1
191
+ }
192
+ }
193
+
194
+ return count
195
+ }
196
+
197
+ /** @deprecated 请用 resizeCanvasUnder */
198
+ export function resizeEchartsUnder(el, echartsLib) {
199
+ return resizeCanvasUnder(el, echartsLib)
200
+ }
201
+
202
+ function wantsResizeCanvas(options = {}) {
203
+ return Boolean(
204
+ options.resizeCanvas ||
205
+ options.echarts || // 兼容旧名
206
+ options.echartsLib
207
+ )
208
+ }
209
+
210
+ function resolveBoundEcharts(options = {}) {
211
+ if (typeof options.echarts === 'object') return options.echarts
212
+ if (options.echartsLib) return options.echartsLib
213
+ return undefined
214
+ }
215
+
216
+ /** options 可以是对象,也可以是返回最新对象的函数(指令 / hook 传 getter 用) */
217
+ function readLiveOptions(options) {
218
+ const value = typeof options === 'function' ? options() : options
219
+ return value && typeof value === 'object' ? value : {}
220
+ }
221
+
222
+ /**
223
+ * @param {HTMLElement} el
224
+ * @param {{
225
+ * resizeCanvas?: boolean,
226
+ * echarts?: boolean | object,
227
+ * echartsLib?: object,
228
+ * onRefresh?: Function
229
+ * } | (() => object)} [options]
230
+ */
231
+ export function createCompensate(el, options = {}) {
232
+ if (!el) throw new Error('[one-screen-scale] compensate element required')
233
+
234
+ const initial = readLiveOptions(options)
235
+ const wantResize = wantsResizeCanvas(initial)
236
+ let raf = 0
237
+ let ro = null
238
+ let mo = null
239
+ let retryTimers = []
240
+ let stopped = false
241
+
242
+ const clearRetries = () => {
243
+ retryTimers.forEach((id) => clearTimeout(id))
244
+ retryTimers = []
245
+ }
246
+
247
+ const refresh = () => {
248
+ if (stopped) return
249
+ const live = readLiveOptions(options)
250
+ const { scale, inverseScale } = applyCompensateTransform(el)
251
+ if (typeof live.onRefresh === 'function') {
252
+ try {
253
+ live.onRefresh({ scale, inverseScale, el })
254
+ } catch {
255
+ /* user callback */
256
+ }
257
+ }
258
+ if (wantsResizeCanvas(live)) {
259
+ const lib = resolveBoundEcharts(live)
260
+ const n = resizeCanvasUnder(el, lib)
261
+ if (n === 0) {
262
+ clearRetries()
263
+ ;[50, 200].forEach((delay) => {
264
+ const id = setTimeout(() => {
265
+ if (stopped) return
266
+ const next = readLiveOptions(options)
267
+ resizeCanvasUnder(el, resolveBoundEcharts(next))
268
+ }, delay)
269
+ retryTimers.push(id)
270
+ })
271
+ }
272
+ }
273
+ }
274
+
275
+ const schedule = () => {
276
+ if (stopped) return
277
+ cancelAnimationFrame(raf)
278
+ raf = requestAnimationFrame(() => {
279
+ raf = 0
280
+ if (stopped) return
281
+ refresh()
282
+ })
283
+ }
284
+
285
+ refresh()
286
+
287
+ if (typeof window !== 'undefined') {
288
+ window.addEventListener(SCALE_CHANGE_EVENT, schedule)
289
+ window.addEventListener('resize', schedule)
290
+ }
291
+
292
+ if (typeof ResizeObserver !== 'undefined') {
293
+ ro = new ResizeObserver(schedule)
294
+ if (el.parentElement) ro.observe(el.parentElement)
295
+ ro.observe(el)
296
+ }
297
+
298
+ if (wantResize && typeof MutationObserver !== 'undefined') {
299
+ mo = new MutationObserver(schedule)
300
+ mo.observe(el, { childList: true, subtree: true })
301
+ }
302
+
303
+ const stop = () => {
304
+ stopped = true
305
+ cancelAnimationFrame(raf)
306
+ clearRetries()
307
+ raf = 0
308
+ if (typeof window !== 'undefined') {
309
+ window.removeEventListener(SCALE_CHANGE_EVENT, schedule)
310
+ window.removeEventListener('resize', schedule)
311
+ }
312
+ ro?.disconnect()
313
+ ro = null
314
+ mo?.disconnect()
315
+ mo = null
316
+ clearCompensateTransform(el)
317
+ }
318
+
319
+ return { refresh, stop }
320
+ }
@@ -3,33 +3,35 @@
3
3
  * useFitScreen(ref, options) → { scale, inverseScale, refresh }
4
4
  */
5
5
  import { ref, onMounted, onUnmounted, watch } from 'vue'
6
- import { createScaler, getInverseScale } from './core.js'
6
+ import { createScaler, SCALE_CHANGE_EVENT } from './core.js'
7
7
 
8
8
  export function useFitScreen(targetRef, options = {}) {
9
9
  const scale = ref(1)
10
10
  const inverseScale = ref(1)
11
11
 
12
12
  let scaler = null
13
- let onResize = null
13
+ let onScale = null
14
14
 
15
15
  function setup(el) {
16
16
  if (!el) return
17
17
  scaler = createScaler(el, options)
18
18
  const s = scaler.start()
19
19
  scale.value = s
20
- inverseScale.value = getInverseScale(el)
20
+ inverseScale.value = s > 0 ? 1 / s : 1
21
21
 
22
- onResize = () => {
23
- scale.value = scaler?.getScale?.() ?? 1
24
- inverseScale.value = getInverseScale(el)
22
+ // 图片加载等导致的重算不经过 window.resize,跟页面缩放事件走
23
+ onScale = (event) => {
24
+ if (event.detail?.el !== el) return
25
+ scale.value = event.detail.scale
26
+ inverseScale.value = event.detail.inverseScale
25
27
  }
26
- window.addEventListener('resize', onResize)
28
+ window.addEventListener(SCALE_CHANGE_EVENT, onScale)
27
29
  }
28
30
 
29
31
  function teardown() {
30
- if (onResize) {
31
- window.removeEventListener('resize', onResize)
32
- onResize = null
32
+ if (onScale) {
33
+ window.removeEventListener(SCALE_CHANGE_EVENT, onScale)
34
+ onScale = null
33
35
  }
34
36
  scaler?.stop()
35
37
  scaler = null
@@ -60,7 +62,7 @@ export function useFitScreen(targetRef, options = {}) {
60
62
  if (!el || !scaler) return
61
63
  const s = scaler.refresh()
62
64
  scale.value = s
63
- inverseScale.value = getInverseScale(el)
65
+ inverseScale.value = s > 0 ? 1 / s : 1
64
66
  }
65
67
 
66
68
  return { scale, inverseScale, refresh }