Demo: Descritor de Imagem com Canvas e IA Local

intermediário

Desenhe no canvas do navegador e peça à IA para descrever seu desenho. Demonstra input multimodal (imagem + texto) via Prompt API com Gemini Nano on-device.

⏳

Verificando...

Visão Geral

Demo que transforma o navegador numa prancheta inteligente: você desenha no canvas e a IA descreve o que vê. Usa a Prompt API com input multimodal — o desenho vira um blob de imagem enviado junto com o prompt de texto pro Gemini Nano, tudo rodando on-device.

Pra quem: Desenvolvedores explorando capacidades multimodais da Prompt API (imagem + texto).

Técnica principal: canvas.toBlob() convertendo o desenho em imagem, enviada como input multimodal via LanguageModel.prompt() com conteúdo misto [{type: "image"}, {type: "text"}].

Requisito: declarar { type: "image" } em expectedInputs ao criar a sessão — sem isso o modelo rejeita a imagem com NotSupportedError.


Wireframe

┌─────────────────────────────────────────────────────┐
│  🎨 Descritor de Imagem com Canvas                  │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌─ Toolbar ──────────────────────────────────────┐ │
│  │ ✏️ Lápis │ 🔴🟢🔵⚫ │ [Espessura ▾] │ 🗑️ Limpar│ │
│  └────────────────────────────────────────────────┘ │
│                                                     │
│  ┌────────────────────────────────────────────────┐ │
│  │                                                │ │
│  │              Canvas (480×360)                   │ │
│  │          área de desenho livre                  │ │
│  │                                                │ │
│  └────────────────────────────────────────────────┘ │
│                                                     │
│  [ 🔍 Descrever Desenho ]                           │
│                                                     │
│  ┌────────────────────────────────────────────────┐ │
│  │  "Parece ser um gato sentado, com orelhas      │ │
│  │   triangulares e bigodes. O traço é simples,   │ │
│  │   feito à mão livre com linhas pretas."        │ │
│  └────────────────────────────────────────────────┘ │
│                                                     │
│  ⚠️ Requer Chrome 138+ com flag multimodal ativa   │
└─────────────────────────────────────────────────────┘

HTML

<section class="demo-container" id="canvas-descriptor">
  <h2>🎨 Descritor de Imagem com Canvas</h2>

  <div id="status-bar" class="status" hidden>
    <span id="status-message"></span>
  </div>

  <div class="toolbar">
    <button id="tool-pencil" class="tool active" title="Lápis">✏️</button>
    <div class="color-picker">
      <button class="color-btn active" data-color="#000000" style="background:#000"></button>
      <button class="color-btn" data-color="#dc2626" style="background:#dc2626"></button>
      <button class="color-btn" data-color="#16a34a" style="background:#16a34a"></button>
      <button class="color-btn" data-color="#2563eb" style="background:#2563eb"></button>
    </div>
    <select id="stroke-width">
      <option value="2">Fina</option>
      <option value="5" selected>Média</option>
      <option value="10">Grossa</option>
    </select>
    <button id="btn-clear" title="Limpar canvas">🗑️ Limpar</button>
  </div>

  <canvas id="draw-canvas" width="480" height="360"></canvas>

  <div class="actions">
    <button id="btn-describe" disabled>🔍 Descrever Desenho</button>
  </div>

  <div id="result" class="result-box" hidden>
    <h3>Descrição da IA:</h3>
    <p id="description-output"></p>
  </div>
</section>

Código JavaScript

class CanvasDescriptor {
  constructor() {
    this.canvas = document.getElementById("draw-canvas");
    this.ctx = this.canvas.getContext("2d");
    this.btnDescribe = document.getElementById("btn-describe");
    this.btnClear = document.getElementById("btn-clear");
    this.resultEl = document.getElementById("result");
    this.outputEl = document.getElementById("description-output");
    this.statusBar = document.getElementById("status-bar");
    this.statusMessage = document.getElementById("status-message");
    this.strokeSelect = document.getElementById("stroke-width");

    this.isDrawing = false;
    this.currentColor = "#000000";
    this.hasContent = false;
    this.session = null;

    this.setupCanvas();
    this.setupToolbar();
    this.init();
  }

