From 0a8c89610be2ffb61ef61ec65844a908ae1fa952 Mon Sep 17 00:00:00 2001 From: "kevin.lhereec" Date: Mon, 17 Aug 2026 14:24:42 +0200 Subject: [PATCH] fix: card image clipping via RenderTexture, remove white hover bg --- client/src/game/objects/CardObject.js | 189 ++++++++++++++------------ 1 file changed, 103 insertions(+), 86 deletions(-) diff --git a/client/src/game/objects/CardObject.js b/client/src/game/objects/CardObject.js index e3eac28..506d4e1 100644 --- a/client/src/game/objects/CardObject.js +++ b/client/src/game/objects/CardObject.js @@ -1,6 +1,5 @@ import Phaser from 'phaser' -// Couleurs des 4 attributs const ATTR = { food: { color: 0xf0c030, label: 'NOU' }, war: { color: 0xdd2222, label: 'GUE' }, @@ -11,15 +10,15 @@ const ATTR = { const CARD_W = 130 const CARD_H = 190 const RADIUS = 10 -const BADGE = 30 // taille du badge de coin -const TITLE_H = 26 // hauteur du bandeau titre en bas +const BADGE = 30 +const TITLE_H = 26 export class CardObject extends Phaser.GameObjects.Container { constructor(scene, x, y, cardData) { super(scene, x, y) - this.cardData = cardData + this.cardData = cardData this._selected = false - this._baseY = y + this._baseY = y this._build() scene.add.existing(this) } @@ -32,84 +31,94 @@ 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 ──────────────────────────────────────────────── + const bgColor = this._darken(teamHex, 0.25) const bg = this.scene.add.graphics() - bg.fillStyle(this._darken(teamHex, 0.2)) + bg.fillStyle(bgColor) bg.fillRoundedRect(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H, RADIUS) this.add(bg) - // ── 3. Image portrait (ratio conservé, recadré au centre) ───────────────── + // ── 3. Image portrait via RenderTexture (reste solidaire du container) ─── if (hasTexture) { - const img = this.scene.add.image(0, 0, textureKey) - - // Calculer le scale pour couvrir toute la carte sans déformer (cover) - const scaleX = CARD_W / img.width - const scaleY = CARD_H / img.height - const scale = Math.max(scaleX, scaleY) - img.setScale(scale) - - // Décaler légèrement vers le haut pour centrer le visage (haut du portrait) - img.setY(-CARD_H * 0.05) - img.setAlpha(0.9) - - // Masque rectangulaire pour clipper l'image dans les bords de la carte - const mask = this.scene.add.graphics() - mask.fillStyle(0xffffff) - mask.fillRoundedRect( - this.x - CARD_W / 2, - this.y - CARD_H / 2, - CARD_W, CARD_H, RADIUS + // RenderTexture aux dimensions de la carte — clippe naturellement + const rt = this.scene.add.renderTexture( + -CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H ) - img.setMask(mask.createGeometryMask()) - this.add(img) - } else { - // Fallback : initiale - const initial = this.scene.add.text(0, 0, this.cardData.name[0], { - fontSize: '70px', fontFamily: 'Georgia', color: '#ffffff', alpha: 0.1 - }).setOrigin(0.5) - this.add(initial) + // Dessiner le fond arrondi dans la RT (pour que les coins soient propres) + const rtBg = this.scene.make.graphics({ x: 0, y: 0, add: false }) + rtBg.fillStyle(bgColor) + rtBg.fillRoundedRect(0, 0, CARD_W, CARD_H, RADIUS) + rt.draw(rtBg, 0, 0) + rtBg.destroy() + + // Calculer le scale "cover" et le centrage dans la RT + 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) + const dw = frame.width * scale + const dh = frame.height * scale + const ox = (CARD_W - dw) / 2 + const oy = (CARD_H - dh) / 2 + + // Créer un Image temporaire pour dessiner dans la RT + const tmpImg = this.scene.make.image({ key: textureKey, add: false }) + tmpImg.setScale(scale) + rt.draw(tmpImg, ox + dw / 2, oy + dh / 2) + tmpImg.destroy() + + // Dégradé haut (pour lisibilité des badges) + const gradT = this.scene.make.graphics({ x: 0, y: 0, add: false }) + gradT.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0.75, 0.75, 0, 0) + gradT.fillRect(0, 0, CARD_W, BADGE + 10) + rt.draw(gradT, 0, 0) + gradT.destroy() + + // Dégradé bas (pour lisibilité du titre) + const gradB = this.scene.make.graphics({ x: 0, y: 0, add: false }) + gradB.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0, 0, 0.85, 0.85) + gradB.fillRect(0, CARD_H - TITLE_H - 12, CARD_W, TITLE_H + 12) + rt.draw(gradB, 0, 0) + gradB.destroy() + + this.add(rt) } - // ── 4. Overlay dégradé bas (pour lisibilité du titre) ──────────────────── - const gradBot = this.scene.add.graphics() - gradBot.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0, 0, 0.85, 0.85) - gradBot.fillRect(-CARD_W / 2, CARD_H / 2 - TITLE_H - 10, CARD_W, TITLE_H + 10) - this.add(gradBot) - - // ── 5. Overlay dégradé haut (pour lisibilité des badges) ───────────────── - const gradTop = this.scene.add.graphics() - gradTop.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0.7, 0.7, 0, 0) - gradTop.fillRect(-CARD_W / 2, -CARD_H / 2, CARD_W, BADGE + 8) - this.add(gradTop) - - // ── 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 ─────────────────────────────────────────────────────── - const nameText = this.scene.add.text(0, CARD_H / 2 - TITLE_H / 2, this.cardData.name, { - fontSize: '13px', - fontFamily: 'Georgia, serif', - fontStyle: 'bold', - color: '#f5e6c8', - stroke: '#000000', - strokeThickness: 3 - }).setOrigin(0.5, 0.5) + // ── 5. Titre en BAS ─────────────────────────────────────────────────────── + const nameText = this.scene.add.text( + 0, CARD_H / 2 - TITLE_H / 2, + this.cardData.name, { + fontSize: '12px', + fontFamily: 'Georgia, serif', + fontStyle: 'bold', + color: '#f5e6c8', + stroke: '#000000', + strokeThickness: 3, + backgroundColor: undefined // pas de fond blanc + } + ).setOrigin(0.5, 0.5) this.add(nameText) - // ── 8. Badges attributs dans les 4 coins (AU-DESSUS de l'image) ─────────── - 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') + // ── 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,16 +131,16 @@ export class CardObject extends Phaser.GameObjects.Container { const value = this.cardData[attr] const S = BADGE - // Position du carré selon le coin - let rx = bx, ry = by + let rx = bx + let 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() @@ -140,37 +149,41 @@ export class CardObject extends Phaser.GameObjects.Container { this.add(g) const cx = rx + S / 2 - const cy = ry + S / 2 - 3 + const cy = ry + S / 2 - 2 - // Valeur 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, + backgroundColor: undefined }).setOrigin(0.5, 0.5)) - // Label 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', + backgroundColor: undefined }).setOrigin(0.5, 0.5)) } - // ── Interactions ───────────────────────────────────────────────────────────── + // ── Interactions ────────────────────────────────────────────────────────────── _onHover() { if (this._selected) return - this.scene.tweens.add({ targets: this, y: this._baseY - 18, duration: 120, ease: 'Power2' }) + this.scene.tweens.add({ + targets: this, y: this._baseY - 18, duration: 120, ease: 'Power2' + }) this.setDepth(10) } _onOut() { if (this._selected) return - this.scene.tweens.add({ targets: this, y: this._baseY, duration: 120, ease: 'Power2' }) + this.scene.tweens.add({ + targets: this, y: this._baseY, duration: 120, ease: 'Power2' + }) this.setDepth(0) } @@ -186,13 +199,17 @@ export class CardObject extends Phaser.GameObjects.Container { select() { this._selected = true this.setDepth(20) - this.scene.tweens.add({ targets: this, y: this._baseY - 28, duration: 150, ease: 'Back.Out' }) + this.scene.tweens.add({ + targets: this, y: this._baseY - 28, duration: 150, ease: 'Back.Out' + }) } deselect() { this._selected = false this.setDepth(0) - this.scene.tweens.add({ targets: this, y: this._baseY, duration: 150, ease: 'Power2' }) + this.scene.tweens.add({ + targets: this, y: this._baseY, duration: 150, ease: 'Power2' + }) } // ── Utilitaires ───────────────────────────────────────────────────────────────