humanmap-vas 1.0.23 → 1.0.24
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/humanmap-vas-standalone.js +199 -5
- package/package.json +1 -1
@@ -7,7 +7,7 @@
|
|
7
7
|
.hm-center { text-align:center; font-weight:600; color:#1f2937; }
|
8
8
|
.hm-toolbar select, .hm-toolbar button { appearance:none; border:1px solid #d1d5db; border-radius:10px; padding:6px 10px; background:#fff; cursor:pointer; font-weight:500; }
|
9
9
|
.hm-canvas-wrap { position:relative; width:100%; height:600px; margin:auto; aspect-ratio: 2/3; background:#fff; }
|
10
|
-
svg.hm-svg { position:absolute; inset:0; width:100%; height:100%; }
|
10
|
+
svg.hm-svg { position:absolute; inset:0; width:100%; height:100%; margin: auto; }
|
11
11
|
.zone { fill: rgba(31,41,55,0); transition: fill 120ms ease; cursor: pointer; }
|
12
12
|
.zone:hover { fill: rgba(31,41,55,0.22); }
|
13
13
|
.zone.readonly { cursor: default; }
|
@@ -18,6 +18,19 @@
|
|
18
18
|
.hm-print-btn { position: absolute; top: 10px; right: 10px; background: rgba(17,24,39,0.85); color: #f9fafb; border: none; border-radius: 8px; cursor: pointer; font-size: 18px; padding: 6px 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); transition: opacity 0.25s ease, background 0.2s ease; opacity: 0; pointer-events: none; }
|
19
19
|
.hm-canvas-wrap:hover .hm-print-btn { opacity: 1; pointer-events: auto; }
|
20
20
|
.hm-print-btn:hover { background: rgba(37,99,235,0.9); }
|
21
|
+
.hm-zoom-float { position: absolute; bottom: 10px; right: 10px; background: rgba(31,41,55,0.85); color: #fff; border: none; border-radius: 50%; width: 42px; height: 42px; font-size: 20px; line-height: 1; cursor: pointer; box-shadow: 0 3px 8px rgba(0,0,0,0.3); transition: transform 0.2s ease, background 0.2s ease; z-index: 20; }
|
22
|
+
.hm-zoom-float:hover { transform: scale(1.08); background: rgba(31,41,55,1); }
|
23
|
+
.hm-zoom-modal { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.85); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; }
|
24
|
+
.hm-zoom-modal.active { opacity: 1; pointer-events: auto; }
|
25
|
+
.hm-zoom-inner { position: relative; width: 95vw; height: 90vh; background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; }
|
26
|
+
.hm-zoom-content { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #fff; overflow: auto; }
|
27
|
+
.hm-zoom-close { position: absolute; top: 12px; right: 12px; border: none; background: rgba(0, 0, 0, 0.75); color: #fff; border-radius: 50%; width: 42px; height: 42px; font-size: 24px; cursor: pointer; z-index: 10000; line-height: 1; }
|
28
|
+
.hm-zoom-close:hover { background: rgba(0, 0, 0, 0.9); }
|
29
|
+
.hm-zoom-modal svg { width: auto; height: auto; max-width: 100%; max-height: 100%; display: block; transition: transform 0.25s ease; }
|
30
|
+
.hm-zoom-inner { transform: scale(0.95); opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; }
|
31
|
+
.hm-zoom-modal.active .hm-zoom-inner { transform: scale(1); opacity: 1; }
|
32
|
+
.hm-zoom-hint { position: absolute; bottom: 10px; right: 20px; color: rgba(0,0,0,0.4); font-size: 14px; font-family: system-ui, sans-serif; background: rgba(255,255,255,0.7); padding: 4px 10px; border-radius: 6px; pointer-events: none; user-select: none; transition: opacity 1s ease 2s; opacity: 1; }
|
33
|
+
|
21
34
|
`;
|
22
35
|
|
23
36
|
// ───────────────────────────────────────────────────────────────────────────
|
@@ -175,6 +188,13 @@
|
|
175
188
|
this.dispatchEvent(new CustomEvent('human-map-vas:readonly-change', {
|
176
189
|
detail: { readOnly: this._readOnly }
|
177
190
|
}));
|
191
|
+
|
192
|
+
window.addEventListener('resize', () => {
|
193
|
+
if (this._view === 'all') {
|
194
|
+
clearTimeout(this._resizeTimer);
|
195
|
+
this._resizeTimer = setTimeout(() => this._renderAllViews(), 150);
|
196
|
+
}
|
197
|
+
});
|
178
198
|
}
|
179
199
|
|
180
200
|
static get observedAttributes() { return ['view', 'img-root', 'read-only']; }
|
@@ -185,6 +205,11 @@
|
|
185
205
|
if (name === 'view') {
|
186
206
|
this._view = newValue;
|
187
207
|
if (this._root) this._renderCanvas();
|
208
|
+
|
209
|
+
this.dispatchEvent(new CustomEvent('human-map-vas:view-changed', {
|
210
|
+
detail: { view: newValue }
|
211
|
+
}));
|
212
|
+
|
188
213
|
return;
|
189
214
|
}
|
190
215
|
|
@@ -312,6 +337,7 @@
|
|
312
337
|
<g id="zones"></g>
|
313
338
|
</svg>
|
314
339
|
<button id="printBtn" title="Imprimir vista" class="hm-print-btn">🖨️</button>
|
340
|
+
<button id="zoom-float" class="hm-zoom-float" title="Ampliar vista">⤢</button>
|
315
341
|
</div>`;
|
316
342
|
this.shadowRoot.append(style,this._root);
|
317
343
|
|
@@ -338,6 +364,8 @@
|
|
338
364
|
this._printCanvasOnly(); // ⬅️ imprime solo el área actual
|
339
365
|
}, 120);
|
340
366
|
});
|
367
|
+
this._els.zoomFloat = this.shadowRoot.getElementById('zoom-float');
|
368
|
+
this._els.zoomFloat.addEventListener('click', () => this._openPreviewModal());
|
341
369
|
|
342
370
|
}
|
343
371
|
|
@@ -368,6 +396,10 @@
|
|
368
396
|
}
|
369
397
|
}
|
370
398
|
|
399
|
+
if (this._els.zoomFloat) {
|
400
|
+
this._els.zoomFloat.style.display = this._readOnly ? 'block' : 'none';
|
401
|
+
}
|
402
|
+
|
371
403
|
// Limpia cualquier render anterior (modo all o vista única)
|
372
404
|
if (this._els && this._els.svg) {
|
373
405
|
this._els.svg.innerHTML = `
|
@@ -411,9 +443,14 @@
|
|
411
443
|
const svg = this._els.svg;
|
412
444
|
svg.innerHTML = ''; // limpiar
|
413
445
|
|
414
|
-
//
|
415
|
-
const
|
416
|
-
const
|
446
|
+
// Permitir configurar columnas dinámicas (por atributo) o calcular automáticamente
|
447
|
+
const defaultCols = 4;
|
448
|
+
const attrCols = parseInt(this.getAttribute('columns'), 10);
|
449
|
+
let cols = !isNaN(attrCols) && attrCols > 0 ? attrCols : defaultCols;
|
450
|
+
|
451
|
+
const gap = 80;
|
452
|
+
const cellW = 560, cellH = 720;
|
453
|
+
|
417
454
|
const gridW = cols * (cellW + gap);
|
418
455
|
const gridH = Math.ceil(VIEWS.length / cols) * (cellH + gap);
|
419
456
|
|
@@ -429,7 +466,9 @@
|
|
429
466
|
const gy = row * (cellH + gap);
|
430
467
|
|
431
468
|
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
432
|
-
|
469
|
+
// Escala dinámica: reduce más si hay muchas columnas
|
470
|
+
const scale = Math.max(0.3, 0.9 - cols * 0.1);
|
471
|
+
g.setAttribute('transform', `translate(${gx}, ${gy}) scale(${scale})`);
|
433
472
|
|
434
473
|
// Fondo (imagen)
|
435
474
|
const img = document.createElementNS('http://www.w3.org/2000/svg', 'image');
|
@@ -511,6 +550,11 @@
|
|
511
550
|
|
512
551
|
gRoot.appendChild(g);
|
513
552
|
});
|
553
|
+
|
554
|
+
if (this._els.zoomFloat) {
|
555
|
+
this._els.zoomFloat.style.display = this._readOnly ? 'block' : 'none';
|
556
|
+
}
|
557
|
+
|
514
558
|
}
|
515
559
|
|
516
560
|
_renderZones(){
|
@@ -665,6 +709,156 @@
|
|
665
709
|
});
|
666
710
|
}
|
667
711
|
|
712
|
+
_openPreviewModal() {
|
713
|
+
if (!this._els || !this._els.svg) return;
|
714
|
+
|
715
|
+
// Clonar el SVG actual
|
716
|
+
const clone = this._els.svg.cloneNode(true);
|
717
|
+
|
718
|
+
// Asegurar que las imágenes se vean correctamente (usar rutas absolutas)
|
719
|
+
clone.querySelectorAll('image').forEach(img => {
|
720
|
+
const href = img.getAttribute('href') || img.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
|
721
|
+
if (href) {
|
722
|
+
const a = document.createElement('a');
|
723
|
+
a.href = href;
|
724
|
+
const abs = a.href;
|
725
|
+
img.setAttribute('href', abs);
|
726
|
+
img.setAttributeNS('http://www.w3.org/1999/xlink', 'href', abs);
|
727
|
+
}
|
728
|
+
});
|
729
|
+
|
730
|
+
// Ajustar estilos dentro del SVG
|
731
|
+
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
732
|
+
style.textContent = `
|
733
|
+
.zone { fill: rgba(31,41,55,0); cursor: default; transition: fill 120ms ease; }
|
734
|
+
.zone.selected { fill: rgba(31,41,55,0.36); }
|
735
|
+
.label { fill: #0a0a0a; font-size: 42px; font-weight: 800;
|
736
|
+
text-anchor: middle; dominant-baseline: middle;
|
737
|
+
pointer-events: none; user-select: none; }
|
738
|
+
.hm-all-label {
|
739
|
+
font-family: system-ui, sans-serif;
|
740
|
+
font-size: 48px;
|
741
|
+
font-weight: 800;
|
742
|
+
fill: #111827;
|
743
|
+
text-anchor: middle;
|
744
|
+
dominant-baseline: middle;
|
745
|
+
}
|
746
|
+
`;
|
747
|
+
clone.insertBefore(style, clone.firstChild);
|
748
|
+
|
749
|
+
// Crear el modal
|
750
|
+
const modal = document.createElement('div');
|
751
|
+
modal.className = 'hm-zoom-modal';
|
752
|
+
modal.innerHTML = `
|
753
|
+
<div class="hm-zoom-inner">
|
754
|
+
<button class="hm-zoom-close" title="Cerrar">×</button>
|
755
|
+
<div class="hm-zoom-content"></div>
|
756
|
+
<div class="hm-zoom-hint">🖱️ Usa la rueda para hacer zoom y arrastra para mover. Presiona <strong>Esc</strong> para cerrar</div>
|
757
|
+
</div>
|
758
|
+
`;
|
759
|
+
|
760
|
+
// Insertar el SVG clonado dentro del modal
|
761
|
+
modal.querySelector('.hm-zoom-content').appendChild(clone);
|
762
|
+
this.shadowRoot.appendChild(modal);
|
763
|
+
|
764
|
+
// Forzar render y animación
|
765
|
+
requestAnimationFrame(() => modal.classList.add('active'));
|
766
|
+
// 🔒 Bloquear scroll del fondo
|
767
|
+
document.body.style.overflow = 'hidden';
|
768
|
+
|
769
|
+
// Cerrar modal
|
770
|
+
const close = () => {
|
771
|
+
modal.classList.remove('active');
|
772
|
+
|
773
|
+
// 🔓 Restaurar scroll del fondo
|
774
|
+
document.body.style.overflow = '';
|
775
|
+
|
776
|
+
setTimeout(() => modal.remove(), 300);
|
777
|
+
|
778
|
+
// Quitar listener del teclado al cerrar
|
779
|
+
document.removeEventListener('keydown', onKey);
|
780
|
+
};
|
781
|
+
|
782
|
+
modal.querySelector('.hm-zoom-close').addEventListener('click', close);
|
783
|
+
modal.addEventListener('click', e => {
|
784
|
+
if (e.target === modal) close();
|
785
|
+
});
|
786
|
+
|
787
|
+
// 🔑 Cerrar con tecla Escape
|
788
|
+
const onKey = e => {
|
789
|
+
if (e.key === 'Escape') close();
|
790
|
+
};
|
791
|
+
document.addEventListener('keydown', onKey);
|
792
|
+
|
793
|
+
// ───────────────────────────────
|
794
|
+
// 🔍 Zoom + Pan interactivo
|
795
|
+
// ───────────────────────────────
|
796
|
+
const content = modal.querySelector('.hm-zoom-content');
|
797
|
+
let scale = 1;
|
798
|
+
let translateX = 0, translateY = 0;
|
799
|
+
let isPanning = false;
|
800
|
+
let startX = 0, startY = 0;
|
801
|
+
|
802
|
+
content.addEventListener('wheel', e => {
|
803
|
+
e.preventDefault();
|
804
|
+
|
805
|
+
const rect = clone.getBoundingClientRect();
|
806
|
+
const offsetX = e.clientX - rect.left;
|
807
|
+
const offsetY = e.clientY - rect.top;
|
808
|
+
|
809
|
+
const delta = e.deltaY < 0 ? 0.1 : -0.1;
|
810
|
+
const newScale = Math.min(Math.max(scale + delta, 0.5), 3);
|
811
|
+
|
812
|
+
// Mantener el punto bajo el puntero "anclado"
|
813
|
+
const dx = offsetX - (offsetX / scale) * newScale;
|
814
|
+
const dy = offsetY - (offsetY / scale) * newScale;
|
815
|
+
translateX += dx;
|
816
|
+
translateY += dy;
|
817
|
+
|
818
|
+
clone.style.transform = `translate(${translateX}px, ${translateY}px) scale(${newScale})`;
|
819
|
+
clone.style.transformOrigin = '0 0';
|
820
|
+
scale = newScale;
|
821
|
+
});
|
822
|
+
|
823
|
+
// ───────────────────────────────
|
824
|
+
// 🖱️ Arrastrar (Pan)
|
825
|
+
// ───────────────────────────────
|
826
|
+
content.addEventListener('mousedown', e => {
|
827
|
+
e.preventDefault();
|
828
|
+
isPanning = true;
|
829
|
+
startX = e.clientX - translateX;
|
830
|
+
startY = e.clientY - translateY;
|
831
|
+
content.style.cursor = 'grabbing';
|
832
|
+
});
|
833
|
+
|
834
|
+
content.addEventListener('mousemove', e => {
|
835
|
+
if (!isPanning) return;
|
836
|
+
translateX = e.clientX - startX;
|
837
|
+
translateY = e.clientY - startY;
|
838
|
+
clone.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
839
|
+
});
|
840
|
+
|
841
|
+
content.addEventListener('mouseup', () => {
|
842
|
+
isPanning = false;
|
843
|
+
content.style.cursor = 'default';
|
844
|
+
});
|
845
|
+
|
846
|
+
content.addEventListener('mouseleave', () => {
|
847
|
+
isPanning = false;
|
848
|
+
content.style.cursor = 'default';
|
849
|
+
});
|
850
|
+
|
851
|
+
// ───────────────────────────────
|
852
|
+
// 🔄 Doble clic para resetear vista
|
853
|
+
// ───────────────────────────────
|
854
|
+
content.addEventListener('dblclick', () => {
|
855
|
+
scale = 1;
|
856
|
+
translateX = 0;
|
857
|
+
translateY = 0;
|
858
|
+
clone.style.transform = '';
|
859
|
+
});
|
860
|
+
|
861
|
+
}
|
668
862
|
|
669
863
|
_emit(){this.dispatchEvent(new CustomEvent('human-map-vas:select',{detail:{selected:this.getSelected()}}));}
|
670
864
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "humanmap-vas",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.24",
|
4
4
|
"description": "**HumanMap VAS** es una librería web que permite graficar el cuerpo humano con vistas anatómicas interactivas para identificar zonas según el sistema VAS. Desarrollada como *Web Component standalone*, puede integrarse fácilmente en proyectos **HTML**, **Django**, o **Vue.js**.",
|
5
5
|
"main": "humanmap-vas-standalone.js",
|
6
6
|
"files": [
|