This commit is contained in:
Mario Voigt 2021-07-23 02:36:56 +02:00
commit 4d622c5291
4878 changed files with 1849508 additions and 0 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['https://y.stadtfabrikanten.org/donate']

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
*.pyc
/.project
/.pydevproject
/.settings/org.eclipse.core.resources.prefs
*.out
extensions/.project
extensions/fablabchemnitz/animate_order/drawing.svg
extensions/fablabchemnitz/animate_order/animate_order.html
extensions/fablabchemnitz/path2flex/DebugPath2Flex.txt
extensions/fablabchemnitz/elliptical_cone_box/DebugEllConicBox.txt
extensions/.pydevproject
extensions/.settings/org.eclipse.core.resources.prefs
extensions/000_Inkscape_Gallery

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

70
README.md Normal file
View File

@ -0,0 +1,70 @@
# MightyScape for Inkscape 1.0+
In short: A maintained extension collection for Inkscape 1.0+, working on Windows and Linux. There are **210 extension folders** with **372 .inx files** inside. We also take part at https://inkscape.org/gallery/=extension/ (with single extension uploads).
# About MightyScape
Looking to get more productive we started using some more special Inkscape extensions. We love Inkscape. And we love things like 3d printing, laser cutting, vinyl cutting, pen plotting, maths, physics, geometry, patterns, 2D drawings, 3D CAD , embroidery and more stuff. All this you can do with Inkscape! We recognized that there is no good source to pull extensions in a quick and clean way. Each developer puts his own code on his hidden/unknown repository and often without enough documentation or visible results for common understanding. Many plugins are completely unknown that way, and a lot of extensions are forked x times or are unmaintained. So many of them do not work with recent Inkscape or were never tested with newer versions so far.
# What and why?
This is a one-to-bundle-them-all collection of hundreds of additional functions to Inkscape (extensions) for the new Python 3 based version 1.X including documentation, made for makers and artists. All plugins where sorted into custom categories (to avoid overloading the standard extension menu of Inkscape). You can find most of them in sub menu "FabLab Chemnitz". We renamed and cleaned a lot of *.inx files and *.py files. We applied some function renamings, id changes (for preferences.xml clean-keeping), spelling fixes, formattings and parameter corrections.
It took years to search and find all them on the web (so much different possible sources where to find!), to read, to comment (report issues), to fix problems, to test, to document and to provide them online. Many extensions were nearly lost in translation.
At least this repo will help to bring alife some good things and will show hidden gold. It meshes things together in a fresh and bundled way - with ease of use and minimum installation stress. A lot of code is not at the optimum. A mass of bugs has to be fixed and different tools should be improved in usage generally. This package will show errors more quickly. So hopefully a lot of new code fixes is result from this package. Maybe some people help to make all the stuff compatible with Inkscape 1.0 and newer.
# Credits / Help to develop
* This is not a repository to steal the work of others. The credits go to each developer, maintainer, commiter, issue reporter and so on.
* All plugins are open source licensed and are fully compatible to be legally inside this repository. This plugin is a fully non-commercial project too
* All plugins were taken from each git repo's master branch (if git/svn available). There might exist some development branches, fork branches or issue comments which might resolve some issues or enhance functionality of provided plugins. To check for recent github forks use https://techgaun.github.io
* A lot of plugins were fixed by ourselves in countless hours
* If you find bugs or have ideas please push them directly to the corresponding root repository of the developer or put it to https://github.com/vmario89/mightyscape-1.X/issues
* Credits for creation of this big package: Mario Voigt / FabLab Chemnitz
# Used software for development
* Gitea and Github for hosting this
* GitEye and SourceTree git frontends for commiting
* LiClipse and NotePad++ for code
* regular Python installation (both Linux and Windows)
# Requirements / Tested environment
* tested with Inkscape
* Windows portable Version (Inkscape 1.2-dev (25cba68, 2021-05-16)) @ Windows 10
* Linux dev trunk (https://inkscape.org/de/release/inkscape-master/gnulinux/ubuntu/ppa/dl/) @ Ubuntu 20 LTS
* tested using Python 3.8.5 64 Bit and 3.9.4 64 Bit
* some extensions require custom Python installation/modules. See documentation at our FabLab Chemnitz Wiki (see below).
* some extensions require additional commands, packages or other installers (see documentation too).
* the structure of this repo is intended the following way: all extensions which require exactly one *.py and one *.inx file are kept on the top level /mightyscape-1.X/extensions/fablabchemnitz. So just copy them to your Inkscape's extension directory. All extension which require additional libraries have their own sub directory. You will find redundancies in this repo like node.exe (NodeJS). We did it this way to give easy possibilty to only pick the extensions you want (instead creating ~200 repositories).
# Remaining ToDos
* clean code
* make more precise documentation with more examples
* check out command line handling of extension. This was totally ignored yet
# Installation, documentation and examples
MightyScape does not work with any releases or feature branches. Just use "git clone" to get the recent commit from master branch.
Please see at https://y.stadtfabrikanten.org/mightyscape-overview for installation tips like required python modules, file locations and other adjustments.
# Donate
<img src="./extensions/fablabchemnitz/000_about_fablabchemnitz.svg">
We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.
You like our work and want to support us? You can donate to our non-profit organization by different ways:
https://y.stadtfabrikanten.org/donate
**Thanks for using our extension and helping us!**
# Locations
This repo has two remotes:
* https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X (root repo origin)
* https://github.com/vmario89/mightyscape-1.X (repo for collaboration. You can create your issues here. Active since May 2021)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,63 @@
#!/bin/bash
echo "Validating inx files with xmllint. Only errors are printed to console"
for folder in */ ; do xmllint --noout --relaxng ./inkscape.extension.rng $folder*.inx > /dev/null 2>> 000_xmllint.out; done; grep -v "validates\|warning: failed to load external entity" 000_xmllint.out; rm 000_xmllint.out
echo "Count of inx files:"
INX=$(find ./ -type f -name "*.inx" | wc -l)
echo INX: $INX
echo "Count of extension folders:"
FOLDERS=$(ls -d */ | wc -l)
echo FOLDERS: $FOLDERS
README="../../README.md"
#replace values in README.md
sed -i 's/\*\*.* extension folders\*\*/\*\*'${FOLDERS}' extension folders\*\*/g' ${README}
sed -i 's/\*\* with .* \.inx files\*\*/\*\* with \*\*'${INX}' \.inx files\*\*/g' ${README}
echo "Removing unrequired pyc cache files"
find . -type d -name "__pycache__" -exec rm -rf {} \;
read -p "Build local gallery extension zip files?" -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Building Inkscape gallery extension zip files"
TARGETDIR="../000_Inkscape_Gallery"
mkdir -p $TARGETDIR > /dev/null
#list of extensions which are uploaded at Inkscape gallery by us at the moment
for EXTENSION in \
"animate_order" \
"cleanup_styles" \
"contour_scanner_and_trimmer" \
"convert_to_polylines" \
"create_links" \
"dxf2papercraft" \
"dxf_dwg_importer" \
"imagetracerjs" \
"inventory_sticker" \
"move_path_node" \
"remove_empty_groups" \
"offset_paths" \
"papercraft_unfold" \
"paperfold" \
"primitive" \
"slic3r_stl_input" \
"split_and_break_bezier_at_t" \
"styles_to_layers" \
"ungrouper_and_element_migrator_filter" \
"unwind_paths" \
"vpypetools"
do
EXTRA=""
if [[ $EXTENSION == "styles_to_layers" ]] || [[ $EXTENSION == "ungrouper_and_element_migrator_filter" ]]; then
EXTRA="${EXTRA} apply_transformations/"
elif [[ $EXTENSION == "styles_to_layers" ]] || [[ $EXTENSION == "ungrouper_and_element_migrator_filter" ]]; then
EXTRA="${EXTRA} remove_empty_groups/"
fi
ZIPFILE=$TARGETDIR/$EXTENSION.zip
rm $ZIPFILE > /dev/null
zip -r $ZIPFILE $EXTENSION/ 000_about_fablabchemnitz.svg $EXTRA
done
fi

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>About/Upgrade MightyScape</name>
<id>fablabchemnitz.de.about_upgrade_mightyscape</id>
<param name="tab" type="notebook">
<page name="tab_settings" gui-text="Upgrade Options">
<param name="convert_to_git" type="bool" gui-text="Convert to .git" gui-description="If you downloaded MightyScape as .zip or .tar.gz you cannot upgrade using this extension. But you can convert your downloaded directory to a .git one by enabling this option">false</param>
<param name="stash_untracked" type="bool" gui-text="Stash untracked files" gui-description="Enable to drop your local changes">false</param>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">MightyScape Extension Collection</label>
<label>2019 - 2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer />
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
<spacer />
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer />
<label appearance="header">Click "Apply" to upgrade MightyScape to recent version!</label>
</page>
<page name="tab_donate" gui-text="Donate">
<label appearance="header">Coffee + Pizza</label>
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
<spacer />
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer />
<label>Thanks for using our extension and helping us!</label>
<image>../000_about_fablabchemnitz.svg</image>
</page>
</param>
<effect needs-live-preview="false">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz"/>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">about_upgrade_mightyscape.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""
Upgrade MightyScape from Inkscape Extension Dialog. Made for end users
Extension for InkScape 1.X
Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org
Date: 14.05.2021
Last patch: 23.07.2021
License: GNU GPL v3
ToDo
- enable stash option
- add while loop to list all remoteCommits by jumping from parent to parent
- add option to create/init .git repo from zip downloaded MightyScape version (to enable upgrader) if no .git dir was found
"""
import inkex
import os
import warnings
from datetime import datetime
try:
import git
from git import Repo #requires GitPython lib
except:
inkex.utils.debug("Error. GitPython was not installed but is required to run the upgrade process!")
exit(1)
class AboutUpgradeMightyScape(inkex.EffectExtension):
def update(self, local_repo, remote):
try:
localCommit = local_repo.head.commit
remote_repo = git.remote.Remote(local_repo, 'origin')
remoteCommit = remote_repo.fetch()[0].commit
self.msg("Latest remote commit is: " + str(remoteCommit)[:7])
if localCommit.hexsha != remoteCommit.hexsha:
ssh_executable = 'git'
with local_repo.git.custom_environment(GIT_SSH=ssh_executable):
origin = local_repo.remotes.origin
origin.fetch()
fetch_info = origin.pull() #finally pull new data
for info in fetch_info: #should return only one line in total
inkex.utils.debug("Updated %s to commit id %s" % (info.ref, str(info.commit)[:7]))
inkex.utils.debug("Please restart Inkscape to let the changes take effect.")
else:
inkex.utils.debug("Nothing to do! MightyScape is already up to date!")
except git.exc.GitCommandError as e:
self.msg("Error: ")
self.msg(e)
return False
return True
def add_arguments(self, pars):
pars.add_argument("--tab")
pars.add_argument("--convert_to_git", type=inkex.Boolean, default=False, help="If you downloaded MightyScape as .zip or .tar.gz you cannot upgrade using this extension. But you can convert your downloaded directory to a .git one by enabling this option")
pars.add_argument("--stash_untracked", type=inkex.Boolean, default=False, help="Stash untracked files and continue to upgrade")
def effect(self):
warnings.simplefilter('ignore', ResourceWarning) #suppress "enable tracemalloc to get the object allocation traceback"
#get the directory of mightyscape
extension_dir = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '../')) #go up to main dir /home/<user>/.config/inkscape/extensions/mightyscape-1.X/
main_dir = os.path.abspath(os.path.join(extension_dir, '../../')) #go up to main dir /home/<user>/.config/inkscape/extensions/mightyscape-1.X/
#create some statistics
totalFolders = 0
for root, folders, files in os.walk(extension_dir):
totalFolders += len(folders)
break #prevent descending into subfolders
totalInx = 0
for root, folders, files in os.walk(extension_dir):
for file in files:
if file.endswith('.inx'):
totalInx += 1
inkex.utils.debug("Locally there are {} extension folders with {} .inx files!\n".format(totalFolders, totalInx))
remotes = []
remotes.append(["https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X.git", "origin"]) #main
remotes.append(["https://github.com/vmario89/mightyscape-1.X.git", "github"]) #copy/second remote
gitDir = os.path.join(main_dir, ".git")
if not os.path.exists(gitDir):
if self.options.convert_to_git is True:
local_repo = Repo.init(main_dir)
local_repo.git.add(all=True)
localRemotes = []
for remote in remotes:
localRemotes.append(local_repo.create_remote(remote[1], url=remote[0]))
localRemotes[0].update()
local_repo.index.commit('.')
if self.options.stash_untracked is True:
local_repo.git.stash('save')
local_repo.git.checkout('origin/master')
#git init
#git add .
#git remote add origin https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X.git
#git remote add github https://github.com/vmario89/mightyscape-1.X.git
#git remote update
#git commit -m "."
#git stash
#git checkout origin/master
else:
self.msg("MightyScape .git directory was not found. It seems you installed MightyScape the traditional way (by downloading and extracting from archive). Please install MightyScape using the git clone method if you want to use the upgrade function. More details can be found in the official README.")
exit(1)
local_repo = Repo(gitDir)
#check if it is a non-empty git repository
if local_repo.bare is False:
if local_repo.is_dirty(untracked_files=True) is False:
if len(local_repo.untracked_files) > 0:
if self.options.stash_untracked is True:
local_repo.git.stash('save')
else:
inkex.utils.debug("There are some untracked files in your MightyScape directory. Still trying to pull recent files from git...")
localLatestCommit = local_repo.head.commit
localCommits = list(local_repo.iter_commits("origin/master", max_count=10, skip=0))
self.msg("Local commit id is: " + str(localLatestCommit)[:7])
self.msg("There are {} local commits at the moment.".format(len(localCommits)))
localCommitList = []
for localCommit in localCommits:
localCommitList.append(localCommit)
#localCommitList.reverse()
self.msg("*"*40)
self.msg("Latest local commits are:")
for i in range(0, len(localCommits)):
self.msg("{} | {} : {}".format(
datetime.utcfromtimestamp(localCommitList[i].committed_date).strftime('%Y-%m-%d %H:%M:%S'),
localCommitList[i].name_rev[:7],
localCommitList[i].message.strip())
)
#self.msg(" - {}: {}".format(localCommitList[i].newhexsha[:7], localCommitList[i].message))
self.msg("*"*40)
#finally run the update
success = self.update(local_repo, remotes[0][0])
if success is False: #try the second remote if first failed
self.msg("Error receiving latest remote commit from main git remote {}. Trying second remote ...".format(remotes[0][0]))
success = self.update(local_repo, remotes[1][0])
if success is False: #if still false:
self.msg("Error receiving latest remote commit from second git remote {}.\nAre you offline? Cannot continue!".format(remotes[0][0]))
exit(1)
else:
inkex.utils.debug("No \".git\" directory found. Seems your MightyScape was not installed with git clone. Please see documentation on how to do that.")
exit(1)
if __name__ == '__main__':
AboutUpgradeMightyScape().run()

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Affine Spirals</name>
<id>fablabchemnitz.de.affine_spirals</id>
<param name="active-tab" type="notebook">
<page name="title" gui-text="Settings">
<param name="num_lines" type="int" min="1" max="100" gui-text="Depth">3</param>
<param name="num_petals" type="int" min="2" max="100" gui-text="petals">2</param>
<param name="shrink_ratio" type="float" min="0.01" max="0.99" precision="2" gui-text="shrink factor">0.8</param>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from Generator"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">affine_spirals.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python3
import inkex
from lxml import etree
from math import cos, sin, pi, exp
__version__ = '0.1'
def line(npts=40, x0=0, y0=0, delta=.5, sgn=1):
#returns a list of points on a line (y = +/- x + c) starting at x0,y0
return [ (x0 + delta*t, y0 + sgn*delta*t) for t in range(npts)]
def ff(v, ww=.25, ds=.4):
#covering map from R^2 ro punctured plane
x,y = v
r,u = exp(-ds*x), cos(pi*ww*y) + 1J*sin(pi*ww*y)
return r*u
def mk_plugs(pts):
#returns a list of complex representing a plug type segment
segs = [fit_plug(end_pts) for end_pts in zip(pts,pts[1:]) ]
tmp = []
for seg in segs:
tmp.extend(seg)
return tmp
def fit_plug(ss):
a,b = ss
rot = complex(b-a)
pts = [0,.45,.4 + .15*1J, .6 + .15*1J, .55, 1]
return [rot*z + a for z in pts]
def pts2curve(cplxs):
'''makes a polyline path element from a list of complex
'''
def cplx2pt(z):
return (z.real,z.imag)
scale = 200
data = [cplx2pt( scale*z ) for z in cplxs ]
pth = [ '%.2f, %.2f '%z for z in data]
return 'M '+ ''.join(pth)
class AffineSpirals(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--num_lines", type=int, default=3)
pars.add_argument("--num_petals", type=int, default=3)
pars.add_argument("--shrink_ratio", type=float, default=3)
pars.add_argument("--active-tab", default='title')
def calc_unit_factor(self):
unit_factor = self.svg.unittouu(str(1.0) + self.options.units)
return unit_factor
def effect(self):
path_stroke = '#DD0000' # take color from tab3
path_fill = 'none' # no fill - just a line
path_stroke_width = 1. # can also be in form '0.6mm'
page_id = self.options.active_tab # sometimes wrong the very first time
styles = [ {'stroke': path_stroke , 'fill': 'none', 'stroke-width': path_stroke_width},
{'stroke': 'none', 'fill': '#FFFF00', 'stroke-width': 0}]
styles = [str(inkex.Style(x)) for x in styles]
# This finds center of current view in inkscape
t = 'translate(%s,%s)' % (self.svg.namedview.center[0], self.svg.namedview.center[1])
# Make a nice useful name
g_attribs = {inkex.addNS('label','inkscape'): 'koch',
inkex.addNS('transform-center-x','inkscape'): str(0),
inkex.addNS('transform-center-y','inkscape'): str(0),
'transform': t,
'style' : styles[1],
'info':'N: '+str(self.options.num_lines) }
topgroup = etree.SubElement(self.svg.get_current_layer(), 'g', g_attribs)
NN = 2*self.options.num_lines
NP = self.options.num_petals
SF = 2*self.options.shrink_ratio
payload = []
for y in range(-NP,NP):
mpts = [ff(z,ww=1./NP, ds=SF) for z in line(npts=NN, y0=y)]
payload.append(mk_plugs(mpts))
mpts = [ff(z,ww=1./NP, ds=SF) for z in line(npts=NN, y0=y,sgn=-1 )]
payload.append(mk_plugs(mpts))
payload = [pts2curve(cc) for cc in payload]
payload = ' '.join(payload)
curve_attribs = { 'style': styles[0], 'd': payload}
etree.SubElement(topgroup, inkex.addNS('path','svg'), curve_attribs)
if __name__ == '__main__':
AffineSpirals().run()

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>AI compatible EPS output</name>
<id>fablabchemnitz.de.ai_compatible_eps_output</id>
<output>
<extension>.eps</extension>
<mimetype>application/eps</mimetype>
<filetypename>Encapsulated PostScript - AI compatible (*.eps)</filetypename>
<filetypetooltip>Adobe Illustrator 7 compatible EPS</filetypetooltip>
</output>
<script>
<command location="inx" interpreter="python">ai_compatible_eps_output.py</command>
</script>
</inkscape-extension>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Animate Order</name>
<id>fablabchemnitz.de.animate_order</id>
<param name="tab" type="notebook">
<page name="tab_general" gui-text="Animate Order">
<label appearance="header">Warning!</label>
<label>Do not use "-inkscape-stroke:hairline" attribute for your paths. It will not render. Your page might be blank!</label>
<spacer/>
<param name="time" type="float" min="0.000" max="9999999.999" precision="3" gui-text="Duration (frames)">5.0</param>
<param name="fps" type="int" min="1" max="100" gui-text="Frame per second (fps)">60</param>
<param name="sequence_type" type="optiongroup" appearance="combo" gui-text="Sequence type">
<option value="oneByOne">Line by line (one by one)</option>
<option value="delayed">Delayed</option>
</param>
<param name="reverse" type="bool" gui-text="Reverse order">false</param>
<param name="browser" type="optiongroup" appearance="combo" gui-text="Browser" gui-description="Select your desired browser (must be installed and must exist in %PATH% variable).">
<option value="chromium">Chromium</option>
<option value="chrome">Google Chrome</option>
<option value="firefox">Firefox</option>
</param>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">Animate Order</label>
<label>Create SVG preview file and show it in browser. Helps to quickly evaluate line order for cutting processes.</label>
<label>2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/animateorder</label>
<spacer/>
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer/>
<label appearance="header">Third Party Modules</label>
<label appearance="url">https://github.com/maxwellito/vivus</label>
<spacer/>
<label appearance="header">MightyScape Extension Collection</label>
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
</page>
<page name="tab_donate" gui-text="Donate">
<label appearance="header">Coffee + Pizza</label>
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
<spacer/>
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer/>
<label>Thanks for using our extension and helping us!</label>
<image>../000_about_fablabchemnitz.svg</image>
</page>
</param>
<effect needs-live-preview="true">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Nesting/Cut Optimization"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">animate_order.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,108 @@
#!/usr/bin/env python3
import inkex
import subprocess
import shutil
import os
import sys
import warnings
"""
Extension for InkScape 1.X
Features
- Create SVG preview file and show it in browser. Helps to quickly evaluate line order for cutting processes
Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org
Date: 21.04.2021
Last patch: 07.05.2021
License: GNU GPL v3
Used version of Vivus JS library: https://github.com/maxwellito/vivus/releases/tag/v0.4.6 - MIT License
Browser config:
Firefox via about:config -> privacy.file_unique_origin = false
ToDo:
- adjust width and height (give options)
- embed config buttons inside html to adjust time/type/... (more flexible than clicking from Inkscape)
- we should do it the way like vivus instant -> http://maxwellito.github.io/vivus / https://maxwellito.github.io/vivus-instant
- the generated SVGs can be downloaded again and include all animations!
- calculate the total length of all paths and auto-adjust the speed to have good visibility
- Possible feature request could be to handle "animate selected objects only". See "Export selection as ... " extension to steal that code.
"""
DETACHED_PROCESS = 0x00000008
class AnimateOrder(inkex.EffectExtension):
def spawnIndependentProcess(self, args): #function to spawn non-blocking inkscape instance. the inkscape command is available because it is added to ENVIRONMENT when Inkscape main instance is started
warnings.simplefilter('ignore', ResourceWarning) #suppress "enable tracemalloc to get the object allocation traceback"
if os.name == 'nt':
subprocess.Popen(args, close_fds=True, creationflags=DETACHED_PROCESS)
else:
subprocess.Popen(args, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
warnings.simplefilter("default", ResourceWarning)
def add_arguments(self, pars):
pars.add_argument("--tab")
pars.add_argument("--time", type=float, default = 5.0, help="Duration (frames)")
pars.add_argument("--fps", type=int, default = 60.0, help="Frames per second (fps)")
pars.add_argument("--sequence_type", help="Sequence type")
pars.add_argument("--reverse", type = inkex.Boolean, default = False, help="Reverse order")
pars.add_argument("--browser", help="Select your desired browser (must be installed and must exist in %PATH% variable).")
def effect(self):
#write current SVG to extensions' directory. Target name must be "drawing.svg" because it is embedded in animate_order.html statically
inFile = "drawing.svg"
extension_dir = os.path.dirname(os.path.realpath(__file__))
shutil.copy2(self.options.input_file, os.path.join(extension_dir, inFile))
target_html = os.path.join(extension_dir, "animate_order.html")
docTitle = self.document.getroot().get("{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}docname")
if docTitle is None:
title = "Animate Order - Vivus JS"
else:
title = "Animate Order - " + docTitle
vivus_include = "./vivus-0.4.6/dist/vivus.js"
duration = self.options.time * self.options.fps # we guess we have 20 ... 60 fps. depends on performance of the machine
frames_per_second = self.options.fps
type = self.options.sequence_type
reverse = str(self.options.reverse).lower()
with open(target_html, "w") as text_file:
print( '<html>' , file=text_file)
print( ' <head>' , file=text_file)
print( ' <meta charset="UTF-8">' , file=text_file)
print( ' <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">' , file=text_file)
print( ' <meta name="viewport" content="width=device-width, initial-scale=1.0">' , file=text_file)
print(f' <title>{title}</title>' , file=text_file)
print( ' <meta name="description" content="SVG Drawing Animation">' , file=text_file)
print( ' </head>' , file=text_file)
print( ' <body>' , file=text_file)
print( ' <button onclick="vi.reset().play();">replay</button>' , file=text_file)
print( ' <br/>' , file=text_file)
print(f' <object id="animate_order" type="image/svg+xml" data="{inFile}"></object>' , file=text_file)
print(f' <script src="{vivus_include}"></script>' , file=text_file)
print( ' <script>' , file=text_file)
print( " var vi = new Vivus('animate_order', {type: '" + f'{type}' + "', \
duration:" + f'{duration}' + ", reverseStack:" + f'{reverse}' + "});" , file=text_file)
print( ' </script>' , file=text_file)
print( ' </body>' , file=text_file)
print( '</html>' , file=text_file)
if os.path.exists(target_html) is False:
inkex.utils.debug("Error. Target file does not exist!")
exit(1)
#now open firefox
args = [self.options.browser, target_html]
try:
self.spawnIndependentProcess(args)
except FileNotFoundError as e:
inkex.utils.debug("Error. Check for correct browser installation and try again!")
exit(1)
if __name__ == '__main__':
AnimateOrder().run()

View File

@ -0,0 +1,37 @@
{
"env": {
"es6": false,
"amd": false,
"browser": true,
"jasmine": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"sourceType": "module"
},
"globals": {
"Pathformer": true,
"Vivus": true,
"window": true,
"document": true,
"define": true,
"jasmine": true,
"it": true,
"expect": true,
"describe": true,
"beforeEach": true,
"afterEach": true,
"spyOn": true
},
"rules": {
"no-cond-assign": 2,
"no-console": 0,
"no-const-assign": 2,
"no-class-assign": 2,
"no-this-before-super": 2,
"no-unused-vars": 1,
"no-empty": 0,
"object-shorthand": [2, "always"]
}
}

View File

@ -0,0 +1,16 @@
### Please define your problem or issue as clear as possible.
For a problem, please fill the following:
**Vivus version**:
**Browser**:
**Steps to reproduce it**:
**JSFiddle link (or similar platform)**:
*No personal website will be allowed, only sandboxed platform where the code is isolated, clear and can be hacked. I don't want to debug uglified code between 42 libraries.*
*[note]*
Please have a minimum of politeness. There's unfortunately only me as contributor/maintainor which help on my free time, I'm not the Amazon customer service or your Mom. I don't ask to send me flowers and tell me how amazing Vivus is (that won't make me help you quicker). But please try to do as much as you can before opening an issue: check that no closed issue mention a similar problem, that your script is executed correctly (conflicts, race conditions...).. Thanks :)
*[/note]*

View File

@ -0,0 +1,6 @@
coverage/
node_modules/
.DS_Store
.idea
yarn.lock
dist/vivus.min.js.map

View File

@ -0,0 +1,5 @@
test
assets
node_modules
coverage
.DS_Store

View File

@ -0,0 +1,8 @@
language: node_js
node_js:
- 'stable'
script: npm run lint && npm run test
before_install:
- export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start

View File

@ -0,0 +1,7 @@
# Contributing
First of all, thanks for contributing, thinking about contributing, or even arriving here by mistake.
Issues are reserved to mention a problem, a bug or discuss about implementing a feature. If you have any question or support request, please use [Gitter](https://gitter.im/maxwellito/vivus). For every issue, please try to give as much information as you can : version, steps to recreate it, examples (via jsFiddle or something like that).
About pull requests, please try to contact the maintainer beforehand. He's a kinda human Grumpy Cat trying to avoid features which can be useful for only 1% of users. The warning is only because it would be sad to see contributors spending time a feature that wouldn't be merged. Otherwise, I would recommend you to add a section in the `hacks.md` file. But if it's a bug fix, the chances to be merged are higher. If necessary please think about updating the tests.

View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) maxwellito
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="200px"
height="90px" viewBox="0 0 200 90" enable-background="new 0 0 200 90" xml:space="preserve">
<g id="Layout">
<g id="time">
<polyline fill="none" stroke="#000000" stroke-width="2" stroke-linecap="square" stroke-miterlimit="10" points="5,73 190,73
185.25,70 185.25,71.75 "/>
<text transform="matrix(1 0 0 1 88.6001 86.5)" font-family="'MyriadPro-Regular'" font-size="12">time</text>
</g>
</g>
<g id="Scenarios">
<g id="async" display="none">
<g id="timeline_3_" display="inline">
<path fill="#4D4D4D" d="M195,6c0,0.55-0.45,1-1,1H6C5.45,7,5,6.55,5,6V4c0-0.55,0.45-1,1-1h188c0.55,0,1,0.45,1,1V6z"/>
<path fill="#4D4D4D" d="M195,13c0,0.55-0.45,1-1,1H6c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h188c0.55,0,1,0.45,1,1V13z"/>
<path fill="#4D4D4D" d="M195,20c0,0.55-0.45,1-1,1H6c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h188c0.55,0,1,0.45,1,1V20z"/>
<path fill="#4D4D4D" d="M195,27c0,0.55-0.45,1-1,1H6c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h188c0.55,0,1,0.45,1,1V27z"/>
<path fill="#4D4D4D" d="M195,34c0,0.55-0.45,1-1,1H6c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h188c0.55,0,1,0.45,1,1V34z"/>
</g>
<g id="duration_3_" display="inline">
<rect x="5" y="52.75" fill="#D36281" width="190" height="4"/>
<line fill="none" stroke="#D36281" stroke-miterlimit="10" x1="5" y1="56.75" x2="5" y2="5"/>
<line fill="none" stroke="#D36281" stroke-miterlimit="10" x1="195" y1="56.75" x2="195" y2="33"/>
</g>
</g>
<g id="oneByOne">
<g id="timeline_2_">
<path fill="#4D4D4D" d="M43,6c0,0.55-0.45,1-1,1H6C5.45,7,5,6.55,5,6V4c0-0.55,0.45-1,1-1h36c0.55,0,1,0.45,1,1V6z"/>
<path fill="#4D4D4D" d="M81,13c0,0.55-0.45,1-1,1H44c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h36c0.55,0,1,0.45,1,1V13z"/>
<path fill="#4D4D4D" d="M119,20c0,0.55-0.45,1-1,1H82c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h36c0.55,0,1,0.45,1,1V20z"/>
<path fill="#4D4D4D" d="M157,27c0,0.55-0.45,1-1,1h-36c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h36c0.55,0,1,0.45,1,1V27z"/>
<path fill="#4D4D4D" d="M195,34c0,0.55-0.45,1-1,1h-36c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h36c0.55,0,1,0.45,1,1V34z"/>
</g>
<g id="duration_2_" display="none">
<rect x="43" y="52.75" display="inline" fill="#D36281" width="38" height="4"/>
<line display="inline" fill="none" stroke="#D36281" stroke-miterlimit="10" x1="43" y1="56.75" x2="43" y2="12"/>
<line display="inline" fill="none" stroke="#D36281" stroke-miterlimit="10" x1="81" y1="56.75" x2="81" y2="12"/>
</g>
</g>
<g id="delayed">
<g id="timeline_1_" display="none">
<path display="inline" fill="#4D4D4D" d="M132,6c0,0.55-0.45,1-1,1H6C5.45,7,5,6.55,5,6V4c0-0.55,0.45-1,1-1h125
c0.55,0,1,0.45,1,1V6z"/>
<path display="inline" fill="#4D4D4D" d="M147.75,13c0,0.55-0.45,1-1,1h-125c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h125
c0.55,0,1,0.45,1,1V13z"/>
<path display="inline" fill="#4D4D4D" d="M163.5,20c0,0.55-0.45,1-1,1h-125c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h125
c0.55,0,1,0.45,1,1V20z"/>
<path display="inline" fill="#4D4D4D" d="M179.25,27c0,0.55-0.45,1-1,1h-125c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h125
c0.55,0,1,0.45,1,1V27z"/>
<path display="inline" fill="#4D4D4D" d="M195,34c0,0.55-0.45,1-1,1H69c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h125
c0.55,0,1,0.45,1,1V34z"/>
</g>
<g id="duration">
<rect x="5" y="52.75" fill="#D36281" width="190" height="4"/>
<line fill="none" stroke="#D36281" stroke-miterlimit="10" x1="5" y1="56.75" x2="5" y2="5"/>
<line fill="none" stroke="#D36281" stroke-miterlimit="10" x1="195" y1="56.75" x2="195" y2="33"/>
</g>
<g id="delay" display="none">
<rect x="5" y="45.75" display="inline" fill="#5AA8C5" width="63" height="4"/>
<line display="inline" fill="none" stroke="#5AA8C5" stroke-miterlimit="10" x1="5" y1="49.75" x2="5" y2="5"/>
<line display="inline" fill="none" stroke="#5AA8C5" stroke-miterlimit="10" x1="68" y1="49.75" x2="68" y2="33"/>
</g>
</g>
<g id="custom" display="none">
<g id="timeline" display="inline">
<path fill="#4D4D4D" d="M24,6c0,0.55-0.45,1-1,1H6C5.45,7,5,6.55,5,6V4c0-0.55,0.45-1,1-1h17c0.55,0,1,0.45,1,1V6z"/>
<path fill="#4D4D4D" d="M79,13c0,0.55-0.45,1-1,1H42c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h36c0.55,0,1,0.45,1,1V13z"/>
<path fill="#4D4D4D" d="M88,20c0,0.55-0.45,1-1,1H51c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h36c0.55,0,1,0.45,1,1V20z"/>
<path fill="#4D4D4D" d="M195,27c0,0.55-0.45,1-1,1h-17c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h17c0.55,0,1,0.45,1,1V27z"/>
<path fill="#4D4D4D" d="M195,34c0,0.55-0.45,1-1,1h-93c-0.55,0-1-0.45-1-1v-2c0-0.55,0.45-1,1-1h93c0.55,0,1,0.45,1,1V34z"/>
</g>
<g id="duration_1_" display="inline">
<rect x="41" y="52.75" fill="#D36281" stroke="#D36281" stroke-miterlimit="10" width="38" height="4"/>
<g>
<g>
<line fill="none" stroke="#D36281" stroke-miterlimit="10" x1="41" y1="56.75" x2="41" y2="55.75"/>
<line fill="none" stroke="#D36281" stroke-miterlimit="10" stroke-dasharray="2.0357,2.0357" x1="41" y1="53.714" x2="41" y2="14.018"/>
<line fill="none" stroke="#D36281" stroke-miterlimit="10" x1="41" y1="13" x2="41" y2="12"/>
</g>
</g>
<g>
<g>
<line fill="none" stroke="#D36281" stroke-miterlimit="10" x1="79" y1="56.75" x2="79" y2="55.75"/>
<line fill="none" stroke="#D36281" stroke-miterlimit="10" stroke-dasharray="2.0357,2.0357" x1="79" y1="53.714" x2="79" y2="14.018"/>
<line fill="none" stroke="#D36281" stroke-miterlimit="10" x1="79" y1="13" x2="79" y2="12"/>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1,16 @@
{
"name": "vivus",
"description": "JavaScript library to make drawing animation on SVG",
"main": "dist/vivus.js",
"licence": "MIT",
"ignore": [
"assets/",
"src/",
"test/",
".gitignore",
".jshintrc",
"gulpfile.js",
"index.html",
"package.json"
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
# Vivus hacks
Some tricks about Vivus are very interesting and might help you but doesn't have their place in the documentation. So you will see them here.
## Animate `fill` with CSS
On inline SVG, it's very easy to animate fill color with just a bit of CSS.
Let's imagine you have an inline SVG element in your page with the ID `mySVG`. You apply the following CSS to make it with fill opacity to 0 by default, and a class with the opacity of 1 (with transition). Then once the animation finished, the class can be added to the svg.
```css
/* Style */
#mySVG * {
fill-opacity: 0;
transition: fill-opacity 1s;
}
#mySVG.finished * {
fill-opacity: 1;
}
```
```js
/* JavaScript */
new Vivus('mySVG', {}, function (obj) {
obj.el.classList.add('finished');
});
```
**WARNING**: This hack cannot work on sandboxed solutions like the use of `object` tag.

View File

@ -0,0 +1,517 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vivus.js - svg animation</title>
<meta name="description" content="SVG Drawing Animation" />
<style type="text/css">
/* Base style */
html {
font-size: 24px;
}
body {
margin: 0 0 40px;
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
font-weight: 200;
color: #666666;
background-color: #FFFFFF;
word-break: break-word;
}
a, a:visited, a:hover, a:link {
color: inherit;
outline: 0;
}
small {
font-weight: 100;
}
p {
font-size: 1rem;
line-height: 1.4rem;
}
button, .button {
margin: 0; padding: 3px 6px;
border-radius: 6px;
border: 1px solid currentColor;
color: inherit;
background-color: rgba(0,0,0,0);
font-size: 0.6rem;
font-weight: 300;
text-decoration: none;
cursor: pointer;
outline: 0;
}
button.active, .button.active {
background-color: currentColor;
}
button.active span, .button.active span {
color: #FFFFFF;
}
i {
background-color: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
svg * {
fill: none;
stroke: currentColor;
}
/* Common components */
.bloc {
color: #f9f9f9;
padding: 1px 0 30px;
clear: both;
}
.content {
margin: auto;
max-width: 960px;
padding: 0 20px;
}
.col3 {
width: 33.33%;
float: left;
text-align: center;
border-bottom-color: currentColor;
border-bottom-style: solid;
}
.col-container {
padding: 0 12px;
}
.col3 p {
font-size: 0.75rem;
line-height: 1.05rem;
}
/* Text */
.bigger {
font-size: 1rem;
font-weight: 100;
line-height: 1.4rem;
}
.center {
text-align: center;
}
.clearer {
clear: both;
}
.striked {
text-decoration: line-through;
}
.italic {
font-style: italic;
}
/* Blocs */
.bloc-head { color: #5aa8c5; padding: 30px; }
.bloc-demo { color: #FF495F; }
.bloc-timing { color: #F7A800; }
.bloc-scenario { color: #4fe084; }
.bloc-doc { color: #69B0CA; }
/* Custom */
#hi-there {
width: 100%;
stroke-width:4;
}
.intro-links {
text-align: right;
}
.example-description {
margin-left: 440px;
}
.timing-description {
min-height: 200px;
margin-right: 240px;
}
.obturateur {
stroke-width: 3;
stroke-miterlimit: 10;
}
#polaroid {
float: left;
width: 400px; height: 320px;
stroke: #f9f9f9;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 10;
}
#timing-example {
float: right;
width: 175px; height: 175px;
}
.goodbye-head {
font-size: 1.5rem;
text-align: center;
margin-bottom: 0;
}
.goodbye-sub {
font-size: 0.875rem;
text-align: center;
margin: 0 0 20px;
}
/* Media queries */
@media (max-width: 960px) {
.button-group { display: block; line-height: 1.8em; }
}
@media (min-width: 768px) {
.col3 { border-bottom: none; }
}
@media (max-width: 767px) {
#polaroid { width: 100%; max-height: 300px; }
.example-description { margin-left: 0; }
.timing-description { margin-right: 0; }
#timing-example { float: none; width: 100%; }
.col3 { width: 100%; float: none; margin-bottom: 25px; padding-bottom: 25px; border-bottom-width: 1px; min-height: 200px; }
.col3:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom-width: 0px; }
}
@media (min-width: 481px) and (max-width: 767px) {
#polaroid { width: 100%; max-height: 300px; }
.example-description { margin-left: 0; }
.timing-description { margin-right: 0; }
.col3:nth-child(2n) svg {
width: 200px;
float: right;
}
.col3:nth-child(2n) .col-container {
text-align: right;
margin-right: 200px;
}
.col3:nth-child(2n+1) svg {
width: 200px;
float: left;
}
.col3:nth-child(2n+1) .col-container {
text-align: left;
margin-left: 200px;
}
}
@media (max-width: 480px) {
.col-container { padding: 0px; }
#polaroid { width: 100%; max-height: 260px; }
}
</style>
</head>
<body>
<!-- Head: HI THERE -->
<div class="bloc bloc-head">
<svg height="300" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 404.7 354" enable-background="new 0 0 404.7 354" id="hi-there" onclick="hi.reset().play();">
<!-- HI -->
<path data-duration="10" d="M324.6,61.2c16.6,0,29.5-12.9,29.5-29.5c0-16.6-12.9-29.5-29.5-29.5c-16.6,0-29.5,12.9-29.5,29.5C295.1,48.4,308,61.2,324.6,61.2z"/>
<path data-duration="130" d="M366.2,204.2c-9.8,0-15-5.6-15-15.1V77.2h-85v28h19.5c9.8,0,8.5,2.1,8.5,11.6v72.4c0,9.5,0.5,15.1-9.3,15.1H277h-20.7c-8.5,0-14.2-4.1-14.2-12.9V52.4c0-8.5,5.7-12.3,14.2-12.3h18.8v-28h-127v28h18.1c8.5,0,9.9,2.1,9.9,8.9v56.1h-75V53.4c0-11.5,8.6-13.3,17-13.3h11v-28H2.2v28h26c8.5,0,12,2.1,12,7.9v142.2c0,8.5-3.6,13.9-12,13.9h-21v33h122v-33h-11c-8.5,0-17-4.1-17-12.2v-57.8h75v58.4c0,9.1-1.4,11.6-9.9,11.6h-18.1v33h122.9h5.9h102.2v-33H366.2z"/>
<path data-async data-delay="20" d="M358.8,82.8c11.1-4.2,18.8-14.7,18.8-27.5c0-8.5-3.4-16-8.9-21.3"/>
<path data-async d="M124.2,105.7V77c0-11.5,9.1-13.8,17.5-13.8h10.5V44.7"/>
<polyline data-async points="147.9,40.2 171.2,63.2 175.7,63.2"/>
<line data-async x1="295.1" y1="32.1" x2="275.2" y2="12.2"/>
<path data-async d="M266.2,204.7V75.9c0-8.5,5.2-12.8,13.7-12.8h18.3V44.7"/>
<polyline data-async points="265.9,105.2 289.2,129.2 293.7,129.2"/>
<polyline data-async points="374.2,204.7 374.2,94.2 358.8,82.8 351.2,77.2"/>
<polyline data-async points="148.2,237.2 171.2,261.2 294.6,261.2 300.5,261.2 402.2,261.2 402.2,228.2 379.2,204.2"/>
<polyline data-async points="124.2,204.7 124.2,157.2 175.7,157.2"/>
<line data-async x1="147.7" y1="228.2" x2="129.2" y2="204.2"/>
<polyline data-async points="7.2,237.3 30.2,261.2 152.2,261.2 152.2,241.7"/>
<polyline data-async points="1.9,40.2 26,63.2 39.7,63.2"/>
<line data-async x1="129.2" y1="12.2" x2="148.2" y2="33.2"/>
<line data-async x1="303.9" y1="53" x2="328.1" y2="77.2"/>
<line x1="345.1" y1="10.5" x2="368.7" y2="34"/>
<!-- there -->
<path data-delay="30" data-duration="60" stroke-linecap="round" stroke-linejoin="round" d="M76.8,337.3c0,0,1.9,12.2,13.1,12.2c22.1,0,23.8-1.8,59-66.4c-19.7,35.7-36.4,66.2-19.3,66.2c15.2,0,22.9-14.2,28.3-23.7c3.3-0.5,24-3.2,35-25.5c4-8.1,4.1-17.8-8.1-15.2c-5.6,1.2-13.1,14.8-15.7,19.2c-7.6,12.7-22.4,45.2-22.4,45.2s10.3-22.4,21.5-22.4c15.5,0-9.4,22.4,4.7,22.4c4.9,0,11.7-11.4,16.6-20.9c7.5,4.7,19.7,1.7,24.5-8.1c10.1-20.4-14.4-12.8-24.5,8.1c-5.5,11.3-2.2,21.1,11.2,21.1c16.4,0,26.1-28.3,30.5-37.5c9.9,2.5,14,2.5,22.7-1.1c-3.5,5.1-24,38.1-8.3,38.1c6.7,0,11.7-11.4,16.6-20.9c7.5,4.7,19.7,1.7,24.5-8.1c10.1-20.4-14.4-12.8-24.5,8.1c-5.5,11.3-2.2,21.1,11.2,21.1c16.4,0,20.6-4,24.7-10.5"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M157.3,300.8c3.8-2.3-29,0.8-35.6,3.2"/>
</svg>
</div>
<!-- Intro -->
<div class="content">
<h1>vivus<small>, bringing your SVGs to life</small></h1>
<p>Vivus is a lightweight JavaScript class (with no dependencies) that allows you to animate SVGs, giving them the appearence of being drawn. There are a variety of different animations available, as well as the option to create a custom script to draw your SVG in whatever way you like.</p>
<p class="intro-links">
<a href="//github.com/maxwellito/vivus" class="button bigger">View on GitHub</a>
</p>
</div>
<!-- Animation examples/demo -->
<div class="bloc bloc-demo">
<div class="content">
<h2>Animation types</h2>
<div>
<div class="col3">
<svg id="obturateur1" class="obturateur" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="100%" height="200px" viewBox="0 0 200 200" enable-background="new 0 0 200 200" onclick="obt1.reset().play();">
<circle cx="100" cy="100" r="90"/>
<circle cx="100" cy="100" r="85.74"/>
<circle cx="100" cy="100" r="72.947"/>
<circle cx="100" cy="100" r="39.74"/>
<line x1="34.042" y1="131.189" x2="67.047" y2="77.781"/>
<line x1="31.306" y1="75.416" x2="92.41" y2="60.987"/>
<line x1="68.81" y1="34.042" x2="122.219" y2="67.046"/>
<line x1="124.584" y1="31.305" x2="139.013" y2="92.409"/>
<line x1="165.957" y1="68.809" x2="132.953" y2="122.219"/>
<line x1="168.693" y1="124.584" x2="107.59" y2="139.012"/>
<line x1="131.19" y1="165.957" x2="77.781" y2="132.953"/>
<line x1="75.417" y1="168.693" x2="60.987" y2="107.59"/>
</svg>
<div class="col-container">
<h3>Delayed</h3>
<p>Every path element is drawn at the same time with a small delay at the start. This is currently the default animation.</p>
<button onclick="obt1.reset().play();">replay</button>
</div>
</div>
<div class="col3">
<svg id="obturateur2" class="obturateur" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="100%" height="200px" viewBox="0 0 200 200" enable-background="new 0 0 200 200" onclick="obt2.reset().play();">
<circle cx="100" cy="100" r="90"/>
<circle cx="100" cy="100" r="85.74"/>
<circle cx="100" cy="100" r="72.947"/>
<circle cx="100" cy="100" r="39.74"/>
<line x1="34.042" y1="131.189" x2="67.047" y2="77.781"/>
<line x1="31.306" y1="75.416" x2="92.41" y2="60.987"/>
<line x1="68.81" y1="34.042" x2="122.219" y2="67.046"/>
<line x1="124.584" y1="31.305" x2="139.013" y2="92.409"/>
<line x1="165.957" y1="68.809" x2="132.953" y2="122.219"/>
<line x1="168.693" y1="124.584" x2="107.59" y2="139.012"/>
<line x1="131.19" y1="165.957" x2="77.781" y2="132.953"/>
<line x1="75.417" y1="168.693" x2="60.987" y2="107.59"/>
</svg>
<div class="col-container">
<h3>Sync</h3>
<p>Each line is drawn synchronously. They all start and finish at the same time, hence the name `sync`.</p>
<button onclick="obt2.reset().play();">replay</button>
</div>
</div>
<div class="col3">
<svg id="obturateur3" class="obturateur" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="100%" height="200px" viewBox="0 0 200 200" enable-background="new 0 0 200 200" onclick="obt3.reset().play();">
<circle cx="100" cy="100" r="90"/>
<circle cx="100" cy="100" r="85.74"/>
<circle cx="100" cy="100" r="72.947"/>
<circle cx="100" cy="100" r="39.74"/>
<line x1="34.042" y1="131.189" x2="67.047" y2="77.781"/>
<line x1="31.306" y1="75.416" x2="92.41" y2="60.987"/>
<line x1="68.81" y1="34.042" x2="122.219" y2="67.046"/>
<line x1="124.584" y1="31.305" x2="139.013" y2="92.409"/>
<line x1="165.957" y1="68.809" x2="132.953" y2="122.219"/>
<line x1="168.693" y1="124.584" x2="107.59" y2="139.012"/>
<line x1="131.19" y1="165.957" x2="77.781" y2="132.953"/>
<line x1="75.417" y1="168.693" x2="60.987" y2="107.59"/>
</svg>
<div class="col-container">
<h3>OneByOne</h3>
<p>Each path element is drawn one after the other. This animation gives the best impression of live drawing.</p>
<button onclick="obt3.reset().play();">replay</button>
</div>
</div>
</div>
<div class="clearer"></div>
</div>
</div>
<!-- Scripting -->
<div class="bloc bloc-timing">
<div class="content">
<h2>Timing function</h2>
<p>To give more freedom, it's possible to override the animation of each path and/or the entire SVG. It works a bit like the CSS animation timing function. But instead of using a cubic-bezier function, it use a simple JavaScript function. It must accept a number as parameter (between 0 to 1), then return a number (also between 0 and 1). It's a hook.</p>
<p>Here an example test to play around with the different properties available.</p>
<div>
<svg id="timing-example" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve" onclick="timing&&timing.reset().play();">
<g stroke-width="4" stroke-linecap="round" stroke-miterlimit="10">
<line x1="68.18066" y1="68.18066" x2="131.81934" y2="131.81934"/>
<line x1="68.18066" y1="131.82031" x2="131.81934" y2="68.17969"/>
<circle cx="100" cy="100" r="65"/>
<circle cx="100" cy="100" r="75"/>
<circle cx="100" cy="100" r="85"/>
<circle cx="100" cy="100" r="95"/>
</g>
</svg>
<div class="timing-description">
<p>Type
<span class="button-group">
<button onclick="timingTest(this,'type','delayed');" class="active"><span>delayed</span></button>
<button onclick="timingTest(this,'type','sync');"><span>sync</span></button>
<button onclick="timingTest(this,'type','oneByOne');"><span>oneByOne</span></button>
</span>
</p>
<p>Path timing function
<span class="button-group">
<button onclick="timingTest(this,'path','LINEAR');" class="active"><span>linear</span></button>
<button onclick="timingTest(this,'path','EASE');"><span>ease</span></button>
<button onclick="timingTest(this,'path','EASE_IN');"><span>ease-in</span></button>
<button onclick="timingTest(this,'path','EASE_OUT');"><span>ease-out</span></button>
<button onclick="timingTest(this,'path','EASE_OUT_BOUNCE');"><span>ease-out bounce</span></button>
</span>
</p>
<p>Anim timing function
<span class="button-group">
<button onclick="timingTest(this,'anim','LINEAR');" class="active"><span>linear</span></button>
<button onclick="timingTest(this,'anim','EASE');"><span>ease</span></button>
<button onclick="timingTest(this,'anim','EASE_IN');"><span>ease-in</span></button>
<button onclick="timingTest(this,'anim','EASE_OUT');"><span>ease-out</span></button>
<button onclick="timingTest(this,'anim','EASE_OUT_BOUNCE');"><span>ease-out bounce</span></button>
</span>
</p>
</div>
</div>
</div>
</div>
<!-- Scripting example -->
<div class="bloc bloc-scenario">
<div class="content">
<div class="script-example">
<svg id="polaroid" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 200 160" enable-background="new 0 0 200 160" onclick="pola.reset().play();">
<!-- Case -->
<!-- The case items will be drawn at the same time (attribute `data-async` on each tag) with a custom duration of 40 frames (attribute `data-duration`). WARNING: When you want to draw a bloc asynchronously (like here), the last item need to be `data-async` free. Otherwise the following tags will also start at the same time. I know it's a bit confusing, play a bit with it and you'll see. -->
<path data-async data-duration="40" d="
M106.725,104.742c-0.773,2.498-3.586,4.229-6.285,3.867L12.473,96.802c-2.699-0.363-4.262-2.682-3.49-5.18l25.164-81.436
c0.771-2.496,3.584-4.229,6.283-3.866l87.966,11.808c2.699,0.362,4.264,2.68,3.49,5.179L106.725,104.742z"/>
<path data-async data-duration="40" d="
M101.02,123.207c-0.773,2.5-3.587,4.23-6.286,3.867L6.766,115.27c-2.699-0.363-4.26-2.682-3.488-5.182l2.91-9.417
c0.771-2.499,3.585-4.23,6.285-3.87l87.967,11.809c2.699,0.361,4.261,2.682,3.49,5.18L101.02,123.207z"/>
<line data-async data-duration="40" x1="103.377" y1="128.225" x2="154.768" y2="155.32"/>
<line data-async data-duration="40" x1="109.852" y1="112.684" x2="155.035" y2="136.906"/>
<path data-async data-duration="40" d="
M9.096,120.207l47.932,21.994c0,0,98.06,12.414,97.74,13.119c-0.318,0.709,5.426-16.205,5.426-16.205l-2.646-96.842
c-1.098-7.587-2.467-11.8-8.559-15.024l-12.635-6.604"/>
<path data-async data-duration="40" d="
M161.586,38.135l30.717,16.085c0,0,5.295,2.323,4.543,6.504l-3.215,10.527c-0.648,2.621-2.939,4.988-8.229,2.798l-9.154-4.701
l-11.834,56.441"/>
<path data-duration="40" d="
M183.148,49.518c0,0,5.295,2.324,4.543,6.506l-3.215,10.526c-0.648,2.622-2.938,4.988-8.229,2.798"/>
<!-- Lens -->
<!-- All item will be drawn line by line, with an exception for the first one, a little delay (attribute `data-delay) to make a break between the drawing of the case and the start of the lens part -->
<path data-delay="20" d="
M87.176,56.143C83.274,68.78,69.043,77.538,55.395,75.706S33.846,62.146,37.75,49.511c3.903-12.637,18.135-21.392,31.783-19.562
C83.181,31.782,91.081,43.51,87.176,56.143z"/>
<path d="
M92.745,56.891c-4.785,15.48-22.219,26.213-38.942,23.969C37.079,78.615,27.4,64.245,32.184,48.763
c4.783-15.48,22.218-26.211,38.94-23.968C87.848,27.041,97.528,41.411,92.745,56.891z"/>
<path d="
M78.99,26.933c16.169,7.426,19.398,10.989,22.026,20.105c1.283,4.449,1.271,9.411-0.3,14.489
c-4.783,15.479-22.217,26.213-38.941,23.969c-8.68-1.165-21.171-7.963-25.613-14.055"/>
<path d="
M42.602,50.162c3.137-10.157,14.573-17.193,25.543-15.722"/>
<!-- Flash -->
<!-- This tag does not have any extra attribute. So it will start when the previous tag is finished. His duration and delay will be the one given in the options. -->
<path d="
M103.789,29.275c-0.568,1.841,0.582,3.549,2.57,3.818l12.807,1.72c1.988,0.266,4.062-1.012,4.633-2.851l1.66-5.38
c0.568-1.843-0.582-3.551-2.57-3.816l-12.807-1.72c-1.988-0.268-4.062,1.01-4.633,2.85L103.789,29.275z"/>
<!-- Output -->
<!-- Same case as Flash -->
<path d="
M11.129,105.791c-0.297,0.965,0.305,1.855,1.346,1.994l81.446,10.932c1.038,0.141,2.123-0.527,2.42-1.49l0,0
c0.298-0.961-0.304-1.855-1.343-1.996l-81.447-10.93C12.51,104.16,11.426,104.828,11.129,105.791L11.129,105.791z"/>
<!-- Design (color lines on the front) -->
<!-- All the lines will start at the same time, because they all have the attribute `data-async` -->
<line data-async x1="47.583" y1="101.505" x2="51.561" y2="88.267"/>
<line data-async x1="53.391" y1="102.326" x2="57.047" y2="90.125"/>
<line data-async x1="59.224" y1="103.068" x2="62.749" y2="91.295"/>
<line data-async x1="65.057" y1="103.814" x2="69.015" y2="90.637"/>
<line data-async x1="72.87" y1="19.969" x2="75.497" y2="11.082"/>
<line data-async x1="78.512" y1="21.325" x2="81.317" y2="11.868"/>
<line data-async x1="83.833" y1="23.718" x2="87.16" y2="12.582"/>
<line data-async x1="89.145" y1="26.141" x2="92.939" y2="13.498"/>
</svg>
<div class="example-description">
<h2>Scenarize</h2>
<p>This feature allows you to script the animation of your SVG. To do this, the custom values will be set directly in the DOM of the SVG.</p>
<p>Here is an example using <i>scenario-sync</i>.<br>I would recommend you look at the source code and the readme file for more information.</p>
<button onclick="pola.reset().play();">replay</button>
<button onclick="pola.play(-3);">rewind</button>
</div>
<div class="clearer"></div>
</div>
</div>
</div>
<!-- Info and documentation link -->
<div class="bloc bloc-doc">
<div class="content">
<p class="center">Play with it on <a href="https://maxwellito.github.io/vivus-instant/">Vivus instant</a>.</p>
<p class="center">More information and documentation on <a href="https://github.com/maxwellito/vivus#vivusjs">GitHub</a>.</p>
</div>
</div>
<!-- Goodbye -->
<div class="content">
<p class="goodbye-head">Thanks for watching.</p>
<p class="goodbye-sub">Made with <span class="striked">love</span> <span class="italic">a keyboard</span></p>
</div>
<!-- Le scripts -->
<script src="dist/vivus.js"></script>
<script>
function easeOutBounce (x) {
var base = -Math.cos(x * (0.5 * Math.PI)) + 1;
var rate = Math.pow(base,1.5);
var rateR = Math.pow(1 - x, 2);
var progress = -Math.abs(Math.cos(rate * (2.5 * Math.PI) )) + 1;
return (1- rateR) + (progress * rateR);
}
var timing,
timingProps = {
type: 'delayed',
duration: 150,
start: 'autostart',
pathTimingFunction: Vivus.LINEAR,
animTimingFunction: Vivus.LINEAR
};
function timingTest (buttonEl, property, type) {
var activeSibling = buttonEl.parentNode.querySelector('button.active');
activeSibling.classList.remove('active');
buttonEl.classList.add('active');
timingProps.type = (property === 'type') ? type : timingProps.type;
timingProps.pathTimingFunction = (property === 'path') ? Vivus[type] : timingProps.pathTimingFunction;
timingProps.animTimingFunction = (property === 'anim') ? Vivus[type] : timingProps.animTimingFunction;
timing && timing.stop().destroy();
timing = new Vivus('timing-example', timingProps);
}
var hi = new Vivus('hi-there', {type: 'scenario-sync', duration: 20, start: 'autostart', dashGap: 20, forceRender: false},
function () {
if (window.console) {
console.log('Animation finished. [log triggered from callback]');
}
}),
obt1 = new Vivus('obturateur1', {type: 'delayed', duration: 150}),
obt2 = new Vivus('obturateur2', {type: 'sync', duration: 150}),
obt3 = new Vivus('obturateur3', {type: 'oneByOne', duration: 150}),
pola = new Vivus('polaroid', {type: 'scenario-sync', duration: 20, forceRender: false});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
{
"name": "vivus",
"version": "0.4.6",
"description": "JavaScript library to make drawing animation on SVG",
"main": "dist/vivus.js",
"scripts": {
"test": "karma start test/karma.conf.js",
"serve": "python -m SimpleHTTPServer 8844",
"lint": "./node_modules/eslint/bin/eslint.js src test",
"build": "npm run build-raw && npm run build-min",
"build-raw": "node src/_build.js > dist/vivus.js",
"build-min": "uglifyjs dist/vivus.js -o dist/vivus.min.js -c -m --source-map"
},
"repository": {
"type": "git",
"url": "https://github.com/maxwellito/vivus.git"
},
"author": "maxwellito",
"license": "MIT",
"bugs": {
"url": "https://github.com/maxwellito/vivus/issues"
},
"homepage": "https://github.com/maxwellito/vivus",
"engine": {
"node": ">=0.10.22"
},
"devDependencies": {
"eslint": "^5.15.3",
"karma": "^4.0.1",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "^1.1.2",
"karma-jasmine": "^2.0.1",
"uglify-js": "^3.5.2"
}
}

View File

@ -0,0 +1,352 @@
# vivus.js
Demo available on http://maxwellito.github.io/vivus
Play with it on [Vivus Instant](https://maxwellito.github.io/vivus-instant/)
Vivus is a lightweight JavaScript class (with no dependencies) that allows you to animate SVGs, giving them the appearance of being drawn. There are a variety of different animations available, as well as the option to create a custom script to draw your SVG in whatever way you like.
Available via:
- [NPM](https://www.npmjs.com/package/vivus): `npm install vivus`
- [Bower](http://bower.io/): `bower install vivus`
- [jsDelivr CDN](http://www.jsdelivr.com/#!vivus): `//cdn.jsdelivr.net/npm/vivus@latest/dist/vivus.min.js`
- [CDNJS CDN](https://cdnjs.com/libraries/vivus)
- [WebJars](http://www.webjars.org/)
Join the conversation on [Gitter](https://gitter.im/maxwellito/vivus)
Try Vivus with your SVG on [Vivus Instant](https://maxwellito.github.io/vivus-instant/). If you plan to use the library to animate a single SVG without callback or controls, this will allow you to download your animated SVG, powered by CSS, JavaScript free.
## Animations
On the following images, the pink color represents the `duration` value, and the blue one is for `delay` value.
### Delayed
![Timeline for delayed animation](https://raw.github.com/maxwellito/vivus/master/assets/delayed.png)
Every path element is drawn at the same time with a small delay at the start. This is currently the default animation.
### Sync
![Timeline for sync animation](https://raw.github.com/maxwellito/vivus/master/assets/sync.png)
Each line is drawn synchronously. They all start and finish at the same time, hence the name `sync`.
### OneByOne
![Timeline for oneByOne animation](https://raw.github.com/maxwellito/vivus/master/assets/oneByOne.png)
Each path element is drawn one after the other. This animation gives the best impression of live drawing. The duration for each line depends on their length to make a constant drawing speed.
## Principles
To get this effect, the script uses the CSS property `strokeDashoffset`. This property manages the stroke offset on every line of the SVG. Now, all we have to do is add some JavaScript to update this value progressively and the magic begins.
However, there's a problem with this. The `strokeDashoffset` property is only available on the path elements. This is an issue because in an SVG there are a lot of elements such as `circle`, `rect`, `line` and `polyline` which will break the animation. So to fix this, there is another class available in the repo called `pathformer`. It's made for transforming all objects of your SVG into `path` elements to be able to use `strokeDashoffset` and animate your SVGs.
_The animation always draws elements in the same order as they are defined in the SVG tag._
There are few conditions that your SVG must meet:
- All elements must have a stroke property and cannot be filled. This is because the animation only looks to progressively draw strokes and will not check for filled colours. For example: fill: "none"; stroke: "#FFF";
- You should avoid creating any hidden path elements in your SVG. Vivus considers them all eligible to be animated, so it is advised to remove them before playing with it. If they are not removed the animation might not achieve the desired effect, with blank areas and gaps appearing.
- `text` elements aren't allowed, they cannot be transformed into `path` elements. See [#22](https://github.com/maxwellito/vivus/issues/22) for more details.
The code is inspired from other repositories. The drawer is inspired from the excellent [Codrops](http://tympanus.net/codrops/) about the post [SVG Drawing Animation](http://tympanus.net/codrops/2013/12/30/svg-drawing-animation/) (if you don't know this website, get ready to have your mind blown). Then for the pathformer, there is a lot of work from [SVGPathConverter](https://github.com/Waest/SVGPathConverter) by [Waest](https://github.com/Waest).
## Usage
As I said, no dependencies here. All you need to do is include the scripts.
**Inline SVG**
```html
<svg id="my-svg">
<path...>
<path...>
<path...>
</svg>
<script>
new Vivus('my-svg', {duration: 200}, myCallback);
</script>
```
**Dynamic load**
```html
<object id="my-svg" type="image/svg+xml" data="link/to/my.svg"></object>
<script>
new Vivus('my-svg', { duration: 200 }, myCallback);
</script>
```
or
```html
<div id="my-div"></div>
<script>
new Vivus('my-div', { duration: 200, file: 'link/to/my.svg' }, myCallback);
</script>
```
By default the `object` created will take the size of the parent element, this one must have a height and width or your SVG might not appear.
If you need to edit this object, it is accessible in the `onReady` callback:
```js
new Vivus('my-div-id', {
file: 'link/to/my.svg',
onReady: function (myVivus) {
// `el` property is the SVG element
myVivus.el.setAttribute('height', 'auto');
}
});
```
Check out the [hacks page](https://github.com/maxwellito/vivus/blob/master/hacks.md) for more tricks.
### Constructor
The Vivus constructor asks for 3 parameters:
- ID (or object) of DOM element to interact with.<br/>It can be an inline SVG or a wrapper element to append an object tag from the option `file`
- Option object (described in the following |
- Callback to call at the end of the animation (optional)
### Option list
| Name | Type | Description |
| -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | string | Defines what kind of animation will be used: `delayed`, `sync`, `oneByOne`, `script`, `scenario` or `scenario-sync`. [Default: `delayed`] |
| `file` | string | Link to the SVG to animate. If set, Vivus will create an object tag and append it to the DOM element given to the constructor. Be careful, use the `onReady` callback before playing with the Vivus instance. |
| `start` | string | Defines how to trigger the animation (`inViewport` once the SVG is in the viewport, `manual` gives you the freedom to call draw method to start, `autostart` makes it start right now). [Default: `inViewport`] |
| `duration` | integer | Animation duration, in frames. [Default: `200`] |
| `delay` | integer | Time between the drawing of first and last path, in frames (only for `delayed` animations). |
| `onReady` | function | Function called when the instance is ready to play. |
| `pathTimingFunction` | function | Timing animation function for each path element of the SVG. Check the [timing function part](#timing-function). |
| `animTimingFunction` | function | Timing animation function for the complete SVG. Check the [timing function part](#timing-function). |
| `dashGap` | integer | Whitespace extra margin between dashes. Increase it in case of glitches at the initial state of the animation. [Default: `2`] |
| `forceRender` | boolean | Force the browser to re-render all updated path items. By default, the value is `true` on IE only. (check the 'troubleshoot' section for more details). |
| `reverseStack` | boolean | Reverse the order of execution. The default behaviour is to render from the first 'path' in the SVG to the last one. This option allow you to reverse the order. [Default: `false`] |
| `selfDestroy` | boolean | Removes all extra styling on the SVG, and leaves it as original. |
### Methods
| Name | Description |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `play(speed, callback)` | Plays the animation with the speed given in parameter. This value can be negative to go backward, between 0 and 1 to go slowly, >1 to go faster, or <0 to go in reverse from current state. [Default: `1`]. Callback executed after the animation is finished (optional) |
| `stop()` | Stops the animation. |
| `reset()` | Reinitialises the SVG to the original state: undrawn. |
| `finish()` | Set the SVG to the final state: drawn. |
| `setFrameProgress(progress)` | Set the progress of the animation. Progress must be a number between 0 and 1. |
| `getStatus()` | Get the status of the animation between `start`, `progress`, `end` |
| `destroy()` | Reset the SVG but make the instance out of order. |
These methods return the object so you can chain the actions.
```js
const myVivus = new Vivus('my-svg-id');
myVivus.stop().reset().play(2);
```
#### Play method callback
Instead of using the global constructor callback when you create the Vivus object, you can add callbacks to be
executed for specific `play` method calls.
```js
const myVivus = new Vivus('my-svg-id');
myVivus.play(1, function () {
// called after the animation completes
});
// alternativly if you leave the speed param blank and use the default, you
// can pass the callback as the first parameter like so.
myVivus.play(function () {
// called after the animation completes
});
```
## Timing function
To give more freedom, it's possible to override the animation of each path and/or the entire SVG. It works a bit like the CSS animation timing function. But instead of using a cubic-bezier function, it use a simple JavaScript function. It must accept a number as parameter (between 0 to 1), then return a number (also between 0 and 1). It's a hook.
If you don't want to create your own, timing methods are available via the constructor object: `EASE`, `EASE_IN`, `EASE_OUT` and `EASE_OUT_BOUNCE`. Then set it in the option object to enjoy them.
```js
// Here, the ease animation will be use for the global drawing.
new Vivus(
'my-svg-id',
{
type: 'delayed',
duration: 200,
animTimingFunction: Vivus.EASE
},
myCallback
);
```
**WARNING**: `animTimingFunction` is called at every frame of the animation, and `pathTimingFunction` is also called at every frame for each path of your SVG. So be careful about them. Keep it simple, or it can affect the performance.
## Extra attributes
The attribute `data-ignore` allows you to ignore path tags from the vivus animation.
```html
<svg id="my-svg">
<path...>
<path data-ignore="true" ...>
<path...>
</svg>
```
In this case, the second path won't be part of the animation.
## Scenarize
This feature allows you to script the animation of your SVG. For this, the custom values will be set directly in the DOM of the SVG.
### `scenario`
This type is easier to understand, but longer to implement. You just have to define the start and duration of each element with `data-start` and `data-duration` attributes. If it is missing, it will use the default value given to the constructor.
The best part of this type is the flexibility it provides. You don't have to respect the order/stack of the SVG and you can start with the last element, then continue with the first to finish with all the rest at the same time.
You will then have to define custom rules for each element in your SVG via extra attributes in your SVG DOM :
- `data-start` (integer)
time when the animation must start, in frames
- `data-duration` (integer)
animation duration of this path, in frames
```html
<svg>
<path data-start="0" data-duration="10" ... />
<path data-start="20" data-duration="10" ... />
<path data-start="20" data-duration="20" ... />
<path data-start="0" data-duration="30" ... />
</svg>
```
### `scenario-sync`
It's not the sexiest code ever, but it's quite flexible. In addition to this, the behaviour is fairly different.
By using this animation type, the default behaviour is the same as `oneByOne`. However, you can define some properties on a specific path item such as the duration, the delay to start (from the end of the previous path) and if it should be played synchronously.
- `data-delay` (integer)
time between the end of the animation of the previous path and the start of the current path, in frames
- `data-duration` (integer)
duration of this path animation, in frames
- `data-async` (no value required)
make the drawing of this path asynchronous. It means the next path will start at the same time.
If a path does not have an attribute for duration or delay then the default values, set in the options, will be used.
Example: here is a simple SVG containing 5 elements. With the following options `{duration: 20, delay: 0}`, we should get this timeline
![Timeline for script animation by default](https://raw.github.com/maxwellito/vivus/master/assets/script_default.png)
This looks like 'oneByOne' animation, synchronous mode. But to make it a bit custom, here is what I can do:
```html
<svg>
<path data-duration="10" ... />
<path data-delay="10" data-async ... />
<path data-delay="15" ... />
<path data-duration="10" data-delay="45" data-async ... />
<path data-duration="50" data-delay="5" ... />
</svg>
```
This scenario should give us
![Timeline for this custom script animation](https://raw.github.com/maxwellito/vivus/master/assets/script_custom.png)
I'm sorry if it does not look very sexy, and it's not really easy to use. I'm happy to make any changes, as long as the idea sounds interesting. Post an issue and I'll be very happy to talk about it!
## Non Scaling
Some SVG elements might use non scaling properties such as `vector-effect="non-scaling-stroke"`, which requires some additional custom logic. On instance construction Vivus will map all the child elements in the SVG and calculate their line length. If the element is resized during the animation, the calculated stroke style properties become invalid and the SVG will display incorrectly.
To keep animation consistency, the method `recalc` should be called when the SVG is resized. It will re-calculate the line length on affected child elements on the next frame calculation.
Code example:
```js
// Create your Vivus instance
const vivusObject = new Vivus('my-div', {
duration: 200,
file: 'link/to/my.svg',
});
// Create your observer and set up a callback on resize
const resizeObserver = new ResizeObserver((entries) => {
// Recalculate the line lengths
vivusObject.recalc();
});
resizeObserver.observe(vivusObject.el);
```
Vivus will provide a warning in the console when it detects stroke scaling.
## Development
To make it simpler a gulp file is set up to automise minifying, JShint and tests.
If you have never used Gulp before this is a good opportunity. To use it, you need to install NodeJS first then run `sudo npm install -g gulp`.
To start, you will need to install the repo dependencies:
```bash
$ npm install
```
Then you can use NPM scripts to run the following tasks:
- `build` make the build (generate `dist/vivus.js` and `dist/vivus.min.js`)
- `lint` run ESlint on the source files
- `test` run Karma
## Troubleshoot
### Internet Explorer
Some SVG weren't working at all. The only solution found was to clone and replace each updated path element. Of course this solution requires more resources and a lot of DOM manipulation, but it will give a smooth animation like other browsers. This fallback is only applied on Internet Explorer (all versions), and can be disabled via the option `forceRender`.
Replacing each updated path by a clone was the only way to force IE to re-render the SVG. On some SVGs this trick is not necessary, but IE can be a bit tricky with this. If you're worried about performance, I would recommend checking if your SVG works correctly by disabling the `forceRender` option. If it works correctly on IE, then keep it like this.
By default, `forceRender` is `true` on Internet Explorer only.
### Firefox
For Firefox users, you might encounter some glitches depending on your SVG and browser version. On versions before 36, there is a problem retrieving path length via `getTotalLength` method. Returning 174321516544 instead of 209 (I'm not exaggerating, this comes from a real case), messing up the entire animation treatment. Unfortunately, there's nothing that this library can do, this is due to Firefox. I hope to find a workaround, but at the moment I can only recommend that you test your animation on previous versions of Firefox.
## Debug
For an easier debug have a look to the attribute `map` of your Vivus object. This contains the mapping of your animation. If you're using a modern browser, I recommend `console.table` to get a nice output of the array which will make your debug easier.
```javascript
const logo = new Vivus('myLogo', { type: 'scenario-sync' });
// The property 'map' contain all the SVG mapping
console.table(logo.map);
```
## Special thanks!
Thanks to all contributors! Also users who pushed me to improve the library by publishing it on NPM, or browser compatibility or features. Also thanks for fixing my awful english :)
- [@jolic](https://github.com/jolic) for dynamic SVG loading, ignore invisible paths, infinite and beyond...
- [@BenMcGeachy](https://github.com/BenMcGeachy) for making the documentation understandable
- [@TranscendOfSypherus](https://github.com/TranscendOfSypherus) for fixing the PathFormer
- [@flyingfisch](https://github.com/flyingfisch) for general helping with issues
- [@morgangiraud](https://github.com/morgangiraud) on the ignore invisible paths
- [@Nerdissimo](https://github.com/Nerdissimo) for inserting SVG without `object` wrapper
- [@jsimnz](https://github.com/jsimnz) for adding callbacks to play method
and many others...

View File

@ -0,0 +1,36 @@
var fs = require('fs');
var pkg = require('../package.json');
var vivus = fs.readFileSync('src/vivus.js', { encoding: 'utf8' });
var pathformer = fs.readFileSync('src/pathformer.js', { encoding: 'utf8' });
var output = `/**
* ${pkg.name} - ${pkg.description}
* @version v${pkg.version}
* @link ${pkg.homepage}
* @license ${pkg.license}
*/
(function () {
${pathformer}
${vivus}
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], function() {
return Vivus;
});
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = Vivus;
} else {
// Browser globals
window.Vivus = Vivus;
}
}());
`;
console.log(output);

View File

@ -0,0 +1,276 @@
'use strict';
/**
* Pathformer
* Beta version
*
* Take any SVG version 1.1 and transform
* child elements to 'path' elements
*
* This code is purely forked from
* https://github.com/Waest/SVGPathConverter
*/
/**
* Class constructor
*
* @param {DOM|String} element Dom element of the SVG or id of it
*/
function Pathformer(element) {
// Test params
if (typeof element === 'undefined') {
throw new Error('Pathformer [constructor]: "element" parameter is required');
}
// Set the element
if (element.constructor === String) {
element = document.getElementById(element);
if (!element) {
throw new Error('Pathformer [constructor]: "element" parameter is not related to an existing ID');
}
}
if (element instanceof window.SVGElement ||
element instanceof window.SVGGElement ||
/^svg$/i.test(element.nodeName)) {
this.el = element;
} else {
throw new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement');
}
// Start
this.scan(element);
}
/**
* List of tags which can be transformed
* to path elements
*
* @type {Array}
*/
Pathformer.prototype.TYPES = ['line', 'ellipse', 'circle', 'polygon', 'polyline', 'rect'];
/**
* List of attribute names which contain
* data. This array list them to check if
* they contain bad values, like percentage.
*
* @type {Array}
*/
Pathformer.prototype.ATTR_WATCH = ['cx', 'cy', 'points', 'r', 'rx', 'ry', 'x', 'x1', 'x2', 'y', 'y1', 'y2'];
/**
* Finds the elements compatible for transform
* and apply the liked method
*
* @param {object} options Object from the constructor
*/
Pathformer.prototype.scan = function (svg) {
var fn, element, pathData, pathDom,
elements = svg.querySelectorAll(this.TYPES.join(','));
for (var i = 0; i < elements.length; i++) {
element = elements[i];
fn = this[element.tagName.toLowerCase() + 'ToPath'];
pathData = fn(this.parseAttr(element.attributes));
pathDom = this.pathMaker(element, pathData);
element.parentNode.replaceChild(pathDom, element);
}
};
/**
* Read `line` element to extract and transform
* data, to make it ready for a `path` object.
*
* @param {DOMelement} element Line element to transform
* @return {object} Data for a `path` element
*/
Pathformer.prototype.lineToPath = function (element) {
var newElement = {},
x1 = element.x1 || 0,
y1 = element.y1 || 0,
x2 = element.x2 || 0,
y2 = element.y2 || 0;
newElement.d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2;
return newElement;
};
/**
* Read `rect` element to extract and transform
* data, to make it ready for a `path` object.
* The radius-border is not taken in charge yet.
* (your help is more than welcomed)
*
* @param {DOMelement} element Rect element to transform
* @return {object} Data for a `path` element
*/
Pathformer.prototype.rectToPath = function (element) {
var newElement = {},
x = parseFloat(element.x) || 0,
y = parseFloat(element.y) || 0,
width = parseFloat(element.width) || 0,
height = parseFloat(element.height) || 0;
if (element.rx || element.ry) {
var rx = parseInt(element.rx, 10) || -1,
ry = parseInt(element.ry, 10) || -1;
rx = Math.min(Math.max(rx < 0 ? ry : rx, 0), width/2);
ry = Math.min(Math.max(ry < 0 ? rx : ry, 0), height/2);
newElement.d = 'M ' + (x + rx) + ',' + y + ' ' +
'L ' + (x + width - rx) + ',' + y + ' ' +
'A ' + rx + ',' + ry + ',0,0,1,' + (x + width) + ',' + (y + ry) + ' ' +
'L ' + (x + width) + ',' + (y + height - ry) + ' ' +
'A ' + rx + ',' + ry + ',0,0,1,' + (x + width - rx) + ',' + (y + height) + ' ' +
'L ' + (x + rx) + ',' + (y + height) + ' ' +
'A ' + rx + ',' + ry + ',0,0,1,' + x + ',' + (y + height - ry) + ' ' +
'L ' + x + ',' + (y + ry) + ' ' +
'A ' + rx + ',' + ry + ',0,0,1,' + (x + rx) + ',' + y;
}
else {
newElement.d = 'M' + x + ' ' + y + ' ' +
'L' + (x + width) + ' ' + y + ' ' +
'L' + (x + width) + ' ' + (y + height) + ' ' +
'L' + x + ' ' + (y + height) + ' Z';
}
return newElement;
};
/**
* Read `polyline` element to extract and transform
* data, to make it ready for a `path` object.
*
* @param {DOMelement} element Polyline element to transform
* @return {object} Data for a `path` element
*/
Pathformer.prototype.polylineToPath = function (element) {
var newElement = {},
points = element.points.trim().split(' '),
i, path;
// Reformatting if points are defined without commas
if (element.points.indexOf(',') === -1) {
var formattedPoints = [];
for (i = 0; i < points.length; i+=2) {
formattedPoints.push(points[i] + ',' + points[i+1]);
}
points = formattedPoints;
}
// Generate the path.d value
path = 'M' + points[0];
for(i = 1; i < points.length; i++) {
if (points[i].indexOf(',') !== -1) {
path += 'L' + points[i];
}
}
newElement.d = path;
return newElement;
};
/**
* Read `polygon` element to extract and transform
* data, to make it ready for a `path` object.
* This method rely on polylineToPath, because the
* logic is similar. The path created is just closed,
* so it needs an 'Z' at the end.
*
* @param {DOMelement} element Polygon element to transform
* @return {object} Data for a `path` element
*/
Pathformer.prototype.polygonToPath = function (element) {
var newElement = Pathformer.prototype.polylineToPath(element);
newElement.d += 'Z';
return newElement;
};
/**
* Read `ellipse` element to extract and transform
* data, to make it ready for a `path` object.
*
* @param {DOMelement} element ellipse element to transform
* @return {object} Data for a `path` element
*/
Pathformer.prototype.ellipseToPath = function (element) {
var newElement = {},
rx = parseFloat(element.rx) || 0,
ry = parseFloat(element.ry) || 0,
cx = parseFloat(element.cx) || 0,
cy = parseFloat(element.cy) || 0,
startX = cx - rx,
startY = cy,
endX = parseFloat(cx) + parseFloat(rx),
endY = cy;
newElement.d = 'M' + startX + ',' + startY +
'A' + rx + ',' + ry + ' 0,1,1 ' + endX + ',' + endY +
'A' + rx + ',' + ry + ' 0,1,1 ' + startX + ',' + endY;
return newElement;
};
/**
* Read `circle` element to extract and transform
* data, to make it ready for a `path` object.
*
* @param {DOMelement} element Circle element to transform
* @return {object} Data for a `path` element
*/
Pathformer.prototype.circleToPath = function (element) {
var newElement = {},
r = parseFloat(element.r) || 0,
cx = parseFloat(element.cx) || 0,
cy = parseFloat(element.cy) || 0,
startX = cx - r,
startY = cy,
endX = parseFloat(cx) + parseFloat(r),
endY = cy;
newElement.d = 'M' + startX + ',' + startY +
'A' + r + ',' + r + ' 0,1,1 ' + endX + ',' + endY +
'A' + r + ',' + r + ' 0,1,1 ' + startX + ',' + endY;
return newElement;
};
/**
* Create `path` elements form original element
* and prepared objects
*
* @param {DOMelement} element Original element to transform
* @param {object} pathData Path data (from `toPath` methods)
* @return {DOMelement} Path element
*/
Pathformer.prototype.pathMaker = function (element, pathData) {
var i, attr, pathTag = document.createElementNS('http://www.w3.org/2000/svg','path');
for(i = 0; i < element.attributes.length; i++) {
attr = element.attributes[i];
if (this.ATTR_WATCH.indexOf(attr.name) === -1) {
pathTag.setAttribute(attr.name, attr.value);
}
}
for(i in pathData) {
pathTag.setAttribute(i, pathData[i]);
}
return pathTag;
};
/**
* Parse attributes of a DOM element to
* get an object of attribute => value
*
* @param {NamedNodeMap} attributes Attributes object from DOM element to parse
* @return {object} Object of attributes
*/
Pathformer.prototype.parseAttr = function (element) {
var attr, output = {};
for (var i = 0; i < element.length; i++) {
attr = element[i];
// Check if no data attribute contains '%', or the transformation is impossible
if (this.ATTR_WATCH.indexOf(attr.name) !== -1 && attr.value.indexOf('%') !== -1) {
throw new Error('Pathformer [parseAttr]: a SVG shape got values in percentage. This cannot be transformed into \'path\' tags. Please use \'viewBox\'.');
}
output[attr.name] = attr.value;
}
return output;
};

View File

@ -0,0 +1,917 @@
'use strict';
var setupEnv, requestAnimFrame, cancelAnimFrame, parsePositiveInt;
/**
* Vivus
* Beta version
*
* Take any SVG and make the animation
* to give give the impression of live drawing
*
* This in more than just inspired from codrops
* At that point, it's a pure fork.
*/
/**
* Class constructor
* option structure
* type: 'delayed'|'sync'|'oneByOne'|'script' (to know if the items must be drawn synchronously or not, default: delayed)
* duration: <int> (in frames)
* start: 'inViewport'|'manual'|'autostart' (start automatically the animation, default: inViewport)
* delay: <int> (delay between the drawing of first and last path)
* dashGap <integer> whitespace extra margin between dashes
* pathTimingFunction <function> timing animation function for each path element of the SVG
* animTimingFunction <function> timing animation function for the complete SVG
* forceRender <boolean> force the browser to re-render all updated path items
* selfDestroy <boolean> removes all extra styling on the SVG, and leaves it as original
*
* The attribute 'type' is by default on 'delayed'.
* - 'delayed'
* all paths are draw at the same time but with a
* little delay between them before start
* - 'sync'
* all path are start and finish at the same time
* - 'oneByOne'
* only one path is draw at the time
* the end of the first one will trigger the draw
* of the next one
*
* All these values can be overwritten individually
* for each path item in the SVG
* The value of frames will always take the advantage of
* the duration value.
* If you fail somewhere, an error will be thrown.
* Good luck.
*
* @constructor
* @this {Vivus}
* @param {DOM|String} element Dom element of the SVG or id of it
* @param {Object} options Options about the animation
* @param {Function} callback Callback for the end of the animation
*/
function Vivus(element, options, callback) {
setupEnv();
// Setup
this.isReady = false;
this.setElement(element, options);
this.setOptions(options);
this.setCallback(callback);
if (this.isReady) {
this.init();
}
}
/**
* Timing functions
**************************************
*
* Default functions to help developers.
* It always take a number as parameter (between 0 to 1) then
* return a number (between 0 and 1)
*/
Vivus.LINEAR = function(x) {
return x;
};
Vivus.EASE = function(x) {
return -Math.cos(x * Math.PI) / 2 + 0.5;
};
Vivus.EASE_OUT = function(x) {
return 1 - Math.pow(1 - x, 3);
};
Vivus.EASE_IN = function(x) {
return Math.pow(x, 3);
};
Vivus.EASE_OUT_BOUNCE = function(x) {
var base = -Math.cos(x * (0.5 * Math.PI)) + 1,
rate = Math.pow(base, 1.5),
rateR = Math.pow(1 - x, 2),
progress = -Math.abs(Math.cos(rate * (2.5 * Math.PI))) + 1;
return 1 - rateR + progress * rateR;
};
/**
* Setters
**************************************
*/
/**
* Check and set the element in the instance
* The method will not return anything, but will throw an
* error if the parameter is invalid
*
* @param {DOM|String} element SVG Dom element or id of it
*/
Vivus.prototype.setElement = function(element, options) {
var onLoad, self;
// Basic check
if (typeof element === 'undefined') {
throw new Error('Vivus [constructor]: "element" parameter is required');
}
// Set the element
if (element.constructor === String) {
element = document.getElementById(element);
if (!element) {
throw new Error(
'Vivus [constructor]: "element" parameter is not related to an existing ID'
);
}
}
this.parentEl = element;
// Load the SVG with XMLHttpRequest and extract the SVG
if (options && options.file) {
self = this;
onLoad = function() {
var domSandbox = document.createElement('div');
domSandbox.innerHTML = this.responseText;
var svgTag = domSandbox.querySelector('svg');
if (!svgTag) {
throw new Error(
'Vivus [load]: Cannot find the SVG in the loaded file : ' +
options.file
);
}
self.el = svgTag;
self.el.setAttribute('width', '100%');
self.el.setAttribute('height', '100%');
self.parentEl.appendChild(self.el);
self.isReady = true;
self.init();
self = null;
};
var oReq = new window.XMLHttpRequest();
oReq.addEventListener('load', onLoad);
oReq.open('GET', options.file);
oReq.send();
return;
}
switch (element.constructor) {
case window.SVGSVGElement:
case window.SVGElement:
case window.SVGGElement:
this.el = element;
this.isReady = true;
break;
case window.HTMLObjectElement:
self = this;
onLoad = function(e) {
if (self.isReady) {
return;
}
self.el =
element.contentDocument &&
element.contentDocument.querySelector('svg');
if (!self.el && e) {
throw new Error(
'Vivus [constructor]: object loaded does not contain any SVG'
);
} else if (self.el) {
if (element.getAttribute('built-by-vivus')) {
self.parentEl.insertBefore(self.el, element);
self.parentEl.removeChild(element);
self.el.setAttribute('width', '100%');
self.el.setAttribute('height', '100%');
}
self.isReady = true;
self.init();
self = null;
}
};
if (!onLoad()) {
element.addEventListener('load', onLoad);
}
break;
default:
throw new Error(
'Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)'
);
}
};
/**
* Set up user option to the instance
* The method will not return anything, but will throw an
* error if the parameter is invalid
*
* @param {object} options Object from the constructor
*/
Vivus.prototype.setOptions = function(options) {
var allowedTypes = [
'delayed',
'sync',
'async',
'nsync',
'oneByOne',
'scenario',
'scenario-sync'
];
var allowedStarts = ['inViewport', 'manual', 'autostart'];
// Basic check
if (options !== undefined && options.constructor !== Object) {
throw new Error(
'Vivus [constructor]: "options" parameter must be an object'
);
} else {
options = options || {};
}
// Set the animation type
if (options.type && allowedTypes.indexOf(options.type) === -1) {
throw new Error(
'Vivus [constructor]: ' +
options.type +
' is not an existing animation `type`'
);
} else {
this.type = options.type || allowedTypes[0];
}
// Set the start type
if (options.start && allowedStarts.indexOf(options.start) === -1) {
throw new Error(
'Vivus [constructor]: ' +
options.start +
' is not an existing `start` option'
);
} else {
this.start = options.start || allowedStarts[0];
}
this.isIE =
window.navigator.userAgent.indexOf('MSIE') !== -1 ||
window.navigator.userAgent.indexOf('Trident/') !== -1 ||
window.navigator.userAgent.indexOf('Edge/') !== -1;
this.duration = parsePositiveInt(options.duration, 120);
this.delay = parsePositiveInt(options.delay, null);
this.dashGap = parsePositiveInt(options.dashGap, 1);
this.forceRender = options.hasOwnProperty('forceRender')
? !!options.forceRender
: this.isIE;
this.reverseStack = !!options.reverseStack;
this.selfDestroy = !!options.selfDestroy;
this.onReady = options.onReady;
this.map = [];
this.frameLength = this.currentFrame = this.delayUnit = this.speed = this.handle = null;
this.ignoreInvisible = options.hasOwnProperty('ignoreInvisible')
? !!options.ignoreInvisible
: false;
this.animTimingFunction = options.animTimingFunction || Vivus.LINEAR;
this.pathTimingFunction = options.pathTimingFunction || Vivus.LINEAR;
if (this.delay >= this.duration) {
throw new Error('Vivus [constructor]: delay must be shorter than duration');
}
};
/**
* Set up callback to the instance
* The method will not return enything, but will throw an
* error if the parameter is invalid
*
* @param {Function} callback Callback for the animation end
*/
Vivus.prototype.setCallback = function(callback) {
// Basic check
if (!!callback && callback.constructor !== Function) {
throw new Error(
'Vivus [constructor]: "callback" parameter must be a function'
);
}
this.callback = callback || function() {};
};
/**
* Core
**************************************
*/
/**
* Map the svg, path by path.
* The method return nothing, it just fill the
* `map` array. Each item in this array represent
* a path element from the SVG, with informations for
* the animation.
*
* ```
* [
* {
* el: <DOMobj> the path element
* length: <number> length of the path line
* startAt: <number> time start of the path animation (in frames)
* duration: <number> path animation duration (in frames)
* },
* ...
* ]
* ```
*
*/
Vivus.prototype.mapping = function() {
var i, paths, path, pAttrs, pathObj, totalLength, lengthMeter, timePoint, scale, hasNonScale;
timePoint = totalLength = lengthMeter = 0;
paths = this.el.querySelectorAll('path');
hasNonScale = false;
for (i = 0; i < paths.length; i++) {
path = paths[i];
if (this.isInvisible(path)) {
continue;
}
pathObj = {
el: path,
length: 0,
startAt: 0,
duration: 0,
isResizeSensitive: false
};
// If vector effect is non-scaling-stroke, the total length won't match the rendered length
// so we need to calculate the scale and apply it
if (path.getAttribute('vector-effect') === 'non-scaling-stroke') {
var rect = path.getBoundingClientRect();
var box = path.getBBox();
scale = Math.max(rect.width / box.width, rect.height / box.height);
pathObj.isResizeSensitive = true;
hasNonScale = true;
} else {
scale = 1;
}
pathObj.length = Math.ceil(path.getTotalLength() * scale);
// Test if the path length is correct
if (isNaN(pathObj.length)) {
if (window.console && console.warn) {
console.warn(
'Vivus [mapping]: cannot retrieve a path element length',
path
);
}
continue;
}
this.map.push(pathObj);
path.style.strokeDasharray =
pathObj.length + ' ' + (pathObj.length + this.dashGap * 2);
path.style.strokeDashoffset = pathObj.length + this.dashGap;
pathObj.length += this.dashGap;
totalLength += pathObj.length;
this.renderPath(i);
}
// Show a warning for non-scaling elements
if (hasNonScale) {
console.warn('Vivus: this SVG contains non-scaling-strokes. You should call instance.recalc() when the SVG is resized or you will encounter unwanted behaviour. See https://github.com/maxwellito/vivus#non-scaling for more info.');
}
totalLength = totalLength === 0 ? 1 : totalLength;
this.delay = this.delay === null ? this.duration / 3 : this.delay;
this.delayUnit = this.delay / (paths.length > 1 ? paths.length - 1 : 1);
// Reverse stack if asked
if (this.reverseStack) {
this.map.reverse();
}
for (i = 0; i < this.map.length; i++) {
pathObj = this.map[i];
switch (this.type) {
case 'delayed':
pathObj.startAt = this.delayUnit * i;
pathObj.duration = this.duration - this.delay;
break;
case 'oneByOne':
pathObj.startAt = (lengthMeter / totalLength) * this.duration;
pathObj.duration = (pathObj.length / totalLength) * this.duration;
break;
case 'sync':
case 'async':
case 'nsync':
pathObj.startAt = 0;
pathObj.duration = this.duration;
break;
case 'scenario-sync':
path = pathObj.el;
pAttrs = this.parseAttr(path);
pathObj.startAt =
timePoint +
(parsePositiveInt(pAttrs['data-delay'], this.delayUnit) || 0);
pathObj.duration = parsePositiveInt(
pAttrs['data-duration'],
this.duration
);
timePoint =
pAttrs['data-async'] !== undefined
? pathObj.startAt
: pathObj.startAt + pathObj.duration;
this.frameLength = Math.max(
this.frameLength,
pathObj.startAt + pathObj.duration
);
break;
case 'scenario':
path = pathObj.el;
pAttrs = this.parseAttr(path);
pathObj.startAt =
parsePositiveInt(pAttrs['data-start'], this.delayUnit) || 0;
pathObj.duration = parsePositiveInt(
pAttrs['data-duration'],
this.duration
);
this.frameLength = Math.max(
this.frameLength,
pathObj.startAt + pathObj.duration
);
break;
}
lengthMeter += pathObj.length;
this.frameLength = this.frameLength || this.duration;
}
};
/**
* Public method to re-evaluate line length for non-scaling lines
* path elements.
*/
Vivus.prototype.recalc = function () {
if (this.mustRecalcScale) {
return;
}
this.mustRecalcScale = requestAnimFrame(function () {
this.performLineRecalc();
}.bind(this));
}
/**
* Private method to re-evaluate line length on non-scaling
* path elements. Then call for a trace to update the SVG.
*/
Vivus.prototype.performLineRecalc = function () {
var pathObj, path, rect, box, scale;
for (var i = 0; i < this.map.length; i++) {
pathObj = this.map[i];
if (pathObj.isResizeSensitive) {
path = pathObj.el;
rect = path.getBoundingClientRect();
box = path.getBBox();
scale = Math.max(rect.width / box.width, rect.height / box.height);
pathObj.length = Math.ceil(path.getTotalLength() * scale);
path.style.strokeDasharray = pathObj.length + ' ' + (pathObj.length + this.dashGap * 2);
}
}
this.trace();
this.mustRecalcScale = null;
}
/**
* Interval method to draw the SVG from current
* position of the animation. It update the value of
* `currentFrame` and re-trace the SVG.
*
* It use this.handle to store the requestAnimationFrame
* and clear it one the animation is stopped. So this
* attribute can be used to know if the animation is
* playing.
*
* Once the animation at the end, this method will
* trigger the Vivus callback.
*
*/
Vivus.prototype.draw = function() {
var self = this;
this.currentFrame += this.speed;
if (this.currentFrame <= 0) {
this.stop();
this.reset();
} else if (this.currentFrame >= this.frameLength) {
this.stop();
this.currentFrame = this.frameLength;
this.trace();
if (this.selfDestroy) {
this.destroy();
}
} else {
this.trace();
this.handle = requestAnimFrame(function() {
self.draw();
});
return;
}
this.callback(this);
if (this.instanceCallback) {
this.instanceCallback(this);
this.instanceCallback = null;
}
};
/**
* Draw the SVG at the current instant from the
* `currentFrame` value. Here is where most of the magic is.
* The trick is to use the `strokeDashoffset` style property.
*
* For optimisation reasons, a new property called `progress`
* is added in each item of `map`. This one contain the current
* progress of the path element. Only if the new value is different
* the new value will be applied to the DOM element. This
* method save a lot of resources to re-render the SVG. And could
* be improved if the animation couldn't be played forward.
*
*/
Vivus.prototype.trace = function() {
var i, progress, path, currentFrame;
currentFrame =
this.animTimingFunction(this.currentFrame / this.frameLength) *
this.frameLength;
for (i = 0; i < this.map.length; i++) {
path = this.map[i];
progress = (currentFrame - path.startAt) / path.duration;
progress = this.pathTimingFunction(Math.max(0, Math.min(1, progress)));
if (path.progress !== progress) {
path.progress = progress;
path.el.style.strokeDashoffset = Math.floor(path.length * (1 - progress));
this.renderPath(i);
}
}
};
/**
* Method forcing the browser to re-render a path element
* from it's index in the map. Depending on the `forceRender`
* value.
* The trick is to replace the path element by it's clone.
* This practice is not recommended because it's asking more
* ressources, too much DOM manupulation..
* but it's the only way to let the magic happen on IE.
* By default, this fallback is only applied on IE.
*
* @param {Number} index Path index
*/
Vivus.prototype.renderPath = function(index) {
if (this.forceRender && this.map && this.map[index]) {
var pathObj = this.map[index],
newPath = pathObj.el.cloneNode(true);
pathObj.el.parentNode.replaceChild(newPath, pathObj.el);
pathObj.el = newPath;
}
};
/**
* When the SVG object is loaded and ready,
* this method will continue the initialisation.
*
* This this mainly due to the case of passing an
* object tag in the constructor. It will wait
* the end of the loading to initialise.
*
*/
Vivus.prototype.init = function() {
// Set object variables
this.frameLength = 0;
this.currentFrame = 0;
this.map = [];
// Start
new Pathformer(this.el);
this.mapping();
this.starter();
if (this.onReady) {
this.onReady(this);
}
};
/**
* Trigger to start of the animation.
* Depending on the `start` value, a different script
* will be applied.
*
* If the `start` value is not valid, an error will be thrown.
* Even if technically, this is impossible.
*
*/
Vivus.prototype.starter = function() {
switch (this.start) {
case 'manual':
return;
case 'autostart':
this.play();
break;
case 'inViewport':
var self = this,
listener = function() {
if (self.isInViewport(self.parentEl, 1)) {
self.play();
window.removeEventListener('scroll', listener);
}
};
window.addEventListener('scroll', listener);
listener();
break;
}
};
/**
* Controls
**************************************
*/
/**
* Get the current status of the animation between
* three different states: 'start', 'progress', 'end'.
* @return {string} Instance status
*/
Vivus.prototype.getStatus = function() {
return this.currentFrame === 0
? 'start'
: this.currentFrame === this.frameLength
? 'end'
: 'progress';
};
/**
* Reset the instance to the initial state : undraw
* Be careful, it just reset the animation, if you're
* playing the animation, this won't stop it. But just
* make it start from start.
*
*/
Vivus.prototype.reset = function() {
return this.setFrameProgress(0);
};
/**
* Set the instance to the final state : drawn
* Be careful, it just set the animation, if you're
* playing the animation on rewind, this won't stop it.
* But just make it start from the end.
*
*/
Vivus.prototype.finish = function() {
return this.setFrameProgress(1);
};
/**
* Set the level of progress of the drawing.
*
* @param {number} progress Level of progress to set
*/
Vivus.prototype.setFrameProgress = function(progress) {
progress = Math.min(1, Math.max(0, progress));
this.currentFrame = Math.round(this.frameLength * progress);
this.trace();
return this;
};
/**
* Play the animation at the desired speed.
* Speed must be a valid number (no zero).
* By default, the speed value is 1.
* But a negative value is accepted to go forward.
*
* And works with float too.
* But don't forget we are in JavaScript, se be nice
* with him and give him a 1/2^x value.
*
* @param {number} speed Animation speed [optional]
*/
Vivus.prototype.play = function(speed, callback) {
this.instanceCallback = null;
if (speed && typeof speed === 'function') {
this.instanceCallback = speed; // first parameter is actually the callback function
speed = null;
} else if (speed && typeof speed !== 'number') {
throw new Error('Vivus [play]: invalid speed');
}
// if the first parameter wasn't the callback, check if the seconds was
if (callback && typeof callback === 'function' && !this.instanceCallback) {
this.instanceCallback = callback;
}
this.speed = speed || 1;
if (!this.handle) {
this.draw();
}
return this;
};
/**
* Stop the current animation, if on progress.
* Should not trigger any error.
*
*/
Vivus.prototype.stop = function() {
if (this.handle) {
cancelAnimFrame(this.handle);
this.handle = null;
}
return this;
};
/**
* Destroy the instance.
* Remove all bad styling attributes on all
* path tags
*
*/
Vivus.prototype.destroy = function() {
this.stop();
var i, path;
for (i = 0; i < this.map.length; i++) {
path = this.map[i];
path.el.style.strokeDashoffset = null;
path.el.style.strokeDasharray = null;
this.renderPath(i);
}
};
/**
* Utils methods
* include methods from Codrops
**************************************
*/
/**
* Method to best guess if a path should added into
* the animation or not.
*
* 1. Use the `data-vivus-ignore` attribute if set
* 2. Check if the instance must ignore invisible paths
* 3. Check if the path is visible
*
* For now the visibility checking is unstable.
* It will be used for a beta phase.
*
* Other improvments are planned. Like detecting
* is the path got a stroke or a valid opacity.
*/
Vivus.prototype.isInvisible = function(el) {
var rect,
ignoreAttr = el.getAttribute('data-ignore');
if (ignoreAttr !== null) {
return ignoreAttr !== 'false';
}
if (this.ignoreInvisible) {
rect = el.getBoundingClientRect();
return !rect.width && !rect.height;
} else {
return false;
}
};
/**
* Parse attributes of a DOM element to
* get an object of {attributeName => attributeValue}
*
* @param {object} element DOM element to parse
* @return {object} Object of attributes
*/
Vivus.prototype.parseAttr = function(element) {
var attr,
output = {};
if (element && element.attributes) {
for (var i = 0; i < element.attributes.length; i++) {
attr = element.attributes[i];
output[attr.name] = attr.value;
}
}
return output;
};
/**
* Reply if an element is in the page viewport
*
* @param {object} el Element to observe
* @param {number} h Percentage of height
* @return {boolean}
*/
Vivus.prototype.isInViewport = function(el, h) {
var scrolled = this.scrollY(),
viewed = scrolled + this.getViewportH(),
elBCR = el.getBoundingClientRect(),
elHeight = elBCR.height,
elTop = scrolled + elBCR.top,
elBottom = elTop + elHeight;
// if 0, the element is considered in the viewport as soon as it enters.
// if 1, the element is considered in the viewport only when it's fully inside
// value in percentage (1 >= h >= 0)
h = h || 0;
return elTop + elHeight * h <= viewed && elBottom >= scrolled;
};
/**
* Get the viewport height in pixels
*
* @return {integer} Viewport height
*/
Vivus.prototype.getViewportH = function() {
var client = this.docElem.clientHeight,
inner = window.innerHeight;
if (client < inner) {
return inner;
} else {
return client;
}
};
/**
* Get the page Y offset
*
* @return {integer} Page Y offset
*/
Vivus.prototype.scrollY = function() {
return window.pageYOffset || this.docElem.scrollTop;
};
setupEnv = function() {
if (Vivus.prototype.docElem) {
return;
}
/**
* Alias for document element
*
* @type {DOMelement}
*/
Vivus.prototype.docElem = window.document.documentElement;
/**
* Alias for `requestAnimationFrame` or
* `setTimeout` function for deprecated browsers.
*
*/
requestAnimFrame = (function() {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(/* function */ callback) {
return window.setTimeout(callback, 1000 / 60);
}
);
})();
/**
* Alias for `cancelAnimationFrame` or
* `cancelTimeout` function for deprecated browsers.
*
*/
cancelAnimFrame = (function() {
return (
window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.oCancelAnimationFrame ||
window.msCancelAnimationFrame ||
function(id) {
return window.clearTimeout(id);
}
);
})();
};
/**
* Parse string to integer.
* If the number is not positive or null
* the method will return the default value
* or 0 if undefined
*
* @param {string} value String to parse
* @param {*} defaultValue Value to return if the result parsed is invalid
* @return {number}
*
*/
parsePositiveInt = function(value, defaultValue) {
var output = parseInt(value, 10);
return output >= 0 ? output : defaultValue;
};

View File

@ -0,0 +1,75 @@
// Karma configuration
// Generated on Fri Jul 18 2014 10:58:08 GMT+0100 (BST)
module.exports = function(config) {
var options = {
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '..',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'test/unit.setup.js',
'src/pathformer.js',
'src/vivus.js',
'test/unit/**.js'
],
// list of files to exclude
exclude: [],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'../src/pathformer.js': ['coverage'],
'../src/vivus.js': ['coverage']
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress', 'coverage'],
// optionally, configure the reporter
coverageReporter: {
type: 'html',
dir: '../coverage/'
},
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true
};
if (process.env.TRAVIS) {
options.customLaunchers = {
Chrome_travis_ci: {
base: 'Chrome',
flags: ['--no-sandbox']
}
};
options.browsers = ['Chrome_travis_ci'];
}
config.set(options);
};

View File

@ -0,0 +1,30 @@
<svg height="300" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 404.7 354" enable-background="new 0 0 404.7 354">
<g stroke="#f9f9f9" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10">
<!-- HI -->
<path data-duration="10" d="M324.6,61.2c16.6,0,29.5-12.9,29.5-29.5c0-16.6-12.9-29.5-29.5-29.5c-16.6,0-29.5,12.9-29.5,29.5C295.1,48.4,308,61.2,324.6,61.2z"/>
<path data-duration="130" d="M366.2,204.2c-9.8,0-15-5.6-15-15.1V77.2h-85v28h19.5c9.8,0,8.5,2.1,8.5,11.6v72.4c0,9.5,0.5,15.1-9.3,15.1H277h-20.7c-8.5,0-14.2-4.1-14.2-12.9V52.4c0-8.5,5.7-12.3,14.2-12.3h18.8v-28h-127v28h18.1c8.5,0,9.9,2.1,9.9,8.9v56.1h-75V53.4c0-11.5,8.6-13.3,17-13.3h11v-28H2.2v28h26c8.5,0,12,2.1,12,7.9v142.2c0,8.5-3.6,13.9-12,13.9h-21v33h122v-33h-11c-8.5,0-17-4.1-17-12.2v-57.8h75v58.4c0,9.1-1.4,11.6-9.9,11.6h-18.1v33h122.9h5.9h102.2v-33H366.2z"/>
<path data-async="" data-delay="20" d="M358.8,82.8c11.1-4.2,18.8-14.7,18.8-27.5c0-8.5-3.4-16-8.9-21.3"/>
<path data-async="" d="M124.2,105.7V77c0-11.5,9.1-13.8,17.5-13.8h10.5V44.7"/>
<polyline data-async="" points="147.9,40.2 171.2,63.2 175.7,63.2"/>
<line data-async="" x1="295.1" y1="32.1" x2="275.2" y2="12.2"/>
<path data-async="" d="M266.2,204.7V75.9c0-8.5,5.2-12.8,13.7-12.8h18.3V44.7"/>
<polyline data-async="" points="265.9,105.2 289.2,129.2 293.7,129.2"/>
<polyline data-async="" points="374.2,204.7 374.2,94.2 358.8,82.8 351.2,77.2"/>
<polyline data-async="" points="148.2,237.2 171.2,261.2 294.6,261.2 300.5,261.2 402.2,261.2 402.2,228.2 379.2,204.2"/>
<polyline data-async="" points="124.2,204.7 124.2,157.2 175.7,157.2"/>
<line data-async="" x1="147.7" y1="228.2" x2="129.2" y2="204.2"/>
<polyline data-async="" points="7.2,237.3 30.2,261.2 152.2,261.2 152.2,241.7"/>
<polyline data-async="" points="1.9,40.2 26,63.2 39.7,63.2"/>
<line data-async="" x1="129.2" y1="12.2" x2="148.2" y2="33.2"/>
<line data-async="" x1="303.9" y1="53" x2="328.1" y2="77.2"/>
<line x1="345.1" y1="10.5" x2="368.7" y2="34"/>
<!-- there -->
<path data-delay="30" data-duration="60" stroke-linecap="round" stroke-linejoin="round" d="M76.8,337.3c0,0,1.9,12.2,13.1,12.2c22.1,0,23.8-1.8,59-66.4c-19.7,35.7-36.4,66.2-19.3,66.2c15.2,0,22.9-14.2,28.3-23.7c3.3-0.5,24-3.2,35-25.5c4-8.1,4.1-17.8-8.1-15.2c-5.6,1.2-13.1,14.8-15.7,19.2c-7.6,12.7-22.4,45.2-22.4,45.2s10.3-22.4,21.5-22.4c15.5,0-9.4,22.4,4.7,22.4c4.9,0,11.7-11.4,16.6-20.9c7.5,4.7,19.7,1.7,24.5-8.1c10.1-20.4-14.4-12.8-24.5,8.1c-5.5,11.3-2.2,21.1,11.2,21.1c16.4,0,26.1-28.3,30.5-37.5c9.9,2.5,14,2.5,22.7-1.1c-3.5,5.1-24,38.1-8.3,38.1c6.7,0,11.7-11.4,16.6-20.9c7.5,4.7,19.7,1.7,24.5-8.1c10.1-20.4-14.4-12.8-24.5,8.1c-5.5,11.3-2.2,21.1,11.2,21.1c16.4,0,20.6-4,24.7-10.5"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M157.3,300.8c3.8-2.3-29,0.8-35.6,3.2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,425 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vivus.js - manual tests page</title>
<meta name="description" content="SVG Drawing Animation" />
<style type="text/css">
/* Base style */
html {
font-size: 24px;
height: 100%;
}
body {
height: 100%;
margin: 0 0 40px;
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
font-weight: 200;
color: #666666;
background-color: #ffffff;
word-break: break-word;
}
h1,
h2,
h3,
h4,
h5,
h6,
p {
font-weight: 100;
}
a,
a:visited,
a:hover,
a:link {
color: inherit;
outline: 0;
}
small {
font-weight: 100;
}
p {
font-size: 1rem;
line-height: 1.4rem;
}
button,
.button {
margin: 0;
padding: 3px 6px;
border-radius: 6px;
border: 1px solid currentColor;
color: inherit;
background-color: rgba(0, 0, 0, 0);
font-size: 0.6rem;
font-weight: 300;
text-decoration: none;
cursor: pointer;
outline: 0;
}
button.active,
.button.active {
background-color: currentColor;
}
button.active span,
.button.active span {
color: #ffffff;
}
i {
background-color: rgba(0, 0, 0, 0.25);
border-radius: 4px;
}
svg * {
fill: none;
stroke: currentColor;
}
table {
border-collapse: collapse;
}
table,
th,
td {
border: 1px solid currentColor;
line-height: 0;
}
/* Layout */
.content {
margin: auto;
max-width: 960px;
width: 100%;
}
.box {
width: 100%;
display: inline-block;
vertical-align: top;
}
.section {
min-height: 90%;
padding: 20px;
box-sizing: border-box;
}
.warning {
padding: 0.5rem 0.75rem;
border: 1px solid currentColor;
color: #fff;
background-color: #c00;
border-radius: 0.25rem;
}
.hidden {
display: none;
}
@media (min-width: 768px) {
.box-50 {
width: 50%;
}
.section {
display: flex;
flex-direction: row;
}
}
/* Themes */
.intro {
display: block;
}
.sunrise {
color: #f037a5;
background-color: #f8c72c;
}
.matrix {
color: #86e0c4;
background-color: #181f21;
}
.electric {
color: #78c9db;
background-color: #e4175b;
}
.night {
color: #d3d679;
background-color: #316bd2;
}
</style>
</head>
<body>
<div class="section intro">
<h2>Vivus manual (cheap) tests.</h2>
<p>
Just scroll along the page and if a glitch appear or the visual
appearance is not like the description, it's not good.
</p>
<p id="config-instructions" class="warning hidden">
To use this page you must use an HTTP server to serve files. Run
<i>npm run serve</i> in the repository then go to the
<a href="http://127.0.0.1:8844/test/manual">test page</a>
</p>
</div>
<div class="section matrix">
<div class="content">
<div class="box box-50">
<p>
This should display the obturateur SVG like on the demo page. The
strokes must be orange. The element must remain an
<i>object</i> tag.
</p>
<button
onclick="this.textContent=(document.querySelector('object#obt')?'Pass!':'Failed.')"
>
Test
</button>
</div>
<div class="box box-50">
<object
id="obt"
data="obturateur.svg"
type="image/svg+xml"
style="width: 100%; max-height: 250px"
></object>
</div>
</div>
</div>
<div class="section sunrise">
<div class="content">
<div class="box box-50">
<div id="polaroid-dynamic"></div>
</div>
<div class="box box-50">
<p>
This should display the polaroid SVG like on the demo page. The
strokes must have the same color as this text.
</p>
</div>
</div>
</div>
<div class="section electric">
<div class="content">
<div class="box box-50">
<p>
This should display the 'Hi there' SVG like ready to start. Be sure
no glitch appear (no small path or dots). Click on the following
button to start.
</p>
<button onclick="hiD.play();">Start</button>
</div>
<div class="box box-50">
<div id="hi-dynamic" style="max-width: 300px; margin: auto"></div>
</div>
</div>
</div>
<div class="section night">
<div class="content">
<div class="box box-50">
<div id="synth-dynamic" style="max-width: 400px; margin: auto"></div>
</div>
<div class="box box-50">
<p>
This should display a synth ready to start. Be sure no glitch appear
(no small path or dots). The animation should use a custom path
timing function (ease_in: slow at start then finish fast.). Click on
the following button to start.
</p>
<button onclick="synthD.play();">Start</button>
</div>
</div>
</div>
<div class="section electric">
<div class="content">
<table id="nonScaling">
<tr style="height: 16.66%">
<td style="width: 16.66%">
<svg viewBox="0 0 100 100" width="100%" height="100%">
<line
vector-effect="non-scaling-stroke"
x1="0"
y1="0"
x2="200"
y2="200"
transform="scale(.5,.5)"
/>
</svg>
</td>
<td style="width: 33.33%">
<svg viewBox="0 0 200 100" width="100%" height="100%">
<line
vector-effect="non-scaling-stroke"
x1="0"
y1="0"
x2="200"
y2="200"
transform="scale(1,.5)"
/>
</svg>
</td>
<td style="width: 50%">
<svg viewBox="0 0 300 100" width="100%" height="100%">
<line
vector-effect="non-scaling-stroke"
x1="0"
y1="0"
x2="200"
y2="200"
transform="scale(1.5,.5)"
/>
</svg>
</td>
</tr>
<tr style="height: 33.33%">
<td>
<svg viewBox="0 0 100 200" width="100%" height="100%">
<line
vector-effect="non-scaling-stroke"
x1="0"
y1="0"
x2="200"
y2="200"
transform="scale(.5,1)"
/>
</svg>
</td>
<td>
<svg viewBox="0 0 200 200" width="100%" height="100%">
<line
vector-effect="non-scaling-stroke"
x1="0"
y1="0"
x2="200"
y2="200"
transform="scale(1,1)"
/>
</svg>
</td>
<td>
<svg viewBox="0 0 300 200" width="100%" height="100%">
<line
vector-effect="non-scaling-stroke"
x1="0"
y1="0"
x2="200"
y2="200"
transform="scale(1.5,1)"
/>
</svg>
</td>
</tr>
<tr style="height: 50%">
<td>
<svg viewBox="0 0 100 300" width="100%" height="100%">
<line
vector-effect="non-scaling-stroke"
x1="0"
y1="0"
x2="200"
y2="200"
transform="scale(.5,1.5)"
/>
</svg>
</td>
<td>
<svg viewBox="0 0 200 300" width="100%" height="100%">
<line
vector-effect="non-scaling-stroke"
x1="0"
y1="0"
x2="200"
y2="200"
transform="scale(1,1.5)"
/>
</svg>
</td>
<td>
<svg viewBox="0 0 300 300" width="100%" height="100%">
<line
vector-effect="non-scaling-stroke"
x1="0"
y1="0"
x2="200"
y2="200"
transform="scale(1.5,1.5)"
/>
</svg>
</td>
</tr>
</table>
<p>
Non scaling path<br />In any case of a resize, the animation of each
line must be complete.
</p>
<button onclick="nonScalingResize();">Resize</button>
<button onclick="nonScalingReplay();">Replay</button>
</div>
</div>
<!-- Le scripts -->
<script src="/dist/vivus.js"></script>
<script>
// Display warning message if not on http server
if (window.location.protocol === 'file:') {
var configIntro = document.getElementById('config-instructions');
configIntro.style.display = 'block';
}
// Obturateur
var obt1 = new Vivus('obt', {
type: 'delayed',
duration: 150,
});
// polaroid-dynamic
var polaroidD = new Vivus('polaroid-dynamic', {
file: 'polaroid.svg',
type: 'scenario-sync',
duration: 20,
});
var hiD = new Vivus('hi-dynamic', {
file: 'hi-there.svg',
type: 'scenario-sync',
duration: 20,
start: 'manual',
});
var synthD = new Vivus('synth-dynamic', {
file: 'synth.svg',
type: 'oneByOne',
duration: 200,
start: 'manual',
animTimingFunction: Vivus.EASE_IN,
});
// Non scaling
const resizeObserver = new ResizeObserver((entries) => {
nonScalingVivuses.forEach((v) => v.recalc());
});
resizeObserver.observe(window.nonScaling);
var nonScalingVivuses = Array.from(
document.querySelectorAll('#nonScaling svg')
).map((svg) => new Vivus(svg, { start: 'autostart' }));
function nonScalingResize() {
const newWidth = Math.floor(Math.random() * 100);
const newHeight = Math.floor(Math.random() * 100);
window.nonScaling.style.width = `${newWidth}%`;
window.nonScaling.style.height = `${newHeight}%`;
nonScalingVivuses.forEach((v) => v.recalc());
}
function nonScalingReplay() {
nonScalingVivuses.forEach((v) => v.reset().play());
}
</script>
</body>
</html>

View File

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="100%" height="200px" viewBox="0 0 200 200" enable-background="new 0 0 200 200">
<g stroke="#f60" fill="none" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" >
<circle cx="100" cy="100" r="90"/>
<circle cx="100" cy="100" r="85.74"/>
<circle cx="100" cy="100" r="72.947"/>
<circle cx="100" cy="100" r="39.74"/>
<line x1="34.042" y1="131.189" x2="67.047" y2="77.781"/>
<line x1="31.306" y1="75.416" x2="92.41" y2="60.987"/>
<line x1="68.81" y1="34.042" x2="122.219" y2="67.046"/>
<line x1="124.584" y1="31.305" x2="139.013" y2="92.409"/>
<line x1="165.957" y1="68.809" x2="132.953" y2="122.219"/>
<line x1="168.693" y1="124.584" x2="107.59" y2="139.012"/>
<line x1="131.19" y1="165.957" x2="77.781" y2="132.953"/>
<line x1="75.417" y1="168.693" x2="60.987" y2="107.59"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 910 B

View File

@ -0,0 +1,62 @@
<svg id="polaroid" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 200 160" enable-background="new 0 0 200 160">
<g stroke="#f9f9f9" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10">
<!-- Case -->
<!-- The case items will be drawn at the same time (attribute `data-async` on each tag) with a custom duration of 40 frames (attribute `data-duration`). WARNING: When you want to draw a bloc asynchronously (like here), the last item need to be `data-async` free. Otherwise the following tags will also start at the same time. I know it's a bit confusing, play a bit with it and you'll see. -->
<path data-async="" data-duration="40" d="
M106.725,104.742c-0.773,2.498-3.586,4.229-6.285,3.867L12.473,96.802c-2.699-0.363-4.262-2.682-3.49-5.18l25.164-81.436
c0.771-2.496,3.584-4.229,6.283-3.866l87.966,11.808c2.699,0.362,4.264,2.68,3.49,5.179L106.725,104.742z"/>
<path data-async="" data-duration="40" d="
M101.02,123.207c-0.773,2.5-3.587,4.23-6.286,3.867L6.766,115.27c-2.699-0.363-4.26-2.682-3.488-5.182l2.91-9.417
c0.771-2.499,3.585-4.23,6.285-3.87l87.967,11.809c2.699,0.361,4.261,2.682,3.49,5.18L101.02,123.207z"/>
<line data-async="" data-duration="40" x1="103.377" y1="128.225" x2="154.768" y2="155.32"/>
<line data-async="" data-duration="40" x1="109.852" y1="112.684" x2="155.035" y2="136.906"/>
<path data-async="" data-duration="40" d="
M9.096,120.207l47.932,21.994c0,0,98.06,12.414,97.74,13.119c-0.318,0.709,5.426-16.205,5.426-16.205l-2.646-96.842
c-1.098-7.587-2.467-11.8-8.559-15.024l-12.635-6.604"/>
<path data-async="" data-duration="40" d="
M161.586,38.135l30.717,16.085c0,0,5.295,2.323,4.543,6.504l-3.215,10.527c-0.648,2.621-2.939,4.988-8.229,2.798l-9.154-4.701
l-11.834,56.441"/>
<path data-duration="40" d="
M183.148,49.518c0,0,5.295,2.324,4.543,6.506l-3.215,10.526c-0.648,2.622-2.938,4.988-8.229,2.798"/>
<!-- Lens -->
<!-- All item will be drawn line by line, with an exception for the first one, a little delay (attribute `data-delay) to make a break between the drawing of the case and the start of the lens part -->
<path data-delay="20" d="
M87.176,56.143C83.274,68.78,69.043,77.538,55.395,75.706S33.846,62.146,37.75,49.511c3.903-12.637,18.135-21.392,31.783-19.562
C83.181,31.782,91.081,43.51,87.176,56.143z"/>
<path d="
M92.745,56.891c-4.785,15.48-22.219,26.213-38.942,23.969C37.079,78.615,27.4,64.245,32.184,48.763
c4.783-15.48,22.218-26.211,38.94-23.968C87.848,27.041,97.528,41.411,92.745,56.891z"/>
<path d="
M78.99,26.933c16.169,7.426,19.398,10.989,22.026,20.105c1.283,4.449,1.271,9.411-0.3,14.489
c-4.783,15.479-22.217,26.213-38.941,23.969c-8.68-1.165-21.171-7.963-25.613-14.055"/>
<path d="
M42.602,50.162c3.137-10.157,14.573-17.193,25.543-15.722"/>
<!-- Flash -->
<!-- This tag does not have any extra attribute. So it will start when the previous tag is finished. His duration and delay will be the one given in the options. -->
<path d="
M103.789,29.275c-0.568,1.841,0.582,3.549,2.57,3.818l12.807,1.72c1.988,0.266,4.062-1.012,4.633-2.851l1.66-5.38
c0.568-1.843-0.582-3.551-2.57-3.816l-12.807-1.72c-1.988-0.268-4.062,1.01-4.633,2.85L103.789,29.275z"/>
<!-- Output -->
<!-- Same case as Flash -->
<path d="
M11.129,105.791c-0.297,0.965,0.305,1.855,1.346,1.994l81.446,10.932c1.038,0.141,2.123-0.527,2.42-1.49l0,0
c0.298-0.961-0.304-1.855-1.343-1.996l-81.447-10.93C12.51,104.16,11.426,104.828,11.129,105.791L11.129,105.791z"/>
<!-- Design (color lines on the front) -->
<!-- All the lines will start at the same time, because they all have the attribute `data-async` -->
<line data-async="" x1="47.583" y1="101.505" x2="51.561" y2="88.267"/>
<line data-async="" x1="53.391" y1="102.326" x2="57.047" y2="90.125"/>
<line data-async="" x1="59.224" y1="103.068" x2="62.749" y2="91.295"/>
<line data-async="" x1="65.057" y1="103.814" x2="69.015" y2="90.637"/>
<line data-async="" x1="72.87" y1="19.969" x2="75.497" y2="11.082"/>
<line data-async="" x1="78.512" y1="21.325" x2="81.317" y2="11.868"/>
<line data-async="" x1="83.833" y1="23.718" x2="87.16" y2="12.582"/>
<line data-async="" x1="89.145" y1="26.141" x2="92.939" y2="13.498"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="378.995px" height="259.5px" viewBox="0 0 378.995 259.5" enable-background="new 0 0 378.995 259.5" xml:space="preserve">
<g id="Synth">
<path fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
M375.5,108.934c1.549,2.693,0.627,5.739-2.059,6.804L72.043,235.257c-2.685,1.064-6.116-0.254-7.665-2.946L5.362,129.69
c-1.548-2.692-0.625-5.737,2.059-6.802L308.818,3.369c2.686-1.065,6.117,0.254,7.666,2.946L375.5,108.934z"/>
<path fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
M376.246,111.47l-2.068,18.344c0,0-0.621,5.361-4.932,7.726L69.601,256.365c-2.685,1.064-6.116-0.254-7.665-2.946L3.693,152.145
c-1.548-2.692-0.878-9.891-0.878-9.891l0.82-7.014"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="373.785" y1="112.765" x2="371.715" y2="130.65"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="70.318" y1="250.17" x2="371.715" y2="130.65"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="319.578" y1="22.078" x2="19.852" y2="140.935"/>
<polyline fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
19.852,140.935 72.387,232.284 70.318,250.17 "/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="371.715" y1="130.65" x2="365.053" y2="119.063"/>
<g id="octaves_1_">
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="93.572" y1="226.72" x2="73.09" y2="191.106"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="114.616" y1="218.126" x2="94.134" y2="182.512"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="136.629" y1="209.646" x2="82.765" y2="115.986"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="158.158" y1="201.108" x2="137.674" y2="165.493"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="179.688" y1="192.572" x2="159.203" y2="156.957"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="201.213" y1="184.034" x2="180.732" y2="148.419"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="222.742" y1="175.497" x2="168.879" y2="81.838"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="244.27" y1="166.959" x2="223.789" y2="131.346"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="265.799" y1="158.423" x2="245.318" y2="122.809"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="287.328" y1="149.886" x2="233.463" y2="56.226"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="308.855" y1="141.349" x2="288.375" y2="105.734"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="330.385" y1="132.812" x2="309.902" y2="97.197"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="351.912" y1="124.274" x2="331.432" y2="88.66"/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="373.441" y1="115.737" x2="319.578" y2="22.078"/>
</g>
<g id="bemols_1_">
<g>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
67.427,190.815 35.241,134.85 39.708,133.061 44.176,131.271 76.24,177.929 76.362,187.236 "/>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
69.3,180.708 36.616,135.539 44.606,132.34 76.24,177.929 "/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="69.077" y1="183.09" x2="68.222" y2="187.702"/>
</g>
<g>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
89.117,182.213 56.931,126.247 61.398,124.458 65.866,122.669 97.93,169.326 98.052,178.634 "/>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
90.99,172.105 58.306,126.937 66.295,123.736 97.93,169.326 "/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="90.767" y1="174.487" x2="89.912" y2="179.1"/>
</g>
<g>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
132.012,165.204 99.826,109.238 104.293,107.449 108.762,105.661 140.825,152.317 140.948,161.625 "/>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
133.885,155.098 101.202,109.928 109.191,106.728 140.825,152.317 "/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="133.661" y1="157.479" x2="132.807" y2="162.091"/>
</g>
<g>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
153.541,156.666 121.354,100.7 125.821,98.911 130.289,97.122 162.354,143.779 162.475,153.088 "/>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
155.416,146.559 122.729,101.39 130.719,98.19 162.354,143.779 "/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="155.191" y1="148.94" x2="154.335" y2="153.554"/>
</g>
<g>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
175.068,148.129 142.881,92.164 147.348,90.374 151.817,88.585 183.881,135.242 184.004,144.551 "/>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
176.941,138.021 144.256,92.853 152.247,89.653 183.881,135.242 "/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="176.719" y1="140.403" x2="175.863" y2="145.017"/>
</g>
<g>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
218.127,131.056 185.939,75.089 190.406,73.3 194.875,71.512 226.938,118.169 227.061,127.476 "/>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
220,120.948 187.314,75.778 195.305,72.579 226.938,118.169 "/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="219.775" y1="123.329" x2="218.922" y2="127.942"/>
</g>
<g>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
239.656,122.518 207.469,66.553 211.936,64.763 216.402,62.975 248.467,109.631 248.59,118.939 "/>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
241.529,112.411 208.844,67.241 216.834,64.042 248.467,109.631 "/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="241.305" y1="114.792" x2="240.449" y2="119.405"/>
</g>
<g>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
282.713,105.443 250.525,49.478 254.992,47.688 259.459,45.9 291.523,92.558 291.646,101.864 "/>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
284.584,95.337 251.902,50.168 259.891,46.968 291.523,92.558 "/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="284.361" y1="97.718" x2="283.508" y2="102.33"/>
</g>
<g>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
304.24,96.906 272.055,40.941 276.52,39.151 280.988,37.363 313.053,84.02 313.174,93.328 "/>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
306.113,86.8 273.43,41.631 281.42,38.431 313.053,84.02 "/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="305.891" y1="89.181" x2="305.035" y2="93.794"/>
</g>
<g>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
325.768,88.369 293.582,32.404 298.049,30.614 302.518,28.825 334.58,75.482 334.703,84.791 "/>
<polygon fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
327.641,78.262 294.957,33.093 302.947,29.894 334.58,75.482 "/>
<line fill="none" stroke="#241F20" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" x1="327.418" y1="80.644" x2="326.562" y2="85.257"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,14 @@
/* Here is a cheap and bad implementation
* of requestAnimationFrame and
* cancelAnimationFrame mock.
* But it's more than enough
* for our tests.
*/
window.requestAnimFrameStack = [];
window.requestAnimationFrame = function (callback) {
window.requestAnimFrameStack.push(callback);
return true;
};
window.cancelAnimationFrame = function () {
window.requestAnimFrameStack = [];
};

View File

@ -0,0 +1,322 @@
'use strict';
/**
* Unit tests for Pathformer
*
*/
describe('Pathformer', function () {
var svgTag,
svgTagId = 'my-svg',
svgGroupTag,
svgGroupTagId = 'my-svg-group';
beforeEach(function () {
// Remove tag if existing
svgTag = document.getElementById(svgTagId);
if (svgTag) {
svgTag.remove();
}
// Create the SVG
svgTag = document.createElementNS('http://www.w3.org/2000/svg','svg');
svgTag.id = svgTagId;
svgTag.innerHTML = '<circle fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" cx="100" cy="100" r="72.947"/>' +
'<circle fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" cx="100" cy="100" r="39.74"/>' +
'<g id="' + svgGroupTagId + '">' +
'<line fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" x1="34.042" y1="131.189" x2="67.047" y2="77.781"/>' +
'<line fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" x1="165.957" y1="68.809" x2="132.953" y2="122.219"/>' +
'<line fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" x1="131.19" y1="165.957" x2="77.781" y2="132.953"/>' +
'<line fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" x1="68.81" y1="34.042" x2="122.219" y2="67.046"/>' +
'</g>';
svgGroupTag = svgTag.querySelector('#'+svgGroupTagId);
// Insert it to the body
document.body.appendChild(svgTag);
});
describe('[param tests]', function () {
// Tests about the SVG element
it('should throw an error if the SVG is given in parameter', function () {
expect(function () {
new Pathformer();
}).toThrow(new Error('Pathformer [constructor]: "element" parameter is required'));
});
it('should work with only the SVG id', function () {
expect(function () {
new Pathformer(svgTagId);
}).not.toThrow();
});
it('should work with only the SVG object', function () {
expect(function () {
new Pathformer(svgTag);
}).not.toThrow();
});
it('should work with only the SVG group object', function () {
expect(function () {
new Pathformer(svgGroupTag);
}).not.toThrow();
});
it('should throw an error if the SVG ID given is invalid', function () {
expect(function () {
new Pathformer('my-unexisting-svg');
}).toThrow(new Error('Pathformer [constructor]: "element" parameter is not related to an existing ID'));
});
it('should throw an error if the ID given is not related to a SVG element', function () {
var divTag = document.createElement('div');
divTag.id = 'my-div';
document.body.appendChild(divTag);
expect(function () {
new Pathformer('my-div');
}).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement'));
});
it('should throw an error if the element is not a correct type (DOM object or string)', function () {
expect(function () { new Pathformer({}); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement'));
expect(function () { new Pathformer(42); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement'));
expect(function () { new Pathformer(false); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement'));
expect(function () { new Pathformer(new Date()); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement'));
expect(function () { new Pathformer(function () {}); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement'));
expect(function () { new Pathformer(document.createElement('div')); }).toThrow(new Error('Pathformer [constructor]: "element" parameter must be a string or a SVGelement'));
});
});
describe('[translation]', function () {
// Line object
describe('line', function () {
it('should return an object with a `d` attribute', function () {
var output = Pathformer.prototype.lineToPath({});
expect(output.d).toBeDefined();
});
it('should return an object with an unclosed shape', function () {
var output = Pathformer.prototype.lineToPath({});
expect(output.d.substr(-1)).not.toEqual('Z');
});
it('should set default positino attributes to zero', function () {
var output = Pathformer.prototype.lineToPath({
x1: '21', x2: '32', y1: '11'
});
expect(output.d.indexOf('0')).not.toEqual(-1);
expect(output.d.indexOf('undefined')).toEqual(-1);
});
});
// Rect object
describe('rect', function () {
it('should return an object with a `d` attribute', function () {
var output = Pathformer.prototype.rectToPath({});
expect(output.d).toBeDefined();
});
it('should return an object with a closed shape', function () {
var output = Pathformer.prototype.rectToPath({});
expect(output.d.substr(-1)).toEqual('Z');
});
it('should set default positino attributes to zero', function () {
var output = Pathformer.prototype.rectToPath({
x: '21', height: '32', width: '11'
});
expect(output.d.indexOf('0')).not.toEqual(-1);
expect(output.d.indexOf('undefined')).toEqual(-1);
});
it('should apply rounded corners', function () {
var result = 'M 50,10 ' +
'L 50,10 A 40,20,0,0,1,90,30 ' +
'L 90,50 A 40,20,0,0,1,50,70 ' +
'L 50,70 A 40,20,0,0,1,10,50 ' +
'L 10,30 A 40,20,0,0,1,50,10';
var output = Pathformer.prototype.rectToPath({
x:10, y:10, width:80, height:60, rx:100, ry:20
});
expect(output.d).toEqual(result);
});
it('should apply rounded corners even when a value is missing', function () {
var result = 'M 30,10 ' +
'L 70,10 A 20,20,0,0,1,90,30 ' +
'L 90,50 A 20,20,0,0,1,70,70 ' +
'L 30,70 A 20,20,0,0,1,10,50 ' +
'L 10,30 A 20,20,0,0,1,30,10';
var output = Pathformer.prototype.rectToPath({
x:10, y:10, width:80, height:60, ry:20
});
expect(output.d).toEqual(result);
});
});
// Polyline object
describe('polyline', function () {
var polyline;
beforeEach(function () {
polyline = {
points: '2,3 4,5 6,7'
};
});
it('should return an object with a `d` attribute', function () {
var output = Pathformer.prototype.polylineToPath(polyline);
expect(output.d).toBeDefined();
});
it('should return an object with an unclosed shape', function () {
var output = Pathformer.prototype.polylineToPath(polyline);
expect(output.d.substr(-1)).not.toEqual('Z');
});
it('should ignore incorrect points', function () {
var output;
polyline.points += ' 43';
output = Pathformer.prototype.polylineToPath(polyline);
expect(output.d.indexOf('43')).toEqual(-1);
});
it('should accept points defined with and without commas', function () {
var outputWithPoint = Pathformer.prototype.polylineToPath(polyline);
var outputWithoutPoint = Pathformer.prototype.polylineToPath({points: '2 3 4 5 6 7'});
expect(outputWithPoint).toEqual(outputWithoutPoint);
});
});
// Polygon object
describe('polygon', function () {
var polygon;
beforeEach(function () {
polygon = {
points: '2,3 4,5 6,7'
};
});
it('should return an object with a `d` attribute', function () {
var output = Pathformer.prototype.polygonToPath(polygon);
expect(output.d).toBeDefined();
});
it('should return an object with a closed shape', function () {
var output = Pathformer.prototype.polygonToPath(polygon);
expect(output.d.substr(-1)).toEqual('Z');
});
});
// Ellipse object
describe('ellipse', function () {
var ellipse;
beforeEach(function () {
ellipse = {
cx: 2,
cy: 3,
rx: 3
};
});
it('should return an object with a `d` attribute', function () {
var output = Pathformer.prototype.ellipseToPath(ellipse);
expect(output.d).toBeDefined();
});
it('should return an object with an unclosed shape', function () {
var output = Pathformer.prototype.ellipseToPath(ellipse);
expect(output.d.substr(-1)).not.toEqual('Z');
});
it('should set default positino attributes to zero', function () {
delete ellipse.cy;
var output = Pathformer.prototype.ellipseToPath(ellipse);
expect(output.d.indexOf('0')).not.toEqual(-1);
expect(output.d.indexOf('undefined')).toEqual(-1);
});
});
// Circle object
describe('circle', function () {
var circle;
beforeEach(function () {
circle = {
cx: 2,
cy: 3,
rx: 3,
r: 1
};
});
it('should return an object with a `d` attribute', function () {
var output = Pathformer.prototype.circleToPath(circle);
expect(output.d).toBeDefined();
});
it('should return an object with an unclosed shape', function () {
var output = Pathformer.prototype.circleToPath(circle);
expect(output.d.substr(-1)).not.toEqual('Z');
});
it('should set default positino attributes to zero', function () {
delete circle.cy;
var output = Pathformer.prototype.circleToPath(circle);
expect(output.d.indexOf('0')).not.toEqual(-1);
expect(output.d.indexOf('undefined')).toEqual(-1);
});
});
});
describe('[utils]', function () {
describe('attribute parser', function () {
it('should return an empty object if attributes length are undefined', function () {
var output = Pathformer.prototype.parseAttr({});
expect(output).toEqual({});
});
});
describe('engine', function () {
it('shouldn\'t throw an error if the SVG got a tag not taken in charge', function () {
svgTag.innerHTML = '<polypentagoneofhell fill="none" stroke="#666666" stroke-width="6" stroke-miterlimit="666" cx="666" cy="666"/>';
expect(function () {
new Pathformer(svgTagId);
}).not.toThrow();
});
it('should remove useless attributes during transformation', function () {
new Pathformer(svgTagId);
expect(svgTag.childNodes[0].getAttribute('cx')).toBe(null);
});
});
describe('validity', function () {
it('should throw error if the SVG contain shape with percentage value', function () {
// Create the SVG
var svgTagPrc = document.createElementNS('http://www.w3.org/2000/svg','svg');
svgTagPrc.innerHTML = '<circle cx="100%" cy="100" r="10"/>';
expect(function () {
new Pathformer(svgTagPrc);
}).toThrow(new Error('Pathformer [parseAttr]: a SVG shape got values in percentage. This cannot be transformed into \'path\' tags. Please use \'viewBox\'.'));
});
it('shouldn\'t throw error if the SVG contain shape with percentage value on a non-data attribute', function () {
// Create the SVG
var svgTagPrc = document.createElementNS('http://www.w3.org/2000/svg','svg');
svgTagPrc.innerHTML = '<circle width="100%" cx="100" cy="100" r="10"/>';
expect(function () {
new Pathformer(svgTagPrc);
}).not.toThrow();
});
});
});
});

View File

@ -0,0 +1,669 @@
'use strict';
/**
* Unit tests for Vivus
*
*/
describe('Vivus', function () {
var ObjectElementMock,
triggerFrames,
myVivus,
objTag,
wrapTag,
svgTag,
svgTagId = 'my-svg',
svgGroupTagId = 'my-svg-group';
// Mock ObjectElement and it's constructor via createElement
ObjectElementMock = function () {
this.loadCb = [];
this.attr = {};
this.addEventListener = function (evtName, cb) {
if (evtName === 'load') {
this.loadCb.push(cb);
}
};
this.loaded = function () {
for (var i = 0; i < this.loadCb.length; i++) {
this.loadCb[i]({target: this});
}
};
this.getBoundingClientRect = function () {
return {
height: 11,
top: 364
};
};
this.insertBefore = function () {};
this.removeChild = function () {};
this.setAttribute = function (key, val) {
this.attr[key] = val;
};
this.getAttribute = function (key) {
return this.attr[key];
};
};
window.HTMLObjectElement = ObjectElementMock;
triggerFrames = function (counter) {
counter = counter || -1;
while (window.requestAnimFrameStack.length && counter !== 0) {
window.requestAnimFrameStack.shift()();
counter--;
}
};
beforeEach(function () {
// Create the SVG
svgTag = document.createElementNS('http://www.w3.org/2000/svg','svg');
svgTag.id = svgTagId;
svgTag.innerHTML = '<circle fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" cx="100" cy="100" r="72.947"/>' +
'<circle fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" cx="100" cy="100" r="39.74"/>' +
'<g id="' + svgGroupTagId + '">' +
'<line fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" x1="34.042" y1="131.189" x2="67.047" y2="77.781"/>' +
'<line fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" x1="165.957" y1="68.809" x2="132.953" y2="122.219"/>' +
'<line fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" x1="131.19" y1="165.957" x2="77.781" y2="132.953"/>' +
'<line fill="none" stroke="#f9f9f9" stroke-width="3" stroke-miterlimit="10" x1="68.81" y1="34.042" x2="122.219" y2="67.046"/>' +
'</g>';
wrapTag = document.createElement('div');
wrapTag.appendChild(svgTag);
document.body.appendChild(wrapTag);
// Reset the request anim frame stack
window.requestAnimFrameStack = [];
});
afterEach(function () {
// Remove tag
svgTag.remove();
wrapTag.remove();
});
describe('[basic tests]', function () {
it('should the class be defined under Vivus name', function () {
expect(Vivus).toBeDefined();
});
it('should have timing functions set', function () {
expect(Vivus.LINEAR).toBeDefined();
expect(Vivus.EASE).toBeDefined();
expect(Vivus.EASE_IN).toBeDefined();
expect(Vivus.EASE_OUT).toBeDefined();
expect(Vivus.EASE_OUT_BOUNCE).toBeDefined();
});
it('should have timing functions returning correct value on limits', function () {
expect(Vivus.LINEAR(0)).toEqual(0);
expect(Vivus.LINEAR(1)).toEqual(1);
expect(Vivus.EASE(0)).toEqual(0);
expect(Vivus.EASE(1)).toEqual(1);
expect(Vivus.EASE_IN(0)).toEqual(0);
expect(Vivus.EASE_IN(1)).toEqual(1);
expect(Vivus.EASE_OUT(0)).toEqual(0);
expect(Vivus.EASE_OUT(1)).toEqual(1);
expect(Vivus.EASE_OUT_BOUNCE(0)).toEqual(0);
expect(Vivus.EASE_OUT_BOUNCE(1)).toEqual(1);
});
});
describe('[param tests]', function () {
// Tests about the SVG element
it('should throw an error if the SVG is given in parameter', function () {
expect(function () {
new Vivus();
}).toThrow(new Error('Vivus [constructor]: "element" parameter is required'));
});
it('should work with only the SVG id', function () {
expect(function () {
new Vivus(svgTagId);
}).not.toThrow();
});
it('should work with only the SVG object', function () {
expect(function () {
new Vivus(svgTag);
}).not.toThrow();
});
it('should work with the SVG group object', function () {
expect(function () {
new Vivus(svgGroupTagId);
}).not.toThrow();
});
it('should throw an error if the SVG ID given is invalid', function () {
expect(function () {
new Vivus('my-unexisting-svg');
}).toThrow(new Error('Vivus [constructor]: "element" parameter is not related to an existing ID'));
});
it('should throw an error if the ID given is not related to a SVG element', function () {
var divTag = document.createElement('div');
divTag.id = 'my-div';
document.body.appendChild(divTag);
expect(function () {
new Vivus('my-div');
}).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)'));
});
it('should accept any DOM element if `file` option is set', function () {
var divTag = document.createElement('div');
spyOn(window, 'XMLHttpRequest');
try {
new Vivus(divTag, {file: 'opensource.svg'});
}
catch(err) {}
expect(window.XMLHttpRequest).toHaveBeenCalled();
});
it('should throw an error if the element is not a correct type (DOM object or string)', function () {
expect(function () { new Vivus({}); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)'));
expect(function () { new Vivus(42); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)'));
expect(function () { new Vivus(false); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)'));
expect(function () { new Vivus(new Date()); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)'));
expect(function () { new Vivus(function () {}); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)'));
expect(function () { new Vivus(document.createElement('div')); }).toThrow(new Error('Vivus [constructor]: "element" parameter is not valid (or miss the "file" attribute)'));
});
it('should accept object element', function () {
// Create a mock Object getElementById
objTag = new ObjectElementMock();
objTag.contentDocument = wrapTag;
expect(function () {
new Vivus(objTag);
}).not.toThrow();
});
it('the vivus state should be ready if the SVG is already loaded', function () {
objTag = new ObjectElementMock();
objTag.contentDocument = wrapTag;
objTag.loaded();
var myVivus = new Vivus(objTag);
expect(myVivus.isReady).toEqual(true);
});
it('the vivus instance should have `el` and `parentEl` different if the element is an object', function () {
objTag = new ObjectElementMock();
objTag.contentDocument = wrapTag;
objTag.loaded();
var myVivus = new Vivus(objTag);
expect(myVivus.parentEl).not.toEqual(myVivus.el);
});
it('should call `onReady` callback once the SVG is loaded', function () {
objTag = new ObjectElementMock();
objTag.contentDocument = document.createElement('div');
var myVivus = new Vivus(objTag);
objTag.contentDocument = wrapTag;
objTag.loaded();
expect(myVivus.isReady).toEqual(true);
});
it('should throw an error if the SVG file does not exists', function () {
objTag = new ObjectElementMock();
objTag.contentDocument = document.createElement('div');
new Vivus(objTag);
expect(function () {
objTag.loaded();
}).toThrow();
});
// Options
it('should work without options', function () {
expect(function () {
new Vivus(svgTag);
}).not.toThrow();
});
it('should throw an error if options is not an object', function () {
expect(function () { new Vivus(svgTag, []); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object'));
expect(function () { new Vivus(svgTag, 42); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object'));
expect(function () { new Vivus(svgTag, false); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object'));
expect(function () { new Vivus(svgTag, new Date()); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object'));
expect(function () { new Vivus(svgTag, 'manual'); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object'));
expect(function () { new Vivus(svgTag, function () {}); }).toThrow(new Error('Vivus [constructor]: "options" parameter must be an object'));
});
// Options
it('should work with empty option object', function () {
expect(function () {
new Vivus(svgTag, {});
}).not.toThrow();
});
it('should throw an error if the `type` value given in options does not exists', function () {
expect(function () {
new Vivus(svgTag, {type: 'by-unicorn'});
}).toThrow(new Error('Vivus [constructor]: by-unicorn is not an existing animation `type`'));
});
it('should throw an error if the `start` value given in options is not a string', function () {
expect(function () {
new Vivus(svgTag, {start: 'when-unicorn-ready'});
}).toThrow(new Error('Vivus [constructor]: when-unicorn-ready is not an existing `start` option'));
});
it('should throw an error if the `delay` value is bigger (or equal) than `duration`', function () {
expect(function () {
new Vivus(svgTag, {duration: 200, delay: 199});
}).not.toThrow();
expect(function () {
new Vivus(svgTag, {duration: 200, delay: 200});
}).toThrow(new Error('Vivus [constructor]: delay must be shorter than duration'));
expect(function () {
new Vivus(svgTag, {duration: 200, delay: 201});
}).toThrow(new Error('Vivus [constructor]: delay must be shorter than duration'));
});
it('should override `duration` if invalid', function () {
myVivus = new Vivus(svgTag, {duration: -12});
expect(myVivus.duration > 0).toBe(true);
});
it('should override `delay` if invalid, with a null value', function () {
myVivus = new Vivus(svgTag, {delay: -12});
expect(!myVivus.delay).toBe(false);
});
it('should set up default values', function () {
myVivus = new Vivus(svgTag, {});
expect(myVivus.type).toBeDefined();
expect(myVivus.start).toBeDefined();
expect(myVivus.duration).toBeDefined();
});
it('the vivus instance should have `el` and `parentEl` equal if the element is a SVG object', function () {
myVivus = new Vivus(svgTag, {});
expect(myVivus.el).toEqual(myVivus.parentEl);
});
// Callback
it('should throw an error if callback is non a function', function () {
expect(function () {
new Vivus(svgTag, {}, 42);
}).toThrow(new Error('Vivus [constructor]: "callback" parameter must be a function'));
});
it('should use scale to determine path length when vector effect is non-scaling-stroke', function () {
var scalingSvgTag = document.createElementNS('http://www.w3.org/2000/svg','svg');
var scalingWrapTag = document.createElement('div');
scalingSvgTag.setAttribute('viewBox', '0 0 500 200');
scalingWrapTag.style.width = '1000px';
scalingSvgTag.id = 'scaling-stroke-test';
scalingSvgTag.innerHTML = '<path vector-effect="non-scaling-stroke" fill="none" stroke="#f9f9f9" stroke-width="3" d="M0,68.57346635098205L20.833333333333332,3.8875909891199285L41.666666666666664,47.366000806779425L62.5,57.171841641625065L83.33333333333333"/>' +
'<path fill="none" stroke="#f9f9f9" stroke-width="3" d="M0,68.57346635098205L20.833333333333332,3.8875909891199285L41.666666666666664,47.366000806779425L62.5,57.171841641625065L83.33333333333333"/>';
scalingWrapTag.appendChild(scalingSvgTag);
document.body.appendChild(scalingWrapTag);
myVivus = new Vivus(scalingSvgTag);
expect(myVivus.map.length).toEqual(2);
expect(myVivus.map[0].length).toEqual(280);
expect(myVivus.map[1].length).toEqual(141);
});
});
describe('[engine]', function () {
// Mapping
describe('Mapping:', function () {
it('should not trigger any error if the SVG is empty', function () {
expect(function () {
var svgTag = document.createElementNS('http://www.w3.org/2000/svg','svg');
myVivus = new Vivus(svgTag, {});
}).not.toThrow();
});
it('should create a mapping of the SVG', function () {
myVivus = new Vivus(svgTag, {});
expect(myVivus.map && myVivus.map.length).toEqual(6);
});
it('should map with correct values for start and duration', function () {
var i, typeIndex, types = ['delayed', 'sync', 'oneByOne', 'scenario', 'scenario-sync'];
for (typeIndex in types) {
myVivus = new Vivus(svgTag, {type: types[typeIndex], duration: 200});
for (i in myVivus.map) {
expect(myVivus.map[i].startAt >= 0).toBe(true);
expect(myVivus.map[i].duration >= 0).toBe(true);
}
}
});
// Tests for 'getTotalLength' method in case of awkward results
describe('SVG parsing issue', function () {
var getTotalLengthBkp = SVGPathElement.prototype.getTotalLength,
warnBkp = console.warn;
beforeEach(function () {
SVGPathElement.prototype.getTotalLength = function () {
return NaN;
};
});
afterEach(function () {
SVGPathElement.prototype.getTotalLength = getTotalLengthBkp;
console.warn = warnBkp;
});
it('should call console.warn if a path length is NaN', function () {
var warnSpy = jasmine.createSpy('spy');
console.warn = warnSpy;
myVivus = new Vivus(svgTag);
expect(warnSpy.calls.count()).toEqual(6);
expect(myVivus.map.length).toEqual(0);
});
it('shouldn\'t call console.warn if not defined a path length is NaN', function () {
console.warn = null;
myVivus = new Vivus(svgTag);
expect(myVivus.map.length).toEqual(0);
});
});
});
describe('Visibility checking:', function () {
it('should not accept a path which is not displayed', function () {
// Hide a path
svgTag.childNodes[1].style.display = 'none';
myVivus = new Vivus(svgTag, {ignoreInvisible: true});
expect(myVivus.map.length).toEqual(5);
});
it('should not accept a path which with an ignore tag', function () {
svgTag.childNodes[1].setAttribute('data-ignore', 'true');
myVivus = new Vivus(svgTag);
expect(myVivus.map.length).toEqual(5);
});
it('should not accept a path which is not displayed', function () {
svgTag.childNodes[1].setAttribute('data-ignore', 'false');
myVivus = new Vivus(svgTag);
expect(myVivus.map.length).toEqual(6);
});
});
// Drawing
describe('Drawing:', function () {
it('should call the callback once the animation is finished', function () {
var done = false;
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'autostart'
}, function () {
done = true;
});
triggerFrames();
expect(done).toBe(true);
});
it('should call the callback once the reverse animation is finished', function () {
var done = false;
myVivus = new Vivus(svgTag, {
type: 'oneByOne',
duration: 6
}, function () {
done = true;
});
myVivus.finish().play(-1);
triggerFrames();
expect(done).toBe(true);
});
it('should call the method callback as the second param once the animation is finished', function () {
var done = false;
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'manual',
});
myVivus.play(1, function() {
done = true;
});
triggerFrames();
expect(done).toBe(true);
});
it('should call the method callback as the first param once the animation is finished', function () {
var done = false;
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'manual',
});
myVivus.play(function() {
done = true;
});
triggerFrames();
expect(done).toBe(true);
});
it('should call the method callback once the reverse animation is finished', function () {
var done = false;
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'manual',
});
myVivus.finish().play(-1, function() {
done = true;
});
triggerFrames();
expect(done).toBe(true);
});
it('should call the method callback provided in the last play call', function () {
var done = false;
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'manual',
});
myVivus.finish().play(-1, function () {});
myVivus.play(function() {
done = true;
});
triggerFrames();
expect(done).toBe(true);
});
it('should call destroy method once the animation is finished', function () {
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'manual',
selfDestroy: true
});
myVivus.destroy = jasmine.createSpy('spy');
myVivus.play();
triggerFrames();
expect(myVivus.destroy.calls.count()).toEqual(1);
});
it('should\' call destroy method if selfDestroy option is not present', function () {
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'manual'
});
myVivus.destroy = jasmine.createSpy('spy');
myVivus.play();
triggerFrames();
expect(myVivus.destroy.calls.count()).toEqual(0);
});
it('should stop animation if destroy has been called', function () {
var callbackSpy = jasmine.createSpy('spy');
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'autostart'
}, callbackSpy);
triggerFrames(1);
myVivus.destroy();
triggerFrames();
expect(callbackSpy.calls.count()).toEqual(0);
});
it('should stop the animation once it reaches currentFrame == 0', function () {
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'manual'
});
myVivus.stop = jasmine.createSpy('spy');
myVivus.play(-1);
triggerFrames();
expect(myVivus.stop.calls.count()).toEqual(1);
});
it('should trace reasonably', function () {
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'manual'
});
spyOn(myVivus, 'trace').and.callThrough();
myVivus.play(0.5);
triggerFrames();
expect(myVivus.trace.calls.count()).toEqual(12);
});
it('should start by the last path if reverseStack is enabled', function () {
myVivus = new Vivus(svgTag, {
type: 'oneByOne',
duration: 5,
reverseStack: true
});
myVivus.setFrameProgress(0.5);
var paths = svgTag.querySelectorAll('path');
expect(+paths[0].style.strokeDashoffset).not.toEqual(0);
expect(+paths[paths.length -1].style.strokeDashoffset).toEqual(0);
});
});
describe('Force Render:', function () {
it('should use renderPath if forceRender option is set to true', function () {
myVivus = new Vivus(svgTag, { duration: 2, start: 'manual', forceRender: true });
var originalFirstPath = myVivus.map[0].el;
myVivus.renderPath(0);
expect(myVivus.map[0].el).not.toBe(originalFirstPath);
});
it('should not use renderPath if forceRender option is set to false', function () {
myVivus = new Vivus(svgTag, { duration: 2, start: 'manual', forceRender: false });
var originalFirstPath = myVivus.map[0].el;
myVivus.renderPath(0);
expect(myVivus.map[0].el).toBe(originalFirstPath);
});
it('renderPath should not throw an error if the index doesn\'t exists', function () {
myVivus = new Vivus(svgTag, { duration: 2, start: 'manual', forceRender: true });
expect(function () {
myVivus.renderPath(42);
}).not.toThrow();
});
});
});
describe('[controls]', function () {
beforeEach(function () {
myVivus = new Vivus(svgTag, {
type: 'oneByOne',
duration: 2,
start: 'manual'
});
});
it('shouldn\'t play if the parameter in incorrect', function () {
expect(function () {myVivus.play('a');}).toThrow(new Error('Vivus [play]: invalid speed'));
expect(function () {myVivus.play({});}).toThrow(new Error('Vivus [play]: invalid speed'));
expect(function () {myVivus.play([]);}).toThrow(new Error('Vivus [play]: invalid speed'));
expect(function () {myVivus.play('1');}).toThrow(new Error('Vivus [play]: invalid speed'));
});
it('should return the correct status', function () {
expect(myVivus.getStatus()).toEqual('start');
myVivus.setFrameProgress(0.5);
expect(myVivus.getStatus()).toEqual('progress');
myVivus.finish();
expect(myVivus.getStatus()).toEqual('end');
myVivus.reset();
expect(myVivus.getStatus()).toEqual('start');
});
it('should play with the normal speed by default', function () {
myVivus.play();
expect(myVivus.speed).toEqual(1);
});
it('shouldn\'t run another process of drawing if the animation is in progress', function () {
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'manual'
});
spyOn(myVivus, 'trace').and.callThrough();
myVivus.play(0.5);
myVivus.play(0.5);
triggerFrames();
expect(myVivus.trace.calls.count()).toEqual(12);
});
it('should stop the animation only when the animation is running', function () {
myVivus = new Vivus(svgTag, {
duration: 6,
start: 'manual'
});
myVivus.play();
expect(myVivus.handle).toBeTruthy();
myVivus.stop();
expect(myVivus.handle).toBeFalsy();
myVivus.stop();
expect(myVivus.handle).toBeFalsy();
});
it('should remove all unecessary styling on every path element', function () {
var i, paths;
myVivus.destroy();
paths = svgTag.querySelectorAll('path');
for (i = 0; i < paths.length; i++) {
expect(!!paths[i].style.strokeDashoffset).toEqual(false);
expect(!!paths[i].style.strokeDasharray).toEqual(false);
}
});
/**
* Where are the tests about `util` methods?
* Well....
* to be honest, I've been struggling a bit for these kind of tests
* which seems difficult to test from Karma.
*/
});
});

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Another Perspective</name>
<id>fablabchemnitz.de.another_perspective</id>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">another_perspective.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,312 @@
#!/usr/bin/env python3
"""
Copyright (C) 2017 Corentin Brulé
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Special thanks and orignal copyrigths : Aaron Spike (2005) and Timo Kähkönen (2012)
"""
import inkex
import re
from lxml import etree
from inkex.transforms import Transform
from inkex.paths import Path, CubicSuperPath
__version__ = '0.1'
debug=False
def distort_path(path_str,source,destination):
path_arr = path_string_to_array(path_str)
subpath_type=""
is_num =""
xy_counter =""
xy=""
path_arr2=[]
subpath_type_upper=""
point=""
i=0
for i in range(len(path_arr)):
patt1 = r"[mzlhvcsqta]"
curr = path_arr[i]
if re.match(patt1,curr,flags=re.I):
xy_counter = -1
subpath_type = curr
subpath_type_upper = subpath_type.upper()
is_num = False
path_arr2.append(curr)
else :
is_num = True
curr = float(curr)
if xy_counter%2 == 0:
xy="x"
else:
xy="y"
if is_num :
if xy=="y" :
point = transferPoint(float(path_arr[i-1]),curr,source,destination)
path_arr2.append(point["x"])
path_arr2.append(point["y"])
xy_counter+=1
path_str = path_array_to_string(path_arr2)
return path_str
def path_array_to_string(path_arr):
path_str=str(path_arr)
path_str=path_str.replace(r"([0-9]),([-0-9])", "$1 $2")
path_str=path_str.replace(r"([0-9]),([-0-9])", "$1 $2")
path_str=path_str.replace(",", "")
path_str=path_str.replace("[", "").replace("]","")
path_str=path_str.replace("'", "")
return path_str
def path_string_to_array(path_str):
patt1=r"[mzlhvcsqta]|-?[0-9.]+" #gi
#path_arr=path_str.match(patt1) #array de résultats
path_arr = re.findall(patt1,path_str,flags=re.I)
patt1=r"[mzlhvcsqta]" #i
i = 0
for i in range(len(path_arr)):
if re.match(path_arr[i],patt1,flags=re.I) == -1:
path_arr[i] = float(path_arr[i])
return path_arr
'''
def isPermissible(p):
p0 = {x:c0.attr("cx"),y:c0.attr("cy")}
p1 = {x:c1.attr("cx"),y:c1.attr("cy")}
p2 = {x:c2.attr("cx"),y:c2.attr("cy")}
p3 = {x:c3.attr("cx"),y:c3.attr("cy")}
a0 = angle(p3, p0, p1)
a1 = angle(p0, p1, p2)
a2 = angle(p1, p2, p3)
a3 = angle(p2, p3, p0)
if not (a0 > 0 and a0 < 180) or not (a1 > 0 and a1 < 180) or not(a2 > 0 and a2 < 180) or not(a3 > 0 and a3 < 180) :
return False
else :
return True
}
def angle(c, b, a):
ab = {x: b.x - a.x, y: b.y - a.y }
cb = {x: b.x - c.x, y: b.y - c.y }
dot = (ab.x * cb.x + ab.y * cb.y)
cross = (ab.x * cb.y - ab.y * cb.x)
alpha = Math.atan2(cross, dot)
return alpha * 180 / PI
}
'''
def transferPoint (xI, yI, source, destination):
ADDING = 0.001 # to avoid dividing by zero
xA = source[0]["x"]
yA = source[0]["y"]
xC = source[2]["x"]
yC = source[2]["y"]
xAu = destination[0]["x"]
yAu = destination[0]["y"]
xBu = destination[1]["x"]
yBu = destination[1]["y"]
xCu = destination[2]["x"]
yCu = destination[2]["y"]
xDu = destination[3]["x"]
yDu = destination[3]["y"]
# Calcultations
if xBu==xCu :
xCu+=ADDING
if xAu==xDu :
xDu+=ADDING
if xAu==xBu :
xBu+=ADDING
if xDu==xCu :
xCu+=ADDING
kBC = float(yBu-yCu)/float(xBu-xCu)
kAD = float(yAu-yDu)/float(xAu-xDu)
kAB = float(yAu-yBu)/float(xAu-xBu)
kDC = float(yDu-yCu)/float(xDu-xCu)
if kBC==kAD :
kAD += ADDING
xE = float(kBC*xBu - kAD*xAu + yAu - yBu) / float(kBC-kAD)
yE = kBC*(xE - xBu) + yBu
if kAB==kDC :
kDC += ADDING
xF = float(kAB*xBu - kDC*xCu + yCu - yBu) / float(kAB-kDC)
yF = kAB*(xF - xBu) + yBu
if xE==xF :
xF += ADDING
kEF = float(yE-yF) / float(xE-xF)
if kEF==kAB:
kAB += ADDING
xG = float(kEF*xDu - kAB*xAu + yAu - yDu) / float(kEF-kAB)
yG = kEF*(xG - xDu) + yDu
if kEF==kBC :
kBC+=ADDING
xH = float(kEF*xDu - kBC*xBu + yBu - yDu) / float(kEF-kBC)
yH = kEF*(xH - xDu) + yDu
rG = float(yC-yI)/float(yC-yA)
rH = float(xI-xA)/float(xC-xA)
xJ = (xG-xDu)*rG + xDu
yJ = (yG-yDu)*rG + yDu
xK = (xH-xDu)*rH + xDu
yK = (yH-yDu)*rH + yDu
if xF==xJ:
xJ+=ADDING
if xE==xK:
xK+=ADDING
kJF = float(yF-yJ) / float(xF-xJ)
kKE = float(yE-yK) / float(xE-xK)
xKE = ""
if kJF==kKE:
kKE += ADDING
xIu = float(kJF*xF - kKE*xE + yE - yF) / float(kJF-kKE)
yIu = kJF * (xIu - xJ) + yJ
b = {"x":xIu,"y":yIu}
b["x"] = round(b["x"])
b["y"] = round(b["y"])
return b
def projection(path_object,coords):
pp_object = Path(path_object).to_arrays()
bounds = Path(path_object).bounding_box()
# Make array of coordinates, every array member represent corner of text path
source = [
{"x":bounds.left,"y":bounds.top},
{"x":bounds.right,"y":bounds.top},
{"x":bounds.right,"y":bounds.bottom},
{"x":bounds.left,"y":bounds.bottom}
]
destination=[
{"x":coords[0][0],"y":coords[0][1]},
{"x":coords[1][0],"y":coords[1][1]},
{"x":coords[2][0],"y":coords[2][1]},
{"x":coords[3][0],"y":coords[3][1]}
]
path_destination = distort_path(path_object,source,destination)
return path_destination
'''
def complex2tulpe(complexNb):
return (complexNb.real,complexNb.imag)
'''
class AnotherPerspective(inkex.EffectExtension):
def envelope2coords(self, path_envelope):
pp_envelope = CubicSuperPath(path_envelope)
if len(pp_envelope[0]) < 4:
inkex.errormsg("The selected envelope (your second path) does not contain enough nodes. Check to have at least 4 nodes.")
exit()
c0 = pp_envelope[0][0][0]
c1 = pp_envelope[0][1][0]
c2 = pp_envelope[0][2][0]
c3 = pp_envelope[0][3][0]
# inkex.debug(str(c0)+" "+str(c1)+" "+str(c2)+" "+str(c3))
return [c0, c1, c2, c3]
def effect(self):
if len(self.options.ids) < 2:
inkex.errormsg("This extension requires two selected paths.")
exit()
obj = self.svg.selected[self.options.ids[0]]
envelope = self.svg.selected[self.options.ids[1]]
if obj.get(inkex.addNS('type','sodipodi')):
inkex.errormsg("The first selected object is of type '%s'.\nTry using the procedure Path->Object to Path." % obj.get(inkex.addNS('type','sodipodi')))
exit()
if obj.tag == inkex.addNS('path','svg') or obj.tag == inkex.addNS('g','svg'):
if envelope.tag == inkex.addNS('path','svg'):
mat = envelope.transform * Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
path = CubicSuperPath(envelope.get('d'))
Path(path).transform(mat)
absolute_envelope_path = envelope.get('d')
# inkex.debug(absolute_envelope_path)
coords_to_project = self.envelope2coords(absolute_envelope_path)
if obj.tag == inkex.addNS('path','svg'):
mat = obj.transform * Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
absolute_d = str(Path(obj.get('d')))
path = CubicSuperPath(absolute_d)
Path(path).transform(mat)
absolute_object_path = str(path)
# inkex.debug(absolute_object_path)
elif obj.tag == inkex.addNS('g','svg'):
absolute_object_path=""
for p in obj.iterfind(".//{http://www.w3.org/2000/svg}path"):
absolute_d = str(Path(p.get('d')))
mat = p.transform * Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
path = CubicSuperPath(absolute_d)
Path(path).transform(mat)
absolute_object_path += str(Path(path))
# inkex.debug(absolute_object_path)
new_path = projection(absolute_object_path,coords_to_project)
attributes = {'d':new_path}
new_element = etree.SubElement(self.svg.get_current_layer(),inkex.addNS('path','svg'),attributes)
else:
if envelope.tag == inkex.addNS('g','svg'):
inkex.errormsg("The second selected object is a group, not a path.\nTry using the procedure Object->Ungroup.")
else:
inkex.errormsg("The second selected object is not a path.\nTry using the procedure Path->Object to Path.")
exit()
else:
inkex.errormsg("The first selected object is not a path.\nTry using the procedure Path->Object to Path.")
exit()
if __name__ == '__main__':
AnotherPerspective().run()

View File

@ -0,0 +1,194 @@
#!/usr/bin/env python3
# Generate Apollonian Gaskets -- the math part.
# Copyright (c) 2014 Ludger Sandig
# This file is part of apollon.
# Apollon is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# Apollon is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with Apollon. If not, see <http://www.gnu.org/licenses/>.
from cmath import *
import random
class Circle(object):
"""
A circle represented by center point as complex number and radius.
"""
def __init__ ( self, mx, my, r ):
"""
@param mx: x center coordinate
@type mx: int or float
@param my: y center coordinate
@type my: int or float
@param r: radius
@type r: int or float
"""
self.r = r
self.m = (mx +my*1j)
def __repr__ ( self ):
"""
Pretty printing
"""
return "Circle( self, %s, %s, %s )" % (self.m.real, self.m.imag, self.r)
def __str__ ( self ):
"""
Pretty printing
"""
return "Circle x:%.3f y:%.3f r:%.3f [cur:%.3f]" % (self.m.real, self.m.imag, self.r.real, self.curvature().real)
def curvature (self):
"""
Get circle's curvature.
@rtype: float
@return: Curvature of the circle.
"""
return 1/self.r
def outerTangentCircle( circle1, circle2, circle3 ):
"""
Takes three externally tangent circles and calculates the fourth one enclosing them.
@param circle1: first circle
@param circle2: second circle
@param circle3: third circle
@type circle1: L{Circle}
@type circle2: L{Circle}
@type circle3: L{Circle}
@return: The enclosing circle
@rtype: L{Circle}
"""
cur1 = circle1.curvature()
cur2 = circle2.curvature()
cur3 = circle3.curvature()
m1 = circle1.m
m2 = circle2.m
m3 = circle3.m
cur4 = -2 * sqrt( cur1*cur2 + cur2*cur3 + cur1 * cur3 ) + cur1 + cur2 + cur3
m4 = ( -2 * sqrt( cur1*m1*cur2*m2 + cur2*m2*cur3*m3 + cur1*m1*cur3*m3 ) + cur1*m1 + cur2*m2 + cur3*m3 ) / cur4
circle4 = Circle( m4.real, m4.imag, 1/cur4 )
return circle4
def tangentCirclesFromRadii( r2, r3, r4 ):
"""
Takes three radii and calculates the corresponding externally
tangent circles as well as a fourth one enclosing them. The enclosing
circle is the first one.
@param r2, r3, r4: Radii of the circles to calculate
@type r2: int or float
@type r3: int or float
@type r4: int or float
@return: The four circles, where the first one is the enclosing one.
@rtype: (L{Circle}, L{Circle}, L{Circle}, L{Circle})
"""
circle2 = Circle( 0, 0, r2 )
circle3 = Circle( r2 + r3, 0, r3 )
m4x = (r2*r2 + r2*r4 + r2*r3 - r3*r4) / (r2 + r3)
m4y = sqrt( (r2 + r4) * (r2 + r4) - m4x*m4x )
circle4 = Circle( m4x, m4y, r4 )
circle1 = outerTangentCircle( circle2, circle3, circle4 )
return ( circle1, circle2, circle3, circle4 )
def secondSolution( fixed, c1, c2, c3 ):
"""
If given four tangent circles, calculate the other one that is tangent
to the last three.
@param fixed: The fixed circle touches the other three, but not
the one to be calculated.
@param c1, c2, c3: Three circles to which the other tangent circle
is to be calculated.
@type fixed: L{Circle}
@type c1: L{Circle}
@type c2: L{Circle}
@type c3: L{Circle}
@return: The circle.
@rtype: L{Circle}
"""
curf = fixed.curvature()
cur1 = c1.curvature()
cur2 = c2.curvature()
cur3 = c3.curvature()
curn = 2 * (cur1 + cur2 + cur3) - curf
mn = (2 * (cur1*c1.m + cur2*c2.m + cur3*c3.m) - curf*fixed.m ) / curn
return Circle( mn.real, mn.imag, 1/curn )
class ApollonianGasket(object):
"""
Container for an Apollonian Gasket.
"""
def __init__(self, c1, c2, c3):
"""
Creates a basic apollonian Gasket with four circles.
@param c1, c2, c3: The curvatures of the three inner circles of the
starting set (i.e. depth 0 of the recursion). The fourth,
enclosing circle will be calculated from them.
@type c1: int or float
@type c2: int or float
@type c3: int or float
"""
self.start = tangentCirclesFromRadii( 1/c1, 1/c2, 1/c3 )
self.genCircles = list(self.start)
def recurse(self, circles, depth, maxDepth):
"""Recursively calculate the smaller circles of the AG up to the
given depth. Note that for depth n we get 2*3^{n+1} circles.
@param maxDepth: Maximal depth of the recursion.
@type maxDepth: int
@param circles: 4-Tuple of circles for which the second
solutions are calculated
@type circles: (L{Circle}, L{Circle}, L{Circle}, L{Circle})
@param depth: Current depth
@type depth: int
"""
if( depth == maxDepth ):
return
(c1, c2, c3, c4) = circles
if( depth == 0 ):
# First recursive step, this is the only time we need to
# calculate 4 new circles.
del self.genCircles[4:]
cspecial = secondSolution( c1, c2, c3, c4 )
self.genCircles.append( cspecial )
self.recurse( (cspecial, c2, c3, c4), 1, maxDepth )
cn2 = secondSolution( c2, c1, c3, c4 )
self.genCircles.append( cn2 )
cn3 = secondSolution( c3, c1, c2, c4 )
self.genCircles.append( cn3 )
cn4 = secondSolution( c4, c1, c2, c3 )
self.genCircles.append( cn4 )
self.recurse( (cn2, c1, c3, c4), depth+1, maxDepth )
self.recurse( (cn3, c1, c2, c4), depth+1, maxDepth )
self.recurse( (cn4, c1, c2, c3), depth+1, maxDepth )
def generate(self, depth):
"""
Wrapper for the recurse function. Generate the AG,
@param depth: Recursion depth of the Gasket
@type depth: int
"""
self.recurse(self.start, 0, depth)

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Apollonian Gasket</name>
<id>fablabchemnitz.de.apollonian_gasket</id>
<param name="active_tab" type="notebook">
<page name="settings" gui-text="Settings">
<param name="depth" type="int" min="2" max="10" gui-text="Depth" gui-description="Warning: high values might calculate really long!">3</param>
<param name="c1" type="float" min="0.1" max="10.0" precision="2" gui-text="c1">2.0</param>
<param name="c2" type="float" min="0.1" max="10.0" precision="2" gui-text="c2">3.0</param>
<param name="c3" type="float" min="0.1" max="10.0" precision="2" gui-text="c3">3.0</param>
<param name="shrink" type="bool" gui-text="shrink circles for cutting">true</param>
<param name="as_paths" type="bool" gui-text="draw svg:path instead svg:circle elements">true</param>
</page>
<page name="Usage" gui-text="Usage">
<label xml:space="preserve">Make an apollonian gasket:
Depth = depth in search tree
c1,c2,c3 = curvatures of first 3 osculating circles</label>
<label appearance="url">https://en.wikipedia.org/wiki/Apollonian_gasket</label>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from Generator"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">apollonian_gasket.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,101 @@
#!/usr/bin/env python3
import inkex
import apolloniangasket_func
from lxml import etree
__version__ = '0.0'
def cplxs2pts(zs):
tt = []
for z in zs:
tt.extend([z.real,z.imag])
return tt
def draw_SVG_circle(parent, r, cx, cy, name):
" structure an SVG circle entity under parent "
circ_attribs = { 'cx': str(cx), 'cy': str(cy),
'r': str(r),
inkex.addNS('label','inkscape'): name}
circle = etree.SubElement(parent, inkex.addNS('circle','svg'), circ_attribs)
def draw_SVG_circleAsPath(parent, r, cx, cy, name):
circ_attribs = {
"d": "M {:0.6f}, {:0.6f} a {:0.6f},{:0.6f} 0 1,0 {:0.6f},0 a {:0.6f},{:0.6f} 0 1,0 {:0.6f},0".format(
cx - r, cy, r, r, 2*r, r, r, -2*r),
inkex.addNS('label','inkscape'): name}
circle = etree.SubElement(parent, inkex.addNS('path','svg'), circ_attribs)
class Gasket(inkex.EffectExtension): # choose a better name
def add_arguments(self, pars):
pars.add_argument("--active_tab")
pars.add_argument("--depth",type=int, default=3)
pars.add_argument("--c1", type=float, default=2.0)
pars.add_argument("--c2", type=float, default=3.0)
pars.add_argument("--c3", type=float, default=3.0)
pars.add_argument("--shrink", type=inkex.Boolean, default=True)
pars.add_argument("--as_paths", type=inkex.Boolean, default=True)
def calc_unit_factor(self):
unit_factor = self.svg.unittouu(str(1.0) + self.options.units)
return unit_factor
### -------------------------------------------------------------------
### Main function and is called when the extension is run.
def effect(self):
#set up path styles
path_stroke = '#DD0000' # take color from tab3
path_fill = 'none' # no fill - just a line
path_stroke_width = self.svg.unittouu(str(0.1) + "mm")
page_id = self.options.active_tab # sometimes wrong the very first time
style_curve = { 'stroke': path_stroke,
'fill': 'none',
'stroke-width': path_stroke_width }
# This finds center of current view in inkscape
t = 'translate(%s,%s)' % (self.svg.namedview.center[0], self.svg.namedview.center[1])
# add a group to the document's current layer
#all the circles inherit style from this group
g_attribs = { inkex.addNS('label','inkscape'): 'zengon' + "_%d"%(self.options.depth),
inkex.addNS('transform-center-x','inkscape'): str(0),
inkex.addNS('transform-center-y','inkscape'): str(0),
'transform': t,
'style' : str(inkex.Style((style_curve))),
'info':'N: '}
topgroup = etree.SubElement(self.svg.get_current_layer(), 'g', g_attribs)
circles = apolloniangasket_func.main(c1=self.options.c1,
c2=self.options.c2,
c3=self.options.c3,
depth=self.options.depth)
#shrink the circles so they don't touch
#useful for laser cutting
if self.options.shrink:
circles = circles[1:]
for cc in circles:
cc.r = abs(cc.r)
if cc.r >.5:
cc.r -= .1
else:
cc.r *= .9
scale_factor = 200
for c in circles:
cx, cy, r = c.m.real, c.m.imag, abs(c.r)
#rescale and add circle to document
cx, cy, r = scale_factor * cx , scale_factor * cy, scale_factor * r
if self.options.as_paths is False:
draw_SVG_circle(topgroup, r, cx, cy, 'apollian')
else:
draw_SVG_circleAsPath(topgroup, r, cx, cy, 'apollian')
if __name__ == '__main__':
Gasket().run()

View File

@ -0,0 +1,112 @@
#!/usr/bin/env python3
# Command line program to create svg apollonian circles
# Copyright (c) 2014 Ludger Sandig
# This file is part of apollon.
# Apollon is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# Apollon is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with Apollon. If not, see <http://www.gnu.org/licenses/>.
import math
from apollon import ApollonianGasket
def ag_to_svg(circles, colors, tresh=0.00005):
"""
Convert a list of circles to svg, optionally color them.
@param circles: A list of L{Circle}s
@param colors: A L{ColorMap} object
@param tresh: Only circles with a radius greater than the product of tresh and maximal radius are saved
"""
svg = []
tresh = .000005
print ('>>', tresh)
# Find the biggest circle, which hopefully is the enclosing one
# and has a negative radius because of this. Note that this does
# not have to be the case if we picked an unlucky set of radii at
# the start. If that was the case, we're screwed now.
big = min(circles, key=lambda c: c.r.real)
# Move biggest circle to front so it gets drawn first
circles.remove(big)
circles.insert(0, big)
if big.r.real < 0:
# Bounding box from biggest circle, lower left corner and two
# times the radius as width
corner = big.m - ( abs(big.r) + abs(big.r) * 1j )
vbwidth = abs(big.r)*2
width = 500 # Hardcoded!
# Line width independent of circle size
lw = (vbwidth/width)
svg.append('<svg xmlns="http://www.w3.org/2000/svg" width="%f" height="%f" viewBox="%f %f %f %f">\n' % (width, width, corner.real, corner.imag, vbwidth, vbwidth))
# Keep stroke width relative
svg.append('<g stroke-width="%f">\n' % lw)
# Iterate through circle list, circles with radius<radmin
# will not be saved because they are too small for printing.
radmin = tresh * abs(big.r)
print(radmin)
for c in circles:
if abs(c.r) > radmin:
fill = colors.color_for(abs(c.r))
svg.append(( '<circle cx="%f" cy="%f" r="%f" fill="%s" stroke="black"/>\n' % (c.m.real, c.m.imag, abs(c.r), fill)))
svg.append('</g>\n')
svg.append('</svg>\n')
return ''.join(svg)
def impossible_combination(c1, c2, c3):
# If any curvatures x, y, z satisfy the equation
# x = 2*sqrt(y*z) + y + z
# then no fourth enclosing circle can be genereated, because it
# would be a line.
# We need to see for c1, c2, c3 if they could be "x".
impossible = False
sets = [(c1,c2,c3), (c2,c3,c1), (c3,c1,c2)]
for (x, y, z) in sets:
if x == 2*math.sqrt(y*z) + y + z:
impossible = True
return impossible
def main(c1=3.,c2=2.,c3=2.,depth=5):
# Sanity checks
for c in [c1, c2,c3]:
if c == 0:
print("Error: curvature or radius can't be 0")
exit(1)
if impossible_combination(c1, c2, c3):
print("Error: no apollonian gasket possible for these curvatures")
exit(1)
ag = ApollonianGasket(c1, c2, c3)
ag.generate(depth)
# Get smallest and biggest radius
smallest = abs(min(ag.genCircles, key=lambda c: abs(c.r.real)).r.real)
biggest = abs(max(ag.genCircles, key=lambda c: abs(c.r.real)).r.real)
return ag.genCircles

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Apply Transformations</name>
<id>fablabchemnitz.de.apply_transformations</id>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">apply_transformations.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,206 @@
#!/usr/bin/env python3
#
# License: GPL2
# Copyright Mark "Klowner" Riedesel
# https://github.com/Klowner/inkscape-applytransforms
#
import copy
import math
from lxml import etree
import inkex
from inkex.paths import CubicSuperPath, Path
from inkex.transforms import Transform
from inkex.styles import Style
NULL_TRANSFORM = Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
class ApplyTransformations(inkex.EffectExtension):
def effect(self):
if self.svg.selected:
for id, shape in self.svg.selected.items():
self.recursiveFuseTransform(shape)
else:
self.recursiveFuseTransform(self.document.getroot())
@staticmethod
def objectToPath(element):
if element.tag == inkex.addNS('g', 'svg'):
return element
if element.tag == inkex.addNS('path', 'svg') or element.tag == 'path':
for attName in element.attrib.keys():
if ("sodipodi" in attName) or ("inkscape" in attName):
del element.attrib[attName]
return element
return element
def scaleStrokeWidth(self, element, transf):
if 'style' in element.attrib:
style = element.attrib.get('style')
style = dict(Style.parse_str(style))
update = False
if 'stroke-width' in style:
try:
stroke_width = float(style.get('stroke-width').strip().replace("px", ""))
stroke_width *= math.sqrt(abs(transf.a * transf.d))
style['stroke-width'] = str(stroke_width)
update = True
except AttributeError:
pass
if update:
element.attrib['style'] = Style(style).to_str()
def recursiveFuseTransform(self, element, transf=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
transf = Transform(transf) * Transform(element.get("transform", None)) #a, b, c, d = linear transformations / e, f = translations
if 'transform' in element.attrib:
del element.attrib['transform']
element = ApplyTransformations.objectToPath(element)
if transf == NULL_TRANSFORM:
# Don't do anything if there is effectively no transform applied
# reduces alerts for unsupported elements
pass
elif 'd' in element.attrib:
d = element.get('d')
p = CubicSuperPath(d)
p = Path(p).to_absolute().transform(transf, True)
element.set('d', str(Path(CubicSuperPath(p).to_path())))
self.scaleStrokeWidth(element, transf)
elif element.tag in [inkex.addNS('polygon', 'svg'),
inkex.addNS('polyline', 'svg')]:
points = element.get('points')
points = points.strip().split(' ')
for k, p in enumerate(points):
if ',' in p:
p = p.split(',')
p = [float(p[0]), float(p[1])]
p = transf.apply_to_point(p)
p = [str(p[0]), str(p[1])]
p = ','.join(p)
points[k] = p
points = ' '.join(points)
element.set('points', points)
self.scaleStrokeWidth(element, transf)
elif element.tag in [inkex.addNS("ellipse", "svg"), inkex.addNS("circle", "svg")]:
def isequal(a, b):
return abs(a - b) <= transf.absolute_tolerance
if element.TAG == "ellipse":
rx = float(element.get("rx"))
ry = float(element.get("ry"))
else:
rx = float(element.get("r"))
ry = rx
cx = float(element.get("cx"))
cy = float(element.get("cy"))
sqxy1 = (cx - rx, cy - ry)
sqxy2 = (cx + rx, cy - ry)
sqxy3 = (cx + rx, cy + ry)
newxy1 = transf.apply_to_point(sqxy1)
newxy2 = transf.apply_to_point(sqxy2)
newxy3 = transf.apply_to_point(sqxy3)
element.set("cx", (newxy1[0] + newxy3[0]) / 2)
element.set("cy", (newxy1[1] + newxy3[1]) / 2)
edgex = math.sqrt(
abs(newxy1[0] - newxy2[0]) ** 2 + abs(newxy1[1] - newxy2[1]) ** 2
)
edgey = math.sqrt(
abs(newxy2[0] - newxy3[0]) ** 2 + abs(newxy2[1] - newxy3[1]) ** 2
)
if not isequal(edgex, edgey) and (
element.TAG == "circle"
or not isequal(newxy2[0], newxy3[0])
or not isequal(newxy1[1], newxy2[1])
):
inkex.utils.errormsg(
"Warning: Shape %s (%s) is approximate only, try Object to path first for better results"
% (element.TAG, element.get("id"))
)
if element.TAG == "ellipse":
element.set("rx", edgex / 2)
element.set("ry", edgey / 2)
else:
element.set("r", edgex / 2)
# this is unstable at the moment
elif element.tag == inkex.addNS("use", "svg"):
href = None
old_href_key = '{http://www.w3.org/1999/xlink}href'
new_href_key = 'href'
if element.attrib.has_key(old_href_key) is True: # {http://www.w3.org/1999/xlink}href (which gets displayed as 'xlink:href') attribute is deprecated. the newer attribute is just 'href'
href = element.attrib.get(old_href_key)
#element.attrib.pop(old_href_key)
if element.attrib.has_key(new_href_key) is True:
href = element.attrib.get(new_href_key) #we might overwrite the previous deprecated xlink:href but it's okay
#element.attrib.pop(new_href_key)
#get the linked object from href attribute
linkedObject = self.document.getroot().xpath("//*[@id = '%s']" % href.lstrip('#')) #we must remove hashtag symbol
linkedObjectCopy = copy.copy(linkedObject[0])
objectType = linkedObject[0].tag
if objectType == inkex.addNS("image", "svg"):
mask = None #image might have an alpha channel
new_mask_id = self.svg.get_unique_id("mask")
newMask = None
if element.attrib.has_key('mask') is True:
mask = element.attrib.get('mask')
#element.attrib.pop('mask')
#get the linked mask from mask attribute. We remove the old and create a new
if mask is not None:
linkedMask = self.document.getroot().xpath("//*[@id = '%s']" % mask.lstrip('url(#').rstrip(')')) #we must remove hashtag symbol
linkedMask[0].delete()
maskAttributes = {'id': new_mask_id}
newMask = etree.SubElement(self.document.getroot(), inkex.addNS('mask', 'svg'), maskAttributes)
width = float(linkedObjectCopy.get('width')) * transf.a
height = float(linkedObjectCopy.get('height')) * transf.d
linkedObjectCopy.set('width', '{:1.6f}'.format(width))
linkedObjectCopy.set('height', '{:1.6f}'.format(height))
linkedObjectCopy.set('x', '{:1.6f}'.format(transf.e))
linkedObjectCopy.set('y', '{:1.6f}'.format(transf.f))
if newMask is not None:
linkedObjectCopy.set('mask', 'url(#' + new_mask_id + ')')
maskRectAttributes = {'x': '{:1.6f}'.format(transf.e), 'y': '{:1.6f}'.format(transf.f), 'width': '{:1.6f}'.format(width), 'height': '{:1.6f}'.format(height), 'style':'fill:#ffffff;'}
maskRect = etree.SubElement(newMask, inkex.addNS('rect', 'svg'), maskRectAttributes)
self.document.getroot().append(linkedObjectCopy) #for each svg:use we append a copy to the document root
element.delete() #then we remove the use object
else:
#self.recursiveFuseTransform(linkedObjectCopy, transf)
self.recursiveFuseTransform(element.unlink(), transf)
elif element.tag in [inkex.addNS('rect', 'svg'),
inkex.addNS('text', 'svg'),
inkex.addNS('image', 'svg')]:
inkex.utils.errormsg(
"Shape %s (%s) not yet supported, try Object to path first"
% (element.TAG, element.get("id"))
)
else:
# e.g. <g style="...">
self.scaleStrokeWidth(element, transf)
for child in element.getchildren():
self.recursiveFuseTransform(child, transf)
if __name__ == '__main__':
ApplyTransformations().run()

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Archimedes Spiral</name>
<id>fablabchemnitz.de.archimedes_spiral</id>
<param name="tab" type="notebook">
<page name="tab_settings" gui-text="Settings">
<label appearance="header">R = r + aθ</label>
<param name="r" type="int" min="0" max="1000000" gui-text="r (mm)">50</param>
<param name="a" type="float" min="0" max="1000000" gui-text="a">3</param>
<param name="step" type="int" min="1" max="300" gui-text="Step" gui-description="The higher the value the better the accuracy. If you set the step value really low the resulting length will not be precise and the curve is maybe not drawn at all">50</param>
<param name="trl" type="optiongroup" appearance="combo" gui-text="Turn direction">
<option value="0">Left</option>
<option value="1">Right</option>
</param>
<param name="length" type="float" min="0" max="1000000" gui-text="Length (mm)">0</param>
<param name="turns" type="int" min="1" max="1000000" gui-text="Turns" gui-description="Works only if you set 'length (mm)' to 0.0">5</param>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">Archimedes Spiral</label>
<label>2020 - 2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/archimedesspiral</label>
<spacer/>
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer/>
<label appearance="header">MightyScape Extension Collection</label>
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
</page>
<page name="tab_donate" gui-text="Donate">
<label appearance="header">Coffee + Pizza</label>
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
<spacer/>
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer/>
<label>Thanks for using our extension and helping us!</label>
<image>../000_about_fablabchemnitz.svg</image>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from Generator"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">archimedes_spiral.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
Copyright (C) 2017 Panagiotis Loukas
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WAphiANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WAphiANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Version 0.2
This script was written by Panagiotis Loukas to make spiral easy.
It simply,
Have fun :)
PS.
Written on Arch.
"""
import inkex
from lxml import etree
from math import cos, sin, pi, log, sqrt
class Archimedes(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument('--tab')
pars.add_argument('--r', type = int, default = '50')
pars.add_argument('--a', type = float, default = '3')
pars.add_argument('--step', type = int, default = '50')
pars.add_argument('--trl', default = '1')
pars.add_argument('--turns', type = float, default = '5')
pars.add_argument('--length', type = float, default = '500')
def effect(self):
th = pi / 3
a = self.options.a
r = self.options.r
length = self.options.length
if length > 0:
turns = self.angle(a, r, length, th) / (2 * pi)
else:
turns = self.options.turns
if self.options.trl == '1':
step = -self.options.step
else:
step = self.options.step
layer = etree.SubElement(self.document.getroot(),'g')
path = etree.Element(inkex.addNS('path','svg'))
path.set('d', self.built(r, step, a, turns))
path.set('style',"fill:none;stroke:#000000;stroke-width:1px;stroke-opacity:1")
layer.append(path)
def built(self, r0, st = 4, a = 4, k = 1, th = 0):
step = 2 * pi / st
r = r0
s = "M " + str(r * cos(th)) + ", " + str(-r * sin(th))
for i in range(0, int(k * (abs(st)))):
prin = th + i * step
meta = th + (i + 1) * step
rp = r0 + abs(a * prin)# instead we put the absolute value the spiral will drift inwards
rm = r0 + abs(a * meta)# at the absolute price closes outwards
s += "a " + str(rm) + "," + str(rm) + " 0 0," + self.options.trl + " " + str(-rp * cos(prin) + rm * cos(meta)) + "," + str(rp * sin(prin) -rm * sin(meta))
return s
# see https://mathepedia.de/Archimedische_Spirale.html for formula of total arc length
def spirallength(self, a, r0, th):
phi = (r0 / a) + th
phi_sqrt = sqrt(phi ** 2 + 1)
return (a / 2) * (phi * phi_sqrt + log(phi + phi_sqrt))
def ds(self, a, r0, th):
return self.spirallength(a, r0, th) - self.spirallength(a, r0, 0)
def angle(self, a, r0, length, th):
i = 0.0
while (True):
ls=self.ds(a, r0, i)
if length - ls > 100:
i += 0.01
elif length - ls > 10: i += 0.001
elif length - ls > 1: i += 0.0001
elif length - ls > 0.1: i += 0.00001
elif length - ls > 0.01: i += 0.000001
else: break
return i
if __name__ == '__main__':
Archimedes().run()

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Barrel Distortion</name>
<id>fablabchemnitz.de.barrel_distorsion</id>
<param name="lambda_coef" type="float" min="-1000.0" max="0.0" precision="2" gui-text="Lambda parameter">-1.0</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">barrel_distorsion.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,100 @@
import math
import re
import inkex
from inkex import bezier
from inkex.paths import Path, CubicSuperPath
class BarrelDistorsion(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--lambda_coef", type=float, default=-5.0, help="command line help")
def distort_coordinates(self, x, y):
"""Method applies barrel distorsion to given points with distorsion center in center of image, selected to
Args:
x (float): X coordinate of given point
y (float): Y coordinate of given point
Returns:
tuple(float, float): Tuple with X,Y distorted coordinates of given point
"""
x_u = (x - self.x_c) / (self.width + self.height)
y_u = (y - self.y_c) / (self.width + self.height)
x_d = x_u / 2 / (self.q * y_u**2 + x_u**2 * self.q) * (1 - math.sqrt(1 - 4 * self.q * y_u**2 - 4 * x_u**2 * self.q))
y_d = y_u / 2 / (self.q * y_u**2 + x_u**2 * self.q) * (1 - math.sqrt(1 - 4 * self.q * y_u**2 - 4 * x_u**2 * self.q))
x_d *= self.width + self.height
y_d *= self.width + self.height
x_d += self.x_c
y_d += self.y_c
return x_d, y_d
def split_into_nodes(self, nodes_number=1000):
for id, node in self.svg.selected.items():
if node.tag == inkex.addNS('path', 'svg'):
p = CubicSuperPath(node.get('d'))
new = []
for sub in p:
new.append([sub[0][:]])
i = 1
while i <= len(sub) - 1:
length = bezier.cspseglength(
new[-1][-1], sub[i])
splits = nodes_number
for s in range(int(splits), 1, -1):
new[-1][-1], next, sub[
i] = bezier.cspbezsplitatlength(
new[-1][-1], sub[i], 1.0 / s)
new[-1].append(next[:])
new[-1].append(sub[i])
i += 1
node.set('d', str(CubicSuperPath(new)))
def effect(self):
if re.match(r'g\d+',
list(self.svg.selected.items())[0][0]) is not None:
raise SystemExit(
"You are trying to distort group of objects.\n This extension works only with path objects due to Inkscape API restrictions.\n Ungroup your objects and try again."
)
self.split_into_nodes()
self.q = self.options.lambda_coef
if self.q == 0.0:
inkex.errormsg("Invalid lambda coefficient. May not be exactly zero.")
return
nodes = []
for id, node in self.svg.selected.items():
if node.tag == inkex.addNS('path', 'svg'):
path = Path(node.get('d')).to_arrays()
nodes += path
nodes_filtered = [x for x in nodes if x[0] != 'Z']
x_coordinates = [x[-1][-2] for x in nodes_filtered]
y_coordinates = [y[-1][-1] for y in nodes_filtered]
self.width = max(x_coordinates) - min(x_coordinates)
self.height = max(y_coordinates) - min(y_coordinates)
self.x_c = sum(x_coordinates) / len(x_coordinates)
self.y_c = sum(y_coordinates) / len(y_coordinates)
for id, node in self.svg.selected.items():
if node.tag == inkex.addNS('path', 'svg'):
path = Path(node.get('d')).to_arrays()
distorted = []
first = True
for cmd, params in path:
if cmd != 'Z':
if first == True:
x = params[-2]
y = params[-1]
distorted.append(
['M',
list(self.distort_coordinates(x, y))])
first = False
else:
x = params[-2]
y = params[-1]
distorted.append(
['L', self.distort_coordinates(x, y)])
node.set('d', str(Path(distorted)))
if __name__ == '__main__':
BarrelDistorsion().run()

View File

@ -0,0 +1,195 @@
#!/usr/bin/env python3
# pylint: disable=too-many-ancestors
# standard library
import os
import sys
import re
import argparse
from shutil import copy2
# from subprocess import Popen, PIPE
# import time
# from lxml import etree
# local library
import inkex
from inkex.command import inkscape
from inkex.elements import _selected as selection
MIN_PYTHON_VERSION = (3, 6) # Mainly for f-strings
if (sys.version_info.major, sys.version_info.minor) < (3, 6):
inkex.Effect.msg(f"Python {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]} or later required.")
sys.exit(1)
class BaseExtension(inkex.Effect):
"""Custom class that makes creation of extensions easier.
Users of this class need not worry about boilerplates, such as how to
call inkscape via shell, and the management of tempfiles. Useful functions
are also provided."""
def __init__(self, custom_effect, args_adder=None):
"""Init base class.
In a typical Inkscape extension that does not make use of BaseExtension,
the effect is determined by the "effect" method of the extension class.
This init function will take in a method, and run it in the "effect" method
together with the other boilerplate.
This init method takes in a function under the custom_effect argument.
This function will handle the user's effects, minus the boilerplate. It
has to return a list[str] object, with each str being a verb that inkscape
can execute."""
inkex.Effect.__init__(self)
self.custom_effect = custom_effect
self._msg = self.msg # The old msg function provided by inkex (only accepts strings)
def msg(*args, sep=' '):
"""Improved msg method, similar to Python's print"""
self._msg(sep.join([str(arg) for arg in args]))
self.msg = msg
if args_adder is not None:
args_adder(self.arg_parser)
self.args_adder = args_adder
def z_sort(self, alist):
"""Return new list sorted in document order (depth-first traversal)."""
return list(self.z_iter(alist))
def z_iter(self, alist):
"""Return iterator over ids in document order (depth-first traversal)."""
id_list = list(alist)
count = len(id_list)
for element in self.document.getroot().iter():
# element_id = element.get('id')
# if element_id is not None and element_id in id_list:
if element in alist:
id_list.remove(element)
yield element
count -= 1
if not count:
return
@staticmethod
def show(obj):
"""Returns a str representation of object"""
def rep(obj):
if hasattr(obj, 'get_id'):
return f"{type(obj).__name__}({obj.get_id()})"
return f"{type(obj).__name__}"
if type(obj).__name__ == 'ElementList':
return ('ElementList(' +
', '.join([rep(child) for child in obj.values()]) +
')')
if isinstance(obj, list):
return '[' + ', '.join(rep(child) for child in obj) + ']'
return rep(obj)
def find(self, obj: any, xpath='/*') -> list:
"""Returns a list of objects which satisfies XPath
Args:
obj (any): Parent object to recurse into. Examples include root, selected, or a group.
xpath (str, optional): Defaults to '/*'.
Returns:
list: [description]
"""
BASIC_TAGS = ('circle', 'ellipse', 'line', 'polygon', 'polyline', 'rect', 'path', 'image', 'g')
SPECIAL_TAGS = {
'l': "svg:g[@inkscape:groupmode='layer']",
'p': 'svg:path'
}
xpath = re.sub(r'((?<=/)(' + '|'.join(BASIC_TAGS) + r')\b)', r'svg:\1', xpath)
for k, v in SPECIAL_TAGS.items():
xpath = re.sub('(?<=/)' + k + r'\b', v, xpath)
xpath = re.sub(r'(?<=\[)(\d+):(\d+)(?=\])', r'position()>=\1 and position()<\2', xpath)
if type(obj).__name__ != 'ElementList':
obj = [obj]
output = []
for child in obj:
matches = child.xpath(xpath, namespaces={
'svg': 'http://www.w3.org/2000/svg',
'inkscape': 'http://www.inkscape.org/namespaces/inkscape'})
for match in matches:
if type(match).__name__ not in ('Defs', 'NamedView', 'Metadata'):
output.append(match)
return output
def effect(self):
"""Main entry point to process current document. Not to be called externally."""
actions_list = self.custom_effect(self)
if actions_list is None or actions_list == []:
self.msg("No actions received. Perhaps you are calling inkex object methods?")
elif isinstance(actions_list, list):
tempfile = self.options.input_file + "-BaseExtension.svg"
# prepare
copy2(self.options.input_file, tempfile)
#disabled because it leads to crash Inkscape: https://gitlab.com/inkscape/inkscape/-/issues/2487
#actions_list.append("FileSave")
#actions_list.append("FileQuit")
#extra_param = "--with-gui"
#workaround to fix it (we use export to tempfile instead processing and saving again)
actions_list.append("export-type:svg")
actions_list.append("export-filename:{}".format(tempfile))
actions_list.append("export-do")
extra_param = "--batch-process"
actions = ";".join(actions_list)
inkscape(tempfile, extra_param, actions=actions)
# finish up
# replace current document with content of temp copy file
self.document = inkex.load_svg(tempfile)
# update self.svg
self.svg = self.document.getroot()
# Clean up tempfile
try:
os.remove(tempfile)
except Exception: # pylint: disable=broad-except
pass
def call(self, child, ext_options):
"""Used to call an extension from another extension"""
old_options = self.options
parser = argparse.ArgumentParser()
child.args_adder(parser)
self.options = parser.parse_args([])
for k, v in ext_options.items():
setattr(self.options, k, v)
output = child.custom_effect(self)
self.options = old_options
return output

View File

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Batch Task</name>
<id>fablabchemnitz.de.batch_task</id>
<param name="tab_main" type="notebook">
<page name="Options" gui-text="Options">
<param name="target" type="optiongroup" appearance="radio" gui-text="XPath upon:">
<option value="root">Entire document</option>
<option value="selected">Only selected objects</option>
</param>
<param name="xpath" type="string" gui-text="XPath:" />
<param name="tab_effect" type="notebook">
<page name="Preset" gui-text="Presets">
<param name="effect_preset1" type="optiongroup" appearance="combo" gui-text="Preset effect 1:">
<option value="">Do nothing</option>
<option value="EditDuplicate">EditDuplicate</option>
<option value="EditDelete">EditDelete</option>
<option value="SelectionGroup">SelectionGroup</option>
<option value="SelectionUnGroup">SelectionUnGroup</option>
<option value="SelectionRaise">SelectionRaise</option>
<option value="SelectionLower">SelectionLower</option>
<option value="SelectionToFront">SelectionToFront</option>
<option value="SelectionToBack">SelectionToBack</option>
<option value="org.inkscape.color.brighter">Brighter</option>
<option value="org.inkscape.color.darker">Darker</option>
</param>
<param name="effect_preset2" type="optiongroup" appearance="combo" gui-text="Preset effect 2:">
<option value="">Do nothing</option>
<option value="EditDuplicate">EditDuplicate</option>
<option value="EditDelete">EditDelete</option>
<option value="SelectionGroup">SelectionGroup</option>
<option value="SelectionUnGroup">SelectionUnGroup</option>
<option value="SelectionRaise">SelectionRaise</option>
<option value="SelectionLower">SelectionLower</option>
<option value="SelectionToFront">SelectionToFront</option>
<option value="SelectionToBack">SelectionToBack</option>
<option value="org.inkscape.color.brighter">Brighter</option>
<option value="org.inkscape.color.darker">Darker</option>
</param>
<param name="effect_preset3" type="optiongroup" appearance="combo" gui-text="Preset effect 3:">
<option value="">Do nothing</option>
<option value="EditDuplicate">EditDuplicate</option>
<option value="EditDelete">EditDelete</option>
<option value="SelectionGroup">SelectionGroup</option>
<option value="SelectionUnGroup">SelectionUnGroup</option>
<option value="SelectionRaise">SelectionRaise</option>
<option value="SelectionLower">SelectionLower</option>
<option value="SelectionToFront">SelectionToFront</option>
<option value="SelectionToBack">SelectionToBack</option>
<option value="org.inkscape.color.brighter">Brighter</option>
<option value="org.inkscape.color.darker">Darker</option>
</param>
</page>
<page name="Simple" gui-text="Simple">
<param name="effect_simple1" type="string" gui-text="Effect 1:" />
<param name="effect_simple2" type="string" gui-text="Effect 2:" />
<param name="effect_simple3" type="string" gui-text="Effect 3:" />
</page>
<page name="Multi" gui-text="Multi">
<param name="effect_multi" type="string" gui-text="Effects:" appearance="multiline" />
</page>
</param>
<!-- <param type="string" name="varname" gui-text="label" indent="1" max-length="5" appearance="multiline">some text</param> -->
<!-- <param name="param_str2" type="string" gui-text="Effects:" [max-length="5" | appearance="multiline"]></param> -->
<!-- <param type="string" name="varname" gui-text="label" [indent="1"] [max-length="5" | appearance="multiline"]>some text</param> -->
<param name="mode" type="optiongroup" appearance="radio" gui-text="Apply effects to:">
<option value="all">Entire selection</option>
<option value="indiv">Each object in selection</option>
</param>
</page>
<page name="Help" gui-text="Help">
<label xml:space="preserve">
This template provides extension writers with a basis to write their python based Inkscape extensions quickly and properly.
This testing help text can be changed to help users of the extension.
</label>
</page>
<page name="null_Reference" gui-text="Reference">
<!--REFERENCE START -->
<param name="null_notebook" type="notebook">
<page name="null_edit" gui-text="Edit">
<label xml:space="preserve">
EditCut
EditCopy
EditPaste</label>
<separator />
<label xml:space="preserve">EditDuplicate
EditClone
SelectionCreateBitmap</label>
<separator />
<label xml:space="preserve">EditDelete</label>
</page>
<page name="null_layer" gui-text="Layer">
<label xml:space="preserve">
LayerNew
LayerRename</label>
</page>
<page name="null_objects" gui-text="Objects">
<label xml:space="preserve">
SelectionGroup
SelectionUnGroup</label>
<separator />
<label xml:space="preserve">ObjectSetClipPath
ObjectUnSetClipPath
ObjectSetMask
ObjectUnSetMask</label>
<separator />
<label xml:space="preserve">SelectionRaise
SelectionLower
SelectionToFront
SelectionToBack</label>
</page>
<page name="null_objects_2" gui-text="Objects 2">
<label xml:space="preserve">
ObjectRotate90
ObjectRotate90CCW
ObjectFlipHorizontally
ObjectFlipVertically</label>
<separator />
<label xml:space="preserve">UnhideAll
UnhideAllInAllLayers
UnlockAll
UnlockAllInAllLayers</label>
</page>
<page name="null_path" gui-text="Path">
<label xml:space="preserve">
SelectionUnion
SelectionDiff
SelectionIntersect</label>
<separator />
<label xml:space="preserve">SelectionSymDiff
SelectionDivide
SelectionCutPath</label>
<separator />
<label xml:space="preserve">SelectionCombine
SelectionBreakApart</label>
<separator />
<label xml:space="preserve">SelectionInset
SelectionOffset
SelectionReverse</label>
</page>
<page name="null_text" gui-text="Text">
<label xml:space="preserve">
SelectionTextToPath
SelectionTextFromPath
SelectionTextRemoveKerns</label>
</page>
<page name="null_filters" gui-text="Filters">
<label xml:space="preserve">
Color
org.inkscape.color.brighter
org.inkscape.color.darker
org.inkscape.color.grayscale
org.inkscape.color.black_and_white</label>
</page>
<page name="null_extensions" gui-text="Extensions">
<label xml:space="preserve">
EffectLast</label>
</page>
</param>
<!-- REFERENCE END -->
</page>
</param>
<param name="dry_run" type="bool" gui-text="Dry run">true</param>
<effect>
<object-type>all</object-type>
<!--object-type>path</object-type-->
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Various"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">batch_task.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,152 @@
#!/usr/bin/env python3
import os
import sys
import re
import subprocess
from BaseExtension import BaseExtension
# For linting purposes
from argparse import ArgumentParser
"""If syntax error occurs here, change inkscape interpreter to python3"""
"""I have yet to find a way for an extension to call another extension with parameters,
without GUI. This extension can be run as part of a standalone extension (using BaseExtension)
or imported for use by another extension. This workaround is done via the 'option' arg in
the 'custom_effect' function"""
def custom_effect(self: BaseExtension):
"""Note: The init of the BaseExtension class will set its 'custom_effect' attr
to this function. Hence, the self arg is of type BaseExtension."""
selected = self.svg.selected
root = self.document.getroot()
actions_list = []
proc = subprocess.run("inkscape --verb-list | grep -oP '^.+?(?=:)'",
shell=True, capture_output=True)
valid_actions_and_verbs = proc.stdout.decode().splitlines()
proc = subprocess.run("inkscape --action-list | grep -oP '^.+?(?= *:)'",
shell=True, capture_output=True)
valid_actions_and_verbs += proc.stdout.decode().splitlines()
self.options.dry_run = self.options.dry_run == 'true'
def verify_action(action):
if ':' in action:
action = action.split(':')[0]
if action not in valid_actions_and_verbs:
raise ValueError(action)
def select_do_individually(objs, actions):
for obj in objs:
actions_list.append("EditDeselect")
actions_list.append("select-by-id:" + obj.get_id())
if isinstance(actions, str):
actions = [actions]
for action in actions:
verify_action(action)
actions_list.append(action)
def select_do_on_all(objs, actions):
for obj in objs:
actions_list.append("select-by-id:" + obj.get_id())
if isinstance(actions, str):
actions = [actions]
for action in actions:
verify_action(action)
actions_list.append(action)
effects = []
try:
if self.options.tab_effect is None:
if self.options.effects is not None:
self.options.tab_effect = 'Multi'
elif self.options.effect1 is not None:
self.options.tab_effect = 'Simple'
elif self.options.tab_effect in ('Preset', 'Simple'):
for attr in ('effect_' + self.options.tab_effect.lower() + str(i) for i in range(1, 4)):
e = getattr(self.options, attr)
if e != None:
effects += [e.strip()]
if effects == []:
raise ValueError
elif self.options.tab_effect == 'Multi':
if self.options.effects is None:
raise ValueError
for line in self.options.effects.split('\\n'):
effects += [e.strip() for e in line.split(';') if e != '']
except ValueError:
self.msg("No effects inputted! Quitting...")
sys.exit(0)
if self.options.target == 'root':
objects = self.find(root, '/svg:svg' + self.options.xpath)
elif self.options.target == 'selected':
objects = self.find(selected, self.options.xpath)
if objects == []:
self.msg(f"No objects satisfies XPath: '{self.options.xpath}'.")
self.msg("Root:", self.show(root))
self.msg("Selected:", self.show(selected))
sys.exit(0)
try:
if self.options.mode == 'all':
select_do_on_all(objects, effects)
elif self.options.mode == 'indiv':
select_do_individually(objects, effects)
except ValueError as e:
self.msg(f"'{e.args[0]}' is not a valid action or verb in inkscape.")
sys.exit(1)
if self.options.dry_run:
self.msg(f"{'DRY RUN':=^40}")
self.msg("Root:", self.show(self.find(root, '/*')))
self.msg("Selected:", self.show(selected))
self.msg()
self.msg("XPath:", self.show(objects))
self.msg()
self.msg("Actions:", actions_list)
sys.exit(0)
return actions_list
def args_adder(arg_parser: ArgumentParser):
arg_parser.add_argument("--target", default='root', help="Object to apply xpath find on")
arg_parser.add_argument("--xpath", default='/*', help="For selection of objects")
arg_parser.add_argument("--tab_main", default=None)
arg_parser.add_argument("--Simple", default=None)
arg_parser.add_argument("--Multi", default=None)
arg_parser.add_argument("--mode", default="all", help="Mode to apply effects on objects")
arg_parser.add_argument("--tab_effect", default=None)
for arg in (*(x + str(y) for x in ('effect_preset', 'effect_simple') for y in range(1, 4)), 'effects'):
arg_parser.add_argument(f"--{arg}", default=None, help="Inkscape verb for path op")
arg_parser.add_argument("--dry_run", default='false')
arg_parser.add_argument("--null_notebook", default='false')
#import inkex
#for key, value in arg_parser.parse_args()._get_kwargs():
# if value is not None:
# inkex.utils.debug("{}={}".format(key, value))
BatchTask = BaseExtension(custom_effect, args_adder=args_adder)
if __name__ == '__main__':
BatchTask.run()
# Namespace(Multi='SelectionDiff', Simple='SelectionDiff', dry_run='false', effect1='SelectionBreakApart', effect2=None, effect3=None, effects=None, ids=['image25'], input_file='/tmp/ink_ext_XXXXXX.svgIDCKU0', mode='all', null='null', output=<_io.BufferedWriter name='<stdout>'>, selected_nodes=[], tab_effect='Simple', tab_main='Options', target='root', xpath='/*')
# Namespace(Multi=None, Simple=None, dry_run='false', effect1='SelectionDelete', effect2=None, effect3=None, effects=None, mode='all', null='false', tab_effect=None, tab_main=None, target='root', xpath='/*')

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Bezier Envelope</name>
<id>fablabchemnitz.de.bezier_envelope</id>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Legacy Tools"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">bezier_envelope.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,399 @@
#!/usr/bin/env python3
'''
Bezier Envelope extension for Inkscape
Copyright (C) 2009 Gerrit Karius
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
About the Bezier Envelope extension:
This extension implements Bezier enveloping.
It takes an arbitrary path (the "letter") and a 4-sided path (the "envelope") as input.
The envelope must be 4 segments long. Unless the letter is to be rotated or flipped,
the envelope should begin at the upper left corner and be drawn clockwise.
The extension then attempts to squeeze the letter into the envelope
by rearranging all anchor and handle points of the letter's path.
In order to do this, the bounding box of the letter is used.
All anchor and bezier handle points get new x and y coordinates between 0% and 100%
according to their place inside the bounding box.
The 4 sides of the envelope are then interpreted as deformed axes.
Points at 0% or 100% could be placed along these axes, but because most points
are somewhere inside the bounding box, some tweening of the axes must be done.
The function mapPointsToMorph does the tweening.
Say, some point is at x=30%, y=40%.
For the tweening, the function tweenCubic first calculates a straight tween
of the y axis at the x percentage of 30%.
This tween axis now floats somewhere between the y axis keys at the x percentage,
but is not necessarily inside the envelope, because the x axes are not straight.
Now, the end points on the two x axes at 30% are calculated. The function match()
takes these points and calculates a "stretch" transform which maps the two anchor
points of the y axis tween to the two points on the x axes by rotating the tween and
stretching it along its endpoints. This transform is then applied to the handle points,
to get the entire tweened y axis to its x tweened position.
Last, the point at the y percentage 40% of this y axis tween is calculated.
That is the final point of the enveloped letter.
Finally, after all of the letter's points have been recalculated in this manner,
the resulting path is taken and replaces the letter's original path.
TODO:
* Currently, both letter and envelope must be paths to work.
-> Arbitrary other shapes like circles and rectangles should be interpreted as paths.
* It should be possible to select several letters, and squeeze them into one envelope as a group.
* It should be possible to insert a clone of the letter, instead of replacing it.
* This program was originally written in Java. Maybe for some code, Python shortcuts can be used.
I hope the comments are not too verbose. Enjoy!
'''
import inkex
from inkex import Transform
from inkex.paths import Path
import math
import sys
import ffgeom
class BezierEnvelope(inkex.EffectExtension):
segmentTypes = ["move","line","quad","cubic","close"]
def effect(self):
if len(self.options.ids) < 2:
inkex.errormsg("Two paths must be selected. The 1st is the letter, the 2nd is the envelope and must have 4 sides.")
exit()
letterElement = self.svg.selected[self.options.ids[0]]
envelopeElement = self.svg.selected[self.options.ids[1]]
if letterElement.get('inkscape:original-d') or envelopeElement.get('inkscape:original-d'):
inkex.errormsg("One or both selected paths have attribute 'inkscape:original-d' which points to Live Path Effects (LPE). Please convert to regular path.")
exit()
if letterElement.tag != inkex.addNS('path','svg') or envelopeElement.tag != inkex.addNS('path','svg'):
inkex.errormsg("Both letter and envelope must be SVG paths.")
exit()
axes = extractMorphAxes(Path( envelopeElement.get('d') ).to_arrays())
if axes is None:
inkex.errormsg("No axes found on envelope.")
exit()
axisCount = len(axes)
if axisCount < 4:
inkex.errormsg("The envelope path has less than 4 segments.")
exit()
for i in range( 0, 4 ):
if axes[i] is None:
inkex.errormsg("axis[%i] is None" % i)
exit()
# morph the enveloped element according to the axes
morph_element( letterElement, envelopeElement, axes );
def morph_element( letterElement, envelopeElement, axes ):
path = Path( letterElement.get('d') ).to_arrays()
morphedPath = morphPath( path, axes )
letterElement.set("d", str(Path(morphedPath)))
# Morphs a path into a new path, according to cubic curved bounding axes.
def morphPath(path, axes):
bounds = [y for x in list(Path(path).bounding_box()) for y in list(x)]
assert len(bounds) == 4
new_path = []
current = [ 0.0, 0.0 ]
start = [ 0.0, 0.0 ]
for cmd, params in path:
segmentType = cmd
points = params
if segmentType == "M":
start[0] = points[0]
start[1] = points[1]
segmentType = convertSegmentToCubic( current, segmentType, points, start )
percentages = [0.0]*len(points)
morphed = [0.0]*len(points)
numPts = getNumPts( segmentType )
normalizePoints( bounds, points, percentages, numPts )
mapPointsToMorph( axes, percentages, morphed, numPts )
addSegment( new_path, segmentType, morphed )
if len(points) >= 2:
current[0] = points[ len(points)-2 ]
current[1] = points[ len(points)-1 ]
return new_path
def getNumPts( segmentType ):
if segmentType == "M":
return 1
if segmentType == "L":
return 1
if segmentType == "Q":
return 2
if segmentType == "C":
return 3
if segmentType == "Z":
return 0
return -1
def addSegment( path, segmentType, points ):
path.append([segmentType,points])
# Converts visible path segments (Z,L,Q) into absolute cubic segments (C).
def convertSegmentToCubic( current, segmentType, points, start ):
if segmentType == "H":
# print(current, points, start)
assert len(points) == 1
points.insert(0, current[0])
# points[0] += current[0]
# print(segmentType, current, points, start)
return convertSegmentToCubic(current, "L", points, start)
elif segmentType == "V":
# print(points)
assert len(points) == 1
points.append(current[1])
# points[1] += current[1]
# print(segmentType, current, points, start)
return convertSegmentToCubic(current, "L", points, start)
if segmentType == "M":
return "M";
if segmentType == "C":
return "C";
elif segmentType == "Z":
for i in range(0,6):
points.append(0.0)
points[4] = start[0]
points[5] = start[1]
thirdX = (points[4] - current[0]) / 3.0
thirdY = (points[5] - current[1]) / 3.0
points[2] = points[4]-thirdX
points[3] = points[5]-thirdY
points[0] = current[0]+thirdX
points[1] = current[1]+thirdY
return "C"
elif segmentType == "L":
for i in range(0,4):
points.append(0.0)
points[4] = points[0]
points[5] = points[1]
thirdX = (points[4] - current[0]) / 3.0
thirdY = (points[5] - current[1]) / 3.0
points[2] = points[4]-thirdX
points[3] = points[5]-thirdY
points[0] = current[0]+thirdX
points[1] = current[1]+thirdY
return "C"
elif segmentType == "Q":
for i in range(0,2):
points.append(0.0)
firstThirdX = (points[0] - current[0]) * 2.0 / 3.0
firstThirdY = (points[1] - current[1]) * 2.0 / 3.0
secondThirdX = (points[2] - points[0]) * 2.0 / 3.0
secondThirdY = (points[3] - points[1]) * 2.0 / 3.0
points[4] = points[2]
points[5] = points[3]
points[0] = current[0] + firstThirdX
points[1] = current[1] + firstThirdY
points[2] = points[2] - secondThirdX
points[3] = points[3] - secondThirdY
return "C"
else:
sys.stderr.write("unsupported segment type: %s\n" % (segmentType))
return segmentType
# Normalizes the points of a path segment, so that they are expressed as percentage coordinates
# relative to the bounding box axes of the total shape.
# @param bounds The bounding box of the shape.
# @param points The points of the segment.
# @param percentages The returned points in normalized percentage form.
# @param numPts
def normalizePoints( bounds, points, percentages, numPts ):
# bounds has structure xmin,xMax,ymin,yMax
xmin,xMax,ymin,yMax = bounds
for i in range( 0, numPts ):
x = i*2
y = i*2+1
percentages[x] = (points[x] - xmin) / (xMax-xmin)
percentages[y] = (points[y] - ymin) / (yMax-ymin)
# Extracts 4 axes from a path. It is assumed that the path starts with a move, followed by 4 cubic paths.
# The extraction reverses the last 2 axes, so that they run in parallel with the first 2.
# @param path The path that is formed by the axes.
# @return The definition points of the 4 cubic path axes as float arrays, bundled in another array.
def extractMorphAxes( path ):
points = []
current = [ 0.0, 0.0 ]
start = [ 0.0, 0.0 ]
# the curved axis definitions go in here
axes = [None]*4
i = 0
for cmd, params in path:
points = params
cmd = convertSegmentToCubic( current, cmd, points, start )
if cmd == "M":
current[0] = points[0]
current[1] = points[1]
start[0] = points[0]
start[1] = points[1]
elif cmd == "C":
# 1st cubic becomes x axis 0
# 2nd cubic becomes y axis 1
# 3rd cubic becomes x axis 2 and is reversed
# 4th cubic becomes y axis 3 and is reversed
if i % 2 == 0:
index = i
else:
index = 4-i
if( i < 2 ):
# axes 1 and 2
axes[index] = [ current[0], current[1], points[0], points[1], points[2], points[3], points[4], points[5] ]
elif( i < 4 ):
# axes 3 and 4
axes[index] = [ points[4], points[5], points[2], points[3], points[0], points[1], current[0], current[1] ]
else:
# more than 4 axes - hopefully it was an unnecessary trailing Z
{}
current[0] = points[4]
current[1] = points[5]
i = i + 1
elif cmd == "Z":
#do nothing
{}
else:
raise Exception("Unsupported segment type: %s" % cmd)
return None
return axes
# Projects points in percentage coordinates into a morphed coordinate system that is framed
# by 2 x cubic curves (along the x axis) and 2 y cubic curves (along the y axis).
# @param axes The x and y axes of the envelope.
# @param percentage The current segment of the letter in normalized percentage form.
# @param morphed The array to hold the returned morphed path.
# @param numPts The number of points to be transformed.
def mapPointsToMorph( axes, percentage, morphed, numPts ):
# rename the axes for legibility
yCubic0 = axes[1]
yCubic1 = axes[3]
xCubic0 = axes[0]
xCubic1 = axes[2]
# morph each point
for i in range( 0, numPts ):
x = i*2
y = i*2+1
# tween between the morphed y axes according to the x percentage
tweenedY = tweenCubic( yCubic0, yCubic1, percentage[x] )
# get 2 points on the morphed x axes
xSpot0 = pointOnCubic( xCubic0, percentage[x] )
xSpot1 = pointOnCubic( xCubic1, percentage[x] )
# create a transform that stretches the y axis tween between these 2 points
yAnchor0 = [ tweenedY[0], tweenedY[1] ]
yAnchor1 = [ tweenedY[6], tweenedY[7] ]
xTransform = match( yAnchor0, yAnchor1, xSpot0, xSpot1 )
# map the y axis tween to the 2 points by applying the stretch transform
for j in range(0,4):
x2 = j*2
y2 = j*2+1
pointOnY = [tweenedY[x2],tweenedY[y2]]
Transform(xTransform).apply_to_point(pointOnY)
tweenedY[x2] = pointOnY[0]
tweenedY[y2] = pointOnY[1]
# get the point on the tweened and transformed y axis according to the y percentage
morphedPoint = pointOnCubic( tweenedY, percentage[y] )
morphed[x] = morphedPoint[0]
morphed[y] = morphedPoint[1]
# Calculates the point on a cubic bezier curve at the given percentage.
def pointOnCubic( c, t ):
point = [0.0,0.0]
_t_2 = t*t
_t_3 = _t_2*t
_1_t = 1-t
_1_t_2 = _1_t*_1_t
_1_t_3 = _1_t_2*_1_t
for i in range( 0, 2 ):
point[i] = c[i]*_1_t_3 + 3*c[2+i]*_1_t_2*t + 3*c[4+i]*_1_t*_t_2 + c[6+i]*_t_3
return point
# Tweens 2 bezier curves in a straightforward way,
# i.e. each of the points on the curve is tweened along a straight line
# between the respective point on key1 and key2.
def tweenCubic( key1, key2, percentage ):
tween = [0.0]*len(key1)
for i in range ( 0, len(key1) ):
tween[i] = key1[i] + percentage * (key2[i] - key1[i])
return tween
# Calculates a transform that matches 2 points to 2 anchors
# by rotating and scaling (up or down) along the axis that is formed by
# a line between the two points.
def match( p1, p2, a1, a2 ):
x = 0
y = 1
# distances
dp = [ p2[x]-p1[x], p2[y]-p1[y] ]
da = [ a2[x]-a1[x], a2[y]-a1[y] ]
# angles
angle_p = math.atan2( dp[x], dp[y] )
angle_a = math.atan2( da[x], da[y] )
# radians
#rp = math.sqrt( dp[x]*dp[x] + dp[y]*dp[y] )
#ra = math.sqrt( da[x]*da[x] + da[y]*da[y] )
rp = math.hypot( dp[x], dp[y] )
ra = math.hypot( da[x], da[y] )
# scale
scale = ra / rp
# transforms in the order they are applied
t1 = Transform( "translate(%f,%f)"%(-p1[x],-p1[y]) ).matrix
#t2 = Transform( "rotate(%f)"%(-angle_p) ).matrix
#t3 = Transform( "scale(%f,%f)"%(scale,scale) ).matrix
#t4 = Transform( "rotate(%f)"%angle_a ).matrix
t2 = rotateTransform(-angle_p)
t3 = scale_transform( scale, scale )
t4 = rotateTransform( angle_a )
t5 = Transform( "translate(%f,%f)"%(a1[x],a1[y]) ).matrix
# transforms in the order they are multiplied
t = t5
t = Transform(t) * Transform(t4)
t = Transform(t) * Transform(t3)
t = Transform(t) * Transform(t2)
t = Transform(t) * Transform(t1)
# return the combined transform
return t
def rotateTransform( a ):
return [[math.cos(a),-math.sin(a),0],[math.sin(a),math.cos(a),0]]
def scale_transform( sx, sy ):
return [[sx,0,0],[0,sy,0]]
if __name__ == '__main__':
BezierEnvelope().run()

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Blobs Texture</name>
<id>fablabchemnitz.de.blobs</id>
<param type="notebook" name="Nmain">
<page name="top" gui-text="All">
<label>Fills a box with blobs.</label>
<param max="256" name="nb" type="int" gui-text="How many blobs?">10</param>
<param name="pgsizep" type="bool" gui-text="Default rectangle to page size?">true</param>
<param max="10000" name="rx" type="int" gui-text="Work area x">1000</param>
<param max="10000" name="ry" type="int" gui-text="Work area y">1000</param>
</page>
<page name="bottom" gui-text="Each">
<label>Each blob.</label>
<param max="256" name="num" type="int" gui-text="Interior points">10</param>
<param max="10000." name="sz" type="float" gui-text="Size of a blob">50.</param>
<param max="1.0" name="cave" type="float" gui-text="Concavity">0.</param>
<param max="10.0" name="blunt" type="float" gui-text="Corner bluntness">0.3</param>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from Generator"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">blobs.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,169 @@
#!/usr/bin/env python3
# These two lines are only needed if you don't put the script directly into
# the installation directory
import math
import inkex
import random
from lxml import etree
class Blobs(inkex.EffectExtension):
"""
Creates a random blob from a convex hull over n points.
The expected degree of the polygon is sqrt(n). The corners
are blunted by the blunt parameter. 0 means sharp. 1 will
result in loopy splines.
"""
def add_arguments(self, pars):
pars.add_argument("--pgsizep", type=inkex.Boolean, default=True, help="Default rectangle to page size?")
pars.add_argument('--num', type = int, default = 25, help = 'Number of random points to start with')
pars.add_argument('--blunt', type = float, default = 0.3, help = 'Bluntness of corners. Should be < 1')
pars.add_argument('--cave', type = float, default = 0.0, help = 'Concavity. Less blobby and more splatty')
pars.add_argument('--rx', type = int, default = 1000, help = 'Size of work area x')
pars.add_argument('--ry', type = int, default = 1000, help = 'Size of work area y')
pars.add_argument('--sz', type = float, default = 50., help = 'Size of a blob')
pars.add_argument('--nb', type = int, default = 10, help = 'Total number of blobs')
pars.add_argument("--Nmain", default='top', help="Active tab.")
def effect(self):
global cave
if self.options.pgsizep:
svg = self.document.getroot()
rx = int(self.svg.unittouu(svg.get('width')))
ry = int(self.svg.unittouu(svg.attrib['height']))
else:
rx = self.options.rx
ry = self.options.ry
blunt = self.options.blunt
cave = self.options.cave
sz = self.options.sz
nb = self.options.nb
num = self.options.num
# Get access to main SVG document element and get its dimensions.
svg = self.document.getroot()
# Create a new layer.
layer = etree.SubElement(svg, 'g')
layer.set(inkex.addNS('label', 'inkscape'), 'Blob Layer')
layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
ctrs = [(random.randrange(rx) , random.randrange(ry))
for i in range(nb) ]
for ctr in ctrs :
points = [(random.gauss(ctr[0], sz) , random.gauss(ctr[1], sz))
for i in range(num) ]
px = hull(points)
pts = [points[px[i]] for i in range(len(px))]
# Create path element
path = etree.Element(inkex.addNS('path','svg'))
path.set('style', str(inkex.Style({'fill':'#000000'})))
pathstring = 'M ' + str(pts[0][0]) + ' ' + str(pts[0][1]) + ' '
for j in range(len(pts)):
k = (j+1) % len(pts)
kk = (j+2) % len(pts)
if j==0 :
(lasth, h1) = sHandles(pts[-1], pts[0], pts[1], blunt)
(h2, hnext) = sHandles(pts[j], pts[k], pts[kk], blunt)
pathstring += "C %f %f %f %f %f %f " % (h1[0], h1[1],
h2[0], h2[1],
pts[k][0], pts[k][1])
h1 = hnext
pathstring += 'Z'
path.set('d', pathstring)
layer.append(path)
def sHandles(pre, pt, post, blunt):
'''I'm proud of this cute little construction for the
spline handles. No doubt someone has thought of it before
but, if not, its name is ACHC Andrew's Cute Handle
Construction. Note: no trig function calls.'''
try :
slope = (post[1] - pt[1]) / (post[0] - pt[0])
except ZeroDivisionError :
slope = math.copysign(1E30 , post[1] - pt[1])
lenpre = distance(pre, pt)
lenpost = distance(pt, post)
lenr = lenpre**2 / lenpost
locx = math.copysign(lenr / math.sqrt(1. + slope**2) , post[0] - pt[0])
mark = (pre[0] - locx , pre[1] - locx*slope)
try :
markslope = (pt[1] - mark[1]) / (pt[0] - mark[0])
except ZeroDivisionError :
markslope = math.copysign(1E30 , pt[1] - mark[1])
prex = math.copysign(lenpre / math.sqrt(1. + markslope**2) ,
pt[0] - mark[0])
hpre = (pt[0] - prex*blunt , pt[1] - prex*markslope*blunt)
postx = prex*lenpost/lenpre
hpost = (pt[0] + postx*blunt , pt[1] + postx*markslope*blunt)
return (hpre, hpost)
"""Blunt=0.3 makes pleasingly round, mostly convex blobs. 0.4 makes them more
concave. 0.6 - 1.0 they're getting more and more pointy. 2.0 - 10. and they
grow appendages like hot-air balloons. 0.1 makes the corners pretty sharp.
0.0 and it's down to the convex hulls that are the basis of the blobs, that
is, polygons"""
def distance(a, b) :
return math.sqrt((a[0] - b[0])**2 + (a[1] - b[1])**2 )
def hull(arg):
"""Convex hull by Graham scan."""
xarr, yarr = zip(* arg)
ymin = min(yarr)
ind = findall(yarr, lambda y: y == ymin)
if len(ind) > 1 :
xshort = [xarr[j] for j in ind]
xmin = min(xshort)
j = ind[xshort.index(xmin)]
ind = j
else :
ind = ind[0]
all = list(range(len(xarr)))
del all[ind]
all.sort(key=lambda i : (xarr[i] - xarr[ind]) /
math.sqrt((xarr[i] - xarr[ind])**2 + (yarr[i] - yarr[ind])**2),
reverse=True)
if len(all) < 3 :
all.insert(0, ind)
return all
ans = [ind]
for i in all :
if len(ans) == 1 :
ans.append(i)
else :
while rightTurn(ans[-2], ans[-1], i, arg) :
ans.pop()
ans.append(i)
return ans
def rightTurn(j, k, l, arg) :
'''Cross product: Ax*By - Ay*Bx = Cz '''
ax = (arg[k][0] - arg[j][0])
by = (arg[l][1] - arg[k][1])
ay = (arg[k][1] - arg[j][1])
bx = (arg[l][0] - arg[k][0])
p = ax*by - ay*bx
dot = ax*bx + ay*by
cos = dot / math.sqrt((ax**2 + ay**2) * (bx**2 + by**2))
crt = 1 - cave*2
if p <= 0 :
return cos < crt #We forgive right turns based on /cave/
else :
return False
def findall(a, f):
r = []
for x, j in zip(a, range(len(a))) :
if f(x) :
r.append(j)
return r
if __name__ == '__main__':
Blobs().run()

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Blueprint Maker</name>
<id>fablabchemnitz.de.blueprint_maker</id>
<param name="palette" type="optiongroup" appearance="combo" gui-text="Palette:">
<option value="blueprint">Blueprint</option>
<option value="black">Screen</option>
<option value="white">Paper</option>
<option value="laser">Laser</option>
</param>
<param name="stroke_width" precision="4" type="float" min="0.001" max="10.000" gui-text="Line thickness:">1.000</param>
<param name="stroke_units" type="optiongroup" appearance="combo" gui-text="Line units:">
<option value="millimeters">mm</option>
<option value="centimeters">cm</option>
<option value="points">pt</option>
<option value="pixels">px</option>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">blueprint_maker.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,117 @@
#!/usr/bin/env python3
import inkex
import copy
class bluePrintMakerData():
def __init__(self,effect):
self.effect=effect
self.stroke_units=effect.options.stroke_units
self.unit_factor=1.0
self.set_units()
self.stroke_width=effect.options.stroke_width*self.unit_factor
self.palette=effect.options.palette
self.background_color=None
self.stroke_color=None
self.set_colors()
self.selected_nodes=[]
if len(effect.options.ids)==0:
self.selected_nodes=[effect.svg.getElementById(x) for x in effect.svg.get_ids()]
self.selected_nodes=[node for node in self.selected_nodes if effect.is_geometry(node)]
else:
self.selected_nodes=[y for x,y in effect.svg.selected.items()]
self.selected_nodes=[node for node in self.selected_nodes if effect.is_geometry(node,shapes=['path','g','rect','ellipse','perspective'])]
self.allowed_ids=[]
self.allowed_nodes=[]
self.set_objects()
def set_units(self):
if self.stroke_units=='millimeters':
self.unit_factor=3.543
if self.stroke_units=='centimeters':
self.unit_factor=35.433
if self.stroke_units=='points':
self.unit_factor=1.25
if self.stroke_units=='pixels':
self.unit_factor=1.0
def set_colors(self):
if self.palette=='blueprint':
self.background_color='#006fde'
self.stroke_color='#ffffff'
if self.palette=='black':
self.background_color='#000000'
self.stroke_color='#ffffff'
if self.palette=='white':
self.background_color='#ffffff'
self.stroke_color='#000000'
if self.palette=='laser':
self.background_color='#ffffff'
self.stroke_color='#ff0000'
def set_objects(self):
for current_id in self.effect.svg.get_ids():
node=self.effect.svg.getElementById(current_id)
if self.effect.is_geometry(node):
self.allowed_ids.append(current_id)
self.allowed_nodes.append(node)
class BluePrintMaker(inkex.EffectExtension):
def __init__(self):
inkex.Effect.__init__(self)
self.arg_parser.add_argument('-p', '--palette', help='Choose the colors...')
self.arg_parser.add_argument('-s', '--stroke_width', type=float, help='Stroke size...')
self.arg_parser.add_argument('-u', '--stroke_units', help='Choose the units...')
self.data=None
def is_a_group(self, node):
data=False
if node.tag==inkex.addNS('g','svg'):
data=True
return data
def is_geometry(self, node, shapes=['path','rect','ellipse','perspective']):
data=False
for s in shapes:
if node.tag==inkex.addNS(s,'svg'):
data=True
return data
def change_page_settings(self):
namedview=self.svg.namedview
namedview.set('pagecolor',self.data.background_color)
namedview.set(inkex.addNS('pageopacity', 'inkscape'),'1')
namedview.set(inkex.addNS('pageshadow', 'inkscape'),'0')
namedview.set('bordercolor',self.data.stroke_color)
namedview.set('borderopacity','1')
return None
def change_this_object(self,node):
styles=dict(inkex.Style.parse_str(node.get('style')))
styles_copy=copy.deepcopy(styles)
styles_copy['stroke']=self.data.stroke_color
styles_copy['stroke_width']=self.data.stroke_width
styles_copy['stroke-opacity']='1'
styles_copy['fill']='none'
styles_copy['fill-opacity']='1'
styles_copy['opacity']='1'
node.set('style',str(inkex.Style(styles_copy)))
return None
def iterate_on_objects(self,node=None):
if self.is_geometry(node):
self.change_this_object(node)
if self.is_a_group(node):
for current_node in list(node):
self.iterate_on_objects(current_node)
def effect(self):
self.data=bluePrintMakerData(self)
self.change_page_settings()
for node in self.data.selected_nodes:
self.iterate_on_objects(node)
return None
if __name__ == '__main__':
BluePrintMaker().run()

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Circular Ground from Template</name>
<id>fablabchemnitz.de.circular_ground_from_template</id>
<label appearance="header">Wrap lace pattern found in template file around a circle.</label>
<label>Note: Drawing can become quite large when "Number of copies around circle" is small or "Diameter" of inside circle is large.</label>
<param name="file" type="path" gui-text="Template file name (full path):" mode="file" filetypes="txt">./templates/</param>
<label appearance="header">Grid description</label>
<hbox indent="1">
<param name="angle" type="float" precision="1" min="30" max="89" gui-text="Angle (degrees):">45.0</param>
</hbox>
<hbox indent="1">
<param name="cols" type="int" min="3" max="1000" gui-text="Number of copies around circle:">30</param>
</hbox>
<label appearance="header">Patch description</label>
<hbox indent="1">
<param name="diameter" type="float" precision="2" min="0.1" max="1000" gui-text="Inner circle diameter:">50</param>
<param name="diamunits" gui-text=" " type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="px">px</option>
<option value="pt">pt</option>
</param>
</hbox>
<hbox indent="1">
<param name="rows" type="int" min="1" max="1000" gui-text="Number of circles:">3</param>
</hbox>
<label appearance="header">Line Appearance</label>
<hbox indent="1">
<param name="linewidth" type="float" precision="2" min="0.01" max="1000" gui-text="Width:">1</param>
<param name="lineunits" gui-text=" " type="optiongroup" appearance="combo">
<option value="px">px</option>
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
</param>
</hbox>
<hbox indent="1">
<param name="linecolor" type="color" appearance="colorbutton" gui-text="Color:">255</param>
</hbox>
<effect needs-live-preview="true">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Grids/Guides"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">circular_ground_from_template.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,434 @@
#!/usr/bin/env python3
# Copyright (c) 2017, Ben Connors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
from math import sin, cos, acos, tan, radians, pi, sqrt, ceil, floor
import inkex
from lxml import etree
__author__ = 'Ben Connors'
__credits__ = ['Ben Connors', 'Veronika Irvine', 'Jo Pol', 'Mark Shafer']
__license__ = 'Simplified BSD'
class Vector:
def __repr__(self):
return 'Vector(%.4f, %.4f)' % (self.dx,self.dy)
def __hash__(self):
return hash((self.dx,self.dy))
def rotate(self,theta):
""" Rotate counterclockwise by theta."""
return self.mag*Vector(cos(self.theta+theta),
sin(self.theta+theta),
_theta=self.theta+theta)
def __mul__(self,other):
return Vector(self.dx*other,self.dy*other,_theta=self.theta)
def __rmul__(self,other):
return self*other
def __init__(self,dx,dy,_theta=None):
""" Create a vector with the specified components.
_theta should NOT be passed in normal use - this value is passed by
vector functions where the angle of the new vector is known in order
to eliminate that calculation.
"""
self.dx = float(dx)
self.dy = float(dy)
self.mag = sqrt(dx**2 + dy**2)
self.tuple = (dx,dy)
## Angle to positive X axis
if _theta == None:
_theta = acos(self.dx/self.mag)
self.theta = 2*pi-_theta if self.dy < 0 else _theta
else:
self.theta = _theta
class CircularGroundFromTemplate(inkex.EffectExtension):
def unitToUu(self,param):
""" Convert units.
Converts a number in some units into the units used internally by
Inkscape.
param is a string representing a number with units attached. An
example would be '3.8mm'. Any units supported by Inkscape
are supported by this function.
This wrapper function catches changes made to the location
of the function between Inkscape versions.
"""
try:
return self.svg.unittouu(param)
except:
return inkex.unittouu(param)
def loadFile(self):
""" Load the specification for the unit cell from the file given.
Note that the specification should be in the following format:
TYPE ROWS COLS
[x1,y1,x2,y2,x3,y3] [x4,y4,x5 ...
And so on. The TYPE is always CHECKER and is ignored by this program.
ROWS specifies the height of the unit cell (i.e. max_y - min_y)
and COLS specifies the same for the width (i.e. max_x - min_x).
Note that this is not enforced when drawing the unit cell - points
may be outside this range. These values are used to determine the
distance between unit cells (i.e. unit cells may overlap).
"""
# Ensure that file exists and has the proper extension
if not self.options.file:
inkex.errormsg('You must specify a template file.')
exit()
self.options.file = self.options.file.strip()
if self.options.file == '':
inkex.errormsg('You must specify a template file.')
exit()
if not os.path.isfile(self.options.file):
inkex.errormsg('You have not specified a valid path for the template file.\n\nYour entry: '+self.options.file)
exit()
extension = os.path.splitext(self.options.file)[1]
if extension != '.txt':
inkex.errormsg('The file name must end with .txt.\n\nYour entry: '+self.options.file)
exit()
data = []
rows, cols = -1, -1
with open(self.options.file,'r') as f:
for line in f:
line = line.strip()
## If rows is not a positive integer, we're on the first line
if rows == -1:
tmp = line.split('\t')
_type,cols,rows = tmp[0],int(tmp[1]),int(tmp[2])
else:
data.append([])
for cell in line[1:-1].split(']\t['):
cell = cell.strip()
## The pattern must be rotated 90 degrees clockwise. It's
## simplest to just do that here
tmp = [float(n) for n in cell.split(',')]
data[-1].append([a for b in zip([rows-i for i in tmp[1::2]],[cols-i for i in tmp[::2]]) for a in b])
return {'type': _type, 'rows': rows, 'cols': cols, 'data' : data}
def line(self,points):
"""
Draw a line from point at (x1, y1) to point at (x2, y2).
Style of line is hard coded and specified by 's'.
"""
# define the motions
path = ('M%.4f,%.4fL' % tuple(points[0][:2])) + 'L'.join([('%f,%f' % tuple(a[:2])) for a in points[1:]])
# define the stroke style
s = {'stroke-linejoin': 'miter',
'stroke-width': self.options.linewidth,
'stroke-opacity': '1.0',
'fill-opacity': '1.0',
'stroke': self.options.linecolor,
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'fill': 'none'
}
## Attributes for new element
attribs = {'style':str(inkex.Style(s)),
'd' : path}
## Add new element
etree.SubElement(self.svg.get_current_layer(), inkex.addNS('path', 'svg'), attribs)
def baseVectors(self,segments):
""" Create vectors for all vertices on the specified polygon."""
## Start at 12 o'clock
theta = pi/2
## Move clockwise
dtheta = -2*pi/segments
vector = Vector(0,self.options.diameter/2)
vectors = [vector]
for i in range(1,segments):
vector = vector.rotate(dtheta)
vectors.append(vector)
return vectors
def fuzzyEquality(self,a,b):
return (a-b <= 1e-8)
def circleWrap(self,points,segments):
""" Wrap a grid around the origin.
<<points>> is a list of 2- or 3-tuples.
In the case of 3-tuples, they should be laid out like: (x,y,name)
Whereas 2-tuples should eliminate the name portion.
Only one format may be passed; they may not be mixed.
x- and y- values are rounded to the nearest integer.
If more precision is desired, scale up the points before calling this function.
x-values should be within [0,segments)
Values not within range will be moved within range.
y-values must be greater than 0
An error will be raised if a y-value is less than 0.
The 'name' portion is not touched by this function; it is merely
passed along. This may be used to identify points or groups of points.
<<radius>> is the inside radius (i.e. distance to origin from a point with
a y-value of 0).
<<segments>> is the number of segments (sides) of the polygon.
<<angle>> is the angle of the diagonal of the square approximation. It must be
somewhere on (0,pi/2).
"""
angle = self.options.angle
if angle <= 0 or angle >= pi/2:
raise ValueError('Angle must be in (0,pi/2)')
vectors = self.baseVectors(segments)
theta = 2*pi/segments
"""
Determine the coefficient to multiply the vectors by in order to deal
with a higher x-value.
With R being the large radius (radius to next y-value) and r being the
small radius (radius to current y-value):
a^2 = r^2 (1 - cos(theta)) ## Cosine law
b^2 = R^2 (1 - cos(theta))
To get the most square-like trapezoid:
R - r = 0.5(a+b)
Subbing in the equations for b^2 and a^2 yields the following lines.
"""
C = sqrt(2*(1-cos(theta)))
val = 2*tan(pi/2-angle)
coeff = (val+C)/(val-C)
diff = coeff-1
## Sort points in order of increasing y-value.
named = False
if len(points[0]) == 3:
named = True
points = [(x,y,name) for x,y,name in sorted(points,key=lambda a: a[1])]
else:
points = [(x,y,None) for x,y in sorted(points,key=lambda a: a[1])]
done = []
cur_y = 0
for point in points:
x,y,name = point
## Check constraints
if y < cur_y:
raise ValueError('Invalid point (%d,%d)' % (x,y))
elif y >= cur_y+1:
## Multiply vectors accordingly
delta = floor(y-cur_y)
vectors = [(coeff**delta)*v for v in vectors]
cur_y = floor(y)
## Wrap x-value to lie in the proper place
## lazy
while x < 0:
x += segments
while x >= segments:
x -= segments
if self.fuzzyEquality(y,int(y)) and self.fuzzyEquality(x,int(x)):
x = int(x)
## Can do it the quick way
wx,wy = vectors[x].tuple
else:
## Use vector rotation
## Determine nearest vector (counterclockwise)
pointer = vectors[floor(x)]
## Scale x and y to be within (0,1)
x -= int(x)
y -= int(y)
c = C*x ## This value is used a lot, cache it
## Now the angle of rotation must be determined using cosine law
factor = 1
if not self.fuzzyEquality(x,0):
## x isn't equal to 0, must rotate vector
n2 = 1+c**2-2*c*cos((pi-theta)/2)
factor = sqrt(n2)
phi = acos((n2+1-c**2)/(2*factor))
pointer = pointer.rotate(-phi)
## Correct vector magnitude
pointer = (1+y*diff)*factor*pointer
wx,wy = pointer.tuple
if named:
done.append((wx,wy,name))
else:
done.append((wx,wy))
return done
def createGround(self,unit,rows,cols,scale=1):
""" Return a lace ground.
This function returns a list of points and corresponding lines that may
be transformed or passed to a drawing function (such as draw_image) in
order to draw a lace ground.
unit is the pattern for the lace ground, in the format returned by
loadFile.
rows and cols are integers and represent the number of horizontal repeats
and vertical repeats of the pattern, respectively.
scale is an optional value that can be used to scale the pattern before it
is repeated. Note that this comes with some constraints - the
template's rows and cols after scaling (i.e. unit['rows']*scale) must
be an integer. For example, a template with 4 rows and 4 cols before
scaling may be scaled by any integer value above 1 and select values
between 1 and 0 (namely 0.25,0.5,0.75). A scale value of 'True' may be
passed if each repeat of the template should fit within a 1x1 square.
"""
data = unit['data']
unit_rows = unit['rows']
unit_cols = unit['cols']
if scale <= 0:
raise ValueError('Scale must be greater than zero')
elif scale != 1:
## The user wants to scale the template
_data = []
for row in data:
_row = []
for c in row:
if scale == True:
_row.append([i for n in zip([a/unit_cols for a in c[::2]],[a/unit_rows for a in c[1::2]]) for i in n])
else:
_row.append([a*scale for a in c])
_data.append(_row)
data = _data
unit_rows *= scale
unit_cols *= scale
## Catching invalid input
if not self.fuzzyEquality(unit_rows,int(unit_rows)):
raise ValueError('Scale factor must result in an integer value for template rows')
if not self.fuzzyEquality(unit_cols,int(unit_cols)):
raise ValueError('Scale factor must result in an integer value for template cols')
unit_rows = int(unit_rows)
unit_cols = int(unit_cols)
line_num = 0
points = []
for c in range(cols):
## Do each column first
x = c*unit_cols
for r in range(rows):
y = r*unit_rows
for row in data:
for x1,y1,x2,y2,x3,y3 in row:
## In order to draw lines in the correct order, an extra
## point must be added
p1 = (x+x1,y+y1,'%09da,1'%line_num)
p2 = (x+x2,y+y2,'%09da,2'%line_num)
p1a = (x+x1,y+y1,'%09db,1'%line_num)
p3 = (x+x3,y+y3,'%09db,3'%line_num)
points.extend([p1,p2,p1a,p3])
line_num += 1
return points
def draw(self, points,line=lambda a: None):
""" Draw the image.
points - a list of points, as returned by createGround.
line - a function that draws a line connecting all points in the passed list in order.
"""
groups = {}
## This loop scales points, sorts them into groups, and gets image parameters
xs = []
ys = []
for x,y,n in points:
xs.append(x)
ys.append(y)
sn = n.split(',',1)
ident = 0
if len(sn) == 2:
ident = int(sn[1])
n = sn[0]
if n not in groups:
groups[n] = []
groups[n].append((x,y,ident))
max_x = max(xs)
min_x = min(xs)
max_y = max(ys)
min_y = min(ys)
## Sort all groups to draw lines in order
for group in groups:
groups[group].sort(key=lambda a:a[2])
## Sort all groups to draw groups in order
groups = sorted([(name,pts) for name,pts in groups.items()],key=lambda a:a[0])
## Draw lines
for name,pts in groups:
_pts = []
for p in pts:
_pts.append([p[0]-min_x,p[1]-min_y])
self.line(_pts)
def add_arguments(self, pars):
pars.add_argument('--file')
pars.add_argument('--angle', type=int)
pars.add_argument('--cols', type=int)
pars.add_argument('--diameter', type=float)
pars.add_argument('--diamunits')
pars.add_argument('--rows', type=int)
pars.add_argument('--linewidth', type=float)
pars.add_argument('--lineunits')
pars.add_argument('--linecolor', type=inkex.Color)
def effect(self):
## Load the file
unit = self.loadFile()
self.options.linecolor = self.options.linecolor.to_rgb()
## Change the input to universal units
self.options.diameter = self.unitToUu(str(self.options.diameter)+self.options.diamunits)
self.options.linewidth = self.unitToUu(str(self.options.linewidth)+self.options.lineunits)
## Convert the angle
self.options.angle = radians(self.options.angle)
## Ensure no y-values are below 0
min_y = min([b for a in [i[1::2] for row in unit['data'] for i in row] for b in a])
if min_y < 0:
data = []
for row in unit['data']:
_row = []
for c in row:
_row.append([a for b in zip(c[::2],[i-min_y for i in c[1::2]]) for a in b])
data.append(_row)
unit['data'] = data
## Create the ground coordinates
points = self.createGround(unit,self.options.rows,self.options.cols)
## Wrap it around a polygon
points = self.circleWrap(points,self.options.cols*unit['cols'])
## Draw everything
self.draw(points,line=lambda a: self.line(a))
if __name__ == '__main__':
CircularGroundFromTemplate().run()

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Ground From Template</name>
<id>fablabchemnitz.de.ground_from_template</id>
<label appearance="header">Fill a rectangular patch with a lace ground pattern from selected template file.</label>
<param name="file" type="path" gui-text="Template file name (full path):" mode="file" filetypes="txt">./templates/</param>
<label appearance="header">Grid description</label>
<hbox indent="1">
<param name="angle" type="float" precision="1" min="30" max="89" gui-text="Angle (degrees):">45.0</param>
</hbox>
<hbox indent="1">
<param name="distance" type="float" precision="2" min="0.01" max="1000.0" gui-text="Distance between footside pins:">5.0</param>
<param name="pinunits" gui-text=" " type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="px">px</option>
<option value="pt">pt</option>
</param>
</hbox>
<label appearance="header">Patch description</label>
<hbox indent="1">
<param name="width" type="float" precision="2" min="0.1" max="1000" gui-text="Width:">50</param>
<param name="patchunits" gui-text=" " type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="px">px</option>
<option value="pt">pt</option>
</param>
</hbox>
<hbox indent="1">
<param name="height" type="float" precision="2" min="0.1" max="1000" gui-text="Height:">50</param>
</hbox>
<label appearance="header">Line Appearance</label>
<hbox indent="1">
<param name="linewidth" type="float" precision="2" min="0.01" max="1000" gui-text="Width:">1</param>
<param name="lineunits" gui-text=" " type="optiongroup" appearance="combo">
<option value="px">px</option>
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
</param>
</hbox>
<hbox indent="1">
<param name="linecolor" type="color" appearance="colorbutton" gui-text="Color:">255</param>
</hbox>
<effect needs-live-preview="true">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Grids/Guides"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">ground_from_template.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,172 @@
#!/usr/bin/env python3
# Copyright (c) 2017, Veronika Irvine
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
from math import sin, cos, radians, ceil
from lxml import etree
import inkex
__author__ = 'Veronika Irvine'
__credits__ = ['Ben Connors', 'Veronika Irvine', 'Mark Shafer']
__license__ = 'Simplified BSD'
class GroundFromTemplate(inkex.EffectExtension):
def loadFile(self):
# Ensure that file exists and has the proper extension
if not self.options.file:
inkex.errormsg('You must specify a template file.')
exit()
self.options.file = self.options.file.strip()
if self.options.file == '':
inkex.errormsg('You must specify a template file.')
exit()
if not os.path.isfile(self.options.file):
inkex.errormsg('You have not specified a valid path for the template file.\n\nYour entry: '+self.options.file)
exit()
extension = os.path.splitext(self.options.file)[1]
if extension != '.txt':
inkex.errormsg('The file name must end with .txt.\n\nYour entry: '+self.options.file)
exit()
data = []
rowCount = 0
colCount = 0
with open(self.options.file,'r') as f:
first = True
for line in f:
if first:
# first line of file gives row count and column count
first = False
line = line.strip()
temp = line.split('\t')
type = temp[0]
rowCount = int(temp[1])
colCount = int(temp[-1])
else:
line = line.strip()
line = line.lstrip('[')
line = line.rstrip(']')
rowData = line.split(']\t[')
data.append([])
for cell in rowData:
cell = cell.strip()
data[-1].append([float(num) for num in cell.split(',')])
return {'type':type, 'rowCount':rowCount, 'colCount':colCount, 'data':data}
def line(self, x1, y1, x2, y2):
"""
Draw a line from point at (x1, y1) to point at (x2, y2).
Style of line is hard coded and specified by 's'.
"""
# define the motions
path = 'M %s,%s L %s,%s' %(x1,y1,x2,y2)
# define the stroke style
s = {'stroke-linejoin': 'miter',
'stroke-width': self.options.linewidth, 'stroke-opacity': '1.0',
'fill-opacity': '1.0', 'stroke': self.options.linecolor,
'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'fill': 'none'
}
# create attributes from style and path
attribs = {'style':str(inkex.Style(s)), 'd':path}
# insert path object into current layer
etree.SubElement(self.svg.get_current_layer(), inkex.addNS('path', 'svg'), attribs)
def draw(self, data, rowCount, colCount):
a = self.options.distance
theta = self.options.angle
deltaX = a*sin(theta)
deltaY = a*cos(theta)
maxRows = ceil(self.options.height / deltaY)
maxCols = ceil(self.options.width / deltaX)
x = 0.0
y = 0.0
repeatY = 0
repeatX = 0
while repeatY * rowCount < maxRows:
x = 0.0
repeatX = 0
while repeatX * colCount < maxCols:
for row in data:
for coords in row:
x1 = x + coords[0]*deltaX
y1 = y + coords[1]*deltaY
x2 = x + coords[2]*deltaX
y2 = y + coords[3]*deltaY
x3 = x + coords[4]*deltaX
y3 = y + coords[5]*deltaY
self.line(x1,y1,x2,y2)
self.line(x1,y1,x3,y3)
repeatX += 1
x += deltaX * colCount
repeatY += 1
y += deltaY * rowCount
def add_arguments(self, pars):
pars.add_argument('-f', '--file', help='File containing lace ground description')
pars.add_argument('--angle', type=float)
pars.add_argument('--distance', type=float)
pars.add_argument('--pinunits')
pars.add_argument('--width', type=float)
pars.add_argument('--patchunits')
pars.add_argument('--height', type=float)
pars.add_argument('--linewidth', type=float)
pars.add_argument('--lineunits')
pars.add_argument('--linecolor', type=inkex.Color)
def effect(self):
result = self.loadFile()
# Convert input to universal units
self.options.width = self.svg.unittouu(str(self.options.width)+self.options.patchunits)
self.options.height = self.svg.unittouu(str(self.options.height)+self.options.patchunits)
self.options.linewidth = self.svg.unittouu(str(self.options.linewidth)+self.options.lineunits)
self.options.distance = self.svg.unittouu(str(self.options.distance)+self.options.pinunits)
# Users expect distance to be the vertical distance between footside pins
# (vertical distance between every other row) but in the script we use it
# as the diagonal distance between grid points
# therefore convert distance based on the angle chosen
self.options.angle = radians(self.options.angle)
self.options.distance = self.options.distance/(2.0*cos(self.options.angle))
# Draw a ground based on file description and user inputs
self.options.linecolor = self.options.linecolor.to_rgb()
# For now, assume style is Checker but could change in future
self.draw(result['data'],result['rowCount'],result['colCount'])
if __name__ == '__main__':
GroundFromTemplate().run()

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Polar Grid</name>
<id>fablabchemnitz.de.polar_grid</id>
<label>Creates a printable polar grid of dots with a constant number of dots per circle and the distance between circles changing at the same speed as the distance between the dots on a circle.</label>
<label appearance="header">Grid style</label>
<hbox indent="1">
<param name="angle" type="float" precision="2" min="15" max="80" gui-text="Grid angle (degrees):">45.0</param>
</hbox>
<hbox indent="1">
<param name="dots" type="int" min="4" max="400" gui-text="Number of dots per circle:">180</param>
</hbox>
<hbox indent="1">
<param name="variant" type="optiongroup" appearance="combo" gui-text="Pattern:">
<option value="">diamond</option>
<option value="rectangle">rectangle</option>
<option value="hexagon1">hexagon (30°)</option>
<option value="hexagon2">hexagon (60°, /3)</option>
<option value="hexagon3">hexagon + triangle (30-45-60°, /2)</option>
<option value="hexagon4">hexagon + diamond (30°)</option>
<option value="hexagon5">hexagon + diamond (60°, /2)</option>
<option value="snow2">snow, hexagon (60°, /6)</option>
<option value="snow1">snow, hexagon + diamond (60°, /8)</option>
</param>
</hbox>
<label appearance="header">Grid size</label>
<hbox indent="1">
<param name="outerDiameter" type="float" precision="2" min="0.5" max="500" gui-text="Outside diameter:">160</param>
<param name="circleDiameterUnits" gui-text=" " type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="px">px</option>
<option value="pt">pt</option>
</param>
</hbox>
<hbox indent="1">
<param name="innerDiameter" type="float" precision="2" min="0.5" max="500" gui-text="Inside diameter:">100</param>
</hbox>
<hbox indent="1">
<param name="alignment" type="optiongroup" appearance="combo" gui-text="Align to:">
<option value="outside">outside circle</option>
<option value="inside">inside circle</option>
</param>
</hbox>
<label appearance="header">Dot properties</label>
<hbox indent="1">
<param name="size" type="float" precision="2" min="0.001" max="10" gui-text="Diameter:">0.5</param>
<param name="dotUnits" gui-text=" " type="optiongroup" appearance="combo">
<option value="px">px</option>
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
</param>
</hbox>
<hbox indent="1">
<param name="fill" type="color" appearance="colorbutton" gui-text="Color:">255</param>
</hbox>
<effect needs-live-preview="true">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Grids/Guides"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">polar_grid.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,184 @@
#!/usr/bin/env python3
# Copyright 2015 Jo Pol
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see http://www.gnu.org/licenses/.
from __future__ import division
from math import pi, sin, cos, tan, radians
from lxml import etree
# We will use the inkex module with the predefined
# Effect base class.
import inkex
__author__ = 'Jo Pol'
__credits__ = ['Veronika Irvine','Jo Pol','Mark Shafer']
__license__ = 'GPLv3'
class PolarGrid(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument('-a', '--angle', type=float, default=45, help='grid angle (degrees)')
pars.add_argument('-d', '--dots', type=int, default=180, help='number of dots on a circle')
pars.add_argument('-o', '--outerDiameter', type=float, default=160, help='outer diameter (mm)')
pars.add_argument('-i', '--innerDiameter', type=float, default=100, help='minimum inner diameter (mm)')
pars.add_argument('-f', '--fill', type=inkex.Color, default='-6711040', help='dot color')
pars.add_argument('-A', '--alignment', default='outside', help='exact diameter on [inside|outside]')
pars.add_argument('-s', '--size', type=float, default=0.5, help='dot diameter (mm)')
pars.add_argument('-v', '--variant', default='', help='omit rows to get [|rectangle|hexagon1]')
pars.add_argument('-cu', '--circleDiameterUnits', default = 'mm', help = 'Circle diameter is measured in these units')
pars.add_argument('-du', '--dotUnits', default = 'px', help = 'Dot diameter is measured in these unites')
def group(self, diameter):
"""
Create a group labeled with the diameter
"""
label = 'diameter: {0:.2f} mm'.format(diameter)
attribs = {inkex.addNS('label', 'inkscape'):label}
return etree.SubElement(self.gridContainer, inkex.addNS('g', 'svg'), attribs)
def dots(self, diameter, circleNr, group):
"""
Draw dots on a grid circle
"""
offset = (circleNr % 2) * 0.5
for dotNr in range (0, self.options.dots):
a = (dotNr + offset) * self.alpha
x = (diameter / 2.0) * cos(a)
y = (diameter / 2.0) * sin(a)
attribs = {'style':self.dotStyle, 'cx':str(x * self.circleScale), 'cy':str(y * self.circleScale), 'r':self.dotR}
etree.SubElement(group, inkex.addNS('circle', 'svg'), attribs)
def iterate(self, diameter, circleNr):
"""
Create a group with a ring of dots.
Returns half of the arc length between the dots
which becomes the distance to the next ring.
"""
group = self.group(diameter)
self.dots(diameter, circleNr, group)
self.generatedCircles.append(group)
return diameter * self.change
def generate(self):
"""
Generate rings with dots, either inside out or outside in
"""
circleNr = 0
flag_error = False
minimum = 2 * self.options.size * self.options.dots /pi
if minimum < self.options.innerDiameter:
minimum = self.options.innerDiameter
else:
flag_error = True
if self.options.alignment == 'outside':
diameter = self.options.outerDiameter
while diameter > minimum:
diameter -= self.iterate(diameter, circleNr)
circleNr += 1
else:
diameter = minimum
while diameter < self.options.outerDiameter:
diameter += self.iterate(diameter, circleNr)
circleNr += 1
# Display message
if flag_error:
# Leave message on top
font_height = 8
text_style = { 'font-size': str(font_height),
'font-family': 'sans-serif',
'text-anchor': 'middle',
'text-align': 'center',
'fill': '#000000' }
text_atts = {'style':str(inkex.Style(text_style)),
'x': '0', 'y': '0'}
text = etree.SubElement(self.gridContainer, 'text', text_atts)
text.text = "Dots overlap. inner changed to %4.1f" % (minimum)
def removeGroups(self, start, increment):
"""
Remove complete rings with dots
"""
for i in range(start, len(self.generatedCircles), increment):
self.svg.get_current_layer().remove(self.generatedCircles[i])
def removeDots(self, i, offset, step):
"""
Remove dots from one circle
"""
group = self.generatedCircles[i]
dots = list(group)
start = len(dots) - 1 - offset
for j in range(start, -1, 0-step):
group.remove(dots[j])
def computations(self, angle):
self.alpha = radians(360.0 / self.options.dots)
correction = pi / (4 * self.options.dots)
correction *= tan(angle*0.93)
self.change = tan(angle - correction) * pi / self.options.dots
def effect(self):
"""
Effect behaviour.
Overrides base class' method and draws something.
"""
# constants
self.dotStyle = str(inkex.Style({'fill': self.options.fill.to_rgb(),'stroke':'none'}))
self.dotScale = self.svg.unittouu("1" + self.options.dotUnits)
self.dotR = str(self.options.size * (self.dotScale/2))
self.circleScale = self.svg.unittouu("1" + self.options.circleDiameterUnits)
self.computations(radians(self.options.angle))
# processing variables
self.generatedCircles = []
self.gridContainer = self.svg.get_current_layer()
self.generate()
if self.options.variant == 'rectangle':
self.removeGroups(1, 2)
elif self.options.variant == 'hexagon1':
self.removeGroups(0, 3)
elif self.options.variant == 'hexagon2' or self.options.variant == 'snow2':
for i in range(0, len(self.generatedCircles), 1):
self.removeDots(i, (((i%2)+1)*2)%3, 3)
elif self.options.variant == 'hexagon3':
for i in range(0, len(self.generatedCircles), 2):
self.removeDots(i, (i//2+1)%2, 2)
elif self.options.variant == 'hexagon4':
self.removeGroups(0, 4)
elif self.options.variant == 'hexagon5' or self.options.variant == 'snow1':
for i in range(0, len(self.generatedCircles), 2):
self.removeDots(i, 1, 2)
self.dotStyle = str(inkex.Style({'fill': 'none','stroke':self.options.fill.to_rgb(),'stroke-width':0.7}))
self.dotR = str((((self.options.innerDiameter * pi) / self.options.dots) / 2) * self.dotScale)
self.generatedCircles = []
if self.options.variant == 'snow2':
self.options.dots = self.options.dots // 3
self.computations(radians(self.options.angle))
self.generate()
elif self.options.variant == 'snow1':
self.generate()
self.removeGroups(1, 2)
for i in range(0, len(self.generatedCircles), 2):
self.removeDots(i, i%4, 2)
for i in range(0, len(self.generatedCircles), 2):
self.removeDots(i, (i+1)%2, 2)
for i in range(2, len(self.generatedCircles), 4):
self.removeDots(i, 0, self.options.dots)
if __name__ == '__main__':
PolarGrid().run()

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Regular Grid</name>
<id>fablabchemnitz.de.regular_grid</id>
<label appearance="header">Creates a grid of dots of specified angle.</label>
<spacer/>
<label appearance="header">Grid description</label>
<hbox indent="1">
<param name="angle" type="float" precision="1" min="30" max="89" gui-text="Angle (degrees):">45.0</param>
</hbox>
<hbox>
<param name="distance" type="float" precision="2" min="0.01" max="1000" gui-text="Distance between footside pins:">5.0</param>
<param name="pinunits" gui-text=" " type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="px">px</option>
<option value="pt">pt</option>
</param>
</hbox>
<label appearance="header">Patch description</label>
<hbox indent="1">
<param name="width" type="float" precision="2" min="0.1" max="1000" gui-text="Width:">50</param>
<param name="patchunits" gui-text=" " type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="px">px</option>
<option value="pt">pt</option>
</param>
</hbox>
<hbox indent="1">
<param name="height" type="float" precision="2" min="0.1" max="1000" gui-text="Height:">50</param>
</hbox>
<label appearance="header">Dot Appearance</label>
<hbox indent="1">
<param name="dotwidth" type="float" precision="2" min="0.01" max="1000" gui-text="Diameter:">2</param>
<param name="dotunits" gui-text=" " type="optiongroup" appearance="combo">
<option value="px">px</option>
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
</param>
</hbox>
<hbox indent="1">
<param name="dotcolor" type="color" appearance="colorbutton" gui-text="Color:">255</param>
</hbox>
<effect needs-live-preview="true">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Grids/Guides"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">regular_grid.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,107 @@
#!/usr/bin/env python3
# Copyright (c) 2017, Veronika Irvine
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from math import sin, cos, radians, ceil
import inkex
from lxml import etree
__author__ = 'Veronika Irvine'
__credits__ = ['Ben Connors', 'Veronika Irvine', 'Mark Shafer']
__license__ = 'Simplified BSD'
class RegularGrid(inkex.EffectExtension):
def circle(self, x, y, r, fill):
# define the stroke style
s = {'fill': fill}
# create attributes from style and define path
attribs = {'style':str(inkex.Style(s)),
'cx':str(x),
'cy':str(y),
'r':str(r)}
# insert path object into current layer
etree.SubElement(self.svg.get_current_layer(), inkex.addNS('circle', 'svg'), attribs)
def drawDot(self, x, y):
self.circle(x, y, self.options.dotwidth, self.options.dotcolor)
def draw(self):
a = self.options.distance
theta = self.options.angle
hgrid = a*sin(theta);
vgrid = a*cos(theta)
rows = int(ceil(self.options.height / vgrid))
cols = int(ceil(self.options.width / hgrid))
y = 0.0
for r in range(rows):
x = 0.0
if (r % 2 == 1):
x += hgrid
for c in range(ceil(cols/2)):
self.drawDot(x, y)
x += 2.0*hgrid;
y += vgrid;
def add_arguments(self, pars):
pars.add_argument('--angle', type=float)
pars.add_argument('--distance', type=float)
pars.add_argument('--pinunits')
pars.add_argument('--width', type=float)
pars.add_argument('--patchunits')
pars.add_argument('--height', type=float)
pars.add_argument('--dotwidth', type=float)
pars.add_argument('--dotunits')
pars.add_argument('--dotcolor', type=inkex.Color)
def effect(self):
"""
Effect behaviour.
Overrides base class' method and draws something.
"""
# Convert user input to universal units
self.options.width = self.svg.unittouu(str(self.options.width)+self.options.patchunits)
self.options.height = self.svg.unittouu(str(self.options.height)+self.options.patchunits)
self.options.distance = self.svg.unittouu(str(self.options.distance)+self.options.pinunits)
# Convert from diameter to radius
self.options.dotwidth = self.svg.unittouu(str(self.options.dotwidth)+self.options.dotunits)/2
# Users expect distance to be the vertical distance between footside pins
# (vertical distance between every other row) but in the script we use it
# as as diagonal distance between grid points
# therefore convert distance based on the angle chosen
self.options.angle = radians(self.options.angle)
self.options.distance = self.options.distance/(2.0*cos(self.options.angle))
# Draw a grid of dots based on user inputs
self.options.dotcolor = self.options.dotcolor.to_rgb()
self.draw()
if __name__ == '__main__':
RegularGrid().run()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,2 @@
CHECKER 1 1
[0,0,1,0,-1,1]

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,3 @@
CHECKER 2 1
[0,0,1,0,0,1]
[0,1,-1,1,0,2]

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,3 @@
CHECKER 2 2
[0,0,1,0,-1,0] [1,0,0,1,2,1]
[0,1,1,1,-1,1] [1,1,0,2,2,2]

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -0,0 +1,3 @@
CHECKER 2 2
[0,0,1,0,-1,1] [1,0,2,0,1,1]
[1,1,0,2,1,2]

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,3 @@
CHECKER 2 4
[0,0,1,0,-1,0] [1,0,2,0,0,1] [2,0,3,0,1,1] [3,0,2,1,4,1]
[0,1,1,1,-1,1] [1,1,2,1,0,2] [2,1,3,1,1,2] [3,1,2,2,4,2]

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1,3 @@
CHECKER 2 4
[0,0,1,0,-1,1] [1,0,2,0,1,1] [2,0,1,1,3,1]
[1,1,0,2,1,2] [3,1,2,2,4,2]

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,3 @@
CHECKER 2 4
[0,0,1,0,-1,1] [1,0,2,0,1,1] [2,0,2,1,3,1]
[1,1,0,2,1,2] [2,1,1,1,2,2] [3,1,2,1,4,2]

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,3 @@
CHECKER 2 4
[0,0,1,0,-1,0] [1,0,2,0,0,1] [2,0,1,1,3,1] [3,0,2,0,4,1]
[0,1,1,1,-1,1] [1,1,2,1,0,2] [2,1,1,2,3,2] [3,1,2,1,4,2]

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,3 @@
CHECKER 2 4
[0,0,1,0,-1,0] [1,0,2,0,1,1] [2,0,1,1,3,1] [3,0,2,0,3,1]
[1,1,0,2,1,2] [3,1,3,2,4,2]

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -0,0 +1,3 @@
CHECKER 2 4
[0,0,1,0,-1,1] [1,0,2,0,1,1] [2,0,3,0,2,1] [3,0,4,0,3,1]
[1,1,0,2,1,2] [2,1,1,1,2,2] [3,1,2,1,3,2]

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,4 @@
CHECKER 3 3
[0,0,1,0,-1,0] [1,0,2,0,0,1] [2,0,1,1,3,1]
[0,1,1,1,-1,1] [1,1,2,1,0,2] [2,1,1,2,3,2]
[0,2,1,2,-1,2] [1,2,2,2,0,3] [2,2,1,3,3,3]

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -0,0 +1,3 @@
CHECKER 3 3
[0,0,1,0,0,2] [1,0,2,0,1,2] [2,0,3,0,2,2]
[0,2,-1,2,0,3] [1,2,0,2,1,3] [2,2,1,2,2,3]

Some files were not shown because too many files have changed in this diff Show More