Skip to content

Commit

Permalink
feat: Add new shape type "spiky_rectangle"
Browse files Browse the repository at this point in the history
  • Loading branch information
miyanokomiya committed Nov 28, 2024
1 parent 6634373 commit f549b4c
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 3 deletions.
2 changes: 2 additions & 0 deletions src/assets/icons/shape_spiky_rectangle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/composables/shapeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import iconCapsule from "../assets/icons/shape_capsule.svg";
import iconCylinder from "../assets/icons/shape_cylinder.svg";
import iconDocumentSymbol from "../assets/icons/shape_document_symbol.svg";
import iconStar from "../assets/icons/shape_star.svg";
import iconSpikyRectangle from "../assets/icons/shape_spiky_rectangle.svg";
import iconBubble from "../assets/icons/shape_bubble.svg";
import iconOneSidedArrow from "../assets/icons/shape_one_sided_arrow.svg";
import iconTwoSidedArrow from "../assets/icons/shape_two_sided_arrow.svg";
Expand Down Expand Up @@ -53,6 +54,7 @@ import { newArcSelectedState } from "./states/appCanvas/arc/arcSelectedState";
import { newDonutSelectedState } from "./states/appCanvas/donut/donutSelectedState";
import { newMoonSelectedState } from "./states/appCanvas/moon/moonSelectedState";
import { newGroupSelectedState } from "./states/appCanvas/group/groupSelectedState";
import { newSpikyRectangleSelectedState } from "./states/appCanvas/spikyRectangle/spikyRectangleSelectedState";

export type ShapeTypeItem = { type: string; icon: string };

