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