Current browser width: —
mobile tablet desktop

Responsive Breakpoints

↔️ Resize your browser window to see the breakpoints switch — each size loads a different teapot color and frame set.

The responsive array tells ease360° to serve a different frame set, canvas size, and crop offset per viewport width. In production you'd serve the same image at different sizes — here the teapot changes color so the swap is immediately visible.

In this example you will learn the following:

How it works

Each entry in the responsive array defines a complete frame set for a max-viewport-width range. ease360° activates the first entry whose breakpoint value is ≥ the current viewport width and monitors for resize events — when the viewport crosses a boundary it fires responsiveUpdate, swaps the frame set, and starts loading. The same progressUpdate callback handles the loader either way.

This demo uses local teapot folders with different colors per breakpoint. In production, pair this with a CDN that supports URL-based image resizing so a single master upload serves every breakpoint — see Example 5 — CDN & Cloudinary for that pattern.

Local frame sets per breakpoint

Each breakpoint gets its own frame set. Here three different teapot colors make the swap immediately visible — in production you'd serve the same image at different sizes from a CDN.

// Build a frame array from a local folder
const makePath = (folder, count) =>
    Array.from({ length: count }, (_, i) => `../images/${folder}/teapot_${i}.jpg`);

// Different colors per breakpoint — makes the swap visually obvious in this demo.
// In production you'd serve the same image at different sizes from a CDN.
const greenTeapot  = makePath('teapot_green_36',  36); // desktop  — 540×540 source
const orangeTeapot = makePath('teapot_orange_36', 36); // tablet   — 1024×1024 source
const blueTeapot   = makePath('teapot_blue_36',   36); // mobile   — 540×540 source

The responsive array

Breakpoints are evaluated as max-viewport-width in px, largest to smallest. ease360° activates the first entry whose breakpoint value is ≥ the current viewport width.

preloadSmart applies independently to each breakpoint swap. When the viewport crosses a breakpoint, ease360° treats the new frame set exactly like a fresh load — it loads every other frame first (50%), fires progressUpdate as they arrive, and drops in the remaining frames on first interaction. This means a user on a slow connection who resizes from desktop to tablet only ever downloads 50% of the tablet frames unless they actually spin the viewer. Each breakpoint pays only for what the user engages with.

The sourceWidth and sourceHeight inside each breakpoint object must match the intrinsic pixel dimensions of that breakpoint's delivered images — not the container display size. ease360° uses these to calculate the internal canvas math and aspect ratio. If you pass incorrect dimensions the image will appear stretched, squashed, or incorrectly cropped.

In this example the orange teapot was rendered at 1024×1024 while the green and blue sets are 540×540. Setting sourceWidth: 1024, sourceHeight: 1024 on the orange breakpoint tells ease360° the correct source ratio — if you left it at 540×540 the image would render incorrectly as shown below.

backgroundSize: 'cover-center' scales the image to fill the canvas while keeping the subject centered — essential for responsive layouts where the canvas aspect ratio changes per breakpoint. Without it you may only see a cropped corner of the image at certain sizes.

backgroundOffsetY shifts the crop position relative to center — useful when the subject sits off-center in the source image. With cover-center this is a post-calculation applied after the centering math. Positive values shift the crop up, negative shift it down — tune this per breakpoint independently to suit your specific image composition.

const myEase360 = ease360('#myEase360', {
    backgroundSize: 'cover-center', // fill canvas, keep subject centered at all breakpoints
    preloadSmart:   true,
    damping:        0.94,

    responsive: [

        // Desktop — green teapot (viewport > 1024px)
        // Source images are 540×540 — width/height must reflect this
        { breakpoint: 1920, frames: greenTeapot,  sourceWidth: 540,  sourceHeight: 540,  flex: { w: true } },

        // Tablet — orange teapot (viewport ≤ 1024px)
        // These source images were rendered at 1024×1024 — width/height must match.
        // Using 540×540 here would produce incorrect scaling and clipping.
        // backgroundOffsetY: 50 shifts the crop up 50px relative to center —
        // positive = up, negative = down. Tune to suit your image composition.
        { breakpoint: 1024, frames: orangeTeapot, sourceWidth: 1024, sourceHeight: 1024, backgroundOffsetY: 50, flex: { w: true } },

        // Mobile — blue teapot (viewport ≤ 640px)
        // Back to 540×540 source images
        { breakpoint: 640,  frames: blueTeapot,   sourceWidth: 540,  sourceHeight: 540,  flex: { w: true } }

    ],

    progressUpdate:   () => onProgress(),
    angleUpdate:      () => onAngle(),
    stateUpdate:      (s) => onState(s),
    responsiveUpdate: () => onResponsive()
});

responsiveUpdate

Fired when the viewport crosses a breakpoint and ease360° begins loading a new frame set. Re-show the loader here — progressUpdate takes care of hiding it again once the new set is fully loaded. The breakpoint readout above the viewer is updated here too.

const onResponsive = () => {
    const loading = document.querySelector('.loading');
    loading.classList.remove('opacity0');
    loading.style.zIndex = '1000';
    document.querySelector('#myEase360 canvas').classList.remove('opacity1');
    loading.querySelector('h4').textContent = 'loading';

    // The width readout and legend pill are updated by the resize listener —
    // onResponsive just needs to handle the loader swap.
};

Production — CDN approach

For production work, replace the local folder paths with CDN URLs and pass a different width per breakpoint. The CDN resizes the same master image on the fly — no separate static folders needed. See Example 5 — CDN & Cloudinary for the full pattern.

// CDN approach — one master upload, resized per breakpoint via URL.
// Use the same three breakpoints as the local example above.
// sourceWidth/height are optional — ease360° probes frames[0] if omitted.
const cdn = { cloud: 'YOURCLOUDID', version: 'YOURVERSION', prefix: 'your-prefix' };

const makeFrames = (width) =>
    Array.from({ length: 36 }, (_, i) => {
        const frame = String(i + 1).padStart(3, '0');
        return `https://res.cloudinary.com/${cdn.cloud}/image/upload/w_${width},q_auto,f_webp/${cdn.version}/${cdn.prefix}-${frame}.jpg`;
    });

responsive: [
    // Desktop > 1024px — 540px frames, Cloudinary delivers 540×540
    { breakpoint: 1920, frames: makeFrames(540), sourceWidth: 540, sourceHeight: 540, flex: { w: true } },

    // Tablet 641–1024px — 400px frames, lighter payload
    { breakpoint: 1024, frames: makeFrames(400), sourceWidth: 400, sourceHeight: 400, flex: { w: true } },

    // Mobile ≤ 640px — 320px frames, lightest payload
    { breakpoint: 640,  frames: makeFrames(320), sourceWidth: 320, sourceHeight: 320, flex: { w: true } }
]