Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixing issues with Firefox, rendering text inside tight box #214

Merged
merged 10 commits into from
Nov 24, 2023
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