I'm programming snake with Javascript. For the background of the different body parts I'm using the following gradient generation:
gibGradient: function() {
var string = "background: linear-gradient(to right, rgba(243,226,199,1) 15%,rgba(193,158,103,1) "+ snake.data.gradientLinks +"%,rgba(182,141,76,1) "+ snake.data.gradientRechts +"%,rgba(233,212,179,1) 90%);";
if ((snake.data.gradientLinks < 85) && (snake.data.modus == "hochzaehlen")) {
snake.data.gradientLinks = snake.data.gradientLinks + 5;
snake.data.gradientRechts = snake.data.gradientRechts + 5;
if (snake.data.gradientLinks >= 85) {
snake.data.modus = "runterZaehlen";
}
}
if ((snake.data.gradientLinks > 20) && (snake.data.modus == "runterZaehlen")) {
snake.data.gradientLinks = snake.data.gradientLinks - 5;
snake.data.gradientRechts = snake.data.gradientRechts - 5;
if (snake.data.gradientLinks <= 20) {
snake.data.modus = "hochzaehlen";
}
}
return string;
},
My problem is that when the snake moves and it changes directions, the gradient needs to be bent to fit in the last body part before the corner ends and the last that follows the straight of the snake.
For Example:
Im using 10x10 px div elements
Now i need the transition when it moves a corner
Anybody got an idea?
I took the time to write a few utility javascript functions you may find useful. They require the use of the jQuery library however. The best way to create bent gradients is to use offset radial gradients. This combined with border radius makes for a really nice effect.
Now it is up to you to
use the right function at the right times (the naming convention of
the functions is sideA_To_sideB so rightToUp means going right will
eventually find sideA and going up will eventually find sideB - sides
being the head or the tail)
make it cross browser (if you are into that sort of thing)
rounding the head and tail would be a nice touch (ideally this rounding would only occur on
vertical and horizontal parts)
Feel free to change the size variable to suit your needs.
EDIT - based on the image you just added I created this : jsfiddle http://jsfiddle.net/Lbydhhkh/. This was done using rotated linear gradients. I still think using my original approach looks better and makes more sense. This should be enough to send you in the right direction though. The pseudocode can still be used for this new code.
var size = 40;
function aToB(gradient) {
return $("<div>").css({
width: size,
height: size,
background: gradient,
position: "absolute"
});
}
function radialOut(x, y, corner) {
var css = {};
css["border-" + corner + "-radius"] = size / 2;
return aToB([
"radial-gradient(",
size,
"px at ",
x,
"px ",
y,
"px, red, blue)"].join("")).css(css);
}
function radialIn(x, y, corner) {
var css = {};
css["border-" + corner + "-radius"] = size / 2;
return aToB([
"radial-gradient(",
size,
"px at ",
x,
"px ",
y,
"px, blue, red)"].join("")).css(css);
}
function downToUp() {
return aToB("linear-gradient(to left, red, blue)");
}
function rightToLeft() {
return aToB("linear-gradient(to bottom, red, blue)");
}
function upToDown() {
return aToB("linear-gradient(to right, red, blue)");
}
function leftToRight() {
return aToB("linear-gradient(to top, red, blue)");
}
function upToRight() {
return radialIn(size, 0, "bottom-left");
}
function leftToUp() {
return radialIn(0, 0, "bottom-right");
}
function downToLeft() {
return radialIn(0, size, "top-right");
}
function rightToDown() {
return radialIn(size, size, "top-left");
}
function rightToUp() {
return radialOut(size, 0, "bottom-left");
}
function upToLeft() {
return radialOut(0, 0, "bottom-right");
}
function leftToDown() {
return radialOut(0, size, "top-right");
}
function downToRight() {
return radialOut(size, size, "top-left");
}
$(function () {
//inner
$("body").append(upToDown().css({
top: size,
left: 0
})).append(upToRight().css({
top: size * 2,
left: 0
})).append(leftToRight().css({
top: size * 2,
left: size
})).append(leftToUp().css({
top: size * 2,
left: size * 2
})).append(downToUp().css({
top: size,
left: size * 2
})).append(downToLeft().css({
top: 0,
left: size * 2
})).append(rightToLeft().css({
top: 0,
left: size
})).append(rightToDown().css({
top: 0,
left: 0
}));
//outer
$("body").append(leftToDown().css({
top: 0,
left: size * 5
})).append(upToDown().css({
top: size,
left: size * 5
})).append(upToLeft().css({
top: size * 2,
left: size * 5
})).append(rightToLeft().css({
top: size * 2,
left: size * 4
})).append(rightToUp().css({
top: size * 2,
left: size * 3
})).append(downToUp().css({
top: size * 1,
left: size * 3
})).append(downToRight().css({
top: 0,
left: size * 3
})).append(leftToRight().css({
top: 0,
left: size * 4
}));
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Also here is some pseudocode to help you call the appropriate functions
while(nextPart()) { //while there are more parts to process
var prev = getPrev(), //returns null or previous part
curr = getCurrent(), //returns current part
next = getNext(), //returns null or next part
a, b, part = [];
//get the direction towards the tail
if(prev) a = curr.getDirectionTo(prev); //returns "up", "right", "down", or "left"
else a = tail.getOppositeDirection(); //returns "up", "right", "down", or "left"
//get the direction towards the head
if(next) b = curr.getDirectionTo(next);
else b = head.getDirection(); //returns "up", "right", "down", or "left"
b = upperCaseFirstLetter(b);
if(!prev) part.push("tail"); //is this a tail?
if(!next) part.push("head"); //is this a head?
//the following line of code calls a function with the form "aToB"
//the variable part does not do anything yet but it can help the called
//function determine if this part is a head, tail, or both for rounding
var domElement = window[a + "To" + b](part);
domElement.css(curr.position()); //properly position the element
$("#container").append(domElement); //add the element to the container
}
Related
I have a container that is expanded and collapsed on click of chevron icon. The code to collapse/expand the container is in the function transformAnimation. The code of transformAnimation is similar to the code on MDN web docs for requestAnimationFrame. The code to animate (scale) the container has been developed on the guidelines of this article on Building performant expand & collapse animations on Chrome Developers website.
I am not able to figure out how to calculate yScale value (which is nothing but the css function scaleY() for collapse/expand animation) as a function of the time elapsed since the start of the animation.
To elaborate what I mean, let's assume that the container is in expanded state. In this state the yScale value of the container is 6. Now when user clicks on the toggle button, in the transformAnimation function for each animation frame, i.e, execution of the requestAnimationFrame callback step function, the value of yScale should decrease from 6 (the expanded state) to 1 (the collapsed state) in the exact duration that I want the animation to run for. So, basically I want to achieve something similar to css property transition-duration: 2s, where I can control the duration.
In the present state, the code to calculate yScale is not working as expected.
const dragExpandableContainer = document.querySelector('.drag-expandable-container');
const dragExpandableContents = document.querySelector('.drag-expandable__contents');
const resizeableControlEl = document.querySelector('.drag-expandable__resize-control');
const content = document.querySelector(`.content`);
const toggleEl = document.querySelector(`.toggle`);
const collapsedHeight = calculateCollapsedHeight();
/* This height is used as the basis for calculating all the scales for the component.
* It acts as proxy for collapsed state.
*/
dragExpandableContainer.style.height = `${collapsedHeight}px`;
// Apply iniial transform to expand
dragExpandableContainer.style.transform = `scale(1, 10)`;
// Apply iniial reverse transform on the contents
dragExpandableContents.style.transform = `scale(1, calc(1/10))`;
let isOpen = true;
const togglePopup = () => {
if (isOpen) {
collapsedAnimation();
toggleEl.classList.remove('toggle-open');
isOpen = false;
} else {
expandAnimation();
toggleEl.classList.add('toggle-open');
isOpen = true
};
};
function calculateCollapsedHeight() {
const collapsedHeight = content.offsetHeight + resizeableControlEl.offsetHeight;
return collapsedHeight;
}
const calculateCollapsedScale = function() {
const collapsedHeight = calculateCollapsedHeight();
const expandedHeight = dragExpandableContainer.getBoundingClientRect().height;
return {
/* Since we are not dealing with scaling on X axis, we keep it 1.
* It can be inverse to if required */
x: 1,
y: expandedHeight / collapsedHeight,
};
};
const calculateExpandScale = function() {
const collapsedHeight = calculateCollapsedHeight();
const expandedHeight = 100;
return {
x: 1,
y: expandedHeight / collapsedHeight,
};
};
function expandAnimation() {
const {
x,
y
} = calculateExpandScale();
transformAnimation('expand', {
x,
y
});
}
function collapsedAnimation() {
const {
x,
y
} = calculateCollapsedScale();
transformAnimation('collapse', {
x,
y
});
}
function transformAnimation(animationType, scale) {
let start, previousTimeStamp;
let done = false;
function step(timestamp) {
if (start === undefined) {
start = timestamp;
}
const elapsed = timestamp - start;
if (previousTimeStamp !== timestamp) {
const count = Math.min(0.1 * elapsed, 200);
//console.log('count', count);
let yScale;
if (animationType === 'expand') {
yScale = (scale.y / 100) * count;
} else yScale = scale.y - (scale.y / 100) * count;
//console.log('yScale', yScale);
if (yScale < 1) yScale = 1;
dragExpandableContainer.style.transform = `scale(${scale.x}, ${yScale})`;
const inverseXScale = 1;
const inverseYScale = 1 / yScale;
dragExpandableContents.style.transform = `scale(${inverseXScale}, ${inverseYScale})`;
if (count === 200) done = true;
//console.log('elapsed', elapsed);
if (elapsed < 1000) {
// Stop the animation after 2 seconds
previousTimeStamp = timestamp;
if (!done) requestAnimationFrame(step);
}
}
}
requestAnimationFrame(step);
}
.drag-expandable-container {
position: absolute;
bottom: 0px;
display: block;
overflow: hidden;
width: 100%;
background-color: #f3f7f7;
transform-origin: bottom left;
}
.drag-expandable__contents {
transform-origin: top left;
}
.toggle {
position: absolute;
top: 2px;
right: 15px;
height: 10px;
width: 10px;
transition: transform 0.2s linear;
}
.toggle-open {
transform: rotate(180deg);
}
.drag-expandable__resize-control {
background-color: #e7eeef;
}
.burger-icon {
width: 12px;
margin: 0 auto;
padding: 2px 0;
}
.burger-icon__line {
height: 1px;
background-color: #738F93;
margin: 2px 0;
}
.drag-expandable__resize-control:hover {
border-top: 1px solid #4caf50;
cursor: ns-resize;
}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css.css">
</head>
<body>
<div class="drag-expandable-container">
<div class="drag-expandable__contents">
<div class="drag-expandable__resize-control">
<div class="burger-icon">
<div class="burger-icon__line"></div>
<div class="burger-icon__line"></div>
<div class="burger-icon__line"></div>
</div>
</div>
<div class="content" />
<div>
<div class="toggle toggle-open" onclick="togglePopup()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.1.1 by #fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M416 352c-8.188 0-16.38-3.125-22.62-9.375L224 173.3l-169.4 169.4c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l192-192c12.5-12.5 32.75-12.5 45.25 0l192 192c12.5 12.5 12.5 32.75 0 45.25C432.4 348.9 424.2 352 416 352z"/></svg>
</div>
</div>
</div>
</div>
</body>
<script type="text/javascript" src="js.js"></script>
</html>
Posting this as an answer for better code formatting.
It sounds like what you are trying to do can be achieved with something called Linear Interpolation (commonly known as "lerp" or "lerping" amongst developers).
Have a look at the following function:
function lerp(v0, v1, t) {
return v0*(1-t)+v1*t
}
This is probably the simplest possible function for linear interpolation, but it should be good enough for what you are trying to calculate. The function takes three parameters:
v0, the initial value, for example 0
v1, the final value, for example 10
t, a value between 0 and 1, representing the "percentage of progress" from v0 to v1
So for example, if you were to call lerp(0, 10, 0.5), the function would return 5, since 5 is "50% of the way" going from 0 to 10.
If you were to call lerp(0, 10, 0.9), the function would return 9.
Furthermore with t = 0, the function returns 0 and with t = 10, the function returns 10.
So applying this to your problem, you have two container heights:
y0 = container height when minimized (0)
y1 = container height when maximized (6)
and then you have the elapsed time (et) and the total time (tt) how long it should take for the container to open and close (2 seconds).
We want to make lerp to return the yScale at some point in time t.
We already have two of the lerp parameters, v0 = y0 and v1 = y1, but we can't directly use et as the t parameter, because et goes from 0 to 2 and t has to go from 0 to 1.
To fix this, we have to scale et to go from 0 to 1.
Here is a full example code showing the scaling and the usage of lerp:
// Y scale value when minimized
let y0 = 0;
// Y scale value when maximized
let y1 = 6;
// Time it should take to go from 0 to 6
let totalTime = 2000;
// The lerp function
function lerp(v0, v1, t) {
return v0*(1-t)+v1*t
}
// A loop going from 0ms to 2000ms to simulate how the lerp function works
// We have to use integers and therefore milliseconds with 100ms steps here,
// because JavaScript does not like incrementing by 0.1
for(let elapsed_time = 0; elapsed_time <= totalTime; elapsed_time += 100)
{
// Scale elapsed time to go from 0 to 1, instead of 0 to 2
let elapsed_time_scaled = elapsed_time / totalTime;
// Get y scale with lerp at the current time
let scale_y = lerp(y0, y1, elapsed_time_scaled).toFixed(2);
// Log the stuff out
console.log(elapsed_time, elapsed_time_scaled, scale_y);
}
Which returns something like this:
0 0 '0.00'
100 0.05 '0.30'
200 0.1 '0.60'
300 0.15 '0.90'
400 0.2 '1.20'
500 0.25 '1.50'
600 0.3 '1.80'
700 0.35 '2.10'
800 0.4 '2.40'
900 0.45 '2.70'
1000 0.5 '3.00'
1100 0.55 '3.30'
1200 0.6 '3.60'
1300 0.65 '3.90'
1400 0.7 '4.20'
1500 0.75 '4.50'
1600 0.8 '4.80'
1700 0.85 '5.10'
1800 0.9 '5.40'
1900 0.95 '5.70'
2000 1 '6.00'
Looks good to me! Would this help you?
So, for our project, we have to make a Wheel of Fortune game (based on the game show), and I made everything (like checking if the word was correct and everything) but I still don't know how to do the animations. What I have to animate is similar to what is in the attached picture. Our teacher told us that we can't use canvas, so I can't use .rotate(). So, does anyone have any ideas as to how this can be accomplished? Remember, I don't even need the wheel to actually rotate; it would work even if just the pictures were to change positions.
Here is the picture of a sample wheel (pretend the numbers are all pictures). I have stored all of the pictures in a single array that contains all of the pictures:
https://en.m.wikipedia.org/wiki/Wheel_of_Fortune_(U.S._game_show)#/media/File%3AWheel_of_Fortune_Round_1_template_Season_31.png
PS: For academic honesty purposes, I will not be able to post my code here.
Thank you for your time, everyone!
This is a very basic way of doing it: http://codepen.io/AaronGeorge/pen/zoOWagDead link
What you utilise is the CSS transform property and use the rotate value.
You'll need to do some math to work out how much you need to rotate the image, but transform: rotate(amount); will do the trick without using canvas.
The following example is a slight remake of How to Draw a Wheel of Fortune - without using HTML Canvas
Create an array of the sectors values, starting from right - clockwise.
Generate a random ang (in radians)
Animate CSS's rotate property using JS's Animations API at a random duration
Create a function getIndex() to get the prize sector index
Assign a "finish" Event listener to get the landed prize sector
const sectors = [
"500", "700", "BMB", "600", "550", "500", "600", "B",
"650", "FP", "700", "LAT", "800", "500", "650", "500",
"900", "B", "2500", "W", "600", "700", "600", "650",
];
// Generate random float in range min-max:
const rand = (m, M) => Math.random() * (M - m) + m;
const tot = sectors.length;
const elWheel = document.querySelector("#wheel");
const elAng = document.querySelector("#ang");
const PI = Math.PI;
const TAU = 2 * PI;
let arc = TAU / tot;
let ang = 0; // Angle rotation in radians
let isSpinning = false;
const imageAngOffset = -arc / 2;
// Get index of current sector
const getIndex = () => Math.floor(tot - (ang % TAU) / TAU * tot) % tot;
const spin = () => {
if (isSpinning) return; // Do nothing
isSpinning = true;
ang += rand(20, 30); // Generate random angle
const anim = elWheel.animate([{rotate: `${ang}rad`}], {
duration: rand(4000, 5000),
easing: "cubic-bezier(0.23, -0.16, 0.2, 1)",
fill: "both"
});
anim.addEventListener("finish", (event) => {
isSpinning = false;
ang += imageAngOffset; // Fix for image rotation offset (half arc)
const index = getIndex();
const value = sectors[index];
console.clear();
console.log(value);
});
};
elWheel.addEventListener("pointerdown", spin);
* {margin: 0;}
#wheel-wrapper {
overflow: hidden;
position: relative;
margin: auto;
width: 90vmin;
aspect-ratio: 1;
display: flex;
/* rotate: -0.25turn; */
}
#wheel {
position: relative;
margin: auto;
width: 90%;
aspect-ratio: 1;
background-image: url(https://i.stack.imgur.com/mcuwP.png);
background-size: cover;
border-radius: 50%;
}
#wheel-wrapper::after {
content: "";
position: absolute;
right: 0;
width: 10%;
background: red;
height: 1%;
transform: translateY(-50%);
top: 50%;
}
<div id="wheel-wrapper">
<div id="wheel"></div>
</div>
Since the rightmost starting sector is not at a perfect 0 degree in the image, don't forget to fix for this by subtracting half arc to the end degree radians ang (see in example above the use of imageAngleOffset).
To rotate the entire wheel wrapper by negative quarter turn (so that the needle stays at the top), uncomment this line in CSS: rotate: -0.25turn;
JavaScript syntax:
context.drawImage(img,srcX,srcY,srcWidth,srcHeight,x,y,width,height);
In Javascript, if I wanted to animate the following spritesheet, I would simply update srcX and srcY every animation frame in order to capture segments of the image.
This results in each frame being clipped and displayed individually onto the canvas, which when updated at a fixed frame rate results in fluid sprite animation, like this:
How can I do this using the "Fabric.js" library?
Note: One way to achieve this would be to set canvasSize = frameSize so that only one frame can be seen at any given time. Then by moving the image around, different frames can be placed inside the canvas in order to simulate animation. This will not work however with a large canvas, or with variable frame sizes.
Look at this,it does the same thing.
A walking human figure.
Fabric.js,image animation.
var URL = 'http://i.stack.imgur.com/M06El.jpg';
var canvas = new fabric.Canvas('canvas');
var positions = {
topSteps:2,
leftSteps:4
};
canWalk(URL,positions);
function canWalk(URL,positions){
var myImage = new Image();
myImage.src = URL;
myImage.onload = function() {
var topStep = myImage.naturalHeight/positions.topSteps;
var leftStep = myImage.naturalWidth/positions.leftSteps;
var docCanvas = document.getElementById('canvas');
docCanvas.height = topStep;
docCanvas.width = leftStep;
fabricImageFromURL(0,0);
var y = 0;
var x = 0;
setInterval(function(){
if(x == positions.leftSteps)
{
x = 0;
y++;
if(y==positions.topSteps)
{
y=0;
}
}
fabricImageFromURL(-y*topStep,-x*leftStep);
x++;
},100);
};
}
function fabricImageFromURL(top, left)
{
console.log(top, left);
fabric.Image.fromURL(URL, function (oImg) {
oImg.set('left', left).set('top',top);
oImg.hasControls = false;
oImg.hasBorders = false;
oImg.selectable = false;
canvas.add(oImg);
canvas.renderAll();
}, {"left": 0, "top": 0, "scaleX": 1, "scaleY": 1});
}
<canvas id="canvas"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.3/fabric.min.js"></script>
By default Fabric won't do it. You need to present 'source' properties in fabric.Image object & extend fabric.Image _render method. Original looks this:
/**
* #private
* #param {CanvasRenderingContext2D} ctx Context to render on
* #param {Boolean} noTransform
*/
_render: function(ctx, noTransform) {
var x, y, imageMargins = this._findMargins(), elementToDraw;
x = (noTransform ? this.left : -this.width / 2);
y = (noTransform ? this.top : -this.height / 2);
if (this.meetOrSlice === 'slice') {
ctx.beginPath();
ctx.rect(x, y, this.width, this.height);
ctx.clip();
}
if (this.isMoving === false && this.resizeFilters.length && this._needsResize()) {
this._lastScaleX = this.scaleX;
this._lastScaleY = this.scaleY;
elementToDraw = this.applyFilters(null, this.resizeFilters, this._filteredEl || this._originalElement, true);
}
else {
elementToDraw = this._element;
}
elementToDraw && ctx.drawImage(elementToDraw,
x + imageMargins.marginX,
y + imageMargins.marginY,
imageMargins.width,
imageMargins.height
);
this._stroke(ctx);
this._renderStroke(ctx);
},
And you need to change it:
fabric.util.object.extend(fabric.Image.prototype, {
_render: function(ctx, noTransform) {
// ...
elementToDraw && ctx.drawImage(
elementToDraw,
this.source.x,
this.source.y,
this.source.width,
this.source.height,
x + imageMargins.marginX,
y + imageMargins.marginY,
imageMargins.width,
imageMargins.height
);
this._renderStroke(ctx);
}
});
You can use a combination of setting the clipTo-function and invoking setLeft / setTop in a loop. Within the fabric.Image constructor's option you pass the property clipTo and tell fabric to cut out a specific part of the image. Then, with setTop / setLeft inside a loop you trigger repainting and thereby invoking clipTo and at the same time re-position the cut image so it always stays in the same place.
I had the same problem and extracted the logic into two functions. Quick rundown of the options:
spriteWidth - width of one animation frame of the sprite
spriteHeight - height of one animation frame of the sprite
totalWidth - width of the whole sprite image
totalHeight - height of the whole sprite image
animationFrameDuration - how long one sprite frame should be shown
startRandom - if you want to start the animation not right away, but randomly within 1 second
left - just like the normal left option of fabric.Image
top - just like the normal top option of fabric.Image
Synchronous version (passing the HTMLImageElement):
/**
* #param imgObj HTMLImageElement
* #param options {
* spriteWidth: number
* spriteHeight: number
* totalWidth: number
* totalHeight: number
* animationFrameDuration: number
* startRandom: boolean (optional)
* left: number (optional)
* top: number (optional)
* }
* #returns fabric.Image
*/
function animateImg(imgObj, options) {
const left = options.left || 0;
const top = options.top || 0;
let x = 0;
let y = 0;
const image = new fabric.Image(imgObj, {
width: options.totalWidth,
height: options.totalHeight,
left: left,
top: top,
clipTo: ctx => {
ctx.rect(-x - options.totalWidth / 2, -y - options.totalHeight / 2, options.spriteWidth, options.spriteHeight);
}
});
setTimeout(() => {
setInterval(() => {
x = (x - options.spriteWidth) % options.totalWidth;
if (x === 0) {
y = (y - options.spriteHeight) % options.totalHeight;
}
image.setLeft(x + left);
image.setTop(y + top);
}, options.animationFrameDuration)
}, options.startRandom ? Math.random() * 1000 : 0);
return image;
}
Asynchronous version (passing the image URL):
/**
* #param imgURL string
* #param options {
* spriteWidth: number
* spriteHeight: number
* totalWidth: number
* totalHeight: number
* animationFrameDuration: number
* startRandom: boolean (optional)
* left: number (optional)
* top: number (optional)
* }
* #param callback (image : fabric.Image) => void
*/
function animateImgFromURL(imgURL, options, callback) {
const left = options.left || 0;
const top = options.top || 0;
let x = 0;
let y = 0;
fabric.Image.fromURL(
imgURL,
image => {
setTimeout(() => {
setInterval(() => {
x = (x - options.spriteWidth) % options.totalWidth;
if (x === 0) {
y = (y - options.spriteHeight) % options.totalHeight;
}
image.setLeft(x);
image.setTop(y);
}, options.animationFrameDuration)
}, options.startRandom ? Math.random() * 1000 : 0);
callback(image);
}, {
width: options.totalWidth,
height: options.totalHeight,
left: 0,
top: 0,
left: left,
top: top,
clipTo: ctx => {
ctx.rect(-x - options.totalWidth / 2, -y - options.totalHeight / 2, options.spriteWidth, options.spriteHeight);
}
});
Note that the above functions do not rerender the canvas, you have to do that yourself.
You can use the above code like this to animate your sprite two times side by side (once synchronous version, once asynchronous):
// Assuming:
// 1. canvas was created
// 2. Sprite is in html with id 'walking'
// 3. Sprite is within folder 'images/walking.jpg'
const img1 = animateImg(document.getElementById('walking'), {
spriteWidth: 125,
spriteHeight: 125,
totalWidth: 500,
totalHeight: 250,
startRandom: true,
animationFrameDuration: 150,
left: 125,
top: 0
});
canvas.add(img1);
animateImgFromURL('images/walking.jpg', {
spriteWidth: 125,
spriteHeight: 125,
totalWidth: 500,
totalHeight: 250,
startRandom: true,
animationFrameDuration: 150
}, image => canvas.add(image));
// hacky way of invoking renderAll in a loop:
setInterval(() => canvas.renderAll(), 10);
I've seen many answers about using closures in JS but nothing that I could adapt to my situation:
I have many words sprawled randomly across the browser window at various sizes and positions.
This function would shrink them all down to the same size, then position them side-by-side, one after the other, left to right (re-ordering the words into a sentence).
function alignWords() {
// starting X position
var x_align = 100;
// loop thru each word, shrink its size (ie. font-size) and position them end-to-end on X axis (with 5px spacing)
$('.other-word').each(function(index, item){
$(item).toggleClass("other-word-animate");
console.log("t- x1 = "+ x_align) // does not increment. Outputs: t- x1 = 100, t- x1 = 100, t- x1 = 100, etc...
$(item).animate({
'font-size': $('.main-word').css("font-size").substr(0, $('.main-word').css("font-size").length-2),
top: $('.main-word').css("top"),
left: x_align // always remains 100, which is wrong
}, function() {
x_align += $(this).width() + 5;
console.log("t- x = "+ x_align); // increments correctly. Outputs: t- x = 154, t- x = 311, t- x = 316, etc...
});
});
}
My incrementing of x_align in the animate() callback is not being reflected in the subsequent loop at left: x_align.
Help much appreciated,
All the callbacks are run long after the animation are started (they're all started at the same time). Your goal isn't 100% clear but you probably want to chain the animation, not run them in parallel like this for example :
var x_align = 100,
otherWords = $('.other-word'),
fontSize = $('.main-word').css("font-size").slice(0, -2),
top = $('.main-word').css("top"),
i = 0, n = otherWords.length;
(function doOne(){
if (i++>=n) return;
otherWords.eq(i).toggleClass("other-word-animate")
.animate({
fontSize: fontSize,
top: top,
left: x_align
}, function(){
x_align += $(this).width() + 5;
doOne();
});
})();
By combining some CSS and Jquery UI / draggable I have created the ability to pan an image and with a little extra JS you can now zoom the image.
The problem I am having is that, if you zoom in the image's top left corner is fixed, as you would expect. What I would like is for the image to stay central (based on the current pan) so that the middle of the image stays in the middle of the container whilst getting larger.
I have written some code for this but doesn't work, I expect my maths is wrong. Could anyone help?
I want it to work like this does. When you scroll into an image it keeps the image centered based on the current pan rather than zooming out from the corner.
HTML:
<div id="creator_container" style="position: relative; width: 300px; height: 400px; overflow: hidden;">
<img src="/images/test.gif" class="user_image" width="300" style="cursor: move; position: absolute; left: 0; top: 0;">
</div>
Javascript:
$("#_popup_creator .user_image").bind('mousewheel', function(event, delta) {
zoomPercentage += delta;
$(this).css('width',zoomPercentage+'%');
$(this).css('height',zoomPercentage+'%');
var widthOffset = (($(this).width() - $(this).parent().width()) / 2);
$(this).css('left', $(this).position().left - widthOffset);
});
Long story short, you need to make a transform matrix to scale by the same amount as the image and then transform the image's position using that matrix. If that explanation is complete greek to you, look up "image transforms" and "matrix math".
The beginning of this page is a pretty good resource to start with even though it's a different programming language:
http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/geom/Matrix.html
Anyway, I've implemented those methods in some projects of my own. Here's the zoom in function from something I wrote that functions the way you want:
function zoomIn(event) {
var prevS = scale;
scale += .1;
$(map).css({width: (baseSizeHor * scale) + "px", height: (baseSizeVer * scale) + "px"});
//scale from middle of screen
var point = new Vector.create([posX - $(viewer).width() / 2, posY - $(viewer).height() / 2, 1]);
var mat = Matrix.I(3);
mat = scaleMatrix(mat, scale / prevS, scale / prevS);
point = transformPoint(mat, point);
//http://stackoverflow.com/questions/1248081/get-the-browser-viewport-dimensions-with-javascript
posX = point.e(1) + $(viewer).width() / 2;
posY = point.e(2) + $(viewer).height() / 2;
$(map).css({left: posX, top: posY});
return false;//prevent drag image out of browser
}
Note the commands "new Vector.create()" and "Matrix.I(3)". Those come from the JavaScript vector/matrix math library http://sylvester.jcoglan.com/
Then note "transformPoint()". That's one of the functions from that ActionScript link (plus hints on http://wxs.ca/js3d/) that I implemented using sylvester.js
For the full set of functions I wrote:
function translateMatrix(mat, dx, dy) {
var m = Matrix.create([
[1,0,dx],
[0,1,dy],
[0,0,1]
]);
return m.multiply(mat);
}
function rotateMatrix(mat, rad) {
var c = Math.cos(rad);
var s = Math.sin(rad);
var m = Matrix.create([
[c,-s,0],
[s,c,0],
[0,0,1]
]);
return m.multiply(mat);
}
function scaleMatrix(mat, sx, sy) {
var m = Matrix.create([
[sx,0,0],
[0,sy,0],
[0,0,1]
]);
return m.multiply(mat);
}
function transformPoint(mat, vec) {
return mat.multiply(vec);
}