/*!
* 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 ``;
};
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)