Add shape detection to token outline creation
This commit is contained in:
parent
21986231fa
commit
3d42b62b74
@ -246,34 +246,68 @@ function MapToken({
|
||||
tokenName = tokenName + "-locked";
|
||||
}
|
||||
|
||||
let outline = tokenState.outline;
|
||||
if (Array.isArray(tokenState.outline)) {
|
||||
outline = [...outline]; // Copy array so we can edit it imutably
|
||||
for (let i = 0; i < outline.length; i += 2) {
|
||||
// Scale outline to the token
|
||||
outline[i] = (outline[i] / tokenState.width) * tokenWidth;
|
||||
outline[i + 1] = (outline[i + 1] / tokenState.height) * tokenHeight;
|
||||
function getScaledOutline() {
|
||||
let outline = tokenState.outline;
|
||||
if (outline.type === "rect") {
|
||||
return {
|
||||
...outline,
|
||||
x: (outline.x / tokenState.width) * tokenWidth,
|
||||
y: (outline.y / tokenState.height) * tokenHeight,
|
||||
width: (outline.width / tokenState.width) * tokenWidth,
|
||||
height: (outline.height / tokenState.height) * tokenHeight,
|
||||
};
|
||||
} else if (outline.type === "circle") {
|
||||
return {
|
||||
...outline,
|
||||
x: (outline.x / tokenState.width) * tokenWidth,
|
||||
y: (outline.y / tokenState.height) * tokenHeight,
|
||||
radius: (outline.radius / tokenState.width) * tokenWidth,
|
||||
};
|
||||
} else {
|
||||
let points = [...outline.points]; // Copy array so we can edit it imutably
|
||||
for (let i = 0; i < points.length; i += 2) {
|
||||
// Scale outline to the token
|
||||
points[i] = (points[i] / tokenState.width) * tokenWidth;
|
||||
points[i + 1] = (points[i + 1] / tokenState.height) * tokenHeight;
|
||||
}
|
||||
return { ...outline, points };
|
||||
}
|
||||
}
|
||||
|
||||
function renderOutline() {
|
||||
const outline = getScaledOutline();
|
||||
const sharedProps = {
|
||||
fill: colors.black,
|
||||
width: tokenWidth,
|
||||
height: tokenHeight,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: tokenState.rotation,
|
||||
offsetX: tokenWidth / 2,
|
||||
offsetY: tokenHeight / 2,
|
||||
opacity: 0.8,
|
||||
};
|
||||
if (outline === "rect") {
|
||||
return <Rect {...sharedProps} />;
|
||||
} else if (outline === "circle") {
|
||||
return <Circle {...sharedProps} offsetX={0} offsetY={0} />;
|
||||
if (outline.type === "rect") {
|
||||
return (
|
||||
<Rect
|
||||
width={outline.width}
|
||||
height={outline.height}
|
||||
x={outline.x}
|
||||
y={outline.y}
|
||||
{...sharedProps}
|
||||
/>
|
||||
);
|
||||
} else if (outline.type === "circle") {
|
||||
return (
|
||||
<Circle
|
||||
radius={outline.radius}
|
||||
x={outline.x}
|
||||
y={outline.y}
|
||||
{...sharedProps}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <Line {...sharedProps} points={outline} closed tension={0.33} />;
|
||||
return (
|
||||
<Line
|
||||
points={outline.points}
|
||||
closed
|
||||
tension={outline.points < 200 ? 0 : 0.33}
|
||||
{...sharedProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -299,7 +333,17 @@ function MapToken({
|
||||
id={tokenState.id}
|
||||
>
|
||||
{!tokenImage ? (
|
||||
renderOutline()
|
||||
<Group
|
||||
width={tokenWidth}
|
||||
height={tokenHeight}
|
||||
x={0}
|
||||
y={0}
|
||||
rotation={tokenState.rotation}
|
||||
offsetX={tokenWidth / 2}
|
||||
offsetY={tokenHeight / 2}
|
||||
>
|
||||
{renderOutline()}
|
||||
</Group>
|
||||
) : (
|
||||
<KonvaImage
|
||||
ref={imageRef}
|
||||
|
@ -508,6 +508,66 @@ class Vector2 {
|
||||
return { x: -p.y, y: p.x };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the centroid of the given points
|
||||
* @param {Vector2[]} points
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static centroid(points) {
|
||||
let center = { x: 0, y: 0 };
|
||||
for (let point of points) {
|
||||
center.x += point.x;
|
||||
center.y += point.y;
|
||||
}
|
||||
if (points.length > 0) {
|
||||
center = { x: center.x / points.length, y: center.y / points.length };
|
||||
}
|
||||
return center;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether given points are rectangular
|
||||
* @param {Vector2[]} points
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static rectangular(points) {
|
||||
if (points.length !== 4) {
|
||||
return false;
|
||||
}
|
||||
// Check whether distance to the center is the same for all four points
|
||||
const centroid = this.centroid(points);
|
||||
let prevDist;
|
||||
for (let point of points) {
|
||||
const dist = this.distance(point, centroid);
|
||||
if (prevDist && dist !== prevDist) {
|
||||
return false;
|
||||
} else {
|
||||
prevDist = dist;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether given points are circular
|
||||
* @param {Vector2[]} points
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static circular(points, threshold = 0.1) {
|
||||
const centroid = this.centroid(points);
|
||||
let distances = [];
|
||||
for (let point of points) {
|
||||
distances.push(this.distance(point, centroid));
|
||||
}
|
||||
if (distances.length > 0) {
|
||||
const maxDistance = Math.max(...distances);
|
||||
const minDistance = Math.min(...distances);
|
||||
return maxDistance - minDistance < threshold;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Vector2;
|
||||
|
@ -1,4 +1,7 @@
|
||||
import imageOutline from "image-outline";
|
||||
|
||||
import blobToBuffer from "./blobToBuffer";
|
||||
import Vector2 from "./Vector2";
|
||||
|
||||
const lightnessDetectionOffset = 0.1;
|
||||
|
||||
@ -152,3 +155,75 @@ export async function createThumbnail(image, type, size = 300, quality = 0.5) {
|
||||
mime: type,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef CircleOutline
|
||||
* @property {"circle"} type
|
||||
* @property {number} x - Center X of the circle
|
||||
* @property {number} y - Center Y of the circle
|
||||
* @property {number} radius
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef RectOutline
|
||||
* @property {"rect"} type
|
||||
* @property {number} width
|
||||
* @property {number} height
|
||||
* @property {number} x - Leftmost X position of the rect
|
||||
* @property {number} y - Topmost Y position of the rect
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef PathOutline
|
||||
* @property {"path"} type
|
||||
* @property {number[]} points - Alternating x, y coordinates zipped together
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {CircleOutline|RectOutline|PathOutline} Outline
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the outline of an image
|
||||
* @param {HTMLImageElement} image
|
||||
* @returns {Outline}
|
||||
*/
|
||||
export function getImageOutline(image, maxPoints = 100) {
|
||||
let baseOutline = imageOutline(image);
|
||||
|
||||
if (baseOutline) {
|
||||
if (baseOutline.length > maxPoints) {
|
||||
baseOutline = Vector2.resample(baseOutline, maxPoints);
|
||||
}
|
||||
const bounds = Vector2.getBoundingBox(baseOutline);
|
||||
if (Vector2.rectangular(baseOutline)) {
|
||||
return {
|
||||
type: "rect",
|
||||
x: Math.round(bounds.min.x),
|
||||
y: Math.round(bounds.min.y),
|
||||
width: Math.round(bounds.width),
|
||||
height: Math.round(bounds.height),
|
||||
};
|
||||
} else if (
|
||||
Vector2.circular(
|
||||
baseOutline,
|
||||
Math.max(bounds.width / 10, bounds.height / 10)
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: "circle",
|
||||
x: Math.round(bounds.center.x),
|
||||
y: Math.round(bounds.center.y),
|
||||
radius: Math.round(Math.min(bounds.width, bounds.height) / 2),
|
||||
};
|
||||
} else {
|
||||
// Flatten and round outline to save on storage size
|
||||
const points = baseOutline
|
||||
.map(({ x, y }) => [Math.round(x), Math.round(y)])
|
||||
.flat();
|
||||
return { type: "path", points };
|
||||
}
|
||||
} else {
|
||||
return { type: "rect", x: 0, y: 0, width: 1, height: 1 };
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
import Case from "case";
|
||||
import imageOutline from "image-outline";
|
||||
|
||||
import blobToBuffer from "./blobToBuffer";
|
||||
import { createThumbnail } from "./image";
|
||||
import Vector2 from "./Vector2";
|
||||
import { createThumbnail, getImageOutline } from "./image";
|
||||
|
||||
export function createTokenState(token, position, userId) {
|
||||
let tokenState = {
|
||||
@ -76,14 +74,7 @@ export async function createTokenFromFile(file, userId) {
|
||||
};
|
||||
assets.push(fileAsset);
|
||||
|
||||
let outline = imageOutline(image);
|
||||
if (outline.length > 100) {
|
||||
outline = Vector2.resample(outline, 100);
|
||||
}
|
||||
// Flatten and round outline to save on storage size
|
||||
outline = outline
|
||||
.map(({ x, y }) => [Math.round(x), Math.round(y)])
|
||||
.flat();
|
||||
const outline = getImageOutline(image);
|
||||
|
||||
const token = {
|
||||
name,
|
||||
|
@ -97,7 +97,7 @@ export function getDefaultTokens(userId) {
|
||||
hideInSidebar: false,
|
||||
width: 256,
|
||||
height: 256,
|
||||
outline: "circle",
|
||||
outline: { type: "circle", x: 128, y: 128, radius: 128 },
|
||||
owner: userId,
|
||||
created: tokenKeys.length - i,
|
||||
lastModified: Date.now(),
|
||||
|
@ -555,11 +555,8 @@ export const versions = {
|
||||
token.defaultCategory = token.category;
|
||||
delete token.category;
|
||||
token.defaultLabel = "";
|
||||
if (token.width === token.height) {
|
||||
token.outline = "circle";
|
||||
} else {
|
||||
token.outline = "rect";
|
||||
}
|
||||
// TODO: move to outline detection
|
||||
token.outline = { type: "circle", x: 256, y: 256, radius: 256 };
|
||||
delete token.lastUsed;
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user