cytoscape-canvas-underlay 1.3.0 → 1.4.0
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.
- package/README.md +11 -1
- package/package.json +48 -48
- package/src/DrawingOverlay.js +115 -19
- package/src/Minimap.js +21 -1
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@ A [Cytoscape.js](https://js.cytoscape.org) plugin for rendering image/PDF backgr
|
|
|
4
4
|
|
|
5
5
|
- **Image & PDF support** — load `.png`, `.jpg`, `.svg`, or `.pdf` as background
|
|
6
6
|
- **Multiple drawings** — overlay additional drawings at arbitrary world positions
|
|
7
|
-
- **Zoom/pan sync** —
|
|
7
|
+
- **Zoom/pan sync** — viewport-synced redraw, coalesced to one draw per frame (requestAnimationFrame) for smooth pan/zoom
|
|
8
|
+
- **Large-image performance** — optional downscaled proxy during pan/zoom for huge backgrounds, full resolution on idle (`lowQualityOnInteraction`); CSS filters are pre-baked once instead of per frame
|
|
8
9
|
- **Pan clamping** — hard boundary or iOS-style rubber-band with spring-back
|
|
9
10
|
- **Minimap** — DOM-based crisp image rendering, two viewport styles (`dim` / `rect`), auto-hide, full customization
|
|
10
11
|
- **Adaptive PDF quality** — low-quality render during interaction, high-quality on idle
|
|
@@ -273,6 +274,15 @@ const inside = api.isPointInDrawing(world.x, world.y);
|
|
|
273
274
|
| `drawingVisible` | `boolean` | `true` | Initial drawing visibility |
|
|
274
275
|
| `graphVisible` | `boolean` | `true` | Initial graph layer visibility |
|
|
275
276
|
|
|
277
|
+
### Interaction Quality
|
|
278
|
+
|
|
279
|
+
For very large drawings (e.g. tens of megapixels), redrawing the full-resolution image on every pan/zoom frame can cause stutter. Enable `lowQualityOnInteraction` to draw a downscaled proxy during interaction and restore full resolution once it settles.
|
|
280
|
+
|
|
281
|
+
| Option | Type | Default | Description |
|
|
282
|
+
|--------|------|---------|-------------|
|
|
283
|
+
| `lowQualityOnInteraction` | `boolean` | `false` | Draw a downscaled proxy + lower image smoothing during pan/zoom, restoring full quality when interaction stops. Recommended for large background images. |
|
|
284
|
+
| `interactionProxyMax` | `number` | `2560` | Max edge (px) of the downscaled proxy used while interacting. Only sources larger than this are downscaled. |
|
|
285
|
+
|
|
276
286
|
### PDF Quality
|
|
277
287
|
|
|
278
288
|
| Option | Type | Default | Description |
|
package/package.json
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "cytoscape-canvas-underlay",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Cytoscape.js plugin for rendering image/PDF canvas underlay behind graph nodes with synchronized zoom and pan",
|
|
5
|
-
"main": "src/index.js",
|
|
6
|
-
"module": "src/index.js",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"import": "./src/index.js",
|
|
11
|
-
"default": "./src/index.js"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"src",
|
|
16
|
-
"README.md",
|
|
17
|
-
"LICENSE"
|
|
18
|
-
],
|
|
19
|
-
"keywords": [
|
|
20
|
-
"cytoscape",
|
|
21
|
-
"cytoscape-extension",
|
|
22
|
-
"cytoscape-plugin",
|
|
23
|
-
"canvas",
|
|
24
|
-
"underlay",
|
|
25
|
-
"background",
|
|
26
|
-
"drawing",
|
|
27
|
-
"pdf",
|
|
28
|
-
"image",
|
|
29
|
-
"p&id",
|
|
30
|
-
"diagram"
|
|
31
|
-
],
|
|
32
|
-
"author": "",
|
|
33
|
-
"license": "MIT",
|
|
34
|
-
"peerDependencies": {
|
|
35
|
-
"cytoscape": "^3.2.0",
|
|
36
|
-
"pdfjs-dist": "^4.0.0 || ^5.0.0"
|
|
37
|
-
},
|
|
38
|
-
"peerDependenciesMeta": {
|
|
39
|
-
"pdfjs-dist": {
|
|
40
|
-
"optional": true
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
"repository": {
|
|
44
|
-
"type": "git",
|
|
45
|
-
"url": ""
|
|
46
|
-
},
|
|
47
|
-
"homepage": ""
|
|
48
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "cytoscape-canvas-underlay",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Cytoscape.js plugin for rendering image/PDF canvas underlay behind graph nodes with synchronized zoom and pan",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"module": "src/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.js",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"cytoscape",
|
|
21
|
+
"cytoscape-extension",
|
|
22
|
+
"cytoscape-plugin",
|
|
23
|
+
"canvas",
|
|
24
|
+
"underlay",
|
|
25
|
+
"background",
|
|
26
|
+
"drawing",
|
|
27
|
+
"pdf",
|
|
28
|
+
"image",
|
|
29
|
+
"p&id",
|
|
30
|
+
"diagram"
|
|
31
|
+
],
|
|
32
|
+
"author": "",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"cytoscape": "^3.2.0",
|
|
36
|
+
"pdfjs-dist": "^4.0.0 || ^5.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"pdfjs-dist": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": ""
|
|
46
|
+
},
|
|
47
|
+
"homepage": ""
|
|
48
|
+
}
|
package/src/DrawingOverlay.js
CHANGED
|
@@ -35,6 +35,12 @@ const DEFAULTS = {
|
|
|
35
35
|
drawingVisible: true, // Show/hide the background drawing
|
|
36
36
|
graphVisible: true, // Show/hide the cytoscape graph layer
|
|
37
37
|
|
|
38
|
+
// ── Interaction Quality ──
|
|
39
|
+
// pan/zoom 중 큰 도면을 다운스케일 프록시 + 저품질 스무딩으로 그려 끊김을 줄인다(멈추면 풀해상도 복원).
|
|
40
|
+
// 큰 배경 이미지에서 pan/zoom 이 끊길 때 true 로 사용.
|
|
41
|
+
lowQualityOnInteraction: false, // pan/zoom 중 저품질 렌더 사용 여부
|
|
42
|
+
interactionProxyMax: 2560, // 프록시 최대 변(px). 이보다 큰 소스만 축소.
|
|
43
|
+
|
|
38
44
|
// ── PDF Quality ──
|
|
39
45
|
qualityDelay: 100, // ms delay before high-quality PDF re-render
|
|
40
46
|
pdfMinRenderSize: 2048, // Minimum PDF render dimension (px)
|
|
@@ -207,11 +213,18 @@ export class DrawingOverlay {
|
|
|
207
213
|
// Additional drawings
|
|
208
214
|
this._drawings = new Map();
|
|
209
215
|
|
|
216
|
+
// PDF 존재 여부 캐시 (zoom/pan마다 순회 방지)
|
|
217
|
+
this._hasPdfCached = false;
|
|
218
|
+
|
|
210
219
|
// RAF / debounce
|
|
211
220
|
this._rafId = null;
|
|
212
221
|
this._qualityTimer = null;
|
|
213
222
|
this._abortController = null;
|
|
214
223
|
|
|
224
|
+
// pan/zoom 중 저품질 렌더 플래그 + 멈춤 감지 타이머
|
|
225
|
+
this._interacting = false;
|
|
226
|
+
this._interactTimer = null;
|
|
227
|
+
|
|
215
228
|
// Pan clamping re-entry guard
|
|
216
229
|
this._isPanAdjusting = false;
|
|
217
230
|
|
|
@@ -293,17 +306,17 @@ export class DrawingOverlay {
|
|
|
293
306
|
_bindEvents() {
|
|
294
307
|
this._onViewport = () => {
|
|
295
308
|
this._enforceLimits();
|
|
296
|
-
this._draw();
|
|
297
|
-
if (this._minimap) this._minimap.render();
|
|
298
309
|
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
this.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
310
|
+
// pan/zoom 중에는 저품질로 그리고, 멈추면 idle 후 고품질로 한 번 더 그린다.
|
|
311
|
+
this._interacting = true;
|
|
312
|
+
if (this._interactTimer) clearTimeout(this._interactTimer);
|
|
313
|
+
this._interactTimer = setTimeout(() => {
|
|
314
|
+
this._interacting = false;
|
|
315
|
+
this._scheduleRedraw();
|
|
316
|
+
}, 120);
|
|
317
|
+
|
|
318
|
+
// 매 이벤트 동기 _draw() 대신 프레임당 1회로 코얼레싱(_scheduleRedraw 가 PDF 고품질 재렌더도 예약).
|
|
319
|
+
this._scheduleRedraw();
|
|
307
320
|
};
|
|
308
321
|
this._onResize = () => {
|
|
309
322
|
this._setupCanvas();
|
|
@@ -340,6 +353,7 @@ export class DrawingOverlay {
|
|
|
340
353
|
this._cancelSpringBack();
|
|
341
354
|
if (this._rafId) cancelAnimationFrame(this._rafId);
|
|
342
355
|
if (this._qualityTimer) clearTimeout(this._qualityTimer);
|
|
356
|
+
if (this._interactTimer) clearTimeout(this._interactTimer);
|
|
343
357
|
if (this._abortController) this._abortController.abort();
|
|
344
358
|
|
|
345
359
|
// Remove rubber-band drag listeners
|
|
@@ -649,6 +663,7 @@ export class DrawingOverlay {
|
|
|
649
663
|
}
|
|
650
664
|
}
|
|
651
665
|
this._loading = false;
|
|
666
|
+
this._updateHasPdf();
|
|
652
667
|
|
|
653
668
|
if (this.opts.fitOnLoad) this.fit();
|
|
654
669
|
this._draw();
|
|
@@ -891,6 +906,16 @@ export class DrawingOverlay {
|
|
|
891
906
|
Public API: Utility
|
|
892
907
|
═══════════════════════════════════════ */
|
|
893
908
|
|
|
909
|
+
/** PDF 존재 여부 캐시 갱신 */
|
|
910
|
+
_updateHasPdf() {
|
|
911
|
+
this._hasPdfCached = this._main.isPdf;
|
|
912
|
+
if (!this._hasPdfCached) {
|
|
913
|
+
for (const d of this._drawings.values()) {
|
|
914
|
+
if (d.isPdf && d.visible) { this._hasPdfCached = true; break; }
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
894
919
|
/** Force redraw. */
|
|
895
920
|
refresh() {
|
|
896
921
|
this._setupCanvas();
|
|
@@ -1044,6 +1069,7 @@ export class DrawingOverlay {
|
|
|
1044
1069
|
}
|
|
1045
1070
|
|
|
1046
1071
|
this._drawings.set(id, state);
|
|
1072
|
+
this._updateHasPdf();
|
|
1047
1073
|
this._draw();
|
|
1048
1074
|
this._emit('drawingAdd', id, { x: state.x, y: state.y, width: state.width, height: state.height });
|
|
1049
1075
|
}
|
|
@@ -1059,6 +1085,7 @@ export class DrawingOverlay {
|
|
|
1059
1085
|
const state = this._drawings.get(id);
|
|
1060
1086
|
if (!state) return;
|
|
1061
1087
|
state.visible = visible;
|
|
1088
|
+
if (state.isPdf) this._updateHasPdf();
|
|
1062
1089
|
this._draw();
|
|
1063
1090
|
}
|
|
1064
1091
|
|
|
@@ -1069,12 +1096,14 @@ export class DrawingOverlay {
|
|
|
1069
1096
|
|
|
1070
1097
|
removeDrawing(id) {
|
|
1071
1098
|
this._drawings.delete(id);
|
|
1099
|
+
this._updateHasPdf();
|
|
1072
1100
|
this._draw();
|
|
1073
1101
|
this._emit('drawingRemove', id);
|
|
1074
1102
|
}
|
|
1075
1103
|
|
|
1076
1104
|
clearDrawings() {
|
|
1077
1105
|
this._drawings.clear();
|
|
1106
|
+
this._updateHasPdf();
|
|
1078
1107
|
this._draw();
|
|
1079
1108
|
}
|
|
1080
1109
|
|
|
@@ -1146,6 +1175,73 @@ export class DrawingOverlay {
|
|
|
1146
1175
|
}
|
|
1147
1176
|
}
|
|
1148
1177
|
|
|
1178
|
+
/**
|
|
1179
|
+
* 필터(invert/brightness/contrast/...)를 적용한 소스를 반환.
|
|
1180
|
+
* 매 프레임 ctx.filter 로 큰 도면을 필터링하면 매우 느리므로, 소스(이미지/PDF캔버스)를
|
|
1181
|
+
* 오프스크린 캔버스에 한 번만 구워두고 필터 문자열/소스가 바뀔 때만 재생성한다.
|
|
1182
|
+
* 필터가 없으면(none) 원본 소스를 그대로 반환.
|
|
1183
|
+
*/
|
|
1184
|
+
_filtered(holder, source, filterStr) {
|
|
1185
|
+
if (!filterStr || filterStr === 'none') return source;
|
|
1186
|
+
const w = source.naturalWidth || source.width;
|
|
1187
|
+
const h = source.naturalHeight || source.height;
|
|
1188
|
+
if (!w || !h) return source;
|
|
1189
|
+
|
|
1190
|
+
const cache = holder._filteredCache;
|
|
1191
|
+
if (cache && cache.src === source && cache.filter === filterStr && cache.w === w && cache.h === h) {
|
|
1192
|
+
return cache.canvas;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const canvas = (cache && cache.canvas) || document.createElement('canvas');
|
|
1196
|
+
if (canvas.width !== w) canvas.width = w;
|
|
1197
|
+
if (canvas.height !== h) canvas.height = h;
|
|
1198
|
+
const cx = canvas.getContext('2d');
|
|
1199
|
+
cx.clearRect(0, 0, w, h);
|
|
1200
|
+
cx.filter = filterStr;
|
|
1201
|
+
cx.imageSmoothingEnabled = true;
|
|
1202
|
+
cx.imageSmoothingQuality = 'high';
|
|
1203
|
+
cx.drawImage(source, 0, 0, w, h);
|
|
1204
|
+
cx.filter = 'none';
|
|
1205
|
+
|
|
1206
|
+
holder._filteredCache = { src: source, filter: filterStr, w, h, canvas };
|
|
1207
|
+
return canvas;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* 큰 소스(예: 10000×7074 도면)를 매 프레임 통째로 drawImage 하면 pan/zoom 이 끊긴다.
|
|
1212
|
+
* → 최대 변(PROXY_MAX) 으로 축소한 캔버스를 캐시해 인터랙션 중에만 사용. 소스가 작으면 원본 그대로.
|
|
1213
|
+
*/
|
|
1214
|
+
_proxy(holder, src) {
|
|
1215
|
+
const PROXY_MAX = this.opts.interactionProxyMax || 2560;
|
|
1216
|
+
const sw = src.naturalWidth || src.width;
|
|
1217
|
+
const sh = src.naturalHeight || src.height;
|
|
1218
|
+
if (!sw || !sh || (sw <= PROXY_MAX && sh <= PROXY_MAX)) return src;
|
|
1219
|
+
|
|
1220
|
+
const scale = PROXY_MAX / Math.max(sw, sh);
|
|
1221
|
+
const pw = Math.max(1, Math.round(sw * scale));
|
|
1222
|
+
const ph = Math.max(1, Math.round(sh * scale));
|
|
1223
|
+
|
|
1224
|
+
const cache = holder._proxyCache;
|
|
1225
|
+
if (cache && cache.src === src && cache.w === pw && cache.h === ph) return cache.canvas;
|
|
1226
|
+
|
|
1227
|
+
const canvas = (cache && cache.canvas) || document.createElement('canvas');
|
|
1228
|
+
canvas.width = pw; canvas.height = ph;
|
|
1229
|
+
const cx = canvas.getContext('2d');
|
|
1230
|
+
cx.clearRect(0, 0, pw, ph);
|
|
1231
|
+
cx.imageSmoothingEnabled = true;
|
|
1232
|
+
cx.imageSmoothingQuality = 'high';
|
|
1233
|
+
cx.drawImage(src, 0, 0, pw, ph);
|
|
1234
|
+
|
|
1235
|
+
holder._proxyCache = { src, w: pw, h: ph, canvas };
|
|
1236
|
+
return canvas;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/** _draw 가 사용할 소스: 필터 적용본. 옵션 켜진 경우 인터랙션 중에는 축소 프록시로 대체(멈추면 풀해상도). */
|
|
1240
|
+
_displaySource(holder, source, filterStr) {
|
|
1241
|
+
const full = this._filtered(holder, source, filterStr);
|
|
1242
|
+
return (this._interacting && this.opts.lowQualityOnInteraction) ? this._proxy(holder, full) : full;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1149
1245
|
/** Draw an image/canvas with optional rotation around its center. */
|
|
1150
1246
|
_drawRotated(ctx, source, x, y, w, h, rotation) {
|
|
1151
1247
|
if (!rotation) {
|
|
@@ -1185,9 +1281,10 @@ export class DrawingOverlay {
|
|
|
1185
1281
|
const hasAdditional = this._drawings.size > 0;
|
|
1186
1282
|
if (!hasMain && !hasAdditional) return;
|
|
1187
1283
|
|
|
1188
|
-
//
|
|
1284
|
+
// Image smoothing — 옵션 켜진 경우에만 pan/zoom 중 'low', 그 외엔 'high'.
|
|
1285
|
+
const lowQ = this._interacting && this.opts.lowQualityOnInteraction;
|
|
1189
1286
|
ctx.imageSmoothingEnabled = true;
|
|
1190
|
-
ctx.imageSmoothingQuality = 'high';
|
|
1287
|
+
ctx.imageSmoothingQuality = lowQ ? 'low' : 'high';
|
|
1191
1288
|
|
|
1192
1289
|
// Apply cytoscape transform
|
|
1193
1290
|
ctx.setTransform(
|
|
@@ -1197,18 +1294,18 @@ export class DrawingOverlay {
|
|
|
1197
1294
|
Math.round(pan.y * dpr)
|
|
1198
1295
|
);
|
|
1199
1296
|
|
|
1200
|
-
// CSS
|
|
1201
|
-
|
|
1297
|
+
// CSS 필터는 매 프레임 ctx.filter 로 적용하지 않고, 미리 구워둔 캔버스(_filtered)를 사용.
|
|
1298
|
+
const filterStr = buildFilterString(this.opts);
|
|
1202
1299
|
|
|
1203
1300
|
// Draw main source
|
|
1204
1301
|
if (hasMain) {
|
|
1205
1302
|
ctx.globalAlpha = this.opts.opacity;
|
|
1206
1303
|
const mainRot = this.opts.rotation || 0;
|
|
1207
1304
|
if (this._main.img) {
|
|
1208
|
-
this._drawRotated(ctx, this._main.img, 0, 0, this._main.w, this._main.h, mainRot);
|
|
1305
|
+
this._drawRotated(ctx, this._displaySource(this._main, this._main.img, filterStr), 0, 0, this._main.w, this._main.h, mainRot);
|
|
1209
1306
|
} else if (this._main.pdfCanvas && this._main.pdfClip) {
|
|
1210
1307
|
const c = this._main.pdfClip;
|
|
1211
|
-
this._drawRotated(ctx, this._main.pdfCanvas, c.x, c.y, c.w, c.h, mainRot);
|
|
1308
|
+
this._drawRotated(ctx, this._displaySource(this._main, this._main.pdfCanvas, filterStr), c.x, c.y, c.w, c.h, mainRot);
|
|
1212
1309
|
}
|
|
1213
1310
|
}
|
|
1214
1311
|
|
|
@@ -1218,15 +1315,14 @@ export class DrawingOverlay {
|
|
|
1218
1315
|
ctx.globalAlpha = state.opacity * this.opts.opacity;
|
|
1219
1316
|
const rot = state.rotation || 0;
|
|
1220
1317
|
if (state.img) {
|
|
1221
|
-
this._drawRotated(ctx, state.img, state.x, state.y, state.width, state.height, rot);
|
|
1318
|
+
this._drawRotated(ctx, this._displaySource(state, state.img, filterStr), state.x, state.y, state.width, state.height, rot);
|
|
1222
1319
|
} else if (state.pdfCanvas && state.pdfClip) {
|
|
1223
1320
|
const c = state.pdfClip;
|
|
1224
|
-
this._drawRotated(ctx, state.pdfCanvas, state.x + c.x, state.y + c.y, c.w, c.h, rot);
|
|
1321
|
+
this._drawRotated(ctx, this._displaySource(state, state.pdfCanvas, filterStr), state.x + c.x, state.y + c.y, c.w, c.h, rot);
|
|
1225
1322
|
}
|
|
1226
1323
|
}
|
|
1227
1324
|
|
|
1228
1325
|
// Reset
|
|
1229
1326
|
ctx.globalAlpha = 1;
|
|
1230
|
-
ctx.filter = 'none';
|
|
1231
1327
|
}
|
|
1232
1328
|
}
|
package/src/Minimap.js
CHANGED
|
@@ -183,7 +183,27 @@ export class Minimap {
|
|
|
183
183
|
|
|
184
184
|
if (main.img && main.img.src) {
|
|
185
185
|
if (this._lastImgSrc !== main.img.src) {
|
|
186
|
-
|
|
186
|
+
// 큰 원본(예: 70MP 도면)을 CSS 배경으로 통째로 디코드하면 메모리·디코드 비용이 큼.
|
|
187
|
+
// → 작은 썸네일 1장으로 축소해 사용(미니맵 화질엔 충분).
|
|
188
|
+
let bg = main.img.src;
|
|
189
|
+
try {
|
|
190
|
+
const THUMB_MAX = 256;
|
|
191
|
+
const iw = main.img.naturalWidth || main.w;
|
|
192
|
+
const ih = main.img.naturalHeight || main.h;
|
|
193
|
+
const s = Math.min(1, THUMB_MAX / Math.max(iw, ih));
|
|
194
|
+
if (s < 1) {
|
|
195
|
+
const tw = Math.max(1, Math.round(iw * s));
|
|
196
|
+
const th = Math.max(1, Math.round(ih * s));
|
|
197
|
+
const c = document.createElement('canvas');
|
|
198
|
+
c.width = tw; c.height = th;
|
|
199
|
+
const cx = c.getContext('2d');
|
|
200
|
+
cx.imageSmoothingEnabled = true;
|
|
201
|
+
cx.imageSmoothingQuality = 'high';
|
|
202
|
+
cx.drawImage(main.img, 0, 0, tw, th);
|
|
203
|
+
bg = c.toDataURL('image/png');
|
|
204
|
+
}
|
|
205
|
+
} catch (_) { /* cross-origin tainted 등 → 원본 src 폴백 */ }
|
|
206
|
+
this._imgDiv.style.backgroundImage = `url(${bg})`;
|
|
187
207
|
this._lastImgSrc = main.img.src;
|
|
188
208
|
if (this._blobUrl) { URL.revokeObjectURL(this._blobUrl); this._blobUrl = null; }
|
|
189
209
|
}
|