Skip to content

Commit

Permalink
Merge pull request #214 from catdad/#213-emoji-in-firefox
Browse files Browse the repository at this point in the history
fixing issues with Firefox, rendering text inside tight box
  • Loading branch information
catdad authored Nov 24, 2023
2 parents 1177ec0 + cff02b7 commit a17396a
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 6 deletions.
204 changes: 204 additions & 0 deletions fixtures/debug.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debug emoji confetti</title>
</head>
<body>
<script>
// this page is a demo that is not built, so fudge the module.exports support
// define a global `module` so that the actual source file can use it
window.module = {};
</script>
<script src="../src/confetti.js"></script>
<script>
// define the `module.exports` as the `confetti` global, the way that the
// cdn distributed file would
window.confetti = module.exports;
</script>

<p><button id="test-confetti">Emoji confetti test</button></p>
<canvas id="debug-canvas" width="600" height="650" style="outline: 1px solid red"></canvas>
<p id="test-text"></p>

<script>
const loadFonts = async () => {
const isSafari = navigator.vendor === 'Apple Computer, Inc.';
const fontName = 'Noto Color Emoji';

await Promise.all([0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => {
const safari = `url(https://fonts.gstatic.com/s/notocoloremoji/v25/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4hQ.${n}.woff2)`;
const realBrowser = `url(https://fonts.gstatic.com/s/notocoloremoji/v25/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.${n}.woff2)`;

const fontFile = new FontFace(
fontName,
isSafari ? safari : realBrowser
);

document.fonts.add(fontFile);

return fontFile.load();
}));

return `"${fontName}"`;
};

const connectTest = ({ fontFamily }) => {
const button = document.getElementById('test-confetti');

const scalar = 3;

// make this a getter so that the shapes don't initialize on page load
const getShapes = ((shapes) => () => {
if (shapes.length) {
return shapes;
}

shapes = [
['🦄', '🍑', '🤣', '🐈', 'bg'].map(text => confetti.shapeFromText({ text, scalar })),
['🦄', '🍑', '🤣', '🐈', 'bg'].map(text => confetti.shapeFromText({ text, scalar, fontFamily }))
];

return shapes;
})([]);

button.onclick = () => {
const [shapesDefault, shapesFont] = getShapes();

confetti({
particleCount: 10,
shapes: shapesDefault,
scalar,
origin: { y: 0.7, x: 0.25 }
});
confetti({
particleCount: 10,
shapes: shapesFont,
scalar,
origin: { y: 0.7, x: 0.75 }
});
};
};

const drawBitmapToCanvas = (bitmap) => {
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext('2d');

ctx.drawImage(bitmap, 0, 0);

return canvas;
};

const drawTransformedEmoji = async ({ fontFamily, canvas, ctx, offsetX = 0, offsetY = 0 }) => {
['🦄', '🍑', '🤣', '🐈'].forEach((text, idx) => {
const shape1 = confetti.shapeFromText({ text, scalar: 1, fontFamily });
const shape2 = confetti.shapeFromText({ text, scalar: 2, fontFamily });
const shape5 = confetti.shapeFromText({ text, scalar: 5, fontFamily });

const y = idx * 100 + 50 + offsetY;

ctx.drawImage(shape1.bitmap, 0, y);
ctx.drawImage(shape2.bitmap, 100, y);
ctx.drawImage(shape5.bitmap, 200, y);

[[shape1, 1], [shape2, 2], [shape5, 5]].forEach(([shape, scale], j) => {
const x = 300 + (j * 100) + offsetX;

const rotation = 3.2;
const scaleX = scale * 0.8;
const scaleY = scale * 1.4;

var matrix = new DOMMatrix([
Math.cos(rotation) * scaleX,
Math.sin(rotation) * scaleX,
-Math.sin(rotation) * scaleY,
Math.cos(rotation) * scaleY,
x,
y
]);
matrix.multiplySelf(new DOMMatrix(shape.matrix));

const pattern = (() => {
try {
// most browsers support this, it's spec
return ctx.createPattern(shape.bitmap, 'no-repeat');
} catch (e) {
// safari doesn't, because of course it doesn't
// so draw the bitmap to a canvas first and create a
// pattern from that canvas
console.log('failed to create bitmap pattern:', e);
return ctx.createPattern(drawBitmapToCanvas(shape.bitmap), 'no-repeat');
}
})();

pattern.setTransform(matrix);

ctx.fillStyle = pattern;
ctx.fillRect(x - 100, y - 100, 300, 300);
});
});
};

