<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>PCdlabs Archives - Présent Composé design</title>
	<atom:link href="https://presentcomposedesign.fr/category/pcdlabs/feed/" rel="self" type="application/rss+xml" />
	<link>https://presentcomposedesign.fr/category/pcdlabs/</link>
	<description>Direction Artistique, concepts &#38; innovations, Design 2D/3D/ia/AR/VR</description>
	<lastBuildDate>Tue, 12 May 2026 12:39:25 +0000</lastBuildDate>
	<language>fr-FR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://presentcomposedesign.fr/wp-content/uploads/2025/07/cropped-logo_PCd_2025-512-32x32.png</url>
	<title>PCdlabs Archives - Présent Composé design</title>
	<link>https://presentcomposedesign.fr/category/pcdlabs/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>My Puzzle</title>
		<link>https://presentcomposedesign.fr/my-puzzle/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Tue, 12 May 2026 12:38:27 +0000</pubDate>
				<category><![CDATA[PCdlabs]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=36614</guid>

					<description><![CDATA[<p>3D Puzzle Viewer 🧩 Puzzle 3D Importer une image Mélanger le puzzle Sauvegarder la progression Générer un lien de partage Pièces correctement placées : 0 Glissez les pièces avec la souris. Molette : zoom Clic droit : rotation caméra 🧩 Puzzle 3D Importer une image Mélanger le puzzle Générer un lien de partage Pièces correctement placées : 0 Glissez les pièces avec la souris. Molette : zoom Clic droit : rotation caméra Puzzle 3D 🧩 Puzzle 3D Importer une image Mélanger le puzzle Pièces correctement placées : 0 🧩 Puzzle 3D Importer une image Mélanger le puzzle Générer un lien de partage Pièces correctement placées : 0 Glissez les pièces avec la souris. Molette : zoom Clic droit : rotation caméra let scene, camera, renderer, controls; let raycaster = new THREE.Raycaster(); let mouse = new THREE.Vector2(); let puzzlePieces = []; let selectedPiece = null; let offset = new THREE.Vector3(); let plane = new THREE.Plane(); let intersection = new THREE.Vector3(); const puzzleSize = 4; const pieceSize = 1; let originalPositions = []; init(); animate(); restorePuzzle(); function init() { scene = new THREE.Scene(); scene.background = new THREE.Color(0x111111); camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.set(0, 0, 8); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; const ambient = new THREE.AmbientLight(0xffffff, 1.2); scene.add(ambient); const directional = new THREE.DirectionalLight(0xffffff, 1.5); directional.position.set(5, 10, 5); scene.add(directional); window.addEventListener('resize', onResize); renderer.domElement.addEventListener('pointerdown', onPointerDown); renderer.domElement.addEventListener('pointermove', onPointerMove); renderer.domElement.addEventListener('pointerup', onPointerUp); document .getElementById('imageUpload') .addEventListener('change', handleImageUpload); document .getElementById('shuffleBtn') .addEventListener('click', explodePuzzle); document .getElementById('shareBtn') .addEventListener('click', generateShareLink); } function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } function clearPuzzle() { puzzlePieces.forEach(piece => { scene.remove(piece); }); puzzlePieces = []; originalPositions = []; } function handleImageUpload(e) { console.log('Import image déclenché'); const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(event) { createPuzzle(event.target.result); }; reader.readAsDataURL(file); // Sauvegarde locale de l'image source localStorage.setItem('puzzle-image', URL.createObjectURL(file)); } function createPuzzle(imageSrc) { clearPuzzle(); const loader = new THREE.TextureLoader(); loader.load(imageSrc, texture => { texture.minFilter = THREE.LinearFilter; for (let y = 0; y < puzzleSize; y++) { for (let x = 0; x < puzzleSize; x++) { const geometry = new THREE.BoxGeometry( pieceSize, pieceSize, 0.2 ); const materials = []; for (let i = 0; i < 6; i++) { materials.push( new THREE.MeshStandardMaterial({ color: i === 4 ? 0xffffff : 0x222222, map: i === 4 ? texture.clone() : null }) ); } const piece = new THREE.Mesh(geometry, materials); const tx = x / puzzleSize; const ty = y / puzzleSize; piece.material[4].map.repeat.set( 1 / puzzleSize, 1 / puzzleSize ); piece.material[4].map.offset.set( tx, 1 - (1 / puzzleSize) - ty ); piece.material[4].map.needsUpdate = true; const posX = (x - puzzleSize / 2) * pieceSize + pieceSize / 2; const posY = -(y - puzzleSize / 2) * pieceSize - pieceSize / 2; piece.position.set(posX, posY, 0); piece.userData.correctPosition = { x: posX, y: posY, z: 0 }; originalPositions.push({ x: posX, y: posY, z: 0 }); scene.add(piece); puzzlePieces.push(piece); } } explodePuzzle(); savePuzzle(); }); } function explodePuzzle() { puzzlePieces.forEach(piece => { piece.position.x += (Math.random() - 0.5) * 10; piece.position.y += (Math.random() - 0.5) * 10; piece.position.z += (Math.random() - 0.5) * 4; piece.rotation.x = Math.random() * Math.PI; piece.rotation.y = Math.random() * Math.PI; }); updateProgress(); } function onPointerDown(event) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(puzzlePieces); if (intersects.length > 0) { selectedPiece = intersects[0].object; plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection(plane.normal), selectedPiece.position ); if (raycaster.ray.intersectPlane(plane, intersection)) { offset.copy(intersection).sub(selectedPiece.position); } controls.enabled = false; } } function onPointerMove(event) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); if (selectedPiece) { if (raycaster.ray.intersectPlane(plane, intersection)) { selectedPiece.position.copy(intersection.sub(offset)); } } } function onPointerUp() { if (selectedPiece) { const correct = selectedPiece.userData.correctPosition; const dist = selectedPiece.position.distanceTo( new THREE.Vector3(correct.x, correct.y, correct.z) ); if (dist < 0.5) { selectedPiece.position.set(correct.x, correct.y, correct.z); selectedPiece.rotation.set(0, 0, 0); } updateProgress(); savePuzzle(); } selectedPiece = null; controls.enabled = true; } function updateProgress() { let good = 0; puzzlePieces.forEach(piece => { const c = piece.userData.correctPosition; const dist = piece.position.distanceTo( new THREE.Vector3(c.x, c.y, c.z) ); if (dist < 0.01) { good++; } }); document.getElementById('progress').innerHTML = `Pièces correctement placées : ${good} / ${puzzlePieces.length}`; } function savePuzzle() { if (!puzzlePieces.length) return; const data = puzzlePieces.map(piece => ({ position: { x: piece.position.x, y: piece.position.y, z: piece.position.z }, rotation: { x: piece.rotation.x, y: piece.rotation.y, z: piece.rotation.z }, correctPosition: piece.userData.correctPosition })); localStorage.setItem('puzzle-progress', JSON.stringify(data)); } function restorePuzzle() { const saved = localStorage.getItem('puzzle-progress'); if (!saved) return; console.log('Progression restaurée automatiquement'); } function generateShareLink() { if (!puzzlePieces.length) return; const shareData = puzzlePieces.map(piece => ({ p: piece.position, r: piece.rotation, c: piece.userData.correctPosition })); const encoded = btoa(JSON.stringify(shareData)); const url = `${window.location.origin}${window.location.pathname}?puzzle=${encoded}`; const input = document.getElementById('shareLink'); input.value = url; input.select(); navigator.clipboard.writeText(url);</p>
<p>Cet article <a href="https://presentcomposedesign.fr/my-puzzle/">My Puzzle</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="36614" class="elementor elementor-36614">
				<div class="elementor-element elementor-element-3bc7f57 e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no e-con e-parent" data-id="3bc7f57" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-a576948 elementor-widget elementor-widget-html" data-id="a576948" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>3D Puzzle Viewer</title>

  <script src="https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/three@0.161.0/examples/js/controls/OrbitControls.js"></script>

  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      font-family: Arial, sans-serif;
    }

    body {
      overflow: hidden;
      background: #0f0f0f;
      color: white;
    }

    canvas {
      display: block;
    }

    #hud {
      position: absolute;
      top: 20px;
      left: 20px;
      z-index: 10;
      background: rgba(0,0,0,0.7);
      padding: 16px;
      border-radius: 14px;
      width: 300px;
      backdrop-filter: blur(10px);
      border: 1px solid rgba(255,255,255,0.1);
    }

    #hud h1 {
      font-size: 18px;
      margin-bottom: 14px;
    }

    .btn {
      width: 100%;
      margin-top: 10px;
      padding: 12px;
      border: none;
      border-radius: 10px;
      cursor: pointer;
      font-size: 14px;
      font-weight: bold;
      transition: 0.2s ease;
    }

    .btn:hover {
      transform: scale(1.02);
    }

    .primary {
      background: #ffffff;
      color: black;
    }

    .secondary {
      background: #1e1e1e;
      color: white;
      border: 1px solid rgba(255,255,255,0.1);
    }

    input[type="file"] {
      display: none;
    }

    .file-label {
      display: block;
      text-align: center;
      padding: 12px;
      background: #1b1b1b;
      border-radius: 10px;
      cursor: pointer;
      border: 1px dashed rgba(255,255,255,0.2);
    }

    #shareLink {
      margin-top: 12px;
      width: 100%;
      background: #121212;
      border: 1px solid rgba(255,255,255,0.1);
      padding: 10px;
      border-radius: 10px;
      color: white;
    }

    #status {
      margin-top: 12px;
      font-size: 12px;
      opacity: 0.8;
      line-height: 1.4;
    }

    #progress {
      margin-top: 10px;
      font-size: 13px;
    }

    @media (max-width: 768px) {
      #hud {
        width: calc(100% - 40px);
      }
    }
  </style>
