Collection of mostly command line tools / PHP scripts. Somewhat out of date.

⌈⌋ branch:  scripts + snippets


Check-in [483352ece2]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:changed spline pacing mode: one less iteration to avert last section stuck at 1.0, reset .frame.args.delay just in case (repeat animations, e.g. fg/bg frames)
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 483352ece2f9b57372acfa7dc4b0c12a2db6e616
User & Date: mario 2022-10-31 09:39:46
Context
2023-05-23
10:10
prepare potential all-merge mode check-in: 58335bd8d7 user: mario tags: trunk
2022-10-31
09:39
changed spline pacing mode: one less iteration to avert last section stuck at 1.0, reset .frame.args.delay just in case (repeat animations, e.g. fg/bg frames) check-in: 483352ece2 user: mario tags: trunk
09:37
more pacing tests check-in: 64ef7120d0 user: mario tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to inkscape/export_gif.py.

38
39
40
41
42
43
44



45
46
47
48
49
50
51
#    { category: Help, label: " 🞂 [merge] enjoins partial layers; and [exclude] skips them" }
#    { category: Help, label: " 🞂 [animate=10] generates ❮animate*❯ subframes, flag: [pace] timing" }
#    { category: Help, label: " 🞂 [--delay=2.5] resets ImageMagick option (delay also for Pillow)",
#    { category: Help, label: $help, appearance: url }
# inx-export: .gif, image/gif, GIF slideshow (*.gif), Graphics Interchange Format 98a
# architecture: all
# pack: export_gif.py, *.inx, pmd2inks, animate_yo.py, svgelements.py, export_gif.svg, LICENSE=/usr/share/doc/inkscape-export-gif/copyright



# format: off
# author: mario#include-once:org
# url: https://inkscape.org/~culturaljuice/★export_gif
# help: https://fossil.include-once.org/scripts/wiki/inkscape
# orig: https://github.com/jespino/inkscape-export-layers
#
# Generates a GIF slideshow from image layers ☰  (Shift+Ctrl+L). The menu







>
>
>







38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#    { category: Help, label: " 🞂 [merge] enjoins partial layers; and [exclude] skips them" }
#    { category: Help, label: " 🞂 [animate=10] generates ❮animate*❯ subframes, flag: [pace] timing" }
#    { category: Help, label: " 🞂 [--delay=2.5] resets ImageMagick option (delay also for Pillow)",
#    { category: Help, label: $help, appearance: url }
# inx-export: .gif, image/gif, GIF slideshow (*.gif), Graphics Interchange Format 98a
# architecture: all
# pack: export_gif.py, *.inx, pmd2inks, animate_yo.py, svgelements.py, export_gif.svg, LICENSE=/usr/share/doc/inkscape-export-gif/copyright
# depends: python:inkex, bin:inkscape >= 1.1
# suggests: python:svgelements >= 1.8
# permissive: 0.5
# format: off
# author: mario#include-once:org
# url: https://inkscape.org/~culturaljuice/★export_gif
# help: https://fossil.include-once.org/scripts/wiki/inkscape
# orig: https://github.com/jespino/inkscape-export-layers
#
# Generates a GIF slideshow from image layers ☰  (Shift+Ctrl+L). The menu
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
        self.gif = parent
        self.convert_png = parent.convert_png
        self.svg = svg  # ← already a deepcopy
        self.layer = layer
        self.frames = layer.animate
        self.delay = float(layer.args.get("delay", self.gif.options.delay))
        self.layer.args.update({
            "delay": self.delay / self.frames   # frame time split between animation steps / TODO: might need reset
        })

    def export(self):
        """ applies transforms in each subframe, yields filename+args list """
        transforms = []
        for anim in self.svg.xpath("//svg:animate"):
            transforms.append([self.animate_style(anim, **anim.attrib), self.pace(anim)])







|







471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
        self.gif = parent
        self.convert_png = parent.convert_png
        self.svg = svg  # ← already a deepcopy
        self.layer = layer
        self.frames = layer.animate
        self.delay = float(layer.args.get("delay", self.gif.options.delay))
        self.layer.args.update({
            "delay": self.delay / self.frames   # frame time split between animation steps
        })

    def export(self):
        """ applies transforms in each subframe, yields filename+args list """
        transforms = []
        for anim in self.svg.xpath("//svg:animate"):
            transforms.append([self.animate_style(anim, **anim.attrib), self.pace(anim)])
491
492
493
494
495
496
497