Expand All @@ -69,6 +71,7 @@ export const shapeTypeList: ShapeTypeItem[] = [
{ type: "cylinder", icon: iconCylinder },
{ type: "document_symbol", icon: iconDocumentSymbol },
{ type: "star", icon: iconStar },
{ type: "spiky_rectangle", icon: iconSpikyRectangle },
{ type: "bubble", icon: iconBubble },
{ type: "one_sided_arrow", icon: iconOneSidedArrow },
{ type: "two_sided_arrow", icon: iconTwoSidedArrow },
Expand Down Expand Up @@ -120,6 +123,8 @@ export function getSingleShapeSelectedStateFn(type: string) {
return newHexagonSelectedState;
case "star":
return newStarSelectedState;
case "spiky_rectangle":
return newSpikyRectangleSelectedState;
case "document_symbol":
return newDocumentSymbolSelectedState;
case "wave":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import { movingShapeControlState } from "../movingShapeControlState";
import { getRectShapeRect, getShapeDetransform, getShapeTransform } from "../../../../shapes/rectPolygon";
import { applyAffine, clamp, IVec2 } from "okageo";
import { snapNumber } from "../../../../utils/geometry";
import {
getSpikeParameters,
getSpikyInnerRectangle,
SpikyRectangleShape,
} from "../../../../shapes/polygons/spikyRectangle";
import { COMMAND_EXAM_SRC } from "../commandExams";
import { defineSingleSelectedHandlerState } from "../singleSelectedHandlerState";
import { newSimplePolygonHandler, SimplePolygonHandler } from "../../../shapeHandlers/simplePolygonHandler";
import { StyleScheme } from "../../../../models";
import { applyLocalSpace, renderValueLabel } from "../../../../utils/renderer";
import { applyStrokeStyle } from "../../../../utils/strokeStyle";

export const newSpikyRectangleSelectedState = defineSingleSelectedHandlerState<
SpikyRectangleShape,
SimplePolygonHandler,
never
>(
(getters) => {
return {
getLabel: () => "RectangleSelected",
handleEvent: (ctx, event) => {
switch (event.type) {
case "pointerdown":
switch (event.data.options.button) {
case 0: {
const targetShape = getters.getTargetShape();
const shapeHandler = getters.getShapeHandler();

const hitResult = shapeHandler.hitTest(event.data.point, ctx.getScale());
shapeHandler.saveHitResult(hitResult);
if (hitResult) {
switch (hitResult.type) {
case "rx":
return () => {
let showLabel = !event.data.options.ctrl;
return movingShapeControlState<SpikyRectangleShape>({
targetId: targetShape.id,
snapType: "custom",
extraCommands: [COMMAND_EXAM_SRC.RESIZE_PROPORTIONALLY],
patchFn: (s, p, movement) => {
const innerRect = getSpikyInnerRectangle(s);
const localP = applyAffine(getShapeDetransform(s), p);
let nextSize = clamp(0, innerRect.width / 2, localP.x - innerRect.x);
if (movement.ctrl) {
showLabel = false;
} else {
nextSize = snapNumber(nextSize, 1);
showLabel = true;
}
return movement.shift ? { rx: nextSize, ry: nextSize } : { rx: nextSize };
},
getControlFn: (s, scale) =>
applyAffine(getShapeTransform(s), getLocalCornerControl(s, scale)[0]),
renderFn: (ctx, renderCtx, s) => {
renderCornerGuidlinesForRadius(
renderCtx,
ctx.getStyleScheme(),
ctx.getScale(),
s,
showLabel,
);
},
});
};
case "ry":
return () => {
let showLabel = !event.data.options.ctrl;
return movingShapeControlState<SpikyRectangleShape>({
targetId: targetShape.id,
snapType: "custom",
extraCommands: [COMMAND_EXAM_SRC.RESIZE_PROPORTIONALLY],
patchFn: (s, p, movement) => {
const innerRect = getSpikyInnerRectangle(s);
const localP = applyAffine(getShapeDetransform(s), p);
let nextSize = clamp(0, innerRect.height / 2, localP.y - innerRect.y);
if (movement.ctrl) {
showLabel = false;
} else {
nextSize = snapNumber(nextSize, 1);
showLabel = true;
}
return movement.shift ? { rx: nextSize, ry: nextSize } : { ry: nextSize };
},
getControlFn: (s, scale) =>
applyAffine(getShapeTransform(s), getLocalCornerControl(s, scale)[1]),
renderFn: (ctx, renderCtx, s) => {
renderCornerGuidlinesForRadius(
renderCtx,
ctx.getStyleScheme(),
ctx.getScale(),
s,
showLabel,
);
},
});
};
case "spike":
return () => {
let showLabel = !event.data.options.ctrl;
return movingShapeControlState<SpikyRectangleShape>({
targetId: targetShape.id,
snapType: "custom",
patchFn: (s, p, movement) => {
const localP = applyAffine(getShapeDetransform(s), p);
let nextWidth = clamp(0, s.width / 2, s.width / 2 - localP.x) * 2;
let nextHeight = clamp(0, s.height / 2, localP.y);
if (movement.ctrl) {
showLabel = false;
} else {
nextWidth = snapNumber(nextWidth, 2);
nextHeight = snapNumber(nextHeight, 1);
showLabel = true;
}
return { spikeSize: { width: nextWidth, height: nextHeight } };
},
getControlFn: (s) =>
applyAffine(getShapeTransform(s), {
x: (s.width - s.spikeSize.width) / 2,
y: s.spikeSize.height,
}),
renderFn: (ctx, renderCtx, s) => {
renderCornerGuidlinesForSpike(
renderCtx,
ctx.getStyleScheme(),
ctx.getScale(),
s,
showLabel,
);
},
});
};
default:
return;
}
}
}
}
}
},
};
},
(ctx, target) =>
newSimplePolygonHandler({
getShapeComposite: ctx.getShapeComposite,
targetId: target.id,
getAnchors: () => {
const scale = ctx.getScale();
const radiusC = getLocalCornerControl(target, scale);
return [
["rx", radiusC[0]],
["ry", radiusC[1]],
["spike", { x: (target.width - target.spikeSize.width) / 2, y: target.spikeSize.height }],
];
},
}),
);

function getLocalCornerControl(shape: SpikyRectangleShape, scale: number): [IVec2, IVec2] {
const margin = 16 * scale;
const { rx, ry } = getSpikeParameters(shape);
const rect = getSpikyInnerRectangle(shape);
return [
{ x: rect.x + rx, y: rect.y - margin },
{ x: rect.x - margin, y: rect.y + ry },
];
}

function renderCornerGuidlinesForRadius(
renderCtx: CanvasRenderingContext2D,
style: StyleScheme,
scale: number,
shape: SpikyRectangleShape,
showLabel = false,
) {
applyLocalSpace(renderCtx, getRectShapeRect(shape), shape.rotation, () => {
const innerRect = getSpikyInnerRectangle(shape);
const [cornerXC, cornerYC] = getLocalCornerControl(shape, scale);

if (showLabel) {
const margin = 20 * scale;
renderValueLabel(
renderCtx,
shape.rx ?? 0,
{ x: innerRect.x, y: innerRect.y - margin },
-shape.rotation,
scale,
true,
);
renderValueLabel(
renderCtx,
shape.ry ?? 0,
{ x: innerRect.x - margin, y: innerRect.y },
-shape.rotation,
scale,
true,
);
}

applyStrokeStyle(renderCtx, {
color: style.selectionSecondaly,
width: 2 * scale,
dash: "short",
});

renderCtx.beginPath();
renderCtx.rect(innerRect.x, innerRect.y, cornerXC.x - innerRect.x, cornerYC.y - innerRect.y);
renderCtx.stroke();
});
}

function renderCornerGuidlinesForSpike(
renderCtx: CanvasRenderingContext2D,
style: StyleScheme,
scale: number,
shape: SpikyRectangleShape,
showLabel = false,
) {
applyLocalSpace(renderCtx, getRectShapeRect(shape), shape.rotation, () => {
if (showLabel) {
const margin = 20 * scale;
renderValueLabel(
renderCtx,
shape.spikeSize.width / 2,
{ x: shape.width / 2 - shape.spikeSize.width / 4, y: -margin },
-shape.rotation,
scale,
true,
);
renderValueLabel(
renderCtx,
shape.spikeSize.height,
{ x: shape.width / 2 - shape.spikeSize.width / 2 - margin, y: shape.spikeSize.height / 2 },
-shape.rotation,
scale,
true,
);
}

applyStrokeStyle(renderCtx, {
color: style.selectionSecondaly,
width: 2 * scale,
dash: "short",
});

renderCtx.beginPath();
renderCtx.rect((shape.width - shape.spikeSize.width) / 2, 0, shape.spikeSize.width / 2, shape.spikeSize.height);
renderCtx.stroke();
});
}
2 changes: 2 additions & 0 deletions src/shapes/commonStructs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { struct as capsule } from "./polygons/capsule";
import { struct as cylinder } from "./polygons/cylinder";
import { struct as document_symbol } from "./polygons/documentSymbol";
import { struct as star } from "./polygons/star";
import { struct as spiky_rectangle } from "./polygons/spikyRectangle";
import { struct as bubble } from "./polygons/bubble";
import { struct as one_sided_arrow } from "./oneSidedArrow";
import { struct as two_sided_arrow } from "./twoSidedArrow";
Expand Down Expand Up @@ -58,6 +59,7 @@ export const SHAPE_COMMON_STRUCTS: {
cylinder,
document_symbol,
star,
spiky_rectangle,
bubble,
one_sided_arrow,
two_sided_arrow,
Expand Down
6 changes: 3 additions & 3 deletions src/shapes/polygons/roundedRectangle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { SimplePath, SimplePolygonShape, getStructForSimplePolygon } from "../si
import { createBoxPadding, getPaddingRect } from "../../utils/boxPadding";
import { createFillStyle } from "../../utils/fillStyle";
import { createStrokeStyle } from "../../utils/strokeStyle";
import { getRoundedRectInnerBounds } from "../../utils/geometry";
import { getBezierControlPaddingForBorderRadius, getRoundedRectInnerBounds } from "../../utils/geometry";

export type RoundedRectangleShape = SimplePolygonShape & {
rx: number;
Expand Down Expand Up @@ -78,8 +78,8 @@ function getPath(shape: RoundedRectangleShape): SimplePath {

function getCornerValue(shape: RoundedRectangleShape): IVec2 {
const { x: rx, y: ry } = getCornerRadius(shape);
const rate = 0.44772; // Magic value to approximate border-radius via cubic-bezier
return { x: rx * rate, y: ry * rate };
const [x, y] = getBezierControlPaddingForBorderRadius(rx, ry);
return { x, y };
}

function getCornerRadius(shape: RoundedRectangleShape): IVec2 {
Expand Down
Loading

0 comments on commit f549b4c

Please sign in to comment.