Custom Renderers
Complete control over waveform visualization with custom rendering implementations
Custom renderers provide complete control over waveform visualization while maintaining compatibility with the library’s core features like caching, events, and option management.
In this guide, we’ll build a complete ConnectedWaveRenderer that creates connected wave segments with alternating up/down patterns, developing it progressively by showing only the changes in each step.
Step 1: Basic Implementation
Section titled “Step 1: Basic Implementation”import type { CustomRenderer, RenderCache, WaveformOptions } from "waveform-renderer";
export class ConnectedWaveRenderer implements CustomRenderer { public render( ctx: CanvasRenderingContext2D, cache: RenderCache, options: Required<WaveformOptions>, staticPath?: Path2D, ): boolean { ctx.save();
try { ctx.clearRect(0, 0, cache.canvasWidth, cache.canvasHeight); ctx.translate(0, cache.canvasHeight / 2); this.renderWaveform(ctx, cache, options, options.backgroundColor);
if (options.progress > 0) { this.renderProgress(ctx, cache, options); } } finally { ctx.restore(); }
return true; }
private renderWaveform( ctx: CanvasRenderingContext2D, cache: RenderCache, options: Required<WaveformOptions>, strokeColor: string, ): void { ctx.strokeStyle = strokeColor; ctx.lineWidth = options.barWidth; ctx.lineCap = "round"; ctx.lineJoin = "round";
for (let i = 0; i < cache.totalBars; i++) { const bar = cache.bars[i]; const x = bar.x; const height = bar.height / 2; const isEven = i % 2 === 0;
this.drawLineSegment(ctx, x, height, cache.singleUnitWidth, isEven); } }
private drawLineSegment( ctx: CanvasRenderingContext2D, x: number, height: number, width: number, isEven: boolean, ): void { ctx.beginPath(); const y = isEven ? height : -height;
ctx.moveTo(x, 0); ctx.lineTo(x, y); ctx.arc(x + width / 2, y, width / 2, Math.PI, 0, isEven); ctx.lineTo(x + width, 0);
ctx.stroke(); }
private renderProgress(ctx: CanvasRenderingContext2D, cache: RenderCache, options: Required<WaveformOptions>): void { ctx.save(); const progressWidth = cache.canvasWidth * options.progress; ctx.beginPath(); ctx.rect(0, -cache.canvasHeight / 2, progressWidth, cache.canvasHeight); ctx.clip(); this.renderWaveform(ctx, cache, options, options.color); ctx.restore(); }}
Step 2: Adding Custom Properties and Position Support
Section titled “Step 2: Adding Custom Properties and Position Support” import type { CustomRenderer, RenderCache, WaveformOptions, RenderMode } from "waveform-renderer";
interface ConnectedWaveOptions { startWithUp?: boolean; }
export class ConnectedWaveRenderer implements CustomRenderer { private connectedOptions: Required<ConnectedWaveOptions>;
constructor(customOptions: ConnectedWaveOptions = {}) { this.connectedOptions = { startWithUp: true, ...customOptions, }; }
public updateOptions(options: Partial<ConnectedWaveOptions>): void { this.connectedOptions = { ...this.connectedOptions, ...options }; }
public render(/* ... */): boolean { ctx.save(); try { ctx.clearRect(0, 0, cache.canvasWidth, cache.canvasHeight); ctx.translate(0, cache.canvasHeight / 2); this.setupCoordinateSystem(ctx, cache, options); this.renderWaveform(ctx, cache, options, options.backgroundColor); this.renderWaveform(ctx, cache, options, options.backgroundColor, false);
if (options.progress > 0) { this.renderProgress(ctx, cache, options); } } finally { ctx.restore(); } return true; }
private setupCoordinateSystem(ctx: CanvasRenderingContext2D, cache: RenderCache, options: Required<WaveformOptions>): void { switch (options.position) { case "center": ctx.translate(0, cache.canvasHeight / 2); break; case "top": ctx.translate(0, cache.canvasHeight / 4); break; case "bottom": ctx.translate(0, (cache.canvasHeight * 3) / 4); break; } }
private renderWaveform( ctx: CanvasRenderingContext2D, cache: RenderCache, options: Required<WaveformOptions>, strokeColor: string strokeColor: string, isProgress: boolean = false ): void { ctx.strokeStyle = strokeColor; ctx.lineWidth = options.barWidth; ctx.lineCap = "round"; ctx.lineJoin = "round"; const segmentWidth = cache.singleUnitWidth;
for (let i = 0; i < cache.totalBars; i++) { const bar = cache.bars[i]; const x = bar.x; const height = bar.height / 2; const x = bar.x + (cache.singleUnitWidth - segmentWidth) / 2; let height = this.calculateHeight(bar.height / 2, cache.canvasHeight, options.position); const isEven = i % 2 === 0; const isEven = this.connectedOptions.startWithUp ? i % 2 === 0 : i % 2 === 1; this.drawLineSegment(ctx, x, height, cache.singleUnitWidth, isEven); this.drawLineSegment(ctx, x, height, segmentWidth, isEven, options); } }
private calculateHeight(barHeight: number, canvasHeight: number, position: RenderMode): number { switch (position) { case "top": case "bottom": return Math.min(barHeight, canvasHeight / 3); default: return Math.min(barHeight, canvasHeight / 2); } }
private drawLineSegment( ctx: CanvasRenderingContext2D, x: number, height: number, width: number, isEven: boolean isEven: boolean, options: Required<WaveformOptions> ): void { /* unchanged */ }
private renderProgress(/* ... */): void { ctx.save(); const progressWidth = cache.canvasWidth * options.progress; ctx.beginPath(); ctx.rect(0, -cache.canvasHeight / 2, progressWidth, cache.canvasHeight); ctx.clip(); const progressWidth = cache.canvasWidth * options.progress; ctx.beginPath(); switch (options.position) { case "center": ctx.rect(0, -cache.canvasHeight / 2, progressWidth, cache.canvasHeight); break; case "top": ctx.rect(0, -cache.canvasHeight / 4, progressWidth, cache.canvasHeight); break; case "bottom": ctx.rect(0, (-cache.canvasHeight * 3) / 4, progressWidth, cache.canvasHeight); break; } ctx.clip(); this.renderWaveform(ctx, cache, options, options.color); this.renderWaveform(ctx, cache, options, options.color, true); ctx.restore(); }}
Step 3: Adding Border Support and Progress Line
Section titled “Step 3: Adding Border Support and Progress Line”public render(/* ... */): boolean { ctx.save(); try { ctx.clearRect(0, 0, cache.canvasWidth, cache.canvasHeight); this.setupCoordinateSystem(ctx, cache, options); this.renderWaveform(ctx, cache, options, options.backgroundColor, false); this.renderWaveform(ctx, cache, options, options.backgroundColor, false);
if (options.progress > 0) { this.renderProgress(ctx, cache, options); }
ctx.restore(); ctx.save(); if (options.progressLine && options.progress > 0) { this.drawProgressLine(ctx, cache, options); } } finally { ctx.restore(); } return true;}
private drawLineSegment( ctx: CanvasRenderingContext2D, x: number, height: number, width: number, isEven: boolean, options: Required<WaveformOptions> options: Required<WaveformOptions>, isProgress: boolean): void { ctx.beginPath(); const y = isEven ? height : -height; const y = isEven ? height : -height; ctx.moveTo(x, 0); ctx.lineTo(x, y); ctx.arc(x + width / 2, y, width / 2, Math.PI, 0, isEven); ctx.lineTo(x + width, 0); ctx.stroke(); if (options.borderWidth > 0) { ctx.save(); ctx.strokeStyle = options.borderColor; ctx.lineWidth = options.borderWidth; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, y); ctx.arc(x + width / 2, y, width / 2, Math.PI, 0, isEven); ctx.lineTo(x + width, 0); ctx.stroke(); ctx.restore(); }}
private drawProgressLine(ctx: CanvasRenderingContext2D, cache: RenderCache, options: Required<WaveformOptions>): void { if (!options.progressLine) return; const { color, heightPercent, position, style, width } = options.progressLine; const x = cache.canvasWidth * options.progress; const lineHeight = cache.canvasHeight * (heightPercent || 1); ctx.save(); ctx.strokeStyle = color || "#FF0000"; ctx.lineWidth = width || 2; ctx.lineCap = "round"; let startY: number; let endY: number; switch (position || "center") { case "bottom": startY = cache.canvasHeight; endY = cache.canvasHeight - lineHeight; break; case "top": startY = 0; endY = lineHeight; break; default: startY = (cache.canvasHeight - lineHeight) / 2; endY = (cache.canvasHeight + lineHeight) / 2; break; } if (style && style !== "solid") { const [dashSize, gapSize] = style === "dashed" ? [8, 4] : [2, 2]; ctx.setLineDash([dashSize, gapSize]); } ctx.beginPath(); ctx.moveTo(x, startY); ctx.lineTo(x, endY); ctx.stroke(); ctx.restore();}
private renderWaveform(/* ... */): void { for (let i = 0; i < cache.totalBars; i++) { this.drawLineSegment(ctx, x, height, segmentWidth, isEven, options); this.drawLineSegment(ctx, x, height, segmentWidth, isEven, options, isProgress); }}
Step 4: Usage Example
Section titled “Step 4: Usage Example”import { WaveformRenderer } from "waveform-renderer";import { ConnectedWaveRenderer } from "./connected-wave-renderer";
const connectedRenderer = new ConnectedWaveRenderer({ startWithUp: true });
const waveform = new WaveformRenderer(canvas, peaks, { color: "#2196F3", backgroundColor: "#E3F2FD", barWidth: 3, gap: 10, borderWidth: 1, borderColor: "#1565C0", position: "center", progressLine: { color: "#FF4081", width: 2, style: "dashed", heightPercent: 0.8, },});
waveform.setCustomRenderer(connectedRenderer);connectedRenderer.updateOptions({ startWithUp: false });waveform.setOptions({ position: "top" });waveform.setOptions({ position: "bottom" });waveform.setCustomRenderer(undefined);
Debugging
Section titled “Debugging”const waveform = new WaveformRenderer(canvas, peaks, { debug: true,});
Next Steps
Section titled “Next Steps”- See Render Hooks for lighter customization options
- Check API Reference for complete method documentation
- Try the Interactive Demo to see custom renderers in action