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.
what i tried:
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:
attempts to set it as background
- apparently fabric.js handles svg differently than images - i can't just set the svg element with
canvas.setBackgroundColor
orcanvas.SetBackgroundImage
. typescript told metypeof SVGSVGElement is not SVGImageElement
, or something along those lines. - i tried using a
fabric.Image
,fabric.Pattern
- also
fabric.loadSVGFromString
=>fabric.util.groupSVGElements(objects, options)
- also creating a
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)}")`
}
present
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:
- still use the old css approach, but use
canvas.on('mouse:move')
->this.viewportTransform
and setbackground-position-x
andbackground-position-y
to current transform % grid size
CodePudding user response:
SOLVED
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:
- when using
inlineSVGString
, instead ofdata: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. - after you set the background color, you have request the canvas to re-render. the callback may look wierd, but its justified:
- just plain old
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. - by using a setTimeout of 0, we can use a kind of hacky strategy to exectute it right after all the other stuff is done. short explanation: js is single-threaded, this schedules the callback at the end of the current event loop. longer answer
- we are not done yet, because using
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 - that's why finally we have arrived to this:
() => setTimeout(() => canvas.requestRenderAll(), 0)
- one small caveat is that if you're initializing the dot matrix in the middle of a function or something, it will get initialized after that. so just ensure you're not doing anything that relies on dot matrix being drawn in the same function as this code. this shouldn't be an issue in the real world though.
- just plain old