const drawDebugEmoji = async ({ fontFamily, canvas, ctx, offsetX = 0, offsetY = 0 }) => {
const text = '🦄';

const draw = ({ offsetX, offsetY, fontFamily }) => {
const opts = { text, scalar: 15 };

if (fontFamily) {
opts.fontFamily = fontFamily;
}

const shape = confetti.shapeFromText(opts);

ctx.drawImage(shape.bitmap, offsetX, offsetY);

ctx.lineWidth = 1;
ctx.strokeStyle = 'orange';
ctx.strokeRect(offsetX, offsetY, shape.bitmap.width, shape.bitmap.height);

return { width: shape.bitmap.width, height: shape.bitmap.height };
};

const { width, height: height1 } = draw({ offsetX: 10 + offsetX, offsetY: 10 + offsetY });
const { height: height2 } = draw({ offsetX: 20 + width + offsetX, offsetY: 10 + offsetY, fontFamily });

return Math.max(height1, height2);
};

const renderTestText = ({ fontFamily }) => {
const fontSize = '20px';
const text = document.getElementById('test-text');

const withFont = document.createElement('div');
Object.assign(withFont.style, { fontFamily, fontSize });
withFont.appendChild(document.createTextNode(`plain text in ${fontFamily}: 🦄 🍑 🤣`));

const withSystemUI = document.createElement('div');
Object.assign(withSystemUI.style, { fontFamily: '"system ui"', fontSize });
withSystemUI.appendChild(document.createTextNode(`plain text in "system ui": 🦄 🍑 🤣`));

text.appendChild(withFont);
text.appendChild(withSystemUI);
};

Promise.resolve().then(async () => {
// const fontFamily = null;
const fontFamily = await loadFonts();

const canvas = document.querySelector('#debug-canvas');
const ctx = canvas.getContext('2d');

renderTestText({ fontFamily });

connectTest({ fontFamily, canvas, ctx });

await drawDebugEmoji({ fontFamily, canvas, ctx });
await drawTransformedEmoji({ fontFamily, canvas, ctx, offsetY: 200 });
}).catch(e => {
console.log('something went wrong:', e);
});
</script>
</body>
</html>
14 changes: 10 additions & 4 deletions src/confetti.js
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@
scalar = 1,
color = '#000000',
// see https://nolanlawson.com/2022/04/08/the-struggle-of-using-native-emoji-on-the-web/
fontFamily = '"Twemoji Mozilla", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", "EmojiOne Color", "Android Emoji", "system emoji", sans-serif';
fontFamily = '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", "EmojiOne Color", "Android Emoji", "Twemoji Mozilla", "system emoji", sans-serif';

if (typeof textData === 'string') {
text = textData;
Expand All @@ -829,15 +829,21 @@

ctx.font = font;
var size = ctx.measureText(text);
var width = Math.floor(size.width);
var height = Math.floor(size.fontBoundingBoxAscent + size.fontBoundingBoxDescent);
var width = Math.ceil(size.actualBoundingBoxRight + size.actualBoundingBoxLeft);
var height = Math.ceil(size.actualBoundingBoxAscent + size.actualBoundingBoxDescent);

var padding = 2;
var x = size.actualBoundingBoxLeft + padding;
var y = size.actualBoundingBoxAscent + padding;
width += padding + padding;
height += padding + padding;

canvas = new OffscreenCanvas(width, height);
ctx = canvas.getContext('2d');
ctx.font = font;
ctx.fillStyle = color;

ctx.fillText(text, 0, fontSize);
ctx.fillText(text, x, y);

var scale = 1 / scalar;

Expand Down
4 changes: 2 additions & 2 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -739,8 +739,8 @@ test('[text] shapeFromText renders an emoji', async t => {
...shape
}, {
type: 'bitmap',
matrix: [ 0.1, 0, 0, 0.1, -6.25, -5.8500000000000005 ],
hash: '8647FpWTCBH'
matrix: [ 0.1, 0, 0, 0.1, -5.7, -5.550000000000001 ],
hash: 'c4y5z8b83AC'
});
});

Expand Down

0 comments on commit a17396a

Please sign in to comment.