animation du ballon avec le joueur et debut d'animation du ballon seul

This commit is contained in:
2024-10-14 14:14:39 +02:00
parent f51a4001d8
commit 3f2eff7fbd
3 changed files with 197 additions and 124 deletions

View File

@@ -7,9 +7,10 @@
.toolbar { .toolbar {
display: flex; display: flex;
justify-content: space-between; flex-wrap: wrap;
align-items: center; justify-content: space-evenly;
width: 620px; /* largeur égale au canvas pour aligner */ align-items: stretch;
width: 690px; /* largeur égale au canvas pour aligner */
margin-bottom: 10px; margin-bottom: 10px;
} }
@@ -48,13 +49,22 @@ canvas {
margin-top: 15px; margin-top: 15px;
} }
.toolbar-act {
margin-top: 10px;
}
.toolbar-mode {
text-align: center;
}
.clear-button { .clear-button {
padding: 10px 20px; padding: 10px 20px;
margin: 2px;
background-color: #ff4d4d; /* Rouge vif */ background-color: #ff4d4d; /* Rouge vif */
color: white; color: white;
border: none; border: none;
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: 12px bold;
border-radius: 5px; border-radius: 5px;
} }
@@ -62,6 +72,36 @@ canvas {
background-color: #ff1a1a; /* couleur plus foncée au survol */ background-color: #ff1a1a; /* couleur plus foncée au survol */
} }
.action-button {
padding: 10px 20px;
margin: 2px;
background-color: #a04dff; /* Rouge vif */
color: white;
border: none;
cursor: pointer;
font-size: 12px bold;
border-radius: 5px;
}
.action-button:hover {
background-color: #ca1aff; /* couleur plus foncée au survol */
}
.anim-button {
padding: 10px 20px;
margin: 2px;
background-color: hsl(224, 100%, 65%); /* Rouge vif */
color: white;
border: none;
cursor: pointer;
font-size: 12px bold;
border-radius: 5px;
}
.anim-button:hover {
background-color: #1a3cff; /* couleur plus foncée au survol */
}
.timeline-container { .timeline-container {
width: 100%; width: 100%;
margin-top: 20px; margin-top: 20px;

View File

@@ -1,50 +1,39 @@
<div class="football-container"> <div class="football-container">
<!-- Sélecteur de couleurs au-dessus du canvas --> <!-- Sélecteur de couleurs au-dessus du canvas -->
<div class="toolbar"> <div class="toolbar">
<!-- <div class="color-palette"> <!-- Saisie du nombre de joueurs -->
<div *ngFor="let color of colors" [style.background]="color" (click)="setColor(color)" class="color-box" [class.selected]="color === selectedColor"> <div class="player-count">
<label for="playerCount">Nombre de joueurs : </label>
<input type="number" id="playerCount" [(ngModel)]="playerCount" min="1" max="25" (change)="updatePlayers()">
</div>
<!-- Saisie du nombre de plots -->
<div class="plot-count">
<label for="plotCount">Nombre de plots : </label>
<input type="number" id="plotCount" [(ngModel)]="plotCount" min="1" max="50" (change)="updatePlots()">
</div>
<!-- Saisie du nombre de plots -->
<div class="piquet-count">
<label for="piquetCount">Nombre de piquets : </label>
<input type="number" id="piquetCount" [(ngModel)]="piquetCount" min="1" max="20" (change)="updatePiquets()">
</div>
<!-- Bouton pour réinitialiser le dessin -->
<!--<button (click)="clearCanvas()" class="clear-button">Réinitialiser</button>-->
<div class="toolbar-act">
<button (click)="setInteractionMode('move')" class="action-button">Déplacer Élément</button>
<button (click)="setInteractionMode('animate')" class="action-button">Dessiner Vecteur</button>
<button (click)="reinitPlayers()" class="anim-button">Initialiser</button>
<button (click)="playTimeline()" class="anim-button">Play Timeline</button>
<button (click)="stopTimeline()" class="anim-button">Stop Timeline</button>
</div> </div>
</div> -->
<!-- Saisie du nombre de joueurs --> <div class="toolbar-mode">
<div class="player-count"> <span><h2>{{ interactionMode }}</h2></span>
<label for="playerCount">Nombre de joueurs : </label> </div>
<input type="number" id="playerCount" [(ngModel)]="playerCount" min="1" max="25" (change)="updatePlayers()"> </div>
</div>
<!-- Saisie du nombre de plots -->
<div class="plot-count">
<label for="plotCount">Nombre de plots : </label>
<input type="number" id="plotCount" [(ngModel)]="plotCount" min="1" max="50" (change)="updatePlots()">
</div>
<!-- Saisie du nombre de plots -->
<div class="piquet-count">
<label for="piquetCount">Nombre de piquets : </label>
<input type="number" id="piquetCount" [(ngModel)]="piquetCount" min="1" max="20" (change)="updatePiquets()">
</div>
<!-- Bouton pour réinitialiser le dessin -->
<button (click)="clearCanvas()" class="clear-button">Réinitialiser</button>
<!-- Sélection de forme -->
<!-- <div class="shape-selector">
<label for="shape">Forme :</label>
<select id="shape" [(ngModel)]="selectedShape">
<option value="none">Aucune</option>
<option value="rectangle">Rectangle</option>
<option value="circle">Cercle</option>
<option value="line">Ligne</option>
</select>
</div> -->
<button (click)="setInteractionMode('move')">Déplacer Élément</button>
<button (click)="setInteractionMode('animate')">Dessiner Vecteur</button>
<!--<button (click)="toggleLoop()">Boucle d'Animation</button>
<button (click)="replayTimeline()">Play again</button>-->
<button (click)="reinitPlayers()">Ré-init les positions</button>
<button (click)="playTimeline()">Play Timeline</button>
<button (click)="stopTimeline()">Stop Timeline</button>
</div>
<!-- <canvas #canvas width="800" height="600" <!-- <canvas #canvas width="800" height="600"
(mousedown)="startDrawing($event)" (mousedown)="startDrawing($event)"

View File

@@ -70,8 +70,6 @@ interface Player {
isMoving: boolean; // Flag d'animation du joueur isMoving: boolean; // Flag d'animation du joueur
progress: number; // Paramètre de progression sur la courbe progress: number; // Paramètre de progression sur la courbe
hasBall: boolean; // Indique si le joueur a le ballon hasBall: boolean; // Indique si le joueur a le ballon
startX: number;
startY: number;
steps: TimelineStep[]; // Stockage des étapes d'animation steps: TimelineStep[]; // Stockage des étapes d'animation
currentStepIndex: number; // Suivre l'étape actuelle dans la timeline currentStepIndex: number; // Suivre l'étape actuelle dans la timeline
} }
@@ -89,21 +87,17 @@ interface Player {
export class FootballFieldComponent { export class FootballFieldComponent {
@ViewChild('canvas', { static: true }) canvas!: ElementRef<HTMLCanvasElement>; @ViewChild('canvas', { static: true }) canvas!: ElementRef<HTMLCanvasElement>;
private ctx!: CanvasRenderingContext2D; private ctx!: CanvasRenderingContext2D;
//private drawing = false;
//private lastX = 0;
//private lastY = 0;
private startX = 0; private startX = 0;
private startY = 0; private startY = 0;
private draggingPlayer: Player | null = null; private draggingPlayer: Player | null = null;
private draggingElement: any = null; // Element (plot, piquet) en cours de déplacement sur le terrain private draggingElement: any = null; // Element (plot, piquet) en cours de déplacement sur le terrain
private attachedPlayer: Player | null = null; private attachedPlayer: Player | null = null;
private magnetRadius: number = 35; // Distance à laquelle le ballon s'attache au joueur private magnetRadius: number = 50; // Distance à laquelle le ballon s'attache au joueur
//private attachedToPlayer: boolean = false; // indique si le ballon est attache a un joueur //private attachedToPlayer: boolean = false; // indique si le ballon est attache a un joueur
private offsetX: number = 0; private offsetX: number = 0;
private offsetY: number = 0; private offsetY: number = 0;
private test: boolean = false; private test: boolean = false;
private prevAngle: number = 0; private prevAngle: number = 0;
//private isDrawingVector: boolean = false;
private endX: number = 0; private endX: number = 0;
private endY: number = 0; private endY: number = 0;
@@ -114,31 +108,12 @@ export class FootballFieldComponent {
// Timestamp de début de l'animation // Timestamp de début de l'animation
private animationStartTime: number = 0; private animationStartTime: number = 0;
//public colors: string[] = ['#ff0000', '#00ff00',
// '#0000ff', '#ffff00',
// '#ff00ff', '#00ffff',
// '#000000', '#808080',
// '#ffa500', '#8a2be2'];
//public selectedColor: string = this.colors[0]; // Couleur sélectionné par défaut (rouge)
// Options de forme
//public selectedShape: string = 'none'; // Par défaut, aucune forme
public playerCount: number = 8; // Nombre de joueurs par défaut public playerCount: number = 8; // Nombre de joueurs par défaut
public plotCount: number = 2; // Nombre de plots par défaut public plotCount: number = 2; // Nombre de plots par défaut
public piquetCount: number = 2; // Nombre de plots par défaut public piquetCount: number = 2; // Nombre de plots par défaut
public players: Player[] = []; public players: Player[] = [];
public plots: Plot[] = []; public plots: Plot[] = [];
public piquets: Piquet[] = []; public piquets: Piquet[] = [];
/*public ball: Circle = { id: 0,
x:400,
y:300,
radius:10,
isDragging:false,
isMoving:false,
progress: 0,
startX: 150,
startY: 150,
timeline: [],
currentStepIndex: 0 };*/
public ball: Ball = { id: 0, public ball: Ball = { id: 0,
design: { x:400, y:300, design: { x:400, y:300,
radius:10, radius:10,
@@ -150,7 +125,8 @@ export class FootballFieldComponent {
currentStepIndex: 0, currentStepIndex: 0,
attachedToPlayer: null }; attachedToPlayer: null };
public selectedPlayer: Player | null = null; public selectedPlayer: Player | null = null;
public isDrawingLine: boolean = false; public isDrawingPlayerLine: boolean = false;
public isDrawingBallLine: boolean = false;
public loopAnimation: boolean = false; // Pour répéter l'animation public loopAnimation: boolean = false; // Pour répéter l'animation
public debugInfo: string = ""; // Variable pour les informations de debogage public debugInfo: string = ""; // Variable pour les informations de debogage
public interactionMode: 'move' | 'animate' = 'move'; // Mode d'interaction sélectionné public interactionMode: 'move' | 'animate' = 'move'; // Mode d'interaction sélectionné
@@ -249,7 +225,7 @@ export class FootballFieldComponent {
this.updateDebugInfo(); this.updateDebugInfo();
// Dessiner la ligne si en mode dessin // Dessiner la ligne si en mode dessin
if (this.isDrawingLine && this.selectedPlayer) { if (this.isDrawingPlayerLine && this.selectedPlayer) {
//console.log("[Draw Field] drawing vector"); //console.log("[Draw Field] drawing vector");
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(this.startX, this.startY); ctx.moveTo(this.startX, this.startY);
@@ -257,6 +233,13 @@ export class FootballFieldComponent {
ctx.strokeStyle = '#FF0000'; ctx.strokeStyle = '#FF0000';
ctx.lineWidth = 4; ctx.lineWidth = 4;
ctx.stroke(); ctx.stroke();
} else if (this.isDrawingBallLine) {
ctx.beginPath();
ctx.moveTo(this.startX, this.startY);
ctx.lineTo(this.endX, this.endY);
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 4;
ctx.stroke();
} }
} }
@@ -286,9 +269,8 @@ export class FootballFieldComponent {
// Déplacer le ballon si attaché à un joueur // Déplacer le ballon si attaché à un joueur
if (this.ball.attachedToPlayer) { if (this.ball.attachedToPlayer) {
const player = this.ball.attachedToPlayer; const player = this.ball.attachedToPlayer;
// Le ballon suit le joueur avec un léger décalage (ajustable) // Le ballon suit le joueur
this.ball.design.x = player.design.x + player.design.radius + this.ball.design.radius + 5; // Décalage à droite du joueur this.updateBallPositionOnPlayer(player);
this.ball.design.y = player.design.y; // Même hauteur que le joueur
} }
// Mettre à jour la position du trait de visualisation du temps // Mettre à jour la position du trait de visualisation du temps
@@ -296,6 +278,14 @@ export class FootballFieldComponent {
} }
}); });
// Mettre à jour la position du ballon
if (this.isAnimating) {
if (this.ball.steps.length > 0) {
console.log("PLUP");
this.updateBallPosition();
}
}
this.drawField(); this.drawField();
requestAnimationFrame(() => this.animate()); requestAnimationFrame(() => this.animate());
@@ -321,16 +311,15 @@ export class FootballFieldComponent {
const elmnts = document.querySelectorAll('[id=time-indicator]'); const elmnts = document.querySelectorAll('[id=time-indicator]');
if (elmnts) { if (elmnts) {
const timeline = document.querySelector('.timeline') as HTMLElement; const timeline = document.querySelector('.timeline') as HTMLElement;
const timelineWidth = timeline.offsetWidth; // Largeur totale de la timeline this.timelineWidth = timeline.offsetWidth; // Largeur totale de la timeline
// Calcul de la position du trait en pixels // Calcul de la position du trait en pixels
let timePosition = (currentTime / this.getTotalTimelineDuration()) * timelineWidth; let timePosition = (currentTime / this.getTotalTimelineDuration()) * this.timelineWidth;
// Si l'indicateur est à la fin de la timeline, on arrete l'animation // Si l'indicateur est à la fin de la timeline, on arrete l'animation et on replace
if(timePosition >= timelineWidth) { // l'indicateur au debut de la timeline
timePosition = timelineWidth; if(timePosition >= this.timelineWidth) {
timePosition = 0;
this.isPlaying = false; this.isPlaying = false;
} }
// Mettre à jour la position du trait
//timeIndicator.style.left = `${timePosition}px`;
// Mettre à jour la position du trait // Mettre à jour la position du trait
elmnts.forEach(element => elmnts.forEach(element =>
@@ -353,6 +342,33 @@ export class FootballFieldComponent {
} }
} }
private updateBallPosition() {
if (this.ball.steps.length === 0 || this.ball.currentStepIndex >= this.ball.steps.length) {
return; // Pas d'animation si la timeline est vide
}
const step = this.ball.steps[this.ball.currentStepIndex];
// Avancer le joueur sur la ligne avec un LERP (Linear Interpolation)
this.ball.progress += this.animationSpeed;
// Temps actuel
const currentTime = performance.now();
if (this.ball.progress >= 1) {
this.ball.progress = 0; // Réinitialiser la progression
this.ball.design.x = step.endX;
this.ball.design.y = step.endY;
this.ball.currentStepIndex++; // Passer à l'étape suivante
// Si on atteint la fin de la timeline, arrêter
if (this.ball.currentStepIndex >= this.ball.steps.length) {
this.isAnimating = false;
}
} else {
const t = this.ball.progress;
this.ball.design.x = (1 - t) * step.startX + t * step.endX;
this.ball.design.y = (1 - t) * step.startY + t * step.endY;
}
}
private updatePlayerPosition(player: Player) { private updatePlayerPosition(player: Player) {
if (player.steps.length === 0 || player.currentStepIndex >= player.steps.length) { if (player.steps.length === 0 || player.currentStepIndex >= player.steps.length) {
return; // Pas d'animation si la timeline est vide return; // Pas d'animation si la timeline est vide
@@ -363,17 +379,6 @@ export class FootballFieldComponent {
player.progress += this.animationSpeed; player.progress += this.animationSpeed;
// Temps actuel // Temps actuel
const currentTime = performance.now(); const currentTime = performance.now();
/*
// Vérifier si le temps actuel correspond à l'intervalle de cette étape
if (currentTime >= step.startTime && currentTime <= step.endTime) {
const t = (currentTime - step.startTime) / (step.endTime - step.startTime);
player.x = (1 - t) * step.startX + t * step.endX;
player.y = (1 - t) * step.startY + t * step.endY;
} else if (currentTime > step.endTime) {
player.currentStepIndex++;
}
*/
if (player.progress >= 1) { if (player.progress >= 1) {
player.progress = 0; // Réinitialiser la progression player.progress = 0; // Réinitialiser la progression
@@ -394,11 +399,10 @@ export class FootballFieldComponent {
player.design.x = (1 - t) * step.startX + t * step.endX; player.design.x = (1 - t) * step.startX + t * step.endX;
player.design.y = (1 - t) * step.startY + t * step.endY; player.design.y = (1 - t) * step.startY + t * step.endY;
// Attacher le ballon si un joueur se rapproche suffisamment // Si le joueur a le ballon "aimanté", on met à jour la position
const nearestPlayer = this.getNearestPlayerToBall(); // du ballon en même temps que celui du ballon
if (nearestPlayer && this.getDistance(this.ball.design, nearestPlayer.design) < this.magnetRadius) { if (player.hasBall) {
//this.attachedPlayer = nearestPlayer; this.updateBallPositionOnPlayer(player);
this.updateBallPositionOnPlayer(nearestPlayer);
} }
} }
} }
@@ -425,8 +429,6 @@ export class FootballFieldComponent {
isDragging: false, isDragging: false,
isMoving: false, isMoving: false,
progress: 0, progress: 0,
startX: 0,
startY: 0,
steps: [], steps: [],
currentStepIndex: 0, currentStepIndex: 0,
hasBall: false }); hasBall: false });
@@ -571,12 +573,26 @@ export class FootballFieldComponent {
} }
if (!this.selectedPlayer) { if (!this.selectedPlayer) {
this.selectPlayer(x, y); console.log("[Mouse Down|Animate] No player selected");
} else if (!this.isDrawingLine) { //this.selectPlayer(x, y);
if (!this.isDrawingBallLine) {
if (this.isInsideCircle(this.ball.design, x, y)) {
this.startX = this.ball.design.x;
this.startY = this.ball.design.y;
this.isDrawingBallLine = true;
console.log("[Mouse Down|Animate] drawing ball line - Start:(",
this.startX,",",
this.startY,"), End:(",
this.endX,",",
this.endY,
")");
}
}
} else if (!this.isDrawingPlayerLine) {
this.startX = this.selectedPlayer.design.x; this.startX = this.selectedPlayer.design.x;
this.startY = this.selectedPlayer.design.y; this.startY = this.selectedPlayer.design.y;
this.isDrawingLine = true; this.isDrawingPlayerLine = true;
console.log("[Mouse Down|Animate] drawing line - Start:(", console.log("[Mouse Down|Animate] drawing player line - Start:(",
this.startX,",", this.startX,",",
this.startY,"), End:(", this.startY,"), End:(",
this.endX,",", this.endX,",",
@@ -605,11 +621,11 @@ export class FootballFieldComponent {
const { x, y } = this.getMousePos(event); const { x, y } = this.getMousePos(event);
if (this.interactionMode === 'animate') { if (this.interactionMode === 'animate') {
console.log("[Mouse Up] Stop animating elements"); console.log("[Mouse Up] Stop animating elements");
if (this.isDrawingLine && this.selectedPlayer) { if (this.isDrawingPlayerLine && this.selectedPlayer) {
console.log("[Mouse Up|Animate] Stop drawing vector"); console.log("[Mouse Up|Animate] Stop drawing player line");
this.endX = x; this.endX = x;
this.endY = y; this.endY = y;
this.isDrawingLine = false; this.isDrawingPlayerLine = false;
// Ajouter l'étape dans la timeline du joueur // Ajouter l'étape dans la timeline du joueur
let prevStartTime:number = 0; let prevStartTime:number = 0;
if (this.selectedPlayer.steps && this.selectedPlayer.steps.length > 0) { if (this.selectedPlayer.steps && this.selectedPlayer.steps.length > 0) {
@@ -627,16 +643,43 @@ export class FootballFieldComponent {
endY: this.endY, endY: this.endY,
duration: 1000 // Durée d'animation arbitraire, peut être ajustée duration: 1000 // Durée d'animation arbitraire, peut être ajustée
}); });
console.log("timeline:", this.selectedPlayer.steps); console.log("timeline player:", this.selectedPlayer.steps);
//this.selectedPlayer.currentStepIndex = 0; // Réinitialiser au début de la timeline // Initialiser l'index à l'étape précédente
this.selectedPlayer.currentStepIndex = this.selectedPlayer.steps.length - 1;
this.startAnimation();
} else if (this.isDrawingBallLine) {
console.log("[Mouse Up|Animate] Stop drawing ball line");
this.endX = x;
this.endY = y;
this.isDrawingBallLine = false;
// Ajouter l'étape dans la timeline du ballon
let prevStartTime:number = 0;
if (this.ball.steps && this.ball.steps.length > 0) {
prevStartTime = this.ball.steps[this.ball.steps.length - 1].endTime;
} else {
prevStartTime = 0;
}
this.ball.steps.push({
startTime: prevStartTime,
endTime: prevStartTime + 1000,
startX: this.startX,
startY: this.startY,
endX: this.endX,
endY: this.endY,
duration: 1000 // Durée d'animation arbitraire, peut être ajustée
});
console.log("timeline ball:", this.ball.steps);
// Initialiser l'index à l'étape précédente
this.ball.currentStepIndex = this.ball.steps.length - 1;
this.startAnimation(); this.startAnimation();
} }
} else if (this.interactionMode === 'move') { } else if (this.interactionMode === 'move') {
console.log("[Mouse Up] Stop moving elements"); console.log("[Mouse Up] Stop moving elements");
this.stopDragging(); this.stopDragging();
// Si on déplace un joueur alors que celui-ci a une animation, // Si on déplace un joueur alors que celui-ci a une timeline,
// on supprime toute sa timeline // on supprime la supprime
for (const player of this.players) { for (const player of this.players) {
if (this.isInsideCircle(player.design, x, y)) { if (this.isInsideCircle(player.design, x, y)) {
// Sélectionner le joueur // Sélectionner le joueur
@@ -703,11 +746,6 @@ export class FootballFieldComponent {
this.updateBallPositionOnPlayer(nearestPlayer); this.updateBallPositionOnPlayer(nearestPlayer);
} }
// // Si le ballon est attaché au joueur deplacer le ballon avec lui en calculant
// // la tangente
// if (this.attachedToPlayer) {
// this.updateBallPositionOnTangent();
// }
this.drawField(); this.drawField();
return; return;
} }
@@ -743,7 +781,6 @@ export class FootballFieldComponent {
} }
private updateBallPositionOnPlayer(player: Player) { private updateBallPositionOnPlayer(player: Player) {
//const player = this.attachedPlayer;
if (!player) return; if (!player) return;
const dx = this.ball.design.x - player.design.x; const dx = this.ball.design.x - player.design.x;
@@ -759,8 +796,11 @@ export class FootballFieldComponent {
this.ball.design.x = player.design.x + (player.design.radius + this.ball.design.radius) * Math.cos(angle); this.ball.design.x = player.design.x + (player.design.radius + this.ball.design.radius) * Math.cos(angle);
this.ball.design.y = player.design.y + (player.design.radius + this.ball.design.radius) * Math.sin(angle); this.ball.design.y = player.design.y + (player.design.radius + this.ball.design.radius) * Math.sin(angle);
// Pour l'animation
this.ball.attachedToPlayer = player;
player.hasBall = true;
} }
/*
private selectPlayer(x: number, y: number) { private selectPlayer(x: number, y: number) {
this.selectedPlayer = this.getNearestPlayer(x, y); this.selectedPlayer = this.getNearestPlayer(x, y);
if (this.selectedPlayer) { if (this.selectedPlayer) {
@@ -768,11 +808,12 @@ export class FootballFieldComponent {
this.selectedPlayer.startY = this.selectedPlayer.design.y; this.selectedPlayer.startY = this.selectedPlayer.design.y;
} }
} }
*/
private startAnimation() { private startAnimation() {
if (this.selectedPlayer) { if (this.selectedPlayer || this.ball.currentStepIndex < this.ball.steps.length) {
this.isAnimating = true; this.isAnimating = true;
this.selectedPlayer.progress = 0; // Réinitialiser la progression if (this.selectedPlayer) this.selectedPlayer.progress = 0; // Réinitialiser la progression
this.ball.progress = 0;
} }
} }
@@ -874,6 +915,9 @@ export class FootballFieldComponent {
this.updatePlayerPosition(player); this.updatePlayerPosition(player);
}); });
// Mise à jour de l'indicateur de timeline
this.updateTimeIndicator(0);
const currentTime = performance.now(); const currentTime = performance.now();
this.updateTimeIndicator(currentTime); this.updateTimeIndicator(currentTime);
} }
@@ -1038,7 +1082,7 @@ export class FootballFieldComponent {
getTotalTimelineDuration(): number { getTotalTimelineDuration(): number {
// Calculer la durée totale de la timeline, par exemple basée sur la durée totale de l'animation // Calculer la durée totale de la timeline, par exemple basée sur la durée totale de l'animation
return 10000; // Par exemple, 10 000 ms pour une timeline de 10 secondes return 20000; // Par exemple, 10 000 ms pour une timeline de 10 secondes
} }
playTimeline() { playTimeline() {