Drop Shadow in puro Javascript (dhtml)

Un altro articolo su come ottenere un effetto drop shadow.

Effettivamente basta una ricerca su a list apart per scoprire tutto o quasi quello che c'è da sapere riguardo l'argomento. Vi sono però alcune considerazioni da fare:

  1. alcuni metodi prevedono l'introduzione di nuovi tag (e quindi markup (x)html non necessario)
  2. altri escamotage risultano poco riutilizzabili; il codice dei CSS deve essere rivisto a seconda del contesto in cui si sta utilizzando l'effetto
  3. sono necessarie delle immagini da disporre dietro ai div
  4. risultano poco immediati al web master neofita

Perchè non pensare allora un metodo universale, cross browser, che scali bene su client che non supportano javascript e/o CSS e che rimanga allo stesso tempo il più semplice possibile ? La risposta a questa domanda si realizza nell'implementazione di alcune semplici funzioni javascript che si occupano di ricreare l'effetto drop shadow attorno all'elemento passato loro come parametro (un oggetto DOM che può quindi rappresentare un div o un'immagine).

Teoria e pratica

Il metodo che sicuramente salta alla mente per primo consiste nel disporre due bordi grigi a destra e sotto l'elemento al quale si vuole applicare l'effetto. Nell'ordine la funzione dovrà quindi

  1. Ottenere la posizione assoluta del div
  2. Ottenere la dimensione del div
  3. Creare un altro div con i due bordi grigi (bottom e right) che non si sovrapponga al div di partenza e abbia un piccolo sfasamento orizzontale e verticale

Vediamo quindi il codice


function dropShadow(eid) {

  if(!document.getElementById || !document.getElementsByTagName || !document.createElement) return;

  var obj = document.getElementById(eid);

  if(!obj|| !obj.offsetParent) return;

  /* riferimento al body del documento */
  var body = document.getElementsByTagName('body')[0];

  var left = 0, top = 0;

  /* ottiene la posizione assoluta del div */

  /* distanza dal bordo sinistro della pagina */
  for (var tmpObj = obj; tmpObj.offsetParent; tmpObj = tmpObj.offsetParent)
    left += tmpObj.offsetLeft;

  /* distanza dal bordo superiore */
  for (var tmpObj = obj; tmpObj.offsetParent; tmpObj = tmpObj.offsetParent)
    top += tmpObj.offsetTop;

  /* spessore dell'ombra ovvero dei bordi del div che andremo ad aggiungere */
  var x1 = 2;

  /* crea il nuovo div */
  var ombra   = document.createElement('div');

  /* viene assegnato lo stile al div ombra in modo che si affianchi al div obiettivo */

  /* posizione */
  ombra.style.position      = 'absolute';
  ombra.style.left          = left + x1 + 'px';
  ombra.style.top           = top + x1 + 'px';

  /* dimensioni */
  ombra.style.height        = obj.offsetHeight - x1 + 'px';
  ombra.style.width         = obj.offsetWidth  - x1 + 'px';

  /* bordi (l'ombra effettiva) */
  ombra.style.borderRight   = x1 + 'px solid #ccc';
  ombra.style.borderBottom  = x1 + 'px solid #ccc';

  /* appende il div creato al corpo del documento */
  if(body.appendChild) body.appendChild(ombra);
}

Una volta compreso il meccanismo il codice si rivela estremamente semplice. In realtà si potrebbe obiettare come a seconda del box model utilizzato dall'uno o dall'altro browser i due div potrebbero effettivamente non risultare affiancati.

Problemi con il posizionamento dei div e possibili soluzioni

Personalmente ritengo che la strada da intraprendere sia quella di evitare CSS hacks vari (per quanto possibile, e tutti sappiamo che spesso e volentieri non lo è). Ritengo inoltre che la scelta più ragionevole consista nel fornire un DOCTYPE html strict al documento (e possibilmente attenersi ad esso :) in modo che Explorer assuma il box model ideato dal w3c. C'è da aggiungere che il doctype switching è supportato (AFAIK) solo da versioni di Explorer superiori alla 5.5.

Un'altra possibilità consiste nel cambiare il box model di mozilla/firefox per adattarlo a quello di IE utilizzando gli elementi del CSS3 ma, poichè l'argomento esula dall'ambito di questo articolo, si rimanda alla pubblicazione su quirksmode.org.

Un bug riscontrato su Explorer fa si che sia calcolata erroneamente la posizione degli elementi static (non relative nè absolute quindi) se questi sono posizionati in contenitori che possiedono un margine o un padding. Al solito le possibilità si dividono tra il gestire questa situzione direttamente nel codice javascript o nel porre margini e padding dell'elemento contenitore a zero (soluzione adottata negli esempi).

Ricapitolando: per far funzionare il tutto al meglio anche su Explorer bisogna inserire all'inizio del documento un DOCTYPE del tipo


<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">

e azzerare i margini del body se si intendono utilizzare div "ombreggiati" con position: static


html body { padding: 0; margin: 0; }

Vediamo a questo punto un primo esempio di drop shadow applicato a un div con padding, bordo e margini.

Considerare il ridimensionamento della finestra

Un'interessante considerazione va fatta sulla possibilità che il div al quale applichiamo l'effetto venga ridimensionato. Se le misure di altezza e larghezza (ma anche di posizione) del div target sono infatti espresse in percentuale, sarà necessario trovare un meccanismo per adattare i div ombra nel caso la pagina cambi dimensione. Tale meccanismo può essere implementato tramite l'evento onresize dell'oggetto window:


function dropShadow( ... ) {
  ...
  var oldRef = window.onresize;
  window.onresize = function() { 
    if(oldRef != null && typeof oldRef != 'undefined')  
      oldRef();
    resizeShadow( ... ); 
  };
  ...
}

La funzione resizeShadow() coincide con la parte di codice preposta a calcolare posizione e dimensioni del div ombra. Avremmo quindi due funzioni, la prima (dropShadow) si occuperà di creare i div necessari, chiamare il metodo resizeShadow e assegnare quest'ultimo come handler per l'evento onresize; la seconda (resizeShadow) calcolerà e modificherà posizione e dimensioni dei div ombra creati. Vedremo poi come il codice si presenterà alla fine del processo.

Migliorare l'effetto con bordi arrotondati

Cerchiamo ora di migliorare esteticamente l'effetto ombra con un po' di creatività. La prima idea consiste nell'affiancare al div target 2 o più div (e quindi bordi) con diverse sfumature di grigio, dal più scuro al più chiaro. Un esempio di questa tecnica mostra come l'effetto appaia già più gradevole.

Un ulteriore passo potrebbe essere agire sugli angoli delle ombre per smussarli. Gli angoli superiore e sinistro dell'ombra possono essere resi più tondeggianti agendo sulle dimensioni dei div ombra (nel caso ve ne siano naturalmente più di uno). Nel codice il meccanismo viene implementato calcolando dimensione e posizioni dei div ombra parametricamente alla loro distanza dal div target. Ne osserviamo qui un esempio.

Rimane aperta la questione dell'angolo inferiore destro. Per ottenere anche su di esso lo stesso effetto la soluzione più semplice sarebbe utilizzare la proprietà border-radius del Css3, ma quest'ultima è ancora troppo poco supportata dai browser in circolazione (al momento della stesura di questo articolo). Un'altra possibilità consiste nel sovrapporre all'angolo un oggetto che lo copra qualche pixel con un bordo tondeggiante o triangolare, un div quindi oppure un carattere speciale (es. una virgola posta ad-hoc). Questa parte non è implementata nel codice di esempio (poichè ne aumenterebbe la complessità rendendolo meno riutilizzabile) ma è data come idea per implementazioni personalizzate.

Vantaggi e svantaggi

Vantaggi di un simile approccio sono quindi:

  1. la possibilità di riutilizzare il codice ed eventualmente personalizzarlo
  2. la possibilità di personalizzare l'effetto drop shadow (ampiezza dell'ombra, angoli)
  3. non è necessario aggiungere ulteriore markup all'(x)html o righe al CSS
  4. non è necessario creare le immagini per le ombre (background)
  5. è applicabile indifferentemente a qualsiasi elemento block level
  6. le ombre seguono il ridimensionamento del div

Svantaggi risultano invece

  1. uno sgradevole effetto di trascinamento quando la pagina viene ridotta o ingrandita
  2. la difficoltà relativa nell'ottenere un angolo inferiore destro smussato

Conclusione

