zoom by mouse wheel on canvas Angular+D3 - javascript

I want to make zoom on HTML canvas by mouse wheel like on google map.
I have two canvases on my project, one for showing an image and one for edit the image. I do for both of the canvases transformation and scale according to the mouse position.
Everything goes fine. the zoom and the draw after the zoom. the problem is when I try to save the original position of the drawing after the zoom - the position is not correct.
HTML:
<canvas id="backgroundCanvas" #backgroundCanvas class="background-canvas" width="930" height="710"></canvas>
<canvas #editCanvas mouseWheel class="edit-canvas" width="930" height="710" [style.cursor]="editMode ? 'pointer' : '-webkit-grab'" (mouseWheelUp)="mouseWheelUpFunc($event)" (mouseWheelDown)="mouseWheel($event)"></canvas>
JS:
this.canvas1 = d3.select("#editCanvas");
this.context = this.canvas1.node().getContext("2d");
this.width = this.canvas1.property("width");
this.height = this.canvas1.property("height");
this.canvas1.call(d3.zoom()
.scaleExtent([1, 4])
.on("zoom", () => {
if (this.zoomStep === 0) {
this.isZoomed = false; //set isZoomed to false for disable to drag the image
}
var wheel = d3.event.sourceEvent.wheelDelta/120;//n or -n
var zoom = 0;
if(wheel < 0)//if it's zoom out
{
this.zoomStep--;
this.setCurrentScale(this.zoomFactor, false);
this.resetBackgroundTopLeft();
console.log("wheel is under 0 ");
zoom = 1/1.05;
}
else//if it's zoom in
{
this.zoomStep++;
this.isZoomed = true;//set isZoomed to true for enable to drag the image
this.setCurrentScale(this.zoomFactor, true);
zoom = 1.05;
}
this.currentzoom *= zoom; //set the current zoom for know whem to stop
this.clearBackgroundCanvas(); // clear the background image befor draw it again.
/*********************************change the background canvas (where the image is)****************************************************/
this.backgroundContext.save();
this.backgroundContext.translate(
d3.event.transform.x,
d3.event.transform.y
);
this.backgroundContext.scale(d3.event.transform.k, d3.event.transform.k);
this.backgroundContext.drawImage(this.curentImage, this.imageTopLeft.x, this.imageTopLeft.y ,this.img.w * this.currentScale, this.img.h * this.currentScale);
this.backgroundContext.restore();
/***********************************change the edit canvas (where the tags are)**************************************************/
this.editContext.save();
this.editContext.translate(
d3.event.transform.x,
d3.event.transform.y
);
this.editContext.scale(d3.event.transform.k, d3.event.transform.k);
this.draw() //clear and then draw the marker again
this.editContext.restore();
/*************************************************************************************/
}))
.on("mousedown.zoom", () =>{
this.onMouseDown1(d3.event);
})
/**This function set the sacle to zoom In or Out according the mouse wheel*/
setCurrentScale = (scale: number, zoomIn: boolean) => {
this.currentScale *= zoomIn ? scale : 1 / scale;
}
/**This function clear all the canvase area */
clearBackgroundCanvas() {
this.backgroundContext.clearRect(0, 0, this.canvas.w, this.canvas.h);
}
onMouseDown1(evt: MouseEvent) {
var coordinates = d3.mouse(this.canvas1.node());
console.log("onMouseDown" + evt.offsetX , evt.offsetY ,coordinates)
if (this.editMode) {
if (this.isInDrawingArea(evt.offsetX, evt.offsetY, this.imageTopLeft)) {
// return relative mouse position
this.currentDrawnMarkerStart = {
x: coordinates[0],
y: coordinates[1]
};
this.isDrawingRect = true;
if(this.scale == null)
this.scale ={k :1 };
this.drawRectBorder(
this.currentDrawnMarkerStart.x ,
this.currentDrawnMarkerStart.y ,
250*this.currentScale*this.scale.k,
250*this.currentScale*this.scale.k,
'#004de6',2*this.scale.k);//color for the rect tag on drawing
const m = {
x: this.currentDrawnMarkerStart.x,
y: this.currentDrawnMarkerStart.y,
w: 250*this.currentScale,
h: 250*this.currentScale,
isSelected: false,
isHovered: false
};
this.addMarker(m);
}
} else if (!this.isDragging && this.isZoomed) {
this.dragStart = {
x: evt.offsetX,
y: evt.offsetY
};
this.isDragging = true;
}
}
drawRectBorder = (x0: number, y0: number, w: number, h: number, color: string, borderW: number = 1) => {
this.editContext.strokeStyle = color;
this.editContext.lineWidth = borderW;
this.editContext.beginPath();
this.editContext.rect(x0, y0, w, h);
this.editContext.stroke();
this.editContext.closePath();
}
addMarker = (r: IMarker) => {
console.log(this.currentScale)
const original = editorUtil.transformMarkerToOriginal(editorUtil.getMarkerTopLeft(r),
this.imageTopLeft, this.currentScale);
// Save coordinates on canvas
this.currentMarkers.push(original);
console.log(original)
}
/** Get the marker's top left corner, with absolute values for w,h */
export const getMarkerTopLeft = (m: IMarker): IMarker => {
const res: IMarker = {
x: m.x + (m.w < 0 ? m.w : 0),
y: m.y + (m.h < 0 ? m.h : 0),
w: Math.abs(m.w),
h: Math.abs(m.h),
isSelected: m.isSelected,
isHovered: m.isHovered
};
return res;
};
/** Get the marker's coordinates with regards to the original image dimensions */
export const transformMarkerToOriginal = (m: IMarker, imageTopLeft: Coordinate, scale: number): IMarker => {
const res: IMarker = {
x: Math.floor((m.x - imageTopLeft.x) / scale),
y: Math.floor((m.y - imageTopLeft.y) / scale),
w: Math.floor(Math.abs(m.w / scale)),
h: Math.floor(Math.abs(m.h / scale)),
isSelected: false,
isHovered: false
};
return res;
};
Thank's!!

