import { base64url } from "jose"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import { Pattern } from "../../core/CosmeticSchemas"; import { PatternDecoder } from "../../core/PatternDecoder"; import { translateText } from "../Utils"; export const BUTTON_WIDTH = 150; @customElement("pattern-button") export class PatternButton extends LitElement { @property({ type: Object }) pattern: Pattern | null = null; @property({ type: Function }) onSelect?: (pattern: Pattern | null) => void; @property({ type: Function }) onPurchase?: (pattern: Pattern) => void; createRenderRoot() { return this; } private translatePatternName(prefix: string, patternName: string): string { const translation = translateText(`${prefix}.${patternName}`); if (translation.startsWith(prefix)) { return patternName[0].toUpperCase() + patternName.substring(1); } return translation; } private handleClick() { const isDefaultPattern = this.pattern === null; if (isDefaultPattern || this.pattern?.product === null) { this.onSelect?.(this.pattern); } } private handlePurchase(e: Event) { e.stopPropagation(); if (this.pattern?.product) { this.onPurchase?.(this.pattern); } } render() { const isDefaultPattern = this.pattern === null; const isPurchasable = !isDefaultPattern && this.pattern?.product !== null; return html`
${isPurchasable ? html` ` : null}
`; } } export function renderPatternPreview( pattern: string | null, width: number, height: number, ): TemplateResult { if (pattern === null) { return renderBlankPreview(width, height); } const dataUrl = generatePreviewDataUrl(pattern, width, height); return html`Pattern preview`; } function renderBlankPreview(width: number, height: number): TemplateResult { return html`
`; } const patternCache = new Map(); const DEFAULT_PATTERN_B64 = "AAAAAA"; // Empty 2x2 pattern const COLOR_SET = [0, 0, 0, 255]; // Black const COLOR_UNSET = [255, 255, 255, 255]; // White function generatePreviewDataUrl( pattern?: string, width?: number, height?: number, ): string { pattern ??= DEFAULT_PATTERN_B64; const patternLookupKey = `${pattern}-${width}-${height}`; if (patternCache.has(patternLookupKey)) { return patternCache.get(patternLookupKey)!; } // Calculate canvas size let decoder: PatternDecoder; try { decoder = new PatternDecoder(pattern, base64url.decode); } catch (e) { console.error("Error decoding pattern", e); return ""; } const scaledWidth = decoder.scaledWidth(); const scaledHeight = decoder.scaledHeight(); width = width === undefined ? scaledWidth : Math.max(1, Math.floor(width / scaledWidth)) * scaledWidth; height = height === undefined ? scaledHeight : Math.max(1, Math.floor(height / scaledHeight)) * scaledHeight; // Create the canvas const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("2D context not supported"); // Create an image const imageData = ctx.createImageData(width, height); const data = imageData.data; let i = 0; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const rgba = decoder.isSet(x, y) ? COLOR_SET : COLOR_UNSET; data[i++] = rgba[0]; // Red data[i++] = rgba[1]; // Green data[i++] = rgba[2]; // Blue data[i++] = rgba[3]; // Alpha } } // Create a data URL ctx.putImageData(imageData, 0, 0); const dataUrl = canvas.toDataURL("image/png"); patternCache.set(patternLookupKey, dataUrl); return dataUrl; }