fix: card image clipping via RenderTexture, remove white hover bg

This commit is contained in:
kevin.lhereec committed 2026-08-17 14:24:42 +02:00
1 parent c7672b1793
commit 0a8c89610b
1 file changed
+103 -86
+103 -86
View File
@@ -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 ───────────────────────────────────────────────────────────────