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"}].

Flag necessária: chrome://flags/#prompt-api-for-gemini-nano-multimodal-input


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 multimodalsession.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árioTratamento
Prompt API não existe (window.LanguageModel undefined)Mensagem + link para requisitos
Flag multimodal desativadaErro 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 mobileEventos touch mapeados, preventDefault evita scroll
Sessão abortadaSilencioso (AbortError ignorado)
Modelo sem suporte multimodalErro de sessão com mensagem explicativa
Múltiplos cliques rápidosBotã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; }