going public

This commit is contained in:
jrkb 2023-09-24 18:39:52 +02:00
commit be7d9edf91
84 changed files with 85206 additions and 0 deletions

41
.gitignore vendored Normal file
View file

@ -0,0 +1,41 @@
# COMPILE TMP
bin/*
!bin/data/
!bin/data/*
!bin/web/
!bin/web/*
obj/
bin/data/ofxMsdfgen
bin/data/ofxGPUFont
# OS
.DS_Store
# EDITORS
*.qbs.user
*.swp
*.swo
*.swn
__pycache__
tags
.ycm_extra_conf.py
compile_commands.json
compile_commands.*.json
.cache
.vroot
# BACKUP
*.bk
# USER
config.make
# FONTS
# fonts have their own licenses
# and are not distributed with this code
bin/data/fonts/*
bin/web/fonts/*
# DIST
# these are related to distributing variable time
upload*

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "src/emscripten-browser-file"]
path = src/emscripten-browser-file
url = git@github.com:themancalledjakob/emscripten-browser-file.git

661
LICENSE Normal file
View file

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are 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.
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.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
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 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 work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
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 AGPL, see
<https://www.gnu.org/licenses/>.

13
Makefile Normal file
View file

@ -0,0 +1,13 @@
# Attempt to load a config.make file.
# If none is found, project defaults in config.project.make will be used.
ifneq ($(wildcard config.make),)
include config.make
endif
# make sure the the OF_ROOT location is defined
ifndef OF_ROOT
OF_ROOT=$(realpath ../../..)
endif
# call the project makefile!
include $(OF_ROOT)/libs/openFrameworksCompiled/project/makefileCommon/compile.project.mk

48
README.md Normal file
View file

@ -0,0 +1,48 @@
# Variable Time
## instructions
Instructions on how to use Variable Time are available [here](https://git.pointer.click/variablelab/variabletime/wiki)
## issues
If you have issues with Variable Time, please write us at `variabletime at pointer.click`.
## run
Though it is possible to run this on your own server, we expect that you will probably just want to visit [variable time](https://variabletime.pointer.click)
Should you not be afraid to dive into the mysterious world of developing Variable Time, please go ahead.
### compile and run
Install all dependencies, then
`$ emmake make && python3 serve.py`
## dependencies
### depends on:
- [openFrameworks 0.12.0](https://openframeworks.cc)
- [ofxVariableLab](https://git.pointer.click/variablelab/ofxvariablelab)
- [ofxMsdfgen](https://git.pointer.click/ofxAddons/ofxmsdfgen)
- [ofxGPUFont](https://git.pointer.click/ofxAddons/ofxgpufont)
- [Emscripten SDK](https://github.com/emscripten-core/emsdk)
### optional:
- [ofxProfiler](https://git.pointer.click/ofxAddons/ofxProfiler)
### included dependencies:
- [ffmpeg.wasm](https://github.com/ffmpegwasm/ffmpeg.wasm)
- [theatre-js](https://git.pointer.click/variablelab/theatre)
we use a custom fork of [theatre-js](https://theatrejs.com), which is a fantastic animation library and framework
- [miniz](https://github.com/richgel999/miniz)
- [emscripten-browser-file](https://github.com/Armchair-Software/emscripten-browser-file)
## development notes:
### https local development
- install [mkcert](https://github.com/FiloSottile/mkcert)
- follow instructions and install certificate file in bin/ssl
### convenience scripts
`$ ./lightclean.sh && emmake make -j$(nproc) && python3 serve.py`
Note: As we load shaders from external repositories, they need to be copied to the data directory before emscripten compilation. This happens automatically if we delete them first.
### fonts
Fonts have different licensing and are not included in this repository.

4
addons.make Normal file
View file

@ -0,0 +1,4 @@
ofxMsdfgen
ofxGPUFont
ofxVariableLab
ofxProfiler

529
assets/template.html Normal file
View file

@ -0,0 +1,529 @@
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="icon" type="image/x-icon" href="/web/assets/vt-favicon.png">
<title>variable time</title>
<style>
html, body {
overscroll-behavior-x: none;
}
body {
font-family: helvetica, sans-serif;
margin: 0;
padding: none;
}
.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
div.emscripten { text-align: center; }
div.emscripten_border { border: 1px solid black; }
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
canvas.emscripten { border: 0px none; outline: none; }
#logo {
display: inline-block;
margin: 20px 0 20px 20px;
}
.spinner {
height: 30px;
width: 30px;
margin: 0;
margin-top: 20px;
margin-left: 20px;
display: inline-block;
vertical-align: top;
-webkit-animation: rotation .8s linear infinite;
-moz-animation: rotation .8s linear infinite;
-o-animation: rotation .8s linear infinite;
animation: rotation 0.8s linear infinite;
border-left: 5px solid #EE3987;
border-right: 5px solid #EE3987;
border-bottom: 5px solid #EE3987;
border-top: 5px solid #CCCCCC;
border-radius: 100%;
background-color: #EEEEEE;
}
@-webkit-keyframes rotation {
from {-webkit-transform: rotate(0deg);}
to {-webkit-transform: rotate(360deg);}
}
@-moz-keyframes rotation {
from {-moz-transform: rotate(0deg);}
to {-moz-transform: rotate(360deg);}
}
@-o-keyframes rotation {
from {-o-transform: rotate(0deg);}
to {-o-transform: rotate(360deg);}
}
@keyframes rotation {
from {transform: rotate(0deg);}
to {transform: rotate(360deg);}
}
#status {
display: inline-block;
vertical-align: top;
margin-top: 30px;
margin-left: 20px;
font-weight: bold;
color: rgb(120, 120, 120);
}
#progress {
height: 20px;
width: 30px;
}
#controls {
display: inline-block;
float: right;
vertical-align: top;
margin-top: 30px;
margin-right: 20px;
}
#output {
width: 100%;
height: 200px;
margin: 0 auto;
margin-top: 10px;
display: block;
background-color: green;
color: black;
font-family: 'Lucida Console', Monaco, monospace;
outline: none;
}
#logo, .spinner, .emscripten, #controls, .emscripten, #output {
display: none;
}
canvas.emscripten {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#timeline {
display: none;
}
#midi_open {
display: none;
}
</style>
<link rel="stylesheet" type="text/css" href="/web/css/demo.css" />
</head>
<body id="body">
<div id="loader">
<div class="loaderChild">
<h1>loading</h1>
<p id="loader_progress_task">task</p>
<p id="loader_progress">|----------------------------------------------------------------------------------------------------|</p>
</div>
</div>
<a id="logo" href="http://emscripten.org">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="110px" height="58px" viewBox="0 0 110 58" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>openFrameworks Logo</title>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="#000000" fill-rule="evenodd">
<path id="Oval-1" d="M58,29 C58,13 45,0 29,0 C13,0 0,13 0,29 C0,45 13,58 29,58 C45,58 58,45 58,29 Z"></path>
<rect id="Rectangle-1" x="59" y="0" width="25" height="58"></rect>
<rect id="Rectangle-2" x="85" y="26" width="15" height="15"></rect>
<path id="Path-2" d="M85,0 L110,0 L85,25 L85,0 Z"></path>
</g>
</svg>
</a>
<div class="spinner" id='spinner'></div>
<div class="emscripten" id="status">Downloading...</div>
<span id='controls'>
<span><input type="checkbox" id="resize">Resize canvas</span>
<span><input type="checkbox" id="pointerLock">Lock/hide mouse pointer &nbsp;&nbsp;&nbsp;</span>
<span><input type="button" value="Fullscreen" onclick="Module.requestFullscreen(document.getElementById('pointerLock').checked,
document.getElementById('resize').checked)">
</span>
</span>
<div class="emscripten">
<progress value="0" max="100" id="progress" hidden=1></progress>
</div>
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
<textarea id="output" rows="8"></textarea>
<script type='text/javascript'>
var VARIABLE_TIME_VERSION = "0.0.1";
window.setLoadingTask = (task = 'loading', percent = 0, niceText = "|") => {
document.getElementById('loader_progress_task').innerHTML = task;
let innerHTML = "|";
for (let i = 0; i < 100; i++) {
if (i < percent) {
innerHTML += niceText[i % niceText.length];
} else {
innerHTML += "-";
}
}
innerHTML += "|";
let progress = document.getElementById("loader_progress");
progress.innerHTML = innerHTML;
};
window.setLoadingDone = () => {
document.getElementById('loader').style.display = 'none';
};
var statusElement = document.getElementById('status');
var progressElement = document.getElementById('progress');
var spinnerElement = document.getElementById('spinner');
var initializedModuleEvent = new Event('initializedModule');
window.moduleInitialized = false;
window.addEventListener('initializedModule', () => {
window.moduleInitialized = true;
});
var Module = {
preRun: [],
postRun: [function() {
window.dispatchEvent(initializedModuleEvent);
// NOTE: this can be used to know when to load saved sessions
}],
print: (function() {
var element = document.getElementById('output');
if (element) element.value = ''; // clear browser cache
return function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
// These replacements are necessary if you render to raw HTML
//text = text.replace(/&/g, "&amp;");
//text = text.replace(/</g, "&lt;");
//text = text.replace(/>/g, "&gt;");
//text = text.replace('\n', '<br>', 'g');
console.log(text);
if (element) {
element.value += text + "\n";
element.scrollTop = element.scrollHeight; // focus on bottom
}
};
})(),
printErr: function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
if (0) { // XXX disabled for safety typeof dump == 'function') {
dump(text + '\n'); // fast, straight to the real console
} else {
console.error(text);
}
},
canvas: (function() {
var canvas = document.getElementById('canvas');
// As a default initial behavior, pop up an alert when webgl context is lost. To make your
// application robust, you may want to override this behavior before shipping!
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
return canvas;
})(),
setStatus: function(text) {
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
if (text === Module.setStatus.text) return;
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
var now = Date.now();
if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
if (m) {
text = m[1];
progressElement.value = parseInt(m[2])*100;
progressElement.max = parseInt(m[4])*100;
progressElement.hidden = false;
spinnerElement.hidden = false;
} else {
progressElement.value = null;
progressElement.max = null;
progressElement.hidden = true;
if (!text) spinnerElement.style.display = 'none';
}
statusElement.innerHTML = text;
},
totalDependencies: 0,
monitorRunDependencies: function(left) {
this.totalDependencies = Math.max(this.totalDependencies, left);
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
}
};
Module.setStatus('Downloading...');
window.onerror = function(event) {
// TODO: do not warn on ok events like simulating an infinite loop or exitStatus
Module.setStatus('Exception thrown, see JavaScript console');
spinnerElement.style.display = 'none';
Module.setStatus = function(text) {
if (text) Module.printErr('[post-exception status] ' + text);
};
};
let isProfiling = false;
window.toggleProfiling = () => {
if (!isProfiling) {
Module.startProfiling();
isProfiling = true;
document.getElementById('debug_profiling').innerHTML = "debug end profiling";
} else {
Module.endProfiling();
isProfiling = false;
document.getElementById('debug_profiling').innerHTML = "debug start profiling";
}
};
</script>
{{{ SCRIPT }}}
<div id="content">
</div>
<div id="layer_panel" class="panel">
<!--<div class="header">-->
<!--<div class="move">move</div>-->
<!--</div>-->
<button id="midi_open">midi</button>
<button id="exporter_open">export</button>
<button id="save_project" onclick="window.tp.downloadProject()">save project</button>
<button id="open_project" onclick="window.tp.uploadProject(true)">open project</button>
<button id="start_new_project" onclick="window.tp.startNewProject()">start new project</button>
<!--<button id="debug_profiling" onclick="window.toggleProfiling()">debug start profiling</button>-->
</div>
<!-- EXPORT BEGIN -->
<div id="exporter">
<div class="exporterChild">
<p id="exporter_close">close</p>
<!--<video id="player" controls></video>-->
<div class="exporter_options_buttons">
<div id="exporter_options">
<div class="exporter_options_child">
<div class="options_cont">
<p class="options_title">Working resolution:</p>
<div class="options">
<p class="artboard_width"></p>
<p class="artboard_height"></p>
<p class="artboard_pixelDensity"></p>
</div>
</div>
<div class="options_cont">
<p class="options_title">Render options (to be adjusted):</p>
<div class="options adjustable">
<label id="artboard_scale_label" for="artboard_scale"></label>
<input type="number" id="artboard_scale" name="artboard_scale" value="1.0" min="0.01" max="8.0" step="0.01" />
<label id="render_timescale_label" for="render_timescale"></label>
<input type="number" id="render_timescale" name="render_timescale" value="1.0" min="0.01" max="8.0" step="0.01" />
</div>
</div>
<div class="options_cont">
<p class="options_title">Render output values:</p>
<div class="options">
<p class="render_width"></p>
<p class="render_height"></p>
<p class="render_pixels"></p>
<p class="render_length"></p>
</div>
</div>
</div>
<div class="exporter_dimension_warning">
<p>
The artboard is too large to render as mp4.* You can still render into a zipfile with still frames. You can convert these into a video with ffmpeg, <a href="https://support.apple.com/guide/quicktime-player/create-a-movie-with-an-image-sequence-qtp315cce984/mac">QuickTime</a> or a Video Editor of your choice.<br><br>* To render as mp4, please scale the artboard dimensions until the render dimensions do not exceed a total of 2.073.600 (1920 * 1080) pixels.
</p>
</div>
</div>
<div class="exportButtonsCont">
<button id="exporter_button_zip" onclick="window.renderFrames('zip')">render frames</button>
<button id="exporter_button_mp4" onclick="window.renderFrames('mp4')">render mp4</button>
<!--<button onclick="window.debugProfile()">debug profile</button>-->
</div>
</div>
<div id="exporter_render_info"><p>Please keep tab open while rendering</p></div>
<p id="export_progress_task">Progress:</p>
<p id="export_progress">|----------------------------------------------------------------------------------------------------|</p>
</div>
</div>
<!-- EXPORT END -->
<div id="notice">
<div class="content">
<div class="what"><p></p></div>
<div class="details"><p></p></div>
</div>
</div>
<!-- MIDI BEGIN -->
<div id="midiController">
<div class="midiMessages"></div>
<div class="inputs"></div>
<div class="outputs"></div>
<div class="buttons">
</div>
</div>
<!-- MIDI END -->
<!-- ABOUT BEGIN -->
<div class="buttons-bottom">
<!-- <div class="links">
<a class="button-overlay" href="https://gitlab.com/pointerstudio" target="_blank">
git↗</a>
<a class="button-overlay" href="https://www.instagram.com/variablelab" target="_blank">ig↗</a>
</div> -->
<div class="button-overlay about" onclick="showAbout()">
about
</div>
</div>
<div class="overlay-text-cont hidden" id="overlay-text-cont">
<div class="overlay-text">
<div class="vt-title">
variable time*
</div>
<div class="about-text">
<!-- <p>Variable Time is a tool for working with variable text in motion and states of transition.</p>
<p>It is developed by artist duo and creative coders studio Pointer*<a class="button-overlay link-in-text first-link" href="https://pointer.click" target="_blank">web↗</a><a class="button-overlay link-in-text" href="https://instagram.com/pointer_studio" target="_blank">ig↗</a> in collaboration with type designer Céline Hurka<a class="button-overlay link-in-text first-link" href="https://celine-hurka.com" target="_blank">web↗</a><a class="button-overlay link-in-text" href="https://www.instagram.com/celinehurka" target="_blank">ig↗</a>. Variable Time is a personal project, which is a part of Variable Lab<a class="button-overlay link-in-text first-link" href="https://variablelab.pointer.click" target="_blank">web↗</a><a class="button-overlay link-in-text" href="https://www.instagram.com/variablelab" target="_blank">ig↗</a> - a series of technical, typographic, visual and theoretical exchanges between studio Pointer*, Celine Hurka and their various collaborators. The toolset of Variable Time will be growing with new experimental features alongside their ongoing research.</p> -->
<div class="textParent aboutParent">
<p>Variable Time is an open-source tool for working with typography in motion and states of transition.</p>
<p>It is developed by artist duo and creative coders studio Pointer<a class="button-overlay link-in-text first-link" href="https://pointer.click" target="_blank">WEB↗</a><a class="button-overlay link-in-text" href="https://instagram.com/pointer_studio" target="_blank">IG↗</a>in collaboration with type designer Céline Hurka<a class="button-overlay link-in-text first-link" href="https://celine-hurka.com" target="_blank">WEB↗</a><a class="button-overlay link-in-text" href="https://www.instagram.com/celinehurka" target="_blank">IG↗</a>. Variable Time is an ongoing personal exploration, driven by their fascination with variable font formats and their potential impact on the future of verbal and non-verbal communication. This tool is one of the first and foundational projects of Variable Lab<a class="button-overlay link-in-text first-link" href="https://variablelab.pointer.click" target="_blank">WEB↗</a><a class="button-overlay link-in-text" href="https://www.instagram.com/variablelab" target="_blank">IG↗</a> a series of technical, typographic, visual and theoretical exchanges between studio Pointer and Céline Hurka. The Variable Time toolkit is designed to evolve in parallel with their ongoing research, continuously expanding with new experimental controls and features.</p>
</div>
<div class="textParent toExtend">
<h4>Current Version <button class="expandText">+</button></h4>
<div class="expanded">
<p>This is the first released version of Variable Time. It is an ongoing project for which we have many future plans and improvements.</p>
<p>Due to the way we approached font rendering, a lot of seemingly standard functionalities had to be rebuild from scratch. This gives us a lot of freedom and offers new ways in which we can approach working with typography, however it also means that some things might take longer to develop.</p>
<p>The current version of the tool only supports a basic Latin character set - so A-Z, 1-9 and punctuation. Extending it with other languages and alternates is on our priority list as well and will hopefully be possible in the upcoming versions.</p>
<p>Kerning. There are multiple ways of storing and reading Kerning tables, and we currently only support one of them. Supporting more sophisticated kerning is also on our priority list.</p>
<p>Apart from that we have exciting plans for the toolkit of Variable Time. Among other things, we are already working on addons that would allow controlling parameters with sound, recording changes live and linking sequences to more types of automated inputs.</p>
</div>
</div>
<div class="instructions">
<!-- <h4>How to use - (extremely) brief intro: </h4> -->
<div class="textParent toExtend">
<h4>Layers <button class="expandText">+</button></h4>
<div class="expanded">
<p>Variable Time allows to design and export time-based typography with an unlimited amount of layers. You can create, remove, duplicate and select layers through the overview panel in the left top corner. </p>
<video muted autoplay controls=0 loop>
<source src="/web/assets/addlayer.mp4" type="video/mp4">
<p>This is fallback content to display for user agents that do not support the video tag.</p>
</video>
</div>
</div>
<div class="textParent toExtend">
<h4>Panel <button class="expandText">+</button></h4>
<div class="expanded">
<p>The panel of Variable Time consists of a number of modifiable parameters. You can adjust these parameters per layer. You can also change the settings of an artboard through the panel if artboard is selected.</p>
<video muted autoplay controls=0 loop>
<source src="/web/assets/props.mp4" type="video/mp4">
<p>This is fallback content to display for user agents that do not support the video tag.</p>
</video>
</div>
</div>
<div class="textParent toExtend">
<h4>Sequence <button class="expandText">+</button></h4>
<div class="expanded">
<p>You can create a new sequence per each modifiable parameter in the panel by clicking on the keyframe (diamond) button on its left. This will create an empty sequence. For adding a keyframe, drag the time indicator to the right place and click on the same keyframe button again.</p>
<video muted autoplay controls=0 loop>
<source src="/web/assets/sequenceprop.mp4" type="video/mp4">
<p>This is fallback content to display for user agents that do not support the video tag.</p>
</video>
<video muted autoplay controls=0 loop>
<source src="/web/assets/addkeyframes.mp4" type="video/mp4">
<p>This is fallback content to display for user agents that do not support the video tag.</p>
</video>
<p>
You can change the duration of a sequence by dragging its right edge in the sequence panel.</p>
<video muted autoplay controls=0 loop>
<source src="/web/assets/changeduration.mp4" type="video/mp4">
<p>This is fallback content to display for user agents that do not support the video tag.</p>
</video>
<p>Selecting multiple keyframes is possible by pressing and holding SHIFT key and dragging the mouse over the keyframes.</p>
<video muted autoplay controls=0 loop>
<source src="/web/assets/selectkeyframes.mp4" type="video/mp4">
<p>This is fallback content to display for user agents that do not support the video tag.</p>
</video>
<p>The easing style can be seen by clicking on the curves button next to the name of sequenced parameter. You can select different easing functions by selecting keyframes and clicking on the timeline in-between them.</p>
<video muted autoplay controls=0 loop>
<source src="/web/assets/showeasing.mp4" type="video/mp4">
<p>This is fallback content to display for user agents that do not support the video tag.</p>
</video>
<video muted autoplay controls=0 loop>
<source src="/web/assets/changeeasing.mp4" type="video/mp4">
<p>This is fallback content to display for user agents that do not support the video tag.</p>
</video>
</div>
</div>
<div class="textParent toExtend">
<h4>Letter Delays <button class="expandText">+</button></h4>
<div class="expanded">
<p>When a parameter is sequenced, a new feature appears in the panel - letter delays. This feature applies animations to each letter with a given delay time. The delay time can differ per each animated parameter.</p>
<video muted autoplay controls=0 loop>
<source src="/web/assets/letter-delays.mp4" type="video/mp4">
<p>This is fallback content to display for user agents that do not support the video tag.</p>
</video>
</div>
</div>
<div class="textParent toExtend">
<h4>Export <button class="expandText">+</button></h4>
<div class="expanded">
<p>Variable Time allows exporting video in mp4 format and static frames. At this step it is also possible to scale the output file resolution and apply time stretch - so make your output speed slower or faster proportionally.*</p>
<p class="note">* Please keep in mind that rendering in mp4 has limits in resolution, however there is (almost) no limit in rendering frames (which you can easily convert to video with the help of various external tools).</p>
</div>
</div>
</div>
<div class="textParent toExtend">
<h4>Typefaces <button class="expandText">+</button></h4>
<div class="expanded">
<p>Variable Time features experimental variable fonts designed by Céline Hurka<a class="button-overlay link-in-text first-link" href="https://celine-hurka.com" target="_blank">WEB↗</a><a class="button-overlay link-in-text" href="https://www.instagram.com/celinehurka" target="_blank">IG↗</a> and Jacob Wise<a class="button-overlay link-in-text first-link" href="https://wisetype.nl" target="_blank">WEB↗</a><a class="button-overlay link-in-text first-link" href="https://www.instagram.com/wise_type" target="_blank">IG↗</a>. These typefaces can be used freely to explore the tool or to be used for personal non-commercial projects made with the tool. If you want to use them for commercial or client work, or if you are not sure if your case applies, please reach out to Céline Hurka and/or Jacob Wise to discuss the possibilities. </p>
<p>Currently, Tonka, the Version collection and Zaft2 are included. Tonka is available on celine-hurka.com<a class="button-overlay link-in-text first-link" href="https://celine-hurka.com/tonka/" target="_blank">WEB↗</a>. Version is Céline Hurkas Type and Media graduation project and has not been published anywhere else yet. Zaft2 is a collaborative project by Céline Hurka and Jacob Wise and is available on wisetype.nl<a class="button-overlay link-in-text first-link" href="https://wisetype.nl/collections/wt-zaft-2" target="_blank">WEB↗</a> since 2021.</p>
</div>
</div>
<div class="textParent toExtend">
<h4>Used frameworks <button class="expandText">+</button></h4>
<div class="expanded">
<p>Variable Time is built up on other open source libraries, such as openFrameworks<a class="button-overlay link-in-text first-link" href="https://www.openframeworks.cc/" target="_blank">WEB↗</a>, FreeType<a class="button-overlay link-in-text first-link" href="https://www.freetype.org/" target="_blank">WEB↗</a>, Theatre.js<a class="button-overlay link-in-text first-link" href="https://www.theatrejs.com/" target="_blank">WEB↗</a>, FFmpeg<a class="button-overlay link-in-text first-link" href="https://www.ffmpeg.org/" target="_blank">WEB↗</a> and more. For a detailed list, please visit the Variable Time source code.</p>
</div>
</div>
<!-- <div class="textParent toExtend">
<h4>Donate <button class="expandText">+</button></h4>
<div class="expanded">
<p>Variable Time is completely free of charge and brings us no commercial profit. A donation of any size would help us to dedicate more time and effort to expanding its toolkit and fixing bugs. ❤️ </p>
<a class="button-overlay" href="https://www.freetype.org/" target="_blank">DONATE↗</a>
</div>
</div> -->
</div>
<div class="button-overlay button-close" onclick="hideAbout()">
close
</div>
<div class="links">
<a class="button-overlay" href="https://gitlab.com/pointerstudio" target="_blank">
git↗</a>
<a class="button-overlay" href="https://www.instagram.com/variablelab" target="_blank">ig↗</a>
<a class="button-overlay" href="mailto:variabletime@pointer.click" target="_blank">EMAIL↗</a>
</div>
<div class="logos">
<p class="contact_us">
We would be excited to see what you make with Variable Time! Feel free to share your visuals with us via <a class="button-overlay link-in-text first-link" href="mailto:variabletime@pointer.click" target="_blank">EMAIL↗</a><br>
</p>
<p>This project is made possible thanks to support from Stimuleringsfonds</p>
<img src="/web/assets/SCI_Woordbeeld_EN_3_regels_RGB.gif" />
</div>
</div>
</div>
<!-- ABOUT END -->
<div id="timeline"><div id="timeline_head"></div><div>
<!-- EXPORT BEGIN -->
<script id="ffmpeg.min.js" type="application/javascript" src="/web/ffmpeg_modules/ffmpeg.min.js"></script>
<!-- EXPORT END -->
<script type="module" src="/web/js/main.js"> </script>
</body>
</html>

BIN
bin/data/420px-01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
bin/data/42px-01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,4 @@
{
"backgroundColor": [0.8313, 0.8313, 0.8313, 1.0],
"tmpExportDir": "data/export"
}

View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 21 21" style="enable-background:new 0 0 21 21;" xml:space="preserve">
<polygon points="12.6,0.3 8.6,0.3 8.6,8.4 0.3,8.4 0.3,12.4 8.6,12.4 8.6,20.6 12.6,20.6 12.6,12.4 20.6,12.4 20.6,8.4 12.6,8.4 "/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

BIN
bin/web/assets/addlayer.mp4 Normal file

Binary file not shown.

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 172 162" style="enable-background:new 0 0 172 162;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;}
.st1{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:37.6701,10.4639;}
.st2{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:38.4324,10.6757;}
</style>
<g>
<path d="M101,68.3c7.7,0,14,6.3,14,14v29.9c0,7.7-6.3,14-14,14H71.1c-7.7,0-14-6.3-14-14V82.3c0-7.7,6.3-14,14-14H101 M101,55.3
H71.1c-14.9,0-27,12.1-27,27v29.9c0,14.9,12.1,27,27,27H101c14.9,0,27-12.1,27-27V82.3C128.1,67.4,116,55.3,101,55.3L101,55.3z"/>
</g>
<g>
<rect x="0.1" y="148.8" width="171.9" height="13"/>
</g>
<g>
<rect x="0.1" y="0.7" width="171.9" height="13"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 203 155" style="enable-background:new 0 0 203 155;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;}
.st1{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:37.6701,10.4639;}
.st2{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:38.4324,10.6757;}
</style>
<g>
<path d="M86,106.8c-7.7,0-14-6.3-14-14V62.9c0-7.7,6.3-14,14-14h29.9c7.7,0,14,6.3,14,14v29.9c0,7.7-6.3,14-14,14H86 M86,119.8
h29.9c14.9,0,27-12.1,27-27V62.9c0-14.9-12.1-27-27-27H86c-14.9,0-27,12.1-27,27v29.9C58.9,107.7,71.1,119.8,86,119.8L86,119.8z"/>
</g>
<g>
<rect x="189.4" y="0.4" width="13" height="155"/>
</g>
<g>
<rect x="-0.2" y="0.4" width="13" height="155"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 172 162" style="enable-background:new 0 0 172 162;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;}
.st1{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:37.6701,10.4639;}
.st2{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:38.4324,10.6757;}
</style>
<g>
<path d="M100.9,52.2c7.7,0,14,6.3,14,14v29.9c0,7.7-6.3,14-14,14H71c-7.7,0-14-6.3-14-14V66.3c0-7.7,6.3-14,14-14H100.9
M100.9,39.2H71c-14.9,0-27,12.1-27,27v29.9c0,14.9,12.1,27,27,27h29.9c14.9,0,27-12.1,27-27V66.3
C127.9,51.3,115.8,39.2,100.9,39.2L100.9,39.2z"/>
</g>
<g>
<rect x="0" y="0.7" width="171.9" height="13"/>
</g>
<g>
<rect x="0" y="148.8" width="171.9" height="13"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 204 155" style="enable-background:new 0 0 204 155;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;}
.st1{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:37.6701,10.4639;}
.st2{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:38.4324,10.6757;}
</style>
<g>
<path d="M87.3,48.9c7.7,0,14,6.3,14,14v29.9c0,7.7-6.3,14-14,14H57.4c-7.7,0-14-6.3-14-14V62.9c0-7.7,6.3-14,14-14H87.3 M87.3,35.9
H57.4c-14.9,0-27,12.1-27,27v29.9c0,14.9,12.1,27,27,27h29.9c14.9,0,27-12.1,27-27V62.9C114.3,48,102.2,35.9,87.3,35.9L87.3,35.9z"
/>
</g>
<g>
<rect x="0.8" y="0.4" width="13" height="155"/>
</g>
<g>
<rect x="190.4" y="0.4" width="13" height="155"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 203 155" style="enable-background:new 0 0 203 155;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;}
.st1{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:37.6701,10.4639;}
.st2{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:38.4324,10.6757;}
</style>
<g>
<path d="M115,106.8c-7.7,0-14-6.3-14-14V62.9c0-7.7,6.3-14,14-14h29.9c7.7,0,14,6.3,14,14v29.9c0,7.7-6.3,14-14,14H115 M115,119.8
h29.9c14.9,0,27-12.1,27-27V62.9c0-14.9-12.1-27-27-27H115c-14.9,0-27,12.1-27,27v29.9C87.9,107.7,100.1,119.8,115,119.8L115,119.8
z"/>
</g>
<g>
<rect x="189.4" y="0.4" width="13" height="155"/>
</g>
<g>
<rect x="-0.2" y="0.4" width="13" height="155"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 168 119" style="enable-background:new 0 0 168 119;" xml:space="preserve">
<g>
<rect x="-0.5" y="0" width="168" height="11"/>
</g>
<g>
<rect x="20.5" y="27" width="126" height="11"/>
</g>
<g>
<rect x="-0.5" y="54" width="168" height="11"/>
</g>
<g>
<rect x="-0.5" y="108" width="168" height="11"/>
</g>
<g>
<rect x="20.5" y="81" width="126" height="11"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 646 B

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 168 119" style="enable-background:new 0 0 168 119;" xml:space="preserve">
<g>
<rect x="-0.5" y="0" width="168" height="11"/>
</g>
<g>
<rect y="27" width="126" height="11"/>
</g>
<g>
<rect y="54" width="168" height="11"/>
</g>
<g>
<rect y="108" width="168" height="11"/>
</g>
<g>
<rect y="81" width="126" height="11"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 610 B

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 168 119" style="enable-background:new 0 0 168 119;" xml:space="preserve">
<g>
<rect y="0" width="168" height="11"/>
</g>
<g>
<rect x="42" y="27" width="126" height="11"/>
</g>
<g>
<rect y="54" width="168" height="11"/>
</g>
<g>
<rect y="108" width="168" height="11"/>
</g>
<g>
<rect x="42" y="81" width="126" height="11"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 615 B

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 172 162" style="enable-background:new 0 0 172 162;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;}
.st1{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:37.6701,10.4639;}
.st2{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:38.4324,10.6757;}
</style>
<g>
<path d="M101,37.1c7.7,0,14,6.3,14,14V81c0,7.7-6.3,14-14,14H71.1c-7.7,0-14-6.3-14-14V51.1c0-7.7,6.3-14,14-14H101 M101,24.1H71.1
c-14.9,0-27,12.1-27,27V81c0,14.9,12.1,27,27,27H101c14.9,0,27-12.1,27-27V51.1C128,36.2,115.9,24.1,101,24.1L101,24.1z"/>
</g>
<g>
<rect x="0.1" y="0.7" width="171.9" height="13"/>
</g>
<g>
<rect x="0.1" y="148.8" width="171.9" height="13"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 18 18" style="enable-background:new 0 0 18 18;" xml:space="preserve">
<polygon points="17.6,3.3 14.8,0.5 9.1,6.2 3.2,0.3 0.4,3.2 6.3,9 0.4,14.8 3.3,17.7 9.1,11.9 14.7,17.5 17.6,14.7 11.9,9 "/>
</svg>

After

Width:  |  Height:  |  Size: 475 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 19" style="enable-background:new 0 0 28 19;" xml:space="preserve">
<path d="M26.9,7.3l-6.2-6.2c-1.2-1.2-3.2-1.2-4.4,0L14,3.4l-2.3-2.3c-0.6-0.6-1.4-0.9-2.2-0.9c-0.8,0-1.6,0.3-2.2,0.9L1.1,7.3
c-1.2,1.2-1.2,3.2,0,4.4l6.2,6.2c0.6,0.6,1.4,0.9,2.2,0.9c0.8,0,1.6-0.3,2.2-0.9l2.3-2.3l2.3,2.3c1.2,1.2,3.2,1.2,4.4,0l6.2-6.2
C28.1,10.5,28.1,8.5,26.9,7.3z M10.3,16.5c-0.2,0.2-0.5,0.3-0.8,0.3s-0.6-0.1-0.8-0.3l-6.2-6.2c-0.2-0.2-0.3-0.5-0.3-0.8
c0-0.3,0.1-0.6,0.3-0.8l6.2-6.2c0.2-0.2,0.5-0.3,0.8-0.3s0.6,0.1,0.8,0.3l2.3,2.3l-2.5,2.5c-1.2,1.2-1.2,3.2,0,4.4l2.5,2.5
L10.3,16.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 61.4 137" style="enable-background:new 0 0 61.4 137;" xml:space="preserve">
<g>
<g>
<path d="M60.7,46.1l-25-43.3C33.5-1,28-1,25.8,2.9l-25,43.3c-2.2,3.8,0.6,8.6,5,8.6h50C60.1,54.7,62.9,49.9,60.7,46.1z"/>
</g>
</g>
<g>
<g>
<path d="M0.8,90.9l25,43.3c2.2,3.8,7.7,3.8,9.9,0l25-43.3c2.2-3.8-0.6-8.6-5-8.6h-50C1.3,82.3-1.4,87,0.8,90.9z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 633 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 61.4 54.7" style="enable-background:new 0 0 61.4 54.7;" xml:space="preserve">
<g>
<g>
<path d="M0.8,8.6l25,43.3c2.2,3.8,7.7,3.8,9.9,0l25-43.3c2.2-3.8-0.6-8.6-5-8.6h-50C1.3,0-1.4,4.8,0.8,8.6z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 490 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 61.4 54.7" style="enable-background:new 0 0 61.4 54.7;" xml:space="preserve">
<g>
<g>
<path d="M60.7,46.1l-25-43.3C33.5-1,28-1,25.8,2.9l-25,43.3c-2.2,3.8,0.6,8.6,5,8.6h50C60.1,54.7,62.9,49.9,60.7,46.1z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 501 B

BIN
bin/web/assets/props.mp4 Normal file

Binary file not shown.

BIN
bin/web/assets/sci_logo.mp4 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 220 153" style="enable-background:new 0 0 220 153;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;}
.st1{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:37.6701,10.4639;}
.st2{fill:none;stroke:#000000;stroke-width:13;stroke-miterlimit:10;stroke-dasharray:38.4324,10.6757;}
</style>
<g>
<rect y="0" width="178" height="13"/>
<rect y="44" width="138" height="13"/>
<rect y="88" width="178" height="13"/>
<rect y="132" width="138" height="13"/>
<path d="M192,44h-18.3v-7.8L149,50.5l24.7,14.3V57H192c15.7,0,28.5-12.8,28.5-28.5S207.7,0,192,0v13c8.5,0,15.5,7,15.5,15.5
S200.5,44,192,44z"/>
<path d="M192,88v13c8.5,0,15.5,7,15.5,15.5s-7,15.5-15.5,15.5h-18.3v-7.8L149,138.5l24.7,14.3V145H192c15.7,0,28.5-12.8,28.5-28.5
S207.7,88,192,88z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

928
bin/web/css/demo.css Executable file
View file

@ -0,0 +1,928 @@
:root {
--padding: 20px;
--about-w: 30vw;
}
body {
position: fixed;
top: 0px;
left: 0px;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
body:not(.debug) .debug {
display: none;
}
body.debug div:not(.centerLine) {
border: 1px solid green;
}
.hideBody {
display: none;
}
/*
* Note: These fonts are not free and therefore not in this repository.
* If you want to buy them, please visit celinehurka.com, thanks! :-)
*
* */
@font-face {
font-family: "vtVF";
src: url("/web/fonts/vtVF.ttf") format("TrueType");
}
@font-face {
font-family: "VariableIcons";
src: url("/web/fonts/variabletimeicons-Regular.otf") format("OpenType")
}
@font-face {
font-family: "Tonka";
src: url("/web/fonts/TonkaVF.woff2") format("woff2")
}
@font-face {
font-family: "Version-2-var";
src: url("/web/fonts/Version-2-var.ttf") format("TrueType");
font-weight: 100 1000;
font-display: swap;
/*animation: fontWeightAnimation 15s infinite ease-in-out;*/
}
#content {
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
opacity: 0.1;
cursor: grab;
}
.textObject {
font-family: "Version-2-var";
font-size: 10em;
font-variation-settings: "wght" 375;
right: 0px;
top: 20px;
position: absolute;
max-width: 50%;
line-height: 1.1em;
z-index: 1;
box-sizing: border-box;
}
.textObject:hover .header {
display: flex;
}
.textObject .header {
display: none;
flex-direction: column;
position: absolute;
top: 0px;
left: 0px;
width: 100%;
padding: 2px;
background-color: blue;
color: white;
font-family: sans-serif;
font-size: 11px;
line-height: 11px;
z-index: 10;
}
.textObject .header .move,
.textObject .header .duplicate,
.textObject .header .delete {
display: flex;
justify-content: center;
align-items: center;
cursor: move;
padding: 4px;
width: 100%;
border: 1px solid red;
}
.textObject .text {
position: absolute;
z-index: 9;
box-sizing: border-box;
}
.textObject:hover .text.original {
border: 1px solid red;
}
.textObject:hover .text.mirror_x,
.textObject:hover .text.mirror_y,
.textObject:hover .text.mirror_xy {
opacity: 0.5;
}
.panel {
font-size: 10em;
right: 0px;
bottom: 10px;
position: absolute;
background-color: transparent;
height: fit-content;
display: flex;
z-index: 1000;
}
.panel .header {
flex-direction: column;
position: absolute;
top: 0px;
left: 0px;
width: 100%;
padding: 2px;
background-color: blue;
display: none;
color: white;
font-family: sans-serif;
font-size: 11px;
line-height: 11px;
}
.panelWrapper:first-child {
order: 0;
}
.panelWrapper {
display: flex;
flex-direction: column;
}
.panel button {
padding: 5px;
box-sizing: border-box;
white-space: nowrap;
border: none;
/* background: white; */
/* color: white; */
font-family: "Tonka";
margin-right: 10px;
text-transform: uppercase;
padding-top: 7px;
width: fit-content;
align-self: flex-end;
margin-top: 10px;
/* color: #ea2333; */
letter-spacing: 0.02em;
}
.panel button:hover {
background-color: #DADADB;
}
.panel .header .move {
display: flex;
cursor: move;
}
#notice {
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 2000;
display: none;
justify-content: center;
align-items: center;
font-family: "Tonka";
font-variation-settings: 'wght' 500;
font-size: 0.8em;
}
#notice.visible {
display: flex;
}
#notice .content {
width: fit-content;
height: fit-content;
padding: 2em;
color: black;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
#notice .content .what p {
color: black;
}
#notice .content .details p {
color: black;
}
.exporterChild * {
font-family: "Tonka";
}
.options_title{
font-variation-settings: 'wght' 600 !important;
font-size: 0.9em;
text-align: right;
}
.options_cont{
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 25px;
padding: 10px;
}
.options_cont:not(.options_cont:first-of-type){
border-left: 1px dashed #91919177;
}
.options_cont p{
margin: 0;
height: fit-content;
font-variation-settings: 'wght' 800;
}
.options{
row-gap: 5px;
display: grid;
height: fit-content;
}
.options.adjustable{
row-gap: 0px;
display: grid;
column-gap: 10px;
font-variation-settings: 'wght' 800;
}
.options.adjustable label{
grid-column-start: 1;
white-space: nowrap;
}
.options input:focus{
border-color: transparent;
}
.options input {
font-variation-settings: 'wght' 800;
margin: 3px 0px 8px 0px;
padding: 1px 2px;
font-size: 1.1em;
/* grid-column-start: 2; */
width: fit-content;
height: fit-content;
/* text-align: center; */
border: none;
}
.exporterChild {
background-color: white;
width: 100%;
/* min-height: fit-content; */
/* max-height: 40vh;*/
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0px 15px;
display: flex;
transition: 0.5s margin-bottom;
}
.exporterChild:not(.exporterShown .exporterChild){
margin-bottom: -50vh;
}
.exporterShown .exporterChild{
margin-bottom: 0vh;
}
#loader{
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: rgba(0, 0, 0, 0.5);
}
#exporter{
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
justify-content: flex-end;
align-items: center;
flex-direction: column;
background-color: rgba(0, 0, 0, 0.5);
}
.exporter_options_child{
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
#export_progress_task{
margin: 15px 0px 0px 0px;
}
.loaderChild {
text-align: center;
}
.loaderChild h1 {
font-size: 1em;
font-family: 'Tonka';
}
#exporter {
opacity: 0;
pointer-events: none;
z-index: 1000;
transition: 0.5s opacity;
display: flex;
}
#exporter.exporterShown{
opacity: 1;
pointer-events: all;
}
#loader {
display: flex;
z-index: 20002;
font-family: 'monospace';
background: white;
}
#loader #loader_progress,
#loader #loader_progress_task,
#exporter #export_progress,
#exporter #export_progress_task {
font-family: 'Tonka';
background-color: white;
font-variation-settings: 'wght' 600;
}
#export_progress{
margin-bottom: 40px;
}
.exportButtonsCont button {
margin: 0px;
background-color: rgba(222, 222, 222, 0.97);
border: none;
cursor: pointer;
padding: 7px 15px;
border-radius: 10px;
font-size: 0.9em;
text-transform: uppercase;
font-variation-settings: 'wght' 800;
}
.exporter_options_buttons{
display: flex;
margin: 0 10px;
}
.exportButtonsCont button:not([disabled]):hover{
background: black;
color: white;
}
.exportButtonsCont {
display: grid;
grid-template-rows: 1fr 1fr;
row-gap: 5px;
padding: 0px 5px;
}
#exporter_options {
/* border: 1px solid green;*/
overflow-y: auto;
width: 100%;
background: rgba(247,247,247,1);
border-radius: 10px;
padding: 10px;
box-sizing: border-box;
margin-left: 5px;
}
#exporter_close:hover{
background: black;
color: white;
}
#exporter_close{
padding: calc(var(--padding)/2) calc(var(--padding)/2) calc(var(--padding)/2.5) calc(var(--padding)/2);
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
width: fit-content;
height: fit-content;
font-variation-settings: 'wght' 800;
text-transform: uppercase;
background: rgba(255, 255, 255, 1);
font-size: 0.8em;
cursor: pointer;
grid-column-start: 5;
grid-row-start: 1;
justify-self: flex-end;
font-family: "Tonka";
color: black;
text-decoration: none;
background: rgba(242, 242, 242, 0.97);
}
.exporter_dimension_warning {
background: pink;
padding: 10px;
box-sizing: border-box;
font-variation-settings: 'wght' 500;
font-size: 0.8em;
flex-direction: column;
border-radius: 10px;
/* max-width: 70%; */
grid-column-start: 1;
grid-column-end: 3;
}
.exporter_dimension_warning p{
margin: 0;
}
#exporter_render_info {
display: none;
color: rgb(234, 35, 51);
padding: 4px;
border-radius: 4px;
justify-content: center;
align-items: center;
margin: 0;
font-family: 'Tonka';
font-variation-settings: 'wght' 800;
}
#exporter_render_info p {
max-width: 500px;
}
.artboard_width::before,
.render_width::before {
content: "width: ";
font-variation-settings: 'wght' 500;
}
.artboard_height::before,
.render_height::before {
content: "height: ";
font-variation-settings: 'wght' 500;
}
.render_pixels::before {
content: "total pixel count: ";
font-variation-settings: 'wght' 500;
}
.render_length::before {
content: "length: ";
font-variation-settings: 'wght' 500;
}
.render_length::after {
content: " seconds";
font-variation-settings: 'wght' 500;
}
.artboard_pixelDensity::before {
content: "pixel density: ";
font-variation-settings: 'wght' 500;
}
#artboard_scale_label::before {
content: "render scale (";
font-variation-settings: 'wght' 500;
}
#artboard_scale_label::after {
content: "): ";
font-variation-settings: 'wght' 500;
}
#render_timescale_label::before {
content: "render timestretch (";
font-variation-settings: 'wght' 500;
}
#render_timescale_label::after {
content: "): ";
font-variation-settings: 'wght' 500;
}
#player {
display: none;
position: relative;
max-width: 80%;
max-height: 80%;
}
/*.fontFamilyWrapper{
order: 0;
}
.fontSizeWrapper{
order: 1;
}
.etterSpacingWrapper{
order: 2;
}
.lineHeighWrapper{
order: 3;
}
.textWrapper{
order: 4;
}
.fontVariationAxesContWrapper{
order: 5;
}
.xWrapper{
order: 6;
}
.yWrapper{
order: 7;
}
.alignButtonsHorizontal{
order: 8;
}
.alignButtonsVertical{
order: 9;
}*/
#midiController {
display: none;
position: absolute;
top: 20px;
left: 20px;
width: 50%;
height: 25%;
background: yellow;
z-index: 1001;
}
#midiController .midiMessages {
overflow-y: scroll;
}
#midiController .buttons div {
cursor: pointer;
}
#timeline {
position: fixed;
bottom: 0px;
left: 0px;
width: 100%;
height: 20px;
padding: 0px;
margin: 0px;
background: grey;
z-index: 1002;
}
#timeline_head {
position: absolute;
bottom: 0px;
left: 0px;
width: 20px;
height: 20px;
padding: 0px;
margin: 0px;
background: black;
z-index: 1002;
}
/* ABOUT BEGIN */
.overlay-text-cont.hidden {
margin-left: calc(var(--about-w)*-1);
}
.overlay-text-cont {
display: flex;
position: fixed;
width: var(--about-w);
height: 100vh;
top: 0;
left: 0;
transition: 0.5s margin-left;
margin-left: 0vw;
overflow-y: scroll;
background: rgba(242, 242, 242, 0.97);
padding-bottom: var(--padding);
box-sizing: border-box;
z-index: 20100;
}
.overlay-text-cont::-webkit-scrollbar {
width: 6px;
}
.overlay-text-cont::-webkit-scrollbar-thumb {
background: #c2c2c2;
}
.overlay-text-cont::-webkit-scrollbar-track {
background: #f1f1f1;
}
.overlay-text {
z-index: 1005;
backdrop-filter: blur(3px);
width: 100%;
padding: calc(var(--padding)/2) calc(var(--padding)/2);
font-family: "Tonka";
display: grid;
column-gap: var(--padding);
margin-bottom: var(--padding);
height: fit-content;
}
.about-text {
grid-column-start: 1;
grid-column-end: 5;
font-size: 0.9em;
}
.overlay-text *::selection {
background-color: #91919177;
}
.overlay-text p {
grid-column-start: 1;
font-variation-settings: 'wght' 500;
line-height: 1.2em;
}
.overlay-text a {
font-variation-settings: 'wght' 700;
color: black;
text-decoration: none;
}
.expanded{
overflow: hidden;
height: fit-content;
transition: 0.5s max-height;
/* padding-left: 1em;*/
display: none;
flex-direction: column;
}
.expanded p:last-of-type{
margin-bottom: 1em;
}
.expanded p:first-of-type{
margin-top: 0.5em;
}
.instructions .textParent{
padding-left: 1em;
}
.textParent:not(.textParent:first-of-type){
/*border-top: 1px dashed #91919177;
padding-top: 0.5em;*/
}
.textParent p{
margin-top: 0px;
margin-bottom: 0.5em;
}
.openText .expanded{
display: flex;
}
.aboutParent{
margin-bottom: 1.5em;
}
.expanded video{
max-width: 250px;
min-width: 200px;
align-self: center;
justify-self: end;
display: flex;
margin: 1em;
border: 1px solid lightgrey;
pointer-events: none;
}
.expandText{
border: none;
font-size: 1em;
cursor: pointer;
display: inline;
mix-blend-mode: multiply;
/* padding: calc(var(--padding)/4); */
padding: calc(var(--padding)/6) calc(var(--padding)/6) calc(var(--padding)/6.5) calc(var(--padding)/6);
background: #91919177;
border-radius: 5px;
margin-left: 0.3em;
width: 1em;
height: 1em;
display: inline-flex;
align-items: center;
justify-content: center;
}
.expandText:hover{
background: black;
color: white;
}
*{
font-weight: normal !important;
}
h4{
font-weight: normal;
margin-top: 0px;
margin-bottom: 0.5em;
font-variation-settings: 'wght' 500;
font-size: 1.2em;
border-top: 1px dashed #91919177;
padding-top: 0.5em;
width: 100%;
}
.button-overlay {
padding: calc(var(--padding)/2) calc(var(--padding)/2) calc(var(--padding)/2.5) calc(var(--padding)/2);
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
width: fit-content;
height: fit-content;
font-variation-settings: 'wght' 750;
text-transform: uppercase;
background: rgba(255, 255, 255, 1);
font-size: 0.8em;
cursor: pointer;
grid-column-start: 5;
grid-row-start: 1;
justify-self: flex-end;
font-family: "Tonka";
color: black;
text-decoration: none;
}
.link-in-text {
display: inline;
mix-blend-mode: multiply;
/* padding: calc(var(--padding)/4); */
padding: calc(var(--padding)/7) calc(var(--padding)/7) calc(var(--padding)/7.5) calc(var(--padding)/7);
background: #91919177;
border-radius: 5px;
margin-left: calc(var(--padding)/8);
margin-right: calc(var(--padding)/8);
font-size: 0.7em;
}
.contact_us{
font-size: 1.1em;
}
.links .button-overlay {
margin: 0 calc(var(--padding)/2) calc(var(--padding)/2) 0;
}
.button-overlay.button-close {
position: sticky;
top: var(--padding);
margin: calc(var(--padding)/2) 0 calc(var(--padding)/2) calc(var(--padding)/2);
}
.links {
position: sticky;
bottom: 0;
grid-row-start: 3;
grid-column-start: 5;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.button-overlay:hover {
background: black;
color: white;
}
.vt-title {
font-family: "vtVF";
font-size: 4em;
text-align: left;
line-height: 1em;
margin-top: 0;
margin-bottom: calc(var(--padding)*2);
}
.letter:not(.hidden .letter) {
animation: key 3s infinite;
}
.logos {
grid-column-start: 1;
grid-column-end: 5;
font-size: 0.8em;
margin-top: calc(var(--padding)*2);
border-top: 1px dashed #91919177;
}
.logos img {
max-width: 15%;
max-height: 5vh;
}
@keyframes key {
0% {
font-variation-settings: "wght" 0, "opsz" 0;
}
50% {
font-variation-settings: "wght" 100, "opsz" 0;
}
100% {
font-variation-settings: "wght" 0, "opsz" 0;
}
}
.buttons-bottom {
position: fixed;
bottom: 0;
width: 10vw;
display: flex;
padding: calc(var(--padding)/2);
flex-direction: column;
z-index: 20000;
}
/* these are in theatre-play.js */
/*.main_panel_button{*/
/*color: red !important;*/
/*font-size: 1.15em;*/
/*}*/
#debug_profiling{
display: none;
}
.upload_font_button{
pointer-events: all;
background: white;
width: fit-content;
cursor: pointer;
font-family: 'Tonka';
border-radius: 5px;
padding: 10px;
text-transform: uppercase;
box-sizing: border-box;
}
.upload_font_button:hover{
background: black;
color: white;
}
.upload_font_button_container{
pointer-events: none;
display: flex;
justify-content: center;
align-items: center;
z-index: 1010;
position: fixed;
background: rgba(0, 0, 0, 0.5);
width: 100%;
height: 100%;
margin: 0px;
top: 0px;
left: 0px;
}
/* ABOUT END */