  setupCanvas() {
    // Fundo branco
    this.ctx.fillStyle = "#ffffff";
    this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);

    // Configuração padrão do traço
    this.ctx.lineCap = "round";
    this.ctx.lineJoin = "round";

    // Eventos de desenho (mouse)
    this.canvas.addEventListener("mousedown", (e) => this.startDraw(e));
    this.canvas.addEventListener("mousemove", (e) => this.draw(e));
    this.canvas.addEventListener("mouseup", () => this.stopDraw());
    this.canvas.addEventListener("mouseleave", () => this.stopDraw());

    // Eventos de desenho (touch)
    this.canvas.addEventListener("touchstart", (e) => {
      e.preventDefault();
      this.startDraw(e.touches[0]);
    });
    this.canvas.addEventListener("touchmove", (e) => {
      e.preventDefault();
      this.draw(e.touches[0]);
    });
    this.canvas.addEventListener("touchend", () => this.stopDraw());
  }

  setupToolbar() {
    // Cores
    document.querySelectorAll(".color-btn").forEach((btn) => {
      btn.addEventListener("click", () => {
        document.querySelector(".color-btn.active")?.classList.remove("active");
        btn.classList.add("active");
        this.currentColor = btn.dataset.color;
      });
    });

    // Limpar
    this.btnClear.addEventListener("click", () => this.clearCanvas());

    // Descrever
    this.btnDescribe.addEventListener("click", () => this.describe());
  }

  getPos(e) {
    const rect = this.canvas.getBoundingClientRect();
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    };
  }

  startDraw(e) {
    this.isDrawing = true;
    const pos = this.getPos(e);
    this.ctx.beginPath();
    this.ctx.moveTo(pos.x, pos.y);
  }

  draw(e) {
    if (!this.isDrawing) return;
    const pos = this.getPos(e);
    this.ctx.strokeStyle = this.currentColor;
    this.ctx.lineWidth = parseInt(this.strokeSelect.value);
    this.ctx.lineTo(pos.x, pos.y);
    this.ctx.stroke();
    this.hasContent = true;
  }

  stopDraw() {
    this.isDrawing = false;
    this.ctx.closePath();
  }

  clearCanvas() {
    this.ctx.fillStyle = "#ffffff";
    this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
    this.hasContent = false;
    this.resultEl.hidden = true;
  }

  async init() {
    if (!("LanguageModel" in window)) {
      this.showStatus("❌ Prompt API não disponível. Use Chrome 138+.", "error");
      return;
    }

    const availability = await LanguageModel.availability();

    if (availability === "unavailable") {
      this.showStatus("❌ Modelo não disponível neste dispositivo.", "error");
      return;
    }

    if (availability === "downloading") {
      this.showStatus("⏳ Baixando modelo... Aguarde.", "loading");
    }

    try {
      this.session = await LanguageModel.create({
        expectedInputs: [
          { type: "image" },
          { type: "text", languages: ["pt", "en"] }
        ],
        expectedOutputs: [{ type: "text", languages: ["pt"] }],
        initialPrompts: [{
          role: "system",
          content: "Você é um assistente visual. Descreva imagens de forma clara e objetiva em português. Se o desenho for abstrato, descreva formas, cores e padrões observados."
        }],
        monitor(m) {
          m.addEventListener("downloadprogress", (e) => {
            const pct = Math.round(e.loaded * 100);
            document.getElementById("status-message").textContent =
              `⏳ Baixando modelo... ${pct}%`;
          });
        }
      });

      this.btnDescribe.disabled = false;
      this.hideStatus();
    } catch (err) {
      this.showStatus(`❌ Erro ao criar sessão: ${err.message}`, "error");
    }
  }

  async describe() {
    if (!this.hasContent) {
      this.showStatus("⚠️ Desenhe algo no canvas primeiro.", "error");
      setTimeout(() => this.hideStatus(), 2000);
      return;
    }

    this.btnDescribe.disabled = true;
    this.btnDescribe.textContent = "⏳ Analisando...";
    this.resultEl.hidden = true;

    try {
      const blob = await this.canvasToBlob();

      const response = await this.session.prompt([
        { type: "image", content: blob },
        { type: "text", content: "Descreva o que você vê neste desenho. Identifique formas, objetos e cores presentes." }
      ]);

      this.outputEl.textContent = response;
      this.resultEl.hidden = false;
    } catch (err) {
      if (err.name === "AbortError") return;
      this.showStatus(`❌ Erro: ${err.message}`, "error");
    } finally {
      this.btnDescribe.disabled = false;
      this.btnDescribe.textContent = "🔍 Descrever Desenho";
    }
  }

  canvasToBlob() {
    return new Promise((resolve) => {
      this.canvas.toBlob((blob) => resolve(blob), "image/png");
    });
  }

  showStatus(msg, type) {
    this.statusBar.hidden = false;
    this.statusBar.className = `status status-${type}`;
    this.statusMessage.textContent = msg;
  }

  hideStatus() {
    this.statusBar.hidden = true;
  }
}

