var CENTRE_LON = 106, VN = { lon: 107, lat: 15 }; var SPECTRUM = ['#d65620', '#9f7aee', '#4575cd', '#71d2f0', '#44b48b', '#f4df69']; function mix(a, b, t) { var pa = [parseInt(a.slice(1,3),16), parseInt(a.slice(3,5),16), parseInt(a.slice(5,7),16)]; var pb = [parseInt(b.slice(1,3),16), parseInt(b.slice(3,5),16), parseInt(b.slice(5,7),16)]; return pa.map(function (v, i) { return Math.round(v + (pb[i] - v) * t); }); } function spectrumAt(t) { t = Math.max(0, Math.min(0.999, t)); var s = t * (SPECTRUM.length - 1), i = Math.floor(s); return mix(SPECTRUM[i], SPECTRUM[i + 1], s - i); } function inside(lon, lat, poly) { var hit = false; for (var i = 0, j = poly.length - 1; i < poly.length; j = i++) { var xi = poly[i][0], yi = poly[i][1], xj = poly[j][0], yj = poly[j][1]; if (((yi > lat) !== (yj > lat)) && (lon < (xj - xi) * (lat - yi) / (yj - yi) + xi)) { hit = !hit; } } return hit; } function isLand(lon, lat) { for (var i = 0; i < LAND.length; i++) { if (inside(lon, lat, LAND[i])) { return true; } } return false; } function mount(canvas, opts) { if (!canvas || !canvas.getContext) { return null; } var ctx = canvas.getContext('2d'); opts = opts || {}; var cities = opts.cities || []; var pinHost = opts.pins ? document.querySelector(opts.pins) : null; var cxFrac = typeof opts.centreX === 'number' ? opts.centreX : 0.72; var cyFrac = typeof opts.centreY === 'number' ? opts.centreY : 0.44; var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; var animate = opts.animate !== false && !reduce; var pts = [], geom = null, raf = null, last = 0, visible = true; var FPS = 30, FRAME = 1000 / FPS; // One pass over the grid, kept until the size changes. function build() { var host = canvas.parentNode, w = host.offsetWidth, h = host.offsetHeight; if (!w || !h) { return false; } var dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = w * dpr; canvas.height = h * dpr; canvas.style.width = w + 'px'; canvas.style.height = h + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); var pitch = 11, scaleX = (w * 1.3) / 360, scaleY = scaleX * 1.15; var cx = w * cxFrac, cy = h * cyFrac; var vx = cx + (((VN.lon - CENTRE_LON + 540) % 360) - 180) * scaleX; var vy = cy - VN.lat * scaleY; var bloom = Math.max(w, h) * 0.42; geom = { w: w, h: h, cx: cx, cy: cy, scaleX: scaleX, scaleY: scaleY }; pts = []; for (var py = 0; py < h; py += pitch) { for (var px = 0; px < w; px += pitch) { var lat = (cy - py) / scaleY; if (lat > 84 || lat < -58) { continue; } var lon = ((px - cx) / scaleX) + CENTRE_LON; var nlon = ((lon + 540) % 360) - 180; if (!isLand(nlon, lat)) { continue; } var near = Math.max(0, 1 - Math.hypot(px - vx, py - vy) / bloom); var far = near < 0.06; var rgb = far ? [17, 26, 74] : spectrumAt(0.18 + (1 - near) * 0.72); pts.push({ x: px, y: py, r: 1.4 + near * 1.5, a: far ? 0.18 : (0.28 + near * 0.62), c: rgb[0] + ',' + rgb[1] + ',' + rgb[2], // Phase from position: the wave then reads as one front crossing the // map rather than each dot flickering independently. p: (px + py) * 0.0075 }); } } return true; } function paint(t) { if (!geom) { return; } ctx.clearRect(0, 0, geom.w, geom.h); var wave = t * 0.0011; for (var i = 0; i < pts.length; i++) { var d = pts[i]; // 0.72..1.0 of the dot's own alpha - present, not blinking. var m = animate ? (0.86 + 0.14 * Math.sin(wave - d.p)) : 1; ctx.globalAlpha = d.a * m; ctx.fillStyle = 'rgb(' + d.c + ')'; ctx.beginPath(); ctx.arc(d.x, d.y, d.r * (animate ? (0.94 + 0.06 * m) : 1), 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; drawPins(); } function drawPins() { if (!pinHost || !cities.length || !geom) { return; } if (getComputedStyle(pinHost).display === 'none') { return; } var GAP = 38, OFF = 30; var list = cities.map(function (c) { return { c: c, x: geom.cx + (((c.lon - CENTRE_LON + 540) % 360) - 180) * geom.scaleX, y: geom.cy - c.lat * geom.scaleY }; }).sort(function (a, b) { return a.y - b.y; }); var mid = list.reduce(function (s, p) { return s + p.y; }, 0) / list.length; var top = mid - GAP * (list.length - 1) / 2; list.forEach(function (p, i) { p.ly = top + i * GAP; p.lx = p.x + OFF; }); // Only rewrite the DOM when the layout actually moved - this runs every // frame otherwise and would thrash on every repaint. var key = list.map(function (p) { return Math.round(p.x) + ':' + Math.round(p.ly); }).join('|'); if (key !== pinHost.getAttribute('data-layout')) { pinHost.setAttribute('data-layout', key); pinHost.innerHTML = list.map(function (p) { return '' + '' + '' + '' + p.c.name + '' + '' + p.c.n + ''; }).join(''); } ctx.strokeStyle = 'rgba(17,26,74,.28)'; ctx.lineWidth = 1; list.forEach(function (p) { ctx.beginPath(); ctx.moveTo(p.x + 7, p.y); ctx.lineTo(p.lx - 4, p.ly); ctx.stroke(); }); } function loop(t) { raf = null; if (!visible || document.hidden) { return; } if (t - last >= FRAME) { last = t; paint(t); } raf = requestAnimationFrame(loop); } function start() { if (!animate || raf) { return; } raf = requestAnimationFrame(loop); } function stop() { if (raf) { cancelAnimationFrame(raf); raf = null; } } function rebuild() { if (build()) { paint(performance.now()); } } rebuild(); start(); var t0; window.addEventListener('resize', function () { clearTimeout(t0); t0 = setTimeout(rebuild, 150); }); // A background nobody can see must not burn a frame budget. document.addEventListener('visibilitychange', function () { if (document.hidden) { stop(); } else { start(); } }); if (window.IntersectionObserver) { new IntersectionObserver(function (entries) { visible = entries[0].isIntersecting; if (visible) { start(); } else { stop(); } }, { threshold: 0 }).observe(canvas); } return { repaint: rebuild, stop: stop, start: start }; } return { mount: mount }; })();
6,652 companies placed by city

Find who builds software in Vietnam.

Is your company listed?

Add your company to the directory, or list a product you have built. Every fact on a company page shows where it came from.