This commit is contained in:
“chrisshank” 2024-12-02 16:16:50 -08:00
commit a5b5d390dd
15 changed files with 236 additions and 132 deletions

View File

@ -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';

View File

@ -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",

138
src/common/Vector.ts Normal file
View File

@ -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);
}
}

View File

@ -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 };
};
}

15
src/common/types.ts Normal file
View File

@ -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[];
};

View File

@ -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;

View File

@ -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);

View File

@ -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;
} }

View File

@ -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() {

View File

@ -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örmerVerlet integration to keep the simulation consistent otherwise it would "explode"! // Each rope part is one of these uses a high precision variant of StörmerVerlet 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 };

View File

@ -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;

View File

@ -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;

View File

@ -12,7 +12,7 @@ export class FolkTimer extends HTMLElement {
} }
#timeMs = 0; #timeMs = 0;
#timeoutId = -1; #timeoutId: NodeJS.Timeout | -1 = -1;
#intervalMs = 100; #intervalMs = 100;

View File

@ -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);
} }
} }

View File

@ -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 });