UNPKG

attractors

Version:

Generating animated attractors for <canvas>

867 lines (739 loc) 27 kB
/** * This code is ugly, but made with love by * @author annemenini * @author steren */ var DEBUG_FLAG = false; var SIZE_SHADOW = 16; var SHADOW_IMAGE = 'shadow-o30-ellipse-' var SHADOW_OPACITY = 0.03; var DELTA_SHADOW_X = 1; var DELTA_SHADOW_Y = 1; var NEW_SEED_CREATION_PROBABILITY = 0; /** Distance to move the points at each frame. */ // Note: We prefer using a constant distance per frame rather than defining a speed. // The speed would result in bad results on low framerate. var STEP_DISTANCE = 1; /** under this width, do not subdivise the quadratic and cubic bezier curves in the text's path */ var TEXT_MIN_WIDTH_TO_SUBDIVISE = 500; var PROBABILITY_POINT_APPEARS_NEAR_TEXT = 0.2; var RANDOMBACKGROUND = 0.05; var DEFAULT_IMPACT_DISTANCE = 1/400; var ATTRACTOR_RADIUS_MIN = 1/50; var ATTRACTOR_RADIUS_MAX = 16 * ATTRACTOR_RADIUS_MIN; var SUBDIVISE_NOGO = 16; // decrease to subdivise more var FONT = 'CamBam/1CamBam_Stick_2.ttf'; //var FONT = 'Codystar/Codystar-Regular.ttf'; //var FONT = 'Fredoka_One/FredokaOne-Regular.ttf'; var canvas, ctx; var shadow; var pixelRatio; var colorSize; var colors; var pointsX, pointsY; var canvasScreenWidth, canvasScreenHeight; var canvasRealWidth, canvasRealHeight; /** Array containing the info if the shadow of a given particule should be drawn */ var drawShadowAtPoint; /** * Array of attractors. * An attractor has: * - x * - y * - weight: between -1 and 1 * - radius: impact distance of this attractor */ var attractors; /** * Array of special attractors. * Special attractors are used for text and NoGo zones. * A special attractor is a segment. */ var specialAttractors; var hasNogoZone; var loadedFont; /** Stores strings of SVG paths (when config.svg is true)n*/ var svgPathArray; /** characteristic distance of the image */ var D; /** bounding box of the main text */ var textTopLeft, textBottomRight; /** Bounding boxes of the no go zones */ var noGoBBoxes; var noGoTopLeft, noGoBottomRight; /** Array of bounding boxes aroung the special attractors **/ var specialAttractorsBoundingBoxes; var typedText = ''; if(config.text) { var folder = config.root || ''; opentype.load(folder + 'fonts/' + FONT, function(err, font) { console.log(font); loadedFont = font; init(); animate(); }); } else { init(); animate(); } function init() { initialize(config); } function initialize(config) { colors = [] colors.push(config.color1); colors.push(config.color2); specialAttractorsBoundingBoxes = []; // if text string is empty, do not consider text attractors at all if(config.text) { var text = config.text; var cleanPath = false; // set cleanPath to true is the text corresponds to the clean specialAttractor if(text == '13 8 2016') { cleanPath = true; } textTopLeft = {}; textBottomRight = {}; specialAttractorsBoundingBoxes.push({ topLeft: textTopLeft, bottomRight: textBottomRight }); } // initialize globals pointsX = []; pointsY = []; drawShadowAtPoint = []; hasNogoZone = config.nogo_zone; if(hasNogoZone) { config.noGoImpactDistance = config.noGoImpactDistance || DEFAULT_IMPACT_DISTANCE; noGoBBoxes = []; } if(!config.textGaussianImpactDistance) { config.textGaussianImpactDistance = DEFAULT_IMPACT_DISTANCE; } pixelRatio = config.pixelratio || window.devicePixelRatio || 1; canvas = document.getElementById(config.id); ctx = canvas.getContext("2d", {alpha : false}); resizeCanvas(); D = Math.max(canvas.width, canvas.height); shadow = new Image(); var folder = config.root || ''; shadow.src = folder + SHADOW_IMAGE + SIZE_SHADOW + 'px.png'; paintCanvasWithBackground(); initAttractors(config.nb_attractors, ATTRACTOR_RADIUS_MIN, ATTRACTOR_RADIUS_MAX); specialAttractors = []; if(config.text) { initTextSpecialAttractors(text, {x: config.text_position_x, y: config.text_position_y}, config.text_width_ratio, cleanPath, PROBABILITY_POINT_APPEARS_NEAR_TEXT); } if(hasNogoZone) { initNoGoZoneSpecialAttractors(); initNoGoCirclesSpecialAttractors(); } initPoints(config.particule_density); initDrawShadow(); colorSize = Math.ceil(pointsX.length / colors.length); ctx.lineWidth = config.line_width * pixelRatio; if(config.svg) { // generate empty strings for SVG paths svgPathArray = []; for(var p = 0; p < pointsX.length; p++) { svgPathArray.push(''); } } } function resizeCanvas() { canvasScreenWidth = canvas.clientWidth; canvasScreenHeight = canvas.clientHeight; canvasRealWidth = canvasScreenWidth * pixelRatio; canvasRealHeight = canvasScreenHeight * pixelRatio; canvas.width = canvasRealWidth; canvas.height = canvasRealHeight; //canvas.style.width = canvasScreenWidth + 'px'; //canvas.style.height = canvasScreenHeight + 'px'; } function paintCanvasWithBackground() { ctx.fillStyle = config.background_color; ctx.fillRect(0, 0, canvasRealWidth, canvasRealHeight); } function animate(timestamp) { requestAnimationFrame( animate ); render(timestamp); } function render(timestamp) { // cut the number of points per number of color, and paint all of the same color at once: // start a path and add each segment to it, and only then, paint it. // This increases performances instead of painting each segment after the other. for(var c = 0; c < colors.length; c++) { ctx.beginPath(); ctx.strokeStyle = colors[c]; for (var i = c * colorSize; i < (c+1) * colorSize; i++ ) { if( Math.random() < NEW_SEED_CREATION_PROBABILITY ) { var newSeed = getPositionOutsideOfSpecialAttractorGaussian(); pointsX[i] = newSeed[0]; pointsY[i] = newSeed[1]; } else { var oldX = pointsX[i]; var oldY = pointsY[i]; var newPosition = getNewPosition(oldX, oldY, i); ctx.moveTo(oldX,oldY); ctx.lineTo(newPosition[0], newPosition[1]); pointsX[i] = newPosition[0]; pointsY[i] = newPosition[1]; } } ctx.stroke(); } if(config.svg) { for(var p = 0; p < pointsX.length; p++) { svgPathArray[p] += ['L', pointsX[p], ' ', pointsY[p], ' '].join(''); } } // draw shadow ctx.globalAlpha = SHADOW_OPACITY; for (var i = 0; i < pointsX.length; i++ ) { if(drawShadowAtPoint[i]) { ctx.drawImage(shadow, pointsX[i] - DELTA_SHADOW_X * pixelRatio, pointsY[i] - DELTA_SHADOW_Y * pixelRatio, SIZE_SHADOW * pixelRatio * config.shadow_scale, SIZE_SHADOW * pixelRatio * config.shadow_scale); } } ctx.globalAlpha = 1.0; } function getNewPosition(x, y, index) { var fieldXY = field(x,y); // if distance is small, reduce probability to draw shadow drawShadowAtPoint[index] = true; if( Math.random() > (fieldXY[0]*fieldXY[0] + fieldXY[1]*fieldXY[1]) ) { drawShadowAtPoint[index] = false; } var ux = -1 * STEP_DISTANCE * pixelRatio * fieldXY[1]; var uy = STEP_DISTANCE * pixelRatio * fieldXY[0]; var newX = x + ux; var newY = y + uy; return [newX, newY]; } /** * Vector of the field at a given point */ function field(x, y) { var ux = 0; var uy = 0; for(var a = 0; a < attractors.length; a++) { var attractor = attractors[a]; var d2 = (x - attractor.x) * (x - attractor.x) + (y - attractor.y) * (y - attractor.y); var d = Math.sqrt(d2); var weight = attractor.weight * Math.exp( -1 * d2 / (attractor.radius * attractor.radius) ); ux += weight * (x - attractor.x) / d; uy += weight * (y - attractor.y) / d; } var norm = Math.sqrt(ux * ux + uy * uy); ux = ux / norm; uy = uy / norm; // If we are near a special attractor, add its contribution to the field if(isNearSpecialAttractor(x,y)) { var closestTextPoint = findClosestPointOnSpecialAttractor(x,y); var textUx = (closestTextPoint.specialAttractor.direction || 1) * (x - closestTextPoint.originX); var textUy = (closestTextPoint.specialAttractor.direction || 1) * (y - closestTextPoint.originY); var norm = Math.sqrt(textUx*textUx + textUy*textUy); textUx = textUx / norm; textUy = textUy / norm; // Combine fields if(closestTextPoint.specialAttractor.type == 'cos') { if(closestTextPoint.distance < closestTextPoint.specialAttractor.impactDistance) { textWeight = 0.5 * (1 + Math.cos(Math.PI * closestTextPoint.distance / (closestTextPoint.specialAttractor.impactDistance))); } else { textWeight = 0; } } else { textWeight = Math.exp( -1 * closestTextPoint.distance * closestTextPoint.distance / (closestTextPoint.specialAttractor.impactDistance * D * D) ); } ux = (1-textWeight)*ux + textWeight * textUx; uy = (1-textWeight)*uy + textWeight * textUy; } return [ux, uy]; } function isNearSpecialAttractor(x,y) { var near = D/8; for(var b = 0; b < specialAttractorsBoundingBoxes.length; b++) { if( x - (specialAttractorsBoundingBoxes[b].topLeft.x - near) > 0 && x - (specialAttractorsBoundingBoxes[b].bottomRight.x + near) < 0 && y - (specialAttractorsBoundingBoxes[b].topLeft.y - near) > 0 && y - (specialAttractorsBoundingBoxes[b].bottomRight.y + near) < 0) { return true; } } return false; } /** @param particuleDensity: number of particule for a square of 1000 * 1000 pixels */ function initPoints(particuleDensity) { // for a device with higher pixel ratio, put more particules. // but do not put pixelRatio * pixelRatio more particules for performances reasons var nbParticules = pixelRatio * particuleDensity * canvasScreenWidth * canvasScreenHeight / 1000000 for(var i = 0; i < nbParticules; i++) { //var newSeed = getPositionOutsideOfSpecialAttractorSquare(4/5); //var newSeed = getPositionOutsideOfSpecialAttractorGaussian(); var newSeed = getPositionOutsideOfSpecialAttractorGaussian(config.init_scale); pointsX.push(newSeed[0]); pointsY.push(newSeed[1]); } } function initDrawShadow() { for(var p = 0; p < pointsX.length; p++) { drawShadowAtPoint.push(true); } } function normalRand() { while(true) { var x = Math.random(); var y = Math.exp(-1*Math.pow(x-0.5, 2)/0.1); if(y>Math.random()) { return x; } } } function initAttractors(nbAtractors, min, max) { attractors = []; var minW = -1; var maxW = 1; var minD = min * D; var maxD = max * D; for( var a = 0; a < nbAtractors; a++) { var attractor = {}; attractor.x = Math.random() * (canvasRealWidth - 1); attractor.y = Math.random() * (canvasRealHeight - 1); attractor.weight = Math.random() * (maxW - minW) + minW; attractor.radius = Math.random() * (maxD - minD) + minD; attractors.push(attractor); if(DEBUG_FLAG) { if(attractor.weight>0) { drawHelperCircle(attractor.x, attractor.y, 100*attractor.weight, 'green'); } else { drawHelperCircle(attractor.x, attractor.y, -100*attractor.weight, 'red'); } } } } function initTextSpecialAttractors(text, textPositionPercent, textWidthRatio, cleanPath, probabilityPointAppearsNearText) { var textPathTopLeft = {x: Infinity, y: Infinity}; var textPathBottomRight = {x: -Infinity, y: -Infinity}; var fontSize = canvasRealWidth / textWidthRatio; // measure the size of a single character var path = loadedFont.getPath(text, 0, 0, fontSize); // get the bounding box of the text path for( var c = 0; c < path.commands.length; c++) { if (path.commands[c].x < textPathTopLeft.x) {textPathTopLeft.x = path.commands[c].x}; if (path.commands[c].y < textPathTopLeft.y) {textPathTopLeft.y = path.commands[c].y}; if (path.commands[c].x > textPathBottomRight.x) {textPathBottomRight.x = path.commands[c].x}; if (path.commands[c].y > textPathBottomRight.y) {textPathBottomRight.y = path.commands[c].y}; } var textWidth = textPathBottomRight.x - textPathTopLeft.x; var textHeight = textPathBottomRight.y - textPathTopLeft.y; var textX = canvasRealWidth * textPositionPercent.x / 100 - textWidth / 2; var textY = canvasRealHeight * textPositionPercent.y / 100 + textHeight / 2; textTopLeft.x = canvasRealWidth * textPositionPercent.x / 100 - textWidth / 2; textTopLeft.y = canvasRealHeight * textPositionPercent.y / 100 - textHeight / 2; textBottomRight.x = textTopLeft.x + textWidth; textBottomRight.y = textTopLeft.y + textHeight; if(DEBUG_FLAG) { loadedFont.drawPoints(ctx, text, textX, textY, fontSize); } var subdiviseBezier = false; if(textWidth > TEXT_MIN_WIDTH_TO_SUBDIVISE) { subdiviseBezier = true; probabilityPointAppearsNearText = probabilityPointAppearsNearText / 2; } if(cleanPath) { var useThisCommand = []; // 1 useThisCommand.push(0); for(var i=0; i<3; i++) {useThisCommand.push(1);} for(var i=0; i<2; i++) {useThisCommand.push(0);} // 3 for(var i=0; i<25; i++) {useThisCommand.push(1);} for(var i=0; i<25; i++) {useThisCommand.push(0);} // 8 for(var i=0; i<34; i++) {useThisCommand.push(1);} for(var i=0; i<35; i++) {useThisCommand.push(0);} // 2 for(var i=0; i<15; i++) {useThisCommand.push(0);} for(var i=0; i<17; i++) {useThisCommand.push(1);} for(var i=0; i<2; i++) {useThisCommand.push(0);} // 0 for(var i=0; i<18; i++) {useThisCommand.push(1);} // 1 useThisCommand.push(0); for(var i=0; i<3; i++) {useThisCommand.push(1);} for(var i=0; i<2; i++) {useThisCommand.push(0);} // 6 for(var i=0; i<21; i++) {useThisCommand.push(1);} for(var i=0; i<21; i++) {useThisCommand.push(0);} } for( var c = 0; c < (path.commands.length-1); c++) { if(!cleanPath || (useThisCommand[c]==1 && useThisCommand[c+1]==1)) { var command2 = path.commands[c+1]; var commandToExecute = command2.type; if(!subdiviseBezier && (command2.type=="C" || command2.type=="Q")) { commandToExecute = "L"; } var command1 = path.commands[c]; // add points near text if( Math.random() < probabilityPointAppearsNearText ) { pointsX.push(textX + command1.x+Math.random()-0.5); pointsY.push(textY + command1.y+Math.random()-0.5); } switch(commandToExecute) { case "L": var specialAttractor = {}; specialAttractor.x1 = textX + command1.x; specialAttractor.y1 = textY + command1.y; specialAttractor.x2 = textX + command2.x; specialAttractor.y2 = textY + command2.y; specialAttractor.impactDistance = config.textGaussianImpactDistance * pixelRatio; specialAttractors.push(specialAttractor); // if a real L (line) if(command2.type == "L") { if( Math.random() < 1 ) { pointsX.push(textX + command1.x+Math.random()-0.5); pointsY.push(textY + command1.y+Math.random()-0.5); } } break; case "Q": var specialAttractor = {}; var t = 1/2; specialAttractor.x1 = textX + command1.x; specialAttractor.y1 = textY + command1.y; var x = bezier([t], [command1.x, command2.x1, command2.x]); var y = bezier([t], [command1.y, command2.y1, command2.y]); specialAttractor.x2 = textX + x[0]; specialAttractor.y2 = textY + y[0]; specialAttractor.impactDistance = config.textGaussianImpactDistance * pixelRatio; specialAttractors.push(specialAttractor); var specialAttractor2 = {}; specialAttractor2.x1 = specialAttractor.x2; specialAttractor2.y1 = specialAttractor.y2; specialAttractor2.x2 = textX + command2.x; specialAttractor2.y2 = textY + command2.y; specialAttractor2.impactDistance = config.textGaussianImpactDistance * pixelRatio; specialAttractors.push(specialAttractor2); break; case "C": var specialAttractor = {}; var x = bezier([1/3, 2/3], [command1.x, command2.x1, command2.x2, command2.x]); var y = bezier([1/3, 2/3], [command1.y, command2.y1, command2.y2, command2.y]); specialAttractor.x1 = textX + command1.x; specialAttractor.y1 = textY + command1.y; specialAttractor.x2 = textX + x[0]; specialAttractor.y2 = textY + y[0]; specialAttractor.impactDistance = config.textGaussianImpactDistance * pixelRatio; specialAttractors.push(specialAttractor); var specialAttractor2 = {}; specialAttractor2.x1 = specialAttractor.x2; specialAttractor2.y1 = specialAttractor.y2; specialAttractor2.x2 = textX + x[1]; specialAttractor2.y2 = textY + y[1]; specialAttractor2.impactDistance = config.textGaussianImpactDistance * pixelRatio; specialAttractors.push(specialAttractor2); var specialAttractor3 = {}; specialAttractor3.x1 = specialAttractor2.x2; specialAttractor3.y1 = specialAttractor2.y2; specialAttractor3.x2 = textX + command2.x; specialAttractor3.y2 = textY + command2.y; specialAttractor3.impactDistance = config.textGaussianImpactDistance * pixelRatio; specialAttractors.push(specialAttractor3); break; default: // "M", "Z" } } } } /** Creates a circular no go zone */ function createNoGoCircleSpecialAttractors(x, y, radius, impactDistance, type, direction) { var circleSubDiv = radius * SUBDIVISE_NOGO / 20; for( var i = 0; i < circleSubDiv; i++ ) { var specialAttractor = {}; specialAttractor.x1 = (x + radius * Math.cos(2*Math.PI / circleSubDiv * i)) * pixelRatio; specialAttractor.y1 = (y + radius * Math.sin(2*Math.PI / circleSubDiv * i)) * pixelRatio; specialAttractor.x2 = (x + radius * Math.cos(2*Math.PI / circleSubDiv * (i+1))) * pixelRatio; specialAttractor.y2 = (y + radius * Math.sin(2*Math.PI / circleSubDiv * (i+1))) * pixelRatio; specialAttractor.impactDistance = impactDistance * pixelRatio; specialAttractor.type = type; specialAttractor.direction = direction; specialAttractors.push(specialAttractor); if(DEBUG_FLAG) { drawHelperCircle(specialAttractor.x1, specialAttractor.y1, 1); } } var bbox = { topLeft: { x: (x - radius) * pixelRatio, y: (y - radius) * pixelRatio, }, bottomRight: { x: (x + radius) * pixelRatio, y: (y + radius) * pixelRatio, }, }; specialAttractorsBoundingBoxes.push(bbox); noGoBBoxes.push(bbox); } function initNoGoCirclesSpecialAttractors() { if(!config.nogoCircles) {return}; for( circle of config.nogoCircles ) { var impactDistance = circle.impactDistance || DEFAULT_IMPACT_DISTANCE; createNoGoCircleSpecialAttractors(circle.x, circle.y, circle.radius, impactDistance, circle.type, circle.direction); } } /** creates a nogo zone from the given parameters: consig.nogoParam */ function initNoGoZoneSpecialAttractors() { if(!config.nogoParam) {return}; var noGoTopLeft = {}; var noGoBottomRight = {}; noGoTopLeft.x = Infinity; noGoTopLeft.y = Infinity; noGoBottomRight.x = -Infinity; noGoBottomRight.y = -Infinity; // define no go zone via a few points var noGoZoneBox = []; noGoZoneBox.push({ x: config.nogoParam.x * pixelRatio, y: config.nogoParam.y * pixelRatio, x1:0, y1:0, x2:0, y2:0}); noGoZoneBox.push({ x: (config.nogoParam.x + config.nogoParam.width) * pixelRatio, y: config.nogoParam.y * pixelRatio, x1:0, y1:0, x2:0, y2:0}); noGoZoneBox.push({ x: (config.nogoParam.x + config.nogoParam.width) * pixelRatio, y: (config.nogoParam.y + config.nogoParam.height) * pixelRatio, x1:0, y1:0, x2:0, y2:0}); noGoZoneBox.push({ x: config.nogoParam.x * pixelRatio, y: (config.nogoParam.y + config.nogoParam.height) * pixelRatio, x1:0, y1:0, x2:0, y2:0}); var n = noGoZoneBox.length; // compute the Bezier handle for(var i=0; i<n; i++) { var P1 = noGoZoneBox[(i-1+n)%n]; var P0 = noGoZoneBox[i]; var P2 = noGoZoneBox[(i+1)%n]; var L = Math.sqrt(Math.pow(P2.x - P1.x, 2) + Math.pow(P2.y - P1.y, 2)); var l1 = Math.sqrt(Math.pow(P0.x - P1.x, 2) + Math.pow(P0.y - P1.y, 2)); var l2 = Math.sqrt(Math.pow(P2.x - P0.x, 2) + Math.pow(P2.y - P0.y, 2)); P0.x1 = P0.x + (l1/3) * (P1.x - P2.x) / L; P0.x2 = P0.x + (l2/3) * (P2.x - P1.x) / L; P0.y1 = P0.y + (l1/3) * (P1.y - P2.y) / L; P0.y2 = P0.y + (l2/3) * (P2.y - P1.y) / L; noGoZoneBox[i] = P0; } // subdivise Bezier curve to create segments for(var i=0; i<n; i++) { var PS = noGoZoneBox[i]; var PE = noGoZoneBox[(i+1)%n]; var L = Math.sqrt(Math.pow(PE.x - PS.x, 2) + Math.pow(PE.y - PS.y, 2)); var T = []; var nT = Math.ceil(L/SUBDIVISE_NOGO); for(var k=0; k<nT; k++) { T.push(k/(nT-1)); } var Bx = bezier(T, [PS.x, PS.x2, PE.x1, PE.x]); var By = bezier(T, [PS.y, PS.y2, PE.y1, PE.y]); for(var j=0; j<(nT-1); j++) { var specialAttractor = {}; specialAttractor.x1 = Bx[j]; specialAttractor.y1 = By[j]; specialAttractor.x2 = Bx[j+1]; specialAttractor.y2 = By[j+1]; specialAttractor.impactDistance = config.noGoImpactDistance * pixelRatio; specialAttractor.type = config.nogoZoneType; specialAttractors.push(specialAttractor); if (specialAttractor.x1 > noGoBottomRight.x) {noGoBottomRight.x = specialAttractor.x1}; if (specialAttractor.y1 > noGoBottomRight.y) {noGoBottomRight.y = specialAttractor.y1}; if (specialAttractor.x1 < noGoTopLeft.x) {noGoTopLeft.x = specialAttractor.x1}; if (specialAttractor.y1 < noGoTopLeft.y) {noGoTopLeft.y = specialAttractor.y1}; specialAttractorsBoundingBoxes.push({ topLeft: noGoTopLeft, bottomRight: noGoBottomRight }); noGoBBoxes.push({ topLeft: noGoTopLeft, bottomRight: noGoBottomRight }); if(DEBUG_FLAG) { drawHelperCircle(specialAttractor.x1, specialAttractor.y1, 1); } } } } function drawHelperCircle(centerX, centerY, radius, fillStyle) { fillStyle = fillStyle || 'green'; ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI, false); ctx.fillStyle = fillStyle ; ctx.fill(); ctx.lineWidth = 5; ctx.strokeStyle = '#003300'; ctx.stroke(); } /** * Finds the closest point to any special attractor segment */ function findClosestPointOnSpecialAttractor(x,y) { var nSpecialAttractor = specialAttractors.length; var currentMinDistance = 0; var currentSpecialAttractor; var ox = 0; var oy = 0; for(var a=0; a<nSpecialAttractor; a++) { var specialAttractor = specialAttractors[a]; var closestSegmentPoint = distanceToSegment(specialAttractor.x1, specialAttractor.y1, specialAttractor.x2, specialAttractor.y2, x, y); if(a == 0 || (closestSegmentPoint.distance) < currentMinDistance) { currentMinDistance = closestSegmentPoint.distance; currentSpecialAttractor = specialAttractor; ox = closestSegmentPoint.originX; oy = closestSegmentPoint.originY; } } return { distance: currentMinDistance, specialAttractor: currentSpecialAttractor, originX: ox, originY: oy } } function isInNoGoZone(x,y) { if(hasNogoZone) { for( let bb of noGoBBoxes ) { if( x - bb.topLeft.x > 0 && x - bb.bottomRight.x < 0 && y - bb.topLeft.y > 0 && y - bb.bottomRight.y < 0) { return true; } } } else{ return false; } return false; } function getPositionOutsideOfSpecialAttractorSquare(sizeRatio) { if(!sizeRatio) {sizeRatio = 1;} return getPositionOutsideOfSpecialAttractor(Math.random, sizeRatio); } function getPositionOutsideOfSpecialAttractorGaussian(sizeRatio) { if(!sizeRatio) {sizeRatio = 1;} return getPositionOutsideOfSpecialAttractor(normalRand, sizeRatio); } /** * @param f: function to use to return point between 0 and 1 * @param sizeRatio: scaling factor outside out which nothing will be used */ function getPositionOutsideOfSpecialAttractor(f, sizeRatio) { if(!sizeRatio) {sizeRatio = 1;} while(true) { var x = f() * canvasRealWidth * sizeRatio + canvasRealWidth * ( 1 - sizeRatio) / 2; var y = f() * canvasRealHeight * sizeRatio + canvasRealHeight * ( 1 - sizeRatio) / 2; if(!isInNoGoZone(x,y)) { return [x, y]; } } } function distanceToSegment(x1, y1, x2, y2, x, y) { var l = (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1); var d = (x-x1)*(x2-x1) + (y-y1)*(y2-y1); if(d>l) { return { distance: Math.sqrt((x2-x) * (x2-x) + (y2-y) * (y2-y)), originX: x2, originY: y2 } } else { if(d<0) { return { distance: Math.sqrt((x1-x) * (x1-x) + (y1-y) * (y1-y)), originX: x1, originY: y1 } } else { var ox = x1 + (x2-x1)*d/l; var oy = y1 + (y2-y1)*d/l; return { distance: Math.sqrt( (ox-x) * (ox-x) + (oy-y) * (oy-y)), originX: ox, originY: oy } } } } function bezier(T, X) { var n = X.length; var w = [0,1,0]; for(var i=1; i<n; i++) { var wNew = [0]; for(var j=0; j<i+1; j++) { wNew.push(w[j] + w[j+1]); } wNew.push(0); w = wNew; } var res = []; var nT = T.length; for(var i=0; i<nT; i++) { var x = 0; var t = T[i]; for(var j=0; j<n; j++) { x += w[j+1]*Math.pow(1-t,n-1-j)*Math.pow(t,j)*X[j]; } res.push(x); } return res; } function randomIntFromInterval(min,max) { return Math.floor(Math.random()*(max-min+1)+min); } /** * @param config.one_path: if true, will create ne <path> per line, default to false */ function generateSVG() { var getPathBegin = function(p) { p = p || 0; var c = Math.floor(p / colorSize); return ["<path fill='none' stroke='", colors[c], "' stroke-width='", config.line_width * pixelRatio, "' d='"].join(''); } var pathEnd = "' />\n"; var svgcontent = ["<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ", canvasRealWidth , " ", canvasRealHeight, "' width='", canvasRealWidth, "' height='", canvasRealHeight, "' style='background-color: ", config.background_color,";'>\n"].join(''); if(config.one_path) { svgcontent += getPathBegin(); } for(var s = 0; s < svgPathArray.length - 1; s++) { if(!config.one_path) { svgcontent += getPathBegin(s); } svgcontent += ["M", svgPathArray[s].substring(1)].join(''); if(!config.one_path) { svgcontent += pathEnd; } } if(config.one_path) { svgcontent += pathEnd; } svgcontent += "</svg>"; saveSVG(svgcontent, 'ninis.svg'); } var saveSVG = (function () { var a = document.createElement("a"); document.body.appendChild(a); a.style = "display: none"; return function (data, fileName) { var blob = new Blob([data], {type: "image/svg+xml"}), url = window.URL.createObjectURL(blob); a.href = url; a.download = fileName; a.click(); window.URL.revokeObjectURL(url); }; }());