Related

Center and fit svg elements inside parent viewbox

I have two nested svg elements, one outer <svg> with a with a dynamic view box (changed by panning and zooming) and an inner element <g> where I can put any number of svg elements. On one of my pages I want to show some personal information, and an svg family tree. I'm trying to automatically center and fit this family tree inside the svg window. First I thought I would just iterate through all my elements and get their position to find the min/max coordinates, but a much more efficient way would be to calculate the trees bounding box, the bounding box of the <g> element, in the outer svg's view box coordinates.
Now I've tried a few solutions. Just getting a bounding box using getBBox() on the inner element returns a bounding box with seemingly correct width and height, but weird x and y. I don't seem to fully understand what getBBox does. I also tried using inner.getBoundingClientRect() and transforming it to containers coordinates, but end up with the exact same bouding box as returned by getBBox()! I hope you can help me. Here's my vue component.
<template>
<div id="svg-container" class="svg-container">
<svg id="svg-canvas-outer" viewBox="0 0 1000 1000">
<svg id="svg-canvas-inner" :viewBox="viewBox">
<g id="svg-content">
<slot></slot> <!-- <--family tree -->
</g>
</svg>
</svg>
</div>
</template>
<script>
export default {
name: 'SvgContainer',
props: {
width: {
type: Number,
default: 1000,
},
height: {
type: Number,
default: 1000,
},
margin: {
type: Number,
default: 50,
},
},
data() {
return {
showControls: false,
controlsWidthBase: 60,
controlsHeightBase: 120,
viewBoxWidth: this.width,
viewBoxHeight: this.height,
viewBoxScale: 1.0,
viewBoxX: 0,
viewBoxY: 0,
startPoint: null,
endPanX: 0,
endPanY: 0,
currentlyPanning: false,
innerSvgElement: undefined,
mousePos: '',
mouseViewportPos: '',
bbox: '',
};
},
computed: {
viewBox() {
return `${this.viewBoxX} ${this.viewBoxY} ${this.scaledViewBoxWidth} ${this.scaledViewBoxHeight}`;
},
viewBoxPretty() {
return `${this.viewBoxX.toFixed(2)} ${this.viewBoxY.toFixed(2)} ${this.scaledViewBoxWidth.toFixed(2)} ${this.scaledViewBoxHeight.toFixed(2)}`;
},
scaledViewBoxWidth() {
return this.viewBoxWidth * this.viewBoxScale;
},
scaledViewBoxHeight() {
return this.viewBoxHeight * this.viewBoxScale;
},
},
methods: {
zoomIn() {
const scaleFactor = this.viewBoxScale * 0.8;
this.$_SvgContainer_zoom(scaleFactor);
},
zoomOut() {
const scaleFactor = this.viewBoxScale / 0.8;
this.$_SvgContainer_zoom(scaleFactor);
},
$_SvgContainer_zoom(newScale) {
const oldWidth = this.scaledViewBoxWidth;
const oldHeight = this.scaledViewBoxHeight;
this.viewBoxScale = newScale;
const newWidth = this.scaledViewBoxWidth;
const newHeight = this.scaledViewBoxHeight;
this.panX((newWidth - oldWidth) / 2);
this.panY((newHeight - oldHeight) / 2);
},
sceneBoundingBox() {
const outer = document.getElementById('svg-canvas-outer');
const inner = document.getElementById('svg-content');
const {x: bx, y: by, bottom: bb, right: br} = inner.getBoundingClientRect();
const pXY = outer.createSVGPoint();
const pBR = outer.createSVGPoint();
pXY.x = bx;
pXY.y = by;
pBR.x = br;
pBR.y = bb;
const inverseCTM = outer.getScreenCTM().inverse();
const {x, y} = pXY.matrixTransform(inverseCTM);
const {x: r, y: b} = pBR.matrixTransform(inverseCTM);
const bbox = inner.getBBox(); // <-- same result as below :(
return {x: x, y: y, width: (r - x), height: (b - y)};
},
centerAndFitContent() {
const {x: boundingX, y: boundingY, width, height} = this.sceneBoundingBox();
// Pan svg container to fit
const boundingCenterX = boundingX + (width / 2);
const boundingCenterY = boundingY + (height / 2);
const sceneCenterX = this.scaledViewBoxWidth / 2;
const sceneCenterY = this.scaledViewBoxHeight / 2;
this.panX(sceneCenterX - boundingCenterX);
this.panY(sceneCenterY - boundingCenterY);
// Zoom svg container to fit
const widthWithMargin = width + this.margin * 2;
const heightWithMargin = height + this.margin * 2;
while (widthWithMargin > this.scaledViewBoxWidth || heightWithMargin > this.scaledViewBoxHeight) {
this.zoomOut();
}
},
},
mounted() {
this.centerAndFitContent();
},
};
</script>
<style scoped>
</style>

