-
Notifications
You must be signed in to change notification settings - Fork 5
/
l0.ShakeIt.moon
634 lines (565 loc) · 21.1 KB
/
l0.ShakeIt.moon
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
export script_name = "Shake It"
export script_description = "Lets you add a shaking effect to fbf typesets with configurable constraints."
export script_version = "0.2.0"
export script_author = "line0"
export script_namespace = "l0.ShakeIt"
DependencyControl = require "l0.DependencyControl"
depCtrl = DependencyControl {
feed: "https://raw.githubusercontent.com/TypesettingTools/line0-Aegisub-Scripts/master/DependencyControl.json",
{
{"a-mo.LineCollection", version: "1.3.0", url: "https://github.com/TypesettingTools/Aegisub-Motion",
feed: "https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json"},
{"l0.ASSFoundation", version:"0.4.3", url: "https://github.com/TypesettingTools/ASSFoundation",
feed: "https://raw.githubusercontent.com/TypesettingTools/ASSFoundation/master/DependencyControl.json"},
{"l0.Functional", version: "0.5.0", url: "https://github.com/TypesettingTools/Functional",
feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"},
{"a-mo.ConfigHandler", version: "1.1.4", url: "https://github.com/TypesettingTools/Aegisub-Motion",
feed: "https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json"},
}
}
LineCollection, ASS, Functional, ConfigHandler = depCtrl\requireModules!
{:list, :math, :string, :table, :unicode, :util, :re } = Functional
logger = depCtrl\getLogger!
-- Enums used in dialog
signChangeModes1D = {
Any: "Allow Any"
Force: "Force"
Prevent: "Prevent"
}
signChangeModes2D = {
Any: "Any number"
Either: "At least one"
One: "Exactly one"
}
tagShakeTargets = {
LineBegin: "Beginning of every line"
ExistingTags: "Every existing override tag"
TagSections: "Every tag section"
}
dialogs = {
shakePosition: {
{
class: "label", label: "Shaking Offset Limits (relative to original position): ",
x: 0, y: 0, width: 10, height: 1,
},
offXMin: {
class: "floatedit",
value: 0, min: 0, step:1, config: true
x: 0, y: 1, width: 3, height: 1
},
{
class: "label", label: "< x <",
x: 3, y: 1, width: 3, height: 1
},
offXMax: {
class: "floatedit",
value: 10, min: 0, step: 1, config: true
x: 6, y: 1, width: 4, height: 1,
},
offYMin: {
class: "floatedit",
value: 0, min: 0, step: 1, config: true
x: 0, y: 2, width: 3, height: 1
},
{
class: "label", label: "< y <",
x: 3, y: 2, width: 3, height: 1
},
offYMax: {
class: "floatedit",
x: 6, y: 2, width: 4, height: 1, config: true
value: 10, min: 0, step: 1
},
{
class: "label", label: "",
x: 0, y: 3, width: 10, height: 1
},
groupLines: {
class: "checkbox", label: "Group lines by:",
value: true, config: true
x: 0, y: 4, width: 1, height: 1
},
groupLinesField: {
class: "dropdown",
items: {"start_time", "end_time", "layer", "effect", "actor"}, value: 'start_time', config: true
x: 1, y: 4, width: 1, height: 1
},
{
class: "label", label: "Shake interval: every",
x: 0, y: 5, width: 1, height: 1
},
interval: {
class:"intedit",
value: 1, min: 1, config: true
x: 1, y: 5, width: 1, height: 1
},
{
class: "label", label: "line group(s)",
x: 2, y: 5, width: 1, height: 1
},
{
class: "label", label: "Angle between subsequent line group offsets:",
x: 0, y: 6, width: 10, height: 1
},
{
class: "label", label: "Min:",
x: 0, y: 7, width: 1, height: 1
},
angleMin: {
class: "floatedit",
value: 0, min: 0, max: 180, step: 1, config: true
x: 1, y: 7, width: 2, height: 1
},
{
class: "label", label: "� Max:",
x: 3, y: 7, width: 3, height: 1
},
angleMax: {
class: "floatedit",
value: 180, min: 0, max: 180, step: 1, config: true
x: 6, y: 7, width: 2, height: 1,
},
{
class: "label", label: "�",
x: 8, y: 7, width: 2, height: 1
},
{
class: "label", label: "",
x: 0, y: 8, width: 10, height: 1
},
{
class: "label", label: "Constraints:",
x: 0, y: 9, width: 10, height: 1
},
signChangeX: {
class: "dropdown",
items: table.values(signChangeModes1D), value: signChangeModes1D.Any, config: true
x: 0, y: 10, width: 2, height: 1
},
{
class: "label", label: "sign change for X offsets of subsequent line groups.",
x: 2, y: 10, width: 5, height: 1
},
signChangeY: {
class: "dropdown",
items: table.values(signChangeModes1D), value: signChangeModes1D.Any, config: true
x: 0, y: 11, width: 2, height: 1
},
{
class: "label", label: "sign change for Y offsets of subsequent line groups.",
x: 2, y: 11, width: 5, height: 1
},
signChangeCmb: {
class: "dropdown",
items: table.values(signChangeModes2D), value: signChangeModes2D.Any, config: true
x: 0, y: 12, width: 2, height: 1
},
{
class: "label", x: 2, y: 12, width: 5, height: 1
label: "of the X and Y offsets must change sign between subsequent line groups.",
},
{
class: "label", label: "",
x: 0, y: 13, width: 10, height: 1
},
{
class: "label", label: "Random Number Generation",
x: 0, y: 14, width: 10, height: 1
},
{
class: "label", label: "Seed:",
x: 0, y: 15, width: 1, height: 1
},
seed: {
class:"intedit",
value: os.time!,
x: 1, y: 15, width: 2, height: 1
},
repeatPattern: {
class: "checkbox", label: "Repeat pattern every",
value: false, config: true,
x: 0, y: 16, width: 1, height: 1
},
repeatInterval: {
class:"intedit",
value: 12, config: true,
x: 1, y: 16, width: 1, height: 1
},
{
class: "label", label: "line group(s)",
x: 2, y: 16, width: 1, height: 1
},
},
shakeScalarTag: {
{
class: "label", label: "Shaking Targets:",
x: 0, y: 0, width: 6, height: 1
},
{
class: "label", label: "Tag:",
x: 0, y: 1, width: 1, height: 1
},
tag: {
class: "dropdown",
items: table.pluck table.filter(ASS.tagMap, (tag) -> tag.type == ASS.Number and not tag.props.global), "overrideName",
value: "\\frz", config: true,
x: 1, y: 1, width: 1, height: 1
},
LineBegin: {
class: "checkbox", label: tagShakeTargets.LineBegin,
value: true, config: true,
x: 0, y: 2, width: 6, height: 1
},
ExistingTags: {
class: "checkbox", label: tagShakeTargets.ExistingTags,
value: false, config: true,
x: 0, y: 3, width: 6, height: 1
},
TagSections: {
class: "checkbox", label: tagShakeTargets.TagSections,
value: false, config: true
x: 0, y: 4, width: 6, height: 1
},
{
class: "label", label: "",
x: 0, y: 5, width: 6, height: 1
},
{
class: "label", label: "Shake offset limits (relative to original tag value): ",
x: 0, y: 6, width: 6, height: 1,
},
absoluteOffsetMin: {
class: "floatedit",
value: 0, min: 0, step:1, config: true,
x: 0, y: 7, width: 2, height: 1
},
{
class: "label", label: "< value <",
x: 2, y: 7, width: 1, height: 1
},
absoluteOffsetMax: {
class: "floatedit",
value: 10, min: 0, step: 1, config: true
x: 3, y: 7, width: 2, height: 1,
},
{
class: "label", label: "",
x: 0, y: 8, width: 6, height: 1
},
groupLines: {
class: "checkbox", label: "Group lines by:",
value: true, config: true,
x: 0, y: 9, width: 1, height: 1
},
groupLinesField: {
class: "dropdown",
items: {"start_time", "end_time", "layer", "effect", "actor"}, value: 'start_time', config: true,
x: 1, y: 9, width: 1, height: 1
},
{
class: "label", label: "Shake interval: every",
x: 0, y: 10, width: 1, height: 1
},
interval: {
class: "intedit",
value: 1, min: 1, config: true,
x: 1, y: 10, width: 1, height: 1
},
{
class: "label", label: "line group(s)",
x: 2, y: 10, width: 1, height: 1
},
{
class: "label", label: "Offset difference range between subsequent line groups:",
x: 0, y: 11, width: 6, height: 1
},
{
class: "label", label: "Min:",
x: 0, y: 12, width: 1, height: 1
},
groupOffsetMin: {
class: "floatedit",
value: 0, min: 0, step: 1, config: true,
x: 1, y: 12, width: 2, height: 1
},
{
class: "label", label: " Max:",
x: 3, y: 12, width: 1, height: 1
},
groupOffsetMax: {
class: "floatedit",
value: 10, min: 0, step: 1, config: true
x: 4, y: 12, width: 2, height: 1,
},
{
class: "label", label: "",
x: 0, y: 13, width: 6, height: 1
},
{
class: "label", label: "Shake offset constraints between subsequent line groups:",
x: 0, y: 14, width: 6, height: 1
},
signChange: {
class: "dropdown",
items: table.values(signChangeModes1D), value: signChangeModes1D.Any, config: true,
x: 0, y: 15, width: 2, height: 1
},
{
class: "label", label: "sign change for tag offsets of subsequent lines.",
x: 2, y: 15, width: 4, height: 1
},
{
class: "label", label: "",
x: 0, y: 16, width: 6, height: 1
},
{
class: "label", label: "",
x: 0, y: 17, width: 6, height: 1
},
{
class: "label", label: "Random Number Generation",
x: 0, y: 18, width: 10, height: 1
},
{
class: "label", label: "Seed:",
x: 0, y: 19, width: 1, height: 1
},
seed: {
class:"intedit",
value: os.time!,
x: 1, y: 19, width: 2, height: 1
},
repeatPattern: {
class: "checkbox", label: "Repeat pattern every",
value: false, config: true,
x: 0, y: 20, width: 1, height: 1
},
repeatInterval: {
class:"intedit",
value: 12, config: true,
x: 1, y: 20, width: 1, height: 1
},
{
class: "label", label: "line group(s)",
x: 2, y: 20, width: 1, height: 1
},
}
}
hasLineRotation = (line) ->
styleTags = line\getDefaultTags nil, false
return true unless styleTags.tags.angle\equal 0
line\modTags {"angle", "angle_x", "angle_y"}, (tag) -> true
groupLines = (lines, field, interval = 1) ->
-- collect selected lines and group if desired
groups = if field
table.values list.groupBy(lines.lines, field), (grpA, grpB) -> grpA[1][field] < grpB[1][field]
else [{line} for line in *lines]
-- group fbf lines to get longer shake interval
if interval > 1
groups = [list.join unpack group for group in *list.chunk groups, interval]
return groups
applyPositionShake = (lines, groups, offsets) ->
aegisub.progress.task "Shaking..."
groupCount = #groups
for i, group in ipairs groups
aegisub.progress.set 20 + 80 * (i-1) / groupCount
aegisub.cancel! if aegisub.progress.is_cancelled!
for line in *group
data = ASS\parse line
pos, align, org = data\getPosition!
modifiedTags = {pos}
if pos.class == ASS.Tag.Move
pos\add offsets[i][1], offsets[i][2], offsets[i][1], offsets[i][2]
else
pos\add offsets[i][1], offsets[i][2]
if hasLineRotation data
modifiedTags[2] = org
org\add offsets[i][1], offsets[i][2]
data\replaceTags modifiedTags
data\commit!
lines\replaceLines!
collectTags = (lines, groups, tagName, targets) ->
maxTagCountPerLine = 0
groupCount = #groups
tagsByGroupAndLine = for i, group in ipairs groups
aegisub.progress.set 10 + 50 * (i-1) / groupCount
aegisub.cancel! if aegisub.progress.is_cancelled!
for line in *group
tags = {}
ass = line.ASS or ASS\parse line
if targets.LineBegin
-- get the tag section right at line begin, create one if it doesn't exist
section = if #ass.sections > 0 and ass.sections[1].instanceOf[ASS.Section.Tag]
ass.sections[1]
else ass\insertSections(ASS.Section.Tag!, 1)[1]
-- get the last matching override tag in that section, create one from style default if it doesn't exist
tags[#tags+1] = section\getTags(tagName, -1, -1, true)[1] or section\insertDefaultTags tagName
if targets.ExistingTags
list.joinInto tags, ass\getTags tagName
if targets.TagSections
ass\callback (section,_,i) ->
tags[#tags+1] = section\getTags(tagName, -1, -1, true)[1] or section\insertTags(
section\getEffectiveTags(true).tags[tagName]),
ASS.Section.Tag
-- deduplicate tags we matched multiple times
tags = table.keys list.makeSet tags
-- sort tags by order of appearance in the line
table.sort tags, (a, b) ->
aSectionPosition = list.indexOf a.parent.parent.sections, a.parent
bSectionPosition = list.indexOf b.parent.parent.sections, b.parent
if aSectionPosition == bSectionPosition
return list.indexOf(a.parent.tags, a) < list.indexOf b.parent.tags, b
return aSectionPosition < bSectionPosition
maxTagCountPerLine = math.max maxTagCountPerLine, #tags
tags
return tagsByGroupAndLine, maxTagCountPerLine
getSingleSign = (mode, offPrev) ->
ref = switch mode
when signChangeModes1D.Prevent then offPrev
when signChangeModes1D.Force then -offPrev
else math.random! - 0.5
return math.sign ref, true
makePositionOffsetGenerator = (res) ->
shakeRadius = math.vector2.distance 0, 0, res.offXMax, res.offYMax
offXPrev, offYPrev, offX, offY = 0, 0
-- allow user to replay a previous shake
math.randomseed res.seed
return (constrainAngle = true, rollLimit = 5000) ->
for i = 1, rollLimit
-- check if X sign change is subject to combined X/Y constraints
xSign = if res.signChangeCmb == signChangeModes2D.One and res.signChangeY == signChangeModes1D.Force
math.sign offXPrev, true
elseif res.signChangeCmb == signChangeModes2D.Either and res.signChangeY == signChangeModes1D.Prevent
math.sign -offXPrev, true
-- otherwise use X-only constraints
else getSingleSign res.signChangeX, offXPrev
-- generate a new horizontal offset with the desired sign
offX = xSign * math.randomFloat res.offXMin, res.offXMax
xSignChanged = offX * offXPrev < 0
-- check if Y sign change is subject to combined X/Y constraints
ySign = if res.signChangeCmb == signChangeModes2D.Either and not xSignChanged
math.sign -offYPrev, true
elseif res.signChangeCmb == signChangeModes2D.One and xSignChanged
math.sign offYPrev, true
-- otherwise use Y-only constraints
else getSingleSign res.signChangeY, offYPrev
-- generate a new vertical offset with the desired sign
offY = ySign * math.randomFloat res.offYMin, res.offYMax
-- scale the current and previous offset vectors to the shake radius
offXNorm, offYNorm = math.vector2.normalize offX, offY, shakeRadius
offXPrevNorm, offYPrevNorm = math.vector2.normalize offXPrev, offYPrev, shakeRadius
-- get the angle difference on the circle around the origin
distance = math.vector2.distance offXPrevNorm, offYPrevNorm, offXNorm, offYNorm
angle = math.degrees math.acos (2*shakeRadius^2 - distance^2) / (2 * shakeRadius^2)
-- and check if is within the user-specified constraints
if not constrainAngle or angle >= res.angleMin and angle <= res.angleMax
offXPrev, offYPrev = offX, offY
return {offX, offY}
-- give up after so many rolls, because we're to lazy to actually do our maths
-- and factor the constraints in when pulling our random numbers
logger\error "Couldn't find offset that satifies chosen angle constraints (Min: #{res.angleMin}�, Max: #{res.angleMax}� for group #{i}. Aborting."
makeSimpleOffset = (prev, min, max, signChangeMode = signChangeModes1D.Any, minDiff = 0, maxDiff = math.huge, rollLimit = 5000) ->
for i = 1, rollLimit
sign = getSingleSign signChangeMode, prev
off = sign * math.randomFloat min, max
diffToPrev = math.abs off-prev
if diffToPrev <= maxDiff and diffToPrev >= minDiff
return off
logger\error "Couldn't find offset that satifies constraints Min=#{minDiff} <= #{prev} <= Max=#{maxDiff}."
makeMultiOffsetGenerator = (res, count) ->
-- this makes all initial offsets start with the same sign if sign change is enforced
-- TODO: maybe offer an option to start with a random sign for every value
offPrev = [0 for _ = 1, count]
-- allow user to replay a previous shake
math.randomseed res.seed
return (applyConstraints = true, rollLimit) ->
minPrevDiff, maxPrevDiff = if applyConstraints
res.groupOffsetMin, res.groupOffsetMax
else 0, math.huge
offPrev = for i = 1, count
makeSimpleOffset offPrev[i], res.absoluteOffsetMin, res.absoluteOffsetMax, res.signChange, minPrevDiff, maxPrevDiff, rollLimit
return offPrev
calculateOffsets = (seriesCount, generator, seed, repeatInterval = math.huge, startProgress = 0, endProgress = 100) ->
offsets = {}
for i = 0, seriesCount - 1
aegisub.progress.set startProgress + (endProgress-startProgress) * i / seriesCount
aegsiub.cancel! if aegisub.progress.is_cancelled!
offsets[1 + i] = if i < repeatInterval
generator i != 0
else offsets[1 + i%repeatInterval]
return offsets
showDialog = (macro) ->
options = ConfigHandler dialogs, depCtrl.configFile, false, script_version, depCtrl.configDir
options\read!
options\updateInterface macro
btn, res = aegisub.dialog.display dialogs[macro]
if btn
options\updateConfiguration res, macro
options\write!
return res
shakePosition = (sub, sel) ->
res = showDialog "shakePosition"
return aegisub.cancel! unless res
-- fix up some user errors
if res.offXMax < res.offXMin
res.offXMin, res.offXMax = res.offXMax, res.offXMin
if res.offYMax < res.offYMin
res.offYMin, res.offYMax = res.offYMax, res.offYMin
if res.angleMax < res.angleMin
res.angleMin, res.angleMax = res.angleMax, res.angleMin
-- check for conflicting constraints
err = {"You have provided conflicting constraints: "}
if res.signChangeX == signChangeModes1D.Force and res.signChangeY == signChangeModes1D.Force
if res.angleMax < 90
err[#err+1] = "Forced sign inversion for X and Y offsets require a maxium angle of at least 90�."
if res.signChangeCmb == signChangeModes2D.One
err[#err+1] = "Can't limit signs to only one of the X and Y offsets because sign changes are separately enforced for both."
elseif res.signChangeX == signChangeModes1D.Prevent and res.signChangeY == signChangeModes1D.Prevent
if res.angleMin > 90
err[#err+1] = "Can't prevent sign inversion for X and Y offsets when the minimum angle is larger than 90�."
if res.signChangeCmb == signChangeModes2D.Either
err[#err+1] = "Can't change signs of either X or Y offsets because they are prevented for both."
logger\assert #err == 1, table.concat err, "\n"
lines = LineCollection sub, sel
aegisub.progress.task "Grouping lines..."
groups = groupLines lines, res.groupLines and res.groupLinesField or nil, res.interval
aegisub.progress.set 10
aegisub.cancel! if aegisub.progress.is_cancelled!
aegisub.progress.task "Rolling dice..."
-- generate offsets for every line group, but don't apply them immediately in case the generator fails
offsets = calculateOffsets #groups, makePositionOffsetGenerator(res),
res.seed, res.repeatPattern and res.repeatInterval or math.huge, 10, 20
-- apply the position offsets to all line groups
aegisub.progress.task "Applying shake..."
applyPositionShake lines, groups, offsets
shakeScalarTag = (sub, sel) ->
res = showDialog "shakeScalarTag"
return aegisub.cancel! unless res
-- fix up some user errors
if res.absoluteOffsetMax < res.absoluteOffsetMin
res.absoluteOffsetMin, res.absoluteOffsetMax = res.absoluteOffsetMax, res.absoluteOffsetMin
if res.groupOffsetMax < res.groupOffsetMin
res.groupOffsetMin, res.groupOffsetMax = res.groupOffsetMax, res.groupOffsetMin
lines = LineCollection sub, sel
aegisub.progress.task "Grouping lines..."
groups = groupLines lines, res.groupLines and res.groupLinesField or nil, res.interval
groupCount = #groups
aegisub.progress.set 10
aegisub.progress.task "Collecting tags..."
tagsByGroupAndLine, offsetCount = collectTags lines, groups, ASS.tagNames[res.tag][1], res
aegisub.progress.task "Rolling dice..."
offsets = calculateOffsets #groups, makeMultiOffsetGenerator(res, offsetCount),
res.seed, res.repeatPattern and res.repeatInterval or math.huge, 60, 70
aegisub.progress.task "Applying shake..."
for g, group in ipairs groups
aegisub.progress.set 70 + 30 * (g-1) / groupCount
aegisub.cancel! if aegisub.progress.is_cancelled!
for tagsByLine in *tagsByGroupAndLine[g]
-- TODO: support tags w/ > 1 parameter
tag\add offsets[g][t] for t, tag in ipairs tagsByLine
line.ASS\commit! for line in *group
lines\replaceLines!
depCtrl\registerMacros {
{"Shake Position", "Applies randomized offsets to line positioning.", shakePosition},
{"Shake Scalar Tag", "Applies randomized offsets to a specified scalar override tag.", shakeScalarTag},
}