How to install Google Font on nodejs-canvas? - node.js

I'm trying to get Google Fonts dynamically with nodejs-canvas and I'm not sure what is the best way to do that.
I understand it needs to be with: "registerFont" method.
This is the code I have now:
const { registerFont, createCanvas, loadImage } = require('canvas')
//registerFont('comicsans.ttf', { family: 'Comic Sans' })
const canvas = createCanvas(200, 200)
const ctx = canvas.getContext('2d')
let name = req.query.name;
// Write "Awesome!"
ctx.font = '30px Impact'
ctx.rotate(0.1)
ctx.fillText(name, 50, 100)
// Draw line under text
var text = ctx.measureText(name)
ctx.strokeStyle = 'rgba(0,0,0,0.5)'
ctx.beginPath()
ctx.lineTo(50, 102)
ctx.lineTo(50 + text.width, 102)
ctx.stroke()
//console.log(canvas.toDataURL());
res.write('<img style="border:1px solid #000;" src="' + canvas.toDataURL() + '" />');
res.end();

Using PT Mono as an example this is how you do it.
First go to the font directly in this case PT Mono.
Next click the "download family" link in the top right. That will give you a zip file which contains the TTF font.
Now copy the ttf file into your node application directory and use registerFont in your file like this.
registerFont('PtMono-Regular.ttf', { family: 'PT Mono' });
As per the documentation make sure you register your fonts before creating the canvas.
Then to use your font on your canvas you'd just use the following to set the font for the context.
ctx.font = "16px 'PT Mono'";
Full Example using our google font just modifying the original example in the node-canvs repo.
const { registerFont, createCanvas } = require('canvas');
registerFont('PtMono-Regular.ttf', { family: 'PT Mono' });
const canvas = createCanvas(500, 500);
const ctx = canvas.getContext('2d');
ctx.font = '16px 'PT Mono'"'
ctx.fillText('This is the font.', 250, 10);

Related

I can't get the Font size on my canvas to work [duplicate]

Is there a way to only change the font size of a canvas context without having to know/write the font family.
var ctx = document.getElementById("canvas").getContext("2d");
ctx.font = '20px Arial'; //Need to speficy both size and family...
Note:
ctx.fontSize = '12px'; //doesn't exist so won't work...
ctx.style.fontSize = '20 px' //doesn't exist so won't work...
//we are changing the ctx, not the canvas itself
Other note: I could do something like: detect where 'px' is, remove what's before 'px' and replace it by my font size. But I'd like something easier than that if possible.
Here is an easier and cleaner way of changing the font size that will work regardless if you are using font-variant or font-weight or not.
First some RegEx to match and return the current font size
const match = /(?<value>\d+\.?\d*)/;
Set an absolute size, assuming your new font size is 12px
ctx.font = ctx.font.replace(match, 12);
Set a relative size, increase by 5:
const newSize = parseFloat(ctx.font.match(match).groups.value + 5);
ctx.font = ctx.font.replace(match, newSize);
Working example:
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// Match on digit at least once, optional decimal, and optional digits
// Named group `value`
const match = /(?<value>\d+\.?\d*)/;
const setFontSize = (size) => ctx.font = ctx.font.replace(match, size);
const adjustFontSize = (amount) => {
const newSize = parseFloat(ctx.font.match(match).groups.value + amount);
return ctx.font = ctx.font.replace(match, newSize);
}
// Default font is 12px sans-serif
console.log( setFontSize(18) ); // 18px sans-serif
console.log( adjustFontSize(2) ); // 20px sans-serif
console.log( adjustFontSize(-5) ); // 15px sans-serif
<canvas id="canvas"></canvas>
Update: (from comments) There is no way around specifying font. The Canvas' font is modeled after the short-hand version of font in CSS.
However, there is always a font set on the canvas (or a font type) so what you can do is to first extract the current font by using it like this:
var cFont = ctx.font;
Then replace size arguments and set it back (note that there might be a style parameter there as well).
A simple split for the sake of example:
var fontArgs = ctx.font.split(' ');
var newSize = '12px';
ctx.font = newSize + ' ' + fontArgs[fontArgs.length - 1]; /// using the last part
You will need support for style if needed (IIRC it comes first if used).
Notice that font-size is fourth parameter, so this will not work if you will have/not have font-variant(bold,italic,oblique), font-variant(normal, small-caps) and font-weight(bold etc).
A cleaner way to not worry about scraping every other parameter:
canvas.style.font = canvas.getContext("2d").font;
canvas.style.fontSize = newVal + "px";
canvas.getContext("2d").font = canvas.style.font;
Explanation:
As mentioned in the first answer, the canvas context will always have a font. So canvas.getContext("2d").font might return something like "10px sans-serif". To not scrape this string we can use the Canvas DOM element (or any other element) to parse the full font spec and populate the other font fields. Which means that after setting:
canvas.style.font = "10px sans-serif";
canvas.style.fontSize = "26px";
the canvas.style.font will become "26px sans-serif". We can then pass this font spec back to the canvas context.
To correctly answer your question, there is no way to change the font size of a canvas context without having to know/write the font family.
try this (using jquery):
var span = $('<span/>').css('font', context.font).css('visibility', 'hidden');
$('body').append(span);
span[0].style.fontWeight = 'bold';
span[0].style.fontSize = '12px';
//add more style here.
var font = span[0].style.font;
span.remove();
return font;

