UNPKG

canvas-scroll-clip

Version:

Canvas based image sequence scroll animation. Zero dependencies.

396 lines (387 loc) 12.3 kB
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 };