legacy/outils/boulier/boulier.js

import { j3pGetNewId } from 'src/legacy/core/functions'
import { isHtmlElement } from 'sesajs/dom'
/**
 * Ajoute un boulier en svg dans conteneur
 * @param {Object} values Les propriétés à affecter à l’objet Boulier
 * @param {boolean} [values.isChinese=true]
 * @param {number} [values.width=600] Largeur du svg (la hauteur est imposée par la taille des boules et leur nombre)
 * @param {number} [values.nbTiges=6]
 * @param {number} [values.epCadre=20] epaisseur du cadre
 * @param {number} [values.epTige=10] épaisseur des tiges
 * @param {number} [values.diametre=25] largeur d’une boule
 * @param {number} [values.fige=false] Passer true pour figer le boulier
 * @param {HTMLElement} values.outputElement un élément pour afficher la valeur du boulier
 * @param {Object} options Des options de comportement
 * @param {boolean} [options.hasResetButton=false] passer true pour mettre le bouton reset
 * @param {boolean} [options.isBlue=false] Passer true pour avoir des boules bleues plutôt que rouges
 * @param {HTMLElement} conteneur élément HTML qui contiendra le boulier
 * @constructor
 */
function Boulier (values, conteneur, options) {
  function addRect (values) {
    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
    rect.setAttribute('x', values.x)
    rect.setAttribute('y', values.y)
    rect.setAttribute('width', values.width)
    rect.setAttribute('height', values.height)
    if (values.style) rect.setAttribute('style', values.style)
    svg.appendChild(rect)
  }

  if (typeof options !== 'object') options = {}
  if (!isHtmlElement(conteneur)) throw Error('Il faut passer un conteneur pour le boulier')
  if (typeof this !== 'object') throw Error('Constructeur qui doit être appelé avec new')

  /**
   * Largeur du svg
   * @type {number}
   */
  this.width = values.width || 600

  /**
   * Largeur d’une boule
   * @type {number}
   */
  this.diametre = values.diametre || 25

  /**
   * Epaisseur du cadre
   * @type {number}
   */
  this.epCadre = values.epCadre || 20

  /**
   * true pour le boulier chinois (défaut), false pour le boulier japonais
   * @type {boolean}
   */
  this.isChinese = values.isChinese !== false

  /**
   * Hauteur des demi-tiges du haut (sans cadre)
   * @type {number}
   */
  this.heightUp = (this.isChinese ? 4 : 3) * this.diametre

  /**
   * Hauteur du svg
   * @type {number}
   */
  this.height = this.epCadre * 3 + this.heightUp + (this.isChinese ? 7 : 6) * this.diametre

  /**
   * Epaisseur de chaque tige
   * @type {number}
   */
  this.epTige = values.epTige || 10

  /**
   * Un élément pour afficher la valeur courante du boulier
   * @type {HTMLElement|null}
   */
  this.outputElement = (isHtmlElement(values.outputElement) && values.outputElement) || null

  /**
   * Si true le boulier est figé
   * @type {boolean}
   */
  this.fige = Boolean(values.fige) // false par défaut

  /**
   * Nombre de tiges
   * @type {number}
   */
  this.nbTiges = values.nbTiges || 6
  // qu’on limite avec le max autorisé
  const maxTiges = Math.floor((this.width - this.epCadre * 4) / (this.diametre * 1.1))
  if (maxTiges < this.nbTiges) {
    console.error(Error('Trop de tiges pour la taille donnée ' + this.nbTiges + ' => ' + maxTiges))
    this.nbTiges = maxTiges
  }
  /**
   * État courant des boules, chaque caractère représentant le nb de boules "actives" (vers le centre) de la demi-tige
   * Avec 4 tiges le premier caractère concerne les boules du haut de la tige de gauche, le 2e les boules du bas de la tige de gauche, etc.
   * On a donc toujours un nombre pair de caractères, tous des chiffres
   * @type {string}
   */
  this.etat = '00'
  while (this.etat.length < this.nbTiges * 2) this.etat += '00'
  // this.etat = '0'.repeat(this.nbTiges * 2) // c’est de l’es6 :-S

  // hasResetButton
  // Création de la zone SVG
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
  svg.setAttribute('width', String(this.width))
  svg.setAttribute('height', String(this.height))

  let dstop1, dstop2, dstop3
  if (options.isBlue) {
    dstop1 = '#8888DD'
    dstop2 = '#0000CC'
    dstop3 = '#000033'
  } else {
    dstop1 = '#FF4D4D'
    dstop2 = '#CC0000'
    dstop3 = '#550000'
  }

  const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
  const grad = document.createElementNS('http://www.w3.org/2000/svg', 'radialGradient')
  const idGradient = j3pGetNewId()
  grad.setAttributeNS(null, 'id', idGradient)
  grad.setAttributeNS(null, 'cx', '0')
  grad.setAttributeNS(null, 'cy', '0')
  grad.setAttributeNS(null, 'r', '100%')
  const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop')
  stop1.setAttributeNS(null, 'offset', '20%')
  stop1.setAttributeNS(null, 'stop-color', dstop1)// FF4D4D
  const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop')
  stop2.setAttributeNS(null, 'offset', '50%')
  stop2.setAttributeNS(null, 'stop-color', dstop2)// CC0000
  const stop3 = document.createElementNS('http://www.w3.org/2000/svg', 'stop')
  stop3.setAttributeNS(null, 'offset', '100%')
  stop3.setAttributeNS(null, 'stop-color', dstop3)// 550000
  grad.appendChild(stop1)
  grad.appendChild(stop2)
  grad.appendChild(stop3)
  defs.appendChild(grad)
  svg.appendChild(defs)

  // on construit la regex d’apres le contexte
  let expr = '^([0-' + (this.isChinese ? '2' : '1') + ']' // boules du haut
  expr += '[0-' + (this.isChinese ? '5' : '4') + '])' // boules du bas
  expr += '{' + this.nbTiges + '}$' // nb de répétitions
  this.etatRegExp = new RegExp(expr)

  // Création des tiges (on laisse l’équivalent du cadre vide à gauche et à droite)
  const interTiges = (this.width - this.epCadre * 4) / this.nbTiges // entre axes
  const style = 'stroke-width:2;stroke:#BABABA;fill:#BABABA;'
  let i, j, x, y
  for (i = 1; i <= this.nbTiges; i++) {
    x = this.epCadre * 2 + interTiges * (i - 0.5)
    y = this.epCadre
    addRect({ x, y, width: this.epTige, height: this.heightUp, style })
    y += this.heightUp + this.epCadre
    addRect({ x, y, width: this.epTige, height: this.height - 3 * this.epCadre - this.heightUp, style })
  }

  // Création du cadre
  const couleurcadre = 'stroke-width:2;stroke:#4D2800;fill:#4D0000'
  addRect({ x: 0, y: 0, width: this.width, height: this.epCadre, style: couleurcadre })
  addRect({ x: 0, y: 0, width: this.epCadre, height: this.height, style: couleurcadre })
  addRect({ x: 0, y: this.height - this.epCadre, width: this.width, height: this.epCadre, style: couleurcadre })
  addRect({ x: this.width - this.epCadre, y: 0, width: this.epCadre, height: this.height, style: couleurcadre })
  addRect({ x: 0, y: this.heightUp + this.epCadre, width: this.width, height: this.epCadre, style: couleurcadre })
  addRect({ x: 10000, y: 10000, width: 1, height: 1, style: 'stroke-width:2;stroke:green;fill:green;' })

  // bouton reset
  if (options.hasResetButton) {
    const cote = this.epCadre * 2
    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
    rect.setAttribute('x', this.width - cote)
    rect.setAttribute('y', this.height - cote)
    rect.setAttribute('width', cote)
    rect.setAttribute('height', cote)
    rect.setAttribute('style', 'stroke-width:2;stroke:#FF6600;fill:#FFCC00;cursor:pointer')
    rect.addEventListener('click', this.reset.bind(this))
    svg.appendChild(rect)
    const text = document.createElementNS('http://www.w3.org/2000/svg', 'text')
    text.setAttribute('x', this.width - cote * 0.9)
    text.setAttribute('y', this.height - cote * 0.3)
    text.setAttribute('style', 'fill:#330000;cursor:pointer;font-size:' + Math.round(cote * 0.4) + 'px;')
    text.appendChild(document.createTextNode('RàZ'))
    text.addEventListener('click', this.reset.bind(this))
    svg.appendChild(text)
  }

  // Création des boules
  let boule, prefix
  const max = this.isChinese ? 7 : 5
  for (i = 1; i <= this.nbTiges; i++) {
    prefix = 'boule' + i
    for (j = 1; j <= max; j++) {
      boule = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
      // affiche gère les cy, on initialise cx ici et laisse cy à 0
      boule.setAttribute('cx', this.epCadre * 2 + interTiges * (i - 0.5))
      boule.setAttribute('cy', 0)
      boule.setAttribute('style', 'stroke-width:0;stroke:maroon;fill:url(#' + idGradient + ');')
      boule.setAttribute('r', this.diametre / 2)
      svg.appendChild(boule)
      boule.addEventListener('click', this.clic.bind(this, i, j))
      this[prefix + j] = boule
    }
  }

  conteneur.appendChild(svg)

  // et on initialise les boules
  this.affiche(this.etat)
} // Boulier

