Hola Guitarristas, recuerdan la serie V-Amp de Behringer?
Estoy en la cruzada de levantar un nuevo diseño de la V-Amp designer llamandola V-Amp Studio...version mejorada de lo que solia ser la interfaz...puede que haga lo mismo pero es mas intuitiva...probablemente no es perfecta y requerirá mejoras...pero con la ayuda de todos, puede salir un buen hijo para este viejo cacharro...en su época no muchos dominaban la emulación pero ahora puede dar mucho este aparatito...COMO DIJE, AUN FALTAN COSILLAS...
Codigo HTML: (PRUEBAS)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>V-AMP STUDIO</title>
<style>
:root {
--bg: #111315;
--panel: #1b1e20;
--panel2: #24282a;
--gold: #c49a50;
--gold2: #e2bd79;
--text: #e8e5df;
--muted: #85898a;
--line: #34393b;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: Arial, sans-serif;
}
header {
height: 70px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28px;
background: #191c1e;
border-bottom: 1px solid var(--line);
}
.logo {
color: var(--gold2);
font-size: 23px;
font-weight: bold;
letter-spacing: 3px;
}
.logo span {
color: white;
font-weight: 300;
}
.status {
color: var(--muted);
font-size: 12px;
}
.status i {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #c49a50;
margin-right: 7px;
}
button, select {
background: var(--panel2);
color: var(--text);
border: 1px solid var(--line);
padding: 10px 13px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
border-color: var(--gold);
color: var(--gold2);
}
.toolbar {
display: flex;
gap: 8px;
}
main {
display: grid;
grid-template-columns: 210px minmax(400px, 1fr) 270px;
gap: 14px;
padding: 16px;
min-height: calc(100vh - 70px);
}
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 7px;
padding: 16px;
}
.section-title {
font-size: 11px;
color: var(--gold);
letter-spacing: 2px;
margin-bottom: 15px;
}
.preset {
padding: 12px 10px;
border-bottom: 1px solid var(--line);
color: #c4c6c5;
cursor: pointer;
font-size: 13px;
}
.preset:hover, .preset.active {
background: #292b29;
color: var(--gold2);
border-left: 3px solid var(--gold);
}
.center {
display: flex;
flex-direction: column;
gap: 14px;
}
.device {
background: linear-gradient(145deg, #292d2f, #17191a);
border: 1px solid #44494a;
border-radius: 8px;
padding: 20px;
box-shadow: inset 0 1px 0 #ffffff0b;
}
.device-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.device-title h2 {
font-size: 17px;
letter-spacing: 3px;
color: var(--gold2);
margin: 0;
}
.knobs {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
}
.knob-wrap {
text-align: center;
}
.knob {
width: 62px;
height: 62px;
margin: auto auto 10px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #626768, #25292a 65%, #111);
border: 3px solid #555b5c;
position: relative;
box-shadow: 0 4px 8px #0009;
}
.knob::before {
content: "";
position: absolute;
top: 5px;
left: 27px;
width: 3px;
height: 14px;
background: var(--gold2);
border-radius: 3px;
transform-origin: 2px 25px;
transform: rotate(var(--angle, 0deg));
}
.knob-wrap input {
width: 100%;
accent-color: var(--gold);
}
.knob-label {
font-size: 10px;
letter-spacing: 1px;
color: #c7c7c2;
}
.knob-value {
font-size: 11px;
color: var(--gold2);
margin-top: 5px;
}
.modules {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.module {
padding: 15px 6px;
background: #202426;
border: 1px solid var(--line);
border-radius: 5px;
color: var(--muted);
font-size: 11px;
letter-spacing: 1px;
}
.module.active {
border-color: var(--gold);
color: var(--gold2);
background: #30291e;
}
.module-content {
min-height: 290px;
}
.module-heading {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.module-heading h2 {
font-size: 15px;
color: var(--gold2);
letter-spacing: 2px;
}
.carousel {
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
gap: 10px;
}
.arrow {
font-size: 23px;
height: 55px;
color: var(--gold);
}
.items {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.item {
min-height: 155px;
padding: 15px 8px;
border: 1px solid var(--line);
border-radius: 6px;
background: linear-gradient(145deg, #292e30, #171a1b);
text-align: center;
cursor: pointer;
}
.item:hover, .item.selected {
border-color: var(--gold);
background: #302a20;
}
.amp-graphic {
width: 85px;
height: 65px;
margin: 0 auto 12px;
border-radius: 5px;
border: 2px solid #777;
background: linear-gradient(#44494a, #191c1d);
display: flex;
align-items: center;
justify-content: center;
color: var(--gold2);
font-weight: bold;
font-size: 19px;
box-shadow: 0 4px 7px #0008;
}
.item-name {
font-size: 12px;
color: #dedbd3;
}
.item small {
display: block;
margin-top: 6px;
font-size: 10px;
color: var(--muted);
}
.effect-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.effect {
min-height: 145px;
background: #222729;
border: 1px solid var(--line);
border-radius: 6px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
}
.effect:hover, .effect.selected {
border-color: var(--gold);
background: #302a20;
}
.effect-icon {
font-size: 30px;
color: var(--gold2);
margin-bottom: 12px;
}
.effect-name {
font-size: 12px;
letter-spacing: 1px;
}
.right-panel h3 {
font-size: 14px;
color: var(--gold2);
letter-spacing: 1px;
margin-top: 0;
}
.detail-label {
font-size: 10px;
color: var(--muted);
letter-spacing: 1px;
margin: 20px 0 7px;
}
.detail-value {
background: #111516;
border: 1px solid var(--line);
padding: 10px;
border-radius: 4px;
font-size: 12px;
}
.control {
margin: 17px 0;
}
.control label {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #c7c7c2;
margin-bottom: 8px;
}
.control input {
width: 100%;
accent-color: var(--gold);
}
.note {
font-size: 10px;
color: var(--muted);
line-height: 1.5;
margin-top: 25px;
}
footer {
color: #777;
font-size: 10px;
padding: 12px 18px;
border-top: 1px solid var(--line);
text-align: center;
}
@media(max-width: 950px) {
main { grid-template-columns: 180px 1fr; }
.right-panel { grid-column: 1 / -1; }
}
@media(max-width: 650px) {
main { display: flex; flex-direction: column; }
.knobs { grid-template-columns: repeat(3, 1fr); }
.modules { grid-template-columns: repeat(2, 1fr); }
.items, .effect-grid { grid-template-columns: 1fr; }
header { padding: 0 12px; }
.toolbar { flex-wrap: wrap; }
}
</style>
</head>
<body>
<header>
<div class="logo">V-AMP <span>STUDIO</span></div>
<div class="status"><i></i> MODO LOCAL · MIDI NO CONECTADO</div>
<div class="toolbar">
<button onclick="newPreset()">NEW</button>
<button onclick="exportPreset()">EXPORT</button>
<button onclick="document.getElementById('importFile').click()">IMPORT</button>
<input id="importFile" type="file" accept=".json" hidden onchange="importPreset(event)">
</div>
</header>
<main>
<aside class="panel">
<div class="section-title">PRESET LIBRARY</div>
<div class="preset active" onclick="selectPreset(this,'New Preset')">01 · New Preset</div>
<div class="preset" onclick="selectPreset(this,'Clean Classic')">02 · Clean Classic</div>
<div class="preset" onclick="selectPreset(this,'Vintage Crunch')">03 · Vintage Crunch</div>
<div class="preset" onclick="selectPreset(this,'British Rock')">04 · British Rock</div>
<div class="preset" onclick="selectPreset(this,'Modern Lead')">05 · Modern Lead</div>
<div class="preset" onclick="selectPreset(this,'Studio Clean')">06 · Studio Clean</div>
<div style="margin-top:20px">
<button style="width:100%" onclick="newPreset()">+ NEW PRESET</button>
</div>
<p class="note">Los presets de esta versión son sesiones locales de trabajo. Todavía no representan memorias nativas del equipo.</p>
</aside>
<section class="center">
<div class="device">
<div class="device-title">
<h2>V-AMP CONTROL DE TONO</h2>
<span style="font-size:10px;color:#888">VIRTUAL CONTROL</span>
</div>
<div class="knobs" id="knobs"></div>
</div>
<div class="panel">
<div class="section-title">SIGNAL ARCHITECTURE</div>
<div class="modules">
<button class="module" data-module="PRE AMP" onclick="showModule('PRE AMP')">PRE AMP</button>
<button class="module active" data-module="AMP" onclick="showModule('AMP')">AMP</button>
<button class="module" data-module="CAB" onclick="showModule('CAB')">CAB</button>
<button class="module" data-module="POST AMP" onclick="showModule('POST AMP')">POST AMP</button>
</div>
</div>
<div class="panel module-content">
<div class="module-heading">
<h2 id="moduleTitle">AMP MODELS</h2>
<span style="font-size:10px;color:#777" id="moduleCounter">01 / 06</span>
</div>
<div id="moduleDisplay"></div>
</div>
</section>
<aside class="panel right-panel">
<div class="section-title">EDITING PANEL</div>
<h3 id="detailTitle">AMP MODEL</h3>
<div class="detail-label">SELECTED COMPONENT</div>
<div class="detail-value" id="detailName">American Clean</div>
<div id="detailControls"></div>
<p class="note">El panel contextual cambia según el módulo o efecto seleccionado. Los controles de esta versión modifican solamente la interfaz.</p>
</aside>
</main>
<footer>
V-AMP STUDIO · LOCAL PROTOTYPE · MIDI INTEGRATION PENDING MANUAL VALIDATION
</footer>
<script>
const knobData = [
["GAIN",50],["LOW",50],["MID",50],
["HIGH",50],["PRESENCE",50],["VOLUME",70]
];
const modules = {
"AMP": {
title:"AMP MODELS",
items:[
"American Clean","Black Twin","British Crunch",
"Classic Rock","Modern Lead","Vintage Drive"
],
type:"amp"
},
"CAB": {
title:"CABINET MODELS",
items:[
"1x8 Vintage","1x12 Open Back","2x12 Classic",
"2x12 Modern","4x12 Vintage","4x12 Modern"
],
type:"cab"
},
"PRE AMP": {
title:"PRE AMP EFFECTS",
items:["Compressor","Wah","Drive"],
type:"effect"
},
"POST AMP": {
title:"POST AMP EFFECTS",
items:["Modulation","Delay","Reverb"],
type:"effect"
}
};
let currentModule = "AMP";
let selectedItem = "American Clean";
let presetName = "New Preset";
let selectedIndex = 0;
function renderKnobs() {
const container = document.getElementById("knobs");
container.innerHTML = "";
knobData.forEach(([name,value],index) => {
const wrap = document.createElement("div");
wrap.className = "knob-wrap";
wrap.innerHTML = `
<div class="knob" id="knob${index}" style="--angle:${(value/100)*270-135}deg"></div>
<div class="knob-label">${name}</div>
<input type="range" min="0" max="100" value="${value}"
oninput="updateKnob(${index},this.value)">
<div class="knob-value" id="knobValue${index}">${value}</div>
`;
container.appendChild(wrap);
});
}
function updateKnob(index,value) {
knobData[index][1] = Number(value);
document.getElementById("knobValue"+index).textContent = value;
document.getElementById("knob"+index).style.setProperty(
"--angle",(value/100)*270-135+"deg"
);
}
function showModule(module) {
currentModule = module;
selectedIndex = 0;
selectedItem = modules[module].items[0];
document.querySelectorAll(".module").forEach(button => {
button.classList.toggle("active",button.dataset.module === module);
});
renderModule();
renderDetails();
}
function renderModule() {
const data = modules[currentModule];
document.getElementById("moduleTitle").textContent = data.title;
if(data.type === "effect") {
document.getElementById("moduleDisplay").innerHTML = `
<div class="effect-grid">
${data.items.map((item,index) => `
<div class="effect ${index===selectedIndex?'selected':''}"
onclick="selectItem(${index})">
<div class="effect-icon">${effectIcon(item)}</div>
<div class="effect-name">${item}</div>
</div>
`).join("")}
</div>
`;
} else {
const items = data.items;
const visible = [];
for(let i=0;i<3;i++) {
visible.push((selectedIndex+i)%items.length);
}
document.getElementById("moduleDisplay").innerHTML = `
<div class="carousel">
<button class="arrow" onclick="moveCarousel(-1)">‹</button>
<div class="items">
${visible.map(index => `
<div class="item ${index===selectedIndex?'selected':''}"
onclick="selectItem(${index})">
<div class="amp-graphic">${data.type==="amp"?"AMP":"CAB"}</div>
<div class="item-name">${items[index]}</div>
<small>${data.type==="amp"?"AMPLIFIER MODEL":"CABINET MODEL"}</small>
</div>
`).join("")}
</div>
<button class="arrow" onclick="moveCarousel(1)">›</button>
</div>
`;
}
document.getElementById("moduleCounter").textContent =
String(selectedIndex+1).padStart(2,"0")+" / "+
String(data.items.length).padStart(2,"0");
}
function effectIcon(item) {
const icons = {
"Compressor":"◉","Wah":"〰","Drive":"⚡",
"Modulation":"◌","Delay":"◷","Reverb":"〰"
};
return icons[item] || "◉";
}
function selectItem(index) {
selectedIndex = index;
selectedItem = modules[currentModule].items[index];
renderModule();
renderDetails();
}
function moveCarousel(direction) {
const length = modules[currentModule].items.length;
selectedIndex = (selectedIndex + direction + length) % length;
selectedItem = modules[currentModule].items[selectedIndex];
renderModule();
renderDetails();
}
function renderDetails() {
document.getElementById("detailTitle").textContent = currentModule;
document.getElementById("detailName").textContent = selectedItem;
const controls = document.getElementById("detailControls");
let names = ["LEVEL","TONE","MIX"];
if(selectedItem === "Compressor") names = ["SUSTAIN","ATTACK","LEVEL"];
if(selectedItem === "Wah") names = ["POSITION","RESONANCE","LEVEL"];
if(selectedItem === "Drive") names = ["DRIVE","TONE","LEVEL"];
if(selectedItem === "Delay") names = ["TIME","FEEDBACK","MIX"];
if(selectedItem === "Reverb") names = ["DECAY","TONE","MIX"];
if(selectedItem === "Modulation") names = ["RATE","DEPTH","MIX"];
controls.innerHTML = names.map((name,index) => `
<div class="control">
<label><span>${name}</span><span id="detailValue${index}">50</span></label>
<input type="range" min="0" max="100" value="50"
oninput="document.getElementById('detailValue${index}').textContent=this.value">
</div>
`).join("");
}
function selectPreset(element,name) {
document.querySelectorAll(".preset").forEach(p => p.classList.remove("active"));
element.classList.add("active");
presetName = name;
}
function newPreset() {
presetName = "New Preset";
document.querySelectorAll(".preset").forEach(p => p.classList.remove("active"));
alert("Nueva sesión iniciada. Puedes configurar tus controles.");
}
function exportPreset() {
const data = {
application:"V-AMP STUDIO",
format:"Local session prototype",
preset:presetName,
knobs:knobData,
module:currentModule,
selectedItem:selectedItem
};
const blob = new Blob([JSON.stringify(data,null,2)],{type:"application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = presetName.replaceAll(" ","_")+".json";
a.click();
URL.revokeObjectURL(url);
}
function importPreset(event) {
const file = event.target.files[0];
if(!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result);
if(!Array.isArray(data.knobs) || data.knobs.length !== knobData.length) {
throw new Error("Formato de sesión no reconocido");
}
data.knobs.forEach((entry,index) => {
knobData[index][1] = Number(entry[1]);
});
presetName = data.preset || "Imported Preset";
renderKnobs();
if(modules[data.module]) {
showModule(data.module);
const index = modules[data.module].items.indexOf(data.selectedItem);
if(index >= 0) selectItem(index);
}
alert("Sesión importada correctamente.");
} catch(error) {
alert("No se pudo importar: "+error.message);
}
};
reader.readAsText(file);
event.target.value = "";
}
renderKnobs();
renderModule();
renderDetails();
</script>
</body>
</html>
Guardar como index.html para probar algunas cosillas...no es otra cosa...
Estoy en la cruzada de levantar un nuevo diseño de la V-Amp designer llamandola V-Amp Studio...version mejorada de lo que solia ser la interfaz...puede que haga lo mismo pero es mas intuitiva...probablemente no es perfecta y requerirá mejoras...pero con la ayuda de todos, puede salir un buen hijo para este viejo cacharro...en su época no muchos dominaban la emulación pero ahora puede dar mucho este aparatito...COMO DIJE, AUN FALTAN COSILLAS...
Codigo HTML: (PRUEBAS)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>V-AMP STUDIO</title>
<style>
:root {
--bg: #111315;
--panel: #1b1e20;
--panel2: #24282a;
--gold: #c49a50;
--gold2: #e2bd79;
--text: #e8e5df;
--muted: #85898a;
--line: #34393b;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: Arial, sans-serif;
}
header {
height: 70px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28px;
background: #191c1e;
border-bottom: 1px solid var(--line);
}
.logo {
color: var(--gold2);
font-size: 23px;
font-weight: bold;
letter-spacing: 3px;
}
.logo span {
color: white;
font-weight: 300;
}
.status {
color: var(--muted);
font-size: 12px;
}
.status i {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #c49a50;
margin-right: 7px;
}
button, select {
background: var(--panel2);
color: var(--text);
border: 1px solid var(--line);
padding: 10px 13px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
border-color: var(--gold);
color: var(--gold2);
}
.toolbar {
display: flex;
gap: 8px;
}
main {
display: grid;
grid-template-columns: 210px minmax(400px, 1fr) 270px;
gap: 14px;
padding: 16px;
min-height: calc(100vh - 70px);
}
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 7px;
padding: 16px;
}
.section-title {
font-size: 11px;
color: var(--gold);
letter-spacing: 2px;
margin-bottom: 15px;
}
.preset {
padding: 12px 10px;
border-bottom: 1px solid var(--line);
color: #c4c6c5;
cursor: pointer;
font-size: 13px;
}
.preset:hover, .preset.active {
background: #292b29;
color: var(--gold2);
border-left: 3px solid var(--gold);
}
.center {
display: flex;
flex-direction: column;
gap: 14px;
}
.device {
background: linear-gradient(145deg, #292d2f, #17191a);
border: 1px solid #44494a;
border-radius: 8px;
padding: 20px;
box-shadow: inset 0 1px 0 #ffffff0b;
}
.device-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.device-title h2 {
font-size: 17px;
letter-spacing: 3px;
color: var(--gold2);
margin: 0;
}
.knobs {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
}
.knob-wrap {
text-align: center;
}
.knob {
width: 62px;
height: 62px;
margin: auto auto 10px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #626768, #25292a 65%, #111);
border: 3px solid #555b5c;
position: relative;
box-shadow: 0 4px 8px #0009;
}
.knob::before {
content: "";
position: absolute;
top: 5px;
left: 27px;
width: 3px;
height: 14px;
background: var(--gold2);
border-radius: 3px;
transform-origin: 2px 25px;
transform: rotate(var(--angle, 0deg));
}
.knob-wrap input {
width: 100%;
accent-color: var(--gold);
}
.knob-label {
font-size: 10px;
letter-spacing: 1px;
color: #c7c7c2;
}
.knob-value {
font-size: 11px;
color: var(--gold2);
margin-top: 5px;
}
.modules {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.module {
padding: 15px 6px;
background: #202426;
border: 1px solid var(--line);
border-radius: 5px;
color: var(--muted);
font-size: 11px;
letter-spacing: 1px;
}
.module.active {
border-color: var(--gold);
color: var(--gold2);
background: #30291e;
}
.module-content {
min-height: 290px;
}
.module-heading {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.module-heading h2 {
font-size: 15px;
color: var(--gold2);
letter-spacing: 2px;
}
.carousel {
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
gap: 10px;
}
.arrow {
font-size: 23px;
height: 55px;
color: var(--gold);
}
.items {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.item {
min-height: 155px;
padding: 15px 8px;
border: 1px solid var(--line);
border-radius: 6px;
background: linear-gradient(145deg, #292e30, #171a1b);
text-align: center;
cursor: pointer;
}
.item:hover, .item.selected {
border-color: var(--gold);
background: #302a20;
}
.amp-graphic {
width: 85px;
height: 65px;
margin: 0 auto 12px;
border-radius: 5px;
border: 2px solid #777;
background: linear-gradient(#44494a, #191c1d);
display: flex;
align-items: center;
justify-content: center;
color: var(--gold2);
font-weight: bold;
font-size: 19px;
box-shadow: 0 4px 7px #0008;
}
.item-name {
font-size: 12px;
color: #dedbd3;
}
.item small {
display: block;
margin-top: 6px;
font-size: 10px;
color: var(--muted);
}
.effect-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.effect {
min-height: 145px;
background: #222729;
border: 1px solid var(--line);
border-radius: 6px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
}
.effect:hover, .effect.selected {
border-color: var(--gold);
background: #302a20;
}
.effect-icon {
font-size: 30px;
color: var(--gold2);
margin-bottom: 12px;
}
.effect-name {
font-size: 12px;
letter-spacing: 1px;
}
.right-panel h3 {
font-size: 14px;
color: var(--gold2);
letter-spacing: 1px;
margin-top: 0;
}
.detail-label {
font-size: 10px;
color: var(--muted);
letter-spacing: 1px;
margin: 20px 0 7px;
}
.detail-value {
background: #111516;
border: 1px solid var(--line);
padding: 10px;
border-radius: 4px;
font-size: 12px;
}
.control {
margin: 17px 0;
}
.control label {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #c7c7c2;
margin-bottom: 8px;
}
.control input {
width: 100%;
accent-color: var(--gold);
}
.note {
font-size: 10px;
color: var(--muted);
line-height: 1.5;
margin-top: 25px;
}
footer {
color: #777;
font-size: 10px;
padding: 12px 18px;
border-top: 1px solid var(--line);
text-align: center;
}
@media(max-width: 950px) {
main { grid-template-columns: 180px 1fr; }
.right-panel { grid-column: 1 / -1; }
}
@media(max-width: 650px) {
main { display: flex; flex-direction: column; }
.knobs { grid-template-columns: repeat(3, 1fr); }
.modules { grid-template-columns: repeat(2, 1fr); }
.items, .effect-grid { grid-template-columns: 1fr; }
header { padding: 0 12px; }
.toolbar { flex-wrap: wrap; }
}
</style>
</head>
<body>
<header>
<div class="logo">V-AMP <span>STUDIO</span></div>
<div class="status"><i></i> MODO LOCAL · MIDI NO CONECTADO</div>
<div class="toolbar">
<button onclick="newPreset()">NEW</button>
<button onclick="exportPreset()">EXPORT</button>
<button onclick="document.getElementById('importFile').click()">IMPORT</button>
<input id="importFile" type="file" accept=".json" hidden onchange="importPreset(event)">
</div>
</header>
<main>
<aside class="panel">
<div class="section-title">PRESET LIBRARY</div>
<div class="preset active" onclick="selectPreset(this,'New Preset')">01 · New Preset</div>
<div class="preset" onclick="selectPreset(this,'Clean Classic')">02 · Clean Classic</div>
<div class="preset" onclick="selectPreset(this,'Vintage Crunch')">03 · Vintage Crunch</div>
<div class="preset" onclick="selectPreset(this,'British Rock')">04 · British Rock</div>
<div class="preset" onclick="selectPreset(this,'Modern Lead')">05 · Modern Lead</div>
<div class="preset" onclick="selectPreset(this,'Studio Clean')">06 · Studio Clean</div>
<div style="margin-top:20px">
<button style="width:100%" onclick="newPreset()">+ NEW PRESET</button>
</div>
<p class="note">Los presets de esta versión son sesiones locales de trabajo. Todavía no representan memorias nativas del equipo.</p>
</aside>
<section class="center">
<div class="device">
<div class="device-title">
<h2>V-AMP CONTROL DE TONO</h2>
<span style="font-size:10px;color:#888">VIRTUAL CONTROL</span>
</div>
<div class="knobs" id="knobs"></div>
</div>
<div class="panel">
<div class="section-title">SIGNAL ARCHITECTURE</div>
<div class="modules">
<button class="module" data-module="PRE AMP" onclick="showModule('PRE AMP')">PRE AMP</button>
<button class="module active" data-module="AMP" onclick="showModule('AMP')">AMP</button>
<button class="module" data-module="CAB" onclick="showModule('CAB')">CAB</button>
<button class="module" data-module="POST AMP" onclick="showModule('POST AMP')">POST AMP</button>
</div>
</div>
<div class="panel module-content">
<div class="module-heading">
<h2 id="moduleTitle">AMP MODELS</h2>
<span style="font-size:10px;color:#777" id="moduleCounter">01 / 06</span>
</div>
<div id="moduleDisplay"></div>
</div>
</section>
<aside class="panel right-panel">
<div class="section-title">EDITING PANEL</div>
<h3 id="detailTitle">AMP MODEL</h3>
<div class="detail-label">SELECTED COMPONENT</div>
<div class="detail-value" id="detailName">American Clean</div>
<div id="detailControls"></div>
<p class="note">El panel contextual cambia según el módulo o efecto seleccionado. Los controles de esta versión modifican solamente la interfaz.</p>
</aside>
</main>
<footer>
V-AMP STUDIO · LOCAL PROTOTYPE · MIDI INTEGRATION PENDING MANUAL VALIDATION
</footer>
<script>
const knobData = [
["GAIN",50],["LOW",50],["MID",50],
["HIGH",50],["PRESENCE",50],["VOLUME",70]
];
const modules = {
"AMP": {
title:"AMP MODELS",
items:[
"American Clean","Black Twin","British Crunch",
"Classic Rock","Modern Lead","Vintage Drive"
],
type:"amp"
},
"CAB": {
title:"CABINET MODELS",
items:[
"1x8 Vintage","1x12 Open Back","2x12 Classic",
"2x12 Modern","4x12 Vintage","4x12 Modern"
],
type:"cab"
},
"PRE AMP": {
title:"PRE AMP EFFECTS",
items:["Compressor","Wah","Drive"],
type:"effect"
},
"POST AMP": {
title:"POST AMP EFFECTS",
items:["Modulation","Delay","Reverb"],
type:"effect"
}
};
let currentModule = "AMP";
let selectedItem = "American Clean";
let presetName = "New Preset";
let selectedIndex = 0;
function renderKnobs() {
const container = document.getElementById("knobs");
container.innerHTML = "";
knobData.forEach(([name,value],index) => {
const wrap = document.createElement("div");
wrap.className = "knob-wrap";
wrap.innerHTML = `
<div class="knob" id="knob${index}" style="--angle:${(value/100)*270-135}deg"></div>
<div class="knob-label">${name}</div>
<input type="range" min="0" max="100" value="${value}"
oninput="updateKnob(${index},this.value)">
<div class="knob-value" id="knobValue${index}">${value}</div>
`;
container.appendChild(wrap);
});
}
function updateKnob(index,value) {
knobData[index][1] = Number(value);
document.getElementById("knobValue"+index).textContent = value;
document.getElementById("knob"+index).style.setProperty(
"--angle",(value/100)*270-135+"deg"
);
}
function showModule(module) {
currentModule = module;
selectedIndex = 0;
selectedItem = modules[module].items[0];
document.querySelectorAll(".module").forEach(button => {
button.classList.toggle("active",button.dataset.module === module);
});
renderModule();
renderDetails();
}
function renderModule() {
const data = modules[currentModule];
document.getElementById("moduleTitle").textContent = data.title;
if(data.type === "effect") {
document.getElementById("moduleDisplay").innerHTML = `
<div class="effect-grid">
${data.items.map((item,index) => `
<div class="effect ${index===selectedIndex?'selected':''}"
onclick="selectItem(${index})">
<div class="effect-icon">${effectIcon(item)}</div>
<div class="effect-name">${item}</div>
</div>
`).join("")}
</div>
`;
} else {
const items = data.items;
const visible = [];
for(let i=0;i<3;i++) {
visible.push((selectedIndex+i)%items.length);
}
document.getElementById("moduleDisplay").innerHTML = `
<div class="carousel">
<button class="arrow" onclick="moveCarousel(-1)">‹</button>
<div class="items">
${visible.map(index => `
<div class="item ${index===selectedIndex?'selected':''}"
onclick="selectItem(${index})">
<div class="amp-graphic">${data.type==="amp"?"AMP":"CAB"}</div>
<div class="item-name">${items[index]}</div>
<small>${data.type==="amp"?"AMPLIFIER MODEL":"CABINET MODEL"}</small>
</div>
`).join("")}
</div>
<button class="arrow" onclick="moveCarousel(1)">›</button>
</div>
`;
}
document.getElementById("moduleCounter").textContent =
String(selectedIndex+1).padStart(2,"0")+" / "+
String(data.items.length).padStart(2,"0");
}
function effectIcon(item) {
const icons = {
"Compressor":"◉","Wah":"〰","Drive":"⚡",
"Modulation":"◌","Delay":"◷","Reverb":"〰"
};
return icons[item] || "◉";
}
function selectItem(index) {
selectedIndex = index;
selectedItem = modules[currentModule].items[index];
renderModule();
renderDetails();
}
function moveCarousel(direction) {
const length = modules[currentModule].items.length;
selectedIndex = (selectedIndex + direction + length) % length;
selectedItem = modules[currentModule].items[selectedIndex];
renderModule();
renderDetails();
}
function renderDetails() {
document.getElementById("detailTitle").textContent = currentModule;
document.getElementById("detailName").textContent = selectedItem;
const controls = document.getElementById("detailControls");
let names = ["LEVEL","TONE","MIX"];
if(selectedItem === "Compressor") names = ["SUSTAIN","ATTACK","LEVEL"];
if(selectedItem === "Wah") names = ["POSITION","RESONANCE","LEVEL"];
if(selectedItem === "Drive") names = ["DRIVE","TONE","LEVEL"];
if(selectedItem === "Delay") names = ["TIME","FEEDBACK","MIX"];
if(selectedItem === "Reverb") names = ["DECAY","TONE","MIX"];
if(selectedItem === "Modulation") names = ["RATE","DEPTH","MIX"];
controls.innerHTML = names.map((name,index) => `
<div class="control">
<label><span>${name}</span><span id="detailValue${index}">50</span></label>
<input type="range" min="0" max="100" value="50"
oninput="document.getElementById('detailValue${index}').textContent=this.value">
</div>
`).join("");
}
function selectPreset(element,name) {
document.querySelectorAll(".preset").forEach(p => p.classList.remove("active"));
element.classList.add("active");
presetName = name;
}
function newPreset() {
presetName = "New Preset";
document.querySelectorAll(".preset").forEach(p => p.classList.remove("active"));
alert("Nueva sesión iniciada. Puedes configurar tus controles.");
}
function exportPreset() {
const data = {
application:"V-AMP STUDIO",
format:"Local session prototype",
preset:presetName,
knobs:knobData,
module:currentModule,
selectedItem:selectedItem
};
const blob = new Blob([JSON.stringify(data,null,2)],{type:"application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = presetName.replaceAll(" ","_")+".json";
a.click();
URL.revokeObjectURL(url);
}
function importPreset(event) {
const file = event.target.files[0];
if(!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result);
if(!Array.isArray(data.knobs) || data.knobs.length !== knobData.length) {
throw new Error("Formato de sesión no reconocido");
}
data.knobs.forEach((entry,index) => {
knobData[index][1] = Number(entry[1]);
});
presetName = data.preset || "Imported Preset";
renderKnobs();
if(modules[data.module]) {
showModule(data.module);
const index = modules[data.module].items.indexOf(data.selectedItem);
if(index >= 0) selectItem(index);
}
alert("Sesión importada correctamente.");
} catch(error) {
alert("No se pudo importar: "+error.message);
}
};
reader.readAsText(file);
event.target.value = "";
}
renderKnobs();
renderModule();
renderDetails();
</script>
</body>
</html>
Guardar como index.html para probar algunas cosillas...no es otra cosa...
2
Responder
Citar
