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:
@@ -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");
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { ImageTextureRenderer } from "./image-textures";
|
||||
import { documentRectToScreenRect } from "./screen-rect";
|
||||
import type { RgbaColor, ScreenRect, WebGlRendererContext } from "./types";
|
||||
import type { AdjustmentPass } from "./adjustment-pass";
|
||||
import { textLayerRenderAsset } from "./text-asset";
|
||||
import { rasterizedTextLayerRenderAsset } from "./text-asset";
|
||||
|
||||
const imageLayerColor: RgbaColor = [0.38, 0.42, 0.5, 1];
|
||||
const imageLayerInsetColor: RgbaColor = [0.48, 0.54, 0.64, 1];
|
||||
@@ -77,7 +77,7 @@ function renderTextLayer(context: WebGlRendererContext, documentIndex: DocumentR
|
||||
const bounds = resolveIndexedLayerBounds(documentIndex, layer);
|
||||
if (!bounds) return;
|
||||
const rect = documentRectToScreenRect(context.canvas, bounds, editor.viewport);
|
||||
imageTextureRenderer.render(textLayerRenderAsset(layer), rect, clipRect, opacity, layer.transform.rotation);
|
||||
imageTextureRenderer.render(rasterizedTextLayerRenderAsset(layer), rect, clipRect, opacity, layer.transform.rotation);
|
||||
}
|
||||
|
||||
function renderLeafLayer(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ImageDocument } from "@core/document";
|
||||
import type { EditorState } from "@editor/state";
|
||||
import { createDocumentReadIndex } from "@editor/document-indexes";
|
||||
import { renderArtboard } from "./artboard";
|
||||
import { createBrushPreviewRenderer } from "./brush-preview";
|
||||
import { createCheckerboardRenderer } from "./checkerboard";
|
||||
@@ -64,7 +65,11 @@ export function createRenderer(canvas: HTMLCanvasElement, backend: RendererBacke
|
||||
for (const artboard of frame.document.artboards) {
|
||||
if (artboard.visible) renderArtboard(rendererContext, artboard, frame.editor.viewport, checkerboardRenderer);
|
||||
}
|
||||
imageTextureRenderer.syncAssets([...frame.document.assets, ...generationCandidatePreviewAssets(frame.editor)]);
|
||||
imageTextureRenderer.syncAssets([
|
||||
...frame.document.assets,
|
||||
...generationCandidatePreviewAssets(frame.editor),
|
||||
...textTextureHandles(frame.document),
|
||||
]);
|
||||
renderLayers(rendererContext, frame.document, frame.editor, imageTextureRenderer, adjustmentPass);
|
||||
|
||||
if (!frame.editor.maskEdit) {
|
||||
@@ -86,6 +91,13 @@ export function createRenderer(canvas: HTMLCanvasElement, backend: RendererBacke
|
||||
return renderer;
|
||||
}
|
||||
|
||||
function textTextureHandles(document: ImageDocument): Array<{ id: string }> {
|
||||
const index = createDocumentReadIndex(document);
|
||||
return [...index.layerById.values()]
|
||||
.filter((layer) => layer.type === "text")
|
||||
.map((layer) => ({ id: `text-render:${layer.id}` }));
|
||||
}
|
||||
|
||||
function createOptionalBrushPreviewRenderer(context: WebGlRendererContext) {
|
||||
try {
|
||||
return createBrushPreviewRenderer(context);
|
||||
|
||||
@@ -14,6 +14,34 @@ export function textLayerRenderAsset(layer: TextLayer): Asset {
|
||||
return { id: `text-render:${layer.id}`, name: layer.name, mimeType: "image/svg+xml", source: `data:image/svg+xml,${encodeURIComponent(svg)}`, intrinsicSize: size };
|
||||
}
|
||||
|
||||
/** Rasterizes authoritative text for the live WebGL renderer without relying on browser SVG text decoding. */
|
||||
export function rasterizedTextLayerRenderAsset(layer: TextLayer): Asset {
|
||||
const size = measureTextLayer(layer);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = Math.max(1, Math.ceil(size.w));
|
||||
canvas.height = Math.max(1, Math.ceil(size.h));
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) throw new Error("Failed to create text rasterization context");
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.fillStyle = layer.style.color;
|
||||
context.font = `${layer.style.fontStyle} ${layer.style.fontWeight} ${layer.style.fontSize}px ${JSON.stringify(layer.style.fontFamily)}, sans-serif`;
|
||||
context.textAlign = layer.style.alignment;
|
||||
context.textBaseline = "alphabetic";
|
||||
const x = layer.style.alignment === "left" ? 0 : layer.style.alignment === "center" ? size.w / 2 : size.w;
|
||||
for (const [index, line] of layer.content.split("\n").entries()) {
|
||||
context.fillText(line || " ", x, (index + 0.82) * layer.style.fontSize * layer.style.lineHeight);
|
||||
}
|
||||
|
||||
return {
|
||||
id: `text-render:${layer.id}`,
|
||||
name: layer.name,
|
||||
mimeType: "image/png",
|
||||
source: canvas.toDataURL("image/png"),
|
||||
intrinsicSize: size,
|
||||
};
|
||||
}
|
||||
|
||||
function escapeXml(value: string) {
|
||||
return value.replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]!);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user