@buley/hexgrid-3d 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,53 +1,53 @@
1
1
  /**
2
2
  * Adapter for embedding notes directly in the grid
3
- *
3
+ *
4
4
  * This adapter allows notes (Cyrano's internal notes, user notes, etc.)
5
5
  * to be embedded directly in GridItems with full metadata preservation.
6
6
  */
7
7
 
8
- import type { GridItem } from './types'
9
- import type { ItemAdapter, AdapterOptions } from './adapters'
8
+ import type { GridItem } from './types';
9
+ import type { ItemAdapter, AdapterOptions } from './adapters';
10
10
 
11
11
  /**
12
12
  * Note content structure
13
13
  */
14
14
  export interface NoteContent {
15
- title?: string
16
- text: string
17
- summary?: string
15
+ title?: string;
16
+ text: string;
17
+ summary?: string;
18
18
  }
19
19
 
20
20
  /**
21
21
  * Note metadata structure
22
22
  */
23
23
  export interface NoteMetadata {
24
- type?: string
25
- category?: string
26
- subcategory?: string
27
- tags?: string[]
28
- targetUserId?: string
29
- status?: 'active' | 'archived' | 'draft'
30
- priority?: 'low' | 'medium' | 'high' | 'urgent'
31
- context?: string
32
- permissions?: 'private' | 'shared' | 'public'
33
- sharedWith?: string[]
24
+ type?: string;
25
+ category?: string;
26
+ subcategory?: string;
27
+ tags?: string[];
28
+ targetUserId?: string;
29
+ status?: 'active' | 'archived' | 'draft';
30
+ priority?: 'low' | 'medium' | 'high' | 'urgent';
31
+ context?: string;
32
+ permissions?: 'private' | 'shared' | 'public';
33
+ sharedWith?: string[];
34
34
  }
35
35
 
36
36
  /**
37
37
  * Note structure matching Affectively's note format
38
38
  */
39
39
  export interface Note {
40
- id: string
41
- content: NoteContent
42
- metadata?: NoteMetadata
43
- date: string
40
+ id: string;
41
+ content: NoteContent;
42
+ metadata?: NoteMetadata;
43
+ date: string;
44
44
  }
45
45
 
46
46
  /**
47
47
  * Calculate velocity for a note based on recency, priority, and metadata richness
48
48
  */
49
49
  function calculateNoteVelocity(note: Note): number {
50
- let velocity = 0.1 // Base minimum
50
+ let velocity = 0.1; // Base minimum
51
51
 
52
52
  // Priority contribution (0-0.3)
53
53
  const priorityMap: Record<string, number> = {
@@ -55,29 +55,30 @@ function calculateNoteVelocity(note: Note): number {
55
55
  high: 0.2,
56
56
  medium: 0.1,
57
57
  low: 0.05,
58
- }
58
+ };
59
59
  if (note.metadata?.priority) {
60
- velocity += priorityMap[note.metadata.priority] || 0.1
60
+ velocity += priorityMap[note.metadata.priority] || 0.1;
61
61
  }
62
62
 
63
63
  // Recency contribution (0-0.4)
64
64
  if (note.date) {
65
- const ageMs = Date.now() - new Date(note.date).getTime()
66
- const ageHours = ageMs / (1000 * 60 * 60)
67
- const recencyFactor = Math.max(0, 1 - ageHours / 168) // Decay over 1 week
68
- velocity += recencyFactor * 0.4
65
+ const ageMs = Date.now() - new Date(note.date).getTime();
66
+ const ageHours = ageMs / (1000 * 60 * 60);
67
+ const recencyFactor = Math.max(0, 1 - ageHours / 168); // Decay over 1 week
68
+ velocity += recencyFactor * 0.4;
69
69
  }
70
70
 
71
71
  // Metadata richness contribution (0-0.2)
72
- let contextScore = 0
73
- if (note.metadata?.type) contextScore += 0.05
74
- if (note.metadata?.category) contextScore += 0.05
75
- if (note.metadata?.tags && note.metadata.tags.length > 0) contextScore += 0.05
76
- if (note.metadata?.context) contextScore += 0.05
77
- velocity += Math.min(contextScore, 0.2)
72
+ let contextScore = 0;
73
+ if (note.metadata?.type) contextScore += 0.05;
74
+ if (note.metadata?.category) contextScore += 0.05;
75
+ if (note.metadata?.tags && note.metadata.tags.length > 0)
76
+ contextScore += 0.05;
77
+ if (note.metadata?.context) contextScore += 0.05;
78
+ velocity += Math.min(contextScore, 0.2);
78
79
 
79
80
  // Clamp to [0.1, 1.0]
80
- return Math.max(0.1, Math.min(1.0, velocity))
81
+ return Math.max(0.1, Math.min(1.0, velocity));
81
82
  }
82
83
 
83
84
  /**
@@ -85,7 +86,7 @@ function calculateNoteVelocity(note: Note): number {
85
86
  */
86
87
  export const noteAdapter: ItemAdapter<Note> = {
87
88
  toGridItem(note: Note, options?: AdapterOptions): GridItem<Note> {
88
- const velocity = options?.velocity ?? calculateNoteVelocity(note)
89
+ const velocity = options?.velocity ?? calculateNoteVelocity(note);
89
90
 
90
91
  return {
91
92
  id: note.id,
@@ -100,25 +101,32 @@ export const noteAdapter: ItemAdapter<Note> = {
100
101
  category: note.metadata?.category,
101
102
  // Store metadata in metrics for filtering/sorting
102
103
  metrics: {
103
- priority: note.metadata?.priority === 'urgent' ? 4 : note.metadata?.priority === 'high' ? 3 : note.metadata?.priority === 'medium' ? 2 : 1,
104
+ priority:
105
+ note.metadata?.priority === 'urgent'
106
+ ? 4
107
+ : note.metadata?.priority === 'high'
108
+ ? 3
109
+ : note.metadata?.priority === 'medium'
110
+ ? 2
111
+ : 1,
104
112
  tagCount: note.metadata?.tags?.length || 0,
105
113
  },
106
- }
114
+ };
107
115
  },
108
116
 
109
117
  fromGridItem(item: GridItem<Note>): Note {
110
118
  if (!item.data) {
111
- throw new Error('GridItem missing note data')
119
+ throw new Error('GridItem missing note data');
112
120
  }
113
- return item.data
121
+ return item.data;
114
122
  },
115
123
 
116
124
  calculateVelocity(note: Note): number {
117
- return calculateNoteVelocity(note)
125
+ return calculateNoteVelocity(note);
118
126
  },
119
127
 
120
128
  extractVisualUrl(note: Note): string | undefined {
121
129
  // Notes can have generated visualizations
122
- return `/api/notes/${note.id}/visualization`
130
+ return `/api/notes/${note.id}/visualization`;
123
131
  },
124
- }
132
+ };
@@ -1,49 +1,53 @@
1
1
  /**
2
2
  * Adapter for embedding ontology entities directly in the grid
3
- *
3
+ *
4
4
  * This adapter allows ontology entities from @emotions-app/shared-utils/ontology
5
5
  * to be embedded directly in GridItems with full metadata preservation.
6
6
  */
7
7
 
8
- import type { OntologyEntity } from '@emotions-app/shared-utils/ontology/types'
9
- import type { GridItem } from './types'
10
- import type { ItemAdapter, AdapterOptions } from './adapters'
8
+ import type { OntologyEntity } from '@emotions-app/shared-utils/ontology/types';
9
+ import type { GridItem } from './types';
10
+ import type { ItemAdapter, AdapterOptions } from './adapters';
11
11
 
12
12
  /**
13
13
  * Calculate velocity for an ontology entity based on provenance confidence
14
14
  * and recency
15
15
  */
16
16
  function calculateEntityVelocity(entity: OntologyEntity): number {
17
- let velocity = 0.1 // Base minimum
17
+ let velocity = 0.1; // Base minimum
18
18
 
19
19
  // Use provenance confidence (0-1) as primary factor
20
- const confidence = entity.metadata.provenance.confidence
21
- velocity += confidence * 0.5
20
+ const confidence = entity.metadata.provenance.confidence;
21
+ velocity += confidence * 0.5;
22
22
 
23
23
  // Recency factor based on lastModified
24
24
  if (entity.metadata.lastModified) {
25
- const ageMs = Date.now() - new Date(entity.metadata.lastModified).getTime()
26
- const ageHours = ageMs / (1000 * 60 * 60)
27
- const recencyFactor = Math.max(0, 1 - ageHours / 168) // Decay over 1 week
28
- velocity += recencyFactor * 0.4
25
+ const ageMs = Date.now() - new Date(entity.metadata.lastModified).getTime();
26
+ const ageHours = ageMs / (1000 * 60 * 60);
27
+ const recencyFactor = Math.max(0, 1 - ageHours / 168); // Decay over 1 week
28
+ velocity += recencyFactor * 0.4;
29
29
  }
30
30
 
31
31
  // Clamp to [0.1, 1.0]
32
- return Math.max(0.1, Math.min(1.0, velocity))
32
+ return Math.max(0.1, Math.min(1.0, velocity));
33
33
  }
34
34
 
35
35
  /**
36
36
  * Adapter for embedding ontology entities directly
37
37
  */
38
38
  export const ontologyEntityAdapter: ItemAdapter<OntologyEntity> = {
39
- toGridItem(entity: OntologyEntity, options?: AdapterOptions): GridItem<OntologyEntity> {
40
- const velocity = options?.velocity ?? calculateEntityVelocity(entity)
39
+ toGridItem(
40
+ entity: OntologyEntity,
41
+ options?: AdapterOptions
42
+ ): GridItem<OntologyEntity> {
43
+ const velocity = options?.velocity ?? calculateEntityVelocity(entity);
41
44
 
42
45
  return {
43
46
  id: entity['@id'],
44
47
  type: 'ontology-entity',
45
48
  title: entity.label,
46
- description: (entity.properties.description as string | undefined) || entity.label,
49
+ description:
50
+ (entity.properties.description as string | undefined) || entity.label,
47
51
  data: entity,
48
52
  ontologyMetadata: {
49
53
  entityId: entity['@id'],
@@ -54,24 +58,27 @@ export const ontologyEntityAdapter: ItemAdapter<OntologyEntity> = {
54
58
  provenance: entity.metadata.provenance,
55
59
  },
56
60
  velocity,
57
- createdAt: entity.metadata.lastModified || entity.metadata.provenance.extractedAt,
61
+ createdAt:
62
+ entity.metadata.lastModified || entity.metadata.provenance.extractedAt,
58
63
  // Extract visual URL if available in properties
59
- imageUrl: options?.visualUrl || (entity.properties.imageUrl as string | undefined),
60
- }
64
+ imageUrl:
65
+ options?.visualUrl ||
66
+ (entity.properties.imageUrl as string | undefined),
67
+ };
61
68
  },
62
69
 
63
70
  fromGridItem(item: GridItem<OntologyEntity>): OntologyEntity {
64
71
  if (!item.data) {
65
- throw new Error('GridItem missing ontology entity data')
72
+ throw new Error('GridItem missing ontology entity data');
66
73
  }
67
- return item.data
74
+ return item.data;
68
75
  },
69
76
 
70
77
  calculateVelocity(entity: OntologyEntity): number {
71
- return calculateEntityVelocity(entity)
78
+ return calculateEntityVelocity(entity);
72
79
  },
73
80
 
74
81
  extractVisualUrl(entity: OntologyEntity): string | undefined {
75
- return entity.properties.imageUrl as string | undefined
82
+ return entity.properties.imageUrl as string | undefined;
76
83
  },
77
- }
84
+ };
@@ -1,85 +1,85 @@
1
1
  type UIState = {
2
- debugOpen: boolean
3
- showStats: boolean
4
- cameraOpen?: boolean
5
- showNarration?: boolean
6
- }
2
+ debugOpen: boolean;
3
+ showStats: boolean;
4
+ cameraOpen?: boolean;
5
+ showNarration?: boolean;
6
+ };
7
7
 
8
8
  // Safe localStorage helpers that never throw
9
9
  const safeGetItem = (key: string): string | null => {
10
10
  // istanbul ignore next
11
- if (typeof window === 'undefined') return null
11
+ if (typeof window === 'undefined') return null;
12
12
  try {
13
- return window.localStorage.getItem(key)
13
+ return window.localStorage.getItem(key);
14
14
  } catch {
15
15
  // istanbul ignore next
16
- return null
16
+ return null;
17
17
  }
18
- }
18
+ };
19
19
 
