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:
responsive array serves different frame sets and canvas dimensions per viewport widthresponsiveUpdate fires on breakpoint changes and how to show the loader in responsesourceWidth and sourceHeight per breakpoint must match the delivered image dimensions, not the display sizebackgroundOffsetY shifts the crop relative to center with cover-centerpreloadSmart applies independently per breakpoint swap
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.
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
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()
});
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.
};
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 } }
]