</head>
<body>

<div id="hud">
  <h1>🧩 Puzzle 3D</h1>

  <label class="file-label" for="imageUpload">
    Importer une image
  </label>

  <input type="file" id="imageUpload" accept="image/*" />

  <button class="btn primary" id="shuffleBtn">
    Mélanger le puzzle
  </button>

  <button class="btn secondary" id="saveBtn">
    Sauvegarder la progression
  </button>

  <button class="btn secondary" id="shareBtn">
    Générer un lien de partage
  </button>

  <input type="text" id="shareLink" readonly placeholder="Lien de partage..." />

  <div id="progress">
    Pièces correctement placées : 0
  </div>

  <div id="status">
    Glissez les pièces avec la souris.<br>
    Molette : zoom<br>
    Clic droit : rotation caméra
  </div>
</div>

<script>
  let scene, camera, renderer, controls;
  let raycaster = new THREE.Raycaster();
  let mouse = new THREE.Vector2();

  let puzzlePieces = [];
  let selectedPiece = null;
  let offset = new THREE.Vector3();
  let plane = new THREE.Plane();
  let intersection = new THREE.Vector3();

  const puzzleSize = 4;
  const pieceSize = 1;

  let originalPositions = [];

  init();
  animate();
  restorePuzzle();

  function init() {
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x111111);

    camera = new THREE.PerspectiveCamera(
      60,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );

    camera.position.set(0, 0, 8);

    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const ambient = new THREE.AmbientLight(0xffffff, 1.2);
    scene.add(ambient);

    const directional = new THREE.DirectionalLight(0xffffff, 1.5);
    directional.position.set(5, 10, 5);
    scene.add(directional);

    window.addEventListener('resize', onResize);

    renderer.domElement.addEventListener('pointerdown', onPointerDown);
    renderer.domElement.addEventListener('pointermove', onPointerMove);
    renderer.domElement.addEventListener('pointerup', onPointerUp);

    document
      .getElementById('imageUpload')
      .addEventListener('change', handleImageUpload);

    document
      .getElementById('shuffleBtn')
      .addEventListener('click', explodePuzzle);

    document
      .getElementById('saveBtn')
      .addEventListener('click', savePuzzle);

    document
      .getElementById('shareBtn')
      .addEventListener('click', generateShareLink);
  }

  function onResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  }

  function clearPuzzle() {
    puzzlePieces.forEach(piece => {
      scene.remove(piece);
    });

    puzzlePieces = [];
    originalPositions = [];
  }

  function handleImageUpload(e) {
    const file = e.target.files[0];

    if (!file) return;

    const reader = new FileReader();

    reader.onload = function(event) {
      createPuzzle(event.target.result);
    };

    reader.readAsDataURL(file);
  }

  function createPuzzle(imageSrc) {
    clearPuzzle();

    const loader = new THREE.TextureLoader();

    loader.load(imageSrc, texture => {
      texture.minFilter = THREE.LinearFilter;

      for (let y = 0; y < puzzleSize; y++) {
        for (let x = 0; x < puzzleSize; x++) {

          const geometry = new THREE.BoxGeometry(
            pieceSize,
            pieceSize,
            0.2
          );

          const materials = [];

          for (let i = 0; i < 6; i++) {
            materials.push(
              new THREE.MeshStandardMaterial({
                color: i === 4 ? 0xffffff : 0x222222,
                map: i === 4 ? texture.clone() : null
              })
            );
          }

          const piece = new THREE.Mesh(geometry, materials);

          const tx = x / puzzleSize;
          const ty = y / puzzleSize;

          piece.material[4].map.repeat.set(
            1 / puzzleSize,
            1 / puzzleSize
          );

          piece.material[4].map.offset.set(
            tx,
            1 - (1 / puzzleSize) - ty
          );

          piece.material[4].map.needsUpdate = true;

          const posX = (x - puzzleSize / 2) * pieceSize + pieceSize / 2;
          const posY = -(y - puzzleSize / 2) * pieceSize - pieceSize / 2;

          piece.position.set(posX, posY, 0);

          piece.userData.correctPosition = {
            x: posX,
            y: posY,
            z: 0
          };

          originalPositions.push({
            x: posX,
            y: posY,
            z: 0
          });

          scene.add(piece);
          puzzlePieces.push(piece);
        }
      }

      explodePuzzle();
      savePuzzle();
    });
  }

  function explodePuzzle() {
    puzzlePieces.forEach(piece => {
      piece.position.x += (Math.random() - 0.5) * 10;
      piece.position.y += (Math.random() - 0.5) * 10;
      piece.position.z += (Math.random() - 0.5) * 4;

      piece.rotation.x = Math.random() * Math.PI;
      piece.rotation.y = Math.random() * Math.PI;
    });

    updateProgress();
  }

  function onPointerDown(event) {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(puzzlePieces);

    if (intersects.length > 0) {
      selectedPiece = intersects[0].object;

      plane.setFromNormalAndCoplanarPoint(
        camera.getWorldDirection(plane.normal),
        selectedPiece.position
      );

      if (raycaster.ray.intersectPlane(plane, intersection)) {
        offset.copy(intersection).sub(selectedPiece.position);
      }

      controls.enabled = false;
    }
  }

  function onPointerMove(event) {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    if (selectedPiece) {
      if (raycaster.ray.intersectPlane(plane, intersection)) {
        selectedPiece.position.copy(intersection.sub(offset));
      }
    }
  }

  function onPointerUp() {
    if (selectedPiece) {
      const correct = selectedPiece.userData.correctPosition;

      const dist = selectedPiece.position.distanceTo(
        new THREE.Vector3(correct.x, correct.y, correct.z)
      );

      if (dist < 0.5) {
        selectedPiece.position.set(correct.x, correct.y, correct.z);
        selectedPiece.rotation.set(0, 0, 0);
      }

      updateProgress();
      savePuzzle();
    }

    selectedPiece = null;
    controls.enabled = true;
  }

  function updateProgress() {
    let good = 0;

    puzzlePieces.forEach(piece => {
      const c = piece.userData.correctPosition;

      const dist = piece.position.distanceTo(
        new THREE.Vector3(c.x, c.y, c.z)
      );

      if (dist < 0.01) {
        good++;
      }
    });

    document.getElementById('progress').innerHTML =
      `Pièces correctement placées : ${good} / ${puzzlePieces.length}`;
  }

  function savePuzzle() {
    if (!puzzlePieces.length) return;

    const data = puzzlePieces.map(piece => ({
      position: {
        x: piece.position.x,
        y: piece.position.y,
        z: piece.position.z
      },
      rotation: {
        x: piece.rotation.x,
        y: piece.rotation.y,
        z: piece.rotation.z
      },
      correctPosition: piece.userData.correctPosition
    }));

    localStorage.setItem('puzzle-progress', JSON.stringify(data));
  }

  function restorePuzzle() {
    const saved = localStorage.getItem('puzzle-progress');

    if (!saved) return;

    console.log('Progression trouvée.');
  }

  function generateShareLink() {
    if (!puzzlePieces.length) return;

    const shareData = puzzlePieces.map(piece => ({
      p: piece.position,
      r: piece.rotation,
      c: piece.userData.correctPosition
<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>3D Puzzle Viewer</title>

  <script src="https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/three@0.161.0/examples/js/controls/OrbitControls.js"></script>

  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      font-family: Arial, sans-serif;
    }

    body {
      overflow: hidden;
      background: #0f0f0f;
      color: white;
    }

    canvas {
      display: block;
    }

    #hud {
      position: absolute;
      top: 20px;
      left: 20px;
      z-index: 10;
      background: rgba(0,0,0,0.7);
      padding: 16px;
      border-radius: 14px;
      width: 300px;
      backdrop-filter: blur(10px);
      border: 1px solid rgba(255,255,255,0.1);
    }

    #hud h1 {
      font-size: 18px;
      margin-bottom: 14px;
    }

    .btn {
      width: 100%;
      margin-top: 10px;
      padding: 12px;
      border: none;
      border-radius: 10px;
      cursor: pointer;
      font-size: 14px;
      font-weight: bold;
      transition: 0.2s ease;
    }

    .btn:hover {
      transform: scale(1.02);
    }

    .primary {
      background: #ffffff;
      color: black;
    }

    .secondary {
      background: #1e1e1e;
      color: white;
      border: 1px solid rgba(255,255,255,0.1);
    }

    input[type="file"] {
      display: none;
    }

    .file-label {
      display: block;
      text-align: center;
      padding: 12px;
      background: #1b1b1b;
      border-radius: 10px;
      cursor: pointer;
      border: 1px dashed rgba(255,255,255,0.2);
    }

    #shareLink {
      margin-top: 12px;
      width: 100%;
      background: #121212;
      border: 1px solid rgba(255,255,255,0.1);
      padding: 10px;
      border-radius: 10px;
      color: white;
    }

    #status {
      margin-top: 12px;
      font-size: 12px;
      opacity: 0.8;
      line-height: 1.4;
    }

    #progress {
      margin-top: 10px;
      font-size: 13px;
    }

    @media (max-width: 768px) {
      #hud {
        width: calc(100% - 40px);
      }
    }
  </style>
