Merge branch 'main' of https://github.com/folk-canvas/folk-canvas
This commit is contained in:
commit
a5b5d390dd
|
|
@ -23,7 +23,7 @@
|
||||||
<body>
|
<body>
|
||||||
<folk-shape x="100" y="100" width="50" height="50"></folk-shape>
|
<folk-shape x="100" y="100" width="50" height="50"></folk-shape>
|
||||||
<folk-shape x="100" y="200" width="50" height="50"></folk-shape>
|
<folk-shape x="100" y="200" width="50" height="50"></folk-shape>
|
||||||
<folk-shape x="100" y="300" width="50" height="50" rotate="45"></folk-shape>
|
<folk-shape x="100" y="300" width="50" height="50" rotation="45"></folk-shape>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { FolkShape } from '../src/folk-shape.ts';
|
import { FolkShape } from '../src/folk-shape.ts';
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build --base=/folk-canvas",
|
"build": "tsc --noEmit && vite build --base=/folk-canvas",
|
||||||
"preview": "vite build && vite preview"
|
"preview": "tsc --noEmit && vite build && vite preview",
|
||||||
|
"types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.26.2",
|
"@babel/parser": "^7.26.2",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import type { Point } from './types.ts';
|
||||||
|
|
||||||
|
export class Vector {
|
||||||
|
/**
|
||||||
|
* Creates a zero vector (0,0)
|
||||||
|
* @returns {Point} A point representing a zero vector
|
||||||
|
*/
|
||||||
|
static zero(): Point {
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtracts vector b from vector a
|
||||||
|
* @param {Point} a - The first vector
|
||||||
|
* @param {Point} b - The vector to subtract
|
||||||
|
* @returns {Point} The resulting vector
|
||||||
|
*/
|
||||||
|
static sub(a: Point, b: Point): Point {
|
||||||
|
return { x: a.x - b.x, y: a.y - b.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds two vectors together
|
||||||
|
* @param {Point} a - The first vector
|
||||||
|
* @param {Point} b - The second vector
|
||||||
|
* @returns {Point} The sum of the two vectors
|
||||||
|
*/
|
||||||
|
static add(a: Point, b: Point): Point {
|
||||||
|
return { x: a.x + b.x, y: a.y + b.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplies two vectors component-wise
|
||||||
|
* @param {Point} a - The first vector
|
||||||
|
* @param {Point} b - The second vector
|
||||||
|
* @returns {Point} The component-wise product of the two vectors
|
||||||
|
*/
|
||||||
|
static mult(a: Point, b: Point): Point {
|
||||||
|
return { x: a.x * b.x, y: a.y * b.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scales a vector by a scalar value
|
||||||
|
* @param {Point} v - The vector to scale
|
||||||
|
* @param {number} scaleFactor - The scaling factor
|
||||||
|
* @returns {Point} The scaled vector
|
||||||
|
*/
|
||||||
|
static scale(v: Point, scaleFactor: number): Point {
|
||||||
|
return { x: v.x * scaleFactor, y: v.y * scaleFactor };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the magnitude (length) of a vector
|
||||||
|
* @param {Point} v - The vector
|
||||||
|
* @returns {number} The magnitude of the vector
|
||||||
|
*/
|
||||||
|
static mag(v: Point): number {
|
||||||
|
return Math.hypot(v.x, v.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a normalized (unit) vector in the same direction
|
||||||
|
* @param {Point} v - The vector to normalize
|
||||||
|
* @returns {Point} The normalized vector
|
||||||
|
*/
|
||||||
|
static normalized(v: Point): Point {
|
||||||
|
const magnitude = Vector.mag(v);
|
||||||
|
return magnitude === 0 ? Vector.zero() : { x: v.x / magnitude, y: v.y / magnitude };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the Euclidean distance between two points
|
||||||
|
* @param {Point} a - The first point
|
||||||
|
* @param {Point} b - The second point
|
||||||
|
* @returns {number} The distance between the points
|
||||||
|
*/
|
||||||
|
static distance(a: Point, b: Point): number {
|
||||||
|
return Math.hypot(a.x - b.x, a.y - b.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the squared distance between two points
|
||||||
|
* Useful for performance when comparing distances
|
||||||
|
* @param {Point} a - The first point
|
||||||
|
* @param {Point} b - The second point
|
||||||
|
* @returns {number} The squared distance between the points
|
||||||
|
*/
|
||||||
|
static distanceSquared(a: Point, b: Point): number {
|
||||||
|
const dx = a.x - b.x;
|
||||||
|
const dy = a.y - b.y;
|
||||||
|
return dx * dx + dy * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linearly interpolates between two points
|
||||||
|
* @param {Point} a - The starting point
|
||||||
|
* @param {Point} b - The ending point
|
||||||
|
* @param {number} t - The interpolation parameter (0-1)
|
||||||
|
* @returns {Point} The interpolated point
|
||||||
|
*/
|
||||||
|
static lerp(a: Point, b: Point, t: number): Point {
|
||||||
|
return {
|
||||||
|
x: a.x + (b.x - a.x) * t,
|
||||||
|
y: a.y + (b.y - a.y) * t,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates a vector by a given angle (in radians)
|
||||||
|
* @param {Point} v - The vector to rotate
|
||||||
|
* @param {number} angle - The angle in radians
|
||||||
|
* @returns {Point} The rotated vector
|
||||||
|
*/
|
||||||
|
static rotate(v: Point, angle: number): Point {
|
||||||
|
const cos = Math.cos(angle);
|
||||||
|
const sin = Math.sin(angle);
|
||||||
|
return {
|
||||||
|
x: v.x * cos - v.y * sin,
|
||||||
|
y: v.x * sin + v.y * cos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates a point around a pivot point by a given angle (in radians)
|
||||||
|
* @param {Point} point - The point to rotate
|
||||||
|
* @param {Point} pivot - The point to rotate around
|
||||||
|
* @param {number} angle - The angle in radians
|
||||||
|
* @returns {Point} The rotated point
|
||||||
|
*/
|
||||||
|
static rotateAround(point: Point, pivot: Point, angle: number): Point {
|
||||||
|
// Translate to origin
|
||||||
|
const translated = Vector.sub(point, pivot);
|
||||||
|
// Rotate around origin
|
||||||
|
const rotated = Vector.rotate(translated, angle);
|
||||||
|
// Translate back
|
||||||
|
return Vector.add(rotated, pivot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
export type Vector2 = { x: number; y: number };
|
|
||||||
|
|
||||||
export class Vector {
|
|
||||||
static zero: () => Vector2 = () => ({ x: 0, y: 0 });
|
|
||||||
static sub: (a: Vector2, b: Vector2) => Vector2 = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
||||||
static add: (a: Vector2, b: Vector2) => Vector2 = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
||||||
static mult: (a: Vector2, b: Vector2) => Vector2 = (a, b) => ({ x: a.x * b.x, y: a.y * b.y });
|
|
||||||
static scale: (v: Vector2, scaleFactor: number) => Vector2 = (v, scaleFactor) => ({
|
|
||||||
x: v.x * scaleFactor,
|
|
||||||
y: v.y * scaleFactor,
|
|
||||||
});
|
|
||||||
static mag: (v: Vector2) => number = (v) => Math.sqrt(v.x * v.x + v.y * v.y);
|
|
||||||
static normalized: (v: Vector2) => Vector2 = (v) => {
|
|
||||||
const mag = Vector.mag(v);
|
|
||||||
return mag === 0 ? Vector.zero() : { x: v.x / mag, y: v.y / mag };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
export type Point = { x: number; y: number };
|
||||||
|
|
||||||
|
export type RotatedDOMRect = DOMRect & {
|
||||||
|
/** in radians */
|
||||||
|
rotation: number;
|
||||||
|
|
||||||
|
/** Returns the center point in worldspace coordinates */
|
||||||
|
center(): Point;
|
||||||
|
|
||||||
|
/** Returns the four corners in worldspace coordinates, in clockwise order */
|
||||||
|
corners(): [Point, Point, Point, Point];
|
||||||
|
|
||||||
|
/** Returns all the vertices in worldspace coordinates */
|
||||||
|
vertices(): Point[];
|
||||||
|
};
|
||||||
|
|
@ -1,35 +1,21 @@
|
||||||
// Adopted from: https://github.com/pshihn/bezier-points/blob/master/src/index.ts
|
// Adopted from: https://github.com/pshihn/bezier-points/blob/master/src/index.ts
|
||||||
|
|
||||||
export type Point = [number, number];
|
import type { Point } from './types.ts';
|
||||||
|
import { Vector } from './Vector.ts';
|
||||||
export interface Vertex {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// distance between 2 points
|
|
||||||
function distance(p1: Point, p2: Point): number {
|
|
||||||
return Math.sqrt(distanceSq(p1, p2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// distance between 2 points squared
|
|
||||||
function distanceSq(p1: Point, p2: Point): number {
|
|
||||||
return Math.pow(p1[0] - p2[0], 2) + Math.pow(p1[1] - p2[1], 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Distance squared from a point p to the line segment vw
|
// Distance squared from a point p to the line segment vw
|
||||||
function distanceToSegmentSq(p: Point, v: Point, w: Point): number {
|
function distanceToSegmentSq(p: Point, v: Point, w: Point): number {
|
||||||
const l2 = distanceSq(v, w);
|
const l2 = Vector.distanceSquared(v, w);
|
||||||
if (l2 === 0) {
|
if (l2 === 0) {
|
||||||
return distanceSq(p, v);
|
return Vector.distanceSquared(p, v);
|
||||||
}
|
}
|
||||||
let t = ((p[0] - v[0]) * (w[0] - v[0]) + (p[1] - v[1]) * (w[1] - v[1])) / l2;
|
let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
|
||||||
t = Math.max(0, Math.min(1, t));
|
t = Math.max(0, Math.min(1, t));
|
||||||
return distanceSq(p, lerp(v, w, t));
|
return Vector.distanceSquared(p, Vector.lerp(v, w, t));
|
||||||
}
|
}
|
||||||
|
|
||||||
function lerp(a: Point, b: Point, t: number): Point {
|
function lerp(a: Point, b: Point, t: number): Point {
|
||||||
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adapted from https://seant23.wordpress.com/2010/11/12/offset-bezier-curves/
|
// Adapted from https://seant23.wordpress.com/2010/11/12/offset-bezier-curves/
|
||||||
|
|
@ -39,13 +25,13 @@ function flatness(points: readonly Point[], offset: number): number {
|
||||||
const p3 = points[offset + 2];
|
const p3 = points[offset + 2];
|
||||||
const p4 = points[offset + 3];
|
const p4 = points[offset + 3];
|
||||||
|
|
||||||
let ux = 3 * p2[0] - 2 * p1[0] - p4[0];
|
let ux = 3 * p2.x - 2 * p1.x - p4.x;
|
||||||
ux *= ux;
|
ux *= ux;
|
||||||
let uy = 3 * p2[1] - 2 * p1[1] - p4[1];
|
let uy = 3 * p2.y - 2 * p1.y - p4.y;
|
||||||
uy *= uy;
|
uy *= uy;
|
||||||
let vx = 3 * p3[0] - 2 * p4[0] - p1[0];
|
let vx = 3 * p3.x - 2 * p4.x - p1.x;
|
||||||
vx *= vx;
|
vx *= vx;
|
||||||
let vy = 3 * p3[1] - 2 * p4[1] - p1[1];
|
let vy = 3 * p3.y - 2 * p4.y - p1.y;
|
||||||
vy *= vy;
|
vy *= vy;
|
||||||
|
|
||||||
if (ux < vx) {
|
if (ux < vx) {
|
||||||
|
|
@ -69,7 +55,7 @@ function getPointsOnBezierCurveWithSplitting(
|
||||||
if (flatness(points, offset) < tolerance) {
|
if (flatness(points, offset) < tolerance) {
|
||||||
const p0 = points[offset + 0];
|
const p0 = points[offset + 0];
|
||||||
if (outPoints.length) {
|
if (outPoints.length) {
|
||||||
const d = distance(outPoints[outPoints.length - 1], p0);
|
const d = Vector.distance(outPoints[outPoints.length - 1], p0);
|
||||||
if (d > 1) {
|
if (d > 1) {
|
||||||
outPoints.push(p0);
|
outPoints.push(p0);
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +162,7 @@ export function getSvgPathFromStroke(stroke: number[][]): string {
|
||||||
return d.join(' ');
|
return d.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function verticesToPolygon(vertices: Vertex[]): string {
|
export function verticesToPolygon(vertices: Point[]): string {
|
||||||
if (vertices.length === 0) return '';
|
if (vertices.length === 0) return '';
|
||||||
|
|
||||||
return `polygon(${vertices.map((vertex) => `${vertex.x}px ${vertex.y}px`).join(', ')})`;
|
return `polygon(${vertices.map((vertex) => `${vertex.x}px ${vertex.y}px`).join(', ')})`;
|
||||||
|
|
@ -184,7 +170,7 @@ export function verticesToPolygon(vertices: Vertex[]): string {
|
||||||
|
|
||||||
const vertexRegex = /(?<x>-?([0-9]*[.])?[0-9]+),\s*(?<y>-?([0-9]*[.])?[0-9]+)/;
|
const vertexRegex = /(?<x>-?([0-9]*[.])?[0-9]+),\s*(?<y>-?([0-9]*[.])?[0-9]+)/;
|
||||||
|
|
||||||
export function parseVertex(str: string): Vertex | null {
|
export function parseVertex(str: string): Point | null {
|
||||||
const results = vertexRegex.exec(str);
|
const results = vertexRegex.exec(str);
|
||||||
|
|
||||||
if (results === null) return null;
|
if (results === null) return null;
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,10 @@ export class FolkConnection extends AbstractArrow {
|
||||||
) as Arrow;
|
) as Arrow;
|
||||||
|
|
||||||
const points = pointsOnBezierCurves([
|
const points = pointsOnBezierCurves([
|
||||||
[sx, sy],
|
{ x: sx, y: sy },
|
||||||
[cx, cy],
|
{ x: cx, y: cy },
|
||||||
[ex, ey],
|
{ x: ex, y: ey },
|
||||||
[ex, ey],
|
{ x: ex, y: ey },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const stroke = getStroke(points, this.#options);
|
const stroke = getStroke(points, this.#options);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { FolkSet } from './folk-set';
|
import { FolkSet } from './folk-set';
|
||||||
import { Vertex, verticesToPolygon } from './common/utils';
|
import { verticesToPolygon } from './common/utils';
|
||||||
|
import type { Point } from './common/types';
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'folk-hull': FolkHull;
|
'folk-hull': FolkHull;
|
||||||
|
|
@ -10,9 +10,9 @@ declare global {
|
||||||
export class FolkHull extends FolkSet {
|
export class FolkHull extends FolkSet {
|
||||||
static tagName = 'folk-hull';
|
static tagName = 'folk-hull';
|
||||||
|
|
||||||
#hull: Vertex[] = [];
|
#hull: Point[] = [];
|
||||||
|
|
||||||
get hull(): ReadonlyArray<Vertex> {
|
get hull(): ReadonlyArray<Point> {
|
||||||
return this.#hull;
|
return this.#hull;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,7 +50,7 @@ export class FolkHull extends FolkSet {
|
||||||
* If not, see <http://www.gnu.org/licenses/>.
|
* If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function comparePoints(a: Vertex, b: Vertex): number {
|
function comparePoints(a: Point, b: Point): number {
|
||||||
if (a.x < b.x) return -1;
|
if (a.x < b.x) return -1;
|
||||||
if (a.x > b.x) return 1;
|
if (a.x > b.x) return 1;
|
||||||
if (a.y < b.y) return -1;
|
if (a.y < b.y) return -1;
|
||||||
|
|
@ -58,8 +58,8 @@ function comparePoints(a: Vertex, b: Vertex): number {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeHull(rects: DOMRectReadOnly[]): Vertex[] {
|
export function makeHull(rects: DOMRectReadOnly[]): Point[] {
|
||||||
const points: Vertex[] = rects
|
const points: Point[] = rects
|
||||||
.flatMap((rect) => [
|
.flatMap((rect) => [
|
||||||
{ x: rect.left, y: rect.top },
|
{ x: rect.left, y: rect.top },
|
||||||
{ x: rect.right, y: rect.top },
|
{ x: rect.right, y: rect.top },
|
||||||
|
|
@ -74,12 +74,12 @@ export function makeHull(rects: DOMRectReadOnly[]): Vertex[] {
|
||||||
// as per the mathematical convention, instead of "down" as per the computer
|
// as per the mathematical convention, instead of "down" as per the computer
|
||||||
// graphics convention. This doesn't affect the correctness of the result.
|
// graphics convention. This doesn't affect the correctness of the result.
|
||||||
|
|
||||||
const upperHull: Array<Vertex> = [];
|
const upperHull: Array<Point> = [];
|
||||||
for (let i = 0; i < points.length; i++) {
|
for (let i = 0; i < points.length; i++) {
|
||||||
const p: Vertex = points[i];
|
const p: Point = points[i];
|
||||||
while (upperHull.length >= 2) {
|
while (upperHull.length >= 2) {
|
||||||
const q: Vertex = upperHull[upperHull.length - 1];
|
const q: Point = upperHull[upperHull.length - 1];
|
||||||
const r: Vertex = upperHull[upperHull.length - 2];
|
const r: Point = upperHull[upperHull.length - 2];
|
||||||
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) upperHull.pop();
|
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) upperHull.pop();
|
||||||
else break;
|
else break;
|
||||||
}
|
}
|
||||||
|
|
@ -87,12 +87,12 @@ export function makeHull(rects: DOMRectReadOnly[]): Vertex[] {
|
||||||
}
|
}
|
||||||
upperHull.pop();
|
upperHull.pop();
|
||||||
|
|
||||||
const lowerHull: Array<Vertex> = [];
|
const lowerHull: Array<Point> = [];
|
||||||
for (let i = points.length - 1; i >= 0; i--) {
|
for (let i = points.length - 1; i >= 0; i--) {
|
||||||
const p: Vertex = points[i];
|
const p: Point = points[i];
|
||||||
while (lowerHull.length >= 2) {
|
while (lowerHull.length >= 2) {
|
||||||
const q: Vertex = lowerHull[lowerHull.length - 1];
|
const q: Point = lowerHull[lowerHull.length - 1];
|
||||||
const r: Vertex = lowerHull[lowerHull.length - 2];
|
const r: Point = lowerHull[lowerHull.length - 2];
|
||||||
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) lowerHull.pop();
|
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) lowerHull.pop();
|
||||||
else break;
|
else break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,11 @@ export class FolkLLM extends HTMLElement {
|
||||||
this.#update(new Set(['systemPrompt', 'prompt']));
|
this.#update(new Set(['systemPrompt', 'prompt']));
|
||||||
}
|
}
|
||||||
|
|
||||||
#session;
|
#session: any;
|
||||||
|
|
||||||
#isModelReady = window?.ai.languageModel.capabilities().then((capabilities) => capabilities.available === 'readily');
|
#isModelReady = window?.ai.languageModel
|
||||||
|
.capabilities()
|
||||||
|
.then((capabilities: any) => capabilities.available === 'readily');
|
||||||
|
|
||||||
#systemPrompt: Prompt = this.getAttribute('system-prompt') || '';
|
#systemPrompt: Prompt = this.getAttribute('system-prompt') || '';
|
||||||
get systemPrompt() {
|
get systemPrompt() {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
// This is a rewrite of https://github.com/guerrillacontra/html5-es6-physics-rope
|
// This is a rewrite of https://github.com/guerrillacontra/html5-es6-physics-rope
|
||||||
|
|
||||||
import { Vector, type Vector2 } from './common/Vector2.ts';
|
import { Vector } from './common/Vector.ts';
|
||||||
|
import type { Point } from './common/types.ts';
|
||||||
import { AbstractArrow } from './abstract-arrow.ts';
|
import { AbstractArrow } from './abstract-arrow.ts';
|
||||||
import { Vertex } from './common/utils.ts';
|
|
||||||
|
|
||||||
const lerp = (first: number, second: number, percentage: number) => first + (second - first) * percentage;
|
const lerp = (first: number, second: number, percentage: number) => first + (second - first) * percentage;
|
||||||
|
|
||||||
// Each rope part is one of these uses a high precision variant of Störmer–Verlet integration to keep the simulation consistent otherwise it would "explode"!
|
// Each rope part is one of these uses a high precision variant of Störmer–Verlet integration to keep the simulation consistent otherwise it would "explode"!
|
||||||
interface RopePoint {
|
interface RopePoint {
|
||||||
pos: Vertex;
|
pos: Point;
|
||||||
distanceToNextPoint: number;
|
distanceToNextPoint: number;
|
||||||
isFixed: boolean;
|
isFixed: boolean;
|
||||||
oldPos: Vertex;
|
oldPos: Point;
|
||||||
velocity: Vertex;
|
velocity: Point;
|
||||||
mass: number;
|
mass: number;
|
||||||
damping: number;
|
damping: number;
|
||||||
prev: RopePoint | null;
|
prev: RopePoint | null;
|
||||||
|
|
@ -162,7 +162,7 @@ export class FolkRope extends AbstractArrow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#generatePoints(start: Vertex, end: Vertex) {
|
#generatePoints(start: Point, end: Point) {
|
||||||
const delta = Vector.sub(end, start);
|
const delta = Vector.sub(end, start);
|
||||||
const len = Vector.mag(delta);
|
const len = Vector.mag(delta);
|
||||||
const resolution = 5;
|
const resolution = 5;
|
||||||
|
|
@ -202,7 +202,7 @@ export class FolkRope extends AbstractArrow {
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
#integratePoint(point: RopePoint, gravity: Vector2) {
|
#integratePoint(point: RopePoint, gravity: Point) {
|
||||||
if (!point.isFixed) {
|
if (!point.isFixed) {
|
||||||
point.velocity = Vector.sub(point.pos, point.oldPos);
|
point.velocity = Vector.sub(point.pos, point.oldPos);
|
||||||
point.oldPos = { ...point.pos };
|
point.oldPos = { ...point.pos };
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,12 @@
|
||||||
import { css, html } from './common/tags';
|
import { css, html } from './common/tags';
|
||||||
import { ResizeObserverManager } from './common/resize-observer';
|
import { ResizeObserverManager } from './common/resize-observer';
|
||||||
import type { Vector2 } from './common/Vector2';
|
import type { Point, RotatedDOMRect } from './common/types';
|
||||||
|
import { Vector } from './common/Vector';
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserverManager();
|
const resizeObserver = new ResizeObserverManager();
|
||||||
|
|
||||||
export type Shape = 'rectangle' | 'circle' | 'triangle';
|
export type Shape = 'rectangle' | 'circle' | 'triangle';
|
||||||
|
|
||||||
type RotatedDOMRect = DOMRect & {
|
|
||||||
// In degrees
|
|
||||||
rotation: number;
|
|
||||||
|
|
||||||
// Returns the center point in worldspace coordinates
|
|
||||||
center(): Vector2;
|
|
||||||
|
|
||||||
// Returns the four corners in worldspace coordinates, in clockwise order
|
|
||||||
corners(): [Vector2, Vector2, Vector2, Vector2];
|
|
||||||
|
|
||||||
// Returns all the vertices in worldspace coordinates
|
|
||||||
vertices(): Vector2[];
|
|
||||||
};
|
|
||||||
export type MoveEventDetail = { movementX: number; movementY: number };
|
export type MoveEventDetail = { movementX: number; movementY: number };
|
||||||
|
|
||||||
export class MoveEvent extends CustomEvent<MoveEventDetail> {
|
export class MoveEvent extends CustomEvent<MoveEventDetail> {
|
||||||
|
|
@ -133,7 +121,7 @@ styles.replaceSync(css`
|
||||||
cursor: var(--fc-nesw-resize, nesw-resize);
|
cursor: var(--fc-nesw-resize, nesw-resize);
|
||||||
}
|
}
|
||||||
|
|
||||||
[part='rotate'] {
|
[part='rotation'] {
|
||||||
z-index: calc(infinity);
|
z-index: calc(infinity);
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -153,7 +141,7 @@ styles.replaceSync(css`
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(:not(:focus-within)) [part^='resize'],
|
:host(:not(:focus-within)) [part^='resize'],
|
||||||
:host(:not(:focus-within)) [part='rotate'] {
|
:host(:not(:focus-within)) [part='rotation'] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
@ -255,8 +243,8 @@ export class FolkShape extends HTMLElement {
|
||||||
#startAngle = 0;
|
#startAngle = 0;
|
||||||
#previousRotation = 0;
|
#previousRotation = 0;
|
||||||
|
|
||||||
// TODO: consider using radians instead of degrees
|
// use degrees in the DOM, but store in radians internally
|
||||||
#rotation = Number(this.getAttribute('rotate')) || 0;
|
#rotation = (Number(this.getAttribute('rotation')) || 0) * (Math.PI / 180);
|
||||||
|
|
||||||
get rotation(): number {
|
get rotation(): number {
|
||||||
return this.#rotation;
|
return this.#rotation;
|
||||||
|
|
@ -265,7 +253,7 @@ export class FolkShape extends HTMLElement {
|
||||||
set rotation(rotation: number) {
|
set rotation(rotation: number) {
|
||||||
this.#previousRotation = this.#rotation;
|
this.#previousRotation = this.#rotation;
|
||||||
this.#rotation = rotation;
|
this.#rotation = rotation;
|
||||||
this.#requestUpdate('rotate');
|
this.#requestUpdate('rotation');
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -281,7 +269,7 @@ export class FolkShape extends HTMLElement {
|
||||||
// Ideally we would creating these lazily on first focus, but the resize handlers need to be around for delegate focus to work.
|
// Ideally we would creating these lazily on first focus, but the resize handlers need to be around for delegate focus to work.
|
||||||
// Maybe can add the first resize handler here, and lazily instantiate the rest when needed?
|
// Maybe can add the first resize handler here, and lazily instantiate the rest when needed?
|
||||||
// I can see it becoming important at scale
|
// I can see it becoming important at scale
|
||||||
shadowRoot.innerHTML = html` <button part="rotate"></button>
|
shadowRoot.innerHTML = html` <button part="rotation"></button>
|
||||||
<button part="resize-nw"></button>
|
<button part="resize-nw"></button>
|
||||||
<button part="resize-ne"></button>
|
<button part="resize-ne"></button>
|
||||||
<button part="resize-se"></button>
|
<button part="resize-se"></button>
|
||||||
|
|
@ -293,12 +281,11 @@ export class FolkShape extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.#update(new Set(['type', 'x', 'y', 'height', 'width', 'rotate']));
|
this.#update(new Set(['type', 'x', 'y', 'height', 'width', 'rotation']));
|
||||||
}
|
}
|
||||||
|
|
||||||
getClientRect(): RotatedDOMRect {
|
getClientRect(): RotatedDOMRect {
|
||||||
const { x, y, width, height, rotation } = this;
|
const { x, y, width, height, rotation } = this;
|
||||||
const radians = (rotation * Math.PI) / 180;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x,
|
x,
|
||||||
|
|
@ -311,37 +298,26 @@ export class FolkShape extends HTMLElement {
|
||||||
bottom: y + height,
|
bottom: y + height,
|
||||||
rotation,
|
rotation,
|
||||||
|
|
||||||
center(): Vector2 {
|
center(): Point {
|
||||||
return {
|
return {
|
||||||
x: this.x + this.width / 2,
|
x: this.x + this.width / 2,
|
||||||
y: this.y + this.height / 2,
|
y: this.y + this.height / 2,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
vertices(): Vector2[] {
|
vertices(): Point[] {
|
||||||
// TODO: Implement
|
// TODO: Implement
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|
||||||
corners(): [Vector2, Vector2, Vector2, Vector2] {
|
corners() {
|
||||||
const center = this.center();
|
const center = this.center();
|
||||||
const cos = Math.cos(radians);
|
const { x, y, width, height, rotation } = this;
|
||||||
const sin = Math.sin(radians);
|
|
||||||
|
|
||||||
const halfWidth = this.width / 2;
|
|
||||||
const halfHeight = this.height / 2;
|
|
||||||
|
|
||||||
// Helper to rotate a point around the center
|
|
||||||
const rotatePoint = (dx: number, dy: number): Vector2 => ({
|
|
||||||
x: center.x + dx * cos - dy * sin,
|
|
||||||
y: center.y + dx * sin + dy * cos,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return vertices in clockwise order: top-left, top-right, bottom-right, bottom-left
|
|
||||||
return [
|
return [
|
||||||
rotatePoint(-halfWidth, -halfHeight), // Top-left
|
Vector.rotateAround({ x, y }, center, rotation),
|
||||||
rotatePoint(halfWidth, -halfHeight), // Top-right
|
Vector.rotateAround({ x: x + width, y }, center, rotation),
|
||||||
rotatePoint(halfWidth, halfHeight), // Bottom-right
|
Vector.rotateAround({ x: x + width, y: y + height }, center, rotation),
|
||||||
rotatePoint(-halfWidth, halfHeight), // Bottom-left
|
Vector.rotateAround({ x, y: y + height }, center, rotation),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -367,7 +343,7 @@ export class FolkShape extends HTMLElement {
|
||||||
const target = event.composedPath()[0] as HTMLElement;
|
const target = event.composedPath()[0] as HTMLElement;
|
||||||
|
|
||||||
// Store initial angle on rotation start
|
// Store initial angle on rotation start
|
||||||
if (target.getAttribute('part') === 'rotate') {
|
if (target.getAttribute('part') === 'rotation') {
|
||||||
// We need to store initial rotation/angle somewhere.
|
// We need to store initial rotation/angle somewhere.
|
||||||
// This is a little awkward as we'll want to do *quite a lot* of this kind of thing.
|
// This is a little awkward as we'll want to do *quite a lot* of this kind of thing.
|
||||||
// Might be an argument for making elements dumber (i.e. not have them manage their own state) and do this from the outside.
|
// Might be an argument for making elements dumber (i.e. not have them manage their own state) and do this from the outside.
|
||||||
|
|
@ -450,13 +426,13 @@ export class FolkShape extends HTMLElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part === 'rotate') {
|
if (part === 'rotation') {
|
||||||
const centerX = this.#x + this.width / 2;
|
const centerX = this.#x + this.width / 2;
|
||||||
const centerY = this.#y + this.height / 2;
|
const centerY = this.#y + this.height / 2;
|
||||||
const currentAngle = Math.atan2(event.clientY - centerY, event.clientX - centerX);
|
const currentAngle = Math.atan2(event.clientY - centerY, event.clientX - centerX);
|
||||||
|
|
||||||
const deltaAngle = currentAngle - this.#startAngle;
|
const deltaAngle = currentAngle - this.#startAngle;
|
||||||
this.rotation = this.#initialRotation + (deltaAngle * 180) / Math.PI;
|
this.rotation = this.#initialRotation + deltaAngle;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -544,13 +520,13 @@ export class FolkShape extends HTMLElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedProperties.has('rotate')) {
|
if (updatedProperties.has('rotation')) {
|
||||||
// Although the change in resize isn't useful inside this component, the outside world might find it helpful to calculate acceleration and other physics
|
// Although the change in resize isn't useful inside this component, the outside world might find it helpful to calculate acceleration and other physics
|
||||||
const notCancelled = this.dispatchEvent(new RotateEvent({ rotate: this.#rotation - this.#previousRotation }));
|
const notCancelled = this.dispatchEvent(new RotateEvent({ rotate: this.#rotation - this.#previousRotation }));
|
||||||
|
|
||||||
if (notCancelled) {
|
if (notCancelled) {
|
||||||
if (updatedProperties.has('rotate')) {
|
if (updatedProperties.has('rotation')) {
|
||||||
this.style.rotate = `${this.#rotation}deg`;
|
this.style.rotate = `${this.#rotation}rad`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.#rotation = this.#previousRotation;
|
this.#rotation = this.#previousRotation;
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ export class FolkSpreadsheet extends HTMLElement {
|
||||||
|
|
||||||
#shadow = this.attachShadow({ mode: 'open' });
|
#shadow = this.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
#textarea;
|
#textarea: HTMLTextAreaElement | null = null;
|
||||||
|
|
||||||
#editedCell: FolkSpreadSheetCell | null = null;
|
#editedCell: FolkSpreadSheetCell | null = null;
|
||||||
|
|
||||||
|
|
@ -315,6 +315,7 @@ export class FolkSpreadsheet extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
#focusTextarea(cell: FolkSpreadSheetCell) {
|
#focusTextarea(cell: FolkSpreadSheetCell) {
|
||||||
|
if (this.#textarea === null) return;
|
||||||
this.#editedCell = cell;
|
this.#editedCell = cell;
|
||||||
const gridColumn = getColumnIndex(cell.column) + 2;
|
const gridColumn = getColumnIndex(cell.column) + 2;
|
||||||
const gridRow = cell.row + 1;
|
const gridRow = cell.row + 1;
|
||||||
|
|
@ -327,6 +328,7 @@ export class FolkSpreadsheet extends HTMLElement {
|
||||||
|
|
||||||
#resetTextarea() {
|
#resetTextarea() {
|
||||||
if (this.#editedCell === null) return;
|
if (this.#editedCell === null) return;
|
||||||
|
if (this.#textarea === null) return;
|
||||||
this.#textarea.style.setProperty('--text-column', '0');
|
this.#textarea.style.setProperty('--text-column', '0');
|
||||||
this.#textarea.style.setProperty('--text-row', '0');
|
this.#textarea.style.setProperty('--text-row', '0');
|
||||||
this.#editedCell.expression = this.#textarea.value;
|
this.#editedCell.expression = this.#textarea.value;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export class FolkTimer extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
#timeMs = 0;
|
#timeMs = 0;
|
||||||
#timeoutId = -1;
|
#timeoutId: NodeJS.Timeout | -1 = -1;
|
||||||
|
|
||||||
#intervalMs = 100;
|
#intervalMs = 100;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export class FolkWeather extends HTMLElement {
|
||||||
|
|
||||||
static observedAttributes = ['coordinates'];
|
static observedAttributes = ['coordinates'];
|
||||||
|
|
||||||
#coordinates = [0, 0] as const;
|
#coordinates: readonly [number, number] = [0, 0];
|
||||||
#results: Weather | null = null;
|
#results: Weather | null = null;
|
||||||
|
|
||||||
get coordinates() {
|
get coordinates() {
|
||||||
|
|
@ -30,9 +30,10 @@ export class FolkWeather extends HTMLElement {
|
||||||
this.setAttribute('coordinates', coordinates.join(', '));
|
this.setAttribute('coordinates', coordinates.join(', '));
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
||||||
if (name === 'coordinates') {
|
if (name === 'coordinates') {
|
||||||
this.#coordinates = newValue.split(',').map((str) => Number(str)) || [0, 0];
|
const [lat = 0, long = 0] = newValue.split(',').map((str) => Number(str));
|
||||||
|
this.#coordinates = [lat, long] as const;
|
||||||
this.fetchWeather(this.#coordinates);
|
this.fetchWeather(this.#coordinates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { AbstractArrow } from './abstract-arrow.js';
|
import { AbstractArrow } from './abstract-arrow.js';
|
||||||
import { Vertex, verticesToPolygon } from './common/utils.js';
|
import { verticesToPolygon } from './common/utils.js';
|
||||||
|
import type { Point } from './common/types.js';
|
||||||
export class FolkXanadu extends AbstractArrow {
|
export class FolkXanadu extends AbstractArrow {
|
||||||
static tagName = 'folk-xanadu';
|
static tagName = 'folk-xanadu';
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ export class FolkXanadu extends AbstractArrow {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The order that vertices are returned is significant
|
// The order that vertices are returned is significant
|
||||||
function computeInlineVertices(rects: DOMRect[]): Vertex[] {
|
function computeInlineVertices(rects: DOMRect[]): Point[] {
|
||||||
rects = rects.map((rect) =>
|
rects = rects.map((rect) =>
|
||||||
DOMRectReadOnly.fromRect({
|
DOMRectReadOnly.fromRect({
|
||||||
height: Math.round(rect.height),
|
height: Math.round(rect.height),
|
||||||
|
|
@ -68,7 +68,7 @@ function computeInlineVertices(rects: DOMRect[]): Vertex[] {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const vertices: Vertex[] = [];
|
const vertices: Point[] = [];
|
||||||
|
|
||||||
if (rects[1].left < rects[0].left) {
|
if (rects[1].left < rects[0].left) {
|
||||||
vertices.push({ x: rects[1].left, y: rects[1].top }, { x: rects[0].left, y: rects[0].bottom });
|
vertices.push({ x: rects[1].left, y: rects[1].top }, { x: rects[0].left, y: rects[0].bottom });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue