diff --git a/config.rb b/config.rb deleted file mode 100644 index 8e6931f..0000000 --- a/config.rb +++ /dev/null @@ -1,11 +0,0 @@ -# Require any additional compass plugins here. - -# Set this to the root of your project when deployed: -http_path = "/" -css_dir = "css" -sass_dir = "sass" -images_dir = "images" -javascripts_dir = "js" - -# You can select your preferred output style here (can be overridden via the command line): -output_style = :expanded or :nested or :compact or :compressed diff --git a/index.html b/index.html index f089467..39ade40 100644 --- a/index.html +++ b/index.html @@ -60,7 +60,6 @@
  • C
  • - diff --git a/js/main.js b/js/main.js index ad255cf..9c277ac 100644 --- a/js/main.js +++ b/js/main.js @@ -1,184 +1,659 @@ -window.onload = function () { - MIDI.loadPlugin({ - soundfontUrl: "./soundfont/", - instrument: "acoustic_grand_piano", - callback: function() { - $(window).trigger('ready'); //trigger an event to know when the plugin is loaded. - } - }); +// Constants +var DELAY = 0; // play one note every quarter second +var VELOCITY = 127; // how hard the note hits +var ACTIVE_FEEDBACK_DURATION = 200; // milliseconds +var NOTE_DURATION = 750; // milliseconds - how long a note plays +var NOTE_OFF_DELAY = 0.75; // seconds - delay before note stops + +// Cached DOM elements and key mappings +var pianoElement = null; +var keyElementsMap = new Map(); // Maps note number to DOM element +var activeNotes = new Set(); // Track currently playing notes to prevent duplicates +var pressedKeys = new Set(); // Track currently pressed keyboard keys to prevent repeat +var activeTimeouts = new Map(); // Track timeouts for cleanup + +// Web MIDI API support +var midiAccess = null; +var midiInputs = new Map(); // Track connected MIDI input devices +var midiDeviceStatus = { + supported: false, + connected: false, + deviceCount: 0 }; -$(window).on('ready', function() { //here we are listening for the ready event. +// Keyboard mapping: lowercase key -> MIDI note (normalized in parseAction) +var KEYBOARD_MAP = { + 'q': 60, // C4 + 'w': 62, // D4 + 'e': 64, // E4 + 'r': 65, // F4 + 't': 67, // G4 + 'y': 69, // A4 + 'u': 71, // B4 + 'i': 72 // C5 +}; + +/** + * Validate MIDI note number + * @param {number} note - The MIDI note number + * @returns {boolean} True if valid + */ +function isValidNote(note) { + return !isNaN(note) && note >= 0 && note <= 127; +} + +/** + * Clear a timeout and remove it from tracking + * @param {number} note - The note associated with the timeout + */ +function clearNoteTimeout(note) { + if (activeTimeouts.has(note)) { + clearTimeout(activeTimeouts.get(note)); + activeTimeouts.delete(note); + } +} + +/** + * Initialize the piano - cache DOM elements and set up event handlers + */ +function initializePiano() { + pianoElement = document.getElementById('piano'); + + if (!pianoElement) { + console.error('Piano element not found'); + return; + } + + // Cache all key elements in a Map for O(1) lookup + var allKeys = document.querySelectorAll('[data-note]'); + allKeys.forEach(function(keyElement) { + var note = parseInt(keyElement.dataset.note, 10); + if (isValidNote(note)) { + keyElementsMap.set(note, keyElement); + } + }); + + // Set up event handlers assignHandlers(); -}); +} + +/** + * Get the note number from a clicked/touched element + * @param {HTMLElement} target - The clicked/touched element + * @returns {number|null} The MIDI note number or null + */ +function getNoteFromElement(target) { + if (!target || !target.dataset) { + return null; + } + + // Check if clicked element has data-note + if (target.dataset.note) { + return parseInt(target.dataset.note, 10); + } + + // Check if clicked element is inside an anchor or black_key + var anchor = target.closest('.anchor, .black_key'); + if (anchor && anchor.dataset && anchor.dataset.note) { + return parseInt(anchor.dataset.note, 10); + } + + // Check parent li for anchor or black_key + var li = target.closest('li'); + if (li) { + var keyElement = li.querySelector('.anchor, .black_key'); + if (keyElement && keyElement.dataset && keyElement.dataset.note) { + return parseInt(keyElement.dataset.note, 10); + } + } + + return null; +} + +/** + * Add visual feedback to a key element (CSS transitions handle the animation) + * @param {HTMLElement} keyElement - The key element to highlight + */ +function addKeyFeedback(keyElement) { + if (keyElement) { + keyElement.classList.add('active'); + // CSS transition handles the visual feedback + // Remove class after duration for cleanup + setTimeout(function() { + if (keyElement) { + keyElement.classList.remove('active'); + } + }, ACTIVE_FEEDBACK_DURATION); + } +} + +/** + * Play a note with proper cleanup + * @param {number} note - The MIDI note number + * @param {number} velocity - The velocity (0-127), defaults to VELOCITY constant + */ +function playNoteInternal(note, velocity) { + if (!isValidNote(note)) { + console.warn('Invalid MIDI note:', note); + return; + } + + // Use provided velocity or default + var noteVelocity = (velocity !== undefined && velocity >= 0 && velocity <= 127) ? velocity : VELOCITY; + + // Clear any existing timeout for this note + clearNoteTimeout(note); + + MIDI.setVolume(0, 127); + MIDI.noteOn(0, note, noteVelocity, DELAY); + MIDI.noteOff(0, note, DELAY + NOTE_OFF_DELAY); + + // Track timeout for cleanup + var timeoutId = setTimeout(function() { + activeNotes.delete(note); + activeTimeouts.delete(note); + }, NOTE_DURATION); + + activeTimeouts.set(note, timeoutId); +} + +/** + * Handle piano key clicks + * @param {Event} event - The click event + */ +function handlePianoClick(event) { + var note = getNoteFromElement(event.target); + + if (note && isValidNote(note)) { + // Prevent duplicate triggers for the same note + if (!activeNotes.has(note)) { + activeNotes.add(note); + + // Get cached key element + var keyElement = keyElementsMap.get(note); + addKeyFeedback(keyElement); + + playNoteInternal(note); + } + } +} + +/** + * Handle touch events for mobile devices + * @param {TouchEvent} event - The touch event + */ +function handleTouchStart(event) { + event.preventDefault(); // Prevent scrolling + var touch = event.touches[0] || event.changedTouches[0]; + if (touch) { + var target = document.elementFromPoint(touch.clientX, touch.clientY); + var note = getNoteFromElement(target); + + if (note && isValidNote(note)) { + // Prevent duplicate triggers for the same note + if (!activeNotes.has(note)) { + activeNotes.add(note); + + // Get cached key element + var keyElement = keyElementsMap.get(note); + addKeyFeedback(keyElement); + + playNoteInternal(note); + } + } + } +} /** * @method assignHandlers creates the click, keydown and keyup event handlers when the font is loaded */ function assignHandlers() { - $('#piano').on('click', function(event) { - var note = $(event.target).data('note'); - playNote(note); - }); - $(document).on('keydown', parseAction); - $(document).on('keyup', releaseAction); + if (!pianoElement) { + console.error('Piano element not found'); + return; + } + + // Handle clicks on piano keys (both white and black keys) + pianoElement.addEventListener('click', handlePianoClick); + + // Handle touch events for mobile devices + pianoElement.addEventListener('touchstart', handleTouchStart, { passive: false }); + + // Keyboard event handlers + document.addEventListener('keydown', parseAction); + document.addEventListener('keyup', releaseAction); } /** - * @method method to execute whenever the user triggers a keyup event. - * @param event + * @method releaseAction executes whenever the user triggers a keyup event. + * @param {KeyboardEvent} event */ function releaseAction(event) { - $(".anchor").removeClass('active'); //make the piano keys look like they're being pressed when user is using a keyboard + var key = event.key ? event.key.toLowerCase() : null; + + // Only process if it's a mapped key + if (key && key in KEYBOARD_MAP) { + pressedKeys.delete(key); + + // Remove active class from the specific key element + var note = KEYBOARD_MAP[key]; + var keyElement = keyElementsMap.get(note); + if (keyElement) { + keyElement.classList.remove('active'); + } + + // Remove from active notes set + activeNotes.delete(note); + clearNoteTimeout(note); + } } /** - * @method parseAction handles keydown events by detecting the user's event keycode and playing the proper note. - * @param event + * @method parseAction handles keydown events by detecting the user's key and playing the proper note. + * @param {KeyboardEvent} event */ function parseAction(event) { - var keycode = event.keyCode; - - switch (keycode) { - case 81: - triggerAction(60) - break; - case 87: - triggerAction(62) - break; - case 69: - triggerAction(64); - break; - case 82: - triggerAction(65); - break; - case 84: - triggerAction(67); - break; - case 89: - triggerAction(69); - break; - case 85: - triggerAction(71); - break; - case 73: - triggerAction(72); - break; - } -} - -/** - * @method triggerAction method to trigger UI change to make the key look pressed and to play the note. - * @param note + // Normalize key to lowercase + var key = event.key ? event.key.toLowerCase() : null; + + // Prevent browser shortcuts and handle key repeat + if (key && key in KEYBOARD_MAP) { + event.preventDefault(); + + // Prevent key repeat - only trigger if key wasn't already pressed + if (!pressedKeys.has(key)) { + pressedKeys.add(key); + + var note = KEYBOARD_MAP[key]; + triggerAction(note); + } + } +} + +/** + * @method triggerAction triggers UI change to make the key look pressed and to play the note. + * @param {number} note - The MIDI note number */ function triggerAction(note) { - $(".anchor[data-note="+note+"]").addClass('active'); - playAugmented(note); + if (!isValidNote(note)) { + return; + } + + // Get cached key element + var keyElement = keyElementsMap.get(note); + if (keyElement) { + addKeyFeedback(keyElement); + } + + // Prevent duplicate triggers + if (!activeNotes.has(note)) { + activeNotes.add(note); + playNoteInternal(note); + } } -var delay = 0; // play one note every quarter second -var velocity = 127; // how hard the note hits - /** - * @method playNote plays the note. - * @param note the midi number of the key the user wants to press. + * @method playNote plays a single note (public API). + * @param {number} note - The MIDI note number (0-127) */ function playNote(note) { - MIDI.setVolume(0, 127); - MIDI.noteOn(0, note, velocity, delay); - MIDI.noteOff(0, note, delay + 0.75); + if (!isValidNote(note)) { + console.warn('Invalid MIDI note:', note); + return; + } + + // Get cached key element for visual feedback + var keyElement = keyElementsMap.get(note); + if (keyElement) { + addKeyFeedback(keyElement); + } + + // Prevent duplicate triggers + if (!activeNotes.has(note)) { + activeNotes.add(note); + playNoteInternal(note); + } } -function multinotes() { - notes = arguments; - MIDI.noteOn(0, root, velocity, delay); - MIDI.noteOn(0, third, velocity, delay); - MIDI.noteOn(0, fifth, velocity, delay); +/** + * @method multinotes plays multiple notes simultaneously (for chords). + * @param {number} root - The root note + * @param {number} third - The third note + * @param {number} fifth - The fifth note + */ +function multinotes(root, third, fifth) { + // Validate all notes + if (!isValidNote(root) || !isValidNote(third) || !isValidNote(fifth)) { + console.warn('Invalid MIDI note in chord:', root, third, fifth); + return; + } + + MIDI.noteOn(0, root, VELOCITY, DELAY); + MIDI.noteOn(0, third, VELOCITY, DELAY); + MIDI.noteOn(0, fifth, VELOCITY, DELAY); } /** - * @playRootMajor play the major chord in the root position - * @param note + * @method playRootMajor plays the major chord in the root position + * @param {number} note */ function playRootMajor(note) { - var root = note, - third = note + 4, - fifth = note +7; + if (!isValidNote(note)) return; + var root = note; + var third = note + 4; + var fifth = note + 7; multinotes(root, third, fifth); } /** - * playFirstMajorInversion play the major chord in the first inversion - * @param note + * @method playFirstMajorInversion plays the major chord in the first inversion + * @param {number} note */ function playFirstMajorInversion(note) { - var root = note+ 4, - third = root+ 3, - fifth = root+5; + if (!isValidNote(note)) return; + var root = note + 4; + var third = root + 3; + var fifth = root + 5; multinotes(root, third, fifth); } /** - * @playSecondMajorInversion play the major chord in teh second inversion - * @param note + * @method playSecondMajorInversion plays the major chord in the second inversion + * @param {number} note */ function playSecondMajorInversion(note) { - var root = note+ 7, - third = root+ 5, - fifth = root+4; + if (!isValidNote(note)) return; + var root = note + 7; + var third = root + 5; + var fifth = root + 4; multinotes(root, third, fifth); } /** - * @method playRootMinor play the minor chord in the root position - * @param note + * @method playRootMinor plays the minor chord in the root position + * @param {number} note */ function playRootMinor(note) { - var root = note, - third = note + 3, - fifth = note +7; + if (!isValidNote(note)) return; + var root = note; + var third = note + 3; + var fifth = note + 7; multinotes(root, third, fifth); } /** - * @method playFirstMinorInversion play the minor chord in the 1st inversion - * @param note + * @method playFirstMinorInversion plays the minor chord in the 1st inversion + * @param {number} note */ function playFirstMinorInversion(note) { - var root = note+ 3, - third = root+ 4, - fifth = root+5; + if (!isValidNote(note)) return; + var root = note + 3; + var third = root + 4; + var fifth = root + 5; multinotes(root, third, fifth); } /** - * @method playSecondMinorInversion play the minor chord in the 2nd inversion - * @param note + * @method playSecondMinorInversion plays the minor chord in the 2nd inversion + * @param {number} note */ function playSecondMinorInversion(note) { - var root = note+ 7, - third = root+ 5, - fifth = root+3; + if (!isValidNote(note)) return; + var root = note + 7; + var third = root + 5; + var fifth = root + 3; multinotes(root, third, fifth); } +/** + * @method playAugmented plays an augmented chord + * @param {number} note + */ function playAugmented(note) { - var root = note, - third = note + 4, - fifth = root + 8; + if (!isValidNote(note)) return; + var root = note; + var third = note + 4; + var fifth = root + 8; multinotes(root, third, fifth); } +/** + * @method playDiminished plays a diminished chord + * @param {number} note + */ function playDiminished(note) { - var root = note, - third = note + 3, - fifth = note + 3; + if (!isValidNote(note)) return; + var root = note; + var third = note + 3; + var fifth = note + 6; // Fixed: was note + 3, should be note + 6 for diminished fifth multinotes(root, third, fifth); } +/** + * @method playSuspended plays a suspended chord + * @param {number} note + */ function playSuspended(note) { - var root = note, - third = note + 5, - fifth = note + 7; + if (!isValidNote(note)) return; + var root = note; + var third = note + 5; + var fifth = note + 7; multinotes(root, third, fifth); -} \ No newline at end of file +} + +/** + * Show error message to user + * @param {string} message - Error message to display + */ +function showError(message) { + console.error(message); + // You could add a visual error message here + // var errorDiv = document.createElement('div'); + // errorDiv.className = 'error-message'; + // errorDiv.textContent = message; + // document.body.appendChild(errorDiv); +} + +/** + * Update MIDI device status indicator + */ +function updateMIDIStatus() { + var statusElement = document.getElementById('midi-status'); + if (!statusElement) { + // Create status element if it doesn't exist + statusElement = document.createElement('div'); + statusElement.id = 'midi-status'; + statusElement.style.cssText = 'position: fixed; top: 10px; right: 10px; padding: 8px 12px; background: rgba(0,0,0,0.7); color: white; border-radius: 4px; font-size: 12px; z-index: 10000; font-family: Arial, sans-serif;'; + document.body.appendChild(statusElement); + } + + if (!midiDeviceStatus.supported) { + statusElement.textContent = 'MIDI: Not supported'; + statusElement.style.background = 'rgba(200,0,0,0.7)'; + } else if (midiDeviceStatus.connected && midiDeviceStatus.deviceCount > 0) { + statusElement.textContent = 'MIDI: ' + midiDeviceStatus.deviceCount + ' device(s) connected'; + statusElement.style.background = 'rgba(0,150,0,0.7)'; + } else { + statusElement.textContent = 'MIDI: No devices connected'; + statusElement.style.background = 'rgba(150,150,0,0.7)'; + } +} + +/** + * Handle MIDI message from physical keyboard + * @param {MIDIMessageEvent} event - MIDI message event + */ +function handleMIDIMessage(event) { + var data = event.data; + var command = data[0] & 0xf0; // Upper nibble is command + var channel = data[0] & 0x0f; // Lower nibble is channel (we ignore for now) + var note = data[1]; + var velocity = data[2]; + + // Note On (0x90) or Note Off (0x80) + if (command === 0x90 && velocity > 0) { + // Note On + if (isValidNote(note)) { + // Prevent duplicate triggers + if (!activeNotes.has(note)) { + activeNotes.add(note); + + // Get cached key element for visual feedback + var keyElement = keyElementsMap.get(note); + if (keyElement) { + addKeyFeedback(keyElement); + } + + // Play note with velocity from MIDI keyboard + playNoteInternal(note, velocity); + } + } + } else if (command === 0x80 || (command === 0x90 && velocity === 0)) { + // Note Off + if (isValidNote(note)) { + // Remove from active notes + activeNotes.delete(note); + clearNoteTimeout(note); + + // Remove visual feedback + var keyElement = keyElementsMap.get(note); + if (keyElement) { + keyElement.classList.remove('active'); + } + } + } +} + +/** + * Handle MIDI input device connection + * @param {MIDIInput} input - MIDI input device + */ +function handleMIDIInputConnected(input) { + console.log('MIDI input connected:', input.name, input.manufacturer); + + // Set up message handler + input.onmidimessage = handleMIDIMessage; + + // Track the input + midiInputs.set(input.id, input); + + // Update status + midiDeviceStatus.connected = true; + midiDeviceStatus.deviceCount = midiInputs.size; + updateMIDIStatus(); +} + +/** + * Handle MIDI input device disconnection + * @param {MIDIInput} input - MIDI input device + */ +function handleMIDIInputDisconnected(input) { + console.log('MIDI input disconnected:', input.name); + + // Remove message handler + input.onmidimessage = null; + + // Remove from tracking + midiInputs.delete(input.id); + + // Update status + midiDeviceStatus.connected = midiInputs.size > 0; + midiDeviceStatus.deviceCount = midiInputs.size; + updateMIDIStatus(); +} + +/** + * Initialize Web MIDI API support + */ +function initializeMIDI() { + // Check if Web MIDI API is supported + if (!navigator.requestMIDIAccess) { + console.warn('Web MIDI API is not supported in this browser'); + midiDeviceStatus.supported = false; + updateMIDIStatus(); + return; + } + + midiDeviceStatus.supported = true; + + // Request MIDI access + navigator.requestMIDIAccess({ sysex: false }) + .then(function(access) { + midiAccess = access; + + // Handle state changes (devices connecting/disconnecting) + access.onstatechange = function(event) { + if (event.port.state === 'connected' && event.port.type === 'input') { + handleMIDIInputConnected(event.port); + } else if (event.port.state === 'disconnected' && event.port.type === 'input') { + handleMIDIInputDisconnected(event.port); + } + }; + + // Connect to existing inputs + var inputs = access.inputs.values(); + for (var input = inputs.next(); input && !input.done; input = inputs.next()) { + if (input.value.state === 'connected') { + handleMIDIInputConnected(input.value); + } + } + + updateMIDIStatus(); + }) + .catch(function(error) { + console.error('Error accessing MIDI devices:', error); + showError('Failed to access MIDI devices: ' + error.message); + midiDeviceStatus.supported = false; + updateMIDIStatus(); + }); +} + +/** + * Initialize when DOM is ready + */ +document.addEventListener('DOMContentLoaded', function() { + // Initialize Web MIDI API for physical keyboard support + initializeMIDI(); + + try { + MIDI.loadPlugin({ + soundfontUrl: "./soundfont/", + instrument: "acoustic_grand_piano", + callback: function() { + // Trigger custom event to know when the plugin is loaded + window.dispatchEvent(new Event('ready')); + }, + onerror: function(error) { + showError('Failed to load MIDI plugin: ' + (error || 'Unknown error')); + } + }); + } catch (error) { + showError('Error initializing MIDI: ' + error.message); + } +}); + +// Set up handlers when MIDI plugin is ready +window.addEventListener('ready', function() { + try { + initializePiano(); + } catch (error) { + showError('Error initializing piano: ' + error.message); + } +}); + +// Cleanup on page unload +window.addEventListener('beforeunload', function() { + // Clear all timeouts + activeTimeouts.forEach(function(timeoutId) { + clearTimeout(timeoutId); + }); + activeTimeouts.clear(); + activeNotes.clear(); + pressedKeys.clear(); + + // Disconnect MIDI inputs + midiInputs.forEach(function(input) { + input.onmidimessage = null; + }); + midiInputs.clear(); +}); diff --git a/sass/main.scss b/sass/main.scss deleted file mode 100644 index 7b32d6f..0000000 --- a/sass/main.scss +++ /dev/null @@ -1,9 +0,0 @@ -/* Welcome to Compass. - * In this file you should write your main styles. (or centralize your imports) - * Import this file using the following HTML or equivalent: - * */ - -@import "compass/reset"; - -/* MODULES */ -@import "modules/piano"; diff --git a/sass/modules/_piano.scss b/sass/modules/_piano.scss deleted file mode 100644 index f39a1ef..0000000 --- a/sass/modules/_piano.scss +++ /dev/null @@ -1,256 +0,0 @@ -/* Piano Wrapper */ - -#p-wrapper { - background: #000; - background: -webkit-linear-gradient(-60deg, black, #333333, black, #666666, #333333 70%); - background: -moz-linear-gradient(-60deg, black, #333333, black, #666666, #333333 70%); - background: -ms-linear-gradient(-60deg, black, #333333, black, #666666, #333333 70%); - background: -o-linear-gradient(-60deg, black, #333333, black, #666666, #333333 70%); - background: linear-gradient(-60deg, black, #333333, black, #666666, #333333 70%); - width: 100%; - position: relative; - -webkit-box-shadow: 0 2px 0px #666666, 0 3px 0px #555555, 0 4px 0px #444444, 0 6px 6px black, inset 0 -1px 1px rgba(255, 255, 255, 0.5), inset 0 -4px 5px black; - -moz-box-shadow: 0 2px 0px #666666, 0 3px 0px #555555, 0 4px 0px #444444, 0 6px 6px black, inset 0 -1px 1px rgba(255, 255, 255, 0.5), inset 0 -4px 5px black; - box-shadow: 0 2px 0px #666666, 0 3px 0px #555555, 0 4px 0px #444444, 0 6px 6px black, inset 0 -1px 1px rgba(255, 255, 255, 0.5), inset 0 -4px 5px black; - border: 2px solid #333; - -webkit-border-radius: 0 0 5px 5px; - -moz-border-radius: 0 0 5px 5px; - border-radius: 0 0 5px 5px; - -webkit-animation: taufik 2s; - -moz-animation: taufik 2s; - animation: taufik 2s; - margin: 0 auto; -} - -/* Tuts */ - -ul#piano { - display: block; - width: 1560px; - height: 240px; - border-top: 2px solid #222; - margin: 0 auto; - li { - list-style: none; - float: left; - display: inline; - width: 30px; - position: relative; - text-align: center; - color: #fff; - font-size: 10px; - font-family: arial, sans-serif; - line-height: 17px; - a, div.anchor { - display: block; - height: 220px; - background: #fff; - background: -webkit-linear-gradient(-30deg, whitesmoke, white); - background: -moz-linear-gradient(-30deg, whitesmoke, white); - background: -ms-linear-gradient(-30deg, whitesmoke, white); - background: -o-linear-gradient(-30deg, whitesmoke, white); - background: linear-gradient(-30deg, whitesmoke, white); - border: 1px solid #ccc; - -webkit-box-shadow: inset 0 1px 0px white, inset 0 -1px 0px white, inset 1px 0px 0px white, inset -1px 0px 0px white, 0 4px 3px rgba(0, 0, 0, 0.7); - -moz-box-shadow: inset 0 1px 0px white, inset 0 -1px 0px white, inset 1px 0px 0px white, inset -1px 0px 0px white, 0 4px 3px rgba(0, 0, 0, 0.7); - box-shadow: inset 0 1px 0px white, inset 0 -1px 0px white, inset 1px 0px 0px white, inset -1px 0px 0px white, 0 4px 3px rgba(0, 0, 0, 0.7); - -webkit-border-radius: 0 0 3px 3px; - -moz-border-radius: 0 0 3px 3px; - border-radius: 0 0 3px 3px; - } - a:active, div.anchor:active, div.active { - -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.4); - -moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.4); - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.4); - position: relative; - top: 2px; - height: 216px; - } - a:active:before, div.anchor:active:before, div.active:before { - content: ""; - width: 0px; - height: 0px; - border-width: 216px 5px 0px; - border-style: solid; - border-color: transparent transparent transparent rgba(0, 0, 0, 0.1); - position: absolute; - left: 0px; - top: 0px; - } - a:active:after, div.anchor:active:after { - content: ""; - width: 0px; - height: 0px; - border-width: 216px 5px 0px; - border-style: solid; - border-color: transparent rgba(0, 0, 0, 0.1) transparent transparent; - position: absolute; - right: 0px; - top: 0px; - } - span { - position: absolute; - top: 0px; - left: -12px; - width: 20px; - height: 120px; - background: #333; - background: -webkit-linear-gradient(-20deg, #333333, black, #333333); - background: -moz-linear-gradient(-20deg, #333333, black, #333333); - background: -ms-linear-gradient(-20deg, #333333, black, #333333); - background: -o-linear-gradient(-20deg, #333333, black, #333333); - background: linear-gradient(-20deg, #333333, black, #333333); - z-index: 10; - border-width: 1px 2px 7px; - border-style: solid; - border-color: #666 #222 #111 #555; - -webkit-box-shadow: inset 0px -1px 2px rgba(255, 255, 255, 0.4), 0 2px 3px rgba(0, 0, 0, 0.4); - -moz-box-shadow: inset 0px -1px 2px rgba(255, 255, 255, 0.4), 0 2px 3px rgba(0, 0, 0, 0.4); - box-shadow: inset 0px -1px 2px rgba(255, 255, 255, 0.4), 0 2px 3px rgba(0, 0, 0, 0.4); - -webkit-border-radius: 0 0 2px 2px; - -moz-border-radius: 0 0 2px 2px; - border-radius: 0 0 2px 2px; - &:active { - border-bottom-width: 2px; - height: 123px; - -webkit-box-shadow: inset 0px -1px 1px rgba(255, 255, 255, 0.4), 0 1px 0px rgba(0, 0, 0, 0.8), 0 2px 2px rgba(0, 0, 0, 0.4), 0 -1px 0px black; - -moz-box-shadow: inset 0px -1px 1px rgba(255, 255, 255, 0.4), 0 1px 0px rgba(0, 0, 0, 0.8), 0 2px 2px rgba(0, 0, 0, 0.4), 0 -1px 0px black; - box-shadow: inset 0px -1px 1px rgba(255, 255, 255, 0.4), 0 1px 0px rgba(0, 0, 0, 0.8), 0 2px 2px rgba(0, 0, 0, 0.4), 0 -1px 0px black; - } - } - b { - position: absolute; - top: 0px; - margin-top: -10px; - background: #111; - color: #fff; - font: bold 14px 'Trebuchet MS',Arial,Sans-Serif; - border: 2px solid #e6e6e6; - -webkit-border-radius: 7px; - -moz-border-radius: 7px; - border-radius: 7px; - width: 100px; - height: 30px; - padding: 10px; - left: -40px; - z-index: 100; - visibility: hidden; - opacity: 0; - -webkit-box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); - -moz-box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); - -webkit-transition: all 0.2s ease-out; - -moz-transition: all 0.2s ease-out; - -ms-transition: all 0.2s ease-out; - -o-transition: all 0.2s ease-out; - -transition: all 0.2s ease-out; - &:before { - content: ""; - display: block; - position: absolute; - top: 100%; - left: 50px; - border-width: 8px; - border-style: solid; - border-color: #e6e6e6 transparent transparent transparent; - } - &:after { - content: ""; - display: block; - position: absolute; - top: 100%; - left: 53px; - border-width: 5px; - border-style: solid; - border-color: #111 transparent transparent transparent; - } - } - &:hover b { - visibility: visible; - opacity: 1; - margin-top: 10px; - } - ul { - position: absolute; - border: 2px solid #e6e6e6; - margin-top: -100px; - top: 100%; - left: 0px; - z-index: 1000; - visibility: hidden; - opacity: 0; - -webkit-box-shadow: 0 2px 7px #000; - -moz-box-shadow: 0 2px 7px #000; - box-shadow: 0 2px 7px #000; - -webkit-transition: all 0.2s ease-out 0.2s; - -moz-transition: all 0.2s ease-out 0.2s; - -ms-transition: all 0.2s ease-out 0.2s; - -o-transition: all 0.2s ease-out 0.2s; - transition: all 0.2s ease-out 0.2s; - } - } -} -ul#piano li { - li { - width: 150px; - height: auto; - display: block; - float: none; - background: transparent; - a { - height: auto; - display: block; - padding: 10px 15px; - background: #333; - font: normal 12px Arial,Sans-Serif; - color: #fff; - text-decoration: none; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - border-radius: 0px; - -webkit-border-radius: 0px; - -moz-border-radius: 0px; - border-width: 1px 0; - border-style: solid; - border-color: #444 transparent #222 transparent; - top: 0px; - margin-top: 0px; - &:active { - height: auto; - display: block; - padding: 10px 15px; - background: #333; - font: normal 12px Arial,Sans-Serif; - color: #fff; - text-decoration: none; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - border-radius: 0px; - -webkit-border-radius: 0px; - -moz-border-radius: 0px; - border-width: 1px 0; - border-style: solid; - border-color: #444 transparent #222 transparent; - top: 0px; - margin-top: 0px; - &:before, &:after { - border: none !important; - } - } - } - } - &:hover { - ul, #search, #contact { - visibility: visible; - opacity: 1; - margin-top: 15px; - } - } - li a:hover { - background: #111; - border-top-color: #222; - border-bottom-color: #000; - } -} \ No newline at end of file