Serving frames from a CDN is the production pattern — one master upload, URL-based resizing per breakpoint, no static image folders. This example uses the G90 teaser shoot served from Cloudinary. The same approach works with any CDN that supports URL transforms.
In this example you will learn the following:
sourceWidth and sourceHeight from the first frame — no hardcoding neededresponsive array for per-breakpoint sizingease360.cdn.js generates frame arrays and responsive sets from a single config objectoverwrite: trueEvery Cloudinary delivery URL follows the same pattern. Understanding each segment makes it easy to build frame arrays for any asset at any size.
https://res.cloudinary.com/{cloud}/image/upload/{transforms}/{version}/{public-id}.jpg
# Example:
https://res.cloudinary.com/dxx6ajk76/image/upload/w_540,q_auto,f_webp/v1778226364/G90-LondonGray-lg-001.jpg
# └────────┘ └───┘ └──────┘ └────┘ └──────────┘ └─────────────────────┘
# cloud name width quality format version public ID
w_540 resizes the master to 540px wide on the fly.
q_auto negotiates quality automatically per browser.
f_webp serves WebP where supported, JPEG as fallback.
The version string (v1778226364) is part of the public ID for
assets uploaded via the Cloudinary dashboard — it ensures the CDN serves
the correct upload. Assets uploaded with overwrite: true on an
existing public ID don't generate a new version string, making re-uploads
seamless with no URL changes needed.
Rather than building Cloudinary URLs by hand, ease360.cdn.js
generates frame arrays from a config object.
Include it alongside the main script — then ease360cdn is
available globally.
<script src="lib/ease360-1.0.0.min.js"></script>
<script src="lib/ease360.cdn.js"></script>
The G90 set has 36 frames named G90-LondonGray-lg-001.jpg
through G90-LondonGray-lg-036.jpg. Define a base config once,
then call ease360cdn.frames() with a width to get an array
at any size.
const base = {
cloudName: 'dxx6ajk76',
version: 'v1778226364',
prefix: 'G90-LondonGray-lg',
frames: 36,
format: 'auto', // WebP where supported, JPEG fallback
quality: 'auto' // Cloudinary negotiates per browser
};
// 36 URLs at 540px — one call, no string building
const frames540 = ease360cdn.frames({ ...base, width: 540 });
// Same master, different width — no copy-paste, no maintenance
const frames375 = ease360cdn.frames({ ...base, width: 375 });
These two options tell ease360° the pixel dimensions of the delivered image — what the browser actually receives, not the master file. As of v1.0.5 they are optional. When omitted, ease360° probes the first frame automatically. When provided, the probe is skipped and init is synchronous.
The critical thing to understand is that these must reflect the
delivered size — what Cloudinary sends at the requested
w_ transform — not the master on the server. The G90 master
is 4326×2850, but w_540 delivers 540×356. ease360° uses these
values for all canvas math. Getting them wrong (or letting the probe read
them automatically) is the difference between a correctly cropped viewer
and a stretched or clipped one.
With ease360.cdn.js included, replace the manual
makeFrames() helper with ease360cdn.frames().
The base config is reused for every call — change the width, get a
different frame set. sourceWidth and sourceHeight
are omitted here — ease360° probes the first frame automatically.
Provide them explicitly to skip the probe for synchronous init.
const base = {
cloudName: 'dxx6ajk76',
version: 'v1778226364',
prefix: 'G90-LondonGray-lg',
frames: 36,
format: 'auto', // WebP where supported, JPEG fallback
quality: 'auto' // Cloudinary negotiates per browser
};
const myEase360 = ease360('#myEase360', {
frames: ease360cdn.frames({ ...base, width: 540 }),
backgroundSize: 'cover',
frameDirection: -1, // reverse spin direction
preloadSmart: true,
progressUpdate: () => onProgress(),
angleUpdate: () => onAngle()
});
The probe loads the first frame with crossOrigin: 'anonymous'
to read pixel dimensions. If your CDN doesn't send an
Access-Control-Allow-Origin header, naturalWidth
returns 0 and ease360° will log a clear error with fix instructions:
[ease360] Could not read frame dimensions — likely a CORS issue.
The probe image loaded but naturalWidth/naturalHeight returned 0.
Fix: Ensure your CDN sends an Access-Control-Allow-Origin header.
For Cloudinary: configure CORS in Settings → Security.
Alternatively, provide sourceWidth and sourceHeight explicitly to skip the probe:
ease360('#viewer', { frames, sourceWidth: 540, sourceHeight: 360 });
Cloudinary sets CORS headers correctly by default so this is only relevant
if you're serving frames from a custom origin. The fallback is always
available — just provide sourceWidth and sourceHeight
explicitly and the probe never runs.
The real advantage of CDN delivery is serving the right size per breakpoint
from a single master. Pass a different width to makeFrames()
per breakpoint — desktop gets 540px, mobile gets 375px. Cloudinary resizes
on the fly. No separate static folders, no separate uploads.
const myEase360 = ease360('#myEase360', {
backgroundSize: 'cover',
preloadSmart: true,
responsive: [
// Desktop — 540px wide, aspect ratio maintained by Cloudinary
{ breakpoint: 1920, frames: makeFrames(540), sourceWidth: 540, sourceHeight: 356, flex: { w: true } },
// Mobile — 375px wide, lighter payload
{ breakpoint: 640, frames: makeFrames(375), sourceWidth: 375, sourceHeight: 248, flex: { w: true } }
],
progressUpdate: () => onProgress(),
responsiveUpdate: () => onResponsive()
});
When you need to update a frame set, upload the new files to Cloudinary with
the same public IDs. In your Cloudinary upload preset, set
overwrite: true — without this, Cloudinary creates a new asset
with a new version string rather than replacing the existing one, and your
old URLs will still serve the old frames.
To update your preset: Cloudinary Dashboard → Settings → Upload →
find your preset → set overwrite: true. From that point
any re-upload replaces the asset at the same URL with no code changes.