@@ -129,6 +129,81 @@ export function drawPortraitPlaceholder(
129129 ctx . restore ( ) ;
130130}
131131
132+ /**
133+ * Draw portrait as a faded watermark with radial fade effect
134+ * Center is visible (~15-20% opacity), edges fade to transparent
135+ */
136+ export function drawWatermarkPortrait (
137+ ctx : CanvasRenderingContext2D ,
138+ portrait : HTMLImageElement ,
139+ centerX : number ,
140+ centerY : number ,
141+ size : number ,
142+ zoom : number = 1 ,
143+ panX : number = 0 ,
144+ panY : number = 0
145+ ) : void {
146+ ctx . save ( ) ;
147+
148+ // Create offscreen canvas for the watermark effect
149+ const watermarkCanvas = document . createElement ( 'canvas' ) ;
150+ watermarkCanvas . width = size ;
151+ watermarkCanvas . height = size ;
152+ const wmCtx = watermarkCanvas . getContext ( '2d' ) ;
153+ if ( ! wmCtx ) {
154+ ctx . restore ( ) ;
155+ return ;
156+ }
157+
158+ // Calculate image dimensions to cover the circle
159+ const imgAspect = portrait . width / portrait . height ;
160+ let drawWidth : number ;
161+ let drawHeight : number ;
162+
163+ if ( imgAspect > 1 ) {
164+ drawHeight = size ;
165+ drawWidth = size * imgAspect ;
166+ } else {
167+ drawWidth = size ;
168+ drawHeight = size / imgAspect ;
169+ }
170+
171+ drawWidth *= zoom ;
172+ drawHeight *= zoom ;
173+
174+ const maxPanX = Math . max ( 0 , ( drawWidth - size ) / 2 ) ;
175+ const maxPanY = Math . max ( 0 , ( drawHeight - size ) / 2 ) ;
176+
177+ const drawX = ( size - drawWidth ) / 2 + panX * maxPanX ;
178+ const drawY = ( size - drawHeight ) / 2 + panY * maxPanY ;
179+
180+ // Draw portrait with desaturation and brightness
181+ wmCtx . filter = 'grayscale(70%) brightness(1.4) contrast(0.6)' ;
182+ wmCtx . drawImage ( portrait , drawX , drawY , drawWidth , drawHeight ) ;
183+ wmCtx . filter = 'none' ;
184+
185+ // Apply radial gradient mask (center visible, edges transparent)
186+ wmCtx . globalCompositeOperation = 'destination-in' ;
187+ const gradient = wmCtx . createRadialGradient (
188+ size / 2 , size / 2 , 0 , // inner circle (center)
189+ size / 2 , size / 2 , size / 2 // outer circle (edge)
190+ ) ;
191+ gradient . addColorStop ( 0 , 'rgba(0,0,0,1)' ) ; // center: fully visible
192+ gradient . addColorStop ( 0.5 , 'rgba(0,0,0,0.8)' ) ; // mid: mostly visible
193+ gradient . addColorStop ( 0.8 , 'rgba(0,0,0,0.3)' ) ; // near edge: fading
194+ gradient . addColorStop ( 1 , 'rgba(0,0,0,0)' ) ; // edge: transparent
195+
196+ wmCtx . fillStyle = gradient ;
197+ wmCtx . fillRect ( 0 , 0 , size , size ) ;
198+
199+ // Draw the watermark onto main canvas with low opacity
200+ ctx . globalAlpha = 0.18 ;
201+ ctx . drawImage ( watermarkCanvas , centerX - size / 2 , centerY - size / 2 ) ;
202+ ctx . globalAlpha = 1 ;
203+
204+ ctx . restore ( ) ;
205+ }
206+
132207export function drawOvalPortrait (
133208 ctx : CanvasRenderingContext2D ,
134209 portrait : HTMLImageElement ,
@@ -343,11 +418,35 @@ export async function renderFrontSide(
343418 const background = await getHueShiftedTemplate ( backgroundSrc , templateHue , width , height ) ;
344419 drawTemplate ( offCtx , background , width , height ) ;
345420
346- // Layer 2: Draw badges - NO hue shift (keep original colors)
421+ // Layer 2: Draw watermark portrait on right side (if portrait available)
422+ if ( portraitSrc ) {
423+ try {
424+ const portrait = await loadImage ( portraitSrc ) ;
425+ // Position: right side of the bill, vertically centered
426+ // Size: roughly 60% of height for a prominent but subtle watermark
427+ const watermarkSize = height * 0.6 ;
428+ const watermarkX = width * 0.78 + 70 ; // Right side
429+ const watermarkY = height * 0.5 ; // Vertically centered
430+ drawWatermarkPortrait (
431+ offCtx ,
432+ portrait ,
433+ watermarkX ,
434+ watermarkY ,
435+ watermarkSize ,
436+ portraitZoom ,
437+ portraitPanX ,
438+ portraitPanY
439+ ) ;
440+ } catch ( e ) {
441+ console . error ( 'Failed to draw watermark:' , e ) ;
442+ }
443+ }
444+
445+ // Layer 3: Draw badges - NO hue shift (keep original colors)
347446 const badges = await loadImage ( badgesSrc ) ;
348447 drawTemplate ( offCtx , badges , width , height ) ;
349448
350- // Layer 3 : Draw portrait if available, or placeholder (UNDER the frame)
449+ // Layer 4 : Draw main portrait if available, or placeholder (UNDER the frame)
351450 if ( portraitSrc ) {
352451 try {
353452 const portrait = await loadImage ( portraitSrc ) ;
@@ -377,17 +476,17 @@ export async function renderFrontSide(
377476 ) ;
378477 }
379478
380- // Layer 4 : Draw frame ON TOP of portrait - NO hue shift (frame stays original)
479+ // Layer 5 : Draw frame ON TOP of portrait - NO hue shift (frame stays original)
381480 const frame = await loadImage ( frameSrc ) ;
382481 drawTemplate ( offCtx , frame , width , height ) ;
383482
384- // Layer 5 : Draw banner text ON TOP of frame
483+ // Layer 6 : Draw banner text ON TOP of frame
385484 // Calculate scale based on preview vs full resolution
386485 // Preview is 0.5 scale (width ~1816), full is 1.0 (width 3633)
387486 const scale = width / 3633 ;
388487 drawBannerText ( offCtx , hours , language , scale ) ;
389488
390- // Layer 6 : Draw name
489+ // Layer 7 : Draw name
391490 if ( name ) {
392491 drawText ( offCtx , name , layout . namePlate ) ;
393492 }
@@ -403,13 +502,17 @@ export async function renderBackSide(
403502 backgroundSrc : string ,
404503 badgesSrc : string ,
405504 frameSrc : string ,
505+ portraitSrc : string | null ,
406506 name : string ,
407507 email : string ,
408508 phone : string ,
409509 description : string ,
410510 layout : TemplateLayout ,
411511 width : number ,
412512 height : number ,
513+ portraitZoom : number = 1 ,
514+ portraitPanX : number = 0 ,
515+ portraitPanY : number = 0 ,
413516 templateHue : number = 0 ,
414517 hours : HourValue = 1 ,
415518 language : Language = 'de'
@@ -431,34 +534,57 @@ export async function renderBackSide(
431534 const background = await getHueShiftedTemplate ( backgroundSrc , templateHue , width , height ) ;
432535 drawTemplate ( offCtx , background , width , height ) ;
433536
434- // Layer 2: Draw badges - NO hue shift (keep original colors)
537+ // Layer 2: Draw watermark portrait in center (if portrait available)
538+ if ( portraitSrc ) {
539+ try {
540+ const portrait = await loadImage ( portraitSrc ) ;
541+ // Position: center of the bill
542+ const watermarkSize = height * 0.6 ;
543+ const watermarkX = width * 0.5 ;
544+ const watermarkY = height * 0.5 ;
545+ drawWatermarkPortrait (
546+ offCtx ,
547+ portrait ,
548+ watermarkX ,
549+ watermarkY ,
550+ watermarkSize ,
551+ portraitZoom ,
552+ portraitPanX ,
553+ portraitPanY
554+ ) ;
555+ } catch ( e ) {
556+ console . error ( 'Failed to draw watermark:' , e ) ;
557+ }
558+ }
559+
560+ // Layer 3: Draw badges - NO hue shift (keep original colors)
435561 const badges = await loadImage ( badgesSrc ) ;
436562 drawTemplate ( offCtx , badges , width , height ) ;
437563
438- // Layer 3 : Draw frame ON TOP - NO hue shift (frame stays original)
564+ // Layer 4 : Draw frame ON TOP - NO hue shift (frame stays original)
439565 const frame = await loadImage ( frameSrc ) ;
440566 drawTemplate ( offCtx , frame , width , height ) ;
441567
442- // Layer 4 : Draw banner text ON TOP of frame
568+ // Layer 5 : Draw banner text ON TOP of frame
443569 const scale = width / 3633 ;
444570 drawBannerText ( offCtx , hours , language , scale ) ;
445571
446- // Layer 5 : Draw contact info
572+ // Layer 6 : Draw contact info
447573 if ( layout . contactInfo && ( name || email || phone ) ) {
448574 drawContactInfo ( offCtx , name , email , phone , layout . contactInfo ) ;
449575 }
450576
451- // Layer 6 : Draw description
577+ // Layer 7 : Draw description
452578 if ( layout . description && description ) {
453579 drawMultilineText ( offCtx , description , layout . description ) ;
454580 }
455581
456- // Layer 7 : Draw name at bottom
582+ // Layer 8 : Draw name at bottom
457583 if ( name ) {
458584 drawText ( offCtx , name , layout . namePlate ) ;
459585 }
460586
461- // Layer 8 : Draw signature field
587+ // Layer 9 : Draw signature field
462588 if ( layout . signature ) {
463589 const signatureLabel = language === 'de' ? 'Unterschrift' : 'Signature' ;
464590 drawSignature ( offCtx , layout . signature , signatureLabel ) ;
0 commit comments