/**
 * Affecte l’état du boulier et place les boules correctement pour correspondre à cet état
 * @param {string} etat
 */
Boulier.prototype.affiche = function affiche (etat) {
  function affichehaut (numerotige, nbbilles) {
    const prefix = 'boule' + numerotige
    let i, boule
    if (me.isChinese) {
      // les billes deplacées
      for (i = 1; i <= nbbilles; i++) {
        boule = me[prefix + (3 - i)]
        boule.setAttribute('cy', me.epCadre + me.heightUp + (0.5 - i) * me.diametre)
      }
      // les billes non déplacées
      for (i = 1; i <= 2 - nbbilles; i++) {
        me[prefix + i].setAttribute('cy', me.epCadre + (i - 1 + 0.5) * me.diametre)
      }
    } else {
      for (i = 1; i <= nbbilles; i++) {
        me[prefix + (2 - i)].setAttribute('cy', me.epCadre + me.heightUp + (0.5 - i) * me.diametre)
      }
      for (i = 1; i <= 1 - nbbilles; i++) {
        me[prefix + i].setAttribute('cy', me.epCadre + (i - 1 + 0.5) * me.diametre)
      }
    }
  }

  function affichebas (numerotige, nbbilles) {
    const prefix = 'boule' + numerotige
    let i
    if (me.isChinese) {
      // les billes deplacées
      for (i = 1; i <= nbbilles; i++) {
        me[prefix + (2 + i)].setAttribute('cy', 2 * me.epCadre + me.heightUp + (i - 0.5) * me.diametre)
      }
      // les billes non déplacées
      for (i = 1; i <= 5 - nbbilles; i++) {
        me[prefix + (8 - i)].setAttribute('cy', me.height - me.epCadre + (0.5 - i) * me.diametre)
      }
    } else {
      for (i = 1; i <= nbbilles; i++) {
        me[prefix + (1 + i)].setAttribute('cy', 2 * me.epCadre + me.heightUp + (i - 0.5) * me.diametre)
      }
      for (i = 1; i <= 4 - nbbilles; i++) {
        me[prefix + (6 - i)].setAttribute('cy', me.height - me.epCadre + (0.5 - i) * me.diametre)
      }
    }
  }

  if (typeof etat !== 'string') throw Error('etat invalide (pas une string) : ' + typeof etat)
  if (!this.etatRegExp.test(etat)) throw Error('etat invalide : ' + etat + ' ne valide pas ' + this.etatRegExp + ' pour un boulier ' + (this.isChinese ? 'chinois' : 'japonais'))
  this.etat = etat

  const me = this
  let i
  for (let t = 1; t <= this.nbTiges; t++) {
    i = (t - 1) * 2 // index dans la chaîne du nb correspondant aux boules du haut
    affichehaut(t, etat.substr(i, 1))
    affichebas(t, etat.substr(i + 1, 1))
  }

  if (this.outputElement) this.outputElement.innerHTML = String(this.getNumber())
} // affiche

