"use strict"; // Originally written by Jeff Thomas // https://codepen.io/aecend/pen/BGeZpa // Alternative random number generator bolted on later by me. (+10 FPS) class Fire { constructor(data) { this.canvas = document.createElement("canvas"); this.canvas.fireObject = this; this.ctx = this.canvas.getContext('2d', {lowLatency: true, alpha: true}); this.cooling = data.cooling; this.imageData = null; this.matrix = null; this.matrixLengthLessWidth = null; this.width = null; this.height = null; this.scale = null; this.randomX = +(new Date); this.randomY = 0; this.randomZ = 0; this.randomW = 0; for(let k = 64; k > 0; --k) this.random(); } random() { let t = this.randomX ^ (this.randomX << 11); this.randomX = this.randomY; this.randomY = this.randomZ; this.randomZ = this.randomW; this.randomW ^= (this.randomW >> 19) ^ t ^ (t >> 8); return (this.randomW >>> 0) / ((1 << 30) * 4); } nextFrame() { let value = null; let pixel = null; let sum = null; let cooling = this.cooling; let intensity = this.intensity; let width = this.width; let widthLessTwo = width - 2; let matrix = this.matrix; let matrixLength = this.matrix.length; let imageData = this.imageData.data; for(let i = 0; i < matrixLength; i++) { let i4 = i << 2; pixel = Math.round(i + width - this.random() + 1.0); if(i % width < widthLessTwo) { sum = this.matrix[pixel] + this.matrix[pixel + 1] + this.matrix[pixel - width + 1] + this.matrix[pixel - width]; value = Math.round(sum * cooling * this.random()) } else { value = 0; } matrix[i] = value; imageData[i4] = value << 2; imageData[i4 + 1] = value; imageData[i4 + 2] = value * 0.25 + 0.5 >> 0; imageData[i4 + 3] = value ? 255 : 0; } this.ctx.putImageData(this.imageData, 0, 0); } initialiseDimensions(width, height, scale){ this.width = this.canvas.width = width; this.height = this.canvas.height = height; this.scale = scale; this.canvas.style.width = width * this.scale + "px"; this.canvas.style.height = height * this.scale + "px"; this.ctx.clearRect(0, 0, this.width, this.height); this.imageData = this.ctx.getImageData(0, 0, this.width, this.height); this.matrix = new Uint16Array(this.width * this.height); this.matrixLengthLessWidth = this.matrix.length - this.width; } //Shapes erase flame moving "over" them - but easier to see shape in flame. addFirePoint(x, y, intensity){ this.matrix[y * this.width + x] = 10000 * intensity * this.random(); } //More accurate, less easier to see shapes, flame continues past shapes. addFirePointWithAddition(x, y, intensity){ let flame = 10000 * intensity * this.random(); let existingFlame = this.matrix[y * this.width + x] this.matrix[y * this.width + x] = flame + existingFlame; } } // ################################################################ // Easy enough to add/remove canvases, change the logo, create // animations of pixelFirearray, and modify fire attributes. var fires = [{ URL: "https://untamed.zone/miscImages/crowFire_logo.png", logoMaxWidth: 250, logoMaxHeight: 170, logoPixelGetter: getImagePixelsByFill, fireWidth: 300, fireHeight: 200, fireScale: 2, fireCooling: 0.93, fireUpdateFunction: updateFirePixelsByFireSlide, fireUpdatePixelCount: null, logoPositionX: 30, logoPositionY: 25, logoIntensity: 0.2 },{ URL: "https://untamed.zone/miscImages/twitter_logo.png", logoMaxWidth: 110, logoMaxHeight: 110, logoPixelGetter: getImagePixelsByScanline, fireWidth: 300, fireHeight: 200, fireScale: 2, fireCooling: 0.93, fireUpdateFunction: updateFirePixelsByFlappy, fireUpdatePixelCount: null, logoPositionX: 100, logoPositionY: 60, logoIntensity: 0.2 },{ URL: "https://untamed.zone/miscImages/apple_logo.png", logoMaxWidth: 120, logoMaxHeight: 120, logoPixelGetter: getImagePixelsByScanline, fireWidth: 300, fireHeight: 200, fireScale: 2, fireCooling: 0.93, fireUpdateFunction: updateFirePixelsByScale, fireUpdatePixelCount: null, logoPositionX: 90, logoPositionY: 40, logoIntensity: 0.2 },{ URL: "https://untamed.zone/miscImages/pentagram_logo.png", logoMaxWidth: 120, logoMaxHeight: 120, logoPixelGetter: getImagePixelsByScanline, fireWidth: 300, fireHeight: 200, fireScale: 2, fireCooling: 0.93, fireUpdateFunction: updateFirePixelsByRotation, fireUpdatePixelCount: null, logoPositionX: 90, logoPositionY: 50, logoIntensity: 0.2 }]; // This loop takes what's in the fire array above, and initialises new instances of Fire for each. // It does this by grabbing a logo picture (async), scaling it, moving pixels into firePixels, adding // mouse events, and adding a canvas to the web page. for(let i = 0; i < fires.length; i++) { let details = fires[i]; details.fireObject = new Fire({cooling: details.fireCooling}); details.fireObject.initialiseDimensions(details.fireWidth, details.fireHeight, details.fireScale); details.pixelFire = []; details.logoPixelGetter(details.URL, details.logoMaxWidth, details.logoMaxHeight, details.pixelFire); document.body.appendChild(details.fireObject.canvas); details.fireObject.canvas.addEventListener("mousemove", mouseMove, false); details.fireObject.canvas.addEventListener("touchmove", mouseMove, false); } // This updates all firePixels if needed, and updates the canvas fires. function nextAnimtationFrame(){ for(let i = 0; i < fires.length; i++) { let details = fires[i]; if(details.fireUpdateFunction) details.fireUpdateFunction(details); drawFireImage(details.fireObject, details.pixelFire, details.logoPositionX, details.logoPositionY, details.logoIntensity, details.fireUpdatePixelCount); details.fireObject.nextFrame(); } requestAnimationFrame(nextAnimtationFrame); } requestAnimationFrame(nextAnimtationFrame); // Activate the canvas fire based on previously scanned firePixels, with custom intensities! function drawFireImage(fire, pixels, x, y, intensity, pixelCount) { let len = pixels.length; if(pixelCount !== null) len = pixelCount; // Can be 0, so can't just use "if(pixelCount)" for(let p = 0; p < len; p++) { let pixel = pixels[p]; fire.addFirePoint(pixel.x + x, pixel.y + y, pixel.fireIntensity * intensity); } } // ################################################################ // Stuff to add a bit of interactivity when the mouse passes a canvas function mouseMove(event) { let p = event; let that = event.currentTarget.fireObject; if(event.type === "touchmove") { event.preventDefault(); p = event.targetTouches[0]; } let pos = getMousePos(p); that.addFirePoint(pos.x, pos.y, 3); } function getMousePos(event) { let unscaledCanvas = event.currentTarget; let rectangleWithCSS = unscaledCanvas.getBoundingClientRect(); //The CSS includes border too, but it's ok; being only 2 pixels in this program. let scaleX = unscaledCanvas.width / rectangleWithCSS.width; let scaleY = unscaledCanvas.height / rectangleWithCSS.height; return { x: Math.round((event.clientX - rectangleWithCSS.left) * scaleX) + 1, y: Math.round((event.clientY - rectangleWithCSS.top) * scaleY) + 1 } } // ################################################################ // Stuff to load images, scale them using the right aspect ratio, // and store them in pixelFire elements. function getImagePixelsByScanline(URL, width, height, points) { var image = new Image(); image.crossOrigin = 'anonymous'; image.onload = function(){getScanImagePixels(image, width, height, points);}; image.src = URL; } function getImagePixelsByFill(URL, width, height, points) { var image = new Image(); image.crossOrigin = 'anonymous'; image.onload = function(){getFillImagePixels(image, width, height, points);}; image.src = URL; } function calculateAspectRatioFit(srcWidth, srcHeight, maxWidth, maxHeight) { let ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight); return { width: srcWidth * ratio, height: srcHeight * ratio }; } function getScanImagePixels(image, maxWidth, maxHeight, points) { let scaledImageDimensions = calculateAspectRatioFit(image.width, image.height, maxWidth, maxHeight); let width = Math.round(scaledImageDimensions.width); let height = Math.round(scaledImageDimensions.height); let c = document.createElement("canvas"); c.width = width; c.height = height; let ctx = c.getContext("2d"); ctx.drawImage(image, 0, 0, width, height); let imageData = ctx.getImageData(0, 0, c.width, c.height); let pixels = imageData.data; let offset = -4; for(let y = 0; y < height; y++) { for(let x = 0; x < width; x++) { offset += 4; let alpha = pixels[offset + 3]; if(alpha == 0) continue; let red = pixels[offset]; let green = pixels[offset + 1]; let blue = pixels[offset + 2]; points.push({x: x, y: y, originalX: x, originalY: y, fireIntensity: (((red + green + blue) / 3.0) / 255) * (alpha / 255), originalFireIntensity: (((red + green + blue) / 3.0) / 255) * (alpha / 255), red: red, green: green, blue: blue, alpha: alpha }); } } } function getFillImagePixels(image, maxWidth, maxHeight, points) { let scaledImageDimensions = calculateAspectRatioFit(image.width, image.height, maxWidth, maxHeight); let width = Math.round(scaledImageDimensions.width); let height = Math.round(scaledImageDimensions.height); let c = document.createElement("canvas"); c.width = width; c.height = height; let ctx = c.getContext("2d"); ctx.drawImage(image, 0, 0, width, height); let imageData = ctx.getImageData(0, 0, c.width, c.height); let pixels = imageData.data; let found = 0; let offset = -4; let pixelStack = []; //Find first purple pixel for(let y = 0; y < height && !found; y++) { for(let x = 0; x < width && !found; x++) { offset += 4; let alpha = pixels[offset + 3]; if(alpha == 0) continue; let red = pixels[offset]; let green = pixels[offset + 1]; let blue = pixels[offset + 2]; if(red == 255 && green == 0 && blue == 255) { found = true; pixelStack.push({x: x, y: y, intensity: 1}); } } } let vertcalStep = width * 4; while(pixelStack.length) { let newPos = pixelStack.shift(); let x = newPos.x; let y = newPos.y; let intensity = newPos.intensity; let pointer = (y * width + x) * 4; let red = pixels[pointer]; let green = pixels[pointer + 1]; let blue = pixels[pointer + 2]; let alpha = pixels[pointer + 3]; points.push({x: x, y: y, originalX: x, originalY: y, fireIntensity: intensity, originalFireIntensity: intensity }); pixels[pointer + 3] = 0; // Below let below = pointer + vertcalStep; if(pixels[below + 3] > 0) { let intensity = getSetIntensity(pixels, below); pixelStack.push({x: x, y: y + 1, intensity: intensity}); } // Above let above = pointer - vertcalStep; if(pixels[above + 3] > 0) { let intensity = getSetIntensity(pixels, above); pixelStack.push({x: x, y: y - 1, intensity: intensity}); } let left = pointer - 4; if(pixels[left + 3] > 0) { let intensity = getSetIntensity(pixels, left); pixelStack.push({x: x - 1, y: y, intensity: intensity}); } let right = pointer + 4; if(pixels[right + 3] > 0) { let intensity = getSetIntensity(pixels, right); pixelStack.push({x: x + 1, y: y, intensity: intensity}); } } } function getSetIntensity(pixels, ptr) { let red = pixels[ptr]; let green = pixels[ptr + 1]; let blue = pixels[ptr + 2]; let alpha = pixels[ptr + 3]; pixels[ptr + 3] = 0; return (((red + green + blue) / 3.0) / 255) * (alpha / 255); } // ################################################################ // Stuff to rotate the firePixels from originalX & Y, to x & y. class Rotator { constructor() { this.degToRad = Math.PI / 180.0; this.cx = null; this.cy = null; this.cos = null; this.sin = null; this.point = [0, 0]; } setAngle(angle) { this.cos = Math.cos(angle * this.degToRad); this.sin = Math.sin(angle * this.degToRad); } setCenter(centerX, centerY){ this.cx = centerX; this.cy = centerY; } rotateCenterOffset(x, y) { point[0] = Math.round((this.cos * (x - this.cx)) + (this.sin * (y - this.cy)) + this.cx); point[1] = Math.round((this.cos * (y - this.cy)) - (this.sin * (x - this.cx)) + this.cy); return point; } rotate(x, y) { point[0] = Math.round((this.cos * x) + (this.sin * y)); point[1] = Math.round((this.cos * y) - (this.sin * x)); return point; } pointsRotateCenterOffset(points) { let len = points.length; for(let i = 0; i < len; i++) { let p = points[i]; p.x = Math.round((this.cos * (p.originalX - this.cx)) + (this.sin * (p.originalY - this.cy)) + this.cx); p.y = Math.round((this.cos * (p.originalY - this.cy)) - (this.sin * (p.originalX - this.cx)) + this.cy); } } pointsRotate(points) { let len = points.length; for(let i = 0; i < len; i++) { let p = points[i]; p.x = Math.round((this.cos * p.originalX) + (this.sin * p.originalY)); p.y = Math.round((this.cos * p.originalY) - (this.sin * p.originalX)); } return rotatedPoints; } } var rotate = new Rotator(); var rotateAngle = 0; function updateFirePixelsByRotation(details) { rotate.setAngle(rotateAngle); rotateAngle += 0.25; rotate.setCenter(60, 60); rotate.pointsRotateCenterOffset(details.pixelFire); } // ################################################################ // Flappy var flappyAngle = 0; var flappyAmplitude = 30; function updateFirePixelsByFlappy(details) { flappyAngle += 0.025; let len = details.pixelFire.length; for(let i = 0; i < len; i++) { let p = details.pixelFire[i]; p.x = p.originalX; p.y = Math.round(p.originalY + (Math.cos(flappyAngle) * flappyAmplitude)); } } // ################################################################ // Scale var scaleAngle = 0; var scaleAmplitude = 1.3; var scaleBaseScale = 0.2; var scaleCenterX = 60; var scaleCenterY = 60; function updateFirePixelsByScale(details) { scaleAngle += 0.0125; let len = details.pixelFire.length; for(let i = 0; i < len; i++) { let p = details.pixelFire[i]; let scaling = scaleBaseScale + Math.abs(Math.cos(scaleAngle) * scaleAmplitude); p.x = scaleCenterX + Math.round((p.originalX - scaleCenterX) * scaling); p.y = scaleCenterY + Math.round((p.originalY - scaleCenterY) * scaling); } } // ################################################################ // Flame slide var slideStart = -1000; var slideEndAddition = 3000; var firePixelIncrease = 10; var slideCurrent = slideStart; function updateFirePixelsByFireSlide(details) { let len = details.pixelFire.length; if(!len) return; let slideFullEnd = slideEndAddition + len; slideCurrent += firePixelIncrease; if(slideCurrent > len) { let fadeIterations = firePixelIncrease * 80; for(let p = 0; p < fadeIterations; p++) { let loc = Math.round((len - 1) * Math.random()); // A point between start and end. loc *= (p / (fadeIterations / 3)); // A point closer to the start. let wholeLoc = Math.round(loc); if(wholeLoc < len) details.pixelFire[wholeLoc].fireIntensity *= 0.8; } } if(slideCurrent > slideFullEnd) { slideCurrent = slideStart; for(let i = 0; i < len; i++) { details.pixelFire[i].fireIntensity = details.pixelFire[i].originalFireIntensity; } } let slideEndPoint = slideCurrent; if(slideEndPoint < 0) slideEndPoint = 0; if(slideEndPoint > len) slideEndPoint = len; details.fireUpdatePixelCount = slideEndPoint; }

!