<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Roule Galette - Activité Interactive</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&display=swap');
/* Structure Plein Écran Rigide */
body {
font-family: 'Fredoka', sans-serif;
background-color: #fdf6e3;
background-image: radial-gradient(#e6d6b6 1px, transparent 1px);
background-size: 20px 20px;
height: 100vh;
width: 100vw;
overflow: hidden; /* Zéro scroll global */
display: flex;
flex-direction: column;
}
/* --- Grille et Cartes --- */
.cards-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
width: 100%;
align-content: start;
justify-items: center;
padding-bottom: 2rem; /* Espace pour le scroll */
}
/* Ajustement pour écrans larges */
@media (min-width: 1024px) {
.cards-grid {
grid-template-columns: repeat(3, 1fr); /* 3 colonnes sur grand écran pour gagner de la hauteur */
align-content: center;
}
}
.card-container {
width: 100%;
max-width: 150px;
aspect-ratio: 4/3;
position: relative;
}
/* --- Sprite CSS (Images) --- */
.puzzle-piece {
width: 100%;
height: 100%;
background-image: url('Gemini_Generated_Image_bmtotubmtotubmto.jpg');
background-size: 300% 200%;
background-repeat: no-repeat;
border-radius: 0.5rem;
cursor: grab;
border: 2px solid #d97706;
box-shadow: 0 3px 0 #b45309; /* Effet 3D léger */
transition: transform 0.1s;
touch-action: none; /* Indispensable pour le drag tactile fluide */
}
.puzzle-piece:active { cursor: grabbing; transform: scale(0.95); box-shadow: 0 1px 0 #b45309; }
.puzzle-piece.selected {
border-color: #3b82f6;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.4);
z-index: 50;
}
/* États de jeu */
.puzzle-piece.locked {
cursor: default;
border-color: #22c55e;
box-shadow: none;
}
/* Positions Sprite */
.pos-1 { background-position: 0% 0%; }
.pos-2 { background-position: 50% 0%; }
.pos-3 { background-position: 100% 0%; }
.pos-4 { background-position: 0% 100%; }
.pos-5 { background-position: 50% 100%; }
.pos-6 { background-position: 100% 100%; }
/* Zones de dépôt */
.drop-zone {
width: 100%;
height: 100%;
border: 2px dashed #cbd5e1;
border-radius: 0.5rem;
background-color: rgba(255, 255, 255, 0.5);
position: relative;
display: flex; justify-content: center; align-items: center;
}
.drop-zone.drag-over { background-color: #e0f2fe; border-color: #3b82f6; }
.drop-zone.correct { background-color: #dcfce7; border-color: #22c55e; border-style: solid; }
.drop-zone.incorrect { background-color: #fee2e2; border-color: #ef4444; border-style: solid; }
.number-badge {
position: absolute; top: -8px; left: -8px;
width: 26px; height: 26px;
background-color: #fff; border: 2px solid #d97706; color: #d97706;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-weight: bold; font-size: 0.9rem;
z-index: 10; pointer-events: none;
}
/* Animation Audio */
@keyframes pulse-audio {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.playing-audio { animation: pulse-audio 1.5s infinite; color: #ea580c; border-color: #ea580c; background-color: #ffedd5; }
/* Fallback */
#image-fallback {
position: absolute; inset: 0; background: rgba(255,255,255,0.95); z-index: 100;
display: none; flex-direction: column; align-items: center; justify-content: center;
}
</style>
</head>
<body class="p-2 select-none">
<!-- En-tête Compact -->
<header class="flex-none bg-white p-2 rounded-xl shadow-sm border border-orange-100 flex items-center justify-between h-14 mb-2">
<h1 class="text-lg font-bold text-orange-800 leading-tight">Roule Galette <span class="hidden sm:inline font-normal text-sm text-gray-500">| Chronologie</span></h1>
<div class="flex items-center gap-2">
<!-- Contrôles Audio -->
<button id="btn-audio" onclick="toggleAudio()" class="flex items-center gap-1 px-3 py-1.5 rounded-lg border border-orange-200 text-orange-700 bg-orange-50 hover:bg-orange-100 transition font-semibold text-sm">
<span id="icon-audio">🔊</span> <span class="hidden sm:inline">Écouter</span>
</button>
<div class="h-6 w-px bg-gray-200 mx-1"></div>
<button onclick="toggleStory()" class="p-2 rounded-lg text-gray-600 hover:bg-gray-100" title="Lire le texte">
📖
</button>
<button onclick="resetGame()" class="p-2 rounded-lg text-gray-600 hover:bg-gray-100" title="Recommencer">
↺
</button>
</div>
</header>
<!-- Zone de Jeu Principale -->
<main class="flex-1 flex flex-col lg:flex-row gap-2 overflow-hidden min-h-0">
<!-- BANQUE D'IMAGES (Source) -->
<div class="flex-1 bg-orange-50/50 rounded-xl border border-orange-100 flex flex-col min-h-0 relative">
<div class="absolute top-0 left-0 right-0 bg-orange-100/80 backdrop-blur-sm p-1 text-center text-xs font-bold text-orange-800 rounded-t-xl z-10">
IMAGES À PLACER
</div>
<div id="source-container" class="cards-grid overflow-y-auto pt-8 p-2">
<!-- Les pièces apparaissent ici -->
</div>
</div>
<!-- FLÈCHE (Visuel pour mobile) -->
<div class="lg:hidden flex justify-center -my-3 z-10">
<div class="bg-white rounded-full p-1 border border-orange-200 shadow text-orange-400">⬇️</div>
</div>
<!-- CHRONOLOGIE (Cible) -->
<div class="flex-1 bg-white rounded-xl border border-orange-100 flex flex-col shadow-sm min-h-0 relative">
<div class="flex-none flex justify-between items-center p-2 border-b border-orange-50 bg-white rounded-t-xl z-10">
<span class="text-xs font-bold text-orange-800 uppercase tracking-wide">Histoire</span>
<button onclick="checkOrder()" class="px-4 py-1 bg-green-500 text-white text-xs font-bold rounded-full shadow hover:bg-green-600 transition transform active:scale-95">
VÉRIFIER
</button>
</div>
<div class="cards-grid overflow-y-auto p-2 pt-2">
<div class="card-container drop-zone" data-slot="1"><div class="number-badge">1</div></div>
<div class="card-container drop-zone" data-slot="2"><div class="number-badge">2</div></div>
<div class="card-container drop-zone" data-slot="3"><div class="number-badge">3</div></div>
<div class="card-container drop-zone" data-slot="4"><div class="number-badge">4</div></div>
<div class="card-container drop-zone" data-slot="5"><div class="number-badge">5</div></div>
<div class="card-container drop-zone" data-slot="6"><div class="number-badge">6</div></div>
</div>
</div>
</main>
<!-- Modal Histoire (Texte) -->
<div id="story-modal" class="fixed inset-0 bg-black/50 hidden flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-white p-6 rounded-2xl shadow-2xl max-w-md w-full max-h-[80vh] flex flex-col">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-orange-800">L'histoire de Roule Galette</h3>
<button onclick="toggleStory()" class="text-gray-400 hover:text-gray-800 text-2xl">×</button>
</div>
<div class="overflow-y-auto text-sm leading-relaxed space-y-3 pr-2 text-gray-700 font-medium">
<p class="p-2 bg-orange-50 rounded"><strong>1.</strong> La vieille fait une galette avec les grains de blé du grenier et la met à cuire.</p>
<p class="p-2 bg-orange-50 rounded"><strong>2.</strong> Elle met la galette à refroidir sur la fenêtre. La galette s'ennuie et se sauve !</p>
<p class="p-2 bg-orange-50 rounded"><strong>3.</strong> Elle rencontre un lapin. Elle chante sa chanson et s'enfuit vite.</p>
<p class="p-2 bg-orange-50 rounded"><strong>4.</strong> Elle rencontre le loup gris puis le gros ours. Elle court très vite pour leur échapper.</p>
<p class="p-2 bg-orange-50 rounded"><strong>5.</strong> Elle rencontre le renard. Le renard rusé fait semblant d'être sourd.</p>
<p class="p-2 bg-orange-50 rounded"><strong>6.</strong> La galette saute sur le nez du renard pour chanter... et HAM ! Le renard la mange.</p>
</div>
</div>
</div>
<!-- Modal Succès -->
<div id="success-modal" class="fixed inset-0 bg-black/60 hidden flex items-center justify-center z-50">
<div class="bg-white p-8 rounded-3xl shadow-2xl text-center max-w-sm mx-4 transform transition-all scale-100">
<div class="text-6xl mb-4 animate-bounce">🦊</div>
<h3 class="text-2xl font-bold text-green-600 mb-2">Bravo !</h3>
<p class="text-gray-600 mb-6">Tu connais bien l'histoire !</p>
<button onclick="closeModal()" class="px-8 py-3 bg-orange-500 text-white rounded-full font-bold hover:bg-orange-600 shadow-lg active:transform active:scale-95 transition">
Rejouer
</button>
</div>
</div>
<!-- Fallback Image -->
<div id="image-fallback">
<p class="font-bold text-red-500 mb-2">Image introuvable</p>
<input type="file" id="file-uploader" accept="image/*" class="text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-orange-50 file:text-orange-700 hover:file:bg-orange-100"/>
</div>
<script>
/* --- CONFIGURATION --- */
const totalPieces = 6;
const correctOrder = [1, 2, 3, 4, 5, 6];
let selectedPiece = null;
// Texte complet de l'histoire pour la synthèse vocale
const storyText = "Voici l'histoire de Roule Galette. " +
"Dans une petite maison, la vieille fait une galette avec les grains de blé du grenier. " +
"Elle la met à cuire, puis la pose sur la fenêtre pour refroidir. Mais la galette s'ennuie, elle se laisse glisser et s'enfuit ! " +
"Sur le chemin, elle rencontre un lapin. Galette, galette, je vais te manger ! dit le lapin. Non ! dit la galette. Et elle se sauve. " +
"Plus loin, elle rencontre le loup gris, puis un gros ours. À chaque fois, elle chante sa petite chanson et court très vite. " +
"Enfin, elle rencontre le renard. Le renard est malin. Il dit : Bonjour galette ! Comme tu es ronde ! " +
"Le renard fait semblant d'être sourd. Il dit : Qu'est-ce que tu chantes ? Approche-toi ! " +
"Pour mieux se faire entendre, la galette saute sur le nez du renard. " +
"Et HAM ! Le renard la mange.";
let synth = window.speechSynthesis;
let utterance = null;
let isPlaying = false;
/* --- INITIALISATION --- */
document.addEventListener('DOMContentLoaded', () => {
initGame();
checkImageLoad();
// Préchargement de la voix (fix pour certains navigateurs)
if (synth.onvoiceschanged !== undefined) {
synth.onvoiceschanged = () => {};
}
});
/* --- AUDIO LOGIC --- */
function toggleAudio() {
const btn = document.getElementById('btn-audio');
const icon = document.getElementById('icon-audio');
if (isPlaying) {
synth.cancel();
isPlaying = false;
btn.classList.remove('playing-audio');
icon.textContent = '🔊';
btn.querySelector('span:not(#icon-audio)').textContent = "Écouter";
} else {
// Créer une nouvelle instance à chaque lecture pour éviter les bugs
utterance = new SpeechSynthesisUtterance(storyText);
utterance.lang = 'fr-FR';
utterance.rate = 0.9; // Légèrement plus lent pour les enfants
utterance.pitch = 1.1; // Un peu plus joyeux
utterance.onend = () => {
isPlaying = false;
btn.classList.remove('playing-audio');
icon.textContent = '🔊';
btn.querySelector('span:not(#icon-audio)').textContent = "Écouter";
};
synth.speak(utterance);
isPlaying = true;
btn.classList.add('playing-audio');
icon.textContent = '⏹️';
btn.querySelector('span:not(#icon-audio)').textContent = "Arrêter";
}
}
/* --- GAME LOGIC --- */
function initGame() {
const sourceContainer = document.getElementById('source-container');
const dropZones = document.querySelectorAll('.drop-zone');
// Reset UI
sourceContainer.innerHTML = '';
dropZones.forEach(zone => {
zone.innerHTML = `<div class="number-badge">${zone.dataset.slot}</div>`;
zone.classList.remove('correct', 'incorrect');
});
// Create Pieces
let pieces = [];
for (let i = 1; i <= totalPieces; i++) {
// Wrapper pour maintenir le ratio
const wrapper = document.createElement('div');
wrapper.className = 'card-container';
const piece = document.createElement('div');
piece.className = `puzzle-piece pos-${i}`;
piece.dataset.id = i;
piece.draggable = true;
// Events
piece.addEventListener('dragstart', handleDragStart);
piece.addEventListener('click', handlePieceClick); // Pour tactile/souris
wrapper.appendChild(piece);
pieces.push(wrapper);
}
// Shuffle & Append
pieces.sort(() => Math.random() - 0.5);
pieces.forEach(p => sourceContainer.appendChild(p));
// Drop Zones Events
dropZones.forEach(zone => {
zone.addEventListener('dragover', handleDragOver);
zone.addEventListener('dragleave', handleDragLeave);
zone.addEventListener('drop', handleDrop);
zone.addEventListener('click', handleZoneClick);
});
}
/* --- INTERACTION (Drag & Click) --- */
function handleDragStart(e) {
e.dataTransfer.setData('text/plain', e.target.dataset.id);
selectedPiece = e.target;
}
function handleDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('drag-over'); }
function handleDragLeave(e) { e.currentTarget.classList.remove('drag-over'); }
function handleDrop(e) {
e.preventDefault();
const zone = e.currentTarget;
zone.classList.remove('drag-over');
const id = e.dataTransfer.getData('text/plain');
const piece = document.querySelector(`.puzzle-piece[data-id="${id}"]`);
placePiece(piece, zone);
}
function handlePieceClick(e) {
e.stopPropagation();
const piece = e.target;
if (piece.classList.contains('locked')) return;
if (selectedPiece === piece) {
deselect();
} else {
deselect();
selectedPiece = piece;
piece.classList.add('selected');
}
}
function handleZoneClick(e) {
const zone = e.currentTarget;
if (selectedPiece && !zone.querySelector('.puzzle-piece')) {
placePiece(selectedPiece, zone);
} else if (zone.querySelector('.puzzle-piece')) {
// Si on clique sur une zone déjà remplie, on sélectionne la pièce dedans
const piece = zone.querySelector('.puzzle-piece');
if(!piece.classList.contains('locked')) {
deselect();
selectedPiece = piece;
piece.classList.add('selected');
}
}
}
function placePiece(piece, zone) {
if (!piece || !zone) return;
// Si la zone est pleine, on ne fait rien (ou échange simple à implémenter si voulu)
if (zone.querySelector('.puzzle-piece')) return;
// Retirer l'ancien wrapper si on vient de la banque
const parent = piece.parentElement;
// Ajouter à la nouvelle zone
zone.appendChild(piece);
// Nettoyage de la banque d'image (retirer wrapper vide)
if (parent.classList.contains('card-container') && parent.parentElement.id === 'source-container') {
parent.remove();
}
deselect();
}
/* --- RETOUR SOURCE --- */
document.getElementById('source-container').addEventListener('click', (e) => {
if ((e.target.id === 'source-container' || e.target.classList.contains('cards-grid')) && selectedPiece) {
// Créer un nouveau wrapper pour la banque
const wrapper = document.createElement('div');
wrapper.className = 'card-container';
wrapper.appendChild(selectedPiece);
document.getElementById('source-container').appendChild(wrapper);
deselect();
}
});
function deselect() {
if (selectedPiece) selectedPiece.classList.remove('selected');
selectedPiece = null;
}
/* --- VALIDATION --- */
function checkOrder() {
const zones = document.querySelectorAll('.drop-zone');
let correctCount = 0;
let filledCount = 0;
zones.forEach(zone => {
const piece = zone.querySelector('.puzzle-piece');
zone.classList.remove('correct', 'incorrect');
if (piece) {
filledCount++;
const pieceId = parseInt(piece.dataset.id);
const slotId = parseInt(zone.dataset.slot);
if (pieceId === slotId) {
zone.classList.add('correct');
piece.classList.add('locked'); // Bloquer les pièces justes
correctCount++;
} else {
zone.classList.add('incorrect');
piece.classList.remove('locked');
}
}
});
if (correctCount === totalPieces) {
document.getElementById('success-modal').classList.remove('hidden');
// Petit son de victoire (optionnel, via synth)
const winMsg = new SpeechSynthesisUtterance("Bravo ! Tu as réussi !");
winMsg.lang = 'fr-FR';
synth.speak(winMsg);
}
}
/* --- UTILITAIRES --- */
function resetGame() {
synth.cancel();
isPlaying = false;
document.getElementById('btn-audio').classList.remove('playing-audio');
document.getElementById('icon-audio').textContent = '🔊';
initGame();
}
function toggleStory() {
document.getElementById('story-modal').classList.toggle('hidden');
}
function closeModal() {
document.getElementById('success-modal').classList.add('hidden');
resetGame();
}
function checkImageLoad() {
const img = new Image();
img.src = 'Gemini_Generated_Image_bmtotubmtotubmto.jpg';
img.onerror = () => { document.getElementById('image-fallback').style.display = 'flex'; };
}
document.getElementById('file-uploader').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
const styleSheet = document.createElement("style");
styleSheet.innerText = `.puzzle-piece { background-image: url('${event.target.result}') !important; }`;
document.head.appendChild(styleSheet);
document.getElementById('image-fallback').style.display = 'none';
};
reader.readAsDataURL(file);
}
});
</script>
</body>
</html>