# aardio 范例: av - 彩纸、星光与音效：给界面操作一点反馈



```aardio
//av - 彩纸、星光与音效：给界面操作一点反馈
//av.video 使用指南: https://www.aardio.com/zh-cn/docs/library/av/video/
//av.audio 使用指南: https://www.aardio.com/zh-cn/docs/library/av/audio/
//缓动与弹簧: https://www.aardio.com/zh-cn/docs/examples/plus/av/motion.html

import fonts.fontAwesome;
import win.ui;
/*DSG{{*/
var winform = win.form(text="av - 彩纸、星光与音效";right=599;bottom=319;bgcolor=0xFFFFFF)
winform.add(
btnDone={cls="plus";text="完成任务";left=330;top=194;right=560;bottom=240;bgcolor=0x59C734;border={radius=8};color=0xFFFFFF;font=LOGFONT(h=-16);notify=1;z=4};
btnPlay={cls="plus";text="播放";left=474;top=38;right=560;bottom=68;bgcolor=0xF0F0F0;border={radius=6};color=0x333333;font=LOGFONT(h=-14);notify=1;z=2};
cmbSound={cls="combobox";left=330;top=40;right=466;bottom=66;edge=1;font=LOGFONT(h=-14);items={};mode="dropdownlist";z=1};
code1={cls="bkplus";text='av.audio.sfx(名称) \u2192 sys.soundPool';left=40;top=56;right=320;bottom=76;align="left";color=0x8A8A8A;font=LOGFONT(h=-13;name='Consolas');z=6};
code2={cls="bkplus";text='create("sparkle") + sfx("sparkle")';left=40;top=140;right=320;bottom=160;align="left";color=0x8A8A8A;font=LOGFONT(h=-13;name='Consolas');z=8};
code3={cls="bkplus";text='create("confetti") + sfx("success")';left=40;top=220;right=320;bottom=240;align="left";color=0x8A8A8A;font=LOGFONT(h=-13;name='Consolas');z=10};
line1={cls="bkplus";left=40;top=100;right=560;bottom=101;bgcolor=0xEEEEEE;z=11};
line2={cls="bkplus";left=40;top=180;right=560;bottom=181;bgcolor=0xEEEEEE;z=12};
note={cls="bkplus";text="粒子按时间直接求值，音效由代码现场合成，不需要任何素材文件。";left=40;top=270;right=560;bottom=294;align="left";color=0x9A9A9A;font=LOGFONT(h=-13);z=13};
star={cls="plus";left=330;top=112;right=378;bottom=160;iconStyle={font=LOGFONT(h=-30;name='FontAwesome')};iconText='\uF006';notify=1;z=3};
starTip={cls="bkplus";text="点击收藏，再点取消";left=396;top=125;right=560;bottom=147;align="left";color=0x8A8A8A;font=LOGFONT(h=-14);z=14};
title1={cls="bkplus";text="试听音效";left=40;top=30;right=300;bottom=54;align="left";color=0x333333;font=LOGFONT(h=-16;weight=700);z=5};
title2={cls="bkplus";text="收藏";left=40;top=114;right=300;bottom=138;align="left";color=0x333333;font=LOGFONT(h=-16;weight=700);z=7};
title3={cls="bkplus";text="完成任务";left=40;top=194;right=300;bottom=218;align="left";color=0x333333;font=LOGFONT(h=-16;weight=700);z=9}
)
/*}}*/

import sys.soundPool;
import time.performance;
import av.audio.sfx;
import av.video.particles;

var particles = av.video.particles;
var $ = av.video;

//---- 音效：启动时生成（每个约 30~100 毫秒）放进音效池，之后一句 play 播放，几个声音可以同时响 ----
var sounds = sys.soundPool(); //没有声卡时为静音模式，play 什么也不做
sounds.add("done",av.audio.sfx("success").trim(-60)); //trim 去掉混响尾部的静音；音量可用 opt.gain（dB）调整
sounds.add("star",av.audio.sfx("sparkle",{dur=0.8}).trim(-60));
sounds.add("unstar",av.audio.sfx("click").trim(-60));

//---- 粒子特效层：铺满客户区的透明画布，粒子可以飞过所有控件 ----
winform.add(fx={cls="canvas";left=0;top=0;marginRight=0;marginBottom=0;anchors={left=1;top=1;right=1;bottom=1}});
winform.fx.setRect( winform.fx.getRect().inflate(100,100) ); //动画可不受窗口限制，orphanWindow 的福利
winform.fx.orphanWindow(true); //转为分层透明窗口，盖在窗体上并跟随窗体移动、缩放
winform.fx.modifyStyleEx(,0x20/*_WS_EX_TRANSPARENT*/); //鼠标穿透到下面的控件

var bursts = {};

//在控件中心爆发。opt 是 particles.create 的参数：scale 按 1080p 视频设计，界面里要缩小并乘以 DPI
var burst = function(kind,ctrl,opt){
	var offset = winform.dpiScale(100); //winform.fx 放大后偏移
	var x,y,cx,cy = ctrl.getPos(); //相对窗口客户区，与特效层坐标一致
	opt.seed = math.random(1,1000000); //每次换个种子，样子不重复
	
	table.push(bursts,{
		ps = particles.create(kind,opt);
		x = x + cx / 2 + offset;
		y = y + cy / 2 +offset;
		start = time.performance.tick() //高精度毫秒，画布每帧都按真实时间求值
	});
	
	if(!winform.fx.animationId) winform.fx.startAnimation(10);
}

winform.fx.onDrawForeground = function(graphics,rc){
	var now = time.performance.tick();
	for(i=#bursts;1;-1){
		var b = bursts[i];
		var t = (now - b.start) / 1000; //爆发后的秒数，粒子位置按 t 直接求值
		if(t > b.ps.life) table.remove(bursts,i);
		else particles.draw(graphics,b.ps,t,b.x,b.y);
	}
}

winform.fx.onAnimation = function(){
	if(#bursts) return true; //粒子全部结束就停止动画
}

//---- 完成任务：彩纸 + 成功音效 ----
winform.btnDone.skin({
	background = {default=0xFF34C759;hover=0xFF4CD06D;active=0xFF2AA84A};
	color = {default=0xFFFFFFFF};
})

winform.btnDone.oncommand = function(){
	burst("confetti",winform.btnDone,{
		scale = winform.dpiScale(0.6);
		colors = [0xFFFF5F6D,0xFF3BA7FF,0xFFFFC233,0xFF34C759,0xFFA66BFF]; //白底上不要用白色
	});
	sounds.play("done");
}

//---- 收藏：星星弹出 + 星光 + 音效 ----
winform.star.skin({
	iconColor = {default=0xFFB8B8B8;hover=0xFFFFB300};
	checked = {iconText='\uF005';iconColor={default=0xFFFFB300}};
})

var starScale = 1;
winform.star.onAnimation = function(prevState,beginning,change,timestamp,duration){
	starScale = beginning + change * $.spring(timestamp / 1000,4,0.35);
	if(timestamp >= duration) starScale = 1;
	owner.scale = starScale;
	if(timestamp < duration) return true;
}

var starred = false;
winform.star.onStateChange = function(state){
	owner.scale = starScale; //skin 切换状态时会重置 scale，这里恢复动画中的值
	
	//鼠标单击、空格键都会切换 checked，在这里统一响应
	if(state.checked == starred) return;
	starred = !starred;
	
	if(starred){
		owner.startAnimation(10,0.4,0.6,700); //从 0.4 倍弹到 1 倍
		burst("sparkle",owner,{
			scale = winform.dpiScale(0.7);
			count = 16; //小图标周围星星少一些更清爽（默认 30）
			radius = winform.dpiScale(40); //星光散开的半径（像素）
			ringRadius = winform.dpiScale(30); //冲击环半径，比星光小一圈
			colors = [0xFFFFB300,0xFFFF7A00,0xFFFFB300,0xFFFF5F6D]; //浅金色在白底上看不清，掺一点橙红
		});
		
		sounds.play("star");
	}
	else {
		sounds.play("unstar");
	}
}

//---- 试听全部音效预设（首次试听时生成） ----
var preview = function(){
	var name = winform.cmbSound.selText;
	if(!sounds.has(name)) sounds.add(name,av.audio.sfx(name).trim(-60));
	sounds.play(name);
}

winform.cmbSound.items = av.audio.sfx.names;
winform.cmbSound.selText = "pop";
winform.cmbSound.onSelChange = preview;
winform.btnPlay.oncommand = preview;
winform.btnPlay.skin({background = {default=0xFFF0F0F0;hover=0xFFE6E6E6;active=0xFFDADADA}});

winform.show();
win.loopMessage();
```