/**
 * @param numTige
 * @param numBoule
 */
Boulier.prototype.clic = function clic (numTige, numBoule) {
  function remplace (position, char) {
    me.etat = me.etat.substring(0, position) + char + me.etat.substring(position + 1)
  }
  // si le boulier est inactif
  if (this.fige) return
  let e // etat de cette boule
  const me = this
  if (this.isChinese) {
    if (numBoule <= 2) {
      e = this.etat.substring(2 * (numTige - 1), 2 * (numTige - 1) + 1)
    } else {
      e = this.etat.substring(2 * (numTige - 1) + 1, 2 * numTige)
    }
  } else {
    if (numBoule <= 1) {
      e = this.etat.substring(2 * (numTige - 1), 2 * (numTige - 1) + 1)
    } else {
      e = this.etat.substring(2 * (numTige - 1) + 1, 2 * numTige)
    }
  }

  if (this.isChinese) {
    switch (numBoule) {
      case 1:
        e = (e === '2') ? '1' : '2'
        remplace(2 * (numTige - 1), e)
        break
      case 2:
        e = (e === '0') ? '1' : '0'
        remplace(2 * (numTige - 1), e)
        break
      default:
        e = ((numBoule - 2) <= Number(e)) ? String(numBoule - 3) : String(numBoule - 2)
        remplace(2 * (numTige - 1) + 1, e)
        break
    }
  } else {
    switch (numBoule) {
      case 1:
        e = (e === '1') ? '0' : '1'
        remplace(2 * (numTige - 1), e)

        break
      default:
        e = ((numBoule - 1) <= Number(e)) ? String(numBoule - 2) : String(numBoule - 1)
        remplace(2 * (numTige - 1) + 1, e)
        break
    }
  }
  this.affiche(this.etat)
} // clic