</head>
<body>

<div id="hud">
  <h1>🧩 Puzzle 3D</h1>

  <label class="file-label" for="imageUpload">
    Importer une image
  </label>

  <input type="file" id="imageUpload" accept="image/*" capture="environment" />

  <button class="btn primary" id="shuffleBtn">
    Mélanger le puzzle
  </button>

  

  <button class="btn secondary" id="shareBtn">
    Générer un lien de partage
  </button>

  <input type="text" id="shareLink" readonly placeholder="Lien de partage..." />

  <div id="progress">
    Pièces correctement placées : 0
  </div>

  <div id="status">
    Glissez les pièces avec la souris.<br>
    Molette : zoom<br>
    Clic droit : rotation caméra
  </div>
</div>

<script>
  let scene, camera, renderer, controls;
  let raycaster = new THREE.Raycaster();
  let mouse = new THREE.Vector2();

  let puzzlePieces = [];
  let selectedPiece = null;
  let offset = new THREE.Vector3();
  let plane = new THREE.Plane();
  let intersection = new THREE.Vector3();

  const puzzleSize = 4;
  const pieceSize = 1;

  let originalPositions = [];

  init();
  animate();
  restorePuzzle();

  function init() {
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x111111);

    camera = new THREE.PerspectiveCamera(
      60,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );

    camera.position.set(0, 0, 8);

    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const ambient = new THREE.AmbientLight(0xffffff, 1.2);
    scene.add(ambient);

    const directional = new THREE.DirectionalLight(0xffffff, 1.5);
    directional.position.set(5, 10, 5);
    scene.add(directional);

    window.addEventListener('resize', onResize);

    renderer.domElement.addEventListener('pointerdown', onPointerDown);
    renderer.domElement.addEventListener('pointermove', onPointerMove);
    renderer.domElement.addEventListener('pointerup', onPointerUp);

    document
      .getElementById('imageUpload')
      .addEventListener('change', handleImageUpload);

    document
      .getElementById('shuffleBtn')
      .addEventListener('click', explodePuzzle);

    

    document
      .getElementById('shareBtn')
      .addEventListener('click', generateShareLink);
  }

  function onResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  }

  function clearPuzzle() {
    puzzlePieces.forEach(piece => {
      scene.remove(piece);
    });

    puzzlePieces = [];
    originalPositions = [];
  }

  function handleImageUpload(e) {
    console.log('Import image déclenché');
    const file = e.target.files[0];

    if (!file) return;

    const reader = new FileReader();

    reader.onload = function(event) {
      createPuzzle(event.target.result);
    };

    reader.readAsDataURL(file);

    // Sauvegarde locale de l'image source
    localStorage.setItem('puzzle-image', URL.createObjectURL(file));
  }

  function createPuzzle(imageSrc) {
    clearPuzzle();

    const loader = new THREE.TextureLoader();

    loader.load(imageSrc, texture => {
      texture.minFilter = THREE.LinearFilter;

      for (let y = 0; y < puzzleSize; y++) {
        for (let x = 0; x < puzzleSize; x++) {

          const geometry = new THREE.BoxGeometry(
            pieceSize,
            pieceSize,
            0.2
          );

          const materials = [];

          for (let i = 0; i < 6; i++) {
            materials.push(
              new THREE.MeshStandardMaterial({
                color: i === 4 ? 0xffffff : 0x222222,
                map: i === 4 ? texture.clone() : null
              })
            );
          }

          const piece = new THREE.Mesh(geometry, materials);

          const tx = x / puzzleSize;
          const ty = y / puzzleSize;

          piece.material[4].map.repeat.set(
            1 / puzzleSize,
            1 / puzzleSize
          );

          piece.material[4].map.offset.set(
            tx,
            1 - (1 / puzzleSize) - ty
          );

          piece.material[4].map.needsUpdate = true;

          const posX = (x - puzzleSize / 2) * pieceSize + pieceSize / 2;
          const posY = -(y - puzzleSize / 2) * pieceSize - pieceSize / 2;

          piece.position.set(posX, posY, 0);

          piece.userData.correctPosition = {
            x: posX,
            y: posY,
            z: 0
          };

          originalPositions.push({
            x: posX,
            y: posY,
            z: 0
          });

          scene.add(piece);
          puzzlePieces.push(piece);
        }
      }

      explodePuzzle();
      savePuzzle();
    });
  }

  function explodePuzzle() {
    puzzlePieces.forEach(piece => {
      piece.position.x += (Math.random() - 0.5) * 10;
      piece.position.y += (Math.random() - 0.5) * 10;
      piece.position.z += (Math.random() - 0.5) * 4;

      piece.rotation.x = Math.random() * Math.PI;
      piece.rotation.y = Math.random() * Math.PI;
    });

    updateProgress();
  }

  function onPointerDown(event) {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(puzzlePieces);

    if (intersects.length > 0) {
      selectedPiece = intersects[0].object;

      plane.setFromNormalAndCoplanarPoint(
        camera.getWorldDirection(plane.normal),
        selectedPiece.position
      );

      if (raycaster.ray.intersectPlane(plane, intersection)) {
        offset.copy(intersection).sub(selectedPiece.position);
      }

      controls.enabled = false;
    }
  }

  function onPointerMove(event) {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    if (selectedPiece) {
      if (raycaster.ray.intersectPlane(plane, intersection)) {
        selectedPiece.position.copy(intersection.sub(offset));
      }
    }
  }

  function onPointerUp() {
    if (selectedPiece) {
      const correct = selectedPiece.userData.correctPosition;

      const dist = selectedPiece.position.distanceTo(
        new THREE.Vector3(correct.x, correct.y, correct.z)
      );

      if (dist < 0.5) {
        selectedPiece.position.set(correct.x, correct.y, correct.z);
        selectedPiece.rotation.set(0, 0, 0);
      }

      updateProgress();
      savePuzzle();
    }

    selectedPiece = null;
    controls.enabled = true;
  }

  function updateProgress() {
    let good = 0;

    puzzlePieces.forEach(piece => {
      const c = piece.userData.correctPosition;

      const dist = piece.position.distanceTo(
        new THREE.Vector3(c.x, c.y, c.z)
      );

      if (dist < 0.01) {
        good++;
      }
    });

    document.getElementById('progress').innerHTML =
      `Pièces correctement placées : ${good} / ${puzzlePieces.length}`;
  }

  function savePuzzle() {
    if (!puzzlePieces.length) return;

    const data = puzzlePieces.map(piece => ({
      position: {
        x: piece.position.x,
        y: piece.position.y,
        z: piece.position.z
      },
      rotation: {
        x: piece.rotation.x,
        y: piece.rotation.y,
        z: piece.rotation.z
      },
      correctPosition: piece.userData.correctPosition
    }));

    localStorage.setItem('puzzle-progress', JSON.stringify(data));
  }

  function restorePuzzle() {
    const saved = localStorage.getItem('puzzle-progress');

    if (!saved) return;

    console.log('Progression restaurée automatiquement');
}

  function generateShareLink() {
    if (!puzzlePieces.length) return;

    const shareData = puzzlePieces.map(piece => ({
      p: piece.position,
      r: piece.rotation,
      c: piece.userData.correctPosition
    }));

    const encoded = btoa(JSON.stringify(shareData));

    const url = `${window.location.origin}${window.location.pathname}?puzzle=${encoded}`;

    const input = document.getElementById('shareLink');

    input.value = url;
    input.select();

    navigator.clipboard.writeText(url);
  }

  function loadSharedPuzzle() {
    const params = new URLSearchParams(window.location.search);
    const puzzle = params.get('puzzle');

    if (!puzzle) return;

    try {
      const data = JSON.parse(atob(puzzle));
      console.log(data);
    } catch(err) {
      console.error(err);
    }
  }

  loadSharedPuzzle();

  function animate() {
    requestAnimationFrame(animate);

    controls.update();
    renderer.render(scene, camera);
  }
</script>

</body>
</html><!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Puzzle 3D</title>

<style>
*{
  margin:0;
  padding:0;
  box-sizing:border-box;
  font-family:Arial,sans-serif;
}

body{
  overflow:hidden;
  background:#050505;
  color:white;
}

canvas{
  display:block;
}

#hud{
  position:absolute;
  top:20px;
  left:20px;
  z-index:10;
  width:320px;
  padding:20px;
  border-radius:24px;
  background:rgba(0,0,0,.78);
  backdrop-filter:blur(12px);
  border:1px solid rgba(255,255,255,.08);
}

#hud h1{
  font-size:20px;
  margin-bottom:18px;
}

.file-label,
.btn{
  width:100%;
  display:flex;
  align-items:center;
  justify-content:center;
  height:64px;
  border-radius:18px;
  cursor:pointer;
  margin-bottom:14px;
  font-size:16px;
  font-weight:700;
  transition:.2s;
  user-select:none;
}

.file-label{
  background:#171717;
  border:1px dashed rgba(255,255,255,.12);
}

.file-label:hover{
  transform:scale(1.02);
}

.btn{
  background:white;
  color:black;
  border:none;
}

.btn:hover{
  transform:scale(1.02);
}

input[type=file]{
  display:none;
}

#progress{
  margin-top:10px;
  font-size:14px;
  opacity:.8;
  line-height:1.5;
}

