Curve editing

This commit is contained in:
Skyler Lehmkuhl 2024-11-20 00:08:50 -05:00
parent 45a055250b
commit 380fcf0c32
3 changed files with 337 additions and 53 deletions

View File

@ -1,3 +1,5 @@
import { Vector } from "./vector.js";
// math-inlining. // math-inlining.
const { abs, cos, sin, acos, atan2, sqrt, pow } = Math; const { abs, cos, sin, acos, atan2, sqrt, pow } = Math;
@ -1968,6 +1970,25 @@ class Bezier {
} while (t_e < 1); } while (t_e < 1);
return circles; return circles;
} }
getStrutPoints(t) {
const p = this.points.map((p) => new Vector(p));
const mt = 1 - t;
let s = 0;
let n = p.length + 1;
while (--n > 1) {
let list = p.slice(s, s + n);
for (let i = 0, e = list.length - 1; i < e; i++) {
let pt = list[i + 1].subtract(list[i + 1].subtract(list[i]).scale(mt));
p.push(pt);
}
s += n;
}
return p;
}
} }
export { Bezier }; export { Bezier };

View File

@ -116,7 +116,6 @@ let actions = {
}, },
execute: (action) => { execute: (action) => {
let object = pointerList[action.parent] let object = pointerList[action.parent]
console.log(object)
let curvesList = action.curves let curvesList = action.curves
let shape = new Shape(action.startx, action.starty, context, action.uuid) let shape = new Shape(action.startx, action.starty, context, action.uuid)
for (let curve of curvesList) { for (let curve of curvesList) {
@ -136,7 +135,68 @@ let actions = {
object.removeShape(shape) object.removeShape(shape)
delete pointerList[action.uuid] delete pointerList[action.uuid]
} }
},
editShape: {
create: (shape, newCurves) => {
let serializableNewCurves = []
for (let curve of newCurves) {
serializableNewCurves.push({ points: curve.points })
} }
let serializableOldCurves = []
for (let curve of shape.curves) {
serializableOldCurves.push({ points: curve.points })
}
let action = {
shape: shape.idx,
oldCurves: serializableOldCurves,
newCurves: serializableNewCurves
}
undoStack.push({name: "editShape", action: action})
actions.editShape.execute(action)
},
execute: (action) => {
let shape = pointerList[action.shape]
let curvesList = action.newCurves
shape.curves = []
for (let curve of curvesList) {
shape.addCurve(
new Bezier(
curve.points[0].x, curve.points[0].y,
curve.points[1].x, curve.points[1].y,
curve.points[2].x, curve.points[2].y,
curve.points[3].x, curve.points[3].y
))
}
},
rollback: (action) => {
let shape = pointerList[action.shape]
let curvesList = action.oldCurves
shape.curves = []
for (let curve of curvesList) {
shape.addCurve(
new Bezier(
curve.points[0].x, curve.points[0].y,
curve.points[1].x, curve.points[1].y,
curve.points[2].x, curve.points[2].y,
curve.points[3].x, curve.points[3].y
))
}}
},
addObject: {
create: () => {},
execute: (action) => {
},
rollback: (action) => {}
},
editObject: {
create: () => {},
execute: (action) => {
},
rollback: (action) => {}
},
} }
function uuidv4() { function uuidv4() {
@ -192,7 +252,7 @@ function selectCurve(context, mouse) {
} }
} }
if (closest) { if (closest) {
return closest return {curve:closest, shape:shape}
} else { } else {
return undefined return undefined
} }
@ -200,6 +260,137 @@ function selectCurve(context, mouse) {
} }
} }
function moldCurve(curve, mouse, oldmouse) {
let diff = {x: mouse.x - oldmouse.x, y: mouse.y - oldmouse.y}
let p = curve.project(mouse)
let min_influence = 0.1
console.log(p.t)
const CP1 = {
x: curve.points[1].x + diff.x*(1-p.t)*2,
y: curve.points[1].y + diff.y*(1-p.t)*2
}
const CP2 = {
x: curve.points[2].x + diff.x*(p.t)*2,
y: curve.points[2].y + diff.y*(p.t)*2
}
return new Bezier(curve.points[0], CP1, CP2, curve.points[3])
// return curve
}
function moldCurveMath(curve, mouse) {
let interpolated = true
let p = curve.project({x: mouse.x, y: mouse.y})
let t1 = p.t;
let struts = curve.getStrutPoints(t1);
let m = {
t: p.t,
B: p,
e1: struts[7],
e2: struts[8]
};
m.d1 = { x: m.e1.x - m.B.x, y: m.e1.y - m.B.y};
m.d2 = { x: m.e2.x - m.B.x, y: m.e2.y - m.B.y};
const S = curve.points[0],
E = curve.points[curve.order],
{B, t, e1, e2} = m,
org = curve.getABC(t, B),
nB = mouse,
d1 = { x: e1.x - B.x, y: e1.y - B.y },
d2 = { x: e2.x - B.x, y: e2.y - B.y },
ne1 = { x: nB.x + d1.x, y: nB.y + d1.y },
ne2 = { x: nB.x + d2.x, y: nB.y + d2.y },
{A, C} = curve.getABC(t, nB),
// The cubic case requires us to derive two control points,
// which we'll do in a separate function to keep the code
// at least somewhat manageable.
{v1, v2, C1, C2} = deriveControlPoints(S, A, E, ne1, ne2, t);
// if (interpolated) {
// For the last example, we need to show what the "ideal" curve
// looks like, in addition to the one we actually get when we
// rely on the B we picked with the `t` value and e1/e2 points
// that point B had...
const ideal = getIdealisedCurve(S, nB, E);
let idealCurve = new Bezier(ideal.S, ideal.C1, ideal.C2, ideal.E);
// }
let molded = new Bezier(S,C1,C2,E);
let falloff = 100
let d = Bezier.getUtils().dist(ideal.B, p);
let t2 = Math.min(falloff, d) / falloff;
console.log(d)
let iC1 = {
x: (1-t2) * molded.points[1].x + t2 * idealCurve.points[1].x,
y: (1-t2) * molded.points[1].y + t2 * idealCurve.points[1].y
};
let iC2 = {
x: (1-t2) * molded.points[2].x + t2 * idealCurve.points[2].x,
y: (1-t2) * molded.points[2].y + t2 * idealCurve.points[2].y
};
let interpolatedCurve = new Bezier(molded.points[0], iC1, iC2, molded.points[3]);
return interpolatedCurve
// return idealCurve
}
function deriveControlPoints(S, A, E, e1, e2, t) {
// Deriving the control points is effectively "doing what
// we talk about in the section", in code:
const v1 = {
x: A.x - (A.x - e1.x)/(1-t),
y: A.y - (A.y - e1.y)/(1-t)
};
const v2 = {
x: A.x - (A.x - e2.x)/t,
y: A.y - (A.y - e2.y)/t
};
const C1 = {
x: S.x + (v1.x - S.x) / t,
y: S.y + (v1.y - S.y) / t
};
const C2 = {
x: E.x + (v2.x - E.x) / (1-t),
y: E.y + (v2.y - E.y) / (1-t)
};
return {v1, v2, C1, C2};
}
function getIdealisedCurve(p1, p2, p3) {
// This "reruns" the curve composition, but with a `t` value
// that is unrelated to the actual point B we picked, instead
// using whatever the appropriate `t` value would be if we were
// trying to fit a circular arc, as per earlier in the section.
const utils = Bezier.getUtils()
const c = utils.getccenter(p1, p2, p3),
d1 = utils.dist(p1, p2),
d2 = utils.dist(p3, p2),
t = d1 / (d1 + d2),
{ A, B, C, S, E } = Bezier.getABC(3, p1, p2, p3, t),
angle = (Math.atan2(E.y-S.y, E.x-S.x) - Math.atan2(B.y-S.y, B.x-S.x) + utils.TAU) % utils.TAU,
bc = (angle < 0 || angle > utils.PI ? -1 : 1) * utils.dist(S, E)/3,
de1 = t * bc,
de2 = (1-t) * bc,
tangent = [
{ x: B.x - 10 * (B.y-c.y), y: B.y + 10 * (B.x-c.x) },
{ x: B.x + 10 * (B.y-c.y), y: B.y - 10 * (B.x-c.x) }
],
tlength = utils.dist(tangent[0], tangent[1]),
dx = (tangent[1].x - tangent[0].x)/tlength,
dy = (tangent[1].y - tangent[0].y)/tlength,
e1 = { x: B.x + de1 * dx, y: B.y + de1 * dy},
e2 = { x: B.x - de2 * dx, y: B.y - de2 * dy },
{v1, v2, C1, C2} = deriveControlPoints(S, A, E, e1, e2, t);
return {A,B,C,S,E,e1,e2,v1,v2,C1,C2};
}
function growBoundingBox(bboxa, bboxb) { function growBoundingBox(bboxa, bboxb) {
bboxa.x.min = Math.min(bboxa.x.min, bboxb.x.min) bboxa.x.min = Math.min(bboxa.x.min, bboxb.x.min)
bboxa.y.min = Math.min(bboxa.y.min, bboxb.y.min) bboxa.y.min = Math.min(bboxa.y.min, bboxb.y.min)
@ -506,10 +697,10 @@ class GraphicsObject {
if (context.activeCurve) { if (context.activeCurve) {
ctx.strokeStyle = "magenta" ctx.strokeStyle = "magenta"
ctx.beginPath() ctx.beginPath()
ctx.moveTo(context.activeCurve.points[0].x, context.activeCurve.points[0].y) ctx.moveTo(context.activeCurve.current.points[0].x, context.activeCurve.current.points[0].y)
ctx.bezierCurveTo(context.activeCurve.points[1].x, context.activeCurve.points[1].y, ctx.bezierCurveTo(context.activeCurve.current.points[1].x, context.activeCurve.current.points[1].y,
context.activeCurve.points[2].x, context.activeCurve.points[2].y, context.activeCurve.current.points[2].x, context.activeCurve.current.points[2].y,
context.activeCurve.points[3].x, context.activeCurve.points[3].y context.activeCurve.current.points[3].x, context.activeCurve.current.points[3].y
) )
ctx.stroke() ctx.stroke()
} }
@ -638,7 +829,6 @@ function stage() {
imageShape.addLine(0, 0) imageShape.addLine(0, 0)
imageShape.recalculateBoundingBox() imageShape.recalculateBoundingBox()
imageObject.addShape(imageShape) imageObject.addShape(imageShape)
console.log(imageObject.bbox())
context.activeObject.addObject( context.activeObject.addObject(
imageObject, imageObject,
mouse.x-width/2 + (20*img.ix), mouse.x-width/2 + (20*img.ix),
@ -665,13 +855,18 @@ function stage() {
pushState() pushState()
context.mouseDown = true context.mouseDown = true
context.activeShape = new Shape(mouse.x, mouse.y, context, true, true) context.activeShape = new Shape(mouse.x, mouse.y, context, true, true)
console.log(context.activeObject)
context.lastMouse = mouse context.lastMouse = mouse
break; break;
case "select": case "select":
let curve = selectCurve(context, mouse) let selection = selectCurve(context, mouse)
if (curve) { if (selection) {
context.dragging = true context.dragging = true
context.activeCurve = {
initial: selection.curve,
current: new Bezier(selection.curve.points),
shape: selection.shape,
startmouse: mouse
}
console.log("gonna move this") console.log("gonna move this")
} else { } else {
let selected = false let selected = false
@ -694,7 +889,6 @@ function stage() {
context.selectionRect = {x1: mouse.x, x2: mouse.x, y1: mouse.y, y2:mouse.y} context.selectionRect = {x1: mouse.x, x2: mouse.x, y1: mouse.y, y2:mouse.y}
} }
} }
console.log(context.selection)
break; break;
default: default:
break; break;
@ -715,16 +909,29 @@ function stage() {
actions.addShape.create(context.activeObject, context.activeShape) actions.addShape.create(context.activeObject, context.activeShape)
// context.activeObject.addShape(context.activeShape) // context.activeObject.addShape(context.activeShape)
context.activeShape = undefined context.activeShape = undefined
console.log(pointerList)
console.log(undoStack)
} }
break; break;
case "rectangle": case "rectangle":
context.activeShape = undefined context.activeShape = undefined
break;
case "select":
if (context.activeCurve) {
let newCurves = []
for (let curve of context.activeCurve.shape.curves) {
if (curve == context.activeCurve.initial) {
newCurves.push(context.activeCurve.current)
} else {
newCurves.push(curve)
}
}
actions.editShape.create(context.activeCurve.shape, newCurves)
}
break;
default: default:
break; break;
} }
context.lastMouse = mouse context.lastMouse = mouse
context.activeCurve = undefined
updateUI() updateUI()
}) })
stage.addEventListener("mousemove", (e) => { stage.addEventListener("mousemove", (e) => {
@ -752,13 +959,16 @@ function stage() {
break; break;
case "select": case "select":
if (context.dragging) { if (context.dragging) {
let dist = vectorDist(mouse, context.activeCurve.points[1]) // let dist = vectorDist(mouse, context.activeCurve.points[1])
let cpoint = context.activeCurve.points[1] // let cpoint = context.activeCurve.points[1]
if (vectorDist(mouse, context.activeCurve.points[2]) < dist) { // if (vectorDist(mouse, context.activeCurve.points[2]) < dist) {
cpoint = context.activeCurve.points[2] // cpoint = context.activeCurve.points[2]
} // }
cpoint.x += (mouse.x - context.lastMouse.x) // cpoint.x += (mouse.x - context.lastMouse.x)
cpoint.y += (mouse.y - context.lastMouse.y) // cpoint.y += (mouse.y - context.lastMouse.y)
context.activeCurve.current.points = moldCurve(
context.activeCurve.initial, mouse, context.activeCurve.startmouse
).points
} else if (context.selectionRect) { } else if (context.selectionRect) {
context.selectionRect.x2 = mouse.x context.selectionRect.x2 = mouse.x
context.selectionRect.y2 = mouse.y context.selectionRect.y2 = mouse.y
@ -769,7 +979,17 @@ function stage() {
} }
} }
} else { } else {
context.activeCurve = selectCurve(context, mouse) let selection = selectCurve(context, mouse)
if (selection) {
context.activeCurve = {
current: selection.curve,
initial: new Bezier(selection.curve.points),
shape: selection.shape,
startmouse: mouse
}
} else {
context.activeCurve = undefined
}
} }
context.lastMouse = mouse context.lastMouse = mouse
break; break;
@ -927,13 +1147,6 @@ function createPane(content=undefined) {
icon.src = "/assets/stage.svg" icon.src = "/assets/stage.svg"
button.appendChild(icon) button.appendChild(icon)
// div.style.display = "grid";
// div.style.gridTemplateColumns = `var(--lineheight) 1fr`
// div.style.gridTemplateRows = "1fr"
// header.style.gridArea = "1 / 1 / 2 / 2"
// content.style.gridArea = "1 / 2 / 2 / 3"
div.className = "vertical-grid" div.className = "vertical-grid"
header.style.height = "calc( 2 * var(--lineheight))" header.style.height = "calc( 2 * var(--lineheight))"
content.style.height = "calc( 100% - 2 * var(--lineheight) )" content.style.height = "calc( 100% - 2 * var(--lineheight) )"
@ -959,26 +1172,12 @@ function splitPane(div, percent, horiz, newPane=undefined) {
div.appendChild(div1) div.appendChild(div1)
div.appendChild(div2) div.appendChild(div2)
// div.style.display = "grid";
// if (horiz) {
// div.classList.add("horizontal-grid")
// div.style.gridTemplateColumns = `${percent}% 1fr`
// div1.style.gridArea = "1 / 1 / 2 / 2"
// div2.style.gridArea = "1 / 2 / 2 / 3"
// } else {
// div.classList.add("vertical-grid")
// div.style.gridTemplateRows = `${percent}% 1fr`
// div1.style.gridArea = "1 / 1 / 2 / 2"
// div2.style.gridArea = "2 / 1 / 3 / 2"
// }
if (horiz) { if (horiz) {
div.className = "horizontal-grid" div.className = "horizontal-grid"
} else { } else {
div.className = "vertical-grid" div.className = "vertical-grid"
} }
div.setAttribute("lb-percent", percent) // TODO: better attribute name div.setAttribute("lb-percent", percent) // TODO: better attribute name
// div1.style.flex = `0 0 ${percent}%`
// div2.style.flex = `1 1 auto`
Coloris({el: ".color-field"}) Coloris({el: ".color-field"})
updateUI() updateUI()
updateLayout(rootPane) updateLayout(rootPane)
@ -1016,8 +1215,6 @@ function updateUI() {
ctx.reset(); ctx.reset();
ctx.fillStyle = "white" ctx.fillStyle = "white"
ctx.fillRect(0,0,canvas.width,canvas.height) ctx.fillRect(0,0,canvas.width,canvas.height)
ctx.fillStyle = "green"
// ctx.fillRect(0,0,200,200)
context.ctx = ctx; context.ctx = ctx;
root.draw(context) root.draw(context)
@ -1025,13 +1222,5 @@ function updateUI() {
context.activeShape.draw(context) context.activeShape.draw(context)
} }
// let mouse;
// if (mouseEvent) {
// mouse = getMousePos(canvas, mouseEvent);
// } else {
// mouse = {x: 0, y: 0}
// }
// ctx.fillRect(mouse.x, mouse.y, 50,50)
} }
// requestAnimationFrame(updateUI)
} }

