421 lines
15 KiB
TypeScript
421 lines
15 KiB
TypeScript
|
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||
|
|
// @ts-nocheck
|
||
|
|
|
||
|
|
import {
|
||
|
|
buildSpritePreviewDataUrl,
|
||
|
|
getSpritePalette,
|
||
|
|
normalizeImagePlayback,
|
||
|
|
} from "../editorCore";
|
||
|
|
import { normalizeEditorTags } from "./tagUtils";
|
||
|
|
|
||
|
|
export const TILE_ART_SIZE = 16;
|
||
|
|
|
||
|
|
export const EYEDROPPER_CURSOR = `url("data:image/svg+xml,${encodeURIComponent(
|
||
|
|
`<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||
|
|
<path d="M20.8 4.2c1.6-1.6 4.3-1.6 5.9 0s1.6 4.3 0 5.9l-3.1 3.1-5.9-5.9 3.1-3.1z" fill="#eef6ff" stroke="#08111d" stroke-width="1.5"/>
|
||
|
|
<path d="M10.8 14.1l6.9-6.9 6.1 6.1-6.9 6.9-2.7.9-.9 2.7-3 3a2.2 2.2 0 0 1-3.1 0l-1-1a2.2 2.2 0 0 1 0-3.1l3-3 2.7-.9.9-2.7z" fill="#7ee8c6" stroke="#08111d" stroke-width="1.5" stroke-linejoin="round"/>
|
||
|
|
<circle cx="8.4" cy="23.6" r="2" fill="#ff5f6d"/>
|
||
|
|
</svg>`,
|
||
|
|
)}") 4 28, crosshair`;
|
||
|
|
|
||
|
|
export function cloneValue(value) {
|
||
|
|
if (typeof structuredClone === "function") {
|
||
|
|
return structuredClone(value);
|
||
|
|
}
|
||
|
|
return value == null ? value : JSON.parse(JSON.stringify(value));
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeRoleList(value) {
|
||
|
|
if (!Array.isArray(value)) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
return Array.from(new Set(
|
||
|
|
value
|
||
|
|
.map((entry) => String(entry || "").trim().toLowerCase())
|
||
|
|
.filter((entry) => entry === "tile" || entry === "sprite"),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function normalizeTimelineRows(rows) {
|
||
|
|
return Array.from({ length: TILE_ART_SIZE }, (_entry, rowIndex) => {
|
||
|
|
const row = Array.isArray(rows) ? String(rows[rowIndex] || "") : "";
|
||
|
|
return row.padEnd(TILE_ART_SIZE, ".").slice(0, TILE_ART_SIZE);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function normalizeWorkingFrames(record) {
|
||
|
|
const rawFrames = Array.isArray(record?.frames) ? record.frames.filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry)) : [];
|
||
|
|
const normalizedFrames = rawFrames.map((entry, index) => ({
|
||
|
|
...cloneValue(entry),
|
||
|
|
id: String(entry.id || `frame_${index}`).trim() || `frame_${index}`,
|
||
|
|
enabled: entry.enabled !== false,
|
||
|
|
index: Number.isFinite(Number(entry.index)) ? Math.max(0, Math.floor(Number(entry.index))) : index,
|
||
|
|
rows: normalizeTimelineRows(entry.rows),
|
||
|
|
}));
|
||
|
|
if (normalizedFrames.length > 0) {
|
||
|
|
return normalizedFrames;
|
||
|
|
}
|
||
|
|
return [{
|
||
|
|
id: "frame_0",
|
||
|
|
enabled: true,
|
||
|
|
index: 0,
|
||
|
|
rows: normalizeTimelineRows(record?.rows),
|
||
|
|
}];
|
||
|
|
}
|
||
|
|
|
||
|
|
export function sortWorkingFrames(frames) {
|
||
|
|
return frames
|
||
|
|
.map((frame, sourceIndex) => ({
|
||
|
|
frame,
|
||
|
|
sourceIndex,
|
||
|
|
sortIndex: Number.isFinite(Number(frame?.index)) ? Number(frame.index) : sourceIndex,
|
||
|
|
}))
|
||
|
|
.sort((left, right) => (
|
||
|
|
left.sortIndex !== right.sortIndex
|
||
|
|
? left.sortIndex - right.sortIndex
|
||
|
|
: left.sourceIndex - right.sourceIndex
|
||
|
|
))
|
||
|
|
.map((entry) => entry.frame);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function normalizeWorkingGraphicRecord(recordType, record) {
|
||
|
|
const source = cloneValue(record) || {};
|
||
|
|
const roles = normalizeRoleList(source.roles);
|
||
|
|
const nextRoles = recordType === "tile"
|
||
|
|
? Array.from(new Set([...roles, "tile"]))
|
||
|
|
: (
|
||
|
|
recordType === "sprite"
|
||
|
|
? Array.from(new Set([...roles, "sprite"]))
|
||
|
|
: roles.filter((entry) => entry !== "sprite")
|
||
|
|
);
|
||
|
|
const frames = normalizeWorkingFrames(source).map((frame, index) => ({
|
||
|
|
...frame,
|
||
|
|
index,
|
||
|
|
}));
|
||
|
|
const requestedDefaultFrameId = String(source.defaultFrame || "").trim();
|
||
|
|
const defaultFrameId = String(
|
||
|
|
frames.find((frame) => String(frame.id || "").trim() === requestedDefaultFrameId)?.id
|
||
|
|
|| frames[0]?.id
|
||
|
|
|| "frame_0",
|
||
|
|
).trim() || "frame_0";
|
||
|
|
const workingRows = normalizeTimelineRows(
|
||
|
|
Array.isArray(source.rows) && source.rows.length > 0
|
||
|
|
? source.rows
|
||
|
|
: (frames.find((frame) => String(frame.id || "").trim() === defaultFrameId)?.rows || frames[0]?.rows || [])
|
||
|
|
);
|
||
|
|
return {
|
||
|
|
...source,
|
||
|
|
id: String(source.id || `${recordType === "tile" ? "tile" : "sprite"}_${Date.now()}`).trim(),
|
||
|
|
name: typeof source.name === "string" ? source.name : "",
|
||
|
|
description: typeof source.description === "string" ? source.description : "",
|
||
|
|
width: TILE_ART_SIZE,
|
||
|
|
height: TILE_ART_SIZE,
|
||
|
|
pixelScale: Math.max(1, Number(source.pixelScale) || 2),
|
||
|
|
opacity: Number.isFinite(Number(source.opacity)) ? Math.max(0, Math.min(1, Number(source.opacity))) : 1,
|
||
|
|
tags: normalizeEditorTags(source.tags),
|
||
|
|
roles: nextRoles,
|
||
|
|
tileSymbol: nextRoles.includes("tile")
|
||
|
|
? (String(source.tileSymbol ?? source.symbol ?? source.id ?? "T").trim().charAt(0) || "T")
|
||
|
|
: "",
|
||
|
|
defaultFrame: defaultFrameId,
|
||
|
|
speed: Number.isFinite(Number(source.speed)) && Number(source.speed) >= 0 ? Number(source.speed) : 0,
|
||
|
|
playback: normalizeImagePlayback(source.playback),
|
||
|
|
frames,
|
||
|
|
rows: workingRows,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function normalizeOpacityValue(value, fallback = 1) {
|
||
|
|
const parsed = Number(value);
|
||
|
|
if (!Number.isFinite(parsed)) {
|
||
|
|
return fallback;
|
||
|
|
}
|
||
|
|
return Math.max(0, Math.min(1, parsed));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function formatOpacityValue(value) {
|
||
|
|
const normalized = normalizeOpacityValue(value, 1);
|
||
|
|
return normalized.toFixed(2).replace(/\.?0+$/, "");
|
||
|
|
}
|
||
|
|
|
||
|
|
export function cloneRows(rows) {
|
||
|
|
return Array.isArray(rows)
|
||
|
|
? rows.map((row) => String(row || "").padEnd(TILE_ART_SIZE, ".").slice(0, TILE_ART_SIZE))
|
||
|
|
: Array.from({ length: TILE_ART_SIZE }, () => ".".repeat(TILE_ART_SIZE));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildRowsPreviewRecord(rows) {
|
||
|
|
return {
|
||
|
|
width: TILE_ART_SIZE,
|
||
|
|
height: TILE_ART_SIZE,
|
||
|
|
rows: cloneRows(rows),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function formatPlaybackLabel(value) {
|
||
|
|
const normalized = normalizeImagePlayback(value);
|
||
|
|
if (normalized === "rewind") {
|
||
|
|
return "Rewind";
|
||
|
|
}
|
||
|
|
if (normalized === "stop") {
|
||
|
|
return "Stop";
|
||
|
|
}
|
||
|
|
return "Normal";
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getWorkingCellSymbol(record, x, y) {
|
||
|
|
const rows = Array.isArray(record?.rows) ? record.rows : [];
|
||
|
|
const row = String(rows[y] || "").padEnd(TILE_ART_SIZE, ".").slice(0, TILE_ART_SIZE);
|
||
|
|
return String(row.charAt(x) || ".").charAt(0) || ".";
|
||
|
|
}
|
||
|
|
|
||
|
|
export function paintWorkingRowsCell(rows, x, y, symbol) {
|
||
|
|
const nextRows = cloneRows(rows);
|
||
|
|
const nextSymbol = String(symbol || ".").charAt(0) || ".";
|
||
|
|
const targetRow = String(nextRows[y] || "").padEnd(TILE_ART_SIZE, ".").slice(0, TILE_ART_SIZE);
|
||
|
|
nextRows[y] = `${targetRow.slice(0, x)}${nextSymbol}${targetRow.slice(x + 1)}`;
|
||
|
|
return nextRows;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getRowsMatrix(rows) {
|
||
|
|
return cloneRows(rows).map((row) => Array.from(row));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildRowsFromMatrix(matrix) {
|
||
|
|
return Array.from({ length: TILE_ART_SIZE }, (_entry, rowIndex) => {
|
||
|
|
const sourceRow = Array.isArray(matrix?.[rowIndex]) ? matrix[rowIndex] : [];
|
||
|
|
return sourceRow.join("").padEnd(TILE_ART_SIZE, ".").slice(0, TILE_ART_SIZE);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getAlternatePaintSymbol(record, preferredSymbol) {
|
||
|
|
const normalizedPreferred = String(preferredSymbol || "").charAt(0) || ".";
|
||
|
|
const palette = getSpritePalette(record || undefined);
|
||
|
|
const nextSymbol = Object.keys(palette)
|
||
|
|
.map((symbol) => String(symbol || "").charAt(0))
|
||
|
|
.find((symbol) => symbol && symbol !== normalizedPreferred && symbol !== ".");
|
||
|
|
return nextSymbol || ".";
|
||
|
|
}
|
||
|
|
|
||
|
|
export function shiftRows(rows, offsetX, offsetY) {
|
||
|
|
const nextRows = Array.from({ length: TILE_ART_SIZE }, () => ".".repeat(TILE_ART_SIZE).split(""));
|
||
|
|
const sourceRows = cloneRows(rows);
|
||
|
|
for (let y = 0; y < TILE_ART_SIZE; y += 1) {
|
||
|
|
const row = sourceRows[y] || ".".repeat(TILE_ART_SIZE);
|
||
|
|
for (let x = 0; x < TILE_ART_SIZE; x += 1) {
|
||
|
|
const nextX = x + offsetX;
|
||
|
|
const nextY = y + offsetY;
|
||
|
|
if (nextX < 0 || nextX >= TILE_ART_SIZE || nextY < 0 || nextY >= TILE_ART_SIZE) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
nextRows[nextY][nextX] = String(row.charAt(x) || ".").charAt(0) || ".";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return buildRowsFromMatrix(nextRows);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function flipRowsHorizontally(rows) {
|
||
|
|
return cloneRows(rows).map((row) => row.split("").reverse().join(""));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function flipRowsVertically(rows) {
|
||
|
|
return cloneRows(rows).slice().reverse();
|
||
|
|
}
|
||
|
|
|
||
|
|
export function rotateRowsClockwise(rows) {
|
||
|
|
const matrix = getRowsMatrix(rows);
|
||
|
|
const nextMatrix = Array.from({ length: TILE_ART_SIZE }, () => Array.from({ length: TILE_ART_SIZE }, () => "."));
|
||
|
|
for (let y = 0; y < TILE_ART_SIZE; y += 1) {
|
||
|
|
for (let x = 0; x < TILE_ART_SIZE; x += 1) {
|
||
|
|
nextMatrix[x][TILE_ART_SIZE - 1 - y] = String(matrix[y]?.[x] || ".").charAt(0) || ".";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return buildRowsFromMatrix(nextMatrix);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function rotateRowsCounterClockwise(rows) {
|
||
|
|
const matrix = getRowsMatrix(rows);
|
||
|
|
const nextMatrix = Array.from({ length: TILE_ART_SIZE }, () => Array.from({ length: TILE_ART_SIZE }, () => "."));
|
||
|
|
for (let y = 0; y < TILE_ART_SIZE; y += 1) {
|
||
|
|
for (let x = 0; x < TILE_ART_SIZE; x += 1) {
|
||
|
|
nextMatrix[TILE_ART_SIZE - 1 - x][y] = String(matrix[y]?.[x] || ".").charAt(0) || ".";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return buildRowsFromMatrix(nextMatrix);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildShapeFillMask(shapeKind, startX, startY, endX, endY) {
|
||
|
|
const minX = Math.max(0, Math.min(startX, endX));
|
||
|
|
const maxX = Math.min(TILE_ART_SIZE - 1, Math.max(startX, endX));
|
||
|
|
const minY = Math.max(0, Math.min(startY, endY));
|
||
|
|
const maxY = Math.min(TILE_ART_SIZE - 1, Math.max(startY, endY));
|
||
|
|
const fillMask = new Set();
|
||
|
|
const shape = shapeKind === "circle" || shapeKind === "triangle" ? shapeKind : "rectangle";
|
||
|
|
const width = Math.max(1, (maxX - minX) + 1);
|
||
|
|
const height = Math.max(1, (maxY - minY) + 1);
|
||
|
|
const centerX = minX + (width / 2);
|
||
|
|
const centerY = minY + (height / 2);
|
||
|
|
const denomX = Math.max(0.5, width / 2);
|
||
|
|
const denomY = Math.max(0.5, height / 2);
|
||
|
|
const triangleAx = minX + (width - 1) / 2;
|
||
|
|
const triangleAy = minY;
|
||
|
|
const triangleBx = minX;
|
||
|
|
const triangleBy = maxY;
|
||
|
|
const triangleCx = maxX;
|
||
|
|
const triangleCy = maxY;
|
||
|
|
const triangleDenominator = ((triangleBy - triangleCy) * (triangleAx - triangleCx)) + ((triangleCx - triangleBx) * (triangleAy - triangleCy));
|
||
|
|
for (let y = minY; y <= maxY; y += 1) {
|
||
|
|
for (let x = minX; x <= maxX; x += 1) {
|
||
|
|
let include;
|
||
|
|
const sampleX = x + 0.5;
|
||
|
|
const sampleY = y + 0.5;
|
||
|
|
if (shape === "rectangle") {
|
||
|
|
include = true;
|
||
|
|
} else if (shape === "circle") {
|
||
|
|
const normX = (sampleX - centerX) / denomX;
|
||
|
|
const normY = (sampleY - centerY) / denomY;
|
||
|
|
include = (normX * normX) + (normY * normY) <= 1;
|
||
|
|
} else if (triangleDenominator !== 0) {
|
||
|
|
const a = (((triangleBy - triangleCy) * (sampleX - triangleCx)) + ((triangleCx - triangleBx) * (sampleY - triangleCy))) / triangleDenominator;
|
||
|
|
const b = (((triangleCy - triangleAy) * (sampleX - triangleCx)) + ((triangleAx - triangleCx) * (sampleY - triangleCy))) / triangleDenominator;
|
||
|
|
const c = 1 - a - b;
|
||
|
|
include = a >= 0 && b >= 0 && c >= 0;
|
||
|
|
} else {
|
||
|
|
include = x === Math.round(triangleAx) && y >= minY && y <= maxY;
|
||
|
|
}
|
||
|
|
if (include === true) {
|
||
|
|
fillMask.add(`${x}:${y}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return fillMask;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildOutlineMask(fillMask) {
|
||
|
|
const outlineMask = new Set();
|
||
|
|
fillMask.forEach((key) => {
|
||
|
|
const [xText, yText] = String(key || "").split(":");
|
||
|
|
const x = Number(xText);
|
||
|
|
const y = Number(yText);
|
||
|
|
const neighbors = [
|
||
|
|
`${x - 1}:${y}`,
|
||
|
|
`${x + 1}:${y}`,
|
||
|
|
`${x}:${y - 1}`,
|
||
|
|
`${x}:${y + 1}`,
|
||
|
|
];
|
||
|
|
if (neighbors.some((neighbor) => !fillMask.has(neighbor))) {
|
||
|
|
outlineMask.add(key);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return outlineMask;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function applyMaskToRows(baseRows, mask, symbol) {
|
||
|
|
const matrix = getRowsMatrix(baseRows);
|
||
|
|
mask.forEach((key) => {
|
||
|
|
const [xText, yText] = String(key || "").split(":");
|
||
|
|
const x = Number(xText);
|
||
|
|
const y = Number(yText);
|
||
|
|
if (x < 0 || x >= TILE_ART_SIZE || y < 0 || y >= TILE_ART_SIZE) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
matrix[y][x] = String(symbol || ".").charAt(0) || ".";
|
||
|
|
});
|
||
|
|
return buildRowsFromMatrix(matrix);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getLineRows(baseRows, startX, startY, endX, endY, symbol) {
|
||
|
|
const normalizedSymbol = String(symbol || ".").charAt(0) || ".";
|
||
|
|
const matrix = getRowsMatrix(baseRows);
|
||
|
|
let x0 = Math.max(0, Math.min(TILE_ART_SIZE - 1, Number(startX) || 0));
|
||
|
|
let y0 = Math.max(0, Math.min(TILE_ART_SIZE - 1, Number(startY) || 0));
|
||
|
|
const x1 = Math.max(0, Math.min(TILE_ART_SIZE - 1, Number(endX) || 0));
|
||
|
|
const y1 = Math.max(0, Math.min(TILE_ART_SIZE - 1, Number(endY) || 0));
|
||
|
|
const deltaX = Math.abs(x1 - x0);
|
||
|
|
const deltaY = Math.abs(y1 - y0);
|
||
|
|
const stepX = x0 < x1 ? 1 : -1;
|
||
|
|
const stepY = y0 < y1 ? 1 : -1;
|
||
|
|
let error = deltaX - deltaY;
|
||
|
|
while (true) {
|
||
|
|
matrix[y0][x0] = normalizedSymbol;
|
||
|
|
if (x0 === x1 && y0 === y1) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
const nextError = error * 2;
|
||
|
|
if (nextError > -deltaY) {
|
||
|
|
error -= deltaY;
|
||
|
|
x0 += stepX;
|
||
|
|
}
|
||
|
|
if (nextError < deltaX) {
|
||
|
|
error += deltaX;
|
||
|
|
y0 += stepY;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return buildRowsFromMatrix(matrix);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildShapeOptionIconMarkup(shapeKind, variant, tone = "draw") {
|
||
|
|
const normalizedShape = shapeKind === "circle" || shapeKind === "triangle" ? shapeKind : "rectangle";
|
||
|
|
const normalizedVariant = variant === "outline" || variant === "two-tone" ? variant : "fill";
|
||
|
|
const normalizedTone = tone === "erase" ? "erase" : "draw";
|
||
|
|
return ""
|
||
|
|
+ `<span class="tile-art-menu-shape-icon is-${normalizedShape} is-${normalizedVariant} is-${normalizedTone}" aria-hidden="true">`
|
||
|
|
+ "<span class=\"tile-art-menu-shape-outline\"></span>"
|
||
|
|
+ "<span class=\"tile-art-menu-shape-fill\"></span>"
|
||
|
|
+ "</span>";
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildLineOptionIconMarkup(tone = "draw") {
|
||
|
|
const normalizedTone = tone === "erase" ? "erase" : "draw";
|
||
|
|
return ""
|
||
|
|
+ `<span class="tile-art-menu-line-icon is-${normalizedTone}" aria-hidden="true">`
|
||
|
|
+ "<span class=\"tile-art-menu-line-stroke\"></span>"
|
||
|
|
+ "</span>";
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildCurrentShapeToolIconMarkup(state) {
|
||
|
|
if (state?.activeTool === "line" || String(state?.activeShapeMenuId || "").trim() === "line") {
|
||
|
|
return buildLineOptionIconMarkup("draw");
|
||
|
|
}
|
||
|
|
return buildShapeOptionIconMarkup(
|
||
|
|
state?.activeShapeKind || "rectangle",
|
||
|
|
state?.activeShapeVariant || "outline",
|
||
|
|
"draw",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildCurrentEraseToolIconMarkup(state) {
|
||
|
|
return buildShapeOptionIconMarkup(
|
||
|
|
state?.activeEraseKind || "rectangle",
|
||
|
|
"fill",
|
||
|
|
"erase",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildTransformCategoryIconMarkup(kind) {
|
||
|
|
const normalizedKind = kind === "flip" ? "flip" : "rotate";
|
||
|
|
return ""
|
||
|
|
+ `<span class="tile-art-menu-transform-icon is-${normalizedKind}" aria-hidden="true">`
|
||
|
|
+ "<span class=\"tile-art-menu-transform-part part-a\"></span>"
|
||
|
|
+ "<span class=\"tile-art-menu-transform-part part-b\"></span>"
|
||
|
|
+ "</span>";
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildTransformOptionIconMarkup(kind) {
|
||
|
|
const normalizedKind = [
|
||
|
|
"rotate-cw",
|
||
|
|
"rotate-ccw",
|
||
|
|
"flip-h",
|
||
|
|
"flip-v",
|
||
|
|
].includes(String(kind || "").trim()) ? String(kind || "").trim() : "rotate-cw";
|
||
|
|
return ""
|
||
|
|
+ `<span class="tile-art-menu-transform-icon is-${normalizedKind}" aria-hidden="true">`
|
||
|
|
+ "<span class=\"tile-art-menu-transform-part part-a\"></span>"
|
||
|
|
+ "<span class=\"tile-art-menu-transform-part part-b\"></span>"
|
||
|
|
+ "</span>";
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildFramePreviewDataUrl(rows, scale = 10) {
|
||
|
|
return buildSpritePreviewDataUrl(buildRowsPreviewRecord(rows), scale);
|
||
|
|
}
|