@media(max-width:768px){

  #hud{
    width:calc(100% - 40px);
  }

}
</style>
</head>

<body>

<div id="hud">

  <h1>🧩 Puzzle 3D</h1>

  <label class="file-label" for="upload">
    Importer une image
  </label>

  <input
    type="file"
    id="upload"
    accept="image/png,image/jpeg,image/jpg,image/webp"
    capture="environment"
  />

  <button class="btn" id="shuffle">
    Mélanger le puzzle
  </button>

  <div id="progress">
    Pièces correctement placées : 0
  </div>

</div>

<script type="module">

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.module.js';

import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.161.0/examples/jsm/controls/OrbitControls.js';

let scene;
let camera;
let renderer;
let controls;

let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();

let pieces = [];

let selected = null;

let plane = new THREE.Plane();
let offset = new THREE.Vector3();
let intersection = new THREE.Vector3();

const SIZE = 4;
const PIECE = 1;

init();
animate();
restore();

function init(){

  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x050505);

  camera = new THREE.PerspectiveCamera(
    60,
    window.innerWidth/window.innerHeight,
    0.1,
    1000
  );

  camera.position.set(0,0,8);

  renderer = new THREE.WebGLRenderer({
    antialias:true
  });

  renderer.setSize(window.innerWidth,window.innerHeight);

  document.body.appendChild(renderer.domElement);

  controls = new OrbitControls(camera,renderer.domElement);

  controls.enableDamping = true;

  const ambient = new THREE.AmbientLight(0xffffff,1.5);
  scene.add(ambient);

  const dir = new THREE.DirectionalLight(0xffffff,2);
  dir.position.set(5,10,5);

  scene.add(dir);

  window.addEventListener('resize',resize);

  renderer.domElement.addEventListener('pointerdown',down);
  renderer.domElement.addEventListener('pointermove',move);
  renderer.domElement.addEventListener('pointerup',up);

  document
    .getElementById('upload')
    .addEventListener('change',upload);

  document
    .getElementById('shuffle')
    .addEventListener('click',explode);

}

function resize(){

  camera.aspect = window.innerWidth/window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth,window.innerHeight);

}

async function upload(e){

  const file = e.target.files[0];

  if(!file) return;

  if(!file.type.startsWith('image/')){
    alert('Image invalide');
    return;
  }

  const reader = new FileReader();

  reader.onload = function(event){

    const base64 = event.target.result;

    localStorage.setItem(
      'puzzle-image',
      base64
    );

    createPuzzle(base64);

  };

  reader.onerror = function(){

    alert('Erreur import image');

  };

  reader.readAsDataURL(file);

}

function clearPuzzle(){

  pieces.forEach(p=>scene.remove(p));

  pieces = [];

}

function createPuzzle(src){

  clearPuzzle();

  const loader = new THREE.TextureLoader();

  loader.load(src,function(texture){

    texture.minFilter = THREE.LinearFilter;

    for(let y=0;y<SIZE;y++){

      for(let x=0;x<SIZE;x++){

        const geo = new THREE.BoxGeometry(
          PIECE,
          PIECE,
          .2
        );

        const mats = [];

        for(let i=0;i<6;i++){

          mats.push(
            new THREE.MeshStandardMaterial({
              color:i===4 ? 0xffffff : 0x222222,
              map:i===4 ? texture.clone() : null
            })
          );

        }

        const mesh = new THREE.Mesh(geo,mats);

        mesh.material[4].map.repeat.set(
          1/SIZE,
          1/SIZE
        );

        mesh.material[4].map.offset.set(
          x/SIZE,
          1-(1/SIZE)-y/SIZE
        );

        mesh.material[4].map.needsUpdate = true;

        const px =
          (x-SIZE/2)*PIECE + PIECE/2;

        const py =
          -(y-SIZE/2)*PIECE - PIECE/2;

        mesh.position.set(px,py,0);

        mesh.userData.correct = {
          x:px,
          y:py,
          z:0
        };

        scene.add(mesh);

        pieces.push(mesh);

      }

    }

    explode();

  });

}

function explode(){

  pieces.forEach(piece=>{

    piece.position.x +=
      (Math.random()-.5)*10;

    piece.position.y +=
      (Math.random()-.5)*10;

    piece.position.z +=
      (Math.random()-.5)*4;

    piece.rotation.x =
      Math.random()*Math.PI;

    piece.rotation.y =
      Math.random()*Math.PI;

  });

  save();
  progress();

}

function down(event){

  mouse.x =
    (event.clientX/window.innerWidth)*2-1;

  mouse.y =
    -(event.clientY/window.innerHeight)*2+1;

  raycaster.setFromCamera(mouse,camera);

  const hit =
    raycaster.intersectObjects(pieces);

  if(hit.length){

    selected = hit[0].object;

    plane.setFromNormalAndCoplanarPoint(
      camera.getWorldDirection(plane.normal),
      selected.position
    );

    if(
      raycaster.ray.intersectPlane(
        plane,
        intersection
      )
    ){

      offset.copy(intersection)
      .sub(selected.position);

    }

    controls.enabled = false;

  }

}

function move(event){

  mouse.x =
    (event.clientX/window.innerWidth)*2-1;

  mouse.y =
    -(event.clientY/window.innerHeight)*2+1;

  raycaster.setFromCamera(mouse,camera);

  if(selected){

    if(
      raycaster.ray.intersectPlane(
        plane,
        intersection
      )
    ){

      selected.position.copy(
        intersection.sub(offset)
      );

    }

  }

}