74
src/vector.js Normal file
View File

@ -0,0 +1,74 @@
class Vector {
constructor(x, y, z) {
if (arguments.length === 1) {
z = x.z;
y = x.y;
x = x.x;
}
this.x = x;
this.y = y;
if (z !== undefined) {
this.z = z;
}
}
dist(other, y, z = 0) {
if (y !== undefined) other = { x: other, y, z };
let sum = 0;
sum += (this.x - other.x) ** 2;
sum += (this.y - other.y) ** 2;
let z1 = this.z ? this.z : 0;
let z2 = other.z ? other.z : 0;
sum += (z1 - z2) ** 2;
return sum ** 0.5;
}
normalize(f) {
let mag = this.dist(0, 0, 0);
return new Vector((f * this.x) / mag, (f * this.y) / mag, (f * this.z) / mag);
}
getAngle() {
return -Math.atan2(this.y, this.x);
}
reflect(other) {
let p = new Vector(other.x - this.x, other.y - this.y);
if (other.z !== undefined) {
p.z = other.z;
if (this.z !== undefined) {
p.z -= this.z;
}
}
return this.subtract(p);
}
add(other) {
let p = new Vector(this.x + other.x, this.y + other.y);
if (this.z !== undefined) {
p.z = this.z;
if (other.z !== undefined) {
p.z += other.z;
}
}
return p;
}
subtract(other) {
let p = new Vector(this.x - other.x, this.y - other.y);
if (this.z !== undefined) {
p.z = this.z;
if (other.z !== undefined) {
p.z -= other.z;
}
}
return p;
}
scale(f = 1) {
if (f === 0) {
return new Vector(0, 0, this.z === undefined ? undefined : 0);
}
let p = new Vector(this.x * f, this.y * f);
if (this.z !== undefined) {
p.z = this.z * f;
}
return p;
}
}
export { Vector };