2
bin/web/ffmpeg_modules/ffmpeg.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

178
bin/web/js/artboard.js Normal file
View file

@ -0,0 +1,178 @@
import {
makeEven,
mapValue,
} from './utils.js';
const Artboard = function(tp, domElement = false, autoInit = true) {
//private
let animationFrameId = false;
let width = window.innerWidth;
let height = window.innerHeight;
let x = 0;
let y = 0;
let props = {
backgroundColor: tp.core.types.rgba({
r: 74,
g: 94,
b: 181,
a: 1
}),
x: tp.core.types.number(x),
y: tp.core.types.number(y),
width: tp.core.types.number(width),
height: tp.core.types.number(height),
zoom: tp.core.types.number(1, {
range: [config.artboard.minimumZoom, config.artboard.maximumZoom],
nudgeMultiplier: config.artboard.incrementZoom
}),
pixelDensity: tp.core.types.number(1.0, {
range: [config.artboard.incrementPixelDensity, config.artboard.maximumPixelDensity],
nudgeMultiplier: config.artboard.incrementPixelDensity
}),
};
const onValuesChange = (values) => {
window.isRenderDirty = true;
//window.cancelAnimationFrame(animationFrameId);
//animationFrameId = window.requestAnimationFrame(() => {
//domElement.style.backgroundColor = `${values.backgroundColor.toString()}`;
//});
// backward compatibility
if (values.zoom < config.artboard.minimumZoom) {
values.zoom = config.artboard.minimumZoom;
}
// makes sure that the number
// is both integer and even
//let makeEven = (n) => {
//let nr = Math.round(n);
//return nr - nr % 2;
//}
values.width = makeEven(values.width);
values.height = makeEven(values.height);
// We are absolutely aware that swearwords in code
// can be considered unprofessional bad practice, but
// (smiley) fuck this:
//const nw = makeRight(values.width);
//const nh = makeRight(values.height);
//const np = [];
//if (nw !== values.width) {
//values.width = nw;
//np.push({'key': 'width', 'value': nw});
//}
//if (nh !== values.height) {
//values.height = nh;
//np.push({'key': 'height', 'value': nh});
//}
//if (np.length > 0 && typeof this.theatreObject !== 'undefined') {
//tp.studio.transaction(({
//set
//}) => {
//np.forEach((e) => {
//set(this.theatreObject.props[e.key], e.value);
//});
//});
let cppProps = props2cppProps(values);
Module.setArtboardProps(cppProps);
};
const init = () => {
props.width.default = width;
props.height.default = height;
if (window.devicePixelRatio > 1) {
const pixelDensity = mapValue(window.devicePixelRatio, 1, 2, 1, 1.4);
const zoom = pixelDensity;
props.pixelDensity.default = pixelDensity;
props.zoom.default = zoom;
}
//this.theatreObject = tp.addObject('artboard', this.props, this.onValuesChange);
this.theatreObject = tp.addObject('artboard', this.props, this.onValuesChange);
tp.studio.transaction(({
set
}) => {
set(this.theatreObject.props, this.theatreObject.value);
});
//tp.studio.transaction(({ set }) => {
//set(this.theatreObject.props.width, width);
//set(this.theatreObject.props.height, height);
//});
};
const props2cppProps = (_props) => {
let cppProps = JSON.parse(JSON.stringify(_props));
let bgIsArray = Array.isArray(cppProps.backgroundColor);
if (bgIsArray && cppProps.backgroundColor.length === 4) {
// nothing to do
} else if (!bgIsArray && cppProps.backgroundColor.hasOwnProperty('r')) {
cppProps.backgroundColor = [
cppProps.backgroundColor.r,
cppProps.backgroundColor.g,
cppProps.backgroundColor.b,
cppProps.backgroundColor.a
];
} else if (!bgIsArray && cppProps.backgroundColor.default.hasOwnProperty('r')) {
cppProps.backgroundColor = [
cppProps.backgroundColor.default.r,
cppProps.backgroundColor.default.g,
cppProps.backgroundColor.default.b,
cppProps.backgroundColor.default.a
];
} else {
console.error('js::layer::props2cppProps', 'color could not be translated');
}
return cppProps;
};
this.values2cppProps = props2cppProps;
// public
this.props = props;
this.onValuesChange = onValuesChange;
this.init = init;
let panelFinderTimeout = false;
this.findInjectPanel = () => {
let doItAgain = true;
if (tp.studio.selection.length === 0 || (tp.studio.selection.length > 0 && tp.studio.selection[0].address.objectKey !== this.id())) {
// do it again
} else {
const panel = tp.getPanel();
if (panel !== null) {
tp.setPanelClasses();
// adjust label text from unfriendly to friendly
Object.keys(config.artboard.friendlyNames).forEach((unfriendlyName) => {
const friendlyName = config.artboard.friendlyNames[unfriendlyName];
const panelPropTitle = tp.getPanelPropTitle(unfriendlyName);
if (panelPropTitle !== null &&
friendlyName !== '') {
panelPropTitle.innerHTML = friendlyName;
}
});
doItAgain = false;
}
}
if (doItAgain) {
clearTimeout(panelFinderTimeout);
panelFinderTimeout = setTimeout(() => {
this.findInjectPanel();
}, 30);
window.artboardPanelFinderTimeout = panelFinderTimeout;
}
};
this.id = ()=> {
return 'artboard';
};
this.hide = () => {
// nothing to do
};
// action
if (typeof domElement !== 'object') {
console.error('whoops, please pass a domElement to Artboard');
}
if (autoInit) {
this.init();
}
};
export {
Artboard
}

