296 lines
33 KiB
JavaScript
296 lines
33 KiB
JavaScript
/* Client proposals. A4 layouts inspired by the original v17.6.7 covers.
|
||
* Coordinates are shared by preview and download; text is measured after local fonts load.
|
||
*/
|
||
(()=>{
|
||
'use strict';
|
||
const W=1000,H=1414,M=70,BOTTOM=1310,SCALE=2.4;
|
||
const SANS='"Caterium Manrope",Arial,sans-serif';
|
||
const SERIF='"Caterium Playfair",Georgia,serif';
|
||
const ITALIC='"Caterium Playfair Italic",Georgia,serif';
|
||
const definitions=[
|
||
['light','Минимализм','split','#fffdf8','#20362d','#98713b','serif','rows'],
|
||
['midnight-glass','Luxury Dark','night','#111916','#f6f1e5','#d4b272','serif','rows'],
|
||
['editorial-grid','Editorial Magazine','editorial','#f6f3ec','#183d30','#94743c','serif','cards'],
|
||
['warm-sun','Warm Sun','sun','#fff4d8','#51361e','#9a5b1b','serif','rows'],
|
||
['bento-cards','Bento Cards','bento','#f3f2eb','#27382b','#8b6022','sans','cards'],
|
||
['event-story','Event Story','story','#f4ece0','#523a2b','#92643c','serif','rows'],
|
||
['black-gold','Food First','photo','#10100e','#fff7e9','#d8b671','serif','cards'],
|
||
['personal-letter','Personal Letter','letter','#f7f1e6','#473b2d','#8b673b','italic','rows'],
|
||
['event-ticket','Event Ticket','ticket','#eef2f3','#203f53','#38677a','sans','rows'],
|
||
['solar-experience','Solar Experience','solar','#ffd54f','#28392c','#77521f','sans','cards'],
|
||
['midnight-compact','Ночной минимализм','compact','#122630','#edf4f4','#9cbec4','sans','compact'],
|
||
['emerald-gold','Изумрудная классика','frame','#123b31','#f8f2df','#d1b578','serif','rows'],
|
||
['neon-emerald','Изумрудный акцент','neon-vertical','#071f1d','#edfff6','#7debaf','sans','compact'],
|
||
['cream-elegance','Кремовая классика','arch','#faf3e6','#35432e','#917445','serif','cards'],
|
||
['neon-menu','Неоновое меню','neon','#111a15','#f1ffef','#a1ed92','sans','compact'],
|
||
['emerald-circles','Изумрудные круги','circles','#102e2c','#f0f6ee','#d3b779','serif','cards'],
|
||
['midnight-checklist','Тёмный чек-лист','checklist','#12241d','#f1f5ea','#d7c593','sans','rows'],
|
||
['gourmet-hero','Гастро-витрина','gallery','#181611','#fff6e6','#dbb775','serif','cards'],
|
||
['diamond-gold','Изумруд и золото','diamond','#133329','#fff6de','#dabe7b','serif','rows'],
|
||
['premium-dark','Премиум тёмный','panorama','#141820','#f7f2e9','#cfb18a','sans','rows'],
|
||
['premium-emerald','Премиум изумрудный','botanical','#0e382e','#f5f2df','#b8d19c','italic','cards']
|
||
];
|
||
const THEMES=Object.fromEntries(definitions.map(([id,name,cover,bg,ink,accent,type,menu])=>[id,{id,name,cover,bg,ink,accent,type,menu,dark:!['light','editorial-grid','warm-sun','bento-cards','event-story','personal-letter','event-ticket','solar-experience','cream-elegance'].includes(id)}]));
|
||
const IDs=Object.keys(THEMES);
|
||
const CURATED_IDS=['light','editorial-grid','midnight-glass','event-story','bento-cards','event-ticket'];
|
||
const names={'light':'Светлая классика','editorial-grid':'Редакционный','midnight-glass':'Вечерний','event-story':'Тёплый приём','bento-cards':'Гастро','event-ticket':'Приглашение'};
|
||
// Resolve old choices without rewriting orders or their saved snapshots.
|
||
const aliases={'warm-sun':'event-story','black-gold':'midnight-glass','personal-letter':'event-story','solar-experience':'bento-cards','midnight-compact':'midnight-glass','emerald-gold':'midnight-glass','neon-emerald':'midnight-glass','cream-elegance':'light','neon-menu':'bento-cards','emerald-circles':'midnight-glass','midnight-checklist':'midnight-glass','gourmet-hero':'bento-cards','diamond-gold':'midnight-glass','premium-dark':'midnight-glass','premium-emerald':'midnight-glass'};
|
||
const resolveTemplate=id=>CURATED_IDS.includes(id)?id:aliases[id]||'light';
|
||
const number=v=>Number.isFinite(Number(v))?Number(v):0;
|
||
const money=v=>Math.round(number(v)).toLocaleString('ru-RU')+' ₽';
|
||
const qty=v=>number(v).toLocaleString('ru-RU',{maximumFractionDigits:3});
|
||
const date=v=>{if(!v)return '';const d=new Date(String(v)+'T12:00:00');return Number.isNaN(d.getTime())?String(v):d.toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'})};
|
||
let fontsPromise;
|
||
async function ready(){
|
||
if(!fontsPromise)fontsPromise=(async()=>{
|
||
if(!window.FontFace||!document.fonts)return;
|
||
const defs=[['Caterium Manrope','Manrope.ttf','200 800'],['Caterium Playfair','PlayfairDisplay.ttf','400 900'],['Caterium Playfair Italic','PlayfairDisplay-Italic.ttf','400 900']];
|
||
await Promise.all(defs.map(async([family,file,weight])=>{
|
||
const face=new FontFace(family,`url("${new URL('fonts/'+file,document.baseURI)}")`,{weight});
|
||
try{await Promise.race([face.load(),new Promise((_,reject)=>setTimeout(()=>reject(new Error('Font timeout')),8000))]);document.fonts.add(face)}catch(e){console.warn('Proposal font fallback:',family,e.message)}
|
||
}));
|
||
// Other app/UI webfonts may still be loading indefinitely. Only the
|
||
// three awaited proposal faces determine this canvas layout.
|
||
})();
|
||
return fontsPromise;
|
||
}
|
||
function loadImage(src){return new Promise(resolve=>{
|
||
if(!/^data:image\/(?:png|jpe?g|webp);base64,/i.test(String(src||'')))return resolve(null);
|
||
const image=new Image();let done=false;
|
||
const finish=v=>{if(done)return;done=true;clearTimeout(timer);resolve(v)};
|
||
const timer=setTimeout(()=>finish(null),4000);image.onload=()=>finish(image);image.onerror=()=>finish(null);image.src=src;
|
||
})}
|
||
function split(ctx,value,width){
|
||
const out=[];
|
||
for(const para of String(value??'').split(/\r?\n/)){
|
||
if(!para.trim()){out.push('');continue}
|
||
let line='';
|
||
for(const word of para.trim().split(/\s+/)){
|
||
if(ctx.measureText((line?line+' ':'')+word).width<=width){line+=(line?' ':'')+word;continue}
|
||
if(line){out.push(line);line=''}
|
||
for(const char of word){if(line&&ctx.measureText(line+char).width>width){out.push(line);line=''}line+=char}
|
||
}
|
||
if(line)out.push(line);
|
||
}
|
||
return out;
|
||
}
|
||
const defaults={showClientName:true,showDate:true,showGuests:true,showGallery:true,showPriceBreakdown:true,showAmountPerGuest:true,showBoxCount:true,showControl:true,showExtras:true,showFooterNote:true,showFinalPhoto:true,metricMode:'weight',texts:{}};
|
||
function settings(s){return {...defaults,...s.pdfSettings,texts:{...defaults.texts,...s.pdfSettings?.texts,...s.offerOverrides?.texts}}}
|
||
function painter(canvas,theme){
|
||
const ctx=canvas.getContext('2d',{alpha:false});const scale=canvas.width/W;ctx.setTransform(scale,0,0,scale,0,0);
|
||
const boxes=canvas.__proposalLayout||[];canvas.__proposalLayout=boxes;
|
||
const body=(size=19,weight=400)=>`${weight} ${size}px ${SANS}`;
|
||
const display=(size=54)=>`${theme.type==='sans'?700:500} ${size}px ${theme.type==='sans'?SANS:theme.type==='italic'?ITALIC:SERIF}`;
|
||
function rect(x,y,w,h,color,r=0){ctx.fillStyle=color;ctx.beginPath();ctx.roundRect(x,y,w,h,r);ctx.fill()}
|
||
function line(x,y,w,color=theme.accent){rect(x,y,w,1,color)}
|
||
function lines(value,w,font){ctx.font=font;return split(ctx,value,w)}
|
||
function write(value,x,y,w,{size=19,weight=400,color=theme.ink,font=body(size,weight),lh=size*1.42,align='left',role='text'}={}){
|
||
const values=Array.isArray(value)?value:lines(value,w,font);ctx.save();ctx.font=font;ctx.textBaseline='top';ctx.textAlign=align;ctx.fillStyle=color;
|
||
const xx=align==='right'?x+w:align==='center'?x+w/2:x;
|
||
values.forEach((v,i)=>ctx.fillText(v,xx,y+i*lh));ctx.restore();
|
||
boxes.push({x,y,w,h:values.length*lh,role,text:values.join('\n'),font});return values.length*lh;
|
||
}
|
||
function fit(value,x,y,w,h,{size=54,min=18,font,displayFont=true,...opts}={}){
|
||
let f,ls,lh;
|
||
do{f=font|| (displayFont?display(size):body(size,opts.weight||500));lh=size*1.27;ls=lines(value,w,f);if(ls.length*lh<=h)break;size-=1}while(size>=min);
|
||
// Cover copy is repeated in the flowing details when unusually long.
|
||
if(ls.length*lh>h){canvas.__proposalOverflow=canvas.__proposalOverflow||[];canvas.__proposalOverflow.push(String(value));return write('Подробности на следующих страницах',x,y,w,{size:18,lh:25,...opts})}
|
||
return write(ls,x,y,w,{...opts,font:f,size,lh});
|
||
}
|
||
function photo(img,x,y,w,h,{radius=0,shape='rect',contain=false}={}){
|
||
ctx.save();ctx.beginPath();if(shape==='circle')ctx.ellipse(x+w/2,y+h/2,w/2,h/2,0,0,Math.PI*2);else if(shape==='diamond'){ctx.moveTo(x+w/2,y);ctx.lineTo(x+w,y+h/2);ctx.lineTo(x+w/2,y+h);ctx.lineTo(x,y+h/2);ctx.closePath()}else ctx.roundRect(x,y,w,h,radius);ctx.clip();
|
||
rect(x,y,w,h,theme.dark?'#263e34':'#e9e3d7');
|
||
if(img){const scale=(contain?Math.min:Math.max)(w/img.width,h/img.height);ctx.drawImage(img,x+(w-img.width*scale)/2,y+(h-img.height*scale)/2,img.width*scale,img.height*scale)}
|
||
ctx.restore();
|
||
}
|
||
return {ctx,body,display,rect,line,lines,write,fit,photo};
|
||
}
|
||
function page(t,kind){const c=document.createElement('canvas');c.width=W*(t.scale||SCALE);c.height=Math.round(H*(t.scale||SCALE));c.dataset.sunProposalTemplate=t.id;c.dataset.sunProposalPage=kind;const d=painter(c,t);d.rect(0,0,W,H,t.bg);return {c,...d}}
|
||
function logoForDocument(image,t){
|
||
if(!image)return null;
|
||
let canvas;try{canvas=window.CateriumBranding.prepareLogoImage(image)}catch(_){return null}
|
||
const ctx=canvas.getContext('2d',{willReadFrequently:true}),pixels=ctx.getImageData(0,0,canvas.width,canvas.height).data;
|
||
const linear=v=>{v/=255;return v<=.04045?v/12.92:((v+.055)/1.055)**2.4};
|
||
const luminance=(r,g,b)=>.2126*linear(r)+.7152*linear(g)+.0722*linear(b);
|
||
const rgb=t.bg.match(/[a-f\d]{2}/gi).map(v=>parseInt(v,16)),bg=luminance(...rgb);
|
||
let light=0,dark=0,weak=0,count=0;
|
||
for(let i=0;i<pixels.length;i+=4){const a=pixels[i+3]/255;if(a<.1)continue;const l=luminance(pixels[i],pixels[i+1],pixels[i+2]);count+=a;if((Math.max(l,bg)+.05)/(Math.min(l,bg)+.05)<2)weak+=a;if(l>.4)light+=a;else dark+=a}
|
||
// A full masthead, never a square tile, provides contrast for monochrome logos.
|
||
const alternate=count&&weak/count>.55;
|
||
return {canvas,bg:alternate?(light>dark?'#17251f':'#fffdf8'):t.bg,ink:alternate?(light>dark?'#fffdf8':'#20362d'):t.ink};
|
||
}
|
||
function brand(d,s,t,logo,{x=M,y=48,w=860}={}){
|
||
let ink=t.ink;
|
||
if(logo){
|
||
ink=logo.ink;
|
||
if(logo.bg!==t.bg)d.rect(0,0,W,y+84,logo.bg);
|
||
const image=logo.canvas,maxW=Math.min(280,w*.44),scale=Math.min(maxW/image.width,76/image.height),lw=image.width*scale,lh=image.height*scale;
|
||
const yy=y+(76-lh)/2;d.ctx.drawImage(image,x,yy,lw,lh);
|
||
(d.c.__proposalImages??=[]).push({role:'logo',x,y:yy,w:lw,h:lh,sourceWidth:image.width,sourceHeight:image.height,background:logo.bg});
|
||
x+=lw+32;w-=lw+32;
|
||
}
|
||
d.fit(s.brandName||'Моя компания',x,y+17,w,57,{size:23,min:15,displayFont:false,weight:700,color:ink});
|
||
}
|
||
function facts(s,cfg){return [cfg.showDate?[date(s.date),s.time].filter(Boolean).join(' · '):'',cfg.showGuests&&number(s.guests)?`${qty(s.guests)} гостей`:''].filter(Boolean)}
|
||
function cover(s,t,images,logo,cfg){
|
||
const d=page(t,'cover'),hero=images.find(Boolean),hero2=images.filter(Boolean)[1]||hero,hero3=images.filter(Boolean)[2]||hero;
|
||
const title=s.event||'Ваше мероприятие',client=cfg.showClientName?s.client||'':'',eyebrow=cfg.texts.proposalLabel||'Индивидуальное предложение';
|
||
const ink=t.ink,accent=t.accent;
|
||
const heading=(x,y,w,h=250,size=64)=>d.fit(title,x,y,w,h,{size});
|
||
const sub=(x,y,w,h=94)=>d.fit(client,x,y,w,h,{size:25,displayFont:false});
|
||
const label=(x,y,w=860)=>d.fit(eyebrow,x,y,w,54,{size:18,displayFont:false,color:accent});
|
||
const fact=(x,y,w=860)=>d.fit(facts(s,cfg).join(' / '),x,y,w,65,{size:21,displayFont:false});
|
||
const note=(x,y,w,h=140)=>d.fit(cfg.texts.heroNote||'Меню, подобранное для вашего события.',x,y,w,h,{size:22,displayFont:false,color:ink});
|
||
const total=(x,y,w=400)=>{d.fit(cfg.texts.totalPriceTitle||'Итоговая стоимость',x,y,w,50,{size:17,displayFont:false,color:accent});d.fit(money(s.pricing?.total),x,y+50,w,100,{size:52})};
|
||
if(!['night','photo'].includes(t.cover))brand(d,s,t,logo);
|
||
switch(t.cover){
|
||
case 'split':
|
||
label(M,198,400);heading(M,265,414,290,62);sub(M,582,395);d.line(M,718,370);note(M,758,380);d.photo(hero,520,195,410,760,{radius:180});fact(M,1010);total(M,1120);break;
|
||
case 'night':
|
||
d.photo(hero,482,155,518,H-155);d.rect(0,0,482,H,t.bg);brand(d,s,t,logo);label(M,235,350);heading(M,310,350,310,58);sub(M,650,345);fact(M,835,345);d.line(M,1000,340);total(M,1050,350);break;
|
||
case 'editorial':{
|
||
label(M,180);const bottom=244+heading(M,244,840,200,72),photoY=Math.max(435,bottom+66);d.line(M,photoY-32,860);d.photo(hero,414,photoY,516,1170-photoY);d.write('МЕНЮ / 01',M,photoY+4,290,{size:19,weight:700,color:accent});sub(M,photoY+95,280,145);fact(M,photoY+290,290);total(M,1040,310);break;}
|
||
case 'sun':
|
||
d.rect(758,180,172,172,'#f2c55b',86);label(M,200,550);heading(M,280,630,250,64);sub(M,562,780);d.photo(hero,320,698,610,390,{radius:180});fact(M,649,850);total(M,1140,530);break;
|
||
case 'bento':
|
||
label(M,180);heading(M,255,850,185,67);sub(M,465,800,65);note(M,545,850,52);d.photo(hero,M,635,556,405,{radius:26});d.photo(hero2,650,635,280,190,{radius:24});d.photo(hero3,650,850,280,190,{radius:24});fact(M,1080);total(M,1160,740);break;
|
||
case 'story':
|
||
label(M,182);heading(M,252,835,185,68);sub(M,470,820,75);note(M,556,840,64);d.line(M,670,860);['Меню','Подготовка','Ваше событие'].forEach((v,i)=>{const x=M+i*310;d.rect(x,662,16,16,accent,8);d.write(v,x,697,240,{size:18})});d.photo(hero,M,757,860,340);fact(M,1140,410);total(553,1130,377);break;
|
||
case 'photo':{
|
||
d.photo(hero,0,0,W,H);const g=d.ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,'rgba(0,0,0,.82)');g.addColorStop(.48,'rgba(0,0,0,.24)');g.addColorStop(1,'rgba(0,0,0,.95)');d.ctx.fillStyle=g;d.ctx.fillRect(0,0,W,H);brand(d,s,t,logo);label(M,215);heading(M,290,800,270,74);sub(M,620,800);fact(M,1010);total(M,1120,820);break;}
|
||
case 'letter':
|
||
d.rect(44,162,912,1125,'#fffcf5',4);label(94,218,700);d.fit(client?`${client},`:'Дорогие гости,',94,308,760,165,{size:56});d.write('Предложение для вашего мероприятия',94,496,730,{size:22});heading(94,549,730,170,44);note(94,761,470,155);d.photo(hero,690,769,210,210,{shape:'circle'});fact(94,1010,790);total(94,1100,570);break;
|
||
case 'ticket':
|
||
d.rect(M,190,860,1080,'#fbfcfa',18);d.rect(740,190,190,1080,'#dbe8ea',18);d.ctx.save();d.ctx.setLineDash([7,9]);d.ctx.strokeStyle=accent;d.ctx.beginPath();d.ctx.moveTo(716,215);d.ctx.lineTo(716,1240);d.ctx.stroke();d.ctx.restore();label(106,246,570);heading(106,329,560,220,59);sub(106,590,550,94);note(106,710,550,64);fact(106,822,550);d.photo(hero,106,930,255,180,{radius:10});total(390,946,280);d.write('МЕНЮ',762,268,150,{size:17,weight:700});if(cfg.showDate){d.fit(date(s.date),765,341,140,180,{size:26,displayFont:false});d.fit(s.time||'',765,570,140,80,{size:34,displayFont:false})}break;
|
||
case 'solar':
|
||
label(M,205);heading(M,276,460,300,64);d.photo(hero,571,390,359,359,{shape:'circle'});sub(M,649,435,150);d.line(M,860,860);fact(M,913);d.rect(M,1040,860,238,'#fff1b5',30);total(106,1080,780);break;
|
||
case 'compact':
|
||
label(M,212);heading(M,300,850,250,70);sub(M,590,820);fact(M,760);d.line(M,867,860);d.photo(hero,550,935,380,330);total(M,980,420);break;
|
||
case 'frame':
|
||
d.ctx.strokeStyle=accent;d.ctx.lineWidth=1;d.ctx.strokeRect(38,158,924,1140);label(96,227,808);d.fit(title,96,313,808,255,{size:67,align:'center'});d.fit(client,96,604,808,90,{size:24,displayFont:false,align:'center'});d.photo(hero,240,750,520,300,{radius:160});fact(96,1103,810);total(96,1175,810);break;
|
||
case 'neon-vertical':
|
||
d.rect(M,218,12,910,accent);label(114,218,815);heading(114,300,800,240,72);sub(114,571,780);d.photo(hero,114,750,390,390,{radius:195});d.photo(hero2,536,750,390,390,{radius:195});fact(114,668,800);total(114,1170,790);break;
|
||
case 'arch':
|
||
label(M,189);d.fit(title,M,265,860,205,{size:65,align:'center'});d.fit(client,M,490,860,94,{size:24,displayFont:false,align:'center'});d.photo(hero,244,648,512,445,{radius:[250,250,10,10]});fact(M,1140,420);total(555,1136,375);break;
|
||
case 'neon':
|
||
d.rect(M,190,860,328,accent,24);d.fit(eyebrow,105,222,790,54,{size:18,displayFont:false,color:'#142019'});d.fit(title,105,290,790,200,{size:62,color:'#142019'});sub(M,558,800);fact(M,660);d.photo(hero,M,785,520,425,{radius:24});d.photo(hero2,616,785,314,205,{radius:24});total(616,1040,314);break;
|
||
case 'circles':
|
||
label(M,213);heading(M,291,820,225,68);sub(M,550,820);d.photo(hero,M,731,400,400,{shape:'circle'});d.photo(hero2,513,711,256,256,{shape:'circle'});d.photo(hero3,701,919,229,229,{shape:'circle'});fact(M,646);total(M,1165,800);break;
|
||
case 'checklist':
|
||
label(M,212);heading(M,294,850,240,67);sub(M,568,810);fact(M,680);d.line(M,786,860);(s.items||[]).slice(0,3).forEach((it,i)=>{const y=843+i*99;d.write(String(i+1).padStart(2,'0'),M,y,70,{size:24,color:accent});d.fit(it.name,170,y,750,77,{size:24,displayFont:false})});total(M,1170,810);break;
|
||
case 'gallery':
|
||
label(M,203);heading(M,280,840,210,68);sub(M,528,810);[hero,hero2,hero3].forEach((im,i)=>d.photo(im,M+i*294,707+i*37,272,355,{radius:136}));fact(M,626);total(M,1160,830);break;
|
||
case 'diamond':
|
||
label(M,200);heading(M,284,840,220,66);sub(M,537,810);fact(M,648);d.photo(hero,430,735,500,455,{shape:'diamond'});d.write('МЕНЮ',M,879,225,{size:18,color:accent});total(M,969,330);break;
|
||
case 'panorama':
|
||
d.photo(hero,0,176,W,467);label(M,701);heading(M,778,840,220,65);sub(M,1040,430,140);fact(M,1209,410);total(550,1135,380);break;
|
||
case 'botanical':
|
||
label(M,199,460);heading(M,284,470,350,64);d.photo(hero,590,205,340,590,{radius:170});sub(M,686,460,155);d.line(M,901,860);note(M,958,475,140);fact(M,1150,445);total(568,1119,362);break;
|
||
}
|
||
return d.c;
|
||
}
|
||
function composition(item){const values=item.compositionTotal?.length?item.compositionTotal:item.composition||[];return (Array.isArray(values)?values:[values]).map(v=>typeof v==='string'?v:v?.name||'').filter(Boolean).join(' · ')}
|
||
async function renderPages(s){
|
||
const cfg=settings(s),items=s.items||[],t={...THEMES[resolveTemplate(s.offerTemplateId)],scale:items.length>24?2:SCALE},cache=new Map();
|
||
const img=src=>{if(!src)return Promise.resolve(null);if(!cache.has(src))cache.set(src,loadImage(src));return cache.get(src)};
|
||
await ready();
|
||
const [rawLogo,images,gallery]=await Promise.all([img(s.logo),Promise.all(items.map(i=>img((i.photoData||i.photo)===s.logo?'':i.photoData||i.photo))),Promise.all((s.finalGallery||[]).map(img))]);
|
||
const logo=logoForDocument(rawLogo,t);
|
||
const covers=[cover(s,t,images,logo,cfg)],pages=[...covers];let d,y;
|
||
const muted=t.dark?'#d1d6ce':'#515b50',lineColor=t.dark?'#426055':'#d9d2c4',panel=t.dark?'#203b30':'#ffffff';
|
||
// A sunny cover is paired with warm paper inside, making long menus easier to read.
|
||
const inner={...t,bg:t.id==='solar-experience'?'#fff9e8':t.bg};
|
||
function newPage(title){d=page(inner,'content');pages.push(d.c);brand(d,s,t,logo,{y:48});d.line(M,135,860,lineColor);y=172;if(title){y+=d.write(title,M,y,860,{size:36,font:d.display(36),lh:46});y+=28}}
|
||
function ensure(h,title){if(!d||y+h>BOTTOM)newPage(title)}
|
||
function flow(value,{size=19,color=t.ink,font,role='text',gap=20}={}){
|
||
if(!String(value||'').trim())return;
|
||
ensure(size*1.45+gap);const lines=d.lines(value,860,font||d.body(size));let pos=0;const lh=size*1.45;
|
||
while(pos<lines.length){let count=Math.floor((BOTTOM-y-gap)/lh);if(count<1){newPage();count=Math.floor((BOTTOM-y-gap)/lh)}const part=lines.slice(pos,pos+count);y+=d.write(part,M,y,860,{font:font||d.body(size),size,lh,color,role});pos+=part.length;if(pos<lines.length)newPage()}
|
||
y+=gap;
|
||
}
|
||
const title=cfg.texts.menuTitle||'Меню';newPage(title);
|
||
for(const value of covers[0].__proposalOverflow||[])flow(value,{size:22});
|
||
const meta=it=>[it.categoryName,it.weight?`Вес: ${it.weight}`:'',number(it.pieces)>0?`${qty(it.pieces)} шт. в порции`:''].filter(Boolean).join(' · ');
|
||
function priceLine(it){return `${qty(it.qty)} × ${money(it.unitPrice)} = ${money(it.sum)}`}
|
||
function measure(it,width,card){return d.lines(it.name||'Позиция меню',width,d.body(card?25:24,700)).length*(card?33:32)+10+(meta(it)?d.lines(meta(it),width,d.body(18)).length*26+10:0)+(composition(it)?d.lines(composition(it),width,d.body(20)).length*29:0)}
|
||
function details(it,x,yy,width,card){
|
||
yy+=d.write(it.name||'Позиция меню',x,yy,width,{size:card?25:24,weight:700,lh:card?33:32,role:'item-name'})+10;
|
||
if(meta(it))yy+=d.write(meta(it),x,yy,width,{size:18,lh:26,color:muted,role:'item-meta'})+10;
|
||
if(composition(it))yy+=d.write(composition(it),x,yy,width,{size:20,lh:29,color:muted,role:'composition'});
|
||
return yy;
|
||
}
|
||
function tallItem(it,index){
|
||
if(y>280)newPage(title+' · продолжение');
|
||
flow(it.name||'Позиция меню',{size:25,font:d.body(25,700),role:'item-name'});flow(meta(it),{size:18,color:muted});flow(composition(it),{size:20,color:muted,role:'composition'});flow(priceLine(it),{size:21,role:'item-price'});d.line(M,y-4,860,lineColor);y+=24;
|
||
}
|
||
if(!items.length)flow('Позиции меню пока не добавлены.',{color:muted});
|
||
if(t.id==='event-story'){
|
||
for(let index=0;index<items.length;index++){
|
||
const it=items[index],photoW=cfg.showGallery&&images[index]?190:0,x=M+24+(photoW?photoW+28:0),width=906-x;
|
||
const h=Math.max(photoW?138:0,measure(it,width,false))+90;
|
||
if(h>1060){tallItem(it,index);continue}
|
||
ensure(h+20,title+' · продолжение');d.rect(M,y,860,h,'#fffaf1',12);
|
||
if(photoW)d.photo(images[index],M+24,y+24,photoW,138,{radius:6});
|
||
details(it,x,y+24,width,false);const priceY=y+h-44;
|
||
d.line(x,priceY-10,width,lineColor);d.write(`${qty(it.qty)} × ${money(it.unitPrice)}`,x,priceY,width*.52,{size:18,color:muted,role:'quantity-price'});
|
||
d.write(money(it.sum),x+width*.55,priceY,width*.45,{size:23,weight:700,align:'right',role:'item-price'});y+=h+20;
|
||
}
|
||
}else if(t.menu==='cards'){
|
||
const cw=415,gap=30,photoH=cfg.showGallery?160:0;
|
||
for(let index=0;index<items.length;){
|
||
const pair=items.slice(index,index+2),heights=pair.map(it=>measure(it,cw-40,true)+photoH+132);
|
||
if(heights.some(h=>h>1080)){tallItem(items[index],index);index++;continue}
|
||
const h=Math.max(...heights);ensure(h+24,title+' · продолжение');
|
||
pair.forEach((it,k)=>{const x=M+k*(cw+gap),r=t.id==='editorial-grid'?0:16;d.rect(x,y,cw,h,t.id==='editorial-grid'?t.bg:panel,r);if(photoH)d.photo(images[index+k],x,y,cw,photoH,{radius:[r,r,0,0]});details(it,x+20,y+photoH+24,cw-40,true);const priceY=y+h-75;d.line(x+20,priceY-15,cw-40,lineColor);d.write(`${qty(it.qty)} × ${money(it.unitPrice)}`,x+20,priceY,cw-40,{size:18,color:muted,role:'quantity-price'});d.write(money(it.sum),x+20,priceY+28,cw-40,{size:23,weight:700,align:'right',role:'item-price'})});y+=h+24;index+=pair.length;
|
||
}
|
||
}else{
|
||
const photoW=cfg.showGallery&&images.some(Boolean)?(t.id==='event-ticket'?90:t.id==='midnight-glass'?120:145):0,tx=M+(photoW?photoW+25:0),tw=860-(tx-M)-185;
|
||
const rowHeight=it=>Math.max(photoW,measure(it,tw,false)+40,d.lines(money(it.sum),176,d.body(23,700)).length*32+12+d.lines(`${qty(it.qty)} × ${money(it.unitPrice)}`,176,d.body(16)).length*23)+42;
|
||
const totalMenuHeight=items.reduce((sum,it)=>sum+rowHeight(it),0),capacity=BOTTOM-y;
|
||
const target=totalMenuHeight/Math.max(1,Math.ceil(totalMenuHeight/Math.max(1,capacity)));let menuStart=y;
|
||
for(let index=0;index<items.length;index++){
|
||
const it=items[index],amountLines=d.lines(money(it.sum),176,d.body(23,700)),amountH=amountLines.length*32,unitLines=d.lines(`${qty(it.qty)} × ${money(it.unitPrice)}`,176,d.body(16)),h=Math.max(photoW,measure(it,tw,false)+40,amountH+12+unitLines.length*23)+42;
|
||
if(h>1080){tallItem(it,index);continue}
|
||
// Balance ordinary rows across pages instead of leaving a short tail page.
|
||
if(target>0&&y>menuStart&&y+h>menuStart+target+h*.4){newPage(title+' · продолжение');menuStart=y}
|
||
ensure(h,title+' · продолжение');if(t.id==='event-ticket'&&index%2===0)d.rect(M-12,y-12,884,h-20,'#e3ebec',6);if(photoW)d.photo(images[index],M,y,photoW,photoW,{radius:t.id==='midnight-glass'?photoW/2:6});details(it,tx,y,tw,false);
|
||
d.write(amountLines,754,y,176,{size:23,lh:32,weight:700,align:'right',role:'item-price'});
|
||
d.write(unitLines,754,y+amountH+12,176,{size:16,lh:23,color:muted,align:'right',role:'quantity-price'});
|
||
d.line(M,y+h-23,860,lineColor);y+=h;
|
||
}
|
||
}
|
||
const p=s.pricing||{},text=cfg.texts;
|
||
const rows=cfg.showPriceBreakdown?[[text.priceItemsLabel||'Стоимость позиций',p.base],...(number(p.manual)?[[text.discountLabel||'Скидка',-number(p.manual)]]:[]),...(number(p.promo)?[[`${text.promoLabel||'Промокод'}${s.promoCode?' '+s.promoCode:''}`,-number(p.promo)]]:[]),[text.menuAfterDiscountLabel||'Меню после скидок',p.itemsTotal],[text.deliveryLabel||'Доставка',p.delivery]]:[];
|
||
// Keep the calculation and its total together when they fit on one page.
|
||
const priceRowHeight=([label,value])=>Math.max(d.lines(label,610,d.body(19)).length*28,d.lines(money(value),215,d.body(21,700)).length*29)+22;
|
||
const summaryHeight=Math.max(60,d.lines(text.summaryTitle||text.totalLabel||'Итого',380,d.display(29)).length*39,d.lines(money(p.total),460,d.body(35,700)).length*45);
|
||
const pricingHeight=22+d.lines(text.pricingTitle||'Расчёт стоимости',860,d.display(34)).length*34*1.45+24+rows.reduce((n,row)=>n+priceRowHeight(row),0)+28+summaryHeight+26+85;
|
||
ensure(Math.min(pricingHeight,BOTTOM-172));
|
||
y+=22;flow(text.pricingTitle||'Расчёт стоимости',{size:34,font:d.display(34),gap:24});
|
||
for(const [label,value] of rows){
|
||
const ls=d.lines(label,610,d.body(19)),rs=d.lines(money(value),215,d.body(21,700)),h=Math.max(ls.length*28,rs.length*29)+22;ensure(h+95);
|
||
d.write(ls,M,y,610,{size:19,lh:28,color:muted,role:'pricing-label'});d.write(rs,715,y,215,{size:21,weight:700,lh:29,align:'right',role:'pricing-value'});y+=h;
|
||
}
|
||
ensure(28+summaryHeight+26);d.line(M,y,860,lineColor);y+=28;const totalLabel=d.lines(text.summaryTitle||text.totalLabel||'Итого',380,d.display(29));d.write(totalLabel,M,y,380,{size:29,font:d.display(29),lh:39,role:'total-label'});const amount=d.lines(money(p.total),460,d.body(35,700));y+=Math.max(60,totalLabel.length*39,d.write(amount,470,y,460,{size:35,weight:700,lh:45,align:'right',role:'total-value'}))+26;
|
||
const metrics=[];const guests=number(s.guests);
|
||
if(cfg.showBoxCount)metrics.push(`${text.boxCountLabel||'Количество боксов'}: ${qty(items.filter(i=>[0,5].includes(number(i.categoryId))).reduce((n,i)=>n+number(i.qty),0))}`);
|
||
if(cfg.showGuests&&guests){if(cfg.showAmountPerGuest)metrics.push(`${text.amountPerGuestLabel||'Сумма на гостя'}: ${money(number(p.itemsTotal)/guests)}`);if(cfg.metricMode==='pieces'){if(number(s.foodPieces))metrics.push(`${text.piecesMetricLabel||'Канапе на гостя'}: ${qty(Math.ceil(number(s.foodPieces)/guests))} шт.`)}else if(number(s.foodGrams))metrics.push(`${text.weightMetricLabel||'Вес еды на гостя'}: ${qty(Math.ceil(number(s.foodGrams)/guests/5)*5)} г`)}
|
||
if(metrics.length)flow(metrics.join(' · '),{size:17,color:muted});
|
||
if(cfg.showFooterNote&&text.footerNote)flow(text.footerNote,{size:17,color:muted,gap:28});
|
||
if(text.salesTitle&&text.salesNote){ensure(155);flow(text.salesTitle,{size:27,font:d.display(27),gap:16});flow(text.salesNote,{size:19,gap:28})}
|
||
const sections=[[cfg.showControl,text.controlTitle||s.controlTitle||'Организация мероприятия',cfg.controlLines||s.controlLines],[cfg.showExtras,text.extrasTitle||s.extrasTitle||'Дополнительно',cfg.extraServices||s.extraServices]].filter(([enabled,,values])=>enabled&&Array.isArray(values)&&values.length);
|
||
const columnH=([,heading,values])=>d.lines(heading,405,d.display(27)).length*36+24+values.reduce((sum,v)=>sum+d.lines('• '+String(v),405,d.body(18)).length*26+12,0);
|
||
if(sections.length===2&§ions.every(section=>columnH(section)<650)){
|
||
const h=Math.max(...sections.map(columnH));ensure(h+35);sections.forEach(([,heading,values],index)=>{const x=M+index*455;let yy=y;d.line(x,yy,405,lineColor);yy+=18;yy+=d.write(heading,x,yy,405,{size:27,font:d.display(27),lh:36})+18;for(const value of values)yy+=d.write('• '+String(value),x,yy,405,{size:18,lh:26})+12});y+=h+35;
|
||
}else for(const [,heading,values] of sections){ensure(135);flow(heading,{size:27,font:d.display(27),gap:16});for(const value of values)flow('• '+String(value),{size:18,gap:10});y+=18}
|
||
const pics=gallery.filter(Boolean).slice(0,2);
|
||
// Gallery fills existing space; it never creates a nearly empty extra page.
|
||
if(cfg.showGallery&&cfg.showFinalPhoto&&pics.length&&BOTTOM-y>=260){const label=text.finalGalleryTitle||'Сервировка вашего события',h=d.lines(label,860,d.display(27)).length*38;const available=BOTTOM-y-h-18;if(available>=180){y+=d.write(label,M,y,860,{size:27,font:d.display(27),lh:38})+18;const w=(860-22*(pics.length-1))/pics.length;pics.forEach((im,i)=>d.photo(im,M+i*(w+22),y,w,Math.min(340,available),{radius:8}))}}
|
||
pages.forEach((c,i)=>{const fd=painter(c,i?inner:t);const darkCover=i===0&&t.cover==='photo';if(i===0&&['night','photo'].includes(t.cover))fd.rect(0,1330,W,84,t.bg);fd.line(M,1343,860,darkCover?'#8b8b78':lineColor);fd.fit([s.brandName||'Моя компания',s.brandCity].filter(Boolean).join(' · '),M,s.brandContacts?1351:1361,720,26,{size:14,min:12,displayFont:false,color:darkCover?'#fff5e6':muted});if(s.brandContacts)fd.fit(s.brandContacts,M,1376,720,26,{size:12,min:10,displayFont:false,color:darkCover?'#fff5e6':muted});fd.write(`${i+1} / ${pages.length}`,850,1361,80,{size:14,align:'right',color:darkCover?'#fff5e6':muted,role:'page-number'});});
|
||
return pages;
|
||
}
|
||
const descriptions={split:'Светлая обложка, вертикальное фото и спокойная типографика.',night:'Тёмная полоса с текстом и фотография на всю высоту.',editorial:'Журнальная сетка, выразительные заголовки и карточки меню.',sun:'Тёплая бумага, солнечный акцент и широкая фотография.',bento:'Модульная фотокомпозиция и меню в двух колонках.',story:'История события с этапами подготовки и панорамным фото.',photo:'Фотография на всю обложку и золотистые акценты.',letter:'Личное обращение на светлой бумаге и курсивные заголовки.',ticket:'Обложка в форме приглашения с отдельной полосой даты.',solar:'Яркая солнечная обложка и светлые внутренние страницы.',compact:'Лаконичная ночная палитра и компактный список меню.',frame:'Тонкая золотистая рамка и симметричная композиция.', 'neon-vertical':'Вертикальный акцент и пара круглых фотографий.',arch:'Кремовая бумага, арочное фото и классические заголовки.',neon:'Яркий заголовок на тёмной бумаге и модульные фото.',circles:'Круглые фотографии разных размеров и изумрудная палитра.',checklist:'Нумерованные акценты меню на строгой тёмной обложке.',gallery:'Три фотографии в арках и крупная журнальная типографика.',diamond:'Фотография в ромбе и изумрудно-золотая палитра.',panorama:'Широкая панорама и сдержанные современные заголовки.',botanical:'Высокая арка, курсивные заголовки и глубокий изумрудный фон.'};
|
||
window.CateriumProposalPDF=Object.freeze({VERSION:'20260918-proposals-six',IDS:IDs,CURATED_IDS,resolveTemplate,templates:CURATED_IDS.map(id=>({id,name:names[id],desc:descriptions[THEMES[id].cover],thumb:`offer-templates/quality-${id}.jpg`})),ready,renderPages});
|
||
})();
|