function up(){

  if(selected){

    const c = selected.userData.correct;

    const dist =
      selected.position.distanceTo(
        new THREE.Vector3(c.x,c.y,c.z)
      );

    if(dist < .5){

      selected.position.set(
        c.x,
        c.y,
        c.z
      );

      selected.rotation.set(0,0,0);

    }

    progress();
    save();

  }

  selected = null;

  controls.enabled = true;

}

function progress(){

  let ok = 0;

  pieces.forEach(piece=>{

    const c = piece.userData.correct;

    const dist =
      piece.position.distanceTo(
        new THREE.Vector3(c.x,c.y,c.z)
      );

    if(dist < .01){
      ok++;
    }

  });

  document.getElementById(
    'progress'
  ).innerHTML =
    `Pièces correctement placées : ${ok}/${pieces.length}`;

}

function save(){

  if(!pieces.length) return;

  const data = pieces.map(piece=>({

    position:{
      x:piece.position.x,
      y:piece.position.y,
      z:piece.position.z
    },

    rotation:{
      x:piece.rotation.x,
      y:piece.rotation.y,
      z:piece.rotation.z
    },

    correct:piece.userData.correct

  }));

  localStorage.setItem(
    'puzzle-save',
    JSON.stringify(data)
  );

}

function restore(){

  const image =
    localStorage.getItem('puzzle-image');

  if(image){

    createPuzzle(image);

    setTimeout(()=>{

      const save =
        localStorage.getItem('puzzle-save');

      if(!save) return;

      const data = JSON.parse(save);

      data.forEach((d,i)=>{

        if(!pieces[i]) return;

        pieces[i].position.set(
          d.position.x,
          d.position.y,
          d.position.z
        );

        pieces[i].rotation.set(
          d.rotation.x,
          d.rotation.y,
          d.rotation.z
        );

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Puzzle 3D</title>

<style>
*{
  margin:0;
  padding:0;
  box-sizing:border-box;
  font-family:Arial,sans-serif;
}

body{
  overflow:hidden;
  background:#050505;
  color:white;
}

canvas{
  display:block;
}

#hud{
  position:absolute;
  top:20px;
  left:20px;
  z-index:10;
  width:320px;
  padding:20px;
  border-radius:24px;
  background:rgba(0,0,0,.78);
  backdrop-filter:blur(12px);
  border:1px solid rgba(255,255,255,.08);
}

#hud h1{
  font-size:20px;
  margin-bottom:18px;
}

.file-label,
.btn{
  width:100%;
  display:flex;
  align-items:center;
  justify-content:center;
  height:64px;
  border-radius:18px;
  cursor:pointer;
  margin-bottom:14px;
  font-size:16px;
  font-weight:700;
  transition:.2s;
  user-select:none;
}

.file-label{
  background:#171717;
  border:1px dashed rgba(255,255,255,.12);
}

.file-label:hover{
  transform:scale(1.02);
}

.btn{
  background:white;
  color:black;
  border:none;
}

.btn:hover{
  transform:scale(1.02);
}

input[type=file]{
  display:none;
}

#progress{
  margin-top:10px;
  font-size:14px;
  opacity:.8;
  line-height:1.5;
}

@media(max-width:768px){

  #hud{
    width:calc(100% - 40px);
  }

}
</style>
</head>

<body>

<div id="hud">

  <h1>🧩 Puzzle 3D</h1>

  <label class="file-label" for="upload">
    Importer une image
  </label>

  <input
    type="file"
    id="upload"
    accept="image/png,image/jpeg,image/jpg,image/webp"
    capture="environment"
  />

  <button class="btn" id="shuffle">
    Mélanger le puzzle
  </button>

  <div id="progress">
    Pièces correctement placées : 0
  </div>

</div>

<script type="module">

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.module.js';

import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.161.0/examples/jsm/controls/OrbitControls.js';

let scene;
let camera;
let renderer;
let controls;

let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();

let pieces = [];

let selected = null;

let plane = new THREE.Plane();
let offset = new THREE.Vector3();
let intersection = new THREE.Vector3();

const SIZE = 4;
const PIECE = 1;

init();
animate();
restore();

function init(){

  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x050505);

  camera = new THREE.PerspectiveCamera(
    60,
    window.innerWidth/window.innerHeight,
    0.1,
    1000
  );

  camera.position.set(0,0,8);

  renderer = new THREE.WebGLRenderer({
    antialias:true
  });

  renderer.setSize(window.innerWidth,window.innerHeight);

  document.body.appendChild(renderer.domElement);

  controls = new OrbitControls(camera,renderer.domElement);

  controls.enableDamping = true;

  const ambient = new THREE.AmbientLight(0xffffff,1.5);
  scene.add(ambient);

  const dir = new THREE.DirectionalLight(0xffffff,2);
  dir.position.set(5,10,5);

  scene.add(dir);

  window.addEventListener('resize',resize);

  renderer.domElement.addEventListener('pointerdown',down);
  renderer.domElement.addEventListener('pointermove',move);
  renderer.domElement.addEventListener('pointerup',up);

  document
    .getElementById('upload')
    .addEventListener('change',upload);

  document
    .getElementById('shuffle')
    .addEventListener('click',explode);

}

function resize(){

  camera.aspect = window.innerWidth/window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth,window.innerHeight);

}

async function upload(e){

  const file = e.target.files[0];

  if(!file) return;

  if(!file.type.startsWith('image/')){
    alert('Image invalide');
    return;
  }

  const reader = new FileReader();

  reader.onload = function(event){

    const base64 = event.target.result;

    localStorage.setItem(
      'puzzle-image',
      base64
    );

    createPuzzle(base64);

  };

  reader.onerror = function(){

    alert('Erreur import image');

  };

  reader.readAsDataURL(file);

}

function clearPuzzle(){

  pieces.forEach(p=>scene.remove(p));

  pieces = [];

}

function createPuzzle(src){

  clearPuzzle();

  const loader = new THREE.TextureLoader();

  loader.load(src,function(texture){

    texture.minFilter = THREE.LinearFilter;

    for(let y=0;y<SIZE;y++){

      for(let x=0;x<SIZE;x++){

        const geo = new THREE.BoxGeometry(
          PIECE,
          PIECE,
          .2
        );

        const mats = [];

        for(let i=0;i<6;i++){

          mats.push(
            new THREE.MeshStandardMaterial({
              color:i===4 ? 0xffffff : 0x222222,
              map:i===4 ? texture.clone() : null
            })
          );

        }

        const mesh = new THREE.Mesh(geo,mats);

        mesh.material[4].map.repeat.set(
          1/SIZE,
          1/SIZE
        );

        mesh.material[4].map.offset.set(
          x/SIZE,
          1-(1/SIZE)-y/SIZE
        );

        mesh.material[4].map.needsUpdate = true;

        const px =
          (x-SIZE/2)*PIECE + PIECE/2;

        const py =
          -(y-SIZE/2)*PIECE - PIECE/2;

        mesh.position.set(px,py,0);

        mesh.userData.correct = {
          x:px,
          y:py,
          z:0
        };

        scene.add(mesh);

        pieces.push(mesh);

      }

    }

    explode();

  });

}

