projection test

This commit is contained in:
Dooho Yi 2022-11-25 14:21:45 +09:00
parent cc44e24e60
commit 206d768fcb
3 changed files with 844 additions and 2 deletions

View file

@ -46,7 +46,7 @@ body::-webkit-scrollbar {
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
/* z-index usage is up to you.. although there is no need of using it because the default stack context will work. */ /* z-index usage is up to you.. although there is no need of using it because the default stack context will work. */
z-index: -1; // this is optional z-index: -1;
} }
.arrow { .arrow {

View file

@ -80,11 +80,38 @@ fastify.get("/", async function (request, reply) {
}); });
} }
} }
});
//get '/projection'
fastify.get("/projection", async function (request, reply) {
//get list
let list = await fs.readdir(process.env.userdata);
list.reverse();
// console.log(list);
let folders = [];
for (const item of list) {
let json = await fs.readFile(process.env.userdata + item + '/fields.json')
.catch((err) => {
console.error(err);
});
if (json != undefined) {
var fields = JSON.parse(json.toString('utf8'));
folders.push({
foldername: item,
group: fields.group,
title: fields.title,
comment: fields.comment,
anchor: fields.anchor
});
}
}
// console.log(folders); // console.log(folders);
// //
reply.view("/src/pages/parade.html", { reply.view("/src/pages/projection.html", {
list: folders list: folders
}); });
}); });

815
src/pages/projection.html Normal file
View file

