Paint bucket!
This commit is contained in:
parent
c27b349668
commit
4d1e42a38b
244
src/main.js
244
src/main.js
|
|
@ -3,7 +3,7 @@ import * as fitCurve from '/fit-curve.js';
|
||||||
import { Bezier } from "/bezier.js";
|
import { Bezier } from "/bezier.js";
|
||||||
import { Quadtree } from './quadtree.js';
|
import { Quadtree } from './quadtree.js';
|
||||||
import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js';
|
import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js';
|
||||||
import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform } from './utils.js';
|
import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform, floodFillRegion, getShapeAtPoint } from './utils.js';
|
||||||
const { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile, readFile: readFile }= window.__TAURI__.fs;
|
const { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile, readFile: readFile }= window.__TAURI__.fs;
|
||||||
const {
|
const {
|
||||||
open: openFileDialog,
|
open: openFileDialog,
|
||||||
|
|
@ -31,6 +31,10 @@ forwardConsole('info', info);
|
||||||
forwardConsole('warn', warn);
|
forwardConsole('warn', warn);
|
||||||
forwardConsole('error', error);
|
forwardConsole('error', error);
|
||||||
|
|
||||||
|
// Debug flags
|
||||||
|
const debugQuadtree =false
|
||||||
|
const debugPaintbucket = false
|
||||||
|
|
||||||
const macOS = navigator.userAgent.includes('Macintosh')
|
const macOS = navigator.userAgent.includes('Macintosh')
|
||||||
|
|
||||||
let simplifyPolyline = simplify
|
let simplifyPolyline = simplify
|
||||||
|
|
@ -41,6 +45,9 @@ let rootPane;
|
||||||
|
|
||||||
let canvases = [];
|
let canvases = [];
|
||||||
|
|
||||||
|
let debugCurves = [];
|
||||||
|
let debugPoints = [];
|
||||||
|
|
||||||
let mode = "select"
|
let mode = "select"
|
||||||
|
|
||||||
let minSegmentSize = 5;
|
let minSegmentSize = 5;
|
||||||
|
|
@ -113,6 +120,7 @@ let mouseEvent;
|
||||||
|
|
||||||
let context = {
|
let context = {
|
||||||
mouseDown: false,
|
mouseDown: false,
|
||||||
|
mousePos: {x: 0, y: 0},
|
||||||
swatches: [
|
swatches: [
|
||||||
"#000000",
|
"#000000",
|
||||||
"#FFFFFF",
|
"#FFFFFF",
|
||||||
|
|
@ -162,18 +170,27 @@ let startProps = {}
|
||||||
|
|
||||||
let actions = {
|
let actions = {
|
||||||
addShape: {
|
addShape: {
|
||||||
create: (parent, shape) => {
|
create: (parent, shape, ctx) => {
|
||||||
if (shape.curves.length==0) return;
|
if (shape.curves.length==0) return;
|
||||||
redoStack.length = 0; // Clear redo stack
|
redoStack.length = 0; // Clear redo stack
|
||||||
let serializableCurves = []
|
let serializableCurves = []
|
||||||
for (let curve of shape.curves) {
|
for (let curve of shape.curves) {
|
||||||
serializableCurves.push({ points: curve.points, color: curve.color })
|
serializableCurves.push({ points: curve.points, color: curve.color })
|
||||||
}
|
}
|
||||||
|
let c = {
|
||||||
|
...context,
|
||||||
|
...ctx
|
||||||
|
}
|
||||||
let action = {
|
let action = {
|
||||||
parent: parent.idx,
|
parent: parent.idx,
|
||||||
curves: serializableCurves,
|
curves: serializableCurves,
|
||||||
startx: shape.startx,
|
startx: shape.startx,
|
||||||
starty: shape.starty,
|
starty: shape.starty,
|
||||||
|
context: {
|
||||||
|
fillShape: c.fillShape,
|
||||||
|
strokeShape: c.strokeShape,
|
||||||
|
fillStyle: c.fillStyle
|
||||||
|
},
|
||||||
uuid: uuidv4()
|
uuid: uuidv4()
|
||||||
}
|
}
|
||||||
undoStack.push({name: "addShape", action: action})
|
undoStack.push({name: "addShape", action: action})
|
||||||
|
|
@ -183,7 +200,11 @@ let actions = {
|
||||||
execute: (action) => {
|
execute: (action) => {
|
||||||
let object = pointerList[action.parent]
|
let object = pointerList[action.parent]
|
||||||
let curvesList = action.curves
|
let curvesList = action.curves
|
||||||
let shape = new Shape(action.startx, action.starty, context, action.uuid)
|
let cxt = {
|
||||||
|
...context,
|
||||||
|
...action.context
|
||||||
|
}
|
||||||
|
let shape = new Shape(action.startx, action.starty, cxt, action.uuid)
|
||||||
for (let curve of curvesList) {
|
for (let curve of curvesList) {
|
||||||
shape.addCurve(new Bezier(
|
shape.addCurve(new Bezier(
|
||||||
curve.points[0].x, curve.points[0].y,
|
curve.points[0].x, curve.points[0].y,
|
||||||
|
|
@ -1363,6 +1384,9 @@ class BaseShape {
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = this.fillStyle
|
ctx.fillStyle = this.fillStyle
|
||||||
}
|
}
|
||||||
|
if (context.debugColor) {
|
||||||
|
ctx.fillStyle = context.debugColor
|
||||||
|
}
|
||||||
if (this.curves.length > 0) {
|
if (this.curves.length > 0) {
|
||||||
ctx.moveTo(this.curves[0].points[0].x, this.curves[0].points[0].y)
|
ctx.moveTo(this.curves[0].points[0].x, this.curves[0].points[0].y)
|
||||||
for (let curve of this.curves) {
|
for (let curve of this.curves) {
|
||||||
|
|
@ -1373,7 +1397,7 @@ class BaseShape {
|
||||||
}
|
}
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
}
|
}
|
||||||
if (this.stroked) {
|
if (this.stroked && !context.debugColor) {
|
||||||
for (let curve of this.curves) {
|
for (let curve of this.curves) {
|
||||||
ctx.strokeStyle = curve.color
|
ctx.strokeStyle = curve.color
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
|
|
@ -1392,7 +1416,9 @@ class BaseShape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Debug, show quadtree
|
// Debug, show quadtree
|
||||||
// this.quadtree.draw(ctx)
|
if (debugQuadtree && this.quadtree && !context.debugColor) {
|
||||||
|
this.quadtree.draw(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1458,6 +1484,7 @@ class Shape extends BaseShape {
|
||||||
midpoint.x, midpoint.y,
|
midpoint.x, midpoint.y,
|
||||||
x, y)
|
x, y)
|
||||||
curve.color = context.strokeStyle
|
curve.color = context.strokeStyle
|
||||||
|
this.quadtree.insert(curve, this.curves.length - 1)
|
||||||
this.curves.push(curve)
|
this.curves.push(curve)
|
||||||
}
|
}
|
||||||
bbox() {
|
bbox() {
|
||||||
|
|
@ -1494,6 +1521,20 @@ class Shape extends BaseShape {
|
||||||
|
|
||||||
return newShape
|
return newShape
|
||||||
}
|
}
|
||||||
|
fromPoints(points, error=30) {
|
||||||
|
console.log(error)
|
||||||
|
this.curves = []
|
||||||
|
let curves = fitCurve.fitCurve(points, error)
|
||||||
|
for (let curve of curves) {
|
||||||
|
let bezier = new Bezier(curve[0][0], curve[0][1],
|
||||||
|
curve[1][0],curve[1][1],
|
||||||
|
curve[2][0], curve[2][1],
|
||||||
|
curve[3][0], curve[3][1])
|
||||||
|
this.curves.push(bezier)
|
||||||
|
this.quadtree.insert(bezier, this.curves.length - 1)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
simplify(mode="corners") {
|
simplify(mode="corners") {
|
||||||
this.quadtree.clear()
|
this.quadtree.clear()
|
||||||
this.inProgress = false
|
this.inProgress = false
|
||||||
|
|
@ -1524,17 +1565,7 @@ class Shape extends BaseShape {
|
||||||
for (let curve of this.curves) {
|
for (let curve of this.curves) {
|
||||||
points.push([curve.points[3].x, curve.points[3].y])
|
points.push([curve.points[3].x, curve.points[3].y])
|
||||||
}
|
}
|
||||||
this.curves = []
|
this.fromPoints(points, error)
|
||||||
let curves = fitCurve.fitCurve(points, error)
|
|
||||||
for (let curve of curves) {
|
|
||||||
let bezier = new Bezier(curve[0][0], curve[0][1],
|
|
||||||
curve[1][0],curve[1][1],
|
|
||||||
curve[2][0], curve[2][1],
|
|
||||||
curve[3][0], curve[3][1])
|
|
||||||
this.curves.push(bezier)
|
|
||||||
this.quadtree.insert(bezier, this.curves.length - 1)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let epsilon = 0.01
|
let epsilon = 0.01
|
||||||
let newCurves = []
|
let newCurves = []
|
||||||
|
|
@ -2184,6 +2215,8 @@ function _newFile(width, height, fps) {
|
||||||
fileWidth = width
|
fileWidth = width
|
||||||
fileHeight = height
|
fileHeight = height
|
||||||
fileFps = fps
|
fileFps = fps
|
||||||
|
undoStack = []
|
||||||
|
redoStack = []
|
||||||
for (let stage of document.querySelectorAll(".stage")) {
|
for (let stage of document.querySelectorAll(".stage")) {
|
||||||
stage.width = width
|
stage.width = width
|
||||||
stage.height = height
|
stage.height = height
|
||||||
|
|
@ -2695,6 +2728,139 @@ function stage() {
|
||||||
break;
|
break;
|
||||||
case "paint_bucket":
|
case "paint_bucket":
|
||||||
let line = {p1: mouse, p2: {x: mouse.x + 3000, y: mouse.y}}
|
let line = {p1: mouse, p2: {x: mouse.x + 3000, y: mouse.y}}
|
||||||
|
debugCurves = []
|
||||||
|
debugPoints = []
|
||||||
|
let epsilon = 5;
|
||||||
|
let min_x = Infinity;
|
||||||
|
let curveB = undefined
|
||||||
|
let point = undefined
|
||||||
|
let regionPoints
|
||||||
|
|
||||||
|
// First, see if there's an existing shape to change the color of
|
||||||
|
const startTime = performance.now()
|
||||||
|
let pointShape = getShapeAtPoint(mouse, context.activeObject.currentFrame.shapes)
|
||||||
|
const endTime = performance.now()
|
||||||
|
|
||||||
|
console.log(pointShape)
|
||||||
|
console.log(`getShapeAtPoint took ${endTime - startTime} milliseconds.`)
|
||||||
|
|
||||||
|
if (pointShape) {
|
||||||
|
actions.colorShape.create(pointShape, context.fillStyle);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We didn't find an existing region to paintbucket, see if we can make one
|
||||||
|
try {
|
||||||
|
regionPoints = floodFillRegion(mouse,epsilon,fileWidth,fileHeight,context, debugPoints, debugPaintbucket)
|
||||||
|
} catch (e) {
|
||||||
|
updateUI()
|
||||||
|
throw e;
|
||||||
|
|
||||||
|
}
|
||||||
|
console.log(regionPoints.length)
|
||||||
|
if (regionPoints.length>0 && regionPoints.length < 10) {
|
||||||
|
// probably a very small area, rerun with minimum epsilon
|
||||||
|
regionPoints = floodFillRegion(mouse,1,fileWidth,fileHeight,context, debugPoints)
|
||||||
|
}
|
||||||
|
let points = []
|
||||||
|
for (let point of regionPoints) {
|
||||||
|
points.push([point.x, point.y])
|
||||||
|
}
|
||||||
|
let cxt = {
|
||||||
|
...context,
|
||||||
|
fillShape: true,
|
||||||
|
strokeShape: false,
|
||||||
|
}
|
||||||
|
let shape = new Shape(regionPoints[0].x, regionPoints[0].y, cxt)
|
||||||
|
shape.fromPoints(points, 1)
|
||||||
|
actions.addShape.create(context.activeObject, shape, cxt)
|
||||||
|
/*
|
||||||
|
for (let i in context.activeObject.currentFrame.shapes) {
|
||||||
|
let shape = context.activeObject.currentFrame.shapes[i]
|
||||||
|
for (let curve of shape.curves) {
|
||||||
|
let intersects = curve.intersects(line)
|
||||||
|
for (let intersect of intersects) {
|
||||||
|
let pos = curve.compute(intersect)
|
||||||
|
if (pos.x < min_x) {
|
||||||
|
curveB = curve
|
||||||
|
min_x = pos.x
|
||||||
|
point = intersect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let previousCurves = [];
|
||||||
|
let previousCurveIds = [];
|
||||||
|
if (curveB) {
|
||||||
|
previousCurves.push(curveB.toString())
|
||||||
|
previousCurveIds.push(curveB.toString())
|
||||||
|
let derivative = curveB.derivative(point)
|
||||||
|
// if curve is moving downward at intersection
|
||||||
|
let splitCurve = curveB.split(point)
|
||||||
|
let curveA;
|
||||||
|
if (derivative.y > 0) {
|
||||||
|
curveA = splitCurve.right
|
||||||
|
} else {
|
||||||
|
curveA = splitCurve.left.reverse()
|
||||||
|
}
|
||||||
|
// debugCurves.push(curveA)
|
||||||
|
for (let i=0; i<2; i++) {
|
||||||
|
|
||||||
|
let min_intersect = Infinity
|
||||||
|
let nextCurve = undefined
|
||||||
|
for (let i in context.activeObject.currentFrame.shapes) {
|
||||||
|
let shape = context.activeObject.currentFrame.shapes[i]
|
||||||
|
for (let j of shape.quadtree.query(curveA.bbox())) {
|
||||||
|
let curve = shape.curves[j]
|
||||||
|
console.log(previousCurveIds)
|
||||||
|
console.log(curve.toString())
|
||||||
|
console.log(curve==(previousCurves.length?previousCurves[0]:undefined))
|
||||||
|
console.log(curve.toString()==(previousCurves.length?previousCurves[0].toString():undefined))
|
||||||
|
if (previousCurves.indexOf(curve.toString()) != -1) {
|
||||||
|
console.log("skipping")
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let intersects = curveA.intersects(curve)
|
||||||
|
// if (intersects.length > 4) continue;
|
||||||
|
console.log(intersects)
|
||||||
|
console.log(curve)
|
||||||
|
for (let intersect of intersects) {
|
||||||
|
intersect = intersect.split('/')
|
||||||
|
let intersect_a = parseFloat(intersect[0])
|
||||||
|
let intersect_b = parseFloat(intersect[1])
|
||||||
|
if (intersect_a < min_intersect) {
|
||||||
|
// console.log(curve)
|
||||||
|
min_intersect = intersect_a
|
||||||
|
nextCurve = curve
|
||||||
|
point = intersect_b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
curveB = nextCurve
|
||||||
|
if (curveB) {
|
||||||
|
// debugCurves.push(curveB)
|
||||||
|
console.log(min_intersect)
|
||||||
|
let splitCurve = curveB.split(point)
|
||||||
|
let d_A = curveA.derivative(min_intersect)
|
||||||
|
let d_B = curveB.derivative(point)
|
||||||
|
curveA = curveA.split(min_intersect).left
|
||||||
|
debugCurves.push(curveA)
|
||||||
|
if ((d_A.x * d_B.y - d_A.y * d_B.x) < 0) {
|
||||||
|
curveA = splitCurve.left.reverse()
|
||||||
|
} else {
|
||||||
|
curveA = splitCurve.right
|
||||||
|
}
|
||||||
|
// debugCurves.push(curveB)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for (let)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
break
|
||||||
// Loop labels in JS!
|
// Loop labels in JS!
|
||||||
shapeLoop:
|
shapeLoop:
|
||||||
// Iterate in reverse so we paintbucket the frontmost shape
|
// Iterate in reverse so we paintbucket the frontmost shape
|
||||||
|
|
@ -3286,6 +3452,52 @@ function updateUI() {
|
||||||
context.activeShape.draw(context)
|
context.activeShape.draw(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug rendering
|
||||||
|
if (debugQuadtree) {
|
||||||
|
|
||||||
|
ctx.fillStyle = "rgba(255,255,255,0.5)"
|
||||||
|
ctx.fillRect(0,0,fileWidth,fileHeight)
|
||||||
|
const ep = 2.5
|
||||||
|
const bbox = {
|
||||||
|
x: { min: context.mousePos.x - ep, max: context.mousePos.x + ep },
|
||||||
|
y: { min: context.mousePos.y - ep, max: context.mousePos.y + ep }
|
||||||
|
};
|
||||||
|
debugCurves = []
|
||||||
|
for (let shape of context.activeObject.currentFrame.shapes) {
|
||||||
|
for (let i of shape.quadtree.query(bbox)) {
|
||||||
|
debugCurves.push(shape.curves[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// let i=4;
|
||||||
|
for (let curve of debugCurves) {
|
||||||
|
ctx.beginPath()
|
||||||
|
// ctx.strokeStyle = `#ff${i}${i}${i}${i}`
|
||||||
|
// i = (i+3)%10
|
||||||
|
ctx.strokeStyle = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
|
||||||
|
ctx.moveTo(curve.points[0].x, curve.points[0].y)
|
||||||
|
ctx.bezierCurveTo(
|
||||||
|
curve.points[1].x, curve.points[1].y,
|
||||||
|
curve.points[2].x, curve.points[2].y,
|
||||||
|
curve.points[3].x, curve.points[3].y
|
||||||
|
)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
let bbox = curve.bbox()
|
||||||
|
ctx.rect(bbox.x.min,bbox.y.min,bbox.x.max-bbox.x.min,bbox.y.max-bbox.y.min)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
let i=0;
|
||||||
|
for (let point of debugPoints) {
|
||||||
|
ctx.beginPath()
|
||||||
|
let j = i.toString(16).padStart(2, '0');
|
||||||
|
ctx.fillStyle = `#${j}ff${j}`
|
||||||
|
i+=1
|
||||||
|
i %= 255
|
||||||
|
ctx.arc(point.x, point.y, 3, 0, 2*Math.PI)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
for (let selectionRect of document.querySelectorAll(".selectionRect")) {
|
for (let selectionRect of document.querySelectorAll(".selectionRect")) {
|
||||||
selectionRect.style.display = "none"
|
selectionRect.style.display = "none"
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,11 @@ class Quadtree {
|
||||||
|
|
||||||
insert (curve, curveIdx) {
|
insert (curve, curveIdx) {
|
||||||
const bbox = curve.bbox()
|
const bbox = curve.bbox()
|
||||||
if (!this.intersects(curve.bbox())) {
|
const isOutside = bbox.x.min < this.boundary.x.min ||
|
||||||
|
bbox.x.max > this.boundary.x.max ||
|
||||||
|
bbox.y.min < this.boundary.y.min ||
|
||||||
|
bbox.y.max > this.boundary.y.max;
|
||||||
|
if (isOutside) {
|
||||||
let newNode = new Quadtree(this.boundary, this.capacity)
|
let newNode = new Quadtree(this.boundary, this.capacity)
|
||||||
newNode.curveIndexes = this.curveIndexes;
|
newNode.curveIndexes = this.curveIndexes;
|
||||||
newNode.curves = this.curves;
|
newNode.curves = this.curves;
|
||||||
|
|
@ -84,6 +88,10 @@ class Quadtree {
|
||||||
|
|
||||||
// Insert a curve into the quadtree, subdividing if necessary
|
// Insert a curve into the quadtree, subdividing if necessary
|
||||||
_insert(curve, curveIdx) {
|
_insert(curve, curveIdx) {
|
||||||
|
if (curve.points[0].x==381.703125) {
|
||||||
|
console.log(this.intersects(curve.bbox()))
|
||||||
|
console.log(this.boundary)
|
||||||
|
}
|
||||||
// If the curve's bounding box doesn't intersect this node's boundary, do nothing
|
// If the curve's bounding box doesn't intersect this node's boundary, do nothing
|
||||||
if (!this.intersects(curve.bbox())) {
|
if (!this.intersects(curve.bbox())) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -91,22 +99,31 @@ class Quadtree {
|
||||||
|
|
||||||
// If the node has space, insert the curve here
|
// If the node has space, insert the curve here
|
||||||
if (this.curves.length < this.capacity) {
|
if (this.curves.length < this.capacity) {
|
||||||
|
|
||||||
|
if (curve.points[0].x==381.703125) {
|
||||||
|
"inserting"
|
||||||
|
}
|
||||||
this.curves.push(curve);
|
this.curves.push(curve);
|
||||||
this.curveIndexes.push(curveIdx)
|
this.curveIndexes.push(curveIdx)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (curve.points[0].x==381.703125) {
|
||||||
|
console.log("going down a level")
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise, subdivide and insert the curve into the appropriate quadrant
|
// Otherwise, subdivide and insert the curve into the appropriate quadrant
|
||||||
if (!this.divided) {
|
if (!this.divided) {
|
||||||
this.subdivide();
|
this.subdivide();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const resultNw = this.nw._insert(curve, curveIdx);
|
||||||
this.nw._insert(curve, curveIdx) ||
|
const resultNe = this.ne._insert(curve, curveIdx);
|
||||||
this.ne._insert(curve, curveIdx) ||
|
const resultSw = this.sw._insert(curve, curveIdx);
|
||||||
this.sw._insert(curve, curveIdx) ||
|
const resultSe = this.se._insert(curve, curveIdx);
|
||||||
this.se._insert(curve, curveIdx)
|
|
||||||
);
|
// Return true if any of the insert operations returned true
|
||||||
|
return resultNw || resultNe || resultSw || resultSe;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query all curves that intersect with a given bounding box
|
// Query all curves that intersect with a given bounding box
|
||||||
|
|
|
||||||
165
src/utils.js
165
src/utils.js
|
|
@ -200,6 +200,167 @@ function generateWaveform(img, buffer, imgHeight, frameWidth, framesPerSecond) {
|
||||||
img.src = dataUrl;
|
img.src = dataUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function floodFillRegion(startPoint, epsilon, fileWidth, fileHeight, context, debugPoints, debugPaintbucket) {
|
||||||
|
// Helper function to check if the point is at the boundary of the region
|
||||||
|
function isBoundaryPoint(point) {
|
||||||
|
return point.x <= 0 || point.x >= fileWidth || point.y <= 0 || point.y >= fileHeight;
|
||||||
|
}
|
||||||
|
let halfEpsilon = epsilon/2
|
||||||
|
|
||||||
|
// Helper function to check if a point is near any curve in the shape
|
||||||
|
function isNearCurve(point, shape) {
|
||||||
|
// Generate bounding box around the point for quadtree query
|
||||||
|
const bbox = {
|
||||||
|
x: { min: point.x - halfEpsilon, max: point.x + halfEpsilon },
|
||||||
|
y: { min: point.y - halfEpsilon, max: point.y + halfEpsilon }
|
||||||
|
};
|
||||||
|
// Get the list of curve indices that are near the point
|
||||||
|
const nearbyCurveIndices = shape.quadtree.query(bbox);
|
||||||
|
// const nearbyCurveIndices = shape.curves.keys()
|
||||||
|
// Check if any of the curves are close enough to the point
|
||||||
|
for (const idx of nearbyCurveIndices) {
|
||||||
|
const curve = shape.curves[idx];
|
||||||
|
const projection = curve.project(point);
|
||||||
|
if (projection.d < epsilon) {
|
||||||
|
return projection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shapes = context.activeObject.currentFrame.shapes;
|
||||||
|
const visited = new Set();
|
||||||
|
const stack = [startPoint];
|
||||||
|
const regionPoints = [];
|
||||||
|
|
||||||
|
// Begin the flood fill process
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const currentPoint = stack.pop();
|
||||||
|
|
||||||
|
// If we reach the boundary of the region, throw an exception
|
||||||
|
if (isBoundaryPoint(currentPoint)) {
|
||||||
|
throw new Error("Flood fill reached the boundary of the area.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current point is already visited, skip it
|
||||||
|
const pointKey = `${currentPoint.x},${currentPoint.y}`;
|
||||||
|
if (visited.has(pointKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited.add(pointKey);
|
||||||
|
if (debugPaintbucket) {
|
||||||
|
debugPoints.push(currentPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
let isNearAnyCurve = false;
|
||||||
|
for (const shape of shapes) {
|
||||||
|
let projection = isNearCurve(currentPoint, shape)
|
||||||
|
if (projection) {
|
||||||
|
isNearAnyCurve = true;
|
||||||
|
regionPoints.push(projection)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the points that are near curves, to prevent jumping past them
|
||||||
|
if (!isNearAnyCurve) {
|
||||||
|
const neighbors = [
|
||||||
|
{ x: currentPoint.x - epsilon, y: currentPoint.y },
|
||||||
|
{ x: currentPoint.x + epsilon, y: currentPoint.y },
|
||||||
|
{ x: currentPoint.x, y: currentPoint.y - epsilon },
|
||||||
|
{ x: currentPoint.x, y: currentPoint.y + epsilon }
|
||||||
|
];
|
||||||
|
// Add unvisited neighbors to the stack
|
||||||
|
for (const neighbor of neighbors) {
|
||||||
|
const neighborKey = `${neighbor.x},${neighbor.y}`;
|
||||||
|
if (!visited.has(neighborKey)) {
|
||||||
|
stack.push(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the region points in connected order
|
||||||
|
return sortPointsByProximity(regionPoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortPointsByProximity(points) {
|
||||||
|
if (points.length <= 1) return points;
|
||||||
|
|
||||||
|
// Start with the first point as the initial sorted point
|
||||||
|
const sortedPoints = [points[0]];
|
||||||
|
points.splice(0, 1); // Remove the first point from the original list
|
||||||
|
|
||||||
|
// Iterate through the remaining points and find the nearest neighbor
|
||||||
|
while (points.length > 0) {
|
||||||
|
const lastPoint = sortedPoints[sortedPoints.length - 1];
|
||||||
|
|
||||||
|
// Find the closest point to the last point
|
||||||
|
let closestIndex = -1;
|
||||||
|
let closestDistance = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
const currentPoint = points[i];
|
||||||
|
const distance = Math.sqrt(Math.pow(currentPoint.x - lastPoint.x, 2) + Math.pow(currentPoint.y - lastPoint.y, 2));
|
||||||
|
|
||||||
|
if (distance < closestDistance) {
|
||||||
|
closestDistance = distance;
|
||||||
|
closestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the closest point to the sorted points
|
||||||
|
sortedPoints.push(points[closestIndex]);
|
||||||
|
points.splice(closestIndex, 1); // Remove the closest point from the original list
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getShapeAtPoint(point, shapes) {
|
||||||
|
// Create a 1x1 off-screen canvas and translate so it is in the first pixel
|
||||||
|
const offscreenCanvas = document.createElement('canvas');
|
||||||
|
offscreenCanvas.width = 1;
|
||||||
|
offscreenCanvas.height = 1;
|
||||||
|
const ctx = offscreenCanvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.translate(-point.x, -point.y);
|
||||||
|
const colorToShapeMap = {};
|
||||||
|
|
||||||
|
// Generate a unique color for each shape (start from #000001 and increment)
|
||||||
|
let colorIndex = 1;
|
||||||
|
|
||||||
|
// Draw all shapes to the off-screen canvas with their unique colors
|
||||||
|
shapes.forEach(shape => {
|
||||||
|
// Generate a unique color for this shape
|
||||||
|
const debugColor = intToHexColor(colorIndex);
|
||||||
|
colorToShapeMap[debugColor] = shape;
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
ctx: ctx,
|
||||||
|
debugColor: debugColor
|
||||||
|
};
|
||||||
|
shape.draw(context);
|
||||||
|
colorIndex++;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pixel = ctx.getImageData(0, 0, 1, 1).data;
|
||||||
|
const sampledColor = rgbToHex(pixel[0], pixel[1], pixel[2]);
|
||||||
|
return colorToShapeMap[sampledColor] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert a number (0-16777215) to a hex color code
|
||||||
|
function intToHexColor(value) {
|
||||||
|
// Ensure the value is between 0 and 16777215 (0xFFFFFF)
|
||||||
|
value = value & 0xFFFFFF;
|
||||||
|
return '#' + value.toString(16).padStart(6, '0').toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert RGB to hex (for sampling)
|
||||||
|
function rgbToHex(r, g, b) {
|
||||||
|
return '#' + (1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
titleCase,
|
titleCase,
|
||||||
|
|
@ -209,5 +370,7 @@ export {
|
||||||
lerp,
|
lerp,
|
||||||
lerpColor,
|
lerpColor,
|
||||||
camelToWords,
|
camelToWords,
|
||||||
generateWaveform
|
generateWaveform,
|
||||||
|
floodFillRegion,
|
||||||
|
getShapeAtPoint
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue