momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Modelarea tablei şi jocului de şah (VIII)

CSS | DOM | jQuery | javaScript | widget
2012 jun

Notaţia tablei de şah şi poziţionarea etichetelor

Până acum am avut în vedere doar o poziţie individuală, furnizată ca şir FEN (într-un <textarea>) spre a fi "explicitată" pe tablă. Ceva mai târziu vom viza fireşte, o listă de mutări - furnizată spre a fi "rulată" pe tablă, mutare după mutare. Ori mutarea curentă trebuie să specifice piesa care trebuie mutată, câmpul de plecare şi câmpul de sosire.

Notaţia uzuală a mutărilor se bazează pe fixarea unei notaţii a câmpurilor tablei: coloanele sunt etichetate prin 'a'..'h', unde 'a' indică verticala din stânga albului; liniile sunt marcate prin 1..8, unde '1' indică prima linie a poziţiei iniţiale a albului. De exemplu, Ng1-f3 (sau scurt, Nf3) notează mutarea calului alb ('N', în notaţia FEN) de pe câmpul g1 (intersecţia coloanei 'g' cu linia '1') pe câmpul f3 (de intersecţie a coloanei 'f' cu linia '3').

Prin urmare (cel puţin pentru cazul când vom viza liste de mutări) este necesar să adăugăm tablei de şah constituite până acum, o "bară" orizontală dedesubt, pentru etichetarea coloanelor şi o "bară" verticală în stânga, pentru marcarea liniilor.

Aceste două bare trebuie să aibă aceeaşi dimensiune (lăţime într-un caz, înălţime în celălalt) ca tabla de şah (diviziunea .boardFields) - depinzând astfel, indirect, de setul de piese curent. Dar putem evita multiplicarea definiţiilor (câte două definiţii de bare, pentru fiecare set de piese), poziţionând absolut faţă de diviziunea .boardFields.

De exemplu, dacă adăugăm ca subdiviziune în .boardFields (care are position: relative)) o diviziune cu position: absolute; width: 100%, atunci aceasta va fi poziţionată faţă de .boardFields şi-i va moşteni lăţimea ("100%" se referă aici la diviziunea faţă de care este poziţionată).

Avem două variante de lucru: una care angajează "bara" ca atare (două <div>-uri, conţinând fiecare câte 8 subdiviziuni pentru etichetele de linii şi respectiv, de coloane) şi una care nu mai implică bare-container, angajând direct 16 <div>-uri pentru etichete.

Dar în prealabil (deşi, cum se poate bănui, am ajuns abia post-factum la această necesitate), ar fi de făcut o anumită îndreptare în codul anterior.

Separarea celor 64 de diviziuni corespunzătoare câmpurilor

Prin setChessTable() au fost create 64 de diviziuni corespunzătoare câmpurilor tablei, iar în unele dintre acestea setPosition() constituia ulterior câte o diviziune pentru piesele existente în poziţia indicată de FEN - încât diviziunea .boardFields arată astfel:

<div class="boardFields">
    <div class="Field Row-8 Col-1 BlackField">   <!-- câmpul a8 -->
        <div class="Piece black-rook"></div>
    </div>
    <!-- ... etc. ... -->                        <!-- ...  -->
    <div class="Field Row-1 Col-8 BlackField">   <!-- câmpul h1 -->
        <div class="Piece white-rook"></div>
    </div>
</div>

Acum - ar urma să adăugăm în div.boardFields, după diviziunile menţionate, cel puţin încă 16 diviziuni pentru etichete.

Dar setPosition() (vezi (VII)) parcurgea diviziunile din .boardFields şi în paralel, caracterele din FEN - înserând o diviziune pentru piesă atunci când caracterul de index curent din FEN indica o piesă, sau ştergând conţinutul diviziunii curente dacă indexul curent din FEN indica alt caracter decât "piesă".

În mod implicit, era asumat că în momentul parcurgerii diviziunea .boardFields conţinea exact cele 64 de diviziuni corespunzătoare câmpurilor tablei; dar după adăugarea diviziunilor pentru etichete, parcurgerea descrisă mai sus se va extinde şi asupra acestora - rezultând pur şi simplu, ştergerea etichetelor respective (conform else $(this).html(""); din finalul metodei setPosition()).

Soluţia banală de a limita parcurgerea cu "if(index < 64)" este proastă ("if"-ul trebuind executat de 64 de ori). Soluţia firească şi într-adevăr bună, constă în prevederea imediat sub div.boardFields, a unui container <div class="flds64"> în care să ambalăm cele 64 de <div>-uri corespunzătoare câmpurilor tablei - separând astfel aceste diviziuni, faţă de cele corespunzătoare etichetelor; parcurgerea menţionată mai sus ar decurge apoi, pe diviziunile din div.flds64.

