// Matter.js Tag Canvas Optimized Version - Tags Fall Only After Page Load (function () { // Don't show tags on screens <= 767px if (window.innerWidth <= 767) { return; } const isMobile = window.innerWidth <= 981; let engine, render, tags = []; let containerWidth, containerHeight; const radius = 20; // Tag border radius function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } // FIXED: Use proper Matter.js API for position updates function updateTagPositions() { tags.forEach((tag) => { if (!tag || !tag.position) return; // Safety check const clampedX = clamp(tag.position.x, radius, containerWidth - radius); const clampedY = clamp(tag.position.y, radius, containerHeight - radius); // Only update if position actually needs clamping if (clampedX !== tag.position.x || clampedY !== tag.position.y) { Matter.Body.setPosition(tag, { x: clampedX, y: clampedY }); } }); } // --- Physics throttling --- let lastPhysicsUpdate = 0; const PHYSICS_FPS = isMobile ? 15 : 30; // Lower on mobile for perf // Will be toggled when hero is on screen let simulationActive = true; function renderLoop(time) { requestAnimationFrame(renderLoop); // Only update physics if simulation is active if (simulationActive) { if (!lastPhysicsUpdate || time - lastPhysicsUpdate > 1000 / PHYSICS_FPS) { Matter.Engine.update(engine, 1000 / 60); lastPhysicsUpdate = time; if (isMobile) updateTagPositions(); } Matter.Render.world(render); } } function initSimulation() { const Engine = Matter.Engine, Render = Matter.Render, Mouse = Matter.Mouse, MouseConstraint = Matter.MouseConstraint, World = Matter.World, Bodies = Matter.Bodies; engine = Engine.create({ timing: { timeScale: isMobile ? 0.6 : 0.8 } }); engine.world.gravity.y = 3; // Start falling immediately const world = engine.world; const containerElement = document.querySelector(".tag-canvas"); containerWidth = containerElement.clientWidth; containerHeight = containerElement.clientHeight; render = Render.create({ element: containerElement, engine: engine, options: { width: containerWidth, height: containerHeight, pixelRatio: window.devicePixelRatio > 1 ? 2 : 1, background: "transparent", wireframes: false } }); // Boundaries const ground = Bodies.rectangle( containerWidth / 2, containerHeight + 50, containerWidth + 200, 100, { isStatic: true, render: { visible: false } } ); const wallLeft = Bodies.rectangle( -50, containerHeight / 2, 100, containerHeight + 200, { isStatic: true, render: { visible: false } } ); const wallRight = Bodies.rectangle( containerWidth + 50, containerHeight / 2, 100, containerHeight + 200, { isStatic: true, render: { visible: false } } ); const roof = Bodies.rectangle( containerWidth / 2, -500, containerWidth + 200, 100, { isStatic: true, render: { visible: false } } ); function getRandomPosX() { const container = document.getElementById("hero"); const containerWidth = container.clientWidth; const margin = 10; const safeSpawnWidth = containerWidth - margin * 2; const randomPosX = Math.random() * safeSpawnWidth + margin; return randomPosX; } function createTag(tagInfo) { const posY = 0; const tag = Bodies.rectangle( getRandomPosX(), posY, tagInfo.width, tagInfo.height, { chamfer: { radius: radius }, restitution: 0.8, render: { sprite: { texture: tagInfo.url, xScale: 1, yScale: 1 } } } ); tag.linkHref = tagInfo.linkHref; return tag; } const tagsDataFull = [ { url: "https://uploads-ssl.webflow.com/66133a38093a875be8ff5474/6615ae1b730183b47ae999a5_PerBlue.svg", width: 168, height: 48, linkHref: "/case-study/perblue", imageUrl: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66477ba6aef9027235146148_Perblue-thumb.webp", linkText: "PerBlue" }, { url: "https://uploads-ssl.webflow.com/66133a38093a875be8ff5474/6615ae1ba30ce24776ab8a73_Natalie%20Cooks.svg", width: 221, height: 48, linkHref: "/case-study/natalie-cooks", imageUrl: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66477ba6aef9027235146147_Natalie-Cooks-Thumb.webp", linkText: "Natalie Cooks" }, { url: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/66f48e9e2b14d4de9428f4e1_Desert%20Dine.svg", width: 206, height: 48, linkHref: "/case-study/desert-dine", imageUrl: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/66f4709653c0e1965f206fd9_dd-cover.webp", linkText: "Desert Dine" }, { url: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/696152afd6f1a4237d3a1653_Trion%20Pill.png", width: 500, height: 48, linkHref: "/case-study/trion-living", imageUrl: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/6965157bde945148e1011527_Trion-Living-Test.svg", linkText: "Trion Living" }, { url: "https://uploads-ssl.webflow.com/66133a38093a875be8ff5474/6615ae1bf4edd863aa249127_WP%20Capital%20Group.svg", width: 256, height: 48, linkHref: "/case-study/wealth-partners-capital-group", imageUrl: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66477ba6aef9027235146157_Wealth-Partners-Thumb.webp", linkText: "WP Capital Group" }, { url: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/66f48e9b001c502dfa3fe026_MHOP%20Fitness.svg", width: 222, height: 48, linkHref: "/case-study/minis-house-of-pain", imageUrl: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/66f4709bd49762e49a5d24ab_mhop-cover.webp", linkText: "MHOP Fitness" }, { url: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66a823a19a9c9fe2134c310e_Majestic%20Painting.svg", width: 234, height: 48, linkHref: "/case-study/majestic-painting", imageUrl: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66b261df213e9dd6e057c568_Majestic%20Painting%20512x268.webp", linkText: "Majestic Painting" }, { url: "https://uploads-ssl.webflow.com/66133a38093a875be8ff5474/6615ae1a46ce6f3c414d8cc1_Structure%20Landscapes.svg", width: 287, height: 48, linkHref: "/case-study/structure-landscapes", imageUrl: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66477ba6aef90272351461a7_StructureLand-Thumb.webp", linkText: "Structure Landscapes" }, { url: "https://uploads-ssl.webflow.com/66133a38093a875be8ff5474/6615ae1aa30ce24776ab89e7_Violumas.svg", width: 180, height: 48, linkHref: "/case-study/violumas", imageUrl: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66477ba6aef902723514611d_Violumas-Thumb.webp", linkText: "Violumas" }, { url: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66a823a054af096728907838_Habitat.svg", width: 298, height: 48, linkHref: "/case-study/habitat", imageUrl: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66b27866c710249c77685b30_Habitat%20512x268.webp", linkText: "Habitat" }, { url: "https://uploads-ssl.webflow.com/66133a38093a875be8ff5474/6615ae1a2ebdb32f8e45f38f_GeoCivix.svg", width: 180, height: 48, linkHref: "/case-study/geocivix", imageUrl: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66477ba6aef902723514613b_Geocivix-Thumb.webp", linkText: "Geocivix" }, { url: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66a823a07ffbd466e140946b_Beauty%20Vault.svg", width: 211, height: 48, linkHref: "/case-study/beauty-vault", imageUrl: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66a914e284c6088b6679fb6a_Beauty%20Vault.webp", linkText: "Beauty Vault" }, { url: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/696915a68c6f5ee59fce3aaa_Soled%20Out.svg", width: 261, height: 48, linkHref: "/case-study/soled-out-jc", imageUrl: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/696941e69a02129b68fd395b_Soled%20Out-Figma%20Screenshot.png", linkText: "Soled Out" }, { url: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/696915a66de2df623b9dac08_Brem%20Method.svg", width: 282, height: 48, linkHref: "/case-study/brem-method", imageUrl: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/69693ffa51bc09e3c1d45398_Brem%20Method-Figma-Screenshot-2.png", linkText: "Brem Method" }, { url: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/69694598f0825431b4b9cc56_Gray%20Area.svg", width: 199, height: 46, linkHref: "/case-study/gray-area-shipping", imageUrl: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/6969439d834ec58f323ddd62_Gray%20Area-%20FIgma%20Screenshot%202.png", linkText: "Gray Area" }, { url: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66477ba6aef902723514618b_CrewChiefs.svg", width: 199, height: 46, linkHref: "/case-study/crew-chiefs", imageUrl: "https://uploads-ssl.webflow.com/66477ba6aef90272351460cc/66477ba6aef9027235146121_crew-chiefs-thumb.webp", linkText: "CrewChiefs" }, { url: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/67db254343a66d7358f2a484_Cover%20Brothers.svg", width: 199, height: 46, linkHref: "/case-study/cover-brothers", imageUrl: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/67db2430280a2898077bdac2_Cover-Brothers-Thumb.webp", linkText: "Cover Brothers" }, { url: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/67db25438cb17c92b4618f0b_Architectural%20Glass.svg", width: 199, height: 46, linkHref: "/case-study/architectural-glass", imageUrl: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/67db24305a0075be1347c120_Architectural-Glass-Thumb.webp", linkText: "Architectural Glass" }, { url: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/67db25434a8f4ce09e7a4c95_Jim%20Adler.svg", width: 199, height: 46, linkHref: "/case-study/jim-adler", imageUrl: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/67db243133956d793420c0e7_JIM-Adler-Thumb.webp", linkText: "Jim Adler" }, { url: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/67db2543561139756068c1c6_Pet%20Payments.svg", width: 199, height: 46, linkHref: "/case-study/pet-payments", imageUrl: "https://cdn.prod.website-files.com/66477ba6aef90272351460cc/67db242f07ea210f8449752c_Pet-Payments-Thumb.webp", linkText: "Pet Payments" } ]; let tagsData = tagsDataFull; if (isMobile) { const mobileTagsIndexes = [0, 1, 2, 3, 4, 5, 7]; tagsData = mobileTagsIndexes.map((index) => tagsDataFull[index]); } tagsData.forEach((tagInfo) => { const tag = createTag(tagInfo); tagInfo.tag = tag; }); tags = tagsData.map((tagInfo) => tagInfo.tag); World.add(engine.world, [ground, wallLeft, wallRight, roof, ...tags]); // Mouse const mouse = Mouse.create(render.canvas), mouseConstraint = MouseConstraint.create(engine, { mouse: mouse, constraint: { stiffness: 0.2, render: { visible: false } } }); World.add(world, mouseConstraint); render.mouse = mouse; mouse.element.removeEventListener("mousewheel", mouse.mousewheel); mouse.element.removeEventListener("DOMMouseScroll", mouse.mousewheel); // --- Click Logic --- let click = false; let dragThreshold = 5; let initialMousePos; function handleClick(event) { const tagCanvas = document.querySelector(".tag-canvas"); if (!tagCanvas) return; const rect = tagCanvas.getBoundingClientRect(); const mouseX = event.clientX; const mouseY = event.clientY; if ( mouseX >= rect.left && mouseX <= rect.right && mouseY >= rect.top && mouseY <= rect.bottom ) { const mousePosition = mouse.position; const bodies = Matter.Composite.allBodies(engine.world); const bodyUnderMouse = Matter.Query.point(bodies, mousePosition)[0]; if (bodyUnderMouse && bodyUnderMouse.linkHref) { window.location.href = bodyUnderMouse.linkHref; } } } if (isMobile) { document.addEventListener("touchstart", (event) => { click = true; initialMousePos = { x: event.touches[0].clientX, y: event.touches[0].clientY }; }); document.addEventListener("touchmove", (event) => { if (click) { const dx = event.touches[0].clientX - initialMousePos.x; const dy = event.touches[0].clientY - initialMousePos.y; if (Math.sqrt(dx * dx + dy * dy) > dragThreshold) { click = false; } } }); document.addEventListener("touchend", (event) => { if (click) handleClick(event); click = false; }); } else { document.addEventListener("mousedown", (event) => { click = true; initialMousePos = { x: event.clientX, y: event.clientY }; }); document.addEventListener("mousemove", (event) => { if (click) { const dx = event.clientX - initialMousePos.x; const dy = event.clientY - initialMousePos.y; if (Math.sqrt(dx * dx + dy * dy) > dragThreshold) { click = false; } } }); document.addEventListener("mouseup", (event) => { if (click) handleClick(event); click = false; }); } // --- Hover Container --- const hoverContainer = document.createElement("div"); hoverContainer.className = "hover-container"; hoverContainer.style.position = "absolute"; hoverContainer.style.opacity = "0"; hoverContainer.style.visibility = "hidden"; hoverContainer.style.transition = "opacity 0.5s ease"; hoverContainer.style.zIndex = "1000"; hoverContainer.style.display = "flex"; hoverContainer.style.flexWrap = "wrap"; hoverContainer.style.width = "319px"; hoverContainer.style.height = "222px"; containerElement.appendChild(hoverContainer); const image = document.createElement("img"); image.style.width = "100%"; image.style.height = "149px"; hoverContainer.appendChild(image); const link = document.createElement("a"); hoverContainer.appendChild(link); Matter.Events.on(mouseConstraint, "mousemove", function (event) { const mousePosition = event.mouse.position; const bodies = Matter.Composite.allBodies(engine.world); const bodyUnderMouse = Matter.Query.point(bodies, mousePosition)[0]; if (!mouseConstraint.constraint.bodyB) { const tagInfo = tagsData.find((tagInfo) => tagInfo.tag === bodyUnderMouse); if (tagInfo) { image.src = tagInfo.imageUrl; link.href = tagInfo.linkHref; link.textContent = tagInfo.linkText; const additionalSpace = 30; hoverContainer.style.top = bodyUnderMouse.position.y - hoverContainer.offsetHeight - additionalSpace + "px"; hoverContainer.style.left = bodyUnderMouse.position.x - 159.5 + "px"; hoverContainer.style.display = "flex"; hoverContainer.style.opacity = "1"; hoverContainer.style.visibility = "visible"; } else { hoverContainer.style.opacity = "0"; hoverContainer.style.visibility = "hidden"; } } else { hoverContainer.style.opacity = "0"; hoverContainer.style.visibility = "hidden"; } }); hoverContainer.addEventListener("mouseenter", function () { hoverContainer.style.opacity = "1"; hoverContainer.style.visibility = "visible"; }); hoverContainer.addEventListener("mouseleave", function () { hoverContainer.style.opacity = "0"; setTimeout(() => { hoverContainer.style.visibility = "hidden"; }, 500); }); Matter.Runner.run(engine); Render.run(render); renderLoop(); startFallingSmoothly(); // <-- add this here } // --- Pause simulation when not in view --- function checkAndInit() { function tryInit() { const containerElement = document.querySelector(".tag-canvas"); if (containerElement && containerElement.offsetParent !== null) { // Section exists and is visible if (!engine) { simulationActive = true; initSimulation(); } return true; } return false; } // Try immediately if (!tryInit()) { // Keep checking every 100ms until found const interval = setInterval(() => { if (tryInit()) clearInterval(interval); }, 100); } } checkAndInit(); // --- Let pills fall only after everything is loaded! --- function startFallingSmoothly() { if (engine && engine.world) { // Start strong engine.world.gravity.y = 3; engine.timing.timeScale = 1.2; // Gradually slow down over 2 seconds const duration = 2000; const start = performance.now(); function smoothSlowdown(now) { const elapsed = now - start; const progress = Math.min(elapsed / duration, 1); // Ease out curve const eased = 1 - Math.pow(1 - progress, 3); // Gravity goes from 3 → 1 engine.world.gravity.y = 3 - 2 * eased; // Time scale from 1.2 → 0.8 engine.timing.timeScale = 1.2 - 0.4 * eased; if (progress < 1) requestAnimationFrame(smoothSlowdown); } requestAnimationFrame(smoothSlowdown); } } // Hide .hero-content while dragging const canvas = document.querySelector("canvas"); const heroContent = document.querySelector(".hero-content"); let isDragging = false; if (canvas) { canvas.addEventListener("mousedown", () => { isDragging = true; if (heroContent) heroContent.classList.add("none"); }); } document.addEventListener("mouseup", () => { if (isDragging) { isDragging = false; if (heroContent) heroContent.classList.remove("none"); } }); })(); // --- END ---