/**
 * Convertit un état en nombre
 * @param {string} etat
 * @return {number}
 */
Boulier.prototype.etatToNb = function etatToNb (etat) {
  if (!this.etatRegExp.test(etat)) throw Error('etat invalide : ' + etat)
  let total = 0
  let puissance = 0
  // on passe en tableau pour prendre les chiffres en partant de la droite, deux par boucle
  const tab = etat.split('')
  while (tab.length) {
    total += (Number(tab.pop()) + 5 * Number(tab.pop())) * Math.pow(10, puissance)
    puissance++
  }

  return total
}

/**
 * Retourne le nombre associé à l’état courant
 * @return {number}
 */
Boulier.prototype.getNumber = function () {
  return this.etatToNb(this.etat)
}

/**
 * Transforme un nombre en état des boules
 * @param {number} n
 * @return {string} l’état
 */
Boulier.prototype.nbToEtat = function nbToEtat (n) {
  // on veut un entier positif
  if (Math.round(n) !== n || n < 0) throw Error('nombre invalide : ' + n)
  // chinois ou japonais ne change rien, on aura toujours pour chaque chiffre
  // 0|1 pour le 1er char (haut de la tige) et 0|1|2|3|4 pour le 2e char (le bas)
  return String(n)
    .split('') // tableau de string
    .map(Number) // tableau de number (chaque chiffre est un elt)
    .map(function (chiffre) {
      //     <--       haut      --->    <--        bas         -->
      return (chiffre > 4 ? '1' : '0') + String(chiffre % 5)
    }).join('')
}

/**
 * Remet le boulier à 0
 */
Boulier.prototype.reset = function reset () {
  if (!this.fige) this.affiche('0'.repeat(this.nbTiges * 2))
}

export default Boulier