From da06ecaf2d5aad662c921f0ea6928492b44bb190 Mon Sep 17 00:00:00 2001 From: "kevin.lhereec" Date: Mon, 17 Aug 2026 14:30:03 +0200 Subject: [PATCH] fix: clip card images via Canvas 2D in BootScene, no Phaser mask needed --- client/src/game/objects/CardObject.js | 102 +++++--------------- client/src/game/scenes/BootScene.js | 128 ++++++++++++++++++++------ 2 files changed, 127 insertions(+), 103 deletions(-) diff --git a/client/src/game/objects/CardObject.js b/client/src/game/objects/CardObject.js index 5d7eddc..7b4372f 100644 --- a/client/src/game/objects/CardObject.js +++ b/client/src/game/objects/CardObject.js @@ -19,7 +19,6 @@ export class CardObject extends Phaser.GameObjects.Container { this.cardData = cardData this._selected = false this._baseY = y - this._maskImg = null // référence au masque pour le détruire proprement this._build() scene.add.existing(this) } @@ -32,10 +31,7 @@ export class CardObject extends Phaser.GameObjects.Container { // ── 1. Ombre ───────────────────────────────────────────────────────────── const shadow = this.scene.add.graphics() shadow.fillStyle(0x000000, 0.5) - shadow.fillRoundedRect( - -CARD_W / 2 + 5, -CARD_H / 2 + 5, - CARD_W, CARD_H, RADIUS - ) + shadow.fillRoundedRect(-CARD_W / 2 + 5, -CARD_H / 2 + 5, CARD_W, CARD_H, RADIUS) this.add(shadow) // ── 2. Fond couleur équipe ──────────────────────────────────────────────── @@ -44,55 +40,27 @@ export class CardObject extends Phaser.GameObjects.Container { bg.fillRoundedRect(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H, RADIUS) this.add(bg) - // ── 3. Image portrait ───────────────────────────────────────────────────── + // ── 3. Image portrait (texture déjà clippée dans BootScene via canvas) ─── if (hasTexture) { - const frame = this.scene.textures.get(textureKey).getSourceImage() - const scaleX = CARD_W / frame.width - const scaleY = CARD_H / frame.height - const scale = Math.max(scaleX, scaleY) - + // La texture est déjà aux bonnes dimensions et clippée en arrondi + // → simple Image, pas de masque, suit le container nativement const img = this.scene.add.image(0, 0, textureKey) - img.setScale(scale) this.add(img) - - // Masque arrondi rendu dans une texture offline, puis appliqué comme BitmapMask - // Le BitmapMask suit le container automatiquement car on l'attache à un objet du container - const maskKey = `mask-card` - if (!this.scene.textures.exists(maskKey)) { - const maskGfx = this.scene.make.graphics({ x: 0, y: 0, add: false }) - maskGfx.fillStyle(0xffffff) - maskGfx.fillRoundedRect(0, 0, CARD_W, CARD_H, RADIUS) - maskGfx.generateTexture(maskKey, CARD_W, CARD_H) - maskGfx.destroy() - } - - // L'image masque est ajoutée AU container → elle suit ses mouvements - this._maskImg = this.scene.add.image(0, 0, maskKey) - this._maskImg.setVisible(false) - this.add(this._maskImg) - - img.setMask(this._maskImg.createBitmapMask()) + } else { + // Fallback : initiale + const initial = this.scene.add.text(0, -10, this.cardData.name[0], { + fontSize: '70px', fontFamily: 'Georgia', color: '#ffffff' + }).setOrigin(0.5).setAlpha(0.1) + this.add(initial) } - // ── 4. Dégradé haut (lisibilité badges) ────────────────────────────────── - const gradT = this.scene.add.graphics() - gradT.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0.75, 0.75, 0, 0) - gradT.fillRect(-CARD_W / 2, -CARD_H / 2, CARD_W, BADGE + 10) - this.add(gradT) - - // ── 5. Dégradé bas (lisibilité titre) ──────────────────────────────────── - const gradB = this.scene.add.graphics() - gradB.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0, 0, 0.85, 0.85) - gradB.fillRect(-CARD_W / 2, CARD_H / 2 - TITLE_H - 12, CARD_W, TITLE_H + 12) - this.add(gradB) - - // ── 6. Bordure couleur équipe ───────────────────────────────────────────── + // ── 4. Bordure couleur équipe ───────────────────────────────────────────── const border = this.scene.add.graphics() border.lineStyle(2.5, teamHex, 1) border.strokeRoundedRect(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H, RADIUS) this.add(border) - // ── 7. Titre en BAS ─────────────────────────────────────────────────────── + // ── 5. Titre en BAS ─────────────────────────────────────────────────────── this.add(this.scene.add.text( 0, CARD_H / 2 - TITLE_H / 2, this.cardData.name, { @@ -105,13 +73,13 @@ export class CardObject extends Phaser.GameObjects.Container { } ).setOrigin(0.5, 0.5)) - // ── 8. Badges attributs dans les 4 coins ───────────────────────────────── + // ── 6. Badges attributs dans les 4 coins ───────────────────────────────── this._addCornerBadge('food', -CARD_W / 2, -CARD_H / 2, 'topLeft') this._addCornerBadge('war', CARD_W / 2, -CARD_H / 2, 'topRight') this._addCornerBadge('health', -CARD_W / 2, CARD_H / 2, 'bottomLeft') this._addCornerBadge('diplo', CARD_W / 2, CARD_H / 2, 'bottomRight') - // ── 9. Interactivité ────────────────────────────────────────────────────── + // ── 7. Interactivité ────────────────────────────────────────────────────── this.setSize(CARD_W, CARD_H) this.setInteractive({ cursor: 'pointer' }) this.on('pointerover', this._onHover, this) @@ -122,18 +90,17 @@ export class CardObject extends Phaser.GameObjects.Container { _addCornerBadge(attr, bx, by, corner) { const { color, label } = ATTR[attr] const value = this.cardData[attr] - const S = BADGE + const S = BADGE - let rx = bx - let ry = by + let rx = bx, ry = by if (corner === 'topRight' || corner === 'bottomRight') rx = bx - S if (corner === 'bottomLeft' || corner === 'bottomRight') ry = by - S const cornerRadii = { - topLeft: { tl: RADIUS, tr: 2, bl: 2, br: RADIUS / 2 }, - topRight: { tl: 2, tr: RADIUS, bl: RADIUS / 2, br: 2 }, - bottomLeft: { tl: 2, tr: RADIUS / 2, bl: RADIUS, br: 2 }, - bottomRight: { tl: RADIUS / 2, tr: 2, bl: 2, br: RADIUS } + topLeft: { tl: RADIUS, tr: 2, bl: 2, br: RADIUS / 2 }, + topRight: { tl: 2, tr: RADIUS, bl: RADIUS / 2, br: 2 }, + bottomLeft: { tl: 2, tr: RADIUS / 2, bl: RADIUS, br: 2 }, + bottomRight: { tl: RADIUS / 2, tr: 2, bl: 2, br: RADIUS } } const g = this.scene.add.graphics() @@ -145,18 +112,12 @@ export class CardObject extends Phaser.GameObjects.Container { const cy = ry + S / 2 - 2 this.add(this.scene.add.text(cx, cy, String(value), { - fontSize: '14px', - fontFamily: 'Georgia, serif', - fontStyle: 'bold', - color: '#ffffff', - stroke: '#000000', - strokeThickness: 2 + fontSize: '14px', fontFamily: 'Georgia, serif', fontStyle: 'bold', + color: '#ffffff', stroke: '#000000', strokeThickness: 2 }).setOrigin(0.5, 0.5)) this.add(this.scene.add.text(cx, cy + 9, label, { - fontSize: '6px', - fontFamily:'Arial, sans-serif', - color: '#ffffffdd' + fontSize: '6px', fontFamily: 'Arial, sans-serif', color: '#ffffffdd' }).setOrigin(0.5, 0.5)) } @@ -175,12 +136,8 @@ export class CardObject extends Phaser.GameObjects.Container { } _onDown() { - if (this._selected) { - this.deselect() - } else { - this.select() - this.emit('select', this) - } + if (this._selected) { this.deselect() } + else { this.select(); this.emit('select', this) } } select() { @@ -195,15 +152,6 @@ export class CardObject extends Phaser.GameObjects.Container { this.scene.tweens.add({ targets: this, y: this._baseY, duration: 150, ease: 'Power2' }) } - destroy(fromScene) { - // Nettoyer le masque proprement - if (this._maskImg) { - this._maskImg.destroy() - this._maskImg = null - } - super.destroy(fromScene) - } - // ── Utilitaires ─────────────────────────────────────────────────────────────── _darken(hex, factor) { diff --git a/client/src/game/scenes/BootScene.js b/client/src/game/scenes/BootScene.js index 7734b09..c431d86 100644 --- a/client/src/game/scenes/BootScene.js +++ b/client/src/game/scenes/BootScene.js @@ -1,48 +1,124 @@ import Phaser from 'phaser' -// Correspondance id personnage → fichier image const CARD_IMAGES = [ - { id: 'queen', file: 'reine.png' }, - { id: 'prince', file: 'prince.png' }, - { id: 'butcher', file: 'boucher.png' }, - { id: 'doctor', file: 'medecin.png' }, - { id: 'apothecary', file: 'apoticaire.png'}, - { id: 'jester', file: 'fou_du_roi.png'}, - { id: 'assassin', file: 'assassin.png' }, - { id: 'peasant', file: 'paysan.png' }, - { id: 'executioner', file: 'bourreau.png' }, - { id: 'knight', file: 'chevalier.png' }, - { id: 'merchant', file: 'marchand.png' }, - { id: 'bishop', file: 'eveque.png' } + { id: 'queen', file: 'reine.png' }, + { id: 'prince', file: 'prince.png' }, + { id: 'butcher', file: 'boucher.png' }, + { id: 'doctor', file: 'medecin.png' }, + { id: 'apothecary', file: 'apoticaire.png' }, + { id: 'jester', file: 'fou_du_roi.png' }, + { id: 'assassin', file: 'assassin.png' }, + { id: 'peasant', file: 'paysan.png' }, + { id: 'executioner', file: 'bourreau.png' }, + { id: 'knight', file: 'chevalier.png' }, + { id: 'merchant', file: 'marchand.png' }, + { id: 'bishop', file: 'eveque.png' } ] +const CARD_W = 130 +const CARD_H = 190 +const RADIUS = 10 + export class BootScene extends Phaser.Scene { constructor() { super('BootScene') } preload() { - // Charger les images des 12 personnages - CARD_IMAGES.forEach(({ id, file }) => { - this.load.image(`card-${id}`, `/cards/${file}`) - }) - - // Barre de chargement simple + // Barre de chargement const { width, height } = this.scale - const bar = this.add.graphics() + const barBg = this.add.graphics() + const barFill = this.add.graphics() + + barBg.fillStyle(0x2a1500) + barBg.fillRect(width / 2 - 150, height / 2 - 10, 300, 20) this.load.on('progress', (v) => { - bar.clear() - bar.fillStyle(0x1a0a00) - bar.fillRect(width / 2 - 150, height / 2 - 10, 300, 20) - bar.fillStyle(0xd4a732) - bar.fillRect(width / 2 - 150, height / 2 - 10, 300 * v, 20) + barFill.clear() + barFill.fillStyle(0xd4a732) + barFill.fillRect(width / 2 - 150, height / 2 - 10, 300 * v, 20) }) - this.load.on('complete', () => bar.destroy()) + this.load.on('complete', () => { + barBg.destroy() + barFill.destroy() + }) + + // Charger les images brutes + CARD_IMAGES.forEach(({ id, file }) => { + this.load.image(`raw-${id}`, `/cards/${file}`) + }) } create() { + // Pour chaque personnage, générer une texture clippée via Canvas 2D + CARD_IMAGES.forEach(({ id }) => { + this._generateClippedTexture(id) + }) + this.scene.start('GameScene') } + + /** + * Crée une texture `card-{id}` de dimensions CARD_W × CARD_H + * avec l'image clippée en rectangle arrondi via Canvas 2D natif. + * Pas de masque Phaser, pas de RenderTexture — juste du canvas. + */ + _generateClippedTexture(id) { + const rawKey = `raw-${id}` + const outKey = `card-${id}` + + if (!this.textures.exists(rawKey)) return + if (this.textures.exists(outKey)) return + + const srcImage = this.textures.get(rawKey).getSourceImage() + + // Créer un canvas offscreen + const canvas = document.createElement('canvas') + canvas.width = CARD_W + canvas.height = CARD_H + const ctx = canvas.getContext('2d') + + // Chemin arrondi (clip) + ctx.beginPath() + ctx.moveTo(RADIUS, 0) + ctx.lineTo(CARD_W - RADIUS, 0) + ctx.quadraticCurveTo(CARD_W, 0, CARD_W, RADIUS) + ctx.lineTo(CARD_W, CARD_H - RADIUS) + ctx.quadraticCurveTo(CARD_W, CARD_H, CARD_W - RADIUS, CARD_H) + ctx.lineTo(RADIUS, CARD_H) + ctx.quadraticCurveTo(0, CARD_H, 0, CARD_H - RADIUS) + ctx.lineTo(0, RADIUS) + ctx.quadraticCurveTo(0, 0, RADIUS, 0) + ctx.closePath() + ctx.clip() + + // Dessiner l'image en mode "cover" (ratio conservé, centré) + const scaleX = CARD_W / srcImage.width + const scaleY = CARD_H / srcImage.height + const scale = Math.max(scaleX, scaleY) + const dw = srcImage.width * scale + const dh = srcImage.height * scale + const ox = (CARD_W - dw) / 2 + const oy = (CARD_H - dh) / 2 + + ctx.drawImage(srcImage, ox, oy, dw, dh) + + // Dégradé haut (lisibilité badges) + const gTop = ctx.createLinearGradient(0, 0, 0, 45) + gTop.addColorStop(0, 'rgba(0,0,0,0.75)') + gTop.addColorStop(1, 'rgba(0,0,0,0)') + ctx.fillStyle = gTop + ctx.fillRect(0, 0, CARD_W, 45) + + // Dégradé bas (lisibilité titre) + const gBot = ctx.createLinearGradient(0, CARD_H - 40, 0, CARD_H) + gBot.addColorStop(0, 'rgba(0,0,0,0)') + gBot.addColorStop(1, 'rgba(0,0,0,0.85)') + ctx.fillStyle = gBot + ctx.fillRect(0, CARD_H - 40, CARD_W, 40) + + // Enregistrer comme texture Phaser + this.textures.addCanvas(outKey, canvas) + } }