Something went wrong. Try again.
[PRE-ALPHA] an open source web-based 2D/3D graphing calculator cartesium.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165import type { Viewport } from '#lib/engine/types.js';import { mathToPixel, pixelToMath, type PixelPoint } from '#lib/graph/viewport.js';
export interface GraphTextElement { id: string; source: string; /** Absent in older documents; those text elements remain visible. */ enabled?: boolean; /** Graph coordinate of the selected anchor point on the display box. */ position: { x: number; y: number }; anchor: GraphTextAnchor; /** Display dimensions in CSS pixels; these do not scale with the graph camera. */ widthPx: number; heightPx: number; fontSizePx: number; /** Null follows the graph theme; explicit colors apply to text and math. */ color: string | null; align: 'left' | 'center' | 'right';}
export function isGraphTextEnabled(text: Pick<GraphTextElement, 'enabled'>): boolean { return text.enabled !== false;}
export type GraphTextHorizontalAnchor = 'left' | 'center' | 'right';
export type GraphTextVerticalAnchor = 'top' | 'middle' | 'bottom';
export interface GraphTextAnchor { horizontal: GraphTextHorizontalAnchor; vertical: GraphTextVerticalAnchor;}
export const GRAPH_TEXT_HORIZONTAL_ANCHORS = ['left', 'center', 'right'] as const;export const GRAPH_TEXT_VERTICAL_ANCHORS = ['top', 'middle', 'bottom'] as const;
export const DEFAULT_GRAPH_TEXT_ANCHOR = { horizontal: 'center', vertical: 'middle'} as const satisfies GraphTextAnchor;
export interface GraphTextScreenBounds { left: number; top: number; width: number; height: number;}
export const DEFAULT_GRAPH_TEXT_WIDTH_PX = 240;export const DEFAULT_GRAPH_TEXT_HEIGHT_PX = 96;export const DEFAULT_GRAPH_TEXT_FONT_SIZE_PX = 16;export const MIN_GRAPH_TEXT_WIDTH_PX = 80;export const MAX_GRAPH_TEXT_WIDTH_PX = 2_000;export const MIN_GRAPH_TEXT_HEIGHT_PX = 40;export const MAX_GRAPH_TEXT_HEIGHT_PX = 2_000;export const MIN_GRAPH_TEXT_FONT_SIZE_PX = 8;export const MAX_GRAPH_TEXT_FONT_SIZE_PX = 96;export const MAX_GRAPH_TEXT_COORDINATE = 1e12;
export function isGraphTextColor(value: unknown): value is string { return typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value);}
export function graphTextScreenBounds( text: Pick<GraphTextElement, 'position' | 'anchor' | 'widthPx' | 'heightPx'>, viewport: Viewport): GraphTextScreenBounds { const anchor = mathToPixel(viewport, text.position.x, text.position.y);
return { left: anchor.x - text.widthPx * horizontalAnchorFraction(text.anchor.horizontal), top: anchor.y - text.heightPx * verticalAnchorFraction(text.anchor.vertical), width: text.widthPx, height: text.heightPx };}
/** Keep the current box in place while changing which point stores its graph coordinate. */export function setGraphTextAnchor( text: GraphTextElement, anchor: GraphTextAnchor, viewport: Viewport): GraphTextElement { const bounds = graphTextScreenBounds(text, viewport);
const position = pixelToMath( viewport, bounds.left + text.widthPx * horizontalAnchorFraction(anchor.horizontal), bounds.top + text.heightPx * verticalAnchorFraction(anchor.vertical) );
if ( Math.abs(position.x) > MAX_GRAPH_TEXT_COORDINATE || Math.abs(position.y) > MAX_GRAPH_TEXT_COORDINATE ) { return text; }
return { ...text, anchor: { ...anchor }, position };}
/** Keep the selected graph anchor under its grabbed screen point in the live camera. */export function moveGraphTextAnchor( viewport: Viewport, pointer: PixelPoint, anchorOffset: PixelPoint): { x: number; y: number } { return pixelToMath(viewport, pointer.x + anchorOffset.x, pointer.y + anchorOffset.y);}
/** The graph-coordinate top-left corner stays fixed while the bottom-right handle moves. */export function graphTextTopLeftAnchor( text: Pick<GraphTextElement, 'position' | 'anchor' | 'widthPx' | 'heightPx'>, viewport: Viewport): { x: number; y: number } { const bounds = graphTextScreenBounds(text, viewport);
return pixelToMath(viewport, bounds.left, bounds.top);}
export function resizeGraphTextFromBottomRight( text: GraphTextElement, topLeftAnchor: { x: number; y: number }, pointer: PixelPoint, viewport: Viewport): GraphTextElement { const topLeft = mathToPixel(viewport, topLeftAnchor.x, topLeftAnchor.y);
const widthPx = clamp( pointer.x - topLeft.x, MIN_GRAPH_TEXT_WIDTH_PX, MAX_GRAPH_TEXT_WIDTH_PX );
const heightPx = clamp( pointer.y - topLeft.y, MIN_GRAPH_TEXT_HEIGHT_PX, MAX_GRAPH_TEXT_HEIGHT_PX );
const position = pixelToMath( viewport, topLeft.x + widthPx * horizontalAnchorFraction(text.anchor.horizontal), topLeft.y + heightPx * verticalAnchorFraction(text.anchor.vertical) );
return { ...text, position, widthPx, heightPx };}
function horizontalAnchorFraction(anchor: GraphTextHorizontalAnchor): number { return anchor === 'left' ? 0 : anchor === 'right' ? 1 : 0.5;}
function verticalAnchorFraction(anchor: GraphTextVerticalAnchor): number { return anchor === 'top' ? 0 : anchor === 'bottom' ? 1 : 0.5;}
function clamp(value: number, minimum: number, maximum: number): number { return Math.max(minimum, Math.min(maximum, value));}