From 55521de63c5119ef7ac70aed882baaf40d58eacc Mon Sep 17 00:00:00 2001 From: Fran Carrascosa Date: Tue, 3 Oct 2023 12:10:48 +0200 Subject: [PATCH 1/5] Rebase version and add publish information --- lerna.json | 2 +- packages/troika-three-text/package.json | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lerna.json b/lerna.json index d2bb623c..45dbe1db 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.48.1", + "version": "1.0.0", "command": { "version": { "conventionalCommits": true diff --git a/packages/troika-three-text/package.json b/packages/troika-three-text/package.json index d71a4ecf..e701f5c6 100644 --- a/packages/troika-three-text/package.json +++ b/packages/troika-three-text/package.json @@ -1,11 +1,22 @@ { "name": "troika-three-text", - "version": "0.48.1", "description": "SDF-based text rendering for Three.js", - "author": "Jason Johnston ", + "version": "1.0.0", + "author": { + "name": "Metrica Sports", + "email": "info@metrica-sports.com" + }, + "versionForked": "0.48.1", + "authorForked": { + "name": "Jason Johnston", + "email": "jason.johnston@protectwise.com" + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com/" + }, "repository": { "type": "git", - "url": "https://github.com/protectwise/troika.git", + "url": "https://github.com/metrica-sports/troika.git", "directory": "packages/troika-three-text" }, "license": "MIT", From 18ae87ce6992761283844475dc146234afd453da Mon Sep 17 00:00:00 2001 From: Fran Carrascosa Date: Tue, 3 Oct 2023 12:14:32 +0200 Subject: [PATCH 2/5] Allow rendering in WebWorkers --- packages/troika-2d/src/HitTestContext.js | 4 ++-- packages/troika-3d/src/facade/WorldTextureProvider.js | 2 +- packages/troika-animation/src/Interpolators.js | 2 +- packages/troika-three-text/src/TextBuilder.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/troika-2d/src/HitTestContext.js b/packages/troika-2d/src/HitTestContext.js index 7425527f..5edb2aab 100644 --- a/packages/troika-2d/src/HitTestContext.js +++ b/packages/troika-2d/src/HitTestContext.js @@ -6,8 +6,8 @@ * whether an Object2DFacade is under the mouse cursor, by passing this as the context * to its render() method. */ - -const hitTestContext = document.createElement('canvas').getContext('2d') +const canvas = typeof document === 'undefined' ? new OffscreenCanvas() : document.createElement('canvas') +const hitTestContext = canvas.getContext('2d') hitTestContext.save() /** diff --git a/packages/troika-3d/src/facade/WorldTextureProvider.js b/packages/troika-3d/src/facade/WorldTextureProvider.js index 2d39c769..abe05e7a 100644 --- a/packages/troika-3d/src/facade/WorldTextureProvider.js +++ b/packages/troika-3d/src/facade/WorldTextureProvider.js @@ -76,7 +76,7 @@ export function makeWorldTextureProvider(WrappedFacadeClass) { } if (newWorldConfig) { this.worldTexture.dispose() - const canvas = document.createElement('canvas') + const canvas = typeof document === 'undefined' ? new OffscreenCanvas() : document.createElement('canvas') canvas.width = newWorldConfig.width canvas.height = newWorldConfig.height this.worldTexture = new CanvasTexture(canvas) diff --git a/packages/troika-animation/src/Interpolators.js b/packages/troika-animation/src/Interpolators.js index 5356c897..54897162 100644 --- a/packages/troika-animation/src/Interpolators.js +++ b/packages/troika-animation/src/Interpolators.js @@ -50,7 +50,7 @@ const colorValueToNumber = (function() { // 2D canvas for evaluating string values if (!colorCanvas) { - colorCanvas = document.createElement('canvas') + colorCanvas = typeof document === 'undefined' ? new OffscreenCanvas() : document.createElement('canvas') colorCanvasCtx = colorCanvas.getContext('2d') } diff --git a/packages/troika-three-text/src/TextBuilder.js b/packages/troika-three-text/src/TextBuilder.js index 1d7ce9c4..89623912 100644 --- a/packages/troika-three-text/src/TextBuilder.js +++ b/packages/troika-three-text/src/TextBuilder.js @@ -164,7 +164,7 @@ function getTextRenderInfo(args, callback) { const glyphsPerRow = (textureWidth / sdfGlyphSize * 4) let atlas = atlases[sdfGlyphSize] if (!atlas) { - const canvas = document.createElement('canvas') + const canvas = typeof document === 'undefined' ? new OffscreenCanvas() : document.createElement('canvas') canvas.width = textureWidth canvas.height = sdfGlyphSize * 256 / glyphsPerRow // start tall enough to fit 256 glyphs atlas = atlases[sdfGlyphSize] = { From 0e18aa352460645d2c13811f14f19a87601ad715 Mon Sep 17 00:00:00 2001 From: Fran Carrascosa Date: Wed, 4 Oct 2023 01:32:12 +0200 Subject: [PATCH 3/5] Needed changes to be able to compile properly after adding '@metrica-sports' prefix to the package --- package.json | 6 +- packages/troika-2d/src/HitTestContext.js | 2 +- .../src/facade/WorldTextureProvider.js | 2 +- .../troika-animation/src/Interpolators.js | 2 +- .../libs/woff2otf.factory.js | 2 +- packages/troika-three-text/package.json | 2 +- packages/troika-three-text/src/TextBuilder.js | 2 +- rollup.config.js | 62 ++++++++----------- 8 files changed, 36 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index e06f497e..d9691b21 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,9 @@ "postinstall": "npm run bootstrap", "bootstrap": "lerna bootstrap --hoist", "build": "lerna exec --ignore=troika-examples -- rollup -c \\$LERNA_ROOT_PATH/rollup.config.js", - "build-typr": "lerna exec --scope=troika-three-text -- npm run build-typr", - "build-unicode-font-resolver": "lerna exec --scope=troika-three-text -- npm run build-unicode-font-resolver", - "build-woff2otf": "lerna exec --scope=troika-three-text -- npm run build-woff2otf", + "build-typr": "lerna exec --scope=@metrica-sports/troika-three-text -- npm run build-typr", + "build-unicode-font-resolver": "lerna exec --scope=@metrica-sports/troika-three-text -- npm run build-unicode-font-resolver", + "build-woff2otf": "lerna exec --scope=@metrica-sports/troika-three-text -- npm run build-woff2otf", "build-yoga": "npm run bootstrap && lerna exec --scope=troika-flex-layout -- npm run build-yoga", "test": "jest", "build-examples": "lerna exec --scope=troika-examples -- npm run build", diff --git a/packages/troika-2d/src/HitTestContext.js b/packages/troika-2d/src/HitTestContext.js index 5edb2aab..2d408eb3 100644 --- a/packages/troika-2d/src/HitTestContext.js +++ b/packages/troika-2d/src/HitTestContext.js @@ -6,7 +6,7 @@ * whether an Object2DFacade is under the mouse cursor, by passing this as the context * to its render() method. */ -const canvas = typeof document === 'undefined' ? new OffscreenCanvas() : document.createElement('canvas') +const canvas = typeof document === 'undefined' ? new OffscreenCanvas(300, 150) : document.createElement('canvas') const hitTestContext = canvas.getContext('2d') hitTestContext.save() diff --git a/packages/troika-3d/src/facade/WorldTextureProvider.js b/packages/troika-3d/src/facade/WorldTextureProvider.js index abe05e7a..ccb50152 100644 --- a/packages/troika-3d/src/facade/WorldTextureProvider.js +++ b/packages/troika-3d/src/facade/WorldTextureProvider.js @@ -76,7 +76,7 @@ export function makeWorldTextureProvider(WrappedFacadeClass) { } if (newWorldConfig) { this.worldTexture.dispose() - const canvas = typeof document === 'undefined' ? new OffscreenCanvas() : document.createElement('canvas') + const canvas = typeof document === 'undefined' ? new OffscreenCanvas(300, 150) : document.createElement('canvas') canvas.width = newWorldConfig.width canvas.height = newWorldConfig.height this.worldTexture = new CanvasTexture(canvas) diff --git a/packages/troika-animation/src/Interpolators.js b/packages/troika-animation/src/Interpolators.js index 54897162..7b9a96c8 100644 --- a/packages/troika-animation/src/Interpolators.js +++ b/packages/troika-animation/src/Interpolators.js @@ -50,7 +50,7 @@ const colorValueToNumber = (function() { // 2D canvas for evaluating string values if (!colorCanvas) { - colorCanvas = typeof document === 'undefined' ? new OffscreenCanvas() : document.createElement('canvas') + colorCanvas = typeof document === 'undefined' ? new OffscreenCanvas(300, 150) : document.createElement('canvas') colorCanvasCtx = colorCanvas.getContext('2d') } diff --git a/packages/troika-three-text/libs/woff2otf.factory.js b/packages/troika-three-text/libs/woff2otf.factory.js index 5adfae74..7b766450 100644 --- a/packages/troika-three-text/libs/woff2otf.factory.js +++ b/packages/troika-three-text/libs/woff2otf.factory.js @@ -5,4 +5,4 @@ Original licenses apply: - fflate: https://github.com/101arrowz/fflate/blob/master/LICENSE (MIT) - woff2otf.js: https://github.com/arty-name/woff2otf/blob/master/woff2otf.js (Apache2) */ -export default function(){return function(r){"use strict";var e=Uint8Array,n=Uint16Array,t=Uint32Array,a=new e([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),i=new e([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),o=new e([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),f=function(r,e){for(var a=new n(31),i=0;i<31;++i)a[i]=e+=1<>>1|(21845&g)<<1;h=(61680&(h=(52428&h)>>>2|(13107&h)<<2))>>>4|(3855&h)<<4,c[g]=((65280&h)>>>8|(255&h)<<8)>>>1}var w=function(r,e,t){for(var a=r.length,i=0,o=new n(e);i>>v]=s}else for(f=new n(a),i=0;i>>15-r[i]);return f},d=new e(288);for(g=0;g<144;++g)d[g]=8;for(g=144;g<256;++g)d[g]=9;for(g=256;g<280;++g)d[g]=7;for(g=280;g<288;++g)d[g]=8;var m=new e(32);for(g=0;g<32;++g)m[g]=5;var b=w(d,9,1),p=w(m,5,1),y=function(r){for(var e=r[0],n=1;ne&&(e=r[n]);return e},L=function(r,e,n){var t=e/8|0;return(r[t]|r[t+1]<<8)>>(7&e)&n},U=function(r,e){var n=e/8|0;return(r[n]|r[n+1]<<8|r[n+2]<<16)>>(7&e)},k=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],T=function(r,e,n){var t=new Error(e||k[r]);if(t.code=r,Error.captureStackTrace&&Error.captureStackTrace(t,T),!n)throw t;return t},O=function(r,f,u){var s=r.length;if(!s||u&&!u.l&&s<5)return f||new e(0);var c=!f||u,g=!u||u.i;u||(u={}),f||(f=new e(3*s));var h,d=function(r){var n=f.length;if(r>n){var t=new e(Math.max(2*n,r));t.set(f),f=t}},m=u.f||0,k=u.p||0,O=u.b||0,A=u.l,x=u.d,E=u.m,D=u.n,M=8*s;do{if(!A){u.f=m=L(r,k,1);var S=L(r,k+1,3);if(k+=3,!S){var V=r[(I=((h=k)/8|0)+(7&h&&1)+4)-4]|r[I-3]<<8,_=I+V;if(_>s){g&&T(0);break}c&&d(O+V),f.set(r.subarray(I,_),O),u.b=O+=V,u.p=k=8*_;continue}if(1==S)A=b,x=p,E=9,D=5;else if(2==S){var j=L(r,k,31)+257,z=L(r,k+10,15)+4,C=j+L(r,k+5,31)+1;k+=14;for(var F=new e(C),P=new e(19),q=0;q>>4)<16)F[q++]=I;else{var K=0,N=0;for(16==I?(N=3+L(r,k,3),k+=2,K=F[q-1]):17==I?(N=3+L(r,k,7),k+=3):18==I&&(N=11+L(r,k,127),k+=7);N--;)F[q++]=K}}var Q=F.subarray(0,j),R=F.subarray(j);E=y(Q),D=y(R),A=w(Q,E,1),x=w(R,D,1)}else T(1);if(k>M){g&&T(0);break}}c&&d(O+131072);for(var W=(1<>>4;if((k+=15&K)>M){g&&T(0);break}if(K||T(2),Z<256)f[O++]=Z;else{if(256==Z){Y=k,A=null;break}var $=Z-254;if(Z>264){var rr=a[q=Z-257];$=L(r,k,(1<>>4;er||T(3),k+=15&er;R=l[nr];if(nr>3){rr=i[nr];R+=U(r,k)&(1<M){g&&T(0);break}c&&d(O+131072);for(var tr=O+$;Or.length)&&(i=r.length);var o=new(r instanceof n?n:r instanceof t?t:e)(i-a);return o.set(r.subarray(a,i)),o}(f,0,O)},A=new e(0);var x="undefined"!=typeof TextDecoder&&new TextDecoder;try{x.decode(A,{stream:!0}),1}catch(r){}return r.convert_streams=function(r){var e=new DataView(r),n=0;function t(){var r=e.getUint16(n);return n+=2,r}function a(){var r=e.getUint32(n);return n+=4,r}function i(r){m.setUint16(b,r),b+=2}function o(r){m.setUint32(b,r),b+=4}for(var f={signature:a(),flavor:a(),length:a(),numTables:t(),reserved:t(),totalSfntSize:a(),majorVersion:t(),minorVersion:t(),metaOffset:a(),metaLength:a(),metaOrigLength:a(),privOffset:a(),privLength:a()},u=0;Math.pow(2,u)<=f.numTables;)u++;u--;for(var v=16*Math.pow(2,u),s=16*f.numTables-v,l=12,c=[],g=0;g>>1|(21845&g)<<1;h=(61680&(h=(52428&h)>>>2|(13107&h)<<2))>>>4|(3855&h)<<4,c[g]=((65280&h)>>>8|(255&h)<<8)>>>1}var w=function(r,e,t){for(var a=r.length,f=0,i=new n(e);f>>v]=l}else for(o=new n(a),f=0;f>>15-r[f]);return o},d=new e(288);for(g=0;g<144;++g)d[g]=8;for(g=144;g<256;++g)d[g]=9;for(g=256;g<280;++g)d[g]=7;for(g=280;g<288;++g)d[g]=8;var m=new e(32);for(g=0;g<32;++g)m[g]=5;var b=w(d,9,1),p=w(m,5,1),y=function(r){for(var e=r[0],n=1;ne&&(e=r[n]);return e},E=function(r,e,n){var t=e/8|0;return(r[t]|r[t+1]<<8)>>(7&e)&n},L=function(r,e){var n=e/8|0;return(r[n]|r[n+1]<<8|r[n+2]<<16)>>(7&e)},T=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],U=function(r,e,n){var t=new Error(e||T[r]);if(t.code=r,Error.captureStackTrace&&Error.captureStackTrace(t,U),!n)throw t;return t},k=function(r,o,u){var l=r.length;if(!l||u&&u.f&&!u.l)return o||new e(0);var c=!o||u,g=!u||u.i;u||(u={}),o||(o=new e(3*l));var h=function(r){var n=o.length;if(r>n){var t=new e(Math.max(2*n,r));t.set(o),o=t}},d=u.f||0,m=u.p||0,T=u.b||0,k=u.l,O=u.d,A=u.m,_=u.n,x=8*l;do{if(!k){d=E(r,m,1);var M=E(r,m+1,3);if(m+=3,!M){var S=r[(Y=4+((m+7)/8|0))-4]|r[Y-3]<<8,D=Y+S;if(D>l){g&&U(0);break}c&&h(T+S),o.set(r.subarray(Y,D),T),u.b=T+=S,u.p=m=8*D,u.f=d;continue}if(1==M)k=b,O=p,A=9,_=5;else if(2==M){var V=E(r,m,31)+257,P=E(r,m+10,15)+4,j=V+E(r,m+5,31)+1;m+=14;for(var z=new e(j),B=new e(19),C=0;C>>4)<16)z[C++]=Y;else{var G=0,H=0;for(16==Y?(H=3+E(r,m,3),m+=2,G=z[C-1]):17==Y?(H=3+E(r,m,7),m+=3):18==Y&&(H=11+E(r,m,127),m+=7);H--;)z[C++]=G}}var I=z.subarray(0,V),J=z.subarray(V);A=y(I),_=y(J),k=w(I,A,1),O=w(J,_,1)}else U(1);if(m>x){g&&U(0);break}}c&&h(T+131072);for(var K=(1<>>4;if((m+=15&G)>x){g&&U(0);break}if(G||U(2),X<256)o[T++]=X;else{if(256==X){W=m,k=null;break}var Z=X-254;if(X>264){var $=a[C=X-257];Z=E(r,m,(1<<$)-1)+v[C],m+=$}var rr=O[L(r,m)&Q],er=rr>>>4;rr||U(3),m+=15&rr;J=s[er];if(er>3){$=f[er];J+=L(r,m)&(1<<$)-1,m+=$}if(m>x){g&&U(0);break}c&&h(T+131072);for(var nr=T+Z;Tr.length)&&(f=r.length);var i=new(2==r.BYTES_PER_ELEMENT?n:4==r.BYTES_PER_ELEMENT?t:e)(f-a);return i.set(r.subarray(a,f)),i}(o,0,T)},O=new e(0);var A="undefined"!=typeof TextDecoder&&new TextDecoder;try{A.decode(O,{stream:!0}),1}catch(r){}return r.convert_streams=function(r){var e=new DataView(r),n=0;function t(){var r=e.getUint16(n);return n+=2,r}function a(){var r=e.getUint32(n);return n+=4,r}function f(r){m.setUint16(b,r),b+=2}function i(r){m.setUint32(b,r),b+=4}for(var o={signature:a(),flavor:a(),length:a(),numTables:t(),reserved:t(),totalSfntSize:a(),majorVersion:t(),minorVersion:t(),metaOffset:a(),metaLength:a(),metaOrigLength:a(),privOffset:a(),privLength:a()},u=0;Math.pow(2,u)<=o.numTables;)u++;u--;for(var v=16*Math.pow(2,u),l=16*o.numTables-v,s=12,c=[],g=0;g { - out[sib] = sib.replace(/-/g, '_') - return out -},{ - react: 'React', - three: 'THREE', - 'bidi-js': 'bidi_js', - 'webgl-sdf-generator': 'webgl_sdf_generator', - 'three/examples/jsm/loaders/GLTFLoader.js': 'THREE.GLTFLoader', - 'prop-types': 'PropTypes', - 'object-path': 'objectPath' -}) +const EXTERNAL_GLOBALS = SIBLING_PACKAGES.reduce( + (out, sib) => { + out[sib] = sib.replace(/-/g, '_') + return out + }, + { + react: 'React', + three: 'THREE', + 'bidi-js': 'bidi_js', + 'webgl-sdf-generator': 'webgl_sdf_generator', + 'three/examples/jsm/loaders/GLTFLoader.js': 'THREE.GLTFLoader', + 'prop-types': 'PropTypes', + 'object-path': 'objectPath', + '@metrica-sports/troika-three-text': 'troika_three_text' + } +) // Some packages (e.g. those with worker code) we want to transpile in the ESM // in addition to the UMD: // TODO make this more fine-grained than the whole package -const TRANSPILE_PACKAGES = [ - 'troika-worker-utils' -] - +const TRANSPILE_PACKAGES = ['troika-worker-utils'] const onwarn = (warning, warn) => { // Quiet the 'Use of eval is strongly discouraged' warnings from Yoga lib @@ -57,7 +55,6 @@ const onwarn = (warning, warn) => { warn(warning) } - // Allow an individual package to define custom entry point(s) and output, via a // json file in its root. If not present, uses a default. let entries @@ -70,22 +67,20 @@ if (fs.existsSync(entriesPath)) { } } - const builds = [] for (let entry of Object.keys(entries)) { - const outFilePrefix = entries[entry] + const outFilePrefix = entries[entry].replace('@metrica-sports/', '') + const outputName = EXTERNAL_GLOBALS[LERNA_PACKAGE_NAME] || outFilePrefix builds.push( // ES module file { input: entry, output: { format: 'esm', - file: `dist/${outFilePrefix}.esm.js` + file: `dist/${outFilePrefix}.esm.js`, }, external: Object.keys(EXTERNAL_GLOBALS), - plugins: [ - TRANSPILE_PACKAGES.includes(LERNA_PACKAGE_NAME) ? buble() : null - ], + plugins: [TRANSPILE_PACKAGES.includes(LERNA_PACKAGE_NAME) ? buble() : null], onwarn }, // UMD file @@ -94,13 +89,11 @@ for (let entry of Object.keys(entries)) { output: { format: 'umd', file: `dist/${outFilePrefix}.umd.js`, - name: EXTERNAL_GLOBALS[LERNA_PACKAGE_NAME], + name: outputName, globals: EXTERNAL_GLOBALS }, external: Object.keys(EXTERNAL_GLOBALS), - plugins: [ - TRANSPILE_PACKAGES.includes(LERNA_PACKAGE_NAME) ? buble() : null - ], + plugins: [TRANSPILE_PACKAGES.includes(LERNA_PACKAGE_NAME) ? buble() : null], onwarn }, // UMD file, minified @@ -109,7 +102,7 @@ for (let entry of Object.keys(entries)) { output: { format: 'umd', file: `dist/${outFilePrefix}.umd.min.js`, - name: EXTERNAL_GLOBALS[LERNA_PACKAGE_NAME], + name: outputName, globals: EXTERNAL_GLOBALS }, external: Object.keys(EXTERNAL_GLOBALS), @@ -122,5 +115,4 @@ for (let entry of Object.keys(entries)) { ) } - export default builds From 892bb35df1cdfb4f53edd7ed312869175092ccc1 Mon Sep 17 00:00:00 2001 From: Fran Carrascosa Date: Fri, 26 Apr 2024 01:25:32 +0200 Subject: [PATCH 4/5] When a font loading fails, invoke the callback to return. The object returned will have a function to indicate there's no glyphs so it'll render with the default font --- packages/troika-three-text/src/FontResolver.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/troika-three-text/src/FontResolver.js b/packages/troika-three-text/src/FontResolver.js index e4ecf5a1..12b38712 100644 --- a/packages/troika-three-text/src/FontResolver.js +++ b/packages/troika-three-text/src/FontResolver.js @@ -50,6 +50,7 @@ export function createFontResolver(fontParser, unicodeFontResolverClient) { function doLoadFont(url, callback) { const onError = err => { console.error(`Failure loading font ${url}`, err) + callback({ supportsCodePoint: () => null }) } try { const request = new XMLHttpRequest() From 3b7cafc9a6b7c706d74dd9d0f88b6e4db74725e4 Mon Sep 17 00:00:00 2001 From: Fran Carrascosa Date: Fri, 26 Apr 2024 01:25:46 +0200 Subject: [PATCH 5/5] Bump version --- packages/troika-three-text/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/troika-three-text/package.json b/packages/troika-three-text/package.json index a496ce69..0daafbb3 100644 --- a/packages/troika-three-text/package.json +++ b/packages/troika-three-text/package.json @@ -1,7 +1,7 @@ { "name": "@metrica-sports/troika-three-text", "description": "SDF-based text rendering for Three.js", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Metrica Sports", "email": "info@metrica-sports.com"