Fixed SVG Layer Position on Open Layers Map

I have a map with multiple Layers. One of them shall project an SVG on the map, indicating certain regions (if not obvious, it's the red area in the images). I got this working on the default zoom level.
But as soon as I change the zoom in any direction, the SVG layer just moves too much in a direction depending on where I initiate the zoom change.
This is the relevant code I have:
//Map init
var mapcenter = ol.proj.fromLonLat([10.615219, 51.799502]);
var layers = [];
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: mapcenter,
zoom: 9.5
})
});
//SVG init
const svgContainer = document.createElement('div');
const xhr = new XMLHttpRequest();
xhr.open('GET', '/shared/harzregionen.svg');
xhr.addEventListener('load', function () {
const svg = xhr.responseXML.documentElement;
svgContainer.ownerDocument.importNode(svg);
svgContainer.appendChild(svg);
});
xhr.send();
const width = 350;
const height = 0;
const svgResolution = 360 / width;
svgContainer.style.width = width + 'px';
svgContainer.style.height = height + 'px';
svgContainer.className = 'svg-layer';
//SVG Layer
map.addLayer(
new ol.layer.Vector({
render: function (frameState) {
const scale = svgResolution / frameState.viewState.resolution;
const center = frameState.viewState.center;
const size = frameState.size;
const cssTransform = ol.transform.composeCssTransform(
300,
120,
480 / frameState.viewState.resolution,
480 / frameState.viewState.resolution,
frameState.viewState.rotation,
-(center[0] - mapcenter[0]) / 420, //These values were kinda
(center[1] - mapcenter[1]) / 250 //trial and error for default zoom
);
svgContainer.style.transform = cssTransform;
svgContainer.style.opacity = this.getOpacity();
return svgContainer;
}
})
);
I'm pretty sure I have to use a certain formula to calculate the values in composeCssTransform but it feels like there should be an easy way to do it instead of trial and error until it somewhat works.
So, my question is: Is there an easy way to lock an SVG layer to a set position on the world map like the layers with markers?

Text in canvas being wiped out with a video canvas underneath

