I am creating a pannable, zoomable, infinite grid editor with fabric.js, typescript and vite. Since i implemented snapping to grid, it would be good to have a visual aid of where the grid actually is. I started with just simply making the grid (or field of dots, because that is nicer), with css, like this:
:root {
--dot-opacity: 40%; /* 22.5% */
--dot-color: hsla(0, 0%, 35%, var(--dot-opacity));
--dot-spacing: 16px;
--dot-size: 2px;
--main-bg: #19191b;
}
and added this to a wrapper element.
background-color: var(--main-bg);
background-repeat: repeat; */
background-image: radial-gradient(circle, var(--dot-color) var(--dot-size), transparent var(--dot-size));
background-size: var(--dot-spacing) var(--dot-spacing);
background-attachment: local;
background-position-x: calc(var(--dot-spacing) / 2) ;
background-position-y: calc(var(--dot-spacing) / 2) ; */
this works pretty well, but once you want to implement panning through fabric.js canvas.viewportTransform
, the dots no longer align.
I still want to retain the dynamic/parametric nature of the grid like i have now, but somehow use fabric.js to set the canvas background to a dot matrix that repeats and also pans with the canvas viewport-transform.
i made a svg with 4 circles, one in each corner, that only show 1/4 of each circle in the viewbox - a tile. there are 2 ways how i attempted to generate them, and both make valid svg:
const circlePositions = [[size, 0], [0, 0], [size, size], [0, size]]
const circleStyle = `fill:#ffffff;stroke:#9d5867;stroke-width:0;`
first attempt: through document.createElementNS
: (assingAttributesSVG just loops over Object.entries and uses svg.setAttribute)
const tileSvgElem = document.createElementNS("http://www.w3.org/2000/svg", "svg")
assignAttributesSVG(tileSvgElem, { width: size, height: size, viewBox: `0 0 ${size} ${size}`, version: "1.1" })
tileSvgElem.innerHTML = `<defs/><g>${
circlePositions
.map(([cx, cy]) => `<circle style="${circleStyle}" cx="${cx}" cy="${cy}" r="${r}"/>`)
.join("\n")
}</g></svg>`
second attempt: through plain string:
const tileSvgString = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" version="1.1" xmlns="http://www.w3.org/2000/svg"><defs/><g>
${circlePositions.map(([cx, cy]) => `<circle style="${circleStyle}" cx="${cx}" cy="${cy}" r="${r}"/>`).join("\n")}
</g></svg>`
both of these do what they are expected, but i have trouble setting them as canvas background:
canvas.setBackgroundColor
or canvas.SetBackgroundImage
. typescript told me typeof SVGSVGElement is not SVGImageElement
, or something along those lines.fabric.Image
, fabric.Pattern
fabric.loadSVGFromString
=> fabric.util.groupSVGElements(objects, options)
fabric.StaticCanvas
with .getElement()
i also tried inlining the svg as a data:svg
, with and without the url("")
:
function inlineSVGString(svgString: string) {
return `data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`
}
function urlSVGString(svgString: string) {
return `url("${inlineSVGString(svgString)}")`
}
after all of this, i still cannot get a repeating dot-matrix/field of dots background for the fabric.js canvas. how do i do this?
there is probably some pretty obvious way to do this that i'm missing, i just picked up fabric.js a few days ago, but the docs are not that great, because they are autogenerated from JSDoc so im kind of relying on demos, tutorial, codepen examples and the typescript defs for fabric.js
other ideas:
canvas.on('mouse:move')
-> this.viewportTransform
and set background-position-x
and background-position-y
to current transform % grid size
used the string attempt to set up a svg for the dots.
wierdly, you can use
canvas.setBackgroundColor
to set a data:image.
const circlePositions = [[size, 0], [0, 0], [size, size], [0, size]]
const circleStyle = `fill:${getDotColor()};stroke:#9d5867;stroke-width:0;`
const tileSvgString = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" version="1.1" xmlns="http://www.w3.org/2000/svg"><defs/><g>
${circlePositions.map(([cx, cy]) => `<circle style="${circleStyle}" cx="${cx}" cy="${cy}" r="${r}"/>`).join("\n")}
</g></svg>`
canvasBgCallback = () => setTimeout(() => canvas.requestRenderAll(), 0)
//@ts-ignore
canvas.setBackgroundColor({source: inlineSVGString(tileSvgString)}, canvasBgCallback)
the two main issues that were causing it:
inlineSVGString
, instead of data:image/svg+xml,<data>
i had it set to use utf-8, like so: data:image/svg+xml,utf8,
which does not work. data:image/svg+xml;utf8,
works sometimes but is unreliable.canvas.requestRenderAll
as the callback works, but it only renders the background once you click the canvas. this is not ideal. same thing happends with a () => canvas.requestRenderAll()
or any other callback.setTimeout(() => canvas.requestRenderAll(), 0)
as a callback does not register that function to be run, it evaluates it immediately and setTimeout does not return a function, only the id of the timeout, resulting in an error() => setTimeout(() => canvas.requestRenderAll(), 0)