update types
This commit is contained in:
parent
b1e20bfd3f
commit
7969bdb706
|
|
@ -4,8 +4,9 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build --base=/folk-canvas",
|
||||
"preview": "vite build && vite preview"
|
||||
"build": "tsc --noEmit && vite build --base=/folk-canvas",
|
||||
"preview": "tsc --noEmit && vite build && vite preview",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.26.2",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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 @@
|
|||
export type Point = { x: number; y: number };
|
||||
|
|
@ -1,35 +1,21 @@
|
|||
// Adopted from: https://github.com/pshihn/bezier-points/blob/master/src/index.ts
|
||||
|
||||
export type Point = [number, number];
|
||||
|
||||
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);
|
||||
}
|
||||
import type { Point } from './types.ts';
|
||||
import { Vector } from './Vector.ts';
|
||||
|
||||
// Distance squared from a point p to the line segment vw
|
||||
function distanceToSegmentSq(p: Point, v: Point, w: Point): number {
|
||||
const l2 = distanceSq(v, w);
|
||||
const l2 = Vector.distanceSquared(v, w);
|
||||
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));
|
||||
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 {
|
||||
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/
|
||||
|
|
@ -39,13 +25,13 @@ function flatness(points: readonly Point[], offset: number): number {
|
|||
const p3 = points[offset + 2];
|
||||
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;
|
||||
let uy = 3 * p2[1] - 2 * p1[1] - p4[1];
|
||||
let uy = 3 * p2.y - 2 * p1.y - p4.y;
|
||||
uy *= uy;
|
||||
let vx = 3 * p3[0] - 2 * p4[0] - p1[0];
|
||||
let vx = 3 * p3.x - 2 * p4.x - p1.x;
|
||||
vx *= vx;
|
||||
let vy = 3 * p3[1] - 2 * p4[1] - p1[1];
|
||||
let vy = 3 * p3.y - 2 * p4.y - p1.y;
|
||||
vy *= vy;
|
||||
|
||||
if (ux < vx) {
|
||||
|
|
@ -69,7 +55,7 @@ function getPointsOnBezierCurveWithSplitting(
|
|||
if (flatness(points, offset) < tolerance) {
|
||||
const p0 = points[offset + 0];
|
||||
if (outPoints.length) {
|
||||
const d = distance(outPoints[outPoints.length - 1], p0);
|
||||
const d = Vector.distance(outPoints[outPoints.length - 1], p0);
|
||||
if (d > 1) {
|
||||
outPoints.push(p0);
|
||||
}
|
||||
|
|
@ -176,7 +162,7 @@ export function getSvgPathFromStroke(stroke: number[][]): string {
|
|||
return d.join(' ');
|
||||
}
|
||||
|
||||
export function verticesToPolygon(vertices: Vertex[]): string {
|
||||
export function verticesToPolygon(vertices: Point[]): string {
|
||||
if (vertices.length === 0) return '';
|
||||
|
||||
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]+)/;
|
||||
|
||||
export function parseVertex(str: string): Vertex | null {
|
||||
export function parseVertex(str: string): Point | null {
|
||||
const results = vertexRegex.exec(str);
|
||||
|
||||
if (results === null) return null;
|
||||
|
|
|
|||
|
|
@ -73,10 +73,10 @@ export class FolkConnection extends AbstractArrow {
|
|||
) as Arrow;
|
||||
|
||||
const points = pointsOnBezierCurves([
|
||||
[sx, sy],
|
||||
[cx, cy],
|
||||
[ex, ey],
|
||||
[ex, ey],
|
||||
{ x: sx, y: sy },
|
||||
{ x: cx, y: cy },
|
||||
{ x: ex, y: ey },
|
||||
{ x: ex, y: ey },
|
||||
]);
|
||||
|
||||
const stroke = getStroke(points, this.#options);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { FolkSet } from './folk-set';
|
||||
import { Vertex, verticesToPolygon } from './common/utils';
|
||||
|
||||
import { verticesToPolygon } from './common/utils';
|
||||
import type { Point } from './common/types';
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'folk-hull': FolkHull;
|
||||
|
|
@ -10,9 +10,9 @@ declare global {
|
|||
export class FolkHull extends FolkSet {
|
||||
static tagName = 'folk-hull';
|
||||
|
||||
#hull: Vertex[] = [];
|
||||
#hull: Point[] = [];
|
||||
|
||||
get hull(): ReadonlyArray<Vertex> {
|
||||
get hull(): ReadonlyArray<Point> {
|
||||
return this.#hull;
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ export class FolkHull extends FolkSet {
|
|||
* 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.y < b.y) return -1;
|
||||
|
|
@ -58,8 +58,8 @@ function comparePoints(a: Vertex, b: Vertex): number {
|
|||
return 0;
|
||||
}
|
||||
|
||||
export function makeHull(rects: DOMRectReadOnly[]): Vertex[] {
|
||||
const points: Vertex[] = rects
|
||||
export function makeHull(rects: DOMRectReadOnly[]): Point[] {
|
||||
const points: Point[] = rects
|
||||
.flatMap((rect) => [
|
||||
{ x: rect.left, 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
|
||||
// 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++) {
|
||||
const p: Vertex = points[i];
|
||||
const p: Point = points[i];
|
||||
while (upperHull.length >= 2) {
|
||||
const q: Vertex = upperHull[upperHull.length - 1];
|
||||
const r: Vertex = upperHull[upperHull.length - 2];
|
||||
const q: Point = upperHull[upperHull.length - 1];
|
||||
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();
|
||||
else break;
|
||||
}
|
||||
|
|
@ -87,12 +87,12 @@ export function makeHull(rects: DOMRectReadOnly[]): Vertex[] {
|
|||
}
|
||||
upperHull.pop();
|
||||
|
||||
const lowerHull: Array<Vertex> = [];
|
||||
const lowerHull: Array<Point> = [];
|
||||
for (let i = points.length - 1; i >= 0; i--) {
|
||||
const p: Vertex = points[i];
|
||||
const p: Point = points[i];
|
||||
while (lowerHull.length >= 2) {
|
||||
const q: Vertex = lowerHull[lowerHull.length - 1];
|
||||
const r: Vertex = lowerHull[lowerHull.length - 2];
|
||||
const q: Point = lowerHull[lowerHull.length - 1];
|
||||
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();
|
||||
else break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@ export class FolkLLM extends HTMLElement {
|
|||
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') || '';
|
||||
get systemPrompt() {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
// 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 { Vertex } from './common/utils.ts';
|
||||
|
||||
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"!
|
||||
interface RopePoint {
|
||||
pos: Vertex;
|
||||
pos: Point;
|
||||
distanceToNextPoint: number;
|
||||
isFixed: boolean;
|
||||
oldPos: Vertex;
|
||||
velocity: Vertex;
|
||||
oldPos: Point;
|
||||
velocity: Point;
|
||||
mass: number;
|
||||
damping: number;
|
||||
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 len = Vector.mag(delta);
|
||||
const resolution = 5;
|
||||
|
|
@ -202,7 +202,7 @@ export class FolkRope extends AbstractArrow {
|
|||
return points;
|
||||
}
|
||||
|
||||
#integratePoint(point: RopePoint, gravity: Vector2) {
|
||||
#integratePoint(point: RopePoint, gravity: Point) {
|
||||
if (!point.isFixed) {
|
||||
point.velocity = Vector.sub(point.pos, point.oldPos);
|
||||
point.oldPos = { ...point.pos };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { css, html } from './common/tags';
|
||||
import { ResizeObserverManager } from './common/resize-observer';
|
||||
import type { Vector2 } from './common/Vector2';
|
||||
import type { Point } from './common/types';
|
||||
|
||||
const resizeObserver = new ResizeObserverManager();
|
||||
|
||||
|
|
@ -11,13 +11,13 @@ type RotatedDOMRect = DOMRect & {
|
|||
rotation: number;
|
||||
|
||||
// Returns the center point in worldspace coordinates
|
||||
center(): Vector2;
|
||||
center(): Point;
|
||||
|
||||
// Returns the four corners in worldspace coordinates, in clockwise order
|
||||
corners(): [Vector2, Vector2, Vector2, Vector2];
|
||||
corners(): [Point, Point, Point, Point];
|
||||
|
||||
// Returns all the vertices in worldspace coordinates
|
||||
vertices(): Vector2[];
|
||||
vertices(): Point[];
|
||||
};
|
||||
export type MoveEventDetail = { movementX: number; movementY: number };
|
||||
|
||||
|
|
@ -311,18 +311,18 @@ export class FolkShape extends HTMLElement {
|
|||
bottom: y + height,
|
||||
rotation,
|
||||
|
||||
center(): Vector2 {
|
||||
center(): Point {
|
||||
return {
|
||||
x: this.x + this.width / 2,
|
||||
y: this.y + this.height / 2,
|
||||
};
|
||||
},
|
||||
vertices(): Vector2[] {
|
||||
vertices(): Point[] {
|
||||
// TODO: Implement
|
||||
return [];
|
||||
},
|
||||
|
||||
corners(): [Vector2, Vector2, Vector2, Vector2] {
|
||||
corners(): [Point, Point, Point, Point] {
|
||||
const center = this.center();
|
||||
const cos = Math.cos(radians);
|
||||
const sin = Math.sin(radians);
|
||||
|
|
@ -331,7 +331,7 @@ export class FolkShape extends HTMLElement {
|
|||
const halfHeight = this.height / 2;
|
||||
|
||||
// Helper to rotate a point around the center
|
||||
const rotatePoint = (dx: number, dy: number): Vector2 => ({
|
||||
const rotatePoint = (dx: number, dy: number): Point => ({
|
||||
x: center.x + dx * cos - dy * sin,
|
||||
y: center.y + dx * sin + dy * cos,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export class FolkSpreadsheet extends HTMLElement {
|
|||
|
||||
#shadow = this.attachShadow({ mode: 'open' });
|
||||
|
||||
#textarea;
|
||||
#textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
#editedCell: FolkSpreadSheetCell | null = null;
|
||||
|
||||
|
|
@ -315,6 +315,7 @@ export class FolkSpreadsheet extends HTMLElement {
|
|||
}
|
||||
|
||||
#focusTextarea(cell: FolkSpreadSheetCell) {
|
||||
if (this.#textarea === null) return;
|
||||
this.#editedCell = cell;
|
||||
const gridColumn = getColumnIndex(cell.column) + 2;
|
||||
const gridRow = cell.row + 1;
|
||||
|
|
@ -327,6 +328,7 @@ export class FolkSpreadsheet extends HTMLElement {
|
|||
|
||||
#resetTextarea() {
|
||||
if (this.#editedCell === null) return;
|
||||
if (this.#textarea === null) return;
|
||||
this.#textarea.style.setProperty('--text-column', '0');
|
||||
this.#textarea.style.setProperty('--text-row', '0');
|
||||
this.#editedCell.expression = this.#textarea.value;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export class FolkTimer extends HTMLElement {
|
|||
}
|
||||
|
||||
#timeMs = 0;
|
||||
#timeoutId = -1;
|
||||
#timeoutId: NodeJS.Timeout | -1 = -1;
|
||||
|
||||
#intervalMs = 100;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export class FolkWeather extends HTMLElement {
|
|||
|
||||
static observedAttributes = ['coordinates'];
|
||||
|
||||
#coordinates = [0, 0] as const;
|
||||
#coordinates: readonly [number, number] = [0, 0];
|
||||
#results: Weather | null = null;
|
||||
|
||||
get coordinates() {
|
||||
|
|
@ -30,9 +30,10 @@ export class FolkWeather extends HTMLElement {
|
|||
this.setAttribute('coordinates', coordinates.join(', '));
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 {
|
||||
static tagName = 'folk-xanadu';
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ export class FolkXanadu extends AbstractArrow {
|
|||
}
|
||||
|
||||
// The order that vertices are returned is significant
|
||||
function computeInlineVertices(rects: DOMRect[]): Vertex[] {
|
||||
function computeInlineVertices(rects: DOMRect[]): Point[] {
|
||||
rects = rects.map((rect) =>
|
||||
DOMRectReadOnly.fromRect({
|
||||
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) {
|
||||
vertices.push({ x: rects[1].left, y: rects[1].top }, { x: rects[0].left, y: rects[0].bottom });
|
||||
|
|
|
|||
Loading…
Reference in New Issue