Genesis G90 — served from Cloudinary

CDN Helper & Cloudinary

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:

Anatomy of a Cloudinary URL

Every 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.

2. Build frame arrays with ease360.cdn.js

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 });

3. sourceWidth and sourceHeight

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.

4. Initialize

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()
});

CORS and the probe

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.

5. Per-breakpoint sizing with the responsive array

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()
});

Re-uploading assets

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.