{"roots":["0:3"],"nodeById":{"0:3":{"type":"WEBPAGE","id":"0:3","name":"/","absoluteBoundingBox":{"x":0.0,"y":0.0,"width":2711.0,"height":1079.0},"isolatedAbsoluteRenderBounds":{"x":0.0,"y":0.0,"width":2711.0,"height":1079.0},"relativeTransform":[[1.0,0.0,0.0],[0.0,1.0,0.0]],"size":{"x":2711.0,"y":1079.0},"fills":[{"opacity":0.0470588244497776,"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":1.0,"b":1.0,"a":1.0},"visible":true}],"strokeAlign":"INSIDE","strokes":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"children":["0:4","1:82","0:6"]},"0:6":{"type":"FRAME","id":"0:6","name":"Mobile","absoluteBoundingBox":{"x":2272.0,"y":100.0,"width":375.0,"height":915.0},"isolatedAbsoluteRenderBounds":{"x":2272.0,"y":100.0,"width":375.0,"height":915.0},"relativeTransform":[[1.0,0.0,2272.0],[0.0,1.0,100.0]],"size":{"x":375.0,"y":915.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":0.0,"g":0.0,"b":0.0,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"paddingRight":87.0,"paddingLeft":88.0,"clipsContent":true,"overflowDirection":"VERTICAL_SCROLLING","layoutMode":"VERTICAL","itemSpacing":64.0,"counterAxisAlignItems":"CENTER","primaryAxisAlignItems":"CENTER","primaryAxisSizingMode":"FIXED","counterAxisSizingMode":"FIXED","isBreakpointFrame":true,"children":["1:90"]},"1:82":{"type":"FRAME","id":"1:82","name":"Tablet","absoluteBoundingBox":{"x":1408.0,"y":100.0,"width":800.0,"height":915.0},"isolatedAbsoluteRenderBounds":{"x":1408.0,"y":100.0,"width":800.0,"height":915.0},"relativeTransform":[[1.0,0.0,1408.0],[0.0,1.0,100.0]],"size":{"x":800.0,"y":915.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":0.0,"g":0.0,"b":0.0,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"paddingRight":300.0,"paddingLeft":300.0,"clipsContent":true,"overflowDirection":"VERTICAL_SCROLLING","layoutMode":"VERTICAL","itemSpacing":65.0,"counterAxisAlignItems":"CENTER","primaryAxisAlignItems":"CENTER","primaryAxisSizingMode":"FIXED","counterAxisSizingMode":"FIXED","isBreakpointFrame":true,"children":["1:89"]},"1:71":{"type":"SVG","id":"1:71","name":"Union","absoluteBoundingBox":{"x":622.32470703125,"y":430.310546875,"width":163.3505859375,"height":186.37890625},"isolatedAbsoluteRenderBounds":{"x":622.32470703125,"y":430.310546875,"width":163.3505859375,"height":186.37890625},"relativeTransform":[[1.0,0.0,0.0],[0.0,1.0,0.0]],"size":{"x":163.3505859375,"y":186.37890625},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":0.310666650533676,"b":0.059999980032444,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","strokes":[],"strokeWeight":0.0,"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"hash":"01324e02e2ef9bc54d2d0e52f30550b738e93372"},"1:88":{"type":"TEXT","id":"1:88","name":"Coming soon.","absoluteBoundingBox":{"x":2397.5,"y":631.39453125,"width":125.0,"height":24.0},"isolatedAbsoluteRenderBounds":{"x":2398.52001953125,"y":636.094543457031,"width":122.427490234375,"height":18.7000122070312},"relativeTransform":[[1.0,0.0,0.0],[0.0,1.0,171.7890625]],"size":{"x":125.0,"y":24.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":1.0,"b":1.0,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"OUTSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"characterStyleOverrides":[],"characters":"Coming soon.","lineIndentations":[0],"lineTypes":["NONE"],"listStartOffsets":[],"lineStyleOverrides":[0],"lineTextDirections":null,"textAutoResize":"WIDTH_AND_HEIGHT","style":{"fontFamily":"AF Sobremesa Variable","fontPostScriptName":"AFSobremesaVariable-Regular","fontStyle":"Regular","textAutoResize":"WIDTH_AND_HEIGHT","fontVariantPosition":"NORMAL","fontSize":20.0,"textAlignHorizontal":"LEFT","textAlignVertical":"TOP","letterSpacing":0.0,"letterSpacingValue":0.0,"letterSpacingUnit":"PERCENT","lineHeightPx":24.0,"lineHeightPercent":100.0,"lineHeightUnit":"INTRINSIC_%","paragraphSpacing":0,"paragraphIndent":0,"listSpacing":0,"italic":false,"textCase":"ORIGINAL","textDecoration":"NONE","textDecorationSkipInk":false,"textDecorationStyle":"solid","textTruncation":"DISABLED","lineHeightPercentFontSize":100},"styleOverrideTable":{}},"1:89":{"type":"FRAME","id":"1:89","name":"Frame 1","absoluteBoundingBox":{"x":1726.5,"y":430.01123046875,"width":163.0,"height":254.9775390625},"isolatedAbsoluteRenderBounds":{"x":1726.5,"y":430.01123046875,"width":163.0,"height":254.9775390625},"relativeTransform":[[1.0,0.0,318.5],[0.0,1.0,330.01123046875]],"size":{"x":163.0,"y":254.9775390625},"fills":[],"strokeAlign":"INSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"layoutMode":"VERTICAL","itemSpacing":45.0,"counterAxisAlignItems":"CENTER","primaryAxisAlignItems":"CENTER","children":["1:83","1:87"]},"1:90":{"type":"FRAME","id":"1:90","name":"Frame 1","absoluteBoundingBox":{"x":2397.5,"y":459.60546875,"width":125.0,"height":195.7890625},"isolatedAbsoluteRenderBounds":{"x":2397.5,"y":459.60546875,"width":125.0,"height":195.7890625},"relativeTransform":[[1.0,0.0,125.5],[0.0,1.0,359.60546875]],"size":{"x":125.0,"y":195.7890625},"fills":[],"strokeAlign":"INSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"layoutMode":"VERTICAL","itemSpacing":44.0,"counterAxisAlignItems":"CENTER","primaryAxisAlignItems":"CENTER","children":["1:73","1:88"]},"1:87":{"type":"TEXT","id":"1:87","name":"Coming soon.","absoluteBoundingBox":{"x":1745.5,"y":660.98876953125,"width":125.0,"height":24.0},"isolatedAbsoluteRenderBounds":{"x":1746.52001953125,"y":665.688781738281,"width":122.427490234375,"height":18.7000122070312},"relativeTransform":[[1.0,0.0,19.0],[0.0,1.0,230.9775390625]],"size":{"x":125.0,"y":24.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":1.0,"b":1.0,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"OUTSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"characterStyleOverrides":[],"characters":"Coming soon.","lineIndentations":[0],"lineTypes":["NONE"],"listStartOffsets":[],"lineStyleOverrides":[0],"lineTextDirections":null,"textAutoResize":"WIDTH_AND_HEIGHT","style":{"fontFamily":"AF Sobremesa Variable","fontPostScriptName":"AFSobremesaVariable-Regular","fontStyle":"Regular","textAutoResize":"WIDTH_AND_HEIGHT","fontVariantPosition":"NORMAL","fontSize":20.0,"textAlignHorizontal":"LEFT","textAlignVertical":"TOP","letterSpacing":0.0,"letterSpacingValue":0.0,"letterSpacingUnit":"PERCENT","lineHeightPx":24.0,"lineHeightPercent":100.0,"lineHeightUnit":"INTRINSIC_%","paragraphSpacing":0,"paragraphIndent":0,"listSpacing":0,"italic":false,"textCase":"ORIGINAL","textDecoration":"NONE","textDecorationSkipInk":false,"textDecorationStyle":"solid","textTruncation":"DISABLED","lineHeightPercentFontSize":100},"styleOverrideTable":{}},"1:83":{"type":"SVG","id":"1:83","name":"Union","absoluteBoundingBox":{"x":1726.5,"y":430.01123046875,"width":163.0,"height":185.9775390625},"isolatedAbsoluteRenderBounds":{"x":1726.5,"y":430.01123046875,"width":163.0,"height":185.9775390625},"relativeTransform":[[1.0,0.0,0.0],[0.0,1.0,0.0]],"size":{"x":163.0,"y":185.9775390625},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":0.310666650533676,"b":0.059999980032444,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","strokes":[],"strokeWeight":0.0,"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"hash":"5827173d78f9ff33d96802f8131fc661e2928360"},"0:4":{"type":"FRAME","id":"0:4","name":"Desktop","absoluteBoundingBox":{"x":64.0,"y":100.0,"width":1280.0,"height":915.0},"isolatedAbsoluteRenderBounds":{"x":64.0,"y":100.0,"width":1280.0,"height":915.0},"relativeTransform":[[1.0,0.0,64.0],[0.0,1.0,100.0]],"size":{"x":1280.0,"y":915.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":0.0,"g":0.0,"b":0.0,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"clipsContent":true,"overflowDirection":"VERTICAL_SCROLLING","layoutMode":"VERTICAL","itemSpacing":64.0,"counterAxisAlignItems":"CENTER","primaryAxisAlignItems":"CENTER","primaryAxisSizingMode":"FIXED","counterAxisSizingMode":"FIXED","isBreakpointFrame":true,"children":["1:91"]},"1:91":{"type":"FRAME","id":"1:91","name":"Frame 1","absoluteBoundingBox":{"x":622.32470703125,"y":430.310546875,"width":163.3505859375,"height":254.37890625},"isolatedAbsoluteRenderBounds":{"x":622.32470703125,"y":430.310546875,"width":163.3505859375,"height":254.37890625},"relativeTransform":[[1.0,0.0,558.32470703125],[0.0,1.0,330.310546875]],"size":{"x":163.3505859375,"y":254.37890625},"fills":[],"strokeAlign":"INSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"layoutMode":"VERTICAL","itemSpacing":44.0,"counterAxisAlignItems":"CENTER","primaryAxisAlignItems":"CENTER","children":["1:71","1:85"]},"1:73":{"type":"SVG","id":"1:73","name":"Union","absoluteBoundingBox":{"x":2404.0,"y":459.60546875,"width":112.0,"height":127.7890625},"isolatedAbsoluteRenderBounds":{"x":2404.0,"y":459.60546875,"width":112.0,"height":127.7890625},"relativeTransform":[[1.0,0.0,6.5],[0.0,1.0,0.0]],"size":{"x":112.0,"y":127.7890625},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":0.310666650533676,"b":0.059999980032444,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","strokes":[],"strokeWeight":0.0,"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"hash":"4bca1596394a915bc96b7637d102efc7047c26ad"},"1:85":{"type":"TEXT","id":"1:85","name":"Coming soon.","absoluteBoundingBox":{"x":641.5,"y":660.689453125,"width":125.0,"height":24.0},"isolatedAbsoluteRenderBounds":{"x":642.52001953125,"y":665.389465332031,"width":122.427490234375,"height":18.7000122070312},"relativeTransform":[[1.0,0.0,19.17529296875],[0.0,1.0,230.37890625]],"size":{"x":125.0,"y":24.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":1.0,"b":1.0,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"OUTSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"characterStyleOverrides":[],"characters":"Coming soon.","lineIndentations":[0],"lineTypes":["NONE"],"listStartOffsets":[],"lineStyleOverrides":[0],"lineTextDirections":null,"textAutoResize":"WIDTH_AND_HEIGHT","style":{"fontFamily":"AF Sobremesa Variable","fontPostScriptName":"AFSobremesaVariable-Regular","fontStyle":"Regular","textAutoResize":"WIDTH_AND_HEIGHT","fontVariantPosition":"NORMAL","fontSize":20.0,"textAlignHorizontal":"LEFT","textAlignVertical":"TOP","letterSpacing":0.0,"letterSpacingValue":0.0,"letterSpacingUnit":"PERCENT","lineHeightPx":24.0,"lineHeightPercent":100.0,"lineHeightUnit":"INTRINSIC_%","paragraphSpacing":0,"paragraphIndent":0,"listSpacing":0,"italic":false,"textCase":"ORIGINAL","textDecoration":"NONE","textDecorationSkipInk":false,"textDecorationStyle":"solid","textTruncation":"DISABLED","lineHeightPercentFontSize":100},"styleOverrideTable":{}}},"assetIdToGuid":{},"guidToUrl":{"0:3":"/"},"fonts":{},"assets":{"5827173d78f9ff33d96802f8131fc661e2928360":{"type":"GENERATED_ASSET","url":"5827173d78f9ff33d96802f8131fc661e2928360.svg","size":{"x":163.0,"y":185.9775390625},"offsets":{"left":{"value":0.0,"unit":"PERCENT"},"right":{"value":0.0,"unit":"PERCENT"},"top":{"value":0.0,"unit":"PERCENT"},"bottom":{"value":0.0,"unit":"PERCENT"}},"format":"SVG"},"4bca1596394a915bc96b7637d102efc7047c26ad":{"type":"GENERATED_ASSET","url":"4bca1596394a915bc96b7637d102efc7047c26ad.svg","size":{"x":112.0,"y":127.7890625},"offsets":{"left":{"value":0.0,"unit":"PERCENT"},"right":{"value":0.0,"unit":"PERCENT"},"top":{"value":0.0,"unit":"PERCENT"},"bottom":{"value":0.0,"unit":"PERCENT"}},"format":"SVG"},"01324e02e2ef9bc54d2d0e52f30550b738e93372":{"type":"GENERATED_ASSET","url":"01324e02e2ef9bc54d2d0e52f30550b738e93372.svg","size":{"x":163.3505859375,"y":186.37890625},"offsets":{"left":{"value":0.0,"unit":"PERCENT"},"right":{"value":0.0,"unit":"PERCENT"},"top":{"value":0.0,"unit":"PERCENT"},"bottom":{"value":0.0,"unit":"PERCENT"}},"format":"SVG"}},"stablePathToAssetInfo":{"1:73":{"hash":"4bca1596394a915bc96b7637d102efc7047c26ad"},"1:83":{"hash":"5827173d78f9ff33d96802f8131fc661e2928360"},"1:71":{"hash":"01324e02e2ef9bc54d2d0e52f30550b738e93372"}},"animateRootIds":[],"siteSettings":{"title":"OhLàKa!","scalingMode":"REFLOW","blockSearchIndexing":true,"customCodeBodyEnd":"<script>\nwindow.addEventListener('load', () => {\n    setTimeout(() => initPixelEffect(), 1500)\n})\n\nfunction initPixelEffect() {\n    const isMobile = () => !window.matchMedia('(hover: hover)').matches\n\n    const getVisibleSVGs = () => {\n        return Array.from(document.querySelectorAll('img[src*=\".svg\"]')).filter(img => {\n            const rect = img.getBoundingClientRect()\n            const style = window.getComputedStyle(img)\n            return rect.width > 0 && rect.height > 0 &&\n                style.display !== 'none' &&\n                style.visibility !== 'hidden' &&\n                style.opacity !== '0'\n        })\n    }\n\n    let activeEffects = []\n    const destroyAll = () => { activeEffects.forEach(fn => fn()); activeEffects = [] }\n\n    const initOnVisible = () => {\n        destroyAll()\n        getVisibleSVGs().forEach(img => {\n            activeEffects.push(isMobile() ? attachMobileEffect(img) : attachDesktopEffect(img))\n        })\n    }\n\n    let resizeTimer\n    window.addEventListener('resize', () => {\n        clearTimeout(resizeTimer)\n        destroyAll()\n        resizeTimer = setTimeout(() => initOnVisible(), 400)\n    })\n\n    initOnVisible()\n}\n\nfunction extractParticles(img, pixelSize, canvasW, canvasH) {\n    const rect = img.getBoundingClientRect()\n    const tmpCanvas = document.createElement('canvas')\n    tmpCanvas.width = rect.width\n    tmpCanvas.height = rect.height\n    const tmpCtx = tmpCanvas.getContext('2d')\n\n    return new Promise(resolve => {\n        const image = new Image()\n        image.crossOrigin = 'anonymous'\n        image.onload = () => {\n            tmpCtx.drawImage(image, 0, 0, rect.width, rect.height)\n            const baseX = (canvasW - rect.width) / 2\n            const baseY = (canvasH - rect.height) / 2\n            const particles = []\n            for (let x = 0; x < rect.width; x += pixelSize) {\n                for (let y = 0; y < rect.height; y += pixelSize) {\n                    const d = tmpCtx.getImageData(x, y, 1, 1).data\n                    if (d[3] < 10) continue\n                    particles.push({\n                        x: baseX + x, y: baseY + y,\n                        ox: baseX + x, oy: baseY + y,\n                        vx: 0, vy: 0,\n                        color: `rgba(${d[0]},${d[1]},${d[2]},${d[3]/255})`\n                    })\n                }\n            }\n            resolve({ particles, rect })\n        }\n        image.src = img.src\n    })\n}\n\n// =====================\n// DESKTOP\n// =====================\nfunction attachDesktopEffect(img) {\n    let canvas, ctx, particles, animFrame\n    let isHovered = false\n    let scale = 0\n    let isSettingUp = false // ✅ verrou\n    const PIXEL_SIZE = 5\n    const CURSOR_RADIUS = 90\n    const ZOOM = 2.5\n    let mouseX = 0, mouseY = 0\n\n    const cleanup = () => {\n        cancelAnimationFrame(animFrame)\n        if (canvas) { canvas.remove(); canvas = null }\n        particles = null; scale = 0; isHovered = false; isSettingUp = false\n        img.style.opacity = '1'\n        img.removeEventListener('mouseenter', onEnter)\n        img.removeEventListener('mousemove', onMove)\n    }\n\n    const setup = async () => {\n        if (isSettingUp || canvas) return // ✅ empêche double setup\n        isSettingUp = true\n\n        const imgRect = img.getBoundingClientRect()\n        const padding = 300\n\n        canvas = document.createElement('canvas')\n        canvas.width = imgRect.width * ZOOM + padding * 2\n        canvas.height = imgRect.height * ZOOM + padding * 2\n\n        canvas.style.cssText = `\n            position: fixed;\n            top: ${imgRect.top + imgRect.height/2 - canvas.height/2}px;\n            left: ${imgRect.left + imgRect.width/2 - canvas.width/2}px;\n            pointer-events: all;\n            z-index: 9999;\n            cursor: pointer;\n        `\n        document.body.appendChild(canvas)\n        ctx = canvas.getContext('2d', { alpha: true })\n\n        const result = await extractParticles(img, PIXEL_SIZE, canvas.width, canvas.height)\n        particles = result.particles\n\n        const cxC = canvas.width / 2\n        const cyC = canvas.height / 2\n        particles.forEach(p => {\n            const localX = p.ox - (canvas.width - imgRect.width) / 2\n            const localY = p.oy - (canvas.height - imgRect.height) / 2\n            p.tx = cxC + (localX - imgRect.width / 2) * ZOOM\n            p.ty = cyC + (localY - imgRect.height / 2) * ZOOM\n        })\n\n        canvas.addEventListener('mousemove', (e) => {\n            mouseX = e.clientX; mouseY = e.clientY\n            isHovered = true\n            img.style.opacity = '0'\n        })\n        canvas.addEventListener('mouseleave', () => { isHovered = false })\n\n        isSettingUp = false // ✅ libère le verrou\n        animFrame = requestAnimationFrame(animate)\n    }\n\n    const lerp = (a, b, t) => a + (b - a) * t\n\n    const animate = () => {\n        if (!canvas || !ctx) return\n        ctx.clearRect(0, 0, canvas.width, canvas.height)\n        scale = lerp(scale, isHovered ? 1 : 0, isHovered ? 0.1 : 0.2)\n\n        const canvasRect = canvas.getBoundingClientRect()\n        const cx = mouseX - canvasRect.left\n        const cy = mouseY - canvasRect.top\n        let allSettled = true\n\n        particles.forEach(p => {\n            const destX = lerp(p.ox, p.tx, scale)\n            const destY = lerp(p.oy, p.ty, scale)\n\n            if (isHovered) {\n                const dx = p.x - cx\n                const dy = p.y - cy\n                const dist = Math.sqrt(dx * dx + dy * dy)\n                if (dist < CURSOR_RADIUS && dist > 0) {\n                    const force = (CURSOR_RADIUS - dist) / CURSOR_RADIUS\n                    p.vx += (dx / dist) * force * 5\n                    p.vy += (dy / dist) * force * 5\n                }\n            }\n\n            const stiffness = isHovered ? 0.12 : 0.35\n            const friction = isHovered ? 0.72 : 0.55\n            p.vx += (destX - p.x) * stiffness\n            p.vy += (destY - p.y) * stiffness\n            p.vx *= friction\n            p.vy *= friction\n            p.x += p.vx\n            p.y += p.vy\n\n            if (!isHovered && (Math.abs(p.x - p.ox) > 0.8 || Math.abs(p.y - p.oy) > 0.8)) allSettled = false\n\n            ctx.fillStyle = p.color\n            ctx.fillRect(Math.round(p.x), Math.round(p.y), PIXEL_SIZE, PIXEL_SIZE)\n        })\n\n        if (!isHovered && allSettled) {\n            cancelAnimationFrame(animFrame)\n            ctx.clearRect(0, 0, canvas.width, canvas.height)\n            particles.forEach(p => { ctx.fillStyle = p.color; ctx.fillRect(p.ox, p.oy, PIXEL_SIZE, PIXEL_SIZE) })\n            img.style.opacity = '1'\n            requestAnimationFrame(() => { if (canvas) { canvas.remove(); canvas = null } })\n            return\n        }\n        animFrame = requestAnimationFrame(animate)\n    }\n\n    const onEnter = (e) => {\n        mouseX = e.clientX; mouseY = e.clientY\n        isHovered = true\n        img.style.opacity = '0'\n        if (!canvas && !isSettingUp) setup() // ✅ verrou vérifié ici aussi\n        else if (canvas) { cancelAnimationFrame(animFrame); animFrame = requestAnimationFrame(animate) }\n    }\n    const onMove = (e) => { mouseX = e.clientX; mouseY = e.clientY }\n\n    img.addEventListener('mouseenter', onEnter)\n    img.addEventListener('mousemove', onMove)\n    return cleanup\n}\n\n// =====================\n// MOBILE\n// =====================\nfunction attachMobileEffect(img) {\n    let canvas, ctx, particles, animFrame, autoTimer\n    let scale = 0\n    let isActive = false\n    let isSettingUp = false // ✅ verrou\n    let wrapper = null\n    const PIXEL_SIZE = 5\n    const CURSOR_RADIUS = 120\n    const ZOOM = 2.0\n    const DELAY = 2500\n    const HOLD = 2000\n    let touchX = 0, touchY = 0\n\n    const cleanup = () => {\n        cancelAnimationFrame(animFrame)\n        clearTimeout(autoTimer)\n        isSettingUp = false\n        isActive = false\n        scale = 0\n        if (canvas) { canvas.remove(); canvas = null }\n        // ✅ Démonte le wrapper proprement\n        if (wrapper && img.parentNode === wrapper) {\n            wrapper.parentNode.insertBefore(img, wrapper)\n            wrapper.remove()\n            wrapper = null\n        }\n        particles = null\n        img.style.opacity = '1'\n    }\n\n    const setup = async () => {\n        if (isSettingUp || canvas) return // ✅ verrou strict\n        isSettingUp = true\n\n        const imgRect = img.getBoundingClientRect()\n        const padding = 200\n\n        // ✅ Crée le wrapper une seule fois\n        if (!wrapper) {\n            wrapper = document.createElement('div')\n            wrapper.style.cssText = `\n                position: relative;\n                display: inline-flex;\n                align-items: center;\n                justify-content: center;\n                width: ${imgRect.width}px;\n                height: ${imgRect.height}px;\n            `\n            img.parentNode.insertBefore(wrapper, img)\n            wrapper.appendChild(img)\n        }\n\n        canvas = document.createElement('canvas')\n        canvas.width = imgRect.width * ZOOM + padding * 2\n        canvas.height = imgRect.height * ZOOM + padding * 2\n\n        canvas.style.cssText = `\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            pointer-events: all;\n            z-index: 9999;\n        `\n        wrapper.appendChild(canvas)\n        ctx = canvas.getContext('2d', { alpha: true })\n\n        const result = await extractParticles(img, PIXEL_SIZE, canvas.width, canvas.height)\n        particles = result.particles\n\n        const cxC = canvas.width / 2\n        const cyC = canvas.height / 2\n        const imgW = imgRect.width\n        const imgH = imgRect.height\n        particles.forEach(p => {\n            const localX = p.ox - (canvas.width - imgW) / 2\n            const localY = p.oy - (canvas.height - imgH) / 2\n            p.tx = cxC + (localX - imgW / 2) * ZOOM\n            p.ty = cyC + (localY - imgH / 2) * ZOOM\n        })\n\n        canvas.addEventListener('touchstart', (e) => {\n            e.preventDefault()\n            clearTimeout(autoTimer) // ✅ stoppe l'auto si touch\n            const t = e.touches[0]\n            touchX = t.clientX\n            touchY = t.clientY\n            isActive = true\n            img.style.opacity = '0'\n        }, { passive: false })\n\n        canvas.addEventListener('touchmove', (e) => {\n            e.preventDefault()\n            const t = e.touches[0]\n            touchX = t.clientX\n            touchY = t.clientY\n        }, { passive: false })\n\n        canvas.addEventListener('touchend', () => {\n            isActive = false\n            autoTimer = setTimeout(() => startAutoZoom(), DELAY)\n        })\n\n        isSettingUp = false // ✅ libère le verrou\n        animFrame = requestAnimationFrame(animate)\n    }\n\n    const lerp = (a, b, t) => a + (b - a) * t\n\n    const animate = () => {\n        if (!canvas || !ctx) return\n        ctx.clearRect(0, 0, canvas.width, canvas.height)\n        scale = lerp(scale, isActive ? 1 : 0, isActive ? 0.1 : 0.2)\n\n        const canvasRect = canvas.getBoundingClientRect()\n        const cx = touchX - canvasRect.left\n        const cy = touchY - canvasRect.top\n        let allSettled = true\n\n        particles.forEach(p => {\n            const destX = lerp(p.ox, p.tx, scale)\n            const destY = lerp(p.oy, p.ty, scale)\n\n            if (isActive) {\n                const dx = p.x - cx\n                const dy = p.y - cy\n                const dist = Math.sqrt(dx * dx + dy * dy)\n                if (dist < CURSOR_RADIUS && dist > 0) {\n                    const force = (CURSOR_RADIUS - dist) / CURSOR_RADIUS\n                    p.vx += (dx / dist) * force * 5\n                    p.vy += (dy / dist) * force * 5\n                }\n            }\n\n            const stiffness = isActive ? 0.12 : 0.35\n            const friction = isActive ? 0.72 : 0.55\n            p.vx += (destX - p.x) * stiffness\n            p.vy += (destY - p.y) * stiffness\n            p.vx *= friction\n            p.vy *= friction\n            p.x += p.vx\n            p.y += p.vy\n\n            if (!isActive && (Math.abs(p.x - p.ox) > 0.8 || Math.abs(p.y - p.oy) > 0.8)) allSettled = false\n\n            ctx.fillStyle = p.color\n            ctx.fillRect(Math.round(p.x), Math.round(p.y), PIXEL_SIZE, PIXEL_SIZE)\n        })\n\n        if (!isActive && allSettled) {\n            cancelAnimationFrame(animFrame)\n            ctx.clearRect(0, 0, canvas.width, canvas.height)\n            ctx.globalAlpha = 1\n            particles.forEach(p => { ctx.fillStyle = p.color; ctx.fillRect(p.ox, p.oy, PIXEL_SIZE, PIXEL_SIZE) })\n            img.style.opacity = '1'\n            requestAnimationFrame(() => {\n                if (canvas) { canvas.remove(); canvas = null }\n                particles = null; scale = 0\n                // ✅ Relance cycle uniquement si pas déjà en cours\n                if (!autoTimer) autoTimer = setTimeout(() => startAutoZoom(), DELAY)\n            })\n            return\n        }\n\n        animFrame = requestAnimationFrame(animate)\n    }\n\n    const startAutoZoom = async () => {\n        autoTimer = null\n        if (isSettingUp || canvas || isActive) return // ✅ triple verrou\n        await setup()\n        if (!canvas) return // ✅ vérifie que le setup a bien réussi\n\n        const canvasRect = canvas.getBoundingClientRect()\n        touchX = canvasRect.left + canvasRect.width / 2\n        touchY = canvasRect.top + canvasRect.height / 2\n        isActive = true\n        img.style.opacity = '0'\n\n        autoTimer = setTimeout(() => {\n            isActive = false\n        }, HOLD)\n    }\n\n    autoTimer = setTimeout(() => startAutoZoom(), DELAY)\n    return cleanup\n}\n</script>","labs":{"E5FBBA911B2B7A09E649D4BE6CDF8591EAEFC881":false}},"sourceCodeHash":""}