P5.js: how to programatically calculate BURN blendMode between 2 colors? - p5.js

I am doing some blendMode(BURN) to paint some shapes. And other shapes I need to painted directly in the resulting color of the previous shapes, so I need to generate the blended resulting color my self.
I am looking for something like:
let blendedColor = blendColorsBurn(color1, color2);

There's no built in function for this. In 2d mode p5.js leverages the canvas's globalCompositeOperation property, which when set to color-burn performs the following color blending operation:
Divides the inverted bottom layer by the top layer, and then inverts the result.
This sounds simple enough, but I wanted to verify it means what it sounds like it means, so I decided to try a quick test implementation. Press the shift and control keys to see the built in BURN blendMode vs my calculations respectively.
let existingContent;
let newContent;
let manualBurn;
let scaleFactor = 1;
function preload() {
existingContent = loadImage("https://www.paulwheeler.us/files/existing-content.png");
newContent = loadImage("https://www.paulwheeler.us/files/new-content.png");
}
function setup() {
createCanvas(windowWidth, windowHeight);
noLoop();
scaleFactor = height / newContent.height;
manualBurn = createImage(newContent.width, newContent.height);
// Divides the inverted bottom layer by the top layer, and then inverts the result.
for (let x = 0; x < newContent.width; x++) {
for (let y = 0; y < newContent.height; y++) {
const c1 = existingContent.get(x, y);
const c2 = newContent.get(x, y);
if (alpha(c2) > 0) {
let a = alpha(c1) / 255;
// Inverted bottom layer
let [inv_r, inv_g, inv_b, inv_a] = [
// Subtracting from alpha instead of 1 is to deal with pre-multiplied alpha
a - red(c1) / 255,
a - green(c1) / 255,
a - blue(c1) / 255,
1 - alpha(c1) / 255,
];
// divided by the top layer
let [div_r, div_g, div_b, div_a] = [
inv_r / (red(c2) / 255),
inv_g / (green(c2) / 255),
inv_b / (blue(c2) / 255),
inv_a / (alpha(c2) / 255),
];
// inverted
let out = [255 * (1 - div_r), 255 * (1 - div_g), 255 * (1 - div_b), max(alpha(c2), 255 * (1 - div_a))];
manualBurn.set(x, y, out);
} else {
manualBurn.set(x, y, c1);
}
}
}
manualBurn.updatePixels();
}
function keyPressed() {
redraw();
}
function keyReleased() {
redraw();
}
function draw() {
clear();
scale(scaleFactor);
if (keyIsDown(CONTROL)) {
blendMode(BLEND);
image(manualBurn, 0, 0);
} else {
image(
existingContent,
0,
0
);
if (keyIsDown(SHIFT)) {
blendMode(BURN);
image(newContent, 0, 0);
}
}
}
html, body {
margin: 0;
padding: 0;
}
body {
background-image: url("https://www.paulwheeler.us/files/checkerboard.jpeg");
background-repeat: repeat;
background-size: 200px;
}
canvas {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
There were definitely some gotchas:
It may be obvious but this math has to be done with values from 0 to 1, not 0 to 255.
colorMode(RGB, 1, 1, 1, 1) does not work because the red(), green(), and blue() function round to integers 🤦‍♂️
When inverting a color, you need to start from the alpha value rather than 1, because of premultiplied alpha.
It's not obvious from the description, but it would appear that the burn images alpha takes precedence.

Related

How do I oscillate the height of individual shapes whilst keeping the total height of their sum constant?

I am trying to create an effect where the total height of a group of shapes is constant (say 300), whilst each shape within that group has a dynamic, oscillating, height. In one instance, maybe the middle shape is 'taller' whilst the outer shapes are shorter.
This desired effect is similar to if you held a slinky, with each end in one hand fixed at 30cm apart, and then shook it around: the total height remains the same (30cm) but the 'sections' inside the slinky are having their individual heights bounce up and down.
My attempts so far use the sin function to get an oscillating number as an angle value increases. This works for the sections, but I can't figure out how to maintain the constant overall height. See the code snippet below; red (and the tip of the bottom black triangle) should always be touching the bottom of the container.
// Prepare variables for angles, separated by 1
let a1 = 0;
let a2 = 1;
let a3 = 2;
let a4 = 3;
let a5 = 4;
// Prepare shape width
let shapeW = 150;
function setup() {
createCanvas(300, 300);
rect(10, 10, 10, 10);
}
function draw() {
background(240);
noStroke();
// Use the sin ratio to 'oscillate' a height value between 0 and 60
let x1 = map(sin(a1), -1, 1, 0, height / 5);
let x2 = map(sin(a2), -1, 1, 0, height / 5);
let x3 = map(sin(a3), -1, 1, 0, height / 5);
let x4 = map(sin(a4), -1, 1, 0, height / 5);
let x5 = map(sin(a5), -1, 1, 0, height / 5);
// Store these in an array so I can loop through
let listOfValues = [x1, x2, x3, x4, x5];
// Loop through and draw shapes
push();
translate((width / 2) - shapeW / 2, 0)
for (let i = 0; i < listOfValues.length; i++) {
fill(255, 0, 0);
rect(0, 0, shapeW, listOfValues[i]);
fill(0)
triangle(0, 0, shapeW / 2, listOfValues[i], shapeW, 0)
translate(0, listOfValues[i]);
}
pop();
// Increment each angle by the same amount
let incAmount = 0.1;
a1 += incAmount;
a2 += incAmount;
a3 += incAmount;
a4 += incAmount;
a5 += incAmount;
}
html,
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.0/lib/p5.js"></script>
With help, I've found the solution is to use binomial coefficients. That is achieved here via a binomial() function. The only caveat is that the number of 'sections' (represented as n) must be an even number.
let angle = 0;
let N;
let containerW = 300;
let shapeW = 150;
let n = 6;
let speed = 0.0075;
function setup() {
createCanvas(containerW, containerW);
N = n * binomial(n, n / 2);
}
function draw() {
background(240);
noStroke();
let listOfVals = [];
for (let i = 0; i < n; i++) {
listOfVals.push(x(i + 1));
}
push();
translate(width / 2 - shapeW / 2, 0);
for (let i = 0; i < listOfVals.length; i++) {
fill(255, 0, 0);
rect(0, 0, shapeW, listOfVals[i] * height);
fill(0);
triangle(0, 0, shapeW / 2, listOfVals[i] * height, shapeW, 0);
translate(0, listOfVals[i] * height);
}
pop();
// Increment angle
angle += speed;
}
function x(k) {
return (2 ** n * sin(angle + (k * PI) / n) ** n) / N;
}
function binomial(n, k) {
if (typeof n !== "number" || typeof k !== "number") return false;
var coeff = 1;
for (var x = n - k + 1; x <= n; x++) coeff *= x;
for (x = 1; x <= k; x++) coeff /= x;
return coeff;
}
html,
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.0/lib/p5.js"></script>
Nice self answer (+1).
This is more of an idea for a slightly different approach, hopefully with a few simplifications:
// Prepare shape width
let shapeW = 150;
// Prepare shape height
let shapeH;
// total number of shapes
let numShapes = 5;
// Increment each angle offset by the same amount
let incAmount = 0.05;
function setup() {
createCanvas(300, 300);
rect(10, 10, 10, 10);
// assign shape height after sketch height has been set
shapeH = height / 5;
}
function draw() {
background(240);
noStroke();
// Loop through and draw shapes
push();
// horizontally center shapes
translate((width - shapeW) / 2, 0);
// for each shape
for (let i = 0; i < numShapes; i++) {
// map the current height to the increment asdasdakrk
let currentH = map(sin(i + (frameCount * incAmount)), -1, 1, 0, shapeH);
fill(255, 0, 0);
rect(0, 0, shapeW, currentH);
fill(0)
triangle(0, 0, shapeW / 2, currentH, shapeW, 0)
translate(0, currentH);
}
pop();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
The above is using the same logic, mostly removing the need for the a1, a2, a3, a4, a5 values as they coincide with the i counter for each shape.
A visual way I think about it having to connect the tip of one triangle with the base of the next triangle (or the current triangle's base being the same as the the previous triangle tip's y position):
// Prepare shape width
let shapeW = 150;
// Prepare shape height
let shapeH;
// total number of shapes
let numShapes = 5;
// Increment each angle offset by the same amount
let incAmount = 0.05;
// sine driven scales
let minYScale = 0.5;
let maxYScale = 2.0;
function setup() {
createCanvas(300, 300);
rect(10, 10, 10, 10);
// assign shape height after sketch height has been set
shapeH = height / 5;
}
function draw() {
background(240);
noStroke();
// Loop through and draw shapes
push();
// horizontally center shapes
translate((width - shapeW) / 2, 0);
// draw red background
fill(255, 0, 0);
rect(0, 0, shapeW, height);
// remember where the previous array base was
let lastY = 0;
// for each shape
for (let i = 0; i < numShapes; i++) {
// map the current y scale to the increment
let currentYScale = map(sin(i + (frameCount * incAmount)), -1, 1, minYScale, maxYScale);
// compute the current scale based on the sine scalar
let currentH = currentYScale * shapeH;
fill(0);
triangle(0, lastY,
shapeW / 2, lastY + currentH,
shapeW, lastY);
// update absolute y position of the arrow base
lastY += currentH;
// optional: for debugging only, visualise lastY
if(mouseIsPressed) rect(-shapeW, lastY, width + shapeW, 3);
}
pop();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

Processing(Java) to p5js - glitch effect

I'm new in p5js and i want to create a noise effect in an image with it. I create a functional sketch with Java in processing, but when i pass it to p5j something is wrong.
The image is download in the html field hwne i put , but the pixels loc staff doesn't.
Can anyone help me!!
This is my sketch:
function setup()
{
createCanvas(400,300);
img = loadImage("data/monja.jpg");
//surface.setResizable(true);
//surface.setSize(img.width, img.height);
background(0);
}
function draw()
{
loadPixels();
img.loadPixels();
for (let x = 0; x < img.width; x++)
{
for (let y = 0; y < img.height; y++)
{
let loc = x+y*width;
let c = brightness(img.pixels[loc]);
let r = red(img.pixels[loc]);
let g = green(img.pixels[loc]);
let b = blue(img.pixels[loc]);
if (c < 70){
img.pixels[loc]= color(random(255));
}
else {
img.pixels[loc] = color(r, g, b);
}
}
}
updatePixels();
//image(img, 0, 0);
}```
To modify the color of certain pixels in an image here are some things to keep in mind.
When we call loadPixels the pixels array is an array of numbers.
How many numbers each pixel gets is determined by the pixel density
If pixel density is 1 then each pixel will get 4 numbers in the array, each with a value from 0 to 255.
The first number determines the amount of red in the pixel, the second green, the third red and the fourth is the alpha value for transparency.
Here is an example that changes pixels with a high red value to a random gray scale to create a glitch effect.
var img;
var c;
function preload(){
img = loadImage("https://i.imgur.com/rpQdRoY.jpeg");
}
function setup()
{
createCanvas(img.width, img.height);
background(0);
let d = pixelDensity();
img.loadPixels();
for (let i = 0; i < 4 * (img.width*d * img.height*d); i += 4) {
if (img.pixels[i] > 150 && img.pixels[i+1] <100&&img.pixels[i+2] < 100){
let rColor = random(255);
img.pixels[i] = rColor;
img.pixels[i + 1] = rColor;
img.pixels[i + 2] = rColor;
img.pixels[i + 3] = rColor;
}
}
img.updatePixels();
}
function draw() {
image(img,0,0);
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.3.0/lib/p5.js"></script>

How to rotate a sprite around a fixed point so it follows cursor

I'm developing a small minigolf game, where the user can shoot moving the cursor around to set an angle, and the force applied will be the length of an arrow (less force when the cursor is closer to the ball). You can check exactly how it works here: https://imgur.com/a/AQ1pi
I have figured out how to rotate the arrow sprite to follow the cursor but I don't know yet how to make it move around the ball, right now it's just rotating in its anchor, in this case the head of the arrow.
I'm using Panda.js (a Pixi.js based framework) to develop the game, but its API is similar to the native Canvas functions. I don't need an exact implementation (that's why I'm not posting any code), but I would like to get some ideas about how to rotate the sprite around a point in a given radius. In this case, the point would be the center of the ball, and the radius will be the ball radius. Thanks!
You set the point of rotation with ctx.translate or ctx.setTransform then apply the rotation with ctx.rotate(ang); Then draw the image offset so that the point of rotation is at (0,0). Ie if you want the point of rotation to be at image coordinates (100,50) then render at ctx.drawImage(image,-100,-50);
To get the angle from a point to the mouse use Math.atan2
requestAnimationFrame(update);
// draws rotated image at x,y.
// cx, cy is the image coords you want it to rotate around
function drawSprite(image, x, y, cx, cy, rotate) {
ctx.setTransform(1, 0, 0, 1, x, y);
ctx.rotate(rotate);
ctx.drawImage(image, -cx, -cy);
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore defaults
}
// function gets the direction from point to mouse and draws an image
// rotated to point at the mouse
function rotateAroundPoint(x, y, mouse) {
const dx = mouse.x - x;
const dy = mouse.y - y;
const dir = Math.atan2(dy, dx);
drawSprite(arrow, x, y, 144, 64, dir);
}
// Main animation loop.
function update(timer) {
globalTime = timer;
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
strokeCircle(150, 75, 10);
rotateAroundPoint(150, 75, mouse);
requestAnimationFrame(update);
}
//=====================================================
// All the rest is unrelated to the answer.
const ctx = canvas.getContext("2d");
const mouse = { x: 0, y: 0, button: false };
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
function mouseEvents(e) {
mouse.bounds = canvas.getBoundingClientRect();
mouse.x = e.pageX - mouse.bounds.left - scrollX;
mouse.y = e.pageY - mouse.bounds.top - scrollY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"), c.width = w, c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w, h), c.ctx = c.getContext("2d"), c);
const drawPath = (ctx, p) => {var i = 0;while (i < p.length) {ctx.lineTo(p[i++], p[i++])}};
const strokeCircle = (l,y=ctx,r=ctx,c=ctx) =>{if(l.p1){c=y; r=leng(l);y=l.p1.y;l=l.p1.x }else if(l.x){c=r;r=y;y=l.y;l=l.x}c.beginPath(); c.arc(l,y,r,0,Math.PI*2); c.stroke()};
const aW = 10;
const aH = 20;
const ind = 5;
const arrow = CImageCtx();
arrow.ctx.beginPath();
drawPath(arrow.ctx, [
ind, 64 - aW,
128 - ind - aH, 64 - aW,
128 - ind - aH, 64 - aH,
128 - ind, 64,
128 - ind - aH, 64 + aH,
128 - ind - aH, 64 + aW,
ind, 64 + aW,
]);
arrow.ctx.fillStyle = "red";
arrow.ctx.fill();
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime;
canvas {
border: 2px solid black;
}
<canvas id="canvas"></canvas>

Finding the furthest point in a grid when compared to other points

I have a rectangular grid of variable size but averaging 500x500 with a small number of x,y points in it (less than 5). I need to find an algorithm that returns an x,y pair that is the farthest away possible from any of the other points.
Context: App's screen (grid) and a set of x,y points (enemies). The player dies and I need an algorithm that respawns them as far away from the enemies so that they don't die immediately after respawning.
What I have so far: The algorithm I wrote works but doesn't perform that great in slower phones. I'm basically dividing up the grid into squares (much like a tic tac toe) and I assign each square a number. I then check every single square against all enemies and store what the closest enemy was at each square. The square with the highest number is the square that has the closest enemy furthest away. I also tried averaging the existing points and doing something similar to this and while the performance was acceptable, the reliability of the method was not.
This is the simplest algorithm I could think of that still gives good results. It only checks 9 possible positions: the corners, the middle of the sides, and the center point. Most of the time the player ends up in a corner, but you obviously need more positions than enemies.
The algorithm runs in 0.013ms on my i5 desktop. If you replace the Math.pow() by Math.abs(), that comes down to 0.0088ms, though obviously with less reliable results. (Oddly enough, that's slower than my other answer which uses trigonometry functions.)
Running the code snippet (repeatedly) will show the result with randomly positioned enemies in a canvas element.
function furthestFrom(enemy) {
var point = [{x:0,y:0},{x:250,y:0},{x:500,y:0},{x:0,y:250},{x:250,y:250},{x:500,y:250},{x:0,y:500},{x:250,y:500},{x:500,y:500}];
var dist2 = [500000,500000,500000,500000,500000,500000,500000,500000,500000];
var max = 0, furthest;
for (var i in point) {
for (var j in enemy) {
var d = Math.pow(point[i].x - enemy[j].x, 2) + Math.pow(point[i].y - enemy[j].y, 2);
if (d < dist2[i]) dist2[i] = d;
}
if (dist2[i] > max) {
max = dist2[i];
furthest = i;
}
}
return(point[furthest]);
}
// CREATE TEST DATA
var enemy = [];
for (var i = 0; i < 5; i++) enemy[i] = {x: Math.round(Math.random() * 500), y: Math.round(Math.random() * 500)};
// RUN FUNCTION
var result = furthestFrom(enemy);
// SHOW RESULT ON CANVAS
var canvas = document.getElementById("canvas");
canvas.width = 500; canvas.height = 500;
canvas = canvas.getContext("2d");
for (var i = 0; i < 5; i++) {
paintDot(canvas, enemy[i].x, enemy[i].y, 10, "red");
}
paintDot(canvas, result.x, result.y, 20, "blue");
function paintDot(canvas, x, y, size, color) {
canvas.beginPath();
canvas.arc(x, y, size, 0, 6.2831853);
canvas.closePath();
canvas.fillStyle = color;
canvas.fill();
}
<BODY STYLE="margin: 0; border: 0; padding: 0;">
<CANVAS ID="canvas" STYLE="width: 200px; height: 200px; background-color: #EEE;"></CANVAS>
</BODY>
This is similar to what you are already doing, but with two passes where the first pass can be fairly crude. First decrease resolution. Divide the 500x500 grid into 10x10 grids each of which is 50x50. For each of the resulting 100 subgrids -- determine which have at least one enemy and locate the subgrid that is furthest away from a subgrid which contains an enemy. At this stage there is only 100 subgrids to worry about. Once you find the subgrid which is furthest away from an enemy -- increase resolution. That subgrid has 50x50 = 2500 squares. Do your original approach with those squares. The result is 50x50 + 100 = 2600 squares to process rather than 500x500 = 250,000. (Adjust the numbers as appropriate for the case in which there isn't 500x500 but with the same basic strategy).
Here is a Python3 implementation. It uses two functions:
1) fullResSearch(a,b,n,enemies) This function takes a set of enemies, a corner location (a,b) and an int, n, and finds the point in the nxn square of positions whose upper-left hand corner is (a,b) and finds the point in that square whose which has the maximum min-distance to an enemy. The enemies are not assumed to be in this nxn grid (although they certainly can be)
2) findSafePoint(n, enemies, mesh = 20) This function takes a set of enemies who are assumed to be in the nxn grid starting at (0,0). mesh determines the size of the subgrids, defaulting to 20. The overall grid is split into mesh x mesh subgrids (or slightly smaller along the boundaries if mesh doesn't divide n) which I think of as territories. I call a territory an enemy territory if it has an enemy in it. I create the set of enemy territories and pass it to fullResSearch with parameter n divided by mesh rather than n. The return value gives me the territory which is farthest from any enemy territory. Such a territory can be regarded as fairly safe. I feed that territory back into fullResSearch to find the safest point in that territory as the overall return function. The resulting point is either optimal or near-optimal and is computed very quickly. Here is the code (together with a test function):
import random
def fullResSearch(a,b,n,enemies):
minDists = [[0]*n for i in range(n)]
for i in range(n):
for j in range(n):
minDists[i][j] = min((a+i - x)**2 + (b+j - y)**2 for (x,y) in enemies)
maximin = 0
for i in range(n):
for j in range(n):
if minDists[i][j] > maximin:
maximin = minDists[i][j]
farthest = (a+i,b+j)
return farthest
def findSafePoint(n, enemies, mesh = 20):
m = n // mesh
territories = set() #enemy territories
for (x,y) in enemies:
i = x//mesh
j = y//mesh
territories.add((i,j))
(i,j) = fullResSearch(0,0,m,territories)
a = i*mesh
b = j*mesh
k = min(mesh,n - a,n - b) #in case mesh doesn't divide n
return fullResSearch(a,b,k,enemies)
def test(n, numEnemies, mesh = 20):
enemies = set()
count = 0
while count < numEnemies:
i = random.randint(0,n-1)
j = random.randint(0,n-1)
if not (i,j) in enemies:
enemies.add ((i,j))
count += 1
for e in enemies: print("Enemy at", e)
print("Safe point at", findSafePoint(n,enemies, mesh))
A typical run:
>>> test(500,5)
Enemy at (216, 67)
Enemy at (145, 251)
Enemy at (407, 256)
Enemy at (111, 258)
Enemy at (26, 298)
Safe point at (271, 499)
(I verified by using fullResSearch on the overall grid that (271,499) is in fact optimal for these enemies)
This method looks at all the enemies from the center point, checks the direction they're in, finds the emptiest sector, and then returns a point on a line through the middle of that sector, 250 away from the center.
The result isn't always perfect, and the safe spot is never in the center (though that could be added), but maybe it's good enough.
The algorithm runs more than a million times per second on my i5 desktop, but a phone may not be that good with trigonometry. The algorithm uses 3 trigonometry functions per enemy: atan2(), cos() and sin(). Those will probably have the most impact on the speed of execution. Maybe you could replace the cos() and sin() with a lookup table.
Run the code snippet to see an example with randomly positioned enemies.
function furthestFrom(e) {
var dir = [], widest = 0, bisect;
for (var i = 0; i < 5; i++) {
dir[i] = Math.atan2(e[i].y - 250, e[i].x - 250);
}
dir.sort(function(a, b){return a - b});
dir.push(dir[0] + 6.2831853);
for (var i = 0; i < 5; i++) {
var angle = dir[i + 1] - dir[i];
if (angle > widest) {
widest = angle;
bisect = dir[i] + angle / 2;
}
}
return({x: 250 * (1 + Math.cos(bisect)), y: 250 * (1 + Math.sin(bisect))});
}
// CREATE TEST DATA
var enemy = [];
for (var i = 0; i < 5; i++) enemy[i] = {x: Math.round(Math.random() * 500), y: Math.round(Math.random() * 500)};
// RUN FUNCTION AND SHOW RESULT ON CANVAS
var result = furthestFrom(enemy);
var canvas = document.getElementById("canvas");
canvas.width = 500; canvas.height = 500;
canvas = canvas.getContext("2d");
for (var i = 0; i < 5; i++) {
paintDot(canvas, enemy[i].x, enemy[i].y, "red");
}
paintDot(canvas, result.x, result.y, "blue");
// PAINT DOT ON CANVAS
function paintDot(canvas, x, y, color) {
canvas.beginPath();
canvas.arc(x, y, 10, 0, 6.2831853);
canvas.closePath();
canvas.fillStyle = color;
canvas.fill();
}
<BODY STYLE="margin: 0; border: 0; padding: 0">
<CANVAS ID="canvas" STYLE="width: 200px; height: 200px; background-color: #EEE;"CANVAS>
</BODY>
Here's an interesting solution, however I cannot test it's efficiency. For each enemy, make a line of numbers from each number, starting with one and increasing by one for each increase in distance. Four initial lines will come from the four edges and each time you go one further out, you create another line coming out at a 90 degree angle, also increasing the number each change in distance. If the number line encounters an already created number that is smaller than it, it will not overwrite it and will stop reaching further. Essentially, this makes it so that if the lines find a number smaller than it, it won't check any further grid marks, eliminating the need to check the entire grid for all of the enemies.
<<<<<<^^^^^^^
<<<<<<^^^^^^^
<<<<<<X>>>>>>
vvvvvvv>>>>>>
vvvvvvv>>>>>>
public void map(int posX, int posY)
{
//left up right down
makeLine(posX, posY, -1, 0, 0, -1);
makeLine(posX, posY, 0, 1, -1, 0);
makeLine(posX, posY, 1, 0, 0, 1);
makeLine(posX, posY, 0, -1, 1, 0);
grid[posX][posY] = 1000;
}
public void makeLine(int posX, int posY, int dirX, int dirY, int dir2X, int dir2Y)
{
int currentVal = 1;
posX += dirX;
posY += dirY;
while (0 <= posX && posX < maxX && posY < maxY && posY >= 0 && currentVal < grid[posX][posY])
{
int secondaryPosX = posX + dir2X;
int secondaryPosY = posY + dir2Y;
int secondaryVal = currentVal + 1;
makeSecondaryLine( secondaryPosX, secondaryPosY, dir2X, dir2Y, secondaryVal);
makeSecondaryLine( secondaryPosX, secondaryPosY, -dir2X, -dir2Y, secondaryVal);
grid[posX][posY] = currentVal;
posX += dirX;
posY += dirY;
currentVal++;
}
}
public void makeSecondaryLine(int secondaryPosX, int secondaryPosY, int dir2X, int dir2Y, int secondaryVal)
{
while (0 <= secondaryPosX && secondaryPosX < maxX && secondaryPosY < maxY &&
secondaryPosY >= 0 && secondaryVal < grid[secondaryPosX][secondaryPosY])
{
grid[secondaryPosX][secondaryPosY] = secondaryVal;
secondaryPosX += dir2X;
secondaryPosY += dir2Y;
secondaryVal++;
}
}
}
Here is the code I used to map out the entire grid. The nice thing about this, is that the number of times the number is checked/written is not that much dependent on the number of enemies on the screen. Using a counter and randomly generated enemies, I was able to get this: 124 enemies and 1528537 checks, 68 enemies and 1246769 checks, 15 enemies and 795695 500 enemies and 1747452 checks. This is a huge difference compared to your earlier code which would do number of enemies * number of spaces.
for 124 enemies you'd have done 31000000 checks, while instead this did 1528537, which is less than 5% of the number of checks normally done.
You can choose some random point at the grid and then move it iteratively from the enemies, here is my implementation in python:
from numpy import array
from numpy.linalg import norm
from random import random as rnd
def get_pos(enem):
# chose random start position
pos = array([rnd() * 500., rnd() * 500.])
# make several steps from enemies
for i in xrange(25): # 25 steps
s = array([0., 0.]) # step direction
for e in enem:
vec = pos - array(e) # direction from enemy
dist = norm(vec) # distance from enemy
vec /= dist # normalize vector
# calculate size of step
step = (1000. / dist) ** 2
vec *= step
s += vec
# update position
pos += s
# ensure that pos is in bounds
pos[0] = min(max(0, pos[0]), 500.)
pos[1] = min(max(0, pos[1]), 500.)
return pos
def get_dist(enem, pos):
dists = [norm(pos - array(e)) for e in enem]
print 'Min dist: %f' % min(dists)
print 'Avg dist: %f' % (sum(dists) / len(dists))
enem = [(0., 0.), (250., 250.), (500., 0.), (0., 500.), (500., 500.)]
pos = get_pos(enem)
print 'Position: %s' % pos
get_dist(enem, pos)
Output:
Position: [ 0. 250.35338215]
Min dist: 249.646618
Avg dist: 373.606883
Triangulate the enemies (there's less than 5?); and triangulate each corner of the grid with the closest pair of enemies to it. The circumcenter of one of these triangles should be a decent place to re-spawn.
Below is an example in JavaScript. I used the canvas method from m69's answer for demonstration. The green dots are the candidates tested to arrive at the blue-colored suggestion. Since we are triangulating the corners, they are not offered as solutions here (perhaps the randomly-closer solutions can be exciting for a player? Alternatively, just test for the corners as well..).
// http://stackoverflow.com/questions/4103405/what-is-the-algorithm-for-finding-the-center-of-a-circle-from-three-points
function circumcenter(x1,y1,x2,y2,x3,y3)
{
var offset = x2 * x2 + y2 * y2;
var bc = ( x1 * x1 + y1 * y1 - offset ) / 2;
var cd = (offset - x3 * x3 - y3 * y3) / 2;
var det = (x1 - x2) * (y2 - y3) - (x2 - x3)* (y1 - y2);
var idet = 1/det;
var centerx = (bc * (y2 - y3) - cd * (y1 - y2)) * idet;
var centery = (cd * (x1 - x2) - bc * (x2 - x3)) * idet;
return [centerx,centery];
}
var best = 0,
candidates = [];
function better(pt,pts){
var temp = Infinity;
for (var i=0; i<pts.length; i+=2){
var d = (pts[i] - pt[0])*(pts[i] - pt[0]) + (pts[i+1] - pt[1])*(pts[i+1] - pt[1]);
if (d <= best)
return false;
else if (d < temp)
temp = d;
}
best = temp;
return true;
}
function f(es){
if (es.length < 2)
return "farthest corner";
var corners = [0,0,500,0,500,500,0,500],
bestcandidate;
// test enemies only
if (es.length > 2){
for (var i=0; i<es.length-4; i+=2){
for (var j=i+2; j<es.length-2; j+=2){
for (var k=j+2; k<es.length; k+=2){
var candidate = circumcenter(es[i],es[i+1],es[j],es[j+1],es[k],es[k+1]);
if (candidate[0] < 0 || candidate[1] < 0 || candidate[0] > 500 || candidate[1] > 500)
continue;
candidates.push(candidate[0]);
candidates.push(candidate[1]);
if (better(candidate,es))
bestcandidate = candidate.slice();
}
}
}
}
//test corners
for (var i=0; i<8; i+=2){
for (var j=0; j<es.length-2; j+=2){
for (var k=j+2; k<es.length; k+=2){
var candidate = circumcenter(corners[i],corners[i+1],es[j],es[j+1],es[k],es[k+1]);
if (candidate[0] < 0 || candidate[1] < 0 || candidate[0] > 500 || candidate[1] > 500)
continue;
candidates.push(candidate[0]);
candidates.push(candidate[1]);
if (better(candidate,es))
bestcandidate = candidate.slice();
}
}
}
best = 0;
return bestcandidate;
}
// SHOW RESULT ON CANVAS
var canvas = document.getElementById("canvas");
canvas.width = 500; canvas.height = 500;
context = canvas.getContext("2d");
//setInterval(function() {
// CREATE TEST DATA
context.clearRect(0, 0, canvas.width, canvas.height);
candidates = [];
var enemy = [];
for (var i = 0; i < 8; i++) enemy.push(Math.round(Math.random() * 500));
// RUN FUNCTION
var result = f(enemy);
for (var i = 0; i < 8; i+=2) {
paintDot(context, enemy[i], enemy[i+1], 10, "red");
}
for (var i = 0; i < candidates.length; i+=2) {
paintDot(context, candidates[i], candidates[i+1], 7, "green");
}
paintDot(context, result[0], result[1], 18, "blue");
function paintDot(context, x, y, size, color) {
context.beginPath();
context.arc(x, y, size, 0, 6.2831853);
context.closePath();
context.fillStyle = color;
context.fill();
}
//},1500);
<BODY STYLE="margin: 0; border: 0; padding: 0;">
<CANVAS ID="canvas" STYLE="width: 200px; height: 200px; background:
radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.15) 30%, rgba(255,255,255,.3) 32%, rgba(255,255,255,0) 33%) 0 0,
radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.1) 11%, rgba(255,255,255,.3) 13%, rgba(255,255,255,0) 14%) 0 0,
radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.2) 17%, rgba(255,255,255,.43) 19%, rgba(255,255,255,0) 20%) 0 110px,
radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.2) 11%, rgba(255,255,255,.4) 13%, rgba(255,255,255,0) 14%) -130px -170px,
radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.2) 11%, rgba(255,255,255,.4) 13%, rgba(255,255,255,0) 14%) 130px 370px,
radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.1) 11%, rgba(255,255,255,.2) 13%, rgba(255,255,255,0) 14%) 0 0,
linear-gradient(45deg, #343702 0%, #184500 20%, #187546 30%, #006782 40%, #0b1284 50%, #760ea1 60%, #83096e 70%, #840b2a 80%, #b13e12 90%, #e27412 100%);
background-size: 470px 470px, 970px 970px, 410px 410px, 610px 610px, 530px 530px, 730px 730px, 100% 100%;
background-color: #840b2a;"></CANVAS>
<!-- http://lea.verou.me/css3patterns/#rainbow-bokeh -->
</BODY>

Compose an image with floating point layers in webgl

I have trying to render an image in the browser which is built like this:
A bunch of rectangles are each filled with a radial gradient (ideally Gaussian, but can be approximated with a few stopping points
Each rectangle is rotated and translated before being deposited on a drawing area
The image is flattened by summing all the intensities of the rectangles (and cropping to the drawing area's dimensions )
The intensity is rescaled so that the highest intensity is 255 and the lowest 0 (ideally I can apply some sort of gamma correction too)
Finally an image is drawn where the color of each pixel is taken from a palette of 256 colors.
The reason I cannot do this easily with a canvas object is that I need to be working in floating points or I'll lose precision. I do not know in advance what the maximum intensity and minimum intensity will be, so I cannot merely draw transparent rectangles and hope for the best.
Is there a way to do this in webgl? If so, how would I go about it?
You can use the regular canvas to perform this task :
1) check min/max of your rects, so you can build a mapping function double -> [0-255] out of that range.
2) draw the rects in 'lighter' mode == add the component values.
3) you might have a saturation when several rects overlaps : if so, double the mapping range and go to 2).
Now if you don't have saturation just adjust the range to use the full [0-255] range of the canvas, and you're done.
Since this algorithm makes use of getImageData, it might not reach 60 fps on all browsers/devices. But more than 10fps on desktop/Chrome seems perfectly possible.
Hopefully the code below will clarify my description :
//noprotect
// boilerplate
var cv = document.getElementById('cv');
var ctx = cv.getContext('2d');
// rectangle collection
var rectCount = 30;
var rects = buildRandRects(rectCount);
iterateToMax();
// --------------------------------------------
function iterateToMax() {
var limit = 10; // loop protection
// initialize min/max mapping based on rects min/max
updateMapping(rects);
//
while (true) {
// draw the scene using current mapping
drawScene();
// get the max int value from the canvas
var max = getMax();
if (max == 255) {
// saturation ?? double the min-max interval
globalMax = globalMin + 2 * (globalMax - globalMin);
} else {
// no sauration ? Just adjust the min-max interval
globalMax = globalMin + (max / 255) * (globalMax - globalMin);
drawScene();
return;
}
limit--;
if (limit <= 0) return;
}
}
// --------------------------------------------
// --------------------------------------------
// Oriented rectangle Class.
function Rect(x, y, w, h, rotation, min, max) {
this.min = min;
this.max = max;
this.draw = function () {
ctx.save();
ctx.fillStyle = createRadialGradient(min, max);
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.scale(w, h);
ctx.fillRect(-1, -1, 2, 2);
ctx.restore();
};
var that = this;
function createRadialGradient(min, max) {
var gd = ctx.createRadialGradient(0, 0, 0, 0, 0, 1);
var start = map(that.min);
var end = map(that.max);
gd.addColorStop(0, 'rgb(' + start + ',' + start + ',' + start + ')');
gd.addColorStop(1, 'rgb(' + end + ',' + end + ',' + end + ')');
return gd;
}
}
// Mapping : float value -> 0-255 value
var globalMin = 0;
var globalMax = 0;
function map(value) {
return 0 | (255 * (value - globalMin) / (globalMax - globalMin));
}
// create initial mapping
function updateMapping(rects) {
globalMin = rects[0].min;
globalMax = rects[0].max;
for (var i = 1; i < rects.length; i++) {
var thisRect = rects[i];
if (thisRect.min < globalMin) globalMin = thisRect.min;
if (thisRect.max > globalMax) globalMax = thisRect.max;
}
}
// Random rect collection
function buildRandRects(rectCount) {
var rects = [];
for (var i = 0; i < rectCount; i++) {
var thisMin = Math.random() * 1000;
var newRect = new Rect(Math.random() * 400, Math.random() * 400, 10 + Math.random() * 50, 10 + Math.random() * 50, Math.random() * 2 * Math.PI, thisMin, thisMin + Math.random() * 1000);
rects.push(newRect);
}
return rects;
}
// draw all rects in 'lighter' mode (=sum values)
function drawScene() {
ctx.save();
ctx.globalCompositeOperation = 'source-over';
ctx.clearRect(0, 0, cv.width, cv.height);
ctx.globalCompositeOperation = 'lighter';
for (var i = 0; i < rectCount; i++) {
var thisRect = rects[i];
thisRect.draw();
}
ctx.restore();
}
// get maximum value for r for this canvas
// ( == max r, g, b value for a gray-only drawing. )
function getMax() {
var data = ctx.getImageData(0, 0, cv.width, cv.height).data;
var max = 0;
for (var i = 0; i < data.length; i += 4) {
if (data[i] > max) max = data[i];
if (max == 255) return 255;
}
return max;
}
<canvas id='cv' width = 400 height = 400></canvas>

Resources