498
499
500
501
502
503
504
                    apply(pace(index / (self.frames - 1))) # durations are mapped into the 0.0 - 1.0 range

            # custom export
            dest = f"{self.gif.tempdir}/{self.gif.index}+{index}.{self.layer.label}.png"
            GIFExport.export_layers(self, dest, show=True)

            yield [dest, self.layer.args]




    @animate_with_values
    def animate_style(self, anim, attributeName, to, **attrib):
        """ <animate to="#000000" attributeName="fill" /> """
        target = self.get_target(anim)
        #self.adapt(target, css={attributeName: attrib["from"]})
        transformed = self.adapt(







>
>
>







494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
                    apply(pace(index / (self.frames - 1))) # durations are mapped into the 0.0 - 1.0 range

            # custom export
            dest = f"{self.gif.tempdir}/{self.gif.index}+{index}.{self.layer.label}.png"
            GIFExport.export_layers(self, dest, show=True)

            yield [dest, self.layer.args]

        # reset, in case frame gets animated again ([fixed]/[bg])
        self.layer.args["delay"] = self.delay

    @animate_with_values
    def animate_style(self, anim, attributeName, to, **attrib):
        """ <animate to="#000000" attributeName="fill" /> """
        target = self.get_target(anim)
        #self.adapt(target, css={attributeName: attrib["from"]})
        transformed = self.adapt(
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551

        return apply

    def animate_rotate(self, target, anim, start, end):
        """ simpler <animateTransform type=rotate> handler, does not fold the matrix at 180° """
        start = strargs(start) or [0]
        end = strargs(end) or [0]
        sfx = [str(coord) for coord in end[1:] + start[1:]]

        def apply(time, start=start[0], end=end[0], sfx=" ".join(sfx[0:2]), prepend=self.had_transform(target)):
            deg = start + (end - start) * time
            self.adapt(target, transform=prepend + f"rotate({deg} {sfx})")
        return apply

    def animate_motion(self, anim, path, **attrib): # pylint: disable=invalid-name







|







543
544
545
546
547
548
549
550
551
552
553
554
555
556
557

        return apply

    def animate_rotate(self, target, anim, start, end):
        """ simpler <animateTransform type=rotate> handler, does not fold the matrix at 180° """
        start = strargs(start) or [0]
        end = strargs(end) or [0]
        sfx = [str(coord) for coord in end[1:] + start[1:]]  # any center coordinates

        def apply(time, start=start[0], end=end[0], sfx=" ".join(sfx[0:2]), prepend=self.had_transform(target)):
            deg = start + (end - start) * time
            self.adapt(target, transform=prepend + f"rotate({deg} {sfx})")
        return apply

    def animate_motion(self, anim, path, **attrib): # pylint: disable=invalid-name
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656

657
658
659
660
661
662
663
664
                    return (r_time - r_begin) / r_end

        if attr("repeatCount"):
            # meh: ignorantly zigzags the repeat into our .delay time slice
            def vary(r_time, count=int(attr("repeatCount")), vary=vary):
                """ amplify by repeatCount, modulo into 0.0 … 1.0 interval """
                return (count * vary(r_time)) % 1.0
                
        if attr("calcMode") == "discrete":
            def vary(r_time):
                """ just snaps from one to the other extreme; nobody will ever use it """
                return 0.0 if r_time < 0.5 else 1.0

        if attr("calcMode") == "paced" or smooth and not attr("calcMode"):
            def vary(r_time, vary=vary):
                """ dense sigmoid (quick approx via KmPlot) """
                return vary(10 / (10 + math.exp(-(9 * r_time - 7))))

        if attr("calcMode") == "spline" and attr("keyTimes"):  # (ignoring the actual keySplines= of course)
            def vary(r_time, steps=strargs(attr("keyTimes", "0;1")), vary=vary):
                """ only linear [0, 0.33, 0.66, 1.0] here, values=[] would require interpretation in handlers """
                dist = 1.0 / len(steps)           # 0.25 for len=4

                offs = int(r_time * len(steps))   # r=0.30 → vary index [1]
                steps = steps[:] + [1.0, 1.0]
                val_diff = steps[offs+1] - steps[offs]   # steps difference 0.33-0.25
                off_diff = (r_time - dist * offs) / dist # offset [2]+0.1 to quantify value
                return vary(steps[offs] + val_diff * off_diff)

        def vary(r_time, vary=vary):
            """ guard for invalid params and calculation hiccups """







|













|
>
|







641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
                    return (r_time - r_begin) / r_end

        if attr("repeatCount"):
            # meh: ignorantly zigzags the repeat into our .delay time slice
            def vary(r_time, count=int(attr("repeatCount")), vary=vary):
                """ amplify by repeatCount, modulo into 0.0 … 1.0 interval """
                return (count * vary(r_time)) % 1.0

        if attr("calcMode") == "discrete":
            def vary(r_time):
                """ just snaps from one to the other extreme; nobody will ever use it """
                return 0.0 if r_time < 0.5 else 1.0

        if attr("calcMode") == "paced" or smooth and not attr("calcMode"):
            def vary(r_time, vary=vary):
                """ dense sigmoid (quick approx via KmPlot) """
                return vary(10 / (10 + math.exp(-(9 * r_time - 7))))

        if attr("calcMode") == "spline" and attr("keyTimes"):  # (ignoring the actual keySplines= of course)
            def vary(r_time, steps=strargs(attr("keyTimes", "0;1")), vary=vary):
                """ only linear [0, 0.33, 0.66, 1.0] here, values=[] would require interpretation in handlers """
                length = len(steps) - 1
                dist = 1.0 / length                   # 0.25 for len=4
                offs = int(r_time * length)           # r=0.30 → vary index [1]
                steps = steps[:] + [1.0, 1.0]
                val_diff = steps[offs+1] - steps[offs]   # steps difference 0.33-0.25
                off_diff = (r_time - dist * offs) / dist # offset [2]+0.1 to quantify value
                return vary(steps[offs] + val_diff * off_diff)

        def vary(r_time, vary=vary):
            """ guard for invalid params and calculation hiccups """