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
}
})
Related
I'd like to make fabricJS objects act like buttons. That is, clicking the object performs an action rather than selecting the object. I saw how this has been done in the example at: http://fabricjs.com/custom-control-render. However, A drag starting in the control is treated as a click. I made a version of this demo in which a click is registered if the mouse does not move between mouseDown and mouseUp events. But, the mouse can be held down and released and it is still treated like a click.
Is there a better/proper way to do this? Ideally when creating the object I'd like to define a clickHandler which replaces the default 'select' behaviour. Or, assign 'select' to Shift-Click or other action.
Here is my modified code for the CodePen example on the demo page above.
var canvas = this.__canvas = new fabric.Canvas('c');
// create a rect object
var deleteIcon = "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E";
var cloneIcon = "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='iso-8859-1'%3F%3E%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 55.699 55.699' width='100px' height='100px' xml:space='preserve'%3E%3Cpath style='fill:%23010002;' d='M51.51,18.001c-0.006-0.085-0.022-0.167-0.05-0.248c-0.012-0.034-0.02-0.067-0.035-0.1 c-0.049-0.106-0.109-0.206-0.194-0.291v-0.001l0,0c0,0-0.001-0.001-0.001-0.002L34.161,0.293c-0.086-0.087-0.188-0.148-0.295-0.197 c-0.027-0.013-0.057-0.02-0.086-0.03c-0.086-0.029-0.174-0.048-0.265-0.053C33.494,0.011,33.475,0,33.453,0H22.177 c-3.678,0-6.669,2.992-6.669,6.67v1.674h-4.663c-3.678,0-6.67,2.992-6.67,6.67V49.03c0,3.678,2.992,6.669,6.67,6.669h22.677 c3.677,0,6.669-2.991,6.669-6.669v-1.675h4.664c3.678,0,6.669-2.991,6.669-6.669V18.069C51.524,18.045,51.512,18.025,51.51,18.001z M34.454,3.414l13.655,13.655h-8.985c-2.575,0-4.67-2.095-4.67-4.67V3.414z M38.191,49.029c0,2.574-2.095,4.669-4.669,4.669H10.845 c-2.575,0-4.67-2.095-4.67-4.669V15.014c0-2.575,2.095-4.67,4.67-4.67h5.663h4.614v10.399c0,3.678,2.991,6.669,6.668,6.669h10.4 v18.942L38.191,49.029L38.191,49.029z M36.777,25.412h-8.986c-2.574,0-4.668-2.094-4.668-4.669v-8.985L36.777,25.412z M44.855,45.355h-4.664V26.412c0-0.023-0.012-0.044-0.014-0.067c-0.006-0.085-0.021-0.167-0.049-0.249 c-0.012-0.033-0.021-0.066-0.036-0.1c-0.048-0.105-0.109-0.205-0.194-0.29l0,0l0,0c0-0.001-0.001-0.002-0.001-0.002L22.829,8.637 c-0.087-0.086-0.188-0.147-0.295-0.196c-0.029-0.013-0.058-0.021-0.088-0.031c-0.086-0.03-0.172-0.048-0.263-0.053 c-0.021-0.002-0.04-0.013-0.062-0.013h-4.614V6.67c0-2.575,2.095-4.67,4.669-4.67h10.277v10.4c0,3.678,2.992,6.67,6.67,6.67h10.399 v21.616C49.524,43.26,47.429,45.355,44.855,45.355z'/%3E%3C/svg%3E%0A"
var deleteImg = document.createElement('img');
deleteImg.src = deleteIcon;
var cloneImg = document.createElement('img');
cloneImg.src = cloneIcon;
fabric.Object.prototype.transparentCorners = false;
fabric.Object.prototype.cornerColor = 'blue';
fabric.Object.prototype.cornerStyle = 'circle';
function Add() {
var rect = new fabric.Rect({
left: 100,
top: 50,
fill: 'yellow',
width: 200,
height: 100,
objectCaching: false,
stroke: 'lightgreen',
strokeWidth: 4,
});
canvas.add(rect);
canvas.setActiveObject(rect);
}
function renderIcon(icon) {
return function renderIcon(ctx, left, top, styleOverride, fabricObject) {
var size = this.cornerSize;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(icon, -size / 2, -size / 2, size, size);
ctx.restore();
}
}
fabric.Object.prototype.controls.deleteControl = new fabric.Control({
x: 0.5,
y: -0.5,
offsetY: -16,
offsetX: 16,
cursorStyle: 'pointer',
mouseUpHandler: deleteObject,
mouseDownHandler: markObject,
render: renderIcon(deleteImg),
cornerSize: 24
});
fabric.Object.prototype.controls.clone = new fabric.Control({
x: -0.5,
y: -0.5,
offsetY: -16,
offsetX: -16,
cursorStyle: 'pointer',
mouseUpHandler: cloneObject,
mouseDownHandler: markObject,
render: renderIcon(cloneImg),
cornerSize: 24
});
Add();
var mX = 0
var mY = 0
function cloneObject(eventData, transform, x, y) {
var target = transform.target;
if (mX == x && mY == y) {
var canvas = target.canvas;
target.clone(function (cloned) {
cloned.left += 10;
cloned.top += 10;
canvas.add(cloned);
});
}
}
function markObject(eventData, transform, x, y) {
mX = x
mY = y
}
function deleteObject(eventData, transform, x, y) {
var target = transform.target;
if (mX == x && mY == y) {
var canvas = target.canvas;
canvas.remove(target);
canvas.requestRenderAll();
}
}
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>
I'm looking for a way to zoom from place to place on a globe in D3 v4 (v4 is important). What I'm looking for is basically exactly this: https://www.jasondavies.com/maps/zoom/ My problem is that Jason Davies obfuscated his code, so I can't read it, and I can't find a bl.ock containing that project or anything similar to it. I'll provide a link to what I've got here: http://plnkr.co/edit/0mjyR3ovTfkDXB8FTG0j?p=preview
The relevant is probably inside the .tween():
.tween("rotate", function () {
var p = d3.geoCentroid(points[i]),
r = d3.geoInterpolate(projection.rotate(), [-p[0], -p[1]]);
return function (t) {
projection.rotate(r(t));
convertedLongLats = [projection(points[0].coordinates), projection(points[1].coordinates)]
c.clearRect(0, 0, width, height);
c.fillStyle = colorGlobe, c.beginPath(), path(sphere), c.fill();
c.fillStyle = colorLand, c.beginPath(), path(land), c.fill();
for (var j = 0; j < points.length; j++) {
var textCoords = projection(points[j].coordinates);
c.fillStyle = textColors, c.textAlign = "center", c.font = "18px FontAwesome", c.fillText(points[j].icon, textCoords[0], textCoords[1]);
textCoords[0] += 15;
c.textAlign = "left", c.font = " 12px Roboto", c.fillText(points[j].location, textCoords[0], textCoords[1]);
}
c.strokeStyle = textColors, c.lineWidth = 4, c.setLineDash([10, 10]), c.beginPath(), c.moveTo(convertedLongLats[0][0], convertedLongLats[0][1]), c.lineTo(convertedLongLats[1][0], convertedLongLats[1][1]), c.stroke();
};
})
Basically, I want what I've got now but I want it to zoom in, pretty much exactly like it is in the Animated World Zoom example I provided above. I'm not really looking for code, I'd rather someone point me in the right direction with an example or something (it's worth noting that I'm fairly new to d3 and that this project is heavily based on World Tour by mbostock, so it uses canvas). Thank you all in advance!
Based on your plunker and comment, a challenge in zooming out between two points in a transition is that the interpolator will only interpolate between two values. The solution in your plunker relies on two interpolators, one for zooming in and zooming out. This method has added un-needed complexity and somewhere along the line, as you note, it jumps to an incorrect scale. You could simplify this:
Take an interpolator that interpolates between -1 and 1, and weight each scale according to the absolute value of the interpolator. At zero, the zoom should be out all the way, while at -1,1, you should be zoomed in:
var s = d3.interpolate(-1,1);
// get the appropriate scale:
scale = Math.abs(0-s(t))*startEndScale + (1-Mat.abs(0-s(t)))*middleScale
This is a little clunky as it goes from zooming out to zooming in rather abruptly, so you could ease it with a sine type easing:
var s = d3.interpolate(0.0000001,Math.PI);
// get the appropriate scale:
scale = (1-Math.abs(Math.sin(s(t))))*startEndScale + Math.abs(Math.sin(s(t)))*middleScale
I've applied this to your plunker here.
For a simple and minimal example using the example that I suggested and your two points and path (and using your plunkr as a base), stripping out the animated line and icons, I would probably put together something like (plunker, snippet below best viewed on full screen):
var width = 600,
height = 600;
var points = [{
type: "Point",
coordinates: [-74.2582011, 40.7058316],
location: "Your Location",
icon: "\uF015"
}, {
type: "Point",
coordinates: [34.8887969, 32.4406351],
location: "Caribe Royale Orlando",
icon: "\uF236"
}];
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var c = canvas.node().getContext("2d");
var point = points[0].coordinates;
var projection = d3.geoOrthographic()
.translate([width / 2, height / 2])
.scale(width / 2)
.clipAngle(90)
.precision(0.6)
.rotate([-point[0], -point[1]]);
var path = d3.geoPath()
.projection(projection)
.context(c);
var colorLand = "#4d4f51",
colorGlobe = "#2e3133",
textColors = "#fff";
d3.json("https://unpkg.com/world-atlas#1/world/110m.json", function (error, world) {
if (error) throw error;
var sphere = { type: "Sphere" };
var land = topojson.feature(world, world.objects.land);
var i = 0;
var scaleMiddle = width/2;
var scaleStartEnd = width * 2;
loop();
function loop() {
d3.transition()
.tween("rotate",function() {
var flightPath = {
type: 'Feature',
geometry: {
type: "LineString",
coordinates: [points[i++%2].coordinates, points[i%2].coordinates]
}
};
// next point:
var p = points[i%2].coordinates;
// current rotation:
var currentRotation = projection.rotate();
// next rotation:
var nextRotation = projection.rotate([-p[0],-p[1]]).rotate();
// Interpolaters:
var r = d3.geoInterpolate(currentRotation,nextRotation);
var s = d3.interpolate(0.0000001,Math.PI);
return function(t) {
// apply interpolated values
projection.rotate(r(t))
.scale( (1-Math.abs(Math.sin(s(t))))*scaleStartEnd + Math.abs(Math.sin(s(t)))*scaleMiddle ) ;
c.clearRect(0, 0, width, height);
c.fillStyle = colorGlobe, c.beginPath(), path(sphere), c.fill();
c.fillStyle = colorLand, c.beginPath(), path(land), c.fill();
c.beginPath(), path(flightPath), c.globalAlpha = 0.5, c.shadowColor = "#fff", c.shadowBlur = 5, c.lineWidth = 0.5, c.strokeStyle = "#fff", c.stroke(), c.shadowBlur = 0, c.globalAlpha = 1;
}
})
.duration(3000)
.on("end", function() { loop(); })
}
});
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
I want to draw image sprite using canvas.
The code not working. How to improve my code.
I have some Error.
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var Game = {
draw_image: function(img, sourceX, sourceY, sourceW, sourceH, destX, destY, destW, destH){
var img = new Image(); // Create new img element
img.src = 'images/background.png'; // Set source path
img.onload = function(){
canvas.width = 1000;
canvas.height = 500;
ctx.drawImage(img, sourceX, sourceY, sourceW, sourceH, destX, destY, destW, destH);
};
}
var BACKGROUND = {
image_top: { x: 5, y: 5, w: 1280, h: 480 , dx:0 ,dy:0 ,dw:500 ,dh:500 },
image_body: { x: 5, y: 495, w: 1280, h: 480 , dx:0 ,dy:150 ,dw:500 ,dh:350},
image_bottom: { x: 5, y: 985, w: 1280, h: 480 , dx:0 ,dy:300 ,dw:500 ,dh:200 }
};
for(var n = 0 ; n < BACKGROUND.length ; n++) {
draw_image(nameImage, BACKGROUND[n].x,BACKGROUND[n].y, BACKGROUND[n].w, BACKGROUND[n].h, BACKGROUND[n].dx, BACKGROUND[n].dy, BACKGROUND[n].dw, BACKGROUND[n].dh );
}
};
To create a sprite animation it's important to know how it works.
You need your spritesheet make with pixel precision ( 1 pixel can mess up your animation ).
Like here, the character is always in the same size area, make it simple when you make your sprites.
With this you can make an object for each sprite you have like :
function Sprite(_position, _numberFrame, _framesize, _image, _duration){
this.position = _position; //Array like { x : 0, y : 0 }
this.rendersize = _rendersize; //Array like { width : 50, height : 80 }
this.framesize = _framesize; //Array like { width : 50, height : 80 }
this.image = _image; //Image object
this.chrono = new Chrono(_duration); //Explanation below
}
For more animation precision you can add a chrono who will manage the time of your animation :
function Chrono(_duration){
this.currentTime = 0;
this.lastTime = 0;
this.timeElapse = 0;
this.duration = _duration;
}
Chrono.prototype.countTime = function(){
this.currentTime = Date.now();
if(this.lastTime != 0)
this.timeElapse += this.currentTime - this.lastTime;
this.lastTime = Date.now();
if(this.timeElpase >= this.duration && this.lastTime != 0){
this.timeElapse = 0;
return TRUE;
} else {
return FALSE;
}
}
Then the function to animate your sprite may like :
Sprite.prototype.render = function(){
if(this.position.x <= this.image.width && this.chrono.countTime()){
this.position.x += this.framesize.x;
} else {
this.position.x = 0;
}
ctx.drawImage(this.image,
this.position.x, this.position.y,
this.framesize.width, this.framesize.height,
this.rendersize.width, this.rendersize.height
);
}
I hope I was clear and helpful,
Cheers
PS: Comments for question or optimisation ideas
You have to many problems with this code to make this sprite animation works. I wouldn't go to point any of the problems with your code, but I highly recommend to read a little bit about functions and variable scope before try to write this kind of code.
Another simple (and best for newbies) solution can be to use a canvas framework as EaselJS, with this you can do something like this to animate an sprite:
var data = {
images: ["images/background.png"],
frames: {width:50, height:50},
animations: {run:[0,4], jump:[5,8,"run"]}
};
var animation = new createjs.BitmapAnimation(data);
animation.gotoAndPlay("run");
I am trying to draw an isometric square with some custom code to build the pixel data up and then put it on a canvas with putImageData.
But I'm not getting the expected results, after a quick look through the raw pixel data of the canvas it seems all the pixel data I built up is getting messed with.
What I want from the below code is a red diamond on a black background. Where am i going wrong?
var Drawing = {};
Drawing.DrawIsoMetricSquare = function(cRenderContext, x, y, iWidth, cColor) {
var iHeight = iWidth / 2;
var iYPos = Math.floor(iHeight / 2) + 1;
var iXPos = 0;
var iRenderGirth = 1;
cPixelData = cRenderContext.createImageData(iWidth, iHeight);
var bExpand = true;
while (iXPos != iWidth) {
var iCurrentRenderGirth = 0;
while (iCurrentRenderGirth != iRenderGirth) {
var iY = iYPos + iCurrentRenderGirth;
//Draw first pixel then second
Drawing.ColorPixelAtPos(cPixelData.data, iXPos, iY, iWidth, cColor);
Drawing.ColorPixelAtPos(cPixelData.data, iXPos + 1, iY, iWidth, cColor);
iCurrentRenderGirth++;
}
//Move to next Render Start
iYPos = bExpand ? (iYPos - 1) : (iYPos + 1);
iXPos += 2;
iRenderGirth = bExpand ? (iRenderGirth + 2) : (iRenderGirth - 2);
bExpand &= iRenderGirth < iHeight;
}
cRenderContext.putImageData(cPixelData, x, y);
};
Drawing.XYPosToPixelPos = function(x, y, iWidth) {
return (x + y * iWidth) * 4;
};
Drawing.ColorPixelAtPos = function(cPixelData, x, y, iWidth, cColor) {
var iPixPos = Drawing.XYPosToPixelPos(x, y, iWidth);
cPixelData[iPixPos++] = cColor.r;
cPixelData[iPixPos++] = cColor.g;
cPixelData[iPixPos++] = cColor.b;
cPixelData[iPixPos] = 1; //Fixed alpha for now
};
var eCanvas = $("<canvas></canvas>");
eCanvas[0].width = 50;
eCanvas[0].height = 50;
$("#render").append(eCanvas);
var cRenderContext = eCanvas[0].getContext('2d');
cRenderContext.fillStyle = "rgba(1, 1, 1, 1)";
cRenderContext.fillRect(0, 0, 50, 50);
Drawing.DrawIsoMetricSquare(cRenderContext, 0, 0, 42, {
r: 255,
g: 0,
b: 0
});
JSFiddle example here
The problems were
a math error which you already fixed.
RGBA specifiers go from 0..255 not 0..1
You need to fill your pixel data with black opaque pixels (they are white by default).
I added a few lines of code and made a new fiddle for you.
http://jsfiddle.net/bsssq/1/
Now you should see the red diamond on the black square.