Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

塗りつぶしのアルゴリズムを変更 #10

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 124 additions & 116 deletions apps/client/app/features/artboard/bucketFill.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,96 @@
// ref: https://github.com/cat-crosswalk/nascalay-frontend/blob/main/src/components/Canvas/bucketFill.ts
// ref: https://en.wikipedia.org/wiki/Flood_fill

import type { Color, LikeEqualColor } from "./types";
import { colorsToRaw, equalColor, hexToColor, rawToColors } from "./utils";

// https://nullpon.moe/dev/sample/canvas/bucketfill.html めちゃめちゃ参考にしてる
// シードフィルアルゴリズムで塗りつぶす
interface Seed {
xRange: [number, number];
y: number;
dy: -1 | 1;
}

/**
* 指定された座標から右方向にまっすぐ targetColor と等しい色を塗りつぶす
*
* @returns 右端の x 座標
*/
function drawToRight(
data: Color[][],
x: number,
y: number,
color: Color,
widthRange: readonly [number, number],
targetColor: Color,
likeEqualColor: LikeEqualColor,
) {
let rightEnd = null;
for (let nowX = x + 1; nowX < widthRange[1]; nowX++) {
const nowColor = data[y][nowX];
if (!likeEqualColor(nowColor, targetColor)) break;
data[y][nowX] = color;
rightEnd = nowX;
}
return rightEnd;
interface Operations {
/**
* キャンバス内の有効な座標かつ targetColor と等しいかどうかを返す
*/
inside: (x: number, y: number) => boolean;
/**
* キャンバスの座標 (x, y) を塗りつぶす色で塗る
*/
set: (x: number, y: number) => void;
pushSeed: (seed: Seed) => void;
}

/**
* 指定された座標から左方向にまっすぐ targetColor と等しい色を塗りつぶす
* seed の範囲を対象色がある限り左に広げる
*
* 左方向に広がった場合は U字ターンしている可能性があるため、はみ出た部分を折り返す形の seed として追加する
*
* @returns 左端の x 座標
*/
function drawToLeft(
data: Color[][],
x: number,
y: number,
color: Color,
widthRange: readonly [number, number],
targetColor: Color,
likeEqualColor: LikeEqualColor,
) {
let leftEnd = null;
for (let nowX = x; nowX >= widthRange[0]; nowX--) {
const nowColor = data[y][nowX];
if (!likeEqualColor(nowColor, targetColor)) break;
data[y][nowX] = color;
leftEnd = nowX;
function searchLeft(
seed: Readonly<Seed>,
{ inside, set, pushSeed }: Operations,
): number {
let x = seed.xRange[0];
if (!inside(x, seed.y)) return x;

while (inside(x - 1, seed.y)) {
x--;
set(x, seed.y);
}

if (x < seed.xRange[0]) {
pushSeed({
xRange: [x, seed.xRange[0] - 1],
y: seed.y - seed.dy,
dy: -seed.dy as -1 | 1,
ras0q marked this conversation as resolved.
Show resolved Hide resolved
});
}
return leftEnd;

return x;
}

/**
* seeds を破壊的に更新する
*/
function updateSeeds(
function createInside(
data: Readonly<Color[][]>,
xLeft: number,
xRight: number,
y: number,
seeds: { x: number; y: number }[],
targetColor: Color,
heightRange: readonly [number, number],
xRange: readonly [number, number],
yRange: readonly [number, number],
targetColor: Readonly<Color>,
likeEqualColor: LikeEqualColor,
) {
if (y < heightRange[0] || y >= heightRange[1]) return;

let prevIsTarget = false;
for (let nowX = xLeft; nowX <= xRight; nowX++) {
const nowColor = data[y][nowX];
if (likeEqualColor(nowColor, targetColor)) {
if (!prevIsTarget) {
seeds.push({ x: nowX, y });
}
prevIsTarget = true;
} else {
prevIsTarget = false;
}
}
): Operations["inside"] {
return (x, y) =>
x >= xRange[0] &&
x < xRange[1] &&
y >= yRange[0] &&
y < yRange[1] &&
likeEqualColor(data[y][x], targetColor);
}
function createSet(data: Color[][], color: Color): Operations["set"] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createInsideもややだけど使うのoperationsの初期化のときだけだしラップするまででもなさそう
好みで

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

しないとしたら bucketFill の関数内にそのまま書くってこと?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そう、pushSeedがそうだからここだけ切り出してるのが気になった

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

処理が長めだったから切ったような記憶がある

return (x, y) => {
data[y][x] = color;
};
}

/**
* canvas を flood fill を用いて塗りつぶす
*
* アルゴリズム自体は wiki にあるものそのまま (ref: https://en.wikipedia.org/wiki/Flood_fill)
*
* @param canvas 塗りつぶす対象の canvas
* @param x 塗りつぶしの始点
* @param y 塗りつぶしの始点
* @param colorCode 塗りつぶす色
* @param widthRange canvas の塗りつぶし範囲の横幅 @default [0, canvas.width]
* @param heightRange canvas の塗りつぶし範囲の縦幅 @default [0, canvas.height]
* @param likeEqualColor 色が等しいかどうかを判定する関数 @default 完全一致
*/
export function bucketFill(
canvas: HTMLCanvasElement,
x: number,
y: number,
colorCode: `#${string}`,
widthRange?: [number, number],
heightRange?: [number, number],
widthRange: [number, number] = [0, canvas.width],
heightRange: [number, number] = [0, canvas.height],
likeEqualColor: LikeEqualColor = equalColor,
) {
const ctx = canvas.getContext("2d");
Expand All @@ -110,56 +111,63 @@ export function bucketFill(
}
if (likeEqualColor(color, targetColor)) return;

const xRange = widthRange ?? [0, width];
const yRange = heightRange ?? [0, height];

const seeds = [{ x, y }];
while (seeds.length > 0) {
// biome-ignore lint/style/noNonNullAssertion: while の条件式から pop は undefined にならない
const { x, y } = seeds.pop()!;

// 左右に塗りつぶす
const leftX =
drawToLeft(
formattedData,
x,
y,
color,
xRange,
targetColor,
likeEqualColor,
) ?? x;
const rightX =
drawToRight(
formattedData,
x,
y,
color,
xRange,
targetColor,
likeEqualColor,
) ?? x;

updateSeeds(
formattedData,
leftX,
rightX,
y + 1,
seeds,
targetColor,
yRange,
likeEqualColor,
);
updateSeeds(
const seeds: Seed[] = [
{ xRange: [x, x], y, dy: 1 },
{ xRange: [x, x], y: y - 1, dy: -1 },
];
const operations: Operations = {
inside: createInside(
formattedData,
leftX,
rightX,
y - 1,
seeds,
widthRange,
heightRange,
targetColor,
yRange,
likeEqualColor,
);
),
set: createSet(formattedData, color),
pushSeed: (seed: Seed) => seeds.push(seed),
};

if (!operations.inside(x, y)) return;
while (seeds.length > 0) {
// biome-ignore lint/style/noNonNullAssertion: while の条件式から pop は undefined にならない
const { xRange, y, dy } = seeds.pop()!;

let x = searchLeft({ xRange, y, dy }, operations);
let [leftX, rightX] = xRange;

while (leftX <= rightX) {
// xRange の左端から右に向かって塗りつぶす
while (operations.inside(leftX, y)) {
operations.set(leftX, y);
leftX++;
}

// 塗った部分があれば、その範囲を次の y に対して seed として追加する
if (leftX > x) {
operations.pushSeed({
xRange: [x, leftX - 1],
y: y + dy,
dy,
});
}

// 右側が溢れた場合は U字ターンしている可能性があるため、はみ出た部分を折り返す形の seed として追加する
if (leftX - 1 > rightX) {
operations.pushSeed({
xRange: [rightX + 1, leftX - 1],
y: y - dy,
dy: -dy as -1 | 1,
});
}

// xRange のうち、塗られない部分はスキップ
leftX++;
while (leftX < rightX && !operations.inside(leftX, y)) {
leftX++;
}

x = leftX;
}
}

imageData.data.set(colorsToRaw(formattedData, width, height));
Expand Down