Note: Minimal working example here. Click on change text button, and you can see the text will appear for one frame and disappear.
I wanted to add an overlay to my video, so I stacked two canvases and filled text to the transparent top canvas.
However, the text does not stick. It disappears.
To test if I was filling the text correctly, I tried using a black canvas (no video) underneath.
I just needed to fillText once and the text stayed.
However, with the video canvas underneath, the text won't stick unless I draw text in requestAnimationFrame, which I believe keeps drawing the same text in every frame, which is unnecessary.
The text canvas is supposed to be separate from the video canvas. Why is this getting wiped out without requestAnimationFrame?
How can I fix it?
Minimal working example here, but the code is also shown below.
import "./styles.css";
import { useEffect, useRef, useState } from "react";
export default function App() {
const inputStreamRef = useRef();
const videoRef = useRef();
const canvasRef = useRef();
const overlayCanvasRef = useRef();
const [text, setText] = useState("Hello");
function drawTextWithBackground() {
const ctx = overlayCanvasRef.current.getContext("2d");
ctx.clearRect(
0,
0,
overlayCanvasRef.current.width,
overlayCanvasRef.current.height
);
/// lets save current state as we make a lot of changes
ctx.save();
/// set font
const font = "50px monospace";
ctx.font = font;
/// draw text from top - makes life easier at the moment
ctx.textBaseline = "top";
/// color for background
ctx.fillStyle = "#FFFFFF";
/// get width of text
const textWidth = ctx.measureText(text).width;
// Just note that this way of "measuring" height is not accurate.
// You can measure height of a font by using a temporary div / span element and
// get the calculated style from that when font and text is set for it.
const textHeight = parseInt(font, 10);
const textXPosition = canvasRef.current.width / 2 - textWidth / 2;
const textYPosition = canvasRef.current.height - textHeight * 3;
ctx.save();
ctx.globalAlpha = 0.4;
/// draw background rect assuming height of font
ctx.fillRect(textXPosition, textYPosition, textWidth, textHeight);
ctx.restore(); // this applies globalAlpha to just this?
/// text color
ctx.fillStyle = "#000";
ctx.fillText(text, textXPosition, textYPosition);
/// restore original state
ctx.restore();
}
function updateCanvas() {
const ctx = canvasRef.current.getContext("2d");
ctx.drawImage(
videoRef.current,
0,
0,
videoRef.current.videoWidth,
videoRef.current.videoHeight
);
// QUESTION: Now the text won't stick unless I uncomment this below,
// which runs drawTextWithBackground in requestAnimationFrame.
// Previosuly, with just a black canvas underneath, I just needed to fillText once
// and the text stayed.
// The text canvas is supposed to be separate. Why is this getting wiped out without requestAnimationFrame?
// drawTextWithBackground();
requestAnimationFrame(updateCanvas);
}
const CAMERA_CONSTRAINTS = {
audio: true,
video: {
// the best (4k) resolution from camera
width: 4096,
height: 2160
}
};
const enableCamera = async () => {
inputStreamRef.current = await navigator.mediaDevices.getUserMedia(
CAMERA_CONSTRAINTS
);
videoRef.current.srcObject = inputStreamRef.current;
await videoRef.current.play();
// We need to set the canvas height/width to match the video element.
canvasRef.current.height = videoRef.current.videoHeight;
canvasRef.current.width = videoRef.current.videoWidth;
overlayCanvasRef.current.height = videoRef.current.videoHeight;
overlayCanvasRef.current.width = videoRef.current.videoWidth;
requestAnimationFrame(updateCanvas);
};
useEffect(() => {
enableCamera();
});
return (
<div className="App">
<video
ref={videoRef}
style={{ visibility: "hidden", position: "absolute" }}
/>
<div style={{ position: "relative", width: "90vw" }}>
<canvas
style={{ width: "100%", top: 0, left: 0, position: "static" }}
ref={canvasRef}
width="500"
height="400"
/>
<canvas
style={{
width: "100%",
top: 0,
left: 0,
position: "absolute"
}}
ref={overlayCanvasRef}
width="500"
height="400"
/>
</div>
<button
onClick={() => {
setText(text + "c");
drawTextWithBackground();
}}
>
change text
</button>
</div>
);
}
Very simple fix
The canvas is being cleared due to how React updates state.
The fix
The simplest fix is to use just the one canvas and draw the text onto the canvas used to display the video.
From the working example change the two functions updateCanvas and drawTextWithBackground as follows.
Note I have removed all the state saving in drawTextWithBackground as the canvas state is lost due to react anyway so why use the very expensive state stack functions.
function drawTextWithBackground(ctx) {
const can = ctx.canvas;
const textH = 50;
ctx.font = textH + "px monospace";
ctx.textBaseline = "top";
ctx.textAlign = "center";
ctx.fillStyle = "#FFFFFF66"; // alpha 0.4 as part of fillstyle
ctx.fillStyle = "#000";
const [W, X, Y] = [ctx.measureText(text).width, can.width, can.height - textH * 3];
ctx.fillRect(X - W / 2, Y, W, textH);
ctx.fillText(text, X, Y);
}
function updateCanvas() {
const vid = videoRef.current;
const ctx = canvasRef.current.getContext("2d");
ctx.drawImage(vid, 0, 0, vid.videoWidth, vid.videoHeight);
drawTextWithBackground(ctx);
requestAnimationFrame(updateCanvas);
}
when you update the canvas from the video, all of the pixels in the canvas are updated with pixels from the video. Think of it like a wall:
you painted "here's my text" on the wall.
Then you painted the whole wall blue. (your text is gone, and the wall is just blue).
If you paint the whole wall blue, and then repaint "here's my text", you'd still see your text on the blue wall.
When you redraw the canvas, you have to re-draw everything you want to appear on the canvas. If the video "covers" your text, you've got to redraw the text.
working example at https://github.com/dougsillars/chyronavideo which draws a chyron (the bar at the bottom of a news story) on each frame of the canvas.