20
20
  const safeSetItem = (key: string, value: string): void => {
21
21
  // istanbul ignore next
22
- if (typeof window === 'undefined') return
22
+ if (typeof window === 'undefined') return;
23
23
  try {
24
- window.localStorage.setItem(key, value)
24
+ window.localStorage.setItem(key, value);
25
25
  } catch {
26
26
  // istanbul ignore next - private browsing, quota exceeded
27
27
  }
28
- }
28
+ };
29
29
 
30
30
  // Initialize showNarration from localStorage if available
31
- const savedNarration = safeGetItem('hexgrid.showNarration')
32
- const initialShowNarration = savedNarration === 'true'
31
+ const savedNarration = safeGetItem('hexgrid.showNarration');
32
+ const initialShowNarration = savedNarration === 'true';
33
33
 
34
34
  const state: UIState = {
35
35
  debugOpen: false,
36
36
  showStats: false,
37
37
  cameraOpen: false,
38
- showNarration: initialShowNarration
39
- }
38
+ showNarration: initialShowNarration,
39
+ };
40
40
 
41
- const listeners = new Set<(s: UIState) => void>()
41
+ const listeners = new Set<(s: UIState) => void>();
42
42
 
43
43
  const uiStore = {
44
44
  getState(): UIState {
45
- return { ...state }
45
+ return { ...state };
46
46
  },
47
47
  set(partial: Partial<UIState>) {
48
- let changed = false
48
+ let changed = false;
49
49
  for (const k of Object.keys(partial) as (keyof UIState)[]) {
50
50
  if (partial[k] !== undefined && state[k] !== partial[k]) {
51
51
  // @ts-ignore
52
- state[k] = partial[k]
53
- changed = true
52
+ state[k] = partial[k];
53
+ changed = true;
54
54
  }
55
55
  }
56
56
  if (changed) {
57
57
  // Persist showNarration to localStorage for cross-refresh consistency
58
58
  if (partial.showNarration !== undefined) {
59
- safeSetItem('hexgrid.showNarration', String(!!partial.showNarration))
59
+ safeSetItem('hexgrid.showNarration', String(!!partial.showNarration));
60
60
  }
61
- for (const cb of Array.from(listeners)) cb({ ...state })
61
+ for (const cb of Array.from(listeners)) cb({ ...state });
62
62
  }
63
63
  },
64
64
  subscribe(cb: (s: UIState) => void) {
65
- listeners.add(cb)
65
+ listeners.add(cb);
66
66
  // emit current immediately
67
- cb({ ...state })
68
- return () => listeners.delete(cb)
67
+ cb({ ...state });
68
+ return () => listeners.delete(cb);
69
69
  },
70
70
  toggleDebug() {
71
- this.set({ debugOpen: !state.debugOpen })
71
+ this.set({ debugOpen: !state.debugOpen });
72
72
  },
73
73
  toggleStats() {
74
- this.set({ showStats: !state.showStats })
74
+ this.set({ showStats: !state.showStats });
75
75
  },
76
76
  toggleCamera() {
77
- this.set({ cameraOpen: !state.cameraOpen })
77
+ this.set({ cameraOpen: !state.cameraOpen });
78
78
  },
79
79
  toggleNarration() {
80
- this.set({ showNarration: !state.showNarration })
81
- }
82
- }
80
+ this.set({ showNarration: !state.showNarration });
81
+ },
82
+ };
83
83
 
84
- export { uiStore }
85
- export default uiStore
84
+ export { uiStore };
85
+ export default uiStore;
package/src/types.ts CHANGED
@@ -2,68 +2,68 @@
2
2
  * Type definitions for HexGrid Visualization
3
3
  */
4
4
 
5
- import type { RefObject } from 'react'
6
- import type { HexGridFeatureFlags } from './features'
5
+ import type { RefObject } from 'react';
6
+ import type { HexGridFeatureFlags } from './features';
7
7
 
8
8
  /**
9
9
  * Photo type for HexGrid visualization
10
- *
10
+ *
11
11
  * This is the unified Photo type used throughout the hexgrid-3d package.
12
12
  * It includes all fields needed for display, media playback, and analytics.
13
13
  */