document.addEventListener("DOMContentLoaded", () => new CanvasDescriptor());

Fluxo UX

  1. Página carrega → verifica suporte da Prompt API + flag multimodal
  2. Modelo disponível → botão “Descrever” habilitado; se downloading, mostra progresso
  3. Usuário desenha → canvas captura traços com cor e espessura escolhidas
  4. Clica “Descrever Desenho” → canvas exportado como PNG blob via toBlob()
  5. Blob enviado como input multimodal → session.prompt([{type:"image"}, {type:"text"}])
  6. Resposta chega → descrição exibida no card de resultado
  7. Limpar → reseta canvas e esconde resultado anterior

Edge Cases e Tratamento de Erros

Cenário Tratamento
Prompt API não existe (window.LanguageModel undefined) Mensagem + link para requisitos
Flag multimodal desativada Erro de sessão capturado, orienta ativar a flag
Canvas vazio (nenhum traço) Bloqueia envio, alerta amigável temporário
Desenho muito simples (1 ponto) Funciona, IA descreve o que conseguir
Blob muito grande (canvas HD) Canvas fixo em 480×360 mantém blob leve (~50KB)
Touch em mobile Eventos touch mapeados, preventDefault evita scroll
Sessão abortada Silencioso (AbortError ignorado)
Modelo sem suporte multimodal Erro de sessão com mensagem explicativa
Múltiplos cliques rápidos Botão desabilitado durante processamento

CSS Essencial

#draw-canvas {
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  cursor: crosshair;
  touch-action: none;
  display: block;
  max-width: 100%;
}

.toolbar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem;
  background: #f9fafb;
  border-radius: 8px;
  margin-bottom: 0.75rem;
  flex-wrap: wrap;
}

.color-picker {
  display: flex;
  gap: 0.25rem;
}

.color-btn {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
}

.color-btn.active {
  border-color: #1f2937;
  box-shadow: 0 0 0 2px #fff, 0 0 0 4px #1f2937;
}

.tool.active {
  background: #dbeafe;
  border-color: #2563eb;
}

.result-box {
  margin-top: 1rem;
  padding: 1rem;
  background: #f0fdf4;
  border: 1px solid #bbf7d0;
  border-radius: 8px;
}

.result-box p {
  line-height: 1.6;
  color: #1f2937;
}

.status-error { color: #dc2626; }
.status-loading { color: #2563eb; }