summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js
blob: 257df55e54fb44fb2d51ed36934dacae864f3088 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import AccessorUtilities from '../../lib/utils/accessor';

const unicodeSupportTestMap = {
  // man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
  // occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
  // woman, biking (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
  // sexZwj: '\u{1F6B4}\u{200D}\u{2640}',
  // family_mwgb
  // Windows 8.1, Firefox 51.0.1 does not support `family_`, `kiss_`, `couple_`
  personZwj: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}',
  // horse_racing_tone5
  // Special case that is not supported on macOS 10.12 even though `skinToneModifier` succeeds
  horseRacing: '\u{1F3C7}\u{1F3FF}',
  // US flag, http://emojipedia.org/flags/
  flag: '\u{1F1FA}\u{1F1F8}',
  // http://emojipedia.org/modifiers/
  skinToneModifier: [
    // spy_tone5
    '\u{1F575}\u{1F3FF}',
    // person_with_ball_tone5
    '\u{26F9}\u{1F3FF}',
    // angel_tone5
    '\u{1F47C}\u{1F3FF}',
  ],
  // rofl, http://emojipedia.org/unicode-9.0/
  '9.0': '\u{1F923}',
  // metal, http://emojipedia.org/unicode-8.0/
  '8.0': '\u{1F918}',
  // spy, http://emojipedia.org/unicode-7.0/
  '7.0': '\u{1F575}',
  // expressionless, http://emojipedia.org/unicode-6.1/
  6.1: '\u{1F611}',
  // japanese_goblin, http://emojipedia.org/unicode-6.0/
  '6.0': '\u{1F47A}',
  // sailboat, http://emojipedia.org/unicode-5.2/
  5.2: '\u{26F5}',
  // mahjong, http://emojipedia.org/unicode-5.1/
  5.1: '\u{1F004}',
  // gear, http://emojipedia.org/unicode-4.1/
  4.1: '\u{2699}',
  // zap, http://emojipedia.org/unicode-4.0/
  '4.0': '\u{26A1}',
  // recycle, http://emojipedia.org/unicode-3.2/
  3.2: '\u{267B}',
  // information_source, http://emojipedia.org/unicode-3.0/
  '3.0': '\u{2139}',
  // heart, http://emojipedia.org/unicode-1.1/
  1.1: '\u{2764}',
};

function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
  // `4 *` because RGBA
  const indexOffset = 4 * pixelOffset;
  const hasColor = imageDataArray[indexOffset + 0] ||
    imageDataArray[indexOffset + 1] ||
    imageDataArray[indexOffset + 2];
  const isVisible = imageDataArray[indexOffset + 3];
  // Check for some sort of color other than black
  if (hasColor && isVisible) {
    return true;
  }
  return false;
}

const chromeMatches = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
const isChrome = chromeMatches && chromeMatches.length > 0;
const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatches[1], 10);

// We use 16px because mobile Safari (iOS 9.3) doesn't properly scale emojis :/
// See 32px, https://i.imgur.com/htY6Zym.png
// See 16px, https://i.imgur.com/FPPsIF8.png
const fontSize = 16;
function generateUnicodeSupportMap(testMap) {
  const testMapKeys = Object.keys(testMap);
  const numTestEntries = testMapKeys
    .reduce((list, testKey) => list.concat(testMap[testKey]), []).length;

  const canvas = document.createElement('canvas');
  (window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
  const ctx = canvas.getContext('2d');
  canvas.width = (2 * fontSize);
  canvas.height = (numTestEntries * fontSize);
  ctx.fillStyle = '#000000';
  ctx.textBaseline = 'middle';
  ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
  // Write each emoji to the canvas vertically
  let writeIndex = 0;
  testMapKeys.forEach((testKey) => {
    const testEntry = testMap[testKey];
    [].concat(testEntry).forEach((emojiUnicode) => {
      ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
      writeIndex += 1;
    });
  });

  // Read from the canvas
  const resultMap = {};
  let readIndex = 0;
  testMapKeys.forEach((testKey) => {
    const testEntry = testMap[testKey];
    // This needs to be a `reduce` instead of `every` because we need to
    // keep the `readIndex` in sync from the writes by running all entries
    const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
      // Sample along the vertical-middle for a couple of characters
      const imageData = ctx.getImageData(
          0,
          (readIndex * fontSize) + (fontSize / 2),
          2 * fontSize,
          1,
        ).data;

      let isValidEmoji = false;
      for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
        const isLookingAtFirstChar = currentPixel < fontSize;
        const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
        // Check for the emoji somewhere along the row
        if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
          isValidEmoji = true;

        // Check to see that nothing is rendered next to the first character
        // to ensure that the ZWJ sequence rendered as one piece
        } else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
          isValidEmoji = false;
          break;
        }
      }

      readIndex += 1;
      return isSatisfied && isValidEmoji;
    }, true);

    resultMap[testKey] = isTestSatisfied;
  });

  resultMap.meta = {
    isChrome,
    chromeVersion,
  };

  return resultMap;
}

function getUnicodeSupportMap() {
  let unicodeSupportMap;
  let userAgentFromCache;

  const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();

  if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');

  try {
    unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
  } catch (err) {
    // swallow
  }

  if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
    unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);

    if (isLocalStorageAvailable) {
      window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
      window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
    }
  }

  return unicodeSupportMap;
}

export {
  getUnicodeSupportMap,
  generateUnicodeSupportMap,
};