Viene di seguito esposto il codice completo ottimizzato (evitando di ricalcolare la dimensione dei div target per ogni div ombra) e object oriented. Seguono quindi esempi di un eventuale utilizzo.


function Shadow(eid) 
{
 if(!document.getElementById || !document.getElementsByTagName 
    || !document.createElement || !document.appendChild) return;

  this.depth    = 5;        /* shadow depth     */
  this.delta    = 5;        /* level of blur    */
  this.margin   = 2;        /* shadow margin from borders   */
  this.decrease = 1;        /* shadow div gradual decrease  */

  this.light    = 0xdddddd; /* lightest shadow  */
  this.dark     = 0x444444; /* darkest shadow   */

  var obj     = document.getElementById(eid);
  var body    = document.getElementsByTagName('body')[0];

  var left; /* distance from top  */
  var top;  /* distance from left */

  var ombra   = new Array();
  var self    = this;

  getDistance = function() {
    left = top = 0;

    for (var tmpObj = obj; tmpObj.offsetParent; tmpObj = tmpObj.offsetParent)
      left += tmpObj.offsetLeft;

    for (var tmpObj = obj; tmpObj.offsetParent; tmpObj = tmpObj.offsetParent)
      top += tmpObj.offsetTop;
  }

  setSize = function(idx, x1, x2, x3) {
    if(ombra[idx]) {
      ombra[idx].style.left    = left + x1 + x2 - x3 + 'px';
      ombra[idx].style.top     = top  + x1 + x2 - x3 + 'px';
      ombra[idx].style.height  = obj.offsetHeight - x1 + x3 + 'px';
      ombra[idx].style.width   = obj.offsetWidth  - x1 + x3 + 'px';
    }
  }

  this.radius = function(i) { return Math.floor(i*i/10) + self.margin; }
  
  this.draw = function() {

    for(var i = 0; i < ombra.length; i++)
      body.removeChild(ombra[i]);
  
    ombra = [];

    getDistance();
  
    var minus = (Math.floor((self.light - self.dark) / (self.depth + self.delta)) >> 16) * 0x10101;
  
    var myColor = self.light;

    for(var i = self.depth; i >= 0; i--)
    {
      myColor = ( myColor - minus ) > self.dark ? myColor - minus : myColor;
  
      ombra[i] = document.createElement('div');
  
      ombra[i].style.position = 'absolute';
  
      colorString = '#' + ((myColor<0x100000)?('0'+myColor.toString(16)):(myColor.toString(16)));
      
      ombra[i].style.borderRight   = '1px solid ' + colorString;
      ombra[i].style.borderBottom  = '1px solid ' + colorString;
  
      setSize(i, self.radius(i), i, i - self.decrease);

      body.appendChild(ombra[i]);
    }

    var oldRef = window.onresize;
    
    window.onresize = function() {
      if(oldRef != null && typeof oldRef != 'undefined')
        oldRef();
      getDistance();
      for(var i = self.depth; i >= 0; i--)
        setSize(i, self.radius(i), i, i - self.decrease);
    };
  }
  return this;
}

Per utilizzare il codice, una volta incluso lo script nella pagina, creo un oggetto Shadow per ogni div al quale voglio aggiungere l'effetto. Posso scegliere a questo punto di cambiare i parametri (come lo spessore dell'ombra o i margini) oppure semplicemente di utilizzare quelli di default. Per utilizzare l'effetto drop shadow chiamo quindi il metodo draw().


var ombra = new Shadow('myDiv');
ombra.depth = 4;
ombra.margin = 2;
ombra.draw();

Più semplicemente è possibile chiamare direttamente il metodo (se non si vogliono cambiare i parametri di default) senza assegnare una variabile ('myDiv' è l'id del div al quale applico l'effetto).


new Shadow('myDiv').draw()

Ho scritto un esempio concreto testato e funzionante su browser Firefox 1.0, Explorer 6, Opera7, Konqueror 3.3.x. Opera mostra un compotramento anomalo quando si tratta di calcolare la dimensione di un elemento con position static: alla sua distanza dal margine superiore della pagine viene aggiunto il margine del primo elemento che si trova dopo il body. Su Konqueror l'effetto par funzionare perfettamente dopo un reload della pagina o un ridimensionamento della finestra, altrimenti sembra che la distanza calcolata differisca di un paio di pixel (per motivi a me sconosciuti).