Quebra-Cabeça de Imagem
Uma imagem é dividida em peças embaralhadas que o aprendiz reordena arrastando. O processo de reconstrução cria familiaridade visual com o diagrama, mapa ou conceito representado.
Experimente
Monte o diagrama do Ciclo da Água
A imagem mostra o ciclo completo da água — evaporação, condensação, chuva e escoamento de volta ao mar. Reconstrua a paisagem na posição certa.
Clique numa peça e depois no quadro, ou arraste. Pode arrastar de volta para a lista de peças para mudar de ideia.
Peças
Código-fonte
Configuração da grade
Uma imagem só, cortada visualmente por CSS — sem gerar um arquivo por peça. cols x rows definem o tamanho da grade e o total de peças.
| 1 |
<?php
|
| 2 |
$imagem = 'diagrama.jpg';
|
| 3 |
$cols = 3;
|
| 4 |
$rows = 2;
|
| 5 |
$total = $cols * $rows;
|
| 6 |
|
| 7 |
$posicoes = range(0, $total - 1);
|
| 8 |
shuffle($posicoes);
|
| 9 |
|
| 10 |
// O número do selo é embaralhado à parte da posição correta — se fosse
|
| 11 |
// sempre "posição + 1", dava pra resolver só combinando números, sem
|
| 12 |
// olhar o conteúdo da imagem.
|
| 13 |
$rotulos = range(1, $total);
|
| 14 |
shuffle($rotulos);
|
| 15 |
?>
|
Grade de destino
Uma célula vazia por peça, mostrando o número da posição como marca d'água até ser preenchida.
| 1 |
<div class="questao" data-tipo="quebra-cabeca-imagem">
|
| 2 |
|
| 3 |
<div class="grade" style="grid-template-columns: repeat(<?= $cols ?>, 1fr);">
|
| 4 |
<?php for ($i = 0; $i < $total; $i++): ?>
|
| 5 |
<div class="slot" data-pos="<?= $i + 1 ?>"></div>
|
| 6 |
<?php endfor; ?>
|
| 7 |
</div>
|
Peças embaralhadas
Cada peça mostra o mesmo arquivo inteiro, com background-size ampliado pra grade toda e background-position deslocado pra sua fatia — o CSS corta, nenhum arquivo por peça.
| 1 |
<div class="pecas" style="grid-template-columns: repeat(<?= $cols ?>, 1fr);">
|
| 2 |
<?php foreach ($posicoes as $pos):
|
| 3 |
$col = $pos % $cols;
|
| 4 |
$row = intdiv($pos, $cols);
|
| 5 |
$bgX = $cols > 1 ? round($col / ($cols - 1) * 100, 2) : 0;
|
| 6 |
$bgY = $rows > 1 ? round($row / ($rows - 1) * 100, 2) : 0;
|
| 7 |
$rotulo = $rotulos[$pos];
|
| 8 |
?>
|
| 9 |
<button type="button" class="peca" draggable="true" data-pos-correta="<?= $pos ?>"
|
| 10 |
style="background-image: url('<?= $imagem ?>'); background-size: <?= $cols * 100 ?>% <?= $rows * 100 ?>%; background-position: <?= $bgX ?>% <?= $bgY ?>%;">
|
| 11 |
<span class="peca-selo"><?= $rotulo ?></span>
|
| 12 |
</button>
|
| 13 |
<?php endforeach; ?>
|
| 14 |
</div>
|
Ações da questão
Verificar, o painel de resultado e o botão de reiniciar — os dois últimos nascem ocultos.
| 1 |
<button type="button" class="questao-verificar">Verificar resposta</button>
|
| 2 |
|
| 3 |
<div class="questao-painel" hidden>
|
| 4 |
<p class="questao-painel-mensagem"></p>
|
| 5 |
</div>
|
| 6 |
|
| 7 |
<button type="button" class="questao-reset" hidden>Tentar novamente</button>
|
| 8 |
|
| 9 |
</div>
|
Grade de destino
Uma célula por peça, com o número da posição como marca d'água até ser preenchida.
| 1 |
.grade {
|
| 2 |
display: grid;
|
| 3 |
gap: 4px;
|
| 4 |
margin-bottom: 16px;
|
| 5 |
}
|
| 6 |
|
| 7 |
.slot {
|
| 8 |
position: relative;
|
| 9 |
aspect-ratio: 1 / 1;
|
| 10 |
border: 2px dashed #cbd5e1;
|
| 11 |
border-radius: 8px;
|
| 12 |
background: #f8fafc;
|
| 13 |
}
|
| 14 |
|
| 15 |
.slot::before {
|
| 16 |
content: attr(data-pos);
|
| 17 |
position: absolute;
|
| 18 |
top: 50%;
|
| 19 |
left: 50%;
|
| 20 |
transform: translate(-50%, -50%);
|
| 21 |
font-size: 1.25rem;
|
| 22 |
font-weight: 700;
|
| 23 |
color: #94a3b8;
|
| 24 |
}
|
| 25 |
|
| 26 |
.slot.preenchido {
|
| 27 |
border-style: solid;
|
| 28 |
border-color: #6366f1;
|
| 29 |
}
|
| 30 |
|
| 31 |
.slot.preenchido::before {
|
| 32 |
content: none;
|
| 33 |
}
|
Bandeja de peças
Mesma grade da área de destino, pra cada peça sair do tamanho certo tanto na bandeja quanto encaixada.
| 1 |
.pecas {
|
| 2 |
display: grid;
|
| 3 |
gap: 4px;
|
| 4 |
margin-bottom: 16px;
|
| 5 |
padding: 12px;
|
| 6 |
border: 1px solid #cbd5e1;
|
| 7 |
border-radius: 12px;
|
| 8 |
background: #f8fafc;
|
| 9 |
}
|
Peça
Botão sem conteúdo de texto — a imagem vem inteira de background-image, cortada por background-size/position calculados no PHP.
| 1 |
.peca {
|
| 2 |
position: relative;
|
| 3 |
display: block;
|
| 4 |
width: 100%;
|
| 5 |
aspect-ratio: 1 / 1;
|
| 6 |
border: 2px solid transparent;
|
| 7 |
border-radius: 8px;
|
| 8 |
background-repeat: no-repeat;
|
| 9 |
background-color: #e2e8f0;
|
| 10 |
cursor: grab;
|
| 11 |
}
|
| 12 |
|
| 13 |
.peca.selecionada {
|
| 14 |
border-color: #6366f1;
|
| 15 |
outline: 2px solid #6366f1;
|
| 16 |
}
|
Selo numérico
Número fixo no canto — embaralhado à parte da posição correta da peça, de propósito, pra não entregar a resposta.
| 1 |
.peca-selo {
|
| 2 |
position: absolute;
|
| 3 |
top: 4px;
|
| 4 |
left: 4px;
|
| 5 |
width: 20px;
|
| 6 |
height: 20px;
|
| 7 |
border-radius: 999px;
|
| 8 |
background: rgba(15, 23, 42, 0.7);
|
| 9 |
color: #fff;
|
| 10 |
font-size: 11px;
|
| 11 |
font-weight: 700;
|
| 12 |
display: flex;
|
| 13 |
align-items: center;
|
| 14 |
justify-content: center;
|
| 15 |
}
|
Ações
Botões de largura total, um de cada vez na tela.
| 1 |
.questao-verificar,
|
| 2 |
.questao-reset {
|
| 3 |
display: block;
|
| 4 |
width: 100%;
|
| 5 |
padding: 12px 16px;
|
| 6 |
border: none;
|
| 7 |
border-radius: 10px;
|
| 8 |
font: inherit;
|
| 9 |
font-weight: 600;
|
| 10 |
cursor: pointer;
|
| 11 |
}
|
| 12 |
|
| 13 |
.questao-verificar {
|
| 14 |
background: #4f46e5;
|
| 15 |
color: #fff;
|
| 16 |
}
|
| 17 |
|
| 18 |
.questao-reset {
|
| 19 |
background: #e5e7eb;
|
| 20 |
color: #1f2937;
|
| 21 |
}
|
Estado oculto
O atributo hidden nativo tem a mesma especificidade da regra de exibição acima — sem isto, "Tentar novamente" apareceria antes de verificar.
| 1 |
.questao-verificar[hidden],
|
| 2 |
.questao-reset[hidden] {
|
| 3 |
display: none;
|
| 4 |
}
|
Painel de resultado
Caixa de destaque com a contagem de acertos, abaixo das peças.
| 1 |
.questao-painel {
|
| 2 |
margin: 16px 0;
|
| 3 |
padding: 16px;
|
| 4 |
border-radius: 12px;
|
| 5 |
background: #1e3a8a;
|
| 6 |
color: #dbeafe;
|
| 7 |
font-weight: 600;
|
| 8 |
}
|
Encontrar os elementos da questão
Localiza a grade de destino, a bandeja de peças e as ações.
| 1 |
document.addEventListener('DOMContentLoaded', function () {
|
| 2 |
document.querySelectorAll('.questao[data-tipo="quebra-cabeca-imagem"]').forEach(function (questao) {
|
| 3 |
var slots = questao.querySelectorAll('.slot');
|
| 4 |
var pecasDiv = questao.querySelector('.pecas');
|
| 5 |
var verificar = questao.querySelector('.questao-verificar');
|
| 6 |
var painel = questao.querySelector('.questao-painel');
|
| 7 |
var reset = questao.querySelector('.questao-reset');
|
| 8 |
var total = slots.length;
|
| 9 |
// A peça selecionada por clique, aguardando o clique no destino.
|
| 10 |
var selecionada = null;
|
| 11 |
|
| 12 |
function pecaDaPosicao(i) {
|
| 13 |
return questao.querySelector('.peca[data-pos-correta="' + i + '"]');
|
| 14 |
}
|
Mover uma peça
Um único ponto de movimentação — troca o container da peça (slot ou bandeja) e devolve o slot antigo ao estado vazio.
| 1 |
function liberar(peca) {
|
| 2 |
var slotAntigo = peca.parentElement;
|
| 3 |
if (slotAntigo && slotAntigo.classList.contains('slot')) {
|
| 4 |
slotAntigo.classList.remove('preenchido');
|
| 5 |
}
|
| 6 |
}
|
| 7 |
|
| 8 |
function mover(peca, destino) {
|
| 9 |
var ehSlot = destino.classList.contains('slot');
|
| 10 |
if (ehSlot) {
|
| 11 |
var ocupante = destino.querySelector('.peca');
|
| 12 |
if (ocupante && ocupante !== peca) pecasDiv.appendChild(ocupante);
|
| 13 |
}
|
| 14 |
liberar(peca);
|
| 15 |
destino.appendChild(peca);
|
| 16 |
peca.classList.remove('selecionada');
|
| 17 |
if (ehSlot) destino.classList.add('preenchido');
|
| 18 |
selecionada = null;
|
| 19 |
}
|
Selecionar e arrastar
Clicar seleciona a peça (o stopPropagation evita que o clique vaze pro slot ou pra bandeja por baixo e mova ela sozinha); arrastar funciona em paralelo, terminando na mesma função de mover.
| 1 |
questao.querySelectorAll('.peca').forEach(function (peca) {
|
| 2 |
peca.addEventListener('click', function (evento) {
|
| 3 |
evento.stopPropagation();
|
| 4 |
if (selecionada === peca) { peca.classList.remove('selecionada'); selecionada = null; return; }
|
| 5 |
if (selecionada) selecionada.classList.remove('selecionada');
|
| 6 |
selecionada = peca;
|
| 7 |
peca.classList.add('selecionada');
|
| 8 |
});
|
| 9 |
peca.addEventListener('dragstart', function (evento) {
|
| 10 |
selecionada = peca;
|
| 11 |
evento.dataTransfer.setData('text/plain', peca.dataset.posCorreta);
|
| 12 |
});
|
| 13 |
});
|
Encaixar numa posição
Clique (com peça selecionada) ou soltar via arraste têm o mesmo efeito.
| 1 |
slots.forEach(function (slot) {
|
| 2 |
slot.addEventListener('click', function () { if (selecionada) mover(selecionada, slot); });
|
| 3 |
slot.addEventListener('dragover', function (evento) { evento.preventDefault(); });
|
| 4 |
slot.addEventListener('drop', function (evento) {
|
| 5 |
evento.preventDefault();
|
| 6 |
var peca = pecaDaPosicao(evento.dataTransfer.getData('text/plain'));
|
| 7 |
if (peca) mover(peca, slot);
|
| 8 |
});
|
| 9 |
});
|
Devolver à bandeja
A bandeja também é um alvo válido — devolver uma peça aqui é como o aluno desfaz uma escolha antes de verificar.
| 1 |
pecasDiv.addEventListener('click', function () { if (selecionada) mover(selecionada, pecasDiv); });
|
| 2 |
pecasDiv.addEventListener('dragover', function (evento) { evento.preventDefault(); });
|
| 3 |
pecasDiv.addEventListener('drop', function (evento) {
|
| 4 |
evento.preventDefault();
|
| 5 |
var peca = pecaDaPosicao(evento.dataTransfer.getData('text/plain'));
|
| 6 |
if (peca) mover(peca, pecasDiv);
|
| 7 |
});
|
Verificar
Uma peça está certa quando sua posição correta bate com o índice do slot em que ela está.
| 1 |
verificar.addEventListener('click', function () {
|
| 2 |
var acertos = 0;
|
| 3 |
slots.forEach(function (slot, i) {
|
| 4 |
var peca = slot.querySelector('.peca');
|
| 5 |
if (peca && parseInt(peca.dataset.posCorreta) === i) acertos++;
|
| 6 |
});
|
| 7 |
|
| 8 |
painel.querySelector('.questao-painel-mensagem').textContent = acertos === total
|
| 9 |
? 'Correto! Imagem montada.'
|
| 10 |
: acertos + ' de ' + total + ' peças no lugar certo.';
|
| 11 |
painel.hidden = false;
|
| 12 |
|
| 13 |
verificar.hidden = true;
|
| 14 |
reset.hidden = false;
|
| 15 |
});
|
Reiniciar
Devolve as peças à bandeja em ordem embaralhada de novo — sem isto, elas voltariam na ordem em que estavam no DOM, que pode ser a própria ordem resolvida.
| 1 |
reset.addEventListener('click', function () {
|
| 2 |
var pecas = Array.prototype.slice.call(questao.querySelectorAll('.peca'));
|
| 3 |
for (var i = pecas.length - 1; i > 0; i--) {
|
| 4 |
var j = Math.floor(Math.random() * (i + 1));
|
| 5 |
var tmp = pecas[i]; pecas[i] = pecas[j]; pecas[j] = tmp;
|
| 6 |
}
|
| 7 |
pecas.forEach(function (p) { p.classList.remove('selecionada'); pecasDiv.appendChild(p); });
|
| 8 |
slots.forEach(function (slot) { slot.classList.remove('preenchido'); });
|
| 9 |
selecionada = null;
|
| 10 |
painel.hidden = true;
|
| 11 |
verificar.hidden = false;
|
| 12 |
reset.hidden = true;
|
| 13 |
});
|