Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components/canvas/shotstack-canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AlignmentGuides } from "@canvas/system/alignment-guides";
import { createWebGLErrorOverlay } from "@canvas/webgl-error-overlay";
import { Edit } from "@core/edit-session";
import { InternalEvent } from "@core/events/edit-events";
import { sanitizeColor } from "@core/shared/color-utils";
import { ms } from "@core/timing/types";
import type { UIController } from "@core/ui/ui-controller";
import { checkWebGLSupport, WebGLUnsupportedError } from "@core/webgl-support";
Expand Down Expand Up @@ -110,7 +111,7 @@ export class Canvas {
this.viewportContainer.sortableChildren = true;

this.editBackground = new pixi.Graphics();
this.editBackground.fillStyle = { color: this.edit.getTimelineBackground() };
this.editBackground.fillStyle = { color: sanitizeColor(this.edit.getTimelineBackground(), "#000000") };
this.editBackground.rect(0, 0, this.edit.size.width, this.edit.size.height);
this.editBackground.fill();
this.viewportContainer.addChild(this.editBackground);
Expand Down Expand Up @@ -533,7 +534,7 @@ export class Canvas {
public updateViewportForSize(width: number, height: number, backgroundColor: string): void {
if (this.editBackground) {
this.editBackground.clear();
this.editBackground.fillStyle = { color: backgroundColor };
this.editBackground.fillStyle = { color: sanitizeColor(backgroundColor, "#000000") };
this.editBackground.rect(0, 0, width, height);
this.editBackground.fill();
}
Expand Down
7 changes: 4 additions & 3 deletions src/core/shared/color-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
* Color helpers for Pixi text/style construction.
*/

/** Matches Pixi Color HEX_PATTERN (3/4/6/8 hex digits, optional # or 0x). */
const HEX_PATTERN = /^(#|0x)?(([a-f0-9]{3}){1,2}([a-f0-9]{2})?)$/i;
/** Matches Pixi Color HEX_PATTERN: exactly 3, 4, 6, or 8 hex digits (optional # or 0x). */
const HEX_PATTERN = /^(#|0x)?([a-f0-9]{3}|[a-f0-9]{4}|[a-f0-9]{6}|[a-f0-9]{8})$/i;
const FUNCTIONAL_COLOR = /^(rgb|rgba|hsl|hsla)\(/i;
const CSS_COLOR_NAME = /^[a-z]+$/i;

/**
* Return a Pixi-safe color string, or `fallback` when the value is missing/invalid.
* Incomplete hex (e.g. `#00`, `#0`) and empty strings fall back instead of throwing in TextStyle.
* Incomplete hex (e.g. `#00`, `#00000`), unsubstituted merge tokens (e.g. `{{ BG_COLOR }}`),
* and empty strings fall back instead of throwing in TextStyle / Graphics fillStyle.
* @internal
*/
export function sanitizeColor(color: string | undefined | null, fallback = "#ffffff"): string {
Expand Down
14 changes: 14 additions & 0 deletions tests/color-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ describe("sanitizeColor", () => {
expect(sanitizeColor(" ")).toBe("#ffffff");
});

it("falls back for 5-digit hex that previously slipped past the sanitizer", () => {
expect(sanitizeColor("#00000")).toBe("#ffffff");
expect(sanitizeColor("#12345")).toBe("#ffffff");
expect(sanitizeColor("0x00000")).toBe("#ffffff");
});

it("falls back for unsubstituted merge-field tokens", () => {
expect(sanitizeColor("{{ BG_COLOR }}")).toBe("#ffffff");
expect(sanitizeColor("{{BG_COLOR}}")).toBe("#ffffff");
expect(sanitizeColor("{{ FONT_COLOR }}", "#000000")).toBe("#000000");
});

it("falls back for nullish and non-string values", () => {
expect(sanitizeColor(undefined)).toBe("#ffffff");
expect(sanitizeColor(null)).toBe("#ffffff");
Expand All @@ -16,12 +28,14 @@ describe("sanitizeColor", () => {
it("preserves valid hex, named, and functional colors", () => {
expect(sanitizeColor("#ffffff")).toBe("#ffffff");
expect(sanitizeColor("#fff")).toBe("#fff");
expect(sanitizeColor("#ffff")).toBe("#ffff");
expect(sanitizeColor("#ff00ff00")).toBe("#ff00ff00");
expect(sanitizeColor("red")).toBe("red");
expect(sanitizeColor("rgb(255, 0, 0)")).toBe("rgb(255, 0, 0)");
});

it("uses a custom fallback when provided", () => {
expect(sanitizeColor("#00", "#000000")).toBe("#000000");
expect(sanitizeColor("#00000", "#000000")).toBe("#000000");
});
});
Loading