canvas-scroll-clip
Version:
Canvas based image sequence scroll animation. Zero dependencies.
396 lines (387 loc) • 12.3 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/helpers/error.ts
var CanvasScrollClipError = class extends Error {
constructor(message) {
if (!message) {
message = `Whoops! Something went wrong.`;
}
super(`${message}`);
Object.setPrototypeOf(this, Object.getPrototypeOf(this));
if (Error.captureStackTrace) {
Error.captureStackTrace(this, CanvasScrollClipError);
}
}
get name() {
return this.constructor.name;
}
};
var AppError = class extends CanvasScrollClipError {
get name() {
return "CanvasScrollClipError";
}
};
var AppLogger = class extends CanvasScrollClipError {
log(message) {
console.log(`${this.name}: ${message}`);
}
warn(message) {
console.warn(`${this.name}: ${message}`);
}
};
var AppWarning = class extends AppLogger {
constructor(message) {
super(message);
this.warn(message);
}
};
// src/helpers/utils.ts
var RegExpLastDigitsMatch = /\d+(?!.*\d+)/;
var AppEvent = {
viewport: {
resize: "viewport.resize",
scroll: "viewport.scroll"
},
images: {
progress: "images.progress",
loaded: "images.loaded"
}
};
var EventList = Object.values(AppEvent).map((e) => Object.values(e)).flat();
var debounce = (func, threshold = 0) => {
let timeout;
const debounced = (...args) => {
if (timeout)
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), threshold);
};
return debounced;
};
function getImage(imageLink) {
return __async(this, null, function* () {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => {
resolve(image);
};
image.src = imageLink;
image.onerror = () => {
reject(new AppError(`Image with name '...${imageLink.slice(-20)}' was not found.`));
};
});
});
}
function preloadImages(frameOptions) {
return new Array(frameOptions.count).fill(0).map((_elem, index) => {
return getImage(getFramePathByIndex(frameOptions, index + 1));
});
}
function getScrollTop() {
return window.pageYOffset || document.documentElement.scrollTop;
}
function getScrollFraction(cViewport, scrollTop = getScrollTop()) {
return (scrollTop - cViewport.top) / (cViewport.bottom - cViewport.screen.height - cViewport.top);
}
function getFramePathByIndex(frameOptions, frameNumber = 1) {
return [
frameOptions.path,
frameOptions.image.start,
frameNumber.toString().padStart(frameOptions.image.padStart, "0"),
frameOptions.image.ending
].join("");
}
function getImageBasePath(firstFramePath) {
const path = firstFramePath.split("/");
path.pop();
return `${path.join("/")}/`;
}
function getImageStructure(firstFramePath, frameCount) {
const img = getPathEnding(firstFramePath);
const ext = getFileSuffix(img);
const seq = getImageSequence(img);
if (frameCount.toString().length > seq.length) {
throw new AppError(`Leading zeros in first frame path has to be more than the frame count and sequence at the end.`);
}
return {
start: img.slice(0, img.indexOf(seq)),
sequence: parseInt(seq),
padStart: seq.length,
ending: img.slice(img.indexOf(seq) + seq.length),
extension: ext
};
}
function getImageSequence(imageName) {
const match = imageName.match(RegExpLastDigitsMatch);
const sequence = match && match[0] !== null ? match[0] : "";
if (sequence.length < 2) {
throw new AppError('Bad image sequence format. Should start with 0 and be longer than 2 numbers, f.e. "frame_01.jpg"');
}
return sequence;
}
function getFileSuffix(fileName) {
const ext = fileName.split(".").pop() || " ";
if (!["jpg", "jpeg", "png", "webp", "avif"].includes(ext)) {
throw new AppError(`Image with extension ['${ext}'] is not supported.`);
}
return `.${ext}`;
}
function getPathEnding(path) {
const splitted = path.split("/");
return splitted.pop() || "";
}
// src/common/events.ts
var EventEmitter = class {
constructor() {
this.on = (event, cb) => {
if (!EventList.includes(event)) {
new AppWarning(`Event ['${event}'] is not supported.`);
}
const observer = this.observers.get(event);
if (observer)
observer.push(cb);
else
this.observers.set(event, [cb]);
};
this.remove = (event, cb) => {
const observer = this.observers.get(event);
if (!observer)
return;
for (let i = 0; i < observer.length; i++) {
if (observer[i] === cb) {
observer.splice(i, 1);
return;
}
}
};
this.emit = (event, ...args) => {
const observer = this.observers.get(event);
if (!observer)
return;
for (let i = 0; i < observer.length; i++) {
observer[i](...args);
}
};
this.observers = /* @__PURE__ */ new Map();
}
};
// src/common/base.ts
var Base = class {
constructor() {
if (!window) {
throw new AppError("window is not found.");
}
if (!document) {
throw new AppError("document is not found.");
}
this.events = new EventEmitter();
this.screen = this.getScreenViewport();
this.bind();
}
bind() {
window.addEventListener("resize", debounce(this.handleResize.bind(this)));
window.addEventListener("scroll", debounce(this.handleScroll.bind(this)));
this.events.on(AppEvent.viewport.resize, (viewport) => {
this.screen = viewport;
});
}
getScreenViewport() {
var _a, _b;
return {
width: window.innerWidth || ((_a = document.documentElement) == null ? void 0 : _a.clientWidth),
height: window.innerHeight || ((_b = document.documentElement) == null ? void 0 : _b.clientHeight)
};
}
handleResize() {
this.events.emit(AppEvent.viewport.resize, this.getScreenViewport());
}
handleScroll() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
this.events.emit(AppEvent.viewport.scroll, scrollTop);
}
};
var base_default = Base;
// src/common/frame.ts
var Frame = class {
constructor({ framePath, frameCount }) {
if (!(framePath == null ? void 0 : framePath.length)) {
throw new AppError("Frame path is not defined.");
}
if (!frameCount) {
throw new AppError("Frame count is not defined.");
}
this.count = parseInt(frameCount);
this.path = getImageBasePath(framePath);
this.image = getImageStructure(framePath, this.count);
}
};
var frame_default = Frame;
// src/common/options.ts
var Options = class extends base_default {
constructor(options) {
super();
this.scrollArea = 0;
this.inputs = options;
this.identifier = options.identifier || "csc";
this.frame = new frame_default(options);
if (options.scrollArea) {
this.scrollArea = parseInt(options.scrollArea, 10);
}
}
set setScrollableArea(height) {
if (this.scrollArea)
return;
this.scrollArea = parseInt(height);
}
};
var options_default = Options;
// src/common/canvas.ts
var Canvas = class extends options_default {
constructor(element, options) {
super(options);
this.loading = true;
this.images = [];
if (!element) {
throw new AppError("HTML element is not defined.");
}
this._container = element;
this._container.innerHTML = "";
this._container.classList.add(`${this.identifier}-container`);
this._container.style.setProperty("height", `${this.scrollArea}px`);
this._canvas = document.createElement("canvas");
this._canvas.classList.add(`${this.identifier}-canvas`);
this._canvas.style.setProperty("display", "block");
this._canvas.style.setProperty("max-height", "100%");
this._canvas.style.setProperty("max-width", "100%");
this._canvas.style.setProperty("object-fit", "cover");
this._canvas.style.setProperty("height", "100vh");
this._canvas.style.setProperty("width", "100%");
this._wrapper = document.createElement("div");
this._wrapper.classList.add(this.identifier);
this._wrapper.style.setProperty("position", "sticky");
this._wrapper.style.setProperty("top", "0");
this._wrapper.appendChild(this._canvas);
this._container.appendChild(this._wrapper);
this.context = this._canvas.getContext("2d");
this.viewport = {
width: this.screen.width,
height: this.screen.height,
top: 0,
bottom: this.screen.height,
screen: this.screen
};
this.events.on(AppEvent.viewport.scroll, (scrollTop) => {
if (!this.loading)
this.drawImageByScrollFraction(getScrollFraction(this.viewport, scrollTop));
});
this.preload();
}
preload() {
return __async(this, null, function* () {
const progress = {
total: this.frame.count,
loaded: 0
};
const imageList = preloadImages(this.frame).map((p) => {
return p.then((image) => {
progress.loaded = ++progress.loaded;
this.events.emit(AppEvent.images.progress, progress);
return image;
});
});
return yield Promise.all(imageList).then((images) => {
var _a, _b;
this.images = images;
this.viewport = __spreadProps(__spreadValues({}, this.viewport), {
width: (_a = this.images[0]) == null ? void 0 : _a.width,
height: (_b = this.images[0]) == null ? void 0 : _b.height,
top: this._container.getBoundingClientRect().top + getScrollTop() || 0,
bottom: this._container.getBoundingClientRect().bottom + getScrollTop() || this.screen.height
});
this._canvas.width = this.viewport.width;
this._canvas.height = this.viewport.height;
this.setScrollableArea = this.viewport.height * 2;
this._container.style.setProperty("height", `${this.scrollArea}px`);
this.drawImageByScrollFraction(getScrollFraction(this.viewport));
this._container.classList.add(`${this.identifier}--loaded`);
this.loading = false;
this.events.emit(AppEvent.images.loaded);
});
});
}
drawImage(image) {
if (!image) {
return;
}
this.context.clearRect(0, 0, this.viewport.width, this.viewport.height);
this.context.drawImage(image, 0, 0, this.viewport.width, this.viewport.height);
}
drawImageByFrameNumber(frameNumber = 0) {
this.drawImage(this.images[frameNumber]);
}
drawImageByScrollFraction(scrollFraction) {
let frameIndex;
frameIndex = Math.min(
this.frame.count - 1,
Math.ceil(scrollFraction * this.frame.count)
);
if (frameIndex <= 0)
frameIndex = 0;
this.drawImageByFrameNumber(frameIndex);
}
};
var canvas_default = Canvas;
// src/main.ts
var Main = class extends canvas_default {
constructor(element, options, callback) {
super(element, options);
this.callback = callback || (() => {
});
this.init();
}
init() {
this.events.on(AppEvent.images.loaded, () => {
debounce(this.callback());
});
}
};
var main_default = Main;
export {
main_default as default
};