mirror of https://github.com/mkrhere/pw2
3 changed files with 364 additions and 0 deletions
@ -0,0 +1,57 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
|
<title>Document</title> |
||||
|
</head> |
||||
|
<body> |
||||
|
<style> |
||||
|
body { |
||||
|
width: 100vw; |
||||
|
height: 100vh; |
||||
|
margin: 40px; |
||||
|
padding: 0; |
||||
|
} |
||||
|
#rectangle { |
||||
|
width: 200px; |
||||
|
height: 120px; |
||||
|
background-color: #3498db; |
||||
|
cursor: grab; |
||||
|
user-select: none; |
||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); |
||||
|
|
||||
|
position: relative; |
||||
|
} |
||||
|
#handle { |
||||
|
width: 10px; |
||||
|
height: 10px; |
||||
|
background-color: #e74c3c; |
||||
|
border-radius: 50%; |
||||
|
|
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
|
||||
|
margin-left: -5px; |
||||
|
margin-top: -5px; |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
#reference { |
||||
|
width: 200px; |
||||
|
height: 120px; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
opacity: 0.2; |
||||
|
background-color: #2ecc71; |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
</style> |
||||
|
<div id="rectangle"> |
||||
|
<div id="handle"></div> |
||||
|
</div> |
||||
|
<div id="reference"></div> |
||||
|
<script type="module" src="mod.ts"></script> |
||||
|
</body> |
||||
|
</html> |
@ -0,0 +1,292 @@ |
|||||
|
const rectangle = document.getElementById("rectangle")!; |
||||
|
const handle = document.getElementById("handle")!; |
||||
|
const reference = document.getElementById("reference")!; |
||||
|
|
||||
|
class Vec2 { |
||||
|
constructor(public x: number, public y: number) {} |
||||
|
|
||||
|
toString() { |
||||
|
return `Vec<${this.x.toString().padStart(3, " ")}, ${this.y |
||||
|
.toString() |
||||
|
.padStart(3, " ")}>`;
|
||||
|
} |
||||
|
|
||||
|
clone() { |
||||
|
return new Vec2(this.x, this.y); |
||||
|
} |
||||
|
|
||||
|
eq(v: Vec2) { |
||||
|
return this.x === v.x && this.y === v.y; |
||||
|
} |
||||
|
|
||||
|
add(x: number, y: number): Vec2; |
||||
|
add(v: Vec2): Vec2; |
||||
|
add(c: number): Vec2; |
||||
|
add(x: number | Vec2, y?: number) { |
||||
|
if (x instanceof Vec2) return new Vec2(this.x + x.x, this.y + x.y); |
||||
|
if (typeof y === "number") return new Vec2(this.x + x, this.y + y); |
||||
|
return new Vec2(this.x + x, this.y + x); |
||||
|
} |
||||
|
|
||||
|
sub(x: number, y: number): Vec2; |
||||
|
sub(v: Vec2): Vec2; |
||||
|
sub(c: number): Vec2; |
||||
|
sub(x: number | Vec2, y?: number) { |
||||
|
if (x instanceof Vec2) return new Vec2(this.x - x.x, this.y - x.y); |
||||
|
if (typeof y === "number") return new Vec2(this.x - x, this.y - y); |
||||
|
return new Vec2(this.x - x, this.y - x); |
||||
|
} |
||||
|
|
||||
|
mult(x: number, y: number): Vec2; |
||||
|
mult(v: Vec2): Vec2; |
||||
|
mult(c: number): Vec2; |
||||
|
mult(x: number | Vec2, y?: number) { |
||||
|
if (x instanceof Vec2) return new Vec2(this.x * x.x, this.y * x.y); |
||||
|
if (typeof y === "number") return new Vec2(this.x * x, this.y * y); |
||||
|
return new Vec2(this.x * x, this.y * x); |
||||
|
} |
||||
|
|
||||
|
div(x: number, y: number): Vec2; |
||||
|
div(v: Vec2): Vec2; |
||||
|
div(c: number): Vec2; |
||||
|
div(x: number | Vec2, y?: number) { |
||||
|
if (x instanceof Vec2) return new Vec2(this.x / x.x, this.y / x.y); |
||||
|
if (typeof y === "number") return new Vec2(this.x / x, this.y / y); |
||||
|
return new Vec2(this.x / x, this.y / x); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class State { |
||||
|
public dragging: boolean; |
||||
|
public origin: Vec2; |
||||
|
public pos: Vec2; |
||||
|
public size: Vec2; |
||||
|
public rot: number; |
||||
|
public cursor: Vec2; |
||||
|
|
||||
|
constructor({ |
||||
|
dragging, |
||||
|
origin, |
||||
|
pos, |
||||
|
size, |
||||
|
rot, |
||||
|
cursor, |
||||
|
}: { |
||||
|
dragging: boolean; |
||||
|
origin: Vec2; |
||||
|
pos: Vec2; |
||||
|
size: Vec2; |
||||
|
rot: number; |
||||
|
cursor: Vec2; |
||||
|
}) { |
||||
|
this.dragging = dragging; |
||||
|
this.origin = origin; |
||||
|
this.pos = pos; |
||||
|
this.size = size; |
||||
|
this.rot = rot; |
||||
|
this.cursor = cursor; |
||||
|
} |
||||
|
|
||||
|
toString() { |
||||
|
return ( |
||||
|
`State [\n` + |
||||
|
` dragging: ${this.dragging},\n` + |
||||
|
` origin: ${this.origin},\n` + |
||||
|
` pos: ${this.pos},\n` + |
||||
|
` rot: ${this.rot},\n` + |
||||
|
` cursor: ${this.cursor}\n` + |
||||
|
`]` |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
clone() { |
||||
|
return new State({ |
||||
|
dragging: this.dragging, |
||||
|
origin: this.origin.clone(), |
||||
|
size: this.size.clone(), |
||||
|
pos: this.pos.clone(), |
||||
|
rot: this.rot, |
||||
|
cursor: this.cursor?.clone(), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
eq(s: State) { |
||||
|
return ( |
||||
|
this.dragging === s.dragging && |
||||
|
this.origin.eq(s.origin) && |
||||
|
this.pos.eq(s.pos) && |
||||
|
this.rot === s.rot && |
||||
|
this.cursor.eq(s.cursor) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getCursorPositionRelativeToElement( |
||||
|
cursor: Vec2, |
||||
|
size: Vec2, |
||||
|
element: HTMLElement, |
||||
|
) { |
||||
|
const boundingRect = element.getBoundingClientRect(); |
||||
|
|
||||
|
const computedStyle = window.getComputedStyle(element); |
||||
|
const transformValue = computedStyle.transform; |
||||
|
|
||||
|
if (transformValue === "none" || !transformValue) |
||||
|
return cursor.sub(boundingRect.left, boundingRect.top); |
||||
|
|
||||
|
const matrix = new DOMMatrix(transformValue); |
||||
|
|
||||
|
const centerX = boundingRect.left + boundingRect.width / 2; |
||||
|
const centerY = boundingRect.top + boundingRect.height / 2; |
||||
|
|
||||
|
// temporarily convert relative to center for easier calculations
|
||||
|
const relativeToCenter = { |
||||
|
x: cursor.x - centerX, |
||||
|
y: cursor.y - centerY, |
||||
|
}; |
||||
|
|
||||
|
const inverseMatrix = matrix.inverse(); |
||||
|
|
||||
|
inverseMatrix.e = 0; |
||||
|
inverseMatrix.f = 0; |
||||
|
|
||||
|
const transformedPoint = { |
||||
|
x: |
||||
|
relativeToCenter.x * inverseMatrix.a + |
||||
|
relativeToCenter.y * inverseMatrix.c, |
||||
|
y: |
||||
|
relativeToCenter.x * inverseMatrix.b + |
||||
|
relativeToCenter.y * inverseMatrix.d, |
||||
|
}; |
||||
|
|
||||
|
// restore relative to top-left
|
||||
|
return new Vec2( |
||||
|
transformedPoint.x + size.x / 2, |
||||
|
transformedPoint.y + size.y / 2, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
interface Transform { |
||||
|
translation: Vec2; // equivalent to state.pos
|
||||
|
rotation: number; // equivalent to state.rot
|
||||
|
scale?: Vec2; // if you need scaling, defaults to (1,1)
|
||||
|
origin: Vec2; // equivalent to state.origin
|
||||
|
} |
||||
|
|
||||
|
function getCursorPositionRelativeToElement2( |
||||
|
cursor: Vec2, // cursor in page coordinates
|
||||
|
size: Vec2, // original element size
|
||||
|
transform: Transform, |
||||
|
) { |
||||
|
const scale = transform.scale ?? new Vec2(1, 1); |
||||
|
|
||||
|
// First get cursor position relative to the element's translated position
|
||||
|
const relativeToElement = cursor.sub(transform.translation); |
||||
|
|
||||
|
// Calculate the actual pivot point (origin) for transformations
|
||||
|
const pivotPoint = transform.origin; |
||||
|
|
||||
|
// Get position relative to pivot point
|
||||
|
const relativeToPivot = relativeToElement.sub(pivotPoint); |
||||
|
|
||||
|
// Apply inverse rotation around pivot
|
||||
|
const cosTheta = Math.cos(-transform.rotation); |
||||
|
const sinTheta = Math.sin(-transform.rotation); |
||||
|
const rotatedPoint = new Vec2( |
||||
|
relativeToPivot.x * cosTheta - relativeToPivot.y * sinTheta, |
||||
|
relativeToPivot.x * sinTheta + relativeToPivot.y * cosTheta, |
||||
|
); |
||||
|
|
||||
|
// Apply inverse scale if present
|
||||
|
const scaledPoint = transform.scale ? rotatedPoint.div(scale) : rotatedPoint; |
||||
|
|
||||
|
// Add back pivot offset to get final position
|
||||
|
return scaledPoint.add(pivotPoint); |
||||
|
} |
||||
|
|
||||
|
const rect = rectangle.getBoundingClientRect(); |
||||
|
|
||||
|
// state is the source of truth
|
||||
|
const state = new State({ |
||||
|
dragging: false, |
||||
|
|
||||
|
// initial origin
|
||||
|
origin: new Vec2(rect.width / 2, rect.height / 2), |
||||
|
|
||||
|
// initial position of the rectangle
|
||||
|
pos: new Vec2(0, 0), |
||||
|
|
||||
|
// size of the rectangle
|
||||
|
size: new Vec2(rect.width, rect.height), |
||||
|
|
||||
|
// initial rotation
|
||||
|
rot: 0, |
||||
|
|
||||
|
// placeholder cursor position
|
||||
|
cursor: new Vec2(0, 0), |
||||
|
}); |
||||
|
|
||||
|
{ |
||||
|
rectangle.style.transformOrigin = `${state.origin.x}px ${state.origin.y}px`; |
||||
|
rectangle.style.transform = `translate(${state.pos.x}px, ${state.pos.y}px) rotate(${state.rot}rad)`; |
||||
|
handle.style.transform = `translate(${state.origin.x}px, ${state.origin.y}px)`; |
||||
|
} |
||||
|
|
||||
|
let prev = state.clone(); |
||||
|
|
||||
|
{ |
||||
|
const rect = rectangle.getBoundingClientRect(); |
||||
|
reference.style.transformOrigin = `${state.origin.x}px ${state.origin.y}px`; |
||||
|
reference.style.transform = `translate(${rect.left}px, ${rect.top}px)`; |
||||
|
reference.style.width = `${rect.width}px`; |
||||
|
reference.style.height = `${rect.height}px`; |
||||
|
} |
||||
|
|
||||
|
rectangle.addEventListener("mousedown", e => { |
||||
|
prev = state.clone(); |
||||
|
state.dragging = true; |
||||
|
}); |
||||
|
|
||||
|
window.addEventListener("mouseup", () => { |
||||
|
state.dragging = false; |
||||
|
}); |
||||
|
|
||||
|
const degree = 180 / Math.PI; |
||||
|
|
||||
|
window.addEventListener("mousemove", e => { |
||||
|
state.cursor = new Vec2(e.pageX, e.pageY); |
||||
|
|
||||
|
if (!state.dragging) return; |
||||
|
|
||||
|
const deltaCursor = state.cursor.sub(prev.cursor); |
||||
|
|
||||
|
state.pos = state.pos.add(deltaCursor); |
||||
|
|
||||
|
const rect = rectangle.getBoundingClientRect(); |
||||
|
reference.style.transformOrigin = `${state.origin.x}px ${state.origin.y}px`; |
||||
|
reference.style.transform = `translate(${rect.left}px, ${rect.top}px)`; |
||||
|
reference.style.width = `${rect.width}px`; |
||||
|
reference.style.height = `${rect.height}px`; |
||||
|
|
||||
|
// state.origin = state.cursor.sub(new Vec2(rect.left, rect.top));
|
||||
|
// state.rot = 0.05 + prev.rot;
|
||||
|
state.origin = getCursorPositionRelativeToElement2( |
||||
|
state.cursor, |
||||
|
state.size, |
||||
|
// rectangle,
|
||||
|
{ |
||||
|
translation: state.pos, |
||||
|
rotation: state.rot, |
||||
|
origin: state.origin, |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
if (!state.eq(prev)) { |
||||
|
// always keep DOM updated to state
|
||||
|
rectangle.style.transformOrigin = `${state.origin.x}px ${state.origin.y}px`; |
||||
|
rectangle.style.transform = `translate(${state.pos.x}px, ${state.pos.y}px) rotate(${state.rot}rad)`; |
||||
|
handle.style.transform = `translate(${state.origin.x}px, ${state.origin.y}px)`; |
||||
|
} |
||||
|
|
||||
|
prev = state.clone(); |
||||
|
}); |
@ -0,0 +1,15 @@ |
|||||
|
import { defineConfig } from "vite"; |
||||
|
import { resolve } from "path"; |
||||
|
|
||||
|
// https://vitejs.dev/config/
|
||||
|
export default defineConfig({ |
||||
|
server: { port: 10000, allowedHosts: ["dev.mkr.thefeathers.co"] }, |
||||
|
plugins: [], |
||||
|
build: { |
||||
|
rollupOptions: { |
||||
|
input: { |
||||
|
main: resolve(__dirname, "index.html"), |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}); |
Loading…
Reference in new issue