Threejs - Best way to render image texture only in a portion of the mesh

I have the .obj of a T-Shirt, it contains a few meshes and materials and I'm coloring it using a CanvasTexture fed by an inline svg.
Now I should add a logo at a specific location (more or less above the heart), but I'm struggling to understand which is the best/proper way of doing it (I'm quite new to 3D graphics and Three.js). This is what I tried so far:
since I'm coloring the T-Shirt through a CanvasTexture fed by an inline svg, I thought it would have been easy to just draw the logo into the svg at specific coordinates. And it was easy indeed, but the logo gets not rendered (or is not visible in some way) on the texture/mesh, although it is visible in the inline svg. So CanvasTexture probably doesn't work with embedded images (I tried both base64 and URL)
so, I started looking into more 3d "native" ways of doing it, but I haven't found one that really makes sense to me. I know there's ShaderMaterial in threejs, which I could use to selectively render pixels of the logo or pixels of the cloth, but that means making a lot of complex computation to figure out where the logo should be and I can't believe drawing a simple JPEG or PNG with specific coordinates and size can be so complex... I must have missed an obvious solution.
EDIT
Here is how I'm adding the image to the inline svg (option 1 above).
Add the image to the inline svg
const groups = Array.from(svg.querySelectorAll('g'));
// this is the "g" tag where I want to add the logo into
const targetGroup = groups.find((group: SVGGElement) => group.getAttribute('id') === "logo_placeholder");
const image = document.createElement('image');
image.setAttribute('width', '64');
image.setAttribute('height', '64');
image.setAttribute('x', '240');
image.setAttribute('y', '512');
image.setAttribute('xlink:href', `data:image/png;base64,${base64}`);
targetGroup.appendChild(image);
Draw inline svg to 2d canvas
static drawSvgToCanvas = async (canvas: HTMLCanvasElement, canvasSize: TSize, svgString: string) => {
return new Promise((resolve, reject) =>
canvas.width = canvasSize.width;
canvas.height = canvasSize.height;
const ctx = canvas.getContext('2d');
const image = new Image(); // eslint-disable-line no-undef
image.src = `data:image/svg+xml;base64,${btoa(svgString)}`;
image.onload = () => {
if (ctx) {
ctx.drawImage(image, 0, 0);
resolve();
} else {
reject(new Error('2D context is not set on canvas'));
}
};
image.onerror = () => {
reject(new Error('Could not load svg image'));
}
});
};
Draw 2d canvas to threejs Texture
const texture = new Three.CanvasTexture(canvas);
texture.mapping = Three.UVMapping; // it's the default
texture.wrapS = Three.RepeatWrapping;
texture.wrapT = Three.RepeatWrapping; // it's the default
texture.magFilter = Three.LinearFilter; // it's the default
texture.minFilter = Three.LinearFilter;
texture.needsUpdate = true;
[...add texture to material...]
For some reason, canvases don't like SVGs with embedded images, so for a similar project I had to do this in two steps, rendering the SVG and the image separately:
First, render the SVG on the canvas, and then render the image on top of that (on the same canvas).
const ctx = canvas.getContext('2d');
ctx.drawImage(imgSVG, 0, 0);
ctx.drawImage(img2, 130, 10, 65, 90);
const texture = new THREE.CanvasTexture(canvas);
Example: https://jsfiddle.net/0f9hm7gx/

