White-label web development · for creative agencies
You send the approved Figma. We build it in WordPress or as a custom front end, including GSAP motion, page transitions, responsive behavior and cross-browser QA. Your team keeps the client relationship and the credit.
We read the intent
We remove the noise
Structure takes shape
Motion connects the states
Code makes it real
Ready for
production.
We read the intent. We remove the noise. Structure takes shape. Motion connects the states. Code makes it real. Ready for production.
(function () { "use strict"; const GSAP_URL = "https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"; const SCROLLTRIGGER_URL = "https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"; const TRIGGER_ID = "leadly-video-scroll-fluid-v1"; const VIDEO_FPS = 30; function loadScript(url, globalName) { return new Promise(function (resolve, reject) { if (window[globalName]) { resolve(); return; } const existing = document.querySelector( 'script[src="' + url + '"]' ); if (existing) { existing.addEventListener( "load", resolve, { once: true } ); existing.addEventListener( "error", reject, { once: true } ); return; } const script = document.createElement("script"); script.src = url; script.async = true; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } async function loadLibraries() { if (!window.gsap) { await loadScript( GSAP_URL, "gsap" ); } if (!window.ScrollTrigger) { await loadScript( SCROLLTRIGGER_URL, "ScrollTrigger" ); } } function clamp(value, min, max) { return Math.min( Math.max(value, min), max ); } function lerp(start, end, progress) { return ( start + (end - start) * progress ); } function smoothstep(value) { const t = clamp(value, 0, 1); return ( t * t * (3 - 2 * t) ); } function getViewportProfile(duration) { const width = window.innerWidth; const height = window.innerHeight; const widthProgress = clamp( (width - 360) / (1600 - 360), 0, 1 ); const portrait = height > width; /* * Todo se interpola de forma continua. * No hay una cantidad fija de scroll por dispositivo. */ let screens = lerp( portrait ? 5.7 : 6.3, 8.8, widthProgress ); screens *= clamp( duration / 10, 0.88, 1.12 ); const firstText = lerp( 0.057, 0.075, widthProgress ); const stageDuration = lerp( 0.124, 0.118, widthProgress ); const stageGap = lerp( 0.015, 0.021, widthProgress ); const lastStageEnd = firstText + (stageDuration + stageGap) * 4 + stageDuration; const finalStart = clamp( lastStageEnd + 0.055, 0.80, 0.835 ); return { scrollDistance: Math.round( height * screens ), firstText: firstText, stageDuration: stageDuration, stageGap: stageGap, finalStart: finalStart, finalEnterEnd: finalStart + 0.055, cueFadeStart: firstText * 0.28, cueFadeEnd: firstText * 0.82, shadeStart: finalStart - 0.07, shadeEnd: finalStart + 0.045, marqueeFadeStart: finalStart - 0.11, marqueeFadeEnd: finalStart - 0.02 }; } function getVisibleHeaderHeight() { const selectors = [ ".elementor-location-header", "header.elementor", "header" ]; for ( let index = 0; index < selectors.length; index += 1 ) { const elements = Array.from( document.querySelectorAll( selectors[index] ) ); for ( let item = 0; item 0 && rect.bottom > 0; if (visible) { return rect.height; } } } return 0; } function setFluidCssVariables(container) { const width = window.innerWidth; const height = window.innerHeight; const widthProgress = clamp( (width - 360) / (1600 - 360), 0, 1 ); const portrait = height > width; /* * Menor porcentaje desplaza visualmente * el contenido hacia la derecha. */ let videoFocus = lerp( 35, 44.5, widthProgress ); if (portrait) { videoFocus -= 1; } const headerHeight = getVisibleHeaderHeight(); const marqueeExtra = lerp( portrait ? 22 : 15, 26, widthProgress ); const marqueeTop = headerHeight + marqueeExtra; const cueSize = lerp( portrait ? 112 : 122, 178, widthProgress ); container.style.setProperty( "--leadly-video-focus-x", videoFocus.toFixed(2) + "%" ); container.style.setProperty( "--leadly-marquee-top", Math.round(marqueeTop) + "px" ); container.style.setProperty( "--leadly-cue-size", Math.round(cueSize) + "px" ); } function configureVideo(video) { video.muted = true; video.defaultMuted = true; video.playsInline = true; video.preload = "auto"; video.autoplay = false; video.loop = false; video.controls = false; video.disablePictureInPicture = true; video.setAttribute("muted", ""); video.setAttribute("playsinline", ""); video.setAttribute("preload", "auto"); video.setAttribute( "x-webkit-airplay", "deny" ); video.removeAttribute("autoplay"); video.removeAttribute("loop"); video.removeAttribute("controls"); video.pause(); } function waitForVideo(video) { return new Promise(function (resolve, reject) { if ( video.readyState >= 1 && Number.isFinite(video.duration) && video.duration > 0 ) { resolve(); return; } function loaded() { cleanup(); resolve(); } function failed() { cleanup(); reject( new Error( "No fue posible cargar los metadatos del video." ) ); } function cleanup() { video.removeEventListener( "loadedmetadata", loaded ); video.removeEventListener( "error", failed ); } video.addEventListener( "loadedmetadata", loaded, { once: true } ); video.addEventListener( "error", failed, { once: true } ); video.load(); }); } /* * El video ya no es un tween GSAP. * Su tiempo se asigna desde el progreso real * y puede avanzar o retroceder. */ function createVideoScrubber( video, endTime ) { let targetTime = 0; let frameRequest = null; let lastAppliedTime = -1; const frameDuration = 1 / VIDEO_FPS; function applyTime(force) { frameRequest = null; const snappedTime = clamp( Math.round( targetTime * VIDEO_FPS ) / VIDEO_FPS, 0, endTime ); const changedEnough = Math.abs( snappedTime - lastAppliedTime ) >= frameDuration * 0.8; if ( !force && !changedEnough ) { return; } try { video.currentTime = snappedTime; lastAppliedTime = snappedTime; } catch (error) { /* * El siguiente frame vuelve a intentarlo. */ } } function setProgress( progress, force ) { targetTime = clamp( progress, 0, 1 ) * endTime; if (force) { if (frameRequest) { cancelAnimationFrame( frameRequest ); frameRequest = null; } applyTime(true); return; } if (!frameRequest) { frameRequest = requestAnimationFrame( function () { applyTime(false); } ); } } function destroy() { if (frameRequest) { cancelAnimationFrame( frameRequest ); frameRequest = null; } } return { setProgress: setProgress, destroy: destroy }; } function hideText(text) { text.style.setProperty( "opacity", "0", "important" ); text.style.setProperty( "visibility", "hidden", "important" ); } function showRegularText( text, opacity, y ) { text.style.setProperty( "opacity", String(opacity), "important" ); text.style.setProperty( "visibility", opacity > 0.001 ? "visible" : "hidden", "important" ); text.style.transform = "translateY(calc(-50% + " + y + "px))"; } function showFinalText( text, opacity, y ) { text.style.setProperty( "opacity", String(opacity), "important" ); text.style.setProperty( "visibility", opacity > 0.001 ? "visible" : "hidden", "important" ); text.style.transform = "translate(-50%, calc(-50% + " + y + "px))"; } function hideAllTexts( regularTexts, finalText ) { regularTexts.forEach( hideText ); hideText(finalText); } function updateRegularText( text, localProgress ) { let opacity; let y; if (localProgress < 0.24) { const progress = smoothstep( localProgress / 0.24 ); opacity = progress; y = 105 * (1 - progress); } else if ( localProgress < 0.76 ) { const progress = ( localProgress - 0.24 ) / 0.52; opacity = 1; y = -12 * progress; } else { const progress = smoothstep( ( localProgress - 0.76 ) / 0.24 ); opacity = 1 - progress; y = -12 - 112 * progress; } showRegularText( text, opacity, y ); } function buildStages(profile) { const stages = []; let start = profile.firstText; for ( let index = 0; index < 5; index += 1 ) { stages.push({ start: start, end: start + profile.stageDuration }); start += profile.stageDuration + profile.stageGap; } return stages; } function updateFinalText( finalText, progress, profile ) { if ( progress < profile.finalStart ) { hideText(finalText); return; } if ( progress profile.cueFadeStart ) { opacity = 1 - smoothstep( ( progress - profile.cueFadeStart ) / ( profile.cueFadeEnd - profile.cueFadeStart ) ); } opacity = clamp( opacity, 0, 1 ); cue.style.opacity = String(opacity); cue.style.visibility = opacity > 0.001 ? "visible" : "hidden"; cue.style.transform = "translate3d(0, " + ( -6 * smoothstep( progress / profile.cueFadeEnd ) ) + "px, 0)"; rotor.style.transform = "rotate(" + progress * 3600 + "deg)"; asterisk.style.transform = "rotate(" + progress * -5400 + "deg)"; } function updateMarquee( marquee, progress, active, profile ) { if (!marquee || !active) { if (marquee) { marquee.style.opacity = "0"; marquee.style.visibility = "hidden"; } return; } const portrait = window.innerHeight > window.innerWidth; const baseOpacity = portrait ? 0.18 : 0.145; let opacity = baseOpacity; if ( progress > profile.marqueeFadeStart ) { opacity = baseOpacity * ( 1 - smoothstep( ( progress - profile.marqueeFadeStart ) / ( profile.marqueeFadeEnd - profile.marqueeFadeStart ) ) ); } opacity = clamp( opacity, 0, baseOpacity ); marquee.style.opacity = String(opacity); marquee.style.visibility = opacity > 0.001 ? "visible" : "hidden"; } function updateShade( shade, progress, profile ) { const portrait = window.innerHeight > window.innerWidth; const baseOpacity = portrait ? 0.45 : 0.38; let opacity = baseOpacity; if ( progress > profile.shadeStart ) { opacity = baseOpacity + ( 1 - baseOpacity ) * smoothstep( ( progress - profile.shadeStart ) / ( profile.shadeEnd - profile.shadeStart ) ); } shade.style.backgroundColor = "rgba(6, 7, 7, " + clamp( opacity, baseOpacity, 1 ) + ")"; } function updateScene( progress, active, profile, regularTexts, finalText, shade, cue, rotor, asterisk, marquee ) { hideAllTexts( regularTexts, finalText ); const stages = buildStages(profile); stages.forEach( function (stage, index) { if ( progress >= stage.start && progress 16; if (!widthChanged) { return; } lastViewportWidth = window.innerWidth; window.clearTimeout( resizeTimer ); resizeTimer = window.setTimeout( function () { ScrollTrigger.refresh(); }, 220 ); } window.addEventListener( "resize", handleResponsiveChange, { passive: true } ); window.addEventListener( "orientationchange", function () { window.setTimeout( function () { lastViewportWidth = window.innerWidth; setFluidCssVariables( container ); ScrollTrigger.refresh(); }, 350 ); }, { passive: true } ); /* * Desbloquea el video en iPhone/iPad. */ function unlockVideo() { const playPromise = video.play(); if ( playPromise && typeof playPromise.then === "function" ) { playPromise .then(function () { video.pause(); scrubber.setProgress( trigger.progress, true ); }) .catch(function () {}); } } document.addEventListener( "touchstart", unlockVideo, { once: true, passive: true } ); document.addEventListener( "pointerdown", unlockVideo, { once: true, passive: true } ); if ( document.fonts && document.fonts.ready ) { document.fonts.ready.then( function () { setFluidCssVariables( container ); ScrollTrigger.refresh(); } ); } window.setTimeout( function () { setFluidCssVariables( container ); ScrollTrigger.refresh(); }, 450 ); } catch (error) { const fallbackContainer = document.querySelector( "#video-scroll-container" ); if (fallbackContainer) { const fallbackVideo = fallbackContainer.querySelector( ".elementor-widget-video video" ); const fallbackRegularTexts = Array.from( fallbackContainer.querySelectorAll( ".leadly-copy:not(.leadly-copy--final)" ) ); const fallbackFinalText = fallbackContainer.querySelector( ".leadly-copy--final" ); fallbackContainer.classList.add( "leadly-video-fallback" ); fallbackContainer.setAttribute( "data-scroll-video-mode", "fallback" ); fallbackRegularTexts.forEach( hideText ); if (fallbackFinalText) { showFinalText( fallbackFinalText, 1, 0 ); } if (fallbackVideo) { fallbackVideo.style.display = "none"; } window.__zicuaScrollVideoState = { mode: "fallback", reason: error && error.message ? error.message : "scene-initialization-failed" }; } } } function boot() { window.setTimeout( initScene, 120 ); } if ( document.readyState === "loading" ) { document.addEventListener( "DOMContentLoaded", boot, { once: true } ); } else { boot(); } })();Selected work
2023 to 2026
Capabilities




Technical direction01
Motion systems02
WordPress & front-end03
Experience QA04
BRIEF · PROTOTYPE · BUILD · REFINEMENT · QA
You lead the client.
We handle the build.
Bring us in when the approved design depends on the front end. We build the interaction, motion and production code without stepping into the client relationship.
- 01
Motion Sprint
We isolate and build the part your team should not have to simplify: a key interaction, page transition or scroll sequence.
- 02
Creative Front-end Build
We turn an approved design into a complete site with CMS, responsive behavior, accessibility and QA. What reaches the browser keeps the hierarchy and intent of the original file.
- 03
Hero Experience
We prototype and build the interaction that defines a campaign or launch, then make sure it supports the page instead of taking it over.
- 04
Pitch Prototype
We turn a presentation idea into a working prototype so your team can test feasibility and let the client experience it before committing to production.
A build process
your team can see and control.
We agree on ownership, checkpoints and handoff before work begins. Delivery can remain under your agency’s name.
01Review
We review the Figma, scope, timing, stack and constraints. If something does not fit, we say so before touching the code.
02Prototype
We test the motion that defines the experience early. Your team approves rhythm, states and behavior while changes are still inexpensive.
03Build
We develop components, transitions, responsive behavior and CMS support within the agreed stack, without forcing an unnecessary rebuild.
04Refine
We treat revisions with the same care as the first pass and flag every decision that affects scope, timing or performance.
05Hand off
We test devices, browsers, keyboard use, reduced motion and performance. Your team receives production-ready code and documentation.
Show us the part
the design depends on.
Send the Figma, deadline and motion reference. We review the difficult interaction, flag the risks and reply with a clear next step.
Send the brief ↗Available for focused sprints or full white-label builds.