Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Draw 2D graphics with the HTML5 Canvas API — shapes, text, images, gradients, transformations, pixel manipulation, and offscreen rendering. Use when tasks involve generating images server-side (with node-canvas), creating charts, image compositing, watermarking, or browser-based drawing applications.
.claude/skills/terminalskills-canvas-api/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 24% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 59% | 0% |
| case-04 | ✓→✗ | ▼ Worse | 45% | 0% |
| case-19 | ✓→✗ | ▼ Worse | 141% | 0% |
| case-10 | ✓→✓ | = Same ✓ | 14% | 0% |
The Canvas 2D API draws shapes, text, and images to a bitmap. In Node.js, the canvas package provides the same API for server-side image generation.
bash# Install node-canvas for server-side rendering. Requires system deps (Cairo). npm install canvas
typescript// src/canvas/shapes.ts — Create a canvas, draw rectangles, circles, and lines. // Works identically in browser (document.createElement) and Node (createCanvas). import { createCanvas } from "canvas"; const canvas = createCanvas(800, 600); const ctx = canvas.getContext("2d"); // Background ctx.fillStyle = "#1a1a2e"; ctx.fillRect(0, 0, 800, 600); // Rectangle with rounded corners ctx.beginPath(); ctx.roundRect(50, 50, 200, 120, 12); ctx.fillStyle = "#e94560"; ctx.fill(); // Circle ctx.beginPath(); ctx.arc(450, 110, 60, 0, Math.PI * 2); ctx.fillStyle = "#0f3460"; ctx.fill(); // Line with gradient const gradient = ctx.createLinearGradient(50, 300, 750, 300); gradient.addColorStop(0, "#e94560"); gradient.addColorStop(1, "#0f3460"); ctx.strokeStyle = gradient; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(50, 300); ctx.lineTo(750, 300); ctx.stroke();
typescript// src/canvas/text.ts — Render styled text on canvas with measurements. import { createCanvas } from "canvas"; const canvas = createCanvas(800, 200); const ctx = canvas.getContext("2d"); ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, 800, 200); // Title text ctx.font = "bold 48px sans-serif"; ctx.fillStyle = "#1a1a2e"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("Hello Canvas", 400, 80); // Measure text for positioning const metrics = ctx.measureText("Hello Canvas"); console.log(`Width: ${metrics.width}px`);
typescript// src/canvas/composite.ts — Load images and composite them with blend modes. // Useful for watermarking, overlays, and social media image generation. import { createCanvas, loadImage } from "canvas"; import fs from "fs"; export async function addWatermark(imagePath: string, watermarkPath: string, outputPath: string) { const image = await loadImage(imagePath); const watermark = await loadImage(watermarkPath); const canvas = createCanvas(image.width, image.height); const ctx = canvas.getContext("2d"); // Draw base image ctx.drawImage(image, 0, 0); // Draw watermark with transparency ctx.globalAlpha = 0.3; ctx.drawImage( watermark, image.width - watermark.width - 20, image.height - watermark.height - 20 ); const buffer = canvas.toBuffer("image/png"); fs.writeFileSync(outputPath, buffer); }
typescript// src/canvas/pixels.ts — Read and modify individual pixels for image processing. // Apply filters like grayscale, invert, or custom convolution kernels. import { createCanvas, loadImage } from "canvas"; export async function grayscale(imagePath: string): Promise<Buffer> { const image = await loadImage(imagePath); const canvas = createCanvas(image.width, image.height); const ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const avg = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114; data[i] = data[i + 1] = data[i + 2] = avg; } ctx.putImageData(imageData, 0, 0); return canvas.toBuffer("image/png"); }
typescript// src/canvas/export.ts — Save canvas output as PNG, JPEG, or PDF. import { createCanvas } from "canvas"; import fs from "fs"; const canvas = createCanvas(400, 400); const ctx = canvas.getContext("2d"); ctx.fillStyle = "#e94560"; ctx.fillRect(0, 0, 400, 400); // PNG fs.writeFileSync("output.png", canvas.toBuffer("image/png")); // JPEG with quality fs.writeFileSync("output.jpg", canvas.toBuffer("image/jpeg", { quality: 0.9 })); // PDF (node-canvas supports PDF backend) const pdfCanvas = createCanvas(400, 400, "pdf"); const pdfCtx = pdfCanvas.getContext("2d"); pdfCtx.fillStyle = "#0f3460"; pdfCtx.fillRect(0, 0, 400, 400); fs.writeFileSync("output.pdf", pdfCanvas.toBuffer("application/pdf"));
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | pass→pass | 10,149 | 5,689 | -44% | 1 | 1 | 0% | 2,283 | 2,604 | +14% | 0 | 0 | — |
case-05 | pass→pass | 17,495 | 14,246 | -19% | 1 | 1 | 0% | 3,940 | 4,424 | +12% | 0 | 0 | — |
case-01 | fail→fail | 10,571 | 6,715 | -36% | 1 | 1 | 0% | 2,267 | 2,827 | +25% | 0 | 0 | — |
case-02 | fail→pass | 12,312 | 7,376 | -40% | 1 | 1 | 0% | 2,457 | 3,044 | +24% | 0 | 0 | — |
case-03 | pass→pass | 20,289 | 17,318 | -15% | 1 | 1 | 0% | 4,671 | 5,591 | +20% | 0 | 0 | — |
case-04 | pass→fail | 15,101 | 13,530 | -10% | 1 | 1 | 0% | 2,930 | 4,248 | +45% | 0 | 0 | — |
case-06 | pass→pass | 12,420 | 8,040 | -35% | 1 | 1 | 0% | 2,478 | 2,780 | +12% | 0 | 0 | — |
case-07 | pass→pass | 9,570 | 5,447 | -43% | 1 | 1 | 0% | 1,948 | 2,414 | +24% | 0 | 0 | — |
case-08 | pass→pass | 9,189 | 8,253 | -10% | 1 | 1 | 0% | 1,940 | 2,942 | +52% | 0 | 0 | — |
case-09 | fail→fail | 11,765 | 6,442 | -45% | 1 | 1 | 0% | 2,577 | 2,707 | +5% | 0 | 0 | — |
case-11 | pass→pass | 11,330 | 9,812 | -13% | 1 | 1 | 0% | 2,286 | 3,214 | +41% | 0 | 0 | — |
case-12 | pass→pass | 4,284 | 2,238 | -48% | 1 | 1 | 0% | 655 | 1,789 | +173% | 0 | 0 | — |
case-13 | fail→pass | 7,480 | 4,861 | -35% | 1 | 1 | 0% | 1,497 | 2,374 | +59% | 0 | 0 | — |
case-14 | pass→pass | 3,583 | 3,268 | -9% | 1 | 1 | 0% | 775 | 2,082 | +169% | 0 | 0 | — |
case-15 | pass→pass | 6,268 | 2,564 | -59% | 1 | 1 | 0% | 1,318 | 1,906 | +45% | 0 | 0 | — |
case-16 | pass→pass | 4,037 | 2,283 | -43% | 1 | 1 | 0% | 738 | 1,769 | +140% | 0 | 0 | — |
case-17 | pass→pass | 3,045 | 3,536 | +16% | 1 | 1 | 0% | 594 | 1,956 | +229% | 0 | 0 | — |
case-18 | pass→pass | 10,982 | 11,437 | +4% | 1 | 1 | 0% | 2,694 | 3,823 | +42% | 0 | 0 | — |
case-19 | pass→fail | 3,931 | 3,037 | -23% | 1 | 1 | 0% | 822 | 1,977 | +141% | 0 | 0 | — |
case-20 | pass→pass | 2,464 | 2,255 | -8% | 1 | 1 | 0% | 427 | 1,701 | +298% | 0 | 0 | — |
case-21 | pass→pass | 6,747 | 4,767 | -29% | 1 | 1 | 0% | 1,308 | 2,291 | +75% | 0 | 0 | — |
case-22 | pass→pass | 6,530 | 2,231 | -66% | 1 | 1 | 0% | 1,320 | 1,802 | +37% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of 0 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.