sound-parade/src/pages/preview.html

377 lines
12 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>흐름을 향하여 걷는</title>
<link rel="stylesheet" href="/default.css" />
<style>
html,
body {
overflow: hidden;
}
</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 class="content">
</div>
<script>
// // force https
// var http_confirm = location.href.split(":")[0];
// if (http_confirm == "http") {
// window.location.replace("https://" + location.host);
// }
//--> https://gist.github.com/mudge/5830382#gistcomment-3398873 + modified trigger to accentp an arg.
function EventEmitter() {
const eventRegister = {};
const on = (name, fn) => {
if (!eventRegister[name]) eventRegister[name] = [];
eventRegister[name].push(fn);
}
const trigger = (name, arg = undefined) => {
if (!eventRegister[name]) return false;
eventRegister[name].forEach((fn) => fn.call(this, arg));
}
const off = (name, fn) => {
if (eventRegister[name]) {
const index = eventRegister[name].indexOf(fn);
if (index >= 0) eventRegister[name].splice(index, 1);
}
}
return {
on, trigger, off
}
}
//get preview target foldername
var foldername = "";
var urlparts = window.location.pathname.replace(/\/\s*$/,'').split('/');
foldername = urlparts[2]; // the server's route will guarantee urlparts[2] existence.
console.log(foldername);
var playing = true;
//set-up a preview stage
async function orchestra() {
//collect info.
var entries = await new Promise(async (resolve, reject) => resolve((await fetch("/entries")).json()));
var fields = await new Promise(async (resolve, reject) => resolve((await fetch("/fields")).json()));
var group = fields[entries.findIndex((e) => e == foldername)].group;
var members = fields.filter(obj => obj.group == group);
var midx = members.findIndex((e) => e.foldername == foldername);
//
function mtrig(m) {
//
m = m % members.length;
//
var idx = entries.findIndex((e) => e == members[m].foldername);
if (idx >= 0) socket.trigger('post', idx);
}
//now, let's orchestrate!
var time = 5;
Tone.Transport.schedule((t) => mtrig(midx - 1), time);
time = time + 15 + Math.random()*(4);
console.log(time);
Tone.Transport.schedule((t) => mtrig(midx), time);
time = time + 16 + Math.random()*(4);
console.log(time);
Tone.Transport.schedule((t) => mtrig(midx + 1), time);
time = time + 5 + Math.random()*(4);
console.log(time);
Tone.Transport.schedule((t) => mtrig(midx + 2), time);
time = time + 5 + Math.random()*(4);
console.log(time);
Tone.Transport.schedule((t) => mtrig(midx), time);
time = time + 70;
console.log(time);
Tone.Transport.schedule((t) => {
playing = false;
});
//
Tone.Transport.start();
//
}
//
setTimeout(() => {
socket.trigger('connect');
}, 100);
//
//var socket = io(location.host);
var socket = new EventEmitter();
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));
});
}
async function setup() {
noCanvas();
if (windowWidth > 1500 && windowWidth > windowHeight) {
fr = 30;
} else {
fr = 20;
}
frameRate(fr);
//p5 'draw()' doesn't work if user is not looking at the tab.
noLoop();
// --> use custom looper.
}
//
var myroom = -1;
var intro;
var ready;
//
socket.on("connect", async function() {
console.log("connected!");
//
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();
roomsel.class("roomsel");
var b = createButton("미리보기 Preview!", 1);
var d = createDiv("흐름을&nbsp; 향하여&nbsp; 걷는 &nbsp;&nbsp;Walking towards the Flow");
d.class("title");
b.mouseClicked(function() {
silence.start();
//clap.start();
orchestra();
setTimeout(() => {
selectAll(".roomsel").forEach(item => {
item.remove();
// 1 second popup '.intro' div
intro = createDiv(
"미리보기<br>«흐름을 향하여 걷는»은 온라인 공간에 소리의 행렬을 만드는 사운드 퍼레이드입니다. 누구나 참여할 수 있는 이 퍼레이드는 여러분이 보내주시는 소리가 모여 만들어집니다.<br>소리가 들리지 않는다면, 볼륨을 확인해주시고, 스마트폰 환경에서는 진동해제해주세요. <br><br>Preview<br>«Walking Toward the Flow» is a sound parade that creates a procession of sounds in the online space. Anyone can participate. This parade is made by collecting the sounds you sent.<br>If there is no sound, check the volume and turn off the vibration in the smartphone environment."
);
intro.class("notice intro"); //-> fadeout & disapear by css animation style.
});
}, 1000);
});
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": "https://p.dianaband.info/public/sound-parade/" + list[post] + "/pixels.png",
"audio": "https://p.dianaband.info/public/sound-parade/" + list[post] + "/audio.mp3",
"alt": "알트",
"size": {
"base": 40,
"random": 20
},
"y": {
"base": 20,
"random": 10
},
"showtime": 20000
};
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 * 1.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.
Tone.Transport.scheduleRepeat((time) => {
//
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");
}
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;
}
}
//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);
if (arr.length == 0 && playing == false) {
setTimeout(() => {
createDiv("미리보기가 끝났습니다. <br>Preview is over.<br><br>").class("notice").style('text-align', 'center')
.child(createButton("퍼레이드 가기").attribute('onclick', 'location.href="/"').style('margin', '1em 1em'))
.child(createButton("닫기").attribute('onclick', 'window.close()').style('margin', '1em 1em'))
}, 3000);
}
}
}
//
}, 1.0/fr);
Tone.Transport.start()
// function randomvoiceplay() {
// (looper = function(timeout) {
// setTimeout(function() {
// voice[int(random(19))].play();
// looper(random(8000, 12000));
// }, timeout);
// })(8000);
// }
</script>
</body>
</html>