@ -0,0 +1,815 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>흐름을 향하여 걷는 Walking towards the Flow</title>
<meta itemprop="description" content="온라인 공간에 소리의 행렬을 만드는 사운드 퍼레이드입니다.">
<meta itemprop="image" content="/thumbnail.jpg">
<link rel="stylesheet" href="/default.css" />
<style>
html,
body {
overflow-x: hidden;
}
.bg {
position: fixed;
top: 0;
left: 0;
background: unset;
background-color: black;
}
#defaultCanvas0 {
display: block;
}
.clear {
display: block;
}
img {
width: 100%;
max-width: 500px;
}
#p5 {
display: inline-block;
}
.tools {
padding: 1em 1em;
display: inline-block;
vertical-align: top;
}
.penselect {
margin: 1em 0em;
}
</style>
<script src="/js/p5-v1.1.9.min.js"></script>
<script src="/js/socket-v2.3.0.io.slim.js"></script>
<script src="/js/Tone-14.8.36.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500&display=swap" rel="stylesheet" />
</head>
<body>
<div class="bg"></div>
<div id="top"></div>
<div class="parade"></div>
<div class="entry">
<div class="notice">
<div class="sticky">
<div class="first"><a href="/en/">EN</a></div>
<div class="second"><a href="#top">퍼레이드</a></div>
</div>
<section>
<h1>소개</h1>
<p>
«흐름을 향하여 걷는»은 온라인 공간에서 열리는 사운드 퍼레이드입니다. 이 소리의 행렬은 여러분이 보내주시는 소리가 모여 만들어지며, 누구나 참여하실 수 있습니다.
</p>
<p>
소리가 흐르는 길은 별들이 순회하는 저 은하수와 같이, 언제나 그 자리에 있으면서 여러분이 소리를 듣고 있든 그렇지 않든 흐르고 있습니다.
</p>
<p>
이 영원한 퍼레이드의 영속성은 우리에게 최후의 안도를 줍니다. 때로는 자신이 위성궤도를 무한히 돌고 있는 우주 부스러기라는 생각이 들다가도, 그것이 관측되는 순간 더는 외로움이나 잊혀짐이 아니라
기억됨이 됩니다.
</p>
<p>
«흐름을 향하여 걷는»은 온사이트 공연으로 함께 하실 수도 있습니다. 11월 27일(일) 진행하는 공연에서는 웹사이트에 모인 소리들이 물리적 공간에 펼쳐지고, 관객은 함께 걷고 멈추며 흐름에
공감해주신 분들의 소리를 듣습니다.
</p>
<p>
온라인으로 진행하는 소리 모집과 온사이트 공연에 많은 관심과 참여 바랍니다.<br />
</p>
<br>
<div class="info">
<p>
소리 모집<br />
2022년 11월 13일(일)26일(토)
</p>
<p>
공연 일정<br />
2022년 11월 27일(일), 16:00
</p>
<p>
공연 장소<br />
서울시립미술관 서소문본관 1층 로비, 2층 러닝스테이션, 옥상정원
</p>
<p>
공연 구성<br />
무리0 약속들 — 무리1 깃발들 — 무리2 신체들 — 무리3 사물들 — 무리4 누구들
</p>
<p>
공연 신청<br />
<a href="https://zrr.kr/aHTS" target="_blank">https://zrr.kr/aHTS</a>
</p>
<p>
작가<br />
<a href="http://dianaband.info" target="_blank">다이애나밴드</a> × <a href="https://cgyoon.kr/"
target="_blank">윤충근</a>
</p>
<p>
문의<br />wonjung24@gmail.com
</p>
</div>
</section>
<section class="participation">
<h1>참여 방법</h1>
<ol>
<li>1. 무리의 특성에 따라 녹음기기나 핸드폰으로 30초 가량의 소리를 녹음해주세요.
<ol class="category">
<li>• 무리1 깃발들 — 좋아하는 것, 가치에 대해 다섯 번 외쳐주세요.</li>
<li>• 무리2 신체들 — 몸에서 나는 소리를 녹음해 보아요. 박수, 휘파람도 좋아요.</li>
<li>• 무리3 사물들 — 주변 사물들의 소리를 찾아 주세요. 뽁뽁이 소리, 구슬 소리</li>
<li>• 무리4 누구들 — 누구의 소리를 모아주세요. 반려동물, 물 소리, 산책의 장소</li>
</ol>
</li>
<li>2. 녹음한 소리 파일을 업로드해주세요.</li>
<li>3. 소리의 제목과 묘사을 입력한 뒤, 소리의 모양을 그려 제출해주세요.</li>
<li>* 혐오 표현이 포함된 음원을 업로드하시면 삭제됩니다.</li>
</ol>
</section>
<section>
<h1>소리 제출</h1>
<form id="form" method="POST" enctype="multipart/form-data">
<ul class="submit">
<li>
<h3>1. 소리 유형</h3>
<ul>
<li>
<input type="radio" name="group" autocomplete="off" value="flag" /> 무리1 깃발들
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="body" /> 무리2 신체들
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="object" /> 무리3 사물들
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="any" required /> 무리4 누구들
</li>
</ul>
</li>
<li>
<h3>2. 소리 파일</h3>
<input type="file" name="audiofile" autocomplete="off" required />
</li>
<li>
<h3>3. 소리 제목</h3>
<input id="title" name="title" type="text" autocomplete="off" required />
</li>
<li>
<h3>4. 소리 묘사</h3>
<input id="comment" name="comment" type="text" autocomplete="off" required />
</li>
<!-- pixels.png ~ made with p5.js -->
<li class="noscroll">
<h3>5. 소리 모양</h3>
<div>
<div id="p5">
</div>
<div class="tools">
<div>
<input class="penselect" type="radio" value="pencil" name="penselect" id="penselect-pencil"
autocomplete="off" checked>
<label for="penselect-pencil">연필</label>
</div>
<div>
<input class="penselect" type="radio" value="erasor" name="penselect" id="penselect-erasor"
autocomplete="off">
<label for="penselect-erasor">지우개</label>
</div>
</div>
</div>
<button type="button" class="clear">다시그리기!</button>
</li>
<li>
<h3>6. 비밀 번호</h3>
<input id="pass" name="pass" type="text" maxlength="2" pattern="^\d{2}$" title="암호는 숫자x2개로 해주세요."
required />
<small>두 자리 숫자를 입력해주세요. 업로드한 소리를 삭제할 때 쓰입니다.</small>
</li>
<li>
<input id="submit" type="submit" value="보내기!" />
<progress id="progress" max="100" value="70" style="display:none"></progress>
</li>
</ul>
<p>
↓ 
</p>
</form>
</section>
<section id="list">
<section id="list_init_kr" style="display:none">
<h1>소리 목록</h1>
<p>제출해주신 소리는 아래 목록에 시간순으로 쌓입니다. 각각의 모양을 클릭하면, 모양에 해당하는 소리를 듣고 소리에 대한 정보를 확인할 수 있습니다.</p>
</section>
<section id="list_init_en" style="display:none">
<h1>Sound &nbsp;List</h1>
<p>The sounds you submitted will be stacked in chronological order in the list below. By clicking on each shape,
you can hear the sound corresponding to the shape and check the information about the sound.</p>
</section>
{{#each list}}
<div class="items" foldername="{{this.foldername}}">
<details>
<summary><a class="anchor" id="{{this.anchor}}"></a><img class="drawing" src="/uploads/{{this.foldername}}/pixels.png" /></summary>
<audio class="sound" preload="none" controls>
<source src="/uploads/{{this.foldername}}/audio.mp3" type="audio/mpeg">
</audio>
<ul class="soundinfo"><hr>
<li>유형 | <div class="group">{{this.group}}</div></li><hr>
<li>제목 | <div class="title">{{this.title}}</div></li><hr>
<li>묘사 | <div class="comment">{{this.comment}}</div></li><hr>
</ul>
<button class="preview" type="button" onclick="javascript:window.open('/preview/{{this.foldername}}', '_blank');">미리보기</button>
<button class="delete" type="button" onclick="del(this)">삭제</button>
</details>
</div>
{{/each}}
</section>
</div><!-- div class="notice" -->
</div><!-- div class="entry" -->
<script>
// // force https
// var http_confirm = location.href.split(":")[0];
// if (http_confirm == "http") {
// window.location.replace("https://" + location.host);
// }
function list_reload() {
loadJSON('/list', list => {
//if lang == kr
let list_init = select('#list_init_kr').html();
//if lang == en
// let list_init = select('#list_init_en').html();
//
var t = "";
list.forEach(item => {
t = t + `
<div class="items" foldername="${item.foldername}">
<details>
<summary><a class="anchor" id="${item.anchor}"></a><img class="drawing" src="/uploads/${item.foldername}/pixels.png" /></summary>
<audio class="sound" preload="none" controls>
<source src="/uploads/${item.foldername}/audio.mp3" type="audio/mpeg">
</audio>
<ul class="soundinfo"><hr>
<li>유형 | <div class="group">${item.group}</div></li><hr>
<li>제목 | <div class="title">${item.title}</div></li><hr>
<li>묘사 | <div class="comment">${item.comment}</div></li><hr>
</ul>
<button class="preview" type="button" onclick="javascript:window.open('/preview/${item.foldername}', '_blank');">미리보기</button>
<button class="delete" type="button" onclick="del(this)">삭제</button>
</details>
</div>
`;
});
select('#list').html(list_init + t);
});
};
var socket = io(location.host);
var n = 0;
var fr = 20;
var arr = [];
var looper;
var score;
var logo;
var silence;
var clap;
//promisify -> new Tone.Player
function AudioImport(url) {
return new Promise((resolve, reject) => {
var audio = new Tone.Player(url, () => resolve(audio));
});
}
//**ENTRY begin >>>
// -- https://stackoverflow.com/a/39577640
function dataURLtoBlob(dataURL) {
let array, binary, i, len;
binary = atob(dataURL.split(",")[1]);
array = [];
i = 0;
len = binary.length;
while (i < len) {
array.push(binary.charCodeAt(i));
i++;
}
return new Blob([new Uint8Array(array)], {
type: "image/png",
});
}
function submitForm(event) {
//TODO : first check if there is a drawing or not. (pixels)
//
var submit = document.getElementById("submit");
submit.setAttribute('disabled', 'disabled');
select("html").style('cursor', 'progress');
//
var form = document.getElementById("form");
var fd = new FormData(form);
//
var unit = 45; //mask&override 'unit' -> for crispy png.
var img2 = createGraphics(cols * unit + 2, rows * unit + 2);
img2.clear();
img2.strokeWeight(0);
img2.fill(255);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
if (p[c * cols + r] == 1) {
img2.rect(c * unit + 1, r * unit + 1, unit, unit);
}
}
}
var dataurl = img2.elt.toDataURL();
var pixels = dataURLtoBlob(dataurl);
fd.append("pixels", pixels, "pixels.png");
for (var pair of fd.entries()) {
console.log(pair[0] + ', ' + pair[1]);
}
//
var request = new XMLHttpRequest();
//
var progress = select("#progress");
progress.style('display', 'unset');
//
request.open("POST", "/");
request.upload.onprogress = (pe) => {
if (pe.lengthComputable) {
progress.elt.max = pe.total;
progress.elt.value = pe.loaded;
}
}
request.onload = function () {
console.log('received http response');
console.log('response status: ', request.status);
console.log('response headers: ', request.getAllResponseHeaders());
console.log('response body: ', request.response);
}
request.onloadend = () => {
// check server's final feedback here.
if (request.status == 200) {
alert('감사합니다. 아래에 소리목록에서 업로드된 음원을 확인해보세요.');
} else {
alert('앗! 소리 파일 전송에 문제가 생겼어요\n wonjung24@gmail.com에 소리 파일을 한번 더 보내주시면 추가하겠습니다.\n 관리자 정보: 서버 응답 [' + request.status + ']');
}
// re-enable next upload.
submit.removeAttribute('disabled');
select("html").style('cursor', 'auto');
form.reset();
for (let a = 0; a < p.length; a++) p[a] = 0; //clear pixel-drawing, too.
// reload list.
list_reload();
}
request.send(fd);
//
event.preventDefault();
}
const form = document.getElementById('form');
form.addEventListener('submit', submitForm);
//// ---- for 'list' rendering ----
function del(that) {
console.log();
let text;
let pass = prompt("패스워드를 맞춰보세요!", "숫자2개");
if (/^\d{2}$/.test(pass)) {
var target = that.parentElement.parentElement.getAttribute('foldername');
const trydelete = async () => {
const response = await fetch('/delete/' + target + '/' + pass);
if (response.ok) {
const json = await response.json();
return Promise.resolve(json);
} else {
return Promise.reject('no response.');
}
}
trydelete().then((resp) => {
if (resp.result) {
alert("지우기 성공!");
// location.reload();
list_reload();
} else {
alert("지우기 실패-");
}
}).catch(console.log);
} else {
alert("암호는 숫자2개...");
}
}
var p = [];
var cols = 17;
var rows = 17;
var unit = 15; //px
var img;
var penselect = "pencil";
var scrollable = true;
// <<< end of **ENTRY
async function setup() {
//**PARADE
noCanvas();
if (windowWidth > 1500 && windowWidth > windowHeight) {
fr = 120;
} else {
fr = 20;
}
frameRate(fr);
//
//p5 'draw()' doesn't work if user is not looking at the tab.
//noLoop(); // <-- BUT, we want 1 for ENTRY graphics!
// --> use custom looper.
//**ENTRY
// list_reload();
// for korean page...
var list_intro = select("#list_init_kr");
list_intro.style('display', 'unset');
var cnv = createCanvas(cols * unit + 2, rows * unit + 2);
cnv.parent("p5");
img = createGraphics(cols * unit + 2, rows * unit + 2);
document.querySelectorAll('.penselect').forEach(item => item.onclick = () => {
penselect = document.querySelector('input[name="penselect"]:checked').value;
});
//
for (var i = 0; i < cols * rows; i++) p.push(0);
document.querySelectorAll('.clear').forEach(item => item.onclick = () => {
for (let a = 0; a < p.length; a++) p[a] = 0;
});
//go to the anchor
console.log(window.location.hash.substring(1));
let elmnt = document.getElementById(window.location.hash.substring(1));
if (elmnt) {
elmnt.scrollIntoView(true);
}
}
//**ENTRY begin >>>
function draw() {
//clear
clear();
//draw the grid
stroke(255);
strokeWeight(0.2);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
noFill();
rect(c * unit + 1, r * unit + 1, unit, unit);
}
}
//pointer
fill(0, 255, 0);
strokeWeight(0);
//mouse way
circle(mouseX, mouseY, 10);
if (mouseIsPressed && mouseButton === LEFT) {
//find slot under the pointer
var mouseC = int(mouseX / unit);
var mouseR = int(mouseY / unit);
if (mouseC >= 0 && mouseC < cols && mouseR >= 0 && mouseR < rows) {
if (penselect == "pencil") p[mouseC * cols + mouseR] = 1;
else if (penselect == "erasor") p[mouseC * cols + mouseR] = 0;
}
}
//touch way
if (touches.length > 0) {
circle(touches[0].x, touches[0].y, 10);
//find slot under the pointer
var mouseC = int(touches[0].x / unit);
var mouseR = int(touches[0].y / unit);
if (mouseC >= 0 && mouseC < cols && mouseR >= 0 && mouseR < rows) {
// if (scrollable == true) {
// scrollable = false;
// // firefox browser @ my android phone -> url bar auto-hiding kills drawing experience.
// // --> https://stackoverflow.com/a/63221105
// document.body.style.marginTop = `-${window.pageYOffset}px`;
// document.body.style.position = 'fixed';
// document.body.style.overflowY = 'scroll';
// }
if (penselect == "pencil") p[mouseC * cols + mouseR] = 1;
else if (penselect == "erasor") p[mouseC * cols + mouseR] = 0;
} else {
// if (scrollable == false) {
// scrollable = true;
// // firefox browser @ my android phone -> url bar auto-hiding kills drawing experience.
// // --> https://stackoverflow.com/a/63221105
// document.body.style.position = '';
// document.body.style.overflowY = '';
// if (document.body.style.marginTop) {
// const scrollTop = -parseInt(document.body.style.marginTop, 10);
// document.body.style.marginTop = '';
// window.scrollTo(window.pageXOffset, scrollTop);
// }
// }
}
}
//draw img
image(img, 0, 0);
img.clear();
img.strokeWeight(0);
img.fill(255);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
if (p[c * cols + r] == 1) {
img.rect(c * unit + 1, r * unit + 1, unit, unit);
}
}
}
}
// <<< end of **ENTRY
//
var myroom = -1;
var intro;
var ready;
//
socket.on("connect", async function () {
console.log("connected!");
setTimeout(gotConnection, 1000);
console.log("- injecting small delay to make sure p5js loaded. ~ 1s");
});
async function gotConnection() {
//
console.log("- after delay!");
//
silence = (await AudioImport("./audio/_silence.wav")).toDestination();
clap = (await AudioImport("./audio/clap01.mp3")).toDestination();
//TESTING... fixed to room 1.
// myroom = 1;
if (myroom == -1 && selectAll(".roomsel").length == 0) {
//initial connection -> ask the room number.
var roomsel = createDiv();
selectAll(".parade")[0].child(roomsel);
roomsel.class("roomsel");
///
var b = createButton("시작하기 Start!"); //, "1");
var d = createDiv("흐름을&nbsp; 향하여&nbsp; 걷는 &nbsp;&nbsp;Walking towards the Flow");
d.class("title");
let starter = function () {
silence.start();
clap.start();
// myroom = parseInt(this.value());
myroom = 1;
socket.emit("room", myroom, function (res) {
if (res) {
console.log("entered the room -> " + myroom);
// myroom-indicator
// var tag = createP(str(myroom));
// selectAll(".parade")[0].child(tag);
//
// setTimeout(function() {
// ready = createP("퍼레이드 시작합니다!!");
// ready.position(
// windowWidth / 2 - windowWidth / 10,
// windowHeight / 2
// );
// }, 1000);
} else {
console.log("rejected!");
}
});
setTimeout(() => {
selectAll(".roomsel").forEach(item => {
item.remove();
// 1 second popup '.intro' div
intro = createDiv(
"«흐름을 향하여 걷는» 소리 행렬이 곧 나타납니다. <br>오른쪽에서 왼쪽으로 지나가는 소리가 들리지 않는다면 볼륨을 확인해주시고, 스마트폰 환경에서는 진동 모드를 해제해주세요. <br>스크롤을 내리면 참여 방법을 확인하고 소리를 업로드할 수 있으며, 스크롤을 맨 위로 올리거나 오른쪽 위의 글씨를 클릭해 퍼레이드로 다시 돌아올 수 있습니다. <br><br>The sound parade «Walking towards the Flow», appears soon. <br>If you can't hear the sound passing from right to left, check the volume, and if you're using a smartphone, turn off the vibrate mode. <br>Scroll down to see how to join and upload sound, scroll to the top or click the text “parade” on the top right to return to the parade.<br><img class='arrow' src='arrow.svg'>"
);
intro.class("notice intro"); //-> fadeout & disapear by css animation style.
});
}, 1000);
};
b.mouseClicked(starter);
d.mouseClicked(starter);
roomsel.child(d);
roomsel.child(b);
} else {
//re-connection -> just connect to remembered room!
socket.emit("room", myroom, function (res) {
if (res) {
console.log("entered the room -> " + myroom);
} else {
console.log("rejected!");
}
});
}
};
//var fading_factor = 0.3; //30%
var fading_factor = 0.5; //50%
socket.on("post", async function (post) {
console.log(post);
var list = await new Promise((resolve, reject) => {
loadJSON("/entries", (json) => resolve(json));
})
console.log(list);
// var object = post.object;
var object = {
"id": 1,
"type": "abc",
"src": "/uploads/" + list[post] + "/pixels.png",
"audio": "/uploads/" + list[post] + "/audio.mp3",
"alt": "알트",
"size": {
"base": 40,
"random": 20
},
"y": {
"base": 40,
"random": 25
},
"showtime": 40000
};
console.log(object);
var img = createImg(object.src, object.alt, "", async function (im) {
//로딩이 끝나면, start!
var pv = new Tone.PanVol(0, -99).toDestination();
var snd = await AudioImport(object.audio); // NOTE: url with spaces didn't work here.
snd.connect(pv).start();
snd.loop = true;
//로딩이 끝나면, show!
im.show();
//그림의 크기와 초기 위치 ==> 가로 보기인 경우
var width = 0;
if (windowWidth > windowHeight) {
width = (windowHeight * (object.size.base * 0.4 + object.size.random * Math.random())) / 100; // 좀더 크게 + 40% (ratio)
im.size(width, AUTO);
im.position(
windowWidth * (1 + fading_factor),
(windowHeight * (object.y.base + object.y.random * Math.random()) * 0.5) / 100 // 좀더 위로 위로 - 50% (ratio)
);
//그림의 크기와 초기 위치 ==> 세로 보기인 경우
} else {
width = (windowHeight * (object.size.base + object.size.random * Math.random())) / 100; // json에서 정한 크기 그대로. (ratio)
im.size(width, AUTO);
im.position(
windowWidth * (1 + fading_factor),
(windowHeight * (object.y.base + object.y.random * Math.random())) / 100 // json에서 정한 위치 그대로. (ratio)
);
}
//추가 정보들
im.attribute("data-type", object.type);
im.attribute("data-showtime", object.showtime / 1000); //milli-sec. -> seconds.
//'아이콘' 들은 애니메이션을 시켜줘야 함...
if (object.type == "icon") {
//
im.class("rotate");
im.style("animation-duration", object.rotate + "s");
var orgs = im.style("transform-origin").split(" ");
var str = parseFloat(orgs[0]) + object.pivot.x + "px";
str = str + " " + parseFloat(orgs[1]) + object.pivot.y + "px";
im.style("transform-origin", str);
//
}
//로딩이 다 되면, rendering array에 추가.
var bundle = {
ref: object,
img: im,
sound: snd,
panvol: pv,
width: width
};
arr.push(bundle);
});
//첨에는 hide
img.hide();
});
//p5 'draw()' doesn't work if user is not looking at the tab.
// --> custom looper is ok.
var looper;
(looper = function (timeout) {
setTimeout(async function () {
//
for (var i = arr.length - 1; i >= 0; i -= 1) {
var bundle = arr[i];
var img = bundle.img;
var showtime = parseFloat(img.attribute("data-showtime"));
var type = img.attribute("data-type");
var x = img.position().x;
var y = img.position().y;
y = y + random(-1, 1);
x = x - windowWidth / (fr * showtime);
//
if (type == "icon") {
img.style("z-index", "-1");
}
3;
img.position(x, y);
var pan = (x / windowWidth) * 2 - 1;
//panning
var snd = bundle.sound;
var pv = bundle.panvol;
if (x >= -bundle.width && x < windowWidth) {
pan = ((x + bundle.width) / (windowWidth + bundle.width)) * 2 - 1;
pv.pan.value = pan;
pv.volume.value = 0;//(dB)
} else {
var range;
var knob;
if (x >= windowWidth) {
range = windowWidth * fading_factor
knob = x - windowWidth;
pv.pan.value = 1;
pv.volume.value = knob / range * -20;
} else if (x < -bundle.width) {
range = windowWidth * fading_factor
knob = (x + bundle.width) * -1;
pv.pan.value = -1;
pv.volume.value = knob / range * -20;
}
}
//scroll down -> volume down
// if (userscroll > windowHeight*2) {
// pv.volume.value = 0; //off
// } else if (userscroll >= windowHeight && userscroll < windowHeight*2) {
// pv.volume.value = (1 - (userscroll - windowHeight)/windowHeight) * pv.volume.value; //fade out
// }
//remove with sound fade-out
var exit_x = -bundle.width - windowWidth * fading_factor;
if (x < exit_x) {
img.remove();
snd.stop();
delete snd;
delete pv;
arr.splice(i, 1);
}
}
//
looper(1000 / fr);
}, timeout);
})(1000 / fr);
// function randomvoiceplay() {
// (looper = function(timeout) {
// setTimeout(function() {
// voice[int(random(19))].play();
// looper(random(8000, 12000));
// }, timeout);
// })(8000);
// }
</script>
</body>
</html>