Necesitatea acestei îndreptări, precum şi însuşi faptul că avem (şi vom avea) de adăugat nişte noi diviziuni arată şi faptul că metoda _create() este formulată prea expeditiv (this.Tabla e constituit într-o singură linie) şi va trebui refăcută, încât să fie mai uşor de extins când va fi nevoie.

Etichetarea prin două bare-container

Adăugăm în div.boardFields, imediat după div.flds64, diviziunile .horBar şi .verBar pentru bara orizontală şi respectiv, cea verticală; ambele trebuie să aibă position: absolute (indicând "top" şi respectiv "left" faţă de div.boardFields) şi width: 100%, respectiv height: 100% (asigurând indirect, potrivirea exactă cu dimensiunea curentă a tablei, care depinde de setul curent de piese).

Fiecare bară conţine câte 8 <div>-uri; proprietatea .posAbs prevăzută fiecăruia dintre acestea serveşte pentru poziţionarea absolută în cadrul barei din care face parte <div>-ul respectiv. Iar .Row-N şi .Col-N precizează pentru fiecare dintre aceste <div>-uri "top" (în cazul barei verticale) şi "left" (în cazul celei orizontale) faţă de bara-container respectivă.

Adăugăm în fişierul "brw-sah.css" definiţiile minimale (cu setările strict necesare):

.horBar, .verBar, .pos-abs { position: absolute; }
.verBar { left: -6%; height: 100%; }  /* faţă de div.boardFields */
.horBar { top: 102%; width: 100%; }     

Rescriem metoda _create(), evidenţiind mai bine structura DOM care trebuie creată şi uşurând eventuala extindere a acesteia:

_create: function() {
    var inputFEN = this.element, // <textarea> pe care se instanţiază 
        opts = this.options, // va referi mai rapid opţiunile
        self = this;    // salvează referinţa 'this' la instanţa curentă
    
    $('<button></button>').text('Load')  // înscrie poziţia, la click()
                          .click(function() { 
                                self._init();   // dar NU "this"._init()     
                                return false;     
                          }).insertAfter( inputFEN );

    var thtml = []; // elementele HTML de inserat ca noduri în DOM
    thtml.push('<div class="chessmen-', opts.field_size, '">');
        thtml.push('<div class="boardFields">');
            thtml.push(this._setChessTable()); // 64 câmpuri, sub div.flds64
            thtml.push(this._setLabels()); // cele două bare cu etichete
        thtml.push('</div>'); // încheie div.boardFields
    thtml.push('</div>'); // încheie div.chessmen-N
    
    /* înscrie în DOM, după butonul Load, reţinând o referinţă 'Tabla' */
    this.Tabla = $(thtml.join('')).insertAfter(inputFEN.next());
    
    /* div.flds64 va fi vizată frecvent, deci instituie o referinţă */
    this.flds64 = this.Tabla.find('.flds64:first');
},

Înscrie cele 64 de <div>-uri corespunzătoare câmpurilor, într-un container div.flds64:

_setChessTable: function() {
    var thtml = ['<div class="flds64">'];
    /* ... codul anterior al funcţiei ... */
    thtml.push("</div>");
    return thtml.join('');
},

Parcurge în ordinea 0..63 <div>-urile conţinute în div.flds64 (şi în paralel, caracterele din FEN - identificând câmpurile <div> pe care trebuie aşezate piesele):

_setPosition: function(fen) {
    /* ... vezi codul anterior ... */
    this.flds64.children().each(function(index) {
        /* ... vezi codul anterior ...*/
    });
},    

În sfârşit, metoda pentru crearea celor două bare cu etichete:

_setLabels: function() {
    var thtml = ['<div class="verBar">']; // bara verticală
    for(var row = 1; row <= 8; row++) 
        thtml.push('<div class="pos-abs Row-', row, '">', row, '</div>');
    thtml.push('</div>');
            
    thtml.push('<div class="horBar">'); // bara orizontală
    for(var col = 1; col <= 8; col++) 
        thtml.push('<div class="pos-abs Col-', col, '">', 
                    String.fromCharCode(96 + col), // 1 -> 'a', 2 -> 'b', etc.
                    '</div>');
    thtml.push('</div>');
            
    return thtml.join('');
},

După aceste modificări în fişierul "brw-sah.js" putem încărca în browser fişierul "brw-sah.html" (redat de exemplu în (VII)).