Overlaying png onto SVG using Sharp?

I've got a set of png files, that are 4500x5400.
What I am trying to do is the following:
Draw a circle that is 485x485 at 300dpi
Overlay the png inside the circle, so that its resized (scaled)
I've been messing around all night but I'm not getting very far:
I've got the code for my circle:
'<svg height="485" width="485"><circle cx="242.5" cy="242.5" r="242.5" fill="#3a4458"/></svg>'
and then some resize code that resizes my png, and masks it.
sharp(`${toConvert}/${file}`)
.trim()
.resize(485, 485)
.embed()
.overlayWith('overlay.png', { cutout: true } )
.toFile(`./converted/${file}-pop.png`)
.catch(err => {
console.log(err)
})
Does anyone know how I can combine the 2 so I can get a coloured circle with my png in it?
For reference, Sharp is an image processing library: https://github.com/lovell/sharp
This is from my project that has a circle cover pages,
First creating a circle SVG with the help of SVG.js you can use background property to get the colored circle.
const window = require('svgdom');
const SVG = require('svg.js')(window);
const document = window.document;
const ShapeUtil = {
drawCircle: function (width, height) {
return new Promise(function (resolve, reject) {
var circleDraw = SVG(document.documentElement).size(width, height);
circleDraw.clear();
circleDraw.viewbox(0, 0, width, height);
circleDraw.circle(width).cx(Math.abs(width / 2)).cy(Math.abs(width / 2));
resolve(circleDraw.svg());
});
}
};
Then overlay created SVG dom to cropped image with Sharp.io :
ShapeUtil.drawCircle( width, height)
.then(function (resultSVG) {
sharp(croppedImage)
.overlayWith(new Buffer(resultSVG), {cutout: true})
.png().toBuffer();
}
For anyone reading this in 2022, svg.js has changed a bit. I'm using Typescript and this is what I did to get it working (based off of #erhanasikoglu's answer):
const { createSVGWindow } = require('svgdom');
const window = createSVGWindow();
import { registerWindow, SVG } from '#svgdotjs/svg.js';
registerWindow(window, window.document);
export const ShapeUtil = {
drawCircle(width: number, height: number) {
var circleDraw = SVG().size(width, height);
circleDraw.clear();
circleDraw.viewbox(0, 0, width, height);
circleDraw
.circle(width)
.cx(Math.abs(width / 2))
.cy(Math.abs(width / 2));
return circleDraw.svg();
},
};
Note that #svgdotjs/svg.js is now the latest package for svgdotjs. It also has typings which is nice. :D

Resources