110
bin/web/js/config.js Normal file
View file

@ -0,0 +1,110 @@
const config = {
artboard: {
minimumZoom: 0.01,
maximumZoom: 200,
incrementZoom: 0.01,
maximumPixelDensity: 3.0,
incrementPixelDensity: 0.01,
friendlyNames: {
'backgroundColor': 'Background<br>Color',
'x': 'Position X',
'y': 'Position Y',
'width': 'Artboard Width',
'height': 'Artboard Height',
'zoom': 'Zoom',
'pixelDensity': 'Preview<br>Resolution',
},
},
layer: {
defaultFonts: ['Version-2', 'TonkaVF'],
letterDelayProps: ['fontSize_px', 'letterSpacing', 'color', 'fontVariationAxes'],
autoCreateFirstLayer: true,
defaultTexts: ['text', 'variable time', 'hello world'],
panelOrder: [
'fontFamily',
'textAlignButtonsHorizontal',
'textAlignment',
'width',
'height',
'fontSize_px',
'letterSpacing',
'lineHeight',
'text',
'fontVariationAxes',
'x',
'y',
'alignButtonsHorizontal',
'alignButtonsVertical',
'rotation',
'transformOrigin',
'mirror_x',
'mirror_x_distance',
'mirror_y',
'mirror_y_distance',
'mirror_xy',
'color',
'letterDelays',
],
friendlyNames: {
'fontFamily': 'Font Family',
'textAlignButtonsHorizontal': '',
'textAlignment': 'Text Alignment',
'width': 'Wrapper Width',
'height': '',
'fontSize_px': 'Font Size',
'letterSpacing': 'Letter Spacing',
'lineHeight': 'Line Height',
'text': 'Text',
'fontVariationAxes': 'Variable Axes',
'x': 'Position X',
'y': 'Position Y',
'alignButtonsHorizontal': '',
'alignButtonsVertical': '',
'rotation': 'Rotation',
'transformOrigin': 'Rotation Origin',
'mirror_x': 'Mirror X',
'mirror_x_distance': 'Mirror X Distance',
'mirror_y': 'Mirror Y',
'mirror_y_distance': 'Mirror Y Distance',
'mirror_xy': 'Mirrox XY',
'color': 'Color',
'letterDelays': 'Letter Delays',
},
},
tp: {
addKeyframesTimeout_s: 0.01,
},
projects: {
savePrefix: 'vte_project_'
},
interactor: {
zoomBaseFactor: 0.001,
zoomDynamicMax: 42,
},
midi: {
touchTimeThreshold_s: 0.5,
smoothingMix: 0.1,
},
fs: {
idbfsDir: '/idbfs',
idbfsFontDir: '/idbfs/fonts',
idbfsTmpDir: '/idbfs/tmp',
},
timeline: {
rolloverReset: true,
rolloverThreshold_s: 0.02,
},
autoSave: true,
};
const Config = function() {
const configKeys = Object.keys(config);
for (let c = 0; c < configKeys.length; c++) {
const key = configKeys[c];
this[key] = config[key];
}
};
export {
Config
}

442
bin/web/js/exporter.js Normal file
View file

@ -0,0 +1,442 @@
import {
makeEven,
clone,
} from './utils.js';
const FfmpegExporter = function() {
let isFfmpegLoaded = false;
let isFfmpegAttached = () => {
return document.getElementById("ffmpeg.min.js") !== null;
};
const attachFfmpeg = () => {
if (!isFfmpegAttached()) {
// this does not work
// we refuse solving this, by simply attaching the script
// in the template from the beginning
//console.log("FFmpegExport::attachFfmpeg", "not attached yet, doing it");
var s = document.createElement("script");
s.id = "ffmpeg.min.js";
s.type = "application/javascript";
s.src = "/web/ffmpeg_modules/ffmpeg.min.js";
const mom = document.getElementById('body');
mom.appendChild(s);
} else {
//console.log("FFmpegExport::attachFfmpeg", "already attached");
}
};
const createFfmpeg = () => {
return new Promise((resolve) => {
if (!isFfmpegLoaded) {
attachFfmpeg();
const {
createFFmpeg
} = FFmpeg;
this.ffmpeg = createFFmpeg({
log: false
});
window.ffmpeg = this.ffmpeg;
this.ffmpeg.setLogger(({
type,
message
}) => {
if (typeof message === 'string' && message.toLowerCase().indexOf('error') >= 0) {
if(confirm('Oh, there seems to be an error transcoding the video.\n'
+ 'Please either decrease resolution or render Frames instead of mp4.\n'
+ '\n'
+ 'Should we reload the page to restore from the error? Your project should still be there afterwards. If you\'re a bit paranoid (nobody blames you), then you can also first save your project to a zipfile and then reload the page yourself')) {
window.location.reload();
}
}
console.log("FFmpegExport::renderDiary", type, message);
//type can be one of following:
//info: internal workflow debug messages
//fferr: ffmpeg native stderr output
//ffout: ffmpeg native stdout output
});
let texts = [
"We perceive loading bars differently depending on what information they show us. This one for exampl",
"something I always wanted to tell you, is how beautiful you are. ",
"The more you wait, the more it won't happen. Or maybe it will, I don't know. I'm a computer program.",
"Waiting is the rust of the soul. ",
"Waiting is a sin against both the time still to come and the moments one is currently disregarding. ",
"Things may come to those who wait, but only the things left by those who hustle. ",
"There is no great achievement that is not the result of patient working and waiting. ",
"What we are waiting for is not as important as what happens while we are waiting. Trust the process.",
"The worst part of life is waiting. The best part of life is to have someone worth waiting for. ",
"You are not just waiting in vain. There is a purpose behind every delay. And if there is no purpose,",
];
let text = texts[Math.floor(Math.random(0,texts.length))];
ffmpeg.setProgress(({
ratio
}) => {
const percent = ratio * 100;
//let text = "somthing I always wanted to tell you, is how beautiful you are ";
//let text = "The more you wait, the more it won't happen. Or maybe it will, I don't know. I'm a computer program.";
let innerHTML = "|";
for (let i = 0; i < 100; i++) {
if (i < percent) {
innerHTML += text[i%text.length];
} else {
innerHTML += "-";
}
}
innerHTML += "|";
let progress = document.getElementById("export_progress");
progress.innerHTML = innerHTML;
/*
* ratio is a float number between 0 to 1.
*/
});
const loadFfmpeg = async (ffmpeg) => {
await ffmpeg.load();
// mount ffmpeg in oF
if (FS.readdir("/data").indexOf("export") < 0) {
FS.mkdir("/data/export");
}
if (FS.readdir("/data/export").indexOf("frames") < 0) {
FS.mkdir("/data/export/frames");
}
ffmpeg.coreFS().mkdir("/frames");
FS.mount(FS.filesystems.PROXYFS, {
root: "/frames",
fs: ffmpeg.coreFS()
}, "/data/export/frames");
isFfmpegLoaded = true;
resolve();
};
loadFfmpeg(this.ffmpeg);
} else { // already loaded
resolve();
}
});
};
const transcodeVideo = async (finishedCallback) => {
const ffmpeg = this.ffmpeg;
//const of_framesDir = '/data/export/frames';
const ffmpeg_framesDir = '/frames';
// const frameNames = FS.readdir(of_framesDir).splice(2); // remove '.', '..'
// ffmpeg.FS('mkdir', ffmpeg_framesDir);
// for (let i = 0; i < frameNames.length; i++) {
// const frameBuffer = FS.readFile(of_framesDir + "/" + frameNames[i]);
// ffmpeg.FS('writeFile', ffmpeg_framesDir + "/" + frameNames[i], frameBuffer);
// }
const progress_task = document.getElementById('export_progress_task');
const progress = document.getElementById('export_progress');
progress_task.innerHTML = 'transcoding video';
{
let innerHTML = "|";
for (let i = 0; i < 100; i++) {
innerHTML += "-";
}
innerHTML += "|";
progress.innerHTML = innerHTML;
}
await ffmpeg.run('-framerate', '30', '-pattern_type', 'glob', '-i', `${ffmpeg_framesDir}/*.png`, '-c:v', 'libx264', '-pix_fmt', 'yuv420p', 'output.mp4');
progress_task.innerHTML = 'preparing download';
progress.innerHTML = '|----------------------------------------------------------------------------------------------------|'
const data = ffmpeg.FS('readFile', 'output.mp4');
progress.innerHTML = '|::::::::::------------------------------------------------------------------------------------------|'
const buffy = URL.createObjectURL(new Blob([data.buffer], {
type: 'video/mp4'
}));
progress.innerHTML = '|:::::::::::::::::::::::::---------------------------------------------------------------------------|'
// yeey, let's create a timestamp!
let date = new Date();
const offset = date.getTimezoneOffset();
date = new Date(date.getTime() - (offset * 60 * 1000));
progress.innerHTML = '|:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::---------------------------------------|'
const timestamp = date.toISOString();
// phew.. alright, it's not pretty but we did it
const filename = tp.sheet.project.address.projectId + "_" + timestamp + ".mp4";
// now: downloading!
let link = document.createElement("a");
link.href = buffy;
link.download = filename;
link.click();
progress.innerHTML = '|::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::|'
setTimeout(() => {
progress_task.innerHTML = 'idle';
progress.innerHTML = '|----------------------------------------------------------------------------------------------------|'
finishedCallback();
}, 500);
// no video
//const video = document.getElementById('player');
//video.src = URL.createObjectURL(new Blob([data.buffer], {
//type: 'video/mp4'
//}));
//video.style.display = 'flex';
};
// public
this.init = createFfmpeg;
this.transcodeVideo = transcodeVideo;
}
const Exporter = function() {
const exporterDom = document.getElementById('exporter');
const exporterDomChild = document.querySelector('.exporterChild');
const exporterOptionsDom = document.getElementById('exporter_options');
let can_export_mp4 = true;
const options = {
artboard: {
width: 0,
height: 0,
pixelDensity: 1,
userScale: 1,
},
};
const renderDimensions = {
width: 0,
height: 0,
timeScale: 1,
};
const updateArtboardOptions = () => {
options.artboard = {...options.artboard, ...Module.getArtboardProps()};
//options.artboard.width = getArtboard().theatreObject.value.width;
//options.artboard.height = getArtboard().theatreObject.value.height;
options.artboard.pixelDensity = getArtboard().theatreObject.value.pixelDensity;
[...exporterDom.querySelectorAll('.artboard_width')].forEach((e) => {
e.innerHTML = options.artboard.width;
});
[...exporterDom.querySelectorAll('.artboard_height')].forEach((e) => {
e.innerHTML = options.artboard.height;
});
[...exporterDom.querySelectorAll('.artboard_pixelDensity')].forEach((e) => {
e.innerHTML = options.artboard.pixelDensity;
e.remove(); // NOTE: as we ignore pixel density for the export, it's not necessary to show it here
});
};
const setArtboardPropsToRenderDimensions = () => {
const artboardValues = clone(options.artboard);//{...options.artboard, ...renderDimensions};
const densityRatio = renderDimensions.width / options.artboard.width;
//artboardValues.pixelDensity *= densityRatio;
artboardValues.pixelDensity = densityRatio;
const artboardCppProps = getArtboard().values2cppProps(artboardValues);
const currentArtboardValues = Module.getArtboardProps();
if (currentArtboardValues.width !== artboardCppProps.width
|| currentArtboardValues.height !== artboardCppProps.height
|| currentArtboardValues.pixelDensity !== artboardCppProps.pixelDensity) {
window.isRenderDirty = true;
Module.setArtboardProps(artboardCppProps);
}
Module.setTimeScale(renderDimensions.timeScale);
};
const resetArtboardProps = () => {
//const artboardCppProps = getArtboard().values2cppProps(options.artboard);
const artboardValues = getArtboard().theatreObject.value;
const artboardCppProps = getArtboard().values2cppProps(artboardValues);
Module.setArtboardProps(artboardCppProps);
Module.setTimeScale(1.0);
};
const updateRenderDimensions = () => {
const currentDimensions = {
width: options.artboard.width * options.artboard.pixelDensity,
height: options.artboard.height * options.artboard.pixelDensity,
};
const artboardUserScaleLabelDom = document.getElementById('artboard_scale_label');
artboardUserScaleLabelDom.innerHTML = options.artboard.userScale;
const timeScaleLabelDom = document.getElementById('render_timescale_label');
timeScaleLabelDom.innerHTML = renderDimensions.timeScale;
const timelineLength_seconds = window.tp.core.val(window.tp.sheet.sequence.pointer.length);
renderDimensions.width = makeEven(currentDimensions.width * options.artboard.userScale * (1.0 / options.artboard.pixelDensity));
renderDimensions.height = makeEven(currentDimensions.height * options.artboard.userScale * (1.0 / options.artboard.pixelDensity));
[...exporterDom.querySelectorAll('.render_width')].forEach((e) => {
e.innerHTML = renderDimensions.width;
});
[...exporterDom.querySelectorAll('.render_height')].forEach((e) => {
e.innerHTML = renderDimensions.height;
});
[...exporterDom.querySelectorAll('.render_pixels')].forEach((e) => {
// 12345678 => 12.345.678
function addDots(nStr) {
nStr += '';
let x = nStr.split('.');
let x1 = x[0];
let x2 = x.length > 1 ? '.' + x[1] : '';
var rgx = /(\d+)(\d{3})/;
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + '.' + '$2'); // changed comma to dot here
}
return x1 + x2;
}
e.innerHTML = addDots(`${renderDimensions.width * renderDimensions.height}`);
});
[...exporterDom.querySelectorAll('.render_length')].forEach((e) => {
e.innerHTML = (timelineLength_seconds / renderDimensions.timeScale).toFixed(2);
});
if (renderDimensions.width * renderDimensions.height > 1920 * 1080) {
exporterDom.querySelector('.exporter_dimension_warning').style.display = 'flex';
exporterDom.querySelector('#exporter_button_mp4').disabled = true;
can_export_mp4 = false;
} else {
exporterDom.querySelector('.exporter_dimension_warning').style.display = 'none';
exporterDom.querySelector('#exporter_button_mp4').disabled = false;
can_export_mp4 = true;
}
};
const registerEvents = () => {
const close_button = document.getElementById('exporter_close');
close_button.addEventListener("click", this.close);
const open_button = document.getElementById('exporter_open');
open_button.addEventListener("click", this.open);
const artboardUserScale_input = document.getElementById('artboard_scale');
artboardUserScale_input.addEventListener('change', (event) => {
options.artboard.userScale = event.target.value;
updateRenderDimensions();
});
artboardUserScale_input.addEventListener('input', (event) => {
options.artboard.userScale = event.target.value;
updateRenderDimensions();
});
const timeScale_input = document.getElementById('render_timescale');
timeScale_input.addEventListener('change', (event) => {
renderDimensions.timeScale = event.target.value;
updateRenderDimensions();
});
timeScale_input.addEventListener('input', (event) => {
renderDimensions.timeScale = event.target.value;
updateRenderDimensions();
});
};
window.isRenderDirty = true;
const cancel = (e) => {
e.stopPropagation();
if(confirm("Closing the export window during an active export will cancel the rendering process and reload the page. Is that okay for you?")) {
window.location.reload();
}
};
const resetAfter = () => {
exporterDom.querySelector('#exporter_button_mp4').disabled = !can_export_mp4;
exporterDom.querySelector('#exporter_button_zip').disabled = false;
const close_button = document.getElementById('exporter_close');
close_button.removeEventListener("click", cancel);
close_button.addEventListener("click", this.close);
exporterDom.querySelector('#exporter_render_info').style.display = 'none';
const progress_task = document.getElementById('export_progress_task');
const progress = document.getElementById('export_progress');
progress_task.innerHTML = 'idle';
progress.innerHTML = '|----------------------------------------------------------------------------------------------------|'
};
this.renderFrames = (exportType) => {
exporterDom.querySelector('#exporter_button_mp4').disabled = true;
exporterDom.querySelector('#exporter_button_zip').disabled = true;
const close_button = document.getElementById('exporter_close');
close_button.addEventListener("click", cancel);
close_button.removeEventListener("click", this.close);
exporterDom.querySelector('#exporter_render_info').style.display = 'flex';
setArtboardPropsToRenderDimensions();
if (window.isRenderDirty) {
tp.sheet.sequence.pause();
if (exportType === 'zip') {
window.renderDone = () => {
window.isRenderDirty = false;
const projectName = tp.sheet.project.address.projectId;
Module.exportFramesAsZip(projectName);
// progress is being set in separate cpp thread
// so we reset some things in ofApp.h -> ZipSaver
resetAfter();
};
} else if (exportType === 'mp4') {
window.renderDone = () => {
window.isRenderDirty = false;
this.ffmpegExporter
.init()
.then(() => {
this.ffmpegExporter
.transcodeVideo(resetAfter);
});
};
} else {
window.renderDone = () => {
console.log('rendering done! now.. what?');
window.isRenderDirty = false;
};
}
Module.setRendering(true);
} else {
if (exportType === 'zip') {
const projectName = tp.sheet.project.address.projectId;
Module.exportFramesAsZip(projectName);
// progress is being set in separate cpp thread
// so we reset some things in ofApp.h -> ZipSaver
resetAfter();
} else if (exportType === 'mp4') {
this.ffmpegExporter
.init()
.then(() => {
this.ffmpegExporter
.transcodeVideo(resetAfter);
});
} else {
console.log('now.. what?');
}
}
};
let isInitialized = false;
this.init = () => {
return new Promise((resolve) => {
if (!isInitialized) {
registerEvents();
this.ffmpegExporter
.init()
.then(() => {
resolve();
});
} else {
resolve();
}
});
};
this.ffmpegExporter = new FfmpegExporter();
this.open = () => {
updateArtboardOptions();
updateRenderDimensions();
const renderWidthDom = exporterDom.querySelector('.render_width');
const renderHeightDom = exporterDom.querySelector('.render_height');
// exporterDom.style.display = 'flex';
exporterDom.classList.add('exporterShown');
// exporterDomChild.style.marginBottom = '0vh';
};
this.close = () => {
// exporterDom.style.display = 'none';
exporterDom.classList.remove('exporterShown');
// exporterDomChild.style.marginBottom = '-50vh';
resetArtboardProps();
};
// action
//init();
};
export {
Exporter
}

108
bin/web/js/interactor.js Normal file
View file

@ -0,0 +1,108 @@
'use strict'
//import {
//config
//} from './config.js'
const Interactor = function() {
// private
let artboard;
let canvas;
let content;
let mouse = {};
const resetMouse = () => {
mouse.isDown = false;
mouse.isDragging = false;
content.style.cursor = 'grab';
}
const moveArtboard = (x, y, relative = true) => {
tp.studio.transaction(({
set
}) => {
if (relative) {
set(artboard.theatreObject.props.x, artboard.theatreObject.value.x + x);
set(artboard.theatreObject.props.y, artboard.theatreObject.value.y + y);
} else {
set(artboard.theatreObject.props.x, x);
set(artboard.theatreObject.props.y, y);
}
});
};
const zoomArtboard = (zoom) => {
zoom = Math.max(config.artboard.minimumZoom, zoom);
zoom = Math.min(config.artboard.maximumZoom, zoom);
tp.studio.transaction(({
set
}) => {
set(artboard.theatreObject.props.zoom, zoom);
});
};
const registerEvents = () => {
content.addEventListener('mousedown', (event) => {
event.preventDefault();
event.cancelBubble = true;
mouse.isDown = true;
});
// the mouse might move out of the content while moving it
// also, we don't capture the mouse. you will have to simply
// drag multiple times if you want to drag very far
document.addEventListener('mousemove', (event) => {
if (mouse.isDown) {
mouse.isDragging = true;
content.style.cursor = 'grabbing';
// for some reason we need to multiply the mouve movement by 0.5
// no idea why. but this is consistent across browsers, so we might
// just leave this here and investigate whenever we have a problem
const factor = 0.5;
moveArtboard(event.movementX * factor, event.movementY * factor);
event.preventDefault();
event.cancelBubble = true;
}
});
document.addEventListener('mouseup', (event) => {
if (mouse.isDown) {
event.preventDefault();
event.cancelBubble = true;
resetMouse();
}
});
content.addEventListener('wheel', (event) => {
const currentZoom = artboard.theatreObject.value.zoom;
const zoomFactor = config.interactor.zoomBaseFactor
* Math.min(config.interactor.zoomDynamicMax, currentZoom);
let zoom = currentZoom + (event.deltaY * zoomFactor);
zoom = Math.min(config.artboard.maximumZoom, Math.max(config.artboard.minimumZoom, zoom));
zoomArtboard(zoom);
const dpiRatio = 1.0; //window.devicePixelRatio ? window.devicePixelRatio : 1.0;
const currentX = artboard.theatreObject.value.x * dpiRatio;
const currentY = artboard.theatreObject.value.y * dpiRatio;
const currentW = artboard.theatreObject.value.width * dpiRatio;
const currentH = artboard.theatreObject.value.height * dpiRatio;
const relativeX = (((currentX * -1) + (event.clientX * dpiRatio)) / currentZoom) / currentW;
const newX = -1 * ((relativeX * currentW * zoom) - event.clientX * dpiRatio);
const relativeY = (((currentY * -1) + (event.clientY * dpiRatio)) / currentZoom) / currentH;
const newY = -1 * ((relativeY * currentH * zoom) - event.clientY * dpiRatio);
const x = newX;
const y = newY;
moveArtboard(x, y, false);
});
};
// public
this.init = () => {
artboard = window.getArtboard();
canvas = document.querySelector('canvas.emscripten');
content = document.querySelector('#content');
resetMouse();
registerEvents();
};
};
export {
Interactor
};

