Related
I'm using the Pixi.js v4 graphics library to make a game with JavaScript. I know that I can draw a black + rounded rectangle like so:
const rectangle = new pixi.Graphics();
rectangle.beginFill(0); // Color it black
rectangle.drawRoundedRect(
0,
0,
100, // Make it 100x100
100,
5, // Make the rounded corners have a radius of 5
);
rectangle.endFill();
stage.addChild(rectangle);
How do I draw a rounded rectangle with a gradient from white to black?
How do I draw a rounded rectangle that has gradual opacity such that it fades in from left to right?
It looks like it's not possible to implement what you need with pixi.js without additional code, but we can do some magic to make it happen. Here's the result of what I've got: https://jsfiddle.net/exkf3zfo/21/
The bottom color is a pure red with 0.2 alpha.
I would split the whole process to the next steps:
Drawing the gradient
Masking the gradient with the rounded mask
Here is the code itself:
var app = new PIXI.Application(800, 600, {
antialias: true
});
document.body.appendChild(app.view);
// Functions
// param color is a number (e.g. 255)
// return value is a string (e.g. ff)
var prepareRGBChannelColor = function(channelColor) {
var colorText = channelColor.toString(16);
if (colorText.length < 2) {
while (colorText.length < 2) {
colorText = "0" + colorText;
}
}
return colorText;
}
// Getting RGB channels from a number color
// param color is a number
// return an RGB channels object {red: number, green: number, blue: number}
var getRGBChannels = function(color) {
var colorText = color.toString(16);
if (colorText.length < 6) {
while (colorText.length < 6) {
colorText = "0" + colorText;
}
}
var result = {
red: parseInt(colorText.slice(0, 2), 16),
green: parseInt(colorText.slice(2, 4), 16),
blue: parseInt(colorText.slice(4, 6), 16)
};
return result;
}
// Preparaiton of a color data object
// param color is a number [0-255]
// param alpha is a number [0-1]
// return the color data object {color: number, alpha: number, channels: {red: number, green: number, blue: number}}
var prepareColorData = function(color, alpha) {
return {
color: color,
alpha: alpha,
channels: getRGBChannels(color)
}
}
// Getting the color of a gradient for a very specific gradient coef
// param from is a color data object
// param to is a color data object
// return value is of the same type
var getColorOfGradient = function(from, to, coef) {
if (!from.alpha && from.alpha !== 0) {
from.alpha = 1;
}
if (!from.alpha && from.alpha !== 0) {
to.alpha = 1;
}
var colorRed = Math.floor(from.channels.red + coef * (to.channels.red - from.channels.red));
colorRed = Math.min(colorRed, 255);
var colorGreen = Math.floor(from.channels.green + coef * (to.channels.green - from.channels.green));
colorGreen = Math.min(colorGreen, 255);
var colorBlue = Math.floor(from.channels.blue + coef * (to.channels.blue - from.channels.blue));
colorBlue = Math.min(colorBlue, 255);
var rgb = prepareRGBChannelColor(colorRed) + prepareRGBChannelColor(colorGreen) + prepareRGBChannelColor(colorBlue);
return {
color: parseInt(rgb, 16),
alpha: from.alpha + coef * (to.alpha - from.alpha)
};
}
var startTime = Date.now();
console.log("start: " + startTime);
// Drawing the gradient
//
var gradient = new PIXI.Graphics();
app.stage.addChild(gradient);
//
var rect = {
width: 200,
height: 200
};
var round = 20;
//
var colorFromData = prepareColorData(0xFF00FF, 1);
var colorToData = prepareColorData(0xFF0000, 0.2);
//
var stepCoef;
var stepColor;
var stepAlpha;
var stepsCount = 100;
var stepHeight = rect.height / stepsCount;
for (var stepIndex = 0; stepIndex < stepsCount; stepIndex++) {
stepCoef = stepIndex / stepsCount;
stepColor = getColorOfGradient(colorFromData, colorToData, stepCoef);
gradient.beginFill(stepColor.color, stepColor.alpha);
gradient.drawRect(
0,
rect.height * stepCoef,
rect.width,
stepHeight
);
}
// Applying a mask with round corners to the gradient
var roundMask = new PIXI.Graphics();
roundMask.beginFill(0x000000);
roundMask.drawRoundedRect(0, 0, rect.width, rect.height, round);
app.stage.addChild(roundMask);
gradient.mask = roundMask;
var endTime = Date.now();
console.log("end: " + endTime);
console.log("total: " + (endTime - startTime));
The interesting thing is that it takes only about 2-5 ms for the whole process!
If you wan't to change colors of the gradient to white>black (as described in the question), just change the next params:
var colorFromData = prepareColorData(0xFF00FF, 1);
var colorToData = prepareColorData(0xFF0000, 0.2);
To:
var colorFromData = prepareColorData(0xFFFFFF, 1);
var colorToData = prepareColorData(0x000000, 0.2);
Not full answer but some extra information
As far I know, you can't use gradient for PIXI.Graphics even for sprites you need extra canvas
Just draw the gradient you want to a canvas:
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createLinearGradient
Then use that canvas as a texture: Texture.fromCanvas(canvas);
Look at this article.
For gradual opacity, Alpha Mask can help
http://pixijs.io/examples/#/demos/alpha-mask.js
P.S Maybe phaser.js can do more
Did you ever figure this out? I couldn't find a solution online either, so I implemented it myself using a filter. Have a look: https://codepen.io/Lancer611/pen/KodabK.
Some of the pixi code:
function newGradientPoly(poly, fill, fillSize){
var container = new PIXI.Sprite();
app.stage.addChild(container);
var shape = new PIXI.Graphics();
shape.beginFill(0xffffff)
.lineStyle(1, 0x333333)
.drawPolygon(poly);
var mask = new PIXI.Graphics();
mask.beginFill(0xffffff, 1)
.drawPolygon(poly);
container.mask = mask;
container.addChild(shape);
var fshaderCode = document.getElementById("fragShader").innerHTML;
fogShader = new PIXI.Filter(null, fshaderCode);
fogShader.uniforms.resolution = [width, height];
fogShader.uniforms.segments = poly.slice();
fogShader.uniforms.count = poly.length/2;
fogShader.uniforms.gSize = fillSize;
fogShader.uniforms.fill = fill;
shape.filters=[fogShader];
}
I've created a pixi plugin for displaying vector drawings in Pixi. The main limitation is that you need to draw your rectangle in the vector art program Omber first, so you need to know the size of your rectangle beforehand (since everything is vector-based, you could theoretically scale things later, but then the rounded corners would end up being a little uneven). The workflow is similar to using sprites: 1. draw your rectangles in Omber 2. export them to gltf 3. load the gltf files in your Pixi program 4. position the rectangles where you want them.
Another possibility is that you could create the gradient as a separate object, and then you can mask it out with a polygon. Here's an example. In that example, I'm using a vector drawing for the gradient, but since gradients don't become blurry when resized, you could probably use a sprite for that as well. I'm not sure if masks have good performance, but if you just need a few of them, then it's probably fine.
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 would like to make a "prototype" of animations for a future game. But I'm totally a noob in kineticJS.
I have an object where I make all my functions:
var app = {}
I have a function init to build a layer, a stage and declare that I will use requestAnimationFrame:
init: function(){
layer = new Kinetic.Layer();
DrawingTab = [];
stage = new Kinetic.Stage({
container: 'canvasDemo',
width: 800,
height: 600
});
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function(callback){
window.setTimeout(callback, 1000 / 60);
};
})();
}
Secondly, I've got one function to build my rects:
createObject: function(){
rect = new Kinetic.Rect({
x: 50,
y: 50,
width: 150,
height: 150,
fill: 'black',
name: 'batteur',
id: 'batteur'
});
rect1 = new Kinetic.Rect({
x: 300,
y: 50,
width: 150,
height: 150,
fill: 'black',
name: 'batteur1',
id: 'batteur1'
});
rect2 = new Kinetic.Rect({
x: 550,
y: 50,
width: 150,
height: 150,
fill: 'black',
name: 'batteur2',
id: 'batteur2'
});
layer.add(rect);
layer.add(rect1);
layer.add(rect2);
stage.add(layer);
DrawingTab.push(rect,rect1,rect2,rect3,rect4,rect5);
}
That's all I did. And then, I want to know how to animate like that:
every 20 secondes, one of the rect (select randomly) change of color,
and the user have to click on it.
the user have 5sec to click on it, and if he doesn't click, the rect change to the beginning color.
I hope explanations are clear and something will can help me, because I'm totally lost.
You should use Kinetic.Animation for animations because it optimizes redraws. Here's an example
If your game is using sprites, you should be using the Sprite shape. Here's an example of that
You don't need requestAnimationFrame or Kinetic.Animation to handle this, considering the kind of animation you want. Only use animations if you need to change the animation status every frame.
See this working DEMO.
Using setInterval and setTimeout the application became more performant.
I reduce the time of change of color to 5 seconds and the time to click to 2 seconds, just to quickly visualization of the features.
Here is the code added:
// times (make changes according)
var timeToChange = 5000; // 5 seconds
var timeToClick = 2000; // 2 seconds
// render all rects
layer.drawScene();
// add a logical rect for each rect in DrawingTab
var LogicalTab = [];
for (var i = 0; i < DrawingTab.length; ++i) {
LogicalTab.push({
isPressed: false,
frame: 0
});
}
// return a random integer between (min, max)
function random(min, max) {
return Math.round(Math.random() * (max - min) + min);
};
// define colors
var colors = ["red", "green", "blue"];
// reset state of current rect
function reset(n) {
var drect = DrawingTab[n];
var lrect = LogicalTab[n];
// check if current rect was clicked
setTimeout(function () {
if (!lrect.isPressed) {
drect.setFill("black");
// redraw scene
layer.drawScene();
lrect.frame = 0;
}
// turn off click event
drect.off("click");
}, timeToClick);
}
// start the animation
var start = setInterval(function () {
// select a rect randomly
var rand = random(0, 2);
var drect = DrawingTab[rand];
var lrect = LogicalTab[rand];
// change color
drect.setFill(colors[lrect.frame]);
// redraw scene
layer.drawScene();
// flag that current rect is not clicked
lrect.isPressed = false;
// check for click events
drect.on("click", function () {
// flag that current rect is clicked
lrect.isPressed = true;
// hold current color
lrect.frame++;
lrect.frame = lrect.frame % colors.length;
});
// reset current rect (only if it is not clicked)
reset(rand);
}, timeToChange);
I'm a newbye here, but I hope I'm able to help. KineticJS don't need requestAnimationFrame, because it has already something that handles animations. so first of all I think you should have a look to this page
if you want to make the rect's color change every 20 s, you may do something like this:
var anim = new Kinetic.Animation(function(frame) {
if(frame.time > 20000)
{
frame.time = 0;
colors = ['red', 'blue', 'violet'];
ora = colors[Math.floor(Math.random()*3)];
DrawingTab[Math.floor(Math.random*6)].setAttrs({fill: ora});
}
},layer);
then, for the 5sec stuff, I tried to write something
var currentRect = { value:0, hasClicked : true };
var anim2 = new Kinetic.Animation(function(frame) {
if(frame.time > 20000)
{
frame.time = 0;
colors = ['red', 'lightblue', 'violet'];
ora = colors[Math.floor(Math.random()*3)];
currentRect.hasClicked = false;
currentRect.value=Math.floor(Math.random()*6);
DrawingTab[currentRect.value].setAttrs({fill: ora});
}
if (!currentRect.hasClicked && frame.time>5000)
{
DrawingTab[currentRect.value].setAttrs({fill: 'black'});
currentRect.hasClicked = true;
}
DrawingTab[currentRect.value].on('click',function(){ if (frame.time<=5000) currentRect.hasClicked = true;});
},layer);
anim2.start();
I've just tried something similiar and it looks like it's working :)
p.s. sorry about my english, I'm only a poor italian student
p.p.s. I'm sure the code can be optimized, but for now I think it can be alright
I have managed to dynamically create an array of shapes, and they are nicely placed at different coordinates.
However, when I try to assign an event within that loop, the result of click is always the same. As if the click event is still referencing the last iteration of my loop.
What am I doing wrong? Thanks!
EDIT: Actually, re-produced this behaviour in an isolated environment:
var stage = new Kinetic.Stage({
container: 'container',
width: 1024,
height: 768
});
var layer = new Kinetic.Layer();
singleSegment=40;
for (var i = 0; i < 4; i++) {
depth=singleSegment+(singleSegment*i);
dotLabel = new Kinetic.Text({
x: depth,
y: depth,
text: "test"
});
dotLabel.on('click', function(evt){
console.log(this.x);
});
layer.add(dotLabel);
}
stage.add(layer);
How do I add different events to these four labels?
You are doing everything correct, I think. but because of this;
console.log(i);
The last value of i is array.length-1, and when it is clicked, it shows that value, which does not change because it's outside of loop when it is clicked.
This will show different value.
console.log(this.attrs.x);
I just had to deal with this same issue. I solved it by storing to each shape its location.
for (var axisItem=0;axisItem<innerCircleXAxisArray.length;axisItem++)
{
var arc = new Kinetic.Shape({
drawFunc: function(canvas){
var allAttrs = this.getAttrs();
var start = allAttrs['start'];
var end = allAttrs['end'];
var context = canvas.getContext();
context.strokeStyle = 'red';
var centerOfCanvasX = canvasWidth / 2;
var centerOfCanvasY = canvasHeight / 2;
context.translate(centerOfCanvasX, centerOfCanvasY);
context.lineWidth = 15;
context.beginPath();
context.arc(0, 0, 284, start , end, true);
canvas.stroke(this); // Fill the path
context.closePath();
context.translate(-centerOfCanvasX, -centerOfCanvasY);
},
fill: 'red',
stroke: 'red',
strokeWidth: 15
});
arc.setAttrs({'start': innerCircleXAxisArray[axisItem]['start'], 'end': innerCircleXAxisArray[axisItem]['end']});
layer.add(arc);
}
stage.add(layer);
When the object is created, I use setAttrs to store the object's location - a start and end angle since these are arcs, but it could just as easily be an x and y point. Then in the drawFunc I use getAttrs to retrieve that data and to draw the object.
I'm trying to animatate a spinning star icon:
var star = this._paper.path("M26.522,12.293l-5.024-0.73c-1.089-0.158-2.378-1.095-2.864-2.081l-2.249-4.554c-0.487-0.986-1.284-0.986-1.771,0l-2.247,4.554c-0.487,0.986-1.776,1.923-2.864,2.081l-5.026,0.73c-1.088,0.158-1.334,0.916-0.547,1.684l3.637,3.544c0.788,0.769,1.28,2.283,1.094,3.368l-0.858,5.004c-0.186,1.085,0.458,1.553,1.432,1.041l4.495-2.363c0.974-0.512,2.566-0.512,3.541,0l4.495,2.363c0.974,0.512,1.618,0.044,1.433-1.041l-0.859-5.004c-0.186-1.085,0.307-2.6,1.095-3.368l3.636-3.544C27.857,13.209,27.611,12.452,26.522,12.293zM22.037,16.089c-1.266,1.232-1.966,3.394-1.67,5.137l0.514,2.984l-2.679-1.409c-0.757-0.396-1.715-0.612-2.702-0.612s-1.945,0.216-2.7,0.61l-2.679,1.409l0.511-2.982c0.297-1.743-0.404-3.905-1.671-5.137l-2.166-2.112l2.995-0.435c1.754-0.255,3.592-1.591,4.373-3.175L15.5,7.652l1.342,2.716c0.781,1.583,2.617,2.92,4.369,3.173l2.992,0.435L22.037,16.089z")
.attr({ fill: "darkred", stroke: "none" })
.transform("t"+starX+","+starY);
var a0 = Raphael.animation({ transform: "r360"}, 2000);
star.animate(a0.repeat(Infinity));
If I remove the translate I get a nice animation. But with the translate the animation is weird.
The parameters for the animation should include the translation as well, since they are the end attributes of the object, not just the difference between the start and the end ones. See the example: http://jsfiddle.net/b9akz/32/.
var paper = new Raphael('holder');
var starX = 100, starY = 100;
var star = paper.path("M26.522,12.293l-5.024-0.73c-1.089-0.158-2.378-1.095-2.864-2.081l-2.249-4.554c-0.487-0.986-1.284-0.986-1.771,0l-2.247,4.554c-0.487,0.986-1.776,1.923-2.864,2.081l-5.026,0.73c-1.088,0.158-1.334,0.916-0.547,1.684l3.637,3.544c0.788,0.769,1.28,2.283,1.094,3.368l-0.858,5.004c-0.186,1.085,0.458,1.553,1.432,1.041l4.495-2.363c0.974-0.512,2.566-0.512,3.541,0l4.495,2.363c0.974,0.512,1.618,0.044,1.433-1.041l-0.859-5.004c-0.186-1.085,0.307-2.6,1.095-3.368l3.636-3.544C27.857,13.209,27.611,12.452,26.522,12.293zM22.037,16.089c-1.266,1.232-1.966,3.394-1.67,5.137l0.514,2.984l-2.679-1.409c-0.757-0.396-1.715-0.612-2.702-0.612s-1.945,0.216-2.7,0.61l-2.679,1.409l0.511-2.982c0.297-1.743-0.404-3.905-1.671-5.137l-2.166-2.112l2.995-0.435c1.754-0.255,3.592-1.591,4.373-3.175L15.5,7.652l1.342,2.716c0.781,1.583,2.617,2.92,4.369,3.173l2.992,0.435L22.037,16.089z")
.attr({ fill: "darkred", stroke: "none" })
.translate(starX,starY);
var a0 = Raphael.animation({ transform: "t"+starX + "," + starY + " r360"}, 2000);
star.animate(a0.repeat(Infinity));
You need to factor in the translation in the animate as follows:
var paper = Raphael(0,0,500,500);
var starX = 50;
var starY = 50;
// code for your path here as its rather long! Including your translate
// using the starX and starY
// then the animation
var a0 = Raphael.animation({ transform: "t"+starX+","+starY+"r360"}, 2000);
star.animate(a0.repeat(Infinity));
I also made a jsfiddle example here for you to see.