Este de observat faptul că etichetele nu sunt "centrate", cum ar fi de dorit. Pentru a obţine şi centrarea, trebuie să adăugăm anumite proprietăţi CSS <div>-urilor care conţin etichetele şi anume, am putea adăuga aceste proprietăţi în definiţia lui .pos-abs

Dar centrarea pe orizontală necesită anumite proprietăţi CSS, iar pe verticală altele; trebuind să adaptăm .pos-abs fiecărui caz - o eliminăm, înlocuind-o (în "brw-sah.css", dar apoi şi în cadrul metodei _setLabels()) de exemplu cu:

.pos-absHor { position: absolute; width: 12.5%; text-align: center; }
.pos-absVer { position: absolute; line-height: 3em; }

Într-adevăr, setând pe un <div> width şi text-align (obligatoriu ambele), conţinutul va fi aliniat în modul indicat (în particular, va fi centrat):

B
<div style="width:30px; text-align:center; 
            height: 35px; border:1px solid black;">B</div>

Centrarea verticală este complicată, dar aici avem un caz particular: este de centrat un singur rând, într-un <div> a cărui înălţime poate fi calculată. În acest caz este suficientă setarea line-height pe o valoare egală cu înălţimea diviziunii respective:

123
<div style="height:30px; line-height:30px;
            width:35px; border:1px solid black;">1234</div>

În cazul nostru, înălţimea este de 12.5% din înălţimea barei verticale - numai că line-height nu poate fi setată astfel (şi am ales o valoare "medie", fiindcă încercarea de a seta "inline" - în cadrul metodei setLabels() - pe valoarea exactă "this.options.field_size" mi-a eşuat…).

Etichetarea directă a liniilor şi coloanelor

În loc să creem cum am făcut mai sus, două bare (poziţionate absolut faţă de div.boardFields) şi apoi diviziuni pentru etichete în interiorul acestora (poziţionate faţă de bară) - am putea să creem direct (şi "de-a valma") cele 16 diviziuni pentru etichete, poziţionându-le nemijlocit faţă de div.boardFields (bazându-ne şi acum, pe .Row-N şi .Col-N):

.Notation-Row, .Notation-Col {  /* pe fiecare din cele 16 DIV */
    position: absolute;  /* faţă de div.boardFields */
} 
.Notation-Row {    /* pentru fiecare DIV de pe verticală */
    left: -6%;     /* stânga faţă de div.boardFields */
    height: 12.5%; /* egal cu pasul "top"-ului din .Row-N */
}   
.Notation-Col {         /* pentru fiecare DIV de pe orizontală */
    top: 102%;          /* jos faţă de div.boardFields */
    width: 12.5%;       /* pasul "left"-ului din .Col-N */
    text-align:center;  /* centrează orizontal eticheta */
} 

setLabels() se scrie mai simplu faţă de varianta precedentă (cu un singur "for") şi acum reuşeşte şi setarea "inline" a proprietăţii line-height cu valoarea exactă (înălţimea câmpului = field_size + 2, unde "+2" reflectă "padding"-ul de 1 pixel prevăzut în .Field):

_setLabels: function() {
    var fld = 2 + parseInt(this.options.field_size), thtml=[];
    for (var N = 1; N <= 8; N++) {
        var colID = String.fromCharCode(96 + N);
        thtml.push("<div style='line-height:", fld, "px' ");
        thtml.push("class='Notation-Row Row-", N, "'>", N, "</div>");
        thtml.push("<div class='Notation-Col Col-", N, "'>", colID, "</div>");
    }
    return thtml.join('');
},

Amintindu-ne de proprietatea .revBoard (vezi "brw-sah.css") şi modificând în _create: thtml.push('<div class="boardFields revBoard">'); obţinem în final tabla redată alături - probând că obţinem centrarea dorită a etichetelor (plus inversarea tablei).

Desigur, pe diviziunile pentru etichete mai trebuie setate unele proprietăţi CSS, de exemplu: color: #98a; (nici chiar "black", ca în imaginea redată aici), font-family: monospace; font-size: 0.9em.

Poate că "font-size" ar trebui corelat cu mărimea diagramei (deci cu setul de piese curent)? În acest scop, am putea defini un "font-size" de bază pe containerul .chessmen-N; ca urmare, valorile definite (în unităţi em) pe sub-diviziuni vor fi proporţionate faţă de valoarea de bază menţionată (permiţând adaptarea mărimii etichetelor faţă de setul de piese).

Dar nu-i cazul să complicăm lucrurile: utilizatorul widget-ului va avea nevoie în pagina proprie de unu-două, poate trei seturi de piese şi va putea uşor să seteze "font-size" pentru etichete, în funcţie de contextul propriu.

vezi Cărţile mele (de programare)

docerpro | Prev | Next