Related
I have some js code that i copied from a youtube tutorial and adapted for my own project to fill the header, the code works as intended and it works when the viewport is smaller than around 1200px, however when i put firefox into full screen the animation does not play & the image is being stretched, not retaining its aspect ratio. I do have a 10/15 year old gpu so i'm guessing thats half my issue. The script uses a png image file of 100x100 pixels, which it then converts into particle color values. Can this be optimized or made to run better. it seems that the wider the viewport the longer the animation takes to kick in, until it finally stops & doesn't work. full screen= [2550x1440]...
The original tutorial is here: Pure Javascript Particle Animations & to convert an image to base64 encoding is here: Image to base64.
HTML:
<html>
<body>
<canvas id="CanV"></canvas>
</body>
</html>
CSS:
#Canv{
position:absolute;
top:-1px;left:-2px;
z-index:67;
width:100vw !important;
max-height: 264px !important;
min-height: 245px !important;
filter:blur(2.27px);
}
Javascript:
window.addEventListener("DOMContentLoaded",(e)=>{
const canv = document.getElementById('Canv');
const ctx = canv.getContext('2d');
canv.width = window.innerWidth;
canv.height = window.innerHeight/ 3.85;
let particleArray = [];
let mouse = {
x: null,
y: null,
radius: 74
}
window.addEventListener('mousemove',(e)=>{
mouse.x = event.x + canv.clientLeft/2;
mouse.y = event.y + canv.clientTop/1.2;
});
function drawImage(){
let imageWidth = png.width; //These to values crop if / sum no.
let imageHeight = png.height;
const data = ctx.getImageData(0, 0, imageWidth, imageHeight); //Gets img data for particles
ctx.clearRect(0,0, canv.width, canv.height); // Clears the original img as its now being stored in the variable data.
class Particle {
constructor(x, y, color, size){
this.x = x + canv.width/2 - png.width * 174, //Chngd Ok:74
this.y = y + canv.height/2 - png.height * 32, //Ch<2 Ok:16
this.color = color,
this.size = 2.28, // Particle Size > Changed this value. from 2 i think!.
this.baseX = x + canv.width/1.8 - png.width * 3.1, //Chngd ok:5.1
this.baseY = y + canv.height/1.2 - png.height * 2.8,
this.density = (Math.random() * 14) + 2;
}
draw() {
ctx.beginPath(); // this creates the sort of force field around the mouse pointer.
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
update() {
ctx.fillStyle = this.color;
// Collision detection
let dx = mouse.x - this.x;
let dy = mouse.y - this.y;
let distance = Math.sqrt(dx * dx + dy * dy);
let forceDirectionX = dx / distance;
let forceDirectionY = dy / distance;
// Max distance, past that the force will be 0
const maxDistance = 144;
let force = (maxDistance - distance) / maxDistance;
if (force < 0) force = 0;
let directionX = (forceDirectionX * force * this.density * 0.6);
let directionY = (forceDirectionY * force * this.density * 8.7); //Ch.this
if (distance < mouse.radius + this.size) {
this.x -= directionX;
this.y -= directionY;
} else {
if (this.x !== this.baseX){
let dx = this.x - this.baseX;
this.x -= dx/54; // Speed Particles return to ori
} if (this.y !== this.baseY){
let dy = this.y - this.baseY;
this.y -= dy/17; // Speed Particles return to ori
}
}
this.draw();
}
}
function init(){
particleArray = [];
for(let y = 0, y2 = data.height; y<y2; y++){
for(let x =0, x2 = data.width; x<x2; x++){
if(data.data[(y * 4 * data.width) + (x*4) + 3] > 128){
let positionX = x + 25;
let positionY = y + 45; // Co-ords on Canv
let color = "rgb(" + data.data[(y * 4 * data.width) + (x * 4)] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 1] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 2] + ")";
particleArray.push(new Particle(positionX * 2, positionY * 2, color));
} /* These number effect png size but its to high */
}
}
}
function animate(){
requestAnimationFrame(animate);
ctx.fillStyle = 'rgba(0,0,0,.07)';
ctx.fillRect(0,0, innerWidth, innerHeight);
for(let i =0; i < particleArray.length; i++){
particleArray[i].update();
}
}
init();
animate();
}
const png = new Image();
png.src = "RemovedBase64StringToBig";
window.addEventListener('load',(e)=>{
console.log('page has loaded');
ctx.drawImage(png, 0, 0);
drawImage();
})
});
have managed to shorten it by about 100 characters by shortening all the variable names > PartArr, ImgWidth, DirX, DirY etc, but apart from minifying it is there any other ways to optimize this? and fix the full screen issue?
I tried to add it to a JSfiddle, So I could link to it here, but I don't think this is allowing the base64 string, its not loading anything anyway. The canvas loads, with the bg just no image or animation.
I've found out what part of the problem is with full screen, the cursor position is actually about 300px to the right of where the actual cursor is, but I still have no idea how to fix this or fix the major lagging performance issues. Guessing its alot to compute even just with 100x100.
One option I can think of to make this perform better would be to move it & its calculations, into its own dedicated web worker & convert the image to Webp but i'm still not very clued up about web workers or how to implement them properly.. Will play around & see what I can put together using All the suggestions in the comments & answers.
I'm adding these links only for future reference, when I come back to this later on:
MDN Canvas Optimizations
Html5Rocks Canvas Performance
Stack Question. Canv ~ Opti
Creating A blob From A Base 64 String in Js
Secondary bonus Question,
is there a maximum file size or max px dimensions,
that can be base64 encoded? only asking this as someone on facebook has recently sent me a question regarding another project with multiple base64 encoded images and I was unsure of the answer..
Shortening your code doesn't help much with performance. I'm using Firefox. To check what's taking your time up the most during browser runs in Firefox, you can read Performance from MDN.
The problem with your solution is your fps is dropping hard. This happens because you are painting each Particle every frame. Imagine how laggy it will be when there are thousands of Particles that you need to paint every frame. This paint call is called from your function Particle.draw (which calls the following: ctx.beginPath, ctx.arc, and ctx.closePath). This function, as said, will be called because of Particle.update for each frame. This is an extremely expensive operation. To improve your fps drastically, you can try to not draw each Particle individually, but rather gather all the Particles' ImageData wholly then placing it in the canvas only once in rAQ (thus only one paint happens). This ImageData is an object that contains the rgba for each pixel on canvas.
In my solution below, I did the following:
For each Particle that is dirty (has been updated), modify the ImageData that is to be put in the canvas
Then, after the whole ImageData has been constructed for one frame, only draw once to the canvas using putImageData. This saves a lot of the time needed to call your function Particle.update to draw each Particle individually.
One other obvious solution is to increase the size of Particles so that there are fewer Particles' pixels that are needed to be processed (to alter ImageData). I've also tweaked the code a little so that the image will always be at least 100px high; you can tweak the maths so that the image will always maintain your aspect ratio and respond to window size.
Here's a working example:
const canvas = document.querySelector('#canvas1')
const ctx = canvas.getContext('2d')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
let canvasWidth = canvas.width
let canvasHeight = canvas.height
let particleArray = []
let imageData = []
// mouse
let mouse = {
x: null,
y: null,
radius: 40
}
window.addEventListener('mousemove', e => {
mouse.x = event.x
mouse.y = event.y
})
function drawImage(width, height) {
let imageWidth = width
let imageHeight = height
const data = ctx.getImageData(0, 0, imageWidth, imageHeight)
class Particle {
constructor(x, y, color, size = 2) {
this.x = Math.round(x + canvas.width / 2 - imageWidth * 2)
this.y = Math.round(y + canvas.height / 2 - imageHeight * 2)
this.color = color
this.size = size
// Records base and previous positions to repaint the canvas to its original background color
this.baseX = Math.round(x + canvas.width / 2 - imageWidth * 2)
this.baseY = Math.round(y + canvas.height / 2 - imageHeight * 2)
this.previousX = null
this.previousY = null
this.density = (Math.random() * 100) + 2
}
stringifyColor() {
return `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, ${this.color.a}`
}
update() {
ctx.fillStyle = this.stringifyColor()
// collision detection
let dx = mouse.x - this.x
let dy = mouse.y - this.y
let distance = Math.sqrt(dx * dx + dy * dy)
let forceDirectionX = dx / distance
let forceDirectionY = dy / distance
// max distance, past that the force will be 0
const maxDistance = 100
let force = (maxDistance - distance) / maxDistance
if (force < 0) force = 0
let directionX = (forceDirectionX * force * this.density)
let directionY = (forceDirectionY * force * this.density)
this.previousX = this.x
this.previousY = this.y
if (distance < mouse.radius + this.size) {
this.x -= directionX
this.y -= directionY
} else {
// Rounded to one decimal number to as x and y cannot be the same (whole decimal-less integer)
// as baseX and baseY by decreasing using a random number / 20
if (Math.round(this.x) !== this.baseX) {
let dx = this.x - this.baseX
this.x -= dx / 20
}
if (Math.round(this.y) !== this.baseY) {
let dy = this.y - this.baseY
this.y -= dy / 20
}
}
}
}
function createParticle(x, y, size) {
if (data.data[(y * 4 * data.width) + (x * 4) + 3] > 128) {
let positionX = x
let positionY = y
let offset = (y * 4 * data.width) + (x * 4)
let color = {
r: data.data[offset],
g: data.data[offset + 1],
b: data.data[offset + 2],
a: data.data[offset + 3]
}
return new Particle(positionX * 4, positionY * 4, color, size)
}
}
// Instead of drawing each Particle one by one, construct an ImageData that can be
// painted into the canvas at once using putImageData()
function updateImageDataWith(particle) {
let x = particle.x
let y = particle.y
let prevX = particle.previousX
let prevY = particle.previousY
let size = particle.size
if (prevX || prevY) {
let prevMinY = Math.round(prevY - size)
let prevMaxY = Math.round(prevY + size)
let prevMinX = Math.round(prevX - size)
let prevMaxX = Math.round(prevX + size)
for (let y = prevMinY; y < prevMaxY; y++){
for (let x = prevMinX; x < prevMaxX; x++) {
if (y < 0 || y > canvasHeight) continue
else if (x < 0 || x > canvasWidth) continue
else {
let offset = y * 4 * canvasWidth + x * 4
imageData.data[offset] = 255
imageData.data[offset + 1] = 255
imageData.data[offset + 2] = 255
imageData.data[offset + 3] = 255
}
}
}
}
let minY = Math.round(y - size)
let maxY = Math.round(y + size)
let minX = Math.round(x - size)
let maxX = Math.round(x + size)
for (let y = minY; y < maxY; y++){
for (let x = minX; x < maxX; x++) {
if (y < 0 || y > canvasHeight) continue
else if (x < 0 || x > canvasWidth) continue
else {
let offset = y * 4 * canvasWidth + x * 4
imageData.data[offset] = particle.color.r
imageData.data[offset + 1] = particle.color.g
imageData.data[offset + 2] = particle.color.b
imageData.data[offset + 3] = particle.color.a
}
}
}
}
function init() {
particleArray = []
imageData = ctx.createImageData(canvasWidth, canvasHeight)
// Initializing imageData to a blank white "page"
for (let data = 1; data <= canvasWidth * canvasHeight * 4; data++) {
imageData.data[data - 1] = data % 4 === 0 ? 255 : 255
}
const size = 2 // Min size is 2
const step = Math.floor(size / 2)
for (let y = 0, y2 = data.height; y < y2; y += step) {
for (let x = 0, x2 = data.width; x < x2; x += step) {
// If particle's alpha value is too low, don't record it
if (data.data[(y * 4 * data.width) + (x * 4) + 3] > 128) {
let newParticle = createParticle(x, y, size)
particleArray.push(newParticle)
updateImageDataWith(newParticle)
}
}
}
}
function animate() {
requestAnimationFrame(animate)
for (let i = 0; i < particleArray.length; i++) {
let imageDataCanUpdateKey = `${Math.round(particleArray[i].x)}${Math.round(particleArray[i].y)}`
particleArray[i].update()
updateImageDataWith(particleArray[i])
}
ctx.putImageData(imageData, 0, 0)
}
init()
animate()
window.addEventListener('resize', e => {
canvas.width = innerWidth
canvas.height = innerHeight
canvasWidth = canvas.width
canvasHeight = canvas.height
init()
})
}
const png = new Image()
png.src = " data:image/png;base64,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"
window.addEventListener('load', e => {
// Ensuring height of image is always 100px
let pngWidth = png.width
let pngHeight = png.height
let divisor = pngHeight / 100
let finalWidth = pngWidth / divisor
let finalHeight = pngHeight / divisor
ctx.drawImage(png, 0, 0, finalWidth, finalHeight)
drawImage(finalWidth, finalHeight)
})
#canvas1 {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
<canvas id="canvas1"></canvas>
UPDATE 2: I have managed to optimize further. Now it can render FullHD image (1920x1080) without downgrading quality (on my PC it runs at about 20fps).
Take a look this code on JSFiddle (you can also tweak values).
Thanks also goes to #Richard (check out his answer) for idea to put all data in ImageData and make a single draw call. Code on JSFiddle is combination of his and mine optimizations (code below is my old code).
EDIT: Updated JSFiddle link, optimized more by spreading work of stationary particles across multiple frames (for given settings it improves performance for about 10%).
Regarding optimization, you won't achieve much by minifying code (in this case) because code that eats up CPU is runtime intensive (executes each frame). Minification is good for optimizing loading, not runtime execution.
Most of time is spent on drawing, and after some investigation I have found few performance optimizations but these are not enough to make big difference (eg. ctx.closePath() can be omitted and this saves some milliseconds).
What you can do is to either reduce resolution of image or skip some pixels in image in order to reduce work.
Additionally you could spread work across multiple frames to improve frame rate (but keep in mind if you spread it on more than few frames you might start seeing flickering).
Fullscreen issue can be solved by simply re-initializing everything on resize event.
Below is code with mentioned optimizations and fullscreen fix. Sample image is 375x375 pixels.
UPDATE: I played a little with code and I managed to improve further performance by optimizing calls (things I mentioned below code snippet). Code is updated with these changes.
var canv
var ctx
//performance critical parameters
const pixelStep = 2 //default 1; increase for images of higher resolution
const maxParticlesToProcessInOneFrame = 20000
//additional performance oriented paramteres
// Max distance, past that the force will be 0
const maxDistance = 144
const mouseRadius = 74
//customization parameters
const ctxFillStyle = 'rgba(0,0,0,.07)'
const speedOfActivatingParticle = 1
const speedOfRestoringParticle = 0.1
const png = new Image();
const mouse = {
x: null,
y: null
}
window.addEventListener('mousemove', (e) => {
mouse.x = event.x + canv.clientLeft;
mouse.y = event.y + canv.clientTop;
})
class Particle {
constructor(x, y, size) {
this.x = x
this.y = y
this.size = pixelStep
this.baseX = x
this.baseY = y
this.density = (Math.random() * 14) + 2
}
draw() {
//ctx.beginPath(); // this creates the sort of force field around the mouse pointer.
//ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.rect(this.x, this.y, this.size * 2, this.size * 2)
//ctx.closePath();
}
update() {
// Collision detection
let dx = mouse.x - this.x;
let dy = mouse.y - this.y;
let distance = Math.sqrt(dx * dx + dy * dy);
if (distance < mouseRadius + this.size) {
let forceDirectionX = dx / distance;
let forceDirectionY = dy / distance;
let force = (maxDistance - distance) / maxDistance;
if (force < 0)
force = 0;
const forceTimesDensity = force * this.density * speedOfActivatingParticle
let directionX = (forceDirectionX * forceTimesDensity);
let directionY = (forceDirectionY * forceTimesDensity); //Ch.this
this.x -= directionX;
this.y -= directionY;
} else {
if (this.x !== this.baseX) {
let dx = this.x - this.baseX;
this.x -= dx * speedOfRestoringParticle; // Speed Particles return to ori
}
if (this.y !== this.baseY) {
let dy = this.y - this.baseY;
this.y -= dy * speedOfRestoringParticle; // Speed Particles return to ori
}
}
this.draw();
}
}
window.addEventListener('resize', initializeCanvas)
window.addEventListener("load", initializeCanvas, {
once: true
})
let animationFrame = null
function initializeCanvas(e) {
cancelAnimationFrame(animationFrame)
canv = document.getElementById('Canv');
ctx = canv.getContext('2d');
canv.width = window.innerWidth;
canv.height = window.innerHeight;
let particles = {}
function drawImage() {
let imageWidth = png.width; //These to values crop if / sum no.
let imageHeight = png.height;
const data = ctx.getImageData(0, 0, imageWidth, imageHeight); //Gets img data for particles
ctx.clearRect(0, 0, canv.width, canv.height); // Clears the original img as its now being stored in the variable data.
function init() {
particles = {}
for (let y = 0, y2 = data.height; y < y2; y += pixelStep) {
for (let x = 0, x2 = data.width; x < x2; x += pixelStep) {
if (data.data[(y * 4 * data.width) + (x * 4) + 3] > 128) {
let positionX = x
let positionY = y
let color = "rgb(" + data.data[(y * 4 * data.width) + (x * 4)] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 1] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 2] + ")";
let particlesArray = particles[color]
if (!particlesArray)
particlesArray = particles[color] = []
particlesArray.push(new Particle(positionX * 2, positionY * 2))
} /* These number effect png size but its to high */
}
}
}
let particlesProcessed = 0
let animateGenerator = animate()
function* animate() {
particlesProcessed = 0
ctx.fillStyle = ctxFillStyle;
ctx.fillRect(0, 0, innerWidth, innerHeight);
let colors = Object.keys(particles)
for (let j = 0; j < colors.length; j++) {
let color = colors[j]
ctx.fillStyle = color
let particlesArray = particles[color]
ctx.beginPath()
for (let i = 0; i < particlesArray.length; i++) {
particlesArray[i].update()
if (++particlesProcessed > maxParticlesToProcessInOneFrame) {
particlesProcessed = 0
ctx.fill()
yield
ctx.beginPath()
}
}
ctx.fill()
}
}
init();
function animateFrame() {
animationFrame = requestAnimationFrame(() => {
if (animateGenerator.next().done) {
animateGenerator = animate()
}
animateFrame()
})
}
animateFrame()
}
console.log('page has loaded');
ctx.drawImage(png, 0, 0, png.width, png.height);
drawImage();
}
png.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXcAAAF3CAMAAABpHvvMAAAC1lBMVEUAAAAAAAAAAADp6ekAAAAAAACcnJwAAAAAAACgoKAAAAAAAAAAAAAAAADq6uoAAAAAAADu7u6Ojo7d3d0AAABISEj5+fmYmJi2traampr6+vqXl5ebm5u3t7dNTU1HR0eRkZG8vLzd3d3f39+zs7NTU1Pw8PDk5OTu7u7////VuTP54C3r6uoAAADVuDDr6+vYvkL38db54Cv12y7r6+3avjL8/PzXuzPw1i/54Cr///3u7e3Wuzn99sP54TTWujcyMjLl3bf43y354TLk27H//vnZwEnWujXh4eH99b3VuTT875YlJSX//vv19fXXvD0GBgZHR0fawU39/Pfr6ufZv0X+/PT7+OnYvDL69uTdx1v8+e7cxVXYvUDy2S78+vH489tbW1vq6OPhzWz64zzu1C9DQ0PXvDvq59yxsbGFhYXfyWHm377cyW7gy2fbw1D65EP//fD+/Ory6b7x57jg0Ij54jcKCgr09PT59N/v5K/t4abezHfZwlTpzzD39/f+++T07Mfp2ZDbxmDr0S/65UkfHx/q6ODfzn/j0HXaxFr65VDgxTHfwzHn4snk2avq3Jnh1Jbiz3HixzH54S/o5NHz68Lh0pD874387oXk0Xn763VsbGzbx2dPT0/cwDL//vbp5dX17864uLjr3p3l1IL87H376m376GD651vnzDD5+fn++t7b29v++dT28dT28NHn4MTs36Hp25Tl033o48717cv+98f99bfw5bP98qjn2Iv76Wf65lQ8PDzdwjL++tju4qr98qHXuzoVFRXn5+fp5tn9867j2Kb88Zzn1ojm1YV9fX12dnbz8/P99LPi1qDlyzAQEBDX19f++M/Nzc3++MvExMTi1ZwrKysaGhrw8PDT09OsrKxWVlbkyTGXl5eQkJD54TO1tbXi16NkZGSnp6eKioqCgoLlyjGdnZ3j4+OkpKRKSkrkyjCpqank1pD353rMpemXAAAAKXRSTlMABAvSFCRoCA5mIh8RFtEnG95Wtxg39GuHavRsaYU1OVSCuLZyMd3D3S9YQ4oAABlBSURBVHja7NQBzoMgDIZh8UcpKzoEtyUmIxCnife/4F+9BAb63KBvvrRhjDHGGGOM1UoUq7knUYnmRsRFKdUWjQ68U3tBqDiA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";
body {
margin: 0;
padding: 0;
}
#Canv {
width: 100vw;
height: 100vh;
filter: blur(1.5px);
}
<canvas id="Canv"></canvas>
If you still need to optimize, you could do some optimization regarding ctx.beginPath(), ctx.fill() and ctx.rect() calls. For example, try to combine sibling pixels (pixels that are next to each other) and render them all in one call. Furthermore, you could merge similar colors in single color, but downside is that image will loose quality (depending on how much colors are merged).
Also (if this is option) you might want to set fixed canvas size rather than dynamically sized.
Disclosure: On my PC given code works nicely, but on others it might still have performance issues. For that reason try to play with pixelStep and maxParticlesToProcessInOneFrame variable values.
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 6 years ago.
Improve this question
If I have two partially transparent images (GIF, PNG, SVG etc.), how do I check if the non-transparent areas of the images intersect?
I'm fine with using canvas if it's necessary. The solution needs to work with all image formats that support transparency. No jQuery please.
Touching
Not Touching
Fast GPU assisted Pixel / Pixel collisions using 2D API.
By using the 2D context globalCompositeOperation you can greatly increase the speed of pixel pixel overlap test.
destination-in
The comp operation "destination-in" will only leave pixels that are visible on the canvas and the image you draw on top of it. Thus you create a canvas, draw one image, then set the comp operation to "destination-in" then draw the second image. If any pixels are overlapping then they will have a non zero alpha. All you do then is read the pixels and if any of them are not zero you know there is an overlap.
More speed
Testing all the pixels in the overlapping area will be slow. You can get the GPU to do some math for you and scale the composite image down. There is some loss as pixels are only 8bit values. This can be overcome by reducing the image in steps and rendering the results several times. Each reduction is like calculating a mean. I scale down by 8 effectively getting the mean of 64 pixels. To stop pixels at the bottom of the range disappearing due to rounding I draw the image several times. I do it 32 time which has the effect of multiplying the alpha channel by 32.
Extending
This method can easily be modified to allow both images to be scaled, skewed and rotated without any major performance hit. You can also use it to test many images with it returning true if all images have pixels overlapping.
Pixels are small so you can get extra speed if you reduce the image size before creating the test canvas in the function. This can give a significant performance boost.
There is a flag reuseCanvas that allows you to reuse the working canvases. If you use the test function a lot (many times a second) then set the flag to true. If you only need the test every now and then then set it to false.
Limits
This method is good for large images that need occasional tests; it is not good for small images and many tests per frame (such as in games where you may need to test 100's of images). For fast (almost perfect pixel) collision tests see Radial Perimeter Test.
The test as a function.
// Use the options to set quality of result
// Slow but perfect
var slowButPerfect = false;
// if reuseCanvas is true then the canvases are resused saving some time
const reuseCanvas = true;
// hold canvas references.
var pixCanvas;
var pixCanvas1;
// returns true if any pixels are overlapping
// img1,img2 the two images to test
// x,y location of img1
// x1,y1 location of img2
function isPixelOverlap(img1,x,y,img2,x1,y1){
var ax,aw,ay,ah,ctx,canvas,ctx1,canvas1,i,w,w1,h,h1;
w = img1.width;
h = img1.height;
w1 = img2.width;
h1 = img2.height;
// function to check if any pixels are visible
function checkPixels(context,w,h){
var imageData = new Uint32Array(context.getImageData(0,0,w,h).data.buffer);
var i = 0;
// if any pixel is not zero then there must be an overlap
while(i < imageData.length){
if(imageData[i++] !== 0){
return true;
}
}
return false;
}
// check if they overlap
if(x > x1 + w1 || x + w < x1 || y > y1 + h1 || y + h < y1){
return false; // no overlap
}
// size of overlapping area
// find left edge
ax = x < x1 ? x1 : x;
// find right edge calculate width
aw = x + w < x1 + w1 ? (x + w) - ax : (x1 + w1) - ax
// do the same for top and bottom
ay = y < y1 ? y1 : y;
ah = y + h < y1 + h1 ? (y + h) - ay : (y1 + h1) - ay
// Create a canvas to do the masking on
if(!reuseCanvas || pixCanvas === undefined){
pixCanvas = document.createElement("canvas");
}
pixCanvas.width = aw;
pixCanvas.height = ah;
ctx = pixCanvas.getContext("2d");
// draw the first image relative to the overlap area
ctx.drawImage(img1,x - ax, y - ay);
// set the composite operation to destination-in
ctx.globalCompositeOperation = "destination-in"; // this means only pixels
// will remain if both images
// are not transparent
ctx.drawImage(img2,x1 - ax, y1 - ay);
ctx.globalCompositeOperation = "source-over";
// are we using slow method???
if(slowButPerfect){
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // no then release referance
}
return checkPixels(ctx,aw,ah);
}
// now draw over its self to amplify any pixels that have low alpha
for(var i = 0; i < 32; i++){
ctx.drawImage(pixCanvas,0,0);
}
// create a second canvas 1/8th the size but not smaller than 1 by 1
if(!reuseCanvas || pixCanvas1 === undefined){
pixCanvas1 = document.createElement("canvas");
}
ctx1 = pixCanvas1.getContext("2d");
// reduced size rw, rh
rw = pixCanvas1.width = Math.max(1,Math.floor(aw/8));
rh = pixCanvas1.height = Math.max(1,Math.floor(ah/8));
// repeat the following untill the canvas is just 64 pixels
while(rw > 8 && rh > 8){
// draw the mask image several times
for(i = 0; i < 32; i++){
ctx1.drawImage(
pixCanvas,
0,0,aw,ah,
Math.random(),
Math.random(),
rw,rh
);
}
// clear original
ctx.clearRect(0,0,aw,ah);
// set the new size
aw = rw;
ah = rh;
// draw the small copy onto original
ctx.drawImage(pixCanvas1,0,0);
// clear reduction canvas
ctx1.clearRect(0,0,pixCanvas1.width,pixCanvas1.height);
// get next size down
rw = Math.max(1,Math.floor(rw / 8));
rh = Math.max(1,Math.floor(rh / 8));
}
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // release ref
pixCanvas1 = undefined;
}
// check for overlap
return checkPixels(ctx,aw,ah);
}
The demo (Use full page)
The demo lets you compare the two methods. The mean time for each test is displayed. (will display NaN if no tests done)
For the best results view the demo full page.
Use left or right mouse buttons to test for overlap. Move the splat image over the other to see overlap result. On my machine I am getting about 11ms for the slow test and 0.03ms for the quick test (using Chrome, much faster on Firefox).
I have not spent much time testing how fast I can get it to work but there is plenty of room to increase the speed by reducing the number of time the images are drawn over each other. At some point faint pixels will be lost.
// Use the options to set quality of result
// Slow but perfect
var slowButPerfect = false;
const reuseCanvas = true;
var pixCanvas;
var pixCanvas1;
// returns true if any pixels are overlapping
function isPixelOverlap(img1,x,y,w,h,img2,x1,y1,w1,h1){
var ax,aw,ay,ah,ctx,canvas,ctx1,canvas1,i;
// function to check if any pixels are visible
function checkPixels(context,w,h){
var imageData = new Uint32Array(context.getImageData(0,0,w,h).data.buffer);
var i = 0;
// if any pixel is not zero then there must be an overlap
while(i < imageData.length){
if(imageData[i++] !== 0){
return true;
}
}
return false;
}
// check if they overlap
if(x > x1 + w1 || x + w < x1 || y > y1 + h1 || y + h < y1){
return false; // no overlap
}
// size of overlapping area
// find left edge
ax = x < x1 ? x1 : x;
// find right edge calculate width
aw = x + w < x1 + w1 ? (x + w) - ax : (x1 + w1) - ax
// do the same for top and bottom
ay = y < y1 ? y1 : y;
ah = y + h < y1 + h1 ? (y + h) - ay : (y1 + h1) - ay
// Create a canvas to do the masking on
if(!reuseCanvas || pixCanvas === undefined){
pixCanvas = document.createElement("canvas");
}
pixCanvas.width = aw;
pixCanvas.height = ah;
ctx = pixCanvas.getContext("2d");
// draw the first image relative to the overlap area
ctx.drawImage(img1,x - ax, y - ay);
// set the composite operation to destination-in
ctx.globalCompositeOperation = "destination-in"; // this means only pixels
// will remain if both images
// are not transparent
ctx.drawImage(img2,x1 - ax, y1 - ay);
ctx.globalCompositeOperation = "source-over";
// are we using slow method???
if(slowButPerfect){
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // no then release reference
}
return checkPixels(ctx,aw,ah);
}
// now draw over its self to amplify any pixels that have low alpha
for(var i = 0; i < 32; i++){
ctx.drawImage(pixCanvas,0,0);
}
// create a second canvas 1/8th the size but not smaller than 1 by 1
if(!reuseCanvas || pixCanvas1 === undefined){
pixCanvas1 = document.createElement("canvas");
}
ctx1 = pixCanvas1.getContext("2d");
// reduced size rw, rh
rw = pixCanvas1.width = Math.max(1,Math.floor(aw/8));
rh = pixCanvas1.height = Math.max(1,Math.floor(ah/8));
// repeat the following untill the canvas is just 64 pixels
while(rw > 8 && rh > 8){
// draw the mask image several times
for(i = 0; i < 32; i++){
ctx1.drawImage(
pixCanvas,
0,0,aw,ah,
Math.random(),
Math.random(),
rw,rh
);
}
// clear original
ctx.clearRect(0,0,aw,ah);
// set the new size
aw = rw;
ah = rh;
// draw the small copy onto original
ctx.drawImage(pixCanvas1,0,0);
// clear reduction canvas
ctx1.clearRect(0,0,pixCanvas1.width,pixCanvas1.height);
// get next size down
rw = Math.max(1,Math.floor(rw / 8));
rh = Math.max(1,Math.floor(rh / 8));
}
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // release ref
pixCanvas1 = undefined;
}
// check for overlap
return checkPixels(ctx,aw,ah);
}
function rand(min,max){
if(max === undefined){
max = min;
min = 0;
}
var r = Math.random() + Math.random() + Math.random() + Math.random() + Math.random();
r += Math.random() + Math.random() + Math.random() + Math.random() + Math.random();
r /= 10;
return (max-min) * r + min;
}
function createImage(w,h){
var c = document.createElement("canvas");
c.width = w;
c.height = h;
c.ctx = c.getContext("2d");
return c;
}
function createCSSColor(h,s,l,a) {
var col = "hsla(";
col += (Math.floor(h)%360) + ",";
col += Math.floor(s) + "%,";
col += Math.floor(l) + "%,";
col += a + ")";
return col;
}
function createSplat(w,h,hue, hue2){
w = Math.floor(w);
h = Math.floor(h);
var c = createImage(w,h);
if(hue2 !== undefined) {
c.highlight = createImage(w,h);
}
var maxSize = Math.min(w,h)/6;
var pow = 5;
while(maxSize > 4 && pow > 0){
var count = Math.min(100,Math.pow(w * h,1/pow) / 2);
while(count-- > 0){
const rhue = rand(360);
const s = rand(25,75);
const l = rand(25,75);
const a = (Math.random()*0.8+0.2).toFixed(3);
const size = rand(4,maxSize);
const x = rand(size,w - size);
const y = rand(size,h - size);
c.ctx.fillStyle = createCSSColor(rhue + hue, s, l, a);
c.ctx.beginPath();
c.ctx.arc(x,y,size,0,Math.PI * 2);
c.ctx.fill();
if (hue2 !== undefined) {
c.highlight.ctx.fillStyle = createCSSColor(rhue + hue2, s, l, a);
c.highlight.ctx.beginPath();
c.highlight.ctx.arc(x,y,size,0,Math.PI * 2);
c.highlight.ctx.fill();
}
}
pow -= 1;
maxSize /= 2;
}
return c;
}
var splat1,splat2;
var slowTime = 0;
var slowCount = 0;
var notSlowTime = 0;
var notSlowCount = 0;
var onResize = function(){
ctx.font = "14px arial";
ctx.textAlign = "center";
splat1 = createSplat(rand(w/2, w), rand(h/2, h), 0, 100);
splat2 = createSplat(rand(w/2, w), rand(h/2, h), 100);
}
function display(){
ctx.clearRect(0,0,w,h)
ctx.setTransform(1.8,0,0,1.8,w/2,0);
ctx.fillText("Fast GPU assisted Pixel collision test using 2D API",0, 14)
ctx.setTransform(1,0,0,1,0,0);
ctx.fillText("Hold left mouse for Traditional collision test. Time : " + (slowTime / slowCount).toFixed(3) + "ms",w /2 , 28 + 14)
ctx.fillText("Hold right (or CTRL left) mouse for GPU assisted collision. Time: "+ (notSlowTime / notSlowCount).toFixed(3) + "ms",w /2 , 28 + 28)
if((mouse.buttonRaw & 0b101) === 0) {
ctx.drawImage(splat1, w / 2 - splat1.width / 2, h / 2 - splat1.height / 2)
ctx.drawImage(splat2, mouse.x - splat2.width / 2, mouse.y - splat2.height / 2);
} else if(mouse.buttonRaw & 0b101){
if((mouse.buttonRaw & 1) && !mouse.ctrl){
slowButPerfect = true;
}else{
slowButPerfect = false;
}
var now = performance.now();
var res = isPixelOverlap(
splat1,
w / 2 - splat1.width / 2, h / 2 - splat1.height / 2,
splat1.width, splat1.height,
splat2,
mouse.x - splat2.width / 2, mouse.y - splat2.height / 2,
splat2.width,splat2.height
)
var time = performance.now() - now;
ctx.drawImage(res ? splat1.highlight: splat1, w / 2 - splat1.width / 2, h / 2 - splat1.height / 2)
ctx.drawImage(splat2, mouse.x - splat2.width / 2, mouse.y - splat2.height / 2);
if(slowButPerfect){
slowTime += time;
slowCount += 1;
}else{
notSlowTime = time;
notSlowCount += 1;
}
if(res){
ctx.setTransform(2,0,0,2,mouse.x,mouse.y);
ctx.fillText("Overlap detected",0,0)
ctx.setTransform(1,0,0,1,0,0);
}
//mouse.buttonRaw = 0;
}
}
// Boilerplate code below
const RESIZE_DEBOUNCE_TIME = 100;
var w, h, cw, ch, canvas, ctx, mouse, createCanvas, resizeCanvas, setGlobals, globalTime = 0, resizeCount = 0;
var firstRun = true;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
}
e.preventDefault();
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element = undefined;
m.active = false;
}
}
return mouse;
})();
resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);
function update1(timer) { // Main update loop
if(ctx === undefined){
return;
}
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update1);
}
requestAnimationFrame(update1);
Hi I want to make a blur effect particle like this:
Can I use shadowBlur and shadowOffsetX/shadowOffsetY to do this? The actual shine will glow and fade a little bit repeatedly, so if I have to write some kind of animation how can I achieve this?
I have tried this code (jsfiddle example) but it doesn't look like the effect. So I wonder how to blur and glow the particle at the same time?
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ra = window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.oRequestAnimationFrame
|| window.msRequestAnimationFrame
|| function(callback) {
window.setTimeout(callback, 1000 / 60);
};
class Particle {
constructor(options) {
this.ctx = options.context;
this.x = options.x;
this.y = options.y;
this.radius = options.radius;
this.lightSize = this.radius;
this.color = options.color;
this.lightDirection = true;
}
glow() {
const lightSpeed = 0.5;
this.lightSize += this.lightDirection ? lightSpeed : -lightSpeed;
if (this.lightSize > this.radius || this.lightSize < this.radius) {
this.lightDirection = !this.lightDirection;
}
}
render() {
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.glow();
this.ctx.globalAlpha = 0.5;
this.ctx.fillStyle = this.color;
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.lightSize,
0, Math.PI * 2
);
this.ctx.fill();
this.ctx.globalAlpha = 0.62;
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius * 0.7, 0, Math.PI * 2);
this.ctx.shadowColor = this.color;
this.ctx.shadowBlur = 6;
this.ctx.shadowOffsetX = 0;
this.ctx.shadowOffsetY = 0;
this.ctx.fill();
}
}
var particle = new Particle({
context: ctx,
x: 60,
y: 80,
radius: 12,
color: '#4d88ff'
});
function run() {
particle.render();
ra(run);
}
run();
<canvas id='canvas'></canvas>
There are several ways to do this. For a particle system my option is to pre render the blur using a blur filter. A common filter is the convolution filter. It uses a small array to determine the amount neighboring pixels contribute to each pixel of the image. You are best to look up convolution functions to understand it.
Wiki Convolution and Wiki Gaussian blur for more info.
I am not much of a fan of the standard Gaussian blur or the convolution filter used so in the demo snippet below you can find my version that I think creates a much better blur. The convolution blur filter is procedurally created and is in the imageTools object.
To use create a filter pass an object with properties size the blur amount in pixels and power is the strength. Lower powers is less spread on the blur.
// image must be loaded or created
var blurFilter = imageTools.createBlurConvolutionArray({size:17,power:1}); // size must be greater than 2 and must be odd eg 3,5,7,9...
// apply the convolution filter on the image. The returned image may be a new
//image if the input image does not have a ctx property pointing to a 2d canvas context
image = imageTools.applyConvolutionFilter(image,blurFilter);
In the demo I create a image, draw a circle on it, copy it and pad it so that there is room for the blur. Then create a blur filter and apply it to the image.
When I render the particles I first draw all the unblurred images, then draw the blurred copies with the ctx.globalCompositeOperation = "screen"; so that they have a shine. To vary the amount of shine I use the ctx.globalAlpha to vary the intensity of the rendered blurred image. To improve the FX I have drawn the blur image twice, once with oscillating scale and next at fixed scale and alpha.
The demo is simple, image tools can be found at the top. Then there is some stuff to setup the canvas and handle resize event. Then there is the code that creates the images, and apply the filters. Then starts the render adds some particles and renders everything.
Look in the function drawParticles for how I draw everything.
imageTools has all the image functions you will need. The imageTools.applyConvolutionFilter will apply any filter (sharpen, outline, and many more) you just need to create the appropriate filter. The apply uses the photon count colour model so gives a very high quality result especially for blurs type effects. (though for sharpen you may want to get in and change the squaring of the RGB values, I personally like it other do not)
The blur filter is not fast so if you apply it to larger images It would be best that you break it up in so you do not block the page execution.
A cheap way to get a blur is to copy the image to blur to a smaller version of itself, eg 1/4 then render it scaled back to normal size, the canvas will apply bilinear filtering on the image give a blur effect. Not the best quality but for most situations it is indistinguishable from the more sophisticated blur that I have presented.
UPDATE
Change the code so that the particles have a bit of a 3dFX to show that the blur can work up to larger scales. The blue particles are 32 by 32 image and the blur is 9 pixels with the blur image being 50by 50 pixels.
var imageTools = (function () {
var tools = {
canvas : function (width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage : function (width, height) {
var image = this.canvas(width, height);
image.ctx = image.getContext("2d");
return image;
},
image2Canvas : function (img) {
var image = this.canvas(img.width, img.height);
image.ctx = image.getContext("2d");
image.drawImage(img, 0, 0);
return image;
},
padImage : function(img,amount){
var image = this.canvas(img.width + amount * 2, img.height + amount * 2);
image.ctx = image.getContext("2d");
image.ctx.drawImage(img, amount, amount);
return image;
},
getImageData : function (image) {
return (image.ctx || (this.image2Canvas(image).ctx)).getImageData(0, 0, image.width, image.height);
},
putImageData : function (image, imgData){
(image.ctx || (this.image2Canvas(image).ctx)).putImageData(imgData,0, 0);
return image;
},
createBlurConvolutionArray : function(options){
var i, j, d; // misc vars
var filterArray = []; // the array to create
var size = options.size === undefined ? 3: options.size; // array size
var center = Math.floor(size / 2); // center of array
// the power ? needs descriptive UI options
var power = options.power === undefined ? 1: options.power;
// dist to corner
var maxDist = Math.sqrt(center * center + center * center);
var dist = 0; // distance sum
var sum = 0; // weight sum
var centerWeight; // center calculated weight
var totalDistance; // calculated total distance from center
// first pass get the total distance
for(i = 0; i < size; i++){
for(j = 0; j < size; j++){
d = (maxDist-Math.sqrt((center-i)*(center-i)+(center-j)*(center-j)));
d = Math.pow(d,power)
dist += d;
}
}
totalDistance = dist; // total distance to all points;
// second pass get the total weight of all but center
for(i = 0; i < size; i++){
for(j = 0; j < size; j++){
d = (maxDist-Math.sqrt((center-i)*(center-i)+(center-j)*(center-j)));
d = Math.pow(d,power)
d = d/totalDistance;
sum += d;
}
}
var scale = 1/sum;
sum = 0; // used to check
for(i = 0; i < size; i++){
for(j = 0; j < size; j++){
d = (maxDist-Math.sqrt((center-i)*(center-i)+(center-j)*(center-j)));
d = Math.pow(d,power)
d = d/totalDistance;
filterArray.push(d*scale);
}
}
return filterArray;
},
applyConvolutionFilter : function(image,filter){
imageData = this.getImageData(image);
imageDataResult = this.getImageData(image);
var w = imageData.width;
var h = imageData.height;
var data = imageData.data;
var data1 = imageDataResult.data;
var side = Math.round(Math.sqrt(filter.length));
var halfSide = Math.floor(side/2);
var r,g,b,a,c;
for(var y = 0; y < h; y++){
for(var x = 0; x < w; x++){
var ind = y*4*w+x*4;
r = 0;
g = 0;
b = 0;
a = 0;
for (var cy=0; cy<side; cy++) {
for (var cx=0; cx<side; cx++) {
var scy = y + cy - halfSide;
var scx = x + cx - halfSide;
if (scy >= 0 && scy < h && scx >= 0 && scx < w) {
var srcOff = (scy*w+scx)*4;
var wt = filter[cy*side+cx];
r += data[srcOff+0] * data[srcOff+0] * wt;
g += data[srcOff+1] * data[srcOff+1] * wt;
b += data[srcOff+2] * data[srcOff+2] * wt;
a += data[srcOff+3] * data[srcOff+3] * wt;
}
}
}
data1[ind+0] = Math.sqrt(Math.max(0,r));
data1[ind+1] = Math.sqrt(Math.max(0,g));
data1[ind+2] = Math.sqrt(Math.max(0,b));
data1[ind+3] = Math.sqrt(Math.max(0,a));
}
}
return this.putImageData(image,imageDataResult);
}
};
return tools;
})();
/** SimpleFullCanvasMouse.js begin **/
const CANVAS_ELEMENT_ID = "canv";
const U = undefined;
var w, h, cw, ch; // short cut vars
var canvas, ctx;
var globalTime = 0;
var createCanvas, resizeCanvas, setGlobals;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
c.id = CANVAS_ELEMENT_ID;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === U) { canvas = createCanvas(); }
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals(); }
}
setGlobals = function(){
cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2;
if(particles && particles.length > 0){
particles.length = 0;
}
}
resizeCanvas(); // create and size canvas
window.addEventListener("resize",resizeCanvas); // add resize event
const IMAGE_SIZE = 32;
const IMAGE_SIZE_HALF = 16;
const GRAV = 2001;
const NUM_PARTICLES = 90;
var background = imageTools.createImage(8,8);
var grad = ctx.createLinearGradient(0,0,0,8);
grad.addColorStop(0,"#000");
grad.addColorStop(1,"#048");
background.ctx.fillStyle = grad;
background.ctx.fillRect(0,0,8,8);
var circle = imageTools.createImage(IMAGE_SIZE,IMAGE_SIZE);
circle.ctx.fillStyle = "#5BF";
circle.ctx.arc(IMAGE_SIZE_HALF, IMAGE_SIZE_HALF, IMAGE_SIZE_HALF -2,0, Math.PI * 2);
circle.ctx.fill();
var blurFilter = imageTools.createBlurConvolutionArray({size:9,power:1}); // size must be greater than 2 and must be odd eg 3,5,7,9...
var blurCircle = imageTools.padImage(circle,9);
blurCircle = imageTools.applyConvolutionFilter(blurCircle,blurFilter)
var sun = imageTools.createImage(64,64);
grad = ctx.createRadialGradient(32,32,0,32,32,32);
grad.addColorStop(0,"#FF0");
grad.addColorStop(1,"#A40");
sun.ctx.fillStyle = grad;
sun.ctx.arc(32,32,32 -2,0, Math.PI * 2);
sun.ctx.fill();
var sunBlur = imageTools.padImage(sun,17);
blurFilter = imageTools.createBlurConvolutionArray({size:17,power:1}); // size must be greater than 2 and must be odd eg 3,5,7,9...
sunBlur = imageTools.applyConvolutionFilter(sunBlur,blurFilter);
var particles = [];
var createParticle = function(x,y,dx,dy){
var dir = Math.atan2(y-ch,x-cw);
var dist = Math.sqrt(Math.pow(y-ch,2)+Math.pow(x-cw,2));
var v = Math.sqrt(GRAV / dist); // get apporox orbital speed
return {
x : x,
y : y,
dx : dx + Math.cos(dir + Math.PI/2) * v, // set orbit speed at tangent
dy : dy + Math.sin(dir + Math.PI/2) * v,
s : (Math.random() + Math.random() + Math.random())/4 + 0.5, // scale
v : (Math.random() + Math.random() + Math.random()) / 3 + 2, // glow vary rate
};
}
var depthSort = function(a,b){
return b.y - a.y;
}
var updateParticles = function(){
var i,p,f,dist,dir;
for(i = 0; i < particles.length; i ++){
p = particles[i];
dist = Math.sqrt(Math.pow(cw-p.x,2)+Math.pow(ch-p.y,2));
dir = Math.atan2(ch-p.y,cw-p.x);
f = GRAV * 1 / (dist * dist);
p.dx += Math.cos(dir) * f;
p.dy += Math.sin(dir) * f;
p.x += p.dx;
p.y += p.dy;
p.rx = ((p.x - cw ) / (p.y + h)) * h + cw;
p.ry = ((p.y - ch ) / (p.y + h)) * h * -0.051+ ch;
//p.ry = ((h-p.y) - ch) * 0.1 + ch;
p.rs = (p.s / (p.y + h)) * h
}
particles.sort(depthSort)
}
var drawParticles = function(){
var i,j,p,f,dist,dir;
// draw behind the sun
for(i = 0; i < particles.length; i ++){
p = particles[i];
if(p.y - ch < 0){
break;
}
ctx.setTransform(p.rs,0,0,p.rs,p.rx,p.ry);
ctx.drawImage(circle,-IMAGE_SIZE_HALF,-IMAGE_SIZE_HALF);
}
// draw glow for behind the sun
ctx.globalCompositeOperation = "screen";
var iw = -blurCircle.width/2;
for(j = 0; j < i; j ++){
p = particles[j];
ctx.globalAlpha = ((Math.sin(globalTime / (50 * p.v)) + 1) / 2) * 0.6 + 0.4;
var scale = (1-(Math.sin(globalTime / (50 * p.v)) + 1) / 2) * 0.6 + 0.6;
ctx.setTransform(p.rs * 1.5 * scale,0,0,p.rs * 1.5* scale,p.rx,p.ry);
ctx.drawImage(blurCircle,iw,iw);
// second pass to intensify the glow
ctx.globalAlpha = 0.7;
ctx.setTransform(p.rs * 1.1,0,0,p.rs * 1.1,p.rx,p.ry);
ctx.drawImage(blurCircle,iw,iw);
}
// draw the sun
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,cw,ch);
ctx.drawImage(sun,-sun.width/2,-sun.height/2);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "screen";
ctx.setTransform(1,0,0,1,cw,ch);
ctx.drawImage(sunBlur,-sunBlur.width/2,-sunBlur.height/2);
var scale = Math.sin(globalTime / 100) *0.5 + 1;
ctx.globalAlpha = (Math.cos(globalTime / 100) + 1) * 0.2 + 0.4;;
ctx.setTransform(1 + scale,0,0,1 + scale,cw,ch);
ctx.drawImage(sunBlur,-sunBlur.width/2,-sunBlur.height/2);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
// draw in front the sun
for(j = i; j < particles.length; j ++){
p = particles[j];
if(p.y > -h){ // don't draw past the near view plane
ctx.setTransform(p.rs,0,0,p.rs,p.rx,p.ry);
ctx.drawImage(circle,-IMAGE_SIZE_HALF,-IMAGE_SIZE_HALF);
}
}
ctx.globalCompositeOperation = "screen";
var iw = -blurCircle.width/2;
for(j = i; j < particles.length; j ++){
p = particles[j];
if(p.y > -h){ // don't draw past the near view plane
ctx.globalAlpha = ((Math.sin(globalTime / (50 * p.v)) + 1) / 2) * 0.6 + 0.4;
var scale = (1-(Math.sin(globalTime / (50 * p.v)) + 1) / 2) * 0.6 + 0.6;
ctx.setTransform(p.rs * 1.5 * scale,0,0,p.rs * 1.5* scale,p.rx,p.ry);
ctx.drawImage(blurCircle,iw,iw);
// second pass to intensify the glow
ctx.globalAlpha = 0.7;
ctx.setTransform(p.rs * 1.1,0,0,p.rs * 1.1,p.rx,p.ry);
ctx.drawImage(blurCircle,iw,iw);
}
}
ctx.globalCompositeOperation = "source-over";
}
var addParticles = function(count){
var ww = (h-10)* 2;
var cx = cw - ww/2;
var cy = ch - ww/2;
for(var i = 0; i < count; i ++){
particles.push(createParticle(cx + Math.random() * ww,cy + Math.random() * ww, Math.random() - 0.5, Math.random() - 0.5));
}
}
function display(){ // put code in here
if(particles.length === 0){
addParticles(NUM_PARTICLES);
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.drawImage(background,0,0,w,h)
updateParticles();
drawParticles();
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
}
function update(timer){ // Main update loop
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
requestAnimationFrame(update);
/** SimpleFullCanvasMouse.js end **/
I'm trying to rotate an element around another element. I've put this code together, and even though the code works fine, I have a little problem with the element's rotation. It's not very accurate. The blue dot leaves the orbit when at the bottom.
I've played with the code but couldn't fix it. You can check the fiddle example.
rotateObj function taken from Felix Eve's answer.
function markCircleCenter(circ, orig) {
if (typeof circ == "string") {
var elm = document.getElementById(circ);
} else { var circle = circ; }
if (typeof orig == "string") {
var mark = document.getElementById(orig);
} else { var elm = orig; }
var width = parseInt(window.getComputedStyle(elm).width, 10);
var height = parseInt(window.getComputedStyle(elm).height, 10);
var top = parseInt(window.getComputedStyle(elm).top, 10);
var left = parseInt(window.getComputedStyle(elm).left, 10);
var markWidth = parseInt(window.getComputedStyle(mark).width, 10);
var markHeight = parseInt(window.getComputedStyle(mark).height, 10);
var circle = {
circle: elm,
x: width/2 + left,
y: height/2 + top,
};
mark.style.position = "absolute";
mark.style.top = circle.y - markWidth/2 + "px";
mark.style.left = circle.x - markHeight/2 + "px";
document.body.insertBefore(mark, elm.nextSibling);
return circle;
}
function placeObjIntoOrbit(obj) {
if (typeof obj == "string") {
var obj = document.getElementById(obj);
} else { var obj = obj; }
var objPos = {
obj: obj,
x: origin.x,
y: parseInt(window.getComputedStyle(origin.circle).top, 10) - parseInt(window.getComputedStyle(obj).width, 10) / 2,
};
obj.style.position = "absolute";
obj.style.top = objPos.y + "px";
obj.style.left = objPos.x + "px";
return objPos;
}
function rotateObj(pointX, pointY, originX, originY, angle) {
var angle = angle * Math.PI / 180.0;
return {
x: Math.cos(angle) * (pointX-originX) - Math.sin(angle) * (pointY-originY) + originX,
y: Math.sin(angle) * (pointX-originX) + Math.cos(angle) * (pointY-originY) + originY,
};
}
var angle = 0;
var origin = markCircleCenter("circle", "origin");
var point = placeObjIntoOrbit("point");
function spin() {
if (angle >= 360) { angle = 0; }
angle += 2;
var newCoor = rotateObj(point.x, point.y, origin.x, origin.y, angle);
point.obj.style.left = newCoor.x + "px";
point.obj.style.top = newCoor.y + "px";
}
setInterval("spin('point')", 1000/30);
You are computing the center of the point but positioning it using its top-left point. You need to take the dimension of the point into account when you position it. Try changing the last two lines in function spin() to:
point.obj.style.left = (newCoor.x - 4) + "px";
point.obj.style.top = (newCoor.y - 4) + "px";
(You should get rid of the "- 4" and substitute half the width and height of the point, but this gets the idea across more tersely.)
I'm trying to create a UI that has a lot of items in circles. Sometimes these circles will have related circles that should be displayed around them.
I was able to cobble together something that works, here.
The problem is that the outer circles start near 0 degrees, and I'd like them to start at an angle supplied by the consumer of the function/library. I was never a star at trigonometry, or geometry, so I could use a little help.
As you can see in the consuming code, there is a setting: startingDegree: 270 that the function getPosition should honor, but I haven't been able to figure out how.
Update 04/02/2014:
as I mentioned in my comment to Salix alba, I wasn't clear above, but what I needed was to be able to specify the radius of the satellite circles, and to have them go only partly all the way around. Salix gave a solution that calculates the size the satellites need to be to fit around the center circle uniformly.
Using some of the hints in Salix's answer, I was able to achieve the desired result... and have an extra "mode," thanks to Salix, in the future.
The working, though still rough, solution is here: http://jsfiddle.net/RD4RZ/11/. Here is the entire code (just so it's all on SO):
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script type="text/javascript" src="//code.jquery.com/jquery-1.10.1.js"></script>
<style type="text/css">
.circle
{
position: absolute;
width: 100px;
height: 100px;
background-repeat: no-repeat;background-position: center center;
border: 80px solid #a19084;
border-radius: 50%;
-moz-border-radius: 50%;
}
.sm
{
border: 2px solid #a19084;
}
</style>
<script type="text/javascript">//<![CDATA[
$(function () {
function sind(x) {
return Math.sin(x * Math.PI / 180);
}
/*the law of cosines:
cc = aa + bb - 2ab cos(C), where c is the satellite diameter a and b are the legs
solving for cos C, cos C = ( aa + bb - cc ) / 2ab
Math.acos((a * a + b * b - c * c) / (2 * a * b)) = C
*/
function solveAngle(a, b, c) { // Returns angle C using law of cosines
var temp = (a * a + b * b - c * c) / (2 * a * b);
if (temp >= -1 && temp <= 1)
return radToDeg(Math.acos(temp));
else
throw "No solution";
}
function radToDeg(x) {
return x / Math.PI * 180;
}
function degToRad(x) {
return x * (Math.PI / 180);
}
var satellite = {
//settings must have: collection (array), itemDiameter (number), minCenterDiameter (number), center (json with x, y numbers)
//optional: itemPadding (number), evenDistribution (boolean), centerPadding (boolean), noOverLap (boolean)
getPosition: function (settings) {
//backwards compat
settings.centerPadding = settings.centerPadding || settings.itemPadding;
settings.noOverLap = typeof settings.noOverLap == 'undefined' ? true : settings.noOverLap;
settings.startingDegree = settings.startingDegree || 270;
settings.startSatellitesOnEdge = typeof settings.startSatellitesOnEdge == 'undefined' ? true : settings.startSatellitesOnEdge;
var itemIndex = $.inArray(settings.item, settings.collection);
var itemCnt = settings.collection.length;
var satelliteSide = settings.itemDiameter + (settings.itemSeparation || 0) + (settings.itemPadding || 0);
var evenDistribution = typeof settings.evenDistribution == 'undefined' ? true : settings.evenDistribution;
var degreeOfSeparation = (360 / itemCnt);
/*
we know all three sides:
one side is the diameter of the satellite itself (plus any padding). the other two
are the parent radius + the radius of the satellite itself (plus any padding).
given that, we need to find the angle of separation using the law of cosines (solveAngle)
*/
//if (!evenDistribution) {
var side1 = ((satelliteSide / 2)) + ((settings.minCenterDiameter + (2 * settings.centerPadding)) / 2);
var side2 = satelliteSide;;
var degreeOfSeparationBasedOnSatellite = solveAngle(side1, side1, side2); //Math.acos(((((side1 * side1) + (side2 * side2)) - (side2 * side2)) / (side2 * side2 * 2)) / 180 * Math.PI) * Math.PI;
degreeOfSeparation = evenDistribution? degreeOfSeparation: settings.noOverLap ? Math.min(degreeOfSeparation, degreeOfSeparationBasedOnSatellite) : degreeOfSeparationBasedOnSatellite;
//}
//angle-angle-side
//a-A-B
var a = satelliteSide;
var A = degreeOfSeparation;
/*
the three angles of any triangle add up to 180. We know one angle (degreeOfSeparation)
and we know the other two are equivalent to each other, so...
*/
var B = (180 - A) / 2;
//b is length necessary to fit all satellites, might be too short to be outside of base circle
var b = a * sind(B) / sind(A);
var offset = (settings.itemDiameter / 2) + (settings.itemPadding || 0); // 1; //
var onBaseCircleLegLength = ((settings.minCenterDiameter / 2) + settings.centerPadding) + offset;
var offBase = false;
if (b > onBaseCircleLegLength) {
offBase = true;
}
b = settings.noOverLap ? Math.max(b, onBaseCircleLegLength) : onBaseCircleLegLength;
var radianDegree = degToRad(degreeOfSeparation);
//log('b=' + b);
//log('settings.center.x=' + settings.center.x);
//log('settings.center.y=' + settings.center.y);
var degreeOffset = settings.startingDegree;
if (settings.startSatellitesOnEdge) {
degreeOffset += ((offBase ? degreeOfSeparation : degreeOfSeparationBasedOnSatellite) / 2);
}
var i = ((Math.PI * degreeOffset) / 180) + (radianDegree * itemIndex);// + (degToRad(degreeOfSeparationBasedOnSatellite) / 2); //(radianDegree) * (itemIndex);
var x = (Math.cos(i) * b) + (settings.center.x - offset);
var y = (Math.sin(i) * b) + (settings.center.y - offset);
return { 'x': Math.round(x), 'y': Math.round(y) };
}
,
/* if we ever want to size satellite by how many need to fit tight around the base circle:
x: function calcCircles(n) {
circles.splice(0); // clear out old circles
var angle = Math.PI / n;
var s = Math.sin(angle);
var r = baseRadius * s / (1 - s);
console.log(angle);
console.log(s);
console.log(r);
console.log(startAngle);
console.log(startAngle / (Math.PI * 2));
for (var i = 0; i < n; ++i) {
var phi = ((Math.PI * startAngle) / 180) + (angle * i * 2);
var cx = 150 + (baseRadius + r) * Math.cos(phi);
var cy = 150 + (baseRadius + r) * Math.sin(phi);
circles.push(new Circle(cx, cy, r));
}
},
*/
//settings must have: collection (array), itemDiameter (number), minCenterDiameter (number), center (json with x, y numbers)
//optional: itemPadding (number), evenDistribution (boolean), centerPadding (boolean), noOverLap (boolean)
getAllPositions: function (settings) {
var point;
var points = [];
var collection = settings.collection;
for (var i = 0; i < collection.length; i++) {
settings.item = collection[i]
points.push(satellite.getPosition(settings));
}
return points;
}
};
var el = $("#center"), cnt = 10, arr = [], itemDiameter= 100;
for (var c = 0; c < cnt; c++) {
arr.push(c);
}
var settings = {
collection: arr,
itemDiameter: itemDiameter,
minCenterDiameter: el.width(),
center: { x: el.width() / 2, y: el.width() / 2 },
itemPadding: 2,
evenDistribution: false,
centerPadding: parseInt(el.css("border-width")),
noOverLap: false,
startingDegree: 270
};
var points = satellite.getAllPositions(settings);
for (var i = 0; i < points.length; i++) {
var $newdiv1 = $("<div></div>");
var div = el.append($newdiv1);
$newdiv1.addClass("circle").addClass("sm");
$newdiv1.text(i);
$newdiv1.css({ left: points[i].x, top: points[i].y, width: itemDiameter +'px', height: itemDiameter +'px' });
}
});//]]>
</script>
</head>
<body>
<div id="center" class="circle" style="left:250px;top:250px" >
</div>
</body>
</html>
The central bit you need to work out is radius of the small circles. If you have R for radius of the central circle and you want to fit n smaller circles around it. Let the as yet unknown radius of the small circle be r. We can construct a right angle triangle with one corner in the center of the big circle one in the center of the small circle and one which is where a line from the center is tangent to the small circle. This will be a right angle. The angle at the center is a the hypotenuse has length R+r the opposite is r and we don't need the adjacent. Using trig
sin(a) = op / hyp = r / (R + r)
rearrange
(R+r) sin(a) = r
R sin(a) + r sin(a) = r
R sin(a) = r - r sin(a)
R sin(a) = (1 - sin(a)) r
r = R sin(a) / ( 1 - sin(a))
once we have r we are pretty much done.
You can see this as a fiddle http://jsfiddle.net/SalixAlba/7mAAS/
// canvas and mousedown related variables
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var $canvas = $("#canvas");
var canvasOffset = $canvas.offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var scrollX = $canvas.scrollLeft();
var scrollY = $canvas.scrollTop();
// save canvas size to vars b/ they're used often
var canvasWidth = canvas.width;
var canvasHeight = canvas.height;
var baseRadius = 50;
var baseCircle = new Circle(150,150,50);
var nCircles = 7;
var startAngle = 15.0;
function Circle(x,y,r) {
this.x = x;
this.y = y;
this.r = r;
}
Circle.prototype.draw = function() {
ctx.beginPath();
ctx.arc(this.x,this.y,this.r, 0, 2 * Math.PI, false);
ctx.stroke();
}
var circles = new Array();
function calcCircles(n) {
circles.splice(0); // clear out old circles
var angle = Math.PI / n;
var s = Math.sin(angle);
var r = baseRadius * s / (1-s);
console.log(angle);
console.log(s);
console.log(r);
for(var i=0;i<n;++i) {
var phi = startAngle + angle * i * 2;
var cx = 150+(baseRadius + r) * Math.cos(phi);
var cy = 150+(baseRadius + r) * Math.sin(phi);
circles.push(new Circle(cx,cy,r));
}
}
function draw() {
baseCircle.draw();
circles.forEach(function(ele){ele.draw()});
}
calcCircles(7);
draw();