You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

262 lines
10 KiB

import math as _m, random as _r
_FLOAT=0; _GATHER=1; _LINES=2; _LFADE=3; _EXPAND=4; _ELLIPSE=5
_CHAR_W_LATIN=10.0; _CHAR_W_CJK=18.0; _SLOTS_PER_LBL=20
def _cjk(ch): return '\u4e00'<=ch<='\u9fff'
def _tier():
x=_r.random()
return 0 if x<0.3 else (1 if x<0.7 else 2)
def _spd(t,s1,s2,s3):
if t==0: return _r.uniform(s1*.8,s1*1.2)
if t==1: return _r.uniform(s2*.8,s2*1.2)
return _r.uniform(s3*.8,s3*1.2)
def _dir(): return -1 if _r.random()>0.1 else 1
def _cpos(i,W,H):
a=i/18*2*_m.pi-_m.pi/2
r=min(W,H)*.28
return W/2+r*_m.cos(a), H/2+r*_m.sin(a)
def _epos(i,W,H):
a=i/18*2*_m.pi-_m.pi/2
return W/2+W*.47*_m.cos(a), H/2+H*.43*_m.sin(a)
def _ease(t):
t=max(0.,min(1.,t))
return 2*t*t if t<.5 else -1+(4-2*t)*t
def _init_bg(W,H,s1,s2,s3):
out=[]
for i in range(80):
t=_tier()
out.append({'x':_r.uniform(0,W),'y':i/80*H,'speed':_spd(t,s1,s2,s3)*_dir()})
return out
def _init_cd(W,H,s1,s2,s3,cw):
out=[]
for i,w in enumerate(cw):
t=_tier()
out.append({'x':_r.uniform(0,W),'y':_r.uniform(0,H),'speed':_spd(t,s1,s2,s3)*_dir(),'idx':i,'word':w})
return out
def _build_rows(nr,rs,wpr,W,H,aw,s1,s2,s3):
fs=max(12,min(80,int(W*.85/max(1,wpr*12)/(10/18)*18)))
cw=fs*.6; out=[]
for i in range(nr):
t=_tier(); sp=_spd(t,s1,s2,s3)*_dir()
sel=_r.choices(aw,k=wpr) if aw else []
gaps=[_r.uniform(60,140) for _ in range(max(0,wpr-1))]
wws=[max(40,len(w.get('english',''))*cw) for w in sel]
tw=sum(wws)+sum(gaps)
sx=_r.uniform(-tw*.5,W-tw*.3)
wr=[]; x=sx
for j,w in enumerate(sel):
wr.append({'id':w['id'],'text':w['english'],'x':x,'width':wws[j]})
x+=wws[j]+(gaps[j] if j<len(gaps) else 0)
out.append({'words':wr,'y':120+i*rs,'speed':sp,'total_w':tw})
return out
def frameEnd(frame):
try: _run(frame)
except Exception as e: print('FE err:',e)
def _run(frame):
root=op('/project1/deep_sound')
W=root.par.Resw.val; H=root.par.Resh.val; dt=1
s1=root.par.Speedslow.val; s2=root.par.Speedmid.val; s3=root.par.Speedfast.val
wpr=int(root.par.Wordsperrow.val)
es=root.par.Expandspd.val; ds=root.par.Drawspd.val; ms=root.par.Marqueespd.val
gw=root.par.Gatherwait.val; lp=root.par.Linepause.val; fo=root.par.Fadeout.val
mcs=root.par.Marqueecharspacing.val; mcsz=root.par.Marqueesize.val
# Read state from stored dict
_DEF={'appState':_FLOAT,'textAlpha':1.,'gatherTimer':-1.,'pauseTimer':-1.,
'dissolving':0,'ellipseAlpha':1.,'currentSeg':0,'labelFadeProgress':0.,
'expandT':0.,'ellipseDrawT':0.,'marqueeAngle':0.,'marqueeAlpha':0.,
'lineAlpha':0.,'pid0':0,'pid1':0,'pid2':0,'pid3':0}
st=root.fetch('state_dict', None)
if st is None: st=dict(_DEF)
ast=int(round(st['appState']))
ta=float(st['textAlpha']); gt=float(st['gatherTimer']); pt=float(st['pauseTimer'])
dv=int(round(st['dissolving'])); ea=float(st['ellipseAlpha']); cs=int(round(st['currentSeg']))
lfp=float(st['labelFadeProgress']); et=float(st['expandT']); edt=float(st['ellipseDrawT'])
mang=float(st['marqueeAngle']); malp=float(st['marqueeAlpha'])
pid0=int(round(st['pid0'])); pid1=int(round(st['pid1']))
pid2=int(round(st['pid2'])); pid3=int(round(st['pid3']))
pids=[pid0,pid1,pid2,pid3]
bg=root.fetch('bg_dots',None); cd=root.fetch('circle_dots',None)
rows=root.fetch('rows',None); segs=root.fetch('line_segs',[])
las=root.fetch('label_alphas',[0.,0.,0.,0.])
wdat=op('words_table'); aw=[]; cw=[]
for r in range(1,wdat.numRows):
w={'id':int(wdat[r,'id'].val),'english':str(wdat[r,'english'].val),
'chinese':str(wdat[r,'chinese'].val),'circle':int(wdat[r,'circle'].val)}
aw.append(w)
if w['circle']==1: cw.append(w)
ts_div=[1,2,3][min(2,max(0,root.par.Textsize.menuIndex))]
fs=max(12,min(80,int(W*.85/max(1,wpr*12)/(10/18)*18)))//ts_div
nr=max(8,int((H-120)/max(1,fs*1.6))); rs2=(H-120)/max(1,nr-1)
pw=root.fetch('prev_wpr',-1); ps=root.fetch('prev_s1',-999.); pts=root.fetch('prev_ts',-1)
if bg is None or rows is None or cd is None or pw!=wpr or abs(ps-s1)>.001 or pts!=ts_div:
bg=_init_bg(W,H,s1,s2,s3)
cd=_init_cd(W,H,s1,s2,s3,cw)
rows=_build_rows(nr,rs2,wpr,W,H,aw,s1,s2,s3)
root.store('prev_wpr',wpr); root.store('prev_s1',s1); root.store('prev_ts',ts_div); root.store('font_size',fs)
fi_spd=root.par.Fadein.val; fo_spd=root.par.Fadeoutspd.val
ta=max(0.,ta-fo_spd) if ast!=_FLOAT else min(1.,ta+fi_spd)
for row in rows:
for wd in row['words']: wd['x']+=row['speed']*dt
if row['words']:
lft=min(wd['x'] for wd in row['words'])
rgt=max(wd['x']+wd['width'] for wd in row['words'])
sh=W+row['total_w']+160
if row['speed']<0 and rgt<-80:
for wd in row['words']: wd['x']+=sh
elif row['speed']>0 and lft>W+80:
for wd in row['words']: wd['x']-=sh
for d in bg:
d['x']+=d['speed']*dt
if d['speed']<0 and d['x']<-80: d['x']+=W+160
elif d['speed']>0 and d['x']>W+80: d['x']-=W+160
for i,d in enumerate(cd):
if ast==_FLOAT:
d['x']+=d['speed']*dt
if d['speed']<0 and d['x']<-80: d['x']+=W+160
elif d['speed']>0 and d['x']>W+80: d['x']-=W+160
elif ast in (_GATHER,_LINES,_LFADE):
tx,ty=_cpos(i,W,H)
d['x']+=(tx-d['x'])*.04*dt; d['y']+=(ty-d['y'])*.04*dt
elif ast==_EXPAND:
tx,ty=_cpos(i,W,H); ex,ey=_epos(i,W,H); t=_ease(et)
d['x']=tx+(ex-tx)*t; d['y']=ty+(ey-ty)*t
elif ast==_ELLIPSE:
ex,ey=_epos(i,W,H); d['x']=ex; d['y']=ey
if ast==_GATHER:
gt-=dt/60.
if gt<=0:
p=sorted(_r.sample(range(18),4)); pid0,pid1,pid2,pid3=p; pids=p
segs=[{'from_idx':p[k],'to_idx':p[k+1],'progress':0.,'done':False} for k in range(3)]
las=[0.,0.,0.,0.]; cs=0; ast=_LINES
elif ast==_LINES:
if cs<len(segs):
segs[cs]['progress']=min(1.,segs[cs]['progress']+.012*dt)
if segs[cs]['progress']>=1. and not segs[cs]['done']:
segs[cs]['done']=True; cs+=1
las[0]=min(1.,las[0]+.02*dt)
if len(segs)>0 and segs[0]['done']: las[1]=min(1.,las[1]+.02*dt)
if len(segs)>1 and segs[1]['done']: las[2]=min(1.,las[2]+.02*dt)
if len(segs)>2 and segs[2]['done']: las[3]=min(1.,las[3]+.02*dt)
if all(s['done'] for s in segs) and all(a>=1. for a in las) and pt<0:
pt=lp
if pt>=0:
pt-=dt/60.
if pt<=0: pt=-1; ast=_LFADE; lfp=0.
elif ast==_LFADE:
lfp=min(1.,lfp+.015*dt)
if lfp>=1.: ast=_EXPAND; et=0.
elif ast==_EXPAND:
et=min(1.,et+es*dt)
if et>=1.: ast=_ELLIPSE
elif ast==_ELLIPSE:
edt=min(1.,edt+ds*dt)
mang-=ms*dt
if edt>=1.: malp=min(1.,malp+.012*dt)
if dv:
ea=max(0.,ea-dt/max(.001,fo*60.))
if ea<=0.:
ast=_FLOAT; dv=0; ea=1.; edt=0.; mang=0.; malp=0.; ta=0.
et=0.; lfp=0.; gt=-1.; pt=-1.; pids=[0,0,0,0]; segs=[]; las=[0.,0.,0.,0.]; cs=0
pid0=pid1=pid2=pid3=0
bg=_init_bg(W,H,s1,s2,s3); cd=_init_cd(W,H,s1,s2,s3,cw)
prox=[]
if ast==_FLOAT:
for i in range(len(bg)):
for j in range(i+1,len(bg)):
dx=bg[i]['x']-bg[j]['x']; dy=bg[i]['y']-bg[j]['y']
dist=_m.sqrt(dx*dx+dy*dy)
if dist<90: prox.append((dist,i,j))
prox.sort(); prox=prox[:int(root.par.Maxlinks.val)]
# Line alpha for Level TOP (no Python alpha math in render)
line_alpha=1. if ast==_LINES else (1.-lfp if ast==_LFADE else 0.)
# Update line-label Text TOPs
for i in range(4):
did=pids[i] if i<len(pids) else 0
chi=''; eng=''
if 0<=did<len(cd):
w=cd[did].get('word',{}); chi=w.get('chinese',''); eng=w.get('english','')
ch_t=root.op(f'lbl_ch_{i}'); en_t=root.op(f'lbl_en_{i}')
if ch_t: ch_t.par.text=chi
if en_t: en_t.par.text=eng
# Update chr_sop fontsizex if Marqueesize changed
pmcsz=root.fetch('prev_mcsz',-1.)
if abs(pmcsz-mcsz)>0.1:
for idx in range(80):
s=root.op(f'marq_geo/chr_sop_{idx:02d}')
if s: s.par.fontsizex=mcsz
root.store('prev_mcsz',mcsz)
# Per-character marquee placement along ellipse
rx_m=W*0.47-34; ry_m=H*0.43-24
for qi in range(4):
ang=mang+qi*_m.pi/2
did_q=pids[qi] if qi<len(pids) else 0
lbl=''
if 0<=did_q<len(cd):
w=cd[did_q].get('word',{}); chi=w.get('chinese',''); eng=w.get('english','')
lbl=f'{chi} {eng}'.strip() if chi else eng
t=ang; base_slot=qi*_SLOTS_PER_LBL
cw_lat=mcsz*0.56; cw_cjk=mcsz
for ci in range(_SLOTS_PER_LBL):
idx=base_slot+ci
sop=root.op(f'marq_geo/chr_sop_{idx:02d}')
tf=root.op(f'marq_geo/chr_tf_{idx:02d}')
if ci<len(lbl):
ch=lbl[ci]
cw=cw_cjk if _cjk(ch) else cw_lat
x=rx_m*_m.cos(t); y=ry_m*_m.sin(t)
rot=_m.degrees(_m.atan2(ry_m*_m.cos(t),-rx_m*_m.sin(t)))
if sop: sop.par.text=ch
if tf: tf.par.tx=round(x,3); tf.par.ty=round(y,3); tf.par.rz=round(rot,3)
ds_dt=_m.sqrt((rx_m*_m.sin(t))**2+(ry_m*_m.cos(t))**2)
t+=(cw+2)*mcs/max(1.,ds_dt)
else:
if sop: sop.par.text=''
if tf: tf.par.tx=0.; tf.par.ty=0.; tf.par.rz=0.
# Update custom params for Level TOP opacity drivers
root.par.Textalpha=round(ta,4)
root.par.Marqalpha=round(malp*ea,4)
root.par.Linealpha=round(line_alpha,4)
# Write all state back to dict (picked up by state_chop Script CHOP each frame)
root.store('state_dict',{'appState':ast,'textAlpha':ta,'gatherTimer':gt,'pauseTimer':pt,
'dissolving':dv,'ellipseAlpha':ea,'currentSeg':cs,'labelFadeProgress':lfp,
'expandT':et,'ellipseDrawT':edt,'marqueeAngle':mang,'marqueeAlpha':malp,
'lineAlpha':line_alpha,'marqAlpha':malp*ea,
'pid0':pid0,'pid1':pid1,'pid2':pid2,'pid3':pid3})
root.store('bg_dots',bg); root.store('circle_dots',cd); root.store('rows',rows)
root.store('line_segs',segs); root.store('label_alphas',las)
root.store('prox_pairs',prox)
def onStart(): pass
def onCreate(): pass
def onExit(): pass