import { j3pAddElt, j3pElement, j3pIsHtmlElement } from 'src/legacy/core/functions'
import 'src/legacy/outils/brouillon/paint.css'
class Paint {
/**
* Crée une zone pour dessiner
* @param {Object} [options]
* @param {string[]} [options.textes]
* @param {string[]} [options.colors] Les colors à proposer, la dernière sera utilisée comme couleur de remplissage
* @param {string|HTMLElement} container
*/
constructor ({ textes = [], colors } = {}) {
if (typeof this !== 'object') throw Error('Constructeur qui doit être appelé avec new')
/**
* Les lettres ou textes déplaçables
* @type {string[]}
*/
this.textes = [...textes]
/** @type {string[]} */
this.colors = Array.isArray(colors) ? [...colors] : ['#000000', '#FFFF00', '#FF0000', '#0000FF', '#00FF00', '#F0F0F0']
// on prend la dernière comme couleur de remplissage
/**
* La couleur de remplissage
* @type {string}
*/
this.fillColor = this.colors.pop()
// et la première comme couleur de trait
this.currentColor = this.colors[0]
} // constructor
/**
* Initialise le DOM
* @param {Object} [options]
* @param {number} [options.width=100]
* @param {number} [options.height=100]
* @param {number} [options.epaisseur=2]
* @private
*/
_init (container, { width = 100, height = 100, epaisseur = 2 }) {
if (typeof container === 'string') container = j3pElement(container)
if (!j3pIsHtmlElement(container)) throw Error('conteneur invalide')
/**
* Conteneur (position relative) qui sert de référence aux positionnements absolus (pour les div des lettres ou le tracé)
* @type {HTMLElement}
*/
this.container = j3pAddElt(container, 'div', '', { className: 'paint', style: { borderRadius: '2px', border: 'solid 2px', background: this.fillColor } })
// on veut au moins 15px de haut pour les couleurs
if (height < this.colors.length * 15 + 26) height = this.colors.length * 15 + 26
this.canvas = j3pAddElt(this.container, 'canvas', { width: width - 50, height })
const divColors = j3pAddElt(this.container, 'div', '', { className: 'colors' })
const clickColorListener = this.onClickColor.bind(this)
for (const color of this.colors) {
const rect = j3pAddElt(divColors, 'div', '', { className: 'color', style: { backgroundColor: color } })
if (color === this.colors[0]) rect.classList.add('selected')
rect.addEventListener('click', clickColorListener)
}
// la dernière case est différente (bouton poubelle)
const divTrash = j3pAddElt(divColors, 'div', '', { className: 'trashIcon' })
divTrash.addEventListener('click', () => {
// on efface tout
this.ctx.fillStyle = this.fillColor
// on retrace le rectangle
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
this.ctx.strokeStyle = this.currentColor
})
this.ctx = this.canvas.getContext('2d')
const paintStyles = getComputedStyle(this.container)
this.canvas.width = (parseInt(paintStyles.getPropertyValue('width')) - 30)
this.ctx.lineWidth = epaisseur
this.ctx.lineJoin = 'round'
this.ctx.lineCap = 'round'
this.ctx.strokeStyle = this.colors[0]
/**
* listener attaché à l’instance pour pouvoir le mettre au mousedown et le retirer au mouseup|mouseout
* @type {EventListener}
*/
this.onDrawListener = this.onDraw.bind(this)
// onMouseDown|onTouchStart va activer onDrawListener
this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this))
this.canvas.addEventListener('touchstart', this.onTouchStart.bind(this))
// faut le virer si ça sort du canvas
this.canvas.addEventListener('mouseout', () => this.canvas.removeEventListener('mousemove', this.onDrawListener, { passive: true }))
this.canvas.addEventListener('touchcancel', () => this.canvas.removeEventListener('touchmove', this.onDrawListener, { passive: true }))
// ceinture & bretelles => on le vire aussi au mouseup|touchend sur document
document.addEventListener('mouseup', () => this.canvas.removeEventListener('mousemove', this.onDrawListener, { passive: true }))
// avec ça, on supprime le listener au 1er doigt "relevé", si y’en avait deux et qu’on en retire un ça coupe (sinon trop compliqué à gérer)
document.addEventListener('touchend', () => this.canvas.removeEventListener('touchmove', this.onDrawListener, { passive: true }))
this.divTextes = []
let offset = 0.5
for (const texte of this.textes) {
const div = j3pAddElt(this.container, 'div', texte, { style: { cursor: 'pointer', position: 'absolute', zIndex: 1, left: '10px', top: `${offset}em` } })
offset += 1
const topCanvas = this.canvas.getBoundingClientRect().top
const { top, height } = div.getBoundingClientRect()
if (top - topCanvas + height > this.canvas.height) this.canvas.height = top - topCanvas + height + 10
const onClickedTextListener = this.onClickedText.bind(this, div)
div.addEventListener('mousedown', () => this.container.addEventListener('mousemove', onClickedTextListener, { passive: false }))
div.addEventListener('touchstart', () => this.container.addEventListener('touchmove', onClickedTextListener, { passive: true }))
document.addEventListener('mouseup', () => this.container.removeEventListener('mousemove', onClickedTextListener, { passive: false }))
document.addEventListener('touchend', () => this.container.removeEventListener('touchmove', onClickedTextListener, { passive: true }))
this.divTextes.push(div)
}
}
/**
* Change de couleur (mis en listener mousedown)
* @param {MouseEvent} event
*/
onClickColor (event) {
// on change la couleur courante
this.ctx.strokeStyle = event.currentTarget.style.backgroundColor
// on vire les .selected déjà mis (normalement y’en a qu’un, mais…)
for (const selected of this.container.querySelectorAll('.selected')) selected.classList.remove('selected')
// et on le met sur la couleur cliquée
event.currentTarget.classList.add('selected')
}
/**
* Listener mousemove sur un texte
* @param {HTMLElement} divTexte
* @param {MouseEvent} event
*/
onClickedText (divTexte, event) {
// si c’est un événement souris faut du preventDefault pour éviter de sélectionner le texte autour
if (!event.touches?.length) event.preventDefault()
const { width, height } = divTexte.getBoundingClientRect()
// on le déplace à l’endroit de la souris
const { left, top, width: widthCanvas, height: heigthCanvas } = this.canvas.getBoundingClientRect()
const { x, y } = this._getClientCoords(event)
// si on sort, on devrait virer le listener mais ici on peut pas, on se contente de ne rien faire
if (x < left + width / 2 || x > left + widthCanvas - width / 2 || y < top + height / 2 || y > top + heigthCanvas - height / 2) return
divTexte.style.left = (x - left - width / 2) + 'px'
divTexte.style.top = (y - top - height / 2) + 'px'
}
/**
* Au mousedown sur canvas faut activer le listener move pour tracer
* @param event
*/
onMouseDown (event) {
this.startDrawing(event)
// et on active le tracé souris
this.canvas.addEventListener('mousemove', this.onDrawListener, { passive: true })
}
/**
* Au touch sur canvas faut activer le listener move pour tracer
* @param event
*/
onTouchStart (event) {
this.startDrawing(event)
// et on active le tracé touch
this.canvas.addEventListener('touchmove', this.onDrawListener, { passive: true })
}
/**
* Prépare ctx pour le tracé qui démarre
* @param event
*/
startDrawing (event) {
// on déplace ctx
this.ctx.beginPath()
const { left, top } = this.container.getBoundingClientRect()
// on mémorise ça, pour éviter de le recalculer à chaque move dans le canvas
this.canvasLeft = left
this.canvasTop = top
this.ctx.moveTo(event.clientX - left, event.clientY - top)
}
/**
* Retourne les coordonnées du pointeur (mouse ou touch)
* @param {MouseEvent|TouchEvent} event
* @return {{x, y}}
* @private
*/
_getClientCoords (event) {
let x, y
if (event.touches?.length) {
// on prend toujours le premier touch, si y’a deux doigts (ou davantage) posé, on ignore les autres
// (mais ce listener sera supprimé au 1er doigt relevé)
x = event.touches[0].clientX
y = event.touches[0].clientY
} else {
x = event.clientX
y = event.clientY
}
return { x, y }
}
/**
* Listener mis au mousedown|touchstart et retiré au mouseup|mouseout|touchend, il trace dans le canvas
* @param {MouseEvent|TouchEvent} event
*/
onDraw (event) {
const { x, y } = this._getClientCoords(event)
this.ctx.lineTo(x - this.canvasLeft, y - this.canvasTop)
this.ctx.stroke()
}
/**
* Crée une zone de dessin et la retourne
* @param {HTMLElement|string} container
* @param {Object} [options]
* @param {number} [options.width=100]
* @param {number} [options.height=100]
* @param {number} [options.epaisseur=2]
* @param {string[]} [options.textes] Les lettres ou textes déplaçables sur la zone de dessin
* @param {string[]} [options.colors] Les colors à proposer, la dernière sera utilisée comme couleur de remplissage
* @return {Paint}
*/
static create (container, { width = 100, height = 100, epaisseur = 2, textes = [], colors } = {}) {
const paint = new Paint({ textes, colors })
paint._init(container, { width, height, epaisseur })
return paint
}
}
export default Paint