dotting
Version:
Dotting is a pixel art editor component library for react
1,302 lines (1,289 loc) • 353 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var React = require('react');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
const DefaultPanZoom = {
scale: 1,
offset: { x: 0, y: 0 },
};
const DefaultGridSquareLength = 20;
const DefaultButtonHeight = 30;
const DefaultButtonMargin = DefaultButtonHeight / 2;
const MaxImageBitMapSideLength = 2500;
var ButtonDirection;
(function (ButtonDirection) {
ButtonDirection["TOP"] = "TOP";
ButtonDirection["BOTTOM"] = "BOTTOM";
ButtonDirection["LEFT"] = "LEFT";
ButtonDirection["RIGHT"] = "RIGHT";
ButtonDirection["TOPLEFT"] = "TOPLEFT";
ButtonDirection["TOPRIGHT"] = "TOPRIGHT";
ButtonDirection["BOTTOMLEFT"] = "BOTTOMLEFT";
ButtonDirection["BOTTOMRIGHT"] = "BOTTOMRIGHT";
})(ButtonDirection || (ButtonDirection = {}));
const DefaultPixelDataDimensions = {
columnCount: 10,
rowCount: 10,
};
var MouseMode;
(function (MouseMode) {
MouseMode["PANNING"] = "PANNING";
MouseMode["PINCHZOOMING"] = "PINCHZOOMING";
MouseMode["EXTENDING"] = "EXTENDING";
MouseMode["DRAWING"] = "DRAWING";
MouseMode["NULL"] = "NULL";
})(MouseMode || (MouseMode = {}));
const DefaultZoomSensitivity = 200;
const DefaultMaxScale = 1.5;
const DefaultMinScale = 0.3;
const CurrentDeviceUserId = "current-device-user-id";
const InteractionExtensionAllowanceRatio = 2;
const InteractionEdgeTouchingRange = 6;
const DashedLineOffsetFromPixelCanvas = 15;
const ExtensionGuideCircleRadius = 3;
const DefaultExtendArrowPadding = 2;
const DefaultBackgroundColor = "#999999";
const DefaultPixelColor = "#ffffff";
const GridMinimumScale = 0.2;
const MinColumnOrRowCount = 2; // the grid size should be at least 2x2
class DottingError extends Error {
constructor(message) {
super(message);
this.name = "DottingError";
}
}
class DuplicateLayerIdError extends DottingError {
constructor(layerId) {
super(`Duplicate layer id ${layerId}. Please make sure all layer ids are unique.`);
this.name = "DuplicateLayerIdError";
}
}
class InvalidSquareDataError extends DottingError {
constructor(layerId) {
const message = layerId ? ` for layer ${layerId}` : "";
super(`Invalid square data${message}. Please make sure all data have the same row and column count.`);
this.name = "InvalidSquareDataError";
}
}
class InvalidDataDimensionsError extends DottingError {
constructor(layerId) {
const message = layerId ? ` for layer ${layerId}` : "";
super(`Invalid data dimensions${message}. Please make sure all data have the same dimensions.`);
this.name = "InvalidDataDimensionsError";
}
}
class InvalidDataIndicesError extends DottingError {
constructor(layerId) {
const message = layerId ? ` for layer ${layerId}` : "";
super(`Invalid data indices${message}. Please make sure all data have the same topRowIndex and leftColumnIndex.`);
this.name = "InvalidDataIndicesError";
}
}
class UnspecifiedLayerIdError extends DottingError {
constructor() {
super(`Layer id has not been specified`);
this.name = "UnspecifiedLayerIdError";
}
}
class LayerNotFoundError extends DottingError {
constructor(layerId) {
super(`Layer ${layerId} not found.`);
this.name = "LayerNotFoundError";
}
}
class UnrecognizedDownloadOptionError extends DottingError {
constructor() {
super(`Unrecognized download option.`);
this.name = "UnrecognizedDownloadOptionError";
}
}
class NoDataToMakeSvgError extends DottingError {
constructor() {
super(`No data to make svg.`);
this.name = "NoDataToMakeSvgError";
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const convertCartesianToScreen = (canvas, cartesianCoord, dpr) => {
const screenPoint = {
x: cartesianCoord.x + canvas.width / dpr / 2,
y: cartesianCoord.y + canvas.height / dpr / 2,
};
return screenPoint;
};
function diffPoints(p1, p2) {
return { x: p1.x - p2.x, y: p1.y - p2.y };
}
function addPoints(p1, p2) {
return { x: p1.x + p2.x, y: p1.y + p2.y };
}
/**
* Actual world point is converted to screen(=viewing) point
* @param point
* @param panZoom
* @returns
*/
function getScreenPoint(point, panZoom) {
const { offset, scale } = panZoom;
return {
x: Math.floor(point.x * scale + offset.x),
y: Math.floor(point.y * scale + offset.y),
};
}
/**
* This is the real point in the actual world
* @param point
* @param panZoom
* @returns
*/
function getWorldPoint(point, panZoom) {
const { offset, scale } = panZoom;
return { x: (point.x - offset.x) / scale, y: (point.y - offset.y) / scale };
}
function lerpRanges(value, range1Start, range1End, range2Start, range2End) {
const ratio = (value - range1Start) / (range1End - range1Start);
return range2Start + (range2End - range2Start) * ratio;
}
function getBresenhamLineIndices(x1, y1, x2, y2) {
const startPosition = { x1, y1 };
const endPosition = { x2, y2 };
const width = x2 - x1;
const height = y2 - y1;
const isGradualSlope = Math.abs(width) >= Math.abs(height);
const directionX = width >= 0 ? 1 : -1;
const directionY = height >= 0 ? 1 : -1;
const fw = directionX * width;
const fh = directionY * height;
let f = isGradualSlope ? fh * 2 - fw : 2 * fw - fh;
const f1 = isGradualSlope ? 2 * fh : 2 * fw;
const f2 = isGradualSlope ? 2 * (fh - fw) : 2 * (fw - fh);
let x = startPosition.x1;
let y = startPosition.y1;
const missingPoints = [];
if (isGradualSlope) {
while (x != endPosition.x2) {
missingPoints.push({ rowIndex: x, columnIndex: y });
if (f < 0) {
f += f1;
}
else {
f += f2;
y += directionY;
}
x += directionX;
}
}
else {
while (y != endPosition.y2) {
missingPoints.push({ rowIndex: x, columnIndex: y });
if (f < 0) {
f += f1;
}
else {
f += f2;
x += directionX;
}
y += directionY;
}
}
return missingPoints;
}
function getBresenhamEllipseIndices(x1, y1, x2, y2, filled) {
const points = [];
let a = Math.abs(x2 - x1);
const b = Math.abs(y2 - y1);
let b1 = b & 1;
let dx = 4 * (1 - a) * b * b;
let dy = 4 * (b1 + 1) * a * a;
let err = dx + dy + b1 * a * a;
let e2 = 0;
if (x1 > x2) {
x1 = x2;
x2 += a;
}
if (y1 > y2) {
y1 = y2;
}
y1 += (b + 1) >> 1;
y2 = y1 - b1;
a = 8 * a * a;
b1 = 8 * b * b;
do {
if (filled) {
for (let row = y2; row <= y1; row++) {
points.push({ columnIndex: x1, rowIndex: row });
points.push({ columnIndex: x2, rowIndex: row });
}
}
else {
points.push({ columnIndex: x2, rowIndex: y1 });
points.push({ columnIndex: x1, rowIndex: y1 });
points.push({ columnIndex: x1, rowIndex: y2 });
points.push({ columnIndex: x2, rowIndex: y2 });
}
e2 = 2 * err;
if (e2 <= dy) {
y1++;
y2--;
err += dy += a;
}
if (e2 >= dx || 2 * err > dy) {
x1++;
x2--;
err += dx += b1;
}
} while (x1 <= x2);
while (y1 - y2 <= b) {
points.push({ columnIndex: x1 - 1, rowIndex: y1 });
points.push({ columnIndex: x2 + 1, rowIndex: y1++ });
points.push({ columnIndex: x1 - 1, rowIndex: y2 });
points.push({ columnIndex: x2 + 1, rowIndex: y2-- });
}
return points.filter((value, index, self) => index === self.findIndex((t) => (t.columnIndex === value.columnIndex && t.rowIndex === value.rowIndex)));
}
const getPointFromTouchyEvent = (evt, element, panZoom) => {
let originY;
let originX;
let offsetX;
let offsetY;
if (window.TouchEvent && evt instanceof TouchEvent) {
//this is for tablet or mobile
let isCanvasTouchIncluded = false;
let firstCanvasTouchIndex = 0;
for (let i = 0; i < evt.touches.length; i++) {
const target = evt.touches.item(i).target;
if (target instanceof HTMLCanvasElement) {
isCanvasTouchIncluded = true;
firstCanvasTouchIndex = i;
break;
}
}
if (isCanvasTouchIncluded) {
return getPointFromTouch(evt.touches[firstCanvasTouchIndex], element, panZoom);
}
else {
return getPointFromTouch(evt.touches[0], element, panZoom);
}
}
else {
//this is for PC
originY = evt.clientY;
originX = evt.clientX;
offsetX = evt.offsetX;
offsetY = evt.offsetY;
}
originY += window.scrollY;
originX += window.scrollX;
return {
y: originY - panZoom.offset.y,
x: originX - panZoom.offset.x,
offsetX: offsetX,
offsetY: offsetY,
};
};
const getPointFromTouch = (touch, element, panZoom) => {
const r = element.getBoundingClientRect();
const originY = touch.clientY;
const originX = touch.clientX;
const offsetX = touch.clientX - r.left;
const offsetY = touch.clientY - r.top;
return {
x: originX - panZoom.offset.x,
y: originY - panZoom.offset.y,
offsetX: offsetX,
offsetY: offsetY,
};
};
const calculateNewPanZoomFromPinchZoom = (evt, element, panZoom, zoomSensitivity, prevPinchZoomDiff, minScale, maxScale) => {
evt.preventDefault();
if (window.TouchEvent && evt instanceof TouchEvent) {
const touchCount = evt.touches.length;
if (touchCount < 2) {
return;
}
const canvasTouchEventIndexes = [];
for (let i = 0; i < touchCount; i++) {
const target = evt.touches.item(i).target;
if (target instanceof HTMLCanvasElement) {
canvasTouchEventIndexes.push(i);
}
}
if (canvasTouchEventIndexes.length !== 2) {
return;
}
const firstTouch = evt.touches[canvasTouchEventIndexes[0]];
const secondTouch = evt.touches[canvasTouchEventIndexes[1]];
const pinchZoomCurrentDiff = Math.abs(firstTouch.clientX - secondTouch.clientX) +
Math.abs(firstTouch.clientY - secondTouch.clientY);
const firstTouchPoint = getPointFromTouch(firstTouch, element, panZoom);
const secondTouchPoint = getPointFromTouch(secondTouch, element, panZoom);
const touchCenterPos = {
x: (firstTouchPoint.offsetX + secondTouchPoint.offsetX) / 2,
y: (firstTouchPoint.offsetY + secondTouchPoint.offsetY) / 2,
};
if (!prevPinchZoomDiff) {
return { pinchZoomDiff: pinchZoomCurrentDiff, panZoom };
}
const deltaX = prevPinchZoomDiff - pinchZoomCurrentDiff;
const zoom = 1 - (deltaX * 2) / (zoomSensitivity * 2);
const newScale = panZoom.scale * zoom;
if (minScale > newScale || newScale > maxScale) {
return;
}
const worldPos = getWorldPoint(touchCenterPos, {
scale: panZoom.scale,
offset: panZoom.offset,
});
const newTouchCenterPos = getScreenPoint(worldPos, {
scale: newScale,
offset: panZoom.offset,
});
const scaleOffset = diffPoints(touchCenterPos, newTouchCenterPos);
const offset = addPoints(panZoom.offset, scaleOffset);
return {
pinchZoomDiff: pinchZoomCurrentDiff,
panZoom: { scale: newScale, offset },
};
}
else {
return null;
}
};
const getMouseCartCoord = (evt, element, panZoom, dpr) => {
evt.preventDefault();
const point = getPointFromTouchyEvent(evt, element, panZoom);
const pointCoord = { x: point.offsetX, y: point.offsetY };
const diffPointsOfMouseOffset = getWorldPoint(pointCoord, panZoom);
const mouseCartCoord = diffPoints(diffPointsOfMouseOffset, {
x: element.width / dpr / 2,
y: element.height / dpr / 2,
});
return mouseCartCoord;
};
const getCenterCartCoordFromTwoTouches = (evt, element, panZoom, dpr) => {
evt.preventDefault();
if (evt.touches && evt.touches.length < 2)
return null;
if (evt.touches.length > 2)
return null;
const touch1 = evt.touches[0];
const touch2 = evt.touches[1];
const touch1Point = getPointFromTouch(touch1, element, panZoom);
const touch2Point = getPointFromTouch(touch2, element, panZoom);
const touchCenterPos = {
x: (touch1Point.offsetX + touch2Point.offsetX) / 2,
y: (touch1Point.offsetY + touch2Point.offsetY) / 2,
};
const diffPointsOfMouseOffset = getWorldPoint(touchCenterPos, panZoom);
const mouseCartCoord = diffPoints(diffPointsOfMouseOffset, {
x: element.width / dpr / 2,
y: element.height / dpr / 2,
});
return mouseCartCoord;
};
const getPixelIndexFromMouseCartCoord = (mouseCartCoord, sortedRowIndices, sortedColumnIndices, gridSquareLength) => {
const leftColumnIndex = sortedColumnIndices[0];
const topRowIndex = sortedRowIndices[0];
const leftTopPoint = {
x: leftColumnIndex * gridSquareLength,
y: topRowIndex * gridSquareLength,
};
if (mouseCartCoord.x > leftTopPoint.x &&
mouseCartCoord.x <
leftTopPoint.x + sortedColumnIndices.length * gridSquareLength &&
mouseCartCoord.y > leftTopPoint.y &&
mouseCartCoord.y <
leftTopPoint.y + sortedRowIndices.length * gridSquareLength) {
// The above conditions are to check if the mouse is in the grid
const rowOffset = Math.floor((mouseCartCoord.y - leftTopPoint.y) / gridSquareLength);
const columnOffset = Math.floor((mouseCartCoord.x - leftTopPoint.x) / gridSquareLength);
return {
rowIndex: sortedRowIndices[rowOffset],
columnIndex: sortedColumnIndices[columnOffset],
};
}
return null;
};
const getIsPointInsideRegion = (point, area) => {
const { areaTopLeftPos, areaBottomRightPos } = getAreaTopLeftAndBottomRight(area);
return (point.x >= areaTopLeftPos.x &&
point.x <= areaBottomRightPos.x &&
point.y >= areaTopLeftPos.y &&
point.y <= areaBottomRightPos.y);
};
const getDoesAreaOverlapPixelgrid = (area, rowCount, columnCount, gridSquareLength) => {
if (!area)
return false;
const { areaTopLeftPos, areaBottomRightPos } = getAreaTopLeftAndBottomRight(area);
const pixelGridLeftTopPoint = {
x: -((columnCount / 2) * gridSquareLength),
y: -((rowCount / 2) * gridSquareLength),
};
const pixelGridRightBottomPoint = {
x: (columnCount / 2) * gridSquareLength,
y: (rowCount / 2) * gridSquareLength,
};
return (areaTopLeftPos.x <= pixelGridRightBottomPoint.x ||
areaTopLeftPos.y <= pixelGridRightBottomPoint.y ||
areaBottomRightPos.x >= pixelGridLeftTopPoint.x ||
areaBottomRightPos.y >= pixelGridLeftTopPoint.y);
};
const getAreaTopLeftAndBottomRight = (area) => {
const isAreaFromLeftToRight = area.startWorldPos.x < area.endWorldPos.x;
const isAreaFromTopToBottom = area.startWorldPos.y < area.endWorldPos.y;
// To ease the algorithm, we will first identify the left top, right top, left bottom and right bottom points
const areaTopLeftPos = {
x: isAreaFromLeftToRight ? area.startWorldPos.x : area.endWorldPos.x,
y: isAreaFromTopToBottom ? area.startWorldPos.y : area.endWorldPos.y,
};
const areaBottomRightPos = {
x: isAreaFromLeftToRight ? area.endWorldPos.x : area.startWorldPos.x,
y: isAreaFromTopToBottom ? area.endWorldPos.y : area.startWorldPos.y,
};
return { areaTopLeftPos, areaBottomRightPos };
};
const convertWorldPosAreaToPixelGridArea = (selectingArea, rowCount, columnCount, gridSquareLength, rowKeysInOrder, columnKeysInOrder) => {
const leftColumnIndex = columnKeysInOrder[0];
const topRowIndex = rowKeysInOrder[0];
const { areaTopLeftPos, areaBottomRightPos } = getAreaTopLeftAndBottomRight(selectingArea);
const pixelGridLeftTopPoint = {
x: leftColumnIndex * gridSquareLength,
y: topRowIndex * gridSquareLength,
};
const selectedRegionTopLeft = {
x: 0,
y: 0,
};
const selectedRegionBottomRight = {
x: 0,
y: 0,
};
// leftTopPoint is the the left top point of the grid
const selectedAreaLeftOffsetAmount = areaTopLeftPos.x - pixelGridLeftTopPoint.x;
// if selectedAreaLeftOffsetAmount is negative, then the selected area's left part is outside the grid
if (selectedAreaLeftOffsetAmount < 0) {
selectedRegionTopLeft.x = pixelGridLeftTopPoint.x;
}
else {
if (selectedAreaLeftOffsetAmount > columnCount * gridSquareLength) {
return null;
}
selectedRegionTopLeft.x =
pixelGridLeftTopPoint.x +
Math.floor(selectedAreaLeftOffsetAmount / gridSquareLength) *
gridSquareLength;
}
// if selectedAreaTopOffsetAmount is negative, then the selected area's top part is outside the grid
const selectedAreaTopOffsetAmount = areaTopLeftPos.y - pixelGridLeftTopPoint.y;
if (selectedAreaTopOffsetAmount < 0) {
selectedRegionTopLeft.y = pixelGridLeftTopPoint.y;
}
else {
if (selectedAreaTopOffsetAmount > rowCount * gridSquareLength) {
return null;
}
selectedRegionTopLeft.y =
pixelGridLeftTopPoint.y +
Math.floor(selectedAreaTopOffsetAmount / gridSquareLength) *
gridSquareLength;
}
// if selectedAreaRightOffsetAmount is positive, then the selected area's right part is outside the grid
const selectedAreaRightOffsetAmount = areaBottomRightPos.x -
(pixelGridLeftTopPoint.x + columnCount * gridSquareLength);
if (selectedAreaRightOffsetAmount > 0) {
selectedRegionBottomRight.x =
pixelGridLeftTopPoint.x + columnCount * gridSquareLength;
}
else {
if (selectedAreaRightOffsetAmount < -columnCount * gridSquareLength) {
return null;
}
selectedRegionBottomRight.x =
pixelGridLeftTopPoint.x +
columnCount * gridSquareLength +
Math.ceil(selectedAreaRightOffsetAmount / gridSquareLength) *
gridSquareLength;
}
// if selectedAreaBottomOffsetAmount is positive, then the selected area's bottom part is outside the grid
const selectedAreaBottomOffsetAmount = areaBottomRightPos.y -
(pixelGridLeftTopPoint.y + rowCount * gridSquareLength);
if (selectedAreaBottomOffsetAmount > 0) {
selectedRegionBottomRight.y =
pixelGridLeftTopPoint.y + rowCount * gridSquareLength;
}
else {
if (selectedAreaBottomOffsetAmount < -rowCount * gridSquareLength) {
return null;
}
selectedRegionBottomRight.y =
pixelGridLeftTopPoint.y +
rowCount * gridSquareLength +
Math.ceil(selectedAreaBottomOffsetAmount / gridSquareLength) *
gridSquareLength;
}
const relativeTopLeftRowIndex = Math.floor((selectedRegionTopLeft.y - pixelGridLeftTopPoint.y) / gridSquareLength);
const relativeTopLeftColumnIndex = Math.floor((selectedRegionTopLeft.x - pixelGridLeftTopPoint.x) / gridSquareLength);
const relativeBottomRightRowIndex = Math.floor((selectedRegionBottomRight.y - pixelGridLeftTopPoint.y) / gridSquareLength);
const relativeBottomRightColumnIndex = Math.floor((selectedRegionBottomRight.x - pixelGridLeftTopPoint.x) / gridSquareLength);
const includedPixelsIndices = [];
for (let i = relativeTopLeftRowIndex; i < relativeBottomRightRowIndex; i++) {
for (let j = relativeTopLeftColumnIndex; j < relativeBottomRightColumnIndex; j++) {
includedPixelsIndices.push({
rowIndex: rowKeysInOrder[i],
columnIndex: columnKeysInOrder[j],
});
}
}
return {
startWorldPos: selectedRegionTopLeft,
endWorldPos: selectedRegionBottomRight,
includedPixelsIndices,
};
};
const returnScrollOffsetFromMouseOffset = (mouseOffset, panZoom, newScale) => {
const worldPos = getWorldPoint(mouseOffset, panZoom);
const newMousePos = getScreenPoint(worldPos, {
scale: newScale,
offset: panZoom.offset,
});
const scaleOffset = diffPoints(mouseOffset, newMousePos);
const offset = addPoints(panZoom.offset, scaleOffset);
return offset;
};
/**
* @summary it will return the overlapping pixel indices of the pixels for an extended selected area
* @param originalPixels - the original pixels
* @param originPixelIndex - the origin pixel index
* @param modifyPixelWidthRatio - the ratio of the width of the modified pixel to the original pixel
* @param modifyPixelHeightRatio - the ratio of the height of the modified pixel to the original pixel
* @param gridSquareLength - the length of the grid square
* @returns the pixel indices of the overlapping pixels
*/
const getOverlappingPixelIndicesForModifiedPixels = (originalPixels, originPixelIndex, modifyPixelWidthRatio, modifyPixelHeightRatio, gridSquareLength) => {
if (modifyPixelHeightRatio < 0 || modifyPixelWidthRatio < 0) {
throw new Error("modifyPixelHeightRatio and modifyPixelWidthRatio should be positive");
}
if (gridSquareLength < 0) {
throw new Error("gridSquareLength should be positive");
}
const pixelsToColor = [];
for (const item of originalPixels) {
const pixelDistanceFromOrigin = {
rowOffset: item.rowIndex - originPixelIndex.rowIndex,
columnOffset: item.columnIndex - originPixelIndex.columnIndex,
};
const pixelWordPosOffset = {
x: pixelDistanceFromOrigin.columnOffset * gridSquareLength,
y: pixelDistanceFromOrigin.rowOffset * gridSquareLength,
};
const cornerWorldPos = {
topLeft: {
x: pixelWordPosOffset.x * modifyPixelWidthRatio,
y: pixelWordPosOffset.y * modifyPixelHeightRatio,
},
topRight: {
x: (pixelWordPosOffset.x + gridSquareLength) * modifyPixelWidthRatio,
y: pixelWordPosOffset.y * modifyPixelHeightRatio,
},
bottomLeft: {
x: pixelWordPosOffset.x * modifyPixelWidthRatio,
y: (pixelWordPosOffset.y + gridSquareLength) * modifyPixelHeightRatio,
},
bottomRight: {
x: (pixelWordPosOffset.x + gridSquareLength) * modifyPixelWidthRatio,
y: (pixelWordPosOffset.y + gridSquareLength) * modifyPixelHeightRatio,
},
};
for (let i = cornerWorldPos.topLeft.x; i < cornerWorldPos.topRight.x; i += gridSquareLength) {
for (let j = cornerWorldPos.topLeft.y; j < cornerWorldPos.bottomLeft.y; j += gridSquareLength) {
const pixelIndex = {
rowIndex: Math.round(originPixelIndex.rowIndex + Math.floor(j / gridSquareLength)),
columnIndex: Math.round(originPixelIndex.columnIndex + Math.floor(i / gridSquareLength)),
color: item.color,
previousColor: item.previousColor,
};
// console.log(originPixelIndex, "originPixelIndex");
// console.log(
// pixelIndex,
// "pixelIndex",
// );
pixelsToColor.push(pixelIndex);
}
}
}
return pixelsToColor;
};
const getGridIndicesFromData = (data) => {
const allRowKeys = Array.from(data.keys());
const allColumnKeys = Array.from(data.get(allRowKeys[0]).keys());
const currentTopIndex = Math.min(...allRowKeys);
const currentLeftIndex = Math.min(...allColumnKeys);
const currentBottomIndex = Math.max(...allRowKeys);
const currentRightIndex = Math.max(...allColumnKeys);
return {
topRowIndex: currentTopIndex,
bottomRowIndex: currentBottomIndex,
leftColumnIndex: currentLeftIndex,
rightColumnIndex: currentRightIndex,
};
};
const getAllGridIndicesSorted = (data) => {
const allRowKeys = Array.from(data.keys());
const allColumnKeys = Array.from(data.get(allRowKeys[0]).keys());
return {
rowIndices: allRowKeys.sort((a, b) => a - b),
columnIndices: allColumnKeys.sort((a, b) => a - b),
};
};
/**
* @description get all the row keys (sorted) from the data
* @param data
*/
const getRowKeysFromData = (data) => {
return Array.from(data.keys()).sort((a, b) => a - b);
};
/**
* @description get all the column keys (sorted) from the data
* @param data
*/
const getColumnKeysFromData = (data) => {
const allRowKeys = getRowKeysFromData(data);
const allColumnKeys = Array.from(data.get(allRowKeys[0]).keys()).sort((a, b) => a - b);
return allColumnKeys;
};
const getColumnCountFromData = (data) => {
if (data.size === 0)
return 0;
return data.entries().next().value[1].size;
};
const getRowCountFromData = (data) => {
return data.size;
};
const extractColoredPixelsFromRow = (data, rowIndex) => {
const rowPixelsMap = data.get(rowIndex);
const pixelModifyItems = [];
Array.from(rowPixelsMap.entries()).forEach(columnData => {
const [key, pixel] = columnData;
if (pixel.color) {
pixelModifyItems.push({
color: pixel.color,
rowIndex: rowIndex,
columnIndex: key,
});
}
});
return pixelModifyItems;
};
const extractColoredPixelsFromColumn = (data, columnIndex) => {
const pixelModifyItems = [];
Array.from(data.entries()).map(rowData => {
const rowIndex = rowData[0];
const row = rowData[1];
if (row.get(columnIndex).color) {
pixelModifyItems.push({
rowIndex: rowIndex,
columnIndex: columnIndex,
color: row.get(columnIndex).color,
});
}
});
return pixelModifyItems;
};
const deleteRowOfData = (data, rowIndex) => {
if (!data.has(rowIndex))
return;
data.delete(rowIndex);
};
const deleteColumnOfData = (data, columnIndex) => {
data.forEach(row => {
if (!row.has(columnIndex)) {
return;
}
row.delete(columnIndex);
});
};
const addRowToData = (data, rowIndex, defaultColor) => {
const columnKeys = getColumnKeysFromData(data);
if (data.has(rowIndex)) {
return;
}
data.set(rowIndex, new Map());
for (const i of columnKeys) {
data.get(rowIndex).set(i, { color: "" });
}
};
const addColumnToData = (data, columnIndex, defaultColor) => {
data.forEach(row => {
if (!row.has(columnIndex)) {
row.set(columnIndex, { color: "" });
}
});
};
const validateSquareArray = (data) => {
const dataRowCount = data.length;
let columnCount = 0;
const rowCount = dataRowCount;
let isDataValid = true;
if (dataRowCount < 2) {
isDataValid = false;
}
else {
const dataColumnCount = data[0].length;
columnCount = dataColumnCount;
if (dataColumnCount < 2) {
isDataValid = false;
}
else {
for (let i = 0; i < dataRowCount; i++) {
if (data[i].length !== dataColumnCount) {
isDataValid = false;
break;
}
}
}
}
return { isDataValid, columnCount, rowCount };
};
const createRowKeyOrderMapfromData = (data) => {
const rowKeys = getRowKeysFromData(data);
const sortedRowKeys = rowKeys.sort((a, b) => a - b);
const minRowKey = sortedRowKeys[0];
const rowKeyOrderMap = new Map();
sortedRowKeys.forEach((key, index) => {
rowKeyOrderMap.set(key, index);
});
return { rowKeyOrderMap, minRowKey };
};
const createColumnKeyOrderMapfromData = (data) => {
const columnKeys = getColumnKeysFromData(data);
const sortedColumnKeys = columnKeys.sort((a, b) => a - b);
const minColumnKey = sortedColumnKeys[0];
const columnKeyOrderMap = new Map();
sortedColumnKeys.forEach((key, index) => {
columnKeyOrderMap.set(key, index);
});
return { columnKeyOrderMap, minColumnKey };
};
const getInBetweenPixelIndicesfromCoords = (previousCoord, currentCoord, gridSquareLength, data) => {
if (!previousCoord || !currentCoord)
return [];
if (Math.abs(currentCoord.x - previousCoord.x) >= gridSquareLength ||
Math.abs(currentCoord.y - previousCoord.y) >= gridSquareLength) {
const { rowIndices, columnIndices } = getAllGridIndicesSorted(data);
const pixelIndex = getPixelIndexFromMouseCartCoord(currentCoord, rowIndices, columnIndices, gridSquareLength);
const previousIndex = getPixelIndexFromMouseCartCoord(previousCoord, rowIndices, columnIndices, gridSquareLength);
if (!previousIndex || !pixelIndex)
return;
if (Math.abs(pixelIndex.columnIndex - previousIndex.columnIndex) >= 1 ||
Math.abs(pixelIndex.rowIndex - previousIndex.rowIndex) >= 1) {
const missingIndices = getBresenhamLineIndices(previousIndex.rowIndex, previousIndex.columnIndex, pixelIndex.rowIndex, pixelIndex.columnIndex);
if (missingIndices.length > 0) {
return missingIndices;
}
}
}
};
const getRectanglePixelIndicesfromCoords = (previousCoord, currentCoord, gridSquareLength, data, filled) => {
if (!previousCoord || !currentCoord)
return [];
if (Math.abs(currentCoord.x - previousCoord.x) >= gridSquareLength ||
Math.abs(currentCoord.y - previousCoord.y) >= gridSquareLength) {
const { rowIndices, columnIndices } = getAllGridIndicesSorted(data);
const pixelIndex = getPixelIndexFromMouseCartCoord(currentCoord, rowIndices, columnIndices, gridSquareLength);
const previousIndex = getPixelIndexFromMouseCartCoord(previousCoord, rowIndices, columnIndices, gridSquareLength);
if (!previousIndex || !pixelIndex)
return;
const points = [];
if (Math.abs(pixelIndex.columnIndex - previousIndex.columnIndex) >= 1 ||
Math.abs(pixelIndex.rowIndex - previousIndex.rowIndex) >= 1) {
const [startRow, endRow] = previousIndex.rowIndex < pixelIndex.rowIndex
? [previousIndex.rowIndex, pixelIndex.rowIndex]
: [pixelIndex.rowIndex, previousIndex.rowIndex];
const [startColumn, endColumn] = previousIndex.columnIndex < pixelIndex.columnIndex
? [previousIndex.columnIndex, pixelIndex.columnIndex]
: [pixelIndex.columnIndex, previousIndex.columnIndex];
for (let row = startRow; row <= endRow; row++) {
for (let column = startColumn; column <= endColumn; column++) {
if (filled ||
row === previousIndex.rowIndex || column === previousIndex.columnIndex ||
row === pixelIndex.rowIndex || column === pixelIndex.columnIndex) {
points.push({
rowIndex: row,
columnIndex: column,
});
}
}
}
}
return points;
}
};
const getEllipsePixelIndicesfromCoords = (previousCoord, currentCoord, gridSquareLength, data, filled) => {
if (!previousCoord || !currentCoord)
return [];
if (Math.abs(currentCoord.x - previousCoord.x) >= gridSquareLength ||
Math.abs(currentCoord.y - previousCoord.y) >= gridSquareLength) {
const { rowIndices, columnIndices } = getAllGridIndicesSorted(data);
const pixelIndex = getPixelIndexFromMouseCartCoord(currentCoord, rowIndices, columnIndices, gridSquareLength);
const previousIndex = getPixelIndexFromMouseCartCoord(previousCoord, rowIndices, columnIndices, gridSquareLength);
if (!previousIndex || !pixelIndex)
return;
if (Math.abs(pixelIndex.columnIndex - previousIndex.columnIndex) >= 1 ||
Math.abs(pixelIndex.rowIndex - previousIndex.rowIndex) >= 1) {
const indices = getBresenhamEllipseIndices(previousIndex.columnIndex, previousIndex.rowIndex, pixelIndex.columnIndex, pixelIndex.rowIndex, filled);
if (indices.length > 0) {
return indices;
}
}
}
};
const validateLayers = (layers) => {
if (!layers) {
throw new Error("No layer provided");
}
if (layers.length === 0) {
throw new Error("initLayers should not be empty. Please provide at least one layer.");
}
const layerIdSet = new Set();
let measuredColumnCount = null;
let measuredRowCount = null;
let measuredTopRowIndex = null;
let measuredLeftColumnIndex = null;
// all init data passed initially, should be
// 1) a square array
// 2) all data should have same row and column count
// 3) all data should have same topRowIndex and leftColumnIndex
layers.forEach(layer => {
if (layerIdSet.has(layer.id)) {
throw new DuplicateLayerIdError(layer.id);
}
layerIdSet.add(layer.id);
const { isDataValid, columnCount, rowCount } = validateSquareArray(layer.data);
if (measuredColumnCount !== null && measuredRowCount !== null) {
if (measuredColumnCount !== columnCount ||
measuredRowCount !== rowCount) {
throw new InvalidDataDimensionsError(layer.id);
}
}
else {
measuredColumnCount = columnCount;
measuredRowCount = rowCount;
}
if (!isDataValid) {
throw new InvalidSquareDataError(layer.id);
}
if (measuredLeftColumnIndex == null || measuredTopRowIndex == null) {
measuredLeftColumnIndex = layer.data[0][0].columnIndex;
measuredTopRowIndex = layer.data[0][0].rowIndex;
}
const topRowIndex = layer.data[0][0].rowIndex;
const leftColumnIndex = layer.data[0][0].columnIndex;
if (topRowIndex !== measuredTopRowIndex) {
throw new InvalidDataIndicesError(layer.id);
}
if (leftColumnIndex !== measuredLeftColumnIndex) {
throw new InvalidDataIndicesError(layer.id);
}
});
return true;
};
class BaseLayer {
constructor({ canvas }) {
this.panZoom = DefaultPanZoom;
// TODO: We needed a key value sorted map!
this.rowKeyOrderMap = new Map();
this.columnKeyOrderMap = new Map();
this.topRowIndex = 0;
this.leftColumnIndex = 0;
this.ctx = canvas.getContext("2d");
this.element = canvas;
}
getContext() {
return this.ctx;
}
getElement() {
return this.element;
}
getRowKeyOrderMap() {
return this.rowKeyOrderMap;
}
getColumnKeyOrderMap() {
return this.columnKeyOrderMap;
}
setPanZoom(panZoom) {
this.panZoom = panZoom;
}
setCriterionDataForRendering(criterionDataForRendering) {
this.criterionDataForRendering = criterionDataForRendering;
const { rowKeyOrderMap, minRowKey } = createRowKeyOrderMapfromData(criterionDataForRendering);
const { columnKeyOrderMap, minColumnKey } = createColumnKeyOrderMapfromData(criterionDataForRendering);
this.rowKeyOrderMap = rowKeyOrderMap;
this.columnKeyOrderMap = columnKeyOrderMap;
this.topRowIndex = minRowKey;
this.leftColumnIndex = minColumnKey;
}
scale(x, y) {
this.ctx.scale(x, y);
}
setWidth(width, devicePixelRatio) {
this.width = width;
this.element.width = devicePixelRatio ? width * devicePixelRatio : width;
this.element.style.width = `${width}px`;
}
setHeight(height, devicePixelRatio) {
this.height = height;
this.element.height = devicePixelRatio ? height * devicePixelRatio : height;
this.element.style.height = `${height}px`;
}
setDpr(dpr) {
this.dpr = dpr;
}
getWidth() {
return this.width;
}
getHeight() {
return this.height;
}
setSize(width, height, devicePixelRatio) {
this.setWidth(width, devicePixelRatio);
this.setHeight(height, devicePixelRatio);
this.dpr = devicePixelRatio ? devicePixelRatio : this.dpr;
}
setTopRowIndex(topRowIndex) {
this.topRowIndex = topRowIndex;
}
setLeftColumnIndex(leftColumnIndex) {
this.leftColumnIndex = leftColumnIndex;
}
getTopRowIndex() {
return this.topRowIndex;
}
getLeftColumnIndex() {
return this.leftColumnIndex;
}
}
class Observable {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class DottingDataLayer extends Observable {
constructor({ data, id, }) {
super();
this.isVisible = true;
this.getColumnKeysFromData = () => {
const allRowKeys = Array.from(this.data.keys());
const allColumnKeys = Array.from(this.data.get(allRowKeys[0]).keys());
return allColumnKeys;
};
this.addRowToData = (rowIndex) => {
const columnKeys = this.columnKeys;
if (this.data.has(rowIndex)) {
return null;
}
this.data.set(rowIndex, new Map());
this.rowKeys.add(rowIndex);
for (const i of columnKeys) {
this.data.get(rowIndex).set(i, { color: "" });
}
this.notify(this.getData());
return rowIndex;
};
this.addColumnToData = (columnIndex) => {
let validColumnIndex = null;
this.data.forEach(row => {
if (!row.has(columnIndex)) {
validColumnIndex = columnIndex;
row.set(columnIndex, { color: "" });
}
});
this.notify(this.getData());
this.columnKeys.add(columnIndex);
return validColumnIndex;
};
this.getCopiedData = () => {
const copiedData = new Map();
this.data.forEach((row, rowIndex) => {
copiedData.set(rowIndex, new Map());
row.forEach((column, columnIndex) => {
copiedData.get(rowIndex).set(columnIndex, Object.assign({}, column));
});
});
return copiedData;
};
const { isDataValid } = validateSquareArray(data);
const leftColumnIndex = data[0][0].columnIndex;
const topRowIndex = data[0][0].rowIndex;
if (!isDataValid) {
throw new Error("Data is not valid");
}
this.data = new Map();
this.id = id;
for (let i = 0; i < data.length; i++) {
this.data.set(topRowIndex + i, new Map());
for (let j = 0; j < data[i].length; j++) {
this.data
.get(topRowIndex + i)
.set(leftColumnIndex + j, { color: data[i][j].color });
}
}
this.rowKeys = new Set(this.data.keys());
this.columnKeys = new Set(this.data.get(topRowIndex).keys());
}
getDataInfo() {
const gridIndices = getGridIndicesFromData(this.data);
const columnCount = getColumnCountFromData(this.data);
const rowCount = getRowCountFromData(this.data);
return {
gridIndices,
columnCount,
rowCount,
};
}
getId() {
return this.id;
}
deleteRowOfData(rowIndex) {
let validRowIndex = null;
if (!this.data.has(rowIndex)) {
throw new Error("Row does not exist");
}
validRowIndex = rowIndex;
this.data.delete(rowIndex);
this.rowKeys.delete(rowIndex);
this.notify(this.getData());
return validRowIndex;
}
deleteColumnOfData(columnIndex) {
let validColumnIndex = null;
this.data.forEach(row => {
if (!row.has(columnIndex)) {
throw new Error("Column does not exist");
}
validColumnIndex = columnIndex;
row.delete(columnIndex);
});
this.columnKeys.delete(columnIndex);
this.notify(this.getData());
return validColumnIndex;
}
clearData() {
const previousPixels = [];
const newPixels = [];
const rowKeys = Array.from(this.data.keys());
const columnKeys = Array.from(this.data.get(rowKeys[0]).keys());
for (const i of rowKeys) {
for (const j of columnKeys) {
previousPixels.push({
rowIndex: i,
columnIndex: j,
color: this.data.get(i).get(j).color,
});
newPixels.push({
rowIndex: i,
columnIndex: j,
color: "",
});
this.data.get(i).set(j, { color: "" });
}
}
this.notify(this.getData());
return { previousPixels, newPixels };
}
setData(data) {
this.data = data;
this.rowKeys = new Set(data.keys());
this.columnKeys = new Set(data.get(Array.from(data.keys())[0]).keys());
this.notify(this.getData());
}
setIsVisible(isVisible) {
this.isVisible = isVisible;
}
getData() {
return this.data;
}
getIsVisible() {
return this.isVisible;
}
getDataArray() {
const data = [];
[...this.data.entries()].forEach(([rowIndex, row]) => {
const rowData = [];
[...row.entries()].forEach(([columnIndex, column]) => {
rowData.push({
rowIndex,
columnIndex,
color: column.color,
});
});
data.push(rowData);
});
return data;
}
}
class DataLayer extends BaseLayer {
constructor({ canvas, layers, }) {
super({ canvas });
this.gridSquareLength = DefaultGridSquareLength;
this.defaultPixelColor = DefaultPixelColor;
this.capturedImageBitmap = null;
this.capturedImageBitmapScale = 1;
this.offscreenCanvas = null;
// const response = DataLayerWorkerString;
// const blob = new Blob([response], { type: "application/javascript" });
// this.setWorker(new Worker(URL.createObjectURL(blob)));
if (layers) {
const topRowIndex = layers[0].data[0][0].rowIndex;
const leftColumnIndex = layers[0].data[0][0].columnIndex;
this.setTopRowIndex(topRowIndex);
this.setLeftColumnIndex(leftColumnIndex);
this.layers = layers.map(layer => new DottingDataLayer({
data: layer.data,
id: layer.id,
}));
}
else {
const topRowIndex = 0;
const leftColumnIndex = 0;
this.setTopRowIndex(topRowIndex);
this.setLeftColumnIndex(leftColumnIndex);
const defaultNestedArray = [];
const { rowCount, columnCount } = DefaultPixelDataDimensions;
this.offscreenCanvas = new OffscreenCanvas(columnCount * this.gridSquareLength, rowCount * this.gridSquareLength);
for (let i = 0; i < rowCount; i++) {
defaultNestedArray.push([]);
for (let j = 0; j < columnCount; j++) {
defaultNestedArray[i].push({
color: "",
rowIndex: i,
columnIndex: j,
});
}
}
this.layers = [
new DottingDataLayer({
data: defaultNestedArray,
id: "layer1",
}),
];
}
this.currentLayer = this.layers[0];
}
getColumnCount() {
return getColumnCountFromData(this.getData());
}
getRowCount() {
return getRowCountFromData(this.getData());
}
getGridIndices() {
return getGridIndicesFromData(this.getData());
}
getDimensions() {
return {
columnCount: this.getColumnCount(),
rowCount: this.getRowCount(),
};
}
getData() {
return this.currentLayer.getData();
}
getLayer(layerId) {
const layer = this.layers.find(layer => layer.getId() === layerId);
if (layer) {
return layer;
}
else {
return null;
}
}
getLayerIndex(layerId) {
return this.layers.findIndex(layer => layer.getId() === layerId);
}
getLayers() {
return this.layers;
}
createLayer(layerId, data) {
const layer = this.getLayer(layerId);
if (layer) {
throw new Error("Layer already exists");
}
else {
const currentLayerInfo = this.currentLayer.getDataInfo();
if (data) {
const { isDataValid, rowCount, columnCount } = validateSquareArray(data);
const leftColumnIndex = data[0][0].columnIndex;
const topRowIndex = data[0][0].rowIndex;
if (!isDataValid) {
throw new Error("Data is not square");
}
if (leftColumnIndex !== currentLayerInfo.gridIndices.leftColumnIndex ||
topRowIndex !== currentLayerInfo.gridIndices.topRowIndex) {
throw new Error("Data grid indice differs from current layer");
}
if (rowCount !== currentLayerInfo.rowCount ||
columnCount !== currentLayerInfo.columnCount) {
throw new Error("Data dimensions differs from current layer");
}
return new DottingDataLayer({
data,
id: layerId,
});
}
else {
const emptyArray = [];
for (let i = 0; i < currentLayerInfo.rowCount; i++) {
emptyArray.push([]);
for (let j = 0; j < currentLayerInfo.columnCount; j++) {
emptyArray[i].push({
rowIndex: currentLayerInfo.gridIndices.topRowIndex + i,
columnIndex: currentLayerInfo.gridIndices.leftColumnIndex + j,
color: "",
});
}
}
return new DottingDataLayer({
data: emptyArray,
id: layerId,
});
}
}
}
getCopiedData() {
const copiedMap = new Map();
Array.from(this.getData().entries()).forEach(([rowIndex, row]) => {
const copiedRow = new Map();
Array.from(row.entries()).forEach(([columnIndex, pixelData]) => {
copiedRow.set(columnIndex, Object.assign({}, pixelData));
});
copiedMap.set(rowIndex, copiedRow);
});
return copiedMap;
}
getCurrentLayer() {
return this.currentLayer;
}
hideLayer(layerId) {
const layer = this.getLayer(layerId);
if (!layer) {
throw new Error("Layer not found");
}
layer.setIsVisible(false);
}
showLayer(layerId) {
const layer = this.getLayer(layerId);
if (!layer) {
throw new Error("Layer not found");
}
layer.setIsVisible(true);
}
isolateLayer(layerId) {