refactor: Update bottom controls for improved styling and functionality

- Adjusted button styles across various components for consistency and better UX.
- Enhanced layout of action controls to utilize whitespace more effectively.
- Updated slider styles for a more modern appearance and improved usability.
- Refined input fields and labels for better accessibility and readability.
- Introduced new app surface styles for a cohesive design across the application.
- Added tests for canvas cursor behavior to ensure correct cursor display during operations.
This commit is contained in:
syntaxbullet
2026-07-11 14:55:32 +02:00
parent 37aa719047
commit 5915c62a9a
31 changed files with 345 additions and 287 deletions

View File

@@ -8,7 +8,7 @@ import { textureCoordinatesForCrop, textureCoordinatesForRect } from "./texture-
export type MaskVisualizationMode = "blackWhite" | "alpha" | "hiddenOverlay";
export type ImageTextureRenderer = {
syncAssets(assets: readonly Asset[]): void;
syncAssets(assets: readonly Pick<Asset, "id">[]): void;
render(asset: Asset, rect: ScreenRect, clipRect?: ScreenRect, opacity?: number, rotation?: number, sourceRect?: Rect): boolean;
renderMasked(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, clipRect?: ScreenRect, opacity?: number, rotation?: number, sourceRect?: Rect): boolean;
renderMaskRevealPreview(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, opacity: number, clipRect?: ScreenRect, layerOpacity?: number, rotation?: number, sourceRect?: Rect): boolean;
@@ -346,7 +346,8 @@ function loadTextureState(
const source = asset.source;
image.onload = () => {
if (isDisposed() || entry.source !== source) return;
const texture = createTexture(context.gl, image);
const textureSource = asset.mimeType === "image/svg+xml" ? rasterizeSvgImage(image, asset) : image;
const texture = createTexture(context.gl, textureSource);
if (entry.previousTexture) context.gl.deleteTexture(entry.previousTexture);
entry.previousTexture = undefined;
entry.current = { status: "ready", texture };
@@ -361,6 +362,17 @@ function loadTextureState(
return { status: "loading", image };
}
function rasterizeSvgImage(image: HTMLImageElement, asset: Asset): HTMLCanvasElement {
const canvas = document.createElement("canvas");
canvas.width = Math.max(1, Math.ceil(asset.intrinsicSize.w));
canvas.height = Math.max(1, Math.ceil(asset.intrinsicSize.h));
const context = canvas.getContext("2d");
if (!context) throw new Error("Failed to create SVG rasterization context");
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(image, 0, 0, canvas.width, canvas.height);
return canvas;
}
function renderableTexture(entry: TextureEntry): WebGLTexture | undefined {
if (entry.current.status === "ready") return entry.current.texture;
return entry.previousTexture;
@@ -392,7 +404,7 @@ function intersectScreenRects(a: ScreenRect, b: ScreenRect): ScreenRect | undefi
return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
}
function createTexture(gl: WebGL2RenderingContext, image: HTMLImageElement) {
function createTexture(gl: WebGL2RenderingContext, image: TexImageSource) {
const texture = gl.createTexture();
if (!texture) throw new Error("Failed to create image texture");