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:
@@ -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>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user