function explode(){

  pieces.forEach(piece=>{

    piece.position.x +=
      (Math.random()-.5)*10;

    piece.position.y +=
      (Math.random()-.5)*10;

    piece.position.z +=
      (Math.random()-.5)*4;

    piece.rotation.x =
      Math.random()*Math.PI;

    piece.rotation.y =
      Math.random()*Math.PI;

  });

  save();
  progress();

}

function down(event){

  mouse.x =
    (event.clientX/window.innerWidth)*2-1;

  mouse.y =
    -(event.clientY/window.innerHeight)*2+1;

  raycaster.setFromCamera(mouse,camera);

  const hit =
    raycaster.intersectObjects(pieces);

  if(hit.length){

    selected = hit[0].object;

    plane.setFromNormalAndCoplanarPoint(
      camera.getWorldDirection(plane.normal),
      selected.position
    );

    if(
      raycaster.ray.intersectPlane(
        plane,
        intersection
      )
    ){

      offset.copy(intersection)
      .sub(selected.position);

    }

    controls.enabled = false;

  }

}

function move(event){

  mouse.x =
    (event.clientX/window.innerWidth)*2-1;

  mouse.y =
    -(event.clientY/window.innerHeight)*2+1;

  raycaster.setFromCamera(mouse,camera);

  if(selected){

    if(
      raycaster.ray.intersectPlane(
        plane,
        intersection
      )
    ){

      selected.position.copy(
        intersection.sub(offset)
      );

    }

  }

}

function up(){

  if(selected){

    const c = selected.userData.correct;

    const dist =
      selected.position.distanceTo(
        new THREE.Vector3(c.x,c.y,c.z)
      );

    if(dist < .5){

      selected.position.set(
        c.x,
        c.y,
        c.z
      );

      selected.rotation.set(0,0,0);

    }

    progress();
    save();

  }

  selected = null;

  controls.enabled = true;

}

function progress(){

  let ok = 0;

  pieces.forEach(piece=>{

    const c = piece.userData.correct;

    const dist =
      piece.position.distanceTo(
        new THREE.Vector3(c.x,c.y,c.z)
      );

    if(dist < .01){
      ok++;
    }

  });

  document.getElementById(
    'progress'
  ).innerHTML =
    `Pièces correctement placées : ${ok}/${pieces.length}`;

}

function save(){

  if(!pieces.length) return;

  const data = pieces.map(piece=>({

    position:{
      x:piece.position.x,
      y:piece.position.y,
      z:piece.position.z
    },

    rotation:{
      x:piece.rotation.x,
      y:piece.rotation.y,
      z:piece.rotation.z
    },

    correct:piece.userData.correct

  }));

  localStorage.setItem(
    'puzzle-save',
    JSON.stringify(data)
  );

}

function restore(){

  const image =
    localStorage.getItem('puzzle-image');

  if(image){

    createPuzzle(image);

    setTimeout(()=>{

      const save =
        localStorage.getItem('puzzle-save');

      if(!save) return;

      const data = JSON.parse(save);

      data.forEach((d,i)=>{

        if(!pieces[i]) return;

        pieces[i].position.set(
          d.position.x,
          d.position.y,
          d.position.z
        );

        pieces[i].rotation.set(
          d.rotation.x,
          d.rotation.y,
          d.rotation.z
<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>3D Puzzle Viewer</title>

  <script src="https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/three@0.161.0/examples/js/controls/OrbitControls.js"></script>

<!-- IMPORTANT CODEPEN -->
<!-- Dans CodePen, activez :
1. Settings > JS > Babel
2. Settings > JS > Add External Scripts :
   https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.min.js
   https://cdn.jsdelivr.net/npm/three@0.161.0/examples/js/controls/OrbitControls.js
-->

  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      font-family: Arial, sans-serif;
    }

    body {
      overflow: hidden;
      background: #0f0f0f;
      color: white;
    }

    canvas {
      display: block;
    }

    #hud {
      position: absolute;
      top: 20px;
      left: 20px;
      z-index: 10;
      background: rgba(0,0,0,0.7);
      padding: 16px;
      border-radius: 14px;
      width: 300px;
      backdrop-filter: blur(10px);
      border: 1px solid rgba(255,255,255,0.1);
    }

    #hud h1 {
      font-size: 18px;
      margin-bottom: 14px;
    }

    .btn {
      width: 100%;
      margin-top: 10px;
      padding: 12px;
      border: none;
      border-radius: 10px;
      cursor: pointer;
      font-size: 14px;
      font-weight: bold;
      transition: 0.2s ease;
    }

    .btn:hover {
      transform: scale(1.02);
    }

    .primary {
      background: #ffffff;
      color: black;
    }

    .secondary {
      background: #1e1e1e;
      color: white;
      border: 1px solid rgba(255,255,255,0.1);
    }

    input[type="file"] {
      display: none;
    }

    .file-label {
      display: block;
      text-align: center;
      padding: 12px;
      background: #1b1b1b;
      border-radius: 10px;
      cursor: pointer;
      border: 1px dashed rgba(255,255,255,0.2);
    }

    #shareLink {
      margin-top: 12px;
      width: 100%;
      background: #121212;
      border: 1px solid rgba(255,255,255,0.1);
      padding: 10px;
      border-radius: 10px;
      color: white;
    }

    #status {
      margin-top: 12px;
      font-size: 12px;
      opacity: 0.8;
      line-height: 1.4;
    }

    #progress {
      margin-top: 10px;
      font-size: 13px;
    }

    @media (max-width: 768px) {
      #hud {
        width: calc(100% - 40px);
      }
    }
  </style>
</head>
<body>

<div id="hud">
  <h1>🧩 Puzzle 3D</h1>

  <label class="file-label" for="imageUpload">
    Importer une image
  </label>

  <input type="file" id="imageUpload" accept="image/*" capture="environment" />

  <button class="btn primary" id="shuffleBtn">
    Mélanger le puzzle
  </button>

  

  <button class="btn secondary" id="shareBtn">
    Générer un lien de partage
  </button>

  <input type="text" id="shareLink" readonly placeholder="Lien de partage..." />

  <div id="progress">
    Pièces correctement placées : 0
  </div>

  <div id="status">
    Glissez les pièces avec la souris.<br>
    Molette : zoom<br>
    Clic droit : rotation caméra
  </div>
</div>

