/*! * Pintura Image Editor 8.13.1 * (c) 2018-2021 PQINA Inc. - All Rights Reserved * License: https://pqina.nl/pintura/license/ */ /* eslint-disable */ const JFIF_MARKER = 0xffe0; const EXIF_MARKER = 0xffe1; const SOS_MARKER = 0xffda; const Markers = { [EXIF_MARKER]: 'exif', [JFIF_MARKER]: 'jfif', [SOS_MARKER]: 'sos', }; const JPEG_SOI_MARKER = 0xffd8; // start of JPEG const JPEG_MARKER_PREFIX = 0xff; var dataViewGetApplicationMarkers = (view) => { // If no SOI marker exit here because we're not going to find the APP1 header in a non-jpeg file if (view.getUint16(0) !== JPEG_SOI_MARKER) return undefined; const markerTypes = Object.keys(Markers).map((v) => parseInt(v, 10)); const length = view.byteLength; // cache the length here let offset = 2; // start at 2 as we skip the SOI marker let marker; // this will hold the current marker // resulting markers let res = undefined; while (offset < length) { // test if marker is valid JPEG marker (starts with ff) if (view.getUint8(offset) !== JPEG_MARKER_PREFIX) break; // let's read the full marker marker = view.getUint16(offset); // read marker if included in marker types, don't if (markerTypes.includes(marker)) { const key = Markers[marker]; if (!res) res = {}; // prevent overwriting by double markers if (!res[key]) { res[key] = { offset, size: view.getUint16(offset + 2), }; } } // Image stream starts here, no markers found if (marker === SOS_MARKER) break; // next offset is 2 to skip over marker type and then we add marker data size to skip to next marker offset += 2 + view.getUint16(offset + 2); } // no APP markers found return res; }; const APP1_MARKER = 0xffe1; const APP1_EXIF_IDENTIFIER = 0x45786966; const TIFF_MARKER = 0x002a; const BYTE_ALIGN_MOTOROLA = 0x4d4d; const BYTE_ALIGN_INTEL = 0x4949; // offset = start of APP1_MARKER var dataViewGetExifTags = (view, offset) => { // If no APP1 marker exit here because we're not going to find the EXIF id header outside of APP1 if (view.getUint16(offset) !== APP1_MARKER) return undefined; // get marker size const size = view.getUint16(offset + 2); // 14197 // Let's skip over app1 marker and size marker (2 + 2 bytes) offset += 4; // We're now at the EXIF header marker (we'll only check the first 4 bytes, reads "exif"), if not there, exit if (view.getUint32(offset) !== APP1_EXIF_IDENTIFIER) return undefined; // Let's skip over 6 byte EXIF marker offset += 6; // Read byte alignment let byteAlignment = view.getUint16(offset); if (byteAlignment !== BYTE_ALIGN_INTEL && byteAlignment !== BYTE_ALIGN_MOTOROLA) return undefined; const storedAsLittleEndian = byteAlignment === BYTE_ALIGN_INTEL; // Skip over byte alignment offset += 2; // Test if valid tiff marker data, should always be 0x002a if (view.getUint16(offset, storedAsLittleEndian) !== TIFF_MARKER) return undefined; // Skip to first IDF, position of IDF is read after tiff marker (offset 2) offset += view.getUint32(offset + 2, storedAsLittleEndian); // helper method to find tag offset by marker const getTagOffsets = (marker) => { let offsets = []; let i = offset; let max = offset + size - 16; for (; i < max; i += 12) { let tagOffset = i; // see if is match, if not, next entry if (view.getUint16(tagOffset, storedAsLittleEndian) !== marker) continue; // add offset offsets.push(tagOffset); } return offsets; }; return { read: (address) => { const tagOffsets = getTagOffsets(address); if (!tagOffsets.length) return undefined; // only return first found tag return view.getUint16(tagOffsets[0] + 8, storedAsLittleEndian); }, write: (address, value) => { const tagOffsets = getTagOffsets(address); if (!tagOffsets.length) return false; // overwrite all found tags (sometimes images can have multiple tags with the same value, let's make sure they're all set) tagOffsets.forEach((offset) => view.setUint16(offset + 8, value, storedAsLittleEndian)); return true; }, }; }; const ORIENTATION_TAG = 0x0112; var arrayBufferImageExif = (data, key, value) => { // no data, no go! if (!data) return; const view = new DataView(data); // Get app1 header offset const markers = dataViewGetApplicationMarkers(view); if (!markers || !markers.exif) return; // Get EXIF tags read/writer const tags = dataViewGetExifTags(view, markers.exif.offset); if (!tags) return; // Read the exif orientation marker return value === undefined ? tags.read(key) : tags.write(key, value); }; const backup = '__pqina_webapi__'; var getNativeAPIRef = (API) => (window[backup] ? window[backup][API] : window[API]); var noop$1 = (...args) => { }; const FileReaderDataFormat = { ArrayBuffer: 'readAsArrayBuffer', }; var readFile = (file, onprogress = noop$1, options = {}) => new Promise((resolve, reject) => { const { dataFormat = FileReaderDataFormat.ArrayBuffer } = options; const reader = new (getNativeAPIRef('FileReader'))(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.onprogress = onprogress; reader[dataFormat](file); }); var blobReadSection = async (blob, slice = [0, blob.size], onprogress) => (await readFile(blob.slice(...slice), onprogress)); var getImageOrientationFromFile = async (file, onprogress) => { const head = await blobReadSection(file, [0, 64 * 1024], onprogress); return arrayBufferImageExif(head, ORIENTATION_TAG) || 1; }; let result$a = null; var isBrowser = () => { if (result$a === null) result$a = typeof window !== 'undefined' && typeof window.document !== 'undefined'; return result$a; }; let result$9 = null; var canOrientImages = () => new Promise((resolve) => { if (result$9 === null) { // 2x1 pixel image 90CW rotated with orientation EXIF header const testSrc = 'data:image/jpg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QA6RXhpZgAATU0AKgAAAAgAAwESAAMAAAABAAYAAAEoAAMAAAABAAIAAAITAAMAAAABAAEAAAAAAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wAALCAABAAIBASIA/8QAJgABAAAAAAAAAAAAAAAAAAAAAxABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAAPwBH/9k='; let testImage = isBrowser() ? new Image() : {}; testImage.onload = () => { // should correct orientation if is presented in landscape, // in which case the browser doesn't autocorrect result$9 = testImage.naturalWidth === 1; testImage = undefined; resolve(result$9); }; testImage.src = testSrc; return; } return resolve(result$9); }); var canvasToImageData = (canvas) => { const imageData = canvas .getContext('2d') .getImageData(0, 0, canvas.width, canvas.height); return imageData; }; var h = (name, attributes, children = []) => { const el = document.createElement(name); // @ts-ignore const descriptors = Object.getOwnPropertyDescriptors(el.__proto__); for (const key in attributes) { if (key === 'style') { el.style.cssText = attributes[key]; } else if ((descriptors[key] && descriptors[key].set) || /textContent|innerHTML/.test(key) || typeof attributes[key] === 'function') { el[key] = attributes[key]; } else { el.setAttribute(key, attributes[key]); } } children.forEach((child) => el.appendChild(child)); return el; }; const MATRICES = { 1: () => [1, 0, 0, 1, 0, 0], 2: (width) => [-1, 0, 0, 1, width, 0], 3: (width, height) => [-1, 0, 0, -1, width, height], 4: (width, height) => [1, 0, 0, -1, 0, height], 5: () => [0, 1, 1, 0, 0, 0], 6: (width, height) => [0, 1, -1, 0, height, 0], 7: (width, height) => [0, -1, -1, 0, height, width], 8: (width) => [0, -1, 1, 0, 0, width], }; var getImageOrientationMatrix = (width, height, orientation = -1) => { if (orientation === -1) orientation = 1; return MATRICES[orientation](width, height); }; var releaseCanvas = (canvas) => { canvas.width = 1; canvas.height = 1; const ctx = canvas.getContext('2d'); ctx && ctx.clearRect(0, 0, 1, 1); }; var isImageData = (obj) => 'data' in obj; var imageDataToCanvas = async (imageData, orientation = 1) => { const [width, height] = (await canOrientImages()) || orientation < 5 ? [imageData.width, imageData.height] : [imageData.height, imageData.width]; const canvas = h('canvas', { width, height }); const ctx = canvas.getContext('2d'); // transform image data ojects into in memory canvas elements so we can transform them (putImageData isn't affect by transforms) if (isImageData(imageData) && !(await canOrientImages()) && orientation > 1) { const inMemoryCanvas = h('canvas', { width: imageData.width, height: imageData.height, }); const ctx = inMemoryCanvas.getContext('2d'); ctx.putImageData(imageData, 0, 0); imageData = inMemoryCanvas; } // get base transformation matrix if (!(await canOrientImages()) && orientation > 1) { ctx.transform.apply(ctx, getImageOrientationMatrix(imageData.width, imageData.height, orientation)); } // can't test for instanceof ImageBitmap as Safari doesn't support it // if still imageData object by this point, we'll use put if (isImageData(imageData)) { ctx.putImageData(imageData, 0, 0); } else { ctx.drawImage(imageData, 0, 0); } // if image data is of type canvas, clean it up if (imageData instanceof HTMLCanvasElement) releaseCanvas(imageData); return canvas; }; var orientImageData = async (imageData, orientation = 1) => { if (orientation === 1) return imageData; // correct image data for when the browser does not correctly read exif orientation headers if (!(await canOrientImages())) return canvasToImageData(await imageDataToCanvas(imageData, orientation)); return imageData; }; var isObject = (v) => typeof v === 'object'; const copy = (val) => (isObject(val) ? deepCopy(val) : val); const deepCopy = (src) => { let dst; if (Array.isArray(src)) { dst = []; src.forEach((val, i) => { dst[i] = copy(val); }); } else { dst = {}; Object.keys(src).forEach((key) => { const val = src[key]; dst[key] = copy(val); }); } return dst; }; var isString = (v) => typeof v === 'string'; var imageToCanvas = (image, canvasMemoryLimit) => { // if these are 0 it's possible that we're trying to convert an SVG that doesn't have width or height attributes // https://bugzilla.mozilla.org/show_bug.cgi?id=1328124 let canvasWidth = image.naturalWidth; let canvasHeight = image.naturalHeight; // determine if requires more memory than limit, if so limit target size const requiredCanvasMemory = canvasWidth * canvasHeight; if (canvasMemoryLimit && requiredCanvasMemory > canvasMemoryLimit) { const canvasScalar = Math.sqrt(canvasMemoryLimit) / Math.sqrt(requiredCanvasMemory); canvasWidth = Math.floor(canvasWidth * canvasScalar); canvasHeight = Math.floor(canvasHeight * canvasScalar); } // create new canvas element const canvas = h('canvas'); canvas.width = canvasWidth; canvas.height = canvasHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(image, 0, 0, canvasWidth, canvasHeight); return canvas; }; // turns image into canvas only after it's fully loaded var imageToCanvasSafe = (image, canvasMemoryLimit) => new Promise((resolve, reject) => { const ready = () => resolve(imageToCanvas(image, canvasMemoryLimit)); if (image.complete && image.width) { // need to test for image.width, on ie11 it will be 0 for object urls ready(); } else { image.onload = ready; image.onerror = reject; } }); var blobToCanvas = async (imageBlob, canvasMemoryLimit) => { const imageElement = h('img', { src: URL.createObjectURL(imageBlob) }); const canvas = await imageToCanvasSafe(imageElement, canvasMemoryLimit); URL.revokeObjectURL(imageElement.src); return canvas; }; var canCreateImageBitmap = () => 'createImageBitmap' in window; var canCreateOffscreenCanvas = () => 'OffscreenCanvas' in window; var isSVGFile = (blob) => /svg/.test(blob.type); var getUniqueId = () => Math.random() .toString(36) .substr(2, 9); var functionToBlob = (fn) => new Blob(['(', typeof fn === 'function' ? fn.toString() : fn, ')()'], { type: 'application/javascript', }); const wrapFunction = (fn) => `function () {self.onmessage = function (message) {(${fn.toString()}).apply(null, message.data.content.concat([function (err, response) { response = response || {}; const transfer = 'data' in response ? [response.data.buffer] : 'width' in response ? [response] : []; return self.postMessage({ id: message.data.id, content: response, error: err }, transfer); }]))}}`; const workerPool = new Map(); var thread = (fn, args, transferList) => new Promise((resolve, reject) => { let workerKey = fn.toString(); let pooledWorker = workerPool.get(workerKey); if (!pooledWorker) { // create worker for this function const workerFn = wrapFunction(fn); // create a new web worker const url = URL.createObjectURL(functionToBlob(workerFn)); const messages = new Map(); const worker = new Worker(url); // create a pooled worker, this object will contain the worker and active messages pooledWorker = { url, worker, messages, terminate: () => { pooledWorker.worker.terminate(); URL.revokeObjectURL(url); }, }; // handle received messages worker.onmessage = function (e) { // should receive message id and message const { id, content, error } = e.data; // message route no longer valid if (!messages.has(id)) return; // get related thread and resolve with returned content const message = messages.get(id); // remove thread from threads cache messages.delete(id); // resolve or reject message based on response from worker error != null ? message.reject(error) : message.resolve(content); }; // pool this worker workerPool.set(workerKey, pooledWorker); } // we need a way to remember this message so we generate a unique id and use that as a key for this request, that way we can link the response back to request in the pooledWorker.onmessage handler const messageId = getUniqueId(); pooledWorker.messages.set(messageId, { resolve, reject }); // use pooled worker and await response pooledWorker.worker.postMessage({ id: messageId, content: args }, transferList); }); var blobToImageData = async (imageBlob, canvasMemoryLimit) => { let imageData; // if can use OffscreenCanvas let's go for it as it will mean we can run this operation on a separate thread if (canCreateImageBitmap() && !isSVGFile(imageBlob) && canCreateOffscreenCanvas()) { try { imageData = await thread((file, canvasMemoryLimit, done) => { createImageBitmap(file) .then((bitmap) => { let canvasWidth = bitmap.width; let canvasHeight = bitmap.height; // determine if requires more memory than limit, if so limit target size const requiredCanvasMemory = canvasWidth * canvasHeight; if (canvasMemoryLimit && requiredCanvasMemory > canvasMemoryLimit) { const canvasScalar = Math.sqrt(canvasMemoryLimit) / Math.sqrt(requiredCanvasMemory); canvasWidth = Math.floor(canvasWidth * canvasScalar); canvasHeight = Math.floor(canvasHeight * canvasScalar); } const canvas = new OffscreenCanvas(canvasWidth, canvasHeight); const ctx = canvas.getContext('2d'); ctx.drawImage(bitmap, 0, 0, canvasWidth, canvasHeight); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); done(null, imageData); }) .catch((err) => { // fail silently done(err); }); }, [imageBlob, canvasMemoryLimit]); } catch (err) { // fails silently on purpose, we'll try to turn the blob into image data in the main thread // console.error(err); } } // use main thread to generate ImageData if (!imageData || !imageData.width) { const canvas = await blobToCanvas(imageBlob, canvasMemoryLimit); imageData = canvasToImageData(canvas); releaseCanvas(canvas); } return imageData; }; var canvasToBlob = (canvas, mimeType = undefined, quality = undefined) => new Promise((resolve, reject) => { try { canvas.toBlob((blob) => { resolve(blob); }, mimeType, quality); } catch (err) { reject(err); } }); var imageDataToBlob = async (imageData, mimeType, quality) => { const canvas = await imageDataToCanvas(imageData); const blob = await canvasToBlob(canvas, mimeType, quality); releaseCanvas(canvas); return blob; }; var blobWriteSection = (blob, section, slice = [0, blob.size]) => { if (!section) return blob; return new Blob([section, blob.slice(...slice)], { type: blob.type }); }; var getExtensionFromMimeType = (mimeType) => (mimeType.match(/\/([a-z]+)/) || [])[1]; var getFilenameWithoutExtension = (name) => name.substr(0, name.lastIndexOf('.')) || name; var getExtensionFromFilename = (filename) => filename.split('.').pop(); const ImageExtensionsRegex = /avif|bmp|gif|jpg|jpeg|jpe|jif|jfif|png|svg|tiff|webp/; /* Support image mime types - image/webp - image/gif - image/avif - image/jpeg - image/png - image/bmp - image/svg+xml */ var getMimeTypeFromExtension = (ext) => { // empty string returned if extension not found if (!ImageExtensionsRegex.test(ext)) return ''; // return MimeType for this extension return 'image/' + (/jfif|jif|jpe|jpg/.test(ext) ? 'jpeg' : ext === 'svg' ? 'svg+xml' : ext); }; var getMimeTypeFromFilename = (name) => name && getMimeTypeFromExtension(getExtensionFromFilename(name).toLowerCase()); var matchFilenameToMimeType = (filename, mimeType) => { // get the mime type that matches this extension const fileMimeType = getMimeTypeFromFilename(filename); // test if type already matches current mime type, no need to change name if (fileMimeType === mimeType) return filename; // get the extension for this mimetype (gets all characters after the "image/" part) // if mimeType doesn't yield an extension, use the fileMimeType const targetMimeTypeExtension = getExtensionFromMimeType(mimeType) || fileMimeType; return `${getFilenameWithoutExtension(filename)}.${targetMimeTypeExtension}`; }; var blobToFile = (blob, filename, mimetype) => { const lastModified = new Date().getTime(); const blobHasMimeType = blob.type.length && !/null|text/.test(blob.type); const blobMimeType = blobHasMimeType ? blob.type : mimetype; const name = matchFilenameToMimeType(filename, blobMimeType); try { return new (getNativeAPIRef('File'))([blob], name, { lastModified, type: blobHasMimeType ? blob.type : blobMimeType, }); } catch (err) { const file = blobHasMimeType ? blob.slice() : blob.slice(0, blob.size, blobMimeType); file.lastModified = lastModified; file.name = name; return file; } }; var getAspectRatio = (w, h) => w / h; var passthrough = (v) => (v); const PI = Math.PI; const HALF_PI = Math.PI / 2; const QUART_PI = HALF_PI / 2; var isRotatedSideways = (a) => { const rotationLimited = Math.abs(a) % Math.PI; return rotationLimited > QUART_PI && rotationLimited < Math.PI - QUART_PI; }; // // generic // const scale = (value, scalar, pivot) => pivot + (value - pivot) * scalar; const ellipseCreateFromRect = (rect) => ({ x: rect.x + rect.width * 0.5, y: rect.y + rect.height * 0.5, rx: rect.width * 0.5, ry: rect.height * 0.5, }); // // vector // const vectorCreateEmpty = () => vectorCreate(0, 0); const vectorCreate = (x, y) => ({ x, y }); const vectorCreateFromSize = (size) => vectorCreate(size.width, size.height); const vectorCreateFromPointerEvent = (e) => vectorCreate(e.pageX, e.pageY); const vectorCreateFromPointerEventOffset = (e) => vectorCreate(e.offsetX, e.offsetY); const vectorClone = (v) => vectorCreate(v.x, v.y); const vectorInvert = (v) => { v.x = -v.x; v.y = -v.y; return v; }; const vectorPerpendicular = (v) => { const x = v.x; v.x = -v.y; v.y = x; return v; }; const vectorRotate = (v, radians, pivot = vectorCreateEmpty()) => { const cos = Math.cos(radians); const sin = Math.sin(radians); const tx = v.x - pivot.x; const ty = v.y - pivot.y; v.x = pivot.x + cos * tx - sin * ty; v.y = pivot.y + sin * tx + cos * ty; return v; }; const vectorLength = (v) => Math.sqrt(v.x * v.x + v.y * v.y); const vectorNormalize = (v) => { const length = Math.sqrt(v.x * v.x + v.y * v.y); if (length === 0) return vectorCreateEmpty(); v.x /= length; v.y /= length; return v; }; const vectorAngle = (v) => Math.atan2(v.y, v.x); const vectorAngleBetween = (a, b) => Math.atan2(b.y - a.y, b.x - a.x); const vectorEqual = (a, b) => a.x === b.x && a.y === b.y; const vectorApply = (v, fn) => { v.x = fn(v.x); v.y = fn(v.y); return v; }; const vectorAdd = (a, b) => { a.x += b.x; a.y += b.y; return a; }; const vectorSubtract = (a, b) => { a.x -= b.x; a.y -= b.y; return a; }; const vectorMultiply = (v, f) => { v.x *= f; v.y *= f; return v; }; const vectorDot = (a, b) => a.x * b.x + a.y * b.y; const vectorDistanceSquared = (a, b = vectorCreateEmpty()) => { const x = a.x - b.x; const y = a.y - b.y; return x * x + y * y; }; const vectorDistance = (a, b = vectorCreateEmpty()) => Math.sqrt(vectorDistanceSquared(a, b)); const vectorCenter = (v) => { let x = 0; let y = 0; v.forEach((v) => { x += v.x; y += v.y; }); return vectorCreate(x / v.length, y / v.length); }; const vectorsFlip = (points, flipX, flipY, cx, cy) => { points.forEach((point) => { point.x = flipX ? cx - (point.x - cx) : point.x; point.y = flipY ? cy - (point.y - cy) : point.y; }); return points; }; const vectorsRotate = (points, angle, cx, cy) => { const s = Math.sin(angle); const c = Math.cos(angle); points.forEach((p) => { p.x -= cx; p.y -= cy; const rx = p.x * c - p.y * s; const ry = p.x * s + p.y * c; p.x = cx + rx; p.y = cy + ry; }); return points; }; // // size // const toSize = (width, height) => ({ width, height }); const sizeClone = (size) => toSize(size.width, size.height); const sizeCreateFromAny = (obj) => toSize(obj.width, obj.height); const sizeCreateFromRect = (r) => toSize(r.width, r.height); const sizeCreateFromArray = (a) => toSize(a[0], a[1]); const sizeCreateFromImageNaturalSize = (image) => toSize(image.naturalWidth, image.naturalHeight); const sizeCreateFromElement = (element) => { if (/img/i.test(element.nodeName)) { return sizeCreateFromImageNaturalSize(element); } return sizeCreateFromAny(element); }; const sizeCreate = (width, height) => toSize(width, height); const sizeEqual = (a, b, format = passthrough) => format(a.width) === format(b.width) && format(a.height) === format(b.height); const sizeScale = (size, scalar) => { size.width *= scalar; size.height *= scalar; return size; }; const sizeCenter = (size) => vectorCreate(size.width * 0.5, size.height * 0.5); const sizeRotate = (size, radians) => { const r = Math.abs(radians); const cos = Math.cos(r); const sin = Math.sin(r); const w = cos * size.width + sin * size.height; const h = sin * size.width + cos * size.height; size.width = w; size.height = h; return size; }; const sizeTurn = (size, radians) => { const w = size.width; const h = size.height; if (isRotatedSideways(radians)) { size.width = h; size.height = w; } return size; }; const sizeContains = (a, b) => a.width >= b.width && a.height >= b.height; const sizeApply = (size, fn) => { size.width = fn(size.width); size.height = fn(size.height); return size; }; const sizeHypotenuse = (size) => Math.sqrt(size.width * size.width + size.height * size.height); const sizeMin = (a, b) => sizeCreate(Math.min(a.width, b.width), Math.min(a.height, b.height)); // // line // const lineCreate = (start, end) => ({ start, end }); const lineClone = (line) => lineCreate(vectorClone(line.start), vectorClone(line.end)); const lineExtend = (line, amount) => { if (amount === 0) return line; const v = vectorCreate(line.start.x - line.end.x, line.start.y - line.end.y); const n = vectorNormalize(v); const m = vectorMultiply(n, amount); line.start.x += m.x; line.start.y += m.y; line.end.x -= m.x; line.end.y -= m.y; return line; }; const lineMultiply = (line, amount) => { if (amount === 0) return line; const v = vectorCreate(line.start.x - line.end.x, line.start.y - line.end.y); const n = vectorNormalize(v); const m = vectorMultiply(n, amount); line.end.x += m.x; line.end.y += m.y; return line; }; const lineExtrude = ({ start, end }, amount) => { if (amount === 0) return [ vectorCreate(start.x, start.y), vectorCreate(start.x, start.y), vectorCreate(end.x, end.y), vectorCreate(end.x, end.y), ]; const a = Math.atan2(end.y - start.y, end.x - start.x); const sina = Math.sin(a) * amount; const cosa = Math.cos(a) * amount; return [ vectorCreate(sina + start.x, -cosa + start.y), vectorCreate(-sina + start.x, cosa + start.y), vectorCreate(-sina + end.x, cosa + end.y), vectorCreate(sina + end.x, -cosa + end.y), ]; }; // // rect // const CornerSigns = [ vectorCreate(-1, -1), vectorCreate(-1, 1), vectorCreate(1, 1), vectorCreate(1, -1), ]; const toRect = (x, y, width, height) => ({ x, y, width, height, }); const rectClone = (rect) => toRect(rect.x, rect.y, rect.width, rect.height); const rectCreateEmpty = () => toRect(0, 0, 0, 0); const rectCreateFromDimensions = (width, height) => toRect(0, 0, width, height); const rectCreateFromSize = (size) => toRect(0, 0, size.width, size.height); const rectCreateFromAny = (obj) => toRect(obj.x || 0, obj.y || 0, obj.width || 0, obj.height || 0); const rectCreateFromPoints = (...args) => { const pts = Array.isArray(args[0]) ? args[0] : args; let xMin = pts[0].x; let xMax = pts[0].x; let yMin = pts[0].y; let yMax = pts[0].y; pts.forEach((point) => { xMin = Math.min(xMin, point.x); xMax = Math.max(xMax, point.x); yMin = Math.min(yMin, point.y); yMax = Math.max(yMax, point.y); }); return toRect(xMin, yMin, xMax - xMin, yMax - yMin); }; const rectCreateFromEllipse = (ellipse) => rectCreate(ellipse.x - ellipse.rx, ellipse.y - ellipse.ry, ellipse.rx * 2, ellipse.ry * 2); const rectCreateWithCenter = (center, size) => toRect(center.x - size.width * 0.5, center.y - size.height * 0.5, size.width, size.height); const rectCreate = (x, y, width, height) => toRect(x, y, width, height); const rectCenter = (rect) => vectorCreate(rect.x + rect.width * 0.5, rect.y + rect.height * 0.5); const rectTranslate = (rect, t) => { rect.x += t.x; rect.y += t.y; return rect; }; const rectScale = (rect, scalar, pivot) => { pivot = pivot || rectCenter(rect); rect.x = scalar * (rect.x - pivot.x) + pivot.x; rect.y = scalar * (rect.y - pivot.y) + pivot.y; rect.width = scalar * rect.width; rect.height = scalar * rect.height; return rect; }; const rectMultiply = (rect, factor) => { rect.x *= factor; rect.y *= factor; rect.width *= factor; rect.height *= factor; return rect; }; const rectDivide = (rect, factor) => { rect.x /= factor; rect.y /= factor; rect.width /= factor; rect.height /= factor; return rect; }; const rectSubtract = (a, b) => { a.x -= b.x; a.y -= b.y; a.width -= b.width; a.height -= b.height; return a; }; const rectAdd = (a, b) => { a.x += b.x; a.y += b.y; a.width += b.width; a.height += b.height; return a; }; const rectEqual = (a, b, format = passthrough) => format(a.x) === format(b.x) && format(a.y) === format(b.y) && format(a.width) === format(b.width) && format(a.height) === format(b.height); const rectAspectRatio = (rect) => getAspectRatio(rect.width, rect.height); const rectUpdate = (rect, x, y, width, height) => { rect.x = x; rect.y = y; rect.width = width; rect.height = height; return rect; }; const rectUpdateWithRect = (a, b) => { a.x = b.x; a.y = b.y; a.width = b.width; a.height = b.height; return a; }; const rectRotate = (rect, radians, pivot) => { if (!pivot) pivot = rectCenter(rect); return rectGetCorners(rect).map((vertex) => vectorRotate(vertex, radians, pivot)); }; const rectCenterRect = (a, b) => toRect(a.width * 0.5 - b.width * 0.5, a.height * 0.5 - b.height * 0.5, b.width, b.height); const rectContainsPoint = (rect, point) => { if (point.x < rect.x) return false; if (point.y < rect.y) return false; if (point.x > rect.x + rect.width) return false; if (point.y > rect.y + rect.height) return false; return true; }; const rectCoverRect = (rect, aspectRatio, offset = vectorCreateEmpty()) => { if (rect.width === 0 || rect.height === 0) return rectCreateEmpty(); const inputAspectRatio = rectAspectRatio(rect); if (!aspectRatio) aspectRatio = inputAspectRatio; let width = rect.width; let height = rect.height; if (aspectRatio > inputAspectRatio) { // height remains the same, width is expanded width = height * aspectRatio; } else { // width remains the same, height is expanded height = width / aspectRatio; } return toRect(offset.x + (rect.width - width) * 0.5, offset.y + (rect.height - height) * 0.5, width, height); }; const rectContainRect = (rect, aspectRatio = rectAspectRatio(rect), offset = vectorCreateEmpty()) => { if (rect.width === 0 || rect.height === 0) return rectCreateEmpty(); let width = rect.width; let height = width / aspectRatio; if (height > rect.height) { height = rect.height; width = height * aspectRatio; } return toRect(offset.x + (rect.width - width) * 0.5, offset.y + (rect.height - height) * 0.5, width, height); }; const rectToBounds = (rect) => [ Math.min(rect.y, rect.y + rect.height), Math.max(rect.x, rect.x + rect.width), Math.max(rect.y, rect.y + rect.height), Math.min(rect.x, rect.x + rect.width), ]; const rectGetCorners = (rect) => [ vectorCreate(rect.x, rect.y), vectorCreate(rect.x + rect.width, rect.y), vectorCreate(rect.x + rect.width, rect.y + rect.height), vectorCreate(rect.x, rect.y + rect.height), ]; const rectApply = (rect, fn) => { if (!rect) return; rect.x = fn(rect.x); rect.y = fn(rect.y); rect.width = fn(rect.width); rect.height = fn(rect.height); return rect; }; const rectApplyPerspective = (rect, perspective, pivot = rectCenter(rect)) => rectGetCorners(rect).map((corner, index) => { const sign = CornerSigns[index]; return vectorCreate(scale(corner.x, 1.0 + sign.x * perspective.x, pivot.x), scale(corner.y, 1.0 + sign.y * perspective.y, pivot.y)); }); const rectNormalizeOffset = (rect) => { rect.x = 0; rect.y = 0; return rect; }; const convexPolyCentroid = (vertices) => { const first = vertices[0]; const last = vertices[vertices.length - 1]; // make sure is closed loop vertices = vectorEqual(first, last) ? vertices : [...vertices, first]; let twiceArea = 0; let i = 0; let x = 0; let y = 0; let fx = first.x; let fy = first.y; let a; let b; let f; const l = vertices.length; for (; i < l; i++) { // current vertex a = vertices[i]; // next vertex b = vertices[i + 1 > l - 1 ? 0 : i + 1]; f = (a.y - fy) * (b.x - fx) - (b.y - fy) * (a.x - fx); twiceArea += f; x += (a.x + b.x - 2 * fx) * f; y += (a.y + b.y - 2 * fy) * f; } f = twiceArea * 3; return vectorCreate(fx + x / f, fy + y / f); }; const lineLineIntersection = (a, b) => getLineLineIntersectionPoint(a.start, a.end, b.start, b.end); const getLineLineIntersectionPoint = (a, b, c, d) => { const denominator = (d.y - c.y) * (b.x - a.x) - (d.x - c.x) * (b.y - a.y); // lines are parallel if (denominator === 0) return undefined; const uA = ((d.x - c.x) * (a.y - c.y) - (d.y - c.y) * (a.x - c.x)) / denominator; const uB = ((b.x - a.x) * (a.y - c.y) - (b.y - a.y) * (a.x - c.x)) / denominator; // intersection is not on the line itself if (uA < 0 || uA > 1 || uB < 0 || uB > 1) return undefined; // return intersection point return vectorCreate(a.x + uA * (b.x - a.x), a.y + uA * (b.y - a.y)); }; // checks if line intersects with one of the lines that can be drawn between the points (in sequence) const linePointsIntersection = (line, points) => { const l = points.length; const intersections = []; for (let i = 0; i < l - 1; i++) { const intersection = getLineLineIntersectionPoint(line.start, line.end, points[i], points[i + 1]); if (!intersection) continue; intersections.push(intersection); } return intersections.length ? intersections : undefined; }; // tests if a point is located in a convex polygon const pointInPoly = (point, vertices) => { let i; let a; let b; let aX; let aY; let bX; let bY; let edgeX; let edgeY; let d; const l = vertices.length; for (i = 0; i < l; i++) { // current vertex a = vertices[i]; // next vertex b = vertices[i + 1 > l - 1 ? 0 : i + 1]; // translate so that point is the origin of the calculation aX = a.x - point.x; aY = a.y - point.y; bX = b.x - point.x; bY = b.y - point.y; edgeX = aX - bX; edgeY = aY - bY; d = edgeX * aY - edgeY * aX; // 0 is ON the edge, but we check for -0.00001 to fix floating point errors if (d < -0.00001) return false; } return true; }; // first tests if points of a are to be found in b, then does the reverse const polyIntersectsWithPoly = (a, b) => !!(a.find((point) => pointInPoly(point, b)) || b.find((point) => pointInPoly(point, a))); const quadLines = (vertices) => { const arr = []; for (let i = 0; i < vertices.length; i++) { let next = i + 1; if (next === vertices.length) next = 0; arr.push(lineCreate(vectorClone(vertices[i]), vectorClone(vertices[next]))); } return arr; }; const ellipseToPolygon = (center, rx, ry, rotation = 0, flipX = false, flipY = false, resolution = 12) => { const points = []; for (let i = 0; i < resolution; i++) { points.push(vectorCreate(center.x + rx * Math.cos((i * (Math.PI * 2)) / resolution), center.y + ry * Math.sin((i * (Math.PI * 2)) / resolution))); } if (flipX || flipY) vectorsFlip(points, flipX, flipY, center.x, center.y); if (rotation) vectorsRotate(points, rotation, center.x, center.y); return points; }; var getImageTransformedRect = (imageSize, imageRotation) => { const imageRect = rectCreateFromSize(imageSize); const imageCenter = rectCenter(imageRect); const imageTransformedVertices = rectRotate(imageRect, imageRotation, imageCenter); return rectNormalizeOffset(rectCreateFromPoints(imageTransformedVertices)); }; var isElement = (v, name) => v instanceof HTMLElement && (name ? new RegExp(`^${name}$`, 'i').test(v.nodeName) : true); var isFile = (v) => v instanceof File; var canvasToFile = async (canvas, mimeType, quality) => { const blob = await canvasToBlob(canvas, mimeType, quality); return blobToFile(blob, 'canvas'); }; var getFilenameFromURL = (url) => url .split('/') .pop() .split(/\?|\#/) .shift(); let isSafari = null; var isSafari$1 = () => { if (isSafari === null) isSafari = isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); return isSafari; }; var getImageElementSize = (imageElement) => new Promise((resolve, reject) => { let shouldAutoRemove = false; // test if image is attached to DOM, if not attached, attach so measurement is correct on Safari if (!imageElement.parentNode && isSafari$1()) { shouldAutoRemove = true; // has width 0 and height 0 to prevent rendering very big SVGs (without width and height) that will for one frame overflow the window and show a scrollbar imageElement.style.cssText = `position:absolute;visibility:hidden;pointer-events:none;left:0;top:0;width:0;height:0;`; document.body.appendChild(imageElement); } // start testing size const measure = () => { const width = imageElement.naturalWidth; const height = imageElement.naturalHeight; const hasSize = width && height; if (!hasSize) return; // clean up image if was attached for measuring if (shouldAutoRemove) imageElement.parentNode.removeChild(imageElement); clearInterval(intervalId); resolve({ width, height }); }; imageElement.onerror = (err) => { clearInterval(intervalId); reject(err); }; const intervalId = setInterval(measure, 1); measure(); }); var getImageSize = async (image) => { // the image element we'll use to load the image let imageElement = image; // if is not an image element, it must be a valid image source if (!imageElement.src) { imageElement = new Image(); imageElement.src = isString(image) ? image : URL.createObjectURL(image); } let size; try { size = await getImageElementSize(imageElement); } finally { isFile(image) && URL.revokeObjectURL(imageElement.src); } return size; }; const awaitComplete = (image) => new Promise((resolve, reject) => { if (image.complete) return resolve(image); image.onload = () => resolve(image); image.onerror = reject; }); var imageToFile = async (imageElement) => { try { const size = await getImageSize(imageElement); const image = await awaitComplete(imageElement); const canvas = document.createElement('canvas'); canvas.width = size.width; canvas.height = size.height; const ctx = canvas.getContext('2d'); ctx.drawImage(image, 0, 0); const blob = await canvasToBlob(canvas); return blobToFile(blob, getFilenameFromURL(image.src)); } catch (err) { throw err; } }; var isDataURI = (str) => /^data:/.test(str); var createProgressEvent = (loaded = 0, lengthComputable = true) => new (getNativeAPIRef('ProgressEvent'))('progress', { loaded: loaded * 100, total: 100, lengthComputable, }); var isImage = (file) => /^image/.test(file.type); var dataURIToFile = async (dataURI, filename = 'data-uri', onprogress = noop$1) => { // basic loader, no size info onprogress(createProgressEvent(0)); const res = await fetch(dataURI); onprogress(createProgressEvent(0.33)); const blob = await res.blob(); let mimeType; if (!isImage(blob)) mimeType = `image/${dataURI.includes(',/9j/') ? 'jpeg' : 'png'}`; onprogress(createProgressEvent(0.66)); const file = blobToFile(blob, filename, mimeType); onprogress(createProgressEvent(1)); return file; }; var getResponseHeader = (xhr, header, parse = (header) => header) => xhr.getAllResponseHeaders().indexOf(header) >= 0 ? parse(xhr.getResponseHeader(header)) : undefined; var getFilenameFromContentDisposition = (header) => { if (!header) return null; const matches = header.split(/filename=|filename\*=.+''/) .splice(1) .map(name => name.trim().replace(/^["']|[;"']{0,2}$/g, '')) .filter(name => name.length); return matches.length ? decodeURI(matches[matches.length - 1]) : null; }; const EditorErrorCode = { URL_REQUEST: 'URL_REQUEST', DOCTYPE_MISSING: 'DOCTYPE_MISSING', }; class EditorError extends Error { constructor(message, code, metadata) { super(message); this.name = 'EditorError'; this.code = code; this.metadata = metadata; } } var fetchFile = (url, onprogress) => new Promise((resolve, reject) => { const handleError = () => reject(new EditorError('Error fetching image', EditorErrorCode.URL_REQUEST, xhr)); const xhr = new XMLHttpRequest(); xhr.onprogress = onprogress; (xhr.onerror = handleError), (xhr.onload = () => { if (!xhr.response || xhr.status >= 300 || xhr.status < 200) return handleError(); // we store the response mime type so we can add it to the blob later on, if it's missing (happens on Safari 10) const mimetype = getResponseHeader(xhr, 'Content-Type'); // try to get filename and any file instructions as well const filename = getResponseHeader(xhr, 'Content-Disposition', getFilenameFromContentDisposition) || getFilenameFromURL(url); // convert to actual file if possible resolve(blobToFile(xhr.response, filename, mimetype || getMimeTypeFromFilename(filename))); }); xhr.open('GET', url); xhr.responseType = 'blob'; xhr.send(); }); var urlToFile = (url, onprogress) => { // use fetch to create blob from data uri if (isDataURI(url)) return dataURIToFile(url, undefined, onprogress); // load file from url return fetchFile(url, onprogress); }; var isBlob = (v) => v instanceof Blob && !(v instanceof File); var srcToFile = async (src, onprogress) => { if (isFile(src) || isBlob(src)) return src; else if (isString(src)) return await urlToFile(src, onprogress); else if (isElement(src, 'canvas')) return await canvasToFile(src); else if (isElement(src, 'img')) return await imageToFile(src); else { throw new EditorError('Invalid image source', 'invalid-image-source'); } }; let result$8 = null; var isMac = () => { if (result$8 === null) result$8 = isBrowser() && /^mac/i.test(navigator.platform); return result$8; }; var isUserAgent = (test) => (isBrowser() ? RegExp(test).test(window.navigator.userAgent) : undefined); let result$7 = null; var isIOS = () => { if (result$7 === null) // first part is for iPhones and iPads iOS 12 and below second part is for iPads with iOS 13 and up result$7 = isBrowser() && (isUserAgent(/iPhone|iPad|iPod/) || (isMac() && navigator.maxTouchPoints >= 1)); return result$7; }; var orientImageSize = async (size, orientation = 1) => { // browser can handle image orientation if ((await canOrientImages()) || isIOS()) return size; // no need to correct size if (orientation < 5) return size; // correct image size return sizeCreate(size.height, size.width); }; var isJPEG = (file) => /jpeg/.test(file.type); var isPlainObject = (obj) => typeof obj == 'object' && obj.constructor == Object; var stringify = (value) => (!isPlainObject(value) ? value : JSON.stringify(value)); var post = (url, dataset, options) => new Promise((resolve, reject) => { const { token = {}, beforeSend = noop$1, onprogress = noop$1 } = options; token.cancel = () => request.abort(); const request = new XMLHttpRequest(); request.upload.onprogress = onprogress; request.onload = () => request.status >= 200 && request.status < 300 ? resolve(request) : reject(request); request.onerror = () => reject(request); request.ontimeout = () => reject(request); request.open('POST', encodeURI(url)); beforeSend(request); request.send(dataset.reduce((formData, args) => { // @ts-ignore formData.append(...args.map(stringify)); return formData; }, new FormData())); }); var ctxRotate = (ctx, rotation = 0, pivot) => { if (rotation === 0) return ctx; ctx.translate(pivot.x, pivot.y); ctx.rotate(rotation); ctx.translate(-pivot.x, -pivot.y); return ctx; }; var ctxTranslate = (ctx, x, y) => { ctx.translate(x, y); return ctx; }; var ctxScale = (ctx, x, y) => { ctx.scale(x, y); return ctx; }; var cropImageData = async (imageData, options = {}) => { const { flipX, flipY, rotation, crop } = options; const imageSize = sizeCreateFromAny(imageData); const shouldFlip = flipX || flipY; const shouldRotate = !!rotation; const cropDefined = crop && (crop.x || crop.y || crop.width || crop.height); const cropCoversImage = cropDefined && rectEqual(crop, rectCreateFromSize(imageSize)); const shouldCrop = cropDefined && !cropCoversImage; // skip! if (!shouldFlip && !shouldRotate && !shouldCrop) return imageData; // create drawing context let imageDataOut; let image = h('canvas', { width: imageData.width, height: imageData.height, }); image.getContext('2d').putImageData(imageData, 0, 0); // flip image data if (shouldFlip) { const ctx = h('canvas', { width: image.width, height: image.height, }).getContext('2d'); ctxScale(ctx, flipX ? -1 : 1, flipY ? -1 : 1); ctx.drawImage(image, flipX ? -image.width : 0, flipY ? -image.height : 0); ctx.restore(); releaseCanvas(image); image = ctx.canvas; } // rotate image data if (shouldRotate) { // if shouldRotate is true we also receive a crop rect const outputSize = sizeApply(sizeCreateFromRect(rectCreateFromPoints(rectRotate(rectCreateFromAny(image), rotation))), Math.floor); const ctx = h('canvas', { width: crop.width, height: crop.height, }).getContext('2d'); ctxTranslate(ctx, -crop.x, -crop.y); ctxRotate(ctx, rotation, sizeCenter(outputSize)); ctx.drawImage(image, (outputSize.width - image.width) * 0.5, (outputSize.height - image.height) * 0.5); ctx.restore(); releaseCanvas(image); image = ctx.canvas; } // crop image data else if (shouldCrop) { const ctx = image.getContext('2d'); imageDataOut = ctx.getImageData(crop.x, crop.y, crop.width, crop.height); releaseCanvas(image); return imageDataOut; } // done, return resulting image data const ctx = image.getContext('2d'); imageDataOut = ctx.getImageData(0, 0, image.width, image.height); releaseCanvas(image); return imageDataOut; }; var resizeTransform = (options, done) => { const { imageData, width, height } = options; const originWidth = imageData.width; const originHeight = imageData.height; const targetWidth = Math.round(width); const targetHeight = Math.round(height); const inputData = imageData.data; const outputData = new Uint8ClampedArray(targetWidth * targetHeight * 4); const ratioWidth = originWidth / targetWidth; const ratioHeight = originHeight / targetHeight; const ratioWidthHalf = Math.ceil(ratioWidth * 0.5); const ratioHeightHalf = Math.ceil(ratioHeight * 0.5); for (let j = 0; j < targetHeight; j++) { for (let i = 0; i < targetWidth; i++) { const x2 = (i + j * targetWidth) * 4; let weight = 0; let weights = 0; let weightsAlpha = 0; let r = 0; let g = 0; let b = 0; let a = 0; const centerY = (j + 0.5) * ratioHeight; for (let yy = Math.floor(j * ratioHeight); yy < (j + 1) * ratioHeight; yy++) { const dy = Math.abs(centerY - (yy + 0.5)) / ratioHeightHalf; const centerX = (i + 0.5) * ratioWidth; const w0 = dy * dy; for (let xx = Math.floor(i * ratioWidth); xx < (i + 1) * ratioWidth; xx++) { let dx = Math.abs(centerX - (xx + 0.5)) / ratioWidthHalf; const w = Math.sqrt(w0 + dx * dx); if (w >= -1 && w <= 1) { weight = 2 * w * w * w - 3 * w * w + 1; if (weight > 0) { dx = 4 * (xx + yy * originWidth); const ref = inputData[dx + 3]; a += weight * ref; weightsAlpha += weight; if (ref < 255) { weight = (weight * ref) / 250; } r += weight * inputData[dx]; g += weight * inputData[dx + 1]; b += weight * inputData[dx + 2]; weights += weight; } } } } outputData[x2] = r / weights; outputData[x2 + 1] = g / weights; outputData[x2 + 2] = b / weights; outputData[x2 + 3] = a / weightsAlpha; } } done(null, { data: outputData, width: targetWidth, height: targetHeight, }); }; var imageDataObjectToImageData = (obj) => { if (obj instanceof ImageData) { return obj; } let imageData; try { imageData = new ImageData(obj.width, obj.height); } catch (err) { // IE + Old EDGE (tested on 12) const canvas = h('canvas'); imageData = canvas.getContext('2d').createImageData(obj.width, obj.height); } imageData.data.set(obj.data); return imageData; }; var resizeImageData = async (imageData, options = {}) => { const { width, height, fit, upscale } = options; // no need to rescale if (!width && !height) return imageData; let targetWidth = width; let targetHeight = height; if (!width) { targetWidth = height; } else if (!height) { targetHeight = width; } if (fit !== 'force') { let scalarWidth = targetWidth / imageData.width; let scalarHeight = targetHeight / imageData.height; let scalar = 1; if (fit === 'cover') { scalar = Math.max(scalarWidth, scalarHeight); } else if (fit === 'contain') { scalar = Math.min(scalarWidth, scalarHeight); } // if image is too small, exit here with original image if (scalar > 1 && upscale === false) return imageData; targetWidth = Math.round(imageData.width * scalar); targetHeight = Math.round(imageData.height * scalar); } // no need to resize? if (imageData.width === targetWidth && imageData.height === targetHeight) return imageData; // let's resize! imageData = await thread(resizeTransform, [{ imageData: imageData, width: targetWidth, height: targetHeight }], [imageData.data.buffer]); // the resizer returns a plain object, not an actual image data object, lets create one return imageDataObjectToImageData(imageData); }; var colorEffect = (options, done) => { const { imageData, matrix } = options; if (!matrix) return done(null, imageData); const outputData = new Uint8ClampedArray(imageData.width * imageData.height * 4); const data = imageData.data; const l = data.length; const m11 = matrix[0]; const m12 = matrix[1]; const m13 = matrix[2]; const m14 = matrix[3]; const m15 = matrix[4]; const m21 = matrix[5]; const m22 = matrix[6]; const m23 = matrix[7]; const m24 = matrix[8]; const m25 = matrix[9]; const m31 = matrix[10]; const m32 = matrix[11]; const m33 = matrix[12]; const m34 = matrix[13]; const m35 = matrix[14]; const m41 = matrix[15]; const m42 = matrix[16]; const m43 = matrix[17]; const m44 = matrix[18]; const m45 = matrix[19]; let index = 0; let r = 0.0; let g = 0.0; let b = 0.0; let a = 0.0; let mr = 0.0; let mg = 0.0; let mb = 0.0; let ma = 0.0; let or = 0.0; let og = 0.0; let ob = 0.0; for (; index < l; index += 4) { r = data[index] / 255; g = data[index + 1] / 255; b = data[index + 2] / 255; a = data[index + 3] / 255; mr = r * m11 + g * m12 + b * m13 + a * m14 + m15; mg = r * m21 + g * m22 + b * m23 + a * m24 + m25; mb = r * m31 + g * m32 + b * m33 + a * m34 + m35; ma = r * m41 + g * m42 + b * m43 + a * m44 + m45; or = Math.max(0, mr * ma) + (1.0 - ma); og = Math.max(0, mg * ma) + (1.0 - ma); ob = Math.max(0, mb * ma) + (1.0 - ma); outputData[index] = Math.max(0.0, Math.min(1.0, or)) * 255; outputData[index + 1] = Math.max(0.0, Math.min(1.0, og)) * 255; outputData[index + 2] = Math.max(0.0, Math.min(1.0, ob)) * 255; outputData[index + 3] = a * 255; } done(null, { data: outputData, width: imageData.width, height: imageData.height, }); }; var convolutionEffect = (options, done) => { const { imageData, matrix } = options; if (!matrix) return done(null, imageData); // calculate kernel weight let kernelWeight = matrix.reduce((prev, curr) => prev + curr); kernelWeight = kernelWeight <= 0 ? 1 : kernelWeight; // input info const inputWidth = imageData.width; const inputHeight = imageData.height; const inputData = imageData.data; let i = 0; let x = 0; let y = 0; const side = Math.round(Math.sqrt(matrix.length)); const sideHalf = Math.floor(side / 2); let r = 0, g = 0, b = 0, a = 0, cx = 0, cy = 0, scy = 0, scx = 0, srcOff = 0, weight = 0; const outputData = new Uint8ClampedArray(inputWidth * inputHeight * 4); for (y = 0; y < inputHeight; y++) { for (x = 0; x < inputWidth; x++) { // calculate the weighed sum of the source image pixels that // fall under the convolution matrix r = 0; g = 0; b = 0; a = 0; for (cy = 0; cy < side; cy++) { for (cx = 0; cx < side; cx++) { scy = y + cy - sideHalf; scx = x + cx - sideHalf; if (scy < 0) { scy = inputHeight - 1; } if (scy >= inputHeight) { scy = 0; } if (scx < 0) { scx = inputWidth - 1; } if (scx >= inputWidth) { scx = 0; } srcOff = (scy * inputWidth + scx) * 4; weight = matrix[cy * side + cx]; r += inputData[srcOff] * weight; g += inputData[srcOff + 1] * weight; b += inputData[srcOff + 2] * weight; a += inputData[srcOff + 3] * weight; } } outputData[i] = r / kernelWeight; outputData[i + 1] = g / kernelWeight; outputData[i + 2] = b / kernelWeight; outputData[i + 3] = a / kernelWeight; i += 4; } } done(null, { data: outputData, width: inputWidth, height: inputHeight, }); }; var vignetteEffect = (options, done) => { let { imageData, strength } = options; if (!strength) return done(null, imageData); const outputData = new Uint8ClampedArray(imageData.width * imageData.height * 4); const inputWidth = imageData.width; const inputHeight = imageData.height; const inputData = imageData.data; const dist = (x, y) => { dx = x - cx; dy = y - cy; return Math.sqrt(dx * dx + dy * dy); }; let x = 0; let y = 0; let cx = inputWidth * 0.5; let cy = inputHeight * 0.5; let dx; let dy; let dm = dist(0, 0); let fr, fg, fb; let br, bg, bb, ba; let fa; let ca; const blend = (index, input, output, alpha) => { br = input[index] / 255; bg = input[index + 1] / 255; bb = input[index + 2] / 255; ba = input[index + 3] / 255; fa = 1.0 - alpha; ca = fa * ba + alpha; output[index] = ((fa * ba * br + alpha * fr) / ca) * 255; output[index + 1] = ((fa * ba * bg + alpha * fg) / ca) * 255; output[index + 2] = ((fa * ba * bb + alpha * fb) / ca) * 255; output[index + 3] = ca * 255; }; if (strength > 0) { fr = 0; fg = 0; fb = 0; } else { strength = Math.abs(strength); fr = 1; fg = 1; fb = 1; } for (y = 0; y < inputHeight; y++) { for (x = 0; x < inputWidth; x++) { blend( // index (x + y * inputWidth) * 4, // data in inputData, // data out outputData, // opacity (dist(x, y) * strength) / dm); } } done(null, { data: outputData, width: imageData.width, height: imageData.height, }); }; var noiseEffect = (options, done) => { const { imageData, level, monochrome = false } = options; if (!level) return done(null, imageData); const outputData = new Uint8ClampedArray(imageData.width * imageData.height * 4); const data = imageData.data; const l = data.length; let index = 0; let r; let g; let b; const rand = () => (-1 + Math.random() * 2) * 255 * level; const pixel = monochrome ? () => { const average = rand(); return [average, average, average]; } : () => { return [rand(), rand(), rand()]; }; for (; index < l; index += 4) { [r, g, b] = pixel(); outputData[index] = data[index] + r; outputData[index + 1] = data[index + 1] + g; outputData[index + 2] = data[index + 2] + b; outputData[index + 3] = data[index + 3]; } done(null, { data: outputData, width: imageData.width, height: imageData.height, }); }; var gammaEffect = (options, done) => { const { imageData, level } = options; if (!level) return done(null, imageData); const outputData = new Uint8ClampedArray(imageData.width * imageData.height * 4); const data = imageData.data; const l = data.length; let index = 0; let r; let g; let b; for (; index < l; index += 4) { r = data[index] / 255; g = data[index + 1] / 255; b = data[index + 2] / 255; outputData[index] = Math.pow(r, level) * 255; outputData[index + 1] = Math.pow(g, level) * 255; outputData[index + 2] = Math.pow(b, level) * 255; outputData[index + 3] = data[index + 3]; } done(null, { data: outputData, width: imageData.width, height: imageData.height, }); }; var isIdentityMatrix = (matrix) => { /* [ 1, 0, 0, 0, 0 0, 1, 0, 0, 0 0, 0, 1, 0, 0 0, 0, 0, 1, 0 ] */ const l = matrix.length; let v; let s = l >= 20 ? 6 : l >= 16 ? 5 : 3; for (let i = 0; i < l; i++) { v = matrix[i]; if (v === 1 && i % s !== 0) return false; else if (v !== 0 && v !== 1) return false; } return true; }; var filterImageData = async (imageData, options = {}) => { const { colorMatrix, convolutionMatrix, gamma: gammaLevel, noise: noiseLevel, vignette: vignetteStrength, } = options; // filters const filters = []; // apply convolution matrix if (convolutionMatrix) { filters.push([convolutionEffect, { matrix: convolutionMatrix.clarity }]); } // apply noise if (gammaLevel > 0) { filters.push([gammaEffect, { level: 1.0 / gammaLevel }]); } // apply color matrix if (colorMatrix && !isIdentityMatrix(colorMatrix)) { filters.push([colorEffect, { matrix: colorMatrix }]); } // apply noise if (noiseLevel > 0 || noiseLevel < 0) { filters.push([noiseEffect, { level: noiseLevel }]); } // apply vignette if (vignetteStrength > 0 || vignetteStrength < 0) { filters.push([vignetteEffect, { strength: vignetteStrength }]); } // no changes if (!filters.length) return imageData; // builds effect chain const chain = (transforms, i) => `(err, imageData) => { (${transforms[i][0].toString()})(Object.assign({ imageData: imageData }, filterInstructions[${i}]), ${transforms[i + 1] ? chain(transforms, i + 1) : 'done'}) }`; const filterChain = `function (options, done) { const filterInstructions = options.filterInstructions; const imageData = options.imageData; (${chain(filters, 0)})(null, imageData) }`; imageData = await thread(filterChain, [ { imageData: imageData, filterInstructions: filters.map((t) => t[1]), }, ], [imageData.data.buffer]); // the resizer returns a plain object, not an actual image data object, lets create one return imageDataObjectToImageData(imageData); }; var isNumber = (v) => typeof v === 'number'; var isEmoji = (str) => isString(str) && str.match(/(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g) !== null; var hasProp = (obj, key) => obj.hasOwnProperty(key); var isFunction = (v) => typeof v === 'function'; var isArray = (arr) => Array.isArray(arr); var isApple = () => isIOS() || isMac(); var isWindows = () => /^win/i.test(navigator.platform); // macos: font-size: 123, x: 63.5, y: 110 // windows: font-size: 112, x: 64, y: 103 // android: font-size: 112, x: 64, y: 102 let x = 64; let y = 102; let fontSize = 112; let hasSetValues = false; var getEmojiSVG = (emoji, alt) => { if (!hasSetValues && isBrowser()) { if (isWindows()) y = 103; if (isApple()) { x = 63.5; y = 110; fontSize = 123; } hasSetValues = true; } return `${emoji}`; }; var SVGToDataURL = (svg) => `data:image/svg+xml,${svg.replace('<', '%3C').replace('>', '%3E')}`; var isBinary = (v) => v instanceof Blob; var toPercentage = (value, total) => `${(value / total) * 100}%`; var colorArrayToRGBA = (color) => `rgba(${Math.round(color[0] * 255)}, ${Math.round(color[1] * 255)}, ${Math.round(color[2] * 255)}, ${isNumber(color[3]) ? color[3] : 1})`; const textPadding = 20; // font offset // font size 16 -> 2, 4 // font size 32 -> 4, 6 // font size 64 -> 8, 12 // font size 128 -> 16, 24 // font size 256 -> 32, 48 let fontOffsetBrowser = undefined; const getBrowserFontOffset = (fontSize) => { if (!fontOffsetBrowser) { // size const size = 32; // let's calculate it const ctx = createSimpleContext(size, size); updateTextContext(ctx, { fontSize: 100, color: '#fff' }); ctx.fillText('F', 0, 0); // get pixel data so we can find the white pixels const data = ctx.getImageData(0, 0, size, size).data; // find x offset let p = 0; let step = 4; let to = data.length; let from = to - size * 4; for (p = from; p < to; p += step) { if (data[p]) break; } const x = (p - from) / step; // find y offset from = (size - 1) * 4; step = size * 4; for (p = from; p < to; p += step) { if (data[p]) break; } const y = (p - from) / step; fontOffsetBrowser = vectorCreate(x, y); // done with canvas releaseCanvas(ctx.canvas); } return vectorCreate(-fontOffsetBrowser.x * fontSize * 0.01, -fontOffsetBrowser.y * fontSize * 0.01); }; const createSimpleContext = (width = 1, height = 1) => { const canvas = h('canvas'); const ctx = canvas.getContext('2d'); ctx.canvas.width = width; ctx.canvas.height = height; return ctx; }; const updateTextContext = (ctx, options) => { const { fontSize = 16, fontFamily = 'sans-serif', fontWeight = 'normal', fontVariant = 'normal', fontStyle = 'normal', textAlign = 'left', color = '#000', } = options; ctx.font = `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize}px ${fontFamily}`; ctx.textBaseline = 'top'; ctx.textAlign = textAlign; ctx.fillStyle = Array.isArray(color) ? colorArrayToRGBA(color) : color; }; const createSimpleTextContext = (options) => { const ctx = createSimpleContext(); updateTextContext(ctx, options); return ctx; }; const computeLineHeight = (fontSize, lineHeight) => isFunction(lineHeight) ? lineHeight(fontSize) : lineHeight; const getMeasureVisibleWidth = (measure) => Math.abs(measure.actualBoundingBoxLeft) + Math.abs(measure.actualBoundingBoxRight); const resizeContextToFitText = (ctx, text, options) => { const { width, height } = measureTextContext(ctx, text, computeLineHeight(options.fontSize, options.lineHeight)); ctx.canvas.width = Math.ceil(width); ctx.canvas.height = Math.ceil(height); return ctx; }; const measureTextContext = (ctx, text, computedLineHeight) => { const storedTextAlign = ctx.textAlign; ctx.textAlign = 'left'; // calculate width const lines = text.split('\n'); const width = lines.reduce((prev, curr) => { const lineWidth = getMeasureVisibleWidth(ctx.measureText(curr)); if (lineWidth > prev) { prev = lineWidth; } return prev; }, 1); ctx.textAlign = storedTextAlign; // calculate height const height = computedLineHeight * lines.length; return sizeCreate(Math.ceil(width), Math.ceil(height)); }; const TextSizeCache = new Map(); const createTextSizeHash = (text, { fontSize, fontFamily, lineHeight, fontWeight, fontStyle, fontVariant }) => `${[text, fontSize, fontWeight, fontStyle, fontVariant, fontFamily].join('_')}_${isFunction(lineHeight) ? lineHeight(fontSize) : lineHeight}`; const textSize = (text, options) => { const ctx = createSimpleTextContext(options); if (options.width) text = wrapText(ctx, text, options.width); const hash = createTextSizeHash(text, options); let size = TextSizeCache.get(hash); if (size) return { ...size }; size = measureTextContext(ctx, text, computeLineHeight(options.fontSize, options.lineHeight)); TextSizeCache.set(hash, size); return { ...size }; }; const wrapText = (ctx, text, lineWidth) => { // exit if no text if (text.length === 0) return ''; const res = []; let lineBuffer = ''; let lineIndex = 0; let measureWidth; const paragraphs = text.split('\n\n'); // draw the current line const pushLine = () => { if (!lineBuffer.length) return; if (!res[lineIndex]) { res[lineIndex] = []; } res[lineIndex].push(lineBuffer); // clear buffer lineBuffer = ''; }; const fitChar = (char) => { const testLine = lineBuffer + char; // measure width of entire line if adding these chars measureWidth = ctx.measureText(testLine).width; // fits on line? if (measureWidth < lineWidth) { lineBuffer = testLine; } else { // doesn't fit but line buffer is empty, just print the character and move to next line if (!lineBuffer.length) { lineBuffer = testLine; pushLine(); } // fits, lets print current line and move char to next line else { pushLine(); lineBuffer = char; } lineIndex++; } }; const fitWord = (word) => { const testLine = lineBuffer.length ? lineBuffer + ' ' + word : word; // measure width of entire line if adding these chars measureWidth = ctx.measureText(testLine).width; // fits on line? if (measureWidth < lineWidth) { lineBuffer = testLine; } // wrap to next line else { // if line buffer is empty, whole word doesn't fit, need to cut it up if (!lineBuffer.length) { word.split('').forEach(fitChar); } // there are words in the buffer that do fit, let's draw the line and move this word to the next line else { // draw current buffer pushLine(); lineIndex++; // retry to fit this word fitWord(word); } } }; paragraphs.forEach((p) => { const lines = p.split('\n'); lines.forEach((l) => { l.split(' ').forEach(fitWord); // end of line reached, if we have words in our buffer // at this point we need to draw them and then move to the next line if (lineBuffer.length) pushLine(); // forced new line lineIndex++; }); // forced new line lineIndex++; }); return res.map((line) => line.join(' ')).join('\n'); }; const drawText$1 = (ctx, text = '', options = {}) => { // exit if no text if (text.length === 0) return ctx; const { x = 0, y = 0, lineWidth = 0, textAlign, fontSize, lineHeight } = options; // determine where the browser will render the font and correct for browser differences const browserFontOffset = vectorAdd(getBrowserFontOffset(fontSize), vectorCreate(fontSize / 12, fontSize / 3.75)); const fontOffsetX = x + browserFontOffset.x; const fontOffsetY = y + browserFontOffset.y; const lineHeightComputed = isFunction(lineHeight) ? lineHeight(fontSize) : lineHeight; let offset = textAlign === 'right' ? lineWidth : textAlign === 'center' ? lineWidth * 0.5 : 0; text.split('\n').forEach((line, i) => { ctx.fillText(line, fontOffsetX + offset, fontOffsetY + i * lineHeightComputed); }); return ctx; }; var fixPrecision = (value, precision = 12) => parseFloat(value.toFixed(precision)); const shapeEqual = (a, b) => { return JSON.stringify(a) === JSON.stringify(b); }; const shapeDeepCopy = (shape) => { const shapeShallowCopy = { ...shape }; const shapeDeepCopy = deepCopy(shapeShallowCopy); return shapeDeepCopy; }; const getContextSize = (context, size = {}) => { const contextAspectRatio = rectAspectRatio(context); let xOut; let yOut; const xIn = size.width || size.rx; const yIn = size.height || size.ry; if (xIn && yIn) return sizeClone(size); if (xIn || yIn) { xOut = parseFloat(xIn || Number.MAX_SAFE_INTEGER); yOut = parseFloat(yIn || Number.MAX_SAFE_INTEGER); const min = Math.min(xOut, yOut); if (isString(xIn) || isString(yIn)) { xOut = `${min}%`; yOut = `${min * contextAspectRatio}%`; } else { xOut = min; yOut = min; } } else { const min = 10; xOut = `${min}%`; yOut = `${min * contextAspectRatio}%`; } const xProp = size.width ? 'width' : size.rx ? 'rx' : undefined; const yProp = size.width ? 'height' : size.rx ? 'ry' : undefined; return { [xProp || 'width']: xOut, [yProp || 'height']: yOut, }; }; const shapeCreateFromEmoji = (emoji, props = {}) => { return { width: undefined, height: undefined, ...props, aspectRatio: 1, backgroundImage: SVGToDataURL(getEmojiSVG(emoji)), }; }; const shapeCreateFromImage = (src, shapeProps = {}) => { const shapeDefaultLayout = shapeIsEllipse(shapeProps) ? {} : { width: undefined, height: undefined, aspectRatio: undefined, }; const shape = { // required/default image shape props backgroundColor: [0, 0, 0, 0], // set default layout props ...shapeDefaultLayout, // merge with custom props ...shapeProps, // set image backgroundImage: // is svg or URL isString(src) ? src : isBinary(src) ? URL.createObjectURL(src) : src, }; return shape; }; const shapeCreateFromPreset = (preset, parentRect) => { let shape; if (isString(preset) || isBinary(preset)) { // default props for "quick" preset const shapeOptions = { ...getContextSize(parentRect), backgroundSize: 'contain', }; // if is emoji, create default markup, if (isEmoji(preset)) { shape = shapeCreateFromEmoji(preset, shapeOptions); } // is URL, create default markup for image else { shape = shapeCreateFromImage(preset, shapeOptions); } } else { // is using src shortcut if (preset.src) { const contextSize = getContextSize(parentRect, preset.shape || preset); // shape options const shapeOptions = { // default shape styles ...preset.shape, // precalcualte size of shape in context ...contextSize, }; // should auto-fix aspect ratio if (preset.width && preset.height && !hasProp(shapeOptions, 'aspectRatio')) { const width = shapeGetPropPixelValue(contextSize, 'width', parentRect); const height = shapeGetPropPixelValue(contextSize, 'height', parentRect); shapeOptions.aspectRatio = getAspectRatio(width, height); } // should auto-contain sticker in container if (!shapeOptions.backgroundSize && !preset.shape && (!preset.width || !preset.height)) shapeOptions.backgroundSize = 'contain'; // emoji markup if (isEmoji(preset.src)) { shape = shapeCreateFromEmoji(preset.src, shapeOptions); } // is url else { shape = shapeCreateFromImage(preset.src, shapeOptions); } } // should have markup defined else if (preset.shape) { shape = shapeDeepCopy(preset.shape); } } if (hasProp(shape, 'backgroundImage')) { // set transparent background if no background color defined if (!hasProp(shape, 'backgroundColor')) { shape.backgroundColor = [0, 0, 0, 0]; } // for image presets, disable styles by default if (!hasProp(shape, 'disableStyle')) { shape.disableStyle = ['backgroundColor', 'strokeColor', 'strokeWidth']; } // by default don't allow flipping if (!hasProp(shape, 'disableFlip')) { shape.disableFlip = true; } } return parentRect ? shapeComputeDisplay(shape, parentRect) : shape; }; const shapeLineGetStartPoint = (line) => vectorCreate(line.x1, line.y1); const shapeLineGetEndPoint = (line) => vectorCreate(line.x2, line.y2); const shapeTextUID = ({ text, textAlign, fontSize, fontFamily, lineHeight, fontWeight, fontStyle, fontVariant, }) => `${[text, textAlign, fontSize, fontWeight, fontStyle, fontVariant, fontFamily].join('_')}_${isFunction(lineHeight) ? lineHeight(fontSize) : lineHeight}`; //#endregion //#region shape testing // shape types const shapeIsText = (shape) => hasProp(shape, 'text'); const shapeIsTextLine = (shape) => shapeIsText(shape) && !(shapeHasRelativeSize(shape) || hasProp(shape, 'width')); const shapeIsTextBox = (shape) => shapeIsText(shape) && (shapeHasRelativeSize(shape) || hasProp(shape, 'width')); const shapeIsRect = (shape) => !shapeIsText(shape) && shapeHasComputedSize(shape); const shapeIsEllipse = (shape) => hasProp(shape, 'rx'); const shapeIsLine = (shape) => hasProp(shape, 'x1') && !shapeIsTriangle(shape); const shapeIsTriangle = (shape) => hasProp(shape, 'x3'); const shapeIsPath = (shape) => hasProp(shape, 'points'); // shape state const shapeIsTextEmpty = (shape) => shapeIsText(shape) && !shape.text.length; const shapeIsTextEditing = (shape) => shapeIsText(shape) && shape.isEditing; const shapeIsVisible = (shape) => hasProp(shape, 'opacity') ? shape.opacity > 0 : true; const shapeIsSelected = (shape) => shape.isSelected; const shapeIsDraft = (shape) => shape._isDraft; const shapeHasSize = (shape) => hasProp(shape, 'width') && hasProp(shape, 'height'); const shapeHasNumericStroke = (shape) => isNumber(shape.strokeWidth) && shape.strokeWidth > 0; // only relevant if is bigger than 0 const shapeHasRelativePosition = (shape) => { const hasRight = hasProp(shape, 'right'); const hasBottom = hasProp(shape, 'bottom'); return hasRight || hasBottom; }; const shapeHasTexture = (shape) => hasProp(shape, 'backgroundImage') || hasProp(shape, 'text'); const shapeHasRelativeSize = (shape) => ((hasProp(shape, 'x') || hasProp(shape, 'left')) && hasProp(shape, 'right')) || ((hasProp(shape, 'y') || hasProp(shape, 'top')) && hasProp(shape, 'bottom')); const shapeHasComputedSize = (shape) => shapeHasSize(shape) || shapeHasRelativeSize(shape); // actions const shapeSelect = (shape) => { shape.isSelected = true; return shape; }; const shapeMakeDraft = (shape) => { shape._isDraft = true; return shape; }; const shapeMakeFinal = (shape) => { shape._isDraft = false; return shape; }; // rights const shapeCanStyle = (shape, style) => { if (shape.disableStyle === true) return false; if (isArray(shape.disableStyle) && style) { return !shape.disableStyle.includes(style); } return true; }; const shapeCanSelect = (shape) => shape.disableSelect !== true && !shapeIsTriangle(shape); const shapeCanRemove = (shape) => shape.disableRemove !== true; const shapeCanDuplicate = (shape) => shape.disableDuplicate !== true && shapeCanMove(shape); const shapeCanReorder = (shape) => shape.disableReorder !== true; const shapeCanFlip = (shape) => { if (shape.disableFlip) return false; if (shapeIsDraft(shape) || shapeHasRelativePosition(shape)) return false; return shapeHasTexture(shape); }; const shapeCanInput = (shape, input) => { if (!shapeIsText(shape)) return false; if (shape.disableInput === true) return false; if (isFunction(shape.disableInput)) return shape.disableInput(input != null ? input : shape.text); return input || true; }; const shapeCanChangeTextLayout = (shape, layout) => { if (shape.disableTextLayout === true) return false; if (isArray(shape.disableTextLayout) && layout) { return !shape.disableTextLayout.includes(layout); } return true; }; const shapeCanManipulate = (shape) => shape.disableManipulate !== true && !shapeIsDraft(shape) && !shapeHasRelativePosition(shape); const shapeCanMove = (shape) => shapeCanManipulate(shape) && shape.disableMove !== true; const shapeCanResize = (shape) => shapeCanManipulate(shape) && shapeCanMove(shape) && shape.disableResize !== true && (shapeHasSize(shape) || shapeIsTextBox(shape) || shapeIsEllipse(shape) || shapeIsLine(shape)); const shapeCanRotate = (shape) => shapeCanManipulate(shape) && shape.disableRotate !== true && (shapeHasSize(shape) || hasProp(shape, 'text') || shapeIsEllipse(shape)); //#endregion //#region shape formatting const shapeDeleteRelativeProps = (shape) => { delete shape.left; delete shape.right; delete shape.top; delete shape.bottom; return shape; }; const shapeDeleteTransformProps = (shape) => { delete shape.rotation; return shape; }; const shapeFormatStroke = (shape) => { shape.strokeWidth = shape.strokeWidth || 1; shape.strokeColor = shape.strokeColor || [0, 0, 0]; return shape; }; const shapeFormatFill = (shape) => { shape.backgroundColor = shape.backgroundColor ? shape.backgroundColor : shape.strokeWidth || shape.backgroundImage ? undefined : [0, 0, 0]; return shape; }; const autoLineHeight = (fontSize) => fontSize * 1.2; const shapeFormatText = (shape) => { shape.fontSize = shape.fontSize || 16; shape.fontFamily = shape.fontFamily || 'sans-serif'; shape.fontWeight = shape.fontWeight || 'normal'; shape.fontStyle = shape.fontStyle || 'normal'; shape.fontVariant = shape.fontVariant || 'normal'; shape.lineHeight = isNumber(shape.lineHeight) ? shape.lineHeight : autoLineHeight; shape.color = shape.color || [0, 0, 0]; return shapeIsTextLine(shape) ? shapeFormatTextLine(shape) : shapeFormatTextBox(shape); }; const shapeFormatTextLine = (shape) => { delete shape.textAlign; return shapeDeleteRelativeProps(shape); }; const shapeFormatTextBox = (shape) => { shape.textAlign = shape.textAlign || 'left'; return shape; }; const shapeFormatRect = (shape) => { shape.cornerRadius = shape.cornerRadius || 0; shape.strokeWidth = shape.strokeWidth || 0; shape.strokeColor = shape.strokeColor || [0, 0, 0]; return shapeFormatFill(shape); }; const shapeFormatTriangle = (shape) => { shape.strokeWidth = shape.strokeWidth || 0; shape.strokeColor = shape.strokeColor || [0, 0, 0]; shapeFormatFill(shape); return shapeDeleteRelativeProps(shape); }; const shapeFormatEllipse = (shape) => { shape.strokeWidth = shape.strokeWidth || 0; shape.strokeColor = shape.strokeColor || [0, 0, 0]; return shapeFormatFill(shape); }; const shapeFormatPath = (shape) => { shapeFormatStroke(shape); shapeDeleteTransformProps(shape); return shapeDeleteRelativeProps(shape); }; const shapeFormatLine = (shape) => { shapeFormatStroke(shape); shape.lineStart = shape.lineStart || undefined; shape.lineEnd = shape.lineEnd || undefined; shapeDeleteTransformProps(shape); return shapeDeleteRelativeProps(shape); }; const shapeFormatDefaults = (shape) => { if (!isString(shape.id)) shape.id = getUniqueId(); if (!hasProp(shape, 'rotation')) shape.rotation = 0; if (!hasProp(shape, 'opacity')) shape.opacity = 1; if (!hasProp(shape, 'disableErase')) shape.disableErase = true; }; const shapeFormat = (shape) => { shapeFormatDefaults(shape); if (shapeIsText(shape)) { shapeFormatText(shape); } else if (shapeIsRect(shape)) { shapeFormatRect(shape); } else if (shapeIsPath(shape)) { shapeFormatPath(shape); } else if (shapeIsLine(shape)) { shapeFormatLine(shape); } else if (shapeIsEllipse(shape)) { shapeFormatEllipse(shape); } else if (shapeIsTriangle(shape)) { shapeFormatTriangle(shape); } return shape; }; const shapeGetDescription = (shape) => { if (shapeIsText(shape)) { return 'text'; } else if (shapeIsRect(shape)) { return 'rectangle'; } else if (shapeIsPath(shape)) { return 'path'; } else if (shapeIsLine(shape)) { return 'line'; } else if (shapeIsEllipse(shape)) { return 'ellipse'; } else if (shapeIsTriangle(shape)) { return 'triangle'; } return; }; //#endregion const toPixelValue = (percentage, total) => (parseFloat(percentage) / 100) * total; //#region shape transforming const xRegExp = new RegExp(/^x|left|^width|rx|fontSize|cornerRadius|strokeWidth/, 'i'); const yRegExp = new RegExp(/^y|top|^height|ry/, 'i'); const rightRegExp = new RegExp(/right/, 'i'); const bottomRegExp = new RegExp(/bottom/, 'i'); const compute = (key, value, { width, height }) => { // handle array of percentage values if (Array.isArray(value)) { return value.map((v) => { if (isObject(v)) { // update the object itself computeProps(v, { width, height }); } return v; }); } // no need to compute (test with typeof instead of for perf) if (typeof value !== 'string') return value; if (!value.endsWith('%')) return value; const f = parseFloat(value) / 100; if (xRegExp.test(key)) return fixPrecision(width * f, 6); if (yRegExp.test(key)) return fixPrecision(height * f, 6); if (rightRegExp.test(key)) return fixPrecision(width - width * f, 6); if (bottomRegExp.test(key)) return fixPrecision(height - height * f, 6); // dont auto-compute return value; }; const computeProps = (obj, size) => { return Object.entries(obj).map(([key, value]) => { obj[key] = compute(key, value, size); }); }; const shapeComputeDisplay = (shape, parentRect) => { computeProps(shape, parentRect); shapeComputeRect(shape, parentRect); return shape; }; const shapeGetPropPixelTotal = (prop, parentRect) => { let total; if (/^x|width|rx|fontSize|strokeWidth|cornerRadius/.test(prop)) { total = parentRect.width; } else if (/^y|height|ry/.test(prop)) { total = parentRect.height; } return total; }; const shapeUpdateProp = (shape, prop, value, parentRect) => { if (!isString(shape[prop])) { shape[prop] = value; return shape; } const total = shapeGetPropPixelTotal(prop, parentRect); shape[prop] = total === undefined ? value : toPercentage(value, total); return shape; }; const shapeGetPropPixelValue = (shape, prop, parentRect) => { if (!isString(shape[prop])) return shape[prop]; return toPixelValue(shape[prop], shapeGetPropPixelTotal(prop, parentRect)); }; const shapeGetPropsPixelValues = (shape, props, parentRect) => { return props.reduce((prev, prop) => { const value = shapeGetPropPixelValue(shape, prop, parentRect); prev[prop] = value; return prev; }, {}); }; const shapeUpdateProps = (shape, props, parentRect) => { Object.keys(props).forEach((key) => shapeUpdateProp(shape, key, props[key], parentRect)); return shape; }; const shapeBounds = (shape) => { const rect = rectCreateEmpty(); const strokeWidth = shape.strokeWidth || 0; if (shapeIsRect(shape)) { rect.x = shape.x - strokeWidth * 0.5; rect.y = shape.y - strokeWidth * 0.5; rect.width = shape.width + strokeWidth; rect.height = shape.height + strokeWidth; } else if (shapeIsLine(shape)) { const { x1, y1, x2, y2 } = shape; const left = Math.abs(Math.min(x1, x2)); const right = Math.abs(Math.max(x1, x2)); const top = Math.abs(Math.min(y1, y2)); const bottom = Math.abs(Math.min(y1, y2)); rect.x = left + strokeWidth * 0.5; rect.y = right + strokeWidth * 0.5; rect.width = right - left + strokeWidth; rect.height = bottom - top + strokeWidth; } else if (shapeIsEllipse(shape)) { rect.x = shape.x - shape.rx + strokeWidth * 0.5; rect.y = shape.y - shape.ry + strokeWidth * 0.5; rect.width = shape.rx * 2 + strokeWidth; rect.height = shape.ry * 2 + strokeWidth; } if (rect && hasProp(shape, 'rotation')) { rectRotate(rect, shape.rotation); } return rectToBounds(rect); }; const shapesBounds = (shapes, parentRect) => { const bounds = shapes .filter((shape) => shape.x < 0 || shape.y < 0 || shape.x1 < 0 || shape.y1 < 0) .reduce((bounds, shape) => { const [top, right, bottom, left] = shapeBounds(shape); bounds.top = Math.min(top, bounds.top); bounds.left = Math.min(left, bounds.left); bounds.bottom = Math.max(bottom, bounds.bottom); bounds.right = Math.max(right, bounds.right); return bounds; }, { top: 0, right: 0, bottom: 0, left: 0, }); if (bounds.right > 0) bounds.right -= parentRect.width; if (bounds.bottom > 0) bounds.bottom -= parentRect.height; return bounds; }; const shapesFromCompositShape = (shape, parentRect, parser) => { const shapeCopy = shapeDeepCopy(shape); shapeComputeDisplay(shapeCopy, parentRect); return parser(shapeCopy); }; const shapeComputeRect = (shape, parentRect) => { if (hasProp(shape, 'left')) shape.x = shape.left; if (hasProp(shape, 'right')) { const r = parentRect.width - shape.right; if (hasProp(shape, 'left')) { shape.x = shape.left; shape.width = Math.max(0, r - shape.left); } else if (hasProp(shape, 'width')) { shape.x = r - shape.width; } } if (hasProp(shape, 'top')) shape.y = shape.top; if (hasProp(shape, 'bottom')) { const b = parentRect.height - shape.bottom; if (hasProp(shape, 'top')) { shape.y = shape.top; shape.height = Math.max(0, b - shape.top); } else if (hasProp(shape, 'height')) { shape.y = b - shape.height; } } return shape; }; const shapeComputeTransform = (shape, translate, scale) => { if (shapeIsPath(shape)) { shape.points .filter((point) => isNumber(point.x)) .forEach((point) => { point.x *= scale; point.y *= scale; point.x += translate.x; point.y += translate.y; }); } if (shapeIsTriangle(shape) && isNumber(shape.x1)) { shape.x1 *= scale; shape.y1 *= scale; shape.x2 *= scale; shape.y2 *= scale; shape.x3 *= scale; shape.y3 *= scale; shape.x1 += translate.x; shape.y1 += translate.y; shape.x2 += translate.x; shape.y2 += translate.y; shape.x3 += translate.x; shape.y3 += translate.y; } if (shapeIsLine(shape) && isNumber(shape.x1)) { shape.x1 *= scale; shape.y1 *= scale; shape.x2 *= scale; shape.y2 *= scale; shape.x1 += translate.x; shape.y1 += translate.y; shape.x2 += translate.x; shape.y2 += translate.y; } if (isNumber(shape.x) && isNumber(shape.y)) { shape.x *= scale; shape.y *= scale; shape.x += translate.x; shape.y += translate.y; } if (isNumber(shape.width) && isNumber(shape.height)) { shape.width *= scale; shape.height *= scale; } if (isNumber(shape.rx) && isNumber(shape.ry)) { shape.rx *= scale; shape.ry *= scale; } if (shapeHasNumericStroke(shape)) { shape.strokeWidth *= scale; } if (shapeIsText(shape) && isNumber(shape.fontSize)) { shape.fontSize *= scale; if (isNumber(shape.width) && !isNumber(shape.width)) shape.width *= scale; } if (hasProp(shape, 'cornerRadius') && isNumber(shape.cornerRadius)) { shape.cornerRadius *= scale; } return shape; }; const shapeGetCenter = (shape) => { if (shapeIsRect(shape)) { return vectorCreate(shape.x + shape.width * 0.5, shape.y + shape.height * 0.5); } if (shapeIsEllipse(shape)) { return vectorCreate(shape.x, shape.y); } if (shapeIsTextBox(shape)) { const height = shape.height || textSize(shape.text, shape).height; return vectorCreate(shape.x + shape.width * 0.5, shape.y + height * 0.5); } if (shapeIsTextLine(shape)) { const size = textSize(shape.text, shape); return vectorCreate(shape.x + size.width * 0.5, shape.y + size.height * 0.5); } if (shapeIsPath(shape)) { return vectorCenter(shape.points); } if (shapeIsLine(shape)) { return vectorCenter([ shapeLineGetStartPoint(shape), shapeLineGetEndPoint(shape), ]); } return undefined; }; //#endregion var ctxRoundRect = (ctx, x, y, width, height, radius) => { if (width < 2 * radius) radius = width / 2; if (height < 2 * radius) radius = height / 2; ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.arcTo(x + width, y, x + width, y + height, radius); ctx.arcTo(x + width, y + height, x, y + height, radius); ctx.arcTo(x, y + height, x, y, radius); ctx.arcTo(x, y, x + width, y, radius); ctx.closePath(); return ctx; }; var isCanvas = (element) => /canvas/i.test(element.nodeName); var isRemoteURL = (url) => new URL(url, location.href).origin !== location.origin; var loadImage = (image, onSize = undefined) => new Promise((resolve, reject) => { // the image element we'll use to load the image let imageElement = image; let sizeCalculated = false; const reportSize = () => { if (sizeCalculated) return; sizeCalculated = true; isFunction(onSize) && /* Use Promise.resolve to make async but place before resolve of parent promise */ Promise.resolve().then(() => onSize(sizeCreate(imageElement.naturalWidth, imageElement.naturalHeight))); }; // if is not an image element, it must be a valid image source if (!imageElement.src) { imageElement = new Image(); // if is remote image, set crossOrigin // why not always set crossOrigin? -> because when set this fires two requests, // one for asking permission and one for downloading the image if (isString(image) && isRemoteURL(image)) imageElement.crossOrigin = 'anonymous'; imageElement.src = isString(image) ? image : URL.createObjectURL(image); } if (imageElement.complete) { reportSize(); return resolve(imageElement); } // try to calculate size faster if (isFunction(onSize)) getImageElementSize(imageElement).then(reportSize).catch(reject); imageElement.onload = () => { reportSize(); resolve(imageElement); }; imageElement.onerror = reject; }); var pubsub = () => { let subs = []; return { sub: (event, callback) => { subs.push({ event, callback }); return () => (subs = subs.filter((subscriber) => subscriber.event !== event || subscriber.callback !== callback)); }, pub: (event, value) => { subs .filter((sub) => sub.event === event) .forEach((sub) => sub.callback(value)); } }; }; const cache = new Map([]); const getImage = (src, options = {}) => new Promise((resolve, reject) => { const { onMetadata = noop$1, onLoad = resolve, onError = reject, onComplete = noop$1, } = options; let imageLoadState = cache.get(src); // start loading if (!imageLoadState) { imageLoadState = { loading: false, complete: false, error: false, image: undefined, size: undefined, bus: pubsub(), }; // store cache.set(src, imageLoadState); } // wait for load imageLoadState.bus.sub('meta', onMetadata); imageLoadState.bus.sub('load', onLoad); imageLoadState.bus.sub('error', onError); imageLoadState.bus.sub('complete', onComplete); // if is canvas, it's already done if (isCanvas(src)) { const canvas = src; // get image const image = canvas.cloneNode(); // update state imageLoadState.complete = true; imageLoadState.image = image; imageLoadState.size = sizeCreateFromElement(canvas); } // already loaded if (imageLoadState.complete) { imageLoadState.bus.pub('meta', { size: imageLoadState.size }); if (imageLoadState.error) { imageLoadState.bus.pub('error', imageLoadState.error); } else { imageLoadState.bus.pub('load', imageLoadState.image); } imageLoadState.bus.pub('complete'); // reset subscribers imageLoadState.bus = pubsub(); return; } // already loading, exit here if (imageLoadState.loading) return; // now loading imageLoadState.loading = true; // resource needs to be loaded loadImage(src, (size) => { imageLoadState.size = size; imageLoadState.bus.pub('meta', { size }); }) .then((image) => { imageLoadState.image = image; imageLoadState.bus.pub('load', image); }) .catch((err) => { imageLoadState.error = err; imageLoadState.bus.pub('error', err); }) .finally(() => { imageLoadState.complete = true; imageLoadState.loading = false; imageLoadState.bus.pub('complete'); // reset subscribers imageLoadState.bus = pubsub(); }); }); const drawCanvas = (ctx, image, srcRect, destRect) => ctx.drawImage(image, srcRect.x, srcRect.x, srcRect.width, srcRect.height, destRect.x, destRect.y, destRect.width, destRect.height); var ctxDrawImage = async (ctx, image, srcRect, destRect, draw = drawCanvas) => { ctx.save(); ctx.clip(); await draw(ctx, image, srcRect, destRect); ctx.restore(); }; const getDrawImageParams = (container, backgroundSize, imageSize) => { let srcRect = rectCreate(0, 0, imageSize.width, imageSize.height); const destRect = rectClone(container); if (backgroundSize === 'contain') { const rect = rectContainRect(container, rectAspectRatio(srcRect)); destRect.width = rect.width; destRect.height = rect.height; destRect.x += rect.x; destRect.y += rect.y; } else if (backgroundSize === 'cover') { srcRect = rectContainRect(rectCreate(0, 0, srcRect.width, srcRect.height), rectAspectRatio(destRect)); } return { srcRect, destRect, }; }; const defineRectShape = (ctx, shape) => { shape.cornerRadius > 0 ? ctxRoundRect(ctx, shape.x, shape.y, shape.width, shape.height, shape.cornerRadius) : ctx.rect(shape.x, shape.y, shape.width, shape.height); return ctx; }; const fillRectShape = (ctx, shape) => { shape.backgroundColor && ctx.fill(); return ctx; }; const strokeRectShape = (ctx, shape) => { shape.strokeWidth && ctx.stroke(); return ctx; }; var drawRect = async (ctx, shape, options = {}) => new Promise(async (resolve, reject) => { const { drawImage } = options; ctx.lineWidth = shape.strokeWidth ? shape.strokeWidth : 1; // 1 is default value for lineWidth prop ctx.strokeStyle = shape.strokeColor ? colorArrayToRGBA(shape.strokeColor) : 'none'; ctx.fillStyle = shape.backgroundColor ? colorArrayToRGBA(shape.backgroundColor) : 'none'; ctx.globalAlpha = shape.opacity; if (shape.backgroundImage) { let image; try { if (isCanvas(shape.backgroundImage)) { image = shape.backgroundImage; } else { image = await getImage(shape.backgroundImage); } } catch (err) { reject(err); } defineRectShape(ctx, shape); fillRectShape(ctx, shape); const { srcRect, destRect } = getDrawImageParams(shape, shape.backgroundSize, sizeCreateFromElement(image)); await ctxDrawImage(ctx, image, srcRect, destRect, drawImage); strokeRectShape(ctx, shape); resolve([]); } else { defineRectShape(ctx, shape); fillRectShape(ctx, shape); strokeRectShape(ctx, shape); resolve([]); } }); var drawEllipse = async (ctx, shape, options = {}) => new Promise(async (resolve, reject) => { const { drawImage } = options; ctx.lineWidth = shape.strokeWidth || 1; // 1 is default value for lineWidth prop ctx.strokeStyle = shape.strokeColor ? colorArrayToRGBA(shape.strokeColor) : 'none'; ctx.fillStyle = shape.backgroundColor ? colorArrayToRGBA(shape.backgroundColor) : 'none'; ctx.globalAlpha = shape.opacity; ctx.ellipse(shape.x, shape.y, shape.rx, shape.ry, 0, 0, Math.PI * 2); shape.backgroundColor && ctx.fill(); if (shape.backgroundImage) { let image; try { image = await getImage(shape.backgroundImage); } catch (err) { reject(err); } const bounds = rectCreate(shape.x - shape.rx, shape.y - shape.ry, shape.rx * 2, shape.ry * 2); const { srcRect, destRect } = getDrawImageParams(bounds, shape.backgroundSize, sizeCreateFromElement(image)); // @ts-ignore await ctxDrawImage(ctx, image, srcRect, destRect, drawImage); shape.strokeWidth && ctx.stroke(); resolve([]); } else { shape.strokeWidth && ctx.stroke(); resolve([]); } }); var drawText = async (ctx, shape, options) => { const size = shape.width && shape.height ? sizeCreateFromAny(shape) : textSize(shape.text, shape); const rect = { x: shape.x, y: shape.y, width: shape.width || size.width, height: size.height, }; drawRect(ctx, { ...shape, ...rect, options, }); updateTextContext(ctx, shape); let tx = 0; if (shape.textAlign == 'center') { tx = -textPadding * 0.5; } else if (shape.textAlign === 'right') { tx = -textPadding; } ctx.rect(shape.x + tx, shape.y, shape.width + textPadding * 2, shape.height); ctx.save(); ctx.clip(); drawText$1(ctx, shape.width ? wrapText(ctx, shape.text, shape.width) : shape.text, { x: shape.x, y: shape.y, fontSize: shape.fontSize, textAlign: shape.textAlign, lineHeight: shape.lineHeight, lineWidth: shape.width, }); ctx.restore(); return []; }; // TODO! START // ----------- var drawLine = async (ctx, shape) => new Promise(async (resolve) => { ctx.lineWidth = shape.strokeWidth || 1; // 1 is default value for lineWidth prop ctx.strokeStyle = shape.strokeColor ? colorArrayToRGBA(shape.strokeColor) : 'none'; ctx.globalAlpha = shape.opacity; let lineStartPosition = shapeLineGetStartPoint(shape); let lineEndPosition = shapeLineGetEndPoint(shape); // draw line ctx.moveTo(lineStartPosition.x, lineStartPosition.y); ctx.lineTo(lineEndPosition.x, lineEndPosition.y); shape.strokeWidth && ctx.stroke(); // draw other shapes resolve([]); }); // TODO! END // ----------- var drawPath = async (ctx, shape) => new Promise((resolve, reject) => { ctx.lineWidth = shape.strokeWidth || 1; // 1 is default value for lineWidth prop ctx.strokeStyle = shape.strokeColor ? colorArrayToRGBA(shape.strokeColor) : 'none'; ctx.fillStyle = shape.backgroundColor ? colorArrayToRGBA(shape.backgroundColor) : 'none'; ctx.globalAlpha = shape.opacity; // draw line const { points } = shape; if (shape.pathClose) ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); const l = points.length; for (let i = 1; i < l; i++) { ctx.lineTo(points[i].x, points[i].y); } if (shape.pathClose) ctx.closePath(); shape.strokeWidth && ctx.stroke(); shape.backgroundColor && ctx.fill(); resolve([]); }); var ctxFlip = (ctx, flipX, flipY, pivot) => { if (!flipX && !flipY) return ctx; ctx.translate(pivot.x, pivot.y); ctx.scale(flipX ? -1 : 1, flipY ? -1 : 1); ctx.translate(-pivot.x, -pivot.y); return ctx; }; const drawShape = async (ctx, shape, options) => { // center, needed for transforms const center = shapeGetCenter(shape); // rotate context ctxRotate(ctx, shape.rotation, center); // flip context ctxFlip(ctx, shape.flipX, shape.flipY, center); let fn; if (shapeIsRect(shape)) { fn = drawRect; } else if (shapeIsEllipse(shape)) { fn = drawEllipse; } else if (shapeIsLine(shape)) { fn = drawLine; } else if (shapeIsPath(shape)) { fn = drawPath; } else if (shapeIsText(shape)) { fn = drawText; } // get shapes return fn ? [shape, ...(await drawShapes(ctx, await fn(ctx, shape, options), options))] : []; }; var drawShapes = async (ctx, shapes, options) => { let drawnShapes = []; for (const shape of shapes) { ctx.save(); // clears previous shape's path ctx.beginPath(); // wait for shape to draw before drawing next shape drawnShapes = [...drawnShapes, ...(await drawShape(ctx, shape, options))]; ctx.restore(); } return drawnShapes; }; var drawImageData = async (imageData, options = {}) => { const { shapes = [], context = imageData, contextBounds = imageData, transform = noop$1, drawImage, preprocessShape = passthrough, } = options; // no shapes to draw if (!shapes.length) return imageData; // create drawing context const canvas = h('canvas'); canvas.width = contextBounds.width; canvas.height = contextBounds.height; const ctx = canvas.getContext('2d'); ctx.putImageData(imageData, contextBounds.x || 0, contextBounds.y || 0); // compute the position of all shapes const computedShapes = shapes .map(shapeDeepCopy) .map((shape) => shapeComputeDisplay(shape, { x: 0, y: 0, width: context.width, height: context.height, })) // need to take into account output size? .map(preprocessShape) .flat(); // compute transforms for all shapes transform(ctx); // draw shapes to canvas await drawShapes(ctx, computedShapes, { drawImage, }); const imageDataOut = ctx.getImageData(0, 0, canvas.width, canvas.height); releaseCanvas(canvas); return imageDataOut; }; var fillImageData = async (imageData, options = {}) => { const { backgroundColor } = options; // no background color set or is fully transparent background color if (!backgroundColor || (backgroundColor && backgroundColor[3] === 0)) return imageData; // fill let imageDataOut; let image = h('canvas'); image.width = imageData.width; image.height = imageData.height; const ctx = image.getContext('2d'); ctx.putImageData(imageData, 0, 0); // fill behind image ctx.globalCompositeOperation = 'destination-over'; ctx.fillStyle = colorArrayToRGBA(backgroundColor); ctx.fillRect(0, 0, image.width, image.height); imageDataOut = ctx.getImageData(0, 0, image.width, image.height); releaseCanvas(image); return imageDataOut; }; var dotColorMatrix = (a, b) => { const res = new Array(20); // R res[0] = a[0] * b[0] + a[1] * b[5] + a[2] * b[10] + a[3] * b[15]; res[1] = a[0] * b[1] + a[1] * b[6] + a[2] * b[11] + a[3] * b[16]; res[2] = a[0] * b[2] + a[1] * b[7] + a[2] * b[12] + a[3] * b[17]; res[3] = a[0] * b[3] + a[1] * b[8] + a[2] * b[13] + a[3] * b[18]; res[4] = a[0] * b[4] + a[1] * b[9] + a[2] * b[14] + a[3] * b[19] + a[4]; // G res[5] = a[5] * b[0] + a[6] * b[5] + a[7] * b[10] + a[8] * b[15]; res[6] = a[5] * b[1] + a[6] * b[6] + a[7] * b[11] + a[8] * b[16]; res[7] = a[5] * b[2] + a[6] * b[7] + a[7] * b[12] + a[8] * b[17]; res[8] = a[5] * b[3] + a[6] * b[8] + a[7] * b[13] + a[8] * b[18]; res[9] = a[5] * b[4] + a[6] * b[9] + a[7] * b[14] + a[8] * b[19] + a[9]; // B res[10] = a[10] * b[0] + a[11] * b[5] + a[12] * b[10] + a[13] * b[15]; res[11] = a[10] * b[1] + a[11] * b[6] + a[12] * b[11] + a[13] * b[16]; res[12] = a[10] * b[2] + a[11] * b[7] + a[12] * b[12] + a[13] * b[17]; res[13] = a[10] * b[3] + a[11] * b[8] + a[12] * b[13] + a[13] * b[18]; res[14] = a[10] * b[4] + a[11] * b[9] + a[12] * b[14] + a[13] * b[19] + a[14]; // A res[15] = a[15] * b[0] + a[16] * b[5] + a[17] * b[10] + a[18] * b[15]; res[16] = a[15] * b[1] + a[16] * b[6] + a[17] * b[11] + a[18] * b[16]; res[17] = a[15] * b[2] + a[16] * b[7] + a[17] * b[12] + a[18] * b[17]; res[18] = a[15] * b[3] + a[16] * b[8] + a[17] * b[13] + a[18] * b[18]; res[19] = a[15] * b[4] + a[16] * b[9] + a[17] * b[14] + a[18] * b[19] + a[19]; return res; }; var getColorMatrixFromColorMatrices = (colorMatrices) => colorMatrices.length ? colorMatrices.reduce((previous, current) => dotColorMatrix([...previous], current), colorMatrices.shift()) : []; var roundFraction = (value, fr = 2) => Math.round(value * fr) / fr; var getImageRedactionScaleFactor = (imageSize, redactionShapes) => { const imageRes = imageSize.width * imageSize.height; const maxShapeSize = redactionShapes.reduce((max, shape) => { if (shape.width > max.width && shape.height > max.height) { max.width = shape.width; max.height = shape.height; } return max; }, { width: 0, height: 0 }); const maxShapeRes = maxShapeSize.width * maxShapeSize.height; const fraction = Math.max(0.5, 0.5 + (1 - maxShapeRes / imageRes) / 2); return roundFraction(fraction, 5); }; function noop() { } const identity = x => x; function assign(tar, src) { // @ts-ignore for (const k in src) tar[k] = src[k]; return tar; } function run(fn) { return fn(); } function blank_object() { return Object.create(null); } function run_all(fns) { fns.forEach(run); } function is_function(thing) { return typeof thing === 'function'; } function safe_not_equal(a, b) { return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); } function is_empty(obj) { return Object.keys(obj).length === 0; } function subscribe(store, ...callbacks) { if (store == null) { return noop; } const unsub = store.subscribe(...callbacks); return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub; } function get_store_value(store) { let value; subscribe(store, _ => value = _)(); return value; } function component_subscribe(component, store, callback) { component.$$.on_destroy.push(subscribe(store, callback)); } function create_slot(definition, ctx, $$scope, fn) { if (definition) { const slot_ctx = get_slot_context(definition, ctx, $$scope, fn); return definition[0](slot_ctx); } } function get_slot_context(definition, ctx, $$scope, fn) { return definition[1] && fn ? assign($$scope.ctx.slice(), definition[1](fn(ctx))) : $$scope.ctx; } function get_slot_changes(definition, $$scope, dirty, fn) { if (definition[2] && fn) { const lets = definition[2](fn(dirty)); if ($$scope.dirty === undefined) { return lets; } if (typeof lets === 'object') { const merged = []; const len = Math.max($$scope.dirty.length, lets.length); for (let i = 0; i < len; i += 1) { merged[i] = $$scope.dirty[i] | lets[i]; } return merged; } return $$scope.dirty | lets; } return $$scope.dirty; } function update_slot(slot, slot_definition, ctx, $$scope, dirty, get_slot_changes_fn, get_slot_context_fn) { const slot_changes = get_slot_changes(slot_definition, $$scope, dirty, get_slot_changes_fn); if (slot_changes) { const slot_context = get_slot_context(slot_definition, ctx, $$scope, get_slot_context_fn); slot.p(slot_context, slot_changes); } } function exclude_internal_props(props) { const result = {}; for (const k in props) if (k[0] !== '$') result[k] = props[k]; return result; } function compute_rest_props(props, keys) { const rest = {}; keys = new Set(keys); for (const k in props) if (!keys.has(k) && k[0] !== '$') rest[k] = props[k]; return rest; } function set_store_value(store, ret, value = ret) { store.set(value); return ret; } function action_destroyer(action_result) { return action_result && is_function(action_result.destroy) ? action_result.destroy : noop; } const is_client = typeof window !== 'undefined'; let now = is_client ? () => window.performance.now() : () => Date.now(); let raf = is_client ? cb => requestAnimationFrame(cb) : noop; const tasks = new Set(); function run_tasks(now) { tasks.forEach(task => { if (!task.c(now)) { tasks.delete(task); task.f(); } }); if (tasks.size !== 0) raf(run_tasks); } /** * Creates a new task that runs on each raf frame * until it returns a falsy value or is aborted */ function loop(callback) { let task; if (tasks.size === 0) raf(run_tasks); return { promise: new Promise(fulfill => { tasks.add(task = { c: callback, f: fulfill }); }), abort() { tasks.delete(task); } }; } function append(target, node) { target.appendChild(node); } function insert(target, node, anchor) { target.insertBefore(node, anchor || null); } function detach(node) { node.parentNode.removeChild(node); } function element(name) { return document.createElement(name); } function svg_element(name) { return document.createElementNS('http://www.w3.org/2000/svg', name); } function text(data) { return document.createTextNode(data); } function space() { return text(' '); } function empty() { return text(''); } function listen(node, event, handler, options) { node.addEventListener(event, handler, options); return () => node.removeEventListener(event, handler, options); } function prevent_default(fn) { return function (event) { event.preventDefault(); // @ts-ignore return fn.call(this, event); }; } function stop_propagation(fn) { return function (event) { event.stopPropagation(); // @ts-ignore return fn.call(this, event); }; } function attr(node, attribute, value) { if (value == null) node.removeAttribute(attribute); else if (node.getAttribute(attribute) !== value) node.setAttribute(attribute, value); } function set_attributes(node, attributes) { // @ts-ignore const descriptors = Object.getOwnPropertyDescriptors(node.__proto__); for (const key in attributes) { if (attributes[key] == null) { node.removeAttribute(key); } else if (key === 'style') { node.style.cssText = attributes[key]; } else if (key === '__value') { node.value = node[key] = attributes[key]; } else if (descriptors[key] && descriptors[key].set) { node[key] = attributes[key]; } else { attr(node, key, attributes[key]); } } } function children(element) { return Array.from(element.childNodes); } function set_data(text, data) { data = '' + data; if (text.wholeText !== data) text.data = data; } function set_input_value(input, value) { input.value = value == null ? '' : value; } function set_style(node, key, value, important) { node.style.setProperty(key, value, important ? 'important' : ''); } function custom_event(type, detail) { const e = document.createEvent('CustomEvent'); e.initCustomEvent(type, false, false, detail); return e; } class HtmlTag { constructor(anchor = null) { this.a = anchor; this.e = this.n = null; } m(html, target, anchor = null) { if (!this.e) { this.e = element(target.nodeName); this.t = target; this.h(html); } this.i(anchor); } h(html) { this.e.innerHTML = html; this.n = Array.from(this.e.childNodes); } i(anchor) { for (let i = 0; i < this.n.length; i += 1) { insert(this.t, this.n[i], anchor); } } p(html) { this.d(); this.h(html); this.i(this.a); } d() { this.n.forEach(detach); } } const active_docs = new Set(); let active = 0; // https://github.com/darkskyapp/string-hash/blob/master/index.js function hash(str) { let hash = 5381; let i = str.length; while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i); return hash >>> 0; } function create_rule(node, a, b, duration, delay, ease, fn, uid = 0) { const step = 16.666 / duration; let keyframes = '{\n'; for (let p = 0; p <= 1; p += step) { const t = a + (b - a) * ease(p); keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`; } const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`; const name = `__svelte_${hash(rule)}_${uid}`; const doc = node.ownerDocument; active_docs.add(doc); const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = doc.head.appendChild(element('style')).sheet); const current_rules = doc.__svelte_rules || (doc.__svelte_rules = {}); if (!current_rules[name]) { current_rules[name] = true; stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length); } const animation = node.style.animation || ''; node.style.animation = `${animation ? `${animation}, ` : ''}${name} ${duration}ms linear ${delay}ms 1 both`; active += 1; return name; } function delete_rule(node, name) { const previous = (node.style.animation || '').split(', '); const next = previous.filter(name ? anim => anim.indexOf(name) < 0 // remove specific animation : anim => anim.indexOf('__svelte') === -1 // remove all Svelte animations ); const deleted = previous.length - next.length; if (deleted) { node.style.animation = next.join(', '); active -= deleted; if (!active) clear_rules(); } } function clear_rules() { raf(() => { if (active) return; active_docs.forEach(doc => { const stylesheet = doc.__svelte_stylesheet; let i = stylesheet.cssRules.length; while (i--) stylesheet.deleteRule(i); doc.__svelte_rules = {}; }); active_docs.clear(); }); } let current_component; function set_current_component(component) { current_component = component; } function get_current_component() { if (!current_component) throw new Error('Function called outside component initialization'); return current_component; } function onMount(fn) { get_current_component().$$.on_mount.push(fn); } function afterUpdate(fn) { get_current_component().$$.after_update.push(fn); } function onDestroy(fn) { get_current_component().$$.on_destroy.push(fn); } function createEventDispatcher() { const component = get_current_component(); return (type, detail) => { const callbacks = component.$$.callbacks[type]; if (callbacks) { // TODO are there situations where events could be dispatched // in a server (non-DOM) environment? const event = custom_event(type, detail); callbacks.slice().forEach(fn => { fn.call(component, event); }); } }; } function setContext(key, context) { get_current_component().$$.context.set(key, context); } function getContext(key) { return get_current_component().$$.context.get(key); } // TODO figure out if we still want to support // shorthand events, or if we want to implement // a real bubbling mechanism function bubble(component, event) { const callbacks = component.$$.callbacks[event.type]; if (callbacks) { callbacks.slice().forEach(fn => fn(event)); } } const dirty_components = []; const binding_callbacks = []; const render_callbacks = []; const flush_callbacks = []; const resolved_promise = Promise.resolve(); let update_scheduled = false; function schedule_update() { if (!update_scheduled) { update_scheduled = true; resolved_promise.then(flush); } } function add_render_callback(fn) { render_callbacks.push(fn); } function add_flush_callback(fn) { flush_callbacks.push(fn); } let flushing = false; const seen_callbacks = new Set(); function flush() { if (flushing) return; flushing = true; do { // first, call beforeUpdate functions // and update components for (let i = 0; i < dirty_components.length; i += 1) { const component = dirty_components[i]; set_current_component(component); update(component.$$); } set_current_component(null); dirty_components.length = 0; while (binding_callbacks.length) binding_callbacks.pop()(); // then, once components are updated, call // afterUpdate functions. This may cause // subsequent updates... for (let i = 0; i < render_callbacks.length; i += 1) { const callback = render_callbacks[i]; if (!seen_callbacks.has(callback)) { // ...so guard against infinite loops seen_callbacks.add(callback); callback(); } } render_callbacks.length = 0; } while (dirty_components.length); while (flush_callbacks.length) { flush_callbacks.pop()(); } update_scheduled = false; flushing = false; seen_callbacks.clear(); } function update($$) { if ($$.fragment !== null) { $$.update(); run_all($$.before_update); const dirty = $$.dirty; $$.dirty = [-1]; $$.fragment && $$.fragment.p($$.ctx, dirty); $$.after_update.forEach(add_render_callback); } } let promise; function wait() { if (!promise) { promise = Promise.resolve(); promise.then(() => { promise = null; }); } return promise; } function dispatch(node, direction, kind) { node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`)); } const outroing = new Set(); let outros; function group_outros() { outros = { r: 0, c: [], p: outros // parent group }; } function check_outros() { if (!outros.r) { run_all(outros.c); } outros = outros.p; } function transition_in(block, local) { if (block && block.i) { outroing.delete(block); block.i(local); } } function transition_out(block, local, detach, callback) { if (block && block.o) { if (outroing.has(block)) return; outroing.add(block); outros.c.push(() => { outroing.delete(block); if (callback) { if (detach) block.d(1); callback(); } }); block.o(local); } } const null_transition = { duration: 0 }; function create_bidirectional_transition(node, fn, params, intro) { let config = fn(node, params); let t = intro ? 0 : 1; let running_program = null; let pending_program = null; let animation_name = null; function clear_animation() { if (animation_name) delete_rule(node, animation_name); } function init(program, duration) { const d = program.b - t; duration *= Math.abs(d); return { a: t, b: program.b, d, duration, start: program.start, end: program.start + duration, group: program.group }; } function go(b) { const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition; const program = { start: now() + delay, b }; if (!b) { // @ts-ignore todo: improve typings program.group = outros; outros.r += 1; } if (running_program || pending_program) { pending_program = program; } else { // if this is an intro, and there's a delay, we need to do // an initial tick and/or apply CSS animation immediately if (css) { clear_animation(); animation_name = create_rule(node, t, b, duration, delay, easing, css); } if (b) tick(0, 1); running_program = init(program, duration); add_render_callback(() => dispatch(node, b, 'start')); loop(now => { if (pending_program && now > pending_program.start) { running_program = init(pending_program, duration); pending_program = null; dispatch(node, running_program.b, 'start'); if (css) { clear_animation(); animation_name = create_rule(node, t, running_program.b, running_program.duration, 0, easing, config.css); } } if (running_program) { if (now >= running_program.end) { tick(t = running_program.b, 1 - t); dispatch(node, running_program.b, 'end'); if (!pending_program) { // we're done if (running_program.b) { // intro — we can tidy up immediately clear_animation(); } else { // outro — needs to be coordinated if (!--running_program.group.r) run_all(running_program.group.c); } } running_program = null; } else if (now >= running_program.start) { const p = now - running_program.start; t = running_program.a + running_program.d * easing(p / running_program.duration); tick(t, 1 - t); } } return !!(running_program || pending_program); }); } } return { run(b) { if (is_function(config)) { wait().then(() => { // @ts-ignore config = config(); go(b); }); } else { go(b); } }, end() { clear_animation(); running_program = pending_program = null; } }; } const globals = (typeof window !== 'undefined' ? window : typeof globalThis !== 'undefined' ? globalThis : global); function destroy_block(block, lookup) { block.d(1); lookup.delete(block.key); } function outro_and_destroy_block(block, lookup) { transition_out(block, 1, 1, () => { lookup.delete(block.key); }); } function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list, lookup, node, destroy, create_each_block, next, get_context) { let o = old_blocks.length; let n = list.length; let i = o; const old_indexes = {}; while (i--) old_indexes[old_blocks[i].key] = i; const new_blocks = []; const new_lookup = new Map(); const deltas = new Map(); i = n; while (i--) { const child_ctx = get_context(ctx, list, i); const key = get_key(child_ctx); let block = lookup.get(key); if (!block) { block = create_each_block(key, child_ctx); block.c(); } else if (dynamic) { block.p(child_ctx, dirty); } new_lookup.set(key, new_blocks[i] = block); if (key in old_indexes) deltas.set(key, Math.abs(i - old_indexes[key])); } const will_move = new Set(); const did_move = new Set(); function insert(block) { transition_in(block, 1); block.m(node, next); lookup.set(block.key, block); next = block.first; n--; } while (o && n) { const new_block = new_blocks[n - 1]; const old_block = old_blocks[o - 1]; const new_key = new_block.key; const old_key = old_block.key; if (new_block === old_block) { // do nothing next = new_block.first; o--; n--; } else if (!new_lookup.has(old_key)) { // remove old block destroy(old_block, lookup); o--; } else if (!lookup.has(new_key) || will_move.has(new_key)) { insert(new_block); } else if (did_move.has(old_key)) { o--; } else if (deltas.get(new_key) > deltas.get(old_key)) { did_move.add(new_key); insert(new_block); } else { will_move.add(old_key); o--; } } while (o--) { const old_block = old_blocks[o]; if (!new_lookup.has(old_block.key)) destroy(old_block, lookup); } while (n) insert(new_blocks[n - 1]); return new_blocks; } function get_spread_update(levels, updates) { const update = {}; const to_null_out = {}; const accounted_for = { $$scope: 1 }; let i = levels.length; while (i--) { const o = levels[i]; const n = updates[i]; if (n) { for (const key in o) { if (!(key in n)) to_null_out[key] = 1; } for (const key in n) { if (!accounted_for[key]) { update[key] = n[key]; accounted_for[key] = 1; } } levels[i] = n; } else { for (const key in o) { accounted_for[key] = 1; } } } for (const key in to_null_out) { if (!(key in update)) update[key] = undefined; } return update; } function get_spread_object(spread_props) { return typeof spread_props === 'object' && spread_props !== null ? spread_props : {}; } function bind(component, name, callback) { const index = component.$$.props[name]; if (index !== undefined) { component.$$.bound[index] = callback; callback(component.$$.ctx[index]); } } function create_component(block) { block && block.c(); } function mount_component(component, target, anchor, customElement) { const { fragment, on_mount, on_destroy, after_update } = component.$$; fragment && fragment.m(target, anchor); if (!customElement) { // onMount happens before the initial afterUpdate add_render_callback(() => { const new_on_destroy = on_mount.map(run).filter(is_function); if (on_destroy) { on_destroy.push(...new_on_destroy); } else { // Edge case - component was destroyed immediately, // most likely as a result of a binding initialising run_all(new_on_destroy); } component.$$.on_mount = []; }); } after_update.forEach(add_render_callback); } function destroy_component(component, detaching) { const $$ = component.$$; if ($$.fragment !== null) { run_all($$.on_destroy); $$.fragment && $$.fragment.d(detaching); // TODO null out other refs, including component.$$ (but need to // preserve final state?) $$.on_destroy = $$.fragment = null; $$.ctx = []; } } function make_dirty(component, i) { if (component.$$.dirty[0] === -1) { dirty_components.push(component); schedule_update(); component.$$.dirty.fill(0); } component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); } function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) { const parent_component = current_component; set_current_component(component); const $$ = component.$$ = { fragment: null, ctx: null, // state props, update: noop, not_equal, bound: blank_object(), // lifecycle on_mount: [], on_destroy: [], on_disconnect: [], before_update: [], after_update: [], context: new Map(parent_component ? parent_component.$$.context : options.context || []), // everything else callbacks: blank_object(), dirty, skip_bound: false }; let ready = false; $$.ctx = instance ? instance(component, options.props || {}, (i, ret, ...rest) => { const value = rest.length ? rest[0] : ret; if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value); if (ready) make_dirty(component, i); } return ret; }) : []; $$.update(); ready = true; run_all($$.before_update); // `false` as a special case of no DOM component $$.fragment = create_fragment ? create_fragment($$.ctx) : false; if (options.target) { if (options.hydrate) { const nodes = children(options.target); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion $$.fragment && $$.fragment.l(nodes); nodes.forEach(detach); } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion $$.fragment && $$.fragment.c(); } if (options.intro) transition_in(component.$$.fragment); mount_component(component, options.target, options.anchor, options.customElement); flush(); } set_current_component(parent_component); } /** * Base class for Svelte components. Used when dev=false. */ class SvelteComponent { $destroy() { destroy_component(this, 1); this.$destroy = noop; } $on(type, callback) { const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); callbacks.push(callback); return () => { const index = callbacks.indexOf(callback); if (index !== -1) callbacks.splice(index, 1); }; } $set($$props) { if (this.$$set && !is_empty($$props)) { this.$$.skip_bound = true; this.$$set($$props); this.$$.skip_bound = false; } } } const subscriber_queue = []; /** * Creates a `Readable` store that allows reading by subscription. * @param value initial value * @param {StartStopNotifier}start start and stop notifications for subscriptions */ function readable(value, start) { return { subscribe: writable(value, start).subscribe }; } /** * Create a `Writable` store that allows both updating and reading by subscription. * @param {*=}value initial value * @param {StartStopNotifier=}start start and stop notifications for subscriptions */ function writable(value, start = noop) { let stop; const subscribers = []; function set(new_value) { if (safe_not_equal(value, new_value)) { value = new_value; if (stop) { // store is ready const run_queue = !subscriber_queue.length; for (let i = 0; i < subscribers.length; i += 1) { const s = subscribers[i]; s[1](); subscriber_queue.push(s, value); } if (run_queue) { for (let i = 0; i < subscriber_queue.length; i += 2) { subscriber_queue[i][0](subscriber_queue[i + 1]); } subscriber_queue.length = 0; } } } } function update(fn) { set(fn(value)); } function subscribe(run, invalidate = noop) { const subscriber = [run, invalidate]; subscribers.push(subscriber); if (subscribers.length === 1) { stop = start(set) || noop; } run(value); return () => { const index = subscribers.indexOf(subscriber); if (index !== -1) { subscribers.splice(index, 1); } if (subscribers.length === 0) { stop(); stop = null; } }; } return { set, update, subscribe }; } function derived(stores, fn, initial_value) { const single = !Array.isArray(stores); const stores_array = single ? [stores] : stores; const auto = fn.length < 2; return readable(initial_value, (set) => { let inited = false; const values = []; let pending = 0; let cleanup = noop; const sync = () => { if (pending) { return; } cleanup(); const result = fn(single ? values[0] : values, set); if (auto) { set(result); } else { cleanup = is_function(result) ? result : noop; } }; const unsubscribers = stores_array.map((store, i) => subscribe(store, (value) => { values[i] = value; pending &= ~(1 << i); if (inited) { sync(); } }, () => { pending |= (1 << i); })); inited = true; sync(); return function stop() { run_all(unsubscribers); cleanup(); }; }); } var mergeObjects = (objects) => objects.reduce((prev, curr) => Object.assign(prev, curr), {}); // @ts-ignore const UPDATE_VALUE = (updateValue) => ({ updateValue }); const DEFAULT_VALUE = (defaultValue) => ({ defaultValue }); const CUSTOM_STORE = (fn) => ({ store: fn }); // @ts-ignore const DERIVED_STORE = (fn) => ({ store: (defaultValue, stores) => derived(...fn(stores)) }); const UNIQUE_DERIVED_STORE = (fn) => ({ store: (defaultValue, stores) => { const [selectedStores, update, isEqual = () => false] = fn(stores); let isFirst = true; let currentValue; return derived(selectedStores, (storeValues, set) => { update(storeValues, (value) => { if (!isFirst && isEqual(currentValue, value)) return; currentValue = value; isFirst = false; set(value); }); }); }, }); const MAP_STORE = (fn) => ({ store: (defaultValue, stores) => { const [valueMapper, observedStores = {}, sorter = undefined] = fn(stores); let storedItems = []; let $observedStores = {}; const mapValue = (item) => valueMapper(item, $observedStores); // set default properties for each item const setValue = (items) => { // was empty, still empty if (!storedItems.length && !items.length) return; // update value storedItems = items; updateValue(); }; const updateValue = () => { const mappedItems = storedItems.map(mapValue); if (sorter) mappedItems.sort(sorter); storedItems = [...mappedItems]; set(mappedItems); }; // TODO: need to at some point unsub from these stores Object.entries(observedStores).map(([name, store]) => { return store.subscribe((value) => { $observedStores[name] = value; if (!value) return; updateValue(); }); }); const { subscribe, set } = writable(defaultValue || []); return { set: setValue, update: (fn) => setValue(fn(storedItems)), subscribe, }; }, }); const createStore = (accessors, stores, options) => { const { store = (defaultValue) => writable(defaultValue), defaultValue = noop$1, // should be a function returning the default value updateValue = undefined, } = options; // create our private store const storeInstance = store(defaultValue(), stores, accessors); const { subscribe, update = noop$1 } = storeInstance; // update = noop because not all stores can be updated // on update private store let unsub; const onUpdate = (cb) => { let ignoreFirstCallback = true; if (unsub) unsub(); unsub = subscribe((value) => { // need to ignore first callback because that returns current value if (ignoreFirstCallback) return (ignoreFirstCallback = false); // now we have the newly assigned value cb(value); unsub(); unsub = undefined; }); }; // create the value updater function, needs access to stores so can read all store values const updateStoreValue = updateValue ? updateValue(accessors) : passthrough; // set and validate value storeInstance.set = (nextValue) => update((previousValue) => updateStoreValue(nextValue, previousValue, onUpdate)); // set default value for external reference storeInstance.defaultValue = defaultValue; // expose store api return storeInstance; }; var createStores = (props) => { const stores = {}; const accessors = {}; props.forEach(([name, ...options]) => { const opts = mergeObjects(options); const store = (stores[name] = createStore(accessors, stores, opts)); const property = { get: () => get_store_value(store), set: store.set, }; Object.defineProperty(accessors, name, property); }); return { stores, accessors, }; }; var props = [ // io ['src'], ['imageReader'], ['imageWriter'], // will process markup items before rendering, used by arrows and frames ['shapePreprocessor'], // will scramble image data for use with image redaction logic ['imageScrambler'], // current images ['images', DEFAULT_VALUE(() => [])], ]; var capitalizeFirstLetter = (str) => str.charAt(0).toUpperCase() + str.slice(1); var defineMethods = (object, api) => { Object.keys(api).forEach((name) => { const descriptor = isFunction(api[name]) ? { value: api[name], writable: false, } : api[name]; Object.defineProperty(object, name, descriptor); }); }; const scalar = 10000; var offsetRectToFitPolygon = (rect, poly) => { const polyLines = quadLines(poly); const offset = vectorCreateEmpty(); const rectVertexes = rectGetCorners(rect); // we can fit it rectVertexes.forEach((vertex) => { // we update each corner by adding the current offset vectorAdd(vertex, offset); // test if point lies in polygon, if so, all is fine and we can exit if (pointInPoly(vertex, poly)) return; polyLines.forEach((line) => { // get angle of edge and draw a ray from the corner perpendicular to the edge const a = Math.atan2(line.start.y - line.end.y, line.start.x - line.end.x); const x = Math.sin(Math.PI - a) * scalar; const y = Math.cos(Math.PI - a) * scalar; const ray = vectorCreate(vertex.x + x, vertex.y + y); // extend the poly line so even if we overshoot the polygon we hit it const lineExtended = lineExtend(lineClone(line), scalar); // get the resulting intersection (there's always an intersection) const intersection = lineLineIntersection(lineCreate(vertex, ray), lineExtended); // no intersection, no need to do anything if (!intersection) return; // update offset to move towards image vectorAdd(offset, vectorSubtract(vectorClone(intersection), vertex)); }); }); // test if any vertexes still fall outside of poly, if so, we can't fit the rect const rectOffset = rectClone(rect); vectorAdd(rectOffset, offset); const rectOffsetVertices = rectGetCorners(rectOffset); const fits = rectOffsetVertices.every((vertex) => pointInPoly(vertex, poly)); if (fits) { rectUpdateWithRect(rect, rectOffset); return true; } return false; }; var limitCropRectToImage = (rect, poly) => { // get crop rect polygon vertexes const rectVertexes = rectGetCorners(rect); // if we end up here it doesn't fit, we might need to adjust const polyLines = quadLines(poly) // extend the poly lines a tiny bit so we // don't shoot rays between line gaps at corners // this caused one intersection to be missing resulting // in error while manipulating crop edges // (rotate image 90degrees -> drag bottom edge) (2021-04-09) .map((line) => lineExtend(line, 5)); const rectCenterPosition = rectCenter(rect); const intersections = []; rectVertexes.forEach((rectVertex) => { const ray = lineMultiply(lineCreate(vectorClone(rectCenterPosition), vectorClone(rectVertex)), 1000000); let intersectionFound = false; polyLines.map(lineClone).forEach((line) => { const intersection = lineLineIntersection(ray, line); if (!intersection || intersectionFound) return; intersections.push(intersection); intersectionFound = true; }); }); // top left -> bottom right const tlbr = vectorDistance(intersections[0], intersections[2]); // top right -> bottom left const trbl = vectorDistance(intersections[1], intersections[3]); // calculate smallest rectangle we can make, use that const rectLimitedVertices = tlbr < trbl ? [intersections[0], intersections[2]] : [intersections[1], intersections[3]]; const rectLimitedToImage = rectCreateFromPoints(rectLimitedVertices); // only use our fitted crop rectangle if it's smaller than our current rectangle, // this would mean that our current rectangle couldn't be moved to make it fit if (rectLimitedToImage.width < rect.width) { // need to center on previous rect rectUpdateWithRect(rect, rectLimitedToImage); return true; } return false; }; var getImagePolygon = (image, imageRotation, imagePerspective = { x: 0, y: 0 }) => { const imageRect = rectCreateFromSize(image); const imageCenter = rectCenter(imageRect); const imagePoly = rectApplyPerspective(imageRect, imagePerspective, imageCenter).map((imageVertex) => vectorRotate(imageVertex, imageRotation, imageCenter)); // get image poly bounds, we need this to offset the poly vertices from 0,0 const imagePolyBounds = rectCreateFromPoints(imagePoly); // get image polygon vertexes return imagePoly.map((imageVertex) => vectorSubtract(imageVertex, imagePolyBounds)); }; var getMaxSizeInRect = (size, rotation = 0, aspectRatio = rectAspectRatio(size)) => { let width; let height; if (rotation !== 0) { const innerAngle = Math.atan2(1, aspectRatio); const rotationSigned = Math.sign(rotation) * rotation; const rotationSignedMod = rotationSigned % Math.PI; const rotationSignedModHalf = rotationSigned % HALF_PI; // determine if is turned on side let hyp; let r; if (rotationSignedMod > QUART_PI && rotationSignedMod < HALF_PI + QUART_PI) { r = rotationSignedModHalf > QUART_PI ? rotationSigned : HALF_PI - rotationSignedModHalf; } else { r = rotationSignedModHalf > QUART_PI ? HALF_PI - rotationSignedModHalf : rotationSigned; } hyp = Math.min(Math.abs(size.height / Math.sin(innerAngle + r)), Math.abs(size.width / Math.cos(innerAngle - r))); width = Math.cos(innerAngle) * hyp; height = width / aspectRatio; } else { width = size.width; height = width / aspectRatio; if (height > size.height) { height = size.height; width = height * aspectRatio; } } return sizeCreate(width, height); }; var limitRectToImage = (rect, imageSize, imageRotation = 0, imagePerspective = vectorCreateEmpty(), minSize) => { // rotation and/or perspective, let's use the "advanced" collision detection method if ((isNumber(imageRotation) && imageRotation !== 0) || imagePerspective.x || imagePerspective.y) { const inputAspectRatio = rectAspectRatio(rect); // test if crop can fit image, if it can, offset the crop so it fits const imagePolygon = getImagePolygon(imageSize, imageRotation, imagePerspective); const maxSizeInRect = getMaxSizeInRect(imageSize, imageRotation, inputAspectRatio); const canFit = rect.width < maxSizeInRect.width && rect.height < maxSizeInRect.height; if (!canFit) { const dx = rect.width * 0.5 - maxSizeInRect.width * 0.5; const dy = rect.height * 0.5 - maxSizeInRect.height * 0.5; // adjust crop rect to max size if (rect.width > maxSizeInRect.width) { rect.width = maxSizeInRect.width; rect.x += dx; } if (rect.height > maxSizeInRect.height) { rect.height = maxSizeInRect.height; rect.y += dy; } // test if has exceeded min size, if so we need to limit the size and recalculate the other edge /* -\ / ---\ h2 ---\ / ---\ +--------w---------+\ /| | ---\ / | | ---\ / | | ---\ / | | -- h1 | | / / | | / / | | / -\ | | / ---\ | | / --+------------------+ / ---\ / --\ / ---\ / ---\ / ---\ / -- */ } offsetRectToFitPolygon(rect, imagePolygon); const wasLimited = limitCropRectToImage(rect, imagePolygon); // this makes sure that after limiting the size, the crop rect is moved to a position that is inside the image if (wasLimited) offsetRectToFitPolygon(rect, imagePolygon); } // no rotation, no perspective, use simple bounds method else { // remember intended aspect ratio so we can try and recreate it let intendedAspectRatio = rectAspectRatio(rect); // limit to image size first, can never exceed that rect.width = Math.min(rect.width, imageSize.width); rect.height = Math.min(rect.height, imageSize.height); // reposition rect so it's always inside image bounds rect.x = Math.max(rect.x, 0); if (rect.x + rect.width > imageSize.width) { rect.x -= rect.x + rect.width - imageSize.width; } rect.y = Math.max(rect.y, 0); if (rect.y + rect.height > imageSize.height) { rect.y -= rect.y + rect.height - imageSize.height; } // we get the center of the current rect so we can center the contained rect to it const intendedCenter = rectCenter(rect); // make sure still adheres to aspect ratio const containedRect = rectContainRect(rect, intendedAspectRatio); containedRect.width = Math.max(minSize.width, containedRect.width); containedRect.height = Math.max(minSize.height, containedRect.height); containedRect.x = intendedCenter.x - containedRect.width * 0.5; containedRect.y = intendedCenter.y - containedRect.height * 0.5; rectUpdateWithRect(rect, containedRect); } }; var applyCropRectAction = (cropRectPrevious, cropRectNext, imageSize, imageRotation, imagePerspective, cropLimitToImageBounds, cropMinSize, cropMaxSize) => { // clone const minSize = sizeClone(cropMinSize); // set upper bounds to crop max size const maxSize = sizeClone(cropMaxSize); // limit max size (more important that min size is respected so first limit max size) const maxScalar = fixPrecision(Math.max(cropRectNext.width / maxSize.width, cropRectNext.height / maxSize.height)); const minScalar = fixPrecision(Math.min(cropRectNext.width / minSize.width, cropRectNext.height / minSize.height)); // clone for resulting crop rect const cropRectOut = rectClone(cropRectNext); // // if exceeds min or max scale correct next crop rectangle to conform to bounds // if (minScalar < 1 || maxScalar > 1) { // center of both previous and next crop rects const previousCropRectCenter = rectCenter(cropRectPrevious); const nextCropRectCenter = rectCenter(cropRectNext); // calculate scales const scalar = minScalar < 1 ? minScalar : maxScalar; const cx = (nextCropRectCenter.x + previousCropRectCenter.x) / 2; const cy = (nextCropRectCenter.y + previousCropRectCenter.y) / 2; const cw = cropRectOut.width / scalar; const ch = cropRectOut.height / scalar; rectUpdate(cropRectOut, cx - cw * 0.5, cy - ch * 0.5, cw, ch); } // no need to limit to bounds, let's go! if (!cropLimitToImageBounds) return { crop: cropRectOut, }; // // make sure the crop is made inside the bounds of the image // limitRectToImage(cropRectOut, imageSize, imageRotation, imagePerspective, minSize); return { crop: cropRectOut, }; }; var getBaseCropRect = (imageSize, transformedCropRect, imageRotation) => { const imageRect = rectCreateFromSize(imageSize); const imageCenter = rectCenter(imageRect); const imageTransformedVertices = rectRotate(imageRect, imageRotation, imageCenter); // get the rotated image bounds center (offset isn't relevant as crop is relative to top left image position) const imageRotatedBoundsCenter = rectCenter(rectNormalizeOffset(rectCreateFromPoints(imageTransformedVertices))); // get the center of the crop inside the rotated image const cropCenterInTransformedImage = rectCenter(transformedCropRect); // invert the rotation of the crop center around the rotated image center const deRotatedCropCenter = vectorRotate(cropCenterInTransformedImage, -imageRotation, imageRotatedBoundsCenter); // calculate crop distance from rotated image center const cropFromCenterOfTransformedImage = vectorSubtract(deRotatedCropCenter, imageRotatedBoundsCenter); // calculate original crop offset (from untransformed image) const originalCropCenterOffset = vectorApply(vectorAdd(imageCenter, cropFromCenterOfTransformedImage), fixPrecision); return rectCreate(originalCropCenterOffset.x - transformedCropRect.width * 0.5, originalCropCenterOffset.y - transformedCropRect.height * 0.5, transformedCropRect.width, transformedCropRect.height); }; var clamp = (value, min, max) => Math.max(min, Math.min(value, max)); var applyRotationAction = (imageRotationPrevious, imageRotation, imageRotationRange, cropRect, imageSize, imagePerspective, cropLimitToImageBounds, cropRectOrigin, cropMinSize, cropMaxSize) => { // clone const minSize = sizeClone(cropMinSize); // set upper bounds to crop max size if image is bigger than max size, // else if should limit to image bounds use image size as limit const maxSize = sizeClone(cropMaxSize); if (cropLimitToImageBounds) { maxSize.width = Math.min(cropMaxSize.width, imageSize.width); maxSize.height = Math.min(cropMaxSize.height, imageSize.height); } let didAttemptDoubleTurn = false; const rotate = (rotationPrevious, rotation) => { // get the base crop rect (position of crop rect in untransformed image) // if we have the base crop rect we can apply the new rotation to it const cropRectBase = getBaseCropRect(imageSize, cropRect, rotationPrevious); // calculate transforms based on new rotation and base crop rect const imageRect = rectCreateFromSize(imageSize); const imageCenter = rectCenter(imageRect); const imageTransformedCorners = rectApplyPerspective(imageRect, imagePerspective, imageCenter); // need this to correct for perspective centroid displacement const perspectiveOffset = vectorSubtract(vectorClone(imageCenter), convexPolyCentroid(imageTransformedCorners)); // rotate around center of image const cropCenter = vectorRotate(rectCenter(cropRectBase), rotation, imageCenter); const rotateCropOffset = vectorSubtract(vectorClone(imageCenter), cropCenter); // get center of image bounds and move to correct position imageTransformedCorners.forEach((imageVertex) => vectorRotate(imageVertex, rotation, imageCenter)); const imageBoundsRect = rectCreateFromPoints(imageTransformedCorners); const imageCentroid = convexPolyCentroid(imageTransformedCorners); const cropOffset = vectorAdd(vectorSubtract(vectorSubtract(imageCentroid, rotateCropOffset), imageBoundsRect), perspectiveOffset); // create output cropRect const cropRectOut = rectCreate(cropOffset.x - cropRectBase.width * 0.5, cropOffset.y - cropRectBase.height * 0.5, cropRectBase.width, cropRectBase.height); // if has size target, scale croprect to target size if (cropRectOrigin) { rectScale(cropRectOut, cropRectOrigin.width / cropRectOut.width); } // if should limit to image bounds if (cropLimitToImageBounds) { const imagePoly = getImagePolygon(imageSize, rotation, imagePerspective); // offsetRectToFitPolygon(cropRectOut, imagePoly); // commenting this fixes poly sliding problem when adjusting rotation limitCropRectToImage(cropRectOut, imagePoly); } //#region if exceeds min or max adjust rotation to conform to bounds const minScalar = fixPrecision(Math.min(cropRectOut.width / minSize.width, cropRectOut.height / minSize.height), 8); const maxScalar = fixPrecision(Math.max(cropRectOut.width / maxSize.width, cropRectOut.height / maxSize.height), 8); if (minScalar < 1 || maxScalar > 1) { // determine if is full image turn const isTurn = fixPrecision(Math.abs(rotation - rotationPrevious)) === fixPrecision(Math.PI / 2); // try another turn if is turning image if (isTurn && !didAttemptDoubleTurn) { didAttemptDoubleTurn = true; return rotate(imageRotationPrevious, imageRotationPrevious + Math.sign(rotation - rotationPrevious) * Math.PI); } } //#endregion return { rotation, crop: rectApply(cropRectOut, (v) => fixPrecision(v, 8)), }; }; // amount of turns applied, we need this to correctly determine the allowed rotation range const imageTurns = Math.sign(imageRotation) * Math.round(Math.abs(imageRotation) / HALF_PI) * HALF_PI; const imageRotationClamped = clamp(imageRotation, imageTurns + imageRotationRange[0], imageTurns + imageRotationRange[1]); // set new crop position return rotate(imageRotationPrevious, imageRotationClamped); }; // @ts-ignore const ORDERED_STATE_PROPS = [ // requirements 'cropLimitToImage', 'cropMinSize', 'cropMaxSize', 'cropAspectRatio', // selection -> flip -> rotate -> perspective -> crop 'flipX', 'flipY', 'rotation', 'crop', // 'perspectiveX', // 'perspectiveY', // effects 'colorMatrix', 'convolutionMatrix', 'gamma', 'vignette', // 'noise', // shapes 'redaction', 'annotation', 'decoration', 'frame', // other 'backgroundColor', 'targetSize', 'metadata', ]; const clone = (value) => { if (isArray(value)) { return value.map(clone); } else if (isObject(value)) { return { ...value }; } return value; }; const filterShapeState = (shapes) => shapes.map((shape) => Object.entries(shape).reduce((copy, [key, value]) => { if (key.startsWith('_')) return copy; copy[key] = value; return copy; }, {})); var stateStore = (_, stores, accessors) => { const observedStores = ORDERED_STATE_PROPS.map((key) => stores[key]); // can only subscribe, setting is done directly through store accessors // @ts-ignore const { subscribe } = derived(observedStores, (values, set) => { // create new state by looping over props in certain order const state = ORDERED_STATE_PROPS.reduce((prev, curr, i) => { prev[curr] = clone(values[i]); return prev; }, {}); // round crop if defined state.crop && rectApply(state.crop, Math.round); // remove internal state props from decoration and annotation state.redaction = state.redaction && filterShapeState(state.redaction); state.annotation = state.annotation && filterShapeState(state.annotation); state.decoration = state.decoration && filterShapeState(state.decoration); // set new state set(state); }); const setState = (state) => { // requires at least some state to be supplied if (!state) return; // make sure crop origin is reset accessors.cropOrigin = undefined; // apply new values ORDERED_STATE_PROPS // remove keys that weren't set .filter((key) => hasProp(state, key)) // apply each key in order .forEach((key) => { accessors[key] = clone(state[key]); }); }; return { set: setState, update: (fn) => setState(fn(null)), subscribe, }; }; var toNumericAspectRatio = (v) => { if (!v) return undefined; if (/:/.test(v)) { const [w, h] = v.split(':'); return w / h; } return parseFloat(v); }; var arrayEqual = (a, b) => { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; }; var padColorArray = (color = [0, 0, 0, 0], opacity = 1.0) => color.length === 4 ? color : [...color, opacity]; // // constants // const MIN_ROTATION = -QUART_PI; const MAX_ROTATION = QUART_PI; // // helper methods // const isCropCentered = (crop, imageSize, imageRotation) => { const cropCenter = vectorApply(rectCenter(crop), (v) => fixPrecision(v, 8)); const imageRect = rectCreateFromSize(imageSize); const imageCenter = rectCenter(imageRect); const imageRotatedVertices = rectRotate(imageRect, imageRotation, imageCenter); const imageBoundsCenter = vectorApply(sizeCenter(rectCreateFromPoints(imageRotatedVertices)), (v) => fixPrecision(v, 8)); const dx = Math.abs(imageBoundsCenter.x - cropCenter.x); const dy = Math.abs(imageBoundsCenter.y - cropCenter.y); return dx < 1 && dy < 1; }; const isCropMaxSize = (cropRect, imageSize, rotation) => { const maxSize = getMaxSizeInRect(imageSize, rotation, rectAspectRatio(cropRect)); return sizeEqual(sizeApply(maxSize, Math.round), sizeApply(sizeClone(cropRect), Math.round)); }; // // updater methods // const updateCropRect = (props) => (cropNext, cropPrevious = cropNext) => { // wait for image to load const { loadState, size, cropMinSize, cropMaxSize, cropLimitToImage, cropAspectRatio, rotation, perspective, } = props; // image hasn't loaded yet, use supplied crop rect if ((!cropNext && !cropPrevious) || !loadState || !loadState.beforeComplete) return cropNext; // crop previous set, crop next set to undefined, set crop to fit image if (!cropNext) cropNext = rectCreateFromSize(getMaxSizeInRect(size, rotation, cropAspectRatio || rectAspectRatio(size))); // apply the action const res = applyCropRectAction(cropPrevious, cropNext, size, rotation, perspective, cropLimitToImage, cropMinSize, cropMaxSize); const cropOut = rectApply(res.crop, (v) => fixPrecision(v, 8)); return cropOut; }; const updateCropAspectRatio = (props) => (aspectRatioNext, aspectRatioPrevious) => { const { loadState, crop, size, rotation, cropLimitToImage } = props; const aspectRatio = toNumericAspectRatio(aspectRatioNext); // no aspect ratio means custom aspect ratio so set to undefined if (!aspectRatio) return undefined; // can't update crop if not defined yet if (!crop || !loadState || !loadState.beforeComplete) return aspectRatio; // calculate difference between aspect ratios, if big difference, re-align in image const aspectRatioDist = aspectRatioPrevious ? Math.abs(aspectRatioNext - aspectRatioPrevious) : 1; // if crop centered scale up if (isCropCentered(crop, size, rotation) && cropLimitToImage && aspectRatioDist >= 0.1) { const imageSize = sizeTurn(sizeClone(size), rotation); props.crop = rectApply(rectContainRect(rectCreateFromSize(imageSize), aspectRatioNext), fixPrecision); } else { const cropSize = { width: crop.height * aspectRatio, height: crop.height, }; const tx = (crop.width - cropSize.width) * 0.5; const ty = (crop.height - cropSize.height) * 0.5; props.crop = rectApply(rectCreate(crop.x + tx, crop.y + ty, cropSize.width, cropSize.height), fixPrecision); } return aspectRatio; }; const updateCropLimitToImage = (props) => (limitToImageNext, limitToImagePrevious, onUpdate) => { // skip if no crop defined const { crop } = props; if (!crop) return limitToImageNext; // if was not limiting previously and now set limiting make sure crop fits bounds if (!limitToImagePrevious && limitToImageNext) { onUpdate(() => (props.crop = rectClone(props.crop))); } return limitToImageNext; }; const updateRotation = (props) => (rotationNext, rotationPrevious, onUpdate) => { // when image rotation is updated we need to adjust the // cropRect offset so rotation happens from cropRect center // no change if (rotationNext === rotationPrevious) return rotationNext; // get relevant data from store state const { loadState, size, rotationRange, cropMinSize, cropMaxSize, crop, perspective, cropLimitToImage, cropOrigin, } = props; // image not ready, exit! if (!crop || !loadState || !loadState.beforeComplete) return rotationNext; // remember if current crop was at max size and centered, if so, resulting crop should also be at max size const cropWasAtMaxSize = isCropMaxSize(crop, size, rotationPrevious); const cropWasCentered = isCropCentered(crop, size, rotationPrevious); // get new state const res = applyRotationAction(rotationPrevious, rotationNext, rotationRange, crop, size, perspective, cropLimitToImage, cropOrigin, cropMinSize, cropMaxSize); // if is centered, and initial crop was at max size expand crop to max size if (cropWasAtMaxSize && cropWasCentered) { const rect = getMaxSizeInRect(size, rotationNext, rectAspectRatio(res.crop)); // move top left corner res.crop.x += res.crop.width * 0.5; res.crop.y += res.crop.height * 0.5; res.crop.x -= rect.width * 0.5; res.crop.y -= rect.height * 0.5; // update size to max size res.crop.width = rect.width; res.crop.height = rect.height; } // return validated rotation value, then, after we assign that value, we update the crop rect // we may only call onUpdate if a change was made onUpdate(() => { props.crop = rectApply(res.crop, (v) => fixPrecision(v, 8)); }); // return result rotation (might have been rotated twice to fit crop rectangle) return res.rotation; }; // updates the range of valid rotation input const updateRotationRange = (imageSize, imageIsRotatedSideways, cropMinSize, cropSize, cropLimitToImage) => { if (!cropLimitToImage) return [MIN_ROTATION, MAX_ROTATION]; /* - 'a' is angle between diagonal and image height - 'b' is angle between diagonal and crop height - 'c' is angle between diagonal and image width - resulting range is then a - b +----------/\------------------------+ | / \ \ | | / \ \ | | / \ \ | | \ \ \ | | \ \ \ | | \ \ \ | | \ \ / | | \ \ / | | \ \ / | | \\a/b | +---------------------\--------------+ */ const scalar = Math.max(cropMinSize.width / cropSize.width, cropMinSize.height / cropSize.height); const minSize = sizeCreate(cropSize.width * scalar, cropSize.height * scalar); // the hypotenus is the length of the diagonal of the min crop size const requiredSpace = sizeHypotenuse(minSize); // minimum space available in horizontal / vertical direction const availableSpace = Math.min(imageSize.width, imageSize.height); // if there's enough space available, we can return the max range if (requiredSpace < availableSpace) return [MIN_ROTATION, MAX_ROTATION]; // if the image is turned we need to swap the width and height const imageWidth = imageIsRotatedSideways ? imageSize.height : imageSize.width; const imageHeight = imageIsRotatedSideways ? imageSize.width : imageSize.height; // subtracting the angle between the hypotenuse and the crop itself const a = Math.acos(minSize.height / requiredSpace); const b = Math.acos(imageHeight / requiredSpace); const c = Math.asin(imageWidth / requiredSpace); const rangeHorizontal = a - b; const rangeVertical = c - a; // range is not a number, it means we can rotate as much as we want if (Number.isNaN(rangeHorizontal) && Number.isNaN(rangeVertical)) return [MIN_ROTATION, MAX_ROTATION]; // get minimum range const range = Number.isNaN(rangeHorizontal) ? rangeVertical : Number.isNaN(rangeVertical) ? rangeHorizontal : Math.min(rangeHorizontal, rangeVertical); // if not, limit range to min and max rotation const rangeMin = Math.max(-range, MIN_ROTATION); const rangeMax = Math.min(range, MAX_ROTATION); return [rangeMin, rangeMax]; }; // updates the range of valid crop rectangle input const updateCropRange = (imageSize, rotation, cropAspectRatio, cropMinSize, cropMaxSize, cropLimitToImage) => { // ! rotation doesn't affect min size, only max size // set lower bounds to crop min size const minSize = sizeClone(cropMinSize); // set upper bounds to crop max size const maxSize = sizeClone(cropMaxSize); // now we got basic bounds, let's see if we should limit to the image bounds, else we done if (!cropLimitToImage) return [minSize, maxSize]; return [minSize, sizeApply(getMaxSizeInRect(imageSize, rotation, cropAspectRatio), Math.round)]; }; const formatShape = (shape, options) => { const { context, props } = options; // only auto-format once if (!shape._isFormatted) { shape = shapeFormat(shape); shape._isFormatted = true; Object.assign(shape, props); } // we need to make sure shape is still correctly positioned relative to parent context // draft cannot be relative // if context changed // if has left top right or bottom if (!shape._isDraft && shapeHasRelativeSize(shape) && (!shape._context || !rectEqual(context, shape._context))) { shape = shapeComputeRect(shape, context); shape._context = { ...context }; } return shape; }; const updateFrame = () => (frameShapeNext) => { if (!frameShapeNext) return; const shape = { frameStyle: undefined, x: 0, y: 0, width: '100%', height: '100%', disableStyle: ['backgroundColor', 'strokeColor', 'strokeWidth'], }; if (isString(frameShapeNext)) { shape.frameStyle = frameShapeNext; } else { Object.assign(shape, frameShapeNext); } return shape; }; var imageProps = [ // image info received from read ['file'], ['size'], // loading and processing state ['loadState'], ['processState'], // derived info [ 'aspectRatio', DERIVED_STORE(({ size }) => [ size, ($size) => ($size ? rectAspectRatio($size) : undefined), ]), ], // image modifications ['perspectiveX', DEFAULT_VALUE(() => 0)], ['perspectiveY', DEFAULT_VALUE(() => 0)], [ 'perspective', DERIVED_STORE(({ perspectiveX, perspectiveY }) => [ [perspectiveX, perspectiveY], ([x, y]) => ({ x, y }), ]), ], ['rotation', DEFAULT_VALUE(() => 0), UPDATE_VALUE(updateRotation)], ['flipX', DEFAULT_VALUE(() => false)], ['flipY', DEFAULT_VALUE(() => false)], ['flip', DERIVED_STORE(({ flipX, flipY }) => [[flipX, flipY], ([x, y]) => ({ x, y })])], [ 'isRotatedSideways', UNIQUE_DERIVED_STORE(({ rotation }) => [ [rotation], ([$rotation], set) => set(isRotatedSideways($rotation)), (prevValue, nextValue) => prevValue !== nextValue, ]), ], ['crop', UPDATE_VALUE(updateCropRect)], ['cropAspectRatio', UPDATE_VALUE(updateCropAspectRatio)], ['cropOrigin'], ['cropMinSize', DEFAULT_VALUE(() => ({ width: 1, height: 1 }))], ['cropMaxSize', DEFAULT_VALUE(() => ({ width: 32768, height: 32768 }))], ['cropLimitToImage', DEFAULT_VALUE(() => true), UPDATE_VALUE(updateCropLimitToImage)], [ 'cropSize', UNIQUE_DERIVED_STORE(({ crop }) => [ [crop], ([$crop], set) => { if (!$crop) return; set(sizeCreate($crop.width, $crop.height)); }, // if is same as previous size, don't trigger update (happens when updating only the crop offset) (prevValue, nextValue) => sizeEqual(prevValue, nextValue), ]), ], [ 'cropRectAspectRatio', DERIVED_STORE(({ cropSize }) => [ [cropSize], ([$cropSize], set) => { if (!$cropSize) return; set(fixPrecision(rectAspectRatio($cropSize), 5)); }, ]), ], [ 'cropRange', UNIQUE_DERIVED_STORE(({ size, rotation, cropRectAspectRatio, cropMinSize, cropMaxSize, cropLimitToImage, }) => [ [size, rotation, cropRectAspectRatio, cropMinSize, cropMaxSize, cropLimitToImage], ([$size, $rotation, $cropRectAspectRatio, $cropMinSize, $cropMaxSize, $cropLimitToImage,], set) => { // wait for image size if (!$size) return; const range = updateCropRange($size, $rotation, $cropRectAspectRatio, $cropMinSize, $cropMaxSize, $cropLimitToImage); set(range); }, // if is same range as previous range, don't trigger update (prevRange, nextRange) => arrayEqual(prevRange, nextRange), ]), ], [ 'rotationRange', UNIQUE_DERIVED_STORE(({ size, isRotatedSideways, cropMinSize, cropSize, cropLimitToImage }) => [ [size, isRotatedSideways, cropMinSize, cropSize, cropLimitToImage], ([$size, $isRotatedSideways, $cropMinSize, $cropSize, $cropLimitToImage], set) => { // wait for image size if (!$size || !$cropSize) return; const range = updateRotationRange($size, $isRotatedSideways, $cropMinSize, $cropSize, $cropLimitToImage); set(range); }, // if is same range as previous range, don't trigger update (prevRange, nextRange) => arrayEqual(prevRange, nextRange), ]), ], // canvas ['backgroundColor', UPDATE_VALUE(() => (color) => padColorArray(color))], // size ['targetSize'], // effects ['colorMatrix'], ['convolutionMatrix'], ['gamma'], ['noise'], ['vignette'], // redaction lives in image space ['redaction', MAP_STORE(({ size }) => [formatShape, { context: size }])], // annotation lives in image space ['annotation', MAP_STORE(({ size }) => [formatShape, { context: size }])], // decoration lives in crop space ['decoration', MAP_STORE(({ crop }) => [formatShape, { context: crop }])], // frame to render on top of the image (or outside) ['frame', UPDATE_VALUE(updateFrame)], // custom metadata ['metadata'], // state of image, used to restore a previous state or request the current state ['state', CUSTOM_STORE(stateStore)], ]; var process = async (value, chainTasks, chainOptions = {}, processOptions) => { // options relevant to the process method itself const { ontaskstart, ontaskprogress, ontaskend, token } = processOptions; // has been cancelled let cancelled = false; // set cancel handler method token.cancel = () => { // cancel called from outside of the process method cancelled = true; }; // step through chain for (const [index, task] of chainTasks.entries()) { // exit when cancelled if (cancelled) return; // get the task function and the id so we can notify the callee of the task that is being started const [fn, id] = task; // start task ontaskstart(index, id); try { value = await fn(value, { ...chainOptions }, (event) => ontaskprogress(index, id, event)); } catch (err) { // stop processing more items in the chain cancelled = true; // pass error back to parent throw err; } ontaskend(index, id); } return value; }; // TODO: find better location for minSize / file load validation var createImageCore = ({ minSize = { width: 1, height: 1 } } = {}) => { // create default store const { stores, accessors } = createStores(imageProps); // pub/sub const { pub, sub } = pubsub(); // processing handler const createProcessingHandler = (stateProp, eventKey) => { const getStore = () => accessors[stateProp] || {}; const setStore = (obj) => (accessors[stateProp] = { ...getStore(), ...obj, timeStamp: Date.now(), }); const hasError = () => getStore().error; const handleError = (error) => { if (hasError()) return; setStore({ error: error, }); pub(`${eventKey}error`, { ...getStore() }); }; return { start() { pub(`${eventKey}start`); }, onabort() { setStore({ abort: true, }); pub(`${eventKey}abort`, { ...getStore() }); }, ontaskstart(index, id) { if (hasError()) return; setStore({ index, task: id, taskProgress: undefined, taskLengthComputable: undefined, }); pub(`${eventKey}taskstart`, { ...getStore() }); }, ontaskprogress(index, id, event) { if (hasError()) return; setStore({ index, task: id, taskProgress: event.loaded / event.total, taskLengthComputable: event.lengthComputable, }); pub(`${eventKey}taskprogress`, { ...getStore() }); pub(`${eventKey}progress`, { ...getStore() }); }, ontaskend(index, id) { if (hasError()) return; setStore({ index, task: id, }); pub(`${eventKey}taskend`, { ...getStore() }); }, ontaskerror(error) { handleError(error); }, error(error) { handleError(error); }, beforeComplete(data) { if (hasError()) return; setStore({ beforeComplete: true }); pub(`before${eventKey}`, data); }, complete(data) { if (hasError()) return; setStore({ complete: true }); pub(eventKey, data); }, }; }; //#region read image const read = (src, { reader }) => { // exit if no reader supplied if (!reader) return; // reset file data to undefined as we're loading a new image Object.assign(accessors, { file: undefined, size: undefined, loadState: undefined, }); // our cancel token so we can abort load if needed, cancel will be set by process let imageReadToken = { cancel: noop$1 }; let imageReadCancelled = false; const imageReadHandler = createProcessingHandler('loadState', 'load'); const processOptions = { token: imageReadToken, ...imageReadHandler, }; const readerState = { src, size: undefined, dest: undefined, }; const readerOptions = {}; // wait a tick before starting image read so the read can be cancelled in loadstart Promise.resolve().then(async () => { try { imageReadHandler.start(); if (imageReadCancelled) return imageReadHandler.onabort(); const output = (await process(readerState, reader, readerOptions, processOptions)); // was cancelled if (imageReadCancelled) return imageReadHandler.onabort(); // get shortcuts for validation const { size, dest } = output || {}; // if we don't have a size if (!size || !size.width || !size.height) throw new EditorError('Image size missing', 'IMAGE_SIZE_MISSING', output); // size of image is too small if (size.width < minSize.width || size.height < minSize.height) throw new EditorError('Image too small', 'IMAGE_TOO_SMALL', { ...output, minWidth: minSize.width, minHeight: minSize.height, }); // update internal data Object.assign(accessors, { size: size, file: dest, }); // before load complete imageReadHandler.beforeComplete(output); // done loading image imageReadHandler.complete(output); } catch (err) { imageReadHandler.error(err); } finally { imageReadToken = undefined; } }); // call to abort load return () => { imageReadCancelled = true; imageReadToken && imageReadToken.cancel(); imageReadHandler.onabort(); }; }; //#endregion //#region write image const write = (writer, options) => { // not ready to start processing if (!accessors.loadState.complete) return; // reset process state to undefined accessors.processState = undefined; const imageWriteHandler = createProcessingHandler('processState', 'process'); const writerState = { src: accessors.file, imageState: accessors.state, dest: undefined, }; // willProcessImageState if (!writer) { imageWriteHandler.start(); imageWriteHandler.complete(writerState); return; } // we need this token to be a blet to cancel the processing operation let imageWriteToken = { cancel: noop$1 }; let imageWriteCancelled = false; const writerOptions = options; const processOptions = { token: imageWriteToken, ...imageWriteHandler, }; // wait a tick before starting image write so the write can be cancelled in processtart Promise.resolve().then(async () => { try { imageWriteHandler.start(); if (imageWriteCancelled) return imageWriteHandler.onabort(); const output = (await process(writerState, writer, writerOptions, processOptions)); imageWriteHandler.complete(output); } catch (err) { imageWriteHandler.error(err); } finally { imageWriteToken = undefined; } }); // call to abort processing return () => { imageWriteCancelled = true; imageWriteToken && imageWriteToken.cancel(); }; }; //#endregion //#region api defineMethods(accessors, { read, write, on: sub, }); //#endregion // expose store API return { accessors, stores, }; }; // @ts-ignore const editorEventsToBubble = [ 'loadstart', 'loadabort', 'loaderror', 'loadprogress', 'load', 'processstart', 'processabort', 'processerror', 'processprogress', 'process', ]; const imagePrivateProps = [ 'flip', 'cropOrigin', 'isRotatedSideways', 'perspective', 'perspectiveX', 'perspectiveY', 'cropRange', ]; const editorPrivateProps = ['images']; const imagePublicProps = imageProps .map(([prop]) => prop) .filter((prop) => !imagePrivateProps.includes(prop)); const getImagePropGroupedName = (prop) => `image${capitalizeFirstLetter(prop)}`; const getEditorProps$1 = () => { const imageProperties = imagePublicProps.map(getImagePropGroupedName); const editorProperties = props .map(([prop]) => prop) .filter((prop) => !editorPrivateProps.includes(prop)); return imageProperties.concat(editorProperties); }; const isImageSource = (src) => isString(src) || isBinary(src) || isElement(src); const isImageState = (obj) => hasProp(obj, 'crop'); var createImageEditor = () => { // create default stores const { stores, accessors } = createStores(props); // set up pub/sub for the app layer const { sub, pub } = pubsub(); const bubble = (name) => (value) => pub(name, value); // helper method const getImageObjSafe = () => (accessors.images ? accessors.images[0] : {}); // initialImageProps is the list of transforms to apply when the image loads let initialImageProps = {}; // create shortcuts to image props : `crop` -> `imageCrop` imagePublicProps.forEach((prop) => { Object.defineProperty(accessors, getImagePropGroupedName(prop), { get: () => { // no image, can't get const image = getImageObjSafe(); if (!image) return; // return from image state return image.accessors[prop]; }, set: (value) => { // always use as initial prop when loading a new image without reset initialImageProps[getImagePropGroupedName(prop)] = value; // no image, we can't update const image = getImageObjSafe(); if (!image) return; // update the image immidiately image.accessors[prop] = value; }, }); }); // internal helper method to get active image const getImage = () => accessors.images && accessors.images[0]; // handling loading an image if a src is set const unsubSrc = stores.src.subscribe((src) => { // no image set, means clear active image if (!src) return (accessors.images = []); // exit here if we don't have an imageReader we'll wait for an imageReader to be defined if (!accessors.imageReader) return; // reset initial image props if an image is already loaded, so props applied to previous image aren't applied to the new one if (accessors.images.length) initialImageProps = {}; // load image in src prop loadSrc(src); }); const unsubReader = stores.imageReader.subscribe((reader) => { // can't do anything without an image reader if (!reader) return; // an image has already been loaded no need to load images that were set earlier if (accessors.images.length) return; // no image to load, we'll wait for images to be set to the `src` prop if (!accessors.src) return; // src is waiting to be loaded so let's pick it up, loadSrc(accessors.src); }); const loadSrc = (src) => { // push it back a tick so we know initialImageProps are set Promise.resolve() .then(() => { // load with initial props return loadImage(src, initialImageProps); }) .catch(() => { // fail silently, any errors are handled with 'loaderror' event }); }; //#endregion //#region public method (note that these are also called from UI, name of method is name of dispatched event in UI) const applyImageOptionsOrState = (image, options) => { // test if options is image state, if so, apply and exit if (isImageState(options)) { accessors.imageState = options; return; } // create an initial crop rect if no crop supplied if (!options.imageCrop) { const imageSize = image.accessors.size; const imageRotation = options.imageRotation || 0; const cropRect = rectCreateFromSize(sizeRotate(sizeClone(imageSize), imageRotation)); const aspectRatio = options.imageCropAspectRatio || (options.imageCropLimitToImage ? rectAspectRatio(imageSize) // use image size if should limit to image : rectAspectRatio(cropRect)); // use rotated crop rect bounds if no limit const crop = rectContainRect(cropRect, aspectRatio); // center the image in the crop rectangle if (!options.imageCropLimitToImage) { crop.x = (imageSize.width - crop.width) / 2; crop.y = (imageSize.height - crop.height) / 2; } options.imageCrop = crop; } // we need to apply these props in the correct order ['imageCropLimitToImage', 'imageCrop', 'imageCropAspectRatio', 'imageRotation'] .filter((prop) => hasProp(options, prop)) .forEach((prop) => { // assign to `image` accessors[prop] = options[prop]; // remove from normalizedOptions so it's not set twice delete options[prop]; }); // don't set the above options for a second time const { imageCropLimitToImage, imageCrop, imageCropAspectRatio, imageRotation, ...remainingOptions } = options; // trigger setState Object.assign(accessors, remainingOptions); }; // load image, resolve when image is loaded let imageLoadAbort; const loadImage = (src, options = {}) => new Promise((resolve, reject) => { // get current image let image = getImage(); // determine min defined image size (is crop min size) const cropLimitedToImage = !(options.cropLimitToImage === false || options.imageCropLimitToImage === false); const cropMinSize = options.cropMinSize || options.imageCropMinSize; const minImageSize = cropLimitedToImage ? cropMinSize : image && image.accessors.cropMinSize; // if already has image, remove existing image if (image) removeImage(); // access image props and stores image = createImageCore({ minSize: minImageSize }); editorEventsToBubble.map((event) => image.accessors.on(event, bubble(event))); // done, clean up listeners const fin = () => { // reset initial props (as now applied) initialImageProps = {}; unsubs.forEach((unsub) => unsub()); }; const unsubs = []; unsubs.push(image.accessors.on('loaderror', (error) => { fin(); reject(error); })); unsubs.push(image.accessors.on('loadabort', () => { fin(); reject({ name: 'AbortError' }); })); unsubs.push(image.accessors.on('load', (output) => { imageLoadAbort = undefined; fin(); resolve(output); })); unsubs.push(image.accessors.on('beforeload', () => applyImageOptionsOrState(image, options))); // set new image accessors.images = [image]; // assign passed options to editor accessors, we ignore 'src' if (options.imageReader) accessors.imageReader = options.imageReader; if (options.imageWriter) accessors.imageWriter = options.imageWriter; // start reading image imageLoadAbort = image.accessors.read(src, { reader: accessors.imageReader }); }); // start processing a loaded image, resolve when image is processed let imageProcessAbort; const processImage = (src, options) => new Promise(async (resolve, reject) => { // if src supplied, first load src, then process if (isImageSource(src)) { await loadImage(src, options); } // if first argument is not `src` but is set it's an options object, so we'll update the options before generating the image else if (src) { if (isImageState(src)) { accessors.imageState = src; } else { Object.assign(accessors, src); } } // get current active image const image = getImage(); // needs image for processing if (!image) return reject('no image'); // done, clean up listeners const fin = () => { imageProcessAbort = undefined; unsubs.forEach((unsub) => unsub()); }; const unsubs = []; unsubs.push(image.accessors.on('processerror', (error) => { fin(); reject(error); })); unsubs.push(image.accessors.on('processabort', () => { fin(); reject({ name: 'AbortError' }); })); unsubs.push(image.accessors.on('process', (output) => { fin(); resolve(output); })); imageProcessAbort = image.accessors.write(accessors.imageWriter, { shapePreprocessor: accessors.shapePreprocessor || passthrough, imageScrambler: accessors.imageScrambler, }); }); const abortProcessImage = () => { const image = getImage(); if (!image) return; if (imageProcessAbort) imageProcessAbort(); image.accessors.processState = undefined; }; // used internally (triggered by 'x' button when error loading image in UI) const abortLoadImage = () => { if (imageLoadAbort) imageLoadAbort(); accessors.images = []; }; // edit image, loads an image and resolve when image is processed const editImage = (src, options) => new Promise((resolve, reject) => { loadImage(src, options) .then(() => { // access image props and stores const { images } = accessors; const image = images[0]; // done, clean up listeners const done = () => { unsubReject(); unsubResolve(); }; const unsubReject = image.accessors.on('processerror', (error) => { done(); reject(error); }); const unsubResolve = image.accessors.on('process', (output) => { done(); resolve(output); }); }) .catch(reject); }); const removeImage = () => { // no images, nothing to remove const image = getImage(); if (!image) return; // try to abort image load if (imageLoadAbort) imageLoadAbort(); image.accessors.loadState = undefined; // clear images accessors.images = []; }; //#endregion Object.defineProperty(accessors, 'stores', { get: () => stores, }); //#region API defineMethods(accessors, { on: sub, loadImage, abortLoadImage, editImage, removeImage, processImage, abortProcessImage, destroy: () => { unsubSrc(); unsubReader(); }, }); return accessors; //#endregion }; const processImage = (src, options) => { const { processImage } = createImageEditor(); return processImage(src, options); }; var getCanvasMemoryLimit = () => { if (!isSafari$1()) return Infinity; const isSafari15 = /15_/.test(navigator.userAgent); if (isIOS()) { // limit resolution a little bit further to prevent drawing issues if (isSafari15) return 3840 * 3840; // old iOS can deal with 4096 * 4096 without issues return 4096 * 4096; } return isSafari15 ? 4096 * 4096 : Infinity; }; // custom method to draw images const canvasDrawImage = async (ctx, image, srcRect, destRect) => { // get resized image const { dest } = await processImage(image, { imageReader: createDefaultImageReader$1(), imageWriter: createDefaultImageWriter$1({ format: 'canvas', targetSize: { ...destRect, upscale: true, }, }), imageCrop: srcRect, }); // draw processed image ctx.drawImage(dest, destRect.x, destRect.y, destRect.width, destRect.height); // release image canvas to free up memory releaseCanvas(dest); }; // connect function in process chain const connect = (fn, getter = (...args) => args, setter) => async (state, options, onprogress) => { // will hold function result // at this point we don't know if the length of this task can be computed onprogress(createProgressEvent(0, false)); // try to run the function let progressUpdated = false; const res = await fn(...getter(state, options, (event) => { progressUpdated = true; onprogress(event); })); // a setter isn't required setter && setter(state, res); // if progress was updated, we expect the connected function to fire the 1/1 event, else we fire it here if (!progressUpdated) onprogress(createProgressEvent(1, false)); return state; }; // // Reader/Writer Presets // const AnyToFile = ({ srcProp = 'src', destProp = 'dest' } = {}) => [ connect(srcToFile, (state, options, onprogress) => [state[srcProp], onprogress], (state, file) => (state[destProp] = file)), 'any-to-file', ]; const BlobReadImageSize = ({ srcProp = 'src', destProp = 'size' } = {}) => [ connect(getImageSize, (state, options) => [state[srcProp]], (state, size) => (state[destProp] = size)), 'read-image-size', ]; const ImageSizeMatchOrientation = ({ srcSize = 'size', srcOrientation = 'orientation', destSize = 'size', } = {}) => [ connect(orientImageSize, (state) => [state[srcSize], state[srcOrientation]], (state, size) => (state[destSize] = size)), 'image-size-match-orientation', ]; const BlobReadImageHead = ({ srcProp = 'src', destProp = 'head' } = {}) => [ connect((blob, slice) => (isJPEG(blob) ? blobReadSection(blob, slice) : undefined), // 64 * 1024 should be plenty to find extract header // Exif metadata are restricted in size to 64 kB in JPEG images because // according to the specification this information must be contained within a single JPEG APP1 segment. (state) => [state[srcProp], [0, 64 * 2048], onprogress], (state, head) => (state[destProp] = head)), 'read-image-head', ]; const ImageHeadReadExifOrientationTag = ({ srcProp = 'head', destProp = 'orientation', } = {}) => [ connect(arrayBufferImageExif, (state) => [state[srcProp], ORIENTATION_TAG], (state, orientation = 1) => (state[destProp] = orientation)), 'read-exif-orientation-tag', ]; const ImageHeadClearExifOrientationTag = ({ srcProp = 'head' } = {}) => [ connect(arrayBufferImageExif, (state) => [state[srcProp], ORIENTATION_TAG, 1]), 'clear-exif-orientation-tag', ]; const ApplyCanvasScalar = ({ srcImageSize = 'size', srcCanvasSize = 'imageData', srcImageState = 'imageState', destImageSize = 'size', destScalar = 'scalar', } = {}) => [ connect((naturalSize, canvasSize, imageState) => { // calculate canvas scalar const scalar = Math.min(canvasSize.width / naturalSize.width, canvasSize.height / naturalSize.height); // done because not scaling if (scalar !== 1) { const { crop, annotation, decoration } = imageState; // origin to scale to const origin = vectorCreateEmpty(); // scale select.crop if (crop) imageState.crop = rectScale(crop, scalar, origin); // scale annotation const translate = vectorCreateEmpty(); imageState.annotation = annotation.map((shape) => shapeComputeTransform(shape, translate, scalar)); // scale decoration imageState.decoration = decoration.map((shape) => shapeComputeTransform(shape, translate, scalar)); } return [scalar, sizeCreateFromAny(canvasSize)]; }, (state) => [state[srcImageSize], state[srcCanvasSize], state[srcImageState]], (state, [scalar, imageSize]) => { state[destScalar] = scalar; state[destImageSize] = imageSize; }), 'calculate-canvas-scalar', ]; const BlobToImageData = ({ srcProp = 'src', destProp = 'imageData', canvasMemoryLimit = undefined, }) => [ connect(blobToImageData, (state) => [state[srcProp], canvasMemoryLimit], (state, imageData) => (state[destProp] = imageData)), 'blob-to-image-data', ]; const ImageDataMatchOrientation = ({ srcImageData = 'imageData', srcOrientation = 'orientation', } = {}) => [ connect(orientImageData, (state) => [state[srcImageData], state[srcOrientation]], (state, imageData) => (state.imageData = imageData)), 'image-data-match-orientation', ]; const ImageDataFill = ({ srcImageData = 'imageData', srcImageState = 'imageState' } = {}) => [ connect(fillImageData, (state) => [ state[srcImageData], { backgroundColor: state[srcImageState].backgroundColor }, ], (state, imageData) => (state.imageData = imageData)), 'image-data-fill', ]; const ImageDataCrop = ({ srcImageData = 'imageData', srcImageState = 'imageState' } = {}) => [ connect(cropImageData, (state) => [ state[srcImageData], { crop: state[srcImageState].crop, rotation: state[srcImageState].rotation, flipX: state[srcImageState].flipX, flipY: state[srcImageState].flipY, }, ], (state, imageData) => (state.imageData = imageData)), 'image-data-crop', ]; const hasTargetSize = (imageState) => !!((imageState.targetSize && imageState.targetSize.width) || (imageState.targetSize && imageState.targetSize.height)); const ImageDataResize = ({ resize = { width: undefined, height: undefined, fit: undefined, upscale: undefined, }, srcProp = 'imageData', srcImageState = 'imageState', destImageScaledSize = 'imageScaledSize', }) => [ connect(resizeImageData, (state) => [ state[srcProp], { width: Math.min(resize.width || Number.MAX_SAFE_INTEGER, (state[srcImageState].targetSize && state[srcImageState].targetSize.width) || Number.MAX_SAFE_INTEGER), height: Math.min(resize.height || Number.MAX_SAFE_INTEGER, (state[srcImageState].targetSize && state[srcImageState].targetSize.height) || Number.MAX_SAFE_INTEGER), fit: resize.fit || 'contain', upscale: hasTargetSize(state[srcImageState]) ? true : resize.upscale || false, }, ], (state, imageData) => { if (!sizeEqual(state.imageData, imageData)) state[destImageScaledSize] = sizeCreateFromAny(imageData); state.imageData = imageData; }), 'image-data-resize', ]; const ImageDataFilter = ({ srcImageData = 'imageData', srcImageState = 'imageState', destImageData = 'imageData', } = {}) => [ connect(filterImageData, (state) => { const { colorMatrix } = state[srcImageState]; const colorMatrices = colorMatrix && Object.keys(colorMatrix) .map((name) => colorMatrix[name]) .filter(Boolean); return [ state[srcImageData], { colorMatrix: colorMatrices && getColorMatrixFromColorMatrices(colorMatrices), convolutionMatrix: state[srcImageState].convolutionMatrix, gamma: state[srcImageState].gamma, noise: state[srcImageState].noise, vignette: state[srcImageState].vignette, }, ]; }, (state, imageData) => (state[destImageData] = imageData)), 'image-data-filter', ]; const createImageContextDrawingTransform = (state, { srcSize, srcImageState, destImageScaledSize }) => (ctx) => { const imageSize = state[srcSize]; const { crop = rectCreateFromSize(imageSize), rotation = 0, flipX, flipY } = state[srcImageState]; const rotatedRect = getImageTransformedRect(imageSize, rotation); const rotatedSize = { width: rotatedRect.width, height: rotatedRect.height, }; // calculate image scalar so we can scale annotations accordingly const scaledSize = state[destImageScaledSize]; const scalar = scaledSize ? Math.min(scaledSize.width / crop.width, scaledSize.height / crop.height) : 1; // calculate center const dx = imageSize.width * 0.5 - rotatedSize.width * 0.5; const dy = imageSize.height * 0.5 - rotatedSize.height * 0.5; const center = sizeCenter(imageSize); // image scalar ctx.scale(scalar, scalar); // offset ctx.translate(-dx, -dy); ctx.translate(-crop.x, -crop.y); // rotation ctx.translate(center.x, center.y); ctx.rotate(rotation); ctx.translate(-center.x, -center.y); // flipping ctx.scale(flipX ? -1 : 1, flipY ? -1 : 1); ctx.translate(flipX ? -imageSize.width : 0, flipY ? -imageSize.height : 0); // annotations are clipped clip to image ctx.rect(0, 0, imageSize.width, imageSize.height); ctx.clip(); }; const ImageDataRedact = ({ srcImageData = 'imageData', srcImageState = 'imageState', destImageData = 'imageData', destScalar = 'scalar', } = {}) => [ connect(async (imageData, imageScrambler, imageBackgroundColor, shapes, scalar) => { // skip! if (!imageScrambler) return imageData; // create scrambled texture version let scrambledCanvas; try { const options = { dataSizeScalar: getImageRedactionScaleFactor(imageData, shapes), }; if (imageBackgroundColor && imageBackgroundColor[3] > 0) { options.backgroundColor = [...imageBackgroundColor]; } scrambledCanvas = await imageScrambler(imageData, options); } catch (err) { } // create drawing context const canvas = h('canvas'); canvas.width = imageData.width; canvas.height = imageData.height; const ctx = canvas.getContext('2d'); ctx.putImageData(imageData, 0, 0); // set up a clip path so we only draw scrambled image within path const path = new Path2D(); shapes.forEach((shape) => { const rect = rectCreate(shape.x, shape.y, shape.width, shape.height); rectMultiply(rect, scalar); const corners = rectRotate(rectClone(rect), shape.rotation); const poly = new Path2D(); corners.forEach((corner, i) => { if (i === 0) return poly.moveTo(corner.x, corner.y); poly.lineTo(corner.x, corner.y); }); path.addPath(poly); }); ctx.clip(path, 'nonzero'); ctx.imageSmoothingEnabled = false; ctx.drawImage(scrambledCanvas, 0, 0, canvas.width, canvas.height); releaseCanvas(scrambledCanvas); // done const imageDataOut = ctx.getImageData(0, 0, canvas.width, canvas.height); // clean up memory usage releaseCanvas(canvas); return imageDataOut; }, (state, { imageScrambler }) => [ state[srcImageData], imageScrambler, state[srcImageState].backgroundColor, state[srcImageState].redaction, state[destScalar], ], (state, imageData) => (state[destImageData] = imageData)), 'image-data-annotate', ]; const ImageDataAnnotate = ({ srcImageData = 'imageData', srcSize = 'size', srcImageState = 'imageState', destImageData = 'imageData', destImageScaledSize = 'imageScaledSize', } = {}) => [ connect(drawImageData, (state, { shapePreprocessor }) => [ state[srcImageData], { shapes: state[srcImageState].annotation, context: state[srcSize], transform: createImageContextDrawingTransform(state, { srcSize, srcImageState, destImageScaledSize, }), drawImage: canvasDrawImage, preprocessShape: (shape) => shapePreprocessor(shape, { isPreview: false }), }, ], (state, imageData) => (state[destImageData] = imageData)), 'image-data-annotate', ]; const ImageDataDecorate = ({ srcImageData = 'imageData', srcImageState = 'imageState', destImageData = 'imageData', destImageScaledSize = 'imageScaledSize', } = {}) => [ connect(drawImageData, (state, { shapePreprocessor }) => [ state[srcImageData], { shapes: state[srcImageState].decoration, context: state[srcImageState].crop, transform: (ctx) => { // calculate image scalar so we can scale decoration accordingly const { crop } = state.imageState; const scaledSize = state[destImageScaledSize]; const scalar = scaledSize ? Math.min(scaledSize.width / crop.width, scaledSize.height / crop.height) : 1; ctx.scale(scalar, scalar); }, drawImage: canvasDrawImage, preprocessShape: (shape) => shapePreprocessor(shape, { isPreview: false }), }, ], (state, imageData) => (state[destImageData] = imageData)), 'image-data-decorate', ]; const ImageDataFrame = ({ srcImageData = 'imageData', srcImageState = 'imageState', destImageData = 'imageData', destImageScaledSize = 'imageScaledSize', } = {}) => [ connect(drawImageData, (state, { shapePreprocessor }) => { const frame = state[srcImageState].frame; if (!frame) return [state[srcImageData]]; const context = { ...state[srcImageState].crop }; const bounds = shapesBounds(shapesFromCompositShape(frame, context, shapePreprocessor), context); context.x = Math.abs(bounds.left); context.y = Math.abs(bounds.top); context.width += Math.abs(bounds.left) + Math.abs(bounds.right); context.height += Math.abs(bounds.top) + Math.abs(bounds.bottom); const { crop } = state.imageState; const scaledSize = state[destImageScaledSize]; const scalar = scaledSize ? Math.min(scaledSize.width / crop.width, scaledSize.height / crop.height) : 1; rectMultiply(context, scalar); // use floor because we can't fill up half pixels context.x = Math.floor(context.x); context.y = Math.floor(context.y); context.width = Math.floor(context.width); context.height = Math.floor(context.height); return [ state[srcImageData], { shapes: [frame], contextBounds: context, transform: (ctx) => { ctx.translate(context.x, context.y); }, drawImage: canvasDrawImage, preprocessShape: (shape) => shapePreprocessor(shape, { isPreview: false }), }, ]; }, (state, imageData) => (state[destImageData] = imageData)), 'image-data-frame', ]; const ImageDataToBlob = ({ mimeType = undefined, quality = undefined, srcImageData = 'imageData', srcFile = 'src', destBlob = 'blob', } = {}) => [ connect(imageDataToBlob, (state) => [ state[srcImageData], mimeType || getMimeTypeFromFilename(state[srcFile].name) || state[srcFile].type, quality, ], (state, blob) => (state[destBlob] = blob)), 'image-data-to-blob', ]; const ImageDataToCanvas = ({ srcImageData = 'imageData', srcOrientation = 'orientation', destCanvas = 'dest', } = {}) => [ connect(imageDataToCanvas, (state) => [state[srcImageData], state[srcOrientation]], (state, canvas) => (state[destCanvas] = canvas)), 'image-data-to-canvas', ]; const writeImageHead = async (blob, head) => { if (!isJPEG(blob) || !head) return blob; // get exif section const view = new DataView(head); const markers = dataViewGetApplicationMarkers(view); if (!markers || !markers.exif) return blob; const { exif } = markers; // from byte 0 to end of exif header const exifBuffer = head.slice(0, exif.offset + exif.size + 2); return blobWriteSection(blob, // insert head buffer into blob exifBuffer, // current blob doesn't have exif header (as outputted by canvas), so we insert ours in // (jpeg header 2) + (jfif size 16) + (app1 header 2) [20]); }; const BlobWriteImageHead = (srcBlob = 'blob', srcHead = 'head', destBlob = 'blob') => [ connect(writeImageHead, (state) => [state[srcBlob], state[srcHead]], (state, blob) => (state[destBlob] = blob)), 'blob-write-image-head', ]; const BlobToFile = ({ renameFile = undefined, srcBlob = 'blob', srcFile = 'src', destFile = 'dest', defaultFilename = undefined, } = {}) => [ connect(blobToFile, (state) => [ state[srcBlob], renameFile ? renameFile(state[srcFile]) : state[srcFile].name || `${defaultFilename}.${getExtensionFromMimeType(state[srcBlob].type)}`, ], (state, file) => (state[destFile] = file)), 'blob-to-file', ]; const Store = ({ url = './', dataset = (state) => [ ['dest', state.dest, state.dest.name], ['imageState', state.imageState], ], destStore = 'store', }) => [ connect( // upload function async (dataset, onprogress) => await post(url, dataset, { onprogress }), // get state values (state, options, onprogress) => [dataset(state), onprogress], // set state values (state, xhr) => (state[destStore] = xhr) // logs XHR request returned by `post` ), 'store', ]; const PropFilter = (allowlist) => [ connect((state) => { // if no allowlist suppleid or is empty array we don't filter if (!allowlist || !allowlist.length) return state; // else we only allow the props defined in the list and delete non matching props Object.keys(state).forEach((key) => { if (allowlist.includes(key)) return; delete state[key]; }); return state; }), 'prop-filter', ]; // Generic image reader, suitable for most use cases const createDefaultImageReader$1 = (options = {}) => { const { orientImage = true, outputProps = ['src', 'dest', 'size'], preprocessImageFile, } = options; return [ // can read most source files and turn them into blobs AnyToFile(), // TODO: test if supported mime/type // called when file created, can be used to read unrecognized files preprocessImageFile && [ connect(preprocessImageFile, (state, options, onprogress) => [ state.dest, options, onprogress, ], (state, file) => (state.dest = file)), 'preprocess-image-file', ], // quickly read size (only reads first part of image) BlobReadImageSize({ srcProp: 'dest' }), // fix image orientation orientImage && BlobReadImageHead({ srcProp: 'dest' }), orientImage && ImageHeadReadExifOrientationTag(), orientImage && ImageSizeMatchOrientation(), // remove unwanted props PropFilter(outputProps), ].filter(Boolean); }; const createDefaultImageWriter$1 = (options = {}) => { const { canvasMemoryLimit = getCanvasMemoryLimit(), orientImage = true, copyImageHead = true, mimeType = undefined, quality = undefined, renameFile = undefined, targetSize = undefined, store = undefined, format = 'file', outputProps = ['src', 'dest', 'imageState', 'store'], preprocessImageSource, preprocessImageState, postprocessImageData, postprocessImageBlob, } = options; return [ // allow preprocessing of image blob, should return a new blob, for example to automatically make image background transparent preprocessImageSource && [ connect(preprocessImageSource, (state, options, onprogress) => [ state.src, options, onprogress, ], (state, src) => (state.src = src)), 'preprocess-image-source', ], // get orientation info (if is jpeg) (orientImage || copyImageHead) && BlobReadImageHead(), orientImage && ImageHeadReadExifOrientationTag(), // get image size BlobReadImageSize(), // allow preproccesing of image state for example to replace placeholders preprocessImageState && [ connect(preprocessImageState, (state, options, onprogress) => [ state.imageState, options, onprogress, ], (state, imageState) => (state.imageState = imageState)), 'preprocess-image-state', ], // get image data BlobToImageData({ canvasMemoryLimit }), // fix image orientation orientImage && ImageSizeMatchOrientation(), orientImage && ImageDataMatchOrientation(), // apply canvas scalar to data ApplyCanvasScalar(), // apply image state ImageDataRedact(), ImageDataCrop(), ImageDataResize({ resize: targetSize }), ImageDataFilter(), ImageDataFill(), ImageDataAnnotate(), ImageDataDecorate(), ImageDataFrame(), // run post processing on image data, for example to apply circular crop postprocessImageData && [ connect(postprocessImageData, (state, options, onprogress) => [ state.imageData, options, onprogress, ], (state, imageData) => (state.imageData = imageData)), 'postprocess-image-data', ], // convert to correct output format format === 'file' ? ImageDataToBlob({ mimeType, quality }) : format === 'canvas' ? ImageDataToCanvas() : [ (state) => { state.dest = state.imageData; return state; }, ], // we overwite the exif orientation tag so the image is oriented correctly format === 'file' && orientImage && ImageHeadClearExifOrientationTag(), // we write the new image head to the target blob format === 'file' && copyImageHead && BlobWriteImageHead(), // allow converting the blob to a different format postprocessImageBlob && [ connect(postprocessImageBlob, ({ blob, imageData, src }, options, onprogress) => [ { blob, imageData, src }, options, onprogress, ], (state, blob) => (state.blob = blob)), 'postprocess-image-file', ], // turn the image blob into a file, will also rename the file format === 'file' && BlobToFile({ defaultFilename: 'image', renameFile }), // upload or process data if is a file format === 'file' ? // used for file output formats store && (isString(store) ? // a basic store to post to Store({ url: store }) : // see if is fully custom or store config isFunction(store) ? // fully custom store function [store, 'store'] : // a store configuration object Store(store)) : // used for imageData and canvas output formats isFunction(store) && [store, 'store'], // remove unwanted props PropFilter(outputProps), ].filter(Boolean); }; var scrambleEffect = (options, done) => { const { imageData, amount = 1 } = options; const intensity = Math.round(Math.max(1, amount) * 2); const range = Math.round(intensity * 0.5); const inputWidth = imageData.width; const inputHeight = imageData.height; const outputData = new Uint8ClampedArray(inputWidth * inputHeight * 4); const inputData = imageData.data; let randomData; let i = 0, x, y, r; let xoffset = 0; let yoffset = 0; let index; const l = inputWidth * inputHeight * 4 - 4; for (y = 0; y < inputHeight; y++) { randomData = crypto.getRandomValues(new Uint8ClampedArray(inputHeight)); for (x = 0; x < inputWidth; x++) { r = randomData[y] / 255; xoffset = 0; yoffset = 0; if (r < 0.5) { xoffset = (-range + Math.round(Math.random() * intensity)) * 4; } if (r > 0.5) { yoffset = (-range + Math.round(Math.random() * intensity)) * (inputWidth * 4); } // limit to image data index = Math.min(Math.max(0, i + xoffset + yoffset), l); outputData[i] = inputData[index]; outputData[i + 1] = inputData[index + 1]; outputData[i + 2] = inputData[index + 2]; outputData[i + 3] = inputData[index + 3]; i += 4; } } done(null, { data: outputData, width: imageData.width, height: imageData.height, }); }; // basic blur covolution matrix const BLUR_MATRIX = [0.0625, 0.125, 0.0625, 0.125, 0.25, 0.125, 0.0625, 0.125, 0.0625]; var imageDataScramble = async (inputData, options = {}) => { if (!inputData) return; const { width, height } = inputData; const { dataSize = 96, dataSizeScalar = 1, scrambleAmount = 4, blurAmount = 6, outputFormat = 'canvas', backgroundColor = [0, 0, 0], } = options; const size = Math.round(dataSize * dataSizeScalar); const scalar = Math.min(size / width, size / height); const outputWidth = Math.floor(width * scalar); const outputHeight = Math.floor(height * scalar); // draw low res preview, add margin so blur isn't transparent const scaledOutputCanvas = (h('canvas', { width: outputWidth, height: outputHeight })); const ctx = scaledOutputCanvas.getContext('2d'); // fill background on transparent images backgroundColor.length = 3; // prevent transparent colors ctx.fillStyle = colorArrayToRGBA(backgroundColor); ctx.fillRect(0, 0, outputWidth, outputHeight); if (isImageData(inputData)) { // temporarily draw to canvas so we can draw image data to scaled context const transferCanvas = h('canvas', { width, height }); transferCanvas.getContext('2d').putImageData(inputData, 0, 0); // draw to scaled context ctx.drawImage(transferCanvas, 0, 0, outputWidth, outputHeight); // release memory releaseCanvas(transferCanvas); } else { // bitmap data ctx.drawImage(inputData, 0, 0, outputWidth, outputHeight); } // get scaled image data for scrambling const imageData = ctx.getImageData(0, 0, outputWidth, outputHeight); // filters to apply const filters = []; // add scramble filter if (scrambleAmount > 0) filters.push([scrambleEffect, { amount: scrambleAmount }]); // add blur filters if (blurAmount > 0) for (let i = 0; i < blurAmount; i++) { filters.push([convolutionEffect, { matrix: BLUR_MATRIX }]); } let imageDataScrambled; if (filters.length) { // builds effect chain const chain = (transforms, i) => `(err, imageData) => { (${transforms[i][0].toString()})(Object.assign({ imageData: imageData }, filterInstructions[${i}]), ${transforms[i + 1] ? chain(transforms, i + 1) : 'done'}) }`; const filterChain = `function (options, done) { const filterInstructions = options.filterInstructions; const imageData = options.imageData; (${chain(filters, 0)})(null, imageData) }`; // scramble image data in separate thread const imageDataObjectScrambled = await thread(filterChain, [ { imageData: imageData, filterInstructions: filters.map((t) => t[1]), }, ], [imageData.data.buffer]); imageDataScrambled = imageDataObjectToImageData(imageDataObjectScrambled); } else { imageDataScrambled = imageData; } if (outputFormat === 'canvas') { // put back scrambled data ctx.putImageData(imageDataScrambled, 0, 0); // return canvas return scaledOutputCanvas; } return imageDataScrambled; }; var getComponentExportedProps = (Component) => { const descriptors = Object.getOwnPropertyDescriptors(Component.prototype); return Object.keys(descriptors).filter((key) => !!descriptors[key]['get']); }; function circOut(t) { return Math.sqrt(1 - --t * t); } function is_date(obj) { return Object.prototype.toString.call(obj) === '[object Date]'; } function get_interpolator(a, b) { if (a === b || a !== a) return () => a; const type = typeof a; if (type !== typeof b || Array.isArray(a) !== Array.isArray(b)) { throw new Error('Cannot interpolate values of different type'); } if (Array.isArray(a)) { const arr = b.map((bi, i) => { return get_interpolator(a[i], bi); }); return t => arr.map(fn => fn(t)); } if (type === 'object') { if (!a || !b) throw new Error('Object cannot be null'); if (is_date(a) && is_date(b)) { a = a.getTime(); b = b.getTime(); const delta = b - a; return t => new Date(a + t * delta); } const keys = Object.keys(b); const interpolators = {}; keys.forEach(key => { interpolators[key] = get_interpolator(a[key], b[key]); }); return t => { const result = {}; keys.forEach(key => { result[key] = interpolators[key](t); }); return result; }; } if (type === 'number') { const delta = b - a; return t => a + t * delta; } throw new Error(`Cannot interpolate ${type} values`); } function tweened(value, defaults = {}) { const store = writable(value); let task; let target_value = value; function set(new_value, opts) { if (value == null) { store.set(value = new_value); return Promise.resolve(); } target_value = new_value; let previous_task = task; let started = false; let { delay = 0, duration = 400, easing = identity, interpolate = get_interpolator } = assign(assign({}, defaults), opts); if (duration === 0) { if (previous_task) { previous_task.abort(); previous_task = null; } store.set(value = target_value); return Promise.resolve(); } const start = now() + delay; let fn; task = loop(now => { if (now < start) return true; if (!started) { fn = interpolate(value, new_value); if (typeof duration === 'function') duration = duration(value, new_value); started = true; } if (previous_task) { previous_task.abort(); previous_task = null; } const elapsed = now - start; if (elapsed > duration) { store.set(value = new_value); return false; } // @ts-ignore store.set(value = fn(easing(elapsed / duration))); return true; }); return task.promise; } return { set, update: (fn, opts) => set(fn(target_value, value), opts), subscribe: store.subscribe }; } // @ts-ignore function tick_spring(ctx, last_value, current_value, target_value) { if (typeof current_value === 'number') { // @ts-ignore const delta = target_value - current_value; // @ts-ignore const velocity = (current_value - last_value) / (ctx.dt || 1 / 60); // guard div by 0 const spring = ctx.opts.stiffness * delta; const damper = ctx.opts.damping * velocity; const acceleration = (spring - damper) * ctx.inv_mass; const d = (velocity + acceleration) * ctx.dt; if (Math.abs(d) < ctx.opts.precision && Math.abs(delta) < ctx.opts.precision) { return target_value; // settled } else { ctx.settled = false; // signal loop to keep ticking // @ts-ignore return current_value + d; } } else if (isArray(current_value)) { // @ts-ignore return current_value.map((_, i) => tick_spring(ctx, last_value[i], current_value[i], target_value[i])); } else if (typeof current_value === 'object') { const next_value = {}; // @ts-ignore for (const k in current_value) { // @ts-ignore next_value[k] = tick_spring(ctx, last_value[k], current_value[k], target_value[k]); } // @ts-ignore return next_value; } else { throw new Error(`Cannot spring ${typeof current_value} values`); } } // export interface Spring { // set: (new_value: any, opts?: SpringUpdateOpts) => Promise; // update: (fn: Function, opts?: SpringUpdateOpts) => Promise; // subscribe: Function; // precision: number; // damping: number; // stiffness: number; // } function spring(value, opts = {}) { const store = writable(value); const { stiffness = 0.15, damping = 0.8, precision = 0.01 } = opts; let last_time; let task; let current_token; let last_value = value; let target_value = value; let inv_mass = 1; let inv_mass_recovery_rate = 0; let cancel_task = false; function set(new_value, opts = {}) { target_value = new_value; const token = (current_token = {}); if (value == null || opts.hard || (spring.stiffness >= 1 && spring.damping >= 1)) { cancel_task = true; // cancel any running animation last_time = null; last_value = new_value; store.set((value = target_value)); return Promise.resolve(); } else if (opts.soft) { const rate = opts.soft === true ? 0.5 : +opts.soft; inv_mass_recovery_rate = 1 / (rate * 60); inv_mass = 0; // infinite mass, unaffected by spring forces } if (!task) { last_time = null; cancel_task = false; const ctx = { inv_mass: undefined, opts: spring, settled: true, dt: undefined, }; task = loop((now) => { if (last_time === null) last_time = now; if (cancel_task) { cancel_task = false; task = null; return false; } inv_mass = Math.min(inv_mass + inv_mass_recovery_rate, 1); // altered so doesn't create a new object ctx.inv_mass = inv_mass; ctx.opts = spring; ctx.settled = true; // tick_spring may signal false ctx.dt = ((now - last_time) * 60) / 1000; const next_value = tick_spring(ctx, last_value, value, target_value); last_time = now; last_value = value; store.set((value = next_value)); if (ctx.settled) task = null; return !ctx.settled; }); } return new Promise((fulfil) => { task.promise.then(() => { if (token === current_token) fulfil(); }); }); } const spring = { set, update: (fn, opts) => set(fn(target_value, value), opts), subscribe: store.subscribe, stiffness, damping, precision, }; return spring; } var prefersReducedMotion = readable(false, set => { const mql = window.matchMedia('(prefers-reduced-motion:reduce)'); set(mql.matches); mql.onchange = () => set(mql.matches); }); var hasResizeObserver = () => 'ResizeObserver' in window; // const rectNext = rectCreateEmpty(); const updateNodeRect = (node, x, y, width, height) => { if (!node.rect) node.rect = rectCreateEmpty(); const rect = node.rect; rectUpdate(rectNext, x, y, width, height); if (rectEqual(rect, rectNext)) return; rectUpdateWithRect(rect, rectNext); node.dispatchEvent(new CustomEvent('measure', { detail: rect })); }; // measures the element const r = Math.round; const measureViewRect = (node) => { const clientRect = node.getBoundingClientRect(); updateNodeRect(node, r(clientRect.x), r(clientRect.y), r(clientRect.width), r(clientRect.height)); }; const measureOffset = (node) => updateNodeRect(node, node.offsetLeft, node.offsetTop, node.offsetWidth, node.offsetHeight); // holds all the elements to measure using requestAnimationFrame const elements = []; // draw loop let frame = null; function tick() { if (!elements.length) { frame = null; return; } elements.forEach((node) => node.measure(node)); frame = requestAnimationFrame(tick); } let observer; // ResizeObserver API not known by TypeScript var measurable = (node, options = {}) => { const { observePosition = false, observeViewRect = false, once = false, disabled = false, } = options; // exit if (disabled) return; // use resize observe if available if (hasResizeObserver() && !observePosition && !observeViewRect) { // we only create one observer, it will observe all registered elements if (!observer) { // @ts-ignore: [2020-02-20] ResizeObserver API not known by TypeScript observer = new ResizeObserver((entries) => { // @ts-ignore entries.forEach((entry) => measureOffset(entry.target)); }); } // start observing this node observer.observe(node); // measure our node for the first time measureOffset(node); // if should only measure once, remove now if (once) observer.unobserve(node); // and we done, need to return a clean up method for when our node is destroyed return { destroy() { // already unobserved this node if (once) return; observer.unobserve(node); // TODO: test if all nodes have been removed, if so, remove observer }, }; } // set measure function node.measure = observeViewRect ? measureViewRect : measureOffset; // add so the element is measured elements.push(node); // start measuring on next frame, we set up a single measure loop, // the loop will check if there's still elements that need to be measured, // else it will stop running if (!frame) frame = requestAnimationFrame(tick); // measure this element now node.measure(node); // remove method return { destroy() { const index = elements.indexOf(node); elements.splice(index, 1); }, }; }; var focusvisible = (element) => { let isKeyboardInteraction = false; const handlePointerdown = () => { isKeyboardInteraction = false; }; const handleKeydown = () => { isKeyboardInteraction = true; }; const handleKeyup = () => { isKeyboardInteraction = false; }; const handleFocus = (e) => { if (!isKeyboardInteraction) return; e.target.dataset.focusVisible = ''; }; const handleBlur = (e) => { delete e.target.dataset.focusVisible; }; const map = { pointerdown: handlePointerdown, keydown: handleKeydown, keyup: handleKeyup, focus: handleFocus, blur: handleBlur, }; Object.keys(map).forEach((event) => element.addEventListener(event, map[event], true)); return { destroy() { Object.keys(map).forEach((event) => element.removeEventListener(event, map[event], true)); }, }; }; const getResourceFromItem = async (item) => new Promise((resolve) => { if (item.kind === 'file') return resolve(item.getAsFile()); item.getAsString(resolve); }); const getResourcesFromEvent = (e) => new Promise((resolve, reject) => { const { items } = e.dataTransfer; if (!items) return resolve([]); Promise.all(Array.from(items).map(getResourceFromItem)) .then((res) => { resolve(res.filter((item) => (isBinary(item) && isImage(item)) || /^http/.test(item))); }) .catch(reject); }); var dropable = (node, options = {}) => { const handleDragOver = (e) => { // need to be prevent default to allow drop e.preventDefault(); }; const handleDrop = async (e) => { e.preventDefault(); e.stopPropagation(); // prevents parents from catching this drop try { const resources = await getResourcesFromEvent(e); node.dispatchEvent(new CustomEvent('dropfiles', { detail: { event: e, resources, }, ...options, })); } catch (err) { // silent, wasn't able to catch } }; node.addEventListener('drop', handleDrop); node.addEventListener('dragover', handleDragOver); // remove method return { destroy() { node.removeEventListener('drop', handleDrop); node.removeEventListener('dragover', handleDragOver); }, }; }; let result$6 = null; var supportsWebGL2 = () => { if (result$6 === null) { if ('WebGL2RenderingContext' in window) { let canvas; try { canvas = h('canvas'); result$6 = !!canvas.getContext('webgl2'); } catch (err) { result$6 = false; } canvas && releaseCanvas(canvas); } else { result$6 = false; } } return result$6; }; var getWebGLContext = (canvas, attrs) => { if (supportsWebGL2()) return canvas.getContext('webgl2', attrs); return (canvas.getContext('webgl', attrs) || canvas.getContext('experimental-webgl', attrs)); }; let result$5 = null; var isSoftwareRendering = () => { if (result$5 === null) { if (isBrowser()) { const canvas = h('canvas'); result$5 = !getWebGLContext(canvas, { failIfMajorPerformanceCaveat: true, }); releaseCanvas(canvas); } else { result$5 = false; } } return result$5; }; var isPowerOf2 = (value) => (value & (value - 1)) === 0; var stringReplace = (str, entries = {}, prefix = '', postfix = '') => { return Object.keys(entries) .filter((key) => !isObject(entries[key])) .reduce((prev, curr) => { return prev.replace(new RegExp(prefix + curr + postfix), entries[curr]); }, str); }; var SHADER_FRAG_HEAD = "#version 300 es\nprecision highp float;\n\nout vec4 fragColor;"; // eslint-disable-line var SHADER_FRAG_INIT = "\nfloat a=1.0;vec4 fillColor=uColor;vec4 textureColor=texture(uTexture,vTexCoord);textureColor*=(1.0-step(1.0,vTexCoord.y))*step(0.0,vTexCoord.y)*(1.0-step(1.0,vTexCoord.x))*step(0.0,vTexCoord.x);"; // eslint-disable-line var SHADER_FRAG_MASK = "\nuniform float uMaskFeather[8];uniform float uMaskBounds[4];uniform float uMaskOpacity;float mask(float x,float y,float bounds[4],float opacity){return 1.0-(1.0-(smoothstep(bounds[3],bounds[3]+1.0,x)*(1.0-smoothstep(bounds[1]-1.0,bounds[1],x))*(1.0-step(bounds[0],y))*step(bounds[2],y)))*(1.0-opacity);}"; // eslint-disable-line var SHADER_FRAG_MASK_APPLY = "\nfloat m=mask(gl_FragCoord.x,gl_FragCoord.y,uMaskBounds,uMaskOpacity);"; // eslint-disable-line var SHADER_FRAG_MASK_FEATHER_APPLY = "\nfloat leftFeatherOpacity=step(uMaskFeather[1],gl_FragCoord.x)*uMaskFeather[0]+((1.0-uMaskFeather[0])*smoothstep(uMaskFeather[1],uMaskFeather[3],gl_FragCoord.x));float rightFeatherOpacity=(1.0-step(uMaskFeather[7],gl_FragCoord.x))*uMaskFeather[4]+((1.0-uMaskFeather[4])*smoothstep(uMaskFeather[7],uMaskFeather[5],gl_FragCoord.x));a*=leftFeatherOpacity*rightFeatherOpacity;"; // eslint-disable-line var SHADER_FRAG_RECT_AA = "\nvec2 scaledPoint=vec2(vRectCoord.x*uSize.x,vRectCoord.y*uSize.y);a*=smoothstep(0.0,1.0,uSize.x-scaledPoint.x);a*=smoothstep(0.0,1.0,uSize.y-scaledPoint.y);a*=smoothstep(0.0,1.0,scaledPoint.x);a*=smoothstep(0.0,1.0,scaledPoint.y);"; // eslint-disable-line var SHADER_FRAG_CORNER_RADIUS = "\nvec2 s=(uSize-2.0)*.5;vec2 r=(vRectCoord*uSize);vec2 p=r-(uSize*.5);float cornerRadius=uCornerRadius[0];bool left=r.x { src = stringReplace(src, type === gl.VERTEX_SHADER ? SHADER_VERT_SNIPPETS : SHADER_FRAG_SNIPPETS, '##').trim(); // ready if supports webgl if (supportsWebGL2()) return src; src = src.replace(/#version.+/gm, '').trim(); src = src.replace(/^\/\/\#/gm, '#'); if (type === gl.VERTEX_SHADER) { src = src.replace(/in /gm, 'attribute ').replace(/out /g, 'varying '); } if (type === gl.FRAGMENT_SHADER) { src = src .replace(/in /gm, 'varying ') .replace(/out.*?;/gm, '') .replace(/texture\(/g, 'texture2D(') .replace(/fragColor/g, 'gl_FragColor'); } return `${src}`; }; const compileShader = (gl, src, type) => { const shader = gl.createShader(type); const transpiledSrc = transpileShader(gl, src, type); gl.shaderSource(shader, transpiledSrc); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(shader)); } return shader; }; const createShader = (gl, vertexShader, fragmentShader, attribs, uniforms) => { const program = gl.createProgram(); gl.attachShader(program, compileShader(gl, vertexShader, gl.VERTEX_SHADER)); gl.attachShader(program, compileShader(gl, fragmentShader, gl.FRAGMENT_SHADER)); gl.linkProgram(program); const locations = {}; attribs.forEach((name) => { locations[name] = gl.getAttribLocation(program, name); }); uniforms.forEach((name) => { locations[name] = gl.getUniformLocation(program, name); }); return { program, locations, }; }; const canMipMap = (source) => { if (supportsWebGL2()) return true; return isPowerOf2(source.width) && isPowerOf2(source.height); }; const applyTextureProperties = (gl, source, options) => { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, canMipMap(source) ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, options.filter // === 'nearest' ? gl.NEAREST : gl.LINEAR ); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); if (canMipMap(source)) gl.generateMipmap(gl.TEXTURE_2D); }; const updateTexture = (gl, texture, source, options) => { gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source); applyTextureProperties(gl, source, options); gl.bindTexture(gl.TEXTURE_2D, null); return texture; }; const applyOpacity = (color, opacity = 1) => color ? [color[0], color[1], color[2], isNumber(color[3]) ? opacity * color[3] : opacity] : [0, 0, 0, 0]; const mat4Create = () => { const mat = new Float32Array(16); mat[0] = 1; mat[5] = 1; mat[10] = 1; mat[15] = 1; return mat; }; const mat4Perspective = (mat, fovy, aspect, near, far) => { const f = 1.0 / Math.tan(fovy / 2); const nf = 1 / (near - far); mat[0] = f / aspect; mat[1] = 0; mat[2] = 0; mat[3] = 0; mat[4] = 0; mat[5] = f; mat[6] = 0; mat[7] = 0; mat[8] = 0; mat[9] = 0; mat[10] = (far + near) * nf; mat[11] = -1; mat[12] = 0; mat[13] = 0; mat[14] = 2 * far * near * nf; mat[15] = 0; }; const mat4Ortho = (mat, left, right, bottom, top, near, far) => { const lr = 1 / (left - right); const bt = 1 / (bottom - top); const nf = 1 / (near - far); mat[0] = -2 * lr; mat[1] = 0; mat[2] = 0; mat[3] = 0; mat[4] = 0; mat[5] = -2 * bt; mat[6] = 0; mat[7] = 0; mat[8] = 0; mat[9] = 0; mat[10] = 2 * nf; mat[11] = 0; mat[12] = (left + right) * lr; mat[13] = (top + bottom) * bt; mat[14] = (far + near) * nf; mat[15] = 1; }; const mat4Translate = (mat, x, y, z) => { mat[12] = mat[0] * x + mat[4] * y + mat[8] * z + mat[12]; mat[13] = mat[1] * x + mat[5] * y + mat[9] * z + mat[13]; mat[14] = mat[2] * x + mat[6] * y + mat[10] * z + mat[14]; mat[15] = mat[3] * x + mat[7] * y + mat[11] * z + mat[15]; }; const mat4Scale = (mat, s) => { mat[0] = mat[0] * s; mat[1] = mat[1] * s; mat[2] = mat[2] * s; mat[3] = mat[3] * s; mat[4] = mat[4] * s; mat[5] = mat[5] * s; mat[6] = mat[6] * s; mat[7] = mat[7] * s; mat[8] = mat[8] * s; mat[9] = mat[9] * s; mat[10] = mat[10] * s; mat[11] = mat[11] * s; }; const mat4ScaleX = (mat, s) => { mat[0] = mat[0] * s; mat[1] = mat[1] * s; mat[2] = mat[2] * s; mat[3] = mat[3] * s; }; const mat4ScaleY = (mat, s) => { mat[4] = mat[4] * s; mat[5] = mat[5] * s; mat[6] = mat[6] * s; mat[7] = mat[7] * s; }; const mat4RotateX = (mat, rad) => { const s = Math.sin(rad); const c = Math.cos(rad); const a10 = mat[4]; const a11 = mat[5]; const a12 = mat[6]; const a13 = mat[7]; const a20 = mat[8]; const a21 = mat[9]; const a22 = mat[10]; const a23 = mat[11]; mat[4] = a10 * c + a20 * s; mat[5] = a11 * c + a21 * s; mat[6] = a12 * c + a22 * s; mat[7] = a13 * c + a23 * s; mat[8] = a20 * c - a10 * s; mat[9] = a21 * c - a11 * s; mat[10] = a22 * c - a12 * s; mat[11] = a23 * c - a13 * s; }; const mat4RotateY = (mat, rad) => { const s = Math.sin(rad); const c = Math.cos(rad); const a00 = mat[0]; const a01 = mat[1]; const a02 = mat[2]; const a03 = mat[3]; const a20 = mat[8]; const a21 = mat[9]; const a22 = mat[10]; const a23 = mat[11]; mat[0] = a00 * c - a20 * s; mat[1] = a01 * c - a21 * s; mat[2] = a02 * c - a22 * s; mat[3] = a03 * c - a23 * s; mat[8] = a00 * s + a20 * c; mat[9] = a01 * s + a21 * c; mat[10] = a02 * s + a22 * c; mat[11] = a03 * s + a23 * c; }; const mat4RotateZ = (mat, rad) => { const s = Math.sin(rad); const c = Math.cos(rad); const a00 = mat[0]; const a01 = mat[1]; const a02 = mat[2]; const a03 = mat[3]; const a10 = mat[4]; const a11 = mat[5]; const a12 = mat[6]; const a13 = mat[7]; mat[0] = a00 * c + a10 * s; mat[1] = a01 * c + a11 * s; mat[2] = a02 * c + a12 * s; mat[3] = a03 * c + a13 * s; mat[4] = a10 * c - a00 * s; mat[5] = a11 * c - a01 * s; mat[6] = a12 * c - a02 * s; mat[7] = a13 * c - a03 * s; }; var degToRad = (degrees) => degrees * Math.PI / 180; var imageFragmentShader = "\n##head\nin vec2 vTexCoord;uniform sampler2D uTexture;uniform sampler2D uTextureMarkup;uniform sampler2D uTextureBlend;uniform vec2 uTextureSize;uniform float uOpacity;uniform vec4 uFillColor;uniform vec4 uOverlayColor;uniform mat4 uColorMatrix;uniform vec4 uColorOffset;uniform float uClarityKernel[9];uniform float uClarityKernelWeight;uniform float uColorGamma;uniform float uColorVignette;uniform float uMaskClip;uniform float uMaskOpacity;uniform float uMaskBounds[4];uniform float uMaskCornerRadius[4];uniform float uMaskFeather[8];vec4 applyGamma(vec4 c,float g){c.r=pow(c.r,g);c.g=pow(c.g,g);c.b=pow(c.b,g);return c;}vec4 applyColorMatrix(vec4 c,mat4 m,vec4 o){vec4 cM=(c*m)+o;cM*=cM.a;return cM;}vec4 applyConvolutionMatrix(vec4 c,float k0,float k1,float k2,float k3,float k4,float k5,float k6,float k7,float k8,float w){vec2 pixel=vec2(1)/uTextureSize;vec4 colorSum=texture(uTexture,vTexCoord-pixel)*k0+texture(uTexture,vTexCoord+pixel*vec2(0.0,-1.0))*k1+texture(uTexture,vTexCoord+pixel*vec2(1.0,-1.0))*k2+texture(uTexture,vTexCoord+pixel*vec2(-1.0,0.0))*k3+texture(uTexture,vTexCoord)*k4+texture(uTexture,vTexCoord+pixel*vec2(1.0,0.0))*k5+texture(uTexture,vTexCoord+pixel*vec2(-1.0,1.0))*k6+texture(uTexture,vTexCoord+pixel*vec2(0.0,1.0))*k7+texture(uTexture,vTexCoord+pixel)*k8;vec4 color=vec4((colorSum/w).rgb,c.a);color.rgb=clamp(color.rgb,0.0,1.0);return color;}vec4 applyVignette(vec4 c,vec2 pos,vec2 center,float v){float d=distance(pos,center)/length(center);float f=1.0-(d*abs(v));if(v>0.0){c.rgb*=f;}else if(v<0.0){c.rgb+=(1.0-f)*(1.0-c.rgb);}return c;}vec4 blendPremultipliedAlpha(vec4 back,vec4 front){return front+(back*(1.0-front.a));}void main(){float x=gl_FragCoord.x;float y=gl_FragCoord.y;float a=1.0;float maskTop=uMaskBounds[0];float maskRight=uMaskBounds[1];float maskBottom=uMaskBounds[2];float maskLeft=uMaskBounds[3];float leftFeatherOpacity=step(uMaskFeather[1],x)*uMaskFeather[0]+((1.0-uMaskFeather[0])*smoothstep(uMaskFeather[1],uMaskFeather[3],x));float rightFeatherOpacity=(1.0-step(uMaskFeather[7],x))*uMaskFeather[4]+((1.0-uMaskFeather[4])*smoothstep(uMaskFeather[7],uMaskFeather[5],x));a*=leftFeatherOpacity*rightFeatherOpacity;float overlayColorAlpha=(smoothstep(maskLeft,maskLeft+1.0,x)*(1.0-smoothstep(maskRight-1.0,maskRight,x))*(1.0-step(maskTop,y))*step(maskBottom,y));if(uOverlayColor.a==0.0){a*=overlayColorAlpha;}vec2 offset=vec2(maskLeft,maskBottom);vec2 size=vec2(maskRight-maskLeft,maskTop-maskBottom)*.5;vec2 center=offset.xy+size.xy;int pixelX=int(step(center.x,x));int pixelY=int(step(y,center.y));float cornerRadius=0.0;if(pixelX==0&&pixelY==0)cornerRadius=uMaskCornerRadius[0];if(pixelX==1&&pixelY==0)cornerRadius=uMaskCornerRadius[1];if(pixelX==0&&pixelY==1)cornerRadius=uMaskCornerRadius[2];if(pixelX==1&&pixelY==1)cornerRadius=uMaskCornerRadius[3];float cornerOffset=sign(cornerRadius)*length(max(abs(gl_FragCoord.xy-size-offset)-size+cornerRadius,0.0))-cornerRadius;float cornerOpacity=1.0-smoothstep(0.0,1.0,cornerOffset);a*=cornerOpacity;vec2 scaledPoint=vec2(vTexCoord.x*uTextureSize.x,vTexCoord.y*uTextureSize.y);a*=smoothstep(0.0,1.0,uTextureSize.x-scaledPoint.x);a*=smoothstep(0.0,1.0,uTextureSize.y-scaledPoint.y);a*=smoothstep(0.0,1.0,scaledPoint.x);a*=smoothstep(0.0,1.0,scaledPoint.y);vec4 color=texture(uTexture,vTexCoord);color=blendPremultipliedAlpha(color,texture(uTextureBlend,vTexCoord));if(uClarityKernelWeight!=-1.0){color=applyConvolutionMatrix(color,uClarityKernel[0],uClarityKernel[1],uClarityKernel[2],uClarityKernel[3],uClarityKernel[4],uClarityKernel[5],uClarityKernel[6],uClarityKernel[7],uClarityKernel[8],uClarityKernelWeight);}color=applyGamma(color,uColorGamma);color=applyColorMatrix(color,uColorMatrix,uColorOffset);color=blendPremultipliedAlpha(uFillColor,color);color*=a;if(uColorVignette!=0.0){vec2 pos=gl_FragCoord.xy-offset;color=applyVignette(color,pos,center-offset,uColorVignette);}color=blendPremultipliedAlpha(color,texture(uTextureMarkup,vTexCoord));vec4 overlayColor=uOverlayColor*(1.0-overlayColorAlpha);overlayColor.rgb*=overlayColor.a;color=blendPremultipliedAlpha(color,overlayColor);if(uOverlayColor.a>0.0&&color.a<1.0&&uFillColor.a>0.0){color=blendPremultipliedAlpha(uFillColor,overlayColor);}color*=uOpacity;fragColor=color;}"; // eslint-disable-line var imageVertexShader = "\n##head\n##text\nvoid main(){vTexCoord=aTexCoord;gl_Position=uMatrix*aPosition;}"; // eslint-disable-line var pathVertexShader = "#version 300 es\n\nin vec4 aPosition;in vec2 aNormal;in float aMiter;out vec2 vNormal;out float vMiter;out float vWidth;uniform float uWidth;uniform mat4 uMatrix;void main(){vMiter=aMiter;vNormal=aNormal;vWidth=(uWidth*.5)+1.0;gl_Position=uMatrix*vec4(aPosition.x+(aNormal.x*vWidth*aMiter),aPosition.y+(aNormal.y*vWidth*aMiter),0,1);}"; // eslint-disable-line var pathFragmentShader = "\n##head\n##mask\nin vec2 vNormal;in float vMiter;in float vWidth;uniform float uWidth;uniform vec4 uColor;uniform vec4 uCanvasColor;void main(){vec4 fillColor=uColor;float m=mask(gl_FragCoord.x,gl_FragCoord.y,uMaskBounds,uMaskOpacity);if(m<=0.0)discard;fillColor.a*=clamp(smoothstep(vWidth-.5,vWidth-1.0,abs(vMiter)*vWidth),0.0,1.0);fillColor.rgb*=fillColor.a;fillColor.rgb*=m;fillColor.rgb+=(1.0-m)*(uCanvasColor.rgb*fillColor.a);fragColor=fillColor;}"; // eslint-disable-line var rectVertexShader = "\n##head\n##text\nin vec2 aRectCoord;out vec2 vRectCoord;void main(){vTexCoord=aTexCoord;vRectCoord=aRectCoord;\n##matrix\n}"; // eslint-disable-line var rectFragmentShader = "\n##head\n##mask\nin vec2 vTexCoord;in vec2 vRectCoord;uniform sampler2D uTexture;uniform vec4 uTextureColor;uniform float uTextureOpacity;uniform vec4 uColor;uniform float uCornerRadius[4];uniform vec2 uSize;uniform vec2 uPosition;uniform vec4 uCanvasColor;uniform int uInverted;void main(){\n##init\n##colorize\n##edgeaa\n##cornerradius\n##maskfeatherapply\nif(uInverted==1)a=1.0-a;\n##maskapply\n##fragcolor\n}"; // eslint-disable-line var ellipseVertexShader = "\n##head\n##text\nout vec2 vTexCoordDouble;void main(){vTexCoordDouble=vec2(aTexCoord.x*2.0-1.0,aTexCoord.y*2.0-1.0);vTexCoord=aTexCoord;\n##matrix\n}"; // eslint-disable-line var ellipseFragmentShader = "\n##head\n##mask\nin vec2 vTexCoord;in vec2 vTexCoordDouble;uniform sampler2D uTexture;uniform float uTextureOpacity;uniform vec2 uRadius;uniform vec4 uColor;uniform int uInverted;uniform vec4 uCanvasColor;void main(){\n##init\nfloat ar=uRadius.x/uRadius.y;vec2 rAA=vec2(uRadius.x-1.0,uRadius.y-(1.0/ar));vec2 scaledPointSq=vec2((vTexCoordDouble.x*uRadius.x)*(vTexCoordDouble.x*uRadius.x),(vTexCoordDouble.y*uRadius.y)*(vTexCoordDouble.y*uRadius.y));float p=(scaledPointSq.x/(uRadius.x*uRadius.x))+(scaledPointSq.y/(uRadius.y*uRadius.y));float pAA=(scaledPointSq.x/(rAA.x*rAA.x))+(scaledPointSq.y/(rAA.y*rAA.y));a=smoothstep(1.0,p/pAA,p);if(uInverted==1)a=1.0-a;\n##maskapply\n##fragcolor\n}"; // eslint-disable-line var triangleVertexShader = "\n##head\nvoid main(){\n##matrix\n}"; // eslint-disable-line var triangleFragmentShader = "\n##head\n##mask\nuniform vec4 uColor;uniform vec4 uCanvasColor;void main(){vec4 fillColor=uColor;\n##maskapply\nfillColor.rgb*=fillColor.a;fillColor.rgb*=m;fillColor.rgb+=(1.0-m)*(uCanvasColor.rgb*fillColor.a);fragColor=fillColor;}"; // eslint-disable-line const createPathSegment = (vertices, index, a, b, c) => { const ab = vectorNormalize(vectorCreate(b.x - a.x, b.y - a.y)); const bc = vectorNormalize(vectorCreate(c.x - b.x, c.y - b.y)); const tangent = vectorNormalize(vectorCreate(ab.x + bc.x, ab.y + bc.y)); const miter = vectorCreate(-tangent.y, tangent.x); const normal = vectorCreate(-ab.y, ab.x); // limit miter length (TEMP fix to prevent spikes, should eventually add caps) const miterLength = Math.min(1 / vectorDot(miter, normal), 5); vertices[index] = b.x; vertices[index + 1] = b.y; vertices[index + 2] = miter.x * miterLength; vertices[index + 3] = miter.y * miterLength; vertices[index + 4] = -1; vertices[index + 5] = b.x; vertices[index + 6] = b.y; vertices[index + 7] = miter.x * miterLength; vertices[index + 8] = miter.y * miterLength; vertices[index + 9] = 1; }; const createPathVertices = (points, close) => { let a, b, c, i = 0; const l = points.length; const stride = 10; const vertices = new Float32Array((close ? l + 1 : l) * stride); const first = points[0]; const last = points[l - 1]; for (i = 0; i < l; i++) { a = points[i - 1]; b = points[i]; c = points[i + 1]; // if previous point not available use inverse vector to next point if (!a) a = close ? last : vectorCreate(b.x + (b.x - c.x), b.y + (b.y - c.y)); // if next point not available use inverse vector from previous point if (!c) c = close ? first : vectorCreate(b.x + (b.x - a.x), b.y + (b.y - a.y)); createPathSegment(vertices, i * stride, a, b, c); } if (close) createPathSegment(vertices, l * stride, last, first, points[1]); return vertices; }; const rectPointsToVertices = (points) => { // [tl, tr, br, bl] // B D // | \ | // A C const vertices = new Float32Array(8); vertices[0] = points[3].x; vertices[1] = points[3].y; vertices[2] = points[0].x; vertices[3] = points[0].y; vertices[4] = points[2].x; vertices[5] = points[2].y; vertices[6] = points[1].x; vertices[7] = points[1].y; return vertices; }; const trianglePointToVertices = (points) => { const vertices = new Float32Array(6); vertices[0] = points[0].x; vertices[1] = points[0].y; vertices[2] = points[1].x; vertices[3] = points[1].y; vertices[4] = points[2].x; vertices[5] = points[2].y; return vertices; }; const createRectPoints = (rect, rotation = 0, flipX, flipY) => { const corners = rectGetCorners(rect); const cx = rect.x + rect.width * 0.5; const cy = rect.y + rect.height * 0.5; if (flipX || flipY) vectorsFlip(corners, flipX, flipY, cx, cy); if (rotation !== 0) vectorsRotate(corners, rotation, cx, cy); return corners; }; const createEllipseOutline = (x, y, width, height, rotation, flipX, flipY) => { const rx = Math.abs(width) * 0.5; const ry = Math.abs(height) * 0.5; const size = Math.abs(width) + Math.abs(height); const precision = Math.max(20, Math.round(size / 6)); return ellipseToPolygon(vectorCreate(x + rx, y + ry), rx, ry, rotation, flipX, flipY, precision); }; const createRectOutline = (x, y, width, height, rotation, cornerRadius, flipX, flipY) => { const points = []; if (cornerRadius.every((v) => v === 0)) { points.push(vectorCreate(x, y), // top left corner vectorCreate(x + width, y), // top right corner vectorCreate(x + width, y + height), // bottom right corner vectorCreate(x, y + height) // bottom left corner ); } else { const [tl, tr, bl, br] = cornerRadius; const l = x; const r = x + width; const t = y; const b = y + height; // start at end of top left corner points.push(vectorCreate(l + tl, t)); pushRectCornerPoints(points, r - tr, t + tr, tr, -1); // move to bottom right corner points.push(vectorCreate(r, t + tr)); pushRectCornerPoints(points, r - br, b - br, br, 0); // move to bottom left corner points.push(vectorCreate(r - br, b)); pushRectCornerPoints(points, l + bl, b - bl, bl, 1); // move to top left corner points.push(vectorCreate(l, b - bl)); pushRectCornerPoints(points, l + tl, t + tl, tl, 2); } if (flipX || flipY) vectorsFlip(points, flipX, flipY, x + width * 0.5, y + height * 0.5); if (rotation) vectorsRotate(points, rotation, x + width * 0.5, y + height * 0.5); return points; }; const pushRectCornerPoints = (points, x, y, radius, offset) => { const precision = Math.min(20, Math.max(4, Math.round(radius / 2))); let p = 0; let s = 0; let rx = 0; let ry = 0; let i = 0; for (; i < precision; i++) { p = i / precision; s = offset * HALF_PI + p * HALF_PI; rx = radius * Math.cos(s); ry = radius * Math.sin(s); points.push(vectorCreate(x + rx, y + ry)); } }; let limit = null; var getWebGLTextureSizeLimit = () => { if (limit !== null) return limit; const canvas = h('canvas'); const gl = getWebGLContext(canvas); limit = gl ? gl.getParameter(gl.MAX_TEXTURE_SIZE) : undefined; releaseCanvas(canvas); return limit; }; // prettier-ignore // B D // | \ | // A C const RECT_UV = new Float32Array([ 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, ]); const CLARITY_IDENTITY = [0, 0, 0, 0, 1, 0, 0, 0, 0]; const COLOR_MATRIX_IDENTITY$1 = [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0]; const TEXTURE_TRANSPARENT_INDEX = 0; const TEXTURE_PREVIEW_BLEND_INDEX = 1; const TEXTURE_PREVIEW_MARKUP_INDEX = 2; const TEXTURE_PREVIEW_INDEX = 3; const TEXTURE_SHAPE_INDEX = 4; const COLOR_TRANSPARENT = [0, 0, 0, 0]; const NO_CORNERS = [0, 0, 0, 0]; const calculateBackgroundUVMap = (width, height, backgroundSize, backgroundPosition, viewPixelDensity) => { if (!backgroundSize || !backgroundPosition) return RECT_UV; const x = backgroundPosition.x / backgroundSize.width; const y = backgroundPosition.y / backgroundSize.height; let w = width / backgroundSize.width / viewPixelDensity; let h = height / backgroundSize.height / viewPixelDensity; w -= x; h -= y; // prettier-ignore // B D // | \ | // A C // bottom left const ax = -x; const ay = h; // top left const bx = -x; const by = -y; // bottom right const cx = w; const cy = h; // top right const dx = w; const dy = -y; return new Float32Array([ ax, ay, bx, by, cx, cy, dx, dy, ]); }; const limitCornerRadius = (r, size) => { return Math.floor(clamp(r, 0, Math.min((size.width - 1) * 0.5, (size.height - 1) * 0.5))); }; var createWebGLCanvas = (canvas) => { const viewSize = { width: 0, height: 0 }; const viewSizeVisual = { width: 0, height: 0 }; const textureSizeLimit = getWebGLTextureSizeLimit() || 1024; let viewAspectRatio; let viewPixelDensity; const markupMatrixCanvas = mat4Create(); const markupMatrixFrameBuffer = mat4Create(); let markupMatrix; let maskTop; let maskRight; let maskBottom; let maskLeft; let maskOpacity; let maskBounds; let IMAGE_MASK_FEATHER; // updated when viewport is resized let RECT_MASK_FEATHER; let CANVAS_COLOR_R = 0; let CANVAS_COLOR_G = 0; let CANVAS_COLOR_B = 0; const indexTextureMap = new Map([]); // resize view const resize = (width, height, pixelDensity) => { // density viewPixelDensity = pixelDensity; // visual size viewSizeVisual.width = width; viewSizeVisual.height = height; // size viewSize.width = width * viewPixelDensity; viewSize.height = height * viewPixelDensity; // calculate the aspect ratio, we use this to determine quad size viewAspectRatio = getAspectRatio(viewSize.width, viewSize.height); // sync dimensions with image data canvas.width = viewSize.width; canvas.height = viewSize.height; // update canvas markup matrix mat4Ortho(markupMatrixCanvas, 0, viewSize.width, viewSize.height, 0, -1, 1); IMAGE_MASK_FEATHER = [1, 0, 1, 0, 1, viewSizeVisual.width, 1, viewSizeVisual.width]; }; // fov is fixed const FOV = degToRad(30); const FOV_TAN_HALF = Math.tan(FOV / 2); // get gl drawing context const gl = getWebGLContext(canvas, { antialias: false, alpha: false, premultipliedAlpha: true, }); // no drawing context received, exit if (!gl) return; // enable derivatives gl.getExtension('OES_standard_derivatives'); // toggle gl settings gl.disable(gl.DEPTH_TEST); // set blend mode, we need it for alpha blending gl.enable(gl.BLEND); /* https://webglfundamentals.org/webgl/lessons/webgl-and-alpha.html most if not all Canvas 2D implementations work with pre-multiplied alpha. That means when you transfer them to WebGL and UNPACK_PREMULTIPLY_ALPHA_WEBGL is false WebGL will convert them back to un-premultipiled. With pre-multiplied alpha on, [1, .5, .5, 0] does not exist, it's always [0, 0, 0, 0] */ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); // something to look into: // gl.UNPACK_COLORSPACE_CONVERSION_WEBGL const transparentTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, transparentTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, // width 1, // height 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(COLOR_TRANSPARENT) // transparent background ); indexTextureMap.set(TEXTURE_TRANSPARENT_INDEX, transparentTexture); // create image markup texture and framebuffer const imageMarkupTexture = gl.createTexture(); indexTextureMap.set(TEXTURE_PREVIEW_MARKUP_INDEX, imageMarkupTexture); const markupFramebuffer = gl.createFramebuffer(); // create image blend texture and framebuffer const imageBlendTexture = gl.createTexture(); indexTextureMap.set(TEXTURE_PREVIEW_BLEND_INDEX, imageBlendTexture); const blendFramebuffer = gl.createFramebuffer(); // color mask not set (this needs to run otherwise Firefox 93+ renders incorrectly) gl.colorMask(true, true, true, true); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); // #region image // create default pixel drawing program, supports what we need const imageShader = createShader(gl, imageVertexShader, imageFragmentShader, ['aPosition', 'aTexCoord'], [ 'uMatrix', 'uTexture', 'uTextureBlend', 'uTextureMarkup', 'uTextureSize', 'uColorGamma', 'uColorVignette', 'uColorOffset', 'uColorMatrix', 'uClarityKernel', 'uClarityKernelWeight', 'uOpacity', 'uMaskOpacity', 'uMaskBounds', 'uMaskCornerRadius', 'uMaskFeather', 'uFillColor', 'uOverlayColor', ]); // create image buffers const imagePositionsBuffer = gl.createBuffer(); const texturePositionsBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, texturePositionsBuffer); gl.bufferData(gl.ARRAY_BUFFER, RECT_UV, gl.STATIC_DRAW); const drawImage = (texture, textureSize, originX, originY, translateX, translateY, rotateX, rotateY, rotateZ, scale, colorMatrix = COLOR_MATRIX_IDENTITY$1, opacity = 1, clarity, gamma = 1, vignette = 0, maskFeather = IMAGE_MASK_FEATHER, maskCornerRadius = NO_CORNERS, imageBackgroundColor = COLOR_TRANSPARENT, imageOverlayColor = COLOR_TRANSPARENT, enableMarkup = false, enableBlend = false) => { // update image texture const imageWidth = textureSize.width * viewPixelDensity; const imageHeight = textureSize.height * viewPixelDensity; const l = imageWidth * -0.5; const t = imageHeight * 0.5; const r = imageWidth * 0.5; const b = imageHeight * -0.5; // prettier-ignore // B D // | \ | // A C const imagePositions = new Float32Array([ l, b, 0, l, t, 0, r, b, 0, r, t, 0, ]); gl.bindBuffer(gl.ARRAY_BUFFER, imagePositionsBuffer); gl.bufferData(gl.ARRAY_BUFFER, imagePositions, gl.STATIC_DRAW); // move image backwards so it's presented in actual pixel size const viewZ = // 1. we calculate the z offset required to have the // image height match the view height /* /| / | / | height / 2 / | f / 2 /__z_| \ | \ | \ | \ | \| */ (textureSize.height / 2 / FOV_TAN_HALF) * // 2. we want to render the image at the actual height, viewsize / height gets us results in a 1:1 presentation (viewSize.height / textureSize.height) * // 3. z has to be negative, therefor multiply by -1 -1; // convert to pixel density translateX *= viewPixelDensity; translateY *= viewPixelDensity; originX *= viewPixelDensity; originY *= viewPixelDensity; // get shader params const { program, locations } = imageShader; // apply const matrix = mat4Create(); mat4Perspective(matrix, FOV, viewAspectRatio, 1, -viewZ * 2); // move image mat4Translate(matrix, translateX, -translateY, viewZ); // set rotation origin in view mat4Translate(matrix, originX, -originY, 0); // rotate image mat4RotateZ(matrix, -rotateZ); // resize mat4Scale(matrix, scale); // reset rotation origin mat4Translate(matrix, -originX, originY, 0); // flip mat4RotateY(matrix, rotateY); mat4RotateX(matrix, rotateX); // // tell context to draw preview // gl.useProgram(program); gl.enableVertexAttribArray(locations.aPosition); gl.enableVertexAttribArray(locations.aTexCoord); // set up texture gl.uniform1i(locations.uTexture, TEXTURE_PREVIEW_INDEX); gl.uniform2f(locations.uTextureSize, textureSize.width, textureSize.height); gl.activeTexture(gl.TEXTURE0 + TEXTURE_PREVIEW_INDEX); gl.bindTexture(gl.TEXTURE_2D, texture); // set up blend texture const blendTextureIndex = enableBlend ? TEXTURE_PREVIEW_BLEND_INDEX : TEXTURE_TRANSPARENT_INDEX; const blendTexture = indexTextureMap.get(blendTextureIndex); gl.uniform1i(locations.uTextureBlend, blendTextureIndex); gl.activeTexture(gl.TEXTURE0 + blendTextureIndex); gl.bindTexture(gl.TEXTURE_2D, blendTexture); // set up markup texture const markupTextureIndex = enableMarkup ? TEXTURE_PREVIEW_MARKUP_INDEX : TEXTURE_TRANSPARENT_INDEX; const markupTexture = indexTextureMap.get(markupTextureIndex); gl.uniform1i(locations.uTextureMarkup, markupTextureIndex); gl.activeTexture(gl.TEXTURE0 + markupTextureIndex); gl.bindTexture(gl.TEXTURE_2D, markupTexture); // set up buffers gl.bindBuffer(gl.ARRAY_BUFFER, imagePositionsBuffer); gl.vertexAttribPointer(locations.aPosition, 3, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, texturePositionsBuffer); gl.vertexAttribPointer(locations.aTexCoord, 2, gl.FLOAT, false, 0, 0); // update matrix gl.uniformMatrix4fv(locations.uMatrix, false, matrix); // overlay color gl.uniform4fv(locations.uOverlayColor, imageOverlayColor); gl.uniform4fv(locations.uFillColor, imageBackgroundColor); // convolution let clarityWeight; if (!clarity || arrayEqual(clarity, CLARITY_IDENTITY)) { clarity = CLARITY_IDENTITY; clarityWeight = -1; } else { clarityWeight = clarity.reduce((prev, curr) => prev + curr, 0); clarityWeight = clarityWeight <= 0 ? 1 : clarityWeight; } gl.uniform1fv(locations.uClarityKernel, clarity); gl.uniform1f(locations.uClarityKernelWeight, clarityWeight); gl.uniform1f(locations.uColorGamma, 1.0 / gamma); gl.uniform1f(locations.uColorVignette, vignette); // set color matrix values gl.uniform4f(locations.uColorOffset, colorMatrix[4], colorMatrix[9], colorMatrix[14], colorMatrix[19]); gl.uniformMatrix4fv(locations.uColorMatrix, false, [ colorMatrix[0], colorMatrix[1], colorMatrix[2], colorMatrix[3], colorMatrix[5], colorMatrix[6], colorMatrix[7], colorMatrix[8], colorMatrix[10], colorMatrix[11], colorMatrix[12], colorMatrix[13], colorMatrix[15], colorMatrix[16], colorMatrix[17], colorMatrix[18], ]); // opacity level gl.uniform1f(locations.uOpacity, opacity); // mask gl.uniform1f(locations.uMaskOpacity, maskOpacity); gl.uniform1fv(locations.uMaskBounds, maskBounds); gl.uniform1fv(locations.uMaskCornerRadius, maskCornerRadius.map((v) => v * viewPixelDensity)); gl.uniform1fv(locations.uMaskFeather, maskFeather.map((v, i) => (i % 2 === 0 ? v : v * viewPixelDensity))); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); gl.disableVertexAttribArray(locations.aPosition); gl.disableVertexAttribArray(locations.aTexCoord); }; //#endregion // #region path const pathShader = createShader(gl, pathVertexShader, pathFragmentShader, ['aPosition', 'aNormal', 'aMiter'], ['uColor', 'uCanvasColor', 'uMatrix', 'uWidth', 'uMaskBounds', 'uMaskOpacity']); const pathBuffer = gl.createBuffer(); const strokePath = (points, width, color, close = false) => { const { program, locations } = pathShader; gl.useProgram(program); gl.enableVertexAttribArray(locations.aPosition); gl.enableVertexAttribArray(locations.aNormal); gl.enableVertexAttribArray(locations.aMiter); const vertices = createPathVertices(points, close); const stride = Float32Array.BYTES_PER_ELEMENT * 5; const normalOffset = Float32Array.BYTES_PER_ELEMENT * 2; // at position 2 const miterOffset = Float32Array.BYTES_PER_ELEMENT * 4; // at position 4 gl.uniform1f(locations.uWidth, width); // add 1 so we can feather the edges gl.uniform4fv(locations.uColor, color); gl.uniformMatrix4fv(locations.uMatrix, false, markupMatrix); gl.uniform4f(locations.uCanvasColor, CANVAS_COLOR_R, CANVAS_COLOR_G, CANVAS_COLOR_B, 1); gl.uniform1fv(locations.uMaskBounds, maskBounds); gl.uniform1f(locations.uMaskOpacity, maskOpacity); gl.bindBuffer(gl.ARRAY_BUFFER, pathBuffer); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); gl.vertexAttribPointer(locations.aPosition, 2, gl.FLOAT, false, stride, 0); gl.vertexAttribPointer(locations.aNormal, 2, gl.FLOAT, false, stride, normalOffset); gl.vertexAttribPointer(locations.aMiter, 1, gl.FLOAT, false, stride, miterOffset); gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length / 5); gl.disableVertexAttribArray(locations.aPosition); gl.disableVertexAttribArray(locations.aNormal); gl.disableVertexAttribArray(locations.aMiter); }; //#endregion // #region triangle const triangleShader = createShader(gl, triangleVertexShader, triangleFragmentShader, ['aPosition'], ['uColor', 'uCanvasColor', 'uMatrix', 'uMaskBounds', 'uMaskOpacity']); const triangleBuffer = gl.createBuffer(); const fillTriangle = (vertices, backgroundColor) => { const { program, locations } = triangleShader; gl.useProgram(program); gl.enableVertexAttribArray(locations.aPosition); gl.uniform4fv(locations.uColor, backgroundColor); gl.uniformMatrix4fv(locations.uMatrix, false, markupMatrix); gl.uniform1fv(locations.uMaskBounds, maskBounds); gl.uniform1f(locations.uMaskOpacity, maskOpacity); gl.uniform4f(locations.uCanvasColor, CANVAS_COLOR_R, CANVAS_COLOR_G, CANVAS_COLOR_B, 1); gl.bindBuffer(gl.ARRAY_BUFFER, triangleBuffer); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); gl.vertexAttribPointer(locations.aPosition, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length / 2); gl.disableVertexAttribArray(locations.aPosition); return vertices; }; //#endregion // #region rect const rectShaderAttributes = ['aPosition', 'aTexCoord', 'aRectCoord']; const rectShaderUniforms = [ 'uTexture', 'uColor', 'uMatrix', 'uCanvasColor', 'uTextureColor', 'uTextureOpacity', 'uPosition', 'uSize', 'uMaskBounds', 'uMaskOpacity', 'uMaskFeather', 'uCornerRadius', 'uInverted', ]; const rectShader = createShader(gl, rectVertexShader, rectFragmentShader, rectShaderAttributes, rectShaderUniforms); const rectBuffer = gl.createBuffer(); const rectTextureBuffer = gl.createBuffer(); const rectCornerBuffer = gl.createBuffer(); const fillRect = (vertices, width, height, cornerRadius, backgroundColor, backgroundImage = transparentTexture, opacity = 1.0, colorFilter = COLOR_TRANSPARENT, uv = RECT_UV, maskFeather = RECT_MASK_FEATHER, inverted) => { const { program, locations } = rectShader; gl.useProgram(program); gl.enableVertexAttribArray(locations.aPosition); gl.enableVertexAttribArray(locations.aTexCoord); gl.enableVertexAttribArray(locations.aRectCoord); gl.uniform4fv(locations.uColor, backgroundColor); gl.uniform2fv(locations.uSize, [width, height]); gl.uniform2fv(locations.uPosition, [vertices[2], vertices[3]]); gl.uniform1i(locations.uInverted, inverted ? 1 : 0); gl.uniform1fv(locations.uCornerRadius, cornerRadius); gl.uniform4f(locations.uCanvasColor, CANVAS_COLOR_R, CANVAS_COLOR_G, CANVAS_COLOR_B, 1); // mask gl.uniform1fv(locations.uMaskFeather, maskFeather.map((v, i) => (i % 2 === 0 ? v : v * viewPixelDensity))); gl.uniform1fv(locations.uMaskBounds, maskBounds); gl.uniform1f(locations.uMaskOpacity, maskOpacity); gl.uniformMatrix4fv(locations.uMatrix, false, markupMatrix); gl.uniform1i(locations.uTexture, TEXTURE_SHAPE_INDEX); gl.uniform4fv(locations.uTextureColor, colorFilter); gl.uniform1f(locations.uTextureOpacity, opacity); gl.activeTexture(gl.TEXTURE0 + TEXTURE_SHAPE_INDEX); gl.bindTexture(gl.TEXTURE_2D, backgroundImage); gl.bindBuffer(gl.ARRAY_BUFFER, rectTextureBuffer); gl.bufferData(gl.ARRAY_BUFFER, uv, gl.STATIC_DRAW); gl.vertexAttribPointer(locations.aTexCoord, 2, gl.FLOAT, false, 0, 0); // we use these coordinates combined with the size of the rect to interpolate and alias edges gl.bindBuffer(gl.ARRAY_BUFFER, rectCornerBuffer); gl.bufferData(gl.ARRAY_BUFFER, RECT_UV, gl.STATIC_DRAW); gl.vertexAttribPointer(locations.aRectCoord, 2, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, rectBuffer); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); gl.vertexAttribPointer(locations.aPosition, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length / 2); gl.disableVertexAttribArray(locations.aPosition); gl.disableVertexAttribArray(locations.aTexCoord); gl.disableVertexAttribArray(locations.aRectCoord); return vertices; }; //#endregion // #region ellipse const ellipseShader = createShader(gl, ellipseVertexShader, ellipseFragmentShader, ['aPosition', 'aTexCoord'], [ 'uTexture', 'uTextureOpacity', 'uColor', 'uCanvasColor', 'uMatrix', 'uRadius', 'uInverted', 'uMaskBounds', 'uMaskOpacity', ]); const ellipseBuffer = gl.createBuffer(); const ellipseTextureBuffer = gl.createBuffer(); const fillEllipse = (vertices, width, height, backgroundColor, backgroundImage = transparentTexture, uv = RECT_UV, opacity = 1.0, inverted = false) => { const { program, locations } = ellipseShader; gl.useProgram(program); gl.enableVertexAttribArray(locations.aPosition); gl.enableVertexAttribArray(locations.aTexCoord); gl.uniformMatrix4fv(locations.uMatrix, false, markupMatrix); gl.uniform2fv(locations.uRadius, [width * 0.5, height * 0.5]); gl.uniform1i(locations.uInverted, inverted ? 1 : 0); gl.uniform4fv(locations.uColor, backgroundColor); gl.uniform4f(locations.uCanvasColor, CANVAS_COLOR_R, CANVAS_COLOR_G, CANVAS_COLOR_B, 1); gl.uniform1fv(locations.uMaskBounds, maskBounds); gl.uniform1f(locations.uMaskOpacity, maskOpacity); gl.uniform1i(locations.uTexture, TEXTURE_SHAPE_INDEX); gl.uniform1f(locations.uTextureOpacity, opacity); gl.activeTexture(gl.TEXTURE0 + TEXTURE_SHAPE_INDEX); gl.bindTexture(gl.TEXTURE_2D, backgroundImage); gl.bindBuffer(gl.ARRAY_BUFFER, ellipseTextureBuffer); gl.bufferData(gl.ARRAY_BUFFER, uv, gl.STATIC_DRAW); gl.vertexAttribPointer(locations.aTexCoord, 2, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, ellipseBuffer); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); gl.vertexAttribPointer(locations.aPosition, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length / 2); gl.disableVertexAttribArray(locations.aPosition); gl.disableVertexAttribArray(locations.aTexCoord); }; //#endregion // // draw calls // const drawPath = (points, strokeWidth, strokeColor, strokeClose, opacity) => { // is no line if (points.length < 2) return; strokePath(points.map((p) => ({ x: p.x * viewPixelDensity, y: p.y * viewPixelDensity, })), strokeWidth * viewPixelDensity, applyOpacity(strokeColor, opacity), strokeClose); }; const drawTriangle = (points, rotation = 0, flipX = false, flipY = false, backgroundColor, opacity) => { if (!backgroundColor) return; const clonedPoints = points.map((p) => ({ x: p.x * viewPixelDensity, y: p.y * viewPixelDensity, })); const center = convexPolyCentroid(clonedPoints); if (flipX || flipY) vectorsFlip(clonedPoints, flipX, flipY, center.x, center.y); vectorsRotate(clonedPoints, rotation, center.x, center.y); const vertices = trianglePointToVertices(clonedPoints); fillTriangle(vertices, applyOpacity(backgroundColor, opacity)); }; const drawRect = (rect, rotation = 0, flipX = false, flipY = false, cornerRadius, backgroundColor, backgroundImage, backgroundSize = undefined, backgroundPosition = undefined, backgroundUVMap = undefined, strokeWidth, strokeColor, opacity, maskFeather = undefined, colorize, inverted) => { // clone first const rectOut = rectMultiply(rectClone(rect), viewPixelDensity); // has radius, doesn't matter for coordinates const cornerRadiusOut = cornerRadius .map((r) => limitCornerRadius(r || 0, rect)) .map((r) => r * viewPixelDensity); // should fill if (backgroundColor || backgroundImage) { // adjust for edge anti-aliasing, if we don't do this the // visible rectangle will be 1 pixel smaller than the actual rectangle const rectFill = rectClone(rectOut); rectFill.x -= 0.5; rectFill.y -= 0.5; rectFill.width += 1; rectFill.height += 1; const points = createRectPoints(rectFill, rotation, flipX, flipY); const vertices = rectPointsToVertices(points); let color; if (colorize) { color = applyOpacity(colorize); // as 0 transparancy is used to test if the colorize filter should be applied we set it to 0.001 if (color[3] === 0) color[3] = 0.001; } fillRect(vertices, rectFill.width, rectFill.height, cornerRadiusOut, applyOpacity(backgroundColor, opacity), backgroundImage, opacity, color, backgroundUVMap ? new Float32Array(backgroundUVMap) : calculateBackgroundUVMap(rectFill.width, rectFill.height, backgroundSize, backgroundPosition, viewPixelDensity), maskFeather, inverted); } // should draw outline if (strokeWidth) { // fixes issue where stroke would render weirdly strokeWidth = Math.min(strokeWidth, rectOut.width, rectOut.height); strokePath( // rect out is already multiplied by pixel density createRectOutline(rectOut.x, rectOut.y, rectOut.width, rectOut.height, rotation, cornerRadiusOut, flipX, flipY), strokeWidth * viewPixelDensity, applyOpacity(strokeColor, opacity), true); } }; const drawEllipse = (center, rx, ry, rotation, flipX, flipY, backgroundColor, backgroundImage, backgroundSize = undefined, backgroundPosition = undefined, backgroundUVMap = undefined, strokeWidth, strokeColor, opacity, inverted) => { const rectOut = rectMultiply(rectCreate(center.x - rx, center.y - ry, rx * 2, ry * 2), viewPixelDensity); if (backgroundColor || backgroundImage) { // adjust for edge anti-aliasing, if we don't do this the // visible rectangle will be 1 pixel smaller than the actual rectangle const rectFill = rectClone(rectOut); rectFill.x -= 0.5; rectFill.y -= 0.5; rectFill.width += 1.0; rectFill.height += 1.0; const points = createRectPoints(rectFill, rotation, flipX, flipY); const vertices = rectPointsToVertices(points); fillEllipse(vertices, rectFill.width, rectFill.height, applyOpacity(backgroundColor, opacity), backgroundImage, backgroundUVMap ? new Float32Array(backgroundUVMap) : calculateBackgroundUVMap(rectFill.width, rectFill.height, backgroundSize, backgroundPosition, viewPixelDensity), opacity, inverted); } if (strokeWidth) strokePath( // rect out is already multiplied by pixeldensity createEllipseOutline(rectOut.x, rectOut.y, rectOut.width, rectOut.height, rotation, flipX, flipY), strokeWidth * viewPixelDensity, applyOpacity(strokeColor, opacity), true); }; //#endregion const glTextures = new Map(); // let currentMarkupFrameBufferSize = { width: 0, height: 0 }; const imageFramebufferSize = {}; imageFramebufferSize[TEXTURE_PREVIEW_MARKUP_INDEX] = { width: 0, height: 0 }; imageFramebufferSize[TEXTURE_PREVIEW_BLEND_INDEX] = { width: 0, height: 0 }; const drawToImageFramebuffer = (index, buffer, imageSize) => { const textureScalar = Math.min(textureSizeLimit / imageSize.width, textureSizeLimit / imageSize.height, 1); const textureWidth = Math.floor(textureScalar * imageSize.width); const textureHeight = Math.floor(textureScalar * imageSize.height); if (!sizeEqual(imageSize, imageFramebufferSize[index])) { // update preview markup texture gl.bindTexture(gl.TEXTURE_2D, indexTextureMap.get(index)); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, textureWidth, textureHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // set the filtering, we don't need mips gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindFramebuffer(gl.FRAMEBUFFER, buffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, indexTextureMap.get(index), 0); // remember so we know when to update the framebuffer imageFramebufferSize[index] = imageSize; } else { gl.bindFramebuffer(gl.FRAMEBUFFER, buffer); } // switch transformMatrix const w = imageSize.width * viewPixelDensity; const h = imageSize.height * viewPixelDensity; mat4Ortho(markupMatrixFrameBuffer, 0, w, h, 0, -1, 1); mat4Translate(markupMatrixFrameBuffer, 0, h, 0); mat4ScaleX(markupMatrixFrameBuffer, 1); mat4ScaleY(markupMatrixFrameBuffer, -1); markupMatrix = markupMatrixFrameBuffer; // framebuffer lives in image space gl.viewport(0, 0, textureWidth, textureHeight); // always transparent gl.colorMask(true, true, true, true); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); // update rect mask RECT_MASK_FEATHER = [ 1, 0, 1, 0, 1, Math.max(viewSize.width, imageSize.width), 1, Math.max(viewSize.width, imageSize.width), ]; }; return { // draw api drawPath, drawTriangle, drawRect, drawEllipse, drawImage, // texture filters textureFilterNearest: gl.NEAREST, textureFilterLinear: gl.LINEAR, //#region texture management textureCreate: () => { return gl.createTexture(); }, textureUpdate: (texture, source, options) => { glTextures.set(texture, source); return updateTexture(gl, texture, source, options); }, textureSize: (texture) => { return sizeCreateFromAny(glTextures.get(texture)); }, textureDelete: (texture) => { const source = glTextures.get(texture); if (source instanceof HTMLCanvasElement && !source.dataset.retain) releaseCanvas(source); glTextures.delete(texture); gl.deleteTexture(texture); }, //#endregion setCanvasColor(color) { CANVAS_COLOR_R = color[0]; CANVAS_COLOR_G = color[1]; CANVAS_COLOR_B = color[2]; }, drawToCanvas() { gl.bindFramebuffer(gl.FRAMEBUFFER, null); // switch transformMatrix markupMatrix = markupMatrixCanvas; // tell webgl about the viewport gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); // black (or other color depending on background) gl.colorMask(true, true, true, false); gl.clearColor(CANVAS_COLOR_R, CANVAS_COLOR_G, CANVAS_COLOR_B, 1); // gl.clearColor(0.25, 0.25, 0.25, 1); // for debugging gl.clear(gl.COLOR_BUFFER_BIT); // update rect mask RECT_MASK_FEATHER = [1, 0, 1, 0, 1, viewSize.width, 1, viewSize.width]; }, drawToImageBlendBuffer(imageSize) { drawToImageFramebuffer(TEXTURE_PREVIEW_BLEND_INDEX, blendFramebuffer, imageSize); }, drawToImageOverlayBuffer(imageSize) { drawToImageFramebuffer(TEXTURE_PREVIEW_MARKUP_INDEX, markupFramebuffer, imageSize); }, // set mask enableMask(rect, opacity) { const maskX = rect.x * viewPixelDensity; const maskY = rect.y * viewPixelDensity; const maskWidth = rect.width * viewPixelDensity; const maskHeight = rect.height * viewPixelDensity; maskLeft = maskX; maskRight = maskLeft + maskWidth; maskTop = viewSize.height - maskY; maskBottom = viewSize.height - (maskY + maskHeight); maskOpacity = 1.0 - opacity; maskBounds = [maskTop, maskRight, maskBottom, maskLeft]; }, disableMask() { maskLeft = 0; maskRight = viewSize.width; maskTop = viewSize.height; maskBottom = 0; maskOpacity = 1; maskBounds = [maskTop, maskRight, maskBottom, maskLeft]; }, // canvas resize, release() { canvas.width = 1; canvas.height = 1; }, }; }; var isImageBitmap = (obj) => 'close' in obj; /* src/core/ui/components/Canvas.svelte generated by Svelte v3.37.0 */ function create_fragment$M(ctx) { let div; let canvas_1; let mounted; let dispose; return { c() { div = element("div"); canvas_1 = element("canvas"); attr(div, "class", "PinturaCanvas"); }, m(target, anchor) { insert(target, div, anchor); append(div, canvas_1); /*canvas_1_binding*/ ctx[24](canvas_1); if (!mounted) { dispose = [ listen(canvas_1, "measure", /*measure_handler*/ ctx[25]), action_destroyer(measurable.call(null, canvas_1)) ]; mounted = true; } }, p: noop, i: noop, o: noop, d(detaching) { if (detaching) detach(div); /*canvas_1_binding*/ ctx[24](null); mounted = false; run_all(dispose); } }; } function instance$M($$self, $$props, $$invalidate) { let canDraw; let drawUpdate; let $background; let $maskOpacityStore; let $mask; let $imageOverlayColor; let $maskFrameOpacityStore; const blendWithCanvasBackground = (back, front) => { const [bR, bG, bB] = back; const [fR, fG, fB, fA] = front; return [fR * fA + bR * (1 - fA), fG * fA + bG * (1 - fA), fB * fA + bB * (1 - fA), 1]; }; // used to dispatch the 'measure' event const dispatch = createEventDispatcher(); let { animate } = $$props; let { maskRect } = $$props; let { maskOpacity = 1 } = $$props; let { maskFrameOpacity = 0.95 } = $$props; let { pixelRatio = 1 } = $$props; let { backgroundColor } = $$props; let { willRender = passthrough } = $$props; let { loadImageData = passthrough } = $$props; let { images = [] } = $$props; let { interfaceImages = [] } = $$props; // internal props let canvas; let canvasGL = null; let width = null; let height = null; // // springyness for main preview // const updateSpring = (spring, value) => spring.set(value, { hard: !animate }); const SPRING_PROPS = { precision: 0.0001 }; const SPRING_PROPS_FRACTION = { precision: SPRING_PROPS.precision * 0.01 }; // Editor UI const background = tweened(undefined, { duration: 250 }); component_subscribe($$self, background, value => $$invalidate(20, $background = value)); const maskOpacityStore = spring(1, SPRING_PROPS_FRACTION); component_subscribe($$self, maskOpacityStore, value => $$invalidate(21, $maskOpacityStore = value)); const maskFrameOpacityStore = spring(1, SPRING_PROPS_FRACTION); component_subscribe($$self, maskFrameOpacityStore, value => $$invalidate(30, $maskFrameOpacityStore = value)); const mask = writable(); component_subscribe($$self, mask, value => $$invalidate(28, $mask = value)); const imageOverlayColor = writable(); component_subscribe($$self, imageOverlayColor, value => $$invalidate(29, $imageOverlayColor = value)); //#region texture loading and binding const TEXT_TEXTURE_MEASURE_CONTEXT = createSimpleContext(); const Textures = new Map([]); const getImageTexture = (image, imageRendering) => { // no texture yet for this source if (!Textures.has(image)) { // is in loading state when is same as source Textures.set(image, image); // get texture filter mode const filter = imageRendering === "pixelated" ? canvasGL.textureFilterNearest : canvasGL.textureFilterLinear; // already loaded if (!isString(image) && (isImageBitmap(image) || isImageData(image) || isCanvas(image))) { // create texture const texture = canvasGL.textureCreate(); // udpate texture in gl canvas canvasGL.textureUpdate(texture, image, { filter }); // update state we now have a texture Textures.set(image, texture); } else // need to load the image { loadImageData(image).then(data => { // create texture const texture = canvasGL.textureCreate(); // udpate texture in gl canvas canvasGL.textureUpdate(texture, data, { filter }); // update state we now have a texture Textures.set(image, texture); // need to redraw because texture is now available requestAnimationFrame(drawUpdate); }).catch(err => { console.error(err); }); } } return Textures.get(image); }; const getTextTexture = shape => { let { text, textAlign, fontFamily, fontSize, fontWeight, fontVariant, fontStyle, lineHeight, width } = shape; // we need this context to correctly wrap text updateTextContext(TEXT_TEXTURE_MEASURE_CONTEXT, { fontSize, fontFamily, fontWeight, fontVariant, fontStyle, textAlign }); // wrap the text const textString = width ? wrapText(TEXT_TEXTURE_MEASURE_CONTEXT, text, width) : text; // create UID for this texture so we can cache it and fetch it later on const textUID = shapeTextUID({ ...shape, text: textString }); // get texture unit assigned to this specific text shape if (!Textures.has(textUID)) { // TODO: Create power of 2 texture and update texture instead of delete -> replace // we need to create a new texture const ctx = createSimpleContext(); updateTextContext(ctx, { fontSize, fontFamily, fontWeight, fontVariant, fontStyle, textAlign }); // calculate canvas height resizeContextToFitText(ctx, textString, { fontSize, fontFamily, fontWeight, fontVariant, fontStyle, textAlign, lineHeight }); const contextMinWidth = ctx.canvas.width; // scale context to account for italic styles ctx.canvas.width += textPadding; // context resized, we now need to re-apply style updateTextContext(ctx, { fontSize, fontFamily, fontWeight, fontVariant, fontStyle, textAlign, color: [1, 0, 1], // color we'll replace in the shader }); // if so, draw text and update texture drawText$1(ctx, textString, { fontSize, textAlign, lineHeight, lineWidth: contextMinWidth }); Textures.set(textUID, canvasGL.textureUpdate(canvasGL.textureCreate(), ctx.canvas, { filter: canvasGL.textureFilterLinear })); } return Textures.get(textUID); }; const getShapeTexture = shape => { let texture; // let's create textures for backgrounds and texts if (shape.backgroundImage) { texture = getImageTexture(shape.backgroundImage, shape.backgroundImageRendering); } else if (isString(shape.text)) { if (shape.width && shape.width < 1 || shape.height && shape.height < 1) return undefined; texture = getTextTexture(shape); } return texture; }; const isTexture = texture => texture instanceof WebGLTexture; const releaseUnusedTextures = usedTextures => { Textures.forEach((registeredTexture, key) => { const isUsed = !!usedTextures.find(usedTexture => usedTexture === registeredTexture); // stil used, no need to release if (isUsed) return; // remove this texture Textures.delete(key); canvasGL.textureDelete(registeredTexture); }); }; //#endregion //#region drawing const drawImageHelper = ({ data, size, origin, translation, rotation, scale, colorMatrix, opacity, convolutionMatrix, gamma, vignette, maskFeather, maskCornerRadius, backgroundColor, overlayColor, enableShapes, enableBlend }) => { // calculate opaque backgroundColor if backgroundColor is transparent and visible if (backgroundColor && backgroundColor[3] < 1 && backgroundColor[3] > 0) { backgroundColor = blendWithCanvasBackground($background, backgroundColor); } // gets texture to use for this image const texture = getImageTexture(data); // draw the image canvasGL.drawImage(texture, size, origin.x, origin.y, translation.x, translation.y, rotation.x, rotation.y, rotation.z, scale, colorMatrix, clamp(opacity, 0, 1), convolutionMatrix, gamma, vignette, maskFeather, maskCornerRadius, backgroundColor, overlayColor, enableShapes, enableBlend); return texture; }; const backgroundCornersToUVMap = ([tl, tr, br, bl]) => { // tl, tr, br, bl -> bl, tl, br, tr // prettier-ignore // B D // | \ | // A C return [bl.x, bl.y, tl.x, tl.y, br.x, br.y, tr.x, tr.y]; }; const drawShapes = (shapes = []) => { return shapes.map(shape => { // only show texture if shape is finished loading let shapeTexture = !shape._isLoading && getShapeTexture(shape); // get the webgl texture let texture = isTexture(shapeTexture) ? shapeTexture : undefined; if (isArray(shape.points)) { // is triangle if (shape.points.length === 3 && shape.backgroundColor) { canvasGL.drawTriangle(shape.points, shape.rotation, shape.flipX, shape.flipY, shape.backgroundColor, shape.strokeWidth, shape.strokeColor, shape.opacity); } else // is normal path { canvasGL.drawPath(shape.points, shape.strokeWidth, shape.strokeColor, shape.pathClose, shape.opacity); } } else // is ellipse if (isNumber(shape.rx) && isNumber(shape.ry)) { let backgroundSize; let backgroundPosition; canvasGL.drawEllipse(shape, shape.rx, shape.ry, shape.rotation, shape.flipX, shape.flipY, shape.backgroundColor, texture, backgroundSize, backgroundPosition, shape.backgroundCorners && backgroundCornersToUVMap(shape.backgroundCorners), shape.strokeWidth, shape.strokeColor, shape.opacity, shape.inverted); } else // is rect if (isString(shape.text) && texture || shape.width) { const textureSize = texture && canvasGL.textureSize(texture); let colorize = undefined; let shapeRect; let shapeCornerRadius = [ shape.cornerRadius, shape.cornerRadius, shape.cornerRadius, shape.cornerRadius ]; if (shape.width) { shapeRect = shape; } else { shapeRect = { x: shape.x, y: shape.y, ...textureSize }; } let backgroundSize; let backgroundPosition; if (textureSize) { // background should be scaled if (shape.backgroundImage && shape.backgroundSize) { // always respect texture aspect ratio const textureAspectRatio = getAspectRatio(textureSize.width, textureSize.height); // adjust position of background if (shape.backgroundSize === "contain") { const rect = rectContainRect(shape, textureAspectRatio, shapeRect); backgroundSize = sizeCreateFromRect(rect); backgroundPosition = vectorCreate((shape.width - backgroundSize.width) * 0.5, (shape.height - backgroundSize.height) * 0.5); } else if (shape.backgroundSize === "cover") { const rect = rectCoverRect(shape, textureAspectRatio, shapeRect); backgroundSize = sizeCreateFromRect(rect); backgroundPosition = vectorCreate(rect.x, rect.y); backgroundPosition = vectorCreate((shape.width - backgroundSize.width) * 0.5, (shape.height - backgroundSize.height) * 0.5); } else { backgroundSize = shape.backgroundSize; backgroundPosition = shape.backgroundPosition; } } else // is text, "background" should be texture size and be positioned based on alignment if (shape.text && shape.width) { // position texture based on text alignment backgroundSize = textureSize; backgroundPosition = vectorCreate(0, 0); // auto height if (!shape.height) shape.height = textureSize.height; // textPadding so text doesn't clip on left and right edges shape.x -= textPadding; shape.width += textPadding * 2; if (shape.textAlign === "left") { backgroundPosition.x = textPadding; } if (shape.textAlign === "center") { backgroundPosition.x = textPadding * 0.5 + (shape.width - textureSize.width) * 0.5; } if (shape.textAlign === "right") { backgroundPosition.x = shape.width - textureSize.width; } } else if (shape.text) { backgroundPosition = vectorCreate(0, 0); backgroundSize = { width: shapeRect.width, height: shapeRect.height }; // texture is slightly larger because of text padding, need to compensate for this in single line mode shapeRect.width -= textPadding; } if (shape.text) colorize = shape.color; } canvasGL.drawRect(shapeRect, shape.rotation, shape.flipX, shape.flipY, shapeCornerRadius, shape.backgroundColor, texture, backgroundSize, backgroundPosition, shape.backgroundCorners && backgroundCornersToUVMap(shape.backgroundCorners), shape.strokeWidth, shape.strokeColor, shape.opacity, undefined, colorize, shape.inverted); } return shapeTexture; }).filter(Boolean); }; // redraws state const usedTextures = []; const redraw = () => { // reset array of textures used in this draw call usedTextures.length = 0; // get top image shortcut const imagesTop = images[0]; // allow dev to inject more shapes const { blendShapes, annotationShapes, interfaceShapes, decorationShapes, frameShapes } = willRender({ // top image state shortcut opacity: imagesTop.opacity, rotation: imagesTop.rotation, scale: imagesTop.scale, // active images images, // canvas size size: sizeCreate(width, height), // canvas background backgroundColor: [...$background] }); const canvasBackgroundColor = [...$background]; const imagesMask = $mask; const imagesMaskOpacity = clamp($maskOpacityStore, 0, 1); const imagesOverlayColor = $imageOverlayColor; const imagesSize = imagesTop.size; const imagesBackgroundColor = imagesTop.backgroundColor; // no need to draw to blend framebuffer if no redactions const hasBlendShapes = blendShapes.length > 0; // no need to draw to markup framebuffer if no annotations const hasAnnotations = annotationShapes.length > 0; // if image has background color const hasImageBackgroundColor = imagesBackgroundColor[3] > 0; // if the overlay is transparent so we can see the canvas const hasTransparentOverlay = imagesMaskOpacity < 1; // set canvas background color to image background color if is defined if (hasTransparentOverlay && hasImageBackgroundColor) { const backR = canvasBackgroundColor[0]; const backG = canvasBackgroundColor[1]; const backB = canvasBackgroundColor[2]; const frontA = 1 - imagesMaskOpacity; const frontR = imagesBackgroundColor[0] * frontA; const frontG = imagesBackgroundColor[1] * frontA; const frontB = imagesBackgroundColor[2] * frontA; const fA = 1 - frontA; canvasBackgroundColor[0] = frontR + backR * fA; canvasBackgroundColor[1] = frontG + backG * fA; canvasBackgroundColor[2] = frontB + backB * fA; canvasBackgroundColor[3] = 1; } canvasGL.setCanvasColor(canvasBackgroundColor); // if has blend shapes draw blend shapes to framebuffer // TODO: only run this if blend shapes have changed if (hasBlendShapes) { canvasGL.disableMask(); canvasGL.drawToImageBlendBuffer(imagesSize); usedTextures.push(...drawShapes(blendShapes)); } // if has annotations draw annotation shapes to framebuffer // TODO: only run this if annotations have changed if (hasAnnotations) { canvasGL.disableMask(); canvasGL.drawToImageOverlayBuffer(imagesSize); usedTextures.push(...drawShapes(annotationShapes)); } // switch to canvas drawing for other elements canvasGL.drawToCanvas(); canvasGL.enableMask(imagesMask, imagesMaskOpacity); // draw a colored rectangle behind main preview image if (hasImageBackgroundColor) { canvasGL.drawRect(imagesMask, 0, false, false, [0, 0, 0, 0], blendWithCanvasBackground($background, imagesBackgroundColor)); } usedTextures.push(...[...images].reverse().map(image => { return drawImageHelper({ ...image, // enable drawing markup if defined enableShapes: hasAnnotations, // enable drawing redactions if defined enableBlend: hasBlendShapes, // mask and overlay positions mask: imagesMask, maskOpacity: imagesMaskOpacity, overlayColor: imagesOverlayColor }); })); // TODO: move vignette here (draw with colorized circular gradient texture instead of in shader) // draw decorations shapes relative to crop canvasGL.enableMask(imagesMask, 1); usedTextures.push(...drawShapes(decorationShapes)); // draw frames if (frameShapes.length) { const shapesInside = frameShapes.filter(shape => !shape.expandsCanvas); const shapesOutside = frameShapes.filter(shape => shape.expandsCanvas); if (shapesInside.length) { usedTextures.push(...drawShapes(shapesInside)); } if (shapesOutside.length) { // the half pixel helps mask the outside shapes at the correct position canvasGL.enableMask( { x: imagesMask.x + 0.5, y: imagesMask.y + 0.5, width: imagesMask.width - 1, height: imagesMask.height - 1 }, $maskFrameOpacityStore ); usedTextures.push(...drawShapes(shapesOutside)); } } // crop mask not used for interface canvasGL.disableMask(); // frames rendered on the outside // draw custom interface shapes usedTextures.push(...drawShapes(interfaceShapes)); interfaceImages.forEach(image => { canvasGL.enableMask(image.mask, image.maskOpacity); // draw background fill if (image.backgroundColor) { canvasGL.drawRect(image.mask, 0, false, false, image.maskCornerRadius, image.backgroundColor, undefined, undefined, undefined, undefined, undefined, image.opacity, image.maskFeather); } // draw image drawImageHelper({ ...image, // update translation to apply `offset` from top left translation: { x: image.translation.x + image.offset.x - width * 0.5, y: image.translation.y + image.offset.y - height * 0.5 } }); }); canvasGL.disableMask(); // determine which textures can be dropped releaseUnusedTextures(usedTextures); }; //#endregion //#region set up // throttled redrawer let lastDraw = Date.now(); const redrawThrottled = () => { const now = Date.now(); const dist = now - lastDraw; if (dist < 48) return; lastDraw = now; redraw(); }; // returns the render function to use for this browser context const selectFittingRenderFunction = () => isSoftwareRendering() ? redrawThrottled : redraw; // after DOM has been altered, redraw to canvas afterUpdate(() => drawUpdate()); // hook up canvas to WebGL drawer onMount(() => $$invalidate(19, canvasGL = createWebGLCanvas(canvas))); // clean up canvas onDestroy(() => { // if canvas wasn't created we don't need to release it if (!canvasGL) return; // done drawing canvasGL.release(); // force release canvas for Safari releaseCanvas(TEXT_TEXTURE_MEASURE_CONTEXT.canvas); }); function canvas_1_binding($$value) { binding_callbacks[$$value ? "unshift" : "push"](() => { canvas = $$value; $$invalidate(2, canvas); }); } const measure_handler = e => { $$invalidate(0, width = e.detail.width); $$invalidate(1, height = e.detail.height); dispatch("measure", { width, height }); }; $$self.$$set = $$props => { if ("animate" in $$props) $$invalidate(9, animate = $$props.animate); if ("maskRect" in $$props) $$invalidate(10, maskRect = $$props.maskRect); if ("maskOpacity" in $$props) $$invalidate(11, maskOpacity = $$props.maskOpacity); if ("maskFrameOpacity" in $$props) $$invalidate(12, maskFrameOpacity = $$props.maskFrameOpacity); if ("pixelRatio" in $$props) $$invalidate(13, pixelRatio = $$props.pixelRatio); if ("backgroundColor" in $$props) $$invalidate(14, backgroundColor = $$props.backgroundColor); if ("willRender" in $$props) $$invalidate(15, willRender = $$props.willRender); if ("loadImageData" in $$props) $$invalidate(16, loadImageData = $$props.loadImageData); if ("images" in $$props) $$invalidate(17, images = $$props.images); if ("interfaceImages" in $$props) $$invalidate(18, interfaceImages = $$props.interfaceImages); }; $$self.$$.update = () => { if ($$self.$$.dirty[0] & /*backgroundColor*/ 16384) { backgroundColor && updateSpring(background, backgroundColor); } if ($$self.$$.dirty[0] & /*maskOpacity*/ 2048) { updateSpring(maskOpacityStore, isNumber(maskOpacity) ? maskOpacity : 1); } if ($$self.$$.dirty[0] & /*maskFrameOpacity*/ 4096) { updateSpring(maskFrameOpacityStore, isNumber(maskFrameOpacity) ? maskFrameOpacity : 1); } if ($$self.$$.dirty[0] & /*maskRect*/ 1024) { maskRect && mask.set(maskRect); } if ($$self.$$.dirty[0] & /*$background, $maskOpacityStore*/ 3145728) { $background && imageOverlayColor.set([ $background[0], $background[1], $background[2], clamp($maskOpacityStore, 0, 1) ]); } if ($$self.$$.dirty[0] & /*canvasGL, width, height, images*/ 655363) { // can draw view $$invalidate(23, canDraw = !!(canvasGL && width && height && images.length)); } if ($$self.$$.dirty[0] & /*width, height, canvasGL, pixelRatio*/ 532483) { // observe width and height changes and resize the canvas proportionally width && height && canvasGL && canvasGL.resize(width, height, pixelRatio); } if ($$self.$$.dirty[0] & /*canDraw*/ 8388608) { // switch to draw method when can draw $$invalidate(22, drawUpdate = canDraw ? selectFittingRenderFunction() : noop$1); } if ($$self.$$.dirty[0] & /*canDraw, drawUpdate*/ 12582912) { // if can draw state is updated and we have a draw update function, time to redraw canDraw && drawUpdate && drawUpdate(); } }; return [ width, height, canvas, dispatch, background, maskOpacityStore, maskFrameOpacityStore, mask, imageOverlayColor, animate, maskRect, maskOpacity, maskFrameOpacity, pixelRatio, backgroundColor, willRender, loadImageData, images, interfaceImages, canvasGL, $background, $maskOpacityStore, drawUpdate, canDraw, canvas_1_binding, measure_handler ]; } class Canvas extends SvelteComponent { constructor(options) { super(); init( this, options, instance$M, create_fragment$M, safe_not_equal, { animate: 9, maskRect: 10, maskOpacity: 11, maskFrameOpacity: 12, pixelRatio: 13, backgroundColor: 14, willRender: 15, loadImageData: 16, images: 17, interfaceImages: 18 }, [-1, -1] ); } } var arrayJoin = (arr, filter = Boolean, str = ' ') => arr.filter(filter).join(str); /* src/core/ui/components/TabList.svelte generated by Svelte v3.37.0 */ function get_each_context$9(ctx, list, i) { const child_ctx = ctx.slice(); child_ctx[17] = list[i]; return child_ctx; } const get_default_slot_changes$1 = dirty => ({ tab: dirty & /*tabNodes*/ 4 }); const get_default_slot_context$1 = ctx => ({ tab: /*tab*/ ctx[17] }); // (52:0) {#if shouldRender} function create_if_block$e(ctx) { let ul; let each_blocks = []; let each_1_lookup = new Map(); let ul_class_value; let current; let each_value = /*tabNodes*/ ctx[2]; const get_key = ctx => /*tab*/ ctx[17].id; for (let i = 0; i < each_value.length; i += 1) { let child_ctx = get_each_context$9(ctx, each_value, i); let key = get_key(child_ctx); each_1_lookup.set(key, each_blocks[i] = create_each_block$9(key, child_ctx)); } return { c() { ul = element("ul"); for (let i = 0; i < each_blocks.length; i += 1) { each_blocks[i].c(); } attr(ul, "class", ul_class_value = arrayJoin(["PinturaTabList", /*klass*/ ctx[0]])); attr(ul, "role", "tablist"); attr(ul, "data-layout", /*layout*/ ctx[1]); }, m(target, anchor) { insert(target, ul, anchor); for (let i = 0; i < each_blocks.length; i += 1) { each_blocks[i].m(ul, null); } /*ul_binding*/ ctx[14](ul); current = true; }, p(ctx, dirty) { if (dirty & /*tabNodes, handleKeyTab, handleClickTab, $$scope*/ 1124) { each_value = /*tabNodes*/ ctx[2]; group_outros(); each_blocks = update_keyed_each(each_blocks, dirty, get_key, 1, ctx, each_value, each_1_lookup, ul, outro_and_destroy_block, create_each_block$9, null, get_each_context$9); check_outros(); } if (!current || dirty & /*klass*/ 1 && ul_class_value !== (ul_class_value = arrayJoin(["PinturaTabList", /*klass*/ ctx[0]]))) { attr(ul, "class", ul_class_value); } if (!current || dirty & /*layout*/ 2) { attr(ul, "data-layout", /*layout*/ ctx[1]); } }, i(local) { if (current) return; for (let i = 0; i < each_value.length; i += 1) { transition_in(each_blocks[i]); } current = true; }, o(local) { for (let i = 0; i < each_blocks.length; i += 1) { transition_out(each_blocks[i]); } current = false; }, d(detaching) { if (detaching) detach(ul); for (let i = 0; i < each_blocks.length; i += 1) { each_blocks[i].d(); } /*ul_binding*/ ctx[14](null); } }; } // (59:8) {#each tabNodes as tab (tab.id)} function create_each_block$9(key_1, ctx) { let li; let button; let button_disabled_value; let t; let li_aria_controls_value; let li_id_value; let li_aria_selected_value; let current; let mounted; let dispose; const default_slot_template = /*#slots*/ ctx[11].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[10], get_default_slot_context$1); function keydown_handler(...args) { return /*keydown_handler*/ ctx[12](/*tab*/ ctx[17], ...args); } function click_handler(...args) { return /*click_handler*/ ctx[13](/*tab*/ ctx[17], ...args); } return { key: key_1, first: null, c() { li = element("li"); button = element("button"); if (default_slot) default_slot.c(); t = space(); button.disabled = button_disabled_value = /*tab*/ ctx[17].disabled; attr(li, "role", "tab"); attr(li, "aria-controls", li_aria_controls_value = /*tab*/ ctx[17].href.substr(1)); attr(li, "id", li_id_value = /*tab*/ ctx[17].tabId); attr(li, "aria-selected", li_aria_selected_value = /*tab*/ ctx[17].selected); this.first = li; }, m(target, anchor) { insert(target, li, anchor); append(li, button); if (default_slot) { default_slot.m(button, null); } append(li, t); current = true; if (!mounted) { dispose = [ listen(button, "keydown", keydown_handler), listen(button, "click", click_handler) ]; mounted = true; } }, p(new_ctx, dirty) { ctx = new_ctx; if (default_slot) { if (default_slot.p && dirty & /*$$scope, tabNodes*/ 1028) { update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[10], dirty, get_default_slot_changes$1, get_default_slot_context$1); } } if (!current || dirty & /*tabNodes*/ 4 && button_disabled_value !== (button_disabled_value = /*tab*/ ctx[17].disabled)) { button.disabled = button_disabled_value; } if (!current || dirty & /*tabNodes*/ 4 && li_aria_controls_value !== (li_aria_controls_value = /*tab*/ ctx[17].href.substr(1))) { attr(li, "aria-controls", li_aria_controls_value); } if (!current || dirty & /*tabNodes*/ 4 && li_id_value !== (li_id_value = /*tab*/ ctx[17].tabId)) { attr(li, "id", li_id_value); } if (!current || dirty & /*tabNodes*/ 4 && li_aria_selected_value !== (li_aria_selected_value = /*tab*/ ctx[17].selected)) { attr(li, "aria-selected", li_aria_selected_value); } }, i(local) { if (current) return; transition_in(default_slot, local); current = true; }, o(local) { transition_out(default_slot, local); current = false; }, d(detaching) { if (detaching) detach(li); if (default_slot) default_slot.d(detaching); mounted = false; run_all(dispose); } }; } function create_fragment$L(ctx) { let if_block_anchor; let current; let if_block = /*shouldRender*/ ctx[4] && create_if_block$e(ctx); return { c() { if (if_block) if_block.c(); if_block_anchor = empty(); }, m(target, anchor) { if (if_block) if_block.m(target, anchor); insert(target, if_block_anchor, anchor); current = true; }, p(ctx, [dirty]) { if (/*shouldRender*/ ctx[4]) { if (if_block) { if_block.p(ctx, dirty); if (dirty & /*shouldRender*/ 16) { transition_in(if_block, 1); } } else { if_block = create_if_block$e(ctx); if_block.c(); transition_in(if_block, 1); if_block.m(if_block_anchor.parentNode, if_block_anchor); } } else if (if_block) { group_outros(); transition_out(if_block, 1, 1, () => { if_block = null; }); check_outros(); } }, i(local) { if (current) return; transition_in(if_block); current = true; }, o(local) { transition_out(if_block); current = false; }, d(detaching) { if (if_block) if_block.d(detaching); if (detaching) detach(if_block_anchor); } }; } function instance$L($$self, $$props, $$invalidate) { let tabNodes; let shouldRender; let { $$slots: slots = {}, $$scope } = $$props; let root; let { class: klass = undefined } = $$props; let { name } = $$props; let { selected } = $$props; let { tabs = [] } = $$props; let { layout = undefined } = $$props; const dispatch = createEventDispatcher(); const focusTab = index => { const tab = root.querySelectorAll("[role=\"tab\"] button")[index]; if (!tab) return; tab.focus(); }; const handleClickTab = (e, id) => { e.preventDefault(); e.stopPropagation(); dispatch("select", id); }; const handleKeyTab = ({ key }, id) => { if (!(/arrow/i).test(key)) return; const index = tabs.findIndex(tab => tab.id === id); // next if ((/right|down/i).test(key)) return focusTab(index < tabs.length - 1 ? index + 1 : 0); // prev if ((/left|up/i).test(key)) return focusTab(index > 0 ? index - 1 : tabs.length - 1); }; const keydown_handler = (tab, e) => handleKeyTab(e, tab.id); const click_handler = (tab, e) => handleClickTab(e, tab.id); function ul_binding($$value) { binding_callbacks[$$value ? "unshift" : "push"](() => { root = $$value; $$invalidate(3, root); }); } $$self.$$set = $$props => { if ("class" in $$props) $$invalidate(0, klass = $$props.class); if ("name" in $$props) $$invalidate(7, name = $$props.name); if ("selected" in $$props) $$invalidate(8, selected = $$props.selected); if ("tabs" in $$props) $$invalidate(9, tabs = $$props.tabs); if ("layout" in $$props) $$invalidate(1, layout = $$props.layout); if ("$$scope" in $$props) $$invalidate(10, $$scope = $$props.$$scope); }; $$self.$$.update = () => { if ($$self.$$.dirty & /*tabs, selected, name*/ 896) { $$invalidate(2, tabNodes = tabs.map(tab => { const isActive = tab.id === selected; return { ...tab, tabId: `tab-${name}-${tab.id}`, href: `#panel-${name}-${tab.id}`, selected: isActive }; })); } if ($$self.$$.dirty & /*tabNodes*/ 4) { $$invalidate(4, shouldRender = tabNodes.length > 1); } }; return [ klass, layout, tabNodes, root, shouldRender, handleClickTab, handleKeyTab, name, selected, tabs, $$scope, slots, keydown_handler, click_handler, ul_binding ]; } class TabList extends SvelteComponent { constructor(options) { super(); init(this, options, instance$L, create_fragment$L, safe_not_equal, { class: 0, name: 7, selected: 8, tabs: 9, layout: 1 }); } } /* src/core/ui/components/TabPanels.svelte generated by Svelte v3.37.0 */ const get_default_slot_changes_1 = dirty => ({ panel: dirty & /*panelNodes*/ 16 }); const get_default_slot_context_1 = ctx => ({ panel: /*panelNodes*/ ctx[4][0].id, panelIsActive: true }); function get_each_context$8(ctx, list, i) { const child_ctx = ctx.slice(); child_ctx[14] = list[i].id; child_ctx[15] = list[i].draw; child_ctx[16] = list[i].panelId; child_ctx[17] = list[i].tabindex; child_ctx[18] = list[i].labelledBy; child_ctx[19] = list[i].hidden; child_ctx[3] = list[i].visible; return child_ctx; } const get_default_slot_changes = dirty => ({ panel: dirty & /*panelNodes*/ 16, panelIsActive: dirty & /*panelNodes*/ 16 }); const get_default_slot_context = ctx => ({ panel: /*id*/ ctx[14], panelIsActive: !/*hidden*/ ctx[19] }); // (56:0) {:else} function create_else_block$5(ctx) { let div1; let div0; let div0_class_value; let current; let mounted; let dispose; const default_slot_template = /*#slots*/ ctx[11].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[10], get_default_slot_context_1); return { c() { div1 = element("div"); div0 = element("div"); if (default_slot) default_slot.c(); attr(div0, "class", div0_class_value = arrayJoin([/*panelClass*/ ctx[1]])); attr(div1, "class", /*klass*/ ctx[0]); attr(div1, "style", /*style*/ ctx[2]); }, m(target, anchor) { insert(target, div1, anchor); append(div1, div0); if (default_slot) { default_slot.m(div0, null); } current = true; if (!mounted) { dispose = [ listen(div1, "measure", /*measure_handler_1*/ ctx[13]), action_destroyer(measurable.call(null, div1)) ]; mounted = true; } }, p(ctx, dirty) { if (default_slot) { if (default_slot.p && dirty & /*$$scope, panelNodes*/ 1040) { update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[10], dirty, get_default_slot_changes_1, get_default_slot_context_1); } } if (!current || dirty & /*panelClass*/ 2 && div0_class_value !== (div0_class_value = arrayJoin([/*panelClass*/ ctx[1]]))) { attr(div0, "class", div0_class_value); } if (!current || dirty & /*klass*/ 1) { attr(div1, "class", /*klass*/ ctx[0]); } if (!current || dirty & /*style*/ 4) { attr(div1, "style", /*style*/ ctx[2]); } }, i(local) { if (current) return; transition_in(default_slot, local); current = true; }, o(local) { transition_out(default_slot, local); current = false; }, d(detaching) { if (detaching) detach(div1); if (default_slot) default_slot.d(detaching); mounted = false; run_all(dispose); } }; } // (35:0) {#if shouldRender} function create_if_block$d(ctx) { let div; let each_blocks = []; let each_1_lookup = new Map(); let div_class_value; let current; let mounted; let dispose; let each_value = /*panelNodes*/ ctx[4]; const get_key = ctx => /*id*/ ctx[14]; for (let i = 0; i < each_value.length; i += 1) { let child_ctx = get_each_context$8(ctx, each_value, i); let key = get_key(child_ctx); each_1_lookup.set(key, each_blocks[i] = create_each_block$8(key, child_ctx)); } return { c() { div = element("div"); for (let i = 0; i < each_blocks.length; i += 1) { each_blocks[i].c(); } attr(div, "class", div_class_value = arrayJoin(["PinturaTabPanels", /*klass*/ ctx[0]])); attr(div, "style", /*style*/ ctx[2]); }, m(target, anchor) { insert(target, div, anchor); for (let i = 0; i < each_blocks.length; i += 1) { each_blocks[i].m(div, null); } current = true; if (!mounted) { dispose = [ listen(div, "measure", /*measure_handler*/ ctx[12]), action_destroyer(measurable.call(null, div, { observePosition: true })) ]; mounted = true; } }, p(ctx, dirty) { if (dirty & /*arrayJoin, panelClass, panelNodes, $$scope*/ 1042) { each_value = /*panelNodes*/ ctx[4]; group_outros(); each_blocks = update_keyed_each(each_blocks, dirty, get_key, 1, ctx, each_value, each_1_lookup, div, outro_and_destroy_block, create_each_block$8, null, get_each_context$8); check_outros(); } if (!current || dirty & /*klass*/ 1 && div_class_value !== (div_class_value = arrayJoin(["PinturaTabPanels", /*klass*/ ctx[0]]))) { attr(div, "class", div_class_value); } if (!current || dirty & /*style*/ 4) { attr(div, "style", /*style*/ ctx[2]); } }, i(local) { if (current) return; for (let i = 0; i < each_value.length; i += 1) { transition_in(each_blocks[i]); } current = true; }, o(local) { for (let i = 0; i < each_blocks.length; i += 1) { transition_out(each_blocks[i]); } current = false; }, d(detaching) { if (detaching) detach(div); for (let i = 0; i < each_blocks.length; i += 1) { each_blocks[i].d(); } mounted = false; run_all(dispose); } }; } // (52:16) {#if draw} function create_if_block_1$e(ctx) { let current; const default_slot_template = /*#slots*/ ctx[11].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[10], get_default_slot_context); return { c() { if (default_slot) default_slot.c(); }, m(target, anchor) { if (default_slot) { default_slot.m(target, anchor); } current = true; }, p(ctx, dirty) { if (default_slot) { if (default_slot.p && dirty & /*$$scope, panelNodes*/ 1040) { update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[10], dirty, get_default_slot_changes, get_default_slot_context); } } }, i(local) { if (current) return; transition_in(default_slot, local); current = true; }, o(local) { transition_out(default_slot, local); current = false; }, d(detaching) { if (default_slot) default_slot.d(detaching); } }; } // (43:8) {#each panelNodes as { id, draw, panelId, tabindex, labelledBy, hidden, visible } function create_each_block$8(key_1, ctx) { let div; let t; let div_class_value; let div_hidden_value; let div_id_value; let div_tabindex_value; let div_aria_labelledby_value; let div_data_inert_value; let current; let if_block = /*draw*/ ctx[15] && create_if_block_1$e(ctx); return { key: key_1, first: null, c() { div = element("div"); if (if_block) if_block.c(); t = space(); attr(div, "class", div_class_value = arrayJoin(["PinturaTabPanel", /*panelClass*/ ctx[1]])); div.hidden = div_hidden_value = /*hidden*/ ctx[19]; attr(div, "id", div_id_value = /*panelId*/ ctx[16]); attr(div, "tabindex", div_tabindex_value = /*tabindex*/ ctx[17]); attr(div, "aria-labelledby", div_aria_labelledby_value = /*labelledBy*/ ctx[18]); attr(div, "data-inert", div_data_inert_value = !/*visible*/ ctx[3]); this.first = div; }, m(target, anchor) { insert(target, div, anchor); if (if_block) if_block.m(div, null); append(div, t); current = true; }, p(new_ctx, dirty) { ctx = new_ctx; if (/*draw*/ ctx[15]) { if (if_block) { if_block.p(ctx, dirty); if (dirty & /*panelNodes*/ 16) { transition_in(if_block, 1); } } else { if_block = create_if_block_1$e(ctx); if_block.c(); transition_in(if_block, 1); if_block.m(div, t); } } else if (if_block) { group_outros(); transition_out(if_block, 1, 1, () => { if_block = null; }); check_outros(); } if (!current || dirty & /*panelClass*/ 2 && div_class_value !== (div_class_value = arrayJoin(["PinturaTabPanel", /*panelClass*/ ctx[1]]))) { attr(div, "class", div_class_value); } if (!current || dirty & /*panelNodes*/ 16 && div_hidden_value !== (div_hidden_value = /*hidden*/ ctx[19])) { div.hidden = div_hidden_value; } if (!current || dirty & /*panelNodes*/ 16 && div_id_value !== (div_id_value = /*panelId*/ ctx[16])) { attr(div, "id", div_id_value); } if (!current || dirty & /*panelNodes*/ 16 && div_tabindex_value !== (div_tabindex_value = /*tabindex*/ ctx[17])) { attr(div, "tabindex", div_tabindex_value); } if (!current || dirty & /*panelNodes*/ 16 && div_aria_labelledby_value !== (div_aria_labelledby_value = /*labelledBy*/ ctx[18])) { attr(div, "aria-labelledby", div_aria_labelledby_value); } if (!current || dirty & /*panelNodes*/ 16 && div_data_inert_value !== (div_data_inert_value = !/*visible*/ ctx[3])) { attr(div, "data-inert", div_data_inert_value); } }, i(local) { if (current) return; transition_in(if_block); current = true; }, o(local) { transition_out(if_block); current = false; }, d(detaching) { if (detaching) detach(div); if (if_block) if_block.d(); } }; } function create_fragment$K(ctx) { let current_block_type_index; let if_block; let if_block_anchor; let current; const if_block_creators = [create_if_block$d, create_else_block$5]; const if_blocks = []; function select_block_type(ctx, dirty) { if (/*shouldRender*/ ctx[5]) return 0; return 1; } current_block_type_index = select_block_type(ctx); if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); return { c() { if_block.c(); if_block_anchor = empty(); }, m(target, anchor) { if_blocks[current_block_type_index].m(target, anchor); insert(target, if_block_anchor, anchor); current = true; }, p(ctx, [dirty]) { let previous_block_index = current_block_type_index; current_block_type_index = select_block_type(ctx); if (current_block_type_index === previous_block_index) { if_blocks[current_block_type_index].p(ctx, dirty); } else { group_outros(); transition_out(if_blocks[previous_block_index], 1, 1, () => { if_blocks[previous_block_index] = null; }); check_outros(); if_block = if_blocks[current_block_type_index]; if (!if_block) { if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); if_block.c(); } else { if_block.p(ctx, dirty); } transition_in(if_block, 1); if_block.m(if_block_anchor.parentNode, if_block_anchor); } }, i(local) { if (current) return; transition_in(if_block); current = true; }, o(local) { transition_out(if_block); current = false; }, d(detaching) { if_blocks[current_block_type_index].d(detaching); if (detaching) detach(if_block_anchor); } }; } function instance$K($$self, $$props, $$invalidate) { let panelNodes; let shouldRender; let { $$slots: slots = {}, $$scope } = $$props; let { class: klass = undefined } = $$props; let { name } = $$props; let { selected } = $$props; let { visible = undefined } = $$props; let { panelClass = undefined } = $$props; let { panels = [] } = $$props; let { style = undefined } = $$props; const drawCache = {}; function measure_handler(event) { bubble($$self, event); } function measure_handler_1(event) { bubble($$self, event); } $$self.$$set = $$props => { if ("class" in $$props) $$invalidate(0, klass = $$props.class); if ("name" in $$props) $$invalidate(6, name = $$props.name); if ("selected" in $$props) $$invalidate(7, selected = $$props.selected); if ("visible" in $$props) $$invalidate(3, visible = $$props.visible); if ("panelClass" in $$props) $$invalidate(1, panelClass = $$props.panelClass); if ("panels" in $$props) $$invalidate(8, panels = $$props.panels); if ("style" in $$props) $$invalidate(2, style = $$props.style); if ("$$scope" in $$props) $$invalidate(10, $$scope = $$props.$$scope); }; $$self.$$.update = () => { if ($$self.$$.dirty & /*panels, selected, visible, name, drawCache*/ 968) { $$invalidate(4, panelNodes = panels.map(id => { const isActive = id === selected; const isVisible = visible ? visible.indexOf(id) !== -1 : true; // remember that this tab was active so we keep drawing it even when it's inactive if (isActive) $$invalidate(9, drawCache[id] = true, drawCache); return { id, panelId: `panel-${name}-${id}`, labelledBy: `tab-${name}-${id}`, hidden: !isActive, visible: isVisible, tabindex: isActive ? 0 : -1, draw: isActive || drawCache[id] }; })); } if ($$self.$$.dirty & /*panelNodes*/ 16) { $$invalidate(5, shouldRender = panelNodes.length > 1); } }; return [ klass, panelClass, style, visible, panelNodes, shouldRender, name, selected, panels, drawCache, $$scope, slots, measure_handler, measure_handler_1 ]; } class TabPanels extends SvelteComponent { constructor(options) { super(); init(this, options, instance$K, create_fragment$K, safe_not_equal, { class: 0, name: 6, selected: 7, visible: 3, panelClass: 1, panels: 8, style: 2 }); } } /* src/core/ui/components/Panel.svelte generated by Svelte v3.37.0 */ function create_fragment$J(ctx) { let div; let switch_instance; let updating_name; let div_class_value; let current; const switch_instance_spread_levels = [/*componentProps*/ ctx[7]]; function switch_instance_name_binding(value) { /*switch_instance_name_binding*/ ctx[19](value); } var switch_value = /*componentView*/ ctx[11]; function switch_props(ctx) { let switch_instance_props = {}; for (let i = 0; i < switch_instance_spread_levels.length; i += 1) { switch_instance_props = assign(switch_instance_props, switch_instance_spread_levels[i]); } if (/*panelName*/ ctx[5] !== void 0) { switch_instance_props.name = /*panelName*/ ctx[5]; } return { props: switch_instance_props }; } if (switch_value) { switch_instance = new switch_value(switch_props(ctx)); binding_callbacks.push(() => bind(switch_instance, "name", switch_instance_name_binding)); /*switch_instance_binding*/ ctx[20](switch_instance); switch_instance.$on("measure", /*measure_handler*/ ctx[21]); } return { c() { div = element("div"); if (switch_instance) create_component(switch_instance.$$.fragment); attr(div, "data-util", /*panelName*/ ctx[5]); attr(div, "class", div_class_value = arrayJoin(["PinturaPanel", /*klass*/ ctx[2]])); attr(div, "style", /*style*/ ctx[6]); }, m(target, anchor) { insert(target, div, anchor); if (switch_instance) { mount_component(switch_instance, div, null); } current = true; }, p(ctx, [dirty]) { const switch_instance_changes = (dirty & /*componentProps*/ 128) ? get_spread_update(switch_instance_spread_levels, [get_spread_object(/*componentProps*/ ctx[7])]) : {}; if (!updating_name && dirty & /*panelName*/ 32) { updating_name = true; switch_instance_changes.name = /*panelName*/ ctx[5]; add_flush_callback(() => updating_name = false); } if (switch_value !== (switch_value = /*componentView*/ ctx[11])) { if (switch_instance) { group_outros(); const old_component = switch_instance; transition_out(old_component.$$.fragment, 1, 0, () => { destroy_component(old_component, 1); }); check_outros(); } if (switch_value) { switch_instance = new switch_value(switch_props(ctx)); binding_callbacks.push(() => bind(switch_instance, "name", switch_instance_name_binding)); /*switch_instance_binding*/ ctx[20](switch_instance); switch_instance.$on("measure", /*measure_handler*/ ctx[21]); create_component(switch_instance.$$.fragment); transition_in(switch_instance.$$.fragment, 1); mount_component(switch_instance, div, null); } else { switch_instance = null; } } else if (switch_value) { switch_instance.$set(switch_instance_changes); } if (!current || dirty & /*panelName*/ 32) { attr(div, "data-util", /*panelName*/ ctx[5]); } if (!current || dirty & /*klass*/ 4 && div_class_value !== (div_class_value = arrayJoin(["PinturaPanel", /*klass*/ ctx[2]]))) { attr(div, "class", div_class_value); } if (!current || dirty & /*style*/ 64) { attr(div, "style", /*style*/ ctx[6]); } }, i(local) { if (current) return; if (switch_instance) transition_in(switch_instance.$$.fragment, local); current = true; }, o(local) { if (switch_instance) transition_out(switch_instance.$$.fragment, local); current = false; }, d(detaching) { if (detaching) detach(div); /*switch_instance_binding*/ ctx[20](null); if (switch_instance) destroy_component(switch_instance); } }; } function instance$J($$self, $$props, $$invalidate) { let style; let componentProps; let $opacityClamped; let $isActivePrivateStore; const dispatch = createEventDispatcher(); let { isActive = true } = $$props; let { isAnimated = true } = $$props; let { stores } = $$props; let { content } = $$props; let { component } = $$props; let { locale } = $$props; let { class: klass = undefined } = $$props; // we remember the view rect in this variable let rect; const opacity = spring(0); const opacityClamped = derived(opacity, $opacity => clamp($opacity, 0, 1)); component_subscribe($$self, opacityClamped, value => $$invalidate(18, $opacityClamped = value)); // throw hide / show events let isHidden = !isActive; // create active store so can be used in derived stores const isActivePrivateStore = writable(isActive); component_subscribe($$self, isActivePrivateStore, value => $$invalidate(22, $isActivePrivateStore = value)); const stateProps = { isActive: derived(isActivePrivateStore, $isActivePrivateStore => $isActivePrivateStore), isActiveFraction: derived(opacityClamped, $opacityClamped => $opacityClamped), isVisible: derived(opacityClamped, $opacityClamped => $opacityClamped > 0) }; // build the component props const componentView = content.view; const componentExportedProps = getComponentExportedProps(componentView); const componentComputedProps = Object.keys(content.props || {}).reduce( (computedProps, key) => { if (!componentExportedProps.includes(key)) return computedProps; computedProps[key] = content.props[key]; return computedProps; }, {} ); const componentComputedStateProps = Object.keys(stateProps).reduce( (computedStateProps, key) => { if (!componentExportedProps.includes(key)) return computedStateProps; computedStateProps[key] = stateProps[key]; return computedStateProps; }, {} ); // class used on panel element let panelName; // we use the `hasBeenMounted` bool to block rect updates until the entire panel is ready let hasBeenMounted = false; onMount(() => { $$invalidate(4, hasBeenMounted = true); }); function switch_instance_name_binding(value) { panelName = value; $$invalidate(5, panelName); } function switch_instance_binding($$value) { binding_callbacks[$$value ? "unshift" : "push"](() => { component = $$value; $$invalidate(0, component); }); } const measure_handler = e => { if (!hasBeenMounted || !isActive) return; $$invalidate(3, rect = e.detail); dispatch("measure", { ...rect }); }; $$self.$$set = $$props => { if ("isActive" in $$props) $$invalidate(1, isActive = $$props.isActive); if ("isAnimated" in $$props) $$invalidate(12, isAnimated = $$props.isAnimated); if ("stores" in $$props) $$invalidate(13, stores = $$props.stores); if ("content" in $$props) $$invalidate(14, content = $$props.content); if ("component" in $$props) $$invalidate(0, component = $$props.component); if ("locale" in $$props) $$invalidate(15, locale = $$props.locale); if ("class" in $$props) $$invalidate(2, klass = $$props.class); }; $$self.$$.update = () => { if ($$self.$$.dirty & /*rect, isActive, component*/ 11) { // when the view rect changes and the panel is in active state or became active, dispatch measure event if (rect && isActive && component) dispatch("measure", rect); } if ($$self.$$.dirty & /*isActive, isAnimated*/ 4098) { opacity.set(isActive ? 1 : 0, { hard: !isAnimated }); } if ($$self.$$.dirty & /*$opacityClamped, isHidden*/ 393216) { if ($opacityClamped <= 0 && !isHidden) { $$invalidate(17, isHidden = true); } else if ($opacityClamped > 0 && isHidden) { $$invalidate(17, isHidden = false); } } if ($$self.$$.dirty & /*hasBeenMounted, isHidden*/ 131088) { hasBeenMounted && dispatch(isHidden ? "hide" : "show"); } if ($$self.$$.dirty & /*$opacityClamped*/ 262144) { dispatch("fade", $opacityClamped); } if ($$self.$$.dirty & /*$opacityClamped*/ 262144) { // only set opacity prop if is below 0 $$invalidate(6, style = $opacityClamped < 1 ? `opacity: ${$opacityClamped}` : undefined); } if ($$self.$$.dirty & /*isActive*/ 2) { set_store_value(isActivePrivateStore, $isActivePrivateStore = isActive, $isActivePrivateStore); } if ($$self.$$.dirty & /*stores, locale*/ 40960) { $$invalidate(7, componentProps = { ...componentComputedProps, ...componentComputedStateProps, stores, locale }); } }; return [ component, isActive, klass, rect, hasBeenMounted, panelName, style, componentProps, dispatch, opacityClamped, isActivePrivateStore, componentView, isAnimated, stores, content, locale, opacity, isHidden, $opacityClamped, switch_instance_name_binding, switch_instance_binding, measure_handler ]; } class Panel extends SvelteComponent { constructor(options) { super(); init(this, options, instance$J, create_fragment$J, safe_not_equal, { isActive: 1, isAnimated: 12, stores: 13, content: 14, component: 0, locale: 15, class: 2, opacity: 16 }); } get opacity() { return this.$$.ctx[16]; } } /* src/core/ui/components/Icon.svelte generated by Svelte v3.37.0 */ function create_fragment$I(ctx) { let svg; let svg_viewBox_value; let current; const default_slot_template = /*#slots*/ ctx[5].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[4], null); return { c() { svg = svg_element("svg"); if (default_slot) default_slot.c(); attr(svg, "class", /*klass*/ ctx[3]); attr(svg, "style", /*style*/ ctx[2]); attr(svg, "width", /*width*/ ctx[0]); attr(svg, "height", /*height*/ ctx[1]); attr(svg, "viewBox", svg_viewBox_value = "0 0 " + /*width*/ ctx[0] + "\n " + /*height*/ ctx[1]); attr(svg, "xmlns", "http://www.w3.org/2000/svg"); attr(svg, "aria-hidden", "true"); attr(svg, "focusable", "false"); attr(svg, "stroke-linecap", "round"); attr(svg, "stroke-linejoin", "round"); }, m(target, anchor) { insert(target, svg, anchor); if (default_slot) { default_slot.m(svg, null); } current = true; }, p(ctx, [dirty]) { if (default_slot) { if (default_slot.p && dirty & /*$$scope*/ 16) { update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[4], dirty, null, null); } } if (!current || dirty & /*klass*/ 8) { attr(svg, "class", /*klass*/ ctx[3]); } if (!current || dirty & /*style*/ 4) { attr(svg, "style", /*style*/ ctx[2]); } if (!current || dirty & /*width*/ 1) { attr(svg, "width", /*width*/ ctx[0]); } if (!current || dirty & /*height*/ 2) { attr(svg, "height", /*height*/ ctx[1]); } if (!current || dirty & /*width, height*/ 3 && svg_viewBox_value !== (svg_viewBox_value = "0 0 " + /*width*/ ctx[0] + "\n " + /*height*/ ctx[1])) { attr(svg, "viewBox", svg_viewBox_value); } }, i(local) { if (current) return; transition_in(default_slot, local); current = true; }, o(local) { transition_out(default_slot, local); current = false; }, d(detaching) { if (detaching) detach(svg); if (default_slot) default_slot.d(detaching); } }; } function instance$I($$self, $$props, $$invalidate) { let { $$slots: slots = {}, $$scope } = $$props; let { width = 24 } = $$props; let { height = 24 } = $$props; let { style = undefined } = $$props; let { class: klass = undefined } = $$props; $$self.$$set = $$props => { if ("width" in $$props) $$invalidate(0, width = $$props.width); if ("height" in $$props) $$invalidate(1, height = $$props.height); if ("style" in $$props) $$invalidate(2, style = $$props.style); if ("class" in $$props) $$invalidate(3, klass = $$props.class); if ("$$scope" in $$props) $$invalidate(4, $$scope = $$props.$$scope); }; return [width, height, style, klass, $$scope, slots]; } class Icon extends SvelteComponent { constructor(options) { super(); init(this, options, instance$I, create_fragment$I, safe_not_equal, { width: 0, height: 1, style: 2, class: 3 }); } } var isEventTarget = (e, element) => element === e.target || element.contains(e.target); /* src/core/ui/components/Button.svelte generated by Svelte v3.37.0 */ function create_if_block_1$d(ctx) { let icon_1; let current; icon_1 = new Icon({ props: { class: "PinturaButtonIcon", $$slots: { default: [create_default_slot$h] }, $$scope: { ctx } } }); return { c() { create_component(icon_1.$$.fragment); }, m(target, anchor) { mount_component(icon_1, target, anchor); current = true; }, p(ctx, dirty) { const icon_1_changes = {}; if (dirty & /*$$scope, icon*/ 1048578) { icon_1_changes.$$scope = { dirty, ctx }; } icon_1.$set(icon_1_changes); }, i(local) { if (current) return; transition_in(icon_1.$$.fragment, local); current = true; }, o(local) { transition_out(icon_1.$$.fragment, local); current = false; }, d(detaching) { destroy_component(icon_1, detaching); } }; } // (44:16) function create_default_slot$h(ctx) { let g; return { c() { g = svg_element("g"); }, m(target, anchor) { insert(target, g, anchor); g.innerHTML = /*icon*/ ctx[1]; }, p(ctx, dirty) { if (dirty & /*icon*/ 2) g.innerHTML = /*icon*/ ctx[1]; }, d(detaching) { if (detaching) detach(g); } }; } // (50:12) {#if label} function create_if_block$c(ctx) { let span; let t; return { c() { span = element("span"); t = text(/*label*/ ctx[0]); attr(span, "class", /*elLabelClass*/ ctx[11]); }, m(target, anchor) { insert(target, span, anchor); append(span, t); }, p(ctx, dirty) { if (dirty & /*label*/ 1) set_data(t, /*label*/ ctx[0]); if (dirty & /*elLabelClass*/ 2048) { attr(span, "class", /*elLabelClass*/ ctx[11]); } }, d(detaching) { if (detaching) detach(span); } }; } // (41:10) function fallback_block$2(ctx) { let span; let t; let current; let if_block0 = /*icon*/ ctx[1] && create_if_block_1$d(ctx); let if_block1 = /*label*/ ctx[0] && create_if_block$c(ctx); return { c() { span = element("span"); if (if_block0) if_block0.c(); t = space(); if (if_block1) if_block1.c(); attr(span, "class", /*elButtonInnerClass*/ ctx[9]); }, m(target, anchor) { insert(target, span, anchor); if (if_block0) if_block0.m(span, null); append(span, t); if (if_block1) if_block1.m(span, null); current = true; }, p(ctx, dirty) { if (/*icon*/ ctx[1]) { if (if_block0) { if_block0.p(ctx, dirty); if (dirty & /*icon*/ 2) { transition_in(if_block0, 1); } } else { if_block0 = create_if_block_1$d(ctx); if_block0.c(); transition_in(if_block0, 1); if_block0.m(span, t); } } else if (if_block0) { group_outros(); transition_out(if_block0, 1, 1, () => { if_block0 = null; }); check_outros(); } if (/*label*/ ctx[0]) { if (if_block1) { if_block1.p(ctx, dirty); } else { if_block1 = create_if_block$c(ctx); if_block1.c(); if_block1.m(span, null); } } else if (if_block1) { if_block1.d(1); if_block1 = null; } if (!current || dirty & /*elButtonInnerClass*/ 512) { attr(span, "class", /*elButtonInnerClass*/ ctx[9]); } }, i(local) { if (current) return; transition_in(if_block0); current = true; }, o(local) { transition_out(if_block0); current = false; }, d(detaching) { if (detaching) detach(span); if (if_block0) if_block0.d(); if (if_block1) if_block1.d(); } }; } function create_fragment$H(ctx) { let button; let current; let mounted; let dispose; const default_slot_template = /*#slots*/ ctx[18].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[20], null); const default_slot_or_fallback = default_slot || fallback_block$2(ctx); return { c() { button = element("button"); if (default_slot_or_fallback) default_slot_or_fallback.c(); attr(button, "type", /*type*/ ctx[4]); attr(button, "style", /*style*/ ctx[2]); button.disabled = /*disabled*/ ctx[3]; attr(button, "class", /*elButtonClass*/ ctx[10]); attr(button, "title", /*label*/ ctx[0]); }, m(target, anchor) { insert(target, button, anchor); if (default_slot_or_fallback) { default_slot_or_fallback.m(button, null); } /*button_binding*/ ctx[19](button); current = true; if (!mounted) { dispose = [ listen(button, "keydown", function () { if (is_function(/*onkeydown*/ ctx[6])) /*onkeydown*/ ctx[6].apply(this, arguments); }), listen(button, "click", function () { if (is_function(/*onclick*/ ctx[5])) /*onclick*/ ctx[5].apply(this, arguments); }), action_destroyer(/*action*/ ctx[7].call(null, button)) ]; mounted = true; } }, p(new_ctx, [dirty]) { ctx = new_ctx; if (default_slot) { if (default_slot.p && dirty & /*$$scope*/ 1048576) { update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[20], dirty, null, null); } } else { if (default_slot_or_fallback && default_slot_or_fallback.p && dirty & /*elButtonInnerClass, elLabelClass, label, icon*/ 2563) { default_slot_or_fallback.p(ctx, dirty); } } if (!current || dirty & /*type*/ 16) { attr(button, "type", /*type*/ ctx[4]); } if (!current || dirty & /*style*/ 4) { attr(button, "style", /*style*/ ctx[2]); } if (!current || dirty & /*disabled*/ 8) { button.disabled = /*disabled*/ ctx[3]; } if (!current || dirty & /*elButtonClass*/ 1024) { attr(button, "class", /*elButtonClass*/ ctx[10]); } if (!current || dirty & /*label*/ 1) { attr(button, "title", /*label*/ ctx[0]); } }, i(local) { if (current) return; transition_in(default_slot_or_fallback, local); current = true; }, o(local) { transition_out(default_slot_or_fallback, local); current = false; }, d(detaching) { if (detaching) detach(button); if (default_slot_or_fallback) default_slot_or_fallback.d(detaching); /*button_binding*/ ctx[19](null); mounted = false; run_all(dispose); } }; } function instance$H($$self, $$props, $$invalidate) { let elButtonInnerClass; let elButtonClass; let elLabelClass; let { $$slots: slots = {}, $$scope } = $$props; let { class: klass = undefined } = $$props; let { label = undefined } = $$props; let { labelClass = undefined } = $$props; let { innerClass = undefined } = $$props; let { hideLabel = false } = $$props; let { icon = undefined } = $$props; let { style = undefined } = $$props; let { disabled = undefined } = $$props; let { type = "button" } = $$props; let { onclick = undefined } = $$props; let { onkeydown = undefined } = $$props; let { action = () => { } } = $$props; let root; const isEventTarget$1 = e => isEventTarget(e, root); const getElement = () => root; function button_binding($$value) { binding_callbacks[$$value ? "unshift" : "push"](() => { root = $$value; $$invalidate(8, root); }); } $$self.$$set = $$props => { if ("class" in $$props) $$invalidate(12, klass = $$props.class); if ("label" in $$props) $$invalidate(0, label = $$props.label); if ("labelClass" in $$props) $$invalidate(13, labelClass = $$props.labelClass); if ("innerClass" in $$props) $$invalidate(14, innerClass = $$props.innerClass); if ("hideLabel" in $$props) $$invalidate(15, hideLabel = $$props.hideLabel); if ("icon" in $$props) $$invalidate(1, icon = $$props.icon); if ("style" in $$props) $$invalidate(2, style = $$props.style); if ("disabled" in $$props) $$invalidate(3, disabled = $$props.disabled); if ("type" in $$props) $$invalidate(4, type = $$props.type); if ("onclick" in $$props) $$invalidate(5, onclick = $$props.onclick); if ("onkeydown" in $$props) $$invalidate(6, onkeydown = $$props.onkeydown); if ("action" in $$props) $$invalidate(7, action = $$props.action); if ("$$scope" in $$props) $$invalidate(20, $$scope = $$props.$$scope); }; $$self.$$.update = () => { if ($$self.$$.dirty & /*innerClass*/ 16384) { $$invalidate(9, elButtonInnerClass = arrayJoin(["PinturaButtonInner", innerClass])); } if ($$self.$$.dirty & /*hideLabel, klass*/ 36864) { $$invalidate(10, elButtonClass = arrayJoin(["PinturaButton", hideLabel && "PinturaButtonIconOnly", klass])); } if ($$self.$$.dirty & /*hideLabel, labelClass*/ 40960) { $$invalidate(11, elLabelClass = arrayJoin([hideLabel ? "implicit" : "PinturaButtonLabel", labelClass])); } }; return [ label, icon, style, disabled, type, onclick, onkeydown, action, root, elButtonInnerClass, elButtonClass, elLabelClass, klass, labelClass, innerClass, hideLabel, isEventTarget$1, getElement, slots, button_binding, $$scope ]; } class Button extends SvelteComponent { constructor(options) { super(); init(this, options, instance$H, create_fragment$H, safe_not_equal, { class: 12, label: 0, labelClass: 13, innerClass: 14, hideLabel: 15, icon: 1, style: 2, disabled: 3, type: 4, onclick: 5, onkeydown: 6, action: 7, isEventTarget: 16, getElement: 17 }); } get isEventTarget() { return this.$$.ctx[16]; } get getElement() { return this.$$.ctx[17]; } } var arrayRemove = (array, predicate) => { const index = array.findIndex(predicate); if (index >= 0) return array.splice(index, 1); return undefined; }; // svelte // constants const INERTIA_THRESHOLD = 0.25; // when force of velocity exceeds this value we drift const INERTIA_DISTANCE_MULTIPLIER = 50; const INERTIA_DURATION_MULTIPLIER = 80; const TAP_DURATION_MAX = 300; const TAP_DISTANCE_MAX = 64; const DOUBLE_TAP_DURATION_MAX = 700; const DOUBLE_TAP_DISTANCE_MAX = 128; const isContextMenuAction = (e) => isNumber(e.button) && e.button !== 0; var interactable = (node, options = {}) => { // set defaults const { inertia = false, matchTarget = false, pinch = false, getEventPosition = (e) => vectorCreate(e.clientX, e.clientY), } = options; // // helpers // function dispatch(type, detail) { node.dispatchEvent(new CustomEvent(type, { detail })); } function resetInertia() { if (inertiaTweenUnsubscribe) inertiaTweenUnsubscribe(); inertiaTweenUnsubscribe = undefined; } //#region pointer registry const pointers = []; const addPointer = (e) => { const pointer = { timeStamp: e.timeStamp, timeStampInitial: e.timeStamp, position: getEventPosition(e), origin: getEventPosition(e), velocity: vectorCreateEmpty(), translation: vectorCreateEmpty(), interactionState: undefined, event: e, }; pointers.push(pointer); pointer.interactionState = getInteractionState(pointers); }; const removePointer = (e) => { const pointer = arrayRemove(pointers, (pointer) => pointer.event.pointerId === e.pointerId); if (pointer) return pointer[0]; }; const getPointerIndex = (e) => pointers.findIndex((pointer) => pointer.event.pointerId === e.pointerId); const flattenPointerOrigin = (pointer) => { pointer.origin.x = pointer.position.x; pointer.origin.y = pointer.position.y; pointer.translation.x = 0; pointer.translation.y = 0; }; const updatePointer = (e) => { const pointer = getPointer(e); if (!pointer) return; const { timeStamp } = e; // position const eventPosition = getEventPosition(e); // duration between previous interaction and new interaction, an interaction duration cannot be faster than 1 millisecond const interactionDuration = Math.max(1, timeStamp - pointer.timeStamp); // calculate velocity pointer.velocity.x = (eventPosition.x - pointer.position.x) / interactionDuration; pointer.velocity.y = (eventPosition.y - pointer.position.y) / interactionDuration; // update the translation pointer.translation.x = eventPosition.x - pointer.origin.x; pointer.translation.y = eventPosition.y - pointer.origin.y; // set new state pointer.timeStamp = timeStamp; pointer.position.x = eventPosition.x; pointer.position.y = eventPosition.y; pointer.event = e; }; const getPointer = (e) => { const i = getPointerIndex(e); if (i < 0) return; return pointers[i]; }; const isSingleTouching = () => pointers.length === 1; const isMultiTouching = () => pointers.length === 2; const getDistance = (pointers, position) => { const distanceTotal = pointers.reduce((prev, curr) => { prev += vectorDistance(position, curr.position); return prev; }, 0); return distanceTotal / pointers.length; }; const getInteractionState = (pointers) => { const center = vectorCenter(pointers.map((pointer) => pointer.position)); const distance = getDistance(pointers, center); return { center, distance, velocity: vectorCenter(pointers.map((pointer) => pointer.velocity)), translation: vectorCenter(pointers.map((pointer) => pointer.translation)), }; }; //#endregion let inertiaTween; let inertiaTweenUnsubscribe; let pinchOffsetDistance; let currentTranslation; let currentScale; let isGesture; let lastTapTimeStamp = 0; let lastTapPosition = undefined; // start handling interactions node.addEventListener('pointerdown', handlePointerdown); function handlePointerdown(e) { // ignore more than two pointers for now if (isMultiTouching()) return; // not interested in context menu if (isContextMenuAction(e)) return; // target should equal node, if it doesn't user might have clicked one of the nodes children if (matchTarget && e.target !== node) return; // stop any previous inertia tweens resetInertia(); // register this pointer addPointer(e); // if is first pointer we need to init the drag gesture if (isSingleTouching()) { // handle pointer events document.documentElement.addEventListener('pointermove', handlePointermove); document.documentElement.addEventListener('pointerup', handlePointerup); document.documentElement.addEventListener('pointercancel', handlePointerup); // clear vars isGesture = false; currentScale = 1; currentTranslation = vectorCreateEmpty(); pinchOffsetDistance = undefined; dispatch('interactionstart', { origin: vectorClone(getPointer(e).origin), }); } else if (pinch) { isGesture = true; pinchOffsetDistance = vectorDistance(pointers[0].position, pointers[1].position); currentTranslation.x += pointers[0].translation.x; currentTranslation.y += pointers[0].translation.y; flattenPointerOrigin(pointers[0]); } } // // pointer move can only be a primary event (other pointers are not handled) // let moveLast = Date.now(); function handlePointermove(e) { // prevent selection of text (Safari) e.preventDefault(); // update pointer state updatePointer(e); let translation = vectorClone(pointers[0].translation); let scalar = currentScale; if (pinch && isMultiTouching()) { // current pinch distance const pinchCurrentDistance = vectorDistance(pointers[0].position, pointers[1].position); // to find out scalar we calculate the difference between the pinch offset and the new pinch const pinchScalar = pinchCurrentDistance / pinchOffsetDistance; // add to existing scalar scalar *= pinchScalar; // current offset vectorAdd(translation, pointers[1].translation); } translation.x += currentTranslation.x; translation.y += currentTranslation.y; // skip update event if last interaction was less than 16 ms ago const now = Date.now(); const dist = now - moveLast; if (dist < 16) return; moveLast = now; dispatch('interactionupdate', { translation, scalar: pinch ? scalar : undefined, }); } // // pointer up can only be a primary event (other pointers are not handled) // function handlePointerup(e) { // test if is my pointer that was released, as we're listining on document it could be other pointers if (!getPointer(e)) return; // remove pointer from active pointers array const removedPointer = removePointer(e); // store current size if (pinch && isSingleTouching()) { // calculate current scale const pinchCurrentDistance = vectorDistance(pointers[0].position, removedPointer.position); currentScale *= pinchCurrentDistance / pinchOffsetDistance; currentTranslation.x += pointers[0].translation.x + removedPointer.translation.x; currentTranslation.y += pointers[0].translation.y + removedPointer.translation.y; flattenPointerOrigin(pointers[0]); } // check if this was a tap let isTap = false; let isDoubleTap = false; if (!isGesture && removedPointer) { const interactionEnd = performance.now(); const interactionDuration = interactionEnd - removedPointer.timeStampInitial; const interactionDistanceSquared = vectorDistanceSquared(removedPointer.translation); isTap = interactionDistanceSquared < TAP_DISTANCE_MAX && interactionDuration < TAP_DURATION_MAX; isDoubleTap = !!(lastTapPosition && isTap && interactionEnd - lastTapTimeStamp < DOUBLE_TAP_DURATION_MAX && vectorDistanceSquared(lastTapPosition, removedPointer.position) < DOUBLE_TAP_DISTANCE_MAX); if (isTap) { lastTapPosition = vectorClone(removedPointer.position); lastTapTimeStamp = interactionEnd; } } // we wait till last multi-touch interaction is finished, all pointers need to be de-registered before proceeding if (pointers.length > 0) return; // stop listening document.documentElement.removeEventListener('pointermove', handlePointermove); document.documentElement.removeEventListener('pointerup', handlePointerup); document.documentElement.removeEventListener('pointercancel', handlePointerup); const translation = vectorClone(removedPointer.translation); const velocity = vectorClone(removedPointer.velocity); // allows cancelling inertia from release handler let inertiaPrevented = false; // user has released interaction dispatch('interactionrelease', { isTap, isDoubleTap, translation, scalar: currentScale, preventInertia: () => (inertiaPrevented = true), }); // stop intantly if not a lot of force applied const force = vectorDistance(velocity); if (inertiaPrevented || !inertia || force < INERTIA_THRESHOLD) { return handleEnd(translation, { isTap, isDoubleTap }); } // drift inertiaTween = tweened(vectorClone(translation), { easing: circOut, duration: force * INERTIA_DURATION_MULTIPLIER, }); inertiaTween .set({ x: translation.x + velocity.x * INERTIA_DISTANCE_MULTIPLIER, y: translation.y + velocity.y * INERTIA_DISTANCE_MULTIPLIER, }) .then(() => { // if has unsubscribed (tween was reset) if (!inertiaTweenUnsubscribe) return; // go! handleEnd(get_store_value(inertiaTween), { isTap, isDoubleTap }); }); inertiaTweenUnsubscribe = inertiaTween.subscribe(handleInertiaUpdate); } function handleInertiaUpdate(inertiaTranslation) { // if is same as previous position, ignore if (!inertiaTranslation) return; // || vectorEqual(inertiaTranslation, translation)) return; // this will handle drift interactions dispatch('interactionupdate', { translation: inertiaTranslation, scalar: pinch ? currentScale : undefined, }); } function handleEnd(translation, tapState) { resetInertia(); dispatch('interactionend', { ...tapState, translation, scalar: pinch ? currentScale : undefined, }); } return { destroy() { resetInertia(); node.removeEventListener('pointerdown', handlePointerdown); }, }; }; var nudgeable = (element, options = {}) => { // if added as action on non focusable element you should add tabindex=0 attribute const { direction = undefined, shiftMultiplier = 10, bubbles = false, stopKeydownPropagation = true, } = options; const isHorizontalDirection = direction === 'horizontal'; const isVerticalDirection = direction === 'vertical'; const handleKeydown = (e) => { const { key } = e; const isShift = e.shiftKey; const isVerticalAction = /up|down/i.test(key); const isHorizontalAction = /left|right/i.test(key); // no directional key if (!isHorizontalAction && !isVerticalAction) return; // is horizontal but up or down pressed if (isHorizontalDirection && isVerticalAction) return; // is vertical but left or right pressed if (isVerticalDirection && isHorizontalAction) return; // if holding shift move by a factor 10 const multiplier = isShift ? shiftMultiplier : 1; if (stopKeydownPropagation) e.stopPropagation(); element.dispatchEvent(new CustomEvent('nudge', { bubbles, detail: vectorCreate((/left/i.test(key) ? -1 : /right/i.test(key) ? 1 : 0) * multiplier, (/up/i.test(key) ? -1 : /down/i.test(key) ? 1 : 0) * multiplier), })); }; element.addEventListener('keydown', handleKeydown); return { destroy() { element.removeEventListener('keydown', handleKeydown); }, }; }; function elastify(translation, dist) { return dist * Math.sign(translation) * Math.log10(1 + Math.abs(translation) / dist); } const elastifyRects = (a, b, dist) => { if (!b) return rectClone(a); const left = a.x + elastify(b.x - a.x, dist); const right = a.x + a.width + elastify(b.x + b.width - (a.x + a.width), dist); const top = a.y + elastify(b.y - a.y, dist); const bottom = a.y + a.height + elastify(b.y + b.height - (a.y + a.height), dist); return { x: left, y: top, width: right - left, height: bottom - top, }; }; var unitToPixels = (value, element) => { if (!value) return; if (/em/.test(value)) return parseInt(value, 10) * 16; if (/px/.test(value)) return parseInt(value, 10); }; var getWheelDelta = (e) => { let d = e.detail || 0; // @ts-ignore const { deltaX, deltaY, wheelDelta, wheelDeltaX, wheelDeltaY } = e; // "detect" x axis interaction for MacOS trackpad if (isNumber(wheelDeltaX) && Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)) { // blink & webkit d = wheelDeltaX / -120; } else if (isNumber(deltaX) && Math.abs(deltaX) > Math.abs(deltaY)) { // quantum d = deltaX / 20; } // @ts-ignore else if (wheelDelta || wheelDeltaY) { // blink & webkit d = (wheelDelta || wheelDeltaY) / -120; } if (!d) { // quantum d = deltaY / 20; } return d; }; /* src/core/ui/components/Scrollable.svelte generated by Svelte v3.37.0 */ function create_fragment$G(ctx) { let div1; let div0; let div1_class_value; let nudgeable_action; let current; let mounted; let dispose; const default_slot_template = /*#slots*/ ctx[37].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[36], null); return { c() { div1 = element("div"); div0 = element("div"); if (default_slot) default_slot.c(); attr(div0, "style", /*childStyle*/ ctx[6]); attr(div1, "class", div1_class_value = arrayJoin(["PinturaScrollable", /*klass*/ ctx[0]])); attr(div1, "style", /*overflowStyle*/ ctx[4]); attr(div1, "data-direction", /*scrollDirection*/ ctx[1]); attr(div1, "data-state", /*containerState*/ ctx[5]); }, m(target, anchor) { insert(target, div1, anchor); append(div1, div0); if (default_slot) { default_slot.m(div0, null); } /*div1_binding*/ ctx[39](div1); current = true; if (!mounted) { dispose = [ listen(div0, "interactionstart", /*handleDragStart*/ ctx[9]), listen(div0, "interactionupdate", /*handleDragMove*/ ctx[11]), listen(div0, "interactionend", /*handleDragEnd*/ ctx[12]), listen(div0, "interactionrelease", /*handleDragRelease*/ ctx[10]), action_destroyer(interactable.call(null, div0, { inertia: true })), listen(div0, "measure", /*measure_handler*/ ctx[38]), action_destroyer(measurable.call(null, div0)), listen(div1, "wheel", /*handleWheel*/ ctx[14], { passive: false }), listen(div1, "scroll", /*handleScroll*/ ctx[16]), listen(div1, "focusin", /*handleFocus*/ ctx[15]), listen(div1, "nudge", /*handleNudge*/ ctx[17]), listen(div1, "measure", /*handleResizeScrollContainer*/ ctx[13]), action_destroyer(measurable.call(null, div1, { observePosition: true })), action_destroyer(nudgeable_action = nudgeable.call(null, div1, { direction: /*scrollDirection*/ ctx[1] === "x" ? "horizontal" : "vertical", stopKeydownPropagation: false })) ]; mounted = true; } }, p(ctx, dirty) { if (default_slot) { if (default_slot.p && dirty[1] & /*$$scope*/ 32) { update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[36], dirty, null, null); } } if (!current || dirty[0] & /*childStyle*/ 64) { attr(div0, "style", /*childStyle*/ ctx[6]); } if (!current || dirty[0] & /*klass*/ 1 && div1_class_value !== (div1_class_value = arrayJoin(["PinturaScrollable", /*klass*/ ctx[0]]))) { attr(div1, "class", div1_class_value); } if (!current || dirty[0] & /*overflowStyle*/ 16) { attr(div1, "style", /*overflowStyle*/ ctx[4]); } if (!current || dirty[0] & /*scrollDirection*/ 2) { attr(div1, "data-direction", /*scrollDirection*/ ctx[1]); } if (!current || dirty[0] & /*containerState*/ 32) { attr(div1, "data-state", /*containerState*/ ctx[5]); } if (nudgeable_action && is_function(nudgeable_action.update) && dirty[0] & /*scrollDirection*/ 2) nudgeable_action.update.call(null, { direction: /*scrollDirection*/ ctx[1] === "x" ? "horizontal" : "vertical", stopKeydownPropagation: false }); }, i(local) { if (current) return; transition_in(default_slot, local); current = true; }, o(local) { transition_out(default_slot, local); current = false; }, d(detaching) { if (detaching) detach(div1); if (default_slot) default_slot.d(detaching); /*div1_binding*/ ctx[39](null); mounted = false; run_all(dispose); } }; } function instance$G($$self, $$props, $$invalidate) { let size; let axis; let containerStyle; let containerFeatherSize; let overflows; let containerState; let childStyle; let $scrollOffset; let $keysPressedStore; let { $$slots: slots = {}, $$scope } = $$props; const dispatch = createEventDispatcher(); const keysPressedStore = getContext("keysPressed"); component_subscribe($$self, keysPressedStore, value => $$invalidate(46, $keysPressedStore = value)); let scrollState = "idle"; let scrollOrigin; let scrollRect; let scrollContainerRect; let scrollReleased; let scrollOffset = spring(0); component_subscribe($$self, scrollOffset, value => $$invalidate(34, $scrollOffset = value)); let { class: klass = undefined } = $$props; let { scrollBlockInteractionDist = 5 } = $$props; let { scrollStep = 10 } = $$props; // the distance multiplier for each mouse scroll interaction (delta) let { scrollFocusMargin = 64 } = $$props; // the margin used around elements to decided where to move the focus so elements are positioned into view with some spacing around them, this allows peaking at next/previous elements let { scrollDirection = "x" } = $$props; let { scrollAutoCancel = false } = $$props; let { elasticity = 0 } = $$props; let { onscroll = noop$1 } = $$props; let { maskFeatherSize = undefined } = $$props; let { maskFeatherStartOpacity = undefined } = $$props; let { maskFeatherEndOpacity = undefined } = $$props; let { scroll = undefined } = $$props; // logic let container; let overflowStyle = ""; // is scroll in reset state let scrollAtRest = true; // triggers onscroll callback scrollOffset.subscribe(value => { const pos = vectorCreateEmpty(); pos[scrollDirection] = value; onscroll(pos); }); const limitOffsetToContainer = offset => Math.max(Math.min(0, offset), scrollContainerRect[size] - scrollRect[size]); let scrollFirstMove; let scrollCancelled; let scrollTranslationPrev; const isHorizontalTranslation = translation => { const velocity = vectorApply(vectorCreate(translation.x - scrollTranslationPrev.x, translation.y - scrollTranslationPrev.y), Math.abs); scrollTranslationPrev = vectorClone(translation); const speed = vectorDistanceSquared(velocity); const diff = velocity.x - velocity.y; return !(speed > 1 && diff < -0.5); }; const handleDragStart = () => { // not overflowing so no need to handle if (!overflows) return; scrollCancelled = false; scrollFirstMove = true; scrollTranslationPrev = vectorCreate(0, 0); scrollReleased = false; $$invalidate(28, scrollState = "idle"); scrollOrigin = get_store_value(scrollOffset); }; const handleDragRelease = ({ detail }) => { if (!overflows) return; scrollReleased = true; $$invalidate(28, scrollState = "idle"); }; const handleDragMove = ({ detail }) => { if (!overflows) return; if (scrollCancelled) return; // fixes problem with single move event fired when clicking if (scrollFirstMove) { scrollFirstMove = false; if (vectorDistanceSquared(detail.translation) < 0.1) return; } if (scrollAutoCancel && scrollDirection === "x" && !isHorizontalTranslation(detail.translation)) { scrollCancelled = true; return; } setScrollOffset(scrollOrigin + detail.translation[scrollDirection], { elastic: true }); }; const handleDragEnd = ({ detail }) => { if (!overflows) return; if (scrollCancelled) return; const offset = scrollOrigin + detail.translation[scrollDirection]; const offsetLimited = limitOffsetToContainer(offset); scrollAtRest = false; scrollOffset.set(offsetLimited).then(res => { if (!scrollReleased) return; scrollAtRest = true; }); }; const handleResizeScrollContainer = ({ detail }) => { $$invalidate(29, scrollContainerRect = detail); dispatch("measure", { x: detail.x, y: detail.y, width: detail.width, height: detail.height }); }; const setScrollOffset = (offset, options = {}) => { const { elastic = false, animate = false } = options; // prevents clicks on child elements if the container is being scrolled if (Math.abs(offset) > scrollBlockInteractionDist && scrollState === "idle" && !scrollReleased) { $$invalidate(28, scrollState = "scrolling"); } const offsetLimited = limitOffsetToContainer(offset); const offsetVisual = elastic && elasticity && !scrollReleased ? offsetLimited + elastify(offset - offsetLimited, elasticity) : offsetLimited; let snapToPosition = true; if (animate) { snapToPosition = false; } else if (!scrollAtRest) { snapToPosition = !scrollReleased; } scrollAtRest = false; scrollOffset.set(offsetVisual, { hard: snapToPosition }).then(res => { if (!scrollReleased) return; scrollAtRest = true; }); }; const handleWheel = e => { // don't do anything if isn't overflowing if (!overflows) return; // scroll down -> move to right/down // scroll up -> move to left/up // don't run default actions, prevent other actions from running e.preventDefault(); e.stopPropagation(); // apply wheel delta to offset const delta = getWheelDelta(e); const offset = get_store_value(scrollOffset); setScrollOffset(offset + delta * scrollStep, { animate: true }); }; const handleFocus = e => { // don't do anything if isn't overflowing if (!overflows) return; // ignore this handler if is dragging if (!scrollReleased && !$keysPressedStore.length) return; let target = e.target; // when a target is marked as implicit we use its parent elemetn if (e.target.classList.contains("implicit")) target = target.parentNode; // get bounds const start = target[scrollDirection === "x" ? "offsetLeft" : "offsetTop"]; //.offsetLeft; const space = target[scrollDirection === "x" ? "offsetWidth" : "offsetHeight"]; //.offsetWidth; const end = start + space; // we need to know the current offset of the scroll so we can determine if the target is in view const currentScrollOffset = get_store_value(scrollOffset); // the margin around elements to keep in mind when focussing items const margin = scrollFocusMargin + maskFeatherSize; if (currentScrollOffset + start < margin) { setScrollOffset(-start + margin); } else if (currentScrollOffset + end > scrollContainerRect[size] - margin) { setScrollOffset(scrollContainerRect[size] - end - margin, { animate: true }); } }; const handleScroll = () => { // the scroll handler corrects auto browser scroll, // is triggered when browser tries to focus an // element outside of the scrollcontiner $$invalidate(3, container[scrollDirection === "x" ? "scrollLeft" : "scrollTop"] = 0, container); }; const handleNudge = ({ detail }) => { const delta = -2 * detail[scrollDirection]; const offset = get_store_value(scrollOffset); setScrollOffset(offset + delta * scrollStep, { animate: true }); }; const measure_handler = e => $$invalidate(2, scrollRect = e.detail); function div1_binding($$value) { binding_callbacks[$$value ? "unshift" : "push"](() => { container = $$value; $$invalidate(3, container); }); } $$self.$$set = $$props => { if ("class" in $$props) $$invalidate(0, klass = $$props.class); if ("scrollBlockInteractionDist" in $$props) $$invalidate(21, scrollBlockInteractionDist = $$props.scrollBlockInteractionDist); if ("scrollStep" in $$props) $$invalidate(22, scrollStep = $$props.scrollStep); if ("scrollFocusMargin" in $$props) $$invalidate(23, scrollFocusMargin = $$props.scrollFocusMargin); if ("scrollDirection" in $$props) $$invalidate(1, scrollDirection = $$props.scrollDirection); if ("scrollAutoCancel" in $$props) $$invalidate(24, scrollAutoCancel = $$props.scrollAutoCancel); if ("elasticity" in $$props) $$invalidate(25, elasticity = $$props.elasticity); if ("onscroll" in $$props) $$invalidate(26, onscroll = $$props.onscroll); if ("maskFeatherSize" in $$props) $$invalidate(20, maskFeatherSize = $$props.maskFeatherSize); if ("maskFeatherStartOpacity" in $$props) $$invalidate(18, maskFeatherStartOpacity = $$props.maskFeatherStartOpacity); if ("maskFeatherEndOpacity" in $$props) $$invalidate(19, maskFeatherEndOpacity = $$props.maskFeatherEndOpacity); if ("scroll" in $$props) $$invalidate(27, scroll = $$props.scroll); if ("$$scope" in $$props) $$invalidate(36, $$scope = $$props.$$scope); }; $$self.$$.update = () => { if ($$self.$$.dirty[0] & /*scrollDirection*/ 2) { $$invalidate(30, size = scrollDirection === "x" ? "width" : "height"); } if ($$self.$$.dirty[0] & /*scrollDirection*/ 2) { $$invalidate(31, axis = scrollDirection.toUpperCase()); } if ($$self.$$.dirty[0] & /*container*/ 8) { $$invalidate(32, containerStyle = container && getComputedStyle(container)); } if ($$self.$$.dirty[0] & /*container*/ 8 | $$self.$$.dirty[1] & /*containerStyle*/ 2) { $$invalidate(33, containerFeatherSize = containerStyle && unitToPixels(containerStyle.getPropertyValue("--scrollable-feather-size"))); } if ($$self.$$.dirty[0] & /*scrollContainerRect, scrollRect, size, maskFeatherStartOpacity, maskFeatherEndOpacity*/ 1611399172 | $$self.$$.dirty[1] & /*$scrollOffset, containerFeatherSize*/ 12) { if ($scrollOffset != null && scrollContainerRect && containerFeatherSize != null && scrollRect) { const startOffset = -1 * $scrollOffset / containerFeatherSize; const endOffset = -(scrollContainerRect[size] - scrollRect[size] - $scrollOffset) / containerFeatherSize; $$invalidate(18, maskFeatherStartOpacity = clamp(1 - startOffset, 0, 1)); $$invalidate(19, maskFeatherEndOpacity = clamp(1 - endOffset, 0, 1)); $$invalidate(20, maskFeatherSize = containerFeatherSize); $$invalidate(4, overflowStyle = `--scrollable-feather-start-opacity: ${maskFeatherStartOpacity};--scrollable-feather-end-opacity: ${maskFeatherEndOpacity}`); } } if ($$self.$$.dirty[0] & /*container, scroll*/ 134217736) { // update scroll position if (container && scroll !== undefined) { if (isNumber(scroll)) setScrollOffset(scroll); else setScrollOffset(scroll.scrollOffset, scroll); } } if ($$self.$$.dirty[0] & /*scrollContainerRect, scrollRect, size*/ 1610612740) { $$invalidate(35, overflows = scrollContainerRect && scrollRect ? scrollRect[size] > scrollContainerRect[size] : undefined); } if ($$self.$$.dirty[0] & /*scrollState*/ 268435456 | $$self.$$.dirty[1] & /*overflows*/ 16) { $$invalidate(5, containerState = arrayJoin([scrollState, overflows ? "overflows" : undefined])); } if ($$self.$$.dirty[1] & /*overflows, axis, $scrollOffset*/ 25) { $$invalidate(6, childStyle = overflows ? `transform: translate${axis}(${$scrollOffset}px)` : undefined); } }; return [ klass, scrollDirection, scrollRect, container, overflowStyle, containerState, childStyle, keysPressedStore, scrollOffset, handleDragStart, handleDragRelease, handleDragMove, handleDragEnd, handleResizeScrollContainer, handleWheel, handleFocus, handleScroll, handleNudge, maskFeatherStartOpacity, maskFeatherEndOpacity, maskFeatherSize, scrollBlockInteractionDist, scrollStep, scrollFocusMargin, scrollAutoCancel, elasticity, onscroll, scroll, scrollState, scrollContainerRect, size, axis, containerStyle, containerFeatherSize, $scrollOffset, overflows, $$scope, slots, measure_handler, div1_binding ]; } class Scrollable extends SvelteComponent { constructor(options) { super(); init( this, options, instance$G, create_fragment$G, safe_not_equal, { class: 0, scrollBlockInteractionDist: 21, scrollStep: 22, scrollFocusMargin: 23, scrollDirection: 1, scrollAutoCancel: 24, elasticity: 25, onscroll: 26, maskFeatherSize: 20, maskFeatherStartOpacity: 18, maskFeatherEndOpacity: 19, scroll: 27 }, [-1, -1] ); } } function fade$1(node, { delay = 0, duration = 400, easing = identity } = {}) { const o = +getComputedStyle(node).opacity; return { delay, duration, easing, css: t => `opacity: ${t * o}` }; } /* src/core/ui/components/StatusMessage.svelte generated by Svelte v3.37.0 */ function create_fragment$F(ctx) { let span; let t; let span_transition; let current; let mounted; let dispose; return { c() { span = element("span"); t = text(/*text*/ ctx[0]); attr(span, "class", "PinturaStatusMessage"); }, m(target, anchor) { insert(target, span, anchor); append(span, t); current = true; if (!mounted) { dispose = [ listen(span, "measure", function () { if (is_function(/*onmeasure*/ ctx[1])) /*onmeasure*/ ctx[1].apply(this, arguments); }), action_destroyer(measurable.call(null, span)) ]; mounted = true; } }, p(new_ctx, [dirty]) { ctx = new_ctx; if (!current || dirty & /*text*/ 1) set_data(t, /*text*/ ctx[0]); }, i(local) { if (current) return; add_render_callback(() => { if (!span_transition) span_transition = create_bidirectional_transition(span, fade$1, {}, true); span_transition.run(1); }); current = true; }, o(local) { if (!span_transition) span_transition = create_bidirectional_transition(span, fade$1, {}, false); span_transition.run(0); current = false; }, d(detaching) { if (detaching) detach(span); if (detaching && span_transition) span_transition.end(); mounted = false; run_all(dispose); } }; } function instance$F($$self, $$props, $$invalidate) { let { text } = $$props; let { onmeasure = noop$1 } = $$props; $$self.$$set = $$props => { if ("text" in $$props) $$invalidate(0, text = $$props.text); if ("onmeasure" in $$props) $$invalidate(1, onmeasure = $$props.onmeasure); }; return [text, onmeasure]; } class StatusMessage extends SvelteComponent { constructor(options) { super(); init(this, options, instance$F, create_fragment$F, safe_not_equal, { text: 0, onmeasure: 1 }); } } /* src/core/ui/components/ProgressIndicator.svelte generated by Svelte v3.37.0 */ function create_fragment$E(ctx) { let span1; let svg; let g; let circle0; let circle1; let t0; let span0; let t1; return { c() { span1 = element("span"); svg = svg_element("svg"); g = svg_element("g"); circle0 = svg_element("circle"); circle1 = svg_element("circle"); t0 = space(); span0 = element("span"); t1 = text(/*formattedValue*/ ctx[0]); attr(circle0, "class", "PinturaProgressIndicatorBar"); attr(circle0, "r", "8.5"); attr(circle0, "cx", "10"); attr(circle0, "cy", "10"); attr(circle0, "stroke-linecap", "round"); attr(circle0, "opacity", ".25"); attr(circle1, "class", "PinturaProgressIndicatorFill"); attr(circle1, "r", "8.5"); attr(circle1, "stroke-dasharray", /*circleValue*/ ctx[1]); attr(circle1, "cx", "10"); attr(circle1, "cy", "10"); attr(circle1, "transform", "rotate(-90) translate(-20)"); attr(g, "fill", "none"); attr(g, "stroke", "currentColor"); attr(g, "stroke-width", "2.5"); attr(g, "stroke-linecap", "round"); attr(g, "opacity", /*circleOpacity*/ ctx[2]); attr(svg, "width", "20"); attr(svg, "height", "20"); attr(svg, "viewBox", "0 0 20 20"); attr(svg, "xmlns", "http://www.w3.org/2000/svg"); attr(svg, "aria-hidden", "true"); attr(svg, "focusable", "false"); attr(span0, "class", "implicit"); attr(span1, "class", "PinturaProgressIndicator"); attr(span1, "data-status", /*status*/ ctx[3]); }, m(target, anchor) { insert(target, span1, anchor); append(span1, svg); append(svg, g); append(g, circle0); append(g, circle1); append(span1, t0); append(span1, span0); append(span0, t1); }, p(ctx, [dirty]) { if (dirty & /*circleValue*/ 2) { attr(circle1, "stroke-dasharray", /*circleValue*/ ctx[1]); } if (dirty & /*circleOpacity*/ 4) { attr(g, "opacity", /*circleOpacity*/ ctx[2]); } if (dirty & /*formattedValue*/ 1) set_data(t1, /*formattedValue*/ ctx[0]); if (dirty & /*status*/ 8) { attr(span1, "data-status", /*status*/ ctx[3]); } }, i: noop, o: noop, d(detaching) { if (detaching) detach(span1); } }; } function instance$E($$self, $$props, $$invalidate) { let formattedValue; let circleValue; let circleOpacity; let status; let $animatedProgressClamped; const dispatch = createEventDispatcher(); let { progress } = $$props; let { min = 0 } = $$props; let { max = 100 } = $$props; let { labelBusy = "Busy" } = $$props; const animatedValue = spring(0, { precision: 0.01 }); const animatedProgressClamped = derived([animatedValue], $animatedValue => clamp($animatedValue, min, max)); component_subscribe($$self, animatedProgressClamped, value => $$invalidate(9, $animatedProgressClamped = value)); animatedProgressClamped.subscribe(value => { if (progress === 1 && Math.round(value) >= 100) dispatch("complete"); }); $$self.$$set = $$props => { if ("progress" in $$props) $$invalidate(5, progress = $$props.progress); if ("min" in $$props) $$invalidate(6, min = $$props.min); if ("max" in $$props) $$invalidate(7, max = $$props.max); if ("labelBusy" in $$props) $$invalidate(8, labelBusy = $$props.labelBusy); }; $$self.$$.update = () => { if ($$self.$$.dirty & /*progress*/ 32) { progress && progress !== Infinity && animatedValue.set(progress * 100); } if ($$self.$$.dirty & /*progress, labelBusy, $animatedProgressClamped*/ 800) { $$invalidate(0, formattedValue = progress === Infinity ? labelBusy : `${Math.round($animatedProgressClamped)}%`); } if ($$self.$$.dirty & /*progress, $animatedProgressClamped*/ 544) { $$invalidate(1, circleValue = progress === Infinity ? "26.5 53" : `${$animatedProgressClamped / 100 * 53} 53`); } if ($$self.$$.dirty & /*progress, $animatedProgressClamped*/ 544) { $$invalidate(2, circleOpacity = Math.min(1, progress === Infinity ? 1 : $animatedProgressClamped / 10)); } if ($$self.$$.dirty & /*progress*/ 32) { $$invalidate(3, status = progress === Infinity ? "busy" : "loading"); } }; return [ formattedValue, circleValue, circleOpacity, status, animatedProgressClamped, progress, min, max, labelBusy, $animatedProgressClamped ]; } class ProgressIndicator extends SvelteComponent { constructor(options) { super(); init(this, options, instance$E, create_fragment$E, safe_not_equal, { progress: 5, min: 6, max: 7, labelBusy: 8 }); } } /* src/core/ui/components/StatusAside.svelte generated by Svelte v3.37.0 */ function create_fragment$D(ctx) { let span; let span_class_value; let current; const default_slot_template = /*#slots*/ ctx[5].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[4], null); return { c() { span = element("span"); if (default_slot) default_slot.c(); attr(span, "class", span_class_value = `PinturaStatusAside ${/*klass*/ ctx[0]}`); attr(span, "style", /*style*/ ctx[1]); }, m(target, anchor) { insert(target, span, anchor); if (default_slot) { default_slot.m(span, null); } current = true; }, p(ctx, [dirty]) { if (default_slot) { if (default_slot.p && dirty & /*$$scope*/ 16) { update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[4], dirty, null, null); } } if (!current || dirty & /*klass*/ 1 && span_class_value !== (span_class_value = `PinturaStatusAside ${/*klass*/ ctx[0]}`)) { attr(span, "class", span_class_value); } if (!current || dirty & /*style*/ 2) { attr(span, "style", /*style*/ ctx[1]); } }, i(local) { if (current) return; transition_in(default_slot, local); current = true; }, o(local) { transition_out(default_slot, local); current = false; }, d(detaching) { if (detaching) detach(span); if (default_slot) default_slot.d(detaching); } }; } function instance$D($$self, $$props, $$invalidate) { let style; let { $$slots: slots = {}, $$scope } = $$props; let { offset = 0 } = $$props; let { opacity = 0 } = $$props; let { class: klass = undefined } = $$props; $$self.$$set = $$props => { if ("offset" in $$props) $$invalidate(2, offset = $$props.offset); if ("opacity" in $$props) $$invalidate(3, opacity = $$props.opacity); if ("class" in $$props) $$invalidate(0, klass = $$props.class); if ("$$scope" in $$props) $$invalidate(4, $$scope = $$props.$$scope); }; $$self.$$.update = () => { if ($$self.$$.dirty & /*offset, opacity*/ 12) { $$invalidate(1, style = `transform:translateX(${offset}px);opacity:${opacity}`); } }; return [klass, style, offset, opacity, $$scope, slots]; } class StatusAside extends SvelteComponent { constructor(options) { super(); init(this, options, instance$D, create_fragment$D, safe_not_equal, { offset: 2, opacity: 3, class: 0 }); } } /* src/core/ui/components/Tag.svelte generated by Svelte v3.37.0 */ function create_if_block_2$9(ctx) { let label; let label_for_value; let current; const default_slot_template = /*#slots*/ ctx[3].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[2], null); let label_levels = [{ for: label_for_value = "_" }, /*attributes*/ ctx[1]]; let label_data = {}; for (let i = 0; i < label_levels.length; i += 1) { label_data = assign(label_data, label_levels[i]); } return { c() { label = element("label"); if (default_slot) default_slot.c(); set_attributes(label, label_data); }, m(target, anchor) { insert(target, label, anchor); if (default_slot) { default_slot.m(label, null); } current = true; }, p(ctx, dirty) { if (default_slot) { if (default_slot.p && dirty & /*$$scope*/ 4) { update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[2], dirty, null, null); } } set_attributes(label, label_data = get_spread_update(label_levels, [ { for: label_for_value }, dirty & /*attributes*/ 2 && /*attributes*/ ctx[1] ])); }, i(local) { if (current) return; transition_in(default_slot, local); current = true; }, o(local) { transition_out(default_slot, local); current = false; }, d(detaching) { if (detaching) detach(label); if (default_slot) default_slot.d(detaching); } }; } // (12:26) function create_if_block_1$c(ctx) { let div; let current; const default_slot_template = /*#slots*/ ctx[3].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[2], null); let div_levels = [/*attributes*/ ctx[1]]; let div_data = {}; for (let i = 0; i < div_levels.length; i += 1) { div_data = assign(div_data, div_levels[i]); } return { c() { div = element("div"); if (default_slot) default_slot.c(); set_attributes(div, div_data); }, m(target, anchor) { insert(target, div, anchor); if (default_slot) { default_slot.m(div, null); } current = true; }, p(ctx, dirty) { if (default_slot) { if (default_slot.p && dirty & /*$$scope*/ 4) { update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[2], dirty, null, null); } } set_attributes(div, div_data = get_spread_update(div_levels, [dirty & /*attributes*/ 2 && /*attributes*/ ctx[1]])); }, i(local) { if (current) return; transition_in(default_slot, local); current = true; }, o(local) { transition_out(default_slot, local); current = false; }, d(detaching) { if (detaching) detach(div); if (default_slot) default_slot.d(detaching); } }; } // (8:0) {#if name === 'div'} function create_if_block$b(ctx) { let div; let current; const default_slot_template = /*#slots*/ ctx[3].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[2], null); let div_levels = [/*attributes*/ ctx[1]]; let div_data = {}; for (let i = 0; i < div_levels.length; i += 1) { div_data = assign(div_data, div_levels[i]); } return { c() { div = element("div"); if (default_slot) default_slot.c(); set_attributes(div, div_data); }, m(target, anchor) { insert(target, div, anchor); if (default_slot) { default_slot.m(div, null); } current = true; }, p(ctx, dirty) { if (default_slot) { if (default_slot.p && dirty & /*$$scope*/ 4) { update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[2], dirty, null, null); } } set_attributes(div, div_data = get_spread_update(div_levels, [dirty & /*attributes*/ 2 && /*attributes*/ ctx[1]])); }, i(local) { if (current) return; transition_in(default_slot, local); current = true; }, o(local) { transition_out(default_slot, local); current = false; }, d(detaching) { if (detaching) detach(div); if (default_slot) default_slot.d(detaching); } }; } function create_fragment$C(ctx) { let current_block_type_index; let if_block; let if_block_anchor; let current; const if_block_creators = [create_if_block$b, create_if_block_1$c, create_if_block_2$9]; const if_blocks = []; function select_block_type(ctx, dirty) { if (/*name*/ ctx[0] === "div") return 0; if (/*name*/ ctx[0] === "span") return 1; if (/*name*/ ctx[0] === "label") return 2; return -1; } if (~(current_block_type_index = select_block_type(ctx))) { if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); } return { c() { if (if_block) if_block.c(); if_block_anchor = empty(); }, m(target, anchor) { if (~current_block_type_index) { if_blocks[current_block_type_index].m(target, anchor); } insert(target, if_block_anchor, anchor); current = true; }, p(ctx, [dirty]) { let previous_block_index = current_block_type_index; current_block_type_index = select_block_type(ctx); if (current_block_type_index === previous_block_index) { if (~current_block_type_index) { if_blocks[current_block_type_index].p(ctx, dirty); } } else { if (if_block) { group_outros(); transition_out(if_blocks[previous_block_index], 1, 1, () => { if_blocks[previous_block_index] = null; }); check_outros(); } if (~current_block_type_index) { if_block = if_blocks[current_block_type_index]; if (!if_block) { if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); if_block.c(); } else { if_block.p(ctx, dirty); } transition_in(if_block, 1); if_block.m(if_block_anchor.parentNode, if_block_anchor); } else { if_block = null; } } }, i(local) { if (current) return; transition_in(if_block); current = true; }, o(local) { transition_out(if_block); current = false; }, d(detaching) { if (~current_block_type_index) { if_blocks[current_block_type_index].d(detaching); } if (detaching) detach(if_block_anchor); } }; } function instance$C($$self, $$props, $$invalidate) { let { $$slots: slots = {}, $$scope } = $$props; let { name = "div" } = $$props; let { attributes = {} } = $$props; $$self.$$set = $$props => { if ("name" in $$props) $$invalidate(0, name = $$props.name); if ("attributes" in $$props) $$invalidate(1, attributes = $$props.attributes); if ("$$scope" in $$props) $$invalidate(2, $$scope = $$props.$$scope); }; return [name, attributes, $$scope, slots]; } class Tag extends SvelteComponent { constructor(options) { super(); init(this, options, instance$C, create_fragment$C, safe_not_equal, { name: 0, attributes: 1 }); } } var getDevicePixelRatio = () => (isBrowser() && window.devicePixelRatio) || 1; // if this is a non retina display snap to pixel let fn = null; var snapToPixel = (v) => { if (fn === null) fn = getDevicePixelRatio() === 1 ? (v) => Math.round(v) : (v) => v; return fn(v); }; /* src/core/ui/components/Details.svelte generated by Svelte v3.37.0 */ const get_details_slot_changes = dirty => ({}); const get_details_slot_context = ctx => ({}); const get_label_slot_changes = dirty => ({}); const get_label_slot_context = ctx => ({}); // (177:0)