2
bin/web/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1084
bin/web/js/layer.js Normal file

File diff suppressed because it is too large Load diff

91
bin/web/js/layerOrder.js Normal file
View file

@ -0,0 +1,91 @@
const LayerOrder = function() {
const layerIDs = [];
let mom= false;
let updateDom = (from, to) => {
const layerID = layerIDs[from];
const domLayerSelector = `.layerMover${layerID}`;
const domLayer = tp.shadowRoot.querySelector(domLayerSelector);
const otherLayerID = layerIDs[to];
const otherDomLayerSelector = `.layerMover${otherLayerID}`;
const otherDomLayer = tp.shadowRoot.querySelector(otherDomLayerSelector);
if (domLayer === null || otherDomLayer == null) {
return false;
}
if (mom === false) {
mom = domLayer.parentNode;
}
if (mom !== otherDomLayer.parentNode) {
return false;
}
if (from > to) {
mom.insertBefore(domLayer, otherDomLayer);
} else {
mom.insertBefore(otherDomLayer, domLayer);
}
return true;
};
const updateOf = () => {
const layers = JSON.parse(JSON.stringify(layerIDs));
layers.reverse();
const v = new Module.vector$string$();
for (let i = 0; i < layers.length; i++) {
v.push_back(layers[i]);
}
Module.setLayerOrder(v);
};
this.add = (id) => {
layerIDs.push(id);
updateOf();
};
this.remove = (id) => {
const i = layerIDs.indexOf(id);
layerIDs.splice(i, 1);
updateOf();
};
this.move = (from, to) => {
if (updateDom(from, to)) {
// remove `from` item and store it
var l = layerIDs.splice(from, 1)[0];
// insert stored item into position `to`
layerIDs.splice(to, 0, l);
updateOf();
} else {
console.error('js::LayerOrder::move failed');
}
};
this.moveDown = (id) => {
let i = layerIDs.indexOf(id);
const nextI = i + 1;
if (nextI >= layerIDs.length) {
console.log('LayerOrder::moveDown', 'cannot move further down');
} else {
this.move(i, nextI);
}
};
this.moveUp = (id) => {
let i = layerIDs.indexOf(id);
const nextI = i - 1;
if (nextI < 0) {
console.log('LayerOrder::moveUp', 'cannot move further up');
} else {
this.move(i, nextI);
}
};
this.get = () => {
return layerIDs;
};
this.set = (newLayerIDs) => {
window.newLayerIDs = newLayerIDs;
window.layerIDs = layerIDs;
newLayerIDs.forEach((id, to_i) => {
//console.log(`id: ${id}, to_i: ${to_i}, layerIDs[0]: ${layerIDs[0]}`);
const from_i = layerIDs.indexOf(id);
//console.log({id, from_i, to_i});
this.move(from_i, to_i);
});
};
};
export {
LayerOrder
};

516
bin/web/js/main.js Normal file
View file

@ -0,0 +1,516 @@
import {
TheatrePlay
} from './theatre-play.js';
import {
Layer
} from './layer.js';
import {
Artboard
} from './artboard.js';
import {
LayerOrder
} from './layerOrder.js';
import {
Exporter
} from './exporter.js';
import {
Interactor
} from './interactor.js';
import {
ModuleFS
} from './moduleFS.js';
//import {
//MidiController
//} from './midiController.js';
import {
makeDraggable,
getBaseName,
uploadFile,
downloadFile,
hashFromString,
clone,
} from './utils.js';
import {
Config
} from './config.js';
window.uploadFile = uploadFile;
window.downloadFile = downloadFile;
window.isInitialized = false;
window.hashFromString = hashFromString;
const config = new Config();
window.config = config;
const tp = new TheatrePlay();
window.tp = tp;
const layers = [];
const layersById = {};
const layerOrder = new LayerOrder();
window.layerOrder = layerOrder;
const fontsAndAxes = [];
let artboard;
const exporter = new Exporter();
const interactor = new Interactor();
const moduleFS = new ModuleFS();
window.moduleFS = moduleFS;
window.panelFinderTimeout = false;
const sequenceEventBuffer = {};
//const midiController = new MidiController();
//window.midiController = midiController;
let about = null;
const getAbout = () => {
if (about === null) {
about = document.querySelector('#overlay-text-cont');
// should we succeed in getting about,
// attach events. this happens then exactly once
if (about !== null) {
const textParents = getAbout().querySelectorAll(".textParent");
textParents.forEach((textParent) => {
const buttonExp = textParent.querySelector(".expandText");
if (buttonExp === null) {
console.error("Could not find .expandText within .textParent");
return;
}
buttonExp.addEventListener("click", () => {
textParent.classList.toggle("openText");
if (textParent.classList.contains("openText")) {
buttonExp.textContent = "-";
} else {
buttonExp.textContent = "+";
}
});
});
}
}
return about;
};
window.showAbout = () => {
if (getAbout() === null) {
return;
}
getAbout().classList.remove("hidden");
}
window.hideAbout = () => {
if (getAbout() !== null) {
getAbout().classList.add("hidden");
}
}
let theatrePanel = null;
const getPanel = () => {
if (theatrePanel === null) {
theatrePanel = tp.shadowRoot.querySelector('[data-testid="DetailPanel-Object"]');
}
return theatrePanel;
};
const findInjectPanel = () => {
const panel = getPanel();
if (panel !== null) {
let bottomButtonsContainer = panel.querySelector('.bottomButtonsContainer');
if (bottomButtonsContainer === null) {
bottomButtonsContainer = document.createElement('div');
bottomButtonsContainer.classList.add("bottomButtonsContainer");
panel.append(bottomButtonsContainer);
}
const exportButton = document.querySelector('#exporter_open');
if (exportButton !== null) {
bottomButtonsContainer.append(exportButton);
exportButton.classList.add("main_panel_button");
}
const saveButton = document.querySelector('#save_project');
if (saveButton !== null) {
bottomButtonsContainer.append(saveButton);
saveButton.classList.add("main_panel_button");
}
const openButton = document.querySelector('#open_project');
if (openButton !== null) {
bottomButtonsContainer.append(openButton);
openButton.classList.add("main_panel_button");
}
const profilingButton = document.querySelector('#debug_profiling');
if (profilingButton !== null) {
bottomButtonsContainer.append(profilingButton);
profilingButton.classList.add("main_panel_button");
}
const startNewButton = document.querySelector('#start_new_project');
if (startNewButton !== null) {
bottomButtonsContainer.append(startNewButton);
startNewButton.classList.add("main_panel_button");
}
} else {
setTimeout(() => {
findInjectPanel();
}, 100);
}
};
window.onload = () => {
window.addEventListener('panelEvent', (e) => {
clearTimeout(window.panelFinderTimeout);
let target = false;
if (e.detail.panelID === 'artboard') {
target = artboard;
} else if (layersById.hasOwnProperty(e.detail.panelID)) {
target = layersById[e.detail.panelID];
}
if (target !== false &&
typeof target !== 'undefined' &&
target.hasOwnProperty('receivePanelEvent')) {
target.receivePanelEvent(e);
}
});
window.addEventListener('sequenceEvent', (e) => {
let target = false;
if (e.detail.panelID === 'artboard') {
target = artboard;
} else {
target = layersById[e.detail.panelID];
}
if (target !== false &&
typeof target !== 'undefined' &&
target.hasOwnProperty('receiveSequenceEvent')) {
target.receiveSequenceEvent(e.detail);
} else {
// whoops, no layers there yet
// let's put this stuff in a buffer and forward it later
if (!sequenceEventBuffer.hasOwnProperty(e.detail.panelID)) {
sequenceEventBuffer[e.detail.panelID] = [];
}
sequenceEventBuffer[e.detail.panelID].push(e.detail);
}
tp.friendlySequenceNames();
});
window.setLoadingTask('setting up animation', 0);
tp.init()
.then(() => {
const content = document.querySelector('#content');
if (window.moduleInitialized) {
postModuleInitialized();
} else {
window.addEventListener('initializedModule', postModuleInitialized);
}
tp.studio.onSelectionChange((newSelection) => {
if (newSelection.length > 0) {
[getArtboard(), getLayers()].flat().forEach((e) => {
if (e.id() === newSelection[0].address.objectKey) {
if (e.id().indexOf('layer-') === 0) {
e.findInjectPanel();
e.showBoundingBoxDiv();
setTimeout(() => {
e.hideBoundingBoxDiv();
}, 60);
} else if (e.id() === 'artboard') {
e.findInjectPanel();
}
}
});
}
findInjectPanel();
tp.friendlySequenceNames();
});
});
// ABOUT BEGIN
var lettersAndLinks = document.querySelectorAll(".vt-title");
lettersAndLinks.forEach(function(element) {
element.innerHTML = element.innerHTML.replace(/\w+/g, '<span class="word">$&</span>');
element.innerHTML = element.innerHTML.replace(/\*/g, '<span class="word">$&</span>');
});
var words = document.querySelectorAll(".word");
words.forEach(function(element) {
element.innerHTML = element.innerHTML.replace(/\S/g, '<span class="letter">$&</span>');
});
var letterElements = document.querySelectorAll(".letter");
letterElements.forEach(function(element, index) {
element.style.animationDelay = (index * -0.3) + "s";
});
// ABOUT END
}
const adjustPanel = () => {
const VTTitle = tp.shadowRoot.querySelector(`.layerMovervariable-time`);
if (VTTitle !== null) {
var titleText = VTTitle.querySelectorAll("span");
titleText.forEach((element, index) => {
if (element.innerHTML == "variable-time") {
element.innerHTML = "vt*";
element.classList.add("vtTitle");
element.innerHTML = element.innerHTML.replace(/\w+/g, '<span class="word">$&</span>');
const wordElements = tp.shadowRoot.querySelectorAll(".word");
wordElements.forEach(word => {
word.innerHTML = word.innerHTML.replace(/\S/g, '<span class="letter">$&</span>');
element.innerHTML = element.innerHTML.replace('*', '<span class="letter">$&</span>');
});
const letterElements = tp.shadowRoot.querySelectorAll(".letter");
letterElements.forEach(letter => {
letter.style.fontVariationSettings = "'wght' " + Math.floor(Math.random() * (100 - 0 + 1) + 0) + ", 'wdth'" + Math.floor(Math.random() * (100 - 0 + 1) + 0) + ", 'opsz'" + Math.floor(Math.random() * (10 - 0 + 1) + 0);
});
element.addEventListener("mouseover", function() {
letterElements.forEach(letter => {
letter.style.fontVariationSettings = "'wght' " + Math.floor(Math.random() * (100 - 0 + 1) + 0) + ", 'wdth'" + Math.floor(Math.random() * (100 - 0 + 1) + 0) + ", 'opsz'" + Math.floor(Math.random() * (10 - 0 + 1) + 0);
});
});
}
})
}
}
const resize = () => {
let width = document.body.clientWidth;
let height = document.body.clientHeight;
let ratio = window.devicePixelRatio ? window.devicePixelRatio : 1;
Module.canvas.setAttribute('width', width * ratio);
Module.canvas.setAttribute('height', height * ratio);
Module.canvas.style.width = `${width}px}`;
Module.canvas.style.height = `${height}px}`;
Module.windowResized(Math.round(width * ratio), Math.round(height * ratio));
};
const postModuleInitialized = () => {
window.setLoadingTask('setting up animation', 80);
moduleFS.init()
.then(() => {
artboard = new Artboard(tp, content);
initPanels();
// NOTE: we know that our TheatrePlay is initialized
tp.connectModuleCallbacks();
exporter.init();
getFontsAndAxes();
tp.loadProject().then(() => {
interactor.init();
resize();
adjustPanel();
window.setLoadingTask('setting up animation', 100);
window.isInitialized = true;
window.setLoadingDone();
window.autoSaveInterval = setInterval(() => {
if (config.autoSave && window.isInitialized) {
tp.saveProject();
}
}, 1000);
});
//midiController.init();
});
window.removeEventListener('initializedModule', postModuleInitialized);
window.addEventListener('resize', function(event) {
resize();
}, true);
};
const getFontsAndAxes = () => {
return new Promise((resolve) => {
const availableFontsAndAxes = listAvailableFontsAndAxes();
const newFontsAndAxes = [];
for (let i in availableFontsAndAxes) {
if (!fontsAndAxes.includes(availableFontsAndAxes[i])) {
// nevermind includes and test ourselves
let reallyNew = true;
fontsAndAxes.forEach((faa) => {
// path is enough
if (faa.fontPath === availableFontsAndAxes[i].fontPath) {
reallyNew = false;
}
});
if (reallyNew) {
fontsAndAxes.push(availableFontsAndAxes[i]);
newFontsAndAxes.push(availableFontsAndAxes[i]);
}
}
}
if (newFontsAndAxes.length > 0) {
const promises = [];
for (let l = 0; l < layers.length; l++) {
layers[l].updateFonts()
.then(() => {
resolve(newFontsAndAxes);
});
}
} else {
resolve(false);
}
});
};
const listAvailableFontsAndAxes = () => {
let availableFontsAndAxes = [];
let fontPaths = Module.listAvailableFonts();
for (let f = 0; f < fontPaths.size(); f++) {
const fontPath = fontPaths.get(f);
const fontName = getBaseName(fontPath);
const cppAxes = Module.listVariationAxes(fontPath);
// turn cppAxes in normal js array of objects
const axes = [];
for (let a = 0; a < cppAxes.size(); a++) {
let axis = {
name: cppAxes.get(a).name,
minValue: cppAxes.get(a).minValue,
maxValue: cppAxes.get(a).maxValue,
defaultValue: cppAxes.get(a).defaultValue
};
axes.push(axis);
}
availableFontsAndAxes.push({
fontName,
fontPath,
axes
});
}
return availableFontsAndAxes;
};
window.listAvailableFontsAndAxes = listAvailableFontsAndAxes;
window.getFontsAndAxes = getFontsAndAxes;
window.getLayers = () => {
return layers;
};
window.moveLayerUp = (layerID) => {
layerOrder.moveUp(layerID);
};
window.moveLayerDown = (layerID) => {
layerOrder.moveDown(layerID);
};
window.getArtboard = () => {
return artboard;
};
const addLayer = (autoInit = true) => {
const layerID = Module.addNewLayer();
const layer = new Layer(tp, layerID, fontsAndAxes, autoInit);
layers.push(layer);
layersById[layerID] = layer;
return layer;
};
const addExistingLayer = (layerID, values) => {
return new Promise((resolve) => {
const layer = new Layer(tp, layerID, fontsAndAxes, false);
// check if fonts exist?
layer.valuesCorrector(values);
const cppProps = layer.values2cppProps(values);
const checkID = Module.addExistingLayer(cppProps, layerID);
layer.init().then(() => {
layers.push(layer);
layersById[layerID] = layer;
resolve(layer);
});
});
};
const duplicateLayer = (originalLayer) => {
return new Promise((resolve) => {
const originalValues = clone(originalLayer.theatreObject.value);
const newLayer = addLayer(false);
newLayer.init(originalValues).then(() => {
const originalKeyframes = tp.getKeyframes(originalLayer);
const addKeyframes = (e) => {
const originalKeys = Object.keys(originalValues);
const givenKeys = e.detail.titles;
let allKeysFound = true;
for (let i = 0; i < originalKeys.length; i++) {
//const originalValue = originalValues[originalKeys[i]];
if (givenKeys.indexOf(originalKeys[i]) < 0) {
//delete originalValues[originalKeys[i]];
allKeysFound = false;
}
};
if (allKeysFound) {
tp.getPanel().removeEventListener("injected", addKeyframes);
}
tp.addKeyframes(newLayer, originalKeyframes).then(() => {
if (allKeysFound) {
resolve();
};
});
};
tp.getPanel().addEventListener("injected", addKeyframes);
newLayer.select();
//tp.shadowRoot.querySelector(`.layerMover${newLayer.id()} div`).click();
});
});
};
const deleteLayer = (id, saveProject = true) => {
let index = -1;
Module.removeLayer(id);
tp.removeObject(id);
layerOrder.remove(id);
// delete from array
for (let i = 0; i < layers.length; i++) {
if (layers[i].id() === id) {
index = i;
}
}
layers[index].prepareForDepartureFromThisBeautifulExperience();
layers.splice(index, 1);
delete layersById[id];
if (saveProject) {
setTimeout(() => {
tp.saveProject();
}, 1000);
}
};
// TODO: better function names
// because, come on. it may be funny for a second
// but tolerance for fun is low when you're grumpy
// because stuff doesn't work
const renderForReal = (position, frameTime) => {
position = position + frameTime;
tp.sheet.sequence.position = position;
Module.renderNextFrame();
}
window.isRenderDirty = true;
window.duplicateLayer = (layer) => {
const noticeDom = document.querySelector('#notice');
noticeDom.classList.add('visible');
noticeDom.querySelector('.what > p').innerHTML = `Duplicating Layer`;
noticeDom.querySelector('.details > p').innerHTML = `Please wait, thank you.`;
duplicateLayer(layer).then(() => {
document.querySelector('#notice').classList.remove('visible');
});
};
window.addLayer = addLayer;
window.addExistingLayer = addExistingLayer;
window.deleteLayer = deleteLayer;
window.renderFrames = exporter.renderFrames;
const layer_panel = document.querySelector('#layer_panel');
const initPanels = () => {
//makeDraggable(layer_panel);
};

View file

