Common Errors & Debugging

These are the mistakes that come up most often when setting up ease360°. Most are silent — no console error, just a viewer that looks wrong. Check here first before digging into the source.

First step when anything goes wrong: open the browser console. ease360° logs descriptive errors and warnings directly there — including what went wrong, why, and how to fix it. Many of the issues below produce a specific console message that will point you straight to the problem.

Canvas is black or viewer doesn't initialize

SymptomThe viewer container is empty or black. No teapot, no spinner, nothing interactive. The page loaded but ease360° never started.

Open the browser console (F12 → Console) and look for a message starting with [ease360]. ease360° logs a descriptive error for the most common failure modes — including CORS blocks, missing assets, and dimension detection failures. The message will tell you exactly what failed and how to fix it.

Common console messages and what they mean:

# Probe image loaded but dimensions blocked — CORS issue
[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.
Alternatively, provide sourceWidth and sourceHeight explicitly to skip the probe.

# First frame didn't load at all — URL wrong or asset missing
[ease360] Probe image failed to load: https://res.cloudinary.com/...
Check the URL is correct and the asset exists.

# Deprecation warnings — old API names still work but should be updated
[ease360] The "width" setting is deprecated as of v1.0.1. Please rename it to "sourceWidth".
[ease360] The "dragAxis" setting is deprecated as of v1.0.3. Please rename it to "dragDirection".

If the console is clean and the viewer still doesn't render, check that ease360.css is included — without it the canvas cursor and opacity transitions won't apply. Also confirm the container element exists in the DOM before the script runs.

sourceWidth / sourceHeight — when to use them and why

SymptomImage appears stretched, squashed, or incorrectly cropped — especially noticeable with cover or cover-center modes. Or the viewer doesn't initialize at all and the console shows a CORS or probe error.

sourceWidth and sourceHeight tell ease360° the pixel dimensions of the image that actually arrives in the browser — not the master file. ease360° uses these for all internal canvas math: scaling, aspect ratio, cover positioning, and crop offsets.

These are optional — when omitted, ease360° probes the first frame automatically. But there are two compelling reasons to provide them explicitly:

Speed. The probe adds an async step before init — typically 50–200ms depending on CDN and connection speed. Providing sourceWidth and sourceHeight skips the probe entirely and ease360° initializes synchronously. For a viewer above the fold this matters.

CORS. The probe image loads with crossOrigin: 'anonymous' to read pixel dimensions. If your server doesn't send an Access-Control-Allow-Origin header, naturalWidth returns 0 and the viewer won't start. Providing explicit dimensions skips the probe entirely — CORS headers become irrelevant for initialization.

// ── Omitting sourceWidth/sourceHeight (probe runs automatically) ───────────
// Convenient — ease360° probes frames[0] and reads naturalWidth/naturalHeight.
// Small async delay before init. Requires CORS headers on your CDN.
const myEase360 = ease360('#myEase360', {
    frames:         makeFrames(540),
    backgroundSize: 'cover'
    // sourceWidth and sourceHeight omitted — ease360° probes automatically
});

// ── Providing sourceWidth/sourceHeight (probe skipped) ─────────────────────
// Synchronous init — no probe, no delay, no CORS dependency.
// Use when you know the delivered size and want the fastest possible start.
const myEase360 = ease360('#myEase360', {
    frames:         makeFrames(540),
    sourceWidth:    540,  // matches w_540 in the Cloudinary URL — not the master
    sourceHeight:   356,  // Cloudinary maintains aspect ratio at w_540
    backgroundSize: 'cover'
});

The value must match the delivered size — what the CDN sends at the requested transform — not the master. If you request w_540, set sourceWidth: 540. Setting it to the master's 1080px while delivering 540px will produce incorrect canvas math.

width / height in responsive breakpoints don't match source images

SymptomOne breakpoint looks correct, another is stretched or the subject is clipped unexpectedly.

Each breakpoint object's sourceWidth and sourceHeight must match the intrinsic pixel dimensions of that breakpoint's source images. Different breakpoints can use different source sizes — but each entry must be internally consistent.

// ✗ Wrong — orange teapot source is 1024×1024 but told it's 540×540
{ breakpoint: 1024, frames: orangeTeapot, sourceWidth: 540, sourceHeight: 540, flex: { w: true } }

// ✓ Correct — width/height match the actual source dimensions
{ breakpoint: 1024, frames: orangeTeapot, sourceWidth: 1024, sourceHeight: 1024, flex: { w: true } }

Frame sequence starts at the wrong index

SymptomFirst or last frame is missing, or the spin jumps on the seam between frame 0 and the last frame.

ease360° renders frames in the exact order of the array. A mismatch between your naming convention (zero-based vs one-based) and your array builder produces a broken seam or a missing frame.

// Zero-based: teapot_0.jpg → teapot_35.jpg
const frames = Array.from({ length: 36 }, (_, i) => `teapot_${i}.jpg`);
// → teapot_0.jpg, teapot_1.jpg, ..., teapot_35.jpg  ✓

// One-based: teapot_1.jpg → teapot_36.jpg
const frames = Array.from({ length: 36 }, (_, i) => `teapot_${i + 1}.jpg`);
// → teapot_1.jpg, teapot_2.jpg, ..., teapot_36.jpg  ✓

// ✗ Common mistake — one-based files, zero-based builder:
// Requests teapot_0.jpg (404) and misses teapot_36.jpg

Progress jumps from 50% to 100% with preloadSmart

SymptomLoading percentage jumps straight to 100% after reaching 50%, or the loader disappears before all frames are loaded.

When preloadSmart: true, ease360° loads every other frame first — 50% of the frames. myEase360.progress reports against the full frame count, so it reaches 0.5 (50%) when the smart load is done. Without the multiplier your loader reads 0–50% then jumps.

// ✗ Wrong — progress only ever shows 0–50%
progressUpdate: () => {
    const pct = Math.floor(myEase360.progress * 100);
    // pct reaches 50, not 100, when smart load completes
}

// ✓ Correct — only multiply during the smart load phase
progressUpdate: () => {
    const inSmartPhase = myEase360.preloadSmart &&
                         myEase360.totalLoaded <= myEase360.frames.length / 2;
    const multiplier   = inSmartPhase ? 2 : 1;
    const pct = Math.floor(myEase360.progress * 100) * multiplier;
    // pct correctly reaches 100 at smart load complete, never exceeds it
}

backgroundOffsetY has no effect with cover-center

SymptomSetting backgroundOffsetY moves the image in cover mode but has no effect when backgroundSize is cover-center.

This was a bug fixed in v1.0.4. Prior to that, cover-center calculated its own vertical position and ignored backgroundOffsetY entirely. Update to v1.0.4 or later — offsets are now applied as post-calculations on top of the centering math.

With cover-center, positive backgroundOffsetY shifts the crop up and negative shifts it down — relative to where center naturally lands, not from the top-left origin.

// Requires ease360° v1.0.4+
{ breakpoint: 1024, frames: orangeTeapot, sourceWidth: 1024, sourceHeight: 1024,
  backgroundSize: 'cover-center',
  backgroundOffsetY: 50,  // shifts crop up 50px relative to center
  flex: { w: true } }

Uncaught SyntaxError: Identifier already declared

SymptomConsole error on page load: "Identifier 'X' has already been declared".

This happens when a variable declared with const or let in a script file conflicts with the same name declared elsewhere in the global scope — often a second <script> tag, an inline script, or a copy-paste that duplicated a block.

The error references a line number in your JS file. Check for duplicate const declarations of the same name. If you need the same variable in multiple scripts, declare it once and share it, or wrap each script in an IIFE to give it its own scope.

// ✗ Two scripts both declare const colorLoaded → error
// script-a.js: const colorLoaded = { green: false };
// script-b.js: const colorLoaded = { green: false }; // ✗ already declared

// ✓ Wrap in an IIFE to scope each script independently
((() => {
    const colorLoaded = { green: false };
    // ... rest of script
})();

dragDirection has no effect

SymptomSetting dragDirection doesn't change which axis drives the spin.

The setting was renamed twice across versions. Use dragDirection (v1.0.0+). The legacy names dragAxis and touchdirection (v0.x) still work with a console deprecation warning but will be removed in a future major version.

// ✓ Current (v1.0.3+)
ease360('#viewer', { dragDirection: 'left-right' });

// Still works but deprecated
ease360('#viewer', { dragAxis: 'left-right' });       // v1.0.2 name
ease360('#viewer', { touchdirection: 'left-right' }); // v0.x name

changeFrames() called as changeFrame()

SymptomClicking a color swatch does nothing — no new images load, no loader appears, the viewer stays on the current set.

The method is changeFrames() — plural, with an s. Calling changeFrame() without the s fails silently because it's not defined on the instance. JavaScript won't throw an error, it just does nothing. This is one of the harder bugs to spot because the rest of the UI — swatch highlight, loader state — may still update correctly while the viewer itself doesn't change.

// ✗ Silent failure — changeFrame is not a method
myEase360.changeFrame(variantFrames.blue);

// ✓ Correct — note the s
myEase360.changeFrames(variantFrames.blue);

// ✓ Responsive variant — also plural
myEase360.changeFramesResponsive([frames1920, frames1440, frames1024]);