<script>
  let scene, camera, renderer, controls;
  let raycaster = new THREE.Raycaster();
  let mouse = new THREE.Vector2();

  let puzzlePieces = [];
  let selectedPiece = null;
  let offset = new THREE.Vector3();
  let plane = new THREE.Plane();
  let intersection = new THREE.Vector3();

  const puzzleSize = 4;
  const pieceSize = 1;

  let originalPositions = [];

  init();
  animate();
  restorePuzzle();

  function init() {
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x111111);

    camera = new THREE.PerspectiveCamera(
      60,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );

    camera.position.set(0, 0, 8);

    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const ambient = new THREE.AmbientLight(0xffffff, 1.2);
    scene.add(ambient);

    const directional = new THREE.DirectionalLight(0xffffff, 1.5);
    directional.position.set(5, 10, 5);
    scene.add(directional);

    window.addEventListener('resize', onResize);

    renderer.domElement.addEventListener('pointerdown', onPointerDown);
    renderer.domElement.addEventListener('pointermove', onPointerMove);
    renderer.domElement.addEventListener('pointerup', onPointerUp);

    document
      .getElementById('imageUpload')
      .addEventListener('change', handleImageUpload);

    document
      .getElementById('shuffleBtn')
      .addEventListener('click', explodePuzzle);

    

    document
      .getElementById('shareBtn')
      .addEventListener('click', generateShareLink);
  }

  function onResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  }

  function clearPuzzle() {
    puzzlePieces.forEach(piece => {
      scene.remove(piece);
    });

    puzzlePieces = [];
    originalPositions = [];
  }

  function handleImageUpload(e) {
    console.log('Import image déclenché');
    const file = e.target.files[0];

    if (!file) return;

    const reader = new FileReader();

    reader.onload = function(event) {
      createPuzzle(event.target.result);
    };

    reader.readAsDataURL(file);

    // Sauvegarde locale de l'image source
    localStorage.setItem('puzzle-image', URL.createObjectURL(file));
  }

  function createPuzzle(imageSrc) {
    clearPuzzle();

    const loader = new THREE.TextureLoader();

    loader.load(imageSrc, texture => {
      texture.minFilter = THREE.LinearFilter;

      for (let y = 0; y < puzzleSize; y++) {
        for (let x = 0; x < puzzleSize; x++) {

          const geometry = new THREE.BoxGeometry(
            pieceSize,
            pieceSize,
            0.2
          );

          const materials = [];

          for (let i = 0; i < 6; i++) {
            materials.push(
              new THREE.MeshStandardMaterial({
                color: i === 4 ? 0xffffff : 0x222222,
                map: i === 4 ? texture.clone() : null
              })
            );
          }

          const piece = new THREE.Mesh(geometry, materials);

          const tx = x / puzzleSize;
          const ty = y / puzzleSize;

          piece.material[4].map.repeat.set(
            1 / puzzleSize,
            1 / puzzleSize
          );

          piece.material[4].map.offset.set(
            tx,
            1 - (1 / puzzleSize) - ty
          );

          piece.material[4].map.needsUpdate = true;

          const posX = (x - puzzleSize / 2) * pieceSize + pieceSize / 2;
          const posY = -(y - puzzleSize / 2) * pieceSize - pieceSize / 2;

          piece.position.set(posX, posY, 0);

          piece.userData.correctPosition = {
            x: posX,
            y: posY,
            z: 0
          };

          originalPositions.push({
            x: posX,
            y: posY,
            z: 0
          });

          scene.add(piece);
          puzzlePieces.push(piece);
        }
      }

      explodePuzzle();
      savePuzzle();
    });
  }

  function explodePuzzle() {
    puzzlePieces.forEach(piece => {
      piece.position.x += (Math.random() - 0.5) * 10;
      piece.position.y += (Math.random() - 0.5) * 10;
      piece.position.z += (Math.random() - 0.5) * 4;

      piece.rotation.x = Math.random() * Math.PI;
      piece.rotation.y = Math.random() * Math.PI;
    });

    updateProgress();
  }

  function onPointerDown(event) {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(puzzlePieces);

    if (intersects.length > 0) {
      selectedPiece = intersects[0].object;

      plane.setFromNormalAndCoplanarPoint(
        camera.getWorldDirection(plane.normal),
        selectedPiece.position
      );

      if (raycaster.ray.intersectPlane(plane, intersection)) {
        offset.copy(intersection).sub(selectedPiece.position);
      }

      controls.enabled = false;
    }
  }

  function onPointerMove(event) {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    if (selectedPiece) {
      if (raycaster.ray.intersectPlane(plane, intersection)) {
        selectedPiece.position.copy(intersection.sub(offset));
      }
    }
  }

  function onPointerUp() {
    if (selectedPiece) {
      const correct = selectedPiece.userData.correctPosition;

      const dist = selectedPiece.position.distanceTo(
        new THREE.Vector3(correct.x, correct.y, correct.z)
      );

      if (dist < 0.5) {
        selectedPiece.position.set(correct.x, correct.y, correct.z);
        selectedPiece.rotation.set(0, 0, 0);
      }

      updateProgress();
      savePuzzle();
    }

    selectedPiece = null;
    controls.enabled = true;
  }

  function updateProgress() {
    let good = 0;

    puzzlePieces.forEach(piece => {
      const c = piece.userData.correctPosition;

      const dist = piece.position.distanceTo(
        new THREE.Vector3(c.x, c.y, c.z)
      );

      if (dist < 0.01) {
        good++;
      }
    });

    document.getElementById('progress').innerHTML =
      `Pièces correctement placées : ${good} / ${puzzlePieces.length}`;
  }

  function savePuzzle() {
    if (!puzzlePieces.length) return;

    const data = puzzlePieces.map(piece => ({
      position: {
        x: piece.position.x,
        y: piece.position.y,
        z: piece.position.z
      },
      rotation: {
        x: piece.rotation.x,
        y: piece.rotation.y,
        z: piece.rotation.z
      },
      correctPosition: piece.userData.correctPosition
    }));

    localStorage.setItem('puzzle-progress', JSON.stringify(data));
  }

  function restorePuzzle() {
    const saved = localStorage.getItem('puzzle-progress');

    if (!saved) return;

    console.log('Progression restaurée automatiquement');
}

  function generateShareLink() {
    if (!puzzlePieces.length) return;

    const shareData = puzzlePieces.map(piece => ({
      p: piece.position,
      r: piece.rotation,
      c: piece.userData.correctPosition
    }));

    const encoded = btoa(JSON.stringify(shareData));

    const url = `${window.location.origin}${window.location.pathname}?puzzle=${encoded}`;

    const input = document.getElementById('shareLink');

    input.value = url;
    input.select();

    navigator.clipboard.writeText(url);
				</div>
				</div>
					</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/my-puzzle/">My Puzzle</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
