diff --git a/package-lock.json b/package-lock.json index dda0cac..1271732 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "3.0.0", "license": "ISC", "dependencies": { + "@evoposter/evaluator": "file:src/@evoposter/evaluator", "@evoposter/nlp": "file:src/@evoposter/nlp", "cors": "^2.8.5", "cross-env": "^7.0.3", @@ -17,7 +18,6 @@ "pm2": "^5.3.0" }, "devDependencies": { - "@evoposter/evaluator": "file:src/@evoposter/evaluator", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.1.0", "@rollup/plugin-terser": "^0.4.3", @@ -10289,7 +10289,6 @@ }, "src/@evoposter/evaluator": { "version": "0.0.1", - "dev": true, "license": "ISC", "devDependencies": { "@rollup/plugin-terser": "^0.4.3", diff --git a/package.json b/package.json index 67bcfdd..c98efca 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "src/server.js", "type": "module", "dependencies": { + "@evoposter/evaluator": "file:src/@evoposter/evaluator", "@evoposter/nlp": "file:src/@evoposter/nlp", "cors": "^2.8.5", "cross-env": "^7.0.3", @@ -33,7 +34,6 @@ }, "homepage": "https://github.com/sergiomrebelo/evo-poster#readme", "devDependencies": { - "@evoposter/evaluator": "file:src/@evoposter/evaluator", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.1.0", "@rollup/plugin-terser": "^0.4.3", diff --git a/src/@evoposter/evaluator/lib/evaluator.min.js b/src/@evoposter/evaluator/lib/evaluator.min.js index 0bd461e..b85863a 100644 --- a/src/@evoposter/evaluator/lib/evaluator.min.js +++ b/src/@evoposter/evaluator/lib/evaluator.min.js @@ -1,2 +1,2 @@ -const t=(t,e,r,l,n)=>l+(t-e)/(r-e)*(n-l),e=(t,e,r)=>Math.min(r,Math.max(e,t)),r=t=>t.reduce(((t,e)=>t+e),0)/t.length||0,l=t=>t.reduce(((t,e)=>t+e),0),n=t=>Math.max(...t),a=t=>Math.min(...t),o=t=>t.filter(((t,e,r)=>r.indexOf(t)===e)),s=t=>{if(t.levels)return{r:parseInt(t.levels[0]),g:parseInt(t.levels[1]),b:parseInt(t.levels[2])};let e=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return e?{r:parseInt(e[1],16),g:parseInt(e[2],16),b:parseInt(e[3],16)}:null},i=(t,e)=>Math.sqrt(Math.pow(t.r-e.r,2)+Math.pow(t.g-e.g,2)+Math.pow(t.b-e.b,2)),f=10,h=[.8,.2],u={MAX_CONSTRAINT:1,WHITE_SPACE_FACTOR:3,MODES:["OVERSET","JUSTIFY","ATTEMPT_JUSTIFY"],DEFAULT_MAX_LIMIT_SCALE:1},p={MIN_RANGE:50,THRESHOLD_VALID:.2,MODES:["DIF","MIN"]},c={MODES:["BOTH","TYPE_FAMILY","CATEGORY"]},g=.5,d=10;const M=u.MAX_CONSTRAINT,E=u.WHITE_SPACE_FACTOR,T=u.DEFAULT_MAX_LIMIT_SCALE,I=u.MODES,b=(e,r)=>t(e=(e=e>=0?0:e)<=-r?-r:e,-r,0,M,0),A=(e,r)=>(e=Math.abs(e),t(e=e>r?r:e,r,0,M,0)),y=(t,e)=>A(t=t>=0?t/E:t,e),O={MODES:["RELATIVE","FIXED"]}.MODES;let m=p.MIN_RANGE,_=p.THRESHOLD_VALID,x=p.MODES;const S=(e,r)=>{const l=r.filter(((t,e,r)=>r.indexOf(t)===e)),n=[];for(let t of l)for(let l=0;ln.indexOf(t)!==e));let o=1;if(!a.length>=1){let a=0;for(let t in r){let o=r[t],s=e[t],i=l.indexOf(o);s!==n[i]&&a++}o=t(a,0,e.length,0,1)}return o},D=(l,o,s="DIF")=>{x.includes(s)||(s="DIF");const i=n(l),f=a(l);let h=Math.abs(i-f);if(h{let l=t(r,p,u,0,h);return l=e(l,0,h),l})),d=[];for(let t in l){let e=l[t],r=Math.abs(e-c),n=Math.abs(r-g[t]);d.push(n)}let M=t(r(d),0,h,1,0);return e(M,0,1)};var F={anger:{color:{typography:["#ff0000","#00ff00"],background:["#ff0000"]},typefaces:["sans-serif","neo-grotesque"]},joy:{color:{typography:[],background:["#ffff00","#00ff00"]},typefaces:["sans-serif","serif"]},trust:{color:{typography:[],background:["#0000ff","#00ff00"]},typefaces:["neo-grotesque"]},sadness:{color:{typography:[],background:["#0071b6"]},typefaces:[]},disgust:{color:{typography:["#800080"],background:[]},typefaces:[]}},L=Object.freeze({__proto__:null,default:F});const R=441.67,w=f,v=h,Y=g,C=d,N=(t,e,r=1)=>{let l=0,n=t.width*r*t.height*r;t.loadPixels();for(let r=0;r<4*n;r+=4){let n={r:t.pixels[r],g:t.pixels[r+1],b:t.pixels[r+2]};i(n,e){const n=[];for(let t in r)n.push(G(e[t],r[t]));let a=0,o=0;const s=l(t);for(let e in t)a+=n[e].x*t[e],o+=n[e].y*t[e];return a/=s,o/=s,{x:a,y:o}},G=(t,e)=>({x:t/2,y:e/2-e/20}),H=async(t,e,r,l)=>{let n=[],a=[];for(let o in r){n.push(r[o]*l[o]);const s=await t.get(0,t.height/2+e.center[o],t.width,e.l[o]);await s.loadPixels();let i=0,f=0,h=0;const u=s.pixels.length/4;for(let t=0;tt*a[e]))},P=(t=[],e,l="OVERSET",n=T)=>{I.includes(l)||(l="OVERSET");let a=[],o=e*n;for(let r of t){let t=e-r,n=M;switch(l){case"JUSTIFY":n=A(t,o);break;case"ATTEMPT_JUSTIFY":n=y(t,o);break;default:n=b(t,o)}a.push(n)}return r([...a])},$=(t,e,r=[],l=[],n={left:0,top:0,right:0,bottom:0})=>{let a=!1,o="",s=Math.abs(n.top)+Math.abs(n.bottom);for(let t of r)s+=parseFloat(t);let i=Math.abs(n.left)+Math.abs(n.right);for(let t of l)i+=parseFloat(t);return i=Math.round(i),s=Math.round(s),s>e?(a=!0,o+=`Grid height is bigger than container (grid:${s}, container:${e}). `):st?(a=!0,o+=`Grid width is bigger than container (grid:${i}, container:${t}). `):i{O.includes(n)||(n="RELATIVE");let o=0;"RELATIVE"===n?o=l(t):"FIXED"===n&&(o=a.height-(a.height*a.margin[1]+a.height*a.margin[3]));const s=t.map((t=>t/o));let i=[];for(let t in e){const r=Math.abs(e[t][3]-s[t]);i.push(r)}return 1-r(i)},X=(t,e,r=1,a=!0,o=[.4,.3,.3])=>{const s=e.map((t=>t[3]));let i,f=[D(t.map((t=>t.weight)),s),D(t.map((t=>t["font-stretch"])),s),r>1?S(t.map((t=>t["font-stretch"])),s):0],h=f.map(((t,e)=>t*o[e]));if(a)i=l(h);else{let t=f.map((t=>t>_)),e=0;for(let r of t)r&&e++;i=n(f)/e}return i},J=async(t,l,n,a,o=L)=>{let f=t.predominant.emotion;if(void 0===o.default[f])return 1;const h=o.default[f].color.typography,u=o.default[f].color.background,p=o.default[f].typefaces;let c=1;if(void 0!==h&&h.length>0){let t=[];for(let e of l){let r=s(e.color),l=Number.MAX_VALUE;for(let t of h){t=s(t);let e=i(r,t);e0){let t=[];for(let e of l){let r=0;const l=a.map((t=>t.family)).indexOf(e.typeface),n=a[l].tags;for(let t of p)n.includes(t)&&(r+=1/p.length);t.push(r)}d=t.length<1?1:r(t),d=e(d,0,1)}return(c+g+d)/3},q=(t,e,l=v)=>{if(t.length<2)return 1;let n=t,a=[];for(let t=0;tt.reduce(((t,r,l)=>t+r*e[l]),0))([r(a),1/o(e).length],l);return s},B=t=>{if(t.length<2)return 1;const e=t;let l=[];for(let t=0;t{null===r&&(e=s(e),r=N(t,e,t.pixelDensity()));return 1-4*Math.pow(r-l,2)},W=(t,e,r="BOTH")=>{V.includes(r)||(r="BOTH");let l=[.5,.5];"TYPE_FAMILY"===r?l=[1,0]:"CATEGORY"===r&&(l=[0,1]);let n=[],a=o(t),s=0,i=0;if("TYPE_FAMILY"!==r){const t=e.map((t=>t.family)),r=e.map((t=>t.category));for(let e of a){const l=t.indexOf(e);-1===l?n.push("undefined"):n.push(r[l])}n=o(n),s=1/n.length}"CATEGORY"!==r&&(i=1/a.length);return[i,s].reduce(((t,e,r)=>t+e*l[r]),0)},z=async(t=null,e,r,l,n,a=null)=>{const o=e.width,s=e.height,i=null===a?await H(t,r,l,n):a,f=k(i,l,n),h=G(o,s);let u=Math.pow((f.x-h.x)/o,2)+Math.pow((f.y-h.y)/s,2);return u=1-Math.pow(Math.abs(u/2),.5),u};var K=()=>{console.log("@evo-poster · evaluator v2.00")};export{q as alignment,K as default,$ as gridAppropriateSize,P as legibility,B as regularity,X as semanticsEmphasis,U as semanticsLayout,J as semanticsVisuals,W as typefaceParing,z as visualBalance,j as whiteSpaceFraction}; +const t=(t,e,r,l,n)=>l+(t-e)/(r-e)*(n-l),e=(t,e,r)=>Math.min(r,Math.max(e,t)),r=t=>t.reduce(((t,e)=>t+e),0)/t.length||0,l=t=>t.reduce(((t,e)=>t+e),0),n=t=>Math.max(...t),a=t=>Math.min(...t),o=t=>t.filter(((t,e,r)=>r.indexOf(t)===e)),s=t=>{if(t.levels)return{r:parseInt(t.levels[0]),g:parseInt(t.levels[1]),b:parseInt(t.levels[2])};let e=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return e?{r:parseInt(e[1],16),g:parseInt(e[2],16),b:parseInt(e[3],16)}:null},i=(t,e)=>Math.sqrt(Math.pow(t.r-e.r,2)+Math.pow(t.g-e.g,2)+Math.pow(t.b-e.b,2)),f=10,h=[.8,.2],u={MAX_CONSTRAINT:1,WHITE_SPACE_FACTOR:3,MODES:["OVERSET","JUSTIFY","ATTEMPT_JUSTIFY"],DEFAULT_MAX_LIMIT_SCALE:1},c={MIN_RANGE:50,THRESHOLD_VALID:.2,MODES:["DIF","MIN"]},p={MODES:["BOTH","TYPE_FAMILY","CATEGORY"]},g={VISUAL_CENTER_FT:20,MODES:["CENTER","LEFT-CENTER","RIGHT-CENTER","LEFT-TOP","RIGHT-TOP","LEFT-BOTTOM","RIGHT-BOTTOM"]},d=.5,E=10;const T=u.MAX_CONSTRAINT,M=u.WHITE_SPACE_FACTOR,I=u.DEFAULT_MAX_LIMIT_SCALE,O=u.MODES,A=(e,r)=>t(e=(e=e>=0?0:e)<=-r?-r:e,-r,0,T,0),b=(e,r)=>(e=Math.abs(e),t(e=e>r?r:e,r,0,T,0)),y=(t,e)=>b(t=t>=0?t/M:t,e),_={MODES:["RELATIVE","FIXED"]}.MODES;let m=c.MIN_RANGE,R=c.THRESHOLD_VALID,S=c.MODES;const x=(e,r)=>{const l=r.filter(((t,e,r)=>r.indexOf(t)===e)),n=[];for(let t of l)for(let l=0;ln.indexOf(t)!==e));let o=1;if(!a.length>=1){let a=0;for(let t in r){let o=r[t],s=e[t],i=l.indexOf(o);s!==n[i]&&a++}o=t(a,0,e.length,0,1)}return o},F=(l,o,s="DIF")=>{S.includes(s)||(s="DIF");const i=n(l),f=a(l);let h=Math.abs(i-f);if(h{let l=t(r,c,u,0,h);return l=e(l,0,h),l})),d=[];for(let t in l){let e=l[t],r=Math.abs(e-p),n=Math.abs(r-g[t]);d.push(n)}let E=t(r(d),0,h,1,0);return e(E,0,1)};var L={anger:{color:{typography:["#ff0000","#00ff00"],background:["#ff0000"]},typefaces:["sans-serif","neo-grotesque"]},joy:{color:{typography:[],background:["#ffff00","#00ff00"]},typefaces:["sans-serif","serif"]},trust:{color:{typography:[],background:["#0000ff","#00ff00"]},typefaces:["neo-grotesque"]},sadness:{color:{typography:[],background:["#0071b6"]},typefaces:[]},disgust:{color:{typography:["#800080"],background:[]},typefaces:[]}},D=Object.freeze({__proto__:null,default:L});const C=441.67,N=f,w=h,v=d,G=E,H=(t,e,r=1)=>{let l=0,n=t.width*r*t.height*r;t.loadPixels();for(let r=0;r<4*n;r+=4){let n={r:t.pixels[r],g:t.pixels[r+1],b:t.pixels[r+2]};i(n,e){const a=[];for(let e in n)a.push(U(t,r[e],n[e]));let o=0,s=0;const i=l(e);for(let t in e)o+=a[t].x*e[t],s+=a[t].y*e[t];return o/=i,s/=i,{x:o,y:s}},U=(t,e,r,l=[0,0,0,0])=>{const n=(t=String(t)).includes("LEFT")?1:t.includes("RIGHT")?2:0,a=t.includes("TOP")?1:t.includes("BOTTOM")?2:0;return{x:0===n?e/2:1===n?e*l[0]:e*(1-l[2]),y:0===a?r/2-r/Y:1===a?r*l[1]:r-r*l[3]}},$=async(t,e,r,l)=>{let n=[],a=[];for(let o in r){n.push(r[o]*l[o]);const s=await t.get(0,t.height/2+e.center[o],t.width,e.l[o]);await s.loadPixels();let i=0,f=0,h=0;const u=s.pixels.length/4;for(let t=0;tt*a[e]))},X=(t=[],e,l="OVERSET",n=I)=>{O.includes(l)||(l="OVERSET");let a=[],o=e*n;for(let r of t){let t=e-r,n=T;switch(l){case"JUSTIFY":n=b(t,o);break;case"ATTEMPT_JUSTIFY":n=y(t,o);break;default:n=A(t,o)}a.push(n)}return r([...a])},B=(t,e,r=[],l=[],n={left:0,top:0,right:0,bottom:0})=>{let a=!1,o="",s=Math.abs(n.top)+Math.abs(n.bottom);for(let t of r)s+=parseFloat(t);let i=Math.abs(n.left)+Math.abs(n.right);for(let t of l)i+=parseFloat(t);return i=Math.round(i),s=Math.round(s),s>e?(a=!0,o+=`Grid height is bigger than container (grid:${s}, container:${e}). `):st?(a=!0,o+=`Grid width is bigger than container (grid:${i}, container:${t}). `):i{_.includes(n)||(n="RELATIVE");let o=0;"RELATIVE"===n?o=l(t):"FIXED"===n&&(o=a.height-(a.height*a.margin[1]+a.height*a.margin[3]));const s=t.map((t=>t/o));let i=[];for(let t in e){const r=Math.abs(e[t][3]-s[t]);i.push(r)}return 1-r(i)},q=(t,e,r=1,a=!0,o=[.4,.3,.3])=>{const s=e.map((t=>t[3]));let i,f=[F(t.map((t=>t.weight)),s),F(t.map((t=>t["font-stretch"])),s),r>1?x(t.map((t=>t["font-stretch"])),s):0],h=f.map(((t,e)=>t*o[e]));if(a)i=l(h);else{let t=f.map((t=>t>R)),e=0;for(let r of t)r&&e++;i=n(f)/e}return i},j=async(t,l,n,a,o=D)=>{let f=t.predominant.emotion;if(void 0===o.default[f])return 1;const h=o.default[f].color.typography,u=o.default[f].color.background,c=o.default[f].typefaces;let p=1;if(void 0!==h&&h.length>0){let t=[];for(let e of l){let r=s(e.color),l=Number.MAX_VALUE;for(let t of h){t=s(t);let e=i(r,t);e0){let t=[];for(let e of l){let r=0;const l=a.map((t=>t.family)).indexOf(e.typeface),n=a[l].tags;for(let t of c)n.includes(t)&&(r+=1/c.length);t.push(r)}d=t.length<1?1:r(t),d=e(d,0,1)}return(p+g+d)/3},W=(t,e,l=w)=>{if(t.length<2)return 1;let n=t,a=[];for(let t=0;tt.reduce(((t,r,l)=>t+r*e[l]),0))([r(a),1/o(e).length],l);return s},z=t=>{if(t.length<2)return 1;const e=t;let l=[];for(let t=0;t{null===r&&(e=s(e),r=H(t,e,t.pixelDensity()));return 1-4*Math.pow(r-l,2)},Q=(t,e,r="BOTH")=>{V.includes(r)||(r="BOTH");let l=[.5,.5];"TYPE_FAMILY"===r?l=[1,0]:"CATEGORY"===r&&(l=[0,1]);let n=[],a=o(t),s=0,i=0;if("TYPE_FAMILY"!==r){const t=e.map((t=>t.family)),r=e.map((t=>t.category));for(let e of a){const l=t.indexOf(e);-1===l?n.push("undefined"):n.push(r[l])}n=o(n),s=1/n.length}"CATEGORY"!==r&&(i=1/a.length);return[i,s].reduce(((t,e,r)=>t+e*l[r]),0)},Z=async(t=null,r,l,n,a,o="CENTER",s=null)=>{const i=r.width,f=r.height;P.includes(o)||(o="CENTER");const h=null===s?await $(t,l,n,a):s,u=k(o,h,n,a);let c=U(o,i,f,r.margin),p=Math.pow((u.x-c.x)/i,2)+Math.pow((u.y-c.y)/f,2);return p=1-Math.pow(Math.abs(p/2),.5),e(p,0,1)};var tt=()=>{console.log("@evo-poster · evaluator v2.00")};export{W as alignment,tt as default,B as gridAppropriateSize,X as legibility,z as regularity,q as semanticsEmphasis,J as semanticsLayout,j as semanticsVisuals,Q as typefaceParing,Z as visualBalance,K as whiteSpaceFraction}; //# sourceMappingURL=evaluator.min.js.map diff --git a/src/@evoposter/evaluator/lib/evaluator.min.js.map b/src/@evoposter/evaluator/lib/evaluator.min.js.map index 77124c5..390f657 100644 --- a/src/@evoposter/evaluator/lib/evaluator.min.js.map +++ b/src/@evoposter/evaluator/lib/evaluator.min.js.map @@ -1 +1 @@ -{"version":3,"file":"evaluator.min.js","sources":["../src/utils.js","../src/metrics.config.js","../src/metrics/Legibility.mjs","../src/metrics/SemanticsLayout.mjs","../src/metrics/SemanticsEmphasis.mjs","../semantics-visual.config.js","../src/metrics/SemanticVisuals.mjs","../src/metrics/Alignment.mjs","../src/metrics/WhiteSpaceFraction.mjs","../src/metrics/TypefaceParing.mjs","../src/metrics/VisualBalance.mjs","../src/index.mjs","../src/metrics/GridAppropriateSize.mjs","../src/metrics/Regularity.mjs"],"sourcesContent":["export const map = (value, minA, maxA, minB, maxB) => {\n return minB + (maxB - minB) * ((value - minA) / (maxA - minA));\n}\n\nexport const constraint = (value, min, max) => {\n return Math.min(max, Math.max(min, value));\n}\n\nexport const arrMean = (arr) => {\n const sum = arr.reduce((a, b) => a + b, 0);\n return (sum / arr.length) || 0;\n}\n\nexport const arrSum = (arr) => {\n return arr.reduce((partialSum, a) => partialSum + a, 0);\n}\n\nexport const arrMax = (arr) => {\n return Math.max(...arr);\n}\n\nexport const arrMin = (arr) => {\n return Math.min(...arr);\n}\n\nexport const arrUnique = (arr) => {\n return arr.filter((value, index, array) => array.indexOf(value) === index);\n}\n\nexport const sumProduct = (arr, weights) => {\n return arr.reduce((s, v, i) => s + v * weights[i], 0);\n}\n\nexport const hexToRGB = (hex) => {\n if (hex[\"levels\"]) {\n return {\n r: parseInt(hex[\"levels\"][0]),\n g: parseInt(hex[\"levels\"][1]),\n b: parseInt(hex[\"levels\"][2])\n }\n }\n\n let result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return result ? {\n r: parseInt(result[1], 16),\n g: parseInt(result[2], 16),\n b: parseInt(result[3], 16)\n } : null;\n}\n\nexport const colorDistance = (a, b) => {\n return Math.sqrt(Math.pow(a.r - b.r, 2) + Math.pow(a.g - b.g, 2) + Math.pow(a.b - b.b, 2));\n}","export const ALIGNMENT = {\n \"A\": 10, // limit to the non-linear function\n \"WEIGHTS\": [.8, .2] // alignment parts weights\n}\n\nexport const LEGIBILITY = {\n \"MAX_CONSTRAINT\": 1,\n \"WHITE_SPACE_FACTOR\": 3,\n \"MODES\": [`OVERSET`, `JUSTIFY`,`ATTEMPT_JUSTIFY`],\n \"DEFAULT_MAX_LIMIT_SCALE\": 1\n}\n\nexport const REGULARITY = {\n \"A\": 10, // limit to the non-linear function\n}\n\nexport const SEMANTICS_EMPHASIS = {\n \"MIN_RANGE\": 50,\n \"THRESHOLD_VALID\": 0.2,\n \"MODES\": [`DIF`, `MIN`]\n}\n\nexport const SEMANTICS_VISUALS = {\n \"MAX_COLOR_DISTANCE\": 441.67\n}\n\nexport const SEMANTICS_LAYOUT = {\n \"MODES\": [`RELATIVE`, `FIXED`]\n}\n\nexport const TYPEFACE_PARING = {\n \"MODES\": [`BOTH`, `TYPE_FAMILY`, `CATEGORY`]\n}\n\nexport const VISUAL_BALANCE = {\n \"VISUAL_CENTER_FT\": 20,\n}\n\nexport const WHITE_SPACE_FRACTION = {\n \"OPTIMAL\": .5,\n \"MIN_DISTANCE\": 10\n}\n\n\nexport default {\n \"DEBUG\": false,\n}","/**\n * Legibility\n *\n * Measure the legibility of the text in the poster\n * it is related to the legibility of the sentence\n * and not the typeface shapes\n *\n * Expected return a value between 0 (good) and (1) bad\n *\n * Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * srebelo[at]dei.uc.pt\n *\n * v1.0.0 August 2018 (as part of evoPoster)\n * v2.0.0 November 2020 (as part of evoPoster)\n * v2.5.0 November 2021 (as part of evoPoster)\n * v3.0.0 November 2023\n */\n\nimport {arrMean, map} from \"../utils.js\";\nimport {LEGIBILITY} from \"../metrics.config.js\";\n\nconst MAX_CONSTRAINT = LEGIBILITY[\"MAX_CONSTRAINT\"];\nconst WHITE_SPACE_FACTOR = LEGIBILITY[\"WHITE_SPACE_FACTOR\"];\nconst DEFAULT_MAX_LIMIT_SCALE = LEGIBILITY[\"DEFAULT_MAX_LIMIT_SCALE\"];\nconst AVAILABLE_MODES = LEGIBILITY [\"MODES\"]\n\nexport const compute = (sentencesLength = [], minSize, mode= 'OVERSET', maxLimitScale= DEFAULT_MAX_LIMIT_SCALE) => {\n if (!AVAILABLE_MODES.includes(mode)) {\n mode = `OVERSET`;\n }\n let results = [];\n let max = minSize * maxLimitScale;\n for (let sentence of sentencesLength) {\n let dif = minSize-sentence;\n let value = MAX_CONSTRAINT;\n switch (mode) {\n case `JUSTIFY`:\n value = justify(dif, max);\n break;\n case `ATTEMPT_JUSTIFY`:\n value = attemptJustify(dif, max);\n break;\n default:\n value = overset(dif, max);\n break;\n }\n results.push(value);\n }\n\n // calculate mean\n const mean = arrMean([...results]);\n\n return mean;\n}\n\nconst overset = (value, max) => {\n // only prejudice when text overfits the poster\n // if dif bigger than 0\n value = value >= 0 ? 0 : value;\n // if dif lower than limit\n value = value <= -max ? -max : value;\n // transform in scale of 1 (bad) to 0 (good)\n return map (value, -max, 0, MAX_CONSTRAINT, 0);\n}\n\nconst justify = (value, max) => {\n // prejudice both overset and small lettering\n value = Math.abs(value)\n // if the dif is bigger than max\n value = value > max ? max : value;\n // transform in scale of 1 (bad) to 0 (good)\n return map (value, max, 0, MAX_CONSTRAINT, 0);\n}\n\n// attempt to justify the text\n// prejudice more when the text overset box than when it is smaller\nconst attemptJustify = (value, max) => {\n // if dif bigger than 0 (it is not overset), soften the value\n value = value >= 0 ? value/WHITE_SPACE_FACTOR : value;\n return justify(value, max);\n}\n\nexport { compute as default };","/**\n * Layout Semantics\n *\n * Measure the appropriate of the layout\n * based on the importance of the content\n * it considers that most important parts of content\n * are those have more emotions related\n *\n * return the mean of difference between all textboxes,\n * a value between 1 (good) and 0 (bad)\n *\n * two modes: RELATIVE and FIXED\n * RELATIVE mode is related to the composition height\n * FIXED height is related to the container's height\n *\n * Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * srebelo[at]dei.uc.pt\n *\n * v1.0.0 April 2020\n * v2.0.0 November 2023\n */\n\nimport {arrMean, arrSum} from \"../utils.js\";\nimport {SEMANTICS_LAYOUT} from \"../metrics.config.js\";\n\nconst AVAILABLE_MODES = SEMANTICS_LAYOUT[\"MODES\"];\n\nexport const compute = (textboxesHeights = [], dist = null, mode=`RELATIVE`, size = {height: 100, margin:[0,0,0,0]}) => {\n // mode validation\n if (!AVAILABLE_MODES.includes(mode)) mode = `RELATIVE`;\n\n // define max height\n let height = 0;\n if (mode === `RELATIVE`) {\n // using the textbox height\n height = arrSum(textboxesHeights);\n } else if (mode === `FIXED`) {\n // using container height\n height = size.height - ((size.height*size.margin[1]) + (size.height*size.margin[3]));\n }\n const percents = textboxesHeights.map((p) => p/height);\n\n // calculate distances\n let distances = [];\n for (let i in dist) {\n const dif = Math.abs(dist[i][3] - percents[i]);\n distances.push(dif);\n }\n\n // return average\n return (1-arrMean(distances));\n}\n\nexport { compute as default };","/**\n * Semantics Emphasis on Visuals\n *\n * Measure the appropriate of the visual features\n * based on the importance of the content\n * it considers that most important parts of content\n * are those have more emotions related\n *\n * return the mean of difference between all textboxes,\n * a value between 1 (good) and 0 (bad)\n *\n * two modes: MIN and DIF\n * MIN takes into account that the most important parts\n * are typeset in the min value possible in the container\n * DIF: take into account only the difference between the parts\n * selects a style and compare based on the distribution of emotions.\n *\n *\n *\n * Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * srebelo[at]dei.uc.pt\n *\n * v1.0.0 November 2023\n */\nimport {arrMax, arrMean, arrMin, arrSum, constraint, map} from \"../utils.js\";\nimport {SEMANTICS_EMPHASIS} from \"../metrics.config.js\";\n\nlet MIN_RANGE = SEMANTICS_EMPHASIS[\"MIN_RANGE\"];\nlet THRESHOLD_VALID = SEMANTICS_EMPHASIS[\"THRESHOLD_VALID\"];\nlet AVAILABLE_MODES = SEMANTICS_EMPHASIS[\"MODES\"];\n\n// by tradition, use more that a method to emphasize\n// the text is considered \"typecrime\".\n// this method enables turn on/off this feature\n// by using the param allowMultiple\nexport const compute = (textboxes, dist, noCurrentTypefaces = 1, allowMultiple = true, weights = [0.4, 0.3, 0.3]) => {\n // if textboxes size is 1 ---> 1\n const perDist = dist.map((e) => e[3]);\n\n const fontWeight = checkDifferenceVariableFeature(textboxes.map((b) => b[\"weight\"]), perDist);\n const fontStretch = checkDifferenceVariableFeature(textboxes.map((b) => b[\"font-stretch\"]), perDist);\n let typefaceDesign = noCurrentTypefaces > 1 ? (checkDifferenceUniqueFeatures(textboxes.map((b) => b[\"font-stretch\"]), perDist)) : 0;\n\n // way of combine and only checks one\n let res = [fontWeight, fontStretch, typefaceDesign];\n let weightedRes = res.map((x,i) => x*weights[i]);\n\n let value;\n\n if (!allowMultiple) {\n // if not allowed multiple emphasis\n // we check what fields are active\n // and penalty when there is active fields\n let active = res.map((r) => r > THRESHOLD_VALID);\n let c = 0;\n for (let a of active) {\n if (a) {\n c++;\n }\n }\n value = arrMax(res)/c;\n } else {\n value = arrSum(weightedRes);\n }\n\n return value;\n}\n\n// check the levels\nconst checkDifferenceUniqueFeatures = (currentFeatures, dist) => {\n // available semantic level\n const uniqueValues = dist.filter((value, index, array) => array.indexOf(value) === index);\n const target = [];\n\n // define the target typeface for each semantic level\n for (let uq of uniqueValues) {\n for (let i=0; i target.indexOf(item) !== index);\n\n let value = 1;\n // if there is duplicate in levels\n // (if yes, difference is max)\n if (!duplicates.length >= 1) {\n // count the amount of tb not in the same typeface\n let c = 0;\n // for each typeface\n for (let i in dist) {\n let level = dist[i];\n let currentValue = currentFeatures[i];\n // get unique index of current semantic level\n let index = uniqueValues.indexOf(level);\n // get target value\n let targetValue = target[index];\n if (currentValue !== targetValue) {\n // if not the same as target\n c++;\n }\n }\n // map value to a value between 0 (no difference) and 1 (max difference)\n value = map(c, 0, currentFeatures.length, 0, 1);\n }\n\n return value;\n}\n\nconst checkDifferenceVariableFeature = (currentFeatures, dist, mode = `DIF`) => {\n if (!AVAILABLE_MODES.includes(mode)) {\n mode = `DIF`;\n }\n // max feature range\n const maxFeature = arrMax(currentFeatures);\n const minFeature = arrMin(currentFeatures);\n let range = Math.abs(maxFeature - minFeature);\n if (range < MIN_RANGE) {\n return 1;\n }\n\n // semantic data range\n const maxSemantic = arrMax(dist);\n const minSemantic = arrMin(dist);\n\n // consider the current variable minimum\n let def = minFeature;\n if (mode === `DIF`) {\n // selects a style used in the first min semantic textbox\n for (let i in dist) {\n if (dist[i] === minSemantic) {\n // consider the difference\n def = currentFeatures[i];\n break;\n }\n }\n }\n\n // create target feature values\n const target = dist.map((e) => {\n // The most neutral sentence are the most regular\n let v = map(e, minSemantic, maxSemantic, 0, range);\n v = constraint(v, 0, range);\n return v;\n });\n\n // distance to target\n const current = [];\n for (let i in currentFeatures) {\n let w = currentFeatures[i];\n let currentDistance = Math.abs(w - def);\n let dif = Math.abs(currentDistance - target[i]);\n current.push(dif);\n }\n\n let mean = map(arrMean(current), 0, range, 1, 0);\n return constraint(mean, 0, 1);\n}\n\n\n\n\nexport { compute as default };","export default {\n anger : {\n color: {\n typography: [`#ff0000`, `#00ff00`],\n background: [`#ff0000`]\n },\n typefaces: [`sans-serif`, `neo-grotesque`]\n },\n joy: {\n color: {\n typography: [],\n background: [`#ffff00`, `#00ff00`]\n },\n typefaces: [`sans-serif`, `serif`]\n },\n trust: {\n color: {\n typography: [],\n background: [`#0000ff`, `#00ff00`]\n },\n typefaces: [`neo-grotesque`]\n },\n sadness: {\n color: {\n typography: [],\n background: [`#0071b6`]\n },\n typefaces: []\n },\n disgust: {\n color: {\n typography: [`#800080`],\n background: []\n },\n typefaces: []\n }\n}","/**\n * Semantic Visuals Measure\n *\n * This function assesses the appropriateness of visual features (type design and colour)\n * based on the emotions collected. It takes into account the configuration file \"visual-semantics.config.js\".\n *\n * The function returns the mean difference between all textboxes,\n * yielding a value between 1 (good) and 0 (bad).\n * If there is no information pertaining to a particular emotion, the value defaults to 1.\n *\n * As of now, the function exclusively considers the predominant emotions from ML analysis.\n *\n * Author: Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * Contact: srebelo[at]dei.uc.pt\n *\n * Version: 1.0.0 (November 2023)\n */\n\nimport {SEMANTICS_VISUALS} from \"../metrics.config.js\";\n\nconst MAX_COLOR_DISTANCE = SEMANTICS_VISUALS[\"MAX_COLOR_DISTANCE\"];\n\nimport * as configurationFile from \"../../semantics-visual.config.js\";\nimport {arrMean, colorDistance, hexToRGB, constraint} from \"../utils.js\";\n\n\nexport const compute = async (data, textboxes, background, typefaceData, config = configurationFile) => {\n\n let emotion = data.predominant.emotion;\n\n if (config[\"default\"][emotion] === undefined) return 1;\n\n const targetTypefaceColors = config[\"default\"][emotion][\"color\"][\"typography\"];\n const targetBackgroundColors = config[\"default\"][emotion][\"color\"][\"background\"];\n const targetTypographyFeatures = config[\"default\"][emotion][\"typefaces\"];\n\n // typography colour\n let meanTypefaceColorDistance = 1;\n if (targetTypefaceColors !== undefined && targetTypefaceColors.length > 0) {\n let typefaceColorsDistances = [];\n for (let t of textboxes) {\n let c = hexToRGB(t.color);\n let typefaceColorsDist = Number.MAX_VALUE;\n for (let targetColor of targetTypefaceColors) {\n targetColor = hexToRGB(targetColor);\n let distance = colorDistance(c, targetColor)\n if (distance < typefaceColorsDist) {\n typefaceColorsDist = distance;\n }\n }\n typefaceColorsDistances.push(typefaceColorsDist);\n }\n\n meanTypefaceColorDistance = typefaceColorsDistances.length < 1 ? 1 : arrMean(typefaceColorsDistances);\n meanTypefaceColorDistance /= MAX_COLOR_DISTANCE;\n meanTypefaceColorDistance = constraint(1-meanTypefaceColorDistance, 0, 1);\n }\n\n // background colour\n let meanTypefaceBackgroundDistance = 1;\n if (targetBackgroundColors !== undefined && targetBackgroundColors.length !== 0) {\n let backgroundColorsDistances = [];\n meanTypefaceBackgroundDistance = 0;\n for (let c of background) {\n c = hexToRGB(c);\n let backgroundColorsDist = Number.MAX_VALUE;\n for (let targetColor of targetBackgroundColors) {\n targetColor = hexToRGB(targetColor);\n let distance = colorDistance(c, targetColor);\n if (distance < backgroundColorsDist) {\n backgroundColorsDist = distance;\n }\n }\n backgroundColorsDistances.push(backgroundColorsDist);\n }\n\n meanTypefaceBackgroundDistance = meanTypefaceBackgroundDistance.length < 1 ? 1 : arrMean(backgroundColorsDistances);\n meanTypefaceBackgroundDistance /= MAX_COLOR_DISTANCE;\n meanTypefaceBackgroundDistance = constraint(1-meanTypefaceBackgroundDistance, 0, 1);\n }\n\n // typeface\n let meanTypefaceError = 1;\n if (targetTypographyFeatures !== undefined && targetTypographyFeatures.length > 0) {\n let fontsTags = [];\n for (let t of textboxes) {\n let tbTagsValue = 0;\n const typefaceIndex = typefaceData.map(t => t.family).indexOf(t[\"typeface\"]);\n const tags = typefaceData[typefaceIndex][\"tags\"];\n for (let t of targetTypographyFeatures) {\n if (tags.includes(t)) {\n tbTagsValue += (1/targetTypographyFeatures.length)\n }\n }\n fontsTags.push(tbTagsValue);\n }\n\n meanTypefaceError = fontsTags.length < 1 ? 1 : arrMean(fontsTags);\n meanTypefaceError = constraint(meanTypefaceError, 0, 1);\n }\n\n return (meanTypefaceColorDistance + meanTypefaceBackgroundDistance + meanTypefaceError)/3;\n\n\n}\n\n\nexport {compute as default};","/**\n * Alignment metric\n *\n * Estimate if the horizontal alignment of text boxes follows a regular pattern\n * Based on Harrington et al. (2004).\n *\n * Include the check if the text Alignment is the same (part B).\n * User can modify the value of WEIGHTS when necessary.\n *\n * return a value between 1 (good) and 0 (bad)\n *\n * Author: Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * Contact: srebelo[at]dei.uc.pt\n *\n * Version 1.0.0 (March 2020)\n * Version: 1.5.0 (November 2023)\n */\n\nimport {arrMean, arrUnique, sumProduct} from \"../utils.js\";\nimport {ALIGNMENT} from \"../metrics.config.js\";\n\nconst A = ALIGNMENT[\"A\"]\nconst WEIGHTS = ALIGNMENT[\"WEIGHTS\"];\n\nexport const compute = (sentenceWidth, textAlignment, weights = WEIGHTS) => {\n if (sentenceWidth.length < 2) {\n return 1;\n }\n\n let histogram = sentenceWidth;\n let results = [];\n\n for (let i = 0; i {\n if (amount === null) {\n color = hexToRGB(color);\n amount = percentTypographyColor(img, color, img.pixelDensity());\n }\n\n const res = 1-4*Math.pow((amount - optimal), 2);\n\n return res;\n}\n\nconst percentTypographyColor = (img, c, d=1) => {\n let amount = 0;\n let distances = [];\n let size = img.width * d * img.height * d ;\n img.loadPixels();\n for (let i = 0; i < 4 * size; i += 4) {\n let current = {\n r: img.pixels[i],\n g: img.pixels[i+1],\n b: img.pixels[i+2]\n }\n const distance = colorDistance(current, c);\n distances.push(distance);\n if (distance < MIN_DISTANCE) {\n amount++;\n }\n }\n return amount/size;\n}\n\nexport {compute as default};","import {arrUnique} from \"../utils.js\";\nimport {TYPEFACE_PARING} from \"../metrics.config.js\";\n\n/**\n * Typeface Pairing Metric\n *\n * Checks if the employed typefaces pair well together.\n * Since each typeface is related to a line of text,\n * this metric considers the typeface name and the classification.\n * Typefaces should be included in the configuration file with corresponding classification values.\n *\n * It had three modes:\n * TYPEFACE: values the use of the same typeface\n * CATEGORY: values the use of typefaces in the same category\n * BOTH: values both\n *\n *\n * Returns a value between 1 (good) and 0 (bad) to indicate compatibility.\n *\n * Author: Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * Contact: srebelo[at]dei.uc.pt\n *\n * Version 1.0.0 (March 2020)\n * Updated Version: 1.5.0 (November 2023)\n */\n\nconst AVAILABLE_MODES = TYPEFACE_PARING[\"MODES\"]\n\nexport const compute = (typefaces, availableTypefaces, mode = `BOTH`) => {\n if (!AVAILABLE_MODES.includes(mode)) {\n mode = `BOTH`;\n }\n\n let weights = [.5, .5]; // heights to BOTH mode\n if (mode === `TYPE_FAMILY`) {\n weights = [1, 0];\n } else if (mode === `CATEGORY`) {\n weights = [0, 1];\n }\n\n let categories = [];\n let usedTypefaces = arrUnique(typefaces);\n let categoriesFactor = 0, typefaceFactor = 0;\n\n if (mode !== `TYPE_FAMILY`) {\n const typefacesNames = availableTypefaces.map(a => a[\"family\"]);\n const typefacesClassification = availableTypefaces.map(a => a[\"category\"]);\n\n for (let typeface of usedTypefaces) {\n const index = typefacesNames.indexOf(typeface);\n if (index === -1) {\n categories.push(`undefined`);\n } else {\n categories.push(typefacesClassification[index]);\n }\n }\n\n categories = arrUnique(categories);\n categoriesFactor = 1/categories.length;\n }\n\n if (mode !== `CATEGORY`) {\n typefaceFactor = 1 / usedTypefaces.length;\n }\n\n const res = [typefaceFactor, categoriesFactor].reduce((s, v, i) => s + v * weights[i], 0);\n\n return res;\n}\n\nexport {compute as default};","/**\n * Visual Balance metric\n *\n * Estimate the visual balance (centred) of the composition.\n * Based on Harrington et al. (2004)\n *\n *\n * Author: Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * Contact: srebelo[at]dei.uc.pt\n *\n * Version 1.0.0 (March 2020)\n * Version: 1.5.0 (November 2023)\n */\n\nimport {arrSum} from \"../utils.js\";\nimport {VISUAL_BALANCE} from \"../metrics.config.js\";\n\n// visual center factor\n// By default, the visual center is taken to be offset a twentieth of the page height towards the top\nconst VISUAL_CENTER_FT = VISUAL_BALANCE[\"VISUAL_CENTER_FT\"];\n\nexport const compute = async (img = null, size, rows, widths, heights, visualWeights = null) => {\n const dx = size[\"width\"];\n const dy = size[\"height\"];\n\n const vw = visualWeights === null ? await visualWeight(img, rows, widths, heights) : visualWeights;\n const bo = balanceCenter(vw, widths, heights);\n const vo = visualCenter(dx, dy);\n\n // calculate central Balance\n let cb = Math.pow(((bo.x-vo.x)/dx), 2) + Math.pow(((bo.y-vo.y)/dy), 2);\n cb = 1-Math.pow(Math.abs(cb/2), 1/2);\n\n return cb;\n}\n\n\nconst balanceCenter = (vws, widths, heights) => {\n // get text box center\n const vc = [];\n for (let i in heights) {\n vc.push(visualCenter(widths[i], heights[i]));\n }\n\n // get page balance center\n let posX = 0, posY = 0;\n const m = arrSum(vws);\n\n for (let i in vws) {\n posX += vc[i].x*vws[i];\n posY += vc[i].y*vws[i];\n }\n\n posX /= m;\n posY /= m;\n\n return {\n x: posX,\n y: posY\n }\n}\n\n\n/**\n* Calculate the visual centre of a element\n* The visual center lies halfway between the left and right edges\n* the visual center is taken to be offset a twentieth of the page height towards the top\n*/\nconst visualCenter = (width, height) => {\n return {\n x: width/2,\n y: height/2 - (height/VISUAL_CENTER_FT)\n }\n}\n\n/**\n * calculate the visual weight of a object\n * visual weight = area x optical density\n */\nconst visualWeight = async (img, rows, widths, heights) => {\n let areas = [];\n let opticalDensity = [];\n\n for (let i in widths) {\n\n // compute areas\n areas.push(widths[i] * heights[i]);\n\n // compute visual weight\n const rendering = await img.get(\n 0,\n img.height/2 + rows[\"center\"][i],\n img.width,\n rows[\"l\"][i]\n );\n\n await rendering.loadPixels();\n\n let r = 0, g = 0, b = 0;\n const realPixelsSize = rendering.pixels.length/4;\n\n for (let i=0; i < rendering.pixels.length; i+=4) {\n r += rendering.pixels[i];\n g += rendering.pixels[i+1];\n b += rendering.pixels[i+2];\n }\n\n r = Math.round(r/realPixelsSize);\n g = Math.round(g/realPixelsSize);\n b = Math.round(b/realPixelsSize);\n\n const avgLuma = (0.2126*r + 0.7152*g + 0.0722*b);\n const t = avgLuma / 255;\n opticalDensity.push(-Math.log10(t));\n }\n\n return areas.map((a, i) => a * opticalDensity[i]);\n}\n\n\nexport {compute as default};\n","/**\n * Evaluation metrics for evo-poster\n * Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * srebelo[at]dei.uc.pt\n *\n * v0.0.1 July 2023\n */\n\nimport * as Legibility from \"./metrics/Legibility.mjs\";\nimport * as GridAppropriateSize from \"./metrics/GridAppropriateSize.mjs\";\nimport * as SemanticsLayout from \"./metrics/SemanticsLayout.mjs\";\nimport * as SemanticsEmphasis from \"./metrics/SemanticsEmphasis.mjs\";\nimport * as SemanticsVisuals from \"./metrics/SemanticVisuals.mjs\";\nimport * as Alignment from \"./metrics/Alignment.mjs\";\nimport * as Regularity from \"./metrics/Regularity.mjs\";\nimport * as WhiteSpaceFraction from \"./metrics/WhiteSpaceFraction.mjs\";\nimport * as TypefaceParing from \"./metrics/TypefaceParing.mjs\";\nimport * as VisualBalance from \"./metrics/VisualBalance.mjs\";\n\n\nexport const legibility = Legibility.compute;\nexport const gridAppropriateSize = GridAppropriateSize.compute;\nexport const semanticsLayout = SemanticsLayout.compute;\nexport const semanticsEmphasis = SemanticsEmphasis.compute;\nexport const semanticsVisuals = SemanticsVisuals.compute;\nexport const alignment = Alignment.compute;\nexport const regularity = Regularity.compute;\nexport const whiteSpaceFraction = WhiteSpaceFraction.compute;\nexport const typefaceParing = TypefaceParing.compute;\nexport const visualBalance = VisualBalance.compute;\n\nexport default () => {\n console.log (`@evo-poster · evaluator v2.00`)\n}\n","/**\n * Grid Size Appropriateness\n *\n * Measure the appropriate of the used grid to the size of container\n * it is related to if the width and height of the grid is\n * in accordance with poster size\n *\n * Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * srebelo[at]dei.uc.pt\n *\n * v1.0.0 November 2023\n */\n\nimport * as CONFIG from \"../metrics.config.js\";\n\nconst DEBUG = CONFIG[\"default\"][\"DEBUG\"];\n\nexport const compute = (containerWidth, containerHeight, rows = [], columns = [], margins = {left:0, top:0, right:0, bottom:0}) => {\n let invalid = false;\n // debug\n let msg = \"\";\n\n // height calculation\n let height = Math.abs(margins.top)+Math.abs(margins.bottom);\n for (let r of rows) {\n height = height + parseFloat(r);\n }\n // width calculation\n let width = Math.abs(margins.left)+Math.abs(margins.right);\n for (let r of columns) {\n width = width + parseFloat(r);\n }\n\n width = Math.round(width);\n height = Math.round(height);\n\n if (height > containerHeight) {\n invalid = true;\n msg += `Grid height is bigger than container (grid:${height}, container:${containerHeight}). `;\n } else if (height < containerHeight) {\n msg += `Grid and container height are not the same (grid:${height}, container:${containerHeight}). `;\n }\n\n if (width > containerWidth) {\n invalid = true;\n msg += `Grid width is bigger than container (grid:${width}, container:${containerWidth}). `;\n } else if (width < containerWidth) {\n msg += `Grid and container width are not the same (grid:${width}, container:${containerWidth}). `;\n }\n\n if (msg !== \"\" && DEBUG) {\n console.warn(msg);\n }\n\n return invalid ? 1 : 0;\n}\n\nexport { compute as default };","/**\n * Alignment metric\n *\n * Estimate if the text boxes follows a regular vertical pattern\n * Based on Harrington et al. (2004).\n *\n *\n * return a value between 1 (good) and 0 (bad)\n *\n * Author: Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * Contact: srebelo[at]dei.uc.pt\n *\n * Version 1.0.0 (March 2020)\n * Version: 1.5.0 (November 2023)\n */\n\nimport {arrMean} from \"../utils.js\";\nimport {REGULARITY} from \"../metrics.config.js\";\n\nconst A = REGULARITY[\"A\"];\n\nexport const compute = (heights) => {\n if (heights.length < 2) {\n return 1;\n }\n\n const histogram = heights;\n\n let results = [];\n\n for (let i = 0; i {\n return minB + (maxB - minB) * ((value - minA) / (maxA - minA));\n}\n\nexport const constraint = (value, min, max) => {\n return Math.min(max, Math.max(min, value));\n}\n\nexport const arrMean = (arr) => {\n const sum = arr.reduce((a, b) => a + b, 0);\n return (sum / arr.length) || 0;\n}\n\nexport const arrSum = (arr) => {\n return arr.reduce((partialSum, a) => partialSum + a, 0);\n}\n\nexport const arrMax = (arr) => {\n return Math.max(...arr);\n}\n\nexport const arrMin = (arr) => {\n return Math.min(...arr);\n}\n\nexport const arrUnique = (arr) => {\n return arr.filter((value, index, array) => array.indexOf(value) === index);\n}\n\nexport const sumProduct = (arr, weights) => {\n return arr.reduce((s, v, i) => s + v * weights[i], 0);\n}\n\nexport const hexToRGB = (hex) => {\n if (hex[\"levels\"]) {\n return {\n r: parseInt(hex[\"levels\"][0]),\n g: parseInt(hex[\"levels\"][1]),\n b: parseInt(hex[\"levels\"][2])\n }\n }\n\n let result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return result ? {\n r: parseInt(result[1], 16),\n g: parseInt(result[2], 16),\n b: parseInt(result[3], 16)\n } : null;\n}\n\nexport const colorDistance = (a, b) => {\n return Math.sqrt(Math.pow(a.r - b.r, 2) + Math.pow(a.g - b.g, 2) + Math.pow(a.b - b.b, 2));\n}","export const ALIGNMENT = {\n \"A\": 10, // limit to the non-linear function\n \"WEIGHTS\": [.8, .2] // alignment parts weights\n}\n\nexport const LEGIBILITY = {\n \"MAX_CONSTRAINT\": 1,\n \"WHITE_SPACE_FACTOR\": 3,\n \"MODES\": [`OVERSET`, `JUSTIFY`,`ATTEMPT_JUSTIFY`],\n \"DEFAULT_MAX_LIMIT_SCALE\": 1\n}\n\nexport const REGULARITY = {\n \"A\": 10, // limit to the non-linear function\n}\n\nexport const SEMANTICS_EMPHASIS = {\n \"MIN_RANGE\": 50,\n \"THRESHOLD_VALID\": 0.2,\n \"MODES\": [`DIF`, `MIN`]\n}\n\nexport const SEMANTICS_VISUALS = {\n \"MAX_COLOR_DISTANCE\": 441.67\n}\n\nexport const SEMANTICS_LAYOUT = {\n \"MODES\": [`RELATIVE`, `FIXED`]\n}\n\nexport const TYPEFACE_PARING = {\n \"MODES\": [`BOTH`, `TYPE_FAMILY`, `CATEGORY`]\n}\n\nexport const VISUAL_BALANCE = {\n \"VISUAL_CENTER_FT\": 20,\n \"MODES\": [\n `CENTER`,\n `LEFT-CENTER`, `RIGHT-CENTER`,\n `LEFT-TOP`, `RIGHT-TOP`,\n `LEFT-BOTTOM`, `RIGHT-BOTTOM`]\n}\n\nexport const WHITE_SPACE_FRACTION = {\n \"OPTIMAL\": .5,\n \"MIN_DISTANCE\": 10\n}\n\n\nexport default {\n \"DEBUG\": false,\n}","/**\n * Legibility\n *\n * Measure the legibility of the text in the poster\n * it is related to the legibility of the sentence\n * and not the typeface shapes\n *\n * Expected return a value between 0 (good) and (1) bad\n *\n * Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * srebelo[at]dei.uc.pt\n *\n * v1.0.0 August 2018 (as part of evoPoster)\n * v2.0.0 November 2020 (as part of evoPoster)\n * v2.5.0 November 2021 (as part of evoPoster)\n * v3.0.0 November 2023\n */\n\nimport {arrMean, map} from \"../utils.js\";\nimport {LEGIBILITY} from \"../metrics.config.js\";\n\nconst MAX_CONSTRAINT = LEGIBILITY[\"MAX_CONSTRAINT\"];\nconst WHITE_SPACE_FACTOR = LEGIBILITY[\"WHITE_SPACE_FACTOR\"];\nconst DEFAULT_MAX_LIMIT_SCALE = LEGIBILITY[\"DEFAULT_MAX_LIMIT_SCALE\"];\nconst AVAILABLE_MODES = LEGIBILITY [\"MODES\"]\n\nexport const compute = (sentencesLength = [], minSize, mode= 'OVERSET', maxLimitScale= DEFAULT_MAX_LIMIT_SCALE) => {\n if (!AVAILABLE_MODES.includes(mode)) {\n mode = `OVERSET`;\n }\n let results = [];\n let max = minSize * maxLimitScale;\n for (let sentence of sentencesLength) {\n let dif = minSize-sentence;\n let value = MAX_CONSTRAINT;\n switch (mode) {\n case `JUSTIFY`:\n value = justify(dif, max);\n break;\n case `ATTEMPT_JUSTIFY`:\n value = attemptJustify(dif, max);\n break;\n default:\n value = overset(dif, max);\n break;\n }\n results.push(value);\n }\n\n // calculate mean\n const mean = arrMean([...results]);\n\n return mean;\n}\n\nconst overset = (value, max) => {\n // only prejudice when text overfits the poster\n // if dif bigger than 0\n value = value >= 0 ? 0 : value;\n // if dif lower than limit\n value = value <= -max ? -max : value;\n // transform in scale of 1 (bad) to 0 (good)\n return map (value, -max, 0, MAX_CONSTRAINT, 0);\n}\n\nconst justify = (value, max) => {\n // prejudice both overset and small lettering\n value = Math.abs(value)\n // if the dif is bigger than max\n value = value > max ? max : value;\n // transform in scale of 1 (bad) to 0 (good)\n return map (value, max, 0, MAX_CONSTRAINT, 0);\n}\n\n// attempt to justify the text\n// prejudice more when the text overset box than when it is smaller\nconst attemptJustify = (value, max) => {\n // if dif bigger than 0 (it is not overset), soften the value\n value = value >= 0 ? value/WHITE_SPACE_FACTOR : value;\n return justify(value, max);\n}\n\nexport { compute as default };","/**\n * Layout Semantics\n *\n * Measure the appropriate of the layout\n * based on the importance of the content\n * it considers that most important parts of content\n * are those have more emotions related\n *\n * return the mean of difference between all textboxes,\n * a value between 1 (good) and 0 (bad)\n *\n * two modes: RELATIVE and FIXED\n * RELATIVE mode is related to the composition height\n * FIXED height is related to the container's height\n *\n * Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * srebelo[at]dei.uc.pt\n *\n * v1.0.0 April 2020\n * v2.0.0 November 2023\n */\n\nimport {arrMean, arrSum} from \"../utils.js\";\nimport {SEMANTICS_LAYOUT} from \"../metrics.config.js\";\n\nconst AVAILABLE_MODES = SEMANTICS_LAYOUT[\"MODES\"];\n\nexport const compute = (textboxesHeights = [], dist = null, mode=`RELATIVE`, size = {height: 100, margin:[0,0,0,0]}) => {\n // mode validation\n if (!AVAILABLE_MODES.includes(mode)) mode = `RELATIVE`;\n\n // define max height\n let height = 0;\n if (mode === `RELATIVE`) {\n // using the textbox height\n height = arrSum(textboxesHeights);\n } else if (mode === `FIXED`) {\n // using container height\n height = size.height - ((size.height*size.margin[1]) + (size.height*size.margin[3]));\n }\n const percents = textboxesHeights.map((p) => p/height);\n\n // calculate distances\n let distances = [];\n for (let i in dist) {\n const dif = Math.abs(dist[i][3] - percents[i]);\n distances.push(dif);\n }\n\n // return average\n return (1-arrMean(distances));\n}\n\nexport { compute as default };","/**\n * Semantics Emphasis on Visuals\n *\n * Measure the appropriate of the visual features\n * based on the importance of the content\n * it considers that most important parts of content\n * are those have more emotions related\n *\n * return the mean of difference between all textboxes,\n * a value between 1 (good) and 0 (bad)\n *\n * two modes: MIN and DIF\n * MIN takes into account that the most important parts\n * are typeset in the min value possible in the container\n * DIF: take into account only the difference between the parts\n * selects a style and compare based on the distribution of emotions.\n *\n *\n *\n * Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * srebelo[at]dei.uc.pt\n *\n * v1.0.0 November 2023\n */\nimport {arrMax, arrMean, arrMin, arrSum, constraint, map} from \"../utils.js\";\nimport {SEMANTICS_EMPHASIS} from \"../metrics.config.js\";\n\nlet MIN_RANGE = SEMANTICS_EMPHASIS[\"MIN_RANGE\"];\nlet THRESHOLD_VALID = SEMANTICS_EMPHASIS[\"THRESHOLD_VALID\"];\nlet AVAILABLE_MODES = SEMANTICS_EMPHASIS[\"MODES\"];\n\n// by tradition, use more that a method to emphasize\n// the text is considered \"typecrime\".\n// this method enables turn on/off this feature\n// by using the param allowMultiple\nexport const compute = (textboxes, dist, noCurrentTypefaces = 1, allowMultiple = true, weights = [0.4, 0.3, 0.3]) => {\n // if textboxes size is 1 ---> 1\n const perDist = dist.map((e) => e[3]);\n\n const fontWeight = checkDifferenceVariableFeature(textboxes.map((b) => b[\"weight\"]), perDist);\n const fontStretch = checkDifferenceVariableFeature(textboxes.map((b) => b[\"font-stretch\"]), perDist);\n let typefaceDesign = noCurrentTypefaces > 1 ? (checkDifferenceUniqueFeatures(textboxes.map((b) => b[\"font-stretch\"]), perDist)) : 0;\n\n // way of combine and only checks one\n let res = [fontWeight, fontStretch, typefaceDesign];\n let weightedRes = res.map((x,i) => x*weights[i]);\n\n let value;\n\n if (!allowMultiple) {\n // if not allowed multiple emphasis\n // we check what fields are active\n // and penalty when there is active fields\n let active = res.map((r) => r > THRESHOLD_VALID);\n let c = 0;\n for (let a of active) {\n if (a) {\n c++;\n }\n }\n value = arrMax(res)/c;\n } else {\n value = arrSum(weightedRes);\n }\n\n return value;\n}\n\n// check the levels\nconst checkDifferenceUniqueFeatures = (currentFeatures, dist) => {\n // available semantic level\n const uniqueValues = dist.filter((value, index, array) => array.indexOf(value) === index);\n const target = [];\n\n // define the target typeface for each semantic level\n for (let uq of uniqueValues) {\n for (let i=0; i target.indexOf(item) !== index);\n\n let value = 1;\n // if there is duplicate in levels\n // (if yes, difference is max)\n if (!duplicates.length >= 1) {\n // count the amount of tb not in the same typeface\n let c = 0;\n // for each typeface\n for (let i in dist) {\n let level = dist[i];\n let currentValue = currentFeatures[i];\n // get unique index of current semantic level\n let index = uniqueValues.indexOf(level);\n // get target value\n let targetValue = target[index];\n if (currentValue !== targetValue) {\n // if not the same as target\n c++;\n }\n }\n // map value to a value between 0 (no difference) and 1 (max difference)\n value = map(c, 0, currentFeatures.length, 0, 1);\n }\n\n return value;\n}\n\nconst checkDifferenceVariableFeature = (currentFeatures, dist, mode = `DIF`) => {\n if (!AVAILABLE_MODES.includes(mode)) {\n mode = `DIF`;\n }\n // max feature range\n const maxFeature = arrMax(currentFeatures);\n const minFeature = arrMin(currentFeatures);\n let range = Math.abs(maxFeature - minFeature);\n if (range < MIN_RANGE) {\n return 1;\n }\n\n // semantic data range\n const maxSemantic = arrMax(dist);\n const minSemantic = arrMin(dist);\n\n // consider the current variable minimum\n let def = minFeature;\n if (mode === `DIF`) {\n // selects a style used in the first min semantic textbox\n for (let i in dist) {\n if (dist[i] === minSemantic) {\n // consider the difference\n def = currentFeatures[i];\n break;\n }\n }\n }\n\n // create target feature values\n const target = dist.map((e) => {\n // The most neutral sentence are the most regular\n let v = map(e, minSemantic, maxSemantic, 0, range);\n v = constraint(v, 0, range);\n return v;\n });\n\n // distance to target\n const current = [];\n for (let i in currentFeatures) {\n let w = currentFeatures[i];\n let currentDistance = Math.abs(w - def);\n let dif = Math.abs(currentDistance - target[i]);\n current.push(dif);\n }\n\n let mean = map(arrMean(current), 0, range, 1, 0);\n return constraint(mean, 0, 1);\n}\n\n\n\n\nexport { compute as default };","export default {\n anger : {\n color: {\n typography: [`#ff0000`, `#00ff00`],\n background: [`#ff0000`]\n },\n typefaces: [`sans-serif`, `neo-grotesque`]\n },\n joy: {\n color: {\n typography: [],\n background: [`#ffff00`, `#00ff00`]\n },\n typefaces: [`sans-serif`, `serif`]\n },\n trust: {\n color: {\n typography: [],\n background: [`#0000ff`, `#00ff00`]\n },\n typefaces: [`neo-grotesque`]\n },\n sadness: {\n color: {\n typography: [],\n background: [`#0071b6`]\n },\n typefaces: []\n },\n disgust: {\n color: {\n typography: [`#800080`],\n background: []\n },\n typefaces: []\n }\n}","/**\n * Semantic Visuals Measure\n *\n * This function assesses the appropriateness of visual features (type design and colour)\n * based on the emotions collected. It takes into account the configuration file \"visual-semantics.config.js\".\n *\n * The function returns the mean difference between all textboxes,\n * yielding a value between 1 (good) and 0 (bad).\n * If there is no information pertaining to a particular emotion, the value defaults to 1.\n *\n * As of now, the function exclusively considers the predominant emotions from ML analysis.\n *\n * Author: Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * Contact: srebelo[at]dei.uc.pt\n *\n * Version: 1.0.0 (November 2023)\n */\n\nimport {SEMANTICS_VISUALS} from \"../metrics.config.js\";\n\nconst MAX_COLOR_DISTANCE = SEMANTICS_VISUALS[\"MAX_COLOR_DISTANCE\"];\n\nimport * as configurationFile from \"../../semantics-visual.config.js\";\nimport {arrMean, colorDistance, hexToRGB, constraint} from \"../utils.js\";\n\n\nexport const compute = async (data, textboxes, background, typefaceData, config = configurationFile) => {\n\n let emotion = data.predominant.emotion;\n\n if (config[\"default\"][emotion] === undefined) return 1;\n\n const targetTypefaceColors = config[\"default\"][emotion][\"color\"][\"typography\"];\n const targetBackgroundColors = config[\"default\"][emotion][\"color\"][\"background\"];\n const targetTypographyFeatures = config[\"default\"][emotion][\"typefaces\"];\n\n // typography colour\n let meanTypefaceColorDistance = 1;\n if (targetTypefaceColors !== undefined && targetTypefaceColors.length > 0) {\n let typefaceColorsDistances = [];\n for (let t of textboxes) {\n let c = hexToRGB(t.color);\n let typefaceColorsDist = Number.MAX_VALUE;\n for (let targetColor of targetTypefaceColors) {\n targetColor = hexToRGB(targetColor);\n let distance = colorDistance(c, targetColor)\n if (distance < typefaceColorsDist) {\n typefaceColorsDist = distance;\n }\n }\n typefaceColorsDistances.push(typefaceColorsDist);\n }\n\n meanTypefaceColorDistance = typefaceColorsDistances.length < 1 ? 1 : arrMean(typefaceColorsDistances);\n meanTypefaceColorDistance /= MAX_COLOR_DISTANCE;\n meanTypefaceColorDistance = constraint(1-meanTypefaceColorDistance, 0, 1);\n }\n\n // background colour\n let meanTypefaceBackgroundDistance = 1;\n if (targetBackgroundColors !== undefined && targetBackgroundColors.length !== 0) {\n let backgroundColorsDistances = [];\n meanTypefaceBackgroundDistance = 0;\n for (let c of background) {\n c = hexToRGB(c);\n let backgroundColorsDist = Number.MAX_VALUE;\n for (let targetColor of targetBackgroundColors) {\n targetColor = hexToRGB(targetColor);\n let distance = colorDistance(c, targetColor);\n if (distance < backgroundColorsDist) {\n backgroundColorsDist = distance;\n }\n }\n backgroundColorsDistances.push(backgroundColorsDist);\n }\n\n meanTypefaceBackgroundDistance = meanTypefaceBackgroundDistance.length < 1 ? 1 : arrMean(backgroundColorsDistances);\n meanTypefaceBackgroundDistance /= MAX_COLOR_DISTANCE;\n meanTypefaceBackgroundDistance = constraint(1-meanTypefaceBackgroundDistance, 0, 1);\n }\n\n // typeface\n let meanTypefaceError = 1;\n if (targetTypographyFeatures !== undefined && targetTypographyFeatures.length > 0) {\n let fontsTags = [];\n for (let t of textboxes) {\n let tbTagsValue = 0;\n const typefaceIndex = typefaceData.map(t => t.family).indexOf(t[\"typeface\"]);\n const tags = typefaceData[typefaceIndex][\"tags\"];\n for (let t of targetTypographyFeatures) {\n if (tags.includes(t)) {\n tbTagsValue += (1/targetTypographyFeatures.length)\n }\n }\n fontsTags.push(tbTagsValue);\n }\n\n meanTypefaceError = fontsTags.length < 1 ? 1 : arrMean(fontsTags);\n meanTypefaceError = constraint(meanTypefaceError, 0, 1);\n }\n\n return (meanTypefaceColorDistance + meanTypefaceBackgroundDistance + meanTypefaceError)/3;\n\n\n}\n\n\nexport {compute as default};","/**\n * Alignment metric\n *\n * Estimate if the horizontal alignment of text boxes follows a regular pattern\n * Based on Harrington et al. (2004).\n *\n * Include the check if the text Alignment is the same (part B).\n * User can modify the value of WEIGHTS when necessary.\n *\n * return a value between 1 (good) and 0 (bad)\n *\n * Author: Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * Contact: srebelo[at]dei.uc.pt\n *\n * Version 1.0.0 (March 2020)\n * Version: 1.5.0 (November 2023)\n */\n\nimport {arrMean, arrUnique, sumProduct} from \"../utils.js\";\nimport {ALIGNMENT} from \"../metrics.config.js\";\n\nconst A = ALIGNMENT[\"A\"]\nconst WEIGHTS = ALIGNMENT[\"WEIGHTS\"];\n\nexport const compute = (sentenceWidth, textAlignment, weights = WEIGHTS) => {\n if (sentenceWidth.length < 2) {\n return 1;\n }\n\n let histogram = sentenceWidth;\n let results = [];\n\n for (let i = 0; i {\n if (amount === null) {\n color = hexToRGB(color);\n amount = percentTypographyColor(img, color, img.pixelDensity());\n }\n\n const res = 1-4*Math.pow((amount - optimal), 2);\n\n return res;\n}\n\nconst percentTypographyColor = (img, c, d=1) => {\n let amount = 0;\n let distances = [];\n let size = img.width * d * img.height * d ;\n img.loadPixels();\n for (let i = 0; i < 4 * size; i += 4) {\n let current = {\n r: img.pixels[i],\n g: img.pixels[i+1],\n b: img.pixels[i+2]\n }\n const distance = colorDistance(current, c);\n distances.push(distance);\n if (distance < MIN_DISTANCE) {\n amount++;\n }\n }\n return amount/size;\n}\n\nexport {compute as default};","import {arrUnique} from \"../utils.js\";\nimport {TYPEFACE_PARING} from \"../metrics.config.js\";\n\n/**\n * Typeface Pairing Metric\n *\n * Checks if the employed typefaces pair well together.\n * Since each typeface is related to a line of text,\n * this metric considers the typeface name and the classification.\n * Typefaces should be included in the configuration file with corresponding classification values.\n *\n * It had three modes:\n * TYPEFACE: values the use of the same typeface\n * CATEGORY: values the use of typefaces in the same category\n * BOTH: values both\n *\n *\n * Returns a value between 1 (good) and 0 (bad) to indicate compatibility.\n *\n * Author: Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * Contact: srebelo[at]dei.uc.pt\n *\n * Version 1.0.0 (March 2020)\n * Updated Version: 1.5.0 (November 2023)\n */\n\nconst AVAILABLE_MODES = TYPEFACE_PARING[\"MODES\"]\n\nexport const compute = (typefaces, availableTypefaces, mode = `BOTH`) => {\n if (!AVAILABLE_MODES.includes(mode)) {\n mode = `BOTH`;\n }\n\n let weights = [.5, .5]; // heights to BOTH mode\n if (mode === `TYPE_FAMILY`) {\n weights = [1, 0];\n } else if (mode === `CATEGORY`) {\n weights = [0, 1];\n }\n\n let categories = [];\n let usedTypefaces = arrUnique(typefaces);\n let categoriesFactor = 0, typefaceFactor = 0;\n\n if (mode !== `TYPE_FAMILY`) {\n const typefacesNames = availableTypefaces.map(a => a[\"family\"]);\n const typefacesClassification = availableTypefaces.map(a => a[\"category\"]);\n\n for (let typeface of usedTypefaces) {\n const index = typefacesNames.indexOf(typeface);\n if (index === -1) {\n categories.push(`undefined`);\n } else {\n categories.push(typefacesClassification[index]);\n }\n }\n\n categories = arrUnique(categories);\n categoriesFactor = 1/categories.length;\n }\n\n if (mode !== `CATEGORY`) {\n typefaceFactor = 1 / usedTypefaces.length;\n }\n\n const res = [typefaceFactor, categoriesFactor].reduce((s, v, i) => s + v * weights[i], 0);\n\n return res;\n}\n\nexport {compute as default};","/**\n * Visual Balance metric\n *\n * Estimate the visual balance (centred) of the composition.\n * Based on Harrington et al. (2004)\n *\n *\n * Author: Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * Contact: srebelo[at]dei.uc.pt\n *\n * Version 1.0.0 (March 2020)\n * Version: 1.5.0 (November 2023)\n */\n\nimport {arrSum, constraint} from \"../utils.js\";\nimport {VISUAL_BALANCE} from \"../metrics.config.js\";\n\n// visual center factor\n// By default, the visual center is taken to be offset a twentieth of the page height towards the top\nconst VISUAL_CENTER_FT = VISUAL_BALANCE[\"VISUAL_CENTER_FT\"];\nconst AVAILABLE_MODES = VISUAL_BALANCE[\"MODES\"];\n\nexport const compute = async (img = null, size, rows, widths, heights, mode= `CENTER`, visualWeights = null) => {\n const dx = size[\"width\"];\n const dy = size[\"height\"];\n if (!AVAILABLE_MODES.includes(mode)) mode = `CENTER`;\n\n const vw = visualWeights === null ? await visualWeight(img, rows, widths, heights) : visualWeights;\n const bo = balanceCenter(mode, vw, widths, heights);\n let vo = visualCenter(mode, dx, dy, size[\"margin\"]);\n\n // calculate central Balance\n let cb = Math.pow(((bo.x-vo.x)/dx), 2) + Math.pow(((bo.y-vo.y)/dy), 2);\n cb = 1-Math.pow(Math.abs(cb/2), 1/2);\n\n return constraint(cb, 0, 1);\n}\n\n\nconst balanceCenter = (mode, vws, widths, heights) => {\n\n // get text box center\n const vc = [];\n for (let i in heights) {\n vc.push(visualCenter(mode, widths[i], heights[i]));\n }\n\n // get page balance center\n let posX = 0, posY = 0;\n const m = arrSum(vws);\n\n for (let i in vws) {\n posX += vc[i].x*vws[i];\n posY += vc[i].y*vws[i];\n }\n\n posX /= m;\n posY /= m;\n\n return {\n x: posX,\n y: posY\n }\n}\n\n\n/**\n* Calculate the visual centre of a element\n* The visual center lies halfway between the left and right edges\n* the visual center is taken to be offset a twentieth of the page height towards the top\n*/\nconst visualCenter = (mode, width, height, margins = [0,0,0,0]) => {\n mode = String(mode)\n const posX = mode.includes(`LEFT`) ? 1 : mode.includes(`RIGHT`) ? 2 : 0;\n const posY = mode.includes(`TOP`) ? 1 : mode.includes(`BOTTOM`) ? 2 : 0;\n\n let x = posX === 0 ? width/2 :\n posX === 1 ? (width * margins[0]) : width * (1 - margins[2]) ;\n let y = posY === 0 ? height/2 - (height/VISUAL_CENTER_FT) :\n posY === 1 ? (height * margins[1]) : height - (height * margins[3])\n\n\n\n return {\n x: x,\n y: y\n }\n}\n\n/**\n * calculate the visual weight of a object\n * visual weight = area x optical density\n */\nconst visualWeight = async (img, rows, widths, heights) => {\n let areas = [];\n let opticalDensity = [];\n\n for (let i in widths) {\n\n // compute areas\n areas.push(widths[i] * heights[i]);\n\n // compute visual weight\n const rendering = await img.get(\n 0,\n img.height/2 + rows[\"center\"][i],\n img.width,\n rows[\"l\"][i]\n );\n\n await rendering.loadPixels();\n\n let r = 0, g = 0, b = 0;\n const realPixelsSize = rendering.pixels.length/4;\n\n for (let i=0; i < rendering.pixels.length; i+=4) {\n r += rendering.pixels[i];\n g += rendering.pixels[i+1];\n b += rendering.pixels[i+2];\n }\n\n r = Math.round(r/realPixelsSize);\n g = Math.round(g/realPixelsSize);\n b = Math.round(b/realPixelsSize);\n\n const avgLuma = (0.2126*r + 0.7152*g + 0.0722*b);\n const t = avgLuma / 255;\n opticalDensity.push(-Math.log10(t));\n }\n\n return areas.map((a, i) => a * opticalDensity[i]);\n}\n\n\nexport {compute as default};\n","/**\n * Evaluation metrics for evo-poster\n * Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * srebelo[at]dei.uc.pt\n *\n * v0.0.1 July 2023\n */\n\nimport * as Legibility from \"./metrics/Legibility.mjs\";\nimport * as GridAppropriateSize from \"./metrics/GridAppropriateSize.mjs\";\nimport * as SemanticsLayout from \"./metrics/SemanticsLayout.mjs\";\nimport * as SemanticsEmphasis from \"./metrics/SemanticsEmphasis.mjs\";\nimport * as SemanticsVisuals from \"./metrics/SemanticVisuals.mjs\";\nimport * as Alignment from \"./metrics/Alignment.mjs\";\nimport * as Regularity from \"./metrics/Regularity.mjs\";\nimport * as WhiteSpaceFraction from \"./metrics/WhiteSpaceFraction.mjs\";\nimport * as TypefaceParing from \"./metrics/TypefaceParing.mjs\";\nimport * as VisualBalance from \"./metrics/VisualBalance.mjs\";\n\n\nexport const legibility = Legibility.compute;\nexport const gridAppropriateSize = GridAppropriateSize.compute;\nexport const semanticsLayout = SemanticsLayout.compute;\nexport const semanticsEmphasis = SemanticsEmphasis.compute;\nexport const semanticsVisuals = SemanticsVisuals.compute;\nexport const alignment = Alignment.compute;\nexport const regularity = Regularity.compute;\nexport const whiteSpaceFraction = WhiteSpaceFraction.compute;\nexport const typefaceParing = TypefaceParing.compute;\nexport const visualBalance = VisualBalance.compute;\n\nexport default () => {\n console.log (`@evo-poster · evaluator v2.00`)\n}\n","/**\n * Grid Size Appropriateness\n *\n * Measure the appropriate of the used grid to the size of container\n * it is related to if the width and height of the grid is\n * in accordance with poster size\n *\n * Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * srebelo[at]dei.uc.pt\n *\n * v1.0.0 November 2023\n */\n\nimport * as CONFIG from \"../metrics.config.js\";\n\nconst DEBUG = CONFIG[\"default\"][\"DEBUG\"];\n\nexport const compute = (containerWidth, containerHeight, rows = [], columns = [], margins = {left:0, top:0, right:0, bottom:0}) => {\n let invalid = false;\n // debug\n let msg = \"\";\n\n // height calculation\n let height = Math.abs(margins.top)+Math.abs(margins.bottom);\n for (let r of rows) {\n height = height + parseFloat(r);\n }\n // width calculation\n let width = Math.abs(margins.left)+Math.abs(margins.right);\n for (let r of columns) {\n width = width + parseFloat(r);\n }\n\n width = Math.round(width);\n height = Math.round(height);\n\n if (height > containerHeight) {\n invalid = true;\n msg += `Grid height is bigger than container (grid:${height}, container:${containerHeight}). `;\n } else if (height < containerHeight) {\n msg += `Grid and container height are not the same (grid:${height}, container:${containerHeight}). `;\n }\n\n if (width > containerWidth) {\n invalid = true;\n msg += `Grid width is bigger than container (grid:${width}, container:${containerWidth}). `;\n } else if (width < containerWidth) {\n msg += `Grid and container width are not the same (grid:${width}, container:${containerWidth}). `;\n }\n\n if (msg !== \"\" && DEBUG) {\n console.warn(msg);\n }\n\n return invalid ? 1 : 0;\n}\n\nexport { compute as default };","/**\n * Alignment metric\n *\n * Estimate if the text boxes follows a regular vertical pattern\n * Based on Harrington et al. (2004).\n *\n *\n * return a value between 1 (good) and 0 (bad)\n *\n * Author: Sérgio M. Rebelo\n * CDV lab. (CMS, CISUC, Portugal)\n * Contact: srebelo[at]dei.uc.pt\n *\n * Version 1.0.0 (March 2020)\n * Version: 1.5.0 (November 2023)\n */\n\nimport {arrMean} from \"../utils.js\";\nimport {REGULARITY} from \"../metrics.config.js\";\n\nconst A = REGULARITY[\"A\"];\n\nexport const compute = (heights) => {\n if (heights.length < 2) {\n return 1;\n }\n\n const histogram = heights;\n\n let results = [];\n\n for (let i = 0; i { +export const compute = async (img = null, size, rows, widths, heights, mode= `CENTER`, visualWeights = null) => { const dx = size["width"]; const dy = size["height"]; + if (!AVAILABLE_MODES.includes(mode)) mode = `CENTER`; const vw = visualWeights === null ? await visualWeight(img, rows, widths, heights) : visualWeights; - const bo = balanceCenter(vw, widths, heights); - const vo = visualCenter(dx, dy); + const bo = balanceCenter(mode, vw, widths, heights); + let vo = visualCenter(mode, dx, dy, size["margin"]); // calculate central Balance let cb = Math.pow(((bo.x-vo.x)/dx), 2) + Math.pow(((bo.y-vo.y)/dy), 2); cb = 1-Math.pow(Math.abs(cb/2), 1/2); - return cb; + return constraint(cb, 0, 1); } -const balanceCenter = (vws, widths, heights) => { +const balanceCenter = (mode, vws, widths, heights) => { + // get text box center const vc = []; for (let i in heights) { - vc.push(visualCenter(widths[i], heights[i])); + vc.push(visualCenter(mode, widths[i], heights[i])); } // get page balance center @@ -67,10 +70,21 @@ const balanceCenter = (vws, widths, heights) => { * The visual center lies halfway between the left and right edges * the visual center is taken to be offset a twentieth of the page height towards the top */ -const visualCenter = (width, height) => { +const visualCenter = (mode, width, height, margins = [0,0,0,0]) => { + mode = String(mode) + const posX = mode.includes(`LEFT`) ? 1 : mode.includes(`RIGHT`) ? 2 : 0; + const posY = mode.includes(`TOP`) ? 1 : mode.includes(`BOTTOM`) ? 2 : 0; + + let x = posX === 0 ? width/2 : + posX === 1 ? (width * margins[0]) : width * (1 - margins[2]) ; + let y = posY === 0 ? height/2 - (height/VISUAL_CENTER_FT) : + posY === 1 ? (height * margins[1]) : height - (height * margins[3]) + + + return { - x: width/2, - y: height/2 - (height/VISUAL_CENTER_FT) + x: x, + y: y } } diff --git a/src/@evoposter/evaluator/test/unit/VisualBalance.test.js b/src/@evoposter/evaluator/test/unit/VisualBalance.test.js index a3c7569..ba60b52 100644 --- a/src/@evoposter/evaluator/test/unit/VisualBalance.test.js +++ b/src/@evoposter/evaluator/test/unit/VisualBalance.test.js @@ -88,6 +88,7 @@ const TESTING_PARAMS = [ heights: [12.38095238095238, 11.38095238095238, 12.38095238095238, 11.38095238095238, 12.38095238095238, 12.38095238095238, 12.38095238095238, 12.38095238095238, 12.38095238095238, 12.38095238095238], widths: [264.93060302734375, 103.13275146484375, 98.85130310058594, 91.71133422851562, 80.71405029296875, 14.855758666992188, 73.46426391601562, 70.31559753417969, 109.78497314453125, 109.39274597167969], visualWeights: [911.5736477154408, 319.2937844631935, 346.174007090054, 434.42082642696874, 345.92923073281816, 77.71624681230338, 312.8142084364499, 268.67683328540517, 426.86878515497756, 441.40525053069166], + mode: `CENTER`, res: 0.6874696913950737, valid: true }, @@ -173,6 +174,7 @@ const TESTING_PARAMS = [ widths: [264.93060302734375, 103.13275146484375, 98.85130310058594, 91.71133422851562, 80.71405029296875, 14.855758666992188, 73.46426391601562, 70.31559753417969, 109.78497314453125, 109.39274597167969], visualWeights: [911.5736477154408, 319.2937844631935, 346.174007090054, 434.42082642696874, 345.92923073281816, 77.71624681230338, 312.8142084364499, 268.67683328540517, 426.86878515497756, 441.40525053069166], res: 0.5, + mode: `CENTER`, valid: false }, { @@ -290,6 +292,7 @@ const TESTING_PARAMS = [ 291.6050576564197 ], res: 0.6983685363606276, + mode: `CENTER`, valid: true }, { @@ -320,6 +323,7 @@ const TESTING_PARAMS = [ widths: [276.33221435546875], visualWeights: [7973.376582108727], res: 0.37440557682834674, + mode: `CENTER`, valid: true, }, { @@ -350,6 +354,7 @@ const TESTING_PARAMS = [ widths: [276], visualWeights: [7973], res: 0, + mode: `CENTER`, valid: false, }, { @@ -404,6 +409,7 @@ const TESTING_PARAMS = [ 1108.3613454753233 ], res: 0.7099021460649569, + mode: `CENTER`, valid: true }, { @@ -458,7 +464,255 @@ const TESTING_PARAMS = [ 1108.3613454753233 ], res: 1, + mode: `CENTER`, valid: false + }, + { + rows: { + "x": { + "left": -45, + "right": 45 + }, + "center": { + "0": -60.39999999999999, + "1": -30.39999999999999, + "2": -1.3999999999999915, + "3": 27.60000000000001, + "4": 56.60000000000001 + }, + "gap": { + "0": { + "bottom": -60.39999999999999, + "top": -60.39999999999999 + }, + "1": { + "bottom": -28.284999999999993, + "top": -32.51499999999999 + }, + "2": { + "bottom": 0.7150000000000083, + "top": -3.5149999999999912 + }, + "3": { + "bottom": 29.715000000000007, + "top": 25.48500000000001 + }, + "4": { + "bottom": 56.60000000000001, + "top": 56.60000000000001 + } + }, + "l": [ + 30.95, + 29.95, + 29.95, + 29.95 + ] + }, + heights: [ + 342.25732421875, + 287.4759826660156, + 293.0855712890625, + 437.04632568359375 + ], + widths: [ + 29.476190476190474, + 28.523809523809522, + 28.523809523809522, + 28.523809523809522 + ], + visualWeights: [ + 3727.5057306443678, + 3622.4177405315445, + 3234.7133591917686, + 4203.703294362204 + ], + res: 0.5303072425405706, + mode: `RIGHT-TOP`, + valid: true + }, + { + rows: { + "x": { + "left": -45, + "right": 45 + }, + "center": { + "0": -63.45, + "1": -31.450000000000003, + "2": -1.4500000000000028, + "3": 28.549999999999997, + "4": 58.55 + }, + "gap": { + "0": { + "bottom": -63.45, + "top": -63.45 + }, + "1": { + "bottom": -29.335000000000004, + "top": -33.565000000000005 + }, + "2": { + "bottom": 0.6649999999999969, + "top": -3.5650000000000026 + }, + "3": { + "bottom": 30.664999999999996, + "top": 26.435 + }, + "4": { + "bottom": 58.55, + "top": 58.55 + } + }, + "l": [ + 32, + 30.95, + 30.95, + 30.95 + ] + }, + heights: [ + 170.4186553955078, + 297.0517272949219, + 174.01736450195312, + 230.81512451171875 + ], + widths: [ + 30.476190476190474, + 29.476190476190474, + 29.476190476190474, + 29.476190476190474 + ], + visualWeights: [ + 2144.2423788076026, + 3100.417377039685, + 2374.310169650738, + 2925.722717489419 + ], + res: 0.5367634140653805, + mode: `RIGHT-TOP`, + valid: true + }, + { + rows: { + "x": { + "left": -45, + "right": 45 + }, + "center": { + "0": -62.425, + "1": -30.424999999999997, + "2": 1.5750000000000028, + "3": 30.575000000000003, + "4": 60.575 + }, + "gap": { + "0": { + "bottom": -62.425, + "top": -62.425 + }, + "1": { + "bottom": -28.31, + "top": -32.54 + }, + "2": { + "bottom": 3.6900000000000026, + "top": -0.5399999999999969 + }, + "3": { + "bottom": 32.690000000000005, + "top": 28.460000000000004 + }, + "4": { + "bottom": 60.575, + "top": 60.575 + } + }, + "l": [ + 32, + 32, + 29.9, + 30.95 + ] + }, + widths: [ + 30.476190476190474, + 30.476190476190474, + 28.476190476190474, + 29.476190476190474 + ], + heights: [ + 133.9654083251953, + 382.00225830078125, + 343.7714538574219, + 477.23687744140625 + ], + visualWeights: [ + 1381.1239085281425, + 5107.805296971529, + 3543.086645087024, + 4825.177613113833 + ], + mode: `RIGHT-TOP`, + res: 0.5367276624494475, + valid: true + }, + { + rows: { + "x": { + "left": -45, + "right": 45 + }, + "center": { + "0": -55.39999999999999, + "1": -16.39999999999999, + "2": 23.60000000000001, + "3": 61.60000000000001 + }, + "gap": { + "0": { + "bottom": -55.39999999999999, + "top": -55.39999999999999 + }, + "1": { + "bottom": -14.284999999999991, + "top": -18.51499999999999 + }, + "2": { + "bottom": 25.715000000000007, + "top": 21.48500000000001 + }, + "3": { + "bottom": 61.60000000000001, + "top": 61.60000000000001 + } + }, + "l": [ + 39.9, + 40, + 38.95 + ] + }, + widths: [ + 38, + 38.095238095238095, + 37.095238095238095 + ], + heights: [ + 594.3903198242188, + 760.325927734375, + 456.15655517578125 + ], + mode: `RIGHT-BOTTOM`, + visualWeights: [ + 9704.27382043399, + 15381.23748966179, + 10104.313922673464 + ], + res: 0, + valid: true } ]; @@ -466,7 +720,7 @@ describe(`Testing visual Balance metric`, () => { for (let i in TESTING_PARAMS) { let params = TESTING_PARAMS[i]; test(`Test ${i}`, async () => { - const res = await visualBalance(null, SIZE, params.rows, params.widths, params.heights, params.visualWeights); + const res = await visualBalance(null, SIZE, params.rows, params.widths, params.heights, params.mode, params.visualWeights); if (params.valid) { expect(res).toBe(params.res); } else {