diff --git a/.cursor/commands/review.md b/.cursor/commands/review.md index d7c0c45c..ab36f7c4 100644 --- a/.cursor/commands/review.md +++ b/.cursor/commands/review.md @@ -49,7 +49,7 @@ Review every point below carefully to ensure files follow consistent code style - [ ] Readability over brevity — use `element` not `el`, `event` not `e` - [ ] Booleans are named `isSomething`, `hasSomething`, or `shouldSomething` — not `something` -- [ ] No title case in text/UI — use `Hello world` not `Hello World` +- [ ] No title case for multi-word text/UI — use `Hello world` not `Hello World` ## Tailwind & Styling @@ -61,12 +61,15 @@ Review every point below carefully to ensure files follow consistent code style {/* ✅ correct */} {/* ❌ wrong */} {/* ❌ unnecessary, size-4 is default */} + {" "} + {/* ❌ completely wrong, 1) doesn't override and 2) size-4 is default */} ``` ## State Management (Zustand) - [ ] React components never use `someStore.getState()` — use the `useSomeStore` hook instead +- [ ] High-frequency stores (timeline, playback, selections) use selectors — `useStore((s) => s.value)` not `const { value } = useStore()` - [ ] Store/manager methods are not passed as props — sub-components access them directly ```tsx @@ -91,6 +94,11 @@ Review every point below carefully to ensure files follow consistent code style - [ ] Code is scannable — use variables and helper functions to make intent clear at a glance - [ ] Complex logic is extracted into well-named variables or helpers +- [ ] No magic numbers or magic values — extract inline literals into named constants + - Applies to colors, durations, thresholds, sizes, config values, etc. + - If it's domain-specific to one file, a `const` at the top of that file is fine + - If it's generic enough, it belongs in `constants/` + - [ ] No redundant single/plural function variants — if a function can operate on multiple items, it should accept an array and handle both cases. Don't create `doThing()` + `doThings()`. ```tsx @@ -116,4 +124,25 @@ Review every point below carefully to ensure files follow consistent code style --- -> Every decision, every edit must be carefully considered. Everything matters. +## Review Methodology + +Do NOT review by reading the file top-to-bottom and noting what jumps out. Instead: + +1. Go through each checklist section **one at a time** +2. For each section, scan the **entire file** for violations of that specific rule +3. Only move to the next section after you've exhausted the current one +4. After all sections are checked, do a final pass: re-read every checklist item and confirm you didn't skip it + +Before outputting the table, list each checklist section and confirm you checked it: +`Signatures ✓ | TypeScript ✓ | JSX ✓ | Organization ✓ | Comments ✓ | Naming ✓ | Tailwind ✓ | State ✓ | Quality ✓ | Keywords ✓` + +--- + +## IMPORTANT: Review Rules + +- **ONLY** flag issues that are explicitly covered by a checklist item above. +- Do **NOT** invent your own rules, suggestions, or "nice to haves" as primary issues. +- The review output must be a table of issues, each one mapping to a specific checklist item. +- If something looks off but isn't covered by the checklist, you can mention it as a brief side note at the end — but keep it clearly separate from the actual review. Always default to fixing the issues covered by the checklist above, unless the user says otherwise. + +> You WILL miss things if you try to review the whole file in one pass. Iterate rule by rule. diff --git a/.cursor/rules/codebase-index.mdc b/.cursor/rules/codebase-index.mdc index c63ad1e7..635e547b 100644 --- a/.cursor/rules/codebase-index.mdc +++ b/.cursor/rules/codebase-index.mdc @@ -19,7 +19,6 @@ When implementing new features: ## apps/web/src/constants editor-constants.ts - export const PLATFORM_LAYOUTS: Record export const PANEL_CONFIG export-constants.ts @@ -27,19 +26,8 @@ export-constants.ts export const EXPORT_MIME_TYPES font-constants.ts - export interface FontOption { - value: string - label: string - category: "system" | "google" | "custom" - weights?: number[] - hasClassName?: boolean - } - export const FONT_OPTIONS: FontOption[] export const DEFAULT_FONT - export type FontFamily = (typeof FONT_OPTIONS)[number]["value"] - export const getFontByValue = (value: string): FontOption | undefined => ... - export const getGoogleFonts = (): FontOption[] => ... - export const getSystemFonts = (): FontOption[] => ... + export const SYSTEM_FONTS language-constants.ts export const LANGUAGES @@ -56,37 +44,43 @@ project-constants.ts site-constants.ts export const SITE_URL export const SITE_INFO - export type ExternalTool = { - name: string; - description: string; - url: string; - icon: React.ElementType; + export type ExternalTool = { + name: string; + description: string; + url: string; + icon: React.ElementType; } export const EXTERNAL_TOOLS: ExternalTool[] export const DEFAULT_LOGO_URL export const SOCIAL_LINKS - export type Sponsor = { - name: string; - url: string; - logo: string; - description: string; + export type Sponsor = { + name: string; + url: string; + logo: string; + description: string; } export const SPONSORS: Sponsor[] -stickers-constants.ts +sticker-constants.ts export const STICKER_CATEGORIES - export const STICKER_CATEGORY_CONFIG: Record< - (typeof STICKER_CATEGORIES)[number], - string | undefined - > text-constants.ts + export const MIN_FONT_SIZE + export const MAX_FONT_SIZE + export const FONT_SIZE_SCALE_REFERENCE + export const DEFAULT_LETTER_SPACING + export const DEFAULT_LINE_HEIGHT export const DEFAULT_TEXT_ELEMENT: Omit timeline-constants.tsx + export const DEFAULT_TRANSFORM: Transform + export const DEFAULT_OPACITY + export const DEFAULT_BLEND_MODE: BlendMode + export const DEFAULT_BOOKMARK_COLOR export const TRACK_COLORS: Record export const TRACK_HEIGHTS: Record export const TRACK_GAP + export const DRAG_THRESHOLD_PX export const TIMELINE_CONSTANTS export const DEFAULT_TIMELINE_VIEW_STATE: TTimelineViewState export const TRACK_ICONS: Record @@ -121,23 +115,50 @@ index.ts ## apps/web/src/hooks +use-container-size.ts + export function useContainerSize({ + containerRef, + }: { + containerRef: React.RefObject; + }) + use-editor.ts export function useEditor(): EditorCore use-file-upload.ts - export function useFileUpload({ - accept, - multiple, - onFilesSelected, + export function useFileUpload({ + accept, + multiple, + onFilesSelected, }: UseFileUploadOptions = {}) +use-focus-lock.ts + export function useFocusLock({ + isActive, + onDismiss, + cursor = "default", + allowSelector, + }: { + isActive: boolean; + onDismiss: () => void; + cursor?: FocusLockCursor; + allowSelector?: string; + }) + +use-fullscreen.ts + export function useFullscreen({ + containerRef, + }: { + containerRef: React.RefObject; + }) + use-infinite-scroll.ts - export function useInfiniteScroll({ - onLoadMore, - hasMore, - isLoading, - threshold = 200, - enabled = true, + export function useInfiniteScroll({ + onLoadMore, + hasMore, + isLoading, + threshold = 200, + enabled = true, }: UseInfiniteScrollOptions) use-keybindings.ts @@ -158,32 +179,52 @@ use-keyboard-shortcuts-help.ts use-mobile.ts export function useIsMobile() +use-paste-media.ts + export function usePasteMedia() + +use-preview-interaction.ts + export function usePreviewInteraction({ + canvasRef, + }: { + canvasRef: React.RefObject; + }) + use-raf-loop.ts export function useRafLoop(callback: ({ time }: { time: number }) => void) use-reveal-item.ts - export function useRevealItem( - highlightId: string | null, - onClearHighlight: () => void, - highlightDuration = 1000, + export function useRevealItem( + highlightId: string | null, + onClearHighlight: () => void, + highlightDuration = 1000, ) +use-shift-key.ts + export function useShiftKey(): RefObject + use-sound-search.ts - export function useSoundSearch({ - query, - commercialOnly, + export function useSoundSearch({ + query, + commercialOnly, + }: { + query: string; + commercialOnly: boolean; + }) + +use-transform-handles.ts + export function useTransformHandles({ + canvasRef, }: { - query: string; - commercialOnly: boolean; + canvasRef: React.RefObject; }) ## apps/web/src/hooks/actions use-action-handler.ts - export function useActionHandler( - action: A, - handler: TActionFunc, - isActive: TActionHandlerOptions, + export function useActionHandler( + action: A, + handler: TActionFunc, + isActive: TActionHandlerOptions, ) use-editor-actions.ts @@ -192,91 +233,111 @@ use-editor-actions.ts ## apps/web/src/hooks/storage use-local-storage.ts - export function useLocalStorage({ - key, - defaultValue, - }: { - key: string; - defaultValue: T; - }): [ - T, - ({ value }: { value: T | ((previousValue: T) => T) }) => void, - boolean, + export function useLocalStorage({ + key, + defaultValue, + }: { + key: string; + defaultValue: T; + }): [ + T, + ({ value }: { value: T | ((previousValue: T) => T) }) => void, + boolean, ] ## apps/web/src/hooks/timeline +use-bookmark-drag.ts + export interface BookmarkDragState { + isDragging: boolean + bookmarkTime: number | null + currentTime: number + } + export function useBookmarkDrag({ + zoomLevel, + scrollRef, + snappingEnabled, + onSnapPointChange, + }: UseBookmarkDragProps) + use-edge-auto-scroll.ts - export function useEdgeAutoScroll({ - isActive, - getMouseClientX, - rulerScrollRef, - tracksScrollRef, - contentWidth, - edgeThreshold = 100, - maxScrollSpeed = 15, + export function useEdgeAutoScroll({ + isActive, + getMouseClientX, + rulerScrollRef, + tracksScrollRef, + contentWidth, + edgeThreshold = 100, + maxScrollSpeed = 15, }: UseEdgeAutoScrollParams): void +use-scroll-position.ts + export function useScrollPosition({ + scrollRef, + }: { + scrollRef: React.RefObject; + }): UseScrollPositionReturn + use-scroll-sync.ts - export function useScrollSync({ - tracksScrollRef, - rulerScrollRef, - trackLabelsScrollRef, - bookmarksScrollRef, + export function useScrollSync({ + tracksScrollRef, + rulerScrollRef, + trackLabelsScrollRef, + bookmarksScrollRef, }: UseScrollSyncProps) use-selection-box.ts - export function useSelectionBox({ - containerRef, - onSelectionComplete, - isEnabled = true, - tracksScrollRef, - zoomLevel, + export function useSelectionBox({ + containerRef, + onSelectionComplete, + isEnabled = true, + tracksScrollRef, + zoomLevel, }: UseSelectionBoxProps) use-snap-indicator-position.ts - export function useSnapIndicatorPosition({ - snapPoint, - zoomLevel, - tracks, - timelineRef, - trackLabelsRef, - tracksScrollRef, + export function useSnapIndicatorPosition({ + snapPoint, + zoomLevel, + tracks, + timelineRef, + trackLabelsRef, + tracksScrollRef, }: UseSnapIndicatorPositionParams): SnapIndicatorPosition use-timeline-drag-drop.ts - export function useTimelineDragDrop({ - containerRef, - headerRef, - zoomLevel, + export function useTimelineDragDrop({ + containerRef, + headerRef, + zoomLevel, }: UseTimelineDragDropProps) use-timeline-playhead.ts - export function useTimelinePlayhead({ - zoomLevel, - rulerRef, - rulerScrollRef, - tracksScrollRef, - playheadRef, + export function useTimelinePlayhead({ + zoomLevel, + rulerRef, + rulerScrollRef, + tracksScrollRef, + playheadRef, }: UseTimelinePlayheadProps) use-timeline-seek.ts - export function useTimelineSeek({ - playheadRef, - trackLabelsRef, - rulerScrollRef, - tracksScrollRef, - zoomLevel, - duration, - isSelecting, - clearSelectedElements, - seek, + export function useTimelineSeek({ + playheadRef, + trackLabelsRef, + rulerScrollRef, + tracksScrollRef, + zoomLevel, + duration, + isSelecting, + clearSelectedElements, + seek, }: UseTimelineSeekProps) use-timeline-snapping.ts export interface SnapPoint { time: number - type: "element-start" | "element-end" | "playhead" + type: "element-start" | "element-end" | "playhead" | "bookmark" elementId?: string trackId?: string } @@ -289,35 +350,37 @@ use-timeline-snapping.ts snapThreshold?: number enableElementSnapping?: boolean enablePlayheadSnapping?: boolean + enableBookmarkSnapping?: boolean } - export function useTimelineSnapping({ - snapThreshold = 10, - enableElementSnapping = true, - enablePlayheadSnapping = true, + export function useTimelineSnapping({ + snapThreshold = 10, + enableElementSnapping = true, + enablePlayheadSnapping = true, + enableBookmarkSnapping = true, }: UseTimelineSnappingOptions = {}) use-timeline-zoom.ts - export function useTimelineZoom({ - containerRef, - minZoom = TIMELINE_CONSTANTS.ZOOM_MIN, - initialZoom, - initialScrollLeft, - initialPlayheadTime, - tracksScrollRef, - rulerScrollRef, + export function useTimelineZoom({ + containerRef, + minZoom = TIMELINE_CONSTANTS.ZOOM_MIN, + initialZoom, + initialScrollLeft, + initialPlayheadTime, + tracksScrollRef, + rulerScrollRef, }: UseTimelineZoomProps): UseTimelineZoomReturn ## apps/web/src/hooks/timeline/element use-element-interaction.ts - export function useElementInteraction({ - zoomLevel, - timelineRef, - tracksContainerRef, - tracksScrollRef, - headerRef, - snappingEnabled, - onSnapPointChange, + export function useElementInteraction({ + zoomLevel, + timelineRef, + tracksContainerRef, + tracksScrollRef, + headerRef, + snappingEnabled, + onSnapPointChange, }: UseElementInteractionProps) use-element-resize.ts @@ -330,12 +393,12 @@ use-element-resize.ts initialStartTime: number initialDuration: number } - export function useTimelineElementResize({ - element, - track, - zoomLevel, - onSnapPointChange, - onResizeStateChange, + export function useTimelineElementResize({ + element, + track, + zoomLevel, + onSnapPointChange, + onResizeStateChange, }: UseTimelineElementResizeProps) use-element-selection.ts @@ -344,35 +407,35 @@ use-element-selection.ts ## apps/web/src/lib drag-data.ts - export function setDragData({ - dataTransfer, - dragData, - }: { - dataTransfer: DataTransfer; - dragData: TimelineDragData; + export function setDragData({ + dataTransfer, + dragData, + }: { + dataTransfer: DataTransfer; + dragData: TimelineDragData; }): void - export function getDragData({ - dataTransfer, - }: { - dataTransfer: DataTransfer; + export function getDragData({ + dataTransfer, + }: { + dataTransfer: DataTransfer; }): TimelineDragData | null - export function hasDragData({ - dataTransfer, - }: { - dataTransfer: DataTransfer; + export function hasDragData({ + dataTransfer, + }: { + dataTransfer: DataTransfer; }): boolean export function clearDragData(): void export.ts - export function getExportMimeType({ - format, - }: { - format: ExportFormat; + export function getExportMimeType({ + format, + }: { + format: ExportFormat; }): string - export function getExportFileExtension({ - format, - }: { - format: ExportFormat; + export function getExportFileExtension({ + format, + }: { + format: ExportFormat; }): string iconify-api.ts @@ -381,14 +444,14 @@ iconify-api.ts prefix: string name: string total: number - author?: { - name: string; - url?: string; + author?: { + name: string; + url?: string; } - license?: { - title: string; - spdx?: string; - url?: string; + license?: { + title: string; + spdx?: string; + url?: string; } samples?: string[] category?: string @@ -410,41 +473,41 @@ iconify-api.ts hidden?: string[] aliases?: Record } - export function getCollections( - category?: string, + export function getCollections( + category?: string, ): Promise> - export function getCollection( - prefix: string, + export function getCollection( + prefix: string, ): Promise - export function searchIcons( - query: string, - limit: number = 64, - prefixes?: string[], - category?: string, + export function searchIcons( + query: string, + limit: number = 64, + prefixes?: string[], + category?: string, ): Promise - export function buildIconSvgUrl( - host: string, - iconName: string, - params?: { - color?: string; - width?: number; - height?: number; - flip?: "horizontal" | "vertical" | "horizontal,vertical"; - rotate?: number | string; - }, + export function buildIconSvgUrl( + host: string, + iconName: string, + params?: { + color?: string; + width?: number; + height?: number; + flip?: "horizontal" | "vertical" | "horizontal,vertical"; + rotate?: number | string; + }, ): string - export function getIconSvgUrl( - iconName: string, - params?: Parameters[2], + export function getIconSvgUrl( + iconName: string, + params?: Parameters[2], ): string - export function downloadSvgAsText( - iconName: string, - params?: Parameters[1], + export function downloadSvgAsText( + iconName: string, + params?: Parameters[1], ): Promise export function svgToFile(svgText: string, fileName: string): File export const POPULAR_COLLECTIONS - export function getCategoriesFromCollections( - collections: Record, + export function getCategoriesFromCollections( + collections: Record, ): string[] rate-limit.ts @@ -454,127 +517,127 @@ rate-limit.ts scenes.ts export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[] - export function buildDefaultScene({ - name, - isMain, - }: { - name: string; - isMain: boolean; + export function buildDefaultScene({ + name, + isMain, + }: { + name: string; + isMain: boolean; }): TScene - export function canDeleteScene({ scene }: { scene: TScene }): { - canDelete: boolean; - reason?: string; + export function canDeleteScene({ scene }: { scene: TScene }): { + canDelete: boolean; + reason?: string; } - export function getFallbackSceneAfterDelete({ - scenes, - deletedSceneId, - currentSceneId, - }: { - scenes: TScene[]; - deletedSceneId: string; - currentSceneId: string | null; + export function getFallbackSceneAfterDelete({ + scenes, + deletedSceneId, + currentSceneId, + }: { + scenes: TScene[]; + deletedSceneId: string; + currentSceneId: string | null; }): TScene | null - export function findCurrentScene({ - scenes, - currentSceneId, - }: { - scenes: TScene[]; - currentSceneId: string; + export function findCurrentScene({ + scenes, + currentSceneId, + }: { + scenes: TScene[]; + currentSceneId: string; }): TScene | null - export function getProjectDurationFromScenes({ - scenes, - }: { - scenes: TScene[]; + export function getProjectDurationFromScenes({ + scenes, + }: { + scenes: TScene[]; }): number - export function updateSceneInArray({ - scenes, - sceneId, - updates, - }: { - scenes: TScene[]; - sceneId: string; - updates: Partial; + export function updateSceneInArray({ + scenes, + sceneId, + updates, + }: { + scenes: TScene[]; + sceneId: string; + updates: Partial; }): TScene[] time.ts - export function roundToFrame({ - time, - fps, - }: { - time: number; - fps: number; + export function roundToFrame({ + time, + fps, + }: { + time: number; + fps: number; }): number - export function formatTimeCode({ - timeInSeconds, - format = "HH:MM:SS:CS", - fps, - }: { - timeInSeconds: number; - format?: TTimeCode; - fps?: number; + export function formatTimeCode({ + timeInSeconds, + format = "HH:MM:SS:CS", + fps, + }: { + timeInSeconds: number; + format?: TTimeCode; + fps?: number; }): string - export function parseTimeCode({ - timeCode, - format = "HH:MM:SS:CS", - fps, - }: { - timeCode: string; - format?: TTimeCode; - fps: number; + export function parseTimeCode({ + timeCode, + format = "HH:MM:SS:CS", + fps, + }: { + timeCode: string; + format?: TTimeCode; + fps: number; }): number | null - export function guessTimeCodeFormat({ - timeCode, - }: { - timeCode: string; + export function guessTimeCodeFormat({ + timeCode, + }: { + timeCode: string; }): TTimeCode | null - export function timeToFrame({ - time, - fps, - }: { - time: number; - fps: number; + export function timeToFrame({ + time, + fps, + }: { + time: number; + fps: number; }): number - export function frameToTime({ - frame, - fps, - }: { - frame: number; - fps: number; + export function frameToTime({ + frame, + fps, + }: { + frame: number; + fps: number; }): number - export function snapTimeToFrame({ - time, - fps, - }: { - time: number; - fps: number; + export function snapTimeToFrame({ + time, + fps, + }: { + time: number; + fps: number; }): number - export function getSnappedSeekTime({ - rawTime, - duration, - fps, - }: { - rawTime: number; - duration: number; - fps: number; + export function getSnappedSeekTime({ + rawTime, + duration, + fps, + }: { + rawTime: number; + duration: number; + fps: number; }): number - export function getLastFrameTime({ - duration, - fps, - }: { - duration: number; - fps: number; + export function getLastFrameTime({ + duration, + fps, + }: { + duration: number; + fps: number; }): number ## apps/web/src/lib/actions definitions.ts - export type TActionCategory = | "playback" - | "navigation" - | "editing" - | "selection" - | "history" - | "timeline" - | "con... + export type TActionCategory = | "playback" + | "navigation" + | "editing" + | "selection" + | "history" + | "timeline" + | "controls" export interface TActionDefinition { description: string category: TActionCategory @@ -587,40 +650,40 @@ definitions.ts export function getDefaultShortcuts(): Record registry.ts - export function bindAction( - action: A, - handler: TActionFunc, + export function bindAction( + action: A, + handler: TActionFunc, ) - export function unbindAction( - action: A, - handler: TActionFunc, + export function unbindAction( + action: A, + handler: TActionFunc, ) - export const invokeAction = ( - action: A, - args?: TArgOfAction, - trigger?: TInvocationTrigger, + export const invokeAction = ( + action: A, + args?: TArgOfAction, + trigger?: TInvocationTrigger, ) => ... types.ts - export type TActionArgsMap = { - "seek-forward": { seconds: number } | undefined; - "seek-backward": { seconds: number } | und... + export type TActionArgsMap = { + "seek-forward": { seconds: number } | undefined; + "seek-backward": { seconds: number } | undef... export type TActionWithArgs = keyof TActionArgsMap - export type TActionWithOptionalArgs = | TActionWithNoArgs + export type TActionWithOptionalArgs = | TActionWithNoArgs | TKeysWithValueUndefined export type TActionWithNoArgs = Exclude - export type TArgOfAction = A extends TActionWithArgs - ? TActionArgsMap[A] + export type TArgOfAction = A extends TActionWithArgs + ? TActionArgsMap[A] : undefined - export type TActionFunc = A extends TActionWithArgs - ? (arg: TArgOfAction, trigger?: TInvocationTrigger) => void - : (_... + export type TActionFunc = A extends TActionWithArgs + ? (arg: TArgOfAction, trigger?: TInvocationTrigger) => void + : (_?:... export type TInvocationTrigger = "keypress" | "mouseclick" - export type TBoundActionList = { - [A in TAction]?: Array>; + export type TBoundActionList = { + [A in TAction]?: Array>; } - export type TActionHandlerOptions = | MutableRefObject - | boolean + export type TActionHandlerOptions = | MutableRefObject + | boolean | undefined ## apps/web/src/lib/auth @@ -637,10 +700,10 @@ query.ts export function getSinglePost({ slug }: { slug: string }) export function getCategories() export function getAuthors() - export function processHtmlContent({ - html, - }: { - html: string; + export function processHtmlContent({ + html, + }: { + html: string; }): Promise ## apps/web/src/lib/db @@ -654,62 +717,82 @@ schema.ts export const accounts export const verifications +## apps/web/src/lib/fonts + +google-fonts.ts + export function getCachedFontAtlas(): FontAtlas | null + export function clearFontAtlasCache(): void + export function prefetchFontAtlas(): Promise + export function loadFullFont({ + family, + weights = [400, 700], + }: { + family: string; + weights?: number[]; + }): Promise + export function loadFonts({ + families, + }: { + families: string[]; + }): Promise + ## apps/web/src/lib/gradients canvas.ts - export function drawCssBackground({ - ctx, - width, - height, - css, - }: { - ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; - width: number; - height: number; - css: string; + export function drawCssBackground({ + ctx, + width, + height, + css, + }: { + ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; + width: number; + height: number; + css: string; }): void parser.ts export type GradientOrientation = LinearOrientation | Array - export type Color = | { type: "hex"; value: string } - | { type: "literal"; value: string } - | { type: "rgb"; value:... + export type Color = | { type: "hex"; value: string } + | { type: "literal"; value: string } + | { type: "rgb"; value: A... export type ColorStop = Color & { length?: Distance } - export type GradientAst = { - type: GradientType; - orientation: GradientOrientation | undefined; - colorStops: Array => ... export const GradientParser ## apps/web/src/lib/media audio.ts - export type CollectedAudioElement = Omit< - AudioElement, - "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl... + export type CollectedAudioElement = Omit< + AudioElement, + "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl" + ... export function createAudioContext(): AudioContext export interface DecodedAudio { samples: Float32Array sampleRate: number } - export function decodeAudioToFloat32({ - audioBlob, - }: { - audioBlob: Blob; + export function decodeAudioToFloat32({ + audioBlob, + }: { + audioBlob: Blob; }): Promise - export function collectAudioElements({ - tracks, - mediaAssets, - audioContext, - }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - audioContext: AudioContext; + export function collectAudioElements({ + tracks, + mediaAssets, + audioContext, + }: { + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + audioContext: AudioContext; }): Promise export interface AudioClipSource { id: string @@ -721,265 +804,553 @@ audio.ts trimEnd: number muted: boolean } - export function collectAudioMixSources({ - tracks, - mediaAssets, - }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; + export function collectAudioMixSources({ + tracks, + mediaAssets, + }: { + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; }): Promise - export function collectAudioClips({ - tracks, - mediaAssets, - }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; + export function collectAudioClips({ + tracks, + mediaAssets, + }: { + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; }): Promise - export function createTimelineAudioBuffer({ - tracks, - mediaAssets, - duration, - sampleRate = 44100, - audioContext, - }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - duration: number; - sampleRate?: number; - audioContext?: AudioContext; + export function createTimelineAudioBuffer({ + tracks, + mediaAssets, + duration, + sampleRate = 44100, + audioContext, + }: { + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + duration: number; + sampleRate?: number; + audioContext?: AudioContext; }): Promise media-utils.ts export const SUPPORTS_AUDIO: readonly MediaType[] - export function mediaSupportsAudio({ - media, - }: { - media: MediaAsset | null | undefined; + export function mediaSupportsAudio({ + media, + }: { + media: MediaAsset | null | undefined; }): boolean - export const getMediaTypeFromFile = ({ - file, - }: { - file: File; + export const getMediaTypeFromFile = ({ + file, + }: { + file: File; }): MediaType | null => ... mediabunny.ts - export function getVideoInfo({ - videoFile, - }: { - videoFile: File; - }): Promise<{ - duration: number; - width: number; - height: number; - fps: number; + export function getVideoInfo({ + videoFile, + }: { + videoFile: File; + }): Promise<{ + duration: number; + width: number; + height: number; + fps: number; }> - export const extractTimelineAudio = ({ - tracks, - mediaAssets, - totalDuration, - onProgress, - }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - totalDuration: number; - onProgress?: (progress: number) => void; + export const extractTimelineAudio = ({ + tracks, + mediaAssets, + totalDuration, + onProgress, + }: { + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + totalDuration: number; + onProgress?: (progress: number) => void; }): Promise => ... processing.ts export interface ProcessedMediaAsset extends Omit - export function generateThumbnail({ - videoFile, - timeInSeconds, - }: { - videoFile: File; - timeInSeconds: number; + export function generateThumbnail({ + videoFile, + timeInSeconds, + }: { + videoFile: File; + timeInSeconds: number; }): Promise - export function generateImageThumbnail({ - imageFile, - }: { - imageFile: File; + export function generateImageThumbnail({ + imageFile, + }: { + imageFile: File; }): Promise - export function processMediaAssets({ - files, - onProgress, - }: { - files: FileList | File[]; - onProgress?: ({ progress }: { progress: number }) => void; + export function processMediaAssets({ + files, + onProgress, + }: { + files: FileList | File[]; + onProgress?: ({ progress }: { progress: number }) => void; }): Promise +## apps/web/src/lib/preview + +element-bounds.ts + export interface ElementBounds { + cx: number + cy: number + width: number + height: number + rotation: number + } + export interface ElementWithBounds { + trackId: string + elementId: string + element: TimelineElement + bounds: ElementBounds + } + export function getElementBounds({ + element, + canvasSize, + mediaAsset, + }: { + element: TimelineElement; + canvasSize: { width: number; height: number }; + mediaAsset?: MediaAsset | null; + }): ElementBounds | null + export function getVisibleElementsWithBounds({ + tracks, + currentTime, + canvasSize, + mediaAssets, + }: { + tracks: TimelineTrack[]; + currentTime: number; + canvasSize: { width: number; height: number }; + mediaAssets: MediaAsset[]; + }): ElementWithBounds[] + +hit-test.ts + export function hitTest({ + canvasX, + canvasY, + elementsWithBounds, + }: { + canvasX: number; + canvasY: number; + elementsWithBounds: ElementWithBounds[]; + }): ElementWithBounds | null + +preview-coords.ts + export function screenToCanvas({ + clientX, + clientY, + canvas, + }: { + clientX: number; + clientY: number; + canvas: HTMLCanvasElement; + }): { x: number; y: number } + export function canvasToOverlay({ + canvasX, + canvasY, + canvasRect, + containerRect, + canvasSize, + }: { + canvasX: number; + canvasY: number; + canvasRect: DOMRect; + containerRect: DOMRect; + canvasSize: { width: number; height: number }; + }): { x: number; y: number } + export function positionToOverlay({ + positionX, + positionY, + canvasRect, + containerRect, + canvasSize, + }: { + positionX: number; + positionY: number; + canvasRect: DOMRect; + containerRect: DOMRect; + canvasSize: { width: number; height: number }; + }): { x: number; y: number } + export function getDisplayScale({ + canvasRect, + canvasSize, + }: { + canvasRect: DOMRect; + canvasSize: { width: number; height: number }; + }): { x: number; y: number } + +preview-snap.ts + export interface SnapLine { + type: "horizontal" | "vertical" + position: number + } + export const MIN_SCALE + export interface SnapResult { + snappedPosition: { x: number; y: number } + activeLines: SnapLine[] + } + export function snapPosition({ + proposedPosition, + canvasSize, + elementSize, + }: { + proposedPosition: { x: number; y: number }; + canvasSize: { width: number; height: number }; + elementSize: { width: number; height: number }; + }): SnapResult + export interface ScaleSnapResult { + snappedScale: number + activeLines: SnapLine[] + } + export function snapScale({ + proposedScale, + position, + baseWidth, + baseHeight, + canvasSize, + }: { + proposedScale: number; + position: { x: number; y: number }; + baseWidth: number; + baseHeight: number; + canvasSize: { width: number; height: number }; + }): ScaleSnapResult + export interface RotationSnapResult { + snappedRotation: number + isSnapped: boolean + } + export function snapRotation({ + proposedRotation, + }: { + proposedRotation: number; + }): RotationSnapResult + +## apps/web/src/lib/stickers + +index.ts + export function searchStickers({ + query, + category, + limit = DEFAULT_SEARCH_LIMIT, + }: { + query: string; + category: StickerCategory; + limit?: number; + }): Promise + export function browseStickers({ + category, + page = 1, + limit = DEFAULT_SEARCH_LIMIT, + }: { + category: StickerCategory; + page?: number; + limit?: number; + }): Promise + +registry.ts + export function registerProvider({ + provider, + }: { + provider: StickerProvider; + }): void + export function hasProvider({ providerId }: { providerId: string }): boolean + export function getProvider({ + providerId, + }: { + providerId: string; + }): StickerProvider + export function getAllProviders(): StickerProvider[] + +resolver.ts + export function resolveStickerId({ + stickerId, + options, + }: { + stickerId: string; + options?: StickerResolveOptions; + }): string + +sticker-id.ts + export function parseStickerId({ stickerId }: { stickerId: string }): { + providerId: string; + providerValue: string; + } + export function buildStickerId({ + providerId, + providerValue, + }: { + providerId: string; + providerValue: string; + }): string + +## apps/web/src/lib/stickers/providers + +emoji.ts + export const emojiProvider: StickerProvider + +flags.ts + export const flagsProvider: StickerProvider + +icons.ts + export const iconsProvider: StickerProvider + +index.ts + export function registerDefaultStickerProviders({ + providersToRegister = defaultProviders, + }: { + providersToRegister?: StickerProvider[]; + } = {}): void + +shapes.ts + export const shapesProvider: StickerProvider + ## apps/web/src/lib/timeline bookmarks.ts - export function findBookmarkIndex({ - bookmarks, - frameTime, - }: { - bookmarks: number[]; - frameTime: number; + export const BOOKMARK_TIME_EPSILON + export function findBookmarkIndex({ + bookmarks, + frameTime, + }: { + bookmarks: Bookmark[]; + frameTime: number; }): number - export function isBookmarkAtTime({ - bookmarks, - frameTime, - }: { - bookmarks: number[]; - frameTime: number; + export function isBookmarkAtTime({ + bookmarks, + frameTime, + }: { + bookmarks: Bookmark[]; + frameTime: number; }): boolean - export function toggleBookmarkInArray({ - bookmarks, - frameTime, + export function toggleBookmarkInArray({ + bookmarks, + frameTime, + }: { + bookmarks: Bookmark[]; + frameTime: number; + }): Bookmark[] + export function removeBookmarkFromArray({ + bookmarks, + frameTime, + }: { + bookmarks: Bookmark[]; + frameTime: number; + }): Bookmark[] + export function updateBookmarkInArray({ + bookmarks, + frameTime, + updates, + }: { + bookmarks: Bookmark[]; + frameTime: number; + updates: Partial>; + }): Bookmark[] + export function moveBookmarkInArray({ + bookmarks, + fromTime, + toTime, + }: { + bookmarks: Bookmark[]; + fromTime: number; + toTime: number; + }): Bookmark[] + export function getFrameTime({ + time, + fps, + }: { + time: number; + fps: number; + }): number + export function getBookmarkAtTime({ + bookmarks, + frameTime, + }: { + bookmarks: Bookmark[]; + frameTime: number; + }): Bookmark | null + export function getBookmarksActiveAtTime({ + bookmarks, + time, + }: { + bookmarks: Bookmark[]; + time: number; + }): Bookmark[] + +drag-utils.ts + export function getMouseTimeFromClientX({ + clientX, + containerRect, + zoomLevel, + scrollLeft, }: { - bookmarks: number[]; - frameTime: number; - }): number[] - export function removeBookmarkFromArray({ - bookmarks, - frameTime, - }: { - bookmarks: number[]; - frameTime: number; - }): number[] - export function getFrameTime({ - time, - fps, - }: { - time: number; - fps: number; + clientX: number; + containerRect: DOMRect; + zoomLevel: number; + scrollLeft: number; }): number drop-utils.ts - export function computeDropTarget({ - elementType, - mouseX, - mouseY, - tracks, - playheadTime, - isExternalDrop, - elementDuration, - pixelsPerSecond, - zoomLevel, - verticalDragDirection, - startTimeOverride, - excludeElementId, + export function computeDropTarget({ + elementType, + mouseX, + mouseY, + tracks, + playheadTime, + isExternalDrop, + elementDuration, + pixelsPerSecond, + zoomLevel, + verticalDragDirection, + startTimeOverride, + excludeElementId, }: ComputeDropTargetParams): DropTarget - export function getDropLineY({ - dropTarget, - tracks, - }: { - dropTarget: DropTarget; - tracks: TimelineTrack[]; + export function getDropLineY({ + dropTarget, + tracks, + }: { + dropTarget: DropTarget; + tracks: TimelineTrack[]; }): number element-utils.ts - export function canElementHaveAudio( - element: TimelineElement, + export function canElementHaveAudio( + element: TimelineElement, ) - export function canElementBeHidden( - element: TimelineElement, + export function isVisualElement( + element: TimelineElement, ) - export function hasMediaId( - element: TimelineElement, + export function canElementBeHidden( + element: TimelineElement, ) - export function requiresMediaId({ - element, - }: { - element: CreateTimelineElement; + export function hasMediaId( + element: TimelineElement, + ) + export function requiresMediaId({ + element, + }: { + element: CreateTimelineElement; }): boolean - export function checkElementOverlaps({ - elements, - }: { - elements: TimelineElement[]; + export function checkElementOverlaps({ + elements, + }: { + elements: TimelineElement[]; }): boolean - export function resolveElementOverlaps({ - elements, - }: { - elements: TimelineElement[]; + export function resolveElementOverlaps({ + elements, + }: { + elements: TimelineElement[]; }): TimelineElement[] - export function wouldElementOverlap({ - elements, - startTime, - endTime, - excludeElementId, - }: { - elements: TimelineElement[]; - startTime: number; - endTime: number; - excludeElementId?: string; + export function wouldElementOverlap({ + elements, + startTime, + endTime, + excludeElementId, + }: { + elements: TimelineElement[]; + startTime: number; + endTime: number; + excludeElementId?: string; }): boolean - export function buildTextElement({ - raw, - startTime, - }: { - raw: Partial>; - startTime: number; + export function buildTextElement({ + raw, + startTime, + }: { + raw: Partial>; + startTime: number; }): CreateTimelineElement - export function buildStickerElement({ - iconName, - startTime, - }: { - iconName: string; - startTime: number; + export function buildStickerElement({ + stickerId, + name, + startTime, + }: { + stickerId: string; + name?: string; + startTime: number; }): CreateStickerElement - export function buildVideoElement({ - mediaId, - name, - duration, - startTime, - }: { - mediaId: string; - name: string; - duration: number; - startTime: number; + export function buildVideoElement({ + mediaId, + name, + duration, + startTime, + }: { + mediaId: string; + name: string; + duration: number; + startTime: number; }): CreateVideoElement - export function buildImageElement({ - mediaId, - name, - duration, - startTime, - }: { - mediaId: string; - name: string; - duration: number; - startTime: number; + export function buildImageElement({ + mediaId, + name, + duration, + startTime, + }: { + mediaId: string; + name: string; + duration: number; + startTime: number; }): CreateImageElement - export function buildUploadAudioElement({ - mediaId, - name, - duration, - startTime, - buffer, - }: { - mediaId: string; - name: string; - duration: number; - startTime: number; - buffer?: AudioBuffer; + export function buildUploadAudioElement({ + mediaId, + name, + duration, + startTime, + buffer, + }: { + mediaId: string; + name: string; + duration: number; + startTime: number; + buffer?: AudioBuffer; }): CreateUploadAudioElement - export function buildLibraryAudioElement({ - sourceUrl, - name, - duration, - startTime, - buffer, - }: { - sourceUrl: string; - name: string; - duration: number; - startTime: number; - buffer?: AudioBuffer; + export function buildElementFromMedia({ + mediaId, + mediaType, + name, + duration, + startTime, + buffer, + }: { + mediaId: string; + mediaType: MediaType; + name: string; + duration: number; + startTime: number; + buffer?: AudioBuffer; + }): CreateTimelineElement + export function buildLibraryAudioElement({ + sourceUrl, + name, + duration, + startTime, + buffer, + }: { + sourceUrl: string; + name: string; + duration: number; + startTime: number; + buffer?: AudioBuffer; }): CreateLibraryAudioElement - export function getElementsAtTime({ - tracks, - time, - }: { - tracks: TimelineTrack[]; - time: number; + export function getElementsAtTime({ + tracks, + time, + }: { + tracks: TimelineTrack[]; + time: number; }): { trackId: string; elementId: string }[] + export function collectFontFamilies({ + tracks, + }: { + tracks: TimelineTrack[]; + }): string[] index.ts - export function calculateTotalDuration({ - tracks, - }: { - tracks: TimelineTrack[]; + export function calculateTotalDuration({ + tracks, + }: { + tracks: TimelineTrack[]; }): number ruler-utils.ts @@ -987,162 +1358,180 @@ ruler-utils.ts labelIntervalSeconds: number tickIntervalSeconds: number } - export function getRulerConfig({ - zoomLevel, - fps, - }: { - zoomLevel: number; - fps: number; + export function getRulerConfig({ + zoomLevel, + fps, + }: { + zoomLevel: number; + fps: number; }): RulerConfig - export function shouldShowLabel({ - time, - labelIntervalSeconds, - }: { - time: number; - labelIntervalSeconds: number; + export function shouldShowLabel({ + time, + labelIntervalSeconds, + }: { + time: number; + labelIntervalSeconds: number; }): boolean - export function formatRulerLabel({ - timeInSeconds, - fps, - }: { - timeInSeconds: number; - fps: number; + export function formatRulerLabel({ + timeInSeconds, + fps, + }: { + timeInSeconds: number; + fps: number; }): string track-utils.ts - export function canTracktHaveAudio( - track: TimelineTrack, + export function canTracktHaveAudio( + track: TimelineTrack, ) - export function canTrackBeHidden( - track: TimelineTrack, + export function canTrackBeHidden( + track: TimelineTrack, ) export function getTrackColor({ type }: { type: TrackType }) export function getTrackClasses({ type }: { type: TrackType }) export function getTrackHeight({ type }: { type: TrackType }): number - export function getCumulativeHeightBefore({ - tracks, - trackIndex, - }: { - tracks: Array<{ type: TrackType }>; - trackIndex: number; + export function getCumulativeHeightBefore({ + tracks, + trackIndex, + }: { + tracks: Array<{ type: TrackType }>; + trackIndex: number; }): number - export function getTotalTracksHeight({ - tracks, - }: { - tracks: Array<{ type: TrackType }>; + export function getTotalTracksHeight({ + tracks, + }: { + tracks: Array<{ type: TrackType }>; }): number - export function buildEmptyTrack({ - id, - type, - name, - }: { - id: string; - type: TrackType; - name?: string; + export function buildEmptyTrack({ + id, + type, + name, + }: { + id: string; + type: TrackType; + name?: string; }): TimelineTrack - export function getDefaultInsertIndexForTrack({ - tracks, - trackType, - }: { - tracks: TimelineTrack[]; - trackType: TrackType; + export function getDefaultInsertIndexForTrack({ + tracks, + trackType, + }: { + tracks: TimelineTrack[]; + trackType: TrackType; }): number - export function getHighestInsertIndexForTrack({ - tracks, - trackType, - }: { - tracks: TimelineTrack[]; - trackType: TrackType; + export function getHighestInsertIndexForTrack({ + tracks, + trackType, + }: { + tracks: TimelineTrack[]; + trackType: TrackType; }): number export function isMainTrack(track: TimelineTrack) - export function getMainTrack({ - tracks, - }: { - tracks: TimelineTrack[]; + export function getMainTrack({ + tracks, + }: { + tracks: TimelineTrack[]; }): TimelineTrack | null - export function ensureMainTrack({ - tracks, - }: { - tracks: TimelineTrack[]; + export function ensureMainTrack({ + tracks, + }: { + tracks: TimelineTrack[]; }): TimelineTrack[] - export function canElementGoOnTrack({ - elementType, - trackType, - }: { - elementType: ElementType; - trackType: TrackType; + export function canElementGoOnTrack({ + elementType, + trackType, + }: { + elementType: ElementType; + trackType: TrackType; }): boolean - export function validateElementTrackCompatibility({ - element, - track, - }: { - element: { type: ElementType }; - track: { type: TrackType }; + export function validateElementTrackCompatibility({ + element, + track, + }: { + element: { type: ElementType }; + track: { type: TrackType }; }): { isValid: boolean; errorMessage?: string } + export function getEarliestMainTrackElement({ + tracks, + excludeElementId, + }: { + tracks: TimelineTrack[]; + excludeElementId?: string; + }): TimelineElement | null + export function enforceMainTrackStart({ + tracks, + targetTrackId, + requestedStartTime, + excludeElementId, + }: { + tracks: TimelineTrack[]; + targetTrackId: string; + requestedStartTime: number; + excludeElementId?: string; + }): number zoom-utils.ts - export function getTimelineZoomMin({ - duration, - containerWidth, - }: { - duration: number; - containerWidth: number | null | undefined; + export function getTimelineZoomMin({ + duration, + containerWidth, + }: { + duration: number; + containerWidth: number | null | undefined; }): number - export function getTimelinePaddingPx({ - containerWidth, - zoomLevel, - minZoom, - }: { - containerWidth: number; - zoomLevel: number; - minZoom: number; + export function getTimelinePaddingPx({ + containerWidth, + zoomLevel, + minZoom, + }: { + containerWidth: number; + zoomLevel: number; + minZoom: number; }): number - export function getZoomPercent({ - zoomLevel, - minZoom, - }: { - zoomLevel: number; - minZoom: number; + export function getZoomPercent({ + zoomLevel, + minZoom, + }: { + zoomLevel: number; + minZoom: number; }): number - export function sliderToZoom({ - sliderPosition, - minZoom, - maxZoom = TIMELINE_CONSTANTS.ZOOM_MAX, - }: { - sliderPosition: number; - minZoom: number; - maxZoom?: number; + export function sliderToZoom({ + sliderPosition, + minZoom, + maxZoom = TIMELINE_CONSTANTS.ZOOM_MAX, + }: { + sliderPosition: number; + minZoom: number; + maxZoom?: number; }): number - export function zoomToSlider({ - zoomLevel, - minZoom, - maxZoom = TIMELINE_CONSTANTS.ZOOM_MAX, - }: { - zoomLevel: number; - minZoom: number; - maxZoom?: number; + export function zoomToSlider({ + zoomLevel, + minZoom, + maxZoom = TIMELINE_CONSTANTS.ZOOM_MAX, + }: { + zoomLevel: number; + minZoom: number; + maxZoom?: number; }): number ## apps/web/src/lib/transcription caption.ts - export function buildCaptionChunks({ - segments, - wordsPerChunk = DEFAULT_WORDS_PER_CAPTION, - minDuration = MIN_CAPTION_DURATION_SECONDS, - }: { - segments: TranscriptionSegment[]; - wordsPerChunk?: number; - minDuration?: number; + export function buildCaptionChunks({ + segments, + wordsPerChunk = DEFAULT_WORDS_PER_CAPTION, + minDuration = MIN_CAPTION_DURATION_SECONDS, + }: { + segments: TranscriptionSegment[]; + wordsPerChunk?: number; + minDuration?: number; }): CaptionChunk[] ## apps/web/src/services/renderer canvas-renderer.ts - export type CanvasRendererParams = { - width: number; - height: number; - fps: number; + export type CanvasRendererParams = { + width: number; + height: number; + fps: number; } export class CanvasRenderer { canvas: OffscreenCanvas | HTMLCanvasElement @@ -1153,33 +1542,33 @@ canvas-renderer.ts constructor({ width, height, fps }: CanvasRendererParams) setSize({ width, height }: { width: number; height: number }) async render({ node, time }: { node: BaseNode; time: number }) - async renderToCanvas({ - node, - time, - targetCanvas, - }: { - node: BaseNode; - time: number; - targetCanvas: HTMLCanvasElement; + async renderToCanvas({ + node, + time, + targetCanvas, + }: { + node: BaseNode; + time: number; + targetCanvas: HTMLCanvasElement; }) } scene-builder.ts - export type BuildSceneParams = { - canvasSize: TCanvasSize; - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - duration: ... + export type BuildSceneParams = { + canvasSize: TCanvasSize; + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + duration: numb... export function buildScene(params: BuildSceneParams) scene-exporter.ts export type ExportFormat = "mp4" | "webm" export type ExportQuality = "low" | "medium" | "high" | "very_high" - export type SceneExporterEvents = { - progress: [progress: number]; - complete: [buffer: ArrayBuffer]; - error: [error: Error]; - c... + export type SceneExporterEvents = { + progress: [progress: number]; + complete: [buffer: ArrayBuffer]; + error: [error: Error]; + cance... export class SceneExporter extends EventEmitter { renderer: CanvasRenderer format: ExportFormat @@ -1187,20 +1576,20 @@ scene-exporter.ts shouldIncludeAudio: boolean audioBuffer: AudioBuffer isCancelled - constructor({ - width, - height, - fps, - format, - quality, - shouldIncludeAudio, - audioBuffer, + constructor({ + width, + height, + fps, + format, + quality, + shouldIncludeAudio, + audioBuffer, }: ExportParams) cancel(): void - async export({ - rootNode, - }: { - rootNode: RootNode; + async export({ + rootNode, + }: { + rootNode: RootNode; }): Promise } @@ -1219,10 +1608,10 @@ indexeddb-adapter.ts async getAll(): Promise async clear(): Promise } - export function deleteDatabase({ - dbName, - }: { - dbName: string; + export function deleteDatabase({ + dbName, + }: { + dbName: string; }): Promise opfs-adapter.ts @@ -1261,19 +1650,20 @@ types.ts ephemeral?: boolean thumbnailUrl?: string } - export type SerializedScene = Omit & { - createdAt: string; - updatedAt: string; + export type SerializedScene = Omit & { + createdAt: string; + updatedAt: string; } - export type SerializedProjectMetadata = Omit< - TProjectMetadata, - "createdAt" | "updatedAt" - > & { - createdAt: string; - updatedAt: st... - export type SerializedProject = Omit & { - metadata: SerializedProjectMetadata; - scenes: Serial... + export type SerializedProjectMetadata = Omit< + TProjectMetadata, + "createdAt" | "updatedAt" + > & { + createdAt: string; + updatedAt: string; + } + export type SerializedProject = Omit & { + metadata: SerializedProjectMetadata; + scenes: Serializ... export interface StorageConfig { projectsDb: string mediaDb: string @@ -1287,11 +1677,11 @@ service.ts export const transcriptionService worker.ts - export type WorkerMessage = | { type: "init"; modelId: string } - | { type: "transcribe"; audio: Float32Array; language: stri... - export type WorkerResponse = | { type: "init-progress"; progress: number } - | { type: "init-complete" } - | { type: "init-err... + export type WorkerMessage = | { type: "init"; modelId: string } + | { type: "transcribe"; audio: Float32Array; language: strin... + export type WorkerResponse = | { type: "init-progress"; progress: number } + | { type: "init-complete" } + | { type: "init-error... ## apps/web/src/services/video-cache @@ -1299,14 +1689,14 @@ service.ts export class VideoCache { sinks initPromises - async getFrameAt({ - mediaId, - file, - time, - }: { - mediaId: string; - file: File; - time: number; + async getFrameAt({ + mediaId, + file, + time, + }: { + mediaId: string; + file: File; + time: number; }): Promise clearVideo({ mediaId }: { mediaId: string }): void clearAll(): void @@ -1345,22 +1735,15 @@ panel-store.ts export type PanelId = keyof PanelSizes export const usePanelStore +preview-store.ts + export const usePreviewStore + sounds-store.ts export const useSoundsStore stickers-store.ts export const useStickersStore -text-properties-store.ts - export type TextPropertiesTab = "text" | "transform" - export interface TextPropertiesTabMeta { - value: TextPropertiesTab - label: string - } - export const TEXT_PROPERTIES_TABS: ReadonlyArray - export function isTextPropertiesTab(value: string) - export const useTextPropertiesStore - timeline-store.ts export const useTimelineStore @@ -1374,61 +1757,61 @@ assets.ts } blog.ts - export type Post = { - id: string; - slug: string; - title: string; - content: string; - description: string; - cove... - export type Pagination = { - limit: number; - currpage: number; - nextPage: number | null; - prevPage: number | null; - to... - export type MarblePostList = { - posts: Post[]; - pagination: Pagination; + export type Post = { + id: string; + slug: string; + title: string; + content: string; + description: string; + coverImage... + export type Pagination = { + limit: number; + currpage: number; + nextPage: number | null; + prevPage: number | null; + totalIt... + export type MarblePostList = { + posts: Post[]; + pagination: Pagination; } - export type MarblePost = { - post: Post; + export type MarblePost = { + post: Post; } - export type Tag = { - id: string; - name: string; - slug: string; + export type Tag = { + id: string; + name: string; + slug: string; } - export type MarbleTag = { - tag: Tag; + export type MarbleTag = { + tag: Tag; } - export type MarbleTagList = { - tags: Tag[]; - pagination: Pagination; + export type MarbleTagList = { + tags: Tag[]; + pagination: Pagination; } - export type Category = { - id: string; - name: string; - slug: string; + export type Category = { + id: string; + name: string; + slug: string; } - export type MarbleCategory = { - category: Category; + export type MarbleCategory = { + category: Category; } - export type MarbleCategoryList = { - categories: Category[]; - pagination: Pagination; + export type MarbleCategoryList = { + categories: Category[]; + pagination: Pagination; } - export type Author = { - id: string; - name: string; - image: string; + export type Author = { + id: string; + name: string; + image: string; } - export type MarbleAuthor = { - author: Author; + export type MarbleAuthor = { + author: Author; } - export type MarbleAuthorList = { - authors: Author[]; - pagination: Pagination; + export type MarbleAuthorList = { + authors: Author[]; + pagination: Pagination; } drag.ts @@ -1442,7 +1825,7 @@ drag.ts } export interface StickerDragData extends BaseDragData { type: "sticker" - iconName: string + stickerId: string } export type TimelineDragData = MediaDragData | TextDragData | StickerDragData @@ -1469,32 +1852,57 @@ export.ts cancelled?: boolean } +fonts.ts + export interface FontOption { + value: string + label: string + category: "system" | "google" | "custom" + weights?: number[] + hasClassName?: boolean + } + export interface GoogleFontMeta { + family: string + category: string + } + export interface FontAtlasEntry { + x: number + y: number + w: number + ch: number + s: string[] + } + export interface FontAtlas { + fonts: Record + } + keybinding.ts - export type ModifierKeys = | "ctrl" - | "alt" - | "shift" - | "ctrl+shift" - | "alt+shift" - | "ctrl+alt" + export type ModifierKeys = | "ctrl" + | "alt" + | "shift" + | "ctrl+shift" + | "alt+shift" + | "ctrl+alt" | "ctrl+alt+shift" - export type Key = | "a" - | "b" - | "c" - | "d" - | "e" - | "f" - | "g" - | "h" - | "i" - | "j" - | "k" - | "l" - |... + export type Key = | "a" + | "b" + | "c" + | "d" + | "e" + | "f" + | "g" + | "h" + | "i" + | "j" + | "k" + | "l" + | "m" + | "n" + ... export type ModifierBasedShortcutKey = `${ModifierKeys}+${Key}` export type SingleCharacterShortcutKey = `${Key}` export type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey - export type KeybindingConfig = { - [key in ShortcutKey]?: TActionWithOptionalArgs; + export type KeybindingConfig = { + [key in ShortcutKey]?: TActionWithOptionalArgs; } language.ts @@ -1502,13 +1910,14 @@ language.ts export type LanguageCode = Language["code"] project.ts - export type TBackground = | { - type: "color"; - color: string; - } - | { - type: "blur"; - blurIntensity: number... + export type TBackground = | { + type: "color"; + color: string; + } + | { + type: "blur"; + blurIntensity: number; + } export interface TCanvasSize { width: number height: number @@ -1544,6 +1953,15 @@ project.ts export type TSortOrder = "asc" | "desc" export type TProjectSortOption = `${TProjectSortKey}-${TSortOrder}` +rendering.ts + export type BlendMode = | "normal" + | "darken" + | "multiply" + | "color-burn" + | "lighten" + | "screen" + | "plus-ligh... + sounds.ts export interface SoundEffect { id: number @@ -1584,18 +2002,69 @@ sounds.ts } stickers.ts - export type StickerCategory = (typeof STICKER_CATEGORIES)[number] + export type StickerCategory = keyof typeof STICKER_CATEGORIES + export interface StickerItem { + id: string + provider: string + name: string + previewUrl: string + metadata: Record + } + export interface StickerSearchResult { + items: StickerItem[] + total: number + hasMore: boolean + } + export interface StickerProviderSearchOptions { + limit?: number + } + export interface StickerProviderBrowseOptions { + page?: number + limit?: number + } + export interface StickerResolveOptions { + width?: number + height?: number + } + export interface StickerProvider { + id: string + search({ + query, + options, + }: { + query: string; + options?: StickerProviderSearchOptions; + }): Promise + browse({ + options, + }: { + options?: StickerProviderBrowseOptions; + }): Promise + resolveUrl({ + stickerId, + options, + }: { + stickerId: string; + options?: StickerResolveOptions; + }): string + } time.ts export type TTimeCode = "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF" timeline.ts + export interface Bookmark { + time: number + note?: string + color?: string + duration?: number + } export interface TScene { id: string name: string isMain: boolean tracks: TimelineTrack[] - bookmarks: number[] + bookmarks: Bookmark[] createdAt: Date updatedAt: Date } @@ -1625,9 +2094,9 @@ timeline.ts export type TimelineTrack = VideoTrack | TextTrack | AudioTrack | StickerTrack export interface Transform { scale: number - position: { - x: number; - y: number; + position: { + x: number; + y: number; } rotate: number } @@ -1647,6 +2116,7 @@ timeline.ts hidden?: boolean transform: Transform opacity: number + blendMode?: BlendMode } export interface ImageElement extends BaseTimelineElement { type: "image" @@ -1654,6 +2124,7 @@ timeline.ts hidden?: boolean transform: Transform opacity: number + blendMode?: BlendMode } export interface TextElement extends BaseTimelineElement { type: "text" @@ -1666,37 +2137,40 @@ timeline.ts fontWeight: "normal" | "bold" fontStyle: "normal" | "italic" textDecoration: "none" | "underline" | "line-through" + letterSpacing?: number + lineHeight?: number hidden?: boolean transform: Transform opacity: number + blendMode?: BlendMode } export interface StickerElement extends BaseTimelineElement { type: "sticker" - iconName: string + stickerId: string hidden?: boolean transform: Transform opacity: number - color?: string + blendMode?: BlendMode } - export type TimelineElement = | AudioElement - | VideoElement - | ImageElement - | TextElement + export type TimelineElement = | AudioElement + | VideoElement + | ImageElement + | TextElement | StickerElement export type ElementType = TimelineElement["type"] export type CreateUploadAudioElement = Omit export type CreateLibraryAudioElement = Omit - export type CreateAudioElement = | CreateUploadAudioElement + export type CreateAudioElement = | CreateUploadAudioElement | CreateLibraryAudioElement export type CreateVideoElement = Omit export type CreateImageElement = Omit export type CreateTextElement = Omit export type CreateStickerElement = Omit - export type CreateTimelineElement = | CreateAudioElement - | CreateVideoElement - | CreateImageElement - | CreateTextElement - | Crea... + export type CreateTimelineElement = | CreateAudioElement + | CreateVideoElement + | CreateImageElement + | CreateTextElement + | CreateSt... export interface ElementDragState { isDragging: boolean elementId: string | null @@ -1746,19 +2220,19 @@ transcription.ts segments: TranscriptionSegment[] language: string } - export type TranscriptionStatus = | "idle" - | "loading-model" - | "transcribing" - | "complete" + export type TranscriptionStatus = | "idle" + | "loading-model" + | "transcribing" + | "complete" | "error" export interface TranscriptionProgress { status: TranscriptionStatus progress: number message?: string } - export type TranscriptionModelId = | "whisper-tiny" - | "whisper-small" - | "whisper-medium" + export type TranscriptionModelId = | "whisper-tiny" + | "whisper-small" + | "whisper-medium" | "whisper-large-v3-turbo" export interface TranscriptionModel { id: TranscriptionModelId @@ -1775,37 +2249,76 @@ transcription.ts ## apps/web/src/utils browser.ts - export function isTypableDOMElement({ - element, - }: { - element: HTMLElement; + export function isTypableDOMElement({ + element, + }: { + element: HTMLElement; }): boolean +color.ts + export type ColorFormat = "hex" | "rgb" | "hsl" | "hsv" + export function hexToHsv({ hex }: { hex: string }): [number, number, number] + export function hsvToHex({ + h, + s, + v, + }: { h: number; s: number; v: number }): string + export function parseHexAlpha({ hex }: { hex: string }): { + rgb: string; + alpha: number; + } + export function appendAlpha({ + rgbHex, + alpha, + }: { rgbHex: string; alpha: number }): string + export function extractColorFromText({ + text, + }: { text: string }): string | null + export function formatColorValue({ + hex, + format, + }: { + hex: string; + format: ColorFormat; + }): string + export function parseColorInput({ + input, + format, + }: { + input: string; + format: ColorFormat; + }): string | null + date.ts export function formatDate({ date }: { date: Date }): string geometry.ts - export function dimensionToAspectRatio({ - width, - height, - }: { - width: number; - height: number; + export function dimensionToAspectRatio({ + width, + height, + }: { + width: number; + height: number; }): string id.ts export function generateUUID(): string math.ts - export function clamp({ - value, - min, - max, - }: { - value: number; - min: number; - max: number; + export function clamp({ + value, + min, + max, + }: { + value: number; + min: number; + max: number; }): number + export function evaluateMathExpression({ + input, + }: { + input: string; + }): number | null platform.ts export function getPlatformSpecialKey(): string @@ -1823,28 +2336,56 @@ ui.ts brand.tsx export function OcVercelIcon({ className }: { className?: string }) - export function OcMarbleIcon({ - className = "", - size = 32, - }: { - className?: string; - size?: number; + export function OcMarbleIcon({ + className = "", + size = 32, + }: { + className?: string; + size?: number; }) - export function OcDataBuddyIcon({ - className = "", - size = 32, - }: { - className?: string; - size?: number; + export function OcDataBuddyIcon({ + className = "", + size = 32, + }: { + className?: string; + size?: number; }) ui.tsx - export function OcVideoIcon({ - className = "", - size = 32, - }: { - className?: string; - size?: number; + export function OcVideoIcon({ + className = "", + size = 32, + }: { + className?: string; + size?: number; + }) + export function OcCheckerboardIcon({ + className = "", + size = 32, + }: { + className?: string; + size?: number; + }) + export function OcFontWeightIcon({ + className = "", + size = 32, + }: { + className?: string; + size?: number; + }) + export function OcSlidersVerticalIcon({ + className = "", + size = 32, + }: { + className?: string; + size?: number; + }) + export function OcSocialIcon({ + className = "", + size = 32, + }: { + className?: string; + size?: number; }) ``` diff --git a/.cursor/settings.json b/.cursor/settings.json new file mode 100644 index 00000000..28a77607 --- /dev/null +++ b/.cursor/settings.json @@ -0,0 +1,7 @@ +{ + "plugins": { + "figma": { + "enabled": true + } + } +} diff --git a/.github/workflows/bun-ci.yml b/.github/workflows/bun-ci.yml index d3e4dcb7..83c3eac9 100644 --- a/.github/workflows/bun-ci.yml +++ b/.github/workflows/bun-ci.yml @@ -27,7 +27,16 @@ jobs: NEXT_PUBLIC_SITE_URL: "http://localhost:3000" UPSTASH_REDIS_REST_URL: "https://your-upstash-redis-url" UPSTASH_REDIS_REST_TOKEN: "your-upstash-redis-token" - + NEXT_PUBLIC_MARBLE_API_URL: "https://placeholder.example.com" + MARBLE_WORKSPACE_KEY: "placeholder" + FREESOUND_CLIENT_ID: "placeholder" + FREESOUND_API_KEY: "placeholder" + CLOUDFLARE_ACCOUNT_ID: "placeholder" + R2_ACCESS_KEY_ID: "placeholder" + R2_SECRET_ACCESS_KEY: "placeholder" + R2_BUCKET_NAME: "placeholder" + MODAL_TRANSCRIPTION_URL: "https://placeholder.example.com" + steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/.vscode/settings.json b/.vscode/settings.json index 7672666a..6caa16da 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,20 +1,20 @@ { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[javascript][typescript][javascriptreact][typescriptreact][json][jsonc][css][graphql]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "typescript.tsdk": "node_modules/typescript/lib", - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "editor.codeActionsOnSave": { - "source.fixAll.biome": "explicit", - "source.organizeImports.biome": "explicit" - }, - "emmet.showExpandedAbbreviation": "never", - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - } + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript][typescript][javascriptreact][typescriptreact][json][jsonc][css][graphql]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "typescript.tsdk": "node_modules/typescript/lib", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "emmet.showExpandedAbbreviation": "never", + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + } } diff --git a/apps/web/.gitignore b/apps/web/.gitignore index dc069c57..4b8a9fe9 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -5,4 +5,6 @@ .env* !.env.example -.next/ \ No newline at end of file +.next/ + +.font-cache/ \ No newline at end of file diff --git a/apps/web/components.json b/apps/web/components.json index 3289f237..5bcedb31 100644 --- a/apps/web/components.json +++ b/apps/web/components.json @@ -1,21 +1,21 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" } diff --git a/apps/web/drizzle.config.ts b/apps/web/drizzle.config.ts index 2d53d6b9..be2a59fc 100644 --- a/apps/web/drizzle.config.ts +++ b/apps/web/drizzle.config.ts @@ -4,20 +4,20 @@ import { webEnv } from "@opencut/env/web"; // Load the right env file based on environment if (webEnv.NODE_ENV === "production") { - dotenv.config({ path: ".env.production" }); + dotenv.config({ path: ".env.production" }); } else { - dotenv.config({ path: ".env.local" }); + dotenv.config({ path: ".env.local" }); } export default { - schema: "./src/schema.ts", - dialect: "postgresql", - migrations: { - table: "drizzle_migrations", - }, - dbCredentials: { - url: webEnv.DATABASE_URL, - }, - out: "./migrations", - strict: webEnv.NODE_ENV === "production", + schema: "./src/schema.ts", + dialect: "postgresql", + migrations: { + table: "drizzle_migrations", + }, + dbCredentials: { + url: webEnv.DATABASE_URL, + }, + out: "./migrations", + strict: webEnv.NODE_ENV === "production", } satisfies Config; diff --git a/apps/web/migrations/meta/0000_snapshot.json b/apps/web/migrations/meta/0000_snapshot.json index 8d505274..2f6c9b30 100644 --- a/apps/web/migrations/meta/0000_snapshot.json +++ b/apps/web/migrations/meta/0000_snapshot.json @@ -1,344 +1,344 @@ { - "id": "33a6742f-89da-4ac5-958f-421aa1cf9bd6", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.accounts": { - "name": "accounts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id_token": { - "name": "id_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sessions_token_unique": { - "name": "sessions_token_unique", - "nullsNotDistinct": false, - "columns": ["token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.verifications": { - "name": "verifications", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.waitlist": { - "name": "waitlist", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "waitlist_email_unique": { - "name": "waitlist_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } + "id": "33a6742f-89da-4ac5-958f-421aa1cf9bd6", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } } diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json index c7d30f16..a3cb0863 100644 --- a/apps/web/migrations/meta/_journal.json +++ b/apps/web/migrations/meta/_journal.json @@ -1,13 +1,13 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1750753385927, - "tag": "0000_brainy_saracen", - "breakpoints": true - } - ] + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1750753385927, + "tag": "0000_brainy_saracen", + "breakpoints": true + } + ] } diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index cd79efb7..fa1ac7f5 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -2,48 +2,48 @@ import type { NextConfig } from "next"; import { withBotId } from "botid/next/config"; const nextConfig: NextConfig = { - compiler: { - removeConsole: process.env.NODE_ENV === "production", - }, - reactStrictMode: true, - productionBrowserSourceMaps: true, - output: "standalone", - images: { - remotePatterns: [ - { - protocol: "https", - hostname: "plus.unsplash.com", - }, - { - protocol: "https", - hostname: "images.unsplash.com", - }, - { - protocol: "https", - hostname: "images.marblecms.com", - }, - { - protocol: "https", - hostname: "lh3.googleusercontent.com", - }, - { - protocol: "https", - hostname: "avatars.githubusercontent.com", - }, - { - protocol: "https", - hostname: "api.iconify.design", - }, - { - protocol: "https", - hostname: "api.simplesvg.com", - }, - { - protocol: "https", - hostname: "api.unisvg.com", - }, - ], - }, + compiler: { + removeConsole: process.env.NODE_ENV === "production", + }, + reactStrictMode: true, + productionBrowserSourceMaps: true, + output: "standalone", + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "plus.unsplash.com", + }, + { + protocol: "https", + hostname: "images.unsplash.com", + }, + { + protocol: "https", + hostname: "images.marblecms.com", + }, + { + protocol: "https", + hostname: "lh3.googleusercontent.com", + }, + { + protocol: "https", + hostname: "avatars.githubusercontent.com", + }, + { + protocol: "https", + hostname: "api.iconify.design", + }, + { + protocol: "https", + hostname: "api.simplesvg.com", + }, + { + protocol: "https", + hostname: "api.unisvg.com", + }, + ], + }, }; export default withBotId(nextConfig); diff --git a/apps/web/package.json b/apps/web/package.json index 31582be1..9a57a379 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,98 +1,107 @@ { - "name": "@opencut/web", - "version": "0.1.0", - "private": true, - "packageManager": "bun@1.2.18", - "scripts": { - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", - "lint": "biome check src/", - "lint:fix": "biome check src/ --write", - "format": "biome format src/ --write", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:push:local": "cross-env NODE_ENV=development drizzle-kit push", - "db:push:prod": "cross-env NODE_ENV=production drizzle-kit push" - }, - "dependencies": { - "@ffmpeg/core": "^0.12.10", - "@ffmpeg/ffmpeg": "^0.12.15", - "@ffmpeg/util": "^0.12.2", - "@hello-pangea/dnd": "^18.0.1", - "@hookform/resolvers": "^3.9.1", - "@hugeicons/core-free-icons": "^3.1.1", - "@hugeicons/react": "^1.1.4", - "@huggingface/transformers": "^3.8.1", - "@opencut/env": "workspace:*", - "@opencut/ui": "workspace:*", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tooltip": "^1.2.8", - "@upstash/ratelimit": "^2.0.6", - "@upstash/redis": "^1.35.4", - "@vercel/analytics": "^1.4.1", - "aws4fetch": "^1.0.20", - "better-auth": "^1.2.7", - "botid": "^1.4.2", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.0.0", - "dayjs": "^1.11.13", - "drizzle-orm": "^0.44.2", - "embla-carousel-react": "^8.5.1", - "eventemitter3": "^5.0.1", - "feed": "^5.1.0", - "input-otp": "^1.4.1", - "lucide-react": "^0.562.0", - "mediabunny": "^1.29.1", - "motion": "^12.18.1", - "nanoid": "^5.1.5", - "next": "16.1.3", - "next-themes": "^0.4.4", - "pg": "^8.16.2", - "postgres": "^3.4.5", - "radix-ui": "^1.4.2", - "react": "^19.0.0", - "react-day-picker": "^8.10.1", - "react-dom": "^19.0.0", - "react-hook-form": "^7.54.0", - "react-icons": "^5.4.0", - "react-markdown": "^10.1.0", - "react-phone-number-input": "^3.4.11", - "react-resizable-panels": "^2.1.7", - "recharts": "^2.14.1", - "rehype-autolink-headings": "^7.1.0", - "rehype-parse": "^9.0.1", - "rehype-sanitize": "^6.0.0", - "rehype-slug": "^6.0.0", - "rehype-stringify": "^10.0.1", - "sonner": "^1.7.1", - "tailwind-merge": "^2.5.5", - "tailwindcss-animate": "^1.0.7", - "unified": "^11.0.5", - "use-deep-compare-effect": "^1.8.1", - "vaul": "^1.1.1", - "wavesurfer.js": "^7.9.8", - "zod": "^3.25.67", - "zustand": "^5.0.2" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.1.11", - "@tailwindcss/typography": "^0.5.16", - "@types/bun": "latest", - "@types/node": "^24.2.1", - "@types/pg": "^8.15.4", - "@types/react": "^19", - "@types/react-dom": "^19", - "cross-env": "^7.0.3", - "drizzle-kit": "^0.31.4", - "dotenv": "^16.5.0", - "postcss": "^8", - "tailwindcss": "^4.1.11", - "tsx": "^4.7.1", - "typescript": "^5.8.3" - } + "name": "@opencut/web", + "version": "0.1.0", + "private": true, + "packageManager": "bun@1.2.18", + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "biome check src/", + "lint:fix": "biome check src/ --write", + "format": "biome format src/ --write", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push:local": "cross-env NODE_ENV=development drizzle-kit push", + "db:push:prod": "cross-env NODE_ENV=production drizzle-kit push" + }, + "dependencies": { + "@ffmpeg/core": "^0.12.10", + "@ffmpeg/ffmpeg": "^0.12.15", + "@ffmpeg/util": "^0.12.2", + "@hello-pangea/dnd": "^18.0.1", + "@hookform/resolvers": "^3.9.1", + "@hugeicons/core-free-icons": "^3.1.1", + "@hugeicons/react": "^1.1.4", + "@huggingface/transformers": "^3.8.1", + "@opencut/env": "workspace:*", + "@opencut/ui": "workspace:*", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-primitive": "^2.1.4", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tooltip": "^1.2.8", + "@types/culori": "^4.0.1", + "@upstash/ratelimit": "^2.0.6", + "@upstash/redis": "^1.35.4", + "@vercel/analytics": "^1.4.1", + "aws4fetch": "^1.0.20", + "better-auth": "^1.2.7", + "botid": "^1.4.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "culori": "^4.0.2", + "dayjs": "^1.11.13", + "drizzle-orm": "^0.44.2", + "embla-carousel-react": "^8.5.1", + "eventemitter3": "^5.0.1", + "feed": "^5.1.0", + "input-otp": "^1.4.1", + "lucide-react": "^0.562.0", + "mediabunny": "^1.29.1", + "motion": "^12.18.1", + "nanoid": "^5.1.5", + "next": "16.1.3", + "next-themes": "^0.4.4", + "pg": "^8.16.2", + "postgres": "^3.4.5", + "radix-ui": "^1.4.3", + "react": "^19.0.0", + "react-day-picker": "^8.10.1", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.0", + "react-icons": "^5.4.0", + "react-markdown": "^10.1.0", + "react-phone-number-input": "^3.4.11", + "react-resizable-panels": "^2.1.7", + "react-window": "^2.2.7", + "recharts": "^2.14.1", + "rehype-autolink-headings": "^7.1.0", + "rehype-parse": "^9.0.1", + "rehype-sanitize": "^6.0.0", + "rehype-slug": "^6.0.0", + "rehype-stringify": "^10.0.1", + "sonner": "^1.7.1", + "tailwind-merge": "^2.5.5", + "tailwindcss-animate": "^1.0.7", + "unified": "^11.0.5", + "use-deep-compare-effect": "^1.8.1", + "vaul": "^1.1.2", + "wavesurfer.js": "^7.9.8", + "zod": "^3.25.67", + "zustand": "^5.0.2" + }, + "devDependencies": { + "@napi-rs/canvas": "^0.1.92", + "@tailwindcss/postcss": "^4.1.11", + "@tailwindcss/typography": "^0.5.16", + "@types/bun": "latest", + "@types/node": "^24.2.1", + "@types/pg": "^8.15.4", + "@types/react": "^19", + "@types/react-dom": "^19", + "cross-env": "^7.0.3", + "dotenv": "^16.5.0", + "drizzle-kit": "^0.31.4", + "postcss": "^8", + "sharp": "^0.34.5", + "tailwindcss": "^4.1.11", + "tsx": "^4.7.1", + "typescript": "^5.8.3" + } } diff --git a/apps/web/postcss.config.mjs b/apps/web/postcss.config.mjs index 79bcf135..6ba14df9 100644 --- a/apps/web/postcss.config.mjs +++ b/apps/web/postcss.config.mjs @@ -1,8 +1,8 @@ /** @type {import('postcss-load-config').Config} */ const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, + plugins: { + "@tailwindcss/postcss": {}, + }, }; export default config; diff --git a/apps/web/public/countries.json b/apps/web/public/countries.json new file mode 100644 index 00000000..8d53cfae --- /dev/null +++ b/apps/web/public/countries.json @@ -0,0 +1,1794 @@ +[ + { + "name": "Andorra", + "code": "AD", + "languages": ["catalan"], + "flag_colors": ["blue", "yellow", "red"], + "region": "Western Europe" + }, + { + "name": "United Arab Emirates", + "code": "AE", + "languages": ["arabic"], + "flag_colors": ["red", "green", "white", "black"], + "region": "Middle East" + }, + { + "name": "Afghanistan", + "code": "AF", + "languages": ["dari", "pashto"], + "flag_colors": ["black", "red", "green"], + "region": "South Asia" + }, + { + "name": "Antigua and Barbuda", + "code": "AG", + "languages": ["english"], + "flag_colors": ["red", "black", "blue", "white", "yellow"], + "region": "Caribbean" + }, + { + "name": "Anguilla", + "code": "AI", + "languages": ["english"], + "flag_colors": ["blue", "white", "orange"], + "region": "Caribbean" + }, + { + "name": "Albania", + "code": "AL", + "languages": ["albanian"], + "flag_colors": ["red", "black"], + "region": "Southern Europe" + }, + { + "name": "Armenia", + "code": "AM", + "languages": ["armenian"], + "flag_colors": ["red", "blue", "orange"], + "region": "Middle East" + }, + { + "name": "Angola", + "code": "AO", + "languages": ["portuguese"], + "flag_colors": ["red", "black", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Antarctica", + "code": "AQ", + "languages": [], + "flag_colors": [], + "region": "Antarctica" + }, + { + "name": "Argentina", + "code": "AR", + "languages": ["spanish"], + "flag_colors": ["blue", "white", "yellow"], + "region": "South America" + }, + { + "name": "American Samoa", + "code": "AS", + "languages": ["english", "samoan"], + "flag_colors": ["red", "white", "blue"], + "region": "Oceania" + }, + { + "name": "Austria", + "code": "AT", + "languages": ["german"], + "flag_colors": ["red", "white"], + "region": "Western Europe" + }, + { + "name": "Australia", + "code": "AU", + "languages": ["english"], + "flag_colors": ["blue", "white", "red"], + "region": "Oceania" + }, + { + "name": "Aruba", + "code": "AW", + "languages": ["dutch", "papiamento"], + "flag_colors": ["blue", "yellow", "red", "white"], + "region": "Caribbean" + }, + { + "name": "Aland Islands", + "code": "AX", + "languages": ["swedish"], + "flag_colors": ["blue", "yellow", "red"], + "region": "Northern Europe" + }, + { + "name": "Azerbaijan", + "code": "AZ", + "languages": ["azerbaijani"], + "flag_colors": ["blue", "red", "green", "white"], + "region": "Middle East" + }, + { + "name": "Bosnia and Herzegovina", + "code": "BA", + "languages": ["bosnian", "croatian", "serbian"], + "flag_colors": ["blue", "yellow", "white"], + "region": "Southern Europe" + }, + { + "name": "Barbados", + "code": "BB", + "languages": ["english"], + "flag_colors": ["blue", "yellow", "black"], + "region": "Caribbean" + }, + { + "name": "Bangladesh", + "code": "BD", + "languages": ["bengali"], + "flag_colors": ["green", "red"], + "region": "South Asia" + }, + { + "name": "Belgium", + "code": "BE", + "languages": ["french", "dutch", "german"], + "flag_colors": ["black", "yellow", "red"], + "region": "Western Europe" + }, + { + "name": "Burkina Faso", + "code": "BF", + "languages": ["french"], + "flag_colors": ["red", "green", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Bulgaria", + "code": "BG", + "languages": ["bulgarian"], + "flag_colors": ["white", "green", "red"], + "region": "Eastern Europe" + }, + { + "name": "Bahrain", + "code": "BH", + "languages": ["arabic"], + "flag_colors": ["red", "white"], + "region": "Middle East" + }, + { + "name": "Burundi", + "code": "BI", + "languages": ["kirundi", "french", "english"], + "flag_colors": ["red", "green", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Benin", + "code": "BJ", + "languages": ["french"], + "flag_colors": ["green", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Saint Barthelemy", + "code": "BL", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Bermuda", + "code": "BM", + "languages": ["english"], + "flag_colors": ["blue", "white", "red"], + "region": "North Atlantic" + }, + { + "name": "Brunei", + "code": "BN", + "languages": ["malay"], + "flag_colors": ["yellow", "white", "black", "red"], + "region": "Southeast Asia" + }, + { + "name": "Bolivia", + "code": "BO", + "languages": ["spanish", "quechua", "aymara"], + "flag_colors": ["red", "yellow", "green"], + "region": "South America" + }, + { + "name": "Sint Eustatius", + "code": "BQ-SE", + "languages": ["dutch", "english"], + "flag_colors": ["blue", "white", "red", "yellow"], + "region": "Caribbean" + }, + { + "name": "Caribbean Netherlands", + "code": "BQ", + "languages": ["dutch", "papiamento", "english"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Brazil", + "code": "BR", + "languages": ["portuguese"], + "flag_colors": ["green", "yellow", "blue", "white"], + "region": "South America" + }, + { + "name": "Bahamas", + "code": "BS", + "languages": ["english"], + "flag_colors": ["blue", "yellow", "black"], + "region": "Caribbean" + }, + { + "name": "Bhutan", + "code": "BT", + "languages": ["dzongkha"], + "flag_colors": ["yellow", "orange", "white"], + "region": "South Asia" + }, + { + "name": "Bouvet Island", + "code": "BV", + "languages": [], + "flag_colors": ["red", "white", "blue"], + "region": "Antarctica" + }, + { + "name": "Botswana", + "code": "BW", + "languages": ["english", "tswana"], + "flag_colors": ["blue", "black", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Belarus", + "code": "BY", + "languages": ["belarusian", "russian"], + "flag_colors": ["red", "green", "white"], + "region": "Eastern Europe" + }, + { + "name": "Belize", + "code": "BZ", + "languages": ["english"], + "flag_colors": ["blue", "red", "white"], + "region": "Central America" + }, + { + "name": "Canada", + "code": "CA", + "languages": ["english", "french"], + "flag_colors": ["red", "white"], + "region": "North America" + }, + { + "name": "Cocos (Keeling) Islands", + "code": "CC", + "languages": ["english"], + "flag_colors": ["green", "red", "yellow", "white"], + "region": "Southeast Asia" + }, + { + "name": "Democratic Republic of the Congo", + "code": "CD", + "languages": ["french", "lingala", "swahili", "tshiluba", "kikongo"], + "flag_colors": ["blue", "red", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Central African Republic", + "code": "CF", + "languages": ["french", "sango"], + "flag_colors": ["blue", "white", "green", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Republic of the Congo", + "code": "CG", + "languages": ["french", "lingala", "kituba"], + "flag_colors": ["green", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Switzerland", + "code": "CH", + "languages": ["german", "french", "italian"], + "flag_colors": ["red", "white"], + "region": "Western Europe" + }, + { + "name": "Cote d'Ivoire", + "code": "CI", + "languages": ["french"], + "flag_colors": ["orange", "white", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Cook Islands", + "code": "CK", + "languages": ["english", "cook islands maori"], + "flag_colors": ["blue", "white"], + "region": "Oceania" + }, + { + "name": "Chile", + "code": "CL", + "languages": ["spanish"], + "flag_colors": ["white", "red", "blue"], + "region": "South America" + }, + { + "name": "Cameroon", + "code": "CM", + "languages": ["english", "french"], + "flag_colors": ["green", "red", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "China", + "code": "CN", + "languages": ["mandarin"], + "flag_colors": ["red", "yellow"], + "region": "East Asia" + }, + { + "name": "Colombia", + "code": "CO", + "languages": ["spanish"], + "flag_colors": ["yellow", "blue", "red"], + "region": "South America" + }, + { + "name": "Costa Rica", + "code": "CR", + "languages": ["spanish"], + "flag_colors": ["blue", "white", "red"], + "region": "Central America" + }, + { + "name": "Cuba", + "code": "CU", + "languages": ["spanish"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Cape Verde", + "code": "CV", + "languages": ["portuguese"], + "flag_colors": ["blue", "white", "red", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Curacao", + "code": "CW", + "languages": ["dutch", "papiamento", "english"], + "flag_colors": ["blue", "yellow", "white"], + "region": "Caribbean" + }, + { + "name": "Christmas Island", + "code": "CX", + "languages": ["english"], + "flag_colors": ["green", "blue", "yellow", "white"], + "region": "Oceania" + }, + { + "name": "Cyprus", + "code": "CY", + "languages": ["greek", "turkish"], + "flag_colors": ["white", "orange", "green"], + "region": "Middle East" + }, + { + "name": "Czechia", + "code": "CZ", + "languages": ["czech"], + "flag_colors": ["white", "red", "blue"], + "region": "Eastern Europe" + }, + { + "name": "Germany", + "code": "DE", + "languages": ["german"], + "flag_colors": ["black", "red", "yellow"], + "region": "Western Europe" + }, + { + "name": "Djibouti", + "code": "DJ", + "languages": ["arabic", "french", "afar", "somali"], + "flag_colors": ["blue", "green", "white", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Denmark", + "code": "DK", + "languages": ["danish"], + "flag_colors": ["red", "white"], + "region": "Northern Europe" + }, + { + "name": "Dominica", + "code": "DM", + "languages": ["english"], + "flag_colors": ["green", "yellow", "black", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Dominican Republic", + "code": "DO", + "languages": ["spanish"], + "flag_colors": ["red", "white", "blue"], + "region": "Caribbean" + }, + { + "name": "Algeria", + "code": "DZ", + "languages": ["arabic", "tamazight"], + "flag_colors": ["green", "white", "red"], + "region": "North Africa" + }, + { + "name": "Ecuador", + "code": "EC", + "languages": ["spanish"], + "flag_colors": ["yellow", "blue", "red"], + "region": "South America" + }, + { + "name": "Estonia", + "code": "EE", + "languages": ["estonian"], + "flag_colors": ["blue", "black", "white"], + "region": "Northern Europe" + }, + { + "name": "Egypt", + "code": "EG", + "languages": ["arabic"], + "flag_colors": ["red", "white", "black", "yellow"], + "region": "North Africa" + }, + { + "name": "Western Sahara", + "code": "EH", + "languages": ["arabic", "spanish"], + "flag_colors": ["black", "white", "green", "red"], + "region": "North Africa" + }, + { + "name": "Eritrea", + "code": "ER", + "languages": ["tigrinya", "arabic", "english"], + "flag_colors": ["red", "green", "blue", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Spain", + "code": "ES", + "languages": ["spanish"], + "flag_colors": ["red", "yellow"], + "region": "Southern Europe" + }, + { + "name": "Ethiopia", + "code": "ET", + "languages": ["amharic", "oromo"], + "flag_colors": ["green", "yellow", "red", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "European Union", + "code": "EU", + "languages": ["english", "french", "german"], + "flag_colors": ["blue", "yellow"], + "region": "Western Europe" + }, + { + "name": "Finland", + "code": "FI", + "languages": ["finnish", "swedish"], + "flag_colors": ["white", "blue"], + "region": "Northern Europe" + }, + { + "name": "Fiji", + "code": "FJ", + "languages": ["english", "fijian", "hindi"], + "flag_colors": ["blue", "white", "red", "yellow"], + "region": "Oceania" + }, + { + "name": "Falkland Islands", + "code": "FK", + "languages": ["english"], + "flag_colors": ["blue", "white", "red", "yellow"], + "region": "South America" + }, + { + "name": "Micronesia", + "code": "FM", + "languages": ["english"], + "flag_colors": ["blue", "white"], + "region": "Oceania" + }, + { + "name": "Faroe Islands", + "code": "FO", + "languages": ["faroese", "danish"], + "flag_colors": ["white", "red", "blue"], + "region": "Northern Europe" + }, + { + "name": "France", + "code": "FR", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Western Europe" + }, + { + "name": "Gabon", + "code": "GA", + "languages": ["french"], + "flag_colors": ["green", "yellow", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "England", + "code": "GB-ENG", + "languages": ["english"], + "flag_colors": ["red", "white"], + "region": "Western Europe" + }, + { + "name": "Northern Ireland", + "code": "GB-NIR", + "languages": ["english", "irish"], + "flag_colors": ["white", "red"], + "region": "Western Europe" + }, + { + "name": "Scotland", + "code": "GB-SCT", + "languages": ["english", "scottish gaelic"], + "flag_colors": ["blue", "white"], + "region": "Western Europe" + }, + { + "name": "Wales", + "code": "GB-WLS", + "languages": ["english", "welsh"], + "flag_colors": ["green", "white", "red"], + "region": "Western Europe" + }, + { + "name": "United Kingdom", + "code": "GB", + "languages": ["english"], + "flag_colors": ["red", "white", "blue"], + "region": "Western Europe" + }, + { + "name": "Grenada", + "code": "GD", + "languages": ["english"], + "flag_colors": ["red", "yellow", "green", "black"], + "region": "Caribbean" + }, + { + "name": "Georgia", + "code": "GE", + "languages": ["georgian"], + "flag_colors": ["white", "red"], + "region": "Middle East" + }, + { + "name": "French Guiana", + "code": "GF", + "languages": ["french"], + "flag_colors": ["green", "yellow", "red"], + "region": "South America" + }, + { + "name": "Guernsey", + "code": "GG", + "languages": ["english", "french"], + "flag_colors": ["white", "red", "yellow"], + "region": "Western Europe" + }, + { + "name": "Ghana", + "code": "GH", + "languages": ["english"], + "flag_colors": ["red", "yellow", "green", "black"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Gibraltar", + "code": "GI", + "languages": ["english"], + "flag_colors": ["red", "white", "yellow"], + "region": "Southern Europe" + }, + { + "name": "Greenland", + "code": "GL", + "languages": ["kalaallisut", "danish"], + "flag_colors": ["red", "white"], + "region": "North America" + }, + { + "name": "Gambia", + "code": "GM", + "languages": ["english"], + "flag_colors": ["red", "blue", "green", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Guinea", + "code": "GN", + "languages": ["french"], + "flag_colors": ["red", "yellow", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Guadeloupe", + "code": "GP", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Equatorial Guinea", + "code": "GQ", + "languages": ["spanish", "french", "portuguese"], + "flag_colors": ["green", "white", "red", "blue", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Greece", + "code": "GR", + "languages": ["greek"], + "flag_colors": ["blue", "white"], + "region": "Southern Europe" + }, + { + "name": "South Georgia and the South Sandwich Islands", + "code": "GS", + "languages": [], + "flag_colors": ["blue", "white", "yellow"], + "region": "Atlantic Ocean" + }, + { + "name": "Guatemala", + "code": "GT", + "languages": ["spanish"], + "flag_colors": ["blue", "white"], + "region": "Central America" + }, + { + "name": "Guam", + "code": "GU", + "languages": ["english", "chamorro"], + "flag_colors": ["blue", "red"], + "region": "Oceania" + }, + { + "name": "Guinea-Bissau", + "code": "GW", + "languages": ["portuguese"], + "flag_colors": ["red", "yellow", "green", "black"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Guyana", + "code": "GY", + "languages": ["english"], + "flag_colors": ["green", "yellow", "red", "black", "white"], + "region": "South America" + }, + { + "name": "Hong Kong", + "code": "HK", + "languages": ["cantonese", "english"], + "flag_colors": ["red", "white"], + "region": "East Asia" + }, + { + "name": "Heard Island and McDonald Islands", + "code": "HM", + "languages": [], + "flag_colors": ["blue", "white", "red"], + "region": "Oceania" + }, + { + "name": "Honduras", + "code": "HN", + "languages": ["spanish"], + "flag_colors": ["blue", "white"], + "region": "Central America" + }, + { + "name": "Croatia", + "code": "HR", + "languages": ["croatian"], + "flag_colors": ["red", "white", "blue"], + "region": "Southern Europe" + }, + { + "name": "Haiti", + "code": "HT", + "languages": ["haitian creole", "french"], + "flag_colors": ["blue", "red", "white"], + "region": "Caribbean" + }, + { + "name": "Hungary", + "code": "HU", + "languages": ["hungarian"], + "flag_colors": ["red", "white", "green"], + "region": "Eastern Europe" + }, + { + "name": "Indonesia", + "code": "ID", + "languages": ["indonesian"], + "flag_colors": ["red", "white"], + "region": "Southeast Asia" + }, + { + "name": "Ireland", + "code": "IE", + "languages": ["english", "irish"], + "flag_colors": ["green", "white", "orange"], + "region": "Western Europe" + }, + { + "name": "Israel", + "code": "IL", + "languages": ["hebrew", "arabic"], + "flag_colors": ["blue", "white"], + "region": "Middle East" + }, + { + "name": "Isle of Man", + "code": "IM", + "languages": ["english", "manx"], + "flag_colors": ["red", "yellow"], + "region": "Western Europe" + }, + { + "name": "India", + "code": "IN", + "languages": ["hindi", "english"], + "flag_colors": ["orange", "white", "green", "blue"], + "region": "South Asia" + }, + { + "name": "British Indian Ocean Territory", + "code": "IO", + "languages": ["english"], + "flag_colors": ["blue", "white", "red", "yellow"], + "region": "South Asia" + }, + { + "name": "Iraq", + "code": "IQ", + "languages": ["arabic", "kurdish"], + "flag_colors": ["red", "white", "black", "green"], + "region": "Middle East" + }, + { + "name": "Iran", + "code": "IR", + "languages": ["persian"], + "flag_colors": ["green", "white", "red"], + "region": "Middle East" + }, + { + "name": "Iceland", + "code": "IS", + "languages": ["icelandic"], + "flag_colors": ["blue", "white", "red"], + "region": "Northern Europe" + }, + { + "name": "Italy", + "code": "IT", + "languages": ["italian"], + "flag_colors": ["green", "white", "red"], + "region": "Southern Europe" + }, + { + "name": "Jersey", + "code": "JE", + "languages": ["english", "french"], + "flag_colors": ["white", "red", "yellow"], + "region": "Western Europe" + }, + { + "name": "Jamaica", + "code": "JM", + "languages": ["english"], + "flag_colors": ["green", "yellow", "black"], + "region": "Caribbean" + }, + { + "name": "Jordan", + "code": "JO", + "languages": ["arabic"], + "flag_colors": ["black", "white", "green", "red"], + "region": "Middle East" + }, + { + "name": "Japan", + "code": "JP", + "languages": ["japanese"], + "flag_colors": ["red", "white"], + "region": "East Asia" + }, + { + "name": "Kenya", + "code": "KE", + "languages": ["english", "swahili"], + "flag_colors": ["black", "red", "green", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Kyrgyzstan", + "code": "KG", + "languages": ["kyrgyz", "russian"], + "flag_colors": ["red", "yellow"], + "region": "Central Asia" + }, + { + "name": "Cambodia", + "code": "KH", + "languages": ["khmer"], + "flag_colors": ["blue", "red", "white"], + "region": "Southeast Asia" + }, + { + "name": "Kiribati", + "code": "KI", + "languages": ["english", "kiribati"], + "flag_colors": ["red", "blue", "white", "yellow"], + "region": "Oceania" + }, + { + "name": "Comoros", + "code": "KM", + "languages": ["comorian", "arabic", "french"], + "flag_colors": ["green", "yellow", "white", "red", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Saint Kitts and Nevis", + "code": "KN", + "languages": ["english"], + "flag_colors": ["green", "red", "black", "yellow", "white"], + "region": "Caribbean" + }, + { + "name": "North Korea", + "code": "KP", + "languages": ["korean"], + "flag_colors": ["red", "blue", "white"], + "region": "East Asia" + }, + { + "name": "South Korea", + "code": "KR", + "languages": ["korean"], + "flag_colors": ["white", "red", "blue", "black"], + "region": "East Asia" + }, + { + "name": "Kuwait", + "code": "KW", + "languages": ["arabic"], + "flag_colors": ["green", "white", "red", "black"], + "region": "Middle East" + }, + { + "name": "Cayman Islands", + "code": "KY", + "languages": ["english"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Kazakhstan", + "code": "KZ", + "languages": ["kazakh", "russian"], + "flag_colors": ["blue", "yellow"], + "region": "Central Asia" + }, + { + "name": "Laos", + "code": "LA", + "languages": ["lao"], + "flag_colors": ["red", "blue", "white"], + "region": "Southeast Asia" + }, + { + "name": "Lebanon", + "code": "LB", + "languages": ["arabic"], + "flag_colors": ["red", "white", "green"], + "region": "Middle East" + }, + { + "name": "Saint Lucia", + "code": "LC", + "languages": ["english"], + "flag_colors": ["blue", "yellow", "black", "white"], + "region": "Caribbean" + }, + { + "name": "Liechtenstein", + "code": "LI", + "languages": ["german"], + "flag_colors": ["blue", "red", "yellow"], + "region": "Western Europe" + }, + { + "name": "Sri Lanka", + "code": "LK", + "languages": ["sinhala", "tamil"], + "flag_colors": ["yellow", "green", "orange", "red"], + "region": "South Asia" + }, + { + "name": "Liberia", + "code": "LR", + "languages": ["english"], + "flag_colors": ["red", "white", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Lesotho", + "code": "LS", + "languages": ["sesotho", "english"], + "flag_colors": ["blue", "white", "green", "black"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Lithuania", + "code": "LT", + "languages": ["lithuanian"], + "flag_colors": ["yellow", "green", "red"], + "region": "Northern Europe" + }, + { + "name": "Luxembourg", + "code": "LU", + "languages": ["luxembourgish", "french", "german"], + "flag_colors": ["red", "white", "blue"], + "region": "Western Europe" + }, + { + "name": "Latvia", + "code": "LV", + "languages": ["latvian"], + "flag_colors": ["red", "white"], + "region": "Northern Europe" + }, + { + "name": "Libya", + "code": "LY", + "languages": ["arabic"], + "flag_colors": ["red", "black", "green", "white"], + "region": "North Africa" + }, + { + "name": "Morocco", + "code": "MA", + "languages": ["arabic", "tamazight"], + "flag_colors": ["red", "green"], + "region": "North Africa" + }, + { + "name": "Monaco", + "code": "MC", + "languages": ["french"], + "flag_colors": ["red", "white"], + "region": "Western Europe" + }, + { + "name": "Moldova", + "code": "MD", + "languages": ["romanian"], + "flag_colors": ["blue", "yellow", "red"], + "region": "Eastern Europe" + }, + { + "name": "Montenegro", + "code": "ME", + "languages": ["montenegrin"], + "flag_colors": ["red", "yellow"], + "region": "Southern Europe" + }, + { + "name": "Saint Martin", + "code": "MF", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Madagascar", + "code": "MG", + "languages": ["malagasy", "french"], + "flag_colors": ["white", "red", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Marshall Islands", + "code": "MH", + "languages": ["marshallese", "english"], + "flag_colors": ["blue", "white", "orange"], + "region": "Oceania" + }, + { + "name": "North Macedonia", + "code": "MK", + "languages": ["macedonian", "albanian"], + "flag_colors": ["red", "yellow"], + "region": "Southern Europe" + }, + { + "name": "Mali", + "code": "ML", + "languages": ["french"], + "flag_colors": ["green", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Myanmar", + "code": "MM", + "languages": ["burmese"], + "flag_colors": ["yellow", "green", "red", "white"], + "region": "Southeast Asia" + }, + { + "name": "Mongolia", + "code": "MN", + "languages": ["mongolian"], + "flag_colors": ["red", "blue", "yellow"], + "region": "East Asia" + }, + { + "name": "Macao", + "code": "MO", + "languages": ["cantonese", "portuguese"], + "flag_colors": ["green", "white", "yellow"], + "region": "East Asia" + }, + { + "name": "Northern Mariana Islands", + "code": "MP", + "languages": ["english", "chamorro", "carolinian"], + "flag_colors": ["blue", "white"], + "region": "Oceania" + }, + { + "name": "Martinique", + "code": "MQ", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "Mauritania", + "code": "MR", + "languages": ["arabic"], + "flag_colors": ["green", "yellow", "red"], + "region": "North Africa" + }, + { + "name": "Montserrat", + "code": "MS", + "languages": ["english"], + "flag_colors": ["blue", "green", "white", "yellow", "red"], + "region": "Caribbean" + }, + { + "name": "Malta", + "code": "MT", + "languages": ["maltese", "english"], + "flag_colors": ["white", "red"], + "region": "Southern Europe" + }, + { + "name": "Mauritius", + "code": "MU", + "languages": ["english", "french", "mauritian creole"], + "flag_colors": ["red", "blue", "yellow", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Maldives", + "code": "MV", + "languages": ["divehi"], + "flag_colors": ["red", "green", "white"], + "region": "South Asia" + }, + { + "name": "Malawi", + "code": "MW", + "languages": ["english", "chichewa"], + "flag_colors": ["black", "red", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Mexico", + "code": "MX", + "languages": ["spanish"], + "flag_colors": ["green", "white", "red"], + "region": "North America" + }, + { + "name": "Malaysia", + "code": "MY", + "languages": ["malay"], + "flag_colors": ["red", "white", "blue", "yellow"], + "region": "Southeast Asia" + }, + { + "name": "Mozambique", + "code": "MZ", + "languages": ["portuguese"], + "flag_colors": ["green", "black", "yellow", "white", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Namibia", + "code": "NA", + "languages": ["english"], + "flag_colors": ["blue", "red", "green", "white", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "New Caledonia", + "code": "NC", + "languages": ["french"], + "flag_colors": ["blue", "red", "green", "yellow", "black"], + "region": "Oceania" + }, + { + "name": "Niger", + "code": "NE", + "languages": ["french"], + "flag_colors": ["orange", "white", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Norfolk Island", + "code": "NF", + "languages": ["english", "norfuk"], + "flag_colors": ["green", "white"], + "region": "Oceania" + }, + { + "name": "Nigeria", + "code": "NG", + "languages": ["english"], + "flag_colors": ["green", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Nicaragua", + "code": "NI", + "languages": ["spanish"], + "flag_colors": ["blue", "white"], + "region": "Central America" + }, + { + "name": "Netherlands", + "code": "NL", + "languages": ["dutch"], + "flag_colors": ["red", "white", "blue"], + "region": "Western Europe" + }, + { + "name": "Norway", + "code": "NO", + "languages": ["norwegian"], + "flag_colors": ["red", "white", "blue"], + "region": "Northern Europe" + }, + { + "name": "Nepal", + "code": "NP", + "languages": ["nepali"], + "flag_colors": ["red", "blue", "white"], + "region": "South Asia" + }, + { + "name": "Nauru", + "code": "NR", + "languages": ["nauruan", "english"], + "flag_colors": ["blue", "yellow", "white"], + "region": "Oceania" + }, + { + "name": "Niue", + "code": "NU", + "languages": ["niuean", "english"], + "flag_colors": ["yellow", "blue", "white", "red"], + "region": "Oceania" + }, + { + "name": "New Zealand", + "code": "NZ", + "languages": ["english", "maori"], + "flag_colors": ["blue", "red", "white"], + "region": "Oceania" + }, + { + "name": "Oman", + "code": "OM", + "languages": ["arabic"], + "flag_colors": ["red", "white", "green"], + "region": "Middle East" + }, + { + "name": "Panama", + "code": "PA", + "languages": ["spanish"], + "flag_colors": ["red", "white", "blue"], + "region": "Central America" + }, + { + "name": "Peru", + "code": "PE", + "languages": ["spanish", "quechua", "aymara"], + "flag_colors": ["red", "white"], + "region": "South America" + }, + { + "name": "French Polynesia", + "code": "PF", + "languages": ["french", "tahitian"], + "flag_colors": ["red", "white"], + "region": "Oceania" + }, + { + "name": "Papua New Guinea", + "code": "PG", + "languages": ["english", "tok pisin", "hiri motu"], + "flag_colors": ["red", "black", "yellow", "white"], + "region": "Oceania" + }, + { + "name": "Philippines", + "code": "PH", + "languages": ["filipino", "english"], + "flag_colors": ["blue", "red", "white", "yellow"], + "region": "Southeast Asia" + }, + { + "name": "Pakistan", + "code": "PK", + "languages": ["urdu", "english"], + "flag_colors": ["green", "white"], + "region": "South Asia" + }, + { + "name": "Poland", + "code": "PL", + "languages": ["polish"], + "flag_colors": ["white", "red"], + "region": "Eastern Europe" + }, + { + "name": "Saint Pierre and Miquelon", + "code": "PM", + "languages": ["french"], + "flag_colors": ["blue", "white", "red", "yellow", "black"], + "region": "North America" + }, + { + "name": "Pitcairn Islands", + "code": "PN", + "languages": ["english", "pitkern"], + "flag_colors": ["blue", "green", "yellow", "red", "white"], + "region": "Oceania" + }, + { + "name": "Puerto Rico", + "code": "PR", + "languages": ["spanish", "english"], + "flag_colors": ["red", "white", "blue"], + "region": "Caribbean" + }, + { + "name": "Palestine", + "code": "PS", + "languages": ["arabic"], + "flag_colors": ["red", "green", "white", "black"], + "region": "Middle East" + }, + { + "name": "Portugal", + "code": "PT", + "languages": ["portuguese"], + "flag_colors": ["green", "red", "yellow"], + "region": "Southern Europe" + }, + { + "name": "Palau", + "code": "PW", + "languages": ["palauan", "english"], + "flag_colors": ["blue", "yellow"], + "region": "Oceania" + }, + { + "name": "Paraguay", + "code": "PY", + "languages": ["spanish", "guarani"], + "flag_colors": ["red", "white", "blue", "yellow"], + "region": "South America" + }, + { + "name": "Qatar", + "code": "QA", + "languages": ["arabic"], + "flag_colors": ["red", "white"], + "region": "Middle East" + }, + { + "name": "Reunion", + "code": "RE", + "languages": ["french", "reunion creole"], + "flag_colors": ["blue", "white", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Romania", + "code": "RO", + "languages": ["romanian"], + "flag_colors": ["blue", "yellow", "red"], + "region": "Eastern Europe" + }, + { + "name": "Serbia", + "code": "RS", + "languages": ["serbian"], + "flag_colors": ["red", "blue", "white", "yellow"], + "region": "Eastern Europe" + }, + { + "name": "Russia", + "code": "RU", + "languages": ["russian"], + "flag_colors": ["white", "blue", "red"], + "region": "Eastern Europe" + }, + { + "name": "Rwanda", + "code": "RW", + "languages": ["kinyarwanda", "french", "english"], + "flag_colors": ["blue", "yellow", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Saudi Arabia", + "code": "SA", + "languages": ["arabic"], + "flag_colors": ["green", "white"], + "region": "Middle East" + }, + { + "name": "Solomon Islands", + "code": "SB", + "languages": ["english"], + "flag_colors": ["blue", "green", "yellow", "white"], + "region": "Oceania" + }, + { + "name": "Seychelles", + "code": "SC", + "languages": ["seychellois creole", "english", "french"], + "flag_colors": ["blue", "yellow", "red", "white", "green"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Sudan", + "code": "SD", + "languages": ["arabic", "english"], + "flag_colors": ["red", "white", "black", "green"], + "region": "North Africa" + }, + { + "name": "Sweden", + "code": "SE", + "languages": ["swedish"], + "flag_colors": ["blue", "yellow"], + "region": "Northern Europe" + }, + { + "name": "Singapore", + "code": "SG", + "languages": ["english", "malay", "mandarin", "tamil"], + "flag_colors": ["red", "white"], + "region": "Southeast Asia" + }, + { + "name": "Saint Helena", + "code": "SH", + "languages": ["english"], + "flag_colors": ["blue", "red", "white", "yellow", "green"], + "region": "Atlantic Ocean" + }, + { + "name": "Slovenia", + "code": "SI", + "languages": ["slovenian"], + "flag_colors": ["white", "blue", "red"], + "region": "Southern Europe" + }, + { + "name": "Svalbard and Jan Mayen", + "code": "SJ", + "languages": ["norwegian"], + "flag_colors": ["red", "white", "blue"], + "region": "Northern Europe" + }, + { + "name": "Slovakia", + "code": "SK", + "languages": ["slovak"], + "flag_colors": ["white", "blue", "red"], + "region": "Eastern Europe" + }, + { + "name": "Sierra Leone", + "code": "SL", + "languages": ["english"], + "flag_colors": ["green", "white", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "San Marino", + "code": "SM", + "languages": ["italian"], + "flag_colors": ["white", "blue"], + "region": "Southern Europe" + }, + { + "name": "Senegal", + "code": "SN", + "languages": ["french"], + "flag_colors": ["green", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Somalia", + "code": "SO", + "languages": ["somali", "arabic"], + "flag_colors": ["blue", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Suriname", + "code": "SR", + "languages": ["dutch"], + "flag_colors": ["green", "white", "red", "yellow"], + "region": "South America" + }, + { + "name": "South Sudan", + "code": "SS", + "languages": ["english"], + "flag_colors": ["black", "red", "green", "blue", "white", "yellow"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Sao Tome and Principe", + "code": "ST", + "languages": ["portuguese"], + "flag_colors": ["green", "yellow", "red", "black"], + "region": "Sub-Saharan Africa" + }, + { + "name": "El Salvador", + "code": "SV", + "languages": ["spanish"], + "flag_colors": ["blue", "white"], + "region": "Central America" + }, + { + "name": "Sint Maarten", + "code": "SX", + "languages": ["dutch", "english"], + "flag_colors": ["red", "white", "blue"], + "region": "Caribbean" + }, + { + "name": "Syria", + "code": "SY", + "languages": ["arabic"], + "flag_colors": ["red", "white", "black", "green"], + "region": "Middle East" + }, + { + "name": "Eswatini", + "code": "SZ", + "languages": ["swazi", "english"], + "flag_colors": ["blue", "red", "yellow", "black", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Turks and Caicos Islands", + "code": "TC", + "languages": ["english"], + "flag_colors": ["blue", "red", "white", "yellow", "green"], + "region": "Caribbean" + }, + { + "name": "Chad", + "code": "TD", + "languages": ["french", "arabic"], + "flag_colors": ["blue", "yellow", "red"], + "region": "Sub-Saharan Africa" + }, + { + "name": "French Southern Territories", + "code": "TF", + "languages": ["french"], + "flag_colors": ["blue", "white", "red"], + "region": "Oceania" + }, + { + "name": "Togo", + "code": "TG", + "languages": ["french"], + "flag_colors": ["green", "yellow", "red", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Thailand", + "code": "TH", + "languages": ["thai"], + "flag_colors": ["red", "white", "blue"], + "region": "Southeast Asia" + }, + { + "name": "Tajikistan", + "code": "TJ", + "languages": ["tajik"], + "flag_colors": ["red", "white", "green", "yellow"], + "region": "Central Asia" + }, + { + "name": "Tokelau", + "code": "TK", + "languages": ["tokelauan", "english"], + "flag_colors": ["blue", "yellow", "white"], + "region": "Oceania" + }, + { + "name": "Timor-Leste", + "code": "TL", + "languages": ["tetum", "portuguese"], + "flag_colors": ["red", "yellow", "black", "white"], + "region": "Southeast Asia" + }, + { + "name": "Turkmenistan", + "code": "TM", + "languages": ["turkmen"], + "flag_colors": ["green", "red", "white"], + "region": "Central Asia" + }, + { + "name": "Tunisia", + "code": "TN", + "languages": ["arabic"], + "flag_colors": ["red", "white"], + "region": "North Africa" + }, + { + "name": "Tonga", + "code": "TO", + "languages": ["tongan", "english"], + "flag_colors": ["red", "white"], + "region": "Oceania" + }, + { + "name": "Turkey", + "code": "TR", + "languages": ["turkish"], + "flag_colors": ["red", "white"], + "region": "Middle East" + }, + { + "name": "Trinidad and Tobago", + "code": "TT", + "languages": ["english"], + "flag_colors": ["red", "black", "white"], + "region": "Caribbean" + }, + { + "name": "Tuvalu", + "code": "TV", + "languages": ["tuvaluan", "english"], + "flag_colors": ["blue", "yellow", "red", "white"], + "region": "Oceania" + }, + { + "name": "Taiwan", + "code": "TW", + "languages": ["mandarin"], + "flag_colors": ["red", "blue", "white"], + "region": "East Asia" + }, + { + "name": "Tanzania", + "code": "TZ", + "languages": ["swahili", "english"], + "flag_colors": ["green", "yellow", "black", "blue"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Ukraine", + "code": "UA", + "languages": ["ukrainian"], + "flag_colors": ["blue", "yellow"], + "region": "Eastern Europe" + }, + { + "name": "Uganda", + "code": "UG", + "languages": ["english", "swahili"], + "flag_colors": ["black", "yellow", "red", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "United States Minor Outlying Islands", + "code": "UM", + "languages": ["english"], + "flag_colors": ["red", "white", "blue"], + "region": "Oceania" + }, + { + "name": "United States", + "code": "US", + "languages": ["english"], + "flag_colors": ["red", "white", "blue"], + "region": "North America" + }, + { + "name": "Uruguay", + "code": "UY", + "languages": ["spanish"], + "flag_colors": ["white", "blue", "yellow"], + "region": "South America" + }, + { + "name": "Uzbekistan", + "code": "UZ", + "languages": ["uzbek"], + "flag_colors": ["blue", "white", "green", "red"], + "region": "Central Asia" + }, + { + "name": "Vatican City", + "code": "VA", + "languages": ["italian", "latin"], + "flag_colors": ["yellow", "white"], + "region": "Southern Europe" + }, + { + "name": "Saint Vincent and the Grenadines", + "code": "VC", + "languages": ["english"], + "flag_colors": ["blue", "yellow", "green", "white"], + "region": "Caribbean" + }, + { + "name": "Venezuela", + "code": "VE", + "languages": ["spanish"], + "flag_colors": ["yellow", "blue", "red", "white"], + "region": "South America" + }, + { + "name": "British Virgin Islands", + "code": "VG", + "languages": ["english"], + "flag_colors": ["blue", "white", "red"], + "region": "Caribbean" + }, + { + "name": "U.S. Virgin Islands", + "code": "VI", + "languages": ["english"], + "flag_colors": ["red", "white", "blue", "yellow"], + "region": "Caribbean" + }, + { + "name": "Vietnam", + "code": "VN", + "languages": ["vietnamese"], + "flag_colors": ["red", "yellow"], + "region": "Southeast Asia" + }, + { + "name": "Vanuatu", + "code": "VU", + "languages": ["bislama", "english", "french"], + "flag_colors": ["red", "green", "black", "yellow"], + "region": "Oceania" + }, + { + "name": "Wallis and Futuna", + "code": "WF", + "languages": ["french", "wallisian", "futunan"], + "flag_colors": ["red", "white", "blue"], + "region": "Oceania" + }, + { + "name": "Samoa", + "code": "WS", + "languages": ["samoan", "english"], + "flag_colors": ["red", "blue", "white"], + "region": "Oceania" + }, + { + "name": "Kosovo", + "code": "XK", + "languages": ["albanian", "serbian"], + "flag_colors": ["blue", "yellow", "white"], + "region": "Eastern Europe" + }, + { + "name": "Yemen", + "code": "YE", + "languages": ["arabic"], + "flag_colors": ["red", "white", "black"], + "region": "Middle East" + }, + { + "name": "Mayotte", + "code": "YT", + "languages": ["french", "shimaore", "kibushi"], + "flag_colors": ["white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "South Africa", + "code": "ZA", + "languages": ["zulu", "xhosa", "afrikaans", "english"], + "flag_colors": ["red", "blue", "green", "yellow", "black", "white"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Zambia", + "code": "ZM", + "languages": ["english"], + "flag_colors": ["green", "red", "black", "orange"], + "region": "Sub-Saharan Africa" + }, + { + "name": "Zimbabwe", + "code": "ZW", + "languages": ["english", "shona", "ndebele"], + "flag_colors": ["green", "yellow", "red", "black", "white"], + "region": "Sub-Saharan Africa" + } +] diff --git a/apps/web/public/fonts/font-atlas.json b/apps/web/public/fonts/font-atlas.json new file mode 100644 index 00000000..deb0eb91 --- /dev/null +++ b/apps/web/public/fonts/font-atlas.json @@ -0,0 +1,11348 @@ +{ + "fonts": { + "42dot Sans": { + "x": 0, + "y": 0, + "w": 131, + "ch": 0, + "s": ["300", "400", "500", "600", "700", "800"] + }, + "ABeeZee": { "x": 145, "y": 0, "w": 106, "ch": 0, "s": ["400", "400i"] }, + "Abel": { "x": 265, "y": 0, "w": 47, "ch": 0, "s": ["400"] }, + "Abhaya Libre": { + "x": 326, + "y": 0, + "w": 139, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Aboreto": { "x": 479, "y": 0, "w": 124, "ch": 0, "s": ["400"] }, + "Abril Fatface": { "x": 617, "y": 0, "w": 152, "ch": 0, "s": ["400"] }, + "Abyssinica SIL": { "x": 783, "y": 0, "w": 165, "ch": 0, "s": ["400"] }, + "Aclonica": { "x": 962, "y": 0, "w": 122, "ch": 0, "s": ["400"] }, + "Acme": { "x": 1098, "y": 0, "w": 63, "ch": 0, "s": ["400"] }, + "Actor": { "x": 0, "y": 40, "w": 64, "ch": 0, "s": ["400"] }, + "Adamina": { "x": 78, "y": 40, "w": 113, "ch": 0, "s": ["400"] }, + "ADLaM Display": { "x": 205, "y": 40, "w": 183, "ch": 0, "s": ["400"] }, + "Advent Pro": { + "x": 402, + "y": 40, + "w": 107, + "ch": 0, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Afacad": { + "x": 523, + "y": 40, + "w": 77, + "ch": 0, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Afacad Flux": { + "x": 614, + "y": 40, + "w": 118, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Agbalumo": { "x": 746, "y": 40, "w": 119, "ch": 0, "s": ["400"] }, + "Agdasima": { "x": 879, "y": 40, "w": 81, "ch": 0, "s": ["400", "700"] }, + "Agu Display": { "x": 974, "y": 40, "w": 140, "ch": 0, "s": ["400"] }, + "Aguafina Script": { "x": 0, "y": 80, "w": 120, "ch": 0, "s": ["400"] }, + "Akatab": { + "x": 134, + "y": 80, + "w": 81, + "ch": 0, + "s": ["400", "500", "600", "700", "800", "900"] + }, + "Akaya Kanadaka": { "x": 229, "y": 80, "w": 176, "ch": 0, "s": ["400"] }, + "Akaya Telivigala": { "x": 419, "y": 80, "w": 165, "ch": 0, "s": ["400"] }, + "Akronim": { "x": 598, "y": 80, "w": 79, "ch": 0, "s": ["400"] }, + "Akshar": { + "x": 691, + "y": 80, + "w": 71, + "ch": 0, + "s": ["300", "400", "500", "600", "700"] + }, + "Aladin": { "x": 776, "y": 80, "w": 61, "ch": 0, "s": ["400"] }, + "Alan Sans": { + "x": 851, + "y": 80, + "w": 116, + "ch": 0, + "s": ["300", "400", "500", "600", "700", "800", "900"] + }, + "Alata": { "x": 981, "y": 80, "w": 68, "ch": 0, "s": ["400"] }, + "Alatsi": { "x": 1063, "y": 80, "w": 66, "ch": 0, "s": ["400"] }, + "Albert Sans": { + "x": 0, + "y": 120, + "w": 133, + "ch": 0, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Aldrich": { "x": 147, "y": 120, "w": 94, "ch": 0, "s": ["400"] }, + "Alef": { "x": 255, "y": 120, "w": 54, "ch": 0, "s": ["400", "700"] }, + "Alegreya": { + "x": 323, + "y": 120, + "w": 92, + "ch": 0, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Alegreya Sans": { + "x": 429, + "y": 120, + "w": 137, + "ch": 0, + "s": [ + "100", + "100i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Alegreya Sans SC": { + "x": 580, + "y": 120, + "w": 179, + "ch": 0, + "s": [ + "100", + "100i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Alegreya SC": { + "x": 773, + "y": 120, + "w": 137, + "ch": 0, + "s": [ + "400", + "400i", + "500", + "500i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Aleo": { + "x": 924, + "y": 120, + "w": 59, + "ch": 0, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Alex Brush": { "x": 997, "y": 120, "w": 117, "ch": 0, "s": ["400"] }, + "Alexandria": { + "x": 0, + "y": 160, + "w": 136, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Alfa Slab One": { "x": 150, "y": 160, "w": 180, "ch": 0, "s": ["400"] }, + "Alice": { "x": 344, "y": 160, "w": 60, "ch": 0, "s": ["400"] }, + "Alike": { "x": 418, "y": 160, "w": 62, "ch": 0, "s": ["400"] }, + "Alike Angular": { "x": 494, "y": 160, "w": 155, "ch": 0, "s": ["400"] }, + "Alkalami": { "x": 663, "y": 160, "w": 109, "ch": 0, "s": ["400"] }, + "Alkatra": { + "x": 786, + "y": 160, + "w": 84, + "ch": 0, + "s": ["400", "500", "600", "700"] + }, + "Allan": { "x": 884, "y": 160, "w": 48, "ch": 0, "s": ["400", "700"] }, + "Allerta": { "x": 946, "y": 160, "w": 93, "ch": 0, "s": ["400"] }, + "Allerta Stencil": { "x": 0, "y": 200, "w": 182, "ch": 0, "s": ["400"] }, + "Allison": { "x": 196, "y": 200, "w": 56, "ch": 0, "s": ["400"] }, + "Allura": { "x": 266, "y": 200, "w": 69, "ch": 0, "s": ["400"] }, + "Almarai": { + "x": 349, + "y": 200, + "w": 88, + "ch": 0, + "s": ["300", "400", "700", "800"] + }, + "Almendra": { + "x": 451, + "y": 200, + "w": 107, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "Almendra Display": { "x": 572, "y": 200, "w": 192, "ch": 0, "s": ["400"] }, + "Almendra SC": { "x": 778, "y": 200, "w": 142, "ch": 0, "s": ["400"] }, + "Alumni Sans": { + "x": 934, + "y": 200, + "w": 96, + "ch": 0, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Alumni Sans Collegiate One": { + "x": 0, + "y": 240, + "w": 201, + "ch": 0, + "s": ["400", "400i"] + }, + "Alumni Sans Inline One": { + "x": 215, + "y": 240, + "w": 180, + "ch": 0, + "s": ["400", "400i"] + }, + "Alumni Sans Pinstripe": { + "x": 409, + "y": 240, + "w": 159, + "ch": 0, + "s": ["400", "400i"] + }, + "Alumni Sans SC": { + "x": 582, + "y": 240, + "w": 125, + "ch": 0, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Amarante": { "x": 721, "y": 240, "w": 108, "ch": 0, "s": ["400"] }, + "Amaranth": { + "x": 843, + "y": 240, + "w": 110, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "Amarna": { + "x": 967, + "y": 240, + "w": 92, + "ch": 0, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Amatic SC": { "x": 1073, "y": 240, "w": 77, "ch": 0, "s": ["400", "700"] }, + "Amethysta": { "x": 0, "y": 280, "w": 133, "ch": 0, "s": ["400"] }, + "Amiko": { + "x": 147, + "y": 280, + "w": 84, + "ch": 0, + "s": ["400", "600", "700"] + }, + "Amiri": { + "x": 245, + "y": 280, + "w": 63, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "Amiri Quran": { "x": 322, "y": 280, "w": 131, "ch": 0, "s": ["400"] }, + "Amita": { "x": 467, "y": 280, "w": 73, "ch": 0, "s": ["400", "700"] }, + "Anaheim": { + "x": 554, + "y": 280, + "w": 93, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Ancizar Sans": { + "x": 661, + "y": 280, + "w": 126, + "ch": 0, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Ancizar Serif": { + "x": 801, + "y": 280, + "w": 136, + "ch": 0, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Andada Pro": { + "x": 951, + "y": 280, + "w": 137, + "ch": 0, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Andika": { + "x": 1102, + "y": 280, + "w": 85, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "Anek Bangla": { + "x": 0, + "y": 320, + "w": 131, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Anek Devanagari": { + "x": 145, + "y": 320, + "w": 176, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Anek Gujarati": { + "x": 335, + "y": 320, + "w": 143, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Anek Gurmukhi": { + "x": 492, + "y": 320, + "w": 164, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Anek Kannada": { + "x": 670, + "y": 320, + "w": 152, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Anek Latin": { + "x": 836, + "y": 320, + "w": 115, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Anek Malayalam": { + "x": 965, + "y": 320, + "w": 171, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Anek Odia": { + "x": 0, + "y": 360, + "w": 108, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Anek Tamil": { + "x": 122, + "y": 360, + "w": 119, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Anek Telugu": { + "x": 255, + "y": 360, + "w": 131, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Angkor": { "x": 400, "y": 360, "w": 111, "ch": 0, "s": ["400"] }, + "Annapurna SIL": { + "x": 525, + "y": 360, + "w": 151, + "ch": 0, + "s": ["400", "700"] + }, + "Annie Use Your Telescope": { + "x": 690, + "y": 360, + "w": 215, + "ch": 0, + "s": ["400"] + }, + "Anonymous Pro": { + "x": 919, + "y": 360, + "w": 179, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "Anta": { "x": 1112, "y": 360, "w": 62, "ch": 0, "s": ["400"] }, + "Antic": { "x": 0, "y": 400, "w": 60, "ch": 0, "s": ["400"] }, + "Antic Didone": { "x": 74, "y": 400, "w": 145, "ch": 0, "s": ["400"] }, + "Antic Slab": { "x": 233, "y": 400, "w": 114, "ch": 0, "s": ["400"] }, + "Anton": { "x": 361, "y": 400, "w": 63, "ch": 0, "s": ["400"] }, + "Anton SC": { "x": 438, "y": 400, "w": 93, "ch": 0, "s": ["400"] }, + "Antonio": { + "x": 545, + "y": 400, + "w": 76, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "Anuphan": { + "x": 635, + "y": 400, + "w": 105, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "Anybody": { + "x": 754, + "y": 400, + "w": 107, + "ch": 0, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Aoboshi One": { "x": 875, "y": 400, "w": 158, "ch": 0, "s": ["400"] }, + "AR One Sans": { + "x": 1047, + "y": 400, + "w": 152, + "ch": 0, + "s": ["400", "500", "600", "700"] + }, + "Arapey": { "x": 0, "y": 440, "w": 75, "ch": 0, "s": ["400", "400i"] }, + "Arbutus": { "x": 89, "y": 440, "w": 128, "ch": 0, "s": ["400"] }, + "Arbutus Slab": { "x": 231, "y": 440, "w": 159, "ch": 0, "s": ["400"] }, + "Architects Daughter": { + "x": 404, + "y": 440, + "w": 237, + "ch": 0, + "s": ["400"] + }, + "Archivo": { + "x": 655, + "y": 440, + "w": 90, + "ch": 0, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Archivo Black": { "x": 759, "y": 440, "w": 190, "ch": 0, "s": ["400"] }, + "Archivo Narrow": { + "x": 963, + "y": 440, + "w": 144, + "ch": 0, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Are You Serious": { "x": 0, "y": 480, "w": 142, "ch": 0, "s": ["400"] }, + "Aref Ruqaa": { + "x": 156, + "y": 480, + "w": 130, + "ch": 0, + "s": ["400", "700"] + }, + "Aref Ruqaa Ink": { + "x": 300, + "y": 480, + "w": 172, + "ch": 0, + "s": ["400", "700"] + }, + "Arima": { + "x": 486, + "y": 480, + "w": 73, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "Arima Madurai": { + "x": 573, + "y": 480, + "w": 170, + "ch": 0, + "s": ["100", "200", "300", "400", "500", "700", "800", "900"] + }, + "Arimo": { + "x": 757, + "y": 480, + "w": 71, + "ch": 0, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Arizonia": { "x": 842, "y": 480, "w": 95, "ch": 0, "s": ["400"] }, + "Armata": { "x": 951, "y": 480, "w": 100, "ch": 0, "s": ["400"] }, + "Arsenal": { + "x": 1065, + "y": 480, + "w": 80, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "Arsenal SC": { + "x": 0, + "y": 520, + "w": 118, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "Artifika": { "x": 132, "y": 520, "w": 105, "ch": 0, "s": ["400"] }, + "Arvo": { + "x": 251, + "y": 520, + "w": 65, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "Arya": { "x": 330, "y": 520, "w": 53, "ch": 0, "s": ["400", "700"] }, + "Asap": { + "x": 397, + "y": 520, + "w": 60, + "ch": 0, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Asap Condensed": { + "x": 471, + "y": 520, + "w": 153, + "ch": 0, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Asar": { "x": 638, "y": 520, "w": 53, "ch": 0, "s": ["400"] }, + "Asimovian": { "x": 705, "y": 520, "w": 117, "ch": 0, "s": ["400"] }, + "Asset": { "x": 836, "y": 520, "w": 154, "ch": 0, "s": ["400"] }, + "Assistant": { + "x": 1004, + "y": 520, + "w": 98, + "ch": 0, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Asta Sans": { + "x": 0, + "y": 560, + "w": 115, + "ch": 0, + "s": ["300", "400", "500", "600", "700", "800"] + }, + "Astloch": { "x": 129, "y": 560, "w": 72, "ch": 0, "s": ["400", "700"] }, + "Asul": { "x": 215, "y": 560, "w": 54, "ch": 0, "s": ["400", "700"] }, + "Athiti": { + "x": 283, + "y": 560, + "w": 62, + "ch": 0, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Atkinson Hyperlegible": { + "x": 359, + "y": 560, + "w": 239, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "Atkinson Hyperlegible Mono": { + "x": 612, + "y": 560, + "w": 403, + "ch": 0, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Atkinson Hyperlegible Next": { + "x": 0, + "y": 600, + "w": 296, + "ch": 0, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Atma": { + "x": 310, + "y": 600, + "w": 59, + "ch": 0, + "s": ["300", "400", "500", "600", "700"] + }, + "Atomic Age": { "x": 383, "y": 600, "w": 144, "ch": 0, "s": ["400"] }, + "Aubrey": { "x": 541, "y": 600, "w": 67, "ch": 0, "s": ["400"] }, + "Audiowide": { "x": 622, "y": 600, "w": 145, "ch": 0, "s": ["400"] }, + "Autour One": { "x": 781, "y": 600, "w": 168, "ch": 0, "s": ["400"] }, + "Average": { "x": 963, "y": 600, "w": 88, "ch": 0, "s": ["400"] }, + "Average Sans": { "x": 0, "y": 640, "w": 138, "ch": 0, "s": ["400"] }, + "Averia Gruesa Libre": { + "x": 152, + "y": 640, + "w": 217, + "ch": 0, + "s": ["400"] + }, + "Averia Libre": { + "x": 383, + "y": 640, + "w": 138, + "ch": 0, + "s": ["300", "300i", "400", "400i", "700", "700i"] + }, + "Averia Sans Libre": { + "x": 535, + "y": 640, + "w": 190, + "ch": 0, + "s": ["300", "300i", "400", "400i", "700", "700i"] + }, + "Averia Serif Libre": { + "x": 739, + "y": 640, + "w": 202, + "ch": 0, + "s": ["300", "300i", "400", "400i", "700", "700i"] + }, + "Azeret Mono": { + "x": 955, + "y": 640, + "w": 180, + "ch": 0, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "B612": { + "x": 0, + "y": 680, + "w": 71, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "B612 Mono": { + "x": 85, + "y": 680, + "w": 149, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "Babylonica": { "x": 248, "y": 680, "w": 90, "ch": 0, "s": ["400"] }, + "Bacasime Antique": { "x": 352, "y": 680, "w": 182, "ch": 0, "s": ["400"] }, + "Bad Script": { "x": 548, "y": 680, "w": 103, "ch": 0, "s": ["400"] }, + "Badeen Display": { "x": 665, "y": 680, "w": 194, "ch": 0, "s": ["400"] }, + "Bagel Fat One": { "x": 873, "y": 680, "w": 158, "ch": 0, "s": ["400"] }, + "Bahiana": { "x": 1045, "y": 680, "w": 58, "ch": 0, "s": ["400"] }, + "Bahianita": { "x": 1117, "y": 680, "w": 63, "ch": 0, "s": ["400"] }, + "Bai Jamjuree": { + "x": 0, + "y": 720, + "w": 152, + "ch": 0, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Bakbak One": { "x": 166, "y": 720, "w": 141, "ch": 0, "s": ["400"] }, + "Ballet": { "x": 321, "y": 720, "w": 65, "ch": 0, "s": ["400"] }, + "Baloo 2": { + "x": 400, + "y": 720, + "w": 84, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Baloo Bhai 2": { + "x": 498, + "y": 720, + "w": 134, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Baloo Bhaijaan 2": { + "x": 646, + "y": 720, + "w": 177, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Baloo Bhaina 2": { + "x": 837, + "y": 720, + "w": 160, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Baloo Chettan 2": { + "x": 1011, + "y": 720, + "w": 172, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Baloo Da 2": { + "x": 0, + "y": 760, + "w": 117, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Baloo Paaji 2": { + "x": 131, + "y": 760, + "w": 138, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Baloo Tamma 2": { + "x": 283, + "y": 760, + "w": 169, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Baloo Tammudu 2": { + "x": 466, + "y": 760, + "w": 191, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Baloo Thambi 2": { + "x": 671, + "y": 760, + "w": 165, + "ch": 0, + "s": ["400", "500", "600", "700", "800"] + }, + "Balsamiq Sans": { + "x": 850, + "y": 760, + "w": 168, + "ch": 0, + "s": ["400", "400i", "700", "700i"] + }, + "Balthazar": { "x": 1032, "y": 760, "w": 96, "ch": 0, "s": ["400"] }, + "Bangers": { "x": 0, "y": 0, "w": 81, "ch": 1, "s": ["400"] }, + "Barlow": { + "x": 95, + "y": 0, + "w": 80, + "ch": 1, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Barlow Condensed": { + "x": 189, + "y": 0, + "w": 158, + "ch": 1, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Barlow Semi Condensed": { + "x": 361, + "y": 0, + "w": 231, + "ch": 1, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Barriecito": { "x": 606, "y": 0, "w": 106, "ch": 1, "s": ["400"] }, + "Barrio": { "x": 726, "y": 0, "w": 80, "ch": 1, "s": ["400"] }, + "Basic": { "x": 820, "y": 0, "w": 61, "ch": 1, "s": ["400"] }, + "Baskervville": { + "x": 895, + "y": 0, + "w": 136, + "ch": 1, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Baskervville SC": { + "x": 0, + "y": 40, + "w": 187, + "ch": 1, + "s": ["400", "500", "600", "700"] + }, + "Battambang": { + "x": 201, + "y": 40, + "w": 147, + "ch": 1, + "s": ["100", "300", "400", "700", "900"] + }, + "Baumans": { "x": 362, "y": 40, "w": 101, "ch": 1, "s": ["400"] }, + "Bayon": { "x": 477, "y": 40, "w": 61, "ch": 1, "s": ["400"] }, + "BBH Bartle": { "x": 552, "y": 40, "w": 298, "ch": 1, "s": ["400"] }, + "BBH Bogle": { "x": 864, "y": 40, "w": 98, "ch": 1, "s": ["400"] }, + "BBH Hegarty": { "x": 976, "y": 40, "w": 182, "ch": 1, "s": ["400"] }, + "BBH Sans Bartle": { "x": 0, "y": 80, "w": 427, "ch": 1, "s": ["400"] }, + "BBH Sans Bogle": { "x": 441, "y": 80, "w": 142, "ch": 1, "s": ["400"] }, + "BBH Sans Hegarty": { "x": 597, "y": 80, "w": 254, "ch": 1, "s": ["400"] }, + "Be Vietnam Pro": { + "x": 865, + "y": 80, + "w": 188, + "ch": 1, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Beau Rivage": { "x": 1067, "y": 80, "w": 108, "ch": 1, "s": ["400"] }, + "Bebas Neue": { "x": 0, "y": 120, "w": 96, "ch": 1, "s": ["400"] }, + "Beiruti": { + "x": 110, + "y": 120, + "w": 66, + "ch": 1, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Belanosima": { + "x": 190, + "y": 120, + "w": 124, + "ch": 1, + "s": ["400", "600", "700"] + }, + "Belgrano": { "x": 328, "y": 120, "w": 115, "ch": 1, "s": ["400"] }, + "Bellefair": { "x": 457, "y": 120, "w": 81, "ch": 1, "s": ["400"] }, + "Belleza": { "x": 552, "y": 120, "w": 79, "ch": 1, "s": ["400"] }, + "Bellota": { + "x": 645, + "y": 120, + "w": 84, + "ch": 1, + "s": ["300", "300i", "400", "400i", "700", "700i"] + }, + "Bellota Text": { + "x": 743, + "y": 120, + "w": 129, + "ch": 1, + "s": ["300", "300i", "400", "400i", "700", "700i"] + }, + "BenchNine": { + "x": 886, + "y": 120, + "w": 82, + "ch": 1, + "s": ["300", "400", "700"] + }, + "Benne": { "x": 982, "y": 120, "w": 66, "ch": 1, "s": ["400"] }, + "Bentham": { "x": 1062, "y": 120, "w": 98, "ch": 1, "s": ["400"] }, + "Berkshire Swash": { "x": 0, "y": 160, "w": 178, "ch": 1, "s": ["400"] }, + "Besley": { + "x": 192, + "y": 160, + "w": 86, + "ch": 1, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Beth Ellen": { "x": 292, "y": 160, "w": 138, "ch": 1, "s": ["400"] }, + "Bevan": { "x": 444, "y": 160, "w": 94, "ch": 1, "s": ["400", "400i"] }, + "BhuTuka Expanded One": { + "x": 552, + "y": 160, + "w": 412, + "ch": 1, + "s": ["400"] + }, + "Big Shoulders": { + "x": 978, + "y": 160, + "w": 117, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Big Shoulders Display": { + "x": 0, + "y": 200, + "w": 158, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Big Shoulders Inline": { + "x": 172, + "y": 200, + "w": 167, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Big Shoulders Inline Display": { + "x": 353, + "y": 200, + "w": 203, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Big Shoulders Inline Text": { + "x": 570, + "y": 200, + "w": 201, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Big Shoulders Stencil": { + "x": 785, + "y": 200, + "w": 177, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Big Shoulders Stencil Display": { + "x": 976, + "y": 200, + "w": 211, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Big Shoulders Stencil Text": { + "x": 0, + "y": 240, + "w": 210, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Big Shoulders Text": { + "x": 224, + "y": 240, + "w": 150, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bigelow Rules": { "x": 388, "y": 240, "w": 86, "ch": 1, "s": ["400"] }, + "Bigshot One": { "x": 488, "y": 240, "w": 138, "ch": 1, "s": ["400"] }, + "Bilbo": { "x": 640, "y": 240, "w": 45, "ch": 1, "s": ["400"] }, + "Bilbo Swash Caps": { "x": 699, "y": 240, "w": 144, "ch": 1, "s": ["400"] }, + "BioRhyme": { + "x": 857, + "y": 240, + "w": 139, + "ch": 1, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "BioRhyme Expanded": { + "x": 0, + "y": 280, + "w": 455, + "ch": 1, + "s": ["200", "300", "400", "700", "800"] + }, + "Birthstone": { "x": 469, "y": 280, "w": 79, "ch": 1, "s": ["400"] }, + "Birthstone Bounce": { + "x": 562, + "y": 280, + "w": 167, + "ch": 1, + "s": ["400", "500"] + }, + "Biryani": { + "x": 743, + "y": 280, + "w": 90, + "ch": 1, + "s": ["200", "300", "400", "600", "700", "800", "900"] + }, + "Bitcount": { + "x": 847, + "y": 280, + "w": 124, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitcount Grid Double": { + "x": 0, + "y": 320, + "w": 296, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitcount Grid Double Ink": { + "x": 310, + "y": 320, + "w": 354, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitcount Grid Single": { + "x": 678, + "y": 320, + "w": 296, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitcount Grid Single Ink": { + "x": 0, + "y": 360, + "w": 354, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitcount Ink": { + "x": 368, + "y": 360, + "w": 181, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitcount Prop Double": { + "x": 563, + "y": 360, + "w": 265, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitcount Prop Double Ink": { + "x": 842, + "y": 360, + "w": 308, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitcount Prop Single": { + "x": 0, + "y": 400, + "w": 244, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitcount Prop Single Ink": { + "x": 258, + "y": 400, + "w": 282, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitcount Single": { + "x": 554, + "y": 400, + "w": 224, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitcount Single Ink": { + "x": 792, + "y": 400, + "w": 282, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bitter": { + "x": 1088, + "y": 400, + "w": 71, + "ch": 1, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "BIZ UDGothic": { + "x": 0, + "y": 440, + "w": 152, + "ch": 1, + "s": ["400", "700"] + }, + "BIZ UDMincho": { + "x": 166, + "y": 440, + "w": 152, + "ch": 1, + "s": ["400", "700"] + }, + "BIZ UDPGothic": { + "x": 332, + "y": 440, + "w": 209, + "ch": 1, + "s": ["400", "700"] + }, + "BIZ UDPMincho": { + "x": 555, + "y": 440, + "w": 208, + "ch": 1, + "s": ["400", "700"] + }, + "Black And White Picture": { + "x": 777, + "y": 440, + "w": 218, + "ch": 1, + "s": ["400"] + }, + "Black Han Sans": { "x": 0, "y": 480, "w": 212, "ch": 1, "s": ["400"] }, + "Black Ops One": { "x": 226, "y": 480, "w": 191, "ch": 1, "s": ["400"] }, + "Blaka": { "x": 431, "y": 480, "w": 57, "ch": 1, "s": ["400"] }, + "Blaka Hollow": { "x": 502, "y": 480, "w": 119, "ch": 1, "s": ["400"] }, + "Blaka Ink": { "x": 635, "y": 480, "w": 87, "ch": 1, "s": ["400"] }, + "Blinker": { + "x": 736, + "y": 480, + "w": 77, + "ch": 1, + "s": ["100", "200", "300", "400", "600", "700", "800", "900"] + }, + "Bodoni Moda": { + "x": 827, + "y": 480, + "w": 155, + "ch": 1, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Bodoni Moda SC": { + "x": 996, + "y": 480, + "w": 200, + "ch": 1, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Bokor": { "x": 0, "y": 520, "w": 58, "ch": 1, "s": ["400"] }, + "Boldonse": { "x": 72, "y": 520, "w": 147, "ch": 1, "s": ["400"] }, + "Bona Nova": { + "x": 233, + "y": 520, + "w": 124, + "ch": 1, + "s": ["400", "400i", "700", "700i"] + }, + "Bona Nova SC": { + "x": 371, + "y": 520, + "w": 170, + "ch": 1, + "s": ["400", "400i", "700", "700i"] + }, + "Bonbon": { "x": 555, "y": 520, "w": 104, "ch": 1, "s": ["400"] }, + "Bonheur Royale": { "x": 673, "y": 520, "w": 124, "ch": 1, "s": ["400"] }, + "Boogaloo": { "x": 811, "y": 520, "w": 81, "ch": 1, "s": ["400"] }, + "Borel": { "x": 906, "y": 520, "w": 69, "ch": 1, "s": ["400"] }, + "Bowlby One": { "x": 989, "y": 520, "w": 174, "ch": 1, "s": ["400"] }, + "Bowlby One SC": { "x": 0, "y": 560, "w": 213, "ch": 1, "s": ["400"] }, + "Braah One": { "x": 227, "y": 560, "w": 124, "ch": 1, "s": ["400"] }, + "Brawler": { "x": 365, "y": 560, "w": 95, "ch": 1, "s": ["400", "700"] }, + "Bree Serif": { "x": 474, "y": 560, "w": 113, "ch": 1, "s": ["400"] }, + "Bricolage Grotesque": { + "x": 601, + "y": 560, + "w": 238, + "ch": 1, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Briem Hand": { + "x": 853, + "y": 560, + "w": 146, + "ch": 1, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Bruno Ace": { "x": 1013, "y": 560, "w": 168, "ch": 1, "s": ["400"] }, + "Bruno Ace SC": { "x": 0, "y": 600, "w": 219, "ch": 1, "s": ["400"] }, + "Brygada 1918": { + "x": 233, + "y": 600, + "w": 157, + "ch": 1, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Bubblegum Sans": { "x": 404, "y": 600, "w": 155, "ch": 1, "s": ["400"] }, + "Bubbler One": { "x": 573, "y": 600, "w": 117, "ch": 1, "s": ["400"] }, + "Buenard": { + "x": 704, + "y": 600, + "w": 93, + "ch": 1, + "s": ["400", "500", "600", "700"] + }, + "Bungee": { "x": 811, "y": 600, "w": 110, "ch": 1, "s": ["400"] }, + "Bungee Hairline": { "x": 935, "y": 600, "w": 249, "ch": 1, "s": ["400"] }, + "Bungee Inline": { "x": 0, "y": 640, "w": 213, "ch": 1, "s": ["400"] }, + "Bungee Outline": { "x": 227, "y": 640, "w": 232, "ch": 1, "s": ["400"] }, + "Bungee Shade": { "x": 473, "y": 640, "w": 229, "ch": 1, "s": ["400"] }, + "Bungee Spice": { "x": 716, "y": 640, "w": 193, "ch": 1, "s": ["400"] }, + "Bungee Tint": { "x": 923, "y": 640, "w": 179, "ch": 1, "s": ["400"] }, + "Butcherman": { "x": 0, "y": 680, "w": 161, "ch": 1, "s": ["400"] }, + "Butterfly Kids": { "x": 175, "y": 680, "w": 116, "ch": 1, "s": ["400"] }, + "Bytesized": { "x": 305, "y": 680, "w": 116, "ch": 1, "s": ["400"] }, + "Cabin": { + "x": 435, + "y": 680, + "w": 67, + "ch": 1, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Cabin Condensed": { + "x": 516, + "y": 680, + "w": 163, + "ch": 1, + "s": ["400", "500", "600", "700"] + }, + "Cabin Sketch": { + "x": 693, + "y": 680, + "w": 141, + "ch": 1, + "s": ["400", "700"] + }, + "Cactus Classical Serif": { + "x": 848, + "y": 680, + "w": 235, + "ch": 1, + "s": ["400"] + }, + "Caesar Dressing": { "x": 0, "y": 720, "w": 164, "ch": 1, "s": ["400"] }, + "Cagliostro": { "x": 178, "y": 720, "w": 115, "ch": 1, "s": ["400"] }, + "Cairo": { + "x": 307, + "y": 720, + "w": 60, + "ch": 1, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Cairo Play": { + "x": 381, + "y": 720, + "w": 108, + "ch": 1, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Cal Sans": { "x": 503, "y": 720, "w": 99, "ch": 1, "s": ["400"] }, + "Caladea": { + "x": 616, + "y": 720, + "w": 84, + "ch": 1, + "s": ["400", "400i", "700", "700i"] + }, + "Calistoga": { "x": 714, "y": 720, "w": 109, "ch": 1, "s": ["400"] }, + "Calligraffitti": { "x": 837, "y": 720, "w": 124, "ch": 1, "s": ["400"] }, + "Cambay": { + "x": 975, + "y": 720, + "w": 92, + "ch": 1, + "s": ["400", "400i", "700", "700i"] + }, + "Cambo": { "x": 1081, "y": 720, "w": 82, "ch": 1, "s": ["400"] }, + "Candal": { "x": 0, "y": 760, "w": 102, "ch": 1, "s": ["400"] }, + "Cantarell": { + "x": 116, + "y": 760, + "w": 109, + "ch": 1, + "s": ["400", "400i", "700", "700i"] + }, + "Cantata One": { "x": 239, "y": 760, "w": 163, "ch": 1, "s": ["400"] }, + "Cantora One": { "x": 416, "y": 760, "w": 134, "ch": 1, "s": ["400"] }, + "Caprasimo": { "x": 564, "y": 760, "w": 139, "ch": 1, "s": ["400"] }, + "Capriola": { "x": 717, "y": 760, "w": 110, "ch": 1, "s": ["400"] }, + "Caramel": { "x": 841, "y": 760, "w": 66, "ch": 1, "s": ["400"] }, + "Carattere": { "x": 921, "y": 760, "w": 79, "ch": 1, "s": ["400"] }, + "Cardo": { + "x": 1014, + "y": 760, + "w": 72, + "ch": 1, + "s": ["400", "400i", "700", "700i"] + }, + "Carlito": { + "x": 1100, + "y": 760, + "w": 73, + "ch": 1, + "s": ["400", "400i", "700", "700i"] + }, + "Carme": { "x": 0, "y": 0, "w": 79, "ch": 2, "s": ["400"] }, + "Carrois Gothic": { "x": 93, "y": 0, "w": 155, "ch": 2, "s": ["400"] }, + "Carrois Gothic SC": { "x": 262, "y": 0, "w": 211, "ch": 2, "s": ["400"] }, + "Carter One": { "x": 487, "y": 0, "w": 136, "ch": 2, "s": ["400"] }, + "Cascadia Code": { + "x": 637, + "y": 0, + "w": 191, + "ch": 2, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Cascadia Mono": { + "x": 842, + "y": 0, + "w": 191, + "ch": 2, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Castoro": { "x": 1047, "y": 0, "w": 93, "ch": 2, "s": ["400", "400i"] }, + "Castoro Titling": { "x": 0, "y": 40, "w": 236, "ch": 2, "s": ["400"] }, + "Catamaran": { + "x": 250, + "y": 40, + "w": 116, + "ch": 2, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Caudex": { + "x": 380, + "y": 40, + "w": 92, + "ch": 2, + "s": ["400", "400i", "700", "700i"] + }, + "Cause": { + "x": 486, + "y": 40, + "w": 72, + "ch": 2, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Caveat": { + "x": 572, + "y": 40, + "w": 63, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Caveat Brush": { "x": 649, "y": 40, "w": 120, "ch": 2, "s": ["400"] }, + "Cedarville Cursive": { + "x": 783, + "y": 40, + "w": 203, + "ch": 2, + "s": ["400"] + }, + "Ceviche One": { "x": 1000, "y": 40, "w": 111, "ch": 2, "s": ["400"] }, + "Chakra Petch": { + "x": 0, + "y": 80, + "w": 154, + "ch": 2, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Changa": { + "x": 168, + "y": 80, + "w": 86, + "ch": 2, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Changa One": { + "x": 268, + "y": 80, + "w": 136, + "ch": 2, + "s": ["400", "400i"] + }, + "Chango": { "x": 418, "y": 80, "w": 128, "ch": 2, "s": ["400"] }, + "Charis SIL": { + "x": 560, + "y": 80, + "w": 116, + "ch": 2, + "s": ["400", "400i", "700", "700i"] + }, + "Charm": { "x": 690, "y": 80, "w": 69, "ch": 2, "s": ["400", "700"] }, + "Charmonman": { "x": 773, "y": 80, "w": 136, "ch": 2, "s": ["400", "700"] }, + "Chathura": { + "x": 923, + "y": 80, + "w": 55, + "ch": 2, + "s": ["100", "300", "400", "700", "800"] + }, + "Chau Philomene One": { + "x": 992, + "y": 80, + "w": 200, + "ch": 2, + "s": ["400", "400i"] + }, + "Chela One": { "x": 0, "y": 120, "w": 97, "ch": 2, "s": ["400"] }, + "Chelsea Market": { "x": 111, "y": 120, "w": 200, "ch": 2, "s": ["400"] }, + "Cherish": { "x": 325, "y": 120, "w": 60, "ch": 2, "s": ["400"] }, + "Cherry Bomb One": { "x": 399, "y": 120, "w": 200, "ch": 2, "s": ["400"] }, + "Cherry Cream Soda": { + "x": 613, + "y": 120, + "w": 274, + "ch": 2, + "s": ["400"] + }, + "Cherry Swash": { + "x": 901, + "y": 120, + "w": 159, + "ch": 2, + "s": ["400", "700"] + }, + "Chewy": { "x": 1074, "y": 120, "w": 73, "ch": 2, "s": ["400"] }, + "Chicle": { "x": 0, "y": 160, "w": 56, "ch": 2, "s": ["400"] }, + "Chilanka": { "x": 70, "y": 160, "w": 99, "ch": 2, "s": ["400"] }, + "Chiron GoRound TC": { + "x": 183, + "y": 160, + "w": 225, + "ch": 2, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Chiron Hei HK": { + "x": 422, + "y": 160, + "w": 163, + "ch": 2, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Chivo": { + "x": 599, + "y": 160, + "w": 72, + "ch": 2, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Chivo Mono": { + "x": 685, + "y": 160, + "w": 152, + "ch": 2, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Chocolate Classical Sans": { + "x": 851, + "y": 160, + "w": 283, + "ch": 2, + "s": ["400"] + }, + "Chokokutai": { "x": 0, "y": 200, "w": 148, "ch": 2, "s": ["400"] }, + "Chonburi": { "x": 162, "y": 200, "w": 138, "ch": 2, "s": ["400"] }, + "Cinzel": { + "x": 314, + "y": 200, + "w": 94, + "ch": 2, + "s": ["400", "500", "600", "700", "800", "900"] + }, + "Cinzel Decorative": { + "x": 422, + "y": 200, + "w": 262, + "ch": 2, + "s": ["400", "700", "900"] + }, + "Clicker Script": { "x": 698, "y": 200, "w": 117, "ch": 2, "s": ["400"] }, + "Climate Crisis": { "x": 829, "y": 200, "w": 247, "ch": 2, "s": ["400"] }, + "Coda": { "x": 1090, "y": 200, "w": 65, "ch": 2, "s": ["400", "800"] }, + "Codystar": { "x": 0, "y": 240, "w": 137, "ch": 2, "s": ["300", "400"] }, + "Coiny": { "x": 151, "y": 240, "w": 79, "ch": 2, "s": ["400"] }, + "Combo": { "x": 244, "y": 240, "w": 69, "ch": 2, "s": ["400"] }, + "Comfortaa": { + "x": 327, + "y": 240, + "w": 143, + "ch": 2, + "s": ["300", "400", "500", "600", "700"] + }, + "Comforter": { "x": 484, "y": 240, "w": 99, "ch": 2, "s": ["400"] }, + "Comforter Brush": { "x": 597, "y": 240, "w": 147, "ch": 2, "s": ["400"] }, + "Comic Neue": { + "x": 758, + "y": 240, + "w": 133, + "ch": 2, + "s": ["300", "300i", "400", "400i", "700", "700i"] + }, + "Comic Relief": { + "x": 905, + "y": 240, + "w": 147, + "ch": 2, + "s": ["400", "700"] + }, + "Coming Soon": { "x": 0, "y": 280, "w": 152, "ch": 2, "s": ["400"] }, + "Comme": { + "x": 166, + "y": 280, + "w": 96, + "ch": 2, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Commissioner": { + "x": 276, + "y": 280, + "w": 166, + "ch": 2, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Concert One": { "x": 456, "y": 280, "w": 136, "ch": 2, "s": ["400"] }, + "Condiment": { "x": 606, "y": 280, "w": 96, "ch": 2, "s": ["400"] }, + "Contrail One": { "x": 716, "y": 280, "w": 128, "ch": 2, "s": ["400"] }, + "Convergence": { "x": 858, "y": 280, "w": 155, "ch": 2, "s": ["400"] }, + "Cookie": { "x": 1027, "y": 280, "w": 55, "ch": 2, "s": ["400"] }, + "Copse": { "x": 1096, "y": 280, "w": 73, "ch": 2, "s": ["400"] }, + "Coral Pixels": { "x": 0, "y": 320, "w": 140, "ch": 2, "s": ["400"] }, + "Corben": { "x": 154, "y": 320, "w": 91, "ch": 2, "s": ["400", "700"] }, + "Corinthia": { "x": 259, "y": 320, "w": 69, "ch": 2, "s": ["400", "700"] }, + "Cormorant": { + "x": 342, + "y": 320, + "w": 113, + "ch": 2, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Cormorant Garamond": { + "x": 469, + "y": 320, + "w": 220, + "ch": 2, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Cormorant Infant": { + "x": 703, + "y": 320, + "w": 181, + "ch": 2, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Cormorant SC": { + "x": 898, + "y": 320, + "w": 167, + "ch": 2, + "s": ["300", "400", "500", "600", "700"] + }, + "Cormorant Unicase": { + "x": 0, + "y": 360, + "w": 222, + "ch": 2, + "s": ["300", "400", "500", "600", "700"] + }, + "Cormorant Upright": { + "x": 236, + "y": 360, + "w": 188, + "ch": 2, + "s": ["300", "400", "500", "600", "700"] + }, + "Cossette Texte": { + "x": 438, + "y": 360, + "w": 164, + "ch": 2, + "s": ["400", "700"] + }, + "Cossette Titre": { + "x": 616, + "y": 360, + "w": 116, + "ch": 2, + "s": ["400", "700"] + }, + "Courgette": { "x": 746, "y": 360, "w": 109, "ch": 2, "s": ["400"] }, + "Courier Prime": { + "x": 869, + "y": 360, + "w": 196, + "ch": 2, + "s": ["400", "400i", "700", "700i"] + }, + "Cousine": { + "x": 1079, + "y": 360, + "w": 109, + "ch": 2, + "s": ["400", "400i", "700", "700i"] + }, + "Coustard": { "x": 0, "y": 400, "w": 115, "ch": 2, "s": ["400", "900"] }, + "Covered By Your Grace": { + "x": 129, + "y": 400, + "w": 206, + "ch": 2, + "s": ["400"] + }, + "Crafty Girls": { "x": 349, "y": 400, "w": 153, "ch": 2, "s": ["400"] }, + "Creepster": { "x": 516, "y": 400, "w": 101, "ch": 2, "s": ["400"] }, + "Crete Round": { + "x": 631, + "y": 400, + "w": 142, + "ch": 2, + "s": ["400", "400i"] + }, + "Crimson Pro": { + "x": 787, + "y": 400, + "w": 127, + "ch": 2, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Crimson Text": { + "x": 928, + "y": 400, + "w": 139, + "ch": 2, + "s": ["400", "400i", "600", "600i", "700", "700i"] + }, + "Croissant One": { "x": 0, "y": 440, "w": 186, "ch": 2, "s": ["400"] }, + "Crushed": { "x": 200, "y": 440, "w": 79, "ch": 2, "s": ["400"] }, + "Cuprum": { + "x": 293, + "y": 440, + "w": 77, + "ch": 2, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Cute Font": { "x": 384, "y": 440, "w": 78, "ch": 2, "s": ["400"] }, + "Cutive": { "x": 476, "y": 440, "w": 98, "ch": 2, "s": ["400"] }, + "Cutive Mono": { "x": 588, "y": 440, "w": 168, "ch": 2, "s": ["400"] }, + "Dai Banna SIL": { + "x": 770, + "y": 440, + "w": 137, + "ch": 2, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Damion": { "x": 921, "y": 440, "w": 78, "ch": 2, "s": ["400"] }, + "Dancing Script": { + "x": 1013, + "y": 440, + "w": 137, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Danfo": { "x": 0, "y": 480, "w": 86, "ch": 2, "s": ["400"] }, + "Dangrek": { "x": 100, "y": 480, "w": 91, "ch": 2, "s": ["400"] }, + "Darker Grotesque": { + "x": 205, + "y": 480, + "w": 163, + "ch": 2, + "s": ["300", "400", "500", "600", "700", "800", "900"] + }, + "Darumadrop One": { "x": 382, "y": 480, "w": 184, "ch": 2, "s": ["400"] }, + "David Libre": { + "x": 580, + "y": 480, + "w": 120, + "ch": 2, + "s": ["400", "500", "700"] + }, + "Dawning of a New Day": { + "x": 714, + "y": 480, + "w": 208, + "ch": 2, + "s": ["400"] + }, + "Days One": { "x": 936, "y": 480, "w": 133, "ch": 2, "s": ["400"] }, + "Dekko": { "x": 1083, "y": 480, "w": 62, "ch": 2, "s": ["400"] }, + "Dela Gothic One": { "x": 0, "y": 520, "w": 234, "ch": 2, "s": ["400"] }, + "Delicious Handrawn": { + "x": 248, + "y": 520, + "w": 161, + "ch": 2, + "s": ["400"] + }, + "Delius": { "x": 423, "y": 520, "w": 74, "ch": 2, "s": ["400"] }, + "Delius Swash Caps": { + "x": 511, + "y": 520, + "w": 214, + "ch": 2, + "s": ["400"] + }, + "Delius Unicase": { + "x": 739, + "y": 520, + "w": 229, + "ch": 2, + "s": ["400", "700"] + }, + "Della Respira": { "x": 982, "y": 520, "w": 149, "ch": 2, "s": ["400"] }, + "Denk One": { "x": 0, "y": 560, "w": 105, "ch": 2, "s": ["400"] }, + "Devonshire": { "x": 119, "y": 560, "w": 87, "ch": 2, "s": ["400"] }, + "Dhurjati": { "x": 220, "y": 560, "w": 75, "ch": 2, "s": ["400"] }, + "Didact Gothic": { "x": 309, "y": 560, "w": 145, "ch": 2, "s": ["400"] }, + "Diphylleia": { "x": 468, "y": 560, "w": 112, "ch": 2, "s": ["400"] }, + "Diplomata": { "x": 594, "y": 560, "w": 252, "ch": 2, "s": ["400"] }, + "Diplomata SC": { "x": 860, "y": 560, "w": 334, "ch": 2, "s": ["400"] }, + "DM Mono": { + "x": 0, + "y": 600, + "w": 109, + "ch": 2, + "s": ["300", "300i", "400", "400i", "500", "500i"] + }, + "DM Sans": { + "x": 123, + "y": 600, + "w": 104, + "ch": 2, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "DM Serif Display": { + "x": 241, + "y": 600, + "w": 181, + "ch": 2, + "s": ["400", "400i"] + }, + "DM Serif Text": { + "x": 436, + "y": 600, + "w": 150, + "ch": 2, + "s": ["400", "400i"] + }, + "Do Hyeon": { "x": 600, "y": 600, "w": 101, "ch": 2, "s": ["400"] }, + "Dokdo": { "x": 715, "y": 600, "w": 68, "ch": 2, "s": ["400"] }, + "Domine": { + "x": 797, + "y": 600, + "w": 101, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Donegal One": { "x": 912, "y": 600, "w": 161, "ch": 2, "s": ["400"] }, + "Dongle": { + "x": 1087, + "y": 600, + "w": 56, + "ch": 2, + "s": ["300", "400", "700"] + }, + "Doppio One": { "x": 0, "y": 640, "w": 138, "ch": 2, "s": ["400"] }, + "Dorsa": { "x": 152, "y": 640, "w": 32, "ch": 2, "s": ["400"] }, + "Dosis": { + "x": 198, + "y": 640, + "w": 56, + "ch": 2, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "DotGothic16": { "x": 268, "y": 640, "w": 140, "ch": 2, "s": ["400"] }, + "Doto": { + "x": 422, + "y": 640, + "w": 66, + "ch": 2, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Dr Sugiyama": { "x": 502, "y": 640, "w": 116, "ch": 2, "s": ["400"] }, + "Duru Sans": { "x": 632, "y": 640, "w": 133, "ch": 2, "s": ["400"] }, + "Dynalight": { "x": 779, "y": 640, "w": 84, "ch": 2, "s": ["400"] }, + "DynaPuff": { + "x": 877, + "y": 640, + "w": 121, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Eagle Lake": { "x": 1012, "y": 640, "w": 153, "ch": 2, "s": ["400"] }, + "East Sea Dokdo": { "x": 0, "y": 680, "w": 118, "ch": 2, "s": ["400"] }, + "Eater": { "x": 132, "y": 680, "w": 90, "ch": 2, "s": ["400"] }, + "EB Garamond": { + "x": 236, + "y": 680, + "w": 141, + "ch": 2, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Economica": { + "x": 391, + "y": 680, + "w": 88, + "ch": 2, + "s": ["400", "400i", "700", "700i"] + }, + "Eczar": { + "x": 493, + "y": 680, + "w": 65, + "ch": 2, + "s": ["400", "500", "600", "700", "800"] + }, + "Edu AU VIC WA NT Arrows": { + "x": 572, + "y": 680, + "w": 302, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Edu AU VIC WA NT Dots": { + "x": 888, + "y": 680, + "w": 261, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Edu AU VIC WA NT Guides": { + "x": 0, + "y": 720, + "w": 285, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Edu AU VIC WA NT Hand": { + "x": 299, + "y": 720, + "w": 274, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Edu AU VIC WA NT Pre": { + "x": 587, + "y": 720, + "w": 257, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Edu NSW ACT Cursive": { + "x": 858, + "y": 720, + "w": 276, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Edu NSW ACT Foundation": { + "x": 0, + "y": 760, + "w": 239, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Edu NSW ACT Hand Pre": { + "x": 253, + "y": 760, + "w": 294, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Edu QLD Beginner": { + "x": 561, + "y": 760, + "w": 177, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Edu QLD Hand": { + "x": 752, + "y": 760, + "w": 175, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Edu SA Beginner": { + "x": 941, + "y": 760, + "w": 155, + "ch": 2, + "s": ["400", "500", "600", "700"] + }, + "Edu SA Hand": { + "x": 0, + "y": 0, + "w": 159, + "ch": 3, + "s": ["400", "500", "600", "700"] + }, + "Edu TAS Beginner": { + "x": 173, + "y": 0, + "w": 175, + "ch": 3, + "s": ["400", "500", "600", "700"] + }, + "Edu VIC WA NT Beginner": { + "x": 362, + "y": 0, + "w": 243, + "ch": 3, + "s": ["400", "500", "600", "700"] + }, + "Edu VIC WA NT Hand": { + "x": 619, + "y": 0, + "w": 230, + "ch": 3, + "s": ["400", "500", "600", "700"] + }, + "Edu VIC WA NT Hand Pre": { + "x": 863, + "y": 0, + "w": 282, + "ch": 3, + "s": ["400", "500", "600", "700"] + }, + "El Messiri": { + "x": 0, + "y": 40, + "w": 109, + "ch": 3, + "s": ["400", "500", "600", "700"] + }, + "Electrolize": { "x": 123, "y": 40, "w": 123, "ch": 3, "s": ["400"] }, + "Elms Sans": { + "x": 260, + "y": 40, + "w": 123, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Elsie": { "x": 397, "y": 40, "w": 61, "ch": 3, "s": ["400", "900"] }, + "Elsie Swash Caps": { + "x": 472, + "y": 40, + "w": 190, + "ch": 3, + "s": ["400", "900"] + }, + "Emblema One": { "x": 676, "y": 40, "w": 198, "ch": 3, "s": ["400"] }, + "Emilys Candy": { "x": 888, "y": 40, "w": 148, "ch": 3, "s": ["400"] }, + "Encode Sans": { + "x": 1050, + "y": 40, + "w": 141, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Encode Sans Condensed": { + "x": 0, + "y": 80, + "w": 227, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Encode Sans Expanded": { + "x": 241, + "y": 80, + "w": 287, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Encode Sans SC": { + "x": 542, + "y": 80, + "w": 186, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Encode Sans Semi Condensed": { + "x": 742, + "y": 80, + "w": 299, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Encode Sans Semi Expanded": { + "x": 0, + "y": 120, + "w": 330, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Engagement": { "x": 344, "y": 120, "w": 80, "ch": 3, "s": ["400"] }, + "Englebert": { "x": 438, "y": 120, "w": 97, "ch": 3, "s": ["400"] }, + "Enriqueta": { + "x": 549, + "y": 120, + "w": 115, + "ch": 3, + "s": ["400", "500", "600", "700"] + }, + "Ephesis": { "x": 678, "y": 120, "w": 72, "ch": 3, "s": ["400"] }, + "Epilogue": { + "x": 764, + "y": 120, + "w": 109, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Epunda Sans": { + "x": 887, + "y": 120, + "w": 133, + "ch": 3, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Epunda Slab": { + "x": 1034, + "y": 120, + "w": 132, + "ch": 3, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Erica One": { "x": 0, "y": 160, "w": 131, "ch": 3, "s": ["400"] }, + "Esteban": { "x": 145, "y": 160, "w": 93, "ch": 3, "s": ["400"] }, + "Estonia": { "x": 252, "y": 160, "w": 47, "ch": 3, "s": ["400"] }, + "Euphoria Script": { "x": 313, "y": 160, "w": 127, "ch": 3, "s": ["400"] }, + "Ewert": { "x": 454, "y": 160, "w": 97, "ch": 3, "s": ["400"] }, + "Exile": { "x": 565, "y": 160, "w": 81, "ch": 3, "s": ["400"] }, + "Exo": { + "x": 660, + "y": 160, + "w": 48, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Exo 2": { + "x": 722, + "y": 160, + "w": 67, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Expletus Sans": { + "x": 803, + "y": 160, + "w": 159, + "ch": 3, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Explora": { "x": 976, "y": 160, "w": 87, "ch": 3, "s": ["400"] }, + "Faculty Glyphic": { "x": 0, "y": 200, "w": 182, "ch": 3, "s": ["400"] }, + "Fahkwang": { + "x": 196, + "y": 200, + "w": 133, + "ch": 3, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Familjen Grotesk": { + "x": 343, + "y": 200, + "w": 178, + "ch": 3, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Fanwood Text": { + "x": 535, + "y": 200, + "w": 140, + "ch": 3, + "s": ["400", "400i"] + }, + "Farro": { + "x": 689, + "y": 200, + "w": 72, + "ch": 3, + "s": ["300", "400", "500", "700"] + }, + "Farsan": { "x": 775, "y": 200, "w": 61, "ch": 3, "s": ["400"] }, + "Fascinate": { "x": 850, "y": 200, "w": 135, "ch": 3, "s": ["400"] }, + "Fascinate Inline": { "x": 0, "y": 240, "w": 220, "ch": 3, "s": ["400"] }, + "Faster One": { "x": 234, "y": 240, "w": 165, "ch": 3, "s": ["400"] }, + "Fasthand": { "x": 413, "y": 240, "w": 84, "ch": 3, "s": ["400"] }, + "Fauna One": { "x": 511, "y": 240, "w": 141, "ch": 3, "s": ["400"] }, + "Faustina": { + "x": 666, + "y": 240, + "w": 97, + "ch": 3, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Federant": { "x": 777, "y": 240, "w": 105, "ch": 3, "s": ["400"] }, + "Federo": { "x": 896, "y": 240, "w": 78, "ch": 3, "s": ["400"] }, + "Felipa": { "x": 988, "y": 240, "w": 60, "ch": 3, "s": ["400"] }, + "Fenix": { "x": 1062, "y": 240, "w": 63, "ch": 3, "s": ["400"] }, + "Festive": { "x": 0, "y": 280, "w": 67, "ch": 3, "s": ["400"] }, + "Figtree": { + "x": 81, + "y": 280, + "w": 85, + "ch": 3, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Finger Paint": { "x": 180, "y": 280, "w": 158, "ch": 3, "s": ["400"] }, + "Finlandica": { + "x": 352, + "y": 280, + "w": 114, + "ch": 3, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Fira Code": { + "x": 480, + "y": 280, + "w": 138, + "ch": 3, + "s": ["300", "400", "500", "600", "700"] + }, + "Fira Mono": { + "x": 632, + "y": 280, + "w": 138, + "ch": 3, + "s": ["400", "500", "700"] + }, + "Fira Sans": { + "x": 784, + "y": 280, + "w": 105, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Fira Sans Condensed": { + "x": 903, + "y": 280, + "w": 211, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Fira Sans Extra Condensed": { + "x": 0, + "y": 320, + "w": 244, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Fjalla One": { "x": 258, "y": 320, "w": 98, "ch": 3, "s": ["400"] }, + "Fjord One": { "x": 370, "y": 320, "w": 115, "ch": 3, "s": ["400"] }, + "Flamenco": { "x": 499, "y": 320, "w": 99, "ch": 3, "s": ["300", "400"] }, + "Flavors": { "x": 612, "y": 320, "w": 82, "ch": 3, "s": ["400"] }, + "Fleur De Leah": { "x": 708, "y": 320, "w": 133, "ch": 3, "s": ["400"] }, + "Flow Block": { "x": 855, "y": 320, "w": 126, "ch": 3, "s": ["400"] }, + "Flow Circular": { "x": 995, "y": 320, "w": 145, "ch": 3, "s": ["400"] }, + "Flow Rounded": { "x": 0, "y": 360, "w": 160, "ch": 3, "s": ["400"] }, + "Foldit": { + "x": 174, + "y": 360, + "w": 54, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Fondamento": { + "x": 242, + "y": 360, + "w": 133, + "ch": 3, + "s": ["400", "400i"] + }, + "Fontdiner Swanky": { "x": 389, "y": 360, "w": 239, "ch": 3, "s": ["400"] }, + "Forum": { "x": 642, "y": 360, "w": 70, "ch": 3, "s": ["400"] }, + "Fragment Mono": { + "x": 726, + "y": 360, + "w": 201, + "ch": 3, + "s": ["400", "400i"] + }, + "Francois One": { "x": 941, "y": 360, "w": 136, "ch": 3, "s": ["400"] }, + "Frank Ruhl Libre": { + "x": 0, + "y": 400, + "w": 184, + "ch": 3, + "s": ["300", "400", "500", "600", "700", "800", "900"] + }, + "Fraunces": { + "x": 198, + "y": 400, + "w": 112, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Freckle Face": { "x": 324, "y": 400, "w": 141, "ch": 3, "s": ["400"] }, + "Fredericka the Great": { + "x": 479, + "y": 400, + "w": 243, + "ch": 3, + "s": ["400"] + }, + "Fredoka": { + "x": 736, + "y": 400, + "w": 96, + "ch": 3, + "s": ["300", "400", "500", "600", "700"] + }, + "Fredoka One": { "x": 846, "y": 400, "w": 155, "ch": 3, "s": ["400"] }, + "Freehand": { "x": 1015, "y": 400, "w": 85, "ch": 3, "s": ["400"] }, + "Freeman": { "x": 0, "y": 440, "w": 91, "ch": 3, "s": ["400"] }, + "Fresca": { "x": 105, "y": 440, "w": 67, "ch": 3, "s": ["400"] }, + "Frijole": { "x": 186, "y": 440, "w": 132, "ch": 3, "s": ["400"] }, + "Fruktur": { "x": 332, "y": 440, "w": 94, "ch": 3, "s": ["400", "400i"] }, + "Fugaz One": { "x": 440, "y": 440, "w": 127, "ch": 3, "s": ["400"] }, + "Fuggles": { "x": 581, "y": 440, "w": 58, "ch": 3, "s": ["400"] }, + "Funnel Display": { + "x": 653, + "y": 440, + "w": 174, + "ch": 3, + "s": ["300", "400", "500", "600", "700", "800"] + }, + "Funnel Sans": { + "x": 841, + "y": 440, + "w": 140, + "ch": 3, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Fustat": { + "x": 995, + "y": 440, + "w": 75, + "ch": 3, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Fuzzy Bubbles": { + "x": 0, + "y": 480, + "w": 179, + "ch": 3, + "s": ["400", "700"] + }, + "Ga Maamli": { "x": 193, "y": 480, "w": 111, "ch": 3, "s": ["400"] }, + "Gabarito": { + "x": 318, + "y": 480, + "w": 99, + "ch": 3, + "s": ["400", "500", "600", "700", "800", "900"] + }, + "Gabriela": { "x": 431, "y": 480, "w": 106, "ch": 3, "s": ["400"] }, + "Gaegu": { + "x": 551, + "y": 480, + "w": 70, + "ch": 3, + "s": ["300", "400", "700"] + }, + "Gafata": { "x": 635, "y": 480, "w": 72, "ch": 3, "s": ["400"] }, + "Gajraj One": { "x": 721, "y": 480, "w": 148, "ch": 3, "s": ["400"] }, + "Galada": { "x": 883, "y": 480, "w": 78, "ch": 3, "s": ["400"] }, + "Galdeano": { "x": 975, "y": 480, "w": 98, "ch": 3, "s": ["400"] }, + "Galindo": { "x": 1087, "y": 480, "w": 101, "ch": 3, "s": ["400"] }, + "Gamja Flower": { "x": 0, "y": 520, "w": 130, "ch": 3, "s": ["400"] }, + "Gantari": { + "x": 144, + "y": 520, + "w": 89, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Gasoek One": { "x": 247, "y": 520, "w": 163, "ch": 3, "s": ["400"] }, + "Gayathri": { + "x": 424, + "y": 520, + "w": 99, + "ch": 3, + "s": ["100", "400", "700"] + }, + "Geist": { + "x": 537, + "y": 520, + "w": 66, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Geist Mono": { + "x": 617, + "y": 520, + "w": 152, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Gelasio": { + "x": 783, + "y": 520, + "w": 87, + "ch": 3, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Gemunu Libre": { + "x": 884, + "y": 520, + "w": 126, + "ch": 3, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Genos": { + "x": 1024, + "y": 520, + "w": 67, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Gentium Book Basic": { + "x": 0, + "y": 560, + "w": 209, + "ch": 3, + "s": ["400", "400i", "700", "700i"] + }, + "Gentium Book Plus": { + "x": 223, + "y": 560, + "w": 198, + "ch": 3, + "s": ["400", "400i", "700", "700i"] + }, + "Gentium Plus": { + "x": 435, + "y": 560, + "w": 140, + "ch": 3, + "s": ["400", "400i", "700", "700i"] + }, + "Geo": { "x": 589, "y": 560, "w": 42, "ch": 3, "s": ["400", "400i"] }, + "Geologica": { + "x": 645, + "y": 560, + "w": 122, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Geom": { + "x": 781, + "y": 560, + "w": 76, + "ch": 3, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Georama": { + "x": 871, + "y": 560, + "w": 102, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Geostar": { "x": 987, "y": 560, "w": 133, "ch": 3, "s": ["400"] }, + "Geostar Fill": { "x": 0, "y": 600, "w": 192, "ch": 3, "s": ["400"] }, + "Germania One": { "x": 206, "y": 600, "w": 137, "ch": 3, "s": ["400"] }, + "GFS Didot": { "x": 357, "y": 600, "w": 121, "ch": 3, "s": ["400"] }, + "GFS Neohellenic": { + "x": 492, + "y": 600, + "w": 155, + "ch": 3, + "s": ["400", "400i", "700", "700i"] + }, + "Gideon Roman": { "x": 661, "y": 600, "w": 172, "ch": 3, "s": ["400"] }, + "Gidole": { "x": 847, "y": 600, "w": 73, "ch": 3, "s": ["400"] }, + "Gidugu": { "x": 934, "y": 600, "w": 58, "ch": 3, "s": ["400"] }, + "Gilda Display": { "x": 1006, "y": 600, "w": 156, "ch": 3, "s": ["400"] }, + "Girassol": { "x": 0, "y": 640, "w": 91, "ch": 3, "s": ["400"] }, + "Give You Glory": { "x": 105, "y": 640, "w": 170, "ch": 3, "s": ["400"] }, + "Glass Antiqua": { "x": 289, "y": 640, "w": 130, "ch": 3, "s": ["400"] }, + "Glegoo": { "x": 433, "y": 640, "w": 90, "ch": 3, "s": ["400", "700"] }, + "Gloock": { "x": 537, "y": 640, "w": 89, "ch": 3, "s": ["400"] }, + "Gloria Hallelujah": { + "x": 640, + "y": 640, + "w": 190, + "ch": 3, + "s": ["400"] + }, + "Glory": { + "x": 844, + "y": 640, + "w": 56, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Gluten": { + "x": 914, + "y": 640, + "w": 90, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Goblin One": { "x": 0, "y": 680, "w": 202, "ch": 3, "s": ["400"] }, + "Gochi Hand": { "x": 216, "y": 680, "w": 121, "ch": 3, "s": ["400"] }, + "Goldman": { "x": 351, "y": 680, "w": 118, "ch": 3, "s": ["400", "700"] }, + "Golos Text": { + "x": 483, + "y": 680, + "w": 130, + "ch": 3, + "s": ["400", "500", "600", "700", "800", "900"] + }, + "Google Sans": { + "x": 627, + "y": 680, + "w": 146, + "ch": 3, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Google Sans Code": { + "x": 787, + "y": 680, + "w": 239, + "ch": 3, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Google Sans Flex": { + "x": 0, + "y": 720, + "w": 193, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Gorditas": { "x": 207, "y": 720, "w": 111, "ch": 3, "s": ["400", "700"] }, + "Gothic A1": { + "x": 332, + "y": 720, + "w": 114, + "ch": 3, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Gotu": { "x": 460, "y": 720, "w": 67, "ch": 3, "s": ["400"] }, + "Goudy Bookletter 1911": { + "x": 541, + "y": 720, + "w": 218, + "ch": 3, + "s": ["400"] + }, + "Gowun Batang": { + "x": 773, + "y": 720, + "w": 162, + "ch": 3, + "s": ["400", "700"] + }, + "Gowun Dodum": { "x": 949, "y": 720, "w": 161, "ch": 3, "s": ["400"] }, + "Graduate": { "x": 0, "y": 760, "w": 134, "ch": 3, "s": ["400"] }, + "Grand Hotel": { "x": 148, "y": 760, "w": 101, "ch": 3, "s": ["400"] }, + "Grandiflora One": { "x": 263, "y": 760, "w": 174, "ch": 3, "s": ["400"] }, + "Grandstander": { + "x": 451, + "y": 760, + "w": 173, + "ch": 3, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Grape Nuts": { "x": 638, "y": 760, "w": 111, "ch": 3, "s": ["400"] }, + "Gravitas One": { "x": 763, "y": 760, "w": 230, "ch": 3, "s": ["400"] }, + "Great Vibes": { "x": 1007, "y": 760, "w": 104, "ch": 3, "s": ["400"] }, + "Grechen Fuemen": { "x": 0, "y": 0, "w": 179, "ch": 4, "s": ["400"] }, + "Grenze": { + "x": 193, + "y": 0, + "w": 69, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Grenze Gotisch": { + "x": 276, + "y": 0, + "w": 135, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Grey Qo": { "x": 425, "y": 0, "w": 64, "ch": 4, "s": ["400"] }, + "Griffy": { "x": 503, "y": 0, "w": 71, "ch": 4, "s": ["400"] }, + "Gruppo": { "x": 588, "y": 0, "w": 95, "ch": 4, "s": ["400"] }, + "Gudea": { + "x": 697, + "y": 0, + "w": 74, + "ch": 4, + "s": ["400", "400i", "700", "700i"] + }, + "Gugi": { "x": 785, "y": 0, "w": 60, "ch": 4, "s": ["400"] }, + "Gulzar": { "x": 859, "y": 0, "w": 77, "ch": 4, "s": ["400"] }, + "Gupter": { + "x": 950, + "y": 0, + "w": 72, + "ch": 4, + "s": ["400", "500", "700"] + }, + "Gurajada": { "x": 1036, "y": 0, "w": 71, "ch": 4, "s": ["400"] }, + "Gwendolyn": { "x": 0, "y": 40, "w": 89, "ch": 4, "s": ["400", "700"] }, + "Habibi": { "x": 103, "y": 40, "w": 81, "ch": 4, "s": ["400"] }, + "Hachi Maru Pop": { "x": 198, "y": 40, "w": 246, "ch": 4, "s": ["400"] }, + "Hahmlet": { + "x": 458, + "y": 40, + "w": 112, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Halant": { + "x": 584, + "y": 40, + "w": 75, + "ch": 4, + "s": ["300", "400", "500", "600", "700"] + }, + "Hammersmith One": { "x": 673, "y": 40, "w": 217, "ch": 4, "s": ["400"] }, + "Hanalei": { "x": 904, "y": 40, "w": 91, "ch": 4, "s": ["400"] }, + "Hanalei Fill": { "x": 1009, "y": 40, "w": 136, "ch": 4, "s": ["400"] }, + "Handjet": { + "x": 0, + "y": 80, + "w": 68, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Handlee": { "x": 82, "y": 80, "w": 88, "ch": 4, "s": ["400"] }, + "Hanken Grotesk": { + "x": 184, + "y": 80, + "w": 181, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Hanuman": { + "x": 379, + "y": 80, + "w": 123, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Happy Monkey": { "x": 516, "y": 80, "w": 189, "ch": 4, "s": ["400"] }, + "Harmattan": { + "x": 719, + "y": 80, + "w": 99, + "ch": 4, + "s": ["400", "500", "600", "700"] + }, + "Headland One": { "x": 832, "y": 80, "w": 185, "ch": 4, "s": ["400"] }, + "Hedvig Letters Sans": { + "x": 0, + "y": 120, + "w": 224, + "ch": 4, + "s": ["400"] + }, + "Hedvig Letters Serif": { + "x": 238, + "y": 120, + "w": 226, + "ch": 4, + "s": ["400"] + }, + "Heebo": { + "x": 478, + "y": 120, + "w": 78, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Henny Penny": { "x": 570, "y": 120, "w": 148, "ch": 4, "s": ["400"] }, + "Hepta Slab": { + "x": 732, + "y": 120, + "w": 150, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Herr Von Muellerhoff": { + "x": 896, + "y": 120, + "w": 145, + "ch": 4, + "s": ["400"] + }, + "Hi Melody": { "x": 1055, "y": 120, "w": 84, "ch": 4, "s": ["400"] }, + "Hina Mincho": { "x": 0, "y": 160, "w": 128, "ch": 4, "s": ["400"] }, + "Hind": { + "x": 142, + "y": 160, + "w": 56, + "ch": 4, + "s": ["300", "400", "500", "600", "700"] + }, + "Hind Guntur": { + "x": 212, + "y": 160, + "w": 134, + "ch": 4, + "s": ["300", "400", "500", "600", "700"] + }, + "Hind Madurai": { + "x": 360, + "y": 160, + "w": 146, + "ch": 4, + "s": ["300", "400", "500", "600", "700"] + }, + "Hind Mysuru": { + "x": 520, + "y": 160, + "w": 138, + "ch": 4, + "s": ["300", "400", "500", "600", "700"] + }, + "Hind Siliguri": { + "x": 672, + "y": 160, + "w": 130, + "ch": 4, + "s": ["300", "400", "500", "600", "700"] + }, + "Hind Vadodara": { + "x": 816, + "y": 160, + "w": 161, + "ch": 4, + "s": ["300", "400", "500", "600", "700"] + }, + "Holtwood One SC": { "x": 0, "y": 200, "w": 292, "ch": 4, "s": ["400"] }, + "Homemade Apple": { "x": 306, "y": 200, "w": 239, "ch": 4, "s": ["400"] }, + "Homenaje": { "x": 559, "y": 200, "w": 85, "ch": 4, "s": ["400"] }, + "Honk": { "x": 658, "y": 200, "w": 59, "ch": 4, "s": ["400"] }, + "Host Grotesk": { + "x": 731, + "y": 200, + "w": 150, + "ch": 4, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Hubballi": { "x": 895, "y": 200, "w": 87, "ch": 4, "s": ["400"] }, + "Hubot Sans": { + "x": 996, + "y": 200, + "w": 137, + "ch": 4, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Huninn": { "x": 0, "y": 240, "w": 89, "ch": 4, "s": ["400"] }, + "Hurricane": { "x": 103, "y": 240, "w": 76, "ch": 4, "s": ["400"] }, + "Iansui": { "x": 193, "y": 240, "w": 78, "ch": 4, "s": ["400"] }, + "Ibarra Real Nova": { + "x": 285, + "y": 240, + "w": 182, + "ch": 4, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "IBM Plex Mono": { + "x": 481, + "y": 240, + "w": 196, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "IBM Plex Sans": { + "x": 691, + "y": 240, + "w": 163, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "IBM Plex Sans Arabic": { + "x": 868, + "y": 240, + "w": 237, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "IBM Plex Sans Condensed": { + "x": 0, + "y": 280, + "w": 263, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "IBM Plex Sans Devanagari": { + "x": 277, + "y": 280, + "w": 289, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "IBM Plex Sans Hebrew": { + "x": 580, + "y": 280, + "w": 253, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "IBM Plex Sans JP": { + "x": 847, + "y": 280, + "w": 204, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "IBM Plex Sans KR": { + "x": 0, + "y": 320, + "w": 199, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "IBM Plex Sans Thai": { + "x": 213, + "y": 320, + "w": 215, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "IBM Plex Sans Thai Looped": { + "x": 442, + "y": 320, + "w": 300, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "IBM Plex Serif": { + "x": 756, + "y": 320, + "w": 167, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Iceberg": { "x": 937, "y": 320, "w": 84, "ch": 4, "s": ["400"] }, + "Iceland": { "x": 1035, "y": 320, "w": 75, "ch": 4, "s": ["400"] }, + "IM Fell Double Pica": { + "x": 0, + "y": 360, + "w": 204, + "ch": 4, + "s": ["400", "400i"] + }, + "IM Fell Double Pica SC": { + "x": 218, + "y": 360, + "w": 261, + "ch": 4, + "s": ["400"] + }, + "IM Fell DW Pica": { + "x": 493, + "y": 360, + "w": 175, + "ch": 4, + "s": ["400", "400i"] + }, + "IM Fell DW Pica SC": { + "x": 682, + "y": 360, + "w": 220, + "ch": 4, + "s": ["400"] + }, + "IM Fell English": { + "x": 916, + "y": 360, + "w": 168, + "ch": 4, + "s": ["400", "400i"] + }, + "IM Fell English SC": { "x": 0, "y": 400, "w": 215, "ch": 4, "s": ["400"] }, + "IM Fell French Canon": { + "x": 229, + "y": 400, + "w": 233, + "ch": 4, + "s": ["400", "400i"] + }, + "IM Fell French Canon SC": { + "x": 476, + "y": 400, + "w": 279, + "ch": 4, + "s": ["400"] + }, + "IM Fell Great Primer": { + "x": 769, + "y": 400, + "w": 220, + "ch": 4, + "s": ["400", "400i"] + }, + "IM Fell Great Primer SC": { + "x": 0, + "y": 440, + "w": 289, + "ch": 4, + "s": ["400"] + }, + "Imbue": { + "x": 303, + "y": 440, + "w": 52, + "ch": 4, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Imperial Script": { "x": 369, "y": 440, "w": 119, "ch": 4, "s": ["400"] }, + "Imprima": { "x": 502, "y": 440, "w": 98, "ch": 4, "s": ["400"] }, + "Inclusive Sans": { + "x": 614, + "y": 440, + "w": 172, + "ch": 4, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Inconsolata": { + "x": 800, + "y": 440, + "w": 140, + "ch": 4, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Inder": { "x": 954, "y": 440, "w": 64, "ch": 4, "s": ["400"] }, + "Indie Flower": { "x": 1032, "y": 440, "w": 125, "ch": 4, "s": ["400"] }, + "Ingrid Darling": { "x": 0, "y": 480, "w": 114, "ch": 4, "s": ["400"] }, + "Inika": { "x": 128, "y": 480, "w": 67, "ch": 4, "s": ["400", "700"] }, + "Inknut Antiqua": { + "x": 209, + "y": 480, + "w": 211, + "ch": 4, + "s": ["300", "400", "500", "600", "700", "800", "900"] + }, + "Inria Sans": { + "x": 434, + "y": 480, + "w": 110, + "ch": 4, + "s": ["300", "300i", "400", "400i", "700", "700i"] + }, + "Inria Serif": { + "x": 558, + "y": 480, + "w": 120, + "ch": 4, + "s": ["300", "300i", "400", "400i", "700", "700i"] + }, + "Inspiration": { "x": 692, "y": 480, "w": 78, "ch": 4, "s": ["400"] }, + "Instrument Sans": { + "x": 784, + "y": 480, + "w": 189, + "ch": 4, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Instrument Serif": { + "x": 987, + "y": 480, + "w": 143, + "ch": 4, + "s": ["400", "400i"] + }, + "Intel One Mono": { + "x": 0, + "y": 520, + "w": 215, + "ch": 4, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Inter": { + "x": 229, + "y": 520, + "w": 60, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Inter Tight": { + "x": 303, + "y": 520, + "w": 113, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Irish Grover": { "x": 430, "y": 520, "w": 139, "ch": 4, "s": ["400"] }, + "Island Moments": { "x": 583, "y": 520, "w": 125, "ch": 4, "s": ["400"] }, + "Istok Web": { + "x": 722, + "y": 520, + "w": 116, + "ch": 4, + "s": ["400", "400i", "700", "700i"] + }, + "Italiana": { "x": 852, "y": 520, "w": 83, "ch": 4, "s": ["400"] }, + "Italianno": { "x": 949, "y": 520, "w": 68, "ch": 4, "s": ["400"] }, + "Itim": { "x": 1031, "y": 520, "w": 52, "ch": 4, "s": ["400"] }, + "Jacquard 12": { "x": 0, "y": 560, "w": 110, "ch": 4, "s": ["400"] }, + "Jacquard 12 Charted": { + "x": 124, + "y": 560, + "w": 184, + "ch": 4, + "s": ["400"] + }, + "Jacquard 24": { "x": 322, "y": 560, "w": 108, "ch": 4, "s": ["400"] }, + "Jacquard 24 Charted": { + "x": 444, + "y": 560, + "w": 179, + "ch": 4, + "s": ["400"] + }, + "Jacquarda Bastarda 9": { + "x": 637, + "y": 560, + "w": 261, + "ch": 4, + "s": ["400"] + }, + "Jacquarda Bastarda 9 Charted": { + "x": 0, + "y": 600, + "w": 357, + "ch": 4, + "s": ["400"] + }, + "Jacques Francois": { "x": 371, "y": 600, "w": 197, "ch": 4, "s": ["400"] }, + "Jacques Francois Shadow": { + "x": 582, + "y": 600, + "w": 315, + "ch": 4, + "s": ["400"] + }, + "Jaini": { "x": 911, "y": 600, "w": 50, "ch": 4, "s": ["400"] }, + "Jaini Purva": { "x": 975, "y": 600, "w": 102, "ch": 4, "s": ["400"] }, + "Jaldi": { "x": 1091, "y": 600, "w": 52, "ch": 4, "s": ["400", "700"] }, + "Jaro": { "x": 0, "y": 640, "w": 50, "ch": 4, "s": ["400"] }, + "Jersey 10": { "x": 64, "y": 640, "w": 84, "ch": 4, "s": ["400"] }, + "Jersey 10 Charted": { + "x": 162, + "y": 640, + "w": 158, + "ch": 4, + "s": ["400"] + }, + "Jersey 15": { "x": 334, "y": 640, "w": 91, "ch": 4, "s": ["400"] }, + "Jersey 15 Charted": { + "x": 439, + "y": 640, + "w": 168, + "ch": 4, + "s": ["400"] + }, + "Jersey 20": { "x": 621, "y": 640, "w": 98, "ch": 4, "s": ["400"] }, + "Jersey 20 Charted": { + "x": 733, + "y": 640, + "w": 176, + "ch": 4, + "s": ["400"] + }, + "Jersey 25": { "x": 923, "y": 640, "w": 102, "ch": 4, "s": ["400"] }, + "Jersey 25 Charted": { "x": 0, "y": 680, "w": 180, "ch": 4, "s": ["400"] }, + "JetBrains Mono": { + "x": 194, + "y": 680, + "w": 210, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Jim Nightshade": { "x": 418, "y": 680, "w": 119, "ch": 4, "s": ["400"] }, + "Joan": { "x": 551, "y": 680, "w": 54, "ch": 4, "s": ["400"] }, + "Jockey One": { "x": 619, "y": 680, "w": 108, "ch": 4, "s": ["400"] }, + "Jolly Lodger": { "x": 741, "y": 680, "w": 90, "ch": 4, "s": ["400"] }, + "Jomhuria": { "x": 845, "y": 680, "w": 64, "ch": 4, "s": ["400"] }, + "Jomolhari": { "x": 923, "y": 680, "w": 116, "ch": 4, "s": ["400"] }, + "Josefin Sans": { + "x": 1053, + "y": 680, + "w": 138, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Josefin Slab": { + "x": 0, + "y": 720, + "w": 123, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Jost": { + "x": 137, + "y": 720, + "w": 43, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Joti One": { "x": 194, "y": 720, "w": 100, "ch": 4, "s": ["400"] }, + "Jua": { "x": 308, "y": 720, "w": 48, "ch": 4, "s": ["400"] }, + "Judson": { + "x": 370, + "y": 720, + "w": 80, + "ch": 4, + "s": ["400", "400i", "700", "700i"] + }, + "Julee": { "x": 464, "y": 720, "w": 57, "ch": 4, "s": ["400"] }, + "Julius Sans One": { "x": 535, "y": 720, "w": 210, "ch": 4, "s": ["400"] }, + "Junge": { "x": 759, "y": 720, "w": 73, "ch": 4, "s": ["400"] }, + "Jura": { + "x": 846, + "y": 720, + "w": 59, + "ch": 4, + "s": ["300", "400", "500", "600", "700"] + }, + "Just Another Hand": { + "x": 919, + "y": 720, + "w": 112, + "ch": 4, + "s": ["400"] + }, + "Just Me Again Down Here": { + "x": 0, + "y": 760, + "w": 200, + "ch": 4, + "s": ["400"] + }, + "K2D": { + "x": 214, + "y": 760, + "w": 56, + "ch": 4, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Kablammo": { "x": 284, "y": 760, "w": 128, "ch": 4, "s": ["400"] }, + "Kadwa": { "x": 426, "y": 760, "w": 87, "ch": 4, "s": ["400", "700"] }, + "Kaisei Decol": { + "x": 527, + "y": 760, + "w": 154, + "ch": 4, + "s": ["400", "500", "700"] + }, + "Kaisei HarunoUmi": { + "x": 695, + "y": 760, + "w": 223, + "ch": 4, + "s": ["400", "500", "700"] + }, + "Kaisei Opti": { + "x": 932, + "y": 760, + "w": 137, + "ch": 4, + "s": ["400", "500", "700"] + }, + "Kaisei Tokumin": { + "x": 0, + "y": 0, + "w": 191, + "ch": 5, + "s": ["400", "500", "700", "800"] + }, + "Kalam": { "x": 205, "y": 0, "w": 70, "ch": 5, "s": ["300", "400", "700"] }, + "Kalnia": { + "x": 289, + "y": 0, + "w": 87, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "Kalnia Glaze": { + "x": 390, + "y": 0, + "w": 162, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "Kameron": { + "x": 566, + "y": 0, + "w": 107, + "ch": 5, + "s": ["400", "500", "600", "700"] + }, + "Kanchenjunga": { + "x": 687, + "y": 0, + "w": 165, + "ch": 5, + "s": ["400", "500", "600", "700"] + }, + "Kanit": { + "x": 866, + "y": 0, + "w": 65, + "ch": 5, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Kantumruy Pro": { + "x": 945, + "y": 0, + "w": 175, + "ch": 5, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Kapakana": { "x": 0, "y": 40, "w": 83, "ch": 5, "s": ["300", "400"] }, + "Karantina": { + "x": 97, + "y": 40, + "w": 66, + "ch": 5, + "s": ["300", "400", "700"] + }, + "Karla": { + "x": 177, + "y": 40, + "w": 65, + "ch": 5, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Karma": { + "x": 256, + "y": 40, + "w": 79, + "ch": 5, + "s": ["300", "400", "500", "600", "700"] + }, + "Katibeh": { "x": 349, "y": 40, "w": 67, "ch": 5, "s": ["400"] }, + "Kaushan Script": { "x": 430, "y": 40, "w": 159, "ch": 5, "s": ["400"] }, + "Kavivanar": { "x": 603, "y": 40, "w": 107, "ch": 5, "s": ["400"] }, + "Kavoon": { "x": 724, "y": 40, "w": 95, "ch": 5, "s": ["400"] }, + "Kay Pho Du": { + "x": 833, + "y": 40, + "w": 132, + "ch": 5, + "s": ["400", "500", "600", "700"] + }, + "Kdam Thmor Pro": { "x": 979, "y": 40, "w": 189, "ch": 5, "s": ["400"] }, + "Keania One": { "x": 0, "y": 80, "w": 126, "ch": 5, "s": ["400"] }, + "Kedebideri": { + "x": 140, + "y": 80, + "w": 120, + "ch": 5, + "s": ["400", "500", "600", "700", "800", "900"] + }, + "Kelly Slab": { "x": 274, "y": 80, "w": 115, "ch": 5, "s": ["400"] }, + "Kenia": { "x": 403, "y": 80, "w": 57, "ch": 5, "s": ["400"] }, + "Khand": { + "x": 474, + "y": 80, + "w": 61, + "ch": 5, + "s": ["300", "400", "500", "600", "700"] + }, + "Khula": { + "x": 549, + "y": 80, + "w": 68, + "ch": 5, + "s": ["300", "400", "600", "700", "800"] + }, + "Kings": { "x": 631, "y": 80, "w": 63, "ch": 5, "s": ["400"] }, + "Kirang Haerang": { "x": 708, "y": 80, "w": 145, "ch": 5, "s": ["400"] }, + "Kite One": { "x": 867, "y": 80, "w": 97, "ch": 5, "s": ["400"] }, + "Kiwi Maru": { + "x": 978, + "y": 80, + "w": 135, + "ch": 5, + "s": ["300", "400", "500"] + }, + "Klee One": { "x": 0, "y": 120, "w": 109, "ch": 5, "s": ["400", "600"] }, + "Knewave": { "x": 123, "y": 120, "w": 103, "ch": 5, "s": ["400"] }, + "Kodchasan": { + "x": 240, + "y": 120, + "w": 139, + "ch": 5, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Kode Mono": { + "x": 393, + "y": 120, + "w": 138, + "ch": 5, + "s": ["400", "500", "600", "700"] + }, + "Koh Santepheap": { + "x": 545, + "y": 120, + "w": 195, + "ch": 5, + "s": ["100", "300", "400", "700", "900"] + }, + "KoHo": { + "x": 754, + "y": 120, + "w": 66, + "ch": 5, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Kolker Brush": { "x": 834, "y": 120, "w": 84, "ch": 5, "s": ["400"] }, + "Konkhmer Sleokchher": { + "x": 0, + "y": 160, + "w": 269, + "ch": 5, + "s": ["400"] + }, + "Kosugi": { "x": 283, "y": 160, "w": 80, "ch": 5, "s": ["400"] }, + "Kosugi Maru": { "x": 377, "y": 160, "w": 140, "ch": 5, "s": ["400"] }, + "Kotta One": { "x": 531, "y": 160, "w": 110, "ch": 5, "s": ["400"] }, + "Koulen": { "x": 655, "y": 160, "w": 72, "ch": 5, "s": ["400"] }, + "Kranky": { "x": 741, "y": 160, "w": 87, "ch": 5, "s": ["400"] }, + "Kreon": { + "x": 842, + "y": 160, + "w": 68, + "ch": 5, + "s": ["300", "400", "500", "600", "700"] + }, + "Kristi": { "x": 924, "y": 160, "w": 46, "ch": 5, "s": ["400"] }, + "Krona One": { "x": 984, "y": 160, "w": 173, "ch": 5, "s": ["400"] }, + "Krub": { + "x": 0, + "y": 200, + "w": 62, + "ch": 5, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Kufam": { + "x": 76, + "y": 200, + "w": 84, + "ch": 5, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Kulim Park": { + "x": 174, + "y": 200, + "w": 116, + "ch": 5, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "600", + "600i", + "700", + "700i" + ] + }, + "Kumar One": { "x": 304, "y": 200, "w": 167, "ch": 5, "s": ["400"] }, + "Kumar One Outline": { + "x": 485, + "y": 200, + "w": 277, + "ch": 5, + "s": ["400"] + }, + "Kumbh Sans": { + "x": 776, + "y": 200, + "w": 147, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Kurale": { "x": 937, "y": 200, "w": 77, "ch": 5, "s": ["400"] }, + "La Belle Aurore": { "x": 1028, "y": 200, "w": 170, "ch": 5, "s": ["400"] }, + "Labrada": { + "x": 0, + "y": 240, + "w": 91, + "ch": 5, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Lacquer": { "x": 105, "y": 240, "w": 102, "ch": 5, "s": ["400"] }, + "Laila": { + "x": 221, + "y": 240, + "w": 60, + "ch": 5, + "s": ["300", "400", "500", "600", "700"] + }, + "Lakki Reddy": { "x": 295, "y": 240, "w": 138, "ch": 5, "s": ["400"] }, + "Lalezar": { "x": 447, "y": 240, "w": 82, "ch": 5, "s": ["400"] }, + "Lancelot": { "x": 543, "y": 240, "w": 80, "ch": 5, "s": ["400"] }, + "Langar": { "x": 637, "y": 240, "w": 83, "ch": 5, "s": ["400"] }, + "Lateef": { + "x": 734, + "y": 240, + "w": 57, + "ch": 5, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Lato": { + "x": 805, + "y": 240, + "w": 55, + "ch": 5, + "s": [ + "100", + "100i", + "300", + "300i", + "400", + "400i", + "700", + "700i", + "900", + "900i" + ] + }, + "Lavishly Yours": { "x": 874, "y": 240, "w": 135, "ch": 5, "s": ["400"] }, + "League Gothic": { "x": 1023, "y": 240, "w": 97, "ch": 5, "s": ["400"] }, + "League Script": { "x": 0, "y": 280, "w": 156, "ch": 5, "s": ["400"] }, + "League Spartan": { + "x": 170, + "y": 280, + "w": 166, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Leckerli One": { "x": 350, "y": 280, "w": 144, "ch": 5, "s": ["400"] }, + "Ledger": { "x": 508, "y": 280, "w": 91, "ch": 5, "s": ["400"] }, + "Lekton": { + "x": 613, + "y": 280, + "w": 80, + "ch": 5, + "s": ["400", "400i", "700", "700i"] + }, + "Lemon": { "x": 707, "y": 280, "w": 100, "ch": 5, "s": ["400"] }, + "Lemonada": { + "x": 821, + "y": 280, + "w": 153, + "ch": 5, + "s": ["300", "400", "500", "600", "700"] + }, + "Lexend": { + "x": 988, + "y": 280, + "w": 93, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Lexend Deca": { + "x": 0, + "y": 320, + "w": 159, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Lexend Exa": { + "x": 173, + "y": 320, + "w": 176, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Lexend Giga": { + "x": 363, + "y": 320, + "w": 205, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Lexend Mega": { + "x": 582, + "y": 320, + "w": 223, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Lexend Peta": { + "x": 819, + "y": 320, + "w": 218, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Lexend Tera": { + "x": 0, + "y": 360, + "w": 221, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Lexend Zetta": { + "x": 235, + "y": 360, + "w": 260, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Libertinus Keyboard": { + "x": 509, + "y": 360, + "w": 490, + "ch": 5, + "s": ["400"] + }, + "Libertinus Math": { "x": 1013, "y": 360, "w": 166, "ch": 5, "s": ["400"] }, + "Libertinus Mono": { "x": 0, "y": 400, "w": 239, "ch": 5, "s": ["400"] }, + "Libertinus Sans": { + "x": 253, + "y": 400, + "w": 161, + "ch": 5, + "s": ["400", "400i", "700", "700i"] + }, + "Libertinus Serif": { + "x": 428, + "y": 400, + "w": 160, + "ch": 5, + "s": ["400", "400i", "600", "600i", "700", "700i"] + }, + "Libertinus Serif Display": { + "x": 602, + "y": 400, + "w": 234, + "ch": 5, + "s": ["400"] + }, + "Libre Barcode 128": { + "x": 850, + "y": 400, + "w": 143, + "ch": 5, + "s": ["400"] + }, + "Libre Barcode 128 Text": { + "x": 1007, + "y": 400, + "w": 183, + "ch": 5, + "s": ["400"] + }, + "Libre Barcode 39": { "x": 0, "y": 440, "w": 193, "ch": 5, "s": ["400"] }, + "Libre Barcode 39 Extended": { + "x": 207, + "y": 440, + "w": 492, + "ch": 5, + "s": ["400"] + }, + "Libre Barcode 39 Extended Text": { + "x": 0, + "y": 480, + "w": 584, + "ch": 5, + "s": ["400"] + }, + "Libre Barcode 39 Text": { + "x": 598, + "y": 480, + "w": 250, + "ch": 5, + "s": ["400"] + }, + "Libre Barcode EAN13 Text": { + "x": 862, + "y": 480, + "w": 87, + "ch": 5, + "s": ["400"] + }, + "Libre Baskerville": { + "x": 963, + "y": 480, + "w": 216, + "ch": 5, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Libre Bodoni": { + "x": 0, + "y": 520, + "w": 153, + "ch": 5, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Libre Caslon Display": { + "x": 167, + "y": 520, + "w": 197, + "ch": 5, + "s": ["400"] + }, + "Libre Caslon Text": { + "x": 378, + "y": 520, + "w": 210, + "ch": 5, + "s": ["400", "400i", "700", "700i"] + }, + "Libre Franklin": { + "x": 602, + "y": 520, + "w": 159, + "ch": 5, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Licorice": { "x": 775, "y": 520, "w": 60, "ch": 5, "s": ["400"] }, + "Life Savers": { + "x": 849, + "y": 520, + "w": 119, + "ch": 5, + "s": ["400", "700", "800"] + }, + "Lilex": { + "x": 982, + "y": 520, + "w": 80, + "ch": 5, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Lilita One": { "x": 1076, "y": 520, "w": 106, "ch": 5, "s": ["400"] }, + "Lily Script One": { "x": 0, "y": 560, "w": 156, "ch": 5, "s": ["400"] }, + "Limelight": { "x": 170, "y": 560, "w": 132, "ch": 5, "s": ["400"] }, + "Linden Hill": { + "x": 316, + "y": 560, + "w": 113, + "ch": 5, + "s": ["400", "400i"] + }, + "LINE Seed JP": { + "x": 443, + "y": 560, + "w": 169, + "ch": 5, + "s": ["100", "400", "700", "800"] + }, + "Linefont": { + "x": 626, + "y": 560, + "w": 28, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Lisu Bosa": { + "x": 668, + "y": 560, + "w": 106, + "ch": 5, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Liter": { "x": 788, "y": 560, "w": 56, "ch": 5, "s": ["400"] }, + "Literata": { + "x": 858, + "y": 560, + "w": 99, + "ch": 5, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Liu Jian Mao Cao": { "x": 971, "y": 560, "w": 164, "ch": 5, "s": ["400"] }, + "Livvic": { + "x": 0, + "y": 600, + "w": 66, + "ch": 5, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "900", + "900i" + ] + }, + "Lobster": { "x": 80, "y": 600, "w": 72, "ch": 5, "s": ["400"] }, + "Lobster Two": { + "x": 166, + "y": 600, + "w": 114, + "ch": 5, + "s": ["400", "400i", "700", "700i"] + }, + "Londrina Outline": { "x": 294, "y": 600, "w": 156, "ch": 5, "s": ["400"] }, + "Londrina Shadow": { "x": 464, "y": 600, "w": 165, "ch": 5, "s": ["400"] }, + "Londrina Sketch": { "x": 643, "y": 600, "w": 156, "ch": 5, "s": ["400"] }, + "Londrina Solid": { + "x": 813, + "y": 600, + "w": 136, + "ch": 5, + "s": ["100", "300", "400", "900"] + }, + "Long Cang": { "x": 963, "y": 600, "w": 98, "ch": 5, "s": ["400"] }, + "Lora": { + "x": 1075, + "y": 600, + "w": 59, + "ch": 5, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Love Light": { "x": 0, "y": 640, "w": 88, "ch": 5, "s": ["400"] }, + "Love Ya Like A Sister": { + "x": 102, + "y": 640, + "w": 235, + "ch": 5, + "s": ["400"] + }, + "Loved by the King": { + "x": 351, + "y": 640, + "w": 145, + "ch": 5, + "s": ["400"] + }, + "Lovers Quarrel": { "x": 510, "y": 640, "w": 92, "ch": 5, "s": ["400"] }, + "Luckiest Guy": { "x": 616, "y": 640, "w": 156, "ch": 5, "s": ["400"] }, + "Lugrasimo": { "x": 786, "y": 640, "w": 144, "ch": 5, "s": ["400"] }, + "Lumanosimo": { "x": 944, "y": 640, "w": 185, "ch": 5, "s": ["400"] }, + "Lunasima": { "x": 0, "y": 680, "w": 119, "ch": 5, "s": ["400", "700"] }, + "Lusitana": { "x": 133, "y": 680, "w": 96, "ch": 5, "s": ["400", "700"] }, + "Lustria": { "x": 243, "y": 680, "w": 86, "ch": 5, "s": ["400"] }, + "Luxurious Roman": { "x": 343, "y": 680, "w": 201, "ch": 5, "s": ["400"] }, + "Luxurious Script": { "x": 558, "y": 680, "w": 128, "ch": 5, "s": ["400"] }, + "LXGW Marker Gothic": { + "x": 700, + "y": 680, + "w": 233, + "ch": 5, + "s": ["400"] + }, + "LXGW WenKai Mono TC": { + "x": 947, + "y": 680, + "w": 236, + "ch": 5, + "s": ["300", "400", "700"] + }, + "LXGW WenKai TC": { + "x": 0, + "y": 720, + "w": 206, + "ch": 5, + "s": ["300", "400", "700"] + }, + "M PLUS 1": { + "x": 220, + "y": 720, + "w": 120, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "M PLUS 1 Code": { + "x": 354, + "y": 720, + "w": 164, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "M PLUS 1p": { + "x": 532, + "y": 720, + "w": 129, + "ch": 5, + "s": ["100", "300", "400", "500", "700", "800", "900"] + }, + "M PLUS 2": { + "x": 675, + "y": 720, + "w": 120, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "M PLUS Code Latin": { + "x": 809, + "y": 720, + "w": 212, + "ch": 5, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "M PLUS Rounded 1c": { + "x": 0, + "y": 760, + "w": 225, + "ch": 5, + "s": ["100", "300", "400", "500", "700", "800", "900"] + }, + "Ma Shan Zheng": { "x": 239, "y": 760, "w": 126, "ch": 5, "s": ["400"] }, + "Macondo": { "x": 379, "y": 760, "w": 100, "ch": 5, "s": ["400"] }, + "Macondo Swash Caps": { + "x": 493, + "y": 760, + "w": 228, + "ch": 5, + "s": ["400"] + }, + "Mada": { + "x": 735, + "y": 760, + "w": 63, + "ch": 5, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Madimi One": { "x": 812, "y": 760, "w": 133, "ch": 5, "s": ["400"] }, + "Magra": { "x": 959, "y": 760, "w": 70, "ch": 5, "s": ["400", "700"] }, + "Maiden Orange": { "x": 1043, "y": 760, "w": 127, "ch": 5, "s": ["400"] }, + "Maitree": { + "x": 0, + "y": 0, + "w": 96, + "ch": 6, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Major Mono Display": { "x": 110, "y": 0, "w": 328, "ch": 6, "s": ["400"] }, + "Mako": { "x": 452, "y": 0, "w": 67, "ch": 6, "s": ["400"] }, + "Mali": { + "x": 533, + "y": 0, + "w": 56, + "ch": 6, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Mallanna": { "x": 603, "y": 0, "w": 96, "ch": 6, "s": ["400"] }, + "Maname": { "x": 713, "y": 0, "w": 100, "ch": 6, "s": ["400"] }, + "Mandali": { "x": 827, "y": 0, "w": 94, "ch": 6, "s": ["400"] }, + "Manjari": { + "x": 935, + "y": 0, + "w": 87, + "ch": 6, + "s": ["100", "400", "700"] + }, + "Manrope": { + "x": 1036, + "y": 0, + "w": 107, + "ch": 6, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Mansalva": { "x": 0, "y": 40, "w": 111, "ch": 6, "s": ["400"] }, + "Manuale": { + "x": 125, + "y": 40, + "w": 101, + "ch": 6, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Manufacturing Consent": { + "x": 240, + "y": 40, + "w": 214, + "ch": 6, + "s": ["400"] + }, + "Marcellus": { "x": 468, "y": 40, "w": 114, "ch": 6, "s": ["400"] }, + "Marcellus SC": { "x": 596, "y": 40, "w": 157, "ch": 6, "s": ["400"] }, + "Marck Script": { "x": 767, "y": 40, "w": 137, "ch": 6, "s": ["400"] }, + "Margarine": { "x": 918, "y": 40, "w": 119, "ch": 6, "s": ["400"] }, + "Marhey": { + "x": 1051, + "y": 40, + "w": 107, + "ch": 6, + "s": ["300", "400", "500", "600", "700"] + }, + "Markazi Text": { + "x": 0, + "y": 80, + "w": 113, + "ch": 6, + "s": ["400", "500", "600", "700"] + }, + "Marko One": { "x": 127, "y": 80, "w": 152, "ch": 6, "s": ["400"] }, + "Marmelad": { "x": 293, "y": 80, "w": 118, "ch": 6, "s": ["400"] }, + "Martel": { + "x": 425, + "y": 80, + "w": 88, + "ch": 6, + "s": ["200", "300", "400", "600", "700", "800", "900"] + }, + "Martel Sans": { + "x": 527, + "y": 80, + "w": 143, + "ch": 6, + "s": ["200", "300", "400", "600", "700", "800", "900"] + }, + "Martian Mono": { + "x": 684, + "y": 80, + "w": 210, + "ch": 6, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Marvel": { + "x": 908, + "y": 80, + "w": 64, + "ch": 6, + "s": ["400", "400i", "700", "700i"] + }, + "Matangi": { + "x": 986, + "y": 80, + "w": 99, + "ch": 6, + "s": ["300", "400", "500", "600", "700", "800", "900"] + }, + "Mate": { "x": 1099, "y": 80, "w": 61, "ch": 6, "s": ["400", "400i"] }, + "Mate SC": { "x": 0, "y": 120, "w": 99, "ch": 6, "s": ["400"] }, + "Matemasie": { "x": 113, "y": 120, "w": 145, "ch": 6, "s": ["400"] }, + "Material Icons": { "x": 272, "y": 120, "w": 344, "ch": 6, "s": ["400"] }, + "Material Icons Outlined": { + "x": 630, + "y": 120, + "w": 560, + "ch": 6, + "s": ["400"] + }, + "Material Icons Round": { + "x": 0, + "y": 160, + "w": 488, + "ch": 6, + "s": ["400"] + }, + "Material Icons Sharp": { + "x": 502, + "y": 160, + "w": 488, + "ch": 6, + "s": ["400"] + }, + "Material Icons Two Tone": { + "x": 0, + "y": 200, + "w": 560, + "ch": 6, + "s": ["400"] + }, + "Material Symbols": { + "x": 574, + "y": 200, + "w": 392, + "ch": 6, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "Material Symbols Outlined": { + "x": 0, + "y": 240, + "w": 608, + "ch": 6, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "Material Symbols Rounded": { + "x": 0, + "y": 280, + "w": 584, + "ch": 6, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "Material Symbols Sharp": { + "x": 598, + "y": 280, + "w": 536, + "ch": 6, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "Maven Pro": { + "x": 0, + "y": 320, + "w": 123, + "ch": 6, + "s": ["400", "500", "600", "700", "800", "900"] + }, + "McLaren": { "x": 137, "y": 320, "w": 118, "ch": 6, "s": ["400"] }, + "Mea Culpa": { "x": 269, "y": 320, "w": 102, "ch": 6, "s": ["400"] }, + "Meddon": { "x": 385, "y": 320, "w": 129, "ch": 6, "s": ["400"] }, + "MedievalSharp": { "x": 528, "y": 320, "w": 170, "ch": 6, "s": ["400"] }, + "Medula One": { "x": 712, "y": 320, "w": 78, "ch": 6, "s": ["400"] }, + "Meera Inimai": { "x": 804, "y": 320, "w": 138, "ch": 6, "s": ["400"] }, + "Megrim": { "x": 956, "y": 320, "w": 87, "ch": 6, "s": ["400"] }, + "Meie Script": { "x": 1057, "y": 320, "w": 133, "ch": 6, "s": ["400"] }, + "Menbere": { + "x": 0, + "y": 360, + "w": 112, + "ch": 6, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "Meow Script": { "x": 126, "y": 360, "w": 119, "ch": 6, "s": ["400"] }, + "Merienda": { + "x": 259, + "y": 360, + "w": 121, + "ch": 6, + "s": ["300", "400", "500", "600", "700", "800", "900"] + }, + "Merienda One": { "x": 394, "y": 360, "w": 177, "ch": 6, "s": ["400"] }, + "Merriweather": { + "x": 585, + "y": 360, + "w": 168, + "ch": 6, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Merriweather Sans": { + "x": 767, + "y": 360, + "w": 223, + "ch": 6, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Metal": { "x": 1004, "y": 360, "w": 58, "ch": 6, "s": ["400"] }, + "Metal Mania": { "x": 1076, "y": 360, "w": 121, "ch": 6, "s": ["400"] }, + "Metamorphous": { "x": 0, "y": 400, "w": 210, "ch": 6, "s": ["400"] }, + "Metrophobic": { "x": 224, "y": 400, "w": 149, "ch": 6, "s": ["400"] }, + "Michroma": { "x": 387, "y": 400, "w": 161, "ch": 6, "s": ["400"] }, + "Micro 5": { "x": 562, "y": 400, "w": 65, "ch": 6, "s": ["400"] }, + "Micro 5 Charted": { "x": 641, "y": 400, "w": 126, "ch": 6, "s": ["400"] }, + "Milonga": { "x": 781, "y": 400, "w": 99, "ch": 6, "s": ["400"] }, + "Miltonian": { "x": 894, "y": 400, "w": 130, "ch": 6, "s": ["400"] }, + "Miltonian Tattoo": { "x": 0, "y": 440, "w": 214, "ch": 6, "s": ["400"] }, + "Mina": { "x": 228, "y": 440, "w": 58, "ch": 6, "s": ["400", "700"] }, + "Mingzat": { "x": 300, "y": 440, "w": 99, "ch": 6, "s": ["400"] }, + "Miniver": { "x": 413, "y": 440, "w": 89, "ch": 6, "s": ["400"] }, + "Miriam Libre": { + "x": 516, + "y": 440, + "w": 161, + "ch": 6, + "s": ["400", "500", "600", "700"] + }, + "Mirza": { + "x": 691, + "y": 440, + "w": 60, + "ch": 6, + "s": ["400", "500", "600", "700"] + }, + "Miss Fajardose": { "x": 765, "y": 440, "w": 108, "ch": 6, "s": ["400"] }, + "Mitr": { + "x": 887, + "y": 440, + "w": 55, + "ch": 6, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Mochiy Pop One": { "x": 956, "y": 440, "w": 223, "ch": 6, "s": ["400"] }, + "Mochiy Pop P One": { "x": 0, "y": 480, "w": 248, "ch": 6, "s": ["400"] }, + "Modak": { "x": 262, "y": 480, "w": 79, "ch": 6, "s": ["400"] }, + "Modern Antiqua": { "x": 355, "y": 480, "w": 196, "ch": 6, "s": ["400"] }, + "Moderustic": { + "x": 565, + "y": 480, + "w": 135, + "ch": 6, + "s": ["300", "400", "500", "600", "700", "800"] + }, + "Mogra": { "x": 714, "y": 480, "w": 80, "ch": 6, "s": ["400"] }, + "Mohave": { + "x": 808, + "y": 480, + "w": 77, + "ch": 6, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Moirai One": { "x": 899, "y": 480, "w": 146, "ch": 6, "s": ["400"] }, + "Molengo": { "x": 1059, "y": 480, "w": 96, "ch": 6, "s": ["400"] }, + "Momo Signature": { "x": 0, "y": 520, "w": 227, "ch": 6, "s": ["400"] }, + "Momo Trust Display": { + "x": 241, + "y": 520, + "w": 249, + "ch": 6, + "s": ["400"] + }, + "Momo Trust Sans": { + "x": 504, + "y": 520, + "w": 207, + "ch": 6, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Mona Sans": { + "x": 725, + "y": 520, + "w": 136, + "ch": 6, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Monda": { + "x": 875, + "y": 520, + "w": 85, + "ch": 6, + "s": ["400", "500", "600", "700"] + }, + "Monofett": { "x": 974, "y": 520, "w": 119, "ch": 6, "s": ["400"] }, + "Monomakh": { "x": 0, "y": 560, "w": 138, "ch": 6, "s": ["400"] }, + "Monomaniac One": { "x": 152, "y": 560, "w": 156, "ch": 6, "s": ["400"] }, + "Monoton": { "x": 322, "y": 560, "w": 143, "ch": 6, "s": ["400"] }, + "Monsieur La Doulaise": { + "x": 479, + "y": 560, + "w": 215, + "ch": 6, + "s": ["400"] + }, + "Montaga": { "x": 708, "y": 560, "w": 102, "ch": 6, "s": ["400"] }, + "Montagu Slab": { + "x": 824, + "y": 560, + "w": 173, + "ch": 6, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "MonteCarlo": { "x": 1011, "y": 560, "w": 107, "ch": 6, "s": ["400"] }, + "Montez": { "x": 1132, "y": 560, "w": 66, "ch": 6, "s": ["400"] }, + "Montserrat": { + "x": 0, + "y": 600, + "w": 141, + "ch": 6, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Montserrat Alternates": { + "x": 155, + "y": 600, + "w": 282, + "ch": 6, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Montserrat Subrayada": { + "x": 451, + "y": 600, + "w": 347, + "ch": 6, + "s": ["400", "700"] + }, + "Montserrat Underline": { + "x": 812, + "y": 600, + "w": 266, + "ch": 6, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Moo Lah Lah": { "x": 0, "y": 640, "w": 137, "ch": 6, "s": ["400"] }, + "Mooli": { "x": 151, "y": 640, "w": 77, "ch": 6, "s": ["400"] }, + "Moon Dance": { "x": 242, "y": 640, "w": 106, "ch": 6, "s": ["400"] }, + "Moul": { "x": 362, "y": 640, "w": 82, "ch": 6, "s": ["400"] }, + "Moulpali": { "x": 458, "y": 640, "w": 90, "ch": 6, "s": ["400"] }, + "Mountains of Christmas": { + "x": 562, + "y": 640, + "w": 219, + "ch": 6, + "s": ["400", "700"] + }, + "Mouse Memoirs": { "x": 795, "y": 640, "w": 116, "ch": 6, "s": ["400"] }, + "Mozilla Headline": { + "x": 925, + "y": 640, + "w": 192, + "ch": 6, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Mozilla Text": { + "x": 0, + "y": 680, + "w": 148, + "ch": 6, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Mr Bedfort": { "x": 162, "y": 680, "w": 124, "ch": 6, "s": ["400"] }, + "Mr Dafoe": { "x": 300, "y": 680, "w": 92, "ch": 6, "s": ["400"] }, + "Mr De Haviland": { "x": 406, "y": 680, "w": 115, "ch": 6, "s": ["400"] }, + "Mrs Saint Delafield": { + "x": 535, + "y": 680, + "w": 159, + "ch": 6, + "s": ["400"] + }, + "Mrs Sheppards": { "x": 708, "y": 680, "w": 139, "ch": 6, "s": ["400"] }, + "Ms Madi": { "x": 861, "y": 680, "w": 89, "ch": 6, "s": ["400"] }, + "Mukta": { + "x": 964, + "y": 680, + "w": 73, + "ch": 6, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Mukta Mahee": { + "x": 1051, + "y": 680, + "w": 146, + "ch": 6, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Mukta Malar": { + "x": 0, + "y": 720, + "w": 137, + "ch": 6, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Mukta Vaani": { + "x": 151, + "y": 720, + "w": 133, + "ch": 6, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Mulish": { + "x": 298, + "y": 720, + "w": 80, + "ch": 6, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Murecho": { + "x": 392, + "y": 720, + "w": 102, + "ch": 6, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "MuseoModerno": { + "x": 508, + "y": 720, + "w": 199, + "ch": 6, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "My Soul": { "x": 721, "y": 720, "w": 107, "ch": 6, "s": ["400"] }, + "Mynerve": { "x": 842, "y": 720, "w": 104, "ch": 6, "s": ["400"] }, + "Mystery Quest": { "x": 960, "y": 720, "w": 152, "ch": 6, "s": ["400"] }, + "Nabla": { "x": 1126, "y": 720, "w": 72, "ch": 6, "s": ["400"] }, + "Namdhinggo": { + "x": 0, + "y": 760, + "w": 130, + "ch": 6, + "s": ["400", "500", "600", "700", "800"] + }, + "Nanum Brush Script": { + "x": 144, + "y": 760, + "w": 168, + "ch": 6, + "s": ["400"] + }, + "Nanum Gothic": { + "x": 326, + "y": 760, + "w": 171, + "ch": 6, + "s": ["400", "700", "800"] + }, + "Nanum Gothic Coding": { + "x": 511, + "y": 760, + "w": 236, + "ch": 6, + "s": ["400", "700"] + }, + "Nanum Myeongjo": { + "x": 761, + "y": 760, + "w": 201, + "ch": 6, + "s": ["400", "700", "800"] + }, + "Nanum Pen Script": { "x": 976, "y": 760, "w": 157, "ch": 6, "s": ["400"] }, + "Narnoor": { + "x": 0, + "y": 0, + "w": 90, + "ch": 7, + "s": ["400", "500", "600", "700", "800"] + }, + "Nata Sans": { + "x": 104, + "y": 0, + "w": 123, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "National Park": { + "x": 241, + "y": 0, + "w": 149, + "ch": 7, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Neonderthaw": { "x": 404, "y": 0, "w": 139, "ch": 7, "s": ["400"] }, + "Nerko One": { "x": 557, "y": 0, "w": 107, "ch": 7, "s": ["400"] }, + "Neucha": { "x": 678, "y": 0, "w": 72, "ch": 7, "s": ["400"] }, + "Neuton": { + "x": 764, + "y": 0, + "w": 76, + "ch": 7, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "700", + "700i", + "800", + "800i" + ] + }, + "New Amsterdam": { "x": 854, "y": 0, "w": 139, "ch": 7, "s": ["400"] }, + "New Rocker": { "x": 1007, "y": 0, "w": 133, "ch": 7, "s": ["400"] }, + "New Tegomin": { "x": 0, "y": 40, "w": 159, "ch": 7, "s": ["400"] }, + "News Cycle": { "x": 173, "y": 40, "w": 116, "ch": 7, "s": ["400", "700"] }, + "Newsreader": { + "x": 303, + "y": 40, + "w": 129, + "ch": 7, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Niconne": { "x": 446, "y": 40, "w": 79, "ch": 7, "s": ["400"] }, + "Niramit": { + "x": 539, + "y": 40, + "w": 84, + "ch": 7, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Nixie One": { "x": 637, "y": 40, "w": 123, "ch": 7, "s": ["400"] }, + "Nobile": { + "x": 774, + "y": 40, + "w": 82, + "ch": 7, + "s": ["400", "400i", "500", "500i", "700", "700i"] + }, + "Nokora": { + "x": 870, + "y": 40, + "w": 91, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Norican": { "x": 975, "y": 40, "w": 78, "ch": 7, "s": ["400"] }, + "Nosifer": { "x": 0, "y": 80, "w": 154, "ch": 7, "s": ["400"] }, + "Notable": { "x": 168, "y": 80, "w": 144, "ch": 7, "s": ["400"] }, + "Nothing You Could Do": { + "x": 326, + "y": 80, + "w": 248, + "ch": 7, + "s": ["400"] + }, + "Noticia Text": { + "x": 588, + "y": 80, + "w": 142, + "ch": 7, + "s": ["400", "400i", "700", "700i"] + }, + "Noto Kufi Arabic": { + "x": 744, + "y": 80, + "w": 196, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Music": { "x": 954, "y": 80, "w": 136, "ch": 7, "s": ["400"] }, + "Noto Naskh Arabic": { + "x": 0, + "y": 120, + "w": 221, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Nastaliq Urdu": { + "x": 235, + "y": 120, + "w": 222, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Rashi Hebrew": { + "x": 471, + "y": 120, + "w": 230, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans": { + "x": 715, + "y": 120, + "w": 124, + "ch": 7, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Noto Sans Adlam": { + "x": 853, + "y": 120, + "w": 202, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Adlam Unjoined": { + "x": 0, + "y": 160, + "w": 310, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Anatolian Hieroglyphs": { + "x": 324, + "y": 160, + "w": 380, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Arabic": { + "x": 718, + "y": 160, + "w": 201, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Armenian": { + "x": 933, + "y": 160, + "w": 240, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Avestan": { "x": 0, "y": 200, "w": 219, "ch": 7, "s": ["400"] }, + "Noto Sans Balinese": { + "x": 233, + "y": 200, + "w": 225, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Bamum": { + "x": 472, + "y": 200, + "w": 219, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Bassa Vah": { + "x": 705, + "y": 200, + "w": 250, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Batak": { "x": 969, "y": 200, "w": 194, "ch": 7, "s": ["400"] }, + "Noto Sans Bengali": { + "x": 0, + "y": 240, + "w": 214, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Bhaiksuki": { + "x": 228, + "y": 240, + "w": 236, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Brahmi": { "x": 478, "y": 240, "w": 212, "ch": 7, "s": ["400"] }, + "Noto Sans Buginese": { + "x": 704, + "y": 240, + "w": 235, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Buhid": { "x": 953, "y": 240, "w": 196, "ch": 7, "s": ["400"] }, + "Noto Sans Canadian Aboriginal": { + "x": 0, + "y": 280, + "w": 361, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Carian": { "x": 375, "y": 280, "w": 203, "ch": 7, "s": ["400"] }, + "Noto Sans Caucasian Albanian": { + "x": 592, + "y": 280, + "w": 353, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Chakma": { "x": 959, "y": 280, "w": 222, "ch": 7, "s": ["400"] }, + "Noto Sans Cham": { + "x": 0, + "y": 320, + "w": 196, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Cherokee": { + "x": 210, + "y": 320, + "w": 238, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Chorasmian": { + "x": 462, + "y": 320, + "w": 268, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Coptic": { "x": 744, "y": 320, "w": 201, "ch": 7, "s": ["400"] }, + "Noto Sans Cuneiform": { + "x": 0, + "y": 360, + "w": 250, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Cypriot": { + "x": 264, + "y": 360, + "w": 211, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Cypro Minoan": { + "x": 489, + "y": 360, + "w": 288, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Deseret": { + "x": 791, + "y": 360, + "w": 218, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Devanagari": { + "x": 0, + "y": 400, + "w": 259, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Display": { + "x": 273, + "y": 400, + "w": 200, + "ch": 7, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Noto Sans Duployan": { + "x": 487, + "y": 400, + "w": 238, + "ch": 7, + "s": ["400", "700"] + }, + "Noto Sans Egyptian Hieroglyphs": { + "x": 739, + "y": 400, + "w": 369, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Elbasan": { "x": 0, "y": 440, "w": 217, "ch": 7, "s": ["400"] }, + "Noto Sans Elymaic": { + "x": 231, + "y": 440, + "w": 222, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Ethiopic": { + "x": 467, + "y": 440, + "w": 220, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Georgian": { + "x": 701, + "y": 440, + "w": 234, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Glagolitic": { + "x": 949, + "y": 440, + "w": 235, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Gothic": { "x": 0, "y": 480, "w": 203, "ch": 7, "s": ["400"] }, + "Noto Sans Grantha": { + "x": 217, + "y": 480, + "w": 222, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Gujarati": { + "x": 453, + "y": 480, + "w": 220, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Gunjala Gondi": { + "x": 687, + "y": 480, + "w": 291, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Gurmukhi": { + "x": 0, + "y": 520, + "w": 243, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Hanifi Rohingya": { + "x": 257, + "y": 520, + "w": 309, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Hanunoo": { + "x": 580, + "y": 520, + "w": 235, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Hatran": { "x": 829, "y": 520, "w": 208, "ch": 7, "s": ["400"] }, + "Noto Sans Hebrew": { + "x": 0, + "y": 560, + "w": 217, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans HK": { + "x": 231, + "y": 560, + "w": 160, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Imperial Aramaic": { + "x": 405, + "y": 560, + "w": 323, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Indic Siyaq Numbers": { + "x": 742, + "y": 560, + "w": 351, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Inscriptional Pahlavi": { + "x": 0, + "y": 600, + "w": 358, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Inscriptional Parthian": { + "x": 372, + "y": 600, + "w": 373, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Javanese": { + "x": 759, + "y": 600, + "w": 229, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans JP": { + "x": 1002, + "y": 600, + "w": 155, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Kaithi": { "x": 0, "y": 640, "w": 195, "ch": 7, "s": ["400"] }, + "Noto Sans Kannada": { + "x": 209, + "y": 640, + "w": 231, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Kawi": { + "x": 454, + "y": 640, + "w": 180, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Kayah Li": { + "x": 648, + "y": 640, + "w": 224, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Kharoshthi": { + "x": 886, + "y": 640, + "w": 253, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Khmer": { + "x": 0, + "y": 680, + "w": 205, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Khojki": { "x": 219, "y": 680, "w": 199, "ch": 7, "s": ["400"] }, + "Noto Sans Khudawadi": { + "x": 432, + "y": 680, + "w": 256, + "ch": 7, + "s": ["400"] + }, + "Noto Sans KR": { + "x": 702, + "y": 680, + "w": 158, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Lao": { + "x": 874, + "y": 680, + "w": 170, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Lao Looped": { + "x": 0, + "y": 720, + "w": 261, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Lepcha": { "x": 275, "y": 720, "w": 211, "ch": 7, "s": ["400"] }, + "Noto Sans Limbu": { "x": 500, "y": 720, "w": 201, "ch": 7, "s": ["400"] }, + "Noto Sans Linear A": { + "x": 715, + "y": 720, + "w": 222, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Linear B": { + "x": 951, + "y": 720, + "w": 222, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Lisu": { + "x": 0, + "y": 760, + "w": 175, + "ch": 7, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Lydian": { "x": 189, "y": 760, "w": 204, "ch": 7, "s": ["400"] }, + "Noto Sans Mahajani": { + "x": 407, + "y": 760, + "w": 234, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Malayalam": { + "x": 655, + "y": 760, + "w": 255, + "ch": 7, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Mandaic": { + "x": 924, + "y": 760, + "w": 226, + "ch": 7, + "s": ["400"] + }, + "Noto Sans Manichaean": { "x": 0, "y": 0, "w": 268, "ch": 8, "s": ["400"] }, + "Noto Sans Marchen": { "x": 282, "y": 0, "w": 227, "ch": 8, "s": ["400"] }, + "Noto Sans Masaram Gondi": { + "x": 523, + "y": 0, + "w": 318, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Math": { "x": 855, "y": 0, "w": 189, "ch": 8, "s": ["400"] }, + "Noto Sans Mayan Numerals": { + "x": 0, + "y": 40, + "w": 322, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Medefaidrin": { + "x": 336, + "y": 40, + "w": 267, + "ch": 8, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Meetei Mayek": { + "x": 617, + "y": 40, + "w": 289, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Mende Kikakui": { + "x": 0, + "y": 80, + "w": 296, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Meroitic": { + "x": 310, + "y": 80, + "w": 222, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Miao": { "x": 546, "y": 80, "w": 186, "ch": 8, "s": ["400"] }, + "Noto Sans Modi": { "x": 746, "y": 80, "w": 187, "ch": 8, "s": ["400"] }, + "Noto Sans Mongolian": { + "x": 947, + "y": 80, + "w": 251, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Mono": { + "x": 0, + "y": 120, + "w": 210, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Mro": { "x": 224, "y": 120, "w": 176, "ch": 8, "s": ["400"] }, + "Noto Sans Multani": { + "x": 414, + "y": 120, + "w": 216, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Myanmar": { + "x": 644, + "y": 120, + "w": 238, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Nabataean": { + "x": 896, + "y": 120, + "w": 254, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Nag Mundari": { + "x": 0, + "y": 160, + "w": 274, + "ch": 8, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Nandinagari": { + "x": 288, + "y": 160, + "w": 272, + "ch": 8, + "s": ["400"] + }, + "Noto Sans New Tai Lue": { + "x": 574, + "y": 160, + "w": 265, + "ch": 8, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Newa": { "x": 853, "y": 160, "w": 194, "ch": 8, "s": ["400"] }, + "Noto Sans NKo": { "x": 0, "y": 200, "w": 178, "ch": 8, "s": ["400"] }, + "Noto Sans NKo Unjoined": { + "x": 192, + "y": 200, + "w": 286, + "ch": 8, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Nushu": { "x": 492, "y": 200, "w": 204, "ch": 8, "s": ["400"] }, + "Noto Sans Ogham": { "x": 710, "y": 200, "w": 214, "ch": 8, "s": ["400"] }, + "Noto Sans Ol Chiki": { + "x": 938, + "y": 200, + "w": 216, + "ch": 8, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Old Hungarian": { + "x": 0, + "y": 240, + "w": 296, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Old Italic": { + "x": 310, + "y": 240, + "w": 230, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Old North Arabian": { + "x": 554, + "y": 240, + "w": 336, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Old Permic": { + "x": 904, + "y": 240, + "w": 253, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Old Persian": { + "x": 0, + "y": 280, + "w": 260, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Old Sogdian": { + "x": 274, + "y": 280, + "w": 260, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Old South Arabian": { + "x": 548, + "y": 280, + "w": 336, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Old Turkic": { + "x": 898, + "y": 280, + "w": 243, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Oriya": { + "x": 0, + "y": 320, + "w": 190, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Osage": { "x": 204, "y": 320, "w": 202, "ch": 8, "s": ["400"] }, + "Noto Sans Osmanya": { + "x": 420, + "y": 320, + "w": 238, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Pahawh Hmong": { + "x": 672, + "y": 320, + "w": 310, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Palmyrene": { + "x": 0, + "y": 360, + "w": 251, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Pau Cin Hau": { + "x": 265, + "y": 360, + "w": 269, + "ch": 8, + "s": ["400"] + }, + "Noto Sans PhagsPa": { + "x": 548, + "y": 360, + "w": 227, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Phoenician": { + "x": 789, + "y": 360, + "w": 254, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Psalter Pahlavi": { + "x": 0, + "y": 400, + "w": 295, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Rejang": { "x": 309, "y": 400, "w": 208, "ch": 8, "s": ["400"] }, + "Noto Sans Runic": { "x": 531, "y": 400, "w": 192, "ch": 8, "s": ["400"] }, + "Noto Sans Samaritan": { + "x": 737, + "y": 400, + "w": 245, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Saurashtra": { + "x": 0, + "y": 440, + "w": 254, + "ch": 8, + "s": ["400"] + }, + "Noto Sans SC": { + "x": 268, + "y": 440, + "w": 157, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Sharada": { + "x": 439, + "y": 440, + "w": 222, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Shavian": { + "x": 675, + "y": 440, + "w": 218, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Siddham": { + "x": 907, + "y": 440, + "w": 227, + "ch": 8, + "s": ["400"] + }, + "Noto Sans SignWriting": { + "x": 0, + "y": 480, + "w": 297, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Sinhala": { + "x": 311, + "y": 480, + "w": 224, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Sogdian": { + "x": 549, + "y": 480, + "w": 216, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Sora Sompeng": { + "x": 779, + "y": 480, + "w": 295, + "ch": 8, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Soyombo": { "x": 0, "y": 520, "w": 236, "ch": 8, "s": ["400"] }, + "Noto Sans Sundanese": { + "x": 250, + "y": 520, + "w": 254, + "ch": 8, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Sunuwar": { + "x": 518, + "y": 520, + "w": 237, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Syloti Nagri": { + "x": 769, + "y": 520, + "w": 260, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Symbols": { + "x": 0, + "y": 560, + "w": 225, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Symbols 2": { + "x": 239, + "y": 560, + "w": 257, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Syriac": { + "x": 510, + "y": 560, + "w": 196, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Syriac Eastern": { + "x": 720, + "y": 560, + "w": 288, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Syriac Western": { + "x": 0, + "y": 600, + "w": 296, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Tagalog": { + "x": 310, + "y": 600, + "w": 218, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Tagbanwa": { + "x": 542, + "y": 600, + "w": 247, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Tai Le": { "x": 803, "y": 600, "w": 193, "ch": 8, "s": ["400"] }, + "Noto Sans Tai Tham": { + "x": 0, + "y": 640, + "w": 243, + "ch": 8, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Tai Viet": { + "x": 257, + "y": 640, + "w": 210, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Takri": { "x": 481, "y": 640, "w": 184, "ch": 8, "s": ["400"] }, + "Noto Sans Tamil": { + "x": 679, + "y": 640, + "w": 190, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Tamil Supplement": { + "x": 0, + "y": 680, + "w": 334, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Tangsa": { + "x": 348, + "y": 680, + "w": 209, + "ch": 8, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans TC": { + "x": 571, + "y": 680, + "w": 156, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Telugu": { + "x": 741, + "y": 680, + "w": 207, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Thaana": { + "x": 962, + "y": 680, + "w": 213, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Thai": { + "x": 0, + "y": 720, + "w": 178, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Thai Looped": { + "x": 192, + "y": 720, + "w": 269, + "ch": 8, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Sans Tifinagh": { + "x": 475, + "y": 720, + "w": 222, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Tirhuta": { + "x": 711, + "y": 720, + "w": 211, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Ugaritic": { + "x": 936, + "y": 720, + "w": 218, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Vai": { "x": 0, "y": 760, "w": 164, "ch": 8, "s": ["400"] }, + "Noto Sans Vithkuqi": { + "x": 178, + "y": 760, + "w": 223, + "ch": 8, + "s": ["400", "500", "600", "700"] + }, + "Noto Sans Wancho": { "x": 415, "y": 760, "w": 228, "ch": 8, "s": ["400"] }, + "Noto Sans Warang Citi": { + "x": 657, + "y": 760, + "w": 264, + "ch": 8, + "s": ["400"] + }, + "Noto Sans Yi": { "x": 935, "y": 760, "w": 149, "ch": 8, "s": ["400"] }, + "Noto Sans Zanabazar Square": { + "x": 0, + "y": 0, + "w": 334, + "ch": 9, + "s": ["400"] + }, + "Noto Serif": { + "x": 348, + "y": 0, + "w": 123, + "ch": 9, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Noto Serif Ahom": { "x": 485, "y": 0, "w": 196, "ch": 9, "s": ["400"] }, + "Noto Serif Armenian": { + "x": 695, + "y": 0, + "w": 245, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Balinese": { + "x": 954, + "y": 0, + "w": 225, + "ch": 9, + "s": ["400"] + }, + "Noto Serif Bengali": { + "x": 0, + "y": 40, + "w": 215, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Devanagari": { + "x": 229, + "y": 40, + "w": 261, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Display": { + "x": 504, + "y": 40, + "w": 212, + "ch": 9, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Noto Serif Dives Akuru": { + "x": 730, + "y": 40, + "w": 274, + "ch": 9, + "s": ["400"] + }, + "Noto Serif Dogra": { "x": 0, "y": 80, "w": 197, "ch": 9, "s": ["400"] }, + "Noto Serif Ethiopic": { + "x": 211, + "y": 80, + "w": 224, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Georgian": { + "x": 449, + "y": 80, + "w": 234, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Grantha": { + "x": 697, + "y": 80, + "w": 223, + "ch": 9, + "s": ["400"] + }, + "Noto Serif Gujarati": { + "x": 934, + "y": 80, + "w": 225, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Gurmukhi": { + "x": 0, + "y": 120, + "w": 248, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Hebrew": { + "x": 262, + "y": 120, + "w": 221, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Hentaigana": { + "x": 497, + "y": 120, + "w": 269, + "ch": 9, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif HK": { + "x": 780, + "y": 120, + "w": 170, + "ch": 9, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif JP": { + "x": 964, + "y": 120, + "w": 157, + "ch": 9, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Kannada": { + "x": 0, + "y": 160, + "w": 231, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Khitan Small Script": { + "x": 245, + "y": 160, + "w": 383, + "ch": 9, + "s": ["400"] + }, + "Noto Serif Khmer": { + "x": 642, + "y": 160, + "w": 208, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Khojki": { + "x": 864, + "y": 160, + "w": 204, + "ch": 9, + "s": ["400", "500", "600", "700"] + }, + "Noto Serif KR": { + "x": 0, + "y": 200, + "w": 167, + "ch": 9, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Lao": { + "x": 181, + "y": 200, + "w": 171, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Makasar": { + "x": 366, + "y": 200, + "w": 226, + "ch": 9, + "s": ["400"] + }, + "Noto Serif Malayalam": { + "x": 606, + "y": 200, + "w": 259, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif NP Hmong": { + "x": 879, + "y": 200, + "w": 252, + "ch": 9, + "s": ["400", "500", "600", "700"] + }, + "Noto Serif Old Uyghur": { + "x": 0, + "y": 240, + "w": 257, + "ch": 9, + "s": ["400"] + }, + "Noto Serif Oriya": { + "x": 271, + "y": 240, + "w": 200, + "ch": 9, + "s": ["400", "500", "600", "700"] + }, + "Noto Serif Ottoman Siyaq": { + "x": 485, + "y": 240, + "w": 298, + "ch": 9, + "s": ["400"] + }, + "Noto Serif SC": { + "x": 797, + "y": 240, + "w": 162, + "ch": 9, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Sinhala": { + "x": 973, + "y": 240, + "w": 227, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Tamil": { + "x": 0, + "y": 280, + "w": 194, + "ch": 9, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Noto Serif Tangut": { + "x": 208, + "y": 280, + "w": 220, + "ch": 9, + "s": ["400"] + }, + "Noto Serif TC": { + "x": 442, + "y": 280, + "w": 164, + "ch": 9, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Telugu": { + "x": 620, + "y": 280, + "w": 207, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Thai": { + "x": 841, + "y": 280, + "w": 180, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Tibetan": { + "x": 0, + "y": 320, + "w": 217, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Noto Serif Todhri": { + "x": 231, + "y": 320, + "w": 201, + "ch": 9, + "s": ["400"] + }, + "Noto Serif Toto": { + "x": 446, + "y": 320, + "w": 179, + "ch": 9, + "s": ["400", "500", "600", "700"] + }, + "Noto Serif Vithkuqi": { + "x": 639, + "y": 320, + "w": 228, + "ch": 9, + "s": ["400", "500", "600", "700"] + }, + "Noto Serif Yezidi": { + "x": 881, + "y": 320, + "w": 198, + "ch": 9, + "s": ["400", "500", "600", "700"] + }, + "Noto Traditional Nushu": { + "x": 0, + "y": 360, + "w": 289, + "ch": 9, + "s": ["300", "400", "500", "600", "700"] + }, + "Noto Znamenny Musical Notation": { + "x": 303, + "y": 360, + "w": 388, + "ch": 9, + "s": ["400"] + }, + "Nova Cut": { "x": 705, "y": 360, "w": 108, "ch": 9, "s": ["400"] }, + "Nova Flat": { "x": 827, "y": 360, "w": 112, "ch": 9, "s": ["400"] }, + "Nova Mono": { "x": 953, "y": 360, "w": 130, "ch": 9, "s": ["400"] }, + "Nova Oval": { "x": 0, "y": 400, "w": 123, "ch": 9, "s": ["400"] }, + "Nova Round": { "x": 137, "y": 400, "w": 140, "ch": 9, "s": ["400"] }, + "Nova Script": { "x": 291, "y": 400, "w": 140, "ch": 9, "s": ["400"] }, + "Nova Slim": { "x": 445, "y": 400, "w": 121, "ch": 9, "s": ["400"] }, + "Nova Square": { "x": 580, "y": 400, "w": 148, "ch": 9, "s": ["400"] }, + "NTR": { "x": 742, "y": 400, "w": 49, "ch": 9, "s": ["400"] }, + "Numans": { "x": 805, "y": 400, "w": 113, "ch": 9, "s": ["400"] }, + "Nunito": { + "x": 932, + "y": 400, + "w": 81, + "ch": 9, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Nunito Sans": { + "x": 1027, + "y": 400, + "w": 139, + "ch": 9, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Nuosu SIL": { "x": 0, "y": 440, "w": 117, "ch": 9, "s": ["400"] }, + "Odibee Sans": { "x": 131, "y": 440, "w": 95, "ch": 9, "s": ["400"] }, + "Odor Mean Chey": { "x": 240, "y": 440, "w": 184, "ch": 9, "s": ["400"] }, + "Offside": { "x": 438, "y": 440, "w": 96, "ch": 9, "s": ["400"] }, + "Oi": { "x": 548, "y": 440, "w": 47, "ch": 9, "s": ["400"] }, + "Ojuju": { + "x": 609, + "y": 440, + "w": 62, + "ch": 9, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Old Standard TT": { + "x": 685, + "y": 440, + "w": 187, + "ch": 9, + "s": ["400", "400i", "700", "700i"] + }, + "Oldenburg": { "x": 886, "y": 440, "w": 136, "ch": 9, "s": ["400"] }, + "Ole": { "x": 1036, "y": 440, "w": 33, "ch": 9, "s": ["400"] }, + "Oleo Script": { + "x": 1083, + "y": 440, + "w": 109, + "ch": 9, + "s": ["400", "700"] + }, + "Oleo Script Swash Caps": { + "x": 0, + "y": 480, + "w": 227, + "ch": 9, + "s": ["400", "700"] + }, + "Onest": { + "x": 241, + "y": 480, + "w": 76, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Oooh Baby": { "x": 331, "y": 480, "w": 109, "ch": 9, "s": ["400"] }, + "Open Sans": { + "x": 454, + "y": 480, + "w": 129, + "ch": 9, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Oranienbaum": { "x": 597, "y": 480, "w": 137, "ch": 9, "s": ["400"] }, + "Orbit": { "x": 748, "y": 480, "w": 80, "ch": 9, "s": ["400"] }, + "Orbitron": { + "x": 842, + "y": 480, + "w": 117, + "ch": 9, + "s": ["400", "500", "600", "700", "800", "900"] + }, + "Oregano": { "x": 973, "y": 480, "w": 78, "ch": 9, "s": ["400", "400i"] }, + "Orelega One": { "x": 0, "y": 520, "w": 138, "ch": 9, "s": ["400"] }, + "Orienta": { "x": 152, "y": 520, "w": 94, "ch": 9, "s": ["400"] }, + "Original Surfer": { "x": 260, "y": 520, "w": 175, "ch": 9, "s": ["400"] }, + "Oswald": { + "x": 449, + "y": 520, + "w": 70, + "ch": 9, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Outfit": { + "x": 533, + "y": 520, + "w": 71, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Over the Rainbow": { "x": 618, "y": 520, "w": 189, "ch": 9, "s": ["400"] }, + "Overlock": { + "x": 821, + "y": 520, + "w": 95, + "ch": 9, + "s": ["400", "400i", "700", "700i", "900", "900i"] + }, + "Overlock SC": { "x": 930, "y": 520, "w": 139, "ch": 9, "s": ["400"] }, + "Overpass": { + "x": 1083, + "y": 520, + "w": 109, + "ch": 9, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Overpass Mono": { + "x": 0, + "y": 560, + "w": 201, + "ch": 9, + "s": ["300", "400", "500", "600", "700"] + }, + "Ovo": { "x": 215, "y": 560, "w": 52, "ch": 9, "s": ["400"] }, + "Oxanium": { + "x": 281, + "y": 560, + "w": 106, + "ch": 9, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Oxygen": { + "x": 401, + "y": 560, + "w": 92, + "ch": 9, + "s": ["300", "400", "700"] + }, + "Oxygen Mono": { "x": 507, "y": 560, "w": 167, "ch": 9, "s": ["400"] }, + "Pacifico": { "x": 688, "y": 560, "w": 87, "ch": 9, "s": ["400"] }, + "Padauk": { "x": 789, "y": 560, "w": 79, "ch": 9, "s": ["400", "700"] }, + "Padyakke Expanded One": { + "x": 0, + "y": 600, + "w": 415, + "ch": 9, + "s": ["400"] + }, + "Palanquin": { + "x": 429, + "y": 600, + "w": 111, + "ch": 9, + "s": ["100", "200", "300", "400", "500", "600", "700"] + }, + "Palanquin Dark": { + "x": 554, + "y": 600, + "w": 172, + "ch": 9, + "s": ["400", "500", "600", "700"] + }, + "Palette Mosaic": { "x": 740, "y": 600, "w": 212, "ch": 9, "s": ["400"] }, + "Pangolin": { "x": 966, "y": 600, "w": 93, "ch": 9, "s": ["400"] }, + "Paprika": { "x": 1073, "y": 600, "w": 105, "ch": 9, "s": ["400"] }, + "Parastoo": { + "x": 0, + "y": 640, + "w": 82, + "ch": 9, + "s": ["400", "500", "600", "700"] + }, + "Parisienne": { "x": 96, "y": 640, "w": 103, "ch": 9, "s": ["400"] }, + "Parkinsans": { + "x": 213, + "y": 640, + "w": 139, + "ch": 9, + "s": ["300", "400", "500", "600", "700", "800"] + }, + "Passero One": { "x": 366, "y": 640, "w": 125, "ch": 9, "s": ["400"] }, + "Passion One": { + "x": 505, + "y": 640, + "w": 114, + "ch": 9, + "s": ["400", "700", "900"] + }, + "Passions Conflict": { + "x": 633, + "y": 640, + "w": 109, + "ch": 9, + "s": ["400"] + }, + "Pathway Extreme": { + "x": 756, + "y": 640, + "w": 214, + "ch": 9, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Pathway Gothic One": { + "x": 984, + "y": 640, + "w": 154, + "ch": 9, + "s": ["400"] + }, + "Patrick Hand": { "x": 0, "y": 680, "w": 118, "ch": 9, "s": ["400"] }, + "Patrick Hand SC": { "x": 132, "y": 680, "w": 154, "ch": 9, "s": ["400"] }, + "Pattaya": { "x": 300, "y": 680, "w": 85, "ch": 9, "s": ["400"] }, + "Patua One": { "x": 399, "y": 680, "w": 117, "ch": 9, "s": ["400"] }, + "Pavanam": { "x": 530, "y": 680, "w": 97, "ch": 9, "s": ["400"] }, + "Paytone One": { "x": 641, "y": 680, "w": 164, "ch": 9, "s": ["400"] }, + "Peddana": { "x": 819, "y": 680, "w": 67, "ch": 9, "s": ["400"] }, + "Peralta": { "x": 900, "y": 680, "w": 108, "ch": 9, "s": ["400"] }, + "Permanent Marker": { "x": 0, "y": 720, "w": 234, "ch": 9, "s": ["400"] }, + "Petemoss": { "x": 248, "y": 720, "w": 60, "ch": 9, "s": ["400"] }, + "Petit Formal Script": { + "x": 322, + "y": 720, + "w": 255, + "ch": 9, + "s": ["400"] + }, + "Petrona": { + "x": 591, + "y": 720, + "w": 89, + "ch": 9, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Philosopher": { + "x": 694, + "y": 720, + "w": 129, + "ch": 9, + "s": ["400", "400i", "700", "700i"] + }, + "Phudu": { + "x": 837, + "y": 720, + "w": 76, + "ch": 9, + "s": ["300", "400", "500", "600", "700", "800", "900"] + }, + "Piazzolla": { + "x": 927, + "y": 720, + "w": 107, + "ch": 9, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Piedra": { "x": 1048, "y": 720, "w": 68, "ch": 9, "s": ["400"] }, + "Pinyon Script": { "x": 0, "y": 760, "w": 128, "ch": 9, "s": ["400"] }, + "Pirata One": { "x": 142, "y": 760, "w": 92, "ch": 9, "s": ["400"] }, + "Pixelify Sans": { + "x": 248, + "y": 760, + "w": 153, + "ch": 9, + "s": ["400", "500", "600", "700"] + }, + "Plaster": { "x": 415, "y": 760, "w": 126, "ch": 9, "s": ["400"] }, + "Platypi": { + "x": 555, + "y": 760, + "w": 90, + "ch": 9, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Play": { "x": 659, "y": 760, "w": 52, "ch": 9, "s": ["400", "700"] }, + "Playball": { "x": 725, "y": 760, "w": 83, "ch": 9, "s": ["400"] }, + "Playfair": { + "x": 822, + "y": 760, + "w": 90, + "ch": 9, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Playfair Display": { + "x": 926, + "y": 760, + "w": 176, + "ch": 9, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Playfair Display SC": { + "x": 0, + "y": 0, + "w": 247, + "ch": 10, + "s": ["400", "400i", "700", "700i", "900", "900i"] + }, + "Playpen Sans": { + "x": 261, + "y": 0, + "w": 161, + "ch": 10, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Playpen Sans Arabic": { + "x": 436, + "y": 0, + "w": 247, + "ch": 10, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Playpen Sans Deva": { + "x": 697, + "y": 0, + "w": 227, + "ch": 10, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Playpen Sans Hebrew": { + "x": 938, + "y": 0, + "w": 255, + "ch": 10, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Playpen Sans Thai": { + "x": 0, + "y": 40, + "w": 220, + "ch": 10, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Playwrite AR": { + "x": 234, + "y": 40, + "w": 199, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite AR Guides": { + "x": 447, + "y": 40, + "w": 294, + "ch": 10, + "s": ["400"] + }, + "Playwrite AT": { + "x": 755, + "y": 40, + "w": 180, + "ch": 10, + "s": ["100", "100i", "200", "200i", "300", "300i", "400", "400i"] + }, + "Playwrite AT Guides": { + "x": 0, + "y": 80, + "w": 273, + "ch": 10, + "s": ["400", "400i"] + }, + "Playwrite AU NSW": { + "x": 287, + "y": 80, + "w": 257, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite AU NSW Guides": { + "x": 558, + "y": 80, + "w": 350, + "ch": 10, + "s": ["400"] + }, + "Playwrite AU QLD": { + "x": 922, + "y": 80, + "w": 249, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite AU QLD Guides": { + "x": 0, + "y": 120, + "w": 344, + "ch": 10, + "s": ["400"] + }, + "Playwrite AU SA": { + "x": 358, + "y": 120, + "w": 217, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite AU SA Guides": { + "x": 589, + "y": 120, + "w": 312, + "ch": 10, + "s": ["400"] + }, + "Playwrite AU TAS": { + "x": 915, + "y": 120, + "w": 235, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite AU TAS Guides": { + "x": 0, + "y": 160, + "w": 329, + "ch": 10, + "s": ["400"] + }, + "Playwrite AU VIC": { + "x": 343, + "y": 160, + "w": 234, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite AU VIC Guides": { + "x": 591, + "y": 160, + "w": 327, + "ch": 10, + "s": ["400"] + }, + "Playwrite BE VLG": { + "x": 932, + "y": 160, + "w": 266, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite BE VLG Guides": { + "x": 0, + "y": 200, + "w": 359, + "ch": 10, + "s": ["400"] + }, + "Playwrite BE WAL": { + "x": 373, + "y": 200, + "w": 319, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite BE WAL Guides": { + "x": 706, + "y": 200, + "w": 420, + "ch": 10, + "s": ["400"] + }, + "Playwrite BR": { + "x": 0, + "y": 240, + "w": 201, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite BR Guides": { + "x": 215, + "y": 240, + "w": 298, + "ch": 10, + "s": ["400"] + }, + "Playwrite CA": { + "x": 527, + "y": 240, + "w": 191, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite CA Guides": { + "x": 732, + "y": 240, + "w": 288, + "ch": 10, + "s": ["400"] + }, + "Playwrite CL": { + "x": 0, + "y": 280, + "w": 194, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite CL Guides": { + "x": 208, + "y": 280, + "w": 290, + "ch": 10, + "s": ["400"] + }, + "Playwrite CO": { + "x": 512, + "y": 280, + "w": 189, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite CO Guides": { + "x": 715, + "y": 280, + "w": 287, + "ch": 10, + "s": ["400"] + }, + "Playwrite CU": { + "x": 0, + "y": 320, + "w": 192, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite CU Guides": { + "x": 206, + "y": 320, + "w": 289, + "ch": 10, + "s": ["400"] + }, + "Playwrite CZ": { + "x": 509, + "y": 320, + "w": 192, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite CZ Guides": { + "x": 715, + "y": 320, + "w": 287, + "ch": 10, + "s": ["400"] + }, + "Playwrite DE Grund": { + "x": 0, + "y": 360, + "w": 251, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite DE Grund Guides": { + "x": 265, + "y": 360, + "w": 342, + "ch": 10, + "s": ["400"] + }, + "Playwrite DE LA": { + "x": 621, + "y": 360, + "w": 232, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite DE LA Guides": { + "x": 867, + "y": 360, + "w": 325, + "ch": 10, + "s": ["400"] + }, + "Playwrite DE SAS": { + "x": 0, + "y": 400, + "w": 241, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite DE SAS Guides": { + "x": 255, + "y": 400, + "w": 333, + "ch": 10, + "s": ["400"] + }, + "Playwrite DE VA": { + "x": 602, + "y": 400, + "w": 225, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite DE VA Guides": { + "x": 841, + "y": 400, + "w": 320, + "ch": 10, + "s": ["400"] + }, + "Playwrite DK Loopet": { + "x": 0, + "y": 440, + "w": 254, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite DK Loopet Guides": { + "x": 268, + "y": 440, + "w": 345, + "ch": 10, + "s": ["400"] + }, + "Playwrite DK Uloopet": { + "x": 627, + "y": 440, + "w": 262, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite DK Uloopet Guides": { + "x": 0, + "y": 480, + "w": 353, + "ch": 10, + "s": ["400"] + }, + "Playwrite ES": { + "x": 367, + "y": 480, + "w": 176, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite ES Deco": { + "x": 557, + "y": 480, + "w": 256, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite ES Deco Guides": { + "x": 827, + "y": 480, + "w": 350, + "ch": 10, + "s": ["400"] + }, + "Playwrite ES Guides": { + "x": 0, + "y": 520, + "w": 268, + "ch": 10, + "s": ["400"] + }, + "Playwrite FR Moderne": { + "x": 282, + "y": 520, + "w": 287, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite FR Moderne Guides": { + "x": 583, + "y": 520, + "w": 383, + "ch": 10, + "s": ["400"] + }, + "Playwrite FR Trad": { + "x": 0, + "y": 560, + "w": 281, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite FR Trad Guides": { + "x": 295, + "y": 560, + "w": 384, + "ch": 10, + "s": ["400"] + }, + "Playwrite GB J": { + "x": 693, + "y": 560, + "w": 191, + "ch": 10, + "s": ["100", "100i", "200", "200i", "300", "300i", "400", "400i"] + }, + "Playwrite GB J Guides": { + "x": 898, + "y": 560, + "w": 284, + "ch": 10, + "s": ["400", "400i"] + }, + "Playwrite GB S": { + "x": 0, + "y": 600, + "w": 190, + "ch": 10, + "s": ["100", "100i", "200", "200i", "300", "300i", "400", "400i"] + }, + "Playwrite GB S Guides": { + "x": 204, + "y": 600, + "w": 282, + "ch": 10, + "s": ["400", "400i"] + }, + "Playwrite HR": { + "x": 500, + "y": 600, + "w": 191, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite HR Guides": { + "x": 705, + "y": 600, + "w": 286, + "ch": 10, + "s": ["400"] + }, + "Playwrite HR Lijeva": { + "x": 0, + "y": 640, + "w": 276, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite HR Lijeva Guides": { + "x": 290, + "y": 640, + "w": 370, + "ch": 10, + "s": ["400"] + }, + "Playwrite HU": { + "x": 674, + "y": 640, + "w": 190, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite HU Guides": { + "x": 878, + "y": 640, + "w": 285, + "ch": 10, + "s": ["400"] + }, + "Playwrite ID": { + "x": 0, + "y": 680, + "w": 194, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite ID Guides": { + "x": 208, + "y": 680, + "w": 296, + "ch": 10, + "s": ["400"] + }, + "Playwrite IE": { + "x": 518, + "y": 680, + "w": 175, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite IE Guides": { + "x": 707, + "y": 680, + "w": 271, + "ch": 10, + "s": ["400"] + }, + "Playwrite IN": { + "x": 992, + "y": 680, + "w": 189, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite IN Guides": { + "x": 0, + "y": 720, + "w": 285, + "ch": 10, + "s": ["400"] + }, + "Playwrite IS": { + "x": 299, + "y": 720, + "w": 156, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite IS Guides": { + "x": 469, + "y": 720, + "w": 249, + "ch": 10, + "s": ["400"] + }, + "Playwrite IT Moderna": { + "x": 732, + "y": 720, + "w": 273, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite IT Moderna Guides": { + "x": 0, + "y": 760, + "w": 365, + "ch": 10, + "s": ["400"] + }, + "Playwrite IT Trad": { + "x": 379, + "y": 760, + "w": 243, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite IT Trad Guides": { + "x": 636, + "y": 760, + "w": 336, + "ch": 10, + "s": ["400"] + }, + "Playwrite MX": { + "x": 986, + "y": 760, + "w": 200, + "ch": 10, + "s": ["100", "200", "300", "400"] + }, + "Playwrite MX Guides": { "x": 0, "y": 0, "w": 297, "ch": 11, "s": ["400"] }, + "Playwrite NG Modern": { + "x": 311, + "y": 0, + "w": 279, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite NG Modern Guides": { + "x": 604, + "y": 0, + "w": 373, + "ch": 11, + "s": ["400"] + }, + "Playwrite NL": { + "x": 991, + "y": 0, + "w": 207, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite NL Guides": { + "x": 0, + "y": 40, + "w": 306, + "ch": 11, + "s": ["400"] + }, + "Playwrite NO": { + "x": 320, + "y": 40, + "w": 189, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite NO Guides": { + "x": 523, + "y": 40, + "w": 285, + "ch": 11, + "s": ["400"] + }, + "Playwrite NZ": { + "x": 822, + "y": 40, + "w": 164, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite NZ Basic": { + "x": 0, + "y": 80, + "w": 231, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite NZ Basic Guides": { + "x": 245, + "y": 80, + "w": 319, + "ch": 11, + "s": ["400"] + }, + "Playwrite NZ Guides": { + "x": 578, + "y": 80, + "w": 256, + "ch": 11, + "s": ["400"] + }, + "Playwrite PE": { + "x": 848, + "y": 80, + "w": 185, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite PE Guides": { + "x": 0, + "y": 120, + "w": 282, + "ch": 11, + "s": ["400"] + }, + "Playwrite PL": { + "x": 296, + "y": 120, + "w": 178, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite PL Guides": { + "x": 488, + "y": 120, + "w": 272, + "ch": 11, + "s": ["400"] + }, + "Playwrite PT": { + "x": 774, + "y": 120, + "w": 188, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite PT Guides": { + "x": 0, + "y": 160, + "w": 284, + "ch": 11, + "s": ["400"] + }, + "Playwrite RO": { + "x": 298, + "y": 160, + "w": 197, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite RO Guides": { + "x": 509, + "y": 160, + "w": 286, + "ch": 11, + "s": ["400"] + }, + "Playwrite SK": { + "x": 809, + "y": 160, + "w": 195, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite SK Guides": { + "x": 0, + "y": 200, + "w": 289, + "ch": 11, + "s": ["400"] + }, + "Playwrite TZ": { + "x": 303, + "y": 200, + "w": 182, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite TZ Guides": { + "x": 499, + "y": 200, + "w": 271, + "ch": 11, + "s": ["400"] + }, + "Playwrite US Modern": { + "x": 784, + "y": 200, + "w": 277, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite US Modern Guides": { + "x": 0, + "y": 240, + "w": 369, + "ch": 11, + "s": ["400"] + }, + "Playwrite US Trad": { + "x": 383, + "y": 240, + "w": 258, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite US Trad Guides": { + "x": 655, + "y": 240, + "w": 355, + "ch": 11, + "s": ["400"] + }, + "Playwrite VN": { + "x": 0, + "y": 280, + "w": 212, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite VN Guides": { + "x": 226, + "y": 280, + "w": 311, + "ch": 11, + "s": ["400"] + }, + "Playwrite ZA": { + "x": 551, + "y": 280, + "w": 182, + "ch": 11, + "s": ["100", "200", "300", "400"] + }, + "Playwrite ZA Guides": { + "x": 747, + "y": 280, + "w": 276, + "ch": 11, + "s": ["400"] + }, + "Plus Jakarta Sans": { + "x": 0, + "y": 320, + "w": 199, + "ch": 11, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Pochaevsk": { "x": 213, "y": 320, "w": 111, "ch": 11, "s": ["400"] }, + "Podkova": { + "x": 338, + "y": 320, + "w": 97, + "ch": 11, + "s": ["400", "500", "600", "700", "800"] + }, + "Poetsen One": { "x": 449, "y": 320, "w": 148, "ch": 11, "s": ["400"] }, + "Poiret One": { "x": 611, "y": 320, "w": 114, "ch": 11, "s": ["400"] }, + "Poller One": { "x": 739, "y": 320, "w": 164, "ch": 11, "s": ["400"] }, + "Poltawski Nowy": { + "x": 917, + "y": 320, + "w": 182, + "ch": 11, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Poly": { "x": 1113, "y": 320, "w": 54, "ch": 11, "s": ["400", "400i"] }, + "Pompiere": { "x": 0, "y": 360, "w": 75, "ch": 11, "s": ["400"] }, + "Ponnala": { "x": 89, "y": 360, "w": 70, "ch": 11, "s": ["400"] }, + "Ponomar": { "x": 173, "y": 360, "w": 97, "ch": 11, "s": ["400"] }, + "Pontano Sans": { + "x": 284, + "y": 360, + "w": 149, + "ch": 11, + "s": ["300", "400", "500", "600", "700"] + }, + "Poor Story": { "x": 447, "y": 360, "w": 99, "ch": 11, "s": ["400"] }, + "Poppins": { + "x": 560, + "y": 360, + "w": 104, + "ch": 11, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Port Lligat Sans": { + "x": 678, + "y": 360, + "w": 157, + "ch": 11, + "s": ["400"] + }, + "Port Lligat Slab": { + "x": 849, + "y": 360, + "w": 153, + "ch": 11, + "s": ["400"] + }, + "Potta One": { "x": 1016, "y": 360, "w": 143, "ch": 11, "s": ["400"] }, + "Pragati Narrow": { + "x": 0, + "y": 400, + "w": 129, + "ch": 11, + "s": ["400", "700"] + }, + "Praise": { "x": 143, "y": 400, "w": 59, "ch": 11, "s": ["400"] }, + "Prata": { "x": 216, "y": 400, "w": 71, "ch": 11, "s": ["400"] }, + "Preahvihear": { "x": 301, "y": 400, "w": 161, "ch": 11, "s": ["400"] }, + "Press Start 2P": { "x": 476, "y": 400, "w": 344, "ch": 11, "s": ["400"] }, + "Pridi": { + "x": 834, + "y": 400, + "w": 60, + "ch": 11, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Princess Sofia": { "x": 908, "y": 400, "w": 131, "ch": 11, "s": ["400"] }, + "Prociono": { "x": 1053, "y": 400, "w": 101, "ch": 11, "s": ["400"] }, + "Prompt": { + "x": 0, + "y": 440, + "w": 92, + "ch": 11, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Prosto One": { "x": 106, "y": 440, "w": 159, "ch": 11, "s": ["400"] }, + "Protest Guerrilla": { + "x": 279, + "y": 440, + "w": 177, + "ch": 11, + "s": ["400"] + }, + "Protest Revolution": { + "x": 470, + "y": 440, + "w": 200, + "ch": 11, + "s": ["400"] + }, + "Protest Riot": { "x": 684, "y": 440, "w": 131, "ch": 11, "s": ["400"] }, + "Protest Strike": { "x": 829, "y": 440, "w": 151, "ch": 11, "s": ["400"] }, + "Proza Libre": { + "x": 994, + "y": 440, + "w": 137, + "ch": 11, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "PT Mono": { "x": 0, "y": 480, "w": 109, "ch": 11, "s": ["400"] }, + "PT Sans": { + "x": 123, + "y": 480, + "w": 88, + "ch": 11, + "s": ["400", "400i", "700", "700i"] + }, + "PT Sans Caption": { + "x": 225, + "y": 480, + "w": 197, + "ch": 11, + "s": ["400", "700"] + }, + "PT Sans Narrow": { + "x": 436, + "y": 480, + "w": 138, + "ch": 11, + "s": ["400", "700"] + }, + "PT Serif": { + "x": 588, + "y": 480, + "w": 93, + "ch": 11, + "s": ["400", "400i", "700", "700i"] + }, + "PT Serif Caption": { + "x": 695, + "y": 480, + "w": 205, + "ch": 11, + "s": ["400", "400i"] + }, + "Public Sans": { + "x": 914, + "y": 480, + "w": 139, + "ch": 11, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Puppies Play": { "x": 1067, "y": 480, "w": 85, "ch": 11, "s": ["400"] }, + "Puritan": { + "x": 0, + "y": 520, + "w": 78, + "ch": 11, + "s": ["400", "400i", "700", "700i"] + }, + "Purple Purse": { "x": 92, "y": 520, "w": 150, "ch": 11, "s": ["400"] }, + "Pushster": { "x": 256, "y": 520, "w": 89, "ch": 11, "s": ["400"] }, + "Qahiri": { "x": 359, "y": 520, "w": 51, "ch": 11, "s": ["400"] }, + "Quando": { "x": 424, "y": 520, "w": 107, "ch": 11, "s": ["400"] }, + "Quantico": { + "x": 545, + "y": 520, + "w": 109, + "ch": 11, + "s": ["400", "400i", "700", "700i"] + }, + "Quattrocento": { + "x": 668, + "y": 520, + "w": 153, + "ch": 11, + "s": ["400", "700"] + }, + "Quattrocento Sans": { + "x": 835, + "y": 520, + "w": 201, + "ch": 11, + "s": ["400", "400i", "700", "700i"] + }, + "Questrial": { "x": 1050, "y": 520, "w": 105, "ch": 11, "s": ["400"] }, + "Quicksand": { + "x": 0, + "y": 560, + "w": 125, + "ch": 11, + "s": ["300", "400", "500", "600", "700"] + }, + "Quintessential": { "x": 139, "y": 560, "w": 144, "ch": 11, "s": ["400"] }, + "Qwigley": { "x": 297, "y": 560, "w": 59, "ch": 11, "s": ["400"] }, + "Qwitcher Grypen": { + "x": 370, + "y": 560, + "w": 111, + "ch": 11, + "s": ["400", "700"] + }, + "Racing Sans One": { "x": 495, "y": 560, "w": 190, "ch": 11, "s": ["400"] }, + "Radio Canada": { + "x": 699, + "y": 560, + "w": 161, + "ch": 11, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Radio Canada Big": { + "x": 874, + "y": 560, + "w": 196, + "ch": 11, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Radley": { "x": 1084, "y": 560, "w": 81, "ch": 11, "s": ["400", "400i"] }, + "Rajdhani": { + "x": 0, + "y": 600, + "w": 92, + "ch": 11, + "s": ["300", "400", "500", "600", "700"] + }, + "Rakkas": { "x": 106, "y": 600, "w": 79, "ch": 11, "s": ["400"] }, + "Raleway": { + "x": 199, + "y": 600, + "w": 102, + "ch": 11, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Raleway Dots": { "x": 315, "y": 600, "w": 156, "ch": 11, "s": ["400"] }, + "Ramabhadra": { "x": 485, "y": 600, "w": 154, "ch": 11, "s": ["400"] }, + "Ramaraja": { "x": 653, "y": 600, "w": 96, "ch": 11, "s": ["400"] }, + "Rambla": { + "x": 763, + "y": 600, + "w": 83, + "ch": 11, + "s": ["400", "400i", "700", "700i"] + }, + "Rammetto One": { "x": 860, "y": 600, "w": 234, "ch": 11, "s": ["400"] }, + "Rampart One": { "x": 0, "y": 640, "w": 170, "ch": 11, "s": ["400"] }, + "Ranchers": { "x": 184, "y": 640, "w": 97, "ch": 11, "s": ["400"] }, + "Rancho": { "x": 295, "y": 640, "w": 62, "ch": 11, "s": ["400"] }, + "Ranga": { "x": 371, "y": 640, "w": 52, "ch": 11, "s": ["400", "700"] }, + "Rasa": { + "x": 437, + "y": 640, + "w": 53, + "ch": 11, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Rationale": { "x": 504, "y": 640, "w": 90, "ch": 11, "s": ["400"] }, + "Ravi Prakash": { "x": 608, "y": 640, "w": 132, "ch": 11, "s": ["400"] }, + "Readex Pro": { + "x": 754, + "y": 640, + "w": 140, + "ch": 11, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Recursive": { + "x": 908, + "y": 640, + "w": 123, + "ch": 11, + "s": ["300", "400", "500", "600", "700", "800", "900"] + }, + "Red Hat Display": { + "x": 0, + "y": 680, + "w": 175, + "ch": 11, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Red Hat Mono": { + "x": 189, + "y": 680, + "w": 181, + "ch": 11, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Red Hat Text": { + "x": 384, + "y": 680, + "w": 146, + "ch": 11, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Red Rose": { + "x": 544, + "y": 680, + "w": 117, + "ch": 11, + "s": ["300", "400", "500", "600", "700"] + }, + "Redacted": { "x": 675, "y": 680, "w": 80, "ch": 11, "s": ["400"] }, + "Redacted Script": { + "x": 769, + "y": 680, + "w": 201, + "ch": 11, + "s": ["300", "400", "700"] + }, + "Reddit Mono": { + "x": 984, + "y": 680, + "w": 157, + "ch": 11, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Reddit Sans": { + "x": 0, + "y": 720, + "w": 134, + "ch": 11, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Reddit Sans Condensed": { + "x": 148, + "y": 720, + "w": 232, + "ch": 11, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Redressed": { "x": 394, "y": 720, "w": 96, "ch": 11, "s": ["400"] }, + "Reem Kufi": { + "x": 504, + "y": 720, + "w": 118, + "ch": 11, + "s": ["400", "500", "600", "700"] + }, + "Reem Kufi Fun": { + "x": 636, + "y": 720, + "w": 161, + "ch": 11, + "s": ["400", "500", "600", "700"] + }, + "Reem Kufi Ink": { "x": 811, "y": 720, "w": 154, "ch": 11, "s": ["400"] }, + "Reenie Beanie": { "x": 979, "y": 720, "w": 122, "ch": 11, "s": ["400"] }, + "Reggae One": { "x": 0, "y": 760, "w": 161, "ch": 11, "s": ["400"] }, + "REM": { + "x": 175, + "y": 760, + "w": 63, + "ch": 11, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Rethink Sans": { + "x": 252, + "y": 760, + "w": 150, + "ch": 11, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Revalia": { "x": 416, "y": 760, "w": 121, "ch": 11, "s": ["400"] }, + "Rhodium Libre": { "x": 551, "y": 760, "w": 187, "ch": 11, "s": ["400"] }, + "Ribeye": { "x": 752, "y": 760, "w": 92, "ch": 11, "s": ["400"] }, + "Ribeye Marrow": { "x": 858, "y": 760, "w": 201, "ch": 11, "s": ["400"] }, + "Righteous": { "x": 1073, "y": 760, "w": 121, "ch": 11, "s": ["400"] }, + "Risque": { "x": 0, "y": 0, "w": 77, "ch": 12, "s": ["400"] }, + "Road Rage": { "x": 91, "y": 0, "w": 72, "ch": 12, "s": ["400"] }, + "Roboto": { + "x": 177, + "y": 0, + "w": 85, + "ch": 12, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Roboto Condensed": { + "x": 276, + "y": 0, + "w": 188, + "ch": 12, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Roboto Flex": { "x": 478, "y": 0, "w": 134, "ch": 12, "s": ["400"] }, + "Roboto Mono": { + "x": 626, + "y": 0, + "w": 167, + "ch": 12, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Roboto Serif": { + "x": 807, + "y": 0, + "w": 162, + "ch": 12, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Roboto Slab": { + "x": 983, + "y": 0, + "w": 141, + "ch": 12, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Rochester": { "x": 0, "y": 40, "w": 85, "ch": 12, "s": ["400"] }, + "Rock 3D": { "x": 99, "y": 40, "w": 137, "ch": 12, "s": ["400"] }, + "Rock Salt": { "x": 250, "y": 40, "w": 153, "ch": 12, "s": ["400"] }, + "RocknRoll One": { "x": 417, "y": 40, "w": 190, "ch": 12, "s": ["400"] }, + "Rokkitt": { + "x": 621, + "y": 40, + "w": 79, + "ch": 12, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Romanesco": { "x": 714, "y": 40, "w": 73, "ch": 12, "s": ["400"] }, + "Ropa Sans": { + "x": 801, + "y": 40, + "w": 102, + "ch": 12, + "s": ["400", "400i"] + }, + "Rosario": { + "x": 917, + "y": 40, + "w": 84, + "ch": 12, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Rosarivo": { + "x": 1015, + "y": 40, + "w": 106, + "ch": 12, + "s": ["400", "400i"] + }, + "Rouge Script": { "x": 0, "y": 80, "w": 104, "ch": 12, "s": ["400"] }, + "Rowdies": { + "x": 118, + "y": 80, + "w": 106, + "ch": 12, + "s": ["300", "400", "700"] + }, + "Rozha One": { "x": 238, "y": 80, "w": 126, "ch": 12, "s": ["400"] }, + "Rubik": { + "x": 378, + "y": 80, + "w": 71, + "ch": 12, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Rubik 80s Fade": { "x": 463, "y": 80, "w": 199, "ch": 12, "s": ["400"] }, + "Rubik Beastly": { "x": 676, "y": 80, "w": 183, "ch": 12, "s": ["400"] }, + "Rubik Broken Fax": { "x": 873, "y": 80, "w": 227, "ch": 12, "s": ["400"] }, + "Rubik Bubbles": { "x": 0, "y": 120, "w": 188, "ch": 12, "s": ["400"] }, + "Rubik Burned": { "x": 202, "y": 120, "w": 179, "ch": 12, "s": ["400"] }, + "Rubik Dirt": { "x": 395, "y": 120, "w": 137, "ch": 12, "s": ["400"] }, + "Rubik Distressed": { + "x": 546, + "y": 120, + "w": 225, + "ch": 12, + "s": ["400"] + }, + "Rubik Doodle Shadow": { + "x": 785, + "y": 120, + "w": 277, + "ch": 12, + "s": ["400"] + }, + "Rubik Doodle Triangles": { + "x": 0, + "y": 160, + "w": 296, + "ch": 12, + "s": ["400"] + }, + "Rubik Gemstones": { "x": 310, "y": 160, "w": 229, "ch": 12, "s": ["400"] }, + "Rubik Glitch": { "x": 553, "y": 160, "w": 165, "ch": 12, "s": ["400"] }, + "Rubik Glitch Pop": { + "x": 732, + "y": 160, + "w": 217, + "ch": 12, + "s": ["400"] + }, + "Rubik Iso": { "x": 963, "y": 160, "w": 124, "ch": 12, "s": ["400"] }, + "Rubik Lines": { "x": 0, "y": 200, "w": 155, "ch": 12, "s": ["400"] }, + "Rubik Maps": { "x": 169, "y": 200, "w": 151, "ch": 12, "s": ["400"] }, + "Rubik Marker Hatch": { + "x": 334, + "y": 200, + "w": 257, + "ch": 12, + "s": ["400"] + }, + "Rubik Maze": { "x": 605, "y": 200, "w": 150, "ch": 12, "s": ["400"] }, + "Rubik Microbe": { "x": 769, "y": 200, "w": 188, "ch": 12, "s": ["400"] }, + "Rubik Mono One": { "x": 0, "y": 240, "w": 294, "ch": 12, "s": ["400"] }, + "Rubik Moonrocks": { "x": 308, "y": 240, "w": 225, "ch": 12, "s": ["400"] }, + "Rubik Pixels": { "x": 547, "y": 240, "w": 162, "ch": 12, "s": ["400"] }, + "Rubik Puddles": { "x": 723, "y": 240, "w": 187, "ch": 12, "s": ["400"] }, + "Rubik Scribble": { "x": 924, "y": 240, "w": 192, "ch": 12, "s": ["400"] }, + "Rubik Spray Paint": { "x": 0, "y": 280, "w": 233, "ch": 12, "s": ["400"] }, + "Rubik Storm": { "x": 247, "y": 280, "w": 165, "ch": 12, "s": ["400"] }, + "Rubik Vinyl": { "x": 426, "y": 280, "w": 150, "ch": 12, "s": ["400"] }, + "Rubik Wet Paint": { "x": 590, "y": 280, "w": 207, "ch": 12, "s": ["400"] }, + "Ruda": { + "x": 811, + "y": 280, + "w": 66, + "ch": 12, + "s": ["400", "500", "600", "700", "800", "900"] + }, + "Rufina": { "x": 891, "y": 280, "w": 82, "ch": 12, "s": ["400", "700"] }, + "Ruge Boogie": { "x": 987, "y": 280, "w": 99, "ch": 12, "s": ["400"] }, + "Ruluko": { "x": 1100, "y": 280, "w": 78, "ch": 12, "s": ["400"] }, + "Rum Raisin": { "x": 0, "y": 320, "w": 100, "ch": 12, "s": ["400"] }, + "Ruslan Display": { "x": 114, "y": 320, "w": 220, "ch": 12, "s": ["400"] }, + "Russo One": { "x": 348, "y": 320, "w": 138, "ch": 12, "s": ["400"] }, + "Ruthie": { "x": 500, "y": 320, "w": 64, "ch": 12, "s": ["400"] }, + "Ruwudu": { + "x": 578, + "y": 320, + "w": 94, + "ch": 12, + "s": ["400", "500", "600", "700"] + }, + "Rye": { "x": 686, "y": 320, "w": 55, "ch": 12, "s": ["400"] }, + "Sacramento": { "x": 755, "y": 320, "w": 104, "ch": 12, "s": ["400"] }, + "Sahitya": { "x": 873, "y": 320, "w": 81, "ch": 12, "s": ["400", "700"] }, + "Sail": { "x": 968, "y": 320, "w": 50, "ch": 12, "s": ["400"] }, + "Saira": { + "x": 1032, + "y": 320, + "w": 66, + "ch": 12, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Saira Condensed": { + "x": 0, + "y": 360, + "w": 149, + "ch": 12, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Saira Extra Condensed": { + "x": 163, + "y": 360, + "w": 167, + "ch": 12, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Saira Semi Condensed": { + "x": 344, + "y": 360, + "w": 227, + "ch": 12, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Saira Stencil One": { + "x": 585, + "y": 360, + "w": 201, + "ch": 12, + "s": ["400"] + }, + "Salsa": { "x": 800, "y": 360, "w": 66, "ch": 12, "s": ["400"] }, + "Sanchez": { "x": 880, "y": 360, "w": 105, "ch": 12, "s": ["400", "400i"] }, + "Sancreek": { "x": 999, "y": 360, "w": 111, "ch": 12, "s": ["400"] }, + "Sankofa Display": { "x": 0, "y": 400, "w": 163, "ch": 12, "s": ["400"] }, + "Sansation": { + "x": 177, + "y": 400, + "w": 119, + "ch": 12, + "s": ["300", "300i", "400", "400i", "700", "700i"] + }, + "Sansita": { + "x": 310, + "y": 400, + "w": 80, + "ch": 12, + "s": ["400", "400i", "700", "700i", "800", "800i", "900", "900i"] + }, + "Sansita Swashed": { + "x": 404, + "y": 400, + "w": 177, + "ch": 12, + "s": ["300", "400", "500", "600", "700", "800", "900"] + }, + "Sarabun": { + "x": 595, + "y": 400, + "w": 94, + "ch": 12, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Sarala": { "x": 703, "y": 400, "w": 74, "ch": 12, "s": ["400", "700"] }, + "Sarina": { "x": 791, "y": 400, "w": 112, "ch": 12, "s": ["400"] }, + "Sarpanch": { + "x": 917, + "y": 400, + "w": 122, + "ch": 12, + "s": ["400", "500", "600", "700", "800", "900"] + }, + "Sassy Frass": { "x": 1053, "y": 400, "w": 78, "ch": 12, "s": ["400"] }, + "Satisfy": { "x": 0, "y": 440, "w": 70, "ch": 12, "s": ["400"] }, + "Savate": { + "x": 84, + "y": 440, + "w": 81, + "ch": 12, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Sawarabi Gothic": { "x": 179, "y": 440, "w": 186, "ch": 12, "s": ["400"] }, + "Sawarabi Mincho": { "x": 379, "y": 440, "w": 204, "ch": 12, "s": ["400"] }, + "Scada": { + "x": 597, + "y": 440, + "w": 69, + "ch": 12, + "s": ["400", "400i", "700", "700i"] + }, + "Scheherazade New": { + "x": 680, + "y": 440, + "w": 175, + "ch": 12, + "s": ["400", "500", "600", "700"] + }, + "Schibsted Grotesk": { + "x": 869, + "y": 440, + "w": 212, + "ch": 12, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Schoolbell": { "x": 1095, "y": 440, "w": 102, "ch": 12, "s": ["400"] }, + "Science Gothic": { + "x": 0, + "y": 480, + "w": 212, + "ch": 12, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Scope One": { "x": 226, "y": 480, "w": 120, "ch": 12, "s": ["400"] }, + "Seaweed Script": { "x": 360, "y": 480, "w": 137, "ch": 12, "s": ["400"] }, + "Secular One": { "x": 511, "y": 480, "w": 144, "ch": 12, "s": ["400"] }, + "Sedan": { "x": 669, "y": 480, "w": 72, "ch": 12, "s": ["400", "400i"] }, + "Sedan SC": { "x": 755, "y": 480, "w": 111, "ch": 12, "s": ["400"] }, + "Sedgwick Ave": { "x": 880, "y": 480, "w": 140, "ch": 12, "s": ["400"] }, + "Sedgwick Ave Display": { + "x": 0, + "y": 520, + "w": 224, + "ch": 12, + "s": ["400"] + }, + "Sekuya": { "x": 238, "y": 520, "w": 138, "ch": 12, "s": ["400"] }, + "Sen": { + "x": 390, + "y": 520, + "w": 48, + "ch": 12, + "s": ["400", "500", "600", "700", "800"] + }, + "Send Flowers": { "x": 452, "y": 520, "w": 129, "ch": 12, "s": ["400"] }, + "Sevillana": { "x": 595, "y": 520, "w": 96, "ch": 12, "s": ["400"] }, + "Seymour One": { "x": 705, "y": 520, "w": 241, "ch": 12, "s": ["400"] }, + "Shadows Into Light": { + "x": 960, + "y": 520, + "w": 172, + "ch": 12, + "s": ["400"] + }, + "Shadows Into Light Two": { + "x": 0, + "y": 560, + "w": 234, + "ch": 12, + "s": ["400"] + }, + "Shafarik": { "x": 248, "y": 560, "w": 89, "ch": 12, "s": ["400"] }, + "Shalimar": { "x": 351, "y": 560, "w": 59, "ch": 12, "s": ["400"] }, + "Shantell Sans": { + "x": 424, + "y": 560, + "w": 166, + "ch": 12, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Shanti": { "x": 604, "y": 560, "w": 75, "ch": 12, "s": ["400"] }, + "Share": { + "x": 693, + "y": 560, + "w": 62, + "ch": 12, + "s": ["400", "400i", "700", "700i"] + }, + "Share Tech": { "x": 769, "y": 560, "w": 109, "ch": 12, "s": ["400"] }, + "Share Tech Mono": { "x": 892, "y": 560, "w": 203, "ch": 12, "s": ["400"] }, + "Shippori Antique": { "x": 0, "y": 600, "w": 206, "ch": 12, "s": ["400"] }, + "Shippori Antique B1": { + "x": 220, + "y": 600, + "w": 241, + "ch": 12, + "s": ["400"] + }, + "Shippori Mincho": { + "x": 475, + "y": 600, + "w": 202, + "ch": 12, + "s": ["400", "500", "600", "700", "800"] + }, + "Shippori Mincho B1": { + "x": 691, + "y": 600, + "w": 237, + "ch": 12, + "s": ["400", "500", "600", "700", "800"] + }, + "Shizuru": { "x": 942, "y": 600, "w": 82, "ch": 12, "s": ["400"] }, + "Shojumaru": { "x": 0, "y": 640, "w": 184, "ch": 12, "s": ["400"] }, + "Short Stack": { "x": 198, "y": 640, "w": 168, "ch": 12, "s": ["400"] }, + "Shrikhand": { "x": 380, "y": 640, "w": 147, "ch": 12, "s": ["400"] }, + "Sigmar": { "x": 541, "y": 640, "w": 99, "ch": 12, "s": ["400"] }, + "Sigmar One": { "x": 654, "y": 640, "w": 172, "ch": 12, "s": ["400"] }, + "Signika": { + "x": 840, + "y": 640, + "w": 85, + "ch": 12, + "s": ["300", "400", "500", "600", "700"] + }, + "Signika Negative": { + "x": 939, + "y": 640, + "w": 180, + "ch": 12, + "s": ["300", "400", "500", "600", "700"] + }, + "Silkscreen": { "x": 0, "y": 680, "w": 173, "ch": 12, "s": ["400", "700"] }, + "Simonetta": { + "x": 187, + "y": 680, + "w": 108, + "ch": 12, + "s": ["400", "400i", "900", "900i"] + }, + "Sintony": { "x": 309, "y": 680, "w": 97, "ch": 12, "s": ["400", "700"] }, + "Sirin Stencil": { "x": 420, "y": 680, "w": 122, "ch": 12, "s": ["400"] }, + "Sirivennela": { "x": 556, "y": 680, "w": 98, "ch": 12, "s": ["400"] }, + "Six Caps": { "x": 668, "y": 680, "w": 47, "ch": 12, "s": ["400"] }, + "Sixtyfour": { "x": 729, "y": 680, "w": 224, "ch": 12, "s": ["400"] }, + "Sixtyfour Convergence": { + "x": 0, + "y": 720, + "w": 512, + "ch": 12, + "s": ["400"] + }, + "Skranji": { "x": 526, "y": 720, "w": 78, "ch": 12, "s": ["400", "700"] }, + "Slabo 13px": { "x": 618, "y": 720, "w": 127, "ch": 12, "s": ["400"] }, + "Slabo 27px": { "x": 759, "y": 720, "w": 111, "ch": 12, "s": ["400"] }, + "Slackey": { "x": 884, "y": 720, "w": 124, "ch": 12, "s": ["400"] }, + "Slackside One": { "x": 1022, "y": 720, "w": 137, "ch": 12, "s": ["400"] }, + "Smokum": { "x": 0, "y": 760, "w": 76, "ch": 12, "s": ["400"] }, + "Smooch": { "x": 90, "y": 760, "w": 73, "ch": 12, "s": ["400"] }, + "Smooch Sans": { + "x": 177, + "y": 760, + "w": 110, + "ch": 12, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Smythe": { "x": 301, "y": 760, "w": 64, "ch": 12, "s": ["400"] }, + "SN Pro": { + "x": 379, + "y": 760, + "w": 84, + "ch": 12, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Sniglet": { "x": 477, "y": 760, "w": 82, "ch": 12, "s": ["400", "800"] }, + "Snippet": { "x": 573, "y": 760, "w": 89, "ch": 12, "s": ["400"] }, + "Snowburst One": { "x": 676, "y": 760, "w": 197, "ch": 12, "s": ["400"] }, + "Sofadi One": { "x": 887, "y": 760, "w": 137, "ch": 12, "s": ["400"] }, + "Sofia": { "x": 1038, "y": 760, "w": 61, "ch": 12, "s": ["400"] }, + "Sofia Sans": { + "x": 0, + "y": 0, + "w": 115, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Sofia Sans Condensed": { + "x": 129, + "y": 0, + "w": 184, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Sofia Sans Extra Condensed": { + "x": 327, + "y": 0, + "w": 188, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Sofia Sans Semi Condensed": { + "x": 529, + "y": 0, + "w": 280, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Solitreo": { "x": 823, "y": 0, "w": 83, "ch": 13, "s": ["400"] }, + "Solway": { + "x": 920, + "y": 0, + "w": 90, + "ch": 13, + "s": ["300", "400", "500", "700", "800"] + }, + "Sometype Mono": { + "x": 0, + "y": 40, + "w": 189, + "ch": 13, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Sono": { + "x": 203, + "y": 40, + "w": 68, + "ch": 13, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Sonsie One": { "x": 285, "y": 40, "w": 194, "ch": 13, "s": ["400"] }, + "Sora": { + "x": 493, + "y": 40, + "w": 65, + "ch": 13, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Sorts Mill Goudy": { + "x": 572, + "y": 40, + "w": 187, + "ch": 13, + "s": ["400", "400i"] + }, + "Sour Gummy": { + "x": 773, + "y": 40, + "w": 149, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Source Code Pro": { + "x": 936, + "y": 40, + "w": 224, + "ch": 13, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Source Sans 3": { + "x": 0, + "y": 80, + "w": 147, + "ch": 13, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Source Sans Pro": { + "x": 161, + "y": 80, + "w": 170, + "ch": 13, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "600", + "600i", + "700", + "700i", + "900", + "900i" + ] + }, + "Source Serif 4": { + "x": 345, + "y": 80, + "w": 164, + "ch": 13, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Source Serif Pro": { + "x": 523, + "y": 80, + "w": 180, + "ch": 13, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "600", + "600i", + "700", + "700i", + "900", + "900i" + ] + }, + "Space Grotesk": { + "x": 717, + "y": 80, + "w": 176, + "ch": 13, + "s": ["300", "400", "500", "600", "700"] + }, + "Space Mono": { + "x": 907, + "y": 80, + "w": 155, + "ch": 13, + "s": ["400", "400i", "700", "700i"] + }, + "Special Elite": { "x": 0, "y": 120, "w": 177, "ch": 13, "s": ["400"] }, + "Special Gothic": { + "x": 191, + "y": 120, + "w": 156, + "ch": 13, + "s": ["400", "500", "600", "700"] + }, + "Special Gothic Condensed One": { + "x": 361, + "y": 120, + "w": 247, + "ch": 13, + "s": ["400"] + }, + "Special Gothic Expanded One": { + "x": 622, + "y": 120, + "w": 424, + "ch": 13, + "s": ["400"] + }, + "Spectral": { + "x": 1060, + "y": 120, + "w": 95, + "ch": 13, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Spectral SC": { + "x": 0, + "y": 160, + "w": 159, + "ch": 13, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Spicy Rice": { "x": 173, "y": 160, "w": 111, "ch": 13, "s": ["400"] }, + "Spinnaker": { "x": 298, "y": 160, "w": 128, "ch": 13, "s": ["400"] }, + "Spirax": { "x": 440, "y": 160, "w": 73, "ch": 13, "s": ["400"] }, + "Splash": { "x": 527, "y": 160, "w": 88, "ch": 13, "s": ["400"] }, + "Spline Sans": { + "x": 629, + "y": 160, + "w": 133, + "ch": 13, + "s": ["300", "400", "500", "600", "700"] + }, + "Spline Sans Mono": { + "x": 776, + "y": 160, + "w": 239, + "ch": 13, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Squada One": { "x": 1029, "y": 160, "w": 111, "ch": 13, "s": ["400"] }, + "Square Peg": { "x": 0, "y": 200, "w": 73, "ch": 13, "s": ["400"] }, + "Sree Krushnadevaraya": { + "x": 87, + "y": 200, + "w": 222, + "ch": 13, + "s": ["400"] + }, + "Sriracha": { "x": 323, "y": 200, "w": 99, "ch": 13, "s": ["400"] }, + "Srisakdi": { "x": 436, "y": 200, "w": 90, "ch": 13, "s": ["400", "700"] }, + "Staatliches": { "x": 540, "y": 200, "w": 114, "ch": 13, "s": ["400"] }, + "Stack Sans Headline": { + "x": 668, + "y": 200, + "w": 235, + "ch": 13, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Stack Sans Notch": { + "x": 917, + "y": 200, + "w": 205, + "ch": 13, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Stack Sans Text": { + "x": 0, + "y": 240, + "w": 195, + "ch": 13, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Stalemate": { "x": 209, "y": 240, "w": 62, "ch": 13, "s": ["400"] }, + "Stalinist One": { "x": 285, "y": 240, "w": 280, "ch": 13, "s": ["400"] }, + "Stardos Stencil": { + "x": 579, + "y": 240, + "w": 168, + "ch": 13, + "s": ["400", "700"] + }, + "Stick": { "x": 761, "y": 240, "w": 65, "ch": 13, "s": ["400"] }, + "Stick No Bills": { + "x": 840, + "y": 240, + "w": 123, + "ch": 13, + "s": ["200", "300", "400", "500", "600", "700", "800"] + }, + "Stint Ultra Condensed": { + "x": 977, + "y": 240, + "w": 139, + "ch": 13, + "s": ["400"] + }, + "Stint Ultra Expanded": { + "x": 0, + "y": 280, + "w": 310, + "ch": 13, + "s": ["400"] + }, + "STIX Two Text": { + "x": 324, + "y": 280, + "w": 156, + "ch": 13, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Stoke": { "x": 494, "y": 280, "w": 84, "ch": 13, "s": ["300", "400"] }, + "Story Script": { "x": 592, "y": 280, "w": 113, "ch": 13, "s": ["400"] }, + "Strait": { "x": 719, "y": 280, "w": 62, "ch": 13, "s": ["400"] }, + "Style Script": { "x": 795, "y": 280, "w": 97, "ch": 13, "s": ["400"] }, + "Sue Ellen Francisco": { + "x": 906, + "y": 280, + "w": 137, + "ch": 13, + "s": ["400"] + }, + "Suez One": { "x": 1057, "y": 280, "w": 113, "ch": 13, "s": ["400"] }, + "Sulphur Point": { + "x": 0, + "y": 320, + "w": 144, + "ch": 13, + "s": ["300", "400", "700"] + }, + "Sumana": { "x": 158, "y": 320, "w": 92, "ch": 13, "s": ["400", "700"] }, + "Sunshiney": { "x": 264, "y": 320, "w": 94, "ch": 13, "s": ["400"] }, + "Supermercado One": { + "x": 372, + "y": 320, + "w": 196, + "ch": 13, + "s": ["400"] + }, + "Sura": { "x": 582, "y": 320, "w": 58, "ch": 13, "s": ["400", "700"] }, + "Suranna": { "x": 654, "y": 320, "w": 86, "ch": 13, "s": ["400"] }, + "Suravaram": { "x": 754, "y": 320, "w": 104, "ch": 13, "s": ["400"] }, + "SUSE": { + "x": 872, + "y": 320, + "w": 65, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "SUSE Mono": { + "x": 951, + "y": 320, + "w": 138, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Suwannaphum": { + "x": 0, + "y": 360, + "w": 184, + "ch": 13, + "s": ["100", "300", "400", "700", "900"] + }, + "Swanky and Moo Moo": { + "x": 198, + "y": 360, + "w": 203, + "ch": 13, + "s": ["400"] + }, + "Syncopate": { + "x": 415, + "y": 360, + "w": 195, + "ch": 13, + "s": ["400", "700"] + }, + "Syne": { + "x": 624, + "y": 360, + "w": 60, + "ch": 13, + "s": ["400", "500", "600", "700", "800"] + }, + "Syne Mono": { "x": 698, "y": 360, "w": 127, "ch": 13, "s": ["400"] }, + "Syne Tactile": { "x": 839, "y": 360, "w": 119, "ch": 13, "s": ["400"] }, + "Tac One": { "x": 972, "y": 360, "w": 73, "ch": 13, "s": ["400"] }, + "Tagesschrift": { "x": 0, "y": 400, "w": 142, "ch": 13, "s": ["400"] }, + "Tai Heritage Pro": { + "x": 156, + "y": 400, + "w": 171, + "ch": 13, + "s": ["400", "700"] + }, + "Tajawal": { + "x": 341, + "y": 400, + "w": 82, + "ch": 13, + "s": ["200", "300", "400", "500", "700", "800", "900"] + }, + "Tangerine": { "x": 437, "y": 400, "w": 67, "ch": 13, "s": ["400", "700"] }, + "Tapestry": { "x": 518, "y": 400, "w": 100, "ch": 13, "s": ["400"] }, + "Taprom": { "x": 632, "y": 400, "w": 70, "ch": 13, "s": ["400"] }, + "TASA Explorer": { + "x": 716, + "y": 400, + "w": 161, + "ch": 13, + "s": ["400", "500", "600", "700", "800"] + }, + "TASA Orbiter": { + "x": 891, + "y": 400, + "w": 153, + "ch": 13, + "s": ["400", "500", "600", "700", "800"] + }, + "Tauri": { "x": 1058, "y": 400, "w": 66, "ch": 13, "s": ["400"] }, + "Taviraj": { + "x": 0, + "y": 440, + "w": 84, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Teachers": { + "x": 98, + "y": 440, + "w": 100, + "ch": 13, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Teko": { + "x": 212, + "y": 440, + "w": 42, + "ch": 13, + "s": ["300", "400", "500", "600", "700"] + }, + "Tektur": { + "x": 268, + "y": 440, + "w": 86, + "ch": 13, + "s": ["400", "500", "600", "700", "800", "900"] + }, + "Telex": { "x": 368, "y": 440, "w": 64, "ch": 13, "s": ["400"] }, + "Tenali Ramakrishna": { + "x": 446, + "y": 440, + "w": 170, + "ch": 13, + "s": ["400"] + }, + "Tenor Sans": { "x": 630, "y": 440, "w": 134, "ch": 13, "s": ["400"] }, + "Text Me One": { "x": 778, "y": 440, "w": 140, "ch": 13, "s": ["400"] }, + "Texturina": { + "x": 932, + "y": 440, + "w": 113, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Thasadith": { + "x": 1059, + "y": 440, + "w": 101, + "ch": 13, + "s": ["400", "400i", "700", "700i"] + }, + "The Girl Next Door": { + "x": 0, + "y": 480, + "w": 215, + "ch": 13, + "s": ["400"] + }, + "The Nautigal": { + "x": 229, + "y": 480, + "w": 102, + "ch": 13, + "s": ["400", "700"] + }, + "Tienne": { + "x": 345, + "y": 480, + "w": 92, + "ch": 13, + "s": ["400", "700", "900"] + }, + "TikTok Sans": { + "x": 451, + "y": 480, + "w": 141, + "ch": 13, + "s": ["300", "400", "500", "600", "700", "800", "900"] + }, + "Tillana": { + "x": 606, + "y": 480, + "w": 75, + "ch": 13, + "s": ["400", "500", "600", "700", "800"] + }, + "Tilt Neon": { "x": 695, "y": 480, "w": 101, "ch": 13, "s": ["400"] }, + "Tilt Prism": { "x": 810, "y": 480, "w": 121, "ch": 13, "s": ["400"] }, + "Tilt Warp": { "x": 945, "y": 480, "w": 109, "ch": 13, "s": ["400"] }, + "Timmana": { "x": 1068, "y": 480, "w": 90, "ch": 13, "s": ["400"] }, + "Tinos": { + "x": 0, + "y": 520, + "w": 62, + "ch": 13, + "s": ["400", "400i", "700", "700i"] + }, + "Tiny5": { "x": 76, "y": 520, "w": 62, "ch": 13, "s": ["400"] }, + "Tiro Bangla": { + "x": 152, + "y": 520, + "w": 134, + "ch": 13, + "s": ["400", "400i"] + }, + "Tiro Devanagari Hindi": { + "x": 300, + "y": 520, + "w": 251, + "ch": 13, + "s": ["400", "400i"] + }, + "Tiro Devanagari Marathi": { + "x": 565, + "y": 520, + "w": 274, + "ch": 13, + "s": ["400", "400i"] + }, + "Tiro Devanagari Sanskrit": { + "x": 853, + "y": 520, + "w": 276, + "ch": 13, + "s": ["400", "400i"] + }, + "Tiro Gurmukhi": { + "x": 0, + "y": 560, + "w": 173, + "ch": 13, + "s": ["400", "400i"] + }, + "Tiro Kannada": { + "x": 187, + "y": 560, + "w": 156, + "ch": 13, + "s": ["400", "400i"] + }, + "Tiro Tamil": { + "x": 357, + "y": 560, + "w": 122, + "ch": 13, + "s": ["400", "400i"] + }, + "Tiro Telugu": { + "x": 493, + "y": 560, + "w": 134, + "ch": 13, + "s": ["400", "400i"] + }, + "Tirra": { + "x": 641, + "y": 560, + "w": 57, + "ch": 13, + "s": ["400", "500", "600", "700", "800", "900"] + }, + "Titan One": { "x": 712, "y": 560, "w": 131, "ch": 13, "s": ["400"] }, + "Titillium Web": { + "x": 857, + "y": 560, + "w": 140, + "ch": 13, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "600", + "600i", + "700", + "700i", + "900", + "900i" + ] + }, + "Tomorrow": { + "x": 1011, + "y": 560, + "w": 126, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Tourney": { + "x": 0, + "y": 600, + "w": 107, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Trade Winds": { "x": 121, "y": 600, "w": 160, "ch": 13, "s": ["400"] }, + "Train One": { "x": 295, "y": 600, "w": 132, "ch": 13, "s": ["400"] }, + "Triodion": { "x": 441, "y": 600, "w": 94, "ch": 13, "s": ["400"] }, + "Trirong": { + "x": 549, + "y": 600, + "w": 92, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Trispace": { + "x": 655, + "y": 600, + "w": 128, + "ch": 13, + "s": ["100", "200", "300", "400", "500", "600", "700", "800"] + }, + "Trocchi": { "x": 797, "y": 600, "w": 100, "ch": 13, "s": ["400"] }, + "Trochut": { + "x": 911, + "y": 600, + "w": 75, + "ch": 13, + "s": ["400", "400i", "700", "700i"] + }, + "Truculenta": { + "x": 1000, + "y": 600, + "w": 100, + "ch": 13, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Trykker": { "x": 0, "y": 640, "w": 98, "ch": 13, "s": ["400"] }, + "Tsukimi Rounded": { + "x": 112, + "y": 640, + "w": 209, + "ch": 13, + "s": ["300", "400", "500", "600", "700"] + }, + "Tuffy": { + "x": 335, + "y": 640, + "w": 63, + "ch": 13, + "s": ["400", "400i", "700", "700i"] + }, + "Tulpen One": { "x": 412, "y": 640, "w": 59, "ch": 13, "s": ["400"] }, + "Turret Road": { + "x": 485, + "y": 640, + "w": 142, + "ch": 13, + "s": ["200", "300", "400", "500", "700", "800"] + }, + "Twinkle Star": { "x": 641, "y": 640, "w": 137, "ch": 13, "s": ["400"] }, + "Ubuntu": { + "x": 792, + "y": 640, + "w": 90, + "ch": 13, + "s": ["300", "300i", "400", "400i", "500", "500i", "700", "700i"] + }, + "Ubuntu Condensed": { + "x": 896, + "y": 640, + "w": 177, + "ch": 13, + "s": ["400"] + }, + "Ubuntu Mono": { + "x": 0, + "y": 680, + "w": 156, + "ch": 13, + "s": ["400", "400i", "700", "700i"] + }, + "Ubuntu Sans": { + "x": 170, + "y": 680, + "w": 144, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Ubuntu Sans Mono": { + "x": 328, + "y": 680, + "w": 224, + "ch": 13, + "s": ["400", "400i", "500", "500i", "600", "600i", "700", "700i"] + }, + "Uchen": { "x": 566, "y": 680, "w": 75, "ch": 13, "s": ["400"] }, + "Ultra": { "x": 655, "y": 680, "w": 85, "ch": 13, "s": ["400"] }, + "Unbounded": { + "x": 754, + "y": 680, + "w": 174, + "ch": 13, + "s": ["200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Uncial Antiqua": { "x": 942, "y": 680, "w": 211, "ch": 13, "s": ["400"] }, + "Underdog": { "x": 0, "y": 720, "w": 113, "ch": 13, "s": ["400"] }, + "Unica One": { "x": 127, "y": 720, "w": 106, "ch": 13, "s": ["400"] }, + "UnifrakturMaguntia": { + "x": 247, + "y": 720, + "w": 204, + "ch": 13, + "s": ["400"] + }, + "Unkempt": { "x": 465, "y": 720, "w": 99, "ch": 13, "s": ["400", "700"] }, + "Unlock": { "x": 578, "y": 720, "w": 93, "ch": 13, "s": ["400"] }, + "Unna": { + "x": 685, + "y": 720, + "w": 60, + "ch": 13, + "s": ["400", "400i", "700", "700i"] + }, + "UoqMunThenKhung": { "x": 759, "y": 720, "w": 245, "ch": 13, "s": ["400"] }, + "Updock": { "x": 1018, "y": 720, "w": 58, "ch": 13, "s": ["400"] }, + "Urbanist": { + "x": 1090, + "y": 720, + "w": 97, + "ch": 13, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Vampiro One": { "x": 0, "y": 760, "w": 165, "ch": 13, "s": ["400"] }, + "Varela": { "x": 179, "y": 760, "w": 81, "ch": 13, "s": ["400"] }, + "Varela Round": { "x": 274, "y": 760, "w": 161, "ch": 13, "s": ["400"] }, + "Varta": { + "x": 449, + "y": 760, + "w": 61, + "ch": 13, + "s": ["300", "400", "500", "600", "700"] + }, + "Vast Shadow": { "x": 524, "y": 760, "w": 227, "ch": 13, "s": ["400"] }, + "Vazirmatn": { + "x": 765, + "y": 760, + "w": 117, + "ch": 13, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Vend Sans": { + "x": 896, + "y": 760, + "w": 121, + "ch": 13, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Vesper Libre": { + "x": 1031, + "y": 760, + "w": 136, + "ch": 13, + "s": ["400", "500", "700", "900"] + }, + "Viaoda Libre": { "x": 0, "y": 0, "w": 126, "ch": 14, "s": ["400"] }, + "Vibes": { "x": 140, "y": 0, "w": 53, "ch": 14, "s": ["400"] }, + "Vibur": { "x": 207, "y": 0, "w": 61, "ch": 14, "s": ["400"] }, + "Victor Mono": { + "x": 282, + "y": 0, + "w": 167, + "ch": 14, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Vidaloka": { "x": 463, "y": 0, "w": 99, "ch": 14, "s": ["400"] }, + "Viga": { "x": 576, "y": 0, "w": 55, "ch": 14, "s": ["400"] }, + "Vina Sans": { "x": 645, "y": 0, "w": 86, "ch": 14, "s": ["400"] }, + "Voces": { "x": 745, "y": 0, "w": 72, "ch": 14, "s": ["400"] }, + "Volkhov": { + "x": 831, + "y": 0, + "w": 104, + "ch": 14, + "s": ["400", "400i", "700", "700i"] + }, + "Vollkorn": { + "x": 949, + "y": 0, + "w": 99, + "ch": 14, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Vollkorn SC": { + "x": 0, + "y": 40, + "w": 166, + "ch": 14, + "s": ["400", "600", "700", "900"] + }, + "Voltaire": { "x": 180, "y": 40, "w": 73, "ch": 14, "s": ["400"] }, + "VT323": { "x": 267, "y": 40, "w": 56, "ch": 14, "s": ["400"] }, + "Vujahday Script": { "x": 337, "y": 40, "w": 151, "ch": 14, "s": ["400"] }, + "Waiting for the Sunrise": { + "x": 502, + "y": 40, + "w": 230, + "ch": 14, + "s": ["400"] + }, + "Wallpoet": { "x": 746, "y": 40, "w": 127, "ch": 14, "s": ["400"] }, + "Walter Turncoat": { "x": 887, "y": 40, "w": 206, "ch": 14, "s": ["400"] }, + "Warnes": { "x": 0, "y": 80, "w": 120, "ch": 14, "s": ["400"] }, + "Water Brush": { "x": 134, "y": 80, "w": 116, "ch": 14, "s": ["400"] }, + "Waterfall": { "x": 264, "y": 80, "w": 77, "ch": 14, "s": ["400"] }, + "Wavefont": { + "x": 355, + "y": 80, + "w": 28, + "ch": 14, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "WDXL Lubrifont JP N": { + "x": 397, + "y": 80, + "w": 185, + "ch": 14, + "s": ["400"] + }, + "WDXL Lubrifont SC": { + "x": 596, + "y": 80, + "w": 170, + "ch": 14, + "s": ["400"] + }, + "WDXL Lubrifont TC": { + "x": 780, + "y": 80, + "w": 168, + "ch": 14, + "s": ["400"] + }, + "Wellfleet": { "x": 962, "y": 80, "w": 117, "ch": 14, "s": ["400"] }, + "Wendy One": { "x": 0, "y": 120, "w": 142, "ch": 14, "s": ["400"] }, + "Whisper": { "x": 156, "y": 120, "w": 76, "ch": 14, "s": ["400"] }, + "WindSong": { "x": 246, "y": 120, "w": 133, "ch": 14, "s": ["400", "500"] }, + "Winky Rough": { + "x": 393, + "y": 120, + "w": 139, + "ch": 14, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Winky Sans": { + "x": 546, + "y": 120, + "w": 120, + "ch": 14, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Wire One": { "x": 680, "y": 120, "w": 57, "ch": 14, "s": ["400"] }, + "Wittgenstein": { + "x": 751, + "y": 120, + "w": 151, + "ch": 14, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Wix Madefor Display": { + "x": 916, + "y": 120, + "w": 238, + "ch": 14, + "s": ["400", "500", "600", "700", "800"] + }, + "Wix Madefor Text": { + "x": 0, + "y": 160, + "w": 202, + "ch": 14, + "s": [ + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i" + ] + }, + "Work Sans": { + "x": 216, + "y": 160, + "w": 132, + "ch": 14, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Workbench": { "x": 362, "y": 160, "w": 116, "ch": 14, "s": ["400"] }, + "Xanh Mono": { + "x": 492, + "y": 160, + "w": 116, + "ch": 14, + "s": ["400", "400i"] + }, + "Yaldevi": { + "x": 622, + "y": 160, + "w": 83, + "ch": 14, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Yanone Kaffeesatz": { + "x": 719, + "y": 160, + "w": 142, + "ch": 14, + "s": ["200", "300", "400", "500", "600", "700"] + }, + "Yantramanav": { + "x": 875, + "y": 160, + "w": 138, + "ch": 14, + "s": ["100", "300", "400", "500", "700", "900"] + }, + "Yarndings 12": { "x": 0, "y": 200, "w": 226, "ch": 14, "s": ["400"] }, + "Yarndings 12 Charted": { + "x": 240, + "y": 200, + "w": 365, + "ch": 14, + "s": ["400"] + }, + "Yarndings 20": { "x": 619, "y": 200, "w": 219, "ch": 14, "s": ["400"] }, + "Yarndings 20 Charted": { + "x": 0, + "y": 240, + "w": 357, + "ch": 14, + "s": ["400"] + }, + "Yatra One": { "x": 371, "y": 240, "w": 127, "ch": 14, "s": ["400"] }, + "Yellowtail": { "x": 512, "y": 240, "w": 93, "ch": 14, "s": ["400"] }, + "Yeon Sung": { "x": 619, "y": 240, "w": 107, "ch": 14, "s": ["400"] }, + "Yeseva One": { "x": 740, "y": 240, "w": 144, "ch": 14, "s": ["400"] }, + "Yesteryear": { "x": 898, "y": 240, "w": 94, "ch": 14, "s": ["400"] }, + "Yomogi": { "x": 1006, "y": 240, "w": 80, "ch": 14, "s": ["400"] }, + "Young Serif": { "x": 0, "y": 280, "w": 149, "ch": 14, "s": ["400"] }, + "Yrsa": { + "x": 163, + "y": 280, + "w": 49, + "ch": 14, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Ysabeau": { + "x": 226, + "y": 280, + "w": 88, + "ch": 14, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Ysabeau Infant": { + "x": 328, + "y": 280, + "w": 161, + "ch": 14, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Ysabeau Office": { + "x": 503, + "y": 280, + "w": 156, + "ch": 14, + "s": [ + "100", + "100i", + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Ysabeau SC": { + "x": 673, + "y": 280, + "w": 130, + "ch": 14, + "s": ["100", "200", "300", "400", "500", "600", "700", "800", "900"] + }, + "Yuji Boku": { "x": 817, "y": 280, "w": 135, "ch": 14, "s": ["400"] }, + "Yuji Hentaigana Akari": { + "x": 0, + "y": 320, + "w": 335, + "ch": 14, + "s": ["400"] + }, + "Yuji Hentaigana Akebono": { + "x": 349, + "y": 320, + "w": 374, + "ch": 14, + "s": ["400"] + }, + "Yuji Mai": { "x": 737, "y": 320, "w": 117, "ch": 14, "s": ["400"] }, + "Yuji Syuku": { "x": 868, "y": 320, "w": 144, "ch": 14, "s": ["400"] }, + "Yusei Magic": { "x": 1026, "y": 320, "w": 138, "ch": 14, "s": ["400"] }, + "Zain": { + "x": 0, + "y": 360, + "w": 54, + "ch": 14, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Zalando Sans": { + "x": 68, + "y": 360, + "w": 161, + "ch": 14, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Zalando Sans Expanded": { + "x": 243, + "y": 360, + "w": 335, + "ch": 14, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "Zalando Sans SemiExpanded": { + "x": 592, + "y": 360, + "w": 361, + "ch": 14, + "s": [ + "200", + "200i", + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i", + "800", + "800i", + "900", + "900i" + ] + }, + "ZCOOL KuaiLe": { "x": 967, "y": 360, "w": 180, "ch": 14, "s": ["400"] }, + "ZCOOL QingKe HuangYou": { + "x": 0, + "y": 400, + "w": 222, + "ch": 14, + "s": ["400"] + }, + "ZCOOL XiaoWei": { "x": 236, "y": 400, "w": 173, "ch": 14, "s": ["400"] }, + "Zen Antique": { "x": 423, "y": 400, "w": 149, "ch": 14, "s": ["400"] }, + "Zen Antique Soft": { + "x": 586, + "y": 400, + "w": 204, + "ch": 14, + "s": ["400"] + }, + "Zen Dots": { "x": 804, "y": 400, "w": 147, "ch": 14, "s": ["400"] }, + "Zen Kaku Gothic Antique": { + "x": 0, + "y": 440, + "w": 267, + "ch": 14, + "s": ["300", "400", "500", "700", "900"] + }, + "Zen Kaku Gothic New": { + "x": 281, + "y": 440, + "w": 231, + "ch": 14, + "s": ["300", "400", "500", "700", "900"] + }, + "Zen Kurenaido": { "x": 526, "y": 440, "w": 145, "ch": 14, "s": ["400"] }, + "Zen Loop": { "x": 685, "y": 440, "w": 65, "ch": 14, "s": ["400", "400i"] }, + "Zen Maru Gothic": { + "x": 764, + "y": 440, + "w": 181, + "ch": 14, + "s": ["300", "400", "500", "700", "900"] + }, + "Zen Old Mincho": { + "x": 959, + "y": 440, + "w": 173, + "ch": 14, + "s": ["400", "500", "600", "700", "900"] + }, + "Zen Tokyo Zoo": { "x": 0, "y": 480, "w": 154, "ch": 14, "s": ["400"] }, + "Zeyada": { "x": 168, "y": 480, "w": 70, "ch": 14, "s": ["400"] }, + "Zhi Mang Xing": { "x": 252, "y": 480, "w": 121, "ch": 14, "s": ["400"] }, + "Zilla Slab": { + "x": 387, + "y": 480, + "w": 102, + "ch": 14, + "s": [ + "300", + "300i", + "400", + "400i", + "500", + "500i", + "600", + "600i", + "700", + "700i" + ] + }, + "Zilla Slab Highlight": { + "x": 503, + "y": 480, + "w": 207, + "ch": 14, + "s": ["400", "700"] + } + } +} diff --git a/apps/web/public/fonts/font-chunk-0.avif b/apps/web/public/fonts/font-chunk-0.avif new file mode 100644 index 00000000..719b4235 Binary files /dev/null and b/apps/web/public/fonts/font-chunk-0.avif differ diff --git a/apps/web/public/fonts/font-chunk-1.avif b/apps/web/public/fonts/font-chunk-1.avif new file mode 100644 index 00000000..c276430b Binary files /dev/null and b/apps/web/public/fonts/font-chunk-1.avif differ diff --git a/apps/web/public/fonts/font-chunk-10.avif b/apps/web/public/fonts/font-chunk-10.avif new file mode 100644 index 00000000..9761e954 Binary files /dev/null and b/apps/web/public/fonts/font-chunk-10.avif differ diff --git a/apps/web/public/fonts/font-chunk-11.avif b/apps/web/public/fonts/font-chunk-11.avif new file mode 100644 index 00000000..d4f788e0 Binary files /dev/null and b/apps/web/public/fonts/font-chunk-11.avif differ diff --git a/apps/web/public/fonts/font-chunk-12.avif b/apps/web/public/fonts/font-chunk-12.avif new file mode 100644 index 00000000..91e643ae Binary files /dev/null and b/apps/web/public/fonts/font-chunk-12.avif differ diff --git a/apps/web/public/fonts/font-chunk-13.avif b/apps/web/public/fonts/font-chunk-13.avif new file mode 100644 index 00000000..6a507e8b Binary files /dev/null and b/apps/web/public/fonts/font-chunk-13.avif differ diff --git a/apps/web/public/fonts/font-chunk-14.avif b/apps/web/public/fonts/font-chunk-14.avif new file mode 100644 index 00000000..f67ab51b Binary files /dev/null and b/apps/web/public/fonts/font-chunk-14.avif differ diff --git a/apps/web/public/fonts/font-chunk-2.avif b/apps/web/public/fonts/font-chunk-2.avif new file mode 100644 index 00000000..bc3ca018 Binary files /dev/null and b/apps/web/public/fonts/font-chunk-2.avif differ diff --git a/apps/web/public/fonts/font-chunk-3.avif b/apps/web/public/fonts/font-chunk-3.avif new file mode 100644 index 00000000..a5b9dd60 Binary files /dev/null and b/apps/web/public/fonts/font-chunk-3.avif differ diff --git a/apps/web/public/fonts/font-chunk-4.avif b/apps/web/public/fonts/font-chunk-4.avif new file mode 100644 index 00000000..505fcb25 Binary files /dev/null and b/apps/web/public/fonts/font-chunk-4.avif differ diff --git a/apps/web/public/fonts/font-chunk-5.avif b/apps/web/public/fonts/font-chunk-5.avif new file mode 100644 index 00000000..7d8c0216 Binary files /dev/null and b/apps/web/public/fonts/font-chunk-5.avif differ diff --git a/apps/web/public/fonts/font-chunk-6.avif b/apps/web/public/fonts/font-chunk-6.avif new file mode 100644 index 00000000..d318967d Binary files /dev/null and b/apps/web/public/fonts/font-chunk-6.avif differ diff --git a/apps/web/public/fonts/font-chunk-7.avif b/apps/web/public/fonts/font-chunk-7.avif new file mode 100644 index 00000000..0dcb81d7 Binary files /dev/null and b/apps/web/public/fonts/font-chunk-7.avif differ diff --git a/apps/web/public/fonts/font-chunk-8.avif b/apps/web/public/fonts/font-chunk-8.avif new file mode 100644 index 00000000..63b0e35a Binary files /dev/null and b/apps/web/public/fonts/font-chunk-8.avif differ diff --git a/apps/web/public/fonts/font-chunk-9.avif b/apps/web/public/fonts/font-chunk-9.avif new file mode 100644 index 00000000..e7936bf0 Binary files /dev/null and b/apps/web/public/fonts/font-chunk-9.avif differ diff --git a/apps/web/public/manifest.json b/apps/web/public/manifest.json index d4a41dc0..2f00e1e9 100644 --- a/apps/web/public/manifest.json +++ b/apps/web/public/manifest.json @@ -1,44 +1,44 @@ { - "name": "OpenCut", - "description": "A simple but powerful video editor that gets the job done. In your browser.", - "display": "standalone", - "start_url": "/", - "icons": [ - { - "src": "/icons/android-icon-36x36.png", - "sizes": "36x36", - "type": "image\/png", - "density": "0.75" - }, - { - "src": "/icons/android-icon-48x48.png", - "sizes": "48x48", - "type": "image\/png", - "density": "1.0" - }, - { - "src": "/icons/android-icon-72x72.png", - "sizes": "72x72", - "type": "image\/png", - "density": "1.5" - }, - { - "src": "/icons/android-icon-96x96.png", - "sizes": "96x96", - "type": "image\/png", - "density": "2.0" - }, - { - "src": "/icons/android-icon-144x144.png", - "sizes": "144x144", - "type": "image\/png", - "density": "3.0" - }, - { - "src": "/icons/android-icon-192x192.png", - "sizes": "192x192", - "type": "image\/png", - "density": "4.0" - } - ] + "name": "OpenCut", + "description": "A simple but powerful video editor that gets the job done. In your browser.", + "display": "standalone", + "start_url": "/", + "icons": [ + { + "src": "/icons/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "/icons/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "/icons/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "/icons/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "/icons/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "/icons/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] } diff --git a/apps/web/public/shapes/circle.svg b/apps/web/public/shapes/circle.svg new file mode 100644 index 00000000..adb837cf --- /dev/null +++ b/apps/web/public/shapes/circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/shapes/diamond.svg b/apps/web/public/shapes/diamond.svg new file mode 100644 index 00000000..4a039b5c --- /dev/null +++ b/apps/web/public/shapes/diamond.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/shapes/hexagon.svg b/apps/web/public/shapes/hexagon.svg new file mode 100644 index 00000000..e1eb087e --- /dev/null +++ b/apps/web/public/shapes/hexagon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/shapes/square.svg b/apps/web/public/shapes/square.svg new file mode 100644 index 00000000..ed9b790f --- /dev/null +++ b/apps/web/public/shapes/square.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/shapes/star.svg b/apps/web/public/shapes/star.svg new file mode 100644 index 00000000..c49e0c53 --- /dev/null +++ b/apps/web/public/shapes/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/shapes/triangle.svg b/apps/web/public/shapes/triangle.svg new file mode 100644 index 00000000..a1bd6b0c --- /dev/null +++ b/apps/web/public/shapes/triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/scripts/generate-font-sprites.ts b/apps/web/scripts/generate-font-sprites.ts new file mode 100644 index 00000000..144d68ec --- /dev/null +++ b/apps/web/scripts/generate-font-sprites.ts @@ -0,0 +1,281 @@ +/** + * Generates font sprite atlas for the font picker. + * + * Downloads Google Fonts from Fontsource, renders each font name as a sprite, + * packs them into chunk images, and outputs a JSON atlas + AVIF images. + * + * Run: npx tsx scripts/generate-font-sprites.ts + * Deps: @napi-rs/canvas sharp (install as devDependencies) + */ + +import { createCanvas, GlobalFonts } from "@napi-rs/canvas"; +import sharp from "sharp"; +import { mkdir, writeFile, readFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const FONT_SIZE = 24; +const ROW_HEIGHT = 40; +const CANVAS_WIDTH = 1200; +const MAX_CHUNK_HEIGHT = 800; +const PADDING_X = 14; +const WIDTH_BUFFER = 8; +const CONCURRENT_DOWNLOADS = 30; + +const OUTPUT_DIR = join(__dirname, "..", "public", "fonts"); +const CACHE_DIR = join(__dirname, "..", ".font-cache"); +const FONTSOURCE_API = "https://api.fontsource.org/v1/fonts"; + +interface FontsourceFont { + id: string; + family: string; + subsets: string[]; + weights: number[]; + styles: string[]; + category: string; + type: string; +} + +interface MeasuredFont { + family: string; + width: number; + styles: string[]; +} + +interface PackedFont { + family: string; + x: number; + y: number; + w: number; +} + +interface AtlasEntry { + x: number; + y: number; + w: number; + ch: number; + s: string[]; +} + +async function fetchFontList(): Promise { + console.log("Fetching font list from Fontsource..."); + const response = await fetch(FONTSOURCE_API); + if (!response.ok) throw new Error(`Fontsource API error: ${response.status}`); + + const data: FontsourceFont[] = await response.json(); + const filtered = data.filter( + (font) => + font.type === "google" && + font.subsets.includes("latin") && + font.weights.includes(400), + ); + + console.log( + ` ${filtered.length} Google fonts with Latin subset + 400 weight`, + ); + return filtered; +} + +async function downloadFont({ id }: { id: string }): Promise { + const cachePath = join(CACHE_DIR, `${id}.woff2`); + + if (existsSync(cachePath)) { + return readFile(cachePath); + } + + // Fontsource CDN serves woff2 for all Google fonts + const url = `https://cdn.jsdelivr.net/fontsource/fonts/${id}@latest/latin-400-normal.woff2`; + try { + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const buffer = Buffer.from(await response.arrayBuffer()); + await writeFile(cachePath, buffer); + return buffer; + } catch { + return null; + } +} + +async function downloadAllFonts({ + fonts, + concurrency, +}: { + fonts: FontsourceFont[]; + concurrency: number; +}): Promise> { + console.log( + `Downloading ${fonts.length} font files (${concurrency} concurrent)...`, + ); + const results = new Map(); + let nextIndex = 0; + let completed = 0; + + async function worker() { + while (nextIndex < fonts.length) { + const index = nextIndex++; + const font = fonts[index]; + const buffer = await downloadFont({ id: font.id }); + if (buffer) results.set(font.id, buffer); + completed++; + if (completed % 100 === 0 || completed === fonts.length) { + process.stdout.write(`\r ${completed}/${fonts.length}`); + } + } + } + + await Promise.all(Array.from({ length: concurrency }, () => worker())); + console.log(`\n Downloaded ${results.size}/${fonts.length} fonts`); + return results; +} + +function measureFonts({ + fonts, + fontBuffers, +}: { + fonts: FontsourceFont[]; + fontBuffers: Map; +}): MeasuredFont[] { + console.log("Registering fonts and measuring text..."); + const measured: MeasuredFont[] = []; + const canvas = createCanvas(CANVAS_WIDTH, ROW_HEIGHT); + const ctx = canvas.getContext("2d"); + + for (const font of fonts) { + const buffer = fontBuffers.get(font.id); + if (!buffer) continue; + + try { + const ok = GlobalFonts.register(buffer, font.family); + if (!ok) continue; + + ctx.font = `${FONT_SIZE}px "${font.family}"`; + const metrics = ctx.measureText(font.family); + const width = Math.ceil(metrics.width) + WIDTH_BUFFER; + + const styles: string[] = []; + for (const weight of font.weights) { + if (font.styles.includes("normal")) styles.push(String(weight)); + if (font.styles.includes("italic")) styles.push(`${weight}i`); + } + + measured.push({ family: font.family, width, styles }); + } catch { + // skip fonts that fail to register + } + } + + measured.sort((a, b) => a.family.localeCompare(b.family)); + console.log(` ${measured.length} fonts measured`); + return measured; +} + +function packIntoChunks({ measured }: { measured: MeasuredFont[] }): { + atlas: Record; + chunks: PackedFont[][]; +} { + console.log("Packing into sprite chunks..."); + const atlas: Record = {}; + const chunks: PackedFont[][] = [[]]; + let chunkIndex = 0; + let cursorX = 0; + let cursorY = 0; + + for (const font of measured) { + // New row if doesn't fit horizontally + if (cursorX + font.width > CANVAS_WIDTH) { + cursorX = 0; + cursorY += ROW_HEIGHT; + } + + // New chunk if doesn't fit vertically + if (cursorY + ROW_HEIGHT > MAX_CHUNK_HEIGHT) { + chunkIndex++; + chunks.push([]); + cursorX = 0; + cursorY = 0; + } + + // Same data goes to BOTH the atlas and the chunk render list + const x = cursorX; + const y = cursorY; + + atlas[font.family] = { + x, + y, + w: font.width, + ch: chunkIndex, + s: font.styles, + }; + chunks[chunkIndex].push({ family: font.family, x, y, w: font.width }); + + cursorX += font.width + PADDING_X; + } + + console.log(` ${chunks.length} chunks`); + return { atlas, chunks }; +} + +async function renderChunks({ + chunks, +}: { + chunks: PackedFont[][]; +}): Promise { + console.log("Rendering sprite chunks..."); + + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + const chunkHeight = Math.max(...chunk.map((f) => f.y)) + ROW_HEIGHT; + + const canvas = createCanvas(CANVAS_WIDTH, chunkHeight); + const ctx = canvas.getContext("2d"); + + for (const font of chunk) { + ctx.font = `${FONT_SIZE}px "${font.family}"`; + ctx.fillStyle = "#000000"; + ctx.textBaseline = "middle"; + ctx.fillText(font.family, font.x, font.y + ROW_HEIGHT / 2); + } + + const pngBuffer = canvas.toBuffer("image/png"); + await sharp(pngBuffer) + .avif({ quality: 80 }) + .toFile(join(OUTPUT_DIR, `font-chunk-${i}.avif`)); + + console.log( + ` Chunk ${i}: ${chunk.length} fonts, ${CANVAS_WIDTH}×${chunkHeight}`, + ); + } +} + +async function main() { + await mkdir(OUTPUT_DIR, { recursive: true }); + await mkdir(CACHE_DIR, { recursive: true }); + + const fonts = await fetchFontList(); + const fontBuffers = await downloadAllFonts({ + fonts, + concurrency: CONCURRENT_DOWNLOADS, + }); + const measured = measureFonts({ fonts, fontBuffers }); + const { atlas, chunks } = packIntoChunks({ measured }); + await renderChunks({ chunks }); + + // Write atlas JSON (compact for smaller file size) + await writeFile( + join(OUTPUT_DIR, "font-atlas.json"), + JSON.stringify({ fonts: atlas }), + ); + + const totalFonts = Object.keys(atlas).length; + console.log( + `\nDone! ${totalFonts} fonts in ${chunks.length} chunks → ${OUTPUT_DIR}`, + ); +} + +main().catch((error) => { + console.error("Failed:", error); + process.exit(1); +}); diff --git a/apps/web/src/app/api/sounds/search/route.ts b/apps/web/src/app/api/sounds/search/route.ts index 3070d152..339a54da 100644 --- a/apps/web/src/app/api/sounds/search/route.ts +++ b/apps/web/src/app/api/sounds/search/route.ts @@ -1,280 +1,280 @@ -import { webEnv } from "@opencut/env/web"; -import { type NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { checkRateLimit } from "@/lib/rate-limit"; - -const searchParamsSchema = z.object({ - q: z.string().max(500, "Query too long").optional(), - type: z.enum(["songs", "effects"]).optional(), - page: z.coerce.number().int().min(1).max(1000).default(1), - page_size: z.coerce.number().int().min(1).max(150).default(20), - sort: z - .enum(["downloads", "rating", "created", "score"]) - .default("downloads"), - min_rating: z.coerce.number().min(0).max(5).default(3), - commercial_only: z.coerce.boolean().default(true), -}); - -const freesoundResultSchema = z.object({ - id: z.number(), - name: z.string(), - description: z.string(), - url: z.string().url(), - previews: z - .object({ - "preview-hq-mp3": z.string().url(), - "preview-lq-mp3": z.string().url(), - "preview-hq-ogg": z.string().url(), - "preview-lq-ogg": z.string().url(), - }) - .optional(), - download: z.string().url().optional(), - duration: z.number(), - filesize: z.number(), - type: z.string(), - channels: z.number(), - bitrate: z.number(), - bitdepth: z.number(), - samplerate: z.number(), - username: z.string(), - tags: z.array(z.string()), - license: z.string(), - created: z.string(), - num_downloads: z.number().optional(), - avg_rating: z.number().optional(), - num_ratings: z.number().optional(), -}); - -const freesoundResponseSchema = z.object({ - count: z.number(), - next: z.string().url().nullable(), - previous: z.string().url().nullable(), - results: z.array(freesoundResultSchema), -}); - -const transformedResultSchema = z.object({ - id: z.number(), - name: z.string(), - description: z.string(), - url: z.string(), - previewUrl: z.string().optional(), - downloadUrl: z.string().optional(), - duration: z.number(), - filesize: z.number(), - type: z.string(), - channels: z.number(), - bitrate: z.number(), - bitdepth: z.number(), - samplerate: z.number(), - username: z.string(), - tags: z.array(z.string()), - license: z.string(), - created: z.string(), - downloads: z.number().optional(), - rating: z.number().optional(), - ratingCount: z.number().optional(), -}); - -const apiResponseSchema = z.object({ - count: z.number(), - next: z.string().nullable(), - previous: z.string().nullable(), - results: z.array(transformedResultSchema), - query: z.string().optional(), - type: z.string(), - page: z.number(), - pageSize: z.number(), - sort: z.string(), - minRating: z.number().optional(), -}); - -function buildSortParameter({ query, sort }: { query?: string; sort: string }) { - if (!query) return `${sort}_desc`; - return sort === "score" ? "score" : `${sort}_desc`; -} - -function applyEffectsFilters({ - params, - min_rating, - commercial_only, -}: { - params: URLSearchParams; - min_rating: number; - commercial_only: boolean; -}) { - params.append("filter", "duration:[* TO 30.0]"); - params.append("filter", `avg_rating:[${min_rating} TO *]`); - - if (commercial_only) { - params.append( - "filter", - 'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")', - ); - } - - params.append( - "filter", - "tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion", - ); -} - -function transformFreesoundResult( - result: z.infer, -) { - return { - id: result.id, - name: result.name, - description: result.description, - url: result.url, - previewUrl: - result.previews?.["preview-hq-mp3"] || - result.previews?.["preview-lq-mp3"], - downloadUrl: result.download, - duration: result.duration, - filesize: result.filesize, - type: result.type, - channels: result.channels, - bitrate: result.bitrate, - bitdepth: result.bitdepth, - samplerate: result.samplerate, - username: result.username, - tags: result.tags, - license: result.license, - created: result.created, - downloads: result.num_downloads || 0, - rating: result.avg_rating || 0, - ratingCount: result.num_ratings || 0, - }; -} - -export async function GET(request: NextRequest) { - try { - const { limited } = await checkRateLimit({ request }); - if (limited) { - return NextResponse.json({ error: "Too many requests" }, { status: 429 }); - } - - const { searchParams } = new URL(request.url); - - const validationResult = searchParamsSchema.safeParse({ - q: searchParams.get("q") || undefined, - type: searchParams.get("type") || undefined, - page: searchParams.get("page") || undefined, - page_size: searchParams.get("page_size") || undefined, - sort: searchParams.get("sort") || undefined, - min_rating: searchParams.get("min_rating") || undefined, - }); - - if (!validationResult.success) { - return NextResponse.json( - { - error: "Invalid parameters", - details: validationResult.error.flatten().fieldErrors, - }, - { status: 400 }, - ); - } - - const { - q: query, - type, - page, - page_size: pageSize, - sort, - min_rating, - commercial_only, - } = validationResult.data; - - if (type === "songs") { - return NextResponse.json( - { - error: "Songs are not available yet", - message: - "Song search functionality is coming soon. Try searching for sound effects instead.", - }, - { status: 501 }, - ); - } - - const baseUrl = "https://freesound.org/apiv2/search/text/"; - - const sortParam = buildSortParameter({ query, sort }); - - const params = new URLSearchParams({ - query: query || "", - token: webEnv.FREESOUND_API_KEY, - page: page.toString(), - page_size: pageSize.toString(), - sort: sortParam, - fields: - "id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings", - }); - - const isEffectsSearch = type === "effects" || !type; - if (isEffectsSearch) { - applyEffectsFilters({ params, min_rating, commercial_only }); - } - - const response = await fetch(`${baseUrl}?${params.toString()}`); - - if (!response.ok) { - const errorText = await response.text(); - console.error("Freesound API error:", response.status, errorText); - return NextResponse.json( - { error: "Failed to search sounds" }, - { status: response.status }, - ); - } - - const rawData = await response.json(); - - const freesoundValidation = freesoundResponseSchema.safeParse(rawData); - if (!freesoundValidation.success) { - console.error( - "Invalid Freesound API response:", - freesoundValidation.error, - ); - return NextResponse.json( - { error: "Invalid response from Freesound API" }, - { status: 502 }, - ); - } - - const data = freesoundValidation.data; - - const transformedResults = data.results.map(transformFreesoundResult); - - const responseData = { - count: data.count, - next: data.next, - previous: data.previous, - results: transformedResults, - query: query || "", - type: type || "effects", - page, - pageSize, - sort, - minRating: min_rating, - }; - - const responseValidation = apiResponseSchema.safeParse(responseData); - if (!responseValidation.success) { - console.error( - "Invalid API response structure:", - responseValidation.error, - ); - return NextResponse.json( - { error: "Internal response formatting error" }, - { status: 500 }, - ); - } - - return NextResponse.json(responseValidation.data); - } catch (error) { - console.error("Error searching sounds:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 }, - ); - } -} +import { webEnv } from "@opencut/env/web"; +import { type NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { checkRateLimit } from "@/lib/rate-limit"; + +const searchParamsSchema = z.object({ + q: z.string().max(500, "Query too long").optional(), + type: z.enum(["songs", "effects"]).optional(), + page: z.coerce.number().int().min(1).max(1000).default(1), + page_size: z.coerce.number().int().min(1).max(150).default(20), + sort: z + .enum(["downloads", "rating", "created", "score"]) + .default("downloads"), + min_rating: z.coerce.number().min(0).max(5).default(3), + commercial_only: z.coerce.boolean().default(true), +}); + +const freesoundResultSchema = z.object({ + id: z.number(), + name: z.string(), + description: z.string(), + url: z.string().url(), + previews: z + .object({ + "preview-hq-mp3": z.string().url(), + "preview-lq-mp3": z.string().url(), + "preview-hq-ogg": z.string().url(), + "preview-lq-ogg": z.string().url(), + }) + .optional(), + download: z.string().url().optional(), + duration: z.number(), + filesize: z.number(), + type: z.string(), + channels: z.number(), + bitrate: z.number(), + bitdepth: z.number(), + samplerate: z.number(), + username: z.string(), + tags: z.array(z.string()), + license: z.string(), + created: z.string(), + num_downloads: z.number().optional(), + avg_rating: z.number().optional(), + num_ratings: z.number().optional(), +}); + +const freesoundResponseSchema = z.object({ + count: z.number(), + next: z.string().url().nullable(), + previous: z.string().url().nullable(), + results: z.array(freesoundResultSchema), +}); + +const transformedResultSchema = z.object({ + id: z.number(), + name: z.string(), + description: z.string(), + url: z.string(), + previewUrl: z.string().optional(), + downloadUrl: z.string().optional(), + duration: z.number(), + filesize: z.number(), + type: z.string(), + channels: z.number(), + bitrate: z.number(), + bitdepth: z.number(), + samplerate: z.number(), + username: z.string(), + tags: z.array(z.string()), + license: z.string(), + created: z.string(), + downloads: z.number().optional(), + rating: z.number().optional(), + ratingCount: z.number().optional(), +}); + +const apiResponseSchema = z.object({ + count: z.number(), + next: z.string().nullable(), + previous: z.string().nullable(), + results: z.array(transformedResultSchema), + query: z.string().optional(), + type: z.string(), + page: z.number(), + pageSize: z.number(), + sort: z.string(), + minRating: z.number().optional(), +}); + +function buildSortParameter({ query, sort }: { query?: string; sort: string }) { + if (!query) return `${sort}_desc`; + return sort === "score" ? "score" : `${sort}_desc`; +} + +function applyEffectsFilters({ + params, + min_rating, + commercial_only, +}: { + params: URLSearchParams; + min_rating: number; + commercial_only: boolean; +}) { + params.append("filter", "duration:[* TO 30.0]"); + params.append("filter", `avg_rating:[${min_rating} TO *]`); + + if (commercial_only) { + params.append( + "filter", + 'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")', + ); + } + + params.append( + "filter", + "tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion", + ); +} + +function transformFreesoundResult( + result: z.infer, +) { + return { + id: result.id, + name: result.name, + description: result.description, + url: result.url, + previewUrl: + result.previews?.["preview-hq-mp3"] || + result.previews?.["preview-lq-mp3"], + downloadUrl: result.download, + duration: result.duration, + filesize: result.filesize, + type: result.type, + channels: result.channels, + bitrate: result.bitrate, + bitdepth: result.bitdepth, + samplerate: result.samplerate, + username: result.username, + tags: result.tags, + license: result.license, + created: result.created, + downloads: result.num_downloads || 0, + rating: result.avg_rating || 0, + ratingCount: result.num_ratings || 0, + }; +} + +export async function GET(request: NextRequest) { + try { + const { limited } = await checkRateLimit({ request }); + if (limited) { + return NextResponse.json({ error: "Too many requests" }, { status: 429 }); + } + + const { searchParams } = new URL(request.url); + + const validationResult = searchParamsSchema.safeParse({ + q: searchParams.get("q") || undefined, + type: searchParams.get("type") || undefined, + page: searchParams.get("page") || undefined, + page_size: searchParams.get("page_size") || undefined, + sort: searchParams.get("sort") || undefined, + min_rating: searchParams.get("min_rating") || undefined, + }); + + if (!validationResult.success) { + return NextResponse.json( + { + error: "Invalid parameters", + details: validationResult.error.flatten().fieldErrors, + }, + { status: 400 }, + ); + } + + const { + q: query, + type, + page, + page_size: pageSize, + sort, + min_rating, + commercial_only, + } = validationResult.data; + + if (type === "songs") { + return NextResponse.json( + { + error: "Songs are not available yet", + message: + "Song search functionality is coming soon. Try searching for sound effects instead.", + }, + { status: 501 }, + ); + } + + const baseUrl = "https://freesound.org/apiv2/search/text/"; + + const sortParam = buildSortParameter({ query, sort }); + + const params = new URLSearchParams({ + query: query || "", + token: webEnv.FREESOUND_API_KEY, + page: page.toString(), + page_size: pageSize.toString(), + sort: sortParam, + fields: + "id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings", + }); + + const isEffectsSearch = type === "effects" || !type; + if (isEffectsSearch) { + applyEffectsFilters({ params, min_rating, commercial_only }); + } + + const response = await fetch(`${baseUrl}?${params.toString()}`); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Freesound API error:", response.status, errorText); + return NextResponse.json( + { error: "Failed to search sounds" }, + { status: response.status }, + ); + } + + const rawData = await response.json(); + + const freesoundValidation = freesoundResponseSchema.safeParse(rawData); + if (!freesoundValidation.success) { + console.error( + "Invalid Freesound API response:", + freesoundValidation.error, + ); + return NextResponse.json( + { error: "Invalid response from Freesound API" }, + { status: 502 }, + ); + } + + const data = freesoundValidation.data; + + const transformedResults = data.results.map(transformFreesoundResult); + + const responseData = { + count: data.count, + next: data.next, + previous: data.previous, + results: transformedResults, + query: query || "", + type: type || "effects", + page, + pageSize, + sort, + minRating: min_rating, + }; + + const responseValidation = apiResponseSchema.safeParse(responseData); + if (!responseValidation.success) { + console.error( + "Invalid API response structure:", + responseValidation.error, + ); + return NextResponse.json( + { error: "Internal response formatting error" }, + { status: 500 }, + ); + } + + return NextResponse.json(responseValidation.data); + } catch (error) { + console.error("Error searching sounds:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx index 2c06cb90..e1afc3a8 100644 --- a/apps/web/src/app/blog/[slug]/page.tsx +++ b/apps/web/src/app/blog/[slug]/page.tsx @@ -120,7 +120,9 @@ function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) { function PostTitle({ title }: { title: string }) { return ( -

{title}

+

+ {title} +

); } diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index 97093912..987af7c6 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -15,6 +15,7 @@ import { EditorProvider } from "@/components/providers/editor-provider"; import { Onboarding } from "@/components/editor/onboarding"; import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog"; import { usePanelStore } from "@/stores/panel-store"; +import { usePasteMedia } from "@/hooks/use-paste-media"; export default function Editor() { const params = useParams(); @@ -35,6 +36,7 @@ export default function Editor() { } function EditorLayout() { + usePasteMedia(); const { panels, setPanel } = usePanelStore(); return ( diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 37b5fe16..2d051c93 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -8,115 +8,115 @@ @plugin "tailwindcss-animate"; :root { - --background: hsl(0, 0%, 100%); - --foreground: hsl(0 0% 11%); - --card: hsl(0, 0%, 100%); - --card-foreground: hsl(0 0% 11%); - --popover: hsl(0, 0%, 100%); - --popover-hover: hsl(0, 0%, 96%); - --popover-foreground: hsl(0 0% 2%); - --primary: #009dff; - --primary-foreground: hsl(0, 0%, 100%); - --secondary: hsl(204, 100%, 97%); - --secondary-border: hsl(204, 100%, 94%); - --secondary-foreground: hsl(200, 98%, 39%); - --muted: hsl(0 0% 85.1%); - --muted-foreground: hsl(0 0% 50%); - --accent: hsl(0, 0%, 96%); - --accent-foreground: hsl(0 0% 2%); - --destructive: hsl(0, 83%, 50%); - --destructive-foreground: hsl(0, 0%, 100%); - --constructive: hsl(141, 71%, 48%); - --constructive-foreground: hsl(0, 0%, 100%); - --border: hsl(0 0% 91%); - --input: hsl(0 0% 85.1%); - --ring: hsl(0, 0%, 55%); - --chart-1: hsl(220 70% 50%); - --chart-2: hsl(160 60% 45%); - --chart-3: hsl(30 80% 55%); - --chart-4: hsl(280 65% 60%); - --chart-5: hsl(340 75% 55%); - --sidebar-background: hsl(0 0% 96.1%); - --sidebar-foreground: hsl(0 0% 2%); - --sidebar-primary: hsl(0 0% 2%); - --sidebar-primary-foreground: hsl(0 0% 91%); - --sidebar-accent: hsl(0 0% 85.1%); - --sidebar-accent-foreground: hsl(0 0% 2%); - --sidebar-border: hsl(0 0% 85.1%); - --sidebar-ring: hsl(0 0% 16.9%); - --sidebar: hsl(0 0% 98%); + --background: hsl(0, 0%, 100%); + --foreground: hsl(0 0% 11%); + --card: hsl(0, 0%, 100%); + --card-foreground: hsl(0 0% 11%); + --popover: hsl(0, 0%, 100%); + --popover-hover: hsl(0, 0%, 96%); + --popover-foreground: hsl(0 0% 2%); + --primary: #009dff; + --primary-foreground: hsl(0, 0%, 100%); + --secondary: hsl(204, 100%, 97%); + --secondary-border: hsl(204, 100%, 94%); + --secondary-foreground: hsl(200, 98%, 39%); + --muted: hsl(0 0% 85.1%); + --muted-foreground: hsl(0 0% 50%); + --accent: hsl(0, 0%, 96%); + --accent-foreground: hsl(0 0% 2%); + --destructive: hsl(0, 83%, 50%); + --destructive-foreground: hsl(0, 0%, 100%); + --constructive: hsl(141, 71%, 48%); + --constructive-foreground: hsl(0, 0%, 100%); + --border: hsl(0 0% 91%); + --input: hsl(0, 0%, 100%); + --ring: hsl(0, 0%, 55%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); + --sidebar-background: hsl(0 0% 96.1%); + --sidebar-foreground: hsl(0 0% 2%); + --sidebar-primary: hsl(0 0% 2%); + --sidebar-primary-foreground: hsl(0 0% 91%); + --sidebar-accent: hsl(0 0% 85.1%); + --sidebar-accent-foreground: hsl(0 0% 2%); + --sidebar-border: hsl(0 0% 85.1%); + --sidebar-ring: hsl(0 0% 16.9%); + --sidebar: hsl(0 0% 98%); } .panel { - --background: hsl(216 13% 98%); - --foreground: hsl(0 0% 13%); - --card: hsl(0, 0%, 98%); - --card-foreground: hsl(0 0% 13%); - --primary-foreground: hsl(0, 0%, 98%); - --secondary: hsl(204, 100%, 95%); - --secondary-border: hsl(204, 100%, 92%); - --secondary-foreground: hsl(200, 98%, 37%); - --muted: hsl(0 0% 83.1%); - --muted-foreground: hsl(0 0% 48%); - --accent: hsl(0, 0%, 93%); - --accent-foreground: hsl(0 0% 5%); - --destructive: hsl(0, 83%, 50%); - --destructive-foreground: hsl(0, 0%, 98%); - --constructive: hsl(141, 71%, 48%); - --constructive-foreground: hsl(0, 0%, 98%); - --border: hsl(0 0% 89%); - --input: hsl(0 0% 83.1%); - --ring: hsl(0, 0%, 53%); + --background: hsl(216 13% 98%); + --foreground: hsl(0 0% 13%); + --card: hsl(0, 0%, 98%); + --card-foreground: hsl(0 0% 13%); + --primary-foreground: hsl(0, 0%, 98%); + --secondary: hsl(204, 100%, 95%); + --secondary-border: hsl(204, 100%, 92%); + --secondary-foreground: hsl(200, 98%, 37%); + --muted: hsl(0 0% 83.1%); + --muted-foreground: hsl(0 0% 48%); + --accent: hsl(0, 0%, 93%); + --accent-foreground: hsl(0 0% 5%); + --destructive: hsl(0, 83%, 50%); + --destructive-foreground: hsl(0, 0%, 98%); + --constructive: hsl(141, 71%, 48%); + --constructive-foreground: hsl(0, 0%, 98%); + --border: hsl(0 0% 89%); + --input: hsl(0 0% 93%); + --ring: hsl(0, 0%, 53%); } .dark { - --background: hsl(0, 0%, 7%); - --foreground: hsl(0 0% 87%); - --card: hsl(0, 0%, 7%); - --card-foreground: hsl(0 0% 87%); - --popover: hsl(0, 0%, 16%); - --popover-hover: hsl(0, 0%, 22%); - --popover-foreground: hsl(0 0% 95%); - --secondary: hsl(204, 100%, 12%); - --secondary-border: hsl(204, 100%, 15%); - --secondary-foreground: hsl(200, 98%, 61%); - --muted: hsl(0 0% 20%); - --accent: hsl(0, 0%, 14%); - --accent-foreground: hsl(0 0% 95%); - --constructive: hsl(141, 71%, 52%); - --border: hsl(0 0% 16%); - --input: hsl(0 0% 20%); - --ring: hsl(0, 0%, 50%); - --sidebar-background: hsl(0 0% 8%); - --sidebar-foreground: hsl(0 0% 95%); - --sidebar-primary: hsl(0 0% 95%); - --sidebar-primary-foreground: hsl(0 0% 15%); - --sidebar-accent: hsl(0 0% 20%); - --sidebar-accent-foreground: hsl(0 0% 95%); - --sidebar-border: hsl(0 0% 20%); - --sidebar-ring: hsl(0 0% 83.1%); - --sidebar: hsl(0 0% 6%); + --background: hsl(0, 0%, 5%); + --foreground: hsl(0 0% 87%); + --card: hsl(0, 0%, 5%); + --card-foreground: hsl(0 0% 87%); + --popover: hsl(0, 0%, 5%); + --popover-hover: hsl(0, 0%, 22%); + --popover-foreground: hsl(0 0% 95%); + --secondary: hsl(204, 100%, 12%); + --secondary-border: hsl(204, 100%, 15%); + --secondary-foreground: hsl(200, 98%, 61%); + --muted: hsl(0 0% 20%); + --accent: hsl(0, 0%, 14%); + --accent-foreground: hsl(0 0% 95%); + --constructive: hsl(141, 71%, 52%); + --border: hsl(0 0% 16%); + --input: hsl(0 0% 5%); + --ring: hsl(0, 0%, 50%); + --sidebar-background: hsl(0 0% 8%); + --sidebar-foreground: hsl(0 0% 95%); + --sidebar-primary: hsl(0 0% 95%); + --sidebar-primary-foreground: hsl(0 0% 15%); + --sidebar-accent: hsl(0 0% 20%); + --sidebar-accent-foreground: hsl(0 0% 95%); + --sidebar-border: hsl(0 0% 20%); + --sidebar-ring: hsl(0 0% 83.1%); + --sidebar: hsl(0 0% 6%); } .dark .panel { - --background: hsl(0 0% 10%); - --foreground: hsl(0 0% 85%); - --card: hsl(0, 0%, 10%); - --card-foreground: hsl(0 0% 85%); - --secondary: hsl(204, 100%, 12%); - --secondary-border: hsl(204, 100%, 17%); - --secondary-foreground: hsl(200, 98%, 63%); - --muted: hsl(0 0% 22%); - --accent: hsl(0, 0%, 15%); - --accent-foreground: hsl(0 0% 93%); - --constructive: hsl(141, 71%, 52%); - --border: hsl(0 0% 18%); - --input: hsl(0 0% 22%); - --ring: hsl(0, 0%, 52%); + --background: hsl(0 0% 10%); + --foreground: hsl(0 0% 85%); + --card: hsl(0, 0%, 10%); + --card-foreground: hsl(0 0% 85%); + --secondary: hsl(204, 100%, 12%); + --secondary-border: hsl(204, 100%, 17%); + --secondary-foreground: hsl(200, 98%, 63%); + --muted: hsl(0 0% 22%); + --accent: hsl(0, 0%, 15%); + --accent-foreground: hsl(0 0% 93%); + --constructive: hsl(141, 71%, 52%); + --border: hsl(0 0% 18%); + --input: hsl(0 0% 22%); + --ring: hsl(0, 0%, 52%); } @layer base { - /* + /* The default border color has changed to `currentcolor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the same as it did with Tailwind CSS v3. @@ -124,166 +124,165 @@ If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ - *, - ::after, - ::before, - ::backdrop, - ::file-selector-button { - border-color: var(--color-gray-200, currentcolor); - } - /* Other default base styles */ - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - /* Prevent back/forward swipe */ - overscroll-behavior-x: contain; - } - ::selection { - @apply bg-primary/35 selection:text-primary-foreground; - } + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + /* Other default base styles */ + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + /* Prevent back/forward swipe */ + overscroll-behavior-x: contain; + } + ::selection { + @apply bg-primary/35 selection:text-primary-foreground; + } } @theme inline { - /* Responsive breakpoints */ - --breakpoint-xs: 30rem; + /* Responsive breakpoints */ + --breakpoint-xs: 30rem; - /* Typography */ - --font-sans: var(--font-inter), sans-serif; + /* Typography */ + --font-sans: var(--font-inter), sans-serif; - /* Font sizes */ - --text-xl: 1.2rem; - --text-base: 0.92rem; - --text-base--line-height: calc(1.5 / 0.95); - --text-xs: 0.75rem; - --text-sm: 0.85rem; - --text-xs--line-height: calc(1 / 0.8); + /* Font sizes */ + --text-xs: 0.72rem; + --text-sm: 0.79rem; + --text-base: 0.92rem; + --text-base--line-height: calc(1.5 / 0.95); + --text-xs--line-height: calc(1 / 0.8); - /* Border radius */ - --radius-lg: 0.82rem; - --radius-md: 0.65rem; - --radius-sm: 0.35rem; + /* Border radius */ + --radius-lg: 0.82rem; + --radius-md: 0.65rem; + --radius-sm: 0.35rem; - /* Palette mapped to root design tokens */ - --color-background: var(--background); - --color-foreground: var(--foreground); + /* Palette mapped to root design tokens */ + --color-background: var(--background); + --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-hover: var(--popover-hover); - --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-popover-hover: var(--popover-hover); + --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-border: var(--secondary-border); - --color-secondary-foreground: var(--secondary-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-border: var(--secondary-border); + --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); - --color-constructive: var(--constructive); - --color-constructive-foreground: var(--constructive-foreground); + --color-constructive: var(--constructive); + --color-constructive-foreground: var(--constructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); - /* Chart colors */ - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); + /* Chart colors */ + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); - /* Sidebar */ - --color-sidebar: var(--sidebar-background); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + /* Sidebar */ + --color-sidebar: var(--sidebar-background); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); - /* Animations */ - --animate-accordion-down: accordion-down 0.2s ease-out; - --animate-accordion-up: accordion-up 0.2s ease-out; + /* Animations */ + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; - @keyframes accordion-down { - from { - height: 0; - } - to { - height: var(--radix-accordion-content-height); - } - } + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } - @keyframes accordion-up { - from { - height: var(--radix-accordion-content-height); - } - to { - height: 0; - } - } + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } } @utility scrollbar-hidden { - -ms-overflow-style: none; - scrollbar-width: none; - &::-webkit-scrollbar { - display: none; - } + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } } @utility scrollbar-x-hidden { - -ms-overflow-style: none; - scrollbar-width: none; - &::-webkit-scrollbar:horizontal { - display: none; - } + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar:horizontal { + display: none; + } } @utility scrollbar-y-hidden { - -ms-overflow-style: none; - scrollbar-width: none; - &::-webkit-scrollbar:vertical { - display: none; - } + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar:vertical { + display: none; + } } @utility scrollbar-thin { - &::-webkit-scrollbar { - width: 6px; - height: 8px; - } - &::-webkit-scrollbar-track { - background: transparent; - } - &::-webkit-scrollbar-thumb { - background: var(--border); - border-radius: 4px; - } - &::-webkit-scrollbar-thumb:hover { - background: var(--muted-foreground); - } + &::-webkit-scrollbar { + width: 6px; + height: 8px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; + } + &::-webkit-scrollbar-thumb:hover { + background: var(--muted-foreground); + } } @layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } } diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx index 492e0cbe..b855cefc 100644 --- a/apps/web/src/app/projects/page.tsx +++ b/apps/web/src/app/projects/page.tsx @@ -245,7 +245,6 @@ function ProjectsToolbar({ projectIds }: { projectIds: string[] }) { - + } > - Exit project - setOpenDialog("shortcuts")} + icon={} > - - Keyboard shortcuts + Shortcuts - + + + + }> - Discord diff --git a/apps/web/src/components/editor/export-button.tsx b/apps/web/src/components/editor/export-button.tsx index f39c18e2..bc44e650 100644 --- a/apps/web/src/components/editor/export-button.tsx +++ b/apps/web/src/components/editor/export-button.tsx @@ -23,7 +23,11 @@ import { type ExportQuality, type ExportResult, } from "@/types/export"; -import { PropertyGroup } from "@/components/editor/panels/properties/property-item"; +import { + Section, + SectionContent, + SectionHeader, +} from "@/components/editor/panels/properties/section"; import { useEditor } from "@/hooks/use-editor"; import { DEFAULT_EXPORT_OPTIONS } from "@/constants/export-constants"; @@ -162,78 +166,83 @@ function ExportPopover({ {!isExporting && ( <>
- - { - if (isExportFormat(value)) { - setFormat(value); - } - }} - > -
- - -
-
- - -
-
-
+
+ + + { + if (isExportFormat(value)) { + setFormat(value); + } + }} + > +
+ + +
+
+ + +
+
+
+
- - { - if (isExportQuality(value)) { - setQuality(value); - } - }} - > +
+ + + { + if (isExportQuality(value)) { + setQuality(value); + } + }} + > +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+ +
- - -
-
- - -
-
- - -
-
- -
- - - - -
- - setIncludeAudio(!!checked) - } - /> - -
-
+
+
diff --git a/apps/web/src/components/editor/panels/assets/draggable-item.tsx b/apps/web/src/components/editor/panels/assets/draggable-item.tsx index 517fdc92..be4176ce 100644 --- a/apps/web/src/components/editor/panels/assets/draggable-item.tsx +++ b/apps/web/src/components/editor/panels/assets/draggable-item.tsx @@ -163,7 +163,9 @@ export function DraggableItem({
{preview}
- {name} + + {name} +
)} diff --git a/apps/web/src/components/editor/panels/assets/index.tsx b/apps/web/src/components/editor/panels/assets/index.tsx index e7bd3d4f..3600d00b 100644 --- a/apps/web/src/components/editor/panels/assets/index.tsx +++ b/apps/web/src/components/editor/panels/assets/index.tsx @@ -4,7 +4,7 @@ import { Separator } from "@/components/ui/separator"; import { type Tab, useAssetsPanelStore } from "@/stores/assets-panel-store"; import { TabBar } from "./tabbar"; import { Captions } from "./views/captions"; -import { MediaView } from "./views/media"; +import { MediaView } from "./views/assets"; import { SettingsView } from "./views/settings"; import { SoundsView } from "./views/sounds"; import { StickersView } from "./views/stickers"; diff --git a/apps/web/src/components/editor/panels/assets/tabbar.tsx b/apps/web/src/components/editor/panels/assets/tabbar.tsx index 31842306..61777a7c 100644 --- a/apps/web/src/components/editor/panels/assets/tabbar.tsx +++ b/apps/web/src/components/editor/panels/assets/tabbar.tsx @@ -61,7 +61,8 @@ export function TabBar() { aria-label={tab.label} className={cn( "flex-col !p-1.5 !rounded-sm !h-auto [&_svg]:size-4.5", - activeTab !== tabKey && "border border-transparent text-muted-foreground", + activeTab !== tabKey && + "border border-transparent text-muted-foreground", )} onClick={() => setActiveTab(tabKey)} > diff --git a/apps/web/src/components/editor/panels/assets/views/media.tsx b/apps/web/src/components/editor/panels/assets/views/assets.tsx similarity index 67% rename from apps/web/src/components/editor/panels/assets/views/media.tsx rename to apps/web/src/components/editor/panels/assets/views/assets.tsx index 59395fd8..808961f1 100644 --- a/apps/web/src/components/editor/panels/assets/views/media.tsx +++ b/apps/web/src/components/editor/panels/assets/views/assets.tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import { useMemo, useState } from "react"; import { toast } from "sonner"; +import { PanelView } from "@/components/editor/panels/assets/views/base-view"; import { MediaDragOverlay } from "@/components/editor/panels/assets/drag-overlay"; import { DraggableItem } from "@/components/editor/panels/assets/draggable-item"; import { Button } from "@/components/ui/button"; @@ -29,14 +30,9 @@ import { useEditor } from "@/hooks/use-editor"; import { useFileUpload } from "@/hooks/use-file-upload"; import { useRevealItem } from "@/hooks/use-reveal-item"; import { processMediaAssets } from "@/lib/media/processing"; -import { - buildImageElement, - buildUploadAudioElement, - buildVideoElement, -} from "@/lib/timeline/element-utils"; +import { buildElementFromMedia } from "@/lib/timeline/element-utils"; import { useAssetsPanelStore } from "@/stores/assets-panel-store"; import type { MediaAsset } from "@/types/assets"; -import type { CreateTimelineElement } from "@/types/timeline"; import { cn } from "@/utils/ui"; import { CloudUploadIcon, @@ -132,7 +128,15 @@ export function MediaView() { asset: MediaAsset; startTime: number; }): boolean => { - const element = createElementFromMedia({ asset, startTime }); + const duration = + asset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION; + const element = buildElementFromMedia({ + mediaId: asset.id, + mediaType: asset.type, + name: asset.name, + duration, + startTime, + }); editor.timeline.insertElement({ element, placement: { mode: "auto" }, @@ -194,171 +198,166 @@ export function MediaView() { const renderCompactPreview = (item: MediaAsset) => previewComponents.get(`compact-${item.id}`); + const mediaActions = ( +
+ + + + + + +

+ {mediaViewMode === "grid" + ? "Switch to list view" + : "Switch to grid view"} +

+
+ + + + + + + + + { + if (sortBy === key) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + } else { + setSortBy(key); + setSortOrder("asc"); + } + }} + /> + { + if (sortBy === key) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + } else { + setSortBy(key); + setSortOrder("asc"); + } + }} + /> + { + if (sortBy === key) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + } else { + setSortBy(key); + setSortOrder("asc"); + } + }} + /> + { + if (sortBy === key) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + } else { + setSortBy(key); + setSortOrder("asc"); + } + }} + /> + + + +

+ Sort by {sortBy} ( + {sortOrder === "asc" ? "ascending" : "descending"}) +

+
+
+
+
+ +
+ ); + return ( <> -
-
- Assets -
- - - - - - -

- {mediaViewMode === "grid" - ? "Switch to list view" - : "Switch to grid view"} -

-
- - - - - - - - - { - if (sortBy === key) { - setSortOrder(sortOrder === "asc" ? "desc" : "asc"); - } else { - setSortBy(key); - setSortOrder("asc"); - } - }} - /> - { - if (sortBy === key) { - setSortOrder(sortOrder === "asc" ? "desc" : "asc"); - } else { - setSortBy(key); - setSortOrder("asc"); - } - }} - /> - { - if (sortBy === key) { - setSortOrder(sortOrder === "asc" ? "desc" : "asc"); - } else { - setSortBy(key); - setSortOrder("asc"); - } - }} - /> - { - if (sortBy === key) { - setSortOrder(sortOrder === "asc" ? "desc" : "asc"); - } else { - setSortBy(key); - setSortOrder("asc"); - } - }} - /> - - - -

- Sort by {sortBy} ( - {sortOrder === "asc" ? "ascending" : "descending"}) -

-
-
-
-
- -
-
- -
-
- {isDragOver || filteredMediaItems.length === 0 ? ( - - ) : mediaViewMode === "grid" ? ( - - ) : ( - - )} -
-
-
+ {isDragOver || filteredMediaItems.length === 0 ? ( + + ) : mediaViewMode === "grid" ? ( + + ) : ( + + )} + ); } @@ -636,40 +635,3 @@ function SortMenuItem({
); } - -function createElementFromMedia({ - asset, - startTime, -}: { - asset: MediaAsset; - startTime: number; -}): CreateTimelineElement { - const duration = - asset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION; - - switch (asset.type) { - case "video": - return buildVideoElement({ - mediaId: asset.id, - name: asset.name, - duration, - startTime, - }); - case "image": - return buildImageElement({ - mediaId: asset.id, - name: asset.name, - duration, - startTime, - }); - case "audio": - return buildUploadAudioElement({ - mediaId: asset.id, - name: asset.name, - duration, - startTime, - }); - default: - throw new Error(`Unsupported media type: ${asset.type}`); - } -} diff --git a/apps/web/src/components/editor/panels/assets/views/base-view.tsx b/apps/web/src/components/editor/panels/assets/views/base-view.tsx new file mode 100644 index 00000000..c613bc2a --- /dev/null +++ b/apps/web/src/components/editor/panels/assets/views/base-view.tsx @@ -0,0 +1,52 @@ +import { cn } from "@/utils/ui"; + +interface PanelViewProps extends React.HTMLAttributes { + title?: string; + actions?: React.ReactNode; + children: React.ReactNode; + contentClassName?: string; + hideHeader?: boolean; + ref?: React.Ref; + onScroll?: React.UIEventHandler; + scrollRef?: React.Ref; +} + +export function PanelView({ + title, + actions, + children, + className, + contentClassName, + hideHeader = false, + ref, + onScroll, + scrollRef, + ...rest +}: PanelViewProps) { + return ( +
+ {!hideHeader && ( +
+ {title} + {actions} +
+ )} +
+
+ {children} +
+
+
+ ); +} diff --git a/apps/web/src/components/editor/panels/assets/views/captions.tsx b/apps/web/src/components/editor/panels/assets/views/captions.tsx index daebf2c6..7a3b16bf 100644 --- a/apps/web/src/components/editor/panels/assets/views/captions.tsx +++ b/apps/web/src/components/editor/panels/assets/views/captions.tsx @@ -1,5 +1,5 @@ import { Button } from "@/components/ui/button"; -import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view"; +import { PanelView } from "@/components/editor/panels/assets/views/base-view"; import { Select, SelectContent, @@ -108,10 +108,7 @@ export function Captions() { }; return ( - +
handleAspectRatioChange({ value })} + > + + + + + Original + {canvasPresets.map((preset, index) => { + const label = dimensionToAspectRatio({ + width: preset.width, + height: preset.height, + }); + return ( + + {label} + + ); + })} + + + + + + + Frame rate + + + + +
+ ); +} + +const BlurPreview = memo( + ({ + blur, + isSelected, + onSelect, + }: { + blur: { label: string; value: number }; + isSelected: boolean; + onSelect: () => void; + }) => ( + + ), +); + +BlurPreview.displayName = "BlurPreview"; + +const BackgroundPreviews = memo( + ({ + backgrounds, + currentBackgroundColor, + isColorBackground, + handleColorSelect, + useBackgroundColor = false, + }: { + backgrounds: string[]; + currentBackgroundColor: string; + isColorBackground: boolean; + handleColorSelect: ({ bg }: { bg: string }) => void; + useBackgroundColor?: boolean; + }) => { + return useMemo( + () => + backgrounds.map((bg, index) => ( + - ), -); - -BlurPreview.displayName = "BlurPreview"; - -const BackgroundPreviews = memo( - ({ - backgrounds, - currentBackgroundColor, - isColorBackground, - handleColorSelect, - useBackgroundColor = false, - }: { - backgrounds: string[]; - currentBackgroundColor: string; - isColorBackground: boolean; - handleColorSelect: ({ bg }: { bg: string }) => void; - useBackgroundColor?: boolean; - }) => { - return useMemo( - () => - backgrounds.map((bg, index) => ( - + + + + } + > + +
); } function StickerGrid({ - icons, - onAdd, - addingSticker, - capSize = false, + items, + shouldCapSize = false, }: { - icons: string[]; - onAdd: (iconName: string) => void; - addingSticker: string | null; - capSize?: boolean; + items: StickerData[]; + shouldCapSize?: boolean; }) { const gridStyle: CSSProperties & { "--sticker-min": string; "--sticker-max"?: string; } = { - gridTemplateColumns: capSize + gridTemplateColumns: shouldCapSize ? "repeat(auto-fill, minmax(var(--sticker-min, 96px), var(--sticker-max, 160px)))" : "repeat(auto-fit, minmax(var(--sticker-min, 96px), 1fr))", "--sticker-min": "96px", - ...(capSize ? { "--sticker-max": "160px" } : {}), + ...(shouldCapSize ? { "--sticker-max": "160px" } : {}), }; return (
- {icons.map((iconName) => ( - - ))} -
- ); -} - -function CollectionGrid({ - collections, - onSelectCollection, -}: { - collections: Array<{ - prefix: string; - name: string; - total: number; - category?: string; - }>; - onSelectCollection: ({ prefix }: { prefix: string }) => void; -}) { - return ( -
- {collections.map((collection) => ( - onSelectCollection({ prefix: collection.prefix })} - /> + {items.map((item) => ( + ))}
); @@ -162,372 +122,125 @@ function EmptyView({ message }: { message: string }) { ); } -function StickersContentView({ category }: { category: StickerCategory }) { +function StickersContentView() { const { searchQuery, - selectedCollection, viewMode, - collections, - currentCollection, searchResults, recentStickers, - isLoadingCollections, - isLoadingCollection, isSearching, - setSearchQuery, - setSelectedCollection, - loadCollections, - searchStickers, - addStickerToTimeline, clearRecentStickers, - setSelectedCategory, - addingSticker, } = useStickersStore(); - const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery); - const [collectionsToShow, setCollectionsToShow] = useState(20); - const [showCollectionItems, setShowCollectionItems] = useState(false); - - const filteredCollections = useMemo(() => { - if (category === "all") { - return Object.entries(collections).map(([prefix, collection]) => ({ - prefix, - name: collection.name, - total: collection.total, - category: collection.category, - })); + const itemsToDisplay = useMemo(() => { + if (viewMode === "search" && searchResults) { + return searchResults.items; } - const collectionList = - POPULAR_COLLECTIONS[category as keyof typeof POPULAR_COLLECTIONS]; - if (!collectionList) return []; + return []; + }, [viewMode, searchResults]); - return collectionList - .map((c) => { - const collection = collections[c.prefix]; - return collection - ? { - prefix: c.prefix, - name: c.name, - total: collection.total, - } - : null; - }) - .filter(Boolean) as Array<{ - prefix: string; - name: string; - total: number; - }>; - }, [collections, category]); - - const { scrollAreaRef, handleScroll } = useInfiniteScroll({ - onLoadMore: () => setCollectionsToShow((prev) => prev + 20), - hasMore: filteredCollections.length > collectionsToShow, - isLoading: isLoadingCollections, - enabled: viewMode === "browse" && !selectedCollection && category === "all", - }); - - useEffect(() => { - if (Object.keys(collections).length === 0) { - loadCollections(); - } - }, [collections, loadCollections]); - - useEffect(() => { - const timer = setTimeout(() => { - if (localSearchQuery !== searchQuery) { - setSearchQuery({ query: localSearchQuery }); - if (localSearchQuery.trim()) { - searchStickers({ query: localSearchQuery }); - } + const recentStickerItems = useMemo(() => { + const items: StickerData[] = []; + for (const stickerId of recentStickers) { + const recentStickerItem = toRecentStickerItem({ stickerId }); + if (recentStickerItem) { + items.push(recentStickerItem); } - }, 500); + } + return items; + }, [recentStickers]); - return () => clearTimeout(timer); - }, [localSearchQuery, searchQuery, searchStickers, setSearchQuery]); + return ( +
+ {recentStickerItems.length > 0 && viewMode === "browse" && ( +
+
+ + Recent + + + + + + +

Clear recent stickers

+
+
+
+
+ +
+ )} - const handleAddSticker = async (iconName: string) => { + {viewMode === "search" && ( +
+ {isSearching ? ( +
+ +
+ ) : searchResults?.items.length ? ( +
+
+ + {searchResults.total} results + +
+ +
+ ) : searchQuery ? ( + + ) : null} +
+ )} +
+ ); +} + +interface StickerItemProps { + item: StickerData; + shouldCapSize?: boolean; +} + +function StickerItem({ item, shouldCapSize = false }: StickerItemProps) { + const { addingSticker, addStickerToTimeline } = useStickersStore(); + const isAdding = addingSticker === item.id; + const [hasImageError, setHasImageError] = useState(false); + + useEffect(() => { + if (!item.id) { + return; + } + setHasImageError(false); + }, [item.id]); + + const displayName = item.name; + + const handleAdd = async () => { try { - await addStickerToTimeline({ iconName }); + await addStickerToTimeline({ + stickerId: item.id, + name: item.name, + }); } catch (error) { console.error("Failed to add sticker:", error); toast.error("Failed to add sticker to timeline"); } }; - const iconsToDisplay = useMemo(() => { - if (viewMode === "search" && searchResults) { - return searchResults.icons; - } - - if (viewMode === "collection" && currentCollection) { - const icons: string[] = []; - - if (currentCollection.uncategorized) { - icons.push( - ...currentCollection.uncategorized.map( - (name) => `${currentCollection.prefix}:${name}`, - ), - ); - } - - if (currentCollection.categories) { - Object.values(currentCollection.categories).forEach((categoryIcons) => { - icons.push( - ...categoryIcons.map( - (name) => `${currentCollection.prefix}:${name}`, - ), - ); - }); - } - - return icons.slice(0, 100); - } - - return []; - }, [viewMode, searchResults, currentCollection]); - - const isInCollection = viewMode === "collection" && !!selectedCollection; - - useEffect(() => { - if (isInCollection) { - setShowCollectionItems(false); - const timer = setTimeout(() => setShowCollectionItems(true), 350); - return () => clearTimeout(timer); - } else { - setShowCollectionItems(false); - } - }, [isInCollection]); - - return ( -
-
- { - if (!expanded && isInCollection) { - setSelectedCollection({ collection: null }); - } - }} - placeholder={ - category === "all" - ? "Search all stickers" - : category === "general" - ? "Search icons" - : category === "brands" - ? "Search brands" - : "Search Emojis" - } - value={localSearchQuery} - onChange={setLocalSearchQuery} - disableAnimation={true} - /> -
- -
- -
- {recentStickers.length > 0 && viewMode === "browse" && ( -
-
- - Recent - - - - - - -

Clear recent stickers

-
-
-
-
- -
- )} - - {viewMode === "collection" && selectedCollection && ( -
- {isLoadingCollection ? ( -
- -
- ) : showCollectionItems ? ( - - ) : ( -
- -
- )} -
- )} - - {viewMode === "search" && ( -
- {isSearching ? ( -
- -
- ) : searchResults?.icons.length ? ( - <> -
- - {searchResults.total} results - -
- - - ) : searchQuery ? ( -
- - {category !== "all" && ( - - )} -
- ) : null} -
- )} - - {viewMode === "browse" && !selectedCollection && ( -
- {isLoadingCollections ? ( -
- -
- ) : ( - <> - {category !== "all" && ( -
- - setSelectedCollection({ collection: prefix }) - } - /> -
- )} - - {category === "all" && filteredCollections.length > 0 && ( -
- - setSelectedCollection({ collection: prefix }) - } - /> -
- )} - - )} -
- )} -
-
-
-
- ); -} - -interface CollectionItemProps { - title: string; - subtitle: string; - onClick: () => void; -} - -function CollectionItem({ title, subtitle, onClick }: CollectionItemProps) { - return ( - - ); -} - -interface StickerItemProps { - iconName: string; - onAdd: (iconName: string) => void; - isAdding?: boolean; - capSize?: boolean; -} - -function StickerItem({ - iconName, - onAdd, - isAdding, - capSize = false, -}: StickerItemProps) { - const [imageError, setImageError] = useState(false); - const [hostIndex, setHostIndex] = useState(0); - - useEffect(() => { - if (!iconName) { - return; - } - setImageError(false); - setHostIndex(0); - }, [iconName]); - - const displayName = iconName.split(":")[1] || iconName; - const collectionPrefix = iconName.split(":")[0]; - - const preview = imageError ? ( + const preview = hasImageError ? (
{displayName} @@ -536,21 +249,13 @@ function StickerItem({ ) : (
{displayName} { - const next = hostIndex + 1; - if (next < ICONIFY_HOSTS.length) { - setHostIndex(next); - } else { - setImageError(true); - } + setHasImageError(true); }} loading="lazy" unoptimized @@ -572,44 +272,63 @@ function StickerItem({ ); return ( - - -
- onAdd(iconName)} - aspectRatio={1} - shouldShowLabel={false} - isRounded={true} - variant="card" - className="" - containerClassName="w-full" - /> - {isAdding && ( -
- -
- )} +
+ + {isAdding && ( +
+
- - -
-

{displayName}

-

{collectionPrefix}

-
-
- + )} +
); } + +function getStickerNameFromId({ stickerId }: { stickerId: string }): string { + const stickerIdParts = stickerId.split(":"); + if (stickerIdParts.length <= 1) { + return stickerId; + } + return ( + stickerIdParts.slice(1).join(":").split(":").pop()?.replaceAll("-", " ") ?? + stickerId + ); +} + +function toRecentStickerItem({ + stickerId, +}: { + stickerId: string; +}): StickerData | null { + try { + const { providerId } = parseStickerId({ stickerId }); + return { + id: stickerId, + provider: providerId, + name: getStickerNameFromId({ stickerId }), + previewUrl: resolveStickerId({ + stickerId, + options: { width: 64, height: 64 }, + }), + metadata: {}, + }; + } catch { + return null; + } +} diff --git a/apps/web/src/components/editor/panels/assets/views/text.tsx b/apps/web/src/components/editor/panels/assets/views/text.tsx index 6360c363..3673acc3 100644 --- a/apps/web/src/components/editor/panels/assets/views/text.tsx +++ b/apps/web/src/components/editor/panels/assets/views/text.tsx @@ -1,5 +1,5 @@ import { DraggableItem } from "@/components/editor/panels/assets/draggable-item"; -import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view"; +import { PanelView } from "@/components/editor/panels/assets/views/base-view"; import { useEditor } from "@/hooks/use-editor"; import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants"; import { buildTextElement } from "@/lib/timeline/element-utils"; @@ -23,7 +23,7 @@ export function TextView() { }; return ( - + - + ); } diff --git a/apps/web/src/components/editor/panels/panel-base-view.tsx b/apps/web/src/components/editor/panels/panel-base-view.tsx deleted file mode 100644 index dd6151d3..00000000 --- a/apps/web/src/components/editor/panels/panel-base-view.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { cn } from "@/utils/ui"; - -interface PanelBaseViewProps { - children?: React.ReactNode; - defaultTab?: string; - value?: string; - onValueChange?: (value: string) => void; - tabs?: { - value: string; - label: string; - icon?: React.ReactNode; - content: React.ReactNode; - }[]; - className?: string; - ref?: React.Ref; -} - -function ViewContent({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ( - -
{children}
-
- ); -} - -export function PanelBaseView({ - children, - defaultTab, - value, - onValueChange, - tabs, - className = "", - ref, -}: PanelBaseViewProps) { - return ( -
- {!tabs || tabs.length === 0 ? ( - {children} - ) : ( - -
-
- - {tabs.map((tab) => ( - - {tab.icon ? ( - - {tab.icon} - - ) : null} - {tab.label} - - ))} - -
- -
- {tabs.map((tab) => ( - - {tab.content} - - ))} -
- )} -
- ); -} diff --git a/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx b/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx new file mode 100644 index 00000000..4ace25ac --- /dev/null +++ b/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEditor } from "@/hooks/use-editor"; +import { findCurrentScene } from "@/lib/scenes"; +import { getBookmarksActiveAtTime } from "@/lib/timeline/bookmarks"; + +export function BookmarkNoteOverlay() { + const editor = useEditor(); + const currentTime = editor.playback.getCurrentTime(); + const activeProject = editor.project.getActive(); + + if (!activeProject) { + return null; + } + + const activeScene = findCurrentScene({ + scenes: activeProject.scenes, + currentSceneId: activeProject.currentSceneId, + }); + + if (!activeScene) { + return null; + } + + const bookmarks = activeScene.bookmarks; + const activeBookmarks = getBookmarksActiveAtTime({ + bookmarks, + time: currentTime, + }); + const bookmarksWithNotes = activeBookmarks.filter( + (bookmark) => bookmark.note != null && bookmark.note.trim() !== "", + ); + + if (bookmarksWithNotes.length === 0) { + return null; + } + + return ( +
+ {bookmarksWithNotes.map((bookmark) => ( +
+ {bookmark.note} +
+ ))} +
+ ); +} diff --git a/apps/web/src/components/editor/panels/preview/context-menu.tsx b/apps/web/src/components/editor/panels/preview/context-menu.tsx new file mode 100644 index 00000000..d5cf535a --- /dev/null +++ b/apps/web/src/components/editor/panels/preview/context-menu.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { + ContextMenuCheckboxItem, + ContextMenuContent, + ContextMenuItem, +} from "@/components/ui/context-menu"; +import { usePreviewStore } from "@/stores/preview-store"; + +export function PreviewContextMenu({ + onToggleFullscreen, + containerRef, +}: { + onToggleFullscreen: () => void; + containerRef: React.RefObject; +}) { + const { overlays, setOverlayVisibility } = usePreviewStore(); + + return ( + + + Full screen + + + setOverlayVisibility({ overlay: "bookmarks", isVisible: !!checked }) + } + > + Show bookmarks + + Show grid + + ); +} diff --git a/apps/web/src/components/editor/panels/preview/index.tsx b/apps/web/src/components/editor/panels/preview/index.tsx index 902547bd..90cb11a3 100644 --- a/apps/web/src/components/editor/panels/preview/index.tsx +++ b/apps/web/src/components/editor/panels/preview/index.tsx @@ -9,18 +9,13 @@ import { useFullscreen } from "@/hooks/use-fullscreen"; import { CanvasRenderer } from "@/services/renderer/canvas-renderer"; import type { RootNode } from "@/services/renderer/nodes/root-node"; import { buildScene } from "@/services/renderer/scene-builder"; -import { formatTimeCode, getLastFrameTime } from "@/lib/time"; +import { getLastFrameTime } from "@/lib/time"; import { PreviewInteractionOverlay } from "./preview-interaction-overlay"; -import { EditableTimecode } from "@/components/editable-timecode"; -import { invokeAction } from "@/lib/actions"; -import { Button } from "@/components/ui/button"; -import { - FullScreenIcon, - PauseIcon, - PlayIcon, -} from "@hugeicons/core-free-icons"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { cn } from "@/utils/ui"; +import { BookmarkNoteOverlay } from "./bookmark-note-overlay"; +import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu"; +import { usePreviewStore } from "@/stores/preview-store"; +import { PreviewContextMenu } from "./context-menu"; +import { PreviewToolbar } from "./toolbar"; function usePreviewSize() { const editor = useEditor(); @@ -32,6 +27,30 @@ function usePreviewSize() { }; } +export function PreviewPanel() { + const containerRef = useRef(null); + const { isFullscreen, toggleFullscreen } = useFullscreen({ containerRef }); + + return ( +
+
+ + +
+ +
+ ); +} + function RenderTreeController() { const editor = useEditor(); const tracks = editor.timeline.getTracks(); @@ -58,98 +77,24 @@ function RenderTreeController() { return null; } -export function PreviewPanel() { - const containerRef = useRef(null); - const { isFullscreen, toggleFullscreen } = useFullscreen({ containerRef }); - - return ( -
-
- - -
- -
- ); -} - -function PreviewToolbar({ - isFullscreen, +function PreviewCanvas({ onToggleFullscreen, + containerRef, }: { - isFullscreen: boolean; onToggleFullscreen: () => void; + containerRef: React.RefObject; }) { - const editor = useEditor(); - const isPlaying = editor.playback.getIsPlaying(); - const currentTime = editor.playback.getCurrentTime(); - const totalDuration = editor.timeline.getTotalDuration(); - const fps = editor.project.getActive().settings.fps; - - return ( -
-
- editor.playback.seek({ time })} - className="text-center" - /> - / - - {formatTimeCode({ - timeInSeconds: totalDuration, - format: "HH:MM:SS:FF", - fps, - })} - -
- - - -
- -
-
- ); -} - -function PreviewCanvas() { const canvasRef = useRef(null); - const containerRef = useRef(null); + const outerContainerRef = useRef(null); + const canvasBoundsRef = useRef(null); const lastFrameRef = useRef(-1); const lastSceneRef = useRef(null); const renderingRef = useRef(false); const { width: nativeWidth, height: nativeHeight } = usePreviewSize(); - const containerSize = useContainerSize({ containerRef }); + const containerSize = useContainerSize({ containerRef: outerContainerRef }); const editor = useEditor(); const activeProject = editor.project.getActive(); + const { overlays } = usePreviewStore(); const renderer = useMemo(() => { return new CanvasRenderer({ @@ -224,24 +169,42 @@ function PreviewCanvas() { return (
- - + + +
+ + + {overlays.bookmarks && } +
+
+ +
); } diff --git a/apps/web/src/components/editor/layout-guide-overlay.tsx b/apps/web/src/components/editor/panels/preview/layout-guide-overlay.tsx similarity index 80% rename from apps/web/src/components/editor/layout-guide-overlay.tsx rename to apps/web/src/components/editor/panels/preview/layout-guide-overlay.tsx index 0f02da1c..44efb4c8 100644 --- a/apps/web/src/components/editor/layout-guide-overlay.tsx +++ b/apps/web/src/components/editor/panels/preview/layout-guide-overlay.tsx @@ -1,27 +1,27 @@ -"use client"; - -import { useEditorStore } from "@/stores/editor-store"; -import Image from "next/image"; - -function TikTokGuide() { - return ( -
- TikTok layout guide -
- ); -} - -export function LayoutGuideOverlay() { - const { layoutGuide } = useEditorStore(); - - if (layoutGuide.platform === null) return null; - if (layoutGuide.platform === "tiktok") return ; - - return null; -} +"use client"; + +import { usePreviewStore } from "@/stores/preview-store"; +import Image from "next/image"; + +function TikTokGuide() { + return ( +
+ TikTok layout guide +
+ ); +} + +export function LayoutGuideOverlay() { + const { layoutGuide } = usePreviewStore(); + + if (layoutGuide.platform === null) return null; + if (layoutGuide.platform === "tiktok") return ; + + return null; +} diff --git a/apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx b/apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx index c13379f0..4c33a1a1 100644 --- a/apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx +++ b/apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx @@ -1,20 +1,54 @@ import { usePreviewInteraction } from "@/hooks/use-preview-interaction"; -import { cn } from "@/utils/ui"; +import { TransformHandles } from "./transform-handles"; +import { SnapGuides } from "./snap-guides"; +import { TextEditOverlay } from "./text-edit-overlay"; export function PreviewInteractionOverlay({ canvasRef, + containerRef, }: { canvasRef: React.RefObject; + containerRef: React.RefObject; }) { - const { onPointerDown, onPointerMove, onPointerUp } = - usePreviewInteraction({ canvasRef }); + const { + onPointerDown, + onPointerMove, + onPointerUp, + onDoubleClick, + snapLines, + editingText, + commitTextEdit, + cancelTextEdit, + } = usePreviewInteraction({ canvasRef }); return ( -
+
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: canvas overlay, pointer-only interaction */} +
+ {editingText ? ( + + ) : ( + + )} + +
); } diff --git a/apps/web/src/components/editor/panels/preview/snap-guides.tsx b/apps/web/src/components/editor/panels/preview/snap-guides.tsx new file mode 100644 index 00000000..4aa714a1 --- /dev/null +++ b/apps/web/src/components/editor/panels/preview/snap-guides.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useEditor } from "@/hooks/use-editor"; +import type { SnapLine } from "@/lib/preview/preview-snap"; +import { positionToOverlay } from "@/lib/preview/preview-coords"; + +export function SnapGuides({ + lines, + canvasRef, + containerRef, +}: { + lines: SnapLine[]; + canvasRef: React.RefObject; + containerRef: React.RefObject; +}) { + const editor = useEditor(); + const canvasSize = editor.project.getActive().settings.canvasSize; + const canvasRect = canvasRef.current?.getBoundingClientRect(); + const containerRect = containerRef.current?.getBoundingClientRect(); + + if (!canvasRect || !containerRect || lines.length === 0) { + return null; + } + + const toOverlayX = (logicalX: number) => + positionToOverlay({ + positionX: logicalX, + positionY: 0, + canvasRect, + containerRect, + canvasSize, + }).x; + + const toOverlayY = (logicalY: number) => + positionToOverlay({ + positionX: 0, + positionY: logicalY, + canvasRect, + containerRect, + canvasSize, + }).y; + + return ( +
+ {lines.map((line) => { + if (line.type === "vertical") { + return ( +
+ ); + } + return ( +
+ ); + })} +
+ ); +} diff --git a/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx b/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx new file mode 100644 index 00000000..265e972d --- /dev/null +++ b/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { useCallback, useEffect, useRef } from "react"; +import { useEditor } from "@/hooks/use-editor"; +import type { TextElement } from "@/types/timeline"; +import { + positionToOverlay, + getDisplayScale, +} from "@/lib/preview/preview-coords"; +import { + DEFAULT_LINE_HEIGHT, + FONT_SIZE_SCALE_REFERENCE, +} from "@/constants/text-constants"; + +const TEXT_BACKGROUND_PADDING = "4px 8px"; +const TEXT_EDIT_VERTICAL_OFFSET_EM = 0.06; + +export function TextEditOverlay({ + canvasRef, + containerRef, + trackId, + elementId, + element, + onCommit, + onCancel, +}: { + canvasRef: React.RefObject; + containerRef: React.RefObject; + trackId: string; + elementId: string; + element: TextElement; + onCommit: () => void; + onCancel: () => void; +}) { + const editor = useEditor(); + const divRef = useRef(null); + + useEffect(() => { + const div = divRef.current; + if (!div) return; + div.focus(); + const range = document.createRange(); + range.selectNodeContents(div); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); + }, []); + + const handleInput = useCallback(() => { + const div = divRef.current; + if (!div) return; + const text = div.innerText; + editor.timeline.previewElements({ + updates: [{ trackId, elementId, updates: { content: text } }], + }); + }, [editor.timeline, trackId, elementId]); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + const { key } = event; + if (key === "Escape") { + event.preventDefault(); + onCancel(); + return; + } + }, + [onCancel], + ); + + const canvasRect = canvasRef.current?.getBoundingClientRect(); + const containerRect = containerRef.current?.getBoundingClientRect(); + const canvasSize = editor.project.getActive().settings.canvasSize; + + if (!canvasRect || !containerRect || !canvasSize) return null; + + const { x: posX, y: posY } = positionToOverlay({ + positionX: element.transform.position.x, + positionY: element.transform.position.y, + canvasRect, + containerRect, + canvasSize, + }); + + const { x: displayScaleX } = getDisplayScale({ + canvasRect, + canvasSize, + }); + + const displayFontSize = + element.fontSize * + (canvasSize.height / FONT_SIZE_SCALE_REFERENCE) * + displayScaleX; + + const verticalAlignmentOffset = + displayFontSize * TEXT_EDIT_VERTICAL_OFFSET_EM; + + const lineHeight = element.lineHeight ?? DEFAULT_LINE_HEIGHT; + const fontWeight = element.fontWeight === "bold" ? "bold" : "normal"; + const fontStyle = element.fontStyle === "italic" ? "italic" : "normal"; + const letterSpacing = element.letterSpacing ?? 0; + const backgroundColor = + element.backgroundColor === "transparent" + ? "transparent" + : element.backgroundColor; + + return ( +
+ {/* biome-ignore lint/a11y/useSemanticElements: contenteditable required for multiline, IME, paste */} +
+ {element.content || ""} +
+
+ ); +} diff --git a/apps/web/src/components/editor/panels/preview/toolbar.tsx b/apps/web/src/components/editor/panels/preview/toolbar.tsx new file mode 100644 index 00000000..7f5cd9bc --- /dev/null +++ b/apps/web/src/components/editor/panels/preview/toolbar.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useEditor } from "@/hooks/use-editor"; +import { formatTimeCode } from "@/lib/time"; +import { invokeAction } from "@/lib/actions"; +import { EditableTimecode } from "@/components/editable-timecode"; +import { Button } from "@/components/ui/button"; +import { + FullScreenIcon, + PauseIcon, + PlayIcon, +} from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { OcSocialIcon } from "@opencut/ui/icons"; +import { Separator } from "@/components/ui/separator"; + +export function PreviewToolbar({ + isFullscreen, + onToggleFullscreen, +}: { + isFullscreen: boolean; + onToggleFullscreen: () => void; +}) { + const editor = useEditor(); + const isPlaying = editor.playback.getIsPlaying(); + const currentTime = editor.playback.getCurrentTime(); + const totalDuration = editor.timeline.getTotalDuration(); + const fps = editor.project.getActive().settings.fps; + + return ( +
+
+ editor.playback.seek({ time })} + className="text-center" + /> + / + + {formatTimeCode({ + timeInSeconds: totalDuration, + format: "HH:MM:SS:FF", + fps, + })} + +
+ + + +
+ + + +
+
+ ); +} diff --git a/apps/web/src/components/editor/panels/preview/transform-handles.tsx b/apps/web/src/components/editor/panels/preview/transform-handles.tsx new file mode 100644 index 00000000..976c3019 --- /dev/null +++ b/apps/web/src/components/editor/panels/preview/transform-handles.tsx @@ -0,0 +1,283 @@ +"use client"; + +import { useTransformHandles } from "@/hooks/use-transform-handles"; +import { useEditor } from "@/hooks/use-editor"; +import { isVisualElement } from "@/lib/timeline/element-utils"; +import { SnapGuides } from "./snap-guides"; +import { canvasToOverlay, getDisplayScale } from "@/lib/preview/preview-coords"; +import type { ElementBounds } from "@/lib/preview/element-bounds"; +import { cn } from "@/utils/ui"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Rotate01Icon } from "@hugeicons/core-free-icons"; + +const HANDLE_SIZE = 10; +const ROTATION_HANDLE_OFFSET = 24; +const ROTATION_HANDLE_RADIUS = 10; +const CORNER_HIT_AREA_SIZE = 18; + +type Corner = "top-left" | "top-right" | "bottom-left" | "bottom-right"; +const CORNERS: Corner[] = [ + "top-left", + "top-right", + "bottom-left", + "bottom-right", +]; + +function getCornerPosition({ + bounds, + corner, +}: { + bounds: ElementBounds; + corner: Corner; +}): { x: number; y: number } { + const halfW = bounds.width / 2; + const halfH = bounds.height / 2; + const angleRad = (bounds.rotation * Math.PI) / 180; + const cos = Math.cos(angleRad); + const sin = Math.sin(angleRad); + + const localX = + corner === "top-left" || corner === "bottom-left" ? -halfW : halfW; + const localY = + corner === "top-left" || corner === "top-right" ? -halfH : halfH; + + return { + x: bounds.cx + (localX * cos - localY * sin), + y: bounds.cy + (localX * sin + localY * cos), + }; +} + +function getRotationHandlePosition({ bounds }: { bounds: ElementBounds }): { + x: number; + y: number; +} { + const angleRad = (bounds.rotation * Math.PI) / 180; + const cos = Math.cos(angleRad); + const sin = Math.sin(angleRad); + const localY = -bounds.height / 2 - ROTATION_HANDLE_OFFSET; + return { + x: bounds.cx - localY * sin, + y: bounds.cy + localY * cos, + }; +} + +function getOverlayContext({ + canvasRef, + containerRef, +}: { + canvasRef: React.RefObject; + containerRef: React.RefObject; +}): { + canvasRect: DOMRect; + containerRect: DOMRect; +} | null { + const canvasRect = canvasRef.current?.getBoundingClientRect(); + const containerRect = containerRef.current?.getBoundingClientRect(); + if (!canvasRect || !containerRect) return null; + return { canvasRect, containerRect }; +} + +export function TransformHandles({ + canvasRef, + containerRef, +}: { + canvasRef: React.RefObject; + containerRef: React.RefObject; +}) { + const { + selectedWithBounds, + hasVisualSelection, + snapLines, + handleCornerPointerDown, + handleRotationPointerDown, + handlePointerMove, + handlePointerUp, + } = useTransformHandles({ canvasRef }); + + const editor = useEditor(); + const canvasSize = editor.project.getActive().settings.canvasSize; + + if (!hasVisualSelection || !selectedWithBounds) return null; + + const overlayContext = getOverlayContext({ canvasRef, containerRef }); + if (!overlayContext) return null; + + const { bounds, element } = selectedWithBounds; + if (!isVisualElement(element)) return null; + + const { canvasRect, containerRect } = overlayContext; + const displayScale = getDisplayScale({ canvasRect, canvasSize }); + + const toOverlay = ({ + canvasX, + canvasY, + }: { + canvasX: number; + canvasY: number; + }) => + canvasToOverlay({ + canvasX, + canvasY, + canvasRect, + containerRect, + canvasSize, + }); + + const center = toOverlay({ canvasX: bounds.cx, canvasY: bounds.cy }); + const outlineWidth = bounds.width * displayScale.x; + const outlineHeight = bounds.height * displayScale.y; + + const rotationHandle = getRotationHandlePosition({ bounds }); + const rotationHandleScreen = toOverlay({ + canvasX: rotationHandle.x, + canvasY: rotationHandle.y, + }); + + return ( +
+ + + {CORNERS.map((corner) => { + const cornerPosition = getCornerPosition({ bounds, corner }); + const screen = toOverlay({ + canvasX: cornerPosition.x, + canvasY: cornerPosition.y, + }); + return ( + + handleCornerPointerDown({ event, corner }) + } + onPointerMove={(event) => handlePointerMove({ event })} + onPointerUp={(event) => handlePointerUp({ event })} + /> + ); + })} + handleRotationPointerDown({ event })} + onPointerMove={(event) => handlePointerMove({ event })} + onPointerUp={(event) => handlePointerUp({ event })} + /> +
+ ); +} + +function BoundingBoxOutline({ + center, + outlineWidth, + outlineHeight, + rotation, +}: { + center: { x: number; y: number }; + outlineWidth: number; + outlineHeight: number; + rotation: number; +}) { + return ( +
+ ); +} + +function CornerHandle({ + corner, + screen, + onPointerDown, + onPointerMove, + onPointerUp, +}: { + corner: Corner; + screen: { x: number; y: number }; + onPointerDown: (event: React.PointerEvent) => void; + onPointerMove: (event: React.PointerEvent) => void; + onPointerUp: (event: React.PointerEvent) => void; +}) { + const isNesw = corner === "top-right" || corner === "bottom-left"; + + return ( + + ); +} + +function RotationHandle({ + screen, + onPointerDown, + onPointerMove, + onPointerUp, +}: { + screen: { x: number; y: number }; + onPointerDown: (event: React.PointerEvent) => void; + onPointerMove: (event: React.PointerEvent) => void; + onPointerUp: (event: React.PointerEvent) => void; +}) { + return ( + + ); +} diff --git a/apps/web/src/components/editor/panels/properties/empty-view.tsx b/apps/web/src/components/editor/panels/properties/empty-view.tsx index 8ccf07ee..c5576dda 100644 --- a/apps/web/src/components/editor/panels/properties/empty-view.tsx +++ b/apps/web/src/components/editor/panels/properties/empty-view.tsx @@ -17,4 +17,4 @@ export function EmptyView() {
); -} \ No newline at end of file +} diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-property-draft.ts b/apps/web/src/components/editor/panels/properties/hooks/use-property-draft.ts new file mode 100644 index 00000000..395666cb --- /dev/null +++ b/apps/web/src/components/editor/panels/properties/hooks/use-property-draft.ts @@ -0,0 +1,72 @@ +import { useReducer, useRef } from "react"; +import { evaluateMathExpression } from "@/utils/math"; + +function looksLikeExpression({ input }: { input: string }): boolean { + const trimmed = input.trim(); + if (!trimmed) return false; + if (/[+*/]/.test(input)) return true; + const minusIndex = trimmed.indexOf("-"); + return minusIndex > 0; +} + +export function usePropertyDraft({ + displayValue: sourceDisplay, + parse, + onPreview, + onCommit, + supportsExpressions = true, +}: { + displayValue: string; + parse: (input: string) => T | null; + onPreview: (value: T) => void; + onCommit: () => void; + supportsExpressions?: boolean; +}) { + const [, forceRender] = useReducer( + (renderVersion: number) => renderVersion + 1, + 0, + ); + const isEditing = useRef(false); + const draft = useRef(""); + + return { + displayValue: isEditing.current ? draft.current : sourceDisplay, + scrubTo: (value: number) => { + const parsed = parse(String(value)); + if (parsed !== null) onPreview(parsed); + }, + commitScrub: onCommit, + onFocus: () => { + isEditing.current = true; + draft.current = sourceDisplay; + forceRender(); + }, + onChange: ( + event: React.ChangeEvent, + ) => { + draft.current = event.target.value; + forceRender(); + + const parsed = parse(event.target.value); + if (parsed !== null) { + onPreview(parsed); + } + }, + onBlur: () => { + if ( + supportsExpressions && + looksLikeExpression({ input: draft.current }) + ) { + const evaluated = evaluateMathExpression({ input: draft.current }); + if (evaluated !== null) { + const parsed = parse(String(evaluated)); + if (parsed !== null) onPreview(parsed); + } + } + onCommit(); + isEditing.current = false; + draft.current = ""; + forceRender(); + }, + }; +} diff --git a/apps/web/src/components/editor/panels/properties/property-item.tsx b/apps/web/src/components/editor/panels/properties/property-item.tsx deleted file mode 100644 index a682097a..00000000 --- a/apps/web/src/components/editor/panels/properties/property-item.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useState } from "react"; -import { cn } from "@/utils/ui"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { MinusSignIcon, PlusSignIcon } from "@hugeicons/core-free-icons"; - -interface PropertyItemProps { - direction?: "row" | "column"; - children: React.ReactNode; - className?: string; -} - -export function PropertyItem({ - direction = "row", - children, - className, -}: PropertyItemProps) { - return ( -
- {children} -
- ); -} - -export function PropertyItemLabel({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ( - - {children} - - ); -} - -export function PropertyItemValue({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return
{children}
; -} - -interface PropertyGroupProps { - title: string; - children: React.ReactNode; - defaultExpanded?: boolean; - collapsible?: boolean; - className?: string; - hasBorderTop?: boolean; - hasBorderBottom?: boolean; -} - -export function PropertyGroup({ - title, - children, - defaultExpanded = true, - collapsible = true, - className, - hasBorderTop = true, - hasBorderBottom = true, -}: PropertyGroupProps) { - const [isExpanded, setIsExpanded] = useState(defaultExpanded); - - return ( -
- {collapsible ? ( - - ) : ( -
- {title} -
- )} - {(collapsible ? isExpanded : true) && ( -
{children}
- )} -
- ); -} - -function PropertyGroupTitle({ - children, - isExpanded = false, -}: { - children: React.ReactNode; - isExpanded?: boolean; -}) { - return ( - - {children} - - ); -} diff --git a/apps/web/src/components/editor/panels/properties/section.tsx b/apps/web/src/components/editor/panels/properties/section.tsx new file mode 100644 index 00000000..0b45065f --- /dev/null +++ b/apps/web/src/components/editor/panels/properties/section.tsx @@ -0,0 +1,161 @@ +import { createContext, useContext, useState } from "react"; +import { cn } from "@/utils/ui"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { ArrowDownIcon } from "@hugeicons/core-free-icons"; + +const sectionExpandedCache = new Map(); + +interface SectionContext { + isOpen: boolean; + toggle: () => void; + collapsible: boolean; +} + +const SectionCtx = createContext(null); + +function useSectionContext() { + return useContext(SectionCtx); +} + +interface SectionProps { + children: React.ReactNode; + collapsible?: boolean; + defaultOpen?: boolean; + sectionKey?: string; + className?: string; + hasBorderTop?: boolean; + hasBorderBottom?: boolean; +} + +export function Section({ + children, + collapsible = false, + defaultOpen = true, + sectionKey, + className, + hasBorderTop = true, + hasBorderBottom = true, +}: SectionProps) { + const cached = sectionKey ? sectionExpandedCache.get(sectionKey) : undefined; + const [isOpen, setIsOpen] = useState(cached ?? defaultOpen); + + const toggle = () => { + const next = !isOpen; + setIsOpen(next); + if (sectionKey) sectionExpandedCache.set(sectionKey, next); + }; + + return ( + +
+ {children} +
+
+ ); +} + +interface SectionHeaderProps { + title: string; + children?: React.ReactNode; + onClick?: () => void; + className?: string; +} + +export function SectionHeader({ + title, + children, + onClick, + className, +}: SectionHeaderProps) { + const ctx = useSectionContext(); + const isCollapsible = ctx?.collapsible ?? false; + const isOpen = ctx?.isOpen ?? true; + const isInteractive = isCollapsible || !!onClick; + + const handleClick = isCollapsible ? ctx?.toggle : onClick; + + const content = ( + <> + + {title} + +
+ {children} + {isCollapsible && ( + + )} +
+ + ); + + const baseClassName = cn( + "flex w-full items-center justify-between h-11 px-3.5", + className, + ); + + if (isInteractive) { + return ( + + ); + } + + return
{content}
; +} + +export function SectionContent({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + const ctx = useSectionContext(); + const isCollapsible = ctx?.collapsible ?? false; + const isOpen = ctx?.isOpen ?? true; + + if (isCollapsible) { + return ( +
+
+
{children}
+
+
+ ); + } + + return
{children}
; +} diff --git a/apps/web/src/components/editor/panels/properties/sections/blending.tsx b/apps/web/src/components/editor/panels/properties/sections/blending.tsx new file mode 100644 index 00000000..430efa25 --- /dev/null +++ b/apps/web/src/components/editor/panels/properties/sections/blending.tsx @@ -0,0 +1,196 @@ +import { useEditor } from "@/hooks/use-editor"; +import { usePropertyDraft } from "../hooks/use-property-draft"; +import { clamp } from "@/utils/math"; +import { NumberField } from "@/components/ui/number-field"; +import { + DEFAULT_BLEND_MODE, + DEFAULT_OPACITY, +} from "@/constants/timeline-constants"; +import { OcCheckerboardIcon } from "@opencut/ui/icons"; +import { Fragment, useRef } from "react"; +import { Section, SectionContent, SectionHeader } from "../section"; +import { + Select, + SelectContent, + SelectItem, + SelectSeparator, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { BlendMode } from "@/types/rendering"; +import type { ElementType } from "@/types/timeline"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { RainDropIcon } from "@hugeicons/core-free-icons"; + +type BlendingElement = { + id: string; + opacity: number; + type: ElementType; + blendMode?: BlendMode; +}; + +const BLEND_MODE_GROUPS = [ + [{ value: "normal", label: "Normal" }], + [ + { value: "darken", label: "Darken" }, + { value: "multiply", label: "Multiply" }, + { value: "color-burn", label: "Color Burn" }, + ], + [ + { value: "lighten", label: "Lighten" }, + { value: "screen", label: "Screen" }, + { value: "plus-lighter", label: "Plus Lighter" }, + { value: "color-dodge", label: "Color Dodge" }, + ], + [ + { value: "overlay", label: "Overlay" }, + { value: "soft-light", label: "Soft Light" }, + { value: "hard-light", label: "Hard Light" }, + ], + [ + { value: "difference", label: "Difference" }, + { value: "exclusion", label: "Exclusion" }, + ], + [ + { value: "hue", label: "Hue" }, + { value: "saturation", label: "Saturation" }, + { value: "color", label: "Color" }, + { value: "luminosity", label: "Luminosity" }, + ], +]; + +export function BlendingSection({ + element, + trackId, +}: { + element: BlendingElement; + trackId: string; +}) { + const editor = useEditor(); + const blendMode = element.blendMode ?? DEFAULT_BLEND_MODE; + const didSelectRef = useRef(false); + const committedBlendModeRef = useRef(blendMode); + if (!editor.timeline.isPreviewActive()) { + committedBlendModeRef.current = blendMode; + } + + const previewBlendMode = (value: BlendMode) => + editor.timeline.previewElements({ + updates: [ + { trackId, elementId: element.id, updates: { blendMode: value } }, + ], + }); + + const commitBlendMode = (value: string) => { + if (editor.timeline.isPreviewActive()) { + editor.timeline.commitPreview(); + } else { + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { blendMode: value as BlendMode }, + }, + ], + }); + } + didSelectRef.current = true; + }; + + const handleBlendModeOpenChange = (isOpen: boolean) => { + if (!isOpen) { + if (!didSelectRef.current) editor.timeline.discardPreview(); + didSelectRef.current = false; + } + }; + + const opacity = usePropertyDraft({ + displayValue: Math.round(element.opacity * 100).toString(), + parse: (input) => { + const parsed = parseFloat(input); + if (Number.isNaN(parsed)) return null; + return clamp({ value: parsed, min: 0, max: 100 }) / 100; + }, + onPreview: (value) => + editor.timeline.previewElements({ + updates: [ + { trackId, elementId: element.id, updates: { opacity: value } }, + ], + }), + onCommit: () => editor.timeline.commitPreview(), + }); + + return ( +
+ + +
+
+ + } + value={opacity.displayValue} + min={0} + max={100} + onFocus={opacity.onFocus} + onChange={opacity.onChange} + onBlur={opacity.onBlur} + onScrub={opacity.scrubTo} + onScrubEnd={opacity.commitScrub} + onReset={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { opacity: DEFAULT_OPACITY }, + }, + ], + }) + } + isDefault={element.opacity === DEFAULT_OPACITY} + dragSensitivity="slow" + /> +
+
+ +
+
+
+
+ ); +} diff --git a/apps/web/src/components/editor/panels/properties/sections/index.tsx b/apps/web/src/components/editor/panels/properties/sections/index.tsx new file mode 100644 index 00000000..8be58f39 --- /dev/null +++ b/apps/web/src/components/editor/panels/properties/sections/index.tsx @@ -0,0 +1,2 @@ +export * from "./transform"; +export * from "./blending"; diff --git a/apps/web/src/components/editor/panels/properties/sections/transform.tsx b/apps/web/src/components/editor/panels/properties/sections/transform.tsx new file mode 100644 index 00000000..17a54b4c --- /dev/null +++ b/apps/web/src/components/editor/panels/properties/sections/transform.tsx @@ -0,0 +1,248 @@ +import { NumberField } from "@/components/ui/number-field"; +import { useEditor } from "@/hooks/use-editor"; +import { usePropertyDraft } from "../hooks/use-property-draft"; +import { clamp } from "@/utils/math"; +import type { ElementType, Transform } from "@/types/timeline"; +import { Section, SectionContent, SectionHeader } from "../section"; +import { Button } from "@/components/ui/button"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { + ArrowExpandIcon, + Link05Icon, + RotateClockwiseIcon, +} from "@hugeicons/core-free-icons"; +import { useState } from "react"; +import { DEFAULT_TRANSFORM } from "@/constants/timeline-constants"; + +type TransformElement = { + id: string; + transform: Transform; + type: ElementType; +}; + +function parseFloat_({ input }: { input: string }): number | null { + const parsed = parseFloat(input); + return Number.isNaN(parsed) ? null : parsed; +} + +export function TransformSection({ + element, + trackId, +}: { + element: TransformElement; + trackId: string; +}) { + const editor = useEditor(); + const [isScaleLocked, setIsScaleLocked] = useState(false); + + const previewTransform = (transform: Partial) => { + editor.timeline.previewElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { transform: { ...element.transform, ...transform } }, + }, + ], + }); + }; + + const commit = () => editor.timeline.commitPreview(); + + const positionX = usePropertyDraft({ + displayValue: Math.round(element.transform.position.x).toString(), + parse: (input) => parseFloat_({ input }), + onPreview: (value) => + previewTransform({ + position: { ...element.transform.position, x: value }, + }), + onCommit: commit, + }); + + const positionY = usePropertyDraft({ + displayValue: Math.round(element.transform.position.y).toString(), + parse: (input) => parseFloat_({ input }), + onPreview: (value) => + previewTransform({ + position: { ...element.transform.position, y: value }, + }), + onCommit: commit, + }); + + const scale = usePropertyDraft({ + displayValue: Math.round(element.transform.scale * 100).toString(), + parse: (input) => { + const parsed = parseFloat_({ input }); + if (parsed === null) return null; + return Math.max(parsed, 1) / 100; + }, + onPreview: (value) => previewTransform({ scale: value }), + onCommit: commit, + }); + const scaleFieldProps = { + className: "flex-1", + value: scale.displayValue, + onFocus: scale.onFocus, + onChange: scale.onChange, + onBlur: scale.onBlur, + dragSensitivity: "slow" as const, + onScrub: scale.scrubTo, + onScrubEnd: scale.commitScrub, + onReset: () => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + transform: { + ...element.transform, + scale: DEFAULT_TRANSFORM.scale, + }, + }, + }, + ], + }), + isDefault: element.transform.scale === DEFAULT_TRANSFORM.scale, + }; + + const rotation = usePropertyDraft({ + displayValue: Math.round(element.transform.rotate).toString(), + parse: (input) => { + const parsed = parseFloat_({ input }); + if (parsed === null) return null; + return clamp({ value: parsed, min: -360, max: 360 }); + }, + onPreview: (value) => previewTransform({ rotate: value }), + onCommit: commit, + }); + + return ( +
+ + +
+
+ {isScaleLocked ? ( + <> + + + + ) : ( + } + {...scaleFieldProps} + className="flex-1" + /> + )} + +
+
+ + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + transform: { + ...element.transform, + position: { + ...element.transform.position, + x: DEFAULT_TRANSFORM.position.x, + }, + }, + }, + }, + ], + }) + } + isDefault={ + element.transform.position.x === DEFAULT_TRANSFORM.position.x + } + /> + + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + transform: { + ...element.transform, + position: { + ...element.transform.position, + y: DEFAULT_TRANSFORM.position.y, + }, + }, + }, + }, + ], + }) + } + isDefault={ + element.transform.position.y === DEFAULT_TRANSFORM.position.y + } + /> +
+ +
+ } + className="flex-1" + value={rotation.displayValue} + onFocus={rotation.onFocus} + onChange={rotation.onChange} + onBlur={rotation.onBlur} + dragSensitivity="slow" + onScrub={rotation.scrubTo} + onScrubEnd={rotation.commitScrub} + onReset={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + transform: { + ...element.transform, + rotate: DEFAULT_TRANSFORM.rotate, + }, + }, + }, + ], + }) + } + isDefault={element.transform.rotate === DEFAULT_TRANSFORM.rotate} + /> +
+
+
+
+ ); +} diff --git a/apps/web/src/components/editor/panels/properties/text-properties.tsx b/apps/web/src/components/editor/panels/properties/text-properties.tsx index 22e50e4e..cfca52dc 100644 --- a/apps/web/src/components/editor/panels/properties/text-properties.tsx +++ b/apps/web/src/components/editor/panels/properties/text-properties.tsx @@ -1,24 +1,31 @@ import { Textarea } from "@/components/ui/textarea"; import { FontPicker } from "@/components/ui/font-picker"; -import type { FontFamily } from "@/constants/font-constants"; import type { TextElement } from "@/types/timeline"; import { Slider } from "@/components/ui/slider"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { useReducer, useRef } from "react"; -import { PanelBaseView } from "@/components/editor/panels/panel-base-view"; -import { - PropertyGroup, - PropertyItem, - PropertyItemLabel, - PropertyItemValue, -} from "./property-item"; +import { NumberField } from "@/components/ui/number-field"; +import { useRef } from "react"; +import { Section, SectionContent, SectionHeader } from "./section"; import { ColorPicker } from "@/components/ui/color-picker"; import { uppercase } from "@/utils/string"; import { clamp } from "@/utils/math"; import { useEditor } from "@/hooks/use-editor"; import { DEFAULT_COLOR } from "@/constants/project-constants"; -import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/constants/text-constants"; +import { + DEFAULT_TEXT_ELEMENT, + MAX_FONT_SIZE, + MIN_FONT_SIZE, +} from "@/constants/text-constants"; +import { usePropertyDraft } from "./hooks/use-property-draft"; +import { TransformSection, BlendingSection } from "./sections"; +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, +} from "@/components/ui/select"; +import { OcFontWeightIcon } from "@opencut/ui/icons"; +import { Label } from "@/components/ui/label"; export function TextProperties({ element, @@ -26,625 +33,230 @@ export function TextProperties({ }: { element: TextElement; trackId: string; +}) { + return ( +
+ + + + + +
+ ); +} + +function ContentSection({ + element, + trackId, +}: { + element: TextElement; + trackId: string; }) { const editor = useEditor(); - const containerRef = useRef(null); - const [, forceRender] = useReducer((x: number) => x + 1, 0); - const isEditingFontSize = useRef(false); - const isEditingOpacity = useRef(false); - const isEditingContent = useRef(false); - const fontSizeDraft = useRef(""); - const opacityDraft = useRef(""); - const contentDraft = useRef(""); - const fontSizeDisplay = isEditingFontSize.current - ? fontSizeDraft.current - : element.fontSize.toString(); - const opacityDisplay = isEditingOpacity.current - ? opacityDraft.current - : Math.round(element.opacity * 100).toString(); - const contentDisplay = isEditingContent.current - ? contentDraft.current - : element.content; - - const lastSelectedColor = useRef(DEFAULT_COLOR); - const initialFontSizeRef = useRef(null); - const initialOpacityRef = useRef(null); - const initialContentRef = useRef(null); - const initialColorRef = useRef(null); - const initialBgColorRef = useRef(null); - - const handleFontSizeChange = ({ value }: { value: string }) => { - fontSizeDraft.current = value; - forceRender(); - - if (value.trim() !== "") { - if (initialFontSizeRef.current === null) { - initialFontSizeRef.current = element.fontSize; - } - const parsed = parseInt(value, 10); - const fontSize = Number.isNaN(parsed) - ? element.fontSize - : clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE }); - editor.timeline.updateElements({ - updates: [{ trackId, elementId: element.id, updates: { fontSize } }], - pushHistory: false, - }); - } - }; - - const handleFontSizeBlur = () => { - if (initialFontSizeRef.current !== null) { - const parsed = parseInt(fontSizeDraft.current, 10); - const fontSize = Number.isNaN(parsed) - ? element.fontSize - : clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE }); - editor.timeline.updateElements({ + const content = usePropertyDraft({ + displayValue: element.content, + parse: (input) => input, + onPreview: (value) => + editor.timeline.previewElements({ updates: [ - { - trackId, - elementId: element.id, - updates: { fontSize: initialFontSizeRef.current }, - }, + { trackId, elementId: element.id, updates: { content: value } }, ], - pushHistory: false, - }); - editor.timeline.updateElements({ - updates: [{ trackId, elementId: element.id, updates: { fontSize } }], - pushHistory: true, - }); - initialFontSizeRef.current = null; - } - isEditingFontSize.current = false; - fontSizeDraft.current = ""; - forceRender(); - }; - - const handleOpacityChange = ({ value }: { value: string }) => { - opacityDraft.current = value; - forceRender(); - - if (value.trim() !== "") { - if (initialOpacityRef.current === null) { - initialOpacityRef.current = element.opacity; - } - const parsed = parseInt(value, 10); - const opacityPercent = Number.isNaN(parsed) - ? Math.round(element.opacity * 100) - : clamp({ value: parsed, min: 0, max: 100 }); - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { opacity: opacityPercent / 100 }, - }, - ], - pushHistory: false, - }); - } - }; - - const handleOpacityBlur = () => { - if (initialOpacityRef.current !== null) { - const parsed = parseInt(opacityDraft.current, 10); - const opacityPercent = Number.isNaN(parsed) - ? Math.round(element.opacity * 100) - : clamp({ value: parsed, min: 0, max: 100 }); - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { opacity: initialOpacityRef.current }, - }, - ], - pushHistory: false, - }); - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { opacity: opacityPercent / 100 }, - }, - ], - pushHistory: true, - }); - initialOpacityRef.current = null; - } - isEditingOpacity.current = false; - opacityDraft.current = ""; - forceRender(); - }; - - const handleColorChange = ({ color }: { color: string }) => { - if (color !== "transparent") { - lastSelectedColor.current = color; - } - if (initialBgColorRef.current === null) { - initialBgColorRef.current = element.backgroundColor; - } - if (initialBgColorRef.current !== null) { - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { backgroundColor: color }, - }, - ], - pushHistory: false, - }); - } else { - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { backgroundColor: color }, - }, - ], - }); - } - }; - - const handleColorChangeEnd = ({ color }: { color: string }) => { - if (initialBgColorRef.current !== null) { - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { backgroundColor: initialBgColorRef.current }, - }, - ], - pushHistory: false, - }); - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { backgroundColor: `#${color}` }, - }, - ], - pushHistory: true, - }); - initialBgColorRef.current = null; - } - }; + }), + onCommit: () => editor.timeline.commitPreview(), + }); return ( -
- - -