{"id":5523,"date":"2026-03-04T22:56:25","date_gmt":"2026-03-05T01:56:25","guid":{"rendered":"https:\/\/fisica2.fica.unsl.edu.ar\/?page_id=5523"},"modified":"2026-03-19T14:41:48","modified_gmt":"2026-03-19T17:41:48","slug":"ley_de_coulomb","status":"publish","type":"page","link":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/","title":{"rendered":"Ley_de_coulomb"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"5523\" class=\"elementor elementor-5523\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-fec4194 elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"fec4194\" data-element_type=\"section\" data-e-type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-7c1c0ee\" data-id=\"7c1c0ee\" data-element_type=\"column\" data-e-type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-90c2305 elementor-widget elementor-widget-html\" data-id=\"90c2305\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\r\n<html lang=\"es\">\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n    <title>Ley de Coulomb - Simulacion 3D Interactiva<\/title>\r\n    <script src=\"https:\/\/cdn.tailwindcss.com\"><\/script>\r\n    <link href=\"https:\/\/fonts.googleapis.com\/css2?family=JetBrains+Mono:wght@400;600&family=Space+Grotesk:wght@400;500;700&display=swap\" rel=\"stylesheet\">\r\n    <style>\r\n        :root {\r\n            --bg-primary: #0a0e17;\r\n            --bg-secondary: #111827;\r\n            --bg-tertiary: #1a2332;\r\n            --text-primary: #e8edf5;\r\n            --text-secondary: #94a3b8;\r\n            --accent-cyan: #22d3ee;\r\n            --accent-red: #ef4444;\r\n            --accent-blue: #3b82f6;\r\n            --accent-green: #10b981;\r\n            --accent-yellow: #f59e0b;\r\n            --accent-orange: #f97316;\r\n            --border-color: #2d3748;\r\n        }\r\n        \r\n        * { margin: 0; padding: 0; box-sizing: border-box; }\r\n        \r\n        body {\r\n            font-family: 'Space Grotesk', sans-serif;\r\n            background: var(--bg-primary);\r\n            color: var(--text-primary);\r\n            overflow: hidden;\r\n        }\r\n        \r\n        .mono { font-family: 'JetBrains Mono', monospace; }\r\n        \r\n        #canvas-container {\r\n            position: fixed;\r\n            top: 0; left: 0;\r\n            width: 100%; height: 100%;\r\n            z-index: 1;\r\n        }\r\n        \r\n        .panel {\r\n            background: linear-gradient(145deg, rgba(17, 24, 39, 0.95), rgba(10, 14, 23, 0.98));\r\n            border: 1px solid var(--border-color);\r\n            backdrop-filter: blur(12px);\r\n            border-radius: 12px;\r\n        }\r\n        \r\n        .control-panel {\r\n            position: fixed; top: 20px; left: 20px;\r\n            width: 340px; max-height: calc(100vh - 40px);\r\n            overflow-y: auto; z-index: 100; padding: 20px;\r\n        }\r\n        \r\n        .control-panel::-webkit-scrollbar { width: 6px; }\r\n        .control-panel::-webkit-scrollbar-track { background: var(--bg-tertiary); border-radius: 3px; }\r\n        .control-panel::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; }\r\n        \r\n        .info-panel {\r\n            position: fixed; top: 20px; right: 20px;\r\n            width: 380px; max-height: calc(100vh - 40px);\r\n            overflow-y: auto; z-index: 100; padding: 20px;\r\n        }\r\n        \r\n        .info-panel::-webkit-scrollbar { width: 6px; }\r\n        .info-panel::-webkit-scrollbar-track { background: var(--bg-tertiary); border-radius: 3px; }\r\n        .info-panel::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; }\r\n        \r\n        .branding-header { display: flex; align-items: center; gap: 12px; padding-bottom: 16px; margin-bottom: 16px; border-bottom: 1px solid var(--border-color); }\r\n        .branding-logo-small { height: 28px; width: auto; object-fit: contain; background: rgba(255, 255, 255, 0.95); padding: 2px 4px; border-radius: 4px; }\r\n        .branding-text-box { display: flex; flex-direction: column; line-height: 1.2; }\r\n        .branding-inst { font-weight: 700; font-size: 15px; color: var(--text-primary); letter-spacing: 0.5px; }\r\n        .branding-year { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--accent-cyan); margin-top: 2px; }\r\n        \r\n        .section-title {\r\n            font-size: 11px; font-weight: 600; text-transform: uppercase;\r\n            letter-spacing: 1.5px; color: var(--text-secondary);\r\n            margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);\r\n        }\r\n        \r\n        .charge-control { background: var(--bg-tertiary); border-radius: 10px; padding: 14px; margin-bottom: 12px; border: 1px solid transparent; transition: all 0.3s ease; }\r\n        .charge-control:hover { border-color: var(--border-color); }\r\n        .charge-control.positive { border-left: 3px solid var(--accent-red); }\r\n        .charge-control.negative { border-left: 3px solid var(--accent-blue); }\r\n        \r\n        .charge-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }\r\n        .charge-label { font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 8px; }\r\n        .charge-indicator { width: 12px; height: 12px; border-radius: 50%; }\r\n        .charge-indicator.positive { background: var(--accent-red); box-shadow: 0 0 8px var(--accent-red); }\r\n        .charge-indicator.negative { background: var(--accent-blue); box-shadow: 0 0 8px var(--accent-blue); }\r\n        .charge-value { font-family: 'JetBrains Mono', monospace; font-size: 13px; color: var(--accent-cyan); }\r\n        \r\n        \/* Estilos para las coordenadas *\/\r\n        .coords-display {\r\n            display: flex; gap: 8px; margin-bottom: 10px; margin-top: 4px;\r\n        }\r\n        .coord-box {\r\n            flex: 1; background: rgba(0,0,0,0.2); padding: 4px; border-radius: 4px; text-align: center;\r\n            font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--accent-yellow);\r\n        }\r\n        .coord-label { color: var(--text-secondary); font-size: 9px; display: block; margin-bottom: 2px; }\r\n        \r\n        .slider-container { margin-top: 8px; }\r\n        .slider-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; display: flex; justify-content: space-between; }\r\n        \r\n        input[type=\"range\"] {\r\n            width: 100%; height: 6px; border-radius: 3px;\r\n            background: var(--bg-primary); outline: none;\r\n            -webkit-appearance: none; cursor: pointer;\r\n        }\r\n        input[type=\"range\"]::-webkit-slider-thumb {\r\n            -webkit-appearance: none; width: 16px; height: 16px;\r\n            border-radius: 50%; background: var(--accent-cyan);\r\n            cursor: pointer; transition: transform 0.2s ease;\r\n        }\r\n        input[type=\"range\"]::-webkit-slider-thumb:hover { transform: scale(1.2); }\r\n        \r\n        select {\r\n            width: 100%; padding: 10px 14px; background: var(--bg-tertiary);\r\n            border: 1px solid var(--border-color); border-radius: 8px;\r\n            color: var(--text-primary); font-family: 'Space Grotesk', sans-serif;\r\n            font-size: 14px; cursor: pointer; outline: none; transition: all 0.3s ease;\r\n        }\r\n        select:hover { border-color: var(--accent-cyan); }\r\n        select:focus { border-color: var(--accent-cyan); box-shadow: 0 0 0 2px rgba(34, 211, 238, 0.2); }\r\n        \r\n        .force-card { background: var(--bg-tertiary); border-radius: 10px; padding: 14px; margin-bottom: 12px; }\r\n        .force-card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }\r\n        .force-card-title { font-weight: 600; font-size: 14px; }\r\n        \r\n        .force-vector-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 10px; margin: 4px 0; background: rgba(0,0,0,0.2); border-radius: 6px; font-size: 12px; }\r\n        .force-type-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; }\r\n        .force-type-badge.attraction { background: rgba(16, 185, 129, 0.2); color: var(--accent-green); }\r\n        .force-type-badge.repulsion { background: rgba(249, 115, 22, 0.2); color: var(--accent-orange); }\r\n        \r\n        .net-force-card { background: linear-gradient(135deg, rgba(34, 211, 238, 0.15), rgba(34, 211, 238, 0.05)); border: 1px solid rgba(34, 211, 238, 0.3); }\r\n        .net-force-value { font-size: 18px; font-weight: 700; color: var(--accent-cyan); font-family: 'JetBrains Mono', monospace; }\r\n        \r\n        .vector-components { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 10px; }\r\n        .vector-comp { background: rgba(0,0,0,0.3); border-radius: 6px; padding: 8px; text-align: center; }\r\n        .vector-comp-label { font-size: 10px; color: var(--text-secondary); margin-bottom: 4px; }\r\n        .vector-comp-value { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--accent-cyan); }\r\n        \r\n        .distance-table { width: 100%; font-size: 11px; border-collapse: collapse; }\r\n        .distance-table th, .distance-table td { padding: 8px; text-align: center; border-bottom: 1px solid var(--border-color); }\r\n        .distance-table th { color: var(--text-secondary); font-weight: 500; }\r\n        .distance-table td { font-family: 'JetBrains Mono', monospace; color: var(--accent-yellow); }\r\n        \r\n        .formula-box { background: var(--bg-tertiary); border-radius: 8px; padding: 12px; margin-top: 12px; text-align: center; }\r\n        .formula { font-family: 'JetBrains Mono', monospace; font-size: 14px; color: var(--accent-cyan); }\r\n        \r\n        .instructions {\r\n            position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);\r\n            z-index: 100; padding: 10px 20px; font-size: 12px; color: var(--text-secondary);\r\n        }\r\n        \r\n        .legend { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 12px; }\r\n        .legend-item { display: flex; align-items: center; gap: 6px; font-size: 11px; }\r\n        .legend-color { width: 14px; height: 14px; border-radius: 3px; }\r\n        \r\n        .toggle-btn {\r\n            background: var(--bg-tertiary); border: 1px solid var(--border-color);\r\n            color: var(--text-primary); padding: 8px 12px; border-radius: 6px;\r\n            font-size: 11px; cursor: pointer; transition: all 0.3s ease;\r\n            margin-right: 6px; margin-bottom: 6px;\r\n        }\r\n        .toggle-btn:hover { border-color: var(--accent-cyan); }\r\n        .toggle-btn.active { background: rgba(34, 211, 238, 0.2); border-color: var(--accent-cyan); color: var(--accent-cyan); }\r\n        \r\n        @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }\r\n        .dragging-hint { animation: pulse 1.5s ease-in-out infinite; }\r\n        \r\n        @media (max-width: 768px) {\r\n            .control-panel, .info-panel { width: calc(100% - 40px); max-height: 40vh; }\r\n            .control-panel { top: auto; bottom: 20px; left: 20px; }\r\n            .info-panel { top: 20px; right: 20px; }\r\n        }\r\n    <\/style>\r\n<\/head>\r\n<body>\r\n    <div id=\"canvas-container\"><\/div>\r\n    \r\n    <!-- Panel de Control -->\r\n    <div class=\"panel control-panel\">\r\n        <div class=\"branding-header\">\r\n            <a href=\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/FISAR_SIM\/\" target=\"_blank\"><img decoding=\"async\" src=\"FISAR_SIM_1\" alt=\"Logo FLUXAR\" class=\"branding-logo-small\" style=\"height:40px;\"><\/a>\r\n            <div class=\"branding-text-box\">\r\n                <span class=\"branding-inst\">FICA - UNSL<\/span>\r\n            <\/div>\r\n        <\/div>\r\n\r\n        <h2 style=\"font-size: 18px; color: var(--text-secondary); font-weight: 700; margin-bottom: 6px;\">Ley de Coulomb<\/h2>\r\n        <p style=\"font-size: 12px; color: var(--text-secondary); margin-bottom: 20px;\">Simulacion Interactiva 3D - Vectores de fuerza<\/p>\r\n        \r\n        <div class=\"section-title\">Configuracion<\/div>\r\n        \r\n        <div style=\"margin-bottom: 16px;\">\r\n            <label class=\"slider-label\" style=\"margin-bottom: 6px; display: block;\">Cantidad de cargas<\/label>\r\n            <select id=\"chargeCount\">\r\n                <option value=\"2\">2 Cargas<\/option>\r\n                <option value=\"3\">3 Cargas<\/option>\r\n                <option value=\"4\">4 Cargas<\/option>\r\n            <\/select>\r\n        <\/div>\r\n        \r\n        <div class=\"section-title\">Visualizacion de Fuerzas<\/div>\r\n        <div style=\"margin-bottom: 12px;\">\r\n            <button class=\"toggle-btn active\" id=\"toggleNetForce\">Fuerza Neta<\/button>\r\n            <button class=\"toggle-btn active\" id=\"toggleIndividual\">F. Individuales<\/button>\r\n            <button class=\"toggle-btn\" id=\"toggleLabels\">Etiquetas<\/button>\r\n        <\/div>\r\n        \r\n        <div class=\"section-title\">Cargas Puntuales<\/div>\r\n        <div id=\"chargeControls\"><\/div>\r\n        \r\n        <div class=\"formula-box\">\r\n            <div style=\"font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;\">Ley de Coulomb (vectorial)<\/div>\r\n            <div class=\"formula\">F = k q1 q2 \/ r\u00b2 * r\u0302<\/div>\r\n            <div style=\"font-size: 10px; color: var(--text-secondary); margin-top: 6px;\">k = 8.99 x 10^9 N m^2\/C^2<\/div>\r\n        <\/div>\r\n        \r\n        <div class=\"legend\">\r\n            <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: var(--accent-red);\"><\/div><span>Carga +<\/span><\/div>\r\n            <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: var(--accent-blue);\"><\/div><span>Carga -<\/span><\/div>\r\n            <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: var(--accent-cyan);\"><\/div><span>F. neta<\/span><\/div>\r\n            <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: var(--accent-green);\"><\/div><span>Atraccion<\/span><\/div>\r\n            <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: var(--accent-orange);\"><\/div><span>Repulsion<\/span><\/div>\r\n        <\/div>\r\n    <\/div>\r\n    \r\n    <!-- Panel de Informacion -->\r\n    <div class=\"panel info-panel\">\r\n        <div class=\"section-title\">Vectores de Fuerza sobre Cada Carga<\/div>\r\n        <div id=\"forcesInfo\"><\/div>\r\n        \r\n        <div class=\"section-title\" style=\"margin-top: 16px;\">Distancias entre Cargas<\/div>\r\n        <div id=\"distancesInfo\"><\/div>\r\n    <\/div>\r\n    \r\n    <!-- Instrucciones -->\r\n    <div class=\"panel instructions\">\r\n        <span class=\"dragging-hint\">Arrastra las cargas para moverlas<\/span> | \r\n        Rotar: click + arrastrar | Zoom: scroll gradual\r\n    <\/div>\r\n\r\n    <script type=\"importmap\">\r\n    {\r\n        \"imports\": {\r\n            \"three\": \"https:\/\/unpkg.com\/three@0.160.0\/build\/three.module.js\",\r\n            \"three\/addons\/\": \"https:\/\/unpkg.com\/three@0.160.0\/examples\/jsm\/\"\r\n        }\r\n    }\r\n    <\/script>\r\n\r\n    <script type=\"module\">\r\n        import * as THREE from 'three';\r\n        import { OrbitControls } from 'three\/addons\/controls\/OrbitControls.js';\r\n\r\n        \/\/ CONSTANTES FISICAS\r\n        const K_COULOMB = 8.99e9;\r\n        const FORCE_ARROW_SCALE = 0.00004;\r\n        const FORCE_COLORS = {\r\n            net: 0x22d3ee,\r\n            attraction: 0x10b981,\r\n            repulsion: 0xf97316\r\n        };\r\n        \r\n        \/\/ VARIABLES GLOBALES\r\n        let scene, camera, renderer, controls;\r\n        let charges = [];\r\n        let gridHelper;\r\n        let raycaster, mouse;\r\n        let selectedCharge = null;\r\n        let isDragging = false;\r\n        let dragPlane = new THREE.Plane();\r\n        let intersection = new THREE.Vector3();\r\n        let offset = new THREE.Vector3();\r\n        \r\n        let showNetForce = true;\r\n        let showIndividual = true;\r\n        let showLabels = false;\r\n        \r\n        \/\/ INICIALIZACION\r\n        function init() {\r\n            scene = new THREE.Scene();\r\n            scene.background = new THREE.Color(0x0a0e17);\r\n            scene.fog = new THREE.Fog(0x0a0e17, 40, 100);\r\n            \r\n            camera = new THREE.PerspectiveCamera(55, window.innerWidth \/ window.innerHeight, 0.1, 1000);\r\n            camera.position.set(18, 14, 18);\r\n            \r\n            renderer = new THREE.WebGLRenderer({ antialias: true });\r\n            renderer.setSize(window.innerWidth, window.innerHeight);\r\n            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\r\n            renderer.shadowMap.enabled = true;\r\n            renderer.shadowMap.type = THREE.PCFSoftShadowMap;\r\n            document.getElementById('canvas-container').appendChild(renderer.domElement);\r\n            \r\n            \/\/ --- CONFIGURACION DE CONTROLES ---\r\n            controls = new OrbitControls(camera, renderer.domElement);\r\n            controls.enableDamping = true;\r\n            controls.dampingFactor = 0.05;\r\n            controls.enableZoom = false; \/\/ Desactivado para usar zoom manual\r\n            controls.enablePan = false;  \/\/ Desactivado para evitar desplazamientos\r\n            controls.rotateSpeed = 0.7;\r\n            \/\/ ----------------------------------\r\n            \r\n            raycaster = new THREE.Raycaster();\r\n            mouse = new THREE.Vector2();\r\n            \r\n            setupLighting();\r\n            setupGrid();\r\n            initCharges(2);\r\n            setupEventListeners();\r\n            animate();\r\n        }\r\n        \r\n        \/\/ --- NUEVA FUNCION DE ZOOM GRADUAL MANUAL ---\r\n        function setupCustomZoom() {\r\n            const canvas = renderer.domElement;\r\n            \r\n            canvas.addEventListener('wheel', (event) => {\r\n                event.preventDefault(); \r\n                \r\n                const zoomSpeed = 0.002; \r\n                const delta = event.deltaY * zoomSpeed;\r\n                \r\n                const direction = new THREE.Vector3();\r\n                camera.getWorldDirection(direction);\r\n                \r\n                camera.position.addScaledVector(direction, -delta * camera.position.length());\r\n                \r\n            }, { passive: false });\r\n        }\r\n        \/\/ --------------------------------------------\r\n        \r\n        function setupLighting() {\r\n            const ambientLight = new THREE.AmbientLight(0x404060, 0.5);\r\n            scene.add(ambientLight);\r\n            \r\n            const mainLight = new THREE.DirectionalLight(0xffffff, 0.9);\r\n            mainLight.position.set(15, 25, 15);\r\n            mainLight.castShadow = true;\r\n            mainLight.shadow.mapSize.width = 2048;\r\n            mainLight.shadow.mapSize.height = 2048;\r\n            scene.add(mainLight);\r\n            \r\n            const fillLight = new THREE.DirectionalLight(0x22d3ee, 0.2);\r\n            fillLight.position.set(-15, 10, -15);\r\n            scene.add(fillLight);\r\n        }\r\n        \r\n        function setupGrid() {\r\n            gridHelper = new THREE.GridHelper(40, 40, 0x1e293b, 0x0f172a);\r\n            gridHelper.position.y = -0.01;\r\n            scene.add(gridHelper);\r\n            \r\n            const planeGeometry = new THREE.PlaneGeometry(50, 50);\r\n            const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x0a0e17, transparent: true, opacity: 0.9, roughness: 0.95, metalness: 0.05 });\r\n            const plane = new THREE.Mesh(planeGeometry, planeMaterial);\r\n            plane.rotation.x = -Math.PI \/ 2;\r\n            plane.position.y = -0.02;\r\n            plane.receiveShadow = true;\r\n            scene.add(plane);\r\n            createAxes();\r\n        }\r\n        \r\n        function createAxes() {\r\n            const axisLength = 15;\r\n            createAxisLine(new THREE.Vector3(-axisLength, 0, 0), new THREE.Vector3(axisLength, 0, 0), 0xef4444);\r\n            createAxisLine(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, axisLength * 0.6, 0), 0x10b981);\r\n            createAxisLine(new THREE.Vector3(0, 0, -axisLength), new THREE.Vector3(0, 0, axisLength), 0x3b82f6);\r\n        }\r\n        \r\n        function createAxisLine(start, end, color) {\r\n            const material = new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.4 });\r\n            const geometry = new THREE.BufferGeometry().setFromPoints([start, end]);\r\n            const line = new THREE.Line(geometry, material);\r\n            scene.add(line);\r\n        }\r\n        \r\n        function initCharges(count) {\r\n            clearCharges();\r\n            const configs = {\r\n                2: [{ position: new THREE.Vector3(-6, 0, 0), charge: 2e-6 }, { position: new THREE.Vector3(6, 0, 0), charge: -2e-6 }],\r\n                3: [{ position: new THREE.Vector3(-5, 0, -4), charge: 3e-6 }, { position: new THREE.Vector3(5, 0, -4), charge: 2e-6 }, { position: new THREE.Vector3(0, 0, 5), charge: -2e-6 }],\r\n                4: [{ position: new THREE.Vector3(-5, 0, -5), charge: 3e-6 }, { position: new THREE.Vector3(5, 0, -5), charge: -2e-6 }, { position: new THREE.Vector3(5, 0, 5), charge: 2e-6 }, { position: new THREE.Vector3(-5, 0, 5), charge: -3e-6 }]\r\n            };\r\n            const config = configs[count];\r\n            for (let i = 0; i < count; i++) { createCharge(i, config[i].position, config[i].charge); }\r\n            createChargeControls(count); \/\/ Primero crear controles\r\n            updateForces(); \/\/ Luego actualizar f\u00edsica\r\n            updateChargeCoords(); \/\/ Finalmente actualizar coords iniciales\r\n        }\r\n        \r\n        function createCharge(index, position, chargeValue) {\r\n            const isPositive = chargeValue >= 0;\r\n            const magnitude = Math.abs(chargeValue);\r\n            const baseRadius = 0.9;\r\n            const radius = baseRadius + magnitude * 1e6 * 0.25;\r\n            const geometry = new THREE.SphereGeometry(radius, 32, 32);\r\n            const material = new THREE.MeshStandardMaterial({ color: isPositive ? 0xef4444 : 0x3b82f6, emissive: isPositive ? 0xef4444 : 0x3b82f6, emissiveIntensity: 0.25, roughness: 0.25, metalness: 0.75 });\r\n            const mesh = new THREE.Mesh(geometry, material);\r\n            mesh.position.copy(position);\r\n            mesh.castShadow = true;\r\n            mesh.receiveShadow = true;\r\n            \r\n            const glowGeometry = new THREE.SphereGeometry(radius * 1.6, 32, 32);\r\n            const glowMaterial = new THREE.MeshBasicMaterial({ color: isPositive ? 0xef4444 : 0x3b82f6, transparent: true, opacity: 0.12, side: THREE.BackSide });\r\n            const glow = new THREE.Mesh(glowGeometry, glowMaterial);\r\n            mesh.add(glow);\r\n            \r\n            const labelSprite = createTextLabel(`q${index + 1}`, '#ffffff');\r\n            labelSprite.position.y = radius + 1.2;\r\n            mesh.add(labelSprite);\r\n            scene.add(mesh);\r\n            \r\n            charges.push({ index: index, mesh: mesh, charge: chargeValue, radius: radius, netForce: new THREE.Vector3(), individualForces: [], arrowGroup: new THREE.Group(), netArrowGroup: new THREE.Group(), labelGroup: new THREE.Group() });\r\n            scene.add(charges[charges.length - 1].arrowGroup);\r\n            scene.add(charges[charges.length - 1].netArrowGroup);\r\n            scene.add(charges[charges.length - 1].labelGroup);\r\n        }\r\n        \r\n        function createTextLabel(text, color) {\r\n            const canvas = document.createElement('canvas');\r\n            const context = canvas.getContext('2d');\r\n            canvas.width = 256; canvas.height = 128;\r\n            context.fillStyle = 'transparent'; context.fillRect(0, 0, canvas.width, canvas.height);\r\n            context.font = 'bold 48px Space Grotesk'; context.fillStyle = color; context.textAlign = 'center'; context.textBaseline = 'middle';\r\n            context.fillText(text, canvas.width \/ 2, canvas.height \/ 2);\r\n            const texture = new THREE.CanvasTexture(canvas);\r\n            const material = new THREE.SpriteMaterial({ map: texture, transparent: true });\r\n            const sprite = new THREE.Sprite(material);\r\n            sprite.scale.set(2, 1, 1);\r\n            return sprite;\r\n        }\r\n        \r\n        function clearCharges() {\r\n            for (const charge of charges) {\r\n                scene.remove(charge.mesh); scene.remove(charge.arrowGroup); scene.remove(charge.netArrowGroup); scene.remove(charge.labelGroup);\r\n                charge.mesh.geometry.dispose(); charge.mesh.material.dispose();\r\n            }\r\n            charges = [];\r\n        }\r\n        \r\n        function calculateForces() {\r\n            const n = charges.length;\r\n            for (const charge of charges) { charge.netForce.set(0, 0, 0); charge.individualForces = []; }\r\n            for (let i = 0; i < n; i++) {\r\n                for (let j = i + 1; j < n; j++) {\r\n                    const q1 = charges[i]; const q2 = charges[j];\r\n                    const r12 = new THREE.Vector3().subVectors(q2.mesh.position, q1.mesh.position);\r\n                    const distance = r12.length();\r\n                    if (distance < 0.5) continue;\r\n                    const r12Unit = r12.clone().normalize();\r\n                    const forceMagnitude = K_COULOMB * Math.abs(q1.charge) * Math.abs(q2.charge) \/ (distance * distance);\r\n                    const sameSign = (q1.charge * q2.charge) > 0;\r\n                    let forceOn1;\r\n                    if (sameSign) forceOn1 = r12Unit.clone().multiplyScalar(-forceMagnitude);\r\n                    else forceOn1 = r12Unit.clone().multiplyScalar(forceMagnitude);\r\n                    const forceOn2 = forceOn1.clone().multiplyScalar(-1);\r\n                    q1.netForce.add(forceOn1); q2.netForce.add(forceOn2);\r\n                    q1.individualForces.push({ from: j, force: forceOn1.clone(), isAttraction: !sameSign, magnitude: forceMagnitude });\r\n                    q2.individualForces.push({ from: i, force: forceOn2.clone(), isAttraction: !sameSign, magnitude: forceMagnitude });\r\n                }\r\n            }\r\n        }\r\n        \r\n        function createArrow(origin, direction, length, color, headLength, headWidth) {\r\n            const group = new THREE.Group();\r\n            const safeLength = Number.isFinite(length) ? Math.max(0.1, Math.min(length, 15)) : 1;\r\n            const safeHeadLength = Number.isFinite(headLength) ? Math.max(0.05, headLength) : 0.2;\r\n            const safeHeadWidth = Number.isFinite(headWidth) ? Math.max(0.03, headWidth) : 0.15;\r\n            if (direction.lengthSq() < 0.0001) return group;\r\n            const dir = direction.clone().normalize();\r\n            const shaftLength = safeLength - safeHeadLength;\r\n            const shaftGeometry = new THREE.CylinderGeometry(0.06, 0.06, Math.max(0.1, shaftLength), 8);\r\n            const shaftMaterial = new THREE.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.3, roughness: 0.4, metalness: 0.6 });\r\n            const shaft = new THREE.Mesh(shaftGeometry, shaftMaterial);\r\n            shaft.position.y = shaftLength \/ 2;\r\n            group.add(shaft);\r\n            const headGeometry = new THREE.ConeGeometry(safeHeadWidth, safeHeadLength, 12);\r\n            const headMaterial = new THREE.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.4, roughness: 0.3, metalness: 0.7 });\r\n            const head = new THREE.Mesh(headGeometry, headMaterial);\r\n            head.position.y = shaftLength + safeHeadLength \/ 2;\r\n            group.add(head);\r\n            group.position.copy(origin);\r\n            const up = new THREE.Vector3(0, 1, 0);\r\n            const quaternion = new THREE.Quaternion();\r\n            quaternion.setFromUnitVectors(up, dir);\r\n            group.setRotationFromQuaternion(quaternion);\r\n            return group;\r\n        }\r\n        \r\n        function updateForces() {\r\n            calculateForces();\r\n            for (const charge of charges) { charge.arrowGroup.clear(); charge.netArrowGroup.clear(); charge.labelGroup.clear(); }\r\n            for (const charge of charges) {\r\n                const pos = charge.mesh.position.clone();\r\n                const chargeRadius = charge.radius;\r\n                if (showIndividual) {\r\n                    for (const indForce of charge.individualForces) {\r\n                        const forceDir = indForce.force.clone();\r\n                        const forceMag = forceDir.length();\r\n                        if (forceMag < 1e-10) continue;\r\n                        const scaledDir = forceDir.clone().multiplyScalar(FORCE_ARROW_SCALE);\r\n                        let arrowLength = scaledDir.length();\r\n                        arrowLength = Math.min(Math.max(arrowLength, 1), 12);\r\n                        const color = indForce.isAttraction ? FORCE_COLORS.attraction : FORCE_COLORS.repulsion;\r\n                        const arrowOrigin = pos.clone().add(forceDir.clone().normalize().multiplyScalar(chargeRadius * 1.1));\r\n                        const arrow = createArrow(arrowOrigin, forceDir.clone().normalize(), arrowLength, color, arrowLength * 0.2, arrowLength * 0.12);\r\n                        charge.arrowGroup.add(arrow);\r\n                        if (showLabels) {\r\n                            const labelPos = arrowOrigin.clone().add(forceDir.clone().normalize().multiplyScalar(arrowLength + 0.8));\r\n                            const label = createForceLabelSimple(forceMag, labelPos, color);\r\n                            charge.labelGroup.add(label);\r\n                        }\r\n                    }\r\n                }\r\n                if (showNetForce) {\r\n                    const netDir = charge.netForce.clone();\r\n                    const netMag = netDir.length();\r\n                    if (netMag > 1e-10) {\r\n                        const scaledNet = netDir.clone().multiplyScalar(FORCE_ARROW_SCALE);\r\n                        let netLength = scaledNet.length();\r\n                        netLength = Math.min(Math.max(netLength, 1.5), 14);\r\n                        const arrowOrigin = pos.clone().add(netDir.clone().normalize().multiplyScalar(chargeRadius * 1.1));\r\n                        const arrow = createArrow(arrowOrigin, netDir.clone().normalize(), netLength, FORCE_COLORS.net, netLength * 0.25, netLength * 0.15);\r\n                        charge.netArrowGroup.add(arrow);\r\n                        if (showLabels) {\r\n                            const labelPos = arrowOrigin.clone().add(netDir.clone().normalize().multiplyScalar(netLength + 1));\r\n                            const label = createForceLabelSimple(netMag, labelPos, FORCE_COLORS.net);\r\n                            charge.labelGroup.add(label);\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            updateInfoPanel();\r\n        }\r\n        \r\n        \/\/ --- NUEVA FUNCION PARA ACTUALIZAR COORDENADAS EN UI ---\r\n        function updateChargeCoords() {\r\n            for (const charge of charges) {\r\n                const pos = charge.mesh.position;\r\n                const elX = document.getElementById(`coord-x-${charge.index}`);\r\n                const elY = document.getElementById(`coord-y-${charge.index}`);\r\n                const elZ = document.getElementById(`coord-z-${charge.index}`);\r\n                \r\n                if(elX) elX.innerText = pos.x.toFixed(2);\r\n                if(elY) elY.innerText = pos.y.toFixed(2);\r\n                if(elZ) elZ.innerText = pos.z.toFixed(2);\r\n            }\r\n        }\r\n        \/\/ -----------------------------------------------------\r\n        \r\n        \/\/ --- MEJORA: ETIQUETAS SIN FONDO Y M\u00c1S PEQUE\u00d1AS ---\r\n        function createForceLabelSimple(magnitude, position, colorHex) {\r\n            const canvas = document.createElement('canvas');\r\n            const context = canvas.getContext('2d');\r\n            \r\n            \/\/ Reducimos tama\u00f1o del canvas\r\n            canvas.width = 200; \r\n            canvas.height = 60;\r\n            \r\n            const text = magnitude.toExponential(1) + ' N';\r\n            \r\n            \/\/ Estilo de texto\r\n            context.font = 'bold 24px JetBrains Mono'; \/\/ Fuente m\u00e1s peque\u00f1a\r\n            context.textAlign = 'center'; \r\n            context.textBaseline = 'middle';\r\n            \r\n            \/\/ 1. Dibujar Borde (Stroke) para contraste\r\n            context.strokeStyle = 'rgba(0, 0, 0, 0.8)'; \/\/ Negro semitransparente\r\n            context.lineWidth = 4;\r\n            context.strokeText(text, canvas.width \/ 2, canvas.height \/ 2);\r\n            \r\n            \/\/ 2. Dibujar Relleno\r\n            \/\/ Convertir hex a string CSS\r\n            const colorStr = '#' + colorHex.toString(16).padStart(6, '0');\r\n            context.fillStyle = colorStr;\r\n            context.fillText(text, canvas.width \/ 2, canvas.height \/ 2);\r\n            \r\n            const texture = new THREE.CanvasTexture(canvas);\r\n            const material = new THREE.SpriteMaterial({ \r\n                map: texture, \r\n                transparent: true, \r\n                depthTest: false \/\/ Asegura que no se oculte detr\u00e1s de objetos\r\n            });\r\n            \r\n            const sprite = new THREE.Sprite(material);\r\n            \/\/ Escala ajustada al nuevo tama\u00f1o\r\n            sprite.scale.set(1.5, 0.45, 1); \r\n            sprite.position.copy(position);\r\n            \r\n            return sprite;\r\n        }\r\n        \/\/ ------------------------------------------------\r\n        \r\n        function createChargeControls(count) {\r\n            const container = document.getElementById('chargeControls');\r\n            container.innerHTML = '';\r\n            for (let i = 0; i < count; i++) {\r\n                const charge = charges[i];\r\n                const isPositive = charge.charge >= 0;\r\n                const chargeValueMicro = charge.charge * 1e6;\r\n                \r\n                const controlDiv = document.createElement('div');\r\n                controlDiv.className = `charge-control ${isPositive ? 'positive' : 'negative'}`;\r\n                controlDiv.id = `charge-control-${i}`;\r\n                controlDiv.innerHTML = `\r\n                    <div class=\"charge-header\">\r\n                        <div class=\"charge-label\">\r\n                            <div class=\"charge-indicator ${isPositive ? 'positive' : 'negative'}\"><\/div>\r\n                            Carga q${i + 1}\r\n                        <\/div>\r\n                        <div class=\"charge-value\" id=\"charge-display-${i}\">${chargeValueMicro >= 0 ? '+' : ''}${chargeValueMicro.toFixed(1)} uC<\/div>\r\n                    <\/div>\r\n                    \r\n                    <!-- COORDENADAS -->\r\n                    <div class=\"coords-display\">\r\n                        <div class=\"coord-box\"><span class=\"coord-label\">X<\/span><span id=\"coord-x-${i}\">${charge.mesh.position.x.toFixed(2)}<\/span><\/div>\r\n                        <div class=\"coord-box\"><span class=\"coord-label\">Y<\/span><span id=\"coord-y-${i}\">${charge.mesh.position.y.toFixed(2)}<\/span><\/div>\r\n                        <div class=\"coord-box\"><span class=\"coord-label\">Z<\/span><span id=\"coord-z-${i}\">${charge.mesh.position.z.toFixed(2)}<\/span><\/div>\r\n                    <\/div>\r\n                    \r\n                    <div class=\"slider-container\">\r\n                        <div class=\"slider-label\">\r\n                            <span>Valor de carga<\/span>\r\n                            <span id=\"slider-value-${i}\">${chargeValueMicro.toFixed(1)} uC<\/span>\r\n                        <\/div>\r\n                        <input type=\"range\" id=\"charge-slider-${i}\" min=\"-5\" max=\"5\" step=\"0.1\" value=\"${chargeValueMicro}\">\r\n                    <\/div>`;\r\n                container.appendChild(controlDiv);\r\n                \r\n                const slider = document.getElementById(`charge-slider-${i}`);\r\n                slider.addEventListener('input', (e) => { updateChargeValue(i, parseFloat(e.target.value) * 1e-6); });\r\n            }\r\n        }\r\n        \r\n        function updateChargeValue(index, newValue) {\r\n            if (index >= charges.length) return;\r\n            const charge = charges[index];\r\n            charge.charge = newValue;\r\n            const isPositive = newValue >= 0;\r\n            const magnitude = Math.abs(newValue);\r\n            charge.mesh.material.color.setHex(isPositive ? 0xef4444 : 0x3b82f6);\r\n            charge.mesh.material.emissive.setHex(isPositive ? 0xef4444 : 0x3b82f6);\r\n            const baseRadius = 0.9;\r\n            const newRadius = baseRadius + magnitude * 1e6 * 0.25;\r\n            charge.mesh.scale.setScalar(newRadius \/ charge.radius);\r\n            const glow = charge.mesh.children[0];\r\n            if (glow && glow.material) glow.material.color.setHex(isPositive ? 0xef4444 : 0x3b82f6);\r\n            const displayValue = (newValue * 1e6).toFixed(1);\r\n            const displayEl = document.getElementById(`charge-display-${index}`);\r\n            const sliderValueEl = document.getElementById(`slider-value-${index}`);\r\n            const controlEl = document.getElementById(`charge-control-${index}`);\r\n            if (displayEl) displayEl.textContent = `${displayValue >= 0 ? '+' : ''}${displayValue} uC`;\r\n            if (sliderValueEl) sliderValueEl.textContent = `${displayValue} uC`;\r\n            if (controlEl) {\r\n                controlEl.classList.remove('positive', 'negative');\r\n                controlEl.classList.add(isPositive ? 'positive' : 'negative');\r\n                const indicator = controlEl.querySelector('.charge-indicator');\r\n                if (indicator) { indicator.classList.remove('positive', 'negative'); indicator.classList.add(isPositive ? 'positive' : 'negative'); }\r\n            }\r\n            updateForces();\r\n        }\r\n        \r\n        function updateInfoPanel() { updateForcesInfo(); updateDistancesInfo(); }\r\n        \r\n        function updateForcesInfo() {\r\n            const container = document.getElementById('forcesInfo');\r\n            if (!container) return;\r\n            let html = '';\r\n            for (const charge of charges) {\r\n                const netMag = charge.netForce.length();\r\n                const isPositive = charge.charge >= 0;\r\n                const chargeColor = isPositive ? '#ef4444' : '#3b82f6';\r\n                html += `<div class=\"force-card\"><div class=\"force-card-header\"><div class=\"charge-indicator ${isPositive ? 'positive' : 'negative'}\"><\/div><div class=\"force-card-title\" style=\"color: ${chargeColor}\">Carga q${charge.index + 1} = ${(charge.charge * 1e6).toFixed(1)} uC<\/div><\/div>`;\r\n                if (charge.individualForces.length > 0) {\r\n                    html += `<div style=\"font-size: 11px; color: var(--text-secondary); margin-bottom: 8px;\">Fuerzas individuales:<\/div>`;\r\n                    for (const indForce of charge.individualForces) {\r\n                        const fMag = indForce.magnitude;\r\n                        const typeClass = indForce.isAttraction ? 'attraction' : 'repulsion';\r\n                        const typeText = indForce.isAttraction ? 'Atraccion' : 'Repulsion';\r\n                        const typeColor = indForce.isAttraction ? 'var(--accent-green)' : 'var(--accent-orange)';\r\n                        const fDir = indForce.force.clone().normalize();\r\n                        const theta = Math.acos(fDir.y) * 180 \/ Math.PI;\r\n                        const phi = Math.atan2(fDir.z, fDir.x) * 180 \/ Math.PI;\r\n                        html += `<div class=\"force-vector-row\"><div><span class=\"force-type-badge ${typeClass}\">${typeText}<\/span><span style=\"margin-left: 6px;\">por q${indForce.from + 1}<\/span><\/div><div style=\"font-family: 'JetBrains Mono', monospace; color: ${typeColor}\">${fMag.toExponential(3)} N<\/div><\/div><div style=\"font-size: 10px; color: var(--text-secondary); padding: 4px 10px 8px;\">Direccion: theta=${theta.toFixed(1)} deg, phi=${phi.toFixed(1)} deg<\/div>`;\r\n                    }\r\n                }\r\n                const netDir = charge.netForce.clone().normalize();\r\n                const netTheta = netMag > 1e-10 ? Math.acos(netDir.y) * 180 \/ Math.PI : 0;\r\n                const netPhi = netMag > 1e-10 ? Math.atan2(netDir.z, netDir.x) * 180 \/ Math.PI : 0;\r\n                html += `<div class=\"force-card net-force-card\" style=\"margin-top: 12px;\"><div style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;\"><span style=\"font-weight: 600; font-size: 13px;\">FUERZA NETA<\/span><span class=\"net-force-value\">${netMag.toExponential(3)} N<\/span><\/div><div class=\"vector-components\"><div class=\"vector-comp\"><div class=\"vector-comp-label\">Fx<\/div><div class=\"vector-comp-value\">${charge.netForce.x.toExponential(2)}<\/div><\/div><div class=\"vector-comp\"><div class=\"vector-comp-label\">Fy<\/div><div class=\"vector-comp-value\">${charge.netForce.y.toExponential(2)}<\/div><\/div><div class=\"vector-comp\"><div class=\"vector-comp-label\">Fz<\/div><div class=\"vector-comp-value\">${charge.netForce.z.toExponential(2)}<\/div><\/div><\/div><div style=\"font-size: 11px; color: var(--text-secondary); margin-top: 8px; text-align: center;\">Direccion: theta=${netTheta.toFixed(1)} deg, phi=${netPhi.toFixed(1)} deg<\/div><\/div><\/div>`;\r\n            }\r\n            container.innerHTML = html;\r\n        }\r\n        \r\n        function updateDistancesInfo() {\r\n            const container = document.getElementById('distancesInfo');\r\n            if (!container) return;\r\n            const n = charges.length;\r\n            let html = '<table class=\"distance-table\"><thead><tr><th>Par<\/th><th>Distancia (m)<\/th><th>Tipo<\/th><\/tr><\/thead><tbody>';\r\n            for (let i = 0; i < n; i++) {\r\n                for (let j = i + 1; j < n; j++) {\r\n                    const distance = charges[i].mesh.position.distanceTo(charges[j].mesh.position);\r\n                    const sameSign = (charges[i].charge * charges[j].charge) > 0;\r\n                    const typeText = sameSign ? 'Repulsion' : 'Atraccion';\r\n                    const typeColor = sameSign ? 'var(--accent-orange)' : 'var(--accent-green)';\r\n                    html += `<tr><td>q${i + 1} - q${j + 1}<\/td><td>${distance.toFixed(3)}<\/td><td style=\"color: ${typeColor}\">${typeText}<\/td><\/tr>`;\r\n                }\r\n            }\r\n            html += '<\/tbody><\/table>';\r\n            container.innerHTML = html;\r\n        }\r\n        \r\n        function setupEventListeners() {\r\n            document.getElementById('chargeCount').addEventListener('change', (e) => { initCharges(parseInt(e.target.value)); });\r\n            document.getElementById('toggleNetForce').addEventListener('click', (e) => { showNetForce = !showNetForce; e.target.classList.toggle('active', showNetForce); updateForces(); });\r\n            document.getElementById('toggleIndividual').addEventListener('click', (e) => { showIndividual = !showIndividual; e.target.classList.toggle('active', showIndividual); updateForces(); });\r\n            document.getElementById('toggleLabels').addEventListener('click', (e) => { showLabels = !showLabels; e.target.classList.toggle('active', showLabels); updateForces(); });\r\n            \r\n            renderer.domElement.addEventListener('mousedown', onMouseDown);\r\n            renderer.domElement.addEventListener('mousemove', onMouseMove);\r\n            renderer.domElement.addEventListener('mouseup', onMouseUp);\r\n            renderer.domElement.addEventListener('touchstart', onTouchStart, { passive: false });\r\n            renderer.domElement.addEventListener('touchmove', onTouchMove, { passive: false });\r\n            renderer.domElement.addEventListener('touchend', onTouchEnd);\r\n            \r\n            setupCustomZoom(); \/\/ Zoom manual\r\n            \r\n            window.addEventListener('resize', onWindowResize);\r\n        }\r\n        \r\n        function onMouseDown(event) {\r\n            event.preventDefault();\r\n            mouse.x = (event.clientX \/ window.innerWidth) * 2 - 1;\r\n            mouse.y = -(event.clientY \/ window.innerHeight) * 2 + 1;\r\n            raycaster.setFromCamera(mouse, camera);\r\n            const meshes = charges.map(c => c.mesh);\r\n            const intersects = raycaster.intersectObjects(meshes);\r\n            if (intersects.length > 0) {\r\n                controls.enabled = false; isDragging = true;\r\n                const intersectedMesh = intersects[0].object;\r\n                selectedCharge = charges.find(c => c.mesh === intersectedMesh);\r\n                dragPlane.setFromNormalAndCoplanarPoint(camera.getWorldDirection(new THREE.Vector3()).negate(), intersectedMesh.position);\r\n                raycaster.ray.intersectPlane(dragPlane, intersection);\r\n                offset.subVectors(intersectedMesh.position, intersection);\r\n                renderer.domElement.style.cursor = 'grabbing';\r\n            }\r\n        }\r\n        \r\n        function onMouseMove(event) {\r\n            if (!isDragging || !selectedCharge) {\r\n                mouse.x = (event.clientX \/ window.innerWidth) * 2 - 1;\r\n                mouse.y = -(event.clientY \/ window.innerHeight) * 2 + 1;\r\n                raycaster.setFromCamera(mouse, camera);\r\n                const meshes = charges.map(c => c.mesh);\r\n                const intersects = raycaster.intersectObjects(meshes);\r\n                renderer.domElement.style.cursor = intersects.length > 0 ? 'grab' : 'default';\r\n                return;\r\n            }\r\n            event.preventDefault();\r\n            mouse.x = (event.clientX \/ window.innerWidth) * 2 - 1;\r\n            mouse.y = -(event.clientY \/ window.innerHeight) * 2 + 1;\r\n            raycaster.setFromCamera(mouse, camera);\r\n            raycaster.ray.intersectPlane(dragPlane, intersection);\r\n            selectedCharge.mesh.position.copy(intersection.add(offset));\r\n            \r\n            updateForces();\r\n            updateChargeCoords(); \r\n        }\r\n        \r\n        function onMouseUp() { isDragging = false; selectedCharge = null; controls.enabled = true; renderer.domElement.style.cursor = 'default'; }\r\n        \r\n        function onTouchStart(event) {\r\n            event.preventDefault();\r\n            const touch = event.touches[0];\r\n            mouse.x = (touch.clientX \/ window.innerWidth) * 2 - 1;\r\n            mouse.y = -(touch.clientY \/ window.innerHeight) * 2 + 1;\r\n            raycaster.setFromCamera(mouse, camera);\r\n            const meshes = charges.map(c => c.mesh);\r\n            const intersects = raycaster.intersectObjects(meshes);\r\n            if (intersects.length > 0) {\r\n                controls.enabled = false; isDragging = true;\r\n                const intersectedMesh = intersects[0].object;\r\n                selectedCharge = charges.find(c => c.mesh === intersectedMesh);\r\n                dragPlane.setFromNormalAndCoplanarPoint(camera.getWorldDirection(new THREE.Vector3()).negate(), intersectedMesh.position);\r\n                raycaster.ray.intersectPlane(dragPlane, intersection);\r\n                offset.subVectors(intersectedMesh.position, intersection);\r\n            }\r\n        }\r\n        \r\n        function onTouchMove(event) {\r\n            if (!isDragging || !selectedCharge) return;\r\n            event.preventDefault();\r\n            const touch = event.touches[0];\r\n            mouse.x = (touch.clientX \/ window.innerWidth) * 2 - 1;\r\n            mouse.y = -(touch.clientY \/ window.innerHeight) * 2 + 1;\r\n            raycaster.setFromCamera(mouse, camera);\r\n            raycaster.ray.intersectPlane(dragPlane, intersection);\r\n            selectedCharge.mesh.position.copy(intersection.add(offset));\r\n            updateForces();\r\n            updateChargeCoords();\r\n        }\r\n        \r\n        function onTouchEnd() { isDragging = false; selectedCharge = null; controls.enabled = true; }\r\n        \r\n        function onWindowResize() {\r\n            camera.aspect = window.innerWidth \/ window.innerHeight;\r\n            camera.updateProjectionMatrix();\r\n            renderer.setSize(window.innerWidth, window.innerHeight);\r\n        }\r\n        \r\n        function animate() {\r\n            requestAnimationFrame(animate);\r\n            controls.update();\r\n            const time = Date.now() * 0.001;\r\n            for (const charge of charges) {\r\n                const glow = charge.mesh.children[0];\r\n                if (glow && glow.material) glow.material.opacity = 0.08 + Math.sin(time * 2 + charge.index) * 0.04;\r\n            }\r\n            renderer.render(scene, camera);\r\n        }\r\n        \r\n        init();\r\n    <\/script>\r\n<\/body>\r\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Ley de Coulomb &#8211; Simulacion 3D Interactiva FICA &#8211; UNSL Ley de Coulomb Simulacion Interactiva 3D &#8211; Vectores de fuerza Configuracion Cantidad de cargas 2&hellip;<\/p>\n","protected":false},"author":7,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"class_list":["post-5523","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.0 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Ley_de_coulomb - F\u00edsica 2<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/\" \/>\n<meta property=\"og:locale\" content=\"es_ES\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Ley_de_coulomb - F\u00edsica 2\" \/>\n<meta property=\"og:description\" content=\"Ley de Coulomb &#8211; Simulacion 3D Interactiva FICA &#8211; UNSL Ley de Coulomb Simulacion Interactiva 3D &#8211; Vectores de fuerza Configuracion Cantidad de cargas 2&hellip;\" \/>\n<meta property=\"og:url\" content=\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/\" \/>\n<meta property=\"og:site_name\" content=\"F\u00edsica 2\" \/>\n<meta property=\"article:modified_time\" content=\"2026-03-19T17:41:48+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Tiempo de lectura\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minuto\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/\",\"url\":\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/\",\"name\":\"Ley_de_coulomb - F\u00edsica 2\",\"isPartOf\":{\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#website\"},\"datePublished\":\"2026-03-05T01:56:25+00:00\",\"dateModified\":\"2026-03-19T17:41:48+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/#breadcrumb\"},\"inLanguage\":\"es-AR\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Inicio\",\"item\":\"https:\/\/fisica2.fica.unsl.edu.ar\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Ley_de_coulomb\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#website\",\"url\":\"https:\/\/fisica2.fica.unsl.edu.ar\/\",\"name\":\"F\u00edsica 2\",\"description\":\"FICA - UNSL\",\"publisher\":{\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/fisica2.fica.unsl.edu.ar\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"es-AR\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#organization\",\"name\":\"SAC- Secretar\u00eda General FICA\",\"url\":\"https:\/\/fisica2.fica.unsl.edu.ar\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"es-AR\",\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/fisica2.fica.unsl.edu.ar\/wp-content\/uploads\/2021\/11\/SG-Logo.png\",\"contentUrl\":\"https:\/\/fisica2.fica.unsl.edu.ar\/wp-content\/uploads\/2021\/11\/SG-Logo.png\",\"width\":4483,\"height\":1231,\"caption\":\"SAC- Secretar\u00eda General FICA\"},\"image\":{\"@id\":\"https:\/\/fisica2.fica.unsl.edu.ar\/#\/schema\/logo\/image\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Ley_de_coulomb - F\u00edsica 2","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/","og_locale":"es_ES","og_type":"article","og_title":"Ley_de_coulomb - F\u00edsica 2","og_description":"Ley de Coulomb &#8211; Simulacion 3D Interactiva FICA &#8211; UNSL Ley de Coulomb Simulacion Interactiva 3D &#8211; Vectores de fuerza Configuracion Cantidad de cargas 2&hellip;","og_url":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/","og_site_name":"F\u00edsica 2","article_modified_time":"2026-03-19T17:41:48+00:00","twitter_card":"summary_large_image","twitter_misc":{"Tiempo de lectura":"1 minuto"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/","url":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/","name":"Ley_de_coulomb - F\u00edsica 2","isPartOf":{"@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#website"},"datePublished":"2026-03-05T01:56:25+00:00","dateModified":"2026-03-19T17:41:48+00:00","breadcrumb":{"@id":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/#breadcrumb"},"inLanguage":"es-AR","potentialAction":[{"@type":"ReadAction","target":["https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/ley_de_coulomb\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https:\/\/fisica2.fica.unsl.edu.ar\/"},{"@type":"ListItem","position":2,"name":"Ley_de_coulomb"}]},{"@type":"WebSite","@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#website","url":"https:\/\/fisica2.fica.unsl.edu.ar\/","name":"F\u00edsica 2","description":"FICA - UNSL","publisher":{"@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/fisica2.fica.unsl.edu.ar\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es-AR"},{"@type":"Organization","@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#organization","name":"SAC- Secretar\u00eda General FICA","url":"https:\/\/fisica2.fica.unsl.edu.ar\/","logo":{"@type":"ImageObject","inLanguage":"es-AR","@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#\/schema\/logo\/image\/","url":"https:\/\/fisica2.fica.unsl.edu.ar\/wp-content\/uploads\/2021\/11\/SG-Logo.png","contentUrl":"https:\/\/fisica2.fica.unsl.edu.ar\/wp-content\/uploads\/2021\/11\/SG-Logo.png","width":4483,"height":1231,"caption":"SAC- Secretar\u00eda General FICA"},"image":{"@id":"https:\/\/fisica2.fica.unsl.edu.ar\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/pages\/5523","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/comments?post=5523"}],"version-history":[{"count":118,"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/pages\/5523\/revisions"}],"predecessor-version":[{"id":6679,"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/pages\/5523\/revisions\/6679"}],"wp:attachment":[{"href":"https:\/\/fisica2.fica.unsl.edu.ar\/index.php\/wp-json\/wp\/v2\/media?parent=5523"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}