@ -0,0 +1,768 @@
'use strict'
import {
mix,
getMix,
mixObject,
} from './utils.js'
const PhysicalMidiMapping = {
"Launch Control MIDI 1": {
"knobs": [
21, 22, 23, 24, 25, 26, 27, 28, // first row
41, 42, 43, 44, 45, 46, 47, 48, // second row
],
"buttons": [
9, 10, 11, 12, 25, 26, 27, 28,
],
"arrows": [
114, 115, 116, 117, // up, down, left, right
]
}
};
const generalControl = {
"Launch Control MIDI 1": {}
};
const MidiController = function() {
window.mixObject = mixObject;
const element = document.querySelector('#midiController');
const openCloseButton = document.querySelector('#midi_open');
const buttons = element.querySelector(".buttons");
let inputs;
let outputs;
let isPanelOpen = false;
let layers = [];
let debugLog = false;
let isInitialized = false;
let activeLayer = 0;
let activePropSet = 0;
window.activeLayer = activeLayer;
let artboardWidth = 1920;
let lastSelectionPoint = -1;
let playbackSpeed = 1;
let setFontVariation = (layer, layerIndex, button, midiValue) => {
if (layer.theatreObject.value.hasOwnProperty('fontVariationAxes') &&
typeof layer.theatreObject.value.fontVariationAxes === 'object') {
const axes = layer.theatreObject.value.fontVariationAxes;
const index = button - 46;
const keys = Object.keys(axes);
if (index < keys.length) {
const key = keys[index];
const axesProps = layer.props.fontVariationAxes.props[key];
const min = axesProps.range[0];
const max = axesProps.range[1];
const v = (midiValue / 127.0) * (max - min) + min;
if (!currentValues[layerIndex].hasOwnProperty('fontVariationAxes')) {
currentValues[layerIndex].fontVariationAxes = {};
}
currentValues[layerIndex].fontVariationAxes[key] = v;
//tp.studio.transaction(({
//set
//}) => {
//set(layer.theatreObject.props.fontVariationAxes[key], v);
//});
}
}
};
let setLetterDelays = (layer, layerIndex, button, midiValue) => {
if (layer.theatreObject.value.hasOwnProperty('letterDelays')) {
const letterDelays = layer.theatreObject.value.letterDelays;
const keys = Object.keys(letterDelays);
const min = 0;
const max = 2000;
const v = (midiValue / 127.0) * (max - min) + min;
//console.log('MidiController::setLetterDelays - font has letterDelays', JSON.parse(JSON.stringify(letterDelays)));
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (typeof letterDelays[key] === 'object') {
const subKeys = Object.keys(letterDelays[key]);
for (let si = 0; si < subKeys.length; si++) {
const subKey = subKeys[si];
if (!currentValues[layerIndex].hasOwnProperty('letterDelays')) {
currentValues[layerIndex].letterDelays = {};
}
if (!currentValues[layerIndex].letterDelays.hasOwnProperty(key)) {
currentValues[layerIndex].letterDelays[key] = {};
}
currentValues[layerIndex].letterDelays[key][subKey] = v;
//tp.studio.transaction(({
//set
//}) => {
//set(layer.theatreObject.props.letterDelays[key][subKey], v);
//});
}
} else {
if (!currentValues[layerIndex].hasOwnProperty('letterDelays')) {
currentValues[layerIndex].letterDelays = {};
}
currentValues[layerIndex].letterDelays[key] = v;
//tp.studio.transaction(({
//set
//}) => {
//set(layer.theatreObject.props.letterDelays[key], v);
//});
}
}
} else {
//console.log('MidiController::setLetterDelays - font has no letterDelays');
}
};
let mirror_x = false;
let mirror_y = false;
let mirror_xy = false;
let setMirror = (button, midiValue) => {
}
let setLayer = (button, midiValue) => {
const layers = getLayers();
if (button === 116 && midiValue === 127) {
activeLayer = (activeLayer + 1) % layers.length;
} else if (button === 117 && midiValue === 127) {
activeLayer = (activeLayer - 1 + layers.length) % layers.length;
}
layers[activeLayer].showBoundingBoxDiv();
setTimeout(() => {
layers[activeLayer].hideBoundingBoxDiv();
}, 100);
};
let setProject = (button, midiValue) => {
const projects = tp.listProjects();
const activeProject = projects.indexOf(tp.sheet.project.address.projectId);
let direction;
if (button === 114 && midiValue === 127) {
direction = 1;
} else if (button === 115 && midiValue === 127) {
direction = -1;
}
const nextProjectIndex = (activeProject + direction + projects.length) % projects.length;
const nextProject = projects[nextProjectIndex];
tp.reloadToProject(nextProject, true);
};
let setBackgroundOpacity = (knob, midiValue) => {
let prop = 'backgroundColor.a';
setValue(getArtboard(), layers.length, prop, midiValue, [0, 1]);
};
let setBackgroundColor = (button, midiValue) => {
let pr = 'backgroundColor.r';
let pg = 'backgroundColor.g';
let pb = 'backgroundColor.b';
let r = Math.random() * 127.0;
let g = Math.random() * 127.0;
let b = Math.random() * 127.0;
setValue(getArtboard(), layers.length, pr, r, [0, 1]);
setValue(getArtboard(), layers.length, pg, g, [0, 1]);
setValue(getArtboard(), layers.length, pb, b, [0, 1]);
};
let setSpeed = (knob, midiValue) => {
if (midiValue >= 62 && midiValue <= 64) {
tp.sheet.sequence.pause();
} else {
const min = -6;
const max = 6;
let v = (midiValue / 127.0) * (max - min) + min;
if (v > 0) {
tp.sheet.sequence.play({
iterationCount: Infinity,
rate: v
});
} else if (v < 0) {
tp.sheet.sequence.play({
direction: 'reverse',
iterationCount: Infinity,
rate: Math.abs(v)
});
}
playbackSpeed = Math.abs(v);
}
};
const sentValues = [];
const sentMidiValues = [];
const currentValues = [];
const valueBuffer = [];
const populateValueBuffer = (_layers) => {
_layers.forEach((layer) => {
currentValues.push({});
sentValues.push({});
sentMidiValues.push({});
valueBuffer.push(new Map());
//console.log('pushed -------------------> ', _layers.length, JSON.parse(JSON.stringify(layer.theatreObject.value)));
});
// artboard
currentValues.push({});
sentValues.push({});
sentMidiValues.push({});
valueBuffer.push(new Map());
};
const addValuesToBuffer = (layerIndex, values, time_s, start_time_s) => {
const layerValueBuffer = valueBuffer[layerIndex];
const copiedValues = JSON.parse(JSON.stringify(values));
if (start_time_s !== -1) {
layerValueBuffer.forEach((value, value_time_s) => {
if (value_time_s > start_time_s && value_time_s <= time_s) {
const keys = Object.keys(values);
for (let k = 0; k < keys.length; k++) {
delete layerValueBuffer.get(value_time_s)[keys[k]];
if (Object.keys(layerValueBuffer.get(value_time_s)).length === 0) {
layerValueBuffer.delete(value_time_s);
break;
}
};
}
});
}
if (layerValueBuffer.has(time_s)) {
layerValueBuffer.set(time_s, {...layerValueBuffer.get(time_s), ...copiedValues});
} else {
layerValueBuffer.set(time_s, copiedValues);
}
};
const getValuesFromBuffer = (layerIndex, time_s) => {
if (valueBuffer[layerIndex].size === 0) {
return {};
} else {
valueBuffer[layerIndex] = new Map([...valueBuffer[layerIndex].entries()].sort());
let mergedValues = {};
let didMergeValues = {};
valueBuffer[layerIndex].forEach((value, value_time_s) => {
if (value_time_s < time_s) {
mergedValues = {...mergedValues, ...value};
} else {
if (Object.keys(didMergeValues).length === 0) {
didMergeValues = JSON.parse(JSON.stringify(mergedValues));
}
Object.keys(value).forEach((key) => {
if(!didMergeValues.hasOwnProperty(key)) {
mergedValues[key] = value[key];
}
});
}
});
return mergedValues;
}
};
this.getValuesFromBuffer = getValuesFromBuffer;
this.currentValue = currentValues;
this.valueBuffer = valueBuffer;
const smoothed = {
184: [21, 22, 23, 24, 26, 27, 28, 41, 42, 43, 44, 45, 46, 47, 48]
};
const ledButtonRowStatus = [0, 0, 0, 0, 0, 0, 0, 0];
const ledButtonRowMapping = [9, 10, 11, 12, 25, 26, 27, 28];
const ledColors = [ 0, 12, 13, 15, 29, 63, 62, 28, 60 ];
const setLed = (button, color, statusCode = 152) => {
outputs.forEach((midiOutput) => {
if (midiOutput.name === "Launch Control MIDI 1") {
midiOutput.send([statusCode, button, color]);
}
});
}
this.ledTimeColor = 2;
this.setLed = setLed;
this.ledMapping = ledButtonRowMapping;
this.ledColors = ledColors;
const mapping = {
"Launch Control MIDI 1": {
'general': {
184: { // status code
114: setProject,
115: setProject,
116: setLayer,
117: setLayer,
28: setSpeed,
25: setBackgroundOpacity,
},
// buttons
152: { // down
9: setBackgroundColor,
10: setBackgroundColor,
11: setBackgroundColor,
12: setBackgroundColor,
25: setBackgroundColor,
26: setBackgroundColor,
27: setBackgroundColor,
28: setBackgroundColor,
},
},
'props': [{
// knobs
184: {
// first row
21: ['x', [0, 1920]],
22: ['y', [0, 1080]],
23: ['fontSize_px', [-1000, 1000]],
24: ['rotation', [-360, 360]],
25: ['transformOrigin', ['top_left', 'top_right', 'center', 'bottom_left', 'bottom_right']],
26: ['letterSpacing', [-3, 3]],
27: ['lineHeight', [0, 10]],
// 28 free
// second row
41: ['color.r', [0, 1]],
42: ['color.g', [0, 1]],
43: ['color.b', [0, 1]],
44: ['color.a', [0, 1]],
45: setLetterDelays,
46: setFontVariation,
47: setFontVariation,
48: setFontVariation,
},
152: { // down
//9: setPropsSet,
//10: setPropsSet,
//11:
//12:
//25:
//26:
//27:
//28:
},
136: { // up
//9:
//10:
//11:
//12:
//25:
//26:
//27:
//28:
}
}]
}
};
this.mapping = mapping;
let updatingMidiValues = {};
let appliedMidiValues = {};
let doApplyMidiValues = true;
window.applyMidiTimeoutMs = 30;
this.mix = mix;
const applyMidiValuesInterval = () => {
if (doApplyMidiValues) {
// apply midi values
const device = "Launch Control MIDI 1";
const mkeys = Object.keys(updatingMidiValues);
if (updatingMidiValues.hasOwnProperty(device)) {
const keys = Object.keys(updatingMidiValues[device]);
for (let i = 0; i < keys.length; i++) {
const key = parseInt(keys[i]);
const statusCode = updatingMidiValues[device][key][0];
const midiValue = updatingMidiValues[device][key][1];
if (!appliedMidiValues[device].hasOwnProperty(keys[i]) ||
appliedMidiValues[device][keys[i]] !== midiValue) {
if (typeof mapping[device].general[statusCode][key] === 'function') {
mapping[device].general[statusCode][key](key, midiValue);
} else if (mapping[device].props[activePropSet][statusCode].hasOwnProperty(key)) {
const pm = mapping[device].props[activePropSet][statusCode][key];
if (typeof pm === 'function') {
pm(getLayers()[activeLayer], activeLayer, key, midiValue);
} else {
setValue(getLayers()[activeLayer], activeLayer, pm[0], midiValue, pm[1]);
}
}
appliedMidiValues[device][key] = updatingMidiValues[device][key];
}
delete updatingMidiValues[device][key];
}
}
setTimeout(() => {
if (doApplyMidiValues) {
requestAnimationFrame(applyMidiValuesInterval);
}
}, window.applyMidiTimeoutMs);
}
};
const directlyApplyMidiValues = (device, key, statusCode, midiValue) => {
if (typeof mapping[device].general[statusCode][key] === 'function') {
mapping[device].general[statusCode][key](key, midiValue);
} else if (mapping[device].props[activePropSet][statusCode].hasOwnProperty(key)) {
const pm = mapping[device].props[activePropSet][statusCode][key];
if (typeof pm === 'function') {
pm(getLayers()[activeLayer], activeLayer, key, midiValue);
} else {
setValue(getLayers()[activeLayer], activeLayer, pm[0], midiValue, pm[1]);
}
}
};
window.mapping = mapping;
if (!("requestMIDIAccess" in navigator)) {
element.innerHTML = `<h1>:-/</h1><p>I'm sorry, but your browser does not support the WebMIDI API ☹️🚫🎹</p>`;
}
const registerEvents = () => {
openCloseButton.addEventListener('click', () => {
if (!isPanelOpen) {
isPanelOpen = true;
element.style.display = 'flex';
} else {
isPanelOpen = false;
element.style.display = 'none';
}
});
const buttonOn = document.createElement('div');
buttonOn.innerHTML = "light on";
buttonOn.addEventListener('click', () => {
outputs.forEach((midiOutput) => {
midiOutput.send([152, 9, 2]);
});
});
const buttonOff = document.createElement('div');
buttonOff.innerHTML = "light off";
buttonOff.addEventListener('click', () => {
outputs.forEach((midiOutput) => {
midiOutput.send([152, 9, 0]);
});
});
const buttonDebug = document.createElement('div');
buttonDebug.innerHTML = "debug on";
buttonDebug.addEventListener('click', () => {
if (debugLog) {
debugLog = false;
buttonDebug.innerHTML = "debug on";
} else {
debugLog = true;
buttonDebug.innerHTML = "debug off";
}
});
buttons.append(buttonOn);
buttons.append(buttonOff);
buttons.append(buttonDebug);
};
window.debugCallTimes = [];
const tryGetLayers = (resolve) => {
if (getLayers().length > 0 && tp.isProjectLoaded) {
resolve();
} else {
setTimeout(() => {
tryGetLayers(resolve);
}, 10);
}
};
const tryGetLayersP = () => {
return new Promise((resolve) => {
tryGetLayers(resolve);
});
};
const selectLayers = () => {
return new Promise((resolve) => {
let delay = 500;//parseInt(localStorage.getItem('debugdelay'));
for (let i = 0; i <= layers.length; i++) {
setTimeout(() => {
if (i < layers.length) {
tp.studio.setSelection([layers[i].theatreObject]);
} else {
tp.studio.setSelection([]);
resolve();
}
}, i * delay);
}
});
};
window.ofRA = true;
window.ofUpdateMS = 1000 / 30;
const timeline_head = document.querySelector('#timeline_head');
const timeline = document.querySelector('#timeline');
let last_realtime_s = -999999;
let last_time_s = -999999;
let last_touchtime_s = -999999;
let last_processed_touchtime_s = -999999;
const ofUpdater = () => {
const realtime_s = performance.now() / 1000.0;
const time_s = tp.sheet.sequence.position;
const percent = time_s / tp.duration * 100;
{
const led = 114;
const statusCode = 184;
const color = [3,2,1,0][Math.floor(realtime_s * 4.0) % 4];
setLed(led, color, statusCode);
}
{
const led = 115;
const statusCode = 184;
const color = [0,1,2,3][Math.floor(realtime_s * 4.0) % 4];
setLed(led, color, statusCode);
}
{
const led = 117;
const statusCode = 184;
const color = [3,2,1,2][Math.floor(realtime_s * 6.0) % 4];
setLed(led, color, statusCode);
}
{
const led = 116;
const statusCode = 184;
const color = [1,2,3,2][Math.floor(realtime_s * 6.0) % 4];
setLed(led, color, statusCode);
}
for (let b = 0; b < ledButtonRowMapping.length; b++) {
const percentIndex = Math.floor((percent * 0.01) * ledButtonRowMapping.length);
if (b === percentIndex) {
ledButtonRowStatus[b] = Math.floor(Math.random() * 127.0);
}
setLed(ledButtonRowMapping[b], ledButtonRowStatus[b]);
}
let currentlyTouching = false;
if (realtime_s - last_touchtime_s < config.midi.touchTimeThreshold_s) {
currentlyTouching = true;
}
if (Object.keys(currentValues[activeLayer]).length > 0 && last_touchtime_s !== last_processed_touchtime_s) {
let starttime_s = -1;
if (realtime_s - last_realtime_s < config.midi.touchTimeThreshold_s) {
starttime_s = last_time_s; // fires first time prematurely, but this is okay (=> -1)
}
addValuesToBuffer(activeLayer, currentValues[activeLayer], time_s, starttime_s);
last_processed_touchtime_s = last_touchtime_s;
last_realtime_s = realtime_s;
last_time_s = time_s;
}
timeline_head.style.left = `calc(${percent}% - 10px)`;
timeline.style.background = currentlyTouching ? 'red' : 'grey';
for (let i = 0; i <= layers.length; i++) {
let bufferValues = JSON.parse(JSON.stringify(getValuesFromBuffer(i, time_s)));
bufferValues = {...bufferValues, ...currentValues[i]};
sentMidiValues[i] = mixObject(sentMidiValues[i], bufferValues, config.midi.smoothingMix);
if (i < layers.length) {
const values = {...layers[i].theatreObject.value, ...sentMidiValues[i]};
sentValues[i] = mixObject(sentValues[i], values, config.midi.smoothingMix);
let p = layers[i].values2cppProps(values);
if (p !== false) {
Module.setProps(p, layers[i].id());
}
} else {
const artboardValues = {...getArtboard().theatreObject.value, ...sentMidiValues[i]}
sentValues[i] = mixObject(sentValues[i], artboardValues, config.midi.smoothingMix);
let cppProps = getArtboard().values2cppProps(artboardValues);
Module.setArtboardProps(cppProps);
}
}
if (!currentlyTouching) {
for (let i = 0; i < currentValues.length; i++) {
currentValues[i] = {};
}
}
if (window.ofRA) {
requestAnimationFrame(ofUpdater);
} else {
setTimeout(() => {
ofUpdater();
}, window.ofUpdateMS);
}
}
const init = () => {
tryGetLayersP().then(() => {
layers = getLayers();
//console.log('what... this is layers' , layers);
const promises = [];
layers.forEach((layer) => {
promises.push(layer.updateFonts());
});
if (tp.sheet.project.address.projectId === 'rudi-midi') {
mapping["Launch Control MIDI 1"].props[0][184][23] = ['fontSize_px', [-128, 128]];
}
if (tp.sheet.project.address.projectId === 'sam-midi') {
mapping["Launch Control MIDI 1"].props[0][184][23] = ['fontSize_px', [-256, 256]];
}
Promise.all(promises).then(() => {
layers.forEach((layer, layerI) => {
const letterDelayProps = [
{
sequenced: true,
prop: ['color'],
},
{
sequenced: true,
prop: ['letterSpacing']
},
{
sequenced: true,
prop: ['fontSize_px']
},
];
if (layer.props.hasOwnProperty('fontVariationAxes')) {
const keys = Object.keys(layer.props.fontVariationAxes.props);
keys.forEach((key) => {
const detail = {
sequenced: true,
prop: ['fontVariationAxes', key],
};
letterDelayProps.push(detail);
});
}
letterDelayProps.forEach((detail, i) => {
// only update theatre for the last one
const updateTheatre = i === letterDelayProps.length - 1;
layer.handleSequenceEvent(detail, updateTheatre)
.then((updatedTheatre) => {
if (updatedTheatre && layerI === layers.length - 1) {
populateValueBuffer(layers);
ofUpdater();
isInitialized = true;
}
});
});
});
});
//selectLayers().then(() => {
//});
});
registerEvents();
navigator.requestMIDIAccess()
.then((access) => {
// Get lists of available MIDI controllers
inputs = access.inputs;
outputs = access.outputs;
const inputText = [];
const outputText = [];
inputs.forEach((midiInput) => {
inputText.push(`FOUND: ${midiInput.name}\n`);
updatingMidiValues[midiInput.name] = {};
appliedMidiValues[midiInput.name] = {};
midiInput.onmidimessage = function(message) {
//window.debugCallTimes.push(performance.now());
if (midiInput.name === "Launch Control MIDI 1") {
const isGeneral =
mapping[midiInput.name]
.general.hasOwnProperty(message.data[0]) &&
mapping[midiInput.name]
.general[message.data[0]].hasOwnProperty(message.data[1]);
const isProp =
mapping[midiInput.name]
.props[activePropSet].hasOwnProperty(message.data[0]) &&
mapping[midiInput.name]
.props[activePropSet][message.data[0]].hasOwnProperty(message.data[1]);
if (isInitialized && (isGeneral || isProp)) {
last_touchtime_s = performance.now() / 1000.0;
updatingMidiValues[midiInput.name][message.data[1]] = [message.data[0], message.data[2]];
//directlyApplyMidiValues(midiInput.name, message.data[1], message.data[0], message.data[2]);
}
autoSwitchPerhaps();
}
if (debugLog) {
element.querySelector(".midiMessages").innerText += `# ${midiInput.name}
${new Date()}
==================================
- Status: ${message.data[0]}
- Data 1: ${message.data[1]}
- Data 2: ${message.data[2]}
==================================\n\n`;
}
};
});
outputs.forEach((midiOutput) => {
outputText.push(`FOUND: ${midiOutput.name}\n`);
});
element.querySelector(".inputs").innerText = inputText.join('');
element.querySelector(".outputs").innerText = outputText.join('');
applyMidiValuesInterval();
// lalalaload another project
//autoSwitchPerhaps();
});
};
let autoSwitchTimeout = false;
const autoSwitchPerhaps = () => {
clearTimeout(autoSwitchTimeout);
autoSwitchTimeout = setTimeout(() => {
setProject(114, 127);
}, 5 * 60 * 1000);
};
const setValue = (layer, layerIndex, prop, value, minMax) => {
let v;
let propName = prop;
if (minMax.length > 2) {
const index = Math.floor((value / 128.0) * minMax.length);
v = minMax[index];
} else {
const min = minMax[0];
const max = minMax[1];
v = (value / 127.0) * (max - min) + min;
if (propName.indexOf('color') === 0) {
propName = propName.split('.')[1];
let color;
if (currentValues[layerIndex].hasOwnProperty('color')) {
color = {...layer.theatreObject.value.color, ...currentValues[layerIndex].color};
} else {
color = layer.theatreObject.value.color;
}
color[propName] = v;
propName = 'color';
v = color;
}
if (propName.indexOf('backgroundColor') === 0) {
propName = propName.split('.')[1];
let backgroundColor;
if (currentValues[layerIndex].hasOwnProperty('backgroundColor')) {
backgroundColor = {...layer.theatreObject.value.backgroundColor, ...currentValues[layerIndex].backgroundColor};
} else {
backgroundColor = layer.theatreObject.value.backgroundColor;
}
backgroundColor[propName] = v;
propName = 'backgroundColor';
v = backgroundColor;
}
}
currentValues[layerIndex][propName] = v;
//tp.studio.transaction(({
//set
//}) => {
//set(layer.theatreObject.props[propName], v);
//});
};
this.init = init;
this.getInputs = () => {
return inputs;
}
this.getOutputs = () => {
return outputs;
}
};
export {
MidiController
};

88
bin/web/js/moduleFS.js Normal file
View file

@ -0,0 +1,88 @@
const ModuleFS = function() {
const MODE_WRITE_TO_PERSISTENT = false;
const MODE_READ_FROM_PERSISTENT = true;
this.init = () => {
return new Promise((resolve) => {
FS.mkdir(config.fs.idbfsDir);
// Then mount with IDBFS type
FS.mount(IDBFS, {}, config.fs.idbfsDir);
this.syncfs(MODE_READ_FROM_PERSISTENT)
.then(() => {
// Then sync with true to get persistent data
if (!FS.analyzePath(config.fs.idbfsFontDir).exists) {
FS.mkdir(config.fs.idbfsFontDir);
}
if (!FS.analyzePath(config.fs.idbfsTmpDir).exists) {
FS.mkdir(config.fs.idbfsTmpDir);
}
resolve();
});
});
};
this.syncfs = (mode = MODE_READ_FROM_PERSISTENT) => {
return new Promise((resolve, reject) => {
FS.syncfs(mode, function(err) {
if (err !== null) {
// Error
console.error(err);
reject(error);
} else {
resolve();
}
});
});
};
// check utils::uploadFile() for details of file
this.save = (file) => {
return new Promise((resolve) => {
if (file.type.indexOf('font') >= 0 || file.hasOwnProperty('isFont') && file.isFont === true) {
var uint8View = new Uint8Array(file.arrayBuffer);
console.log('trying to save the font file, file, uint8View', file, uint8View);
if (!FS.analyzePath(`${config.fs.idbfsFontDir}/${file.name}`).exists) {
FS.createDataFile(config.fs.idbfsFontDir, file.name, uint8View, true, true);
}
this.syncfs(MODE_WRITE_TO_PERSISTENT)
.then(() => {
resolve(true);
});
} else if (file.type.indexOf('zip') >= 0 || file.hasOwnProperty('isZip') && file.isZip === true) {
var uint8View = new Uint8Array(file.arrayBuffer);
var filePath = `${config.fs.idbfsTmpDir}/${file.name}`;
console.log(filePath);
if (!FS.analyzePath(filePath).exists) {
FS.createDataFile(config.fs.idbfsTmpDir, file.name, uint8View, true, true);
}
this.syncfs(MODE_WRITE_TO_PERSISTENT)
.then(() => {
resolve(filePath);
});
} else {
resolve(false);
}
});
};
this.delete = (file) => {
if (file.type.indexOf('zip') >= 0 || file.hasOwnProperty('isZip') && file.isZip === true) {
var filePath = `${config.fs.idbfsTmpDir}/${file.name}`;
if (!FS.analyzePath(filePath).exists) {
console.log(`moduleFS::delete(${filePath})`, `file does not exist`);
} else {
FS.unlink(filePath);
}
this.syncfs(MODE_WRITE_TO_PERSISTENT)
.then(() => {
resolve(true);
});
} else {
resolve(false);
}
};
};
export {
ModuleFS
};

50
bin/web/js/record.js Normal file
View file

@ -0,0 +1,50 @@
const Record = function(tp) {
const hot = {};
const addRecordButton = (layer, propTitle, isActive) => {
const panel = tp.getPanel();
const panelPropTitle = tp.getPanelPropTitle(propTitle);
if (panelPropTitle !== null) {
const container = tp.getPanelPropContainer(panelPropTitle);
if (container === null) {
console.log("Record::addRecordButton",
`impossible! cannot find panelPropContainer for ${propTitle}`);
} else if (container.querySelector('.recordButton') !== null) {
console.log("Record::addRecordButton",
`already added an record button for ${propTitle}`);
} else {
const button = document.createElement('div');
button.classList.add('recordButton');
button.classList.add(`recordButton${propTitle}`);
button.innerHTML = `<img src="/web/assets/record.svg" alt="record" />`;
container.append(button);
button.addEventListener('click', () => {
if (!hot.hasOwnProperty(layer.id())) {
hot[layer.id()] = {};
}
if (!hot[layer.id()].hasOwnProperty(propTitle)) {
hot[layer.id()][propTitle] = {};
button.classList.add('active');
} else {
delete hot[layer.id()][propTitle];
if (Object.keys(hot[layer.id()]).length === 0) {
delete hot[layer.id()];
}
button.classList.remove('active');
}
});
}
} else {
console.log("Record::addRecordButton",
`cannot find panelPropTitle for ${propTitle}`);
}
};
// public
this.addRecordButton = addRecordButton;
};
export {
Record
}

118
bin/web/js/script.js Normal file
View file

@ -0,0 +1,118 @@
const scannerLine = document.getElementById("scannerLine");
const scannerLineH = document.getElementById("scannerLineH");
$(document).ready(function() {
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
$("body").removeClass("hideBody");
$("body").addClass("mobile");
}else if(('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0) && (testratio<0.7777777) ){
$("body").removeClass("hideBody");
$("body").addClass("mobile mobileOld");
}else{
$("body").removeClass("hideBody");
$("body").addClass("desktop")
};
const img = new Image();
img.crossOrigin = "anonymous";
img.src = "./media/360_F_163966311_qh3qSk57mw9oLPOklZigzX9zlB5DgdaM.jpeg";
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const scanner = $(".scanner");
const destination = document.getElementById("letter");
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
img.addEventListener("load", () => {
canvas.height = canvas.width * (img.height / img.width);
scanner.css("width", $("#canvas").width());
scanner.css("height", $("#canvas").height());
var hRatio = canvas.width / img.width ;
var vRatio = canvas.height / img.height ;
var ratio = Math.min ( hRatio, vRatio );
ctx.drawImage(img, 0,0, img.width, img.height, 0,0,img.width*ratio, img.height*ratio);
// ctx.drawImage(img, 0, 0);
img.style.display = "none";
});
const hoveredColor = document.getElementById("hovered-color");
const selectedColor = document.getElementById("selected-color");
// let rect = scannelLine.getBoundingClientRect();
function pick() {
const bounding = canvas.getBoundingClientRect();
const letters = document.getElementById("typeMaster");
const letter = $(".letter");
letter.each(function(i,e){
let rect = scannerLine.getBoundingClientRect();
let rectH = scannerLineH.getBoundingClientRect();
const x = rect.left - bounding.left;
const y = rectH.top - bounding.top;
const pixel = ctx.getImageData(x - (i*2), y , 1, 1);
const data = pixel.data;
let rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
hoveredColor.style.background = rgba;
hoveredColor.textContent = rgba;
// letters.style.color = rgba;
// $(".typeMaster").css("background",rgba);
$(e).css("color",rgba);
$(e).css("font-variation-settings","'wght' "+ ((data[0]) + (data[1]) + (data[2])));
return rgba;
})
}
const milliseconds = 100;
window.setInterval(pick, milliseconds);
});
$(window).on("load", function(){
$("#typeMaster").html(function(index, html) {
return html.replace(/\w+/g, '<span class="word">$&</span>');
});
$(".word").html(function(index, html) {
return html.replace(/\S/g, '<span class="letter">$&</span>');
})
var length = $(".letter").length;
var currentMousePos = { x: -1, y: -1 };
});
var slider = document.getElementById("myRange");
var output = document.getElementById("demo");
output.innerHTML = "duration X: " + slider.value; // Display the default slider value
var sliderY = document.getElementById("myRangeY");
var outputY = document.getElementById("demoY");
outputY.innerHTML = "duration Y: " + sliderY.value; // Display the default slider value
slider.oninput = function() {
output.innerHTML = "duration X: " + this.value;
scannerLine.style.animationDuration = this.value + "s";
}
sliderY.oninput = function() {
outputY.innerHTML = "duration Y: " + this.value;
scannerLineH.style.animationDuration = this.value + "s";
}

1037
bin/web/js/theatre-play.js Normal file

File diff suppressed because it is too large Load diff

408
bin/web/js/utils.js Normal file
View file

