diff --git a/src/composables/lineBounding.ts b/src/composables/lineBounding.ts index af5cda25..6306fea4 100644 --- a/src/composables/lineBounding.ts +++ b/src/composables/lineBounding.ts @@ -5,7 +5,7 @@ import { newCircleHitTest } from "./shapeHitTest"; import { applyStrokeStyle } from "../utils/strokeStyle"; import { TAU, getCurveLerpFn, isPointCloseToCurveSpline } from "../utils/geometry"; import { applyFillStyle } from "../utils/fillStyle"; -import { applyCurvePath, applyPath, renderMoveIcon, renderPlusIcon } from "../utils/renderer"; +import { applyCurvePath, applyPath, renderMoveIcon, renderOutlinedCircle, renderPlusIcon } from "../utils/renderer"; const VERTEX_R = 7; const ADD_VERTEX_ANCHOR_RATE = 1; @@ -215,18 +215,12 @@ export function newLineBounding(option: Option) { const optimizeAnchorP = getOptimizeAnchorP(scale); if (optimizeAnchorP) { - applyFillStyle(ctx, { color: style.selectionPrimary }); - ctx.beginPath(); - ctx.ellipse(optimizeAnchorP.x, optimizeAnchorP.y, vertexSize, vertexSize, 0, 0, TAU); - ctx.fill(); + renderOutlinedCircle(ctx, optimizeAnchorP, vertexSize, style.transformAnchor); } const optimizeAnchorQ = getOptimizeAnchorQ(scale); if (optimizeAnchorQ) { - applyFillStyle(ctx, { color: style.selectionPrimary }); - ctx.beginPath(); - ctx.ellipse(optimizeAnchorQ.x, optimizeAnchorQ.y, vertexSize, vertexSize, 0, 0, TAU); - ctx.fill(); + renderOutlinedCircle(ctx, optimizeAnchorQ, vertexSize, style.transformAnchor); } if (hitResult) { diff --git a/src/composables/shapeHandlers/arrowHandler.ts b/src/composables/shapeHandlers/arrowHandler.ts index ef212d65..88682791 100644 --- a/src/composables/shapeHandlers/arrowHandler.ts +++ b/src/composables/shapeHandlers/arrowHandler.ts @@ -4,7 +4,7 @@ import { StyleScheme } from "../../models"; import { ShapeComposite } from "../shapeComposite"; import { applyFillStyle } from "../../utils/fillStyle"; import { TAU, getRadianForDirection4 } from "../../utils/geometry"; -import { renderSwitchDirection } from "../../utils/renderer"; +import { renderOutlinedCircle, renderSwitchDirection } from "../../utils/renderer"; import { COLORS } from "../../utils/color"; import { getArrowHeadPoint, getArrowTailPoint } from "../../utils/arrows"; import { defineShapeHandler } from "./core"; @@ -77,13 +77,10 @@ export const newArrowHandler = defineShapeHandler((optio ] as const ).forEach(([p, highlight]) => { if (highlight) { - applyFillStyle(ctx, { color: style.selectionSecondaly }); + renderOutlinedCircle(ctx, p, threshold, style.selectionSecondaly); } else { - applyFillStyle(ctx, { color: style.selectionPrimary }); + renderOutlinedCircle(ctx, p, threshold, style.transformAnchor); } - ctx.beginPath(); - ctx.arc(p.x, p.y, threshold, 0, TAU); - ctx.fill(); }); if (hitResult?.type === "direction") { diff --git a/src/composables/shapeHandlers/arrowTwoHandler.ts b/src/composables/shapeHandlers/arrowTwoHandler.ts index 43e70043..47c021fa 100644 --- a/src/composables/shapeHandlers/arrowTwoHandler.ts +++ b/src/composables/shapeHandlers/arrowTwoHandler.ts @@ -4,7 +4,7 @@ import { StyleScheme } from "../../models"; import { ShapeComposite } from "../shapeComposite"; import { applyFillStyle } from "../../utils/fillStyle"; import { TAU, getRadianForDirection4 } from "../../utils/geometry"; -import { renderSwitchDirection } from "../../utils/renderer"; +import { renderOutlinedCircle, renderSwitchDirection } from "../../utils/renderer"; import { COLORS } from "../../utils/color"; import { getArrowHeadPoint, getArrowTailPoint } from "../../utils/arrows"; import { defineShapeHandler } from "./core"; @@ -72,13 +72,10 @@ export const newArrowTwoHandler = defineShapeHandler( ] as const ).forEach(([p, highlight]) => { if (highlight) { - applyFillStyle(ctx, { color: style.selectionSecondaly }); + renderOutlinedCircle(ctx, p, threshold, style.selectionSecondaly); } else { - applyFillStyle(ctx, { color: style.selectionPrimary }); + renderOutlinedCircle(ctx, p, threshold, style.transformAnchor); } - ctx.beginPath(); - ctx.arc(p.x, p.y, threshold, 0, TAU); - ctx.fill(); }); if (hitResult?.type === "direction") { diff --git a/src/composables/shapeHandlers/bubbleHandler.ts b/src/composables/shapeHandlers/bubbleHandler.ts index 767158d8..f27dc6be 100644 --- a/src/composables/shapeHandlers/bubbleHandler.ts +++ b/src/composables/shapeHandlers/bubbleHandler.ts @@ -5,7 +5,7 @@ import { applyFillStyle } from "../../utils/fillStyle"; import { TAU } from "../../utils/geometry"; import { defineShapeHandler } from "./core"; import { BubbleShape, getBeakControls, getBeakSize } from "../../shapes/polygons/bubble"; -import { applyLocalSpace, scaleGlobalAlpha } from "../../utils/renderer"; +import { applyLocalSpace, renderOutlinedCircle, scaleGlobalAlpha } from "../../utils/renderer"; import { getLocalAbsolutePoint, getShapeDetransform } from "../../shapes/simplePolygon"; import { applyStrokeStyle } from "../../utils/strokeStyle"; @@ -75,14 +75,10 @@ export const newBubbleHandler = defineShapeHandler((opt ] as const ).forEach(([p, highlight, size = threshold]) => { if (highlight) { - applyFillStyle(ctx, { color: style.selectionSecondaly }); + renderOutlinedCircle(ctx, p, size, style.selectionSecondaly); } else { - applyFillStyle(ctx, { color: style.selectionPrimary }); + renderOutlinedCircle(ctx, p, size, style.transformAnchor); } - - ctx.beginPath(); - ctx.arc(p.x, p.y, size, 0, TAU); - ctx.fill(); }); if (hitResult?.type === "beakOriginC" || hitResult?.type === "beakTipC" || hitResult?.type === "beakSizeC") { diff --git a/src/composables/shapeHandlers/crossHandler.ts b/src/composables/shapeHandlers/crossHandler.ts index be0b6b1e..31b84b4e 100644 --- a/src/composables/shapeHandlers/crossHandler.ts +++ b/src/composables/shapeHandlers/crossHandler.ts @@ -1,10 +1,9 @@ import { IVec2, getDistance, getRectCenter, sub } from "okageo"; import { StyleScheme } from "../../models"; import { ShapeComposite } from "../shapeComposite"; -import { applyFillStyle } from "../../utils/fillStyle"; import { TAU, getRotateFn } from "../../utils/geometry"; import { defineShapeHandler } from "./core"; -import { applyLocalSpace, renderValueLabel } from "../../utils/renderer"; +import { applyLocalSpace, renderOutlinedCircle, renderValueLabel } from "../../utils/renderer"; import { applyStrokeStyle } from "../../utils/strokeStyle"; import { CrossShape } from "../../shapes/polygons/cross"; @@ -52,14 +51,10 @@ export const newCrossHandler = defineShapeHandler((optio ctx.stroke(); if (hitResult) { - applyFillStyle(ctx, { color: style.selectionSecondaly }); + renderOutlinedCircle(ctx, controlSizeP, threshold, style.selectionSecondaly); } else { - applyFillStyle(ctx, { color: style.transformAnchor }); + renderOutlinedCircle(ctx, controlSizeP, threshold, style.transformAnchor); } - - ctx.beginPath(); - ctx.arc(controlSizeP.x, controlSizeP.y, threshold, 0, TAU); - ctx.fill(); }); } @@ -93,9 +88,6 @@ export function renderMovingCrossAnchor( ctx.arc(shape.width / 2, shape.height / 2, shape.crossSize / 2, 0, TAU); ctx.stroke(); - applyFillStyle(ctx, { color: style.selectionSecondaly }); - ctx.beginPath(); - ctx.arc(p.x, p.y, threshold, 0, TAU); - ctx.fill(); + renderOutlinedCircle(ctx, p, threshold, style.selectionSecondaly); }); } diff --git a/src/composables/shapeHandlers/cylinderHandler.ts b/src/composables/shapeHandlers/cylinderHandler.ts index 89c38b24..b4db9977 100644 --- a/src/composables/shapeHandlers/cylinderHandler.ts +++ b/src/composables/shapeHandlers/cylinderHandler.ts @@ -1,11 +1,9 @@ import { IVec2, applyAffine, getDistance } from "okageo"; import { StyleScheme } from "../../models"; import { ShapeComposite } from "../shapeComposite"; -import { applyFillStyle } from "../../utils/fillStyle"; -import { TAU } from "../../utils/geometry"; import { defineShapeHandler } from "./core"; import { CylinderShape } from "../../shapes/polygons/cylinder"; -import { applyLocalSpace } from "../../utils/renderer"; +import { applyLocalSpace, renderOutlinedCircle } from "../../utils/renderer"; import { getShapeDetransform } from "../../shapes/simplePolygon"; const ANCHOR_SIZE = 6; @@ -57,14 +55,10 @@ export const newCylinderHandler = defineShapeHandler( ] as const ).forEach(([p, highlight]) => { if (highlight) { - applyFillStyle(ctx, { color: style.selectionSecondaly }); + renderOutlinedCircle(ctx, p, threshold, style.selectionSecondaly); } else { - applyFillStyle(ctx, { color: style.transformAnchor }); + renderOutlinedCircle(ctx, p, threshold, style.transformAnchor); } - - ctx.beginPath(); - ctx.arc(p.x, p.y, threshold, 0, TAU); - ctx.fill(); }); }); } @@ -88,12 +82,9 @@ export function renderMovingCylinderAnchor( x: shape.width * shape.c0.x, y: shape.height * shape.c0.y, }; + const threshold = ANCHOR_SIZE * scale; applyLocalSpace(ctx, { x: shape.p.x, y: shape.p.y, width: shape.width, height: shape.height }, shape.rotation, () => { - applyFillStyle(ctx, { color: style.selectionSecondaly }); - applyFillStyle(ctx, { color: style.selectionSecondaly }); - ctx.beginPath(); - ctx.arc(nextControlP.x, nextControlP.y, 6 * scale, 0, TAU); - ctx.fill(); + renderOutlinedCircle(ctx, nextControlP, threshold, style.selectionSecondaly); }); } diff --git a/src/composables/shapeHandlers/roundedRectangleHandler.ts b/src/composables/shapeHandlers/roundedRectangleHandler.ts index 8399f5bf..e76fd33e 100644 --- a/src/composables/shapeHandlers/roundedRectangleHandler.ts +++ b/src/composables/shapeHandlers/roundedRectangleHandler.ts @@ -1,10 +1,8 @@ import { IVec2, applyAffine, getDistance } from "okageo"; import { StyleScheme } from "../../models"; import { ShapeComposite } from "../shapeComposite"; -import { applyFillStyle } from "../../utils/fillStyle"; -import { TAU } from "../../utils/geometry"; import { defineShapeHandler } from "./core"; -import { applyLocalSpace, renderValueLabel } from "../../utils/renderer"; +import { applyLocalSpace, renderOutlinedCircle, renderValueLabel } from "../../utils/renderer"; import { getShapeDetransform } from "../../shapes/simplePolygon"; import { applyStrokeStyle } from "../../utils/strokeStyle"; import { RoundedRectangleShape } from "../../shapes/polygons/roundedRectangle"; @@ -57,14 +55,10 @@ export const newRoundedRectangleHandler = defineShapeHandler { if (highlight) { - applyFillStyle(ctx, { color: style.selectionSecondaly }); + renderOutlinedCircle(ctx, p, size, style.selectionSecondaly); } else { - applyFillStyle(ctx, { color: style.transformAnchor }); + renderOutlinedCircle(ctx, p, size, style.transformAnchor); } - - ctx.beginPath(); - ctx.arc(p.x, p.y, size, 0, TAU); - ctx.fill(); }); }); diff --git a/src/composables/shapeHandlers/simplePolygonHandler.ts b/src/composables/shapeHandlers/simplePolygonHandler.ts index 273f422f..84d86f30 100644 --- a/src/composables/shapeHandlers/simplePolygonHandler.ts +++ b/src/composables/shapeHandlers/simplePolygonHandler.ts @@ -4,7 +4,13 @@ import { ShapeComposite } from "../shapeComposite"; import { applyFillStyle } from "../../utils/fillStyle"; import { TAU, getRadianForDirection4, getRotateFn } from "../../utils/geometry"; import { defineShapeHandler } from "./core"; -import { applyLocalSpace, applyPath, renderSwitchDirection, renderValueLabel } from "../../utils/renderer"; +import { + applyLocalSpace, + applyPath, + renderOutlinedCircle, + renderSwitchDirection, + renderValueLabel, +} from "../../utils/renderer"; import { applyStrokeStyle } from "../../utils/strokeStyle"; import { SimplePolygonShape, @@ -88,15 +94,10 @@ export const newSimplePolygonHandler = defineShapeHandler((a) => [a[1], a[0] === hitResult?.type]) .forEach(([p, highlight]) => { if (highlight) { - applyFillStyle(ctx, { color: style.selectionSecondaly }); - applyStrokeStyle(ctx, { color: style.selectionSecondaly }); + renderOutlinedCircle(ctx, p, threshold, style.selectionSecondaly); } else { - applyFillStyle(ctx, { color: style.transformAnchor }); - applyStrokeStyle(ctx, { color: style.transformAnchor }); + renderOutlinedCircle(ctx, p, threshold, style.transformAnchor); } - ctx.beginPath(); - ctx.arc(p.x, p.y, threshold, 0, TAU); - ctx.fill(); }); if (direction4Anchor) { diff --git a/src/composables/states/appCanvas/arrow/movingArrowFromState.ts b/src/composables/states/appCanvas/arrow/movingArrowFromState.ts index 27499193..9210c2eb 100644 --- a/src/composables/states/appCanvas/arrow/movingArrowFromState.ts +++ b/src/composables/states/appCanvas/arrow/movingArrowFromState.ts @@ -1,11 +1,10 @@ import type { AppCanvasState } from "../core"; import { newSelectionHubState } from "../selectionHubState"; -import { applyFillStyle } from "../../../../utils/fillStyle"; -import { TAU } from "../../../../utils/geometry"; import { add } from "okageo"; import { getPatchByLayouts } from "../../../shapeLayoutHandler"; import { ShapeSnapping, SnappingResult, newShapeSnapping, renderSnappingResult } from "../../../shapeSnapping"; import { ArrowCommonShape, getArrowHeadPoint, patchToMoveTail } from "../../../../utils/arrows"; +import { renderOutlinedCircle } from "../../../../utils/renderer"; interface Option { targetId: string; @@ -62,10 +61,7 @@ export function newMovingArrowFromState(option: Option): AppCanvasState { render: (ctx, renderCtx) => { const tmpShape: ArrowCommonShape = { ...targetShape, ...ctx.getTmpShapeMap()[targetShape.id] }; const headP = getArrowHeadPoint(tmpShape); - applyFillStyle(renderCtx, { color: ctx.getStyleScheme().selectionSecondaly }); - renderCtx.beginPath(); - renderCtx.arc(headP.x, headP.y, 6 * ctx.getScale(), 0, TAU); - renderCtx.fill(); + renderOutlinedCircle(renderCtx, headP, 6 * ctx.getScale(), ctx.getStyleScheme().selectionSecondaly); if (snappingResult) { renderSnappingResult(renderCtx, { diff --git a/src/composables/states/appCanvas/arrow/movingArrowHeadState.ts b/src/composables/states/appCanvas/arrow/movingArrowHeadState.ts index 8aa805bb..71826d93 100644 --- a/src/composables/states/appCanvas/arrow/movingArrowHeadState.ts +++ b/src/composables/states/appCanvas/arrow/movingArrowHeadState.ts @@ -1,11 +1,11 @@ import type { AppCanvasState } from "../core"; import { newSelectionHubState } from "../selectionHubState"; -import { applyFillStyle } from "../../../../utils/fillStyle"; -import { TAU, getRotateFn } from "../../../../utils/geometry"; +import { getRotateFn } from "../../../../utils/geometry"; import { add, clamp, sub } from "okageo"; import { getPatchByLayouts } from "../../../shapeLayoutHandler"; import { ArrowCommonShape, getHeadControlPoint, getHeadMaxLength } from "../../../../utils/arrows"; import { getNormalizedSimplePolygonShape } from "../../../../shapes/simplePolygon"; +import { renderOutlinedCircle } from "../../../../utils/renderer"; interface Option { targetId: string; @@ -75,10 +75,7 @@ export function newMovingArrowHeadState(option: Option): AppCanvasState { render: (ctx, renderCtx) => { const tmpShape: ArrowCommonShape = { ...targetShape, ...ctx.getTmpShapeMap()[targetShape.id] }; const headControlP = getHeadControlPoint(tmpShape); - applyFillStyle(renderCtx, { color: ctx.getStyleScheme().selectionSecondaly }); - renderCtx.beginPath(); - renderCtx.arc(headControlP.x, headControlP.y, 6 * ctx.getScale(), 0, TAU); - renderCtx.fill(); + renderOutlinedCircle(renderCtx, headControlP, 6 * ctx.getScale(), ctx.getStyleScheme().selectionSecondaly); }, }; } diff --git a/src/composables/states/appCanvas/arrow/movingArrowTailState.ts b/src/composables/states/appCanvas/arrow/movingArrowTailState.ts index dd80afb7..473431ee 100644 --- a/src/composables/states/appCanvas/arrow/movingArrowTailState.ts +++ b/src/composables/states/appCanvas/arrow/movingArrowTailState.ts @@ -1,11 +1,11 @@ import type { AppCanvasState } from "../core"; import { newSelectionHubState } from "../selectionHubState"; import { OneSidedArrowShape, getTailControlPoint } from "../../../../shapes/oneSidedArrow"; -import { applyFillStyle } from "../../../../utils/fillStyle"; -import { TAU, getRotateFn } from "../../../../utils/geometry"; +import { getRotateFn } from "../../../../utils/geometry"; import { add, clamp, sub } from "okageo"; import { getPatchByLayouts } from "../../../shapeLayoutHandler"; import { getNormalizedSimplePolygonShape } from "../../../../shapes/simplePolygon"; +import { renderOutlinedCircle } from "../../../../utils/renderer"; interface Option { targetId: string; @@ -71,10 +71,7 @@ export function newMovingArrowTailState(option: Option): AppCanvasState { render: (ctx, renderCtx) => { const tmpShape: OneSidedArrowShape = { ...targetShape, ...ctx.getTmpShapeMap()[targetShape.id] }; const tailControlP = getTailControlPoint(tmpShape); - applyFillStyle(renderCtx, { color: ctx.getStyleScheme().selectionSecondaly }); - renderCtx.beginPath(); - renderCtx.arc(tailControlP.x, tailControlP.y, 6 * ctx.getScale(), 0, TAU); - renderCtx.fill(); + renderOutlinedCircle(renderCtx, tailControlP, 6 * ctx.getScale(), ctx.getStyleScheme().selectionSecondaly); }, }; } diff --git a/src/composables/states/appCanvas/arrow/movingArrowToState.ts b/src/composables/states/appCanvas/arrow/movingArrowToState.ts index 2bb7533f..6c075564 100644 --- a/src/composables/states/appCanvas/arrow/movingArrowToState.ts +++ b/src/composables/states/appCanvas/arrow/movingArrowToState.ts @@ -1,11 +1,10 @@ import type { AppCanvasState } from "../core"; import { newSelectionHubState } from "../selectionHubState"; -import { applyFillStyle } from "../../../../utils/fillStyle"; -import { TAU } from "../../../../utils/geometry"; import { add } from "okageo"; import { getPatchByLayouts } from "../../../shapeLayoutHandler"; import { ShapeSnapping, SnappingResult, newShapeSnapping, renderSnappingResult } from "../../../shapeSnapping"; import { ArrowCommonShape, getArrowHeadPoint, patchToMoveHead } from "../../../../utils/arrows"; +import { renderOutlinedCircle } from "../../../../utils/renderer"; interface Option { targetId: string; @@ -62,10 +61,7 @@ export function newMovingArrowToState(option: Option): AppCanvasState { render: (ctx, renderCtx) => { const tmpShape: ArrowCommonShape = { ...targetShape, ...ctx.getTmpShapeMap()[targetShape.id] }; const headP = getArrowHeadPoint(tmpShape); - applyFillStyle(renderCtx, { color: ctx.getStyleScheme().selectionSecondaly }); - renderCtx.beginPath(); - renderCtx.arc(headP.x, headP.y, 6 * ctx.getScale(), 0, TAU); - renderCtx.fill(); + renderOutlinedCircle(renderCtx, headP, 6 * ctx.getScale(), ctx.getStyleScheme().selectionSecondaly); if (snappingResult) { renderSnappingResult(renderCtx, { diff --git a/src/composables/states/appCanvas/movingShapeControlState.ts b/src/composables/states/appCanvas/movingShapeControlState.ts index a57ac847..f51e32bc 100644 --- a/src/composables/states/appCanvas/movingShapeControlState.ts +++ b/src/composables/states/appCanvas/movingShapeControlState.ts @@ -1,13 +1,12 @@ import type { AppCanvasState, AppCanvasStateContext } from "./core"; import { newSelectionHubState } from "./selectionHubState"; -import { applyFillStyle } from "../../../utils/fillStyle"; -import { TAU } from "../../../utils/geometry"; import { IVec2, add } from "okageo"; import { getPatchByLayouts } from "../../shapeLayoutHandler"; import { ShapeSnapping, SnappingResult, newShapeSnapping, renderSnappingResult } from "../../shapeSnapping"; import { Shape } from "../../../models"; import { COMMAND_EXAM_SRC } from "./commandExams"; import { CommandExam, EditMovement } from "../types"; +import { renderOutlinedCircle } from "../../../utils/renderer"; export type RenderShapeControlFn = ( ctx: AppCanvasStateContext, @@ -95,10 +94,7 @@ export function movingShapeControlState(option: Option): App render: (ctx, renderCtx) => { const tmpShape: T = { ...targetShape, ...ctx.getTmpShapeMap()[targetShape.id] }; const control = option.getControlFn(tmpShape, ctx.getScale()); - applyFillStyle(renderCtx, { color: ctx.getStyleScheme().selectionSecondaly }); - renderCtx.beginPath(); - renderCtx.arc(control.x, control.y, 6 * ctx.getScale(), 0, TAU); - renderCtx.fill(); + renderOutlinedCircle(renderCtx, control, 6 * ctx.getScale(), ctx.getStyleScheme().selectionSecondaly); if (snappingResult) { renderSnappingResult(renderCtx, { diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts index 402a13db..e5bdf336 100644 --- a/src/utils/renderer.ts +++ b/src/utils/renderer.ts @@ -1,9 +1,9 @@ import { IRectangle, IVec2, PathSegmentRaw, add, getRadian, getUnit, isSame, multi, rotate, sub } from "okageo"; -import { ISegment, getArcCurveParamsByNormalizedControl, getRotateFn } from "./geometry"; +import { ISegment, TAU, getArcCurveParamsByNormalizedControl, getRotateFn } from "./geometry"; import { applyStrokeStyle } from "./strokeStyle"; import { applyFillStyle } from "./fillStyle"; import { COLORS } from "./color"; -import { CurveControl } from "../models"; +import { Color, CurveControl } from "../models"; import { DEFAULT_FONT_SIZE } from "./textEditor"; export function applyPath(ctx: CanvasRenderingContext2D | Path2D, path: IVec2[], closed = false, reverse = false) { @@ -118,6 +118,18 @@ export function createSVGCurvePath( return ret; } +export function renderOutlinedCircle(ctx: CanvasRenderingContext2D, p: IVec2, r: number, fillColor: Color) { + ctx.fillStyle = "#000"; + ctx.beginPath(); + ctx.arc(p.x, p.y, r, 0, TAU); + ctx.fill(); + + applyFillStyle(ctx, { color: fillColor }); + ctx.beginPath(); + ctx.arc(p.x, p.y, r * 0.9, 0, TAU); + ctx.fill(); +} + export function renderArrow(ctx: CanvasRenderingContext2D, [a, b]: ISegment, size: number) { const v = sub(b, a); const n = isSame(a, b) ? { x: size, y: 0 } : multi(getUnit(v), size);