Home > Mobile >  fabric.js canvas repeating background with dynamic SVG
fabric.js canvas repeating background with dynamic SVG

Time:02-05

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 or canvas.SetBackgroundImage. typescript told me typeof 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 set background-position-x and background-position-y to current transform % grid size

CodePudding user response:

SOLVED

result 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 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.
  • 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.
  • Related