@ -0,0 +1,408 @@
/////////////////////////////////////
const UUID = function() {
let allowedIdChars = "0123456789abcdef";
// fallback in case we cannot crypto
const notUniqueId = (t = 16) => {
let out = "";
for (let i = 0; i < t; i++) {
out += allowedIdChars[(Math.random() * allowedIdChars.length + Math.floor(performance.now())) % allowedIdChars.length];
}
return out;
}
// from https://github.com/ai/nanoid/blob/main/nanoid.js
const uniqueId = (t = 16) => {
const indices = crypto.getRandomValues(new Uint8Array(t));
let out = "";
for (var i = 0; i < t; i++) {
out += allowedIdChars[indices[i] % allowedIdChars.length];
}
return out;
}
this.getUuid = () => {
return typeof crypto === 'object' && typeof crypto.getRandomValues === 'function' ?
uniqueId() : notUniqueId();
}
}
const uuid = new UUID();
const getUuid = () => {
return uuid.getUuid();
}
const makeEven = (n) => {
const nr = Math.round(n);
return nr - nr % 2;
}
const getMix = (before_s, after_s, time_s, clamp = true) => {
const diff = after_s - before_s;
const travel = time_s - before_s;
if (diff === 0 || travel === 0) {
return 0;
} else if (clamp) {
return Math.min(Math.max(travel / diff));
} else {
return travel / diff;
}
}
const mix = (a, b, m, t) => {
if (Math.abs(a - b) < t) {
return b;
} else {
return a * (1.0 - m) + b * m;
}
};
const mixObject = (a, b, m) => {
const out = JSON.parse(JSON.stringify(a));
const a_keys = Object.keys(a);
const b_keys = Object.keys(b);
let keys = [...new Set([...a_keys, ...b_keys])];
keys.forEach((key) => {
if (!a.hasOwnProperty(key)) {
out[key] = b[key];
} else if (!b.hasOwnProperty(key)) {
out[key] = a[key];
} else {
if (typeof a[key] === 'object') {
out[key] = mixObject(a[key], b[key], m);
} else if (typeof a[key] === 'number') {
out[key] = a[key] * (1.0 - m) + b[key] * m;
} else {
out[key] = b[key];
}
}
});
return out;
};
/////////////////////////////////////
const htmlToElement = (html) => {
var template = document.createElement('template');
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
return template.content.firstChild;
}
/////////////////////////////////////
// download(textData, 'lol.txt', 'text/plain');
// download(jsonData, 'lol.json', 'application/json');
function downloadFile(content, fileName, contentType) {
var a = document.createElement("a");
var file = new Blob([content], {
type: contentType
});
a.href = URL.createObjectURL(file);
a.download = fileName;
a.click();
}
function uploadFile(expectedType = 'application/json') {
return new Promise((resolve, reject) => {
var input = document.createElement('input');
input.type = 'file';
input.addEventListener('change', () => {
let json;
let files = input.files;
if (files.length == 0) return;
const file = files[0];
console.log('file', file);
let reader = new FileReader();
if (expectedType === 'application/zip' || file.type === 'application/zip') {
reader.onload = (e) => {
const f = e.target.result;
console.log(e, file.name, file.size, file.type, f);
resolve({
name: file.name,
size: file.size,
type: file.type,
arrayBuffer: f,
});
};
reader.onerror = (e) => reject(e.target.error.name);
reader.readAsArrayBuffer(file);
} else if (expectedType === 'application/json') {
reader.onload = (e) => {
console.log(e);
const f = e.target.result;
// This is a regular expression to identify carriage
// Returns and line breaks
//const lines = file.split(/\r\n|\n/);
if (file.type === expectedType) {
try {
json = JSON.parse(f);
resolve(json);
} catch (e) {
reject("Caught: " + e.message)
}
}
};
reader.onerror = (e) => reject(e.target.error.name);
reader.readAsText(file);
} else if (expectedType.indexOf('font') >= 0) {
console.log('expect font');
reader.onload = (e) => {
console.log(e);
const f = e.target.result;
if (file.type.indexOf('font') >= 0) {
console.log('is font');
//var uint8View = new Uint8Array(f);
//console.log('trying to save the font file, file, uint8View', file, uint8View);
//FS.createDataFile(config.fs.idbfsFontDir, file.name, uint8View, true, true);
resolve({
name: file.name,
size: file.size,
type: file.type,
arrayBuffer: f
});
} else {
const extension = file.name.split('.').reverse().shift()
const fileType = `font/${extension}`;
if(confirm(`${file.name} has type ${file.type} instead of the expected ${fileType}. are you sure this is a font?`)) {
const outputFile = {
isFont: true,
name: file.name,
size: file.size,
type: file.type,
arrayBuffer: f
};
console.log({outputFile});
resolve(outputFile);
} else {
reject('not a font');
}
}
};
reader.onerror = (e) => reject(e.target.error.name);
reader.readAsArrayBuffer(file);
} else {
alert(`unknown filetype ${file.type}, what are you uploading?`);
resolve(false);
}
});
input.click();
//var a = document.createElement('a');
//a.onclick = () => {
//var e = document.createEvent('MouseEvents');
//e.initEvent('click', true, false);
//input.dispatchEvent(e);
//};
//a.click();
});
}
const makeDraggable = (elmnt, draggedCallback = false) => {
var pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
if (elmnt.querySelector('.header .move')) {
// if present, the header is where you move the DIV from:
elmnt.querySelector('.header .move').onmousedown = dragMouseDown;
} else {
// otherwise, move the DIV from anywhere inside the DIV:
elmnt.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
if (typeof draggedCallback === 'function') {
draggedCallback();
}
}
}
const cyrb53 = (str, seed = 0) => {
let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
const hashFromString = (str, prefix = 'hash') => {
return `${prefix}${cyrb53(str)}`;
};
function getBaseName(filePath) {
return filePath.substring(filePath.lastIndexOf('/') + 1, filePath.lastIndexOf('.'))
}
function verifyVariableTimeProject(vt_project) {
const exampleProject = {
projectId: 'exampleProject',
variable_time_version: VARIABLE_TIME_VERSION,
theatre: 'complete theatre saveFile',
layerOrder: ['layer-0', 'layer-4', 'layer-2'],
};
if (!vt_project) {
console.error('Utils::verifyVariableTimeProject::couldNotVerify',
'project equals false',
'this is what we received',
vt_project,
'compare to following example',
exampleProject);
return false;
}
if (typeof vt_project === 'string') {
console.error('Utils::verifyVariableTimeProject::couldNotVerify',
'project is a string, please first parse json ',
'this is what we received',
vt_project,
'compare to following example',
exampleProject);
return false;
// do not allow strings
//try {
//vt_project = JSON.parse(vt_project);
//} catch (e) {
//console.error('Utils::verifyVariableTimeProject::couldNotVerify',
//'project is a string,
//but could not parse json ',
//'this is what we received',
//vt_project,
//'compare to following example',
//exampleProject);
//return false;
//}
}
if (typeof vt_project !== 'object') {
console.error('Utils::verifyVariableTimeProject::couldNotVerify',
'project is not an object',
'this is what we received',
vt_project,
'compare to following example',
exampleProject);
return false;
}
const exampleKeys = Object.keys(exampleProject);
for (let i = 0; i < exampleKeys.length; i++) {
if (!vt_project.hasOwnProperty(exampleKeys[i])) {
console.error('Utils::verifyVariableTimeProject::couldNotVerify',
`${exampleKeys[i]} missing`,
'this is what we received',
vt_project,
'compare to following example',
exampleProject);
return false;
}
}
return true;
}
function clone(a) {
return JSON.parse(JSON.stringify(a));
};
function getParents(elem, until = null) {
const parents = [];
let done = false;
while (!done) {
elem = elem.parentNode;
if (elem === until) {
done = true;
} else if (elem === null) {
// until is not a parent
return null;
} else {
parents.push(elem);
}
}
return parents;
}
function arraysEqual(a, b, sortingMatters = false) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
if (!Array.isArray(a) || !Array.isArray(b)) return false;
let _a = sortingMatters ? a : a.toSorted();
let _b = sortingMatters ? b : b.toSorted();
for (var i = 0; i < _a.length; ++i) {
if (_a[i] !== _b[i]) return false;
}
return true;
}
const mapValue = (value, low1, high1, low2, high2, clamp=false) => {
const mapped = low2 + (high2 - low2) * (value - low1) / (high1 - low1);
return clamp ? Math.min(high2 > low2 ? high2 : low2, Math.max(low2 < high2 ? low2 : high2, mapped)) : mapped;
}
/////////////////////////////////////
export {
getUuid,
htmlToElement,
downloadFile,
uploadFile,
makeDraggable,
getBaseName,
hashFromString,
verifyVariableTimeProject,
makeEven,
mix,
getMix,
mixObject,
clone,
getParents,
arraysEqual,
mapValue,
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12
browser.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
fly ()
{
nohup $@ < /dev/null > /dev/null 2>&1 &
echo $!
}
#echo $(fly /usr/bin/chromium --incognito --screen=1 $@)
#/usr/bin/chromium --incognito --screen=1 $@ &
/usr/bin/chromium --screen=1 --enable-logging --password-store=basic --v=1 $@ &
echo $!

14
clean.sh Executable file
View file

@ -0,0 +1,14 @@
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
PREVIOUS_DIR=$(pwd)
cd $DIR
make clean && rm -rf obj && rm -rf ../../../addons/obj
rm -rf ../../../libs/openFrameworksCompiled/lib/linux64/obj
rm -rf ../../../libs/openFrameworksCompiled/lib/linux64/libopenFrameworks.a
rm -rf ../../../libs/openFrameworksCompiled/lib/emscripten/obj
rm -rf ../../../libs/openFrameworksCompiled/lib/emscripten/libopenFrameworks.bc
cd $PREVIOUS_DIR

14
compatibleHa.sh Executable file
View file

@ -0,0 +1,14 @@
#!/bin/bash
# get compatible dependencies hash
CUR_PWD=$(pwd)
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
ADDONS=$(cat addons.make)
for a in $ADDONS; do
echo "compatible $a hash:"
cd ${DIR}/../../../addons/$a && git rev-parse HEAD && cd $CUR_PWD
done
cd $CUR_PWD

25
generate_compile_commands.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
PREVIOUS_DIR="$(pwd)"
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd $DIR
# clean
make clean && rm -rf obj && rm -rf ../../../addons/obj
rm -rf ../../../libs/openFrameworksCompiled/lib/linux64/obj
rm -rf ../../../libs/openFrameworksCompiled/lib/linux64/libopenFrameworks.a
# generate
bear -- make -j$(nproc)
# rename
mv compile_commands.json compile_commands.linux64.json
# clean
make clean && rm -rf obj && rm -rf ../../../addons/obj
rm -rf ../../../libs/openFrameworksCompiled/lib/emscripten/obj
rm -rf ../../../libs/openFrameworksCompiled/lib/emscripten/libopenFrameworks.bc
bear -- emmake make -j$(nproc)
cd $PREVIOUS_DIR

3
grepJs.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
grep bin/web/js --exclude="*.min.*" --exclude="node_modules" --exclude="*.swp" --exclude="*.bundle.js" --exclude="script.js" --exclude="ffmpeg_modules" -nr -e $@

16
lightclean.sh Executable file
View file

@ -0,0 +1,16 @@
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
PREVIOUS_DIR=$(pwd)
cd $DIR
project=$(basename $DIR)
rm -rf bin/$project*
rm -rf bin/data/ofxMsdfgen
rm -rf bin/data/ofxGPUFont
cp -r ../../../addons/ofxMsdfgen/data/ofxMsdfgen ./bin/data/
cp -r ../../../addons/ofxGPUFont/data/ofxGPUFont ./bin/data/
cd $PREVIOUS_DIR

84
serve.py Executable file
View file

@ -0,0 +1,84 @@
#!/usr/bin/env python3
from http.server import HTTPServer, SimpleHTTPRequestHandler, test # type: ignore
from pathlib import Path
import os
import sys
import argparse
import subprocess
import ssl
# openssl req -new -x509 -keyout ssl/key.pem -out ssl/server.pem -days 365 -nodes
class CORSRequestHandler(SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
self.send_header("Access-Control-Allow-Origin", "*")
super().end_headers()
def shell_open(url):
if sys.platform == "win32":
os.startfile(url)
else:
opener = "open" if sys.platform == "darwin" else "xdg-open"
subprocess.call([opener, url])
def serve(root, port, run_browser):
os.chdir(root)
protocol = 'http' # will be upgraded if we have certfiles, see below
open_host = 'localhost' # we serve on 0.0.0.0 for network, but open localhost
host = '0.0.0.0'
server_address = (host, port)
# test(CORSRequestHandler, HTTPServer, port=port)
httpd = HTTPServer(server_address, CORSRequestHandler)
certfile = "ssl/localhost+4.pem"
keyfile = "ssl/localhost+4-key.pem"
if not os.path.exists(certfile):
print("using ssl/server.pm")
certfile = "ssl/server.pem"
if not os.path.exists(keyfile):
print("using ssl/key.pm")
keyfile = "ssl/key.pem"
if os.path.exists(certfile) and os.path.exists(keyfile):
httpd.socket = ssl.wrap_socket(httpd.socket,
server_side=True,
certfile=certfile,
keyfile=keyfile,
ssl_version=ssl.PROTOCOL_TLS)
protocol = 'https'
if run_browser:
# Open the served page in the user's default browser.
print("Opening the served URL in the default browser (use `--no-browser` or `-n` to disable this).")
subprocess.call([f"../browser.sh", f"{protocol}://{open_host}:{port}/msdf-theatre.html"])
print(f"serving on port {port}")
httpd.serve_forever()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-p", "--port", help="port to listen on", default=8060, type=int)
parser.add_argument(
"-r", "--root", help="path to serve as root (relative to `platform/web/`)", default="./bin", type=Path
)
browser_parser = parser.add_mutually_exclusive_group(required=False)
browser_parser.add_argument(
"-n", "--no-browser", help="don't open default web browser automatically", dest="browser", action="store_false"
)
parser.set_defaults(browser=True)
args = parser.parse_args()
# Change to the directory where the script is located,
# so that the script can be run from any location.
os.chdir(Path(__file__).resolve().parent)
serve(args.root, args.port, args.browser)

87
src/Artboard.cpp Normal file
View file

@ -0,0 +1,87 @@
#include "Artboard.h"
namespace VariableEditor {
void Artboard::setup(){
props.width = ofGetWidth();
props.height = ofGetHeight();
position = glm::vec3(0);
fboSettings.width = makeEven(props.width * props.pixelDensity);
fboSettings.height = makeEven(props.height * props.pixelDensity);
fboSettings.numSamples = 0;
fboSettings.internalformat = GL_RGBA;
fboSettings.minFilter = GL_LINEAR_MIPMAP_LINEAR;
fboSettings.maxFilter = GL_NEAREST;
//fboSettings.textureTarget = GL_TEXTURE_2D_MULTISAMPLE;
fbo.allocate(fboSettings);
fbo.begin();
ofClear(ofFloatColor(props.backgroundColor[0],
props.backgroundColor[1],
props.backgroundColor[2],
props.backgroundColor[3]));
fbo.end();
}
void Artboard::setProps(const ArtboardProps & props){
if(props.width != this->props.width
|| props.height != this->props.height
|| props.pixelDensity != this->props.pixelDensity){
ofFbo newFbo;
fboSettings.width = makeEven(props.width * props.pixelDensity);
fboSettings.height = makeEven(props.height * props.pixelDensity);
newFbo.allocate(fboSettings);
newFbo.resetAnchor();
newFbo.begin();
ofClear(ofFloatColor(props.backgroundColor[0],
props.backgroundColor[1],
props.backgroundColor[2],
props.backgroundColor[3]));
newFbo.end();
fbo = std::move(newFbo);
}
position = glm::vec3(props.x, props.y, 0);
this->props = props;
}
ArtboardProps Artboard::getProps(){
return props;
}
void Artboard::setPosition(float x,
float y,
float z){
position = glm::vec3(x, y, z);
}
glm::ivec2 Artboard::getShape(){
return glm::ivec2(props.width, props.height);
}
void Artboard::begin(){
fbo.begin();
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE);
ofPushStyle();
ofFill();
ofSetColor(ofFloatColor(props.backgroundColor[0],
props.backgroundColor[1],
props.backgroundColor[2],
props.backgroundColor[3]));
ofDrawRectangle(0, 0, fbo.getWidth(), fbo.getHeight());
ofPopStyle();
}
void Artboard::end(){
fbo.getTexture().generateMipmap();
fbo.end();
}
void Artboard::draw(){
ofPushMatrix();
ofTranslate(position);
ofScale(props.zoom);
fbo.draw(0, 0, makeEven(fbo.getWidth() / props.pixelDensity), makeEven(fbo.getHeight() / props.pixelDensity));
ofPopMatrix();
}
const ofFbo & Artboard::getFbo(){
return fbo;
}
}

39
src/Artboard.h Normal file
View file

@ -0,0 +1,39 @@
#pragma once
#include "ofMain.h"
namespace VariableEditor {
struct ArtboardProps {
std::array <float, 4> backgroundColor = {1.0, 1.0, 1.0, 1.0};
int x;
int y;
int width;
int height;
float zoom;
float pixelDensity = 1.0;
};
class Artboard {
int makeEven(float n){
n = n + 0.5 - (n < 0);
int nr = int(n);
return nr - nr % 2;
}
public:
void setup();
void setProps(const ArtboardProps & props);
ArtboardProps getProps();
void setPosition(float x, float y, float z);
glm::ivec2 getShape();
void begin();
void end();
void draw();
const ofFbo & getFbo();
private:
ArtboardProps props;
ofFboSettings fboSettings;
ofFbo fbo;
glm::vec3 position;
};
}

1
src/Exporter.cpp Normal file
View file

@ -0,0 +1 @@
#include "Exporter.h"

8
src/Exporter.h Normal file
View file

@ -0,0 +1,8 @@
#pragma once
#include "Zip.h"
namespace VariableEditor {
class Exporter {
};
}

52
src/Zip.cpp Normal file
View file

@ -0,0 +1,52 @@
#include "Zip.h"
#include "zip/zip.h"
namespace VariableEditor {
Zip::Zip(const std::string & filename){
//zip = zip_open(filename.c_str(), ZIP_DEFAULT_COMPRESSION_LEVEL, 'w');
zip = zip_stream_open(NULL, 0, ZIP_DEFAULT_COMPRESSION_LEVEL, 'w');
}
Zip::~Zip(){
if(!closed){
this->close();
}
}
void Zip::addFile(const std::string & filename){
zip_entry_open(zip, filename.c_str());
{
zip_entry_fwrite(zip, filename.c_str());
}
zip_entry_close(zip);
}
void Zip::addBuffer(std::string filename, char * inbuf, size_t inbufsize){
zip_entry_open(zip, filename.c_str());
{
zip_entry_write(zip, inbuf, inbufsize);
}
zip_entry_close(zip);
}
void Zip::getOutputBuffer(char * * outbuf, size_t & outbufsize){
zip_stream_copy(zip, (void * *)outbuf, &outbufsize);
}
void Zip::close(){
std::cout << "close zip" << std::endl;
zip_stream_close(zip);
closed = true;
}
UnZip::UnZip(const std::string & filename, const std::string & outdir) {
//zip = zip_open(filename.c_str(), ZIP_DEFAULT_COMPRESSION_LEVEL, 'r');
zip_extract(filename.c_str(), outdir.c_str(), NULL, NULL);
//zip_stream_extract(inbuf, inbufsize, outdir.c_str(), NULL, NULL);
//free(inbuf);
}
UnZip::~UnZip() {};
}

32
src/Zip.h Normal file
View file

@ -0,0 +1,32 @@
#pragma once
#include "zip/zip.h"
#include <string>
#include <iostream>
#include "ofMain.h"
namespace VariableEditor {
class Zip {
public:
Zip(const std::string & filename = "lol.zip");
~Zip();
//void open(const std::string & filename);
void addFile(const std::string & filename);
void addBuffer(std::string filename, char * inbuf, size_t inbufsize);
void getOutputBuffer(char * * outbuf, size_t & outbufsize);
void close();
private:
struct zip_t * zip;
bool closed = false;
};
class UnZip {
public:
UnZip(const std::string & filename, const std::string & outdir);
~UnZip();
private:
struct zip_t * zip;
bool closed = false;
};
}

View file

@ -0,0 +1,2 @@
*.swp
*.swo

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Armchair-Software
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.

View file

@ -0,0 +1,131 @@
# Emscripten Browser File Library
Header-only C++ library to receive files from, and offer files to, the browser the Emscripten program is running in. Compact implementation in a single header file.
Intended for use in Emscripten code, this enables the user to "upload" files to your program using their native file selector, and to "download" files from your program to save to disk, as if they were interacting with a remote website filesystem.
See also [tar_to_stream.h](https://github.com/Armchair-Software/tar_to_stream), to tarball multiple files in memory for a single download.
## Use cases:
* Implement an "upload" function, that enables users to choose a file using their browser's native file selector - this file is read directly into your program's memory.
* Candidate files can be filtered as with an "accept" attribute, identical to [`<input>` elements with `type="file"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file).
* Implement a "download" function, that allows you to create a "file" in memory, and offer it for "download" using the browser's native file-save dialogue.
* Filename and MIME type can be specified.
## Functionality
* `emscripten_browser_file::download()` - your program shares a section of memory, and the user receives it as a file they can save
* `emscripten_browser_file::upload()` - user selects a file on their filesystem, and your program receives the contents in memory
### Download
From the user's point of view, the `download` function acts as if the user has chosen to download a file from the web. In this case, you define a buffer referencing data in memory and specify a filename and MIME type, and the user's browser either shows a "save as" interface asking where the file should be saved, or saves it to their default save location, as per their browser preferences.
#### Example
```cpp
#include <emscripten_browser_file.h>
auto main()->int {
std::string filename{"hello_world.txt"};
std::string mime_type{"application/text/plain"};
std::string data{"Hello world!\n"};
emscripten_browser_file::download(filename, mime_type, data);
}
```
The download call takes the following arguments:
```cpp
emscripten_browser_file::download(
std::string const &filename, // the default filename for the browser to save. Note that browsers do not have to honour this, and may choose to mangle it
std::string const &mime_type, // the MIME type of the data, treated as if it were a webserver serving a file
std::string_view buffer // a buffer describing the data to download - can be any array of bytes, passed as a string_view
) {
```
`download` also has an override accepting `std::string` instead of `char const*`.
For files containing binary data, you will usually want to use the MIME type `application/octet-stream`.
### Upload
From the user's point of view, the `upload` function acts as if the user is uploading a file to a remote website. In this case, the file is loaded into a buffer in memory (referred to by a `std::string_view`) that is accessible to a C++ callback function you define.
#### Example
```cpp
#include <emscripten_browser_file.h>
void handle_upload_file(std::string const &filename, std::string const &mime_type, std::string_view buffer, void*) {
// define a handler to process the file
// ...
}
auto main()->int {
// open the browser's file selector, and pass the file to the upload handler
emscripten_browser_file::upload(".png,.jpg,.jpeg", handle_upload_file);
}
```
The upload call takes the following arguments:
```cpp
emscripten_browser_file::upload(
char const *accept_types, // an "accept" attribute, listing what file types can be accepted - see: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers
upload_handler callback, // a callback function to call with the received data
void *callback_data = nullptr, // optional pointer to pass to your callback function
);
```
`upload` also has an override accepting `std::string` instead of `char const*`.
The callback must have the following signature:
```cpp
void handle_upload_file(
std::string const &filename, // the filename of the file the user selected
std::string const &mime_type, // the MIME type of the file the user selected, for example "image/png"
std::string_view buffer, // the file's content is exposed in this string_view - access the data with buffer.data() and size with buffer.size().
void *callback_data = nullptr // optional callback data - identical to whatever you passed to handle_upload_file()
);
```
#### Using callback data
The callback can receive additional data through a void pointer passed to the `upload` function:
```cpp
#include <emscripten_browser_file.h>
#include <iostream>
void handle_upload_file(std::string const &filename, std::string const &mime_type, std::string_view buffer, void *callback_data) {
// define a handler to process the file
auto my_data{*reintrepret_cast<std::string*>(my_data)};
std::cout << "Received callback data: " << my_data << std::endl;
}
auto main()->int {
std::string my_data{"hello world"};
auto my_data_ptr{reintrepret_cast<void*>(&my_data)};
// pass callback data to the handler
emscripten_browser_file::upload(".png,.jpg,.jpeg", handle_upload_file, my_data_ptr);
}
```
You can use this to pass shared state, or any other data to the callback function - for example an instance of a class whose member function should be called to deal with the received data.
## Building
Necessary emsripten link flags:
- Building with emscripten will require you to pass, if you do not already do so, `-sEXPORTED_RUNTIME_METHODS=[ccall]` at the link stage.
- This uses dynamic memory allocation, so you need `-sALLOW_MEMORY_GROWTH=1` at the link stage.
- Depending on your optimisation settings, the compiler may remove JS `malloc` and `free` functions (this happens with `-O3` at the time of writing, see [emscripten issue 6882](https://github.com/emscripten-core/emscripten/issues/6882)). This can be avoided by explicitly exporting those functions: add `-sEXPORTED_FUNCTIONS=[_main,_malloc,_free]` at the link stage.
## Other useful libraries
You may also find the following Emscripten helper libraries useful:
- [Emscripten Browser Clipboard Library](https://github.com/Armchair-Software/emscripten-browser-clipboard) - easy handling of browser copy and paste events in your C++ code.
- [Emscripten Browser Cursor](https://github.com/Armchair-Software/emscripten-browser-cursor) - easy manipulation of browser mouse pointer cursors from C++.

View file

@ -0,0 +1,101 @@
#ifndef EMSCRIPTEN_UPLOAD_FILE_H_INCLUDED
#define EMSCRIPTEN_UPLOAD_FILE_H_INCLUDED
#include <string>
#include <emscripten.h>
#define _EM_JS_INLINE(ret, c_name, js_name, params, code) \
extern "C" { \
ret c_name params EM_IMPORT(js_name); \
EMSCRIPTEN_KEEPALIVE \
__attribute__((section("em_js"), aligned(1))) inline char __em_js__##js_name[] = \
#params "<::>" code; \
}
#define EM_JS_INLINE(ret, name, params, ...) _EM_JS_INLINE(ret, name, name, params, #__VA_ARGS__)
namespace emscripten_browser_file {
/////////////////////////////////// Interface //////////////////////////////////
using upload_handler = void(*)(std::string const&, std::string const&, std::string_view buffer, void*);
inline void upload(std::string const &accept_types, upload_handler callback, void *callback_data = nullptr);
inline void download(std::string const &filename, std::string const &mime_type, std::string_view buffer);
///////////////////////////////// Implementation ///////////////////////////////
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-variable-declarations"
EM_JS_INLINE(void, upload, (char const *accept_types, upload_handler callback, void *callback_data), {
/// Prompt the browser to open the file selector dialogue, and pass the file to the given handler
/// Accept-types are in the format ".png,.jpeg,.jpg" as per https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
/// Upload handler callback signature is:
/// void my_handler(std::string const &filename, std::string const &mime_type, std::string_view buffer, void *callback_data = nullptr);
globalThis["open_file"] = function(e) {
const file_reader = new FileReader();
file_reader.onload = (event) => {
const uint8Arr = new Uint8Array(event.target.result);
const num_bytes = uint8Arr.length * uint8Arr.BYTES_PER_ELEMENT;
const data_ptr = Module["_malloc"](num_bytes);
const data_on_heap = new Uint8Array(Module["HEAPU8"].buffer, data_ptr, num_bytes);
data_on_heap.set(uint8Arr);
Module["ccall"]('upload_file_return', 'number', ['string', 'string', 'number', 'number', 'number', 'number'], [event.target.filename, event.target.mime_type, data_on_heap.byteOffset, uint8Arr.length, callback, callback_data]);
Module["_free"](data_ptr);
};
file_reader.filename = e.target.files[0].name;
file_reader.mime_type = e.target.files[0].type;
file_reader.readAsArrayBuffer(e.target.files[0]);
};
var file_selector = document.createElement('input');
file_selector.setAttribute('type', 'file');
file_selector.setAttribute('onchange', 'globalThis["open_file"](event)');
file_selector.setAttribute('accept', UTF8ToString(accept_types));
file_selector.click();
});
#pragma GCC diagnostic pop
inline void upload(std::string const &accept_types, upload_handler callback, void *callback_data) {
/// C++ wrapper for javascript upload call
upload(accept_types.c_str(), callback, callback_data);
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-variable-declarations"
EM_JS_INLINE(void, download, (char const *filename, char const *mime_type, void const *buffer, size_t buffer_size), {
/// Offer a buffer in memory as a file to download, specifying download filename and mime type
var a = document.createElement('a');
a.download = UTF8ToString(filename);
var bufferCopy = new ArrayBuffer(buffer_size);
var uint8Array = new Uint8Array(bufferCopy);
uint8Array.set(new Uint8Array(Module["HEAPU8"].buffer, buffer, buffer_size));
a.href = URL.createObjectURL(new Blob([uint8Array], {type: UTF8ToString(mime_type)}));
a.click();
});
#pragma GCC diagnostic pop
inline void download(std::string const &filename, std::string const &mime_type, std::string_view buffer) {
/// C++ wrapper for javascript download call, accepting a string_view
download(filename.c_str(), mime_type.c_str(), buffer.data(), buffer.size());
}
namespace {
extern "C" {
EMSCRIPTEN_KEEPALIVE inline int upload_file_return(char const *filename, char const *mime_type, char *buffer, size_t buffer_size, upload_handler callback, void *callback_data);
EMSCRIPTEN_KEEPALIVE inline int upload_file_return(char const *filename, char const *mime_type, char *buffer, size_t buffer_size, upload_handler callback, void *callback_data) {
/// Load a file - this function is called from javascript when the file upload is activated
callback(filename, mime_type, {buffer, buffer_size}, callback_data);
return 1;
}
}
}
}
#endif // EMSCRIPTEN_UPLOAD_FILE_H_INCLUDED

388
src/main.cpp Normal file
View file

@ -0,0 +1,388 @@
#include "Artboard.h"
#include "Layer.h"
#include "Utils.h"
#include "import-font.h"
#include "ofFileUtils.h"
#include "ofMain.h"
#include "ofApp.h"
#include "ofUtils.h"
#include "ofxProfiler.h"
#include "ofWindowSettings.h"
#include <string>
//========================================================================
// MAIN APP
// shared_ptr to main app
// this is global, so we can call it in our binding functions
shared_ptr <VariableEditor::ofApp> app;
//========================================================================
// EMSCRIPTEN BINDING FUNCTIONS
// here we add functions to be called from javascript
#ifdef TARGET_EMSCRIPTEN
string addExistingLayer(ofxVariableLab::Layer::Props props,
string layerID){
if(props.fontPath == ""){ // TODO: cleaner handling
ofxVariableLab::Layer::Props p;
props.fontPath = p.fontPath;
}
return app->layerComposition.addLayer(props,
layerID);
}
string addNewLayer(){
ofxVariableLab::Layer::Props props;
return app->layerComposition.addLayer(props);
}
void removeLayer(string layerID){
app->layerComposition.removeLayer(layerID);
}
vector <string> debug_getLayers(){
vector <string> out;
auto & layersMap = app->layerComposition.getLayers();
for(const auto & [k, v] : layersMap){
out.push_back(k);
}
return out;
}
void debug_downloadFbo(string name){
if(name == "artboard"){
app->downloadFboAsImage("artboard" + ofGetTimestampString() + ".png", app->artboard.getFbo());
}else if(name == "fbo"){
app->downloadFboAsImage("fbo" + ofGetTimestampString() + ".png", app->fbo);
}
}
ofxVariableLab::Layer::Props getProps(string layerID){
return app->layerComposition.getLayer(layerID)->getProps();
}
bool setProps(ofxVariableLab::Layer::Props props,
string layerID){
props.letterDelay /= app->timeScale;
for(const auto & [k, v]: props.letterDelays){
props.letterDelays[k] /= app->timeScale;
}
app->layerComposition.getLayer(layerID)->setProps(props);
return true; // TODO: do not always return true maybe
}
VariableEditor::ArtboardProps getArtboardProps(){
return app->artboard.getProps();
}
bool setArtboardProps(VariableEditor::ArtboardProps props){
app->artboard.setProps(props);
return true; // TODO: do not always return true maybe
}
ofxVariableLab::Layer::BoundingBox getBoundingBox(string layerID){
return app->layerComposition.getLayer(layerID)->getBoundingBox();
}
void resetLetterDelay(){
for(const auto & l: app->layerComposition.getLayers()){
l.second->clearPropsBuffer();
}
}
void setLayerOrder(vector <string> _layerOrder){
vector <ofxVariableLab::LayerID> layerOrder;
for(int i = 0; i < _layerOrder.size(); i++){
ofxVariableLab::LayerID layer = _layerOrder[i];
layerOrder.push_back(layer);
}
app->layerComposition.setLayerOrder(_layerOrder);
}
void setPlaying(bool playing){
app->setPlaying(playing);
}
// TODO: rename this to exportRendering or sth similar
void setRendering(bool rendering){
ofDirectory exportDir(app->settings.tmpExportDir + "/frames");
if(!exportDir.exists()){
exportDir.create(true);
}
if(rendering){
app->currentFrame = 0;
app->recordedFrameNames.clear();
exportDir.listDir();
for(ofFile file : exportDir.getFiles()){
//file.removeFile(file.getAbsolutePath(), false);
file.remove();
}
double timelineLenth_seconds = EM_ASM_DOUBLE({
return window.tp.core.val(window.tp.sheet.sequence.pointer.length);
});
double fps = 30.0;
app->guessedFrameCount = std::ceil((fps * timelineLenth_seconds) / app->timeScale);
app->timelineLength_seconds = timelineLenth_seconds;
}
app->rendering = rendering;
}
bool getRendering(){
return app->rendering;
}
void renderNextFrame(){
app->renderNextFrame = true;
}
void setTimeScale(float timeScale){
app->timeScale = timeScale;
}
std::string importProjectAsZip(std::string filepath, std::string outdir){
ofDisableDataPath();
ofDirectory dir(outdir);
if(!dir.exists()){
dir.createDirectory(outdir);
}
VariableEditor::UnZip unzip(filepath, outdir);
dir.listDir();
ofDirectory userFonts("/idbfs/fonts");
nlohmann::json json;
json["files"] = nlohmann::json::array();
for(int i = 0; i < dir.size(); i++){
ofFile file = dir.getFile(i);
if(ofToLower(file.getAbsolutePath()).find("project.json") != std::string::npos){
try{
std::ifstream project(file.getAbsolutePath());
project >> json["project"];
}
catch(nlohmann::json::exception & e){
std::cerr << "Module::importProjectAsZip " << e.what() << std::endl;
}
}
if(ofToLower(file.getExtension()) == "ttf"
|| ofToLower(file.getExtension()) == "otf"){
if(!file.moveTo(userFonts)){
std::cout << "Module::importProjectAsZip" << " - cannot move font " << file.getFileName();
}
}else{
file.remove();
}
json["files"].push_back(dir.getPath(i));
}
EM_ASM({
FS.syncfs(false, function(err){
});
});
return json.dump();
}
void exportFramesAsZip(string projectName){
app->downloadFramesAsZip(projectName);
}
void downloadProject(string projectName,
string projectJsonString){
app->downloadProject(projectName,
projectJsonString);
}
std::vector <std::string> listAvailableFonts(){
vector <std::string> out;
ofEnableDataPath();
ofDirectory rootDir(".");
rootDir.listDir();
{
ofDirectory fontsDir("fonts");
fontsDir.sort();
fontsDir.allowExt("ttf");
fontsDir.allowExt("otf");
fontsDir.listDir();
for(int i = 0; i < fontsDir.size(); i++){
std::string fontPath = "data/" + fontsDir.getPath(i);
out.push_back(fontPath);
}
}
{
ofDirectory fontsDir("free-fonts");
fontsDir.sort();
fontsDir.allowExt("ttf");
fontsDir.allowExt("otf");
fontsDir.listDir();
for(int i = 0; i < fontsDir.size(); i++){
std::string fontPath = "data/" + fontsDir.getPath(i);
out.push_back(fontPath);
}
}
ofDirectory idbfsDir("/idbfs");
if(idbfsDir.exists()){
ofDirectory userFonts("/idbfs/fonts");
if(userFonts.exists()){
userFonts.sort();
userFonts.allowExt("ttf");
userFonts.allowExt("otf");
userFonts.listDir();
for(int i = 0; i < userFonts.size(); i++){
std::string fontPath = userFonts.getPath(i);
ofStringReplace(fontPath, "/idbfs/", "idbfs/");
out.push_back(fontPath);
}
}
}
return out;
}
std::vector <ofxVariableLab::FontVariationAxis> listVariationAxes(std::string fontPath){
std::vector <ofxVariableLab::FontVariationAxis> axes;
ofxVariableLab::listFontVariationAxes(fontPath, axes);
return axes;
}
void windowResized(int w, int h){
app->windowResized(w, h);
}
void setUniform1f(string name, float value){
cout << "VariableEditor::setUniform1f " << name << ": " << ofToString(value) << endl;
app->layerComposition.setUniform1fv({name}, {value});
}
void setUniform1i(string name, int value){
app->layerComposition.setUniform1iv({name}, {value});
}
void startProfiling(){
#if OFX_PROFILER
OFX_PROFILER_BEGIN_SESSION("profiling", "results.json");
#else
std::cout << "not compiled with internal profiling support" << endl;
std::cout << "make sure to include ofxProfiler and define OFX_PROFILER=1" << endl;
#endif
}
void endProfiling(){
#if OFX_PROFILER
OFX_PROFILER_END_SESSION();
#ifdef TARGET_EMSCRIPTEN
std::ifstream f("results.json");
nlohmann::json json = nlohmann::json::parse(f);
app->downloadJson("results.json", json);
#endif
#else
std::cout << "not compiled with internal profiling support" << endl;
std::cout << "make sure to include ofxProfiler and define OFX_PROFILER=1" << endl;
#endif
}
//========================================================================
// EMSCRIPTEN BINDING BINDINGS
// here we describe classes and functions to bind them to javascript
EMSCRIPTEN_BINDINGS(name_is_irrelevant){
emscripten::value_object <VariableEditor::ArtboardProps>("ArtboardProps")
.field("backgroundColor", &VariableEditor::ArtboardProps::backgroundColor)
.field("x", &VariableEditor::ArtboardProps::x)
.field("y", &VariableEditor::ArtboardProps::y)
.field("width", &VariableEditor::ArtboardProps::width)
.field("height", &VariableEditor::ArtboardProps::height)
.field("zoom", &VariableEditor::ArtboardProps::zoom)
.field("pixelDensity", &VariableEditor::ArtboardProps::pixelDensity)
;
emscripten::value_object <ofxVariableLab::Layer::Props>("Props")
.field("x", &ofxVariableLab::Layer::Props::x)
.field("y", &ofxVariableLab::Layer::Props::y)
.field("width", &ofxVariableLab::Layer::Props::width)
.field("height", &ofxVariableLab::Layer::Props::height)
.field("rotation", &ofxVariableLab::Layer::Props::rotation)
.field("fontSize_px", &ofxVariableLab::Layer::Props::fontSize_px)
.field("letterSpacing", &ofxVariableLab::Layer::Props::letterSpacing)
.field("lineHeight", &ofxVariableLab::Layer::Props::lineHeight)
.field("textAlignment", &ofxVariableLab::Layer::Props::textAlignment)
.field("color", &ofxVariableLab::Layer::Props::color)
.field("mirror_x", &ofxVariableLab::Layer::Props::mirror_x)
.field("mirror_x_distance", &ofxVariableLab::Layer::Props::mirror_x_distance)
.field("mirror_y", &ofxVariableLab::Layer::Props::mirror_y)
.field("mirror_y_distance", &ofxVariableLab::Layer::Props::mirror_y_distance)
.field("mirror_xy", &ofxVariableLab::Layer::Props::mirror_xy)
.field("letterDelay", &ofxVariableLab::Layer::Props::letterDelay)
.field("transformOrigin", &ofxVariableLab::Layer::Props::transformOrigin)
.field("text", &ofxVariableLab::Layer::Props::text)
.field("fontPath", &ofxVariableLab::Layer::Props::fontPath)
.field("fontVariations", &ofxVariableLab::Layer::Props::fontVariations)
.field("letterDelays", &ofxVariableLab::Layer::Props::letterDelays)
;
emscripten::value_object <ofxVariableLab::FontVariationAxis>("FontVariationAxis")
.field("name", &ofxVariableLab::FontVariationAxis::name)
.field("minValue", &ofxVariableLab::FontVariationAxis::minValue)
.field("maxValue", &ofxVariableLab::FontVariationAxis::maxValue)
.field("defaultValue", &ofxVariableLab::FontVariationAxis::defaultValue)
;
emscripten::value_object <ofxVariableLab::Layer::BoundingBox>("BoundingBox")
.field("x", &ofxVariableLab::Layer::BoundingBox::x)
.field("y", &ofxVariableLab::Layer::BoundingBox::y)
.field("w", &ofxVariableLab::Layer::BoundingBox::w)
.field("h", &ofxVariableLab::Layer::BoundingBox::h)
;
// Register std::array<float, 4> because Props::color is interpreted as such
emscripten::value_array <std::array <float, 4> >("array_float_4")
.element(emscripten::index <0>())
.element(emscripten::index <1>())
.element(emscripten::index <2>())
.element(emscripten::index <3>())
;
emscripten::class_ <ofxVariableLab::FontVariation>("FontVariation")
.constructor <>()
.property("name", &ofxVariableLab::FontVariation::name)
.property("value", &ofxVariableLab::FontVariation::value)
;
emscripten::register_vector <ofxVariableLab::FontVariationAxis>("FontVariationAxes");
emscripten::register_vector <ofxVariableLab::FontVariation>("FontVariations");
emscripten::register_vector <std::string>("vector<string>");
emscripten::register_map <std::string, float>("map<string, float>");
emscripten::function("addExistingLayer", &addExistingLayer);
emscripten::function("addNewLayer", &addNewLayer);
emscripten::function("removeLayer", &removeLayer);
emscripten::function("getProps", &getProps);
emscripten::function("setProps", &setProps);
//emscripten::function("listAvailableFontsAndAxes", &listAvailableFontsAndAxes);
emscripten::function("listAvailableFonts", &listAvailableFonts);
emscripten::function("listVariationAxes", &listVariationAxes);
emscripten::function("getArtboardProps", &getArtboardProps);
emscripten::function("setArtboardProps", &setArtboardProps);
emscripten::function("getBoundingBox", &getBoundingBox);
emscripten::function("resetLetterDelay", &resetLetterDelay);
emscripten::function("setLayerOrder", &setLayerOrder);
emscripten::function("setPlaying", &setPlaying);
emscripten::function("setRendering", &setRendering);
emscripten::function("getRendering", &getRendering);
emscripten::function("setTimeScale", &setTimeScale);
emscripten::function("windowResized", &windowResized);
emscripten::function("exportFramesAsZip", &exportFramesAsZip);
emscripten::function("importProjectAsZip", &importProjectAsZip, emscripten::allow_raw_pointers());
emscripten::function("downloadProject", &downloadProject);
// debug
emscripten::function("setUniform1f", &setUniform1f);
emscripten::function("setUniform1i", &setUniform1i);
emscripten::function("startProfiling", &startProfiling);
emscripten::function("endProfiling", &endProfiling);
emscripten::function("debug_getLayers", &debug_getLayers);
emscripten::function("debug_downloadFbo", &debug_downloadFbo);
}
#endif
//========================================================================
// MAIN
// well, we have to start somewhere
int main(){
#ifdef OF_TARGET_OPENGLES
ofGLESWindowSettings settings;
//settings.setSize(1920, 1080);
settings.numSamples = 4;
settings.glesVersion = 3;
#else
ofGLWindowSettings settings;
settings.windowMode = OF_WINDOW;
settings.setSize(1920, 1080);
settings.setPosition(glm::vec2(1920, 0));
settings.setGLVersion(3, 2);
#endif
ofCreateWindow(settings);
app = make_shared <VariableEditor::ofApp>();
// this kicks off the running of my app
// can be OF_WINDOW or OF_FULLSCREEN
// pass in width and height too:
ofRunApp(app);
}

506
src/ofApp.cpp Normal file
View file

@ -0,0 +1,506 @@
#include "ofApp.h"
#include "Layer.h"
#include "LayerComposition.h"
#include "MsdfLayer.h"
#include "conversion.h"
#include "import-font.h"
#include "of3dUtils.h"
#include "ofAppRunner.h"
#include "ofColor.h"
#include "ofEvents.h"
#include "ofFileUtils.h"
#include "ofGraphics.h"
#include "ofGraphicsBaseTypes.h"
#include "ofGraphicsConstants.h"
#include "ofTexture.h"
#include "ofUtils.h"
#include "ofWindowSettings.h"
#include "ofxProfiler.h"
#include <GL/glext.h>
#include <filesystem>
#ifdef TARGET_EMSCRIPTEN
#include <emscripten/em_asm.h>
#include <GLES/gl.h>
#include <GLES3/gl3.h>
#endif
#include <memory>
static int AA = 1;
namespace VariableEditor {
//--------------------------------------------------------------
void ofApp::setup(){
OFX_PROFILER_FUNCTION();
EM_ASM({window.setLoadingTask('setting up rendering', 0)});
{
ofFile sf("appSettings.json");
if(sf.exists()){
ofJson json = ofLoadJson("appSettings.json");
settings.from_json(json, settings);
}
}
ofSetFrameRate(30);
ofSetVerticalSync(false);
cam.setVFlip(true); // otherwise everything is weird
cam.setPosition(ofGetWidth() / 2, ofGetHeight() / 2, 935.335);
//cam.setPosition(0, 0, -1000);
cam.lookAt(glm::vec3(cam.getPosition().x, cam.getPosition().y, 0),
glm::vec3(0, 1, 0));
//cam.disableMouseInput();
cam.enableOrtho();
observer.setupPerspective(true, // vflip
60, // fov
0, // nearDist
10000000, // farDist
glm::vec2(0, 0) // lensOffset
);
EM_ASM({window.setLoadingTask('setting up rendering', 10)});
ofDisableArbTex();
fboSettings.width = ofGetWidth() * AA;
fboSettings.height = ofGetHeight() * AA;
fboSettings.numSamples = 0;
fboSettings.internalformat = GL_RGBA;
fboSettings.minFilter = GL_LINEAR_MIPMAP_LINEAR;
fboSettings.maxFilter = GL_LINEAR;
//fboSettings.textureTarget = GL_TEXTURE_2D_MULTISAMPLE;
fbo.allocate(fboSettings);
fbo.begin();
ofClear(ofColor(settings.backgroundColor[0],
settings.backgroundColor[1],
settings.backgroundColor[2],
settings.backgroundColor[3]));
fbo.end();
//fbo.allocate(ofGetWidth() * AA, ofGetHeight() * AA, GL_RGB);
EM_ASM({window.setLoadingTask('setting up rendering', 30)});
layerComposition.setup();
EM_ASM({window.setLoadingTask('setting up rendering', 90)});
layerComposition.setVFlip(true);
#ifndef TARGET_OPENGLES
{
std::string fontPath = "data/fonts/Version-2-var.ttf";
ofxVariableLab::LayerType type = ofxVariableLab::LayerType::GPUFONT;
ofxVariableLab::Layer::Props props;
props.fontPath = fontPath;
props.text = "yo, whatever you want, and especially pancakes";
props.y = 120;
props.x = 95;
layerComposition.addLayer(
{fontPath, type},
props,
{{"Weight", 100.0}, {"Weight", 700.0}}
);
}
#endif
image42.load("42px-01.png");
image420.load("420px-01.png");
int maxSamples;
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
cout << "MAX_SAMPLES: " << ofToString(maxSamples) << endl;
int maxTextureSize;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);
cout << "MAX_TEXTURE_SIZE: " << ofToString(maxTextureSize) << endl;
if(maxTextureSize < 16384){
#ifdef TARGET_EMSCRIPTEN
// yeaaaaahh, well this is mostly for firefox on a mac
EM_ASM({
alert('Your browser / operating system only allows quite small textures, which can lead to glitches.\n\nWe recommend:\nMac OS + Chrome\nWindows + Chrome\nLinux + Chrome / Firefox\n\nPS: If you do not want to use Chrome, Brave is a good Chrome-based browser.');
});
#endif
}
const unsigned char * glVersion = glGetString(GL_VERSION);
int glMajor, glMinor;
glGetIntegerv(GL_MAJOR_VERSION, &glMajor);
glGetIntegerv(GL_MINOR_VERSION, &glMinor);
cout << "GL_VERSION: " << glVersion
<< " | Major(" << ofToString(glMajor) << ")"
<< " | Minor(" << ofToString(glMajor) << ")"
<< endl;
artboard.setup();
EM_ASM({window.setLoadingTask('setting up rendering', 100)});
}
//--------------------------------------------------------------
void ofApp::update(){
OFX_PROFILER_FUNCTION();
if(!rendering || renderNextFrame){
layerComposition.update();
}
zipSaver.update();
projectZipSaver.update();
}
//--------------------------------------------------------------
void ofApp::draw(){
OFX_PROFILER_FUNCTION();
if(!rendering || renderNextFrame){
ofCamera & camera = observing ? observer : cam;
artboard.begin();
camera.begin();
ofPushMatrix();
layerComposition.draw(artboard.getShape().x,
artboard.getShape().y);
ofPopMatrix();
camera.end();
artboard.end();
fbo.begin();
ofFloatColor bg(settings.backgroundColor[0],
settings.backgroundColor[1],
settings.backgroundColor[2],
settings.backgroundColor[3]);
ofClear(bg);
artboard.draw();
fbo.end();
ofDisableAlphaBlending();
ofDisableDepthTest();
fbo.getTexture().generateMipmap();
fbo.draw(0, 0, ofGetWidth(), ofGetHeight());
ofPushStyle();
ofDrawBitmapStringHighlight("fps: " + ofToString(ofGetFrameRate()), 20, ofGetHeight() - 120);
//ofDrawBitmapStringHighlight("ortho: " + ofToString(cam.getOrtho()), 20, ofGetHeight() - 200);
ofPopStyle();
ofEnableDepthTest();
}
if(rendering){
//if(currentFrame < guessedFrameCount){
int n_zero = log10(guessedFrameCount) + 1;
string frame_number_padded = std::string(n_zero, 'x');
if(currentFrame > 0){
string frame_number = ofToString(currentFrame - 1);
frame_number_padded = std::string(n_zero - std::min(n_zero, (int)frame_number.length()), '0') + frame_number;
saveFrame(frame_number_padded, artboard.getFbo());
recordedFrameNames.push_back(frame_number_padded);
}
currentFrame++;
#ifdef TARGET_EMSCRIPTEN
int percent = (theatrePosition / timelineLength_seconds) * 100;
EM_ASM_INT({
let percent = $0;
document.getElementById('export_progress_task').innerHTML = 'rendering';
let innerHTML = "|";
const niceText = "rendering is the process of generating an image by means of a computer program. ";
for(let i = 0; i < 100; i++){
if(i < percent){
innerHTML += niceText[i % niceText.length];
}else{
innerHTML += "-";
}
}
innerHTML += "|";
let progress = document.getElementById("export_progress");
progress.innerHTML = innerHTML;
}, percent);
double theatrePostPosition = EM_ASM_DOUBLE({
window.tp.sheet.sequence.position = $0;
return window.tp.sheet.sequence.position;
}, theatrePosition);
if(theatrePostPosition >= timelineLength_seconds
|| theatrePosition > theatrePostPosition){
cout << std::fixed
<< "------------------------- "
<< "frame: " << frame_number_padded << endl
<< "theatrePosition: " << ofToString(theatrePosition) << std::endl
<< "timelineLEngth|_second: " << ofToString(timelineLength_seconds) << std::endl
<< "theatrePostPosition: " << ofToString(theatrePostPosition) << std::endl
;
EM_ASM({
window.renderDone();
});
theatrePosition = 0;
renderNextFrame = 0;
rendering = false;
}else{
theatrePosition += timeScale / 30.0f;
renderNextFrame = true;
}
#endif
}
//}
}
void ofApp::drawGrid(){
}
void ofApp::setPlaying(bool playing){
this->playing = playing;
}
//--------------------------------------------------------------
void ofApp::keyPressed(int key){
//cout << "pressed " << (char)key << "(" << ofToString(key) << ")" << endl;
if(inputPressed.count(ofKey(key)) == 0){
inputPressed.insert(ofKey(key));
}
return;
if(key == 's'){
#ifdef TARGET_EMSCRIPTEN
//atlasImage->save("web_atlasImage.png");
for(const auto & a : layerComposition.getAtlasLayerCombos()){
if(a.second->getIdentifier().type == ofxVariableLab::LayerType::MSDFGEN){
auto combo =
static_pointer_cast <ofxVariableLab::MsdfAtlasLayerCombo>(a.second);
string imageName = combo->getAtlasImagePath();
ofStringReplace(imageName, "/", "_");
ofStringReplace(imageName, "data_atlascache_", "");
downloadImage(imageName, combo->getAtlasImage());
}else if(a.second->getIdentifier().type == ofxVariableLab::LayerType::GPUFONT){
auto combo =
static_pointer_cast <ofxVariableLab::GPUFontAtlasLayerCombo>(a.second);
}
}
#else
for(const auto & a : layerComposition.getAtlasLayerCombos()){
if(a.second->getIdentifier().type == ofxVariableLab::LayerType::MSDFGEN){
auto combo =
static_pointer_cast <ofxVariableLab::MsdfAtlasLayerCombo>(a.second);
string imageName = combo->getAtlasImagePath();
ofStringReplace(imageName, "/", "_");
ofStringReplace(imageName, "data_atlascache_", "");
combo->getAtlasImage().save(imageName);
}else if(a.second->getIdentifier().type == ofxVariableLab::LayerType::GPUFONT){
auto combo =
static_pointer_cast <ofxVariableLab::GPUFontAtlasLayerCombo>(a.second);
}
}
#endif
}
if(key == 'a'){
#ifdef TARGET_EMSCRIPTEN
downloadFboAsImage("web_fboImage.png", fbo);
//atlasImage->save("web_atlasImage.png");
#else
ofPixels pixels;
fbo.readToPixels(pixels);
ofSaveImage(pixels, "linux64_fboImage.png");
#endif
}
if(key == 'o'){
if(cam.getOrtho()){
cout << "diable ortho" << endl;
cam.disableOrtho();
}else{
cout << "eable ortho" << endl;
cam.enableOrtho();
}
}
if(key == 'p'){
observing = !observing;
}
if(key == 'c'){
cout << "observer to cam" << endl;
observer.setGlobalOrientation(cam.getGlobalOrientation());
observer.setGlobalPosition(cam.getGlobalPosition());
observer.setScale(cam.getGlobalScale());
observer.setFov(cam.getFov());
}
}
//--------------------------------------------------------------
void ofApp::keyReleased(int key){
//cout << "released " << (char)key << "(" << ofToString(key) << ")" << endl;
if(inputPressed.count(ofKey(key)) > 0){
inputPressed.erase(ofKey(key));
}
#ifdef OFX_PROFILER
//if(key == 'o'){
//OFX_PROFILER_BEGIN_SESSION("profiling", "results.json");
//}
//if(key == 'p'){
//OFX_PROFILER_END_SESSION();
//#ifdef TARGET_EMSCRIPTEN
//std::ifstream f("results.json");
//nlohmann::json json = nlohmann::json::parse(f);
//downloadJson("results.json", json);
//#endif
//cout << json.dump() << endl;
//}
//if(key == 'r'){
//rendering = !rendering;
//}
#endif
}
//--------------------------------------------------------------
void ofApp::mouseMoved(int x, int y){
//cout << "mouse moved" << endl;
//if(inputPressed.count(OF_MOUSE_BUTTON_MIDDLE) > 0
//|| (inputPressed.count(OF_MOUSE_BUTTON_LEFT) > 0 && inputPressed.count(OF_KEY_CONTROL) > 0)){
//artboard.setPosition(x, y, 0);
//}
}
//--------------------------------------------------------------
void ofApp::mouseDragged(int x, int y, int button){
}
//--------------------------------------------------------------
void ofApp::mousePressed(int x, int y, int button){
//cout << "mouse pressed " << button << endl;
if(inputPressed.count(ofKey(button)) == 0){
inputPressed.insert(ofKey(button));
}
}
//--------------------------------------------------------------
void ofApp::mouseReleased(int x, int y, int button){
//cout << "mouse released " << button << endl;
if(inputPressed.count(ofKey(button)) == 0){
inputPressed.erase(ofKey(button));
}
}
//--------------------------------------------------------------
void ofApp::mouseEntered(int x, int y){
//cout << "mouse entered" << endl;
}
//--------------------------------------------------------------
void ofApp::mouseExited(int x, int y){
//cout << "mouse exited" << endl;
}
void ofApp::mouseScrolled(ofMouseEventArgs & mouse){
this->mouseScrolled(mouse.x, mouse.y, mouse.scrollX, mouse.scrollY);
}
void ofApp::mouseScrolled(int x, int y, float scrollX, float scrollY){
//cout << "scroll "
//<< "x: " << ofToString(x) << " "
//<< "y: " << ofToString(y) << " "
//<< "scrollX: " << ofToString(scrollX) << " "
//<< "scrollY: " << ofToString(scrollY) << " "
//;
}
//--------------------------------------------------------------
void ofApp::windowResized(int w, int h){
cout << "window resized " << ofToString(w) << "x" << ofToString(h) << endl;
ofSetWindowShape(w, h);
fboSettings.width = w * AA;
fboSettings.height = h * AA;
fboSettings.numSamples = 0;
fboSettings.internalformat = GL_RGBA;
fboSettings.minFilter = GL_LINEAR_MIPMAP_LINEAR;
fboSettings.maxFilter = GL_LINEAR;
//fboSettings.textureTarget = GL_TEXTURE_2D_MULTISAMPLE;
ofFbo newFbo;
newFbo.allocate(fboSettings);
newFbo.begin();
ofClear(ofColor(settings.backgroundColor[0],
settings.backgroundColor[1],
settings.backgroundColor[2],
settings.backgroundColor[3]));
newFbo.end();
fbo = std::move(newFbo);
}
//--------------------------------------------------------------
void ofApp::gotMessage(ofMessage msg){
}
//--------------------------------------------------------------
void ofApp::dragEvent(ofDragInfo dragInfo){
}
void ofApp::saveFrame(const string & filename,
const ofFbo & _fbo){
ofImage image;
image.setUseTexture(false);
image.allocate(_fbo.getWidth(),
_fbo.getHeight(),
OF_IMAGE_COLOR_ALPHA);
_fbo.readToPixels(image.getPixels());
image.save(settings.tmpExportDir + "/frames/" + filename + ".png");
}
#ifdef TARGET_EMSCRIPTEN
void ofApp::downloadImage(const string & filename,
const ofImage & image){
downloadPixelsAsImage(filename, image.getPixels());
}
void ofApp::downloadFramesAsZip(const string & projectName){
if(!zipSaver.isThreadRunning()){
cout << "ofApp::downloadFramesAsZip" << endl;
zipSaver.setup(projectName,
settings.tmpExportDir + "/frames",
recordedFrameNames
);
zipSaver.startThread();
}
}
void ofApp::downloadFboAsImage(const string & filename,
const ofFbo & _fbo){
ofPixels pixels;
_fbo.readToPixels(pixels);
downloadPixelsAsImage(filename, pixels);
}
void ofApp::downloadFrame(const string & filename){
cout << "downloadFrame " << filename << endl;
ofImage image;
image.load(settings.tmpExportDir + "/frames/" + filename + ".png");
downloadImage(filename,
image);
}
void ofApp::downloadPixelsAsImage(const string & filename,
const ofPixels & pixels){
ofBuffer buffer;
ofSaveImage(pixels, buffer, OF_IMAGE_FORMAT_PNG);
emscripten_browser_file::download(filename.c_str(),
"image/png",
buffer.getData(),
buffer.size());
}
void ofApp::downloadJson(const string & filename,
const nlohmann::json & json){
string jsonString = json.dump();
emscripten_browser_file::download(filename.c_str(),
"application/json",
jsonString.data(),
jsonString.size());
}
void ofApp::downloadProject(const string & projectName,
const string & projectJsonString){
if(!projectZipSaver.isThreadRunning()){
projectZipSaver.setup(projectName,
projectJsonString);
projectZipSaver.startThread();
}
}
#endif
void ofApp::exit(){
zipSaver.stopThread();
}
}

343
src/ofApp.h Normal file
View file

@ -0,0 +1,343 @@
#pragma once
#include "Zip.h"
#include "Atlas.h"
#include "conversion.h"
#include "Artboard.h"
#include "ofEasyCam.h"
#include "ofMain.h"
#include "ofQuaternion.h"
#include "ofTrueTypeFont.h"
#include "ofxVariableLab.h"
#include "ofxProfiler.h"
#include <unordered_map>
#ifdef TARGET_EMSCRIPTEN
#include <emscripten/em_macros.h>
#include <emscripten/bind.h>
#include <emscripten.h>
#include <emscripten-browser-file/emscripten_browser_file.h>
#endif
using namespace msdfgen;
// TODO: better antialias
// possibly just draw a bigger fbo?
// or do it properly:
// https://github.com/emscripten-core/emscripten/issues/7898
//
// TODO: fix linux build
namespace VariableEditor {
struct AppSettings {
std::array <float, 4> backgroundColor = {212 / 255.0,
212 / 255.0,
212 / 255.0,
1}; // check data/appSettings.json
string tmpExportDir = "data/export";
string tmpImportDir = "data/import";
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AppSettings,
backgroundColor,
tmpExportDir);
};
class ZipProjectSaver : public ofThread {
public:
void setup(string projectName,
string projectJsonString){
this->projectName = projectName;
this->projectJsonString = projectJsonString;
this->userFontsPath = "/idbfs/fonts";
this->timestamp = ofGetTimestampString();
this->filename = projectName + "_project_" + timestamp + ".zip";
}
void update(){
if(freshDownload.load()){
emscripten_browser_file::download(filename.c_str(),
"application/zip",
buffer,
buffer_size);
freshDownload.store(false);
}
}
void threadedFunction(){
Zip zip(filename.c_str());
ofDisableDataPath();
{
char buffy[projectJsonString.length()];
projectJsonString.copy(buffy, projectJsonString.length());
zip.addBuffer("project.json", buffy, projectJsonString.length());
}
ofDirectory userFonts(userFontsPath);
userFonts.sort();
userFonts.allowExt("ttf");
userFonts.allowExt("otf");
userFonts.listDir();
for(int i = 0; i < userFonts.size(); i++){
std::string fontFilename = userFonts.getName(i);
std::string fontFilepath = userFontsPath + "/" + fontFilename;
ofFile file = userFonts.getFile(i);
ofStringReplace(fontFilepath, "/idbfs/", "idbfs/");
if(of::filesystem::exists(fontFilepath)){
//cout << "huuurrayy " << fontFilepath << " exists" << endl;
}else{
cout << "ofApp::downloadProject() trying to load " << fontFilepath << " but it does not exist." << endl;
}
file.open(fontFilepath);
ofBuffer buffy = file.readToBuffer();
zip.addBuffer(fontFilename, buffy.getData(), buffy.size());
}
buffer = NULL;
buffer_size = 0;
zip.getOutputBuffer(&buffer, buffer_size);
zip.close();
ofEnableDataPath();
freshDownload.store(true);
}
void exit(){
free(buffer);
}
string projectName;
string projectJsonString;
string userFontsPath;
string timestamp;
string filename;
char * buffer;
size_t buffer_size;
std::atomic <bool> freshDownload{false};
std::atomic <int> percent{0};
};
class ZipSaver : public ofThread {
public:
void setup(string projectName,
string framePath,
vector <string> recordedFrameNames){
this->projectName = projectName;
this->framePath = framePath;
this->recordedFrameNames = recordedFrameNames;
this->timestamp = ofGetTimestampString();
this->filename = projectName + "_frames_" + timestamp + ".zip";
}
void update(){
if(freshDownload.load()){
EM_ASM({
document.getElementById('export_progress_task').innerHTML = 'rendering';
let innerHTML = "|";
for(let i = 0; i < 100; i++){
innerHTML += "-";
}
innerHTML += "|";
let progress = document.getElementById("export_progress");
progress.innerHTML = innerHTML;
let progress_task = document.getElementById("export_progress_task");
progress_task.innerHTML = "idle";
});
emscripten_browser_file::download(filename.c_str(),
"application/zip",
buffer,
buffer_size);
freshDownload.store(false);
}else if(isThreadRunning()){
setProgress(percent.load());
}
}
void setProgress(int percent){
EM_ASM_INT({
let percent = $0;
document.getElementById('export_progress_task').innerHTML = 'rendering';
let innerHTML = "|";
const niceText = "zip ";
for(let i = 0; i < 100; i++){
if(i < percent){
innerHTML += niceText[i % niceText.length];
}else{
innerHTML += "-";
}
}
innerHTML += "|";
let progress = document.getElementById("export_progress");
progress.innerHTML = innerHTML;
let progress_task = document.getElementById("export_progress_task");
progress_task.innerHTML = "creating zip file";
}, percent);
}
void threadedFunction(){
Zip zip(filename.c_str());
ofDisableDataPath();
int total = recordedFrameNames.size();
int i = 0;
for(const std::string & f: recordedFrameNames){
std::string filepath = framePath + "/" + f + ".png";
if(of::filesystem::exists(filepath)){
//cout << "huuurrayy " << filepath << " exists" << endl;
}else{
cout << "ofApp::downloadFramesAsZip() trying to load " << filepath << " but it does not exist." << endl;
}
ofImage image;
image.setUseTexture(false);
image.load(filepath);
ofBuffer buffer;
ofSaveImage(image.getPixels(), buffer, OF_IMAGE_FORMAT_PNG);
zip.addBuffer(f + ".png", buffer.getData(), buffer.size());
percent.store((float(i) / float(total)) * 100.0f);
i++;
}
buffer = NULL;
buffer_size = 0;
zip.getOutputBuffer(&buffer, buffer_size);
zip.close();
ofEnableDataPath();
freshDownload.store(true);
}
void exit(){
free(buffer);
}
string projectName;
string framePath;
string timestamp;
string filename;
std::vector <string> recordedFrameNames;
char * buffer;
size_t buffer_size;
std::atomic <bool> freshDownload{false};
std::atomic <int> percent{0};
};
class ZipUnpacker : public ofThread {
public:
void setup(){
this->timestamp = ofGetTimestampString();
}
void update(){
if(freshUpload.load()){
freshUpload.store(false);
}else if(isThreadRunning()){
//setProgress(percent.load());
}
}
void setProgress(int percent){
EM_ASM_INT({
let percent = $0;
document.getElementById('export_progress_task').innerHTML = 'uploading and unpacking';
let innerHTML = "|";
const niceText = "zip ";
for(let i = 0; i < 100; i++){
if(i < percent){
innerHTML += niceText[i % niceText.length];
}else{
innerHTML += "-";
}
}
innerHTML += "|";
let progress = document.getElementById("import_progress");
progress.innerHTML = innerHTML;
let progress_task = document.getElementById("import_progress_task");
progress_task.innerHTML = "creating zip file";
}, percent);
}
void threadedFunction(){
ofDisableDataPath();
}
void exit(){
free(buffer);
}
string timestamp;
char * buffer;
size_t buffer_size;
std::atomic <bool> freshUpload{false};
std::atomic <int> percent{0};
};
class ofApp : public ofBaseApp {
public:
void setup() override;
void update() override;
void draw() override;
void drawGrid();
void setPlaying(bool playing);
void keyPressed(int key) override;
void keyReleased(int key) override;
void mouseMoved(int x, int y) override;
void mouseDragged(int x, int y, int button) override;
void mousePressed(int x, int y, int button) override;
void mouseReleased(int x, int y, int button) override;
void mouseEntered(int x, int y) override;
void mouseExited(int x, int y) override;
void mouseScrolled(ofMouseEventArgs & mouse) override;
void mouseScrolled(int x, int y, float scrollX, float scrollY) override;
void windowResized(int w, int h) override;
void dragEvent(ofDragInfo dragInfo) override;
void gotMessage(ofMessage msg) override;
void saveFrame(const string & filename,
const ofFbo & _fbo);
#ifdef TARGET_EMSCRIPTEN
void downloadFrame(const string & filename);
void downloadFramesAsZip(const string & projectName = "project");
static void downloadImage(const string & filename,
const ofImage & image);
static void downloadFboAsImage(const string & filename,
const ofFbo & _fbo);
static void downloadPixelsAsImage(const string & filename,
const ofPixels & image);
static void downloadJson(const string & filename,
const nlohmann::json & json);
void downloadProject(const string & projectName,
const string & projectJsonString);
#endif
void exit() override;
ofxVariableLab::LayerComposition layerComposition;
ofCamera cam;
ofFboSettings fboSettings;
ofFbo fbo;
ofEasyCam observer;
bool observing = false;
bool playing = true;
bool rendering = false;
bool renderNextFrame = false;
int guessedFrameCount = 30;
double timelineLength_seconds = 0;
int currentFrame = 0;
double theatrePosition = 0;
double timeScale = 1.0;
ofImage image42;
ofImage image420;
AppSettings settings;
ofTrueTypeFont ttf;
Artboard artboard;
std::set <ofKey> inputPressed;
// EXPORTER
std::vector <std::string> recordedFrameNames;
ZipSaver zipSaver;
ZipProjectSaver projectZipSaver;
};
}

10130
src/zip/miniz.h Normal file

File diff suppressed because it is too large Load diff

1793
src/zip/zip.c Normal file

File diff suppressed because it is too large Load diff

466
src/zip/zip.h Normal file
View file

@ -0,0 +1,466 @@
/*
* 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 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.
*
* source: https://github.com/kuba--/zip
*/
#pragma once
#ifndef ZIP_H
#define ZIP_H
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#ifndef ZIP_SHARED
#define ZIP_EXPORT
#else
#ifdef _WIN32
#ifdef ZIP_BUILD_SHARED
#define ZIP_EXPORT __declspec(dllexport)
#else
#define ZIP_EXPORT __declspec(dllimport)
#endif
#else
#define ZIP_EXPORT __attribute__((visibility("default")))
#endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
#if !defined(_POSIX_C_SOURCE) && defined(_MSC_VER)
// 64-bit Windows is the only mainstream platform
// where sizeof(long) != sizeof(void*)
#ifdef _WIN64
typedef long long ssize_t; /* byte count or error */
#else
typedef long ssize_t; /* byte count or error */
#endif
#endif
/**
* @mainpage
*
* Documenation for @ref zip.
*/
/**
* @addtogroup zip
* @{
*/
/**
* Default zip compression level.
*/
#define ZIP_DEFAULT_COMPRESSION_LEVEL 6
/**
* Error codes
*/
#define ZIP_ENOINIT -1 // not initialized
#define ZIP_EINVENTNAME -2 // invalid entry name
#define ZIP_ENOENT -3 // entry not found
#define ZIP_EINVMODE -4 // invalid zip mode
#define ZIP_EINVLVL -5 // invalid compression level
#define ZIP_ENOSUP64 -6 // no zip 64 support
#define ZIP_EMEMSET -7 // memset error
#define ZIP_EWRTENT -8 // cannot write data to entry
#define ZIP_ETDEFLINIT -9 // cannot initialize tdefl compressor
#define ZIP_EINVIDX -10 // invalid index
#define ZIP_ENOHDR -11 // header not found
#define ZIP_ETDEFLBUF -12 // cannot flush tdefl buffer
#define ZIP_ECRTHDR -13 // cannot create entry header
#define ZIP_EWRTHDR -14 // cannot write entry header
#define ZIP_EWRTDIR -15 // cannot write to central dir
#define ZIP_EOPNFILE -16 // cannot open file
#define ZIP_EINVENTTYPE -17 // invalid entry type
#define ZIP_EMEMNOALLOC -18 // extracting data using no memory allocation
#define ZIP_ENOFILE -19 // file not found
#define ZIP_ENOPERM -20 // no permission
#define ZIP_EOOMEM -21 // out of memory
#define ZIP_EINVZIPNAME -22 // invalid zip archive name
#define ZIP_EMKDIR -23 // make dir error
#define ZIP_ESYMLINK -24 // symlink error
#define ZIP_ECLSZIP -25 // close archive error
#define ZIP_ECAPSIZE -26 // capacity size too small
#define ZIP_EFSEEK -27 // fseek error
#define ZIP_EFREAD -28 // fread error
#define ZIP_EFWRITE -29 // fwrite error
/**
* Looks up the error message string coresponding to an error number.
* @param errnum error number
* @return error message string coresponding to errnum or NULL if error is not
* found.
*/
extern ZIP_EXPORT const char *zip_strerror(int errnum);
/**
* @struct zip_t
*
* This data structure is used throughout the library to represent zip archive -
* forward declaration.
*/
struct zip_t;
/**
* Opens zip archive with compression level using the given mode.
*
* @param zipname zip archive file name.
* @param level compression level (0-9 are the standard zlib-style levels).
* @param mode file access mode.
* - 'r': opens a file for reading/extracting (the file must exists).
* - 'w': creates an empty file for writing.
* - 'a': appends to an existing archive.
*
* @return the zip archive handler or NULL on error
*/
extern ZIP_EXPORT struct zip_t *zip_open(const char *zipname, int level,
char mode);
/**
* Closes the zip archive, releases resources - always finalize.
*
* @param zip zip archive handler.
*/
extern ZIP_EXPORT void zip_close(struct zip_t *zip);
/**
* Determines if the archive has a zip64 end of central directory headers.
*
* @param zip zip archive handler.
*
* @return the return code - 1 (true), 0 (false), negative number (< 0) on
* error.
*/
extern ZIP_EXPORT int zip_is64(struct zip_t *zip);
/**
* Opens an entry by name in the zip archive.
*
* For zip archive opened in 'w' or 'a' mode the function will append
* a new entry. In readonly mode the function tries to locate the entry
* in global dictionary.
*
* @param zip zip archive handler.
* @param entryname an entry name in local dictionary.
*
* @return the return code - 0 on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT int zip_entry_open(struct zip_t *zip, const char *entryname);
/**
* Opens an entry by name in the zip archive.
*
* For zip archive opened in 'w' or 'a' mode the function will append
* a new entry. In readonly mode the function tries to locate the entry
* in global dictionary (case sensitive).
*
* @param zip zip archive handler.
* @param entryname an entry name in local dictionary (case sensitive).
*
* @return the return code - 0 on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT int zip_entry_opencasesensitive(struct zip_t *zip,
const char *entryname);
/**
* Opens a new entry by index in the zip archive.
*
* This function is only valid if zip archive was opened in 'r' (readonly) mode.
*
* @param zip zip archive handler.
* @param index index in local dictionary.
*
* @return the return code - 0 on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT int zip_entry_openbyindex(struct zip_t *zip, size_t index);
/**
* Closes a zip entry, flushes buffer and releases resources.
*
* @param zip zip archive handler.
*
* @return the return code - 0 on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT int zip_entry_close(struct zip_t *zip);
/**
* Returns a local name of the current zip entry.
*
* The main difference between user's entry name and local entry name
* is optional relative path.
* Following .ZIP File Format Specification - the path stored MUST not contain
* a drive or device letter, or a leading slash.
* All slashes MUST be forward slashes '/' as opposed to backwards slashes '\'
* for compatibility with Amiga and UNIX file systems etc.
*
* @param zip: zip archive handler.
*
* @return the pointer to the current zip entry name, or NULL on error.
*/
extern ZIP_EXPORT const char *zip_entry_name(struct zip_t *zip);
/**
* Returns an index of the current zip entry.
*
* @param zip zip archive handler.
*
* @return the index on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT ssize_t zip_entry_index(struct zip_t *zip);
/**
* Determines if the current zip entry is a directory entry.
*
* @param zip zip archive handler.
*
* @return the return code - 1 (true), 0 (false), negative number (< 0) on
* error.
*/
extern ZIP_EXPORT int zip_entry_isdir(struct zip_t *zip);
/**
* Returns the uncompressed size of the current zip entry.
* Alias for zip_entry_uncomp_size (for backward compatibility).
*
* @param zip zip archive handler.
*
* @return the uncompressed size in bytes.
*/
extern ZIP_EXPORT unsigned long long zip_entry_size(struct zip_t *zip);
/**
* Returns the uncompressed size of the current zip entry.
*
* @param zip zip archive handler.
*
* @return the uncompressed size in bytes.
*/
extern ZIP_EXPORT unsigned long long zip_entry_uncomp_size(struct zip_t *zip);
/**
* Returns the compressed size of the current zip entry.
*
* @param zip zip archive handler.
*
* @return the compressed size in bytes.
*/
extern ZIP_EXPORT unsigned long long zip_entry_comp_size(struct zip_t *zip);
/**
* Returns CRC-32 checksum of the current zip entry.
*
* @param zip zip archive handler.
*
* @return the CRC-32 checksum.
*/
extern ZIP_EXPORT unsigned int zip_entry_crc32(struct zip_t *zip);
/**
* Compresses an input buffer for the current zip entry.
*
* @param zip zip archive handler.
* @param buf input buffer.
* @param bufsize input buffer size (in bytes).
*
* @return the return code - 0 on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT int zip_entry_write(struct zip_t *zip, const void *buf,
size_t bufsize);
/**
* Compresses a file for the current zip entry.
*
* @param zip zip archive handler.
* @param filename input file.
*
* @return the return code - 0 on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT int zip_entry_fwrite(struct zip_t *zip, const char *filename);
/**
* Extracts the current zip entry into output buffer.
*
* The function allocates sufficient memory for a output buffer.
*
* @param zip zip archive handler.
* @param buf output buffer.
* @param bufsize output buffer size (in bytes).
*
* @note remember to release memory allocated for a output buffer.
* for large entries, please take a look at zip_entry_extract function.
*
* @return the return code - the number of bytes actually read on success.
* Otherwise a negative number (< 0) on error.
*/
extern ZIP_EXPORT ssize_t zip_entry_read(struct zip_t *zip, void **buf,
size_t *bufsize);
/**
* Extracts the current zip entry into a memory buffer using no memory
* allocation.
*
* @param zip zip archive handler.
* @param buf preallocated output buffer.
* @param bufsize output buffer size (in bytes).
*
* @note ensure supplied output buffer is large enough.
* zip_entry_size function (returns uncompressed size for the current
* entry) can be handy to estimate how big buffer is needed.
* For large entries, please take a look at zip_entry_extract function.
*
* @return the return code - the number of bytes actually read on success.
* Otherwise a negative number (< 0) on error (e.g. bufsize is not large
* enough).
*/
extern ZIP_EXPORT ssize_t zip_entry_noallocread(struct zip_t *zip, void *buf,
size_t bufsize);
/**
* Extracts the current zip entry into output file.
*
* @param zip zip archive handler.
* @param filename output file.
*
* @return the return code - 0 on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT int zip_entry_fread(struct zip_t *zip, const char *filename);
/**
* Extracts the current zip entry using a callback function (on_extract).
*
* @param zip zip archive handler.
* @param on_extract callback function.
* @param arg opaque pointer (optional argument, which you can pass to the
* on_extract callback)
*
* @return the return code - 0 on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT int
zip_entry_extract(struct zip_t *zip,
size_t (*on_extract)(void *arg, uint64_t offset,
const void *data, size_t size),
void *arg);
/**
* Returns the number of all entries (files and directories) in the zip archive.
*
* @param zip zip archive handler.
*
* @return the return code - the number of entries on success, negative number
* (< 0) on error.
*/
extern ZIP_EXPORT ssize_t zip_entries_total(struct zip_t *zip);
/**
* Deletes zip archive entries.
*
* @param zip zip archive handler.
* @param entries array of zip archive entries to be deleted.
* @param len the number of entries to be deleted.
* @return the number of deleted entries, or negative number (< 0) on error.
*/
extern ZIP_EXPORT ssize_t zip_entries_delete(struct zip_t *zip,
char *const entries[], size_t len);
/**
* Extracts a zip archive stream into directory.
*
* If on_extract is not NULL, the callback will be called after
* successfully extracted each zip entry.
* Returning a negative value from the callback will cause abort and return an
* error. The last argument (void *arg) is optional, which you can use to pass
* data to the on_extract callback.
*
* @param stream zip archive stream.
* @param size stream size.
* @param dir output directory.
* @param on_extract on extract callback.
* @param arg opaque pointer.
*
* @return the return code - 0 on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT int
zip_stream_extract(const char *stream, size_t size, const char *dir,
int (*on_extract)(const char *filename, void *arg),
void *arg);
/**
* Opens zip archive stream into memory.
*
* @param stream zip archive stream.
* @param size stream size.
*
* @return the zip archive handler or NULL on error
*/
extern ZIP_EXPORT struct zip_t *zip_stream_open(const char *stream, size_t size,
int level, char mode);
/**
* Copy zip archive stream output buffer.
*
* @param zip zip archive handler.
* @param buf output buffer. User should free buf.
* @param bufsize output buffer size (in bytes).
*
* @return copy size
*/
extern ZIP_EXPORT ssize_t zip_stream_copy(struct zip_t *zip, void **buf,
size_t *bufsize);
/**
* Close zip archive releases resources.
*
* @param zip zip archive handler.
*
* @return
*/
extern ZIP_EXPORT void zip_stream_close(struct zip_t *zip);
/**
* Creates a new archive and puts files into a single zip archive.
*
* @param zipname zip archive file.
* @param filenames input files.
* @param len: number of input files.
*
* @return the return code - 0 on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT int zip_create(const char *zipname, const char *filenames[],
size_t len);
/**
* Extracts a zip archive file into directory.
*
* If on_extract_entry is not NULL, the callback will be called after
* successfully extracted each zip entry.
* Returning a negative value from the callback will cause abort and return an
* error. The last argument (void *arg) is optional, which you can use to pass
* data to the on_extract_entry callback.
*
* @param zipname zip archive file.
* @param dir output directory.
* @param on_extract_entry on extract callback.
* @param arg opaque pointer.
*
* @return the return code - 0 on success, negative number (< 0) on error.
*/
extern ZIP_EXPORT int zip_extract(const char *zipname, const char *dir,
int (*on_extract_entry)(const char *filename,
void *arg),
void *arg);
/** @} */
#ifdef __cplusplus
}
#endif
#endif