// thoughts process / where this came from: // 0. because theme is public address - the interractives functionnalities on the website particularly matters (point of focus) // 1. first idea was represent a crowd feeling on a website // * how our perception of sound in public place is influenced by the presence of others (layer of noise, heat, crowded or intimacy feeling) // * how can this be translated to a website (what its specificities?) // 2. decided to have a shape that manifest the people presences on the website (when you connect you directly "feel" the presence of others) // 3. decided to make it interactive - through the chat / or through an added buttons, so the audience can makes this shapes varies according to their experience of the live/sounds // it's like dancing or reacting to a sound in physical space, there is not only presence but actions/reactions // 4. the shape should look/feel connected to sounds in design, but we want to avoid the classic waveform approach // so we went with a linguistic effect (bouba/kiki effect) as an inspiration, // that speaks about how the human brain like to connect certain shapes to certain sounds, and has some cultural invariance // --> pointy shapes make "k" sounds // --> rounded shapes make "b" sounds // 5. conclusion: // a. the shapes number of points is the number of people connected, so it changes with people connecting/disconnecting // b. everybody can react to the point and make their point "dance" by switching it's behavior (pointy or rounded) // 6. because the theme is public address, it then symbolicaly made sense to see this shape as a cable/link/network between the sounds, the artist and the audience // so it has 2 ends that does outside of the canvas, and if a lot of people are there it really feels like a dense network, with lot of connection // WARNING: // the way those path are encoded is PUNK // this is why those curves looks COOL // this is also why it makes no sense in term of svg specification // there is overlayed points, mix of quadratic and cubic bezier, chain reactions, etc. // TODO: // if we use S, we end up with angular move at the begining and end correct that? let svg = document.getElementById("sonic-crowd-network"); let path = document.getElementById("sonic-crowd-network-path"); let viewbox = 1000; // a square let margins = 250; // points don't go in those margins let start_moves = 2; let end_moves = 1; // let number = 4; // let number = 6; // let number = 12; // let number = 18; // let number = 24; // let number = 100; let number = 200; // let possible_move_types = ["L"]; // let possible_move_types = ["S"]; // let possible_move_types = ["T"]; // let possible_move_types = ["S", "T"]; let possible_move_types = ["L", "S", "T"]; // -- map a value to a new range function map_value(value, min, max, new_min, new_max){ return (((value - min) / (max - min)) * (new_max - new_min)) + new_min; } function mid_point(a,b){ return [(a[0]+b[0])/2, (a[1]+b[1])/2]; } // -- create a point in the viewbox with random position function create_random_position(){ let end_point = [Math.random(), Math.random()]; end_point = end_point.map(x => map_value(x, 0, 1, margins, viewbox - margins)); end_point = end_point.map(x => Math.round(x)); return end_point; } function create_random_position_circle(){ let radius = 200; let center = viewbox/2; let steps = 36; // let angle = Math.random() * 2 * Math.PI; let angle = Math.floor(Math.random() * steps) / steps * 2 * Math.PI; let end_point = [Math.cos(angle), Math.sin(angle)]; end_point = end_point.map(x => (x * radius) + center); end_point = end_point.map(x => Math.round(x)); return end_point; } function pick_random(arr){ return arr[Math.floor(Math.random()*arr.length)]; } function craft_move(type, end_point, next_end_point){ // assign end control point according to move type let end_control_point = []; let values = []; switch(type) { case "L": // no need to pick start & end control point values = end_point; break; case "S": // IDENTITY TRICK 1: // the end control point is the next point in path order // (we insert at the start to next is previously added) // the start control point is the symmetry of the last one // by chaining those it creates loops // --> because the end control of a point points toward the next point, // and the start control of a point points toward the previous point end_control_point = next_end_point; values = end_control_point.concat(end_point); break; case "T": // IDENTITY TRICK 2: // we chain two T moves as one // by chaining those it creates explosions, because (from the spec): // The control point is a reflection of the control point of the previous curve command. // If the previous command wasn't a quadratic Bézier curve, // the control point is the same as the curve starting point (current point). // --> this create a chain reaction where all their control point are reflections // until it reaches the first one (a C move - not quadratic), so they're all reflection // of the first start point after the first C move. values = end_point.concat(end_point); break; default: console.log("Error: unrecognised move type"); } let move = { type: type, values: values } return move; } // -- create a new move based on random function create_random_move(next_end_point){ // assign random position let end_point = create_random_position(); // let end_point = create_random_position_circle(); // assign random move type let type = pick_random(possible_move_types); // create the values according to type and positions let new_move = craft_move(type, end_point, next_end_point); return new_move; } // -- parse path data function parse_path(){ // get back d attribute and parse/split let path_data = path.getPathData(); // we chained two T moves as one so we group them two by two let real_path_data = []; for (let i = 0; i < path_data.length; i++) { let type = path_data[i].type; if (type == "T"){ // duplicate values path_data[i].values = path_data[i].values.concat(path_data[i].values); real_path_data.push(path_data[i]); // skip the next i = i+1; } else{ real_path_data.push(path_data[i]); } } return real_path_data; } // -- add a new move, // for when somebody connect to the website function add_move(){ // get back d attribute and parse/split let path_data = parse_path(path); // let start_path_data = path_data.slice(0, -1); //all move but not the last one // let last_move = path_data[path_data.length - 1]; //the last move // let previous_move = start_path_data[start_path_data.length - 1]; // let previous_end_point = previous_move.values.slice(-2); let first_moves = path_data.slice(0,2); let end_path_data = path_data.slice(2, path_data.length); let previous_added_move = end_path_data[0]; let previous_added_end_point = previous_added_move.values.slice(-2); // create new move let new_move = create_random_move(previous_added_end_point); // add the move and update svg // start_path_data.push(new_move); // start_path_data.push(last_move); path_data = first_moves.concat([new_move], end_path_data); path.setPathData(path_data); console.log(path_data); } // -- remove the i move, // for when somebody disconnect of the website // NOTE: this create discontinuity with S type move // because the anchor of the previous than the one removed // will point to nowhere... function remove_move(i){ // get back d attribute and parse/split let path_data = parse_path(path); let number_of_moves = path_data.length - (start_moves+end_moves); let j = start_moves + i; // splice to remove elem i if(i < number_of_moves){ path_data.splice(j,1); } // write back to svg path.setPathData(path_data); } function remove_random_move(){ let path_data = parse_path(path); let number_of_moves = path_data.length - (start_moves+end_moves); let i = Math.floor(Math.random()*number_of_moves); remove_move(i); } // -- switch the type of move i function switch_move(i){ // get back d attribute and parse/split let path_data = parse_path(path); let number_of_moves = path_data.length - (start_moves+end_moves); let j = start_moves + i; if(i < number_of_moves){ // create the new move let move = path_data[j]; let type = move.type; let end_point = move.values.slice(-2); let next_move = path_data[j+1]; let next_end_point = next_move.values.slice(-2); let new_possible_move_types = possible_move_types.filter(t => t != type); let new_type = pick_random(new_possible_move_types); let new_move = craft_move(new_type, end_point, next_end_point); // replace old move by new move path_data.splice(j,1,new_move); // write back to svg path.setPathData(path_data); } } function switch_random_move(){ let path_data = parse_path(path); let number_of_moves = path_data.length - (start_moves+end_moves); let i = Math.floor(Math.random()*number_of_moves); switch_move(i); } // -- init the path // as a line with the two ends points and anchors function init_path(){ let tpoint = [viewbox/2, 0]; // option 1: then the explosion is centered (at least the first one) // let mpoint = [viewbox/2, viewbox/2]; // option 2: the first explosion center is random (= this point) let mpoint = create_random_position_circle(); let bpoint = [viewbox/2, viewbox]; let anchor_length = viewbox/2; let init_move = { type: "M", values: tpoint } let start_move = { type: "C", values: [tpoint[0], tpoint[1] + anchor_length, //start control point mpoint[0], mpoint[1], //end control point (=null) mpoint[0], mpoint[1]] //end point } let last_move = { type: "C", values: [mpoint[0], mpoint[1], //start control point (=null) bpoint[0], bpoint[1] - anchor_length, //end control point bpoint[0], bpoint[1]] //end point } let moves = [init_move, start_move, last_move]; path.setPathData(moves); } function save_svg(){ let path_data = parse_path(path); let number_of_moves = path_data.length - (start_moves+end_moves); let fileContent = svg.outerHTML; let bb = new Blob([fileContent ], {type: "image/svg+xml"}); let a = document.createElement('a'); a.download = 'PA_'+possible_move_types.join("-")+'_'+number_of_moves+'.svg'; a.href = window.URL.createObjectURL(bb); a.click(); } // -- create a random path of n move (+ start and end) function random_path(n){ init_path(); for (let i = 0; i < n; i++) { add_move(); } } // init_path(); random_path(number);