14
14
  export interface Photo {
15
- id: string
16
- title: string
17
-
15
+ id: string;
16
+ title: string;
17
+
18
18
  // Image URLs - imageUrl is primary, url is for backward compatibility
19
- imageUrl: string
20
- url?: string // Alias for imageUrl (backward compatibility)
21
- thumbnailUrl?: string
22
-
19
+ imageUrl: string;
20
+ url?: string; // Alias for imageUrl (backward compatibility)
21
+ thumbnailUrl?: string;
22
+
23
23
  // Display metadata
24
- alt: string
25
- category: string
26
- description?: string
27
-
24
+ alt: string;
25
+ category: string;
26
+ description?: string;
27
+
28
28
  // Source information
29
- source: string
30
- sourceUrl?: string
31
- createdAt?: string
32
-
29
+ source: string;
30
+ sourceUrl?: string;
31
+ createdAt?: string;
32
+
33
33
  // Shop integration
34
- shopUrl?: string
35
- location?: string
36
-
34
+ shopUrl?: string;
35
+ location?: string;
36
+
37
37
  // Media type flags
38
- isVideo?: boolean
39
- videoUrl?: string
40
- isTweet?: boolean
41
- tweetUrl?: string
42
- redditUrl?: string
43
- durationSeconds?: number
44
-
38
+ isVideo?: boolean;
39
+ videoUrl?: string;
40
+ isTweet?: boolean;
41
+ tweetUrl?: string;
42
+ redditUrl?: string;
43
+ durationSeconds?: number;
44
+
45
45
  // Competition/ranking system
46
- velocity?: number // Normalized velocity [0.1, 1.0] for meritocratic competition
47
-
46
+ velocity?: number; // Normalized velocity [0.1, 1.0] for meritocratic competition
47
+
48
48
  // User info
49
- userId?: string
50
- username?: string
51
- platform?: string
52
- author?: string
53
- authorUrl?: string
54
-
49
+ userId?: string;
50
+ username?: string;
51
+ platform?: string;
52
+ author?: string;
53
+ authorUrl?: string;
54
+
55
55
  // Metrics for analytics
56
- views?: number
57
- likes?: number
58
- comments?: number
59
- shares?: number
60
- upvotes?: number
61
- retweets?: number
62
- replies?: number
63
- age_in_hours?: number
64
-
56
+ views?: number;
57
+ likes?: number;
58
+ comments?: number;
59
+ shares?: number;
60
+ upvotes?: number;
61
+ retweets?: number;
62
+ replies?: number;
63
+ age_in_hours?: number;
64
+
65
65
  // Visual
66
- dominantColor?: string
66
+ dominantColor?: string;
67
67
  }
68
68
 
69
69
  /**
@@ -72,81 +72,92 @@ export interface Photo {
72
72
  */
73
73
  export interface GridItem<T = unknown> {
74
74
  // Core identification
75
- id: string
76
- type: string // 'photo' | 'note' | 'emotion' | 'ontology-entity' | 'custom'
75
+ id: string;
76
+ type: string; // 'photo' | 'note' | 'emotion' | 'ontology-entity' | 'custom'
77
77
 
78
78
  // Visual representation (optional - allows non-visual items)
79
- imageUrl?: string
80
- thumbnailUrl?: string
81
- videoUrl?: string
79
+ imageUrl?: string;
80
+ thumbnailUrl?: string;
81
+ videoUrl?: string;
82
82
 
83
83
  // Display metadata
84
- title?: string
85
- alt?: string
86
- description?: string
87
- category?: string
84
+ title?: string;
85
+ alt?: string;
86
+ description?: string;
87
+ category?: string;
88
88
 
89
89
  // Embedded data object (preserves original type)
90
- data?: T
90
+ data?: T;
91
91
 
92
92
  // Ontology metadata (optional, for ontology-aware items)
93
93
  ontologyMetadata?: {
94
- entityId?: string
95
- entityType?: string | string[]
96
- properties?: Record<string, unknown>
94
+ entityId?: string;
95
+ entityType?: string | string[];
96
+ properties?: Record<string, unknown>;
97
97
  provenance?: {
98
- source: string
99
- extractedAt: string
100
- confidence: number
101
- }
102
- }
98
+ source: string;
99
+ extractedAt: string;
100
+ confidence: number;
101
+ };
102
+ };
103
103
 
104
104
  // Grid behavior
