File size: 4,340 Bytes
67b042f
 
 
 
09644e5
67b042f
 
 
 
 
 
 
09644e5
67b042f
 
 
 
09644e5
 
 
 
 
 
 
67b042f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
09644e5
67b042f
 
09644e5
67b042f
 
 
 
 
 
 
 
 
09644e5
67b042f
 
 
09644e5
67b042f
 
 
 
 
 
 
 
09644e5
67b042f
 
 
 
09644e5
67b042f
 
 
 
 
 
 
 
 
 
 
 
09644e5
 
 
67b042f
 
 
 
 
 
09644e5
67b042f
 
 
 
 
 
 
 
09644e5
67b042f
 
 
 
09644e5
67b042f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
09644e5
67b042f
 
 
 
 
 
 
 
 
 
 
 
 
09644e5
67b042f
 
 
 
 
09644e5
 
 
67b042f
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import gifshot from 'gifshot';

export interface GifGenerationOptions {
  images: string[];
  interval?: number; // Duration of each frame in seconds
  gifWidth?: number;
  gifHeight?: number;
  quality?: number;
}

export interface GifGenerationResult {
  success: boolean;
  image?: string; // GIF data URL
  error?: string;
}

/**
 * Add step counter to an image
 * @param imageSrc Image source (base64 or URL)
 * @param stepNumber Step number
 * @param totalSteps Total number of steps
 * @param width Image width
 * @param height Image height
 * @returns Promise resolved with modified image in base64
 */
const addStepCounter = async (
  imageSrc: string,
  stepNumber: number,
  totalSteps: number,
  width: number,
  height: number
): Promise<string> => {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = 'anonymous';

    img.onload = () => {
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext('2d');

      if (!ctx) {
        reject(new Error('Cannot get canvas context'));
        return;
      }

      // Draw the image
      ctx.drawImage(img, 0, 0, width, height);

      // Configure counter style
      const fontSize = Math.max(12, Math.floor(height * 0.08));
      const padding = Math.max(6, Math.floor(height * 0.03));
      const text = `${stepNumber}/${totalSteps}`;

      ctx.font = `bold ${fontSize}px Arial, sans-serif`;
      const textMetrics = ctx.measureText(text);
      const textWidth = textMetrics.width;
      const textHeight = fontSize;

      // Position at bottom right
      const x = width - textWidth - padding * 2;
      const y = height - padding * 2;

      // Draw semi-transparent rectangle for readability
      ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
      ctx.fillRect(
        x - padding,
        y - textHeight - padding,
        textWidth + padding * 2,
        textHeight + padding * 2
      );

      // Draw black text
      ctx.fillStyle = '#000000';
      ctx.textBaseline = 'top';
      ctx.fillText(text, x, y - textHeight);

      // Convert canvas to base64
      resolve(canvas.toDataURL('image/png'));
    };

    img.onerror = () => {
      reject(new Error('Failed to load image'));
    };

    img.src = imageSrc;
  });
};

/**
 * Generate a GIF from a list of images (base64 or URLs)
 * @param options GIF generation options
 * @returns Promise resolved with generation result
 */
export const generateGif = async (
  options: GifGenerationOptions
): Promise<GifGenerationResult> => {
  const {
    images,
    interval = 1.5, // 1.5 seconds per frame by default
    gifWidth = 400,
    gifHeight = 200,
    quality = 10,
  } = options;

  if (!images || images.length === 0) {
    return {
      success: false,
      error: 'No images provided to generate GIF',
    };
  }

  try {
    // Add counter to each image
    const imagesWithCounter = await Promise.all(
      images.map((img, index) =>
        addStepCounter(img, index + 1, images.length, gifWidth, gifHeight)
      )
    );

    return new Promise((resolve) => {
      gifshot.createGIF(
        {
          images: imagesWithCounter,
          interval,
          gifWidth,
          gifHeight,
          numFrames: imagesWithCounter.length,
          frameDuration: interval,
          sampleInterval: quality,
        },
        (obj: { error: boolean; errorMsg?: string; image?: string }) => {
          if (obj.error) {
            resolve({
              success: false,
              error: obj.errorMsg || 'Error during GIF generation',
            });
          } else {
            resolve({
              success: true,
              image: obj.image,
            });
          }
        }
      );
    });
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
};

/**
 * Download a GIF (data URL) with a filename
 * @param dataUrl GIF data URL
 * @param filename Filename to download
 */
export const downloadGif = (dataUrl: string, filename: string = 'trace-replay.gif') => {
  const link = document.createElement('a');
  link.href = dataUrl;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
};