Add animation controls and enhance arrow drawing in CourtDrawingRender component

Implemented animation controls for arrow rendering in the CourtDrawingRender component, allowing users to start and stop animations. Enhanced the drawArrow method to support progressive rendering based on animation state. Added computed properties to determine the presence of arrows and updated the component's lifecycle methods for proper animation management. Improved styling for animation buttons to enhance user experience.
This commit is contained in:
Torsten Schulz (local)
2025-11-04 15:54:18 +01:00
parent e8766b919a
commit a3ed130211

View File

@@ -1,5 +1,15 @@
<template>
<div class="render-container">
<div class="animation-controls" v-if="hasArrows">
<button
type="button"
class="btn-animate"
@click="startAnimation"
:disabled="isAnimating"
>
{{ isAnimating ? 'Animation läuft...' : 'Animation starten' }}
</button>
</div>
<canvas
ref="canvas"
:width="config.canvas.width"
@@ -31,6 +41,9 @@ export default {
return {
canvas: null,
ctx: null,
isAnimating: false,
animationState: null, // { currentArrowIndex, startTime, arrowProgress }
animationFrameId: null,
config: {
// Mehr Platz links für die drei Startkreise (Rand außerhalb des Tisches)
canvas: { width: 640, height: 400 },
@@ -98,9 +111,23 @@ export default {
}
};
},
computed: {
hasArrows() {
if (!this.drawingData) return false;
// Pfeile werden nur gezeichnet wenn ein main target existiert (tp)
// Zusätzliche Pfeile werden nur gezeichnet wenn tp && extras.length
// Daher muss hasArrows nur true sein, wenn ein main target existiert
const tp = Number(this.drawingData.targetPosition);
return !!tp;
}
},
watch: {
drawingData: {
handler() {
// Wenn sich drawingData ändert, stoppe laufende Animation
if (this.isAnimating) {
this.stopAnimation();
}
this.redraw();
},
deep: true,
@@ -116,6 +143,10 @@ export default {
this.redraw();
});
},
beforeUnmount() {
// Cleanup: stoppe Animation falls noch läuft
this.stopAnimation();
},
methods: {
init() {
this.canvas = this.$refs.canvas;
@@ -277,24 +308,35 @@ export default {
const startYOffset = isVH ? (sc.radius + ar.vhOffsetY) : ar.rhOffsetY;
return { x: startX, y: y + startYOffset };
},
drawArrow(ctx, from, to, color, label) {
drawArrow(ctx, from, to, color, label, progress = 1.0) {
// progress: 0.0 = nicht sichtbar, 1.0 = vollständig gezeichnet
if (progress <= 0) return;
const { width, headLength } = this.config.arrows;
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = width;
// Berechne aktuellen Endpunkt basierend auf progress
const currentX = from.x + (to.x - from.x) * progress;
const currentY = from.y + (to.y - from.y) * progress;
// line
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.lineTo(currentX, currentY);
ctx.stroke();
// arrow head
const angle = Math.atan2(to.y - from.y, to.x - from.x);
ctx.beginPath();
ctx.moveTo(to.x, to.y);
ctx.lineTo(to.x - headLength * Math.cos(angle - Math.PI / 6), to.y - headLength * Math.sin(angle - Math.PI / 6));
ctx.lineTo(to.x - headLength * Math.cos(angle + Math.PI / 6), to.y - headLength * Math.sin(angle + Math.PI / 6));
ctx.closePath();
ctx.fill();
// arrow head nur zeichnen wenn progress > 0.1 (kleine Puffer für bessere Sichtbarkeit)
if (progress > 0.1) {
const angle = Math.atan2(currentY - from.y, currentX - from.x);
ctx.beginPath();
ctx.moveTo(currentX, currentY);
ctx.lineTo(currentX - headLength * Math.cos(angle - Math.PI / 6), currentY - headLength * Math.sin(angle - Math.PI / 6));
ctx.lineTo(currentX - headLength * Math.cos(angle + Math.PI / 6), currentY - headLength * Math.sin(angle + Math.PI / 6));
ctx.closePath();
ctx.fill();
}
},
drawLabelBelow(ctx, text, anchor) {
if (!text) return;
@@ -319,24 +361,26 @@ export default {
const ctx = this.ctx;
const from = this.getStartPoint();
// First arrow: to right target
// Sammle alle Pfeile für Animation
const allArrows = [];
const tp = Number(this.drawingData.targetPosition);
// First arrow: to right target
if (tp) {
const to = this.computeRightTargetPosition(tp);
// Zielmarkierung (unter dem Pfeilkopf)
this.drawHitMarker(ctx, to);
const strokeSide = this.drawingData.strokeType || '';
const spinAbbrev = this.abbrevSpin(this.drawingData.spinType);
// Text gehört an die Quelle (ohne "target")
const sourceLabel = `${strokeSide} ${spinAbbrev}`.trim();
const toEnd = { x: to.x - this.config.targetCircles.radius, y: to.y };
this.drawArrow(ctx, from, toEnd, this.config.arrows.primaryColor);
// Unter dem Startkreis beschriften
const startCenter = this.getStartCircleCenter();
this.drawLabelBelow(ctx, sourceLabel, startCenter);
allArrows.push({
from: from,
to: toEnd,
toCenter: to,
color: this.config.arrows.primaryColor,
index: 0,
label: `${this.drawingData.strokeType || ''} ${this.abbrevSpin(this.drawingData.spinType)}`.trim(),
labelAnchor: this.getStartCircleCenter()
});
}
// Additional arrows (up to 3), alternating left/right, starting from right target
// Additional arrows
const extras = Array.isArray(this.drawingData.additionalStrokes) ? this.drawingData.additionalStrokes : [];
if (tp && extras.length) {
const colors = [
@@ -344,31 +388,63 @@ export default {
this.config.arrows.tertiaryColor,
this.config.arrows.quaternaryColor
];
const max = Math.min(3, extras.length);
const max = extras.length;
const sourceRightCenter = this.computeRightTargetPosition(tp);
let prevPoint = { x: sourceRightCenter.x - this.config.targetCircles.radius, y: sourceRightCenter.y };
for (let i = 0; i < max; i++) {
const stroke = extras[i];
const side = i % 2 === 0 ? 'left' : 'right';
let toCenter, toPoint;
if (side === 'left') {
const leftNum = Number(stroke.targetPosition);
const toLeftCenter = this.computeLeftTargetPosition(leftNum);
// Zielmarkierung links
this.drawHitMarker(ctx, toLeftCenter);
const toLeft = { x: toLeftCenter.x + this.config.leftTargetCircles.radius, y: toLeftCenter.y };
this.drawArrow(ctx, prevPoint, toLeft, colors[i]);
prevPoint = toLeft; // beginnt an der Pfeilspitze des vorherigen
toCenter = this.computeLeftTargetPosition(leftNum);
toPoint = { x: toCenter.x + this.config.leftTargetCircles.radius, y: toCenter.y };
} else {
const rightNum = Number(stroke.targetPosition);
const toRightCenter = this.computeRightTargetPosition(rightNum);
// Zielmarkierung rechts
this.drawHitMarker(ctx, toRightCenter);
const toRight = { x: toRightCenter.x - this.config.targetCircles.radius, y: toRightCenter.y };
this.drawArrow(ctx, prevPoint, toRight, colors[i]);
prevPoint = toRight;
toCenter = this.computeRightTargetPosition(rightNum);
toPoint = { x: toCenter.x - this.config.targetCircles.radius, y: toCenter.y };
}
allArrows.push({
from: prevPoint,
to: toPoint,
toCenter: toCenter,
color: colors[i % colors.length], // Zyklisch durch die verfügbaren Farben wechseln
index: i + 1
});
prevPoint = toPoint;
}
}
// Zeichne Pfeile basierend auf Animationsstatus
allArrows.forEach((arrow, idx) => {
let progress = 1.0;
if (this.isAnimating && this.animationState) {
const state = this.animationState;
if (idx < state.currentArrowIndex) {
// Pfeil bereits vollständig gezeichnet
progress = 1.0;
} else if (idx === state.currentArrowIndex) {
// Aktuell animierter Pfeil
progress = state.arrowProgress;
} else {
// Pfeil noch nicht dran
progress = 0.0;
}
}
// Zeichne Pfeil
this.drawArrow(ctx, arrow.from, arrow.to, arrow.color, null, progress);
// Zeichne Zielmarkierung nur wenn Pfeil vollständig animiert wurde
if (progress >= 1.0 && arrow.toCenter) {
this.drawHitMarker(ctx, arrow.toCenter);
}
// Zeichne Label nur wenn Pfeil vollständig
if (progress >= 1.0 && arrow.label && arrow.labelAnchor) {
this.drawLabelBelow(ctx, arrow.label, arrow.labelAnchor);
}
});
},
applyRenderCode(code) {
try {
@@ -522,6 +598,73 @@ export default {
Gegenläufer: 'GL'
};
return map[spin] || spin;
},
startAnimation() {
if (this.isAnimating || !this.hasArrows) return;
// Zähle Anzahl der Pfeile - muss exakt der Logik in drawArrowsAndLabels() entsprechen
const tp = Number(this.drawingData.targetPosition);
const extras = Array.isArray(this.drawingData.additionalStrokes) ? this.drawingData.additionalStrokes : [];
// Erster Pfeil wird nur hinzugefügt wenn tp truthy
// Zusätzliche Pfeile werden nur hinzugefügt wenn tp truthy UND extras.length > 0
const mainArrow = tp ? 1 : 0;
const additionalArrows = (tp && extras.length) ? extras.length : 0;
const totalArrows = mainArrow + additionalArrows;
if (totalArrows === 0) return;
this.isAnimating = true;
this.animationState = {
currentArrowIndex: 0,
startTime: performance.now(),
arrowProgress: 0.0,
totalArrows: totalArrows
};
this.animateFrame();
},
animateFrame() {
if (!this.isAnimating || !this.animationState) {
this.stopAnimation();
return;
}
const now = performance.now();
const state = this.animationState;
const elapsed = now - state.startTime;
const durationPerArrow = 1000; // 1 Sekunde pro Pfeil
// Berechne aktuellen Pfeil-Index basierend auf vergangener Zeit
const currentArrowIndex = Math.floor(elapsed / durationPerArrow);
if (currentArrowIndex >= state.totalArrows) {
// Animation abgeschlossen - alle Pfeile wurden animiert
this.stopAnimation();
return;
}
// Berechne Progress für den aktuellen Pfeil (0.0 bis 1.0 innerhalb seiner Sekunde)
const timeWithinCurrentArrow = elapsed % durationPerArrow;
const arrowProgress = Math.min(1.0, timeWithinCurrentArrow / durationPerArrow);
state.currentArrowIndex = currentArrowIndex;
state.arrowProgress = arrowProgress;
// Neuzeichnen
this.redraw();
// Nächster Frame
this.animationFrameId = requestAnimationFrame(() => this.animateFrame());
},
stopAnimation() {
this.isAnimating = false;
this.animationState = null;
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
// Finales Redraw um alle Pfeile vollständig zu zeigen
this.redraw();
}
}
};
@@ -531,7 +674,41 @@ export default {
.render-container {
width: 100%;
}
canvas { display: block; max-width: 100%; height: auto; }
canvas {
display: block;
max-width: 100%;
height: auto;
}
.animation-controls {
text-align: center;
margin-bottom: 1rem;
}
.btn-animate {
padding: 0.5rem 1rem;
font-size: 0.9rem;
font-weight: 500;
border: 1px solid #007bff;
border-radius: 4px;
background-color: #007bff;
color: white;
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s;
}
.btn-animate:hover:not(:disabled) {
background-color: #0056b3;
border-color: #0056b3;
}
.btn-animate:disabled {
opacity: 0.6;
cursor: not-allowed;
background-color: #6c757d;
border-color: #6c757d;
}
</style>