105
- velocity?: number
106
- source?: string
107
- sourceUrl?: string
108
- createdAt?: string
105
+ velocity?: number;
106
+ source?: string;
107
+ sourceUrl?: string;
108
+ createdAt?: string;
109
109
 
110
110
  // Metrics (flexible for any data type)
111
- metrics?: Record<string, number>
111
+ metrics?: Record<string, number>;
112
112
 
113
113
  // Legacy Photo fields (for backward compatibility)
114
- url?: string // Maps to imageUrl
115
- userId?: string
116
- username?: string
117
- platform?: string
118
- author?: string
119
- authorUrl?: string
120
- likes?: number
121
- views?: number
122
- comments?: number
123
- dominantColor?: string
114
+ url?: string; // Maps to imageUrl
115
+ userId?: string;
116
+ username?: string;
117
+ platform?: string;
118
+ author?: string;
119
+ authorUrl?: string;
120
+ likes?: number;
121
+ views?: number;
122
+ comments?: number;
123
+ dominantColor?: string;
124
124
  }
125
125
 
126
126
  export interface HexGridProps {
127
- photos: Photo[]
128
- onHexClick?: (photo: Photo) => void
129
- spacing?: number
130
- canvasRef?: RefObject<HTMLCanvasElement>
131
- onLeaderboardUpdate?: (leaderboard: any) => void
132
- autoplayQueueLimit?: number
133
- onAutoplayQueueLimitChange?: (limit: number) => void
134
- modalOpen?: boolean
135
- userId?: string
136
- username?: string
137
- featureFlags?: HexGridFeatureFlags
127
+ photos: Photo[];
128
+ onHexClick?: (photo: Photo) => void;
129
+ spacing?: number;
130
+ canvasRef?: RefObject<HTMLCanvasElement>;
131
+ onLeaderboardUpdate?: (leaderboard: any) => void;
132
+ autoplayQueueLimit?: number;
133
+ onAutoplayQueueLimitChange?: (limit: number) => void;
134
+ modalOpen?: boolean;
135
+ userId?: string;
136
+ username?: string;
137
+ featureFlags?: HexGridFeatureFlags;
138
+ enableEnhanced?: boolean;
139
+ enhancedConfig?: {
140
+ useWasm?: boolean;
141
+ enableParticles?: boolean;
142
+ enableFlowField?: boolean;
143
+ };
144
+ onTerritoryEvent?: (event: {
145
+ type: 'conquest' | 'birth' | 'death';
146
+ toOwner: number;
147
+ fromOwner?: number;
148
+ }) => Promise<void>;
138
149
  }
139
150
 
140
151
  export interface UIState {
141
- debugOpen: boolean
142
- showStats: boolean
143
- cameraOpen: boolean
144
- showNarration: boolean
152
+ debugOpen: boolean;
153
+ showStats: boolean;
154
+ cameraOpen: boolean;
155
+ showNarration: boolean;
145
156
  }
146
157
 
147
158
  export interface WorkerDebug {
148
- curveUDeg: number
149
- curveVDeg: number
150
- batchPerFrame: number
151
- [key: string]: any
159
+ curveUDeg: number;
160
+ curveVDeg: number;
161
+ batchPerFrame: number;
162
+ [key: string]: any;
152
163
  }
@@ -12,14 +12,17 @@
12
12
  */
13
13
  export function getProxiedImageUrl(imageUrl: string): string {
14
14
  if (!imageUrl || typeof imageUrl !== 'string') {
15
- return imageUrl
15
+ return imageUrl;
16
16
  }
17
-
17
+
18
18
  // Only proxy preview.redd.it URLs (they have CORS issues)
19
19
  // external-preview.redd.it URLs work fine, so don't proxy them
20
- if (imageUrl.includes('preview.redd.it') && !imageUrl.includes('external-preview.redd.it')) {
21
- return `/api/proxy-image?url=${encodeURIComponent(imageUrl)}`
20
+ if (
21
+ imageUrl.includes('preview.redd.it') &&
22
+ !imageUrl.includes('external-preview.redd.it')
23
+ ) {
24
+ return `/api/proxy-image?url=${encodeURIComponent(imageUrl)}`;
22
25
  }
23
-
24
- return imageUrl
26
+
27
+ return imageUrl;
25
28
  }