Konva - Change offset position after zoom and rotation

I'm trying to implement a rotation and zoom feature with a slider. I need an image to always rotate from the center of the viewport. The main idea is changing the position and offset of the stage after drag event. Here's what I've tried
const width = window.innerWidth
const height = window.innerHeight
// scale is temp
let stage = new Konva.Stage({
container: 'canvas',
width,
height,
draggable: true,
offset: {
x: width / 2,
y: height / 2
},
x: width / 2,
y: height / 2
})
let mapLayer = new Konva.Layer()
let imageObj = new Image()
imageObj.onload = function () {
let map = new Konva.Image({
image: imageObj,
prevX: 0,
prevY: 0
})
layer.add(map)
stage.add(mapLayer)
stage.on('dragstart', () => {
map.prevX = map.getAbsolutePosition().x
map.prevY = map.getAbsolutePosition().y
})
stage.on('dragend', () => {
let curX = map.getAbsolutePosition().x
let curY = map.getAbsolutePosition().y
let deltaX = Math.abs(map.prevX - curX)
let deltaY = Math.abs(map.prevY - curY)
if (curX > map.prevX) {
stage.offsetX(stage.offsetX() - deltaX)
stage.x(stage.x() - deltaX)
} else {
stage.offsetX(stage.offsetX() + deltaX)
stage.x(stage.x() + deltaX)
}
if (curY > map.prevY) {
stage.offsetY(stage.offsetY() - deltaY)
stage.y(stage.y() - deltaY)
} else {
stage.offsetY(stage.offsetY() + deltaY)
stage.y(stage.y() + deltaY)
}
stage.draw()
})
}
(if you want a full source code, clone it from here and run yarn run dev from terminal, the app lives on localhost:3000
It works fine when the image is in the normal position (not zoomed and rotated yet) but after ANY kind of rotation or zooming, dragging the stage will cause the stage to be re-positioned in a weird fashion (the offset is still correct though). How can I set position and offset correctly?
The solution to your initial issue below.
Note that I put a circle and a tooltip as well to a single layer to be able to drag them as a single one. Also, it should be added after map to have binding work.
import Konva from 'konva'
import map from './../resources/unpar.svg'
const width = window.innerWidth
const height = window.innerHeight
// scale is temp
let stage = new Konva.Stage({
container: 'canvas',
width,
height,
offset: {
x: width / 2,
y: height / 2
},
x: width / 2,
y: height / 2
})
let layer = new Konva.Layer({draggable: true})
let testCircle = new Konva.Circle({
x: 633,
y: 590,
radius: 10,
fill: 'white',
stroke: 'black'
})
testCircle.on('mousemove', function () {
tooltip.position({
x: testCircle.x() - 90,
y: testCircle.y() - 50
})
tooltip.text('Pintu Depan Gedung 10')
tooltip.show()
layer.batchDraw()
})
testCircle.on('mouseout', function () {
tooltip.hide()
layer.draw()
})
var tooltip = new Konva.Text({
text: '',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
textFill: 'white',
fill: 'black',
alpha: 0.75,
visible: false
})
let imageObj = new Image()
imageObj.onload = function () {
const map = new Konva.Image({
image: imageObj,
})
layer.add(map)
layer.add(testCircle)
layer.add(tooltip)
stage.add(layer)
}
imageObj.src = map
export default stage

Javascript canvas hover over a tile

I have a canvas which I am drawing from certain tiles (new Image()), lets say there are for example 10x10 tiles in my canvas.
Every tile has its own data I want to display "popup" with information about this tile everytime user hovers over the tile with mouse, it should be following the mouse until user leaves the tile.
Example of what I want to achieve would be google maps, but it should be following the mouse as I move the mouse over the tile:
https://i.gyazo.com/d32cd5869ae9b2e0d9a7053729e2d2aa.mp4
You will need three things to achieve this:
A way to draw the background image on your canvas
A way to track mouse position over the canvas
A list of "zones" and a way to determine which zone is "triggered"
Here is an implementation of the above points:
var GridOverlay = /** #class */ (function () {
/**
* Creates an instance of GridOverlay.
* #param {string} imageSrc
* #param {{
* position: IPoint,
* size: IPoint,
* message: string
* }[]} [zones=[]]
* #memberof GridOverlay
*/
function GridOverlay(imageSrc, zones) {
if (zones === void 0) { zones = []; }
var _this = this;
this.zones = zones;
/**
* The last registered position of the cursor
*
* #type {{ x: number, y: number }}
* #memberof GridOverlay
*/
this.mousePosition = { x: 0, y: 0 };
//Create an image element
this.img = document.createElement("img");
//Create a canvas element
this.canvas = document.createElement("canvas");
//When the image is loaded
this.img.onload = function () {
//Scale canvas to image
_this.canvas.width = _this.img.naturalWidth;
_this.canvas.height = _this.img.naturalHeight;
//Draw on canvas
_this.draw();
};
//Set the "src" attribute to begin loading
this.img.src = imageSrc;
//Listen for "mousemove" on our canvas
this.canvas.onmousemove = this.mouseMove.bind(this);
}
/**
* Registers the current position of the cursor over the canvas, saves the coordinates and calls "draw"
*
* #param {MouseEvent} evt
* #memberof GridOverlay
*/
GridOverlay.prototype.mouseMove = function (evt) {
this.mousePosition.x = evt.offsetX;
this.mousePosition.y = evt.offsetY;
this.draw();
};
/**
* Clears and redraws the canvas with the latest data
*
* #memberof GridOverlay
*/
GridOverlay.prototype.draw = function () {
//Get drawing context
var ctx = this.canvas.getContext("2d");
//Clear canvas
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
//Draw background
ctx.drawImage(this.img, 0, 0);
//Loop through zones
for (var zoneIndex = 0; zoneIndex < this.zones.length; zoneIndex++) {
var zone = this.zones[zoneIndex];
//Check for cursor in zone
if (zone.position.x <= this.mousePosition.x &&
zone.position.x + zone.size.x >= this.mousePosition.x &&
zone.position.y <= this.mousePosition.y &&
zone.position.y + zone.size.y >= this.mousePosition.y) {
//Display zone message on cursor position
ctx.fillText(zone.message, this.mousePosition.x, this.mousePosition.y);
//Break so that we only show a single message
break;
}
}
return this;
};
return GridOverlay;
}());
//TEST
var grid = new GridOverlay("https://placeholdit.imgix.net/~text?txtsize=60&txt=1&w=500&h=500", [
{ message: "Zone 1", position: { x: 10, y: 10 }, size: { x: 100, y: 100 } },
{ message: "Zone 2", position: { x: 80, y: 80 }, size: { x: 100, y: 100 } },
]);
document.body.appendChild(grid.canvas);

Highcharts manually added svg elements not following stock graph on pan

I have a click event in my highstock / highchart graph, I have successfully added custom drawing tools such as adding lines and text. Here is the code for that
$('#stockchart-canvas-container').on('click','svg',function(e){
var svg = $('#stockchart-canvas-container svg')[0];
var point= svg.createSVGPoint(), svgP
point.x = e.clientX
point.y = e.clientY
svgP = point.matrixTransform(svg.getScreenCTM().inverse());
if(user.selected_tool=='line'){
if(user.previous_x == undefined && user.previous_y == undefined) {
user.current_x = svgP.x
user.current_y = svgP.y
user.previous_x = 0
user.previous_y = 0
$('#stockchart-canvas-container').on('mousemove','svg',function(ev){
var svg2 = $('#stockchart-canvas-container svg')[0];
var point2= svg.createSVGPoint(), svgP2
point2.x = ev.clientX
point2.y = ev.clientY
svgP2 = point2.matrixTransform(svg2.getScreenCTM().inverse());
$('#temp-line').remove()
stockchart.renderer.path(['M',
user.current_x,
user.current_y,
'L',
svgP2.x,
svgP2.y,
'Z',
]).attr({'stroke-width':2,stroke:'#ccc',id:'temp-line'}).add(stockchart.seriesGroup)
})
} else {
$('#stockchart-canvas-container').off('mousemove')
stockchart.renderer.path(['M',
user.current_x,
user.current_y,
'L',
svgP.x,
svgP.y,
'Z'
]).attr({'stroke-width':2,stroke:'#ccc'}).add(stockchart.seriesGroup)
user.current_x=0
user.current_y=0
user.previous_x=undefined
user.previous_y=undefined
}
} else if (user.selected_tool=='text') {
$('#insert-text-modal').modal('show')
$('#accept-insert-text').on('click',function(){
if($('#text-input').val()){
stockchart.renderer.text($('#text-input').val(),svgP.x,svgP.y).add(stockchart.seriesGroup)
}
$(this).off('click')
$('#insert-text-modal').modal('hide')
})
}
})
My problem is that I want the line and the text to follow the stock graph as I pan or zoom the graph. Any ideas how I can do this?
You have to preserve coordinate values at the moment the text/line is drawn - the coordinates in terms of axes. On each chart redraw, you need to reposition the line/text - so you have to calculate new pixel position (which can be calculated via axis.toPixels) and set the new values to the line/text. For a text you need to calculate one point, for a path element you need to recalculate each segment.
See the code below:
Function for calculating pixels from values and values from pixels - it includes some basic logic for hiding a text if it overflows a chart's plot area - but it should be adjusted depending on your needs.
function translate (x, y, chart, toPixels) {
const xAxis = chart.xAxis[0]
const yAxis = chart.yAxis[0]
let tx, ty, hide
if (toPixels) {
tx = xAxis.toPixels(x)
ty = yAxis.toPixels(y)
if (tx < xAxis.left || tx > xAxis.left + xAxis.width) {
hide = true
} else if (!hide && (ty < yAxis.top || ty > yAxis.top + yAxis.height)) {
hide = true
}
if (hide) {
tx = -9e7
ty = -9e7
}
} else {
tx = xAxis.toValue(x)
ty = yAxis.toValue(y)
}
return { x: tx, y: ty }
}
On chart click - it adds the text and keep in the array, on chart redraw r - it repositions items.
chart: {
events: {
load: function () {
this.drawnItems = []
},
click: function (e) {
const { x, y } = e
const text = this.renderer.text('custom text', x, y).add()
text.point = translate(x, y, this)
this.drawnItems.push(text)
},
redraw: function () {
this.drawnItems.forEach(item => {
const { x, y } = item.point
item.attr(translate(x, y, this, true))
})
}
}
},
Live example: http://jsfiddle.net/nsf67ro6/

FabricJS Clipping and SVG Export

I have a small fabricjs scene which loads images and clips them to SVG shapes that are defined in an SVG layout. The clipping is done using the code that is specified in this stack.
I have two issues:
When I export to SVG (see the "export to SVG" button), the clipping does not persist. Is this possible, perhaps to export to SVG maybe by converting the clippings to in SVG?
I have a SVG string inside the let clippingSVG variable. Is it possible to apply this SVG as another clipPath for the complete canvas (including export possibility), or the group of images?
Thanks in advance
This is rough implementation of top of the other answer that propose a toSVG method change to make clip ( fixed ) respected in toSVG.
The non fixed case is harder, i hope this helps.
var img01URL = 'http://fabricjs.com/assets/printio.png';
var img02URL = 'http://fabricjs.com/lib/pug.jpg';
var img03URL = 'http://fabricjs.com/assets/ladybug.png';
var img03URL = 'http://fabricjs.com/assets/ladybug.png';
function toSvg() {
document.getElementById('svg').innerHTML = canvas.toSVG();
}
fabric.Image.prototype.toSVG = function(reviver) {
var markup = this._createBaseSVGMarkup(), x = -this.width / 2, y = -this.height / 2, clipUrl = '';
if (this.clipPath) {
var id = fabric.Object.__uid++;
if (this.clipPath.fixed) {
markup.push('<clipPath id="myClip' + id + '">\n',
this.clipPath.toSVG(reviver),
'</clipPath>\n');
}
clipUrl = ' clip-path="url(#myClip' + id + ')" ';
}
markup.push('<g ', clipUrl, '>',
'<g transform="', this.getSvgTransform(), this.getSvgTransformMatrix(), '"', '>\n',
'\t<image ', this.getSvgId(), 'xlink:href="', this.getSvgSrc(true),
'" x="', x, '" y="', y,
'" style="', this.getSvgStyles(),
// we're essentially moving origin of transformation from top/left corner to the center of the shape
// by wrapping it in container <g> element with actual transformation, then offsetting object to the top/left
// so that object's center aligns with container's left/top
'" width="', this.width,
'" height="', this.height,
'"></image>\n'
);
if (this.stroke || this.strokeDashArray) {
var origFill = this.fill;
this.fill = null;
markup.push(
'<rect ',
'x="', x, '" y="', y,
'" width="', this.width, '" height="', this.height,
'" style="', this.getSvgStyles(),
'"/>\n'
);
this.fill = origFill;
}
markup.push('</g></g>\n');
return reviver ? reviver(markup.join('')) : markup.join('');
};
fabric.Image.prototype._render = function(ctx) {
// custom clip code
if (this.clipPath) {
ctx.save();
if (this.clipPath.fixed) {
var retina = this.canvas.getRetinaScaling();
ctx.setTransform(retina, 0, 0, retina, 0, 0);
// to handle zoom
ctx.transform.apply(ctx, this.canvas.viewportTransform);
//
this.clipPath.transform(ctx);
}
this.clipPath._render(ctx);
ctx.restore();
ctx.clip();
}
// end custom clip code
var x = -this.width / 2, y = -this.height / 2, elementToDraw;
if (this.isMoving === false && this.resizeFilter && this._needsResize()) {
this._lastScaleX = this.scaleX;
this._lastScaleY = this.scaleY;
this.applyResizeFilters();
}
elementToDraw = this._element;
elementToDraw && ctx.drawImage(elementToDraw,
0, 0, this.width, this.height,
x, y, this.width, this.height);
this._stroke(ctx);
this._renderStroke(ctx);
};
var canvas = new fabric.Canvas('c');
canvas.setZoom(0.5)
fabric.Image.fromURL(img01URL, function(oImg) {
oImg.scale(.25);
oImg.left = 10;
oImg.top = 10;
oImg.clipPath = new fabric.Circle({radius: 40, top: 50, left: 50, fixed: true, fill: '', stroke: '' });
canvas.add(oImg);
canvas.renderAll();
});
fabric.Image.fromURL(img02URL, function(oImg) {
oImg.scale(.40);
oImg.left = 180;
oImg.top = 0;
oImg.clipPath = new fabric.Path('M85.6,606.2c-13.2,54.5-3.9,95.7,23.3,130.7c27.2,35-3.1,55.2-25.7,66.1C60.7,814,52.2,821,50.6,836.5c-1.6,15.6,19.5,76.3,29.6,86.4c10.1,10.1,32.7,31.9,47.5,54.5c14.8,22.6,34.2,7.8,34.2,7.8c14,10.9,28,0,28,0c24.9,11.7,39.7-4.7,39.7-4.7c12.4-14.8-14-30.3-14-30.3c-16.3-28.8-28.8-5.4-33.5-11.7s-8.6-7-33.5-35.8c-24.9-28.8,39.7-19.5,62.2-24.9c22.6-5.4,65.4-34.2,65.4-34.2c0,34.2,11.7,28.8,28.8,46.7c17.1,17.9,24.9,29.6,47.5,38.9c22.6,9.3,33.5,7.8,53.7,21c20.2,13.2,62.2,10.9,62.2,10.9c18.7,6.2,36.6,0,36.6,0c45.1,0,26.5-15.6,10.1-36.6c-16.3-21-49-3.1-63.8-13.2c-14.8-10.1-51.4-25.7-70-36.6c-18.7-10.9,0-30.3,0-48.2c0-17.9,14-31.9,14-31.9h72.4c0,0,56-3.9,70.8,26.5c14.8,30.3,37.3,36.6,38.1,52.9c0.8,16.3-13.2,17.9-13.2,17.9c-31.1-8.6-31.9,41.2-31.9,41.2c38.1,50.6,112-21,112-21c85.6-7.8,79.4-133.8,79.4-133.8c17.1-12.4,44.4-45.1,62.2-74.7c17.9-29.6,68.5-52.1,113.6-30.3c45.1,21.8,52.9-14.8,52.9-14.8c15.6,2.3,20.2-17.9,20.2-17.9c20.2-22.6-15.6-28-16.3-84c-0.8-56-47.5-66.1-45.1-82.5c2.3-16.3,49.8-68.5,38.1-63.8c-10.2,4.1-53,25.3-63.7,30.7c-0.4-1.4-1.1-3.4-2.5-6.6c-6.2-14-74.7,30.3-74.7,30.3s-108.5,64.2-129.6,68.9c-21,4.7-18.7-9.3-44.3-7c-25.7,2.3-38.5,4.7-154.1-44.4c-115.6-49-326,29.8-326,29.8s-168.1-267.9-28-383.4C265.8,13,78.4-83.3,32.9,168.8C-12.6,420.9,98.9,551.7,85.6,606.2z',{top: 0, left: 180, fixed: true, fill: '', stroke: '', scaleX: 0.2, scaleY: 0.2 });
canvas.add(oImg);
canvas.renderAll();
});
#c {
border:1px solid #ccc;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.17/fabric.min.js"></script>
<button onClick='toSvg();'>TOSVG</button>
<canvas id="c" width="400" height="400"></canvas>
<div id="svg"></div>
Thank you AndreaBogazzi for starting me off on the right foot. I'd prefer to use a subclass of fabric.Image rather than replace a couple of its prototype methods. Cut-and-paste can be dangerous; in fact, the newest version of fabricjs is already incompatible with the that solution. So, here's a subclass version of the same solution. In my case I'm always using a fixed clipPath so I removed all references to fixed.
const ClippedImage = fabric.util.createClass(fabric.Image, {
type: 'ClippedImage',
initialized: function(options) {
options || (options = {})
this.callSuper('initialize', options)
},
_render: function(ctx) {
if (this.clipPath) {
ctx.save()
var retina = this.canvas.getRetinaScaling()
ctx.setTransform(retina, 0, 0, retina, 0, 0)
ctx.transform.apply(ctx, this.canvas.viewportTransform)
this.clipPath.transform(ctx)
this.clipPath._render(ctx)
ctx.restore()
ctx.clip()
}
this.callSuper('_render', ctx)
},
toSVG: function(reviver) {
let result = this.callSuper('toSVG')
if(this.clipPath) {
const clipId = `Clipped${fabric.Object.__uid++}`
result = `
<clipPath id="${clipId}">
${this.clipPath.toSVG(reviver)}
</clipPath>
<g clip-path="url(#${clipId})">${result}</g>`
}
return reviver ? reviver(result) : result
}
})

Categories